use solana_account::{AccountSharedData, ReadableAccount};
use solana_address::Address;
use solana_signature::Signature;
use solana_transaction_error::TransactionError;
use crate::{
InspectMode, Svm, SvmTypesHost,
accounts_db::{StateCheckpoint, is_managed_sysvar_account},
error::HPSVMError,
state_change_sink::{AccountChange, AccountChangeRecord, StateChangeSink},
tx_registry::TxRegistryError,
types::{TransactionMetadata, TransactionResult},
};
#[derive(Debug)]
pub struct DetachedTx {
pub(crate) meta: TransactionMetadata,
pub(crate) post_accounts: Vec<(Address, AccountSharedData)>,
pub(crate) status: Result<(), TransactionError>,
pub(crate) included: bool,
pub(crate) fee_payer: Option<Address>,
pub(crate) fatal_error: Option<HPSVMError>,
pub(crate) inspector_live: bool,
}
impl DetachedTx {
pub fn meta(&self) -> &TransactionMetadata {
&self.meta
}
pub fn post_accounts(&self) -> &[(Address, AccountSharedData)] {
&self.post_accounts
}
pub fn status(&self) -> &Result<(), TransactionError> {
&self.status
}
pub fn included(&self) -> bool {
self.included
}
pub fn fee_payer(&self) -> Option<Address> {
self.fee_payer
}
#[inline]
pub(crate) fn take_fatal(&mut self) -> Option<HPSVMError> {
self.fatal_error.take()
}
#[cold]
pub(crate) fn for_dispatch_failure(error: &TxRegistryError, signature: Signature) -> Self {
Self {
meta: TransactionMetadata { signature, ..Default::default() },
post_accounts: Vec::new(),
status: Err(error.transaction_error()),
included: false,
fee_payer: None,
fatal_error: None,
inspector_live: false,
}
}
}
#[must_use = "call commit to keep the staged post-state or discard to roll it back"]
pub struct ExecutedTx<'svm, 'a, T: SvmTypesHost> {
svm: &'svm mut Svm<'a, T>,
checkpoint: StateCheckpoint,
result: TransactionResult,
history_entry: Option<(Signature, TransactionResult)>,
cache_dirty: bool,
changes: Vec<AccountChangeRecord>,
inspector_live: bool,
}
impl<'svm, 'a, T: SvmTypesHost> ExecutedTx<'svm, 'a, T> {
pub(crate) fn new(
svm: &'svm mut Svm<'a, T>,
checkpoint: StateCheckpoint,
result: TransactionResult,
history_entry: Option<(Signature, TransactionResult)>,
cache_dirty: bool,
changes: Vec<AccountChangeRecord>,
inspector_live: bool,
) -> Self {
Self { svm, checkpoint, result, history_entry, cache_dirty, changes, inspector_live }
}
pub const fn result(&self) -> &TransactionResult {
&self.result
}
pub fn commit(self) -> TransactionResult {
let Self { svm, history_entry, result, changes, inspector_live, .. } = self;
if inspector_live {
for record in &changes {
svm.on_account_change::<{ InspectMode::DynInspector }>(AccountChange {
address: record.address,
before: record.before.as_ref(),
after: record.after.as_ref(),
});
}
}
if let Some((signature, entry)) = history_entry {
svm.history.add_new_transaction(signature, entry);
}
result
}
pub fn commit_with<S: StateChangeSink>(
self,
sink: &mut S,
) -> Result<TransactionResult, S::Error> {
let Self { svm, checkpoint, result, history_entry, cache_dirty, changes, inspector_live } =
self;
for record in &changes {
let change = AccountChange {
address: record.address,
before: record.before.as_ref(),
after: record.after.as_ref(),
};
if let Err(error) = sink.on_account_change(change) {
svm.accounts.rollback(checkpoint);
if cache_dirty {
svm.accounts.rebuild_sysvar_cache();
svm.accounts.rebuild_program_cache().expect(
"rollback restores previously valid state, so the cache rebuild cannot fail",
);
}
return Err(error);
}
}
if inspector_live {
for record in &changes {
svm.on_account_change::<{ InspectMode::DynInspector }>(AccountChange {
address: record.address,
before: record.before.as_ref(),
after: record.after.as_ref(),
});
}
}
if let Some((signature, entry)) = history_entry {
svm.history.add_new_transaction(signature, entry);
}
let _ = checkpoint;
Ok(result)
}
pub fn discard(self) -> TransactionResult {
self.svm.accounts.rollback(self.checkpoint);
if self.cache_dirty {
self.svm.accounts.rebuild_sysvar_cache();
self.svm.accounts.rebuild_program_cache().expect(
"rollback restores previously valid state, so the cache rebuild cannot fail",
);
}
self.result
}
}
pub(crate) struct StagedFailure {
error: crate::error::HPSVMError,
result: TransactionResult,
}
impl StagedFailure {
pub(crate) const fn new(error: crate::error::HPSVMError, result: TransactionResult) -> Self {
Self { error, result }
}
pub(crate) fn into_error(self) -> crate::error::HPSVMError {
self.error
}
pub(crate) fn into_result(self) -> TransactionResult {
self.result
}
}
pub(crate) fn staging_touches_caches(
post_accounts: &[(Address, AccountSharedData)],
overlay: &crate::accounts_db::AccountsDb,
) -> bool {
post_accounts.iter().any(|(address, account)| {
account.executable() ||
is_managed_sysvar_account(address) ||
overlay.get_account_ref(address).is_some_and(|pre| pre.executable())
})
}
#[cfg(test)]
mod tests {
use solana_keypair::Keypair;
use solana_message::Message;
use solana_signer::Signer;
use solana_system_interface::instruction::transfer;
use solana_transaction::Transaction;
use super::*;
use crate::Hpsvm;
fn transfer_fixture() -> (Hpsvm, Keypair, Address, Transaction, Signature) {
let mut svm = Hpsvm::new();
let payer = Keypair::new();
let payer_address = payer.pubkey();
let recipient = Address::new_unique();
svm.airdrop(&payer_address, 10_000).unwrap();
let tx = Transaction::new(
&[&payer],
Message::new(&[transfer(&payer_address, &recipient, 64)], Some(&payer_address)),
svm.latest_blockhash(),
);
let signature = tx.signatures[0];
(svm, payer, recipient, tx, signature)
}
#[test]
fn commit_keeps_the_journal_entries_that_stage_the_post_state() {
let (mut svm, _, _, tx, signature) = transfer_fixture();
let before = svm.accounts.journal_len();
let result = svm.transact(tx).expect("transact should stage").commit();
assert!(result.is_ok());
assert!(svm.accounts.journal_len() > before);
assert!(svm.history.check_transaction(&signature));
}
#[test]
fn discard_truncates_the_journal_back_to_the_checkpoint() {
let (mut svm, payer, recipient, tx, signature) = transfer_fixture();
let payer_address = payer.pubkey();
let before = svm.accounts.journal_len();
let payer_before = svm.get_account(&payer_address).unwrap();
let result = svm.transact(tx).expect("transact should stage").discard();
assert!(result.is_ok());
assert_eq!(svm.accounts.journal_len(), before);
assert!(!svm.history.check_transaction(&signature));
assert_eq!(svm.get_account(&payer_address).unwrap(), payer_before);
assert_eq!(svm.get_balance(&recipient), None);
}
#[test]
fn discard_of_a_failed_execution_restores_state_without_history() {
let (mut svm, payer, recipient, _, _) = transfer_fixture();
let payer_address = payer.pubkey();
let payer_before = svm.get_account(&payer_address).unwrap();
let failing = Transaction::new(
&[&payer],
Message::new(&[transfer(&payer_address, &recipient, u64::MAX)], Some(&payer_address)),
svm.latest_blockhash(),
);
svm.set_sigverify(false);
let before = svm.accounts.journal_len();
let err = svm.transact(failing).expect("a failing execution must still stage").discard();
let err = err.expect_err("discard must surface the execution failure");
assert_eq!(svm.accounts.journal_len(), before);
assert_eq!(svm.get_account(&payer_address).unwrap(), payer_before);
assert_eq!(svm.get_balance(&recipient), None);
assert!(svm.get_transaction(&err.meta.signature).is_none());
}
}