mirror of
https://github.com/lux-cpp/consensus.git
synced 2026-10-30 18:14:55 +00:00
Two gaps, one rule. The portable certificate had only Cert::verify — structure, ordering and every signature — and that is not an accept rule: its last clause counts distinct valid accepts against the certificate's OWN threshold, so one declaring 1 clears it on a single signature. Nothing on this side could weigh a GOSSIPED certificate against a validator set at all, while Go's gossip path could, and a certificate two implementations admit and one cannot weigh is not one rule. Cert now carries verify_weighted(const Keys&, const Stake&), clause for clause with Go's VerifyWeighted and Rust's verify_weighted, over a new node-keyed Stake seam — separate from Keys because membership and weight are separate facts read from separate places. And the quorum a certificate declares is now the set's number in both objects. QuorumCertEngine::verify_cert has always demanded the derived floor exactly; the portable one demands it too, and the free function signer_floor(Tier, n) is the one definition both read, which is Go's chain.SignerFloor and Rust's finality::signer_floor. Equality and not a lower bound, in both directions. decision.json carries the declaration beside the derived floor so a runner checks one against the other, and three of its eighteen rows declare something else on purpose. The LOCAL gate is handed no certificate and therefore no declaration, so it accepts those three and the harness says so outright: two objects, two questions. The rest of the corpus is byte-identical — cert_verify and cert_wire included, because the structural predicate did not move. Co-authored-by: Hanzo Dev <[email protected]>