From 0fcd134aa9cc8c398f884809727ddee940d78ffa Mon Sep 17 00:00:00 2001 From: Dmytro Kozhevin Date: Thu, 27 Jun 2024 16:59:11 -0400 Subject: [PATCH] Parallel tx set nomination --- Builds/VisualStudio/stellar-core.vcxproj | 6 + .../VisualStudio/stellar-core.vcxproj.filters | 18 + configure.ac | 12 +- src/herder/ParallelTxSetBuilder.cpp | 379 +++++++++ src/herder/ParallelTxSetBuilder.h | 21 + src/herder/SurgePricingUtils.cpp | 7 + src/herder/SurgePricingUtils.h | 25 +- src/herder/TxSetFrame.cpp | 730 ++++++++++++------ src/herder/TxSetFrame.h | 27 +- src/herder/TxSetUtils.cpp | 4 + src/herder/test/TxSetTests.cpp | 316 ++++++++ src/herder/test/UpgradesTests.cpp | 27 +- src/ledger/NetworkConfig.cpp | 24 +- src/main/Config.cpp | 4 + src/main/Config.h | 2 + src/simulation/ApplyLoad.cpp | 3 + src/util/ProtocolVersion.h | 5 +- src/util/TxResource.cpp | 3 +- 18 files changed, 1337 insertions(+), 276 deletions(-) create mode 100644 src/herder/ParallelTxSetBuilder.cpp create mode 100644 src/herder/ParallelTxSetBuilder.h 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/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/TxSetFrame.cpp b/src/herder/TxSetFrame.cpp index aa75ea17c2..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" @@ -421,40 +422,6 @@ sortedForApplyParallel(TxStageFrameList const& stages, Hash const& txSetHash) return stages; } -// 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(TxSetPhaseFrame const& phase, Application& app, - uint64_t lowerBoundCloseTimeOffset, - uint64_t upperBoundCloseTimeOffset) -{ - 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) - { - 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; -} - bool addWireTxsToList(Hash const& networkID, xdr::xvector const& xdrTxs, @@ -521,14 +488,9 @@ computeLaneBaseFee(TxSetPhase phase, LedgerHeader const& ledgerHeader, return laneBaseFee; } -std::pair> -applySurgePricing(TxSetPhase phase, TxFrameList const& txs, Application& app) +std::shared_ptr +createSurgePricingLangeConfig(TxSetPhase phase, Application& app) { - ZoneScoped; - - auto const& lclHeader = - app.getLedgerManager().getLastClosedLedgerHeader().header; - std::vector hadTxNotFittingLane; std::shared_ptr surgePricingLaneConfig; if (phase == TxSetPhase::CLASSIC) { @@ -556,6 +518,18 @@ applySurgePricing(TxSetPhase phase, TxFrameList const& txs, Application& app) 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), @@ -565,27 +539,102 @@ applySurgePricing(TxSetPhase phase, TxFrameList const& txs, Application& app) surgePricingLaneConfig = std::make_shared(limits); } - auto includedTxs = SurgePricingPriorityQueue::getMostTopTxsWithinLimits( + return surgePricingLaneConfig; +} + +TxFrameList +buildSurgePricedSequentialPhase( + TxFrameList const& txs, + std::shared_ptr surgePricingLaneConfig, + std::vector& hadTxNotFittingLane) +{ + ZoneScoped; + return SurgePricingPriorityQueue::getMostTopTxsWithinLimits( txs, surgePricingLaneConfig, hadTxNotFittingLane); +} - size_t laneCount = surgePricingLaneConfig->getLaneLimits().size(); - std::vector lowestLaneFee(laneCount, - std::numeric_limits::max()); - for (auto const& tx : includedTxs) +std::pair, + std::shared_ptr> +applySurgePricing(TxSetPhase phase, TxFrameList const& txs, Application& app) +{ + 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) + { + includedTxs = buildSurgePricedParallelSorobanPhase( + txs, app.getConfig(), + app.getLedgerManager().getSorobanNetworkConfig(), + surgePricingLaneConfig, hadTxNotFittingLane); + } + else { - size_t lane = surgePricingLaneConfig->getLane(*tx); - auto perOpFee = computePerOpFee(*tx, lclHeader.ledgerVersion); - lowestLaneFee[lane] = std::min(lowestLaneFee[lane], perOpFee); + 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; - for (auto const& tx : includedTxs) - { + visitIncludedTxs([&inclusionFeeMap, &laneBaseFee, + &surgePricingLaneConfig](auto const& tx) { inclusionFeeMap[tx] = laneBaseFee[surgePricingLaneConfig->getLane(*tx)]; - } + }); return std::make_pair(includedTxs, inclusionFeeMapPtr); } @@ -628,6 +677,38 @@ computeBaseFeeForLegacyTxSet(LedgerHeader const& lclHeader, 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) @@ -738,27 +819,26 @@ makeTxSetFromTransactions(PerPhaseTransactionList const& txPhases, auto phaseType = static_cast(i); auto [includedTxs, inclusionFeeMap] = applySurgePricing(phaseType, validatedTxs, app); - if (phaseType != TxSetPhase::SOROBAN || - protocolVersionIsBefore(app.getLedgerManager() - .getLastClosedLedgerHeader() - .header.ledgerVersion, - PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) - { - validatedPhases.emplace_back( - TxSetPhaseFrame(std::move(includedTxs), inclusionFeeMap)); - } - // This is a temporary stub for building a valid parallel tx set - // without any parallelization. - else - { - TxStageFrameList stages; - if (!includedTxs.empty()) - { - stages.emplace_back().push_back(includedTxs); - } - validatedPhases.emplace_back( - TxSetPhaseFrame(std::move(stages), inclusionFeeMap)); - } + + 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(); @@ -812,6 +892,8 @@ makeTxSetFromTransactions(PerPhaseTransactionList const& txPhases, upperBoundCloseTimeOffset); if (!valid) { + outputApplicableTxSet->checkValid(app, lowerBoundCloseTimeOffset, + upperBoundCloseTimeOffset); throw std::runtime_error("Created invalid tx set frame"); } @@ -824,17 +906,15 @@ TxSetXDRFrame::makeEmpty(LedgerHeaderHistoryEntry const& lclHeader) if (protocolVersionStartsFrom(lclHeader.header.ledgerVersion, SOROBAN_PROTOCOL_VERSION)) { - std::vector emptyPhases( - static_cast(TxSetPhase::PHASE_COUNT), - TxSetPhaseFrame::makeEmpty(false)); + bool isParallelSoroban = false; #ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION - if (protocolVersionStartsFrom(lclHeader.header.ledgerVersion, - PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) - { - emptyPhases[static_cast(TxSetPhase::SOROBAN)] = - TxSetPhaseFrame::makeEmpty(true); - } + 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, lclHeader.hash, @@ -903,10 +983,10 @@ makeTxSetFromTransactions(TxFrameList txs, Application& app, std::vector overridePhases; for (size_t i = 0; i < resPhases.size(); ++i) { - overridePhases.emplace_back( - TxSetPhaseFrame(std::move(perPhaseTxs[i]), - std::make_shared( - resPhases[i].getInclusionFeeMap()))); + 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); @@ -957,30 +1037,17 @@ TxSetXDRFrame::prepareForApply(Application& app) const } auto const& xdrPhases = xdrTxSet.v1TxSet().phases; - for (auto const& xdrPhase : xdrPhases) + for (size_t phaseId = 0; phaseId < xdrPhases.size(); ++phaseId) { - auto maybePhase = - TxSetPhaseFrame::makeFromWire(app.getNetworkID(), xdrPhase); + auto maybePhase = TxSetPhaseFrame::makeFromWire( + static_cast(phaseId), app.getNetworkID(), + xdrPhases[phaseId]); if (!maybePhase) { return nullptr; } phaseFrames.emplace_back(std::move(*maybePhase)); } - for (size_t phaseId = 0; phaseId < phaseFrames.size(); ++phaseId) - { - auto phase = static_cast(phaseId); - for (auto const& tx : phaseFrames[phaseId]) - { - if ((tx->isSoroban() && phase != TxSetPhase::SOROBAN) || - (!tx->isSoroban() && phase != TxSetPhase::CLASSIC)) - { - CLOG_DEBUG(Herder, "Got bad generalized txSet with invalid " - "phase transactions"); - return nullptr; - } - } - } } else { @@ -1290,11 +1357,12 @@ TxSetPhaseFrame::Iterator::operator!=(Iterator const& other) const } std::optional -TxSetPhaseFrame::makeFromWire(Hash const& networkID, +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: @@ -1330,7 +1398,9 @@ TxSetPhaseFrame::makeFromWire(Hash const& networkID, break; } } - return TxSetPhaseFrame(std::move(txList), inclusionFeeMapPtr); + phaseFrame.emplace( + TxSetPhaseFrame(phase, std::move(txList), inclusionFeeMapPtr)); + break; } #ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION case 1: @@ -1395,12 +1465,16 @@ TxSetPhaseFrame::makeFromWire(Hash const& networkID, "stages are not sorted"); return std::nullopt; } - return TxSetPhaseFrame(std::move(stages), inclusionFeeMapPtr); + phaseFrame.emplace( + TxSetPhaseFrame(phase, std::move(stages), inclusionFeeMapPtr)); + break; } #endif + default: + releaseAssert(false); } - - return std::nullopt; + releaseAssert(phaseFrame); + return phaseFrame; } std::optional @@ -1424,23 +1498,26 @@ TxSetPhaseFrame::makeFromWireLegacy( { inclusionFeeMap[tx] = baseFee; } - return TxSetPhaseFrame(std::move(txList), inclusionFeeMapPtr); + return TxSetPhaseFrame(TxSetPhase::CLASSIC, std::move(txList), + inclusionFeeMapPtr); } TxSetPhaseFrame -TxSetPhaseFrame::makeEmpty(bool isParallel) +TxSetPhaseFrame::makeEmpty(TxSetPhase phase, bool isParallel) { if (isParallel) { - return TxSetPhaseFrame(TxStageFrameList{}, + return TxSetPhaseFrame(phase, TxStageFrameList{}, std::make_shared()); } - return TxSetPhaseFrame(TxFrameList{}, std::make_shared()); + return TxSetPhaseFrame(phase, TxFrameList{}, + std::make_shared()); } TxSetPhaseFrame::TxSetPhaseFrame( - TxFrameList const& txs, std::shared_ptr inclusionFeeMap) - : mInclusionFeeMap(inclusionFeeMap), mIsParallel(false) + TxSetPhase phase, TxFrameList const& txs, + std::shared_ptr inclusionFeeMap) + : mPhase(phase), mInclusionFeeMap(inclusionFeeMap), mIsParallel(false) { if (!txs.empty()) { @@ -1449,8 +1526,12 @@ TxSetPhaseFrame::TxSetPhaseFrame( } TxSetPhaseFrame::TxSetPhaseFrame( - TxStageFrameList&& txs, std::shared_ptr inclusionFeeMap) - : mStages(txs), mInclusionFeeMap(inclusionFeeMap), mIsParallel(true) + TxSetPhase phase, TxStageFrameList&& txs, + std::shared_ptr inclusionFeeMap) + : mPhase(phase) + , mStages(txs) + , mInclusionFeeMap(inclusionFeeMap) + , mIsParallel(true) { } @@ -1467,24 +1548,41 @@ TxSetPhaseFrame::end() const } size_t -TxSetPhaseFrame::size() const +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 size = 0; - for (auto const& stage : mStages) +size_t +TxSetPhaseFrame::size(LedgerHeader const& lclHeader) const +{ + switch (mPhase) { - for (auto const& thread : stage) - { - size += thread.size(); - } + case TxSetPhase::CLASSIC: + return protocolVersionStartsFrom(lclHeader.ledgerVersion, + ProtocolVersion::V_11) + ? sizeOp() + : sizeTx(); + case TxSetPhase::SOROBAN: + return sizeOp(); } - return size; } bool TxSetPhaseFrame::empty() const { - return size() == 0; + return sizeTx() == 0; } bool @@ -1542,17 +1640,242 @@ TxSetPhaseFrame::sortedForApply(Hash const& txSetHash) const { if (isParallel()) { - return TxSetPhaseFrame(sortedForApplyParallel(mStages, txSetHash), + return TxSetPhaseFrame(mPhase, + sortedForApplyParallel(mStages, txSetHash), mInclusionFeeMap); } else { return TxSetPhaseFrame( - sortedForApplySequential(getSequentialTxs(), txSetHash), + 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, std::vector const& phases, @@ -1646,73 +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; - }; // Generalized transaction sets should always have 2 phases by // construction. releaseAssert(mPhases.size() == static_cast(TxSetPhase::PHASE_COUNT)); - for (auto const& phase : mPhases) - { - if (!checkFeeMap(phase.getInclusionFeeMap())) - { - return false; - } - } - if (mPhases[static_cast(TxSetPhase::CLASSIC)].isParallel()) - { - CLOG_DEBUG(Herder, - "Got bad txSet: classic phase can't be parallel"); - return false; - } - bool needParallelSorobanPhase = protocolVersionStartsFrom( - lcl.header.ledgerVersion, PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION); - if (mPhases[static_cast(TxSetPhase::SOROBAN)].isParallel() != - needParallelSorobanPhase) - { - CLOG_DEBUG(Herder, - "Got bad txSet: Soroban phase parallel support " - "does not match the current protocol; '{}' was " - "expected", - needParallelSorobanPhase); - return false; - } } - - 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) @@ -1720,9 +1985,9 @@ ApplicableTxSetFrame::checkValid(Application& app, // First, ensure the tx set does not contain multiple txs per source // account std::unordered_set seenAccounts; - for (auto const& phaseTxs : mPhases) + for (auto const& phase : mPhases) { - for (auto const& tx : phaseTxs) + for (auto const& tx : phase) { if (!seenAccounts.insert(tx->getSourceID()).second) { @@ -1743,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)) { - LedgerTxn ltx(app.getLedgerTxnRoot()); - 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 : mPhases) + 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; } @@ -1798,12 +2063,7 @@ ApplicableTxSetFrame::size(LedgerHeader const& lh, size_t ApplicableTxSetFrame::sizeOp(TxSetPhase phase) const { - ZoneScoped; - auto const& txs = mPhases.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 @@ -1811,9 +2071,9 @@ ApplicableTxSetFrame::sizeOpTotal() const { ZoneScoped; size_t total = 0; - for (size_t i = 0; i < mPhases.size(); i++) + for (auto const& phase : mPhases) { - total += sizeOp(static_cast(i)); + total += phase.sizeOp(); } return total; } @@ -1821,7 +2081,7 @@ ApplicableTxSetFrame::sizeOpTotal() const size_t ApplicableTxSetFrame::sizeTx(TxSetPhase phase) const { - return mPhases.at(static_cast(phase)).size(); + return mPhases.at(static_cast(phase)).sizeTx(); } size_t @@ -1829,9 +2089,9 @@ ApplicableTxSetFrame::sizeTxTotal() const { ZoneScoped; size_t total = 0; - for (size_t i = 0; i < mPhases.size(); i++) + for (auto const& phase : mPhases) { - total += sizeTx(static_cast(i)); + total += phase.sizeTx(); } return total; } @@ -1839,9 +2099,9 @@ ApplicableTxSetFrame::sizeTxTotal() const std::optional ApplicableTxSetFrame::getTxBaseFee(TransactionFrameBaseConstPtr const& tx) const { - for (auto const& phaseTxs : mPhases) + for (auto const& phase : mPhases) { - auto const& phaseMap = phaseTxs.getInclusionFeeMap(); + auto const& phaseMap = phase.getInclusionFeeMap(); if (auto it = phaseMap.find(tx); it != phaseMap.end()) { return it->second; diff --git a/src/herder/TxSetFrame.h b/src/herder/TxSetFrame.h index beb5c65858..b480bc0150 100644 --- a/src/herder/TxSetFrame.h +++ b/src/herder/TxSetFrame.h @@ -281,7 +281,9 @@ class TxSetPhaseFrame }; Iterator begin() const; Iterator end() const; - size_t size() 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 @@ -312,16 +314,16 @@ class TxSetPhaseFrame TxFrameList& invalidTxs, bool enforceTxsApplyOrder); #endif - - TxSetPhaseFrame(TxFrameList const& txs, + TxSetPhaseFrame(TxSetPhase phase, TxFrameList const& txs, std::shared_ptr inclusionFeeMap); - TxSetPhaseFrame(TxStageFrameList&& txs, + TxSetPhaseFrame(TxSetPhase phase, TxStageFrameList&& txs, std::shared_ptr inclusionFeeMap); // Creates a new phase from `TransactionPhase` XDR coming from a // `GeneralizedTransactionSet`. static std::optional - makeFromWire(Hash const& networkID, TransactionPhase const& xdrPhase); + makeFromWire(TxSetPhase phase, Hash const& networkID, + TransactionPhase const& xdrPhase); // Creates a new phase from all the transactions in the legacy // `TransactionSet` XDR. @@ -330,10 +332,20 @@ class TxSetPhaseFrame xdr::xvector const& xdrTxs); // Creates a valid empty phase with given `isParallel` flag. - static TxSetPhaseFrame makeEmpty(bool isParallel); + 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; @@ -436,6 +448,7 @@ class ApplicableTxSetFrame private: #endif TxSetXDRFrameConstPtr toWireTxSetFrame() const; + std::optional getTxSetSorobanResource() const; private: friend class TxSetXDRFrame; @@ -471,8 +484,6 @@ class ApplicableTxSetFrame ApplicableTxSetFrame(ApplicableTxSetFrame const&) = default; ApplicableTxSetFrame(ApplicableTxSetFrame&&) = default; - std::optional getTxSetSorobanResource() const; - void toXDR(TransactionSet& set) const; void toXDR(GeneralizedTransactionSet& generalizedTxSet) const; diff --git a/src/herder/TxSetUtils.cpp b/src/herder/TxSetUtils.cpp index b51f130538..546044b59f 100644 --- a/src/herder/TxSetUtils.cpp +++ b/src/herder/TxSetUtils.cpp @@ -126,6 +126,10 @@ TxSetUtils::sortParallelTxsInHashOrder(TxStageFrameList const& stages) 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()); }); diff --git a/src/herder/test/TxSetTests.cpp b/src/herder/test/TxSetTests.cpp index 0bcc928bd5..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 @@ -1066,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 0dfaf96641..3656b6a002 100644 --- a/src/herder/test/UpgradesTests.cpp +++ b/src/herder/test/UpgradesTests.cpp @@ -864,22 +864,23 @@ TEST_CASE("config upgrade validation for protocol 22", "[upgrades]") header.scpValue.closeTime = headerTime; ConfigUpgradeSetFrameConstPtr configUpgradeSet; - + LedgerTxn ltx(app->getLedgerTxnRoot()); { Upgrades::UpgradeParameters scheduledUpgrades; - LedgerTxn ltx(app->getLedgerTxnRoot()); - configUpgradeSet = makeParallelComputeUpdgrade(ltx, 10); + LedgerTxn upgradeLtx(ltx); + configUpgradeSet = makeParallelComputeUpdgrade(upgradeLtx, 10); scheduledUpgrades.mUpgradeTime = genesis(0, 1); scheduledUpgrades.mConfigUpgradeSetKey = configUpgradeSet->getKey(); app->getHerder().setUpgrades(scheduledUpgrades); - ltx.commit(); + upgradeLtx.commit(); } - LedgerTxn ltx(app->getLedgerTxnRoot()); + ltx.loadHeader().current() = header; LedgerUpgrade outUpgrade; + auto ls = LedgerSnapshot(ltx); REQUIRE(Upgrades::isValidForApply( toUpgradeType(makeConfigUpgrade(*configUpgradeSet)), - outUpgrade, *app, ltx, header) == expectedRes); + outUpgrade, *app, ls) == expectedRes); }; SECTION("valid for apply") @@ -2346,7 +2347,7 @@ TEST_CASE("configuration initialized in version upgrade", "[upgrades]") TEST_CASE("parallel Soroban settings upgrade", "[upgrades]") { VirtualClock clock; - auto cfg = getTestConfig(0); + auto cfg = getTestConfig(0, Config::TESTDB_IN_MEMORY_NO_OFFERS); cfg.USE_CONFIG_FOR_GENESIS = false; auto app = createTestApplication(clock, cfg); @@ -2364,8 +2365,8 @@ TEST_CASE("parallel Soroban settings upgrade", "[upgrades]") } { - LedgerTxn ltx(app->getLedgerTxnRoot()); - REQUIRE(!ltx.load(getParallelComputeSettingsLedgerKey())); + LedgerSnapshot ls(*app); + REQUIRE(!ls.load(getParallelComputeSettingsLedgerKey())); } executeUpgrade(*app, makeProtocolVersionUpgrade(static_cast( @@ -2373,9 +2374,9 @@ TEST_CASE("parallel Soroban settings upgrade", "[upgrades]") // Make sure initial value is correct. { - LedgerTxn ltx(app->getLedgerTxnRoot()); + LedgerSnapshot ls(*app); auto parellelComputeEntry = - ltx.load(getParallelComputeSettingsLedgerKey()) + ls.load(getParallelComputeSettingsLedgerKey()) .current() .data.configSetting(); REQUIRE(parellelComputeEntry.configSettingID() == @@ -2400,9 +2401,9 @@ TEST_CASE("parallel Soroban settings upgrade", "[upgrades]") executeUpgrade(*app, makeConfigUpgrade(*configUpgradeSet)); } - LedgerTxn ltx(app->getLedgerTxnRoot()); + LedgerSnapshot ls(*app); - REQUIRE(ltx.load(getParallelComputeSettingsLedgerKey()) + REQUIRE(ls.load(getParallelComputeSettingsLedgerKey()) .current() .data.configSetting() .contractParallelCompute() diff --git a/src/ledger/NetworkConfig.cpp b/src/ledger/NetworkConfig.cpp index af863ce741..4ed45b4bbe 100644 --- a/src/ledger/NetworkConfig.cpp +++ b/src/ledger/NetworkConfig.cpp @@ -1344,17 +1344,18 @@ SorobanNetworkConfig::loadFromLedger(AbstractLedgerTxn& ltxRoot, loadExecutionLanesSettings(ltx); loadBucketListSizeWindow(ltx); loadEvictionIterator(ltx); - // NB: this should follow loading state archival settings - maybeUpdateBucketListWindowSize(ltx); - // NB: this should follow loading/updating bucket list window - // size and state archival settings - computeWriteFee(configMaxProtocol, protocolVersion); 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 + // size and state archival settings + computeWriteFee(configMaxProtocol, protocolVersion); } void @@ -2013,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); 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/util/ProtocolVersion.h b/src/util/ProtocolVersion.h index 2769eacd3d..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) @@ -51,5 +52,5 @@ bool protocolVersionEquals(uint32_t protocolVersion, constexpr ProtocolVersion SOROBAN_PROTOCOL_VERSION = ProtocolVersion::V_20; constexpr ProtocolVersion PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION = - ProtocolVersion::V_22; + 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; }