Skip to content

Commit be994a5

Browse files
committed
Parallelize Soroban pre-apply step.
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.
1 parent 53be922 commit be994a5

18 files changed

Lines changed: 5121 additions & 146 deletions

src/ledger/ImmutableLedgerView.cpp

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,12 @@ CheckValidLedgerViewWrapper::CheckValidLedgerViewWrapper(
188188
{
189189
}
190190

191+
CheckValidLedgerViewWrapper::CheckValidLedgerViewWrapper(
192+
std::unique_ptr<AbstractLedgerView const> getter)
193+
: mGetter(std::move(getter))
194+
{
195+
}
196+
191197
LedgerHeaderWrapper
192198
CheckValidLedgerViewWrapper::getLedgerHeader() const
193199
{
@@ -355,6 +361,70 @@ ImmutableLedgerView::executeWithMaybeInnerSnapshot(
355361
"ImmutableLedgerView::executeWithMaybeInnerSnapshot is illegal: "
356362
"ImmutableLedgerView has no nested snapshots");
357363
}
364+
SorobanPreApplyLedgerView::SorobanPreApplyLedgerView(
365+
std::shared_ptr<LedgerHeader const> header, AbstractLedgerTxn& ltx,
366+
ApplyLedgerView const& lclView)
367+
: mHeader(std::move(header)), mLtx(ltx), mLclView(lclView)
368+
{
369+
}
370+
371+
LedgerHeaderWrapper
372+
SorobanPreApplyLedgerView::getLedgerHeader() const
373+
{
374+
return LedgerHeaderWrapper(mHeader);
375+
}
376+
377+
LedgerEntryWrapper
378+
SorobanPreApplyLedgerView::getAccount(AccountID const& account) const
379+
{
380+
return load(accountKey(account));
381+
}
382+
383+
LedgerEntryWrapper
384+
SorobanPreApplyLedgerView::getAccount(LedgerHeaderWrapper const& header,
385+
TransactionFrame const& tx) const
386+
{
387+
return getAccount(tx.getSourceID());
388+
}
389+
390+
LedgerEntryWrapper
391+
SorobanPreApplyLedgerView::getAccount(LedgerHeaderWrapper const& header,
392+
TransactionFrame const& tx,
393+
AccountID const& accountID) const
394+
{
395+
return getAccount(accountID);
396+
}
397+
398+
LedgerEntryWrapper
399+
SorobanPreApplyLedgerView::load(LedgerKey const& key) const
400+
{
401+
auto entryPair = mLtx.getNewestVersionBelowRoot(key);
402+
if (entryPair.first)
403+
{
404+
// Modified in this ledger, so the ltx has the authoritative version.
405+
// A null entry means it has been deleted.
406+
if (!entryPair.second)
407+
{
408+
return LedgerEntryWrapper(std::make_shared<LedgerEntry const>());
409+
}
410+
// Alias the entry owned by the ltx instead of copying it: the aliasing
411+
// constructor shares ownership with the InternalLedgerEntry while
412+
// pointing at the LedgerEntry nested inside it.
413+
return LedgerEntryWrapper(std::shared_ptr<LedgerEntry const>(
414+
entryPair.second, &entryPair.second->ledgerEntry()));
415+
}
416+
// Not modified in this ledger, so the last closed ledger snapshot is
417+
// up to date.
418+
return LedgerEntryWrapper(mLclView.loadLiveEntry(key));
419+
}
420+
421+
void
422+
SorobanPreApplyLedgerView::executeWithMaybeInnerSnapshot(
423+
std::function<void(CheckValidLedgerViewWrapper const& ledgerView)> f) const
424+
{
425+
throw std::runtime_error("SorobanPreApplyLedgerView::"
426+
"executeWithMaybeInnerSnapshot is not supported");
427+
}
358428

359429
// === Live BucketList wrapper methods ===
360430

src/ledger/ImmutableLedgerView.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,39 @@ class ApplyLedgerView : private ImmutableLedgerView,
219219
using ImmutableLedgerView::scanLiveEntriesOfType;
220220
};
221221

