mirror of
https://github.com/saymrwulf/swisspost-evoting-go-poc.git
synced 2026-07-23 19:32:29 +00:00
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>
This commit is contained in:
parent
cad6e112e3
commit
a5b9ce8ea3
3 changed files with 263 additions and 0 deletions
64
pkg/trace/sink.go
Normal file
64
pkg/trace/sink.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package trace
|
||||
|
||||
import "sync"
|
||||
|
||||
// Short elides a long value for compact display: keeps the first and last few
|
||||
// characters. Renderers that want the full value use the raw Values map.
|
||||
func Short(s string) string {
|
||||
const head, tail = 8, 6
|
||||
if len(s) <= head+tail+1 {
|
||||
return s
|
||||
}
|
||||
return s[:head] + "…" + s[len(s)-tail:]
|
||||
}
|
||||
|
||||
// ChanSink forwards events to a buffered channel, dropping if the consumer is
|
||||
// too slow (tracing must never stall the ceremony). Dropped counts are tracked.
|
||||
type ChanSink struct {
|
||||
C chan Event
|
||||
dropped uint64
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewChanSink creates a ChanSink with the given buffer size.
|
||||
func NewChanSink(buffer int) *ChanSink {
|
||||
return &ChanSink{C: make(chan Event, buffer)}
|
||||
}
|
||||
|
||||
func (s *ChanSink) Handle(e Event) {
|
||||
select {
|
||||
case s.C <- e:
|
||||
default:
|
||||
s.mu.Lock()
|
||||
s.dropped++
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Dropped returns how many events were dropped due to a full buffer.
|
||||
func (s *ChanSink) Dropped() uint64 {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.dropped
|
||||
}
|
||||
|
||||
// SliceSink collects events in memory (used in tests).
|
||||
type SliceSink struct {
|
||||
mu sync.Mutex
|
||||
Events []Event
|
||||
}
|
||||
|
||||
func (s *SliceSink) Handle(e Event) {
|
||||
s.mu.Lock()
|
||||
s.Events = append(s.Events, e)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Snapshot returns a copy of the collected events.
|
||||
func (s *SliceSink) Snapshot() []Event {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
out := make([]Event, len(s.Events))
|
||||
copy(out, s.Events)
|
||||
return out
|
||||
}
|
||||
140
pkg/trace/trace.go
Normal file
140
pkg/trace/trace.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
// 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())
|
||||
}
|
||||
59
pkg/trace/trace_test.go
Normal file
59
pkg/trace/trace_test.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package trace
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDisabledByDefaultIsCheap(t *testing.T) {
|
||||
if Enabled() {
|
||||
t.Fatal("tracing should be off with no sinks")
|
||||
}
|
||||
called := false
|
||||
EmitFunc(func() Event { called = true; return Event{} })
|
||||
if called {
|
||||
t.Fatal("EmitFunc built an event while tracing was disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscribeReceivesEvents(t *testing.T) {
|
||||
sink := &SliceSink{}
|
||||
unsub := Subscribe(sink)
|
||||
defer unsub()
|
||||
|
||||
SetContext("control-component-0", "tally")
|
||||
Emit(Event{
|
||||
Kind: KindChallenge,
|
||||
Caption: "Fiat-Shamir challenge",
|
||||
LaTeX: `e = \mathcal{H}(g, y, c) \bmod q`,
|
||||
Values: map[string]string{"e": "12345678901234567890"},
|
||||
})
|
||||
Emit(Event{Party: "voter-0001", Kind: KindEncrypt, Caption: "encrypt ballot"})
|
||||
|
||||
got := sink.Snapshot()
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("got %d events, want 2", len(got))
|
||||
}
|
||||
if got[0].Seq == 0 || got[1].Seq <= got[0].Seq {
|
||||
t.Fatalf("sequence numbers not monotonic: %d, %d", got[0].Seq, got[1].Seq)
|
||||
}
|
||||
if got[0].Party != "control-component-0" || got[0].Phase != "tally" {
|
||||
t.Fatalf("context not stamped: %+v", got[0])
|
||||
}
|
||||
if got[1].Party != "voter-0001" {
|
||||
t.Fatalf("explicit party overridden: %+v", got[1])
|
||||
}
|
||||
|
||||
unsub()
|
||||
if Enabled() {
|
||||
t.Fatal("unsubscribe did not clear the last sink")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortElision(t *testing.T) {
|
||||
if Short("12345") != "12345" {
|
||||
t.Fatal("short values must pass through")
|
||||
}
|
||||
long := "1234567890123456789012345"
|
||||
e := Short(long)
|
||||
if e == long || len(e) >= len(long) {
|
||||
t.Fatalf("long value not elided: %q", e)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue