From 8318cb9174f9cab44483c1e93648a67a81f67f8a Mon Sep 17 00:00:00 2001 From: prpeh Date: Wed, 31 Dec 2025 12:17:53 +0700 Subject: [PATCH 1/3] feat: implement Fusaka (EIP-7825) transaction gas limit Introduces a fixed transaction gas limit of 16,777,216 (2^24) as per EIP-7825. - types: add MAX_TX_GAS_LIMIT constant - vm: reject txs exceeding limit in execute_block - tx_pool: reject txs exceeding limit in add_transaction - tests: add unit tests for gas limit enforcement --- src/tx_pool.rs | 36 ++++++++++++++++++++ src/types.rs | 1 + src/vm.rs | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/src/tx_pool.rs b/src/tx_pool.rs index 0e843af..799243e 100644 --- a/src/tx_pool.rs +++ b/src/tx_pool.rs @@ -15,6 +15,8 @@ pub enum PoolError { InvalidNonce(u64, u64), #[error("Storage Error: {0}")] StorageError(String), + #[error("Gas Limit Exceeded: max {0}, got {1}")] + GasLimitExceeded(u64, u64), } /// A simple Transaction Pool (Mempool). @@ -41,6 +43,14 @@ impl TxPool { /// Add a transaction to the pool. pub fn add_transaction(&self, tx: Transaction) -> Result<(), PoolError> { + // 0. Check Gas Limit (Fusaka) + if tx.gas_limit > crate::types::MAX_TX_GAS_LIMIT { + return Err(PoolError::GasLimitExceeded( + crate::types::MAX_TX_GAS_LIMIT, + tx.gas_limit, + )); + } + // 1. Validate Signature let sighash = tx.sighash(); if !verify(&tx.public_key, &sighash.0, &tx.signature) { @@ -237,4 +247,30 @@ mod tests { _ => panic!("Expected InvalidNonce"), } } + + #[test] + fn test_pool_gas_limit() { + use crate::types::MAX_TX_GAS_LIMIT; + let storage = Arc::new(MemStorage::new()); + let pool = TxPool::new(storage); + let (pk, sk) = generate_keypair(); + + let mut tx = Transaction { + chain_id: 1, + nonce: 0, + max_priority_fee_per_gas: U256::ZERO, + max_fee_per_gas: U256::ZERO, + gas_limit: MAX_TX_GAS_LIMIT + 1, // Exceeds + to: None, + value: U256::ZERO, + data: Bytes::new(), + access_list: vec![], + public_key: pk.clone(), + signature: crate::crypto::Signature::default(), + }; + tx.signature = sign(&sk, &tx.sighash().0); + + let res = pool.add_transaction(tx); + assert!(matches!(res, Err(PoolError::GasLimitExceeded(..)))); + } } diff --git a/src/types.rs b/src/types.rs index 9fbc922..43bc29d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; pub type View = u64; pub const DEFAULT_BLOCK_GAS_LIMIT: u64 = 30_000_000; +pub const MAX_TX_GAS_LIMIT: u64 = 16_777_216; // 2^24 (Fusaka EIP-7825) pub const INITIAL_BASE_FEE: u64 = 10_000_000; // 0.01 Gwei #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/vm.rs b/src/vm.rs index 906743e..de476e5 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -32,6 +32,90 @@ mod tests { // ... } + + #[test] + fn test_transaction_gas_limit() { + use crate::crypto::{Hash, generate_keypair, sign}; + use crate::storage::Storage; + use crate::types::{Block, Bytes, MAX_TX_GAS_LIMIT, QuorumCertificate, Transaction}; // Import trait for save_account + + // Generate keys FIRST + let (pk, sk) = generate_keypair(); + + let storage = Arc::new(MemStorage::new()); + { + // Fund the sender + let addr = crate::types::keccak256(pk.0.to_bytes()); + let address = crate::types::Address::from_slice(&addr[12..]); + let account = crate::storage::AccountInfo { + nonce: 0, + balance: crate::types::U256::from(10_000_000_000_000_000_000u64), // 10 ETH + code_hash: crate::crypto::Hash(crate::types::keccak256(&[]).0), // Empty Code Hash + code: None, + }; + storage.save_account(&address, &account).unwrap(); + } + + let state = Arc::new(Mutex::new(StateManager::new(storage, None))); + // Block limit 30M, Tx limit 16.7M + let executor = Executor::new(state, 30_000_000); + + // 1. Valid Tx (Below Limit) + let mut tx_ok = Transaction { + chain_id: 1, + nonce: 0, + max_priority_fee_per_gas: U256::from(1_000_000_000u64), // 1 Gwei + max_fee_per_gas: U256::from(10_000_000_000u64), // 10 Gwei + gas_limit: MAX_TX_GAS_LIMIT, // Borderline OK + to: None, + value: U256::ZERO, + data: Bytes::new(), + access_list: vec![], + public_key: pk.clone(), + signature: crate::crypto::Signature::default(), + }; + tx_ok.signature = sign(&sk, &tx_ok.sighash().0); + + let mut block = + Block::new_dummy(pk.clone(), 1, Hash::default(), QuorumCertificate::default()); + block.base_fee_per_gas = U256::from(10_000_000); // 0.01 Gwei + block.payload = vec![tx_ok]; + + if let Err(e) = executor.execute_block(&mut block) { + panic!("Valid Transaction Failed: {:?}", e); + } + + // 2. Invalid Tx (Above Limit) + let mut tx_bad = Transaction { + chain_id: 1, + nonce: 1, + max_priority_fee_per_gas: U256::ZERO, + max_fee_per_gas: U256::ZERO, + gas_limit: MAX_TX_GAS_LIMIT + 1, // Exceeds + to: None, + value: U256::ZERO, + data: Bytes::new(), + access_list: vec![], + public_key: pk.clone(), + signature: crate::crypto::Signature::default(), + }; + tx_bad.signature = sign(&sk, &tx_bad.sighash().0); + + let mut block_bad = + Block::new_dummy(pk.clone(), 2, Hash::default(), QuorumCertificate::default()); + block_bad.payload = vec![tx_bad]; + + let res = executor.execute_block(&mut block_bad); + match res { + Err(ExecutionError::Transaction(msg)) => { + assert!(msg.contains("Fusaka")); + } + _ => panic!( + "Expected Transaction Error with Fusaka message, got {:?}", + res + ), + } + } } #[derive(Clone)] @@ -67,6 +151,11 @@ impl Executor { self.process_liveness_slashing(block, &mut db); for tx in &block.payload { + if tx.gas_limit > crate::types::MAX_TX_GAS_LIMIT { + return Err(ExecutionError::Transaction( + "Tx exceeds fixed tx gas limit (Fusaka)".into(), + )); + } if tx.gas_limit > self.block_gas_limit { return Err(ExecutionError::Transaction( "Tx exceeds block gas limit".into(), From 950d9209a7e311bdc2535755f09c3545b954216c Mon Sep 17 00:00:00 2001 From: prpeh Date: Wed, 31 Dec 2025 12:24:03 +0700 Subject: [PATCH 2/3] test: fix unused imports and mutable warnings in slashing_test.rs --- tests/slashing_test.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/slashing_test.rs b/tests/slashing_test.rs index b8843d0..5e98f62 100644 --- a/tests/slashing_test.rs +++ b/tests/slashing_test.rs @@ -2,7 +2,6 @@ use ockham::consensus::{ConsensusAction, SimplexState}; use ockham::crypto::{Hash, PrivateKey, PublicKey}; use ockham::storage::Storage; use ockham::types::{Block, QuorumCertificate, U256, Vote, VoteType}; -use revm::Database; use std::sync::Arc; use std::sync::Mutex; @@ -60,7 +59,7 @@ fn test_slashing_flow() { // Initialize Stakes for Offender { - let mut db = state_manager.lock().unwrap(); + let db = state_manager.lock().unwrap(); let mut state = db.get_consensus_state().unwrap().unwrap(); state.stakes.insert(offender_addr, U256::from(5000u64)); db.save_consensus_state(&state).unwrap(); @@ -177,7 +176,7 @@ fn test_slashing_flow() { executor.execute_block(&mut block_to_exec).unwrap(); // Check Stake - let mut db = validator.executor.state.lock().unwrap(); + let db = validator.executor.state.lock().unwrap(); let state = db.get_consensus_state().unwrap().unwrap(); let stake = state.stakes.get(&offender_addr).unwrap(); From e67cb3108831a37d100c421f97143f6ef6c85aee Mon Sep 17 00:00:00 2001 From: prpeh Date: Wed, 31 Dec 2025 12:30:12 +0700 Subject: [PATCH 3/3] refactor: move gas limit tests to tests/gas_limit_test.rs --- src/tx_pool.rs | 26 --------- src/vm.rs | 84 ----------------------------- tests/gas_limit_test.rs | 113 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 110 deletions(-) create mode 100644 tests/gas_limit_test.rs diff --git a/src/tx_pool.rs b/src/tx_pool.rs index 799243e..dcca4ce 100644 --- a/src/tx_pool.rs +++ b/src/tx_pool.rs @@ -247,30 +247,4 @@ mod tests { _ => panic!("Expected InvalidNonce"), } } - - #[test] - fn test_pool_gas_limit() { - use crate::types::MAX_TX_GAS_LIMIT; - let storage = Arc::new(MemStorage::new()); - let pool = TxPool::new(storage); - let (pk, sk) = generate_keypair(); - - let mut tx = Transaction { - chain_id: 1, - nonce: 0, - max_priority_fee_per_gas: U256::ZERO, - max_fee_per_gas: U256::ZERO, - gas_limit: MAX_TX_GAS_LIMIT + 1, // Exceeds - to: None, - value: U256::ZERO, - data: Bytes::new(), - access_list: vec![], - public_key: pk.clone(), - signature: crate::crypto::Signature::default(), - }; - tx.signature = sign(&sk, &tx.sighash().0); - - let res = pool.add_transaction(tx); - assert!(matches!(res, Err(PoolError::GasLimitExceeded(..)))); - } } diff --git a/src/vm.rs b/src/vm.rs index de476e5..4bfd480 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -32,90 +32,6 @@ mod tests { // ... } - - #[test] - fn test_transaction_gas_limit() { - use crate::crypto::{Hash, generate_keypair, sign}; - use crate::storage::Storage; - use crate::types::{Block, Bytes, MAX_TX_GAS_LIMIT, QuorumCertificate, Transaction}; // Import trait for save_account - - // Generate keys FIRST - let (pk, sk) = generate_keypair(); - - let storage = Arc::new(MemStorage::new()); - { - // Fund the sender - let addr = crate::types::keccak256(pk.0.to_bytes()); - let address = crate::types::Address::from_slice(&addr[12..]); - let account = crate::storage::AccountInfo { - nonce: 0, - balance: crate::types::U256::from(10_000_000_000_000_000_000u64), // 10 ETH - code_hash: crate::crypto::Hash(crate::types::keccak256(&[]).0), // Empty Code Hash - code: None, - }; - storage.save_account(&address, &account).unwrap(); - } - - let state = Arc::new(Mutex::new(StateManager::new(storage, None))); - // Block limit 30M, Tx limit 16.7M - let executor = Executor::new(state, 30_000_000); - - // 1. Valid Tx (Below Limit) - let mut tx_ok = Transaction { - chain_id: 1, - nonce: 0, - max_priority_fee_per_gas: U256::from(1_000_000_000u64), // 1 Gwei - max_fee_per_gas: U256::from(10_000_000_000u64), // 10 Gwei - gas_limit: MAX_TX_GAS_LIMIT, // Borderline OK - to: None, - value: U256::ZERO, - data: Bytes::new(), - access_list: vec![], - public_key: pk.clone(), - signature: crate::crypto::Signature::default(), - }; - tx_ok.signature = sign(&sk, &tx_ok.sighash().0); - - let mut block = - Block::new_dummy(pk.clone(), 1, Hash::default(), QuorumCertificate::default()); - block.base_fee_per_gas = U256::from(10_000_000); // 0.01 Gwei - block.payload = vec![tx_ok]; - - if let Err(e) = executor.execute_block(&mut block) { - panic!("Valid Transaction Failed: {:?}", e); - } - - // 2. Invalid Tx (Above Limit) - let mut tx_bad = Transaction { - chain_id: 1, - nonce: 1, - max_priority_fee_per_gas: U256::ZERO, - max_fee_per_gas: U256::ZERO, - gas_limit: MAX_TX_GAS_LIMIT + 1, // Exceeds - to: None, - value: U256::ZERO, - data: Bytes::new(), - access_list: vec![], - public_key: pk.clone(), - signature: crate::crypto::Signature::default(), - }; - tx_bad.signature = sign(&sk, &tx_bad.sighash().0); - - let mut block_bad = - Block::new_dummy(pk.clone(), 2, Hash::default(), QuorumCertificate::default()); - block_bad.payload = vec![tx_bad]; - - let res = executor.execute_block(&mut block_bad); - match res { - Err(ExecutionError::Transaction(msg)) => { - assert!(msg.contains("Fusaka")); - } - _ => panic!( - "Expected Transaction Error with Fusaka message, got {:?}", - res - ), - } - } } #[derive(Clone)] diff --git a/tests/gas_limit_test.rs b/tests/gas_limit_test.rs new file mode 100644 index 0000000..4e64fd3 --- /dev/null +++ b/tests/gas_limit_test.rs @@ -0,0 +1,113 @@ +use ockham::crypto::{Hash, generate_keypair, sign}; +use ockham::state::StateManager; +use ockham::storage::{AccountInfo, MemStorage, Storage}; +use ockham::tx_pool::{PoolError, TxPool}; +use ockham::types::{ + Address, Block, Bytes, MAX_TX_GAS_LIMIT, QuorumCertificate, Transaction, U256, +}; +use ockham::vm::{ExecutionError, Executor}; +use std::sync::{Arc, Mutex}; + +#[test] +fn test_transaction_gas_limit() { + // Generate keys + let (pk, sk) = generate_keypair(); + + let storage = Arc::new(MemStorage::new()); + { + // Fund the sender + let addr = ockham::types::keccak256(pk.0.to_bytes()); + let address = Address::from_slice(&addr[12..]); + let account = AccountInfo { + nonce: 0, + balance: U256::from(10_000_000_000_000_000_000u64), // 10 ETH + code_hash: Hash(ockham::types::keccak256(&[]).0), // Empty Code Hash + code: None, + }; + storage.save_account(&address, &account).unwrap(); + } + + let state = Arc::new(Mutex::new(StateManager::new(storage, None))); + // Block limit 30M, Tx limit 16.7M + let executor = Executor::new(state, 30_000_000); + + // 1. Valid Tx (Below Limit) + let mut tx_ok = Transaction { + chain_id: 1, + nonce: 0, + max_priority_fee_per_gas: U256::from(1_000_000_000u64), // 1 Gwei + max_fee_per_gas: U256::from(10_000_000_000u64), // 10 Gwei + gas_limit: MAX_TX_GAS_LIMIT, // Borderline OK + to: None, + value: U256::ZERO, + data: Bytes::new(), + access_list: vec![], + public_key: pk.clone(), + signature: ockham::crypto::Signature::default(), + }; + tx_ok.signature = sign(&sk, &tx_ok.sighash().0); + + let mut block = Block::new_dummy(pk.clone(), 1, Hash::default(), QuorumCertificate::default()); + block.base_fee_per_gas = U256::from(10_000_000); // 0.01 Gwei + block.payload = vec![tx_ok]; + + if let Err(e) = executor.execute_block(&mut block) { + panic!("Valid Transaction Failed: {:?}", e); + } + + // 2. Invalid Tx (Above Limit) + let mut tx_bad = Transaction { + chain_id: 1, + nonce: 1, + max_priority_fee_per_gas: U256::ZERO, + max_fee_per_gas: U256::ZERO, + gas_limit: MAX_TX_GAS_LIMIT + 1, // Exceeds + to: None, + value: U256::ZERO, + data: Bytes::new(), + access_list: vec![], + public_key: pk.clone(), + signature: ockham::crypto::Signature::default(), + }; + tx_bad.signature = sign(&sk, &tx_bad.sighash().0); + + let mut block_bad = + Block::new_dummy(pk.clone(), 2, Hash::default(), QuorumCertificate::default()); + block_bad.payload = vec![tx_bad]; + + let res = executor.execute_block(&mut block_bad); + match res { + Err(ExecutionError::Transaction(msg)) => { + assert!(msg.contains("Fusaka")); + } + _ => panic!( + "Expected Transaction Error with Fusaka message, got {:?}", + res + ), + } +} + +#[test] +fn test_pool_gas_limit() { + let storage = Arc::new(MemStorage::new()); + let pool = TxPool::new(storage); + let (pk, sk) = generate_keypair(); + + let mut tx = Transaction { + chain_id: 1, + nonce: 0, + max_priority_fee_per_gas: U256::ZERO, + max_fee_per_gas: U256::ZERO, + gas_limit: MAX_TX_GAS_LIMIT + 1, // Exceeds + to: None, + value: U256::ZERO, + data: Bytes::new(), + access_list: vec![], + public_key: pk.clone(), + signature: ockham::crypto::Signature::default(), + }; + tx.signature = sign(&sk, &tx.sighash().0); + + let res = pool.add_transaction(tx); + assert!(matches!(res, Err(PoolError::GasLimitExceeded(..)))); +}