mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
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.
This commit is contained in:
@@ -66,14 +66,33 @@ func (e *UniformEmitter) Emit(msg interface{}) ([]types.NodeID, error) {
|
||||
return shuffled[:k], nil
|
||||
}
|
||||
|
||||
// cryptoRandInt returns a cryptographically secure random integer in [0, max).
|
||||
// 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
|
||||
_, _ = rand.Read(buf[:])
|
||||
return int(binary.LittleEndian.Uint64(buf[:]) % uint64(max))
|
||||
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
|
||||
|
||||
+13
-3
@@ -57,14 +57,24 @@ func (c *UniformCut) Sample(k int) []types.NodeID {
|
||||
return shuffled[:k]
|
||||
}
|
||||
|
||||
// cryptoRandInt returns a cryptographically secure random integer in [0, max).
|
||||
// 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
|
||||
_, _ = rand.Read(buf[:])
|
||||
return int(binary.LittleEndian.Uint64(buf[:]) % uint64(max))
|
||||
for {
|
||||
_, _ = rand.Read(buf[:])
|
||||
v := binary.LittleEndian.Uint64(buf[:])
|
||||
if v < limit {
|
||||
return int(v % uint64(max))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Luminance implements Cut interface
|
||||
|
||||
@@ -112,12 +112,21 @@ func (c *StakeWeightedCut) Luminance() Luminance {
|
||||
}
|
||||
}
|
||||
|
||||
// cryptoRandUint64 returns a cryptographically secure random uint64 in [0, max).
|
||||
// cryptoRandUint64 returns a cryptographically secure random uint64
|
||||
// in [0, max) — uniformly distributed via rejection sampling. Closes
|
||||
// BLOCKERS.md CR-13: the prior `% max` form was biased on non-power-of-2
|
||||
// stake totals, structurally exploitable by stake-weight grinding.
|
||||
func cryptoRandUint64(max uint64) uint64 {
|
||||
if max <= 1 {
|
||||
return 0
|
||||
}
|
||||
limit := (^uint64(0) / max) * max
|
||||
var buf [8]byte
|
||||
_, _ = rand.Read(buf[:])
|
||||
return binary.LittleEndian.Uint64(buf[:]) % max
|
||||
for {
|
||||
_, _ = rand.Read(buf[:])
|
||||
v := binary.LittleEndian.Uint64(buf[:])
|
||||
if v < limit {
|
||||
return v % max
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ type roundDigestInputs struct {
|
||||
proofBackend config.ProofBackendID
|
||||
proofFormat config.ProofFormatID
|
||||
verifierID config.VerifierID
|
||||
effectivePolicy uint8 // BLOCKERS.md CR-12
|
||||
networkID uint32
|
||||
chainID uint32
|
||||
epoch uint64
|
||||
@@ -57,6 +58,7 @@ func canonicalRoundDigest() roundDigestInputs {
|
||||
proofBackend: config.ProofBackendP3QSTARKFRISHA3,
|
||||
proofFormat: config.ProofFormatP3QBinaryV1,
|
||||
verifierID: config.VerifierP3QSTARKFRISHA3PQ,
|
||||
effectivePolicy: 1, // PolicyQuorum non-zero baseline
|
||||
networkID: 0xC0DE0002,
|
||||
chainID: 0xDEADBEEF,
|
||||
epoch: 0x1122334455667788,
|
||||
@@ -83,6 +85,7 @@ func (in roundDigestInputs) compute() (RoundDigest, error) {
|
||||
in.profileID,
|
||||
in.hashSuite, in.identityScheme, in.finalityScheme,
|
||||
in.proofPolicy, in.proofBackend, in.proofFormat, in.verifierID,
|
||||
in.effectivePolicy,
|
||||
in.networkID, in.chainID,
|
||||
in.epoch, in.height,
|
||||
in.roundOrView,
|
||||
|
||||
@@ -97,6 +97,7 @@ func ComputeRoundDigest(
|
||||
proofBackend config.ProofBackendID,
|
||||
proofFormat config.ProofFormatID,
|
||||
verifierID config.VerifierID,
|
||||
effectivePolicy uint8, // BLOCKERS.md CR-12: bind effective policy byte
|
||||
networkID, chainID uint32,
|
||||
epoch, height uint64,
|
||||
roundOrView uint32,
|
||||
@@ -128,6 +129,10 @@ func ComputeRoundDigest(
|
||||
return RoundDigest{}, fmt.Errorf("%w: proofFormat", ErrRoundDigestZeroField)
|
||||
case verifierID == config.VerifierNone:
|
||||
return RoundDigest{}, fmt.Errorf("%w: verifierID", ErrRoundDigestZeroField)
|
||||
case effectivePolicy == 0:
|
||||
// BLOCKERS.md CR-12: zero effective policy is the cross-policy
|
||||
// replay attack vector. Refuse explicitly.
|
||||
return RoundDigest{}, fmt.Errorf("%w: effectivePolicy", ErrRoundDigestZeroField)
|
||||
case networkID == 0:
|
||||
return RoundDigest{}, fmt.Errorf("%w: networkID", ErrRoundDigestZeroField)
|
||||
case chainID == 0:
|
||||
@@ -169,6 +174,13 @@ func ComputeRoundDigest(
|
||||
{byte(proofBackend)},
|
||||
{byte(proofFormat)},
|
||||
verifierBytes,
|
||||
// BLOCKERS.md CR-12 closure — bind effective policy byte into
|
||||
// the digest. Without this, an adversary could sign a Round R
|
||||
// as PolicyPQ (P+Q witnesses) and strip the Q witness on
|
||||
// retransmit, presenting the bare P witness as a PolicyQuorum
|
||||
// cert under an identical digest. The policy byte must be in
|
||||
// the transcript so the verifier-side policy floor enforces.
|
||||
{effectivePolicy},
|
||||
netBytes,
|
||||
chainBytes,
|
||||
epochBytes,
|
||||
|
||||
@@ -24,6 +24,7 @@ type canonicalRoundDigestInputs struct {
|
||||
proofBackend config.ProofBackendID
|
||||
proofFormat config.ProofFormatID
|
||||
verifierID config.VerifierID
|
||||
effectivePolicy uint8 // BLOCKERS.md CR-12: bound into digest
|
||||
networkID uint32
|
||||
chainID uint32
|
||||
epoch uint64
|
||||
@@ -62,6 +63,7 @@ func canonical() canonicalRoundDigestInputs {
|
||||
proofBackend: config.ProofBackendP3QSTARKFRISHA3,
|
||||
proofFormat: config.ProofFormatSTARKFRIBinaryV1,
|
||||
verifierID: config.VerifierP3QSTARKFRISHA3PQ,
|
||||
effectivePolicy: 1, // PolicyQuorum non-zero baseline
|
||||
networkID: 0xC0DE0001,
|
||||
chainID: 0xDEADBEEF,
|
||||
epoch: 0x1122334455667788,
|
||||
@@ -86,6 +88,7 @@ func compute(t *testing.T, in canonicalRoundDigestInputs) RoundDigest {
|
||||
in.profileID,
|
||||
in.hashSuite, in.identityScheme, in.finalityScheme,
|
||||
in.proofPolicy, in.proofBackend, in.proofFormat, in.verifierID,
|
||||
in.effectivePolicy,
|
||||
in.networkID, in.chainID,
|
||||
in.epoch, in.height,
|
||||
in.roundOrView,
|
||||
@@ -244,6 +247,8 @@ func TestComputeRoundDigest_FixedHashFamily(t *testing.T) {
|
||||
{byte(in.proofBackend)},
|
||||
{byte(in.proofFormat)},
|
||||
verifierBytes,
|
||||
// BLOCKERS.md CR-12 mirror — must match production order.
|
||||
{in.effectivePolicy},
|
||||
netBytes,
|
||||
chainBytes,
|
||||
epochBytes,
|
||||
|
||||
Reference in New Issue
Block a user