//! Execution context for contract calls. //! //! The context provides contracts with information about the current //! execution environment (caller, value, block info, etc.). use synor_types::{Address, Hash256, Network}; use crate::gas::GasMeter; use crate::storage::{ContractStorage, MemoryStorage}; use crate::ContractId; /// Block information available to contracts. #[derive(Clone, Debug)] pub struct BlockInfo { /// Block height. pub height: u64, /// Block timestamp (Unix milliseconds). pub timestamp: u64, /// Block hash (of selected parent). pub hash: Hash256, /// Blue score. pub blue_score: u64, /// DAA score (for difficulty). pub daa_score: u64, /// Coinbase address. pub coinbase: Address, } impl Default for BlockInfo { fn default() -> Self { BlockInfo { height: 0, timestamp: 0, hash: Hash256::default(), blue_score: 0, daa_score: 0, coinbase: Address::from_ed25519_pubkey(Network::Mainnet, &[0; 32]), } } } /// Transaction information. #[derive(Clone, Debug)] pub struct TransactionInfo { /// Transaction hash. pub hash: Hash256, /// Transaction origin (original sender). pub origin: Address, /// Gas price. pub gas_price: u64, /// Gas limit. pub gas_limit: u64, } impl Default for TransactionInfo { fn default() -> Self { TransactionInfo { hash: Hash256::default(), origin: Address::from_ed25519_pubkey(Network::Mainnet, &[0; 32]), gas_price: 1, gas_limit: 10_000_000, } } } /// Call-specific context. #[derive(Clone, Debug)] pub struct CallContext { /// The contract being called. pub contract: ContractId, /// The caller (previous contract or EOA). pub caller: Address, /// Value sent with the call. pub value: u64, /// Input data (method + args). pub input: Vec, /// Whether this is a delegate call. pub is_delegate: bool, /// Whether this is a static call (read-only). pub is_static: bool, /// Call depth. pub depth: u32, } impl CallContext { /// Creates a new call context. pub fn new(contract: ContractId, caller: Address, value: u64, input: Vec) -> Self { CallContext { contract, caller, value, input, is_delegate: false, is_static: false, depth: 0, } } /// Creates a static (read-only) call context. pub fn static_call(contract: ContractId, caller: Address, input: Vec) -> Self { CallContext { contract, caller, value: 0, input, is_delegate: false, is_static: true, depth: 0, } } /// Creates a delegate call context. pub fn delegate_call( contract: ContractId, caller: Address, value: u64, input: Vec, ) -> Self { CallContext { contract, caller, value, input, is_delegate: true, is_static: false, depth: 0, } } /// Increments call depth for nested calls. pub fn nested(&self, contract: ContractId, value: u64, input: Vec) -> Self { CallContext { contract, // Current contract becomes caller - use contract hash as "address" caller: Address::from_ed25519_pubkey(Network::Mainnet, self.contract.as_bytes()), value, input, is_delegate: false, is_static: self.is_static, // Inherit static flag depth: self.depth + 1, } } } /// Full execution context. pub struct ExecutionContext { /// Block information. pub block: BlockInfo, /// Transaction information. pub tx: TransactionInfo, /// Current call context. pub call: CallContext, /// Gas meter. pub gas: GasMeter, /// Contract storage. pub storage: S, /// Return data from last call. pub return_data: Vec, /// Logs accumulated. pub logs: Vec, /// Chain ID. pub chain_id: u64, } impl ExecutionContext { /// Creates a new execution context. pub fn new( block: BlockInfo, tx: TransactionInfo, call: CallContext, gas_limit: u64, storage: S, chain_id: u64, ) -> Self { ExecutionContext { block, tx, call, gas: GasMeter::new(gas_limit), storage, return_data: Vec::new(), logs: Vec::new(), chain_id, } } /// Creates a context for testing. pub fn testing(call: CallContext) -> ExecutionContext { ExecutionContext { block: BlockInfo::default(), tx: TransactionInfo::default(), call, gas: GasMeter::new(10_000_000), storage: MemoryStorage::new(), return_data: Vec::new(), logs: Vec::new(), chain_id: 1, } } /// Gets the current contract address as Address. pub fn address(&self) -> Address { // Contract address derived from contract ID hash Address::from_ed25519_pubkey(Network::Mainnet, self.call.contract.as_bytes()) } /// Gets the caller address. pub fn caller(&self) -> Address { self.call.caller.clone() } /// Gets the transaction origin. pub fn origin(&self) -> Address { self.tx.origin.clone() } /// Gets the call value. pub fn value(&self) -> u64 { self.call.value } /// Gets the call input data. pub fn calldata(&self) -> &[u8] { &self.call.input } /// Gets the call data size. pub fn calldatasize(&self) -> usize { self.call.input.len() } /// Gets remaining gas. pub fn gas_remaining(&self) -> u64 { self.gas.remaining() } /// Gets the block timestamp. pub fn timestamp(&self) -> u64 { self.block.timestamp } /// Gets the block height. pub fn block_height(&self) -> u64 { self.block.height } /// Gets the block hash. pub fn block_hash(&self) -> Hash256 { self.block.hash } /// Checks if this is a static call. pub fn is_static(&self) -> bool { self.call.is_static } /// Checks if state modifications are allowed. pub fn can_modify_state(&self) -> bool { !self.call.is_static } /// Sets the return data. pub fn set_return_data(&mut self, data: Vec) { self.return_data = data; } /// Gets the last return data. pub fn get_return_data(&self) -> &[u8] { &self.return_data } /// Emits a log. pub fn emit_log(&mut self, topics: Vec, data: Vec) -> Result<(), crate::VmError> { if self.is_static() { return Err(crate::VmError::ExecutionError( "Cannot emit logs in static call".into(), )); } // Charge gas self.gas .charge_log(data.len(), topics.len()) .map_err(|_e| crate::VmError::OutOfGas { used: self.gas.consumed(), limit: self.gas.limit(), })?; self.logs.push(crate::ContractLog { contract: self.call.contract, topics, data, }); Ok(()) } /// Creates a nested context for a sub-call. pub fn nested_call( &self, target: ContractId, value: u64, input: Vec, gas_limit: u64, ) -> Result, crate::VmError> { if self.call.depth >= crate::MAX_CALL_DEPTH { return Err(crate::VmError::CallDepthExceeded(self.call.depth + 1)); } let nested_call = self.call.nested(target, value, input); Ok(ExecutionContext { block: self.block.clone(), tx: self.tx.clone(), call: nested_call, gas: GasMeter::new(gas_limit), storage: MemoryStorage::new(), // Nested calls get fresh storage view return_data: Vec::new(), logs: Vec::new(), chain_id: self.chain_id, }) } } /// Builder for execution contexts. pub struct ContextBuilder { block: BlockInfo, tx: TransactionInfo, call: Option, gas_limit: u64, storage: Option, chain_id: u64, } impl ContextBuilder { /// Creates a new context builder. pub fn new() -> Self { ContextBuilder { block: BlockInfo::default(), tx: TransactionInfo::default(), call: None, gas_limit: 10_000_000, storage: None, chain_id: 1, } } /// Sets block info. pub fn block(mut self, block: BlockInfo) -> Self { self.block = block; self } /// Sets transaction info. pub fn transaction(mut self, tx: TransactionInfo) -> Self { self.tx = tx; self } /// Sets call context. pub fn call(mut self, call: CallContext) -> Self { self.call = Some(call); self } /// Sets gas limit. pub fn gas_limit(mut self, limit: u64) -> Self { self.gas_limit = limit; self } /// Sets storage. pub fn storage(mut self, storage: S) -> Self { self.storage = Some(storage); self } /// Sets chain ID. pub fn chain_id(mut self, chain_id: u64) -> Self { self.chain_id = chain_id; self } /// Builds the context. pub fn build(self) -> Result, &'static str> { let call = self.call.ok_or("Call context is required")?; let storage = self.storage.ok_or("Storage is required")?; Ok(ExecutionContext::new( self.block, self.tx, call, self.gas_limit, storage, self.chain_id, )) } } impl Default for ContextBuilder { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use crate::storage::MemoryStorage; use synor_types::Network; fn test_contract_id() -> ContractId { ContractId::from_bytes([0x42; 32]) } fn test_address() -> Address { Address::from_ed25519_pubkey(Network::Mainnet, &[0x11; 32]) } #[test] fn test_call_context() { let contract = test_contract_id(); let caller = test_address(); let ctx = CallContext::new(contract, caller, 1000, vec![1, 2, 3]); assert_eq!(ctx.value, 1000); assert_eq!(ctx.depth, 0); assert!(!ctx.is_static); } #[test] fn test_nested_call() { let contract = test_contract_id(); let caller = test_address(); let ctx = CallContext::new(contract, caller, 1000, vec![]); let nested = ctx.nested(ContractId::from_bytes([0x99; 32]), 500, vec![4, 5, 6]); assert_eq!(nested.depth, 1); assert_eq!(nested.value, 500); } #[test] fn test_execution_context() { let contract = test_contract_id(); let caller = test_address(); let call = CallContext::new(contract, caller, 1000, vec![1, 2, 3]); let ctx = ExecutionContext::::testing(call); assert_eq!(ctx.value(), 1000); assert_eq!(ctx.calldatasize(), 3); assert!(ctx.gas_remaining() > 0); } #[test] fn test_static_call() { let contract = test_contract_id(); let caller = test_address(); let call = CallContext::static_call(contract, caller, vec![]); let ctx = ExecutionContext::::testing(call); assert!(ctx.is_static()); assert!(!ctx.can_modify_state()); } }