Skip to main content

tenzro_types/
block.rs

1//! Block types for Tenzro Network
2//!
3//! This module defines the block structure used to organize transactions
4//! and maintain the blockchain state.
5
6use crate::primitives::{Address, BlockHeight, Hash, Timestamp};
7use sha2::{Digest, Sha256};
8use crate::transaction::SignedTransaction;
9use serde::{Deserialize, Serialize};
10
11/// Maximum number of transactions allowed per block
12pub const MAX_TRANSACTIONS_PER_BLOCK: usize = 10_000;
13
14/// A block header containing metadata about a block
15///
16/// The header includes cryptographic commitments to the block's contents
17/// and links to the previous block.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct BlockHeader {
20    /// The height of this block
21    pub height: BlockHeight,
22    /// The HotStuff-2 view at which this block was proposed.
23    ///
24    /// Required for view-sync on inbound proposals: receivers must advance
25    /// their local view to match `view` before voting, so that the resulting
26    /// vote is bucketed at the same view as the proposer's self-vote and a
27    /// quorum can form. See `tenzro-consensus::HotStuff2Engine::on_proposal`.
28    ///
29    /// Mirrors HotStuff-2 (Malkhi & Nayak, eprint 2023/397) Figure 1: the
30    /// proposal message `⟨propose, B_k, v, C_{v'}(B_{k-1})⟩_{L_v}` carries
31    /// `v` as a first-class field. Equivalent to Aptos `Block::round()`.
32    #[serde(default)]
33    pub view: u64,
34    /// Hash of the previous block
35    pub prev_hash: Hash,
36    /// Merkle root of all transactions in the block
37    pub tx_root: Hash,
38    /// Merkle root of the state after executing this block
39    pub state_root: Hash,
40    /// Block timestamp
41    pub timestamp: Timestamp,
42    /// Address of the block proposer
43    pub proposer: Address,
44    /// Consensus proof for this block
45    pub consensus_proof: ConsensusProof,
46    /// Additional metadata
47    pub metadata: BlockMetadata,
48}
49
50impl BlockHeader {
51    /// Creates a new block header at view 0.
52    ///
53    /// For consensus-driven proposals use [`BlockHeader::new_at_view`] to
54    /// stamp the proposer's current view. Genesis uses view 0.
55    pub fn new(
56        height: BlockHeight,
57        prev_hash: Hash,
58        tx_root: Hash,
59        state_root: Hash,
60        proposer: Address,
61        consensus_proof: ConsensusProof,
62    ) -> Self {
63        Self::new_at_view(height, 0, prev_hash, tx_root, state_root, proposer, consensus_proof)
64    }
65
66    /// Creates a new block header stamped with the proposer's view.
67    pub fn new_at_view(
68        height: BlockHeight,
69        view: u64,
70        prev_hash: Hash,
71        tx_root: Hash,
72        state_root: Hash,
73        proposer: Address,
74        consensus_proof: ConsensusProof,
75    ) -> Self {
76        Self {
77            height,
78            view,
79            prev_hash,
80            tx_root,
81            state_root,
82            timestamp: Timestamp::now(),
83            proposer,
84            consensus_proof,
85            metadata: BlockMetadata::default(),
86        }
87    }
88
89    /// Computes the hash of the block header using canonical binary encoding.
90    ///
91    /// Fields are hashed in order: height, view, prev_hash, tx_root,
92    /// state_root, timestamp, proposer, gas_used, gas_limit, tx_count,
93    /// protocol_version, base_fee_per_gas. Using LE bytes for integer
94    /// fields ensures deterministic output regardless of platform or
95    /// serialization format.
96    ///
97    /// `base_fee_per_gas` is encoded as a presence-tagged u128:
98    /// `[0u8]` for `None`, `[1u8] || u128_le` for `Some(_)`. This
99    /// distinguishes a zero base fee from an unset base fee while
100    /// keeping the canonical encoding deterministic. Per EIP-1559 +
101    /// go-ethereum `VerifyEIP1559Header`, the base fee is part of the
102    /// signed block hash so validators reach byzantine agreement on it.
103    pub fn hash(&self) -> Hash {
104        let mut hasher = Sha256::new();
105        hasher.update(self.height.0.to_le_bytes());
106        hasher.update(self.view.to_le_bytes());
107        hasher.update(self.prev_hash.0);
108        hasher.update(self.tx_root.0);
109        hasher.update(self.state_root.0);
110        hasher.update(self.timestamp.0.to_le_bytes());
111        hasher.update(self.proposer.0);
112        hasher.update(self.metadata.gas_used.to_le_bytes());
113        hasher.update(self.metadata.gas_limit.to_le_bytes());
114        hasher.update(self.metadata.tx_count.to_le_bytes());
115        hasher.update((self.metadata.protocol_version as u64).to_le_bytes());
116        match self.metadata.base_fee_per_gas {
117            None => hasher.update([0u8]),
118            Some(bf) => {
119                hasher.update([1u8]);
120                hasher.update(bf.to_le_bytes());
121            }
122        }
123        Hash::new(hasher.finalize().into())
124    }
125}
126
127/// EIP-1559 fee-market parameters used for base-fee calculation.
128///
129/// Mirrors `tenzro_vm::eip1559::Eip1559Config` but lives in `tenzro-types`
130/// so the pure [`calculate_next_base_fee`] function can be invoked from
131/// the consensus layer without taking a dependency on `tenzro-vm`.
132///
133/// Defaults match `Eip1559Config::default()` in `tenzro-vm` (initial
134/// 1 Gwei, 15M target / 30M max, 12.5% max change per block, 0.1 Gwei
135/// floor, 1000 Gwei ceiling).
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
137pub struct FeeMarketParams {
138    pub initial_base_fee: u128,
139    pub target_gas_per_block: u64,
140    pub base_fee_change_denominator: u64,
141    pub min_base_fee: u128,
142    pub max_base_fee: u128,
143}
144
145impl Default for FeeMarketParams {
146    fn default() -> Self {
147        Self {
148            initial_base_fee: 1_000_000_000,
149            target_gas_per_block: 15_000_000,
150            base_fee_change_denominator: 8,
151            min_base_fee: 100_000_000,
152            max_base_fee: 1_000_000_000_000,
153        }
154    }
155}
156
157/// Pure EIP-1559 base-fee derivation for the next block.
158///
159/// Given the parent block's base fee and gas usage, returns what the
160/// child block's base fee MUST be. Both proposers (when stamping
161/// `BlockMetadata::base_fee_per_gas`) and validators (when verifying
162/// an inbound proposal) call this and reject any block that diverges.
163///
164/// Genesis-edge case: if `parent_base_fee` is `None` (parent predates
165/// EIP-1559) or the parent's `gas_limit` is 0 (genesis itself),
166/// returns `params.initial_base_fee`. Mirrors go-ethereum's behavior
167/// at the London-fork boundary.
168///
169/// The formula matches `FeeMarket::calculate_next_base_fee` byte-for-byte:
170/// - `parent_gas_used == target`: no change
171/// - `parent_gas_used > target`: increase by `current * delta / target / denom`, min 1 wei
172/// - `parent_gas_used < target`: decrease by `current * delta / target / denom` (saturating)
173/// - Result clamped to `[min_base_fee, max_base_fee]`.
174pub fn calculate_next_base_fee(
175    parent_base_fee: Option<u128>,
176    parent_gas_used: u64,
177    parent_gas_limit: u64,
178    params: &FeeMarketParams,
179) -> u128 {
180    // Genesis-edge: parent has no base fee, OR parent has zero gas_limit
181    // (genesis block) — child uses the initial fee.
182    let current = match parent_base_fee {
183        Some(bf) if parent_gas_limit > 0 => bf,
184        _ => return params.initial_base_fee,
185    };
186
187    let target = params.target_gas_per_block;
188    let denom = params.base_fee_change_denominator as u128;
189
190    let next_fee = if parent_gas_used == target {
191        current
192    } else if parent_gas_used > target {
193        let gas_delta = (parent_gas_used - target) as u128;
194        let fee_delta = (current * gas_delta) / (target as u128) / denom;
195        current + fee_delta.max(1)
196    } else {
197        let gas_delta = (target - parent_gas_used) as u128;
198        let fee_delta = (current * gas_delta) / (target as u128) / denom;
199        current.saturating_sub(fee_delta)
200    };
201
202    next_fee.clamp(params.min_base_fee, params.max_base_fee)
203}
204
205/// Additional metadata included in a block header
206#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
207pub struct BlockMetadata {
208    /// Total gas used in this block
209    pub gas_used: u64,
210    /// Gas limit for this block
211    pub gas_limit: u64,
212    /// Number of transactions in this block
213    pub tx_count: u64,
214    /// Protocol version
215    pub protocol_version: u32,
216    /// EIP-1559 base fee per gas for this block, in wei-equivalent (TNZO base units / 10^18).
217    ///
218    /// Stamped at block-finalize time from the live `FeeMarket`. This is the
219    /// minimum gas price that any transaction in this block had to bid; the
220    /// portion of effective gas price equal to this is burned. `eth_feeHistory`
221    /// reads this field directly per block, so it must be persisted with the
222    /// block header rather than reconstructed from the runtime fee market
223    /// (which only retains a bounded recent window).
224    ///
225    /// `None` for genesis and any block produced before EIP-1559 wiring; new
226    /// blocks always carry `Some(_)`.
227    pub base_fee_per_gas: Option<u128>,
228}
229
230/// Proof of consensus for a block
231///
232/// Contains the cryptographic evidence that the block was produced
233/// according to the consensus rules.
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct ConsensusProof {
236    /// The consensus algorithm used
237    pub algorithm: ConsensusAlgorithm,
238    /// Proof data specific to the consensus algorithm
239    pub proof_data: Vec<u8>,
240    /// Signatures from validators
241    pub signatures: Vec<ValidatorSignature>,
242}
243
244impl ConsensusProof {
245    /// Creates a new consensus proof
246    pub fn new(algorithm: ConsensusAlgorithm, proof_data: Vec<u8>) -> Self {
247        Self {
248            algorithm,
249            proof_data,
250            signatures: Vec::new(),
251        }
252    }
253
254    /// Adds a validator signature to the proof
255    pub fn add_signature(&mut self, signature: ValidatorSignature) {
256        self.signatures.push(signature);
257    }
258}
259
260/// The consensus algorithm used to produce a block
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
262pub enum ConsensusAlgorithm {
263    /// Proof of Stake
264    PoS,
265    /// Practical Byzantine Fault Tolerance
266    PBFT,
267    /// Tendermint-style consensus
268    Tendermint,
269}
270
271/// A signature from a validator in the consensus process
272#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
273pub struct ValidatorSignature {
274    /// The validator's address
275    pub validator: Address,
276    /// The signature bytes
277    pub signature: Vec<u8>,
278    /// The voting power of the validator
279    pub voting_power: u128,
280}
281
282/// A complete block on Tenzro Network
283///
284/// Contains the header and all transactions included in the block.
285///
286/// The `transactions` vector is bounded by `MAX_DESERIALIZED_TX_COUNT`
287/// at deserialization time to protect against OOM via untrusted payloads
288/// (HIGH #69 in the production audit).
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
290pub struct Block {
291    /// The block header
292    pub header: BlockHeader,
293    /// All transactions in this block
294    #[serde(deserialize_with = "crate::validation::bounded_tx_vec")]
295    pub transactions: Vec<SignedTransaction>,
296}
297
298impl Block {
299    /// Creates a new block
300    pub fn new(header: BlockHeader, transactions: Vec<SignedTransaction>) -> Self {
301        Self {
302            header,
303            transactions,
304        }
305    }
306
307    /// Creates a new block with validation
308    ///
309    /// Returns an error if the number of transactions exceeds MAX_TRANSACTIONS_PER_BLOCK
310    pub fn new_validated(header: BlockHeader, transactions: Vec<SignedTransaction>) -> Result<Self, crate::error::TenzroError> {
311        if transactions.len() > MAX_TRANSACTIONS_PER_BLOCK {
312            return Err(crate::error::TenzroError::InvalidBlock(format!(
313                "Too many transactions: {} exceeds maximum of {}",
314                transactions.len(),
315                MAX_TRANSACTIONS_PER_BLOCK
316            )));
317        }
318        Ok(Self {
319            header,
320            transactions,
321        })
322    }
323
324    /// Returns the hash of the block
325    pub fn hash(&self) -> Hash {
326        self.header.hash()
327    }
328
329    /// Returns the height of the block
330    pub fn height(&self) -> BlockHeight {
331        self.header.height
332    }
333
334    /// Returns the timestamp of the block
335    pub fn timestamp(&self) -> Timestamp {
336        self.header.timestamp
337    }
338
339    /// Returns the number of transactions in the block
340    pub fn tx_count(&self) -> usize {
341        self.transactions.len()
342    }
343
344    /// Returns the block proposer
345    pub fn proposer(&self) -> Address {
346        self.header.proposer
347    }
348
349    /// Validates the block structure
350    ///
351    /// Checks basic invariants like transaction count matching metadata.
352    pub fn validate_structure(&self) -> bool {
353        self.transactions.len() as u64 == self.header.metadata.tx_count
354    }
355}