Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TransactionBatch - hold RuntimeTransaction #3041

Merged

Conversation

apfitzge
Copy link

@apfitzge apfitzge commented Sep 30, 2024

Problem

  • RuntimeTransaction helps us in multiple ways:
    • caches metadata so we do not re-calculate later in pipeline
    • easier transition to new transaction type (fields not present on new tx type are cached)

Summary of Changes

  • Use RuntimeTransaction wherever required by forcing it in TransactionBatch
  • RuntimeTransaction implements Deref for the wrapped transaction type, so in many places this does not require any changes in function implementation
  • This PR is a near minimum of changes required to get TransactionBatch to use RuntimeTransaction. This makes the type available wherever we may need it in our processing pipeline, and we can optimize some of the processing to use the meta in the pipeline in subsequent PRs.

Fixes #

Copy link

mergify bot commented Sep 30, 2024

If this PR represents a change to the public RPC API:

  1. Make sure it includes a complementary update to rpc-client/ (example)
  2. Open a follow-up PR to update the JavaScript client @solana/web3.js (example)

Thank you for keeping the RPC clients in sync with the server API @apfitzge.

@apfitzge apfitzge force-pushed the sep30_big_runtime_transaction_transition branch 2 times, most recently from fb11a17 to 3fddfe9 Compare October 1, 2024 13:52
@apfitzge apfitzge self-assigned this Oct 1, 2024
@apfitzge apfitzge force-pushed the sep30_big_runtime_transaction_transition branch 4 times, most recently from b766751 to 7c93e54 Compare October 7, 2024 13:16
@apfitzge apfitzge marked this pull request as ready for review October 7, 2024 14:07
@apfitzge apfitzge requested review from tao-stones and ryoqun October 7, 2024 14:07
@@ -24,7 +24,7 @@ use {
/// for example: message hash, simple-vote-tx flag, limits set by instructions
pub trait StaticMeta {
fn message_hash(&self) -> &Hash;
fn is_simple_vote_tx(&self) -> bool;
fn is_simple_vote_transaction(&self) -> bool;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tao-stones changed the name here to be consistent with SanitizedTransaction, meaning no impl changes when we start to use the transaction traits.
Prefer the longer/explicit name anyway instead of abbreviation.

let txs = batch
.sanitized_transactions()
.iter()
.map(|tx| tx.deref().clone())
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will be replaced by use of upcoming trait SVMTransactionAdapter in a future PR

@apfitzge apfitzge requested a review from jstarry October 7, 2024 14:35
banks-server/src/banks_server.rs Outdated Show resolved Hide resolved
runtime-transaction/src/runtime_transaction.rs Outdated Show resolved Hide resolved
core/src/banking_stage/consumer.rs Outdated Show resolved Hide resolved
@@ -204,13 +209,11 @@ impl PrioritizationFeeCache {
continue;
}

let compute_budget_limits = process_compute_budget_instructions(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

@jstarry
Copy link

jstarry commented Oct 7, 2024

I'm in favor of introducing SVMTransactionAdapter in this PR to clean up the derefs

@apfitzge
Copy link
Author

apfitzge commented Oct 7, 2024

I'm in favor of introducing SVMTransactionAdapter in this PR to clean up the derefs

Introducing it now only gets rid of one deref. think for the comparison on fully_verify_transaction we need some other mechanism for comparing. Agree with you that comparing the .deref() of each isnt' good.

address_loader,
reserved_account_keys,
)
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it has two steps here: creating the "static" version of RuntimeTransaction::<SanitizedVersionedTransaction>, then creating the "dynamic" version of RuntimeTransaction<SanitizedTransaction>; this illustrates the ideal use of RuntimeTransaction, which is: the "static" version can advance to "dynamic" version; and "dynamic" version is the superset of "static" version.

How do you think to create "static" version in ImmutableDeserializedPacket::new(), then consume "static" by advance it into "dynamic" when needed? I know you are expecting IDP to be retired soon, but I think it's worth to update IDP to properly use RuntimeTransaction to shake out its usages, but also save one clone of SanitizedVersionedTransaction at ln 128.

I'm thinking something like this:

  1. change ImmutableDeserializePacket def to:
enum RuntimeTransactionVersion {
    Static (RuntimeTransaction::<SanitizedVersionedTransaction>),
    Dynamic (RuntimeTransaction::<SanitizedTransaction>),
}

pub struct ImmutableDeserializedPacket {
    original_packet: Packet,
    runtime_transaction: RuntimeTransactionVersion,
    // ^^ it caches the rest of stuff IDP used to have
}
  1. change new() to:
    pub fn new(packet: Packet) -> Result<Self, DeserializedPacketError> {
        let versioned_transaction: VersionedTransaction = packet.deserialize_slice(..)?;
        let sanitized_transaction = SanitizedVersionedTransaction::try_from(versioned_transaction)?;

        Ok(Self {
            original_packet: packet,
            runtime_transaction: Static(RuntimeTransaction::<SanitizedVersionedTransaction>::try_from(
                sanitized_transaction, None, None)),
        })
    }
  1. So the build_sanitized_transaction() would be renamed to:
    pub fn get_sanitized_transaction(
        &mut self,
        votes_only: bool,
        address_loader: impl AddressLoader,
        reserved_account_keys: &HashSet<Pubkey>,
    ) -> Result<()> {
        if votes_only && !self.is_simple_vote() {
            return None;
        }

        if self.runtime_transaction.is_static() {
            // consume static, advance to dynamic
            self.runtime_transaction = Dynamic( 
                RuntimeTransaction::<SanitizedTransaction>::try_from(
                    self.runtime_transaction, address_loader, reserved_account_keys)
            )?;
        }

        Ok(())
    }

(sorry for pseudo code may not be practical)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could do something like that. enum should be relatively light overhead. i'll test it out tomorrow.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking through it this morning. I'm not sure it's worth the added complexity.

If we stick to not implementing Clone for RuntimeTransaction (could practice imo), then to process the batch we need to take owership of the RuntimeTransaction; we'd then need to put the RuntimeTransaction back into the ImmutableDeserializedPacket on retry.
This adds a ton of complexity and possible bugs.

I think also transitionining from static->dynamic runtime also breaks the naming of Immutable.

Copy link
Author

@apfitzge apfitzge Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one option is to only implement Clone for RuntimeTransaction<SanitizedTransaction>; but that feels a bit dirty, given how much effort I just spent to remove all the SanitizedTransaction::clone from replay stage.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chatted offline, we believed IDP will be refactored out soon, so we'll leave it as is for now.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds fine to leave as is but was also wondering about if we create and store a RuntimeTransaction<SanitizedVersionedTransaction> inside ImmutableDeserializedPacket::new and then add a new RuntimeTransaction<SanitizedTransaction>::try_from_ref function which takes a &RuntimeTransaction::<SanitizedVersionedTransaction> and does the clone internally?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This brings back some concern we had about adding Clone to the TransactionMeta which means we must be careful about the Meta size. Adding Clone now may it difficult for us to remove in the future.

See above comment, the IDP is short-lived and will be removed soon.

@apfitzge apfitzge force-pushed the sep30_big_runtime_transaction_transition branch from 7c93e54 to 33de7d8 Compare October 17, 2024 18:25
@@ -139,7 +147,6 @@ pub struct UsageCostDetails<'a, Tx: SVMMessage> {
pub programs_execution_cost: u64,
pub loaded_accounts_data_size_cost: u64,
pub allocated_accounts_data_size: u64,
pub signature_details: TransactionSignatureDetails,
Copy link
Author

@apfitzge apfitzge Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is stored by RuntimeTransaction so we can get rid of it here

tao-stones
tao-stones previously approved these changes Oct 29, 2024
Copy link

@tao-stones tao-stones left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks great!

address_loader,
reserved_account_keys,
)
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chatted offline, we believed IDP will be refactored out soon, so we'll leave it as is for now.

let transactions: Vec<SanitizedTransaction> = batch
.sanitized_transactions()
.iter()
.map(|tx| tx.deref().clone())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought we touched on this before, just to be clear, the deref().clone() doesn't have meaningful impact on perf, as this function is on hot path.

@apfitzge apfitzge force-pushed the sep30_big_runtime_transaction_transition branch from f7d6794 to 2c50748 Compare October 30, 2024 16:25
@apfitzge
Copy link
Author

@jstarry @tao-stones I rebased on master just to make sure CI still passes - make sure no SanitizedTransactions snuck in in new tests anywhere.

I also pushed the SVMTransactionAdapter trait so we can remove some of the derefs that were previously being done.

@@ -624,45 +616,6 @@ mod tests {
assert_eq!(1, data_bytes_cost);
}

#[test]
fn test_cost_model_with_failed_compute_budget_transaction() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can keep this test but change it so that there is a compute budget ix that fails in sanitize_and_convert_to_compute_budget_limits rather than in ComputeBudgetInstructionDetails::try_from since any errors in try_from would be caught earlier when RuntimeTransaction is created. Also, shouldn't get_transaction_cost be updated to use a runtime tx to avoid reprocessing compute budget ixs?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this test back as you described - ended up just updating cost-model code to use RuntimeTransaction as well to make it so I don't need to update the test again next PR

@@ -24,7 +24,10 @@ use {
transaction_batch::TransactionBatch,
verify_precompiles::verify_precompiles,
},
solana_runtime_transaction::instructions_processor::process_compute_budget_instructions,
solana_runtime_transaction::{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason that execute_and_commit_transactions_locked is recomputing the compute budget details still rather than using them from runtime tx?

solana_runtime_transaction::instructions_processor::process_compute_budget_instructions,
solana_runtime_transaction::{
instructions_processor::process_compute_budget_instructions,
runtime_transaction::RuntimeTransaction,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also how come check_fee_payer_unlocked isn't using runtime tx now?

address_loader,
reserved_account_keys,
)
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds fine to leave as is but was also wondering about if we create and store a RuntimeTransaction<SanitizedVersionedTransaction> inside ImmutableDeserializedPacket::new and then add a new RuntimeTransaction<SanitizedTransaction>::try_from_ref function which takes a &RuntimeTransaction::<SanitizedVersionedTransaction> and does the clone internally?

@@ -28,7 +28,10 @@ use {
solana_cost_model::cost_model::CostModel,
solana_measure::measure_us,
solana_runtime::{bank::Bank, bank_forks::BankForks},
solana_runtime_transaction::instructions_processor::process_compute_budget_instructions,
solana_runtime_transaction::{
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could eliminate one compute budget calculation by using the compute budget details inside runtime tx inside buffer_packets

Copy link

@jstarry jstarry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's very ergonomic to not implement clone for runtime tx, especially for tests. I would be in favor of adding a clone impl generally but can we at the very least add a dcou impl?

#[cfg(feature = "dev-context-only-utils")]
impl<T> Clone for RuntimeTransaction<T>
where
    T: Clone,
{
    fn clone(&self) -> Self {
        Self {
            transaction: self.transaction.clone(),
            meta: self.meta.clone(),
        }
    }
}

/// that are required by some outside interfaces.
/// Eventually this trait should go away, but for now it is necessary.
pub trait SVMTransactionAdapter: SVMTransaction {
fn as_sanitized_transaction(&self) -> impl Borrow<SanitizedTransaction>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes more sense to have a to_sanitized_transaction method which does the clone internally since we only really need this adapter method for clones. The commit_transactions call-site for geyser can use deref directly

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commit_transactions is where we actually need/want this as borrow.

Because we need to have a SanitizedTransaction and if we don't we need to convert to one - it can't just be deref once we are generic.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but doing the borrow there also let's us not clone the the SanitizedTransaction if that's what we already have

@apfitzge apfitzge mentioned this pull request Nov 4, 2024
@apfitzge apfitzge changed the title Use RuntimeTransaction TransactionBatch - hold RuntimeTransaction Nov 4, 2024
@apfitzge apfitzge merged commit 9b56f30 into anza-xyz:master Nov 4, 2024
52 checks passed
@apfitzge apfitzge deleted the sep30_big_runtime_transaction_transition branch November 4, 2024 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants