Files
zeekayandHanzo Dev bba3234534 consensus: a quorum of zero is not a quorum
The chain engine decides on `acceptVotes() >= alpha`, so alpha=0 makes that
predicate hold over an EMPTY accept set. Any arriving vote — including one
AGAINST the block — then marks it accepted with zero accept votes, and Poll's
`rejectVotes() >= alpha` fires on nothing too, rejecting every fresh block and
dropping it from the tips. Two partitioned nodes could settle different blocks
at the same height with no quorum and no cert.

alpha=0 was reachable because Config.Validate had no callers — New,
NewWithConfig, NewWithParams and NewRuntime all reached NewChainConsensus
unchecked — and because Parameters.Valid gated its alpha checks behind
`AlphaPreference != 0`. The one value that destroys safety was the one value
that skipped validation.

Three independent layers close it, each judged by ONE definition of a safe
quorum:

  - config.Parameters.ValidQuorum, split out of Valid, judges K and the two
    integer alphas: 1 <= alpha <= K (unconditional now), AlphaConfidence >=
    AlphaPreference (never enforced before, so preference=15/confidence=1
    passed), and the Byzantine overlap floor, also unconditional. Valid calls
    it, so the two cannot drift. The split lets the engine demand SAFETY
    without also demanding the descriptive Alpha ratio and block timings,
    which have no bearing on whether a quorum can fork.

  - Transitive.Start refuses to run unsafe parameters. The check belongs here,
    not in the constructors: options are applied AFTER config, so WithParams
    can replace params a constructor already validated. Start is the one point
    every engine passes through with its final parameters.

  - NewChainConsensus, the single place the running predicate is armed, pins
    (k, alpha) into k >= 1, 1 <= alpha <= k. A path that skips both checks
    still cannot arm a predicate satisfiable by nothing.

Reclamp gains the safety floor as well. It installs a quorum on a LIVE engine
from a DERIVED validator count, and used to accept any well-formed alpha —
including k=4/alpha=2, where two disjoint quorums overlap in nothing. The
supermajority the committee sizer derives clears the floor at every k, so no
legitimate re-clamp is refused.

Fixtures that encoded unsafe quorums are corrected rather than exempted.
params5 was K=5/alpha=3: two 3-of-5 quorums overlap in a SINGLE node, so one
faulty validator (f=1 at K=5) can certify both sides of a fork. It is now
K=5/alpha=4 — the Byzantine floor, and what FeasibleParams(Local, 5) derives
for the live nets — which makes params5Prod an exact duplicate, so it goes.
Two config cases declared K and Beta with no integer alpha at all and expected
Valid to pass; that is a zero-vote quorum, not an unspecified one. Catch-up
certs carrying four signatures now declare the 4-of-5 threshold they are, and
the forged-signature case declares a real threshold so it still fails on the
forgery rather than on a stale bound.

The single-validator path is untouched: K=1/alpha=1 satisfies the floor, needs
no vote verifier, and bftCommittee still leaves a genuine K=1 preset unclamped.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 12:01:23 -07:00

