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>
235 lines
8.2 KiB
Go
235 lines
8.2 KiB
Go
package party
|
||
|
||
import (
|
||
"fmt"
|
||
"math/big"
|
||
|
||
"github.com/user/evote/pkg/elgamal"
|
||
emath "github.com/user/evote/pkg/math"
|
||
"github.com/user/evote/pkg/mixnet"
|
||
"github.com/user/evote/pkg/returncodes"
|
||
"github.com/user/evote/pkg/trace"
|
||
"github.com/user/evote/pkg/transport"
|
||
"github.com/user/evote/pkg/zkp"
|
||
)
|
||
|
||
// Tally-phase message types.
|
||
const (
|
||
MsgStartTally = "start-tally" // ceremony -> server: produce padded mix input
|
||
MsgMixInput = "mix-input" // server -> ceremony: padded ciphertexts
|
||
MsgShuffle = "shuffle" // ceremony -> CCj: shuffle + partial decrypt
|
||
MsgShuffled = "shuffled" // CCj -> ceremony: partially decrypted ciphertexts
|
||
MsgFinalMix = "final-mix" // ceremony -> EB: final shuffle + decrypt
|
||
MsgFinalDone = "final-done" // EB -> ceremony
|
||
)
|
||
|
||
type mixInputPayload struct {
|
||
Cts wireCiphertextVector `json:"cts"`
|
||
}
|
||
|
||
type shuffleReq struct {
|
||
Stage int `json:"stage"` // CC index driving the remaining-key computation
|
||
Cts wireCiphertextVector `json:"cts"`
|
||
}
|
||
|
||
type shuffleResp struct {
|
||
Cts wireCiphertextVector `json:"cts"` // partially decrypted ciphertexts
|
||
}
|
||
|
||
type finalResp struct {
|
||
Result map[int]int `json:"result"`
|
||
}
|
||
|
||
// RunTally drives the mix-net: the server pads the ballot box and hands it to
|
||
// CC0; each CC shuffles + partially decrypts and passes the result to the next;
|
||
// the electoral board performs the final shuffle + decryption. Ciphertext
|
||
// handoffs travel over the signed transport (validated on decode); each party
|
||
// posts its shuffle and decryption proofs to the public transcript (the
|
||
// bulletin board), which the verifier re-checks in RunVerify.
|
||
func (c *Ceremony) RunTally() error {
|
||
trace.Phase("tally")
|
||
c.logf("\n--- TALLY PHASE (multi-party) ---")
|
||
group := c.Config.Group
|
||
|
||
// 1. Ask the server for the padded mix input.
|
||
env, err := c.send(c.Verifier.id, NameServer, MsgStartTally, struct{}{})
|
||
if err != nil {
|
||
return fmt.Errorf("start tally: %w", err)
|
||
}
|
||
var mi mixInputPayload
|
||
if err := transport.UnmarshalPayload(env.Payload, &mi); err != nil {
|
||
return err
|
||
}
|
||
cts, err := mi.Cts.decode(group)
|
||
if err != nil {
|
||
return fmt.Errorf("mix input: %w", err)
|
||
}
|
||
c.Transcript.MixInput = cts
|
||
c.Transcript.Shuffles = nil
|
||
c.Transcript.PartialDecrypts = nil
|
||
c.Transcript.DecryptProofs = nil
|
||
c.logf(" tally: mixing %d ciphertexts through %d CCs + EB", cts.Size(), c.Config.NumCCs)
|
||
|
||
// 2. Chain through the control components.
|
||
for j := 0; j < c.Config.NumCCs; j++ {
|
||
env, err := c.send(c.Verifier.id, CCName(j), MsgShuffle, shuffleReq{
|
||
Stage: j, Cts: encodeCiphertextVector(cts),
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("cc%d shuffle: %w", j, err)
|
||
}
|
||
var resp shuffleResp
|
||
if err := transport.UnmarshalPayload(env.Payload, &resp); err != nil {
|
||
return err
|
||
}
|
||
cts, err = resp.Cts.decode(group)
|
||
if err != nil {
|
||
return fmt.Errorf("cc%d output: %w", j, err)
|
||
}
|
||
c.logf(" tally: cc%d shuffled + partially decrypted %d ciphertexts", j, cts.Size())
|
||
}
|
||
|
||
// 3. Electoral board: final shuffle + full decryption.
|
||
env, err = c.send(c.Verifier.id, NameEB, MsgFinalMix, shuffleReq{Cts: encodeCiphertextVector(cts)})
|
||
if err != nil {
|
||
return fmt.Errorf("eb final mix: %w", err)
|
||
}
|
||
var fr finalResp
|
||
if err := transport.UnmarshalPayload(env.Payload, &fr); err != nil {
|
||
return err
|
||
}
|
||
c.Transcript.Result = fr.Result
|
||
c.logf(" tally: electoral board decrypted final plaintexts; result posted to transcript")
|
||
return nil
|
||
}
|
||
|
||
// handleStartTally (server) pads the ballot box to N>=2 and returns the mix
|
||
// input, persisting it so the verifier can check shuffle 0 against it.
|
||
func (p *VotingServer) handleStartTally(env *transport.Envelope) (*transport.Envelope, error) {
|
||
group := p.cer.Config.Group
|
||
zq := emath.ZqGroupFromGqGroup(group)
|
||
|
||
cts := make([]elgamal.Ciphertext, len(p.st.ballotBox))
|
||
for i, b := range p.st.ballotBox {
|
||
cts[i] = b.Ciphertext
|
||
}
|
||
vec := elgamal.NewCiphertextVector(cts)
|
||
for vec.Size() < 2 {
|
||
trivial := elgamal.EncryptOnes(emath.RandomZqElement(zq), p.st.electionPK)
|
||
vec = vec.Append(trivial)
|
||
}
|
||
return reply(p.id, env.From, MsgMixInput, env.Nonce, mixInputPayload{Cts: encodeCiphertextVector(vec)})
|
||
}
|
||
|
||
// handleShuffle (CC) shuffles + partially decrypts, posts proofs to the
|
||
// transcript, and returns the partially decrypted ciphertexts.
|
||
func (p *ControlComponent) handleShuffle(env *transport.Envelope) (*transport.Envelope, error) {
|
||
var req shuffleReq
|
||
if err := transport.UnmarshalPayload(env.Payload, &req); err != nil {
|
||
return nil, err
|
||
}
|
||
cfg := p.cer.Config
|
||
group := cfg.Group
|
||
in, err := req.Cts.decode(group)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("cc%d shuffle input: %w", p.index, err)
|
||
}
|
||
|
||
// Remaining public key = CCs[stage..] + EB, read from the public transcript.
|
||
remaining := remainingPK(p.cer.Transcript, req.Stage, cfg.NumCCs)
|
||
|
||
trace.SetContext(p.id.Name, "tally")
|
||
vs := mixnet.GenVerifiableShuffle(in, remaining, group)
|
||
|
||
// Partial decrypt with this CC's private key; produce decryption proofs.
|
||
decrypted := make([]elgamal.Ciphertext, vs.ShuffledCiphertexts.Size())
|
||
decProofs := make([]zkp.DecryptionProof, vs.ShuffledCiphertexts.Size())
|
||
for i := 0; i < vs.ShuffledCiphertexts.Size(); i++ {
|
||
ct := vs.ShuffledCiphertexts.Get(i)
|
||
decrypted[i] = elgamal.PartialDecrypt(ct, p.st.keyPair.SK)
|
||
msg := elgamal.Decrypt(ct, p.st.keyPair.SK)
|
||
decProofs[i] = zkp.GenDecryptionProof(ct, p.st.keyPair.SK, p.st.keyPair.PK, msg, group)
|
||
}
|
||
out := elgamal.NewCiphertextVector(decrypted)
|
||
trace.EmitFunc(func() trace.Event {
|
||
return trace.Event{
|
||
Party: p.id.Name,
|
||
Kind: trace.KindDecrypt,
|
||
Caption: fmt.Sprintf("%s partially decrypts %d ciphertexts and proves correctness", p.id.Name, out.Size()),
|
||
LaTeX: `\phi_i' = \phi_i \cdot \gamma_i^{-sk}, \quad \text{proof: } \log_g pk = \log_{\gamma_i} (\gamma_i^{sk})`,
|
||
ASCII: "φ'_i = φ_i · γ_i^{-sk} + ZK proof log_g(pk) = log_γ(γ^sk)",
|
||
Values: map[string]string{
|
||
"count": fmt.Sprintf("%d", out.Size()),
|
||
"proof_e": decProofs[0].E.Value().String(),
|
||
},
|
||
}
|
||
})
|
||
|
||
// Post proofs to the transcript (bulletin board).
|
||
p.cer.Transcript.Shuffles = append(p.cer.Transcript.Shuffles, vs)
|
||
p.cer.Transcript.PartialDecrypts = append(p.cer.Transcript.PartialDecrypts, out)
|
||
p.cer.Transcript.DecryptProofs = append(p.cer.Transcript.DecryptProofs, decProofs)
|
||
|
||
return reply(p.id, env.From, MsgShuffled, env.Nonce, shuffleResp{Cts: encodeCiphertextVector(out)})
|
||
}
|
||
|
||
// handleFinalMix (EB) performs the final shuffle and full decryption, posts the
|
||
// shuffle to the transcript, decodes plaintexts, and returns the tally.
|
||
func (p *ElectoralBoard) handleFinalMix(env *transport.Envelope) (*transport.Envelope, error) {
|
||
var req shuffleReq
|
||
if err := transport.UnmarshalPayload(env.Payload, &req); err != nil {
|
||
return nil, err
|
||
}
|
||
cfg := p.cer.Config
|
||
group := cfg.Group
|
||
in, err := req.Cts.decode(group)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("eb final input: %w", err)
|
||
}
|
||
|
||
trace.SetContext(p.id.Name, "tally")
|
||
vs := mixnet.GenVerifiableShuffle(in, p.st.keyPair.PK, group)
|
||
p.cer.Transcript.Shuffles = append(p.cer.Transcript.Shuffles, vs)
|
||
|
||
// Full decryption and decode.
|
||
plaintexts := make([]*emath.GqVector, vs.ShuffledCiphertexts.Size())
|
||
result := make(map[int]int)
|
||
primes := transcriptPrimes(p.cer.Transcript)
|
||
for i := 0; i < vs.ShuffledCiphertexts.Size(); i++ {
|
||
msg := elgamal.Decrypt(vs.ShuffledCiphertexts.Get(i), p.st.keyPair.SK)
|
||
plaintexts[i] = msg.Elements
|
||
first := msg.Get(0)
|
||
if first.IsIdentity() {
|
||
continue // padding ciphertext
|
||
}
|
||
selected, err := returncodes.DecodeVoteChecked(first.Value(), primes)
|
||
if err != nil {
|
||
// A ballot that does not decode is counted as spoiled, not fatal.
|
||
continue
|
||
}
|
||
for _, opt := range selected {
|
||
result[opt]++
|
||
}
|
||
}
|
||
p.cer.Transcript.FinalPlaintexts = plaintexts
|
||
|
||
return reply(p.id, env.From, MsgFinalDone, env.Nonce, finalResp{Result: result})
|
||
}
|
||
|
||
// remainingPK combines CCs[stage..NumCCs) + EB public keys from the transcript.
|
||
func remainingPK(tr *PublicTranscript, stage, numCCs int) elgamal.PublicKey {
|
||
pks := make([]elgamal.PublicKey, 0, numCCs-stage+1)
|
||
for k := stage; k < numCCs; k++ {
|
||
pks = append(pks, tr.CCElectionPKs[k])
|
||
}
|
||
pks = append(pks, tr.EBPublicKey)
|
||
return elgamal.CombinePublicKeys(pks...)
|
||
}
|
||
|
||
func transcriptPrimes(tr *PublicTranscript) []*big.Int {
|
||
out := make([]*big.Int, len(tr.Primes))
|
||
for i, s := range tr.Primes {
|
||
out[i], _ = new(big.Int).SetString(s, 10)
|
||
}
|
||
return out
|
||
}
|