use crate::config::ConsensusConfig;
use crate::error::{ConsensusError, Result};
use crate::mempool::Mempool;
use std::sync::Arc;
use tenzro_types::block::{
calculate_next_base_fee, Block, BlockHeader, BlockMetadata, ConsensusAlgorithm,
ConsensusProof, FeeMarketParams,
};
use tenzro_types::primitives::{Address, BlockHeight, Hash};
use tenzro_types::transaction::SignedTransaction;
pub struct BlockProposer {
mempool: Arc<Mempool>,
config: Arc<ConsensusConfig>,
}
impl BlockProposer {
pub fn new(mempool: Arc<Mempool>, config: Arc<ConsensusConfig>) -> Self {
Self { mempool, config }
}
pub fn propose_block(
&self,
height: BlockHeight,
view: u64,
prev_hash: Hash,
proposer: Address,
state_root: Hash,
parent_base_fee: Option<u128>,
parent_gas_used: u64,
parent_gas_limit: u64,
) -> Result<Block> {
let transactions = self.select_transactions()?;
if transactions.is_empty() {
tracing::debug!("No transactions available for block proposal");
}
let tx_root = self.calculate_tx_root(&transactions);
let base_fee = calculate_next_base_fee(
parent_base_fee,
parent_gas_used,
parent_gas_limit,
&FeeMarketParams::default(),
);
let metadata = self.create_metadata(&transactions, base_fee);
let gas_used = metadata.gas_used;
let consensus_proof = ConsensusProof::new(ConsensusAlgorithm::PBFT, Vec::new());
let header = BlockHeader::new_at_view(
height,
view,
prev_hash,
tx_root,
state_root,
proposer,
consensus_proof,
)
.with_metadata(metadata);
let block = Block::new(header, transactions);
tracing::info!(
height = %height,
view = view,
tx_count = block.tx_count(),
gas_used = gas_used,
base_fee_per_gas = base_fee,
proposer = %proposer,
"Block proposed"
);
Ok(block)
}
fn select_transactions(&self) -> Result<Vec<SignedTransaction>> {
self.mempool.cleanup_expired();
let transactions = self.mempool.select_transactions(
self.config.max_transactions_per_block,
self.config.max_gas_per_block,
);
Ok(transactions)
}
fn calculate_tx_root(&self, transactions: &[SignedTransaction]) -> Hash {
if transactions.is_empty() {
return Hash::default();
}
let mut combined = Vec::new();
for tx in transactions {
combined.extend_from_slice(tx.transaction.hash().as_bytes());
}
let hash_bytes = tenzro_crypto::hash::sha256(&combined);
Hash::new(hash_bytes.as_bytes().try_into().unwrap_or([0u8; 32]))
}
fn create_metadata(&self, transactions: &[SignedTransaction], base_fee: u128) -> BlockMetadata {
let tx_count = transactions.len() as u64;
let gas_used: u64 = transactions
.iter()
.map(|tx| tx.transaction.gas_limit)
.sum();
BlockMetadata {
gas_used,
gas_limit: self.config.max_gas_per_block,
tx_count,
protocol_version: 1,
base_fee_per_gas: Some(base_fee),
}
}
pub fn validate_proposal(&self, block: &Block, expected_height: BlockHeight) -> Result<()> {
if block.height() != expected_height {
return Err(ConsensusError::InvalidHeight {
expected: expected_height,
actual: block.height(),
});
}
if !block.validate_structure() {
return Err(ConsensusError::InvalidProposal(
"Invalid block structure".to_string(),
));
}
if block.tx_count() > self.config.max_transactions_per_block {
return Err(ConsensusError::InvalidProposal(format!(
"Too many transactions: {} > {}",
block.tx_count(),
self.config.max_transactions_per_block
)));
}
if block.header.metadata.gas_used > self.config.max_gas_per_block {
return Err(ConsensusError::InvalidProposal(format!(
"Gas limit exceeded: {} > {}",
block.header.metadata.gas_used,
self.config.max_gas_per_block
)));
}
self.validate_block_size(block)?;
if !self.validate_transaction_ordering(&block.transactions) {
return Err(ConsensusError::InvalidProposal(
"Invalid transaction ordering".to_string(),
));
}
tracing::debug!(
height = %block.height(),
tx_count = block.tx_count(),
"Block proposal validated"
);
Ok(())
}
pub fn validate_base_fee(&self, block: &Block, parent: &Block) -> Result<()> {
let expected = calculate_next_base_fee(
parent.header.metadata.base_fee_per_gas,
parent.header.metadata.gas_used,
parent.header.metadata.gas_limit,
&FeeMarketParams::default(),
);
match block.header.metadata.base_fee_per_gas {
Some(actual) if actual == expected => Ok(()),
Some(actual) => Err(ConsensusError::InvalidProposal(format!(
"EIP-1559 base fee mismatch: expected {}, got {} (parent height {}, gas_used {}, gas_limit {})",
expected,
actual,
parent.height(),
parent.header.metadata.gas_used,
parent.header.metadata.gas_limit,
))),
None => Err(ConsensusError::InvalidProposal(
"EIP-1559 base fee missing from block metadata".to_string(),
)),
}
}
fn validate_transaction_ordering(&self, transactions: &[SignedTransaction]) -> bool {
if transactions.len() <= 1 {
return true;
}
for i in 0..transactions.len() - 1 {
let current_gas_price = transactions[i].transaction.gas_price;
let next_gas_price = transactions[i + 1].transaction.gas_price;
if current_gas_price < next_gas_price {
return false;
}
}
true
}
pub fn estimate_block_size(&self, block: &Block) -> usize {
serde_json::to_string(block)
.map(|s| s.len())
.unwrap_or(0)
}
pub fn validate_block_size(&self, block: &Block) -> Result<()> {
let size = self.estimate_block_size(block);
if size > self.config.max_block_size {
return Err(ConsensusError::InvalidProposal(format!(
"Block size {} exceeds maximum {}",
size, self.config.max_block_size
)));
}
Ok(())
}
}
trait BlockHeaderExt {
fn with_metadata(self, metadata: BlockMetadata) -> Self;
}
impl BlockHeaderExt for BlockHeader {
fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
self.metadata = metadata;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mempool::Mempool;
use tenzro_crypto::pq::MlDsaSigningKey;
use tenzro_types::primitives::{ChainId, Nonce};
use tenzro_types::transaction::{Transaction, TransactionType};
use tenzro_types::Signature;
fn create_test_transaction(gas_price: u64, nonce: u64) -> SignedTransaction {
let pq_key = MlDsaSigningKey::generate();
let tx = Transaction::new(
ChainId::from(1),
Address::default(),
Address::default(),
Nonce::from(nonce),
TransactionType::Transfer { amount: 1000 },
21000,
gas_price,
pq_key.verifying_key_bytes().to_vec(),
);
let pq_sig = pq_key.sign(tx.hash().as_bytes()).to_vec();
SignedTransaction::new(tx, Signature::default(), pq_sig)
}
#[test]
fn test_propose_block() {
let config = Arc::new(ConsensusConfig::default());
let mempool = Arc::new(Mempool::new(config.clone()));
let proposer = BlockProposer::new(mempool.clone(), config);
mempool.add_transaction(create_test_transaction(100, 1)).unwrap();
mempool.add_transaction(create_test_transaction(200, 2)).unwrap();
let block = proposer
.propose_block(
BlockHeight::from(1),
0,
Hash::default(),
Address::default(),
Hash::default(),
None, 0, 0, )
.unwrap();
assert_eq!(block.height(), BlockHeight::from(1));
assert_eq!(block.tx_count(), 2);
assert_eq!(
block.header.metadata.base_fee_per_gas,
Some(FeeMarketParams::default().initial_base_fee)
);
}
#[test]
fn test_validate_proposal() {
let config = Arc::new(ConsensusConfig::default());
let mempool = Arc::new(Mempool::new(config.clone()));
let proposer = BlockProposer::new(mempool, config);
let block = Block::new(
BlockHeader::new(
BlockHeight::from(1),
Hash::default(),
Hash::default(),
Hash::default(),
Address::default(),
ConsensusProof::new(ConsensusAlgorithm::PBFT, Vec::new()),
),
vec![],
);
assert!(proposer.validate_proposal(&block, BlockHeight::from(1)).is_ok());
}
#[test]
fn test_validate_wrong_height() {
let config = Arc::new(ConsensusConfig::default());
let mempool = Arc::new(Mempool::new(config.clone()));
let proposer = BlockProposer::new(mempool, config);
let block = Block::new(
BlockHeader::new(
BlockHeight::from(1),
Hash::default(),
Hash::default(),
Hash::default(),
Address::default(),
ConsensusProof::new(ConsensusAlgorithm::PBFT, Vec::new()),
),
vec![],
);
let result = proposer.validate_proposal(&block, BlockHeight::from(2));
assert!(result.is_err());
}
}