From 7d133f2b6e4a541517ba876a7b50e77da7de4ca8 Mon Sep 17 00:00:00 2001 From: Wei Tang Date: Wed, 8 Nov 2023 02:32:00 +0100 Subject: [PATCH] Implement standard invoker (#207) --- interpreter/Cargo.toml | 2 + interpreter/src/call_create.rs | 88 ++++++-- interpreter/src/lib.rs | 4 +- interpreter/src/runtime.rs | 26 +++ src/call_stack.rs | 12 +- src/gasometer.rs | 2 - src/invoker.rs | 14 +- src/standard/gasometer/mod.rs | 19 +- src/standard/invoker.rs | 383 +++++++++++++++++++++++++++++++++ src/standard/mod.rs | 5 + 10 files changed, 517 insertions(+), 38 deletions(-) create mode 100644 src/standard/invoker.rs diff --git a/interpreter/Cargo.toml b/interpreter/Cargo.toml index 8e8012f93..857eeb85b 100644 --- a/interpreter/Cargo.toml +++ b/interpreter/Cargo.toml @@ -17,6 +17,7 @@ serde = { version = "1.0", default-features = false, features = ["derive"], opti bytes = { version = "1.5", default-features = false } auto_impl = "1.0" sha3 = { version = "0.10", default-features = false } +rlp = { version = "0.5", default-features = false } [dev-dependencies] hex = "0.4" @@ -29,6 +30,7 @@ std = [ "scale-codec/std", "scale-info/std", "bytes/std", + "rlp/std", ] with-codec = [ "scale-codec", diff --git a/interpreter/src/call_create.rs b/interpreter/src/call_create.rs index 2e4226d3f..9e844c0de 100644 --- a/interpreter/src/call_create.rs +++ b/interpreter/src/call_create.rs @@ -1,5 +1,8 @@ use crate::utils::{h256_to_u256, u256_to_usize}; -use crate::{Context, ExitError, ExitException, ExitResult, Machine, Memory, RuntimeState}; +use crate::{ + Context, ExitError, ExitException, ExitResult, Machine, Memory, Opcode, RuntimeFullBackend, + RuntimeState, Transfer, +}; use core::cmp::{max, min}; use primitive_types::{H160, H256, U256}; use sha3::{Digest, Keccak256}; @@ -21,8 +24,39 @@ pub enum CreateScheme { /// Salt. salt: H256, }, - /// Create at a fixed location. - Fixed(H160), +} + +impl CreateScheme { + pub fn address(&self, handler: &H) -> H160 { + match self { + CreateScheme::Create2 { + caller, + code_hash, + salt, + } => { + let mut hasher = Keccak256::new(); + hasher.update([0xff]); + hasher.update(&caller[..]); + hasher.update(&salt[..]); + hasher.update(&code_hash[..]); + H256::from_slice(hasher.finalize().as_slice()).into() + } + CreateScheme::Legacy { caller } => { + let nonce = handler.nonce(*caller); + let mut stream = rlp::RlpStream::new_list(2); + stream.append(caller); + stream.append(&nonce); + H256::from_slice(Keccak256::digest(&stream.out()).as_slice()).into() + } + } + } + + pub fn caller(&self) -> H160 { + match self { + Self::Create2 { caller, .. } => *caller, + Self::Legacy { caller } => *caller, + } + } } /// Call scheme. @@ -38,22 +72,47 @@ pub enum CallScheme { StaticCall, } -/// Transfer from source to target, with given value. -#[derive(Clone, Debug)] -pub struct Transfer { - /// Source address. - pub source: H160, - /// Target address. - pub target: H160, - /// Transfer value. - pub value: U256, -} - pub enum CallCreateTrapData { Call(CallTrapData), Create(CreateTrapData), } +impl CallCreateTrapData { + pub fn target_gas(&self) -> Option { + match self { + Self::Call(CallTrapData { gas, .. }) => Some(*gas), + Self::Create(_) => None, + } + } + + pub fn new_from + AsMut>( + opcode: Opcode, + machine: &mut Machine, + ) -> Result { + match opcode { + Opcode::CREATE => Ok(Self::Create(CreateTrapData::new_create_from(machine)?)), + Opcode::CREATE2 => Ok(Self::Create(CreateTrapData::new_create2_from(machine)?)), + Opcode::CALL => Ok(Self::Call(CallTrapData::new_from( + CallScheme::Call, + machine, + )?)), + Opcode::CALLCODE => Ok(Self::Call(CallTrapData::new_from( + CallScheme::CallCode, + machine, + )?)), + Opcode::DELEGATECALL => Ok(Self::Call(CallTrapData::new_from( + CallScheme::DelegateCall, + machine, + )?)), + Opcode::STATICCALL => Ok(Self::Call(CallTrapData::new_from( + CallScheme::StaticCall, + machine, + )?)), + _ => Err(ExitException::InvalidOpcode(opcode).into()), + } + } +} + pub struct CallTrapData { pub target: H160, pub transfer: Option, @@ -254,6 +313,7 @@ impl CallTrapData { } } +#[derive(Clone, Debug)] pub struct CreateTrapData { pub scheme: CreateScheme, pub value: U256, diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs index 68fa94711..f563b6c81 100644 --- a/interpreter/src/lib.rs +++ b/interpreter/src/lib.rs @@ -20,7 +20,9 @@ pub use crate::error::{Capture, ExitError, ExitException, ExitFatal, ExitResult, pub use crate::eval::{Control, Efn, Etable}; pub use crate::memory::Memory; pub use crate::opcode::Opcode; -pub use crate::runtime::{CallCreateTrap, Context, RuntimeBackend, RuntimeState}; +pub use crate::runtime::{ + CallCreateTrap, Context, RuntimeBackend, RuntimeFullBackend, RuntimeState, Transfer, +}; pub use crate::stack::Stack; pub use crate::valids::Valids; diff --git a/interpreter/src/runtime.rs b/interpreter/src/runtime.rs index 5a64bfb29..8dd6a9044 100644 --- a/interpreter/src/runtime.rs +++ b/interpreter/src/runtime.rs @@ -35,6 +35,17 @@ pub struct Context { pub apparent_value: U256, } +/// Transfer from source to target, with given value. +#[derive(Clone, Debug)] +pub struct Transfer { + /// Source address. + pub source: H160, + /// Target address. + pub target: H160, + /// Transfer value. + pub value: U256, +} + pub trait CallCreateTrap: Sized { fn call_create_trap(opcode: Opcode) -> Self; } @@ -98,3 +109,18 @@ pub trait RuntimeBackend { /// Mark an address to be deleted, with funds transferred to target. fn mark_delete(&mut self, address: H160, target: H160) -> Result<(), ExitError>; } + +pub trait RuntimeFullBackend: RuntimeBackend { + /// Get the current nonce of an account. + fn nonce(&self, address: H160) -> U256; + /// Fully delete storages of an account. + fn reset_storage(&mut self, address: H160); + /// Set code of an account. + fn set_code(&mut self, address: H160, code: Vec); + /// Reset balance of an account. + fn reset_balance(&mut self, address: H160); + /// Initiate a transfer. + fn transfer(&mut self, transfer: Transfer) -> Result<(), ExitError>; + /// Increase the nonce value. + fn inc_nonce(&mut self, address: H160) -> Result<(), ExitError>; +} diff --git a/src/call_stack.rs b/src/call_stack.rs index d6d97bf9e..de1103a74 100644 --- a/src/call_stack.rs +++ b/src/call_stack.rs @@ -23,7 +23,7 @@ enum LastCallStackData { } pub struct CallStack<'backend, 'invoker, S, G, H, Tr, I: Invoker> { - stack: Vec>, + stack: Vec>, last: LastCallStackData, initial_depth: usize, backend: &'backend mut H, @@ -154,8 +154,8 @@ where self.initial_depth + self.stack.len() + 1, ) { Capture::Exit(Ok(trap_data)) => { - match self.invoker.enter_trap_stack(&trap_data, self.backend) { - Ok(sub_machine) => { + match self.invoker.enter_trap_stack(trap_data, self.backend) { + Ok((trap_data, sub_machine)) => { self.stack.push(TrappedCallStackData { trap_data, machine }); LastCallStackData::Running { @@ -207,14 +207,14 @@ where Capture::Exit(exit) => return (machine, exit), Capture::Trap(trap) => { let prepared_trap_data: Capture< - Result, + Result, Infallible, > = invoker.prepare_trap(trap, &mut machine, backend, initial_depth + 1); match prepared_trap_data { Capture::Exit(Ok(trap_data)) => { - match invoker.enter_trap_stack(&trap_data, backend) { - Ok(sub_machine) => { + match invoker.enter_trap_stack(trap_data, backend) { + Ok((trap_data, sub_machine)) => { let (sub_machine, sub_result) = if heap_depth .map(|hd| initial_depth + 1 >= hd) .unwrap_or(false) diff --git a/src/gasometer.rs b/src/gasometer.rs index 673687fe0..404936c2b 100644 --- a/src/gasometer.rs +++ b/src/gasometer.rs @@ -25,9 +25,7 @@ pub enum GasometerMergeStrategy { pub trait Gasometer: Sized { type Gas: Gas; - type Config; - fn new(gas_limit: Self::Gas, machine: &Machine, config: Self::Config) -> Self; fn record_stepn( &mut self, machine: &Machine, diff --git a/src/invoker.rs b/src/invoker.rs index f47b8f69f..a0da37b6c 100644 --- a/src/invoker.rs +++ b/src/invoker.rs @@ -2,28 +2,32 @@ use crate::{Capture, ExitError, ExitResult, GasedMachine}; pub trait Invoker { type Interrupt; - type CallCreateTrapData; + type CallCreateTrapPrepareData; + type CallCreateTrapEnterData; fn exit_trap_stack( &self, result: ExitResult, child: GasedMachine, - trap_data: Self::CallCreateTrapData, + trap_data: Self::CallCreateTrapEnterData, parent: &mut GasedMachine, handler: &mut H, ) -> Result<(), ExitError>; + /// The separation of `prepare_trap` and `enter_trap_stack` is to give an opportunity for the + /// trait to return `Self::Interrupt`. When `Self::Interrupt` is `Infallible`, there's no + /// difference whether a code is in `prepare_trap` or `enter_trap_stack`. fn prepare_trap( &self, trap: Tr, machine: &mut GasedMachine, handler: &mut H, depth: usize, - ) -> Capture, Self::Interrupt>; + ) -> Capture, Self::Interrupt>; fn enter_trap_stack( &self, - trap_data: &Self::CallCreateTrapData, + trap_data: Self::CallCreateTrapPrepareData, handler: &mut H, - ) -> Result, ExitError>; + ) -> Result<(Self::CallCreateTrapEnterData, GasedMachine), ExitError>; } diff --git a/src/standard/gasometer/mod.rs b/src/standard/gasometer/mod.rs index 3e8bf18a0..e4df8e760 100644 --- a/src/standard/gasometer/mod.rs +++ b/src/standard/gasometer/mod.rs @@ -48,7 +48,7 @@ impl<'config> Gasometer<'config> { } /// Record an explicit cost. - fn record_cost_nocleanup(&mut self, cost: u64) -> Result<(), ExitError> { + pub fn record_cost(&mut self, cost: u64) -> Result<(), ExitError> { let all_gas_cost = self.total_used_gas() + cost; if self.gas_limit < all_gas_cost { Err(ExitException::OutOfGas.into()) @@ -57,13 +57,8 @@ impl<'config> Gasometer<'config> { Ok(()) } } -} - -impl<'config, S: AsRef, H: RuntimeBackend> GasometerT for Gasometer<'config> { - type Gas = u64; - type Config = &'config Config; - fn new(gas_limit: u64, _machine: &Machine, config: &'config Config) -> Self { + pub fn new(gas_limit: u64, _machine: &Machine, config: &'config Config) -> Self { Self { gas_limit, memory_gas: 0, @@ -72,6 +67,10 @@ impl<'config, S: AsRef, H: RuntimeBackend> GasometerT for Ga config, } } +} + +impl<'config, S: AsRef, H: RuntimeBackend> GasometerT for Gasometer<'config> { + type Gas = u64; fn record_stepn( &mut self, @@ -83,7 +82,7 @@ impl<'config, S: AsRef, H: RuntimeBackend> GasometerT for Ga let opcode = machine.peek_opcode().ok_or(ExitException::OutOfGas)?; if let Some(cost) = consts::STATIC_COST_TABLE[opcode.as_usize()] { - gasometer.record_cost_nocleanup(cost)?; + gasometer.record_cost(cost)?; } else { let address = machine.state.as_ref().context.address; let (gas, memory_gas) = dynamic_opcode_cost( @@ -97,7 +96,7 @@ impl<'config, S: AsRef, H: RuntimeBackend> GasometerT for Ga let cost = gas.cost(gasometer.gas(), gasometer.config)?; let refund = gas.refund(gasometer.config); - gasometer.record_cost_nocleanup(cost)?; + gasometer.record_cost(cost)?; if refund >= 0 { gasometer.refunded_gas += refund as u64; } else { @@ -121,7 +120,7 @@ impl<'config, S: AsRef, H: RuntimeBackend> GasometerT for Ga fn record_codedeposit(&mut self, len: usize) -> Result<(), ExitError> { self.perform(|gasometer| { let cost = len as u64 * consts::G_CODEDEPOSIT; - gasometer.record_cost_nocleanup(cost)?; + gasometer.record_cost(cost)?; Ok(()) }) } diff --git a/src/standard/invoker.rs b/src/standard/invoker.rs new file mode 100644 index 000000000..ba74a6566 --- /dev/null +++ b/src/standard/invoker.rs @@ -0,0 +1,383 @@ +use super::{Config, GasedMachine, Gasometer, Machine}; +use crate::call_create::{CallCreateTrapData, CallTrapData, CreateTrapData}; +use crate::{ + Capture, Context, ExitError, ExitException, ExitResult, Gasometer as GasometerT, + GasometerMergeStrategy, Invoker as InvokerT, Opcode, RuntimeFullBackend, RuntimeState, + TransactionalBackend, TransactionalMergeStrategy, Transfer, +}; +use alloc::rc::Rc; +use core::cmp::min; +use core::convert::Infallible; +use primitive_types::{H160, U256}; + +pub enum CallCreateTrapPrepareData { + Call { + gas_limit: u64, + is_static: bool, + trap: CallTrapData, + }, + Create { + gas_limit: u64, + is_static: bool, + trap: CreateTrapData, + }, +} + +pub enum CallCreateTrapEnterData { + Call { trap: CallTrapData }, + Create { trap: CreateTrapData, address: H160 }, +} + +pub struct Invoker<'config> { + config: &'config Config, +} + +impl<'config, H> InvokerT, H, Opcode> for Invoker<'config> +where + H: RuntimeFullBackend + TransactionalBackend, +{ + type Interrupt = Infallible; + type CallCreateTrapPrepareData = CallCreateTrapPrepareData; + type CallCreateTrapEnterData = CallCreateTrapEnterData; + + fn prepare_trap( + &self, + opcode: Opcode, + machine: &mut GasedMachine<'config>, + _handler: &mut H, + depth: usize, + ) -> Capture, Infallible> { + fn l64(gas: u64) -> u64 { + gas - gas / 64 + } + + if depth >= self.config.call_stack_limit { + return Capture::Exit(Err(ExitException::CallTooDeep.into())); + } + + let trap_data = match CallCreateTrapData::new_from(opcode, &mut machine.machine) { + Ok(trap_data) => trap_data, + Err(err) => return Capture::Exit(Err(err)), + }; + + let after_gas = U256::from(if self.config.call_l64_after_gas { + l64(machine.gasometer.gas()) + } else { + machine.gasometer.gas() + }); + let target_gas = trap_data.target_gas().unwrap_or(after_gas); + let gas_limit = min(after_gas, target_gas); + + let gas_limit = if gas_limit > U256::from(u64::MAX) { + return Capture::Exit(Err(ExitException::OutOfGas.into())); + } else { + gas_limit.as_u64() + }; + + match machine.gasometer.record_cost(gas_limit) { + Ok(()) => (), + Err(err) => return Capture::Exit(Err(err)), + } + + let is_static = if machine.is_static { + true + } else { + match &trap_data { + CallCreateTrapData::Call(CallTrapData { is_static, .. }) => *is_static, + _ => false, + } + }; + + Capture::Exit(Ok(match trap_data { + CallCreateTrapData::Call(call_trap_data) => CallCreateTrapPrepareData::Call { + gas_limit, + is_static, + trap: call_trap_data, + }, + CallCreateTrapData::Create(create_trap_data) => CallCreateTrapPrepareData::Create { + gas_limit, + is_static, + trap: create_trap_data, + }, + })) + } + + fn enter_trap_stack( + &self, + trap_data: Self::CallCreateTrapPrepareData, + handler: &mut H, + ) -> Result<(Self::CallCreateTrapEnterData, GasedMachine<'config>), ExitError> { + match trap_data { + CallCreateTrapPrepareData::Create { + gas_limit, + is_static, + trap, + } => enter_create_trap_stack(gas_limit, is_static, trap, handler, self.config), + CallCreateTrapPrepareData::Call { + gas_limit, + is_static, + trap, + } => enter_call_trap_stack(gas_limit, is_static, trap, handler, self.config), + } + } + + fn exit_trap_stack( + &self, + result: ExitResult, + mut child: GasedMachine<'config>, + trap_data: Self::CallCreateTrapEnterData, + parent: &mut GasedMachine<'config>, + handler: &mut H, + ) -> Result<(), ExitError> { + match trap_data { + CallCreateTrapEnterData::Create { address, trap } => { + let retbuf = child.machine.into_retbuf(); + let result = exit_create_trap_stack_no_exit_substate( + result.map(|_| address), + &retbuf, + &mut child.gasometer, + handler, + self.config, + ); + + match &result { + Ok(_) => { + handler.pop_substate(TransactionalMergeStrategy::Commit); + GasometerT::::merge( + &mut parent.gasometer, + child.gasometer, + GasometerMergeStrategy::Commit, + ); + } + Err(ExitError::Reverted) => { + handler.pop_substate(TransactionalMergeStrategy::Discard); + GasometerT::::merge( + &mut parent.gasometer, + child.gasometer, + GasometerMergeStrategy::Revert, + ); + } + Err(_) => { + handler.pop_substate(TransactionalMergeStrategy::Discard); + } + }; + + trap.feedback(result, retbuf, &mut parent.machine)?; + + Ok(()) + } + CallCreateTrapEnterData::Call { trap } => { + let retbuf = child.machine.into_retbuf(); + + match &result { + Ok(_) => { + handler.pop_substate(TransactionalMergeStrategy::Commit); + GasometerT::::merge( + &mut parent.gasometer, + child.gasometer, + GasometerMergeStrategy::Commit, + ); + } + Err(ExitError::Reverted) => { + handler.pop_substate(TransactionalMergeStrategy::Discard); + GasometerT::::merge( + &mut parent.gasometer, + child.gasometer, + GasometerMergeStrategy::Revert, + ); + } + Err(_) => { + handler.pop_substate(TransactionalMergeStrategy::Discard); + } + }; + + trap.feedback(result, retbuf, &mut parent.machine)?; + + Ok(()) + } + } + } +} + +fn enter_create_trap_stack<'config, H>( + gas_limit: u64, + is_static: bool, + trap_data: CreateTrapData, + handler: &mut H, + config: &'config Config, +) -> Result<(CallCreateTrapEnterData, GasedMachine<'config>), ExitError> +where + H: RuntimeFullBackend + TransactionalBackend, +{ + handler.push_substate(); + + let work = || -> Result<(CallCreateTrapEnterData, GasedMachine<'config>), ExitError> { + let CreateTrapData { + scheme, + value, + code, + } = trap_data.clone(); + + let caller = scheme.caller(); + let address = scheme.address(handler); + + handler.mark_hot(caller, None)?; + handler.mark_hot(address, None)?; + + if handler.balance(caller) < value { + return Err(ExitException::OutOfFund.into()); + } + + handler.inc_nonce(caller)?; + + if handler.code_size(address) != U256::zero() || handler.nonce(address) > U256::zero() { + return Err(ExitException::CreateCollision.into()); + } + + handler.reset_storage(address); + + let context = Context { + address, + caller, + apparent_value: value, + }; + + let transfer = Transfer { + source: caller, + target: address, + value, + }; + + handler.transfer(transfer)?; + + if config.create_increase_nonce { + handler.inc_nonce(address)?; + } + + let machine = Machine::new( + Rc::new(code), + Rc::new(Vec::new()), + config.stack_limit, + config.memory_limit, + RuntimeState { + context, + retbuf: Vec::new(), + gas: U256::zero(), + }, + ); + + let gasometer = Gasometer::new(gas_limit, &machine, config); + + Ok(( + CallCreateTrapEnterData::Create { + address, + trap: trap_data, + }, + GasedMachine { + machine, + gasometer, + is_static, + }, + )) + }; + + match work() { + Ok(machine) => Ok(machine), + Err(err) => { + handler.pop_substate(TransactionalMergeStrategy::Discard); + Err(err) + } + } +} + +fn exit_create_trap_stack_no_exit_substate<'config, H>( + result: Result, + retbuf: &Vec, + gasometer: &mut Gasometer<'config>, + handler: &mut H, + config: &'config Config, +) -> Result +where + H: RuntimeFullBackend + TransactionalBackend, +{ + fn check_first_byte(config: &Config, code: &[u8]) -> Result<(), ExitError> { + if config.disallow_executable_format && Some(&Opcode::EOFMAGIC.as_u8()) == code.first() { + return Err(ExitException::InvalidOpcode(Opcode::EOFMAGIC).into()); + } + Ok(()) + } + + let address = result?; + check_first_byte(config, &retbuf[..])?; + + if let Some(limit) = config.create_contract_limit { + if retbuf.len() > limit { + return Err(ExitException::CreateContractLimit.into()); + } + } + + GasometerT::::record_codedeposit(gasometer, retbuf.len())?; + + handler.set_code(address, retbuf.clone()); + + Ok(address) +} + +fn enter_call_trap_stack<'config, H>( + mut gas_limit: u64, + is_static: bool, + trap_data: CallTrapData, + handler: &mut H, + config: &'config Config, +) -> Result<(CallCreateTrapEnterData, GasedMachine<'config>), ExitError> +where + H: RuntimeFullBackend + TransactionalBackend, +{ + handler.push_substate(); + + let work = || -> Result<(CallCreateTrapEnterData, GasedMachine<'config>), ExitError> { + handler.mark_hot(trap_data.context.address, None)?; + let code = handler.code(trap_data.target); + + if let Some(transfer) = trap_data.transfer.clone() { + if transfer.value != U256::zero() { + gas_limit = gas_limit.saturating_add(config.call_stipend); + } + + handler.transfer(transfer)?; + } + + // TODO: precompile contracts. + + let machine = Machine::new( + Rc::new(code), + Rc::new(trap_data.input.clone()), + config.stack_limit, + config.memory_limit, + RuntimeState { + context: trap_data.context.clone(), + retbuf: Vec::new(), + gas: U256::zero(), + }, + ); + + let gasometer = Gasometer::new(gas_limit, &machine, config); + + Ok(( + CallCreateTrapEnterData::Call { trap: trap_data }, + GasedMachine { + machine, + gasometer, + is_static, + }, + )) + }; + + match work() { + Ok(machine) => Ok(machine), + Err(err) => { + handler.pop_substate(TransactionalMergeStrategy::Discard); + Err(err) + } + } +} diff --git a/src/standard/mod.rs b/src/standard/mod.rs index 979ed263b..18afd3a3d 100644 --- a/src/standard/mod.rs +++ b/src/standard/mod.rs @@ -1,7 +1,12 @@ mod config; mod gasometer; +mod invoker; pub use self::config::Config; pub use self::gasometer::Gasometer; +pub use self::invoker::Invoker; pub type Machine = crate::Machine; +pub type Efn = crate::Efn; +pub type Etable> = crate::Etable; +pub type GasedMachine<'config> = crate::GasedMachine>;