diff --git a/Cargo.lock b/Cargo.lock index 9f1ac7838e..5c2f24a29e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1137,7 +1137,10 @@ checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" dependencies = [ "ethereum-types", "hex", + "once_cell", + "regex", "serde", + "serde_json", "sha3 0.10.8", "thiserror", "uint", @@ -1153,6 +1156,7 @@ dependencies = [ "fixed-hash", "impl-codec", "impl-rlp", + "impl-serde", "scale-info", "tiny-keccak 2.0.2", ] @@ -1185,6 +1189,7 @@ dependencies = [ "fixed-hash", "impl-codec", "impl-rlp", + "impl-serde", "primitive-types", "scale-info", "uint", @@ -1663,6 +1668,15 @@ dependencies = [ "rlp", ] +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + [[package]] name = "impl-trait-for-tuples" version = "0.2.2" @@ -2755,6 +2769,7 @@ dependencies = [ "impl-codec", "impl-num-traits", "impl-rlp", + "impl-serde", "scale-info", "uint", ] diff --git a/client-sdk/go/types/address.go b/client-sdk/go/types/address.go index b8d5909f34..e654938411 100644 --- a/client-sdk/go/types/address.go +++ b/client-sdk/go/types/address.go @@ -205,3 +205,8 @@ func NewAddressFromConsensus(addr staking.Address) Address { func NewAddressFromConsensusPublicKey(pk signature.PublicKey) Address { return NewAddress(NewSignatureAddressSpecEd25519(ed25519.PublicKey(pk))) } + +// NewAddressFromEth creates a new address from an Eth-compatible address. +func NewAddressFromEth(ethAddress []byte) Address { + return NewAddressRaw(AddressV0Secp256k1EthContext, ethAddress) +} diff --git a/client-sdk/go/types/address_test.go b/client-sdk/go/types/address_test.go index 0b54d96b85..8f50a326c0 100644 --- a/client-sdk/go/types/address_test.go +++ b/client-sdk/go/types/address_test.go @@ -68,3 +68,17 @@ func TestAddressRaw(t *testing.T) { addr := NewAddressRaw(AddressV0Secp256k1EthContext, ethAddress) require.EqualValues("oasis1qrk58a6j2qn065m6p06jgjyt032f7qucy5wqeqpt", addr.String()) } + +func TestNewAddressFromEth(t *testing.T) { + // Dave from test keys. + ethAddr, err := hex.DecodeString("Dce075E1C39b1ae0b75D554558b6451A226ffe00") + require.NoError(t, err, "hex.DecodeString") + addr := NewAddressFromEth(ethAddr) + require.Equal(t, addr.String(), "oasis1qrk58a6j2qn065m6p06jgjyt032f7qucy5wqeqpt") + + // Erin from test keys. + ethAddr, err = hex.DecodeString("709EEbd979328A2B3605A160915DEB26E186abF8") + require.NoError(t, err, "hex.DecodeString") + addr = NewAddressFromEth(ethAddr) + require.Equal(t, addr.String(), "oasis1qqcd0qyda6gtwdrfcqawv3s8cr2kupzw9v967au6") +} diff --git a/runtime-sdk/modules/contracts/src/results.rs b/runtime-sdk/modules/contracts/src/results.rs index 2ae625b53d..cf3954fd17 100644 --- a/runtime-sdk/modules/contracts/src/results.rs +++ b/runtime-sdk/modules/contracts/src/results.rs @@ -148,6 +148,15 @@ fn process_subcalls( // preconfigured the amount of available gas. ::Core::use_tx_gas(ctx, result.gas_used)?; + // Forward any emitted event tags. + ctx.emit_etags(result.state.events); + + // Forward any emitted runtime messages. + for (msg, hook) in result.state.messages { + // This should never fail as child context has the right limits configured. + ctx.emit_message(msg, hook)?; + } + // Process replies based on filtering criteria. let result = result.call_result; match (reply, result.is_success()) { diff --git a/runtime-sdk/modules/contracts/src/test.rs b/runtime-sdk/modules/contracts/src/test.rs index 812d68f275..d239c32769 100644 --- a/runtime-sdk/modules/contracts/src/test.rs +++ b/runtime-sdk/modules/contracts/src/test.rs @@ -275,10 +275,13 @@ fn test_hello_contract_call() { "there should only be one denomination" ); - let (etags, messages) = tx_ctx.commit(); - let tags = etags.into_tags(); + let state = tx_ctx.commit(); + let tags = state.events.into_tags(); // Make sure no runtime messages got emitted. - assert!(messages.is_empty(), "no runtime messages should be emitted"); + assert!( + state.messages.is_empty(), + "no runtime messages should be emitted" + ); // Make sure a contract event was emitted and is properly formatted. assert_eq!(tags.len(), 2, "two events should have been emitted"); assert_eq!(tags[0].key, b"accounts\x00\x00\x00\x01"); // accounts.Transfer (code = 1) event diff --git a/runtime-sdk/modules/evm/Cargo.toml b/runtime-sdk/modules/evm/Cargo.toml index c40597894b..736adf9da8 100644 --- a/runtime-sdk/modules/evm/Cargo.toml +++ b/runtime-sdk/modules/evm/Cargo.toml @@ -42,6 +42,7 @@ oasis-runtime-sdk = { path = "../..", features = ["test"] } rand = "0.7.3" serde = { version = "1.0.144", features = ["derive"] } serde_json = { version = "1.0.87", features = ["raw_value"] } +ethabi = { version = "18.0.0", default-features = false, features = ["std", "full-serde"]} [[bench]] name = "criterion_benchmark" diff --git a/runtime-sdk/modules/evm/src/backend.rs b/runtime-sdk/modules/evm/src/backend.rs index 93bba5dd84..79d1c52834 100644 --- a/runtime-sdk/modules/evm/src/backend.rs +++ b/runtime-sdk/modules/evm/src/backend.rs @@ -1,131 +1,373 @@ -//! EVM backend. -use std::{cell::RefCell, marker::PhantomData}; +use std::{ + cell::RefCell, + collections::{btree_map::Entry, BTreeMap, BTreeSet}, + marker::PhantomData, + mem, +}; -use evm::backend::{Apply, Backend as EVMBackend, Basic, Log}; +use evm::{ + backend::{Backend, Basic, Log}, + executor::stack::{Accessed, StackState, StackSubstateMetadata}, + ExitError, Transfer, +}; +use primitive_types::{H160, H256, U256}; use oasis_runtime_sdk::{ + context::{self, TxContext}, core::common::crypto::hash::Hash, - modules::{accounts::API as _, core::API as _}, + modules::{ + accounts::API as _, + core::{self, API as _}, + }, storage::CurrentStore, + subcall, types::token, - Context, Runtime, + Runtime, }; -use crate::{ - state, - types::{H160, H256, U256}, - Config, -}; +use crate::{state, types, Config}; -/// The maximum number of bytes that may be generated by one invocation of [`EVMBackendExt::random_bytes`]. +/// The maximum number of bytes that may be generated by one invocation of +/// [`EVMBackendExt::random_bytes`]. +/// +/// The precompile function also limits the number of bytes returned, but it's here, too, to prevent +/// accidental memory overconsumption. /// -/// The precompile function also limits the number of bytes returned, but it's here, too, to prevent accidental memory overconsumption. /// This constant might make a good config param, if anyone asks or this changes frequently. pub(crate) const RNG_MAX_BYTES: u64 = 1024; /// Information required by the evm crate. -#[derive(Clone, Default, PartialEq, Eq, cbor::Encode, cbor::Decode)] +#[derive(Clone, Default, PartialEq, Eq)] pub struct Vicinity { pub gas_price: U256, pub origin: H160, } /// Backend for the evm crate that enables the use of our storage. -pub struct Backend<'ctx, C: Context, Cfg: Config> { +pub struct OasisBackend<'ctx, C: TxContext, Cfg: Config> { vicinity: Vicinity, ctx: RefCell<&'ctx mut C>, + pending_state: RefCell, _cfg: PhantomData, } -impl<'ctx, C: Context, Cfg: Config> Backend<'ctx, C, Cfg> { +impl<'ctx, C: TxContext, Cfg: Config> OasisBackend<'ctx, C, Cfg> { pub fn new(ctx: &'ctx mut C, vicinity: Vicinity) -> Self { Self { vicinity, ctx: RefCell::new(ctx), + pending_state: RefCell::new(context::State::default()), _cfg: PhantomData, } } } -impl<'ctx, C: Context, Cfg: Config> EVMBackend for Backend<'ctx, C, Cfg> { - fn gas_price(&self) -> primitive_types::U256 { - self.vicinity.gas_price.into() +/// An extension trait implemented for any [`Backend`]. +pub(crate) trait EVMBackendExt { + /// Returns at most `num_bytes` bytes of cryptographically secure random bytes. + /// The optional personalization string may be included to increase domain separation. + fn random_bytes(&self, num_bytes: u64, pers: &[u8]) -> Vec; + + /// Perform a subcall. + /// + /// Note that `state` from the resulting `SubcallResult` is replaced with a default value as + /// it needs to be stored in substate. + fn subcall( + &self, + info: subcall::SubcallInfo, + validator: V, + ) -> Result; +} + +impl EVMBackendExt for &T { + fn random_bytes(&self, num_bytes: u64, pers: &[u8]) -> Vec { + (*self).random_bytes(num_bytes, pers) + } + + fn subcall( + &self, + info: subcall::SubcallInfo, + validator: V, + ) -> Result { + (*self).subcall(info, validator) + } +} + +impl<'ctx, C: TxContext, Cfg: Config> EVMBackendExt for OasisBackend<'ctx, C, Cfg> { + fn random_bytes(&self, num_bytes: u64, pers: &[u8]) -> Vec { + // Refuse to generate more than 1 KiB in one go. + // EVM memory gas is checked only before and after calls, so we won't + // see the quadratic memory cost until after this call uses its time. + let num_bytes = num_bytes.min(RNG_MAX_BYTES) as usize; + let mut ctx = self.ctx.borrow_mut(); + let mut rng = ctx.rng(pers).expect("unable to access RNG"); + let mut rand_bytes = vec![0u8; num_bytes]; + rand_core::RngCore::try_fill_bytes(&mut rng, &mut rand_bytes).expect("RNG is inoperable"); + rand_bytes + } + + fn subcall( + &self, + info: subcall::SubcallInfo, + validator: V, + ) -> Result { + let mut ctx = self.ctx.borrow_mut(); + + // Execute the subcall. + let mut result = subcall::call(&mut ctx, info, validator)?; + // Store state after subcall execution for substate handling. + self.pending_state + .borrow_mut() + .merge_from(mem::take(&mut result.state)); + + Ok(result) + } +} + +/// Oasis-specific substate implementation for the EVM stack executor. +/// +/// The substate is used to track nested transactional state that can be either be committed or +/// reverted. This is similar to `Context` in the SDK. +/// +/// See the `evm` crate for details. +struct OasisStackSubstate<'config> { + metadata: StackSubstateMetadata<'config>, + parent: Option>>, + logs: Vec, + deletes: BTreeSet, + state: context::State, +} + +impl<'config> OasisStackSubstate<'config> { + fn new(metadata: StackSubstateMetadata<'config>) -> Self { + Self { + metadata, + parent: None, + logs: Vec::new(), + deletes: BTreeSet::new(), + state: context::State::default(), + } + } + + fn metadata(&self) -> &StackSubstateMetadata<'config> { + &self.metadata + } + + fn metadata_mut(&mut self) -> &mut StackSubstateMetadata<'config> { + &mut self.metadata + } + + fn enter(&mut self, gas_limit: u64, is_static: bool) { + let mut entering = Self { + metadata: self.metadata.spit_child(gas_limit, is_static), + parent: None, + logs: Vec::new(), + deletes: BTreeSet::new(), + state: context::State::default(), + }; + mem::swap(&mut entering, self); + + self.parent = Some(Box::new(entering)); + } + + fn exit_commit(&mut self) -> Result<(), ExitError> { + let mut exited = *self.parent.take().expect("Cannot commit on root substate"); + mem::swap(&mut exited, self); + + self.metadata.swallow_commit(exited.metadata)?; + self.logs.append(&mut exited.logs); + self.deletes.append(&mut exited.deletes); + self.state.merge_from(exited.state); + + Ok(()) + } + + fn exit_revert(&mut self) -> Result<(), ExitError> { + let mut exited = *self.parent.take().expect("Cannot revert on root substate"); + mem::swap(&mut exited, self); + + self.metadata.swallow_revert(exited.metadata) + } + + fn exit_discard(&mut self) -> Result<(), ExitError> { + let mut exited = *self.parent.take().expect("Cannot discard on root substate"); + mem::swap(&mut exited, self); + + self.metadata.swallow_discard(exited.metadata) + } + + fn recursive_is_cold bool>(&self, f: &F) -> bool { + let local_is_accessed = self.metadata.accessed().as_ref().map(f).unwrap_or(false); + if local_is_accessed { + false + } else { + self.parent + .as_ref() + .map(|p| p.recursive_is_cold(f)) + .unwrap_or(true) + } + } + + fn deleted(&self, address: H160) -> bool { + if self.deletes.contains(&address) { + return true; + } + + if let Some(parent) = self.parent.as_ref() { + return parent.deleted(address); + } + + false + } + + fn log(&mut self, address: H160, topics: Vec, data: Vec) { + self.logs.push(Log { + address, + topics, + data, + }); + } + + fn set_deleted(&mut self, address: H160) { + self.deletes.insert(address); + } +} + +/// Oasis-specific state implementation for the EVM stack executor. +/// +/// The state maintains a hierarchy of nested transactional states (through [`OasisStackSubstate`]) +/// and exposes it through accessors to the EVM stack executor. +/// +/// See the `evm` crate for details. +pub struct OasisStackState<'ctx, 'backend, 'config, C: TxContext, Cfg: Config> { + backend: &'backend OasisBackend<'ctx, C, Cfg>, + substate: OasisStackSubstate<'config>, + original_storage: BTreeMap<(types::H160, types::H256), types::H256>, +} + +impl<'ctx, 'backend, 'config, C: TxContext, Cfg: Config> + OasisStackState<'ctx, 'backend, 'config, C, Cfg> +{ + /// Create a new Oasis-specific state for the EVM stack executor. + pub fn new( + metadata: StackSubstateMetadata<'config>, + backend: &'backend OasisBackend<'ctx, C, Cfg>, + ) -> Self { + Self { + backend, + substate: OasisStackSubstate::new(metadata), + original_storage: BTreeMap::new(), + } + } + + /// Applies any final state by emitting SDK events/messages. + /// + /// Note that storage has already been committed to the top-level current store. + pub fn apply(mut self) -> Result<(), crate::Error> { + // Abort if SELFDESTRUCT was used. + if !self.substate.deletes.is_empty() { + return Err(crate::Error::ExecutionFailed( + "SELFDESTRUCT not supported".to_owned(), + )); + } + + // Merge from top-level pending state. + self.substate + .state + .merge_from(mem::take(&mut self.backend.pending_state.borrow_mut())); + + let mut ctx = self.backend.ctx.borrow_mut(); + + // Forward SDK events. + ctx.emit_etags(self.substate.state.events); + + // Forward any emitted runtime messages. + for (msg, hook) in self.substate.state.messages { + ctx.emit_message(msg, hook)?; + } + + // Emit logs as events. + for log in self.substate.logs { + ctx.emit_event(crate::Event::Log { + address: log.address.into(), + topics: log.topics.iter().map(|&topic| topic.into()).collect(), + data: log.data, + }); + } + + Ok(()) + } +} + +impl<'ctx, 'backend, 'config, C: TxContext, Cfg: Config> Backend + for OasisStackState<'ctx, 'backend, 'config, C, Cfg> +{ + fn gas_price(&self) -> U256 { + self.backend.vicinity.gas_price } - fn origin(&self) -> primitive_types::H160 { - self.vicinity.origin.into() + fn origin(&self) -> H160 { + self.backend.vicinity.origin } - fn block_hash(&self, number: primitive_types::U256) -> primitive_types::H256 { + fn block_hash(&self, number: U256) -> H256 { CurrentStore::with(|store| { let block_hashes = state::block_hashes(store); if let Some(hash) = block_hashes.get::<_, Hash>(&number.low_u64().to_be_bytes()) { - primitive_types::H256::from_slice(hash.as_ref()) + H256::from_slice(hash.as_ref()) } else { - primitive_types::H256::default() + H256::default() } }) } - fn block_number(&self) -> primitive_types::U256 { - self.ctx.borrow().runtime_header().round.into() + fn block_number(&self) -> U256 { + self.backend.ctx.borrow().runtime_header().round.into() } - fn block_coinbase(&self) -> primitive_types::H160 { + fn block_coinbase(&self) -> H160 { // Does not make sense in runtime context. - primitive_types::H160::default() + H160::default() } - fn block_timestamp(&self) -> primitive_types::U256 { - self.ctx.borrow().runtime_header().timestamp.into() + fn block_timestamp(&self) -> U256 { + self.backend.ctx.borrow().runtime_header().timestamp.into() } - fn block_difficulty(&self) -> primitive_types::U256 { + fn block_difficulty(&self) -> U256 { // Does not make sense in runtime context. - primitive_types::U256::zero() + U256::zero() } - fn block_gas_limit(&self) -> primitive_types::U256 { - ::Core::max_batch_gas(&mut self.ctx.borrow_mut()).into() + fn block_gas_limit(&self) -> U256 { + ::Core::max_batch_gas(&mut self.backend.ctx.borrow_mut()).into() } - fn block_base_fee_per_gas(&self) -> primitive_types::U256 { + fn block_base_fee_per_gas(&self) -> U256 { ::Core::min_gas_price( - &mut self.ctx.borrow_mut(), + &mut self.backend.ctx.borrow_mut(), &Cfg::TOKEN_DENOMINATION, ) .into() } - fn chain_id(&self) -> primitive_types::U256 { + fn chain_id(&self) -> U256 { Cfg::CHAIN_ID.into() } - fn exists(&self, address: primitive_types::H160) -> bool { + fn exists(&self, address: H160) -> bool { let acct = self.basic(address); - !(acct.nonce == primitive_types::U256::zero() - && acct.balance == primitive_types::U256::zero()) + !(acct.nonce == U256::zero() && acct.balance == U256::zero()) } - fn basic(&self, address: primitive_types::H160) -> Basic { - let ctx = self.ctx.borrow_mut(); - + fn basic(&self, address: H160) -> Basic { // Derive SDK account address from the Ethereum address. let sdk_address = Cfg::map_address(address); // Fetch balance and nonce from SDK accounts. Note that these can never fail. let balance = Cfg::Accounts::get_balance(sdk_address, Cfg::TOKEN_DENOMINATION).unwrap(); - let mut nonce = Cfg::Accounts::get_nonce(sdk_address).unwrap(); - - // If this is the caller's address and this is not a simulation context, return the nonce - // decremented by one to cancel out the SDK nonce changes. - if address == self.origin() && !ctx.is_simulation() { - // NOTE: This should not overflow as in non-simulation context the nonce should have - // been incremented by the authentication handler. Tests should make sure to - // either configure simulation mode or set up the nonce correctly. - nonce -= 1; - } + let nonce = Cfg::Accounts::get_nonce(sdk_address).unwrap(); Basic { nonce: nonce.into(), @@ -133,198 +375,173 @@ impl<'ctx, C: Context, Cfg: Config> EVMBackend for Backend<'ctx, C, Cfg> { } } - fn code(&self, address: primitive_types::H160) -> Vec { - let address: H160 = address.into(); - + fn code(&self, address: H160) -> Vec { CurrentStore::with(|store| { let store = state::codes(store); store.get(address).unwrap_or_default() }) } - fn storage( - &self, - address: primitive_types::H160, - index: primitive_types::H256, - ) -> primitive_types::H256 { - let address: H160 = address.into(); - let idx: H256 = index.into(); + fn storage(&self, address: H160, key: H256) -> H256 { + let address: types::H160 = address.into(); + let key: types::H256 = key.into(); - let mut ctx = self.ctx.borrow_mut(); - let res: H256 = state::with_storage::(*ctx, &address, |store| { - store.get(idx).unwrap_or_default() + let mut ctx = self.backend.ctx.borrow_mut(); + let res: types::H256 = state::with_storage::(*ctx, &address, |store| { + store.get(key).unwrap_or_default() }); res.into() } - fn original_storage( - &self, - _address: primitive_types::H160, - _index: primitive_types::H256, - ) -> Option { - None + fn original_storage(&self, address: H160, key: H256) -> Option { + Some( + self.original_storage + .get(&(address.into(), key.into())) + .cloned() + .map(Into::into) + .unwrap_or_else(|| self.storage(address, key)), + ) } } -/// An extension trait implemented for any [`EVMBackend`]. -pub(crate) trait EVMBackendExt { - /// Returns at most `num_bytes` bytes of cryptographically secure random bytes. - /// The optional personalization string may be included to increase domain separation. - fn random_bytes(&self, num_bytes: u64, pers: &[u8]) -> Vec; -} +impl<'ctx, 'backend, 'config, C: TxContext, Cfg: Config> StackState<'config> + for OasisStackState<'ctx, 'backend, 'config, C, Cfg> +{ + fn metadata(&self) -> &StackSubstateMetadata<'config> { + self.substate.metadata() + } -impl EVMBackendExt for &T { - fn random_bytes(&self, num_bytes: u64, pers: &[u8]) -> Vec { - (*self).random_bytes(num_bytes, pers) + fn metadata_mut(&mut self) -> &mut StackSubstateMetadata<'config> { + self.substate.metadata_mut() } -} -impl<'ctx, C: Context, Cfg: Config> EVMBackendExt for Backend<'ctx, C, Cfg> { - fn random_bytes(&self, num_bytes: u64, pers: &[u8]) -> Vec { - // Refuse to generate more than 1 KiB in one go. - // EVM memory gas is checked only before and after calls, so we won't - // see the quadratic memory cost until after this call uses its time. - let num_bytes = num_bytes.min(RNG_MAX_BYTES) as usize; - let mut ctx = self.ctx.borrow_mut(); - let mut rng = ctx.rng(pers).expect("unable to access RNG"); - let mut rand_bytes = vec![0u8; num_bytes]; - rand_core::RngCore::try_fill_bytes(&mut rng, &mut rand_bytes).expect("RNG is inoperable"); - rand_bytes + fn enter(&mut self, gas_limit: u64, is_static: bool) { + self.substate.state = mem::take(&mut self.backend.pending_state.borrow_mut()); + self.substate.enter(gas_limit, is_static); + + CurrentStore::start_transaction(); } -} -/// EVM backend that can apply changes and return an exit value. -pub trait ApplyBackendResult { - /// Apply given values and logs at backend and return an exit value. - fn apply(&mut self, values: A, logs: L) -> evm::ExitReason - where - A: IntoIterator>, - I: IntoIterator, - L: IntoIterator; -} + fn exit_commit(&mut self) -> Result<(), ExitError> { + self.substate.exit_commit()?; -impl<'c, C: Context, Cfg: Config> ApplyBackendResult for Backend<'c, C, Cfg> { - fn apply(&mut self, values: A, logs: L) -> evm::ExitReason - where - A: IntoIterator>, - I: IntoIterator, - L: IntoIterator, - { - // Keep track of the total supply change as a paranoid sanity check as it seems to be cheap - // enough to do (all balances should already be in the storage cache). - let mut total_supply_add = 0u128; - let mut total_supply_sub = 0u128; - // Keep origin handy for nonce sanity checks. - let origin = self.vicinity.origin; - let is_simulation = self.ctx.get_mut().is_simulation(); - - for apply in values { - match apply { - Apply::Delete { .. } => { - // Apply::Delete indicates a SELFDESTRUCT action which is not supported. - // This assumes that Apply::Delete is ALWAYS and ONLY invoked in SELFDESTRUCT opcodes, which indeed is the case: - // https://github.com/rust-blockchain/evm/blob/0fbde9fa7797308290f89111c6abe5cee55a5eac/runtime/src/eval/system.rs#L258-L267 - // - // NOTE: We cannot just check the executors ExitReason if the reason was suicide, - // because that doesn't work in case of cross-contract suicide calls, as only - // the top-level exit reason is returned. - return evm::ExitFatal::Other("SELFDESTRUCT not supported".into()).into(); - } - Apply::Modify { - address, - basic, - code, - storage, - reset_storage: _, - } => { - // Reset storage is ignored since storage cannot be efficiently reset as this - // would require iterating over all of the storage keys. This is fine as reset_storage - // is only ever called on non-empty storage when doing SELFDESTRUCT, which we don't support. - - let addr: H160 = address.into(); - // Derive SDK account address from the Ethereum address. - let address = Cfg::map_address(address); - - // Update account balance and nonce. - let amount = basic.balance.as_u128(); - let old_amount = - Cfg::Accounts::get_balance(address, Cfg::TOKEN_DENOMINATION).unwrap(); - if amount > old_amount { - total_supply_add = - total_supply_add.checked_add(amount - old_amount).unwrap(); - } else { - total_supply_sub = - total_supply_sub.checked_add(old_amount - amount).unwrap(); - } - let amount = token::BaseUnits::new(amount, Cfg::TOKEN_DENOMINATION); - // Setting the balance like this is dangerous, but we have a sanity check below - // to ensure that this never results in any tokens being either minted or - // burned. - Cfg::Accounts::set_balance(address, &amount); - - // Sanity check nonce updates to make sure that they behave exactly the same as - // what we do anyway when authenticating transactions. - let nonce = basic.nonce.low_u64(); - if !is_simulation { - let old_nonce = Cfg::Accounts::get_nonce(address).unwrap(); - - if addr == origin { - // Origin's nonce must stay the same as we cancelled out the changes. Note - // that in reality this means that the nonce has been incremented by one. - assert!(nonce == old_nonce, - "evm execution would not increment origin nonce correctly ({old_nonce} -> {nonce})"); - } else { - // Other nonces must either stay the same or increment. - assert!(nonce >= old_nonce, - "evm execution would not update non-origin nonce correctly ({old_nonce} -> {nonce})"); - } - } - Cfg::Accounts::set_nonce(address, nonce); - - // Handle code updates. - if let Some(code) = code { - CurrentStore::with(|store| { - let mut store = state::codes(store); - store.insert(addr, code); - }); - } - - // Handle storage updates. - for (index, value) in storage { - let idx: H256 = index.into(); - let val: H256 = value.into(); - - let ctx = self.ctx.get_mut(); - if value == primitive_types::H256::default() { - state::with_storage::(*ctx, &addr, |store| { - store.remove(idx) - }); - } else { - state::with_storage::(*ctx, &addr, |store| { - store.insert(idx, val) - }); - } - } - } - } + CurrentStore::commit_transaction(); + + Ok(()) + } + + fn exit_revert(&mut self) -> Result<(), ExitError> { + self.substate.exit_revert()?; + + CurrentStore::rollback_transaction(); + + Ok(()) + } + + fn exit_discard(&mut self) -> Result<(), ExitError> { + self.substate.exit_discard()?; + + CurrentStore::rollback_transaction(); + + Ok(()) + } + + fn is_empty(&self, address: H160) -> bool { + self.basic(address).balance == U256::zero() + && self.basic(address).nonce == U256::zero() + && self.code(address).len() == 0 + } + + fn deleted(&self, address: H160) -> bool { + self.substate.deleted(address) + } + + fn is_cold(&self, address: H160) -> bool { + self.substate + .recursive_is_cold(&|a| a.accessed_addresses.contains(&address)) + } + + fn is_storage_cold(&self, address: H160, key: H256) -> bool { + self.substate + .recursive_is_cold(&|a: &Accessed| a.accessed_storage.contains(&(address, key))) + } + + fn inc_nonce(&mut self, address: H160) { + // Do not increment the origin nonce as that has already been handled by the SDK. + if address == self.origin() { + return; } - // NOTE: This should never happen and if it does it would cause an invariant violation - // so we better abort to avoid corrupting state. - assert!( - total_supply_add == total_supply_sub, - "evm execution would lead to invariant violation ({total_supply_add} != {total_supply_sub})", - ); + let address = Cfg::map_address(address); + Cfg::Accounts::inc_nonce(address); + } - // Emit logs as events. - for log in logs { - self.ctx.get_mut().emit_event(crate::Event::Log { - address: log.address.into(), - topics: log.topics.iter().map(|&topic| topic.into()).collect(), - data: log.data, + fn set_storage(&mut self, address: H160, key: H256, value: H256) { + let mut ctx = self.backend.ctx.borrow_mut(); + + let address: types::H160 = address.into(); + let key: types::H256 = key.into(); + let value: types::H256 = value.into(); + + // We cache the current value if this is the first time we modify it in the transaction. + if let Entry::Vacant(e) = self.original_storage.entry((address, key)) { + let original = state::with_storage::(*ctx, &address, |store| { + store.get(key).unwrap_or_default() + }); + // No need to cache if same value. + if original != value { + e.insert(original); + } + } + + if value == types::H256::default() { + state::with_storage::(*ctx, &address, |store| { + store.remove(key); + }); + } else { + state::with_storage::(*ctx, &address, |store| { + store.insert(key, value); }); } + } + + fn reset_storage(&mut self, _address: H160) { + // Reset storage is ignored since storage cannot be efficiently reset as this would require + // iterating over all of the storage keys. This is fine as reset_storage is only ever called + // on non-empty storage when doing SELFDESTRUCT, which we don't support. + } + + fn log(&mut self, address: H160, topics: Vec, data: Vec) { + self.substate.log(address, topics, data); + } + + fn set_deleted(&mut self, address: H160) { + // Note that we will abort during apply if SELFDESTRUCT was used. + self.substate.set_deleted(address) + } + + fn set_code(&mut self, address: H160, code: Vec) { + CurrentStore::with(|store| { + let mut store = state::codes(store); + store.insert(address, code); + }); + } + + fn transfer(&mut self, transfer: Transfer) -> Result<(), ExitError> { + let from = Cfg::map_address(transfer.source); + let to = Cfg::map_address(transfer.target); + let amount = transfer.value.as_u128(); + let amount = token::BaseUnits::new(amount, Cfg::TOKEN_DENOMINATION); + + Cfg::Accounts::transfer_silent(from, to, &amount).map_err(|_| ExitError::OutOfFund) + } + + fn reset_balance(&mut self, _address: H160) { + // Reset balance is ignored since it exists due to a bug in SELFDESTRUCT, which we + // don't support. + } - evm::ExitSucceed::Returned.into() + fn touch(&mut self, _address: H160) { + // Do not do anything. } } diff --git a/runtime-sdk/modules/evm/src/lib.rs b/runtime-sdk/modules/evm/src/lib.rs index ac6bfdc78b..4912c815bb 100644 --- a/runtime-sdk/modules/evm/src/lib.rs +++ b/runtime-sdk/modules/evm/src/lib.rs @@ -1,5 +1,4 @@ //! EVM module. - #![feature(array_chunks)] #![feature(test)] @@ -14,7 +13,7 @@ pub mod state; pub mod types; use evm::{ - executor::stack::{MemoryStackState, StackExecutor, StackSubstateMetadata}, + executor::stack::{StackExecutor, StackSubstateMetadata}, Config as EVMConfig, }; use once_cell::sync::OnceCell; @@ -40,9 +39,9 @@ use oasis_runtime_sdk::{ }, }; -use backend::ApplyBackendResult; use types::{H160, H256, U256}; +pub mod mock; #[cfg(test)] mod test; @@ -525,8 +524,8 @@ impl Module { &mut StackExecutor< 'static, '_, - MemoryStackState<'_, 'static, backend::Backend<'_, C, Cfg>>, - precompile::Precompiles>, + backend::OasisStackState<'_, '_, 'static, C, Cfg>, + precompile::Precompiles>, >, u64, ) -> (evm::ExitReason, Vec), @@ -538,8 +537,8 @@ impl Module { let fee_denomination = ctx.tx_auth_info().fee.amount.denomination().clone(); let vicinity = backend::Vicinity { - gas_price: gas_price.into(), - origin: source, + gas_price, + origin: source.into(), }; // The maximum gas fee has already been withdrawn in authenticate_tx(). @@ -547,9 +546,9 @@ impl Module { .checked_mul(primitive_types::U256::from(gas_limit)) .ok_or(Error::FeeOverflow)?; - let mut backend = backend::Backend::<'_, C, Cfg>::new(ctx, vicinity); + let backend = backend::OasisBackend::<'_, C, Cfg>::new(ctx, vicinity); let metadata = StackSubstateMetadata::new(gas_limit, cfg); - let stackstate = MemoryStackState::new(metadata, &backend); + let stackstate = backend::OasisStackState::new(metadata, &backend); let precompiles = precompile::Precompiles::new(&backend); let mut executor = StackExecutor::new_with_precompiles(stackstate, cfg, &precompiles); @@ -571,11 +570,8 @@ impl Module { .checked_sub(fee) .ok_or(Error::InsufficientBalance)?; - let (vals, logs) = executor.into_state().deconstruct(); - // Apply can fail in case of unsupported actions. - let exit_reason = backend.apply(vals, logs); - if let Err(err) = process_evm_result(exit_reason, Vec::new()) { + if let Err(err) = executor.into_state().apply() { ::Core::use_tx_gas(ctx, gas_used)?; return Err(err); }; diff --git a/runtime-sdk/modules/evm/src/mock.rs b/runtime-sdk/modules/evm/src/mock.rs new file mode 100644 index 0000000000..0d62094f6d --- /dev/null +++ b/runtime-sdk/modules/evm/src/mock.rs @@ -0,0 +1,87 @@ +//! Mock functionality for use during testing. +use uint::hex::FromHex; + +use oasis_runtime_sdk::{ + dispatcher, + testing::mock::{CallOptions, Signer}, + types::address::SignatureAddressSpec, + BatchContext, +}; + +use crate::types::{self, H160}; + +/// A mock EVM signer for use during tests. +pub struct EvmSigner(Signer); + +impl EvmSigner { + /// Create a new mock signer using the given nonce and signature spec. + pub fn new(nonce: u64, sigspec: SignatureAddressSpec) -> Self { + Self(Signer::new(nonce, sigspec)) + } + + /// Dispatch a call to the given EVM contract method. + pub fn call_evm( + &mut self, + ctx: &mut C, + address: H160, + name: &str, + param_types: &[ethabi::ParamType], + params: &[ethabi::Token], + ) -> dispatcher::DispatchResult + where + C: BatchContext, + { + self.call_evm_opts(ctx, address, name, param_types, params, Default::default()) + } + + /// Dispatch a call to the given EVM contract method with the given options. + pub fn call_evm_opts( + &mut self, + ctx: &mut C, + address: H160, + name: &str, + param_types: &[ethabi::ParamType], + params: &[ethabi::Token], + opts: CallOptions, + ) -> dispatcher::DispatchResult + where + C: BatchContext, + { + let data = [ + ethabi::short_signature(name, param_types).to_vec(), + ethabi::encode(params), + ] + .concat(); + + self.call_opts( + ctx, + "evm.Call", + types::Call { + address, + value: 0.into(), + data, + }, + opts, + ) + } +} + +impl std::ops::Deref for EvmSigner { + type Target = Signer; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for EvmSigner { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Load contract bytecode from a hex-encoded string. +pub fn load_contract_bytecode(raw: &str) -> Vec { + Vec::from_hex(raw.split_whitespace().collect::()) + .expect("compiled contract should be a valid hex string") +} diff --git a/runtime-sdk/modules/evm/src/precompile/mod.rs b/runtime-sdk/modules/evm/src/precompile/mod.rs index 412c5af37f..82ec847ae6 100644 --- a/runtime-sdk/modules/evm/src/precompile/mod.rs +++ b/runtime-sdk/modules/evm/src/precompile/mod.rs @@ -13,6 +13,7 @@ use crate::{backend::EVMBackendExt, Config}; mod confidential; mod sha512; mod standard; +mod subcall; pub mod testing; @@ -91,7 +92,7 @@ impl PrecompileSet for Precompiles<'_, Cfg, B> { return None; } Some(match (address[0], address[18], address[19]) { - // Standard precompiles. + // Ethereum-compatible. (0, 0, 1) => standard::call_ecrecover(handle), (0, 0, 2) => standard::call_sha256(handle), (0, 0, 3) => standard::call_ripemd160(handle), @@ -100,7 +101,7 @@ impl PrecompileSet for Precompiles<'_, Cfg, B> { (0, 0, 6) => standard::call_bn128_add(handle), (0, 0, 7) => standard::call_bn128_mul(handle), (0, 0, 8) => standard::call_bn128_pairing(handle), - // Confidential precompiles. + // Oasis-specific, confidential. (1, 0, 1) => confidential::call_random_bytes(handle, self.backend), (1, 0, 2) => confidential::call_x25519_derive(handle), (1, 0, 3) => confidential::call_deoxysii_seal(handle), @@ -109,24 +110,26 @@ impl PrecompileSet for Precompiles<'_, Cfg, B> { (1, 0, 6) => confidential::call_sign(handle), (1, 0, 7) => confidential::call_verify(handle), (1, 0, 8) => confidential::call_curve25519_compute_public(handle), - // Other precompiles. + // Oasis-specific, general. (1, 1, 1) => sha512::call_sha512_256(handle), + (1, 1, 2) => subcall::call_subcall(handle, self.backend), _ => return Cfg::additional_precompiles().and_then(|pc| pc.execute(handle)), }) } fn is_precompile(&self, address: H160) -> bool { + // See above table in `execute` for matching on what is a valid precompile address. let addr_bytes = address.as_bytes(); let (a0, a18, a19) = (address[0], addr_bytes[18], addr_bytes[19]); (address[1..18].iter().all(|b| *b == 0) && matches!( (a0, a18, a19, Cfg::CONFIDENTIAL), - // Standard. + // Ethereum-compatible. (0, 0, 1..=8, _) | - // Confidential. + // Oasis-specific, confidential. (1, 0, 1..=8, true) | - // Other. - (1, 1, 1, _) + // Oasis-specific, general. + (1, 1, 1..=2, _) )) || Cfg::additional_precompiles() .map(|pc| pc.is_precompile(address)) diff --git a/runtime-sdk/modules/evm/src/precompile/subcall.rs b/runtime-sdk/modules/evm/src/precompile/subcall.rs new file mode 100644 index 0000000000..c82a4a8356 --- /dev/null +++ b/runtime-sdk/modules/evm/src/precompile/subcall.rs @@ -0,0 +1,537 @@ +use ethabi::{ParamType, Token}; +use evm::{ + executor::stack::{PrecompileFailure, PrecompileHandle, PrecompileOutput}, + ExitError, ExitSucceed, +}; + +use crate::backend::EVMBackendExt; +use oasis_runtime_sdk::{ + module::CallResult, modules::core::Error, subcall, types::transaction::CallerAddress, +}; + +use super::{record_linear_cost, PrecompileResult}; + +/// A subcall validator which prevents any subcalls from re-entering the EVM module. +struct ForbidReentrancy; + +impl subcall::Validator for ForbidReentrancy { + fn validate(&self, info: &subcall::SubcallInfo) -> Result<(), Error> { + if info.method.starts_with("evm.") { + return Err(Error::Forbidden); + } + Ok(()) + } +} + +const SUBCALL_BASE_COST: u64 = 10; +const SUBCALL_WORD_COST: u64 = 1; + +pub(super) fn call_subcall( + handle: &mut impl PrecompileHandle, + backend: &B, +) -> PrecompileResult { + record_linear_cost( + handle, + handle.input().len() as u64, + SUBCALL_BASE_COST, + SUBCALL_WORD_COST, + )?; + + // Ensure that the precompile is called using a regular call (and not a delegatecall) so the + // caller is actually the address of the calling contract. + if handle.context().address != handle.code_address() { + return Err(PrecompileFailure::Error { + exit_status: ExitError::Other("invalid call".into()), + }); + } + + let mut call_args = ethabi::decode( + &[ + ParamType::Bytes, // method + ParamType::Bytes, // body (CBOR) + ], + handle.input(), + ) + .map_err(|e| PrecompileFailure::Error { + exit_status: ExitError::Other(e.to_string().into()), + })?; + + // Parse raw arguments. + let body = call_args.pop().unwrap().into_bytes().unwrap(); + let method = call_args.pop().unwrap().into_bytes().unwrap(); + + // Parse body as CBOR. + let body = cbor::from_slice(&body).map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("body is malformed".into()), + })?; + + // Parse method. + let method = String::from_utf8(method).map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("method is malformed".into()), + })?; + + // Cap maximum amount of gas that can be used. + let max_gas = handle.remaining_gas(); + + let result = backend + .subcall( + subcall::SubcallInfo { + caller: CallerAddress::EthAddress(handle.context().caller.into()), + method, + body, + max_depth: 8, + max_gas, + }, + ForbidReentrancy, + ) + .map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("subcall failed".into()), + })?; + + // Charge gas (this shouldn't fail given that we set the limit appropriately). + handle.record_cost(result.gas_used)?; + + match result.call_result { + CallResult::Ok(value) => Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: ethabi::encode(&[ + Token::Uint(0.into()), // status_code + Token::Bytes(cbor::to_vec(value)), // response + ]), + }), + CallResult::Failed { code, module, .. } => Ok(PrecompileOutput { + exit_status: ExitSucceed::Returned, + output: ethabi::encode(&[ + Token::Uint(code.into()), // status_code + Token::Bytes(module.into()), // response + ]), + }), + CallResult::Aborted(_) => { + // TODO: Should propagate abort. + Err(PrecompileFailure::Error { + exit_status: ExitError::Other("subcall failed".into()), + }) + } + } +} + +#[cfg(test)] +mod test { + use std::collections::BTreeMap; + + use ethabi::{ParamType, Token}; + + use oasis_runtime_sdk::{ + context, + module::{self, Module as _}, + modules::{accounts, core}, + testing::{ + keys, + mock::{CallOptions, Mock}, + }, + types::{ + address::Address, + token::{self, BaseUnits, Denomination}, + transaction::Fee, + }, + BatchContext, Runtime, Version, + }; + + use crate as evm; + use crate::{ + mock::{load_contract_bytecode, EvmSigner}, + types::{self, H160}, + Config as _, + }; + + struct TestConfig; + + type Core = core::Module; + type Accounts = accounts::Module; + type Evm = evm::Module; + + impl core::Config for TestConfig {} + + impl evm::Config for TestConfig { + type Accounts = Accounts; + + type AdditionalPrecompileSet = (); + + const CHAIN_ID: u64 = 0x42; + + const TOKEN_DENOMINATION: Denomination = Denomination::NATIVE; + } + + struct TestRuntime; + + impl Runtime for TestRuntime { + const VERSION: Version = Version::new(0, 0, 0); + type Core = Core; + type Modules = (Core, Accounts, Evm); + + fn genesis_state() -> ::Genesis { + ( + core::Genesis { + parameters: core::Parameters { + max_batch_gas: u64::MAX, + max_tx_size: 32 * 1024, + max_tx_signers: 1, + max_multisig_signers: 8, + gas_costs: Default::default(), + min_gas_price: BTreeMap::from([(token::Denomination::NATIVE, 0)]), + }, + }, + accounts::Genesis { + balances: BTreeMap::from([( + keys::dave::address(), + BTreeMap::from([(Denomination::NATIVE, 1_000_000)]), + )]), + total_supplies: BTreeMap::from([(Denomination::NATIVE, 1_000_000)]), + ..Default::default() + }, + evm::Genesis { + ..Default::default() + }, + ) + } + } + + /// Test contract code. + static TEST_CONTRACT_CODE_HEX: &str = + include_str!("../../../../../tests/e2e/contracts/subcall/evm_subcall_compiled.hex"); + /// Test contract ABI. + static TEST_CONTRACT_ABI_JSON: &str = + include_str!("../../../../../tests/e2e/contracts/subcall/evm_subcall_abi.json"); + + fn init_and_deploy_contract(ctx: &mut C, signer: &mut EvmSigner) -> H160 { + TestRuntime::migrate(ctx); + + let test_contract = load_contract_bytecode(TEST_CONTRACT_CODE_HEX); + + // Create contract. + let dispatch_result = signer.call( + ctx, + "evm.Create", + types::Create { + value: 0.into(), + init_code: test_contract, + }, + ); + let result = dispatch_result.result.unwrap(); + let result: Vec = cbor::from_value(result).unwrap(); + let contract_address = H160::from_slice(&result); + + contract_address + } + + #[test] + fn test_subcall_dispatch() { + let mut mock = Mock::default(); + let mut ctx = mock.create_ctx_for_runtime::(context::Mode::ExecuteTx, false); + let mut signer = EvmSigner::new(0, keys::dave::sigspec()); + + // Create contract. + let contract_address = init_and_deploy_contract(&mut ctx, &mut signer); + + // Call into the test contract. + let dispatch_result = signer.call_evm( + &mut ctx, + contract_address, + "test", + &[ + ParamType::Bytes, // method + ParamType::Bytes, // body + ], + &[ + Token::Bytes("accounts.Transfer".into()), + Token::Bytes(cbor::to_vec(accounts::types::Transfer { + to: keys::alice::address(), + amount: BaseUnits::new(1_000, Denomination::NATIVE), + })), + ], + ); + assert!( + !dispatch_result.result.is_success(), + "call should fail due to insufficient balance" + ); + + // Transfer some tokens to the contract. + let dispatch_result = signer.call( + &mut ctx, + "accounts.Transfer", + accounts::types::Transfer { + to: TestConfig::map_address(contract_address.into()), + amount: BaseUnits::new(2_000, Denomination::NATIVE), + }, + ); + assert!( + dispatch_result.result.is_success(), + "transfer should succeed" + ); + + // Call into test contract again. + let dispatch_result = signer.call_evm( + &mut ctx, + contract_address, + "test", + &[ + ParamType::Bytes, // method + ParamType::Bytes, // body + ], + &[ + Token::Bytes("accounts.Transfer".into()), + Token::Bytes(cbor::to_vec(accounts::types::Transfer { + to: keys::alice::address(), + amount: BaseUnits::new(1_000, Denomination::NATIVE), + })), + ], + ); + assert!(dispatch_result.result.is_success(), "call should succeed"); + + // Make sure two events were emitted and are properly formatted. + let tags = &dispatch_result.tags; + assert_eq!(tags.len(), 2, "two events should have been emitted"); + assert_eq!(tags[0].key, b"accounts\x00\x00\x00\x01"); // accounts.Transfer (code = 1) event + assert_eq!(tags[1].key, b"core\x00\x00\x00\x01"); // core.GasUsed (code = 1) event + + #[derive(Debug, Default, cbor::Decode)] + struct TransferEvent { + from: Address, + to: Address, + amount: token::BaseUnits, + } + + let events: Vec = cbor::from_slice(&tags[0].value).unwrap(); + assert_eq!(events.len(), 2); // One event for subcall, other event for fee payment. + let event = &events[0]; + assert_eq!(event.from, Address::from_eth(contract_address.as_ref())); + assert_eq!(event.to, keys::alice::address()); + assert_eq!(event.amount, BaseUnits::new(1_000, Denomination::NATIVE)); + } + + #[test] + fn test_require_regular_call() { + let mut mock = Mock::default(); + let mut ctx = mock.create_ctx_for_runtime::(context::Mode::ExecuteTx, false); + let mut signer = EvmSigner::new(0, keys::dave::sigspec()); + + // Create contract. + let contract_address = init_and_deploy_contract(&mut ctx, &mut signer); + + // Call into the test contract. + let dispatch_result = signer.call_evm( + &mut ctx, + contract_address, + "test_delegatecall", + &[ + ParamType::Bytes, // method + ParamType::Bytes, // body + ], + &[ + Token::Bytes("accounts.Transfer".into()), + Token::Bytes(cbor::to_vec(accounts::types::Transfer { + to: keys::alice::address(), + amount: BaseUnits::new(0, Denomination::NATIVE), + })), + ], + ); + if let module::CallResult::Failed { + module, + code, + message, + } = dispatch_result.result + { + assert_eq!(module, "evm"); + assert_eq!(code, 8); + assert_eq!(message, "reverted: subcall failed"); + } else { + panic!("call should fail due to delegatecall"); + } + } + + #[test] + fn test_no_reentrance() { + let mut mock = Mock::default(); + let mut ctx = mock.create_ctx_for_runtime::(context::Mode::ExecuteTx, false); + let mut signer = EvmSigner::new(0, keys::dave::sigspec()); + + // Create contract. + let contract_address = init_and_deploy_contract(&mut ctx, &mut signer); + + // Call into the test contract. + let dispatch_result = signer.call_evm( + &mut ctx, + contract_address, + "test", + &[ + ParamType::Bytes, // method + ParamType::Bytes, // body + ], + &[ + Token::Bytes("evm.Call".into()), + Token::Bytes(cbor::to_vec(evm::types::Call { + address: contract_address, + value: 0.into(), + data: [ + ethabi::short_signature("test", &[ParamType::Bytes, ParamType::Bytes]) + .to_vec(), + ethabi::encode(&[ + Token::Bytes("accounts.Transfer".into()), + Token::Bytes(cbor::to_vec(accounts::types::Transfer { + to: keys::alice::address(), + amount: BaseUnits::new(0, Denomination::NATIVE), + })), + ]), + ] + .concat(), + })), + ], + ); + if let module::CallResult::Failed { + module, + code, + message, + } = dispatch_result.result + { + assert_eq!(module, "evm"); + assert_eq!(code, 8); + assert_eq!(message, "reverted: subcall failed"); + } else { + panic!("call should fail due to re-entrancy not being allowed"); + } + } + + #[test] + fn test_gas_accounting() { + let mut mock = Mock::default(); + let mut ctx = mock.create_ctx_for_runtime::(context::Mode::ExecuteTx, false); + let mut signer = EvmSigner::new(0, keys::dave::sigspec()); + + // Create contract. + let contract_address = init_and_deploy_contract(&mut ctx, &mut signer); + + // Make transfers more expensive so we can test an out-of-gas condition. + Accounts::set_params(accounts::Parameters { + gas_costs: accounts::GasCosts { + tx_transfer: 100_000, + }, + ..Default::default() + }); + + // First try a call with enough gas. + let dispatch_result = signer.call_evm_opts( + &mut ctx, + contract_address, + "test", + &[ + ParamType::Bytes, // method + ParamType::Bytes, // body + ], + &[ + Token::Bytes("accounts.Transfer".into()), + Token::Bytes(cbor::to_vec(accounts::types::Transfer { + to: keys::alice::address(), + amount: BaseUnits::new(0, Denomination::NATIVE), + })), + ], + CallOptions { + fee: Fee { + gas: 130_000, + ..Default::default() + }, + }, + ); + assert!( + dispatch_result.result.is_success(), + "call with enough gas should succeed" + ); + + // Then lower the amount such that the inner call would fail, but the rest of execution + // can still continue (e.g. to trigger the revert). + let dispatch_result = signer.call_evm_opts( + &mut ctx, + contract_address, + "test", + &[ + ParamType::Bytes, // method + ParamType::Bytes, // body + ], + &[ + Token::Bytes("accounts.Transfer".into()), + Token::Bytes(cbor::to_vec(accounts::types::Transfer { + to: keys::alice::address(), + amount: BaseUnits::new(0, Denomination::NATIVE), + })), + ], + CallOptions { + fee: Fee { + gas: 120_000, + ..Default::default() + }, + }, + ); + if let module::CallResult::Failed { + module, + code, + message, + } = dispatch_result.result + { + assert_eq!(module, "evm"); + assert_eq!(code, 8); + + let message = message.strip_prefix("reverted: ").unwrap(); + let data = base64::decode(message).unwrap(); + let abi = ethabi::Contract::load(TEST_CONTRACT_ABI_JSON.as_bytes()).unwrap(); + let mut err = abi + .error("SubcallFailed") + .unwrap() + .decode(&data[4..]) + .unwrap(); + + let subcall_module = err.pop().unwrap().into_bytes().unwrap(); + let subcall_code: u64 = err.pop().unwrap().into_uint().unwrap().try_into().unwrap(); + + assert_eq!(subcall_module, "core".as_bytes()); + assert_eq!(subcall_code, 12); // Error code 12 for module core is "out of gas". + } else { + panic!("call should fail due to subcall running out of gas"); + } + + // Then raise the amount such that the inner call would succeed but the rest of the + // execution would fail. + let dispatch_result = signer.call_evm_opts( + &mut ctx, + contract_address, + "test_spin", // Version that spins, wasting gas, after the subcall. + &[ + ParamType::Bytes, // method + ParamType::Bytes, // body + ], + &[ + Token::Bytes("accounts.Transfer".into()), + Token::Bytes(cbor::to_vec(accounts::types::Transfer { + to: keys::alice::address(), + amount: BaseUnits::new(0, Denomination::NATIVE), + })), + ], + CallOptions { + fee: Fee { + gas: 127_710, + ..Default::default() + }, + }, + ); + if let module::CallResult::Failed { + module, + code, + message, + } = dispatch_result.result + { + assert_eq!(module, "evm"); + assert_eq!(code, 2); + assert_eq!(message, "execution failed: out of gas"); + } else { + panic!("call should fail due to running out of gas"); + } + } +} diff --git a/runtime-sdk/modules/evm/src/precompile/testing.rs b/runtime-sdk/modules/evm/src/precompile/testing.rs index c3ff2ae729..3694d5829a 100644 --- a/runtime-sdk/modules/evm/src/precompile/testing.rs +++ b/runtime-sdk/modules/evm/src/precompile/testing.rs @@ -4,7 +4,11 @@ use evm::{ }; pub use primitive_types::{H160, H256}; -use oasis_runtime_sdk::{modules::accounts::Module, types::token::Denomination}; +use oasis_runtime_sdk::{ + modules::{accounts::Module, core::Error}, + subcall, + types::token::Denomination, +}; use super::{PrecompileResult, Precompiles}; @@ -30,6 +34,14 @@ impl crate::backend::EVMBackendExt for MockBackend { .chain((pers.len()..(num_bytes as usize)).map(|i| i as u8)) .collect() } + + fn subcall( + &self, + _info: subcall::SubcallInfo, + _validator: V, + ) -> Result { + unimplemented!() + } } struct MockPrecompileHandle<'a> { diff --git a/runtime-sdk/modules/evm/src/raw_tx.rs b/runtime-sdk/modules/evm/src/raw_tx.rs index 6d2565af70..c34adee035 100644 --- a/runtime-sdk/modules/evm/src/raw_tx.rs +++ b/runtime-sdk/modules/evm/src/raw_tx.rs @@ -178,7 +178,8 @@ pub fn decode( fee: transaction::Fee { amount: token::BaseUnits(resolved_fee_amount, token::Denomination::NATIVE), gas: gas_limit, - consensus_messages: 0, + // TODO: Allow customization, maybe through call data? + consensus_messages: 1, }, ..Default::default() }, diff --git a/runtime-sdk/src/context.rs b/runtime-sdk/src/context.rs index 7d67d9a1de..73af095e61 100644 --- a/runtime-sdk/src/context.rs +++ b/runtime-sdk/src/context.rs @@ -61,6 +61,27 @@ impl From<&Mode> for &'static str { } } +/// State after applying the context. +#[derive(Clone, Debug, Default)] +pub struct State { + /// Emitted event tags. + pub events: EventTags, + /// Emitted messages to consensus layer. + pub messages: Vec<(roothash::Message, MessageEventHookInvocation)>, +} + +impl State { + /// Merge a different state into this state. + pub fn merge_from(&mut self, other: State) { + for (key, event) in other.events { + let events = self.events.entry(key).or_insert_with(Vec::new); + events.extend(event); + } + + self.messages.extend(other.messages); + } +} + /// Local configuration key the value of which determines whether expensive queries should be /// allowed or not, and also whether smart contracts should be simulated for `core.EstimateGas`. /// DEPRECATED and superseded by LOCAL_CONFIG_ESTIMATE_GAS_BY_SIMULATING_CONTRACTS and LOCAL_CONFIG_ALLOWED_QUERIES. @@ -244,12 +265,7 @@ pub trait Context { /// # Storage /// /// This does not commit any storage transaction. - fn commit( - self, - ) -> ( - EventTags, - Vec<(roothash::Message, MessageEventHookInvocation)>, - ); + fn commit(self) -> State; /// Rollback any changes made by this context. This method only needs to be called explicitly /// in case you want to retrieve possibly emitted unconditional events. Simply dropping the @@ -358,12 +374,7 @@ impl<'a, 'b, C: Context> Context for std::cell::RefMut<'a, &'b mut C> { self.deref().io_ctx() } - fn commit( - self, - ) -> ( - EventTags, - Vec<(roothash::Message, MessageEventHookInvocation)>, - ) { + fn commit(self) -> State { unimplemented!() } @@ -462,6 +473,56 @@ pub trait TxContext: Context { fn emit_unconditional_event(&mut self, event: E); } +impl<'a, 'b, C: TxContext> TxContext for std::cell::RefMut<'a, &'b mut C> { + fn tx_index(&self) -> usize { + self.deref().tx_index() + } + + fn tx_size(&self) -> u32 { + self.deref().tx_size() + } + + fn tx_auth_info(&self) -> &transaction::AuthInfo { + self.deref().tx_auth_info() + } + + fn tx_call_format(&self) -> transaction::CallFormat { + self.deref().tx_call_format() + } + + fn is_read_only(&self) -> bool { + self.deref().is_read_only() + } + + fn is_internal(&self) -> bool { + self.deref().is_internal() + } + + fn internal(self) -> Self { + unimplemented!() + } + + fn tx_caller_address(&self) -> Address { + self.deref().tx_caller_address() + } + + fn tx_value(&mut self, key: &'static str) -> ContextValue<'_, V> { + self.deref_mut().tx_value(key) + } + + fn emit_message( + &mut self, + msg: roothash::Message, + hook: MessageEventHookInvocation, + ) -> Result<(), Error> { + self.deref_mut().emit_message(msg, hook) + } + + fn emit_unconditional_event(&mut self, event: E) { + self.deref_mut().emit_unconditional_event(event) + } +} + /// Dispatch context for the whole batch. pub struct RuntimeBatchContext<'a, R: runtime::Runtime> { mode: Mode, @@ -596,13 +657,11 @@ impl<'a, R: runtime::Runtime> Context for RuntimeBatchContext<'a, R> { IoContext::create_child(&self.io_ctx) } - fn commit( - self, - ) -> ( - EventTags, - Vec<(roothash::Message, MessageEventHookInvocation)>, - ) { - (self.block_etags, self.messages) + fn commit(self) -> State { + State { + events: self.block_etags, + messages: self.messages, + } } fn rollback(self) -> EventTags { @@ -837,19 +896,17 @@ impl<'round, 'store, R: runtime::Runtime> Context for RuntimeTxContext<'round, ' IoContext::create_child(&self.io_ctx) } - fn commit( - mut self, - ) -> ( - EventTags, - Vec<(roothash::Message, MessageEventHookInvocation)>, - ) { + fn commit(mut self) -> State { // Merge unconditional events into regular events on success. for (key, val) in self.etags_unconditional { let tag = self.etags.entry(key).or_insert_with(Vec::new); tag.extend(val) } - (self.etags, self.messages) + State { + events: self.etags, + messages: self.messages, + } } fn rollback(self) -> EventTags { diff --git a/runtime-sdk/src/dispatcher.rs b/runtime-sdk/src/dispatcher.rs index a8996d94c8..43c5c1cd12 100644 --- a/runtime-sdk/src/dispatcher.rs +++ b/runtime-sdk/src/dispatcher.rs @@ -303,16 +303,16 @@ impl Dispatcher { )) } else { // Commit store and return emitted tags and messages. - let (etags, messages) = ctx.commit(); + let state = ctx.commit(); TransactionResult::Commit(( DispatchResult { result, - tags: etags.into_tags(), + tags: state.events.into_tags(), priority, sender_metadata, call_format_metadata, }, - messages, + state.messages, )) } }) @@ -571,15 +571,14 @@ impl Dispatcher { R::Modules::end_block(&mut ctx); // Commit the context and retrieve the emitted messages. - let (block_tags, messages) = ctx.commit(); - let (messages, handlers) = messages.into_iter().unzip(); - + let state = ctx.commit(); + let (messages, handlers) = state.messages.into_iter().unzip(); Self::save_emitted_message_handlers(handlers); Ok(ExecuteBatchResult { results, messages, - block_tags: block_tags.into_tags(), + block_tags: state.events.into_tags(), tx_reject_hashes: vec![], in_msgs_count: 0, // TODO: Support processing incoming messages. }) diff --git a/runtime-sdk/src/modules/accounts/mod.rs b/runtime-sdk/src/modules/accounts/mod.rs index da158c36a9..81f84a3d02 100644 --- a/runtime-sdk/src/modules/accounts/mod.rs +++ b/runtime-sdk/src/modules/accounts/mod.rs @@ -147,6 +147,9 @@ pub trait API { amount: &token::BaseUnits, ) -> Result<(), Error>; + /// Transfer an amount from one account to the other without emitting an event. + fn transfer_silent(from: Address, to: Address, amount: &token::BaseUnits) -> Result<(), Error>; + /// Mint new tokens, increasing the total supply. fn mint(ctx: &mut C, to: Address, amount: &token::BaseUnits) -> Result<(), Error>; @@ -160,6 +163,9 @@ pub trait API { /// Fetch an account's current nonce. fn get_nonce(address: Address) -> Result; + /// Increments an account's nonce. + fn inc_nonce(address: Address); + /// Sets an account's balance of the given denomination. /// /// # Warning @@ -423,10 +429,7 @@ impl API for Module { return Ok(()); } - // Subtract from source account. - Self::sub_amount(from, amount)?; - // Add to destination account. - Self::add_amount(to, amount)?; + Self::transfer_silent(from, to, amount)?; // Emit a transfer event. ctx.emit_event(Event::Transfer { @@ -438,6 +441,15 @@ impl API for Module { Ok(()) } + fn transfer_silent(from: Address, to: Address, amount: &token::BaseUnits) -> Result<(), Error> { + // Subtract from source account. + Self::sub_amount(from, amount)?; + // Add to destination account. + Self::add_amount(to, amount)?; + + Ok(()) + } + fn mint(ctx: &mut C, to: Address, amount: &token::BaseUnits) -> Result<(), Error> { if ctx.is_check_only() || amount.amount() == 0 { return Ok(()); @@ -504,6 +516,17 @@ impl API for Module { }) } + fn inc_nonce(address: Address) { + CurrentStore::with(|store| { + let store = storage::PrefixStore::new(store, &MODULE_NAME); + let mut accounts = + storage::TypedStore::new(storage::PrefixStore::new(store, &state::ACCOUNTS)); + let mut account: types::Account = accounts.get(address).unwrap_or_default(); + account.nonce = account.nonce.saturating_add(1); + accounts.insert(address, account); + }) + } + fn set_balance(address: Address, amount: &token::BaseUnits) { CurrentStore::with(|store| { let store = storage::PrefixStore::new(store, &MODULE_NAME); diff --git a/runtime-sdk/src/modules/consensus/test.rs b/runtime-sdk/src/modules/consensus/test.rs index f3aea5ccc6..36deb75f5c 100644 --- a/runtime-sdk/src/modules/consensus/test.rs +++ b/runtime-sdk/src/modules/consensus/test.rs @@ -56,9 +56,9 @@ fn test_api_transfer() { ) .expect("transfer should succeed"); - let (_, msgs) = tx_ctx.commit(); - assert_eq!(1, msgs.len(), "one message should be emitted"); - let (msg, hook) = msgs.first().unwrap(); + let state = tx_ctx.commit(); + assert_eq!(1, state.messages.len(), "one message should be emitted"); + let (msg, hook) = state.messages.first().unwrap(); assert_eq!( &Message::Staking(Versioned::new( @@ -126,9 +126,9 @@ fn test_api_transfer_scaling() { ) .expect("transfer should succeed"); - let (_, msgs) = tx_ctx.commit(); - assert_eq!(1, msgs.len(), "one message should be emitted"); - let (msg, hook) = msgs.first().unwrap(); + let state = tx_ctx.commit(); + assert_eq!(1, state.messages.len(), "one message should be emitted"); + let (msg, hook) = state.messages.first().unwrap(); assert_eq!( &Message::Staking(Versioned::new( @@ -167,9 +167,9 @@ fn test_api_withdraw() { ) .expect("withdraw should succeed"); - let (_, msgs) = tx_ctx.commit(); - assert_eq!(1, msgs.len(), "one message should be emitted"); - let (msg, hook) = msgs.first().unwrap(); + let state = tx_ctx.commit(); + assert_eq!(1, state.messages.len(), "one message should be emitted"); + let (msg, hook) = state.messages.first().unwrap(); assert_eq!( &Message::Staking(Versioned::new( @@ -212,9 +212,9 @@ fn test_api_withdraw_scaling() { ) .expect("withdraw should succeed"); - let (_, msgs) = tx_ctx.commit(); - assert_eq!(1, msgs.len(), "one message should be emitted"); - let (msg, hook) = msgs.first().unwrap(); + let state = tx_ctx.commit(); + assert_eq!(1, state.messages.len(), "one message should be emitted"); + let (msg, hook) = state.messages.first().unwrap(); assert_eq!( &Message::Staking(Versioned::new( @@ -252,9 +252,9 @@ fn test_api_escrow() { ) .expect("escrow should succeed"); - let (_, msgs) = tx_ctx.commit(); - assert_eq!(1, msgs.len(), "one message should be emitted"); - let (msg, hook) = msgs.first().unwrap(); + let state = tx_ctx.commit(); + assert_eq!(1, state.messages.len(), "one message should be emitted"); + let (msg, hook) = state.messages.first().unwrap(); assert_eq!( &Message::Staking(Versioned::new( @@ -297,9 +297,9 @@ fn test_api_escrow_scaling() { ) .expect("escrow should succeed"); - let (_, msgs) = tx_ctx.commit(); - assert_eq!(1, msgs.len(), "one message should be emitted"); - let (msg, hook) = msgs.first().unwrap(); + let state = tx_ctx.commit(); + assert_eq!(1, state.messages.len(), "one message should be emitted"); + let (msg, hook) = state.messages.first().unwrap(); assert_eq!( &Message::Staking(Versioned::new( @@ -342,9 +342,9 @@ fn test_api_reclaim_escrow() { ) .expect("reclaim escrow should succeed"); - let (_, msgs) = tx_ctx.commit(); - assert_eq!(1, msgs.len(), "one message should be emitted"); - let (msg, hook) = msgs.first().unwrap(); + let state = tx_ctx.commit(); + assert_eq!(1, state.messages.len(), "one message should be emitted"); + let (msg, hook) = state.messages.first().unwrap(); assert_eq!( &Message::Staking(Versioned::new( diff --git a/runtime-sdk/src/modules/consensus_accounts/test.rs b/runtime-sdk/src/modules/consensus_accounts/test.rs index ab0e847446..d46637ff99 100644 --- a/runtime-sdk/src/modules/consensus_accounts/test.rs +++ b/runtime-sdk/src/modules/consensus_accounts/test.rs @@ -216,9 +216,9 @@ fn test_api_deposit() { ) .expect("deposit tx should succeed"); - let (_, mut msgs) = tx_ctx.commit(); - assert_eq!(1, msgs.len(), "one message should be emitted"); - let (msg, hook) = msgs.pop().unwrap(); + let mut state = tx_ctx.commit(); + assert_eq!(1, state.messages.len(), "one message should be emitted"); + let (msg, hook) = state.messages.pop().unwrap(); assert_eq!( Message::Staking(Versioned::new( @@ -260,8 +260,8 @@ fn test_api_deposit() { ); // Make sure events were emitted. - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert_eq!(tags.len(), 2, "deposit and mint events should be emitted"); assert_eq!(tags[0].key, b"accounts\x00\x00\x00\x03"); // accounts.Mint (code = 3) event assert_eq!(tags[1].key, b"consensus_accounts\x00\x00\x00\x01"); // consensus_accounts.Deposit (code = 1) event @@ -461,9 +461,9 @@ fn test_api_withdraw(signer_sigspec: SignatureAddressSpec) { ) .expect("withdraw tx should succeed"); - let (_, mut msgs) = tx_ctx.commit(); - assert_eq!(1, msgs.len(), "one message should be emitted"); - let (msg, hook) = msgs.pop().unwrap(); + let mut state = tx_ctx.commit(); + assert_eq!(1, state.messages.len(), "one message should be emitted"); + let (msg, hook) = state.messages.pop().unwrap(); assert_eq!( Message::Staking(Versioned::new( @@ -514,8 +514,8 @@ fn test_api_withdraw(signer_sigspec: SignatureAddressSpec) { ); // Make sure events were emitted. - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert_eq!(tags.len(), 2, "withdraw and burn events should be emitted"); assert_eq!(tags[0].key, b"accounts\x00\x00\x00\x02"); // accounts.Burn (code = 2) event assert_eq!(tags[1].key, b"consensus_accounts\x00\x00\x00\x02"); // consensus_accounts.Withdraw (code = 2) event @@ -593,9 +593,9 @@ fn test_api_withdraw_handler_failure() { ) .expect("withdraw tx should succeed"); - let (_, mut msgs) = tx_ctx.commit(); - assert_eq!(1, msgs.len(), "one message should be emitted"); - let (msg, hook) = msgs.pop().unwrap(); + let mut state = tx_ctx.commit(); + assert_eq!(1, state.messages.len(), "one message should be emitted"); + let (msg, hook) = state.messages.pop().unwrap(); assert_eq!( Message::Staking(Versioned::new( @@ -651,8 +651,8 @@ fn test_api_withdraw_handler_failure() { ); // Make sure events were emitted. - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert_eq!( tags.len(), 2, @@ -745,9 +745,9 @@ fn perform_delegation(ctx: &mut C, success: bool) -> u64 { ) .expect("delegate tx should succeed"); - let (_, mut msgs) = tx_ctx.commit(); - assert_eq!(1, msgs.len(), "one message should be emitted"); - let (msg, hook) = msgs.pop().unwrap(); + let mut state = tx_ctx.commit(); + assert_eq!(1, state.messages.len(), "one message should be emitted"); + let (msg, hook) = state.messages.pop().unwrap(); assert_eq!( Message::Staking(Versioned::new( @@ -829,8 +829,8 @@ fn test_api_delegate() { ); // Make sure events were emitted. - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert_eq!(tags.len(), 2, "delegate and burn events should be emitted"); assert_eq!(tags[0].key, b"accounts\x00\x00\x00\x02"); // accounts.Burn (code = 2) event assert_eq!(tags[1].key, b"consensus_accounts\x00\x00\x00\x03"); // consensus_accounts.Delegate (code = 3) event @@ -948,8 +948,8 @@ fn test_api_delegate_fail() { ); // Make sure events were emitted. - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert_eq!(tags.len(), 2, "delegate and burn events should be emitted"); assert_eq!(tags[0].key, b"accounts\x00\x00\x00\x01"); // accounts.Transfer (code = 1) event assert_eq!(tags[1].key, b"consensus_accounts\x00\x00\x00\x03"); // consensus_accounts.Delegate (code = 3) event @@ -993,9 +993,9 @@ fn perform_undelegation( ) .expect("undelegate tx should succeed"); - let (_, mut msgs) = tx_ctx.commit(); - assert_eq!(1, msgs.len(), "one message should be emitted"); - let (msg, hook) = msgs.pop().unwrap(); + let mut state = tx_ctx.commit(); + assert_eq!(1, state.messages.len(), "one message should be emitted"); + let (msg, hook) = state.messages.pop().unwrap(); assert_eq!( Message::Staking(Versioned::new( @@ -1154,8 +1154,8 @@ fn test_api_undelegate() { let rt_address = Address::from_runtime_id(ctx.runtime_id()); // Make sure events were emitted. - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert_eq!(tags.len(), 1, "undelegate start event should be emitted"); assert_eq!(tags[0].key, b"consensus_accounts\x00\x00\x00\x04"); // consensus_accounts.UndelegateStart (code = 4) event @@ -1179,8 +1179,8 @@ fn test_api_undelegate() { Module::::end_block(&mut ctx); // Make sure nothing changes. - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert_eq!(tags.len(), 0, "no events should be emitted"); } @@ -1203,8 +1203,8 @@ fn test_api_undelegate() { Module::::end_block(&mut ctx); // Make sure events were emitted. - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert_eq!( tags.len(), 2, @@ -1289,8 +1289,8 @@ fn test_api_undelegate_fail() { let (nonce, _) = perform_undelegation(&mut ctx, Some(false)); // Make sure events were emitted. - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert_eq!(tags.len(), 1, "undelegate start event should be emitted"); assert_eq!(tags[0].key, b"consensus_accounts\x00\x00\x00\x04"); // consensus_accounts.UndelegateStart (code = 4) event @@ -1331,8 +1331,8 @@ fn test_api_undelegate_suspension() { let rt_address = Address::from_runtime_id(ctx.runtime_id()); // Make sure no events were emitted. - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert!(tags.is_empty(), "no events should be emitted"); // Simulate the runtime resuming and processing both undelegate results and the debonding period @@ -1378,8 +1378,8 @@ fn test_api_undelegate_suspension() { Module::::end_block(&mut ctx); // Make sure events were emitted. - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert_eq!( tags.len(), 3, diff --git a/runtime-sdk/src/modules/core/test.rs b/runtime-sdk/src/modules/core/test.rs index 62fcf2a869..f2cdb632fe 100644 --- a/runtime-sdk/src/modules/core/test.rs +++ b/runtime-sdk/src/modules/core/test.rs @@ -1018,8 +1018,8 @@ fn test_emit_events() { ctx.emit_event(TestEvent { i: 3 }); ctx.emit_event(TestEvent { i: 1 }); - let (etags, _) = ctx.commit(); - let tags = etags.clone().into_tags(); + let state = ctx.commit(); + let tags = state.events.clone().into_tags(); assert_eq!(tags.len(), 1, "1 emitted tag expected"); let events: Vec = cbor::from_slice(&tags[0].value).unwrap(); @@ -1028,15 +1028,15 @@ fn test_emit_events() { assert_eq!(TestEvent { i: 3 }, events[1], "expected events emitted"); assert_eq!(TestEvent { i: 1 }, events[2], "expected events emitted"); - etags + state.events }); // Forward tx emitted etags. ctx.emit_etags(etags); // Emit one more event. ctx.emit_event(TestEvent { i: 0 }); - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert_eq!(tags.len(), 1, "1 emitted tag expected"); let events: Vec = cbor::from_slice(&tags[0].value).unwrap(); @@ -1077,20 +1077,20 @@ fn test_gas_used_events() { ) .expect("after_handle_call should succeed"); - let (etags, _) = tx_ctx.commit(); - let tags = etags.clone().into_tags(); + let state = tx_ctx.commit(); + let tags = state.events.clone().into_tags(); assert_eq!(tags.len(), 1, "1 emitted tag expected"); let expected = cbor::to_vec(vec![Event::GasUsed { amount: 10 }]); assert_eq!(tags[0].value, expected, "expected events emitted"); - etags + state.events }); // Forward tx emitted etags. ctx.emit_etags(etags); - let (etags, _) = ctx.commit(); - let tags = etags.into_tags(); + let state = ctx.commit(); + let tags = state.events.into_tags(); assert_eq!(tags.len(), 1, "1 emitted tags expected"); let expected = cbor::to_vec(vec![Event::GasUsed { amount: 10 }]); diff --git a/runtime-sdk/src/subcall.rs b/runtime-sdk/src/subcall.rs index 43eef42a6f..f4d2263d80 100644 --- a/runtime-sdk/src/subcall.rs +++ b/runtime-sdk/src/subcall.rs @@ -1,8 +1,8 @@ //! Subcall dispatch. -use std::{cell::RefCell, collections::BTreeMap}; +use std::cell::RefCell; use crate::{ - context::{BatchContext, Context, TxContext}, + context::{BatchContext, Context, State, TxContext}, dispatcher, module::CallResult, modules::core::{Error, API as _}, @@ -49,6 +49,8 @@ pub struct SubcallInfo { /// Result of dispatching a subcall. #[derive(Debug)] pub struct SubcallResult { + /// State after applying the subcall context. + pub state: State, /// Result of the subcall. pub call_result: CallResult, /// Gas used by the subcall. @@ -137,7 +139,7 @@ pub fn call( let remaining_messages = ctx.remaining_messages(); // Execute a transaction in a child context. - let (call_result, gas, etags, messages) = ctx.with_child(ctx.mode(), |mut ctx| { + let (call_result, gas, state) = ctx.with_child(ctx.mode(), |mut ctx| { // Generate an internal transaction. let tx = transaction::Transaction { version: transaction::LATEST_TRANSACTION_VERSION, @@ -180,11 +182,11 @@ pub fn call( // Commit store and return emitted tags and messages on successful dispatch, // otherwise revert state and ignore any emitted events/messages. if result.is_success() { - let (etags, messages) = ctx.commit(); - TransactionResult::Commit((result, gas, etags, messages)) + let state = ctx.commit(); + TransactionResult::Commit((result, gas, state)) } else { // Ignore tags/messages on failure. - TransactionResult::Rollback((result, gas, BTreeMap::new(), vec![])) + TransactionResult::Rollback((result, gas, Default::default())) } }) }); @@ -198,16 +200,8 @@ pub fn call( // Compute the amount of gas used. let gas_used = info.max_gas.saturating_sub(gas); - // Forward any emitted event tags. - ctx.emit_etags(etags); - - // Forward any emitted runtime messages. - for (msg, hook) in messages { - // This should never fail as child context has the right limits configured. - ctx.emit_message(msg, hook)?; - } - Ok(SubcallResult { + state, call_result, gas_used, }) diff --git a/runtime-sdk/src/testing/mock.rs b/runtime-sdk/src/testing/mock.rs index ebb50d7f02..db9f33c535 100644 --- a/runtime-sdk/src/testing/mock.rs +++ b/runtime-sdk/src/testing/mock.rs @@ -12,15 +12,15 @@ use oasis_core_runtime::{ }; use crate::{ - context::{Mode, RuntimeBatchContext}, - history, + context::{BatchContext, Mode, RuntimeBatchContext}, + dispatcher, history, keymanager::KeyManager, module::MigrationHandler, modules, runtime::Runtime, storage::{CurrentStore, MKVSStore}, testing::{configmap, keymanager::MockKeyManagerClient}, - types::transaction, + types::{address::SignatureAddressSpec, transaction}, }; pub struct Config; @@ -176,3 +176,83 @@ pub fn transaction() -> transaction::Transaction { }, } } + +/// Options that can be used during mock signer calls. +#[derive(Clone, Debug)] +pub struct CallOptions { + /// Transaction fee. + pub fee: transaction::Fee, +} + +impl Default for CallOptions { + fn default() -> Self { + Self { + fee: transaction::Fee { + amount: Default::default(), + gas: 1_000_000, + consensus_messages: 0, + }, + } + } +} + +/// A mock signer for use during tests. +pub struct Signer { + nonce: u64, + sigspec: SignatureAddressSpec, +} + +impl Signer { + /// Create a new mock signer using the given nonce and signature spec. + pub fn new(nonce: u64, sigspec: SignatureAddressSpec) -> Self { + Self { nonce, sigspec } + } + + /// Dispatch a call to the given method. + pub fn call(&mut self, ctx: &mut C, method: &str, body: B) -> dispatcher::DispatchResult + where + C: BatchContext, + B: cbor::Encode, + { + self.call_opts(ctx, method, body, Default::default()) + } + + /// Dispatch a call to the given method with the given options. + pub fn call_opts( + &mut self, + ctx: &mut C, + method: &str, + body: B, + opts: CallOptions, + ) -> dispatcher::DispatchResult + where + C: BatchContext, + B: cbor::Encode, + { + let tx = transaction::Transaction { + version: 1, + call: transaction::Call { + format: transaction::CallFormat::Plain, + method: method.to_owned(), + body: cbor::to_value(body), + ..Default::default() + }, + auth_info: transaction::AuthInfo { + signer_info: vec![transaction::SignerInfo::new_sigspec( + self.sigspec.clone(), + self.nonce, + )], + fee: opts.fee, + ..Default::default() + }, + }; + + let result = dispatcher::Dispatcher::::dispatch_tx(ctx, 1024, tx, 0) + .expect("dispatch should work"); + + // Increment the nonce. + self.nonce += 1; + + result + } +} diff --git a/runtime-sdk/src/types/address.rs b/runtime-sdk/src/types/address.rs index 1c54f0192f..77136400e1 100644 --- a/runtime-sdk/src/types/address.rs +++ b/runtime-sdk/src/types/address.rs @@ -163,6 +163,15 @@ impl Address { Address::new(ADDRESS_V0_MULTISIG_CONTEXT, ADDRESS_V0_VERSION, &config_vec) } + /// Creates a new address from an Ethereum-compatible address. + pub fn from_eth(eth_address: &[u8]) -> Self { + Address::new( + ADDRESS_V0_SECP256K1ETH_CONTEXT, + ADDRESS_V0_VERSION, + eth_address, + ) + } + /// Tries to create a new address from Bech32-encoded string. pub fn from_bech32(data: &str) -> Result { let (hrp, data, variant) = bech32::decode(data).map_err(|_| Error::MalformedAddress)?; @@ -376,6 +385,16 @@ mod test { ); } + #[test] + fn test_address_from_eth() { + let eth_address = hex::decode("dce075e1c39b1ae0b75d554558b6451a226ffe00").unwrap(); + let addr = Address::from_eth(ð_address); + assert_eq!( + addr.to_bech32(), + "oasis1qrk58a6j2qn065m6p06jgjyt032f7qucy5wqeqpt" + ); + } + #[test] fn test_address_raw() { let eth_address = hex::decode("dce075e1c39b1ae0b75d554558b6451a226ffe00").unwrap(); diff --git a/tests/e2e/contracts/subcall/evm_subcall.sol b/tests/e2e/contracts/subcall/evm_subcall.sol new file mode 100644 index 0000000000..aed34ee50f --- /dev/null +++ b/tests/e2e/contracts/subcall/evm_subcall.sol @@ -0,0 +1,37 @@ +pragma solidity ^0.8.0; + +contract Test { + address private constant SUBCALL = 0x0100000000000000000000000000000000000102; + + error SubcallFailed(uint64 code, bytes module); + + function test(bytes calldata method, bytes calldata body) public payable returns (bytes memory) { + (bool success, bytes memory data) = SUBCALL.call(abi.encode(method, body)); + require(success, "subcall failed"); + return decodeResponse(data); + } + + function test_delegatecall(bytes calldata method, bytes calldata body) public returns (bytes memory) { + (bool success, bytes memory data) = SUBCALL.delegatecall(abi.encode(method, body)); + require(success, "subcall failed"); + return data; + } + + function test_spin(bytes calldata method, bytes calldata body) public returns (bytes memory) { + (bool success, bytes memory data) = SUBCALL.call(abi.encode(method, body)); + require(success, "subcall failed"); + for (int i = 0; i < 100; i++) { + // Spin. + } + return data; + } + + function decodeResponse(bytes memory raw) internal pure returns (bytes memory) { + (uint64 status_code, bytes memory data) = abi.decode(raw, (uint64, bytes)); + + if (status_code != 0) { + revert SubcallFailed(status_code, data); + } + return data; + } +} diff --git a/tests/e2e/contracts/subcall/evm_subcall_abi.json b/tests/e2e/contracts/subcall/evm_subcall_abi.json new file mode 100644 index 0000000000..73e2df3caa --- /dev/null +++ b/tests/e2e/contracts/subcall/evm_subcall_abi.json @@ -0,0 +1,90 @@ +[ + { + "inputs": [ + { + "internalType": "uint64", + "name": "code", + "type": "uint64" + }, + { + "internalType": "bytes", + "name": "module", + "type": "bytes" + } + ], + "name": "SubcallFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "method", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "body", + "type": "bytes" + } + ], + "name": "test", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "method", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "body", + "type": "bytes" + } + ], + "name": "test_delegatecall", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "method", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "body", + "type": "bytes" + } + ], + "name": "test_spin", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/tests/e2e/contracts/subcall/evm_subcall_compiled.hex b/tests/e2e/contracts/subcall/evm_subcall_compiled.hex new file mode 100644 index 0000000000..98387feb67 --- /dev/null +++ b/tests/e2e/contracts/subcall/evm_subcall_compiled.hex @@ -0,0 +1 @@ +608060405234801561001057600080fd5b50610a0c806100206000396000f3fe6080604052600436106100345760003560e01c80630c5561a61461003957806323bfb16a1461006957806350d82f38146100a6575b600080fd5b610053600480360381019061004e91906104df565b6100e3565b60405161006091906105f0565b60405180910390f35b34801561007557600080fd5b50610090600480360381019061008b91906104df565b6101e2565b60405161009d91906105f0565b60405180910390f35b3480156100b257600080fd5b506100cd60048036038101906100c891906104df565b6102d7565b6040516100da91906105f0565b60405180910390f35b606060008073010000000000000000000000000000000000010273ffffffffffffffffffffffffffffffffffffffff168787878760405160200161012a949392919061064e565b60405160208183030381529060405260405161014691906106c5565b6000604051808303816000865af19150503d8060008114610183576040519150601f19603f3d011682016040523d82523d6000602084013e610188565b606091505b5091509150816101cd576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016101c490610739565b60405180910390fd5b6101d6816103ee565b92505050949350505050565b606060008073010000000000000000000000000000000000010273ffffffffffffffffffffffffffffffffffffffff1687878787604051602001610229949392919061064e565b60405160208183030381529060405260405161024591906106c5565b600060405180830381855af49150503d8060008114610280576040519150601f19603f3d011682016040523d82523d6000602084013e610285565b606091505b5091509150816102ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102c190610739565b60405180910390fd5b8092505050949350505050565b606060008073010000000000000000000000000000000000010273ffffffffffffffffffffffffffffffffffffffff168787878760405160200161031e949392919061064e565b60405160208183030381529060405260405161033a91906106c5565b6000604051808303816000865af19150503d8060008114610377576040519150601f19603f3d011682016040523d82523d6000602084013e61037c565b606091505b5091509150816103c1576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103b890610739565b60405180910390fd5b60005b60648112156103e05780806103d890610792565b9150506103c4565b508092505050949350505050565b606060008083806020019051810190610407919061093b565b9150915060008267ffffffffffffffff161461045c5781816040517f575a7c4d0000000000000000000000000000000000000000000000000000000081526004016104539291906109a6565b60405180910390fd5b8092505050919050565b6000604051905090565b600080fd5b600080fd5b600080fd5b600080fd5b600080fd5b60008083601f84011261049f5761049e61047a565b5b8235905067ffffffffffffffff8111156104bc576104bb61047f565b5b6020830191508360018202830111156104d8576104d7610484565b5b9250929050565b600080600080604085870312156104f9576104f8610470565b5b600085013567ffffffffffffffff81111561051757610516610475565b5b61052387828801610489565b9450945050602085013567ffffffffffffffff81111561054657610545610475565b5b61055287828801610489565b925092505092959194509250565b600081519050919050565b600082825260208201905092915050565b60005b8381101561059a57808201518184015260208101905061057f565b60008484015250505050565b6000601f19601f8301169050919050565b60006105c282610560565b6105cc818561056b565b93506105dc81856020860161057c565b6105e5816105a6565b840191505092915050565b6000602082019050818103600083015261060a81846105b7565b905092915050565b82818337600083830152505050565b600061062d838561056b565b935061063a838584610612565b610643836105a6565b840190509392505050565b60006040820190508181036000830152610669818688610621565b9050818103602083015261067e818486610621565b905095945050505050565b600081905092915050565b600061069f82610560565b6106a98185610689565b93506106b981856020860161057c565b80840191505092915050565b60006106d18284610694565b915081905092915050565b600082825260208201905092915050565b7f73756263616c6c206661696c6564000000000000000000000000000000000000600082015250565b6000610723600e836106dc565b915061072e826106ed565b602082019050919050565b6000602082019050818103600083015261075281610716565b9050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000819050919050565b600061079d82610788565b91507f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff82036107cf576107ce610759565b5b600182019050919050565b600067ffffffffffffffff82169050919050565b6107f7816107da565b811461080257600080fd5b50565b600081519050610814816107ee565b92915050565b600080fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b610857826105a6565b810181811067ffffffffffffffff821117156108765761087561081f565b5b80604052505050565b6000610889610466565b9050610895828261084e565b919050565b600067ffffffffffffffff8211156108b5576108b461081f565b5b6108be826105a6565b9050602081019050919050565b60006108de6108d98461089a565b61087f565b9050828152602081018484840111156108fa576108f961081a565b5b61090584828561057c565b509392505050565b600082601f8301126109225761092161047a565b5b81516109328482602086016108cb565b91505092915050565b6000806040838503121561095257610951610470565b5b600061096085828601610805565b925050602083015167ffffffffffffffff81111561098157610980610475565b5b61098d8582860161090d565b9150509250929050565b6109a0816107da565b82525050565b60006040820190506109bb6000830185610997565b81810360208301526109cd81846105b7565b9050939250505056fea2646970667358221220bb4ee71d5a949c56534abbc225739b6bc549487627a3c5eaf9d7528688bff2e564736f6c63430008120033 \ No newline at end of file diff --git a/tests/e2e/contracts/subcall/subcall.go b/tests/e2e/contracts/subcall/subcall.go new file mode 100644 index 0000000000..4d8be9d110 --- /dev/null +++ b/tests/e2e/contracts/subcall/subcall.go @@ -0,0 +1,36 @@ +package subcall + +import ( + _ "embed" + "encoding/hex" + "fmt" + "strings" + + ethABI "github.com/ethereum/go-ethereum/accounts/abi" +) + +// CompiledHex is the compiled subcall contract in hex encoding. +// +//go:embed evm_subcall_compiled.hex +var CompiledHex string + +// Compiled is the compiled subcall contract. +var Compiled = func() []byte { + contract, err := hex.DecodeString(strings.TrimSpace(CompiledHex)) + if err != nil { + panic(fmt.Errorf("failed to decode contract: %w", err)) + } + return contract +}() + +//go:embed evm_subcall_abi.json +var evmSubcallABIJson string + +// ABI is the ABI of the subcall contract. +var ABI = func() ethABI.ABI { + abi, err := ethABI.JSON(strings.NewReader(evmSubcallABIJson)) + if err != nil { + panic(err) + } + return abi +}() diff --git a/tests/e2e/evmtest.go b/tests/e2e/evmtest.go index 80b6a90950..83706a5a66 100644 --- a/tests/e2e/evmtest.go +++ b/tests/e2e/evmtest.go @@ -7,6 +7,7 @@ import ( _ "embed" "encoding/hex" "fmt" + "math/big" "strings" ethMath "github.com/ethereum/go-ethereum/common/math" @@ -16,16 +17,21 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/cbor" "github.com/oasisprotocol/oasis-core/go/common/logging" "github.com/oasisprotocol/oasis-core/go/common/quantity" + consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" + "github.com/oasisprotocol/oasis-core/go/oasis-test-runner/oasis" + staking "github.com/oasisprotocol/oasis-core/go/staking/api" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/callformat" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/client" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/crypto/signature" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/accounts" + consensusAccounts "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/consensusaccounts" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/core" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/evm" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/testing" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/types" + contractSubcall "github.com/oasisprotocol/oasis-sdk/tests/e2e/contracts/subcall" "github.com/oasisprotocol/oasis-sdk/tests/e2e/txgen" ) @@ -139,6 +145,7 @@ func evmCall(ctx context.Context, rtc client.RuntimeClient, e evm.V1, signer sig } } + txB.SetFeeConsensusMessages(1) tx := txB.SetFeeAmount(types.NewBaseUnits(*quantity.NewFromUint64(gasPrice * gasLimit), types.NativeDenomination)).GetTransaction() result, err := txgen.SignAndSubmitTxRaw(ctx, rtc, signer, *tx, gasLimit) if err != nil { @@ -1168,6 +1175,63 @@ func C10lEVMMessageSigningTest(sc *RuntimeScenario, log *logging.Logger, conn *g return messageSigningEVMTest(log, rtc, c10l) } +// SubcallDelegationTest performs a delegation from the EVM by using the subcall precompile. +func SubcallDelegationTest(sc *RuntimeScenario, log *logging.Logger, conn *grpc.ClientConn, rtc client.RuntimeClient) error { + ctx := context.Background() + ev := evm.NewV1(rtc) + consAccounts := consensusAccounts.NewV1(rtc) + gasPrice := uint64(2) + + // Deploy the contract. + value := big.NewInt(0).Bytes() // Don't send any tokens. + contractAddr, err := evmCreate(ctx, rtc, ev, testing.Dave.Signer, value, contractSubcall.Compiled, gasPrice, nonc10l) + if err != nil { + return fmt.Errorf("failed to deploy contract: %w", err) + } + + // Start watching consensus and runtime events. + cons := consensus.NewConsensusClient(conn) + stakingClient := cons.Staking() + ch, sub, err := stakingClient.WatchEvents(ctx) + if err != nil { + return err + } + defer sub.Close() + acCh, err := rtc.WatchEvents(ctx, []client.EventDecoder{consAccounts}, false) + if err != nil { + return err + } + + // Call the method. + amount := types.NewBaseUnits(*quantity.NewFromUint64(10_000), types.NativeDenomination) + consensusAmount := quantity.NewFromUint64(10) // Consensus amount is scaled. + data, err := contractSubcall.ABI.Pack("test", []byte("consensus.Delegate"), cbor.Marshal(consensusAccounts.Delegate{ + To: testing.Alice.Address, + Amount: amount, + })) + if err != nil { + return fmt.Errorf("failed to pack arguments: %w", err) + } + + value = big.NewInt(10_000).Bytes() // Send tokens to contract so it has something to delegate. + _, err = evmCall(ctx, rtc, ev, testing.Dave.Signer, contractAddr, value, data, gasPrice, nonc10l) + if err != nil { + return fmt.Errorf("failed to call contract: %w", err) + } + + // Verify that delegation succeeded. + runtimeAddr := staking.NewRuntimeAddress(runtimeID) + contractSdkAddress := types.NewAddressFromEth(contractAddr) + if err = ensureStakingEvent(log, ch, makeAddEscrowCheck(runtimeAddr, staking.Address(testing.Alice.Address), consensusAmount)); err != nil { + return fmt.Errorf("ensuring runtime->alice add escrow consensus event: %w", err) + } + if err = ensureRuntimeEvent(log, acCh, makeDelegateCheck(contractSdkAddress, 0, testing.Alice.Address, amount)); err != nil { + return fmt.Errorf("ensuring contract delegate runtime event: %w", err) + } + + return nil +} + // EVMParametersTest tests parameters methods. func EVMParametersTest(sc *RuntimeScenario, log *logging.Logger, conn *grpc.ClientConn, rtc client.RuntimeClient) error { ctx := context.Background() @@ -1180,3 +1244,17 @@ func EVMParametersTest(sc *RuntimeScenario, log *logging.Logger, conn *grpc.Clie return nil } + +// EVMRuntimeFixture prepares the runtime fixture for the EVM tests. +func EVMRuntimeFixture(ff *oasis.NetworkFixture) { + // The EVM runtime has 110_000 TEST tokens already minted internally. Since we connect it to the + // consensus layer (via the consensus module), we should make sure that the runtime's account in + // the consensus layer also has a similar amount as otherwise the delegation tests will fail. + runtimeAddress := staking.NewRuntimeAddress(ff.Runtimes[1].ID) + _ = ff.Network.StakingGenesis.TotalSupply.Add(quantity.NewFromUint64(110_000)) + ff.Network.StakingGenesis.Ledger[runtimeAddress] = &staking.Account{ + General: staking.GeneralAccount{ + Balance: *quantity.NewFromUint64(110_000), + }, + } +} diff --git a/tests/e2e/runtime.go b/tests/e2e/runtime.go index 2228967e5c..310561ad18 100644 --- a/tests/e2e/runtime.go +++ b/tests/e2e/runtime.go @@ -74,31 +74,51 @@ type RuntimeScenario struct { // RunTest is a list of test functions to run once the network is up. RunTest []RunTestFunction - client client.RuntimeClient + client client.RuntimeClient + fixtureModifier FixtureModifierFunc +} + +// ScenarioOption is an option that can be specified to modify an aspect of the scenario. +type ScenarioOption func(*RuntimeScenario) + +// FixtureModifierFunc is a function that performs arbitrary modifications to a given fixture. +type FixtureModifierFunc func(*oasis.NetworkFixture) + +// WithCustomFixture applies the given fixture modifier function to the runtime scenario fixture. +func WithCustomFixture(fm FixtureModifierFunc) ScenarioOption { + return func(sc *RuntimeScenario) { + sc.fixtureModifier = fm + } } // NewRuntimeScenario creates a new runtime test scenario using the given // runtime and test functions. -func NewRuntimeScenario(runtimeName string, tests []RunTestFunction) *RuntimeScenario { +func NewRuntimeScenario(runtimeName string, tests []RunTestFunction, opts ...ScenarioOption) *RuntimeScenario { sc := &RuntimeScenario{ Scenario: *e2e.NewScenario(runtimeName), RuntimeName: runtimeName, RunTest: tests, } + sc.Flags.String(cfgRuntimeBinaryDirDefault, "../../target/debug", "path to the runtime binaries directory") sc.Flags.String(cfgRuntimeLoader, "../../../oasis-core/target/default/debug/oasis-core-runtime-loader", "path to the runtime loader") sc.Flags.String(cfgKeymanagerBinary, "", "path to the keymanager binary") sc.Flags.Bool(cfgIasMock, true, "if mock IAS service should be used") sc.Flags.String(cfgRuntimeProvisioner, "sandboxed", "the runtime provisioner: mock, unconfined, or sandboxed") + for _, opt := range opts { + opt(sc) + } + return sc } func (sc *RuntimeScenario) Clone() scenario.Scenario { return &RuntimeScenario{ - Scenario: sc.Scenario.Clone(), - RuntimeName: sc.RuntimeName, - RunTest: append(make([]RunTestFunction, 0, len(sc.RunTest)), sc.RunTest...), + Scenario: sc.Scenario.Clone(), + RuntimeName: sc.RuntimeName, + RunTest: append(make([]RunTestFunction, 0, len(sc.RunTest)), sc.RunTest...), + fixtureModifier: sc.fixtureModifier, } } @@ -272,6 +292,11 @@ func (sc *RuntimeScenario) Fixture() (*oasis.NetworkFixture, error) { } } + // Apply fixture modifier function when configured. + if sc.fixtureModifier != nil { + sc.fixtureModifier(ff) + } + return ff, nil } diff --git a/tests/e2e/scenarios.go b/tests/e2e/scenarios.go index e0e3ac636f..a9b2db0ed5 100644 --- a/tests/e2e/scenarios.go +++ b/tests/e2e/scenarios.go @@ -50,8 +50,9 @@ var ( SimpleERC20EVMTest, SimpleEVMSuicideTest, SimpleEVMCallSuicideTest, + SubcallDelegationTest, EVMParametersTest, - }) + }, WithCustomFixture(EVMRuntimeFixture)) // C10lEVMRuntime is the c10l-evm runtime test. C10lEVMRuntime = NewRuntimeScenario("test-runtime-c10l-evm", []RunTestFunction{ @@ -67,7 +68,7 @@ var ( C10lEVMRNGTest, C10lEVMMessageSigningTest, EVMParametersTest, - }) + }, WithCustomFixture(EVMRuntimeFixture)) // SimpleContractsRuntime is the simple-contracts runtime test. SimpleContractsRuntime = NewRuntimeScenario("test-runtime-simple-contracts", []RunTestFunction{ diff --git a/tests/e2e/simple_consensus.go b/tests/e2e/simple_consensus.go index 9bc3ff928b..f54112c743 100644 --- a/tests/e2e/simple_consensus.go +++ b/tests/e2e/simple_consensus.go @@ -251,10 +251,10 @@ func ConsensusDepositWithdrawalTest(sc *RuntimeScenario, log *logging.Logger, co cons := consensus.NewConsensusClient(conn) stakingClient := cons.Staking() ch, sub, err := stakingClient.WatchEvents(ctx) - defer sub.Close() if err != nil { return err } + defer sub.Close() consDenomination := types.Denomination("TEST") @@ -468,10 +468,10 @@ func ConsensusDelegationTest(sc *RuntimeScenario, log *logging.Logger, conn *grp cons := consensus.NewConsensusClient(conn) stakingClient := cons.Staking() ch, sub, err := stakingClient.WatchEvents(ctx) - defer sub.Close() if err != nil { return err } + defer sub.Close() consDenomination := types.Denomination("TEST") diff --git a/tests/runtimes/simple-evm/src/lib.rs b/tests/runtimes/simple-evm/src/lib.rs index 3b0aef51e1..2a47a9a3e3 100644 --- a/tests/runtimes/simple-evm/src/lib.rs +++ b/tests/runtimes/simple-evm/src/lib.rs @@ -40,6 +40,8 @@ impl sdk::Runtime for Runtime { type Modules = ( modules::accounts::Module, + modules::consensus::Module, + modules::consensus_accounts::Module, modules::core::Module, evm::Module, ); @@ -76,6 +78,21 @@ impl sdk::Runtime for Runtime { }, ..Default::default() }, + modules::consensus::Genesis { + parameters: modules::consensus::Parameters { + consensus_denomination: Denomination::NATIVE, + // Test scaling consensus base units when transferring them into the runtime. + consensus_scaling_factor: 1000, + }, + }, + modules::consensus_accounts::Genesis { + parameters: modules::consensus_accounts::Parameters { + // These are free, in order to simplify testing. We do test gas accounting + // with other methods elsewhere though. + gas_costs: Default::default(), + ..Default::default() + }, + }, modules::core::Genesis { parameters: modules::core::Parameters { max_batch_gas: 2_000_000,