diff --git a/engine-precompiles/src/lib.rs b/engine-precompiles/src/lib.rs index 68708f5d7..73ce32444 100644 --- a/engine-precompiles/src/lib.rs +++ b/engine-precompiles/src/lib.rs @@ -12,6 +12,7 @@ pub mod modexp; pub mod native; mod prelude; pub mod prepaid_gas; +pub mod promise_result; pub mod random; pub mod secp256k1; mod utils; @@ -30,10 +31,12 @@ use crate::random::RandomSeed; use crate::secp256k1::ECRecover; use aurora_engine_sdk::env::Env; use aurora_engine_sdk::io::IO; +use aurora_engine_sdk::promise::ReadOnlyPromiseHandler; use aurora_engine_types::{account_id::AccountId, types::Address, vec, BTreeMap, Box}; use evm::backend::Log; use evm::executor::{self, stack::PrecompileHandle}; use evm::{Context, ExitError, ExitSucceed}; +use promise_result::PromiseResult; #[derive(Debug, Default)] pub struct PrecompileOutput { @@ -94,7 +97,7 @@ impl HardFork for Istanbul {} impl HardFork for Berlin {} -pub struct Precompiles<'a, I, E> { +pub struct Precompiles<'a, I, E, H> { pub generic_precompiles: prelude::BTreeMap>, // Cannot be part of the generic precompiles because the `dyn` type-erasure messes with // with the lifetime requirements on the type parameter `I`. @@ -102,9 +105,12 @@ pub struct Precompiles<'a, I, E> { pub ethereum_exit: ExitToEthereum, pub predecessor_account_id: PredecessorAccount<'a, E>, pub prepaid_gas: PrepaidGas<'a, E>, + pub promise_results: PromiseResult, } -impl<'a, I: IO + Copy, E: Env> executor::stack::PrecompileSet for Precompiles<'a, I, E> { +impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> executor::stack::PrecompileSet + for Precompiles<'a, I, E, H> +{ fn execute( &self, handle: &mut impl PrecompileHandle, @@ -139,16 +145,17 @@ impl<'a, I: IO + Copy, E: Env> executor::stack::PrecompileSet for Precompiles<'a } } -pub struct PrecompileConstructorContext<'a, I, E> { +pub struct PrecompileConstructorContext<'a, I, E, H> { pub current_account_id: AccountId, pub random_seed: H256, pub io: I, pub env: &'a E, + pub promise_handler: H, } -impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> { +impl<'a, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> Precompiles<'a, I, E, H> { #[allow(dead_code)] - pub fn new_homestead(ctx: PrecompileConstructorContext<'a, I, E>) -> Self { + pub fn new_homestead(ctx: PrecompileConstructorContext<'a, I, E, H>) -> Self { let addresses = vec![ ECRecover::ADDRESS, SHA256::ADDRESS, @@ -168,7 +175,7 @@ impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> { } #[allow(dead_code)] - pub fn new_byzantium(ctx: PrecompileConstructorContext<'a, I, E>) -> Self { + pub fn new_byzantium(ctx: PrecompileConstructorContext<'a, I, E, H>) -> Self { let addresses = vec![ ECRecover::ADDRESS, SHA256::ADDRESS, @@ -198,7 +205,7 @@ impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> { Self::with_generic_precompiles(map, ctx) } - pub fn new_istanbul(ctx: PrecompileConstructorContext<'a, I, E>) -> Self { + pub fn new_istanbul(ctx: PrecompileConstructorContext<'a, I, E, H>) -> Self { let addresses = vec![ ECRecover::ADDRESS, SHA256::ADDRESS, @@ -230,7 +237,7 @@ impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> { Self::with_generic_precompiles(map, ctx) } - pub fn new_berlin(ctx: PrecompileConstructorContext<'a, I, E>) -> Self { + pub fn new_berlin(ctx: PrecompileConstructorContext<'a, I, E, H>) -> Self { let addresses = vec![ ECRecover::ADDRESS, SHA256::ADDRESS, @@ -262,19 +269,20 @@ impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> { Self::with_generic_precompiles(map, ctx) } - pub fn new_london(ctx: PrecompileConstructorContext<'a, I, E>) -> Self { + pub fn new_london(ctx: PrecompileConstructorContext<'a, I, E, H>) -> Self { // no precompile changes in London HF Self::new_berlin(ctx) } fn with_generic_precompiles( generic_precompiles: BTreeMap>, - ctx: PrecompileConstructorContext<'a, I, E>, + ctx: PrecompileConstructorContext<'a, I, E, H>, ) -> Self { let near_exit = ExitToNear::new(ctx.current_account_id.clone(), ctx.io); let ethereum_exit = ExitToEthereum::new(ctx.current_account_id, ctx.io); let predecessor_account_id = PredecessorAccount::new(ctx.env); let prepaid_gas = PrepaidGas::new(ctx.env); + let promise_results = PromiseResult::new(ctx.promise_handler); Self { generic_precompiles, @@ -282,6 +290,7 @@ impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> { ethereum_exit, predecessor_account_id, prepaid_gas, + promise_results, } } @@ -298,6 +307,8 @@ impl<'a, I: IO + Copy, E: Env> Precompiles<'a, I, E> { return Some(f(&self.predecessor_account_id)); } else if address == prepaid_gas::ADDRESS { return Some(f(&self.prepaid_gas)); + } else if address == promise_result::ADDRESS { + return Some(f(&self.promise_results)); } self.generic_precompiles .get(&address) diff --git a/engine-precompiles/src/prepaid_gas.rs b/engine-precompiles/src/prepaid_gas.rs index 041a23bce..9b59b7988 100644 --- a/engine-precompiles/src/prepaid_gas.rs +++ b/engine-precompiles/src/prepaid_gas.rs @@ -5,7 +5,7 @@ use aurora_engine_sdk::env::Env; use aurora_engine_types::{vec, U256}; use evm::{Context, ExitError}; -/// predecessor_account_id precompile address +/// prepaid_gas precompile address /// /// Address: `0x536822d27de53629ef1f84c60555689e9488609f` /// This address is computed as: `&keccak("prepaidGas")[12..]` diff --git a/engine-precompiles/src/promise_result.rs b/engine-precompiles/src/promise_result.rs new file mode 100644 index 000000000..1899f66a6 --- /dev/null +++ b/engine-precompiles/src/promise_result.rs @@ -0,0 +1,90 @@ +use super::{EvmPrecompileResult, Precompile}; +use crate::prelude::types::{Address, EthGas}; +use crate::PrecompileOutput; +use aurora_engine_sdk::promise::ReadOnlyPromiseHandler; +use aurora_engine_types::{Cow, Vec}; +use borsh::BorshSerialize; +use evm::{Context, ExitError}; + +/// get_promise_results precompile address +/// +/// Address: `0x0a3540f79be10ef14890e87c1a0040a68cc6af71` +/// This address is computed as: `&keccak("getPromiseResults")[12..]` +pub const ADDRESS: Address = crate::make_address(0x0a3540f7, 0x9be10ef14890e87c1a0040a68cc6af71); + +pub mod costs { + use crate::prelude::types::EthGas; + + /// This cost is always charged for calling this precompile. + pub const PROMISE_RESULT_BASE_COST: EthGas = EthGas::new(125); + /// This is the cost per byte of promise result data. + pub const PROMISE_RESULT_BYTE_COST: EthGas = EthGas::new(1); +} + +pub struct PromiseResult { + handler: H, +} + +impl PromiseResult { + pub fn new(handler: H) -> Self { + Self { handler } + } +} + +impl Precompile for PromiseResult { + fn required_gas(_input: &[u8]) -> Result { + // Only gives the cost we can know without reading any promise data. + // This allows failing fast in the case the base cost cannot even be covered. + Ok(costs::PROMISE_RESULT_BASE_COST) + } + + fn run( + &self, + input: &[u8], + target_gas: Option, + _context: &Context, + _is_static: bool, + ) -> EvmPrecompileResult { + let mut cost = Self::required_gas(input)?; + let check_cost = |cost: EthGas| -> Result<(), ExitError> { + if let Some(target_gas) = target_gas { + if cost > target_gas { + return Err(ExitError::OutOfGas); + } + } + Ok(()) + }; + check_cost(cost)?; + + let num_promises = self.handler.ro_promise_results_count(); + let n_usize = usize::try_from(num_promises).map_err(crate::utils::err_usize_conv)?; + let mut results = Vec::with_capacity(n_usize); + for i in 0..num_promises { + if let Some(result) = self.handler.ro_promise_result(i) { + let n_bytes = u64::try_from(result.size()).map_err(crate::utils::err_usize_conv)?; + cost += n_bytes * costs::PROMISE_RESULT_BYTE_COST; + check_cost(cost)?; + results.push(result); + } + } + + let bytes = results + .try_to_vec() + .map_err(|_| ExitError::Other(Cow::Borrowed("ERR_PROMISE_RESULT_SERIALIZATION")))?; + Ok(PrecompileOutput::without_logs(cost, bytes)) + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::sdk::types::near_account_to_evm_address; + use crate::promise_result; + + #[test] + fn test_get_promise_results_precompile_id() { + assert_eq!( + promise_result::ADDRESS, + near_account_to_evm_address("getPromiseResults".as_bytes()) + ); + } +} diff --git a/engine-sdk/src/near_runtime.rs b/engine-sdk/src/near_runtime.rs index 23a62b6a9..7852fd978 100644 --- a/engine-sdk/src/near_runtime.rs +++ b/engine-sdk/src/near_runtime.rs @@ -260,6 +260,8 @@ impl crate::env::Env for Runtime { } impl crate::promise::PromiseHandler for Runtime { + type ReadOnly = Self; + fn promise_results_count(&self) -> u64 { unsafe { exports::promise_results_count() } } @@ -379,6 +381,10 @@ impl crate::promise::PromiseHandler for Runtime { exports::promise_return(promise.raw()); } } + + fn read_only(&self) -> Self::ReadOnly { + Self + } } /// Some host functions are not usable in NEAR view calls. diff --git a/engine-sdk/src/promise.rs b/engine-sdk/src/promise.rs index 86a2b85bc..c6f0bac73 100644 --- a/engine-sdk/src/promise.rs +++ b/engine-sdk/src/promise.rs @@ -17,6 +17,8 @@ impl PromiseId { } pub trait PromiseHandler { + type ReadOnly: ReadOnlyPromiseHandler; + fn promise_results_count(&self) -> u64; fn promise_result(&self, index: u64) -> Option; @@ -33,4 +35,59 @@ pub trait PromiseHandler { let base = self.promise_create_call(&args.base); self.promise_attach_callback(base, &args.callback) } + + fn read_only(&self) -> Self::ReadOnly; +} + +pub trait ReadOnlyPromiseHandler { + fn ro_promise_results_count(&self) -> u64; + fn ro_promise_result(&self, index: u64) -> Option; +} + +impl ReadOnlyPromiseHandler for T { + fn ro_promise_results_count(&self) -> u64 { + self.promise_results_count() + } + + fn ro_promise_result(&self, index: u64) -> Option { + self.promise_result(index) + } +} + +/// A promise handler which does nothing. Should only be used when promises can be safely ignored. +#[derive(Debug, Copy, Clone)] +pub struct Noop; + +impl PromiseHandler for Noop { + type ReadOnly = Self; + + fn promise_results_count(&self) -> u64 { + 0 + } + + fn promise_result(&self, _index: u64) -> Option { + None + } + + fn promise_create_call(&mut self, _args: &PromiseCreateArgs) -> PromiseId { + PromiseId::new(0) + } + + fn promise_attach_callback( + &mut self, + _base: PromiseId, + _callback: &PromiseCreateArgs, + ) -> PromiseId { + PromiseId::new(0) + } + + fn promise_create_batch(&mut self, _args: &PromiseBatchAction) -> PromiseId { + PromiseId::new(0) + } + + fn promise_return(&mut self, _promise: PromiseId) {} + + fn read_only(&self) -> Self::ReadOnly { + Self + } } diff --git a/engine-standalone-storage/src/promise.rs b/engine-standalone-storage/src/promise.rs index 2293fa33d..19ff476d9 100644 --- a/engine-standalone-storage/src/promise.rs +++ b/engine-standalone-storage/src/promise.rs @@ -2,16 +2,31 @@ use aurora_engine_sdk::promise::{PromiseHandler, PromiseId}; use aurora_engine_types::parameters::{PromiseBatchAction, PromiseCreateArgs}; use aurora_engine_types::types::PromiseResult; -/// A promise handler which does nothing. Should only be used when promises can be safely ignored. -pub struct Noop; +/// Implements `PromiseHandler` so that it can be used in the standalone engine implementation of +/// methods like `call`, however since the standalone engine cannot schedule promises in a +/// meaningful way, the mutable implementations are no-ops. Functionally, this is only an implementation +/// of `ReadOnlyPromiseHandler`, which is needed for the standalone engine to properly serve the +/// EVM precompile that gives back information on the results of promises (possibly scheduled using +/// the cross-contract calls feature). +#[derive(Debug, Clone, Copy)] +pub struct NoScheduler<'a> { + pub promise_data: &'a [Option>], +} + +impl<'a> PromiseHandler for NoScheduler<'a> { + type ReadOnly = Self; -impl PromiseHandler for Noop { fn promise_results_count(&self) -> u64 { - 0 + u64::try_from(self.promise_data.len()).unwrap_or_default() } - fn promise_result(&self, _index: u64) -> Option { - None + fn promise_result(&self, index: u64) -> Option { + let i = usize::try_from(index).ok()?; + let result = match self.promise_data.get(i)? { + Some(bytes) => PromiseResult::Successful(bytes.clone()), + None => PromiseResult::Failed, + }; + Some(result) } fn promise_create_call(&mut self, _args: &PromiseCreateArgs) -> PromiseId { @@ -31,4 +46,8 @@ impl PromiseHandler for Noop { } fn promise_return(&mut self, _promise: PromiseId) {} + + fn read_only(&self) -> Self::ReadOnly { + *self + } } diff --git a/engine-standalone-storage/src/relayer_db/mod.rs b/engine-standalone-storage/src/relayer_db/mod.rs index 9771b4e75..b2f7b68a6 100644 --- a/engine-standalone-storage/src/relayer_db/mod.rs +++ b/engine-standalone-storage/src/relayer_db/mod.rs @@ -83,7 +83,8 @@ where random_seed: H256::zero(), prepaid_gas: DEFAULT_PREPAID_GAS, }; - let mut handler = crate::promise::Noop; + // We use the Noop handler here since the relayer DB does not contain any promise information. + let mut handler = aurora_engine_sdk::promise::Noop; while let Some(row) = rows.next()? { let near_tx_hash = row.near_hash; @@ -150,6 +151,7 @@ where caller: env.predecessor_account_id(), attached_near: 0, transaction: crate::sync::types::TransactionKind::Submit(tx), + promise_data: Vec::new(), }; storage.set_transaction_included(tx_hash, &tx_msg, &diff)?; } @@ -256,6 +258,7 @@ mod test { caller: "aurora".parse().unwrap(), attached_near: 0, transaction: TransactionKind::Unknown, + promise_data: Vec::new(), }, &diff, ) diff --git a/engine-standalone-storage/src/sync/mod.rs b/engine-standalone-storage/src/sync/mod.rs index 6f233e8a0..89c6d2f48 100644 --- a/engine-standalone-storage/src/sync/mod.rs +++ b/engine-standalone-storage/src/sync/mod.rs @@ -128,7 +128,9 @@ fn execute_transaction<'db>( TransactionKind::Submit(tx) => { // We can ignore promises in the standalone engine because it processes each receipt separately // and it is fed a stream of receipts (it does not schedule them) - let mut handler = crate::promise::Noop; + let mut handler = crate::promise::NoScheduler { + promise_data: &transaction_message.promise_data, + }; let transaction_bytes: Vec = tx.into(); let tx_hash = aurora_engine_sdk::keccak(&transaction_bytes); @@ -151,7 +153,13 @@ fn execute_transaction<'db>( } other => { - let result = non_submit_execute(other, io, env, relayer_address); + let result = non_submit_execute( + other, + io, + env, + relayer_address, + &transaction_message.promise_data, + ); (near_receipt_id, result) } }; @@ -169,11 +177,12 @@ fn non_submit_execute<'db>( mut io: EngineStateAccess<'db, 'db, 'db>, env: env::Fixed, relayer_address: Address, + promise_data: &[Option>], ) -> Result, error::Error> { let result = match transaction { TransactionKind::Call(args) => { // We can ignore promises in the standalone engine (see above) - let mut handler = crate::promise::Noop; + let mut handler = crate::promise::NoScheduler { promise_data }; let mut engine = engine::Engine::new(relayer_address, env.current_account_id(), io, &env)?; @@ -184,7 +193,7 @@ fn non_submit_execute<'db>( TransactionKind::Deploy(input) => { // We can ignore promises in the standalone engine (see above) - let mut handler = crate::promise::Noop; + let mut handler = crate::promise::NoScheduler { promise_data }; let mut engine = engine::Engine::new(relayer_address, env.current_account_id(), io, &env)?; @@ -195,7 +204,7 @@ fn non_submit_execute<'db>( TransactionKind::DeployErc20(args) => { // No promises can be created by `deploy_erc20_token` - let mut handler = crate::promise::Noop; + let mut handler = crate::promise::NoScheduler { promise_data }; let result = engine::deploy_erc20_token(args.clone(), io, &env, &mut handler)?; Some(TransactionExecutionResult::DeployErc20(result)) @@ -203,7 +212,7 @@ fn non_submit_execute<'db>( TransactionKind::FtOnTransfer(args) => { // No promises can be created by `ft_on_transfer` - let mut handler = crate::promise::Noop; + let mut handler = crate::promise::NoScheduler { promise_data }; let mut engine = engine::Engine::new(relayer_address, env.current_account_id(), io, &env)?; @@ -328,7 +337,7 @@ fn non_submit_execute<'db>( maybe_args .clone() .map(|args| { - let mut handler = crate::promise::Noop; + let mut handler = crate::promise::NoScheduler { promise_data }; let engine_state = engine::get_state(&io)?; let result = engine::refund_on_error(io, &env, engine_state, args, &mut handler); diff --git a/engine-standalone-storage/src/sync/types.rs b/engine-standalone-storage/src/sync/types.rs index 91c1b34a9..395ebfbeb 100644 --- a/engine-standalone-storage/src/sync/types.rs +++ b/engine-standalone-storage/src/sync/types.rs @@ -39,6 +39,9 @@ pub struct TransactionMessage { pub attached_near: u128, /// Details of the transaction that was executed pub transaction: TransactionKind, + /// Results from previous NEAR receipts + /// (only present when this transaction is a callback of another transaction). + pub promise_data: Vec>>, } impl TransactionMessage { @@ -48,7 +51,12 @@ impl TransactionMessage { } pub fn try_from_slice(bytes: &[u8]) -> Result { - let borshable = BorshableTransactionMessage::try_from_slice(bytes)?; + let borshable = match BorshableTransactionMessage::try_from_slice(bytes) { + Ok(b) => b, + // To avoid DB migration, allow fallback on deserializing V1 messages + Err(_) => BorshableTransactionMessageV1::try_from_slice(bytes) + .map(BorshableTransactionMessage::V1)?, + }; Self::try_from(borshable).map_err(|e| { let message = e.as_str(); std::io::Error::new(std::io::ErrorKind::Other, message) @@ -105,30 +113,55 @@ pub enum TransactionKind { Unknown, } +/// This data type represents `TransactionMessage` above in the way consistent with how it is +/// stored on disk (in the DB). This type implements borsh (de)serialization. The purpose of +/// having a private struct for borsh, which is separate from the main `TransactionMessage` +/// which is used in the actual logic of executing transactions, +/// is to decouple the on-disk representation of the data from how it is used in the code. +/// This allows us to keep the `TransactionMessage` structure clean (no need to worry about +/// backwards compatibility with storage), hiding the complexity which is not important to +/// the logic of processing transactions. +/// +/// V1 is an older version of `TransactionMessage`, before the addition of `promise_data`. +/// +/// V2 is a structurally identical message to `TransactionMessage` above. +/// +/// For details of what the individual fields mean, see the comments on the main +/// `TransactionMessage` type. #[derive(BorshDeserialize, BorshSerialize)] -struct BorshableTransactionMessage<'a> { - /// Hash of the block which included this transaction +enum BorshableTransactionMessage<'a> { + V1(BorshableTransactionMessageV1<'a>), + V2(BorshableTransactionMessageV2<'a>), +} + +#[derive(BorshDeserialize, BorshSerialize)] +struct BorshableTransactionMessageV1<'a> { pub block_hash: [u8; 32], - /// Receipt ID of the receipt that was actually executed on NEAR pub near_receipt_id: [u8; 32], - /// If multiple Aurora transactions are included in the same block, - /// this index gives the order in which they should be executed. pub position: u16, - /// True if the transaction executed successfully on the blockchain, false otherwise. pub succeeded: bool, - /// NEAR account that signed the transaction pub signer: Cow<'a, AccountId>, - /// NEAR account that called the Aurora engine contract pub caller: Cow<'a, AccountId>, - /// Amount of NEAR token attached to the transaction pub attached_near: u128, - /// Details of the transaction that was executed pub transaction: BorshableTransactionKind<'a>, } +#[derive(BorshDeserialize, BorshSerialize)] +struct BorshableTransactionMessageV2<'a> { + pub block_hash: [u8; 32], + pub near_receipt_id: [u8; 32], + pub position: u16, + pub succeeded: bool, + pub signer: Cow<'a, AccountId>, + pub caller: Cow<'a, AccountId>, + pub attached_near: u128, + pub transaction: BorshableTransactionKind<'a>, + pub promise_data: Cow<'a, Vec>>>, +} + impl<'a> From<&'a TransactionMessage> for BorshableTransactionMessage<'a> { fn from(t: &'a TransactionMessage) -> Self { - Self { + Self::V2(BorshableTransactionMessageV2 { block_hash: t.block_hash.0, near_receipt_id: t.near_receipt_id.0, position: t.position, @@ -137,7 +170,8 @@ impl<'a> From<&'a TransactionMessage> for BorshableTransactionMessage<'a> { caller: Cow::Borrowed(&t.caller), attached_near: t.attached_near, transaction: (&t.transaction).into(), - } + promise_data: Cow::Borrowed(&t.promise_data), + }) } } @@ -145,16 +179,30 @@ impl<'a> TryFrom> for TransactionMessage { type Error = aurora_engine_transactions::Error; fn try_from(t: BorshableTransactionMessage<'a>) -> Result { - Ok(Self { - block_hash: H256(t.block_hash), - near_receipt_id: H256(t.near_receipt_id), - position: t.position, - succeeded: t.succeeded, - signer: t.signer.into_owned(), - caller: t.caller.into_owned(), - attached_near: t.attached_near, - transaction: t.transaction.try_into()?, - }) + match t { + BorshableTransactionMessage::V1(t) => Ok(Self { + block_hash: H256(t.block_hash), + near_receipt_id: H256(t.near_receipt_id), + position: t.position, + succeeded: t.succeeded, + signer: t.signer.into_owned(), + caller: t.caller.into_owned(), + attached_near: t.attached_near, + transaction: t.transaction.try_into()?, + promise_data: Vec::new(), + }), + BorshableTransactionMessage::V2(t) => Ok(Self { + block_hash: H256(t.block_hash), + near_receipt_id: H256(t.near_receipt_id), + position: t.position, + succeeded: t.succeeded, + signer: t.signer.into_owned(), + caller: t.caller.into_owned(), + attached_near: t.attached_near, + transaction: t.transaction.try_into()?, + promise_data: t.promise_data.into_owned(), + }), + } } } diff --git a/engine-tests/src/test_utils/mod.rs b/engine-tests/src/test_utils/mod.rs index a56dc826a..b9d63d727 100644 --- a/engine-tests/src/test_utils/mod.rs +++ b/engine-tests/src/test_utils/mod.rs @@ -1,6 +1,6 @@ use aurora_engine::parameters::ViewCallArgs; use aurora_engine_types::account_id::AccountId; -use aurora_engine_types::types::NEP141Wei; +use aurora_engine_types::types::{NEP141Wei, PromiseResult}; use borsh::{BorshDeserialize, BorshSerialize}; use libsecp256k1::{self, Message, PublicKey, SecretKey}; use near_primitives::runtime::config_store::RuntimeConfigStore; @@ -87,6 +87,9 @@ pub(crate) struct AuroraRunner { // Use the standalone in parallel if set. This allows checking both // implementations give the same results. pub standalone_runner: Option, + // Empty by default. Can be set in tests if the transaction should be + // executed as if it was a callback. + pub promise_results: Vec, } /// Same as `AuroraRunner`, but consumes `self` on execution (thus preventing building on @@ -188,6 +191,17 @@ impl AuroraRunner { input, ); + let vm_promise_results: Vec<_> = self + .promise_results + .iter() + .map(|p| match p { + PromiseResult::Failed => near_vm_logic::types::PromiseResult::Failed, + PromiseResult::NotReady => near_vm_logic::types::PromiseResult::NotReady, + PromiseResult::Successful(bytes) => { + near_vm_logic::types::PromiseResult::Successful(bytes.clone()) + } + }) + .collect(); let (maybe_outcome, maybe_error) = match near_vm_runner::run( &self.code, method_name, @@ -195,7 +209,7 @@ impl AuroraRunner { self.context.clone(), &self.wasm_config, &self.fees_config, - &[], + &vm_promise_results, self.current_protocol_version, Some(&self.cache), ) { @@ -212,7 +226,7 @@ impl AuroraRunner { && (method_name == SUBMIT || method_name == CALL || method_name == DEPLOY_ERC20) { standalone_runner - .submit_raw(method_name, &self.context) + .submit_raw(method_name, &self.context, &self.promise_results) .unwrap(); self.validate_standalone(); } @@ -561,6 +575,7 @@ impl Default for AuroraRunner { current_protocol_version: u32::MAX, previous_logs: Default::default(), standalone_runner: None, + promise_results: Vec::new(), } } } diff --git a/engine-tests/src/test_utils/standalone/mocks/promise.rs b/engine-tests/src/test_utils/standalone/mocks/promise.rs index bf9d70b57..a201d1996 100644 --- a/engine-tests/src/test_utils/standalone/mocks/promise.rs +++ b/engine-tests/src/test_utils/standalone/mocks/promise.rs @@ -32,6 +32,8 @@ impl PromiseTracker { } impl PromiseHandler for PromiseTracker { + type ReadOnly = Self; + fn promise_results_count(&self) -> u64 { self.promise_results.len() as u64 } @@ -73,4 +75,13 @@ impl PromiseHandler for PromiseTracker { fn promise_return(&mut self, promise: PromiseId) { self.returned_promise = Some(promise); } + + fn read_only(&self) -> Self::ReadOnly { + Self { + internal_index: 0, + promise_results: self.promise_results.clone(), + scheduled_promises: Default::default(), + returned_promise: Default::default(), + } + } } diff --git a/engine-tests/src/test_utils/standalone/mod.rs b/engine-tests/src/test_utils/standalone/mod.rs index b762f60b4..85e9baeac 100644 --- a/engine-tests/src/test_utils/standalone/mod.rs +++ b/engine-tests/src/test_utils/standalone/mod.rs @@ -2,7 +2,7 @@ use aurora_engine::engine; use aurora_engine::parameters::{CallArgs, DeployErc20TokenArgs, SubmitResult, TransactionStatus}; use aurora_engine_sdk::env::{self, Env}; use aurora_engine_transactions::legacy::{LegacyEthSignedTransaction, TransactionLegacy}; -use aurora_engine_types::types::{Address, NearGas, Wei}; +use aurora_engine_types::types::{Address, NearGas, PromiseResult, Wei}; use aurora_engine_types::{H256, U256}; use borsh::BorshDeserialize; use engine_standalone_storage::{ @@ -43,7 +43,7 @@ impl StandaloneRunner { .unwrap(); env.block_height += 1; let transaction_hash = H256::zero(); - let tx_msg = Self::template_tx_msg(storage, &env, 0, transaction_hash); + let tx_msg = Self::template_tx_msg(storage, &env, 0, transaction_hash, &[]); let result = storage.with_engine_access(env.block_height, 0, &[], |io| { mocks::init_evm(io, env, chain_id); }); @@ -77,7 +77,7 @@ impl StandaloneRunner { }; env.block_height += 1; - let tx_msg = Self::template_tx_msg(storage, &env, 0, transaction_hash); + let tx_msg = Self::template_tx_msg(storage, &env, 0, transaction_hash, &[]); let result = storage.with_engine_access(env.block_height, 0, &[], |io| { mocks::mint_evm_account(address, balance, nonce, code, io, env) @@ -126,6 +126,7 @@ impl StandaloneRunner { storage, env, &mut self.cumulative_diff, + &[], ) } @@ -144,6 +145,7 @@ impl StandaloneRunner { storage, env, &mut self.cumulative_diff, + &[], ) } @@ -161,7 +163,7 @@ impl StandaloneRunner { let transaction_bytes = rlp::encode(signed_tx).to_vec(); let transaction_hash = aurora_engine_sdk::keccak(&transaction_bytes); - let mut tx_msg = Self::template_tx_msg(storage, &env, 0, transaction_hash); + let mut tx_msg = Self::template_tx_msg(storage, &env, 0, transaction_hash, &[]); tx_msg.position = transaction_position; tx_msg.transaction = TransactionKind::Submit(transaction_bytes.as_slice().try_into().unwrap()); @@ -182,6 +184,7 @@ impl StandaloneRunner { &mut self, method_name: &str, ctx: &near_vm_logic::VMContext, + promise_results: &[PromiseResult], ) -> Result { let mut env = self.env.clone(); env.block_height = ctx.block_index; @@ -201,11 +204,13 @@ impl StandaloneRunner { storage, &mut env, &mut self.cumulative_diff, + promise_results, ) } else if method_name == test_utils::CALL { let call_args = CallArgs::try_from_slice(&ctx.input).unwrap(); let transaction_hash = aurora_engine_sdk::keccak(&ctx.input); - let mut tx_msg = Self::template_tx_msg(storage, &env, 0, transaction_hash); + let mut tx_msg = + Self::template_tx_msg(storage, &env, 0, transaction_hash, promise_results); tx_msg.transaction = TransactionKind::Call(call_args); let outcome = sync::execute_transaction_message(storage, tx_msg).unwrap(); @@ -216,7 +221,8 @@ impl StandaloneRunner { } else if method_name == test_utils::DEPLOY_ERC20 { let deploy_args = DeployErc20TokenArgs::try_from_slice(&ctx.input).unwrap(); let transaction_hash = aurora_engine_sdk::keccak(&ctx.input); - let mut tx_msg = Self::template_tx_msg(storage, &env, 0, transaction_hash); + let mut tx_msg = + Self::template_tx_msg(storage, &env, 0, transaction_hash, promise_results); tx_msg.transaction = TransactionKind::DeployErc20(deploy_args); let outcome = sync::execute_transaction_message(storage, tx_msg).unwrap(); @@ -275,6 +281,7 @@ impl StandaloneRunner { env: &env::Fixed, transaction_position: u16, transaction_hash: H256, + promise_results: &[PromiseResult], ) -> TransactionMessage { let block_hash = mocks::compute_block_hash(env.block_height); let block_metadata = BlockMetadata { @@ -284,6 +291,13 @@ impl StandaloneRunner { storage .set_block_data(block_hash, env.block_height, block_metadata) .unwrap(); + let promise_data = promise_results + .iter() + .map(|p| match p { + PromiseResult::Failed | PromiseResult::NotReady => None, + PromiseResult::Successful(bytes) => Some(bytes.clone()), + }) + .collect(); TransactionMessage { block_hash, near_receipt_id: transaction_hash, @@ -293,6 +307,7 @@ impl StandaloneRunner { caller: env.predecessor_account_id(), attached_near: env.attached_deposit, transaction: TransactionKind::Unknown, + promise_data, } } @@ -302,10 +317,16 @@ impl StandaloneRunner { storage: &'db mut Storage, env: &mut env::Fixed, cumulative_diff: &mut Diff, + promise_results: &[PromiseResult], ) -> Result { let transaction_hash = aurora_engine_sdk::keccak(&transaction_bytes); - let mut tx_msg = - Self::template_tx_msg(storage, env, transaction_position, transaction_hash); + let mut tx_msg = Self::template_tx_msg( + storage, + env, + transaction_position, + transaction_hash, + promise_results, + ); tx_msg.transaction = TransactionKind::Submit(transaction_bytes.try_into().unwrap()); let outcome = sync::execute_transaction_message(storage, tx_msg).unwrap(); diff --git a/engine-tests/src/tests/mod.rs b/engine-tests/src/tests/mod.rs index 7689f5a0d..e6fd6b7b4 100644 --- a/engine-tests/src/tests/mod.rs +++ b/engine-tests/src/tests/mod.rs @@ -12,6 +12,7 @@ mod meta_parsing; mod multisender; mod one_inch; mod prepaid_gas_precompile; +mod promise_results_precompile; mod random; mod repro; pub(crate) mod sanity; diff --git a/engine-tests/src/tests/promise_results_precompile.rs b/engine-tests/src/tests/promise_results_precompile.rs new file mode 100644 index 000000000..e6fd09e1f --- /dev/null +++ b/engine-tests/src/tests/promise_results_precompile.rs @@ -0,0 +1,143 @@ +use crate::test_utils::{self, standalone}; +use aurora_engine_precompiles::promise_result::{self, costs}; +use aurora_engine_transactions::legacy::TransactionLegacy; +use aurora_engine_types::{ + types::{Address, EthGas, NearGas, PromiseResult, Wei}, + U256, +}; +use borsh::BorshSerialize; + +const NEAR_GAS_PER_EVM: u64 = 175_000_000; + +#[test] +fn test_promise_results_precompile() { + let mut signer = test_utils::Signer::random(); + let mut runner = test_utils::deploy_evm(); + + let mut standalone = standalone::StandaloneRunner::default(); + standalone.init_evm(); + + let promise_results = vec![ + PromiseResult::Successful(hex::decode("deadbeef").unwrap()), + PromiseResult::Failed, + ]; + + let transaction = TransactionLegacy { + nonce: signer.use_nonce().into(), + gas_price: U256::zero(), + gas_limit: u64::MAX.into(), + to: Some(promise_result::ADDRESS), + value: Wei::zero(), + data: Vec::new(), + }; + + runner.promise_results = promise_results.clone(); + let result = runner + .submit_transaction(&signer.secret_key, transaction.clone()) + .unwrap(); + + let standalone_result = standalone + .submit_raw("submit", &runner.context, &promise_results) + .unwrap(); + + assert_eq!(result, standalone_result); + + assert_eq!( + test_utils::unwrap_success(result), + promise_results.try_to_vec().unwrap(), + ); +} + +#[test] +fn test_promise_result_gas_cost() { + let mut runner = test_utils::deploy_evm(); + let mut standalone = standalone::StandaloneRunner::default(); + standalone.init_evm(); + runner.standalone_runner = Some(standalone); + let mut signer = test_utils::Signer::random(); + runner.context.block_index = aurora_engine::engine::ZERO_ADDRESS_FIX_HEIGHT + 1; + + // Baseline transaction that does essentially nothing. + let (_, baseline) = runner + .submit_with_signer_profiled(&mut signer, |nonce| TransactionLegacy { + nonce, + gas_price: U256::zero(), + gas_limit: u64::MAX.into(), + to: Some(Address::from_array([0; 20])), + value: Wei::zero(), + data: Vec::new(), + }) + .unwrap(); + + let mut profile_for_promises = |promise_data: Vec| -> (u64, u64, u64) { + let input_length: usize = promise_data.iter().map(|p| p.size()).sum(); + runner.promise_results = promise_data; + let (submit_result, profile) = runner + .submit_with_signer_profiled(&mut signer, |nonce| TransactionLegacy { + nonce, + gas_price: U256::zero(), + gas_limit: u64::MAX.into(), + to: Some(promise_result::ADDRESS), + value: Wei::zero(), + data: Vec::new(), + }) + .unwrap(); + assert!(submit_result.status.is_ok()); + // Subtract off baseline transaction to isolate just precompile things + ( + u64::try_from(input_length).unwrap(), + profile.all_gas() - baseline.all_gas(), + submit_result.gas_used, + ) + }; + + let promise_results = vec![ + PromiseResult::Successful(hex::decode("deadbeef").unwrap()), + PromiseResult::Failed, + PromiseResult::Successful(vec![1u8; 100]), + ]; + + let (x1, y1, evm1) = profile_for_promises(Vec::new()); + let (x2, y2, evm2) = profile_for_promises(promise_results); + + let cost_per_byte = (y2 - y1) / (x2 - x1); + let base_cost = NearGas::new(y1 - cost_per_byte * x1); + + let base_cost = EthGas::new(base_cost.as_u64() / NEAR_GAS_PER_EVM); + let cost_per_byte = cost_per_byte / NEAR_GAS_PER_EVM; + + let within_5_percent = |a: u64, b: u64| -> bool { + let x = a.max(b); + let y = a.min(b); + + 20 * (x - y) <= x + }; + assert!( + within_5_percent(base_cost.as_u64(), costs::PROMISE_RESULT_BASE_COST.as_u64()), + "Incorrect promise_result base cost. Expected: {} Actual: {}", + base_cost, + costs::PROMISE_RESULT_BASE_COST + ); + + assert!( + within_5_percent(cost_per_byte, costs::PROMISE_RESULT_BYTE_COST.as_u64()), + "Incorrect promise_result per byte cost. Expected: {} Actual: {}", + cost_per_byte, + costs::PROMISE_RESULT_BYTE_COST + ); + + let total_gas1 = y1 + baseline.all_gas(); + let total_gas2 = y2 + baseline.all_gas(); + assert!( + within_5_percent(evm1, total_gas1 / NEAR_GAS_PER_EVM), + "Incorrect EVM gas used. Expected: {} Actual: {}", + evm1, + total_gas1 / NEAR_GAS_PER_EVM + ); + assert!( + within_5_percent(evm2, total_gas2 / NEAR_GAS_PER_EVM), + "Incorrect EVM gas used. Expected: {} Actual: {}", + evm2, + total_gas2 / NEAR_GAS_PER_EVM + ); +} diff --git a/engine-tests/src/tests/repro.rs b/engine-tests/src/tests/repro.rs index 1dc64a681..d8d628b8a 100644 --- a/engine-tests/src/tests/repro.rs +++ b/engine-tests/src/tests/repro.rs @@ -152,7 +152,9 @@ fn repro_common<'a>(context: ReproContext<'a>) { .set_engine_account_id(&"aurora".parse().unwrap()) .unwrap(); json_snapshot::initialize_engine_state(&mut standalone.storage, snapshot).unwrap(); - let standalone_result = standalone.submit_raw("submit", &runner.context).unwrap(); + let standalone_result = standalone + .submit_raw("submit", &runner.context, &[]) + .unwrap(); assert_eq!( submit_result.try_to_vec().unwrap(), standalone_result.try_to_vec().unwrap() diff --git a/engine-tests/src/tests/sanity.rs b/engine-tests/src/tests/sanity.rs index 5031e3785..2ecb0a064 100644 --- a/engine-tests/src/tests/sanity.rs +++ b/engine-tests/src/tests/sanity.rs @@ -110,14 +110,18 @@ fn test_transaction_to_zero_address() { // Prior to the fix the zero address is interpreted as None, causing a contract deployment. // It also incorrectly derives the sender address, so does not increment the right nonce. context.block_index = aurora_engine::engine::ZERO_ADDRESS_FIX_HEIGHT - 1; - let result = runner.submit_raw(test_utils::SUBMIT, &context).unwrap(); + let result = runner + .submit_raw(test_utils::SUBMIT, &context, &[]) + .unwrap(); assert_eq!(result.gas_used, 53_000); runner.env.block_height = aurora_engine::engine::ZERO_ADDRESS_FIX_HEIGHT; assert_eq!(runner.get_nonce(&address), U256::zero()); // After the fix this transaction is simply a transfer of 0 ETH to the zero address context.block_index = aurora_engine::engine::ZERO_ADDRESS_FIX_HEIGHT; - let result = runner.submit_raw(test_utils::SUBMIT, &context).unwrap(); + let result = runner + .submit_raw(test_utils::SUBMIT, &context, &[]) + .unwrap(); assert_eq!(result.gas_used, 21_000); runner.env.block_height = aurora_engine::engine::ZERO_ADDRESS_FIX_HEIGHT + 1; assert_eq!(runner.get_nonce(&address), U256::one()); diff --git a/engine-tests/src/tests/standalone/storage.rs b/engine-tests/src/tests/standalone/storage.rs index 9e5106a8d..b21584da0 100644 --- a/engine-tests/src/tests/standalone/storage.rs +++ b/engine-tests/src/tests/standalone/storage.rs @@ -268,6 +268,7 @@ fn test_transaction_index() { caller: "placeholder.near".parse().unwrap(), attached_near: 0, transaction: TransactionKind::Unknown, + promise_data: Vec::new(), }; let tx_included = engine_standalone_storage::TransactionIncluded { block_hash, diff --git a/engine-tests/src/tests/standalone/sync.rs b/engine-tests/src/tests/standalone/sync.rs index ff1fd1461..6f4252832 100644 --- a/engine-tests/src/tests/standalone/sync.rs +++ b/engine-tests/src/tests/standalone/sync.rs @@ -53,6 +53,7 @@ fn test_consume_deposit_message() { caller: runner.env.predecessor_account_id(), attached_near: 0, transaction: sync::types::TransactionKind::Deposit(proof.try_to_vec().unwrap()), + promise_data: Vec::new(), }; let outcome = sync::consume_message( @@ -84,6 +85,7 @@ fn test_consume_deposit_message() { caller: runner.env.predecessor_account_id(), attached_near: 0, transaction: sync::types::TransactionKind::FinishDeposit(finish_deposit_args), + promise_data: Vec::new(), }; let outcome = sync::consume_message( @@ -116,6 +118,7 @@ fn test_consume_deposit_message() { caller: runner.env.predecessor_account_id(), attached_near: 0, transaction: sync::types::TransactionKind::FtOnTransfer(ft_on_transfer_args), + promise_data: Vec::new(), }; sync::consume_message( @@ -145,6 +148,7 @@ fn test_consume_deploy_message() { caller: runner.env.predecessor_account_id(), attached_near: 0, transaction: sync::types::TransactionKind::Deploy(input), + promise_data: Vec::new(), }; sync::consume_message( @@ -196,6 +200,7 @@ fn test_consume_deploy_erc20_message() { caller: runner.env.predecessor_account_id(), attached_near: 0, transaction: sync::types::TransactionKind::DeployErc20(args), + promise_data: Vec::new(), }; // Deploy ERC-20 (this would be the flow for bridging a new NEP-141 to Aurora) @@ -233,6 +238,7 @@ fn test_consume_deploy_erc20_message() { caller: runner.env.predecessor_account_id(), attached_near: 0, transaction: sync::types::TransactionKind::FtOnTransfer(args), + promise_data: Vec::new(), }; // Mint new tokens (via ft_on_transfer flow, same as the bridge) @@ -289,6 +295,7 @@ fn test_consume_ft_on_transfer_message() { caller: runner.env.predecessor_account_id(), attached_near: 0, transaction: sync::types::TransactionKind::FtOnTransfer(args), + promise_data: Vec::new(), }; sync::consume_message( @@ -332,6 +339,7 @@ fn test_consume_call_message() { recipient_address, transfer_amount, )), + promise_data: Vec::new(), }; sync::consume_message( @@ -381,6 +389,7 @@ fn test_consume_submit_message() { caller: runner.env.predecessor_account_id(), attached_near: 0, transaction: sync::types::TransactionKind::Submit(eth_transaction), + promise_data: Vec::new(), }; sync::consume_message( diff --git a/engine-tests/src/tests/standalone/tracing.rs b/engine-tests/src/tests/standalone/tracing.rs index 9d3454de0..0b929b3a5 100644 --- a/engine-tests/src/tests/standalone/tracing.rs +++ b/engine-tests/src/tests/standalone/tracing.rs @@ -74,6 +74,7 @@ fn test_evm_tracing_with_storage() { caller: "system".parse().unwrap(), attached_near: 0, transaction: engine_standalone_storage::sync::types::TransactionKind::Unknown, + promise_data: Vec::new(), }, diff, maybe_result: Ok(None), diff --git a/engine-types/src/lib.rs b/engine-types/src/lib.rs index 51ff05637..f2cf3590b 100644 --- a/engine-types/src/lib.rs +++ b/engine-types/src/lib.rs @@ -25,8 +25,8 @@ mod v0 { vec::Vec, }; pub use core::{ - cmp::Ordering, fmt::Display, marker::PhantomData, mem, ops::Add, ops::Div, ops::Mul, - ops::Sub, ops::SubAssign, + cmp::Ordering, fmt::Display, marker::PhantomData, mem, ops::Add, ops::AddAssign, ops::Div, + ops::Mul, ops::Sub, ops::SubAssign, }; pub use primitive_types::{H160, H256, U256}; } diff --git a/engine-types/src/types/gas.rs b/engine-types/src/types/gas.rs index 2ec7c3eb3..f5f36a5e4 100644 --- a/engine-types/src/types/gas.rs +++ b/engine-types/src/types/gas.rs @@ -1,5 +1,5 @@ use crate::fmt::Formatter; -use crate::{Add, Display, Div, Mul, Sub}; +use crate::{Add, AddAssign, Display, Div, Mul, Sub}; use borsh::{BorshDeserialize, BorshSerialize}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -67,6 +67,12 @@ impl Add for EthGas { } } +impl AddAssign for EthGas { + fn add_assign(&mut self, rhs: EthGas) { + self.0 += rhs.0 + } +} + impl Div for EthGas { type Output = EthGas; diff --git a/engine-types/src/types/mod.rs b/engine-types/src/types/mod.rs index d212209db..82e666199 100644 --- a/engine-types/src/types/mod.rs +++ b/engine-types/src/types/mod.rs @@ -70,6 +70,15 @@ pub enum PromiseResult { Failed, } +impl PromiseResult { + pub fn size(&self) -> usize { + match self { + Self::Failed | Self::NotReady => 1, + Self::Successful(bytes) => bytes.len(), + } + } +} + /// ft_resolve_transfer result of eth-connector pub struct FtResolveTransferResult { pub amount: Balance, diff --git a/engine/src/engine.rs b/engine/src/engine.rs index 5082661c5..1b9ad86c1 100644 --- a/engine/src/engine.rs +++ b/engine/src/engine.rs @@ -10,7 +10,7 @@ use crate::map::BijectionMap; use aurora_engine_sdk::caching::FullCache; use aurora_engine_sdk::env::Env; use aurora_engine_sdk::io::{StorageIntermediate, IO}; -use aurora_engine_sdk::promise::{PromiseHandler, PromiseId}; +use aurora_engine_sdk::promise::{PromiseHandler, PromiseId, ReadOnlyPromiseHandler}; use crate::accounting; use crate::parameters::{DeployErc20TokenArgs, NewCallArgs, TransactionStatus}; @@ -347,18 +347,19 @@ impl AsRef<[u8]> for EngineStateError { } } -struct StackExecutorParams<'a, I, E> { - precompiles: Precompiles<'a, I, E>, +struct StackExecutorParams<'a, I, E, H> { + precompiles: Precompiles<'a, I, E, H>, gas_limit: u64, } -impl<'env, I: IO + Copy, E: Env> StackExecutorParams<'env, I, E> { +impl<'env, I: IO + Copy, E: Env, H: ReadOnlyPromiseHandler> StackExecutorParams<'env, I, E, H> { fn new( gas_limit: u64, current_account_id: AccountId, random_seed: H256, io: I, env: &'env E, + ro_promise_handler: H, ) -> Self { Self { precompiles: Precompiles::new_london(PrecompileConstructorContext { @@ -366,6 +367,7 @@ impl<'env, I: IO + Copy, E: Env> StackExecutorParams<'env, I, E> { random_seed, io, env, + promise_handler: ro_promise_handler, }), gas_limit, } @@ -378,7 +380,7 @@ impl<'env, I: IO + Copy, E: Env> StackExecutorParams<'env, I, E> { 'static, 'a, executor::stack::MemoryStackState>, - Precompiles<'env, I, E>, + Precompiles<'env, I, E, H>, > { let metadata = executor::stack::StackSubstateMetadata::new(self.gas_limit, CONFIG); let state = executor::stack::MemoryStackState::new(metadata, engine); @@ -528,6 +530,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { self.env.random_seed(), self.io, self.env, + handler.read_only(), ); let mut executor = executor_params.make_executor(self); let address = executor.create_address(CreateScheme::Legacy { @@ -614,6 +617,7 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { self.env.random_seed(), self.io, self.env, + handler.read_only(), ); let mut executor = executor_params.make_executor(self); let (exit_reason, result) = executor.transact_call( @@ -665,6 +669,8 @@ impl<'env, I: IO + Copy, E: Env> Engine<'env, I, E> { self.env.random_seed(), self.io, self.env, + // View calls cannot interact with promises + aurora_engine_sdk::promise::Noop, ); let mut executor = executor_params.make_executor(self); let (status, result) = executor.transact_call(