swisspost-evoting-go-poc/pkg/trace/trace.go
saymrwulf a5b9ce8ea3 Add pkg/trace: surface-agnostic live crypto-event stream
The foundation for the "watch the mathematics execute" cockpit. Each meaningful
crypto operation will emit one Event carrying its LaTeX notation plus the real
runtime values, tagged with party + phase. One stream feeds both a terminal view
(ASCII/Unicode) and a browser view (typeset KaTeX) — no double-maintenance.

- Off by default with near-zero cost: Emit/EmitFunc return immediately when no
  sink is attached (atomic fast path), so instrumentation is free in normal runs.
- Sinks: ChanSink (buffered, drops rather than stalling the ceremony), SliceSink
  (tests). Short() elides ~77-digit values for compact display.

Tests: disabled-is-cheap, monotonic sequence + context stamping, elision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:59:52 +02:00

140 lines
4.5 KiB
Go

// Package trace is a live event stream for the cryptographic operations the
// system performs. Each meaningful operation (sampling, encryption, commitment,
// Fiat-Shamir challenge, shuffle, signature, key agreement) emits one Event
// carrying its notation as a LaTeX template plus the REAL runtime values, so a
// renderer can show the mathematics that is executing at the instant it runs.
//
// The stream is surface-agnostic: a terminal view renders ASCII/Unicode, a
// browser view renders typeset LaTeX (KaTeX) — both consume the same events.
//
// Tracing is off by default and has near-zero cost when no sink is attached:
// Emit returns immediately if there are no subscribers. Instrumentation sites
// wrap value formatting in a closure (via EmitFunc) so that formatting work is
// skipped entirely when tracing is off.
package trace
import (
"sync"
"sync/atomic"
)
// Kind categorizes an operation so a renderer can group or icon it.
type Kind string
const (
KindSample Kind = "sample" // random sampling from Z_q / permutations
KindEncrypt Kind = "encrypt" // ElGamal encryption
KindDecrypt Kind = "decrypt" // (partial) decryption
KindExp Kind = "exp" // group exponentiation of note
KindCommit Kind = "commit" // Pedersen commitment
KindChallenge Kind = "challenge" // Fiat-Shamir challenge derivation
KindProof Kind = "proof" // ZK proof generation / verification
KindShuffle Kind = "shuffle" // mix-net permutation + re-encryption
KindSign Kind = "sign" // Ed25519 signature (transport)
KindVerify Kind = "verify" // signature / proof verification
KindKeyEx Kind = "keyex" // X25519 key agreement
KindNote Kind = "note" // phase/step narration, no math
)
// Event is one instrumented operation.
type Event struct {
Seq uint64 `json:"seq"` // monotonic sequence number
Party string `json:"party"` // which stakeholder performed it
Phase string `json:"phase"` // setup / voting / tally / verify
Kind Kind `json:"kind"` // operation category
Caption string `json:"caption"` // one-line human description
LaTeX string `json:"latex"` // notation, with \VAL{name} placeholders
ASCII string `json:"ascii"` // terminal-friendly fallback (optional)
Values map[string]string `json:"values"` // placeholder name -> real runtime value (decimal/hex)
}
// Sink receives events. Implementations must be safe for concurrent use and must
// not block the emitter for long (buffer or drop internally if needed).
type Sink interface {
Handle(Event)
}
var (
mu sync.RWMutex
sinks []Sink
active atomic.Bool // fast path: true when at least one sink is attached
seqCtr atomic.Uint64
curParty atomic.Value // string: default party if an emit omits one
curPhase atomic.Value // string: current phase
)
// Subscribe attaches a sink and returns an unsubscribe function.
func Subscribe(s Sink) func() {
mu.Lock()
sinks = append(sinks, s)
active.Store(true)
mu.Unlock()
return func() {
mu.Lock()
defer mu.Unlock()
for i, x := range sinks {
if x == s {
sinks = append(sinks[:i], sinks[i+1:]...)
break
}
}
active.Store(len(sinks) > 0)
}
}
// Enabled reports whether any sink is attached. Instrumentation can check this
// to skip expensive value formatting.
func Enabled() bool { return active.Load() }
// SetContext sets the default party/phase stamped onto events that omit them.
func SetContext(party, phase string) {
curParty.Store(party)
curPhase.Store(phase)
}
// Phase sets just the current phase.
func Phase(phase string) { curPhase.Store(phase) }
func ctxParty() string {
if v, ok := curParty.Load().(string); ok {
return v
}
return ""
}
func ctxPhase() string {
if v, ok := curPhase.Load().(string); ok {
return v
}
return ""
}
// Emit publishes an event. It fills Seq, and Party/Phase from context if unset.
// Returns immediately when tracing is disabled.
func Emit(e Event) {
if !active.Load() {
return
}
e.Seq = seqCtr.Add(1)
if e.Party == "" {
e.Party = ctxParty()
}
if e.Phase == "" {
e.Phase = ctxPhase()
}
mu.RLock()
current := sinks
mu.RUnlock()
for _, s := range current {
s.Handle(e)
}
}
// EmitFunc builds and emits an event only when tracing is enabled, so callers
// can defer all value formatting into build. Use this at hot instrumentation
// sites where formatting big integers would otherwise cost even when tracing off.
func EmitFunc(build func() Event) {
if !active.Load() {
return
}
Emit(build())
}