Skip to content

Latest commit

 

History

History
339 lines (265 loc) · 10.1 KB

File metadata and controls

339 lines (265 loc) · 10.1 KB

Core Types

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.

Primitives

ViewNumber

#[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);

Height

#[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.

BlockHash

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct BlockHash(pub [u8; 32]);

32-byte Blake3 hash of a block.

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]));

Certificates

QuorumCertificate (QC)

#[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.

DoubleCertificate (DC)

#[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.

TimeoutCertificate (TC)

#[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.

Vote

#[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)
}

Signing Bytes

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.

Domain Separator Tags

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.

Validators

ValidatorId

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct ValidatorId(pub u64);

ValidatorInfo

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidatorInfo {
    pub id: ValidatorId,
    pub public_key: PublicKey,
    pub power: u64,
}

ValidatorSet

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));  // 1

The 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

Epochs

#[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.

Cryptographic Primitives

Signature

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signature(pub Vec<u8>);

PublicKey

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PublicKey(pub Vec<u8>);

AggregateSignature

#[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 signed

ConsensusMessage (Wire Protocol)

The 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.