merge: adopt vote.rs + zap.rs into the canonical crate (lux-rs fork retirement); Tally derives its quorum from the set

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-09-02 01:58:33 -07:00
co-authored by Hanzo Dev
8 changed files with 1879 additions and 3 deletions
Generated
+1 -1
View File
@@ -408,7 +408,7 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lux-consensus"
version = "1.22.6"
version = "1.22.7"
dependencies = [
"blake3",
"blst",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "lux-consensus"
version = "1.22.6"
version = "1.22.7"
edition = "2021"
description = "Lux Consensus SDK — pure-Rust Quasar post-quantum probabilistic consensus (Wave / FPC / Photon / Focus / Quasar PQ finality via hybrid BLS12-381 + Pulsar = P3Q)"
license-file = "LICENSE"
+7 -1
View File
@@ -313,7 +313,13 @@ impl QuorumCert {
/// The count-only predicate: every structural clause, plus a valid signature
/// from each of at least `threshold` distinct voters.
///
/// This is NOT a standalone accept rule, and must not be used as one. The
/// A CERTIFICATE IS A RECORD, NOT A CAPABILITY. Every field on it is public
/// and anyone may build one that says anything; holding one authorises
/// nothing. What authorises is [`Self::verify_weighted`], and EVERY acceptor
/// runs it — this, the count-only predicate, is the structural pre-filter
/// inside it.
///
/// So it is NOT a standalone accept rule, and must not be used as one. The
/// `threshold` it counts against is a field of the certificate — the
/// certificate names its own quorum — so on its own this clears a 1-of-n
/// certificate that declares `threshold: 1`. It is a building block:
+24
View File
@@ -216,6 +216,30 @@ pub fn two_thirds_count(n: i64) -> i64 {
two_thirds_stake_floor(n as u64) as i64 + 1
}
/// The DISTINCT signers a certificate must carry to attest `tier` over a set of
/// `n` signers — the whole of a certificate's authority in seats, and a function
/// of the set and the rung, never of the certificate.
///
/// One definition, read in three places: the assembler picks its alpha from it,
/// the weighted predicate enforces it, and the derived-threshold clause compares
/// the certificate's own declaration against it. A second spelling anywhere is
/// how a certificate acquires a quorum of its own choosing.
///
/// * Nova — [`nova_signer_floor`], which saturates at three: local execution has
/// to stay reachable on a small chain, and it is reorgable.
/// * Quasar — [`two_thirds_count`], the export supermajority read in seats, the
/// same supermajority the stake clause reads in weight.
///
/// A rung that is not an accept tier has no floor and gets none: 0, which every
/// caller reads as a refusal. Go's `chain.SignerFloor`.
pub fn signer_floor(tier: Finality, n: i64) -> i64 {
match tier {
Finality::Nova => nova_signer_floor(n),
Finality::Quasar => two_thirds_count(n),
_ => 0,
}
}
/// The minimum vote count that CAN reach the two-thirds-by-stake predicate for a
/// weight vector: order heaviest first and count until the running stake first
/// exceeds the floor. Returns 0 for an empty set, a zero total, or a vector with
+15
View File
@@ -13,6 +13,10 @@
//! - **Photon**: Light-based validator sampling with luminance tracking
//! - **Focus**: Confidence accumulation through β consecutive rounds
//! - **Quasar**: Post-quantum finality with hybrid BLS + Pulsar signatures
//! - **Vote**: A signed vote on the wire, and the tally that turns arriving
//! votes into a certificate the whole `cert` predicate has already passed
//! - **Zap**: The transport frame those votes travel under — the same wire Go
//! and C++ speak
//!
//! ## Example
//!
@@ -68,6 +72,16 @@ pub mod finality;
pub mod cert;
pub mod pop;
// The vote plane: what one validator puts on the wire, and the tally that turns
// what arrives into a certificate. It states no accept rule of its own — the
// rule is `cert`'s, and `vote::Tally::cert` is the only door out of a tally.
pub mod vote;
// The ZAP transport frame the vote plane travels under — the same wire Go's
// `luxfi/api/zap` writes and `luxcpp/zap` reads. Layer A only: nothing in it
// knows what a vote is.
pub mod zap;
/// SHA-256. The crate's one hash, from the BLS library already linked here.
///
/// Every derivation the network agrees on runs through this — there is no
@@ -95,6 +109,7 @@ pub use crate::focus::*;
pub use crate::wave::*;
pub use crate::quasar::*;
pub use crate::engine::*;
pub use crate::vote::{SignedVote, Slot, Tally, VoteTransport, VOTE};
// ============= TYPES MODULE =============
+509
View File
@@ -0,0 +1,509 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//! A vote on the wire, and the rule that turns votes into a certificate.
//!
//! Four things live here and nothing else:
//!
//! * [`SignedVote`] — one validator's signed statement about one position,
//! and its ZAP frame payload.
//! * [`Slot`] — the one place a validator may make one statement, read out of
//! the signed bytes so equivocation is visible before a key is used.
//! * [`VoteTransport`] — the seam a node disseminates over. Consensus states
//! the shape and never opens a socket; the node supplies the mesh.
//! * [`Tally`] — verify, deduplicate, and issue a [`QuorumCert`] that has
//! passed the whole of [`QuorumCert::verify_weighted`].
//!
//! THE PAYLOAD CARRIES THE SIGNED BYTES. A vote frame holds the exact
//! [`canonical_vote_message`] its signer signed, not the fields to rebuild it
//! from. A receiver that re-derives the preimage is a second implementation of
//! the derivation, and two derivations are how one node verifies a message the
//! other never made. The position is recovered *from* the message, so the
//! signed bytes and the read position cannot disagree. The C++ lane sends the
//! block id alone and re-derives, which is the shape this one refuses; [`VOTE`]
//! spells that divergence out and says which side owes the reconciliation.
//!
//! The transport identity — `block_id`, `parent_id` — is not signed and so is
//! not carried; a decoded position leaves those empty, and re-encoding is
//! byte-identical because [`Position::signed_identity`] resolves the degrade in
//! one place.
//!
//! THIS MODULE STATES NO ACCEPT RULE. A tally counts what it can verify and
//! nothing more; whether the votes it holds amount to finality is
//! [`crate::cert`]'s question, asked through [`QuorumCert::verify_weighted`] and
//! answered against the LIVE validator set — the signer floor, the stake floor
//! and the minimum Byzantine committee, recomputed every time. There is no door
//! out of here that skips it: [`Tally::cert`] is the only way to obtain a
//! certificate from a tally, and it runs the full predicate before it returns
//! one. And a caller states no threshold to begin with: [`Tally::cert`] reads
//! the rung's floor off the set at the moment it issues, so there is no
//! declaration for the set to disagree with.
use std::collections::BTreeMap;
use crate::cert::{CertError, NodeId, QuorumCert, StakeSource, Vote, VoteVerifier, SIGNATURE_LEN};
use crate::finality::{
canonical_vote_message, signer_floor, Finality, Id, Position, EMPTY, QC_FINALITY,
QUORUM_CERT_VERSION, VOTE_MESSAGE_LEN, VOTE_TAG,
};
use crate::pop::NODE_LEN;
use crate::zap;
/// The ZAP message type a consensus vote travels under.
///
/// It sits in the block Go reserves for Quasar — the finality tier a vote
/// carries — and is the next free id there. `luxfi/api/zap` (`wire.go`) spells
/// 60 `MsgSetQuasarFinalized` and 61 `MsgQuasarHeight` of the 60..63 range and
/// stops, so 62 is unclaimed in the registry the plugin boundary reads.
///
/// It is deliberately NOT `0x11`. The C++ lane's `vote_codec.hpp` uses `0x11`,
/// which is 17, which the Go registry already holds as `MsgResponse`. One id
/// space, one meaning: a link that ever carried both would dispatch a vote to
/// the response path.
///
/// THE TWO LANES DO NOT YET CARRY EACH OTHER'S VOTES, and the id is the smaller
/// half of why. C++ frames `block_id(32) ‖ pubkey(48) ‖ sig(96)` — 188 payload
/// bytes naming its signer by its 48-byte key — where this lane frames
/// `signed_message(226) ‖ node(20) ‖ sig(96)`, 354 bytes naming its signer by
/// the 20-byte NodeID a proof of possession binds. C++ signs the same
/// [`canonical_vote_message`] and then sends only the block id, so its receiver
/// rebuilds the preimage it checks — the second derivation this module refuses
/// to have.
///
/// Two of the three have a standard to be held to, and this side is the one
/// holding it: 0x11 is `MsgResponse` in the Go registry, and Go names a
/// validator by the 20-byte `ids.NodeID` its proof of possession signs over.
/// The third is this module's own rule. So the reconciliation is owed on the C++
/// side — and until it is paid the two exchange frames neither can read, since
/// each refuses the other's type id and, past that, the other's field widths.
/// Nothing here should be read as saying they interoperate today.
///
/// 62 IS NOT UNCLAIMED EVERYWHERE. `github.com/luxfi/mpc`
/// (`pkg/transport/wire.go`) extends the same frame with its own 60..69 —
/// `MsgMPCReady` is also 62 — so the id is free in the registry and taken on the
/// MPC ring. Nothing here is wrong on the validator mesh, which carries votes
/// and no MPC traffic and is where these frames travel; and the MPC ring cannot
/// be reached by accident either, since it listens on its own port behind
/// mandatory PQ TLS 1.3 and closes any connection whose FIRST frame is not a
/// `MsgMPCReady` carrying JSON — which a vote payload is not. But "62 is free"
/// is true of `api/zap` and not of every link, and a reader deciding where else
/// to send a vote should know which. That extension also spells 64..69, above
/// the `TYPE_MASK` ceiling, so its 64 is a bare [`zap::ERROR_FLAG`] — a
/// registry-wide reconciliation is owed, and this end is not the place to spend
/// it.
pub const VOTE: u8 = 62;
/// The exact size of a vote frame payload: three length-prefixed fields.
pub const VOTE_PAYLOAD_LEN: usize = 4 + VOTE_MESSAGE_LEN + 4 + NODE_LEN + 4 + SIGNATURE_LEN;
/// Why a vote was not counted. Every variant means the vote does not happen;
/// none of them means "probably fine".
///
/// The clauses a CERTIFICATE owns are not respelled here — they are carried in
/// [`VoteError::Cert`] as the very values [`crate::cert`] names, so the version,
/// the role, the tier and every floor have one vocabulary across both modules
/// and a caller matching on them matches on one set of facts.
///
/// A vote plane that grows a refusal must be able to say so without breaking
/// every caller that matches on this, so the enum is closed to exhaustive
/// matching from outside: a downstream `match` carries a wildcard, and a new
/// clause reaches it rather than a compile error. Nothing here is added lightly
/// — but it is added before publication, because it can never be added after.
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VoteError {
/// The bytes are not a vote: a field of the wrong width, a length that
/// outruns the buffer, trailing bytes past the last field, a foreign tag,
/// or an accept byte that is neither 0 nor 1.
Wire,
/// A vote over some other position, offered to this tally. Decided on the
/// SIGNED BYTES, so any position that produces this tally's message is this
/// tally's statement and no other is.
Position,
/// A reject. A finality certificate carries accepts, so a reject is not a
/// vote a tally can hold — it is a statement about the same slot that a
/// caller must handle somewhere else.
NotAccept,
/// The signer does not resolve in the set, or the signature does not verify
/// under the key that does. An unregistered signer is refused here and not
/// skipped: a key the set never admitted through
/// [`crate::cert::ValidatorSet::insert`] — and so never proved possession of
/// — can put nothing behind a vote.
Signature,
/// A certificate clause refused it, including the two quorum floors and the
/// minimum committee.
Cert(CertError),
}
impl From<CertError> for VoteError {
fn from(e: CertError) -> Self {
VoteError::Cert(e)
}
}
impl std::fmt::Display for VoteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VoteError::Wire => f.write_str("not a vote frame"),
VoteError::Position => f.write_str("vote is over another position"),
VoteError::NotAccept => f.write_str("a reject is not a finality vote"),
VoteError::Signature => f.write_str("signer does not resolve, or signature is bad"),
VoteError::Cert(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for VoteError {}
/// One validator's signed statement about one position.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SignedVote {
pub position: Position,
pub accept: bool,
pub node: NodeId,
pub signature: Vec<u8>,
}
impl SignedVote {
/// The bytes this vote's signature is over.
pub fn message(&self) -> Vec<u8> {
canonical_vote_message(&self.position, self.accept)
}
/// Does the signature verify, under the key the set resolves for this node
/// at this epoch? An unresolvable signer is a false, not a skip.
pub fn verify(&self, verifier: &dyn VoteVerifier, epoch_height: u64) -> bool {
verifier.verify_vote(&self.node, &self.message(), &self.signature, epoch_height)
}
/// The certificate record this vote becomes.
pub fn record(&self) -> Vote {
Vote {
node_id: self.node,
accept: self.accept,
signature: self.signature.clone(),
}
}
/// The ZAP frame payload: the signed message, the signer, the signature.
pub fn encode(&self) -> Vec<u8> {
let mut w = zap::Writer::with_capacity(VOTE_PAYLOAD_LEN);
w.bytes(&self.message())
.bytes(&self.node)
.bytes(&self.signature);
w.take()
}
/// Read a vote out of a frame payload.
///
/// Every width is exact and trailing bytes are refused, so `vote ‖ garbage`
/// is not a vote and a peer cannot smuggle bytes past the decoder.
pub fn decode(payload: &[u8]) -> Result<Self, VoteError> {
let mut r = zap::Reader::new(payload);
let message = r.bytes().ok_or(VoteError::Wire)?;
let node = r.bytes().ok_or(VoteError::Wire)?;
let signature = r.bytes().ok_or(VoteError::Wire)?;
if r.remaining() != 0 {
return Err(VoteError::Wire);
}
if node.len() != NODE_LEN || signature.len() != SIGNATURE_LEN {
return Err(VoteError::Wire);
}
let (position, accept) = read_message(message)?;
Ok(SignedVote {
position,
accept,
node: node.try_into().map_err(|_| VoteError::Wire)?,
signature: signature.to_vec(),
})
}
}
/// Recover the position and the accept bit from the bytes that were signed.
///
/// The inverse of [`canonical_vote_message`], and checked against it by
/// round-trip: the tag, version and role are the network's, or these are not
/// this network's signed bytes.
pub fn read_message(message: &[u8]) -> Result<(Position, bool), VoteError> {
if message.len() != VOTE_MESSAGE_LEN {
return Err(VoteError::Wire);
}
// Offsets are the layout `canonical_vote_message` writes, in order.
let tag = VOTE_TAG.len(); // 18
if &message[..tag] != VOTE_TAG {
return Err(VoteError::Wire);
}
let version = u16::from_be_bytes([message[tag], message[tag + 1]]);
if version != QUORUM_CERT_VERSION {
return Err(CertError::Version {
got: version,
want: QUORUM_CERT_VERSION,
}
.into());
}
let role = message[tag + 2];
if role != QC_FINALITY {
return Err(CertError::Type {
got: role,
want: QC_FINALITY,
}
.into());
}
let id = |at: usize| -> Id { message[at..at + 32].try_into().expect("32 bytes") };
let chain_id = id(21);
let height = u64::from_be_bytes(message[53..61].try_into().expect("8 bytes"));
let round = u32::from_be_bytes(message[61..65].try_into().expect("4 bytes"));
let canonical_id = id(65);
let parent_canonical_id = id(97);
let execution_state_root = id(129);
let payload_root = id(161);
let validator_set_root = id(193);
let accept = match message[225] {
0x00 => false,
0x01 => true,
// The accept byte is bound into the signature, so a third value is a
// message no signer produced. Refuse it rather than coerce it to true.
_ => return Err(VoteError::Wire),
};
Ok((
Position {
chain_id,
height,
round,
block_id: EMPTY,
parent_id: EMPTY,
canonical_id,
parent_canonical_id,
execution_state_root,
payload_root,
validator_set_root,
},
accept,
))
}
/// The one place a validator may make one statement.
///
/// A chain, a height, and a round. Two messages that share a slot and differ in
/// any other byte are the same validator saying two things about one point in
/// the chain — an equivocation, which is what a slashing rule is for and what a
/// fork is made of. Naming the slot is what lets a signer refuse to be the
/// second one.
///
/// It is derived from the SIGNED bytes, never from the envelope: the transport
/// identity is not signed, so two wrappings of one inner block share a slot, as
/// they must.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Slot {
pub chain: Id,
pub height: u64,
pub round: u32,
}
impl Slot {
/// The slot a position falls in.
pub fn of(position: &Position) -> Slot {
Slot {
chain: position.chain_id,
height: position.height,
round: position.round,
}
}
/// The slot a signed message falls in, read out of the message itself.
///
/// Refuses anything that is not this network's signed bytes, so a slot can
/// never be taken from a message the tag, version or role disowns.
pub fn read(message: &[u8]) -> Result<Slot, VoteError> {
read_message(message).map(|(position, _)| Slot::of(&position))
}
}
/// How a node disseminates its own vote.
///
/// Consensus states the seam and never opens a socket: a node carries this over
/// a validator mesh, and a test carries it over a channel. An implementation may
/// echo the vote back to its origin — [`Tally`] deduplicates by signer, so a
/// self-echo costs nothing and is how an originator's own vote reaches its own
/// tally.
pub trait VoteTransport {
fn broadcast(&self, vote: &SignedVote);
}
/// Votes for one position at one epoch, and the certificate they add up to.
///
/// A vote is HELD when, and only when, it is an accept, it is over THIS tally's
/// position, and its signature verifies under the key the set resolves for its
/// signer. Everything else is refused with a reason.
///
/// Holding is not accepting. Whether the held votes are a quorum is decided by
/// [`QuorumCert::verify_weighted`] against the live set, inside [`Tally::cert`],
/// which is the only way out — see the module note.
///
/// A TALLY STATES NO THRESHOLD. There is no field for one and no argument that
/// sets one: [`Tally::cert`] reads [`signer_floor`] over the set as it stands
/// when the certificate is issued, and that number is what the certificate
/// carries. So a caller cannot ask for a lower bar — and cannot pin a higher one
/// either, which matters as much: a floor fixed when the tally opened is a floor
/// from a set that may no longer exist, and it would strand a tally holding a
/// real quorum of the set that does.
///
/// ONE TALLY, ONE EPOCH. The epoch height is fixed when the tally opens and both
/// the signature check and the stake read use it. A tally that verified under
/// one epoch's keys and weighed under another's would be checking a signature
/// against one set and counting it in another, which is the disagreement
/// [`StakeSource`] exists to prevent.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tally {
position: Position,
tier: Finality,
epoch_height: u64,
message: Vec<u8>,
votes: BTreeMap<NodeId, Vec<u8>>,
}
impl Tally {
/// A tally for one position at one rung, at one epoch.
///
/// A rung that is not Nova or Quasar is not a rung a certificate can claim,
/// and is refused here so no later step has to, with the clause
/// [`QuorumCert::verify`] would have refused it with. It is the only thing
/// left to refuse: the quorum is the set's, and it is read when the
/// certificate is issued rather than promised now.
pub fn new(position: Position, tier: Finality, epoch_height: u64) -> Result<Self, VoteError> {
if tier != Finality::Nova && tier != Finality::Quasar {
return Err(CertError::UnknownTier(tier).into());
}
let message = canonical_vote_message(&position, true);
Ok(Tally {
position,
tier,
epoch_height,
message,
votes: BTreeMap::new(),
})
}
/// The bytes a validator must sign to be counted here.
pub fn message(&self) -> &[u8] {
&self.message
}
pub fn position(&self) -> &Position {
&self.position
}
pub fn tier(&self) -> Finality {
self.tier
}
/// The epoch every signature here was checked under, and the epoch its
/// certificate is weighed at.
pub fn epoch_height(&self) -> u64 {
self.epoch_height
}
/// Distinct signers held so far.
pub fn len(&self) -> usize {
self.votes.len()
}
pub fn is_empty(&self) -> bool {
self.votes.is_empty()
}
/// Hold one vote. `Ok(true)` when it was newly recorded, `Ok(false)` when
/// that signer was already held — the first signature from a signer stands,
/// so an equivocating peer cannot displace what it already said.
///
/// The already-held test runs BEFORE the signature check, because the check
/// is a BLS pairing and the test is a map lookup. A vote arriving off a mesh
/// is replayed and echoed as a matter of course, and a peer that could make
/// every repeat cost a pairing would have found a way to spend this node's
/// CPU by sending the same frame twice.
pub fn add(
&mut self,
vote: &SignedVote,
verifier: &dyn VoteVerifier,
) -> Result<bool, VoteError> {
if !vote.accept {
return Err(VoteError::NotAccept);
}
// Comparing the SIGNED BYTES, not the struct: what the signer committed
// to is exactly this message, and any position that produces it is the
// same statement.
if vote.message() != self.message {
return Err(VoteError::Position);
}
// Held already, so nothing this frame carries can change the tally: the
// first signature stands whatever the second one is. Deciding that here
// is what keeps a replay off the pairing.
if self.votes.contains_key(&vote.node) {
return Ok(false);
}
if !verifier.verify_vote(
&vote.node,
&self.message,
&vote.signature,
self.epoch_height,
) {
return Err(VoteError::Signature);
}
self.votes.insert(vote.node, vote.signature.clone());
Ok(true)
}
/// The certificate these votes make — or the clause that refuses it.
///
/// The certificate's threshold is DERIVED here and now — [`signer_floor`]
/// for this rung over the set this source reports at this epoch — and it is
/// read at the moment of issue, not when the tally opened. That is the whole
/// of a certificate's authority in seats, and it is the set's number: there
/// is no argument by which a caller states it, so there is nothing for the
/// set to disagree with. Reading it late is what keeps a tally live across a
/// set that moves under it; a number pinned at open would be a claim about a
/// set that has since changed size, and every party checking the certificate
/// re-derives it from the set they see.
///
/// The whole predicate then runs: [`QuorumCert::verify_weighted`] over the
/// live set at this tally's epoch, which is [`QuorumCert::verify`] plus the
/// rung's floors — for Quasar the stake supermajority, the distinct-signer
/// count and [`crate::finality::MIN_BFT_COMMITTEE`]; for Nova the stake
/// majority and `nova_signer_floor`. Every one of them is recomputed from
/// the set, so a tally can no more declare its own quorum than a certificate
/// can.
///
/// Signatures are checked twice — once as each vote is held, once as the
/// certificate is verified — and that is the point. The certificate a caller
/// receives has passed the same check any other party will run on it, so a
/// tally cannot issue one that only it believes.
pub fn cert(
&self,
verifier: &dyn VoteVerifier,
stake: &dyn StakeSource,
) -> Result<QuorumCert, VoteError> {
let votes: Vec<Vote> = self
.votes
.iter()
.map(|(node, signature)| Vote {
node_id: *node,
accept: true,
signature: signature.clone(),
})
.collect();
let n = stake.signer_count(self.epoch_height);
let derived = signer_floor(self.tier, n);
// The floor is a count of seats and the certificate states it in a `u32`.
// A set claiming more signers than that can hold is not a set this tally
// holds a quorum of, and saying so in the clause that names seats is
// truer than truncating the number the certificate would carry.
let threshold = u32::try_from(derived).map_err(|_| CertError::SignerFloor {
have: self.votes.len() as i64,
need: derived,
n,
})?;
let cert = QuorumCert::assemble(self.tier, self.position.clone(), threshold, &votes)?;
cert.verify_weighted(verifier, stake, self.epoch_height)?;
Ok(cert)
}
}
+376
View File
@@ -0,0 +1,376 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//! The ZAP transport frame, in Rust.
//!
//! This is layer A only — how a message is put on a stream and taken off it.
//! Nothing here knows what a vote is, and nothing here knows what a chain is.
//!
//! It is the same wire the other two lanes already speak. Go writes it in
//! `luxfi/api/zap` (`transport.go`: `WriteMessage`/`ReadMessage`, `Buffer`,
//! `Reader`); C++ reads and writes it in `luxcpp/zap`
//! (`include/lux/zap/wire.hpp`), which names the Go file as its own reference.
//! Rust had no such reader, so the Rust lane could not carry a consensus vote
//! between validators at all. One frame:
//!
//! ```text
//! [4-byte BE length][1-byte type][`length` bytes of payload]
//! ```
//!
//! Big-endian everywhere, because that is what Go writes. A field written with
//! [`Writer::bytes`] is a 4-byte BE length followed by the raw bytes, matching
//! Go's `Buffer.WriteBytes` and C++'s `Writer::write_bytes` byte for byte.
//!
//! WHY NOT `hanzo-zap`: that crate speaks a different protocol that shares the
//! name — the arena message format of `luxfi/zap` (magic `ZAP\0`, a 16-byte
//! header, relative-offset slots) framed by a **little-endian** length and no
//! type byte. It is not this wire, and a vote written with it is not a frame
//! any Go or C++ peer can read.
use std::io::{self, Read, Write};
/// Set on every response frame (bit 7).
pub const RESPONSE_FLAG: u8 = 0x80;
/// Set on a response frame that carries an application error (bit 6).
pub const ERROR_FLAG: u8 = 0x40;
/// The bits a service-defined type id may use.
///
/// A type id with bit 6 or 7 set arrives at the far end looking like a flag, so
/// the id space is the low six bits and nothing above them.
pub const TYPE_MASK: u8 = 0x3F;
/// The largest payload a frame may announce: 16 MiB, as Go and C++ both cap it.
pub const MAX_MESSAGE: u32 = 16 * 1024 * 1024;
/// Length prefix plus type byte.
pub const HEADER_LEN: usize = 5;
/// The base type id, with the response and error flags removed.
pub const fn strip_flags(raw: u8) -> u8 {
raw & TYPE_MASK
}
/// Whether the response flag is set.
pub const fn is_response(raw: u8) -> bool {
raw & RESPONSE_FLAG != 0
}
/// Whether the error flag is set.
pub const fn is_error(raw: u8) -> bool {
raw & ERROR_FLAG != 0
}
/// Big-endian encoder for a frame payload.
#[derive(Debug, Default, Clone)]
pub struct Writer {
buf: Vec<u8>,
}
impl Writer {
pub fn new() -> Self {
Writer { buf: Vec::new() }
}
pub fn with_capacity(n: usize) -> Self {
Writer {
buf: Vec::with_capacity(n),
}
}
pub fn u8(&mut self, v: u8) -> &mut Self {
self.buf.push(v);
self
}
pub fn u32(&mut self, v: u32) -> &mut Self {
self.buf.extend_from_slice(&v.to_be_bytes());
self
}
pub fn u64(&mut self, v: u64) -> &mut Self {
self.buf.extend_from_slice(&v.to_be_bytes());
self
}
/// A length-prefixed field: 4-byte BE length, then the bytes.
pub fn bytes(&mut self, b: &[u8]) -> &mut Self {
self.u32(b.len() as u32);
self.buf.extend_from_slice(b);
self
}
pub fn take(self) -> Vec<u8> {
self.buf
}
pub fn as_slice(&self) -> &[u8] {
&self.buf
}
}
/// Big-endian decoder over a borrowed payload.
///
/// Every read is checked and returns `None` on a short buffer; there is no
/// panicking accessor, so a hostile payload cannot take the process down.
#[derive(Debug, Clone)]
pub struct Reader<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
pub fn new(data: &'a [u8]) -> Self {
Reader { data, pos: 0 }
}
pub fn remaining(&self) -> usize {
self.data.len() - self.pos
}
pub fn u8(&mut self) -> Option<u8> {
let v = *self.data.get(self.pos)?;
self.pos += 1;
Some(v)
}
pub fn u32(&mut self) -> Option<u32> {
let s: [u8; 4] = self.data.get(self.pos..self.pos + 4)?.try_into().ok()?;
self.pos += 4;
Some(u32::from_be_bytes(s))
}
pub fn u64(&mut self) -> Option<u64> {
let s: [u8; 8] = self.data.get(self.pos..self.pos + 8)?.try_into().ok()?;
self.pos += 8;
Some(u64::from_be_bytes(s))
}
/// A length-prefixed field, borrowed out of the payload.
///
/// The length is checked against what is left before anything is taken, so
/// an announced length larger than the payload is a refusal and never an
/// allocation.
///
/// The end of the field is computed with `checked_add` because the length is
/// a peer's 32-bit number and the position is ours. Where `usize` is 32 bits
/// the sum of the two can overflow, and an overflow is a PANIC wherever
/// overflow checks are on — so a peer that can announce a length can halt
/// the process, which is the attack.
///
/// It is NOT an over-read. A wrapped end lands BELOW the cursor, and a range
/// whose start is past its end is one `get` answers `None` to, so the
/// unchecked version refuses the same bytes wherever it survives them at
/// all. The fix buys the refusal on a target where the alternative is
/// dying — the width of a pointer is not a thing a wire format gets to
/// depend on.
pub fn bytes(&mut self) -> Option<&'a [u8]> {
let n = self.u32()? as usize;
let end = self.pos.checked_add(n)?;
let s = self.data.get(self.pos..end)?;
self.pos = end;
Some(s)
}
}
/// Write one frame: header then payload, in a single call to the stream.
///
/// One write, not two, because two writes on a shared stream can interleave with
/// another frame's header and desynchronise the reader for good.
pub fn write_frame<W: Write>(w: &mut W, msg_type: u8, payload: &[u8]) -> io::Result<()> {
if payload.len() as u64 > MAX_MESSAGE as u64 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"zap: frame too large",
));
}
let mut frame = Vec::with_capacity(HEADER_LEN + payload.len());
frame.extend_from_slice(&(payload.len() as u32).to_be_bytes());
frame.push(msg_type);
frame.extend_from_slice(payload);
w.write_all(&frame)
}
/// Read one frame, refusing an announced length above `max`.
///
/// `max` is the caller's statement of what this link carries. A link that only
/// ever carries a vote says so, and a peer that announces 16 MiB on it is
/// refused before a byte is allocated.
pub fn read_frame<R: Read>(r: &mut R, max: u32) -> io::Result<(u8, Vec<u8>)> {
let mut header = [0u8; HEADER_LEN];
r.read_exact(&mut header)?;
let len = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
if len > max.min(MAX_MESSAGE) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"zap: frame too large",
));
}
let mut payload = vec![0u8; len as usize];
r.read_exact(&mut payload)?;
Ok((header[4], payload))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_frame_is_a_be_length_then_a_type_then_the_payload() {
let mut out = Vec::new();
write_frame(&mut out, 0x2a, &[1, 2, 3]).expect("write");
assert_eq!(out, vec![0, 0, 0, 3, 0x2a, 1, 2, 3]);
}
#[test]
fn a_written_frame_reads_back() {
let mut out = Vec::new();
write_frame(&mut out, 62, b"hello").expect("write");
let (t, p) = read_frame(&mut out.as_slice(), 1024).expect("read");
assert_eq!(t, 62);
assert_eq!(p, b"hello");
}
#[test]
fn a_field_is_a_be_length_then_the_bytes() {
let mut w = Writer::new();
w.bytes(&[0xaa, 0xbb]);
assert_eq!(w.as_slice(), &[0, 0, 0, 2, 0xaa, 0xbb]);
}
#[test]
fn a_field_round_trips() {
let mut w = Writer::new();
w.bytes(b"one").bytes(b"").bytes(b"three");
let buf = w.take();
let mut r = Reader::new(&buf);
assert_eq!(r.bytes(), Some(&b"one"[..]));
assert_eq!(r.bytes(), Some(&b""[..]));
assert_eq!(r.bytes(), Some(&b"three"[..]));
assert_eq!(r.remaining(), 0);
}
#[test]
fn an_over_long_length_is_refused_not_allocated() {
// A four-gigabyte announcement over a two-byte payload.
let hostile = [0xffu8, 0xff, 0xff, 0xff, 0x01, 0x02];
let mut r = Reader::new(&hostile);
assert_eq!(r.bytes(), None);
}
/// A length near `u32::MAX` over a five-byte payload is refused.
///
/// On a 64-bit target — which is every target this suite runs on — that is
/// the whole of what this can say: the sum cannot overflow there, so what is
/// held is the bounds check and not the `checked_add`, and dropping the
/// `checked_add` leaves this green. The overflow it guards is a 32-bit
/// target's, no such target is in CI, and this test is the one that would
/// carry the property the day one is.
#[test]
fn a_length_that_would_wrap_the_cursor_is_refused() {
for announced in [u32::MAX, u32::MAX - 1, 1 << 31, 1 << 24] {
let mut hostile = announced.to_be_bytes().to_vec();
hostile.extend_from_slice(b"short");
let mut r = Reader::new(&hostile);
assert_eq!(r.bytes(), None, "announced {announced}");
// And a refusal leaves the cursor where a caller can still see the
// remaining bytes, rather than past the end of the buffer.
assert!(r.remaining() <= hostile.len());
}
}
#[test]
fn a_frame_over_the_links_limit_is_refused() {
let mut framed = Vec::new();
write_frame(&mut framed, 62, &vec![0u8; 300]).expect("write");
let err = read_frame(&mut framed.as_slice(), 188).expect_err("must refuse");
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn a_truncated_frame_is_an_error_not_a_short_payload() {
let framed = [0u8, 0, 0, 8, 62, 1, 2, 3];
let err = read_frame(&mut framed.as_slice(), 1024).expect_err("must refuse");
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
}
/// The frame's constants are not this crate's to choose. Every one of them
/// is read off `luxfi/api/zap` (`wire.go`) and matched by `luxcpp/zap`
/// (`include/lux/zap/wire.hpp`); a frame written under any other value is
/// one no Go or C++ peer can read. Pinning them here means a change to one
/// of them stops being a silent fork of the wire.
#[test]
fn the_constants_are_the_ones_go_writes_and_cpp_reads() {
// Go: MsgResponseFlag = 0x80, MsgErrorFlag = 0x40.
// C++: MsgResponseFlag = 0x80, MsgErrorFlag = 0x40.
assert_eq!(RESPONSE_FLAG, 0x80);
assert_eq!(ERROR_FLAG, 0x40);
// C++: MsgTypeMask = 0x3F. Go has no such constant and open-codes the
// strip; `strip_flags_is_gos_own_expression` below is what holds them
// together.
assert_eq!(TYPE_MASK, 0x3F);
// Go: MaxMessageSize = 16 * 1024 * 1024. C++: MaxMessageSize, same.
assert_eq!(MAX_MESSAGE, 16 * 1024 * 1024);
// Go: HeaderSize = 5. C++: HeaderSize = 5.
assert_eq!(HEADER_LEN, 5);
}
/// Go names no mask: its call sites clear the two flag bits with `&^`, as in
/// `respType &^ (MsgResponseFlag | MsgErrorFlag)`. Masking with `TYPE_MASK`
/// is the same operation given a name, and this is the proof — over every
/// byte, not over a sample.
#[test]
fn strip_flags_is_gos_own_expression() {
for raw in 0..=u8::MAX {
assert_eq!(
strip_flags(raw),
raw & !(RESPONSE_FLAG | ERROR_FLAG),
"raw {raw}"
);
}
}
/// The header Go builds is `BigEndian.PutUint32(header[0:4], len(payload))`
/// then `header[4] = byte(msgType)`. Written out, so an endianness that ever
/// flipped would be caught by the bytes and not by a peer.
#[test]
fn the_header_is_big_endian_length_then_type() {
let mut out = Vec::new();
write_frame(&mut out, VOTE_LIKE, &[0xde, 0xad]).expect("write");
assert_eq!(
&out[..4],
&[0x00, 0x00, 0x00, 0x02],
"length is 4-byte big-endian"
);
assert_eq!(out[4], VOTE_LIKE, "then the type byte");
assert_eq!(&out[5..], &[0xde, 0xad]);
// A length that needs more than one byte, so a little-endian writer
// could not pass by accident.
let mut out = Vec::new();
write_frame(&mut out, VOTE_LIKE, &vec![0u8; 258]).expect("write");
assert_eq!(&out[..4], &[0x00, 0x00, 0x01, 0x02]);
}
/// `Buffer.WriteBytes` in Go writes a 4-byte big-endian length and then the
/// raw bytes; `Writer::write_bytes` in C++ writes the same. A field is the
/// same shape as a frame's length prefix and must not drift from it.
#[test]
fn a_field_is_big_endian_too() {
let mut w = Writer::new();
w.bytes(&vec![0u8; 300]);
assert_eq!(&w.as_slice()[..4], &[0x00, 0x00, 0x01, 0x2c]);
}
/// A service id fit for this wire: the 60..63 block Go reserves for Quasar.
const VOTE_LIKE: u8 = 62;
#[test]
fn flags_ride_above_the_id_space() {
assert_eq!(strip_flags(62 | RESPONSE_FLAG | ERROR_FLAG), 62);
assert!(is_response(62 | RESPONSE_FLAG));
assert!(is_error(62 | ERROR_FLAG));
assert!(!is_response(62));
}
}
+946
View File
@@ -0,0 +1,946 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
//! The vote plane: the frame, the slot, and the tally.
//!
//! Two questions are asked here and they are not the same question.
//!
//! The first is whether a vote survives the wire unchanged — whether the bytes
//! a validator signed are the bytes a receiver checks, and whether every
//! near-miss frame is refused rather than repaired. `SignedVote` carries the
//! signed message itself, so this is answered by round-trip and by mutation.
//!
//! The second is whether a tally can hand out a certificate the network would
//! not accept. It must not be able to. A tally holds votes; it states no accept
//! rule; the rule is `cert`'s, and [`Tally::cert`] is the only door out of a
//! tally and runs the whole of `verify_weighted` before it returns. The tests
//! from `a_tally_derives_its_quorum_from_the_set` down are that property, one
//! floor at a time: the stake supermajority, the distinct-signer count, the
//! minimum Byzantine committee, and — through registration — the proof of
//! possession that decides who may be counted at all.
use blst::min_pk::SecretKey;
use lux_consensus::cert::{
CertError, NodeId, QuorumCert, StakeSource, ValidatorSet, VoteVerifier, DST,
};
use lux_consensus::finality::{
canonical_vote_message, half_stake_floor, nova_signer_floor, signer_floor, two_thirds_count,
two_thirds_stake_floor, Finality, Position, EMPTY, VOTE_MESSAGE_LEN,
};
use lux_consensus::pop::{self, NODE_LEN};
use lux_consensus::vote::{
read_message, SignedVote, Slot, Tally, VoteError, VoteTransport, VOTE, VOTE_PAYLOAD_LEN,
};
use lux_consensus::zap;
/// The epoch these tests register and vote at. One value, so a test that meant
/// to change it has to say so.
const EPOCH: u64 = 9;
/// A validator: a key, an id, and a weight.
struct Signer {
id: NodeId,
sk: SecretKey,
weight: u64,
}
impl Signer {
/// Deterministic per `n`, so a failure reproduces exactly.
fn new(n: u8, weight: u64) -> Self {
let sk = SecretKey::key_gen(&[n; 32], &[]).expect("key_gen");
let mut id = [0u8; NODE_LEN];
id[0] = n;
Signer { id, sk, weight }
}
fn public(&self) -> [u8; 48] {
self.sk.sk_to_pk().compress()
}
/// The proof of possession this validator presents at registration.
fn pop(&self) -> Vec<u8> {
pop::sign(&self.sk, &self.id, &self.public())
}
/// A signed statement about `pos`. The signature is over the canonical
/// message, which is the only thing a verifier ever checks.
fn vote(&self, pos: &Position, accept: bool) -> SignedVote {
let message = canonical_vote_message(pos, accept);
SignedVote {
position: pos.clone(),
accept,
node: self.id,
signature: self.sk.sign(&message, DST, &[]).compress().to_vec(),
}
}
}
/// A committee of `n` equal-weight validators, admitted through the proof path.
fn committee(n: u8, weight: u64) -> (Vec<Signer>, ValidatorSet) {
let signers: Vec<Signer> = (1..=n).map(|i| Signer::new(i, weight)).collect();
let mut set = ValidatorSet::new();
for s in &signers {
set.insert(s.id, s.weight, &s.public(), &s.pop())
.expect("insert");
}
(signers, set)
}
/// A position with every slot populated, so the canonical-degrade path is not
/// what is under test.
fn position() -> Position {
Position {
chain_id: [0xc1; 32],
height: 42,
round: 7,
block_id: [0xb1; 32],
parent_id: [0xb2; 32],
canonical_id: [0xca; 32],
parent_canonical_id: [0xcb; 32],
execution_state_root: [0xe5; 32],
payload_root: [0x70; 32],
validator_set_root: [0x55; 32],
}
}
/// A tally at [`EPOCH`], for the rung under test. There is nothing else to
/// state: the quorum is the set's, read when a certificate is asked for.
fn tally(tier: Finality) -> Tally {
Tally::new(position(), tier, EPOCH).expect("a tally opens")
}
// ── The frame ───────────────────────────────────────────────────────
#[test]
fn a_vote_frame_round_trips() {
let (s, _) = committee(1, 1);
let v = s[0].vote(&position(), true);
let payload = v.encode();
assert_eq!(payload.len(), VOTE_PAYLOAD_LEN);
let back = SignedVote::decode(&payload).expect("decode");
// The transport ids are not signed, so they do not survive — and the signed
// bytes are identical either way, which is the property that matters.
assert_eq!(back.message(), v.message());
assert_eq!(back.node, v.node);
assert_eq!(back.signature, v.signature);
assert!(back.accept);
assert_eq!(back.encode(), payload);
assert_eq!(
back.position.block_id, EMPTY,
"the envelope is not in the signature"
);
}
#[test]
fn a_position_with_no_inner_outer_split_round_trips_too() {
// The degrade case: canonical slots unset, so the message binds the
// transport ids. Re-encoding must produce the same bytes.
let mut pos = position();
pos.canonical_id = EMPTY;
pos.parent_canonical_id = EMPTY;
let (s, _) = committee(1, 1);
let v = s[0].vote(&pos, true);
let back = SignedVote::decode(&v.encode()).expect("decode");
assert_eq!(back.message(), v.message());
assert_eq!(back.encode(), v.encode());
}
#[test]
fn trailing_bytes_are_not_a_vote() {
let (s, _) = committee(1, 1);
let mut payload = s[0].vote(&position(), true).encode();
payload.push(0);
assert_eq!(SignedVote::decode(&payload), Err(VoteError::Wire));
}
#[test]
fn a_wrong_width_field_is_not_a_vote() {
let (s, _) = committee(1, 1);
let v = s[0].vote(&position(), true);
// A 19-byte node id.
let mut w = zap::Writer::new();
w.bytes(&v.message())
.bytes(&v.node[..NODE_LEN - 1])
.bytes(&v.signature);
assert_eq!(SignedVote::decode(&w.take()), Err(VoteError::Wire));
// A 95-byte signature.
let mut w = zap::Writer::new();
w.bytes(&v.message())
.bytes(&v.node)
.bytes(&v.signature[..95]);
assert_eq!(SignedVote::decode(&w.take()), Err(VoteError::Wire));
}
#[test]
fn a_truncated_payload_is_not_a_vote() {
let (s, _) = committee(1, 1);
let payload = s[0].vote(&position(), true).encode();
for cut in [0, 1, 4, 100, VOTE_PAYLOAD_LEN - 1] {
assert_eq!(
SignedVote::decode(&payload[..cut]),
Err(VoteError::Wire),
"cut {cut}"
);
}
}
#[test]
fn a_foreign_tag_is_not_this_networks_vote() {
let (s, _) = committee(1, 1);
let v = s[0].vote(&position(), true);
let mut message = v.message();
message[0] = b'X';
let mut w = zap::Writer::new();
w.bytes(&message).bytes(&v.node).bytes(&v.signature);
assert_eq!(SignedVote::decode(&w.take()), Err(VoteError::Wire));
}
#[test]
fn a_third_accept_value_is_refused() {
let (s, _) = committee(1, 1);
let v = s[0].vote(&position(), true);
let mut message = v.message();
*message.last_mut().expect("accept byte") = 0x02;
let mut w = zap::Writer::new();
w.bytes(&message).bytes(&v.node).bytes(&v.signature);
assert_eq!(SignedVote::decode(&w.take()), Err(VoteError::Wire));
}
/// A refusal names the clause. The version and the role live in the signed
/// bytes, so a message from another certificate version is refused as THAT, and
/// with the same values `cert` would have refused it with — one vocabulary
/// across both modules.
#[test]
fn a_foreign_version_and_a_foreign_role_are_named() {
let base = canonical_vote_message(&position(), true);
let mut wrong_version = base.clone();
wrong_version[18] = 0;
wrong_version[19] = 99;
assert_eq!(
read_message(&wrong_version),
Err(VoteError::Cert(CertError::Version { got: 99, want: 3 })),
);
let mut wrong_role = base;
wrong_role[20] = 7;
assert_eq!(
read_message(&wrong_role),
Err(VoteError::Cert(CertError::Type { got: 7, want: 1 })),
);
}
#[test]
fn the_message_is_the_length_the_standard_fixes() {
assert_eq!(
canonical_vote_message(&position(), true).len(),
VOTE_MESSAGE_LEN
);
assert_eq!(read_message(&[]), Err(VoteError::Wire));
assert_eq!(
read_message(&[0u8; VOTE_MESSAGE_LEN - 1]),
Err(VoteError::Wire)
);
}
#[test]
fn the_vote_type_id_fits_the_id_space() {
// A type id with bit 6 or 7 set arrives at the far end looking like a flag,
// so stripping the flags must leave the id untouched.
assert_eq!(
zap::strip_flags(VOTE),
VOTE,
"a type id must not collide with a flag"
);
}
// ── The slot ────────────────────────────────────────────────────────
#[test]
fn a_slot_is_the_chain_the_height_and_the_round() {
let pos = Position {
chain_id: [7u8; 32],
height: 42,
round: 3,
..Default::default()
};
let slot = Slot::of(&pos);
assert_eq!(
slot,
Slot {
chain: [7u8; 32],
height: 42,
round: 3
}
);
// And it is readable out of the signed bytes, which is where a receiver gets
// it from.
assert_eq!(
Slot::read(&canonical_vote_message(&pos, true)).unwrap(),
slot
);
}
#[test]
fn accepting_and_rejecting_share_one_slot() {
// Two statements a validator must not both make: same point in the chain,
// opposite answers.
let pos = Position {
chain_id: [9u8; 32],
height: 1,
round: 0,
..Default::default()
};
let yes = canonical_vote_message(&pos, true);
let no = canonical_vote_message(&pos, false);
assert_ne!(yes, no, "the accept bit is signed");
assert_eq!(Slot::read(&yes).unwrap(), Slot::read(&no).unwrap());
}
#[test]
fn two_wrappings_of_one_block_share_one_slot() {
// The transport identity is not signed, so a block wrapped twice is one
// statement, not two.
let inner = Position {
chain_id: [4u8; 32],
height: 8,
round: 1,
canonical_id: [5u8; 32],
block_id: [0xAA; 32],
..Default::default()
};
let other = Position {
block_id: [0xBB; 32],
..inner.clone()
};
assert_eq!(Slot::of(&inner), Slot::of(&other));
assert_eq!(
canonical_vote_message(&inner, true),
canonical_vote_message(&other, true)
);
}
#[test]
fn a_message_this_network_disowns_has_no_slot() {
assert_eq!(Slot::read(&[]), Err(VoteError::Wire));
let mut message = canonical_vote_message(&Position::default(), true);
message[0] ^= 0xFF; // the tag
assert_eq!(Slot::read(&message), Err(VoteError::Wire));
}
// ── What a tally will hold ──────────────────────────────────────────
/// A tally opens at a rung a certificate can claim, or it does not open. That is
/// the whole of what opening one can refuse — there is no threshold to state, so
/// a quorum of nobody is not something a caller can ask for in the first place.
#[test]
fn a_tally_opens_only_at_an_accept_rung() {
assert_eq!(
Tally::new(position(), Finality::Wave, EPOCH),
Err(VoteError::Cert(CertError::UnknownTier(Finality::Wave))),
);
Tally::new(position(), Finality::Nova, EPOCH).expect("nova opens");
Tally::new(position(), Finality::Quasar, EPOCH).expect("quasar opens");
}
#[test]
fn one_signer_counts_once() {
let (s, set) = committee(2, 1);
let mut t = tally(Finality::Quasar);
let v = s[0].vote(&position(), true);
assert!(t.add(&v, &set).expect("first"));
assert!(
!t.add(&v, &set).expect("second"),
"a repeat is not a second signer"
);
assert_eq!(t.len(), 1);
}
#[test]
fn a_reject_is_not_a_finality_vote() {
let (s, set) = committee(1, 1);
let mut t = tally(Finality::Quasar);
assert_eq!(
t.add(&s[0].vote(&position(), false), &set),
Err(VoteError::NotAccept)
);
}
#[test]
fn a_vote_for_another_position_does_not_count_here() {
let (s, set) = committee(1, 1);
let mut other = position();
other.height += 1;
let mut t = tally(Finality::Quasar);
assert_eq!(
t.add(&s[0].vote(&other, true), &set),
Err(VoteError::Position)
);
}
#[test]
fn a_forged_signature_is_refused() {
let (s, set) = committee(2, 1);
// Node 0's identity carrying node 1's signature.
let mut v = s[1].vote(&position(), true);
v.node = s[0].id;
let mut t = tally(Finality::Quasar);
assert_eq!(t.add(&v, &set), Err(VoteError::Signature));
}
/// A real key over the real message, from a node the set never admitted. There
/// is nothing wrong with the signature; there is no one it stands for.
#[test]
fn a_signer_the_set_never_admitted_is_refused() {
let (_, set) = committee(1, 1);
let stranger = Signer::new(0xff, 1);
let mut t = tally(Finality::Quasar);
assert_eq!(
t.add(&stranger.vote(&position(), true), &set),
Err(VoteError::Signature)
);
assert!(t.is_empty());
}
/// A member the chain carries without a key. It counts toward membership and
/// can never put a vote behind its stake — the fail-closed direction, held
/// through the vote plane and not only at the certificate.
#[test]
fn a_keyless_member_cannot_be_counted() {
let (s, mut set) = committee(4, 1);
let spectator = Signer::new(0x40, 1_000_000);
set.insert_unkeyed(spectator.id, spectator.weight)
.expect("a keyless member is a member");
let mut t = tally(Finality::Quasar);
assert_eq!(
t.add(&spectator.vote(&position(), true), &set),
Err(VoteError::Signature)
);
assert!(t
.add(&s[0].vote(&position(), true), &set)
.expect("a keyed member is held"));
assert_eq!(t.len(), 1);
}
// ── What a tally will NOT certify ───────────────────────────────────
/// THE PROPERTY THIS MODULE EXISTS FOR.
///
/// A tally is handed one perfectly valid signature over a set of seven, and its
/// holder has no way to ask for a quorum of one — there is no argument for it.
/// The number the certificate would carry is `signer_floor` over the set, five
/// of seven, so the lone vote is refused by a floor the caller had no part in.
#[test]
fn a_tally_derives_its_quorum_from_the_set() {
let (s, set) = committee(7, 1);
let mut t = tally(Finality::Quasar);
assert!(t.add(&s[0].vote(&position(), true), &set).expect("held"));
assert_eq!(
t.cert(&set, &set),
Err(VoteError::Cert(CertError::BelowThreshold {
have: 1,
need: two_thirds_count(7) as u32,
})),
"one of seven is not the quorum the set derives",
);
// And the number the certificate ends up carrying is that same derived
// floor — the one every party checking it computes from the set it holds,
// rather than a value it reads off the certificate and believes.
for signer in s.iter().skip(1).take(4) {
assert!(t.add(&signer.vote(&position(), true), &set).expect("held"));
}
let cert = t.cert(&set, &set).expect("five of seven exports");
assert_eq!(i64::from(cert.threshold), signer_floor(Finality::Quasar, 7));
assert_eq!(cert.threshold, 5);
}
/// The rung certifies at the set's floor, and the vote before it is not enough.
/// On an equal-weight set the two Quasar floors are one bar in two units, so the
/// last vote crosses both at once.
///
/// There is ONE bar here where there used to be two. A tally carries no
/// declaration beside the set's floor, so there is no arrangement of the two in
/// which the caller's number is the one that decides: the certificate is built
/// to the floor the set derives, or it is not built.
#[test]
fn a_quasar_tally_certifies_at_the_sets_floor_and_not_before() {
let (s, set) = committee(7, 1);
let need = two_thirds_count(7);
assert_eq!(need, 5);
let short = need as usize - 1;
let mut t = tally(Finality::Quasar);
for signer in s.iter().take(short) {
assert!(t.add(&signer.vote(&position(), true), &set).expect("held"));
}
assert_eq!(
t.cert(&set, &set),
Err(VoteError::Cert(CertError::BelowThreshold {
have: 4,
need: 5
})),
"four of seven is not an export quorum, and nothing can ask for less",
);
// The fifth vote crosses both floors at once.
assert!(t
.add(&s[short].vote(&position(), true), &set)
.expect("held"));
let cert = t.cert(&set, &set).expect("five of seven exports");
assert_eq!(cert.votes.len(), 5);
assert_eq!(cert.tier, Finality::Quasar);
assert_eq!(cert.threshold, need as u32);
}
/// The two export floors are independent, and on a lopsided set each binds
/// alone: the whale holds the stake and not the seats, the small holders hold
/// the seats and not the stake. Neither exports. This is why the rule is read in
/// both units — two thirds of the stake is one signature wherever two thirds of
/// the stake is one validator.
#[test]
fn neither_stake_nor_seats_alone_exports() {
let whale = Signer::new(1, 1_000);
let rest: Vec<Signer> = (2..=7).map(|i| Signer::new(i, 1)).collect();
let mut set = ValidatorSet::new();
for s in std::iter::once(&whale).chain(rest.iter()) {
set.insert(s.id, s.weight, &s.public(), &s.pop())
.expect("insert");
}
// Stake without seats. The whale's 1000 of 1006 is past floor(2·1006/3) =
// 670, so the stake clause would pass; one of seven is not the five seats
// the set derives, and the seat floor is what refuses.
assert!(1_000 > two_thirds_stake_floor(1_006));
let mut alone = tally(Finality::Quasar);
assert!(alone
.add(&whale.vote(&position(), true), &set)
.expect("held"));
assert_eq!(
alone.cert(&set, &set),
Err(VoteError::Cert(CertError::BelowThreshold {
have: 1,
need: two_thirds_count(7) as u32,
})),
"the stake was there and the seats were not",
);
// Seats without stake. Five of the seven signers is the seat floor exactly,
// so the certificate is built — and the stake clause refuses it on five
// units of 1006.
let mut small = tally(Finality::Quasar);
for s in rest.iter().take(5) {
assert!(small.add(&s.vote(&position(), true), &set).expect("held"));
}
assert_eq!(
small.cert(&set, &set),
Err(VoteError::Cert(CertError::StakeBelowSupermajority {
voted: 5,
signer: 1_006,
need_above: two_thirds_stake_floor(1_006),
})),
"the seats were there and the stake was not",
);
}
/// A DERIVED FLOOR IS THE LIVE ONE.
///
/// A floor read when the tally opened is a claim about a set that may no longer
/// exist, and a set that SHRINKS strands a tally holding it: the pinned number
/// counts validators who have gone. Nothing is pinned, so the same held votes
/// are weighed against whichever set is live when the certificate is asked for —
/// short of the ten's floor, a supermajority of the seven's.
#[test]
fn a_set_that_moves_under_a_tally_does_not_strand_it() {
let signers: Vec<Signer> = (1..=10).map(|i| Signer::new(i, 1)).collect();
let admit = |take: usize| -> ValidatorSet {
let mut set = ValidatorSet::new();
for s in signers.iter().take(take) {
set.insert(s.id, s.weight, &s.public(), &s.pop())
.expect("insert");
}
set
};
let wide = admit(10);
let narrow = admit(7); // three keys gone at the same epoch
let mut t = tally(Finality::Quasar);
for s in signers.iter().take(6) {
assert!(t.add(&s.vote(&position(), true), &wide).expect("held"));
}
// Against the ten, six seats are short of the seven that set derives.
assert_eq!(two_thirds_count(10), 7);
assert_eq!(
t.cert(&wide, &wide),
Err(VoteError::Cert(CertError::BelowThreshold {
have: 6,
need: 7
})),
);
// Against the seven, the same six votes are a supermajority, and the
// certificate carries the floor the seven derive rather than the ten's.
let cert = t.cert(&narrow, &narrow).expect("six of seven exports");
assert_eq!(i64::from(cert.threshold), signer_floor(Finality::Quasar, 7));
assert_eq!(cert.threshold, 5);
cert.verify_weighted(&narrow, &narrow, EPOCH)
.expect("and it verifies for anyone holding that set");
}
/// A SOURCE THAT CANNOT BE ANSWERED IS REFUSED, not rounded to one that can.
///
/// The floor is a count of seats and a certificate states it in 32 bits. A
/// source claiming more signers than that can hold has claimed a set no
/// certificate here can speak for; truncating the number would build one naming
/// a quorum nobody derived. It fails closed instead, in the clause that names
/// seats — which is also the honest reading, since four voters are not
/// six quintillion.
#[test]
fn a_signer_count_past_the_certificates_field_is_refused() {
struct Absurd<'a>(&'a ValidatorSet);
impl StakeSource for Absurd<'_> {
fn weight(&self, n: &NodeId, h: u64) -> u64 {
self.0.weight(n, h)
}
fn signer_stake(&self, h: u64) -> u64 {
self.0.signer_stake(h)
}
fn signer_count(&self, _h: u64) -> i64 {
i64::MAX
}
}
let (s, set) = committee(4, 1);
let mut t = tally(Finality::Quasar);
for signer in &s {
assert!(t.add(&signer.vote(&position(), true), &set).expect("held"));
}
assert_eq!(
t.cert(&set, &Absurd(&set)),
Err(VoteError::Cert(CertError::SignerFloor {
have: 4,
need: two_thirds_count(i64::MAX),
n: i64::MAX,
})),
);
}
/// A REPLAY DOES NO PAIRING.
///
/// A vote that arrives twice is decided by the map and not by the curve: the
/// already-held test runs before the signature check. A mesh replays and echoes
/// as a matter of course, so a peer that could make every repeat cost a BLS
/// verification would have found this node's CPU by sending one frame twice.
#[test]
fn a_replayed_vote_is_dropped_before_the_signature_check() {
use std::cell::Cell;
/// A verifier that counts what it was asked to check.
struct Counting<'a> {
inner: &'a ValidatorSet,
pairings: Cell<usize>,
}
impl VoteVerifier for Counting<'_> {
fn verify_vote(&self, n: &NodeId, m: &[u8], sig: &[u8], h: u64) -> bool {
self.pairings.set(self.pairings.get() + 1);
self.inner.verify_vote(n, m, sig, h)
}
}
let (s, set) = committee(4, 1);
let counting = Counting {
inner: &set,
pairings: Cell::new(0),
};
let v = s[0].vote(&position(), true);
let mut t = tally(Finality::Quasar);
assert!(t.add(&v, &counting).expect("first"));
assert_eq!(counting.pairings.get(), 1, "the first vote is verified");
for _ in 0..16 {
assert!(!t.add(&v, &counting).expect("a replay is not an error"));
}
assert_eq!(counting.pairings.get(), 1, "and no copy of it is");
// Including a copy carrying garbage where the signature was: a held signer's
// second frame is dropped on the lookup, so it never reaches the curve.
let mut forged = v.clone();
forged.signature = vec![0u8; 96];
assert!(!t.add(&forged, &counting).expect("still just a replay"));
assert_eq!(counting.pairings.get(), 1);
assert_eq!(t.len(), 1);
// What the tally holds is the FIRST signature, not the last frame — which is
// why dropping the copy unverified loses nothing.
for signer in s.iter().skip(1) {
assert!(t
.add(&signer.vote(&position(), true), &counting)
.expect("held"));
}
let cert = t.cert(&set, &set).expect("four of four exports");
assert_eq!(
cert.votes[0].signature, v.signature,
"the first signature stands"
);
}
/// Below four signers the fault budget f = ⌊(n−1)/3⌋ is zero: every signer is
/// load-bearing and a single compromised key forges the export. A unanimous
/// three-signer set clears both other floors and still cannot export.
#[test]
fn a_tally_cannot_export_below_the_minimum_committee() {
let (s, set) = committee(3, 1);
let mut t = tally(Finality::Quasar);
for signer in &s {
assert!(t.add(&signer.vote(&position(), true), &set).expect("held"));
}
assert_eq!(
t.cert(&set, &set),
Err(VoteError::Cert(CertError::MinCommittee { n: 3, need: 4 })),
"unanimity over three is not a Byzantine supermajority",
);
}
/// Nova is a different rung with different floors, and a tally runs whichever it
/// opened at — it does not carry one rule and label it two.
#[test]
fn a_nova_tally_runs_the_nova_floors() {
let (s, set) = committee(7, 1);
assert_eq!(nova_signer_floor(7), 3);
assert_eq!(half_stake_floor(7), 3);
let mut t = tally(Finality::Nova);
for signer in s.iter().take(3) {
assert!(t.add(&signer.vote(&position(), true), &set).expect("held"));
}
// Three clears the signer floor and ties the stake majority, which must be
// STRICTLY exceeded.
assert_eq!(
t.cert(&set, &set),
Err(VoteError::Cert(CertError::StakeBelowMajority {
voted: 3,
signer: 7,
need_above: 3
})),
);
assert!(t.add(&s[3].vote(&position(), true), &set).expect("held"));
let cert = t.cert(&set, &set).expect("four of seven ignites");
assert_eq!(cert.tier, Finality::Nova);
// And Nova's four is not Quasar's five: the same votes at the export rung
// are refused.
let mut q = tally(Finality::Quasar);
for signer in s.iter().take(4) {
assert!(q.add(&signer.vote(&position(), true), &set).expect("held"));
}
assert!(
q.cert(&set, &set).is_err(),
"the export rung is not the ignition rung"
);
}
/// A rogue key — `g1·x − Σ pk_others`, the key whose holder knows no secret —
/// cannot be registered, so it never becomes a signer and never reaches a tally.
/// The proof of possession is what closes it, and it is closed at the door.
#[test]
fn a_rogue_key_never_reaches_a_tally() {
let (s, mut set) = committee(4, 1);
let rogue_id = {
let mut id = [0u8; NODE_LEN];
id[0] = 0xAA;
id
};
// No secret is known for this key, so no proof over (node, key) can be made;
// any bytes offered as one are refused.
let honest = s[0].public();
assert_eq!(
set.insert(rogue_id, 1, &honest, &[0u8; 96]),
Err(CertError::PopInvalid),
"a proof that is not a signature is not a proof",
);
// Nor can it be smuggled in behind an honest validator's own proof: a proof
// binds the one (node, key) pair it was made for.
assert_eq!(
set.insert(rogue_id, 1, &honest, &s[0].pop()),
Err(CertError::PopInvalid),
"a proof made for another node does not travel",
);
// So the identity resolves to nothing, and a tally holds nothing for it.
let mut t = tally(Finality::Quasar);
let forged = SignedVote {
position: position(),
accept: true,
node: rogue_id,
signature: vec![0u8; 96],
};
assert_eq!(t.add(&forged, &set), Err(VoteError::Signature));
}
// ── What a certificate a tally issues is worth ──────────────────────
/// The certificate a tally hands out is not a private artefact: it passes the
/// same predicate any third party runs, and survives its own encoding.
#[test]
fn the_certificate_a_tally_issues_verifies_for_a_third_party() {
let (s, set) = committee(7, 1);
let mut t = tally(Finality::Quasar);
for signer in s.iter().take(5) {
assert!(t.add(&signer.vote(&position(), true), &set).expect("held"));
}
let cert = t.cert(&set, &set).expect("a quorum certifies");
assert_eq!(i64::from(cert.threshold), signer_floor(Finality::Quasar, 7));
// A party that never saw the tally, holding only the set.
cert.verify_weighted(&set, &set, EPOCH)
.expect("it verifies for anyone");
// Votes are in canonical order, which is the distinctness clause too.
let ids: Vec<NodeId> = cert.votes.iter().map(|v| v.node_id).collect();
let mut sorted = ids.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(ids, sorted, "strictly increasing node ids");
}
/// One tally, one epoch. The height is fixed when the tally opens, and BOTH the
/// signature check and the stake read use it — a tally cannot verify against one
/// set and weigh against another.
#[test]
fn a_tally_reads_one_epoch_on_both_sides() {
/// A set that exists at exactly one height. Outside it there are no keys and
/// no stake, which is what an epoch boundary looks like to a reader.
struct AtEpoch<'a> {
inner: &'a ValidatorSet,
epoch: u64,
}
impl VoteVerifier for AtEpoch<'_> {
fn verify_vote(&self, n: &NodeId, m: &[u8], s: &[u8], h: u64) -> bool {
h == self.epoch && self.inner.verify_vote(n, m, s, h)
}
}
impl StakeSource for AtEpoch<'_> {
fn weight(&self, n: &NodeId, h: u64) -> u64 {
if h == self.epoch {
self.inner.weight(n, h)
} else {
0
}
}
fn signer_stake(&self, h: u64) -> u64 {
if h == self.epoch {
self.inner.signer_stake(h)
} else {
0
}
}
fn signer_count(&self, h: u64) -> i64 {
if h == self.epoch {
self.inner.signer_count(h)
} else {
0
}
}
}
let (s, set) = committee(7, 1);
let live = AtEpoch {
inner: &set,
epoch: EPOCH,
};
// At the epoch the tally opened at, everything resolves.
let mut here = Tally::new(position(), Finality::Quasar, EPOCH).expect("tally");
for signer in s.iter().take(5) {
assert!(here
.add(&signer.vote(&position(), true), &live)
.expect("held"));
}
here.cert(&live, &live).expect("a quorum at its own epoch");
// A tally opened at another height reads that height on the SIGNATURE side:
// no key resolves there, so nothing is even held, and the refusal is the
// signature clause rather than a quorum one.
let mut elsewhere = Tally::new(position(), Finality::Quasar, EPOCH + 1).expect("tally");
assert_eq!(elsewhere.epoch_height(), EPOCH + 1);
assert_eq!(
elsewhere.add(&s[0].vote(&position(), true), &live),
Err(VoteError::Signature),
"a signature is checked at the tally's epoch",
);
// And it reads that height on the STAKE side, which the same tally cannot
// show — it holds nothing to weigh. So: a tally that DID hold votes, handed
// a source alive only at some other epoch. The stake read is the tally's
// epoch, so the source answers zero and the certificate fails closed.
let dead = AtEpoch {
inner: &set,
epoch: EPOCH + 1,
};
assert_eq!(
here.cert(&live, &dead),
Err(VoteError::Cert(CertError::StakeZero {
epoch_height: EPOCH
})),
"the stake is read at the tally's epoch, not at the source's",
);
}
// ── The transport seam ──────────────────────────────────────────────
/// Consensus states the shape and opens no socket. A test carries a vote over a
/// vector; a node carries it over a mesh; the tally cannot tell.
#[test]
fn the_transport_is_a_seam_the_caller_fills() {
use std::cell::RefCell;
struct Echo(RefCell<Vec<SignedVote>>);
impl VoteTransport for Echo {
fn broadcast(&self, vote: &SignedVote) {
self.0.borrow_mut().push(vote.clone());
}
}
let (s, set) = committee(4, 1);
let wire = Echo(RefCell::new(Vec::new()));
for signer in &s {
wire.broadcast(&signer.vote(&position(), true));
}
// An implementation may echo a vote back to its origin; the tally
// deduplicates by signer, so a self-echo costs nothing.
let mut t = tally(Finality::Quasar);
let sent = wire.0.borrow();
for v in sent.iter().chain(sent.iter()) {
t.add(v, &set).expect("held or already held");
}
assert_eq!(t.len(), 4, "four signers, however many times each spoke");
}
/// A certificate assembled outside a tally is subject to the same rule. There is
/// no second door: `QuorumCert::assemble` still produces something, and it is
/// `verify_weighted` that refuses it — which is exactly what `Tally::cert` runs.
#[test]
fn the_rule_is_the_same_rule_outside_a_tally() {
let (s, set) = committee(7, 1);
let pos = position();
let votes: Vec<_> = s
.iter()
.take(2)
.map(|x| x.vote(&pos, true).record())
.collect();
let loose = QuorumCert::assemble(Finality::Quasar, pos, 2, &votes).expect("assembles");
assert!(
loose.verify_weighted(&set, &set, EPOCH).is_err(),
"assembling is not accepting, here as anywhere",
);
}