Files
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

185 lines
10 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.
// verified_cert.go — the SINGLE finality authority token.
//
// THE RULE (one rule, one place): No VerifiedQuorumCert, no finality.
//
// Acceptance is collapsed onto ONE structurally-enforced path. Every finality
// trigger — a vote arrived, a re-poll fired, the pending queue changed, a block
// was built/verified, a poll timeout ticked, a cert was gossiped in — routes to
// Transitive.TryAccept, which either obtains a VerifiedQuorumCert or refuses
// (ErrNoVerifiedQC) and lets the trigger retry later. The ONLY function that can
// finalize a block is Transitive.AcceptWithCert, and it CANNOT be called without
// a VerifiedQuorumCert value.
//
// A VerifiedQuorumCert is UNFORGEABLE outside this package: its only field (qc) is
// unexported, so no other package can construct a non-zero one with a struct
// literal. FIVE things produce it, and every one either runs the full predicate
// first or has no votes to run it on:
//
// 1. BuildVerifiedQuorumCert — the exported minting door, from raw votes over a set.
// 2. HandleIncomingCert — a gossiped certificate, through verifyCert.
// 3. VerifyCatchupCertificate/AcceptCatchupBlock — the same, on the catch-up road.
// 4. assembleVerifiedCertLocked → assembleCertLocked — the engine's own assembly,
// which verifies before it caches.
// 5. buildSingleValidatorCertLocked — the K==1 road, and the one that is not a
// predicate: on a genuinely single-validator chain it synthesizes a zero-vote
// Nova token, because the sole validator's own accept IS the quorum and there
// is no other party to protect against. Fenced three ways (both callers gate on
// K()==1, a real signed 1-of-1 is preferred whenever one assembles, and the
// zero token is returned once the live count passes one on a chain that has
// finalized anything), and Nova only, so it is never an export door.
//
// 2 through 4 promote through wrapVerifiedCert, which is unexported and refuses
// nil — there is no exported escape hatch that skips verification. A raw α-of-K
// COUNT ("enough voters responded",
// consensus.IsAccepted, "enough pending callbacks") is a LIVENESS signal only:
// it may trigger TryAccept, but it can never itself produce a VerifiedQuorumCert
// and therefore can never finalize. This is the structural form of HIGH-3: the
// count road is no longer an acceptance authority — it is a retry signal.
package chain
import (
"errors"
"math"
)
// ErrNoVerifiedQC is returned by TryAccept when no verified quorum certificate
// exists for the block yet. It is NOT an error condition to log loudly — it is
// the normal "not final yet, keep waiting / re-poll" answer on the liveness
// path. A trigger that gets it should retry on its next tick; it must NEVER
// finalize in response to it.
var ErrNoVerifiedQC = errors.New("chain: no verified quorum cert for block — not final (liveness retry, not an accept)")
// ErrCertNeedsStake is the derived-authority refusal at the minting door: a
// certificate was asked for with no stake source to derive its floor from. Every
// floor a certificate declares and is held to is a function of the validator set
// and the rung; with no set there is no floor, and a caller naming one in its
// place is the certificate choosing its own quorum with an extra step.
var ErrCertNeedsStake = errors.New("chain: a cert cannot be minted without a stake source — the floor it declares is derived from the validator set, and there is no set here")
// ErrCertFloorUnstatable is the refusal for a floor no certificate can carry: a
// tier with no floor at all (an unknown rung), a set that derives none (n<1), or —
// at about six billion signers — a floor past what the certificate's uint32
// threshold can hold. Narrowing it would wrap to a number a lone signer meets.
var ErrCertFloorUnstatable = errors.New("chain: the floor this set derives for this tier is not a quorum a cert can state")
// VerifiedQuorumCert is proof that a block met the finality predicate: α distinct
// validators signed ACCEPT over the exact position AND (on a stake-weighted
// chain) those voters hold a strict ⅔ supermajority of stake at the cert's
// epoch. Holding a non-zero VerifiedQuorumCert is the ONLY thing that authorizes
// finalization (AcceptWithCert takes one by value).
//
// The wrapped cert is unexported. There is deliberately NO exported field and NO
// exported raw constructor: a VerifiedQuorumCert can be produced ONLY by
// BuildVerifiedQuorumCert, which verifies. A zero VerifiedQuorumCert{} carries a
// nil cert and is rejected by AcceptWithCert — so even a zero literal cannot
// finalize anything.
type VerifiedQuorumCert struct {
// qc is the verified witness. Unexported: unforgeable outside this file.
// nil ⇒ the zero value ⇒ NOT a finality authority (AcceptWithCert refuses it).
qc *QuorumCert
}
// IsZero reports whether this is the zero VerifiedQuorumCert (no verified witness
// inside). AcceptWithCert refuses a zero cert; a TryAccept that cannot build a
// real cert returns the zero value alongside ErrNoVerifiedQC.
func (v VerifiedQuorumCert) IsZero() bool { return v.qc == nil }
// Cert returns the underlying verified QuorumCert (for gossip / logging /
// per-height-guard finalization). It is the cert that already passed
// VerifyWeighted; callers must not mutate it. nil for the zero value.
func (v VerifiedQuorumCert) Cert() *QuorumCert { return v.qc }
// BuildVerifiedQuorumCert assembles a quorum certificate from the collected
// SIGNED accept votes and verifies it under the FULL finality predicate before
// wrapping it. It is the exported producer of the finality authority token.
//
// verifier — the chain's VoteVerifier (BLS / ML-DSA / secp256k1). nil ⇒ fail closed.
// stake — the chain's StakeSource: the set this certificate's floor is read
// off. nil ⇒ ErrCertNeedsStake, at BOTH rungs. See below.
// tier — Nova (local-execution majority) or Quasar (export ⅔-by-stake); selects
// which floor is derived and which stake clause VerifyWeighted enforces.
// epochHeight — the P-chain epoch the per-voter pubkeys, set-root and stake tally
// are all read at (MEDIUM-1).
// pos — the consensus position the votes (and the cert) bind to.
// votes — the collected SIGNED accept records (caller has already filtered
// to those whose signature verified; Assemble+Verify re-check).
//
// NO CALLER STATES A QUORUM. The floor is SignerFloor(tier, n) over the set as it
// stands at this epoch, read here and now, and it is what the certificate carries.
// It was an argument once, and the argument was the hole: on the count-only road
// nothing re-derived it, so one vote and an alpha of one minted an authority token
// on a chain of five while the same certificate arriving over the wire was refused.
// A door that can be opened with a number the caller chose is not a door. Now there
// is nothing to state, so there is nothing to disagree with — the shape Rust's
// Tally::cert and the C++ engine already have.
//
// NO SET, NO CERTIFICATE, at both rungs. A floor is a property of the set; with no
// stake source there is no set, no floor and nothing to check a would-be quorum
// against. This is where the arrival road and the minting road part, and the
// difference is what the roads are: Transitive.verifyCert admits a certificate that
// ALREADY carries checked signatures from distinct in-set validators, so with no
// stake source it can still derive a floor from this node's own committee and hold
// the declaration to it. Here there are no arrived signatures — the caller hands
// over raw votes — so a configured committee would be checking the caller against
// the caller. An equal-stake chain mints through the engine, whose committee is its
// own, or supplies a uniform StakeSource; both are a set.
//
// Returns the zero VerifiedQuorumCert and ErrNoVerifiedQC (wrapping the precise
// predicate failure) if a verified quorum is not yet present — this is the
// LIVENESS answer, never a force. NEVER weakens VerifyWeighted.
func BuildVerifiedQuorumCert(
verifier VoteVerifier,
stake StakeSource,
tier Finality,
epochHeight uint64,
pos VotePosition,
votes []SignedVote,
) (VerifiedQuorumCert, error) {
if verifier == nil {
return VerifiedQuorumCert{}, ErrNoVerifiedQC
}
if stake == nil {
return VerifiedQuorumCert{}, errors.Join(ErrNoVerifiedQC, ErrCertNeedsStake)
}
// The floor, derived. An unknown tier derives 0 and an unresolved set derives a
// number no honest assembler could have meant, so both land here rather than in a
// certificate. The upper bound is the width a certificate states its quorum in:
// SignerFloor counts at full width precisely so this narrowing is a decision and
// not an accident.
alpha := SignerFloor(tier, stake.SignerCount(epochHeight))
if alpha < 1 || alpha > math.MaxUint32 {
return VerifiedQuorumCert{}, errors.Join(ErrNoVerifiedQC, ErrCertFloorUnstatable)
}
cert, err := AssembleQuorumCert(pos, tier, uint32(alpha), votes)
if err != nil {
// Quorum not assembled yet (sub-threshold / not-yet-arrived). Liveness:
// keep waiting. Wrap the precise cause for diagnosis, present
// ErrNoVerifiedQC to the caller's control flow.
return VerifiedQuorumCert{}, errors.Join(ErrNoVerifiedQC, err)
}
// THE finality predicate — the strict stake floors, the rung's signer floor, and
// the derived-threshold clause, which re-reads the set and so re-checks the very
// number this function just stamped. Nothing wraps without clearing it.
if err := cert.VerifyWeighted(verifier, stake, epochHeight); err != nil {
return VerifiedQuorumCert{}, errors.Join(ErrNoVerifiedQC, err)
}
return VerifiedQuorumCert{qc: cert}, nil
}
// wrapVerifiedCert promotes an ALREADY-verified *QuorumCert into the authority
// token. It is intentionally unexported and used ONLY on paths that have just
// verified the cert through the same predicate (the incoming-cert path, which
// runs VerifyWeighted/Verify in HandleIncomingCert, and the engine's
// assembleCertLocked, which verifies before caching). It refuses nil so the zero
// value can never be promoted. Within-package only — there is no exported escape
// hatch that skips verification.
func wrapVerifiedCert(cert *QuorumCert) (VerifiedQuorumCert, bool) {
if cert == nil {
return VerifiedQuorumCert{}, false
}
return VerifiedQuorumCert{qc: cert}, true
}