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:
2026-08-16 11:42:58 -07:00
co-authored by Hanzo Dev
parent 7b9b2b179f
commit 4abf37c952
33 changed files with 832 additions and 1783 deletions
-39
View File
@@ -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)
}
}
}
+8 -8
View File
@@ -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()
}
+25 -25
View File
@@ -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")
}
}
+87
View File
@@ -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()
}
-69
View File
@@ -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
+3 -86
View File
@@ -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
View File
@@ -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)
-61
View File
@@ -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
-115
View File
@@ -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
View File
@@ -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))
}
+24
View File
@@ -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)
}
}
}
+127
View File
@@ -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)
}
}
}
-31
View File
@@ -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
}
-323
View File
@@ -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)
}
}
-33
View File
@@ -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)
}
}
+92
View File
@@ -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)
}
}
-74
View File
@@ -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 {
-138
View File
@@ -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)
}
}
+34
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+9 -4
View File
@@ -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)
+7 -73
View File
@@ -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).
+177
View File
@@ -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)
}
}
}
+11 -9
View File
@@ -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
}
+4 -4
View File
@@ -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")
}
-280
View File
@@ -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)
}
})
}
+87
View File
@@ -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)
}
}
-21
View File
@@ -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"),
}
}
-63
View File
@@ -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()
}
}
+36
View File
@@ -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")
}
}