12 Commits
Author SHA1 Message Date
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
zeekayandHanzo Dev 287b89c64f cert: derive the floor on the road a certificate arrives by, too
The rule landed at the door that MINTS a token from raw votes. Auditing the three
sites that promote a certificate to that token found the same hole one function
away, on the road a certificate ARRIVES by: with no stake source, verifyCert fell
to the count-only Verify, whose last clause counts distinct valid accepts against
the number the CERTIFICATE declares. So a gossiped Quasar certificate declaring a
quorum of one, carrying one signature, was promoted and finalized.

The two doors answer differently, and the difference is the threat rather than an
oversight. Minting takes the floor from the CALLER's alpha, so with no set there
is nothing to check the caller against and the export rung is refused outright.
Admitting one that arrived checks N real signatures from N distinct in-set
validators; what was missing was only that its declared quorum was its own. So the
floor is derived there too, from the node's live committee — which is that road's
authoritative view of its set. Refusing the rung outright instead would retire the
equal-stake deployment, which is a larger decision than this clause is.

THE PRICE, stated because it is real: with no stake model the committee is a
CONFIGURED number, so that road now needs the fleet to agree on it. Before, any
declaration was admitted and a node with a different K interoperated by accident.
That is the cost of not letting a certificate name its own quorum, and it is why
the minting door refuses the rung rather than weighing it — a configured committee
is strong enough to CHECK a declaration and not strong enough to ORIGINATE one.

And the count-only road reads a DIFFERENT derived number, which is the trap this
change nearly walked into. SignerFloor is the distinct-signer guard the weighted
road carries beside a stake clause; Quorum is the count that IS the predicate
where no stake clause stands beside it. They agree at Quasar and diverge at Nova
past five signers, because NovaSignerFloor saturates at three — correct as a guard
on a road whose majority is carried in stake, wrong as a majority on a road that
has only the count. assembleCertLocked already declares NovaQuorum on that arm, so
reading SignerFloor there would make a node refuse its OWN certificates at six
signers and up. Every committee the fixtures use is four or five, exactly where
the two agree, so nothing in the suite would have said so:
TestTheTwoRoadsDeriveTheNumberTheyDeclare walks 1..64 instead.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-02 02:23:17 -07:00
zeekayandHanzo Dev 8d94ad1e03 cert: derive a certificate's quorum from the set, never from the certificate
A certificate carries a Threshold, and until now that field was its own
authority on the count-only road: Verify's last clause counts distinct valid
accepts against the number the CERTIFICATE names, so one declaring 1 clears it
on a single signature. C++ has always demanded the derived floor exactly, so the
same certificate was accepted by two implementations and refused by the third.

VerifyWeighted now requires Threshold == SignerFloor(tier, n) — the floor this
set gives this rung. Equality and not a lower bound: an over-claim names a bar
this set does not set, and tolerating it would let a certificate redefine the
rung upward exactly as tolerating an under-claim lets it redefine the rung down.

SignerFloor is the one definition of a rung's count floor, read by the rung
predicates, by the assemblers and by the new clause, so the number enforced and
the number declared cannot drift.

Two corollaries the rule forces:

  No set, no export. BuildVerifiedQuorumCert refuses any tier that authorizes
  export when no stake source is supplied. That road counts votes against the
  caller's own alpha, so an export token could be minted over one signer with
  alpha of one — and AcceptWithCert trusts the token, which is the point of the
  token. The accept rung keeps the road: it authorizes local execution the chain
  can still reorg away.

  The assembler reads the same n the verifier does. assembleCertLocked derived
  Nova's declared floor from effectiveCommittee, which sizes the SAMPLE and is
  floored at the minimum Byzantine committee. At three signers the sampler said
  three and the set said two, so a node would have built a certificate its own
  peers refuse. The export path already read SignerCount.

The corpus grows five rows — three that declare a quorum of their own, and two
lopsided sets where the stake half binds alone, which an equal set cannot state
because both halves bind at one edge there. Two frozen verdicts change the clause
they are NAMED by, from the stake floor to the count, and neither changes from
refused: over an equal set a certificate short of its stake is also short of the
quorum it now has to declare.

