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:
parent
e61a736d44
commit
422a095647
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue