From 4527320c940d33a7ce40ce422b4d2fb77dc8e74e Mon Sep 17 00:00:00 2001 From: Michael Birch Date: Tue, 7 Nov 2023 12:19:09 -0500 Subject: [PATCH] Fix(standalone): properly compute runtime random value (#863) ## Description The Near runtime has a random value available to smart contracts via a cost function. Aurora exposes that random value to EVM smart contracts in two ways: (1) via a custom precompile, and (2) via [EVM opcode 0x44](https://www.evm.codes/#44?fork=shanghai) since it was changed from `DIFFICULTY` to `PREVRANDAO` after the merge. Therefore it is important for the standalone engine to be able to correctly provide the same random value as would be present on-chain. In this PR I fix the standalone engine to be able to correctly reproduce the Near runtime random value based on the implementation present in nearcore. There will also be a follow-up PR to `borealis-engine-lib` to make use of this change in the Borealis Engine there. The random value is computed as `sha256(block_random_value || action_hash)` where the `block_random_value` comes from the protocol-level VRF (this was the value which previously we were using directly) and the `action_hash` is derived from the (`FunctionCall`) `Action` that is being executed in the Near runtime. To have the `action_hash` available to the standalone engine I am adding a new field to `TransactionMessage` which stores this value. In the tests the `action_hash` field is populated in a reasonable way, but not exactly as it would be in nearcore because there is no actual `Action` (we skip straight to the Wasm execution). However, in the follow-up PR in `borealis-engine-lib` the field will be populated from the Near data. Once this change is made, it will fix a bug in Borealis Engine where it was not correctly reproducing the execution of contracts involving randomness. ## Performance / NEAR gas cost considerations N/A : change is to standalone engine only. ## Testing The test for the random precompile has been updated to reflect this change. A test related to tracing is also changed to no longer depend on the randomness precompile because the test did not rely on the specifics of which precompile was used and now using randomness in the tests has extra setup steps. --- .../src/relayer_db/mod.rs | 2 + engine-standalone-storage/src/sync/mod.rs | 16 +++++++- engine-standalone-storage/src/sync/types.rs | 40 +++++++++++++++++++ engine-tests/src/tests/random.rs | 18 +++++++-- engine-tests/src/tests/repro.rs | 2 +- engine-tests/src/tests/sanity.rs | 8 +++- .../src/tests/standalone/call_tracer.rs | 33 +++++++++++---- engine-tests/src/tests/standalone/storage.rs | 1 + engine-tests/src/tests/standalone/sync.rs | 9 +++++ engine-tests/src/tests/standalone/tracing.rs | 1 + engine-tests/src/utils/mod.rs | 18 +++++++-- engine-tests/src/utils/standalone/mod.rs | 26 +++++++++++- 12 files changed, 155 insertions(+), 19 deletions(-) diff --git a/engine-standalone-storage/src/relayer_db/mod.rs b/engine-standalone-storage/src/relayer_db/mod.rs index d36355438..b19434c5b 100644 --- a/engine-standalone-storage/src/relayer_db/mod.rs +++ b/engine-standalone-storage/src/relayer_db/mod.rs @@ -155,6 +155,7 @@ where transaction: crate::sync::types::TransactionKind::Submit(tx), promise_data: Vec::new(), raw_input: transaction_bytes, + action_hash: H256::default(), }; storage.set_transaction_included(tx_hash, &tx_msg, &diff)?; } @@ -268,6 +269,7 @@ mod test { transaction: TransactionKind::Unknown, promise_data: Vec::new(), raw_input: Vec::new(), + action_hash: H256::default(), }, &diff, ) diff --git a/engine-standalone-storage/src/sync/mod.rs b/engine-standalone-storage/src/sync/mod.rs index 803c75fe0..a16310537 100644 --- a/engine-standalone-storage/src/sync/mod.rs +++ b/engine-standalone-storage/src/sync/mod.rs @@ -384,6 +384,10 @@ where let predecessor_account_id = transaction_message.caller.clone(); let near_receipt_id = transaction_message.near_receipt_id; let current_account_id = engine_account_id; + let random_seed = compute_random_seed( + &transaction_message.action_hash, + &block_metadata.random_seed, + ); let env = env::Fixed { signer_account_id, current_account_id, @@ -391,7 +395,7 @@ where block_height, block_timestamp: block_metadata.timestamp, attached_deposit: transaction_message.attached_near, - random_seed: block_metadata.random_seed, + random_seed, prepaid_gas: DEFAULT_PREPAID_GAS, }; @@ -435,6 +439,16 @@ where (tx_hash, diff, result) } +/// Based on nearcore implementation: +/// +fn compute_random_seed(action_hash: &H256, block_random_value: &H256) -> H256 { + const BYTES_LEN: usize = 32 + 32; + let mut bytes: Vec = Vec::with_capacity(BYTES_LEN); + bytes.extend_from_slice(action_hash.as_bytes()); + bytes.extend_from_slice(block_random_value.as_bytes()); + aurora_engine_sdk::sha256(&bytes) +} + /// Handles all transaction kinds other than `submit`. /// The `submit` transaction kind is special because it is the only one where the transaction hash /// differs from the NEAR receipt hash. diff --git a/engine-standalone-storage/src/sync/types.rs b/engine-standalone-storage/src/sync/types.rs index 06ae54ade..4d859b317 100644 --- a/engine-standalone-storage/src/sync/types.rs +++ b/engine-standalone-storage/src/sync/types.rs @@ -53,6 +53,14 @@ pub struct TransactionMessage { pub promise_data: Vec>>, /// Raw bytes passed as input when executed in the Near Runtime. pub raw_input: Vec, + /// A Near protocol quantity equal to + /// `sha256(receipt_id || block_hash || le_bytes(u64 - action_index))`. + /// This quantity is used together with the block random seed + /// to generate the random value available to the transaction. + /// nearcore references: + /// - https://github.com/near/nearcore/blob/00ca2f3f73e2a547ba881f76ecc59450dbbef6e2/core/primitives/src/utils.rs#L261 + /// - https://github.com/near/nearcore/blob/00ca2f3f73e2a547ba881f76ecc59450dbbef6e2/core/primitives/src/utils.rs#L295 + pub action_hash: H256, } impl TransactionMessage { @@ -679,6 +687,7 @@ enum BorshableTransactionMessage<'a> { V1(BorshableTransactionMessageV1<'a>), V2(BorshableTransactionMessageV2<'a>), V3(BorshableTransactionMessageV3<'a>), + V4(BorshableTransactionMessageV4<'a>), } #[derive(BorshDeserialize, BorshSerialize)] @@ -720,6 +729,21 @@ struct BorshableTransactionMessageV3<'a> { pub raw_input: Cow<'a, Vec>, } +#[derive(BorshDeserialize, BorshSerialize)] +struct BorshableTransactionMessageV4<'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>>>, + pub raw_input: Cow<'a, Vec>, + pub action_hash: [u8; 32], +} + impl<'a> From<&'a TransactionMessage> for BorshableTransactionMessage<'a> { fn from(t: &'a TransactionMessage) -> Self { Self::V3(BorshableTransactionMessageV3 { @@ -756,6 +780,7 @@ impl<'a> TryFrom> for TransactionMessage { transaction, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }) } BorshableTransactionMessage::V2(t) => { @@ -772,6 +797,7 @@ impl<'a> TryFrom> for TransactionMessage { transaction, promise_data: t.promise_data.into_owned(), raw_input, + action_hash: H256::default(), }) } BorshableTransactionMessage::V3(t) => Ok(Self { @@ -785,6 +811,20 @@ impl<'a> TryFrom> for TransactionMessage { transaction: t.transaction.try_into()?, promise_data: t.promise_data.into_owned(), raw_input: t.raw_input.into_owned(), + action_hash: H256::default(), + }), + BorshableTransactionMessage::V4(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(), + raw_input: t.raw_input.into_owned(), + action_hash: H256(t.action_hash), }), } } diff --git a/engine-tests/src/tests/random.rs b/engine-tests/src/tests/random.rs index 473887ab1..35b4f9358 100644 --- a/engine-tests/src/tests/random.rs +++ b/engine-tests/src/tests/random.rs @@ -1,12 +1,17 @@ use crate::utils; use crate::utils::solidity::random::{Random, RandomConstructor}; use aurora_engine_types::H256; +use rand::SeedableRng; #[test] fn test_random_number_precompile() { let random_seed = H256::from_slice(vec![7; 32].as_slice()); - let mut signer = utils::Signer::random(); - let mut runner = utils::deploy_runner().with_random_seed(random_seed); + let secret_key = { + let mut rng = rand::rngs::StdRng::from_seed(random_seed.0); + libsecp256k1::SecretKey::random(&mut rng) + }; + let mut signer = utils::Signer::new(secret_key); + let mut runner = utils::deploy_runner().with_block_random_value(random_seed); let random_ctr = RandomConstructor::load(); let nonce = signer.use_nonce(); @@ -14,6 +19,13 @@ fn test_random_number_precompile() { .deploy_contract(&signer.secret_key, |ctr| ctr.deploy(nonce), random_ctr) .into(); + // Value derived from `random_seed` above together with the `action_hash` + // of the following transaction. + let expected_value = H256::from_slice( + &hex::decode("1a71249ace8312de8ed3640c852d5d542b04b2caec668325f6e18811244e7f5c").unwrap(), + ); + runner.context.random_seed = expected_value.0.to_vec(); + let counter_value = random.random_seed(&mut runner, &mut signer); - assert_eq!(counter_value, random_seed); + assert_eq!(counter_value, expected_value); } diff --git a/engine-tests/src/tests/repro.rs b/engine-tests/src/tests/repro.rs index 35b7eede4..2615e8c44 100644 --- a/engine-tests/src/tests/repro.rs +++ b/engine-tests/src/tests/repro.rs @@ -171,7 +171,7 @@ fn repro_common(context: &ReproContext) { let mut standalone = standalone::StandaloneRunner::default(); json_snapshot::initialize_engine_state(&standalone.storage, snapshot).unwrap(); let standalone_result = standalone - .submit_raw("submit", &runner.context, &[]) + .submit_raw("submit", &runner.context, &[], None) .unwrap(); assert_eq!( submit_result.try_to_vec().unwrap(), diff --git a/engine-tests/src/tests/sanity.rs b/engine-tests/src/tests/sanity.rs index f490c2a2f..ed658418c 100644 --- a/engine-tests/src/tests/sanity.rs +++ b/engine-tests/src/tests/sanity.rs @@ -212,14 +212,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_height = ZERO_ADDRESS_FIX_HEIGHT - 1; - let result = runner.submit_raw(utils::SUBMIT, &context, &[]).unwrap(); + let result = runner + .submit_raw(utils::SUBMIT, &context, &[], None) + .unwrap(); assert_eq!(result.gas_used, 53_000); runner.env.block_height = 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_height = ZERO_ADDRESS_FIX_HEIGHT; - let result = runner.submit_raw(utils::SUBMIT, &context, &[]).unwrap(); + let result = runner + .submit_raw(utils::SUBMIT, &context, &[], None) + .unwrap(); assert_eq!(result.gas_used, 21_000); runner.env.block_height = ZERO_ADDRESS_FIX_HEIGHT + 1; assert_eq!(runner.get_nonce(&address), U256::one()); diff --git a/engine-tests/src/tests/standalone/call_tracer.rs b/engine-tests/src/tests/standalone/call_tracer.rs index 78d973d22..4e437efa4 100644 --- a/engine-tests/src/tests/standalone/call_tracer.rs +++ b/engine-tests/src/tests/standalone/call_tracer.rs @@ -1,4 +1,4 @@ -use crate::prelude::H256; +use crate::prelude::{H160, H256}; use crate::utils::solidity::erc20::{ERC20Constructor, ERC20}; use crate::utils::{self, standalone, Signer}; use aurora_engine_modexp::AuroraModExp; @@ -50,13 +50,32 @@ fn test_trace_precompile_direct_call() { runner.init_evm(); + let input = hex::decode("0000ca110000").unwrap(); + let precompile_cost = { + use aurora_engine_precompiles::Precompile; + let context = evm::Context { + address: H160::default(), + caller: H160::default(), + apparent_value: U256::zero(), + }; + let result = + aurora_engine_precompiles::identity::Identity.run(&input, None, &context, false); + result.unwrap().cost.as_u64() + }; let tx = aurora_engine_transactions::legacy::TransactionLegacy { nonce: signer.use_nonce().into(), gas_price: U256::zero(), gas_limit: u64::MAX.into(), - to: Some(aurora_engine_precompiles::random::RandomSeed::ADDRESS), + to: Some(aurora_engine_precompiles::identity::Identity::ADDRESS), value: Wei::zero(), - data: Vec::new(), + data: input.clone(), + }; + let intrinsic_cost = { + let signed_tx = + utils::sign_transaction(tx.clone(), Some(runner.chain_id), &signer.secret_key); + let kind = aurora_engine_transactions::EthTransactionKind::Legacy(signed_tx); + let norm_tx = aurora_engine_transactions::NormalizedEthTransaction::try_from(kind).unwrap(); + norm_tx.intrinsic_gas(&evm::Config::shanghai()).unwrap() }; let mut listener = CallTracer::default(); @@ -71,12 +90,12 @@ fn test_trace_precompile_direct_call() { let expected_trace = call_tracer::CallFrame { call_type: call_tracer::CallType::Call, from: utils::address_from_secret_key(&signer.secret_key), - to: Some(aurora_engine_precompiles::random::RandomSeed::ADDRESS), + to: Some(aurora_engine_precompiles::identity::Identity::ADDRESS), value: U256::zero(), gas: u64::MAX, - gas_used: 21000_u64, - input: Vec::new(), - output: [0u8; 32].to_vec(), + gas_used: intrinsic_cost + precompile_cost, + input: input.clone(), + output: input, error: None, calls: Vec::new(), }; diff --git a/engine-tests/src/tests/standalone/storage.rs b/engine-tests/src/tests/standalone/storage.rs index e72c79500..1e987c06f 100644 --- a/engine-tests/src/tests/standalone/storage.rs +++ b/engine-tests/src/tests/standalone/storage.rs @@ -273,6 +273,7 @@ fn test_transaction_index() { transaction: TransactionKind::Unknown, promise_data: Vec::new(), raw_input: Vec::new(), + action_hash: H256::default(), }; 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 43f8a7476..1a14a1633 100644 --- a/engine-tests/src/tests/standalone/sync.rs +++ b/engine-tests/src/tests/standalone/sync.rs @@ -64,6 +64,7 @@ fn test_consume_deposit_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( @@ -102,6 +103,7 @@ fn test_consume_deposit_message() { // (which is `true` because the proof is valid in this case). promise_data: vec![Some(true.try_to_vec().unwrap())], raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( @@ -136,6 +138,7 @@ fn test_consume_deposit_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( @@ -170,6 +173,7 @@ fn test_consume_deploy_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( @@ -226,6 +230,7 @@ fn test_consume_deploy_erc20_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; // Deploy ERC-20 (this would be the flow for bridging a new NEP-141 to Aurora) @@ -268,6 +273,7 @@ fn test_consume_deploy_erc20_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; // Mint new tokens (via ft_on_transfer flow, same as the bridge) @@ -334,6 +340,7 @@ fn test_consume_ft_on_transfer_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( @@ -382,6 +389,7 @@ fn test_consume_call_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( @@ -436,6 +444,7 @@ fn test_consume_submit_message() { transaction: tx_kind, promise_data: Vec::new(), raw_input, + action_hash: H256::default(), }; let outcome = sync::consume_message::( diff --git a/engine-tests/src/tests/standalone/tracing.rs b/engine-tests/src/tests/standalone/tracing.rs index 616a574bf..bdcbcf72e 100644 --- a/engine-tests/src/tests/standalone/tracing.rs +++ b/engine-tests/src/tests/standalone/tracing.rs @@ -73,6 +73,7 @@ fn test_evm_tracing_with_storage() { transaction: engine_standalone_storage::sync::types::TransactionKind::Unknown, promise_data: Vec::new(), raw_input: Vec::new(), + action_hash: H256::default(), }, diff, maybe_result: Ok(None), diff --git a/engine-tests/src/utils/mod.rs b/engine-tests/src/utils/mod.rs index 2018c467a..d031d305c 100644 --- a/engine-tests/src/utils/mod.rs +++ b/engine-tests/src/utils/mod.rs @@ -98,6 +98,12 @@ pub struct AuroraRunner { // Empty by default. Can be set in tests if the transaction should be // executed as if it was a callback. pub promise_results: Vec, + // None by default. Can be set if the transaction requires randomness + // from the Near runtime. + // Note: this only sets the random value for the block, the random + // value available in the runtime is derived from this value and + // another hash that depends on the transaction itself. + pub block_random_value: Option, } /// Same as `AuroraRunner`, but consumes `self` on execution (thus preventing building on @@ -234,7 +240,12 @@ impl AuroraRunner { self.previous_logs = outcome.logs.clone(); if let Some(standalone_runner) = &mut self.standalone_runner { - standalone_runner.submit_raw(method_name, &self.context, &self.promise_results)?; + standalone_runner.submit_raw( + method_name, + &self.context, + &self.promise_results, + self.block_random_value, + )?; self.validate_standalone(); } @@ -539,8 +550,8 @@ impl AuroraRunner { outcome.return_data.as_value().unwrap() } - pub fn with_random_seed(mut self, random_seed: H256) -> Self { - self.context.random_seed = random_seed.as_bytes().to_vec(); + pub const fn with_block_random_value(mut self, random_seed: H256) -> Self { + self.block_random_value = Some(random_seed); self } @@ -645,6 +656,7 @@ impl Default for AuroraRunner { previous_logs: Vec::new(), standalone_runner: Some(standalone::StandaloneRunner::default()), promise_results: Vec::new(), + block_random_value: None, } } } diff --git a/engine-tests/src/utils/standalone/mod.rs b/engine-tests/src/utils/standalone/mod.rs index b437d9661..d22592836 100644 --- a/engine-tests/src/utils/standalone/mod.rs +++ b/engine-tests/src/utils/standalone/mod.rs @@ -198,6 +198,7 @@ impl StandaloneRunner { method_name: &str, ctx: &near_vm_logic::VMContext, promise_results: &[PromiseResult], + block_random_value: Option, ) -> Result { let mut env = self.env.clone(); env.block_height = ctx.block_height; @@ -207,8 +208,8 @@ impl StandaloneRunner { env.current_account_id = ctx.current_account_id.as_ref().parse().unwrap(); env.signer_account_id = ctx.signer_account_id.as_ref().parse().unwrap(); env.prepaid_gas = NearGas::new(ctx.prepaid_gas); - if ctx.random_seed.len() == 32 { - env.random_seed = H256::from_slice(&ctx.random_seed); + if let Some(value) = block_random_value { + env.random_seed = value; } let promise_data: Vec<_> = promise_results @@ -239,6 +240,19 @@ impl StandaloneRunner { ); tx_msg.transaction = transaction_kind; + if ctx.random_seed.len() == 32 { + let runtime_random_value = { + use near_primitives_core::hash::CryptoHash; + let action_hash = CryptoHash(tx_msg.action_hash.0); + let random_seed = CryptoHash(env.random_seed.0); + near_primitives::utils::create_random_seed(u32::MAX, action_hash, random_seed) + }; + assert_eq!( + ctx.random_seed, runtime_random_value, + "Runtime random value should match computed value when it is specified" + ); + } + let outcome = sync::execute_transaction_message::(storage, tx_msg).unwrap(); self.cumulative_diff.append(outcome.diff.clone()); storage::commit(storage, &outcome); @@ -314,6 +328,13 @@ impl StandaloneRunner { PromiseResult::Successful(bytes) => Some(bytes.clone()), }) .collect(); + let action_hash = { + let mut bytes = Vec::with_capacity(32 + 32 + 8); + bytes.extend_from_slice(transaction_hash.as_bytes()); + bytes.extend_from_slice(block_hash.as_bytes()); + bytes.extend_from_slice(&(u64::MAX - u64::from(transaction_position)).to_le_bytes()); + aurora_engine_sdk::sha256(&bytes) + }; TransactionMessage { block_hash, near_receipt_id: transaction_hash, @@ -325,6 +346,7 @@ impl StandaloneRunner { transaction: TransactionKind::Unknown, promise_data, raw_input, + action_hash, } }