The rungs' own count clauses are unreachable as a result — Verify proves count
reaches the declaration and the new clause proves the declaration is the floor —
and are kept as the place the floor is defined and as the guard that survives any
change to the declaration rule. Written down in LLM.md beside the other walls
behind walls.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-02 02:23:07 -07:00
zeekayandHanzo Dev 43c74b012d go: name every refusal, then check that the named one is the one given
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]>
2026-09-01 22:19:35 -07:00
zeekayandHanzo Dev ac10e53f84 cert: give the export rung a floor on the SET, so a shrinking denominator cannot shrink past a committee
Reading both floors over the signers let a set shrink to its signers, and a rung
whose floors are read over n is satisfiable at every n. TwoThirdsCount(1) is 1, so
one signature over a one-signer set clears the count floor and the stake floor
together and mints export finality. Byzantine tolerance is f = ⌊(n−1)/3⌋, which is
ZERO for n of one, two and three: such a certificate absorbs no fault at all, and
one compromised key among its signers forges it outright.

So the export rung refuses a signing set below minBFTCommittee — the same constant
bftCommittee floors committee SELECTION at, read at the certificate instead of at
the sampler, and reused rather than restated. It is a floor on the SET, stated
beside the n<1 refusal because neither quorum floor implies it: both are read over
n and both shrink with it. Nova has no such clause and must not grow one — it
authorizes local execution the chain can still reorg away, so a small or
partitioned chain has to be able to make progress. A three-validator chain now
PRODUCES and never CERTIFIES; operators need n ≥ 4 to export with any fault
tolerance. Rust mirrors it as CertError::MinCommittee, which the corpus vocabulary
folds into Go's one belowThreshold class.

The transcription guard was reading the wrong denominator. verdict_test computed
n as len(c.Set), the membership ROLL, so a floor moved onto the roll would agree
with the guard and be re-blessed by -update — the second line of defence agreeing
with the regression it exists to catch. It now counts the seats that hold a key,
on the Quasar branch and the Nova one, and transcribes the committee floor as a
search for the least n whose fault budget reaches one rather than as the constant.
Proven by construction: with the corpus source reporting the roll as its signer
count and the golden re-blessed, the old guard passes and the new one fails.

The corpus could not have caught it either. quasar_keyless_third had three signers
on a roll of four, and ⌊2·3/3⌋+1 and ⌊2·4/3⌋+1 are both three, so its count half
was vacuous. Two rows now separate the denominators one at a time:
quasar_keyless_stake (six signers, a seventh member holding a third of the roll —
the count floor is five either way, so only stake decides) and
quasar_keyless_count (four signers, two members holding one unit each — the stake
clears under either reading, so only the count decides). TestKeylessRowsBind-
BothDenominators fails if either is lost or flattened.

quasar_keyless_third itself FLIPS to a refusal, and it is the row that states the
new clause: the recorded stake floor is 200 over the three hundred that can sign
and not 333 over the five hundred the chain carries, so the denominator IS the
signers — and the certificate is refused anyway, on neither quorum floor. Reading
the floors over the signers stops a spectator stranding a chain; it is not a way
for a chain with three signers to export. Every other frozen verdict is
byte-identical, and the admission section and the signed vectors are untouched.

StakeSource gains CarriedStake, and no floor may read it. SignerStake alone
decides a certificate correctly and cannot show the set shrinking underneath one:
a chain whose signable stake has fallen to a sliver of what it carries still
certifies at ⅔ of the sliver, and ResponsiveStakePct — voted over signer — stays
at one the whole way down. FinalityStatus.SignerStakePct is the number that falls,
and the one to alarm on. Rust has carried(), C++ has total_stake(); Go had lost
the projection when the denominator moved.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 20:52:28 -07:00
zeekayandHanzo Dev f4913a3fd7 cert: read both floors over the signers, so stake that cannot vote cannot strand export
A validator set may carry a member with no signing key. It holds stake and no
verifier will ever accept a signature from it — it is a spectator, not a party to
the vote. Both quorum floors were nonetheless read over the whole membership:
StakeSource.TotalStake summed its weight and ValidatorCount counted its seat.

That is a category error with an arithmetic consequence. Once keyless stake
reaches a third of what the chain carries, floor(2/3 * carried) is above the
whole of the signable stake, so the export rung cannot be reached even with every
signer in the set agreeing — permanently, with no misbehaviour anywhere. Past a
half, Nova goes the same way. The count floor strands the same way in seats: two
thirds of the roll can exceed the number of signatures the set is able to produce.

