diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index 0693308ef0..01e04a3f5a 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -10,7 +10,7 @@ concurrency: cancel-in-progress: true jobs: - job: + documentation: runs-on: ubuntu-latest permissions: contents: write diff --git a/BUILD.md b/BUILD.md index fd985dce81..1ba767cd88 100644 --- a/BUILD.md +++ b/BUILD.md @@ -288,7 +288,7 @@ It fixes some source files to add missing `#include`s. Single-config generators: ``` - cmake --build . + cmake --build . -j $(nproc) ``` Multi-config generators: diff --git a/cmake/RippledDocs.cmake b/cmake/RippledDocs.cmake index d93bc119c0..dda277bffa 100644 --- a/cmake/RippledDocs.cmake +++ b/cmake/RippledDocs.cmake @@ -53,9 +53,9 @@ set(download_script "${CMAKE_BINARY_DIR}/docs/download-cppreference.cmake") file(WRITE "${download_script}" "file(DOWNLOAD \ - http://upload.cppreference.com/mwiki/images/b/b2/html_book_20190607.zip \ + https://github.com/PeterFeicht/cppreference-doc/releases/download/v20250209/html-book-20250209.zip \ ${CMAKE_BINARY_DIR}/docs/cppreference.zip \ - EXPECTED_HASH MD5=82b3a612d7d35a83e3cb1195a63689ab \ + EXPECTED_HASH MD5=bda585f72fbca4b817b29a3d5746567b \ )\n \ execute_process( \ COMMAND \"${CMAKE_COMMAND}\" -E tar -xf cppreference.zip \ diff --git a/docs/build/environment.md b/docs/build/environment.md index 7fe89ffb49..760be144d8 100644 --- a/docs/build/environment.md +++ b/docs/build/environment.md @@ -23,7 +23,7 @@ direction. ``` apt update -apt install --yes curl git libssl-dev python3.10-dev python3-pip make g++-11 libprotobuf-dev protobuf-compiler +apt install --yes curl git libssl-dev pipx python3.10-dev python3-pip make g++-11 libprotobuf-dev protobuf-compiler curl --location --remote-name \ "https://github.com/Kitware/CMake/releases/download/v3.25.1/cmake-3.25.1.tar.gz" @@ -35,7 +35,8 @@ make --jobs $(nproc) make install cd .. -pip3 install 'conan<2' +pipx install 'conan<2' +pipx ensurepath ``` [1]: https://github.com/thejohnfreeman/rippled-docker/blob/master/ubuntu-22.04/install.sh diff --git a/include/xrpl/protocol/Batch.h b/include/xrpl/protocol/Batch.h new file mode 100644 index 0000000000..1388bbd2f1 --- /dev/null +++ b/include/xrpl/protocol/Batch.h @@ -0,0 +1,37 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace ripple { + +inline void +serializeBatch( + Serializer& msg, + std::uint32_t const& flags, + std::vector const& txids) +{ + msg.add32(HashPrefix::batch); + msg.add32(flags); + msg.add32(std::uint32_t(txids.size())); + for (auto const& txid : txids) + msg.addBitString(txid); +} + +} // namespace ripple \ No newline at end of file diff --git a/include/xrpl/protocol/HashPrefix.h b/include/xrpl/protocol/HashPrefix.h index ab825658e8..7e486af4c0 100644 --- a/include/xrpl/protocol/HashPrefix.h +++ b/include/xrpl/protocol/HashPrefix.h @@ -88,6 +88,9 @@ enum class HashPrefix : std::uint32_t { /** Credentials signature */ credential = detail::make_hash_prefix('C', 'R', 'D'), + + /** Batch */ + batch = detail::make_hash_prefix('B', 'C', 'H'), }; template diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 49bad8a076..898fd06fbd 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -169,6 +169,9 @@ std::size_t constexpr maxTrim = 25; */ std::size_t constexpr permissionMaxSize = 10; +/** The maximum number of transactions that can be in a batch. */ +std::size_t constexpr maxBatchTxCount = 8; + } // namespace ripple #endif diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index b00495bf76..f0d2157283 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -125,10 +125,16 @@ public: @return `true` if valid signature. If invalid, the error message string. */ enum class RequireFullyCanonicalSig : bool { no, yes }; + Expected checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; + Expected + checkBatchSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const; + // SQL Functions with metadata. static std::string const& getMetaSQLInsertReplaceHeader(); @@ -144,6 +150,9 @@ public: char status, std::string const& escapedMetaData) const; + std::vector + getBatchTransactionIDs() const; + private: Expected checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const; @@ -153,12 +162,24 @@ private: RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; + Expected + checkBatchSingleSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig) const; + + Expected + checkBatchMultiSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const; + STBase* copy(std::size_t n, void* buf) const override; STBase* move(std::size_t n, void* buf) override; friend class detail::STVar; + mutable std::vector batch_txn_ids_; }; bool diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index b87bc3f8a4..4483d6251a 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -139,8 +139,8 @@ enum TEMcodes : TERUnderlyingType { temARRAY_EMPTY, temARRAY_TOO_LARGE, - temBAD_TRANSFER_FEE, + temINVALID_INNER_BATCH, }; //------------------------------------------------------------------------------ @@ -645,37 +645,37 @@ using TER = TERSubset; //------------------------------------------------------------------------------ inline bool -isTelLocal(TER x) +isTelLocal(TER x) noexcept { - return ((x) >= telLOCAL_ERROR && (x) < temMALFORMED); + return (x >= telLOCAL_ERROR && x < temMALFORMED); } inline bool -isTemMalformed(TER x) +isTemMalformed(TER x) noexcept { - return ((x) >= temMALFORMED && (x) < tefFAILURE); + return (x >= temMALFORMED && x < tefFAILURE); } inline bool -isTefFailure(TER x) +isTefFailure(TER x) noexcept { - return ((x) >= tefFAILURE && (x) < terRETRY); + return (x >= tefFAILURE && x < terRETRY); } inline bool -isTerRetry(TER x) +isTerRetry(TER x) noexcept { - return ((x) >= terRETRY && (x) < tesSUCCESS); + return (x >= terRETRY && x < tesSUCCESS); } inline bool -isTesSuccess(TER x) +isTesSuccess(TER x) noexcept { - return ((x) == tesSUCCESS); + return (x == tesSUCCESS); } inline bool -isTecClaim(TER x) +isTecClaim(TER x) noexcept { return ((x) >= tecCLAIM); } diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 505000cfd6..31c3ffa205 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -58,7 +58,8 @@ namespace ripple { // clang-format off // Universal Transaction flags: constexpr std::uint32_t tfFullyCanonicalSig = 0x80000000; -constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig; +constexpr std::uint32_t tfInnerBatchTxn = 0x40000000; +constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig | tfInnerBatchTxn; constexpr std::uint32_t tfUniversalMask = ~tfUniversal; // AccountSet flags: @@ -97,6 +98,7 @@ constexpr std::uint32_t tfPassive = 0x00010000; constexpr std::uint32_t tfImmediateOrCancel = 0x00020000; constexpr std::uint32_t tfFillOrKill = 0x00040000; constexpr std::uint32_t tfSell = 0x00080000; + constexpr std::uint32_t tfOfferCreateMask = ~(tfUniversal | tfPassive | tfImmediateOrCancel | tfFillOrKill | tfSell); @@ -239,6 +241,20 @@ constexpr std::uint32_t const tfVaultPrivate = 0x00010000; static_assert(tfVaultPrivate == lsfVaultPrivate); constexpr std::uint32_t const tfVaultShareNonTransferable = 0x00020000; constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate | tfVaultShareNonTransferable); + +// Batch Flags: +constexpr std::uint32_t tfAllOrNothing = 0x00010000; +constexpr std::uint32_t tfOnlyOne = 0x00020000; +constexpr std::uint32_t tfUntilFailure = 0x00040000; +constexpr std::uint32_t tfIndependent = 0x00080000; +/** + * @note If nested Batch transactions are supported in the future, the tfInnerBatchTxn flag + * will need to be removed from this mask to allow Batch transaction to be inside + * the sfRawTransactions array. + */ +constexpr std::uint32_t const tfBatchMask = + ~(tfUniversal | tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent) | tfInnerBatchTxn; + // clang-format on } // namespace ripple diff --git a/include/xrpl/protocol/TxMeta.h b/include/xrpl/protocol/TxMeta.h index 9422d697ca..02fde2ffe5 100644 --- a/include/xrpl/protocol/TxMeta.h +++ b/include/xrpl/protocol/TxMeta.h @@ -46,7 +46,10 @@ private: CtorHelper); public: - TxMeta(uint256 const& transactionID, std::uint32_t ledger); + TxMeta( + uint256 const& transactionID, + std::uint32_t ledger, + std::optional parentBatchId = std::nullopt); TxMeta(uint256 const& txID, std::uint32_t ledger, Blob const&); TxMeta(uint256 const& txID, std::uint32_t ledger, std::string const&); TxMeta(uint256 const& txID, std::uint32_t ledger, STObject const&); @@ -130,6 +133,27 @@ public: return static_cast(mDelivered); } + void + setParentBatchId(uint256 const& parentBatchId) + { + mParentBatchId = parentBatchId; + } + + uint256 + getParentBatchId() const + { + XRPL_ASSERT( + hasParentBatchId(), + "ripple::TxMeta::getParentBatchId : non-null batch id"); + return *mParentBatchId; + } + + bool + hasParentBatchId() const + { + return static_cast(mParentBatchId); + } + private: uint256 mTransactionID; std::uint32_t mLedger; @@ -137,6 +161,7 @@ private: int mResult; std::optional mDelivered; + std::optional mParentBatchId; STArray mNodes; }; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index b16e22f7a3..9c567318d7 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -32,6 +32,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(SingleAssetVault, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 63bc52de6a..dbef597ea0 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -195,6 +195,7 @@ TYPED_SFIELD(sfHookNamespace, UINT256, 32) TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) TYPED_SFIELD(sfVaultID, UINT256, 35) +TYPED_SFIELD(sfParentBatchID, UINT256, 36) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -357,6 +358,8 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30) UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31) UNTYPED_SFIELD(sfPriceData, OBJECT, 32) UNTYPED_SFIELD(sfCredential, OBJECT, 33) +UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) +UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -388,3 +391,5 @@ UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26) UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27) UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) UNTYPED_SFIELD(sfPermissions, ARRAY, 29) +UNTYPED_SFIELD(sfRawTransactions, ARRAY, 30) +UNTYPED_SFIELD(sfBatchSigners, ARRAY, 31, SField::sMD_Default, SField::notSigning) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 0f614df692..5d5faae505 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -514,6 +514,12 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::delegatable, ({ {sfAmount, soeOPTIONAL, soeMPTSupported}, })) +/** This transaction type batches together transactions. */ +TRANSACTION(ttBATCH, 71, Batch, Delegation::notDelegatable, ({ + {sfRawTransactions, soeREQUIRED}, + {sfBatchSigners, soeOPTIONAL}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html @@ -548,4 +554,3 @@ TRANSACTION(ttUNL_MODIFY, 102, UNLModify, Delegation::notDelegatable, ({ {sfLedgerSequence, soeREQUIRED}, {sfUNLModifyValidator, soeREQUIRED}, })) - diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index de3560d7f9..9dff4cc4f3 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -83,6 +83,8 @@ JSS(PriceDataSeries); // field. JSS(PriceData); // field. JSS(Provider); // field. JSS(QuoteAsset); // in: Oracle. +JSS(RawTransaction); // in: Batch +JSS(RawTransactions); // in: Batch JSS(SLE_hit_rate); // out: GetCounts. JSS(Scale); // field. JSS(SettleDelay); // in: TransactionSign diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index ecfca9743d..3f3b1e00c0 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -158,6 +158,13 @@ InnerObjectFormats::InnerObjectFormats() add(sfPermission.jsonName.c_str(), sfPermission.getCode(), {{sfPermissionValue, soeREQUIRED}}); + + add(sfBatchSigner.jsonName.c_str(), + sfBatchSigner.getCode(), + {{sfAccount, soeREQUIRED}, + {sfSigningPubKey, soeOPTIONAL}, + {sfTxnSignature, soeOPTIONAL}, + {sfSigners, soeOPTIONAL}}); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 7b6b4c1ee2..ee26dd69de 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -262,6 +265,42 @@ STTx::checkSign( return Unexpected("Internal signature check failure."); } +Expected +STTx::checkBatchSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + try + { + XRPL_ASSERT( + getTxnType() == ttBATCH, + "STTx::checkBatchSign : not a batch transaction"); + if (getTxnType() != ttBATCH) + { + JLOG(debugLog().fatal()) << "not a batch transaction"; + return Unexpected("Not a batch transaction."); + } + STArray const& signers{getFieldArray(sfBatchSigners)}; + for (auto const& signer : signers) + { + Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey); + auto const result = signingPubKey.empty() + ? checkBatchMultiSign(signer, requireCanonicalSig, rules) + : checkBatchSingleSign(signer, requireCanonicalSig); + + if (!result) + return result; + } + return {}; + } + catch (std::exception const& e) + { + JLOG(debugLog().error()) + << "Batch signature check failed: " << e.what(); + } + return Unexpected("Internal batch signature check failure."); +} + Json::Value STTx::getJson(JsonOptions options) const { @@ -341,79 +380,90 @@ STTx::getMetaSQL( getFieldU32(sfSequence) % inLedger % status % rTxn % escapedMetaData); } -Expected -STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const +static Expected +singleSignHelper( + STObject const& signer, + Slice const& data, + bool const fullyCanonical) { // We don't allow both a non-empty sfSigningPubKey and an sfSigners. // That would allow the transaction to be signed two ways. So if both // fields are present the signature is invalid. - if (isFieldPresent(sfSigners)) + if (signer.isFieldPresent(sfSigners)) return Unexpected("Cannot both single- and multi-sign."); bool validSig = false; try { - bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || - (requireCanonicalSig == RequireFullyCanonicalSig::yes); - - auto const spk = getFieldVL(sfSigningPubKey); - + auto const spk = signer.getFieldVL(sfSigningPubKey); if (publicKeyType(makeSlice(spk))) { - Blob const signature = getFieldVL(sfTxnSignature); - Blob const data = getSigningData(*this); - + Blob const signature = signer.getFieldVL(sfTxnSignature); validSig = verify( PublicKey(makeSlice(spk)), - makeSlice(data), + data, makeSlice(signature), fullyCanonical); } } catch (std::exception const&) { - // Assume it was a signature failure. validSig = false; } - if (validSig == false) + + if (!validSig) return Unexpected("Invalid signature."); - // Signature was verified. + return {}; } Expected -STTx::checkMultiSign( - RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules) const +STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const +{ + auto const data = getSigningData(*this); + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes); + return singleSignHelper(*this, makeSlice(data), fullyCanonical); +} + +Expected +STTx::checkBatchSingleSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig) const +{ + Serializer msg; + serializeBatch(msg, getFlags(), getBatchTransactionIDs()); + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes); + return singleSignHelper(batchSigner, msg.slice(), fullyCanonical); +} + +Expected +multiSignHelper( + STObject const& signerObj, + bool const fullyCanonical, + std::function makeMsg, + Rules const& rules) { // Make sure the MultiSigners are present. Otherwise they are not // attempting multi-signing and we just have a bad SigningPubKey. - if (!isFieldPresent(sfSigners)) + if (!signerObj.isFieldPresent(sfSigners)) return Unexpected("Empty SigningPubKey."); // We don't allow both an sfSigners and an sfTxnSignature. Both fields // being present would indicate that the transaction is signed both ways. - if (isFieldPresent(sfTxnSignature)) + if (signerObj.isFieldPresent(sfTxnSignature)) return Unexpected("Cannot both single- and multi-sign."); - STArray const& signers{getFieldArray(sfSigners)}; + STArray const& signers{signerObj.getFieldArray(sfSigners)}; // There are well known bounds that the number of signers must be within. - if (signers.size() < minMultiSigners || - signers.size() > maxMultiSigners(&rules)) + if (signers.size() < STTx::minMultiSigners || + signers.size() > STTx::maxMultiSigners(&rules)) return Unexpected("Invalid Signers array size."); - // We can ease the computational load inside the loop a bit by - // pre-constructing part of the data that we hash. Fill a Serializer - // with the stuff that stays constant from signature to signature. - Serializer const dataStart{startMultiSigningData(*this)}; - // We also use the sfAccount field inside the loop. Get it once. - auto const txnAccountID = getAccountID(sfAccount); - - // Determine whether signatures must be full canonical. - bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || - (requireCanonicalSig == RequireFullyCanonicalSig::yes); + auto const txnAccountID = signerObj.getAccountID(sfAccount); // Signers must be in sorted order by AccountID. AccountID lastAccountID(beast::zero); @@ -441,18 +491,13 @@ STTx::checkMultiSign( bool validSig = false; try { - Serializer s = dataStart; - finishMultiSigningData(accountID, s); - auto spk = signer.getFieldVL(sfSigningPubKey); - if (publicKeyType(makeSlice(spk))) { Blob const signature = signer.getFieldVL(sfTxnSignature); - validSig = verify( PublicKey(makeSlice(spk)), - s.slice(), + makeMsg(accountID).slice(), makeSlice(signature), fullyCanonical); } @@ -471,6 +516,90 @@ STTx::checkMultiSign( return {}; } +Expected +STTx::checkBatchMultiSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == RequireFullyCanonicalSig::yes); + + // We can ease the computational load inside the loop a bit by + // pre-constructing part of the data that we hash. Fill a Serializer + // with the stuff that stays constant from signature to signature. + Serializer dataStart; + serializeBatch(dataStart, getFlags(), getBatchTransactionIDs()); + return multiSignHelper( + batchSigner, + fullyCanonical, + [&dataStart](AccountID const& accountID) mutable -> Serializer { + Serializer s = dataStart; + finishMultiSigningData(accountID, s); + return s; + }, + rules); +} + +Expected +STTx::checkMultiSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == RequireFullyCanonicalSig::yes); + + // We can ease the computational load inside the loop a bit by + // pre-constructing part of the data that we hash. Fill a Serializer + // with the stuff that stays constant from signature to signature. + Serializer dataStart = startMultiSigningData(*this); + return multiSignHelper( + *this, + fullyCanonical, + [&dataStart](AccountID const& accountID) mutable -> Serializer { + Serializer s = dataStart; + finishMultiSigningData(accountID, s); + return s; + }, + rules); +} + +/** + * @brief Retrieves a batch of transaction IDs from the STTx. + * + * This function returns a vector of transaction IDs by extracting them from + * the field array `sfRawTransactions` within the STTx. If the batch + * transaction IDs have already been computed and cached in `batch_txn_ids_`, + * it returns the cached vector. Otherwise, it computes the transaction IDs, + * caches them, and then returns the vector. + * + * @return A vector of `uint256` containing the batch transaction IDs. + * + * @note The function asserts that the `sfRawTransactions` field array is not + * empty and that the size of the computed batch transaction IDs matches the + * size of the `sfRawTransactions` field array. + */ +std::vector +STTx::getBatchTransactionIDs() const +{ + XRPL_ASSERT( + getTxnType() == ttBATCH, + "STTx::getBatchTransactionIDs : not a batch transaction"); + XRPL_ASSERT( + getFieldArray(sfRawTransactions).size() != 0, + "STTx::getBatchTransactionIDs : empty raw transactions"); + if (batch_txn_ids_.size() != 0) + return batch_txn_ids_; + + for (STObject const& rb : getFieldArray(sfRawTransactions)) + batch_txn_ids_.push_back(rb.getHash(HashPrefix::transactionID)); + + XRPL_ASSERT( + batch_txn_ids_.size() == getFieldArray(sfRawTransactions).size(), + "STTx::getBatchTransactionIDs : batch transaction IDs size mismatch"); + return batch_txn_ids_; +} + //------------------------------------------------------------------------------ static bool @@ -606,6 +735,42 @@ invalidMPTAmountInTx(STObject const& tx) return false; } +static bool +isRawTransactionOkay(STObject const& st, std::string& reason) +{ + if (!st.isFieldPresent(sfRawTransactions)) + return true; + + if (st.isFieldPresent(sfBatchSigners) && + st.getFieldArray(sfBatchSigners).size() > maxBatchTxCount) + { + reason = "Batch Signers array exceeds max entries."; + return false; + } + + auto const& rawTxns = st.getFieldArray(sfRawTransactions); + if (rawTxns.size() > maxBatchTxCount) + { + reason = "Raw Transactions array exceeds max entries."; + return false; + } + for (STObject raw : rawTxns) + { + try + { + TxType const tt = + safe_cast(raw.getFieldU16(sfTransactionType)); + raw.applyTemplate(getTxFormat(tt)->getSOTemplate()); + } + catch (std::exception const& e) + { + reason = e.what(); + return false; + } + } + return true; +} + bool passesLocalChecks(STObject const& st, std::string& reason) { @@ -630,6 +795,9 @@ passesLocalChecks(STObject const& st, std::string& reason) return false; } + if (!isRawTransactionOkay(st, reason)) + return false; + return true; } @@ -645,10 +813,13 @@ sterilize(STTx const& stx) bool isPseudoTx(STObject const& tx) { - auto t = tx[~sfTransactionType]; + auto const t = tx[~sfTransactionType]; + if (!t) return false; - auto tt = safe_cast(*t); + + auto const tt = safe_cast(*t); + return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY; } diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 943a0e601b..68125fab83 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -217,6 +217,7 @@ transResults() MAKE_ERROR(temARRAY_EMPTY, "Malformed: Array is empty."), MAKE_ERROR(temARRAY_TOO_LARGE, "Malformed: Array is too large."), MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."), + MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."), MAKE_ERROR(terRETRY, "Retry transaction."), MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."), diff --git a/src/libxrpl/protocol/TxMeta.cpp b/src/libxrpl/protocol/TxMeta.cpp index d9a9f0db87..2083fc8eaf 100644 --- a/src/libxrpl/protocol/TxMeta.cpp +++ b/src/libxrpl/protocol/TxMeta.cpp @@ -56,6 +56,9 @@ TxMeta::TxMeta( if (obj.isFieldPresent(sfDeliveredAmount)) setDeliveredAmount(obj.getFieldAmount(sfDeliveredAmount)); + + if (obj.isFieldPresent(sfParentBatchID)) + setParentBatchId(obj.getFieldH256(sfParentBatchID)); } TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj) @@ -76,6 +79,9 @@ TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj) if (obj.isFieldPresent(sfDeliveredAmount)) setDeliveredAmount(obj.getFieldAmount(sfDeliveredAmount)); + + if (obj.isFieldPresent(sfParentBatchID)) + setParentBatchId(obj.getFieldH256(sfParentBatchID)); } TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, Blob const& vec) @@ -91,11 +97,15 @@ TxMeta::TxMeta( { } -TxMeta::TxMeta(uint256 const& transactionID, std::uint32_t ledger) +TxMeta::TxMeta( + uint256 const& transactionID, + std::uint32_t ledger, + std::optional parentBatchId) : mTransactionID(transactionID) , mLedger(ledger) , mIndex(static_cast(-1)) , mResult(255) + , mParentBatchId(parentBatchId) , mNodes(sfAffectedNodes) { mNodes.reserve(32); @@ -231,6 +241,10 @@ TxMeta::getAsObject() const metaData.emplace_back(mNodes); if (hasDeliveredAmount()) metaData.setFieldAmount(sfDeliveredAmount, getDeliveredAmount()); + + if (hasParentBatchId()) + metaData.setFieldH256(sfParentBatchID, getParentBatchId()); + return metaData; } diff --git a/src/test/app/AMM_test.cpp b/src/test/app/AMM_test.cpp index e0b3dc1ec7..7450aea805 100644 --- a/src/test/app/AMM_test.cpp +++ b/src/test/app/AMM_test.cpp @@ -7020,7 +7020,10 @@ private: // is confusing. Issue usd(USD.issue().currency, amm.ammAccount()); auto amount = amountFromString(usd, "10"); - env(claw(gw, amount), ter(tecAMM_ACCOUNT)); + auto const err = features[featureSingleAssetVault] + ? tecPSEUDO_ACCOUNT + : tecAMM_ACCOUNT; + env(claw(gw, amount), ter(err)); } } diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index 4ae18d9d28..03283e4611 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -28,12 +28,6 @@ namespace test { class AccountDelete_test : public beast::unit_test::suite { private: - std::uint32_t - openLedgerSeq(jtx::Env& env) - { - return env.current()->seq(); - } - // Helper function that verifies the expected DeliveredAmount is present. // // NOTE: the function _infers_ the transaction to operate on by calling @@ -83,26 +77,6 @@ private: return jv; }; - // Close the ledger until the ledger sequence is large enough to close - // the account. If margin is specified, close the ledger so `margin` - // more closes are needed - void - incLgrSeqForAccDel( - jtx::Env& env, - jtx::Account const& acc, - std::uint32_t margin = 0) - { - int const delta = [&]() -> int { - if (env.seq(acc) + 255 > openLedgerSeq(env)) - return env.seq(acc) - openLedgerSeq(env) + 255 - margin; - return 0; - }(); - BEAST_EXPECT(margin == 0 || delta >= 0); - for (int i = 0; i < delta; ++i) - env.close(); - BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255 - margin); - } - public: void testBasics() @@ -368,7 +342,6 @@ public: NetClock::time_point const& cancelAfter) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -398,7 +371,6 @@ public: [](Account const& account, Account const& from, std::uint32_t seq) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCancel; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); jv[sfOwner.jsonName] = from.human(); jv[sfOfferSequence.jsonName] = seq; @@ -536,7 +508,6 @@ public: auto payChanClaim = [&]() { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelClaim; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = alice.human(); jv[sfChannel.jsonName] = to_string(payChanKey.key); jv[sfBalance.jsonName] = diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp new file mode 100644 index 0000000000..6874a42c9e --- /dev/null +++ b/src/test/app/Batch_test.cpp @@ -0,0 +1,3860 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +class Batch_test : public beast::unit_test::suite +{ + struct TestLedgerData + { + int index; + std::string txType; + std::string result; + std::string txHash; + std::optional batchID; + }; + + struct TestBatchData + { + std::string result; + std::string txHash; + }; + + Json::Value + getTxByIndex(Json::Value const& jrr, int const index) + { + for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions]) + { + if (txn[jss::metaData][sfTransactionIndex.jsonName] == index) + return txn; + } + return {}; + } + + Json::Value + getLastLedger(jtx::Env& env) + { + Json::Value params; + params[jss::ledger_index] = env.closed()->seq(); + params[jss::transactions] = true; + params[jss::expand] = true; + return env.rpc("json", "ledger", to_string(params)); + } + + void + validateInnerTxn( + jtx::Env& env, + std::string const& batchID, + TestLedgerData const& ledgerResult) + { + Json::Value const jrr = env.rpc("tx", ledgerResult.txHash)[jss::result]; + BEAST_EXPECT(jrr[sfTransactionType.jsonName] == ledgerResult.txType); + BEAST_EXPECT( + jrr[jss::meta][sfTransactionResult.jsonName] == + ledgerResult.result); + BEAST_EXPECT(jrr[jss::meta][sfParentBatchID.jsonName] == batchID); + } + + void + validateClosedLedger( + jtx::Env& env, + std::vector const& ledgerResults) + { + auto const jrr = getLastLedger(env); + auto const transactions = + jrr[jss::result][jss::ledger][jss::transactions]; + BEAST_EXPECT(transactions.size() == ledgerResults.size()); + for (TestLedgerData const& ledgerResult : ledgerResults) + { + auto const txn = getTxByIndex(jrr, ledgerResult.index); + BEAST_EXPECT(txn[jss::hash].asString() == ledgerResult.txHash); + BEAST_EXPECT(txn.isMember(jss::metaData)); + Json::Value const meta = txn[jss::metaData]; + BEAST_EXPECT( + txn[sfTransactionType.jsonName] == ledgerResult.txType); + BEAST_EXPECT( + meta[sfTransactionResult.jsonName] == ledgerResult.result); + if (ledgerResult.batchID) + validateInnerTxn(env, *ledgerResult.batchID, ledgerResult); + } + } + + template + std::pair, std::string> + submitBatch(jtx::Env& env, TER const& result, Args&&... args) + { + auto batchTxn = env.jt(std::forward(args)...); + env(batchTxn, jtx::ter(result)); + + auto const ids = batchTxn.stx->getBatchTransactionIDs(); + std::vector txIDs; + for (auto const& id : ids) + txIDs.push_back(strHex(id)); + TxID const batchID = batchTxn.stx->getTransactionID(); + return std::make_pair(txIDs, strHex(batchID)); + } + + static uint256 + getCheckIndex(AccountID const& account, std::uint32_t uSequence) + { + return keylet::check(account, uSequence).key; + } + + static std::unique_ptr + makeSmallQueueConfig( + std::map extraTxQ = {}, + std::map extraVoting = {}) + { + auto p = test::jtx::envconfig(); + auto& section = p->section("transaction_queue"); + section.set("ledgers_in_queue", "2"); + section.set("minimum_queue_size", "2"); + section.set("min_ledgers_to_compute_size_limit", "3"); + section.set("max_ledger_counts_to_store", "100"); + section.set("retry_sequence_percent", "25"); + section.set("normal_consensus_increase_percent", "0"); + + for (auto const& [k, v] : extraTxQ) + section.set(k, v); + + return p; + } + + auto + openLedgerFee(jtx::Env& env, XRPAmount const& batchFee) + { + using namespace jtx; + + auto const& view = *env.current(); + auto metrics = env.app().getTxQ().getMetrics(view); + return toDrops(metrics.openLedgerFeeLevel, batchFee) + 1; + } + + void + testEnable(FeatureBitset features) + { + testcase("enabled"); + + using namespace test::jtx; + using namespace std::literals; + + for (bool const withBatch : {true, false}) + { + auto const amend = withBatch ? features : features - featureBatch; + test::jtx::Env env{*this, envconfig(), amend}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + // ttBatch + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const txResult = + withBatch ? ter(tesSUCCESS) : ter(temDISABLED); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + txResult); + env.close(); + } + + // tfInnerBatchTxn + // If the feature is disabled, the transaction fails with + // temINVALID_FLAG If the feature is enabled, the transaction fails + // early in checkValidity() + { + auto const txResult = + withBatch ? ter(telENV_RPC_FAILED) : ter(temINVALID_FLAG); + env(pay(alice, bob, XRP(1)), + txflags(tfInnerBatchTxn), + txResult); + env.close(); + } + + env.close(); + } + } + + void + testPreflight(FeatureBitset features) + { + testcase("preflight"); + + using namespace test::jtx; + using namespace std::literals; + + //---------------------------------------------------------------------- + // preflight + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + // temBAD_FEE: preflight1 + { + env(batch::outer(alice, env.seq(alice), XRP(-1), tfAllOrNothing), + ter(temBAD_FEE)); + env.close(); + } + + // DEFENSIVE: temINVALID_FLAG: Batch: inner batch flag. + // ACTUAL: telENV_RPC_FAILED: checkValidity() + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfInnerBatchTxn), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temINVALID_FLAG: Batch: invalid flags. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfDisallowXRP), + ter(temINVALID_FLAG)); + env.close(); + } + + // temINVALID_FLAG: Batch: too many flags. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + txflags(tfAllOrNothing | tfOnlyOne), + ter(temINVALID_FLAG)); + env.close(); + } + + // temARRAY_EMPTY: Batch: txns array must have at least 2 entries. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + ter(temARRAY_EMPTY)); + env.close(); + } + + // temARRAY_EMPTY: Batch: txns array must have at least 2 entries. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + ter(temARRAY_EMPTY)); + env.close(); + } + + // DEFENSIVE: temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries. + // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay() + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 9); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + batch::inner(pay(alice, bob, XRP(1)), seq + 3), + batch::inner(pay(alice, bob, XRP(1)), seq + 4), + batch::inner(pay(alice, bob, XRP(1)), seq + 5), + batch::inner(pay(alice, bob, XRP(1)), seq + 6), + batch::inner(pay(alice, bob, XRP(1)), seq + 7), + batch::inner(pay(alice, bob, XRP(1)), seq + 8), + batch::inner(pay(alice, bob, XRP(1)), seq + 9), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate Txn found. + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(10)), seq + 1)); + + env(jt.jv, batch::sig(bob), ter(temREDUNDANT)); + env.close(); + } + + // temINVALID: Batch: batch cannot have inner batch txn. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner( + batch::outer(alice, seq, batchFee, tfAllOrNothing), seq), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + ter(temINVALID)); + env.close(); + } + + // temINVALID_FLAG: Batch: inner txn must have the + // tfInnerBatchTxn flag. + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1[jss::Flags] = 0; + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(temINVALID_FLAG)); + env.close(); + } + + // temBAD_SIGNATURE: Batch: inner txn cannot include TxnSignature. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto jt = env.jt(pay(alice, bob, XRP(1))); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(jt.jv, seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + ter(temBAD_SIGNATURE)); + env.close(); + } + + // temBAD_SIGNER: Batch: inner txn cannot include Signers. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = pay(alice, bob, XRP(1)); + tx1[sfSigners.jsonName] = Json::arrayValue; + tx1[sfSigners.jsonName][0U][sfSigner.jsonName] = Json::objectValue; + tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfAccount.jsonName] = + alice.human(); + tx1[sfSigners.jsonName][0U][sfSigner.jsonName] + [sfSigningPubKey.jsonName] = strHex(alice.pk()); + tx1[sfSigners.jsonName][0U][sfSigner.jsonName] + [sfTxnSignature.jsonName] = "DEADBEEF"; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(tx1, seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + ter(temBAD_SIGNER)); + env.close(); + } + + // temBAD_REGKEY: Batch: inner txn must include empty + // SigningPubKey. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1); + tx1[jss::SigningPubKey] = strHex(alice.pk()); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(1)), seq + 2)); + + env(jt.jv, ter(temBAD_REGKEY)); + env.close(); + } + + // temINVALID_INNER_BATCH: Batch: inner txn preflight failed. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // amount can't be negative + batch::inner(pay(alice, bob, XRP(-1)), seq + 2), + ter(temINVALID_INNER_BATCH)); + env.close(); + } + + // temBAD_FEE: Batch: inner txn must have a fee of 0. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1); + tx1[jss::Fee] = to_string(env.current()->fees().base); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + ter(temBAD_FEE)); + env.close(); + } + + // temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence + // and TicketSequence. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(pay(alice, bob, XRP(1)), 0, 1); + tx1[jss::Sequence] = seq + 1; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + ter(temSEQ_AND_TICKET)); + env.close(); + } + + // temSEQ_AND_TICKET: Batch: inner txn must have either Sequence or + // TicketSequence. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + ter(temSEQ_AND_TICKET)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate sequence found: + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 1), + ter(temREDUNDANT)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate ticket found: + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, seq + 1), + batch::inner(pay(alice, bob, XRP(2)), 0, seq + 1), + ter(temREDUNDANT)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate ticket & sequence found: + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 1), + ter(temREDUNDANT)); + env.close(); + } + + // DEFENSIVE: temARRAY_TOO_LARGE: Batch: signers array exceeds 8 + // entries. + // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay() + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 9, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(5)), seq + 2), + batch::sig( + bob, + carol, + alice, + bob, + carol, + alice, + bob, + carol, + alice, + alice), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temBAD_SIGNER: Batch: signer cannot be the outer account + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::sig(alice, bob), + ter(temBAD_SIGNER)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate signer found + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::sig(bob, bob), + ter(temREDUNDANT)); + env.close(); + } + + // temBAD_SIGNER: Batch: no account signature for inner txn. + // Note: Extra signature by bob + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(5)), seq + 2), + batch::sig(bob), + ter(temBAD_SIGNER)); + env.close(); + } + + // temBAD_SIGNER: Batch: no account signature for inner txn. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::sig(carol), + ter(temBAD_SIGNER)); + env.close(); + } + + // temBAD_SIGNATURE: Batch: invalid batch txn signature. + { + auto const seq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto jt = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq)); + + Serializer msg; + serializeBatch( + msg, tfAllOrNothing, jt.stx->getBatchTransactionIDs()); + auto const sig = ripple::sign(bob.pk(), bob.sk(), msg.slice()); + jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName] + [sfAccount.jsonName] = bob.human(); + jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName] + [sfSigningPubKey.jsonName] = strHex(alice.pk()); + jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName] + [sfTxnSignature.jsonName] = + strHex(Slice{sig.data(), sig.size()}); + + env(jt.jv, ter(temBAD_SIGNATURE)); + env.close(); + } + + // temBAD_SIGNER: Batch: invalid batch signers. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::inner(pay(carol, alice, XRP(5)), env.seq(carol)), + batch::sig(bob), + ter(temBAD_SIGNER)); + env.close(); + } + } + + void + testPreclaim(FeatureBitset features) + { + testcase("preclaim"); + + using namespace test::jtx; + using namespace std::literals; + + //---------------------------------------------------------------------- + // preclaim + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const elsa = Account("elsa"); + auto const frank = Account("frank"); + auto const phantom = Account("phantom"); + env.memoize(phantom); + + env.fund(XRP(10000), alice, bob, carol, dave, elsa, frank); + env.close(); + + //---------------------------------------------------------------------- + // checkSign.checkSingleSign + + // tefBAD_AUTH: Bob is not authorized to sign for Alice + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(20)), seq + 2), + sig(bob), + ter(tefBAD_AUTH)); + env.close(); + } + + //---------------------------------------------------------------------- + // checkBatchSign.checkMultiSign + + // tefNOT_MULTI_SIGNING: SignersList not enabled + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {dave, carol}), + ter(tefNOT_MULTI_SIGNING)); + env.close(); + } + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + env(signers(bob, 2, {{carol, 1}, {dave, 1}, {elsa, 1}})); + env.close(); + + // tefBAD_SIGNATURE: Account not in SignersList + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, frank}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefBAD_SIGNATURE: Wrong publicKey type + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, Account("dave", KeyType::ed25519)}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefMASTER_DISABLED: Master key disabled + { + env(regkey(elsa, frank)); + env(fset(elsa, asfDisableMaster), sig(elsa)); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, elsa}), + ter(tefMASTER_DISABLED)); + env.close(); + } + + // tefBAD_SIGNATURE: Signer does not exist + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, phantom}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefBAD_SIGNATURE: Signer has not enabled RegularKey + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + Account const davo{"davo", KeyType::ed25519}; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, Reg{dave, davo}}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefBAD_SIGNATURE: Wrong RegularKey Set + { + env(regkey(dave, frank)); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + Account const davo{"davo", KeyType::ed25519}; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, Reg{dave, davo}}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefBAD_QUORUM + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol}), + ter(tefBAD_QUORUM)); + env.close(); + } + + // tesSUCCESS: BatchSigners.Signers + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, dave}), + ter(tesSUCCESS)); + env.close(); + } + + // tesSUCCESS: Multisign + BatchSigners.Signers + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 4, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, dave}), + msig(bob, carol), + ter(tesSUCCESS)); + env.close(); + } + + //---------------------------------------------------------------------- + // checkBatchSign.checkSingleSign + + // tefBAD_AUTH: Inner Account is not signer + { + auto const ledSeq = env.current()->seq(); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, phantom, XRP(1000)), seq + 1), + batch::inner(noop(phantom), ledSeq), + batch::sig(Reg{phantom, carol}), + ter(tefBAD_AUTH)); + env.close(); + } + + // tefBAD_AUTH: Account is not signer + { + auto const ledSeq = env.current()->seq(); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1000)), seq + 1), + batch::inner(noop(bob), ledSeq), + batch::sig(Reg{bob, carol}), + ter(tefBAD_AUTH)); + env.close(); + } + + // tesSUCCESS: Signed With Regular Key + { + env(regkey(bob, carol)); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(bob, alice, XRP(2)), env.seq(bob)), + batch::sig(Reg{bob, carol}), + ter(tesSUCCESS)); + env.close(); + } + + // tesSUCCESS: Signed With Master Key + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(bob, alice, XRP(2)), env.seq(bob)), + batch::sig(bob), + ter(tesSUCCESS)); + env.close(); + } + + // tefMASTER_DISABLED: Signed With Master Key Disabled + { + env(regkey(bob, carol)); + env(fset(bob, asfDisableMaster), sig(bob)); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(bob, alice, XRP(2)), env.seq(bob)), + batch::sig(bob), + ter(tefMASTER_DISABLED)); + env.close(); + } + } + + void + testBadRawTxn(FeatureBitset features) + { + testcase("bad raw txn"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + + // Invalid: sfTransactionType + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::TransactionType); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + + // Invalid: sfAccount + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::Account); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + + // Invalid: sfSequence + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::Sequence); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + + // Invalid: sfFee + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::Fee); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + + // Invalid: sfSigningPubKey + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::SigningPubKey); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + } + + void + testBadSequence(FeatureBitset features) + { + testcase("bad sequence"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + // Invalid: Alice Sequence is a past sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq - 10), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Alice Sequence is a future sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq + 10), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Bob Sequence is a past sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq - 10), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Bob Sequence is a future sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq + 10), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Outer and Inner Sequence are the same + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + } + + void + testBadOuterFee(FeatureBitset features) + { + testcase("bad outer fee"); + + using namespace test::jtx; + using namespace std::literals; + + // Bad Fee Without Signer + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 0, 2) + auto const batchFee = batch::calcBatchFee(env, 0, 1); + auto const aliceSeq = env.seq(alice); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(15)), aliceSeq + 2), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee With MultiSign + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 2, 2) + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const aliceSeq = env.seq(alice); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(15)), aliceSeq + 2), + msig(bob, carol), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee With MultiSign + BatchSigners + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 3, 2) + auto const batchFee = batch::calcBatchFee(env, 2, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + msig(bob, carol), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee With MultiSign + BatchSigners.Signers + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + env(signers(bob, 2, {{alice, 1}, {carol, 1}})); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 4, 2) + auto const batchFee = batch::calcBatchFee(env, 3, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::msig(bob, {alice, carol}), + msig(bob, carol), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee With BatchSigners + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 1, 2) + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee Dynamic Fee Calculation + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + auto const ammCreate = + [&alice](STAmount const& amount, STAmount const& amount2) { + Json::Value jv; + jv[jss::Account] = alice.human(); + jv[jss::Amount] = amount.getJson(JsonOptions::none); + jv[jss::Amount2] = amount2.getJson(JsonOptions::none); + jv[jss::TradingFee] = 0; + jv[jss::TransactionType] = jss::AMMCreate; + return jv; + }; + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(ammCreate(XRP(10), USD(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(10)), seq + 2), + ter(telINSUF_FEE_P)); + env.close(); + } + } + + void + testCalculateBaseFee(FeatureBitset features) + { + testcase("calculate base fee"); + + using namespace test::jtx; + using namespace std::literals; + + // telENV_RPC_FAILED: Batch: txns array exceeds 8 entries. + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 9); + auto const aliceSeq = env.seq(alice); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries. + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 9); + auto const aliceSeq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq)); + + env.app().openLedger().modify( + [&](OpenView& view, beast::Journal j) { + auto const result = + ripple::apply(env.app(), view, *jt.stx, tapNONE, j); + BEAST_EXPECT( + !result.applied && result.ter == temARRAY_TOO_LARGE); + return result.applied; + }); + } + + // telENV_RPC_FAILED: Batch: signers array exceeds 8 entries. + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 9, 2); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(5)), aliceSeq + 2), + batch::sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temARRAY_TOO_LARGE: Batch: signers array exceeds 8 entries. + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 9); + auto const aliceSeq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(5)), aliceSeq + 2), + batch::sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob)); + + env.app().openLedger().modify( + [&](OpenView& view, beast::Journal j) { + auto const result = + ripple::apply(env.app(), view, *jt.stx, tapNONE, j); + BEAST_EXPECT( + !result.applied && result.ter == temARRAY_TOO_LARGE); + return result.applied; + }); + } + } + + void + testAllOrNothing(FeatureBitset features) + { + testcase("all or nothing"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + // all + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // tec failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequence + BEAST_EXPECT(env.seq(alice) == seq + 1); + + // Alice pays Fee; Bob should not be affected + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // tef failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tefNO_AUTH_REQUIRED: trustline auth is not required + batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequence + BEAST_EXPECT(env.seq(alice) == seq + 1); + + // Alice pays Fee; Bob should not be affected + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // ter failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // terPRE_TICKET: ticket does not exist + batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequence + BEAST_EXPECT(env.seq(alice) == seq + 1); + + // Alice pays Fee; Bob should not be affected + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + } + + void + testOnlyOne(FeatureBitset features) + { + testcase("only one"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, dave, gw); + env.close(); + + // all transactions fail + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 1), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 2), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tecUNFUNDED_PAYMENT", txIDs[0], batchID}, + {2, "Payment", "tecUNFUNDED_PAYMENT", txIDs[1], batchID}, + {3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // first transaction fails + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tecUNFUNDED_PAYMENT", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // tec failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 2), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // tef failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + // tefNO_AUTH_REQUIRED: trustline auth is not required + batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1)); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // ter failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + // terPRE_TICKET: ticket does not exist + batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1)); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // tec (tecKILLED) error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 6); + + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 1), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 2), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 3), + batch::inner(pay(alice, bob, XRP(100)), seq + 4), + batch::inner(pay(alice, carol, XRP(100)), seq + 5), + batch::inner(pay(alice, dave, XRP(100)), seq + 6)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "OfferCreate", "tecKILLED", txIDs[0], batchID}, + {2, "OfferCreate", "tecKILLED", txIDs[1], batchID}, + {3, "OfferCreate", "tecKILLED", txIDs[2], batchID}, + {4, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(100) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol); + } + } + + void + testUntilFailure(FeatureBitset features) + { + testcase("until failure"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, dave, gw); + env.close(); + + // first transaction fails + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + batch::inner(pay(alice, bob, XRP(2)), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tecUNFUNDED_PAYMENT", txIDs[0], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // all transactions succeed + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + batch::inner(pay(alice, bob, XRP(3)), seq + 3), + batch::inner(pay(alice, bob, XRP(4)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[2], batchID}, + {4, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 5); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(10)); + } + + // tec error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // tef error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // tefNO_AUTH_REQUIRED: trustline auth is not required + batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // ter error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // terPRE_TICKET: ticket does not exist + batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // tec (tecKILLED) error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(100)), seq + 1), + batch::inner(pay(alice, carol, XRP(100)), seq + 2), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 3), + batch::inner(pay(alice, dave, XRP(100)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "OfferCreate", "tecKILLED", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100)); + } + } + + void + testIndependent(FeatureBitset features) + { + testcase("independent"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + + // multiple transactions fail + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 2), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tecUNFUNDED_PAYMENT", txIDs[1], batchID}, + {3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID}, + {4, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 5); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(4) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(4)); + } + + // tec error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID}, + {4, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 5); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(6) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(6)); + } + + // tef error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // tefNO_AUTH_REQUIRED: trustline auth is not required + batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6)); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(6)); + } + + // ter error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // terPRE_TICKET: ticket does not exist + batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6)); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(6)); + } + + // tec (tecKILLED) error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(100)), seq + 1), + batch::inner(pay(alice, carol, XRP(100)), seq + 2), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "OfferCreate", "tecKILLED", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100)); + } + } + + void + testInnerSubmitRPC(FeatureBitset features) + { + testcase("inner submit rpc"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + auto submitAndValidate = [&](Slice const& slice) { + auto const jrr = env.rpc("submit", strHex(slice))[jss::result]; + BEAST_EXPECT( + jrr[jss::status] == "error" && + jrr[jss::error] == "invalidTransaction" && + jrr[jss::error_exception] == + "fails local checks: Malformed: Invalid inner batch " + "transaction."); + env.close(); + }; + + // Invalid RPC Submission: TxnSignature + // - has `TxnSignature` field + // - has no `SigningPubKey` field + // - has no `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice)); + txn[sfTxnSignature] = "DEADBEEF"; + STParsedJSONObject parsed("test", txn.getTxn()); + Serializer s; + parsed.object->add(s); + submitAndValidate(s.slice()); + } + + // Invalid RPC Submission: SigningPubKey + // - has no `TxnSignature` field + // - has `SigningPubKey` field + // - has no `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice)); + txn[sfSigningPubKey] = strHex(alice.pk()); + STParsedJSONObject parsed("test", txn.getTxn()); + Serializer s; + parsed.object->add(s); + submitAndValidate(s.slice()); + } + + // Invalid RPC Submission: Signers + // - has no `TxnSignature` field + // - has empty `SigningPubKey` field + // - has `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice)); + txn[sfSigners] = Json::arrayValue; + STParsedJSONObject parsed("test", txn.getTxn()); + Serializer s; + parsed.object->add(s); + submitAndValidate(s.slice()); + } + + // Invalid RPC Submission: tfInnerBatchTxn + // - has no `TxnSignature` field + // - has empty `SigningPubKey` field + // - has no `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice)); + STParsedJSONObject parsed("test", txn.getTxn()); + Serializer s; + parsed.object->add(s); + auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result]; + BEAST_EXPECT( + jrr[jss::status] == "success" && + jrr[jss::engine_result] == "temINVALID_FLAG"); + + env.close(); + } + } + + void + testAccountActivation(FeatureBitset features) + { + testcase("account activation"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice); + env.close(); + env.memoize(bob); + + auto const preAlice = env.balance(alice); + auto const ledSeq = env.current()->seq(); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1000)), seq + 1), + batch::inner(fset(bob, asfAllowTrustLineClawback), ledSeq), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "AccountSet", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Bob consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == ledSeq + 1); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - batchFee); + BEAST_EXPECT(env.balance(bob) == XRP(1000)); + } + + void + testAccountSet(FeatureBitset features) + { + testcase("account set"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(noop(alice), seq + 1); + std::string domain = "example.com"; + tx1[sfDomain] = strHex(domain); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(1)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "AccountSet", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT( + sle->getFieldVL(sfDomain) == Blob(domain.begin(), domain.end())); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + void + testAccountDelete(FeatureBitset features) + { + testcase("account delete"); + + using namespace test::jtx; + using namespace std::literals; + + // tfIndependent: account delete success + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + incLgrSeqForAccDel(env, alice); + for (int i = 0; i < 5; ++i) + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2) + + env.current()->fees().increment; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(acctdelete(alice, bob), seq + 2), + // terNO_ACCOUNT: alice does not exist + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "AccountDelete", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice does not exist; Bob receives Alice's XRP + BEAST_EXPECT(!env.le(keylet::account(alice))); + BEAST_EXPECT(env.balance(bob) == preBob + (preAlice - batchFee)); + } + + // tfIndependent: account delete fails + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + incLgrSeqForAccDel(env, alice); + for (int i = 0; i < 5; ++i) + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + env.trust(bob["USD"](1000), alice); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2) + + env.current()->fees().increment; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tecHAS_OBLIGATIONS: alice has obligations + batch::inner(acctdelete(alice, bob), seq + 2), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "AccountDelete", "tecHAS_OBLIGATIONS", txIDs[1], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice does not exist; Bob receives XRP + BEAST_EXPECT(env.le(keylet::account(alice))); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // tfAllOrNothing: account delete fails + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + incLgrSeqForAccDel(env, alice); + for (int i = 0; i < 5; ++i) + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2) + + env.current()->fees().increment; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(acctdelete(alice, bob), seq + 2), + // terNO_ACCOUNT: alice does not exist + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + + // Alice still exists; Bob is unchanged + BEAST_EXPECT(env.le(keylet::account(alice))); + BEAST_EXPECT(env.balance(bob) == preBob); + } + } + + void + testObjectCreateSequence(FeatureBitset features) + { + testcase("object create w/ sequence"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + // success + { + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + uint256 const chkID{getCheckIndex(bob, env.seq(bob))}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(check::create(bob, alice, USD(10)), bobSeq), + batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq + 1), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "CheckCash", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + + // Alice pays Fee; Bob XRP Unchanged + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + + // Alice pays USD & Bob receives USD + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD + USD(10)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10)); + } + + // failure + { + env(fset(alice, asfRequireDest)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + uint256 const chkID{getCheckIndex(bob, env.seq(bob))}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfIndependent), + // tecDST_TAG_NEEDED - alice has enabled asfRequireDest + batch::inner(check::create(bob, alice, USD(10)), bobSeq), + batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq + 1), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tecDST_TAG_NEEDED", txIDs[0], batchID}, + {2, "CheckCash", "tecNO_ENTRY", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + + // Bob consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + + // Alice pays Fee; Bob XRP Unchanged + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + + // Alice pays USD & Bob receives USD + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + } + + void + testObjectCreateTicket(FeatureBitset features) + { + testcase("object create w/ ticket"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 3); + uint256 const chkID{getCheckIndex(bob, bobSeq + 1)}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(ticket::create(bob, 10), bobSeq), + batch::inner(check::create(bob, alice, USD(10)), 0, bobSeq + 1), + batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq + 1), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "TicketCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "CheckCreate", "tesSUCCESS", txIDs[1], batchID}, + {3, "CheckCash", "tesSUCCESS", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + BEAST_EXPECT(env.seq(bob) == bobSeq + 10 + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD + USD(10)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10)); + } + + void + testObjectCreate3rdParty(FeatureBitset features) + { + testcase("object create w/ 3rd party"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 2, 2); + uint256 const chkID{getCheckIndex(bob, env.seq(bob))}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(carol, carolSeq, batchFee, tfAllOrNothing), + batch::inner(check::create(bob, alice, USD(10)), bobSeq), + batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq), + batch::sig(alice, bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "CheckCash", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + BEAST_EXPECT(env.seq(carol) == carolSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(carol) == preCarol - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD + USD(10)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10)); + } + + void + testTickets(FeatureBitset features) + { + { + testcase("tickets outer"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq + 0), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + ticket::use(aliceTicketSeq)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 9); + BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 9); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + { + testcase("tickets inner"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq), + batch::inner(pay(alice, bob, XRP(2)), 0, aliceTicketSeq + 1)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8); + BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + { + testcase("tickets outer inner"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq), + ticket::use(aliceTicketSeq)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8); + BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + } + + void + testSequenceOpenLedger(FeatureBitset features) + { + testcase("sequence open ledger"); + + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + + // Before Batch Txn w/ retry following ledger + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. Because the + // terPRE_SEQ is outside of the batch this noop transaction will ge + // reapplied in the following ledger + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const carolSeq = env.seq(carol); + + // AccountSet Txn + auto const noopTxn = env.jt(noop(alice), seq(aliceSeq + 2)); + auto const noopTxnID = to_string(noopTxn.stx->getTransactionID()); + env(noopTxn, ter(terPRE_SEQ)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(carol, carolSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + batch::sig(alice)); + env.close(); + + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger contains noop txn + std::vector testCases = { + {0, "AccountSet", "tesSUCCESS", noopTxnID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + } + + // Before Batch Txn w/ same sequence + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // AccountSet Txn + auto const noopTxn = env.jt(noop(alice), seq(aliceSeq + 1)); + env(noopTxn, ter(terPRE_SEQ)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 2)); + env.close(); + + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + + // After Batch Txn w/ same sequence + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 2)); + + auto const noopTxn = env.jt(noop(alice), seq(aliceSeq + 1)); + auto const noopTxnID = to_string(noopTxn.stx->getTransactionID()); + env(noopTxn, ter(tesSUCCESS)); + env.close(); + + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + + // Outer Batch terPRE_SEQ + { + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const carolSeq = env.seq(carol); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + terPRE_SEQ, + batch::outer(carol, carolSeq + 1, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + batch::sig(alice)); + + // AccountSet Txn + auto const noopTxn = env.jt(noop(carol), seq(carolSeq)); + auto const noopTxnID = to_string(noopTxn.stx->getTransactionID()); + env(noopTxn, ter(tesSUCCESS)); + env.close(); + + { + std::vector testCases = { + {0, "AccountSet", "tesSUCCESS", noopTxnID, std::nullopt}, + {1, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {2, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger contains no transactions + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + } + + void + testTicketsOpenLedger(FeatureBitset features) + { + testcase("tickets open ledger"); + + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + // Before Batch Txn w/ same ticket + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // AccountSet Txn + auto const noopTxn = + env.jt(noop(alice), ticket::use(aliceTicketSeq + 1)); + auto const noopTxnID = to_string(noopTxn.stx->getTransactionID()); + env(noopTxn, ter(tesSUCCESS)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq), + ticket::use(aliceTicketSeq)); + env.close(); + + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + + // After Batch Txn w/ same ticket + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq), + ticket::use(aliceTicketSeq)); + + // AccountSet Txn + auto const noopTxn = + env.jt(noop(alice), ticket::use(aliceTicketSeq + 1)); + env(noopTxn); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + } + + void + testObjectsOpenLedger(FeatureBitset features) + { + testcase("objects open ledger"); + + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + // Consume Object Before Batch Txn + { + // IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY + // because the create transaction has not been applied because the + // batch will run in the close ledger process. The batch will be + // allied and then retry this transaction in the current ledger. + + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // CheckCash Txn + uint256 const chkID{getCheckIndex(alice, aliceSeq)}; + auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10))); + auto const objTxnID = to_string(objTxn.stx->getTransactionID()); + env(objTxn, ter(tecNO_ENTRY)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(check::create(alice, bob, XRP(10)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + ticket::use(aliceTicketSeq)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "CheckCash", "tesSUCCESS", objTxnID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + + // Create Object Before Batch Txn + { + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + + // CheckCreate Txn + uint256 const chkID{getCheckIndex(alice, aliceSeq)}; + auto const objTxn = env.jt(check::create(alice, bob, XRP(10))); + auto const objTxnID = to_string(objTxn.stx->getTransactionID()); + env(objTxn, ter(tesSUCCESS)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(check::cash(bob, chkID, XRP(10)), bobSeq), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + ticket::use(aliceTicketSeq), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "CheckCreate", "tesSUCCESS", objTxnID, std::nullopt}, + {1, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {2, "CheckCash", "tesSUCCESS", txIDs[0], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + } + + // After Batch Txn + { + // IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY + // because the create transaction has not been applied because the + // batch will run in the close ledger process. The batch will be + // applied and then retry this transaction in the current ledger. + + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + uint256 const chkID{getCheckIndex(alice, aliceSeq)}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(check::create(alice, bob, XRP(10)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + ticket::use(aliceTicketSeq)); + + // CheckCash Txn + auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10))); + auto const objTxnID = to_string(objTxn.stx->getTransactionID()); + env(objTxn, ter(tecNO_ENTRY)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "CheckCash", "tesSUCCESS", objTxnID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + } + } + + void + testPseudoTxn(FeatureBitset features) + { + testcase("pseudo txn with tfInnerBatchTxn"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + STTx const stx = STTx(ttAMENDMENT, [&](auto& obj) { + obj.setAccountID(sfAccount, AccountID()); + obj.setFieldH256(sfAmendment, uint256(2)); + obj.setFieldU32(sfLedgerSequence, env.seq(alice)); + obj.setFieldU32(sfFlags, tfInnerBatchTxn); + }); + + std::string reason; + BEAST_EXPECT(isPseudoTx(stx)); + BEAST_EXPECT(!passesLocalChecks(stx, reason)); + BEAST_EXPECT(reason == "Cannot submit pseudo transactions."); + env.app().openLedger().modify([&](OpenView& view, beast::Journal j) { + auto const result = ripple::apply(env.app(), view, stx, tapNONE, j); + BEAST_EXPECT(!result.applied && result.ter == temINVALID_FLAG); + return result.applied; + }); + } + + void + testOpenLedger(FeatureBitset features) + { + testcase("batch open ledger"); + // IMPORTANT: When a transaction is submitted outside of a batch and + // another transaction is part of the batch, the batch might fail + // because the sequence is out of order. This is because the canonical + // order of transactions is determined by the account first. So in this + // case, alice's batch comes after bobs self submitted transaction even + // though the payment was submitted after the batch. + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + XRPAmount const baseFee = env.current()->fees().base; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const bobSeq = env.seq(bob); + + // Alice Pays Bob (Open Ledger) + auto const payTxn1 = env.jt(pay(alice, bob, XRP(10)), seq(aliceSeq)); + auto const payTxn1ID = to_string(payTxn1.stx->getTransactionID()); + env(payTxn1, ter(tesSUCCESS)); + + // Alice & Bob Atomic Batch + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq + 1, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 2), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob)); + + // Bob pays Alice (Open Ledger) + auto const payTxn2 = env.jt(pay(bob, alice, XRP(5)), seq(bobSeq + 1)); + auto const payTxn2ID = to_string(payTxn2.stx->getTransactionID()); + env(payTxn2, ter(terPRE_SEQ)); + env.close(); + + std::vector testCases = { + {0, "Payment", "tesSUCCESS", payTxn1ID, std::nullopt}, + {1, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {2, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + env.close(); + { + // next ledger includes the payment txn + std::vector testCases = { + {0, "Payment", "tesSUCCESS", payTxn2ID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == aliceSeq + 3); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == bobSeq + 2); + + // Alice pays XRP & Fee; Bob receives XRP & pays Fee + BEAST_EXPECT( + env.balance(alice) == preAlice - XRP(10) - batchFee - baseFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(10) - baseFee); + } + + void + testBatchTxQueue(FeatureBitset features) + { + testcase("batch tx queue"); + + using namespace test::jtx; + using namespace std::literals; + + // only outer batch transactions are counter towards the queue size + { + test::jtx::Env env{ + *this, + makeSmallQueueConfig( + {{"minimum_txn_in_ledger_standalone", "2"}}), + nullptr, + beast::severities::kError}; + + auto alice = Account("alice"); + auto bob = Account("bob"); + auto carol = Account("carol"); + + // Fund across several ledgers so the TxQ metrics stay restricted. + env.fund(XRP(10000), noripple(alice, bob)); + env.close(env.now() + 5s, 10000ms); + env.fund(XRP(10000), noripple(carol)); + env.close(env.now() + 5s, 10000ms); + + // Fill the ledger + env(noop(alice)); + env(noop(alice)); + env(noop(alice)); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); + + env(noop(carol), ter(terQUEUED)); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + + // Queue Batch + { + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + ter(terQUEUED)); + } + + checkMetrics(*this, env, 2, std::nullopt, 3, 2); + + // Replace Queued Batch + { + env(batch::outer( + alice, + aliceSeq, + openLedgerFee(env, batchFee), + tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + ter(tesSUCCESS)); + env.close(); + } + + checkMetrics(*this, env, 0, 12, 1, 6); + } + + // inner batch transactions are counter towards the ledger tx count + { + test::jtx::Env env{ + *this, + makeSmallQueueConfig( + {{"minimum_txn_in_ledger_standalone", "2"}}), + nullptr, + beast::severities::kError}; + + auto alice = Account("alice"); + auto bob = Account("bob"); + auto carol = Account("carol"); + + // Fund across several ledgers so the TxQ metrics stay restricted. + env.fund(XRP(10000), noripple(alice, bob)); + env.close(env.now() + 5s, 10000ms); + env.fund(XRP(10000), noripple(carol)); + env.close(env.now() + 5s, 10000ms); + + // Fill the ledger leaving room for 1 queued transaction + env(noop(alice)); + env(noop(alice)); + checkMetrics(*this, env, 0, std::nullopt, 2, 2); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + + // Batch Successful + { + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + ter(tesSUCCESS)); + } + + checkMetrics(*this, env, 0, std::nullopt, 3, 2); + + env(noop(carol), ter(terQUEUED)); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); + } + } + + void + testBatchNetworkOps(FeatureBitset features) + { + testcase("batch network ops"); + + using namespace test::jtx; + using namespace std::literals; + + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + + auto alice = Account("alice"); + auto bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto submitTx = [&](std::uint32_t flags) -> uint256 { + auto jt = env.jt(pay(alice, bob, XRP(1)), txflags(flags)); + Serializer s; + jt.stx->add(s); + env.app().getOPs().submitTransaction(jt.stx); + return jt.stx->getTransactionID(); + }; + + auto processTxn = [&](std::uint32_t flags) -> uint256 { + auto jt = env.jt(pay(alice, bob, XRP(1)), txflags(flags)); + Serializer s; + jt.stx->add(s); + std::string reason; + auto transaction = + std::make_shared(jt.stx, reason, env.app()); + env.app().getOPs().processTransaction( + transaction, false, true, NetworkOPs::FailHard::yes); + return transaction->getID(); + }; + + // Validate: NetworkOPs::submitTransaction() + { + // Submit a tx with tfInnerBatchTxn + uint256 const txBad = submitTx(tfInnerBatchTxn); + BEAST_EXPECT(env.app().getHashRouter().getFlags(txBad) == 0); + } + + // Validate: NetworkOPs::processTransaction() + { + uint256 const txid = processTxn(tfInnerBatchTxn); + // HashRouter::getFlags() should return SF_BAD + BEAST_EXPECT(env.app().getHashRouter().getFlags(txid) == SF_BAD); + } + } + + void + testBatchDelegate(FeatureBitset features) + { + testcase("batch delegate"); + + using namespace test::jtx; + using namespace std::literals; + + // delegated non atomic inner + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + auto tx = batch::inner(pay(alice, bob, XRP(1)), seq + 1); + tx[jss::Delegate] = bob.human(); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx, + batch::inner(pay(alice, bob, XRP(2)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // delegated atomic inner + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + + env(delegate::set(bob, carol, {"Payment"})); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + + auto tx = batch::inner(pay(bob, alice, XRP(1)), bobSeq); + tx[jss::Delegate] = carol.human(); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + tx, + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + // NOTE: Carol would normally pay the fee for delegated txns, but + // because the batch is atomic, the fee is paid by the batch + BEAST_EXPECT(env.balance(carol) == preCarol); + } + + // delegated non atomic inner (AccountSet) + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + auto tx = batch::inner(noop(alice), seq + 1); + std::string const domain = "example.com"; + tx[sfDomain.jsonName] = strHex(domain); + tx[jss::Delegate] = bob.human(); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx, + batch::inner(pay(alice, bob, XRP(2)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "AccountSet", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(2)); + } + } + + void + testWithFeats(FeatureBitset features) + { + testEnable(features); + testPreflight(features); + testPreclaim(features); + testBadRawTxn(features); + testBadSequence(features); + testBadOuterFee(features); + testCalculateBaseFee(features); + testAllOrNothing(features); + testOnlyOne(features); + testUntilFailure(features); + testIndependent(features); + testInnerSubmitRPC(features); + testAccountActivation(features); + testAccountSet(features); + testAccountDelete(features); + testObjectCreateSequence(features); + testObjectCreateTicket(features); + testObjectCreate3rdParty(features); + testTickets(features); + testSequenceOpenLedger(features); + testTicketsOpenLedger(features); + testObjectsOpenLedger(features); + testPseudoTxn(features); + testOpenLedger(features); + testBatchTxQueue(features); + testBatchNetworkOps(features); + testBatchDelegate(features); + } + +public: + void + run() override + { + using namespace test::jtx; + auto const sa = supported_amendments(); + testWithFeats(sa); + } +}; + +BEAST_DEFINE_TESTSUITE(Batch, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 5136627148..ca173a6993 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -231,6 +231,7 @@ class Delegate_test : public beast::unit_test::suite ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"UNLModify"}), ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"SetFee"}), ter(tecNO_PERMISSION)); + env(delegate::set(gw, alice, {"Batch"}), ter(tecNO_PERMISSION)); } } diff --git a/src/test/app/MultiSign_test.cpp b/src/test/app/MultiSign_test.cpp index b24c7ca39e..8c1880c1a0 100644 --- a/src/test/app/MultiSign_test.cpp +++ b/src/test/app/MultiSign_test.cpp @@ -460,7 +460,7 @@ public: // Attempt a multisigned transaction that meets the quorum. auto const baseFee = env.current()->fees().base; std::uint32_t aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{cheri, cher}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{cheri, cher}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -480,7 +480,7 @@ public: BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{becky, beck}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{becky, beck}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -488,7 +488,7 @@ public: aliceSeq = env.seq(alice); env(noop(alice), fee(3 * baseFee), - msig(msig::Reg{becky, beck}, msig::Reg{cheri, cher})); + msig(Reg{becky, beck}, Reg{cheri, cher})); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); } @@ -783,12 +783,12 @@ public: BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{cheri, cher}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{cheri, cher}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{daria, dari}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{daria, dari}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -801,7 +801,7 @@ public: aliceSeq = env.seq(alice); env(noop(alice), fee(5 * baseFee), - msig(becky, msig::Reg{cheri, cher}, msig::Reg{daria, dari}, jinni)); + msig(becky, Reg{cheri, cher}, Reg{daria, dari}, jinni)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -820,7 +820,7 @@ public: aliceSeq = env.seq(alice); env(noop(alice), fee(9 * baseFee), - msig(becky, msig::Reg{cheri, cher}, msig::Reg{daria, dari}, jinni)); + msig(becky, Reg{cheri, cher}, Reg{daria, dari}, jinni)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -828,7 +828,7 @@ public: aliceSeq = env.seq(alice); env(noop(alice), fee(5 * baseFee), - msig(becky, cheri, msig::Reg{daria, dari}, jinni)); + msig(becky, cheri, Reg{daria, dari}, jinni)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -853,8 +853,8 @@ public: fee(9 * baseFee), msig( becky, - msig::Reg{cheri, cher}, - msig::Reg{daria, dari}, + Reg{cheri, cher}, + Reg{daria, dari}, haunt, jinni, phase, @@ -1349,7 +1349,7 @@ public: // Becky cannot 2-level multisign for alice. 2-level multisigning // is not supported. env(noop(alice), - msig(msig::Reg{becky, bogie}), + msig(Reg{becky, bogie}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); @@ -1358,7 +1358,7 @@ public: // not yet enabled. Account const beck{"beck", KeyType::ed25519}; env(noop(alice), - msig(msig::Reg{becky, beck}), + msig(Reg{becky, beck}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); @@ -1368,13 +1368,13 @@ public: env(regkey(becky, beck), msig(demon), fee(2 * baseFee)); env.close(); - env(noop(alice), msig(msig::Reg{becky, beck}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{becky, beck}), fee(2 * baseFee)); env.close(); // The presence of becky's regular key does not influence whether she // can 2-level multisign; it still won't work. env(noop(alice), - msig(msig::Reg{becky, demon}), + msig(Reg{becky, demon}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 7b69cee1ce..947640495d 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -42,97 +43,6 @@ class TxQPosNegFlows_test : public beast::unit_test::suite static constexpr FeeLevel64 baseFeeLevel{256}; static constexpr FeeLevel64 minEscalationFeeLevel = baseFeeLevel * 500; - void - checkMetrics( - int line, - jtx::Env& env, - std::size_t expectedCount, - std::optional expectedMaxCount, - std::size_t expectedInLedger, - std::size_t expectedPerLedger, - std::uint64_t expectedMinFeeLevel = baseFeeLevel.fee(), - std::uint64_t expectedMedFeeLevel = minEscalationFeeLevel.fee()) - { - FeeLevel64 const expectedMin{expectedMinFeeLevel}; - FeeLevel64 const expectedMed{expectedMedFeeLevel}; - auto const metrics = env.app().getTxQ().getMetrics(*env.current()); - using namespace std::string_literals; - - metrics.referenceFeeLevel == baseFeeLevel - ? pass() - : fail( - "reference: "s + - std::to_string(metrics.referenceFeeLevel.value()) + "/" + - std::to_string(baseFeeLevel.value()), - __FILE__, - line); - - metrics.txCount == expectedCount - ? pass() - : fail( - "txCount: "s + std::to_string(metrics.txCount) + "/" + - std::to_string(expectedCount), - __FILE__, - line); - - metrics.txQMaxSize == expectedMaxCount - ? pass() - : fail( - "txQMaxSize: "s + - std::to_string(metrics.txQMaxSize.value_or(0)) + "/" + - std::to_string(expectedMaxCount.value_or(0)), - __FILE__, - line); - - metrics.txInLedger == expectedInLedger - ? pass() - : fail( - "txInLedger: "s + std::to_string(metrics.txInLedger) + "/" + - std::to_string(expectedInLedger), - __FILE__, - line); - - metrics.txPerLedger == expectedPerLedger - ? pass() - : fail( - "txPerLedger: "s + std::to_string(metrics.txPerLedger) + "/" + - std::to_string(expectedPerLedger), - __FILE__, - line); - - metrics.minProcessingFeeLevel == expectedMin - ? pass() - : fail( - "minProcessingFeeLevel: "s + - std::to_string(metrics.minProcessingFeeLevel.value()) + - "/" + std::to_string(expectedMin.value()), - __FILE__, - line); - - metrics.medFeeLevel == expectedMed - ? pass() - : fail( - "medFeeLevel: "s + - std::to_string(metrics.medFeeLevel.value()) + "/" + - std::to_string(expectedMed.value()), - __FILE__, - line); - - auto const expectedCurFeeLevel = expectedInLedger > expectedPerLedger - ? expectedMed * expectedInLedger * expectedInLedger / - (expectedPerLedger * expectedPerLedger) - : metrics.referenceFeeLevel; - - metrics.openLedgerFeeLevel == expectedCurFeeLevel - ? pass() - : fail( - "openLedgerFeeLevel: "s + - std::to_string(metrics.openLedgerFeeLevel.value()) + "/" + - std::to_string(expectedCurFeeLevel.value()), - __FILE__, - line); - } - void fillQueue(jtx::Env& env, jtx::Account const& account) { @@ -244,7 +154,7 @@ class TxQPosNegFlows_test : public beast::unit_test::suite // transactions as though they are ordinary transactions. auto const flagPerLedger = 1 + ripple::detail::numUpVotedAmendments(); auto const flagMaxQueue = ledgersInQueue * flagPerLedger; - checkMetrics(__LINE__, env, 0, flagMaxQueue, 0, flagPerLedger); + checkMetrics(*this, env, 0, flagMaxQueue, 0, flagPerLedger); // Pad a couple of txs with normal fees so the median comes // back down to normal @@ -255,7 +165,7 @@ class TxQPosNegFlows_test : public beast::unit_test::suite // metrics to reset to defaults, EXCEPT the maxQueue size. using namespace std::chrono_literals; env.close(env.now() + 5s, 10000ms); - checkMetrics(__LINE__, env, 0, flagMaxQueue, 0, expectedPerLedger); + checkMetrics(*this, env, 0, flagMaxQueue, 0, expectedPerLedger); auto const fees = env.current()->fees(); BEAST_EXPECT(fees.base == XRPAmount{base}); BEAST_EXPECT(fees.reserve == XRPAmount{reserve}); @@ -287,37 +197,37 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Create several accounts while the fee is cheap so they all apply. env.fund(XRP(50000), noripple(alice, bob, charlie, daria)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Alice - price starts exploding: held env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 3); + checkMetrics(*this, env, 1, std::nullopt, 4, 3); // Bob with really high fee - applies env(noop(bob), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 5, 3); + checkMetrics(*this, env, 1, std::nullopt, 5, 3); // Daria with low fee: hold env(noop(daria), fee(baseFee * 100), queued); - checkMetrics(__LINE__, env, 2, std::nullopt, 5, 3); + checkMetrics(*this, env, 2, std::nullopt, 5, 3); env.close(); // Verify that the held transactions got applied - checkMetrics(__LINE__, env, 0, 10, 2, 5); + checkMetrics(*this, env, 0, 10, 2, 5); ////////////////////////////////////////////////////////////// // Make some more accounts. We'll need them later to abuse the queue. env.fund(XRP(50000), noripple(elmo, fred, gwen, hank)); - checkMetrics(__LINE__, env, 0, 10, 6, 5); + checkMetrics(*this, env, 0, 10, 6, 5); // Now get a bunch of transactions held. env(noop(alice), fee(baseFee * 1.2), queued); - checkMetrics(__LINE__, env, 1, 10, 6, 5); + checkMetrics(*this, env, 1, 10, 6, 5); env(noop(bob), fee(baseFee), queued); // won't clear the queue env(noop(charlie), fee(baseFee * 2), queued); @@ -326,11 +236,11 @@ public: env(noop(fred), fee(baseFee * 1.9), queued); env(noop(gwen), fee(baseFee * 1.6), queued); env(noop(hank), fee(baseFee * 1.8), queued); - checkMetrics(__LINE__, env, 8, 10, 6, 5); + checkMetrics(*this, env, 8, 10, 6, 5); env.close(); // Verify that the held transactions got applied - checkMetrics(__LINE__, env, 1, 12, 7, 6); + checkMetrics(*this, env, 1, 12, 7, 6); // Bob's transaction is still stuck in the queue. @@ -339,45 +249,45 @@ public: // Hank sends another txn env(noop(hank), fee(baseFee), queued); // But he's not going to leave it in the queue - checkMetrics(__LINE__, env, 2, 12, 7, 6); + checkMetrics(*this, env, 2, 12, 7, 6); // Hank sees his txn got held and bumps the fee, // but doesn't even bump it enough to requeue env(noop(hank), fee(baseFee * 1.1), ter(telCAN_NOT_QUEUE_FEE)); - checkMetrics(__LINE__, env, 2, 12, 7, 6); + checkMetrics(*this, env, 2, 12, 7, 6); // Hank sees his txn got held and bumps the fee, // enough to requeue, but doesn't bump it enough to // apply to the ledger env(noop(hank), fee(baseFee * 600), queued); // But he's not going to leave it in the queue - checkMetrics(__LINE__, env, 2, 12, 7, 6); + checkMetrics(*this, env, 2, 12, 7, 6); // Hank sees his txn got held and bumps the fee, // high enough to get into the open ledger, because // he doesn't want to wait. env(noop(hank), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, 12, 8, 6); + checkMetrics(*this, env, 1, 12, 8, 6); // Hank then sends another, less important txn // (In addition to the metrics, this will verify that // the original txn got removed.) env(noop(hank), fee(baseFee * 2), queued); - checkMetrics(__LINE__, env, 2, 12, 8, 6); + checkMetrics(*this, env, 2, 12, 8, 6); env.close(); // Verify that bob and hank's txns were applied - checkMetrics(__LINE__, env, 0, 16, 2, 8); + checkMetrics(*this, env, 0, 16, 2, 8); // Close again with a simulated time leap to // reset the escalation limit down to minimum env.close(env.now() + 5s, 10000ms); - checkMetrics(__LINE__, env, 0, 16, 0, 3); + checkMetrics(*this, env, 0, 16, 0, 3); // Then close once more without the time leap // to reset the queue maxsize down to minimum env.close(); - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); ////////////////////////////////////////////////////////////// @@ -390,7 +300,7 @@ public: env(noop(gwen), fee(largeFee)); env(noop(fred), fee(largeFee)); env(noop(elmo), fee(largeFee)); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); // Use explicit fees so we can control which txn // will get dropped @@ -406,7 +316,7 @@ public: // Queue is full now. // clang-format off - checkMetrics(__LINE__, env, 6, 6, 4, 3, txFeeLevelByAccount(env, daria) + 1); + checkMetrics(*this, env, 6, 6, 4, 3, txFeeLevelByAccount(env, daria) + 1); // clang-format on // Try to add another transaction with the default (low) fee, // it should fail because the queue is full. @@ -419,7 +329,7 @@ public: // Queue is still full, of course, but the min fee has gone up // clang-format off - checkMetrics(__LINE__, env, 6, 6, 4, 3, txFeeLevelByAccount(env, elmo) + 1); + checkMetrics(*this, env, 6, 6, 4, 3, txFeeLevelByAccount(env, elmo) + 1); // clang-format on // Close out the ledger, the transactions are accepted, the @@ -428,11 +338,11 @@ public: // is put back in. Neat. env.close(); // clang-format off - checkMetrics(__LINE__, env, 2, 8, 5, 4, baseFeeLevel.fee(), calcMedFeeLevel(FeeLevel64{baseFeeLevel.fee() * largeFeeMultiplier})); + checkMetrics(*this, env, 2, 8, 5, 4, baseFeeLevel.fee(), calcMedFeeLevel(FeeLevel64{baseFeeLevel.fee() * largeFeeMultiplier})); // clang-format on env.close(); - checkMetrics(__LINE__, env, 0, 10, 2, 5); + checkMetrics(*this, env, 0, 10, 2, 5); ////////////////////////////////////////////////////////////// @@ -446,10 +356,10 @@ public: env(noop(daria)); env(pay(alice, iris, XRP(1000)), queued); env(noop(iris), seq(1), fee(baseFee * 2), ter(terNO_ACCOUNT)); - checkMetrics(__LINE__, env, 1, 10, 6, 5); + checkMetrics(*this, env, 1, 10, 6, 5); env.close(); - checkMetrics(__LINE__, env, 0, 12, 1, 6); + checkMetrics(*this, env, 0, 12, 1, 6); env.require(balance(iris, XRP(1000))); BEAST_EXPECT(env.seq(iris) == 11); @@ -475,7 +385,7 @@ public: ++metrics.txCount; checkMetrics( - __LINE__, + *this, env, metrics.txCount, metrics.txQMaxSize, @@ -496,14 +406,14 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Fund alice and then fill the ledger. env.fund(XRP(50000), noripple(alice)); env(noop(alice)); env(noop(alice)); env(noop(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); ////////////////////////////////////////////////////////////////// @@ -515,11 +425,11 @@ public: env(noop(alice), ticket::use(tkt1 - 2), ter(tefNO_TICKET)); env(noop(alice), ticket::use(tkt1 - 1), ter(terPRE_TICKET)); env.require(owners(alice, 0), tickets(alice, 0)); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 3); + checkMetrics(*this, env, 1, std::nullopt, 4, 3); env.close(); env.require(owners(alice, 250), tickets(alice, 250)); - checkMetrics(__LINE__, env, 0, 8, 1, 4); + checkMetrics(*this, env, 0, 8, 1, 4); BEAST_EXPECT(env.seq(alice) == tkt1 + 250); ////////////////////////////////////////////////////////////////// @@ -547,7 +457,7 @@ public: ticket::use(tkt1 + 13), fee(baseFee * 2.3), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, expectedMinFeeLevel); + checkMetrics(*this, env, 8, 8, 5, 4, expectedMinFeeLevel); // Check which of the queued transactions got into the ledger by // attempting to replace them. @@ -579,7 +489,7 @@ public: // the queue. env(noop(alice), ticket::use(tkt1 + 13), ter(telCAN_NOT_QUEUE_FEE)); - checkMetrics(__LINE__, env, 3, 10, 6, 5); + checkMetrics(*this, env, 3, 10, 6, 5); ////////////////////////////////////////////////////////////////// @@ -610,7 +520,7 @@ public: env(noop(alice), seq(nextSeq + 5), queued); env(noop(alice), seq(nextSeq + 6), queued); env(noop(alice), seq(nextSeq + 7), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 10, 10, 6, 5, 257); + checkMetrics(*this, env, 10, 10, 6, 5, 257); // Check which of the queued transactions got into the ledger by // attempting to replace them. @@ -638,7 +548,7 @@ public: env(noop(alice), seq(nextSeq + 6), ter(telCAN_NOT_QUEUE_FEE)); env(noop(alice), seq(nextSeq + 7), ter(telCAN_NOT_QUEUE_FEE)); - checkMetrics(__LINE__, env, 4, 12, 7, 6); + checkMetrics(*this, env, 4, 12, 7, 6); BEAST_EXPECT(env.seq(alice) == nextSeq + 4); ////////////////////////////////////////////////////////////////// @@ -669,7 +579,7 @@ public: fee(baseFee * 2.1), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); env.close(); env.require(owners(alice, 231), tickets(alice, 231)); @@ -700,7 +610,7 @@ public: env(noop(alice), seq(nextSeq + 7), ter(telCAN_NOT_QUEUE_FEE)); BEAST_EXPECT(env.seq(alice) == nextSeq + 6); - checkMetrics(__LINE__, env, 6, 14, 8, 7); + checkMetrics(*this, env, 6, 14, 8, 7); ////////////////////////////////////////////////////////////////// @@ -739,7 +649,7 @@ public: env(noop(alice), seq(nextSeq + 7), ter(tefPAST_SEQ)); BEAST_EXPECT(env.seq(alice) == nextSeq + 8); - checkMetrics(__LINE__, env, 0, 16, 6, 8); + checkMetrics(*this, env, 0, 16, 6, 8); } void @@ -754,28 +664,28 @@ public: auto gw = Account("gw"); auto USD = gw["USD"]; - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); // Create accounts env.fund(XRP(50000), noripple(alice, gw)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 2); + checkMetrics(*this, env, 0, std::nullopt, 2, 2); env.close(); - checkMetrics(__LINE__, env, 0, 4, 0, 2); + checkMetrics(*this, env, 0, 4, 0, 2); // Alice creates an unfunded offer while the ledger is not full env(offer(alice, XRP(1000), USD(1000)), ter(tecUNFUNDED_OFFER)); - checkMetrics(__LINE__, env, 0, 4, 1, 2); + checkMetrics(*this, env, 0, 4, 1, 2); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 4, 3, 2); + checkMetrics(*this, env, 0, 4, 3, 2); // Alice creates an unfunded offer that goes in the queue env(offer(alice, XRP(1000), USD(1000)), ter(terQUEUED)); - checkMetrics(__LINE__, env, 1, 4, 3, 2); + checkMetrics(*this, env, 1, 4, 3, 2); // The offer comes out of the queue env.close(); - checkMetrics(__LINE__, env, 0, 6, 1, 3); + checkMetrics(*this, env, 0, 6, 1, 3); } void @@ -794,44 +704,44 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); // Create several accounts while the fee is cheap so they all apply. env.fund(XRP(50000), noripple(alice, bob, charlie)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); // Future transaction for Alice - fails env(noop(alice), fee(openLedgerCost(env)), seq(env.seq(alice) + 1), ter(terPRE_SEQ)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); // Current transaction for Alice: held env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 3, 2); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); // Alice - sequence is too far ahead, so won't queue. env(noop(alice), seq(env.seq(alice) + 2), ter(telCAN_NOT_QUEUE)); - checkMetrics(__LINE__, env, 1, std::nullopt, 3, 2); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); // Bob with really high fee - applies env(noop(bob), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 2); + checkMetrics(*this, env, 1, std::nullopt, 4, 2); // Daria with low fee: hold env(noop(charlie), fee(baseFee * 100), queued); - checkMetrics(__LINE__, env, 2, std::nullopt, 4, 2); + checkMetrics(*this, env, 2, std::nullopt, 4, 2); // Alice with normal fee: hold env(noop(alice), seq(env.seq(alice) + 1), queued); - checkMetrics(__LINE__, env, 3, std::nullopt, 4, 2); + checkMetrics(*this, env, 3, std::nullopt, 4, 2); env.close(); // Verify that the held transactions got applied // Alice's bad transaction applied from the // Local Txs. - checkMetrics(__LINE__, env, 0, 8, 4, 4); + checkMetrics(*this, env, 0, 8, 4, 4); } void @@ -853,7 +763,7 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); // Fund across several ledgers so the TxQ metrics stay restricted. env.fund(XRP(1000), noripple(alice, bob)); @@ -863,11 +773,11 @@ public: env.fund(XRP(1000), noripple(edgar, felicia)); env.close(env.now() + 5s, 10000ms); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); env(noop(bob)); env(noop(charlie)); env(noop(daria)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); BEAST_EXPECT(env.current()->info().seq == 6); // Fail to queue an item with a low LastLedgerSeq @@ -886,7 +796,7 @@ public: env(noop(charlie), fee(largeFee), queued); env(noop(daria), fee(largeFee), queued); env(noop(edgar), fee(largeFee), queued); - checkMetrics(__LINE__, env, 5, std::nullopt, 3, 2); + checkMetrics(*this, env, 5, std::nullopt, 3, 2); { auto& txQ = env.app().getTxQ(); auto aliceStat = txQ.getAccountTxs(alice.id()); @@ -910,7 +820,7 @@ public: } env.close(); - checkMetrics(__LINE__, env, 1, 6, 4, 3); + checkMetrics(*this, env, 1, 6, 4, 3); // Keep alice's transaction waiting. env(noop(bob), fee(largeFee), queued); @@ -918,12 +828,12 @@ public: env(noop(daria), fee(largeFee), queued); env(noop(edgar), fee(largeFee), queued); env(noop(felicia), fee(largeFee - 1), queued); - checkMetrics(__LINE__, env, 6, 6, 4, 3, 257); + checkMetrics(*this, env, 6, 6, 4, 3, 257); env.close(); // alice's transaction is still hanging around // clang-format off - checkMetrics(__LINE__, env, 1, 8, 5, 4, baseFeeLevel.fee(), baseFeeLevel.fee() * largeFeeMultiplier); + checkMetrics(*this, env, 1, 8, 5, 4, baseFeeLevel.fee(), baseFeeLevel.fee() * largeFeeMultiplier); // clang-format on BEAST_EXPECT(env.seq(alice) == 3); @@ -938,7 +848,7 @@ public: env(noop(edgar), fee(anotherLargeFee), queued); env(noop(felicia), fee(anotherLargeFee - 1), queued); env(noop(felicia), fee(anotherLargeFee - 1), seq(env.seq(felicia) + 1), queued); - checkMetrics(__LINE__, env, 8, 8, 5, 4, baseFeeLevel.fee() + 1, baseFeeLevel.fee() * largeFeeMultiplier); + checkMetrics(*this, env, 8, 8, 5, 4, baseFeeLevel.fee() + 1, baseFeeLevel.fee() * largeFeeMultiplier); // clang-format on env.close(); @@ -946,7 +856,7 @@ public: // into the ledger, so her transaction is gone, // though one of felicia's is still in the queue. // clang-format off - checkMetrics(__LINE__, env, 1, 10, 6, 5, baseFeeLevel.fee(), baseFeeLevel.fee() * largeFeeMultiplier); + checkMetrics(*this, env, 1, 10, 6, 5, baseFeeLevel.fee(), baseFeeLevel.fee() * largeFeeMultiplier); // clang-format on BEAST_EXPECT(env.seq(alice) == 3); BEAST_EXPECT(env.seq(felicia) == 7); @@ -954,7 +864,7 @@ public: env.close(); // And now the queue is empty // clang-format off - checkMetrics(__LINE__, env, 0, 12, 1, 6, baseFeeLevel.fee(), baseFeeLevel.fee() * anotherLargeFeeMultiplier); + checkMetrics(*this, env, 0, 12, 1, 6, baseFeeLevel.fee(), baseFeeLevel.fee() * anotherLargeFeeMultiplier); // clang-format on BEAST_EXPECT(env.seq(alice) == 3); BEAST_EXPECT(env.seq(felicia) == 8); @@ -976,7 +886,7 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); // Fund across several ledgers so the TxQ metrics stay restricted. env.fund(XRP(1000), noripple(alice, bob)); @@ -988,21 +898,21 @@ public: env(noop(alice)); env(noop(alice)); env(noop(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); env(noop(bob), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 3, 2); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); // Since Alice's queue is empty this blocker can go into her queue. env(regkey(alice, bob), fee(0), queued); - checkMetrics(__LINE__, env, 2, std::nullopt, 3, 2); + checkMetrics(*this, env, 2, std::nullopt, 3, 2); // Close out this ledger so we can get a maxsize env.close(); - checkMetrics(__LINE__, env, 0, 6, 2, 3); + checkMetrics(*this, env, 0, 6, 2, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); constexpr auto aliceFeeMultiplier = 3; auto feeAlice = baseFee * aliceFeeMultiplier; @@ -1013,12 +923,12 @@ public: feeAlice = (feeAlice + 1) * 125 / 100; ++seqAlice; } - checkMetrics(__LINE__, env, 4, 6, 4, 3); + checkMetrics(*this, env, 4, 6, 4, 3); // Bob adds a zero fee blocker to his queue. auto const seqBob = env.seq(bob); env(regkey(bob, alice), fee(0), queued); - checkMetrics(__LINE__, env, 5, 6, 4, 3); + checkMetrics(*this, env, 5, 6, 4, 3); // Carol fills the queue. auto feeCarol = feeAlice; @@ -1030,7 +940,7 @@ public: ++seqCarol; } // clang-format off - checkMetrics( __LINE__, env, 6, 6, 4, 3, baseFeeLevel.fee() * aliceFeeMultiplier + 1); + checkMetrics(*this, env, 6, 6, 4, 3, baseFeeLevel.fee() * aliceFeeMultiplier + 1); // clang-format on // Carol submits high enough to beat Bob's average fee which kicks @@ -1042,20 +952,20 @@ public: env.close(); // Some of Alice's transactions stay in the queue. Bob's // transaction returns to the TxQ. - checkMetrics(__LINE__, env, 5, 8, 5, 4); + checkMetrics(*this, env, 5, 8, 5, 4); BEAST_EXPECT(env.seq(alice) == seqAlice - 4); BEAST_EXPECT(env.seq(bob) == seqBob); BEAST_EXPECT(env.seq(carol) == seqCarol + 1); env.close(); // The remaining queued transactions flush through to the ledger. - checkMetrics(__LINE__, env, 0, 10, 5, 5); + checkMetrics(*this, env, 0, 10, 5, 5); BEAST_EXPECT(env.seq(alice) == seqAlice); BEAST_EXPECT(env.seq(bob) == seqBob + 1); BEAST_EXPECT(env.seq(carol) == seqCarol + 1); env.close(); - checkMetrics(__LINE__, env, 0, 10, 0, 5); + checkMetrics(*this, env, 0, 10, 0, 5); BEAST_EXPECT(env.seq(alice) == seqAlice); BEAST_EXPECT(env.seq(bob) == seqBob + 1); BEAST_EXPECT(env.seq(carol) == seqCarol + 1); @@ -1101,19 +1011,19 @@ public: auto queued = ter(terQUEUED); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); env.fund(XRP(1000), noripple(alice, bob)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 2); + checkMetrics(*this, env, 0, std::nullopt, 2, 2); // Fill the ledger env(noop(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); // Put a transaction in the queue env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 3, 2); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); // Now cheat, and bypass the queue. { @@ -1131,12 +1041,12 @@ public: }); env.postconditions(jt, parsed); } - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 2); + checkMetrics(*this, env, 1, std::nullopt, 4, 2); env.close(); // Alice's queued transaction failed in TxQ::accept // with tefPAST_SEQ - checkMetrics(__LINE__, env, 0, 8, 0, 4); + checkMetrics(*this, env, 0, 8, 0, 4); } void @@ -1158,7 +1068,7 @@ public: auto queued = ter(terQUEUED); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // ledgers in queue is 2 because of makeConfig auto const initQueueMax = initFee(env, 3, 2, 10, 200, 50); @@ -1166,11 +1076,11 @@ public: // Create several accounts while the fee is cheap so they all apply. env.fund(drops(2000), noripple(alice)); env.fund(XRP(500000), noripple(bob, charlie, daria)); - checkMetrics(__LINE__, env, 0, initQueueMax, 4, 3); + checkMetrics(*this, env, 0, initQueueMax, 4, 3); // Alice - price starts exploding: held env(noop(alice), fee(11), queued); - checkMetrics(__LINE__, env, 1, initQueueMax, 4, 3); + checkMetrics(*this, env, 1, initQueueMax, 4, 3); auto aliceSeq = env.seq(alice); auto bobSeq = env.seq(bob); @@ -1178,28 +1088,28 @@ public: // Alice - try to queue a second transaction, but leave a gap env(noop(alice), seq(aliceSeq + 2), fee(100), ter(telCAN_NOT_QUEUE)); - checkMetrics(__LINE__, env, 1, initQueueMax, 4, 3); + checkMetrics(*this, env, 1, initQueueMax, 4, 3); // Alice - queue a second transaction. Yay! env(noop(alice), seq(aliceSeq + 1), fee(13), queued); - checkMetrics(__LINE__, env, 2, initQueueMax, 4, 3); + checkMetrics(*this, env, 2, initQueueMax, 4, 3); // Alice - queue a third transaction. Yay. env(noop(alice), seq(aliceSeq + 2), fee(17), queued); - checkMetrics(__LINE__, env, 3, initQueueMax, 4, 3); + checkMetrics(*this, env, 3, initQueueMax, 4, 3); // Bob - queue a transaction env(noop(bob), queued); - checkMetrics(__LINE__, env, 4, initQueueMax, 4, 3); + checkMetrics(*this, env, 4, initQueueMax, 4, 3); // Bob - queue a second transaction env(noop(bob), seq(bobSeq + 1), fee(50), queued); - checkMetrics(__LINE__, env, 5, initQueueMax, 4, 3); + checkMetrics(*this, env, 5, initQueueMax, 4, 3); // Charlie - queue a transaction, with a higher fee // than default env(noop(charlie), fee(15), queued); - checkMetrics(__LINE__, env, 6, initQueueMax, 4, 3); + checkMetrics(*this, env, 6, initQueueMax, 4, 3); BEAST_EXPECT(env.seq(alice) == aliceSeq); BEAST_EXPECT(env.seq(bob) == bobSeq); @@ -1208,7 +1118,7 @@ public: env.close(); // Verify that all of but one of the queued transactions // got applied. - checkMetrics(__LINE__, env, 1, 8, 5, 4); + checkMetrics(*this, env, 1, 8, 5, 4); // Verify that the stuck transaction is Bob's second. // Even though it had a higher fee than Alice's and @@ -1230,7 +1140,7 @@ public: queued); ++aliceSeq; } - checkMetrics(__LINE__, env, 8, 8, 5, 4, 513); + checkMetrics(*this, env, 8, 8, 5, 4, 513); { auto& txQ = env.app().getTxQ(); auto aliceStat = txQ.getAccountTxs(alice.id()); @@ -1261,24 +1171,24 @@ public: json(jss::LastLedgerSequence, lastLedgerSeq + 7), fee(aliceFee), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 513); + checkMetrics(*this, env, 8, 8, 5, 4, 513); // Charlie - try to add another item to the queue, // which fails because fee is lower than Alice's // queued average. env(noop(charlie), fee(19), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 513); + checkMetrics(*this, env, 8, 8, 5, 4, 513); // Charlie - add another item to the queue, which // causes Alice's last txn to drop env(noop(charlie), fee(30), queued); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); // Alice - now attempt to add one more to the queue, // which fails because the last tx was dropped, so // there is no complete chain. env(noop(alice), seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); // Alice wants this tx more than the dropped tx, // so resubmits with higher fee, but the queue @@ -1287,7 +1197,7 @@ public: seq(aliceSeq - 1), fee(aliceFee), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); // Try to replace a middle item in the queue // without enough fee. @@ -1297,18 +1207,18 @@ public: seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE_FEE)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); // Replace a middle item from the queue successfully ++aliceFee; env(noop(alice), seq(aliceSeq), fee(aliceFee), queued); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); env.close(); // Alice's transactions processed, along with // Charlie's, and the lost one is replayed and // added back to the queue. - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); aliceSeq = env.seq(alice) + 1; @@ -1322,18 +1232,18 @@ public: seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); // Try to spend more than Alice can afford with all the other txs. aliceSeq += 2; env(noop(alice), seq(aliceSeq), fee(aliceFee), ter(terINSUF_FEE_B)); - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); // Replace the last queued item with a transaction that will // bankrupt Alice --aliceFee; env(noop(alice), seq(aliceSeq), fee(aliceFee), queued); - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); // Alice - Attempt to queue a last transaction, but it // fails because the fee in flight is too high, before @@ -1344,14 +1254,14 @@ public: seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); env.close(); // All of Alice's transactions applied. - checkMetrics(__LINE__, env, 0, 12, 4, 6); + checkMetrics(*this, env, 0, 12, 4, 6); env.close(); - checkMetrics(__LINE__, env, 0, 12, 0, 6); + checkMetrics(*this, env, 0, 12, 0, 6); // Alice is broke env.require(balance(alice, XRP(0))); @@ -1361,17 +1271,17 @@ public: // account limit (10) txs. fillQueue(env, bob); bobSeq = env.seq(bob); - checkMetrics(__LINE__, env, 0, 12, 7, 6); + checkMetrics(*this, env, 0, 12, 7, 6); for (int i = 0; i < 10; ++i) env(noop(bob), seq(bobSeq + i), queued); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Bob hit the single account limit env(noop(bob), seq(bobSeq + 10), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Bob can replace one of the earlier txs regardless // of the limit env(noop(bob), seq(bobSeq + 5), fee(20), queued); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Try to replace a middle item in the queue // with enough fee to bankrupt bob and make the @@ -1382,7 +1292,7 @@ public: seq(bobSeq + 5), fee(bobFee), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Attempt to replace a middle item in the queue with enough fee // to bankrupt bob, and also to use fee averaging to clear out the @@ -1396,14 +1306,14 @@ public: seq(bobSeq + 5), fee(bobFee), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Close the ledger and verify that the queued transactions succeed // and bob has the right ending balance. env.close(); - checkMetrics(__LINE__, env, 3, 14, 8, 7); + checkMetrics(*this, env, 3, 14, 8, 7); env.close(); - checkMetrics(__LINE__, env, 0, 16, 3, 8); + checkMetrics(*this, env, 0, 16, 3, 8); env.require(balance(bob, drops(499'999'999'750))); } @@ -1431,20 +1341,20 @@ public: BEAST_EXPECT(env.current()->fees().base == 10); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 4); + checkMetrics(*this, env, 0, std::nullopt, 0, 4); // Create several accounts while the fee is cheap so they all apply. env.fund(XRP(50000), noripple(alice, bob, charlie, daria)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 4); + checkMetrics(*this, env, 0, std::nullopt, 4, 4); env.close(); - checkMetrics(__LINE__, env, 0, 8, 0, 4); + checkMetrics(*this, env, 0, 8, 0, 4); env.fund(XRP(50000), noripple(elmo, fred, gwen, hank)); - checkMetrics(__LINE__, env, 0, 8, 4, 4); + checkMetrics(*this, env, 0, 8, 4, 4); env.close(); - checkMetrics(__LINE__, env, 0, 8, 0, 4); + checkMetrics(*this, env, 0, 8, 0, 4); ////////////////////////////////////////////////////////////// @@ -1455,7 +1365,7 @@ public: env(noop(gwen)); env(noop(fred)); env(noop(elmo)); - checkMetrics(__LINE__, env, 0, 8, 5, 4); + checkMetrics(*this, env, 0, 8, 5, 4); auto aliceSeq = env.seq(alice); auto bobSeq = env.seq(bob); @@ -1482,7 +1392,7 @@ public: // Queue is full now. Minimum fee now reflects the // lowest fee in the queue. auto minFeeLevel = txFeeLevelByAccount(env, alice); - checkMetrics(__LINE__, env, 8, 8, 5, 4, minFeeLevel + 1); + checkMetrics(*this, env, 8, 8, 5, 4, minFeeLevel + 1); // Try to add another transaction with the default (low) fee, // it should fail because it can't replace the one already @@ -1495,13 +1405,13 @@ public: env(noop(charlie), fee(100), seq(charlieSeq + 1), queued); // Queue is still full. - checkMetrics(__LINE__, env, 8, 8, 5, 4, minFeeLevel + 1); + checkMetrics(*this, env, 8, 8, 5, 4, minFeeLevel + 1); // Six txs are processed out of the queue into the ledger, // leaving two txs. The dropped tx is retried from localTxs, and // put back into the queue. env.close(); - checkMetrics(__LINE__, env, 3, 10, 6, 5); + checkMetrics(*this, env, 3, 10, 6, 5); // This next test should remain unchanged regardless of // transaction ordering @@ -1587,7 +1497,7 @@ public: env(noop(gwen), seq(gwenSeq + qTxCount1[gwen.id()]++), fee(15), queued); minFeeLevel = txFeeLevelByAccount(env, gwen) + 1; - checkMetrics(__LINE__, env, 10, 10, 6, 5, minFeeLevel); + checkMetrics(*this, env, 10, 10, 6, 5, minFeeLevel); // Add another transaction, with a higher fee, // Not high enough to get into the ledger, but high @@ -1597,13 +1507,13 @@ public: seq(aliceSeq + qTxCount1[alice.id()]++), queued); - checkMetrics(__LINE__, env, 10, 10, 6, 5, minFeeLevel); + checkMetrics(*this, env, 10, 10, 6, 5, minFeeLevel); // Seven txs are processed out of the queue, leaving 3. One // dropped tx is retried from localTxs, and put back into the // queue. env.close(); - checkMetrics(__LINE__, env, 4, 12, 7, 6); + checkMetrics(*this, env, 4, 12, 7, 6); // Refresh the queue counts auto qTxCount2 = getTxsQueued(); @@ -1668,13 +1578,13 @@ public: auto alice = Account("alice"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 1); + checkMetrics(*this, env, 0, std::nullopt, 0, 1); env.fund(XRP(50000), noripple(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 1, 1); + checkMetrics(*this, env, 0, std::nullopt, 1, 1); env(fset(alice, asfAccountTxnID)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 1); + checkMetrics(*this, env, 0, std::nullopt, 2, 1); // Immediately after the fset, the sfAccountTxnID field // is still uninitialized, so preflight succeeds here, @@ -1683,14 +1593,14 @@ public: json(R"({"AccountTxnID": "0"})"), ter(telCAN_NOT_QUEUE)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 1); + checkMetrics(*this, env, 0, std::nullopt, 2, 1); env.close(); // The failed transaction is retried from LocalTx // and succeeds. - checkMetrics(__LINE__, env, 0, 4, 1, 2); + checkMetrics(*this, env, 0, 4, 1, 2); env(noop(alice)); - checkMetrics(__LINE__, env, 0, 4, 2, 2); + checkMetrics(*this, env, 0, 4, 2, 2); env(noop(alice), json(R"({"AccountTxnID": "0"})"), ter(tefWRONG_PRIOR)); } @@ -1714,10 +1624,10 @@ public: auto alice = Account("alice"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); env.fund(XRP(50000), noripple(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 1, 2); + checkMetrics(*this, env, 0, std::nullopt, 1, 2); FeeLevel64 medFeeLevel; for (int i = 0; i < 10; ++i) @@ -1737,12 +1647,12 @@ public: env(noop(alice), fee(cost)); } - checkMetrics(__LINE__, env, 0, std::nullopt, 11, 2); + checkMetrics(*this, env, 0, std::nullopt, 11, 2); env.close(); // If not for the maximum, the per ledger would be 11. // clang-format off - checkMetrics(__LINE__, env, 0, 10, 0, 5, baseFeeLevel.fee(), calcMedFeeLevel(medFeeLevel)); + checkMetrics(*this, env, 0, 10, 0, 5, baseFeeLevel.fee(), calcMedFeeLevel(medFeeLevel)); // clang-format on } @@ -1831,22 +1741,22 @@ public: // ledgers in queue is 2 because of makeConfig auto const initQueueMax = initFee(env, 3, 2, 10, 200, 50); - checkMetrics(__LINE__, env, 0, initQueueMax, 0, 3); + checkMetrics(*this, env, 0, initQueueMax, 0, 3); env.fund(drops(5000), noripple(alice)); env.fund(XRP(50000), noripple(bob)); - checkMetrics(__LINE__, env, 0, initQueueMax, 2, 3); + checkMetrics(*this, env, 0, initQueueMax, 2, 3); auto USD = bob["USD"]; env(offer(alice, USD(5000), drops(5000)), require(owners(alice, 1))); - checkMetrics(__LINE__, env, 0, initQueueMax, 3, 3); + checkMetrics(*this, env, 0, initQueueMax, 3, 3); env.close(); - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); // Fill up the ledger fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); // Queue up a couple of transactions, plus one // more expensive one. @@ -1855,7 +1765,7 @@ public: env(noop(alice), seq(aliceSeq++), queued); env(noop(alice), seq(aliceSeq++), queued); env(noop(alice), fee(drops(1000)), seq(aliceSeq), queued); - checkMetrics(__LINE__, env, 4, 6, 4, 3); + checkMetrics(*this, env, 4, 6, 4, 3); // This offer should take Alice's offer // up to Alice's reserve. @@ -1863,7 +1773,7 @@ public: fee(openLedgerCost(env)), require( balance(alice, drops(250)), owners(alice, 1), lines(alice, 1))); - checkMetrics(__LINE__, env, 4, 6, 5, 3); + checkMetrics(*this, env, 4, 6, 5, 3); // Try adding a new transaction. // Too many fees in flight. @@ -1871,12 +1781,12 @@ public: fee(drops(200)), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 4, 6, 5, 3); + checkMetrics(*this, env, 4, 6, 5, 3); // Close the ledger. All of Alice's transactions // take a fee, except the last one. env.close(); - checkMetrics(__LINE__, env, 1, 10, 3, 5); + checkMetrics(*this, env, 1, 10, 3, 5); env.require(balance(alice, drops(250 - 30))); // Still can't add a new transaction for Alice, @@ -1885,7 +1795,7 @@ public: fee(drops(200)), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 1, 10, 3, 5); + checkMetrics(*this, env, 1, 10, 3, 5); /* At this point, Alice's transaction is indefinitely stuck in the queue. Eventually it will either @@ -1897,13 +1807,13 @@ public: for (int i = 0; i < 9; ++i) { env.close(); - checkMetrics(__LINE__, env, 1, 10, 0, 5); + checkMetrics(*this, env, 1, 10, 0, 5); } // And Alice's transaction expires (via the retry limit, // not LastLedgerSequence). env.close(); - checkMetrics(__LINE__, env, 0, 10, 0, 5); + checkMetrics(*this, env, 0, 10, 0, 5); } void @@ -1922,11 +1832,11 @@ public: Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000), noripple(alice, bob)); env.memoize(charlie); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 3); + checkMetrics(*this, env, 0, std::nullopt, 2, 3); { // Cannot put a blocker in an account's queue if that queue // already holds two or more (non-blocker) entries. @@ -1935,7 +1845,7 @@ public: env(noop(alice)); // Set a regular key just to clear the password spent flag env(regkey(alice, charlie)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Put two "normal" txs in the queue auto const aliceSeq = env.seq(alice); @@ -1961,11 +1871,11 @@ public: // Other accounts are not affected env(noop(bob), queued); - checkMetrics(__LINE__, env, 3, std::nullopt, 4, 3); + checkMetrics(*this, env, 3, std::nullopt, 4, 3); // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 8, 4, 4); + checkMetrics(*this, env, 0, 8, 4, 4); } { // Replace a lone non-blocking tx with a blocker. @@ -2006,7 +1916,7 @@ public: // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 10, 3, 5); + checkMetrics(*this, env, 0, 10, 3, 5); } { // Put a blocker in an empty queue. @@ -2034,7 +1944,7 @@ public: // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 12, 3, 6); + checkMetrics(*this, env, 0, 12, 3, 6); } } @@ -2054,12 +1964,12 @@ public: Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000), noripple(alice, bob)); env.memoize(charlie); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 3); + checkMetrics(*this, env, 0, std::nullopt, 2, 3); std::uint32_t tkt{env.seq(alice) + 1}; { @@ -2070,7 +1980,7 @@ public: env(ticket::create(alice, 250), seq(tkt - 1)); // Set a regular key just to clear the password spent flag env(regkey(alice, charlie)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Put two "normal" txs in the queue auto const aliceSeq = env.seq(alice); @@ -2100,11 +2010,11 @@ public: // Other accounts are not affected env(noop(bob), queued); - checkMetrics(__LINE__, env, 3, std::nullopt, 4, 3); + checkMetrics(*this, env, 3, std::nullopt, 4, 3); // Drain the queue and local transactions. env.close(); - checkMetrics(__LINE__, env, 0, 8, 5, 4); + checkMetrics(*this, env, 0, 8, 5, 4); // Show that the local transactions have flushed through as well. BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -2166,7 +2076,7 @@ public: // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 10, 4, 5); + checkMetrics(*this, env, 0, 10, 4, 5); // Show that the local transactions have flushed through as well. BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -2200,7 +2110,7 @@ public: // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 12, 3, 6); + checkMetrics(*this, env, 0, 12, 3, 6); } } @@ -2232,10 +2142,10 @@ public: auto limit = 3; - checkMetrics(__LINE__, env, 0, initQueueMax, 0, limit); + checkMetrics(*this, env, 0, initQueueMax, 0, limit); env.fund(XRP(50000), noripple(alice, charlie), gw); - checkMetrics(__LINE__, env, 0, initQueueMax, limit + 1, limit); + checkMetrics(*this, env, 0, initQueueMax, limit + 1, limit); auto USD = gw["USD"]; auto BUX = gw["BUX"]; @@ -2250,16 +2160,16 @@ public: // If this offer crosses, all of alice's // XRP will be taken (except the reserve). env(offer(alice, BUX(5000), XRP(50000)), queued); - checkMetrics(__LINE__, env, 1, initQueueMax, limit + 1, limit); + checkMetrics(*this, env, 1, initQueueMax, limit + 1, limit); // But because the reserve is protected, another // transaction will be allowed to queue env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, initQueueMax, limit + 1, limit); + checkMetrics(*this, env, 2, initQueueMax, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // But once we close the ledger, we find alice // has plenty of XRP, because the offer didn't @@ -2271,7 +2181,7 @@ public: ////////////////////////////////////////// // Offer with high XRP out and high total fee blocks later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2279,12 +2189,12 @@ public: // Alice creates an offer with a fee of half the reserve env(offer(alice, BUX(5000), XRP(50000)), fee(drops(100)), queued); - checkMetrics(__LINE__, env, 1, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 1, limit * 2, limit + 1, limit); // Alice creates another offer with a fee // that brings the total to just shy of the reserve env(noop(alice), fee(drops(99)), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); // So even a noop will look like alice // doesn't have the balance to pay the fee @@ -2292,11 +2202,11 @@ public: fee(drops(51)), seq(aliceSeq + 2), ter(terINSUF_FEE_B)); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 3, limit); + checkMetrics(*this, env, 0, limit * 2, 3, limit); // But once we close the ledger, we find alice // has plenty of XRP, because the offer didn't @@ -2308,7 +2218,7 @@ public: ////////////////////////////////////////// // Offer with high XRP out and super high fee blocks later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2317,7 +2227,7 @@ public: // Alice creates an offer with a fee larger than the reserve // This one can queue because it's the first in the queue for alice env(offer(alice, BUX(5000), XRP(50000)), fee(drops(300)), queued); - checkMetrics(__LINE__, env, 1, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 1, limit * 2, limit + 1, limit); // So even a noop will look like alice // doesn't have the balance to pay the fee @@ -2325,11 +2235,11 @@ public: fee(drops(51)), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 1, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 1, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // But once we close the ledger, we find alice // has plenty of XRP, because the offer didn't @@ -2341,7 +2251,7 @@ public: ////////////////////////////////////////// // Offer with low XRP out allows later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2351,11 +2261,11 @@ public: // And later transactions are just fine env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // But once we close the ledger, we find alice // has plenty of XRP, because the offer didn't @@ -2367,7 +2277,7 @@ public: ////////////////////////////////////////// // Large XRP payment doesn't block later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2380,11 +2290,11 @@ public: // But because the reserve is protected, another // transaction will be allowed to queue env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // But once we close the ledger, we find alice // still has most of her balance, because the @@ -2394,7 +2304,7 @@ public: ////////////////////////////////////////// // Small XRP payment allows later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2405,11 +2315,11 @@ public: // And later transactions are just fine env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // The payment succeeds env.require( @@ -2420,19 +2330,19 @@ public: auto const amount = USD(500000); env(trust(alice, USD(50000000))); env(trust(charlie, USD(50000000))); - checkMetrics(__LINE__, env, 0, limit * 2, 4, limit); + checkMetrics(*this, env, 0, limit * 2, 4, limit); // Close so we don't have to deal // with tx ordering in consensus. env.close(); env(pay(gw, alice, amount)); - checkMetrics(__LINE__, env, 0, limit * 2, 1, limit); + checkMetrics(*this, env, 0, limit * 2, 1, limit); // Close so we don't have to deal // with tx ordering in consensus. env.close(); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2445,11 +2355,11 @@ public: // But that's fine, because it doesn't affect // alice's XRP balance (other than the fee, of course). env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // So once we close the ledger, alice has her // XRP balance, but her USD balance went to charlie. @@ -2469,7 +2379,7 @@ public: env.close(); fillQueue(env, charlie); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2485,11 +2395,11 @@ public: // But because the reserve is protected, another // transaction will be allowed to queue env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // So once we close the ledger, alice sent a payment // to charlie using only a portion of her XRP balance @@ -2504,7 +2414,7 @@ public: // Small XRP to IOU payment allows later txs. fillQueue(env, charlie); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2519,11 +2429,11 @@ public: // And later transactions are just fine env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // So once we close the ledger, alice sent a payment // to charlie using only a portion of her XRP balance @@ -2540,7 +2450,7 @@ public: env.close(); fillQueue(env, charlie); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2550,11 +2460,11 @@ public: env(noop(alice), seq(aliceSeq + 1), ter(terINSUF_FEE_B)); BEAST_EXPECT(env.balance(alice) == drops(30)); - checkMetrics(__LINE__, env, 1, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 1, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 1, limit); + checkMetrics(*this, env, 0, limit * 2, 1, limit); BEAST_EXPECT(env.balance(alice) == drops(5)); } @@ -2639,27 +2549,27 @@ public: Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Fund accounts while the fee is cheap so they all apply. env.fund(XRP(50000), noripple(alice, bob, charlie)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 3); + checkMetrics(*this, env, 0, std::nullopt, 3, 3); // Alice - no fee change yet env(noop(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Bob with really high fee - applies env(noop(bob), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 0, std::nullopt, 5, 3); + checkMetrics(*this, env, 0, std::nullopt, 5, 3); // Charlie with low fee: queued env(noop(charlie), fee(baseFee * 100), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 5, 3); + checkMetrics(*this, env, 1, std::nullopt, 5, 3); env.close(); // Verify that the queued transaction was applied - checkMetrics(__LINE__, env, 0, 10, 1, 5); + checkMetrics(*this, env, 0, 10, 1, 5); ///////////////////////////////////////////////////////////////// @@ -2670,7 +2580,7 @@ public: env(noop(bob), fee(baseFee * 100)); env(noop(bob), fee(baseFee * 100)); env(noop(bob), fee(baseFee * 100)); - checkMetrics(__LINE__, env, 0, 10, 6, 5); + checkMetrics(*this, env, 0, 10, 6, 5); // Use explicit fees so we can control which txn // will get dropped @@ -2695,7 +2605,7 @@ public: env(noop(alice), fee(baseFee * 2.1), seq(aliceSeq++), queued); // Queue is full now. - checkMetrics(__LINE__, env, 10, 10, 6, 5, expectedFeeLevel + 1); + checkMetrics(*this, env, 10, 10, 6, 5, expectedFeeLevel + 1); // Try to add another transaction with the default (low) fee, // it should fail because the queue is full. @@ -2825,7 +2735,7 @@ public: auto const bob = Account("bob"); env.fund(XRP(500000), noripple(alice, bob)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 1); + checkMetrics(*this, env, 0, std::nullopt, 2, 1); auto const aliceSeq = env.seq(alice); BEAST_EXPECT(env.current()->info().seq == 3); @@ -2845,7 +2755,7 @@ public: seq(aliceSeq + 3), json(R"({"LastLedgerSequence":11})"), ter(terQUEUED)); - checkMetrics(__LINE__, env, 4, std::nullopt, 2, 1); + checkMetrics(*this, env, 4, std::nullopt, 2, 1); auto const bobSeq = env.seq(bob); // Ledger 4 gets 3, // Ledger 5 gets 4, @@ -2854,17 +2764,17 @@ public: { env(noop(bob), seq(bobSeq + i), fee(baseFee * 20), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 4 + 3 + 4 + 5, std::nullopt, 2, 1); + checkMetrics(*this, env, 4 + 3 + 4 + 5, std::nullopt, 2, 1); // Close ledger 3 env.close(); - checkMetrics(__LINE__, env, 4 + 4 + 5, 20, 3, 2); + checkMetrics(*this, env, 4 + 4 + 5, 20, 3, 2); // Close ledger 4 env.close(); - checkMetrics(__LINE__, env, 4 + 5, 30, 4, 3); + checkMetrics(*this, env, 4 + 5, 30, 4, 3); // Close ledger 5 env.close(); // Alice's first two txs expired. - checkMetrics(__LINE__, env, 2, 40, 5, 4); + checkMetrics(*this, env, 2, 40, 5, 4); // Because aliceSeq is missing, aliceSeq + 1 fails env(noop(alice), seq(aliceSeq + 1), ter(terPRE_SEQ)); @@ -2873,27 +2783,27 @@ public: env(fset(alice, asfAccountTxnID), seq(aliceSeq), ter(telCAN_NOT_QUEUE_BLOCKS)); - checkMetrics(__LINE__, env, 2, 40, 5, 4); + checkMetrics(*this, env, 2, 40, 5, 4); // However we can fill the gap with a non-blocker. env(noop(alice), seq(aliceSeq), fee(baseFee * 2), ter(terQUEUED)); - checkMetrics(__LINE__, env, 3, 40, 5, 4); + checkMetrics(*this, env, 3, 40, 5, 4); // Attempt to queue up a new aliceSeq + 1 tx that's a blocker. env(fset(alice, asfAccountTxnID), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BLOCKS)); - checkMetrics(__LINE__, env, 3, 40, 5, 4); + checkMetrics(*this, env, 3, 40, 5, 4); // Queue up a non-blocker replacement for aliceSeq + 1. env(noop(alice), seq(aliceSeq + 1), fee(baseFee * 2), ter(terQUEUED)); - checkMetrics(__LINE__, env, 4, 40, 5, 4); + checkMetrics(*this, env, 4, 40, 5, 4); // Close ledger 6 env.close(); // We expect that all of alice's queued tx's got into // the open ledger. - checkMetrics(__LINE__, env, 0, 50, 4, 5); + checkMetrics(*this, env, 0, 50, 4, 5); BEAST_EXPECT(env.seq(alice) == aliceSeq + 4); } @@ -2927,7 +2837,7 @@ public: auto const bob = Account("bob"); env.fund(XRP(500000), noripple(alice, bob)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 1); + checkMetrics(*this, env, 0, std::nullopt, 2, 1); auto const aliceSeq = env.seq(alice); BEAST_EXPECT(env.current()->info().seq == 3); @@ -2974,7 +2884,7 @@ public: seq(aliceSeq + 19), json(R"({"LastLedgerSequence":11})"), ter(terQUEUED)); - checkMetrics(__LINE__, env, 10, std::nullopt, 2, 1); + checkMetrics(*this, env, 10, std::nullopt, 2, 1); auto const bobSeq = env.seq(bob); // Ledger 4 gets 2 from bob and 1 from alice, @@ -2984,21 +2894,21 @@ public: { env(noop(bob), seq(bobSeq + i), fee(baseFee * 20), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 10 + 2 + 4 + 5, std::nullopt, 2, 1); + checkMetrics(*this, env, 10 + 2 + 4 + 5, std::nullopt, 2, 1); // Close ledger 3 env.close(); - checkMetrics(__LINE__, env, 9 + 4 + 5, 20, 3, 2); + checkMetrics(*this, env, 9 + 4 + 5, 20, 3, 2); BEAST_EXPECT(env.seq(alice) == aliceSeq + 12); // Close ledger 4 env.close(); - checkMetrics(__LINE__, env, 9 + 5, 30, 4, 3); + checkMetrics(*this, env, 9 + 5, 30, 4, 3); BEAST_EXPECT(env.seq(alice) == aliceSeq + 12); // Close ledger 5 env.close(); // Three of Alice's txs expired. - checkMetrics(__LINE__, env, 6, 40, 5, 4); + checkMetrics(*this, env, 6, 40, 5, 4); BEAST_EXPECT(env.seq(alice) == aliceSeq + 12); // Top off Alice's queue again using Tickets so the sequence gap is @@ -3009,7 +2919,7 @@ public: env(noop(alice), ticket::use(aliceSeq + 4), ter(terQUEUED)); env(noop(alice), ticket::use(aliceSeq + 5), ter(terQUEUED)); env(noop(alice), ticket::use(aliceSeq + 6), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 11, 40, 5, 4); + checkMetrics(*this, env, 11, 40, 5, 4); // Even though alice's queue is full we can still slide in a couple // more transactions because she has a sequence gap. But we @@ -3040,7 +2950,7 @@ public: // Finally we can fill in the entire gap. env(noop(alice), seq(aliceSeq + 18), ter(terQUEUED)); - checkMetrics(__LINE__, env, 14, 40, 5, 4); + checkMetrics(*this, env, 14, 40, 5, 4); // Verify that nothing can be added now that the gap is filled. env(noop(alice), seq(aliceSeq + 20), ter(telCAN_NOT_QUEUE_FULL)); @@ -3049,18 +2959,18 @@ public: // but alice adds some more transaction(s) so expectedCount // may not reduce to 8. env.close(); - checkMetrics(__LINE__, env, 9, 50, 6, 5); + checkMetrics(*this, env, 9, 50, 6, 5); BEAST_EXPECT(env.seq(alice) == aliceSeq + 15); // Close ledger 7. That should remove 4 more of alice's transactions. env.close(); - checkMetrics(__LINE__, env, 2, 60, 7, 6); + checkMetrics(*this, env, 2, 60, 7, 6); BEAST_EXPECT(env.seq(alice) == aliceSeq + 19); // Close one last ledger to see all of alice's transactions moved // into the ledger, including the tickets env.close(); - checkMetrics(__LINE__, env, 0, 70, 2, 7); + checkMetrics(*this, env, 0, 70, 2, 7); BEAST_EXPECT(env.seq(alice) == aliceSeq + 21); } @@ -3079,7 +2989,7 @@ public: env.fund(XRP(100000), alice, bob); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 7, 6); + checkMetrics(*this, env, 0, std::nullopt, 7, 6); // Queue up several transactions for alice sign-and-submit auto const aliceSeq = env.seq(alice); @@ -3100,7 +3010,7 @@ public: noop(alice), fee(baseFee * 100), seq(none), ter(terQUEUED))( submitParams); } - checkMetrics(__LINE__, env, 5, std::nullopt, 7, 6); + checkMetrics(*this, env, 5, std::nullopt, 7, 6); { auto aliceStat = txQ.getAccountTxs(alice.id()); SeqProxy seq = SeqProxy::sequence(aliceSeq); @@ -3126,25 +3036,25 @@ public: // Give them a higher fee so they'll beat alice's. for (int i = 0; i < 8; ++i) envs(noop(bob), fee(baseFee * 200), seq(none), ter(terQUEUED))(); - checkMetrics(__LINE__, env, 13, std::nullopt, 7, 6); + checkMetrics(*this, env, 13, std::nullopt, 7, 6); env.close(); - checkMetrics(__LINE__, env, 5, 14, 8, 7); + checkMetrics(*this, env, 5, 14, 8, 7); // Put some more txs in the queue for bob. // Give them a higher fee so they'll beat alice's. fillQueue(env, bob); for (int i = 0; i < 9; ++i) envs(noop(bob), fee(baseFee * 200), seq(none), ter(terQUEUED))(); - checkMetrics(__LINE__, env, 14, 14, 8, 7, 25601); + checkMetrics(*this, env, 14, 14, 8, 7, 25601); env.close(); // Put some more txs in the queue for bob. // Give them a higher fee so they'll beat alice's. fillQueue(env, bob); for (int i = 0; i < 10; ++i) envs(noop(bob), fee(baseFee * 200), seq(none), ter(terQUEUED))(); - checkMetrics(__LINE__, env, 15, 16, 9, 8); + checkMetrics(*this, env, 15, 16, 9, 8); env.close(); - checkMetrics(__LINE__, env, 4, 18, 10, 9); + checkMetrics(*this, env, 4, 18, 10, 9); { // Bob has nothing left in the queue. auto bobStat = txQ.getAccountTxs(bob.id()); @@ -3172,7 +3082,7 @@ public: // Now, fill the gap. envs(noop(alice), fee(baseFee * 100), seq(none), ter(terQUEUED))( submitParams); - checkMetrics(__LINE__, env, 5, 18, 10, 9); + checkMetrics(*this, env, 5, 18, 10, 9); { auto aliceStat = txQ.getAccountTxs(alice.id()); auto seq = aliceSeq; @@ -3187,7 +3097,7 @@ public: } env.close(); - checkMetrics(__LINE__, env, 0, 20, 5, 10); + checkMetrics(*this, env, 0, 20, 5, 10); { // Bob's data has been cleaned up. auto bobStat = txQ.getAccountTxs(bob.id()); @@ -3246,10 +3156,10 @@ public: BEAST_EXPECT(!queue_data.isMember(jss::max_spend_drops_total)); BEAST_EXPECT(!queue_data.isMember(jss::transactions)); } - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3278,7 +3188,7 @@ public: submitParams); envs(noop(alice), fee(baseFee * 10), seq(none), ter(terQUEUED))( submitParams); - checkMetrics(__LINE__, env, 4, 6, 4, 3); + checkMetrics(*this, env, 4, 6, 4, 3); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3330,7 +3240,7 @@ public: // Drain the queue so we can queue up a blocker. env.close(); - checkMetrics(__LINE__, env, 0, 8, 4, 4); + checkMetrics(*this, env, 0, 8, 4, 4); // Fill the ledger and then queue up a blocker. envs(noop(alice), seq(none))(submitParams); @@ -3341,7 +3251,7 @@ public: seq(none), json(jss::LastLedgerSequence, 10), ter(terQUEUED))(submitParams); - checkMetrics(__LINE__, env, 1, 8, 5, 4); + checkMetrics(*this, env, 1, 8, 5, 4); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3405,7 +3315,7 @@ public: fee(baseFee * 10), seq(none), ter(telCAN_NOT_QUEUE_BLOCKED))(submitParams); - checkMetrics(__LINE__, env, 1, 8, 5, 4); + checkMetrics(*this, env, 1, 8, 5, 4); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3483,9 +3393,9 @@ public: } env.close(); - checkMetrics(__LINE__, env, 0, 10, 2, 5); + checkMetrics(*this, env, 0, 10, 2, 5); env.close(); - checkMetrics(__LINE__, env, 0, 10, 0, 5); + checkMetrics(*this, env, 0, 10, 0, 5); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3555,10 +3465,10 @@ public: state[jss::load_factor_fee_reference] == 256); } - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); auto aliceSeq = env.seq(alice); auto submitParams = Json::Value(Json::objectValue); @@ -3568,7 +3478,7 @@ public: fee(baseFee * 10), seq(aliceSeq + i), ter(terQUEUED))(submitParams); - checkMetrics(__LINE__, env, 4, 6, 4, 3); + checkMetrics(*this, env, 4, 6, 4, 3); { auto const server_info = env.rpc("server_info"); @@ -3794,7 +3704,7 @@ public: // Fund the first few accounts at non escalated fee env.fund(XRP(50000), noripple(a, b, c, d)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // First transaction establishes the messaging using namespace std::chrono_literals; @@ -3844,7 +3754,7 @@ public: jv[jss::load_factor_fee_reference] == 256; })); - checkMetrics(__LINE__, env, 0, 8, 0, 4); + checkMetrics(*this, env, 0, 8, 0, 4); // Fund then next few accounts at non escalated fee env.fund(XRP(50000), noripple(e, f, g, h, i)); @@ -3858,7 +3768,7 @@ public: env(noop(e), fee(baseFee), queued); env(noop(f), fee(baseFee), queued); env(noop(g), fee(baseFee), queued); - checkMetrics(__LINE__, env, 7, 8, 5, 4); + checkMetrics(*this, env, 7, 8, 5, 4); // Last transaction escalates the fee BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -3928,7 +3838,7 @@ public: auto alice = Account("alice"); auto bob = Account("bob"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000000), alice, bob); fillQueue(env, alice); @@ -3982,7 +3892,7 @@ public: seq(aliceSeq++), ter(terQUEUED)); - checkMetrics(__LINE__, env, 3, std::nullopt, 4, 3); + checkMetrics(*this, env, 3, std::nullopt, 4, 3); // Figure out how much it would cost to cover all the // queued txs + itself @@ -3994,7 +3904,7 @@ public: // the edge case test. env(noop(alice), fee(totalFee), seq(aliceSeq++), ter(terQUEUED)); - checkMetrics(__LINE__, env, 4, std::nullopt, 4, 3); + checkMetrics(*this, env, 4, std::nullopt, 4, 3); // Now repeat the process including the new tx // and avoiding the rounding error @@ -4004,7 +3914,7 @@ public: // Submit a transaction with that fee. It will succeed. env(noop(alice), fee(totalFee), seq(aliceSeq++)); - checkMetrics(__LINE__, env, 0, std::nullopt, 9, 3); + checkMetrics(*this, env, 0, std::nullopt, 9, 3); } testcase("replace last tx with enough to clear queue"); @@ -4029,7 +3939,7 @@ public: seq(aliceSeq++), ter(terQUEUED)); - checkMetrics(__LINE__, env, 3, std::nullopt, 9, 3); + checkMetrics(*this, env, 3, std::nullopt, 9, 3); // Figure out how much it would cost to cover all the // queued txs + itself @@ -4041,10 +3951,10 @@ public: env(noop(alice), fee(totalFee), seq(aliceSeq++)); // The queue is clear - checkMetrics(__LINE__, env, 0, std::nullopt, 12, 3); + checkMetrics(*this, env, 0, std::nullopt, 12, 3); env.close(); - checkMetrics(__LINE__, env, 0, 24, 0, 12); + checkMetrics(*this, env, 0, 24, 0, 12); } testcase("replace middle tx with enough to clear queue"); @@ -4060,7 +3970,7 @@ public: ter(terQUEUED)); } - checkMetrics(__LINE__, env, 5, 24, 13, 12); + checkMetrics(*this, env, 5, 24, 13, 12); // Figure out how much it would cost to cover 3 txns uint64_t const totalFee = calcTotalFee(baseFee * 10 * 2, 3); @@ -4068,7 +3978,7 @@ public: aliceSeq -= 3; env(noop(alice), fee(totalFee), seq(aliceSeq++)); - checkMetrics(__LINE__, env, 2, 24, 16, 12); + checkMetrics(*this, env, 2, 24, 16, 12); auto const aliceQueue = env.app().getTxQ().getAccountTxs(alice.id()); BEAST_EXPECT(aliceQueue.size() == 2); @@ -4083,7 +3993,7 @@ public: // Close the ledger to clear the queue env.close(); - checkMetrics(__LINE__, env, 0, 32, 2, 16); + checkMetrics(*this, env, 0, 32, 2, 16); } testcase("clear queue failure (load)"); @@ -4109,7 +4019,7 @@ public: totalPaid += baseFee * 2.2; } - checkMetrics(__LINE__, env, 4, 32, 17, 16); + checkMetrics(*this, env, 4, 32, 17, 16); // Figure out how much it would cost to cover all the txns // + 1 @@ -4123,11 +4033,11 @@ public: env(noop(alice), fee(totalFee), seq(aliceSeq++), ter(terQUEUED)); // The original last transaction is still in the queue - checkMetrics(__LINE__, env, 5, 32, 17, 16); + checkMetrics(*this, env, 5, 32, 17, 16); // With high load, some of the txs stay in the queue env.close(); - checkMetrics(__LINE__, env, 3, 34, 2, 17); + checkMetrics(*this, env, 3, 34, 2, 17); // Load drops back down feeTrack.setRemoteFee(origFee); @@ -4135,14 +4045,14 @@ public: // Because of the earlier failure, alice can not clear the queue, // no matter how high the fee fillQueue(env, bob); - checkMetrics(__LINE__, env, 3, 34, 18, 17); + checkMetrics(*this, env, 3, 34, 18, 17); env(noop(alice), fee(XRP(1)), seq(aliceSeq++), ter(terQUEUED)); - checkMetrics(__LINE__, env, 4, 34, 18, 17); + checkMetrics(*this, env, 4, 34, 18, 17); // With normal load, those txs get into the ledger env.close(); - checkMetrics(__LINE__, env, 0, 36, 4, 18); + checkMetrics(*this, env, 0, 36, 4, 18); } } @@ -4164,77 +4074,77 @@ public: {"maximum_txn_per_account", "200"}})); auto alice = Account("alice"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000000), alice); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); auto seqAlice = env.seq(alice); auto txCount = 140; for (int i = 0; i < txCount; ++i) env(noop(alice), seq(seqAlice++), ter(terQUEUED)); - checkMetrics(__LINE__, env, txCount, std::nullopt, 4, 3); + checkMetrics(*this, env, txCount, std::nullopt, 4, 3); // Close a few ledgers successfully, so the limit grows env.close(); // 4 + 25% = 5 txCount -= 6; - checkMetrics(__LINE__, env, txCount, 10, 6, 5, 257); + checkMetrics(*this, env, txCount, 10, 6, 5, 257); env.close(); // 6 + 25% = 7 txCount -= 8; - checkMetrics(__LINE__, env, txCount, 14, 8, 7, 257); + checkMetrics(*this, env, txCount, 14, 8, 7, 257); env.close(); // 8 + 25% = 10 txCount -= 11; - checkMetrics(__LINE__, env, txCount, 20, 11, 10, 257); + checkMetrics(*this, env, txCount, 20, 11, 10, 257); env.close(); // 11 + 25% = 13 txCount -= 14; - checkMetrics(__LINE__, env, txCount, 26, 14, 13, 257); + checkMetrics(*this, env, txCount, 26, 14, 13, 257); env.close(); // 14 + 25% = 17 txCount -= 18; - checkMetrics(__LINE__, env, txCount, 34, 18, 17, 257); + checkMetrics(*this, env, txCount, 34, 18, 17, 257); env.close(); // 18 + 25% = 22 txCount -= 23; - checkMetrics(__LINE__, env, txCount, 44, 23, 22, 257); + checkMetrics(*this, env, txCount, 44, 23, 22, 257); env.close(); // 23 + 25% = 28 txCount -= 29; - checkMetrics(__LINE__, env, txCount, 56, 29, 28); + checkMetrics(*this, env, txCount, 56, 29, 28); // From 3 expected to 28 in 7 "fast" ledgers. // Close the ledger with a delay. env.close(env.now() + 5s, 10000ms); txCount -= 15; - checkMetrics(__LINE__, env, txCount, 56, 15, 14); + checkMetrics(*this, env, txCount, 56, 15, 14); // Close the ledger with a delay. env.close(env.now() + 5s, 10000ms); txCount -= 8; - checkMetrics(__LINE__, env, txCount, 56, 8, 7); + checkMetrics(*this, env, txCount, 56, 8, 7); // Close the ledger with a delay. env.close(env.now() + 5s, 10000ms); txCount -= 4; - checkMetrics(__LINE__, env, txCount, 56, 4, 3); + checkMetrics(*this, env, txCount, 56, 4, 3); // From 28 expected back down to 3 in 3 "slow" ledgers. // Confirm the minimum sticks env.close(env.now() + 5s, 10000ms); txCount -= 4; - checkMetrics(__LINE__, env, txCount, 56, 4, 3); + checkMetrics(*this, env, txCount, 56, 4, 3); BEAST_EXPECT(!txCount); } @@ -4250,35 +4160,35 @@ public: {"maximum_txn_per_account", "200"}})); auto alice = Account("alice"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000000), alice); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); auto seqAlice = env.seq(alice); auto txCount = 43; for (int i = 0; i < txCount; ++i) env(noop(alice), seq(seqAlice++), ter(terQUEUED)); - checkMetrics(__LINE__, env, txCount, std::nullopt, 4, 3); + checkMetrics(*this, env, txCount, std::nullopt, 4, 3); // Close a few ledgers successfully, so the limit grows env.close(); // 4 + 150% = 10 txCount -= 11; - checkMetrics(__LINE__, env, txCount, 20, 11, 10, 257); + checkMetrics(*this, env, txCount, 20, 11, 10, 257); env.close(); // 11 + 150% = 27 txCount -= 28; - checkMetrics(__LINE__, env, txCount, 54, 28, 27); + checkMetrics(*this, env, txCount, 54, 28, 27); // From 3 expected to 28 in 7 "fast" ledgers. // Close the ledger with a delay. env.close(env.now() + 5s, 10000ms); txCount -= 4; - checkMetrics(__LINE__, env, txCount, 54, 4, 3); + checkMetrics(*this, env, txCount, 54, 4, 3); // From 28 expected back down to 3 in 3 "slow" ledgers. @@ -4306,19 +4216,19 @@ public: auto const queued = ter(terQUEUED); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Create account env.fund(XRP(50000), noripple(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 1, 3); + checkMetrics(*this, env, 0, std::nullopt, 1, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Queue a transaction auto const aliceSeq = env.seq(alice); env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 3); + checkMetrics(*this, env, 1, std::nullopt, 4, 3); // Now, apply a (different) transaction directly // to the open ledger, bypassing the queue @@ -4334,23 +4244,23 @@ public: return result.applied; }); // the queued transaction is still there - checkMetrics(__LINE__, env, 1, std::nullopt, 5, 3); + checkMetrics(*this, env, 1, std::nullopt, 5, 3); // The next transaction should be able to go into the open // ledger, even though aliceSeq is queued. In earlier incarnations // of the TxQ this would cause an assert. env(noop(alice), seq(aliceSeq + 1), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 6, 3); + checkMetrics(*this, env, 1, std::nullopt, 6, 3); // Now queue a couple more transactions to make sure // they succeed despite aliceSeq being queued env(noop(alice), seq(aliceSeq + 2), queued); env(noop(alice), seq(aliceSeq + 3), queued); - checkMetrics(__LINE__, env, 3, std::nullopt, 6, 3); + checkMetrics(*this, env, 3, std::nullopt, 6, 3); // Now close the ledger. One of the queued transactions // (aliceSeq) should be dropped. env.close(); - checkMetrics(__LINE__, env, 0, 12, 2, 6); + checkMetrics(*this, env, 0, 12, 2, 6); } void @@ -4371,11 +4281,11 @@ public: auto queued = ter(terQUEUED); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Create account env.fund(XRP(50000), noripple(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 1, 3); + checkMetrics(*this, env, 0, std::nullopt, 1, 3); // Create tickets std::uint32_t const tktSeq0{env.seq(alice) + 1}; @@ -4383,12 +4293,12 @@ public: // Fill the queue so the next transaction will be queued. fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Queue a transaction with a ticket. Leave an unused ticket // on either side. env(noop(alice), ticket::use(tktSeq0 + 1), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 3); + checkMetrics(*this, env, 1, std::nullopt, 4, 3); // Now, apply a (different) transaction directly // to the open ledger, bypassing the queue @@ -4406,25 +4316,25 @@ public: return result.applied; }); // the queued transaction is still there - checkMetrics(__LINE__, env, 1, std::nullopt, 5, 3); + checkMetrics(*this, env, 1, std::nullopt, 5, 3); // The next (sequence-based) transaction should be able to go into // the open ledger, even though tktSeq0 is queued. Note that this // sequence-based transaction goes in front of the queued // transaction, so the queued transaction is left in the queue. env(noop(alice), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 6, 3); + checkMetrics(*this, env, 1, std::nullopt, 6, 3); // We should be able to do the same thing with a ticket that goes // if front of the queued transaction. This one too will leave // the queued transaction in place. env(noop(alice), ticket::use(tktSeq0 + 0), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 7, 3); + checkMetrics(*this, env, 1, std::nullopt, 7, 3); // We have one ticketed transaction in the queue. We should able // to add another to the queue. env(noop(alice), ticket::use(tktSeq0 + 2), queued); - checkMetrics(__LINE__, env, 2, std::nullopt, 7, 3); + checkMetrics(*this, env, 2, std::nullopt, 7, 3); // Here we try to force the queued transactions into the ledger by // adding one more queued (ticketed) transaction that pays enough @@ -4440,12 +4350,12 @@ public: // transaction is equally capable of going into the ledger independent // of all other ticket- or sequence-based transactions. env(noop(alice), ticket::use(tktSeq0 + 3), fee(XRP(10))); - checkMetrics(__LINE__, env, 2, std::nullopt, 8, 3); + checkMetrics(*this, env, 2, std::nullopt, 8, 3); // Now close the ledger. One of the queued transactions // (the one with tktSeq0 + 1) should be dropped. env.close(); - checkMetrics(__LINE__, env, 0, 16, 1, 8); + checkMetrics(*this, env, 0, 16, 1, 8); } void @@ -4496,7 +4406,7 @@ public: env.close(); env.fund(XRP(10000), fiona); env.close(); - checkMetrics(__LINE__, env, 0, 10, 0, 2); + checkMetrics(*this, env, 0, 10, 0, 2); // Close ledgers until the amendments show up. int i = 0; @@ -4508,7 +4418,7 @@ public: } auto expectedPerLedger = ripple::detail::numUpVotedAmendments() + 1; checkMetrics( - __LINE__, env, 0, 5 * expectedPerLedger, 0, expectedPerLedger); + *this, env, 0, 5 * expectedPerLedger, 0, expectedPerLedger); // Now wait 2 weeks modulo 256 ledgers for the amendments to be // enabled. Speed the process by closing ledgers every 80 minutes, @@ -4524,7 +4434,7 @@ public: // We're very close to the flag ledger. Fill the ledger. fillQueue(env, alice); checkMetrics( - __LINE__, + *this, env, 0, 5 * expectedPerLedger, @@ -4575,7 +4485,7 @@ public: } std::size_t expectedInQueue = 60; checkMetrics( - __LINE__, + *this, env, expectedInQueue, 5 * expectedPerLedger, @@ -4602,7 +4512,7 @@ public: expectedInLedger -= expectedInQueue; ++expectedPerLedger; checkMetrics( - __LINE__, + *this, env, expectedInQueue, 5 * expectedPerLedger, @@ -4689,7 +4599,7 @@ public: // of their transactions expire out of the queue. To start out // alice fills the ledger. fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 50, 7, 6); + checkMetrics(*this, env, 0, 50, 7, 6); // Now put a few transactions into alice's queue, including one that // will expire out soon. @@ -4735,9 +4645,9 @@ public: env(noop(fiona), seq(seqFiona++), fee(--feeDrops), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 34, 50, 7, 6); + checkMetrics(*this, env, 34, 50, 7, 6); env.close(); - checkMetrics(__LINE__, env, 26, 50, 8, 7); + checkMetrics(*this, env, 26, 50, 8, 7); // Re-fill the queue so alice and bob stay stuck. feeDrops = medFee; @@ -4748,9 +4658,9 @@ public: env(noop(ellie), seq(seqEllie++), fee(--feeDrops), ter(terQUEUED)); env(noop(fiona), seq(seqFiona++), fee(--feeDrops), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 38, 50, 8, 7); + checkMetrics(*this, env, 38, 50, 8, 7); env.close(); - checkMetrics(__LINE__, env, 29, 50, 9, 8); + checkMetrics(*this, env, 29, 50, 9, 8); // One more time... feeDrops = medFee; @@ -4761,9 +4671,9 @@ public: env(noop(ellie), seq(seqEllie++), fee(--feeDrops), ter(terQUEUED)); env(noop(fiona), seq(seqFiona++), fee(--feeDrops), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 41, 50, 9, 8); + checkMetrics(*this, env, 41, 50, 9, 8); env.close(); - checkMetrics(__LINE__, env, 29, 50, 10, 9); + checkMetrics(*this, env, 29, 50, 10, 9); // Finally the stage is set. alice's and bob's transactions expired // out of the queue which caused the dropPenalty flag to be set on @@ -4785,7 +4695,7 @@ public: env(noop(carol), seq(seqCarol++), fee(--feeDrops), ter(terQUEUED)); env(noop(daria), seq(seqDaria++), fee(--feeDrops), ter(terQUEUED)); env(noop(ellie), seq(seqEllie++), fee(--feeDrops), ter(terQUEUED)); - checkMetrics(__LINE__, env, 48, 50, 10, 9); + checkMetrics(*this, env, 48, 50, 10, 9); // Now induce a fee jump which should cause all the transactions // in the queue to fail with telINSUF_FEE_P. @@ -4802,7 +4712,7 @@ public: // o The _last_ transaction should be dropped from alice's queue. // o The first failing transaction should be dropped from bob's queue. env.close(); - checkMetrics(__LINE__, env, 46, 50, 0, 10); + checkMetrics(*this, env, 46, 50, 0, 10); // Run the local fee back down. while (env.app().getFeeTrack().lowerLocalFee()) @@ -4810,7 +4720,7 @@ public: // bob fills the ledger so it's easier to probe the TxQ. fillQueue(env, bob); - checkMetrics(__LINE__, env, 46, 50, 11, 10); + checkMetrics(*this, env, 46, 50, 11, 10); // Before the close() alice had two transactions in her queue. // We now expect her to have one. Here's the state of alice's queue. @@ -4928,7 +4838,7 @@ public: env.close(); - checkMetrics(__LINE__, env, 0, 50, 4, 6); + checkMetrics(*this, env, 0, 50, 4, 6); } { @@ -4989,7 +4899,7 @@ public: // The ticket transactions that didn't succeed or get queued succeed // this time because the tickets got consumed when the offers came // out of the queue - checkMetrics(__LINE__, env, 0, 50, 8, 7); + checkMetrics(*this, env, 0, 50, 8, 7); } } @@ -5010,7 +4920,7 @@ public: {"account_reserve", "0"}, {"owner_reserve", "0"}})); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // ledgers in queue is 2 because of makeConfig auto const initQueueMax = initFee(env, 3, 2, 0, 0, 0); @@ -5056,34 +4966,34 @@ public: } } - checkMetrics(__LINE__, env, 0, initQueueMax, 0, 3); + checkMetrics(*this, env, 0, initQueueMax, 0, 3); // The noripple is to reduce the number of transactions required to // fund the accounts. There is no rippling in this test. env.fund(XRP(100000), noripple(alice)); - checkMetrics(__LINE__, env, 0, initQueueMax, 1, 3); + checkMetrics(*this, env, 0, initQueueMax, 1, 3); env.close(); - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); env(noop(alice), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 0, 6, 5, 3); + checkMetrics(*this, env, 0, 6, 5, 3); auto aliceSeq = env.seq(alice); env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, 6, 5, 3); + checkMetrics(*this, env, 1, 6, 5, 3); env(noop(alice), seq(aliceSeq + 1), fee(10), queued); - checkMetrics(__LINE__, env, 2, 6, 5, 3); + checkMetrics(*this, env, 2, 6, 5, 3); { auto const fee = env.rpc("fee"); @@ -5126,7 +5036,7 @@ public: env.close(); - checkMetrics(__LINE__, env, 0, 10, 2, 5); + checkMetrics(*this, env, 0, 10, 2, 5); } void diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 67cc3812df..5aab737669 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -2273,6 +2273,8 @@ class Vault_test : public beast::unit_test::suite env(pay(issuer, owner, asset(500))); env.trust(asset(1000), depositor); env(pay(issuer, depositor, asset(500))); + env.trust(asset(1000), charlie); + env(pay(issuer, charlie, asset(5))); env.close(); auto [tx, keylet] = vault.create( @@ -2362,7 +2364,7 @@ class Vault_test : public beast::unit_test::suite env(credentials::create(depositor, credIssuer1, credType)); env(credentials::accept(depositor, credIssuer1, credType)); env(credentials::create(charlie, credIssuer1, credType)); - env(credentials::accept(charlie, credIssuer1, credType)); + // charlie's credential not accepted env.close(); auto credSle = env.le(credKeylet); BEAST_EXPECT(credSle != nullptr); @@ -2376,7 +2378,7 @@ class Vault_test : public beast::unit_test::suite tx = vault.deposit( {.depositor = charlie, .id = keylet.key, .amount = asset(50)}); - env(tx, ter{tecINSUFFICIENT_FUNDS}); + env(tx, ter{tecNO_AUTH}); env.close(); } @@ -2384,6 +2386,8 @@ class Vault_test : public beast::unit_test::suite testcase("private vault depositor lost authorization"); env(credentials::deleteCred( credIssuer1, depositor, credIssuer1, credType)); + env(credentials::deleteCred( + credIssuer1, charlie, credIssuer1, credType)); env.close(); auto credSle = env.le(credKeylet); BEAST_EXPECT(credSle == nullptr); @@ -2396,18 +2400,84 @@ class Vault_test : public beast::unit_test::suite env.close(); } - { - testcase("private vault depositor new authorization"); - env(credentials::create(depositor, credIssuer2, credType)); - env(credentials::accept(depositor, credIssuer2, credType)); - env.close(); + auto const shares = [&env, keylet = keylet, this]() -> Asset { + auto const vault = env.le(keylet); + BEAST_EXPECT(vault != nullptr); + return MPTIssue(vault->at(sfShareMPTID)); + }(); - auto tx = vault.deposit( - {.depositor = depositor, - .id = keylet.key, - .amount = asset(50)}); - env(tx); - env.close(); + { + testcase("private vault expired authorization"); + uint32_t const closeTime = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count(); + { + auto tx0 = + credentials::create(depositor, credIssuer2, credType); + tx0[sfExpiration] = closeTime + 20; + env(tx0); + tx0 = credentials::create(charlie, credIssuer2, credType); + tx0[sfExpiration] = closeTime + 20; + env(tx0); + env.close(); + + env(credentials::accept(depositor, credIssuer2, credType)); + env(credentials::accept(charlie, credIssuer2, credType)); + env.close(); + } + + { + auto tx1 = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx1); + env.close(); + + auto const tokenKeylet = keylet::mptoken( + shares.get().getMptID(), depositor.id()); + BEAST_EXPECT(env.le(tokenKeylet) != nullptr); + } + + { + // time advance + env.close(); + env.close(); + env.close(); + + auto const credsKeylet = + credentials::keylet(depositor, credIssuer2, credType); + BEAST_EXPECT(env.le(credsKeylet) != nullptr); + + auto tx2 = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(1)}); + env(tx2, ter{tecEXPIRED}); + env.close(); + + BEAST_EXPECT(env.le(credsKeylet) == nullptr); + } + + { + auto const credsKeylet = + credentials::keylet(charlie, credIssuer2, credType); + BEAST_EXPECT(env.le(credsKeylet) != nullptr); + auto const tokenKeylet = keylet::mptoken( + shares.get().getMptID(), charlie.id()); + BEAST_EXPECT(env.le(tokenKeylet) == nullptr); + + auto tx3 = vault.deposit( + {.depositor = charlie, + .id = keylet.key, + .amount = asset(2)}); + env(tx3, ter{tecEXPIRED}); + + env.close(); + BEAST_EXPECT(env.le(credsKeylet) == nullptr); + BEAST_EXPECT(env.le(tokenKeylet) == nullptr); + } } { diff --git a/src/test/jtx.h b/src/test/jtx.h index 6b73ca63ec..2e4764a403 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -31,8 +31,10 @@ #include #include #include +#include #include #include +#include #include #include #include diff --git a/src/test/jtx/SignerUtils.h b/src/test/jtx/SignerUtils.h new file mode 100644 index 0000000000..7b1ae5007c --- /dev/null +++ b/src/test/jtx/SignerUtils.h @@ -0,0 +1,56 @@ +#ifndef RIPPLE_TEST_JTX_SIGNERUTILS_H_INCLUDED +#define RIPPLE_TEST_JTX_SIGNERUTILS_H_INCLUDED + +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +struct Reg +{ + Account acct; + Account sig; + + Reg(Account const& masterSig) : acct(masterSig), sig(masterSig) + { + } + + Reg(Account const& acct_, Account const& regularSig) + : acct(acct_), sig(regularSig) + { + } + + Reg(char const* masterSig) : acct(masterSig), sig(masterSig) + { + } + + Reg(char const* acct_, char const* regularSig) + : acct(acct_), sig(regularSig) + { + } + + bool + operator<(Reg const& rhs) const + { + return acct < rhs.acct; + } +}; + +// Utility function to sort signers +inline void +sortSigners(std::vector& signers) +{ + std::sort( + signers.begin(), signers.end(), [](Reg const& lhs, Reg const& rhs) { + return lhs.acct < rhs.acct; + }); +} + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 534419494d..ae46ea4fe3 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -31,6 +32,14 @@ #include +#if (defined(__clang_major__) && __clang_major__ < 15) +#include +using source_location = std::experimental::source_location; +#else +#include +using std::source_location; +#endif + namespace ripple { namespace test { namespace jtx { @@ -445,7 +454,6 @@ create(A const& account, A const& dest, STAmount const& sendMax) jv[sfSendMax.jsonName] = sendMax.getJson(JsonOptions::none); jv[sfDestination.jsonName] = to_string(dest); jv[sfTransactionType.jsonName] = jss::CheckCreate; - jv[sfFlags.jsonName] = tfUniversal; return jv; } // clang-format on @@ -461,6 +469,102 @@ create( } // namespace check +static constexpr FeeLevel64 baseFeeLevel{256}; +static constexpr FeeLevel64 minEscalationFeeLevel = baseFeeLevel * 500; + +template +void +checkMetrics( + Suite& test, + jtx::Env& env, + std::size_t expectedCount, + std::optional expectedMaxCount, + std::size_t expectedInLedger, + std::size_t expectedPerLedger, + std::uint64_t expectedMinFeeLevel = baseFeeLevel.fee(), + std::uint64_t expectedMedFeeLevel = minEscalationFeeLevel.fee(), + source_location const location = source_location::current()) +{ + int line = location.line(); + char const* file = location.file_name(); + FeeLevel64 const expectedMin{expectedMinFeeLevel}; + FeeLevel64 const expectedMed{expectedMedFeeLevel}; + auto const metrics = env.app().getTxQ().getMetrics(*env.current()); + using namespace std::string_literals; + + metrics.referenceFeeLevel == baseFeeLevel + ? test.pass() + : test.fail( + "reference: "s + + std::to_string(metrics.referenceFeeLevel.value()) + "/" + + std::to_string(baseFeeLevel.value()), + file, + line); + + metrics.txCount == expectedCount + ? test.pass() + : test.fail( + "txCount: "s + std::to_string(metrics.txCount) + "/" + + std::to_string(expectedCount), + file, + line); + + metrics.txQMaxSize == expectedMaxCount + ? test.pass() + : test.fail( + "txQMaxSize: "s + std::to_string(metrics.txQMaxSize.value_or(0)) + + "/" + std::to_string(expectedMaxCount.value_or(0)), + file, + line); + + metrics.txInLedger == expectedInLedger + ? test.pass() + : test.fail( + "txInLedger: "s + std::to_string(metrics.txInLedger) + "/" + + std::to_string(expectedInLedger), + file, + line); + + metrics.txPerLedger == expectedPerLedger + ? test.pass() + : test.fail( + "txPerLedger: "s + std::to_string(metrics.txPerLedger) + "/" + + std::to_string(expectedPerLedger), + file, + line); + + metrics.minProcessingFeeLevel == expectedMin + ? test.pass() + : test.fail( + "minProcessingFeeLevel: "s + + std::to_string(metrics.minProcessingFeeLevel.value()) + "/" + + std::to_string(expectedMin.value()), + file, + line); + + metrics.medFeeLevel == expectedMed + ? test.pass() + : test.fail( + "medFeeLevel: "s + std::to_string(metrics.medFeeLevel.value()) + + "/" + std::to_string(expectedMed.value()), + file, + line); + + auto const expectedCurFeeLevel = expectedInLedger > expectedPerLedger + ? expectedMed * expectedInLedger * expectedInLedger / + (expectedPerLedger * expectedPerLedger) + : metrics.referenceFeeLevel; + + metrics.openLedgerFeeLevel == expectedCurFeeLevel + ? test.pass() + : test.fail( + "openLedgerFeeLevel: "s + + std::to_string(metrics.openLedgerFeeLevel.value()) + "/" + + std::to_string(expectedCurFeeLevel.value()), + file, + line); +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/acctdelete.h b/src/test/jtx/acctdelete.h index 98a23c6de2..21d00cb727 100644 --- a/src/test/jtx/acctdelete.h +++ b/src/test/jtx/acctdelete.h @@ -23,6 +23,8 @@ #include #include +#include + namespace ripple { namespace test { namespace jtx { @@ -31,6 +33,15 @@ namespace jtx { Json::Value acctdelete(Account const& account, Account const& dest); +// Close the ledger until the ledger sequence is large enough to close +// the account. If margin is specified, close the ledger so `margin` +// more closes are needed +void +incLgrSeqForAccDel( + jtx::Env& env, + jtx::Account const& acc, + std::uint32_t margin = 0); + } // namespace jtx } // namespace test diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h new file mode 100644 index 0000000000..ab235c293f --- /dev/null +++ b/src/test/jtx/batch.h @@ -0,0 +1,169 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_BATCH_H_INCLUDED +#define RIPPLE_TEST_JTX_BATCH_H_INCLUDED + +#include +#include +#include +#include +#include + +#include + +#include "test/jtx/SignerUtils.h" + +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Batch operations */ +namespace batch { + +/** Calculate Batch Fee. */ +XRPAmount +calcBatchFee( + jtx::Env const& env, + uint32_t const& numSigners, + uint32_t const& txns = 0); + +/** Batch. */ +Json::Value +outer( + jtx::Account const& account, + uint32_t seq, + STAmount const& fee, + std::uint32_t flags); + +/** Adds a new Batch Txn on a JTx and autofills. */ +class inner +{ +private: + Json::Value txn_; + std::uint32_t seq_; + std::optional ticket_; + +public: + inner( + Json::Value const& txn, + std::uint32_t const& sequence, + std::optional const& ticket = std::nullopt, + std::optional const& fee = std::nullopt) + : txn_(txn), seq_(sequence), ticket_(ticket) + { + txn_[jss::SigningPubKey] = ""; + txn_[jss::Sequence] = seq_; + txn_[jss::Fee] = "0"; + txn_[jss::Flags] = txn_[jss::Flags].asUInt() | tfInnerBatchTxn; + + // Optionally set ticket sequence + if (ticket_.has_value()) + { + txn_[jss::Sequence] = 0; + txn_[sfTicketSequence.jsonName] = *ticket_; + } + } + + void + operator()(Env&, JTx& jtx) const; + + Json::Value& + operator[](Json::StaticString const& key) + { + return txn_[key]; + } + + void + removeMember(Json::StaticString const& key) + { + txn_.removeMember(key); + } + + Json::Value const& + getTxn() const + { + return txn_; + } +}; + +/** Set a batch signature on a JTx. */ +class sig +{ +public: + std::vector signers; + + sig(std::vector signers_) : signers(std::move(signers_)) + { + sortSigners(signers); + } + + template + requires std::convertible_to + explicit sig(AccountType&& a0, Accounts&&... aN) + : signers{std::forward(a0), std::forward(aN)...} + { + sortSigners(signers); + } + + void + operator()(Env&, JTx& jt) const; +}; + +/** Set a batch nested multi-signature on a JTx. */ +class msig +{ +public: + Account master; + std::vector signers; + + msig(Account const& masterAccount, std::vector signers_) + : master(masterAccount), signers(std::move(signers_)) + { + sortSigners(signers); + } + + template + requires std::convertible_to + explicit msig( + Account const& masterAccount, + AccountType&& a0, + Accounts&&... aN) + : master(masterAccount) + , signers{std::forward(a0), std::forward(aN)...} + { + sortSigners(signers); + } + + void + operator()(Env&, JTx& jt) const; +}; + +} // namespace batch + +} // namespace jtx + +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 3482e7e867..6345253584 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -821,7 +821,6 @@ pay(Account const& account, AccountID const& to, STAmount const& amount) jv[jss::Amount] = amount.getJson(JsonOptions::none); jv[jss::Destination] = to_string(to); jv[jss::TransactionType] = jss::Payment; - jv[jss::Flags] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index e5b136e9c0..cb8141b9f3 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -219,7 +219,6 @@ escrow(AccountID const& account, AccountID const& to, STAmount const& amount) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv[jss::Destination] = to_string(to); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -231,7 +230,6 @@ finish(AccountID const& account, AccountID const& from, std::uint32_t seq) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowFinish; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv[sfOwner.jsonName] = to_string(from); jv[sfOfferSequence.jsonName] = seq; @@ -243,7 +241,6 @@ cancel(AccountID const& account, Account const& from, std::uint32_t seq) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCancel; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv[sfOwner.jsonName] = from.human(); jv[sfOfferSequence.jsonName] = seq; @@ -264,7 +261,6 @@ create( { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv[jss::Destination] = to_string(to); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -286,7 +282,6 @@ fund( { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelFund; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv[sfChannel.fieldName] = to_string(channel); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -306,7 +301,6 @@ claim( { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelClaim; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv["Channel"] = to_string(channel); if (amount) diff --git a/src/test/jtx/impl/acctdelete.cpp b/src/test/jtx/impl/acctdelete.cpp index 842eea7fc2..acce912d46 100644 --- a/src/test/jtx/impl/acctdelete.cpp +++ b/src/test/jtx/impl/acctdelete.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include @@ -36,6 +37,28 @@ acctdelete(jtx::Account const& account, jtx::Account const& dest) return jv; } +// Close the ledger until the ledger sequence is large enough to close +// the account. If margin is specified, close the ledger so `margin` +// more closes are needed +void +incLgrSeqForAccDel(jtx::Env& env, jtx::Account const& acc, std::uint32_t margin) +{ + using namespace jtx; + auto openLedgerSeq = [](jtx::Env& env) -> std::uint32_t { + return env.current()->seq(); + }; + + int const delta = [&]() -> int { + if (env.seq(acc) + 255 > openLedgerSeq(env)) + return env.seq(acc) - openLedgerSeq(env) + 255 - margin; + return 0; + }(); + env.test.BEAST_EXPECT(margin == 0 || delta >= 0); + for (int i = 0; i < delta; ++i) + env.close(); + env.test.BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255 - margin); +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/impl/batch.cpp b/src/test/jtx/impl/batch.cpp new file mode 100644 index 0000000000..055ed3fb55 --- /dev/null +++ b/src/test/jtx/impl/batch.cpp @@ -0,0 +1,154 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace batch { + +XRPAmount +calcBatchFee( + test::jtx::Env const& env, + uint32_t const& numSigners, + uint32_t const& txns) +{ + XRPAmount const feeDrops = env.current()->fees().base; + return ((numSigners + 2) * feeDrops) + feeDrops * txns; +} + +// Batch. +Json::Value +outer( + jtx::Account const& account, + uint32_t seq, + STAmount const& fee, + std::uint32_t flags) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::Batch; + jv[jss::Account] = account.human(); + jv[jss::RawTransactions] = Json::Value{Json::arrayValue}; + jv[jss::Sequence] = seq; + jv[jss::Flags] = flags; + jv[jss::Fee] = to_string(fee); + return jv; +} + +void +inner::operator()(Env& env, JTx& jt) const +{ + auto const index = jt.jv[jss::RawTransactions].size(); + Json::Value& batchTransaction = jt.jv[jss::RawTransactions][index]; + + // Initialize the batch transaction + batchTransaction = Json::Value{}; + batchTransaction[jss::RawTransaction] = txn_; +} + +void +sig::operator()(Env& env, JTx& jt) const +{ + auto const mySigners = signers; + std::optional st; + try + { + // required to cast the STObject to STTx + jt.jv[jss::SigningPubKey] = ""; + st = parse(jt.jv); + } + catch (parse_error const&) + { + env.test.log << pretty(jt.jv) << std::endl; + Rethrow(); + } + STTx const& stx = STTx{std::move(*st)}; + auto& js = jt[sfBatchSigners.getJsonName()]; + for (std::size_t i = 0; i < mySigners.size(); ++i) + { + auto const& e = mySigners[i]; + auto& jo = js[i][sfBatchSigner.getJsonName()]; + jo[jss::Account] = e.acct.human(); + jo[jss::SigningPubKey] = strHex(e.sig.pk().slice()); + + Serializer msg; + serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs()); + auto const sig = ripple::sign( + *publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice()); + jo[sfTxnSignature.getJsonName()] = + strHex(Slice{sig.data(), sig.size()}); + } +} + +void +msig::operator()(Env& env, JTx& jt) const +{ + auto const mySigners = signers; + std::optional st; + try + { + // required to cast the STObject to STTx + jt.jv[jss::SigningPubKey] = ""; + st = parse(jt.jv); + } + catch (parse_error const&) + { + env.test.log << pretty(jt.jv) << std::endl; + Rethrow(); + } + STTx const& stx = STTx{std::move(*st)}; + auto& bs = jt[sfBatchSigners.getJsonName()]; + auto const index = jt[sfBatchSigners.jsonName].size(); + auto& bso = bs[index][sfBatchSigner.getJsonName()]; + bso[jss::Account] = master.human(); + bso[jss::SigningPubKey] = ""; + auto& is = bso[sfSigners.getJsonName()]; + for (std::size_t i = 0; i < mySigners.size(); ++i) + { + auto const& e = mySigners[i]; + auto& iso = is[i][sfSigner.getJsonName()]; + iso[jss::Account] = e.acct.human(); + iso[jss::SigningPubKey] = strHex(e.sig.pk().slice()); + + Serializer msg; + serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs()); + finishMultiSigningData(e.acct.id(), msg); + auto const sig = ripple::sign( + *publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice()); + iso[sfTxnSignature.getJsonName()] = + strHex(Slice{sig.data(), sig.size()}); + } +} + +} // namespace batch + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/check.cpp b/src/test/jtx/impl/check.cpp index f5aa76658c..831bc900e7 100644 --- a/src/test/jtx/impl/check.cpp +++ b/src/test/jtx/impl/check.cpp @@ -37,7 +37,6 @@ cash(jtx::Account const& dest, uint256 const& checkId, STAmount const& amount) jv[sfAmount.jsonName] = amount.getJson(JsonOptions::none); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCash; - jv[sfFlags.jsonName] = tfUniversal; return jv; } @@ -53,7 +52,6 @@ cash( jv[sfDeliverMin.jsonName] = atLeast.value.getJson(JsonOptions::none); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCash; - jv[sfFlags.jsonName] = tfUniversal; return jv; } @@ -65,7 +63,6 @@ cancel(jtx::Account const& dest, uint256 const& checkId) jv[sfAccount.jsonName] = dest.human(); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCancel; - jv[sfFlags.jsonName] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/creds.cpp b/src/test/jtx/impl/creds.cpp index f29bc45e20..eae3b9501b 100644 --- a/src/test/jtx/impl/creds.cpp +++ b/src/test/jtx/impl/creds.cpp @@ -39,8 +39,6 @@ create( jv[jss::Account] = issuer.human(); jv[jss::Subject] = subject.human(); - - jv[jss::Flags] = tfUniversal; jv[sfCredentialType.jsonName] = strHex(credType); return jv; @@ -57,8 +55,6 @@ accept( jv[jss::Account] = subject.human(); jv[jss::Issuer] = issuer.human(); jv[sfCredentialType.jsonName] = strHex(credType); - jv[jss::Flags] = tfUniversal; - return jv; } @@ -75,7 +71,6 @@ deleteCred( jv[jss::Subject] = subject.human(); jv[jss::Issuer] = issuer.human(); jv[sfCredentialType.jsonName] = strHex(credType); - jv[jss::Flags] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/dids.cpp b/src/test/jtx/impl/dids.cpp index 67a523403c..1b443a5d9d 100644 --- a/src/test/jtx/impl/dids.cpp +++ b/src/test/jtx/impl/dids.cpp @@ -35,7 +35,6 @@ set(jtx::Account const& account) Json::Value jv; jv[jss::TransactionType] = jss::DIDSet; jv[jss::Account] = to_string(account.id()); - jv[jss::Flags] = tfUniversal; return jv; } @@ -45,7 +44,6 @@ setValid(jtx::Account const& account) Json::Value jv; jv[jss::TransactionType] = jss::DIDSet; jv[jss::Account] = to_string(account.id()); - jv[jss::Flags] = tfUniversal; jv[sfURI.jsonName] = strHex(std::string{"uri"}); return jv; } @@ -56,7 +54,6 @@ del(jtx::Account const& account) Json::Value jv; jv[jss::TransactionType] = jss::DIDDelete; jv[jss::Account] = to_string(account.id()); - jv[jss::Flags] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/ledgerStateFixes.cpp b/src/test/jtx/impl/ledgerStateFixes.cpp index 8c78069191..b7df78dd11 100644 --- a/src/test/jtx/impl/ledgerStateFixes.cpp +++ b/src/test/jtx/impl/ledgerStateFixes.cpp @@ -39,7 +39,6 @@ nftPageLinks(jtx::Account const& acct, jtx::Account const& owner) jv[sfLedgerFixType.jsonName] = LedgerStateFix::nfTokenPageLink; jv[sfOwner.jsonName] = owner.human(); jv[sfTransactionType.jsonName] = jss::LedgerStateFix; - jv[sfFlags.jsonName] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/multisign.cpp b/src/test/jtx/impl/multisign.cpp index a802528247..6ed6df6804 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -65,17 +65,6 @@ signers(Account const& account, none_t) //------------------------------------------------------------------------------ -msig::msig(std::vector signers_) : signers(std::move(signers_)) -{ - // Signatures must be applied in sorted order. - std::sort( - signers.begin(), - signers.end(), - [](msig::Reg const& lhs, msig::Reg const& rhs) { - return lhs.acct.id() < rhs.acct.id(); - }); -} - void msig::operator()(Env& env, JTx& jt) const { diff --git a/src/test/jtx/impl/pay.cpp b/src/test/jtx/impl/pay.cpp index 82fe910e9b..d1d994059e 100644 --- a/src/test/jtx/impl/pay.cpp +++ b/src/test/jtx/impl/pay.cpp @@ -35,7 +35,7 @@ pay(AccountID const& account, AccountID const& to, AnyAmount amount) jv[jss::Amount] = amount.value.getJson(JsonOptions::none); jv[jss::Destination] = to_string(to); jv[jss::TransactionType] = jss::Payment; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } Json::Value diff --git a/src/test/jtx/impl/txflags.cpp b/src/test/jtx/impl/txflags.cpp index 77c46f35b3..12c9cfeb83 100644 --- a/src/test/jtx/impl/txflags.cpp +++ b/src/test/jtx/impl/txflags.cpp @@ -28,7 +28,7 @@ namespace jtx { void txflags::operator()(Env&, JTx& jt) const { - jt[jss::Flags] = v_ /*| tfUniversal*/; + jt[jss::Flags] = v_ /*| tfFullyCanonicalSig*/; } } // namespace jtx diff --git a/src/test/jtx/impl/xchain_bridge.cpp b/src/test/jtx/impl/xchain_bridge.cpp index c63734ee8f..86e9deda7c 100644 --- a/src/test/jtx/impl/xchain_bridge.cpp +++ b/src/test/jtx/impl/xchain_bridge.cpp @@ -84,7 +84,6 @@ bridge_create( minAccountCreate->getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainCreateBridge; - jv[jss::Flags] = tfUniversal; return jv; } @@ -107,7 +106,6 @@ bridge_modify( minAccountCreate->getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainModifyBridge; - jv[jss::Flags] = tfUniversal; return jv; } @@ -126,7 +124,6 @@ xchain_create_claim_id( jv[sfOtherChainSource.getJsonName()] = otherChainSource.human(); jv[jss::TransactionType] = jss::XChainCreateClaimID; - jv[jss::Flags] = tfUniversal; return jv; } @@ -148,7 +145,6 @@ xchain_commit( jv[sfOtherChainDestination.getJsonName()] = dst->human(); jv[jss::TransactionType] = jss::XChainCommit; - jv[jss::Flags] = tfUniversal; return jv; } @@ -169,7 +165,6 @@ xchain_claim( jv[sfAmount.getJsonName()] = amt.value.getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainClaim; - jv[jss::Flags] = tfUniversal; return jv; } @@ -191,7 +186,6 @@ sidechain_xchain_account_create( reward.value.getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainAccountCreateCommit; - jv[jss::Flags] = tfUniversal; return jv; } @@ -242,7 +236,6 @@ claim_attestation( result[sfDestination.getJsonName()] = toBase58(*dst); result[jss::TransactionType] = jss::XChainAddClaimAttestation; - result[jss::Flags] = tfUniversal; return result; } @@ -297,7 +290,6 @@ create_account_attestation( rewardAmount.value.getJson(JsonOptions::none); result[jss::TransactionType] = jss::XChainAddAccountCreateAttestation; - result[jss::Flags] = tfUniversal; return result; } diff --git a/src/test/jtx/multisign.h b/src/test/jtx/multisign.h index 6bcb1a671c..1fed895c6d 100644 --- a/src/test/jtx/multisign.h +++ b/src/test/jtx/multisign.h @@ -21,6 +21,7 @@ #define RIPPLE_TEST_JTX_MULTISIGN_H_INCLUDED #include +#include #include #include #include @@ -65,48 +66,19 @@ signers(Account const& account, none_t); class msig { public: - struct Reg - { - Account acct; - Account sig; - - Reg(Account const& masterSig) : acct(masterSig), sig(masterSig) - { - } - - Reg(Account const& acct_, Account const& regularSig) - : acct(acct_), sig(regularSig) - { - } - - Reg(char const* masterSig) : acct(masterSig), sig(masterSig) - { - } - - Reg(char const* acct_, char const* regularSig) - : acct(acct_), sig(regularSig) - { - } - - bool - operator<(Reg const& rhs) const - { - return acct < rhs.acct; - } - }; - std::vector signers; -public: - msig(std::vector signers_); + msig(std::vector signers_) : signers(std::move(signers_)) + { + sortSigners(signers); + } template requires std::convertible_to explicit msig(AccountType&& a0, Accounts&&... aN) - : msig{std::vector{ - std::forward(a0), - std::forward(aN)...}} + : signers{std::forward(a0), std::forward(aN)...} { + sortSigners(signers); } void diff --git a/src/test/overlay/compression_test.cpp b/src/test/overlay/compression_test.cpp index 76c38fd59b..4ecbe7f232 100644 --- a/src/test/overlay/compression_test.cpp +++ b/src/test/overlay/compression_test.cpp @@ -473,17 +473,14 @@ public: Config c; std::stringstream str; str << "[reduce_relay]\n" - << "vp_enable=1\n" - << "vp_squelch=1\n" + << "vp_base_squelch_enable=1\n" << "[compression]\n" << enable << "\n"; c.loadFromString(str.str()); auto env = std::make_shared(*this); env->app().config().COMPRESSION = c.COMPRESSION; - env->app().config().VP_REDUCE_RELAY_ENABLE = - c.VP_REDUCE_RELAY_ENABLE; - env->app().config().VP_REDUCE_RELAY_SQUELCH = - c.VP_REDUCE_RELAY_SQUELCH; + env->app().config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = + c.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE; return env; }; auto handshake = [&](int outboundEnable, int inboundEnable) { @@ -496,7 +493,7 @@ public: env->app().config().COMPRESSION, false, env->app().config().TX_REDUCE_RELAY_ENABLE, - env->app().config().VP_REDUCE_RELAY_ENABLE); + env->app().config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE); http_request_type http_request; http_request.version(request.version()); http_request.base() = request.base(); diff --git a/src/test/overlay/reduce_relay_test.cpp b/src/test/overlay/reduce_relay_test.cpp index 18aebbe194..a8aafcfa06 100644 --- a/src/test/overlay/reduce_relay_test.cpp +++ b/src/test/overlay/reduce_relay_test.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include @@ -32,6 +33,8 @@ #include +#include +#include #include #include @@ -517,7 +520,8 @@ class OverlaySim : public Overlay, public reduce_relay::SquelchHandler public: using id_t = Peer::id_t; using clock_type = ManualClock; - OverlaySim(Application& app) : slots_(app.logs(), *this), logs_(app.logs()) + OverlaySim(Application& app) + : slots_(app.logs(), *this, app.config()), logs_(app.logs()) { } @@ -986,7 +990,10 @@ protected: network_.overlay().isCountingState(validator); BEAST_EXPECT( countingState == false && - selected.size() == reduce_relay::MAX_SELECTED_PEERS); + selected.size() == + env_.app() + .config() + .VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS); } // Trigger Link Down or Peer Disconnect event @@ -1188,7 +1195,10 @@ protected: { BEAST_EXPECT( squelched == - MAX_PEERS - reduce_relay::MAX_SELECTED_PEERS); + MAX_PEERS - + env_.app() + .config() + .VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS); n++; } }, @@ -1197,7 +1207,9 @@ protected: purge, resetClock); auto selected = network_.overlay().getSelected(network_.validator(0)); - BEAST_EXPECT(selected.size() == reduce_relay::MAX_SELECTED_PEERS); + BEAST_EXPECT( + selected.size() == + env_.app().config().VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS); BEAST_EXPECT(n == 1); // only one selection round auto res = checkCounting(network_.validator(0), false); BEAST_EXPECT(res); @@ -1261,7 +1273,11 @@ protected: unsquelched++; }); BEAST_EXPECT( - unsquelched == MAX_PEERS - reduce_relay::MAX_SELECTED_PEERS); + unsquelched == + MAX_PEERS - + env_.app() + .config() + .VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS); BEAST_EXPECT(checkCounting(network_.validator(0), true)); }); } @@ -1282,7 +1298,11 @@ protected: }); auto peers = network_.overlay().getPeers(network_.validator(0)); BEAST_EXPECT( - unsquelched == MAX_PEERS - reduce_relay::MAX_SELECTED_PEERS); + unsquelched == + MAX_PEERS - + env_.app() + .config() + .VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS); BEAST_EXPECT(checkCounting(network_.validator(0), true)); }); } @@ -1314,42 +1334,164 @@ protected: void testConfig(bool log) { - doTest("Config Test", log, [&](bool log) { + doTest("Test Config - squelch enabled (legacy)", log, [&](bool log) { Config c; std::string toLoad(R"rippleConfig( [reduce_relay] vp_enable=1 -vp_squelch=1 )rippleConfig"); c.loadFromString(toLoad); - BEAST_EXPECT(c.VP_REDUCE_RELAY_ENABLE == true); - BEAST_EXPECT(c.VP_REDUCE_RELAY_SQUELCH == true); + BEAST_EXPECT(c.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE == true); + }); + + doTest("Test Config - squelch disabled (legacy)", log, [&](bool log) { + Config c; + + std::string toLoad(R"rippleConfig( +[reduce_relay] +vp_enable=0 +)rippleConfig"); + + c.loadFromString(toLoad); + BEAST_EXPECT(c.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE == false); Config c1; - toLoad = (R"rippleConfig( + toLoad = R"rippleConfig( [reduce_relay] -vp_enable=0 -vp_squelch=0 -)rippleConfig"); +)rippleConfig"; c1.loadFromString(toLoad); - BEAST_EXPECT(c1.VP_REDUCE_RELAY_ENABLE == false); - BEAST_EXPECT(c1.VP_REDUCE_RELAY_SQUELCH == false); + BEAST_EXPECT(c1.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE == false); + }); + + doTest("Test Config - squelch enabled", log, [&](bool log) { + Config c; + + std::string toLoad(R"rippleConfig( +[reduce_relay] +vp_base_squelch_enable=1 +)rippleConfig"); + + c.loadFromString(toLoad); + BEAST_EXPECT(c.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE == true); + }); + + doTest("Test Config - squelch disabled", log, [&](bool log) { + Config c; + + std::string toLoad(R"rippleConfig( +[reduce_relay] +vp_base_squelch_enable=0 +)rippleConfig"); + + c.loadFromString(toLoad); + BEAST_EXPECT(c.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE == false); + }); + + doTest("Test Config - legacy and new", log, [&](bool log) { + Config c; + + std::string toLoad(R"rippleConfig( +[reduce_relay] +vp_base_squelch_enable=0 +vp_enable=0 +)rippleConfig"); + + std::string error; + auto const expectedError = + "Invalid reduce_relay" + " cannot specify both vp_base_squelch_enable and vp_enable " + "options. " + "vp_enable was deprecated and replaced by " + "vp_base_squelch_enable"; + + try + { + c.loadFromString(toLoad); + } + catch (std::runtime_error& e) + { + error = e.what(); + } + + BEAST_EXPECT(error == expectedError); + }); + + doTest("Test Config - max selected peers", log, [&](bool log) { + Config c; + + std::string toLoad(R"rippleConfig( +[reduce_relay] +)rippleConfig"); + + c.loadFromString(toLoad); + BEAST_EXPECT(c.VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS == 5); + + Config c1; + + toLoad = R"rippleConfig( +[reduce_relay] +vp_base_squelch_max_selected_peers=6 +)rippleConfig"; + + c1.loadFromString(toLoad); + BEAST_EXPECT(c1.VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS == 6); Config c2; toLoad = R"rippleConfig( [reduce_relay] -vp_enabled=1 -vp_squelched=1 +vp_base_squelch_max_selected_peers=2 )rippleConfig"; - c2.loadFromString(toLoad); - BEAST_EXPECT(c2.VP_REDUCE_RELAY_ENABLE == false); - BEAST_EXPECT(c2.VP_REDUCE_RELAY_SQUELCH == false); + std::string error; + auto const expectedError = + "Invalid reduce_relay" + " vp_base_squelch_max_selected_peers must be " + "greater than or equal to 3"; + try + { + c2.loadFromString(toLoad); + } + catch (std::runtime_error& e) + { + error = e.what(); + } + + BEAST_EXPECT(error == expectedError); + }); + } + + void + testBaseSquelchReady(bool log) + { + doTest("BaseSquelchReady", log, [&](bool log) { + ManualClock::reset(); + auto createSlots = [&](bool baseSquelchEnabled) + -> reduce_relay::Slots { + env_.app().config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = + baseSquelchEnabled; + return reduce_relay::Slots( + env_.app().logs(), network_.overlay(), env_.app().config()); + }; + // base squelching must not be ready if squelching is disabled + BEAST_EXPECT(!createSlots(false).baseSquelchReady()); + + // base squelch must not be ready as not enough time passed from + // bootup + BEAST_EXPECT(!createSlots(true).baseSquelchReady()); + + ManualClock::advance(reduce_relay::WAIT_ON_BOOTUP + minutes{1}); + + // base squelch enabled and bootup time passed + BEAST_EXPECT(createSlots(true).baseSquelchReady()); + + // even if time passed, base squelching must not be ready if turned + // off in the config + BEAST_EXPECT(!createSlots(false).baseSquelchReady()); }); } @@ -1425,7 +1567,7 @@ vp_squelched=1 auto run = [&](int npeers) { handler.maxDuration_ = 0; reduce_relay::Slots slots( - env_.app().logs(), handler); + env_.app().logs(), handler, env_.app().config()); // 1st message from a new peer switches the slot // to counting state and resets the counts of all peers + // MAX_MESSAGE_THRESHOLD + 1 messages to reach the threshold @@ -1503,14 +1645,12 @@ vp_squelched=1 std::stringstream str; str << "[reduce_relay]\n" << "vp_enable=" << enable << "\n" - << "vp_squelch=" << enable << "\n" << "[compression]\n" << "1\n"; c.loadFromString(str.str()); - env_.app().config().VP_REDUCE_RELAY_ENABLE = - c.VP_REDUCE_RELAY_ENABLE; - env_.app().config().VP_REDUCE_RELAY_SQUELCH = - c.VP_REDUCE_RELAY_SQUELCH; + env_.app().config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = + c.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE; + env_.app().config().COMPRESSION = c.COMPRESSION; }; auto handshake = [&](int outboundEnable, int inboundEnable) { @@ -1523,7 +1663,7 @@ vp_squelched=1 env_.app().config().COMPRESSION, false, env_.app().config().TX_REDUCE_RELAY_ENABLE, - env_.app().config().VP_REDUCE_RELAY_ENABLE); + env_.app().config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE); http_request_type http_request; http_request.version(request.version()); http_request.base() = request.base(); @@ -1563,7 +1703,13 @@ vp_squelched=1 Network network_; public: - reduce_relay_test() : env_(*this), network_(env_.app()) + reduce_relay_test() + : env_(*this, jtx::envconfig([](std::unique_ptr cfg) { + cfg->VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = true; + cfg->VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS = 6; + return cfg; + })) + , network_(env_.app()) { } @@ -1582,6 +1728,7 @@ public: testInternalHashRouter(log); testRandomSquelch(log); testHandshake(log); + testBaseSquelchReady(log); } }; diff --git a/src/test/rpc/AccountLines_test.cpp b/src/test/rpc/AccountLines_test.cpp index 6e6f0def19..42acea4111 100644 --- a/src/test/rpc/AccountLines_test.cpp +++ b/src/test/rpc/AccountLines_test.cpp @@ -580,7 +580,6 @@ public: STAmount const& amount) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -596,7 +595,6 @@ public: PublicKey const& pk) { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index b723095aeb..7a48db73bd 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -698,7 +698,6 @@ public: // gw creates an escrow that we can look for in the ledger. Json::Value jvEscrow; jvEscrow[jss::TransactionType] = jss::EscrowCreate; - jvEscrow[jss::Flags] = tfUniversal; jvEscrow[jss::Account] = gw.human(); jvEscrow[jss::Destination] = gw.human(); jvEscrow[jss::Amount] = XRP(100).value().getJson(JsonOptions::none); @@ -912,7 +911,6 @@ public: // for. Json::Value jvPayChan; jvPayChan[jss::TransactionType] = jss::PaymentChannelCreate; - jvPayChan[jss::Flags] = tfUniversal; jvPayChan[jss::Account] = gw.human(); jvPayChan[jss::Destination] = alice.human(); jvPayChan[jss::Amount] = @@ -938,7 +936,6 @@ public: // gw creates a DID that we can look for in the ledger. Json::Value jvDID; jvDID[jss::TransactionType] = jss::DIDSet; - jvDID[jss::Flags] = tfUniversal; jvDID[jss::Account] = gw.human(); jvDID[sfURI.jsonName] = strHex(std::string{"uri"}); env(jvDID); diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index 9af3fdcb61..6e25c26e58 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -458,7 +458,6 @@ class AccountTx_test : public beast::unit_test::suite STAmount const& amount) { Json::Value escro; escro[jss::TransactionType] = jss::EscrowCreate; - escro[jss::Flags] = tfUniversal; escro[jss::Account] = account.human(); escro[jss::Destination] = to.human(); escro[jss::Amount] = amount.getJson(JsonOptions::none); @@ -487,7 +486,6 @@ class AccountTx_test : public beast::unit_test::suite { Json::Value escrowFinish; escrowFinish[jss::TransactionType] = jss::EscrowFinish; - escrowFinish[jss::Flags] = tfUniversal; escrowFinish[jss::Account] = alice.human(); escrowFinish[sfOwner.jsonName] = alice.human(); escrowFinish[sfOfferSequence.jsonName] = escrowFinishSeq; @@ -496,7 +494,6 @@ class AccountTx_test : public beast::unit_test::suite { Json::Value escrowCancel; escrowCancel[jss::TransactionType] = jss::EscrowCancel; - escrowCancel[jss::Flags] = tfUniversal; escrowCancel[jss::Account] = alice.human(); escrowCancel[sfOwner.jsonName] = alice.human(); escrowCancel[sfOfferSequence.jsonName] = escrowCancelSeq; @@ -510,7 +507,6 @@ class AccountTx_test : public beast::unit_test::suite std::uint32_t payChanSeq{env.seq(alice)}; Json::Value payChanCreate; payChanCreate[jss::TransactionType] = jss::PaymentChannelCreate; - payChanCreate[jss::Flags] = tfUniversal; payChanCreate[jss::Account] = alice.human(); payChanCreate[jss::Destination] = gw.human(); payChanCreate[jss::Amount] = @@ -527,7 +523,6 @@ class AccountTx_test : public beast::unit_test::suite { Json::Value payChanFund; payChanFund[jss::TransactionType] = jss::PaymentChannelFund; - payChanFund[jss::Flags] = tfUniversal; payChanFund[jss::Account] = alice.human(); payChanFund[sfChannel.jsonName] = payChanIndex; payChanFund[jss::Amount] = diff --git a/src/test/rpc/JSONRPC_test.cpp b/src/test/rpc/JSONRPC_test.cpp index cd26758c1f..22c7dfd1dc 100644 --- a/src/test/rpc/JSONRPC_test.cpp +++ b/src/test/rpc/JSONRPC_test.cpp @@ -2132,6 +2132,127 @@ public: result[jss::result][jss::request][jss::command] == "bad_command"); } + void + testAutoFillFails() + { + testcase("autofill fails"); + using namespace test::jtx; + + // test batch raw transactions max size + { + Env env(*this); + auto ledger = env.current(); + auto const& feeTrack = env.app().getFeeTrack(); + Json::Value req; + Account const alice("alice"); + Account const bob("bob"); + env.fund(XRP(100000), alice); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + batch::inner(pay(alice, bob, XRP(3)), seq + 3), + batch::inner(pay(alice, bob, XRP(4)), seq + 4), + batch::inner(pay(alice, bob, XRP(5)), seq + 5), + batch::inner(pay(alice, bob, XRP(6)), seq + 6), + batch::inner(pay(alice, bob, XRP(7)), seq + 7), + batch::inner(pay(alice, bob, XRP(8)), seq + 8), + batch::inner(pay(alice, bob, XRP(9)), seq + 9)); + + jt.jv.removeMember(jss::Fee); + jt.jv.removeMember(jss::TxnSignature); + req[jss::tx_json] = jt.jv; + Json::Value result = checkFee( + req, + Role::ADMIN, + true, + env.app().config(), + feeTrack, + env.app().getTxQ(), + env.app()); + BEAST_EXPECT(result.size() == 0); + BEAST_EXPECT( + req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == + env.current()->fees().base.jsonClipped()); + } + + // test signers max size + { + Env env(*this); + auto ledger = env.current(); + auto const& feeTrack = env.app().getFeeTrack(); + Json::Value req; + Account const alice("alice"); + Account const bob("bob"); + env.fund(XRP(100000), alice, bob); + env.close(); + + auto jt = env.jtnofill( + noop(alice), + msig( + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice)); + + req[jss::tx_json] = jt.jv; + Json::Value result = checkFee( + req, + Role::ADMIN, + true, + env.app().config(), + feeTrack, + env.app().getTxQ(), + env.app()); + BEAST_EXPECT(result.size() == 0); + BEAST_EXPECT( + req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == + env.current()->fees().base.jsonClipped()); + } + } + void testAutoFillFees() { @@ -2785,6 +2906,7 @@ public: run() override { testBadRpcCommand(); + testAutoFillFails(); testAutoFillFees(); testAutoFillEscalatedFees(); testAutoFillNetworkID(); diff --git a/src/test/rpc/LedgerData_test.cpp b/src/test/rpc/LedgerData_test.cpp index b56cb241dd..c2b22efc00 100644 --- a/src/test/rpc/LedgerData_test.cpp +++ b/src/test/rpc/LedgerData_test.cpp @@ -369,7 +369,6 @@ public: { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = Account{"bob5"}.human(); jv[jss::Destination] = Account{"bob6"}.human(); jv[jss::Amount] = XRP(50).value().getJson(JsonOptions::none); @@ -383,7 +382,6 @@ public: { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = Account{"bob6"}.human(); jv[jss::Destination] = Account{"bob7"}.human(); jv[jss::Amount] = XRP(100).value().getJson(JsonOptions::none); diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index cb6f6d45e2..83232f79c8 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -1259,7 +1259,6 @@ class LedgerEntry_test : public beast::unit_test::suite NetClock::time_point const& cancelAfter) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index f27f0c2915..a4360ccc8b 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -465,6 +465,36 @@ class Simulate_test : public beast::unit_test::suite } } + void + testInvalidTransactionType() + { + testcase("Invalid transaction type"); + + using namespace jtx; + + Env env(*this); + + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(1000000), alice, bob); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(10)), seq + 1)); + + jt.jv.removeMember(jss::TxnSignature); + Json::Value params; + params[jss::tx_json] = jt.jv; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT(resp[jss::result][jss::error] == "notImpl"); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == "Not implemented."); + } + void testSuccessfulTransaction() { @@ -1081,6 +1111,7 @@ public: { testParamErrors(); testFeeError(); + testInvalidTransactionType(); testSuccessfulTransaction(); testTransactionNonTecFailure(); testTransactionTecFailure(); diff --git a/src/xrpld/app/ledger/detail/BuildLedger.cpp b/src/xrpld/app/ledger/detail/BuildLedger.cpp index 954507a006..4305426753 100644 --- a/src/xrpld/app/ledger/detail/BuildLedger.cpp +++ b/src/xrpld/app/ledger/detail/BuildLedger.cpp @@ -208,11 +208,17 @@ buildLedger( applyTransactions(app, built, txns, failedTxns, accum, j); if (!txns.empty() || !failedTxns.empty()) - JLOG(j.debug()) << "Applied " << applied << " transactions; " - << failedTxns.size() << " failed and " - << txns.size() << " will be retried."; + JLOG(j.debug()) + << "Applied " << applied << " transactions; " + << failedTxns.size() << " failed and " << txns.size() + << " will be retried. " + << "Total transactions in ledger (including Inner Batch): " + << accum.txCount(); else - JLOG(j.debug()) << "Applied " << applied << " transactions."; + JLOG(j.debug()) + << "Applied " << applied << " transactions. " + << "Total transactions in ledger (including Inner Batch): " + << accum.txCount(); }); } diff --git a/src/xrpld/app/ledger/detail/OpenLedger.cpp b/src/xrpld/app/ledger/detail/OpenLedger.cpp index 86a3b4b840..2c98caaa6d 100644 --- a/src/xrpld/app/ledger/detail/OpenLedger.cpp +++ b/src/xrpld/app/ledger/detail/OpenLedger.cpp @@ -26,6 +26,8 @@ #include #include +#include + #include namespace ripple { @@ -120,6 +122,18 @@ OpenLedger::accept( { auto const& tx = txpair.first; auto const txId = tx->getTransactionID(); + + // skip batch txns + // LCOV_EXCL_START + if (tx->isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch)) + { + XRPL_ASSERT( + txpair.second && txpair.second->isFieldPresent(sfParentBatchID), + "Inner Batch transaction missing sfParentBatchID"); + continue; + } + // LCOV_EXCL_STOP + if (auto const toSkip = app.getHashRouter().shouldRelay(txId)) { JLOG(j_.debug()) << "Relaying recovered tx " << txId; diff --git a/src/xrpld/app/misc/CredentialHelpers.cpp b/src/xrpld/app/misc/CredentialHelpers.cpp index 03ad1f9c80..81355f1792 100644 --- a/src/xrpld/app/misc/CredentialHelpers.cpp +++ b/src/xrpld/app/misc/CredentialHelpers.cpp @@ -336,9 +336,7 @@ verifyValidDomain( credentials.push_back(keyletCredential.key); } - // Result intentionally ignored. - [[maybe_unused]] bool _ = credentials::removeExpired(view, credentials, j); - + bool const foundExpired = credentials::removeExpired(view, credentials, j); for (auto const& h : credentials) { auto sleCredential = view.read(keylet::credential(h)); @@ -349,7 +347,7 @@ verifyValidDomain( return tesSUCCESS; } - return tecNO_PERMISSION; + return foundExpired ? tecEXPIRED : tecNO_PERMISSION; } TER diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index d87dea3c52..c8197b2219 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -64,6 +64,7 @@ #include #include #include +#include #include #include #include @@ -1190,6 +1191,15 @@ NetworkOPsImp::submitTransaction(std::shared_ptr const& iTrans) return; } + // Enforce Network bar for batch txn + if (iTrans->isFlag(tfInnerBatchTxn) && + m_ledgerMaster.getValidatedRules().enabled(featureBatch)) + { + JLOG(m_journal.error()) + << "Submitted transaction invalid: tfInnerBatchTxn flag present."; + return; + } + // this is an asynchronous interface auto const trans = sterilize(*iTrans); @@ -1249,15 +1259,25 @@ NetworkOPsImp::preProcessTransaction(std::shared_ptr& transaction) return false; } + auto const view = m_ledgerMaster.getCurrentLedger(); + + // This function is called by several different parts of the codebase + // under no circumstances will we ever accept an inner txn within a batch + // txn from the network. + auto const sttx = *transaction->getSTransaction(); + if (sttx.isFlag(tfInnerBatchTxn) && view->rules().enabled(featureBatch)) + { + transaction->setStatus(INVALID); + transaction->setResult(temINVALID_FLAG); + app_.getHashRouter().setFlags(transaction->getID(), SF_BAD); + return false; + } + // NOTE eahennis - I think this check is redundant, // but I'm not 100% sure yet. // If so, only cost is looking up HashRouter flags. - auto const view = m_ledgerMaster.getCurrentLedger(); - auto const [validity, reason] = checkValidity( - app_.getHashRouter(), - *transaction->getSTransaction(), - view->rules(), - app_.config()); + auto const [validity, reason] = + checkValidity(app_.getHashRouter(), sttx, view->rules(), app_.config()); XRPL_ASSERT( validity == Validity::Valid, "ripple::NetworkOPsImp::processTransaction : valid validity"); @@ -1659,13 +1679,17 @@ NetworkOPsImp::apply(std::unique_lock& batchLock) { auto const toSkip = app_.getHashRouter().shouldRelay(e.transaction->getID()); - - if (toSkip) + if (auto const sttx = *(e.transaction->getSTransaction()); + toSkip && + // Skip relaying if it's an inner batch txn and batch + // feature is enabled + !(sttx.isFlag(tfInnerBatchTxn) && + newOL->rules().enabled(featureBatch))) { protocol::TMTransaction tx; Serializer s; - e.transaction->getSTransaction()->add(s); + sttx.add(s); tx.set_rawtransaction(s.data(), s.size()); tx.set_status(protocol::tsCURRENT); tx.set_receivetimestamp( @@ -1677,7 +1701,7 @@ NetworkOPsImp::apply(std::unique_lock& batchLock) } } - if (validatedLedgerIndex) + if (!isTemMalformed(e.result) && validatedLedgerIndex) { auto [fee, accountSeq, availableSeq] = app_.getTxQ().getTxRequiredFeeAndSeq( @@ -3020,6 +3044,11 @@ NetworkOPsImp::pubProposedTransaction( std::shared_ptr const& transaction, TER result) { + // never publish an inner txn inside a batch txn + if (transaction->isFlag(tfInnerBatchTxn) && + ledger->rules().enabled(featureBatch)) + return; + MultiApiJson jvObj = transJson(transaction, result, false, ledger, std::nullopt); diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index adf96d0e14..6924dae6c8 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -737,6 +737,13 @@ TxQ::apply( STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; + // See if the transaction is valid, properly formed, + // etc. before doing potentially expensive queue + // replace and multi-transaction operations. + auto const pfresult = preflight(app, view.rules(), *tx, flags, j); + if (pfresult.ter != tesSUCCESS) + return {pfresult.ter, false}; + // See if the transaction paid a high enough fee that it can go straight // into the ledger. if (auto directApplied = tryDirectApply(app, view, tx, flags, j)) @@ -749,13 +756,6 @@ TxQ::apply( // o The transaction paid a high enough fee that fee averaging will apply. // o The transaction will be queued. - // See if the transaction is valid, properly formed, - // etc. before doing potentially expensive queue - // replace and multi-transaction operations. - auto const pfresult = preflight(app, view.rules(), *tx, flags, j); - if (pfresult.ter != tesSUCCESS) - return {pfresult.ter, false}; - // If the account is not currently in the ledger, don't queue its tx. auto const account = (*tx)[sfAccount]; Keylet const accountKey{keylet::account(account)}; diff --git a/src/xrpld/app/tx/applySteps.h b/src/xrpld/app/tx/applySteps.h index 2a5557ff4b..ec7180e263 100644 --- a/src/xrpld/app/tx/applySteps.h +++ b/src/xrpld/app/tx/applySteps.h @@ -165,6 +165,8 @@ struct PreflightResult public: /// From the input - the transaction STTx const& tx; + /// From the input - the batch identifier, if part of a batch + std::optional const parentBatchId; /// From the input - the rules Rules const rules; /// Consequences of the transaction @@ -183,6 +185,7 @@ public: Context const& ctx_, std::pair const& result) : tx(ctx_.tx) + , parentBatchId(ctx_.parentBatchId) , rules(ctx_.rules) , consequences(result.second) , flags(ctx_.flags) @@ -210,6 +213,8 @@ public: ReadView const& view; /// From the input - the transaction STTx const& tx; + /// From the input - the batch identifier, if part of a batch + std::optional const parentBatchId; /// From the input - the flags ApplyFlags const flags; /// From the input - the journal @@ -217,6 +222,7 @@ public: /// Intermediate transaction result TER const ter; + /// Success flag - whether the transaction is likely to /// claim a fee bool const likelyToClaimFee; @@ -226,6 +232,7 @@ public: PreclaimResult(Context const& ctx_, TER ter_) : view(ctx_.view) , tx(ctx_.tx) + , parentBatchId(ctx_.parentBatchId) , flags(ctx_.flags) , j(ctx_.j) , ter(ter_) @@ -255,6 +262,7 @@ public: @return A `PreflightResult` object containing, among other things, the `TER` code. */ +/** @{ */ PreflightResult preflight( Application& app, @@ -263,6 +271,16 @@ preflight( ApplyFlags flags, beast::Journal j); +PreflightResult +preflight( + Application& app, + Rules const& rules, + uint256 const& parentBatchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j); +/** @} */ + /** Gate a transaction based on static ledger information. The transaction is checked against all possible diff --git a/src/xrpld/app/tx/detail/ApplyContext.cpp b/src/xrpld/app/tx/detail/ApplyContext.cpp index 71fe246f15..79cbb7f40d 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.cpp +++ b/src/xrpld/app/tx/detail/ApplyContext.cpp @@ -29,6 +29,7 @@ namespace ripple { ApplyContext::ApplyContext( Application& app_, OpenView& base, + std::optional const& parentBatchId, STTx const& tx_, TER preclaimResult_, XRPAmount baseFee_, @@ -41,7 +42,11 @@ ApplyContext::ApplyContext( , journal(journal_) , base_(base) , flags_(flags) + , parentBatchId_(parentBatchId) { + XRPL_ASSERT( + parentBatchId.has_value() == ((flags_ & tapBATCH) == tapBATCH), + "Parent Batch ID should be set if batch apply flag is set"); view_.emplace(&base_, flags_); } @@ -54,7 +59,8 @@ ApplyContext::discard() std::optional ApplyContext::apply(TER ter) { - return view_->apply(base_, tx, ter, flags_ & tapDRY_RUN, journal); + return view_->apply( + base_, tx, ter, parentBatchId_, flags_ & tapDRY_RUN, journal); } std::size_t diff --git a/src/xrpld/app/tx/detail/ApplyContext.h b/src/xrpld/app/tx/detail/ApplyContext.h index 715d4ea471..720d0aeea3 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.h +++ b/src/xrpld/app/tx/detail/ApplyContext.h @@ -39,11 +39,34 @@ public: explicit ApplyContext( Application& app, OpenView& base, + std::optional const& parentBatchId, STTx const& tx, TER preclaimResult, XRPAmount baseFee, ApplyFlags flags, - beast::Journal = beast::Journal{beast::Journal::getNullSink()}); + beast::Journal journal = beast::Journal{beast::Journal::getNullSink()}); + + explicit ApplyContext( + Application& app, + OpenView& base, + STTx const& tx, + TER preclaimResult, + XRPAmount baseFee, + ApplyFlags flags, + beast::Journal journal = beast::Journal{beast::Journal::getNullSink()}) + : ApplyContext( + app, + base, + std::nullopt, + tx, + preclaimResult, + baseFee, + flags, + journal) + { + XRPL_ASSERT( + (flags & tapBATCH) == 0, "Batch apply flag should not be set"); + } Application& app; STTx const& tx; @@ -131,6 +154,9 @@ private: OpenView& base_; ApplyFlags flags_; std::optional view_; + + // The ID of the batch transaction we are executing under, if seated. + std::optional parentBatchId_; }; } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp new file mode 100644 index 0000000000..dcac889a5a --- /dev/null +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -0,0 +1,482 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace ripple { + +/** + * @brief Calculates the total base fee for a batch transaction. + * + * This function computes the required base fee for a batch transaction, + * including the base fee for the batch itself, the sum of base fees for + * all inner transactions, and additional fees for each batch signer. + * It performs overflow checks and validates the structure of the batch + * and its signers. + * + * @param view The ledger view providing fee and state information. + * @param tx The batch transaction to calculate the fee for. + * @return XRPAmount The total base fee required for the batch transaction. + * + * @throws std::overflow_error If any fee calculation would overflow the + * XRPAmount type. + * @throws std::length_error If the number of inner transactions or signers + * exceeds the allowed maximum. + * @throws std::invalid_argument If an inner transaction is itself a batch + * transaction. + */ +XRPAmount +Batch::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + XRPAmount const maxAmount{ + std::numeric_limits::max()}; + + // batchBase: view.fees().base for batch processing + default base fee + XRPAmount const baseFee = Transactor::calculateBaseFee(view, tx); + + // LCOV_EXCL_START + if (baseFee > maxAmount - view.fees().base) + throw std::overflow_error("XRPAmount overflow"); + // LCOV_EXCL_STOP + + XRPAmount const batchBase = view.fees().base + baseFee; + + // Calculate the Inner Txn Fees + XRPAmount txnFees{0}; + if (tx.isFieldPresent(sfRawTransactions)) + { + auto const& txns = tx.getFieldArray(sfRawTransactions); + + XRPL_ASSERT( + txns.size() <= maxBatchTxCount, + "Raw Transactions array exceeds max entries."); + + // LCOV_EXCL_START + if (txns.size() > maxBatchTxCount) + throw std::length_error( + "Raw Transactions array exceeds max entries"); + // LCOV_EXCL_STOP + + for (STObject txn : txns) + { + STTx const stx = STTx{std::move(txn)}; + + XRPL_ASSERT( + stx.getTxnType() != ttBATCH, "Inner Batch transaction found."); + + // LCOV_EXCL_START + if (stx.getTxnType() == ttBATCH) + throw std::invalid_argument("Inner Batch transaction found"); + // LCOV_EXCL_STOP + + auto const fee = ripple::calculateBaseFee(view, stx); + // LCOV_EXCL_START + if (txnFees > maxAmount - fee) + throw std::overflow_error("XRPAmount overflow"); + // LCOV_EXCL_STOP + txnFees += fee; + } + } + + // Calculate the Signers/BatchSigners Fees + std::int32_t signerCount = 0; + if (tx.isFieldPresent(sfBatchSigners)) + { + auto const& signers = tx.getFieldArray(sfBatchSigners); + XRPL_ASSERT( + signers.size() <= maxBatchTxCount, + "Batch Signers array exceeds max entries."); + + // LCOV_EXCL_START + if (signers.size() > maxBatchTxCount) + throw std::length_error("Batch Signers array exceeds max entries"); + // LCOV_EXCL_STOP + + for (STObject const& signer : signers) + { + if (signer.isFieldPresent(sfTxnSignature)) + signerCount += 1; + else if (signer.isFieldPresent(sfSigners)) + signerCount += signer.getFieldArray(sfSigners).size(); + } + } + + // LCOV_EXCL_START + if (signerCount > 0 && view.fees().base > maxAmount / signerCount) + throw std::overflow_error("XRPAmount overflow"); + // LCOV_EXCL_STOP + + XRPAmount signerFees = signerCount * view.fees().base; + + // LCOV_EXCL_START + if (signerFees > maxAmount - txnFees) + throw std::overflow_error("XRPAmount overflow"); + if (txnFees + signerFees > maxAmount - batchBase) + throw std::overflow_error("XRPAmount overflow"); + // LCOV_EXCL_STOP + + // 10 drops per batch signature + sum of inner tx fees + batchBase + return signerFees + txnFees + batchBase; +} + +/** + * @brief Performs preflight validation checks for a Batch transaction. + * + * This function validates the structure and contents of a Batch transaction + * before it is processed. It ensures that the Batch feature is enabled, + * checks for valid flags, validates the number and uniqueness of inner + * transactions, and enforces correct signing and fee requirements. + * + * The following validations are performed: + * - The Batch feature must be enabled in the current rules. + * - Only one of the mutually exclusive batch flags must be set. + * - The batch must contain at least two and no more than the maximum allowed + * inner transactions. + * - Each inner transaction must: + * - Be unique within the batch. + * - Not itself be a Batch transaction. + * - Have the tfInnerBatchTxn flag set. + * - Not include a TxnSignature or Signers field. + * - Have an empty SigningPubKey. + * - Pass its own preflight checks. + * - Have a fee of zero. + * - Have either Sequence or TicketSequence set, but not both or neither. + * - Not duplicate Sequence or TicketSequence values for the same account (for + * certain flags). + * - Validates that all required inner transaction accounts are present in the + * batch signers array, and that all batch signers are unique and not the outer + * account. + * - Verifies the batch signature if batch signers are present. + * + * @param ctx The PreflightContext containing the transaction and environment. + * @return NotTEC Returns tesSUCCESS if all checks pass, or an appropriate error + * code otherwise. + */ +NotTEC +Batch::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureBatch)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const parentBatchId = ctx.tx.getTransactionID(); + auto const outerAccount = ctx.tx.getAccountID(sfAccount); + auto const flags = ctx.tx.getFlags(); + + if (flags & tfBatchMask) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "invalid flags."; + return temINVALID_FLAG; + } + + if (std::popcount( + flags & + (tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent)) != 1) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "too many flags."; + return temINVALID_FLAG; + } + + auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions); + if (rawTxns.size() <= 1) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "txns array must have at least 2 entries."; + return temARRAY_EMPTY; + } + + if (rawTxns.size() > maxBatchTxCount) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "txns array exceeds 8 entries."; + return temARRAY_TOO_LARGE; + } + + // Validation Inner Batch Txns + std::unordered_set requiredSigners; + std::unordered_set uniqueHashes; + std::unordered_map> + accountSeqTicket; + for (STObject rb : rawTxns) + { + STTx const stx = STTx{std::move(rb)}; + auto const hash = stx.getTransactionID(); + if (!uniqueHashes.emplace(hash).second) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "duplicate Txn found. " + << "txID: " << hash; + return temREDUNDANT; + } + + if (stx.getFieldU16(sfTransactionType) == ttBATCH) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "batch cannot have an inner batch txn. " + << "txID: " << hash; + return temINVALID; + } + + if (!(stx.getFlags() & tfInnerBatchTxn)) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have the tfInnerBatchTxn flag. " + << "txID: " << hash; + return temINVALID_FLAG; + } + + if (stx.isFieldPresent(sfTxnSignature)) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn cannot include TxnSignature. " + << "txID: " << hash; + return temBAD_SIGNATURE; + } + + if (stx.isFieldPresent(sfSigners)) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn cannot include Signers. " + << "txID: " << hash; + return temBAD_SIGNER; + } + + if (!stx.getSigningPubKey().empty()) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn SigningPubKey must be empty. " + << "txID: " << hash; + return temBAD_REGKEY; + } + + auto const innerAccount = stx.getAccountID(sfAccount); + if (auto const preflightResult = ripple::preflight( + ctx.app, ctx.rules, parentBatchId, stx, tapBATCH, ctx.j); + preflightResult.ter != tesSUCCESS) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn preflight failed: " + << transHuman(preflightResult.ter) << " " + << "txID: " << hash; + return temINVALID_INNER_BATCH; + } + + // Check that the fee is zero + if (auto const fee = stx.getFieldAmount(sfFee); + !fee.native() || fee.xrp() != beast::zero) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have a fee of 0. " + << "txID: " << hash; + return temBAD_FEE; + } + + // Check that Sequence and TicketSequence are not both present + if (stx.isFieldPresent(sfTicketSequence) && + stx.getFieldU32(sfSequence) != 0) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have exactly one of Sequence and " + "TicketSequence. " + << "txID: " << hash; + return temSEQ_AND_TICKET; + } + + // Verify that either Sequence or TicketSequence is present + if (!stx.isFieldPresent(sfTicketSequence) && + stx.getFieldU32(sfSequence) == 0) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have either Sequence or " + "TicketSequence. " + << "txID: " << hash; + return temSEQ_AND_TICKET; + } + + // Duplicate sequence and ticket checks + if (flags & (tfAllOrNothing | tfUntilFailure)) + { + if (auto const seq = stx.getFieldU32(sfSequence); seq != 0) + { + if (!accountSeqTicket[innerAccount].insert(seq).second) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "duplicate sequence found: " + << "txID: " << hash; + return temREDUNDANT; + } + } + + if (stx.isFieldPresent(sfTicketSequence)) + { + if (auto const ticket = stx.getFieldU32(sfTicketSequence); + !accountSeqTicket[innerAccount].insert(ticket).second) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "duplicate ticket found: " + << "txID: " << hash; + return temREDUNDANT; + } + } + } + + // If the inner account is the same as the outer account, do not add the + // inner account to the required signers set. + if (innerAccount != outerAccount) + requiredSigners.insert(innerAccount); + } + + // LCOV_EXCL_START + if (auto const ret = preflight2(ctx); !isTesSuccess(ret)) + return ret; + // LCOV_EXCL_STOP + + // Validation Batch Signers + std::unordered_set batchSigners; + if (ctx.tx.isFieldPresent(sfBatchSigners)) + { + STArray const& signers = ctx.tx.getFieldArray(sfBatchSigners); + + // Check that the batch signers array is not too large. + if (signers.size() > maxBatchTxCount) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "signers array exceeds 8 entries."; + return temARRAY_TOO_LARGE; + } + + // Add batch signers to the set to ensure all signer accounts are + // unique. Meanwhile, remove signer accounts from the set of inner + // transaction accounts (`requiredSigners`). By the end of the loop, + // `requiredSigners` should be empty, indicating that all inner + // accounts are matched with signers. + for (auto const& signer : signers) + { + AccountID const signerAccount = signer.getAccountID(sfAccount); + if (signerAccount == outerAccount) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "signer cannot be the outer account: " << signerAccount; + return temBAD_SIGNER; + } + + if (!batchSigners.insert(signerAccount).second) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "duplicate signer found: " << signerAccount; + return temREDUNDANT; + } + + // Check that the batch signer is in the required signers set. + // Remove it if it does, as it can be crossed off the list. + if (requiredSigners.erase(signerAccount) == 0) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "no account signature for inner txn."; + return temBAD_SIGNER; + } + } + + // Check the batch signers signatures. + auto const sigResult = ctx.tx.checkBatchSign( + STTx::RequireFullyCanonicalSig::yes, ctx.rules); + + if (!sigResult) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "invalid batch txn signature: " << sigResult.error(); + return temBAD_SIGNATURE; + } + } + + if (!requiredSigners.empty()) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "invalid batch signers."; + return temBAD_SIGNER; + } + return tesSUCCESS; +} + +/** + * @brief Checks the validity of signatures for a batch transaction. + * + * This method first verifies the standard transaction signature by calling + * Transactor::checkSign. If the signature is not valid it returns the + * corresponding error code. + * + * Next, it verifies the batch-specific signature requirements by calling + * Transactor::checkBatchSign. If this check fails, it also returns the + * corresponding error code. + * + * If both checks succeed, the function returns tesSUCCESS. + * + * @param ctx The PreclaimContext containing transaction and environment data. + * @return NotTEC Returns tesSUCCESS if all signature checks pass, or an error + * code otherwise. + */ +NotTEC +Batch::checkSign(PreclaimContext const& ctx) +{ + if (auto ret = Transactor::checkSign(ctx); !isTesSuccess(ret)) + return ret; + + if (auto ret = Transactor::checkBatchSign(ctx); !isTesSuccess(ret)) + return ret; + + return tesSUCCESS; +} + +/** + * @brief Applies the outer batch transaction. + * + * This method is responsible for applying the outer batch transaction. + * The inner transactions within the batch are applied separately in the + * `applyBatchTransactions` method after the outer transaction is processed. + * + * @return TER Returns tesSUCCESS to indicate successful application of the + * outer batch transaction. + */ +TER +Batch::doApply() +{ + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/Batch.h b/src/xrpld/app/tx/detail/Batch.h new file mode 100644 index 0000000000..211bce0589 --- /dev/null +++ b/src/xrpld/app/tx/detail/Batch.h @@ -0,0 +1,55 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_BATCH_H_INCLUDED +#define RIPPLE_TX_BATCH_H_INCLUDED + +#include +#include + +#include +#include + +namespace ripple { + +class Batch : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit Batch(ApplyContext& ctx) : Transactor(ctx) + { + } + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static NotTEC + checkSign(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index 599819151a..0c16182ed8 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -214,7 +214,7 @@ SetAccount::checkPermission(ReadView const& view, STTx const& tx) // AccountSet transaction. If any delegated account is trying to // update the flag on behalf of another account, it is not // authorized. - if (uSetFlag != 0 || uClearFlag != 0 || uTxFlags != tfFullyCanonicalSig) + if (uSetFlag != 0 || uClearFlag != 0 || uTxFlags & tfUniversalMask) return tecNO_PERMISSION; if (tx.isFieldPresent(sfEmailHash) && diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index baba7d131e..cc82f7c3ca 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include namespace ripple { @@ -42,6 +43,13 @@ namespace ripple { NotTEC preflight0(PreflightContext const& ctx) { + if (isPseudoTx(ctx.tx) && ctx.tx.isFlag(tfInnerBatchTxn)) + { + JLOG(ctx.j.warn()) << "Pseudo transactions cannot contain the " + "tfInnerBatchTxn flag."; + return temINVALID_FLAG; + } + if (!isPseudoTx(ctx.tx) || ctx.tx.isFieldPresent(sfNetworkID)) { uint32_t nodeNID = ctx.app.config().NETWORK_ID; @@ -136,6 +144,14 @@ preflight1(PreflightContext const& ctx) ctx.tx.isFieldPresent(sfAccountTxnID)) return temINVALID; + if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch)) + return temINVALID_FLAG; + + XRPL_ASSERT( + ctx.tx.isFlag(tfInnerBatchTxn) == ctx.parentBatchId.has_value() || + !ctx.rules.enabled(featureBatch), + "Inner batch transaction must have a parent batch ID."); + return tesSUCCESS; } @@ -176,25 +192,13 @@ preflight2(PreflightContext const& ctx) if (sigValid.first == Validity::SigBad) { JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second; - return temINVALID; + return temINVALID; // LCOV_EXCL_LINE } return tesSUCCESS; } //------------------------------------------------------------------------------ -PreflightContext::PreflightContext( - Application& app_, - STTx const& tx_, - Rules const& rules_, - ApplyFlags flags_, - beast::Journal j_) - : app(app_), tx(tx_), rules(rules_), flags(flags_), j(j_) -{ -} - -//------------------------------------------------------------------------------ - Transactor::Transactor(ApplyContext& ctx) : ctx_(ctx), j_(ctx.journal), account_(ctx.tx.getAccountID(sfAccount)) { @@ -251,6 +255,16 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) return temBAD_FEE; auto const feePaid = ctx.tx[sfFee].xrp(); + + if (ctx.flags & tapBATCH) + { + if (feePaid == beast::zero) + return tesSUCCESS; + + JLOG(ctx.j.trace()) << "Batch: Fee must be zero."; + return temBAD_FEE; // LCOV_EXCL_LINE + } + if (!isLegalAmount(feePaid) || feePaid < beast::zero) return temBAD_FEE; @@ -557,51 +571,113 @@ Transactor::apply() NotTEC Transactor::checkSign(PreclaimContext const& ctx) { - if (ctx.flags & tapDRY_RUN) + // Ignore signature check on batch inner transactions + if (ctx.tx.isFlag(tfInnerBatchTxn) && + ctx.view.rules().enabled(featureBatch)) { - // This code must be different for `simulate` - // Since the public key may be empty even for single signing - if (ctx.tx.isFieldPresent(sfSigners)) - return checkMultiSign(ctx); - return checkSingleSign(ctx); + // Defensive Check: These values are also checked in Batch::preflight + if (ctx.tx.isFieldPresent(sfTxnSignature) || + !ctx.tx.getSigningPubKey().empty() || + ctx.tx.isFieldPresent(sfSigners)) + { + return temINVALID_FLAG; // LCOV_EXCL_LINE + } + return tesSUCCESS; } - // If the pk is empty, then we must be multi-signing. - if (ctx.tx.getSigningPubKey().empty()) - return checkMultiSign(ctx); - return checkSingleSign(ctx); -} + auto const idAccount = ctx.tx[~sfDelegate].value_or(ctx.tx[sfAccount]); -NotTEC -Transactor::checkSingleSign(PreclaimContext const& ctx) -{ - // Check that the value in the signing key slot is a public key. + // If the pk is empty and not simulate or simulate and signers, + // then we must be multi-signing. + if ((ctx.flags & tapDRY_RUN && ctx.tx.isFieldPresent(sfSigners)) || + (!(ctx.flags & tapDRY_RUN) && ctx.tx.getSigningPubKey().empty())) + { + STArray const& txSigners(ctx.tx.getFieldArray(sfSigners)); + return checkMultiSign(ctx.view, idAccount, txSigners, ctx.flags, ctx.j); + } + + // Check Single Sign auto const pkSigner = ctx.tx.getSigningPubKey(); + // This ternary is only needed to handle `simulate` + XRPL_ASSERT( + (ctx.flags & tapDRY_RUN) || !pkSigner.empty(), + "ripple::Transactor::checkSingleSign : non-empty signer or simulation"); + if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(pkSigner))) { JLOG(ctx.j.trace()) << "checkSingleSign: signing public key type is unknown"; return tefBAD_AUTH; // FIXME: should be better error! } - - // Look up the account. - auto const idAccount = ctx.tx.isFieldPresent(sfDelegate) - ? ctx.tx.getAccountID(sfDelegate) - : ctx.tx.getAccountID(sfAccount); + auto const idSigner = pkSigner.empty() + ? idAccount + : calcAccountID(PublicKey(makeSlice(pkSigner))); auto const sleAccount = ctx.view.read(keylet::account(idAccount)); if (!sleAccount) return terNO_ACCOUNT; - // This ternary is only needed to handle `simulate` - XRPL_ASSERT( - (ctx.flags & tapDRY_RUN) || !pkSigner.empty(), - "ripple::Transactor::checkSingleSign : non-empty signer or simulation"); - auto const idSigner = pkSigner.empty() - ? idAccount - : calcAccountID(PublicKey(makeSlice(pkSigner))); + return checkSingleSign( + idSigner, idAccount, sleAccount, ctx.view.rules(), ctx.j); +} + +NotTEC +Transactor::checkBatchSign(PreclaimContext const& ctx) +{ + NotTEC ret = tesSUCCESS; + STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)}; + for (auto const& signer : signers) + { + auto const idAccount = signer.getAccountID(sfAccount); + + Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey); + if (pkSigner.empty()) + { + STArray const& txSigners(signer.getFieldArray(sfSigners)); + if (ret = checkMultiSign( + ctx.view, idAccount, txSigners, ctx.flags, ctx.j); + !isTesSuccess(ret)) + return ret; + } + else + { + // LCOV_EXCL_START + if (!publicKeyType(makeSlice(pkSigner))) + return tefBAD_AUTH; + // LCOV_EXCL_STOP + + auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner))); + auto const sleAccount = ctx.view.read(keylet::account(idAccount)); + + // A batch can include transactions from an un-created account ONLY + // when the account master key is the signer + if (!sleAccount) + { + if (idAccount != idSigner) + return tefBAD_AUTH; + + return tesSUCCESS; + } + + if (ret = checkSingleSign( + idSigner, idAccount, sleAccount, ctx.view.rules(), ctx.j); + !isTesSuccess(ret)) + return ret; + } + } + return ret; +} + +NotTEC +Transactor::checkSingleSign( + AccountID const& idSigner, + AccountID const& idAccount, + std::shared_ptr sleAccount, + Rules const& rules, + beast::Journal j) +{ bool const isMasterDisabled = sleAccount->isFlag(lsfDisableMaster); - if (ctx.view.rules().enabled(fixMasterKeyAsRegularKey)) + if (rules.enabled(fixMasterKeyAsRegularKey)) { // Signed with regular key. if ((*sleAccount)[~sfRegularKey] == idSigner) @@ -638,16 +714,14 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) else if (sleAccount->isFieldPresent(sfRegularKey)) { // Signing key does not match master or regular key. - JLOG(ctx.j.trace()) - << "checkSingleSign: Not authorized to use account."; + JLOG(j.trace()) << "checkSingleSign: Not authorized to use account."; return tefBAD_AUTH; } else { // No regular key on account and signing key does not match master key. // FIXME: Why differentiate this case from tefBAD_AUTH? - JLOG(ctx.j.trace()) - << "checkSingleSign: Not authorized to use account."; + JLOG(j.trace()) << "checkSingleSign: Not authorized to use account."; return tefBAD_AUTH_MASTER; } @@ -655,18 +729,20 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) } NotTEC -Transactor::checkMultiSign(PreclaimContext const& ctx) +Transactor::checkMultiSign( + ReadView const& view, + AccountID const& id, + STArray const& txSigners, + ApplyFlags const& flags, + beast::Journal j) { - auto const id = ctx.tx.isFieldPresent(sfDelegate) - ? ctx.tx.getAccountID(sfDelegate) - : ctx.tx.getAccountID(sfAccount); // Get mTxnAccountID's SignerList and Quorum. std::shared_ptr sleAccountSigners = - ctx.view.read(keylet::signers(id)); + view.read(keylet::signers(id)); // If the signer list doesn't exist the account is not multi-signing. if (!sleAccountSigners) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid: Not a multi-signing account."; return tefNOT_MULTI_SIGNING; } @@ -681,12 +757,11 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) "ripple::Transactor::checkMultiSign : signer list ID is 0"); auto accountSigners = - SignerEntries::deserialize(*sleAccountSigners, ctx.j, "ledger"); + SignerEntries::deserialize(*sleAccountSigners, j, "ledger"); if (!accountSigners) return accountSigners.error(); // Get the array of transaction signers. - STArray const& txSigners(ctx.tx.getFieldArray(sfSigners)); // Walk the accountSigners performing a variety of checks and see if // the quorum is met. @@ -705,7 +780,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) { if (++iter == accountSigners->end()) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account."; return tefBAD_SIGNATURE; } @@ -713,7 +788,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) if (iter->account != txSignerAcctID) { // The SigningAccount is not in the SignerEntries. - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account."; return tefBAD_SIGNATURE; } @@ -723,16 +798,16 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // public key. auto const spk = txSigner.getFieldVL(sfSigningPubKey); - if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(spk))) + if (!(flags & tapDRY_RUN) && !publicKeyType(makeSlice(spk))) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "checkMultiSign: signing public key type is unknown"; return tefBAD_SIGNATURE; } // This ternary is only needed to handle `simulate` XRPL_ASSERT( - (ctx.flags & tapDRY_RUN) || !spk.empty(), + (flags & tapDRY_RUN) || !spk.empty(), "ripple::Transactor::checkMultiSign : non-empty signer or " "simulation"); AccountID const signingAcctIDFromPubKey = spk.empty() @@ -764,7 +839,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // In any of these cases we need to know whether the account is in // the ledger. Determine that now. - auto sleTxSignerRoot = ctx.view.read(keylet::account(txSignerAcctID)); + auto const sleTxSignerRoot = view.read(keylet::account(txSignerAcctID)); if (signingAcctIDFromPubKey == txSignerAcctID) { @@ -777,7 +852,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) if (signerAccountFlags & lsfDisableMaster) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Signer:Account lsfDisableMaster."; return tefMASTER_DISABLED; } @@ -789,21 +864,21 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // Public key must hash to the account's regular key. if (!sleTxSignerRoot) { - JLOG(ctx.j.trace()) << "applyTransaction: Non-phantom signer " - "lacks account root."; + JLOG(j.trace()) << "applyTransaction: Non-phantom signer " + "lacks account root."; return tefBAD_SIGNATURE; } if (!sleTxSignerRoot->isFieldPresent(sfRegularKey)) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Account lacks RegularKey."; return tefBAD_SIGNATURE; } if (signingAcctIDFromPubKey != sleTxSignerRoot->getAccountID(sfRegularKey)) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Account doesn't match RegularKey."; return tefBAD_SIGNATURE; } @@ -815,8 +890,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // Cannot perform transaction if quorum is not met. if (weightSum < sleAccountSigners->getFieldU32(sfSignerQuorum)) { - JLOG(ctx.j.trace()) - << "applyTransaction: Signers failed to meet quorum."; + JLOG(j.trace()) << "applyTransaction: Signers failed to meet quorum."; return tefBAD_QUORUM; } @@ -904,7 +978,11 @@ removeDeletedTrustLines( } } -/** Reset the context, discarding any changes made and adjust the fee */ +/** Reset the context, discarding any changes made and adjust the fee. + + @param fee The transaction fee to be charged. + @return A pair containing the transaction result and the actual fee charged. + */ std::pair Transactor::reset(XRPAmount fee) { @@ -912,9 +990,10 @@ Transactor::reset(XRPAmount fee) auto const txnAcct = view().peek(keylet::account(ctx_.tx.getAccountID(sfAccount))); + + // The account should never be missing from the ledger. But if it + // is missing then we can't very well charge it a fee, can we? if (!txnAcct) - // The account should never be missing from the ledger. But if it - // is missing then we can't very well charge it a fee, can we? return {tefINTERNAL, beast::zero}; auto const payerSle = ctx_.tx.isFieldPresent(sfDelegate) @@ -1024,7 +1103,6 @@ Transactor::operator()() { // If the tapFAIL_HARD flag is set, a tec result // must not do anything - ctx_.discard(); applied = false; } diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 4956f021df..42d4861a63 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -37,14 +37,38 @@ public: STTx const& tx; Rules const rules; ApplyFlags flags; + std::optional parentBatchId; beast::Journal const j; + PreflightContext( + Application& app_, + STTx const& tx_, + uint256 parentBatchId_, + Rules const& rules_, + ApplyFlags flags_, + beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) + : app(app_) + , tx(tx_) + , rules(rules_) + , flags(flags_) + , parentBatchId(parentBatchId_) + , j(j_) + { + XRPL_ASSERT( + (flags_ & tapBATCH) == tapBATCH, "Batch apply flag should be set"); + } + PreflightContext( Application& app_, STTx const& tx_, Rules const& rules_, ApplyFlags flags_, - beast::Journal j_); + beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) + : app(app_), tx(tx_), rules(rules_), flags(flags_), j(j_) + { + XRPL_ASSERT( + (flags_ & tapBATCH) == 0, "Batch apply flag should not be set"); + } PreflightContext& operator=(PreflightContext const&) = delete; @@ -57,8 +81,9 @@ public: Application& app; ReadView const& view; TER preflightResult; - STTx const& tx; ApplyFlags flags; + STTx const& tx; + std::optional const parentBatchId; beast::Journal const j; PreclaimContext( @@ -67,14 +92,39 @@ public: TER preflightResult_, STTx const& tx_, ApplyFlags flags_, + std::optional parentBatchId_, beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) : app(app_) , view(view_) , preflightResult(preflightResult_) - , tx(tx_) , flags(flags_) + , tx(tx_) + , parentBatchId(parentBatchId_) , j(j_) { + XRPL_ASSERT( + parentBatchId.has_value() == ((flags_ & tapBATCH) == tapBATCH), + "Parent Batch ID should be set if batch apply flag is set"); + } + + PreclaimContext( + Application& app_, + ReadView const& view_, + TER preflightResult_, + STTx const& tx_, + ApplyFlags flags_, + beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) + : PreclaimContext( + app_, + view_, + preflightResult_, + tx_, + flags_, + std::nullopt, + j_) + { + XRPL_ASSERT( + (flags_ & tapBATCH) == 0, "Batch apply flag should not be set"); } PreclaimContext& @@ -139,6 +189,9 @@ public: static NotTEC checkSign(PreclaimContext const& ctx); + static NotTEC + checkBatchSign(PreclaimContext const& ctx); + // Returns the fee in fee units, not scaled for load. static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); @@ -200,9 +253,19 @@ private: TER payFee(); static NotTEC - checkSingleSign(PreclaimContext const& ctx); + checkSingleSign( + AccountID const& idSigner, + AccountID const& idAccount, + std::shared_ptr sleAccount, + Rules const& rules, + beast::Journal j); static NotTEC - checkMultiSign(PreclaimContext const& ctx); + checkMultiSign( + ReadView const& view, + AccountID const& idAccount, + STArray const& txSigners, + ApplyFlags const& flags, + beast::Journal j); void trapTransaction(uint256) const; }; diff --git a/src/xrpld/app/tx/detail/apply.cpp b/src/xrpld/app/tx/detail/apply.cpp index 615fd6a92d..889a520032 100644 --- a/src/xrpld/app/tx/detail/apply.cpp +++ b/src/xrpld/app/tx/detail/apply.cpp @@ -23,6 +23,7 @@ #include #include +#include namespace ripple { @@ -43,6 +44,28 @@ checkValidity( { auto const id = tx.getTransactionID(); auto const flags = router.getFlags(id); + + // Ignore signature check on batch inner transactions + if (tx.isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch)) + { + // Defensive Check: These values are also checked in Batch::preflight + if (tx.isFieldPresent(sfTxnSignature) || + !tx.getSigningPubKey().empty() || tx.isFieldPresent(sfSigners)) + return { + Validity::SigBad, + "Malformed: Invalid inner batch transaction."}; + + std::string reason; + if (!passesLocalChecks(tx, reason)) + { + router.setFlags(id, SF_LOCALBAD); + return {Validity::SigGoodOnly, reason}; + } + + router.setFlags(id, SF_SIGGOOD); + return {Validity::Valid, ""}; + } + if (flags & SF_SIGBAD) // Signature is known bad return {Validity::SigBad, "Transaction has bad signature."}; @@ -106,6 +129,16 @@ forceValidity(HashRouter& router, uint256 const& txid, Validity validity) router.setFlags(txid, flags); } +template +ApplyResult +apply(Application& app, OpenView& view, PreflightChecks&& preflightChecks) +{ + STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; + NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; + + return doApply(preclaim(preflightChecks(), app, view), app, view); +} + ApplyResult apply( Application& app, @@ -114,12 +147,89 @@ apply( ApplyFlags flags, beast::Journal j) { - STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; - NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; + return apply(app, view, [&]() mutable { + return preflight(app, view.rules(), tx, flags, j); + }); +} - auto pfresult = preflight(app, view.rules(), tx, flags, j); - auto pcresult = preclaim(pfresult, app, view); - return doApply(pcresult, app, view); +ApplyResult +apply( + Application& app, + OpenView& view, + uint256 const& parentBatchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j) +{ + return apply(app, view, [&]() mutable { + return preflight(app, view.rules(), parentBatchId, tx, flags, j); + }); +} + +static bool +applyBatchTransactions( + Application& app, + OpenView& batchView, + STTx const& batchTxn, + beast::Journal j) +{ + XRPL_ASSERT( + batchTxn.getTxnType() == ttBATCH && + batchTxn.getFieldArray(sfRawTransactions).size() != 0, + "Batch transaction missing sfRawTransactions"); + + auto const parentBatchId = batchTxn.getTransactionID(); + auto const mode = batchTxn.getFlags(); + + auto applyOneTransaction = + [&app, &j, &parentBatchId, &batchView](STTx&& tx) { + OpenView perTxBatchView(batch_view, batchView); + + auto const ret = + apply(app, perTxBatchView, parentBatchId, tx, tapBATCH, j); + XRPL_ASSERT( + ret.applied == (isTesSuccess(ret.ter) || isTecClaim(ret.ter)), + "Inner transaction should not be applied"); + + JLOG(j.debug()) << "BatchTrace[" << parentBatchId + << "]: " << tx.getTransactionID() << " " + << (ret.applied ? "applied" : "failure") << ": " + << transToken(ret.ter); + + // If the transaction should be applied push its changes to the + // whole-batch view. + if (ret.applied && (isTesSuccess(ret.ter) || isTecClaim(ret.ter))) + perTxBatchView.apply(batchView); + + return ret; + }; + + int applied = 0; + + for (STObject rb : batchTxn.getFieldArray(sfRawTransactions)) + { + auto const result = applyOneTransaction(STTx{std::move(rb)}); + XRPL_ASSERT( + result.applied == + (isTesSuccess(result.ter) || isTecClaim(result.ter)), + "Outer Batch failure, inner transaction should not be applied"); + + if (result.applied) + ++applied; + + if (!isTesSuccess(result.ter)) + { + if (mode & tfAllOrNothing) + return false; + + if (mode & tfUntilFailure) + break; + } + else if (mode & tfOnlyOne) + break; + } + + return applied != 0; } ApplyTransactionResult @@ -141,10 +251,22 @@ applyTransaction( try { auto const result = apply(app, view, txn, flags, j); + if (result.applied) { JLOG(j.debug()) - << "Transaction applied: " << transHuman(result.ter); + << "Transaction applied: " << transToken(result.ter); + + // The batch transaction was just applied; now we need to apply + // its inner transactions as necessary. + if (isTesSuccess(result.ter) && txn.getTxnType() == ttBATCH) + { + OpenView wholeBatchView(batch_view, view); + + if (applyBatchTransactions(app, wholeBatchView, txn, j)) + wholeBatchView.apply(view); + } + return ApplyTransactionResult::Success; } diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 5e8c125e83..34259ebef0 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -104,7 +105,6 @@ with_txn_type(TxType txnType, F&& f) #undef TRANSACTION #pragma pop_macro("TRANSACTION") - default: throw UnknownTxnType(txnType); } @@ -227,6 +227,22 @@ invoke_preclaim(PreclaimContext const& ctx) } } +/** + * @brief Calculates the base fee for a given transaction. + * + * This function determines the base fee required for the specified transaction + * by invoking the appropriate fee calculation logic based on the transaction + * type. It uses a type-dispatch mechanism to select the correct calculation + * method. + * + * @param view The ledger view to use for fee calculation. + * @param tx The transaction for which the base fee is to be calculated. + * @return The calculated base fee as an XRPAmount. + * + * @throws std::exception If an error occurs during fee calculation, including + * but not limited to unknown transaction types or internal errors, the function + * logs an error and returns an XRPAmount of zero. + */ static XRPAmount invoke_calculateBaseFee(ReadView const& view, STTx const& tx) { @@ -320,7 +336,28 @@ preflight( } catch (std::exception const& e) { - JLOG(j.fatal()) << "apply: " << e.what(); + JLOG(j.fatal()) << "apply (preflight): " << e.what(); + return {pfctx, {tefEXCEPTION, TxConsequences{tx}}}; + } +} + +PreflightResult +preflight( + Application& app, + Rules const& rules, + uint256 const& parentBatchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j) +{ + PreflightContext const pfctx(app, tx, parentBatchId, rules, flags, j); + try + { + return {pfctx, invoke_preflight(pfctx)}; + } + catch (std::exception const& e) + { + JLOG(j.fatal()) << "apply (preflight): " << e.what(); return {pfctx, {tefEXCEPTION, TxConsequences{tx}}}; } } @@ -334,18 +371,31 @@ preclaim( std::optional ctx; if (preflightResult.rules != view.rules()) { - auto secondFlight = preflight( - app, - view.rules(), - preflightResult.tx, - preflightResult.flags, - preflightResult.j); + auto secondFlight = [&]() { + if (preflightResult.parentBatchId) + return preflight( + app, + view.rules(), + preflightResult.parentBatchId.value(), + preflightResult.tx, + preflightResult.flags, + preflightResult.j); + + return preflight( + app, + view.rules(), + preflightResult.tx, + preflightResult.flags, + preflightResult.j); + }(); + ctx.emplace( app, view, secondFlight.ter, secondFlight.tx, secondFlight.flags, + secondFlight.parentBatchId, secondFlight.j); } else @@ -356,8 +406,10 @@ preclaim( preflightResult.ter, preflightResult.tx, preflightResult.flags, + preflightResult.parentBatchId, preflightResult.j); } + try { if (ctx->preflightResult != tesSUCCESS) @@ -366,7 +418,7 @@ preclaim( } catch (std::exception const& e) { - JLOG(ctx->j.fatal()) << "apply: " << e.what(); + JLOG(ctx->j.fatal()) << "apply (preclaim): " << e.what(); return {*ctx, tefEXCEPTION}; } } @@ -399,6 +451,7 @@ doApply(PreclaimResult const& preclaimResult, Application& app, OpenView& view) ApplyContext ctx( app, view, + preclaimResult.parentBatchId, preclaimResult.tx, preclaimResult.ter, calculateBaseFee(view, preclaimResult.tx), diff --git a/src/xrpld/core/Config.h b/src/xrpld/core/Config.h index 4fdce92c8a..a58867958b 100644 --- a/src/xrpld/core/Config.h +++ b/src/xrpld/core/Config.h @@ -242,19 +242,18 @@ public: // size, but we allow admins to explicitly set it in the config. std::optional SWEEP_INTERVAL; - // Reduce-relay - these parameters are experimental. - // Enable reduce-relay features - // Validation/proposal reduce-relay feature - bool VP_REDUCE_RELAY_ENABLE = false; - // Send squelch message to peers. Generally this config should - // have the same value as VP_REDUCE_RELAY_ENABLE. It can be - // used for testing the feature's function without - // affecting the message relaying. To use it for testing, - // set it to false and set VP_REDUCE_RELAY_ENABLE to true. - // Squelch messages will not be sent to the peers in this case. - // Set log level to debug so that the feature function can be - // analyzed. - bool VP_REDUCE_RELAY_SQUELCH = false; + // Reduce-relay - Experimental parameters to control p2p routing algorithms + + // Enable base squelching of duplicate validation/proposal messages + bool VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = false; + + ///////////////////// !!TEMPORARY CODE BLOCK!! //////////////////////// + // Temporary squelching config for the peers selected as a source of // + // validator messages. The config must be removed once squelching is // + // made the default routing algorithm // + std::size_t VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS = 5; + ///////////////// END OF TEMPORARY CODE BLOCK ///////////////////// + // Transaction reduce-relay feature bool TX_REDUCE_RELAY_ENABLE = false; // If tx reduce-relay feature is disabled diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index b132987d08..1a07109b74 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -737,8 +737,44 @@ Config::loadFromString(std::string const& fileContents) if (exists(SECTION_REDUCE_RELAY)) { auto sec = section(SECTION_REDUCE_RELAY); - VP_REDUCE_RELAY_ENABLE = sec.value_or("vp_enable", false); - VP_REDUCE_RELAY_SQUELCH = sec.value_or("vp_squelch", false); + + ///////////////////// !!TEMPORARY CODE BLOCK!! //////////////////////// + // vp_enable config option is deprecated by vp_base_squelch_enable // + // This option is kept for backwards compatibility. When squelching // + // is the default algorithm, it must be replaced with: // + // VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = // + // sec.value_or("vp_base_squelch_enable", true); // + if (sec.exists("vp_base_squelch_enable") && sec.exists("vp_enable")) + Throw( + "Invalid " SECTION_REDUCE_RELAY + " cannot specify both vp_base_squelch_enable and vp_enable " + "options. " + "vp_enable was deprecated and replaced by " + "vp_base_squelch_enable"); + + if (sec.exists("vp_base_squelch_enable")) + VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = + sec.value_or("vp_base_squelch_enable", false); + else if (sec.exists("vp_enable")) + VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = + sec.value_or("vp_enable", false); + else + VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE = false; + ///////////////// !!END OF TEMPORARY CODE BLOCK!! ///////////////////// + + ///////////////////// !!TEMPORARY CODE BLOCK!! /////////////////////// + // Temporary squelching config for the peers selected as a source of // + // validator messages. The config must be removed once squelching is // + // made the default routing algorithm. // + VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS = + sec.value_or("vp_base_squelch_max_selected_peers", 5); + if (VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS < 3) + Throw( + "Invalid " SECTION_REDUCE_RELAY + " vp_base_squelch_max_selected_peers must be " + "greater than or equal to 3"); + ///////////////// !!END OF TEMPORARY CODE BLOCK!! ///////////////////// + TX_REDUCE_RELAY_ENABLE = sec.value_or("tx_enable", false); TX_REDUCE_RELAY_METRICS = sec.value_or("tx_metrics", false); TX_REDUCE_RELAY_MIN_PEERS = sec.value_or("tx_min_peers", 20); @@ -747,9 +783,9 @@ Config::loadFromString(std::string const& fileContents) TX_REDUCE_RELAY_MIN_PEERS < 10) Throw( "Invalid " SECTION_REDUCE_RELAY - ", tx_min_peers must be greater or equal to 10" - ", tx_relay_percentage must be greater or equal to 10 " - "and less or equal to 100"); + ", tx_min_peers must be greater than or equal to 10" + ", tx_relay_percentage must be greater than or equal to 10 " + "and less than or equal to 100"); } if (getSingleSection(secConfig, SECTION_MAX_TRANSACTIONS, strTemp, j_)) diff --git a/src/xrpld/ledger/ApplyView.h b/src/xrpld/ledger/ApplyView.h index 1abff33be0..1e4a5a112a 100644 --- a/src/xrpld/ledger/ApplyView.h +++ b/src/xrpld/ledger/ApplyView.h @@ -42,6 +42,9 @@ enum ApplyFlags : std::uint32_t { // Transaction came from a privileged source tapUNLIMITED = 0x400, + // Transaction is executing as part of a batch + tapBATCH = 0x800, + // Transaction shouldn't be applied // Signatures shouldn't be checked tapDRY_RUN = 0x1000 diff --git a/src/xrpld/ledger/ApplyViewImpl.h b/src/xrpld/ledger/ApplyViewImpl.h index 1c282565b1..d170cf71ff 100644 --- a/src/xrpld/ledger/ApplyViewImpl.h +++ b/src/xrpld/ledger/ApplyViewImpl.h @@ -58,6 +58,7 @@ public: OpenView& to, STTx const& tx, TER ter, + std::optional parentBatchId, bool isDryRun, beast::Journal j); diff --git a/src/xrpld/ledger/OpenView.h b/src/xrpld/ledger/OpenView.h index ecc618e185..a1fa195a69 100644 --- a/src/xrpld/ledger/OpenView.h +++ b/src/xrpld/ledger/OpenView.h @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -39,13 +40,21 @@ namespace ripple { Views constructed with this tag will have the rules of open ledgers applied during transaction processing. -*/ -struct open_ledger_t + */ +inline constexpr struct open_ledger_t { - explicit open_ledger_t() = default; -}; + explicit constexpr open_ledger_t() = default; +} open_ledger{}; -extern open_ledger_t const open_ledger; +/** Batch view construction tag. + + Views constructed with this tag are part of a stack of views + used during batch transaction applied. + */ +inline constexpr struct batch_view_t +{ + explicit constexpr batch_view_t() = default; +} batch_view{}; //------------------------------------------------------------------------------ @@ -97,6 +106,10 @@ private: ReadView const* base_; detail::RawStateTable items_; std::shared_ptr hold_; + + /// In batch mode, the number of transactions already executed. + std::size_t baseTxCount_ = 0; + bool open_ = true; public: @@ -142,7 +155,6 @@ public: The tx list starts empty and will contain all newly inserted tx. */ - /** @{ */ OpenView( open_ledger_t, ReadView const* base, @@ -156,7 +168,11 @@ public: : OpenView(open_ledger, &*base, rules, base) { } - /** @} */ + + OpenView(batch_view_t, OpenView& base) : OpenView(std::addressof(base)) + { + baseTxCount_ = base.txCount(); + } /** Construct a new last closed ledger. diff --git a/src/xrpld/ledger/detail/ApplyStateTable.cpp b/src/xrpld/ledger/detail/ApplyStateTable.cpp index c11a72d782..2a740093d9 100644 --- a/src/xrpld/ledger/detail/ApplyStateTable.cpp +++ b/src/xrpld/ledger/detail/ApplyStateTable.cpp @@ -116,6 +116,7 @@ ApplyStateTable::apply( STTx const& tx, TER ter, std::optional const& deliver, + std::optional const& parentBatchId, bool isDryRun, beast::Journal j) { @@ -126,9 +127,11 @@ ApplyStateTable::apply( std::optional metadata; if (!to.open() || isDryRun) { - TxMeta meta(tx.getTransactionID(), to.seq()); + TxMeta meta(tx.getTransactionID(), to.seq(), parentBatchId); + if (deliver) meta.setDeliveredAmount(*deliver); + Mods newMod; for (auto& item : items_) { diff --git a/src/xrpld/ledger/detail/ApplyStateTable.h b/src/xrpld/ledger/detail/ApplyStateTable.h index b1bac733fc..5a2e0bcf54 100644 --- a/src/xrpld/ledger/detail/ApplyStateTable.h +++ b/src/xrpld/ledger/detail/ApplyStateTable.h @@ -72,6 +72,7 @@ public: STTx const& tx, TER ter, std::optional const& deliver, + std::optional const& parentBatchId, bool isDryRun, beast::Journal j); diff --git a/src/xrpld/ledger/detail/ApplyViewImpl.cpp b/src/xrpld/ledger/detail/ApplyViewImpl.cpp index 74b71c8324..3fd9478b54 100644 --- a/src/xrpld/ledger/detail/ApplyViewImpl.cpp +++ b/src/xrpld/ledger/detail/ApplyViewImpl.cpp @@ -31,10 +31,11 @@ ApplyViewImpl::apply( OpenView& to, STTx const& tx, TER ter, + std::optional parentBatchId, bool isDryRun, beast::Journal j) { - return items_.apply(to, tx, ter, deliver_, isDryRun, j); + return items_.apply(to, tx, ter, deliver_, parentBatchId, isDryRun, j); } std::size_t diff --git a/src/xrpld/ledger/detail/OpenView.cpp b/src/xrpld/ledger/detail/OpenView.cpp index 5c62d8cef8..73e502a5e2 100644 --- a/src/xrpld/ledger/detail/OpenView.cpp +++ b/src/xrpld/ledger/detail/OpenView.cpp @@ -23,8 +23,6 @@ namespace ripple { -open_ledger_t const open_ledger{}; - class OpenView::txs_iter_impl : public txs_type::iter_base { private: @@ -124,7 +122,7 @@ OpenView::OpenView(ReadView const* base, std::shared_ptr hold) std::size_t OpenView::txCount() const { - return txs_.size(); + return baseTxCount_ + txs_.size(); } void @@ -269,7 +267,7 @@ OpenView::rawTxInsert( std::forward_as_tuple(key), std::forward_as_tuple(txn, metaData)); if (!result.second) - LogicError("rawTxInsert: duplicate TX id" + to_string(key)); + LogicError("rawTxInsert: duplicate TX id: " + to_string(key)); } } // namespace ripple diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index aa6e2dda8f..e9499a287a 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -2391,8 +2391,19 @@ enforceMPTokenAuthorization( auto const keylet = keylet::mptoken(mptIssuanceID, account); auto const sleToken = view.read(keylet); // NOTE: might be null auto const maybeDomainID = sleIssuance->at(~sfDomainID); - bool const authorizedByDomain = maybeDomainID.has_value() && - verifyValidDomain(view, account, *maybeDomainID, j) == tesSUCCESS; + bool expired = false; + bool const authorizedByDomain = [&]() -> bool { + // NOTE: defensive here, shuld be checked in preclaim + if (!maybeDomainID.has_value()) + return false; // LCOV_EXCL_LINE + + auto const ter = verifyValidDomain(view, account, *maybeDomainID, j); + if (isTesSuccess(ter)) + return true; + if (ter == tecEXPIRED) + expired = true; + return false; + }(); if (!authorizedByDomain && sleToken == nullptr) { @@ -2403,14 +2414,14 @@ enforceMPTokenAuthorization( // 3. Account has all expired credentials (deleted in verifyValidDomain) // // Either way, return tecNO_AUTH and there is nothing else to do - return tecNO_AUTH; + return expired ? tecEXPIRED : tecNO_AUTH; } else if (!authorizedByDomain && maybeDomainID.has_value()) { // Found an MPToken but the account is not authorized and we expect // it to have been authorized by the domain. This could be because the // credentials used to create the MPToken have expired or been deleted. - return tecNO_AUTH; + return expired ? tecEXPIRED : tecNO_AUTH; } else if (!authorizedByDomain) { diff --git a/src/xrpld/overlay/Slot.h b/src/xrpld/overlay/Slot.h index 6ae3c9a142..0956eb06f7 100644 --- a/src/xrpld/overlay/Slot.h +++ b/src/xrpld/overlay/Slot.h @@ -20,6 +20,7 @@ #ifndef RIPPLE_OVERLAY_SLOT_H_INCLUDED #define RIPPLE_OVERLAY_SLOT_H_INCLUDED +#include #include #include @@ -32,7 +33,6 @@ #include #include -#include #include #include #include @@ -109,16 +109,25 @@ private: using id_t = Peer::id_t; using time_point = typename clock_type::time_point; + // a callback to report ignored squelches + using ignored_squelch_callback = std::function; + /** Constructor * @param journal Journal for logging * @param handler Squelch/Unsquelch implementation + * @param maxSelectedPeers the maximum number of peers to be selected as + * validator message source */ - Slot(SquelchHandler const& handler, beast::Journal journal) + Slot( + SquelchHandler const& handler, + beast::Journal journal, + uint16_t maxSelectedPeers) : reachedThreshold_(0) , lastSelected_(clock_type::now()) , state_(SlotState::Counting) , handler_(handler) , journal_(journal) + , maxSelectedPeers_(maxSelectedPeers) { } @@ -129,7 +138,7 @@ private: * slot's state to Counting. If the number of messages for the peer is > * MIN_MESSAGE_THRESHOLD then add peer to considered peers pool. If the * number of considered peers who reached MAX_MESSAGE_THRESHOLD is - * MAX_SELECTED_PEERS then randomly select MAX_SELECTED_PEERS from + * maxSelectedPeers_ then randomly select maxSelectedPeers_ from * considered peers, and call squelch handler for each peer, which is not * selected and not already in Squelched state. Set the state for those * peers to Squelched and reset the count of all peers. Set slot's state to @@ -139,9 +148,14 @@ private: * @param id Peer id which received the message * @param type Message type (Validation and Propose Set only, * others are ignored, future use) + * @param callback A callback to report ignored squelches */ void - update(PublicKey const& validator, id_t id, protocol::MessageType type); + update( + PublicKey const& validator, + id_t id, + protocol::MessageType type, + ignored_squelch_callback callback); /** Handle peer deletion when a peer disconnects. * If the peer is in Selected state then @@ -223,17 +237,26 @@ private: time_point expire; // squelch expiration time time_point lastMessage; // time last message received }; + std::unordered_map peers_; // peer's data + // pool of peers considered as the source of messages // from validator - peers that reached MIN_MESSAGE_THRESHOLD std::unordered_set considered_; + // number of peers that reached MAX_MESSAGE_THRESHOLD std::uint16_t reachedThreshold_; + // last time peers were selected, used to age the slot typename clock_type::time_point lastSelected_; + SlotState state_; // slot's state SquelchHandler const& handler_; // squelch/unsquelch handler beast::Journal const journal_; // logging + + // the maximum number of peers that should be selected as a validator + // message source + uint16_t const maxSelectedPeers_; }; template @@ -264,7 +287,8 @@ void Slot::update( PublicKey const& validator, id_t id, - protocol::MessageType type) + protocol::MessageType type, + ignored_squelch_callback callback) { using namespace std::chrono; auto now = clock_type::now(); @@ -302,6 +326,10 @@ Slot::update( peer.lastMessage = now; + // report if we received a message from a squelched peer + if (peer.state == PeerState::Squelched) + callback(); + if (state_ != SlotState::Counting || peer.state == PeerState::Squelched) return; @@ -319,17 +347,17 @@ Slot::update( return; } - if (reachedThreshold_ == MAX_SELECTED_PEERS) + if (reachedThreshold_ == maxSelectedPeers_) { - // Randomly select MAX_SELECTED_PEERS peers from considered. + // Randomly select maxSelectedPeers_ peers from considered. // Exclude peers that have been idling > IDLED - // it's possible that deleteIdlePeer() has not been called yet. - // If number of remaining peers != MAX_SELECTED_PEERS + // If number of remaining peers != maxSelectedPeers_ // then reset the Counting state and let deleteIdlePeer() handle // idled peers. std::unordered_set selected; auto const consideredPoolSize = considered_.size(); - while (selected.size() != MAX_SELECTED_PEERS && considered_.size() != 0) + while (selected.size() != maxSelectedPeers_ && considered_.size() != 0) { auto i = considered_.size() == 1 ? 0 : rand_int(considered_.size() - 1); @@ -347,7 +375,7 @@ Slot::update( selected.insert(id); } - if (selected.size() != MAX_SELECTED_PEERS) + if (selected.size() != maxSelectedPeers_) { JLOG(journal_.trace()) << "update: selection failed " << Slice(validator) << " " << id; @@ -364,7 +392,7 @@ Slot::update( << *std::next(s, 1) << " " << *std::next(s, 2); XRPL_ASSERT( - peers_.size() >= MAX_SELECTED_PEERS, + peers_.size() >= maxSelectedPeers_, "ripple::reduce_relay::Slot::update : minimum peers"); // squelch peers which are not selected and @@ -382,7 +410,7 @@ Slot::update( str << k << " "; v.state = PeerState::Squelched; std::chrono::seconds duration = - getSquelchDuration(peers_.size() - MAX_SELECTED_PEERS); + getSquelchDuration(peers_.size() - maxSelectedPeers_); v.expire = now + duration; handler_.squelch(validator, k, duration.count()); } @@ -544,15 +572,41 @@ class Slots final public: /** - * @param app Applicaton reference + * @param logs reference to the logger * @param handler Squelch/unsquelch implementation + * @param config reference to the global config */ - Slots(Logs& logs, SquelchHandler const& handler) - : handler_(handler), logs_(logs), journal_(logs.journal("Slots")) + Slots(Logs& logs, SquelchHandler const& handler, Config const& config) + : handler_(handler) + , logs_(logs) + , journal_(logs.journal("Slots")) + , baseSquelchEnabled_(config.VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE) + , maxSelectedPeers_(config.VP_REDUCE_RELAY_SQUELCH_MAX_SELECTED_PEERS) { } ~Slots() = default; - /** Calls Slot::update of Slot associated with the validator. + + /** Check if base squelching feature is enabled and ready */ + bool + baseSquelchReady() + { + return baseSquelchEnabled_ && reduceRelayReady(); + } + + /** Check if reduce_relay::WAIT_ON_BOOTUP time passed since startup */ + bool + reduceRelayReady() + { + if (!reduceRelayReady_) + reduceRelayReady_ = + reduce_relay::epoch(clock_type::now()) > + reduce_relay::WAIT_ON_BOOTUP; + + return reduceRelayReady_; + } + + /** Calls Slot::update of Slot associated with the validator, with a noop + * callback. * @param key Message's hash * @param validator Validator's public key * @param id Peer's id which received the message @@ -563,7 +617,25 @@ public: uint256 const& key, PublicKey const& validator, id_t id, - protocol::MessageType type); + protocol::MessageType type) + { + updateSlotAndSquelch(key, validator, id, type, []() {}); + } + + /** Calls Slot::update of Slot associated with the validator. + * @param key Message's hash + * @param validator Validator's public key + * @param id Peer's id which received the message + * @param type Received protocol message type + * @param callback A callback to report ignored validations + */ + void + updateSlotAndSquelch( + uint256 const& key, + PublicKey const& validator, + id_t id, + protocol::MessageType type, + typename Slot::ignored_squelch_callback callback); /** Check if peers stopped relaying messages * and if slots stopped receiving messages from the validator. @@ -651,10 +723,16 @@ private: bool addPeerMessage(uint256 const& key, id_t id); + std::atomic_bool reduceRelayReady_{false}; + hash_map> slots_; SquelchHandler const& handler_; // squelch/unsquelch handler Logs& logs_; beast::Journal const journal_; + + bool const baseSquelchEnabled_; + uint16_t const maxSelectedPeers_; + // Maintain aged container of message/peers. This is required // to discard duplicate message from the same peer. A message // is aged after IDLED seconds. A message received IDLED seconds @@ -702,7 +780,8 @@ Slots::updateSlotAndSquelch( uint256 const& key, PublicKey const& validator, id_t id, - protocol::MessageType type) + protocol::MessageType type, + typename Slot::ignored_squelch_callback callback) { if (!addPeerMessage(key, id)) return; @@ -712,15 +791,17 @@ Slots::updateSlotAndSquelch( { JLOG(journal_.trace()) << "updateSlotAndSquelch: new slot " << Slice(validator); - auto it = slots_ - .emplace(std::make_pair( - validator, - Slot(handler_, logs_.journal("Slot")))) - .first; - it->second.update(validator, id, type); + auto it = + slots_ + .emplace(std::make_pair( + validator, + Slot( + handler_, logs_.journal("Slot"), maxSelectedPeers_))) + .first; + it->second.update(validator, id, type, callback); } else - it->second.update(validator, id, type); + it->second.update(validator, id, type, callback); } template diff --git a/src/xrpld/overlay/detail/ConnectAttempt.cpp b/src/xrpld/overlay/detail/ConnectAttempt.cpp index 30763b1357..84fbd36d32 100644 --- a/src/xrpld/overlay/detail/ConnectAttempt.cpp +++ b/src/xrpld/overlay/detail/ConnectAttempt.cpp @@ -209,7 +209,7 @@ ConnectAttempt::onHandshake(error_code ec) app_.config().COMPRESSION, app_.config().LEDGER_REPLAY, app_.config().TX_REDUCE_RELAY_ENABLE, - app_.config().VP_REDUCE_RELAY_ENABLE); + app_.config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE); buildHandshake( req_, diff --git a/src/xrpld/overlay/detail/Handshake.cpp b/src/xrpld/overlay/detail/Handshake.cpp index 657d28072f..e3617a1d98 100644 --- a/src/xrpld/overlay/detail/Handshake.cpp +++ b/src/xrpld/overlay/detail/Handshake.cpp @@ -414,7 +414,7 @@ makeResponse( app.config().COMPRESSION, app.config().LEDGER_REPLAY, app.config().TX_REDUCE_RELAY_ENABLE, - app.config().VP_REDUCE_RELAY_ENABLE)); + app.config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE)); buildHandshake(resp, sharedValue, networkID, public_ip, remote_ip, app); diff --git a/src/xrpld/overlay/detail/Handshake.h b/src/xrpld/overlay/detail/Handshake.h index 37f138b88b..1cd733ef56 100644 --- a/src/xrpld/overlay/detail/Handshake.h +++ b/src/xrpld/overlay/detail/Handshake.h @@ -139,7 +139,7 @@ makeResponse( // compression feature static constexpr char FEATURE_COMPR[] = "compr"; -// validation/proposal reduce-relay feature +// validation/proposal reduce-relay base squelch feature static constexpr char FEATURE_VPRR[] = "vprr"; // transaction reduce-relay feature static constexpr char FEATURE_TXRR[] = "txrr"; @@ -221,7 +221,7 @@ peerFeatureEnabled( @param txReduceRelayEnabled if true then transaction reduce-relay feature is enabled @param vpReduceRelayEnabled if true then validation/proposal reduce-relay - feature is enabled + base squelch feature is enabled @return X-Protocol-Ctl header value */ std::string @@ -241,8 +241,7 @@ makeFeaturesRequestHeader( @param txReduceRelayEnabled if true then transaction reduce-relay feature is enabled @param vpReduceRelayEnabled if true then validation/proposal reduce-relay - feature is enabled - @param vpReduceRelayEnabled if true then reduce-relay feature is enabled + base squelch feature is enabled @return X-Protocol-Ctl header value */ std::string diff --git a/src/xrpld/overlay/detail/OverlayImpl.cpp b/src/xrpld/overlay/detail/OverlayImpl.cpp index e1ccc2ee84..3cc5b2a024 100644 --- a/src/xrpld/overlay/detail/OverlayImpl.cpp +++ b/src/xrpld/overlay/detail/OverlayImpl.cpp @@ -142,7 +142,7 @@ OverlayImpl::OverlayImpl( , m_resolver(resolver) , next_id_(1) , timer_count_(0) - , slots_(app.logs(), *this) + , slots_(app.logs(), *this, app.config()) , m_stats( std::bind(&OverlayImpl::collect_metrics, this), collector, @@ -1390,8 +1390,7 @@ makeSquelchMessage( void OverlayImpl::unsquelch(PublicKey const& validator, Peer::id_t id) const { - if (auto peer = findPeerByShortID(id); - peer && app_.config().VP_REDUCE_RELAY_SQUELCH) + if (auto peer = findPeerByShortID(id); peer) { // optimize - multiple message with different // validator might be sent to the same peer @@ -1405,8 +1404,7 @@ OverlayImpl::squelch( Peer::id_t id, uint32_t squelchDuration) const { - if (auto peer = findPeerByShortID(id); - peer && app_.config().VP_REDUCE_RELAY_SQUELCH) + if (auto peer = findPeerByShortID(id); peer) { peer->send(makeSquelchMessage(validator, true, squelchDuration)); } @@ -1419,6 +1417,9 @@ OverlayImpl::updateSlotAndSquelch( std::set&& peers, protocol::MessageType type) { + if (!slots_.baseSquelchReady()) + return; + if (!strand_.running_in_this_thread()) return post( strand_, @@ -1427,7 +1428,9 @@ OverlayImpl::updateSlotAndSquelch( }); for (auto id : peers) - slots_.updateSlotAndSquelch(key, validator, id, type); + slots_.updateSlotAndSquelch(key, validator, id, type, [&]() { + reportInboundTraffic(TrafficCount::squelch_ignored, 0); + }); } void @@ -1437,12 +1440,17 @@ OverlayImpl::updateSlotAndSquelch( Peer::id_t peer, protocol::MessageType type) { + if (!slots_.baseSquelchReady()) + return; + if (!strand_.running_in_this_thread()) return post(strand_, [this, key, validator, peer, type]() { updateSlotAndSquelch(key, validator, peer, type); }); - slots_.updateSlotAndSquelch(key, validator, peer, type); + slots_.updateSlotAndSquelch(key, validator, peer, type, [&]() { + reportInboundTraffic(TrafficCount::squelch_ignored, 0); + }); } void diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index bca2cfd8c7..68894fb234 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include @@ -112,20 +113,21 @@ PeerImp::PeerImp( headers_, FEATURE_TXRR, app_.config().TX_REDUCE_RELAY_ENABLE)) - , vpReduceRelayEnabled_(app_.config().VP_REDUCE_RELAY_ENABLE) , ledgerReplayEnabled_(peerFeatureEnabled( headers_, FEATURE_LEDGER_REPLAY, app_.config().LEDGER_REPLAY)) , ledgerReplayMsgHandler_(app, app.getLedgerReplayer()) { - JLOG(journal_.info()) << "compression enabled " - << (compressionEnabled_ == Compressed::On) - << " vp reduce-relay enabled " - << vpReduceRelayEnabled_ - << " tx reduce-relay enabled " - << txReduceRelayEnabled_ << " on " << remote_address_ - << " " << id_; + JLOG(journal_.info()) + << "compression enabled " << (compressionEnabled_ == Compressed::On) + << " vp reduce-relay base squelch enabled " + << peerFeatureEnabled( + headers_, + FEATURE_VPRR, + app_.config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE) + << " tx reduce-relay enabled " << txReduceRelayEnabled_ << " on " + << remote_address_ << " " << id_; } PeerImp::~PeerImp() @@ -1282,6 +1284,18 @@ PeerImp::handleTransaction( auto stx = std::make_shared(sit); uint256 txID = stx->getTransactionID(); + // Charge strongly for attempting to relay a txn with tfInnerBatchTxn + // LCOV_EXCL_START + if (stx->isFlag(tfInnerBatchTxn) && + getCurrentTransactionRules()->enabled(featureBatch)) + { + JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing " + "tfInnerBatchTxn (handleTransaction)."; + fee_.update(Resource::feeModerateBurdenPeer, "inner batch txn"); + return; + } + // LCOV_EXCL_STOP + int flags; constexpr std::chrono::seconds tx_interval = 10s; @@ -1720,8 +1734,7 @@ PeerImp::onMessage(std::shared_ptr const& m) { // Count unique messages (Slots has it's own 'HashRouter'), which a peer // receives within IDLED seconds since the message has been relayed. - if (reduceRelayReady() && relayed && - (stopwatch().now() - *relayed) < reduce_relay::IDLED) + if (relayed && (stopwatch().now() - *relayed) < reduce_relay::IDLED) overlay_.updateSlotAndSquelch( suppression, publicKey, id_, protocol::mtPROPOSE_LEDGER); @@ -2368,10 +2381,8 @@ PeerImp::onMessage(std::shared_ptr const& m) { // Count unique messages (Slots has it's own 'HashRouter'), which a // peer receives within IDLED seconds since the message has been - // relayed. Wait WAIT_ON_BOOTUP time to let the server establish - // connections to peers. - if (reduceRelayReady() && relayed && - (stopwatch().now() - *relayed) < reduce_relay::IDLED) + // relayed. + if (relayed && (stopwatch().now() - *relayed) < reduce_relay::IDLED) overlay_.updateSlotAndSquelch( key, val->getSignerPublic(), id_, protocol::mtVALIDATION); @@ -2838,6 +2849,18 @@ PeerImp::checkTransaction( // VFALCO TODO Rewrite to not use exceptions try { + // charge strongly for relaying batch txns + // LCOV_EXCL_START + if (stx->isFlag(tfInnerBatchTxn) && + getCurrentTransactionRules()->enabled(featureBatch)) + { + JLOG(p_journal_.warn()) << "Ignoring Network relayed Tx containing " + "tfInnerBatchTxn (checkSignature)."; + charge(Resource::feeModerateBurdenPeer, "inner batch txn"); + return; + } + // LCOV_EXCL_STOP + // Expired? if (stx->isFieldPresent(sfLastLedgerSequence) && (stx->getFieldU32(sfLastLedgerSequence) < @@ -2980,7 +3003,7 @@ PeerImp::checkPropose( // as part of the squelch logic. auto haveMessage = app_.overlay().relay( *packet, peerPos.suppressionID(), peerPos.publicKey()); - if (reduceRelayReady() && !haveMessage.empty()) + if (!haveMessage.empty()) overlay_.updateSlotAndSquelch( peerPos.suppressionID(), peerPos.publicKey(), @@ -3015,7 +3038,7 @@ PeerImp::checkValidation( // as part of the squelch logic. auto haveMessage = overlay_.relay(*packet, key, val->getSignerPublic()); - if (reduceRelayReady() && !haveMessage.empty()) + if (!haveMessage.empty()) { overlay_.updateSlotAndSquelch( key, @@ -3481,16 +3504,6 @@ PeerImp::isHighLatency() const return latency_ >= peerHighLatency; } -bool -PeerImp::reduceRelayReady() -{ - if (!reduceRelayReady_) - reduceRelayReady_ = - reduce_relay::epoch(UptimeClock::now()) > - reduce_relay::WAIT_ON_BOOTUP; - return vpReduceRelayEnabled_ && reduceRelayReady_; -} - void PeerImp::Metrics::add_message(std::uint64_t bytes) { diff --git a/src/xrpld/overlay/detail/PeerImp.h b/src/xrpld/overlay/detail/PeerImp.h index 8fbafa1ee9..ecd3fc7f63 100644 --- a/src/xrpld/overlay/detail/PeerImp.h +++ b/src/xrpld/overlay/detail/PeerImp.h @@ -116,7 +116,6 @@ private: clock_type::time_point const creationTime_; reduce_relay::Squelch squelch_; - inline static std::atomic_bool reduceRelayReady_{false}; // Notes on thread locking: // @@ -190,9 +189,7 @@ private: hash_set txQueue_; // true if tx reduce-relay feature is enabled on the peer. bool txReduceRelayEnabled_ = false; - // true if validation/proposal reduce-relay feature is enabled - // on the peer. - bool vpReduceRelayEnabled_ = false; + bool ledgerReplayEnabled_ = false; LedgerReplayMsgHandler ledgerReplayMsgHandler_; @@ -521,11 +518,6 @@ private: handleHaveTransactions( std::shared_ptr const& m); - // Check if reduce-relay feature is enabled and - // reduce_relay::WAIT_ON_BOOTUP time passed since the start - bool - reduceRelayReady(); - public: //-------------------------------------------------------------------------- // @@ -705,7 +697,6 @@ PeerImp::PeerImp( headers_, FEATURE_TXRR, app_.config().TX_REDUCE_RELAY_ENABLE)) - , vpReduceRelayEnabled_(app_.config().VP_REDUCE_RELAY_ENABLE) , ledgerReplayEnabled_(peerFeatureEnabled( headers_, FEATURE_LEDGER_REPLAY, @@ -714,13 +705,15 @@ PeerImp::PeerImp( { read_buffer_.commit(boost::asio::buffer_copy( read_buffer_.prepare(boost::asio::buffer_size(buffers)), buffers)); - JLOG(journal_.info()) << "compression enabled " - << (compressionEnabled_ == Compressed::On) - << " vp reduce-relay enabled " - << vpReduceRelayEnabled_ - << " tx reduce-relay enabled " - << txReduceRelayEnabled_ << " on " << remote_address_ - << " " << id_; + JLOG(journal_.info()) + << "compression enabled " << (compressionEnabled_ == Compressed::On) + << " vp reduce-relay base squelch enabled " + << peerFeatureEnabled( + headers_, + FEATURE_VPRR, + app_.config().VP_REDUCE_RELAY_BASE_SQUELCH_ENABLE) + << " tx reduce-relay enabled " << txReduceRelayEnabled_ << " on " + << remote_address_ << " " << id_; } template diff --git a/src/xrpld/overlay/detail/TrafficCount.h b/src/xrpld/overlay/detail/TrafficCount.h index e93163683b..8dc02def5f 100644 --- a/src/xrpld/overlay/detail/TrafficCount.h +++ b/src/xrpld/overlay/detail/TrafficCount.h @@ -109,6 +109,8 @@ public: squelch, squelch_suppressed, // egress traffic amount suppressed by squelching + squelch_ignored, // the traffic amount that came from peers ignoring + // squelch messages // TMHaveSet message: get_set, // transaction sets we try to get @@ -262,6 +264,7 @@ public: {validatorlist, "validator_lists"}, {squelch, "squelch"}, {squelch_suppressed, "squelch_suppressed"}, + {squelch_ignored, "squelch_ignored"}, {get_set, "set_get"}, {share_set, "set_share"}, {ld_tsc_get, "ledger_data_Transaction_Set_candidate_get"}, @@ -326,6 +329,7 @@ protected: {validatorlist, {validatorlist}}, {squelch, {squelch}}, {squelch_suppressed, {squelch_suppressed}}, + {squelch_ignored, {squelch_ignored}}, {get_set, {get_set}}, {share_set, {share_set}}, {ld_tsc_get, {ld_tsc_get}}, diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 9387aba505..a4454c6e8a 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -464,9 +464,6 @@ transactionPreProcessImpl( hasTicketSeq ? 0 : app.getTxQ().nextQueuableSeq(sle).value(); } - if (!tx_json.isMember(jss::Flags)) - tx_json[jss::Flags] = tfFullyCanonicalSig; - if (!tx_json.isMember(jss::NetworkID)) { auto const networkId = app.config().NETWORK_ID; @@ -749,6 +746,7 @@ transactionFormatResultImpl(Transaction::pointer tpTrans, unsigned apiVersion) [[nodiscard]] static XRPAmount getTxFee(Application const& app, Config const& config, Json::Value tx) { + auto const& ledger = app.openLedger().current(); // autofilling only needed in this function so that the `STParsedJSONObject` // parsing works properly it should not be modifying the actual `tx` object if (!tx.isMember(jss::Fee)) @@ -776,6 +774,9 @@ getTxFee(Application const& app, Config const& config, Json::Value tx) if (!tx[jss::Signers].isArray()) return config.FEES.reference_fee; + if (tx[jss::Signers].size() > STTx::maxMultiSigners(&ledger->rules())) + return config.FEES.reference_fee; + // check multi-signed signers for (auto& signer : tx[jss::Signers]) { @@ -804,6 +805,10 @@ getTxFee(Application const& app, Config const& config, Json::Value tx) try { STTx const& stTx = STTx(std::move(parsed.object.value())); + std::string reason; + if (!passesLocalChecks(stTx, reason)) + return config.FEES.reference_fee; + return calculateBaseFee(*app.openLedger().current(), stTx); } catch (std::exception& e) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 5f69c203ff..3c175883c5 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -342,6 +342,11 @@ doSimulate(RPC::JsonContext& context) return jvResult; } + if (stTx->getTxnType() == ttBATCH) + { + return RPC::make_error(rpcNOT_IMPL); + } + std::string reason; auto transaction = std::make_shared(stTx, reason, context.app); // Actually run the transaction through the transaction processor