From 422a095647626e59f3160f19c24b128b2968f437 Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Tue, 28 Dec 2021 12:25:46 -0700 Subject: [PATCH] Add (preflight) simulation to BanksClient (#22084) * Add more-legitimate conversion from legacy Transaction to SanitizedTransaction * Add Banks method with preflight checks * Expose BanksClient method with preflight checks * Unwrap simulation err * Add Bank simulation method that works on unfrozen Banks * Add simpler api * Better name: BanksTransactionResultWithSimulation --- banks-client/src/error.rs | 19 +++++++-- banks-client/src/lib.rs | 66 +++++++++++++++++++++++++++++++- banks-interface/src/lib.rs | 17 ++++++++ banks-server/src/banks_server.rs | 52 +++++++++++++++++++++++-- runtime/src/bank.rs | 9 +++++ sdk/src/transaction/sanitized.rs | 16 +++++--- 6 files changed, 165 insertions(+), 14 deletions(-) diff --git a/banks-client/src/error.rs b/banks-client/src/error.rs index de1bea79c1..6f27f3ce5f 100644 --- a/banks-client/src/error.rs +++ b/banks-client/src/error.rs @@ -19,14 +19,21 @@ pub enum BanksClientError { #[error("transport transaction error: {0}")] TransactionError(#[from] TransactionError), + + #[error("simulation error: {err:?}, logs: {logs:?}, units_consumed: {units_consumed:?}")] + SimulationError { + err: TransactionError, + logs: Vec, + units_consumed: u64, + }, } impl BanksClientError { pub fn unwrap(&self) -> TransactionError { - if let BanksClientError::TransactionError(err) = self { - err.clone() - } else { - panic!("unexpected transport error") + match self { + BanksClientError::TransactionError(err) + | BanksClientError::SimulationError { err, .. } => err.clone(), + _ => panic!("unexpected transport error"), } } } @@ -40,6 +47,9 @@ impl From for io::Error { BanksClientError::TransactionError(err) => { Self::new(io::ErrorKind::Other, err.to_string()) } + BanksClientError::SimulationError { err, .. } => { + Self::new(io::ErrorKind::Other, err.to_string()) + } } } } @@ -57,6 +67,7 @@ impl From for TransportError { Self::IoError(io::Error::new(io::ErrorKind::Other, err.to_string())) } BanksClientError::TransactionError(err) => Self::TransactionError(err), + BanksClientError::SimulationError { err, .. } => Self::TransactionError(err), } } } diff --git a/banks-client/src/lib.rs b/banks-client/src/lib.rs index 520ae0e09a..70a0377420 100644 --- a/banks-client/src/lib.rs +++ b/banks-client/src/lib.rs @@ -10,7 +10,7 @@ use { crate::error::BanksClientError, borsh::BorshDeserialize, futures::{future::join_all, Future, FutureExt, TryFutureExt}, - solana_banks_interface::{BanksRequest, BanksResponse}, + solana_banks_interface::{BanksRequest, BanksResponse, BanksTransactionResultWithSimulation}, solana_program::{ clock::Slot, fee_calculator::FeeCalculator, hash::Hash, program_pack::Pack, pubkey::Pubkey, rent::Rent, sysvar::Sysvar, @@ -120,6 +120,22 @@ impl BanksClient { .map_err(Into::into) } + pub fn process_transaction_with_preflight_and_commitment_and_context( + &mut self, + ctx: Context, + transaction: Transaction, + commitment: CommitmentLevel, + ) -> impl Future> + '_ + { + self.inner + .process_transaction_with_preflight_and_commitment_and_context( + ctx, + transaction, + commitment, + ) + .map_err(Into::into) + } + pub fn get_account_with_commitment_and_context( &mut self, ctx: Context, @@ -201,6 +217,54 @@ impl BanksClient { }) } + /// Send a transaction and return any preflight (sanitization or simulation) errors, or return + /// after the transaction has been rejected or reached the given level of commitment. + pub fn process_transaction_with_preflight_and_commitment( + &mut self, + transaction: Transaction, + commitment: CommitmentLevel, + ) -> impl Future> + '_ { + let mut ctx = context::current(); + ctx.deadline += Duration::from_secs(50); + self.process_transaction_with_preflight_and_commitment_and_context( + ctx, + transaction, + commitment, + ) + .map(|result| match result? { + BanksTransactionResultWithSimulation { + result: None, + simulation_details: _, + } => Err(BanksClientError::ClientError( + "invalid blockhash or fee-payer", + )), + BanksTransactionResultWithSimulation { + result: Some(Err(err)), + simulation_details: Some(simulation_details), + } => Err(BanksClientError::SimulationError { + err, + logs: simulation_details.logs, + units_consumed: simulation_details.units_consumed, + }), + BanksTransactionResultWithSimulation { + result: Some(result), + simulation_details: _, + } => result.map_err(Into::into), + }) + } + + /// Send a transaction and return any preflight (sanitization or simulation) errors, or return + /// after the transaction has been finalized or rejected. + pub fn process_transaction_with_preflight( + &mut self, + transaction: Transaction, + ) -> impl Future> + '_ { + self.process_transaction_with_preflight_and_commitment( + transaction, + CommitmentLevel::default(), + ) + } + /// Send a transaction and return until the transaction has been finalized or rejected. pub fn process_transaction( &mut self, diff --git a/banks-interface/src/lib.rs b/banks-interface/src/lib.rs index ad2ff1ab48..597cf60167 100644 --- a/banks-interface/src/lib.rs +++ b/banks-interface/src/lib.rs @@ -30,6 +30,19 @@ pub struct TransactionStatus { pub confirmation_status: Option, } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TransactionSimulationDetails { + pub logs: Vec, + pub units_consumed: u64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BanksTransactionResultWithSimulation { + pub result: Option>, + pub simulation_details: Option, +} + #[tarpc::service] pub trait Banks { async fn send_transaction_with_context(transaction: Transaction); @@ -44,6 +57,10 @@ pub trait Banks { -> Option; async fn get_slot_with_context(commitment: CommitmentLevel) -> Slot; async fn get_block_height_with_context(commitment: CommitmentLevel) -> u64; + async fn process_transaction_with_preflight_and_commitment_and_context( + transaction: Transaction, + commitment: CommitmentLevel, + ) -> BanksTransactionResultWithSimulation; async fn process_transaction_with_commitment_and_context( transaction: Transaction, commitment: CommitmentLevel, diff --git a/banks-server/src/banks_server.rs b/banks-server/src/banks_server.rs index 5cd4372a25..6e94d24401 100644 --- a/banks-server/src/banks_server.rs +++ b/banks-server/src/banks_server.rs @@ -2,9 +2,14 @@ use { bincode::{deserialize, serialize}, futures::{future, prelude::stream::StreamExt}, solana_banks_interface::{ - Banks, BanksRequest, BanksResponse, TransactionConfirmationStatus, TransactionStatus, + Banks, BanksRequest, BanksResponse, BanksTransactionResultWithSimulation, + TransactionConfirmationStatus, TransactionSimulationDetails, TransactionStatus, + }, + solana_runtime::{ + bank::{Bank, TransactionSimulationResult}, + bank_forks::BankForks, + commitment::BlockCommitmentCache, }, - solana_runtime::{bank::Bank, bank_forks::BankForks, commitment::BlockCommitmentCache}, solana_sdk::{ account::Account, clock::Slot, @@ -15,7 +20,7 @@ use { message::{Message, SanitizedMessage}, pubkey::Pubkey, signature::Signature, - transaction::{self, Transaction}, + transaction::{self, SanitizedTransaction, Transaction}, }, solana_send_transaction_service::{ send_transaction_service::{SendTransactionService, TransactionInfo}, @@ -242,6 +247,47 @@ impl Banks for BanksServer { self.bank(commitment).block_height() } + async fn process_transaction_with_preflight_and_commitment_and_context( + self, + ctx: Context, + transaction: Transaction, + commitment: CommitmentLevel, + ) -> BanksTransactionResultWithSimulation { + let sanitized_transaction = + match SanitizedTransaction::try_from_legacy_transaction(transaction.clone()) { + Err(err) => { + return BanksTransactionResultWithSimulation { + result: Some(Err(err)), + simulation_details: None, + }; + } + Ok(tx) => tx, + }; + if let TransactionSimulationResult { + result: Err(err), + logs, + post_simulation_accounts: _, + units_consumed, + } = self + .bank(commitment) + .simulate_transaction_unchecked(sanitized_transaction) + { + return BanksTransactionResultWithSimulation { + result: Some(Err(err)), + simulation_details: Some(TransactionSimulationDetails { + logs, + units_consumed, + }), + }; + } + BanksTransactionResultWithSimulation { + result: self + .process_transaction_with_commitment_and_context(ctx, transaction, commitment) + .await, + simulation_details: None, + } + } + async fn process_transaction_with_commitment_and_context( self, _: Context, diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 6f2db139d6..ec394399f4 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -3134,6 +3134,15 @@ impl Bank { ) -> TransactionSimulationResult { assert!(self.is_frozen(), "simulation bank must be frozen"); + self.simulate_transaction_unchecked(transaction) + } + + /// Run transactions against a bank without committing the results; does not check if the bank + /// is frozen, enabling use in single-Bank test frameworks + pub fn simulate_transaction_unchecked( + &self, + transaction: SanitizedTransaction, + ) -> TransactionSimulationResult { let number_of_accounts = transaction.message().account_keys_len(); let batch = self.prepare_simulation_batch(transaction); let mut timings = ExecuteTimings::default(); diff --git a/sdk/src/transaction/sanitized.rs b/sdk/src/transaction/sanitized.rs index f904aac256..4bf68597c9 100644 --- a/sdk/src/transaction/sanitized.rs +++ b/sdk/src/transaction/sanitized.rs @@ -76,20 +76,24 @@ impl SanitizedTransaction { }) } - /// Create a sanitized transaction from a legacy transaction. Used for tests only. - pub fn from_transaction_for_tests(tx: Transaction) -> Self { - tx.sanitize().unwrap(); + pub fn try_from_legacy_transaction(tx: Transaction) -> Result { + tx.sanitize()?; if tx.message.has_duplicates() { - Result::::Err(TransactionError::AccountLoadedTwice).unwrap(); + return Err(TransactionError::AccountLoadedTwice); } - Self { + Ok(Self { message_hash: tx.message.hash(), message: SanitizedMessage::Legacy(tx.message), is_simple_vote_tx: false, signatures: tx.signatures, - } + }) + } + + /// Create a sanitized transaction from a legacy transaction. Used for tests only. + pub fn from_transaction_for_tests(tx: Transaction) -> Self { + Self::try_from_legacy_transaction(tx).unwrap() } /// Return the first signature for this transaction.