From a21fc3f3034f0efa51b17816e683f487c26db29f Mon Sep 17 00:00:00 2001 From: Tao Zhu <82401714+taozhu-chicago@users.noreply.github.com> Date: Thu, 21 Apr 2022 02:38:07 -0500 Subject: [PATCH] Apply transaction actual execution units to cost_tracker (#24311) * Pass the sum of consumed compute units to cost_tracker * cost model tracks builtins and bpf programs separately, enabling adjust block cost by actual bpf programs execution costs * Copied nightly-only experimental `checked_add_(un)signed` implementation to sdk * Add function to update cost tracker with execution cost adjustment * Review suggestion - using enum instead of struct for CommitTransactionDetails Co-authored-by: Justin Starry * review - rename variable to distinguish accumulated_consumed_units from individual compute_units_consumed * not to use signed integer operations * Review - using saturating_add_assign!(), and checked_*().unwrap_or() * Review - using Ordering enum to cmp * replace checked_ with saturating_ * review - remove unnecessary Option<> * Review - add function to report number of non-zero units account to metrics --- core/src/banking_stage.rs | 27 ++- core/src/qos_service.rs | 92 ++++++--- rpc/src/transaction_status_service.rs | 2 + runtime/src/accounts.rs | 1 + runtime/src/bank.rs | 7 + runtime/src/cost_tracker.rs | 260 +++++++++++++++++++++++--- runtime/src/message_processor.rs | 11 ++ 7 files changed, 341 insertions(+), 59 deletions(-) diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index e6157b263..89f41f8da 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -100,6 +100,12 @@ struct RecordTransactionsSummary { retryable_indexes: Vec, } +#[derive(Debug)] +pub enum CommitTransactionDetails { + Committed { compute_units: u64 }, + NotCommitted, +} + pub struct ExecuteAndCommitTransactionsOutput { // Total number of transactions that were passed as candidates for execution transactions_attempted_execution_count: usize, @@ -113,9 +119,8 @@ pub struct ExecuteAndCommitTransactionsOutput { // to the block ending. retryable_transaction_indexes: Vec, // A result that indicates whether transactions were successfully - // committed into the Poh stream. If so, the result tells us - // how many such transactions were committed - commit_transactions_result: Result, PohRecorderError>, + // committed into the Poh stream. + commit_transactions_result: Result, PohRecorderError>, execute_and_commit_timings: LeaderExecuteAndCommitTimings, } @@ -1205,9 +1210,18 @@ impl BankingStage { .. } = load_and_execute_transactions_output; - let mut transactions_execute_and_record_status: Vec = execution_results + let mut transactions_execute_and_record_status: Vec<_> = execution_results .iter() - .map(|execution_result| execution_result.was_executed()) + .map(|execution_result| match execution_result { + TransactionExecutionResult::Executed(details) => { + CommitTransactionDetails::Committed { + compute_units: details.executed_units, + } + } + TransactionExecutionResult::NotExecuted { .. } => { + CommitTransactionDetails::NotCommitted + } + }) .collect(); let (freeze_lock, freeze_lock_time) = @@ -1237,7 +1251,7 @@ impl BankingStage { // mark transactions that were executed but not recorded retryable_record_transaction_indexes.iter().for_each(|i| { - transactions_execute_and_record_status[*i] = false; + transactions_execute_and_record_status[*i] = CommitTransactionDetails::NotCommitted; }); inc_new_counter_info!( @@ -2168,6 +2182,7 @@ mod tests { inner_instructions: None, durable_nonce_fee: None, return_data: None, + executed_units: 0u64, }) } diff --git a/core/src/qos_service.rs b/core/src/qos_service.rs index 409f03f45..fb493f674 100644 --- a/core/src/qos_service.rs +++ b/core/src/qos_service.rs @@ -3,7 +3,7 @@ //! how transactions are included in blocks, and optimize those blocks. //! use { - crate::banking_stage::BatchedTransactionDetails, + crate::banking_stage::{BatchedTransactionDetails, CommitTransactionDetails}, crossbeam_channel::{unbounded, Receiver, Sender}, solana_measure::measure::Measure, solana_runtime::{ @@ -177,7 +177,7 @@ impl QosService { pub fn update_or_remove_transaction_costs<'a>( transaction_costs: impl Iterator, transaction_qos_results: impl Iterator>, - transaction_committed_status: Option<&Vec>, + transaction_committed_status: Option<&Vec>, bank: &Arc, ) { match transaction_committed_status { @@ -196,20 +196,27 @@ impl QosService { fn update_transaction_costs<'a>( transaction_costs: impl Iterator, transaction_qos_results: impl Iterator>, - transaction_committed_status: &Vec, + transaction_committed_status: &Vec, bank: &Arc, ) { let mut cost_tracker = bank.write_cost_tracker().unwrap(); transaction_costs .zip(transaction_qos_results) .zip(transaction_committed_status) - .for_each(|((tx_cost, qos_inclusion_result), was_committed)| { - // Only transactions that the qos service included have to be - // checked for update - if qos_inclusion_result.is_ok() && !*was_committed { - cost_tracker.remove(tx_cost); - } - }); + .for_each( + |((tx_cost, qos_inclusion_result), transaction_committed_details)| { + // Only transactions that the qos service included have to be + // checked for update + if qos_inclusion_result.is_ok() { + match transaction_committed_details { + CommitTransactionDetails::Committed { compute_units } => { + cost_tracker.update_execution_cost(tx_cost, *compute_units) + } + CommitTransactionDetails::NotCommitted => cost_tracker.remove(tx_cost), + } + } + }, + ); } fn remove_transaction_costs<'a>( @@ -661,19 +668,27 @@ mod tests { let txs: Vec = (0..transaction_count) .map(|_| transfer_tx.clone()) .collect(); + let execute_units_adjustment = 10u64; - // assert all tx_costs should be applied to cost_tracker if all execution_results are all commited + // assert all tx_costs should be applied to cost_tracker if all execution_results are all committed { let qos_service = QosService::new(Arc::new(RwLock::new(CostModel::default())), 1); let txs_costs = qos_service.compute_transaction_costs(txs.iter()); - let total_txs_costs: u64 = txs_costs.iter().map(|cost| cost.sum()).sum(); + let total_txs_cost: u64 = txs_costs.iter().map(|cost| cost.sum()).sum(); let (qos_results, _num_included) = qos_service.select_transactions_per_cost(txs.iter(), txs_costs.iter(), &bank); assert_eq!( - total_txs_costs, + total_txs_cost, bank.read_cost_tracker().unwrap().block_cost() ); - let commited_status: Vec = (0..transaction_count).map(|_| true).collect(); + // all transactions are committed with actual units more than estimated + let commited_status: Vec = txs_costs + .iter() + .map(|tx_cost| CommitTransactionDetails::Committed { + compute_units: tx_cost.bpf_execution_cost + execute_units_adjustment, + }) + .collect(); + let final_txs_cost = total_txs_cost + execute_units_adjustment * transaction_count; QosService::update_or_remove_transaction_costs( txs_costs.iter(), qos_results.iter(), @@ -681,7 +696,7 @@ mod tests { &bank, ); assert_eq!( - total_txs_costs, + final_txs_cost, bank.read_cost_tracker().unwrap().block_cost() ); assert_eq!( @@ -708,12 +723,17 @@ mod tests { .map(|_| transfer_tx.clone()) .collect(); - // assert all tx_costs should be removed from cost_tracker if all execution_results are all NotExecuted + // assert all tx_costs should be removed from cost_tracker if all execution_results are all Not Committed { let qos_service = QosService::new(Arc::new(RwLock::new(CostModel::default())), 1); let txs_costs = qos_service.compute_transaction_costs(txs.iter()); + let total_txs_cost: u64 = txs_costs.iter().map(|cost| cost.sum()).sum(); let (qos_results, _num_included) = qos_service.select_transactions_per_cost(txs.iter(), txs_costs.iter(), &bank); + assert_eq!( + total_txs_cost, + bank.read_cost_tracker().unwrap().block_cost() + ); QosService::update_or_remove_transaction_costs( txs_costs.iter(), qos_results.iter(), @@ -741,31 +761,55 @@ mod tests { let txs: Vec = (0..transaction_count) .map(|_| transfer_tx.clone()) .collect(); + let execute_units_adjustment = 10u64; // assert only commited tx_costs are applied cost_tracker { let qos_service = QosService::new(Arc::new(RwLock::new(CostModel::default())), 1); let txs_costs = qos_service.compute_transaction_costs(txs.iter()); + let total_txs_cost: u64 = txs_costs.iter().map(|cost| cost.sum()).sum(); let (qos_results, _num_included) = qos_service.select_transactions_per_cost(txs.iter(), txs_costs.iter(), &bank); - let commited_status: Vec = (0..transaction_count).map(|n| n != 0).collect(); + assert_eq!( + total_txs_cost, + bank.read_cost_tracker().unwrap().block_cost() + ); + // Half of transactions are not committed, the rest with cost adjustment + let commited_status: Vec = txs_costs + .iter() + .enumerate() + .map(|(n, tx_cost)| { + if n % 2 == 0 { + CommitTransactionDetails::NotCommitted + } else { + CommitTransactionDetails::Committed { + compute_units: tx_cost.bpf_execution_cost + execute_units_adjustment, + } + } + }) + .collect(); QosService::update_or_remove_transaction_costs( txs_costs.iter(), qos_results.iter(), Some(&commited_status), &bank, ); - let expected_committed_units: u64 = txs_costs - .iter() - .enumerate() - .map(|(n, cost)| if n < 1 { 0 } else { cost.sum() }) - .sum(); + + // assert the final block cost + let mut expected_final_txs_count = 0u64; + let mut expected_final_block_cost = 0u64; + txs_costs.iter().enumerate().for_each(|(n, cost)| { + if n % 2 != 0 { + expected_final_txs_count += 1; + expected_final_block_cost += cost.sum() + execute_units_adjustment; + } + }); assert_eq!( - expected_committed_units, + expected_final_block_cost, bank.read_cost_tracker().unwrap().block_cost() ); assert_eq!( - transaction_count - 1, + expected_final_txs_count, bank.read_cost_tracker().unwrap().transaction_count() ); } diff --git a/rpc/src/transaction_status_service.rs b/rpc/src/transaction_status_service.rs index 0e8b4d44e..8599e63eb 100644 --- a/rpc/src/transaction_status_service.rs +++ b/rpc/src/transaction_status_service.rs @@ -102,6 +102,7 @@ impl TransactionStatusService { inner_instructions, durable_nonce_fee, return_data, + .. } = details; let lamports_per_signature = match durable_nonce_fee { Some(DurableNonceFee::Valid(lamports_per_signature)) => { @@ -352,6 +353,7 @@ pub(crate) mod tests { .unwrap(), )), return_data: None, + executed_units: 0u64, }); let balances = TransactionBalancesSet { diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index 3ba25e2d0..12c9d4060 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -1398,6 +1398,7 @@ mod tests { inner_instructions: None, durable_nonce_fee: nonce.map(DurableNonceFee::from), return_data: None, + executed_units: 0u64, }) } diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 439142e05..fc7b9c9dd 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -597,6 +597,7 @@ pub struct TransactionExecutionDetails { pub inner_instructions: Option, pub durable_nonce_fee: Option, pub return_data: Option, + pub executed_units: u64, } /// Type safe representation of a transaction execution attempt which @@ -3964,6 +3965,8 @@ impl Bank { let (blockhash, lamports_per_signature) = self.last_blockhash_and_lamports_per_signature(); + let mut executed_units = 0u64; + let mut process_message_time = Measure::start("process_message_time"); let process_result = MessageProcessor::process_message( &self.builtin_programs.vec, @@ -3980,8 +3983,10 @@ impl Bank { blockhash, lamports_per_signature, self.load_accounts_data_len(), + &mut executed_units, ); process_message_time.stop(); + saturating_add_assign!( timings.execute_accessories.process_message_us, process_message_time.as_us() @@ -4062,6 +4067,7 @@ impl Bank { inner_instructions, durable_nonce_fee, return_data, + executed_units, }) } @@ -6991,6 +6997,7 @@ pub(crate) mod tests { inner_instructions: None, durable_nonce_fee: nonce.map(DurableNonceFee::from), return_data: None, + executed_units: 0u64, }) } diff --git a/runtime/src/cost_tracker.rs b/runtime/src/cost_tracker.rs index f883bb0c7..3af6f31ae 100644 --- a/runtime/src/cost_tracker.rs +++ b/runtime/src/cost_tracker.rs @@ -5,8 +5,8 @@ //! use { crate::{block_cost_limits::*, cost_model::TransactionCost}, - solana_sdk::{clock::Slot, pubkey::Pubkey}, - std::collections::HashMap, + solana_sdk::{clock::Slot, pubkey::Pubkey, saturating_add_assign}, + std::{cmp::Ordering, collections::HashMap}, }; const WRITABLE_ACCOUNTS_PER_BLOCK: usize = 512; @@ -95,6 +95,29 @@ impl CostTracker { Ok(self.block_cost) } + pub fn update_execution_cost( + &mut self, + estimated_tx_cost: &TransactionCost, + actual_execution_units: u64, + ) { + let estimated_execution_units = estimated_tx_cost.bpf_execution_cost; + match actual_execution_units.cmp(&estimated_execution_units) { + Ordering::Equal => (), + Ordering::Greater => { + self.add_transaction_execution_cost( + estimated_tx_cost, + actual_execution_units - estimated_execution_units, + ); + } + Ordering::Less => { + self.sub_transaction_execution_cost( + estimated_tx_cost, + estimated_execution_units - actual_execution_units, + ); + } + } + } + pub fn remove(&mut self, tx_cost: &TransactionCost) { self.remove_transaction_cost(tx_cost); } @@ -121,11 +144,7 @@ impl CostTracker { ("block_cost", self.block_cost as i64, i64), ("vote_cost", self.vote_cost as i64, i64), ("transaction_count", self.transaction_count as i64, i64), - ( - "number_of_accounts", - self.cost_by_writable_accounts.len() as i64, - i64 - ), + ("number_of_accounts", self.number_of_accounts() as i64, i64), ("costliest_account", costliest_account.to_string(), String), ("costliest_account_cost", costliest_account_cost as i64, i64), ("account_data_size", self.account_data_size, i64), @@ -199,40 +218,56 @@ impl CostTracker { fn add_transaction_cost(&mut self, tx_cost: &TransactionCost) { let cost = tx_cost.sum(); - for account_key in tx_cost.writable_accounts.iter() { - let account_cost = self - .cost_by_writable_accounts - .entry(*account_key) - .or_insert(0); - *account_cost = account_cost.saturating_add(cost); - } - self.block_cost = self.block_cost.saturating_add(cost); - if tx_cost.is_simple_vote { - self.vote_cost = self.vote_cost.saturating_add(cost); - } - self.account_data_size = self - .account_data_size - .saturating_add(tx_cost.account_data_size); - self.transaction_count = self.transaction_count.saturating_add(1); + self.add_transaction_execution_cost(tx_cost, cost); + saturating_add_assign!(self.account_data_size, tx_cost.account_data_size); + saturating_add_assign!(self.transaction_count, 1); } fn remove_transaction_cost(&mut self, tx_cost: &TransactionCost) { let cost = tx_cost.sum(); + self.sub_transaction_execution_cost(tx_cost, cost); + self.account_data_size = self + .account_data_size + .saturating_sub(tx_cost.account_data_size); + self.transaction_count = self.transaction_count.saturating_sub(1); + } + + /// Apply additional actual execution units to cost_tracker + fn add_transaction_execution_cost(&mut self, tx_cost: &TransactionCost, adjustment: u64) { for account_key in tx_cost.writable_accounts.iter() { let account_cost = self .cost_by_writable_accounts .entry(*account_key) .or_insert(0); - *account_cost = account_cost.saturating_sub(cost); + *account_cost = account_cost.saturating_add(adjustment); } - self.block_cost = self.block_cost.saturating_sub(cost); + self.block_cost = self.block_cost.saturating_add(adjustment); if tx_cost.is_simple_vote { - self.vote_cost = self.vote_cost.saturating_sub(cost); + self.vote_cost = self.vote_cost.saturating_add(adjustment); } - self.account_data_size = self - .account_data_size - .saturating_sub(tx_cost.account_data_size); - self.transaction_count = self.transaction_count.saturating_sub(1); + } + + /// Substract extra execution units from cost_tracker + fn sub_transaction_execution_cost(&mut self, tx_cost: &TransactionCost, adjustment: u64) { + for account_key in tx_cost.writable_accounts.iter() { + let account_cost = self + .cost_by_writable_accounts + .entry(*account_key) + .or_insert(0); + *account_cost = account_cost.saturating_sub(adjustment); + } + self.block_cost = self.block_cost.saturating_sub(adjustment); + if tx_cost.is_simple_vote { + self.vote_cost = self.vote_cost.saturating_sub(adjustment); + } + } + + /// count number of none-zero CU accounts + fn number_of_accounts(&self) -> usize { + self.cost_by_writable_accounts + .iter() + .map(|(_key, units)| if *units > 0 { 1 } else { 0 }) + .sum() } } @@ -676,4 +711,171 @@ mod tests { assert_eq!(acct2, costliest_account); } } + + #[test] + fn test_adjust_transaction_execution_cost() { + let acct1 = Pubkey::new_unique(); + let acct2 = Pubkey::new_unique(); + let acct3 = Pubkey::new_unique(); + let cost = 100; + let account_max = cost * 2; + let block_max = account_max * 3; // for three accts + + let mut testee = CostTracker::new(account_max, block_max, block_max, None); + let tx_cost = TransactionCost { + writable_accounts: vec![acct1, acct2, acct3], + bpf_execution_cost: cost, + ..TransactionCost::default() + }; + let mut expected_block_cost = tx_cost.sum(); + let expected_tx_count = 1; + assert!(testee.try_add(&tx_cost).is_ok()); + assert_eq!(expected_block_cost, testee.block_cost()); + assert_eq!(expected_tx_count, testee.transaction_count()); + testee + .cost_by_writable_accounts + .iter() + .for_each(|(_key, units)| { + assert_eq!(expected_block_cost, *units); + }); + + // adjust up + { + let adjustment = 50u64; + testee.add_transaction_execution_cost(&tx_cost, adjustment); + expected_block_cost += 50; + assert_eq!(expected_block_cost, testee.block_cost()); + assert_eq!(expected_tx_count, testee.transaction_count()); + testee + .cost_by_writable_accounts + .iter() + .for_each(|(_key, units)| { + assert_eq!(expected_block_cost, *units); + }); + } + + // adjust down + { + let adjustment = 50u64; + testee.sub_transaction_execution_cost(&tx_cost, adjustment); + expected_block_cost -= 50; + assert_eq!(expected_block_cost, testee.block_cost()); + assert_eq!(expected_tx_count, testee.transaction_count()); + testee + .cost_by_writable_accounts + .iter() + .for_each(|(_key, units)| { + assert_eq!(expected_block_cost, *units); + }); + } + + // adjust overflow + { + testee.add_transaction_execution_cost(&tx_cost, u64::MAX); + // expect block cost set to limit + assert_eq!(u64::MAX, testee.block_cost()); + assert_eq!(expected_tx_count, testee.transaction_count()); + testee + .cost_by_writable_accounts + .iter() + .for_each(|(_key, units)| { + assert_eq!(u64::MAX, *units); + }); + } + + // adjust underflow + { + testee.sub_transaction_execution_cost(&tx_cost, u64::MAX); + // expect block cost set to limit + assert_eq!(u64::MIN, testee.block_cost()); + assert_eq!(expected_tx_count, testee.transaction_count()); + testee + .cost_by_writable_accounts + .iter() + .for_each(|(_key, units)| { + assert_eq!(u64::MIN, *units); + }); + // assert the number of non-empty accounts is zero, but map + // still contains 3 account + assert_eq!(0, testee.number_of_accounts()); + assert_eq!(3, testee.cost_by_writable_accounts.len()); + } + } + + #[test] + fn test_update_execution_cost() { + let acct1 = Pubkey::new_unique(); + let acct2 = Pubkey::new_unique(); + let acct3 = Pubkey::new_unique(); + let cost = 100; + + let tx_cost = TransactionCost { + writable_accounts: vec![acct1, acct2, acct3], + bpf_execution_cost: cost, + ..TransactionCost::default() + }; + + let mut cost_tracker = CostTracker::default(); + + // Assert OK to add tx_cost + assert!(cost_tracker.try_add(&tx_cost).is_ok()); + let (_costliest_account, costliest_account_cost) = cost_tracker.find_costliest_account(); + assert_eq!(cost, cost_tracker.block_cost); + assert_eq!(cost, costliest_account_cost); + assert_eq!(1, cost_tracker.transaction_count); + + // assert no-change if actual units is same as estimated units + let mut expected_cost = cost; + cost_tracker.update_execution_cost(&tx_cost, cost); + let (_costliest_account, costliest_account_cost) = cost_tracker.find_costliest_account(); + assert_eq!(expected_cost, cost_tracker.block_cost); + assert_eq!(expected_cost, costliest_account_cost); + assert_eq!(1, cost_tracker.transaction_count); + + // assert cost are adjusted down + let reduced_units = 3; + expected_cost -= reduced_units; + cost_tracker.update_execution_cost(&tx_cost, cost - reduced_units); + let (_costliest_account, costliest_account_cost) = cost_tracker.find_costliest_account(); + assert_eq!(expected_cost, cost_tracker.block_cost); + assert_eq!(expected_cost, costliest_account_cost); + assert_eq!(1, cost_tracker.transaction_count); + + // assert cost are adjusted up + let increased_units = 1; + expected_cost += increased_units; + cost_tracker.update_execution_cost(&tx_cost, cost + increased_units); + let (_costliest_account, costliest_account_cost) = cost_tracker.find_costliest_account(); + assert_eq!(expected_cost, cost_tracker.block_cost); + assert_eq!(expected_cost, costliest_account_cost); + assert_eq!(1, cost_tracker.transaction_count); + } + + #[test] + fn test_remove_transaction_cost() { + let mut cost_tracker = CostTracker::default(); + + let cost = 100u64; + let tx_cost = TransactionCost { + writable_accounts: vec![Pubkey::new_unique()], + bpf_execution_cost: cost, + ..TransactionCost::default() + }; + + cost_tracker.add_transaction_cost(&tx_cost); + // assert cost_tracker is reverted to default + assert_eq!(1, cost_tracker.transaction_count); + assert_eq!(1, cost_tracker.number_of_accounts()); + assert_eq!(cost, cost_tracker.block_cost); + assert_eq!(0, cost_tracker.vote_cost); + assert_eq!(0, cost_tracker.account_data_size); + + cost_tracker.remove_transaction_cost(&tx_cost); + // assert cost_tracker is reverted to default + assert_eq!(0, cost_tracker.transaction_count); + assert_eq!(0, cost_tracker.number_of_accounts()); + assert_eq!(0, cost_tracker.block_cost); + assert_eq!(0, cost_tracker.vote_cost); + assert_eq!(0, cost_tracker.account_data_size); + } } diff --git a/runtime/src/message_processor.rs b/runtime/src/message_processor.rs index 7b3fb29f7..ba0516f1d 100644 --- a/runtime/src/message_processor.rs +++ b/runtime/src/message_processor.rs @@ -64,6 +64,7 @@ impl MessageProcessor { blockhash: Hash, lamports_per_signature: u64, current_accounts_data_len: u64, + accumulated_consumed_units: &mut u64, ) -> Result { let mut invoke_context = InvokeContext::new( transaction_context, @@ -134,6 +135,8 @@ impl MessageProcessor { timings, ); time.stop(); + *accumulated_consumed_units = + accumulated_consumed_units.saturating_add(compute_units_consumed); timings.details.accumulate_program( program_id, time.as_us(), @@ -282,6 +285,7 @@ mod tests { Hash::default(), 0, 0, + &mut 0, ); assert!(result.is_ok()); assert_eq!( @@ -330,6 +334,7 @@ mod tests { Hash::default(), 0, 0, + &mut 0, ); assert_eq!( result, @@ -368,6 +373,7 @@ mod tests { Hash::default(), 0, 0, + &mut 0, ); assert_eq!( result, @@ -503,6 +509,7 @@ mod tests { Hash::default(), 0, 0, + &mut 0, ); assert_eq!( result, @@ -536,6 +543,7 @@ mod tests { Hash::default(), 0, 0, + &mut 0, ); assert!(result.is_ok()); @@ -566,6 +574,7 @@ mod tests { Hash::default(), 0, 0, + &mut 0, ); assert!(result.is_ok()); assert_eq!( @@ -644,7 +653,9 @@ mod tests { Hash::default(), 0, 0, + &mut 0, ); + assert_eq!( result, Err(TransactionError::InstructionError(