Files
consensus/engine/chain/ladder_floor_test.go
zeekayandHanzo Dev 43c74b012d go: name every refusal, then check that the named one is the one given
QuorumCert.Verify returns a different typed error for each clause of the finality
predicate, precisely so one refusal can be told from another. Fourteen are
declared. Four were ever named by a test — the two the R5 floors turn on, plus
ordering and a bad signature — and the other ten had nothing asserted on them at
all: nil cert, nil verifier, version, role, unknown tier, zero threshold, no
votes, a non-accept vote, the nova stake floor. Every one of those clauses would
have passed with its body replaced by `return errors.New("no")`, which is the
shape of test that measures nothing.

So every clause now has a row: a certificate proven valid by the control above
it, one field changed, and the error that clause owes. Wrong version, wrong role,
a tier that is neither rung, a threshold of zero, no votes, a duplicate voter,
voters out of order, a reject inside a finality cert, a signature over another
position, a voter nothing can resolve, a quorum larger than the votes carried —
and on the weighted side, an unresolved set, a set below the minimum Byzantine
committee, signable stake of zero, the seats without the stake and the stake
without the seats. The same table exists in Rust and in C++; the three are meant
to be read side by side, because three implementations refusing one certificate
for three reasons is a fork waiting for the cert they disagree about.

The fifteenth name, ErrQCVotePosition, is returned by nothing. The var block's
comment says each maps 1:1 onto a clause; that one maps onto no clause, because a
vote that signed another position fails as a bad signature — the message is
rebuilt from the CERT's position and never from the vote's. It is left alone here
and reported rather than covered, since no input reaches it.

The proof-of-possession rejects follow the frozen corpus and go past it: the
class is asserted, not merely that something failed. Encoding before possession
is what keeps a pairing off a point that was never a point, and a test that only
checked for non-nil would pass with the order reversed. The corpus has no
49-byte-key case; this side carries one, because the clause is `!=` and a
verifier guarding only the short side would check a proof against a PREFIX of the
published key.

Elsewhere: the parameter doors (every clause, and zero still meaning unset),
the corpus's own stake seam (three projections over the signers, the fourth over
the roll), the two arithmetic doors that must not wrap, the warp filter/sum/
aggregate read as the step a forged message would attack, the vote frame's
refusals including a four-gigabyte signature that is not there, and the durable
guard: what survives a restart, what a floor that could not be written must not
report, and every shape of file that is present and does not decode.

  engine/chain 87.2 → 89.7 · validator 84.7 → 98.3 · pop 85.3 → 91.2
  config 99.0 → 100 · conformance 93.8 → 97.4

Of the named core, cert/finality/quorum/quasar/descent/guard/verified_cert are
whole; what remains there is four statements no caller can reach, written down in
LLM.md so the next reader does not spend an afternoon each on them. The rest of
the gap is engine.go and the VM/network integration surface, which wants
multi-node harnesses rather than unit tests.

Tests only — no consensus logic moved.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 22:19:35 -07:00

