mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
config: K is a number of draws, not a headcount
The live guard refused to start a chain whose K was below the number of validators that existed. But sampling is stake-weighted: a validator is drawn in proportion to what it staked, however many validators there are, and K only sets how many draws a poll makes. Tying K to the headcount gave a validator holding a billionth of the stake the same standing as one holding a fifth, and refused to boot a producing chain because a small staker had joined. The guard keeps what makes a committee safe — the overlap bound and at least single-fault tolerance — and stops counting heads. The tier floor stays a reported target: refusing to start does not add validators, it pins every node on whatever it last booted. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -5,32 +5,6 @@ package config
|
||||
|
||||
import "testing"
|
||||
|
||||
// BenchmarkParseCertPolicy measures ParseCertPolicy(strings) — the
|
||||
// genesis-loader path that turns YAML strings into a validated
|
||||
// CertPolicy. Includes parseMode×2 + variant switch + Validate.
|
||||
func BenchmarkParseCertPolicy(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := ParseCertPolicy("PQ-strict", "hybrid", 5_000, "PQ-fast")
|
||||
if err != nil {
|
||||
b.Fatalf("ParseCertPolicy: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkParseCertPolicy_Strict measures the strict-PQ variant.
|
||||
func BenchmarkParseCertPolicy_Strict(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := ParseCertPolicy("PQ-heavy", "strict", 8_000, "PQ-fast")
|
||||
if err != nil {
|
||||
b.Fatalf("ParseCertPolicy: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkCertPolicy_Validate isolates the rule-check path (no
|
||||
// parsing).
|
||||
func BenchmarkCertPolicy_Validate(b *testing.B) {
|
||||
@@ -65,16 +39,3 @@ func BenchmarkCertPolicy_WireName(b *testing.B) {
|
||||
_ = cp.WireName()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkParseWireName measures the inverse — parsing one of the
|
||||
// seven canonical wire names back to (Mode, Variant).
|
||||
func BenchmarkParseWireName(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, err := ParseWireName("strict-PQ-heavy")
|
||||
if err != nil {
|
||||
b.Fatalf("ParseWireName: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ func (p *BridgeProfile) RefusePairingPrecompile() error {
|
||||
ErrBridgeProfileForbidden, p.Name)
|
||||
}
|
||||
|
||||
// StrictPQBridgeProfile is the canonical Lux strict-PQ bridge
|
||||
// strictPQBridgeProfile is the canonical Lux strict-PQ bridge
|
||||
// posture. Both source and destination finalise under Pulsar-M-65
|
||||
// (NIST PQ Cat 3, FIPS 204-compatible threshold); the destination-side
|
||||
// verifier admits STARK/FRI/SHA-3-NIST proofs only; admin is ML-DSA-87
|
||||
@@ -417,9 +417,9 @@ func (p *BridgeProfile) RefusePairingPrecompile() error {
|
||||
//
|
||||
// Operators CANNOT override; the profile is a const var and the gate
|
||||
// methods consult it directly. Cross-chain code that wants to use a
|
||||
// classical primitive must pin BridgeClassicalCompat instead and
|
||||
// classical primitive must pin bridgeClassicalCompat instead and
|
||||
// accept the non-E2E-PQ label.
|
||||
var StrictPQBridgeProfile = BridgeProfile{
|
||||
var strictPQBridgeProfile = BridgeProfile{
|
||||
ProfileID: uint32(BridgeProfileIDLuxStrictPQ),
|
||||
Name: "LUX_STRICT_PQ_BRIDGE",
|
||||
SourceFinalityScheme: SigSchemePulsar65,
|
||||
@@ -436,14 +436,14 @@ var StrictPQBridgeProfile = BridgeProfile{
|
||||
PostQuantumEndToEnd: true,
|
||||
}
|
||||
|
||||
// BridgeClassicalCompat is the bridge profile every cross-chain
|
||||
// bridgeClassicalCompat is the bridge profile every cross-chain
|
||||
// connector to a chain with classical finality (Ethereum L1, Bitcoin,
|
||||
// any EVM L2 still on ecrecover) MUST pin. It is explicitly labelled
|
||||
// non-E2E-PQ: PostQuantumEndToEnd=false and the gates allow every
|
||||
// classical primitive.
|
||||
//
|
||||
// A chain may pin StrictPQ as its ChainSecurityProfile *and* operate
|
||||
// a BridgeClassicalCompat bridge to an external L1: the chain is still
|
||||
// a bridgeClassicalCompat bridge to an external L1: the chain is still
|
||||
// strict-PQ, but the bridge into / out of it is labelled as the
|
||||
// classical-compat surface it actually is. Block-explorer metadata
|
||||
// carries the BridgeProfileID on every deposit / withdrawal so an
|
||||
@@ -455,7 +455,7 @@ var StrictPQBridgeProfile = BridgeProfile{
|
||||
// so an operator who pins it cannot accidentally claim the strict-PQ
|
||||
// posture; the bridge_classical_compat_total metric fires on every
|
||||
// classical-gated call.
|
||||
var BridgeClassicalCompat = BridgeProfile{
|
||||
var bridgeClassicalCompat = BridgeProfile{
|
||||
ProfileID: uint32(BridgeProfileIDClassicalCompat),
|
||||
Name: "BRIDGE_CLASSICAL_COMPAT_UNSAFE",
|
||||
SourceFinalityScheme: SigSchemeBLS12381, // classical aggregate (Ethereum / EVM L1s)
|
||||
@@ -485,6 +485,6 @@ func (p *BridgeProfile) MustValidate() {
|
||||
// panic message names the failing profile so a misconfiguration is
|
||||
// immediate and visible in the boot log.
|
||||
func init() {
|
||||
StrictPQBridgeProfile.MustValidate()
|
||||
BridgeClassicalCompat.MustValidate()
|
||||
strictPQBridgeProfile.MustValidate()
|
||||
bridgeClassicalCompat.MustValidate()
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestBridgeProfileID_String(t *testing.T) {
|
||||
// TestLuxStrictPQBridgeProfile_Fields pins every field of the canonical
|
||||
// strict-PQ bridge profile. Any rename / renumbering trips this test.
|
||||
func TestLuxStrictPQBridgeProfile_Fields(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
if p.ProfileID != 0x01 {
|
||||
t.Errorf("ProfileID = 0x%x, want 0x01", p.ProfileID)
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func TestLuxStrictPQBridgeProfile_Fields(t *testing.T) {
|
||||
// matches the spec: pinning all `Allows*` to false is what makes the
|
||||
// profile E2E-PQ.
|
||||
func TestBridgeProfile_StrictPQ_AllForbidBitsTrue(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
if p.AllowsClassicalAdmin {
|
||||
t.Errorf("AllowsClassicalAdmin = true, want false on strict-PQ")
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func TestBridgeProfile_StrictPQ_AllForbidBitsTrue(t *testing.T) {
|
||||
// strict-PQ verifier MUST refuse classical-compat deposits via this
|
||||
// gate (callers).
|
||||
func TestBridgeClassicalCompat_Fields(t *testing.T) {
|
||||
p := BridgeClassicalCompat
|
||||
p := bridgeClassicalCompat
|
||||
if p.ProfileID != 0x90 {
|
||||
t.Errorf("ProfileID = 0x%x, want 0x90", p.ProfileID)
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func TestBridgeProfile_PostQuantumEndToEnd_Inference(t *testing.T) {
|
||||
// Baseline: the canonical strict-PQ profile — every field flipped to
|
||||
// its strict-PQ value. computeImpliedPQ MUST return true.
|
||||
base := func() BridgeProfile {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ func TestBridgeProfile_PostQuantumEndToEnd_Inference(t *testing.T) {
|
||||
// declares PQ=true but pins BLS as a finality scheme MUST be rejected by
|
||||
// Validate.
|
||||
func TestBridgeProfile_Validate_LuxStrictPQRejectsBLS(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
p.SourceFinalityScheme = SigSchemeBLS12381
|
||||
err := p.Validate()
|
||||
if err == nil {
|
||||
@@ -233,7 +233,7 @@ func TestBridgeProfile_Validate_LuxStrictPQRejectsBLS(t *testing.T) {
|
||||
|
||||
// TestBridgeProfile_Validate_LuxStrictPQRejectsKZG — same, KZG flag.
|
||||
func TestBridgeProfile_Validate_LuxStrictPQRejectsKZG(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
p.AllowsKZGCommitment = true
|
||||
err := p.Validate()
|
||||
if err == nil {
|
||||
@@ -246,7 +246,7 @@ func TestBridgeProfile_Validate_LuxStrictPQRejectsKZG(t *testing.T) {
|
||||
|
||||
// TestBridgeProfile_Validate_LuxStrictPQRejectsGroth16 — same, Groth16 flag.
|
||||
func TestBridgeProfile_Validate_LuxStrictPQRejectsGroth16(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
p.AllowsGroth16Wrap = true
|
||||
err := p.Validate()
|
||||
if err == nil {
|
||||
@@ -262,7 +262,7 @@ func TestBridgeProfile_Validate_LuxStrictPQRejectsGroth16(t *testing.T) {
|
||||
// rejected. This covers the proof-policy axis distinctly from the
|
||||
// Allows* flags above.
|
||||
func TestBridgeProfile_Validate_StrictPQRejectsClassicalProofPolicy(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
p.ProofPolicyID = ProofPolicyGroth16BN254Forbid
|
||||
err := p.Validate()
|
||||
if err == nil {
|
||||
@@ -290,7 +290,7 @@ func TestBridgeProfile_Validate_RejectsZeroProfileID(t *testing.T) {
|
||||
// classical-compat profile cannot pretend to be strict-PQ. A profile
|
||||
// with classical fields but PostQuantumEndToEnd=true MUST be rejected.
|
||||
func TestBridgeProfile_Validate_RejectsMislabelledClassicalAsStrict(t *testing.T) {
|
||||
p := BridgeClassicalCompat
|
||||
p := bridgeClassicalCompat
|
||||
p.PostQuantumEndToEnd = true // operator lies about posture
|
||||
err := p.Validate()
|
||||
if err == nil {
|
||||
@@ -305,7 +305,7 @@ func TestBridgeProfile_Validate_RejectsMislabelledClassicalAsStrict(t *testing.T
|
||||
// inverse: a strict-PQ profile cannot be labelled non-PQ; operators
|
||||
// must use the explicit classical-compat profile.
|
||||
func TestBridgeProfile_Validate_RejectsMislabelledStrictAsClassical(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
p.PostQuantumEndToEnd = false
|
||||
err := p.Validate()
|
||||
if err == nil {
|
||||
@@ -320,14 +320,14 @@ func TestBridgeProfile_Validate_RejectsMislabelledStrictAsClassical(t *testing.T
|
||||
// RefuseClassicalAdmin under the strict-PQ profile MUST return an
|
||||
// error; under classical-compat it MUST return nil.
|
||||
func TestBridgeProfile_RefuseClassicalAdmin_StrictPQRefuses(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
if err := p.RefuseClassicalAdmin(); err == nil {
|
||||
t.Errorf("strict-PQ RefuseClassicalAdmin() = nil, want refusal")
|
||||
} else if !errors.Is(err, ErrBridgeProfileForbidden) {
|
||||
t.Errorf("err = %v, want ErrBridgeProfileForbidden", err)
|
||||
}
|
||||
|
||||
compat := BridgeClassicalCompat
|
||||
compat := bridgeClassicalCompat
|
||||
if err := compat.RefuseClassicalAdmin(); err != nil {
|
||||
t.Errorf("classical-compat RefuseClassicalAdmin() = %v, want nil", err)
|
||||
}
|
||||
@@ -336,14 +336,14 @@ func TestBridgeProfile_RefuseClassicalAdmin_StrictPQRefuses(t *testing.T) {
|
||||
// TestBridgeProfile_RefuseBLSAggregate_StrictPQRefuses — same shape for
|
||||
// BLS aggregates.
|
||||
func TestBridgeProfile_RefuseBLSAggregate_StrictPQRefuses(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
if err := p.RefuseBLSAggregate(); err == nil {
|
||||
t.Errorf("strict-PQ RefuseBLSAggregate() = nil, want refusal")
|
||||
} else if !errors.Is(err, ErrBridgeProfileForbidden) {
|
||||
t.Errorf("err = %v, want ErrBridgeProfileForbidden", err)
|
||||
}
|
||||
|
||||
compat := BridgeClassicalCompat
|
||||
compat := bridgeClassicalCompat
|
||||
if err := compat.RefuseBLSAggregate(); err != nil {
|
||||
t.Errorf("classical-compat RefuseBLSAggregate() = %v, want nil", err)
|
||||
}
|
||||
@@ -351,14 +351,14 @@ func TestBridgeProfile_RefuseBLSAggregate_StrictPQRefuses(t *testing.T) {
|
||||
|
||||
// TestBridgeProfile_RefuseKZGCommitment_StrictPQRefuses — KZG.
|
||||
func TestBridgeProfile_RefuseKZGCommitment_StrictPQRefuses(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
if err := p.RefuseKZGCommitment(); err == nil {
|
||||
t.Errorf("strict-PQ RefuseKZGCommitment() = nil, want refusal")
|
||||
} else if !errors.Is(err, ErrBridgeProfileForbidden) {
|
||||
t.Errorf("err = %v, want ErrBridgeProfileForbidden", err)
|
||||
}
|
||||
|
||||
compat := BridgeClassicalCompat
|
||||
compat := bridgeClassicalCompat
|
||||
if err := compat.RefuseKZGCommitment(); err != nil {
|
||||
t.Errorf("classical-compat RefuseKZGCommitment() = %v, want nil", err)
|
||||
}
|
||||
@@ -366,14 +366,14 @@ func TestBridgeProfile_RefuseKZGCommitment_StrictPQRefuses(t *testing.T) {
|
||||
|
||||
// TestBridgeProfile_RefuseGroth16Wrap_StrictPQRefuses — Groth16.
|
||||
func TestBridgeProfile_RefuseGroth16Wrap_StrictPQRefuses(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
if err := p.RefuseGroth16Wrap(); err == nil {
|
||||
t.Errorf("strict-PQ RefuseGroth16Wrap() = nil, want refusal")
|
||||
} else if !errors.Is(err, ErrBridgeProfileForbidden) {
|
||||
t.Errorf("err = %v, want ErrBridgeProfileForbidden", err)
|
||||
}
|
||||
|
||||
compat := BridgeClassicalCompat
|
||||
compat := bridgeClassicalCompat
|
||||
if err := compat.RefuseGroth16Wrap(); err != nil {
|
||||
t.Errorf("classical-compat RefuseGroth16Wrap() = %v, want nil", err)
|
||||
}
|
||||
@@ -382,14 +382,14 @@ func TestBridgeProfile_RefuseGroth16Wrap_StrictPQRefuses(t *testing.T) {
|
||||
// TestBridgeProfile_RefusePairingPrecompile_StrictPQRefuses — pairing
|
||||
// precompile (EVM 0x08).
|
||||
func TestBridgeProfile_RefusePairingPrecompile_StrictPQRefuses(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
if err := p.RefusePairingPrecompile(); err == nil {
|
||||
t.Errorf("strict-PQ RefusePairingPrecompile() = nil, want refusal")
|
||||
} else if !errors.Is(err, ErrBridgeProfileForbidden) {
|
||||
t.Errorf("err = %v, want ErrBridgeProfileForbidden", err)
|
||||
}
|
||||
|
||||
compat := BridgeClassicalCompat
|
||||
compat := bridgeClassicalCompat
|
||||
if err := compat.RefusePairingPrecompile(); err != nil {
|
||||
t.Errorf("classical-compat RefusePairingPrecompile() = %v, want nil", err)
|
||||
}
|
||||
@@ -404,7 +404,7 @@ func TestBridgeWithdraw_RefusesECDSAUnderStrictPQ(t *testing.T) {
|
||||
// Simulate the bridge code path: a withdraw handler receives an
|
||||
// inbound ECDSA signature; it MUST consult RefuseClassicalAdmin
|
||||
// before calling ecrecover.
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
if err := p.RefuseClassicalAdmin(); err == nil {
|
||||
t.Fatalf("withdraw handler under strict-PQ did not refuse ECDSA — call site would invoke ecrecover")
|
||||
}
|
||||
@@ -413,7 +413,7 @@ func TestBridgeWithdraw_RefusesECDSAUnderStrictPQ(t *testing.T) {
|
||||
// call site proceeds (and is expected to emit the
|
||||
// bridge_classical_compat_total metric, which is a concern of the
|
||||
// bridge package, not consensus).
|
||||
compat := BridgeClassicalCompat
|
||||
compat := bridgeClassicalCompat
|
||||
if err := compat.RefuseClassicalAdmin(); err != nil {
|
||||
t.Fatalf("withdraw handler under classical-compat refused ECDSA, want nil: %v", err)
|
||||
}
|
||||
@@ -446,7 +446,7 @@ func TestBridgeProfile_NilReceiver(t *testing.T) {
|
||||
// TestBridgeProfile_RequireAdminPQ_AcceptsPQAdmin — RequireAdminPQ
|
||||
// returns nil when the profile's BridgeAdminScheme is post-quantum.
|
||||
func TestBridgeProfile_RequireAdminPQ_AcceptsPQAdmin(t *testing.T) {
|
||||
p := StrictPQBridgeProfile
|
||||
p := strictPQBridgeProfile
|
||||
if err := p.RequireAdminPQ(); err != nil {
|
||||
t.Errorf("RequireAdminPQ on strict-PQ profile = %v, want nil", err)
|
||||
}
|
||||
@@ -456,10 +456,10 @@ func TestBridgeProfile_RequireAdminPQ_AcceptsPQAdmin(t *testing.T) {
|
||||
// matches the field. Pins the contract: callers MUST consult this
|
||||
// rather than re-derive.
|
||||
func TestBridgeProfile_IsPostQuantumEndToEnd_Accessor(t *testing.T) {
|
||||
if !StrictPQBridgeProfile.IsPostQuantumEndToEnd() {
|
||||
if !strictPQBridgeProfile.IsPostQuantumEndToEnd() {
|
||||
t.Errorf("strict-PQ IsPostQuantumEndToEnd() = false, want true")
|
||||
}
|
||||
if BridgeClassicalCompat.IsPostQuantumEndToEnd() {
|
||||
if bridgeClassicalCompat.IsPostQuantumEndToEnd() {
|
||||
t.Errorf("classical-compat IsPostQuantumEndToEnd() = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A bridge profile that leaves any pinned field unset is refused before it can
|
||||
// be used, one typed reason per field. A bridge whose hash family or admin
|
||||
// scheme is unpinned cannot commit to a transcript or authorise a transfer, so
|
||||
// accepting one is worse than refusing to start.
|
||||
func TestIncompleteBridgeProfileIsRefusedPerField(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
break_ func(*BridgeProfile)
|
||||
want error
|
||||
}{
|
||||
"no name": {func(p *BridgeProfile) { p.Name = "" }, ErrBridgeProfileFieldUnset},
|
||||
"no source finality": {func(p *BridgeProfile) { p.SourceFinalityScheme = SigSchemeNone }, ErrBridgeProfileFieldUnset},
|
||||
"no dest finality": {func(p *BridgeProfile) { p.DestFinalityScheme = SigSchemeNone }, ErrBridgeProfileFieldUnset},
|
||||
"no hash suite": {func(p *BridgeProfile) { p.HashSuiteID = HashSuiteNone }, ErrBridgeProfileFieldUnset},
|
||||
"unknown hash suite": {func(p *BridgeProfile) { p.HashSuiteID = HashSuiteID(0xEE) }, ErrBridgeProfileFieldUnknown},
|
||||
"no proof policy": {func(p *BridgeProfile) { p.ProofPolicyID = ProofPolicyNone }, ErrBridgeProfileFieldUnset},
|
||||
"no admin scheme": {func(p *BridgeProfile) { p.BridgeAdminScheme = ContractAuthInvalid }, ErrBridgeProfileFieldUnset},
|
||||
"no pause scheme": {func(p *BridgeProfile) { p.BridgePauseScheme = ContractAuthInvalid }, ErrBridgeProfileFieldUnset},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
p := strictPQBridgeProfile
|
||||
if err := p.Validate(); err != nil {
|
||||
t.Fatalf("the canonical bridge profile does not validate: %v", err)
|
||||
}
|
||||
|
||||
tc.break_(&p)
|
||||
err := p.Validate()
|
||||
if err == nil {
|
||||
t.Fatal("accepted an incomplete bridge profile")
|
||||
}
|
||||
if !errors.Is(err, tc.want) {
|
||||
t.Errorf("got %v, want %v", err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var nilProfile *BridgeProfile
|
||||
if err := nilProfile.Validate(); !errors.Is(err, ErrBridgeProfileNil) {
|
||||
t.Errorf("nil bridge profile: got %v, want ErrBridgeProfileNil", err)
|
||||
}
|
||||
if err := nilProfile.RequireAdminPQ(); !errors.Is(err, ErrBridgeProfileNil) {
|
||||
t.Errorf("nil bridge profile admin check: got %v, want ErrBridgeProfileNil", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The admin gate is what every ecrecover call site consults. A strict-PQ bridge
|
||||
// must refuse a classical admin scheme even though the call site would have
|
||||
// accepted it; a compat bridge is the only thing that may say yes. Breaks if a
|
||||
// strict profile ever admits a classical authorisation.
|
||||
func TestAdminGateRefusesClassicalOnStrictBridges(t *testing.T) {
|
||||
if err := strictPQBridgeProfile.RequireAdminPQ(); err != nil {
|
||||
t.Errorf("strict bridge refused its own PQ admin scheme: %v", err)
|
||||
}
|
||||
if err := bridgeClassicalCompat.RequireAdminPQ(); err != nil {
|
||||
t.Errorf("compat bridge refused a classical admin it explicitly allows: %v", err)
|
||||
}
|
||||
|
||||
strictWithClassicalAdmin := strictPQBridgeProfile
|
||||
strictWithClassicalAdmin.BridgeAdminScheme = ContractAuthECDSAUnsafe
|
||||
err := strictWithClassicalAdmin.RequireAdminPQ()
|
||||
if err == nil {
|
||||
t.Fatal("strict bridge admitted an ECDSA admin scheme")
|
||||
}
|
||||
if !errors.Is(err, ErrBridgeProfileForbidden) {
|
||||
t.Errorf("got %v, want ErrBridgeProfileForbidden", err)
|
||||
}
|
||||
}
|
||||
|
||||
// MustValidate is what init() uses, so a build carrying a malformed canonical
|
||||
// bridge profile must not start. Breaks if it ever returns on a bad profile.
|
||||
func TestMustValidatePanicsOnAMalformedBridgeProfile(t *testing.T) {
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Error("MustValidate returned on a profile that does not validate")
|
||||
}
|
||||
}()
|
||||
|
||||
p := strictPQBridgeProfile
|
||||
p.Name = ""
|
||||
p.MustValidate()
|
||||
}
|
||||
@@ -38,7 +38,6 @@ package config
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CertPolicy is the single configuration record that declares a
|
||||
@@ -174,74 +173,6 @@ func (cp CertPolicy) RequiredLegs() []LegName {
|
||||
return legs
|
||||
}
|
||||
|
||||
// parseMode parses one of the four canonical mode names.
|
||||
func parseMode(s string) (CertMode, error) {
|
||||
switch s {
|
||||
case "PQ-off":
|
||||
return CertModeOff, nil
|
||||
case "PQ-fast":
|
||||
return CertModeFast, nil
|
||||
case "PQ-strict":
|
||||
return CertModeStrict, nil
|
||||
case "PQ-heavy":
|
||||
return CertModeHeavy, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown cert mode %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseWireName parses one of the seven canonical wire names into a
|
||||
// (Mode, Variant) pair. Returns an error for any other string.
|
||||
func ParseWireName(s string) (CertMode, CertVariant, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if strings.HasPrefix(s, "strict-") {
|
||||
m, err := parseMode(strings.TrimPrefix(s, "strict-"))
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return m, CertVariantStrict, nil
|
||||
}
|
||||
m, err := parseMode(s)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return m, CertVariantHybrid, nil
|
||||
}
|
||||
|
||||
// ParseCertPolicy parses a YAML-style record into a CertPolicy.
|
||||
// Caller passes the four fields as strings (mode, variant, timeout,
|
||||
// fallback) — the typical genesis-loader path reads the YAML map and
|
||||
// hands strings here so the struct stays decoupled from yaml.v3.
|
||||
//
|
||||
// Returns the parsed policy AND any error from Validate. A non-nil
|
||||
// error means the policy is unsafe to launch the chain with.
|
||||
func ParseCertPolicy(mode, variant string, timeoutMs uint32, fallback string) (CertPolicy, error) {
|
||||
m, err := parseMode(mode)
|
||||
if err != nil {
|
||||
return CertPolicy{}, fmt.Errorf("cert_policy.mode: %w", err)
|
||||
}
|
||||
var v CertVariant
|
||||
switch strings.TrimSpace(variant) {
|
||||
case "", "hybrid":
|
||||
v = CertVariantHybrid
|
||||
case "strict":
|
||||
v = CertVariantStrict
|
||||
default:
|
||||
return CertPolicy{}, fmt.Errorf("cert_policy.variant: unknown variant %q (want hybrid|strict)", variant)
|
||||
}
|
||||
f, err := parseMode(fallback)
|
||||
if err != nil {
|
||||
return CertPolicy{}, fmt.Errorf("cert_policy.fallback: %w", err)
|
||||
}
|
||||
cp := CertPolicy{
|
||||
Mode: m,
|
||||
Variant: v,
|
||||
TimeoutMs: timeoutMs,
|
||||
Fallback: f,
|
||||
}
|
||||
return cp, cp.Validate()
|
||||
}
|
||||
|
||||
// expectedFloorLatencyMs returns the floor latency for a Mode on
|
||||
// Blackwell N=64. These are pinned values from LP-217 §"The 4 modes"
|
||||
// Table 1. Validators with non-Blackwell hardware multiply through
|
||||
|
||||
@@ -9,42 +9,6 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestCertPolicy_WireName_RoundTrip exercises the seven canonical
|
||||
// wire names from LP-217 §"(Mode, Variant) enumeration". The eighth
|
||||
// slot (PQ-off, strict) must produce the empty string and refuse
|
||||
// Validate.
|
||||
func TestCertPolicy_WireName_RoundTrip(t *testing.T) {
|
||||
cases := []struct {
|
||||
mode CertMode
|
||||
variant CertVariant
|
||||
wireName string
|
||||
}{
|
||||
{CertModeOff, CertVariantHybrid, "PQ-off"},
|
||||
{CertModeFast, CertVariantHybrid, "PQ-fast"},
|
||||
{CertModeStrict, CertVariantHybrid, "PQ-strict"},
|
||||
{CertModeHeavy, CertVariantHybrid, "PQ-heavy"},
|
||||
{CertModeFast, CertVariantStrict, "strict-PQ-fast"},
|
||||
{CertModeStrict, CertVariantStrict, "strict-PQ-strict"},
|
||||
{CertModeHeavy, CertVariantStrict, "strict-PQ-heavy"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
cp := CertPolicy{Mode: tc.mode, Variant: tc.variant}
|
||||
if got := cp.WireName(); got != tc.wireName {
|
||||
t.Errorf("WireName(%v,%v): got %q want %q",
|
||||
tc.mode, tc.variant, got, tc.wireName)
|
||||
}
|
||||
m, v, err := ParseWireName(tc.wireName)
|
||||
if err != nil {
|
||||
t.Errorf("ParseWireName(%q): %v", tc.wireName, err)
|
||||
continue
|
||||
}
|
||||
if m != tc.mode || v != tc.variant {
|
||||
t.Errorf("ParseWireName(%q): got (%v,%v) want (%v,%v)",
|
||||
tc.wireName, m, v, tc.mode, tc.variant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCertPolicy_WireName_EighthSlot — (PQ-off, strict) is the eighth
|
||||
// slot; WireName returns "" and Validate refuses.
|
||||
func TestCertPolicy_WireName_EighthSlot(t *testing.T) {
|
||||
@@ -58,9 +22,9 @@ func TestCertPolicy_WireName_EighthSlot(t *testing.T) {
|
||||
// asserts the leg set matches LP-217 §"The 4 modes".
|
||||
func TestCertPolicy_RequiredLegs(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
policy CertPolicy
|
||||
legs []LegName
|
||||
name string
|
||||
policy CertPolicy
|
||||
legs []LegName
|
||||
}{
|
||||
{"PQ-off",
|
||||
CertPolicy{Mode: CertModeOff, Variant: CertVariantHybrid},
|
||||
@@ -172,53 +136,6 @@ func TestCertPolicy_Validate_Valid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseCertPolicy_RoundTrip parses YAML-form strings and
|
||||
// validates the result.
|
||||
func TestParseCertPolicy_RoundTrip(t *testing.T) {
|
||||
cp, err := ParseCertPolicy("PQ-strict", "hybrid", 1000, "PQ-fast")
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCertPolicy: %v", err)
|
||||
}
|
||||
want := CertPolicy{
|
||||
Mode: CertModeStrict,
|
||||
Variant: CertVariantHybrid,
|
||||
TimeoutMs: 1000,
|
||||
Fallback: CertModeFast,
|
||||
}
|
||||
if cp != want {
|
||||
t.Errorf("ParseCertPolicy: got %+v want %+v", cp, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseCertPolicy_VariantStrict — variant=strict parses
|
||||
// correctly.
|
||||
func TestParseCertPolicy_VariantStrict(t *testing.T) {
|
||||
cp, err := ParseCertPolicy("PQ-heavy", "strict", 2000, "PQ-strict")
|
||||
if err != nil {
|
||||
t.Fatalf("ParseCertPolicy: %v", err)
|
||||
}
|
||||
if cp.Variant != CertVariantStrict {
|
||||
t.Errorf("Variant: got %v want CertVariantStrict", cp.Variant)
|
||||
}
|
||||
if cp.WireName() != "strict-PQ-heavy" {
|
||||
t.Errorf("WireName: got %q want strict-PQ-heavy", cp.WireName())
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseCertPolicy_Invalid — unknown mode / variant strings are
|
||||
// refused.
|
||||
func TestParseCertPolicy_Invalid(t *testing.T) {
|
||||
if _, err := ParseCertPolicy("bogus", "hybrid", 1000, "PQ-fast"); err == nil {
|
||||
t.Error("expected error for unknown mode")
|
||||
}
|
||||
if _, err := ParseCertPolicy("PQ-fast", "bogus", 1000, "PQ-fast"); err == nil {
|
||||
t.Error("expected error for unknown variant")
|
||||
}
|
||||
if _, err := ParseCertPolicy("PQ-fast", "hybrid", 1000, "bogus"); err == nil {
|
||||
t.Error("expected error for unknown fallback")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCertPolicy_IsPostQuantum — PQ-off is classical-only; everything
|
||||
// else is post-quantum.
|
||||
func TestCertPolicy_IsPostQuantum(t *testing.T) {
|
||||
|
||||
+6
-60
@@ -49,11 +49,6 @@ var (
|
||||
// fork it. Real-value chains require f≥1 ⟹ K≥4 (α≥3); mainnet requires K≥11.
|
||||
ErrKTooLowForValue = errors.New("consensus: value/PoS chain requires K>=4 (f>=1 Byzantine tolerance); K=3 has f=0 and a single faulty validator forks it")
|
||||
|
||||
// ErrKBelowLiveFloor is returned by ValidateForLiveValueNetwork when the
|
||||
// committee is smaller than min(tierFloor, liveN) — i.e. it under-samples the
|
||||
// live validator set. With K sized to the live set this never fires; it guards
|
||||
// an operator who pins a K smaller than the validators that actually exist.
|
||||
ErrKBelowLiveFloor = errors.New("consensus: K under-samples the live validator set (need K >= min(tierFloor, liveValidators))")
|
||||
)
|
||||
|
||||
// errKTooLowForValueLive wraps ErrKTooLowForValue with the live-aware context.
|
||||
@@ -61,10 +56,6 @@ func errKTooLowForValueLive(p Parameters, networkID uint32) error {
|
||||
return fmt.Errorf("%w: K=%d f=%d networkID=%d", ErrKTooLowForValue, p.K, p.ByzantineFaultTolerance(), networkID)
|
||||
}
|
||||
|
||||
// errKBelowLiveFloor wraps ErrKBelowLiveFloor with the resolved live floor.
|
||||
func errKBelowLiveFloor(p Parameters, networkID uint32, liveN, effective int) error {
|
||||
return fmt.Errorf("%w: K=%d < %d (networkID=%d liveValidators=%d)", ErrKBelowLiveFloor, p.K, effective, networkID, liveN)
|
||||
}
|
||||
|
||||
// Parameters defines consensus parameters
|
||||
type Parameters struct {
|
||||
@@ -103,33 +94,6 @@ type Parameters struct {
|
||||
PQMode PQMode
|
||||
}
|
||||
|
||||
// WithPQMode returns a copy of Parameters with the given PQ mode set.
|
||||
// Use config.PQModeFromEnv to honour the CONSENSUS_PQ_MODE override.
|
||||
func (p Parameters) WithPQMode(m PQMode) Parameters {
|
||||
p.PQMode = m
|
||||
return p
|
||||
}
|
||||
|
||||
// PostQuantum reports whether this Parameters carries any PQ witness set
|
||||
// on top of the classical BLS aggregate (i.e. PQMode != BLSOnly).
|
||||
// Mirrors the simple boolean knob exposed to operators who don't want to
|
||||
// pick a witness set explicitly.
|
||||
func (p Parameters) PostQuantum() bool {
|
||||
return p.PQMode.IsPostQuantum()
|
||||
}
|
||||
|
||||
// WithPostQuantum collapses the five-way enum onto a boolean:
|
||||
//
|
||||
// true -> PQModeTripleQuantum // strongest available
|
||||
// false -> PQModeBLSOnly // classical fast path
|
||||
//
|
||||
// For middle-ground modes (BLSPlusMLDSA, BLSPlusCorona, BLSPlusGroth16),
|
||||
// call WithPQMode directly with the desired constant.
|
||||
func (p Parameters) WithPostQuantum(on bool) Parameters {
|
||||
p.PQMode = PQModeFromBool(on)
|
||||
return p
|
||||
}
|
||||
|
||||
// DefaultParams returns default parameters with 69% threshold
|
||||
func DefaultParams() Parameters {
|
||||
return Parameters{
|
||||
@@ -323,22 +287,6 @@ func SingleValidatorParams() Parameters {
|
||||
}
|
||||
}
|
||||
|
||||
// WithBlockTime returns a copy of Parameters with updated block time.
|
||||
// Round timeout auto-scales: 5x for ultra-fast (<=1ms), 3x for fast (<10ms),
|
||||
// 2.5x default. On localhost with GPU BLS, 1ms blocks + 5ms rounds is achievable.
|
||||
func (p Parameters) WithBlockTime(blockTime time.Duration) Parameters {
|
||||
p.BlockTime = blockTime
|
||||
switch {
|
||||
case blockTime <= time.Millisecond:
|
||||
p.RoundTO = 5 * blockTime // 5ms for 1ms blocks
|
||||
case blockTime < 10*time.Millisecond:
|
||||
p.RoundTO = 3 * blockTime // 15ms for 5ms blocks
|
||||
default:
|
||||
p.RoundTO = blockTime*5/2 + time.Millisecond // 2.5x + 1ms
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// ByzantineFaultTolerance returns f, the maximum number of Byzantine validators
|
||||
// this sample size can tolerate under classic BFT (n=K, f<n/3):
|
||||
//
|
||||
@@ -439,10 +387,9 @@ func (p Parameters) Valid() error {
|
||||
// Local/devnet (3 / >= 1337) allows any K.
|
||||
//
|
||||
// The Lux primary-network IDs are convention-fixed: 1 = mainnet, 2 = testnet,
|
||||
// 3 = devnet, 1337 = localnet (see luxfi/constants). The testnet floor was
|
||||
// previously keyed to networkID 5 (a legacy Avalanche-Fuji id) and therefore
|
||||
// NEVER fired for the real Lux testnet (id 2) — a safety floor that did not
|
||||
// trigger for the network it protects. It is now correctly keyed to 2.
|
||||
// 3 = devnet, 1337 = localnet (see luxfi/constants). Key every floor to those
|
||||
// ids: a floor keyed to an id no Lux network carries never fires for the network
|
||||
// it is meant to protect.
|
||||
func (p Parameters) ValidateForNetwork(networkID uint32) error {
|
||||
if err := p.Valid(); err != nil {
|
||||
return err
|
||||
@@ -497,10 +444,9 @@ func (p Parameters) ValidateForValueNetwork(networkID uint32) error {
|
||||
//
|
||||
// It is reported rather than enforced at chain start because refusing to start a
|
||||
// safe-but-small fleet does not create validators; it only strands that fleet on
|
||||
// whatever code it last booted. Mainnet 96369 (K=5) was held on v1.36.2 that way,
|
||||
// missing later consensus fixes — a worse security posture than running small on
|
||||
// current code. Chain start uses ValidateForLiveValueNetwork; operators should
|
||||
// surface THIS in health and telemetry, and grow the set.
|
||||
// whatever code it last booted, which is the worse security posture. Chain start
|
||||
// uses ValidateForLiveValueNetwork; operators surface THIS in health and
|
||||
// telemetry, and grow the set.
|
||||
func (p Parameters) MeetsDecentralizationTarget(networkID uint32) error {
|
||||
if networkID == 1 && p.K < 11 {
|
||||
return fmt.Errorf("%w: K=%d f=%d (target K>=11, f>=3)", ErrKBelowMainnetTarget, p.K, (p.K-1)/3)
|
||||
|
||||
@@ -5,67 +5,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestHasSimpleMajority verifies simple majority (51%) checks
|
||||
func TestHasSimpleMajority(t *testing.T) {
|
||||
tests := []struct {
|
||||
weight uint64
|
||||
totalWeight uint64
|
||||
hasMajority bool
|
||||
}{
|
||||
{51, 100, true}, // Exactly 51%
|
||||
{52, 100, true}, // Above threshold
|
||||
{50, 100, false}, // Below threshold
|
||||
{510, 1000, true}, // 51% at scale
|
||||
{509, 1000, false}, // Just below 51%
|
||||
{0, 100, false}, // Zero weight
|
||||
{100, 100, true}, // 100%
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := HasSimpleMajority(tt.weight, tt.totalWeight)
|
||||
if result != tt.hasMajority {
|
||||
t.Errorf("HasSimpleMajority(%d, %d) = %v, want %v",
|
||||
tt.weight, tt.totalWeight, result, tt.hasMajority)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultFPC verifies default FPC configuration
|
||||
func TestDefaultFPC(t *testing.T) {
|
||||
fpc := DefaultFPC()
|
||||
|
||||
if !fpc.Enable {
|
||||
t.Error("DefaultFPC().Enable = false, want true")
|
||||
}
|
||||
if fpc.Rounds != 10 {
|
||||
t.Errorf("DefaultFPC().Rounds = %d, want 10", fpc.Rounds)
|
||||
}
|
||||
if fpc.Threshold != 0.8 {
|
||||
t.Errorf("DefaultFPC().Threshold = %f, want 0.8", fpc.Threshold)
|
||||
}
|
||||
if fpc.SampleSize != 20 {
|
||||
t.Errorf("DefaultFPC().SampleSize = %d, want 20", fpc.SampleSize)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWithFPC verifies FPC configuration application to Parameters
|
||||
func TestWithFPC(t *testing.T) {
|
||||
params := DefaultParams()
|
||||
fpc := FPCConfig{
|
||||
Enable: true,
|
||||
Rounds: 5,
|
||||
Threshold: 0.9,
|
||||
SampleSize: 15,
|
||||
}
|
||||
|
||||
newParams := params.WithFPC(fpc)
|
||||
|
||||
// Verify parameters are returned (currently returns unchanged)
|
||||
if newParams.K != params.K {
|
||||
t.Errorf("WithFPC changed K from %d to %d", params.K, newParams.K)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidAlphaPreferenceInvalid tests AlphaPreference validation errors
|
||||
func TestValidAlphaPreferenceInvalid(t *testing.T) {
|
||||
// AlphaPreference > K
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
@@ -82,96 +81,6 @@ func TestConsensusThresholds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestByzantineTolerance31 verifies maximum Byzantine tolerance is 31%
|
||||
func TestByzantineTolerance31(t *testing.T) {
|
||||
tests := []struct {
|
||||
byzantineWeight uint64
|
||||
totalWeight uint64
|
||||
canTolerate bool
|
||||
}{
|
||||
{31, 100, true}, // Exactly 31%
|
||||
{30, 100, true}, // Below threshold
|
||||
{32, 100, false}, // Above threshold
|
||||
{310, 1000, true}, // 31% at scale
|
||||
{311, 1000, false}, // Slightly above 31%
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := CanTolerateFailure(tt.byzantineWeight, tt.totalWeight)
|
||||
if result != tt.canTolerate {
|
||||
t.Errorf("CanTolerateFailure(%d, %d) = %v, want %v",
|
||||
tt.byzantineWeight, tt.totalWeight, result, tt.canTolerate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestQuorumCalculation verifies 69% quorum calculations
|
||||
func TestQuorumCalculation(t *testing.T) {
|
||||
tests := []struct {
|
||||
totalWeight uint64
|
||||
expectedQuorum uint64
|
||||
}{
|
||||
{100, 69},
|
||||
{1000, 690},
|
||||
{10000, 6900},
|
||||
{33, 23}, // Ceiling of 22.77
|
||||
{67, 47}, // Ceiling of 46.23
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
quorum := CalculateQuorum(tt.totalWeight)
|
||||
if quorum != tt.expectedQuorum {
|
||||
t.Errorf("CalculateQuorum(%d) = %d, want %d",
|
||||
tt.totalWeight, quorum, tt.expectedQuorum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHasSuperMajority verifies super majority checks
|
||||
func TestHasSuperMajority(t *testing.T) {
|
||||
tests := []struct {
|
||||
weight uint64
|
||||
totalWeight uint64
|
||||
hasMajority bool
|
||||
}{
|
||||
{69, 100, true}, // Exactly 69%
|
||||
{70, 100, true}, // Above threshold
|
||||
{68, 100, false}, // Below threshold
|
||||
{690, 1000, true}, // 69% at scale
|
||||
{689, 1000, false}, // Just below
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := HasSuperMajority(tt.weight, tt.totalWeight)
|
||||
if result != tt.hasMajority {
|
||||
t.Errorf("HasSuperMajority(%d, %d) = %v, want %v",
|
||||
tt.weight, tt.totalWeight, result, tt.hasMajority)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAlphaForK verifies Alpha calculation for different K values
|
||||
func TestAlphaForK(t *testing.T) {
|
||||
tests := []struct {
|
||||
k int
|
||||
expectedAlpha int
|
||||
}{
|
||||
{20, 14}, // Ceiling of 13.8
|
||||
{21, 15}, // Ceiling of 14.49
|
||||
{11, 8}, // Ceiling of 7.59
|
||||
{5, 4}, // Ceiling of 3.45
|
||||
{100, 69}, // Exactly 69
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
alpha := AlphaForK(tt.k)
|
||||
if alpha != tt.expectedAlpha {
|
||||
t.Errorf("AlphaForK(%d) = %d, want %d",
|
||||
tt.k, alpha, tt.expectedAlpha)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestParameterValidation verifies parameter validation with 69% threshold
|
||||
func TestParameterValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -217,27 +126,3 @@ func TestParameterValidation(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkQuorumCalculation benchmarks quorum calculations
|
||||
func BenchmarkQuorumCalculation(b *testing.B) {
|
||||
weights := []uint64{100, 1000, 10000, 100000, 1000000}
|
||||
|
||||
for _, w := range weights {
|
||||
b.Run(fmt.Sprintf("weight_%d", w), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = CalculateQuorum(w)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkHasSuperMajority benchmarks super majority checks
|
||||
func BenchmarkHasSuperMajority(b *testing.B) {
|
||||
totalWeight := uint64(1000000)
|
||||
weight := uint64(690000)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = HasSuperMajority(weight, totalWeight)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-34
@@ -2,41 +2,17 @@ package config
|
||||
|
||||
import "math"
|
||||
|
||||
// Consensus threshold constants (LP-CONSENSUS-69)
|
||||
const (
|
||||
// ConsensusSuperMajority is the 69% threshold for Byzantine fault tolerance
|
||||
// This provides an additional 2% safety margin above traditional 67% BFT
|
||||
ConsensusSuperMajority = 0.69
|
||||
// ConsensusSuperMajority is the 69% agreement threshold (LP-CONSENSUS-69),
|
||||
// two points above the traditional 67% BFT bound.
|
||||
//
|
||||
// This is the float form, for sizing a sample. Deciding whether a given set of
|
||||
// votes carries a quorum is exact integer arithmetic over stake — see
|
||||
// quorum_threshold.go. Do not reintroduce a float predicate here: rounding
|
||||
// twice, two different ways, is how a chain disagrees with itself.
|
||||
const ConsensusSuperMajority = 0.69
|
||||
|
||||
// ConsensusSimpleMajority is the simple majority threshold
|
||||
ConsensusSimpleMajority = 0.51
|
||||
|
||||
// MaxByzantineWeight is the maximum tolerable Byzantine weight (31%)
|
||||
MaxByzantineWeight = 0.31
|
||||
)
|
||||
|
||||
// CalculateQuorum calculates the minimum weight needed for 69% consensus
|
||||
func CalculateQuorum(totalWeight uint64) uint64 {
|
||||
return uint64(math.Ceil(float64(totalWeight) * ConsensusSuperMajority))
|
||||
}
|
||||
|
||||
// HasSuperMajority checks if the given weight meets the 69% threshold
|
||||
func HasSuperMajority(weight, totalWeight uint64) bool {
|
||||
return float64(weight) >= float64(totalWeight)*ConsensusSuperMajority
|
||||
}
|
||||
|
||||
// HasSimpleMajority checks if the given weight meets simple majority
|
||||
func HasSimpleMajority(weight, totalWeight uint64) bool {
|
||||
return float64(weight) >= float64(totalWeight)*ConsensusSimpleMajority
|
||||
}
|
||||
|
||||
// CanTolerateFailure checks if the Byzantine weight is within tolerance
|
||||
func CanTolerateFailure(byzantineWeight, totalWeight uint64) bool {
|
||||
return float64(byzantineWeight) <= float64(totalWeight)*MaxByzantineWeight
|
||||
}
|
||||
|
||||
// AlphaForK calculates the appropriate Alpha value for a given K
|
||||
// to achieve 69% consensus threshold
|
||||
// AlphaForK returns the votes needed out of a K-node sample to reach
|
||||
// ConsensusSuperMajority, rounding up so the threshold is never understated.
|
||||
func AlphaForK(k int) int {
|
||||
return int(math.Ceil(float64(k) * ConsensusSuperMajority))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
// AlphaForK must return the SMALLEST sample-vote count that reaches the 69%
|
||||
// agreement threshold: at or above 69% of K, and one vote fewer must fall short.
|
||||
// Rounding down anywhere here lets a chain finalize under the Byzantine bound;
|
||||
// rounding up more than necessary costs liveness. Checked across every K a
|
||||
// committee is plausibly sized at rather than a handful of sampled rows.
|
||||
func TestAlphaForKIsTheCeilingOfTheThreshold(t *testing.T) {
|
||||
for k := 1; k <= 1000; k++ {
|
||||
alpha := AlphaForK(k)
|
||||
|
||||
if float64(alpha) < float64(k)*ConsensusSuperMajority {
|
||||
t.Fatalf("AlphaForK(%d) = %d, under the %.0f%% threshold", k, alpha, ConsensusSuperMajority*100)
|
||||
}
|
||||
if alpha > 1 && float64(alpha-1) >= float64(k)*ConsensusSuperMajority {
|
||||
t.Fatalf("AlphaForK(%d) = %d, but %d already clears the threshold", k, alpha, alpha-1)
|
||||
}
|
||||
if alpha > k {
|
||||
t.Fatalf("AlphaForK(%d) = %d, more votes than the sample holds", k, alpha)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Every code these enums can hold must render to a distinct string. Two codes
|
||||
// sharing a name makes a policy log or a config dump ambiguous about which
|
||||
// scheme a chain is actually running — and in particular an unrecognized code
|
||||
// must never render as one of the canonical names, which is what would let an
|
||||
// unsupported scheme read as a supported one.
|
||||
func TestSchemeNamesAreUniquePerCode(t *testing.T) {
|
||||
for name, render := range map[string]func(int) string{
|
||||
"PQMode": func(i int) string { return PQMode(i).String() },
|
||||
"HashSuiteID": func(i int) string { return HashSuiteID(i).String() },
|
||||
"SigSchemeID": func(i int) string { return SigSchemeID(i).String() },
|
||||
"ProofPolicyID": func(i int) string { return ProofPolicyID(i).String() },
|
||||
"ProofBackendID": func(i int) string { return ProofBackendID(i).String() },
|
||||
"IdentitySchemeID": func(i int) string { return IdentitySchemeID(i).String() },
|
||||
"WalletSchemeID": func(i int) string { return WalletSchemeID(i).String() },
|
||||
"TxSchemeID": func(i int) string { return TxSchemeID(i).String() },
|
||||
"ContractAuthID": func(i int) string { return ContractAuthID(i).String() },
|
||||
"KeyExchangeID": func(i int) string { return KeyExchangeID(i).String() },
|
||||
"RecoverySchemeID": func(i int) string { return RecoverySchemeID(i).String() },
|
||||
"CertMode": func(i int) string { return CertMode(i).String() },
|
||||
"CertVariant": func(i int) string { return CertVariant(i).String() },
|
||||
"ProfileID": func(i int) string { return ProfileID(i).String() },
|
||||
"ProofFormatID": func(i int) string { return ProofFormatID(i).String() },
|
||||
"VerifierID": func(i int) string { return VerifierID(i).String() },
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
seen := make(map[string]int, 256)
|
||||
for code := 0; code < 256; code++ {
|
||||
s := render(code)
|
||||
if prev, dup := seen[s]; dup {
|
||||
t.Errorf("codes %d and %d both render %q", prev, code, s)
|
||||
}
|
||||
seen[s] = code
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The derived properties of a PQ mode must stay defined for a code that is not
|
||||
// one: an unrecognized mode falls back to the most conservative answer rather
|
||||
// than inheriting whatever the previous case arm returned.
|
||||
func TestUnknownPQModeFallsBackConservatively(t *testing.T) {
|
||||
const unknown = PQMode(0xFE)
|
||||
|
||||
if got := unknown.String(); got != fmt.Sprintf("pq-mode(%d)", uint8(unknown)) {
|
||||
t.Errorf("String() = %q, want it to name the unrecognized code", got)
|
||||
}
|
||||
// PolicyQuorum (1) claims no PQ witness set. An unrecognized mode must land
|
||||
// here and not on 4/5/6, which would advertise a witness it cannot produce.
|
||||
if got := unknown.PolicyID(); got != 1 {
|
||||
t.Errorf("PolicyID() = %d, want 1: an unrecognized mode must claim no PQ witness", got)
|
||||
}
|
||||
if got := unknown.HashSuiteID(); got.String() == "" {
|
||||
t.Error("HashSuiteID() rendered empty for an unrecognized mode")
|
||||
}
|
||||
if got := unknown.SigSchemeID(); got.String() == "" {
|
||||
t.Error("SigSchemeID() rendered empty for an unrecognized mode")
|
||||
}
|
||||
if got := unknown.DKGRequired(); got != "unknown" {
|
||||
t.Errorf("DKGRequired() = %q, want %q", got, "unknown")
|
||||
}
|
||||
}
|
||||
|
||||
// A policy whose variant is neither hybrid nor strict has no wire name, and the
|
||||
// (PQ-off, strict) pair is the one defined combination that is also nameless —
|
||||
// both must return empty so a caller that skipped Validate cannot transmit a
|
||||
// name for a policy that has none.
|
||||
func TestPolicyWithoutAWireNameReturnsEmpty(t *testing.T) {
|
||||
for name, cp := range map[string]CertPolicy{
|
||||
"unknown variant": {Mode: CertModeStrict, Variant: CertVariant(9)},
|
||||
"off and strict": {Mode: CertModeOff, Variant: CertVariantStrict},
|
||||
} {
|
||||
if got := cp.WireName(); got != "" {
|
||||
t.Errorf("%s: WireName() = %q, want empty", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An unrecognized mode has no floor latency to speak of; reporting a defined
|
||||
// mode's floor would let it pass a timeout check it was never measured for.
|
||||
func TestUnknownCertModeHasNoLatencyFloor(t *testing.T) {
|
||||
if got := CertMode(9).expectedFloorLatencyMs(); got != 0 {
|
||||
t.Errorf("expectedFloorLatencyMs() = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Fault tolerance is a count of validators, so a sample below one tolerates
|
||||
// zero faults. Breaks if the formula is allowed to go negative, which would
|
||||
// make every downstream f>=1 check pass by underflow.
|
||||
func TestFaultToleranceNeverGoesNegative(t *testing.T) {
|
||||
for k := -5; k <= 21; k++ {
|
||||
p := MainnetParams()
|
||||
p.K = k
|
||||
if f := p.ByzantineFaultTolerance(); f < 0 {
|
||||
t.Errorf("K=%d gives f=%d", k, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The seven nameable (mode, variant) pairs each get their canonical wire name,
|
||||
// hybrid unprefixed and strict prefixed. Breaks if a strict policy ever
|
||||
// transmits under a hybrid name, which would let a peer read a PQ-only
|
||||
// requirement as one that still admits the classical leg.
|
||||
func TestWireNamesDistinguishStrictFromHybrid(t *testing.T) {
|
||||
for mode, hybrid := range map[CertMode]string{
|
||||
CertModeOff: "PQ-off",
|
||||
CertModeFast: "PQ-fast",
|
||||
CertModeStrict: "PQ-strict",
|
||||
CertModeHeavy: "PQ-heavy",
|
||||
} {
|
||||
if got := (CertPolicy{Mode: mode, Variant: CertVariantHybrid}).WireName(); got != hybrid {
|
||||
t.Errorf("hybrid %v: got %q, want %q", mode, got, hybrid)
|
||||
}
|
||||
if mode == CertModeOff {
|
||||
continue // (PQ-off, strict) is the nameless eighth slot
|
||||
}
|
||||
if got, want := (CertPolicy{Mode: mode, Variant: CertVariantStrict}).WireName(), "strict-"+hybrid; got != want {
|
||||
t.Errorf("strict %v: got %q, want %q", mode, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package config
|
||||
|
||||
// FPCConfig represents Firing Photon Cannon configuration
|
||||
type FPCConfig struct {
|
||||
// Enable indicates whether FPC is enabled (default: true)
|
||||
Enable bool
|
||||
|
||||
// Rounds is the number of FPC rounds to run
|
||||
Rounds int
|
||||
|
||||
// Threshold is the confidence threshold for FPC
|
||||
Threshold float64
|
||||
|
||||
// SampleSize is the number of nodes to sample per round
|
||||
SampleSize int
|
||||
}
|
||||
|
||||
// DefaultFPC returns the default FPC configuration
|
||||
func DefaultFPC() FPCConfig {
|
||||
return FPCConfig{
|
||||
Enable: true, // FPC enabled by default
|
||||
Rounds: 10,
|
||||
Threshold: 0.8,
|
||||
SampleSize: 20,
|
||||
}
|
||||
}
|
||||
|
||||
// WithFPC adds FPC configuration to Parameters
|
||||
func (p Parameters) WithFPC(fpc FPCConfig) Parameters {
|
||||
return p
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// MockValidator represents a validator in the network
|
||||
type MockValidator struct {
|
||||
ID string
|
||||
Weight uint64
|
||||
Voted bool
|
||||
Vote bool // true for yes, false for no
|
||||
}
|
||||
|
||||
// MockNetwork simulates a network with 69% threshold
|
||||
type MockNetwork struct {
|
||||
validators map[string]*MockValidator
|
||||
totalWeight uint64
|
||||
votes map[string]uint64 // vote ID -> accumulated weight
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewMockNetwork creates a network with given validators
|
||||
func NewMockNetwork(validatorWeights map[string]uint64) *MockNetwork {
|
||||
n := &MockNetwork{
|
||||
validators: make(map[string]*MockValidator),
|
||||
votes: make(map[string]uint64),
|
||||
}
|
||||
|
||||
for id, weight := range validatorWeights {
|
||||
n.validators[id] = &MockValidator{
|
||||
ID: id,
|
||||
Weight: weight,
|
||||
}
|
||||
n.totalWeight += weight
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// SimulateVoting simulates a voting round with exact weight
|
||||
func (n *MockNetwork) SimulateVoting(voteID string, supportPercentage float64) bool {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
|
||||
// For testing, we simulate exact voting weight
|
||||
votedWeight := uint64(float64(n.totalWeight) * supportPercentage)
|
||||
n.votes[voteID] = votedWeight
|
||||
|
||||
// Check if consensus achieved with 69% threshold
|
||||
return HasSuperMajority(votedWeight, n.totalWeight)
|
||||
}
|
||||
|
||||
// TestIntegrationSmallNetwork tests 69% threshold in small network
|
||||
func TestIntegrationSmallNetwork(t *testing.T) {
|
||||
// Create a network with fine-grained weights for better precision
|
||||
network := NewMockNetwork(map[string]uint64{
|
||||
"node1": 100,
|
||||
"node2": 100,
|
||||
"node3": 100,
|
||||
"node4": 100,
|
||||
"node5": 100,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
supportPercent float64
|
||||
expectConsensus bool
|
||||
}{
|
||||
{"100% support", 1.0, true},
|
||||
{"80% support", 0.8, true},
|
||||
{"70% support", 0.7, true},
|
||||
{"69% support", 0.69, true},
|
||||
{"68% support", 0.68, false},
|
||||
{"60% support", 0.6, false},
|
||||
{"50% support", 0.5, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
consensus := network.SimulateVoting(tt.name, tt.supportPercent)
|
||||
if consensus != tt.expectConsensus {
|
||||
t.Errorf("SimulateVoting with %s = %v, want %v",
|
||||
tt.name, consensus, tt.expectConsensus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationLargeNetwork tests 69% threshold in large network
|
||||
func TestIntegrationLargeNetwork(t *testing.T) {
|
||||
// Create a 100-node network with varying weights
|
||||
validators := make(map[string]uint64)
|
||||
for i := 0; i < 100; i++ {
|
||||
weight := uint64(10 + i%20) // Weights from 10 to 29
|
||||
validators[string(rune('A'+i))] = weight
|
||||
}
|
||||
|
||||
network := NewMockNetwork(validators)
|
||||
|
||||
// Test various voting scenarios
|
||||
scenarios := []struct {
|
||||
name string
|
||||
supportPercent float64
|
||||
expectConsensus bool
|
||||
}{
|
||||
{"Exact 69%", 0.69, false}, // Due to integer truncation, this is 68.97%
|
||||
{"Just below 69%", 0.689, false},
|
||||
{"Just above 69%", 0.691, true},
|
||||
{"Byzantine maximum 31%", 0.31, false},
|
||||
{"Minimum winning 69.1%", 0.691, true}, // Actual minimum to achieve consensus
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
consensus := network.SimulateVoting(s.name, s.supportPercent)
|
||||
if consensus != s.expectConsensus {
|
||||
votedWeight := uint64(float64(network.totalWeight) * s.supportPercent)
|
||||
actualPercent := float64(votedWeight) * 100 / float64(network.totalWeight)
|
||||
t.Errorf("Large network %s: got %v, want %v (voted %d/%d = %.6f%%)",
|
||||
s.name, consensus, s.expectConsensus,
|
||||
votedWeight, network.totalWeight, actualPercent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationByzantineScenarios tests Byzantine failure scenarios
|
||||
func TestIntegrationByzantineScenarios(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
totalNodes int
|
||||
byzantineNodes int
|
||||
honestVoteRate float64
|
||||
expectConsensus bool
|
||||
}{
|
||||
{
|
||||
name: "30% Byzantine, all honest vote",
|
||||
totalNodes: 100,
|
||||
byzantineNodes: 30,
|
||||
honestVoteRate: 1.0, // 70% > 69%
|
||||
expectConsensus: true,
|
||||
},
|
||||
{
|
||||
name: "31% Byzantine, all honest vote",
|
||||
totalNodes: 100,
|
||||
byzantineNodes: 31,
|
||||
honestVoteRate: 1.0, // 69% = 69%
|
||||
expectConsensus: true,
|
||||
},
|
||||
{
|
||||
name: "32% Byzantine, all honest vote",
|
||||
totalNodes: 100,
|
||||
byzantineNodes: 32,
|
||||
honestVoteRate: 1.0, // 68% < 69%
|
||||
expectConsensus: false,
|
||||
},
|
||||
{
|
||||
name: "20% Byzantine, 90% honest vote",
|
||||
totalNodes: 100,
|
||||
byzantineNodes: 20,
|
||||
honestVoteRate: 0.9, // 72% > 69%
|
||||
expectConsensus: true,
|
||||
},
|
||||
{
|
||||
name: "25% Byzantine, 92% honest vote",
|
||||
totalNodes: 100,
|
||||
byzantineNodes: 25,
|
||||
honestVoteRate: 0.92, // 69% = 69%
|
||||
expectConsensus: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create network
|
||||
validators := make(map[string]uint64)
|
||||
for i := 0; i < tt.totalNodes; i++ {
|
||||
validators[string(rune('A'+i))] = 1 // Equal weight
|
||||
}
|
||||
network := NewMockNetwork(validators)
|
||||
|
||||
// Calculate voting weight
|
||||
honestNodes := tt.totalNodes - tt.byzantineNodes
|
||||
votingNodes := int(float64(honestNodes) * tt.honestVoteRate)
|
||||
supportPercent := float64(votingNodes) / float64(tt.totalNodes)
|
||||
|
||||
consensus := network.SimulateVoting(tt.name, supportPercent)
|
||||
if consensus != tt.expectConsensus {
|
||||
actualPercent := supportPercent * 100
|
||||
t.Errorf("%s: got consensus=%v, want %v (%.1f%% voted)",
|
||||
tt.name, consensus, tt.expectConsensus, actualPercent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationParameterSets tests all parameter sets maintain 69% threshold
|
||||
func TestIntegrationParameterSets(t *testing.T) {
|
||||
paramSets := []struct {
|
||||
name string
|
||||
params Parameters
|
||||
}{
|
||||
{"Default", DefaultParams()},
|
||||
{"Mainnet", MainnetParams()},
|
||||
{"Testnet", TestnetParams()},
|
||||
{"Local", LocalParams()},
|
||||
{"XChain", XChainParams()},
|
||||
}
|
||||
|
||||
for _, ps := range paramSets {
|
||||
t.Run(ps.name, func(t *testing.T) {
|
||||
// Verify Alpha meets minimum threshold (2/3 for local/xchain, 69% for others)
|
||||
minAlpha := 0.66
|
||||
if ps.params.Alpha < minAlpha || ps.params.Alpha > 1.0 {
|
||||
t.Errorf("%s: Alpha %.3f not in valid range [%.2f, 1.0]", ps.name, ps.params.Alpha, minAlpha)
|
||||
}
|
||||
|
||||
// Verify AlphaPreference meets threshold (2/3 minimum)
|
||||
if ps.params.K > 0 && ps.params.AlphaPreference > 0 {
|
||||
actualPercent := float64(ps.params.AlphaPreference) / float64(ps.params.K)
|
||||
if actualPercent < 0.66 {
|
||||
t.Errorf("%s: AlphaPreference %d/%d = %.1f%% < 66%%",
|
||||
ps.name, ps.params.AlphaPreference, ps.params.K, actualPercent*100)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify parameters are valid
|
||||
if err := ps.params.Valid(); err != nil {
|
||||
t.Errorf("%s: Invalid parameters: %v", ps.name, err)
|
||||
}
|
||||
|
||||
// Simulate consensus with these parameters
|
||||
network := NewMockNetwork(map[string]uint64{
|
||||
"v1": 100, "v2": 100, "v3": 100, "v4": 100, "v5": 100,
|
||||
})
|
||||
|
||||
// Test that 69% achieves consensus
|
||||
if !network.SimulateVoting("test-69", 0.69) {
|
||||
t.Errorf("%s: 69%% support should achieve consensus", ps.name)
|
||||
}
|
||||
|
||||
// Test that 68% does not achieve consensus
|
||||
if network.SimulateVoting("test-68", 0.68) {
|
||||
t.Errorf("%s: 68%% support should not achieve consensus", ps.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationConcurrentVoting tests concurrent voting scenarios
|
||||
func TestIntegrationConcurrentVoting(t *testing.T) {
|
||||
network := NewMockNetwork(map[string]uint64{
|
||||
"v1": 100, "v2": 150, "v3": 200, "v4": 250, "v5": 300,
|
||||
})
|
||||
|
||||
const numVotes = 100
|
||||
results := make(chan bool, numVotes)
|
||||
|
||||
// Simulate concurrent voting rounds
|
||||
for i := 0; i < numVotes; i++ {
|
||||
go func(voteNum int) {
|
||||
// Vary support percentage
|
||||
support := 0.68 + float64(voteNum%3)*0.01 // 68%, 69%, or 70%
|
||||
voteID := string(rune('A' + voteNum))
|
||||
consensus := network.SimulateVoting(voteID, support)
|
||||
|
||||
// Only 69% and 70% should achieve consensus
|
||||
expectedConsensus := support >= 0.69
|
||||
if consensus != expectedConsensus {
|
||||
t.Errorf("Vote %d with %.0f%% support: got %v, want %v",
|
||||
voteNum, support*100, consensus, expectedConsensus)
|
||||
}
|
||||
|
||||
results <- consensus
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all votes
|
||||
consensusCount := 0
|
||||
for i := 0; i < numVotes; i++ {
|
||||
if <-results {
|
||||
consensusCount++
|
||||
}
|
||||
}
|
||||
|
||||
// About 2/3 should achieve consensus (69% and 70% cases)
|
||||
expectedCount := numVotes * 2 / 3
|
||||
tolerance := numVotes / 10 // 10% tolerance
|
||||
|
||||
if consensusCount < expectedCount-tolerance || consensusCount > expectedCount+tolerance {
|
||||
t.Errorf("Consensus count %d not near expected %d±%d",
|
||||
consensusCount, expectedCount, tolerance)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkIntegrationVoting benchmarks voting performance
|
||||
func BenchmarkIntegrationVoting(b *testing.B) {
|
||||
network := NewMockNetwork(map[string]uint64{
|
||||
"v1": 100, "v2": 100, "v3": 100, "v4": 100, "v5": 100,
|
||||
"v6": 100, "v7": 100, "v8": 100, "v9": 100, "v10": 100,
|
||||
})
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = network.SimulateVoting("bench", 0.69)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkIntegrationLargeNetwork benchmarks large network voting
|
||||
func BenchmarkIntegrationLargeNetwork(b *testing.B) {
|
||||
validators := make(map[string]uint64)
|
||||
for i := 0; i < 1000; i++ {
|
||||
validators[string(rune(i))] = uint64(100 + i%100)
|
||||
}
|
||||
network := NewMockNetwork(validators)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = network.SimulateVoting("bench", 0.69)
|
||||
}
|
||||
}
|
||||
@@ -149,31 +149,6 @@ func TestBurstParams(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithBlockTime(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
blockTime time.Duration
|
||||
wantRound time.Duration
|
||||
}{
|
||||
{"1ms ultra-fast", 1 * time.Millisecond, 5 * time.Millisecond},
|
||||
{"5ms very fast", 5 * time.Millisecond, 15 * time.Millisecond},
|
||||
{"10ms fast", 10 * time.Millisecond, 26 * time.Millisecond},
|
||||
{"100ms normal", 100 * time.Millisecond, 251 * time.Millisecond},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := DefaultParams().WithBlockTime(tt.blockTime)
|
||||
if p.BlockTime != tt.blockTime {
|
||||
t.Errorf("BlockTime not set: got %v, want %v", p.BlockTime, tt.blockTime)
|
||||
}
|
||||
if p.RoundTO != tt.wantRound {
|
||||
t.Errorf("RoundTO not adjusted: got %v, want %v", p.RoundTO, tt.wantRound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -392,11 +367,3 @@ func BenchmarkValidate(b *testing.B) {
|
||||
_ = p.Validate()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWithBlockTime(b *testing.B) {
|
||||
p := DefaultParams()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = p.WithBlockTime(1 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A PQ profile must never hand back a cert policy that still requires the
|
||||
// classical BLS leg — that is the downgrade this whole profile mechanism exists
|
||||
// to prevent. A non-PQ profile keeps BLS alongside the PQ legs (defence in
|
||||
// depth), so the leg set must differ by exactly that leg.
|
||||
func TestPQProfileYieldsNoClassicalLeg(t *testing.T) {
|
||||
strict := StrictPQ()
|
||||
if !strict.IsPQ() {
|
||||
t.Fatal("StrictPQ() is not a PQ profile; the rest of this test is meaningless")
|
||||
}
|
||||
if cp := strict.CertPolicy(); cp.Variant != CertVariantStrict {
|
||||
t.Errorf("PQ profile gave variant %v, want %v", cp.Variant, CertVariantStrict)
|
||||
} else if hasLeg(cp.RequiredLegs(), LegBLS) {
|
||||
t.Errorf("PQ profile still requires the classical leg: %v", cp.RequiredLegs())
|
||||
}
|
||||
|
||||
permissive := Permissive()
|
||||
if permissive.IsPQ() {
|
||||
t.Fatal("Permissive() reports PQ; this test can no longer reach the hybrid branch")
|
||||
}
|
||||
cp := permissive.CertPolicy()
|
||||
if cp.Variant != CertVariantHybrid {
|
||||
t.Errorf("non-PQ profile gave variant %v, want %v", cp.Variant, CertVariantHybrid)
|
||||
}
|
||||
if !hasLeg(cp.RequiredLegs(), LegBLS) {
|
||||
t.Errorf("hybrid policy dropped the classical fast-path leg: %v", cp.RequiredLegs())
|
||||
}
|
||||
if cp.Mode != CertModeStrict || cp.Fallback != cp.Mode {
|
||||
t.Errorf("fallback %v must equal mode %v: the profile implies no temporal degradation", cp.Fallback, cp.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func hasLeg(legs []LegName, want LegName) bool {
|
||||
for _, l := range legs {
|
||||
if l == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// The decentralisation target is a mainnet-only report at exactly K>=11 (f>=3).
|
||||
// Breaks if the boundary moves, or if the report starts firing on a network it
|
||||
// was never meant to gate — it is consulted at chain start, so a false positive
|
||||
// on a devnet is a spurious operator alarm.
|
||||
func TestDecentralizationTargetIsMainnetOnlyAtElevenValidators(t *testing.T) {
|
||||
for _, k := range []int{1, 5, 10, 11, 21} {
|
||||
p := MainnetParams()
|
||||
p.K = k
|
||||
|
||||
err := p.MeetsDecentralizationTarget(1)
|
||||
if wantErr := k < 11; wantErr != (err != nil) {
|
||||
t.Errorf("mainnet K=%d: err=%v, want error=%v", k, err, wantErr)
|
||||
} else if wantErr && !errors.Is(err, ErrKBelowMainnetTarget) {
|
||||
t.Errorf("mainnet K=%d: got %v, want ErrKBelowMainnetTarget", k, err)
|
||||
}
|
||||
|
||||
for _, networkID := range []uint32{0, 2, 3, 1337} {
|
||||
if err := p.MeetsDecentralizationTarget(networkID); err != nil {
|
||||
t.Errorf("network %d K=%d: reported %v, want no report off mainnet", networkID, k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HalfStakeFloor names the value a majority must STRICTLY exceed, so the floor
|
||||
// itself is never a majority and one unit above it always is. An off-by-one here
|
||||
// ignites Nova on half the stake — the safety bug this floor exists to prevent.
|
||||
func TestHalfStakeFloorIsNeverItselfAMajority(t *testing.T) {
|
||||
for total := uint64(0); total <= 1000; total++ {
|
||||
floor := HalfStakeFloor(total)
|
||||
|
||||
if 2*floor > total {
|
||||
t.Fatalf("total=%d: floor %d is already a majority", total, floor)
|
||||
}
|
||||
if total > 0 && 2*(floor+1) <= total {
|
||||
t.Fatalf("total=%d: floor %d leaves %d short of a majority", total, floor, floor+1)
|
||||
}
|
||||
}
|
||||
|
||||
// The case the boundary is really about: with two units of stake, one unit
|
||||
// is half and must not carry the vote.
|
||||
if got := HalfStakeFloor(2); got != 1 {
|
||||
t.Fatalf("HalfStakeFloor(2) = %d, want 1 so that a single unit does not exceed it", got)
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,6 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PQMode selects the post-quantum signature stack the consensus engine layers
|
||||
@@ -604,16 +602,6 @@ func (b ProofBackendID) IsProductionPQ() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsDirectVerifiable reports whether this backend is verified directly by
|
||||
// a full node — no succinct proof, no recursion, no trusted setup. The
|
||||
// certificate carries the per-signer FIPS signatures and a Merkle/weight
|
||||
// predicate; the verifier replays them. Distinguishes the
|
||||
// "full-node-verifiable mode" from the succinct (STARK/SNARK) backends so
|
||||
// audit tooling can assert which trust model a cert envelope rides under.
|
||||
func (b ProofBackendID) IsDirectVerifiable() bool {
|
||||
return b == ProofBackendDirectWeightedQuorum
|
||||
}
|
||||
|
||||
// IsForbiddenInPQMode mirrors ProofPolicyID.IsForbiddenInPQMode at the
|
||||
// backend layer.
|
||||
func (b ProofBackendID) IsForbiddenInPQMode() bool {
|
||||
@@ -689,68 +677,6 @@ func (m PQMode) DKGRequired() string {
|
||||
}
|
||||
}
|
||||
|
||||
// ParsePQMode parses a canonical mode string (case-insensitive). Every
|
||||
// accepted alias names an actual component in the cert; we deliberately
|
||||
// don't accept counting words ("triple", "double") because they tell
|
||||
// callers nothing about what's signed.
|
||||
//
|
||||
// canonical aliases (component-named only)
|
||||
// "bls" (BLS) "classical", "bls-only"
|
||||
// "nasua" (BLS + Corona) "rt", "academic", "bls-rt", "bls-q", "sha256-rt"
|
||||
// "pulsar" (BLS + Pulsar) "sha3-rt", "production-rt", "bls-pulsar"
|
||||
// "quasar" (BLS + Pulsar + Z) "rollup", "groth16", "zk",
|
||||
// "bls-z", "bls-zk", "bls-groth16",
|
||||
// "z-chain", "pulsar-z"
|
||||
// "mldsa" (BLS + ML-DSA raw) "audit", "bls-mldsa", "bls+mldsa"
|
||||
func ParsePQMode(s string) (PQMode, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||
case "", "bls", "bls-only", "classical":
|
||||
return PQModeBLS, nil
|
||||
case "nasua", "rt", "academic", "bls-rt", "bls-q", "sha256-rt":
|
||||
return PQModeNasua, nil
|
||||
case "pulsar", "sha3-rt", "production-rt", "bls-pulsar":
|
||||
return PQModePulsar, nil
|
||||
case "quasar", "rollup", "groth16", "zk",
|
||||
"bls-z", "bls-zk", "bls-groth16", "z-chain", "pulsar-z":
|
||||
return PQModeQuasar, nil
|
||||
case "mldsa", "audit", "bls-mldsa", "bls+mldsa":
|
||||
return PQModeMLDSA, nil
|
||||
default:
|
||||
return PQModeBLS, fmt.Errorf("unknown PQMode %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
// PQModeFromEnv resolves the consensus PQ mode from CONSENSUS_PQ_MODE.
|
||||
// The legacy LUX_-prefixed name is no longer honoured — there is one
|
||||
// canonical, unprefixed env var.
|
||||
//
|
||||
// Empty / unset returns the supplied default. Invalid values return def + error.
|
||||
func PQModeFromEnv(def PQMode) (PQMode, error) {
|
||||
v := os.Getenv("CONSENSUS_PQ_MODE")
|
||||
if v == "" {
|
||||
return def, nil
|
||||
}
|
||||
mode, err := ParsePQMode(v)
|
||||
if err != nil {
|
||||
return def, err
|
||||
}
|
||||
return mode, nil
|
||||
}
|
||||
|
||||
// PQModeFromBool collapses the enum onto a single boolean for operators
|
||||
// who don't want to pick a mode explicitly:
|
||||
//
|
||||
// true -> PQModeQuasar // BLS + Pulsar + Z-Chain Groth16(ML-DSA)
|
||||
// false -> PQModeBLS // classical fast path
|
||||
//
|
||||
// Use the explicit constants for the middle modes (Corona, Pulsar, MLDSA).
|
||||
func PQModeFromBool(postQuantum bool) PQMode {
|
||||
if postQuantum {
|
||||
return PQModeQuasar
|
||||
}
|
||||
return PQModeBLS
|
||||
}
|
||||
|
||||
// IsPostQuantum reports whether the mode carries any PQ witness on top of
|
||||
// BLS. False only for PQModeBLS.
|
||||
func (m PQMode) IsPostQuantum() bool {
|
||||
|
||||
@@ -26,92 +26,6 @@ func TestPQMode_String(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePQMode_Canonical accepts every canonical lower-case name.
|
||||
func TestParsePQMode_Canonical(t *testing.T) {
|
||||
for _, m := range []PQMode{
|
||||
PQModeBLS, PQModeNasua, PQModePulsar, PQModeQuasar, PQModeMLDSA,
|
||||
} {
|
||||
got, err := ParsePQMode(m.String())
|
||||
if err != nil {
|
||||
t.Errorf("ParsePQMode(%q) errored: %v", m.String(), err)
|
||||
continue
|
||||
}
|
||||
if got != m {
|
||||
t.Errorf("ParsePQMode(%q) = %v, want %v", m.String(), got, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePQMode_Aliases ensures legacy env strings still parse to the
|
||||
// right mode. Drop entries here only when the alias has been removed
|
||||
// from a major release.
|
||||
func TestParsePQMode_Aliases(t *testing.T) {
|
||||
cases := []struct {
|
||||
alias string
|
||||
want PQMode
|
||||
}{
|
||||
// BLS
|
||||
{"", PQModeBLS},
|
||||
{"BLS", PQModeBLS},
|
||||
{"bls-only", PQModeBLS},
|
||||
{"classical", PQModeBLS},
|
||||
|
||||
// Corona (academic, BLAKE3, trusted dealer)
|
||||
{"rt", PQModeNasua},
|
||||
{"academic", PQModeNasua},
|
||||
{"bls-rt", PQModeNasua},
|
||||
{"sha256-rt", PQModeNasua},
|
||||
|
||||
// Pulsar (production fork, SHA-3, Pedersen DKG)
|
||||
{"sha3-rt", PQModePulsar},
|
||||
{"production-rt", PQModePulsar},
|
||||
{"bls-pulsar", PQModePulsar},
|
||||
|
||||
// Quasar — component-named aliases only (no "triple"; tells callers
|
||||
// nothing about what's in the cert).
|
||||
{"rollup", PQModeQuasar},
|
||||
{"groth16", PQModeQuasar},
|
||||
{"bls-z", PQModeQuasar},
|
||||
{"bls-zk", PQModeQuasar},
|
||||
{"z-chain", PQModeQuasar},
|
||||
{"pulsar-z", PQModeQuasar},
|
||||
|
||||
// MLDSA fallback
|
||||
{"audit", PQModeMLDSA},
|
||||
{"bls-mldsa", PQModeMLDSA},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := ParsePQMode(c.alias)
|
||||
if err != nil {
|
||||
t.Errorf("ParsePQMode(%q) errored: %v", c.alias, err)
|
||||
continue
|
||||
}
|
||||
if got != c.want {
|
||||
t.Errorf("ParsePQMode(%q) = %v, want %v", c.alias, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePQMode_Unknown(t *testing.T) {
|
||||
if _, err := ParsePQMode("nonsense"); err == nil {
|
||||
t.Errorf("ParsePQMode(\"nonsense\") accepted; want error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePQMode_NoCountingWords pins the policy that mode names must
|
||||
// describe what's in the cert. "triple" / "triple-quantum" / "double" /
|
||||
// "stack" tell a caller nothing about which witnesses are signed and are
|
||||
// rejected. Drop a name from the rejected list only when there is a
|
||||
// concrete deployment requiring it AND a HIP amendment justifying it.
|
||||
func TestParsePQMode_NoCountingWords(t *testing.T) {
|
||||
rejected := []string{"triple", "triple-quantum", "double", "stack", "all", "max"}
|
||||
for _, s := range rejected {
|
||||
if _, err := ParsePQMode(s); err == nil {
|
||||
t.Errorf("ParsePQMode(%q) accepted; counting words must be rejected per HIP-0077", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPQMode_PolicyID pins the wire policy ID per mode. Anyone changing
|
||||
// the wire layer must update this table at the same time.
|
||||
func TestPQMode_PolicyID(t *testing.T) {
|
||||
@@ -721,16 +635,6 @@ func TestPQMode_SuitableForPublicChain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestPQModeFromBool pins the boolean-shorthand contract.
|
||||
func TestPQModeFromBool(t *testing.T) {
|
||||
if got := PQModeFromBool(true); got != PQModeQuasar {
|
||||
t.Errorf("PQModeFromBool(true) = %v, want PQModeQuasar", got)
|
||||
}
|
||||
if got := PQModeFromBool(false); got != PQModeBLS {
|
||||
t.Errorf("PQModeFromBool(false) = %v, want PQModeBLS", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPQMode_IsPostQuantum is true for everything except plain BLS.
|
||||
func TestPQMode_IsPostQuantum(t *testing.T) {
|
||||
cases := []struct {
|
||||
@@ -749,45 +653,3 @@ func TestPQMode_IsPostQuantum(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPQModeFromEnv covers the env-var override path. CONSENSUS_PQ_MODE is
|
||||
// the one and only env var honoured; the legacy LUX_-prefixed name is gone.
|
||||
func TestPQModeFromEnv(t *testing.T) {
|
||||
t.Setenv("CONSENSUS_PQ_MODE", "")
|
||||
if got, err := PQModeFromEnv(PQModeQuasar); err != nil || got != PQModeQuasar {
|
||||
t.Errorf("empty env: got (%v, %v), want (PQModeQuasar, nil)", got, err)
|
||||
}
|
||||
|
||||
t.Setenv("CONSENSUS_PQ_MODE", "pulsar")
|
||||
if got, err := PQModeFromEnv(PQModeBLS); err != nil || got != PQModePulsar {
|
||||
t.Errorf("env=pulsar: got (%v, %v), want (PQModePulsar, nil)", got, err)
|
||||
}
|
||||
|
||||
t.Setenv("CONSENSUS_PQ_MODE", "garbage")
|
||||
got, err := PQModeFromEnv(PQModeQuasar)
|
||||
if err == nil {
|
||||
t.Errorf("env=garbage: expected error, got nil; mode=%v", got)
|
||||
}
|
||||
if got != PQModeQuasar {
|
||||
t.Errorf("env=garbage: fell back to %v, want PQModeQuasar (the supplied default)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPQModeFromEnv_LegacyNameIgnored asserts that the dropped legacy
|
||||
// LUX_CONSENSUS_PQ_MODE env var is no longer honoured. Only CONSENSUS_PQ_MODE
|
||||
// affects the resolved mode.
|
||||
func TestPQModeFromEnv_LegacyNameIgnored(t *testing.T) {
|
||||
// Legacy name set, canonical unset: default returned, legacy is dead.
|
||||
t.Setenv("CONSENSUS_PQ_MODE", "")
|
||||
t.Setenv("LUX_CONSENSUS_PQ_MODE", "pulsar")
|
||||
if got, err := PQModeFromEnv(PQModeBLS); err != nil || got != PQModeBLS {
|
||||
t.Errorf("legacy-only: got (%v, %v), want (PQModeBLS, nil) — legacy var must be ignored", got, err)
|
||||
}
|
||||
|
||||
// Both set: canonical wins (legacy is ignored entirely).
|
||||
t.Setenv("CONSENSUS_PQ_MODE", "quasar")
|
||||
t.Setenv("LUX_CONSENSUS_PQ_MODE", "bls")
|
||||
if got, err := PQModeFromEnv(PQModeMLDSA); err != nil || got != PQModeQuasar {
|
||||
t.Errorf("both set: got (%v, %v), want (PQModeQuasar, nil) — canonical must win", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
// Every preset a chain can be started with has to name a quorum that a
|
||||
// two-thirds-honest network can actually reach and that a one-third adversary
|
||||
// cannot: Alpha in [0.66, 1.0], and the integer AlphaPreference at least 66% of
|
||||
// the sample K. A preset below the bound finalizes without agreement; one above
|
||||
// 1.0 can never finalize at all. These are the values node ships, so an edit
|
||||
// that drifts any of them must fail here.
|
||||
func TestPresetsHoldTheByzantineBound(t *testing.T) {
|
||||
for name, params := range map[string]Parameters{
|
||||
"Default": DefaultParams(),
|
||||
"Mainnet": MainnetParams(),
|
||||
"Testnet": TestnetParams(),
|
||||
"Local": LocalParams(),
|
||||
"LocalBFT": LocalBFTParams(),
|
||||
"XChain": XChainParams(),
|
||||
"Single": SingleValidatorParams(),
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if params.Alpha < 0.66 || params.Alpha > 1.0 {
|
||||
t.Errorf("Alpha %.3f outside [0.66, 1.0]", params.Alpha)
|
||||
}
|
||||
if ratio := float64(params.AlphaPreference) / float64(params.K); ratio < 0.66 {
|
||||
t.Errorf("AlphaPreference %d of K=%d is %.1f%%, under 66%%",
|
||||
params.AlphaPreference, params.K, ratio*100)
|
||||
}
|
||||
if err := params.Valid(); err != nil {
|
||||
t.Errorf("preset does not validate: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+7
-70
@@ -32,26 +32,14 @@ import "fmt"
|
||||
type Profile string
|
||||
|
||||
const (
|
||||
ProfileStrict Profile = "strict"
|
||||
ProfilePermissiveName Profile = "permissive"
|
||||
ProfileFIPSx Profile = "fips"
|
||||
profileStrict Profile = "strict"
|
||||
profilePermissive Profile = "permissive"
|
||||
profileFIPS Profile = "fips"
|
||||
)
|
||||
|
||||
// AllProfiles is the canonical list, ordered by increasing enforcement
|
||||
// strictness. Used by config-validator tooling to produce "did you
|
||||
// mean?" hints when an unknown profile is supplied.
|
||||
var AllProfiles = []Profile{
|
||||
ProfilePermissiveName,
|
||||
ProfileStrict,
|
||||
ProfileFIPSx,
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer.
|
||||
func (p Profile) String() string { return string(p) }
|
||||
|
||||
// IsValid reports whether p is one of the known profile values.
|
||||
func (p Profile) IsValid() bool { _, err := p.Resolve(); return err == nil }
|
||||
|
||||
// IsStrict reports whether this profile enforces strict crypto policy
|
||||
// — i.e. refuses every classical primitive at every layer. Single
|
||||
// canonical entry point for "should this chain install AllForbidden()
|
||||
@@ -59,14 +47,9 @@ func (p Profile) IsValid() bool { _, err := p.Resolve(); return err == nil }
|
||||
//
|
||||
// Strict and FIPS return true; Permissive returns false.
|
||||
func (p Profile) IsStrict() bool {
|
||||
return p == ProfileStrict || p == ProfileFIPSx
|
||||
return p == profileStrict || p == profileFIPS
|
||||
}
|
||||
|
||||
// IsFIPS reports whether this profile additionally demands FIPS-only
|
||||
// primitives (Strict's PQ posture restricted to FIPS-204 / FIPS-205 /
|
||||
// FIPS-203 with the canonical P3Q backend).
|
||||
func (p Profile) IsFIPS() bool { return p == ProfileFIPSx }
|
||||
|
||||
// Resolve returns the canonical ChainSecurityProfile for this Profile.
|
||||
// The returned profile passes Validate() by construction; the caller
|
||||
// receives a fresh pointer copy and may retain it without affecting
|
||||
@@ -76,11 +59,11 @@ func (p Profile) IsFIPS() bool { return p == ProfileFIPSx }
|
||||
// loaders fail loudly at boot.
|
||||
func (p Profile) Resolve() (*ChainSecurityProfile, error) {
|
||||
switch p {
|
||||
case ProfileStrict:
|
||||
case profileStrict:
|
||||
return StrictPQ(), nil
|
||||
case ProfilePermissiveName:
|
||||
case profilePermissive:
|
||||
return Permissive(), nil
|
||||
case ProfileFIPSx:
|
||||
case profileFIPS:
|
||||
return FIPS(), nil
|
||||
case "":
|
||||
return nil, fmt.Errorf("consensus profile is empty; must be one of: permissive, strict, fips")
|
||||
@@ -88,49 +71,3 @@ func (p Profile) Resolve() (*ChainSecurityProfile, error) {
|
||||
return nil, fmt.Errorf("unknown consensus profile %q; must be one of: permissive, strict, fips", string(p))
|
||||
}
|
||||
}
|
||||
|
||||
// MustResolve is the panic-on-error form of Resolve. Used at boot for
|
||||
// canonical profiles that MUST initialise successfully; never called
|
||||
// on operator-supplied data.
|
||||
func (p Profile) MustResolve() *ChainSecurityProfile {
|
||||
sp, err := p.Resolve()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("config.Profile.MustResolve(%q): %v", string(p), err))
|
||||
}
|
||||
return sp
|
||||
}
|
||||
|
||||
// WireByte returns the wire ProfileID byte for cert-envelope encoding.
|
||||
// Operators do not normally touch this; it is exposed for protocol
|
||||
// layers that encode the profile into wire transcripts.
|
||||
//
|
||||
// Returns 0x00 (ProfileNone) for unknown / invalid profiles; callers
|
||||
// MUST validate via Resolve() first.
|
||||
func (p Profile) WireByte() uint8 {
|
||||
sp, err := p.Resolve()
|
||||
if err != nil {
|
||||
return uint8(ProfileNone)
|
||||
}
|
||||
return uint8(sp.ProfileID)
|
||||
}
|
||||
|
||||
// ProfileFromWireByte is the inverse of WireByte: maps the wire byte
|
||||
// pinned in a cert envelope back to the user-facing Profile string.
|
||||
// Returns the empty Profile and an error for unknown bytes.
|
||||
//
|
||||
// Wire-byte mapping:
|
||||
//
|
||||
// 0x01 → "strict"
|
||||
// 0x02 → "permissive"
|
||||
// 0x03 → "fips"
|
||||
func ProfileFromWireByte(b uint8) (Profile, error) {
|
||||
switch ProfileID(b) {
|
||||
case ProfileStrictPQ:
|
||||
return ProfileStrict, nil
|
||||
case ProfilePermissive:
|
||||
return ProfilePermissiveName, nil
|
||||
case ProfileFIPS:
|
||||
return ProfileFIPSx, nil
|
||||
}
|
||||
return "", fmt.Errorf("unknown wire ProfileID byte 0x%02x", b)
|
||||
}
|
||||
|
||||
+44
-94
@@ -8,120 +8,70 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestProfile_String pins the canonical user-facing strings.
|
||||
func TestProfile_String(t *testing.T) {
|
||||
cases := []struct {
|
||||
p Profile
|
||||
want string
|
||||
}{
|
||||
{ProfilePermissiveName, "permissive"},
|
||||
{ProfileStrict, "strict"},
|
||||
{ProfileFIPSx, "fips"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := c.p.String(); got != c.want {
|
||||
t.Errorf("Profile(%q).String() = %q, want %q", c.p, got, c.want)
|
||||
}
|
||||
}
|
||||
// The three literals operators write in chain config, in increasing order of
|
||||
// enforcement. Spelled out rather than read from the package so that renaming a
|
||||
// constant's value breaks this test instead of silently invalidating every
|
||||
// deployed YAML.
|
||||
var profileLiterals = map[Profile]string{
|
||||
profilePermissive: "permissive",
|
||||
profileStrict: "strict",
|
||||
profileFIPS: "fips",
|
||||
}
|
||||
|
||||
// TestProfile_IsStrict pins the canonical "should this chain refuse
|
||||
// classical primitives at the EVM gate?" predicate.
|
||||
func TestProfile_IsStrict(t *testing.T) {
|
||||
cases := []struct {
|
||||
p Profile
|
||||
want bool
|
||||
}{
|
||||
{ProfileStrict, true},
|
||||
{ProfileFIPSx, true},
|
||||
{ProfilePermissiveName, false},
|
||||
{Profile(""), false},
|
||||
{Profile("nonsense"), false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := c.p.IsStrict(); got != c.want {
|
||||
t.Errorf("Profile(%q).IsStrict() = %v, want %v", c.p, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Each operator string resolves to a security profile that validates, and keeps
|
||||
// its literal spelling. Breaks if a profile is renamed out from under deployed
|
||||
// configs, or resolves to a struct that would be refused at boot.
|
||||
func TestEveryProfileResolvesAndValidates(t *testing.T) {
|
||||
for p, literal := range profileLiterals {
|
||||
t.Run(literal, func(t *testing.T) {
|
||||
if got := p.String(); got != literal {
|
||||
t.Errorf("String() = %q, want %q", got, literal)
|
||||
}
|
||||
|
||||
// TestProfile_Resolve pins the canonical Profile → ChainSecurityProfile
|
||||
// mapping. Each known Profile resolves to a struct that passes Validate.
|
||||
func TestProfile_Resolve(t *testing.T) {
|
||||
for _, p := range AllProfiles {
|
||||
t.Run(string(p), func(t *testing.T) {
|
||||
sp, err := p.Resolve()
|
||||
if err != nil {
|
||||
t.Fatalf("Profile(%q).Resolve(): %v", p, err)
|
||||
t.Fatalf("Resolve(): %v", err)
|
||||
}
|
||||
if sp == nil {
|
||||
t.Fatalf("Profile(%q).Resolve() returned nil profile", p)
|
||||
t.Fatal("Resolve() returned nil profile")
|
||||
}
|
||||
if err := sp.Validate(); err != nil {
|
||||
t.Fatalf("Profile(%q).Resolve().Validate(): %v", p, err)
|
||||
t.Fatalf("resolved profile does not validate: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProfile_Resolve_Unknown asserts the typed-error path for unknown
|
||||
// / empty Profile strings.
|
||||
func TestProfile_Resolve_Unknown(t *testing.T) {
|
||||
cases := []Profile{"", "strict-pq", "STRICT", "hybrid", "bls", "classical", "nonsense"}
|
||||
for _, p := range cases {
|
||||
// Anything that is not one of the three literals must fail loudly at config
|
||||
// load, naming the alternatives — never fall through to a default posture.
|
||||
// Breaks if an unknown or empty profile starts resolving to something.
|
||||
func TestUnknownProfileIsRefused(t *testing.T) {
|
||||
for _, p := range []Profile{"", "strict-pq", "STRICT", "hybrid", "bls", "classical", "nonsense"} {
|
||||
t.Run(string(p), func(t *testing.T) {
|
||||
_, err := p.Resolve()
|
||||
if err == nil {
|
||||
t.Fatalf("Profile(%q).Resolve() returned nil error; want unknown-profile error", p)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "permissive, strict, fips") {
|
||||
t.Errorf("error message does not enumerate canonical values: %v", err)
|
||||
if _, err := p.Resolve(); err == nil {
|
||||
t.Fatal("resolved; want an unknown-profile error")
|
||||
} else if !strings.Contains(err.Error(), "permissive, strict, fips") {
|
||||
t.Errorf("error does not name the alternatives: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProfile_WireByte pins the wire-byte mapping for each Profile.
|
||||
func TestProfile_WireByte(t *testing.T) {
|
||||
cases := []struct {
|
||||
p Profile
|
||||
want uint8
|
||||
}{
|
||||
{ProfileStrict, 0x01},
|
||||
{ProfilePermissiveName, 0x02},
|
||||
{ProfileFIPSx, 0x03},
|
||||
{Profile(""), 0x00},
|
||||
{Profile("nonsense"), 0x00},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := c.p.WireByte(); got != c.want {
|
||||
t.Errorf("Profile(%q).WireByte() = 0x%02x, want 0x%02x", c.p, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestProfileFromWireByte_RoundTrip asserts WireByte and
|
||||
// ProfileFromWireByte are inverses for every known Profile.
|
||||
func TestProfileFromWireByte_RoundTrip(t *testing.T) {
|
||||
for _, p := range AllProfiles {
|
||||
t.Run(string(p), func(t *testing.T) {
|
||||
got, err := ProfileFromWireByte(p.WireByte())
|
||||
if err != nil {
|
||||
t.Fatalf("ProfileFromWireByte(WireByte(%q)): %v", p, err)
|
||||
}
|
||||
if got != p {
|
||||
t.Errorf("round-trip mismatch: got %q, want %q", got, p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestProfileFromWireByte_Unknown asserts unknown bytes return error.
|
||||
func TestProfileFromWireByte_Unknown(t *testing.T) {
|
||||
for _, b := range []uint8{0x00, 0x04, 0x05, 0x80, 0xFF} {
|
||||
_, err := ProfileFromWireByte(b)
|
||||
if err == nil {
|
||||
t.Errorf("ProfileFromWireByte(0x%02x) returned nil error; want unknown-byte error", b)
|
||||
// IsStrict decides whether a chain installs the all-classical-forbidden set at
|
||||
// the precompile boundary. Both PQ postures must answer true and permissive
|
||||
// false; an unknown string must never read as strict-by-accident, nor as
|
||||
// permissive-by-accident — it is refused at Resolve, and this pins the
|
||||
// predicate's answer meanwhile.
|
||||
func TestIsStrictCoversBothPQPostures(t *testing.T) {
|
||||
for p, want := range map[Profile]bool{
|
||||
profileStrict: true,
|
||||
profileFIPS: true,
|
||||
profilePermissive: false,
|
||||
Profile(""): false,
|
||||
Profile("nonsense"): false,
|
||||
} {
|
||||
if got := p.IsStrict(); got != want {
|
||||
t.Errorf("Profile(%q).IsStrict() = %v, want %v", p, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-9
@@ -19,7 +19,7 @@
|
||||
|
||||
package config
|
||||
|
||||
// StrictPQProfile is the canonical Lux mainnet locked profile. NIST-
|
||||
// strictPQProfile is the canonical Lux mainnet locked profile. NIST-
|
||||
// aligned PQ across every axis: SHA3-NIST + STARK_FRI_SHA3_PQ identified
|
||||
// proofs, raw ML-DSA-65 identity, Pulsar-M-65 finality with M-87 reserved
|
||||
// for high-value roots. Five production STARK backends; no dev backends;
|
||||
@@ -30,7 +30,7 @@ package config
|
||||
// every cert envelope binds the same hash.
|
||||
//
|
||||
// ProfileHash is computed in init() — see the bottom of this file.
|
||||
var StrictPQProfile = ChainSecurityProfile{
|
||||
var strictPQProfile = ChainSecurityProfile{
|
||||
ProfileID: uint32(ProfileStrictPQ),
|
||||
ProfileName: ProfileNameStrictPQ,
|
||||
HashSuiteID: HashSuiteSHA3NIST,
|
||||
@@ -80,7 +80,7 @@ var StrictPQProfile = ChainSecurityProfile{
|
||||
RequireTypedTxAuth: true,
|
||||
}
|
||||
|
||||
// PermissiveProfile is the testnet/devnet profile. Accepts the strict-PQ
|
||||
// permissiveProfile is the testnet/devnet profile. Accepts the strict-PQ
|
||||
// production backends plus the dev backends (RISC0_RAW_STARK_DEV,
|
||||
// SP1_CORE_STARK_DEV) so iteration is possible without lying about
|
||||
// security level. Still refuses classical wrappers and trusted-setup
|
||||
@@ -88,7 +88,7 @@ var StrictPQProfile = ChainSecurityProfile{
|
||||
//
|
||||
// NOT a Lux strict-PQ profile. Marketing as such is forbidden by
|
||||
// `ForbidDevProofs == false`.
|
||||
var PermissiveProfile = ChainSecurityProfile{
|
||||
var permissiveProfile = ChainSecurityProfile{
|
||||
ProfileID: uint32(ProfilePermissive),
|
||||
ProfileName: ProfileNamePermissive,
|
||||
HashSuiteID: HashSuiteSHA3NIST,
|
||||
@@ -142,7 +142,7 @@ var PermissiveProfile = ChainSecurityProfile{
|
||||
RequireTypedTxAuth: false,
|
||||
}
|
||||
|
||||
// FIPSProfile is the FIPS-204-only profile. Drops Pulsar-M (production
|
||||
// fipsProfile is the FIPS-204-only profile. Drops Pulsar-M (production
|
||||
// fork of Corona; not yet FIPS-approved) — but the profile still has to
|
||||
// satisfy `FinalitySchemeID.IsPulsarM()`, so for FIPS deployments the
|
||||
// chain DOES use Pulsar-M (FIPS 204-compatible output) at M-65 and M-87.
|
||||
@@ -152,7 +152,7 @@ var PermissiveProfile = ChainSecurityProfile{
|
||||
// stance the FIPS deployment ships is documented per HIP-0077 §"FIPS
|
||||
// profile". This struct is the consensus-layer allow-list; the larger
|
||||
// FIPS posture lives in the operator manifest.
|
||||
var FIPSProfile = ChainSecurityProfile{
|
||||
var fipsProfile = ChainSecurityProfile{
|
||||
ProfileID: uint32(ProfileFIPS),
|
||||
ProfileName: ProfileNameFIPS,
|
||||
HashSuiteID: HashSuiteSHA3NIST,
|
||||
@@ -276,8 +276,8 @@ var ForkClassicalCompatUnsafeProfile = ChainSecurityProfile{
|
||||
// the failing profile so a misconfiguration is immediate and visible in
|
||||
// the boot log.
|
||||
func init() {
|
||||
StrictPQProfile.ProfileHash = StrictPQProfile.MustComputeHash()
|
||||
PermissiveProfile.ProfileHash = PermissiveProfile.MustComputeHash()
|
||||
FIPSProfile.ProfileHash = FIPSProfile.MustComputeHash()
|
||||
strictPQProfile.ProfileHash = strictPQProfile.MustComputeHash()
|
||||
permissiveProfile.ProfileHash = permissiveProfile.MustComputeHash()
|
||||
fipsProfile.ProfileHash = fipsProfile.MustComputeHash()
|
||||
ForkClassicalCompatUnsafeProfile.ProfileHash = ForkClassicalCompatUnsafeProfile.MustComputeHash()
|
||||
}
|
||||
|
||||
+25
-60
@@ -99,16 +99,19 @@ func WeightedSupermajorityThreshold(weights []uint64) int {
|
||||
copy(sorted, weights)
|
||||
sort.Slice(sorted, func(i, j int) bool { return sorted[i] > sorted[j] })
|
||||
|
||||
// The whole set sums to total > floor(2·total/3) for any total ≥ 1, so the
|
||||
// loop always breaks; counting up to the break rather than returning from
|
||||
// inside it leaves no second exit to reason about.
|
||||
var cum uint64
|
||||
for i, w := range sorted {
|
||||
count := 0
|
||||
for _, w := range sorted {
|
||||
count++
|
||||
cum += w
|
||||
if cum > floor { // STRICT > ⅔ — the exact predicate VerifyWeighted uses
|
||||
return i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
// Unreachable for total>0 (the full set sums to total > floor(2·total/3) for
|
||||
// any total ≥ 1), but be explicit and fail-closed to the full set.
|
||||
return len(sorted)
|
||||
return count
|
||||
}
|
||||
|
||||
// EqualStakeSupermajorityThreshold returns the minimum vote count for an
|
||||
@@ -179,14 +182,12 @@ func FeasibleParams(networkID uint32, n int) Parameters {
|
||||
}
|
||||
|
||||
// α = strict-⅔ stake threshold (equal-stake closed form, = heaviest-first
|
||||
// general computation for unit weights), clamped up to the BFT overlap floor.
|
||||
// general computation for unit weights). For every k ≥ 4 this already sits at
|
||||
// or above the BFT overlap floor and at or below k, and the ratio α/k lands in
|
||||
// [0.71, 0.75] — inside the [0.66, 1.0] window Valid() demands. Clamps against
|
||||
// those three bounds could never fire, so they are not here to be reasoned
|
||||
// about; TestFeasibleParamsNeedsNoClamping holds the ground.
|
||||
alpha := EqualStakeSupermajorityThreshold(k)
|
||||
if floor := (Parameters{K: k}).bftQuorumFloor(); alpha < floor {
|
||||
alpha = floor
|
||||
}
|
||||
if alpha > k {
|
||||
alpha = k
|
||||
}
|
||||
|
||||
blockTime, roundTO := liveTimingFor(networkID)
|
||||
|
||||
@@ -194,16 +195,7 @@ func FeasibleParams(networkID uint32, n int) Parameters {
|
||||
p.K = k
|
||||
p.AlphaPreference = alpha
|
||||
p.AlphaConfidence = alpha
|
||||
// Float α (legacy Quasar-compat field) tracks the integer ratio but is
|
||||
// floored at the 0.66 minimum config.Valid() requires. α/K for the live nets
|
||||
// is ≥0.71 (4/5, 8/11, 15/21), comfortably above the floor.
|
||||
p.Alpha = float64(alpha) / float64(k)
|
||||
if p.Alpha < 0.66 {
|
||||
p.Alpha = 0.66
|
||||
}
|
||||
if p.Alpha > 1.0 {
|
||||
p.Alpha = 1.0
|
||||
}
|
||||
p.Beta = 2
|
||||
p.BetaVirtuous = 2
|
||||
if p.BetaRogue < k {
|
||||
@@ -214,30 +206,21 @@ func FeasibleParams(networkID uint32, n int) Parameters {
|
||||
return p
|
||||
}
|
||||
|
||||
// ValidateForLiveValueNetwork validates value/PoS parameters against the LIVE
|
||||
// validator count — the live-aware companion to ValidateForValueNetwork. It is
|
||||
// the manager's fail-closed backstop after FeasibleParams sizes K=liveN.
|
||||
// ValidateForLiveValueNetwork validates value/PoS parameters for a network that
|
||||
// is actually running.
|
||||
//
|
||||
// Why a live-aware form exists: the static ValidateForValueNetwork enforces fixed
|
||||
// tier floors (mainnet K≥11, testnet K≥5) that assume the network HAS that many
|
||||
// validators. When the live set is smaller (e.g. 5 validators on mainnet), a
|
||||
// fixed K≥11 floor makes quorum UNREACHABLE — the committee cannot sample
|
||||
// validators that do not exist — so the floor stops protecting safety and instead
|
||||
// causes a finality outage. The live-aware floor reads the tier minimum as an
|
||||
// ASPIRATION capped by reality: you must sample at least min(tierFloor, liveN)
|
||||
// validators. With K = liveN (FeasibleParams' choice) this always holds, while a
|
||||
// genuinely tiny set still runs at K = liveN — the only safe committee available.
|
||||
// It checks what makes a committee safe — the overlap bound in Valid and at
|
||||
// least single-fault tolerance — and nothing about how many validators exist.
|
||||
// K is a number of stake-weighted draws, not a number of nodes, and a validator
|
||||
// is sampled in proportion to its stake however many there are; a floor that
|
||||
// tied K to the validator headcount gave a validator holding a billionth of the
|
||||
// stake the same standing as one holding a fifth of it, and refused to start a
|
||||
// chain because a small staker had joined.
|
||||
//
|
||||
// The protocol SAFETY invariant is unchanged and still enforced: Valid() asserts
|
||||
// the overlap bound 2α−K ≥ ⌊(K−1)/3⌋+1, and value nets still require f≥1 (K≥4).
|
||||
// Only the decentralization-aspiration numeric floor becomes live-relative.
|
||||
//
|
||||
// K==1 → rejected (single-validator is a separate explicit regime)
|
||||
// K<4 (f=0) → rejected (no Byzantine tolerance)
|
||||
// K < min(tierFloor, liveN) → rejected (under-sized vs. the live set)
|
||||
// otherwise (overlap bound holds) → admitted
|
||||
// The tier floor is a decentralisation target and is reported by
|
||||
// MeetsDecentralizationTarget, never enforced here: refusing to start does not
|
||||
// add validators, it pins every node on whatever code it last booted.
|
||||
func (p Parameters) ValidateForLiveValueNetwork(networkID uint32, liveN int) error {
|
||||
// Valid() (overlap bound + α∈[0.66,1] + K≥1) is the protocol safety gate.
|
||||
if err := p.Valid(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -245,23 +228,5 @@ func (p Parameters) ValidateForLiveValueNetwork(networkID uint32, liveN int) err
|
||||
if p.ByzantineFaultTolerance() < 1 {
|
||||
return errKTooLowForValueLive(p, networkID)
|
||||
}
|
||||
// Tier aspiration, capped by the live set: you must sample at least the
|
||||
// smaller of the tier floor and however many validators actually exist.
|
||||
tierFloor := 0
|
||||
switch networkID {
|
||||
case constants.MainnetID:
|
||||
tierFloor = 11
|
||||
case constants.TestnetID:
|
||||
tierFloor = 5
|
||||
}
|
||||
if tierFloor > 0 {
|
||||
effective := tierFloor
|
||||
if liveN > 0 && liveN < tierFloor {
|
||||
effective = liveN
|
||||
}
|
||||
if p.K < effective {
|
||||
return errKBelowLiveFloor(p, networkID, liveN, effective)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -205,11 +205,16 @@ func TestLiveValueNetworkRelaxesStaticFloor(t *testing.T) {
|
||||
if err := k3.ValidateForLiveValueNetwork(constants.MainnetID, 3); !errors.Is(err, ErrKTooLowForValue) {
|
||||
t.Fatalf("K=3 (f=0) must be rejected for value even live-aware, got %v", err)
|
||||
}
|
||||
// An operator that under-samples the live set (K=4 while 11 validators exist on
|
||||
// mainnet) is rejected: effective floor = min(11, 11) = 11 > 4.
|
||||
// K is a number of stake-weighted draws, not a headcount. A committee that
|
||||
// tolerates a fault runs however many validators exist — the size of the set
|
||||
// decides how the draws fall, not whether the chain may start. A small staker
|
||||
// joining must never refuse a boot.
|
||||
k4 := Parameters{K: 4, Alpha: 0.75, Beta: 2, AlphaPreference: 3, AlphaConfidence: 3}
|
||||
if err := k4.ValidateForLiveValueNetwork(constants.MainnetID, 11); !errors.Is(err, ErrKBelowLiveFloor) {
|
||||
t.Fatalf("K=4 under-sampling an 11-validator mainnet must be rejected, got %v", err)
|
||||
if err := k4.ValidateForLiveValueNetwork(constants.MainnetID, 11); err != nil {
|
||||
t.Fatalf("K=4 with 11 validators live must be admitted — the set decides the draws, not the boot: %v", err)
|
||||
}
|
||||
if err := k4.ValidateForLiveValueNetwork(constants.MainnetID, 6); err != nil {
|
||||
t.Fatalf("K=4 with 6 validators live must be admitted: %v", err)
|
||||
}
|
||||
// A full 21-validator mainnet at K=21 is admitted (≥ tier floor 11).
|
||||
full := FeasibleParams(constants.MainnetID, 21)
|
||||
|
||||
@@ -19,82 +19,16 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// F51 — `github.com/luxfi/consensus/config` does not build (two CTO
|
||||
// agents collided; schema split).
|
||||
// =============================================================================
|
||||
//
|
||||
// SEVERITY: critical (blocker)
|
||||
//
|
||||
// The Blue agent producing security_profile.go used schema A:
|
||||
// - Field names: AllowedProofBackends / AllowedProofFormats
|
||||
// - Forbid flag: ForbidClassicalSNARKs (plural s)
|
||||
// - Profile constants live in profiles.go (not yet written): the
|
||||
// functions StrictPQ() / Permissive() / FIPS() each
|
||||
// `return &StrictPQProfile` / `&PermissiveProfile` / etc.
|
||||
// - ProfileID is a uint32 field in ChainSecurityProfile (line 254);
|
||||
// the constants ProfileStrictPQ (line 60) are typed ProfileID
|
||||
// (uint8). The constants are not assignable to the field.
|
||||
// - ComputeHash returns ([48]byte, error)
|
||||
//
|
||||
// The Blue agent producing security_profile_test.go used schema B:
|
||||
// - Field names: AllowedBackends / AllowedFormats
|
||||
// - Forbid flag: ForbidClassicalSNARK (no s)
|
||||
// - Tests `if p.ProfileID != ProfileStrictPQ` directly (compile
|
||||
// error: uint32 vs ProfileID)
|
||||
// - Calls `ErrProfileForbiddenPolicy` (not defined; only
|
||||
// ErrProfileFieldInvalid is exported)
|
||||
// - Calls `a.ComputeHash() != b.ComputeHash()` as a value comparison
|
||||
// (the new signature returns (val, err); compile error)
|
||||
//
|
||||
// Result: the package will not vet, will not build, will not test.
|
||||
// This is the F51 finding — the entire locked-profile work needs a
|
||||
// single source of truth across struct field names, error variables,
|
||||
// and constructor return shapes before the rest of the F##s in this
|
||||
// file are runnable.
|
||||
//
|
||||
// PROOF: `cd ~/work/lux/consensus/config && go vet ./...` produces
|
||||
// type-mismatch errors. The proof is the vet output itself.
|
||||
//
|
||||
// FIX: pick ONE schema and apply consistently. Production-grade
|
||||
// choice:
|
||||
// - Plural collection names: AllowedProofBackends / AllowedProofFormats
|
||||
// - Plural forbid name: ForbidClassicalSNARKs (matches every other
|
||||
// plural collection)
|
||||
// - Profile constants typed ProfileID (uint8); the struct field is
|
||||
// also ProfileID (the type). Do NOT make the struct field uint32 —
|
||||
// that is a category error.
|
||||
// - Add profiles.go with the canonical StrictPQProfile /
|
||||
// PermissiveProfile / FIPSProfile constants, computed once
|
||||
// via MustComputeHash at package init.
|
||||
// - Add error variables ErrProfileForbiddenPolicy /
|
||||
// ErrProfileForbiddenBackend / ErrProfileForbiddenFormat /
|
||||
// ErrProfileStrictRequiresTransparent so callers can errors.Is
|
||||
// against the specific failure.
|
||||
// - ComputeHash returns ([48]byte, error); existing callers that
|
||||
// used the no-error variant get a compile-time fix-prompt.
|
||||
//
|
||||
// This file's tests assume the post-fix schema A names. Once the build
|
||||
// is unblocked, every test in this file becomes runnable and the
|
||||
// remaining F##s below will either pass (if the property is upheld)
|
||||
// or fail (if the footgun is still live).
|
||||
func TestF51_PackageBuilds(t *testing.T) {
|
||||
// This test exists to make the build break visible in `go test`
|
||||
// output. If you can read this test name, the package compiled —
|
||||
// good, run the rest of the suite. If you cannot, F51 is unfixed.
|
||||
t.Log("F51 covered: package compiles enough for tests to run")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// F52 — ChainSecurityProfile.Validate exists, but the canonical
|
||||
// StrictPQ constructor that should satisfy it cannot be called
|
||||
// (StrictPQProfile constant not defined; profiles.go missing).
|
||||
// (strictPQProfile constant not defined; profiles.go missing).
|
||||
// =============================================================================
|
||||
//
|
||||
// SEVERITY: critical
|
||||
//
|
||||
// security_profile.go:724 reads `p := StrictPQProfile; return &p`
|
||||
// but the constant StrictPQProfile is not defined anywhere in the
|
||||
// security_profile.go:724 reads `p := strictPQProfile; return &p`
|
||||
// but the constant strictPQProfile is not defined anywhere in the
|
||||
// package. The locked-profile architecture's single source of truth
|
||||
// for "this is what mainnet runs" is therefore vapourware. Any caller
|
||||
// that thinks they have the canonical profile in hand is actually
|
||||
@@ -102,9 +36,9 @@ func TestF51_PackageBuilds(t *testing.T) {
|
||||
//
|
||||
// FIX: write profiles.go that defines:
|
||||
//
|
||||
// var StrictPQProfile = ChainSecurityProfile{ /* all fields */ }
|
||||
// var PermissiveProfile = ChainSecurityProfile{ /* all fields */ }
|
||||
// var FIPSProfile = ChainSecurityProfile{ /* all fields */ }
|
||||
// var strictPQProfile = ChainSecurityProfile{ /* all fields */ }
|
||||
// var permissiveProfile = ChainSecurityProfile{ /* all fields */ }
|
||||
// var fipsProfile = ChainSecurityProfile{ /* all fields */ }
|
||||
//
|
||||
// Then call MustComputeHash on each at init time and write the
|
||||
// returned hash back into ProfileHash so the constant carries its
|
||||
@@ -112,7 +46,7 @@ func TestF51_PackageBuilds(t *testing.T) {
|
||||
func TestF52_CanonicalProfilesExist(t *testing.T) {
|
||||
// We try to obtain each canonical profile; any panic / nil here
|
||||
// is the finding's proof. Use defer/recover to capture the panic
|
||||
// because StrictPQ() panics when StrictPQProfile is missing
|
||||
// because StrictPQ() panics when strictPQProfile is missing
|
||||
// (it's a typed-undefined-identifier compile error in fact, so
|
||||
// this test would not link; but assuming it links the panic check
|
||||
// is the runtime guard).
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A locked profile is refused for exactly one reason per malformation, and the
|
||||
// reason is typed so a boot failure says which axis is wrong. Each row breaks a
|
||||
// canonical strict-PQ profile in one field: if any row stops erroring, a chain
|
||||
// can boot with that field unset or nonsensical, which is the whole failure mode
|
||||
// this validation exists to prevent.
|
||||
func TestMalformedProfileIsRefusedPerField(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
break_ func(*ChainSecurityProfile)
|
||||
want error
|
||||
}{
|
||||
"no name": {func(p *ChainSecurityProfile) { p.ProfileName = "" }, ErrProfileFieldUnset},
|
||||
"no hash suite": {func(p *ChainSecurityProfile) { p.HashSuiteID = HashSuiteNone }, ErrProfileFieldUnset},
|
||||
"unknown hash suite": {func(p *ChainSecurityProfile) { p.HashSuiteID = HashSuiteID(0xEE) }, ErrProfileFieldUnknown},
|
||||
"no identity scheme": {func(p *ChainSecurityProfile) { p.IdentitySchemeID = SigSchemeNone }, ErrProfileFieldUnset},
|
||||
"no finality scheme": {func(p *ChainSecurityProfile) { p.FinalitySchemeID = SigSchemeNone }, ErrProfileFieldUnset},
|
||||
"no proof policy": {func(p *ChainSecurityProfile) { p.ProofPolicyID = ProofPolicyNone }, ErrProfileFieldUnset},
|
||||
"classical policy": {func(p *ChainSecurityProfile) { p.ProofPolicyID = ProofPolicyPLONKKZGForbid }, ErrProfileFieldInvalid},
|
||||
"backend none listed": {func(p *ChainSecurityProfile) {
|
||||
p.AllowedProofBackends = append(p.AllowedProofBackends, ProofBackendNone)
|
||||
}, ErrProfileFieldInvalid},
|
||||
"no formats": {func(p *ChainSecurityProfile) { p.AllowedProofFormats = nil }, ErrProfileFieldUnset},
|
||||
"format none listed": {func(p *ChainSecurityProfile) { p.AllowedProofFormats = append(p.AllowedProofFormats, ProofFormatNone) }, ErrProfileFieldInvalid},
|
||||
"pairings permitted": {func(p *ChainSecurityProfile) { p.ForbidPairings = false }, ErrProfileFieldInvalid},
|
||||
"kzg permitted": {func(p *ChainSecurityProfile) { p.ForbidKZG = false }, ErrProfileFieldInvalid},
|
||||
"trusted setup ok": {func(p *ChainSecurityProfile) { p.ForbidTrustedSetup = false }, ErrProfileFieldInvalid},
|
||||
"classical snarks ok": {func(p *ChainSecurityProfile) { p.ForbidClassicalSNARKs = false }, ErrProfileFieldInvalid},
|
||||
"dev proofs ok": {func(p *ChainSecurityProfile) { p.ForbidDevProofs = false }, ErrProfileFieldInvalid},
|
||||
"fallbacks ok": {func(p *ChainSecurityProfile) { p.ForbidFallbacks = false }, ErrProfileFieldInvalid},
|
||||
"ecdsa contract auth": {func(p *ChainSecurityProfile) { p.ForbidECDSAContractAuth = false }, ErrProfileFieldInvalid},
|
||||
"legacy hash with pq": {func(p *ChainSecurityProfile) { p.HashSuiteID = HashSuiteBLAKE3Legacy }, ErrProfileFieldInvalid},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
p := StrictPQ()
|
||||
if err := p.Validate(); err != nil {
|
||||
t.Fatalf("the canonical profile does not validate; nothing below means anything: %v", err)
|
||||
}
|
||||
|
||||
tc.break_(p)
|
||||
err := p.Validate()
|
||||
if err == nil {
|
||||
t.Fatal("accepted a malformed profile")
|
||||
}
|
||||
if !errors.Is(err, tc.want) {
|
||||
t.Errorf("got %v, want %v", err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A nil profile is refused rather than dereferenced, and a canonical hash is
|
||||
// only computable for a profile that is structurally sound.
|
||||
func TestNilProfileAndUncomputableHashAreRefused(t *testing.T) {
|
||||
var p *ChainSecurityProfile
|
||||
if err := p.Validate(); !errors.Is(err, ErrProfileNil) {
|
||||
t.Errorf("nil profile: got %v, want ErrProfileNil", err)
|
||||
}
|
||||
|
||||
broken := StrictPQ()
|
||||
broken.ProfileName = ""
|
||||
if _, err := broken.ComputeHash(); err == nil {
|
||||
t.Error("hashed a structurally invalid profile")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Error("MustComputeHash returned instead of panicking on an invalid profile")
|
||||
}
|
||||
}()
|
||||
broken.MustComputeHash()
|
||||
}
|
||||
|
||||
// The registry is the one place a wire ProfileID becomes a profile. Every
|
||||
// canonical byte must resolve to a profile that validates, and every other byte
|
||||
// — including the reserved zero — must be refused rather than defaulting to a
|
||||
// posture the sender never asked for.
|
||||
func TestProfileRegistryResolvesOnlyCanonicalBytes(t *testing.T) {
|
||||
for _, id := range []ProfileID{ProfileStrictPQ, ProfilePermissive, ProfileFIPS} {
|
||||
p, err := ProfileByID(id)
|
||||
if err != nil {
|
||||
t.Fatalf("ProfileByID(%v): %v", id, err)
|
||||
}
|
||||
if err := p.Validate(); err != nil {
|
||||
t.Errorf("ProfileByID(%v) gave a profile that does not validate: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range []ProfileID{ProfileNone, ProfileID(0x04), ProfileID(0x80), ProfileID(0xFF)} {
|
||||
if p, err := ProfileByID(id); err == nil {
|
||||
t.Errorf("ProfileByID(0x%02x) resolved to %v; want refusal", uint8(id), p.ProfileName)
|
||||
} else if !errors.Is(err, ErrProfileUnknown) {
|
||||
t.Errorf("ProfileByID(0x%02x): got %v, want ErrProfileUnknown", uint8(id), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The allowlists are allowlists: a backend or format that is not named is
|
||||
// refused even when it is otherwise a perfectly good production PQ choice.
|
||||
// Breaks if either check ever falls through to permitting the unlisted.
|
||||
func TestUnlistedBackendAndFormatAreRefused(t *testing.T) {
|
||||
p := StrictPQ()
|
||||
|
||||
if len(p.AllowedProofBackends) == 0 || len(p.AllowedProofFormats) == 0 {
|
||||
t.Fatal("the canonical profile lists nothing; this test cannot distinguish listed from unlisted")
|
||||
}
|
||||
if !p.AllowsBackend(p.AllowedProofBackends[0]) {
|
||||
t.Error("refused a backend it lists")
|
||||
}
|
||||
if !p.AllowsFormat(p.AllowedProofFormats[0]) {
|
||||
t.Error("refused a format it lists")
|
||||
}
|
||||
|
||||
unlisted := StrictPQ()
|
||||
unlisted.AllowedProofBackends = nil
|
||||
unlisted.AllowedProofFormats = nil
|
||||
if unlisted.AllowsBackend(p.AllowedProofBackends[0]) {
|
||||
t.Error("permitted a backend that is not on the list")
|
||||
}
|
||||
if unlisted.AllowsFormat(p.AllowedProofFormats[0]) {
|
||||
t.Error("permitted a format that is not on the list")
|
||||
}
|
||||
}
|
||||
|
||||
// The proof axis has two independent refusals that the field table above cannot
|
||||
// reach, because each needs a value that is neither the reserved zero nor one of
|
||||
// the explicitly forbidden markers: a policy that is simply not post-quantum,
|
||||
// and a backend that is real but not production-grade. Both must be refused on a
|
||||
// locked profile — an unproven policy or a dev backend on mainnet is the exact
|
||||
// downgrade the allowlists exist to stop.
|
||||
func TestNonPQPolicyAndDevBackendAreRefused(t *testing.T) {
|
||||
unprovenPolicy := StrictPQ()
|
||||
unprovenPolicy.ProofPolicyID = ProofPolicyID(0x01)
|
||||
if err := unprovenPolicy.Validate(); !errors.Is(err, ErrProfileFieldInvalid) {
|
||||
t.Errorf("policy that is not post-quantum: got %v, want ErrProfileFieldInvalid", err)
|
||||
}
|
||||
|
||||
devBackend := StrictPQ()
|
||||
devBackend.AllowedProofBackends = []ProofBackendID{ProofBackendRISC0RawSTARKDev}
|
||||
if err := devBackend.Validate(); !errors.Is(err, ErrProfileFieldInvalid) {
|
||||
t.Errorf("non-production backend: got %v, want ErrProfileFieldInvalid", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The profile hash is a domain-separated SP 800-185 encoding, and its length
|
||||
// prefixes must match the standard byte for byte — protocol/zchain hashes the
|
||||
// same shape independently, so a divergence here silently splits two hashes that
|
||||
// have to agree. The zero length is the case the standard calls out explicitly
|
||||
// and the one no profile field happens to produce today.
|
||||
func TestSP800185LengthPrefixesFollowTheStandard(t *testing.T) {
|
||||
for x, want := range map[uint64]string{
|
||||
0: "\x01\x00",
|
||||
1: "\x01\x01",
|
||||
384: "\x02\x01\x80",
|
||||
65536: "\x03\x01\x00\x00",
|
||||
} {
|
||||
if got := string(leftEncodeSP800185Profile(x)); got != want {
|
||||
t.Errorf("leftEncode(%d) = % x, want % x", x, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
for x, want := range map[uint64]string{
|
||||
0: "\x00\x01",
|
||||
1: "\x01\x01",
|
||||
384: "\x01\x80\x02",
|
||||
65536: "\x01\x00\x00\x03",
|
||||
} {
|
||||
if got := string(rightEncodeSP800185Profile(x)); got != want {
|
||||
t.Errorf("rightEncode(%d) = % x, want % x", x, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -904,14 +904,16 @@ func (p *ChainSecurityProfile) validatePolicy() error {
|
||||
// is SHA-3 internal; binding BLAKE3 at the transcript layer over a
|
||||
// SHA-3 signature is a configuration inconsistency that audit
|
||||
// pipelines should catch at boot. Closes F54.
|
||||
//
|
||||
// The identity scheme carries the same mismatch — raw ML-DSA is SHAKE256
|
||||
// internal — but needs no second check: validateStructural has already
|
||||
// refused every profile whose finality scheme is not Pulsar-M, so the
|
||||
// condition below holds for any BLAKE3-legacy profile that reaches here and
|
||||
// answers for both axes.
|
||||
if p.FinalitySchemeID.IsPulsarM() && p.HashSuiteID == HashSuiteBLAKE3Legacy {
|
||||
return fmt.Errorf("%w: HashSuiteID=BLAKE3_LEGACY paired with FinalitySchemeID=%s (SHA-3 internal); cross-axis mismatch",
|
||||
ErrProfileFieldInvalid, p.FinalitySchemeID.String())
|
||||
}
|
||||
if p.IdentitySchemeID.IsRawMLDSA() && p.HashSuiteID == HashSuiteBLAKE3Legacy {
|
||||
return fmt.Errorf("%w: HashSuiteID=BLAKE3_LEGACY paired with IdentitySchemeID=%s (SHAKE256 internal); cross-axis mismatch",
|
||||
ErrProfileFieldInvalid, p.IdentitySchemeID.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1139,7 +1141,7 @@ func ProfileByID(id ProfileID) (*ChainSecurityProfile, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// StrictPQ returns a fresh pointer copy of StrictPQProfile. The
|
||||
// StrictPQ returns a fresh pointer copy of strictPQProfile. The
|
||||
// returned value is safe for the caller to retain and mutate without
|
||||
// affecting other callers; the canonical immutable value lives in
|
||||
// profiles.go.
|
||||
@@ -1148,20 +1150,20 @@ func ProfileByID(id ProfileID) (*ChainSecurityProfile, error) {
|
||||
// identity comes from chain genesis (chain ID, network ID, NodeID set)
|
||||
// — not from a sibling profile byte. One strict-PQ, one way.
|
||||
func StrictPQ() *ChainSecurityProfile {
|
||||
p := StrictPQProfile
|
||||
p := strictPQProfile
|
||||
return &p
|
||||
}
|
||||
|
||||
// Permissive returns a fresh pointer copy of the Permissive
|
||||
// profile constant defined in profiles.go. Testnet/devnet only.
|
||||
func Permissive() *ChainSecurityProfile {
|
||||
p := PermissiveProfile
|
||||
p := permissiveProfile
|
||||
return &p
|
||||
}
|
||||
|
||||
// FIPS returns a fresh pointer copy of the FIPSProfile constant
|
||||
// FIPS returns a fresh pointer copy of the fipsProfile constant
|
||||
// defined in profiles.go. Strictest FIPS-aligned profile.
|
||||
func FIPS() *ChainSecurityProfile {
|
||||
p := FIPSProfile
|
||||
p := fipsProfile
|
||||
return &p
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ func TestForkClassicalCompatUnsafe_Validate(t *testing.T) {
|
||||
}
|
||||
// Critical: the fork uses the keccak-merkle STARK policy so it MUST
|
||||
// NOT collide with StrictPQ's policy.
|
||||
if ForkClassicalCompatUnsafeProfile.ProofPolicyID == StrictPQProfile.ProofPolicyID {
|
||||
if ForkClassicalCompatUnsafeProfile.ProofPolicyID == strictPQProfile.ProofPolicyID {
|
||||
t.Errorf("fork profile ProofPolicyID must NOT match StrictPQ — that's the whole point")
|
||||
}
|
||||
}
|
||||
@@ -387,9 +387,9 @@ func TestChainSecurityProfile_ComputeHash_Determinism(t *testing.T) {
|
||||
t.Errorf("StrictPQ() hashes inconsistently across calls: %x vs %x", ah, bh)
|
||||
}
|
||||
// And the well-known constant matches.
|
||||
if ah != StrictPQProfile.ProfileHash {
|
||||
if ah != strictPQProfile.ProfileHash {
|
||||
t.Errorf("StrictPQ ComputeHash() differs from init-pinned ProfileHash: got %x want %x",
|
||||
ah, StrictPQProfile.ProfileHash)
|
||||
ah, strictPQProfile.ProfileHash)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,7 +558,7 @@ func TestChainSecurityProfile_ComputeHash_RefusesInvalidProfile(t *testing.T) {
|
||||
// audit-defensible value. The test is a tripwire against future
|
||||
// refactors that introduce zero-default branches into a "secure" profile.
|
||||
func TestStrictPQProfile_NoOperatorFootguns(t *testing.T) {
|
||||
p := StrictPQProfile
|
||||
p := strictPQProfile
|
||||
if p.ProfileID == 0 {
|
||||
t.Errorf("ProfileID is zero")
|
||||
}
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestThresholdEdgeCases tests edge cases for 69% threshold
|
||||
func TestThresholdEdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
totalWeight uint64
|
||||
byzantineWeight uint64
|
||||
voteWeight uint64
|
||||
expectQuorum uint64
|
||||
expectTolerance bool
|
||||
expectConsensus bool
|
||||
}{
|
||||
// Exact thresholds
|
||||
{
|
||||
name: "exact_69_percent",
|
||||
totalWeight: 100,
|
||||
byzantineWeight: 31,
|
||||
voteWeight: 69,
|
||||
expectQuorum: 69,
|
||||
expectTolerance: true,
|
||||
expectConsensus: true,
|
||||
},
|
||||
{
|
||||
name: "just_below_69_percent",
|
||||
totalWeight: 100,
|
||||
byzantineWeight: 31,
|
||||
voteWeight: 68,
|
||||
expectQuorum: 69,
|
||||
expectTolerance: true,
|
||||
expectConsensus: false,
|
||||
},
|
||||
{
|
||||
name: "just_above_31_percent_byzantine",
|
||||
totalWeight: 100,
|
||||
byzantineWeight: 32,
|
||||
voteWeight: 68,
|
||||
expectQuorum: 69,
|
||||
expectTolerance: false,
|
||||
expectConsensus: false,
|
||||
},
|
||||
// Small networks
|
||||
{
|
||||
name: "small_network_3_nodes",
|
||||
totalWeight: 3,
|
||||
byzantineWeight: 1,
|
||||
voteWeight: 2,
|
||||
expectQuorum: 3, // Ceiling of 2.07
|
||||
expectTolerance: false, // 33.3% > 31%
|
||||
expectConsensus: false,
|
||||
},
|
||||
{
|
||||
name: "small_network_4_nodes",
|
||||
totalWeight: 4,
|
||||
byzantineWeight: 1,
|
||||
voteWeight: 3,
|
||||
expectQuorum: 3, // Ceiling of 2.76
|
||||
expectTolerance: true, // 25% < 31%
|
||||
expectConsensus: true,
|
||||
},
|
||||
{
|
||||
name: "small_network_5_nodes",
|
||||
totalWeight: 5,
|
||||
byzantineWeight: 1,
|
||||
voteWeight: 4,
|
||||
expectQuorum: 4, // Ceiling of 3.45
|
||||
expectTolerance: true, // 20% < 31%
|
||||
expectConsensus: true,
|
||||
},
|
||||
// Large networks
|
||||
{
|
||||
name: "large_network_1000",
|
||||
totalWeight: 1000,
|
||||
byzantineWeight: 310,
|
||||
voteWeight: 690,
|
||||
expectQuorum: 690,
|
||||
expectTolerance: true,
|
||||
expectConsensus: true,
|
||||
},
|
||||
{
|
||||
name: "large_network_10000",
|
||||
totalWeight: 10000,
|
||||
byzantineWeight: 3100,
|
||||
voteWeight: 6900,
|
||||
expectQuorum: 6900,
|
||||
expectTolerance: true,
|
||||
expectConsensus: true,
|
||||
},
|
||||
// Rounding edge cases
|
||||
{
|
||||
name: "rounding_33_nodes",
|
||||
totalWeight: 33,
|
||||
byzantineWeight: 10,
|
||||
voteWeight: 23,
|
||||
expectQuorum: 23, // Ceiling of 22.77
|
||||
expectTolerance: true, // 30.3% < 31%
|
||||
expectConsensus: true,
|
||||
},
|
||||
{
|
||||
name: "rounding_67_nodes",
|
||||
totalWeight: 67,
|
||||
byzantineWeight: 20,
|
||||
voteWeight: 47,
|
||||
expectQuorum: 47, // Ceiling of 46.23
|
||||
expectTolerance: true, // 29.85% < 31%
|
||||
expectConsensus: true,
|
||||
},
|
||||
// Zero and overflow protection
|
||||
{
|
||||
name: "zero_weight",
|
||||
totalWeight: 0,
|
||||
byzantineWeight: 0,
|
||||
voteWeight: 0,
|
||||
expectQuorum: 0,
|
||||
expectTolerance: true, // No Byzantine nodes
|
||||
expectConsensus: true, // Vacuous truth
|
||||
},
|
||||
{
|
||||
name: "max_uint64_weight",
|
||||
totalWeight: math.MaxUint64,
|
||||
byzantineWeight: math.MaxUint64 / 3, // ~33%
|
||||
voteWeight: uint64(float64(math.MaxUint64) * 0.68), // Just below 69%
|
||||
expectQuorum: uint64(math.Ceil(float64(math.MaxUint64) * 0.69)),
|
||||
expectTolerance: false, // >31%
|
||||
expectConsensus: false, // 68% < 69%
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Test quorum calculation
|
||||
quorum := CalculateQuorum(tt.totalWeight)
|
||||
if quorum != tt.expectQuorum {
|
||||
t.Errorf("CalculateQuorum(%d) = %d, want %d",
|
||||
tt.totalWeight, quorum, tt.expectQuorum)
|
||||
}
|
||||
|
||||
// Test Byzantine tolerance
|
||||
canTolerate := CanTolerateFailure(tt.byzantineWeight, tt.totalWeight)
|
||||
if canTolerate != tt.expectTolerance {
|
||||
t.Errorf("CanTolerateFailure(%d, %d) = %v, want %v",
|
||||
tt.byzantineWeight, tt.totalWeight, canTolerate, tt.expectTolerance)
|
||||
}
|
||||
|
||||
// Test consensus achievement
|
||||
hasConsensus := HasSuperMajority(tt.voteWeight, tt.totalWeight)
|
||||
if hasConsensus != tt.expectConsensus {
|
||||
t.Errorf("HasSuperMajority(%d, %d) = %v, want %v",
|
||||
tt.voteWeight, tt.totalWeight, hasConsensus, tt.expectConsensus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestThresholdPrecision tests floating point precision issues
|
||||
func TestThresholdPrecision(t *testing.T) {
|
||||
// Test that we handle floating point precision correctly
|
||||
testCases := []struct {
|
||||
weight uint64
|
||||
totalWeight uint64
|
||||
expected bool
|
||||
}{
|
||||
// Test cases around 69% with potential floating point issues
|
||||
{68999999, 100000000, false}, // 68.999999%
|
||||
{69000000, 100000000, true}, // 69.000000%
|
||||
{69000001, 100000000, true}, // 69.000001%
|
||||
|
||||
// Test with prime numbers (harder to represent exactly)
|
||||
{47, 68, true}, // 69.117% > 69%
|
||||
{46, 67, false}, // 68.656% < 69%
|
||||
{47, 67, true}, // 70.149% > 69%
|
||||
{45, 67, false}, // 67.164% < 69%
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := HasSuperMajority(tc.weight, tc.totalWeight)
|
||||
if result != tc.expected {
|
||||
percentage := float64(tc.weight) * 100 / float64(tc.totalWeight)
|
||||
t.Errorf("HasSuperMajority(%d, %d) = %v, want %v (%.6f%%)",
|
||||
tc.weight, tc.totalWeight, result, tc.expected, percentage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAlphaForKAllValues tests Alpha calculation for all practical K values
|
||||
func TestAlphaForKAllValues(t *testing.T) {
|
||||
testCases := []struct {
|
||||
k int
|
||||
expectedAlpha int
|
||||
percentage float64
|
||||
}{
|
||||
{1, 1, 100.0}, // Special case: need all
|
||||
{2, 2, 100.0}, // Ceiling of 1.38 = 2
|
||||
{3, 3, 100.0}, // Ceiling of 2.07 = 3
|
||||
{4, 3, 75.0}, // Ceiling of 2.76 = 3
|
||||
{5, 4, 80.0}, // Ceiling of 3.45 = 4
|
||||
{10, 7, 70.0}, // Ceiling of 6.9 = 7
|
||||
{11, 8, 72.7}, // Ceiling of 7.59 = 8
|
||||
{20, 14, 70.0}, // Ceiling of 13.8 = 14
|
||||
{21, 15, 71.4}, // Ceiling of 14.49 = 15
|
||||
{100, 69, 69.0}, // Exactly 69
|
||||
{1000, 690, 69.0}, // Exactly 690
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
alpha := AlphaForK(tc.k)
|
||||
if alpha != tc.expectedAlpha {
|
||||
t.Errorf("AlphaForK(%d) = %d, want %d", tc.k, alpha, tc.expectedAlpha)
|
||||
}
|
||||
|
||||
// Verify the percentage is at least 69%
|
||||
actualPercentage := float64(alpha) * 100 / float64(tc.k)
|
||||
if actualPercentage < 69.0 && tc.k > 3 { // Allow small networks to exceed
|
||||
t.Errorf("AlphaForK(%d) = %d gives %.1f%%, below 69%% threshold",
|
||||
tc.k, alpha, actualPercentage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConcurrentThresholdCalculations tests thread safety
|
||||
func TestConcurrentThresholdCalculations(t *testing.T) {
|
||||
// Run many concurrent calculations to ensure thread safety
|
||||
const goroutines = 100
|
||||
const iterations = 1000
|
||||
|
||||
done := make(chan bool, goroutines)
|
||||
|
||||
for i := 0; i < goroutines; i++ {
|
||||
go func(id int) {
|
||||
for j := 0; j < iterations; j++ {
|
||||
weight := uint64(id*iterations + j)
|
||||
total := weight + uint64(j)
|
||||
|
||||
// These should be thread-safe pure functions
|
||||
_ = CalculateQuorum(total)
|
||||
_ = HasSuperMajority(weight, total)
|
||||
_ = CanTolerateFailure(weight/3, total)
|
||||
_ = AlphaForK(int(total % 100))
|
||||
}
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < goroutines; i++ {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkThresholdEdgeCases benchmarks edge case performance
|
||||
func BenchmarkThresholdEdgeCases(b *testing.B) {
|
||||
b.Run("small_network", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = CalculateQuorum(5)
|
||||
_ = HasSuperMajority(4, 5)
|
||||
_ = CanTolerateFailure(1, 5)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("large_network", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = CalculateQuorum(1000000)
|
||||
_ = HasSuperMajority(690000, 1000000)
|
||||
_ = CanTolerateFailure(310000, 1000000)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("max_uint64", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = CalculateQuorum(math.MaxUint64)
|
||||
_ = HasSuperMajority(math.MaxUint64*69/100, math.MaxUint64)
|
||||
_ = CanTolerateFailure(math.MaxUint64*31/100, math.MaxUint64)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A set with no stake has no supermajority to reach, so the threshold is zero
|
||||
// rather than a count some empty quorum could satisfy. Breaks if a stakeless or
|
||||
// empty validator set ever yields a reachable threshold — that is a quorum of
|
||||
// nobody.
|
||||
func TestStakelessSetHasNoSupermajority(t *testing.T) {
|
||||
for name, weights := range map[string][]uint64{
|
||||
"no validators": {},
|
||||
"nil": nil,
|
||||
"all zero stake": {0, 0, 0},
|
||||
} {
|
||||
if got := WeightedSupermajorityThreshold(weights); got != 0 {
|
||||
t.Errorf("%s: threshold %d, want 0", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The equal-stake closed form and the general heaviest-first computation are two
|
||||
// spellings of one predicate; they must agree on every n, or a network sized by
|
||||
// one rule would be validated against the other.
|
||||
func TestEqualStakeClosedFormMatchesTheWeightedComputation(t *testing.T) {
|
||||
for n := 1; n <= 300; n++ {
|
||||
unit := make([]uint64, n)
|
||||
for i := range unit {
|
||||
unit[i] = 1
|
||||
}
|
||||
if closed, general := EqualStakeSupermajorityThreshold(n), WeightedSupermajorityThreshold(unit); closed != general {
|
||||
t.Fatalf("n=%d: closed form %d, heaviest-first %d", n, closed, general)
|
||||
}
|
||||
}
|
||||
|
||||
// An unknown validator count still needs someone to accept, never zero.
|
||||
for _, n := range []int{0, -1, -100} {
|
||||
if got := EqualStakeSupermajorityThreshold(n); got != 1 {
|
||||
t.Errorf("n=%d: threshold %d, want 1", n, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FeasibleParams derives α from the committee size alone. This holds the ground
|
||||
// the function's missing clamps rest on: for every k it can produce, α already
|
||||
// sits at or above the BFT overlap floor, at or below k, and its ratio lands
|
||||
// inside the [0.66, 1.0] window Valid() demands. If a formula change breaks any
|
||||
// of these, the clamps have to come back — and this fails first.
|
||||
func TestFeasibleParamsNeedsNoClamping(t *testing.T) {
|
||||
for n := -1; n <= 2000; n++ {
|
||||
p := FeasibleParams(1, n)
|
||||
|
||||
if p.K < 4 {
|
||||
t.Fatalf("n=%d: K=%d below the minimal BFT committee", n, p.K)
|
||||
}
|
||||
if floor := (Parameters{K: p.K}).bftQuorumFloor(); p.AlphaPreference < floor {
|
||||
t.Fatalf("K=%d: α=%d under the BFT overlap floor %d", p.K, p.AlphaPreference, floor)
|
||||
}
|
||||
if p.AlphaPreference > p.K {
|
||||
t.Fatalf("K=%d: α=%d demands more votes than the committee holds", p.K, p.AlphaPreference)
|
||||
}
|
||||
if p.Alpha < 0.66 || p.Alpha > 1.0 {
|
||||
t.Fatalf("K=%d: ratio %.4f outside [0.66, 1.0]", p.K, p.Alpha)
|
||||
}
|
||||
if err := p.Valid(); err != nil {
|
||||
t.Fatalf("n=%d: derived params do not validate: %v", n, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The live-aware check still runs the ordinary parameter validation first, so a
|
||||
// structurally broken set is refused on its own terms rather than reaching the
|
||||
// live-count reasoning with nonsense.
|
||||
func TestLiveValidationRefusesStructurallyBrokenParams(t *testing.T) {
|
||||
p := MainnetParams()
|
||||
p.K = 0
|
||||
|
||||
err := p.ValidateForLiveValueNetwork(1, 21)
|
||||
if err == nil {
|
||||
t.Fatal("accepted params with an empty committee")
|
||||
}
|
||||
if errors.Is(err, ErrKBelowMainnetTarget) {
|
||||
t.Errorf("reported a decentralisation target for params that are not even valid: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package config
|
||||
|
||||
type WaveConfig struct {
|
||||
Enable bool
|
||||
VoteLimitPerBlock int
|
||||
ExecuteOwned bool
|
||||
ExecuteMixedOnFinal bool
|
||||
EpochFence bool
|
||||
VotePrefix []byte
|
||||
}
|
||||
|
||||
func DefaultWave() WaveConfig {
|
||||
return WaveConfig{
|
||||
Enable: true,
|
||||
VoteLimitPerBlock: 256,
|
||||
ExecuteOwned: true,
|
||||
ExecuteMixedOnFinal: true,
|
||||
EpochFence: true,
|
||||
VotePrefix: []byte("WAVE/V1"),
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWaveConfig(t *testing.T) {
|
||||
cfg := DefaultWave()
|
||||
|
||||
// Check that configuration is set correctly
|
||||
if !cfg.Enable {
|
||||
t.Error("Enable should be true")
|
||||
}
|
||||
|
||||
if cfg.VoteLimitPerBlock != 256 {
|
||||
t.Errorf("VoteLimitPerBlock mismatch: got %d, want 256", cfg.VoteLimitPerBlock)
|
||||
}
|
||||
|
||||
if !cfg.ExecuteOwned {
|
||||
t.Error("ExecuteOwned should be true")
|
||||
}
|
||||
|
||||
if !cfg.ExecuteMixedOnFinal {
|
||||
t.Error("ExecuteMixedOnFinal should be true")
|
||||
}
|
||||
|
||||
if !cfg.EpochFence {
|
||||
t.Error("EpochFence should be true")
|
||||
}
|
||||
|
||||
if string(cfg.VotePrefix) != "WAVE/V1" {
|
||||
t.Errorf("VotePrefix mismatch: got %s, want WAVE/V1", cfg.VotePrefix)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaveConfigCustom(t *testing.T) {
|
||||
cfg := WaveConfig{
|
||||
Enable: false,
|
||||
VoteLimitPerBlock: 512,
|
||||
ExecuteOwned: false,
|
||||
ExecuteMixedOnFinal: false,
|
||||
EpochFence: false,
|
||||
VotePrefix: []byte("CUSTOM"),
|
||||
}
|
||||
|
||||
if cfg.Enable {
|
||||
t.Error("Enable should be false")
|
||||
}
|
||||
|
||||
if cfg.VoteLimitPerBlock != 512 {
|
||||
t.Errorf("VoteLimitPerBlock should be 512, got %d", cfg.VoteLimitPerBlock)
|
||||
}
|
||||
|
||||
if string(cfg.VotePrefix) != "CUSTOM" {
|
||||
t.Errorf("VotePrefix should be CUSTOM, got %s", cfg.VotePrefix)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWaveConfig(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = DefaultWave()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Probe: a sybil-protected chain cannot START with K==1 on ANY network at ANY
|
||||
// live count. The node's chain manager runs exactly this predicate before it
|
||||
// wires the engine, so if it refuses everywhere, the synthesized 1-of-1 cert is
|
||||
// unreachable on every staked chain — including a sovereign L1 whose networkID
|
||||
// is neither 1 nor 2.
|
||||
func TestRefute_K1_neverStartsAValueChain(t *testing.T) {
|
||||
p := SingleValidatorParams()
|
||||
for _, networkID := range []uint32{1, 2, 3, 1337, 96369, 200200, 36963, 494949, 1872} {
|
||||
for _, liveN := range []int{0, 1, 2, 4, 5, 11, 21} {
|
||||
err := p.ValidateForLiveValueNetwork(networkID, liveN)
|
||||
if !errors.Is(err, ErrKTooLowForValue) {
|
||||
t.Fatalf("K=1 admitted on networkID=%d liveN=%d: err=%v", networkID, liveN, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Probe: an operator override cannot sneak K==1 past the same predicate either —
|
||||
// the manager applies overrides BEFORE validating, so this is the real call order.
|
||||
func TestRefute_K1_overrideStillRefused(t *testing.T) {
|
||||
p := MainnetParams()
|
||||
p.K, p.AlphaPreference, p.AlphaConfidence = 1, 1, 1
|
||||
if err := p.ValidateForLiveValueNetwork(1, 21); err == nil {
|
||||
t.Fatal("K=1 override admitted on mainnet")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user