Files
consensus/engine/chain/tally_overflow_test.go
zeekayandHanzo Dev 731dc22f23 cert: a floor is read once, where it is authoritative
Red found no way past the predicate — every refusal was fail-closed. What it
found instead was the opposite failure, three times over: a number about a quorum
read somewhere that is not the authority for it.

THE DOORS IN FRONT OF THE GATE. HandleIncomingCert and VerifyCatchupCertificate
each carried a cheap sub-quorum filter over the sample committee — NovaQuorum(K)
for Nova, Alpha() for Quasar — in front of verifyCert. That is a THIRD spelling of
a quantity with one definition, and it is a different quantity: the gate derives
the accept rung's floor from the SIGNING SET, where NovaSignerFloor saturates at
three, while the door read the committee's majority, which grows with K. At six
seats the gate accepts a certificate declaring three and the door demanded four;
at nine it demanded five; at eleven, six. And where the set is SMALLER than the
committee the door asked the set for more than the set's own floor: three signers
derive two, and a door reading a committee of four asked for three.

So the exact bytes a node's own assembler produces never reached the predicate
that would have admitted them. A door in front of a gate may only refuse what the
gate refuses, and these could not, so they are gone. Nothing is lost by not asking
twice: the gate demands EQUALITY with the derived floor, which is strictly
stronger than the "at least" a door applied. Nor is any work saved by refusing
early — the certificates a floor filter drops are the ones carrying FEW votes,
which are the cheap ones, and Verify stops at the first signature that fails.

THE CALLER'S ALPHA. BuildVerifiedQuorumCert took the floor as an ARGUMENT, and on
the count-only road nothing re-derived it: one vote and an alpha of one minted an
authority token on a chain of five, while a certificate carrying those exact bytes
was refused on arrival. The docstring said an alpha that disagrees with the set
yields no token, and that was true only of the weighted road. So there is no
argument. The floor is SignerFloor(tier, SignerCount(epoch)), read at the door,
and there is nothing left for a caller to disagree with.

With that, a nil stake source is refused at BOTH rungs rather than at the export
rung alone: a floor is a property of the set, and with no set there is no floor and
nothing to mint. exportNeedsStake goes with it — one rule where there were two, and
the rule Rust and C++ already have by their types, each taking a set by reference
so the absent case is unrepresentable rather than checked. The equal-stake road
keeps minting through the engine, whose committee is its own, and keeps ADMITTING
on the arrival road, where signatures have already arrived to be weighed.

THE WIDTH. SignerFloor and Quorum returned uint32 while counting a set held in an
int, so the floor wrapped at about six billion signers and SignerFloor(Quasar, ~6.4e9)
answered 1 — a floor a lone signer meets. They return int now and are narrowed only
where a certificate is written, which is one place; and because the derived clause
compares at full width, a mint that narrowed is caught by it rather than believed.

THE LAST SECOND SPELLING. attestation.go's export trigger said config.TwoThirdsCount(n)
where it means SignerFloor(Quasar, n). Same number, and that is the point: a trigger
spelled apart from the rule it must never outrun is a trigger that can drift from it.

TestTheDoorAdmitsWhatTheGateAdmits walks the four sizes where the two numbers part,
on both arrival roads, and fails on the old filter at every one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-02 02:56:02 -07:00

