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

261 lines
12 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.
// attestation.go — Quasar, the post-acceptance ATTESTATION sidecar.
//
// THE DECOMPLECTION (owner decision, 2026-07-09). We had built the finality certificate as a
// SECOND consensus — Tendermint rounds, prevotes, POL, locks, view-change — with its own
// liveness, layered on top of the metastable sampler (Nova). Two deciders braided is where
// every incident lived (the static committee, the cert-receive alias, the stale-alias lock).
// But the artifact we actually need — a portable ⅔-stake quorum cert for bridges and the
// 0x9999 DEX settlement receipt — is an ATTESTATION OF AN OUTCOME, not a decider. The sampler
// already decides. So the cert demotes to this sidecar:
//
// - Nova (metastable sampling) is THE ONE decider. A block is ACCEPTED by β-confidence; that
// is what advances lastAccepted and drives VM.Accept. Chain liveness == sampling liveness.
// - Quasar (this file) runs strictly AFTER Accept. Having accepted inner block B at height H,
// a node signs the finality message for B and gossips it. A collector aggregates the
// signatures of ≥⅔ of the LIVE stake into a certificate for external consumers. The cert
// TRAILS acceptance and NEVER gates it — an absent or late cert cannot stall the chain.
//
// WHY IT IS SAFE (the reduction). Honest nodes sign ONLY what they accepted. Sampling accepts
// at most one block per height (w.h.p. under the metastable adversary bound). A conflicting
// cert for B'≠B at height H needs ≥α signers on B'; with α=⌊2n/3⌋+1 and Byzantine stake f<⅓,
// every α-set holds >⅓ honest stake, and those honest nodes accepted B, not B' — so they never
// signed B'. The cert's safety therefore REDUCES to sampling's single-accept-per-height plus
// the same ≥⅔-honest-stake assumption the old α-of-K cert already required: no worse, and with
// NO added liveness dependency (nothing waits on the cert).
//
// INNER-CANONICAL BY CONSTRUCTION. A node attests only after it accepted the INNER block, so
// the outer proposervm envelope does not exist at this layer — the whole stale-alias class
// (an outer id where an inner id belongs) cannot arise here. The lock rebase, committedSlot,
// vote-guard, and view-change machinery become unnecessary, not merely fixed.
//
// DRY. This reuses the finality crypto UNCHANGED: the signed message is CanonicalVoteMessage
// (binds the inner canonical identity + epoch set-root, excludes the outer envelope), the
// cert is a QuorumCert, aggregation is AssembleQuorumCert, and external verification is
// QuorumCert.VerifyWeighted (⅔-by-stake at the epoch). An attestation is therefore
// byte-identical to a legacy finality accept vote — only WHEN it is cast changes (post-Accept,
// deterministically the accepted block; no rounds, no locks, no POL). External verifiers are
// unaffected by the engine swap.
package chain
import (
"bytes"
"fmt"
"sync"
"github.com/luxfi/ids"
)
// attestKey identifies the finality subject: (height, inner canonical execution id). The outer
// envelope id is ABSENT by construction — attestation is post-Accept, over the inner block.
type attestKey struct {
height uint64
canonical ids.ID
}
// attestBucket accumulates the verified attestations for ONE subject. All votes in a bucket
// bind the byte-identical CanonicalVoteMessage(pos) (enforced on ingest), so AssembleQuorumCert
// over them yields a cert that verifies.
type attestBucket struct {
pos VotePosition
msg []byte // CanonicalVoteMessage(pos), cached; the bucket's fixed signed message
votes map[ids.NodeID]SignedVote
}
// QuasarAttestor collects post-acceptance attestations into a ⅔-stake finality certificate for
// external consumers. It is an ARTIFACT builder, never a decider: no acceptance path, no
// lock/round/committedSlot state, no durable consensus state. Safe for concurrent use.
type QuasarAttestor struct {
mu sync.Mutex
verifier VoteVerifier // verifies a peer attestation's signature at the block's epoch
stake StakeSource // ⅔-by-stake threshold + live committee size at the epoch
buckets map[attestKey]*attestBucket // in-flight, pruned when a cert emits at the height
certs map[uint64]*QuorumCert // emitted artifacts, by height (the external cert chain)
subjects map[uint64]map[ids.ID]struct{} // distinct canonicals attested per height (equivocation evidence)
}
// NewQuasarAttestor builds the sidecar. verifier + stake are the SAME finality-crypto
// dependencies the engine already wires. The block's P-chain EPOCH is a VALUE the caller binds
// to each Ingest (bound to the position it verifies), NOT a shared height→epoch place — so two
// concurrent ingests of same-height forked blocks under different epochs can never cross epochs.
// A nil stake source is rejected at emit time (fail-closed — a cert must be ⅔-stake checkable).
func NewQuasarAttestor(verifier VoteVerifier, stake StakeSource) *QuasarAttestor {
return &QuasarAttestor{
verifier: verifier,
stake: stake,
buckets: map[attestKey]*attestBucket{},
certs: map[uint64]*QuorumCert{},
subjects: map[uint64]map[ids.ID]struct{}{},
}
}
// Attest is the POST-ACCEPT hook: this node, having ALREADY accepted the block at pos via the
// sampler, signs the finality message and returns its own attestation to gossip. It performs NO
// acceptance decision and has NO side effect on the chain — a signing failure is returned to the
// caller and simply means this node contributes no attestation (best-effort; the chain does not
// care). The returned SignedVote is byte-compatible with a legacy finality accept vote.
func (q *QuasarAttestor) Attest(pos VotePosition, signer VoteSigner, self ids.NodeID) (SignedVote, error) {
if signer == nil {
return SignedVote{}, fmt.Errorf("quasar attest: nil signer")
}
sig, err := signer.SignVote(CanonicalVoteMessage(pos))
if err != nil {
return SignedVote{}, fmt.Errorf("quasar attest: sign: %w", err)
}
return SignedVote{NodeID: self, Accept: true, Signature: sig}, nil
}
// Ingest verifies and records one attestation (own or peer) for the accepted block at pos, at
// the block's P-chain `epoch` (a VALUE bound to this position — the SAME epoch the accept votes
// were verified under; NOT a shared height-keyed place). Returns the emitted certificate the
// FIRST time this subject reaches the ⅔-stake threshold (nil, false otherwise). It NEVER accepts
// a block and NEVER blocks — its only outputs are a verified-attestation record and, once enough
// stake has attested, a cert.
//
// Fail-closed: an attestation whose signature does not verify at the block's epoch, or a
// non-accept vote, or one whose message disagrees with an established bucket, is dropped.
func (q *QuasarAttestor) Ingest(pos VotePosition, epoch uint64, att SignedVote) (*QuorumCert, bool, error) {
if !att.Accept {
return nil, false, fmt.Errorf("quasar ingest: non-accept attestation from %s", att.NodeID)
}
canonical := pos.CanonicalID
if canonical == ids.Empty {
canonical = pos.BlockID // bare-block degrade (inner == outer)
}
msg := CanonicalVoteMessage(pos)
if q.verifier == nil || !q.verifier.VerifyVote(att.NodeID, msg, att.Signature, epoch) {
return nil, false, fmt.Errorf("quasar ingest: signature failed verification (voter %s height %d)", att.NodeID, pos.Height)
}
// A nil stake source is a configuration that cannot certify anything, and
// this is fail-closed by DESIGN. It has to be a rejection here, not a nil
// dereference at q.stake.SignerCount below: every entry point takes peer
// data, so the deref is a remote crash on any node wired without stake.
if q.stake == nil {
return nil, false, fmt.Errorf("quasar ingest: no stake source configured")
}
q.mu.Lock()
defer q.mu.Unlock()
// Record the distinct-canonical evidence for the height BEFORE the
// already-certified check. A double-signer racing the honest cert gains
// nothing, so the ordering it picks is to sign the second execution AFTER the
// height certifies — and returning on `done` before recording the subject
// dropped exactly that evidence. The signature verified; only the record of
// it was skipped. Evidence is a property of the signed votes, not of whether
// a cert already exists, so it is recorded either way.
if q.subjects[pos.Height] == nil {
q.subjects[pos.Height] = map[ids.ID]struct{}{}
}
q.subjects[pos.Height][canonical] = struct{}{}
// Already certified at this height → no new cert to emit, but the evidence
// above is kept.
if _, done := q.certs[pos.Height]; done {
return nil, false, nil
}
key := attestKey{height: pos.Height, canonical: canonical}
b := q.buckets[key]
if b == nil {
b = &attestBucket{pos: pos, msg: msg, votes: map[ids.NodeID]SignedVote{}}
q.buckets[key] = b
} else if !bytes.Equal(b.msg, msg) {
// Same (height, canonical) but a DIFFERENT signed message (e.g. a disagreeing set-root
// or state-root) — cannot aggregate into one cert; drop, fail-closed.
return nil, false, fmt.Errorf("quasar ingest: attestation message mismatch for canonical %s at height %d", canonical, pos.Height)
}
cp := att
cp.Signature = append([]byte(nil), att.Signature...)
b.votes[att.NodeID] = cp
// Try to emit once the export count floor is reached. n is the LIVE signing set at
// the epoch (no static K/α), so the quorum tracks the set each height. This call is
// the assembly TRIGGER and not the gate: it is read from the one definition of the
// floor — SignerFloor, the same function the verifier compares this certificate's
// declaration against — so the attestor never stops trying before the verifier would
// pass, and VerifyWeighted below is the sole authority on whether it does. That
// asymmetry is deliberate: the trigger is allowed to be looser than the rule and must
// never be tighter. It is looser at exactly one place: a set below minBFTCommittee
// reaches this count and is refused by the export rung's committee clause, so such a
// chain assembles and discards rather than emitting. One authority, and it is the
// verifier.
n := q.stake.SignerCount(epoch)
threshold := SignerFloor(Quasar, n)
if len(b.votes) < threshold {
return nil, false, nil
}
votes := make([]SignedVote, 0, len(b.votes))
for _, v := range b.votes {
votes = append(votes, v)
}
cert, err := AssembleQuorumCert(b.pos, Quasar, uint32(threshold), votes)
if err != nil {
return nil, false, nil // not yet assemblable (below threshold after dedup); keep collecting
}
// The ⅔-stake predicate is the real finality gate for external consumers.
if err := cert.VerifyWeighted(q.verifier, q.stake, epoch); err != nil {
return nil, false, nil // count floor reached but stake supermajority not yet — keep collecting
}
q.certs[pos.Height] = cert
delete(q.buckets, key) // artifact emitted; stop accumulating for this subject
return cert, true, nil
}
// CertAt returns the emitted finality certificate at a height, if the ⅔-stake attestation has
// formed. This is the external artifact bridges and the 0x9999 DEX receipt consume.
func (q *QuasarAttestor) CertAt(height uint64) (*QuorumCert, bool) {
q.mu.Lock()
defer q.mu.Unlock()
c, ok := q.certs[height]
return c, ok
}
// EquivocationEvidenceAt returns the distinct inner canonical ids that have been ATTESTED at a
// height. More than one is evidence that stake signed two different blocks at one height —
// which, under honest-sign-only + the ⅔ threshold, can never produce two certs, but IS
// slashable evidence. One (or zero) is the healthy case.
func (q *QuasarAttestor) EquivocationEvidenceAt(height uint64) ([]ids.ID, bool) {
q.mu.Lock()
defer q.mu.Unlock()
s, ok := q.subjects[height]
if !ok || len(s) == 0 {
return nil, false
}
out := make([]ids.ID, 0, len(s))
for id := range s {
out = append(out, id)
}
return out, len(out) > 1
}
// PruneBelow drops all in-flight and emitted state strictly below height (bounded memory). The
// external cert chain is expected to be persisted by its consumer; the attestor keeps only a
// live window. No durable consensus state is involved — recovery is boot-from-lastAccepted and
// re-attest the frontier.
func (q *QuasarAttestor) PruneBelow(height uint64) {
q.mu.Lock()
defer q.mu.Unlock()
for k := range q.buckets {
if k.height < height {
delete(q.buckets, k)
}
}
for h := range q.certs {
if h < height {
delete(q.certs, h)
}
}
for h := range q.subjects {
if h < height {
delete(q.subjects, h)
}
}
}