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}