222+
// An ledger view used by the read-only phase of the Soroban pre-apply.
223+
//
224+
// It's a thin wrapper around the LTX representing the current ledger state,
225+
// and the LCL view, which allows the pre-apply phase to observe the changes
226+
// that happened in the classic phase.
227+
//
228+
// Lookups are first attempted in the LTX *newest version* only (which is thread
229+
// safe as long as we don't mutate the LTX), and only then in the LCL view.
230+
class SorobanPreApplyLedgerView : public AbstractLedgerView
231+
{
232+
public:
233+
SorobanPreApplyLedgerView(std::shared_ptr<LedgerHeader const> header,
234+
AbstractLedgerTxn& ltx,
235+
ApplyLedgerView const& lclView);
236+
237+
LedgerHeaderWrapper getLedgerHeader() const override;
238+
LedgerEntryWrapper getAccount(AccountID const& account) const override;
239+
LedgerEntryWrapper getAccount(LedgerHeaderWrapper const& header,
240+
TransactionFrame const& tx) const override;
241+
LedgerEntryWrapper getAccount(LedgerHeaderWrapper const& header,
242+
TransactionFrame const& tx,
243+
AccountID const& accountID) const override;
244+
LedgerEntryWrapper load(LedgerKey const& key) const override;
245+
void executeWithMaybeInnerSnapshot(
246+
std::function<void(CheckValidLedgerViewWrapper const&)> f)
247+
const override;
248+
249+
private:
250+
std::shared_ptr<LedgerHeader const> mHeader;
251+
AbstractLedgerTxn& mLtx;
252+
ApplyLedgerView mLclView;
253+
};
254+
222255
// A helper class to create and query read-only snapshots
223256
// Automatically decides whether to create a BucketList (recommended), or SQL
224257
// snapshot (deprecated, but currently supported)
@@ -235,6 +268,8 @@ class CheckValidLedgerViewWrapper : public NonMovableOrCopyable
235268
CheckValidLedgerViewWrapper(AbstractLedgerTxn& ltx);
236269
CheckValidLedgerViewWrapper(Application& app);
237270
explicit CheckValidLedgerViewWrapper(ImmutableLedgerView const& ledgerView);
271+
explicit CheckValidLedgerViewWrapper(
272+
std::unique_ptr<AbstractLedgerView const> getter);
238273
#ifdef BUILD_TESTS
239274
// Set by overlay-only mode call sites so commonValid skips the seqnum
240275
// equality check: on-disk seqnums are frozen at genesis while

