swisspost-evoting-go-poc/pkg/party/trace_test.go
saymrwulf c24650885e Instrument the deep Bayer-Groth layers: commitments, sub-arguments, decryption
Take the live-math cockpit down to the level of the Swiss Post crypto-primitives
class structure. The shuffle proof is no longer one line — you can now watch it
being constructed:

- Pedersen matrix commitment (CommitmentService analog): c_A = Comm(A; r),
  c_{A,j} = h^{r_j} Π g_i^{A_ij}, emitted from CommitMatrix.
- All five Bayer-Groth sub-arguments, mirroring the *ArgumentService classes:
  ShuffleArgument (composition + x,y,z challenges), ProductArgument,
  HadamardArgument (entrywise product), ZeroArgument (bilinear star-map),
  SingleValueProductArgument, MultiExponentiationArgument — each emits its
  defining relation as LaTeX with live dimensions.
- Partial decryption + decryption proof (DecryptionProofService analog):
  φ'_i = φ_i·γ_i^{-sk} with the ZK proof that log_g(pk) = log_γ(γ^sk).

New trace.KindArgument. Low-level Commit stays uninstrumented (called in
verification too — would flood the stream); CommitMatrix is the semantic step.

Test: a 6-voter ceremony (N=6 → 2×3 shuffle matrix, so m>1 and the full argument
tree runs) captures 345 live events across 8 kinds, and asserts all five named
sub-arguments appear.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 16:26:03 +02:00

116 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package party
import (
"testing"
"github.com/user/evote/pkg/trace"
)
// TestCeremonyEmitsLiveCryptoEvents runs the full ceremony with a trace sink
// attached and confirms that the real cryptographic operations emit events
// carrying live runtime values — the foundation of the "watch the math execute"
// cockpit. It checks that each headline operation kind appears with non-empty
// values and correct LaTeX.
func TestCeremonyEmitsLiveCryptoEvents(t *testing.T) {
sink := &trace.SliceSink{}
unsub := trace.Subscribe(sink)
defer unsub()
// 6 voters → N=6 ballots → the shuffle matrix is 2×3 (m>1), so the full
// Bayer-Groth argument tree runs (Hadamard/Zero only fire when m>1).
cfg := testConfig(t, 6, 3)
c, err := NewCeremony(cfg, nil)
if err != nil {
t.Fatalf("NewCeremony: %v", err)
}
if err := c.RunSetup(); err != nil {
t.Fatalf("RunSetup: %v", err)
}
if err := c.RunCards(); err != nil {
t.Fatalf("RunCards: %v", err)
}
if err := c.RunVoting([][]int{{0}, {1}, {2}, {0}, {1}, {2}}); err != nil {
t.Fatalf("RunVoting: %v", err)
}
if err := c.RunTally(); err != nil {
t.Fatalf("RunTally: %v", err)
}
if err := c.RunVerify(); err != nil {
t.Fatalf("RunVerify: %v", err)
}
events := sink.Snapshot()
if len(events) == 0 {
t.Fatal("no trace events emitted during a full ceremony")
}
seen := map[trace.Kind]trace.Event{}
for _, e := range events {
seen[e.Kind] = e
}
// Every headline operation must have fired at least once — including the
// deep Bayer-Groth layers (commitment, sub-arguments, partial decryption)
// that mirror the Swiss Post crypto-primitives class structure.
for _, k := range []trace.Kind{
trace.KindSign, // Ed25519 transport signatures
trace.KindKeyEx, // X25519 ECDH (card delivery)
trace.KindEncrypt, // ballot encryption
trace.KindChallenge, // Fiat-Shamir challenge
trace.KindShuffle, // Bayer-Groth mix-net (top level)
trace.KindCommit, // Pedersen matrix commitment
trace.KindArgument, // a Bayer-Groth sub-argument
trace.KindDecrypt, // CC partial decryption + proof
} {
e, ok := seen[k]
if !ok {
t.Errorf("no %q event emitted", k)
continue
}
if e.LaTeX == "" {
t.Errorf("%q event has empty LaTeX", k)
}
if len(e.Values) == 0 {
t.Errorf("%q event carries no live values", k)
}
}
// All five Bayer-Groth sub-arguments must appear (matching the Swiss Post
// *ArgumentService classes). We identify them by a keyword in the caption.
argCaptions := map[string]bool{}
for _, e := range events {
if e.Kind == trace.KindArgument {
argCaptions[e.Caption] = true
}
}
for _, want := range []string{"Shuffle argument", "Product argument", "Hadamard argument",
"Zero argument", "Single-value product argument", "Multi-exponentiation argument"} {
found := false
for cap := range argCaptions {
if len(cap) >= len(want) && cap[:len(want)] == want {
found = true
break
}
}
if !found {
t.Errorf("sub-argument %q never appeared in the trace", want)
}
}
// Spot-check that the shuffle event reports the padded ballot count (N>=3).
if sh, ok := seen[trace.KindShuffle]; ok {
if sh.Values["N"] == "" {
t.Error("shuffle event missing N")
}
}
// Events must be tagged with a phase, and signatures with the acting party.
if seen[trace.KindSign].Party == "" {
t.Error("signature event not attributed to a party")
}
if seen[trace.KindShuffle].Phase != "tally" {
t.Errorf("shuffle phase = %q, want tally", seen[trace.KindShuffle].Phase)
}
t.Logf("captured %d live crypto events across %d kinds", len(events), len(seen))
}