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

97 lines
2.6 KiB
Go

package prism
import (
"crypto/rand"
"encoding/binary"
"github.com/luxfi/consensus/core/types"
)
// Cut provides random cutting of peers for consensus voting (like a prism cuts light)
type Cut[T comparable] interface {
// Sample returns k random peers for voting (cuts k rays from the population)
Sample(k int) []types.NodeID
// Luminance returns light intensity metrics for the cut
Luminance() Luminance
}
// Luminance measures the intensity of light across the peer network
// Following SI units: lux (lx) = lumens per square meter
type Luminance struct {
ActivePeers int
TotalPeers int
Lx float64 // Illuminance in lux (lx) - minimum 1 lx per active peer/photon
}
// UniformCut implements uniform random cutting
type UniformCut struct {
peers []types.NodeID
}
// NewUniformCut creates a new uniform cut
func NewUniformCut(peers []types.NodeID) *UniformCut {
return &UniformCut{peers: peers}
}
// Sample implements Cut interface using Fisher-Yates shuffle with crypto/rand.
// This provides uniform random sampling of k peers from the population.
func (c *UniformCut) Sample(k int) []types.NodeID {
n := len(c.peers)
if k >= n {
return c.peers
}
// Create a copy to shuffle (don't modify original)
shuffled := make([]types.NodeID, n)
copy(shuffled, c.peers)
// Fisher-Yates shuffle with cryptographic randomness
// Only need to shuffle first k elements
for i := 0; i < k; i++ {
// Generate random index j where i <= j < n
j := i + cryptoRandInt(n-i)
shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
}
return shuffled[:k]
}
// cryptoRandInt returns a cryptographically secure random integer in
// [0, max) — uniformly distributed via rejection sampling. Closes
// BLOCKERS.md CR-13. See protocol/photon/emitter.go for the same
// rationale; both samplers were exploitable under nation-state
// committee-grinding.
func cryptoRandInt(max int) int {
if max <= 0 {
return 0
}
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))
}
}
}
// Luminance implements Cut interface
func (c *UniformCut) Luminance() Luminance {
activePeers := len(c.peers)
// Minimum 1 lx per active peer/photon, scaling with network health
lx := float64(activePeers) // Base: 1 lx per peer
if activePeers >= 100 {
lx = 500.0 // Office lighting level for healthy large networks
} else if activePeers >= 20 {
lx = 300.0 // Classroom level for medium networks
}
return Luminance{
ActivePeers: activePeers,
TotalPeers: activePeers,
Lx: lx,
}
}