src/transactions/FeeBumpTransactionFrame.cpp

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -83,39 +83,52 @@ FeeBumpTransactionFrame::FeeBumpTransactionFrame(
8383
#endif
8484

8585
void
86-
FeeBumpTransactionFrame::preParallelApply(
87-
AppConnector& app, AbstractLedgerTxn& ltx, TransactionMetaBuilder& meta,
88-
MutableTransactionResultBase& txResult,
86+
FeeBumpTransactionFrame::preParallelApplyReadOnly(
87+
AppConnector& app, CheckValidLedgerViewWrapper const& ls,
88+
TransactionMetaBuilder& meta, MutableTransactionResultBase& txResult,
8989
SorobanNetworkConfig const& sorobanConfig) const
9090
{
9191
try
9292
{
93-
LedgerTxn ltxTx(ltx);
94-
removeOneTimeSignerKeyFromFeeSource(ltxTx);
95-
meta.pushTxChangesBefore(ltxTx);
96-
ltxTx.commit();
93+
mInnerTx->preParallelApplyReadOnlyWithOptionallyChargedFee(
94+
/*chargeFee=*/false, app, ls, meta, txResult, sorobanConfig,
95+
getContentsHash());
9796
}
9897
catch (std::exception& e)
9998
{
100-
printErrorAndAbort("Exception in preParallelApply ", e.what());
99+
printErrorAndAbort("Exception during read-only preParallelApply: ",
100+
e.what());
101101
}
102102
catch (...)
103103
{
104-
printErrorAndAbort("Unknown exception in preParallelApply");
104+
printErrorAndAbort(
105+
"Unknown exception during read-only preParallelApply");
105106
}
107+
}
106108

109+
void
110+
FeeBumpTransactionFrame::preParallelApplyWrite(
111+
AppConnector& app, AbstractLedgerTxn& ltx, TransactionMetaBuilder& meta,
112+
MutableTransactionResultBase const& txResult) const
113+
{
107114
try
108115
{
109-
mInnerTx->preParallelApply(/*chargeFee=*/false, app, ltx, meta,
110-
txResult, sorobanConfig, getContentsHash());
116+
{
117+
LedgerTxn ltxTx(ltx);
118+
removeOneTimeSignerKeyFromFeeSource(ltxTx);
119+
meta.pushTxChangesBefore(ltxTx);
120+
ltxTx.commit();
121+
}
122+
mInnerTx->preParallelApplyWrite(app, ltx, meta, txResult);
111123
}
112124
catch (std::exception& e)
113125
{
114-
printErrorAndAbort("Exception during preParallelApply: ", e.what());
126+
printErrorAndAbort("Exception during preParallelApply writes: ",
127+
e.what());
115128
}
116129
catch (...)
117130
{
118-
printErrorAndAbort("Unknown exception during preParallelApply");
131+
printErrorAndAbort("Unknown exception during preParallelApply writes");
119132
}
120133
}
121134

src/transactions/FeeBumpTransactionFrame.h

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,14 @@ class FeeBumpTransactionFrame : public TransactionFrameBase
9090

9191
~FeeBumpTransactionFrame() override = default;
9292

93-
void
94-
preParallelApply(AppConnector& app, AbstractLedgerTxn& ltx,
95-
TransactionMetaBuilder& meta,
96-
MutableTransactionResultBase& txResult,
97-
SorobanNetworkConfig const& sorobanConfig) const override;
93+
void preParallelApplyReadOnly(
94+
AppConnector& app, CheckValidLedgerViewWrapper const& ls,
95+
TransactionMetaBuilder& meta, MutableTransactionResultBase& txResult,
96+
SorobanNetworkConfig const& sorobanConfig) const override;
97+
98+
void preParallelApplyWrite(
99+
AppConnector& app, AbstractLedgerTxn& ltx, TransactionMetaBuilder& meta,
100+
MutableTransactionResultBase const& txResult) const override;
98101

99102
std::optional<ParallelTxSuccessVal> parallelApply(
100103
AppConnector& app, ThreadParallelApplyLedgerState const& threadState,

src/transactions/ParallelApplyUtils.cpp

Lines changed: 103 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@
88
#include "ledger/LedgerTxn.h"
99
#include "ledger/NetworkConfig.h"
1010
#include "main/AppConnector.h"
11+
#include "transactions/OperationFrame.h"
1112
#include "transactions/ParallelApplyStage.h"
1213
#include "transactions/TransactionFrameBase.h"
14+
#include "transactions/TransactionUtils.h"
15+
#include "util/BatchExecutor.h"
1316
#include "util/GlobalChecks.h"
1417
#include "xdr/Stellar-ledger-entries.h"
1518
#include "xdrpp/printer.h"
1619
#include <fmt/core.h>
1720
#include <fmt/std.h>
1821
#include <thread>
22+
#include <unordered_map>
1923

2024
namespace
2125
{
@@ -174,8 +178,21 @@ updateMaxOfRoTTLBump(UnorderedMap<LedgerKey, uint32_t>& roTTLBumps,
174178
}
175179
}
176180

181+
void
182+
commitPreParallelApplyWrites(AppConnector& app, AbstractLedgerTxn& ltx,
183+
std::vector<TxBundle const*> const& txBundles)
184+
{
185+
ZoneScoped;
186+
for (auto const* txBundle : txBundles)
187+
{
188+
txBundle->getTx()->preParallelApplyWrite(
189+
app, ltx, txBundle->getEffects().getMeta(),
190+
txBundle->getResPayload());
191+
}
177192
}
178193

194+
} // namespace
195+
179196
namespace stellar
180197
{
181198

@@ -319,14 +336,13 @@ GlobalParallelApplyLedgerState::GlobalParallelApplyLedgerState(
319336
// had their sequence numbers bumped and fees charged. preParallelApply will
320337
// update sequence numbers so it needs to be called before we check
321338
// LedgerTxn.
322-
preParallelApplyAndCollectModifiedClassicEntries(app, ltx, stages);
339+
preApplyAndCollectModifiedClassicEntries(app, ltx, stages);
323340
}
324341

325342
void
326-
GlobalParallelApplyLedgerState::
327-
preParallelApplyAndCollectModifiedClassicEntries(
328-
AppConnector& app, AbstractLedgerTxn& ltx,
329-
std::vector<ApplyStage> const& stages)
343+
GlobalParallelApplyLedgerState::preApplyAndCollectModifiedClassicEntries(
344+
AppConnector& app, AbstractLedgerTxn& ltx,
345+
std::vector<ApplyStage> const& stages)
330346
{
331347
auto fetchInMemoryClassicEntries =
332348
[&](xdr::xvector<LedgerKey> const& keys) {
@@ -353,34 +369,99 @@ GlobalParallelApplyLedgerState::
353369
}
354370
};
355371

356-
// First call preParallelApply on all transactions,
357-
// and then load from footprints. This order is important
358-
// because preParallelApply modifies the fee source accounts
359-
// and those accounts could show up in the footprint
360-
// of a different transaction.
372+
std::vector<TxBundle const*> txBundles;
361373
for (auto const& stage : stages)
362374
{
363375
for (auto const& txBundle : stage)
364376
{
365-
// Make sure to call preParallelApply on all txs because this will
366-
// modify the fee source accounts sequence numbers.
367-
txBundle.getTx()->preParallelApply(
368-
app, ltx, txBundle.getEffects().getMeta(),
369-
txBundle.getResPayload(), mSorobanConfig);
377+
txBundles.emplace_back(&txBundle);
370378
}
371379
}
372380

373-
for (auto const& stage : stages)
381+
// Pre-apply all the transactions before loading the footprint entries. This
382+
// order is important because the pre-apply modifies the source accounts,
383+
// and those accounts could show up in the footprint of a transaction
384+
// applied by a different thread, thus breaking the invariant that
385+
// transactions are independent of each other acrooss threads.
386+
//
387+
// The pre-apply process is done in two phases: a parallel read-only phase
388+
// where the transactions are validated, and a serial write phase where the
389+
// writes are committed to the ledger.
390+
//
391+
// This phase separatation hinges on the fact that the validation outcome
392+
// of any Soroban transaction can't be influenced by the pre-apply writes
393+
// performed by another Soroban transaction. Specifically, pre-apply writes
394+
// only include:
395+
// - The source account sequence number bumps - this is fine because we
396+
// have only a single transaction per source account per ledger
397+
// - The removal of one-time pre-authorized tx signers - this is also fine
398+
// because any given transaction in a ledger is unique, and increasing the
399+
// sub-entry count of a source/sponsor account is not relevant at that
400+
// point, as the fees have already been successfully charged.
401+
402+
auto header =
403+
std::make_shared<LedgerHeader const>(ltx.loadHeader().current());
404+
readOnlyParallelPreApply(app, txBundles, header, ltx);
405+
commitPreParallelApplyWrites(app, ltx, txBundles);
406+
407+
for (auto const& txBundle : txBundles)
374408
{
375-
for (auto const& txBundle : stage)
376-
{
377-
auto const& footprint =
378-
txBundle.getTx()->sorobanResources().footprint;
409+
auto const& footprint = txBundle->getTx()->sorobanResources().footprint;
410+
fetchInMemoryClassicEntries(footprint.readWrite);
411+
fetchInMemoryClassicEntries(footprint.readOnly);
412+
}
413+
}
379414

380-
fetchInMemoryClassicEntries(footprint.readWrite);
381-
fetchInMemoryClassicEntries(footprint.readOnly);
415+
void
416+
GlobalParallelApplyLedgerState::readOnlyParallelPreApply(
417+
AppConnector& app, std::vector<TxBundle const*> const& txBundles,
418+
std::shared_ptr<LedgerHeader const> header, AbstractLedgerTxn& ltx)
419+
{
420+
ZoneScoped;
421+
if (txBundles.empty())
422+
{
423+
return;
424+
}
425+
426+
// Run pre-apply for [begin, end) transaction indices.
427+
auto runRange = [&](size_t begin, size_t end) {
428+
// NB: mLCLApplyView is not thread-safe, so we need to copy it into a
429+
// thread-local view.
430+
CheckValidLedgerViewWrapper ledgerView(
431+
std::make_unique<SorobanPreApplyLedgerView>(header, ltx,
432+
mLCLApplyView));
433+
for (size_t i = begin; i < end; ++i)
434+
{
435+
auto const* txBundle = txBundles[i];
436+
txBundle->getTx()->preParallelApplyReadOnly(
437+
app, ledgerView, txBundle->getEffects().getMeta(),
438+
txBundle->getResPayload(), mSorobanConfig);
382439
}
440+
};
441+
442+
size_t taskCount = app.getBatchExecutor().preferredTaskCount();
443+
if (taskCount <= 1)
444+
{
445+
runRange(0, txBundles.size());
446+
return;
447+
}
448+
449+
std::vector<std::function<int()>> tasks;
450+
tasks.reserve(taskCount);
451+
size_t begin = 0;
452+
size_t baseChunk = txBundles.size() / taskCount;
453+
size_t remainder = txBundles.size() % taskCount;
454+
for (size_t i = 0; i < taskCount; ++i)
455+
{
456+
size_t end = begin + baseChunk + (i < remainder ? 1 : 0);
457+
tasks.emplace_back([runRange, begin, end]() {
458+
runRange(begin, end);
459+
return 0;
460+
});
461+
begin = end;
383462
}
463+
releaseAssert(begin == txBundles.size());
464+
app.getBatchExecutor().executeBatch(std::move(tasks));
384465
}
385466

386467
void

0 commit comments

Comments
 (0)