174 lines
6.8 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.
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// ladder_floor_test.go — the rungs and the Nova floors, at their boundaries.
//
// Two things live here. The LADDER's names and its three authorization
// predicates: the names are what RPC status and metrics carry verbatim, so a
// rename is an interface break and not a cosmetic one, and the predicates are
// the boundary a bridge gates on. And the NOVA floors as functions of the live
// count n — read at n = 0 and n = 1, where the whole reason they are written as
// functions and not as constants is that a transiently-empty validator view must
// not be allowed to self-accept.
package chain
import "testing"
// TestTheLadderNamesAreTheOntology holds each rung's name to the lowercase
// astrophysics word. These strings are the RPC status and the metric label; a
// generic finality word appearing here is a change every consumer sees.
func TestTheLadderNamesAreTheOntology(t *testing.T) {
for _, row := range []struct {
rung Finality
name string
}{
{Photon, "photon"},
{Wave, "wave"},
{Nova, "nova"},
{Quasar, "quasar"},
{Horizon, "horizon"},
} {
if got := row.rung.String(); got != row.name {
t.Fatalf("rung %d is %q, want %q", row.rung, got, row.name)
}
}
// A rung off the ladder names itself as such rather than as the last rung it
// happens to sit above — a wire-decoded byte out of range must not print as
// "horizon" and read as irreversible.
if got := Finality(200).String(); got != "unknown" {
t.Fatalf("a rung off the ladder is %q, want %q", got, "unknown")
}
}
// TestTheAuthorizationBoundariesAreDistinct is THE INVARIANT read as a table:
// each of the three predicates fires at exactly one rung and not one rung lower.
// Collapsing any two of them is how a block below Quasar leaves the chain.
func TestTheAuthorizationBoundariesAreDistinct(t *testing.T) {
for _, row := range []struct {
rung Finality
local, export, irreversible bool
}{
{Photon, false, false, false},
{Wave, false, false, false},
{Nova, true, false, false},
{Quasar, true, true, false},
{Horizon, true, true, true},
} {
if got := row.rung.AuthorizesLocalExecution(); got != row.local {
t.Fatalf("%s authorizes local execution = %v, want %v", row.rung, got, row.local)
}
if got := row.rung.AuthorizesExport(); got != row.export {
t.Fatalf("%s authorizes export = %v, want %v", row.rung, got, row.export)
}
if got := row.rung.AuthorizesIrreversibleSettlement(); got != row.irreversible {
t.Fatalf("%s authorizes irreversible settlement = %v, want %v", row.rung, got, row.irreversible)
}
}
}
// TestNovaQuorumNeverReturnsZero is the 1085013 guard stated directly: an
// unresolved or empty validator view reports n<1, and a quorum of zero would let
// a node whose set momentarily read empty accept its own block. The floor is one.
func TestNovaQuorumNeverReturnsZero(t *testing.T) {
for _, n := range []int{-1000, -1, 0} {
if q := NovaQuorum(n); q != 1 {
t.Fatalf("NovaQuorum(%d) = %d, want 1 — an unresolved set must never quorum at zero", n, q)
}
}
for _, row := range []struct{ n, want int }{
{1, 1}, {2, 2}, {3, 2}, {4, 3}, {5, 3}, {6, 4}, {21, 11},
} {
if q := NovaQuorum(row.n); q != row.want {
t.Fatalf("NovaQuorum(%d) = %d, want %d", row.n, q, row.want)
}
}
}
// TestNovaSignerFloorSaturates holds the difference between the two rungs'
// count floors. Nova's asks only "is this more than one party?" before a block
// may drive local execution the chain can still reorg away, so it saturates at
// the majority of the minimum BFT committee and does not grow with the set. It
// is capped by the live majority so a genuinely small chain stays satisfiable.
func TestNovaSignerFloorSaturates(t *testing.T) {
ceiling := NovaQuorum(minBFTCommittee)
for _, row := range []struct{ n, want int }{
{0, 1}, {1, 1}, {2, 2}, {3, 2}, {4, 3}, {5, 3}, {100, 3}, {10000, 3},
} {
if got := NovaSignerFloor(row.n); got != row.want {
t.Fatalf("NovaSignerFloor(%d) = %d, want %d", row.n, got, row.want)
}
}
if NovaSignerFloor(10000) != ceiling {
t.Fatalf("the floor must saturate at NovaQuorum(minBFTCommittee)=%d", ceiling)
}
// And it never exceeds the majority it is capped by, at any size.
for n := 0; n <= 64; n++ {
if f, q := NovaSignerFloor(n), NovaQuorum(n); f > q {
t.Fatalf("NovaSignerFloor(%d)=%d exceeds NovaQuorum(%d)=%d — a small chain cannot ignite", n, f, n, q)
}
}
}
// TestCrashToleranceIsCeilHalfMinusOne states the degraded-mode signal against
// the closed form its documentation claims — ⌈n/2⌉−1 — computed here rather than
// taken from NovaQuorum, so the two derivations have to agree. Reading it back
// as `n - NovaQuorum(n)` would restate the implementation and hold nothing: it
// would still pass with the majority itself wrong.
func TestCrashToleranceIsCeilHalfMinusOne(t *testing.T) {
for _, n := range []int{-1, 0, 1} {
if got := CrashTolerance(n); got != 0 {
t.Fatalf("CrashTolerance(%d) = %d, want 0 — a lone node survives no loss", n, got)
}
}
for n := 2; n <= 64; n++ {
want := (n+1)/2 - 1 // ⌈n/2⌉ − 1
if got := CrashTolerance(n); got != want {
t.Fatalf("CrashTolerance(%d) = %d, want ceil(n/2)-1 = %d", n, got, want)
}
// And the property that closed form exists to state: losing exactly the
// tolerance still leaves a majority, losing one more does not.
if n-CrashTolerance(n) < NovaQuorum(n) {
t.Fatalf("at n=%d, losing %d leaves %d alive, below the majority of %d",
n, CrashTolerance(n), n-CrashTolerance(n), NovaQuorum(n))
}
if n-CrashTolerance(n)-1 >= NovaQuorum(n) {
t.Fatalf("at n=%d the tolerance %d understates what nova survives", n, CrashTolerance(n))
}
}
}
// TestNovaBetaIsOneOnlyForALoneNode holds the hysteresis rule: a lone node has
// no peer to confirm against, so any wait is a phantom-peer stall; from two
// upward one confirming round stops a transient majority flip igniting.
func TestNovaBetaIsOneOnlyForALoneNode(t *testing.T) {
for _, n := range []int{-1, 0, 1} {
if got := NovaBeta(n); got != 1 {
t.Fatalf("NovaBeta(%d) = %d, want 1", n, got)
}
}
for _, n := range []int{2, 3, 4, 21, 1000} {
if got := NovaBeta(n); got != 2 {
t.Fatalf("NovaBeta(%d) = %d, want 2", n, got)
}
}
}
// TestTheModeNamesAreStable pins the three regime names. They are what a log
// line and an operator's alarm read, and "unknown" in particular is the degraded
// answer — it must never render as one of the two live regimes.
func TestTheModeNamesAreStable(t *testing.T) {
for _, row := range []struct {
mode ConsensusMode
name string
}{
{ModeSingleValidator, "single-validator"},
{ModeQuorumFinality, "quorum-finality"},
{ModeUnknown, "unknown"},
{ConsensusMode(99), "unknown"},
} {
if got := row.mode.String(); got != row.name {
t.Fatalf("mode %d is %q, want %q", row.mode, got, row.name)
}
}
}