From ba003bbe456f9aa23cc07a5a47c03b90bc67eadc Mon Sep 17 00:00:00 2001 From: Dmytro Kozhevin Date: Thu, 27 Jun 2024 16:59:11 -0400 Subject: [PATCH] Refactor TxSet validation logic. Also added the validation logic for the parallel Soroban phase, which has motivated the refactoring in the first place. The general idea is to move the phase-specific validation into phase frames. Also improved the test coverage for the TxSet validation: - Fixed the test for XDR structure validation - the whole 'valid' section has never been executed. - Added a test for Soroban resource validation - Added more coverage for parallel tx set phase validation --- src/herder/TxSetFrame.cpp | 712 +++++++++------ src/herder/TxSetFrame.h | 44 +- src/herder/test/TestTxSetUtils.cpp | 69 +- src/herder/test/TestTxSetUtils.h | 14 +- src/herder/test/TxSetTests.cpp | 1345 +++++++++++++++++++--------- src/herder/test/UpgradesTests.cpp | 12 +- src/ledger/LedgerManagerImpl.cpp | 10 +- src/ledger/NetworkConfig.cpp | 13 + src/ledger/NetworkConfig.h | 3 + src/util/TxResource.cpp | 3 +- 10 files changed, 1518 insertions(+), 707 deletions(-) diff --git a/src/herder/TxSetFrame.cpp b/src/herder/TxSetFrame.cpp index 9e69d55a5f..eef0c3938f 100644 --- a/src/herder/TxSetFrame.cpp +++ b/src/herder/TxSetFrame.cpp @@ -112,11 +112,11 @@ validateParallelComponent(ParallelTxsComponent const& component) CLOG_DEBUG(Herder, "Got bad txSet: empty stage"); return false; } - for (auto const& thread : stage) + for (auto const& cluster : stage) { - if (thread.empty()) + if (cluster.empty()) { - CLOG_DEBUG(Herder, "Got bad txSet: empty thread"); + CLOG_DEBUG(Herder, "Got bad txSet: empty cluster"); return false; } } @@ -315,13 +315,13 @@ parallelPhaseToXdr(TxStageFrameList const& txs, { auto& xdrStage = component.executionStages.emplace_back(); xdrStage.reserve(stage.size()); - for (auto const& thread : stage) + for (auto const& cluster : stage) { - auto& xdrThread = xdrStage.emplace_back(); - xdrThread.reserve(thread.size()); - for (auto const& tx : thread) + auto& xdrCluster = xdrStage.emplace_back(); + xdrCluster.reserve(cluster.size()); + for (auto const& tx : cluster) { - xdrThread.push_back(tx->getEnvelope()); + xdrCluster.push_back(tx->getEnvelope()); } } } @@ -404,12 +404,12 @@ sortedForApplyParallel(TxStageFrameList const& stages, Hash const& txSetHash) ApplyTxSorter sorter(txSetHash); for (auto& stage : sortedStages) { - for (auto& thread : stage) + for (auto& cluster : stage) { - std::sort(thread.begin(), thread.end(), sorter); + std::sort(cluster.begin(), cluster.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 + // There is no need to shuffle clusters in the stage, as they are + // independent, so the apply order doesn't matter even if the clusters // are being applied sequentially. } std::sort(sortedStages.begin(), sortedStages.end(), @@ -421,41 +421,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; - releaseAssert(threadIsMain()); - // 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 += 1; - for (auto const& tx : phase) - { - auto txResult = tx->checkValid(app.getAppConnector(), 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, @@ -631,6 +596,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) @@ -749,8 +746,8 @@ makeTxSetFromTransactions(PerPhaseTransactionList const& txPhases, .header.ledgerVersion, PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) { - validatedPhases.emplace_back( - TxSetPhaseFrame(std::move(includedTxs), inclusionFeeMap)); + validatedPhases.emplace_back(TxSetPhaseFrame( + phaseType, std::move(includedTxs), inclusionFeeMap)); } // This is a temporary stub for building a valid parallel tx set // without any parallelization. @@ -762,7 +759,7 @@ makeTxSetFromTransactions(PerPhaseTransactionList const& txPhases, stages.emplace_back().push_back(includedTxs); } validatedPhases.emplace_back( - TxSetPhaseFrame(std::move(stages), inclusionFeeMap)); + TxSetPhaseFrame(phaseType, std::move(stages), inclusionFeeMap)); } } @@ -829,17 +826,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, @@ -910,10 +905,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); @@ -964,30 +959,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 { @@ -1052,9 +1034,9 @@ TxSetXDRFrame::sizeTxTotal() const for (auto const& stage : phase.parallelTxsComponent().executionStages) { - for (auto const& thread : stage) + for (auto const& cluster : stage) { - totalSize += thread.size(); + totalSize += cluster.size(); } } break; @@ -1115,11 +1097,11 @@ TxSetXDRFrame::sizeOpTotalForLogging() const for (auto const& stage : phase.parallelTxsComponent().executionStages) { - for (auto const& thread : stage) + for (auto const& cluster : stage) { totalSize += - std::accumulate(thread.begin(), thread.end(), 0ull, - accumulateTxsFn); + std::accumulate(cluster.begin(), cluster.end(), + 0ull, accumulateTxsFn); } } break; @@ -1166,9 +1148,9 @@ TxSetXDRFrame::createTransactionFrames(Hash const& networkID) const for (auto const& stage : phase.parallelTxsComponent().executionStages) { - for (auto const& thread : stage) + for (auto const& cluster : stage) { - for (auto const& tx : thread) + for (auto const& tx : cluster) { txs.emplace_back( TransactionFrameBase::makeTransactionFromWire( @@ -1243,12 +1225,12 @@ TxSetPhaseFrame::Iterator::operator*() const { if (mStageIndex >= mStages.size() || - mThreadIndex >= mStages[mStageIndex].size() || - mTxIndex >= mStages[mStageIndex][mThreadIndex].size()) + mClusterIndex >= mStages[mStageIndex].size() || + mTxIndex >= mStages[mStageIndex][mClusterIndex].size()) { throw std::runtime_error("TxPhase iterator out of bounds"); } - return mStages[mStageIndex][mThreadIndex][mTxIndex]; + return mStages[mStageIndex][mClusterIndex][mTxIndex]; } TxSetPhaseFrame::Iterator& @@ -1259,13 +1241,13 @@ TxSetPhaseFrame::Iterator::operator++() throw std::runtime_error("TxPhase iterator out of bounds"); } ++mTxIndex; - if (mTxIndex >= mStages[mStageIndex][mThreadIndex].size()) + if (mTxIndex >= mStages[mStageIndex][mClusterIndex].size()) { mTxIndex = 0; - ++mThreadIndex; - if (mThreadIndex >= mStages[mStageIndex].size()) + ++mClusterIndex; + if (mClusterIndex >= mStages[mStageIndex].size()) { - mThreadIndex = 0; + mClusterIndex = 0; ++mStageIndex; } } @@ -1284,7 +1266,7 @@ bool TxSetPhaseFrame::Iterator::operator==(Iterator const& other) const { return mStageIndex == other.mStageIndex && - mThreadIndex == other.mThreadIndex && mTxIndex == other.mTxIndex && + mClusterIndex == other.mClusterIndex && mTxIndex == other.mTxIndex && // Make sure to compare the pointers, not the contents, both for // correctness and optimization. &mStages == &other.mStages; @@ -1297,11 +1279,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: @@ -1337,7 +1320,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: @@ -1354,11 +1339,11 @@ TxSetPhaseFrame::makeFromWire(Hash const& networkID, { auto& stage = stages.emplace_back(); stage.reserve(xdrStage.size()); - for (auto const& xdrThread : xdrStage) + for (auto const& xdrCluster : xdrStage) { - auto& thread = stage.emplace_back(); - thread.reserve(xdrThread.size()); - for (auto const& env : xdrThread) + auto& cluster = stage.emplace_back(); + cluster.reserve(xdrCluster.size()); + for (auto const& env : xdrCluster) { auto tx = TransactionFrameBase::makeTransactionFromWire( networkID, env); @@ -1368,14 +1353,14 @@ TxSetPhaseFrame::makeFromWire(Hash const& networkID, "transaction has invalid XDR"); return std::nullopt; } - thread.push_back(tx); + cluster.push_back(tx); inclusionFeeMap[tx] = baseFee; } - if (!std::is_sorted(thread.begin(), thread.end(), + if (!std::is_sorted(cluster.begin(), cluster.end(), &TxSetUtils::hashTxSorter)) { CLOG_DEBUG(Herder, "Got bad generalized txSet: " - "thread is not sorted"); + "cluster is not sorted"); return std::nullopt; } } @@ -1402,12 +1387,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 @@ -1431,23 +1420,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()) { @@ -1456,8 +1448,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) { } @@ -1474,24 +1470,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 @@ -1549,17 +1562,296 @@ 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(); + // Verify the fee map for the phase. This check is independent of the phase + // type or contents. + if (!checkFeeMap(getInclusionFeeMap(), lcl.header)) + { + return false; + } + + bool isSoroban = mPhase == TxSetPhase::SOROBAN; + + // Ensure that the phase contains only the transactions of expected + // kind (Soroban or classic). + 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; + } + } + + // Then check the phase-specific properties. This may rely on transactions + // belonging to the valid phase. + bool checkPhaseSpecific = + isSoroban + ? checkValidSoroban( + lcl.header, + app.getLedgerManager().getSorobanNetworkConfigReadOnly()) + : checkValidClassic(lcl.header); + if (!checkPhaseSpecific) + { + 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; + } + // Ensure the total resources are not over ledger limit. + auto totalResources = getTotalResources(); + if (!totalResources) + { + CLOG_DEBUG(Herder, "Got bad txSet: total Soroban resources overflow"); + return false; + } + + auto maxResources = sorobanConfig.maxLedgerResources(); + + if (protocolVersionStartsFrom(lclHeader.ledgerVersion, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) + { + maxResources.setVal(Resource::Type::INSTRUCTIONS, + std::numeric_limits::max()); + } + if (anyGreater(*totalResources, maxResources)) + { + CLOG_DEBUG(Herder, + "Got bad txSet: needed resources exceed ledger " + "limits {} > {}", + totalResources->toString(), maxResources.toString()); + return false; + } + + if (!isParallel()) + { + return true; + } + auto const& stages = getParallelStages(); + + // Verify that number of clusters is not exceeded per stage. There is no + // limit for the number of stages or transactions per cluster. + for (auto const& stage : stages) + { + if (stage.size() > sorobanConfig.ledgerMaxDependentTxClusters()) + { + CLOG_DEBUG(Herder, + "Got bad txSet: too many clusters in Soroban " + "stage {} > {}", + stage.size(), + sorobanConfig.ledgerMaxDependentTxClusters()); + return false; + } + } + + // Verify that 'sequential' instructions don't exceed the ledger-wide + // limit. + // Every may have multiple clusters and its runtime is considered to be + // bounded by the slowest cluster (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& cluster : stage) + { + int64_t clusterInstructions = 0; + for (auto const& tx : cluster) + { + // clusterInstructions + tx->sorobanResources().instructions > + // std::numeric_limits::max() + if (clusterInstructions > + std::numeric_limits::max() - + tx->sorobanResources().instructions) + { + CLOG_DEBUG(Herder, "Got bad txSet: Soroban sequential " + "instructions overflow"); + return false; + } + clusterInstructions += tx->sorobanResources().instructions; + } + stageInstructions = + std::max(stageInstructions, clusterInstructions); + } + // 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 clusters within + // every stage. + for (auto const& stage : stages) + { + UnorderedSet stageReadOnlyKeys; + UnorderedSet stageReadWriteKeys; + for (auto const& cluster : stage) + { + std::vector clusterReadOnlyKeys; + std::vector clusterReadWriteKeys; + for (auto const& tx : cluster) + { + auto const& footprint = tx->sorobanResources().footprint; + + for (auto const& key : footprint.readOnly) + { + if (stageReadWriteKeys.count(key) > 0) + { + CLOG_DEBUG( + Herder, + "Got bad generalized txSet: cluster footprint " + "conflicts with another cluster within stage"); + return false; + } + clusterReadOnlyKeys.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: cluster footprint " + "conflicts with another cluster within stage"); + return false; + } + clusterReadWriteKeys.push_back(key); + } + } + stageReadOnlyKeys.insert(clusterReadOnlyKeys.begin(), + clusterReadOnlyKeys.end()); + stageReadWriteKeys.insert(clusterReadWriteKeys.begin(), + clusterReadWriteKeys.end()); + } + } + return true; +} + +// This assumes that the overall phase structure 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.getAppConnector(), 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; +} + +std::optional +TxSetPhaseFrame::getTotalResources() const +{ + auto total = mPhase == TxSetPhase::SOROBAN ? Resource::makeEmptySoroban() + : Resource::makeEmpty(1); + for (auto const& tx : *this) + { + if (total.canAdd(tx->getResources(/* useByteLimitInClassic */ false))) + { + total += tx->getResources(/* useByteLimitInClassic */ false); + } + else + { + return std::nullopt; + } + } + return std::make_optional(total); +} + ApplicableTxSetFrame::ApplicableTxSetFrame( Application& app, bool isGeneralized, Hash const& previousLedgerHash, std::vector const& phases, @@ -1659,83 +1951,25 @@ 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) { - // First, ensure the tx set does not contain multiple txs per source + // 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) { @@ -1746,64 +1980,34 @@ ApplicableTxSetFrame::checkValid(Application& app, } } } - - // Second, ensure total resources are not over ledger limit - auto totalTxSetRes = getTxSetSorobanResource(); - if (!totalTxSetRes) - { - CLOG_DEBUG(Herder, - "Got bad txSet: total Soroban resources overflow"); - return false; - } - - { - 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; - } - } } - 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; } @@ -1811,12 +2015,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 @@ -1824,9 +2023,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; } @@ -1834,7 +2033,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 @@ -1842,9 +2041,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; } @@ -1852,9 +2051,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; @@ -1863,25 +2062,6 @@ ApplicableTxSetFrame::getTxBaseFee(TransactionFrameBaseConstPtr const& tx) const throw std::runtime_error("Transaction not found in tx set"); } -std::optional -ApplicableTxSetFrame::getTxSetSorobanResource() const -{ - releaseAssert(mPhases.size() > static_cast(TxSetPhase::SOROBAN)); - auto total = Resource::makeEmptySoroban(); - for (auto const& tx : mPhases[static_cast(TxSetPhase::SOROBAN)]) - { - if (total.canAdd(tx->getResources(/* useByteLimitInClassic */ false))) - { - total += tx->getResources(/* useByteLimitInClassic */ false); - } - else - { - return std::nullopt; - } - } - return std::make_optional(total); -} - int64_t ApplicableTxSetFrame::getTotalFees(LedgerHeader const& lh) const { diff --git a/src/herder/TxSetFrame.h b/src/herder/TxSetFrame.h index beb5c65858..d5fe7d433c 100644 --- a/src/herder/TxSetFrame.h +++ b/src/herder/TxSetFrame.h @@ -196,18 +196,18 @@ class TxSetXDRFrame : public NonMovableOrCopyable // - 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 +// - A 'stage' (`TxStageFrame`) consists of several independent 'clusters' +// (`TxClusterFrame`). Transactions in different 'clusters' are independent of // each other and can be applied in parallel. -// - A 'thread' (`TxThreadFrame`) consists of transactions that should +// - A 'cluster' (`TxClusterFrame`) 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 +// the cluster are necessarily conflicting with each other; it is possible +// that some, or even all transactions in the cluster 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 TxClusterFrame = TxFrameList; +using TxStageFrame = std::vector; using TxStageFrameList = std::vector; // Alias for the map from transaction to its inclusion fee as defined by the @@ -276,12 +276,14 @@ class TxSetPhaseFrame Iterator(TxStageFrameList const& txs, size_t stageIndex); TxStageFrameList const& mStages; size_t mStageIndex = 0; - size_t mThreadIndex = 0; + size_t mClusterIndex = 0; size_t mTxIndex = 0; }; 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 @@ -289,6 +291,8 @@ class TxSetPhaseFrame // transactions in the same lane) InclusionFeeMap const& getInclusionFeeMap() const; + std::optional getTotalResources() const; + private: friend class TxSetXDRFrame; friend class ApplicableTxSetFrame; @@ -312,16 +316,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 +334,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; @@ -471,8 +485,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/test/TestTxSetUtils.cpp b/src/herder/test/TestTxSetUtils.cpp index 41692761d9..17c8ae3448 100644 --- a/src/herder/test/TestTxSetUtils.cpp +++ b/src/herder/test/TestTxSetUtils.cpp @@ -31,7 +31,7 @@ makeTxSetXDR(std::vector const& txs, } GeneralizedTransactionSet -makeGeneralizedTxSetXDR(std::vector const& phases, +makeGeneralizedTxSetXDR(std::vector const& phases, Hash const& previousLedgerHash, bool useParallelSorobanPhase) { @@ -76,11 +76,11 @@ makeGeneralizedTxSetXDR(std::vector const& phases, } if (!txs.empty()) { - auto& thread = + auto& cluster = component.executionStages.emplace_back().emplace_back(); for (auto const& tx : txs) { - thread.emplace_back(tx->getEnvelope()); + cluster.emplace_back(tx->getEnvelope()); } } #else @@ -120,7 +120,7 @@ makeNonValidatedTxSet(std::vector const& txs, std::pair makeNonValidatedGeneralizedTxSet( - std::vector const& txsPerBaseFee, Application& app, + std::vector const& txsPerBaseFee, Application& app, Hash const& previousLedgerHash, std::optional useParallelSorobanPhase) { if (!useParallelSorobanPhase.has_value()) @@ -157,5 +157,66 @@ makeNonValidatedTxSetBasedOnLedgerVersion( } } +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +void +normalizeParallelPhaseXDR(TransactionPhase& phase) +{ + auto compareTxHash = [](TransactionEnvelope const& tx1, + TransactionEnvelope const& tx2) -> bool { + return xdrSha256(tx1) < xdrSha256(tx2); + }; + for (auto& stage : phase.parallelTxsComponent().executionStages) + { + for (auto& cluster : stage) + { + std::sort(cluster.begin(), cluster.end(), compareTxHash); + } + std::sort(stage.begin(), stage.end(), + [&](auto const& c1, auto const& c2) { + return compareTxHash(c1.front(), c2.front()); + }); + } + std::sort(phase.parallelTxsComponent().executionStages.begin(), + phase.parallelTxsComponent().executionStages.end(), + [&](auto const& s1, auto const& s2) { + return compareTxHash(s1.front().front(), s2.front().front()); + }); +} + +std::pair +makeNonValidatedGeneralizedTxSet(PhaseComponents const& classicTxsPerBaseFee, + std::optional sorobanBaseFee, + TxStageFrameList const& sorobanTxsPerStage, + Application& app, + Hash const& previousLedgerHash) +{ + auto xdrTxSet = makeGeneralizedTxSetXDR({classicTxsPerBaseFee}, + previousLedgerHash, false); + xdrTxSet.v1TxSet().phases.emplace_back(1); + auto& phase = xdrTxSet.v1TxSet().phases.back(); + if (sorobanBaseFee) + { + phase.parallelTxsComponent().baseFee.activate() = *sorobanBaseFee; + } + + auto& stages = phase.parallelTxsComponent().executionStages; + for (auto const& stage : sorobanTxsPerStage) + { + auto& xdrStage = stages.emplace_back(); + for (auto const& cluster : stage) + { + auto& xdrCluster = xdrStage.emplace_back(); + for (auto const& tx : cluster) + { + xdrCluster.emplace_back(tx->getEnvelope()); + } + } + } + normalizeParallelPhaseXDR(phase); + auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); + return std::make_pair(txSet, txSet->prepareForApply(app)); +} +#endif + } // namespace testtxset } // namespace stellar diff --git a/src/herder/test/TestTxSetUtils.h b/src/herder/test/TestTxSetUtils.h index b87e3a92d0..b7275e7623 100644 --- a/src/herder/test/TestTxSetUtils.h +++ b/src/herder/test/TestTxSetUtils.h @@ -12,11 +12,11 @@ namespace stellar namespace testtxset { -using ComponentPhases = std::vector< +using PhaseComponents = std::vector< std::pair, std::vector>>; std::pair makeNonValidatedGeneralizedTxSet( - std::vector const& txsPerBaseFee, Application& app, + std::vector const& txsPerBaseFee, Application& app, Hash const& previousLedgerHash, std::optional useParallelSorobanPhase = std::nullopt); @@ -24,5 +24,15 @@ std::pair makeNonValidatedTxSetBasedOnLedgerVersion( std::vector const& txs, Application& app, Hash const& previousLedgerHash); +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION +void normalizeParallelPhaseXDR(TransactionPhase& phase); + +std::pair +makeNonValidatedGeneralizedTxSet(PhaseComponents const& classicTxsPerBaseFee, + std::optional sorobanBaseFee, + TxStageFrameList const& sorobanTxsPerStage, + Application& app, + Hash const& previousLedgerHash); +#endif } // namespace testtxset } // namespace stellar diff --git a/src/herder/test/TxSetTests.cpp b/src/herder/test/TxSetTests.cpp index d83ef7cf1c..89f9fa4c4f 100644 --- a/src/herder/test/TxSetTests.cpp +++ b/src/herder/test/TxSetTests.cpp @@ -2,6 +2,7 @@ // 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 "crypto/SHA.h" #include "herder/TxSetFrame.h" #include "herder/test/TestTxSetUtils.h" #include "ledger/LedgerManager.h" @@ -13,6 +14,8 @@ #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" #include "util/XDRCereal.h" @@ -25,10 +28,9 @@ using namespace txtest; TEST_CASE("generalized tx set XDR validation", "[txset]") { Config 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; VirtualClock clock; Application::pointer app = createTestApplication(clock, cfg); @@ -41,6 +43,12 @@ TEST_CASE("generalized tx set XDR validation", "[txset]") auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); REQUIRE(txSet->prepareForApply(*app) == nullptr); } + SECTION("one phase") + { + auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); + xdrTxSet.v1TxSet().phases.emplace_back(); + REQUIRE(txSet->prepareForApply(*app) == nullptr); + } SECTION("too many phases") { xdrTxSet.v1TxSet().phases.emplace_back(); @@ -49,424 +57,395 @@ TEST_CASE("generalized tx set XDR validation", "[txset]") auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); REQUIRE(txSet->prepareForApply(*app) == nullptr); } - SECTION("incorrect base fee order") + + SECTION("two phase scenarios") { xdrTxSet.v1TxSet().phases.emplace_back(); xdrTxSet.v1TxSet().phases.emplace_back(); - for (int i = 0; i < xdrTxSet.v1TxSet().phases.size(); ++i) - { - SECTION("phase " + std::to_string(i)) + int txId = 0; + auto buildTx = [&](TransactionEnvelope& txEnv, bool isSoroban) { + txEnv.v1().tx.operations.emplace_back(); + // The fee is actually not relevant for XDR validation, we just use + // it to have different tx envelopes. + txEnv.v1().tx.fee = 100 + txId; + ++txId; + txEnv.v1().tx.operations.back().body.type( + isSoroban ? OperationType::INVOKE_HOST_FUNCTION + : OperationType::PAYMENT); + if (isSoroban) { - SECTION("all components discounted") + txEnv.v1().tx.ext.v(1); + txEnv.v1().tx.ext.sorobanData().resourceFee = 1000; + } + }; + auto compareTxHash = [](TransactionEnvelope const& tx1, + TransactionEnvelope const& tx2) -> bool { + return xdrSha256(tx1) < xdrSha256(tx2); + }; + auto v0Phase = + [&](std::vector> componentBaseFees, + bool isSoroban) -> TransactionPhase { + TransactionPhase phase(0); + for (auto const& baseFee : componentBaseFees) + { + auto& component = phase.v0Components().emplace_back( + TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); + if (baseFee) { - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1500; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1400; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1600; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - - auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); - REQUIRE(txSet->prepareForApply(*app) == nullptr); + component.txsMaybeDiscountedFee().baseFee.activate() = + *baseFee; } - SECTION("non-discounted component out of place") + for (int i = 0; i < 10; ++i) { + auto& txEnv = + component.txsMaybeDiscountedFee().txs.emplace_back( + ENVELOPE_TYPE_TX); + buildTx(txEnv, isSoroban); + } + std::sort(component.txsMaybeDiscountedFee().txs.begin(), + component.txsMaybeDiscountedFee().txs.end(), + compareTxHash); + } + return phase; + }; + // Collection of per-phase scenarios: each scenario consists of a phase + // XDR, a flag indicating whether the XDR is valid or not, and a string + // name of the scenario. + std::vector< + std::vector>> + scenarios(static_cast(TxSetPhase::PHASE_COUNT)); + // Most of the scenarios are the same for Soroban and Classic phases, so + // generate the same scenarios for both. + for (int phaseId = 0; + phaseId < static_cast(TxSetPhase::PHASE_COUNT); ++phaseId) + { + bool isSoroban = phaseId == static_cast(TxSetPhase::SOROBAN); + + // Valid scenarios + scenarios[phaseId].emplace_back(v0Phase({}, isSoroban), true, + "no txs"); + scenarios[phaseId].emplace_back(v0Phase({std::nullopt}, isSoroban), + true, + "single no discount component"); + scenarios[phaseId].emplace_back(v0Phase({1000}, isSoroban), true, + "single discount component"); + scenarios[phaseId].emplace_back( + v0Phase({1000, 1001, 1002}, isSoroban), true, + "multiple discount components"); + auto validMultiComponentPhase = + v0Phase({std::nullopt, 1000, 2000, 3000}, isSoroban); + scenarios[phaseId].emplace_back( + validMultiComponentPhase, true, + "multiple discount components and no discount component"); - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1500; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1600; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); + { + auto phase = validMultiComponentPhase; + phase.v0Components().emplace_back( + TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); + phase.v0Components().back() = phase.v0Components()[1]; + phase.v0Components() + .back() + .txsMaybeDiscountedFee() + .baseFee.activate() = 10000; + // Note, that during XDR validation we don't try to check that + // transactions are unique across components or phases. + scenarios[phaseId].emplace_back(phase, true, + "duplicate txs across phases"); + } + + // Invalid scenarios + scenarios[phaseId].emplace_back( + v0Phase({1000, 3000, 2000}, isSoroban), false, + "incorrect discounted component order"); + scenarios[phaseId].emplace_back( + v0Phase({1000, std::nullopt, 2000}, isSoroban), false, + "incorrect non-discounted component order"); + scenarios[phaseId].emplace_back( + v0Phase({std::nullopt, std::nullopt, 1000}, isSoroban), false, + "duplicate non-discounted component"); + scenarios[phaseId].emplace_back( + v0Phase({std::nullopt, 1000, 1000, 2000}, isSoroban), false, + "duplicate discounted component"); + { + auto phase = v0Phase({1000}, isSoroban); + phase.v0Components()[0].txsMaybeDiscountedFee().txs.clear(); - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); + scenarios[phaseId].emplace_back(phase, false, + "single empty component"); + } + { + auto phase = validMultiComponentPhase; + phase.v0Components()[3].txsMaybeDiscountedFee().txs.clear(); - auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); - REQUIRE(txSet->prepareForApply(*app) == nullptr); + scenarios[phaseId].emplace_back( + phase, false, + "one empty component among non-empty components"); + } + { + auto phase = validMultiComponentPhase; + phase.v0Components()[1] + .txsMaybeDiscountedFee() + .txs.emplace_back( + phase.v0Components()[1].txsMaybeDiscountedFee().txs[5]); + std::sort( + phase.v0Components()[0].txsMaybeDiscountedFee().txs.begin(), + phase.v0Components()[0].txsMaybeDiscountedFee().txs.end(), + compareTxHash); + scenarios[phaseId].emplace_back( + phase, false, "duplicate txs within a component"); + } + { + auto phase = validMultiComponentPhase; + std::swap( + phase.v0Components()[2].txsMaybeDiscountedFee().txs[4], + phase.v0Components()[2].txsMaybeDiscountedFee().txs[5]); + + scenarios[phaseId].emplace_back( + phase, false, "non-canonical tx order within component"); + } + + // Invalid scenarios specific to Soroban. + if (isSoroban) + { + { + auto phase = validMultiComponentPhase; + TransactionEnvelope tx(EnvelopeType::ENVELOPE_TYPE_TX_V0); + tx.v0().tx.operations = phase.v0Components()[1] + .txsMaybeDiscountedFee() + .txs[0] + .v1() + .tx.operations; + phase.v0Components()[1].txsMaybeDiscountedFee().txs[0] = tx; + std::sort(phase.v0Components()[1] + .txsMaybeDiscountedFee() + .txs.begin(), + phase.v0Components()[1] + .txsMaybeDiscountedFee() + .txs.end(), + compareTxHash); + scenarios[phaseId].emplace_back( + phase, false, "v0 envelope for Soroban tx"); } - SECTION( - "with non-discounted component, discounted out of place") { - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() + auto phase = validMultiComponentPhase; + phase.v0Components()[0] .txsMaybeDiscountedFee() - .txs.emplace_back(); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1500; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1400; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() + .txs[7] + .v1() + .tx.ext.v(0); + std::sort(phase.v0Components()[0] + .txsMaybeDiscountedFee() + .txs.begin(), + phase.v0Components()[0] + .txsMaybeDiscountedFee() + .txs.end(), + compareTxHash); + scenarios[phaseId].emplace_back( + phase, false, "Soroban tx without extension"); + } + { + auto phase = validMultiComponentPhase; + phase.v0Components()[3] .txsMaybeDiscountedFee() - .txs.emplace_back(); - - auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); - REQUIRE(txSet->prepareForApply(*app) == nullptr); + .txs[4] + .v1() + .tx.ext.sorobanData() + .resourceFee = -1; + std::sort(phase.v0Components()[3] + .txsMaybeDiscountedFee() + .txs.begin(), + phase.v0Components()[3] + .txsMaybeDiscountedFee() + .txs.end(), + compareTxHash); + + scenarios[phaseId].emplace_back( + phase, false, "Soroban tx with negative resource fee"); } } } - } - SECTION("duplicate base fee") - { - xdrTxSet.v1TxSet().phases.emplace_back(); - xdrTxSet.v1TxSet().phases.emplace_back(); - for (int i = 0; i < xdrTxSet.v1TxSet().phases.size(); ++i) - { - SECTION("phase " + std::to_string(i)) + // When doing XDR conversion we don't verify that transactions belong + // to the correct phase, so we can also swap the phase for every + // scenario without changing the XDR validity conditions. + auto classicScenariosSize = scenarios[0].size(); + scenarios[0].insert(scenarios[0].end(), scenarios[1].begin(), + scenarios[1].end()); + scenarios[1].insert(scenarios[1].end(), scenarios[1].begin(), + scenarios[1].begin() + classicScenariosSize); +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + // Scenarios for the Soroban parallel phase. + auto parallelPhase = [&](std::vector> shape, + bool normalize = true, + std::optional baseFee = + std::nullopt) -> TransactionPhase { + TransactionPhase phase(1); + if (baseFee) { - SECTION("duplicate discounts") - { - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1500; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1500; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1600; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - - auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); - REQUIRE(txSet->prepareForApply(*app) == nullptr); - } - SECTION("duplicate non-discounted components") + phase.parallelTxsComponent().baseFee.activate() = *baseFee; + } + for (auto const& stageClusters : shape) + { + auto& stage = + phase.parallelTxsComponent().executionStages.emplace_back(); + for (int txCount : stageClusters) { + auto& cluster = stage.emplace_back(); - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1500; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); + for (int i = 0; i < txCount; ++i) + { + auto& txEnv = cluster.emplace_back( + EnvelopeType::ENVELOPE_TYPE_TX); + buildTx(txEnv, true); + } - auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); - REQUIRE(txSet->prepareForApply(*app) == nullptr); + std::sort(cluster.begin(), cluster.end(), compareTxHash); } } - } - } - SECTION("empty component") - { - xdrTxSet.v1TxSet().phases.emplace_back(); - xdrTxSet.v1TxSet().phases.emplace_back(); - - for (int i = 0; i < xdrTxSet.v1TxSet().phases.size(); ++i) - { - SECTION("phase " + std::to_string(i)) + if (normalize) { - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - - auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); - REQUIRE(txSet->prepareForApply(*app) == nullptr); + testtxset::normalizeParallelPhaseXDR(phase); } + + return phase; + }; + + auto prevScenariosSize = scenarios[1].size(); + + // Valid scenarios + scenarios[1].emplace_back(parallelPhase({}), true, + "parallel Soroban - no txs"); + scenarios[1].emplace_back(parallelPhase({}, true, 1000), true, + "parallel Soroban - no txs, fee discount"); + scenarios[1].emplace_back(parallelPhase({{10}}), true, + "parallel Soroban - 1 stage, 1 cluster"); + scenarios[1].emplace_back( + parallelPhase({{10, 2, 14, 1, 3}}), true, + "parallel Soroban - 1 stage, multiple clusters"); + scenarios[1].emplace_back( + parallelPhase({{1}, {3}, {2}}), true, + "parallel Soroban - multiple single-cluster stages"); + + auto validMultiStagePhase = + parallelPhase({{2, 3, 4, 5}, {3, 2}, {5, 4, 5}, {2, 4}}); + scenarios[1].emplace_back( + validMultiStagePhase, true, + "parallel Soroban - multiple multi-cluster stages"); + scenarios[1].emplace_back( + parallelPhase({{1, 2, 3, 4, 5}, {2}, {5, 4, 1}, {1, 1}}, true, + 1000), + true, + "parallel Soroban - multiple multi-cluster stages with fee " + "discount"); + + // Invalid scenarios + scenarios[1].emplace_back( + parallelPhase({{0}}, false), false, + "parallel Soroban - single stage with empty cluster"); + scenarios[1].emplace_back( + parallelPhase({{2}, {}}, false), false, + "parallel Soroban - one of the stages has no clusters"); + scenarios[1].emplace_back( + parallelPhase({{2}, {{0}}, {3, 5}}, false), false, + "parallel Soroban - one of the stages has empty cluster"); + scenarios[1].emplace_back( + parallelPhase({{2}, {{0}}}, false), false, + "parallel Soroban - one of the stages has empty cluster"); + scenarios[1].emplace_back(parallelPhase({{}, {}, {}}, false), false, + "parallel Soroban - multiple empty stages"); + scenarios[1].emplace_back( + parallelPhase({{{}, {0}}, {{0}}, {}}, false), false, + "parallel Soroban - multiple empty and empty cluster stages"); + scenarios[1].emplace_back( + parallelPhase({{10, 2, 0, 1, 3}}, false), false, + "parallel Soroban - empty cluster among non-empty ones"); + scenarios[1].emplace_back(parallelPhase({{0, 1, 0, 0, 3}}, false), + false, + "parallel Soroban - multiple empty clusters"); + { + auto phase = validMultiStagePhase; + std::swap(phase.parallelTxsComponent().executionStages[1], + phase.parallelTxsComponent().executionStages[2]); + scenarios[1].emplace_back( + phase, false, "parallel Soroban - stages incorrectly ordered"); } - } - SECTION("wrong tx type in phases") - { - xdrTxSet.v1TxSet().phases.emplace_back(); - xdrTxSet.v1TxSet().phases.emplace_back(); - SECTION("classic phase") { - xdrTxSet.v1TxSet().phases[1].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[1] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); + auto phase = validMultiStagePhase; + std::swap(phase.parallelTxsComponent().executionStages[1][0], + phase.parallelTxsComponent().executionStages[2][1]); + scenarios[1].emplace_back( + phase, false, + "parallel Soroban - clusters incorrectly ordered"); } - SECTION("soroban phase") { - xdrTxSet.v1TxSet().phases[0].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[0] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - - auto& txEnv = xdrTxSet.v1TxSet() - .phases[0] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.back(); - txEnv.v0().tx.operations.emplace_back(); - txEnv.v0().tx.operations.back().body.type(INVOKE_HOST_FUNCTION); + auto phase = validMultiStagePhase; + std::swap(phase.parallelTxsComponent().executionStages[1][1][0], + phase.parallelTxsComponent().executionStages[1][1][1]); + scenarios[1].emplace_back(phase, false, + "parallel Soroban - transactions " + "incorrectly ordered within cluster"); } - auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); - REQUIRE(txSet->prepareForApply(*app) == nullptr); - } - SECTION("valid XDR") - { + { + auto phase = validMultiStagePhase; + TransactionEnvelope tx(EnvelopeType::ENVELOPE_TYPE_TX_V0); + tx.v0().tx.operations = phase.parallelTxsComponent() + .executionStages[2][1][1] + .v1() + .tx.operations; + phase.parallelTxsComponent().executionStages[2][1][1] = tx; + testtxset::normalizeParallelPhaseXDR(phase); + scenarios[1].emplace_back( + phase, false, "parallel Soroban - v0 envelope for Soroban tx"); + } + { + auto phase = validMultiStagePhase; + phase.parallelTxsComponent().executionStages[3][0][0].v1().tx.ext.v( + 0); + testtxset::normalizeParallelPhaseXDR(phase); + scenarios[1].emplace_back( + phase, false, + "parallel Soroban - Soroban tx without extension"); + } + { + auto phase = validMultiStagePhase; + phase.parallelTxsComponent() + .executionStages[0][1][1] + .v1() + .tx.ext.sorobanData() + .resourceFee = -1; + testtxset::normalizeParallelPhaseXDR(phase); + + scenarios[1].emplace_back( + phase, false, + "parallel Soroban - Soroban tx with negative resource fee"); + } + // Also add all the parallel Soroban scenarios to the classic phase - + // this is never valid as we don't allow parallel component in classic + // phase, but some additional coverage wouldn't hurt. + for (size_t i = prevScenariosSize; i < scenarios[1].size(); ++i) + { + scenarios[0].emplace_back(scenarios[1][i]); + std::get<1>(scenarios[0].back()) = false; + } +#endif + for (auto const& [classicPhase, classicIsValid, classicScenario] : + scenarios[0]) - for (int i = 0; i < xdrTxSet.v1TxSet().phases.size(); ++i) { - auto maybeAddSorobanOp = [&](GeneralizedTransactionSet& txSet) { - if (i == 1) - { - auto& txEnv = xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.back(); - txEnv.v0().tx.operations.emplace_back(); - txEnv.v0().tx.operations.back().body.type( - INVOKE_HOST_FUNCTION); // tx->isSoroban() == - // true - } - }; - SECTION("phase " + std::to_string(i)) + xdrTxSet.v1TxSet().phases[0] = classicPhase; + for (auto const& [sorobanPhase, sorobanIsValid, sorobanScenario] : + scenarios[1]) { - SECTION("no transactions") + xdrTxSet.v1TxSet().phases[1] = sorobanPhase; + auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); + INFO("Classic phase: " + classicScenario + + ", Soroban phase: " + sorobanScenario); + bool valid = classicIsValid && sorobanIsValid; + if (valid) { - xdrTxSet.v1TxSet().phases.emplace_back(); - xdrTxSet.v1TxSet().phases.emplace_back(); - auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); - REQUIRE(txSet->prepareForApply(*app) == nullptr); + REQUIRE(txSet->prepareForApply(*app) != nullptr); } - SECTION("single component") + else { - xdrTxSet.v1TxSet().phases.emplace_back(); - xdrTxSet.v1TxSet().phases.emplace_back(); - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - maybeAddSorobanOp(xdrTxSet); - auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); - REQUIRE(txSet->prepareForApply(*app) == nullptr); - } - SECTION("multiple components") - { - xdrTxSet.v1TxSet().phases.emplace_back(); - xdrTxSet.v1TxSet().phases.emplace_back(); - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - maybeAddSorobanOp(xdrTxSet); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1400; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - maybeAddSorobanOp(xdrTxSet); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1500; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - maybeAddSorobanOp(xdrTxSet); - - xdrTxSet.v1TxSet().phases[i].v0Components().emplace_back( - TXSET_COMP_TXS_MAYBE_DISCOUNTED_FEE); - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .baseFee.activate() = 1600; - xdrTxSet.v1TxSet() - .phases[i] - .v0Components() - .back() - .txsMaybeDiscountedFee() - .txs.emplace_back(); - maybeAddSorobanOp(xdrTxSet); - - auto txSet = TxSetXDRFrame::makeFromWire(xdrTxSet); REQUIRE(txSet->prepareForApply(*app) == nullptr); } } @@ -491,9 +470,10 @@ testGeneralizedTxSetXDRConversion(ProtocolVersion protocolVersion) sorobanCfg.mLedgerMaxTxCount = 5; }); auto root = TestAccount::createRoot(*app); + int accountId = 0; auto createTxs = [&](int cnt, int fee, bool isSoroban = false) { - std::vector txs; + std::vector txs; for (int i = 0; i < cnt; ++i) { auto source = @@ -519,15 +499,10 @@ testGeneralizedTxSetXDRConversion(ProtocolVersion protocolVersion) } return txs; }; - auto checkXdrRoundtrip = [&](GeneralizedTransactionSet const& txSetXdr) { auto txSetFrame = TxSetXDRFrame::makeFromWire(txSetXdr); - ApplicableTxSetFrameConstPtr applicableFrame; - { - LedgerTxn ltx(app->getLedgerTxnRoot()); - applicableFrame = txSetFrame->prepareForApply(*app); - } - + ApplicableTxSetFrameConstPtr applicableFrame = + txSetFrame->prepareForApply(*app); REQUIRE(applicableFrame->checkValid(*app, 0, 0)); GeneralizedTransactionSet newXdr; applicableFrame->toWireTxSetFrame()->toXDR(newXdr); @@ -536,25 +511,25 @@ testGeneralizedTxSetXDRConversion(ProtocolVersion protocolVersion) SECTION("empty set") { - auto [_, ApplicableTxSetFrame] = + auto [_, applicableTxSetFrame] = testtxset::makeNonValidatedGeneralizedTxSet( {{}, {}}, *app, app->getLedgerManager().getLastClosedLedgerHeader().hash); GeneralizedTransactionSet txSetXdr; - ApplicableTxSetFrame->toWireTxSetFrame()->toXDR(txSetXdr); + applicableTxSetFrame->toWireTxSetFrame()->toXDR(txSetXdr); REQUIRE(txSetXdr.v1TxSet().phases[0].v0Components().empty()); checkXdrRoundtrip(txSetXdr); } SECTION("one discounted component set") { - auto [_, ApplicableTxSetFrame] = + auto [_, applicableTxSetFrame] = testtxset::makeNonValidatedGeneralizedTxSet( {{std::make_pair(1234LL, createTxs(5, 1234))}, {}}, *app, app->getLedgerManager().getLastClosedLedgerHeader().hash); GeneralizedTransactionSet txSetXdr; - ApplicableTxSetFrame->toWireTxSetFrame()->toXDR(txSetXdr); + applicableTxSetFrame->toWireTxSetFrame()->toXDR(txSetXdr); REQUIRE(txSetXdr.v1TxSet().phases[0].v0Components().size() == 1); REQUIRE(*txSetXdr.v1TxSet() .phases[0] @@ -570,13 +545,13 @@ testGeneralizedTxSetXDRConversion(ProtocolVersion protocolVersion) } SECTION("one non-discounted component set") { - auto [_, ApplicableTxSetFrame] = + auto [_, applicableTxSetFrame] = testtxset::makeNonValidatedGeneralizedTxSet( {{std::make_pair(std::nullopt, createTxs(5, 4321))}, {}}, *app, app->getLedgerManager().getLastClosedLedgerHeader().hash); GeneralizedTransactionSet txSetXdr; - ApplicableTxSetFrame->toWireTxSetFrame()->toXDR(txSetXdr); + applicableTxSetFrame->toWireTxSetFrame()->toXDR(txSetXdr); REQUIRE(txSetXdr.v1TxSet().phases[0].v0Components().size() == 1); REQUIRE(!txSetXdr.v1TxSet() .phases[0] @@ -592,7 +567,7 @@ testGeneralizedTxSetXDRConversion(ProtocolVersion protocolVersion) } SECTION("multiple component sets") { - auto [_, ApplicableTxSetFrame] = + auto [_, applicableTxSetFrame] = testtxset::makeNonValidatedGeneralizedTxSet( {{std::make_pair(12345LL, createTxs(3, 12345)), std::make_pair(123LL, createTxs(1, 123)), @@ -602,7 +577,7 @@ testGeneralizedTxSetXDRConversion(ProtocolVersion protocolVersion) *app, app->getLedgerManager().getLastClosedLedgerHeader().hash); GeneralizedTransactionSet txSetXdr; - ApplicableTxSetFrame->toWireTxSetFrame()->toXDR(txSetXdr); + applicableTxSetFrame->toWireTxSetFrame()->toXDR(txSetXdr); auto const& comps = txSetXdr.v1TxSet().phases[0].v0Components(); REQUIRE(comps.size() == 4); REQUIRE(!comps[0].txsMaybeDiscountedFee().baseFee); @@ -615,6 +590,126 @@ testGeneralizedTxSetXDRConversion(ProtocolVersion protocolVersion) REQUIRE(comps[3].txsMaybeDiscountedFee().txs.size() == 3); checkXdrRoundtrip(txSetXdr); } +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + if (protocolVersionStartsFrom(static_cast(protocolVersion), + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) + { + SECTION("parallel Soroban phase") + { + modifySorobanNetworkConfig( + *app, [](SorobanNetworkConfig& sorobanCfg) { + sorobanCfg.mLedgerMaxTxCount = 100; + sorobanCfg.mLedgerMaxDependentTxClusters = 5; + }); + testtxset::PhaseComponents classicPhase = { + {std::nullopt, createTxs(3, 1000, false)}, + {500, createTxs(3, 1200, false)}, + {1500, createTxs(3, 1500, false)}}; + SECTION("single stage, single cluster") + { + auto [_, applicableTxSetFrame] = + testtxset::makeNonValidatedGeneralizedTxSet( + classicPhase, 1234, {{createTxs(10, 1234, true)}}, *app, + app->getLedgerManager() + .getLastClosedLedgerHeader() + .hash); + + GeneralizedTransactionSet txSetXdr; + applicableTxSetFrame->toWireTxSetFrame()->toXDR(txSetXdr); + // Smoke test classic phase + REQUIRE(txSetXdr.v1TxSet().phases[0].v() == 0); + REQUIRE(txSetXdr.v1TxSet().phases[0].v0Components().size() == + 3); + REQUIRE(*txSetXdr.v1TxSet() + .phases[0] + .v0Components()[1] + .txsMaybeDiscountedFee() + .baseFee == 500); + + REQUIRE(txSetXdr.v1TxSet().phases[1].v() == 1); + REQUIRE(*txSetXdr.v1TxSet() + .phases[1] + .parallelTxsComponent() + .baseFee == 1234); + REQUIRE(txSetXdr.v1TxSet() + .phases[1] + .parallelTxsComponent() + .executionStages.size() == 1); + REQUIRE(txSetXdr.v1TxSet() + .phases[1] + .parallelTxsComponent() + .executionStages[0] + .size() == 1); + REQUIRE(txSetXdr.v1TxSet() + .phases[1] + .parallelTxsComponent() + .executionStages[0][0] + .size() == 10); + checkXdrRoundtrip(txSetXdr); + } + + SECTION("single stage, multiple clusters") + { + auto [_, applicableTxSetFrame] = + testtxset::makeNonValidatedGeneralizedTxSet( + {}, 1234, + {{createTxs(10, 1234, true), createTxs(5, 2000, true), + createTxs(3, 1500, true)}}, + *app, + app->getLedgerManager() + .getLastClosedLedgerHeader() + .hash); + + GeneralizedTransactionSet txSetXdr; + applicableTxSetFrame->toWireTxSetFrame()->toXDR(txSetXdr); + REQUIRE(txSetXdr.v1TxSet().phases[1].v() == 1); + REQUIRE(*txSetXdr.v1TxSet() + .phases[1] + .parallelTxsComponent() + .baseFee == 1234); + REQUIRE(txSetXdr.v1TxSet() + .phases[1] + .parallelTxsComponent() + .executionStages.size() == 1); + REQUIRE(txSetXdr.v1TxSet() + .phases[1] + .parallelTxsComponent() + .executionStages[0] + .size() == 3); + checkXdrRoundtrip(txSetXdr); + } + + SECTION("multiple stages, multiple clusters") + { + auto [_, applicableTxSetFrame] = + testtxset::makeNonValidatedGeneralizedTxSet( + classicPhase, std::nullopt, + {{createTxs(1, 1234, true), createTxs(3, 2000, true), + createTxs(5, 1500, true), createTxs(2, 1000, true), + createTxs(7, 3000, true)}, + {createTxs(3, 1234, true), createTxs(5, 1300, true)}, + {createTxs(2, 4321, true)}}, + *app, + app->getLedgerManager() + .getLastClosedLedgerHeader() + .hash); + + GeneralizedTransactionSet txSetXdr; + applicableTxSetFrame->toWireTxSetFrame()->toXDR(txSetXdr); + REQUIRE(txSetXdr.v1TxSet().phases[1].v() == 1); + REQUIRE(!txSetXdr.v1TxSet() + .phases[1] + .parallelTxsComponent() + .baseFee); + REQUIRE(txSetXdr.v1TxSet() + .phases[1] + .parallelTxsComponent() + .executionStages.size() == 3); + checkXdrRoundtrip(txSetXdr); + } + } + } +#endif SECTION("built from transactions") { auto const& lclHeader = @@ -786,7 +881,8 @@ SECTION("parallel soroban protocol version") #endif } -TEST_CASE("soroban phase version validation", "[txset][soroban]") +TEST_CASE("applicable txset validation - Soroban phase version is correct", + "[txset][soroban]") { auto runTest = [](uint32_t protocolVersion, bool useParallelSorobanPhase) -> bool { @@ -840,6 +936,449 @@ TEST_CASE("soroban phase version validation", "[txset][soroban]") #endif } +TEST_CASE("applicable txset validation - transactions belong to correct phase", + "[txset][soroban]") +{ + auto runTest = [](uint32_t protocolVersion) { + VirtualClock clock; + auto cfg = getTestConfig(); + cfg.LEDGER_PROTOCOL_VERSION = protocolVersion; + cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION = protocolVersion; + auto app = createTestApplication(clock, cfg); + overrideSorobanNetworkConfigForTest(*app); + auto root = TestAccount::createRoot(*app); + int accountId = 0; + auto createTx = [&](bool isSoroban) { + auto source = + root.create("source" + std::to_string(accountId++), + app->getLedgerManager().getLastMinBalance(2)); + TransactionFrameBaseConstPtr tx = nullptr; + if (isSoroban) + { + SorobanResources resources; + resources.instructions = 800'000; + resources.readBytes = 1000; + resources.writeBytes = 1000; + tx = createUploadWasmTx(*app, source, 1000, 100'000'000, + resources); + } + else + { + tx = transactionFromOperations( + *app, source.getSecretKey(), source.nextSequenceNumber(), + {createAccount( + getAccount(std::to_string(accountId++)).getPublicKey(), + 1)}, + 2000); + } + LedgerSnapshot ls(*app); + REQUIRE(tx->checkValid(app->getAppConnector(), ls, 0, 0, 0) + ->isSuccess()); + return tx; + }; + + auto validateTxSet = + [&](std::vector> + phaseTxs) { + std::vector phases(2); + if (!phaseTxs[0].empty()) + { + phases[0].emplace_back(100, phaseTxs[0]); + } + if (!phaseTxs[1].empty()) + { + phases[1].emplace_back(100, phaseTxs[1]); + } + auto txSet = testtxset::makeNonValidatedGeneralizedTxSet( + phases, *app, + app->getLedgerManager() + .getLastClosedLedgerHeader() + .hash) + .second; + REQUIRE(txSet); + return txSet->checkValid(*app, 0, 0); + }; + SECTION("empty phases") + { + REQUIRE(validateTxSet({{}, {}})); + } + SECTION("non-empty correct phases") + { + REQUIRE(validateTxSet( + {{createTx(false), createTx(false), createTx(false)}, + {createTx(true), createTx(true)}})); + } + SECTION("classic tx in Soroban phase") + { + REQUIRE(!validateTxSet({{}, {createTx(false)}})); + REQUIRE(!validateTxSet( + {{createTx(false), createTx(false), createTx(false)}, + {createTx(true), createTx(false), createTx(true)}})); + } + SECTION("Soroban tx in classic phase") + { + REQUIRE(!validateTxSet({{createTx(true)}, {}})); + REQUIRE(!validateTxSet( + {{createTx(false), createTx(true), createTx(false)}, + {createTx(true), createTx(true), createTx(true)}})); + } + SECTION("both phases mixed") + { + REQUIRE(!validateTxSet({{createTx(true)}, {createTx(false)}})); + REQUIRE(!validateTxSet( + {{createTx(false), createTx(true), createTx(false)}, + {createTx(true), createTx(true), createTx(false)}})); + } + }; + SECTION("previous protocol") + { + runTest(Config::CURRENT_LEDGER_PROTOCOL_VERSION - 1); + } + SECTION("current protocol") + { + runTest(Config::CURRENT_LEDGER_PROTOCOL_VERSION); + } +} + +TEST_CASE("applicable txset validation - Soroban resources", "[txset][soroban]") +{ + auto runTest = [](uint32_t protocolVersion) { + VirtualClock clock; + auto cfg = getTestConfig(); + cfg.LEDGER_PROTOCOL_VERSION = protocolVersion; + cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION = protocolVersion; + + auto app = createTestApplication(clock, cfg); + overrideSorobanNetworkConfigForTest(*app); + auto root = TestAccount::createRoot(*app); + + int accountId = 0; + int footprintId = 0; + auto ledgerKey = [&](int id) { + LedgerKey key(LedgerEntryType::CONTRACT_DATA); + key.contractData().key.type(SCValType::SCV_I32); + key.contractData().key.i32() = id; + return key; + }; + + auto createTx = [&](std::vector addRoFootprint = {}, + std::vector addRwFootprint = {}) { + auto source = root.create("source" + std::to_string(accountId++), + 1'000'000'000); + Operation op; + op.body.type(INVOKE_HOST_FUNCTION); + op.body.invokeHostFunctionOp().hostFunction.type( + HOST_FUNCTION_TYPE_UPLOAD_CONTRACT_WASM); + SorobanResources resources; + resources.instructions = 1'000'000; + resources.readBytes = 5'000; + resources.writeBytes = 2'000; + for (int i = 0; i < 8; ++i) + { + resources.footprint.readOnly.push_back( + ledgerKey(footprintId++)); + } + for (int i = 0; i < 2; ++i) + { + resources.footprint.readWrite.push_back( + ledgerKey(footprintId++)); + } + for (auto id : addRoFootprint) + { + resources.footprint.readOnly.push_back( + ledgerKey(1'000'000'000 + id)); + } + for (auto id : addRwFootprint) + { + resources.footprint.readWrite.push_back( + ledgerKey(1'000'000'000 + id)); + } + + auto tx = sorobanTransactionFrameFromOps( + app->getNetworkID(), source, {op}, {}, resources, 2000, + 100'000'000); + LedgerSnapshot ls(*app); + REQUIRE(tx->checkValid(app->getAppConnector(), ls, 0, 0, 0) + ->isSuccess()); + return tx; + }; + + SECTION("individual ledger resource limits") + { + auto txSize = xdr::xdr_size(createTx()->getEnvelope()); + // Update the ledger limits to the minimum values that + // accommodate 20 txs created by `createTx()`. + modifySorobanNetworkConfig( + *app, [&](SorobanNetworkConfig& sorobanCfg) { + sorobanCfg.mLedgerMaxInstructions = 20 * 1'000'000; + sorobanCfg.mLedgerMaxReadBytes = 20 * 5000; + sorobanCfg.mLedgerMaxWriteBytes = 20 * 2000; + sorobanCfg.mLedgerMaxReadLedgerEntries = 20 * 10; + sorobanCfg.mLedgerMaxWriteLedgerEntries = 20 * 2; + sorobanCfg.mLedgerMaxTxCount = 20; + sorobanCfg.mLedgerMaxTransactionsSizeBytes = 20 * txSize; + + if (protocolVersionStartsFrom( + protocolVersion, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) + { + sorobanCfg.mLedgerMaxDependentTxClusters = 4; + // Technically we could use /= 4 here, but that + // would make for a less interesting scenario as + // every stage will need to have exactly the same + // clusters. + sorobanCfg.mLedgerMaxInstructions /= 2; + } + }); + + auto buildAndValidate = [&]() { + std::vector txs; + for (int i = 0; i < 20; ++i) + { + txs.push_back(createTx()); + } + ApplicableTxSetFrameConstPtr txSet; + if (protocolVersionIsBefore( + protocolVersion, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) + { + txSet = testtxset::makeNonValidatedGeneralizedTxSet( + {{}, {{1000, txs}}}, *app, + app->getLedgerManager() + .getLastClosedLedgerHeader() + .hash) + .second; + } + else + { +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + auto takeTxs = [&](int from, int to) { + return std::vector( + txs.begin() + from, txs.begin() + to); + }; + TxStageFrameList txsPerStage = { + // 3 sequential transactions + { + takeTxs(0, 1), + takeTxs(1, 3), + takeTxs(3, 6), + takeTxs(6, 8), + }, + // 4 sequential transactions + { + takeTxs(8, 12), + takeTxs(12, 13), + takeTxs(13, 15), + }, + // 3 sequential transactions + { + takeTxs(15, 17), + takeTxs(17, 20), + }, + // 10 sequential transactions total, accounting for + // a + // half of ledger max instructions. + }; + txSet = testtxset::makeNonValidatedGeneralizedTxSet( + {}, 1234, txsPerStage, *app, + app->getLedgerManager() + .getLastClosedLedgerHeader() + .hash) + .second; +#endif + } + return txSet->checkValid(*app, 0, 0); + }; + SECTION("sufficient resources") + { + REQUIRE(buildAndValidate()); + } + SECTION("instruction limit exceeded") + { + modifySorobanNetworkConfig( + *app, [&](SorobanNetworkConfig& sorobanCfg) { + sorobanCfg.mLedgerMaxInstructions -= 1; + }); + REQUIRE(!buildAndValidate()); + } + SECTION("read bytes limit exceeded") + { + modifySorobanNetworkConfig( + *app, [&](SorobanNetworkConfig& sorobanCfg) { + sorobanCfg.mLedgerMaxReadBytes -= 1; + }); + REQUIRE(!buildAndValidate()); + } + SECTION("write bytes limit exceeded") + { + modifySorobanNetworkConfig( + *app, [&](SorobanNetworkConfig& sorobanCfg) { + sorobanCfg.mLedgerMaxWriteBytes -= 1; + }); + REQUIRE(!buildAndValidate()); + } + SECTION("read entries limit exceeded") + { + modifySorobanNetworkConfig( + *app, [&](SorobanNetworkConfig& sorobanCfg) { + sorobanCfg.mLedgerMaxReadLedgerEntries -= 1; + }); + REQUIRE(!buildAndValidate()); + } + SECTION("write entries limit exceeded") + { + modifySorobanNetworkConfig( + *app, [&](SorobanNetworkConfig& sorobanCfg) { + sorobanCfg.mLedgerMaxWriteLedgerEntries -= 1; + }); + REQUIRE(!buildAndValidate()); + } + SECTION("tx size limit exceeded") + { + modifySorobanNetworkConfig( + *app, [&](SorobanNetworkConfig& sorobanCfg) { + sorobanCfg.mLedgerMaxTransactionsSizeBytes -= 1; + }); + REQUIRE(!buildAndValidate()); + } + SECTION("tx count limit exceeded") + { + modifySorobanNetworkConfig( + *app, [&](SorobanNetworkConfig& sorobanCfg) { + sorobanCfg.mLedgerMaxTxCount -= 1; + }); + REQUIRE(!buildAndValidate()); + } +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + if (protocolVersionStartsFrom( + protocolVersion, PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) + { + SECTION("dependent clusters limit exceeded") + { + modifySorobanNetworkConfig( + *app, [&](SorobanNetworkConfig& sorobanCfg) { + sorobanCfg.mLedgerMaxDependentTxClusters -= 1; + }); + REQUIRE(!buildAndValidate()); + } + } +#endif + } +#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION + if (protocolVersionStartsFrom(protocolVersion, + PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION)) + { + SECTION("data dependency validation") + { + + auto buildAndValidate = [&](TxStageFrameList txsPerStage) { + auto txSet = testtxset::makeNonValidatedGeneralizedTxSet( + {}, 1234, txsPerStage, *app, + app->getLedgerManager() + .getLastClosedLedgerHeader() + .hash) + .second; + return txSet->checkValid(*app, 0, 0); + }; + + // Relax the per-ledger limits to ensure we're not running + // into any. + modifySorobanNetworkConfig( + *app, [&](SorobanNetworkConfig& sorobanCfg) { + sorobanCfg.mLedgerMaxInstructions = + std::numeric_limits::max(); + sorobanCfg.mLedgerMaxReadBytes = + std::numeric_limits::max(); + sorobanCfg.mLedgerMaxWriteBytes = + std::numeric_limits::max(); + sorobanCfg.mLedgerMaxReadLedgerEntries = + std::numeric_limits::max(); + sorobanCfg.mLedgerMaxWriteLedgerEntries = + std::numeric_limits::max(); + sorobanCfg.mLedgerMaxTxCount = + std::numeric_limits::max(); + sorobanCfg.mLedgerMaxTransactionsSizeBytes = + std::numeric_limits::max(); + sorobanCfg.mLedgerMaxDependentTxClusters = + std::numeric_limits::max(); + }); + TxStageFrameList nonConflictingTxsPerStage = { + { + // Cluster with RO-RW conflict + {createTx({1}, {2}), createTx({2}, {3})}, + // Cluster with RW-RW conflict + {createTx({1, 4}, {5}), createTx({1, 6}, {5})}, + // Cluster without conflicts + {createTx({1, 4, 7}, {8}), createTx({6, 7}, {9})}, + }, + { + // Cluster that would conflict with every + // cluster in previous stage + {createTx({}, {1, 2, 3, 4, 5, 6, 7, 8}), + createTx({1, 2, 3}, {4, 5, 6}), + createTx({1, 2}, {3, 4})}, + // Cluster without conflicts + {createTx({9}, {10}), createTx({9}, {11}), + createTx({9}, {12, 13})}, + }}; + SECTION("no dependencies between clusters") + { + REQUIRE(buildAndValidate(nonConflictingTxsPerStage)); + } + SECTION("RO-RW conflict") + { + TxStageFrameList txsPerStage = {{ + {createTx({1}, {})}, + {createTx({}, {1})}, + }}; + REQUIRE(!buildAndValidate(txsPerStage)); + } + SECTION("RW-RW conflict") + { + TxStageFrameList txsPerStage = {{ + {createTx({}, {1})}, + {createTx({}, {1})}, + }}; + REQUIRE(!buildAndValidate(txsPerStage)); + } + SECTION("one stage with a conflict") + { + auto txsPerStage = nonConflictingTxsPerStage; + txsPerStage.push_back({ + {createTx({1}, {})}, + {createTx({}, {1})}, + }); + REQUIRE(!buildAndValidate(txsPerStage)); + } + SECTION("one cluster conflict among multiple clusters") + { + auto txsPerStage = nonConflictingTxsPerStage; + txsPerStage[0][2].push_back(createTx({}, {5})); + REQUIRE(!buildAndValidate(txsPerStage)); + } + SECTION("multiple conflicting clusters") + { + auto txsPerStage = nonConflictingTxsPerStage; + txsPerStage[0][2].push_back(createTx({9, 10}, {12, 13, 4})); + txsPerStage[1].emplace_back().push_back( + createTx({9, 10}, {12, 13, 8})); + REQUIRE(!buildAndValidate(txsPerStage)); + } + } + } +#endif + }; + + SECTION("previous protocol") + { + runTest(Config::CURRENT_LEDGER_PROTOCOL_VERSION - 1); + } + SECTION("current protocol") + { + runTest(Config::CURRENT_LEDGER_PROTOCOL_VERSION); + } +} + TEST_CASE("generalized tx set with multiple txs per source account", "[txset][soroban]") { @@ -989,7 +1528,7 @@ TEST_CASE("generalized tx set fees", "[txset][soroban]") SECTION("valid txset") { - testtxset::ComponentPhases sorobanTxs; + testtxset::PhaseComponents sorobanTxs; bool isParallelSoroban = protocolVersionStartsFrom(Config::CURRENT_LEDGER_PROTOCOL_VERSION, PARALLEL_SOROBAN_PHASE_PROTOCOL_VERSION); diff --git a/src/herder/test/UpgradesTests.cpp b/src/herder/test/UpgradesTests.cpp index c8f17f20b8..5222236fb6 100644 --- a/src/herder/test/UpgradesTests.cpp +++ b/src/herder/test/UpgradesTests.cpp @@ -2373,8 +2373,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( @@ -2382,9 +2382,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() == @@ -2409,9 +2409,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/LedgerManagerImpl.cpp b/src/ledger/LedgerManagerImpl.cpp index affd40e066..ece4d06d86 100644 --- a/src/ledger/LedgerManagerImpl.cpp +++ b/src/ledger/LedgerManagerImpl.cpp @@ -438,15 +438,7 @@ LedgerManagerImpl::maxLedgerResources(bool isSoroban) if (isSoroban) { - auto conf = getSorobanNetworkConfigReadOnly(); - std::vector limits = {conf.ledgerMaxTxCount(), - conf.ledgerMaxInstructions(), - conf.ledgerMaxTransactionSizesBytes(), - conf.ledgerMaxReadBytes(), - conf.ledgerMaxWriteBytes(), - conf.ledgerMaxReadLedgerEntries(), - conf.ledgerMaxWriteLedgerEntries()}; - return Resource(limits); + return getSorobanNetworkConfigReadOnly().maxLedgerResources(); } else { diff --git a/src/ledger/NetworkConfig.cpp b/src/ledger/NetworkConfig.cpp index 6702f76c7e..6bf7640b85 100644 --- a/src/ledger/NetworkConfig.cpp +++ b/src/ledger/NetworkConfig.cpp @@ -1993,6 +1993,19 @@ SorobanNetworkConfig::ledgerMaxDependentTxClusters() const return mLedgerMaxDependentTxClusters; } +Resource +SorobanNetworkConfig::maxLedgerResources() const +{ + std::vector limits = {ledgerMaxTxCount(), + ledgerMaxInstructions(), + ledgerMaxTransactionSizesBytes(), + ledgerMaxReadBytes(), + ledgerMaxWriteBytes(), + ledgerMaxReadLedgerEntries(), + ledgerMaxWriteLedgerEntries()}; + return Resource(limits); +} + #ifdef BUILD_TESTS StateArchivalSettings& SorobanNetworkConfig::stateArchivalSettings() diff --git a/src/ledger/NetworkConfig.h b/src/ledger/NetworkConfig.h index b3140b6523..19d85a1cfd 100644 --- a/src/ledger/NetworkConfig.h +++ b/src/ledger/NetworkConfig.h @@ -7,6 +7,7 @@ #include "ledger/LedgerTxn.h" #include "main/Config.h" #include "rust/RustBridge.h" +#include "util/TxResource.h" #include #include @@ -341,6 +342,8 @@ class SorobanNetworkConfig // Parallel execution settings uint32_t ledgerMaxDependentTxClusters() const; + Resource maxLedgerResources() const; + #ifdef BUILD_TESTS StateArchivalSettings& stateArchivalSettings(); EvictionIterator& evictionIterator(); 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; }