From 309aed911438bf11ab7f97ca9fbb309add599a49 Mon Sep 17 00:00:00 2001 From: prpeh Date: Sat, 20 Dec 2025 15:56:21 +0700 Subject: [PATCH] feat: implement standard RPC methods (Issue 32) - Implements call, estimate_gas, get_code, get_block_by_number - Updates Executor to support ephemeral execution - Updates Main to wire Executor to RPC - Adds extended RPC tests --- src/client.rs | 9 +++ src/main.rs | 3 +- src/rpc.rs | 138 ++++++++++++++++++++++++++++++++++++++++++++++ src/vm.rs | 63 +++++++++++++++++++++ tests/rpc_test.rs | 129 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 341 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 1813bf0..ece834f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -37,6 +37,15 @@ impl OckhamClient { Ok(balance) } + pub async fn get_transaction_count( + &self, + address: Address, + ) -> Result> { + let params = rpc_params![address]; + let nonce: u64 = self.client.request("get_transaction_count", params).await?; + Ok(nonce) + } + pub async fn send_transaction( &self, nonce: u64, diff --git a/src/main.rs b/src/main.rs index 7885f47..5ebb6d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,7 +71,7 @@ async fn main() -> Result<(), Box> { committee, storage.clone(), tx_pool.clone(), - executor, + executor.clone(), block_gas_limit, ); @@ -82,6 +82,7 @@ async fn main() -> Result<(), Box> { let rpc_impl = OckhamRpcImpl::new( storage.clone(), tx_pool.clone(), + executor.clone(), block_gas_limit, bg_tx_sender, ); diff --git a/src/rpc.rs b/src/rpc.rs index e9ff4f1..a8bc1ee 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -4,8 +4,18 @@ use crate::tx_pool::TxPool; use crate::types::{Address, Block, Transaction, U256}; use jsonrpsee::core::{RpcResult, async_trait}; use jsonrpsee::proc_macros::rpc; +use serde::Deserialize; use std::sync::Arc; +#[derive(Deserialize)] +pub struct CallRequest { + pub from: Option
, + pub to: Option
, + pub gas: Option, + pub gas_price: Option, + pub value: Option, + pub data: Option, +} #[rpc(server)] pub trait OckhamRpc { #[method(name = "get_block_by_hash")] @@ -23,16 +33,32 @@ pub trait OckhamRpc { #[method(name = "get_balance")] fn get_balance(&self, address: Address) -> RpcResult; + #[method(name = "get_transaction_count")] + fn get_transaction_count(&self, address: Address) -> RpcResult; + #[method(name = "chain_id")] fn chain_id(&self) -> RpcResult; #[method(name = "suggest_base_fee")] fn suggest_base_fee(&self) -> RpcResult; + + #[method(name = "call")] + fn call(&self, request: CallRequest, _block: Option) -> RpcResult; + + #[method(name = "estimate_gas")] + fn estimate_gas(&self, request: CallRequest, _block: Option) -> RpcResult; + + #[method(name = "get_code")] + fn get_code(&self, address: Address, _block: Option) -> RpcResult; + + #[method(name = "get_block_by_number")] + fn get_block_by_number(&self, number: String) -> RpcResult>; } pub struct OckhamRpcImpl { storage: Arc, tx_pool: Arc, + executor: crate::vm::Executor, block_gas_limit: u64, broadcast_sender: tokio::sync::mpsc::Sender, } @@ -41,12 +67,14 @@ impl OckhamRpcImpl { pub fn new( storage: Arc, tx_pool: Arc, + executor: crate::vm::Executor, block_gas_limit: u64, broadcast_sender: tokio::sync::mpsc::Sender, ) -> Self { Self { storage, tx_pool, + executor, block_gas_limit, broadcast_sender, } @@ -132,6 +160,18 @@ impl OckhamRpcServer for OckhamRpcImpl { Ok(account.map(|a| a.balance).unwrap_or_default()) } + fn get_transaction_count(&self, address: Address) -> RpcResult { + let account = self.storage.get_account(&address).map_err(|e| { + jsonrpsee::types::ErrorObject::owned( + -32000, + format!("Storage error: {:?}", e), + None::<()>, + ) + })?; + + Ok(account.map(|a| a.nonce).unwrap_or_default()) + } + fn chain_id(&self) -> RpcResult { Ok(1337) // TODO: Config } @@ -186,4 +226,102 @@ impl OckhamRpcServer for OckhamRpcImpl { Ok(parent_base_fee.saturating_sub(base_fee_decrease)) } } + + fn call(&self, request: CallRequest, _block: Option) -> RpcResult { + let caller = request.from.unwrap_or_default(); + let value = request.value.unwrap_or_default(); + let data = request.data.unwrap_or_default(); + let gas = request.gas.unwrap_or(self.block_gas_limit); + + let (_, output) = self + .executor + .execute_ephemeral(caller, request.to, value, data, gas, vec![]) + .map_err(|e| { + jsonrpsee::types::ErrorObject::owned( + -32000, + format!("Execution Error: {:?}", e), + None::<()>, + ) + })?; + + Ok(crate::types::Bytes::from(output)) + } + + fn estimate_gas(&self, request: CallRequest, _block: Option) -> RpcResult { + let caller = request.from.unwrap_or_default(); + let value = request.value.unwrap_or_default(); + let data = request.data.unwrap_or_default(); + let gas = request.gas.unwrap_or(self.block_gas_limit); + + let (gas_used, _) = self + .executor + .execute_ephemeral(caller, request.to, value, data, gas, vec![]) + .map_err(|e| { + jsonrpsee::types::ErrorObject::owned( + -32000, + format!("Execution Error: {:?}", e), + None::<()>, + ) + })?; + + Ok(gas_used) + } + + fn get_code(&self, address: Address, _block: Option) -> RpcResult { + let account = self.storage.get_account(&address).map_err(|e| { + jsonrpsee::types::ErrorObject::owned( + -32000, + format!("Storage Error: {:?}", e), + None::<()>, + ) + })?; + + if let Some(info) = account { + if let Some(code) = info.code { + Ok(code) + } else if info.code_hash != Hash::default() { + let code = self + .storage + .get_code(&info.code_hash) + .map_err(|e| { + jsonrpsee::types::ErrorObject::owned( + -32000, + format!("Storage Error: {:?}", e), + None::<()>, + ) + })? + .unwrap_or_default(); + Ok(code) + } else { + Ok(crate::types::Bytes::default()) + } + } else { + Ok(crate::types::Bytes::default()) + } + } + + fn get_block_by_number(&self, number: String) -> RpcResult> { + let view = if number == "latest" { + if let Some(state) = self.storage.get_consensus_state().unwrap_or(None) { + state.preferred_view + } else { + return Ok(None); + } + } else if let Some(stripped) = number.strip_prefix("0x") { + u64::from_str_radix(stripped, 16).unwrap_or(0) + } else { + number.parse::().unwrap_or(0) + }; + + if let Some(qc) = self.storage.get_qc(view).map_err(|e| { + jsonrpsee::types::ErrorObject::owned(-32000, format!("{:?}", e), None::<()>) + })? { + let block = self.storage.get_block(&qc.block_hash).map_err(|e| { + jsonrpsee::types::ErrorObject::owned(-32000, format!("{:?}", e), None::<()>) + })?; + Ok(block) + } else { + Ok(None) + } + } } diff --git a/src/vm.rs b/src/vm.rs index 2b18426..a4770b6 100644 --- a/src/vm.rs +++ b/src/vm.rs @@ -549,4 +549,67 @@ impl Executor { Ok(()) } + + /// Execute a transaction ephemerally (no commit, for RPC 'call' and 'estimate_gas') + pub fn execute_ephemeral( + &self, + caller: Address, + to: Option
, + value: U256, + data: crate::types::Bytes, + gas_limit: u64, + _access_list: Vec, // Future proofing + ) -> Result<(u64, Vec), ExecutionError> { + let mut db = self.state.lock().unwrap(); + + // Setup EVM + let mut evm = EVM::new(); + evm.database(&mut *db); + + // Env setup (similar to execute_block but for single tx) + // We might need 'block' info for env.block, use default or current pending? + // For accurate simulation, we should use the 'pending' block context or 'latest'. + //db.get_consensus_state() gives us head. + // For now, use defaults for BlockEnv. + + let tx_env = &mut evm.env.tx; + tx_env.caller = caller; + tx_env.transact_to = if let Some(addr) = to { + TransactTo::Call(addr) + } else { + TransactTo::Create(CreateScheme::Create) + }; + tx_env.data = data; + tx_env.value = value; + tx_env.gas_limit = gas_limit; + tx_env.gas_price = U256::ZERO; // Simulation usually 0 or free + tx_env.gas_priority_fee = None; + tx_env.nonce = None; // Ignore nonce for simulation + + // Execute + let result_and_state = evm + .transact() + .map_err(|e| ExecutionError::Evm(format!("{:?}", e)))?; + + let result = result_and_state.result; + + match result { + ExecutionResult::Success { + gas_used, output, .. + } => { + let data = match output { + revm::primitives::Output::Call(b) => b.to_vec(), + revm::primitives::Output::Create(b, _) => b.to_vec(), + }; + Ok((gas_used, data)) + } + ExecutionResult::Revert { gas_used, output } => { + // For 'call', we often want the revert data too. + Ok((gas_used, output.to_vec())) + } + ExecutionResult::Halt { reason, .. } => { + Err(ExecutionError::Evm(format!("Halted: {:?}", reason))) + } + } + } } diff --git a/tests/rpc_test.rs b/tests/rpc_test.rs index d47d98a..f977ee4 100644 --- a/tests/rpc_test.rs +++ b/tests/rpc_test.rs @@ -24,10 +24,16 @@ async fn test_rpc_get_status() { storage.save_consensus_state(&state).unwrap(); let tx_pool = Arc::new(ockham::tx_pool::TxPool::new(storage.clone())); + let state_manager = Arc::new(std::sync::Mutex::new(ockham::state::StateManager::new( + storage.clone(), + None, + ))); + let executor = ockham::vm::Executor::new(state_manager, ockham::types::DEFAULT_BLOCK_GAS_LIMIT); let (tx_sender, _rx) = tokio::sync::mpsc::channel(100); let rpc = OckhamRpcImpl::new( storage, tx_pool, + executor, ockham::types::DEFAULT_BLOCK_GAS_LIMIT, tx_sender, ); @@ -82,10 +88,16 @@ async fn test_rpc_get_block() { storage.save_consensus_state(&state).unwrap(); let tx_pool = Arc::new(ockham::tx_pool::TxPool::new(storage.clone())); + let state_manager = Arc::new(std::sync::Mutex::new(ockham::state::StateManager::new( + storage.clone(), + None, + ))); + let executor = ockham::vm::Executor::new(state_manager, ockham::types::DEFAULT_BLOCK_GAS_LIMIT); let (tx_sender, _rx) = tokio::sync::mpsc::channel(100); let rpc = OckhamRpcImpl::new( storage, tx_pool, + executor, ockham::types::DEFAULT_BLOCK_GAS_LIMIT, tx_sender, ); @@ -124,3 +136,120 @@ async fn test_rpc_get_block() { // Let's just check it returns Ok for MVP. println!("Suggested Base Fee: {:?}", fee); } + +#[tokio::test] +async fn test_rpc_get_transaction_count() { + let storage = Arc::new(MemStorage::new()); + + // Create an account with a specific nonce + let (pk, _) = ockham::crypto::generate_keypair(); + let pk_bytes = pk.0.to_bytes(); + let hash = ockham::types::keccak256(pk_bytes); + let address = ockham::types::Address::from_slice(&hash[12..]); + + let account = ockham::storage::AccountInfo { + nonce: 42, + balance: ockham::types::U256::ZERO, + code_hash: ockham::crypto::Hash(ockham::types::keccak256([]).into()), + code: None, + }; + storage.save_account(&address, &account).unwrap(); + + let tx_pool = Arc::new(ockham::tx_pool::TxPool::new(storage.clone())); + let state_manager = Arc::new(std::sync::Mutex::new(ockham::state::StateManager::new( + storage.clone(), + None, + ))); + let executor = ockham::vm::Executor::new(state_manager, ockham::types::DEFAULT_BLOCK_GAS_LIMIT); + let (tx_sender, _rx) = tokio::sync::mpsc::channel(100); + let rpc = OckhamRpcImpl::new( + storage, + tx_pool, + executor, + ockham::types::DEFAULT_BLOCK_GAS_LIMIT, + tx_sender, + ); + + // Call RPC + let result = rpc.get_transaction_count(address); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 42); + + // Test non-existent account + let (pk2, _) = ockham::crypto::generate_keypair(); + let pk2_bytes = pk2.0.to_bytes(); + let hash2 = ockham::types::keccak256(pk2_bytes); + let address2 = ockham::types::Address::from_slice(&hash2[12..]); + + let result2 = rpc.get_transaction_count(address2); + assert!(result2.is_ok()); + assert_eq!(result2.unwrap(), 0); +} + +#[tokio::test] +async fn test_rpc_extended() { + let storage = Arc::new(MemStorage::new()); + // Setup Account with Code + let (pk, _) = ockham::crypto::generate_keypair(); + let pk_bytes = pk.0.to_bytes(); + let hash = ockham::types::keccak256(pk_bytes); + let address = ockham::types::Address::from_slice(&hash[12..]); + + let code_bytes = vec![0x60, 0x00, 0x60, 0x00, 0x53]; // PUSH1 00 PUSH1 00 MSTORE8 (Simple nonsense) + let code_hash = ockham::crypto::Hash(ockham::types::keccak256(&code_bytes).into()); + let code = ockham::types::Bytes::from(code_bytes.clone()); + + let account = ockham::storage::AccountInfo { + nonce: 1, + balance: ockham::types::U256::from(100), + code_hash, + code: Some(code.clone()), + }; + storage.save_account(&address, &account).unwrap(); + storage.save_code(&code_hash, &code).unwrap(); + + let tx_pool = Arc::new(ockham::tx_pool::TxPool::new(storage.clone())); + let state_manager = Arc::new(std::sync::Mutex::new(ockham::state::StateManager::new( + storage.clone(), + None, + ))); + let executor = ockham::vm::Executor::new(state_manager, ockham::types::DEFAULT_BLOCK_GAS_LIMIT); + let (tx_sender, _rx) = tokio::sync::mpsc::channel(100); + let rpc = OckhamRpcImpl::new( + storage, + tx_pool, + executor, + ockham::types::DEFAULT_BLOCK_GAS_LIMIT, + tx_sender, + ); + + // 1. get_code + let res_code = rpc.get_code(address, None); + assert!(res_code.is_ok()); + assert_eq!(res_code.unwrap(), code); + + // 2. call (to the account) + let request = ockham::rpc::CallRequest { + from: None, + to: Some(address), + gas: Some(100000), + gas_price: None, + value: None, + data: None, + }; + let res_call = rpc.call(request, None); + assert!(res_call.is_ok()); + + // 3. estimate_gas + let request_est = ockham::rpc::CallRequest { + from: None, + to: Some(address), + gas: None, + gas_price: None, + value: None, + data: None, + }; + let res_est = rpc.estimate_gas(request_est, None); + assert!(res_est.is_ok()); + println!("Estimated Gas: {}", res_est.unwrap()); +}