swisspost-evoting-go-poc/pkg/mixnet/commitment.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

121 lines
3.4 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 mixnet
import (
"fmt"
"math/big"
"github.com/user/evote/pkg/hash"
emath "github.com/user/evote/pkg/math"
"github.com/user/evote/pkg/trace"
)
var oneBI = big.NewInt(1)
// CommitmentKey represents a Pedersen commitment key (h, g_1, ..., g_v).
type CommitmentKey struct {
H emath.GqElement // First element h
G *emath.GqVector // Elements g_1, ..., g_v
}
// Size returns v (the number of g elements, not counting h).
func (ck CommitmentKey) Size() int {
return ck.G.Size()
}
// Group returns the group of this commitment key.
func (ck CommitmentKey) Group() *emath.GqGroup {
return ck.H.Group()
}
// GenCommitmentKey generates a verifiable Pedersen commitment key.
// Uses hash-based generation: w = (hash(q, "commitmentKey", i, count) + 1)^2 mod p
func GenCommitmentKey(numElements int, group *emath.GqGroup) CommitmentKey {
q := group.Q()
p := group.P()
g := group.Generator()
var values []emath.GqElement
count := 0
i := 0
for count <= numElements {
// u = RecursiveHashToZq(q, "commitmentKey", q, i, count) + 1
u := hash.RecursiveHashToZq(q,
hash.HashableString{Value: "commitmentKey"},
hash.HashableBigInt{Value: q},
hash.HashableBigInt{Value: big.NewInt(int64(i))},
hash.HashableBigInt{Value: big.NewInt(int64(count))},
)
uPlusOne := new(big.Int).Add(u, oneBI)
// w = uPlusOne^2 mod p
w := new(big.Int).Exp(uPlusOne, big.NewInt(2), p)
// Check w != 1 AND w != g AND w not already in values
wIsOne := w.Cmp(oneBI) == 0
wIsG := w.Cmp(g.Value()) == 0
wInValues := false
for _, v := range values {
if v.Value().Cmp(w) == 0 {
wInValues = true
break
}
}
if !wIsOne && !wIsG && !wInValues {
elem, _ := emath.NewGqElement(w, group)
values = append(values, elem)
count++
}
i++
}
// h = values[0], g = values[1..numElements]
gElems := make([]emath.GqElement, numElements)
copy(gElems, values[1:numElements+1])
return CommitmentKey{
H: values[0],
G: emath.GqVectorOf(gElems...),
}
}
// Commit computes a Pedersen commitment: C = h^r * Π(g_i^a_i)
func (ck CommitmentKey) Commit(a *emath.ZqVector, r emath.ZqElement) emath.GqElement {
if a.Size() > ck.Size() {
panic("vector size must not exceed commitment key size")
}
// h^r
result := ck.H.Exponentiate(r)
// Π(g_i^a_i)
for i := 0; i < a.Size(); i++ {
result = result.Multiply(ck.G.Get(i).Exponentiate(a.Get(i)))
}
return result
}
// CommitMatrix computes commitments to each column of a matrix.
func (ck CommitmentKey) CommitMatrix(A *emath.ZqMatrix, r *emath.ZqVector) *emath.GqVector {
if A.NumCols() != r.Size() {
panic("number of columns must match randomness vector size")
}
commitments := make([]emath.GqElement, A.NumCols())
for j := 0; j < A.NumCols(); j++ {
commitments[j] = ck.Commit(A.GetColumn(j), r.Get(j))
}
result := emath.GqVectorOf(commitments...)
trace.EmitFunc(func() trace.Event {
return trace.Event{
Kind: trace.KindCommit,
Caption: fmt.Sprintf("Pedersen commitment to a %d×%d matrix (one per column)", A.NumRows(), A.NumCols()),
LaTeX: `\mathbf{c}_A = \mathrm{Comm}_{ck}(A;\, \mathbf{r}), \quad c_{A,j} = h^{r_j} \textstyle\prod_{i=1}^{n} g_i^{A_{ij}}`,
ASCII: "c_A,j = h^r_j · Π_i g_i^{A_ij} (Pedersen, per column)",
Values: map[string]string{
"rows": fmt.Sprintf("%d", A.NumRows()),
"cols": fmt.Sprintf("%d", A.NumCols()),
"c_A0": result.Get(0).Value().String(),
},
}
})
return result
}