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/src/backend.rs b/runtime-sdk/modules/evm/src/backend.rs index 93bba5dd84..24eebb980a 100644 --- a/runtime-sdk/modules/evm/src/backend.rs +++ b/runtime-sdk/modules/evm/src/backend.rs @@ -1,131 +1,316 @@ -//! EVM backend. -use std::{cell::RefCell, marker::PhantomData}; +use std::{ + cell::RefCell, + collections::{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 [`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; + + /// Perform a subcall. + 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) + } +} + +pub struct OasisStackSubstate<'config> { + metadata: StackSubstateMetadata<'config>, + parent: Option>>, + logs: Vec, + deletes: BTreeSet, + state: context::State, +} + +impl<'config> OasisStackSubstate<'config> { + pub fn new(metadata: StackSubstateMetadata<'config>) -> Self { + Self { + metadata, + parent: None, + logs: Vec::new(), + deletes: BTreeSet::new(), + state: context::State::default(), + } + } + + pub fn metadata(&self) -> &StackSubstateMetadata<'config> { + &self.metadata + } + + pub fn metadata_mut(&mut self) -> &mut StackSubstateMetadata<'config> { + &mut self.metadata + } + + pub 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)); + + CurrentStore::start_transaction(); + } + + pub 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); + + CurrentStore::commit_transaction(); + + Ok(()) + } + + pub fn exit_revert(&mut self) -> Result<(), ExitError> { + let mut exited = *self.parent.take().expect("Cannot discard on root substate"); + mem::swap(&mut exited, self); + + self.metadata.swallow_revert(exited.metadata)?; + + CurrentStore::rollback_transaction(); + + Ok(()) + } + + pub 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)?; + + CurrentStore::rollback_transaction(); + + Ok(()) + } + + 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) + } + } + + pub 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 + } + + pub fn log(&mut self, address: H160, topics: Vec, data: Vec) { + self.logs.push(Log { + address, + topics, + data, + }); + } + + pub fn set_deleted(&mut self, address: H160) { + self.deletes.insert(address); + } +} + +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> 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 +318,211 @@ 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) } -} -/// 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() + } + + fn exit_revert(&mut self) -> Result<(), ExitError> { + self.substate.exit_revert() + } + + fn exit_discard(&mut self) -> Result<(), ExitError> { + self.substate.exit_discard() + } + + 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)) + } -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) - }); - } - } - } + 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; + } + + let address = Cfg::map_address(address); + Cfg::Accounts::inc_nonce(address); + } + + 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. + use std::collections::btree_map::Entry::Vacant; + if let 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); } } - // 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})", - ); + 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. + } + + fn touch(&mut self, _address: H160) { + // Do not do anything. + } +} + +impl<'ctx, 'backend, 'config, C: TxContext, Cfg: Config> + OasisStackState<'ctx, 'backend, 'config, C, Cfg> +{ + pub fn new( + metadata: StackSubstateMetadata<'config>, + backend: &'backend OasisBackend<'ctx, C, Cfg>, + ) -> Self { + Self { + backend, + substate: OasisStackSubstate::new(metadata), + original_storage: BTreeMap::new(), + } + } + + 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/messages. + ctx.emit_etags(self.substate.state.events); + + // Forward any emitted runtime messages. + for (msg, hook) in self.substate.state.messages { + // This should never fail as child contexts have the right limits configured. + ctx.emit_message(msg, hook)?; + } // Emit logs as events. - for log in logs { - self.ctx.get_mut().emit_event(crate::Event::Log { + 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, }); } - evm::ExitSucceed::Returned.into() + Ok(()) } } diff --git a/runtime-sdk/modules/evm/src/lib.rs b/runtime-sdk/modules/evm/src/lib.rs index f0fe9294d8..9a71de4fcb 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)] @@ -12,7 +11,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; @@ -38,9 +37,9 @@ use oasis_runtime_sdk::{ }, }; -use backend::ApplyBackendResult; use types::{H160, H256, U256}; +pub mod mock; #[cfg(test)] mod test; @@ -533,8 +532,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), @@ -546,8 +545,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(). @@ -555,9 +554,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); @@ -579,11 +578,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..22c2609e0a --- /dev/null +++ b/runtime-sdk/modules/evm/src/mock.rs @@ -0,0 +1,67 @@ +//! Mock functionality for use during testing. +use uint::hex::FromHex; + +use oasis_runtime_sdk::{ + dispatcher, testing::mock::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, + { + let data = [ + ethabi::short_signature(name, param_types).to_vec(), + ethabi::encode(params), + ] + .concat(); + + self.call( + ctx, + "evm.Call", + types::Call { + address, + value: 0.into(), + data, + }, + ) + } +} + +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 b013a94131..a89f79c814 100644 --- a/runtime-sdk/modules/evm/src/precompile/mod.rs +++ b/runtime-sdk/modules/evm/src/precompile/mod.rs @@ -12,6 +12,7 @@ use crate::{backend::EVMBackendExt, Config}; mod confidential; mod standard; +mod subcall; #[cfg(test)] mod test; @@ -80,33 +81,36 @@ impl PrecompileSet for Precompiles<'_, Cfg, B> { if !self.is_precompile(address) { return None; } - Some(match (address[0], address[19]) { - (0, 1) => standard::call_ecrecover(handle), - (0, 2) => standard::call_sha256(handle), - (0, 3) => standard::call_ripemd160(handle), - (0, 4) => standard::call_datacopy(handle), - (0, 5) => standard::call_bigmodexp(handle), - (1, 1) => confidential::call_random_bytes(handle, self.backend), - (1, 2) => confidential::call_x25519_derive(handle), - (1, 3) => confidential::call_deoxysii_seal(handle), - (1, 4) => confidential::call_deoxysii_open(handle), - (1, 5) => confidential::call_keypair_generate(handle), - (1, 6) => confidential::call_sign(handle), - (1, 7) => confidential::call_verify(handle), - (1, 8) => confidential::call_curve25519_compute_public(handle), + Some(match (address[0], address[18], address[19]) { + // Ethereum-compatible. + (0, 0, 1) => standard::call_ecrecover(handle), + (0, 0, 2) => standard::call_sha256(handle), + (0, 0, 3) => standard::call_ripemd160(handle), + (0, 0, 4) => standard::call_datacopy(handle), + (0, 0, 5) => standard::call_bigmodexp(handle), + // Oasis-specific, confidential-only. + (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), + (1, 0, 4) => confidential::call_deoxysii_open(handle), + (1, 0, 5) => confidential::call_keypair_generate(handle), + (1, 0, 6) => confidential::call_sign(handle), + (1, 0, 7) => confidential::call_verify(handle), + (1, 0, 8) => confidential::call_curve25519_compute_public(handle), + // Oasis-specific, general. + (1, 1, 1) => subcall::call_subcall(handle, self.backend), _ => return Cfg::additional_precompiles().and_then(|pc| pc.execute(handle)), }) } fn is_precompile(&self, address: H160) -> bool { - // All Ethereum precompiles are zero except for the last byte, which is no more than five. - // Otherwise, when confidentiality is enabled, Oasis precompiles start with one and have a last byte of no more than four. + // See above table in `execute` for matching on what is a valid precompile address. let addr_bytes = address.as_bytes(); - let (first, last) = (address[0], addr_bytes[19]); - (address[1..19].iter().all(|b| *b == 0) + let (a0, a18, a19) = (address[0], addr_bytes[18], addr_bytes[19]); + (address[1..18].iter().all(|b| *b == 0) && matches!( - (first, last, Cfg::CONFIDENTIAL), - (0, 1..=5, _) | (1, 1..=8, true) + (a0, a18, a19, Cfg::CONFIDENTIAL), + (0, 0, 1..=5, _) | (1, 0, 1..=8, true) | (1, 1, 1, _) )) || 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..c53e8f9366 --- /dev/null +++ b/runtime-sdk/modules/evm/src/precompile/subcall.rs @@ -0,0 +1,371 @@ +use ethabi::ParamType; +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(()) + } +} + +// XXX: fix these costs +const SUBCALL_BASE_COST: u64 = 1_000; +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 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: cbor::to_vec(value), + }), + CallResult::Failed { message, .. } => Err(PrecompileFailure::Error { + exit_status: ExitError::Other(message.into()), + }), + 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, + modules::{accounts, core}, + testing::{keys, mock::Mock}, + types::token::{self, BaseUnits, Denomination}, + 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: core::GasCosts { + tx_byte: 0, + auth_signature: 0, + auth_multisig_signer: 0, + callformat_x25519_deoxysii: 0, + }, + 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/evm_subcall_compiled.hex"); + + 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"); + } + + #[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"); + } + } +} diff --git a/runtime-sdk/modules/evm/src/precompile/test.rs b/runtime-sdk/modules/evm/src/precompile/test.rs index 71ee250436..88380316ca 100644 --- a/runtime-sdk/modules/evm/src/precompile/test.rs +++ b/runtime-sdk/modules/evm/src/precompile/test.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/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..570dd34ada 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,54 @@ pub fn transaction() -> transaction::Transaction { }, } } + +/// 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, + { + 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, + )], + // TODO: Support customizing the fee structure. + fee: transaction::Fee { + amount: Default::default(), + gas: 1000000, + consensus_messages: 0, + }, + ..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/tests/e2e/contracts/evm_subcall.sol b/tests/e2e/contracts/evm_subcall.sol new file mode 100644 index 0000000000..05f12315bf --- /dev/null +++ b/tests/e2e/contracts/evm_subcall.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.8.0; + +contract Test { + address private constant SUBCALL = 0x0100000000000000000000000000000000000101; + + function test(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"); + return 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; + } +} diff --git a/tests/e2e/contracts/evm_subcall_compiled.hex b/tests/e2e/contracts/evm_subcall_compiled.hex new file mode 100644 index 0000000000..dc40632889 --- /dev/null +++ b/tests/e2e/contracts/evm_subcall_compiled.hex @@ -0,0 +1 @@ +608060405234801561001057600080fd5b506105a6806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80630c5561a61461003b57806323bfb16a1461006b575b600080fd5b610055600480360381019061005091906102f6565b61009b565b6040516100629190610407565b60405180910390f35b610085600480360381019061008091906102f6565b610192565b6040516100929190610407565b60405180910390f35b606060008073010000000000000000000000000000000000010173ffffffffffffffffffffffffffffffffffffffff16878787876040516020016100e29493929190610465565b6040516020818303038152906040526040516100fe91906104dc565b6000604051808303816000865af19150503d806000811461013b576040519150601f19603f3d011682016040523d82523d6000602084013e610140565b606091505b509150915081610185576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161017c90610550565b60405180910390fd5b8092505050949350505050565b606060008073010000000000000000000000000000000000010173ffffffffffffffffffffffffffffffffffffffff16878787876040516020016101d99493929190610465565b6040516020818303038152906040526040516101f591906104dc565b600060405180830381855af49150503d8060008114610230576040519150601f19603f3d011682016040523d82523d6000602084013e610235565b606091505b50915091508161027a576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161027190610550565b60405180910390fd5b8092505050949350505050565b600080fd5b600080fd5b600080fd5b600080fd5b600080fd5b60008083601f8401126102b6576102b5610291565b5b8235905067ffffffffffffffff8111156102d3576102d2610296565b5b6020830191508360018202830111156102ef576102ee61029b565b5b9250929050565b600080600080604085870312156103105761030f610287565b5b600085013567ffffffffffffffff81111561032e5761032d61028c565b5b61033a878288016102a0565b9450945050602085013567ffffffffffffffff81111561035d5761035c61028c565b5b610369878288016102a0565b925092505092959194509250565b600081519050919050565b600082825260208201905092915050565b60005b838110156103b1578082015181840152602081019050610396565b60008484015250505050565b6000601f19601f8301169050919050565b60006103d982610377565b6103e38185610382565b93506103f3818560208601610393565b6103fc816103bd565b840191505092915050565b6000602082019050818103600083015261042181846103ce565b905092915050565b82818337600083830152505050565b60006104448385610382565b9350610451838584610429565b61045a836103bd565b840190509392505050565b60006040820190508181036000830152610480818688610438565b90508181036020830152610495818486610438565b905095945050505050565b600081905092915050565b60006104b682610377565b6104c081856104a0565b93506104d0818560208601610393565b80840191505092915050565b60006104e882846104ab565b915081905092915050565b600082825260208201905092915050565b7f73756263616c6c206661696c6564000000000000000000000000000000000000600082015250565b600061053a600e836104f3565b915061054582610504565b602082019050919050565b600060208201905081810360008301526105698161052d565b905091905056fea26469706673582212206c71e5a5a4ce75bb78eeb80c0636512ad48c6453d59b3ed617eb4cfe3c4bcfa564736f6c63430008120033 \ No newline at end of file