diff --git a/Builds/VisualStudio/stellar-core.vcxproj b/Builds/VisualStudio/stellar-core.vcxproj index b53342f8ce..5ed8369b8c 100644 --- a/Builds/VisualStudio/stellar-core.vcxproj +++ b/Builds/VisualStudio/stellar-core.vcxproj @@ -539,6 +539,7 @@ exit /b 0 + @@ -684,6 +685,8 @@ exit /b 0 + + @@ -996,6 +999,7 @@ exit /b 0 + @@ -1104,6 +1108,8 @@ exit /b 0 + + diff --git a/Builds/VisualStudio/stellar-core.vcxproj.filters b/Builds/VisualStudio/stellar-core.vcxproj.filters index 6bb795c159..fd2f163e59 100644 --- a/Builds/VisualStudio/stellar-core.vcxproj.filters +++ b/Builds/VisualStudio/stellar-core.vcxproj.filters @@ -1377,6 +1377,15 @@ ledger + + herder + + + simulation + + + simulation + @@ -2402,6 +2411,15 @@ main + + herder + + + simulation + + + simulation + ledger diff --git a/configure.ac b/configure.ac index 1ec808f239..5f183ceca9 100644 --- a/configure.ac +++ b/configure.ac @@ -32,7 +32,7 @@ if test -z "${WFLAGS+set}"; then WFLAGS="$WFLAGS -Werror=unused-result" fi -test "${CFLAGS+set}" || CFLAGS="-g -O2 -fno-omit-frame-pointer" +test "${CFLAGS+set}" || CFLAGS="-g -fno-omit-frame-pointer" test "${CXXFLAGS+set}" || CXXFLAGS="$CFLAGS" AC_PROG_CC([clang gcc cc]) @@ -115,6 +115,16 @@ AS_IF([test "x$enable_codecoverage" = "xyes"], [ CFLAGS="$CFLAGS -fprofile-instr-generate -fcoverage-mapping" ]) +AC_ARG_ENABLE([debugmode], + AS_HELP_STRING([--enable-debugmode], + [build in debug mode])) + +AS_IF([test "x$enable_debugmode" != "xyes"], [ + AC_MSG_NOTICE([ adding -O2 optimization flags ]) + CXXFLAGS="$CXXFLAGS -O2" + CFLAGS="$CFLAGS -O2" +]) + AC_ARG_ENABLE([threadsanitizer], AS_HELP_STRING([--enable-threadsanitizer], [build with thread-sanitizer (TSan) instrumentation])) diff --git a/src/herder/HerderImpl.cpp b/src/herder/HerderImpl.cpp index e3e1eeb601..6e8c137e86 100644 --- a/src/herder/HerderImpl.cpp +++ b/src/herder/HerderImpl.cpp @@ -1350,7 +1350,7 @@ HerderImpl::triggerNextLedger(uint32_t ledgerSeqToTrigger, // our first choice for this round's set is all the tx we have collected // during last few ledger closes auto const& lcl = mLedgerManager.getLastClosedLedgerHeader(); - TxSetPhaseTransactions txPhases; + PerPhaseTransactionList txPhases; txPhases.emplace_back(mTransactionQueue.getTransactions(lcl.header)); if (protocolVersionStartsFrom(lcl.header.ledgerVersion, @@ -1415,7 +1415,7 @@ HerderImpl::triggerNextLedger(uint32_t ledgerSeqToTrigger, upperBoundCloseTimeOffset = nextCloseTime - lcl.header.scpValue.closeTime; lowerBoundCloseTimeOffset = upperBoundCloseTimeOffset; - TxSetPhaseTransactions invalidTxPhases; + PerPhaseTransactionList invalidTxPhases; invalidTxPhases.resize(txPhases.size()); auto [proposedSet, applicableProposedSet] = diff --git a/src/herder/ParallelTxSetBuilder.cpp b/src/herder/ParallelTxSetBuilder.cpp new file mode 100644 index 0000000000..ba418ae9b4 --- /dev/null +++ b/src/herder/ParallelTxSetBuilder.cpp @@ -0,0 +1,379 @@ +// Copyright 2024 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "herder/ParallelTxSetBuilder.h" +#include "herder/SurgePricingUtils.h" +#include "herder/TxSetFrame.h" +#include "transactions/TransactionFrameBase.h" +#include "util/BitSet.h" + +#include + +namespace stellar +{ +namespace +{ + +struct ParallelPartitionConfig +{ + ParallelPartitionConfig(Config const& cfg, + SorobanNetworkConfig const& sorobanCfg) + : mStageCount( + std::max(cfg.SOROBAN_PHASE_STAGE_COUNT, static_cast(1))) + , mThreadsPerStage(sorobanCfg.ledgerMaxParallelThreads()) + , mInstructionsPerThread(sorobanCfg.ledgerMaxInstructions() / + mStageCount) + { + } + + uint64_t + instructionsPerStage() const + { + return mInstructionsPerThread * mThreadsPerStage; + } + + uint32_t mStageCount = 0; + uint32_t mThreadsPerStage = 0; + uint64_t mInstructionsPerThread = 0; +}; + +struct BuilderTx +{ + size_t mId = 0; + uint32_t mInstructions = 0; + BitSet mReadOnlyFootprint; + BitSet mReadWriteFootprint; + + BuilderTx(size_t txId, TransactionFrameBase const& tx, + UnorderedMap const& entryIdMap) + : mId(txId), mInstructions(tx.sorobanResources().instructions) + { + auto const& footprint = tx.sorobanResources().footprint; + for (auto const& key : footprint.readOnly) + { + mReadOnlyFootprint.set(entryIdMap.at(key)); + } + for (auto const& key : footprint.readWrite) + { + mReadWriteFootprint.set(entryIdMap.at(key)); + } + } +}; + +struct Cluster +{ + uint64_t mInstructions = 0; + BitSet mReadOnlyEntries; + BitSet mReadWriteEntries; + BitSet mTxIds; + size_t mBinId = 0; + + explicit Cluster(BuilderTx const& tx) : mInstructions(tx.mInstructions) + { + mReadOnlyEntries.inplaceUnion(tx.mReadOnlyFootprint); + mReadWriteEntries.inplaceUnion(tx.mReadWriteFootprint); + mTxIds.set(tx.mId); + } + + void + merge(Cluster const& other) + { + mInstructions += other.mInstructions; + mReadOnlyEntries.inplaceUnion(other.mReadOnlyEntries); + mReadWriteEntries.inplaceUnion(other.mReadWriteEntries); + mTxIds.inplaceUnion(other.mTxIds); + } +}; + +class Stage +{ + public: + Stage(ParallelPartitionConfig cfg) : mConfig(cfg) + { + mBinPacking.resize(mConfig.mThreadsPerStage); + mBinInstructions.resize(mConfig.mThreadsPerStage); + } + + bool + tryAdd(BuilderTx const& tx) + { + ZoneScoped; + if (mInstructions + tx.mInstructions > mConfig.instructionsPerStage()) + { + return false; + } + + auto conflictingClusters = getConflictingClusters(tx); + + bool packed = false; + auto newClusters = createNewClusters(tx, conflictingClusters, packed); + releaseAssert(!newClusters.empty()); + if (newClusters.back().mInstructions > mConfig.mInstructionsPerThread) + { + return false; + } + if (packed) + { + mClusters = newClusters; + mInstructions += tx.mInstructions; + return true; + } + + std::vector newBinInstructions; + auto newPacking = binPacking(newClusters, newBinInstructions); + if (newPacking.empty()) + { + return false; + } + mClusters = newClusters; + mBinPacking = newPacking; + mInstructions += tx.mInstructions; + mBinInstructions = newBinInstructions; + return true; + } + + void + visitAllTransactions(std::function visitor) const + { + for (auto const& cluster : mClusters) + { + size_t txId = 0; + while (cluster.mTxIds.nextSet(txId)) + { + visitor(cluster.mBinId, txId); + ++txId; + } + } + } + + private: + std::unordered_set + getConflictingClusters(BuilderTx const& tx) const + { + std::unordered_set conflictingClusters; + for (Cluster const& cluster : mClusters) + { + bool isConflicting = tx.mReadOnlyFootprint.intersectionCount( + cluster.mReadWriteEntries) > 0 || + tx.mReadWriteFootprint.intersectionCount( + cluster.mReadOnlyEntries) > 0 || + tx.mReadWriteFootprint.intersectionCount( + cluster.mReadWriteEntries) > 0; + if (isConflicting) + { + conflictingClusters.insert(&cluster); + } + } + return conflictingClusters; + } + + std::vector + createNewClusters(BuilderTx const& tx, + std::unordered_set const& txConflicts, + bool& packed) + { + std::vector newClusters; + newClusters.reserve(mClusters.size()); + for (auto const& cluster : mClusters) + { + if (txConflicts.find(&cluster) == txConflicts.end()) + { + newClusters.push_back(cluster); + } + } + + newClusters.emplace_back(tx); + for (auto const* cluster : txConflicts) + { + newClusters.back().merge(*cluster); + } + + if (newClusters.back().mInstructions > mConfig.mInstructionsPerThread) + { + return newClusters; + } + + for (auto const& cluster : txConflicts) + { + mBinInstructions[cluster->mBinId] -= cluster->mInstructions; + mBinPacking[cluster->mBinId].inplaceDifference(cluster->mTxIds); + } + + packed = false; + + for (size_t binId = 0; binId < mConfig.mThreadsPerStage; ++binId) + { + if (mBinInstructions[binId] + newClusters.back().mInstructions <= + mConfig.mInstructionsPerThread) + { + mBinInstructions[binId] += newClusters.back().mInstructions; + mBinPacking[binId].inplaceUnion(newClusters.back().mTxIds); + newClusters.back().mBinId = binId; + packed = true; + break; + } + } + if (!packed) + { + for (auto const& cluster : txConflicts) + { + mBinInstructions[cluster->mBinId] += cluster->mInstructions; + mBinPacking[cluster->mBinId].inplaceUnion(cluster->mTxIds); + } + } + return newClusters; + } + + std::vector + binPacking(std::vector& clusters, + std::vector& binInsns) const + { + std::sort(clusters.begin(), clusters.end(), + [](auto const& a, auto const& b) { + return a.mInstructions > b.mInstructions; + }); + size_t const binCount = mConfig.mThreadsPerStage; + std::vector bins(binCount); + binInsns.resize(binCount); + for (auto& cluster : clusters) + { + bool packed = false; + for (size_t i = 0; i < binCount; ++i) + { + if (binInsns[i] + cluster.mInstructions <= + mConfig.mInstructionsPerThread) + { + binInsns[i] += cluster.mInstructions; + bins[i].inplaceUnion(cluster.mTxIds); + cluster.mBinId = i; + packed = true; + break; + } + } + if (!packed) + { + return std::vector(); + } + } + return bins; + } + + std::vector mClusters; + std::vector mBinPacking; + std::vector mBinInstructions; + int64_t mInstructions = 0; + ParallelPartitionConfig mConfig; +}; + +} // namespace + +TxStageFrameList +buildSurgePricedParallelSorobanPhase( + TxFrameList const& txFrames, Config const& cfg, + SorobanNetworkConfig const& sorobanCfg, + std::shared_ptr laneConfig, + std::vector& hadTxNotFittingLane) +{ + ZoneScoped; + UnorderedMap entryIdMap; + + auto addToMap = [&entryIdMap](LedgerKey const& key) { + auto sz = entryIdMap.size(); + entryIdMap.emplace(key, sz); + }; + for (auto const& txFrame : txFrames) + { + auto const& footprint = txFrame->sorobanResources().footprint; + for (auto const& key : footprint.readOnly) + { + addToMap(key); + } + for (auto const& key : footprint.readWrite) + { + addToMap(key); + } + } + + std::unordered_map builderTxForTx; + for (size_t i = 0; i < txFrames.size(); ++i) + { + auto const& txFrame = txFrames[i]; + builderTxForTx.emplace(txFrame, BuilderTx(i, *txFrame, entryIdMap)); + } + + SurgePricingPriorityQueue queue( + /* isHighestPriority */ true, laneConfig, + stellar::rand_uniform(0, std::numeric_limits::max())); + for (auto const& tx : txFrames) + { + queue.add(tx); + } + + ParallelPartitionConfig partitionCfg(cfg, sorobanCfg); + std::vector stages(partitionCfg.mStageCount, partitionCfg); + + auto visitor = [&stages, + &builderTxForTx](TransactionFrameBaseConstPtr const& tx) { + bool added = false; + auto builderTxIt = builderTxForTx.find(tx); + releaseAssert(builderTxIt != builderTxForTx.end()); + for (auto& stage : stages) + { + if (stage.tryAdd(builderTxIt->second)) + { + added = true; + break; + } + } + if (added) + { + return SurgePricingPriorityQueue::VisitTxResult::PROCESSED; + } + return SurgePricingPriorityQueue::VisitTxResult::REJECTED; + }; + + std::vector laneLeftUntilLimit; + queue.popTopTxs(/* allowGaps */ true, visitor, laneLeftUntilLimit, + hadTxNotFittingLane); + releaseAssert(hadTxNotFittingLane.size() == 1); + + TxStageFrameList resStages; + resStages.reserve(stages.size()); + for (auto const& stage : stages) + { + auto& resStage = resStages.emplace_back(); + resStage.reserve(partitionCfg.mThreadsPerStage); + + std::unordered_map threadIdToStageThread; + + stage.visitAllTransactions([&resStage, &txFrames, + &threadIdToStageThread](size_t threadId, + size_t txId) { + auto it = threadIdToStageThread.find(threadId); + if (it == threadIdToStageThread.end()) + { + it = threadIdToStageThread.emplace(threadId, resStage.size()) + .first; + resStage.emplace_back(); + } + resStage[it->second].push_back(txFrames[txId]); + }); + for (auto const& thread : resStage) + { + releaseAssert(!thread.empty()); + } + } + while (!resStages.empty() && resStages.back().empty()) + { + resStages.pop_back(); + } + for (auto const& stage : resStages) + { + releaseAssert(!stage.empty()); + } + + return resStages; +} + +} // namespace stellar diff --git a/src/herder/ParallelTxSetBuilder.h b/src/herder/ParallelTxSetBuilder.h new file mode 100644 index 0000000000..e97d58767e --- /dev/null +++ b/src/herder/ParallelTxSetBuilder.h @@ -0,0 +1,21 @@ +#pragma once + +// Copyright 2024 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "herder/SurgePricingUtils.h" +#include "herder/TxSetFrame.h" +#include "ledger/NetworkConfig.h" +#include "main/Config.h" + +namespace stellar +{ + +TxStageFrameList buildSurgePricedParallelSorobanPhase( + TxFrameList const& txFrames, Config const& cfg, + SorobanNetworkConfig const& sorobanCfg, + std::shared_ptr laneConfig, + std::vector& hadTxNotFittingLane); + +} // namespace stellar diff --git a/src/herder/SurgePricingUtils.cpp b/src/herder/SurgePricingUtils.cpp index cc12e6e403..68c315bb29 100644 --- a/src/herder/SurgePricingUtils.cpp +++ b/src/herder/SurgePricingUtils.cpp @@ -319,6 +319,13 @@ SurgePricingPriorityQueue::popTopTxs( laneLeftUntilLimit[lane] -= res; } } + else if (visitRes == VisitTxResult::REJECTED) + { + // If a transaction hasn't been processed, then it is considered to + // be not fitting the lane. + hadTxNotFittingLane[GENERIC_LANE] = true; + hadTxNotFittingLane[lane] = true; + } erase(currIt); } } diff --git a/src/herder/SurgePricingUtils.h b/src/herder/SurgePricingUtils.h index 08473e43a8..c8c96d659c 100644 --- a/src/herder/SurgePricingUtils.h +++ b/src/herder/SurgePricingUtils.h @@ -133,6 +133,9 @@ class SurgePricingPriorityQueue // Transaction should be skipped and not counted towards the lane // limits. SKIPPED, + // Like `SKIPPED`, but marks the fact that the transaction didn't fit + // into the lane due to reasons beyond the lane's resource limit. + REJECTED, // Transaction has been processed and should be counted towards the // lane limits. PROCESSED @@ -184,6 +187,17 @@ class SurgePricingPriorityQueue std::vector>& txsToEvict) const; + // Generalized method for visiting and popping the top transactions in the + // queue until the lane limits are reached. + // This is a destructive method that removes all or most of the queue + // elements and thus should be used with care. + void popTopTxs( + bool allowGaps, + std::function const& + visitor, + std::vector& laneResourcesLeftUntilLimit, + std::vector& hadTxNotFittingLane); + private: class TxComparator { @@ -236,17 +250,6 @@ class SurgePricingPriorityQueue std::vector mutable mIters; }; - // Generalized method for visiting and popping the top transactions in the - // queue until the lane limits are reached. - // This is a destructive method that removes all or most of the queue - // elements and thus should be used with care. - void popTopTxs( - bool allowGaps, - std::function const& - visitor, - std::vector& laneResourcesLeftUntilLimit, - std::vector& hadTxNotFittingLane); - void erase(Iterator const& it); void erase(size_t lane, SurgePricingPriorityQueue::TxSortedSet::iterator iter); diff --git a/src/herder/TransactionQueue.cpp b/src/herder/TransactionQueue.cpp index d763262e3a..394de13485 100644 --- a/src/herder/TransactionQueue.cpp +++ b/src/herder/TransactionQueue.cpp @@ -937,11 +937,11 @@ TransactionQueue::isBanned(Hash const& hash) const }); } -TxSetTransactions +TxFrameList TransactionQueue::getTransactions(LedgerHeader const& lcl) const { ZoneScoped; - TxSetTransactions txs; + TxFrameList txs; uint32_t const nextLedgerSeq = lcl.ledgerSeq + 1; int64_t const startingSeq = getStartingSequenceNumber(nextLedgerSeq); diff --git a/src/herder/TransactionQueue.h b/src/herder/TransactionQueue.h index 86cd2c97fd..81b2409853 100644 --- a/src/herder/TransactionQueue.h +++ b/src/herder/TransactionQueue.h @@ -143,7 +143,7 @@ class TransactionQueue bool isBanned(Hash const& hash) const; TransactionFrameBaseConstPtr getTx(Hash const& hash) const; - TxSetTransactions getTransactions(LedgerHeader const& lcl) const; + TxFrameList getTransactions(LedgerHeader const& lcl) const; bool sourceAccountPending(AccountID const& accountID) const; virtual size_t getMaxQueueSizeOps() const = 0; diff --git a/src/herder/TxSetFrame.cpp b/src/herder/TxSetFrame.cpp index 73e705ff93..834eb90e8d 100644 --- a/src/herder/TxSetFrame.cpp +++ b/src/herder/TxSetFrame.cpp @@ -9,6 +9,7 @@ #include "crypto/Random.h" #include "crypto/SHA.h" #include "database/Database.h" +#include "herder/ParallelTxSetBuilder.h" #include "herder/SurgePricingUtils.h" #include "ledger/LedgerManager.h" #include "ledger/LedgerTxn.h" @@ -37,17 +38,111 @@ namespace stellar namespace { +std::string +getTxSetPhaseName(TxSetPhase phase) +{ + switch (phase) + { + case TxSetPhase::CLASSIC: + return "classic"; + case TxSetPhase::SOROBAN: + return "soroban"; + default: + throw std::runtime_error("Unknown phase"); + } +} + +bool +validateSequentialPhaseXDRStructure(TransactionPhase const& phase) +{ + bool componentsNormalized = + std::is_sorted(phase.v0Components().begin(), phase.v0Components().end(), + [](auto const& c1, auto const& c2) { + if (!c1.txsMaybeDiscountedFee().baseFee || + !c2.txsMaybeDiscountedFee().baseFee) + { + return !c1.txsMaybeDiscountedFee().baseFee && + c2.txsMaybeDiscountedFee().baseFee; + } + return *c1.txsMaybeDiscountedFee().baseFee < + *c2.txsMaybeDiscountedFee().baseFee; + }); + if (!componentsNormalized) + { + CLOG_DEBUG(Herder, "Got bad txSet: incorrect component order"); + return false; + } + + bool componentBaseFeesUnique = + std::adjacent_find(phase.v0Components().begin(), + phase.v0Components().end(), + [](auto const& c1, auto const& c2) { + if (!c1.txsMaybeDiscountedFee().baseFee || + !c2.txsMaybeDiscountedFee().baseFee) + { + return !c1.txsMaybeDiscountedFee().baseFee && + !c2.txsMaybeDiscountedFee().baseFee; + } + return *c1.txsMaybeDiscountedFee().baseFee == + *c2.txsMaybeDiscountedFee().baseFee; + }) == phase.v0Components().end(); + if (!componentBaseFeesUnique) + { + CLOG_DEBUG(Herder, "Got bad txSet: duplicate component base fees"); + return false; + } + for (auto const& component : phase.v0Components()) + { + if (component.txsMaybeDiscountedFee().txs.empty()) + { + CLOG_DEBUG(Herder, "Got bad txSet: empty component"); + return false; + } + } + return true; +} + +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +bool +validateParallelComponent(ParallelTxsComponent const& component) +{ + for (auto const& stage : component.executionStages) + { + if (stage.empty()) + { + CLOG_DEBUG(Herder, "Got bad txSet: empty stage"); + return false; + } + for (auto const& thread : stage) + { + if (thread.empty()) + { + CLOG_DEBUG(Herder, "Got bad txSet: empty thread"); + return false; + } + } + } + return true; +} +#endif + bool validateTxSetXDRStructure(GeneralizedTransactionSet const& txSet) { +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + int const MAX_PHASE = 1; +#else + int const MAX_PHASE = 0; +#endif if (txSet.v() != 1) { CLOG_DEBUG(Herder, "Got bad txSet: unsupported version {}", txSet.v()); return false; } + auto phaseCount = static_cast(TxSetPhase::PHASE_COUNT); auto const& txSetV1 = txSet.v1TxSet(); // There was no protocol with 1 phase, so checking for 2 phases only - if (txSetV1.phases.size() != static_cast(TxSetPhase::PHASE_COUNT)) + if (txSetV1.phases.size() != phaseCount) { CLOG_DEBUG(Herder, "Got bad txSet: exactly 2 phases are expected, got {}", @@ -55,62 +150,42 @@ validateTxSetXDRStructure(GeneralizedTransactionSet const& txSet) return false; } - for (auto const& phase : txSetV1.phases) + for (size_t phaseId = 0; phaseId < phaseCount; ++phaseId) { - if (phase.v() != 0) + auto const& phase = txSetV1.phases[phaseId]; + if (phase.v() > MAX_PHASE) { CLOG_DEBUG(Herder, "Got bad txSet: unsupported phase version {}", phase.v()); return false; } - - bool componentsNormalized = std::is_sorted( - phase.v0Components().begin(), phase.v0Components().end(), - [](auto const& c1, auto const& c2) { - if (!c1.txsMaybeDiscountedFee().baseFee || - !c2.txsMaybeDiscountedFee().baseFee) - { - return !c1.txsMaybeDiscountedFee().baseFee && - c2.txsMaybeDiscountedFee().baseFee; - } - return *c1.txsMaybeDiscountedFee().baseFee < - *c2.txsMaybeDiscountedFee().baseFee; - }); - if (!componentsNormalized) - { - CLOG_DEBUG(Herder, "Got bad txSet: incorrect component order"); - return false; - } - - bool componentBaseFeesUnique = - std::adjacent_find( - phase.v0Components().begin(), phase.v0Components().end(), - [](auto const& c1, auto const& c2) { - if (!c1.txsMaybeDiscountedFee().baseFee || - !c2.txsMaybeDiscountedFee().baseFee) - { - return !c1.txsMaybeDiscountedFee().baseFee && - !c2.txsMaybeDiscountedFee().baseFee; - } - return *c1.txsMaybeDiscountedFee().baseFee == - *c2.txsMaybeDiscountedFee().baseFee; - }) == phase.v0Components().end(); - if (!componentBaseFeesUnique) +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + if (phase.v() == 1) { - CLOG_DEBUG(Herder, "Got bad txSet: duplicate component base fees"); - return false; + if (phaseId != static_cast(TxSetPhase::SOROBAN)) + { + CLOG_DEBUG(Herder, + "Got bad txSet: non-Soroban parallel phase {}", + phase.v()); + return false; + } + if (!validateParallelComponent(phase.parallelTxsComponent())) + { + return false; + } } - for (auto const& component : phase.v0Components()) + else +#endif { - if (component.txsMaybeDiscountedFee().txs.empty()) + if (!validateSequentialPhaseXDRStructure(phase)) { - CLOG_DEBUG(Herder, "Got bad txSet: empty component"); return false; } } } return true; } + // We want to XOR the tx hash with the set hash. // This way people can't predict the order that txs will be applied in struct ApplyTxSorter @@ -124,14 +199,14 @@ struct ApplyTxSorter operator()(TransactionFrameBasePtr const& tx1, TransactionFrameBasePtr const& tx2) const { - // need to use the hash of whole tx here since multiple txs could have - // the same Contents + // need to use the hash of whole tx here since multiple txs could + // have the same Contents return lessThanXored(tx1->getFullHash(), tx2->getFullHash(), mSetHash); } }; Hash -computeNonGenericTxSetContentsHash(TransactionSet const& xdrTxSet) +computeNonGeneralizedTxSetContentsHash(TransactionSet const& xdrTxSet) { ZoneScoped; SHA256 hasher; @@ -143,8 +218,8 @@ computeNonGenericTxSetContentsHash(TransactionSet const& xdrTxSet) return hasher.finish(); } -// Note: Soroban txs also use this functionality for simplicity, as it's a no-op -// (all Soroban txs have 1 op max) +// Note: Soroban txs also use this functionality for simplicity, as it's a +// no-op (all Soroban txs have 1 op max) int64_t computePerOpFee(TransactionFrameBase const& tx, uint32_t ledgerVersion) { @@ -158,7 +233,7 @@ computePerOpFee(TransactionFrameBase const& tx, uint32_t ledgerVersion) } void -transactionsToTransactionSetXDR(TxSetTransactions const& txs, +transactionsToTransactionSetXDR(TxFrameList const& txs, Hash const& previousLedgerHash, TransactionSet& txSet) { @@ -172,159 +247,536 @@ transactionsToTransactionSetXDR(TxSetTransactions const& txs, txSet.previousLedgerHash = previousLedgerHash; } +void +sequentialPhaseToXdr(TxFrameList const& txs, + InclusionFeeMap const& inclusionFeeMap, + TransactionPhase& xdrPhase) +{ + xdrPhase.v(0); + + std::map, size_t> feeTxCount; + for (auto const& [_, fee] : inclusionFeeMap) + { + ++feeTxCount[fee]; + } + auto& components = xdrPhase.v0Components(); + // Reserve a component per unique base fee in order to have the correct + // pointers in componentPerBid map. + components.reserve(feeTxCount.size()); + + std::map, xdr::xvector*> + componentPerBid; + for (auto const& [fee, txCount] : feeTxCount) + { + components.emplace_back(TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); + auto& discountedFeeComponent = + components.back().txsMaybeDiscountedFee(); + if (fee) + { + discountedFeeComponent.baseFee.activate() = *fee; + } + componentPerBid[fee] = &discountedFeeComponent.txs; + componentPerBid[fee]->reserve(txCount); + } + auto sortedTxs = TxSetUtils::sortTxsInHashOrder(txs); + for (auto const& tx : sortedTxs) + { + componentPerBid[inclusionFeeMap.find(tx)->second]->push_back( + tx->getEnvelope()); + } +} + +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +void +parallelPhaseToXdr(TxStageFrameList const& txs, + InclusionFeeMap const& inclusionFeeMap, + TransactionPhase& xdrPhase) +{ + xdrPhase.v(1); + + std::optional baseFee; + if (!inclusionFeeMap.empty()) + { + baseFee = inclusionFeeMap.begin()->second; + } + // We currently don't support multi-component parallel perPhaseTxs, so make + // sure all txs have the same base fee. + for (auto const& [_, fee] : inclusionFeeMap) + { + releaseAssert(fee == baseFee); + } + auto& component = xdrPhase.parallelTxsComponent(); + if (baseFee) + { + component.baseFee.activate() = *baseFee; + } + component.executionStages.reserve(txs.size()); + auto sortedTxs = TxSetUtils::sortParallelTxsInHashOrder(txs); + for (auto const& stage : sortedTxs) + { + auto& xdrStage = component.executionStages.emplace_back(); + xdrStage.reserve(stage.size()); + for (auto const& thread : stage) + { + auto& xdrThread = xdrStage.emplace_back(); + xdrThread.reserve(thread.size()); + for (auto const& tx : thread) + { + xdrThread.push_back(tx->getEnvelope()); + } + } + } +} + +#endif + void transactionsToGeneralizedTransactionSetXDR( - TxSetPhaseTransactions const& phaseTxs, - std::vector>> const& - phaseInclusionFeeMap, - Hash const& previousLedgerHash, GeneralizedTransactionSet& generalizedTxSet) + std::vector const& phases, Hash const& previousLedgerHash, + GeneralizedTransactionSet& generalizedTxSet) { ZoneScoped; - releaseAssert(phaseTxs.size() == phaseInclusionFeeMap.size()); generalizedTxSet.v(1); generalizedTxSet.v1TxSet().previousLedgerHash = previousLedgerHash; - - for (int i = 0; i < phaseTxs.size(); ++i) + generalizedTxSet.v1TxSet().phases.resize(phases.size()); + for (int i = 0; i < phases.size(); ++i) { - auto const& txPhase = phaseTxs[i]; - auto& phase = - generalizedTxSet.v1TxSet().phases.emplace_back().v0Components(); + auto const& txPhase = phases[i]; + txPhase.toXDR(generalizedTxSet.v1TxSet().phases[i]); + } +} - auto const& feeMap = phaseInclusionFeeMap[i]; - std::map, size_t> feeTxCount; - for (auto const& [tx, fee] : feeMap) - { - ++feeTxCount[fee]; - } - // Reserve a component per unique base fee in order to have the correct - // pointers in componentPerBid map. - phase.reserve(feeTxCount.size()); +TxFrameList +sortedForApplySequential(TxFrameList const& txs, Hash const& txSetHash) +{ + TxFrameList retList; + retList.reserve(txs.size()); - std::map, xdr::xvector*> - componentPerBid; - for (auto const& [fee, txCount] : feeTxCount) + auto txQueues = TxSetUtils::buildAccountTxQueues(txs); + + // build txBatches + // txBatches i-th element contains each i-th transaction for + // accounts with a transaction in the transaction set + std::vector> txBatches; + + while (!txQueues.empty()) + { + txBatches.emplace_back(); + auto& curBatch = txBatches.back(); + // go over all users that still have transactions + for (auto it = txQueues.begin(); it != txQueues.end();) { - phase.emplace_back(TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - auto& discountedFeeComponent = phase.back().txsMaybeDiscountedFee(); - if (fee) + auto& txQueue = *it; + curBatch.emplace_back(txQueue->getTopTx()); + txQueue->popTopTx(); + if (txQueue->empty()) { - discountedFeeComponent.baseFee.activate() = *fee; + // done with that user + it = txQueues.erase(it); + } + else + { + ++it; } - componentPerBid[fee] = &discountedFeeComponent.txs; - componentPerBid[fee]->reserve(txCount); } - auto sortedTxs = TxSetUtils::sortTxsInHashOrder(txPhase); - for (auto const& tx : sortedTxs) + } + + for (auto& batch : txBatches) + { + // randomize each batch using the hash of the transaction set + // as a way to randomize even more + ApplyTxSorter s(txSetHash); + std::sort(batch.begin(), batch.end(), s); + for (auto const& tx : batch) { - componentPerBid[feeMap.find(tx)->second]->push_back( - tx->getEnvelope()); + retList.push_back(tx); } } + + return retList; } -// This assumes that the phase validation has already been done, -// specifically that there are no transactions that belong to the same -// source account, and that the ledger sequence corresponds to the -bool -phaseTxsAreValid(TxSetTransactions const& phase, Application& app, - uint64_t lowerBoundCloseTimeOffset, - uint64_t upperBoundCloseTimeOffset) +TxStageFrameList +sortedForApplyParallel(TxStageFrameList const& stages, Hash const& txSetHash) { ZoneScoped; - // This is done so minSeqLedgerGap is validated against the next - // ledgerSeq, which is what will be used at apply time - - // Grab read-only latest ledger state; This is only used to validate tx sets - // for LCL+1 - LedgerSnapshot ls(app); - ls.getLedgerHeader().currentToModify().ledgerSeq = - app.getLedgerManager().getLastClosedLedgerNum() + 1; - for (auto const& tx : phase) + TxStageFrameList sortedStages = stages; + ApplyTxSorter sorter(txSetHash); + for (auto& stage : sortedStages) { - auto txResult = tx->checkValid(app, ls, 0, lowerBoundCloseTimeOffset, - upperBoundCloseTimeOffset); - if (!txResult->isSuccess()) + for (auto& thread : stage) { + std::sort(thread.begin(), thread.end(), sorter); + } + // There is no need to shuffle threads in the stage, as they are + // independent, so the apply order doesn't matter even if the threads + // are being applied sequentially. + } + std::sort(sortedStages.begin(), sortedStages.end(), + [&sorter](auto const& a, auto const& b) { + releaseAssert(!a.empty() && !b.empty()); + releaseAssert(!a.front().empty() && !b.front().empty()); + return sorter(a.front().front(), b.front().front()); + }); + return stages; +} - CLOG_DEBUG( - Herder, "Got bad txSet: tx invalid tx: {} result: {}", - xdrToCerealString(tx->getEnvelope(), "TransactionEnvelope"), - txResult->getResultCode()); +bool +addWireTxsToList(Hash const& networkID, + xdr::xvector const& xdrTxs, + TxFrameList& txList) +{ + auto prevSize = txList.size(); + txList.reserve(prevSize + xdrTxs.size()); + for (auto const& env : xdrTxs) + { + auto tx = TransactionFrameBase::makeTransactionFromWire(networkID, env); + if (!tx->XDRProvidesValidFee()) + { return false; } + txList.push_back(tx); + } + if (!std::is_sorted(txList.begin() + prevSize, txList.end(), + &TxSetUtils::hashTxSorter)) + { + return false; } return true; } -} // namespace -TxSetXDRFrame::TxSetXDRFrame(TransactionSet const& xdrTxSet) - : mXDRTxSet(xdrTxSet) - , mEncodedSize(xdr::xdr_argpack_size(xdrTxSet)) - , mHash(computeNonGenericTxSetContentsHash(xdrTxSet)) +std::vector +computeLaneBaseFee(TxSetPhase phase, LedgerHeader const& ledgerHeader, + SurgePricingLaneConfig const& surgePricingConfig, + std::vector const& lowestLaneFee, + std::vector const& hadTxNotFittingLane) { + std::vector laneBaseFee(lowestLaneFee.size(), + ledgerHeader.baseFee); + auto minBaseFee = + *std::min_element(lowestLaneFee.begin(), lowestLaneFee.end()); + for (size_t lane = 0; lane < laneBaseFee.size(); ++lane) + { + // If generic lane is full, then any transaction had to compete with not + // included transactions and independently of the lane they need to have + // at least the minimum fee in the tx set applied. + if (hadTxNotFittingLane[SurgePricingPriorityQueue::GENERIC_LANE]) + { + laneBaseFee[lane] = minBaseFee; + } + // If limited lane is full, then the transactions in this lane also had + // to compete with each other and have a base fee associated with this + // lane only. + if (lane != SurgePricingPriorityQueue::GENERIC_LANE && + hadTxNotFittingLane[lane]) + { + laneBaseFee[lane] = lowestLaneFee[lane]; + } + if (laneBaseFee[lane] > ledgerHeader.baseFee) + { + CLOG_WARNING( + Herder, + "{} phase: surge pricing for '{}' lane is in effect with base " + "fee={}, baseFee={}", + getTxSetPhaseName(phase), + lane == SurgePricingPriorityQueue::GENERIC_LANE ? "generic" + : "DEX", + laneBaseFee[lane], ledgerHeader.baseFee); + } + } + return laneBaseFee; } -TxSetXDRFrame::TxSetXDRFrame(GeneralizedTransactionSet const& xdrTxSet) - : mXDRTxSet(xdrTxSet) - , mEncodedSize(xdr::xdr_argpack_size(xdrTxSet)) - , mHash(xdrSha256(xdrTxSet)) +std::shared_ptr +createSurgePricingLangeConfig(TxSetPhase phase, Application& app) { -} + std::shared_ptr surgePricingLaneConfig; + if (phase == TxSetPhase::CLASSIC) + { + auto maxOps = + Resource({static_cast( + app.getLedgerManager().getLastMaxTxSetSizeOps()), + MAX_CLASSIC_BYTE_ALLOWANCE}); + std::optional dexOpsLimit; + if (app.getConfig().MAX_DEX_TX_OPERATIONS_IN_TX_SET) + { + // DEX operations limit implies that DEX transactions should + // compete with each other in in a separate fee lane, which + // is only possible with generalized tx set. + dexOpsLimit = + Resource({*app.getConfig().MAX_DEX_TX_OPERATIONS_IN_TX_SET, + MAX_CLASSIC_BYTE_ALLOWANCE}); + } -TxSetXDRFrameConstPtr -TxSetXDRFrame::makeFromWire(TransactionSet const& xdrTxSet) -{ - ZoneScoped; - std::shared_ptr txSet(new TxSetXDRFrame(xdrTxSet)); - return txSet; + surgePricingLaneConfig = + std::make_shared(maxOps, dexOpsLimit); + } + else + { + releaseAssert(phase == TxSetPhase::SOROBAN); + + auto limits = app.getLedgerManager().maxLedgerResources( + /* isSoroban */ true); + // When building Soroban tx sets with parallel execution support, + // instructions are accounted for by the build logic, not by the surge + // pricing config, so we need to relax the instruction limit in surge + // pricing logic. + if (protocolVersionStartsFrom(app.getLedgerManager() + .getLastClosedLedgerHeader() + .header.ledgerVersion, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) + { + limits.setVal(Resource::Type::INSTRUCTIONS, + std::numeric_limits::max()); + } + + auto byteLimit = + std::min(static_cast(MAX_SOROBAN_BYTE_ALLOWANCE), + limits.getVal(Resource::Type::TX_BYTE_SIZE)); + limits.setVal(Resource::Type::TX_BYTE_SIZE, byteLimit); + + surgePricingLaneConfig = + std::make_shared(limits); + } + return surgePricingLaneConfig; } -TxSetXDRFrameConstPtr -TxSetXDRFrame::makeFromWire(GeneralizedTransactionSet const& xdrTxSet) +TxFrameList +buildSurgePricedSequentialPhase( + TxFrameList const& txs, + std::shared_ptr surgePricingLaneConfig, + std::vector& hadTxNotFittingLane) { ZoneScoped; - std::shared_ptr txSet(new TxSetXDRFrame(xdrTxSet)); - return txSet; + return SurgePricingPriorityQueue::getMostTopTxsWithinLimits( + txs, surgePricingLaneConfig, hadTxNotFittingLane); } -TxSetXDRFrameConstPtr -TxSetXDRFrame::makeFromStoredTxSet(StoredTransactionSet const& storedSet) +std::pair, + std::shared_ptr> +applySurgePricing(TxSetPhase phase, TxFrameList const& txs, Application& app) { - if (storedSet.v() == 0) + ZoneScoped; + auto surgePricingLaneConfig = createSurgePricingLangeConfig(phase, app); + std::vector hadTxNotFittingLane; + bool isParallelSoroban = + phase == TxSetPhase::SOROBAN && + protocolVersionStartsFrom(app.getLedgerManager() + .getLastClosedLedgerHeader() + .header.ledgerVersion, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION); + std::variant includedTxs; + if (isParallelSoroban) { - return TxSetXDRFrame::makeFromWire(storedSet.txSet()); + includedTxs = buildSurgePricedParallelSorobanPhase( + txs, app.getConfig(), + app.getLedgerManager().getSorobanNetworkConfig(), + surgePricingLaneConfig, hadTxNotFittingLane); } - return TxSetXDRFrame::makeFromWire(storedSet.generalizedTxSet()); + else + { + includedTxs = buildSurgePricedSequentialPhase( + txs, surgePricingLaneConfig, hadTxNotFittingLane); + } + + auto visitIncludedTxs = + [&includedTxs]( + std::function visitor) { + std::visit( + [&visitor](auto const& txs) { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + for (auto const& tx : txs) + { + visitor(tx); + } + } + else if constexpr (std::is_same_v) + { + for (auto const& stage : txs) + { + for (auto const& thread : stage) + { + for (auto const& tx : thread) + { + visitor(tx); + } + } + } + } + else + { + releaseAssert(false); + } + }, + includedTxs); + }; + + std::vector lowestLaneFee; + auto const& lclHeader = + app.getLedgerManager().getLastClosedLedgerHeader().header; + + size_t laneCount = surgePricingLaneConfig->getLaneLimits().size(); + lowestLaneFee.resize(laneCount, std::numeric_limits::max()); + visitIncludedTxs( + [&lowestLaneFee, &surgePricingLaneConfig, &lclHeader](auto const& tx) { + size_t lane = surgePricingLaneConfig->getLane(*tx); + auto perOpFee = computePerOpFee(*tx, lclHeader.ledgerVersion); + lowestLaneFee[lane] = std::min(lowestLaneFee[lane], perOpFee); + }); + auto laneBaseFee = + computeLaneBaseFee(phase, lclHeader, *surgePricingLaneConfig, + lowestLaneFee, hadTxNotFittingLane); + auto inclusionFeeMapPtr = std::make_shared(); + auto& inclusionFeeMap = *inclusionFeeMapPtr; + visitIncludedTxs([&inclusionFeeMap, &laneBaseFee, + &surgePricingLaneConfig](auto const& tx) { + inclusionFeeMap[tx] = laneBaseFee[surgePricingLaneConfig->getLane(*tx)]; + }); + + return std::make_pair(includedTxs, inclusionFeeMapPtr); } -std::pair -makeTxSetFromTransactions(TxSetPhaseTransactions const& txPhases, - Application& app, uint64_t lowerBoundCloseTimeOffset, - uint64_t upperBoundCloseTimeOffset -#ifdef BUILD_TESTS - , - bool skipValidation -#endif -) +size_t +countOps(TxFrameList const& txs) { - TxSetPhaseTransactions invalidTxs; - invalidTxs.resize(txPhases.size()); - return makeTxSetFromTransactions(txPhases, app, lowerBoundCloseTimeOffset, - upperBoundCloseTimeOffset, invalidTxs -#ifdef BUILD_TESTS - , - skipValidation -#endif - ); + return std::accumulate(txs.begin(), txs.end(), size_t(0), + [&](size_t a, TransactionFrameBasePtr const& tx) { + return a + tx->getNumOperations(); + }); +} + +int64_t +computeBaseFeeForLegacyTxSet(LedgerHeader const& lclHeader, + TxFrameList const& txs) +{ + ZoneScoped; + auto ledgerVersion = lclHeader.ledgerVersion; + int64_t lowestBaseFee = std::numeric_limits::max(); + for (auto const& tx : txs) + { + int64_t txBaseFee = computePerOpFee(*tx, ledgerVersion); + lowestBaseFee = std::min(lowestBaseFee, txBaseFee); + } + int64_t baseFee = lclHeader.baseFee; + + if (protocolVersionStartsFrom(ledgerVersion, ProtocolVersion::V_11)) + { + size_t surgeOpsCutoff = 0; + if (lclHeader.maxTxSetSize >= MAX_OPS_PER_TX) + { + surgeOpsCutoff = lclHeader.maxTxSetSize - MAX_OPS_PER_TX; + } + if (countOps(txs) > surgeOpsCutoff) + { + baseFee = lowestBaseFee; + } + } + return baseFee; +} + +bool +checkFeeMap(InclusionFeeMap const& feeMap, LedgerHeader const& lclHeader) +{ + for (auto const& [tx, fee] : feeMap) + { + if (!fee) + { + continue; + } + if (*fee < lclHeader.baseFee) + { + + CLOG_DEBUG(Herder, + "Got bad txSet: {} has too low component " + "base fee {}", + hexAbbrev(lclHeader.previousLedgerHash), *fee); + return false; + } + if (tx->getInclusionFee() < getMinInclusionFee(*tx, lclHeader, fee)) + { + CLOG_DEBUG(Herder, + "Got bad txSet: {} has tx with fee bid ({}) lower " + "than base fee ({})", + hexAbbrev(lclHeader.previousLedgerHash), + tx->getInclusionFee(), + getMinInclusionFee(*tx, lclHeader, fee)); + return false; + } + } + return true; +} + +} // namespace + +TxSetXDRFrame::TxSetXDRFrame(TransactionSet const& xdrTxSet) + : mXDRTxSet(xdrTxSet) + , mEncodedSize(xdr::xdr_argpack_size(xdrTxSet)) + , mHash(computeNonGeneralizedTxSetContentsHash(xdrTxSet)) +{ +} + +TxSetXDRFrame::TxSetXDRFrame(GeneralizedTransactionSet const& xdrTxSet) + : mXDRTxSet(xdrTxSet) + , mEncodedSize(xdr::xdr_argpack_size(xdrTxSet)) + , mHash(xdrSha256(xdrTxSet)) +{ +} + +TxSetXDRFrameConstPtr +TxSetXDRFrame::makeFromWire(TransactionSet const& xdrTxSet) +{ + ZoneScoped; + std::shared_ptr txSet(new TxSetXDRFrame(xdrTxSet)); + return txSet; +} + +TxSetXDRFrameConstPtr +TxSetXDRFrame::makeFromWire(GeneralizedTransactionSet const& xdrTxSet) +{ + ZoneScoped; + std::shared_ptr txSet(new TxSetXDRFrame(xdrTxSet)); + return txSet; +} + +TxSetXDRFrameConstPtr +TxSetXDRFrame::makeFromStoredTxSet(StoredTransactionSet const& storedSet) +{ + if (storedSet.v() == 0) + { + return TxSetXDRFrame::makeFromWire(storedSet.txSet()); + } + return TxSetXDRFrame::makeFromWire(storedSet.generalizedTxSet()); +} + +std::pair +makeTxSetFromTransactions(PerPhaseTransactionList const& txPhases, + Application& app, uint64_t lowerBoundCloseTimeOffset, + uint64_t upperBoundCloseTimeOffset +#ifdef BUILD_TESTS + , + bool skipValidation +#endif +) +{ + PerPhaseTransactionList invalidTxs; + invalidTxs.resize(txPhases.size()); + return makeTxSetFromTransactions(txPhases, app, lowerBoundCloseTimeOffset, + upperBoundCloseTimeOffset, invalidTxs +#ifdef BUILD_TESTS + , + skipValidation +#endif + ); } std::pair -makeTxSetFromTransactions(TxSetPhaseTransactions const& txPhases, +makeTxSetFromTransactions(PerPhaseTransactionList const& txPhases, Application& app, uint64_t lowerBoundCloseTimeOffset, uint64_t upperBoundCloseTimeOffset, - TxSetPhaseTransactions& invalidTxs + PerPhaseTransactionList& invalidTxs #ifdef BUILD_TESTS , bool skipValidation @@ -335,12 +787,12 @@ makeTxSetFromTransactions(TxSetPhaseTransactions const& txPhases, releaseAssert(txPhases.size() <= static_cast(TxSetPhase::PHASE_COUNT)); - TxSetPhaseTransactions validatedPhases; - for (int i = 0; i < txPhases.size(); ++i) + std::vector validatedPhases; + for (size_t i = 0; i < txPhases.size(); ++i) { - auto& txs = txPhases[i]; + auto const& phaseTxs = txPhases[i]; bool expectSoroban = static_cast(i) == TxSetPhase::SOROBAN; - if (!std::all_of(txs.begin(), txs.end(), [&](auto const& tx) { + if (!std::all_of(phaseTxs.begin(), phaseTxs.end(), [&](auto const& tx) { return tx->isSoroban() == expectSoroban; })) { @@ -349,20 +801,44 @@ makeTxSetFromTransactions(TxSetPhaseTransactions const& txPhases, } auto& invalid = invalidTxs[i]; + TxFrameList validatedTxs; #ifdef BUILD_TESTS if (skipValidation) { - validatedPhases.emplace_back(txs); + validatedTxs = phaseTxs; } else { #endif - validatedPhases.emplace_back( - TxSetUtils::trimInvalid(txs, app, lowerBoundCloseTimeOffset, - upperBoundCloseTimeOffset, invalid)); + validatedTxs = TxSetUtils::trimInvalid( + phaseTxs, app, lowerBoundCloseTimeOffset, + upperBoundCloseTimeOffset, invalid); #ifdef BUILD_TESTS } #endif + auto phaseType = static_cast(i); + auto [includedTxs, inclusionFeeMap] = + applySurgePricing(phaseType, validatedTxs, app); + + std::visit( + [&validatedPhases, phaseType, inclusionFeeMap](auto&& txs) { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + validatedPhases.emplace_back( + TxSetPhaseFrame(phaseType, txs, inclusionFeeMap)); + } + else if constexpr (std::is_same_v) + { + validatedPhases.emplace_back(TxSetPhaseFrame( + phaseType, std::move(txs), inclusionFeeMap)); + } + else + { + releaseAssert(false); + } + }, + includedTxs); } auto const& lclHeader = app.getLedgerManager().getLastClosedLedgerHeader(); @@ -371,7 +847,7 @@ makeTxSetFromTransactions(TxSetPhaseTransactions const& txPhases, std::unique_ptr preliminaryApplicableTxSet( new ApplicableTxSetFrame(app, lclHeader, validatedPhases, std::nullopt)); - preliminaryApplicableTxSet->applySurgePricing(app); + // Do the roundtrip through XDR to ensure we never build an incorrect tx set // for nomination. auto outputTxSet = preliminaryApplicableTxSet->toWireTxSetFrame(); @@ -402,7 +878,7 @@ makeTxSetFromTransactions(TxSetPhaseTransactions const& txPhases, outputApplicableTxSet->numPhases(); if (valid) { - for (int i = 0; i < preliminaryApplicableTxSet->numPhases(); ++i) + for (size_t i = 0; i < preliminaryApplicableTxSet->numPhases(); ++i) { valid = valid && preliminaryApplicableTxSet->sizeTx( static_cast(i)) == @@ -416,6 +892,8 @@ makeTxSetFromTransactions(TxSetPhaseTransactions const& txPhases, upperBoundCloseTimeOffset); if (!valid) { + outputApplicableTxSet->checkValid(app, lowerBoundCloseTimeOffset, + upperBoundCloseTimeOffset); throw std::runtime_error("Created invalid tx set frame"); } @@ -428,14 +906,19 @@ TxSetXDRFrame::makeEmpty(LedgerHeaderHistoryEntry const& lclHeader) if (protocolVersionStartsFrom(lclHeader.header.ledgerVersion, SOROBAN_PROTOCOL_VERSION)) { - TxSetPhaseTransactions emptyPhases( - static_cast(TxSetPhase::PHASE_COUNT)); - std::vector>> - emptyFeeMap(static_cast(TxSetPhase::PHASE_COUNT)); + bool isParallelSoroban = false; +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + isParallelSoroban = + protocolVersionStartsFrom(lclHeader.header.ledgerVersion, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION); +#endif + std::vector emptyPhases = { + TxSetPhaseFrame::makeEmpty(TxSetPhase::CLASSIC, false), + TxSetPhaseFrame::makeEmpty(TxSetPhase::SOROBAN, isParallelSoroban)}; + GeneralizedTransactionSet txSet; - transactionsToGeneralizedTransactionSetXDR(emptyPhases, emptyFeeMap, - lclHeader.hash, txSet); + transactionsToGeneralizedTransactionSetXDR(emptyPhases, lclHeader.hash, + txSet); return TxSetXDRFrame::makeFromWire(txSet); } TransactionSet txSet; @@ -445,7 +928,7 @@ TxSetXDRFrame::makeEmpty(LedgerHeaderHistoryEntry const& lclHeader) TxSetXDRFrameConstPtr TxSetXDRFrame::makeFromHistoryTransactions(Hash const& previousLedgerHash, - TxSetTransactions const& txs) + TxFrameList const& txs) { TransactionSet txSet; transactionsToTransactionSetXDR(txs, previousLedgerHash, txSet); @@ -454,49 +937,58 @@ TxSetXDRFrame::makeFromHistoryTransactions(Hash const& previousLedgerHash, #ifdef BUILD_TESTS std::pair -makeTxSetFromTransactions(TxSetTransactions txs, Application& app, +makeTxSetFromTransactions(TxFrameList txs, Application& app, uint64_t lowerBoundCloseTimeOffset, uint64_t upperBoundCloseTimeOffset, bool enforceTxsApplyOrder) { - TxSetTransactions invalid; + TxFrameList invalid; return makeTxSetFromTransactions(txs, app, lowerBoundCloseTimeOffset, upperBoundCloseTimeOffset, invalid, enforceTxsApplyOrder); } std::pair -makeTxSetFromTransactions(TxSetTransactions txs, Application& app, +makeTxSetFromTransactions(TxFrameList txs, Application& app, uint64_t lowerBoundCloseTimeOffset, uint64_t upperBoundCloseTimeOffset, - TxSetTransactions& invalidTxs, - bool enforceTxsApplyOrder) + TxFrameList& invalidTxs, bool enforceTxsApplyOrder) { auto lclHeader = app.getLedgerManager().getLastClosedLedgerHeader(); - TxSetPhaseTransactions phases; - phases.resize(protocolVersionStartsFrom(lclHeader.header.ledgerVersion, - SOROBAN_PROTOCOL_VERSION) - ? 2 - : 1); + PerPhaseTransactionList perPhaseTxs; + perPhaseTxs.resize(protocolVersionStartsFrom(lclHeader.header.ledgerVersion, + SOROBAN_PROTOCOL_VERSION) + ? 2 + : 1); for (auto& tx : txs) { if (tx->isSoroban()) { - phases[static_cast(TxSetPhase::SOROBAN)].push_back(tx); + perPhaseTxs[static_cast(TxSetPhase::SOROBAN)].push_back(tx); } else { - phases[static_cast(TxSetPhase::CLASSIC)].push_back(tx); + perPhaseTxs[static_cast(TxSetPhase::CLASSIC)].push_back(tx); } } - TxSetPhaseTransactions invalid; - invalid.resize(phases.size()); - auto res = makeTxSetFromTransactions(phases, app, lowerBoundCloseTimeOffset, - upperBoundCloseTimeOffset, invalid, - enforceTxsApplyOrder); + PerPhaseTransactionList invalid; + invalid.resize(perPhaseTxs.size()); + auto res = makeTxSetFromTransactions( + perPhaseTxs, app, lowerBoundCloseTimeOffset, upperBoundCloseTimeOffset, + invalid, enforceTxsApplyOrder); if (enforceTxsApplyOrder) { - res.second->mApplyOrderOverride = txs; + auto const& resPhases = res.second->getPhases(); + // This only supports sequential tx sets for now. + std::vector overridePhases; + for (size_t i = 0; i < resPhases.size(); ++i) + { + overridePhases.emplace_back(TxSetPhaseFrame( + static_cast(i), std::move(perPhaseTxs[i]), + std::make_shared( + resPhases[i].getInclusionFeeMap()))); + } + res.second->mApplyOrderPhases = overridePhases; res.first->mApplicableTxSetOverride = std::move(res.second); } invalidTxs = invalid[0]; @@ -533,7 +1025,7 @@ TxSetXDRFrame::prepareForApply(Application& app) const } #endif ZoneScoped; - std::unique_ptr txSet{}; + std::vector phaseFrames; if (isGeneralizedTxSet()) { auto const& xdrTxSet = std::get(mXDRTxSet); @@ -543,62 +1035,34 @@ TxSetXDRFrame::prepareForApply(Application& app) const "Got bad generalized txSet with invalid XDR structure"); return nullptr; } - auto const& phases = xdrTxSet.v1TxSet().phases; - TxSetPhaseTransactions defaultPhases; - defaultPhases.resize(phases.size()); + auto const& xdrPhases = xdrTxSet.v1TxSet().phases; - txSet = std::unique_ptr(new ApplicableTxSetFrame( - app, true, previousLedgerHash(), defaultPhases, mHash)); - - releaseAssert(phases.size() <= - static_cast(TxSetPhase::PHASE_COUNT)); - for (auto phaseId = 0; phaseId < phases.size(); phaseId++) + for (size_t phaseId = 0; phaseId < xdrPhases.size(); ++phaseId) { - auto const& phase = phases[phaseId]; - auto const& components = phase.v0Components(); - for (auto const& component : components) + auto maybePhase = TxSetPhaseFrame::makeFromWire( + static_cast(phaseId), app.getNetworkID(), + xdrPhases[phaseId]); + if (!maybePhase) { - switch (component.type()) - { - case TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE: - std::optional baseFee; - if (component.txsMaybeDiscountedFee().baseFee) - { - baseFee = *component.txsMaybeDiscountedFee().baseFee; - } - if (!txSet->addTxsFromXdr( - app.getNetworkID(), - component.txsMaybeDiscountedFee().txs, true, - baseFee, static_cast(phaseId))) - { - CLOG_DEBUG(Herder, - "Got bad generalized txSet: transactions " - "are not ordered correctly or contain " - "invalid phase transactions"); - return nullptr; - } - break; - } + return nullptr; } + phaseFrames.emplace_back(std::move(*maybePhase)); } } else { auto const& xdrTxSet = std::get(mXDRTxSet); - txSet = std::unique_ptr(new ApplicableTxSetFrame( - app, false, previousLedgerHash(), {TxSetTransactions{}}, mHash)); - if (!txSet->addTxsFromXdr(app.getNetworkID(), xdrTxSet.txs, false, - std::nullopt, TxSetPhase::CLASSIC)) + auto maybePhase = TxSetPhaseFrame::makeFromWireLegacy( + app.getLedgerManager().getLastClosedLedgerHeader().header, + app.getNetworkID(), xdrTxSet.txs); + if (!maybePhase) { - CLOG_DEBUG(Herder, - "Got bad txSet: transactions are not ordered correctly " - "or contain invalid phase transactions"); return nullptr; } - txSet->computeTxFeesForNonGeneralizedSet( - app.getLedgerManager().getLastClosedLedgerHeader().header); + phaseFrames.emplace_back(std::move(*maybePhase)); } - return txSet; + return std::unique_ptr(new ApplicableTxSetFrame( + app, isGeneralizedTxSet(), previousLedgerHash(), phaseFrames, mHash)); } bool @@ -635,9 +1099,28 @@ TxSetXDRFrame::sizeTxTotal() const size_t totalSize = 0; for (auto const& phase : txSet.phases) { - for (auto const& component : phase.v0Components()) + switch (phase.v()) { - totalSize += component.txsMaybeDiscountedFee().txs.size(); + case 0: + for (auto const& component : phase.v0Components()) + { + totalSize += component.txsMaybeDiscountedFee().txs.size(); + } + break; +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + case 1: + for (auto const& stage : + phase.parallelTxsComponent().executionStages) + { + for (auto const& thread : stage) + { + totalSize += thread.size(); + } + } + break; +#endif + default: + break; } } return totalSize; @@ -676,12 +1159,33 @@ TxSetXDRFrame::sizeOpTotalForLogging() const size_t totalSize = 0; for (auto const& phase : txSet.phases) { - for (auto const& component : phase.v0Components()) + switch (phase.v()) { - totalSize += std::accumulate( - component.txsMaybeDiscountedFee().txs.begin(), - component.txsMaybeDiscountedFee().txs.end(), 0ull, - accumulateTxsFn); + case 0: + for (auto const& component : phase.v0Components()) + { + totalSize += std::accumulate( + component.txsMaybeDiscountedFee().txs.begin(), + component.txsMaybeDiscountedFee().txs.end(), 0ull, + accumulateTxsFn); + } + break; +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + case 1: + for (auto const& stage : + phase.parallelTxsComponent().executionStages) + { + for (auto const& thread : stage) + { + totalSize += + std::accumulate(thread.begin(), thread.end(), 0ull, + accumulateTxsFn); + } + } + break; +#endif + default: + break; } } return totalSize; @@ -693,10 +1197,10 @@ TxSetXDRFrame::sizeOpTotalForLogging() const } } -TxSetPhaseTransactions +PerPhaseTransactionList TxSetXDRFrame::createTransactionFrames(Hash const& networkID) const { - TxSetPhaseTransactions phaseTxs; + PerPhaseTransactionList phaseTxs; if (isGeneralizedTxSet()) { auto const& txSet = @@ -704,14 +1208,38 @@ TxSetXDRFrame::createTransactionFrames(Hash const& networkID) const for (auto const& phase : txSet.phases) { auto& txs = phaseTxs.emplace_back(); - for (auto const& component : phase.v0Components()) + switch (phase.v()) { - for (auto const& tx : component.txsMaybeDiscountedFee().txs) + case 0: + for (auto const& component : phase.v0Components()) { - txs.emplace_back( - TransactionFrameBase::makeTransactionFromWire(networkID, - tx)); + for (auto const& tx : component.txsMaybeDiscountedFee().txs) + { + txs.emplace_back( + TransactionFrameBase::makeTransactionFromWire( + networkID, tx)); + } + } + break; +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + case 1: + for (auto const& stage : + phase.parallelTxsComponent().executionStages) + { + for (auto const& thread : stage) + { + for (auto const& tx : thread) + { + txs.emplace_back( + TransactionFrameBase::makeTransactionFromWire( + networkID, tx)); + } + } } + break; +#endif + default: + break; } } } @@ -759,19 +1287,602 @@ TxSetXDRFrame::storeXDR(StoredTransactionSet& txSet) const } else { - txSet.v(0); - txSet.txSet() = std::get(mXDRTxSet); + txSet.v(0); + txSet.txSet() = std::get(mXDRTxSet); + } +} + +TxSetPhaseFrame::Iterator::Iterator(TxStageFrameList const& txs, + size_t stageIndex) + : mStages(txs), mStageIndex(stageIndex) +{ +} + +TransactionFrameBasePtr +TxSetPhaseFrame::Iterator::operator*() const +{ + + if (mStageIndex >= mStages.size() || + mThreadIndex >= mStages[mStageIndex].size() || + mTxIndex >= mStages[mStageIndex][mThreadIndex].size()) + { + throw std::runtime_error("TxPhase iterator out of bounds"); + } + return mStages[mStageIndex][mThreadIndex][mTxIndex]; +} + +TxSetPhaseFrame::Iterator& +TxSetPhaseFrame::Iterator::operator++() +{ + if (mStageIndex >= mStages.size()) + { + throw std::runtime_error("TxPhase iterator out of bounds"); + } + ++mTxIndex; + if (mTxIndex >= mStages[mStageIndex][mThreadIndex].size()) + { + mTxIndex = 0; + ++mThreadIndex; + if (mThreadIndex >= mStages[mStageIndex].size()) + { + mThreadIndex = 0; + ++mStageIndex; + } + } + return *this; +} + +TxSetPhaseFrame::Iterator +TxSetPhaseFrame::Iterator::operator++(int) +{ + auto it = *this; + ++(*this); + return it; +} + +bool +TxSetPhaseFrame::Iterator::operator==(Iterator const& other) const +{ + return mStageIndex == other.mStageIndex && + mThreadIndex == other.mThreadIndex && mTxIndex == other.mTxIndex && + // Make sure to compare the pointers, not the contents, both for + // correctness and optimization. + &mStages == &other.mStages; +} + +bool +TxSetPhaseFrame::Iterator::operator!=(Iterator const& other) const +{ + return !(*this == other); +} + +std::optional +TxSetPhaseFrame::makeFromWire(TxSetPhase phase, Hash const& networkID, + TransactionPhase const& xdrPhase) +{ + auto inclusionFeeMapPtr = std::make_shared(); + auto& inclusionFeeMap = *inclusionFeeMapPtr; + std::optional phaseFrame; + switch (xdrPhase.v()) + { + case 0: + { + TxFrameList txList; + auto const& components = xdrPhase.v0Components(); + for (auto const& component : components) + { + switch (component.type()) + { + case TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE: + std::optional baseFee; + if (component.txsMaybeDiscountedFee().baseFee) + { + baseFee = *component.txsMaybeDiscountedFee().baseFee; + } + size_t prevSize = txList.size(); + if (!addWireTxsToList(networkID, + component.txsMaybeDiscountedFee().txs, + txList)) + { + CLOG_DEBUG(Herder, + "Got bad generalized txSet: transactions " + "are not ordered correctly or contain " + "invalid transactions"); + return std::nullopt; + } + for (auto it = txList.begin() + prevSize; it != txList.end(); + ++it) + { + inclusionFeeMap[*it] = baseFee; + } + break; + } + } + phaseFrame.emplace( + TxSetPhaseFrame(phase, std::move(txList), inclusionFeeMapPtr)); + break; + } +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + case 1: + { + auto const& xdrStages = xdrPhase.parallelTxsComponent().executionStages; + std::optional baseFee; + if (xdrPhase.parallelTxsComponent().baseFee) + { + baseFee = *xdrPhase.parallelTxsComponent().baseFee; + } + TxStageFrameList stages; + stages.reserve(xdrStages.size()); + for (auto const& xdrStage : xdrStages) + { + auto& stage = stages.emplace_back(); + stage.reserve(xdrStage.size()); + for (auto const& xdrThread : xdrStage) + { + auto& thread = stage.emplace_back(); + thread.reserve(xdrThread.size()); + for (auto const& env : xdrThread) + { + auto tx = TransactionFrameBase::makeTransactionFromWire( + networkID, env); + if (!tx->XDRProvidesValidFee()) + { + CLOG_DEBUG(Herder, "Got bad generalized txSet: " + "transaction has invalid XDR"); + return std::nullopt; + } + thread.push_back(tx); + inclusionFeeMap[tx] = baseFee; + } + if (!std::is_sorted(thread.begin(), thread.end(), + &TxSetUtils::hashTxSorter)) + { + CLOG_DEBUG(Herder, "Got bad generalized txSet: " + "thread is not sorted"); + return std::nullopt; + } + } + if (!std::is_sorted(stage.begin(), stage.end(), + [](auto const& a, auto const& b) { + releaseAssert(!a.empty() && !b.empty()); + return TxSetUtils::hashTxSorter(a.front(), + b.front()); + })) + { + CLOG_DEBUG(Herder, "Got bad generalized txSet: " + "stage is not sorted"); + return std::nullopt; + } + } + if (!std::is_sorted(stages.begin(), stages.end(), + [](auto const& a, auto const& b) { + releaseAssert(!a.empty() && !b.empty()); + return TxSetUtils::hashTxSorter( + a.front().front(), b.front().front()); + })) + { + CLOG_DEBUG(Herder, "Got bad generalized txSet: " + "stages are not sorted"); + return std::nullopt; + } + phaseFrame.emplace( + TxSetPhaseFrame(phase, std::move(stages), inclusionFeeMapPtr)); + break; + } +#endif + default: + releaseAssert(false); + } + releaseAssert(phaseFrame); + return phaseFrame; +} + +std::optional +TxSetPhaseFrame::makeFromWireLegacy( + LedgerHeader const& lclHeader, Hash const& networkID, + xdr::xvector const& xdrTxs) +{ + TxFrameList txList; + if (!addWireTxsToList(networkID, xdrTxs, txList)) + { + CLOG_DEBUG( + Herder, + "Got bad legacy txSet: transactions are not ordered correctly " + "or contain invalid phase transactions"); + return std::nullopt; + } + auto inclusionFeeMapPtr = std::make_shared(); + auto& inclusionFeeMap = *inclusionFeeMapPtr; + int64_t baseFee = computeBaseFeeForLegacyTxSet(lclHeader, txList); + for (auto const& tx : txList) + { + inclusionFeeMap[tx] = baseFee; + } + return TxSetPhaseFrame(TxSetPhase::CLASSIC, std::move(txList), + inclusionFeeMapPtr); +} + +TxSetPhaseFrame +TxSetPhaseFrame::makeEmpty(TxSetPhase phase, bool isParallel) +{ + if (isParallel) + { + return TxSetPhaseFrame(phase, TxStageFrameList{}, + std::make_shared()); + } + return TxSetPhaseFrame(phase, TxFrameList{}, + std::make_shared()); +} + +TxSetPhaseFrame::TxSetPhaseFrame( + TxSetPhase phase, TxFrameList const& txs, + std::shared_ptr inclusionFeeMap) + : mPhase(phase), mInclusionFeeMap(inclusionFeeMap), mIsParallel(false) +{ + if (!txs.empty()) + { + mStages.emplace_back().push_back(txs); + } +} + +TxSetPhaseFrame::TxSetPhaseFrame( + TxSetPhase phase, TxStageFrameList&& txs, + std::shared_ptr inclusionFeeMap) + : mPhase(phase) + , mStages(txs) + , mInclusionFeeMap(inclusionFeeMap) + , mIsParallel(true) +{ +} + +TxSetPhaseFrame::Iterator +TxSetPhaseFrame::begin() const +{ + return TxSetPhaseFrame::Iterator(mStages, 0); +} + +TxSetPhaseFrame::Iterator +TxSetPhaseFrame::end() const +{ + return TxSetPhaseFrame::Iterator(mStages, mStages.size()); +} + +size_t +TxSetPhaseFrame::sizeTx() const +{ + ZoneScoped; + return std::distance(this->begin(), this->end()); +} + +size_t +TxSetPhaseFrame::sizeOp() const +{ + ZoneScoped; + return std::accumulate(this->begin(), this->end(), size_t(0), + [&](size_t a, TransactionFrameBasePtr const& tx) { + return a + tx->getNumOperations(); + }); +} + +size_t +TxSetPhaseFrame::size(LedgerHeader const& lclHeader) const +{ + switch (mPhase) + { + case TxSetPhase::CLASSIC: + return protocolVersionStartsFrom(lclHeader.ledgerVersion, + ProtocolVersion::V_11) + ? sizeOp() + : sizeTx(); + case TxSetPhase::SOROBAN: + return sizeOp(); + } +} + +bool +TxSetPhaseFrame::empty() const +{ + return sizeTx() == 0; +} + +bool +TxSetPhaseFrame::isParallel() const +{ + return mIsParallel; +} + +TxStageFrameList const& +TxSetPhaseFrame::getParallelStages() const +{ + releaseAssert(isParallel()); + return mStages; +} + +TxFrameList const& +TxSetPhaseFrame::getSequentialTxs() const +{ + releaseAssert(!isParallel()); + static TxFrameList empty; + if (mStages.empty()) + { + return empty; + } + return mStages.at(0).at(0); +} + +void +TxSetPhaseFrame::toXDR(TransactionPhase& xdrPhase) const +{ + + if (isParallel()) + { + +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + parallelPhaseToXdr(mStages, *mInclusionFeeMap, xdrPhase); +#else + releaseAssert(false); +#endif + } + else + { + sequentialPhaseToXdr(getSequentialTxs(), *mInclusionFeeMap, xdrPhase); + } +} + +InclusionFeeMap const& +TxSetPhaseFrame::getInclusionFeeMap() const +{ + return *mInclusionFeeMap; +} + +TxSetPhaseFrame +TxSetPhaseFrame::sortedForApply(Hash const& txSetHash) const +{ + if (isParallel()) + { + return TxSetPhaseFrame(mPhase, + sortedForApplyParallel(mStages, txSetHash), + mInclusionFeeMap); + } + else + { + return TxSetPhaseFrame( + mPhase, sortedForApplySequential(getSequentialTxs(), txSetHash), + mInclusionFeeMap); + } +} + +bool +TxSetPhaseFrame::checkValid(Application& app, + uint64_t lowerBoundCloseTimeOffset, + uint64_t upperBoundCloseTimeOffset) const +{ + auto const& lcl = app.getLedgerManager().getLastClosedLedgerHeader(); + bool isSoroban = mPhase == TxSetPhase::SOROBAN; + bool checkPhaseSpecific = + isSoroban + ? checkValidSoroban( + lcl.header, app.getLedgerManager().getSorobanNetworkConfig()) + : checkValidClassic(lcl.header); + if (!checkPhaseSpecific) + { + return false; + } + + for (auto const& tx : *this) + { + if (tx->isSoroban() != isSoroban) + { + CLOG_DEBUG(Herder, + "Got bad generalized txSet with invalid " + "phase {} transactions", + static_cast(mPhase)); + return false; + } + } + + if (!checkFeeMap(getInclusionFeeMap(), lcl.header)) + { + return false; + } + + return txsAreValid(app, lowerBoundCloseTimeOffset, + upperBoundCloseTimeOffset); +} + +bool +TxSetPhaseFrame::checkValidClassic(LedgerHeader const& lclHeader) const +{ + if (isParallel()) + { + CLOG_DEBUG(Herder, "Got bad txSet: classic phase can't be parallel"); + return false; + } + if (this->size(lclHeader) > lclHeader.maxTxSetSize) + { + CLOG_DEBUG(Herder, "Got bad txSet: too many classic txs {} > {}", + this->size(lclHeader), lclHeader.maxTxSetSize); + return false; + } + return true; +} + +bool +TxSetPhaseFrame::checkValidSoroban( + LedgerHeader const& lclHeader, + SorobanNetworkConfig const& sorobanConfig) const +{ + bool needParallelSorobanPhase = protocolVersionStartsFrom( + lclHeader.ledgerVersion, PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION); + if (isParallel() != needParallelSorobanPhase) + { + CLOG_DEBUG(Herder, + "Got bad txSet: Soroban phase parallel support " + "does not match the current protocol; '{}' was " + "expected", + needParallelSorobanPhase); + return false; + } + + if (!isParallel()) + { + return true; + } + auto const& stages = getParallelStages(); + + // Verify that number of threads is not exceeded per stage. There is no + // limit for the number of stages or transactions per thread. + for (auto const& stage : stages) + { + if (stage.size() > sorobanConfig.ledgerMaxParallelThreads()) + { + CLOG_DEBUG(Herder, + "Got bad txSet: too many threads in Soroban " + "stage {} > {}", + stage.size(), sorobanConfig.ledgerMaxParallelThreads()); + return false; + } + } + + // Verify that 'sequential' instructions don't exceed the ledger-wide + // limit. + // Every may have multiple thread and its runtime is considered to be + // bounded by the slowest thread (i.e. the one with the most instructions). + // Stages are meant to be executed sequentially, so the ledger-wide + // instructions should be limited by the sum of the stages' instructions. + int64_t totalInstructions = 0; + for (auto const& stage : stages) + { + int64_t stageInstructions = 0; + for (auto const& thread : stage) + { + int64_t threadInstructions = 0; + for (auto const& tx : thread) + { + // threadInstructions + tx->sorobanResources().instructions > + // std::numeric_limits::max() + if (threadInstructions > + std::numeric_limits::max() - + tx->sorobanResources().instructions) + { + CLOG_DEBUG(Herder, "Got bad txSet: Soroban per-thread " + "instructions overflow"); + return false; + } + threadInstructions += tx->sorobanResources().instructions; + } + stageInstructions = std::max(stageInstructions, threadInstructions); + } + // totalInstructions + stageInstructions > + // std::numeric_limits::max() + if (totalInstructions > + std::numeric_limits::max() - stageInstructions) + { + CLOG_DEBUG(Herder, + "Got bad txSet: Soroban total instructions overflow"); + return false; + } + totalInstructions += stageInstructions; + } + if (totalInstructions > sorobanConfig.ledgerMaxInstructions()) + { + CLOG_DEBUG( + Herder, + "Got bad txSet: Soroban total instructions exceed limit: {} > {}", + totalInstructions, sorobanConfig.ledgerMaxInstructions()); + return false; + } + + // Verify that there are no read-write conflicts between threads within + // every stage. + for (auto const& stage : stages) + { + UnorderedSet stageReadOnlyKeys; + UnorderedSet stageReadWriteKeys; + for (auto const& thread : stage) + { + std::vector threadReadOnlyKeys; + std::vector threadReadWriteKeys; + for (auto const& tx : thread) + { + auto const& footprint = tx->sorobanResources().footprint; + + for (auto const& key : footprint.readOnly) + { + if (stageReadWriteKeys.count(key) > 0) + { + CLOG_DEBUG( + Herder, + "Got bad generalized txSet: thread footprint " + "conflicts with another thread within stage"); + return false; + } + threadReadOnlyKeys.push_back(key); + } + for (auto const& key : footprint.readWrite) + { + if (stageReadOnlyKeys.count(key) > 0 || + stageReadWriteKeys.count(key) > 0) + { + CLOG_DEBUG( + Herder, + "Got bad generalized txSet: thread footprint " + "conflicts with another thread within stage"); + return false; + } + threadReadWriteKeys.push_back(key); + } + } + stageReadOnlyKeys.insert(threadReadOnlyKeys.begin(), + threadReadOnlyKeys.end()); + stageReadWriteKeys.insert(threadReadWriteKeys.begin(), + threadReadWriteKeys.end()); + } + } + return true; +} + +// This assumes that the phase validation has already been done, +// specifically that there are no transactions that belong to the same +// source account. +bool +TxSetPhaseFrame::txsAreValid(Application& app, + uint64_t lowerBoundCloseTimeOffset, + uint64_t upperBoundCloseTimeOffset) const +{ + ZoneScoped; + // This is done so minSeqLedgerGap is validated against the next + // ledgerSeq, which is what will be used at apply time + + // Grab read-only latest ledger state; This is only used to validate tx sets + // for LCL+1 + LedgerSnapshot ls(app); + ls.getLedgerHeader().currentToModify().ledgerSeq = + app.getLedgerManager().getLastClosedLedgerNum() + 1; + for (auto const& tx : *this) + { + auto txResult = tx->checkValid(app, ls, 0, lowerBoundCloseTimeOffset, + upperBoundCloseTimeOffset); + if (!txResult->isSuccess()) + { + + CLOG_DEBUG( + Herder, "Got bad txSet: tx invalid tx: {} result: {}", + xdrToCerealString(tx->getEnvelope(), "TransactionEnvelope"), + txResult->getResultCode()); + return false; + } } + return true; } -ApplicableTxSetFrame::ApplicableTxSetFrame(Application& app, bool isGeneralized, - Hash const& previousLedgerHash, - TxSetPhaseTransactions const& txs, - std::optional contentsHash) +ApplicableTxSetFrame::ApplicableTxSetFrame( + Application& app, bool isGeneralized, Hash const& previousLedgerHash, + std::vector const& phases, + std::optional contentsHash) : mIsGeneralized(isGeneralized) , mPreviousLedgerHash(previousLedgerHash) - , mTxPhases(txs) - , mPhaseInclusionFeeMap(mTxPhases.size()) + , mPhases(phases) , mContentsHash(contentsHash) { releaseAssert(previousLedgerHash == @@ -780,12 +1891,13 @@ ApplicableTxSetFrame::ApplicableTxSetFrame(Application& app, bool isGeneralized, ApplicableTxSetFrame::ApplicableTxSetFrame( Application& app, LedgerHeaderHistoryEntry const& lclHeader, - TxSetPhaseTransactions const& txs, std::optional contentsHash) + std::vector const& phases, + std::optional contentsHash) : ApplicableTxSetFrame( app, protocolVersionStartsFrom(lclHeader.header.ledgerVersion, SOROBAN_PROTOCOL_VERSION), - lclHeader.hash, txs, contentsHash) + lclHeader.hash, phases, contentsHash) { } @@ -796,73 +1908,33 @@ ApplicableTxSetFrame::getContentsHash() const return *mContentsHash; } -TxSetTransactions const& -ApplicableTxSetFrame::getTxsForPhase(TxSetPhase phase) const +TxSetPhaseFrame const& +ApplicableTxSetFrame::getPhase(TxSetPhase phaseTxs) const { - releaseAssert(static_cast(phase) < mTxPhases.size()); - return mTxPhases.at(static_cast(phase)); + releaseAssert(static_cast(phaseTxs) < mPhases.size()); + return mPhases.at(static_cast(phaseTxs)); } -TxSetTransactions -ApplicableTxSetFrame::getTxsInApplyOrder() const +std::vector const& +ApplicableTxSetFrame::getPhases() const { -#ifdef BUILD_TESTS - if (mApplyOrderOverride) - { - return *mApplyOrderOverride; - } -#endif - ZoneScoped; - - // Use a single vector to order transactions from all phases - std::vector retList; - retList.reserve(sizeTxTotal()); + return mPhases; +} - for (auto const& phase : mTxPhases) +std::vector const& +ApplicableTxSetFrame::getPhasesInApplyOrder() const +{ + ZoneScoped; + if (mApplyOrderPhases.empty()) { - auto txQueues = TxSetUtils::buildAccountTxQueues(phase); - - // build txBatches - // txBatches i-th element contains each i-th transaction for accounts - // with a transaction in the transaction set - std::vector> txBatches; - - while (!txQueues.empty()) - { - txBatches.emplace_back(); - auto& curBatch = txBatches.back(); - // go over all users that still have transactions - for (auto it = txQueues.begin(); it != txQueues.end();) - { - auto& txQueue = *it; - curBatch.emplace_back(txQueue->getTopTx()); - txQueue->popTopTx(); - if (txQueue->empty()) - { - // done with that user - it = txQueues.erase(it); - } - else - { - ++it; - } - } - } - - for (auto& batch : txBatches) + mApplyOrderPhases.reserve(mPhases.size()); + for (auto const& phaseTxs : mPhases) { - // randomize each batch using the hash of the transaction set - // as a way to randomize even more - ApplyTxSorter s(getContentsHash()); - std::sort(batch.begin(), batch.end(), s); - for (auto const& tx : batch) - { - retList.push_back(tx); - } + mApplyOrderPhases.emplace_back( + phaseTxs.sortedForApply(getContentsHash())); } } - - return retList; + return mApplyOrderPhases; } // need to make sure every account that is submitting a tx has enough to pay @@ -897,53 +1969,15 @@ ApplicableTxSetFrame::checkValid(Application& app, if (isGeneralizedTxSet()) { - auto checkFeeMap = [&](auto const& feeMap) { - for (auto const& [tx, fee] : feeMap) - { - if (!fee) - { - continue; - } - if (*fee < lcl.header.baseFee) - { - - CLOG_DEBUG( - Herder, - "Got bad txSet: {} has too low component base fee {}", - hexAbbrev(mPreviousLedgerHash), *fee); - return false; - } - if (tx->getInclusionFee() < - getMinInclusionFee(*tx, lcl.header, fee)) - { - CLOG_DEBUG( - Herder, - "Got bad txSet: {} has tx with fee bid ({}) lower " - "than base fee ({})", - hexAbbrev(mPreviousLedgerHash), tx->getInclusionFee(), - getMinInclusionFee(*tx, lcl.header, fee)); - return false; - } - } - return true; - }; - - if (!checkFeeMap(getInclusionFeeMap(TxSetPhase::CLASSIC))) - { - return false; - } - if (!checkFeeMap(getInclusionFeeMap(TxSetPhase::SOROBAN))) - { - return false; - } + // Generalized transaction sets should always have 2 phases by + // construction. + releaseAssert(mPhases.size() == + static_cast(TxSetPhase::PHASE_COUNT)); } - - if (this->size(lcl.header, TxSetPhase::CLASSIC) > lcl.header.maxTxSetSize) + else { - CLOG_DEBUG(Herder, "Got bad txSet: too many classic txs {} > {}", - this->size(lcl.header, TxSetPhase::CLASSIC), - lcl.header.maxTxSetSize); - return false; + // Legacy tx sets should have 1 phase by construction. + releaseAssert(mPhases.size() == 1); } if (needGeneralizedTxSet) @@ -951,7 +1985,7 @@ ApplicableTxSetFrame::checkValid(Application& app, // First, ensure the tx set does not contain multiple txs per source // account std::unordered_set seenAccounts; - for (auto const& phase : mTxPhases) + for (auto const& phase : mPhases) { for (auto const& tx : phase) { @@ -974,54 +2008,54 @@ ApplicableTxSetFrame::checkValid(Application& app, return false; } + auto limits = app.getLedgerManager().maxLedgerResources( + /* isSoroban */ true); + // When building Soroban tx sets with parallel execution support, + // instructions are accounted for by the build logic, not by the + // surge pricing config, so we need to relax the instruction limit + // in surge pricing logic. + if (protocolVersionStartsFrom(app.getLedgerManager() + .getLastClosedLedgerHeader() + .header.ledgerVersion, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) { - auto limits = app.getLedgerManager().maxLedgerResources( - /* isSoroban */ true); - if (anyGreater(*totalTxSetRes, limits)) - { - CLOG_DEBUG(Herder, - "Got bad txSet: needed resources exceed ledger " - "limits {} > {}", - totalTxSetRes->toString(), limits.toString()); - return false; - } + limits.setVal(Resource::Type::INSTRUCTIONS, + std::numeric_limits::max()); + } + if (anyGreater(*totalTxSetRes, limits)) + { + CLOG_DEBUG(Herder, + "Got bad txSet: needed resources exceed ledger " + "limits {} > {}", + totalTxSetRes->toString(), limits.toString()); + return false; } } - - bool allValid = true; - for (auto const& txs : mTxPhases) + for (auto const& phase : mPhases) { - if (!phaseTxsAreValid(txs, app, lowerBoundCloseTimeOffset, + if (!phase.checkValid(app, lowerBoundCloseTimeOffset, upperBoundCloseTimeOffset)) { - allValid = false; - break; + return false; } } - return allValid; + return true; } size_t ApplicableTxSetFrame::size(LedgerHeader const& lh, - std::optional phase) const + std::optional phaseType) const { - size_t sz = 0; - if (!phase) - { - if (numPhases() > static_cast(TxSetPhase::SOROBAN)) - { - sz += sizeOp(TxSetPhase::SOROBAN); - } - } - else if (phase.value() == TxSetPhase::SOROBAN) + ZoneScoped; + if (phaseType) { - sz += sizeOp(TxSetPhase::SOROBAN); + return mPhases.at(static_cast(*phaseType)).size(lh); } - if (!phase || phase.value() == TxSetPhase::CLASSIC) + + size_t sz = 0; + for (auto const& phase : mPhases) { - sz += protocolVersionStartsFrom(lh.ledgerVersion, ProtocolVersion::V_11) - ? sizeOp(TxSetPhase::CLASSIC) - : sizeTx(TxSetPhase::CLASSIC); + sz += phase.size(lh); } return sz; } @@ -1029,12 +2063,7 @@ ApplicableTxSetFrame::size(LedgerHeader const& lh, size_t ApplicableTxSetFrame::sizeOp(TxSetPhase phase) const { - ZoneScoped; - auto const& txs = mTxPhases.at(static_cast(phase)); - return std::accumulate(txs.begin(), txs.end(), size_t(0), - [&](size_t a, TransactionFrameBasePtr const& tx) { - return a + tx->getNumOperations(); - }); + return mPhases.at(static_cast(phase)).sizeOp(); } size_t @@ -1042,150 +2071,51 @@ ApplicableTxSetFrame::sizeOpTotal() const { ZoneScoped; size_t total = 0; - for (int i = 0; i < mTxPhases.size(); i++) + for (auto const& phase : mPhases) { - total += sizeOp(static_cast(i)); + total += phase.sizeOp(); } return total; } size_t -ApplicableTxSetFrame::sizeTxTotal() const -{ - ZoneScoped; - size_t total = 0; - for (int i = 0; i < mTxPhases.size(); i++) - { - total += sizeTx(static_cast(i)); - } - return total; -} - -void -ApplicableTxSetFrame::computeTxFeesForNonGeneralizedSet( - LedgerHeader const& lclHeader) +ApplicableTxSetFrame::sizeTx(TxSetPhase phase) const { - ZoneScoped; - auto ledgerVersion = lclHeader.ledgerVersion; - int64_t lowBaseFee = std::numeric_limits::max(); - releaseAssert(mTxPhases.size() == 1); - for (auto const& txPtr : mTxPhases[0]) - { - int64_t txBaseFee = computePerOpFee(*txPtr, ledgerVersion); - lowBaseFee = std::min(lowBaseFee, txBaseFee); - } - computeTxFeesForNonGeneralizedSet(lclHeader, lowBaseFee, - /* enableLogging */ false); + return mPhases.at(static_cast(phase)).sizeTx(); } -void -ApplicableTxSetFrame::computeTxFeesForNonGeneralizedSet( - LedgerHeader const& lclHeader, int64_t lowestBaseFee, bool enableLogging) +size_t +ApplicableTxSetFrame::sizeTxTotal() const { ZoneScoped; - int64_t baseFee = lclHeader.baseFee; - - if (protocolVersionStartsFrom(lclHeader.ledgerVersion, - ProtocolVersion::V_11)) - { - size_t surgeOpsCutoff = 0; - if (lclHeader.maxTxSetSize >= MAX_OPS_PER_TX) - { - surgeOpsCutoff = lclHeader.maxTxSetSize - MAX_OPS_PER_TX; - } - if (sizeOp(TxSetPhase::CLASSIC) > surgeOpsCutoff) - { - baseFee = lowestBaseFee; - if (enableLogging) - { - CLOG_WARNING(Herder, "surge pricing in effect! {} > {}", - sizeOp(TxSetPhase::CLASSIC), surgeOpsCutoff); - } - } - } - - releaseAssert(mTxPhases.size() == 1); - releaseAssert(mPhaseInclusionFeeMap.size() == 1); - auto const& phase = mTxPhases[static_cast(TxSetPhase::CLASSIC)]; - auto& feeMap = getInclusionFeeMapMut(TxSetPhase::CLASSIC); - for (auto const& tx : phase) - { - feeMap[tx] = baseFee; - } -} - -void -ApplicableTxSetFrame::computeTxFees( - TxSetPhase phase, LedgerHeader const& ledgerHeader, - SurgePricingLaneConfig const& surgePricingConfig, - std::vector const& lowestLaneFee, - std::vector const& hadTxNotFittingLane) -{ - releaseAssert(isGeneralizedTxSet()); - releaseAssert(lowestLaneFee.size() == hadTxNotFittingLane.size()); - std::vector laneBaseFee(lowestLaneFee.size(), - ledgerHeader.baseFee); - auto minBaseFee = - *std::min_element(lowestLaneFee.begin(), lowestLaneFee.end()); - for (size_t lane = 0; lane < laneBaseFee.size(); ++lane) - { - // If generic lane is full, then any transaction had to compete with not - // included transactions and independently of the lane they need to have - // at least the minimum fee in the tx set applied. - if (hadTxNotFittingLane[SurgePricingPriorityQueue::GENERIC_LANE]) - { - laneBaseFee[lane] = minBaseFee; - } - // If limited lane is full, then the transactions in this lane also had - // to compete with each other and have a base fee associated with this - // lane only. - if (lane != SurgePricingPriorityQueue::GENERIC_LANE && - hadTxNotFittingLane[lane]) - { - laneBaseFee[lane] = lowestLaneFee[lane]; - } - if (laneBaseFee[lane] > ledgerHeader.baseFee) - { - CLOG_WARNING( - Herder, - "{} phase: surge pricing for '{}' lane is in effect with base " - "fee={}, baseFee={}", - getTxSetPhaseName(phase), - lane == SurgePricingPriorityQueue::GENERIC_LANE ? "generic" - : "DEX", - laneBaseFee[lane], ledgerHeader.baseFee); - } - } - - auto const& txs = mTxPhases.at(static_cast(phase)); - auto& feeMap = getInclusionFeeMapMut(phase); - for (auto const& tx : txs) + size_t total = 0; + for (auto const& phase : mPhases) { - feeMap[tx] = laneBaseFee[surgePricingConfig.getLane(*tx)]; + total += phase.sizeTx(); } + return total; } std::optional -ApplicableTxSetFrame::getTxBaseFee(TransactionFrameBaseConstPtr const& tx, - LedgerHeader const& lclHeader) const +ApplicableTxSetFrame::getTxBaseFee(TransactionFrameBaseConstPtr const& tx) const { - for (auto const& phaseMap : mPhaseInclusionFeeMap) + for (auto const& phase : mPhases) { + auto const& phaseMap = phase.getInclusionFeeMap(); if (auto it = phaseMap.find(tx); it != phaseMap.end()) { return it->second; } } throw std::runtime_error("Transaction not found in tx set"); - return std::nullopt; } std::optional ApplicableTxSetFrame::getTxSetSorobanResource() const { - releaseAssert(mTxPhases.size() > static_cast(TxSetPhase::SOROBAN)); + releaseAssert(mPhases.size() > static_cast(TxSetPhase::SOROBAN)); auto total = Resource::makeEmptySoroban(); - for (auto const& tx : mTxPhases[static_cast(TxSetPhase::SOROBAN)]) + for (auto const& tx : mPhases[static_cast(TxSetPhase::SOROBAN)]) { if (total.canAdd(tx->getResources(/* useByteLimitInClassic */ false))) { @@ -1204,16 +2134,13 @@ ApplicableTxSetFrame::getTotalFees(LedgerHeader const& lh) const { ZoneScoped; int64_t total{0}; - std::for_each(mTxPhases.begin(), mTxPhases.end(), - [&](TxSetTransactions const& phase) { - total += std::accumulate( - phase.begin(), phase.end(), int64_t(0), - [&](int64_t t, TransactionFrameBasePtr const& tx) { - return t + - tx->getFee(lh, getTxBaseFee(tx, lh), true); - }); - }); - + for (auto const& phaseTxs : mPhases) + { + for (auto const& tx : phaseTxs) + { + total += tx->getFee(lh, getTxBaseFee(tx), true); + } + } return total; } @@ -1222,15 +2149,13 @@ ApplicableTxSetFrame::getTotalInclusionFees() const { ZoneScoped; int64_t total{0}; - std::for_each(mTxPhases.begin(), mTxPhases.end(), - [&](TxSetTransactions const& phase) { - total += std::accumulate( - phase.begin(), phase.end(), int64_t(0), - [&](int64_t t, TransactionFrameBasePtr const& tx) { - return t + tx->getInclusionFee(); - }); - }); - + for (auto const& phaseTxs : mPhases) + { + for (auto const& tx : phaseTxs) + { + total += tx->getInclusionFee(); + } + } return total; } @@ -1247,7 +2172,8 @@ ApplicableTxSetFrame::summary() const FMT_STRING("txs:{}, ops:{}, base_fee:{}"), sizeTxTotal(), sizeOpTotal(), // NB: fee map can't be empty at this stage (checked above). - getInclusionFeeMap(TxSetPhase::CLASSIC) + mPhases[static_cast(TxSetPhase::CLASSIC)] + .getInclusionFeeMap() .begin() ->second.value_or(0)); } @@ -1286,18 +2212,17 @@ ApplicableTxSetFrame::summary() const }; std::string status; - releaseAssert(mTxPhases.size() <= + releaseAssert(mPhases.size() <= static_cast(TxSetPhase::PHASE_COUNT)); - for (auto i = 0; i != mTxPhases.size(); i++) + for (size_t i = 0; i < mPhases.size(); i++) { if (!status.empty()) { status += ", "; } - status += fmt::format( - FMT_STRING("{} phase: {}"), - getTxSetPhaseName(static_cast(i)), - feeStats(getInclusionFeeMap(static_cast(i)))); + status += fmt::format(FMT_STRING("{} phase: {}"), + getTxSetPhaseName(static_cast(i)), + feeStats(mPhases[i].getInclusionFeeMap())); } return status; } @@ -1307,8 +2232,9 @@ ApplicableTxSetFrame::toXDR(TransactionSet& txSet) const { ZoneScoped; releaseAssert(!isGeneralizedTxSet()); - releaseAssert(mTxPhases.size() == 1); - transactionsToTransactionSetXDR(mTxPhases[0], mPreviousLedgerHash, txSet); + releaseAssert(mPhases.size() == 1); + transactionsToTransactionSetXDR(mPhases[0].getSequentialTxs(), + mPreviousLedgerHash, txSet); } void @@ -1316,10 +2242,9 @@ ApplicableTxSetFrame::toXDR(GeneralizedTransactionSet& generalizedTxSet) const { ZoneScoped; releaseAssert(isGeneralizedTxSet()); - releaseAssert(mTxPhases.size() <= + releaseAssert(mPhases.size() <= static_cast(TxSetPhase::PHASE_COUNT)); - transactionsToGeneralizedTransactionSetXDR(mTxPhases, mPhaseInclusionFeeMap, - mPreviousLedgerHash, + transactionsToGeneralizedTransactionSetXDR(mPhases, mPreviousLedgerHash, generalizedTxSet); } @@ -1348,168 +2273,4 @@ ApplicableTxSetFrame::isGeneralizedTxSet() const return mIsGeneralized; } -bool -ApplicableTxSetFrame::addTxsFromXdr( - Hash const& networkID, xdr::xvector const& txs, - bool useBaseFee, std::optional baseFee, TxSetPhase phase) -{ - auto& phaseTxs = mTxPhases.at(static_cast(phase)); - size_t oldSize = phaseTxs.size(); - phaseTxs.reserve(oldSize + txs.size()); - auto& inclusionFeeMap = getInclusionFeeMapMut(phase); - for (auto const& env : txs) - { - auto tx = TransactionFrameBase::makeTransactionFromWire(networkID, env); - if (!tx->XDRProvidesValidFee()) - { - return false; - } - // Phase should be consistent with the tx we're trying to add - if ((tx->isSoroban() && phase != TxSetPhase::SOROBAN) || - (!tx->isSoroban() && phase != TxSetPhase::CLASSIC)) - { - return false; - } - - phaseTxs.push_back(tx); - if (useBaseFee) - { - inclusionFeeMap[tx] = baseFee; - } - } - return std::is_sorted(phaseTxs.begin() + oldSize, phaseTxs.end(), - &TxSetUtils::hashTxSorter); -} - -void -ApplicableTxSetFrame::applySurgePricing(Application& app) -{ - ZoneScoped; - releaseAssert(mTxPhases.size() <= - static_cast(TxSetPhase::PHASE_COUNT)); - auto const& lclHeader = - app.getLedgerManager().getLastClosedLedgerHeader().header; - for (int i = 0; i < mTxPhases.size(); i++) - { - TxSetPhase phaseType = static_cast(i); - auto& phase = mTxPhases[i]; - if (phaseType == TxSetPhase::CLASSIC) - { - auto maxOps = - Resource({static_cast( - app.getLedgerManager().getLastMaxTxSetSizeOps()), - MAX_CLASSIC_BYTE_ALLOWANCE}); - std::optional dexOpsLimit; - if (isGeneralizedTxSet() && - app.getConfig().MAX_DEX_TX_OPERATIONS_IN_TX_SET) - { - // DEX operations limit implies that DEX transactions should - // compete with each other in in a separate fee lane, which is - // only possible with generalized tx set. - dexOpsLimit = - Resource({*app.getConfig().MAX_DEX_TX_OPERATIONS_IN_TX_SET, - MAX_CLASSIC_BYTE_ALLOWANCE}); - } - - auto surgePricingLaneConfig = - std::make_shared(maxOps, dexOpsLimit); - - std::vector hadTxNotFittingLane; - - auto includedTxs = - SurgePricingPriorityQueue::getMostTopTxsWithinLimits( - phase, surgePricingLaneConfig, hadTxNotFittingLane); - - size_t laneCount = surgePricingLaneConfig->getLaneLimits().size(); - std::vector lowestLaneFee( - laneCount, std::numeric_limits::max()); - for (auto const& tx : includedTxs) - { - size_t lane = surgePricingLaneConfig->getLane(*tx); - auto perOpFee = computePerOpFee(*tx, lclHeader.ledgerVersion); - lowestLaneFee[lane] = std::min(lowestLaneFee[lane], perOpFee); - } - - phase = includedTxs; - if (isGeneralizedTxSet()) - { - computeTxFees(TxSetPhase::CLASSIC, lclHeader, - *surgePricingLaneConfig, lowestLaneFee, - hadTxNotFittingLane); - } - else - { - computeTxFeesForNonGeneralizedSet( - lclHeader, - lowestLaneFee[SurgePricingPriorityQueue::GENERIC_LANE], - /* enableLogging */ true); - } - } - else - { - releaseAssert(isGeneralizedTxSet()); - releaseAssert(phaseType == TxSetPhase::SOROBAN); - - auto limits = app.getLedgerManager().maxLedgerResources( - /* isSoroban */ true); - - auto byteLimit = - std::min(static_cast(MAX_SOROBAN_BYTE_ALLOWANCE), - limits.getVal(Resource::Type::TX_BYTE_SIZE)); - limits.setVal(Resource::Type::TX_BYTE_SIZE, byteLimit); - - auto surgePricingLaneConfig = - std::make_shared(limits); - - std::vector hadTxNotFittingLane; - auto includedTxs = - SurgePricingPriorityQueue::getMostTopTxsWithinLimits( - phase, surgePricingLaneConfig, hadTxNotFittingLane); - - size_t laneCount = surgePricingLaneConfig->getLaneLimits().size(); - std::vector lowestLaneFee( - laneCount, std::numeric_limits::max()); - for (auto const& tx : includedTxs) - { - size_t lane = surgePricingLaneConfig->getLane(*tx); - auto perOpFee = computePerOpFee(*tx, lclHeader.ledgerVersion); - lowestLaneFee[lane] = std::min(lowestLaneFee[lane], perOpFee); - } - - phase = includedTxs; - computeTxFees(phaseType, lclHeader, *surgePricingLaneConfig, - lowestLaneFee, hadTxNotFittingLane); - } - } -} - -std::unordered_map> const& -ApplicableTxSetFrame::getInclusionFeeMap(TxSetPhase phase) const -{ - size_t phaseId = static_cast(phase); - releaseAssert(phaseId < mPhaseInclusionFeeMap.size()); - return mPhaseInclusionFeeMap[phaseId]; -} - -std::unordered_map>& -ApplicableTxSetFrame::getInclusionFeeMapMut(TxSetPhase phase) -{ - size_t phaseId = static_cast(phase); - releaseAssert(phaseId < mPhaseInclusionFeeMap.size()); - return mPhaseInclusionFeeMap[phaseId]; -} - -std::string -getTxSetPhaseName(TxSetPhase phase) -{ - switch (phase) - { - case TxSetPhase::CLASSIC: - return "classic"; - case TxSetPhase::SOROBAN: - return "soroban"; - default: - throw std::runtime_error("Unknown phase"); - } -} } // namespace stellar diff --git a/src/herder/TxSetFrame.h b/src/herder/TxSetFrame.h index e8942046c9..b480bc0150 100644 --- a/src/herder/TxSetFrame.h +++ b/src/herder/TxSetFrame.h @@ -9,6 +9,7 @@ #include "overlay/StellarXDR.h" #include "transactions/TransactionFrame.h" #include "util/NonCopyable.h" +#include "util/ProtocolVersion.h" #include "xdr/Stellar-internal.h" #include @@ -33,10 +34,8 @@ enum class TxSetPhase PHASE_COUNT }; -using TxSetTransactions = std::vector; -using TxSetPhaseTransactions = std::vector; - -std::string getTxSetPhaseName(TxSetPhase phase); +using TxFrameList = std::vector; +using PerPhaseTransactionList = std::vector; // Creates a valid ApplicableTxSetFrame and corresponding TxSetXDRFrame // from the provided transactions. @@ -51,26 +50,26 @@ std::string getTxSetPhaseName(TxSetPhase phase); // transaction pointers. std::pair makeTxSetFromTransactions( - TxSetPhaseTransactions const& txPhases, Application& app, + PerPhaseTransactionList const& txPhases, Application& app, uint64_t lowerBoundCloseTimeOffset, uint64_t upperBoundCloseTimeOffset #ifdef BUILD_TESTS // Skips the tx set validation and preserves the pointers // to the passed-in transactions - use in conjunction with - // `orderOverride` argument in test-only overrides. + // `enforceTxsApplyOrder` argument in test-only overrides. , bool skipValidation = false #endif ); std::pair makeTxSetFromTransactions( - TxSetPhaseTransactions const& txPhases, Application& app, + PerPhaseTransactionList const& txPhases, Application& app, uint64_t lowerBoundCloseTimeOffset, uint64_t upperBoundCloseTimeOffset, - TxSetPhaseTransactions& invalidTxsPerPhase + PerPhaseTransactionList& invalidTxsPerPhase #ifdef BUILD_TESTS // Skips the tx set validation and preserves the pointers // to the passed-in transactions - use in conjunction with - // `orderOverride` argument in test-only overrides. + // `enforceTxsApplyOrder` argument in test-only overrides. , bool skipValidation = false #endif @@ -78,15 +77,15 @@ makeTxSetFromTransactions( #ifdef BUILD_TESTS std::pair -makeTxSetFromTransactions(TxSetTransactions txs, Application& app, +makeTxSetFromTransactions(TxFrameList txs, Application& app, uint64_t lowerBoundCloseTimeOffset, uint64_t upperBoundCloseTimeOffset, bool enforceTxsApplyOrder = false); std::pair -makeTxSetFromTransactions(TxSetTransactions txs, Application& app, +makeTxSetFromTransactions(TxFrameList txs, Application& app, uint64_t lowerBoundCloseTimeOffset, uint64_t upperBoundCloseTimeOffset, - TxSetTransactions& invalidTxs, + TxFrameList& invalidTxs, bool enforceTxsApplyOrder = false); #endif @@ -99,8 +98,7 @@ makeTxSetFromTransactions(TxSetTransactions txs, Application& app, // // Before even trying to validate and apply a TxSetXDRFrame it has // to be interpreted and prepared for apply using the ledger state -// this TxSetXDRFrame refers to. This is typically performed by -// `prepareForApply` method. +// this TxSetXDRFrame refers to. This is performed by `prepareForApply` method. class TxSetXDRFrame : public NonMovableOrCopyable { public: @@ -124,7 +122,7 @@ class TxSetXDRFrame : public NonMovableOrCopyable // historical transactions. static TxSetXDRFrameConstPtr makeFromHistoryTransactions(Hash const& previousLedgerHash, - TxSetTransactions const& txs); + TxFrameList const& txs); void toXDR(TransactionSet& set) const; void toXDR(GeneralizedTransactionSet& generalizedTxSet) const; @@ -152,8 +150,11 @@ class TxSetXDRFrame : public NonMovableOrCopyable // Returns the hash of this tx set. Hash const& getContentsHash() const; + // Returns the hash of the previous ledger that this tx set refers to. Hash const& previousLedgerHash() const; + // Returns the total number of transactions in this tx set (even if it's + // not structurally valid). size_t sizeTxTotal() const; // Gets the size of this transaction set in operations. @@ -170,7 +171,8 @@ class TxSetXDRFrame : public NonMovableOrCopyable // This is only necessary to serve a very specific use case of updating // the transaction queue with wired tx sets. Otherwise, use // getTransactionsForPhase() in `ApplicableTxSetFrame`. - TxSetPhaseTransactions createTransactionFrames(Hash const& networkID) const; + PerPhaseTransactionList + createTransactionFrames(Hash const& networkID) const; #ifdef BUILD_TESTS mutable ApplicableTxSetFrameConstPtr mApplicableTxSetOverride; @@ -187,6 +189,169 @@ class TxSetXDRFrame : public NonMovableOrCopyable Hash mHash; }; +// The following definitions are used to represent the 'parallel' phase of the +// transaction set. +// +// The structure of this phase is as follows: +// - The whole phase (`TxStageFrameList`) consists of several sequential +// 'stages' (`TxStageFrame`). A stage has to be executed after every +// transaction in the previous stage has been applied. +// - A 'stage' (`TxStageFrame`) consists of several parallel 'threads' +// (`TxThreadFrame`). Transactions in different 'threads' are independent of +// each other and can be applied in parallel. +// - A 'thread' (`TxThreadFrame`) consists of transactions that should +// generally be applied sequentially. However, not all the transactions in +// the thread are necessarily conflicting with each other; it is possible +// that some, or even all transactions in the thread structure can be applied +// in parallel with each other (depending on their footprints). +// +// This structure mimics the XDR structure of the `ParallelTxsComponent`. +using TxThreadFrame = TxFrameList; +using TxStageFrame = std::vector; +using TxStageFrameList = std::vector; + +// Alias for the map from transaction to its inclusion fee as defined by the +// transaction set. +using InclusionFeeMap = + std::unordered_map>; + +// `TxSetPhaseFrame` represents a single phase of the `ApplicableTxSetFrame`. +// +// Phases can only be created as a part of the `ApplicableTxSetFrame` and thus +// don't have any public constructors. +// +// Phase may either wrap the corresponding `TransactionPhase` XDR for +// generalized transactions sets, or represent all the transactions in the +// 'legacy' transaction set (which is considered to have only a single phase). +// +// This does not assume any specific order of transactions by default - the +// phase in 'apply' order has to be explicitly requested from the parent +// `ApplicableTxSetFrame` via `getPhasesInApplyOrder` method. +class TxSetPhaseFrame +{ + public: + // Returns true when this phase can be applied in parallel. + // Currently only Soroban phase can be parallel, and only starting from + // PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION protocol + bool isParallel() const; + + // Returns the parallel stages of this phase. + // + // This may only be called when `isParallel()` is true. + TxStageFrameList const& getParallelStages() const; + // Returns all the transactions in this phase if it's not parallel. + // + // This may only be called when `isParallel()` is false. + TxFrameList const& getSequentialTxs() const; + + // Serializes this phase to the provided XDR. + void toXDR(TransactionPhase& xdrPhase) const; + + // Iterator over all transactions in this phase. + // The order of iteration is defined by the parent `ApplicableTxSetFrame`. + // If the phase is sorted for apply, then the iteration order can be used + // to determine a stable index of every transaction in the phase, even if + // the phase is parallel and can have certain transaction applied in + // arbitrary order. + class Iterator + { + public: + using value_type = TransactionFrameBasePtr; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + using iterator_category = std::forward_iterator_tag; + + TransactionFrameBasePtr operator*() const; + + Iterator& operator++(); + Iterator operator++(int); + + bool operator==(Iterator const& other) const; + bool operator!=(Iterator const& other) const; + + private: + friend class TxSetPhaseFrame; + + Iterator(TxStageFrameList const& txs, size_t stageIndex); + TxStageFrameList const& mStages; + size_t mStageIndex = 0; + size_t mThreadIndex = 0; + size_t mTxIndex = 0; + }; + Iterator begin() const; + Iterator end() const; + size_t sizeTx() const; + size_t sizeOp() const; + size_t size(LedgerHeader const& lclHeader) const; + bool empty() const; + + // Get _inclusion_ fee map for this phase. The map contains lowest base + // fee for each transaction (lowest base fee is identical for all + // transactions in the same lane) + InclusionFeeMap const& getInclusionFeeMap() const; + + private: + friend class TxSetXDRFrame; + friend class ApplicableTxSetFrame; + + friend std::pair + makeTxSetFromTransactions(PerPhaseTransactionList const& txPhases, + Application& app, + uint64_t lowerBoundCloseTimeOffset, + uint64_t upperBoundCloseTimeOffset, + PerPhaseTransactionList& invalidTxsPerPhase +#ifdef BUILD_TESTS + , + bool skipValidation +#endif + ); +#ifdef BUILD_TESTS + friend std::pair + makeTxSetFromTransactions(TxFrameList txs, Application& app, + uint64_t lowerBoundCloseTimeOffset, + uint64_t upperBoundCloseTimeOffset, + TxFrameList& invalidTxs, + bool enforceTxsApplyOrder); +#endif + TxSetPhaseFrame(TxSetPhase phase, TxFrameList const& txs, + std::shared_ptr inclusionFeeMap); + TxSetPhaseFrame(TxSetPhase phase, TxStageFrameList&& txs, + std::shared_ptr inclusionFeeMap); + + // Creates a new phase from `TransactionPhase` XDR coming from a + // `GeneralizedTransactionSet`. + static std::optional + makeFromWire(TxSetPhase phase, Hash const& networkID, + TransactionPhase const& xdrPhase); + + // Creates a new phase from all the transactions in the legacy + // `TransactionSet` XDR. + static std::optional + makeFromWireLegacy(LedgerHeader const& lclHeader, Hash const& networkID, + xdr::xvector const& xdrTxs); + + // Creates a valid empty phase with given `isParallel` flag. + static TxSetPhaseFrame makeEmpty(TxSetPhase phase, bool isParallel); + + // Returns a copy of this phase with transactions sorted for apply. + TxSetPhaseFrame sortedForApply(Hash const& txSetHash) const; + bool checkValid(Application& app, uint64_t lowerBoundCloseTimeOffset, + uint64_t upperBoundCloseTimeOffset) const; + bool checkValidClassic(LedgerHeader const& lclHeader) const; + bool checkValidSoroban(LedgerHeader const& lclHeader, + SorobanNetworkConfig const& sorobanConfig) const; + + bool txsAreValid(Application& app, uint64_t lowerBoundCloseTimeOffset, + uint64_t upperBoundCloseTimeOffset) const; + + TxSetPhase mPhase; + + TxStageFrameList mStages; + std::shared_ptr mInclusionFeeMap; + bool mIsParallel; +}; + // Transaction set that is suitable for being applied to the ledger. // // This is not necessarily a fully *valid* transaction set: further validation @@ -201,51 +366,64 @@ class ApplicableTxSetFrame public: // Returns the base fee for the transaction or std::nullopt when the // transaction is not discounted. - std::optional getTxBaseFee(TransactionFrameBaseConstPtr const& tx, - LedgerHeader const& lclHeader) const; + std::optional + getTxBaseFee(TransactionFrameBaseConstPtr const& tx) const; + + // Gets the phase frame for the given phase in arbitrary order. + TxSetPhaseFrame const& getPhase(TxSetPhase phase) const; - // Gets all the transactions belonging to this frame in arbitrary order. - TxSetTransactions const& getTxsForPhase(TxSetPhase phase) const; + // Gets all the phases of this transaction set with transactions in + // arbitrary order. + std::vector const& getPhases() const; - // Build a list of transaction ready to be applied to the last closed - // ledger, based on the transaction set. + // Gets all the phases of this transaction set, each phase with + // transactions sorted for apply. // - // The order satisfies: - // * transactions for an account are sorted by sequence number (ascending) - // * the order between accounts is randomized - TxSetTransactions getTxsInApplyOrder() const; + // For the generalized transaction sets, the order is defined by shuffling + // all the transactions that are applied sequentially relatively to each + // other using the hash of the transaction set. + // + // For the legacy transaction sets, the apply order satisfies : + // - Transactions for an account are sorted by sequence number (ascending). + // - The order between accounts is randomized. + std::vector const& getPhasesInApplyOrder() const; - // Checks if this tx set frame is valid in the context of the current LCL. + // Checks if this transaction set frame is valid in the context of the + // current LCL. // This can be called when LCL does not match `previousLedgerHash`, but // then validation will never pass. bool checkValid(Application& app, uint64_t lowerBoundCloseTimeOffset, uint64_t upperBoundCloseTimeOffset) const; + // Returns the size of this whole transaction set, or the specified phase + // in operations or transactions (for older protocol versions). size_t size(LedgerHeader const& lh, std::optional phase = std::nullopt) const; - size_t - sizeTx(TxSetPhase phase) const - { - return mTxPhases.at(static_cast(phase)).size(); - } + // Returns the total number of transactions in the given phase. + size_t sizeTx(TxSetPhase phase) const; + // Returns the total number of transactions in this tx set. size_t sizeTxTotal() const; + // Returns the total number of operations in the given phase. + size_t sizeOp(TxSetPhase phase) const; + // Returns the total number of operations in this tx set. + size_t sizeOpTotal() const; + + // Returns whether this transaction set is empty. bool empty() const { return sizeTxTotal() == 0; } + // Returns the number of phases in this tx set. size_t numPhases() const { - return mTxPhases.size(); + return mPhases.size(); } - size_t sizeOp(TxSetPhase phase) const; - size_t sizeOpTotal() const; - // Returns the sum of all fees that this transaction set would take. int64_t getTotalFees(LedgerHeader const& lh) const; @@ -254,29 +432,33 @@ class ApplicableTxSetFrame int64_t getTotalInclusionFees() const; // Returns whether this transaction set is generalized, i.e. representable - // by GeneralizedTransactionSet XDR. + // by `GeneralizedTransactionSet` XDR. bool isGeneralizedTxSet() const; - // Returns a short description of this transaction set. + // Returns a short description of this transaction set for logging. std::string summary() const; + // Returns the hash of this transaction set. Hash const& getContentsHash() const; - // This shouldn't be needed for the regular flows, but is useful + // Converts this transaction set to XDR. + // This shouldn't be exposed for the regular flows, but is useful to expose // to cover XDR roundtrips in tests. #ifndef BUILD_TESTS private: #endif TxSetXDRFrameConstPtr toWireTxSetFrame() const; + std::optional getTxSetSorobanResource() const; private: friend class TxSetXDRFrame; + friend std::pair - makeTxSetFromTransactions(TxSetPhaseTransactions const& txPhases, + makeTxSetFromTransactions(PerPhaseTransactionList const& txPhases, Application& app, uint64_t lowerBoundCloseTimeOffset, uint64_t upperBoundCloseTimeOffset, - TxSetPhaseTransactions& invalidTxsPerPhase + PerPhaseTransactionList& invalidTxsPerPhase #ifdef BUILD_TESTS , bool skipValidation @@ -284,68 +466,43 @@ class ApplicableTxSetFrame ); #ifdef BUILD_TESTS friend std::pair - makeTxSetFromTransactions(TxSetTransactions txs, Application& app, + makeTxSetFromTransactions(TxFrameList txs, Application& app, uint64_t lowerBoundCloseTimeOffset, uint64_t upperBoundCloseTimeOffset, - TxSetTransactions& invalidTxs, + TxFrameList& invalidTxs, bool enforceTxsApplyOrder); #endif ApplicableTxSetFrame(Application& app, LedgerHeaderHistoryEntry const& lclHeader, - TxSetPhaseTransactions const& txs, + std::vector const& phases, std::optional contentsHash); ApplicableTxSetFrame(Application& app, bool isGeneralized, Hash const& previousLedgerHash, - TxSetPhaseTransactions const& txs, + std::vector const& phases, std::optional contentsHash); ApplicableTxSetFrame(ApplicableTxSetFrame const&) = default; ApplicableTxSetFrame(ApplicableTxSetFrame&&) = default; - void computeTxFeesForNonGeneralizedSet(LedgerHeader const& lclHeader); - - bool addTxsFromXdr(Hash const& networkID, - xdr::xvector const& txs, - bool useBaseFee, std::optional baseFee, - TxSetPhase phase); - void applySurgePricing(Application& app); - - void computeTxFeesForNonGeneralizedSet(LedgerHeader const& lclHeader, - int64_t lowestBaseFee, - bool enableLogging); - - void computeTxFees(TxSetPhase phase, LedgerHeader const& ledgerHeader, - SurgePricingLaneConfig const& surgePricingConfig, - std::vector const& lowestLaneFee, - std::vector const& hadTxNotFittingLane); - std::optional getTxSetSorobanResource() const; - - // Get _inclusion_ fee map for a given phase. The map contains lowest base - // fee for each transaction (lowest base fee is identical for all - // transactions in the same lane) - std::unordered_map> const& - getInclusionFeeMap(TxSetPhase phase) const; - - std::unordered_map>& - getInclusionFeeMapMut(TxSetPhase phase); void toXDR(TransactionSet& set) const; void toXDR(GeneralizedTransactionSet& generalizedTxSet) const; bool const mIsGeneralized; Hash const mPreviousLedgerHash; + + // All the phases of this transaction set. + // // There can only be 1 phase (classic) prior to protocol 20. - // Starting protocol 20, there are 2 phases (classic and soroban). - std::vector mTxPhases; + // Starting with protocol 20, there are 2 phases (classic and Soroban). + std::vector const mPhases; - std::vector>> - mPhaseInclusionFeeMap; + // The phases with transactions sorted for apply. + // + // This is `mutable` because we want to do the sorting lazily only for the + // transaction sets that are actually applied. + mutable std::vector mApplyOrderPhases; std::optional mContentsHash; -#ifdef BUILD_TESTS - mutable std::optional mApplyOrderOverride; -#endif }; } // namespace stellar diff --git a/src/herder/TxSetUtils.cpp b/src/herder/TxSetUtils.cpp index 4157cf936c..546044b59f 100644 --- a/src/herder/TxSetUtils.cpp +++ b/src/herder/TxSetUtils.cpp @@ -35,8 +35,8 @@ namespace { // Target use case is to remove a subset of invalid transactions from a TxSet. // I.e. txSet.size() >= txsToRemove.size() -TxSetTransactions -removeTxs(TxSetTransactions const& txs, TxSetTransactions const& txsToRemove) +TxFrameList +removeTxs(TxFrameList const& txs, TxFrameList const& txsToRemove) { UnorderedSet txsToRemoveSet; txsToRemoveSet.reserve(txsToRemove.size()); @@ -45,7 +45,7 @@ removeTxs(TxSetTransactions const& txs, TxSetTransactions const& txsToRemove) std::inserter(txsToRemoveSet, txsToRemoveSet.end()), [](TransactionFrameBasePtr const& tx) { return tx->getFullHash(); }); - TxSetTransactions newTxs; + TxFrameList newTxs; newTxs.reserve(txs.size() - txsToRemove.size()); for (auto const& tx : txs) { @@ -105,17 +105,46 @@ TxSetUtils::hashTxSorter(TransactionFrameBasePtr const& tx1, return tx1->getFullHash() < tx2->getFullHash(); } -TxSetTransactions -TxSetUtils::sortTxsInHashOrder(TxSetTransactions const& transactions) +TxFrameList +TxSetUtils::sortTxsInHashOrder(TxFrameList const& transactions) { ZoneScoped; - TxSetTransactions sortedTxs(transactions); + TxFrameList sortedTxs(transactions); std::sort(sortedTxs.begin(), sortedTxs.end(), TxSetUtils::hashTxSorter); return sortedTxs; } +TxStageFrameList +TxSetUtils::sortParallelTxsInHashOrder(TxStageFrameList const& stages) +{ + ZoneScoped; + TxStageFrameList sortedStages = stages; + for (auto& stage : sortedStages) + { + for (auto& thread : stage) + { + std::sort(thread.begin(), thread.end(), TxSetUtils::hashTxSorter); + } + std::sort(stage.begin(), stage.end(), [](auto const& a, auto const& b) { + if (a.empty() && b.empty()) + { + int t = 0; + } + releaseAssert(!a.empty() && !b.empty()); + return hashTxSorter(a.front(), b.front()); + }); + } + std::sort(sortedStages.begin(), sortedStages.end(), + [](auto const& a, auto const& b) { + releaseAssert(!a.empty() && !b.empty()); + releaseAssert(!a.front().empty() && !b.front().empty()); + return hashTxSorter(a.front().front(), b.front().front()); + }); + return sortedStages; +} + std::vector> -TxSetUtils::buildAccountTxQueues(TxSetTransactions const& txs) +TxSetUtils::buildAccountTxQueues(TxFrameList const& txs) { ZoneScoped; UnorderedMap> actTxMap; @@ -136,8 +165,8 @@ TxSetUtils::buildAccountTxQueues(TxSetTransactions const& txs) return queues; } -TxSetTransactions -TxSetUtils::getInvalidTxList(TxSetTransactions const& txs, Application& app, +TxFrameList +TxSetUtils::getInvalidTxList(TxFrameList const& txs, Application& app, uint64_t lowerBoundCloseTimeOffset, uint64_t upperBoundCloseTimeOffset) { @@ -149,7 +178,7 @@ TxSetUtils::getInvalidTxList(TxSetTransactions const& txs, Application& app, ls.getLedgerHeader().currentToModify().ledgerSeq = app.getLedgerManager().getLastClosedLedgerNum() + 1; - TxSetTransactions invalidTxs; + TxFrameList invalidTxs; for (auto const& tx : txs) { @@ -164,11 +193,11 @@ TxSetUtils::getInvalidTxList(TxSetTransactions const& txs, Application& app, return invalidTxs; } -TxSetTransactions -TxSetUtils::trimInvalid(TxSetTransactions const& txs, Application& app, +TxFrameList +TxSetUtils::trimInvalid(TxFrameList const& txs, Application& app, uint64_t lowerBoundCloseTimeOffset, uint64_t upperBoundCloseTimeOffset, - TxSetTransactions& invalidTxs) + TxFrameList& invalidTxs) { invalidTxs = getInvalidTxList(txs, app, lowerBoundCloseTimeOffset, upperBoundCloseTimeOffset); diff --git a/src/herder/TxSetUtils.h b/src/herder/TxSetUtils.h index ee6abb6242..a7e94421a6 100644 --- a/src/herder/TxSetUtils.h +++ b/src/herder/TxSetUtils.h @@ -34,25 +34,25 @@ class TxSetUtils static bool hashTxSorter(TransactionFrameBasePtr const& tx1, TransactionFrameBasePtr const& tx2); - static TxSetTransactions - sortTxsInHashOrder(TxSetTransactions const& transactions); + static TxFrameList sortTxsInHashOrder(TxFrameList const& transactions); + static TxStageFrameList + sortParallelTxsInHashOrder(TxStageFrameList const& stages); static std::vector> - buildAccountTxQueues(TxSetTransactions const& txs); + buildAccountTxQueues(TxFrameList const& txs); // Returns transactions from a TxSet that are invalid. If // returnEarlyOnFirstInvalidTx is true, return immediately if an invalid // transaction is found (instead of finding all of them), this is useful for // checking if a TxSet is valid. - static TxSetTransactions - getInvalidTxList(TxSetTransactions const& txs, Application& app, - uint64_t lowerBoundCloseTimeOffset, - uint64_t upperBoundCloseTimeOffset); - - static TxSetTransactions trimInvalid(TxSetTransactions const& txs, - Application& app, - uint64_t lowerBoundCloseTimeOffset, - uint64_t upperBoundCloseTimeOffset, - TxSetTransactions& invalidTxs); + static TxFrameList getInvalidTxList(TxFrameList const& txs, + Application& app, + uint64_t lowerBoundCloseTimeOffset, + uint64_t upperBoundCloseTimeOffset); + + static TxFrameList trimInvalid(TxFrameList const& txs, Application& app, + uint64_t lowerBoundCloseTimeOffset, + uint64_t upperBoundCloseTimeOffset, + TxFrameList& invalidTxs); }; // class TxSetUtils } // namespace stellar diff --git a/src/herder/Upgrades.cpp b/src/herder/Upgrades.cpp index 12f7bb0737..4fef0ab3a8 100644 --- a/src/herder/Upgrades.cpp +++ b/src/herder/Upgrades.cpp @@ -1252,6 +1252,11 @@ Upgrades::applyVersionUpgrade(Application& app, AbstractLedgerTxn& ltx, { SorobanNetworkConfig::createCostTypesForV22(ltx, app); } + if (needUpgradeToVersion(PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION, + prevVersion, newVersion)) + { + SorobanNetworkConfig::createLedgerEntriesForParallelSoroban(ltx, app); + } } void diff --git a/src/herder/test/HerderTests.cpp b/src/herder/test/HerderTests.cpp index 9366adb7c3..e92da66c70 100644 --- a/src/herder/test/HerderTests.cpp +++ b/src/herder/test/HerderTests.cpp @@ -352,7 +352,7 @@ testTxSet(uint32 protocolVersion) { auto newUser = TestAccount{*app, getAccount("doesnotexist")}; txs.push_back(newUser.tx({payment(root, 1)})); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions(txs, *app, 0, 0, removed).second; REQUIRE(removed.size() == 1); @@ -364,7 +364,7 @@ testTxSet(uint32 protocolVersion) setSeqNum(std::static_pointer_cast(txPtr), txs[0]->getSeqNum() + 5); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions(txs, *app, 0, 0, removed).second; REQUIRE(removed.size() == 1); @@ -376,7 +376,7 @@ testTxSet(uint32 protocolVersion) txs.back() = accounts.back().tx( {payment(accounts.back().getPublicKey(), 10000)}); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions(txs, *app, 0, 0, removed).second; REQUIRE(removed.size() == 1); @@ -388,7 +388,7 @@ testTxSet(uint32 protocolVersion) std::static_pointer_cast(txs[0]); setMaxTime(tx, UINT64_MAX); tx->clearCached(); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions(txs, *app, 0, 0, removed).second; REQUIRE(removed.size() == 1); @@ -422,8 +422,8 @@ testTxSetWithFeeBumps(uint32 protocolVersion) auto account2 = root.create("a2", minBalance2); auto account3 = root.create("a3", minBalance2); - auto compareTxs = [](TxSetTransactions const& actual, - TxSetTransactions const& expected) { + auto compareTxs = [](TxFrameList const& actual, + TxFrameList const& expected) { auto actualNormalized = actual; auto expectedNormalized = expected; std::sort(actualNormalized.begin(), actualNormalized.end()); @@ -437,7 +437,7 @@ testTxSetWithFeeBumps(uint32 protocolVersion) { auto tx1 = transaction(*app, account1, 1, 1, 100); auto fb1 = feeBump(*app, account2, tx1, minBalance2); - TxSetTransactions invalidTxs; + TxFrameList invalidTxs; auto txSet = makeTxSetFromTransactions({fb1}, *app, 0, 0, invalidTxs); compareTxs(invalidTxs, {fb1}); @@ -449,7 +449,7 @@ testTxSetWithFeeBumps(uint32 protocolVersion) auto fb1 = feeBump(*app, account2, tx1, minBalance2); auto tx2 = transaction(*app, account1, 2, 1, 100); auto fb2 = feeBump(*app, account2, tx2, 200); - TxSetTransactions invalidTxs; + TxFrameList invalidTxs; auto txSet = makeTxSetFromTransactions({fb1, fb2}, *app, 0, 0, invalidTxs); compareTxs(invalidTxs, {fb1, fb2}); @@ -463,7 +463,7 @@ testTxSetWithFeeBumps(uint32 protocolVersion) auto fb1 = feeBump(*app, account2, tx1, 200); auto tx2 = transaction(*app, account1, 2, 1, 100); auto fb2 = feeBump(*app, account2, tx2, minBalance2); - TxSetTransactions invalidTxs; + TxFrameList invalidTxs; auto txSet = makeTxSetFromTransactions({fb1, fb2}, *app, 0, 0, invalidTxs); compareTxs(invalidTxs, {fb2}); @@ -479,7 +479,7 @@ testTxSetWithFeeBumps(uint32 protocolVersion) auto tx2 = transaction(*app, account1, 2, -1, 100); auto fb2 = feeBump(*app, account2, tx2, minBalance2 - minBalance0 - 199); - TxSetTransactions invalidTxs; + TxFrameList invalidTxs; auto txSet = makeTxSetFromTransactions({fb1, fb2}, *app, 0, 0, invalidTxs); compareTxs(invalidTxs, {fb2}); @@ -492,7 +492,7 @@ testTxSetWithFeeBumps(uint32 protocolVersion) auto fb1 = feeBump(*app, account2, tx1, 200); auto tx2 = transaction(*app, account2, 1, 1, 100); auto fb2 = feeBump(*app, account2, tx2, minBalance2); - TxSetTransactions invalidTxs; + TxFrameList invalidTxs; auto txSet = makeTxSetFromTransactions({fb1, fb2}, *app, 0, 0, invalidTxs); compareTxs(invalidTxs, {fb2}); @@ -506,7 +506,7 @@ testTxSetWithFeeBumps(uint32 protocolVersion) auto tx2 = transaction(*app, account2, 1, -1, 100); auto fb2 = feeBump(*app, account2, tx2, minBalance2 - minBalance0 - 199); - TxSetTransactions invalidTxs; + TxFrameList invalidTxs; auto txSet = makeTxSetFromTransactions({fb1, fb2}, *app, 0, 0, invalidTxs); compareTxs(invalidTxs, {fb2}); @@ -522,7 +522,7 @@ testTxSetWithFeeBumps(uint32 protocolVersion) auto tx3 = transaction(*app, account1, 3, 1, 100); auto fb3 = feeBump(*app, account2, tx3, minBalance2 - minBalance0 - 199); - TxSetTransactions invalidTxs; + TxFrameList invalidTxs; auto txSet = makeTxSetFromTransactions({fb1, fb2, fb3}, *app, 0, 0, invalidTxs); compareTxs(invalidTxs, {fb2, fb3}); @@ -588,7 +588,7 @@ TEST_CASE("txset with PreconditionsV2", "[herder][txset]") auto txInvalid = transactionWithV2Precondition( *app, a1, 1, 100, minSeqAgeCond(minGap + 1)); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions({txInvalid}, *app, 0, 0, removed) .second; @@ -658,7 +658,7 @@ TEST_CASE("txset with PreconditionsV2", "[herder][txset]") { auto txInvalid = transactionWithV2Precondition( *app, a2, 1, 100, ledgerBoundsCond(lclNum + 2, 0)); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions({tx1, txInvalid}, *app, 0, 0, removed); REQUIRE(removed.back() == txInvalid); @@ -675,7 +675,7 @@ TEST_CASE("txset with PreconditionsV2", "[herder][txset]") { auto txInvalid = transactionWithV2Precondition( *app, a2, 1, 100, ledgerBoundsCond(0, lclNum)); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions({tx1, txInvalid}, *app, 0, 0, removed); REQUIRE(removed.back() == txInvalid); @@ -704,14 +704,14 @@ TEST_CASE("txset with PreconditionsV2", "[herder][txset]") SECTION("success") { tx->addSignature(root.getSecretKey()); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions({tx}, *app, 0, 0, removed); REQUIRE(removed.empty()); } SECTION("fail") { - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions({tx}, *app, 0, 0, removed); REQUIRE(removed.back() == tx); @@ -730,14 +730,14 @@ TEST_CASE("txset with PreconditionsV2", "[herder][txset]") SECTION("success") { tx->addSignature(a2.getSecretKey()); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions({tx}, *app, 0, 0, removed); REQUIRE(removed.empty()); } SECTION("fail") { - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions({tx}, *app, 0, 0, removed); REQUIRE(removed.back() == tx); @@ -749,7 +749,7 @@ TEST_CASE("txset with PreconditionsV2", "[herder][txset]") auto txDupeSigner = transactionWithV2Precondition(*app, a1, 1, 100, cond); txDupeSigner->addSignature(root.getSecretKey()); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions({txDupeSigner}, *app, 0, 0, removed); REQUIRE(removed.back() == txDupeSigner); @@ -759,7 +759,7 @@ TEST_CASE("txset with PreconditionsV2", "[herder][txset]") { auto rootTx = transactionWithV2Precondition(*app, root, 1, 100, cond); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions({rootTx}, *app, 0, 0, removed); REQUIRE(removed.empty()); @@ -774,14 +774,14 @@ TEST_CASE("txset with PreconditionsV2", "[herder][txset]") { tx->addSignature(root.getSecretKey()); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions({tx}, *app, 0, 0, removed); REQUIRE(removed.empty()); } SECTION("signature missing") { - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions({tx}, *app, 0, 0, removed); REQUIRE(removed.back() == tx); @@ -797,7 +797,7 @@ TEST_CASE("txset with PreconditionsV2", "[herder][txset]") {root.op(payment(a1, 1))}, {root}, cond); - TxSetTransactions removed; + TxFrameList removed; auto txSet = makeTxSetFromTransactions({tx}, *app, 0, 0, removed); REQUIRE(removed.empty()); } @@ -1078,7 +1078,7 @@ TEST_CASE("tx set hits overlay byte limit during construction", }; auto testPhaseWithOverlayLimit = [&](TxSetPhase const& phase) { - TxSetTransactions txs; + TxFrameList txs; size_t totalSize = 0; int txCount = 0; @@ -1089,17 +1089,17 @@ TEST_CASE("tx set hits overlay byte limit during construction", totalSize += xdr::xdr_size(txs.back()->getEnvelope()); } - TxSetPhaseTransactions invalidPhases; + PerPhaseTransactionList invalidPhases; invalidPhases.resize(static_cast(TxSetPhase::PHASE_COUNT)); - TxSetPhaseTransactions phases; + PerPhaseTransactionList phases; if (phase == TxSetPhase::SOROBAN) { - phases = TxSetPhaseTransactions{{}, txs}; + phases = PerPhaseTransactionList{{}, txs}; } else { - phases = TxSetPhaseTransactions{txs, {}}; + phases = PerPhaseTransactionList{txs, {}}; } auto [txSet, applicableTxSet] = @@ -1107,7 +1107,7 @@ TEST_CASE("tx set hits overlay byte limit during construction", REQUIRE(txSet->encodedSize() <= MAX_MESSAGE_SIZE); REQUIRE(invalidPhases[static_cast(phase)].empty()); - auto const& phaseTxs = applicableTxSet->getTxsForPhase(phase); + auto const& phaseTxs = applicableTxSet->getPhase(phase); auto trimmedSize = std::accumulate(phaseTxs.begin(), phaseTxs.end(), size_t(0), [&](size_t a, TransactionFrameBasePtr const& tx) { @@ -1148,7 +1148,7 @@ TEST_CASE("surge pricing", "[herder][txset][soroban]") { auto tx = makeMultiPayment(destAccount, root, 1, 100, 0, 1); - TxSetTransactions invalidTxs; + TxFrameList invalidTxs; auto txSet = makeTxSetFromTransactions({tx}, *app, 0, 0, invalidTxs).second; @@ -1166,10 +1166,10 @@ TEST_CASE("surge pricing", "[herder][txset][soroban]") auto sorobanTx = createUploadWasmTx( *app, root, baseFee, DEFAULT_TEST_RESOURCE_FEE, resources); - TxSetPhaseTransactions invalidTxs; + PerPhaseTransactionList invalidTxs; invalidTxs.resize(static_cast(TxSetPhase::PHASE_COUNT)); auto txSet = makeTxSetFromTransactions( - TxSetPhaseTransactions{{}, {sorobanTx}}, *app, 0, + PerPhaseTransactionList{{}, {sorobanTx}}, *app, 0, 0, invalidTxs) .second; @@ -1222,7 +1222,7 @@ TEST_CASE("surge pricing", "[herder][txset][soroban]") auto generateTxs = [&](std::vector& accounts, SorobanNetworkConfig conf) { - TxSetTransactions txs; + TxFrameList txs; for (auto& acc : accounts) { SorobanResources res; @@ -1287,10 +1287,10 @@ TEST_CASE("surge pricing", "[herder][txset][soroban]") invalidSoroban = createUploadWasmTx( *app, acc2, baseFee, DEFAULT_TEST_RESOURCE_FEE, resources); } - TxSetPhaseTransactions invalidPhases; + PerPhaseTransactionList invalidPhases; invalidPhases.resize(static_cast(TxSetPhase::PHASE_COUNT)); auto txSet = makeTxSetFromTransactions( - TxSetPhaseTransactions{{tx}, {invalidSoroban}}, + PerPhaseTransactionList{{tx}, {invalidSoroban}}, *app, 0, 0, invalidPhases) .second; @@ -1303,11 +1303,11 @@ TEST_CASE("surge pricing", "[herder][txset][soroban]") } SECTION("classic and soroban fit") { - TxSetPhaseTransactions invalidPhases; + PerPhaseTransactionList invalidPhases; invalidPhases.resize(static_cast(TxSetPhase::PHASE_COUNT)); auto txSet = makeTxSetFromTransactions( - TxSetPhaseTransactions{{tx}, {sorobanTx}}, *app, 0, - 0, invalidPhases) + PerPhaseTransactionList{{tx}, {sorobanTx}}, *app, + 0, 0, invalidPhases) .second; // Everything fits @@ -1317,11 +1317,11 @@ TEST_CASE("surge pricing", "[herder][txset][soroban]") } SECTION("classic and soroban in the same phase are rejected") { - TxSetPhaseTransactions invalidPhases; + PerPhaseTransactionList invalidPhases; invalidPhases.resize(1); REQUIRE_THROWS_AS(makeTxSetFromTransactions( - TxSetPhaseTransactions{{tx, sorobanTx}}, *app, - 0, 0, invalidPhases), + PerPhaseTransactionList{{tx, sorobanTx}}, + *app, 0, 0, invalidPhases), std::runtime_error); } SECTION("soroban surge pricing, classic unaffected") @@ -1329,21 +1329,23 @@ TEST_CASE("surge pricing", "[herder][txset][soroban]") // Another soroban tx with higher fee, which will be selected auto sorobanTxHighFee = createUploadWasmTx( *app, acc3, baseFee * 2, DEFAULT_TEST_RESOURCE_FEE, resources); - TxSetPhaseTransactions invalidPhases; + PerPhaseTransactionList invalidPhases; invalidPhases.resize(static_cast(TxSetPhase::PHASE_COUNT)); - auto txSet = - makeTxSetFromTransactions( - TxSetPhaseTransactions{{tx}, {sorobanTx, sorobanTxHighFee}}, - *app, 0, 0, invalidPhases) - .second; + auto txSet = makeTxSetFromTransactions( + PerPhaseTransactionList{ + {tx}, {sorobanTx, sorobanTxHighFee}}, + *app, 0, 0, invalidPhases) + .second; REQUIRE(std::all_of(invalidPhases.begin(), invalidPhases.end(), [](auto const& txs) { return txs.empty(); })); REQUIRE(txSet->sizeTxTotal() == 2); - auto const& classicTxs = txSet->getTxsForPhase(TxSetPhase::CLASSIC); + auto const& classicTxs = + txSet->getPhase(TxSetPhase::CLASSIC).getSequentialTxs(); REQUIRE(classicTxs.size() == 1); REQUIRE(classicTxs[0]->getFullHash() == tx->getFullHash()); - auto const& sorobanTxs = txSet->getTxsForPhase(TxSetPhase::SOROBAN); + auto const& sorobanTxs = + txSet->getPhase(TxSetPhase::SOROBAN).getSequentialTxs(); REQUIRE(sorobanTxs.size() == 1); REQUIRE(sorobanTxs[0]->getFullHash() == sorobanTxHighFee->getFullHash()); @@ -1365,11 +1367,11 @@ TEST_CASE("surge pricing", "[herder][txset][soroban]") auto smallSorobanLowFee = createUploadWasmTx( *app, acc4, baseFee / 10, DEFAULT_TEST_RESOURCE_FEE, resources); - TxSetPhaseTransactions invalidPhases; + PerPhaseTransactionList invalidPhases; invalidPhases.resize(static_cast(TxSetPhase::PHASE_COUNT)); auto txSet = makeTxSetFromTransactions( - TxSetPhaseTransactions{ + PerPhaseTransactionList{ {tx}, {sorobanTxHighFee, smallSorobanLowFee, sorobanTx}}, *app, 0, 0, invalidPhases) @@ -1378,10 +1380,11 @@ TEST_CASE("surge pricing", "[herder][txset][soroban]") REQUIRE(std::all_of(invalidPhases.begin(), invalidPhases.end(), [](auto const& txs) { return txs.empty(); })); REQUIRE(txSet->sizeTxTotal() == 3); - auto const& classicTxs = txSet->getTxsForPhase(TxSetPhase::CLASSIC); + auto const& classicTxs = + txSet->getPhase(TxSetPhase::CLASSIC).getSequentialTxs(); REQUIRE(classicTxs.size() == 1); REQUIRE(classicTxs[0]->getFullHash() == tx->getFullHash()); - for (auto const& t : txSet->getTxsForPhase(TxSetPhase::SOROBAN)) + for (auto const& t : txSet->getPhase(TxSetPhase::SOROBAN)) { // smallSorobanLowFee was picked over sorobanTx to fill the gap bool pickedGap = @@ -1397,11 +1400,11 @@ TEST_CASE("surge pricing", "[herder][txset][soroban]") { SECTION("iteration " + std::to_string(i)) { - TxSetPhaseTransactions invalidPhases; + PerPhaseTransactionList invalidPhases; invalidPhases.resize( static_cast(TxSetPhase::PHASE_COUNT)); auto txSet = makeTxSetFromTransactions( - TxSetPhaseTransactions{ + PerPhaseTransactionList{ {tx}, generateTxs(accounts, conf)}, *app, 0, 0, invalidPhases) .second; @@ -1410,9 +1413,9 @@ TEST_CASE("surge pricing", "[herder][txset][soroban]") invalidPhases.begin(), invalidPhases.end(), [](auto const& txs) { return txs.empty(); })); auto const& classicTxs = - txSet->getTxsForPhase(TxSetPhase::CLASSIC); + txSet->getPhase(TxSetPhase::CLASSIC).getSequentialTxs(); auto const& sorobanTxs = - txSet->getTxsForPhase(TxSetPhase::SOROBAN); + txSet->getPhase(TxSetPhase::SOROBAN).getSequentialTxs(); REQUIRE(classicTxs.size() == 1); REQUIRE(classicTxs[0]->getFullHash() == tx->getFullHash()); // Depending on resources generated for each tx, can only @@ -1425,7 +1428,7 @@ TEST_CASE("surge pricing", "[herder][txset][soroban]") } SECTION("tx sets over limits are invalid") { - TxSetTransactions txs = generateTxs(accounts, conf); + TxFrameList txs = generateTxs(accounts, conf); auto txSet = testtxset::makeNonValidatedGeneralizedTxSet( {{}, {std::make_pair(500, txs)}}, *app, @@ -1453,12 +1456,6 @@ TEST_CASE("surge pricing with DEX separation", "[herder][txset]") VirtualClock clock; Application::pointer app = createTestApplication(clock, cfg); - LedgerHeader lhCopy; - { - LedgerTxn ltx(app->getLedgerTxnRoot()); - lhCopy = ltx.loadHeader().current(); - } - auto root = TestAccount::createRoot(*app); auto accountA = root.create("accountA", 5000000000); @@ -1478,8 +1475,9 @@ TEST_CASE("surge pricing with DEX separation", "[herder][txset]") int64_t expectedDexBaseFee) { auto txSet = makeTxSetFromTransactions(txs, *app, 0, 0).second; size_t cntA = 0, cntB = 0, cntC = 0, cntD = 0; - auto resTxs = txSet->getTxsInApplyOrder(); - for (auto const& tx : resTxs) + auto const& phases = txSet->getPhasesInApplyOrder(); + + for (auto const& tx : phases[static_cast(TxSetPhase::CLASSIC)]) { if (tx->getSourceID() == accountA.getPublicKey()) { @@ -1506,7 +1504,7 @@ TEST_CASE("surge pricing with DEX separation", "[herder][txset]") REQUIRE(seqNumD == tx->getSeqNum()); } - auto baseFee = txSet->getTxBaseFee(tx, lhCopy); + auto baseFee = txSet->getTxBaseFee(tx); REQUIRE(baseFee); if (tx->hasDexOperations()) { @@ -1517,6 +1515,7 @@ TEST_CASE("surge pricing with DEX separation", "[herder][txset]") REQUIRE(*baseFee == expectedNonDexBaseFee); } } + REQUIRE(cntA == expectedTxsA); REQUIRE(cntB == expectedTxsB); REQUIRE(cntC == expectedTxsC); @@ -1686,19 +1685,21 @@ TEST_CASE("surge pricing with DEX separation holds invariants", auto txs = genTxs(txCountDistr(Catch::rng())); auto txSet = makeTxSetFromTransactions(txs, *app, 0, 0).second; - auto resTxs = txSet->getTxsInApplyOrder(); + auto const& phases = txSet->getPhasesInApplyOrder(); std::array opsCounts{}; std::array baseFees{}; - for (auto const& resTx : resTxs) + + for (auto const& resTx : + phases[static_cast(TxSetPhase::CLASSIC)]) { auto isDex = static_cast(resTx->hasDexOperations()); opsCounts[isDex] += resTx->getNumOperations(); - auto baseFee = txSet->getTxBaseFee(resTx, lhCopy); + auto baseFee = txSet->getTxBaseFee(resTx); REQUIRE(baseFee); if (baseFees[isDex] != 0) { - // All base fees should be the same among the transaction - // categories. + // All base fees should be the same among the + // transaction categories. REQUIRE(baseFees[isDex] == *baseFee); } else @@ -1706,6 +1707,7 @@ TEST_CASE("surge pricing with DEX separation holds invariants", baseFees[isDex] = *baseFee; } } + REQUIRE(opsCounts[0] + opsCounts[1] <= cfg.TESTING_UPGRADE_MAX_TX_SET_SIZE); if (maxDexOps) @@ -2211,7 +2213,7 @@ testSCPDriver(uint32 protocolVersion, uint32_t maxTxSetSize, size_t expectedOps) tx->addSignature(root.getSecretKey()); auto [txSet, applicableTxSet] = testtxset::makeNonValidatedTxSetBasedOnLedgerVersion( - protocolVersion, {tx}, *app, + {tx}, *app, app->getLedgerManager().getLastClosedLedgerHeader().hash); // Build a StellarValue containing the transaction set we just @@ -2232,10 +2234,11 @@ testSCPDriver(uint32 protocolVersion, uint32_t maxTxSetSize, size_t expectedOps) // makeTxSetFromTransactions() trims the transaction if // and only if we expect it to be invalid. auto closeTimeOffset = nextCloseTime - lclCloseTime; - TxSetTransactions removed; + TxFrameList removed; TxSetUtils::trimInvalid( - applicableTxSet->getTxsForPhase(TxSetPhase::CLASSIC), *app, - closeTimeOffset, closeTimeOffset, removed); + applicableTxSet->getPhase(TxSetPhase::CLASSIC) + .getSequentialTxs(), + *app, closeTimeOffset, closeTimeOffset, removed); REQUIRE(removed.size() == (expectValid ? 0 : 1)); }; @@ -3147,18 +3150,40 @@ TEST_CASE("soroban txs each parameter surge priced", "[soroban][herder]") lclHeader.scpValue.txSetHash); GeneralizedTransactionSet xdrTxSet; txSet->toXDR(xdrTxSet); - auto const& components = - xdrTxSet.v1TxSet() - .phases.at(static_cast(TxSetPhase::SOROBAN)) - .v0Components(); - if (!components.empty()) + auto const& phase = xdrTxSet.v1TxSet().phases.at( + static_cast(TxSetPhase::SOROBAN)); + std::optional baseFee; + switch (phase.v()) { - auto baseFee = - components.at(0).txsMaybeDiscountedFee().baseFee; - hadSorobanSurgePricing = hadSorobanSurgePricing || - (baseFee && *baseFee > 100); + case 0: + if (!phase.v0Components().empty() && + phase.v0Components() + .at(0) + .txsMaybeDiscountedFee() + .baseFee) + { + + baseFee = *phase.v0Components() + .at(0) + .txsMaybeDiscountedFee() + .baseFee; + } + break; +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + case 1: + if (phase.parallelTxsComponent().baseFee) + { + baseFee = *phase.parallelTxsComponent().baseFee; + } + break; +#endif + default: + releaseAssert(false); } + hadSorobanSurgePricing = + hadSorobanSurgePricing || (baseFee && *baseFee > 100); + return loadGenDone.count() > currLoadGenCount && secondLoadGenDone.count() > secondLoadGenCount; }, @@ -4406,7 +4431,7 @@ externalize(SecretKey const& sk, LedgerManager& lm, HerderImpl& herder, auto classicTxs = txs; - TxSetTransactions sorobanTxs; + TxFrameList sorobanTxs; for (auto it = classicTxs.begin(); it != classicTxs.end();) { if ((*it)->isSoroban()) @@ -4420,7 +4445,7 @@ externalize(SecretKey const& sk, LedgerManager& lm, HerderImpl& herder, } } - TxSetPhaseTransactions txsPhases{classicTxs}; + PerPhaseTransactionList txsPhases{classicTxs}; txsPhases.emplace_back(sorobanTxs); @@ -4474,12 +4499,11 @@ TEST_CASE("do not flood invalid transactions", "[herder]") auto const& lhhe = lm.getLastClosedLedgerHeader(); auto txs = tq.getTransactions(lhhe.header); - auto txSet = makeTxSetFromTransactions(txs, *app, 0, 0).second; - REQUIRE(txSet->sizeTxTotal() == 1); - REQUIRE( - txSet->getTxsForPhase(TxSetPhase::CLASSIC).front()->getContentsHash() == - tx1a->getContentsHash()); - REQUIRE(txSet->checkValid(*app, 0, 0)); + auto [_, applicableTxSet] = makeTxSetFromTransactions(txs, *app, 0, 0); + REQUIRE(applicableTxSet->sizeTxTotal() == 1); + REQUIRE((*applicableTxSet->getPhase(TxSetPhase::CLASSIC).begin()) + ->getContentsHash() == tx1a->getContentsHash()); + REQUIRE(applicableTxSet->checkValid(*app, 0, 0)); } TEST_CASE("do not flood too many soroban transactions", diff --git a/src/herder/test/TestTxSetUtils.cpp b/src/herder/test/TestTxSetUtils.cpp index e27c43c3d5..302be58dde 100644 --- a/src/herder/test/TestTxSetUtils.cpp +++ b/src/herder/test/TestTxSetUtils.cpp @@ -3,6 +3,7 @@ // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 #include "herder/test/TestTxSetUtils.h" +#include "ledger/LedgerManager.h" #include "ledger/LedgerTxn.h" #include "main/Application.h" #include "util/ProtocolVersion.h" @@ -30,13 +31,26 @@ makeTxSetXDR(std::vector const& txs, } GeneralizedTransactionSet -makeGeneralizedTxSetXDR(std::vector const& txsPerBaseFeePhases, - Hash const& previousLedgerHash) +makeGeneralizedTxSetXDR(std::vector const& phases, + Hash const& previousLedgerHash, + bool useParallelSorobanPhase) { GeneralizedTransactionSet xdrTxSet(1); - for (auto& txsPerBaseFee : txsPerBaseFeePhases) + for (size_t i = 0; i < phases.size(); ++i) { - auto normalizedTxsPerBaseFee = txsPerBaseFee; + releaseAssert(i < static_cast(TxSetPhase::PHASE_COUNT)); + auto const& phase = phases[i]; + bool isParallelSorobanPhase = false; +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + if (useParallelSorobanPhase && + i == static_cast(TxSetPhase::SOROBAN)) + { + releaseAssert(phase.size() <= 1); + isParallelSorobanPhase = true; + } +#endif + + auto normalizedTxsPerBaseFee = phase; std::sort(normalizedTxsPerBaseFee.begin(), normalizedTxsPerBaseFee.end()); for (auto& [_, txs] : normalizedTxsPerBaseFee) @@ -45,19 +59,48 @@ makeGeneralizedTxSetXDR(std::vector const& txsPerBaseFeePhases, } xdrTxSet.v1TxSet().previousLedgerHash = previousLedgerHash; - auto& phase = xdrTxSet.v1TxSet().phases.emplace_back(); + auto& xdrPhase = xdrTxSet.v1TxSet().phases.emplace_back(); + if (isParallelSorobanPhase) + { + xdrPhase.v(1); + } for (auto const& [baseFee, txs] : normalizedTxsPerBaseFee) { - auto& component = phase.v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - if (baseFee) + if (isParallelSorobanPhase) { - component.txsMaybeDiscountedFee().baseFee.activate() = *baseFee; +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + auto& component = xdrPhase.parallelTxsComponent(); + if (baseFee) + { + component.baseFee.activate() = *baseFee; + } + if (!txs.empty()) + { + auto& thread = + component.executionStages.emplace_back().emplace_back(); + for (auto const& tx : txs) + { + thread.emplace_back(tx->getEnvelope()); + } + } +#else + releaseAssert(false); +#endif } - auto& componentTxs = component.txsMaybeDiscountedFee().txs; - for (auto const& tx : txs) + else { - componentTxs.emplace_back(tx->getEnvelope()); + auto& component = xdrPhase.v0Components().emplace_back( + TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); + if (baseFee) + { + component.txsMaybeDiscountedFee().baseFee.activate() = + *baseFee; + } + auto& componentTxs = component.txsMaybeDiscountedFee().txs; + for (auto const& tx : txs) + { + componentTxs.emplace_back(tx->getEnvelope()); + } } } } @@ -80,17 +123,24 @@ makeNonValidatedGeneralizedTxSet( std::vector const& txsPerBaseFee, Application& app, Hash const& previousLedgerHash) { - auto xdrTxSet = makeGeneralizedTxSetXDR(txsPerBaseFee, previousLedgerHash); + bool useParallelSorobanPhase = protocolVersionStartsFrom( + app.getLedgerManager().getLastClosedLedgerHeader().header.ledgerVersion, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION); + auto xdrTxSet = makeGeneralizedTxSetXDR(txsPerBaseFee, previousLedgerHash, + useParallelSorobanPhase); auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); return std::make_pair(txSet, txSet->prepareForApply(app)); } std::pair makeNonValidatedTxSetBasedOnLedgerVersion( - uint32_t ledgerVersion, std::vector const& txs, - Application& app, Hash const& previousLedgerHash) + std::vector const& txs, Application& app, + Hash const& previousLedgerHash) { - if (protocolVersionStartsFrom(ledgerVersion, SOROBAN_PROTOCOL_VERSION)) + if (protocolVersionStartsFrom(app.getLedgerManager() + .getLastClosedLedgerHeader() + .header.ledgerVersion, + SOROBAN_PROTOCOL_VERSION)) { return makeNonValidatedGeneralizedTxSet( {{std::make_pair(100LL, txs)}, {}}, app, previousLedgerHash); diff --git a/src/herder/test/TestTxSetUtils.h b/src/herder/test/TestTxSetUtils.h index be9b7eac1e..4aca1428bd 100644 --- a/src/herder/test/TestTxSetUtils.h +++ b/src/herder/test/TestTxSetUtils.h @@ -21,7 +21,7 @@ makeNonValidatedGeneralizedTxSet( std::pair makeNonValidatedTxSetBasedOnLedgerVersion( - uint32_t ledgerVersion, std::vector const& txs, - Application& app, Hash const& previousLedgerHash); + std::vector const& txs, Application& app, + Hash const& previousLedgerHash); } // namespace testtxset } // namespace stellar diff --git a/src/herder/test/TransactionQueueTests.cpp b/src/herder/test/TransactionQueueTests.cpp index 53760d1e8b..922016a58e 100644 --- a/src/herder/test/TransactionQueueTests.cpp +++ b/src/herder/test/TransactionQueueTests.cpp @@ -202,7 +202,7 @@ class TransactionQueueTest REQUIRE(fees == expectedFees); - TxSetTransactions expectedTxs; + TxFrameList expectedTxs; size_t totOps = 0; for (auto const& accountState : state.mAccountStates) { diff --git a/src/herder/test/TxSetTests.cpp b/src/herder/test/TxSetTests.cpp index 79a8488880..1b6ad18389 100644 --- a/src/herder/test/TxSetTests.cpp +++ b/src/herder/test/TxSetTests.cpp @@ -12,6 +12,9 @@ #include "test/TestUtils.h" #include "test/TxTests.h" #include "test/test.h" +#include "transactions/MutableTransactionResult.h" +#include "transactions/TransactionUtils.h" +#include "transactions/test/SorobanTxTestUtils.h" #include "util/ProtocolVersion.h" namespace stellar @@ -472,14 +475,17 @@ TEST_CASE("generalized tx set XDR validation", "[txset]") } } -TEST_CASE("generalized tx set XDR conversion", "[txset]") +void +testGeneralizedTxSetXDRConversion(ProtocolVersion protocolVersion) { VirtualClock clock; auto cfg = getTestConfig(); - cfg.LEDGER_PROTOCOL_VERSION = - static_cast(SOROBAN_PROTOCOL_VERSION); + cfg.LEDGER_PROTOCOL_VERSION = static_cast(protocolVersion); cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION = - static_cast(SOROBAN_PROTOCOL_VERSION); + static_cast(protocolVersion); + bool isParallelSoroban = protocolVersionStartsFrom( + cfg.LEDGER_PROTOCOL_VERSION, PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION); + Application::pointer app = createTestApplication(clock, cfg); overrideSorobanNetworkConfigForTest(*app); modifySorobanNetworkConfig(*app, [](SorobanNetworkConfig& sorobanCfg) { @@ -523,6 +529,7 @@ TEST_CASE("generalized tx set XDR conversion", "[txset]") TransactionMode::READ_ONLY_WITHOUT_SQL_TXN); applicableFrame = txSetFrame->prepareForApply(*app); } + REQUIRE(applicableFrame->checkValid(*app, 0, 0)); GeneralizedTransactionSet newXdr; applicableFrame->toWireTxSetFrame()->toXDR(newXdr); @@ -650,17 +657,42 @@ TEST_CASE("generalized tx set XDR conversion", "[txset]") GeneralizedTransactionSet txSetXdr; txSet->toXDR(txSetXdr); REQUIRE(txSetXdr.v1TxSet().phases.size() == 2); - for (auto const& phase : txSetXdr.v1TxSet().phases) + for (auto i = 0; i < txSetXdr.v1TxSet().phases.size(); ++i) { + auto const& phase = txSetXdr.v1TxSet().phases[i]; + // Base inclusion fee is 100 for all phases since no // surge pricing kicked in - REQUIRE(phase.v0Components().size() == 1); - REQUIRE(*phase.v0Components()[0] - .txsMaybeDiscountedFee() - .baseFee == lclHeader.header.baseFee); - REQUIRE(phase.v0Components()[0] - .txsMaybeDiscountedFee() - .txs.size() == 5); + if (i == static_cast(TxSetPhase::SOROBAN) && + isParallelSoroban) + { +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + REQUIRE(phase.v() == 1); + REQUIRE(*phase.parallelTxsComponent().baseFee == + lclHeader.header.baseFee); + REQUIRE(phase.parallelTxsComponent() + .executionStages.size() == 1); + REQUIRE(phase.parallelTxsComponent() + .executionStages[0] + .size() == 1); + REQUIRE(phase.parallelTxsComponent() + .executionStages[0][0] + .size() == 5); +#else + releaseAssert(false); +#endif + } + else + { + REQUIRE(phase.v() == 0); + REQUIRE(phase.v0Components().size() == 1); + REQUIRE(*phase.v0Components()[0] + .txsMaybeDiscountedFee() + .baseFee == lclHeader.header.baseFee); + REQUIRE(phase.v0Components()[0] + .txsMaybeDiscountedFee() + .txs.size() == 5); + } } checkXdrRoundtrip(txSetXdr); } @@ -679,19 +711,42 @@ TEST_CASE("generalized tx set XDR conversion", "[txset]") GeneralizedTransactionSet txSetXdr; txSet->toXDR(txSetXdr); REQUIRE(txSetXdr.v1TxSet().phases.size() == 2); - for (int i = 0; i < txSetXdr.v1TxSet().phases.size(); i++) + for (auto i = 0; i < txSetXdr.v1TxSet().phases.size(); ++i) { auto const& phase = txSetXdr.v1TxSet().phases[i]; auto expectedBaseFee = i == 0 ? lclHeader.header.baseFee : higherFeeSorobanTxs[0]->getInclusionFee(); - REQUIRE(phase.v0Components().size() == 1); - REQUIRE(*phase.v0Components()[0] - .txsMaybeDiscountedFee() - .baseFee == expectedBaseFee); - REQUIRE(phase.v0Components()[0] - .txsMaybeDiscountedFee() - .txs.size() == 5); + if (i == static_cast(TxSetPhase::SOROBAN) && + isParallelSoroban) + { +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + REQUIRE(phase.v() == 1); + REQUIRE(*phase.parallelTxsComponent().baseFee == + expectedBaseFee); + REQUIRE(phase.parallelTxsComponent() + .executionStages.size() == 1); + REQUIRE(phase.parallelTxsComponent() + .executionStages[0] + .size() == 1); + REQUIRE(phase.parallelTxsComponent() + .executionStages[0][0] + .size() == 5); +#else + releaseAssert(false); +#endif + } + else + { + REQUIRE(phase.v() == 0); + REQUIRE(phase.v0Components().size() == 1); + REQUIRE(*phase.v0Components()[0] + .txsMaybeDiscountedFee() + .baseFee == expectedBaseFee); + REQUIRE(phase.v0Components()[0] + .txsMaybeDiscountedFee() + .txs.size() == 5); + } } checkXdrRoundtrip(txSetXdr); } @@ -716,15 +771,31 @@ TEST_CASE("generalized tx set XDR conversion", "[txset]") } } +TEST_CASE("generalized tx set XDR conversion", + "[txset]"){SECTION("soroban protocol version"){ + testGeneralizedTxSetXDRConversion(SOROBAN_PROTOCOL_VERSION); +} +SECTION("current protocol version") +{ + testGeneralizedTxSetXDRConversion( + static_cast(Config::CURRENT_LEDGER_PROTOCOL_VERSION)); +} +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +SECTION("parallel soroban protocol version") +{ + testGeneralizedTxSetXDRConversion(PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION); +} +#endif +} + TEST_CASE("generalized tx set with multiple txs per source account", "[txset][soroban]") { VirtualClock clock; auto cfg = getTestConfig(); - cfg.LEDGER_PROTOCOL_VERSION = - static_cast(SOROBAN_PROTOCOL_VERSION); + cfg.LEDGER_PROTOCOL_VERSION = Config::CURRENT_LEDGER_PROTOCOL_VERSION; cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION = - static_cast(SOROBAN_PROTOCOL_VERSION); + Config::CURRENT_LEDGER_PROTOCOL_VERSION; Application::pointer app = createTestApplication(clock, cfg); auto root = TestAccount::createRoot(*app); int accountId = 1; @@ -814,10 +885,9 @@ TEST_CASE("generalized tx set fees", "[txset][soroban]") { VirtualClock clock; auto cfg = getTestConfig(); - cfg.LEDGER_PROTOCOL_VERSION = - static_cast(SOROBAN_PROTOCOL_VERSION); + cfg.LEDGER_PROTOCOL_VERSION = Config::CURRENT_LEDGER_PROTOCOL_VERSION; cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION = - static_cast(SOROBAN_PROTOCOL_VERSION); + Config::CURRENT_LEDGER_PROTOCOL_VERSION; Application::pointer app = createTestApplication(clock, cfg); overrideSorobanNetworkConfigForTest(*app); @@ -866,6 +936,35 @@ TEST_CASE("generalized tx set fees", "[txset][soroban]") SECTION("valid txset") { + testtxset::ComponentPhases sorobanTxs; + bool isParallelSoroban = + protocolVersionStartsFrom(Config::CURRENT_LEDGER_PROTOCOL_VERSION, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION); + if (isParallelSoroban) + { + sorobanTxs = {std::make_pair( + 1000, std::vector{ + createTx(1, 1250, /* isSoroban */ true), + createTx(1, 1000, /* isSoroban */ true), + createTx(1, 1200, /* isSoroban */ true)})}; + } + else + { + sorobanTxs = { + std::make_pair(500, + std::vector{ + createTx(1, 1000, /* isSoroban */ true), + createTx(1, 500, /* isSoroban */ true)}), + std::make_pair(1000, + std::vector{ + createTx(1, 1250, /* isSoroban */ true), + createTx(1, 1000, /* isSoroban */ true), + createTx(1, 1200, /* isSoroban */ true)}), + std::make_pair(std::nullopt, + std::vector{ + createTx(1, 5000, /* isSoroban */ true), + createTx(1, 20000, /* isSoroban */ true)})}; + } auto txSet = testtxset::makeNonValidatedGeneralizedTxSet( {{std::make_pair(500, @@ -878,20 +977,7 @@ TEST_CASE("generalized tx set fees", "[txset][soroban]") std::make_pair(std::nullopt, std::vector{ createTx(2, 10000), createTx(5, 100000)})}, - {std::make_pair(500, - std::vector{ - createTx(1, 1000, /* isSoroban */ true), - createTx(1, 500, /* isSoroban */ true)}), - std::make_pair(1000, - std::vector{ - createTx(1, 1250, /* isSoroban */ true), - createTx(1, 1000, /* isSoroban */ true), - createTx(1, 1200, /* isSoroban */ true)}), - std::make_pair( - std::nullopt, - std::vector{ - createTx(1, 5000, /* isSoroban */ true), - createTx(1, 20000, /* isSoroban */ true)})}}, + sorobanTxs}, *app, app->getLedgerManager().getLastClosedLedgerHeader().hash) .second; @@ -899,18 +985,23 @@ TEST_CASE("generalized tx set fees", "[txset][soroban]") for (auto i = 0; i < static_cast(TxSetPhase::PHASE_COUNT); ++i) { std::vector> fees; - for (auto const& tx : - txSet->getTxsForPhase(static_cast(i))) + for (auto const& tx : txSet->getPhase(static_cast(i))) { - fees.push_back( - txSet->getTxBaseFee(tx, app->getLedgerManager() - .getLastClosedLedgerHeader() - .header)); + fees.push_back(txSet->getTxBaseFee(tx)); } std::sort(fees.begin(), fees.end()); - REQUIRE(fees == std::vector>{ - std::nullopt, std::nullopt, 500, 500, 1000, - 1000, 1000}); + if (isParallelSoroban && + i == static_cast(TxSetPhase::SOROBAN)) + { + REQUIRE(fees == + std::vector>{1000, 1000, 1000}); + } + else + { + REQUIRE(fees == std::vector>{ + std::nullopt, std::nullopt, 500, 500, 1000, + 1000, 1000}); + } } } SECTION("tx with too low discounted fee") @@ -978,5 +1069,318 @@ TEST_CASE("generalized tx set fees", "[txset][soroban]") } } +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +TEST_CASE("parallel tx set building", "[txset][soroban]") +{ + uint32_t const STAGE_COUNT = 4; + uint32_t const THREAD_COUNT = 8; + + VirtualClock clock; + auto cfg = getTestConfig(); + cfg.LEDGER_PROTOCOL_VERSION = + static_cast(PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION); + cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION = + static_cast(PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION); + cfg.SOROBAN_PHASE_STAGE_COUNT = STAGE_COUNT; + Application::pointer app = createTestApplication(clock, cfg); + overrideSorobanNetworkConfigForTest(*app); + modifySorobanNetworkConfig( + *app, [THREAD_COUNT](SorobanNetworkConfig& sorobanCfg) { + sorobanCfg.mLedgerMaxInstructions = 400'000'000; + sorobanCfg.mLedgerMaxReadLedgerEntries = 3000; + sorobanCfg.mLedgerMaxWriteLedgerEntries = 2000; + sorobanCfg.mLedgerMaxReadBytes = 1'000'000; + sorobanCfg.mLedgerMaxWriteBytes = 100'000; + sorobanCfg.mLedgerMaxTxCount = 1000; + sorobanCfg.mLedgerMaxParallelThreads = THREAD_COUNT; + }); + auto root = TestAccount::createRoot(*app); + std::map accounts; + int accountId = 1; + SCAddress contract(SC_ADDRESS_TYPE_CONTRACT); + + auto generateKey = [&contract](int i) { + return stellar::contractDataKey( + contract, txtest::makeU32(i), + i % 2 == 0 ? ContractDataDurability::PERSISTENT + : ContractDataDurability::TEMPORARY); + }; + + auto createTx = [&](int instructions, std::vector const& roKeys, + std::vector rwKeys, int64_t inclusionFee = 1000, + int readBytes = 1000, int writeBytes = 100) { + auto it = accounts.find(accountId); + if (it == accounts.end()) + { + it = accounts + .emplace(accountId, root.create(std::to_string(accountId), + 1'000'000'000)) + .first; + } + ++accountId; + auto source = it->second; + SorobanResources resources; + resources.instructions = instructions; + resources.readBytes = readBytes; + resources.writeBytes = writeBytes; + for (auto roKeyId : roKeys) + { + resources.footprint.readOnly.push_back(generateKey(roKeyId)); + } + for (auto rwKeyId : rwKeys) + { + resources.footprint.readWrite.push_back(generateKey(rwKeyId)); + } + auto resourceFee = sorobanResourceFee(*app, resources, 10'000, 40); + // It doesn't really matter what tx does as we're only interested in + // its resources. + auto tx = createUploadWasmTx(*app, source, inclusionFee, resourceFee, + resources); + LedgerSnapshot ls(*app); + auto res = tx->checkValid(*app, ls, 0, 0, 0); + if (!res->isSuccess()) + { + int t = 0; + } + REQUIRE(tx->checkValid(*app, ls, 0, 0, 0)->isSuccess()); + + return tx; + }; + + auto validateShape = [&](ApplicableTxSetFrame const& txSet, + size_t stageCount, size_t threadsPerStage, + size_t txsPerThread) { + auto const& phase = + txSet.getPhase(TxSetPhase::SOROBAN).getParallelStages(); + + REQUIRE(phase.size() == stageCount); + for (auto const& stage : phase) + { + REQUIRE(stage.size() == threadsPerStage); + for (auto const& thread : stage) + { + REQUIRE(thread.size() == txsPerThread); + } + } + }; + + auto validateBaseFee = [&](ApplicableTxSetFrame const& txSet, + int64_t baseFee) { + for (auto const& tx : txSet.getPhase(TxSetPhase::SOROBAN)) + { + + REQUIRE(txSet.getTxBaseFee(tx) == baseFee); + } + }; + + SECTION("no conflicts") + { + SECTION("single stage") + { + std::vector sorobanTxs; + for (int i = 0; i < THREAD_COUNT; ++i) + { + sorobanTxs.push_back(createTx(100'000'000, {4 * i, 4 * i + 1}, + {4 * i + 2, 4 * i + 3})); + } + PerPhaseTransactionList phases = {{}, sorobanTxs}; + auto [_, txSet] = makeTxSetFromTransactions(phases, *app, 0, 0); + validateShape(*txSet, 1, THREAD_COUNT, 1); + validateBaseFee(*txSet, 100); + } + SECTION("all stages") + { + std::vector sorobanTxs; + for (int i = 0; i < STAGE_COUNT * THREAD_COUNT; ++i) + { + sorobanTxs.push_back(createTx(100'000'000, {4 * i, 4 * i + 1}, + {4 * i + 2, 4 * i + 3})); + } + PerPhaseTransactionList phases = {{}, sorobanTxs}; + auto [_, txSet] = makeTxSetFromTransactions(phases, *app, 0, 0); + + validateShape(*txSet, STAGE_COUNT, THREAD_COUNT, 1); + validateBaseFee(*txSet, 100); + } + SECTION("all stages, smaller txs") + { + std::vector sorobanTxs; + for (int i = 0; i < STAGE_COUNT * THREAD_COUNT * 5; ++i) + { + sorobanTxs.push_back(createTx(20'000'000, {4 * i, 4 * i + 1}, + {4 * i + 2, 4 * i + 3})); + } + PerPhaseTransactionList phases = {{}, sorobanTxs}; + auto [_, txSet] = makeTxSetFromTransactions(phases, *app, 0, 0); + + validateShape(*txSet, STAGE_COUNT, THREAD_COUNT, 5); + validateBaseFee(*txSet, 100); + } + + SECTION("all stages, smaller txs with prioritization") + { + std::vector sorobanTxs; + for (int i = 0; i < STAGE_COUNT * THREAD_COUNT * 10; ++i) + { + sorobanTxs.push_back(createTx( + 20'000'000, {4 * i, 4 * i + 1}, {4 * i + 2, 4 * i + 3}, + /* inclusionFee*/ (i + 1) * 1000LL)); + } + PerPhaseTransactionList phases = {{}, sorobanTxs}; + auto [_, txSet] = makeTxSetFromTransactions(phases, *app, 0, 0); + + validateShape(*txSet, STAGE_COUNT, THREAD_COUNT, 5); + validateBaseFee( + *txSet, 10LL * STAGE_COUNT * THREAD_COUNT * 1000 / 2 + 1000); + } + + SECTION("read bytes limit reached") + { + std::vector sorobanTxs; + for (int i = 0; i < STAGE_COUNT * THREAD_COUNT; ++i) + { + sorobanTxs.push_back(createTx(1'000'000, {4 * i, 4 * i + 1}, + {4 * i + 2, 4 * i + 3}, + /* inclusionFee */ 100 + i, + /* readBytes */ 100'000)); + } + PerPhaseTransactionList phases = {{}, sorobanTxs}; + auto [_, txSet] = makeTxSetFromTransactions(phases, *app, 0, 0); + + validateShape(*txSet, 1, 1, 10); + validateBaseFee(*txSet, 100 + STAGE_COUNT * THREAD_COUNT - 10); + } + } + + SECTION("with conflicts") + { + SECTION("all RW conflicting") + { + std::vector sorobanTxs; + for (int i = 0; i < THREAD_COUNT * STAGE_COUNT; ++i) + { + sorobanTxs.push_back(createTx(100'000'000, + {4 * i + 1, 4 * i + 2}, + {4 * i + 3, 0, 4 * i + 4}, + /* inclusionFee */ 100 + i)); + } + PerPhaseTransactionList phases = {{}, sorobanTxs}; + auto [_, txSet] = makeTxSetFromTransactions(phases, *app, 0, 0); + validateShape(*txSet, STAGE_COUNT, 1, 1); + validateBaseFee(*txSet, + 100 + THREAD_COUNT * STAGE_COUNT - STAGE_COUNT); + } + SECTION("all RO conflict with one RW") + { + std::vector sorobanTxs; + sorobanTxs.push_back(createTx(100'000'000, {1, 2}, {0, 3, 4}, + /* inclusionFee */ 1'000'000)); + for (int i = 1; i < THREAD_COUNT * STAGE_COUNT * 5; ++i) + { + sorobanTxs.push_back(createTx(20'000'000, + {0, 4 * i + 1, 4 * i + 2}, + {4 * i + 3, 4 * i + 4}, + /* inclusionFee */ 100 + i)); + } + + PerPhaseTransactionList phases = {{}, sorobanTxs}; + auto [_, txSet] = makeTxSetFromTransactions(phases, *app, 0, 0); + auto const& phase = + txSet->getPhase(TxSetPhase::SOROBAN).getParallelStages(); + + bool wasSingleThreadStage = false; + + for (auto const& stage : phase) + { + if (stage.size() == 1) + { + REQUIRE(!wasSingleThreadStage); + wasSingleThreadStage = true; + REQUIRE(stage[0].size() == 1); + REQUIRE(stage[0][0]->getEnvelope() == + sorobanTxs[0]->getEnvelope()); + continue; + } + REQUIRE(stage.size() == THREAD_COUNT); + for (auto const& thread : stage) + { + REQUIRE(thread.size() == 5); + } + } + // We can't include any of the small txs into stage 0, as it's + // occupied by high fee tx that writes entry 0. + validateBaseFee(*txSet, 100 + THREAD_COUNT * 5); + } + } + SECTION("smoke test") + { + auto runTest = [&]() { + std::uniform_int_distribution<> maxInsnsDistr(20'000'000, + 100'000'000); + std::uniform_int_distribution<> keyRangeDistr(50, 1000); + + std::vector sorobanTxs; + std::uniform_int_distribution<> insnsDistr( + 1'000'000, maxInsnsDistr(Catch::rng())); + std::uniform_int_distribution<> keyCountDistr(1, 10); + std::uniform_int_distribution<> keyDistr( + 1, keyRangeDistr(Catch::rng())); + std::uniform_int_distribution<> feeDistr(100, 100'000); + std::uniform_int_distribution<> readBytesDistr(100, 10'000); + std::uniform_int_distribution<> writeBytesDistr(10, 1000); + accountId = 1; + for (int iter = 0; iter < 500; ++iter) + { + int roKeyCount = keyCountDistr(Catch::rng()); + int rwKeyCount = keyCountDistr(Catch::rng()); + std::unordered_set usedKeys; + std::vector roKeys; + std::vector rwKeys; + for (int i = 0; i < roKeyCount + rwKeyCount; ++i) + { + int key = keyDistr(Catch::rng()); + while (usedKeys.find(key) != usedKeys.end()) + { + key = keyDistr(Catch::rng()); + } + if (i < roKeyCount) + { + roKeys.push_back(key); + } + else + { + rwKeys.push_back(key); + } + usedKeys.insert(key); + } + sorobanTxs.push_back(createTx(insnsDistr(Catch::rng()), roKeys, + rwKeys, feeDistr(Catch::rng()), + readBytesDistr(Catch::rng()), + writeBytesDistr(Catch::rng()))); + } + PerPhaseTransactionList phases = {{}, sorobanTxs}; + // NB: `makeTxSetFromTransactions` does an XDR roundtrip and + // validation, so just calling it does a good amount of smoke + // testing. + auto [_, txSet] = makeTxSetFromTransactions(phases, *app, 0, 0); + auto const& phase = + txSet->getPhase(TxSetPhase::SOROBAN).getParallelStages(); + // The only thing we can really be sure about is that all the + // stages are utilized, as we have enough transactions. + REQUIRE(phase.size() == STAGE_COUNT); + auto resources = *txSet->getTxSetSorobanResource(); + std::cout << "txs: " << txSet->sizeTxTotal() + << ", max insns : " << insnsDistr.max() + << ", key range: " << keyDistr.max() + << ", txset resources: " << resources.toString() + << std::endl; + }; + for (int iter = 0; iter < 100; ++iter) + { + runTest(); + } + } +} +#endif } // namespace } // namespace stellar diff --git a/src/herder/test/UpgradesTests.cpp b/src/herder/test/UpgradesTests.cpp index 3a1b3adf56..3656b6a002 100644 --- a/src/herder/test/UpgradesTests.cpp +++ b/src/herder/test/UpgradesTests.cpp @@ -283,6 +283,29 @@ getBucketListSizeWindowKey() return windowKey; } +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +LedgerKey +getParallelComputeSettingsLedgerKey() +{ + LedgerKey maxContractSizeKey(CONFIG_SETTING); + maxContractSizeKey.configSetting().configSettingID = + CONFIG_SETTING_CONTRACT_PARALLEL_COMPUTE_V0; + return maxContractSizeKey; +} + +ConfigUpgradeSetFrameConstPtr +makeParallelComputeUpdgrade(AbstractLedgerTxn& ltx, uint32_t maxParallelThreads) +{ + // Make entry for the upgrade + ConfigUpgradeSet configUpgradeSet; + auto& configEntry = configUpgradeSet.updatedEntry.emplace_back(); + configEntry.configSettingID(CONFIG_SETTING_CONTRACT_PARALLEL_COMPUTE_V0); + configEntry.contractParallelCompute().ledgerMaxParallelThreads = + maxParallelThreads; + return makeConfigUpgradeSet(ltx, configUpgradeSet); +} +#endif + void testListUpgrades(VirtualClock::system_time_point preferredUpgradeDatetime, bool shouldListAny) @@ -825,6 +848,56 @@ TEST_CASE("config upgrade validation", "[upgrades]") } } +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +TEST_CASE("config upgrade validation for protocol 22", "[upgrades]") +{ + auto runTest = [&](uint32_t protocolVersion, + Upgrades::UpgradeValidity expectedRes) { + VirtualClock clock; + auto cfg = getTestConfig(0); + cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION = protocolVersion; + auto app = createTestApplication(clock, cfg); + + LedgerHeader header; + auto headerTime = VirtualClock::to_time_t(genesis(0, 2)); + header.ledgerVersion = protocolVersion; + header.scpValue.closeTime = headerTime; + + ConfigUpgradeSetFrameConstPtr configUpgradeSet; + LedgerTxn ltx(app->getLedgerTxnRoot()); + { + Upgrades::UpgradeParameters scheduledUpgrades; + LedgerTxn upgradeLtx(ltx); + configUpgradeSet = makeParallelComputeUpdgrade(upgradeLtx, 10); + + scheduledUpgrades.mUpgradeTime = genesis(0, 1); + scheduledUpgrades.mConfigUpgradeSetKey = configUpgradeSet->getKey(); + app->getHerder().setUpgrades(scheduledUpgrades); + upgradeLtx.commit(); + } + ltx.loadHeader().current() = header; + LedgerUpgrade outUpgrade; + auto ls = LedgerSnapshot(ltx); + REQUIRE(Upgrades::isValidForApply( + toUpgradeType(makeConfigUpgrade(*configUpgradeSet)), + outUpgrade, *app, ls) == expectedRes); + }; + + SECTION("valid for apply") + { + runTest(static_cast(PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION), + Upgrades::UpgradeValidity::VALID); + } + + SECTION("unsupported protocol") + { + runTest(static_cast(PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION) - + 1, + Upgrades::UpgradeValidity::INVALID); + } +} +#endif + TEST_CASE("config upgrades applied to ledger", "[soroban][upgrades]") { VirtualClock clock; @@ -1671,8 +1744,9 @@ TEST_CASE("upgrade to version 10", "[upgrades]") " offers that do not satisfy thresholds") { // Pay txFee to send 4*baseReserve + 3*txFee for net balance - // decrease of 4*baseReserve + 4*txFee. This matches the balance - // decrease from creating 4 offers as in the next test section. + // decrease of 4*baseReserve + 4*txFee. This matches the + // balance decrease from creating 4 offers as in the next + // test section. a1.pay(root, 4 * lm.getLastReserve() + 3 * txFee); std::vector offers; @@ -1809,8 +1883,9 @@ TEST_CASE("upgrade to version 10", "[upgrades]") " unauthorized offers") { // Pay txFee to send 4*baseReserve + 3*txFee for net balance - // decrease of 4*baseReserve + 4*txFee. This matches the balance - // decrease from creating 4 offers as in the next test section. + // decrease of 4*baseReserve + 4*txFee. This matches the + // balance decrease from creating 4 offers as in the next + // test section. a1.pay(root, 4 * lm.getLastReserve() + 3 * txFee); std::vector offers; @@ -1959,11 +2034,10 @@ TEST_CASE("upgrade to version 11", "[upgrades]") uint64_t minBalance = lm.getLastMinBalance(5); uint64_t big = minBalance + ledgerSeq; uint64_t closeTime = 60 * 5 * ledgerSeq; - auto txSet = makeTxSetFromTransactions( - TxSetTransactions{ - root.tx({txtest::createAccount(stranger, big)})}, - *app, 0, 0) - .first; + auto txSet = + makeTxSetFromTransactions( + {root.tx({txtest::createAccount(stranger, big)})}, *app, 0, 0) + .first; // On 4th iteration of advance (a.k.a. ledgerSeq 5), perform a // ledger-protocol version upgrade to the new protocol, to activate @@ -2022,8 +2096,8 @@ TEST_CASE("upgrade to version 11", "[upgrades]") // Check several subtle characteristics of the post-upgrade // environment: // - Old-protocol merges stop happening (there should have - // been 6 before the upgrade, but we re-use a merge we did at - // ledger 1 for ledger 2 spill, so the counter is at 5) + // been 6 before the upgrade, but we re-use a merge we did + // at ledger 1 for ledger 2 spill, so the counter is at 5) // - New-protocol merges start happening. // - At the upgrade (5), we find 1 INITENTRY in lev[0].curr // - The next two (6, 7), propagate INITENTRYs to lev[0].snap @@ -2085,9 +2159,7 @@ TEST_CASE("upgrade to version 12", "[upgrades]") uint64_t closeTime = 60 * 5 * ledgerSeq; TxSetXDRFrameConstPtr txSet = makeTxSetFromTransactions( - TxSetTransactions{ - root.tx({txtest::createAccount(stranger, big)})}, - *app, 0, 0) + {root.tx({txtest::createAccount(stranger, big)})}, *app, 0, 0) .first; // On 4th iteration of advance (a.k.a. ledgerSeq 5), perform a @@ -2138,8 +2210,8 @@ TEST_CASE("upgrade to version 12", "[upgrades]") REQUIRE(getVers(lev1Snap) == oldProto); REQUIRE(mc.mPostShadowRemovalProtocolMerges == 6); // One more old-style merge despite the upgrade - // At ledger 8, level 2 spills, and starts an old-style merge, - // as level 1 snap is still of old version + // At ledger 8, level 2 spills, and starts an old-style + // merge, as level 1 snap is still of old version REQUIRE(mc.mPreShadowRemovalProtocolMerges == 6); break; case 7: @@ -2271,6 +2343,77 @@ TEST_CASE("configuration initialized in version upgrade", "[upgrades]") } } +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +TEST_CASE("parallel Soroban settings upgrade", "[upgrades]") +{ + VirtualClock clock; + auto cfg = getTestConfig(0, Config::TESTDB_IN_MEMORY_NO_OFFERS); + cfg.USE_CONFIG_FOR_GENESIS = false; + + auto app = createTestApplication(clock, cfg); + + executeUpgrade(*app, + makeProtocolVersionUpgrade( + static_cast(SOROBAN_PROTOCOL_VERSION) - 1)); + + for (uint32_t version = static_cast(SOROBAN_PROTOCOL_VERSION); + version < + static_cast(PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION); + ++version) + { + executeUpgrade(*app, makeProtocolVersionUpgrade(version)); + } + + { + LedgerSnapshot ls(*app); + REQUIRE(!ls.load(getParallelComputeSettingsLedgerKey())); + } + + executeUpgrade(*app, makeProtocolVersionUpgrade(static_cast( + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION))); + + // Make sure initial value is correct. + { + LedgerSnapshot ls(*app); + auto parellelComputeEntry = + ls.load(getParallelComputeSettingsLedgerKey()) + .current() + .data.configSetting(); + REQUIRE(parellelComputeEntry.configSettingID() == + CONFIG_SETTING_CONTRACT_PARALLEL_COMPUTE_V0); + REQUIRE(parellelComputeEntry.contractParallelCompute() + .ledgerMaxParallelThreads == + InitialSorobanNetworkConfig::LEDGER_MAX_PARALLEL_THREADS); + + // Check that BucketList size window initialized with current BL + // size + auto const& networkConfig = + app->getLedgerManager().getSorobanNetworkConfig(); + REQUIRE(networkConfig.ledgerMaxParallelThreads() == + InitialSorobanNetworkConfig::LEDGER_MAX_PARALLEL_THREADS); + } + + // Execute an upgrade. + { + LedgerTxn ltx(app->getLedgerTxnRoot()); + auto configUpgradeSet = makeParallelComputeUpdgrade(ltx, 5); + ltx.commit(); + executeUpgrade(*app, makeConfigUpgrade(*configUpgradeSet)); + } + + LedgerSnapshot ls(*app); + + REQUIRE(ls.load(getParallelComputeSettingsLedgerKey()) + .current() + .data.configSetting() + .contractParallelCompute() + .ledgerMaxParallelThreads == 5); + REQUIRE(app->getLedgerManager() + .getSorobanNetworkConfig() + .ledgerMaxParallelThreads() == 5); +} +#endif + TEST_CASE_VERSIONS("upgrade base reserve", "[upgrades]") { VirtualClock clock; @@ -2547,8 +2690,9 @@ TEST_CASE_VERSIONS("upgrade base reserve", "[upgrades]") createOffers(sponsoredAcc, offers, sponsoredAccPullOffers); createOffers(sponsoredAcc2, offers, true); - // prepare ops to transfer sponsorship of all sponsoredAcc - // offers and one offer from sponsoredAcc2 to sponsoringAcc + // prepare ops to transfer sponsorship of all + // sponsoredAcc offers and one offer from sponsoredAcc2 + // to sponsoringAcc std::vector ops = { sponsoringAcc.op( beginSponsoringFutureReserves(sponsoredAcc)), @@ -2585,15 +2729,17 @@ TEST_CASE_VERSIONS("upgrade base reserve", "[upgrades]") if (sponsoredAccPullOffers) { - // SponsoringAcc is now sponsoring all 12 of sponsoredAcc's - // offers. SponsoredAcc has 4 subentries. It also has enough - // lumens to cover 12 more subentries after the sponsorship - // update. After the upgrade to double the baseReserve, this - // account will need to cover the 4 subEntries, so we only - // need 4 extra baseReserves before the upgrade. Pay out the - // rest (8 reserves) so we can get our orders pulled on - // upgrade. 16(total reserves) - 4(subEntries) - - // 4(base reserve increase) = 8(extra base reserves) + // SponsoringAcc is now sponsoring all 12 of + // sponsoredAcc's offers. SponsoredAcc has 4 + // subentries. It also has enough lumens to cover 12 + // more subentries after the sponsorship update. + // After the upgrade to double the baseReserve, this + // account will need to cover the 4 subEntries, so + // we only need 4 extra baseReserves before the + // upgrade. Pay out the rest (8 reserves) so we can + // get our orders pulled on upgrade. 16(total + // reserves) - 4(subEntries) - 4(base reserve + // increase) = 8(extra base reserves) sponsoredAcc.pay(root, baseReserve * 8); } @@ -2607,8 +2753,8 @@ TEST_CASE_VERSIONS("upgrade base reserve", "[upgrades]") sponsoringAcc.pay(root, 1); } - // This account needs to lose a base reserve to get its orders - // pulled + // This account needs to lose a base reserve to get its + // orders pulled sponsoredAcc2.pay(root, baseReserve); // execute upgrade @@ -2677,8 +2823,8 @@ TEST_CASE_VERSIONS("upgrade base reserve", "[upgrades]") root.create(sponsoredSeed, lm.getLastMinBalance(14) + 3999 + 15 * txFee); - // This account will have one sponsored offer and will always - // have it's offers pulled. + // This account will have one sponsored offer and will + // always have it's offers pulled. auto sponsored2 = root.create( "C", 2 * lm.getLastMinBalance(13) + 3999 + 15 * txFee); @@ -2706,8 +2852,8 @@ TEST_CASE_VERSIONS("upgrade base reserve", "[upgrades]") }; for_versions_from(14, *app, [&] { - // Swap the seeds to test that the ordering of accounts doesn't - // matter when upgrading + // Swap the seeds to test that the ordering of accounts + // doesn't matter when upgrading SECTION("account A is sponsored") { sponsorshipTestsBySeed("B", "A"); diff --git a/src/history/test/HistoryTests.cpp b/src/history/test/HistoryTests.cpp index 2b882a9099..4d97f285a1 100644 --- a/src/history/test/HistoryTests.cpp +++ b/src/history/test/HistoryTests.cpp @@ -1075,9 +1075,7 @@ TEST_CASE("Catchup non-initentry buckets to initentry-supporting works", uint64_t closeTime = 60 * 5 * ledgerSeq; auto [txSet, applicableTxSet] = makeTxSetFromTransactions( - TxSetTransactions{ - root.tx({txtest::createAccount(stranger, big)})}, - *a, 0, 0); + {root.tx({txtest::createAccount(stranger, big)})}, *a, 0, 0); // On first iteration of advance, perform a ledger-protocol version // upgrade to the new protocol, to activate INITENTRY behaviour. diff --git a/src/history/test/HistoryTestsUtils.cpp b/src/history/test/HistoryTestsUtils.cpp index 5119d372a4..f80bfdda3e 100644 --- a/src/history/test/HistoryTestsUtils.cpp +++ b/src/history/test/HistoryTestsUtils.cpp @@ -501,8 +501,8 @@ CatchupSimulation::generateRandomLedger(uint32_t version) auto phases = protocolVersionStartsFrom( lm.getLastClosedLedgerHeader().header.ledgerVersion, SOROBAN_PROTOCOL_VERSION) - ? TxSetPhaseTransactions{txs, sorobanTxs} - : TxSetPhaseTransactions{txs}; + ? PerPhaseTransactionList{txs, sorobanTxs} + : PerPhaseTransactionList{txs}; TxSetXDRFrameConstPtr txSet = makeTxSetFromTransactions(phases, mApp, 0, 0).first; diff --git a/src/ledger/LedgerManagerImpl.cpp b/src/ledger/LedgerManagerImpl.cpp index 2b7f328671..4635810375 100644 --- a/src/ledger/LedgerManagerImpl.cpp +++ b/src/ledger/LedgerManagerImpl.cpp @@ -900,22 +900,13 @@ LedgerManagerImpl::closeLedger(LedgerCloseData const& ledgerData) ledgerCloseMeta->populateTxSet(*txSet); } - // the transaction set that was agreed upon by consensus - // was sorted by hash; we reorder it so that transactions are - // sorted such that sequence numbers are respected - std::vector const txs = - applicableTxSet->getTxsInApplyOrder(); - // first, prefetch source accounts for txset, then charge fees - prefetchTxSourceIds(txs); + prefetchTxSourceIds(*applicableTxSet); auto const mutableTxResults = - processFeesSeqNums(txs, ltx, *applicableTxSet, ledgerCloseMeta); + processFeesSeqNums(*applicableTxSet, ltx, ledgerCloseMeta); - TransactionResultSet txResultSet; - txResultSet.results.reserve(txs.size()); - // Subtle: after this call, `header` is invalidated, and is not safe to use - applyTransactions(*applicableTxSet, txs, mutableTxResults, ltx, txResultSet, - ledgerCloseMeta); + auto txResultSet = applyTransactions(*applicableTxSet, mutableTxResults, + ltx, ledgerCloseMeta); if (mApp.getConfig().MODE_STORES_HISTORY_MISC) { storeTxSet(mApp.getDatabase(), ltx.loadHeader().current().ledgerSeq, @@ -966,7 +957,7 @@ LedgerManagerImpl::closeLedger(LedgerCloseData const& ledgerData) uem.changes = changes; } // Note: Index from 1 rather than 0 to match the behavior of - // storeTransaction and storeTransactionFee. + // storeTransaction. if (mApp.getConfig().MODE_STORES_HISTORY_MISC) { Upgrades::storeUpgradeHistory(getDatabase(), ledgerSeq, @@ -1362,13 +1353,12 @@ mergeOpInTx(std::vector const& ops) std::vector LedgerManagerImpl::processFeesSeqNums( - std::vector const& txs, - AbstractLedgerTxn& ltxOuter, ApplicableTxSetFrame const& txSet, + ApplicableTxSetFrame const& txSet, AbstractLedgerTxn& ltxOuter, std::unique_ptr const& ledgerCloseMeta) { ZoneScoped; std::vector txResults; - txResults.reserve(txs.size()); + txResults.reserve(txSet.sizeTxTotal()); CLOG_DEBUG(Ledger, "processing fees and sequence numbers"); int index = 0; try @@ -1378,42 +1368,43 @@ LedgerManagerImpl::processFeesSeqNums( std::map accToMaxSeq; bool mergeSeen = false; - for (auto tx : txs) + for (auto const& phase : txSet.getPhasesInApplyOrder()) { - LedgerTxn ltxTx(ltx); - - txResults.push_back( - tx->processFeeSeqNum(ltxTx, txSet.getTxBaseFee(tx, header))); - - if (protocolVersionStartsFrom( - ltxTx.loadHeader().current().ledgerVersion, - ProtocolVersion::V_19)) + for (auto const& tx : phase) { - auto res = - accToMaxSeq.emplace(tx->getSourceID(), tx->getSeqNum()); - if (!res.second) + LedgerTxn ltxTx(ltx); + txResults.push_back( + tx->processFeeSeqNum(ltxTx, txSet.getTxBaseFee(tx))); + + if (protocolVersionStartsFrom( + ltxTx.loadHeader().current().ledgerVersion, + ProtocolVersion::V_19)) { - res.first->second = - std::max(res.first->second, tx->getSeqNum()); + auto res = + accToMaxSeq.emplace(tx->getSourceID(), tx->getSeqNum()); + if (!res.second) + { + res.first->second = + std::max(res.first->second, tx->getSeqNum()); + } + + if (mergeOpInTx(tx->getRawOperations())) + { + mergeSeen = true; + } } - if (mergeOpInTx(tx->getRawOperations())) + LedgerEntryChanges changes = ltxTx.getChanges(); + if (ledgerCloseMeta) { - mergeSeen = true; + ledgerCloseMeta->pushTxProcessingEntry(); + ledgerCloseMeta->setLastTxProcessingFeeProcessingChanges( + changes); } + ++index; + ltxTx.commit(); } - - LedgerEntryChanges changes = ltxTx.getChanges(); - if (ledgerCloseMeta) - { - ledgerCloseMeta->pushTxProcessingEntry(); - ledgerCloseMeta->setLastTxProcessingFeeProcessingChanges( - changes); - } - ++index; - ltxTx.commit(); } - if (protocolVersionStartsFrom(ltx.loadHeader().current().ledgerVersion, ProtocolVersion::V_19) && mergeSeen) @@ -1456,24 +1447,25 @@ LedgerManagerImpl::processFeesSeqNums( } void -LedgerManagerImpl::prefetchTxSourceIds( - std::vector const& txs) +LedgerManagerImpl::prefetchTxSourceIds(ApplicableTxSetFrame const& txSet) { ZoneScoped; if (mApp.getConfig().PREFETCH_BATCH_SIZE > 0) { UnorderedSet keys; - for (auto const& tx : txs) + for (auto const& phase : txSet.getPhases()) { - tx->insertKeysForFeeProcessing(keys); + for (auto const& tx : phase) + { + tx->insertKeysForFeeProcessing(keys); + } } mApp.getLedgerTxnRoot().prefetchClassic(keys); } } void -LedgerManagerImpl::prefetchTransactionData( - std::vector const& txs) +LedgerManagerImpl::prefetchTransactionData(ApplicableTxSetFrame const& txSet) { ZoneScoped; if (mApp.getConfig().PREFETCH_BATCH_SIZE > 0) @@ -1481,22 +1473,25 @@ LedgerManagerImpl::prefetchTransactionData( UnorderedSet sorobanKeys; auto lkMeter = make_unique(); UnorderedSet classicKeys; - for (auto const& tx : txs) + for (auto const& phase : txSet.getPhases()) { - if (tx->isSoroban()) + for (auto const& tx : phase) { - if (mApp.getConfig().isUsingBucketListDB()) + if (tx->isSoroban()) { - tx->insertKeysForTxApply(sorobanKeys, lkMeter.get()); + if (mApp.getConfig().isUsingBucketListDB()) + { + tx->insertKeysForTxApply(sorobanKeys, lkMeter.get()); + } + } + else + { + tx->insertKeysForTxApply(classicKeys, nullptr); } - } - else - { - tx->insertKeysForTxApply(classicKeys, nullptr); } } - // Prefetch classic and soroban keys separately for greater visibility - // into the performance of each mode. + // Prefetch classic and soroban keys separately for greater + // visibility into the performance of each mode. if (mApp.getConfig().isUsingBucketListDB()) { if (!sorobanKeys.empty()) @@ -1509,21 +1504,20 @@ LedgerManagerImpl::prefetchTransactionData( } } -void +TransactionResultSet LedgerManagerImpl::applyTransactions( ApplicableTxSetFrame const& txSet, - std::vector const& txs, std::vector const& mutableTxResults, - AbstractLedgerTxn& ltx, TransactionResultSet& txResultSet, + AbstractLedgerTxn& ltx, std::unique_ptr const& ledgerCloseMeta) { ZoneNamedN(txsZone, "applyTransactions", true); - releaseAssert(txs.size() == mutableTxResults.size()); + size_t numTxs = txSet.sizeTxTotal(); + size_t numOps = txSet.sizeOpTotal(); + releaseAssert(numTxs == mutableTxResults.size()); int index = 0; // Record counts - auto numTxs = txs.size(); - auto numOps = txSet.sizeOpTotal(); if (numTxs > 0) { mTransactionCount.Update(static_cast(numTxs)); @@ -1534,92 +1528,96 @@ LedgerManagerImpl::applyTransactions( CLOG_INFO(Tx, "applying ledger {} ({})", ltx.loadHeader().current().ledgerSeq, txSet.summary()); } + TransactionResultSet txResultSet; + txResultSet.results.reserve(numTxs); - prefetchTransactionData(txs); - + prefetchTransactionData(txSet); + auto phases = txSet.getPhasesInApplyOrder(); Hash sorobanBasePrngSeed = txSet.getContentsHash(); uint64_t txNum{0}; uint64_t txSucceeded{0}; uint64_t txFailed{0}; uint64_t sorobanTxSucceeded{0}; uint64_t sorobanTxFailed{0}; - for (size_t i = 0; i < txs.size(); ++i) + size_t resultIndex = 0; + for (auto const& phase : phases) { - ZoneNamedN(txZone, "applyTransaction", true); - auto tx = txs.at(i); - auto mutableTxResult = mutableTxResults.at(i); - - auto txTime = mTransactionApply.TimeScope(); - TransactionMetaFrame tm(ltx.loadHeader().current().ledgerVersion); - CLOG_DEBUG(Tx, " tx#{} = {} ops={} txseq={} (@ {})", index, - hexAbbrev(tx->getContentsHash()), tx->getNumOperations(), - tx->getSeqNum(), - mApp.getConfig().toShortString(tx->getSourceID())); - - Hash subSeed = sorobanBasePrngSeed; - // If tx can use the seed, we need to compute a sub-seed for it. - if (tx->isSoroban()) - { - SHA256 subSeedSha; - subSeedSha.add(sorobanBasePrngSeed); - subSeedSha.add(xdr::xdr_to_opaque(txNum)); - subSeed = subSeedSha.finish(); - } - ++txNum; - - tx->apply(mApp, ltx, tm, mutableTxResult, subSeed); - tx->processPostApply(mApp, ltx, tm, mutableTxResult); - TransactionResultPair results; - results.transactionHash = tx->getContentsHash(); - results.result = mutableTxResult->getResult(); - if (results.result.result.code() == TransactionResultCode::txSUCCESS) + for (auto const& tx : phase) { + ZoneNamedN(txZone, "applyTransaction", true); + auto mutableTxResult = mutableTxResults.at(resultIndex++); + + auto txTime = mTransactionApply.TimeScope(); + TransactionMetaFrame tm(ltx.loadHeader().current().ledgerVersion); + CLOG_DEBUG(Tx, " tx#{} = {} ops={} txseq={} (@ {})", index, + hexAbbrev(tx->getContentsHash()), tx->getNumOperations(), + tx->getSeqNum(), + mApp.getConfig().toShortString(tx->getSourceID())); + + Hash subSeed = sorobanBasePrngSeed; + // If tx can use the seed, we need to compute a sub-seed for it. if (tx->isSoroban()) { - ++sorobanTxSucceeded; + SHA256 subSeedSha; + subSeedSha.add(sorobanBasePrngSeed); + subSeedSha.add(xdr::xdr_to_opaque(txNum)); + subSeed = subSeedSha.finish(); } - ++txSucceeded; - } - else - { - if (tx->isSoroban()) + ++txNum; + + tx->apply(mApp, ltx, tm, mutableTxResult, subSeed); + tx->processPostApply(mApp, ltx, tm, mutableTxResult); + TransactionResultPair results; + results.transactionHash = tx->getContentsHash(); + results.result = mutableTxResult->getResult(); + if (results.result.result.code() == + TransactionResultCode::txSUCCESS) { - ++sorobanTxFailed; + if (tx->isSoroban()) + { + ++sorobanTxSucceeded; + } + ++txSucceeded; + } + else + { + if (tx->isSoroban()) + { + ++sorobanTxFailed; + } + ++txFailed; } - ++txFailed; - } - - // First gather the TransactionResultPair into the TxResultSet for - // hashing into the ledger header. - txResultSet.results.emplace_back(results); + // First gather the TransactionResultPair into the TxResultSet for + // hashing into the ledger header. + txResultSet.results.emplace_back(results); #ifdef BUILD_TESTS - mLastLedgerTxMeta.push_back(tm); + mLastLedgerTxMeta.push_back(tm); #endif + // Then potentially add that TRP and its associated TransactionMeta + // into the associated slot of any LedgerCloseMeta we're collecting. + if (ledgerCloseMeta) + { + ledgerCloseMeta->setTxProcessingMetaAndResultPair( + tm.getXDR(), std::move(results), index); + } - // Then potentially add that TRP and its associated TransactionMeta - // into the associated slot of any LedgerCloseMeta we're collecting. - if (ledgerCloseMeta) - { - ledgerCloseMeta->setTxProcessingMetaAndResultPair( - tm.getXDR(), std::move(results), index); - } - - // Then finally store the results and meta into the txhistory table. - // if we're running in a mode that has one. - // - // Note to future: when we eliminate the txhistory for archiving, the - // next step can be removed. - // - // Also note: for historical reasons the history tables number - // txs counting from 1, not 0. We preserve this for the time being - // in case anyone depends on it. - ++index; - if (mApp.getConfig().MODE_STORES_HISTORY_MISC) - { - auto ledgerSeq = ltx.loadHeader().current().ledgerSeq; - storeTransaction(mApp.getDatabase(), ledgerSeq, tx, tm.getXDR(), - txResultSet, mApp.getConfig()); + // Then finally store the results and meta into the txhistory table. + // if we're running in a mode that has one. + // + // Note to future: when we eliminate the txhistory for archiving, + // the next step can be removed. + // + // Also note: for historical reasons the history tables number + // txs counting from 1, not 0. We preserve this for the time being + // in case anyone depends on it. + ++index; + if (mApp.getConfig().MODE_STORES_HISTORY_MISC) + { + auto ledgerSeq = ltx.loadHeader().current().ledgerSeq; + storeTransaction(mApp.getDatabase(), ledgerSeq, tx, tm.getXDR(), + txResultSet, mApp.getConfig()); + } } } @@ -1628,6 +1626,7 @@ LedgerManagerImpl::applyTransactions( mSorobanTransactionApplySucceeded.inc(sorobanTxSucceeded); mSorobanTransactionApplyFailed.inc(sorobanTxFailed); logTxApplyMetrics(ltx, numTxs, numOps); + return txResultSet; } void @@ -1661,8 +1660,9 @@ LedgerManagerImpl::storeCurrentLedger(LedgerHeader const& header, { bl = mApp.getBucketManager().getBucketList(); } - // Store the current HAS in the database; this is really just to checkpoint - // the bucketlist so we can survive a restart and re-attach to the buckets. + // Store the current HAS in the database; this is really just to + // checkpoint the bucketlist so we can survive a restart and re-attach + // to the buckets. HistoryArchiveState has(header.ledgerSeq, bl, mApp.getConfig().NETWORK_PASSPHRASE); @@ -1744,24 +1744,26 @@ LedgerManagerImpl::ledgerClosed( ledgerSeq, currLedgerVers); // There is a subtle bug in the upgrade path that wasn't noticed until - // protocol 20. For a ledger that upgrades from protocol vN to vN+1, there - // are two different assumptions in different parts of the ledger-close - // path: - // - In closeLedger we mostly treat the ledger as being on vN, eg. during + // protocol 20. For a ledger that upgrades from protocol vN to vN+1, + // there are two different assumptions in different parts of the + // ledger-close path: + // - In closeLedger we mostly treat the ledger as being on vN, eg. + // during // tx apply and LCM construction. - // - In the final stage, when we call ledgerClosed, we pass vN+1 because - // the upgrade completed and modified the ltx header, and we fish the - // protocol out of the ltx header - // Before LedgerCloseMetaV1, this inconsistency was mostly harmless since - // LedgerCloseMeta was not modified after the LTX header was modified. - // However, starting with protocol 20, LedgerCloseMeta is modified after - // updating the ltx header when populating BucketList related meta. This - // means that this function will attempt to call LedgerCloseMetaV1 - // functions, but ledgerCloseMeta is actually a LedgerCloseMetaV0 because it - // was constructed with the previous protocol version prior to the upgrade. - // Due to this, we must check the initial protocol version of ledger instead - // of the ledger version of the current ltx header, which may have been - // modified via an upgrade. + // - In the final stage, when we call ledgerClosed, we pass vN+1 + // because + // the upgrade completed and modified the ltx header, and we fish + // the protocol out of the ltx header + // Before LedgerCloseMetaV1, this inconsistency was mostly harmless + // since LedgerCloseMeta was not modified after the LTX header was + // modified. However, starting with protocol 20, LedgerCloseMeta is + // modified after updating the ltx header when populating BucketList + // related meta. This means that this function will attempt to call + // LedgerCloseMetaV1 functions, but ledgerCloseMeta is actually a + // LedgerCloseMetaV0 because it was constructed with the previous + // protocol version prior to the upgrade. Due to this, we must check the + // initial protocol version of ledger instead of the ledger version of + // the current ltx header, which may have been modified via an upgrade. transferLedgerEntriesToBucketList( ltx, ledgerCloseMeta, ltx.loadHeader().current(), initialLedgerVers); if (ledgerCloseMeta && diff --git a/src/ledger/LedgerManagerImpl.h b/src/ledger/LedgerManagerImpl.h index a5b1ae860a..c24830a64d 100644 --- a/src/ledger/LedgerManagerImpl.h +++ b/src/ledger/LedgerManagerImpl.h @@ -75,15 +75,13 @@ class LedgerManagerImpl : public LedgerManager std::unique_ptr mNextMetaToEmit; std::vector processFeesSeqNums( - std::vector const& txs, - AbstractLedgerTxn& ltxOuter, ApplicableTxSetFrame const& txSet, + ApplicableTxSetFrame const& txSet, AbstractLedgerTxn& ltxOuter, std::unique_ptr const& ledgerCloseMeta); - void applyTransactions( + TransactionResultSet applyTransactions( ApplicableTxSetFrame const& txSet, - std::vector const& txs, std::vector const& mutableTxResults, - AbstractLedgerTxn& ltx, TransactionResultSet& txResultSet, + AbstractLedgerTxn& ltx, std::unique_ptr const& ledgerCloseMeta); // initialLedgerVers must be the ledger version at the start of the ledger. @@ -95,9 +93,8 @@ class LedgerManagerImpl : public LedgerManager uint32_t initialLedgerVers); void storeCurrentLedger(LedgerHeader const& header, bool storeHeader); - void - prefetchTransactionData(std::vector const& txs); - void prefetchTxSourceIds(std::vector const& txs); + void prefetchTransactionData(ApplicableTxSetFrame const& txSet); + void prefetchTxSourceIds(ApplicableTxSetFrame const& txSet); void closeLedgerIf(LedgerCloseData const& ledgerData); State mState; diff --git a/src/ledger/NetworkConfig.cpp b/src/ledger/NetworkConfig.cpp index 33bca72cb4..4ed45b4bbe 100644 --- a/src/ledger/NetworkConfig.cpp +++ b/src/ledger/NetworkConfig.cpp @@ -1025,6 +1025,19 @@ updateMemCostParamsEntryForV22(AbstractLedgerTxn& ltxRoot) ltx.commit(); } +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +ConfigSettingEntry +initialParallelComputeEntry() +{ + ConfigSettingEntry entry(CONFIG_SETTING_CONTRACT_PARALLEL_COMPUTE_V0); + entry.contractParallelCompute().ledgerMaxParallelThreads = + InitialSorobanNetworkConfig::LEDGER_MAX_PARALLEL_THREADS; + return entry; +} +#endif + + + ConfigSettingEntry initialBucketListSizeWindow(Application& app) { @@ -1173,6 +1186,15 @@ SorobanNetworkConfig::isValidConfigSettingEntry(ConfigSettingEntry const& cfg, case ConfigSettingID::CONFIG_SETTING_EVICTION_ITERATOR: valid = true; break; +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + case ConfigSettingID::CONFIG_SETTING_CONTRACT_PARALLEL_COMPUTE_V0: + valid = protocolVersionStartsFrom( + ledgerVersion, PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION) && + cfg.contractParallelCompute().ledgerMaxParallelThreads > 0; + break; +#endif + default: + break; } return valid; } @@ -1257,6 +1279,17 @@ SorobanNetworkConfig::createCostTypesForV22(AbstractLedgerTxn& ltx, updateMemCostParamsEntryForV22(ltx); } +void +SorobanNetworkConfig::createLedgerEntriesForParallelSoroban( + AbstractLedgerTxn& ltx, Application& app) +{ +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + ZoneScoped; + createConfigSettingEntry( + initialParallelComputeEntry(), ltx, + static_cast(PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)); +#endif +} void SorobanNetworkConfig::initializeGenesisLedgerForTesting( uint32_t genesisLedgerProtocol, AbstractLedgerTxn& ltx, Application& app) @@ -1282,6 +1315,11 @@ SorobanNetworkConfig::initializeGenesisLedgerForTesting( { SorobanNetworkConfig::createCostTypesForV22(ltx, app); } + if (protocolVersionStartsFrom(genesisLedgerProtocol, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) + { + SorobanNetworkConfig::createLedgerEntriesForParallelSoroban(ltx, app); + } } void @@ -1306,6 +1344,13 @@ SorobanNetworkConfig::loadFromLedger(AbstractLedgerTxn& ltxRoot, loadExecutionLanesSettings(ltx); loadBucketListSizeWindow(ltx); loadEvictionIterator(ltx); + + if (protocolVersionStartsFrom(protocolVersion, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) + { + loadParallelComputeConfig(ltx); + } + // NB: this should follow loading state archival settings maybeUpdateBucketListWindowSize(ltx); // NB: this should follow loading/updating bucket list window @@ -1511,6 +1556,21 @@ SorobanNetworkConfig::loadEvictionIterator(AbstractLedgerTxn& ltx) mEvictionIterator = txle.current().data.configSetting().evictionIterator(); } +void +SorobanNetworkConfig::loadParallelComputeConfig(AbstractLedgerTxn& ltx) +{ + ZoneScoped; +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + LedgerKey key(CONFIG_SETTING); + key.configSetting().configSettingID = + ConfigSettingID::CONFIG_SETTING_CONTRACT_PARALLEL_COMPUTE_V0; + auto le = ltx.loadWithoutRecord(key).current(); + auto const& configSetting = + le.data.configSetting().contractParallelCompute(); + mLedgerMaxParallelThreads = configSetting.ledgerMaxParallelThreads; +#endif +} + void SorobanNetworkConfig::writeBucketListSizeWindow( AbstractLedgerTxn& ltxRoot) const @@ -1954,6 +2014,19 @@ SorobanNetworkConfig::writeAllSettings(AbstractLedgerTxn& ltx, entries.emplace_back( writeConfigSettingEntry(stateArchivalSettingsEntry, ltx, app)); +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + if (protocolVersionStartsFrom(ltx.loadHeader().current().ledgerVersion, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) + { + ConfigSettingEntry parallelComputeEntry( + CONFIG_SETTING_CONTRACT_PARALLEL_COMPUTE_V0); + parallelComputeEntry.contractParallelCompute() + .ledgerMaxParallelThreads = mLedgerMaxParallelThreads; + entries.emplace_back( + writeConfigSettingEntry(parallelComputeEntry, ltx, app)); + } +#endif + writeBucketListSizeWindow(ltx); updateEvictionIterator(ltx, mEvictionIterator); @@ -2029,6 +2102,12 @@ SorobanNetworkConfig::updateEvictionIterator( ltx.commit(); } +uint32_t +SorobanNetworkConfig::ledgerMaxParallelThreads() const +{ + return mLedgerMaxParallelThreads; +} + #ifdef BUILD_TESTS StateArchivalSettings& SorobanNetworkConfig::stateArchivalSettings() diff --git a/src/ledger/NetworkConfig.h b/src/ledger/NetworkConfig.h index f5265bf916..78939d3e44 100644 --- a/src/ledger/NetworkConfig.h +++ b/src/ledger/NetworkConfig.h @@ -138,6 +138,9 @@ struct InitialSorobanNetworkConfig // General execution settings static constexpr uint32_t LEDGER_MAX_TX_COUNT = 1; + + // Parallel execution settings + static constexpr uint32_t LEDGER_MAX_PARALLEL_THREADS = 1; }; // Defines the subset of the `InitialSorobanNetworkConfig` to be overridden for @@ -221,6 +224,9 @@ class SorobanNetworkConfig // upgrade. static void createCostTypesForV22(AbstractLedgerTxn& ltx, Application& app); + + static void createLedgerEntriesForParallelSoroban(AbstractLedgerTxn& ltx, + Application& app); // Test-only function that initializes contract network configuration // bypassing the normal upgrade process (i.e. when genesis ledger starts not // at v1) @@ -332,6 +338,10 @@ class SorobanNetworkConfig void updateEvictionIterator(AbstractLedgerTxn& ltxRoot, EvictionIterator const& newIter) const; + + // Parallel execution settings + uint32_t ledgerMaxParallelThreads() const; + #ifdef BUILD_TESTS StateArchivalSettings& stateArchivalSettings(); EvictionIterator& evictionIterator(); @@ -353,6 +363,7 @@ class SorobanNetworkConfig void loadExecutionLanesSettings(AbstractLedgerTxn& ltx); void loadBucketListSizeWindow(AbstractLedgerTxn& ltx); void loadEvictionIterator(AbstractLedgerTxn& ltx); + void loadParallelComputeConfig(AbstractLedgerTxn& ltx); void computeWriteFee(uint32_t configMaxProtocol, uint32_t protocolVersion); // If newSize is different than the current BucketList size sliding window, // update the window. If newSize < currSize, pop entries off window. If @@ -417,11 +428,7 @@ class SorobanNetworkConfig // FIFO queue, push_back/pop_front std::deque mBucketListSizeSnapshots; - uint64_t mAverageBucketListSize{0}; - -#ifdef BUILD_TESTS - void writeAllSettings(AbstractLedgerTxn& ltx, Application& app) const; -#endif + uint64_t mAverageBucketListSize{}; // Host cost params ContractCostParams mCpuCostParams{}; @@ -430,6 +437,13 @@ class SorobanNetworkConfig // State archival settings StateArchivalSettings mStateArchivalSettings{}; mutable EvictionIterator mEvictionIterator{}; + + // Parallel execution settings + uint32_t mLedgerMaxParallelThreads{}; + +#ifdef BUILD_TESTS + void writeAllSettings(AbstractLedgerTxn& ltx, Application& app) const; +#endif }; } diff --git a/src/main/Config.cpp b/src/main/Config.cpp index 58c1eb8dc8..ca0765a25d 100644 --- a/src/main/Config.cpp +++ b/src/main/Config.cpp @@ -307,6 +307,10 @@ Config::Config() : NODE_SEED(SecretKey::random()) EMIT_LEDGER_CLOSE_META_EXT_V1 = false; FORCE_OLD_STYLE_LEADER_ELECTION = false; + // This is not configurable for now. It doesn't need to be a network-wide + // setting, but on the other hand there aren't many good values for it and + // it's not clear what the right way to configure it would be, if at all. + SOROBAN_PHASE_STAGE_COUNT = 1; #ifdef BUILD_TESTS TEST_CASES_ENABLED = false; diff --git a/src/main/Config.h b/src/main/Config.h index 600a349259..53175b70d5 100644 --- a/src/main/Config.h +++ b/src/main/Config.h @@ -692,6 +692,8 @@ class Config : public std::enable_shared_from_this bool EMIT_SOROBAN_TRANSACTION_META_EXT_V1; bool EMIT_LEDGER_CLOSE_META_EXT_V1; + uint32_t SOROBAN_PHASE_STAGE_COUNT; + #ifdef BUILD_TESTS // If set to true, the application will be aware this run is for a test // case. This is used right now in the signal handler to exit() instead of diff --git a/src/simulation/ApplyLoad.cpp b/src/simulation/ApplyLoad.cpp index b4d40fd2b6..af7f93ad71 100644 --- a/src/simulation/ApplyLoad.cpp +++ b/src/simulation/ApplyLoad.cpp @@ -1,4 +1,7 @@ #include "simulation/ApplyLoad.h" + +#include + #include "herder/Herder.h" #include "ledger/LedgerManager.h" #include "test/TxTests.h" diff --git a/src/test/TxTests.cpp b/src/test/TxTests.cpp index b306e737c1..5918e6b6f1 100644 --- a/src/test/TxTests.cpp +++ b/src/test/TxTests.cpp @@ -536,15 +536,15 @@ closeLedgerOn(Application& app, uint32 ledgerSeq, TimePoint closeTime, } else { - TxSetTransactions classic; - TxSetTransactions soroban; + TxFrameList classic; + TxFrameList soroban; for (auto const& tx : txs) { tx->isSoroban() ? soroban.emplace_back(tx) : classic.emplace_back(tx); } - TxSetPhaseTransactions phases = {classic}; + PerPhaseTransactionList phases = {classic}; if (!soroban.empty()) { phases.emplace_back(soroban); diff --git a/src/transactions/test/TxEnvelopeTests.cpp b/src/transactions/test/TxEnvelopeTests.cpp index 99dc560533..22c414d296 100644 --- a/src/transactions/test/TxEnvelopeTests.cpp +++ b/src/transactions/test/TxEnvelopeTests.cpp @@ -64,11 +64,11 @@ TEST_CASE("txset - correct apply order", "[tx][envelope]") auto tx1 = b1.tx({accountMerge(a1)}); auto tx2 = a1.tx({a1.op(payment(root, 112)), a1.op(payment(root, 101))}); - auto txSet = - makeTxSetFromTransactions(TxSetTransactions{tx1, tx2}, *app, 0, 0) - .second; + auto txSet = makeTxSetFromTransactions({tx1, tx2}, *app, 0, 0).second; - auto txs = txSet->getTxsInApplyOrder(); + auto txs = + txSet->getPhasesInApplyOrder()[static_cast(TxSetPhase::CLASSIC)] + .getSequentialTxs(); REQUIRE(txs.size() == 2); // Sort for apply re-orders transaction set based on the contents hash if (lessThanXored(tx1->getFullHash(), tx2->getFullHash(), diff --git a/src/util/ProtocolVersion.h b/src/util/ProtocolVersion.h index b908b8f4a9..6fbeb957f8 100644 --- a/src/util/ProtocolVersion.h +++ b/src/util/ProtocolVersion.h @@ -34,7 +34,8 @@ enum class ProtocolVersion : uint32_t V_19, V_20, V_21, - V_22 + V_22, + V_23 }; // Checks whether provided protocolVersion is before (i.e. strictly lower than) @@ -50,4 +51,6 @@ bool protocolVersionEquals(uint32_t protocolVersion, ProtocolVersion equalsVersion); constexpr ProtocolVersion SOROBAN_PROTOCOL_VERSION = ProtocolVersion::V_20; +constexpr ProtocolVersion PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION = + ProtocolVersion::V_23; } diff --git a/src/util/TxResource.cpp b/src/util/TxResource.cpp index cb3b9bdf90..cb8b413a43 100644 --- a/src/util/TxResource.cpp +++ b/src/util/TxResource.cpp @@ -169,7 +169,8 @@ Resource::canAdd(Resource const& other) const releaseAssert(size() == other.size()); for (size_t i = 0; i < size(); i++) { - if (INT64_MAX - mResources[i] < other.mResources[i]) + if (std::numeric_limits::max() - mResources[i] < + other.mResources[i]) { return false; }