mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
consensus(chain): dynamically clamp K/α to live validator count
A static preset (TestnetParams K=11/α=8) assigned to a network smaller than the preset wedges finality: the α-of-K quorum cert demands more affirmative votes than there are validators, so every block verifies but none finalizes. NewRuntime now scales an oversized committee down to the live set and derives α = ⌊2K/3⌋+1 — the BFT supermajority every preset already encodes (K4→α3, K11→α8, K20→α14, K21→α15) — at the single chokepoint where the validator count is known. A preset that already fits is untouched. The ⅔-stake cert still layers weighted safety on top, so a smaller committee never weakens the supermajority. One mechanism: any validator count boots a satisfiable, BFT-valid quorum without per-network tuning to drift.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package chain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/consensus/config"
|
||||
)
|
||||
|
||||
// TestBFTCommittee_ClampsOversizedPresetToLiveSet is the regression for the
|
||||
// testnet finality wedge: TestnetParams (K=11, α=8) was assigned to a network
|
||||
// running only 5 validators, so the α-of-K quorum cert demanded 8 affirmative
|
||||
// votes from 5 nodes — unreachable — and the chain wedged (every block verified,
|
||||
// no block ever finalized). bftCommittee must shrink K to the live set and pick a
|
||||
// reachable BFT quorum.
|
||||
func TestBFTCommittee_ClampsOversizedPresetToLiveSet(t *testing.T) {
|
||||
k, alpha, clamped := bftCommittee(11, 5) // testnet's exact wedge
|
||||
if !clamped {
|
||||
t.Fatalf("K=11 with 5 validators MUST clamp; got clamped=false")
|
||||
}
|
||||
if k != 5 {
|
||||
t.Errorf("K should clamp to live count 5, got %d", k)
|
||||
}
|
||||
if alpha != 4 { // ⌊2·5/3⌋+1 = 4 → 4-of-5, a reachable BFT supermajority
|
||||
t.Errorf("α for 5 validators should be 4, got %d", alpha)
|
||||
}
|
||||
if alpha > k {
|
||||
t.Errorf("α=%d > K=%d is unsatisfiable — the wedge would persist", alpha, k)
|
||||
}
|
||||
// BFT α-floor: 2α − K ≥ f+1 with f = ⌊(K-1)/3⌋. For K=5: f=1, 2·4−5=3 ≥ 2. ✓
|
||||
if f := (k - 1) / 3; 2*alpha-k < f+1 {
|
||||
t.Errorf("α=%d K=%d violates BFT floor 2α−K ≥ f+1 (f=%d)", alpha, k, f)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBFTCommittee_LeavesFittingPresetsUntouched proves the clamp never rewrites a
|
||||
// preset that already fits the validator set — its hand-tuned α (which is NOT
|
||||
// always ⌊2K/3⌋+1, e.g. LocalParams K=3/α=2) must survive verbatim.
|
||||
func TestBFTCommittee_LeavesFittingPresetsUntouched(t *testing.T) {
|
||||
for _, count := range []int{11, 21, 50} { // count ≥ K for each preset below
|
||||
for name, k := range map[string]int{"default": 20, "mainnet": 21, "testnet": 11} {
|
||||
if k > count {
|
||||
continue
|
||||
}
|
||||
if _, _, clamped := bftCommittee(k, count); clamped {
|
||||
t.Errorf("%s preset (K=%d) with %d validators must NOT clamp", name, k, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
// count unknown (-1) or empty (0): never clamp — a missing/empty set must not
|
||||
// force K to a degenerate value.
|
||||
for _, count := range []int{0, -1} {
|
||||
if _, _, clamped := bftCommittee(20, count); clamped {
|
||||
t.Errorf("count=%d must not clamp", count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBFTCommittee_FormulaReproducesEveryPreset is the invariant that lets the
|
||||
// clamp reuse the BFT formula safely: ⌊2K/3⌋+1 equals the α every production
|
||||
// preset already encodes, so a clamped committee is indistinguishable from a
|
||||
// preset authored for that exact size.
|
||||
func TestBFTCommittee_FormulaReproducesEveryPreset(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
params config.Parameters
|
||||
wantedAlpha int
|
||||
}{
|
||||
{"DefaultParams", config.DefaultParams(), 14}, // K=20 → 14
|
||||
{"MainnetParams", config.MainnetParams(), 15}, // K=21 → 15
|
||||
{"TestnetParams", config.TestnetParams(), 8}, // K=11 → 8
|
||||
{"LocalBFTParams", config.LocalBFTParams(), 3}, // K=4 → 3
|
||||
}
|
||||
for _, tc := range cases {
|
||||
// Clamp a network whose live set is exactly the preset's K: bftCommittee is
|
||||
// invoked with count == K-? Use count = K so the "shrink" path with
|
||||
// count==k is a no-op; instead verify the formula directly against α.
|
||||
gotAlpha := 2*tc.params.K/3 + 1
|
||||
if gotAlpha != tc.wantedAlpha {
|
||||
t.Errorf("%s: ⌊2·%d/3⌋+1 = %d, preset α = %d — formula diverges from preset",
|
||||
tc.name, tc.params.K, gotAlpha, tc.wantedAlpha)
|
||||
}
|
||||
if tc.params.AlphaPreference != tc.wantedAlpha {
|
||||
t.Errorf("%s: preset AlphaPreference=%d, expected %d (preset drift)",
|
||||
tc.name, tc.params.AlphaPreference, tc.wantedAlpha)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,22 @@ type Runtime struct {
|
||||
fastFollowHeight uint64
|
||||
}
|
||||
|
||||
// bftCommittee scales a preset sample size k down to the live validator count and
|
||||
// returns the BFT-supermajority quorum α = ⌊2k/3⌋+1 for the resulting committee.
|
||||
// It only ever shrinks an oversized committee (count < k) — a preset that already
|
||||
// fits (k ≤ count) is reported unchanged (clamped=false) so its hand-tuned α is
|
||||
// preserved. The α formula reproduces every preset exactly: K4→α3, K11→α8,
|
||||
// K20→α14, K21→α15, guaranteeing the result clears the BFT α-floor (2α−k ≥ f+1)
|
||||
// while staying reachable (α ≤ k). This is the one mechanism that keeps a network
|
||||
// from booting with an unsatisfiable quorum (α > live validators), which wedges
|
||||
// finality permanently: every block verifies but the α-of-K cert never assembles.
|
||||
func bftCommittee(k, count int) (newK, alpha int, clamped bool) {
|
||||
if count <= 0 || k <= count {
|
||||
return k, 0, false
|
||||
}
|
||||
return count, 2*count/3 + 1, true
|
||||
}
|
||||
|
||||
// NewRuntime creates a fully wired consensus runtime ready for production use.
|
||||
//
|
||||
// This is the single, canonical way to create a chain consensus runtime for node integration.
|
||||
@@ -181,6 +197,29 @@ func NewRuntime(cfg NetworkConfig) *Runtime {
|
||||
if cfg.Params != nil {
|
||||
params = *cfg.Params
|
||||
}
|
||||
|
||||
// Dynamic committee sizing (liveness, one mechanism): a static preset K can
|
||||
// exceed the live validator count. TestnetParams (K=11, α=8) on a 5-validator
|
||||
// network demands 8 affirmative votes from 5 nodes — unreachable — so NO block
|
||||
// ever finalizes: each block verifies but the α-of-K quorum cert never
|
||||
// assembles and the chain wedges. Scale K down to the live set and α to the BFT
|
||||
// supermajority ⌊2K/3⌋+1 — the exact relation every preset already encodes
|
||||
// (K4→α3, K11→α8, K20→α14, K21→α15) — so any validator count yields a
|
||||
// satisfiable, BFT-valid (K, α) with no per-network preset tuning to drift. The
|
||||
// clamp only ever shrinks an oversized committee (params.K > count); a preset
|
||||
// that already fits (K ≤ count) is untouched. The ⅔-STAKE cert
|
||||
// (WithStakeWeighting, below) still layers weighted BFT safety on top of this
|
||||
// count floor, so a smaller committee never weakens the supermajority guarantee.
|
||||
validatorCount := -1
|
||||
if cfg.Validators != nil {
|
||||
validatorCount = cfg.Validators.Count(cfg.NetworkID)
|
||||
if k, alpha, clamped := bftCommittee(params.K, validatorCount); clamped {
|
||||
params.K = k
|
||||
params.AlphaPreference = alpha
|
||||
params.AlphaConfidence = alpha
|
||||
}
|
||||
}
|
||||
|
||||
engine := NewWithParams(params)
|
||||
|
||||
// Wire α-of-K quorum-cert finality for multi-validator chains. The engine
|
||||
@@ -230,12 +269,11 @@ func NewRuntime(cfg NetworkConfig) *Runtime {
|
||||
|
||||
// Log validator set status for debugging
|
||||
hasLogger := cfg.Logger != nil && !cfg.Logger.IsZero()
|
||||
if cfg.Validators != nil {
|
||||
count := cfg.Validators.Count(cfg.NetworkID)
|
||||
if validatorCount >= 0 {
|
||||
if hasLogger {
|
||||
cfg.Logger.Info("consensus engine initialized with validator set",
|
||||
log.Stringer("networkID", cfg.NetworkID),
|
||||
log.Int("validatorCount", count),
|
||||
log.Int("validatorCount", validatorCount),
|
||||
log.Int("k", params.K),
|
||||
log.Int("alpha", params.AlphaPreference))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user