So the seam says what it means. TotalStake becomes SignerStake and ValidatorCount
becomes SignerCount, both read over the validators that can actually sign, and
Weight answers 0 for a keyless member as it already did for a stranger. The
rename is deliberate rather than a doc change: an implementer of the interface
has to confront the question, instead of silently keeping a denominator that
strands its own chain.

This is the two-thirds rule read over the right set, not a weakening of it.
Quorum intersection is untouched — two disjoint quorums each past 2/3 of the
signer stake would sum past the whole of it — and the fault budget simply stops
being spent on parties known in advance to cast no vote. A key absent at the
epoch is a known non-participant; BFT's third is a budget for the unknown ones.

Rust follows: ValidatorSet is both the verifier and the source, and its
insert_unkeyed door made this reachable through a public API. It now keeps
`signable` beside `total` — total still bounds admission, because a set whose
weights cannot be represented is refused whether or not the seat that overflows
it can sign, and signable is what a quorum is measured against. carried() names
the first for what it is.

C++ needed no change and is the reason to believe the shape: a Validator there IS
a public key and the engine's set is keyed by one, so a spectator has never had a
slot to occupy and total_stake_ was always the signer stake.

Corpus: quasar_keyless_third, the case the R5 TODO could not state. Three seats
of a hundred hold keys, a fourth holds two hundred and none; all three signers
sign and the export rung admits it on 300 of 300, where the roll denominator
would refuse it against floor(2*500/3)=333 forever. Every existing verdict is
byte-identical — the seat gains an omitempty keyless flag, so a set of ordinary
seats encodes exactly as before.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 19:07:15 -07:00
zeekayandHanzo Dev 82ab92a70f cert: give the export rung a distinct-signer floor, so two thirds means two thirds of the parties
The Quasar rung read only stake. Two thirds of the stake is one signature
wherever two thirds of the stake is one validator, so a whale holding a
supermajority could mint an export-grade certificate alone — and export-grade
finality is what a bridge, a settlement and a cross-chain message admit on. A
certificate one key can produce is not a claim that a Byzantine supermajority
agreed, however much weight stands behind the key.

VerifyWeighted now demands config.TwoThirdsCount(n) = floor(2n/3)+1 DISTINCT
signers of a Quasar certificate as well as the stake. Two independent
predicates, neither sufficient alone, the same shape Nova has one rung up.

