Reference for the core data types defined in hotmint-types (blocks, certificates, votes, validators, epochs, and the wire message enum). Supporting types live in their own modules — BlockContext/OwnedBlockContext/TxContext in context.rs, EndBlockResponse/Event/ValidatorUpdate in validator_update.rs, EquivocationProof in evidence.rs, and the state-sync types (SyncRequest, SyncResponse, SnapshotInfo, …) in sync.rs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
pub struct ViewNumber(pub u64);Monotonically increasing view number. Corresponds to v in the HotStuff-2 paper.
let view = ViewNumber(0);
let next = ViewNumber(view.as_u64() + 1);#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
pub struct Height(pub u64);Block height in the committed chain. Corresponds to k in the paper.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct BlockHash(pub [u8; 32]);32-byte Blake3 hash of a block.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Block {
pub height: Height,
pub parent_hash: BlockHash,
pub view: ViewNumber,
pub proposer: ValidatorId,
/// Unix timestamp in milliseconds, set by the proposer.
/// Validators verify monotonicity and future drift.
pub timestamp: u64,
pub payload: Vec<u8>,
/// Application state root after executing the parent block.
pub app_hash: BlockHash,
/// Equivocation evidence collected by the proposer.
pub evidence: Vec<EquivocationProof>,
pub hash: BlockHash,
}Corresponds to B_k := (b_k, h_{k-1}) in the paper. The payload field contains length-prefixed transactions (encoded by Mempool::collect_payload).
// genesis block
let genesis = Block::genesis();
assert_eq!(genesis.height, Height(0));
assert_eq!(genesis.parent_hash, BlockHash([0u8; 32]));#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuorumCertificate {
pub block_hash: BlockHash,
pub view: ViewNumber,
pub aggregate_signature: AggregateSignature,
/// Epoch in which this QC was formed. Included in signing bytes to prevent
/// cross-epoch QC reuse.
pub epoch: EpochNumber,
}Corresponds to C_v(B_k) — an aggregate signature covering more than 2/3 of the voting power on a block hash. Formed when the leader collects sufficient phase-1 votes.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DoubleCertificate {
pub inner_qc: QuorumCertificate,
pub outer_qc: QuorumCertificate,
/// Aggregated vote extensions from the Vote2 round.
pub vote_extensions: Vec<(ValidatorId, Vec<u8>)>,
}Corresponds to C_v(C_v(B_k)) — a QC of a QC. The inner_qc is the original QC on the block, and the outer_qc is the QC formed from phase-2 votes on that QC. Triggers the commit of the referenced block and all uncommitted ancestors.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeoutCertificate {
pub view: ViewNumber,
pub aggregate_signature: AggregateSignature,
pub highest_qcs: Vec<Option<QuorumCertificate>>,
}Corresponds to TC_v — proof that validators holding more than 2/3 of the voting power timed out in view. Contains an aggregate signature over the timeout messages and each signer's highest_qc (indexed by validator position in the set) to help the next leader pick the best chain to extend.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vote {
/// Epoch in which this vote was signed. Selects the validator set used
/// for verification and QC formation, and is covered by the signature.
pub epoch: EpochNumber,
pub block_hash: BlockHash,
pub view: ViewNumber,
pub validator: ValidatorId,
pub signature: Signature,
pub vote_type: VoteType,
/// ABCI++ vote extension (attached to Vote2 messages).
pub extension: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VoteType {
Vote, // phase-1 vote (on a block proposal)
Vote2, // phase-2 vote (on a QC / Prepare message)
}Vote::signing_bytes constructs the canonical byte sequence that is signed by each voter:
pub fn signing_bytes(
chain_id_hash: &[u8; 32],
epoch: EpochNumber,
view: ViewNumber,
validator: ValidatorId,
block_hash: &BlockHash,
vote_type: VoteType,
extension: Option<&[u8]>,
) -> Vec<u8>The layout is:
domain_tag (HOTMINT_VOTE_V3\0)
|| chain_id_hash (32 bytes)
|| epoch (8 bytes LE)
|| view (8 bytes LE)
|| validator_id (8 bytes LE)
|| block_hash (32 bytes)
|| vote_type (1 byte: Vote = 0, Vote2 = 1)
|| extension_marker (1 byte)
|| extension_len (8 bytes LE)
|| extension_hash (32 bytes: Blake3 of the extension, zero-filled when absent)
The chain_id_hash (a 32-byte Blake3 hash of the chain ID string), epoch number, validator id, vote type, extension digest, and the domain tag together prevent cross-chain, cross-epoch, cross-validator, cross-message-type, and vote-extension replay attacks.
Each signed message type uses a unique null-terminated domain tag prefix:
| Message | Tag |
|---|---|
| Vote / Vote2 | HOTMINT_VOTE_V3\0 |
| Proposal | HOTMINT_PROPOSAL_V1\0 |
| Prepare | HOTMINT_PREPARE_V1\0 |
| Wish (timeout) | HOTMINT_WISH_V1\0 |
| StatusCert | HOTMINT_STATUS_V2\0 |
All signing byte layouts follow the same pattern: tag || chain_id_hash || message-specific fields.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct ValidatorId(pub u64);#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidatorInfo {
pub id: ValidatorId,
pub public_key: PublicKey,
pub power: u64,
}let vs = ValidatorSet::new(vec![
ValidatorInfo { id: ValidatorId(0), public_key: pk0, power: 1 },
ValidatorInfo { id: ValidatorId(1), public_key: pk1, power: 1 },
ValidatorInfo { id: ValidatorId(2), public_key: pk2, power: 1 },
ValidatorInfo { id: ValidatorId(3), public_key: pk3, power: 1 },
]);
assert_eq!(vs.validator_count(), 4);
assert_eq!(vs.quorum_threshold(), 3); // floor(2*4/3) + 1 = strictly > 2/3
assert_eq!(vs.max_faulty_power(), 1); // total_power - quorum_threshold
// round-robin leader election
let leader = vs.leader_for_view(ViewNumber(5)).unwrap(); // validators[5 % 4] = validators[1]
// O(1) lookups
let idx = vs.index_of(ValidatorId(2)); // Some(2)
let info = vs.get(ValidatorId(2)); // Some(&ValidatorInfo)
let power = vs.power_of(ValidatorId(2)); // 1The index map is automatically rebuilt during deserialization. You only need to call rebuild_index() manually if you modify the validators list directly:
let mut vs: ValidatorSet = serde_json::from_str(&json)?;
// index_map is already populated — no manual rebuild needed
vs.rebuild_index(); // only needed if you mutate validators directly#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
pub struct EpochNumber(pub u64);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Epoch {
pub number: EpochNumber,
pub start_view: ViewNumber,
pub validator_set: ValidatorSet,
}An epoch defines a validator set with a starting view. Epoch transitions happen when the staking module signals a validator set change; the new epoch takes effect at start_view (deterministically set to commit_view + 2), so all honest nodes apply the transition at the same view.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signature(pub Vec<u8>);#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PublicKey(pub Vec<u8>);#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregateSignature {
pub signers: Vec<bool>, // bitfield: signers[i] == true if validator i signed
pub signatures: Vec<Signature>,
}let mut agg = AggregateSignature::new(4); // 4 validators
agg.add(0, sig_from_validator_0)?;
agg.add(2, sig_from_validator_2)?;
agg.add(3, sig_from_validator_3)?;
assert_eq!(agg.count(), 3); // 3 of 4 signedThe ConsensusMessage enum defines all message types exchanged between validators:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConsensusMessage {
// Leader -> All: block proposal with justify QC
Propose {
block: Box<Block>,
justify: Box<QuorumCertificate>,
double_cert: Option<Box<DoubleCertificate>>,
signature: Signature,
/// Uncommitted ancestors referenced by `double_cert`, so a replica
/// that missed them can still walk the commit chain.
ancestor_blocks: Vec<Block>,
},
// Replica -> Leader: phase-1 vote on a proposed block
VoteMsg(Vote),
// Leader -> All: QC formed, update your lock
Prepare {
certificate: QuorumCertificate,
signature: Signature,
},
// Replica -> Next Leader: phase-2 vote on the QC
Vote2Msg(Vote),
// Any -> All: timeout, requesting view change
Wish {
target_view: ViewNumber,
validator: ValidatorId,
highest_qc: Option<QuorumCertificate>,
signature: Signature,
},
// Any -> All: aggregated timeout proof
TimeoutCert(TimeoutCertificate),
// Replica -> Leader: status report at view entry
StatusCert {
locked_qc: Option<QuorumCertificate>,
validator: ValidatorId,
signature: Signature,
},
// Any -> All: equivocation proof (gossip)
Evidence(EquivocationProof),
}All messages are serialized with postcard (postcard) for network transport.