use near_crypto::{InMemorySigner, KeyType};
use near_primitives::account::{AccessKey, Account};
use near_primitives::hash::{hash, CryptoHash};
use near_primitives::receipt::Receipt;
use near_primitives::state_record::StateRecord;
use near_primitives::test_utils::MockEpochInfoProvider;
use near_primitives::transaction::{ExecutionOutcomeWithId, SignedTransaction};
use near_primitives::types::{AccountId, Balance};
use near_primitives::version::PROTOCOL_VERSION;
use near_store::test_utils::create_tries;
use near_store::ShardTries;
use node_runtime::{ApplyState, Runtime};
use random_config::random_config;
use std::collections::HashMap;
use std::sync::{Arc, Condvar, Mutex};
use std::thread;
use std::thread::JoinHandle;
pub mod random_config;
pub const TESTING_INIT_BALANCE: Balance = 1_000_000_000 * NEAR_BASE;
pub const TESTING_INIT_STAKE: Balance = 50_000_000 * NEAR_BASE;
pub const NEAR_BASE: Balance = 1_000_000_000_000_000_000_000_000;
pub struct StandaloneRuntime {
pub apply_state: ApplyState,
pub runtime: Runtime,
pub tries: ShardTries,
pub signer: InMemorySigner,
pub root: CryptoHash,
pub epoch_info_provider: MockEpochInfoProvider,
}
impl StandaloneRuntime {
pub fn account_id(&self) -> String {
self.signer.account_id.clone()
}
pub fn new(signer: InMemorySigner, state_records: &[StateRecord], tries: ShardTries) -> Self {
let mut runtime_config = random_config();
runtime_config.wasm_config.limit_config.max_total_prepaid_gas = 10u64.pow(15);
runtime_config.transaction_costs.action_receipt_creation_config.execution =
runtime_config.wasm_config.limit_config.max_total_prepaid_gas / 64;
runtime_config.transaction_costs.data_receipt_creation_config.base_cost.execution =
runtime_config.wasm_config.limit_config.max_total_prepaid_gas / 64;
let runtime = Runtime::new();
let (store_update, root) =
runtime.apply_genesis_state(tries.clone(), 0, &[], state_records, &runtime_config);
store_update.commit().unwrap();
let apply_state = ApplyState {
block_index: 0,
prev_block_hash: Default::default(),
block_hash: Default::default(),
epoch_id: Default::default(),
epoch_height: 0,
gas_price: 100,
block_timestamp: 0,
gas_limit: None,
random_seed: Default::default(),
current_protocol_version: PROTOCOL_VERSION,
config: Arc::new(runtime_config),
cache: None,
#[cfg(feature = "protocol_feature_evm")]
evm_chain_id: near_chain_configs::TESTNET_EVM_CHAIN_ID,
profile: Default::default(),
};
Self {
apply_state,
runtime,
tries,
signer,
root,
epoch_info_provider: MockEpochInfoProvider::default(),
}
}
pub fn process_block(
&mut self,
receipts: &[Receipt],
transactions: &[SignedTransaction],
) -> (Vec<Receipt>, Vec<ExecutionOutcomeWithId>) {
let apply_result = self
.runtime
.apply(
self.tries.get_trie_for_shard(0),
self.root,
&None,
&self.apply_state,
receipts,
transactions,
&self.epoch_info_provider,
)
.unwrap();
let (store_update, root) = self.tries.apply_all(&apply_result.trie_changes, 0).unwrap();
self.root = root;
store_update.commit().unwrap();
self.apply_state.block_index += 1;
(apply_result.outgoing_receipts, apply_result.outcomes)
}
}
#[derive(Default)]
pub struct RuntimeMailbox {
pub incoming_transactions: Vec<SignedTransaction>,
pub incoming_receipts: Vec<Receipt>,
}
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<HashMap<String, RuntimeMailbox>>, Condvar),
pub state_records: Arc<Vec<StateRecord>>,
pub signers: Vec<InMemorySigner>,
pub executed_transactions: Mutex<HashMap<String, Vec<SignedTransaction>>>,
pub executed_receipts: Mutex<HashMap<String, Vec<Receipt>>>,
pub transaction_logs: Mutex<Vec<ExecutionOutcomeWithId>>,
}
impl RuntimeGroup {
pub fn new_with_account_ids(
account_ids: Vec<AccountId>,
num_existing_accounts: u64,
contract_code: &[u8],
) -> Arc<Self> {
let mut res = Self::default();
assert!(num_existing_accounts <= account_ids.len() as u64);
let (state_records, signers) =
Self::state_records_signers(account_ids, num_existing_accounts, contract_code);
Arc::make_mut(&mut res.state_records).extend(state_records);
for signer in signers {
res.signers.push(signer.clone());
res.mailboxes.0.lock().unwrap().insert(signer.account_id.clone(), Default::default());
}
Arc::new(res)
}
pub fn new(num_runtimes: u64, num_existing_accounts: u64, contract_code: &[u8]) -> Arc<Self> {
let account_ids = (0..num_runtimes).map(|i| format!("near_{}", i)).collect();
Self::new_with_account_ids(account_ids, num_existing_accounts, contract_code)
}
fn state_records_signers(
account_ids: Vec<AccountId>,
num_existing_accounts: u64,
contract_code: &[u8],
) -> (Vec<StateRecord>, Vec<InMemorySigner>) {
let code_hash = hash(contract_code);
let mut state_records = vec![];
let mut signers = vec![];
for (i, account_id) in account_ids.into_iter().enumerate() {
let signer = InMemorySigner::from_seed(&account_id, KeyType::ED25519, &account_id);
if (i as u64) < num_existing_accounts {
state_records.push(StateRecord::Account {
account_id: account_id.to_string(),
account: Account {
amount: TESTING_INIT_BALANCE,
locked: TESTING_INIT_STAKE,
code_hash,
storage_usage: 0,
},
});
state_records.push(StateRecord::AccessKey {
account_id: account_id.to_string(),
public_key: signer.public_key.clone(),
access_key: AccessKey::full_access().into(),
});
state_records
.push(StateRecord::Contract { account_id, code: contract_code.to_vec() });
}
signers.push(signer);
}
(state_records, signers)
}
pub fn start_runtimes(
group: Arc<Self>,
transactions: Vec<SignedTransaction>,
) -> Vec<JoinHandle<()>> {
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 signer in &group.signers {
let signer = signer.clone();
let state_records = Arc::clone(&group.state_records);
let runtime_factory =
move || StandaloneRuntime::new(signer, &state_records, create_tries());
handles.push(Self::start_runtime_in_thread(group.clone(), runtime_factory));
}
handles
}
fn start_runtime_in_thread<F>(group: Arc<Self>, runtime_factory: F) -> JoinHandle<()>
where
F: FnOnce() -> StandaloneRuntime + Send + 'static,
{
thread::spawn(move || {
let mut runtime = runtime_factory();
loop {
let account_id = runtime.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
.process_block(&mailbox.incoming_receipts, &mailbox.incoming_transactions);
mailbox.incoming_receipts.clear();
mailbox.incoming_transactions.clear();
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();
}
})
}
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.")
}
pub fn get_transaction_log(&self, producer_hash: &CryptoHash) -> ExecutionOutcomeWithId {
self.transaction_logs
.lock()
.unwrap()
.iter()
.find_map(|tl| if &tl.id == 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!()
}
}
#[macro_export]
macro_rules! tuplet {
{() = $v:expr, $message:expr } => {
assert!($v.is_empty(), "{}", $message);
};
{($y:ident) = $v:expr, $message:expr } => {
assert_eq!($v.len(), 1, "{}", $message);
let $y = &$v[0];
};
{ ($y:ident $(, $x:ident)*) = $v:expr, $message:expr } => {
let ($y, $($x),*) = tuplet!($v ; 1 ; ($($x),*) ; (&$v[0]), $message );
};
{ $v:expr ; $j:expr ; ($y:ident $(, $x:ident)*) ; ($($a:expr),*), $message:expr } => {
tuplet!( $v ; $j+1 ; ($($x),*) ; ($($a),*,&$v[$j]), $message )
};
{ $v:expr ; $j:expr ; () ; $accu:expr, $message:expr } => { {
assert_eq!($v.len(), $j, "{}", $message);
$accu
} }
}
#[macro_export]
macro_rules! assert_receipts {
($group:ident, $transaction:ident => [ $($receipt:ident),* ] ) => {
let transaction_log = $group.get_transaction_log(&$transaction.get_hash());
tuplet!(( $($receipt),* ) = transaction_log.outcome.receipt_ids, "Incorrect number of produced receipts for transaction");
};
($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, "Incorrect number of actions");
$(
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_transaction_log(&r.get_hash());
tuplet!(( $($produced_receipt),* ) = receipt_log.outcome.receipt_ids, "Incorrect number of produced receipts for a receipt");
};
}
#[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{..}), {}
=> []);
}
}