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]>
106 lines
4.0 KiB
Go
106 lines
4.0 KiB
Go
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// registration_overflow_test.go — the two clauses that are about ARITHMETIC
|
|
// rather than about identity, plus the nil entry.
|
|
//
|
|
// The total weight is what both stake floors are taken of, and Go's + is
|
|
// modular: a sum past 2^64 does not fail, it returns a different number and
|
|
// every floor read against it is a floor read against a lie. Two voters at 2^63
|
|
// wrap to zero, and a "majority of zero" is any vote at all. So both doors check
|
|
// the add before they take it, and both must — the two doors admit the same
|
|
// sets, and a set one accepts and the other wraps on is a set two nodes disagree
|
|
// about the existence of.
|
|
package validators
|
|
|
|
import (
|
|
"errors"
|
|
"math"
|
|
"testing"
|
|
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
// TestRegisterRefusesATotalThatWraps holds the admission door's arithmetic. The
|
|
// two weights are individually representable and their sum is not; the door must
|
|
// name the overflow rather than admit a set whose total is smaller than one of
|
|
// its members.
|
|
func TestRegisterRefusesATotalThatWraps(t *testing.T) {
|
|
big := registration(t, 41, 0x01, math.MaxUint64-1)
|
|
rest := registration(t, 42, 0x02, 2)
|
|
|
|
set, err := Register([]Registration{big, rest})
|
|
if !errors.Is(err, ErrWeightOverflow) {
|
|
t.Fatalf("want the overflow refusal, got %v", err)
|
|
}
|
|
if len(set.Validators) != 0 || set.TotalWeight != 0 {
|
|
t.Fatal("a refused registration returned a set — a caller must not read one out of a no")
|
|
}
|
|
|
|
// The same two weights one unit apart do fit, so the refusal above is the
|
|
// boundary and not a blanket rejection of large stake.
|
|
fits := registration(t, 42, 0x02, 1)
|
|
ok, err := Register([]Registration{big, fits})
|
|
if err != nil {
|
|
t.Fatalf("a total of exactly MaxUint64 is representable: %v", err)
|
|
}
|
|
if ok.TotalWeight != math.MaxUint64 {
|
|
t.Fatalf("total weight = %d, want MaxUint64", ok.TotalWeight)
|
|
}
|
|
}
|
|
|
|
// TestRegisterRefusesAKeyedSeatWithNoStake is the phantom signer at the
|
|
// admission door — the seat that raises the count of distinct signers a floor is
|
|
// read against and raises the weight by nothing. Flatten refuses it with the
|
|
// same error; that agreement is what makes the two doors one rule.
|
|
func TestRegisterRefusesAKeyedSeatWithNoStake(t *testing.T) {
|
|
good := registration(t, 51, 0x01, 10)
|
|
phantom := registration(t, 52, 0x02, 0)
|
|
|
|
set, err := Register([]Registration{good, phantom})
|
|
if !errors.Is(err, ErrZeroWeight) {
|
|
t.Fatalf("want the zero-weight refusal, got %v", err)
|
|
}
|
|
if len(set.Validators) != 0 {
|
|
t.Fatal("the set is admitted whole or not at all")
|
|
}
|
|
}
|
|
|
|
// TestFlattenRefusesATotalThatWraps is the same arithmetic on the other door,
|
|
// and it is checked BEFORE the key is looked at — a set whose total does not
|
|
// exist is refused whether or not this node could have parsed its keys.
|
|
func TestFlattenRefusesATotalThatWraps(t *testing.T) {
|
|
a, first := output(0x01, secret(t, 61), math.MaxUint64-1)
|
|
b, second := output(0x02, secret(t, 62), 2)
|
|
|
|
set, err := FlattenValidatorSet(map[ids.NodeID]*GetValidatorOutput{a: first, b: second})
|
|
if !errors.Is(err, ErrWeightOverflow) {
|
|
t.Fatalf("want the overflow refusal, got %v", err)
|
|
}
|
|
if len(set.Validators) != 0 || set.TotalWeight != 0 {
|
|
t.Fatal("a refused flatten returned a set")
|
|
}
|
|
}
|
|
|
|
// TestFlattenSkipsAnAbsentEntry covers the map that names a node and holds
|
|
// nothing for it. It is neither a member nor a defect — it contributes no
|
|
// weight, no key and no seat, and the set around it is still the set.
|
|
func TestFlattenSkipsAnAbsentEntry(t *testing.T) {
|
|
present, entry := output(0x01, secret(t, 71), 10)
|
|
absent := nodeID(0x02)
|
|
|
|
set, err := FlattenValidatorSet(map[ids.NodeID]*GetValidatorOutput{
|
|
present: entry,
|
|
absent: nil,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("an absent entry is not a refusal: %v", err)
|
|
}
|
|
if len(set.Validators) != 1 {
|
|
t.Fatalf("the set has %d validators, want the one that is actually there", len(set.Validators))
|
|
}
|
|
if set.TotalWeight != 10 {
|
|
t.Fatalf("total weight = %d, want 10 — an absent entry weighs nothing", set.TotalWeight)
|
|
}
|
|
}
|