Files
zeekayandHanzo Dev bdb331cfac chain: accept a block only on the two-thirds certificate
Local, unreleased. Acceptance and the ledger's decided height move only on
the Quasar (two-thirds) certificate; a certificate's tier label is read at
that rung. Winner in two tiers; votes parked only from their authenticated
signer, bounded and expiring; relay once per proposer a height; stateless
blocks charged to no sender.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-24 17:29:55 -07:00

319 lines
15 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.
// accept_cert_gate_test.go — the acceptance-authority collapse tests.
//
// One rule under test: No VerifiedQuorumCert, no finality. Every finality
// trigger funnels through Transitive.TryAccept; the SOLE finalizer is
// AcceptWithCert, which cannot run without a VerifiedQuorumCert; and a raw α-of-K
// COUNT ("enough voters responded", consensus.IsAccepted) is a LIVENESS signal
// only — it may trigger TryAccept but can never decide finality. These tests
// drive REAL signed votes through the live engine (no forged anything) and prove
// the count road is no longer an acceptance authority.
package chain
import (
"context"
"testing"
"time"
"github.com/luxfi/consensus/config"
"github.com/luxfi/ids"
)
// driveSignedAccepts tracks blk as a verified, non-own pending block on a fresh
// stake-weighted engine, then feeds REAL signed accept votes from validators
// [first,last]. It returns the engine + chainID so a test can assert finality.
// No self-vote is injected (non-own block), so the ONLY accepts are the ones fed.
func driveSignedAccepts(
t *testing.T,
vs *testValidatorSet,
stake StakeSource,
rec *recordingGossiper,
blk *verifyOnceBlock,
voters []int,
) (*Transitive, ids.ID) {
t.Helper()
return driveSignedAcceptsAt(t, dyn5(), vs, stake, rec, blk, voters)
}
// driveSignedAcceptsAt is driveSignedAccepts with the committee size spelled out, for
// sets that are not five (the six-entry lux-mainnet set, say).
func driveSignedAcceptsAt(
t *testing.T,
params config.Parameters,
vs *testValidatorSet,
stake StakeSource,
rec *recordingGossiper,
blk *verifyOnceBlock,
voters []int,
) (*Transitive, ids.ID) {
t.Helper()
e, chainID := newQuorumEngineOpts(t, params, vs, 0, rec, WithStakeWeighting(stake))
cb := &Block{id: blk.id, parentID: blk.parentID, height: blk.height, timestamp: blk.timestamp.Unix(), data: blk.bytes}
_ = e.consensus.AddBlock(context.Background(), cb)
e.mu.Lock()
e.pendingBlocks[blk.id] = &PendingBlock{ConsensusBlock: cb, VMBlock: blk, ProposedAt: time.Now(), Round: 0}
e.mu.Unlock()
pos := VotePosition{ChainID: chainID, Height: blk.height, Round: 0, BlockID: blk.id, ParentID: blk.parentID}
for _, i := range voters {
e.ReceiveVote(vs.signedVote(i, pos))
}
return e, chainID
}
// mustNotFinalize fails if blk is VM-accepted (AcceptCalled>=1) or reported
// accepted within d. It is the SAFETY assertion: a sub-quorum coalition must
// never reach finality.
func mustNotFinalize(t *testing.T, e *Transitive, blk *verifyOnceBlock, d time.Duration, why string) {
t.Helper()
if waitFor(d, func() bool { return blk.AcceptCalled() >= 1 }) {
t.Fatalf("%s: SAFETY VIOLATION — block finalized (VM.Accept ran %d×), IsAccepted=%v",
why, blk.AcceptCalled(), e.IsAccepted(blk.id))
}
if e.IsAccepted(blk.id) {
t.Fatalf("%s: IsAccepted reported finality with no verified cert", why)
}
}
// mustFinalize fails unless blk is VM-accepted exactly once and reported accepted
// within d. It is the LIVENESS assertion: a ⅔ supermajority — more than two thirds of the
// signing stake, from at least TwoThirdsCount(n) signers — DOES drive VM.Accept. That is
// the one certificate a block is accepted on, and the same one it is exported on.
func mustFinalize(t *testing.T, e *Transitive, blk *verifyOnceBlock, d time.Duration, why string) {
t.Helper()
if !waitFor(d, func() bool { return e.IsAccepted(blk.id) }) {
t.Fatalf("%s: LIVENESS FAILURE — a ⅔ quorum did not accept (AcceptCalled=%d)", why, blk.AcceptCalled())
}
if got := blk.AcceptCalled(); got != 1 {
t.Fatalf("%s: block must VM.Accept exactly once, got %d", why, got)
}
}
// mustQuasar fails unless blk reaches the EXPORT (Quasar, ⅔-by-stake) tier within d — the
// export LIVENESS assertion: a real >⅔-stake supermajority DOES certify for export.
func mustQuasar(t *testing.T, e *Transitive, blk *verifyOnceBlock, d time.Duration, why string) {
t.Helper()
if !waitFor(d, func() bool { qh, ok := e.QuasarHeight(); return ok && qh >= blk.height }) {
qh, ok := e.QuasarHeight()
t.Fatalf("%s: EXPORT LIVENESS FAILURE — block did not reach Quasar (⅔-stake); QuasarHeight=(%d,%v), want ≥%d",
why, qh, ok, blk.height)
}
}
// mustNotQuasar fails if blk reaches the EXPORT (Quasar) tier within d. It is the
// export-SAFETY assertion: a coalition below the ⅔ supermajority reaches neither
// acceptance nor export finality (bridges / DEX settlement / cross-chain), and the Quasar
// frontier is NOT advanced to its block.
func mustNotQuasar(t *testing.T, e *Transitive, blk *verifyOnceBlock, d time.Duration, why string) {
t.Helper()
if waitFor(d, func() bool { qh, ok := e.QuasarHeight(); return ok && qh >= blk.height }) {
t.Fatalf("%s: EXPORT SAFETY VIOLATION — a sub-⅔-stake coalition reached Quasar (⅔-stake export)", why)
}
}
// TestNoAcceptWithoutVerifiedQC: the SOLE finalizer refuses a zero
// VerifiedQuorumCert. Even an internal caller cannot finalize by passing the zero
// value — the rule "no VerifiedQuorumCert, no finality" is structural.
func TestNoAcceptWithoutVerifiedQC(t *testing.T) {
vs := newTestValidatorSet(5)
stake := newStakeMap(vs, 20, 20, 20, 20, 20)
rec := &recordingGossiper{}
e, _ := newQuorumEngineOpts(t, dyn5(), vs, 0, rec, WithStakeWeighting(stake))
blk := newTestBlock(1, ids.Empty, "no-qc")
cb := &Block{id: blk.id, parentID: blk.parentID, height: blk.height, timestamp: blk.timestamp.Unix()}
_ = e.consensus.AddBlock(context.Background(), cb)
e.mu.Lock()
e.pendingBlocks[blk.id] = &PendingBlock{ConsensusBlock: cb, VMBlock: blk, ProposedAt: time.Now()}
e.mu.Unlock()
// Calling the sole finalizer with the ZERO cert must refuse and finalize nothing.
if err := e.AcceptWithCert(context.Background(), blk.id, VerifiedQuorumCert{}); err != ErrNoVerifiedQC {
t.Fatalf("AcceptWithCert(zero cert) must return ErrNoVerifiedQC, got %v", err)
}
if blk.AcceptCalled() != 0 {
t.Fatalf("zero cert finalized a block (VM.Accept ran %d×)", blk.AcceptCalled())
}
if e.IsAccepted(blk.id) {
t.Fatal("zero cert produced finality")
}
}
// TestHeadcountMajorityWithoutStakeAcceptsNothing: a head-count is not a quorum. Four of five
// validators sign, holding 4 of 100 stake. A block is accepted only on more than two thirds
// of the signing stake, so this coalition gets neither acceptance nor export.
func TestHeadcountMajorityWithoutStakeAcceptsNothing(t *testing.T) {
vs := newTestValidatorSet(5)
// EQUAL vote weight; SKEWED stake → the four voters hold 4/100.
skew := newStakeMap(vs, 96, 1, 1, 1, 1)
rec := &recordingGossiper{}
blk := newTestBlock(1, ids.Empty, "count-majority")
e, _ := driveSignedAccepts(t, vs, skew, rec, blk, []int{1, 2, 3, 4})
mustNotFinalize(t, e, blk, 2*time.Second, "4/100-stake coalition (accept)")
mustNotQuasar(t, e, blk, 500*time.Millisecond, "4/100-stake coalition (export gate)")
}
// TestSkewedStakeHeadcountMajorityRejected: A=60%, B=C=D=E=10%. B+C+D+E vote — a 4-of-5
// headcount holding 40%. A stake minority causes no state transition at either tier.
func TestSkewedStakeHeadcountMajorityRejected(t *testing.T) {
vs := newTestValidatorSet(5)
// A=60, B..E=10 each (total 100). The four small holders sum to 40%.
skew := newStakeMap(vs, 60, 10, 10, 10, 10)
rec := &recordingGossiper{}
blk := newTestBlock(1, ids.Empty, "skew-40pct")
// B,C,D,E vote (indices 1..4): headcount 4/5 but only 40% of stake. A abstains.
e, _ := driveSignedAccepts(t, vs, skew, rec, blk, []int{1, 2, 3, 4})
mustNotFinalize(t, e, blk, 2*time.Second, "40%-stake coalition (accept)")
mustNotQuasar(t, e, blk, 500*time.Millisecond, "40%-stake coalition (export gate)")
}
// TestEqualStakeFourOfFiveAcceptedWithQC: equal stake, four of five validators
// sign accept → count=4=α AND stake=80% > ⅔ → a verified QC exists → the block
// finalizes through AcceptWithCert. Proves the gate does not over-block real
// quorums (liveness).
func TestEqualStakeFourOfFiveAcceptedWithQC(t *testing.T) {
vs := newTestValidatorSet(5)
equal := newStakeMap(vs, 20, 20, 20, 20, 20) // 80% from any four
rec := &recordingGossiper{}
blk := newTestBlock(1, ids.Empty, "equal-4of5")
e, _ := driveSignedAccepts(t, vs, equal, rec, blk, []int{0, 1, 2, 3})
mustFinalize(t, e, blk, 2*time.Second, "equal-stake 4/5 with verified QC")
// A verified cert was gossiped (the finality proof followers finalize on).
if !waitFor(time.Second, func() bool {
rec.mu.Lock()
defer rec.mu.Unlock()
return len(rec.certs) >= 1
}) {
t.Fatal("a verified α-of-K cert must be gossiped on finalization")
}
}
// TestThreeOfFiveAcceptsNothingTheFourthAcceptsAndExports: three of five sign — a bare
// majority, 60% of equal stake, below ⅔ — and the block is neither accepted nor exported:
// one equivocator could put a majority on each of two blocks at one height. The fourth
// signature carries it past ⅔, and the block is accepted and exported in the same step.
func TestThreeOfFiveAcceptsNothingTheFourthAcceptsAndExports(t *testing.T) {
vs := newTestValidatorSet(5)
equal := newStakeMap(vs, 20, 20, 20, 20, 20) // three → 60% < ⅔
rec := &recordingGossiper{}
blk := newTestBlock(1, ids.Empty, "three-of-five")
e, chainID := driveSignedAccepts(t, vs, equal, rec, blk, []int{0, 1, 2})
mustNotFinalize(t, e, blk, 2*time.Second, "3/5 bare majority (accept)")
mustNotQuasar(t, e, blk, 500*time.Millisecond, "3/5 = 60% stake (export gate)")
pos := VotePosition{ChainID: chainID, Height: blk.height, Round: 0, BlockID: blk.id, ParentID: blk.parentID}
e.ReceiveVote(vs.signedVote(3, pos))
mustFinalize(t, e, blk, 2*time.Second, "4/5 = 80% stake (accept)")
mustQuasar(t, e, blk, 2*time.Second, "4/5 = 80% stake (export)")
}
// TestVoteArrivalWithoutStakeAcceptsNothingUntilTheHeavyVote: a vote arriving is a LIVENESS
// trigger into TryAccept, never an authority. B,C,D,E (4/5 count, 40% stake) sign and nothing
// is accepted: acceptance needs more than two thirds of the signing stake. When the heavy
// holder A (60%) signs, the block is accepted and exported in the same step.
func TestVoteArrivalWithoutStakeAcceptsNothingUntilTheHeavyVote(t *testing.T) {
vs := newTestValidatorSet(5)
skew := newStakeMap(vs, 60, 10, 10, 10, 10) // A=60%
rec := &recordingGossiper{}
blk := newTestBlock(1, ids.Empty, "vote-trigger")
// First only B,C,D,E vote: 4/5 headcount, 40% stake — under ⅔ of the stake.
e, chainID := driveSignedAccepts(t, vs, skew, rec, blk, []int{1, 2, 3, 4})
mustNotFinalize(t, e, blk, 2*time.Second, "40%-stake before heavy vote (accept)")
mustNotQuasar(t, e, blk, 500*time.Millisecond, "40%-stake before heavy vote (export gate)")
// The heavy-stake holder A (index 0) votes. 40%+60% = 100% carries the block past ⅔:
// it is accepted and its export certificate completes in the same step.
pos := VotePosition{ChainID: chainID, Height: blk.height, Round: 0, BlockID: blk.id, ParentID: blk.parentID}
e.ReceiveVote(vs.signedVote(0, pos))
mustFinalize(t, e, blk, 2*time.Second, "heavy-stake vote completes the ⅔ supermajority")
mustQuasar(t, e, blk, 2*time.Second, "heavy-stake vote completes the ⅔ export supermajority")
}
// TestRepollIsNotAnAuthority: a re-poll firing is a LIVENESS trigger (the pollLoop re-examines
// pending blocks), a RETRY and not an authority: no amount of re-polling manufactures the
// ⅔-STAKE supermajority a block is accepted on, for a 4-of-5 head-count holding 40% of stake.
func TestRepollIsNotAnAuthority(t *testing.T) {
vs := newTestValidatorSet(5)
skew := newStakeMap(vs, 60, 10, 10, 10, 10)
rec := &recordingGossiper{}
blk := newTestBlock(1, ids.Empty, "repoll-trigger")
e, _ := driveSignedAccepts(t, vs, skew, rec, blk, []int{1, 2, 3, 4})
// Fire the re-poll finalizer repeatedly — a retry, never an authority. The ⅔
// supermajority cannot be re-polled into existence.
for i := 0; i < 25; i++ {
e.processPendingBlocks()
}
mustNotFinalize(t, e, blk, time.Second, "40%-stake coalition under repeated re-poll (accept)")
mustNotQuasar(t, e, blk, 500*time.Millisecond, "40%-stake coalition under repeated re-poll (export gate)")
}
// TestVerifiedQuorumCertUnforgeableOutsideBuilder: a VerifiedQuorumCert can be
// produced ONLY by the verifying builder. The zero value (the only literal a
// foreign package can write, since qc is unexported) is NOT a finality authority,
// and BuildVerifiedQuorumCert refuses to mint one for a sub-quorum / bad-signature
// vote set. So no code path outside the builder can fabricate the authority token.
func TestVerifiedQuorumCertUnforgeableOutsideBuilder(t *testing.T) {
vs := newTestValidatorSet(5)
equal := newStakeMap(vs, 20, 20, 20, 20, 20)
chainID := ids.GenerateTestID()
pos := VotePosition{ChainID: chainID, Height: 1, Round: 0, BlockID: ids.GenerateTestID(), ParentID: ids.Empty}
// 1) The zero value carries no witness and is rejected by the finalizer.
var zero VerifiedQuorumCert
if !zero.IsZero() || zero.Cert() != nil {
t.Fatal("zero VerifiedQuorumCert must be empty (no embedded cert)")
}
// 2) Builder REFUSES a sub-quorum (3 votes against a derived floor of 4): no token.
sub := []SignedVote{
{NodeID: vs.nodeID(0), Accept: true, Signature: vs.sign(0, pos)},
{NodeID: vs.nodeID(1), Accept: true, Signature: vs.sign(1, pos)},
{NodeID: vs.nodeID(2), Accept: true, Signature: vs.sign(2, pos)},
}
if vc, err := BuildVerifiedQuorumCert(vs, equal, Quasar, 1, pos, sub); err == nil || !vc.IsZero() {
t.Fatalf("builder must refuse a sub-α vote set (got err=%v zero=%v)", err, vc.IsZero())
}
// 3) Builder REFUSES a forged signature (wrong signer's bytes): no token.
forged := []SignedVote{
{NodeID: vs.nodeID(0), Accept: true, Signature: vs.sign(1, pos)}, // node 0 claims, node 1 signed
{NodeID: vs.nodeID(1), Accept: true, Signature: vs.sign(1, pos)},
{NodeID: vs.nodeID(2), Accept: true, Signature: vs.sign(2, pos)},
{NodeID: vs.nodeID(3), Accept: true, Signature: vs.sign(3, pos)},
}
if vc, err := BuildVerifiedQuorumCert(vs, equal, Quasar, 1, pos, forged); err == nil || !vc.IsZero() {
t.Fatalf("builder must refuse a forged-signature vote set (got err=%v zero=%v)", err, vc.IsZero())
}
// 4) Builder MINTS a token for a genuine α-of-K, ⅔-stake vote set — and ONLY
// then is it non-zero. This is the sole production route.
good := []SignedVote{
{NodeID: vs.nodeID(0), Accept: true, Signature: vs.sign(0, pos)},
{NodeID: vs.nodeID(1), Accept: true, Signature: vs.sign(1, pos)},
{NodeID: vs.nodeID(2), Accept: true, Signature: vs.sign(2, pos)},
{NodeID: vs.nodeID(3), Accept: true, Signature: vs.sign(3, pos)},
}
vc, err := BuildVerifiedQuorumCert(vs, equal, Quasar, 1, pos, good)
if err != nil || vc.IsZero() {
t.Fatalf("builder must mint a token for a real ⅔ quorum (err=%v zero=%v)", err, vc.IsZero())
}
if vc.Cert() == nil || vc.Cert().VoterCount() != 4 {
t.Fatalf("minted token must carry the 4-voter cert, got %v", vc.Cert())
}
}