TwoThirdsCount is the count that value has always been, given its own name and
one home. It is derived from TwoThirdsStakeFloor over unit weights rather than
restated, so the count and the stake predicate cannot drift. It replaces three
spellings of the same closed form that had grown apart in reach if not yet in
value: config.EqualStakeSupermajorityThreshold (the alpha sizer), the private
chain.quasarQuorum (the attestor's assembly trigger), and the clause that was
missing from the verifier entirely.

Stake is read first and the count second. Both must hold, so the order names
only which clause a refusal reports: on an equal-stake set the two are one bar
in two units and bind together, and the count clause binds ALONE only where the
weights are lopsided — so reaching it means the stake was there and the signers
were not. Every verdict the R3 corpus froze is unchanged.

Liveness. Nothing that exports today stops exporting. QuasarAttestor, the only
producer of export certificates, already assembled at exactly floor(2n/3)+1
attestations, and FeasibleParams already sized alpha — the floor
HandleIncomingCert applies to a gossiped Quasar cert — to the same number.
Producer, gossip door and verifier now read one function, so the verifier's new
clause refuses nothing the engine can build and exactly what it never could.
quasar_count_floor_test.go holds all three to that.

The tests that moved were the ones written on three seats, where floor(2n/3)+1
is three and no two-signer certificate can isolate the stake half any more.
They ask the same question on four.

Rust: the same clause in Cert::verify_quasar_supermajority, the same name.
1.22.4 -> 1.22.5 — this changes what the crate accepts.

Also, at the already-admitted door: the zero-weight refusal is justified by
agreement with Register, not by the phantom-signer argument alone. A seat whose
key does not decode can never sign and is refused anyway, because the weight is
read before the key is — and deferring it would make admission depend on which
keys a node's crypto build can parse.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 17:39:39 -07:00
zeekayandHanzo Dev 3a06f4888d validator: refuse a keyed seat with no stake at the second door
Register refuses a validator admitted with no stake, because a keyed seat
carrying zero weight is a phantom signer: it raises the count of distinct
signers a floor is read against and contributes nothing to the stake the same
certificate is weighed by. FlattenValidatorSet admitted it, so the clause the
two doors are supposed to share meant one thing at registration and another on
a set the chain had already admitted.

The corpus said so before the code did. AdmissionCase already records that "the
weight clauses are common to both" doors, and the transcribed rule in
verdict_test.go already demanded a zeroWeight refusal for any case carrying a
weightless seat, at either door — there was simply no case exercising this one.
There is now, produced by the live function, and it fails against the old door
on the golden AND on the transcribed rule independently.

Refused, not skipped, and that is not the trade this door makes for an
undecodable key. Skipping a bad key costs a signer the set was going to lose
anyway; admitting a weightless keyed seat is a signer the set gains for free.
A KEYLESS seat with no stake is untouched: it is skipped before the weight
clause, never becomes a signer, and neither signs nor weighs — the leniency this
door keeps, and the one Rust's insert_unkeyed keeps for the same reason.

Placed where Register places it and where Rust's ValidatorSet::insert places it:
after the key is known to be present, before it is decoded. A shaped key is
therefore enough to reach the clause on either side, which is what lets the Rust
runner answer this row instead of skipping it as it must skip the other two at
this door.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 16:32:21 -07:00
zeekayandHanzo Dev 2fab570b4b conformance: freeze what the predicate DECIDES, not only what it encodes
The corpus stated what a validator signs and what a certificate looks like
on the wire. That is necessary and it is not sufficient: an implementation
can reproduce every byte and still finalize on the wrong number of signers,
because encoding and deciding are different questions. A build carrying no
weighted predicate at all passed a corpus that only ever asked it to encode.

So the corpus now asks the other question. Each case states a validator set,
the distinct signers a certificate carries and the rung it attests, and
records what VerifyWeighted decided about it — read from the live predicate,
never restated, the same way the proof-of-possession vector records what
pop.Verify returns.

Both edges of a forty-one seat set are pinned, seven seats apart, so an
implementation that confuses the majority floor for the supermajority floor
cannot pass by accident. The lone holder of a stake majority is pinned
refusing on the COUNT: stake cannot buy a pass past the signer floor, which
is what stops a single holder self-igniting.

Signatures are not the subject. Every vote is resolved as correctly signed,
so a case can only turn on the weighted half. Braiding the two would mean a
weighted rule could not be stated without a key ceremony, and a runner that
lacked one would report PASS by skipping the case — the failure this closes.

The golden alone would let a moved threshold be re-blessed with -update, so
the boundaries are also stated outright and recomputed in arbitrary
precision: 2*voted > total, and 3*voted > 2*total. Neither can share an
off-by-one with the floors it checks.

The export rung's own signer floor (R4) and the keyless denominator (R5) are
left as open decisions rather than frozen expectations, marked where the next
cut extends this section.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 13:18:51 -07:00
zeekayandHanzo Dev 5639cf92e9 consensus: re-cut the golden corpus for the seed encoding, and say so in the standard
TestCorpusMatchesGolden caught the change, which is what it is for: 472 values
moved, all of them under /fpc, nothing added, removed or restructured. The vote
message, the certificate, the ladder, the count thresholds and the committee are
byte-identical, so the encoding change reached exactly the plane it was meant to
and no other.

Two of the moved values are the reason this is a protocol change and not a
refactor: /fpc/thresholds[1]/alpha went from 3 to 4. A round at that seed and
phase now accepts on one more vote. Freezing the encoding before anything runs
it is the whole point of doing this now.

standard.mdx carried the old preimage, and a standard that is quietly wrong is
worse than none. It now shows the domain and the lengths, and says what they
buy: without them the boundary between the chain id and the parent slides, and a
chain that names itself chain_id‖H derives what chain_id derives at parent H —
which trades away the input nobody can know before the previous epoch finalizes,
the input the whole construction exists to bind.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-29 18:12:18 -07:00
zeekayandHanzo Dev b41c532fa4 rust: derive θ with SHA-256, and verify the signatures
Two implementations of one protocol disagreed about when a round accepts
and about what a certificate proves, and nothing in the repository said
so because each agreed with itself.

θ. The selector's doc comment said "PRF (SHA256)" and the code called a
SipHash-like mixer. Go's protocol/wave/fpc computes θ with crypto/sha256,
and α = ⌈θ·k⌉ is the vote count a round accepts on, so the two nodes
applied different accept counts to identical votes: at phase 0 with k=20
the mixer returns 11 where Go returns 15. Measured both ways — the mixer
is spliced back in under tests/conformance.rs and the check fails on θ
before it reaches α. The seed was worse than the PRF: three hardcoded
ASCII constants, public forever, so every future threshold was readable
in advance. derive_epoch_seed is Go's DeriveEpochSeed, which binds the
previous epoch's block hash and is therefore unknown while the current
epoch is still open.

The phase now advances once per counted round, past the k-vote gate,
which is where Go advances it. It was advancing once per vote, k times
too fast, so two nodes one round apart read θ from different phases.

Signatures. verify_certificate ended in

    !cert.aggregated_sig.is_empty() || !cert.quantum_sigs.is_empty()

and the aggregator it asked about returned vec![0u8; 48] whenever it had
nothing to aggregate. Zero bytes are not empty. It had nothing to
aggregate on every input, because it sliced each signature to 48 bytes
and handed it to a G2 decoder: luxfi/crypto/bls signs under
BLS_SIG_BLS12381G2, so a public key is 48 bytes and a signature is 96,
and this crate had the two the other way round. So forty validators
voting with no signature at all produced a certificate that verified,
and there was no input for which it did not.

finality::Cert is engine/chain.QuorumCert — the position, the rung, the
threshold, and one signature per named signer. It encodes to the bytes
the network gossips, decodes strictly, and verifies Go's seven clauses:
version, role, tier, non-zero threshold, non-empty votes, strictly
increasing signers, accept-only, and every signature checked against a
registered key over the message the cert's OWN position derives. A
signature does not travel between positions. verify_stake adds the rung's
stake predicate, re-derived from the live set rather than read from the
cert, so a Nova quorum relabeled Quasar fails; an unresolved set fails
closed.

The engine no longer issues certificates. Its block carries an id, a
parent, a height and a payload, and a certificate binds a chain id, a
round and three roots — nothing it can assemble is a statement the
network signs, so what stood there manufactured finality out of a
position it did not have and votes it had not checked.

The corpus carries the rule now: 8 epoch seeds and 192 thresholds read
from the live Go selector, θ as exact IEEE-754 bits because α is a
ceiling and the last bit decides. Certificates are compared over their
whole wire form; the previous check read four header bytes.

Also removed: build.rs linked a C library no Rust symbol references, and
libc was unused. BENCHMARK_RESULTS.md reported a benchmark that has not
compiled in a long time — it imported five names this crate does not
export — so the benchmark is rewritten against the real API and the
retracted report is gone.

33 tests, no warnings.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 23:52:34 -07:00
zeekayandHanzo Dev a91b81b959 consensus: write the finality standard down as data
The C++ implementation signs 70 bytes under "LUX/chain/vote/v1\0"; this
one signs 226 under v2. Neither side could ever verify the other, and
nothing in the repository said so. The rule was only ever expressed as
Go, so the only way to check another language against it was to read it
and retype it — which is how the two drifted.

conformance/corpus.json is the standard as data: the signed message, the
certificate wire form, both stake floors across the full u64 range, the
ladder's authorizations and the committee parameters, all read from the
live definitions rather than restated beside them. An implementation in
another language is conformant when it reproduces that file.

A golden file alone would be weak — it can be re-blessed with -update
and the change vanishes. So the load-bearing facts are also stated
outright, transcribed from the spec and sharing no code with the
encoder: the message is 226 bytes, the tag is the one the network uses,
the two-thirds floor recomputed in arbitrary precision, and every signed
axis moved one at a time to prove it is bound. Re-blessing the golden
does not get past those.

CanonicalRejectMessage is exported alongside it. The engine has always
signed reject votes, but only the accept half could be reached from
outside the package, so only half the standard could be written down.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 23:26:41 -07:00