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
This commit is contained in:
Tyera Eulberg 2021-12-28 12:25:46 -07:00 committed by GitHub
parent e61a736d44
commit 422a095647
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 165 additions and 14 deletions

View File

@ -19,14 +19,21 @@ pub enum BanksClientError {
#[error("transport transaction error: {0}")] #[error("transport transaction error: {0}")]
TransactionError(#[from] TransactionError), TransactionError(#[from] TransactionError),
#[error("simulation error: {err:?}, logs: {logs:?}, units_consumed: {units_consumed:?}")]
SimulationError {
err: TransactionError,
logs: Vec<String>,
units_consumed: u64,
},
} }
impl BanksClientError { impl BanksClientError {
pub fn unwrap(&self) -> TransactionError { pub fn unwrap(&self) -> TransactionError {
if let BanksClientError::TransactionError(err) = self { match self {
err.clone() BanksClientError::TransactionError(err)
} else { | BanksClientError::SimulationError { err, .. } => err.clone(),
panic!("unexpected transport error") _ => panic!("unexpected transport error"),
} }
} }
} }
@ -40,6 +47,9 @@ impl From<BanksClientError> for io::Error {
BanksClientError::TransactionError(err) => { BanksClientError::TransactionError(err) => {
Self::new(io::ErrorKind::Other, err.to_string()) 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<BanksClientError> for TransportError {
Self::IoError(io::Error::new(io::ErrorKind::Other, err.to_string())) Self::IoError(io::Error::new(io::ErrorKind::Other, err.to_string()))
} }
BanksClientError::TransactionError(err) => Self::TransactionError(err), BanksClientError::TransactionError(err) => Self::TransactionError(err),
BanksClientError::SimulationError { err, .. } => Self::TransactionError(err),
} }
} }
} }

View File

@ -10,7 +10,7 @@ use {
crate::error::BanksClientError, crate::error::BanksClientError,
borsh::BorshDeserialize, borsh::BorshDeserialize,
futures::{future::join_all, Future, FutureExt, TryFutureExt}, futures::{future::join_all, Future, FutureExt, TryFutureExt},
solana_banks_interface::{BanksRequest, BanksResponse}, solana_banks_interface::{BanksRequest, BanksResponse, BanksTransactionResultWithSimulation},
solana_program::{ solana_program::{
clock::Slot, fee_calculator::FeeCalculator, hash::Hash, program_pack::Pack, pubkey::Pubkey, clock::Slot, fee_calculator::FeeCalculator, hash::Hash, program_pack::Pack, pubkey::Pubkey,
rent::Rent, sysvar::Sysvar, rent::Rent, sysvar::Sysvar,
@ -120,6 +120,22 @@ impl BanksClient {
.map_err(Into::into) .map_err(Into::into)
} }
pub fn process_transaction_with_preflight_and_commitment_and_context(
&mut self,
ctx: Context,
transaction: Transaction,
commitment: CommitmentLevel,
) -> impl Future<Output = Result<BanksTransactionResultWithSimulation, BanksClientError>> + '_
{
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( pub fn get_account_with_commitment_and_context(
&mut self, &mut self,
ctx: Context, 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<Output = Result<(), BanksClientError>> + '_ {
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<Output = Result<(), BanksClientError>> + '_ {
self.process_transaction_with_preflight_and_commitment(
transaction,
CommitmentLevel::default(),
)
}
/// Send a transaction and return until the transaction has been finalized or rejected. /// Send a transaction and return until the transaction has been finalized or rejected.
pub fn process_transaction( pub fn process_transaction(
&mut self, &mut self,

View File

@ -30,6 +30,19 @@ pub struct TransactionStatus {
pub confirmation_status: Option<TransactionConfirmationStatus>, pub confirmation_status: Option<TransactionConfirmationStatus>,
} }
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TransactionSimulationDetails {
pub logs: Vec<String>,
pub units_consumed: u64,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BanksTransactionResultWithSimulation {
pub result: Option<transaction::Result<()>>,
pub simulation_details: Option<TransactionSimulationDetails>,
}
#[tarpc::service] #[tarpc::service]
pub trait Banks { pub trait Banks {
async fn send_transaction_with_context(transaction: Transaction); async fn send_transaction_with_context(transaction: Transaction);
@ -44,6 +57,10 @@ pub trait Banks {
-> Option<TransactionStatus>; -> Option<TransactionStatus>;
async fn get_slot_with_context(commitment: CommitmentLevel) -> Slot; async fn get_slot_with_context(commitment: CommitmentLevel) -> Slot;
async fn get_block_height_with_context(commitment: CommitmentLevel) -> u64; 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( async fn process_transaction_with_commitment_and_context(
transaction: Transaction, transaction: Transaction,
commitment: CommitmentLevel, commitment: CommitmentLevel,

View File

@ -2,9 +2,14 @@ use {
bincode::{deserialize, serialize}, bincode::{deserialize, serialize},
futures::{future, prelude::stream::StreamExt}, futures::{future, prelude::stream::StreamExt},
solana_banks_interface::{ 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::{ solana_sdk::{
account::Account, account::Account,
clock::Slot, clock::Slot,
@ -15,7 +20,7 @@ use {
message::{Message, SanitizedMessage}, message::{Message, SanitizedMessage},
pubkey::Pubkey, pubkey::Pubkey,
signature::Signature, signature::Signature,
transaction::{self, Transaction}, transaction::{self, SanitizedTransaction, Transaction},
}, },
solana_send_transaction_service::{ solana_send_transaction_service::{
send_transaction_service::{SendTransactionService, TransactionInfo}, send_transaction_service::{SendTransactionService, TransactionInfo},
@ -242,6 +247,47 @@ impl Banks for BanksServer {
self.bank(commitment).block_height() 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( async fn process_transaction_with_commitment_and_context(
self, self,
_: Context, _: Context,

View File

@ -3134,6 +3134,15 @@ impl Bank {
) -> TransactionSimulationResult { ) -> TransactionSimulationResult {
assert!(self.is_frozen(), "simulation bank must be frozen"); 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 number_of_accounts = transaction.message().account_keys_len();
let batch = self.prepare_simulation_batch(transaction); let batch = self.prepare_simulation_batch(transaction);
let mut timings = ExecuteTimings::default(); let mut timings = ExecuteTimings::default();

View File

@ -76,20 +76,24 @@ impl SanitizedTransaction {
}) })
} }
/// Create a sanitized transaction from a legacy transaction. Used for tests only. pub fn try_from_legacy_transaction(tx: Transaction) -> Result<Self> {
pub fn from_transaction_for_tests(tx: Transaction) -> Self { tx.sanitize()?;
tx.sanitize().unwrap();
if tx.message.has_duplicates() { if tx.message.has_duplicates() {
Result::<Self>::Err(TransactionError::AccountLoadedTwice).unwrap(); return Err(TransactionError::AccountLoadedTwice);
} }
Self { Ok(Self {
message_hash: tx.message.hash(), message_hash: tx.message.hash(),
message: SanitizedMessage::Legacy(tx.message), message: SanitizedMessage::Legacy(tx.message),
is_simple_vote_tx: false, is_simple_vote_tx: false,
signatures: tx.signatures, 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. /// Return the first signature for this transaction.