Files
zeekay 07ffec05a7 consensus: close BLOCKERS CR-12 + CR-13 (cross-policy replay + sampling bias)
CR-12 — bind effectivePolicy byte into ComputeRoundDigest transcript
  protocol/quasar/round_digest.go: new uint8 parameter, refused if zero,
  bound as a TupleHash part between verifierBytes and netBytes.
  Closes cross-policy replay: an adversary can no longer sign a Round R
  as PolicyPQ and strip the Q witness on retransmit, presenting the
  bare P witness as a PolicyQuorum cert under an identical digest.
  Tests updated: canonical inputs include effectivePolicy=1, FixedHash-
  Family mirror parts include the policy byte at the matching position.

CR-13 — rejection sampling closes modulo bias in three samplers
  protocol/photon/emitter.go cryptoRandInt
  protocol/prism/cut.go cryptoRandInt
  protocol/prism/stake_weighted_cut.go cryptoRandUint64
  Prior `% max` form was structurally biased for non-power-of-2 max;
  under nation-state grinding model (Pinkas-Reiter style) the bias was
  an exploit on committee sampling. Rejection sampling computes the
  largest multiple of max that fits in uint64 and rejects values above
  it. Expected iterations ~1-2; never more than 2 in expectation.

Tests pass on protocol/quasar, protocol/photon, protocol/prism.
BLOCKERS.md CR-12 and CR-13 marked closed.

Two of 13 critical findings down. Remaining 11 are in progress.
2026-05-11 22:54:56 -07:00

102 lines
2.8 KiB
Go
Raw Permalink 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 photon
import (
"crypto/rand"
"encoding/binary"
"github.com/luxfi/consensus/core/types"
)
// Emitter emits consensus messages
type Emitter interface {
// Emit emits a message to selected nodes
Emit(msg interface{}) ([]types.NodeID, error)
// EmitTo emits a message to specific nodes
EmitTo(nodes []types.NodeID, msg interface{}) error
}
// DefaultEmitterOptions returns default emitter options
func DefaultEmitterOptions() EmitterOptions {
return EmitterOptions{
K: 20,
Fanout: 4,
Timeout: 1000,
}
}
// EmitterOptions defines emitter options
type EmitterOptions struct {
K int // Committee size
Fanout int // Number of nodes to emit to
Timeout int // Timeout in milliseconds
}
// UniformEmitter implements uniform random emission
type UniformEmitter struct {
nodes []types.NodeID
options EmitterOptions
}
// NewUniformEmitter creates a new uniform emitter
func NewUniformEmitter(nodes []types.NodeID, options EmitterOptions) *UniformEmitter {
return &UniformEmitter{
nodes: nodes,
options: options,
}
}
// Emit selects a uniform random subset of nodes using Fisher-Yates shuffle
// with crypto/rand (same algorithm as prism.UniformCut.Sample).
func (e *UniformEmitter) Emit(msg interface{}) ([]types.NodeID, error) {
n := len(e.nodes)
k := e.options.Fanout
if k >= n {
return e.nodes, nil
}
// Shuffle a copy so we don't mutate the original slice order.
shuffled := make([]types.NodeID, n)
copy(shuffled, e.nodes)
for i := 0; i < k; i++ {
j := i + cryptoRandInt(n-i)
shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
}
return shuffled[:k], nil
}
// cryptoRandInt returns a cryptographically secure random integer in
// [0, max) — uniformly distributed. Closes BLOCKERS.md CR-13: prior
// implementation used `binary.LittleEndian.Uint64(buf[:]) % uint64(max)`
// which introduces modulo bias for non-power-of-2 max. Under the
// nation-state grinding threat model, that bias was a structural
// exploit on committee sampling (Pinkas-Reiter style).
//
// This implementation uses rejection sampling: read 8 bytes, reject
// values in the high partial bucket, retry until the sample falls in
// the perfectly-uniform range. Expected loops ~1.0–2.0 (never more
// than 2.0 in expectation for any positive max).
func cryptoRandInt(max int) int {
if max <= 0 {
return 0
}
// Bound to refuse: largest multiple of max <= 2^64. Values above
// this are biased and must be rejected.
limit := (^uint64(0) / uint64(max)) * uint64(max)
var buf [8]byte
for {
_, _ = rand.Read(buf[:])
v := binary.LittleEndian.Uint64(buf[:])
if v < limit {
return int(v % uint64(max))
}
// Bias-region hit; resample.
}
}
// EmitTo emits a message to specific nodes
func (e *UniformEmitter) EmitTo(nodes []types.NodeID, msg interface{}) error {
return nil
}