mirror of
https://github.com/luxfi/consensus.git
synced 2026-09-24 00:26:58 +00:00
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]>
198 lines
6.8 KiB
Go
198 lines
6.8 KiB
Go
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||
// See the file LICENSE for licensing terms.
|
||
|
||
// valid_table_test.go — every clause of the parameter doors, one row each.
|
||
//
|
||
// Valid is the last thing between an operator's config file and a running
|
||
// engine, and each of its clauses refuses one shape of nonsense. A table that
|
||
// only asked "was this refused?" would pass with the whole body replaced by
|
||
// `return ErrParametersInvalid`, so each row here starts from a parameter set
|
||
// proven valid by the control, changes exactly ONE field, and names the door
|
||
// that must close.
|
||
//
|
||
// ValidQuorum is the safety-critical subset — K, the two α's, and the BFT
|
||
// overlap bound — and the engine enforces it independently at Start. Valid runs
|
||
// it and then checks the operational fields around it, so the rows are split the
|
||
// same way the code is.
|
||
package config
|
||
|
||
import (
|
||
"errors"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// TestTheDefaultParametersAreValid is the control. Every refusal below is the
|
||
// default with one field moved, so a broken default would make the whole table
|
||
// pass for the wrong reason.
|
||
func TestTheDefaultParametersAreValid(t *testing.T) {
|
||
p := DefaultParams()
|
||
if err := p.ValidQuorum(); err != nil {
|
||
t.Fatalf("the default quorum is not valid: %v", err)
|
||
}
|
||
if err := p.Valid(); err != nil {
|
||
t.Fatalf("the default parameters are not valid: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestValidQuorumRefusalTable walks the safety-critical subset. The last row is
|
||
// the one the others exist to protect: two α-quorums must overlap in more than f
|
||
// nodes, or two disjoint quorums each certify a conflicting block.
|
||
func TestValidQuorumRefusalTable(t *testing.T) {
|
||
for _, row := range []struct {
|
||
holds string
|
||
change func(*Parameters)
|
||
want error
|
||
}{
|
||
{
|
||
holds: "a sample of no validators is not a sample",
|
||
change: func(p *Parameters) { p.K = 0 },
|
||
want: ErrInvalidK,
|
||
},
|
||
{
|
||
holds: "a negative sample size is refused as the same nonsense",
|
||
change: func(p *Parameters) { p.K = -1 },
|
||
want: ErrInvalidK,
|
||
},
|
||
{
|
||
holds: "a preference quorum of zero would accept on no votes",
|
||
change: func(p *Parameters) { p.AlphaPreference = 0 },
|
||
want: ErrParametersInvalid,
|
||
},
|
||
{
|
||
holds: "a preference quorum larger than the sample can never be reached",
|
||
change: func(p *Parameters) { p.AlphaPreference = p.K + 1 },
|
||
want: ErrParametersInvalid,
|
||
},
|
||
{
|
||
holds: "a confidence quorum of zero would confirm on no votes",
|
||
change: func(p *Parameters) { p.AlphaConfidence = 0 },
|
||
want: ErrParametersInvalid,
|
||
},
|
||
{
|
||
holds: "a confidence quorum larger than the sample can never be reached",
|
||
change: func(p *Parameters) { p.AlphaConfidence = p.K + 1 },
|
||
want: ErrParametersInvalid,
|
||
},
|
||
{
|
||
holds: "confidence is the stronger commitment and may not cost fewer votes than preference",
|
||
change: func(p *Parameters) { p.AlphaConfidence = p.AlphaPreference - 1 },
|
||
want: ErrAlphaConfidenceBelowPreference,
|
||
},
|
||
{
|
||
holds: "an alpha below the BFT overlap bound lets two disjoint quorums certify a fork",
|
||
change: func(p *Parameters) { p.AlphaPreference, p.AlphaConfidence = p.K/2+1, p.K/2+1 },
|
||
want: ErrAlphaBelowBFTQuorum,
|
||
},
|
||
} {
|
||
t.Run(row.holds, func(t *testing.T) {
|
||
p := DefaultParams()
|
||
row.change(&p)
|
||
if err := p.ValidQuorum(); !errors.Is(err, row.want) {
|
||
t.Fatalf("ValidQuorum: want %v, got %v", err, row.want)
|
||
}
|
||
// Valid runs the quorum door first, so the same parameters are
|
||
// refused there with the same reason — one definition, two callers.
|
||
if err := p.Valid(); !errors.Is(err, row.want) {
|
||
t.Fatalf("Valid: want %v, got %v", row.want, err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestValidRefusalTable walks the operational clauses — the ones outside the
|
||
// safety-critical subset. They bound the fields that size queues and timers, and
|
||
// each refuses only the values that make no sense rather than every value an
|
||
// operator might reasonably choose, so zero is left meaning "unset" throughout.
|
||
func TestValidRefusalTable(t *testing.T) {
|
||
for _, row := range []struct {
|
||
holds string
|
||
change func(*Parameters)
|
||
want error
|
||
}{
|
||
{
|
||
holds: "a threshold below two thirds is not a supermajority",
|
||
change: func(p *Parameters) { p.Alpha = 0.5 },
|
||
want: ErrInvalidAlpha,
|
||
},
|
||
{
|
||
holds: "a threshold above unity asks for more votes than exist",
|
||
change: func(p *Parameters) { p.Alpha = 1.5 },
|
||
want: ErrInvalidAlpha,
|
||
},
|
||
{
|
||
holds: "a confidence depth of zero accepts on the first round it sees",
|
||
change: func(p *Parameters) { p.Beta = 0 },
|
||
want: ErrInvalidBeta,
|
||
},
|
||
{
|
||
holds: "a block time below a millisecond is a timer the scheduler cannot honour",
|
||
change: func(p *Parameters) { p.BlockTime = time.Microsecond },
|
||
want: ErrBlockTimeTooLow,
|
||
},
|
||
{
|
||
holds: "a round shorter than a block cannot contain one",
|
||
change: func(p *Parameters) { p.RoundTO = p.BlockTime - time.Millisecond },
|
||
want: ErrRoundTimeoutTooLow,
|
||
},
|
||
{
|
||
holds: "a negative virtuous depth is not a depth",
|
||
change: func(p *Parameters) { p.BetaVirtuous = -1 },
|
||
want: ErrParametersInvalid,
|
||
},
|
||
{
|
||
holds: "a rogue depth below the virtuous one would confirm a contested block sooner",
|
||
change: func(p *Parameters) { p.BetaRogue = p.BetaVirtuous - 1 },
|
||
want: ErrParametersInvalid,
|
||
},
|
||
{
|
||
holds: "a negative poll concurrency is not a count of polls",
|
||
change: func(p *Parameters) { p.ConcurrentPolls = -1 },
|
||
want: ErrParametersInvalid,
|
||
},
|
||
{
|
||
holds: "a negative processing target is not a count of items",
|
||
change: func(p *Parameters) { p.OptimalProcessing = -1 },
|
||
want: ErrParametersInvalid,
|
||
},
|
||
{
|
||
holds: "a negative outstanding-item bound is not a bound",
|
||
change: func(p *Parameters) { p.MaxOutstandingItems = -1 },
|
||
want: ErrParametersInvalid,
|
||
},
|
||
{
|
||
holds: "a negative processing deadline is a deadline already past",
|
||
change: func(p *Parameters) { p.MaxItemProcessingTime = -time.Second },
|
||
want: ErrParametersInvalid,
|
||
},
|
||
} {
|
||
t.Run(row.holds, func(t *testing.T) {
|
||
p := DefaultParams()
|
||
row.change(&p)
|
||
if err := p.Valid(); !errors.Is(err, row.want) {
|
||
t.Fatalf("want %v, got %v", row.want, err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestZeroMeansUnsetForTheOperationalFields is the other half of the clauses
|
||
// above, and the reason they are written `!= 0 && < 1` rather than `< 1`: an
|
||
// operator that leaves a field out gets the engine's own default, not a refusal.
|
||
// Testing only the refusals would let the guards tighten onto zero unnoticed,
|
||
// which would reject every partially-specified config in the fleet.
|
||
func TestZeroMeansUnsetForTheOperationalFields(t *testing.T) {
|
||
p := DefaultParams()
|
||
p.BetaRogue = 0
|
||
p.ConcurrentPolls = 0
|
||
p.OptimalProcessing = 0
|
||
p.MaxOutstandingItems = 0
|
||
p.MaxItemProcessingTime = 0
|
||
p.BlockTime = 0
|
||
p.RoundTO = 0
|
||
|
||
if err := p.Valid(); err != nil {
|
||
t.Fatalf("an unset operational field must mean unset, not invalid: %v", err)
|
||
}
|
||
}
|