165 lines
7.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.
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// tally_overflow_test.go — the tally has to be the sum it claims to be.
//
// Both rungs decide by comparing a summed stake against a floor read off the
// total. Go's + is modular, so an unchecked loop does not fail when the weights
// a StakeSource reports run past 2^64 — it returns a DIFFERENT number and the
// comparison proceeds as if nothing happened. That is the fail-open direction:
// the wrapped value can sit above the floor while the stake it claims to
// represent does not exist, and the cert is accepted on arithmetic rather than
// on votes.
//
// Each case here states the wrap outright — the value the old loop produced,
// and the floor it cleared — and then asserts the predicate refuses. Stating
// the wrap is what makes these tests bite: without it they would pass against
// an implementation that simply refused everything.
package chain
import (
"errors"
"math"
"testing"
"github.com/luxfi/consensus/config"
validators "github.com/luxfi/consensus/validator"
"github.com/luxfi/ids"
)
// wrappingStake reports per-voter weights that sum past 2^64 against a total
// that does not. No admitted set can hold these numbers — Register and
// FlattenValidatorSet both refuse a set whose weights overflow — so reaching
// this is evidence about the SOURCE, not about the votes.
func wrappingStake(vs *testValidatorSet, n int, weights map[int]uint64) *stakeMap {
s := &stakeMap{w: make(map[ids.NodeID]uint64, n), total: math.MaxUint64}
for i := 0; i < n; i++ {
s.w[vs.nodeID(i)] = weights[i]
}
return s
}
// TestNovaTallyRefusesAWrappingStakeSource — three voters at 2^63 sum to 2^64 +
// 2^63, which wraps to 2^63 and clears floor(total/2) = 2^63 − 1 by exactly one.
// The signer floor is met, so nothing else stands in the way: before the tally
// was checked this cert was ACCEPTED at the local-execution rung.
func TestNovaTallyRefusesAWrappingStakeSource(t *testing.T) {
vs := newTestValidatorSet(5)
pos := VotePosition{ChainID: ids.GenerateTestID(), Height: 1, BlockID: ids.GenerateTestID()}
const epoch = uint64(1) // == pos.Height; the stake map is height-independent
const half = uint64(1) << 63
src := wrappingStake(vs, 5, map[int]uint64{0: half, 1: half, 2: half})
votes := make([]SignedVote, 0, 3)
for _, i := range []int{0, 1, 2} {
votes = append(votes, SignedVote{NodeID: vs.nodeID(i), Accept: true, Signature: vs.sign(i, pos)})
}
cert := certDeclaring(t, pos, Nova, 5, votes)
// The count clause must be satisfied, or the case would be refused before
// the tally is ever read and would prove nothing about it.
if floor := NovaSignerFloor(src.SignerCount(epoch)); cert.VoterCount() < floor {
t.Fatalf("this set does not reach the signer floor (%d of %d), so the tally is never read",
cert.VoterCount(), floor)
}
// The wrap, stated outright: what the unchecked loop computed, and the floor
// it cleared. If either stops holding, this case no longer demonstrates the
// fail-open and the assertion below would pass for the wrong reason.
var wrapped uint64
for i := range cert.Votes {
wrapped += src.Weight(cert.Votes[i].NodeID, epoch)
}
if wrapped != half {
t.Fatalf("the tally no longer wraps to 2^63 (got %d) — reread this case", wrapped)
}
if got := config.HalfStakeFloor(src.SignerStake(epoch)); wrapped <= got {
t.Fatalf("the wrapped tally %d no longer clears floor(total/2)=%d, so there is no "+
"fail-open left to demonstrate", wrapped, got)
}
// And the predicate refuses it, on the tally rather than on the floor.
err := cert.VerifyWeighted(vs, src, epoch)
if !errors.Is(err, validators.ErrWeightOverflow) {
t.Fatalf("a wrapping tally cleared the Nova majority: %v", err)
}
}
// TestQuasarTallyRefusesAWrappingStakeSource — the same hole at the EXPORT rung,
// which is the one a bridge admits on. Two voters at 2^63 + 7·10^18 sum past
// 2^64 and wrap to 1.4·10^19, above floor(2·total/3). Quasar carries no signer
// floor, so the tally is the only thing between this cert and export-grade
// finality.
func TestQuasarTallyRefusesAWrappingStakeSource(t *testing.T) {
vs := newTestValidatorSet(5)
pos := VotePosition{ChainID: ids.GenerateTestID(), Height: 1, BlockID: ids.GenerateTestID()}
const epoch = uint64(1)
// Two seats hold a weight that wraps when summed, and two hold a unit each so
// the certificate reaches the export rung's DISTINCT-SIGNER floor of four. The
// count clause has to be satisfied or the tally is never read, and a row about
// the tally that is answered by the count proves nothing about the tally.
const w = uint64(1)<<63 + 7_000_000_000_000_000_000
src := wrappingStake(vs, 5, map[int]uint64{0: w, 1: w, 2: 1, 3: 1})
votes := make([]SignedVote, 0, 4)
for _, i := range []int{0, 1, 2, 3} {
votes = append(votes, SignedVote{NodeID: vs.nodeID(i), Accept: true, Signature: vs.sign(i, pos)})
}
// Declaring the floor the five-signer set derives, so the row reaches the tally
// rather than being answered by a threshold the certificate named for itself.
cert := certDeclaring(t, pos, Quasar, 5, votes)
if floor := SignerFloor(Quasar, src.SignerCount(epoch)); cert.VoterCount() < floor {
t.Fatalf("this set does not reach the export signer floor (%d of %d), so the tally is never read",
cert.VoterCount(), floor)
}
var wrapped uint64
for i := range cert.Votes {
wrapped += src.Weight(cert.Votes[i].NodeID, epoch)
}
if got := config.TwoThirdsStakeFloor(src.SignerStake(epoch)); wrapped <= got {
t.Fatalf("the wrapped tally %d no longer clears floor(2*total/3)=%d, so there is no "+
"fail-open left to demonstrate", wrapped, got)
}
err := cert.VerifyWeighted(vs, src, epoch)
if !errors.Is(err, validators.ErrWeightOverflow) {
t.Fatalf("a wrapping tally cleared the export supermajority: %v", err)
}
}
// TestTallyStillSumsASetThatFits — the guard must not have made the honest path
// stricter. A set whose voters sum to exactly the widest representable total is
// still tallied and still decided on its merits.
func TestTallyStillSumsASetThatFits(t *testing.T) {
vs := newTestValidatorSet(minBFTCommittee)
pos := VotePosition{ChainID: ids.GenerateTestID(), Height: 1, BlockID: ids.GenerateTestID()}
const epoch = uint64(1)
const half = uint64(1) << 63
// Every seat carries stake and every seat signs. A weightless seat would be
// refused by both admission doors (validators.ErrZeroWeight), and a set below
// minBFTCommittee signers is refused by the export rung's committee clause —
// neither would reach the tally this test is about.
src := &stakeMap{
w: map[ids.NodeID]uint64{
vs.nodeID(0): half,
vs.nodeID(1): half - 3,
vs.nodeID(2): 1,
vs.nodeID(3): 1,
},
total: math.MaxUint64, // == half + (half-3) + 1 + 1, the widest total that fits
}
votes := make([]SignedVote, 0, minBFTCommittee)
for _, i := range []int{0, 1, 2, 3} {
votes = append(votes, SignedVote{NodeID: vs.nodeID(i), Accept: true, Signature: vs.sign(i, pos)})
}
cert := certDeclaring(t, pos, Quasar, minBFTCommittee, votes)
if err := cert.VerifyWeighted(vs, src, epoch); err != nil {
t.Fatalf("a set that fits was refused: %v", err)
}
}