diff --git a/src/herder/TxSetFrame.cpp b/src/herder/TxSetFrame.cpp index 9e69d55a5f..989e34406f 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,29 +1225,30 @@ 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& TxSetPhaseFrame::Iterator::operator++() { - if (mStageIndex >= mStages.size()) + if (mStageIndex >= mStages.size() || + mClusterIndex >= mStages[mStageIndex].size()) { 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 +1267,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 +1280,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 +1321,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 +1340,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 +1354,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 +1388,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 +1421,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 +1449,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 +1471,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 +1563,300 @@ 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(); + + // With parallel Soroban phase the instruction limit validation is more + // complex than just comparing the total instructions to the ledger-wide + // limit. Thus, we skip the instruction check for the parallel phase and + // do the proper check further below. + 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 +1956,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 +1985,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 +2020,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 +2028,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 +2038,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 +2046,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 +2056,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 +2067,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..376cb3a0ea 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[0].begin(), + scenarios[0].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; }