mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
Two-stage exact-rational (math/big) security math: per-committee capture
p = Sigma_{i=t..n} C(n,i) f^i (1-f)^(n-i), and r-of-m forgery
P_fail = Sigma_{j=r..m} C(m,j) p^j (1-p)^(m-j) — the EXACT binomial tail,
not the p^r approximation (m=r reduces to p^r, proven exactly). Default
Pulsar-HYBRID-PQ-v1 (n=8,t=7,m=12,r=8,f=1/3) = per-committee 2^-8.59,
r-of-m 2^-59.80; all-of-r (m=r=8) 2^-68.74. ratLog2 via big.Float
mantissa/exponent stays accurate below the float64 underflow floor.
The exact tool CORRECTS the n=16 family figures from the design notes:
n=16,t=12 is 2^-10.30 (not 2^-7.9), n=16,t=14 is 2^-16.36 (not 2^-13.5)
— n=16 families are stronger per committee than previously tabulated.
n=8 figures confirmed. Exact-invariant + monotonicity + tabulation tests.
177 lines
6.7 KiB
Go
177 lines
6.7 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
|
||
// pulsar_sampled_security.go — the EXACT r-of-m sampled-certificate security
|
||
// tool. It answers the one question the whole construction rests on: given an
|
||
// adversary holding a stake fraction f, what is the probability it can forge a
|
||
// Pulsar sampled finality certificate?
|
||
//
|
||
// # The two-stage binomial
|
||
//
|
||
// A sampled cert forges iff the adversary captures ≥ r of the m sampled
|
||
// committees, where "capture" of one committee means controlling ≥ t of its n
|
||
// members (then it can produce that committee's t-of-n group signature). With
|
||
// the conservative i.i.d.-Binomial model (the true draw is stake-weighted
|
||
// WITHOUT replacement, which is strictly harder to capture):
|
||
//
|
||
// per-committee capture p = Pr[X ≥ t], X ~ Binom(n, f)
|
||
// = Σ_{i=t..n} C(n,i) f^i (1-f)^(n-i)
|
||
// r-of-m forgery P_fail = Pr[Y ≥ r], Y ~ Binom(m, p)
|
||
// = Σ_{j=r..m} C(m,j) p^j (1-p)^(m-j)
|
||
//
|
||
// This is the Pulsar equivalent of Avalanche's β confidence accumulation:
|
||
// security comes from the REPETITION count r against the per-committee capture
|
||
// p, NOT from any single committee's threshold. The all-of-r special case m = r
|
||
// gives P_fail = p^r exactly; m > r trades a precisely-quantified security cost
|
||
// for m−r committees of liveness slack.
|
||
//
|
||
// # Exact arithmetic
|
||
//
|
||
// Every probability is computed in exact rational arithmetic (math/big.Rat) — no
|
||
// floating-point rounding anywhere in the probability. Only the final
|
||
// human-facing "security bits" (−log₂ P_fail) uses floating point, and that via
|
||
// a big.Float mantissa/exponent split so it stays accurate for probabilities far
|
||
// below the float64 underflow floor (e.g. 2⁻²⁰⁰).
|
||
//
|
||
// # Decomplected
|
||
//
|
||
// This file owns ONLY the security math. It is a sizing/audit tool: it picks
|
||
// nothing and verifies nothing. The verifier (pulsar_sampled_verify.go) enforces
|
||
// the r-of-m count; this file tells an operator what that r BUYS for a given
|
||
// (n, t, m, f). The default parameter set lives with the sortition
|
||
// (PulsarHybridPQv1, pulsar_sortition.go); this file evaluates it.
|
||
package quasar
|
||
|
||
import (
|
||
"math"
|
||
"math/big"
|
||
)
|
||
|
||
// CommitteeCaptureProbability returns the EXACT per-committee capture
|
||
// probability p — the probability that an adversary holding fraction f =
|
||
// fNum/fDen of stake controls ≥ t of a committee's n members, modelled as
|
||
// Binom(n, f):
|
||
//
|
||
// p = Σ_{i=t..n} C(n,i) f^i (1-f)^(n-i).
|
||
//
|
||
// Conservative: the real selection is stake-weighted WITHOUT replacement, which
|
||
// makes capturing ≥ t strictly less likely than this i.i.d. bound. Returns a
|
||
// big.Rat in [0, 1]. t ≤ 0 yields 1 (the whole distribution); t > n yields 0.
|
||
func CommitteeCaptureProbability(n, t int, fNum, fDen uint64) *big.Rat {
|
||
f := big.NewRat(int64(fNum), int64(fDen))
|
||
g := new(big.Rat).Sub(big.NewRat(1, 1), f) // 1 - f
|
||
lo := t
|
||
if lo < 0 {
|
||
lo = 0
|
||
}
|
||
sum := new(big.Rat)
|
||
for i := lo; i <= n; i++ {
|
||
term := new(big.Rat).SetInt(binomial(n, i)) // C(n,i)
|
||
term.Mul(term, ratPow(f, i)) // · f^i
|
||
term.Mul(term, ratPow(g, n-i)) // · (1-f)^(n-i)
|
||
sum.Add(sum, term)
|
||
}
|
||
return sum
|
||
}
|
||
|
||
// SampledFailureProbability returns the EXACT r-of-m finality-forgery
|
||
// probability given a per-committee capture probability p:
|
||
//
|
||
// P_fail = Σ_{j=r..m} C(m,j) p^j (1-p)^(m-j).
|
||
//
|
||
// This is the EXACT binomial tail, NOT the p^r all-of-r approximation. For m = r
|
||
// it equals p^r exactly; for m > r it is strictly larger (the attacker needs
|
||
// only r of m), the exact price of m−r committees of liveness slack. r ≤ 0
|
||
// yields 1; r > m yields 0.
|
||
func SampledFailureProbability(p *big.Rat, m, r int) *big.Rat {
|
||
q := new(big.Rat).Sub(big.NewRat(1, 1), p) // 1 - p
|
||
lo := r
|
||
if lo < 0 {
|
||
lo = 0
|
||
}
|
||
sum := new(big.Rat)
|
||
for j := lo; j <= m; j++ {
|
||
term := new(big.Rat).SetInt(binomial(m, j)) // C(m,j)
|
||
term.Mul(term, ratPow(p, j)) // · p^j
|
||
term.Mul(term, ratPow(q, m-j)) // · (1-p)^(m-j)
|
||
sum.Add(sum, term)
|
||
}
|
||
return sum
|
||
}
|
||
|
||
// CaptureProbability evaluates the per-committee capture probability p for this
|
||
// parameter set (n=N, t=T, f=FMaxNum/FMaxDen).
|
||
func (sp SortitionParams) CaptureProbability() *big.Rat {
|
||
return CommitteeCaptureProbability(int(sp.N), int(sp.T), uint64(sp.FMaxNum), uint64(sp.FMaxDen))
|
||
}
|
||
|
||
// FailureProbability evaluates the r-of-m finality-forgery probability P_fail
|
||
// for this parameter set: SampledFailureProbability(CaptureProbability(), M, R).
|
||
func (sp SortitionParams) FailureProbability() *big.Rat {
|
||
return SampledFailureProbability(sp.CaptureProbability(), int(sp.M), int(sp.R))
|
||
}
|
||
|
||
// CaptureBits returns the per-committee capture security level −log₂ p (larger
|
||
// is safer). A committee with p = 2⁻⁸·⁶ returns ≈ 8.6.
|
||
func (sp SortitionParams) CaptureBits() float64 {
|
||
return -ratLog2(sp.CaptureProbability())
|
||
}
|
||
|
||
// SecurityBits returns the r-of-m finality-forgery security level −log₂ P_fail
|
||
// (larger is safer). The default Pulsar-HYBRID-PQ-v1 set returns ≈ 59.8.
|
||
func (sp SortitionParams) SecurityBits() float64 {
|
||
return -ratLog2(sp.FailureProbability())
|
||
}
|
||
|
||
// ----------------------------------------------------------------------------
|
||
// Exact helpers — integer binomials, rational powers, and a precise rational
|
||
// log₂ that does not underflow on tiny probabilities.
|
||
// ----------------------------------------------------------------------------
|
||
|
||
// binomial returns C(n, k) as an exact big.Int via the multiplicative formula
|
||
// (no factorial overflow). C(n,k)=0 for k<0 or k>n.
|
||
func binomial(n, k int) *big.Int {
|
||
if k < 0 || k > n {
|
||
return big.NewInt(0)
|
||
}
|
||
if k > n-k {
|
||
k = n - k // symmetry: fewer multiplications
|
||
}
|
||
res := big.NewInt(1)
|
||
num := new(big.Int)
|
||
den := new(big.Int)
|
||
for i := 0; i < k; i++ {
|
||
res.Mul(res, num.SetInt64(int64(n-i)))
|
||
res.Div(res, den.SetInt64(int64(i+1)))
|
||
}
|
||
return res
|
||
}
|
||
|
||
// ratPow returns base^exp for a non-negative integer exponent, exactly. exp ≤ 0
|
||
// returns 1.
|
||
func ratPow(base *big.Rat, exp int) *big.Rat {
|
||
res := big.NewRat(1, 1)
|
||
for i := 0; i < exp; i++ {
|
||
res.Mul(res, base)
|
||
}
|
||
return res
|
||
}
|
||
|
||
// ratLog2 returns log₂(r) for a positive rational r, accurately even when r is
|
||
// far below the float64 underflow floor. It splits r = mant · 2^exp with mant ∈
|
||
// [0.5, 1) (via big.Float.MantExp), takes log₂ of the safely-representable
|
||
// mantissa, and adds the exact integer exponent. r ≤ 0 returns −∞ / NaN.
|
||
func ratLog2(r *big.Rat) float64 {
|
||
switch r.Sign() {
|
||
case 0:
|
||
return math.Inf(-1)
|
||
case -1:
|
||
return math.NaN()
|
||
}
|
||
bf := new(big.Float).SetPrec(256).SetRat(r)
|
||
mant := new(big.Float).SetPrec(256)
|
||
exp := bf.MantExp(mant) // r = mant · 2^exp, mant ∈ [0.5, 1)
|
||
m64, _ := mant.Float64()
|
||
return math.Log2(m64) + float64(exp)
|
||
}
|