mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-09-05 20:30:43 +00:00
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>
116 lines
3.5 KiB
Go
116 lines
3.5 KiB
Go
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))
|
||
}
|