swisspost-evoting-go-poc/pkg/party/e2e_test.go
saymrwulf 23723fddd6 Tally + verification: full multi-party ceremony runs end-to-end
The mix-net now runs across separate parties over the signed transport: the
server pads the ballot box and hands it to CC0; each CC shuffles + partially
decrypts and passes the (validated) ciphertexts to the next; the electoral
board performs the final shuffle + decryption. Ciphertext handoffs cross the
authenticated transport; each party posts its shuffle and decryption proofs to
the public transcript (the bulletin board).

- tally.go: RunTally orchestration + per-party handlers (server pad, CC shuffle,
  EB final decrypt). Persists the padded mix input and per-stage partial
  decrypts to the transcript (fixes F7/F8 in the multi-party setting).
- verify.go: RunVerify has the verifier independently re-check every CC Schnorr
  proof and the whole shuffle chain from the transcript alone (no secrets).
- returncodes: DecodeVoteChecked returns an error instead of panicking on a
  non-smooth plaintext (fixes F12), used on the tally path so a corrupt ballot
  is counted as spoiled rather than crashing the tally.

Tests: the full ceremony (setup -> cards -> voting -> tally -> verify) produces
the correct tally over 124 verified transport messages; the verifier rejects a
transcript with swapped Schnorr proofs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:23:56 +02:00

86 lines
2.3 KiB
Go

package party
import (
"testing"
)
// TestFullMultiPartyCeremony runs the entire election over the transport:
// distributed setup, confidential card distribution, ballot submission with CC
// verification, the CC->CC->EB mix-net, and independent verification — then
// checks the decoded tally matches the cast votes and that every phase's
// messages were carried as verified envelopes.
func TestFullMultiPartyCeremony(t *testing.T) {
cfg := testConfig(t, 5, 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)
}
// Votes: options 0,1,1,2,1 -> {0:1, 1:3, 2:1}.
selections := [][]int{{0}, {1}, {1}, {2}, {1}}
if err := c.RunVoting(selections); 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)
}
want := map[int]int{0: 1, 1: 3, 2: 1}
got := c.Result()
for opt, w := range want {
if got[opt] != w {
t.Errorf("option %d: got %d, want %d", opt, got[opt], w)
}
}
if len(got) != len(want) {
t.Errorf("result has %d options, want %d (%v)", len(got), len(want), got)
}
if c.Bus.Count() == 0 {
t.Fatal("no messages were carried over the transport")
}
t.Logf("ceremony carried %d verified transport messages", c.Bus.Count())
}
// TestVerifyDetectsTamperedTranscript confirms the verifier rejects a transcript
// whose shuffle output was altered after the fact.
func TestVerifyDetectsTamperedTranscript(t *testing.T) {
cfg := testConfig(t, 3, 3)
c, err := NewCeremony(cfg, nil)
if err != nil {
t.Fatalf("NewCeremony: %v", err)
}
mustRun(t, c)
// Corrupt the first CC's Schnorr proof set in the transcript.
c.Transcript.CCSchnorr[0] = c.Transcript.CCSchnorr[1]
if err := c.RunVerify(); err == nil {
t.Fatal("verifier accepted a transcript with swapped Schnorr proofs")
}
}
func mustRun(t *testing.T, c *Ceremony) {
t.Helper()
if err := c.RunSetup(); err != nil {
t.Fatal(err)
}
if err := c.RunCards(); err != nil {
t.Fatal(err)
}
if err := c.RunVoting([][]int{{0}, {1}, {2}}); err != nil {
t.Fatal(err)
}
if err := c.RunTally(); err != nil {
t.Fatal(err)
}
}