diff --git a/runtime/near-vm-runner/tests/res/test_contract_rs.wasm b/runtime/near-vm-runner/tests/res/test_contract_rs.wasm index 3432e0e161a..aa82793d8bb 100755 Binary files a/runtime/near-vm-runner/tests/res/test_contract_rs.wasm and b/runtime/near-vm-runner/tests/res/test_contract_rs.wasm differ diff --git a/runtime/near-vm-runner/tests/test-contract-rs/.cargo/config b/runtime/near-vm-runner/tests/test-contract-rs/.cargo/config deleted file mode 100644 index f4e8c002fc2..00000000000 --- a/runtime/near-vm-runner/tests/test-contract-rs/.cargo/config +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target = "wasm32-unknown-unknown" diff --git a/runtime/near-vm-runner/tests/test-contract-rs/Cargo.lock b/runtime/near-vm-runner/tests/test-contract-rs/Cargo.lock index 37063f67809..6dacf3cc9bc 100644 --- a/runtime/near-vm-runner/tests/test-contract-rs/Cargo.lock +++ b/runtime/near-vm-runner/tests/test-contract-rs/Cargo.lock @@ -5,9 +5,14 @@ name = "cfg-if" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "itoa" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "libc" -version = "0.2.60" +version = "0.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -16,40 +21,47 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "test-contract-rs" -version = "0.0.1" -dependencies = [ - "wee_alloc 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", -] +name = "ryu" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "unreachable" -version = "1.0.0" +name = "serde" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_json" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.100 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" +name = "test-contract-rs" +version = "0.1.0" +dependencies = [ + "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", + "wee_alloc 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "wee_alloc" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", "memory_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "winapi" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -68,11 +80,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] "checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33" -"checksum libc 0.2.60 (registry+https://github.com/rust-lang/crates.io-index)" = "d44e80633f007889c7eff624b709ab43c92d708caad982295768a7b13ca3b5eb" +"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" +"checksum libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)" = "34fcd2c08d2f832f376f4173a231990fa5aef4e99fb569867318a227ef4c06ba" "checksum memory_units 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" -"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" -"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" -"checksum wee_alloc 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "31d4e6572d21ac55398bc91db827f48c3fdb8152ae60f4c358f6780e1035ffcc" -"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" +"checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" +"checksum serde 1.0.100 (registry+https://github.com/rust-lang/crates.io-index)" = "f4473e8506b213730ff2061073b48fa51dcc66349219e2e7c5608f0296a1d95a" +"checksum serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)" = "051c49229f282f7c6f3813f8286cc1e3323e8051823fce42c7ea80fe13521704" +"checksum wee_alloc 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/runtime/near-vm-runner/tests/test-contract-rs/Cargo.toml b/runtime/near-vm-runner/tests/test-contract-rs/Cargo.toml index 3f338d91d26..bddf1b85697 100644 --- a/runtime/near-vm-runner/tests/test-contract-rs/Cargo.toml +++ b/runtime/near-vm-runner/tests/test-contract-rs/Cargo.toml @@ -1,18 +1,23 @@ [package] name = "test-contract-rs" -version = "0.0.1" +version = "0.1.0" authors = ["Near Inc "] +edition = "2018" [lib] crate-type = ["cdylib"] [dependencies] -wee_alloc = { version = "0.4.4", default-features = false } - -[workspace] -members = [] +serde_json = "1.0" +wee_alloc = { version = "0.4.5", default-features = false } [profile.release] -panic = "abort" -lto = true +codegen-units = 1 +# Tell `rustc` to optimize for small code size. opt-level = "z" +lto = true +debug = false +panic = "abort" + +[workspace] +members = [] diff --git a/runtime/near-vm-runner/tests/test-contract-rs/build.sh b/runtime/near-vm-runner/tests/test-contract-rs/build.sh index df3338e04a0..d076eafa7b5 100755 --- a/runtime/near-vm-runner/tests/test-contract-rs/build.sh +++ b/runtime/near-vm-runner/tests/test-contract-rs/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -cargo +nightly build --target wasm32-unknown-unknown --release +RUSTFLAGS='-C link-arg=-s' cargo +nightly build --target wasm32-unknown-unknown --release cp target/wasm32-unknown-unknown/release/test_contract_rs.wasm ../res/ rm -rf target diff --git a/runtime/near-vm-runner/tests/test-contract-rs/src/lib.rs b/runtime/near-vm-runner/tests/test-contract-rs/src/lib.rs index 0c7298761d2..fa676ce5580 100644 --- a/runtime/near-vm-runner/tests/test-contract-rs/src/lib.rs +++ b/runtime/near-vm-runner/tests/test-contract-rs/src/lib.rs @@ -363,3 +363,74 @@ fn internal_recurse(n: u64) -> u64 { internal_recurse(n - 1) + 1 } } + +// Can be used for debugging +#[no_mangle] +fn log_u64(msg: u64) { + unsafe { + log_utf8(8, &msg as *const u64 as u64); + } +} + +#[no_mangle] +fn call_promise() { + unsafe { + input(0); + let data = vec![0u8; register_len(0) as usize]; + read_register(0, data.as_ptr() as u64); + let input_args: serde_json::Value = serde_json::from_slice(&data).unwrap(); + for arg in input_args.as_array().unwrap() { + let actual_id = if let Some(create) = arg.get("create") { + let account_id = create["account_id"].as_str().unwrap().as_bytes(); + let method_name = create["method_name"].as_str().unwrap().as_bytes(); + let arguments = serde_json::to_vec(&create["arguments"]).unwrap(); + let amount = create["amount"].as_i64().unwrap() as u128; + let gas = create["gas"].as_i64().unwrap() as u64; + promise_create( + account_id.len() as u64, + account_id.as_ptr() as u64, + method_name.len() as u64, + method_name.as_ptr() as u64, + arguments.len() as u64, + arguments.as_ptr() as u64, + &amount as *const u128 as *const u64 as u64, + gas, + ) + } else if let Some(then) = arg.get("then") { + let promise_index = then["promise_index"].as_i64().unwrap() as u64; + let account_id = then["account_id"].as_str().unwrap().as_bytes(); + let method_name = then["method_name"].as_str().unwrap().as_bytes(); + let arguments = serde_json::to_vec(&then["arguments"]).unwrap(); + let amount = then["amount"].as_i64().unwrap() as u128; + let gas = then["gas"].as_i64().unwrap() as u64; + promise_then( + promise_index, + account_id.len() as u64, + account_id.as_ptr() as u64, + method_name.len() as u64, + method_name.as_ptr() as u64, + arguments.len() as u64, + arguments.as_ptr() as u64, + &amount as *const u128 as *const u64 as u64, + gas, + ) + } else if let Some(and) = arg.get("and") { + let and = and.as_array().unwrap(); + let mut curr = and[0].as_i64().unwrap() as u64; + for other in &and[1..] { + curr = promise_and(curr, other.as_i64().unwrap() as u64); + } + curr + } else { + unimplemented!() + }; + let expected_id = arg["id"].as_i64().unwrap() as u64; + assert_eq!(actual_id, expected_id); + if let Some(ret) = arg.get("return") { + if ret.as_bool().unwrap() == true { + promise_return(actual_id); + } + } + } + } +} diff --git a/runtime/runtime/Cargo.toml b/runtime/runtime/Cargo.toml index ba03e49d81b..14e546fb160 100644 --- a/runtime/runtime/Cargo.toml +++ b/runtime/runtime/Cargo.toml @@ -35,5 +35,6 @@ ethereum-rlp = "0.2" ethereum-block = "0.3" ethereum-hexutil = "0.2" tempdir = "0.3" +serde_json = "1.0.40" testlib = { path = "../../test-utils/testlib" } diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index ed44c9c17a1..e4190db5242 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -317,7 +317,7 @@ impl Runtime { }) } - pub fn process_transaction( + fn process_transaction( &self, state_update: &mut TrieUpdate, apply_state: &ApplyState, @@ -648,7 +648,7 @@ impl Runtime { } } - pub fn process_receipt( + fn process_receipt( &self, state_update: &mut TrieUpdate, apply_state: &ApplyState, diff --git a/runtime/runtime/tests/runtime_group_tools.rs b/runtime/runtime/tests/runtime_group_tools.rs new file mode 100644 index 00000000000..2e8357eda2c --- /dev/null +++ b/runtime/runtime/tests/runtime_group_tools.rs @@ -0,0 +1,363 @@ +use near_crypto::{InMemorySigner, KeyType}; +use near_primitives::account::AccessKey; +use near_primitives::hash::{hash, CryptoHash}; +use near_primitives::receipt::Receipt; +use near_primitives::serialize::to_base64; +use near_primitives::transaction::{SignedTransaction, TransactionLog}; +use near_primitives::types::{Balance, MerkleHash}; +use near_primitives::views::AccountView; +use near_store::test_utils::create_trie; +use near_store::{Trie, TrieUpdate}; +use node_runtime::config::RuntimeConfig; +use node_runtime::ethereum::EthashProvider; +use node_runtime::{ApplyState, Runtime, StateRecord}; +use std::collections::HashMap; +use std::sync::{Arc, Condvar, Mutex}; +use std::thread; +use std::thread::JoinHandle; +use tempdir::TempDir; + +/// Initial balance used in tests. +pub const TESTING_INIT_BALANCE: Balance = 1_000_000_000_000_000; +/// Validator's stake used in tests. +pub const TESTING_INIT_STAKE: Balance = 50_000_000; + +pub struct StandaloneRuntime { + // We hold the reference to the temporary folder to avoid it being cleaned up by the drop. + #[allow(dead_code)] + pub ethash_dir: TempDir, + pub apply_state: ApplyState, + pub runtime: Runtime, + pub trie: Arc, + pub signer: InMemorySigner, + pub root: MerkleHash, +} + +impl StandaloneRuntime { + pub fn account_id(&self) -> String { + self.signer.account_id.clone() + } + + pub fn new(signer: InMemorySigner, state_records: &[StateRecord]) -> Self { + let trie = create_trie(); + let ethash_dir = + TempDir::new(format!("ethash_dir_{}", signer.account_id).as_str()).unwrap(); + let ethash_provider = + Arc::new(std::sync::Mutex::new(EthashProvider::new(ethash_dir.path()))); + let runtime_config = RuntimeConfig::default(); + + let runtime = Runtime::new(runtime_config, ethash_provider); + let trie_update = TrieUpdate::new(trie.clone(), MerkleHash::default()); + + let (store_update, root) = runtime.apply_genesis_state(trie_update, &[], state_records); + store_update.commit().unwrap(); + + let apply_state = ApplyState { + // Put each runtime into a separate shard. + block_index: 0, + // Epoch length is long enough to avoid corner cases. + epoch_length: 4, + gas_price: 1, + }; + + Self { ethash_dir, apply_state, runtime, trie, signer, root } + } + + pub fn process_block( + &mut self, + receipts: Vec, + transactions: Vec, + ) -> (Vec, Vec) { + let state_update = TrieUpdate::new(self.trie.clone(), self.root); + let apply_result = + self.runtime.apply(state_update, &self.apply_state, &receipts, &transactions).unwrap(); + + let (store_update, root) = apply_result.trie_changes.into(self.trie.clone()).unwrap(); + self.root = root; + store_update.commit().unwrap(); + self.apply_state.block_index += 1; + + (apply_result.new_receipts, apply_result.tx_result) + } +} + +#[derive(Default)] +pub struct RuntimeMailbox { + pub incoming_transactions: Vec, + pub incoming_receipts: Vec, +} + +impl RuntimeMailbox { + pub fn is_emtpy(&self) -> bool { + self.incoming_receipts.is_empty() && self.incoming_transactions.is_empty() + } +} + +#[derive(Default)] +pub struct RuntimeGroup { + pub mailboxes: (Mutex>, Condvar), + pub runtimes: Vec>>, + + /// Account id of the runtime on which the transaction was executed mapped to the transactions. + pub executed_transactions: Mutex>>, + /// Account id of the runtime on which the receipt was executed mapped to the list of the receipts. + pub executed_receipts: Mutex>>, + /// List of the transaction logs. + pub transaction_logs: Mutex>, +} + +impl RuntimeGroup { + pub fn new(num_runtimes: u64, contract_code: &[u8]) -> Arc { + let mut res = Self::default(); + let (state_records, signers) = Self::state_records_signers(num_runtimes, contract_code); + + for signer in signers { + res.mailboxes.0.lock().unwrap().insert(signer.account_id.clone(), Default::default()); + let runtime = Arc::new(Mutex::new(StandaloneRuntime::new(signer, &state_records))); + res.runtimes.push(runtime); + } + Arc::new(res) + } + + /// Get state records and signers for standalone runtimes. + fn state_records_signers( + num_runtimes: u64, + contract_code: &[u8], + ) -> (Vec, Vec) { + let code_hash = hash(contract_code); + let mut state_records = vec![]; + let mut signers = vec![]; + for i in 0..num_runtimes { + let account_id = format!("near.{}", i); + let signer = InMemorySigner::from_seed(&account_id, KeyType::ED25519, &account_id); + state_records.push(StateRecord::Account { + account_id: account_id.to_string(), + account: AccountView { + amount: TESTING_INIT_BALANCE, + staked: TESTING_INIT_STAKE, + code_hash: code_hash.clone().into(), + storage_usage: 0, + storage_paid_at: 0, + }, + }); + state_records.push(StateRecord::AccessKey { + account_id: account_id.to_string(), + public_key: signer.public_key.into(), + access_key: AccessKey::full_access().into(), + }); + state_records + .push(StateRecord::Contract { account_id, code: to_base64(contract_code) }); + signers.push(signer); + } + (state_records, signers) + } + + pub fn start_runtimes( + group: Arc, + transactions: Vec, + ) -> Vec> { + for transaction in transactions { + group + .mailboxes + .0 + .lock() + .unwrap() + .get_mut(&transaction.transaction.signer_id) + .unwrap() + .incoming_transactions + .push(transaction); + } + + let mut handles = vec![]; + for runtime in &group.runtimes { + handles.push(Self::start_runtime_in_thread(group.clone(), runtime.clone())); + } + handles + } + + fn start_runtime_in_thread( + group: Arc, + runtime: Arc>, + ) -> JoinHandle<()> { + thread::spawn(move || loop { + let account_id = runtime.lock().unwrap().account_id(); + + let mut mailboxes = group.mailboxes.0.lock().unwrap(); + loop { + if !mailboxes.get(&account_id).unwrap().is_emtpy() { + break; + } + if mailboxes.values().all(|m| m.is_emtpy()) { + return; + } + mailboxes = group.mailboxes.1.wait(mailboxes).unwrap(); + } + + let mailbox = mailboxes.get_mut(&account_id).unwrap(); + group + .executed_receipts + .lock() + .unwrap() + .entry(account_id.clone()) + .or_insert_with(Vec::new) + .extend(mailbox.incoming_receipts.clone()); + group + .executed_transactions + .lock() + .unwrap() + .entry(account_id.clone()) + .or_insert_with(Vec::new) + .extend(mailbox.incoming_transactions.clone()); + + let (new_receipts, transaction_results) = runtime.lock().unwrap().process_block( + mailbox.incoming_receipts.drain(..).collect(), + mailbox.incoming_transactions.drain(..).collect(), + ); + group.transaction_logs.lock().unwrap().extend(transaction_results); + for new_receipt in new_receipts { + let locked_other_mailbox = mailboxes.get_mut(&new_receipt.receiver_id).unwrap(); + locked_other_mailbox.incoming_receipts.push(new_receipt); + } + group.mailboxes.1.notify_all(); + }) + } + + /// Get receipt that was executed by the given runtime based on hash. + pub fn get_receipt(&self, executing_runtime: &str, hash: &CryptoHash) -> Receipt { + self.executed_receipts + .lock() + .unwrap() + .get(executing_runtime) + .expect("Runtime not found") + .iter() + .find_map(|r| if &r.get_hash() == hash { Some(r.clone()) } else { None }) + .expect("Runtime does not contain the receipt with the given hash.") + } + + /// Get transaction log produced by the execution of given transaction/receipt + /// identified by `producer_hash`. + pub fn get_produced_receipt_hashes(&self, producer_hash: &CryptoHash) -> TransactionLog { + self.transaction_logs + .lock() + .unwrap() + .iter() + .find_map(|tl| if &tl.hash == producer_hash { Some(tl.clone()) } else { None }) + .expect("The execution log of the given receipt is missing") + } + + pub fn get_receipt_debug(&self, hash: &CryptoHash) -> (String, Receipt) { + for (executed_runtime, tls) in self.executed_receipts.lock().unwrap().iter() { + if let Some(res) = + tls.iter().find_map(|r| if &r.get_hash() == hash { Some(r.clone()) } else { None }) + { + return (executed_runtime.clone(), res); + } + } + unimplemented!() + } +} + +/// Binds a tuple to a vector. +/// # Examples: +/// +/// ``` +/// let v = vec![1,2,3]; +/// tuplet!((a,b,c) = v); +/// assert_eq!(a, &1); +/// assert_eq!(b, &2); +/// assert_eq!(c, &3); +/// ``` +#[macro_export] +macro_rules! tuplet { + {() = $v:expr} => { + assert!($v.is_empty()); + }; + {($y:ident) = $v:expr } => { + let $y = &$v[0]; + assert_eq!($v.len(), 1); + }; + { ($y:ident $(, $x:ident)*) = $v:expr } => { + let ($y, $($x),*) = tuplet!($v ; 1 ; ($($x),*) ; (&$v[0]) ); + }; + { $v:expr ; $j:expr ; ($y:ident $(, $x:ident)*) ; ($($a:expr),*) } => { + tuplet!( $v ; $j+1 ; ($($x),*) ; ($($a),*,&$v[$j]) ) + }; + { $v:expr ; $j:expr ; () ; $accu:expr } => { { + assert_eq!($v.len(), $j); + $accu + } } +} + +#[macro_export] +macro_rules! assert_receipts { + ($group:ident, $transaction:ident => [ $($receipt:ident),* ] ) => { + let transaction_log = $group.get_produced_receipt_hashes(&$transaction.get_hash()); + tuplet!(( $($receipt),* ) = transaction_log.result.receipts); + }; + ($group:ident, $from:expr => $receipt:ident @ $to:expr, + $receipt_pat:pat, + $receipt_assert:block, + $actions_name:ident, + $($action_name:ident, $action_pat:pat, $action_assert:block ),+ + => [ $($produced_receipt:ident),*] ) => { + let r = $group.get_receipt($to, $receipt); + assert_eq!(r.predecessor_id, $from.to_string()); + assert_eq!(r.receiver_id, $to.to_string()); + match &r.receipt { + $receipt_pat => { + $receipt_assert + tuplet!(( $($action_name),* ) = $actions_name); + $( + match $action_name { + $action_pat => { + $action_assert + } + _ => panic!("Action {:#?} does not satisfy the pattern {}", $action_name, stringify!($action_pat)), + } + )* + } + _ => panic!("Receipt {:#?} does not satisfy the pattern {}", r, stringify!($receipt_pat)), + } + let receipt_log = $group.get_produced_receipt_hashes(&r.get_hash()); + tuplet!(( $($produced_receipt),* ) = receipt_log.result.receipts); + }; + ($group:ident, $from:expr => $receipt:ident @ $to:expr, + $receipt_pat:pat, + $receipt_assert:block + => [ $($produced_receipt:ident),*] ) => { + let r = $group.get_receipt($to, $receipt); + assert_eq!(r.predecessor_id, $from.to_string()); + assert_eq!(r.receiver_id, $to.to_string()); + match &r.receipt { + $receipt_pat => { + $receipt_assert + } + _ => panic!("Receipt {:#?} does not satisfy the pattern {}", r, stringify!($receipt_pat)), + } + let receipt_log = $group.get_produced_receipt_hashes(&r.get_hash()); + tuplet!(( $($produced_receipt),* ) = receipt_log.result.receipts); + }; +} + +/// A short form for refunds. +/// ``` +/// assert_refund!(group, ref1 @ "near.0"); +/// ``` +/// expands into: +/// ``` +/// assert_receipts!(group, "system" => ref1 @ "near.0", +/// ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, +/// actions, +/// a0, Action::Transfer(TransferAction{..}), {} +/// => []); +/// ``` +#[macro_export] +macro_rules! assert_refund { + ($group:ident, $receipt:ident @ $to:expr) => { + assert_receipts!($group, "system" => $receipt @ $to, + ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, + actions, + a0, Action::Transfer(TransferAction{..}), {} + => []); + } +} diff --git a/runtime/runtime/tests/test_async_calls.rs b/runtime/runtime/tests/test_async_calls.rs new file mode 100644 index 00000000000..ff62184c587 --- /dev/null +++ b/runtime/runtime/tests/test_async_calls.rs @@ -0,0 +1,375 @@ +use crate::runtime_group_tools::RuntimeGroup; +use near_primitives::hash::CryptoHash; +use near_primitives::receipt::{ActionReceipt, ReceiptEnum}; + +pub mod runtime_group_tools; + +#[test] +fn test_simple_func_call() { + let wasm_binary: &[u8] = include_bytes!("../../near-vm-runner/tests/res/test_contract_rs.wasm"); + let group = RuntimeGroup::new(2, wasm_binary); + let signer_sender = group.runtimes[0].lock().unwrap().signer.clone(); + let signer_receiver = group.runtimes[1].lock().unwrap().signer.clone(); + + let signed_transaction = SignedTransaction::from_actions( + 1, + signer_sender.account_id.clone(), + signer_receiver.account_id.clone(), + &signer_sender, + vec![Action::FunctionCall(FunctionCallAction { + method_name: "sum_n".to_string(), + args: 10u64.to_le_bytes().to_vec(), + gas: 1_000_000, + deposit: 0, + })], + CryptoHash::default(), + ); + + let handles = RuntimeGroup::start_runtimes(group.clone(), vec![signed_transaction.clone()]); + for h in handles { + h.join().unwrap(); + } + + use near_primitives::transaction::*; + assert_receipts!(group, signed_transaction => [r0]); + assert_receipts!(group, "near.0" => r0 @ "near.1", + ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, + actions, + a0, Action::FunctionCall(FunctionCallAction{..}), {} + => [ref1] ); + assert_refund!(group, ref1 @ "near.0"); +} + +// single promise, no callback (A->B) +#[test] +fn test_single_promise_no_callback() { + let wasm_binary: &[u8] = include_bytes!("../../near-vm-runner/tests/res/test_contract_rs.wasm"); + let group = RuntimeGroup::new(3, wasm_binary); + let signer_sender = group.runtimes[0].lock().unwrap().signer.clone(); + let signer_receiver = group.runtimes[1].lock().unwrap().signer.clone(); + + let data = serde_json::json!([ + {"create": { + "account_id": "near.2", + "method_name": "call_promise", + "arguments": [], + "amount": 0, + "gas": 300_000, + }, "id": 0 } + ]); + + let signed_transaction = SignedTransaction::from_actions( + 1, + signer_sender.account_id.clone(), + signer_receiver.account_id.clone(), + &signer_sender, + vec![Action::FunctionCall(FunctionCallAction { + method_name: "call_promise".to_string(), + args: serde_json::to_vec(&data).unwrap(), + gas: 1_000_000, + deposit: 0, + })], + CryptoHash::default(), + ); + + let handles = RuntimeGroup::start_runtimes(group.clone(), vec![signed_transaction.clone()]); + for h in handles { + h.join().unwrap(); + } + + use near_primitives::transaction::*; + assert_receipts!(group, signed_transaction => [r0]); + assert_receipts!(group, "near.0" => r0 @ "near.1", + ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 1_000_000); + assert_eq!(*deposit, 0); + } + => [r1, ref0] ); + assert_receipts!(group, "near.1" => r1 @ "near.2", + ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 300_000); + assert_eq!(*deposit, 0); + } + => [ref1]); + assert_refund!(group, ref0 @ "near.0"); + assert_refund!(group, ref1 @ "near.0"); +} + +// single promise with callback (A->B=>C) +#[test] +fn test_single_promise_with_callback() { + let wasm_binary: &[u8] = include_bytes!("../../near-vm-runner/tests/res/test_contract_rs.wasm"); + let group = RuntimeGroup::new(4, wasm_binary); + let signer_sender = group.runtimes[0].lock().unwrap().signer.clone(); + let signer_receiver = group.runtimes[1].lock().unwrap().signer.clone(); + + let data = serde_json::json!([ + {"create": { + "account_id": "near.2", + "method_name": "call_promise", + "arguments": [], + "amount": 0, + "gas": 300_000, + }, "id": 0 }, + {"then": { + "promise_index": 0, + "account_id": "near.3", + "method_name": "call_promise", + "arguments": [], + "amount": 0, + "gas": 300_000, + }, "id": 1} + ]); + + let signed_transaction = SignedTransaction::from_actions( + 1, + signer_sender.account_id.clone(), + signer_receiver.account_id.clone(), + &signer_sender, + vec![Action::FunctionCall(FunctionCallAction { + method_name: "call_promise".to_string(), + args: serde_json::to_vec(&data).unwrap(), + gas: 1_000_000, + deposit: 0, + })], + CryptoHash::default(), + ); + + let handles = RuntimeGroup::start_runtimes(group.clone(), vec![signed_transaction.clone()]); + for h in handles { + h.join().unwrap(); + } + + use near_primitives::transaction::*; + assert_receipts!(group, signed_transaction => [r0]); + assert_receipts!(group, "near.0" => r0 @ "near.1", + ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 1_000_000); + assert_eq!(*deposit, 0); + } + => [r1, r2, ref0] ); + let data_id; + assert_receipts!(group, "near.1" => r1 @ "near.2", + ReceiptEnum::Action(ActionReceipt{actions, output_data_receivers, ..}), { + assert_eq!(output_data_receivers.len(), 1); + data_id = output_data_receivers[0].data_id.clone(); + }, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 300_000); + assert_eq!(*deposit, 0); + } + => [ref1]); + assert_receipts!(group, "near.1" => r2 @ "near.3", + ReceiptEnum::Action(ActionReceipt{actions, input_data_ids, ..}), { + assert_eq!(input_data_ids.len(), 1); + assert_eq!(data_id, input_data_ids[0].clone()); + }, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 300_000); + assert_eq!(*deposit, 0); + } + => [ref2]); + + assert_refund!(group, ref0 @ "near.0"); + assert_refund!(group, ref1 @ "near.0"); + assert_refund!(group, ref2 @ "near.0"); +} + +// two promises, no callbacks (A->B->C) +#[test] +fn test_two_promises_no_callbacks() { + let wasm_binary: &[u8] = include_bytes!("../../near-vm-runner/tests/res/test_contract_rs.wasm"); + let group = RuntimeGroup::new(4, wasm_binary); + let signer_sender = group.runtimes[0].lock().unwrap().signer.clone(); + let signer_receiver = group.runtimes[1].lock().unwrap().signer.clone(); + + let data = serde_json::json!([ + {"create": { + "account_id": "near.2", + "method_name": "call_promise", + "arguments": [ + {"create": { + "account_id": "near.3", + "method_name": "call_promise", + "arguments": [], + "amount": 0, + "gas": 300_000, + }, "id": 0} + ], + "amount": 0, + "gas": 600_000, + }, "id": 0 }, + + ]); + + let signed_transaction = SignedTransaction::from_actions( + 1, + signer_sender.account_id.clone(), + signer_receiver.account_id.clone(), + &signer_sender, + vec![Action::FunctionCall(FunctionCallAction { + method_name: "call_promise".to_string(), + args: serde_json::to_vec(&data).unwrap(), + gas: 1_000_000, + deposit: 0, + })], + CryptoHash::default(), + ); + + let handles = RuntimeGroup::start_runtimes(group.clone(), vec![signed_transaction.clone()]); + for h in handles { + h.join().unwrap(); + } + + use near_primitives::transaction::*; + assert_receipts!(group, signed_transaction => [r0]); + assert_receipts!(group, "near.0" => r0 @ "near.1", + ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 1_000_000); + assert_eq!(*deposit, 0); + } + => [r1, ref0] ); + assert_receipts!(group, "near.1" => r1 @ "near.2", + ReceiptEnum::Action(ActionReceipt{actions, ..}), { }, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 600_000); + assert_eq!(*deposit, 0); + } + => [r2, ref1]); + assert_receipts!(group, "near.2" => r2 @ "near.3", + ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 300_000); + assert_eq!(*deposit, 0); + } + => [ref2]); + + assert_refund!(group, ref0 @ "near.0"); + assert_refund!(group, ref1 @ "near.0"); + assert_refund!(group, ref2 @ "near.0"); +} + +// two promises, with two callbacks (A->B->C=>D=>E) where call to E is initialized by completion of D. +#[test] +fn test_two_promises_with_two_callbacks() { + let wasm_binary: &[u8] = include_bytes!("../../near-vm-runner/tests/res/test_contract_rs.wasm"); + let group = RuntimeGroup::new(6, wasm_binary); + let signer_sender = group.runtimes[0].lock().unwrap().signer.clone(); + let signer_receiver = group.runtimes[1].lock().unwrap().signer.clone(); + + let data = serde_json::json!([ + {"create": { + "account_id": "near.2", + "method_name": "call_promise", + "arguments": [ + {"create": { + "account_id": "near.3", + "method_name": "call_promise", + "arguments": [], + "amount": 0, + "gas": 1_000_000, + }, "id": 0}, + + {"then": { + "promise_index": 0, + "account_id": "near.4", + "method_name": "call_promise", + "arguments": [], + "amount": 0, + "gas": 1_000_000, + }, "id": 1} + ], + "amount": 0, + "gas": 3_000_000, + }, "id": 0 }, + + {"then": { + "promise_index": 0, + "account_id": "near.5", + "method_name": "call_promise", + "arguments": [], + "amount": 0, + "gas": 3_000_000, + }, "id": 1} + ]); + + let signed_transaction = SignedTransaction::from_actions( + 1, + signer_sender.account_id.clone(), + signer_receiver.account_id.clone(), + &signer_sender, + vec![Action::FunctionCall(FunctionCallAction { + method_name: "call_promise".to_string(), + args: serde_json::to_vec(&data).unwrap(), + gas: 10_000_000, + deposit: 0, + })], + CryptoHash::default(), + ); + + let handles = RuntimeGroup::start_runtimes(group.clone(), vec![signed_transaction.clone()]); + for h in handles { + h.join().unwrap(); + } + + use near_primitives::transaction::*; + assert_receipts!(group, signed_transaction => [r0]); + assert_receipts!(group, "near.0" => r0 @ "near.1", + ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 10_000_000); + assert_eq!(*deposit, 0); + } + => [r1, cb1, ref0] ); + assert_receipts!(group, "near.1" => r1 @ "near.2", + ReceiptEnum::Action(ActionReceipt{actions, ..}), { }, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 3_000_000); + assert_eq!(*deposit, 0); + } + => [r2, cb2, ref1]); + assert_receipts!(group, "near.2" => r2 @ "near.3", + ReceiptEnum::Action(ActionReceipt{actions, ..}), {}, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 1_000_000); + assert_eq!(*deposit, 0); + } + => [ref2]); + assert_receipts!(group, "near.2" => cb2 @ "near.4", + ReceiptEnum::Action(ActionReceipt{actions, ..}), { }, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 1_000_000); + assert_eq!(*deposit, 0); + } + => [ref3]); + assert_receipts!(group, "near.1" => cb1 @ "near.5", + ReceiptEnum::Action(ActionReceipt{actions, ..}), { }, + actions, + a0, Action::FunctionCall(FunctionCallAction{gas, deposit, ..}), { + assert_eq!(*gas, 3_000_000); + assert_eq!(*deposit, 0); + } + => [ref4]); + + assert_refund!(group, ref0 @ "near.0"); + assert_refund!(group, ref1 @ "near.0"); + assert_refund!(group, ref2 @ "near.0"); + assert_refund!(group, ref3 @ "near.0"); + assert_refund!(group, ref4 @ "near.0"); +} diff --git a/test-utils/testlib/src/standard_test_cases.rs b/test-utils/testlib/src/standard_test_cases.rs index c807d9daa47..b101e92dfbb 100644 --- a/test-utils/testlib/src/standard_test_cases.rs +++ b/test-utils/testlib/src/standard_test_cases.rs @@ -224,7 +224,7 @@ pub fn test_send_money(node: impl Node) { staked: TESTING_INIT_STAKE, code_hash: default_code_hash().into(), storage_paid_at: 0, - storage_usage: 254500, + storage_usage: 69823, } ); let result2 = node_user.view_account(&bob_account()).unwrap(); @@ -235,7 +235,7 @@ pub fn test_send_money(node: impl Node) { staked: TESTING_INIT_STAKE, code_hash: default_code_hash().into(), storage_paid_at: 0, - storage_usage: 254500, + storage_usage: 69823, } ); }