509 lines
22 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"errors"
"fmt"
"time"
)
// Error variables for parameter validation
var (
ErrParametersInvalid = errors.New("invalid consensus parameters")
ErrInvalidK = errors.New("k must be >= 1")
ErrInvalidAlpha = errors.New("alpha must be between 0.66 and 1.0")
ErrInvalidBeta = errors.New("beta must be >= 1")
ErrBlockTimeTooLow = errors.New("block time must be >= 1ms")
ErrRoundTimeoutTooLow = errors.New("round timeout must be >= block time")
// ErrKTooLowForMainnet enforces the mainnet sample-size floor. K>=11 is the
// floor that gives mainnet a meaningful Byzantine margin: it is NOT merely
// "K>=11" as a magic number — it is the smallest K for which the BFT overlap
// bound 2*AlphaPreference - K >= floor((K-1)/3)+1 (= f+1) can hold with
// f=floor((K-1)/3)>=3, so two quorums always overlap in >f honest nodes and
// a sub-1/3 coalition can neither finalize nor fork. A smaller K shrinks f
// toward 0 and makes a cheap stake-fraction (51%-style) attack viable.
// ErrKBelowMainnetTarget reports that mainnet params are SAFE but below the
// K>=11 (f>=3) decentralisation target. Informational: never a reason to
// refuse to start a chain that satisfies the BFT overlap bound.
ErrKBelowMainnetTarget = errors.New("consensus: mainnet K is below the K>=11 (f>=3) decentralisation target; params are Byzantine-safe but the validator set should grow")
ErrKTooLowForMainnet = errors.New("consensus: mainnet (networkID=1) requires K>=11 so the BFT overlap bound 2*AlphaPreference-K >= floor((K-1)/3)+1 holds with f=floor((K-1)/3)>=3; a smaller K drives f->0 and enables a cheap sub-1/3 (51%-style) attack")
// ErrKTooLowForTestnet is the testnet analogue: K>=5 keeps f=floor((K-1)/3)>=1
// so the same overlap bound (2*AlphaPreference-K >= f+1) is satisfiable with
// at least single-fault Byzantine tolerance.
ErrKTooLowForTestnet = errors.New("consensus: testnet (networkID=2) requires K>=5 so the BFT overlap bound 2*AlphaPreference-K >= floor((K-1)/3)+1 holds with f=floor((K-1)/3)>=1; a smaller K leaves f=0 and a single faulty validator can fork it")
// ErrAlphaBelowBFTQuorum is returned by Valid() when the integer accept
// quorum (AlphaPreference) is too small for the sample size K to guarantee
// safety under Byzantine faults. Two α-quorums must overlap in MORE than f
// honest nodes so they cannot certify conflicting blocks; that requires
//
// 2·AlphaPreference − K ≥ f + 1, where f = ⌊(K-1)/3⌋.
//
// A config that fails this (e.g. K=3/α=2 treated as BFT) admits a single
// faulty validator forking the chain — exactly the round-1 hole.
ErrAlphaBelowBFTQuorum = errors.New("consensus: alpha quorum too small for K to be Byzantine-safe (need 2*AlphaPreference - K >= floor((K-1)/3)+1)")
// ErrAlphaConfidenceBelowPreference is returned by Valid() when the confidence
// quorum is smaller than the preference quorum. Confidence is the STRONGER
// commitment — a block may not become confident on fewer votes than it needed
// to become preferred — so AlphaConfidence < AlphaPreference inverts the ladder
// and lets the confidence round finalize on a quorum the preference round
// already rejected.
ErrAlphaConfidenceBelowPreference = errors.New("consensus: AlphaConfidence must be >= AlphaPreference (the confidence quorum can never be weaker than the preference quorum)")
// ErrKTooLowForValue is returned by ValidateForValueNetwork when a value /
// PoS chain is configured with K<4. K=3 tolerates f=⌊2/3⌋=0 Byzantine
// validators — i.e. NO fault tolerance — so a single Byzantine validator can
// 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")
)
// errKTooLowForValueLive wraps ErrKTooLowForValue with the live-aware context.
func errKTooLowForValueLive(p Parameters, networkID uint32) error {
return fmt.Errorf("%w: K=%d f=%d networkID=%d", ErrKTooLowForValue, p.K, p.ByzantineFaultTolerance(), networkID)
}
// Parameters defines consensus parameters
type Parameters struct {
K int
Alpha float64 // For compatibility with Quasar
Beta uint32 // For compatibility with Quasar
AlphaPreference int
AlphaConfidence int
BetaVirtuous int
BetaRogue int
ConcurrentPolls int
ConcurrentRepolls int
OptimalProcessing int
MaxOutstandingItems int
MaxItemProcessingTime time.Duration
Parents int
BatchSize int
BlockTime time.Duration // For compatibility
RoundTO time.Duration // For compatibility
GasLimit uint64 // Per-block gas limit (0 = use chain default)
// ConvergenceSettleWindow is how long a contested fork slot is observed (since the
// last competing sibling was tracked) before this node casts its ONE per-height
// accept signature for the converged (lowest-canonical) winner. It MUST exceed the
// sibling-gossip latency of the deployment: a settle shorter than gossip lets a node
// bind its vote to an incomplete sibling set (a non-global-minimum block), which —
// under the height-only vote-once rule — is an UNRECOVERABLE vote split. It is
// decoupled from RoundTO on purpose so an operator can lengthen it for high-latency
// (WAN) validator sets WITHOUT slowing the round cadence. Zero ⇒ auto (RoundTO/2,
// clamped [150ms, 2s]); see Transitive.convergenceSettleWindow.
ConvergenceSettleWindow time.Duration
// PQMode selects which post-quantum signature paths the engine runs
// alongside BLS. Zero value (PQModeBLSOnly) preserves the classical
// fast path. See pq_mode.go for the full enum.
PQMode PQMode
}
// DefaultParams returns default parameters with 69% threshold
func DefaultParams() Parameters {
return Parameters{
K: 20,
Alpha: 0.69, // 69% threshold for BFT
Beta: 14, // Adjusted for 69% (was 15)
AlphaPreference: 14, // 70% of K for 69% threshold
AlphaConfidence: 14, // Matches AlphaPreference for 69%
BetaVirtuous: 14, // Virtuous confidence for 69%
BetaRogue: 20, // Rogue confidence remains high
ConcurrentPolls: 4,
ConcurrentRepolls: 4,
OptimalProcessing: 10,
MaxOutstandingItems: 1024,
MaxItemProcessingTime: 2 * time.Minute,
Parents: 2, // Reduced for efficiency
BatchSize: 30,
BlockTime: 100 * time.Millisecond,
RoundTO: 250 * time.Millisecond,
}
}
// MainnetParams returns mainnet parameters with 69% threshold
func MainnetParams() Parameters {
p := DefaultParams()
p.K = 21
p.AlphaPreference = 15 // ~71% of K for 69% threshold
p.AlphaConfidence = 15 // Matches AlphaPreference
p.BetaVirtuous = 15 // Virtuous confidence for 69%
p.BlockTime = 200 * time.Millisecond
p.RoundTO = 400 * time.Millisecond
return p
}
// TestnetParams returns testnet parameters with 69% threshold
func TestnetParams() Parameters {
p := DefaultParams()
p.K = 11
p.Alpha = 0.69 // 69% threshold
p.Beta = 8 // Adjusted for 69%
p.AlphaPreference = 8 // ~73% of K for 69% threshold
p.AlphaConfidence = 8 // Matches AlphaPreference
p.BetaVirtuous = 8 // Virtuous confidence for 69%
p.BlockTime = 100 * time.Millisecond
p.RoundTO = 225 * time.Millisecond
return p
}
// LocalParams returns local parameters with 2/3 threshold for 3-node networks.
// Uses 1ms block time for maximum throughput on localhost (zero network latency).
//
// SECURITY: K=3/α=2 is f=0 (CFT) — a single Byzantine validator forks it. This
// is the documented CRITICAL-2 fork config and is FORBIDDEN on value networks
// (ValidateForValueNetwork rejects it). Use it ONLY for a single-operator local
// network where every node is trusted (no value across independent parties). For
// a multi-node local network that must be genuinely BFT, use LocalBFTParams.
func LocalParams() Parameters {
p := DefaultParams()
p.K = 3
p.Alpha = 0.67 // 2/3 threshold
p.Beta = 2 // Adjusted for 2/3
p.AlphaPreference = 2 // 2 of 3 for preference
p.AlphaConfidence = 2 // 2 of 3 for confidence
p.BetaVirtuous = 2 // Virtuous confidence for 2/3
p.BlockTime = 1 * time.Millisecond
p.RoundTO = 5 * time.Millisecond
return p
}
// LocalBFTParams returns the MINIMAL Byzantine-fault-tolerant parameter set for a
// small local network: K=4, α=3 (AlphaPreference = AlphaConfidence = 3). This is
// f=1 (3·1+1 = 4 the minimal BFT committee), the smallest K that
// ValidateForValueNetwork admits (K≥4). It clears the BFT α-floor
// (2·AlphaPreference − K = 2·3 − 4 = 2 ≥ f+1 = 2), so a single faulty validator
// can neither forge a quorum nor stall finality.
//
// Why this exists: DefaultParams (K=20, α=14) is unsatisfiable on a small local
// network — α=14 affirmative votes are unreachable with only 3-4 validators, so
// NO block ever finalizes (the P-Chain freezes at height 0 and no chain is
// created). A local 4-validator devnet must use K=4 to make quorum reachable
// while staying genuinely BFT. Unlike LocalParams (K=3, f=0), this passes
// ValidateForValueNetwork and the CRITICAL-2 multi-node-is-BFT regression — it
// is a REAL BFT set, just sized for a minimal committee. Requires ≥4 validators
// to provide f=1 in practice; with fewer, quorum needs near-unanimity and runtime
// fault tolerance degrades to f=0 (still safe, just not live under one fault).
//
// Keeps LocalParams' localhost timing (1ms blocks / 5ms rounds).
func LocalBFTParams() Parameters {
p := DefaultParams()
p.K = 4
p.Alpha = 0.67 // ⌈3/4⌉ ≈ 2/3-style threshold for a 4-committee
p.Beta = 2 // fast confirmation on localhost
p.AlphaPreference = 3 // 3 of 4 for preference (f=1)
p.AlphaConfidence = 3 // 3 of 4 for confidence (f=1)
p.BetaVirtuous = 2 // virtuous confidence
p.BetaRogue = 4 // rogue confidence stays above the committee size
p.BlockTime = 1 * time.Millisecond
p.RoundTO = 5 * time.Millisecond
return p
}
// BurstParams returns parameters for maximum throughput burst mode.
// Designed for GPU EVM + Block-STM on high-bandwidth networks (800Gbps+).
// 1ms blocks × 100K txs/block (2.1B gas) = 100M TPS theoretical ceiling.
// Actual throughput bounded by GPU execution speed (Block-STM parallel).
func BurstParams() Parameters {
return Parameters{
K: 3,
Alpha: 0.67,
Beta: 2,
AlphaPreference: 2,
AlphaConfidence: 2,
BetaVirtuous: 2,
BetaRogue: 3,
ConcurrentPolls: 8,
ConcurrentRepolls: 8,
OptimalProcessing: 50,
MaxOutstandingItems: 8192,
MaxItemProcessingTime: 10 * time.Second,
Parents: 2,
BatchSize: 100,
BlockTime: 1 * time.Millisecond,
RoundTO: 5 * time.Millisecond,
GasLimit: 2_100_000_000, // 2.1B gas → 100K simple txs/block
}
}
// SoloGPUParams returns parameters for a single-node GPU-accelerated validator.
// Tuned for Apple Silicon (M1/M2/M3/M4) with unified memory:
// - 1ms blocks, K=1 self-vote (no network latency)
// - 1B gas limit — GPU EVM fills ~47K txs/block at 21K gas each
// - With C++ GPU EVM (1M TPS): ~1M TPS sustained
// - With Go EVM (188K TPS): ~188K TPS sustained
// - Consensus overhead: <13μs per block (measured 76K blocks/sec)
func SoloGPUParams() Parameters {
return Parameters{
K: 1,
Alpha: 1.0,
Beta: 1,
AlphaPreference: 1,
AlphaConfidence: 1,
BetaVirtuous: 1,
BetaRogue: 1,
ConcurrentPolls: 4,
ConcurrentRepolls: 4,
OptimalProcessing: 20,
MaxOutstandingItems: 4096,
MaxItemProcessingTime: 5 * time.Second,
Parents: 1,
BatchSize: 50,
BlockTime: 1 * time.Millisecond,
RoundTO: 5 * time.Millisecond,
GasLimit: 1_000_000_000, // 1B gas → 47K txs/block
}
}
// XChainParams returns X-Chain parameters with 2/3 threshold for 3-node networks
func XChainParams() Parameters {
p := DefaultParams()
p.K = 3
p.Alpha = 0.67 // 2/3 threshold
p.Beta = 2 // Adjusted for 2/3
p.AlphaPreference = 2 // 2 of 3 for preference
p.AlphaConfidence = 2 // 2 of 3 for confidence
p.BetaVirtuous = 2 // Virtuous confidence for 2/3
p.BlockTime = 1 * time.Millisecond
p.RoundTO = 5 * time.Millisecond
return p
}
// SingleValidatorParams returns parameters for single validator mainnet (K=1)
// This configuration is used for POA mode with a single staking validator
func SingleValidatorParams() Parameters {
return Parameters{
K: 1, // Single validator
Alpha: 1.0, // 100% threshold (only one validator)
Beta: 1, // Immediate finalization
AlphaPreference: 1, // Single validator preference
AlphaConfidence: 1, // Single validator confidence
BetaVirtuous: 1, // Immediate virtuous confidence
BetaRogue: 1, // No rogue behavior possible
ConcurrentPolls: 1, // Single poll at a time
ConcurrentRepolls: 1, // Single repoll if needed
OptimalProcessing: 1, // Process one at a time
MaxOutstandingItems: 256, // Reduced for single validator
MaxItemProcessingTime: 30 * time.Second, // Faster timeout for single validator
Parents: 1, // Single parent for linear chain
BatchSize: 10, // Smaller batches for single validator
BlockTime: 100 * time.Millisecond, // Fast block time
RoundTO: 200 * time.Millisecond, // Quick round timeout
}
}
// ByzantineFaultTolerance returns f, the maximum number of Byzantine validators
// this sample size can tolerate under classic BFT (n=K, f<n/3):
//
// f = ⌊(K-1)/3⌋
//
// K=1→0, K=3→0, K=4→1, K=7→2, K=10→3, K=11→3, K=21→6. A value chain needs f≥1,
// hence K≥4 (see ValidateForValueNetwork).
func (p Parameters) ByzantineFaultTolerance() int {
if p.K < 1 {
return 0
}
return (p.K - 1) / 3
}
// bftQuorumFloor returns the minimum integer accept quorum α for safety at this
// K: the smallest α with 2α − K ≥ f + 1, i.e. α ≥ ⌈(K + f + 1)/2⌉.
func (p Parameters) bftQuorumFloor() int {
return BFTQuorumFloor(p.K)
}
// BFTQuorumFloor is the smallest integer accept quorum α that is SAFE at sample
// size k: the least α satisfying the overlap bound 2α − k ≥ f + 1 with
// f = ⌊(k−1)/3⌋, i.e. α = ⌈(k + f + 1)/2⌉.
//
// This is the ONE definition of "how weak may a quorum be". Valid() rejects any
// AlphaPreference below it, and the engine refuses to INSTALL a quorum below it
// (NewChainConsensus / Reclamp), so a config that never reaches Valid() still
// cannot arm a sub-quorum accept predicate. k < 1 yields 1: no committee ever
// runs with α=0, which would accept on zero votes.
//
// It is the safety FLOOR, not the ⅔ supermajority the committee sizer picks
// (⌊2k/3⌋+1, which is ≥ this and sometimes strictly above it — k=3 → floor 2 vs
// ⅔ 3). Presets sit at or above the floor and are left exactly as tuned.
func BFTQuorumFloor(k int) int {
if k < 1 {
return 1
}
f := (k - 1) / 3
return (k + f + 1 + 1) / 2 // ceil((k+f+1)/2)
}
// Validate validates parameters (compatibility method)
func (p Parameters) Validate() error {
return p.Valid()
}
// ValidQuorum judges the INTEGER quorum — K, AlphaPreference, AlphaConfidence —
// and nothing else. These are the only parameters the chain engine actually counts
// toward finality, so they carry the whole safety argument, while the rest of what
// Valid checks (the descriptive Alpha ratio, block timings, queue sizes) is
// well-formedness. Splitting them lets the engine enforce SAFETY at Start without
// also demanding fields that have no bearing on whether a quorum can fork.
//
// Valid calls this, so there is one definition and the two can never drift.
//
// The three invariants:
//
// - 1 ≤ AlphaPreference ≤ K, and the same for AlphaConfidence. α is the literal
// accept predicate (`acceptVotes() >= alpha`), so α=0 satisfies it with ZERO
// votes: every block self-finalizes on every node with no quorum at all, and two
// nodes independently self-finalize conflicting blocks. There is no "unset" α —
// a K≥1 committee always needs at least one vote — so 0 is a mis-derivation, not
// a default.
//
// - AlphaConfidence ≥ AlphaPreference. Confidence is the stronger commitment; it
// may not be reached on fewer votes than preference required.
//
// - 2·AlphaPreference − K ≥ f+1 with f = ⌊(K−1)/3⌋ (equivalently AlphaPreference ≥
// BFTQuorumFloor(K)). Two α-quorums must overlap in more than f nodes, else two
// disjoint quorums can each certify a conflicting block and the chain forks.
//
// Because AlphaConfidence ≥ AlphaPreference, the floor binds both α's at once.
func (p Parameters) ValidQuorum() error {
if p.K < 1 {
return ErrInvalidK
}
if p.AlphaPreference < 1 || p.AlphaPreference > p.K {
return ErrParametersInvalid
}
if p.AlphaConfidence < 1 || p.AlphaConfidence > p.K {
return ErrParametersInvalid
}
if p.AlphaConfidence < p.AlphaPreference {
return fmt.Errorf("%w: AlphaConfidence=%d < AlphaPreference=%d",
ErrAlphaConfidenceBelowPreference, p.AlphaConfidence, p.AlphaPreference)
}
f := p.ByzantineFaultTolerance()
if 2*p.AlphaPreference-p.K < f+1 {
return fmt.Errorf("%w: K=%d AlphaPreference=%d f=%d (2*%d-%d=%d < %d)",
ErrAlphaBelowBFTQuorum, p.K, p.AlphaPreference, f,
p.AlphaPreference, p.K, 2*p.AlphaPreference-p.K, f+1)
}
return nil
}
// Valid validates parameters with threshold enforcement
func (p Parameters) Valid() error {
// Check K, Alpha, Beta first - these are always required
if p.K < 1 {
return ErrInvalidK
}
// Enforce minimum 2/3 threshold (with small tolerance for rounding)
if p.Alpha < 0.66 || p.Alpha > 1.0 {
return ErrInvalidAlpha
}
if p.Beta < 1 {
return ErrInvalidBeta
}
if p.BlockTime > 0 && p.BlockTime < time.Millisecond {
return ErrBlockTimeTooLow
}
if p.BlockTime > 0 && p.RoundTO > 0 && p.RoundTO < p.BlockTime {
return ErrRoundTimeoutTooLow
}
// The integer QUORUM — the safety-critical subset, defined once in ValidQuorum
// and enforced independently by the engine at Start.
if err := p.ValidQuorum(); err != nil {
return err
}
if p.BetaVirtuous < 0 {
return ErrParametersInvalid
}
if p.BetaRogue != 0 && p.BetaRogue < p.BetaVirtuous {
return ErrParametersInvalid
}
if p.ConcurrentPolls != 0 && p.ConcurrentPolls < 1 {
return ErrParametersInvalid
}
if p.OptimalProcessing != 0 && p.OptimalProcessing < 1 {
return ErrParametersInvalid
}
if p.MaxOutstandingItems != 0 && p.MaxOutstandingItems < 1 {
return ErrParametersInvalid
}
if p.MaxItemProcessingTime != 0 && p.MaxItemProcessingTime <= 0 {
return ErrParametersInvalid
}
return nil
}
// ValidateForNetwork validates parameters are safe for the given network.
// Mainnet (networkID 1) requires K >= 11, testnet (networkID 2) requires K >= 5.
// 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). 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
}
switch networkID {
case 1: // mainnet
if p.K < 11 {
return ErrKTooLowForMainnet
}
case 2: // testnet
if p.K < 5 {
return ErrKTooLowForTestnet
}
}
return nil
}
// ValidateForValueNetwork validates parameters are safe for a chain that
// finalizes REAL VALUE across independent parties (a PoS / value chain — C, D,
// any L1 that custodies funds). It is STRICTER than ValidateForNetwork: a value
// chain must tolerate at least one Byzantine validator (f≥1), so K=3 (f=0) is
// FORBIDDEN regardless of networkID. Single-validator (K==1) value chains are a
// separate, explicitly-chosen regime (POA --dev) and are NOT admitted here —
// value across independent parties requires a real quorum.
//
// K==1 → rejected (use the single-validator regime knowingly, not here)
// K==2,3 → rejected (f=0, no Byzantine tolerance — a single fault forks)
// K>=4 → f≥1, admitted (subject to the mainnet/testnet floors below)
// mainnet(1) → K>=11
// testnet(2) → K>=5
//
// Use this (not ValidateForNetwork) when selecting params for a multi-node value
// chain; the node's value-DEX activation also fails closed on the engine Mode.
func (p Parameters) ValidateForValueNetwork(networkID uint32) error {
if err := p.ValidateForNetwork(networkID); err != nil {
return err
}
if p.ByzantineFaultTolerance() < 1 {
return fmt.Errorf("%w: K=%d f=%d networkID=%d", ErrKTooLowForValue, p.K, p.ByzantineFaultTolerance(), networkID)
}
return nil
}
// MeetsDecentralizationTarget reports whether params meet the network's
// decentralisation TARGET. This is a REPORT, not a gate — deliberately separate
// from the safety predicates above.
//
// Mainnet's target is K >= 11, which yields f = floor((K-1)/3) >= 3: the set can
// lose three validators and still finalise. A smaller set that satisfies the BFT
// overlap bound (see ErrAlphaBelowBFTQuorum) is safe against the faults it CAN
// tolerate — it is simply more fragile, and more centralised.
//
// 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, 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)
}
return nil
}