Files
consensus/config/parameters_test.go
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

375 lines
9.1 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 (
"testing"
"time"
)
func TestDefaultParams(t *testing.T) {
p := DefaultParams()
if p.K != 20 {
t.Errorf("expected K=20, got %d", p.K)
}
// 69% threshold update
if p.Alpha != 0.69 {
t.Errorf("expected Alpha=0.69 (69%% threshold), got %f", p.Alpha)
}
if p.Beta != 14 {
t.Errorf("expected Beta=14 (adjusted for 69%%), got %d", p.Beta)
}
if p.RoundTO != 250*time.Millisecond {
t.Errorf("expected RoundTO=250ms, got %v", p.RoundTO)
}
if p.BlockTime != 100*time.Millisecond {
t.Errorf("expected BlockTime=100ms, got %v", p.BlockTime)
}
}
func TestXChainParams(t *testing.T) {
p := XChainParams()
// Test ultra-fast 1ms blocks for 100Gbps networks
if p.BlockTime != 1*time.Millisecond {
t.Errorf("X-Chain should support 1ms blocks, got %v", p.BlockTime)
}
if p.RoundTO != 5*time.Millisecond {
t.Errorf("X-Chain round timeout should be 5ms, got %v", p.RoundTO)
}
if p.K != 3 {
t.Errorf("X-Chain should have K=3, got %d", p.K)
}
// 2/3 threshold
if p.Alpha != 0.67 {
t.Errorf("X-Chain should have Alpha=0.67 (2/3 threshold), got %f", p.Alpha)
}
if p.Beta != 2 {
t.Errorf("X-Chain should have Beta=2 (adjusted for 2/3), got %d", p.Beta)
}
}
func TestMainnetParams(t *testing.T) {
p := MainnetParams()
if p.K != 21 {
t.Errorf("Mainnet should have 21 validators, got %d", p.K)
}
// 69% threshold update
if p.Alpha != 0.69 {
t.Errorf("Mainnet Alpha should be 0.69 (69%% threshold), got %f", p.Alpha)
}
if p.Beta != 14 {
t.Errorf("Mainnet Beta should be 14 (adjusted for 69%%), got %d", p.Beta)
}
if p.BlockTime != 200*time.Millisecond {
t.Errorf("Mainnet BlockTime should be 200ms, got %v", p.BlockTime)
}
}
func TestTestnetParams(t *testing.T) {
p := TestnetParams()
if p.K != 11 {
t.Errorf("Testnet should have 11 validators, got %d", p.K)
}
// 69% threshold update
if p.Alpha != 0.69 {
t.Errorf("Testnet Alpha should be 0.69 (69%% threshold), got %f", p.Alpha)
}
if p.Beta != 8 {
t.Errorf("Testnet Beta should be 8 (adjusted for 69%%), got %d", p.Beta)
}
}
func TestLocalParams(t *testing.T) {
p := LocalParams()
if p.K != 3 {
t.Errorf("Local should have 3 validators, got %d", p.K)
}
if p.BlockTime != 1*time.Millisecond {
t.Errorf("Local BlockTime should be 1ms, got %v", p.BlockTime)
}
if p.RoundTO != 5*time.Millisecond {
t.Errorf("Local RoundTO should be 5ms, got %v", p.RoundTO)
}
}
func TestSoloGPUParams(t *testing.T) {
p := SoloGPUParams()
if p.K != 1 {
t.Errorf("SoloGPU K should be 1, got %d", p.K)
}
if p.BlockTime != 1*time.Millisecond {
t.Errorf("SoloGPU BlockTime should be 1ms, got %v", p.BlockTime)
}
if p.GasLimit != 1_000_000_000 {
t.Errorf("SoloGPU GasLimit should be 1B, got %d", p.GasLimit)
}
if err := p.Validate(); err != nil {
t.Errorf("SoloGPU params should be valid, got %v", err)
}
// 1B gas / 21000 gas = 47,619 txs/block × 1000 blocks/sec = 47.6M TPS ceiling
txsPerBlock := p.GasLimit / 21000
if txsPerBlock != 47619 {
t.Errorf("Expected 47619 txs/block, got %d", txsPerBlock)
}
}
func TestBurstParams(t *testing.T) {
p := BurstParams()
if p.BlockTime != 1*time.Millisecond {
t.Errorf("Burst BlockTime should be 1ms, got %v", p.BlockTime)
}
if p.RoundTO != 5*time.Millisecond {
t.Errorf("Burst RoundTO should be 5ms, got %v", p.RoundTO)
}
if p.GasLimit != 2_100_000_000 {
t.Errorf("Burst GasLimit should be 2.1B, got %d", p.GasLimit)
}
if p.MaxOutstandingItems != 8192 {
t.Errorf("Burst MaxOutstandingItems should be 8192, got %d", p.MaxOutstandingItems)
}
if p.ConcurrentPolls != 8 {
t.Errorf("Burst ConcurrentPolls should be 8, got %d", p.ConcurrentPolls)
}
if err := p.Validate(); err != nil {
t.Errorf("Burst params should be valid, got %v", err)
}
// Verify: 2.1B gas / 21000 gas per tx = 100K txs/block
// 100K txs × 1000 blocks/sec = 100M TPS
txsPerBlock := p.GasLimit / 21000
tps := txsPerBlock * 1000 // 1ms blocks = 1000 blocks/sec
if tps != 100_000_000 {
t.Errorf("Burst TPS should be 100M, got %d", tps)
}
}
func TestValidate(t *testing.T) {
tests := []struct {
name string
params Parameters
wantErr error
}{
{
name: "valid params",
params: DefaultParams(),
wantErr: nil,
},
{
name: "invalid K",
params: Parameters{
K: 0,
Alpha: 0.69, // 69% threshold
Beta: 14, // Adjusted for 69%
RoundTO: 250 * time.Millisecond,
BlockTime: 100 * time.Millisecond,
},
wantErr: ErrInvalidK,
},
{
name: "invalid Alpha too low",
params: Parameters{
K: 20,
Alpha: 0.3,
Beta: 15,
RoundTO: 250 * time.Millisecond,
BlockTime: 100 * time.Millisecond,
},
wantErr: ErrInvalidAlpha,
},
{
name: "invalid Alpha too high",
params: Parameters{
K: 20,
Alpha: 1.5,
Beta: 15,
RoundTO: 250 * time.Millisecond,
BlockTime: 100 * time.Millisecond,
},
wantErr: ErrInvalidAlpha,
},
{
name: "invalid Beta",
params: Parameters{
K: 20,
Alpha: 0.69, // 69% threshold
Beta: 0,
RoundTO: 250 * time.Millisecond,
BlockTime: 100 * time.Millisecond,
},
wantErr: ErrInvalidBeta,
},
{
name: "block time too low",
params: Parameters{
K: 20,
Alpha: 0.69, // 69% threshold
Beta: 14, // Adjusted for 69%
RoundTO: 250 * time.Millisecond,
BlockTime: 500 * time.Microsecond, // < 1ms
},
wantErr: ErrBlockTimeTooLow,
},
{
name: "round timeout too low",
params: Parameters{
K: 20,
Alpha: 0.69, // 69% threshold
Beta: 14, // Adjusted for 69%
RoundTO: 50 * time.Millisecond,
BlockTime: 100 * time.Millisecond,
},
wantErr: ErrRoundTimeoutTooLow,
},
{
// K=5 → α=4 (⌊2·5/3⌋+1, which is also the BFT floor for K=5). The
// integer α is required for a config to be valid at all — a params
// value with AlphaPreference=0 is a zero-vote quorum, not a default.
name: "1ms blocks are valid",
params: Parameters{
K: 5,
Alpha: 0.69, // 69% threshold
Beta: 4, // Adjusted for 69%
AlphaPreference: 4,
AlphaConfidence: 4,
RoundTO: 5 * time.Millisecond,
BlockTime: 1 * time.Millisecond,
},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.params.Validate()
if err != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestValidateForNetwork(t *testing.T) {
tests := []struct {
name string
params Parameters
networkID uint32
wantErr error
}{
// Mainnet (networkID=1): K >= 11 required
{
name: "mainnet params on mainnet",
params: MainnetParams(),
networkID: 1,
wantErr: nil,
},
{
name: "testnet params on mainnet",
params: TestnetParams(),
networkID: 1,
wantErr: nil, // K=11, exactly at minimum
},
{
name: "burst params on mainnet rejected",
params: BurstParams(),
networkID: 1,
wantErr: ErrKTooLowForMainnet,
},
{
name: "solo GPU params on mainnet rejected",
params: SoloGPUParams(),
networkID: 1,
wantErr: ErrKTooLowForMainnet,
},
{
name: "local params on mainnet rejected",
params: LocalParams(),
networkID: 1,
wantErr: ErrKTooLowForMainnet,
},
// Testnet (networkID=2, the real Lux testnet — NOT the legacy 5): K >= 5.
{
name: "testnet params on testnet",
params: TestnetParams(),
networkID: 2,
wantErr: nil,
},
{
name: "burst params on testnet rejected",
params: BurstParams(),
networkID: 2,
wantErr: ErrKTooLowForTestnet,
},
{
name: "solo GPU params on testnet rejected",
params: SoloGPUParams(),
networkID: 2,
wantErr: ErrKTooLowForTestnet,
},
// Regression: the legacy id 5 must NOT spuriously gate as testnet (the
// floor was wrongly keyed to 5 and never fired for the real testnet 2).
// On id 5 only the base Valid() applies, so a weak-but-Valid set passes.
{
name: "burst params on legacy id 5 (no testnet floor)",
params: BurstParams(),
networkID: 5,
wantErr: nil,
},
// Local/devnet (networkID >= 1337): any K allowed
{
name: "burst params on local",
params: BurstParams(),
networkID: 1337,
wantErr: nil,
},
{
name: "solo GPU params on local",
params: SoloGPUParams(),
networkID: 1337,
wantErr: nil,
},
{
name: "local params on devnet",
params: LocalParams(),
networkID: 9999,
wantErr: nil,
},
// Basic validation still runs first
{
name: "invalid params fail before network check",
params: Parameters{
K: 0,
Alpha: 0.69,
Beta: 14,
},
networkID: 1337,
wantErr: ErrInvalidK,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.params.ValidateForNetwork(tt.networkID)
if err != tt.wantErr {
t.Errorf("ValidateForNetwork(%d) error = %v, wantErr %v", tt.networkID, err, tt.wantErr)
}
})
}
}
func BenchmarkValidate(b *testing.B) {
p := DefaultParams()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = p.Validate()
}
}