Skip to content

Parallelize transaction validation for transaction sets. - #5449

Draft
dmkozh wants to merge 2 commits into
stellar:masterfrom
dmkozh:txset_par_checkvalid
Draft

Parallelize transaction validation for transaction sets.#5449
dmkozh wants to merge 2 commits into
stellar:masterfrom
dmkozh:txset_par_checkvalid

Conversation

@dmkozh

@dmkozh dmkozh commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Description

This affects both nominating a new transaction set, and validating the incoming transaction set. Thanks to the fact that most of the time we're either applying the ledger, or validating a transaction set, we can use the efficient CPU-pinned batch executor for this.

This speeds up the invalid transaction trimming step by ~20ms on large benchmarks, and in general should increase the transaction validation step proportionally to the number of cores.

Checklist

  • Reviewed the contributing document
  • Rebased on top of master (no merge commits)
  • Ran clang-format v8.0.0 (via make format or the Visual Studio extension)
  • Compiles
  • Ran all tests
  • If change impacts performance, include supporting evidence per the performance document

dmkozh added 2 commits August 14, 2026 16:09
While the current logic intertwines reads and writes, in fact it can be cleanly separated into a read-only validation step, and a sequential commit step that simply bumps the sequence numbers and removes pre-authorized tx signers. This is possible because that while the writes change the entries that take part in validation, none of these changes are relevant during the validation. Specifically, sequence number bump is only observable by a single transaction (the one that has the respective account as a source), and the pre-authorized tx signer by definition belongs to a single transaction. There is also a subtle caveat to the latter operation: it increases the available balance of the signer owner (or its sponsor), but since at the pre-apply time the fees have already been charged, we're only checking that the account available balance is non-negative, which is an invariant that must always hold in the current protocol.

The change is not protocol-gated because it's not a protocol change for the *current* protocol. It was technically a protocol change prior to p26 where we had a bug that actually did allow overcharging the fee bump source accounts and thus making their available balance to go negative. However, the bug has been fixed without the behavior ever triggering on-chain, and thus this replay-only behavior change should be non-observable.

This change significantly speeds up the pre-apply step. On the local high TPL benchmarks I'm getting 30-60ms improvement locally compared to the main branch version.
This affects both nominating a new transaction set, and validating the incoming transaction set. Thanks to the fact that most of the time we're either applying the ledger, or validating a transaction set, we can use the efficient CPU-pinned batch executor for this.

This speeds up the invalid transaction trimming step by ~20ms on large benchmarks, and in general should increase the transaction validation step proportionally to the number of cores.
@dmkozh
dmkozh force-pushed the txset_par_checkvalid branch from 12192a2 to 443a838 Compare September 9, 2026 21:42
virtual LedgerHeaderWrapper getLedgerHeader() const = 0;
// Returns the pointer to Soroban network config snapshot associated with
// this view, or nullptr when the view doesn't carry one.
virtual SorobanNetworkConfig const* getSorobanNetworkConfig() const = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not a fan of the SorobanNetworkConfig changes here, just because the optional is a footgun. We already have some places where the config is an optional due to replay of older protocol versions, now this can mean you just happen to have a different ledger state view. I don't think this is necessary, can we not just have the main thread get the soroban network config then pass a copy/reference to be used by the parallel workers instead?

//
// Lookups are first attempted in the LTX *newest version* only (which is thread
// safe as long as we don't mutate the LTX), and only then in the LCL view.
class SorobanPreApplyLedgerView : public AbstractLedgerView

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This class seems sketchy to me for a few reasons. First, we're directly calling LedgerTxn functions in a multithreaded environment. We don't have any assertions that come with regular LedgerTxn functions, like that there are no children invalidating the current map, the main thread isn't calling mutating functions like load, etc.

I'd rather just use the mGlobalEntryMap struct here instead of a new viewer object, or if the interface is too tricky, something similar. Basically, I want to keep the pattern where a single thread always calls into Ltx and prepares some const, thread safe mappiong of relevant hot state for the multithreaded RO consumer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

First, we're directly calling LedgerTxn functions in a multithreaded environment.

We're just calling the thread-safe getNewestBelowRoot here. We could add another layer of abstraction on top of LTX, but I'm not sure if that wins us much. This view is specialized and it seems unlikely that anyone would accidentally use it outside of the intended context. LTX could be const here though, which removes concerns about internal mutation. I would not worry about the external mutation; with parallelization we're introducing a lot of blocking parallel steps which are easy to argue about, and I don't see much point in making the code more complicated to account for the hypothetical off-thread mutation. Basically we can treat BatchExecutor steps as sequential steps from the overall app standpoint, we only need to make sure that the worker threads do not race between each other.

I'd rather just use the mGlobalEntryMap struct here instead of a new viewer object, or if the interface is too tricky, something similar.

I'm not sure what do you mean, this is a view that we need to pass to CheckValidLedgerViewWrapper. I don't think we need to create yet another interface for that.

Comment thread src/transactions/TransactionFrame.cpp
#endif
~LedgerTxnReadOnly() override;
LedgerHeaderWrapper getLedgerHeader() const override;
SorobanNetworkConfig const* getSorobanNetworkConfig() const override;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This seems like a bit of a footgun, since in the tx subysystem a null NetworkConfig could be for historical protocol replay, or if our LedgerView just happens to by of the wrong type. Can we just keep getLastClosedSorobanNetworkConfig as is, call it from the main thread, and pass in a reference/copy to the multithread workers?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think it's appropriate for the ledger view to provide access to the config, and . But also upon revisiting the code it seems weird that we also load the config from the snapshot. I wouldn't dwell on LedgerTxnReadOnly specifically, as we use it as kind of a hack in tests. In prod code paths LedgerTxnReadOnly is not really used as a full AbstractLedgerView, and it probably doesn't have to inherit the full interface for what it does. I'll need to experiment a bit to figure that out. Ultimately I feel like this points at some issues with the current design. I would prefer resolving these instead of shoving the network config as yet another side input for the transaction machinery.

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.

2 participants