diff --git a/chain/chain/src/test_utils.rs b/chain/chain/src/test_utils.rs index b2ff7ea9e06..1a96d9bd423 100644 --- a/chain/chain/src/test_utils.rs +++ b/chain/chain/src/test_utils.rs @@ -32,7 +32,7 @@ use crate::types::{ ValidatorSignatureVerificationResult, Weight, }; use crate::{Chain, ChainGenesis, ValidTransaction}; -use near_primitives::errors::InvalidTxErrorOrStorageError; +use near_primitives::errors::RuntimeError; use near_primitives::merkle::{merklize, verify_path, MerklePath}; pub const DEFAULT_STATE_NUM_PARTS: u64 = 17; /* TODO MOO */ @@ -441,7 +441,7 @@ impl RuntimeAdapter for KeyValueRuntime { _gas_price: Balance, _state_root: StateRoot, transaction: SignedTransaction, - ) -> Result { + ) -> Result { Ok(ValidTransaction { transaction }) } diff --git a/chain/chain/src/types.rs b/chain/chain/src/types.rs index 1bf5d7d134d..d16065cb1eb 100644 --- a/chain/chain/src/types.rs +++ b/chain/chain/src/types.rs @@ -4,7 +4,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use near_crypto::{Signature, Signer}; pub use near_primitives::block::{Block, BlockHeader, Weight}; -use near_primitives::errors::InvalidTxErrorOrStorageError; +use near_primitives::errors::RuntimeError; use near_primitives::hash::{hash, CryptoHash}; use near_primitives::merkle::{merklize, MerklePath}; use near_primitives::receipt::Receipt; @@ -141,7 +141,7 @@ pub trait RuntimeAdapter: Send + Sync { gas_price: Balance, state_root: StateRoot, transaction: SignedTransaction, - ) -> Result; + ) -> Result; /// Filter transactions by verifying each one by one in the given order. Every successful /// verification stores the updated account balances to be used by next transactions. diff --git a/chain/client/src/client.rs b/chain/client/src/client.rs index 15f441d170f..17717abeca9 100644 --- a/chain/client/src/client.rs +++ b/chain/client/src/client.rs @@ -23,7 +23,7 @@ use near_crypto::Signature; use near_network::types::{ChunkPartMsg, PeerId, ReasonForBan}; use near_network::{NetworkClientResponses, NetworkRequests}; use near_primitives::block::{Block, BlockHeader}; -use near_primitives::errors::InvalidTxErrorOrStorageError; +use near_primitives::errors::{InvalidTxError, RuntimeError}; use near_primitives::hash::CryptoHash; use near_primitives::merkle::{merklize, MerklePath}; use near_primitives::receipt::Receipt; @@ -855,11 +855,14 @@ impl Client { self.forward_tx(valid_transaction.transaction) } } - Err(InvalidTxErrorOrStorageError::InvalidTxError(err)) => { + Err(RuntimeError::InvalidTxError(err)) => { debug!(target: "client", "Invalid tx: {:?}", err); NetworkClientResponses::InvalidTx(err) } - Err(InvalidTxErrorOrStorageError::StorageError(err)) => panic!(err), + Err(RuntimeError::StorageError(err)) => panic!("{}", err), + Err(RuntimeError::BalanceMismatch(err)) => { + unreachable!("Unexpected BalanceMismatch error in validate_tx: {}", err) + } } } else { // We are not tracking this shard, so there is no way to validate this tx. Just rerouting. diff --git a/core/primitives/src/errors.rs b/core/primitives/src/errors.rs index 91e50ac247e..5824f2d38b4 100644 --- a/core/primitives/src/errors.rs +++ b/core/primitives/src/errors.rs @@ -166,24 +166,91 @@ impl Display for InvalidAccessKeyError { } } +/// Happens when the input balance doesn't match the output balance in Runtime apply. +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq)] +pub struct BalanceMismatchError { + // Input balances + pub initial_accounts_balance: Balance, + pub incoming_receipts_balance: Balance, + pub initial_postponed_receipts_balance: Balance, + // Output balances + pub final_accounts_balance: Balance, + pub outgoing_receipts_balance: Balance, + pub final_postponed_receipts_balance: Balance, + pub total_rent_paid: Balance, + pub total_validator_reward: Balance, + pub total_balance_burnt: Balance, +} + +impl Display for BalanceMismatchError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + // Using saturating add to avoid overflow in display + let initial_balance = self + .initial_accounts_balance + .saturating_add(self.incoming_receipts_balance) + .saturating_add(self.initial_postponed_receipts_balance); + let final_balance = self + .final_accounts_balance + .saturating_add(self.outgoing_receipts_balance) + .saturating_add(self.final_postponed_receipts_balance) + .saturating_add(self.total_rent_paid) + .saturating_add(self.total_validator_reward) + .saturating_add(self.total_balance_burnt); + write!( + f, + "Balance Mismatch Error. The input balance {} doesn't match output balance {}\n\ + Inputs:\n\ + \tInitial accounts balance sum: {}\n\ + \tIncoming receipts balance sum: {}\n\ + \tInitial postponed receipts balance sum: {}\n\ + Outputs:\n\ + \tFinal accounts balance sum: {}\n\ + \tOutgoing receipts balance sum: {}\n\ + \tFinal postponed receipts balance sum: {}\n\ + \tTotal rent paid: {}\n\ + \tTotal validators reward: {}\n\ + \tTotal balance burnt: {}", + initial_balance, + final_balance, + self.initial_accounts_balance, + self.incoming_receipts_balance, + self.initial_postponed_receipts_balance, + self.final_accounts_balance, + self.outgoing_receipts_balance, + self.final_postponed_receipts_balance, + self.total_rent_paid, + self.total_validator_reward, + self.total_balance_burnt + ) + } +} + +/// Error returned from `Runtime::apply` #[derive(Debug, Clone, PartialEq, Eq)] -pub enum InvalidTxErrorOrStorageError { +pub enum RuntimeError { InvalidTxError(InvalidTxError), StorageError(StorageError), + BalanceMismatch(BalanceMismatchError), } -impl From for InvalidTxErrorOrStorageError { +impl From for RuntimeError { fn from(e: StorageError) -> Self { - InvalidTxErrorOrStorageError::StorageError(e) + RuntimeError::StorageError(e) + } +} + +impl From for RuntimeError { + fn from(e: BalanceMismatchError) -> Self { + RuntimeError::BalanceMismatch(e) } } -impl From for InvalidTxErrorOrStorageError +impl From for RuntimeError where T: Into, { fn from(e: T) -> Self { - InvalidTxErrorOrStorageError::InvalidTxError(e.into()) + RuntimeError::InvalidTxError(e.into()) } } diff --git a/near/src/runtime.rs b/near/src/runtime.rs index 94c4e8a3451..b7cc0700beb 100644 --- a/near/src/runtime.rs +++ b/near/src/runtime.rs @@ -16,7 +16,7 @@ use near_chain::{BlockHeader, Error, ErrorKind, RuntimeAdapter, ValidTransaction use near_crypto::{PublicKey, Signature}; use near_epoch_manager::{BlockInfo, EpochConfig, EpochManager, RewardCalculator}; use near_primitives::account::{AccessKey, Account}; -use near_primitives::errors::InvalidTxErrorOrStorageError; +use near_primitives::errors::RuntimeError; use near_primitives::hash::CryptoHash; use near_primitives::receipt::Receipt; use near_primitives::serialize::from_base64; @@ -497,7 +497,7 @@ impl RuntimeAdapter for NightshadeRuntime { gas_price: Balance, state_root: StateRoot, transaction: SignedTransaction, - ) -> Result { + ) -> Result { let mut state_update = TrieUpdate::new(self.trie.clone(), state_root.hash); let apply_state = ApplyState { block_index, @@ -641,9 +641,10 @@ impl RuntimeAdapter for NightshadeRuntime { .runtime .apply(state_update, &apply_state, &receipts, &transactions) .map_err(|e| match e { - InvalidTxErrorOrStorageError::InvalidTxError(_) => ErrorKind::InvalidTransactions, - InvalidTxErrorOrStorageError::StorageError(_) => { - panic!("Storage error. Corrupted db or invalid state."); + RuntimeError::InvalidTxError(_) => ErrorKind::InvalidTransactions, + RuntimeError::BalanceMismatch(e) => panic!("{}", e), + RuntimeError::StorageError(e) => { + panic!("Storage error. Corrupted db or invalid state. {}", e); } })?; diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 2baf79ecef2..d0348182446 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -44,8 +44,8 @@ use crate::config::{ }; pub use crate::store::StateRecord; use near_primitives::errors::{ - ActionError, ExecutionError, InvalidAccessKeyError, InvalidTxError, - InvalidTxErrorOrStorageError, + ActionError, BalanceMismatchError, ExecutionError, InvalidAccessKeyError, InvalidTxError, + RuntimeError, }; mod actions; @@ -168,7 +168,7 @@ impl Runtime { state_update: &mut TrieUpdate, apply_state: &ApplyState, signed_transaction: &SignedTransaction, - ) -> Result { + ) -> Result { let transaction = &signed_transaction.transaction; let signer_id = &transaction.signer_id; if !is_valid_account_id(&signer_id) { @@ -321,7 +321,7 @@ impl Runtime { new_local_receipts: &mut Vec, new_receipts: &mut Vec, stats: &mut ApplyStats, - ) -> Result { + ) -> Result { near_metrics::inc_counter(&metrics::TRANSACTION_PROCESSED_TOTAL); let outcome = match self.verify_and_charge_transaction(state_update, apply_state, signed_transaction) @@ -850,7 +850,7 @@ impl Runtime { apply_state: &ApplyState, prev_receipts: &[Receipt], transactions: &[SignedTransaction], - ) -> Result { + ) -> Result { // TODO(#1481): Remove clone after Runtime bug is fixed. let initial_state = state_update.clone(); @@ -914,7 +914,7 @@ impl Runtime { transactions: &[SignedTransaction], new_receipts: &[Receipt], stats: &ApplyStats, - ) -> Result<(), InvalidTxErrorOrStorageError> { + ) -> Result<(), RuntimeError> { // Accounts let all_accounts_ids: HashSet = transactions .iter() @@ -931,8 +931,8 @@ impl Runtime { .into_iter() .sum::()) }; - let initial_account_balance = total_accounts_balance(&initial_state)?; - let final_account_balance = total_accounts_balance(&final_state)?; + let initial_accounts_balance = total_accounts_balance(&initial_state)?; + let final_accounts_balance = total_accounts_balance(&final_state)?; // Receipts let receipt_cost = |receipt: &Receipt| -> Result { Ok(match &receipt.receipt { @@ -997,55 +997,45 @@ impl Runtime { .filter_map(|x| x) .collect(); - let total_postponed_receipts_cost = |state| -> Result { + let total_postponed_receipts_cost = |state| -> Result { Ok(all_potential_postponed_receipt_ids .iter() .map(|(account_id, receipt_id)| { Ok(get_receipt(state, account_id, &receipt_id)? .map_or(Ok(0), |r| receipt_cost(&r))?) }) - .collect::, InvalidTxErrorOrStorageError>>()? + .collect::, RuntimeError>>()? .into_iter() .sum::()) }; let initial_postponed_receipts_balance = total_postponed_receipts_cost(initial_state)?; let final_postponed_receipts_balance = total_postponed_receipts_cost(final_state)?; // Sum it up - let initial_balance = initial_account_balance + let initial_balance = initial_accounts_balance + incoming_receipts_balance + initial_postponed_receipts_balance; - let final_balance = final_account_balance + let final_balance = final_accounts_balance + outgoing_receipts_balance + final_postponed_receipts_balance + stats.total_rent_paid + stats.total_validator_reward + stats.total_balance_burnt; if initial_balance != final_balance { - panic!( - "Runtime Apply initial balance sum {} doesn't match final balance sum {}\n\ - \tInitial account balance sum: {}\n\ - \tFinal account balance sum: {}\n\ - \tIncoming receipts balance sum: {}\n\ - \tOutgoing receipts balance sum: {}\n\ - \tInitial postponed receipts balance sum: {}\n\ - \tFinal postponed receipts balance sum: {}\n\ - \t{:?}\n\ - \tIncoming Receipts {:#?}\n\ - \tOutgoing Receipts {:#?}", - initial_balance, - final_balance, - initial_account_balance, - final_account_balance, + Err(BalanceMismatchError { + initial_accounts_balance, + final_accounts_balance, incoming_receipts_balance, outgoing_receipts_balance, initial_postponed_receipts_balance, final_postponed_receipts_balance, - stats, - prev_receipts, - new_receipts, - ); + total_balance_burnt: stats.total_balance_burnt, + total_rent_paid: stats.total_rent_paid, + total_validator_reward: stats.total_validator_reward, + } + .into()) + } else { + Ok(()) } - Ok(()) } pub fn compute_storage_usage(&self, records: &[StateRecord]) -> HashMap { @@ -1206,6 +1196,7 @@ mod tests { use testlib::runtime_utils::{alice_account, bob_account}; use super::*; + use assert_matches::assert_matches; use near_crypto::{InMemorySigner, KeyType}; use near_primitives::transaction::TransferAction; @@ -1252,14 +1243,13 @@ mod tests { } #[test] - #[should_panic] fn test_check_balance_unaccounted_refund() { let trie = create_trie(); let root = MerkleHash::default(); let initial_state = TrieUpdate::new(trie.clone(), root); let final_state = TrieUpdate::new(trie.clone(), root); let runtime = Runtime::new(RuntimeConfig::default()); - runtime + let err = runtime .check_balance( &initial_state, &final_state, @@ -1268,7 +1258,8 @@ mod tests { &[], &ApplyStats::default(), ) - .unwrap_or_else(|_| ()); + .unwrap_err(); + assert_matches!(err, RuntimeError::BalanceMismatch(_)); } #[test] diff --git a/test-utils/testlib/src/user/runtime_user.rs b/test-utils/testlib/src/user/runtime_user.rs index 31557481aa1..cee8bbba84e 100644 --- a/test-utils/testlib/src/user/runtime_user.rs +++ b/test-utils/testlib/src/user/runtime_user.rs @@ -18,7 +18,7 @@ use node_runtime::{ApplyState, Runtime}; use crate::user::{User, POISONED_LOCK_ERR}; use near::config::INITIAL_GAS_PRICE; -use near_primitives::errors::InvalidTxErrorOrStorageError; +use near_primitives::errors::RuntimeError; /// Mock client without chain, used in RuntimeUser and RuntimeNode pub struct MockClient { @@ -74,10 +74,9 @@ impl RuntimeUser { .runtime .apply(state_update, &apply_state, &receipts, &txs) .map_err(|e| match e { - InvalidTxErrorOrStorageError::InvalidTxError(e) => format!("{}", e), - InvalidTxErrorOrStorageError::StorageError(e) => { - panic!("Storage error {:?}", e) - } + RuntimeError::InvalidTxError(e) => format!("{}", e), + RuntimeError::BalanceMismatch(e) => panic!("{}", e), + RuntimeError::StorageError(e) => panic!("Storage error {:?}", e), })?; for outcome_with_id in apply_result.tx_result.into_iter() { self.transaction_results