diff --git a/.codecov.yml b/.codecov.yml index 6df3786197..b97039e8b6 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -7,13 +7,13 @@ comment: show_carryforward_flags: false coverage: - range: "60..80" + range: "70..85" precision: 1 round: nearest status: project: default: - target: 60% + target: 75% threshold: 2% patch: default: 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/README.md b/README.md index cc002a2dd8..0315c37428 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![codecov](https://codecov.io/gh/XRPLF/rippled/graph/badge.svg?token=WyFr5ajq3O)](https://codecov.io/gh/XRPLF/rippled) + # The XRP Ledger The [XRP Ledger](https://xrpl.org/) is a decentralized cryptographic ledger powered by a network of peer-to-peer nodes. The XRP Ledger uses a novel Byzantine Fault Tolerant consensus algorithm to settle and record transactions in a secure distributed database without a central operator. diff --git a/SECURITY.md b/SECURITY.md index 4e845735d4..eb7437d2f9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -83,7 +83,7 @@ To report a qualifying bug, please send a detailed report to: |Long Key ID | `0xCD49A0AFC57929BE` | |Fingerprint | `24E6 3B02 37E0 FA9C 5E96 8974 CD49 A0AF C579 29BE` | -The full PGP key for this address, which is also available on several key servers (e.g. on [keys.gnupg.net](https://keys.gnupg.net)), is: +The full PGP key for this address, which is also available on several key servers (e.g. on [keyserver.ubuntu.com](https://keyserver.ubuntu.com)), is: ``` -----BEGIN PGP PUBLIC KEY BLOCK----- mQINBFUwGHYBEAC0wpGpBPkd8W1UdQjg9+cEFzeIEJRaoZoeuJD8mofwI5Ejnjdt 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/beast/hash/uhash.h b/include/xrpl/beast/hash/uhash.h index ab3eaad039..ac4ba7256d 100644 --- a/include/xrpl/beast/hash/uhash.h +++ b/include/xrpl/beast/hash/uhash.h @@ -30,7 +30,7 @@ namespace beast { template struct uhash { - explicit uhash() = default; + uhash() = default; using result_type = typename Hasher::result_type; diff --git a/include/xrpl/beast/net/IPEndpoint.h b/include/xrpl/beast/net/IPEndpoint.h index e66e7f4caa..345ba4b8da 100644 --- a/include/xrpl/beast/net/IPEndpoint.h +++ b/include/xrpl/beast/net/IPEndpoint.h @@ -215,7 +215,7 @@ namespace std { template <> struct hash<::beast::IP::Endpoint> { - explicit hash() = default; + hash() = default; std::size_t operator()(::beast::IP::Endpoint const& endpoint) const @@ -230,7 +230,7 @@ namespace boost { template <> struct hash<::beast::IP::Endpoint> { - explicit hash() = default; + hash() = default; std::size_t operator()(::beast::IP::Endpoint const& endpoint) const diff --git a/include/xrpl/protocol/AccountID.h b/include/xrpl/protocol/AccountID.h index 2677dd76bc..295cf41e4f 100644 --- a/include/xrpl/protocol/AccountID.h +++ b/include/xrpl/protocol/AccountID.h @@ -149,7 +149,7 @@ namespace std { template <> struct hash : ripple::AccountID::hasher { - explicit hash() = default; + hash() = default; }; } // namespace std 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/Book.h b/include/xrpl/protocol/Book.h index 0a04deb277..a8b9afacac 100644 --- a/include/xrpl/protocol/Book.h +++ b/include/xrpl/protocol/Book.h @@ -21,6 +21,7 @@ #define RIPPLE_PROTOCOL_BOOK_H_INCLUDED #include +#include #include #include @@ -36,12 +37,17 @@ class Book final : public CountedObject public: Issue in; Issue out; + std::optional domain; Book() { } - Book(Issue const& in_, Issue const& out_) : in(in_), out(out_) + Book( + Issue const& in_, + Issue const& out_, + std::optional const& domain_) + : in(in_), out(out_), domain(domain_) { } }; @@ -61,6 +67,8 @@ hash_append(Hasher& h, Book const& b) { using beast::hash_append; hash_append(h, b.in, b.out); + if (b.domain) + hash_append(h, *(b.domain)); } Book @@ -71,7 +79,8 @@ reversed(Book const& book); [[nodiscard]] inline constexpr bool operator==(Book const& lhs, Book const& rhs) { - return (lhs.in == rhs.in) && (lhs.out == rhs.out); + return (lhs.in == rhs.in) && (lhs.out == rhs.out) && + (lhs.domain == rhs.domain); } /** @} */ @@ -82,7 +91,18 @@ operator<=>(Book const& lhs, Book const& rhs) { if (auto const c{lhs.in <=> rhs.in}; c != 0) return c; - return lhs.out <=> rhs.out; + if (auto const c{lhs.out <=> rhs.out}; c != 0) + return c; + + // Manually compare optionals + if (lhs.domain && rhs.domain) + return *lhs.domain <=> *rhs.domain; // Compare values if both exist + if (!lhs.domain && rhs.domain) + return std::weak_ordering::less; // Empty is considered less + if (lhs.domain && !rhs.domain) + return std::weak_ordering::greater; // Non-empty is greater + + return std::weak_ordering::equivalent; // Both are empty } /** @} */ @@ -104,7 +124,7 @@ private: boost::base_from_member, 1>; public: - explicit hash() = default; + hash() = default; using value_type = std::size_t; using argument_type = ripple::Issue; @@ -126,12 +146,14 @@ template <> struct hash { private: - using hasher = std::hash; + using issue_hasher = std::hash; + using uint256_hasher = ripple::uint256::hasher; - hasher m_hasher; + issue_hasher m_issue_hasher; + uint256_hasher m_uint256_hasher; public: - explicit hash() = default; + hash() = default; using value_type = std::size_t; using argument_type = ripple::Book; @@ -139,8 +161,12 @@ public: value_type operator()(argument_type const& value) const { - value_type result(m_hasher(value.in)); - boost::hash_combine(result, m_hasher(value.out)); + value_type result(m_issue_hasher(value.in)); + boost::hash_combine(result, m_issue_hasher(value.out)); + + if (value.domain) + boost::hash_combine(result, m_uint256_hasher(*value.domain)); + return result; } }; @@ -154,7 +180,7 @@ namespace boost { template <> struct hash : std::hash { - explicit hash() = default; + hash() = default; using Base = std::hash; // VFALCO NOTE broken in vs2012 @@ -164,7 +190,7 @@ struct hash : std::hash template <> struct hash : std::hash { - explicit hash() = default; + hash() = default; using Base = std::hash; // VFALCO NOTE broken in vs2012 diff --git a/include/xrpl/protocol/ErrorCodes.h b/include/xrpl/protocol/ErrorCodes.h index 66b4dd178c..9c9319ba42 100644 --- a/include/xrpl/protocol/ErrorCodes.h +++ b/include/xrpl/protocol/ErrorCodes.h @@ -154,7 +154,10 @@ enum error_code_i { // Simulate rpcTX_SIGNED = 96, - rpcLAST = rpcTX_SIGNED // rpcLAST should always equal the last code. + // Pathfinding + rpcDOMAIN_MALFORMED = 97, + + rpcLAST = rpcDOMAIN_MALFORMED // rpcLAST should always equal the last code. }; /** Codes returned in the `warnings` array of certain RPC commands. 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/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index d78d3e9dd0..30f6438f02 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -152,6 +152,7 @@ enum LedgerSpecificFlags { // ltOFFER lsfPassive = 0x00010000, lsfSell = 0x00020000, // True, offer was placed as a sell. + lsfHybrid = 0x00040000, // True, offer is hybrid. // ltRIPPLE_STATE lsfLowReserve = 0x00010000, // True, if entry counts toward reserve. diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index fb8e7ce9d1..94d8a9696b 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -255,6 +255,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 e89a780655..012b33412a 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -153,6 +153,11 @@ public: checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; + Expected + checkBatchSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const; + // SQL Functions with metadata. static std::string const& getMetaSQLInsertReplaceHeader(); @@ -168,6 +173,9 @@ public: char status, std::string const& escapedMetaData) const; + std::vector + getBatchTransactionIDs() const; + private: Expected checkSingleSign( @@ -180,12 +188,24 @@ private: Rules const& rules, STObject const* pSig) 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 24aa92399c..cf9ccbbbc9 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,8 +98,9 @@ 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 tfHybrid = 0x00100000; constexpr std::uint32_t tfOfferCreateMask = - ~(tfUniversal | tfPassive | tfImmediateOrCancel | tfFillOrKill | tfSell); + ~(tfUniversal | tfPassive | tfImmediateOrCancel | tfFillOrKill | tfSell | tfHybrid); // Payment flags: constexpr std::uint32_t tfNoRippleDirect = 0x00010000; @@ -243,6 +245,19 @@ 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; + // LoanSet flags: // True, indicates the loan supports overpayments constexpr std::uint32_t const tfLoanOverpayment = 0x00010000; 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/UintTypes.h b/include/xrpl/protocol/UintTypes.h index 9a7284158e..1d6b3e23dc 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -63,6 +63,9 @@ using NodeID = base_uint<160, detail::NodeIDTag>; * and a 160-bit account */ using MPTID = base_uint<192>; +/** Domain is a 256-bit hash representing a specific domain. */ +using Domain = base_uint<256>; + /** XRP currency. */ Currency const& xrpCurrency(); @@ -119,25 +122,25 @@ namespace std { template <> struct hash : ripple::Currency::hasher { - explicit hash() = default; + hash() = default; }; template <> struct hash : ripple::NodeID::hasher { - explicit hash() = default; + hash() = default; }; template <> struct hash : ripple::Directory::hasher { - explicit hash() = default; + hash() = default; }; template <> struct hash : ripple::uint256::hasher { - explicit hash() = default; + hash() = default; }; } // namespace std diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 0b71de3a8d..c04b04d4c1 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -33,6 +33,8 @@ // Keep it sorted in reverse chronological order. XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) @@ -148,4 +150,4 @@ XRPL_RETIRE(fix1512) XRPL_RETIRE(fix1523) XRPL_RETIRE(fix1528) -// clang-format on \ No newline at end of file +// clang-format on diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index d009a67bf5..49bd578f24 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -189,6 +189,7 @@ LEDGER_ENTRY(ltDIR_NODE, 0x0064, DirectoryNode, directory, ({ {sfNFTokenID, soeOPTIONAL}, {sfPreviousTxnID, soeOPTIONAL}, {sfPreviousTxnLgrSeq, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL} })) /** The ledger object which lists details about amendments on the network. @@ -250,6 +251,8 @@ LEDGER_ENTRY(ltOFFER, 0x006f, Offer, offer, ({ {sfPreviousTxnID, soeREQUIRED}, {sfPreviousTxnLgrSeq, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL}, + {sfAdditionalBooks, soeOPTIONAL}, })) /** A ledger object which describes a deposit preauthorization. diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 29771944d2..0e4c6d00b3 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -193,7 +193,7 @@ TYPED_SFIELD(sfEmitParentTxnID, UINT256, 11) TYPED_SFIELD(sfEmitNonce, UINT256, 12) TYPED_SFIELD(sfEmitHookHash, UINT256, 13) TYPED_SFIELD(sfAMMID, UINT256, 14, - SField::sMD_PseudoAccount |SField::sMD_Default) + SField::sMD_PseudoAccount | SField::sMD_Default) // 256-bit (uncommon) TYPED_SFIELD(sfBookDirectory, UINT256, 16) @@ -217,9 +217,10 @@ TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) TYPED_SFIELD(sfVaultID, UINT256, 35, SField::sMD_PseudoAccount | SField::sMD_Default) -TYPED_SFIELD(sfLoanBrokerID, UINT256, 36, +TYPED_SFIELD(sfParentBatchID, UINT256, 36) +TYPED_SFIELD(sfLoanBrokerID, UINT256, 37, SField::sMD_PseudoAccount | SField::sMD_Default) -TYPED_SFIELD(sfLoanID, UINT256, 37) +TYPED_SFIELD(sfLoanID, UINT256, 38) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -393,7 +394,10 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30) UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31) UNTYPED_SFIELD(sfPriceData, OBJECT, 32) UNTYPED_SFIELD(sfCredential, OBJECT, 33) -UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 34, SField::sMD_Default, SField::notSigning) +UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) +UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) +UNTYPED_SFIELD(sfBook, OBJECT, 36) +UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::sMD_Default, SField::notSigning) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -409,6 +413,7 @@ UNTYPED_SFIELD(sfMemos, ARRAY, 9) UNTYPED_SFIELD(sfNFTokens, ARRAY, 10) UNTYPED_SFIELD(sfHooks, ARRAY, 11) UNTYPED_SFIELD(sfVoteSlots, ARRAY, 12) +UNTYPED_SFIELD(sfAdditionalBooks, ARRAY, 13) // array of objects (uncommon) UNTYPED_SFIELD(sfMajorities, ARRAY, 16) @@ -425,5 +430,7 @@ 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) // clang-format on diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 5a07fcebbf..6472564081 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -71,6 +71,7 @@ TRANSACTION(ttPAYMENT, 0, Payment, {sfDestinationTag, soeOPTIONAL}, {sfDeliverMin, soeOPTIONAL, soeMPTSupported}, {sfCredentialIDs, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL}, })) /** This transaction type creates an escrow object. */ @@ -153,6 +154,7 @@ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, {sfTakerGets, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, {sfOfferSequence, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL}, })) /** This transaction type cancels existing offers to trade one asset for another. */ @@ -814,7 +816,18 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, {sfAmount, soeOPTIONAL, soeMPTSupported}, })) -/** Reserve 71-73 for future Vault-related transactions */ +/** This transaction type batches together transactions. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttBATCH, 71, Batch, + Delegation::notDelegatable, + noPriv, ({ + {sfRawTransactions, soeREQUIRED}, + {sfBatchSigners, soeOPTIONAL}, +})) + +/** Reserve 72-73 for future Vault-related transactions */ /** This transaction creates and updates a Loan Broker */ #if TRANSACTION_INCLUDE @@ -976,4 +989,3 @@ TRANSACTION(ttUNL_MODIFY, 102, UNLModify, {sfLedgerSequence, soeREQUIRED}, {sfUNLModifyValidator, soeREQUIRED}, })) - diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index fc7f367562..b71c3c80d6 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/include/xrpl/resource/detail/Key.h b/include/xrpl/resource/detail/Key.h index f953d5103e..188ee142da 100644 --- a/include/xrpl/resource/detail/Key.h +++ b/include/xrpl/resource/detail/Key.h @@ -53,7 +53,7 @@ struct Key struct key_equal { - explicit key_equal() = default; + key_equal() = default; bool operator()(Key const& lhs, Key const& rhs) const diff --git a/src/libxrpl/protocol/Book.cpp b/src/libxrpl/protocol/Book.cpp index cfd1fc61dc..2114deab6b 100644 --- a/src/libxrpl/protocol/Book.cpp +++ b/src/libxrpl/protocol/Book.cpp @@ -48,7 +48,7 @@ operator<<(std::ostream& os, Book const& x) Book reversed(Book const& book) { - return Book(book.out, book.in); + return Book(book.out, book.in, book.domain); } } // namespace ripple diff --git a/src/libxrpl/protocol/ErrorCodes.cpp b/src/libxrpl/protocol/ErrorCodes.cpp index b3d1b812b5..3109f51d05 100644 --- a/src/libxrpl/protocol/ErrorCodes.cpp +++ b/src/libxrpl/protocol/ErrorCodes.cpp @@ -116,7 +116,8 @@ constexpr static ErrorInfo unorderedErrorInfos[]{ {rpcUNKNOWN_COMMAND, "unknownCmd", "Unknown method.", 405}, {rpcORACLE_MALFORMED, "oracleMalformed", "Oracle request is malformed.", 400}, {rpcBAD_CREDENTIALS, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400}, - {rpcTX_SIGNED, "transactionSigned", "Transaction should not be signed.", 400}}; + {rpcTX_SIGNED, "transactionSigned", "Transaction should not be signed.", 400}, + {rpcDOMAIN_MALFORMED, "domainMalformed", "Domain is malformed.", 400}}; // clang-format on // Sort and validate unorderedErrorInfos at compile time. Should be diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 73ef3e8a3e..d87241b719 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -119,12 +119,19 @@ getBookBase(Book const& book) XRPL_ASSERT( isConsistent(book), "ripple::getBookBase : input is consistent"); - auto const index = indexHash( - LedgerNameSpace::BOOK_DIR, - book.in.currency, - book.out.currency, - book.in.account, - book.out.account); + auto const index = book.domain ? indexHash( + LedgerNameSpace::BOOK_DIR, + book.in.currency, + book.out.currency, + book.in.account, + book.out.account, + *(book.domain)) + : indexHash( + LedgerNameSpace::BOOK_DIR, + book.in.currency, + book.out.currency, + book.in.account, + book.out.account); // Return with quality 0. auto k = keylet::quality({ltDIR_NODE, index}, 0); diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index fdbc9d7772..ebb6646f65 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -159,6 +159,20 @@ InnerObjectFormats::InnerObjectFormats() sfPermission.getCode(), {{sfPermissionValue, soeREQUIRED}}); + add(sfBatchSigner.jsonName.c_str(), + sfBatchSigner.getCode(), + {{sfAccount, soeREQUIRED}, + {sfSigningPubKey, soeOPTIONAL}, + {sfTxnSignature, soeOPTIONAL}, + {sfSigners, soeOPTIONAL}}); + + add(sfBook.jsonName, + sfBook.getCode(), + { + {sfBookDirectory, soeREQUIRED}, + {sfBookNode, soeREQUIRED}, + }); + add(sfCounterpartySignature.jsonName, sfCounterpartySignature.getCode(), { diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 7b00a05790..3a6fc13b8e 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 @@ -283,6 +286,42 @@ STTx::checkSign( return {}; } +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 { @@ -362,13 +401,12 @@ STTx::getMetaSQL( getFieldU32(sfSequence) % inLedger % status % rTxn % escapedMetaData); } -Expected -STTx::checkSingleSign( - RequireFullyCanonicalSig requireCanonicalSig, - STObject const* pSig) const +static Expected +singleSignHelper( + STObject const& sigObject, + Slice const& data, + bool const fullyCanonical) { - STObject const& sigObject{pSig ? *pSig : *this}; - // 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. @@ -378,42 +416,60 @@ STTx::checkSingleSign( bool validSig = false; try { - bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || - (requireCanonicalSig == RequireFullyCanonicalSig::yes); - auto const spk = sigObject.getFieldVL(sfSigningPubKey); - if (publicKeyType(makeSlice(spk))) { Blob const signature = sigObject.getFieldVL(sfTxnSignature); - Blob const data = getSigningData(*this); - 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( +STTx::checkSingleSign( RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules, STObject const* pSig) const { STObject const& sigObject{pSig ? *pSig : *this}; + auto const data = getSigningData(*this); + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes); + return singleSignHelper(sigObject, 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& sigObject, + std::optional txnAccountID, + 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 (!sigObject.isFieldPresent(sfSigners)) @@ -427,22 +483,10 @@ STTx::checkMultiSign( STArray const& signers{sigObject.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); - // Signers must be in sorted order by AccountID. AccountID lastAccountID(beast::zero); @@ -451,7 +495,7 @@ STTx::checkMultiSign( auto const accountID = signer.getAccountID(sfAccount); // The account owner may not usually multisign for themselves. - if (!pSig && accountID == txnAccountID) + if (txnAccountID == accountID) return Unexpected("Invalid multisigner."); // No duplicate signers allowed. @@ -469,18 +513,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); } @@ -499,6 +538,100 @@ 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, + std::nullopt, + fullyCanonical, + [&dataStart](AccountID const& accountID) mutable -> Serializer { + Serializer s = dataStart; + finishMultiSigningData(accountID, s); + return s; + }, + rules); +} + +Expected +STTx::checkMultiSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules, + STObject const* pSig) const +{ + STObject const& sigObject{pSig ? *pSig : *this}; + + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == RequireFullyCanonicalSig::yes); + + // Used inside the loop in multiSignHelper to enforce that + // the account owner may not multisign for themselves. + auto const txnAccountID = + pSig ? std::nullopt : std::optional(getAccountID(sfAccount)); + + // 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( + sigObject, + txnAccountID, + 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 @@ -634,6 +767,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) { @@ -658,6 +827,9 @@ passesLocalChecks(STObject const& st, std::string& reason) return false; } + if (!isRawTransactionOkay(st, reason)) + return false; + return true; } @@ -673,10 +845,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/AMMExtended_test.cpp b/src/test/app/AMMExtended_test.cpp index d7caed9601..f9750eaa53 100644 --- a/src/test/app/AMMExtended_test.cpp +++ b/src/test/app/AMMExtended_test.cpp @@ -2156,6 +2156,7 @@ private: OfferCrossing::no, std::nullopt, smax, + std::nullopt, flowJournal); }(); 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/CrossingLimits_test.cpp b/src/test/app/CrossingLimits_test.cpp index 1e19a178c2..cef0b03399 100644 --- a/src/test/app/CrossingLimits_test.cpp +++ b/src/test/app/CrossingLimits_test.cpp @@ -558,8 +558,11 @@ public: using namespace jtx; auto const sa = supported_amendments(); testAll(sa); - testAll(sa - featureFlowSortStrands); - testAll(sa - featureFlowCross - featureFlowSortStrands); + testAll(sa - featurePermissionedDEX); + testAll(sa - featureFlowSortStrands - featurePermissionedDEX); + testAll( + sa - featureFlowCross - featureFlowSortStrands - + featurePermissionedDEX); } }; 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/DeliverMin_test.cpp b/src/test/app/DeliverMin_test.cpp index b079b93680..4ee7c9c72e 100644 --- a/src/test/app/DeliverMin_test.cpp +++ b/src/test/app/DeliverMin_test.cpp @@ -143,7 +143,9 @@ public: { using namespace jtx; auto const sa = supported_amendments(); - test_convert_all_of_an_asset(sa - featureFlowCross); + test_convert_all_of_an_asset( + sa - featureFlowCross - featurePermissionedDEX); + test_convert_all_of_an_asset(sa - featurePermissionedDEX); test_convert_all_of_an_asset(sa); } }; diff --git a/src/test/app/Discrepancy_test.cpp b/src/test/app/Discrepancy_test.cpp index 8e306282a7..bc72b2fd16 100644 --- a/src/test/app/Discrepancy_test.cpp +++ b/src/test/app/Discrepancy_test.cpp @@ -147,7 +147,8 @@ public: { using namespace test::jtx; auto const sa = supported_amendments(); - testXRPDiscrepancy(sa - featureFlowCross); + testXRPDiscrepancy(sa - featureFlowCross - featurePermissionedDEX); + testXRPDiscrepancy(sa - featurePermissionedDEX); testXRPDiscrepancy(sa); } }; diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index ae65432ac7..d0b8686db6 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -494,6 +494,7 @@ struct Flow_test : public beast::unit_test::suite OfferCrossing::no, std::nullopt, smax, + std::nullopt, flowJournal); }(); @@ -1475,7 +1476,8 @@ struct Flow_test : public beast::unit_test::suite using namespace jtx; auto const sa = supported_amendments(); - testWithFeats(sa - featureFlowCross); + testWithFeats(sa - featureFlowCross - featurePermissionedDEX); + testWithFeats(sa - featurePermissionedDEX); testWithFeats(sa); testEmptyStrand(sa); } @@ -1490,13 +1492,16 @@ struct Flow_manual_test : public Flow_test auto const all = supported_amendments(); FeatureBitset const flowCross{featureFlowCross}; FeatureBitset const f1513{fix1513}; + FeatureBitset const permDex{featurePermissionedDEX}; - testWithFeats(all - flowCross - f1513); - testWithFeats(all - flowCross); - testWithFeats(all - f1513); + testWithFeats(all - flowCross - f1513 - permDex); + testWithFeats(all - flowCross - permDex); + testWithFeats(all - f1513 - permDex); + testWithFeats(all - permDex); testWithFeats(all); - testEmptyStrand(all - f1513); + testEmptyStrand(all - f1513 - permDex); + testEmptyStrand(all - permDex); testEmptyStrand(all); } }; diff --git a/src/test/app/Freeze_test.cpp b/src/test/app/Freeze_test.cpp index 36578cbc6b..b28e794688 100644 --- a/src/test/app/Freeze_test.cpp +++ b/src/test/app/Freeze_test.cpp @@ -2020,9 +2020,11 @@ public: }; using namespace test::jtx; auto const sa = supported_amendments(); - testAll(sa - featureFlowCross - featureDeepFreeze); - testAll(sa - featureFlowCross); - testAll(sa - featureDeepFreeze); + testAll( + sa - featureFlowCross - featureDeepFreeze - featurePermissionedDEX); + testAll(sa - featureFlowCross - featurePermissionedDEX); + testAll(sa - featureDeepFreeze - featurePermissionedDEX); + testAll(sa - featurePermissionedDEX); testAll(sa); } }; 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/Offer_test.cpp b/src/test/app/Offer_test.cpp index 4da8d8101e..0891b27df8 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -5419,13 +5419,16 @@ public: static FeatureBitset const immediateOfferKilled{ featureImmediateOfferKilled}; FeatureBitset const fillOrKill{fixFillOrKill}; + FeatureBitset const permDEX{featurePermissionedDEX}; - static std::array const feats{ - all - takerDryOffer - immediateOfferKilled, - all - flowCross - takerDryOffer - immediateOfferKilled, - all - flowCross - immediateOfferKilled, - all - rmSmallIncreasedQOffers - immediateOfferKilled - fillOrKill, - all - fillOrKill, + static std::array const feats{ + all - takerDryOffer - immediateOfferKilled - permDEX, + all - flowCross - takerDryOffer - immediateOfferKilled - permDEX, + all - flowCross - immediateOfferKilled - permDEX, + all - rmSmallIncreasedQOffers - immediateOfferKilled - fillOrKill - + permDEX, + all - fillOrKill - permDEX, + all - permDEX, all}; if (BEAST_EXPECT(instance < feats.size())) @@ -5479,12 +5482,21 @@ class OfferWOFillOrKill_test : public OfferBaseUtil_test } }; +class OfferWOPermDEX_test : public OfferBaseUtil_test +{ + void + run() override + { + OfferBaseUtil_test::run(5); + } +}; + class OfferAllFeatures_test : public OfferBaseUtil_test { void run() override { - OfferBaseUtil_test::run(5, true); + OfferBaseUtil_test::run(6, true); } }; @@ -5500,14 +5512,16 @@ class Offer_manual_test : public OfferBaseUtil_test FeatureBitset const immediateOfferKilled{featureImmediateOfferKilled}; FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval}; FeatureBitset const fillOrKill{fixFillOrKill}; + FeatureBitset const permDEX{featurePermissionedDEX}; - testAll(all - flowCross - f1513 - immediateOfferKilled); - testAll(all - flowCross - immediateOfferKilled); - testAll(all - immediateOfferKilled - fillOrKill); - testAll(all - fillOrKill); + testAll(all - flowCross - f1513 - immediateOfferKilled - permDEX); + testAll(all - flowCross - immediateOfferKilled - permDEX); + testAll(all - immediateOfferKilled - fillOrKill - permDEX); + testAll(all - fillOrKill - permDEX); + testAll(all - permDEX); testAll(all); - testAll(all - flowCross - takerDryOffer); + testAll(all - flowCross - takerDryOffer - permDEX); } }; @@ -5516,6 +5530,7 @@ BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFlowCross, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWTakerDryOffer, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWOSmallQOffers, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferWOFillOrKill, tx, ripple, 2); +BEAST_DEFINE_TESTSUITE_PRIO(OfferWOPermDEX, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_PRIO(OfferAllFeatures, tx, ripple, 2); BEAST_DEFINE_TESTSUITE_MANUAL_PRIO(Offer_manual, tx, ripple, 20); diff --git a/src/test/app/Path_test.cpp b/src/test/app/Path_test.cpp index f325b0d2be..6ff22a5dc7 100644 --- a/src/test/app/Path_test.cpp +++ b/src/test/app/Path_test.cpp @@ -18,11 +18,12 @@ //============================================================================== #include +#include +#include #include +#include -#include #include -#include #include #include #include @@ -34,7 +35,12 @@ #include #include +#include +#include #include +#include +#include +#include namespace ripple { namespace test { @@ -126,7 +132,8 @@ public: jtx::Account const& dst, STAmount const& saDstAmount, std::optional const& saSendMax = std::nullopt, - std::optional const& saSrcCurrency = std::nullopt) + std::optional const& saSrcCurrency = std::nullopt, + std::optional const& domain = std::nullopt) { using namespace jtx; @@ -163,6 +170,8 @@ public: j[jss::currency] = to_string(saSrcCurrency.value()); sc.append(j); } + if (domain) + params[jss::domain] = to_string(*domain); Json::Value result; gate g; @@ -187,10 +196,11 @@ public: jtx::Account const& dst, STAmount const& saDstAmount, std::optional const& saSendMax = std::nullopt, - std::optional const& saSrcCurrency = std::nullopt) + std::optional const& saSrcCurrency = std::nullopt, + std::optional const& domain = std::nullopt) { Json::Value result = find_paths_request( - env, src, dst, saDstAmount, saSendMax, saSrcCurrency); + env, src, dst, saDstAmount, saSendMax, saSrcCurrency, domain); BEAST_EXPECT(!result.isMember(jss::error)); STAmount da; @@ -363,9 +373,11 @@ public: } void - path_find() + path_find(bool const domainEnabled) { - testcase("path find"); + testcase( + std::string("path find") + (domainEnabled ? " w/ " : " w/o ") + + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); @@ -377,31 +389,50 @@ public: env(pay(gw, "alice", USD(70))); env(pay(gw, "bob", USD(50))); + std::optional domainID; + if (domainEnabled) + domainID = setupDomain(env, {"alice", "bob", gw}); + STPathSet st; STAmount sa; - std::tie(st, sa, std::ignore) = - find_paths(env, "alice", "bob", Account("bob")["USD"](5)); + std::tie(st, sa, std::ignore) = find_paths( + env, + "alice", + "bob", + Account("bob")["USD"](5), + std::nullopt, + std::nullopt, + domainID); BEAST_EXPECT(same(st, stpath("gateway"))); BEAST_EXPECT(equal(sa, Account("alice")["USD"](5))); } void - xrp_to_xrp() + xrp_to_xrp(bool const domainEnabled) { using namespace jtx; - testcase("XRP to XRP"); + testcase( + std::string("XRP to XRP") + (domainEnabled ? " w/ " : " w/o ") + + "domain"); Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob"); env.close(); - auto const result = find_paths(env, "alice", "bob", XRP(5)); + std::optional domainID; + if (domainEnabled) + domainID = setupDomain(env, {"alice", "bob"}); + + auto const result = find_paths( + env, "alice", "bob", XRP(5), std::nullopt, std::nullopt, domainID); BEAST_EXPECT(std::get<0>(result).empty()); } void - path_find_consume_all() + path_find_consume_all(bool const domainEnabled) { - testcase("path find consume all"); + testcase( + std::string("path find consume all") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; { @@ -414,11 +445,22 @@ public: env.trust(Account("alice")["USD"](100), "dan"); env.trust(Account("dan")["USD"](100), "edward"); + std::optional domainID; + if (domainEnabled) + domainID = setupDomain( + env, {"alice", "bob", "carol", "dan", "edward"}); + STPathSet st; STAmount sa; STAmount da; std::tie(st, sa, da) = find_paths( - env, "alice", "edward", Account("edward")["USD"](-1)); + env, + "alice", + "edward", + Account("edward")["USD"](-1), + std::nullopt, + std::nullopt, + domainID); BEAST_EXPECT(same(st, stpath("dan"), stpath("bob", "carol"))); BEAST_EXPECT(equal(sa, Account("alice")["USD"](110))); BEAST_EXPECT(equal(da, Account("edward")["USD"](110))); @@ -431,8 +473,22 @@ public: env.fund(XRP(10000), "alice", "bob", "carol", gw); env.close(); env.trust(USD(100), "bob", "carol"); + env.close(); env(pay(gw, "carol", USD(100))); - env(offer("carol", XRP(100), USD(100))); + env.close(); + + std::optional domainID; + if (domainEnabled) + { + domainID = + setupDomain(env, {"alice", "bob", "carol", "gateway"}); + env(offer("carol", XRP(100), USD(100)), domain(*domainID)); + } + else + { + env(offer("carol", XRP(100), USD(100))); + } + env.close(); STPathSet st; STAmount sa; @@ -442,23 +498,44 @@ public: "alice", "bob", Account("bob")["AUD"](-1), - std::optional(XRP(100000000))); + std::optional(XRP(1000000)), + std::nullopt, + domainID); BEAST_EXPECT(st.empty()); std::tie(st, sa, da) = find_paths( env, "alice", "bob", Account("bob")["USD"](-1), - std::optional(XRP(100000000))); + std::optional(XRP(1000000)), + std::nullopt, + domainID); BEAST_EXPECT(sa == XRP(100)); BEAST_EXPECT(equal(da, Account("bob")["USD"](100))); + + // if domain is used, finding path in the open offerbook will return + // empty result + if (domainEnabled) + { + std::tie(st, sa, da) = find_paths( + env, + "alice", + "bob", + Account("bob")["USD"](-1), + std::optional(XRP(1000000)), + std::nullopt, + std::nullopt); // not specifying a domain + BEAST_EXPECT(st.empty()); + } } } void - alternative_path_consume_both() + alternative_path_consume_both(bool const domainEnabled) { - testcase("alternative path consume both"); + testcase( + std::string("alternative path consume both") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); @@ -471,10 +548,26 @@ public: env.trust(gw2_USD(800), "alice"); env.trust(USD(700), "bob"); env.trust(gw2_USD(900), "bob"); - env(pay(gw, "alice", USD(70))); - env(pay(gw2, "alice", gw2_USD(70))); - env(pay("alice", "bob", Account("bob")["USD"](140)), - paths(Account("alice")["USD"])); + + std::optional domainID; + if (domainEnabled) + { + domainID = + setupDomain(env, {"alice", "bob", "gateway", "gateway2"}); + env(pay(gw, "alice", USD(70)), domain(*domainID)); + env(pay(gw2, "alice", gw2_USD(70)), domain(*domainID)); + env(pay("alice", "bob", Account("bob")["USD"](140)), + paths(Account("alice")["USD"]), + domain(*domainID)); + } + else + { + env(pay(gw, "alice", USD(70))); + env(pay(gw2, "alice", gw2_USD(70))); + env(pay("alice", "bob", Account("bob")["USD"](140)), + paths(Account("alice")["USD"])); + } + env.require(balance("alice", USD(0))); env.require(balance("alice", gw2_USD(0))); env.require(balance("bob", USD(70))); @@ -486,9 +579,11 @@ public: } void - alternative_paths_consume_best_transfer() + alternative_paths_consume_best_transfer(bool const domainEnabled) { - testcase("alternative paths consume best transfer"); + testcase( + std::string("alternative paths consume best transfer") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); @@ -502,9 +597,22 @@ public: env.trust(gw2_USD(800), "alice"); env.trust(USD(700), "bob"); env.trust(gw2_USD(900), "bob"); - env(pay(gw, "alice", USD(70))); - env(pay(gw2, "alice", gw2_USD(70))); - env(pay("alice", "bob", USD(70))); + + std::optional domainID; + if (domainEnabled) + { + domainID = + setupDomain(env, {"alice", "bob", "gateway", "gateway2"}); + env(pay(gw, "alice", USD(70)), domain(*domainID)); + env(pay(gw2, "alice", gw2_USD(70)), domain(*domainID)); + env(pay("alice", "bob", USD(70)), domain(*domainID)); + } + else + { + env(pay(gw, "alice", USD(70))); + env(pay(gw2, "alice", gw2_USD(70))); + env(pay("alice", "bob", USD(70))); + } env.require(balance("alice", USD(0))); env.require(balance("alice", gw2_USD(70))); env.require(balance("bob", USD(70))); @@ -548,9 +656,13 @@ public: } void - alternative_paths_limit_returned_paths_to_best_quality() + alternative_paths_limit_returned_paths_to_best_quality( + bool const domainEnabled) { - testcase("alternative paths - limit returned paths to best quality"); + testcase( + std::string( + "alternative paths - limit returned paths to best quality") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); @@ -566,14 +678,31 @@ public: env.trust(gw2_USD(800), "alice", "bob"); env.trust(Account("alice")["USD"](800), "dan"); env.trust(Account("bob")["USD"](800), "dan"); + env.close(); env(pay(gw2, "alice", gw2_USD(100))); + env.close(); env(pay("carol", "alice", Account("carol")["USD"](100))); + env.close(); env(pay(gw, "alice", USD(100))); + env.close(); + + std::optional domainID; + if (domainEnabled) + { + domainID = + setupDomain(env, {"alice", "bob", "carol", "dan", gw, gw2}); + } STPathSet st; STAmount sa; - std::tie(st, sa, std::ignore) = - find_paths(env, "alice", "bob", Account("bob")["USD"](5)); + std::tie(st, sa, std::ignore) = find_paths( + env, + "alice", + "bob", + Account("bob")["USD"](5), + std::nullopt, + std::nullopt, + domainID); BEAST_EXPECT(same( st, stpath("gateway"), @@ -584,9 +713,11 @@ public: } void - issues_path_negative_issue() + issues_path_negative_issue(bool const domainEnabled) { - testcase("path negative: Issue #5"); + testcase( + std::string("path negative: Issue #5") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); env.fund(XRP(10000), "alice", "bob", "carol", "dan"); @@ -597,14 +728,35 @@ public: env(pay("bob", "carol", Account("bob")["USD"](75))); env.require(balance("bob", Account("carol")["USD"](-75))); env.require(balance("carol", Account("bob")["USD"](75))); + env.close(); - auto result = - find_paths(env, "alice", "bob", Account("bob")["USD"](25)); + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {"alice", "bob", "carol", "dan"}); + } + + auto result = find_paths( + env, + "alice", + "bob", + Account("bob")["USD"](25), + std::nullopt, + std::nullopt, + domainID); BEAST_EXPECT(std::get<0>(result).empty()); env(pay("alice", "bob", Account("alice")["USD"](25)), ter(tecPATH_DRY)); + env.close(); - result = find_paths(env, "alice", "bob", Account("alice")["USD"](25)); + result = find_paths( + env, + "alice", + "bob", + Account("alice")["USD"](25), + std::nullopt, + std::nullopt, + domainID); BEAST_EXPECT(std::get<0>(result).empty()); env.require(balance("alice", Account("bob")["USD"](0))); @@ -671,9 +823,11 @@ public: // bob will hold gateway AUD // alice pays bob gateway AUD using XRP void - via_offers_via_gateway() + via_offers_via_gateway(bool const domainEnabled) { - testcase("via gateway"); + testcase( + std::string("via gateway") + (domainEnabled ? " w/ " : " w/o ") + + "domain"); using namespace jtx; Env env = pathTestEnv(); auto const gw = Account("gateway"); @@ -681,15 +835,43 @@ public: env.fund(XRP(10000), "alice", "bob", "carol", gw); env.close(); env(rate(gw, 1.1)); + env.close(); env.trust(AUD(100), "bob", "carol"); + env.close(); env(pay(gw, "carol", AUD(50))); - env(offer("carol", XRP(50), AUD(50))); - env(pay("alice", "bob", AUD(10)), sendmax(XRP(100)), paths(XRP)); + env.close(); + + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {"alice", "bob", "carol", gw}); + env(offer("carol", XRP(50), AUD(50)), domain(*domainID)); + env.close(); + env(pay("alice", "bob", AUD(10)), + sendmax(XRP(100)), + paths(XRP), + domain(*domainID)); + env.close(); + } + else + { + env(offer("carol", XRP(50), AUD(50))); + env.close(); + env(pay("alice", "bob", AUD(10)), sendmax(XRP(100)), paths(XRP)); + env.close(); + } + env.require(balance("bob", AUD(10))); env.require(balance("carol", AUD(39))); - auto const result = - find_paths(env, "alice", "bob", Account("bob")["USD"](25)); + auto const result = find_paths( + env, + "alice", + "bob", + Account("bob")["USD"](25), + std::nullopt, + std::nullopt, + domainID); BEAST_EXPECT(std::get<0>(result).empty()); } @@ -865,9 +1047,11 @@ public: } void - path_find_01() + path_find_01(bool const domainEnabled) { - testcase("Path Find: XRP -> XRP and XRP -> IOU"); + testcase( + std::string("Path Find: XRP -> XRP and XRP -> IOU") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; @@ -899,16 +1083,28 @@ public: env(pay(G3, M1, G3["ABC"](25000))); env.close(); - env(offer(M1, G1["XYZ"](1000), G2["XYZ"](1000))); - env(offer(M1, XRP(10000), G3["ABC"](1000))); + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {A1, A2, A3, G1, G2, G3, M1}); + env(offer(M1, G1["XYZ"](1000), G2["XYZ"](1000)), domain(*domainID)); + env(offer(M1, XRP(10000), G3["ABC"](1000)), domain(*domainID)); + env.close(); + } + else + { + env(offer(M1, G1["XYZ"](1000), G2["XYZ"](1000))); + env(offer(M1, XRP(10000), G3["ABC"](1000))); + env.close(); + } STPathSet st; STAmount sa, da; { auto const& send_amt = XRP(10); - std::tie(st, sa, da) = - find_paths(env, A1, A2, send_amt, std::nullopt, xrpCurrency()); + std::tie(st, sa, da) = find_paths( + env, A1, A2, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(st.empty()); } @@ -918,15 +1114,21 @@ public: // does not exist. auto const& send_amt = XRP(200); std::tie(st, sa, da) = find_paths( - env, A1, Account{"A0"}, send_amt, std::nullopt, xrpCurrency()); + env, + A1, + Account{"A0"}, + send_amt, + std::nullopt, + xrpCurrency(), + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(st.empty()); } { auto const& send_amt = G3["ABC"](10); - std::tie(st, sa, da) = - find_paths(env, A2, G3, send_amt, std::nullopt, xrpCurrency()); + std::tie(st, sa, da) = find_paths( + env, A2, G3, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, XRP(100))); BEAST_EXPECT(same(st, stpath(IPE(G3["ABC"])))); @@ -934,8 +1136,8 @@ public: { auto const& send_amt = A2["ABC"](1); - std::tie(st, sa, da) = - find_paths(env, A1, A2, send_amt, std::nullopt, xrpCurrency()); + std::tie(st, sa, da) = find_paths( + env, A1, A2, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, XRP(10))); BEAST_EXPECT(same(st, stpath(IPE(G3["ABC"]), G3))); @@ -943,8 +1145,8 @@ public: { auto const& send_amt = A3["ABC"](1); - std::tie(st, sa, da) = - find_paths(env, A1, A3, send_amt, std::nullopt, xrpCurrency()); + std::tie(st, sa, da) = find_paths( + env, A1, A3, send_amt, std::nullopt, xrpCurrency(), domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, XRP(10))); BEAST_EXPECT(same(st, stpath(IPE(G3["ABC"]), G3, A2))); @@ -952,9 +1154,11 @@ public: } void - path_find_02() + path_find_02(bool const domainEnabled) { - testcase("Path Find: non-XRP -> XRP"); + testcase( + std::string("Path Find: non-XRP -> XRP") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; @@ -975,23 +1179,53 @@ public: env(pay(G3, M1, G3["ABC"](1200))); env.close(); - env(offer(M1, G3["ABC"](1000), XRP(10000))); + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {A1, A2, G3, M1}); + env(offer(M1, G3["ABC"](1000), XRP(10000)), domain(*domainID)); + } + else + { + env(offer(M1, G3["ABC"](1000), XRP(10000))); + } STPathSet st; STAmount sa, da; - auto const& send_amt = XRP(10); - std::tie(st, sa, da) = - find_paths(env, A1, A2, send_amt, std::nullopt, A2["ABC"].currency); - BEAST_EXPECT(equal(da, send_amt)); - BEAST_EXPECT(equal(sa, A1["ABC"](1))); - BEAST_EXPECT(same(st, stpath(G3, IPE(xrpIssue())))); + + { + std::tie(st, sa, da) = find_paths( + env, + A1, + A2, + send_amt, + std::nullopt, + A2["ABC"].currency, + domainID); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, A1["ABC"](1))); + BEAST_EXPECT(same(st, stpath(G3, IPE(xrpIssue())))); + } + + // domain offer will not be considered in pathfinding for non-domain + // paths + if (domainEnabled) + { + std::tie(st, sa, da) = find_paths( + env, A1, A2, send_amt, std::nullopt, A2["ABC"].currency); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(st.empty()); + } } void - path_find_04() + path_find_04(bool const domainEnabled) { - testcase("Path Find: Bitstamp and SnapSwap, liquidity with no offers"); + testcase( + std::string( + "Path Find: Bitstamp and SnapSwap, liquidity with no offers") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; @@ -1019,13 +1253,23 @@ public: env(pay(G2SW, M1, G2SW["HKD"](5000))); env.close(); + std::optional domainID; + if (domainEnabled) + domainID = setupDomain(env, {A1, A2, G1BS, G2SW, M1}); + STPathSet st; STAmount sa, da; { auto const& send_amt = A2["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A1, A2, send_amt, std::nullopt, A2["HKD"].currency); + env, + A1, + A2, + send_amt, + std::nullopt, + A2["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same(st, stpath(G1BS, M1, G2SW))); @@ -1034,7 +1278,13 @@ public: { auto const& send_amt = A1["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A2, A1, send_amt, std::nullopt, A1["HKD"].currency); + env, + A2, + A1, + send_amt, + std::nullopt, + A1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A2["HKD"](10))); BEAST_EXPECT(same(st, stpath(G2SW, M1, G1BS))); @@ -1043,7 +1293,13 @@ public: { auto const& send_amt = A2["HKD"](10); std::tie(st, sa, da) = find_paths( - env, G1BS, A2, send_amt, std::nullopt, A1["HKD"].currency); + env, + G1BS, + A2, + send_amt, + std::nullopt, + A1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G1BS["HKD"](10))); BEAST_EXPECT(same(st, stpath(M1, G2SW))); @@ -1052,7 +1308,13 @@ public: { auto const& send_amt = M1["HKD"](10); std::tie(st, sa, da) = find_paths( - env, M1, G1BS, send_amt, std::nullopt, A1["HKD"].currency); + env, + M1, + G1BS, + send_amt, + std::nullopt, + A1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, M1["HKD"](10))); BEAST_EXPECT(st.empty()); @@ -1061,7 +1323,13 @@ public: { auto const& send_amt = A1["HKD"](10); std::tie(st, sa, da) = find_paths( - env, G2SW, A1, send_amt, std::nullopt, A1["HKD"].currency); + env, + G2SW, + A1, + send_amt, + std::nullopt, + A1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G2SW["HKD"](10))); BEAST_EXPECT(same(st, stpath(M1, G1BS))); @@ -1069,9 +1337,11 @@ public: } void - path_find_05() + path_find_05(bool const domainEnabled) { - testcase("Path Find: non-XRP -> non-XRP, same currency"); + testcase( + std::string("Path Find: non-XRP -> non-XRP, same currency") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; @@ -1108,9 +1378,21 @@ public: env(pay(G2, M2, G2["HKD"](5000))); env.close(); - env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); - env(offer(M2, XRP(10000), G2["HKD"](1000))); - env(offer(M2, G1["HKD"](1000), XRP(10000))); + std::optional domainID; + if (domainEnabled) + { + domainID = + setupDomain(env, {A1, A2, A3, A4, G1, G2, G3, G4, M1, M2}); + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(*domainID)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), domain(*domainID)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), domain(*domainID)); + } + else + { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); + env(offer(M2, XRP(10000), G2["HKD"](1000))); + env(offer(M2, G1["HKD"](1000), XRP(10000))); + } STPathSet st; STAmount sa, da; @@ -1120,7 +1402,13 @@ public: // Source -> Destination (repay source issuer) auto const& send_amt = G1["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A1, G1, send_amt, std::nullopt, G1["HKD"].currency); + env, + A1, + G1, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainID); BEAST_EXPECT(st.empty()); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); @@ -1131,7 +1419,13 @@ public: // Source -> Destination (repay destination issuer) auto const& send_amt = A1["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A1, G1, send_amt, std::nullopt, G1["HKD"].currency); + env, + A1, + G1, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainID); BEAST_EXPECT(st.empty()); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); @@ -1142,7 +1436,13 @@ public: // Source -> AC -> Destination auto const& send_amt = A3["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A1, A3, send_amt, std::nullopt, G1["HKD"].currency); + env, + A1, + A3, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same(st, stpath(G1))); @@ -1153,7 +1453,13 @@ public: // Source -> OB -> Destination auto const& send_amt = G2["HKD"](10); std::tie(st, sa, da) = find_paths( - env, G1, G2, send_amt, std::nullopt, G1["HKD"].currency); + env, + G1, + G2, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G1["HKD"](10))); BEAST_EXPECT(same( @@ -1169,7 +1475,13 @@ public: // Source -> AC -> OB -> Destination auto const& send_amt = G2["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A1, G2, send_amt, std::nullopt, G1["HKD"].currency); + env, + A1, + G2, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same( @@ -1182,10 +1494,17 @@ public: { // I4) XRP bridge" -- - // Source -> AC -> OB to XRP -> OB from XRP -> AC -> Destination + // Source -> AC -> OB to XRP -> OB from XRP -> AC -> + // Destination auto const& send_amt = A2["HKD"](10); std::tie(st, sa, da) = find_paths( - env, A1, A2, send_amt, std::nullopt, G1["HKD"].currency); + env, + A1, + A2, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, A1["HKD"](10))); BEAST_EXPECT(same( @@ -1198,9 +1517,11 @@ public: } void - path_find_06() + path_find_06(bool const domainEnabled) { - testcase("Path Find: non-XRP -> non-XRP, same currency)"); + testcase( + std::string("Path Find: non-XRP -> non-XRP, same currency)") + + (domainEnabled ? " w/ " : " w/o ") + "domain"); using namespace jtx; Env env = pathTestEnv(); Account A1{"A1"}; @@ -1227,24 +1548,36 @@ public: env(pay(G2, M1, G2["HKD"](5000))); env.close(); - env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {A1, A2, A3, G1, G2, M1}); + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), domain(*domainID)); + } + else + { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); + } // E) Gateway to user // Source -> OB -> AC -> Destination auto const& send_amt = A2["HKD"](10); STPathSet st; STAmount sa, da; - std::tie(st, sa, da) = - find_paths(env, G1, A2, send_amt, std::nullopt, G1["HKD"].currency); + std::tie(st, sa, da) = find_paths( + env, G1, A2, send_amt, std::nullopt, G1["HKD"].currency, domainID); BEAST_EXPECT(equal(da, send_amt)); BEAST_EXPECT(equal(sa, G1["HKD"](10))); BEAST_EXPECT(same(st, stpath(M1, G2), stpath(IPE(G2["HKD"]), G2))); } void - receive_max() + receive_max(bool const domainEnabled) { - testcase("Receive max"); + testcase( + std::string("Receive max") + (domainEnabled ? " w/ " : " w/o ") + + "domain"); + using namespace jtx; auto const alice = Account("alice"); auto const bob = Account("bob"); @@ -1260,10 +1593,28 @@ public: env.close(); env(pay(gw, charlie, USD(10))); env.close(); - env(offer(charlie, XRP(10), USD(10))); - env.close(); - auto [st, sa, da] = - find_paths(env, alice, bob, USD(-1), XRP(100).value()); + + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {alice, bob, charlie, gw}); + env(offer(charlie, XRP(10), USD(10)), domain(*domainID)); + env.close(); + } + else + { + env(offer(charlie, XRP(10), USD(10))); + env.close(); + } + + auto [st, sa, da] = find_paths( + env, + alice, + bob, + USD(-1), + XRP(100).value(), + std::nullopt, + domainID); BEAST_EXPECT(sa == XRP(10)); BEAST_EXPECT(equal(da, USD(10))); if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1)) @@ -1283,10 +1634,28 @@ public: env.close(); env(pay(gw, alice, USD(10))); env.close(); - env(offer(charlie, USD(10), XRP(10))); - env.close(); - auto [st, sa, da] = - find_paths(env, alice, bob, drops(-1), USD(100).value()); + + std::optional domainID; + if (domainEnabled) + { + domainID = setupDomain(env, {alice, bob, charlie, gw}); + env(offer(charlie, USD(10), XRP(10)), domain(*domainID)); + env.close(); + } + else + { + env(offer(charlie, USD(10), XRP(10))); + env.close(); + } + + auto [st, sa, da] = find_paths( + env, + alice, + bob, + drops(-1), + USD(100).value(), + std::nullopt, + domainID); BEAST_EXPECT(sa == USD(10)); BEAST_EXPECT(equal(da, XRP(10))); if (BEAST_EXPECT(st.size() == 1 && st[0].size() == 1)) @@ -1363,6 +1732,360 @@ public: test("no ripple -> no ripple", false, false, false); } + void + hybrid_offer_path() + { + testcase("Hybrid offer path"); + using namespace jtx; + + // test cases copied from path_find_05 and ensures path results for + // different combinations of open/domain/hybrid offers. `func` is a + // lambda param that creates different types of offers + auto testPathfind = [&](auto func, bool const domainEnabled = false) { + Env env = pathTestEnv(); + Account A1{"A1"}; + Account A2{"A2"}; + Account A3{"A3"}; + Account A4{"A4"}; + Account G1{"G1"}; + Account G2{"G2"}; + Account G3{"G3"}; + Account G4{"G4"}; + Account M1{"M1"}; + Account M2{"M2"}; + + env.fund(XRP(1000), A1, A2, A3, G1, G2, G3, G4); + env.fund(XRP(10000), A4); + env.fund(XRP(11000), M1, M2); + env.close(); + + env.trust(G1["HKD"](2000), A1); + env.trust(G2["HKD"](2000), A2); + env.trust(G1["HKD"](2000), A3); + env.trust(G1["HKD"](100000), M1); + env.trust(G2["HKD"](100000), M1); + env.trust(G1["HKD"](100000), M2); + env.trust(G2["HKD"](100000), M2); + env.close(); + + env(pay(G1, A1, G1["HKD"](1000))); + env(pay(G2, A2, G2["HKD"](1000))); + env(pay(G1, A3, G1["HKD"](1000))); + env(pay(G1, M1, G1["HKD"](1200))); + env(pay(G2, M1, G2["HKD"](5000))); + env(pay(G1, M2, G1["HKD"](1200))); + env(pay(G2, M2, G2["HKD"](5000))); + env.close(); + + std::optional domainID = + setupDomain(env, {A1, A2, A3, A4, G1, G2, G3, G4, M1, M2}); + BEAST_EXPECT(domainID); + + func(env, M1, M2, G1, G2, *domainID); + + STPathSet st; + STAmount sa, da; + + { + // A) Borrow or repay -- + // Source -> Destination (repay source issuer) + auto const& send_amt = G1["HKD"](10); + std::tie(st, sa, da) = find_paths( + env, + A1, + G1, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainEnabled ? domainID : std::nullopt); + BEAST_EXPECT(st.empty()); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, A1["HKD"](10))); + } + + { + // A2) Borrow or repay -- + // Source -> Destination (repay destination issuer) + auto const& send_amt = A1["HKD"](10); + std::tie(st, sa, da) = find_paths( + env, + A1, + G1, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainEnabled ? domainID : std::nullopt); + BEAST_EXPECT(st.empty()); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, A1["HKD"](10))); + } + + { + // B) Common gateway -- + // Source -> AC -> Destination + auto const& send_amt = A3["HKD"](10); + std::tie(st, sa, da) = find_paths( + env, + A1, + A3, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainEnabled ? domainID : std::nullopt); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, A1["HKD"](10))); + BEAST_EXPECT(same(st, stpath(G1))); + } + + { + // C) Gateway to gateway -- + // Source -> OB -> Destination + auto const& send_amt = G2["HKD"](10); + std::tie(st, sa, da) = find_paths( + env, + G1, + G2, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainEnabled ? domainID : std::nullopt); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, G1["HKD"](10))); + BEAST_EXPECT(same( + st, + stpath(IPE(G2["HKD"])), + stpath(M1), + stpath(M2), + stpath(IPE(xrpIssue()), IPE(G2["HKD"])))); + } + + { + // D) User to unlinked gateway via order book -- + // Source -> AC -> OB -> Destination + auto const& send_amt = G2["HKD"](10); + std::tie(st, sa, da) = find_paths( + env, + A1, + G2, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainEnabled ? domainID : std::nullopt); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, A1["HKD"](10))); + BEAST_EXPECT(same( + st, + stpath(G1, M1), + stpath(G1, M2), + stpath(G1, IPE(G2["HKD"])), + stpath(G1, IPE(xrpIssue()), IPE(G2["HKD"])))); + } + + { + // I4) XRP bridge" -- + // Source -> AC -> OB to XRP -> OB from XRP -> AC -> + // Destination + auto const& send_amt = A2["HKD"](10); + std::tie(st, sa, da) = find_paths( + env, + A1, + A2, + send_amt, + std::nullopt, + G1["HKD"].currency, + domainEnabled ? domainID : std::nullopt); + BEAST_EXPECT(equal(da, send_amt)); + BEAST_EXPECT(equal(sa, A1["HKD"](10))); + BEAST_EXPECT(same( + st, + stpath(G1, M1, G2), + stpath(G1, M2, G2), + stpath(G1, IPE(G2["HKD"]), G2), + stpath(G1, IPE(xrpIssue()), IPE(G2["HKD"]), G2))); + } + }; + + // the following tests exercise different combinations of open/hybrid + // offers to make sure that hybrid offers work in pathfinding for open + // order book + { + testPathfind([](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, XRP(10000), G2["HKD"](1000))); + env(offer(M2, G1["HKD"](1000), XRP(10000))); + }); + + testPathfind([](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, G1["HKD"](1000), XRP(10000))); + }); + + testPathfind([](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID), + txflags(tfHybrid)); + }); + + testPathfind([](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); + env(offer(M2, XRP(10000), G2["HKD"](1000))); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID), + txflags(tfHybrid)); + }); + + testPathfind([](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000))); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID), + txflags(tfHybrid)); + }); + } + + // the following tests exercise different combinations of domain/hybrid + // offers to make sure that hybrid offers work in pathfinding for domain + // order book + { + testPathfind( + [](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID)); + }, + true); + + testPathfind( + [](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID)); + }, + true); + + testPathfind( + [](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID), + txflags(tfHybrid)); + }, + true); + + testPathfind( + [](Env& env, + Account M1, + Account M2, + Account G1, + Account G2, + uint256 domainID) { + env(offer(M1, G1["HKD"](1000), G2["HKD"](1000)), + domain(domainID)); + env(offer(M2, XRP(10000), G2["HKD"](1000)), + domain(domainID), + txflags(tfHybrid)); + env(offer(M2, G1["HKD"](1000), XRP(10000)), + domain(domainID), + txflags(tfHybrid)); + }, + true); + } + } + + void + amm_domain_path() + { + testcase("AMM not used in domain path"); + using namespace jtx; + Env env = pathTestEnv(); + PermissionedDEX permDex(env); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + permDex; + AMM amm(env, alice, XRP(10), USD(50)); + + STPathSet st; + STAmount sa, da; + + auto const& send_amt = XRP(1); + + // doing pathfind with domain won't include amm + std::tie(st, sa, da) = find_paths( + env, bob, carol, send_amt, std::nullopt, USD.currency, domainID); + BEAST_EXPECT(st.empty()); + + // a non-domain pathfind returns amm in the path + std::tie(st, sa, da) = + find_paths(env, bob, carol, send_amt, std::nullopt, USD.currency); + BEAST_EXPECT(same(st, stpath(gw, IPE(xrpIssue())))); + } + void run() override { @@ -1370,35 +2093,43 @@ public: no_direct_path_no_intermediary_no_alternatives(); direct_path_no_intermediary(); payment_auto_path_find(); - path_find(); - path_find_consume_all(); - alternative_path_consume_both(); - alternative_paths_consume_best_transfer(); + indirect_paths_path_find(); alternative_paths_consume_best_transfer_first(); - alternative_paths_limit_returned_paths_to_best_quality(); - issues_path_negative_issue(); issues_path_negative_ripple_client_issue_23_smaller(); issues_path_negative_ripple_client_issue_23_larger(); - via_offers_via_gateway(); - indirect_paths_path_find(); quality_paths_quality_set_and_test(); trust_auto_clear_trust_normal_clear(); trust_auto_clear_trust_auto_clear(); - xrp_to_xrp(); - receive_max(); noripple_combinations(); - // The following path_find_NN tests are data driven tests - // that were originally implemented in js/coffee and migrated - // here. The quantities and currencies used are taken directly from - // those legacy tests, which in some cases probably represented - // customer use cases. + for (bool const domainEnabled : {false, true}) + { + path_find(domainEnabled); + path_find_consume_all(domainEnabled); + alternative_path_consume_both(domainEnabled); + alternative_paths_consume_best_transfer(domainEnabled); + alternative_paths_limit_returned_paths_to_best_quality( + domainEnabled); + issues_path_negative_issue(domainEnabled); + via_offers_via_gateway(domainEnabled); + xrp_to_xrp(domainEnabled); + receive_max(domainEnabled); - path_find_01(); - path_find_02(); - path_find_04(); - path_find_05(); - path_find_06(); + // The following path_find_NN tests are data driven tests + // that were originally implemented in js/coffee and migrated + // here. The quantities and currencies used are taken directly from + // those legacy tests, which in some cases probably represented + // customer use cases. + + path_find_01(domainEnabled); + path_find_02(domainEnabled); + path_find_04(domainEnabled); + path_find_05(domainEnabled); + path_find_06(domainEnabled); + } + + hybrid_offer_path(); + amm_domain_path(); } }; diff --git a/src/test/app/PayStrand_test.cpp b/src/test/app/PayStrand_test.cpp index 4d743d9d7c..9188da62ac 100644 --- a/src/test/app/PayStrand_test.cpp +++ b/src/test/app/PayStrand_test.cpp @@ -27,6 +27,9 @@ #include #include #include +#include + +#include namespace ripple { namespace test { @@ -656,6 +659,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, env.app().logs().journal("Flow")); BEAST_EXPECT(ter == expTer); if (sizeof...(expSteps) != 0) @@ -684,6 +688,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, env.app().logs().journal("Flow")); (void)_; BEAST_EXPECT(ter == tesSUCCESS); @@ -701,6 +706,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, env.app().logs().journal("Flow")); (void)_; BEAST_EXPECT(ter == tesSUCCESS); @@ -738,7 +744,7 @@ struct PayStrand_test : public beast::unit_test::suite STPath(), tesSUCCESS, D{alice, gw, usdC}, - B{USD, EUR}, + B{USD, EUR, std::nullopt}, D{gw, bob, eurC}); // Path with explicit offer @@ -749,7 +755,7 @@ struct PayStrand_test : public beast::unit_test::suite STPath({ipe(EUR)}), tesSUCCESS, D{alice, gw, usdC}, - B{USD, EUR}, + B{USD, EUR, std::nullopt}, D{gw, bob, eurC}); // Path with offer that changes issuer only @@ -761,7 +767,7 @@ struct PayStrand_test : public beast::unit_test::suite STPath({iape(carol)}), tesSUCCESS, D{alice, gw, usdC}, - B{USD, carol["USD"]}, + B{USD, carol["USD"], std::nullopt}, D{carol, bob, usdC}); // Path with XRP src currency @@ -772,7 +778,7 @@ struct PayStrand_test : public beast::unit_test::suite STPath({ipe(USD)}), tesSUCCESS, XRPS{alice}, - B{XRP, USD}, + B{XRP, USD, std::nullopt}, D{gw, bob, usdC}); // Path with XRP dst currency. @@ -787,7 +793,7 @@ struct PayStrand_test : public beast::unit_test::suite xrpAccount()}}), tesSUCCESS, D{alice, gw, usdC}, - B{USD, XRP}, + B{USD, XRP, std::nullopt}, XRPS{bob}); // Path with XRP cross currency bridged payment @@ -798,8 +804,8 @@ struct PayStrand_test : public beast::unit_test::suite STPath({cpe(xrpCurrency())}), tesSUCCESS, D{alice, gw, usdC}, - B{USD, XRP}, - B{XRP, EUR}, + B{USD, XRP, std::nullopt}, + B{XRP, EUR, std::nullopt}, D{gw, bob, eurC}); // XRP -> XRP transaction can't include a path @@ -821,6 +827,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, flowJournal); BEAST_EXPECT(r.first == temBAD_PATH); } @@ -837,6 +844,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, flowJournal); BEAST_EXPECT(r.first == temBAD_PATH); } @@ -853,6 +861,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, flowJournal); BEAST_EXPECT(r.first == temBAD_PATH); } @@ -990,6 +999,7 @@ struct PayStrand_test : public beast::unit_test::suite true, OfferCrossing::no, ammContext, + std::nullopt, env.app().logs().journal("Flow")); BEAST_EXPECT(ter == tesSUCCESS); BEAST_EXPECT(equal(strand, D{alice, gw, usdC})); @@ -1017,12 +1027,13 @@ struct PayStrand_test : public beast::unit_test::suite false, OfferCrossing::no, ammContext, + std::nullopt, env.app().logs().journal("Flow")); BEAST_EXPECT(ter == tesSUCCESS); BEAST_EXPECT(equal( strand, D{alice, gw, usdC}, - B{USD.issue(), xrpIssue()}, + B{USD.issue(), xrpIssue(), std::nullopt}, XRPS{bob})); } } @@ -1201,6 +1212,7 @@ struct PayStrand_test : public beast::unit_test::suite dstAcc, noAccount(), pathSet, + std::nullopt, env.app().logs(), &inputs); BEAST_EXPECT(r.result() == temBAD_PATH); @@ -1213,6 +1225,7 @@ struct PayStrand_test : public beast::unit_test::suite noAccount(), srcAcc, pathSet, + std::nullopt, env.app().logs(), &inputs); BEAST_EXPECT(r.result() == temBAD_PATH); @@ -1225,6 +1238,7 @@ struct PayStrand_test : public beast::unit_test::suite dstAcc, srcAcc, pathSet, + std::nullopt, env.app().logs(), &inputs); BEAST_EXPECT(r.result() == temBAD_PATH); @@ -1237,6 +1251,7 @@ struct PayStrand_test : public beast::unit_test::suite dstAcc, srcAcc, pathSet, + std::nullopt, env.app().logs(), &inputs); BEAST_EXPECT(r.result() == temBAD_PATH); @@ -1253,13 +1268,16 @@ struct PayStrand_test : public beast::unit_test::suite { using namespace jtx; auto const sa = supported_amendments(); - testToStrand(sa - featureFlowCross); + testToStrand(sa - featureFlowCross - featurePermissionedDEX); + testToStrand(sa - featurePermissionedDEX); testToStrand(sa); - testRIPD1373(sa - featureFlowCross); + testRIPD1373(sa - featureFlowCross - featurePermissionedDEX); + testRIPD1373(sa - featurePermissionedDEX); testRIPD1373(sa); - testLoop(sa - featureFlowCross); + testLoop(sa - featureFlowCross - featurePermissionedDEX); + testLoop(sa - featurePermissionedDEX); testLoop(sa); testNoAccount(sa); diff --git a/src/test/app/PermissionedDEX_test.cpp b/src/test/app/PermissionedDEX_test.cpp new file mode 100644 index 0000000000..693381debf --- /dev/null +++ b/src/test/app/PermissionedDEX_test.cpp @@ -0,0 +1,1595 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +using namespace jtx; + +class PermissionedDEX_test : public beast::unit_test::suite +{ + [[nodiscard]] bool + offerExists(Env const& env, Account const& account, std::uint32_t offerSeq) + { + return static_cast(env.le(keylet::offer(account.id(), offerSeq))); + } + + [[nodiscard]] bool + checkOffer( + Env const& env, + Account const& account, + std::uint32_t offerSeq, + STAmount const& takerPays, + STAmount const& takerGets, + uint32_t const flags = 0, + bool const domainOffer = false) + { + auto offerInDir = [&](uint256 const& directory, + uint64_t const pageIndex, + std::optional domain = + std::nullopt) -> bool { + auto const page = env.le(keylet::page(directory, pageIndex)); + if (!page) + return false; + + if (domain != (*page)[~sfDomainID]) + return false; + + auto const& indexes = page->getFieldV256(sfIndexes); + for (auto const& index : indexes) + { + if (index == keylet::offer(account, offerSeq).key) + return true; + } + + return false; + }; + + auto const sle = env.le(keylet::offer(account.id(), offerSeq)); + if (!sle) + return false; + if (sle->getFieldAmount(sfTakerGets) != takerGets) + return false; + if (sle->getFieldAmount(sfTakerPays) != takerPays) + return false; + if (sle->getFlags() != flags) + return false; + if (domainOffer && !sle->isFieldPresent(sfDomainID)) + return false; + if (!domainOffer && sle->isFieldPresent(sfDomainID)) + return false; + if (!offerInDir( + sle->getFieldH256(sfBookDirectory), + sle->getFieldU64(sfBookNode), + (*sle)[~sfDomainID])) + return false; + + if (sle->isFlag(lsfHybrid)) + { + if (!sle->isFieldPresent(sfDomainID)) + return false; + if (!sle->isFieldPresent(sfAdditionalBooks)) + return false; + if (sle->getFieldArray(sfAdditionalBooks).size() != 1) + return false; + + auto const& additionalBookDirs = + sle->getFieldArray(sfAdditionalBooks); + + for (auto const& bookDir : additionalBookDirs) + { + auto const& dirIndex = bookDir.getFieldH256(sfBookDirectory); + auto const& dirNode = bookDir.getFieldU64(sfBookNode); + + // the directory is for the open order book, so the dir + // doesn't have domainID + if (!offerInDir(dirIndex, dirNode, std::nullopt)) + return false; + } + } + else + { + if (sle->isFieldPresent(sfAdditionalBooks)) + return false; + } + + return true; + } + + uint256 + getBookDirKey( + Book const& book, + STAmount const& takerPays, + STAmount const& takerGets) + { + return keylet::quality( + keylet::book(book), getRate(takerGets, takerPays)) + .key; + } + + std::optional + getDefaultOfferDirKey( + Env const& env, + Account const& account, + std::uint32_t offerSeq) + { + if (auto const sle = env.le(keylet::offer(account.id(), offerSeq))) + return Keylet(ltDIR_NODE, (*sle)[sfBookDirectory]).key; + + return {}; + } + + [[nodiscard]] bool + checkDirectorySize(Env const& env, uint256 directory, std::uint32_t dirSize) + { + std::optional pageIndex{0}; + std::uint32_t dirCnt = 0; + + do + { + auto const page = env.le(keylet::page(directory, *pageIndex)); + if (!page) + break; + + pageIndex = (*page)[~sfIndexNext]; + dirCnt += (*page)[sfIndexes].size(); + + } while (pageIndex.value_or(0)); + + return dirCnt == dirSize; + } + + void + testOfferCreate(FeatureBitset features) + { + testcase("OfferCreate"); + + // test preflight + { + Env env(*this, features - featurePermissionedDEX); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), + domain(domainID), + ter(temDISABLED)); + env.close(); + + env.enableFeature(featurePermissionedDEX); + env.close(); + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + } + + // test preflight: permissioned dex cannot be used without enable + // flowcross + { + Env env(*this, features - featureFlowCross); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), + domain(domainID), + ter(temDISABLED)); + env.close(); + + env.enableFeature(featureFlowCross); + env.close(); + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + } + + // preclaim - someone outside of the domain cannot create domain offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // create devin account who is not part of the domain + Account devin("devin"); + env.fund(XRP(1000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + env(offer(devin, XRP(10), USD(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + + // domain owner also issues a credential for devin + env(credentials::create(devin, domainOwner, credType)); + env.close(); + + // devin still cannot create offer since he didn't accept credential + env(offer(devin, XRP(10), USD(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + env(offer(devin, XRP(10), USD(10)), domain(domainID)); + env.close(); + } + + // preclaim - someone with expired cred cannot create domain offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // create devin account who is not part of the domain + Account devin("devin"); + env.fund(XRP(1000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + auto jv = credentials::create(devin, domainOwner, credType); + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count(); + jv[sfExpiration.jsonName] = t + 20; + env(jv); + + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // devin can still create offer while his cred is not expired + env(offer(devin, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // time advance + env.close(std::chrono::seconds(20)); + + // devin cannot create offer with expired cred + env(offer(devin, XRP(10), USD(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + } + + // preclaim - cannot create an offer in a non existent domain + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + uint256 const badDomain{ + "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134" + "E5"}; + + env(offer(bob, XRP(10), USD(10)), + domain(badDomain), + ter(tecNO_PERMISSION)); + env.close(); + } + + // apply - offer can be created even if takergets issuer is not in + // domain + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(credentials::deleteCred( + domainOwner, gw, domainOwner, credType)); + env.close(); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + } + + // apply - offer can be created even if takerpays issuer is not in + // domain + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(credentials::deleteCred( + domainOwner, gw, domainOwner, credType)); + env.close(); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), XRP(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, USD(10), XRP(10), 0, true)); + } + + // apply - two domain offers cross with each other + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + + // a non domain offer cannot cross with domain offer + env(offer(carol, USD(10), XRP(10))); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); + } + + // apply - create lots of domain offers + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + std::vector offerSeqs; + offerSeqs.reserve(100); + + for (size_t i = 0; i <= 100; i++) + { + auto const bobOfferSeq{env.seq(bob)}; + offerSeqs.emplace_back(bobOfferSeq); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + } + + for (auto const offerSeq : offerSeqs) + { + env(offer_cancel(bob, offerSeq)); + env.close(); + BEAST_EXPECT(!offerExists(env, bob, offerSeq)); + } + } + } + + void + testPayment(FeatureBitset features) + { + testcase("Payment"); + + // test preflight - without enabling featurePermissionedDEX amendment + { + Env env(*this, features - featurePermissionedDEX); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(pay(bob, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(temDISABLED)); + env.close(); + + env.enableFeature(featurePermissionedDEX); + env.close(); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + env(pay(bob, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + } + + // preclaim - cannot send payment with non existent domain + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + uint256 const badDomain{ + "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E3370F3649CE134" + "E5"}; + + env(pay(bob, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(badDomain), + ter(tecNO_PERMISSION)); + env.close(); + } + + // preclaim - payment with non-domain destination fails + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // create devin account who is not part of the domain + Account devin("devin"); + env.fund(XRP(1000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + // devin is not part of domain + env(pay(alice, devin, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + + // domain owner also issues a credential for devin + env(credentials::create(devin, domainOwner, credType)); + env.close(); + + // devin has not yet accepted cred + env(pay(alice, devin, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // devin can now receive payment after he is in domain + env(pay(alice, devin, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + } + + // preclaim - non-domain sender cannot send payment + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // create devin account who is not part of the domain + Account devin("devin"); + env.fund(XRP(1000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + // devin tries to send domain payment + env(pay(devin, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + + // domain owner also issues a credential for devin + env(credentials::create(devin, domainOwner, credType)); + env.close(); + + // devin has not yet accepted cred + env(pay(devin, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(tecNO_PERMISSION)); + env.close(); + + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // devin can now send payment after he is in domain + env(pay(devin, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + } + + // apply - domain owner can always send and receive domain payment + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // domain owner can always be destination + env(pay(alice, domainOwner, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // domain owner can send + env(pay(domainOwner, alice, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + } + } + + void + testBookStep(FeatureBitset features) + { + testcase("Book step"); + + // test domain cross currency payment consuming one offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // create a regular offer without domain + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); + + auto const regularDirKey = + getDefaultOfferDirKey(env, bob, regularOfferSeq); + BEAST_EXPECT(regularDirKey); + BEAST_EXPECT(checkDirectorySize(env, *regularDirKey, 1)); + + // a domain payment cannot consume regular offers + env(pay(alice, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + + // create a domain offer + auto const domainOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT(checkOffer( + env, bob, domainOfferSeq, XRP(10), USD(10), 0, true)); + + auto const domainDirKey = + getDefaultOfferDirKey(env, bob, domainOfferSeq); + BEAST_EXPECT(domainDirKey); + BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 1)); + + // cross-currency permissioned payment consumed + // domain offer instead of regular offer + env(pay(alice, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + BEAST_EXPECT(!offerExists(env, bob, domainOfferSeq)); + BEAST_EXPECT( + checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); + + // domain directory is empty + BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 0)); + BEAST_EXPECT(checkDirectorySize(env, *regularDirKey, 1)); + } + + // test domain payment consuming two offers in the path + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const EUR = gw["EUR"]; + env.trust(EUR(1000), alice); + env.close(); + env.trust(EUR(1000), bob); + env.close(); + env.trust(EUR(1000), carol); + env.close(); + env(pay(gw, bob, EUR(100))); + env.close(); + + // create XRP/USD domain offer + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); + + // payment fail because there isn't eur offer + env(pay(alice, carol, EUR(10)), + path(~USD, ~EUR), + sendmax(XRP(10)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); + + // bob creates a regular USD/EUR offer + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), EUR(10))); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, regularOfferSeq, USD(10), EUR(10))); + + // alice tries to pay again, but still fails because the regular + // offer cannot be consumed + env(pay(alice, carol, EUR(10)), + path(~USD, ~EUR), + sendmax(XRP(10)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + + // bob creates a domain USD/EUR offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), EUR(10)), domain(domainID)); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, eurOfferSeq, USD(10), EUR(10), 0, true)); + + // alice successfully consume two domain offers: xrp/usd and usd/eur + env(pay(alice, carol, EUR(5)), + sendmax(XRP(5)), + domain(domainID), + path(~USD, ~EUR)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT( + checkOffer(env, bob, eurOfferSeq, USD(5), EUR(5), 0, true)); + + // alice successfully consume two domain offers and deletes them + // we compute path this time using `paths` + env(pay(alice, carol, EUR(5)), + sendmax(XRP(5)), + domain(domainID), + paths(XRP)); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, usdOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, eurOfferSeq)); + + // regular offer is not consumed + BEAST_EXPECT( + checkOffer(env, bob, regularOfferSeq, USD(10), EUR(10))); + } + + // domain payment cannot consume offer from another domain + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // Fund devin and create USD trustline + Account badDomainOwner("badDomainOwner"); + Account devin("devin"); + env.fund(XRP(1000), badDomainOwner, devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + auto const badCredType = "badCred"; + pdomain::Credentials credentials{{badDomainOwner, badCredType}}; + env(pdomain::setTx(badDomainOwner, credentials)); + + auto objects = pdomain::getObjects(badDomainOwner, env); + auto const badDomainID = objects.begin()->first; + + env(credentials::create(devin, badDomainOwner, badCredType)); + env.close(); + env(credentials::accept(devin, badDomainOwner, badCredType)); + + // devin creates a domain offer in another domain + env(offer(devin, XRP(10), USD(10)), domain(badDomainID)); + env.close(); + + // domain payment can't consume an offer from another domain + env(pay(alice, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + + // bob creates an offer under the right domain + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + + // domain payment now consumes from the right domain + env(pay(alice, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + } + + // sanity check: devin, who is part of the domain but doesn't have a + // trustline with USD issuer, can successfully make a payment using + // offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // fund devin but don't create a USD trustline with gateway + Account devin("devin"); + env.fund(XRP(1000), devin); + env.close(); + + // domain owner also issues a credential for devin + env(credentials::create(devin, domainOwner, credType)); + env.close(); + + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // successful payment because offer is consumed + env(pay(devin, alice, USD(10)), sendmax(XRP(10)), domain(domainID)); + env.close(); + } + + // offer becomes unfunded when offer owner's cred expires + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // create devin account who is not part of the domain + Account devin("devin"); + env.fund(XRP(1000), devin); + env.close(); + env.trust(USD(1000), devin); + env.close(); + env(pay(gw, devin, USD(100))); + env.close(); + + auto jv = credentials::create(devin, domainOwner, credType); + uint32_t const t = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count(); + jv[sfExpiration.jsonName] = t + 20; + env(jv); + + env(credentials::accept(devin, domainOwner, credType)); + env.close(); + + // devin can still create offer while his cred is not expired + auto const offerSeq{env.seq(devin)}; + env(offer(devin, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // devin's offer can still be consumed while his cred isn't expired + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID)); + env.close(); + BEAST_EXPECT( + checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true)); + + // advance time + env.close(std::chrono::seconds(20)); + + // devin's offer is unfunded now due to expired cred + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT( + checkOffer(env, devin, offerSeq, XRP(5), USD(5), 0, true)); + } + + // offer becomes unfunded when offer owner's cred is removed + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const offerSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // bob's offer can still be consumed while his cred exists + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID)); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true)); + + // remove bob's cred + env(credentials::deleteCred( + domainOwner, bob, domainOwner, credType)); + env.close(); + + // bob's offer is unfunded now due to expired cred + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, offerSeq, XRP(5), USD(5), 0, true)); + } + } + + void + testRippling(FeatureBitset features) + { + testcase("Rippling"); + + // test a non-domain account can still be part of rippling in a domain + // payment. If the domain wishes to control who is allowed to ripple + // through, they should set the rippling individually + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const EURA = alice["EUR"]; + auto const EURB = bob["EUR"]; + + env.trust(EURA(100), bob); + env.trust(EURB(100), carol); + env.close(); + + // remove bob from domain + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); + env.close(); + + // alice can still ripple through bob even though he's not part + // of the domain, this is intentional + env(pay(alice, carol, EURB(10)), paths(EURA), domain(domainID)); + env.close(); + env.require(balance(bob, EURA(10)), balance(carol, EURB(10))); + + // carol sets no ripple on bob + env(trust(carol, bob["EUR"](0), bob, tfSetNoRipple)); + env.close(); + + // payment no longer works because carol has no ripple on bob + env(pay(alice, carol, EURB(5)), + paths(EURA), + domain(domainID), + ter(tecPATH_DRY)); + env.close(); + env.require(balance(bob, EURA(10)), balance(carol, EURB(10))); + } + + void + testOfferTokenIssuerInDomain(FeatureBitset features) + { + testcase("Offer token issuer in domain"); + + // whether the issuer is in the domain should NOT affect whether an + // offer can be consumed in domain payment + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // create an xrp/usd offer with usd as takergets + auto const bobOffer1Seq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + // create an usd/xrp offer with usd as takerpays + auto const bobOffer2Seq{env.seq(bob)}; + env(offer(bob, USD(10), XRP(10)), domain(domainID), txflags(tfPassive)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOffer1Seq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(checkOffer( + env, bob, bobOffer2Seq, USD(10), XRP(10), lsfPassive, true)); + + // remove gateway from domain + env(credentials::deleteCred(domainOwner, gw, domainOwner, credType)); + env.close(); + + // payment succeeds even if issuer is not in domain + // xrp/usd offer is consumed + env(pay(alice, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + BEAST_EXPECT(!offerExists(env, bob, bobOffer1Seq)); + + // payment succeeds even if issuer is not in domain + // usd/xrp offer is consumed + env(pay(alice, carol, XRP(10)), + path(~XRP), + sendmax(USD(10)), + domain(domainID)); + env.close(); + BEAST_EXPECT(!offerExists(env, bob, bobOffer2Seq)); + } + + void + testRemoveUnfundedOffer(FeatureBitset features) + { + testcase("Remove unfunded offer"); + + // checking that an unfunded offer will be implictly removed by a + // successfuly payment tx + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, XRP(100), USD(100)), domain(domainID)); + env.close(); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(20), USD(20)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(20), USD(20), 0, true)); + BEAST_EXPECT( + checkOffer(env, alice, aliceOfferSeq, XRP(100), USD(100), 0, true)); + + auto const domainDirKey = getDefaultOfferDirKey(env, bob, bobOfferSeq); + BEAST_EXPECT(domainDirKey); + BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 2)); + + // remove alice from domain and thus alice's offer becomes unfunded + env(credentials::deleteCred(domainOwner, alice, domainOwner, credType)); + env.close(); + + env(pay(gw, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + + // alice's unfunded offer is removed implicitly + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(checkDirectorySize(env, *domainDirKey, 1)); + } + + void + testAmmNotUsed(FeatureBitset features) + { + testcase("AMM not used"); + + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + AMM amm(env, alice, XRP(10), USD(50)); + + // a domain payment isn't able to consume AMM + env(pay(bob, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + + // a non domain payment can use AMM + env(pay(bob, carol, USD(5)), path(~USD), sendmax(XRP(5))); + env.close(); + + // USD amount in AMM is changed + auto [xrp, usd, lpt] = amm.balances(XRP, USD); + BEAST_EXPECT(usd == USD(45)); + } + + void + testHybridOfferCreate(FeatureBitset features) + { + testcase("Hybrid offer create"); + + // test preflight - invalid hybrid flag + { + Env env(*this, features - featurePermissionedDEX); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + env(offer(bob, XRP(10), USD(10)), + domain(domainID), + txflags(tfHybrid), + ter(temDISABLED)); + env.close(); + + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + ter(temINVALID_FLAG)); + env.close(); + + env.enableFeature(featurePermissionedDEX); + env.close(); + + // hybrid offer must have domainID + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + ter(temINVALID_FLAG)); + env.close(); + + // hybrid offer must have domainID + auto const offerSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + domain(domainID)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, offerSeq, XRP(10), USD(10), lsfHybrid, true)); + } + + // apply - domain offer can cross with hybrid + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + domain(domainID)); + env.close(); + + BEAST_EXPECT(checkOffer( + env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); + } + + // apply - open offer can cross with hybrid + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + domain(domainID)); + env.close(); + + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + BEAST_EXPECT(checkOffer( + env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10))); + env.close(); + + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); + } + + // apply - by default, hybrid offer tries to cross with offers in the + // domain book + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, true)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + + // hybrid offer auto crosses with domain offer + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), + domain(domainID), + txflags(tfHybrid)); + env.close(); + + BEAST_EXPECT(!offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(ownerCount(env, alice) == 2); + } + + // apply - hybrid offer does not automatically cross with open offers + // because by default, it only tries to cross domain offers + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const bobOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(ownerCount(env, bob) == 3); + + // hybrid offer auto crosses with domain offer + auto const aliceOfferSeq{env.seq(alice)}; + env(offer(alice, USD(10), XRP(10)), + domain(domainID), + txflags(tfHybrid)); + env.close(); + + BEAST_EXPECT(offerExists(env, alice, aliceOfferSeq)); + BEAST_EXPECT(offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT( + checkOffer(env, bob, bobOfferSeq, XRP(10), USD(10), 0, false)); + BEAST_EXPECT(checkOffer( + env, alice, aliceOfferSeq, USD(10), XRP(10), lsfHybrid, true)); + BEAST_EXPECT(ownerCount(env, alice) == 3); + } + } + + void + testHybridInvalidOffer(FeatureBitset features) + { + testcase("Hybrid invalid offer"); + + // bob has a hybrid offer and then he is removed from domain. + // in this case, the hybrid offer will be considered as unfunded even in + // a regular payment + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(50), USD(50)), txflags(tfHybrid), domain(domainID)); + env.close(); + + // remove bob from domain + env(credentials::deleteCred(domainOwner, bob, domainOwner, credType)); + env.close(); + + // bob's hybrid offer is unfunded and can not be consumed in a domain + // payment + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + + // bob's unfunded hybrid offer can't be consumed even with a regular + // payment + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, hybridOfferSeq, XRP(50), USD(50), lsfHybrid, true)); + + // create a regular offer + auto const regularOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + BEAST_EXPECT(offerExists(env, bob, regularOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(10), USD(10))); + + auto const sleHybridOffer = + env.le(keylet::offer(bob.id(), hybridOfferSeq)); + BEAST_EXPECT(sleHybridOffer); + auto const openDir = + sleHybridOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256( + sfBookDirectory); + BEAST_EXPECT(checkDirectorySize(env, openDir, 2)); + + // this normal payment should consume the regular offer and remove the + // unfunded hybrid offer + env(pay(alice, carol, USD(5)), path(~USD), sendmax(XRP(5))); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); + BEAST_EXPECT(checkOffer(env, bob, regularOfferSeq, XRP(5), USD(5))); + BEAST_EXPECT(checkDirectorySize(env, openDir, 1)); + } + + void + testHybridBookStep(FeatureBitset features) + { + testcase("Hybrid book step"); + + // both non domain and domain payments can consume hybrid offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + domain(domainID)); + env.close(); + + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + + // hybrid offer can't be consumed since bob is not in domain anymore + env(pay(alice, carol, USD(5)), path(~USD), sendmax(XRP(5))); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); + } + + // someone from another domain can't cross hybrid if they specified + // wrong domainID + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + // Fund accounts + Account badDomainOwner("badDomainOwner"); + Account devin("devin"); + env.fund(XRP(1000), badDomainOwner, devin); + env.close(); + + auto const badCredType = "badCred"; + pdomain::Credentials credentials{{badDomainOwner, badCredType}}; + env(pdomain::setTx(badDomainOwner, credentials)); + + auto objects = pdomain::getObjects(badDomainOwner, env); + auto const badDomainID = objects.begin()->first; + + env(credentials::create(devin, badDomainOwner, badCredType)); + env.close(); + env(credentials::accept(devin, badDomainOwner, badCredType)); + env.close(); + + auto const hybridOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + domain(domainID)); + env.close(); + + // other domains can't consume the offer + env(pay(devin, badDomainOwner, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(badDomainID), + ter(tecPATH_DRY)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, hybridOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + + env(pay(alice, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, hybridOfferSeq, XRP(5), USD(5), lsfHybrid, true)); + + // hybrid offer can't be consumed since bob is not in domain anymore + env(pay(alice, carol, USD(5)), path(~USD), sendmax(XRP(5))); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, hybridOfferSeq)); + } + + // test domain payment consuming two offers w/ hybrid offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const EUR = gw["EUR"]; + env.trust(EUR(1000), alice); + env.close(); + env.trust(EUR(1000), bob); + env.close(); + env.trust(EUR(1000), carol); + env.close(); + env(pay(gw, bob, EUR(100))); + env.close(); + + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10)), domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); + + // payment fail because there isn't eur offer + env(pay(alice, carol, EUR(5)), + path(~USD, ~EUR), + sendmax(XRP(5)), + domain(domainID), + ter(tecPATH_PARTIAL)); + env.close(); + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, true)); + + // bob creates a hybrid eur offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), EUR(10)), + domain(domainID), + txflags(tfHybrid)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, eurOfferSeq, USD(10), EUR(10), lsfHybrid, true)); + + // alice successfully consume two domain offers: xrp/usd and usd/eur + env(pay(alice, carol, EUR(5)), + path(~USD, ~EUR), + sendmax(XRP(5)), + domain(domainID)); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, true)); + BEAST_EXPECT(checkOffer( + env, bob, eurOfferSeq, USD(5), EUR(5), lsfHybrid, true)); + } + + // test regular payment using a regular offer and a hybrid offer + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + auto const EUR = gw["EUR"]; + env.trust(EUR(1000), alice); + env.close(); + env.trust(EUR(1000), bob); + env.close(); + env.trust(EUR(1000), carol); + env.close(); + env(pay(gw, bob, EUR(100))); + env.close(); + + // bob creates a regular usd offer + auto const usdOfferSeq{env.seq(bob)}; + env(offer(bob, XRP(10), USD(10))); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(10), USD(10), 0, false)); + + // bob creates a hybrid eur offer + auto const eurOfferSeq{env.seq(bob)}; + env(offer(bob, USD(10), EUR(10)), + domain(domainID), + txflags(tfHybrid)); + env.close(); + BEAST_EXPECT(checkOffer( + env, bob, eurOfferSeq, USD(10), EUR(10), lsfHybrid, true)); + + // alice successfully consume two offers: xrp/usd and usd/eur + env(pay(alice, carol, EUR(5)), path(~USD, ~EUR), sendmax(XRP(5))); + env.close(); + + BEAST_EXPECT( + checkOffer(env, bob, usdOfferSeq, XRP(5), USD(5), 0, false)); + BEAST_EXPECT(checkOffer( + env, bob, eurOfferSeq, USD(5), EUR(5), lsfHybrid, true)); + } + } + + void + testHybridOfferDirectories(FeatureBitset features) + { + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + + std::vector offerSeqs; + offerSeqs.reserve(100); + + Book domainBook{Issue(XRP), Issue(USD), domainID}; + Book openBook{Issue(XRP), Issue(USD), std::nullopt}; + + auto const domainDir = getBookDirKey(domainBook, XRP(10), USD(10)); + auto const openDir = getBookDirKey(openBook, XRP(10), USD(10)); + + size_t dirCnt = 100; + + for (size_t i = 1; i <= dirCnt; i++) + { + auto const bobOfferSeq{env.seq(bob)}; + offerSeqs.emplace_back(bobOfferSeq); + env(offer(bob, XRP(10), USD(10)), + txflags(tfHybrid), + domain(domainID)); + env.close(); + + auto const sleOffer = env.le(keylet::offer(bob.id(), bobOfferSeq)); + BEAST_EXPECT(sleOffer); + BEAST_EXPECT(sleOffer->getFieldH256(sfBookDirectory) == domainDir); + BEAST_EXPECT( + sleOffer->getFieldArray(sfAdditionalBooks).size() == 1); + BEAST_EXPECT( + sleOffer->getFieldArray(sfAdditionalBooks)[0].getFieldH256( + sfBookDirectory) == openDir); + + BEAST_EXPECT(checkOffer( + env, bob, bobOfferSeq, XRP(10), USD(10), lsfHybrid, true)); + BEAST_EXPECT(checkDirectorySize(env, domainDir, i)); + BEAST_EXPECT(checkDirectorySize(env, openDir, i)); + } + + for (auto const offerSeq : offerSeqs) + { + env(offer_cancel(bob, offerSeq)); + env.close(); + dirCnt--; + BEAST_EXPECT(!offerExists(env, bob, offerSeq)); + BEAST_EXPECT(checkDirectorySize(env, domainDir, dirCnt)); + BEAST_EXPECT(checkDirectorySize(env, openDir, dirCnt)); + } + } + + void + testAutoBridge(FeatureBitset features) + { + testcase("Auto bridge"); + + Env env(*this, features); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + PermissionedDEX(env); + auto const EUR = gw["EUR"]; + + for (auto const& account : {alice, bob, carol}) + { + env(trust(account, EUR(10000))); + env.close(); + } + + env(pay(gw, carol, EUR(1))); + env.close(); + + auto const aliceOfferSeq{env.seq(alice)}; + auto const bobOfferSeq{env.seq(bob)}; + env(offer(alice, XRP(100), USD(1)), domain(domainID)); + env(offer(bob, EUR(1), XRP(100)), domain(domainID)); + env.close(); + + // carol's offer should cross bob and alice's offers due to auto + // bridging + auto const carolOfferSeq{env.seq(carol)}; + env(offer(carol, USD(1), EUR(1)), domain(domainID)); + env.close(); + + BEAST_EXPECT(!offerExists(env, bob, aliceOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, bobOfferSeq)); + BEAST_EXPECT(!offerExists(env, bob, carolOfferSeq)); + } + +public: + void + run() override + { + FeatureBitset const all{jtx::supported_amendments()}; + + // Test domain offer (w/o hyrbid) + testOfferCreate(all); + testPayment(all); + testBookStep(all); + testRippling(all); + testOfferTokenIssuerInDomain(all); + testRemoveUnfundedOffer(all); + testAmmNotUsed(all); + testAutoBridge(all); + + // Test hybrid offers + testHybridOfferCreate(all); + testHybridBookStep(all); + testHybridInvalidOffer(all); + testHybridOfferDirectories(all); + } +}; + +BEAST_DEFINE_TESTSUITE(PermissionedDEX, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/SetAuth_test.cpp b/src/test/app/SetAuth_test.cpp index e55fbc4d5d..a4c2df6228 100644 --- a/src/test/app/SetAuth_test.cpp +++ b/src/test/app/SetAuth_test.cpp @@ -75,7 +75,8 @@ struct SetAuth_test : public beast::unit_test::suite { using namespace jtx; auto const sa = supported_amendments(); - testAuth(sa - featureFlowCross); + testAuth(sa - featureFlowCross - featurePermissionedDEX); + testAuth(sa - featurePermissionedDEX); testAuth(sa); } }; diff --git a/src/test/app/TheoreticalQuality_test.cpp b/src/test/app/TheoreticalQuality_test.cpp index 0269d206cc..1b3e6d9a82 100644 --- a/src/test/app/TheoreticalQuality_test.cpp +++ b/src/test/app/TheoreticalQuality_test.cpp @@ -267,6 +267,7 @@ class TheoreticalQuality_test : public beast::unit_test::suite sb.rules().enabled(featureOwnerPaysFee), OfferCrossing::no, ammContext, + std::nullopt, dummyJ); BEAST_EXPECT(sr.first == tesSUCCESS); diff --git a/src/test/app/TrustAndBalance_test.cpp b/src/test/app/TrustAndBalance_test.cpp index 037a7e0d89..8f092a725f 100644 --- a/src/test/app/TrustAndBalance_test.cpp +++ b/src/test/app/TrustAndBalance_test.cpp @@ -481,7 +481,8 @@ public: using namespace test::jtx; auto const sa = supported_amendments(); - testWithFeatures(sa - featureFlowCross); + testWithFeatures(sa - featureFlowCross - featurePermissionedDEX); + testWithFeatures(sa - featurePermissionedDEX); testWithFeatures(sa); } }; 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 d6e1dfc73f..4d0baf5bd5 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -53,10 +53,11 @@ namespace ripple { -using namespace test::jtx; - class Vault_test : public beast::unit_test::suite { + using PrettyAsset = ripple::test::jtx::PrettyAsset; + using PrettyAmount = ripple::test::jtx::PrettyAmount; + static auto constexpr negativeAmount = [](PrettyAsset const& asset) -> PrettyAmount { return {STAmount{asset.raw(), 1ul, 0, true, STAmount::unchecked{}}, ""}; @@ -1210,6 +1211,7 @@ class Vault_test : public beast::unit_test::suite testCreateFailMPT() { using namespace test::jtx; + Env env{*this, supported_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; @@ -1231,6 +1233,7 @@ class Vault_test : public beast::unit_test::suite testNonTransferableShares() { using namespace test::jtx; + Env env{*this, supported_amendments() | featureSingleAssetVault}; Account issuer{"issuer"}; Account owner{"owner"}; @@ -1787,6 +1790,8 @@ class Vault_test : public beast::unit_test::suite void testWithIOU() { + using namespace test::jtx; + auto testCase = [&, this]( std::function 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); + } } { @@ -2455,6 +2532,8 @@ class Vault_test : public beast::unit_test::suite void testWithDomainCheckXRP() { + using namespace test::jtx; + testcase("private XRP vault"); Env env{*this, supported_amendments() | featureSingleAssetVault}; @@ -2586,6 +2665,8 @@ class Vault_test : public beast::unit_test::suite void testRPC() { + using namespace test::jtx; + testcase("RPC"); Env env{*this, supported_amendments() | featureSingleAssetVault}; Account const owner{"owner"}; diff --git a/src/test/jtx.h b/src/test/jtx.h index 6b73ca63ec..fa67780cbd 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -31,11 +31,14 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include #include #include @@ -50,6 +53,7 @@ #include #include #include +#include #include #include #include diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index 3a2171a420..452f504374 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -595,13 +595,16 @@ public: } /** Return metadata for the last JTx. - - Effects: - - The open ledger is closed as if by a call - to close(). The metadata for the last - transaction ID, if any, is returned. - */ + * + * NOTE: this has a side effect of closing the open ledger. + * The ledger will only be closed if it includes transactions. + * + * Effects: + * + * The open ledger is closed as if by a call + * to close(). The metadata for the last + * transaction ID, if any, is returned. + */ std::shared_ptr meta(); 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 0ff8e404fd..7b1a249a88 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -33,6 +34,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 { @@ -640,7 +649,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 @@ -656,6 +664,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); +} + /* LoanBroker */ /******************************************************************************/ 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/domain.h b/src/test/jtx/domain.h new file mode 100644 index 0000000000..4af270c1d0 --- /dev/null +++ b/src/test/jtx/domain.h @@ -0,0 +1,45 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 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. +*/ +//============================================================================== + +#pragma once + +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Set the domain on a JTx. */ +class domain +{ +private: + uint256 v_; + +public: + explicit domain(uint256 const& v) : v_(v) + { + } + + void + operator()(Env&, JTx& jt) const; +}; + +} // namespace jtx +} // namespace test +} // namespace ripple 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/Env.cpp b/src/test/jtx/impl/Env.cpp index 60ed546059..a6b17532dd 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -467,7 +467,12 @@ Env::postconditions( std::shared_ptr Env::meta() { - close(); + if (current()->txCount() != 0) + { + // close the ledger if it has not already been closed + // (metadata is not finalized until the ledger is closed) + close(); + } auto const item = closed()->txRead(txid_); return item.second; } diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 500d30fc1c..4c0c77dc1c 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -239,7 +239,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); @@ -251,7 +250,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; @@ -263,7 +261,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; @@ -284,7 +281,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); @@ -306,7 +302,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); @@ -326,7 +321,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/domain.cpp b/src/test/jtx/impl/domain.cpp new file mode 100644 index 0000000000..51adb4ce98 --- /dev/null +++ b/src/test/jtx/impl/domain.cpp @@ -0,0 +1,36 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 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 + +namespace ripple { +namespace test { +namespace jtx { + +void +domain::operator()(Env&, JTx& jt) const +{ + jt[sfDomainID.jsonName] = to_string(v_); +} + +} // namespace jtx +} // namespace test +} // namespace ripple 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 eb2e16f8c3..fc6163a2f3 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -65,18 +65,6 @@ signers(Account const& account, none_t) //------------------------------------------------------------------------------ -msig::msig(SField const* subField_, std::vector signers_) - : signers(std::move(signers_)), subField(subField_) -{ - // 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/paths.cpp b/src/test/jtx/impl/paths.cpp index 2a45909eb9..f230305469 100644 --- a/src/test/jtx/impl/paths.cpp +++ b/src/test/jtx/impl/paths.cpp @@ -23,6 +23,8 @@ #include +#include + namespace ripple { namespace test { namespace jtx { @@ -34,6 +36,18 @@ paths::operator()(Env& env, JTx& jt) const auto const from = env.lookup(jv[jss::Account].asString()); auto const to = env.lookup(jv[jss::Destination].asString()); auto const amount = amountFromJson(sfAmount, jv[jss::Amount]); + + std::optional domain; + if (jv.isMember(sfDomainID.jsonName)) + { + if (!jv[sfDomainID.jsonName].isString()) + return; + uint256 num; + auto const s = jv[sfDomainID.jsonName].asString(); + if (num.parseHex(s)) + domain = num; + } + Pathfinder pf( std::make_shared( env.current(), env.app().journal("RippleLineCache")), @@ -43,6 +57,7 @@ paths::operator()(Env& env, JTx& jt) const in_.account, amount, std::nullopt, + domain, env.app()); if (!pf.findPaths(depth_)) return; 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/permissioned_dex.cpp b/src/test/jtx/impl/permissioned_dex.cpp new file mode 100644 index 0000000000..04497ebbdc --- /dev/null +++ b/src/test/jtx/impl/permissioned_dex.cpp @@ -0,0 +1,85 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 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 + +namespace ripple { +namespace test { +namespace jtx { + +uint256 +setupDomain( + jtx::Env& env, + std::vector const& accounts, + jtx::Account const& domainOwner, + std::string const& credType) +{ + using namespace jtx; + env.fund(XRP(100000), domainOwner); + env.close(); + + pdomain::Credentials credentials{{domainOwner, credType}}; + env(pdomain::setTx(domainOwner, credentials)); + + auto const objects = pdomain::getObjects(domainOwner, env); + auto const domainID = objects.begin()->first; + + for (auto const& account : accounts) + { + env(credentials::create(account, domainOwner, credType)); + env.close(); + env(credentials::accept(account, domainOwner, credType)); + env.close(); + } + return domainID; +} + +PermissionedDEX::PermissionedDEX(Env& env) + : gw("permdex-gateway") + , domainOwner("permdex-domainOwner") + , alice("permdex-alice") + , bob("permdex-bob") + , carol("permdex-carol") + , USD(gw["USD"]) + , credType("permdex-abcde") +{ + // Fund accounts + env.fund(XRP(100000), alice, bob, carol, gw); + env.close(); + + domainID = setupDomain(env, {alice, bob, carol, gw}, domainOwner, credType); + + for (auto const& account : {alice, bob, carol, domainOwner}) + { + env.trust(USD(1000), account); + env.close(); + + env(pay(gw, account, USD(100))); + env.close(); + } +} + +} // namespace jtx +} // namespace test +} // namespace ripple 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 a364d07ca6..aedc660d92 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,42 +66,15 @@ 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; SField const* const subField = nullptr; static constexpr SField* const topLevel = nullptr; -public: - msig(SField const* subField_, std::vector signers_); + msig(SField const* subField_, std::vector signers_) + : signers(std::move(signers_)), subField(subField_) + { + sortSigners(signers); + } msig(SField const& subField_, std::vector signers_) : msig{&subField_, signers_} diff --git a/src/test/jtx/permissioned_dex.h b/src/test/jtx/permissioned_dex.h new file mode 100644 index 0000000000..fb32e1c1be --- /dev/null +++ b/src/test/jtx/permissioned_dex.h @@ -0,0 +1,51 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 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. +*/ +//============================================================================== + +#pragma once + +#include +namespace ripple { +namespace test { +namespace jtx { + +uint256 +setupDomain( + jtx::Env& env, + std::vector const& accounts, + jtx::Account const& domainOwner = jtx::Account("domainOwner"), + std::string const& credType = "Cred"); + +class PermissionedDEX +{ +public: + Account gw; + Account domainOwner; + Account alice; + Account bob; + Account carol; + IOU USD; + uint256 domainID; + std::string credType; + + PermissionedDEX(Env& env); +}; + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/ledger/BookDirs_test.cpp b/src/test/ledger/BookDirs_test.cpp index ed7ca91083..28d9d2c102 100644 --- a/src/test/ledger/BookDirs_test.cpp +++ b/src/test/ledger/BookDirs_test.cpp @@ -37,7 +37,7 @@ struct BookDirs_test : public beast::unit_test::suite env.close(); { - Book book(xrpIssue(), USD.issue()); + Book book(xrpIssue(), USD.issue(), std::nullopt); { auto d = BookDirs(*env.current(), book); BEAST_EXPECT(std::begin(d) == std::end(d)); @@ -53,14 +53,16 @@ struct BookDirs_test : public beast::unit_test::suite env(offer("alice", Account("alice")["USD"](50), XRP(10))); auto d = BookDirs( *env.current(), - Book(Account("alice")["USD"].issue(), xrpIssue())); + Book( + Account("alice")["USD"].issue(), xrpIssue(), std::nullopt)); BEAST_EXPECT(std::distance(d.begin(), d.end()) == 1); } { env(offer("alice", gw["CNY"](50), XRP(10))); - auto d = - BookDirs(*env.current(), Book(gw["CNY"].issue(), xrpIssue())); + auto d = BookDirs( + *env.current(), + Book(gw["CNY"].issue(), xrpIssue(), std::nullopt)); BEAST_EXPECT(std::distance(d.begin(), d.end()) == 1); } @@ -70,7 +72,7 @@ struct BookDirs_test : public beast::unit_test::suite env(offer("alice", USD(50), Account("bob")["CNY"](10))); auto d = BookDirs( *env.current(), - Book(USD.issue(), Account("bob")["CNY"].issue())); + Book(USD.issue(), Account("bob")["CNY"].issue(), std::nullopt)); BEAST_EXPECT(std::distance(d.begin(), d.end()) == 1); } @@ -80,7 +82,8 @@ struct BookDirs_test : public beast::unit_test::suite for (auto k = 0; k < 80; ++k) env(offer("alice", AUD(i), XRP(j))); - auto d = BookDirs(*env.current(), Book(AUD.issue(), xrpIssue())); + auto d = BookDirs( + *env.current(), Book(AUD.issue(), xrpIssue(), std::nullopt)); BEAST_EXPECT(std::distance(d.begin(), d.end()) == 240); auto i = 1, j = 3, k = 0; for (auto const& e : d) @@ -101,7 +104,8 @@ struct BookDirs_test : public beast::unit_test::suite { using namespace jtx; auto const sa = supported_amendments(); - test_bookdir(sa - featureFlowCross); + test_bookdir(sa - featureFlowCross - featurePermissionedDEX); + test_bookdir(sa - featurePermissionedDEX); test_bookdir(sa); } }; diff --git a/src/test/ledger/Directory_test.cpp b/src/test/ledger/Directory_test.cpp index 825d7ff340..7aa6f149b8 100644 --- a/src/test/ledger/Directory_test.cpp +++ b/src/test/ledger/Directory_test.cpp @@ -132,7 +132,8 @@ struct Directory_test : public beast::unit_test::suite // Now check the orderbook: it should be in the order we placed // the offers. - auto book = BookDirs(*env.current(), Book({xrpIssue(), USD.issue()})); + auto book = BookDirs( + *env.current(), Book({xrpIssue(), USD.issue(), std::nullopt})); int count = 1; for (auto const& offer : book) @@ -291,7 +292,8 @@ struct Directory_test : public beast::unit_test::suite // should have no entries and be empty: { Sandbox sb(env.closed().get(), tapNONE); - uint256 const bookBase = getBookBase({xrpIssue(), USD.issue()}); + uint256 const bookBase = + getBookBase({xrpIssue(), USD.issue(), std::nullopt}); BEAST_EXPECT(dirIsEmpty(sb, keylet::page(bookBase))); BEAST_EXPECT(!sb.succ(bookBase, getQualityNext(bookBase))); diff --git a/src/test/ledger/Invariants_test.cpp b/src/test/ledger/Invariants_test.cpp index 10e36f2f21..567d12ce22 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -1094,6 +1094,30 @@ class Invariants_test : public beast::unit_test::suite }); } + void + createPermissionedDomain( + ApplyContext& ac, + std::shared_ptr& sle, + test::jtx::Account const& A1, + test::jtx::Account const& A2) + { + sle->setAccountID(sfOwner, A1); + sle->setFieldU32(sfSequence, 10); + + STArray credentials(sfAcceptedCredentials, 2); + for (std::size_t n = 0; n < 2; ++n) + { + auto cred = STObject::makeInnerObject(sfCredential); + cred.setAccountID(sfIssuer, A2); + auto credType = "cred_type" + std::to_string(n); + cred.setFieldVL( + sfCredentialType, Slice(credType.c_str(), credType.size())); + credentials.push_back(std::move(cred)); + } + sle->setFieldArray(sfAcceptedCredentials, credentials); + ac.view().insert(sle); + }; + void testPermissionedDomainInvariants() { @@ -1201,36 +1225,15 @@ class Invariants_test : public beast::unit_test::suite STTx{ttPERMISSIONED_DOMAIN_SET, [](STObject& tx) {}}, {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); - auto const createPD = [](ApplyContext& ac, - std::shared_ptr& sle, - Account const& A1, - Account const& A2) { - sle->setAccountID(sfOwner, A1); - sle->setFieldU32(sfSequence, 10); - - STArray credentials(sfAcceptedCredentials, 2); - for (std::size_t n = 0; n < 2; ++n) - { - auto cred = STObject::makeInnerObject(sfCredential); - cred.setAccountID(sfIssuer, A2); - auto credType = "cred_type" + std::to_string(n); - cred.setFieldVL( - sfCredentialType, Slice(credType.c_str(), credType.size())); - credentials.push_back(std::move(cred)); - } - sle->setFieldArray(sfAcceptedCredentials, credentials); - ac.view().insert(sle); - }; - testcase << "PermissionedDomain Set 1"; doInvariantCheck( {{"permissioned domain with no rules."}}, - [createPD](Account const& A1, Account const& A2, ApplyContext& ac) { + [&](Account const& A1, Account const& A2, ApplyContext& ac) { Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); auto slePd = std::make_shared(pdKeylet); // create PD - createPD(ac, slePd, A1, A2); + createPermissionedDomain(ac, slePd, A1, A2); // update PD with empty rules { @@ -1249,12 +1252,12 @@ class Invariants_test : public beast::unit_test::suite doInvariantCheck( {{"permissioned domain bad credentials size " + std::to_string(tooBig)}}, - [createPD](Account const& A1, Account const& A2, ApplyContext& ac) { + [&](Account const& A1, Account const& A2, ApplyContext& ac) { Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); auto slePd = std::make_shared(pdKeylet); // create PD - createPD(ac, slePd, A1, A2); + createPermissionedDomain(ac, slePd, A1, A2); // update PD { @@ -1284,12 +1287,12 @@ class Invariants_test : public beast::unit_test::suite testcase << "PermissionedDomain Set 3"; doInvariantCheck( {{"permissioned domain credentials aren't sorted"}}, - [createPD](Account const& A1, Account const& A2, ApplyContext& ac) { + [&](Account const& A1, Account const& A2, ApplyContext& ac) { Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); auto slePd = std::make_shared(pdKeylet); // create PD - createPD(ac, slePd, A1, A2); + createPermissionedDomain(ac, slePd, A1, A2); // update PD { @@ -1319,12 +1322,12 @@ class Invariants_test : public beast::unit_test::suite testcase << "PermissionedDomain Set 4"; doInvariantCheck( {{"permissioned domain credentials aren't unique"}}, - [createPD](Account const& A1, Account const& A2, ApplyContext& ac) { + [&](Account const& A1, Account const& A2, ApplyContext& ac) { Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); auto slePd = std::make_shared(pdKeylet); // create PD - createPD(ac, slePd, A1, A2); + createPermissionedDomain(ac, slePd, A1, A2); // update PD { @@ -1348,6 +1351,175 @@ class Invariants_test : public beast::unit_test::suite {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); } + void + testPermissionedDEX() + { + using namespace test::jtx; + testcase << "PermissionedDEX"; + + doInvariantCheck( + {{"domain doesn't exist"}}, + [](Account const& A1, Account const&, ApplyContext& ac) { + Keylet const offerKey = keylet::offer(A1.id(), 10); + auto sleOffer = std::make_shared(offerKey); + sleOffer->setAccountID(sfAccount, A1); + sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10)); + sleOffer->setFieldAmount(sfTakerGets, XRP(1)); + ac.view().insert(sleOffer); + return true; + }, + XRPAmount{}, + STTx{ + ttOFFER_CREATE, + [](STObject& tx) { + tx.setFieldH256( + sfDomainID, + uint256{ + "F10D0CC9A0F9A3CBF585B80BE09A186483668FDBDD39AA7E33" + "70F3649CE134E5"}); + Account const A1{"A1"}; + tx.setFieldAmount(sfTakerPays, A1["USD"](10)); + tx.setFieldAmount(sfTakerGets, XRP(1)); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + // missing domain ID in offer object + doInvariantCheck( + {{"hybrid offer is malformed"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + createPermissionedDomain(ac, slePd, A1, A2); + + Keylet const offerKey = keylet::offer(A2.id(), 10); + auto sleOffer = std::make_shared(offerKey); + sleOffer->setAccountID(sfAccount, A2); + sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10)); + sleOffer->setFieldAmount(sfTakerGets, XRP(1)); + sleOffer->setFlag(lsfHybrid); + + STArray bookArr; + bookArr.push_back(STObject::makeInnerObject(sfBook)); + sleOffer->setFieldArray(sfAdditionalBooks, bookArr); + ac.view().insert(sleOffer); + return true; + }, + XRPAmount{}, + STTx{ttOFFER_CREATE, [&](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + // more than one entry in sfAdditionalBooks + doInvariantCheck( + {{"hybrid offer is malformed"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + createPermissionedDomain(ac, slePd, A1, A2); + + Keylet const offerKey = keylet::offer(A2.id(), 10); + auto sleOffer = std::make_shared(offerKey); + sleOffer->setAccountID(sfAccount, A2); + sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10)); + sleOffer->setFieldAmount(sfTakerGets, XRP(1)); + sleOffer->setFlag(lsfHybrid); + sleOffer->setFieldH256(sfDomainID, pdKeylet.key); + + STArray bookArr; + bookArr.push_back(STObject::makeInnerObject(sfBook)); + bookArr.push_back(STObject::makeInnerObject(sfBook)); + sleOffer->setFieldArray(sfAdditionalBooks, bookArr); + ac.view().insert(sleOffer); + return true; + }, + XRPAmount{}, + STTx{ttOFFER_CREATE, [&](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + // hybrid offer missing sfAdditionalBooks + doInvariantCheck( + {{"hybrid offer is malformed"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + createPermissionedDomain(ac, slePd, A1, A2); + + Keylet const offerKey = keylet::offer(A2.id(), 10); + auto sleOffer = std::make_shared(offerKey); + sleOffer->setAccountID(sfAccount, A2); + sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10)); + sleOffer->setFieldAmount(sfTakerGets, XRP(1)); + sleOffer->setFlag(lsfHybrid); + sleOffer->setFieldH256(sfDomainID, pdKeylet.key); + ac.view().insert(sleOffer); + return true; + }, + XRPAmount{}, + STTx{ttOFFER_CREATE, [&](STObject& tx) {}}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + doInvariantCheck( + {{"transaction consumed wrong domains"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + createPermissionedDomain(ac, slePd, A1, A2); + + Keylet const badDomainKeylet = + keylet::permissionedDomain(A1.id(), 20); + auto sleBadPd = std::make_shared(badDomainKeylet); + createPermissionedDomain(ac, sleBadPd, A1, A2); + + Keylet const offerKey = keylet::offer(A2.id(), 10); + auto sleOffer = std::make_shared(offerKey); + sleOffer->setAccountID(sfAccount, A2); + sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10)); + sleOffer->setFieldAmount(sfTakerGets, XRP(1)); + sleOffer->setFieldH256(sfDomainID, pdKeylet.key); + ac.view().insert(sleOffer); + return true; + }, + XRPAmount{}, + STTx{ + ttOFFER_CREATE, + [&](STObject& tx) { + Account const A1{"A1"}; + Keylet const badDomainKey = + keylet::permissionedDomain(A1.id(), 20); + tx.setFieldH256(sfDomainID, badDomainKey.key); + tx.setFieldAmount(sfTakerPays, A1["USD"](10)); + tx.setFieldAmount(sfTakerGets, XRP(1)); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + + doInvariantCheck( + {{"domain transaction affected regular offers"}}, + [&](Account const& A1, Account const& A2, ApplyContext& ac) { + Keylet const pdKeylet = keylet::permissionedDomain(A1.id(), 10); + auto slePd = std::make_shared(pdKeylet); + createPermissionedDomain(ac, slePd, A1, A2); + + Keylet const offerKey = keylet::offer(A2.id(), 10); + auto sleOffer = std::make_shared(offerKey); + sleOffer->setAccountID(sfAccount, A2); + sleOffer->setFieldAmount(sfTakerPays, A1["USD"](10)); + sleOffer->setFieldAmount(sfTakerGets, XRP(1)); + ac.view().insert(sleOffer); + return true; + }, + XRPAmount{}, + STTx{ + ttOFFER_CREATE, + [&](STObject& tx) { + Account const A1{"A1"}; + Keylet const domainKey = + keylet::permissionedDomain(A1.id(), 10); + tx.setFieldH256(sfDomainID, domainKey.key); + tx.setFieldAmount(sfTakerPays, A1["USD"](10)); + tx.setFieldAmount(sfTakerGets, XRP(1)); + }}, + {tecINVARIANT_FAILED, tecINVARIANT_FAILED}); + } + Keylet createLoanBroker( jtx::Account const& a, @@ -1830,6 +2002,7 @@ public: testValidNewAccountRoot(); testNFTokenPageInvariants(); testPermissionedDomainInvariants(); + testPermissionedDEX(); testNoModifiedUnmodifiableFields(); testValidPseudoAccounts(); testValidLoanBroker(); diff --git a/src/test/ledger/PaymentSandbox_test.cpp b/src/test/ledger/PaymentSandbox_test.cpp index 303e700f40..8bb0666e06 100644 --- a/src/test/ledger/PaymentSandbox_test.cpp +++ b/src/test/ledger/PaymentSandbox_test.cpp @@ -421,7 +421,8 @@ public: }; using namespace jtx; auto const sa = supported_amendments(); - testAll(sa - featureFlowCross); + testAll(sa - featureFlowCross - featurePermissionedDEX); + testAll(sa - featurePermissionedDEX); testAll(sa); } }; 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/protocol/Issue_test.cpp b/src/test/protocol/Issue_test.cpp index 53ebf5be24..35f3a3bd8c 100644 --- a/src/test/protocol/Issue_test.cpp +++ b/src/test/protocol/Issue_test.cpp @@ -22,7 +22,10 @@ #include #include +#include + #include +#include #include #include #include @@ -46,6 +49,8 @@ namespace ripple { class Issue_test : public beast::unit_test::suite { public: + using Domain = uint256; + // Comparison, hash tests for uint60 (via base_uint) template void @@ -239,6 +244,120 @@ public: } } + template + void + testIssueDomainSet() + { + Currency const c1(1); + AccountID const i1(1); + Currency const c2(2); + AccountID const i2(2); + Issue const a1(c1, i1); + Issue const a2(c2, i2); + uint256 const domain1{1}; + uint256 const domain2{2}; + + Set c; + + c.insert(std::make_pair(a1, domain1)); + if (!BEAST_EXPECT(c.size() == 1)) + return; + c.insert(std::make_pair(a2, domain1)); + if (!BEAST_EXPECT(c.size() == 2)) + return; + c.insert(std::make_pair(a2, domain2)); + if (!BEAST_EXPECT(c.size() == 3)) + return; + + if (!BEAST_EXPECT(c.erase(std::make_pair(Issue(c1, i2), domain1)) == 0)) + return; + if (!BEAST_EXPECT(c.erase(std::make_pair(a1, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(std::make_pair(a2, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(std::make_pair(a2, domain2)) == 1)) + return; + if (!BEAST_EXPECT(c.empty())) + return; + } + + template + void + testIssueDomainMap() + { + Currency const c1(1); + AccountID const i1(1); + Currency const c2(2); + AccountID const i2(2); + Issue const a1(c1, i1); + Issue const a2(c2, i2); + uint256 const domain1{1}; + uint256 const domain2{2}; + + Map c; + + c.insert(std::make_pair(std::make_pair(a1, domain1), 1)); + if (!BEAST_EXPECT(c.size() == 1)) + return; + c.insert(std::make_pair(std::make_pair(a2, domain1), 2)); + if (!BEAST_EXPECT(c.size() == 2)) + return; + c.insert(std::make_pair(std::make_pair(a2, domain2), 2)); + if (!BEAST_EXPECT(c.size() == 3)) + return; + + if (!BEAST_EXPECT(c.erase(std::make_pair(Issue(c1, i2), domain1)) == 0)) + return; + if (!BEAST_EXPECT(c.erase(std::make_pair(a1, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(std::make_pair(a2, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(std::make_pair(a2, domain2)) == 1)) + return; + if (!BEAST_EXPECT(c.empty())) + return; + } + + void + testIssueDomainSets() + { + testcase("std::set >"); + testIssueDomainSet>>(); + + testcase("std::set >"); + testIssueDomainSet>>(); + + testcase("hash_set >"); + testIssueDomainSet>>(); + + testcase("hash_set >"); + testIssueDomainSet>>(); + } + + void + testIssueDomainMaps() + { + testcase("std::map , int>"); + testIssueDomainMap, int>>(); + + testcase("std::map , int>"); + testIssueDomainMap, int>>(); + +#if RIPPLE_ASSETS_ENABLE_STD_HASH + testcase("hash_map , int>"); + testIssueDomainMap, int>>(); + + testcase("hash_map , int>"); + testIssueDomainMap, int>>(); + + testcase("hardened_hash_map , int>"); + testIssueDomainMap, int>>(); + + testcase("hardened_hash_map , int>"); + testIssueDomainMap, int>>(); +#endif + } + void testIssueSets() { @@ -306,15 +425,88 @@ public: Issue a2(c1, i2); Issue a3(c2, i2); Issue a4(c3, i2); + uint256 const domain1{1}; + uint256 const domain2{2}; - BEAST_EXPECT(Book(a1, a2) != Book(a2, a3)); - BEAST_EXPECT(Book(a1, a2) < Book(a2, a3)); - BEAST_EXPECT(Book(a1, a2) <= Book(a2, a3)); - BEAST_EXPECT(Book(a2, a3) <= Book(a2, a3)); - BEAST_EXPECT(Book(a2, a3) == Book(a2, a3)); - BEAST_EXPECT(Book(a2, a3) >= Book(a2, a3)); - BEAST_EXPECT(Book(a3, a4) >= Book(a2, a3)); - BEAST_EXPECT(Book(a3, a4) > Book(a2, a3)); + // Books without domains + BEAST_EXPECT(Book(a1, a2, std::nullopt) != Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a1, a2, std::nullopt) < Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a1, a2, std::nullopt) <= Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) <= Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) == Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) >= Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a3, a4, std::nullopt) >= Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a3, a4, std::nullopt) > Book(a2, a3, std::nullopt)); + + // test domain books + { + // Books with different domains + BEAST_EXPECT(Book(a2, a3, domain1) != Book(a2, a3, domain2)); + BEAST_EXPECT(Book(a2, a3, domain1) < Book(a2, a3, domain2)); + BEAST_EXPECT(Book(a2, a3, domain2) > Book(a2, a3, domain1)); + + // One Book has a domain, the other does not + BEAST_EXPECT(Book(a2, a3, domain1) != Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) < Book(a2, a3, domain1)); + BEAST_EXPECT(Book(a2, a3, domain1) > Book(a2, a3, std::nullopt)); + + // Both Books have the same domain + BEAST_EXPECT(Book(a2, a3, domain1) == Book(a2, a3, domain1)); + BEAST_EXPECT(Book(a2, a3, domain2) == Book(a2, a3, domain2)); + BEAST_EXPECT( + Book(a2, a3, std::nullopt) == Book(a2, a3, std::nullopt)); + + // Both Books have no domain + BEAST_EXPECT( + Book(a2, a3, std::nullopt) == Book(a2, a3, std::nullopt)); + + // Testing comparisons with >= and <= + + // When comparing books with domain1 vs domain2 + BEAST_EXPECT(Book(a2, a3, domain1) <= Book(a2, a3, domain2)); + BEAST_EXPECT(Book(a2, a3, domain2) >= Book(a2, a3, domain1)); + BEAST_EXPECT(Book(a2, a3, domain1) >= Book(a2, a3, domain1)); + BEAST_EXPECT(Book(a2, a3, domain2) <= Book(a2, a3, domain2)); + + // One Book has domain1 and the other has no domain + BEAST_EXPECT(Book(a2, a3, domain1) > Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) < Book(a2, a3, domain1)); + + // One Book has domain2 and the other has no domain + BEAST_EXPECT(Book(a2, a3, domain2) > Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) < Book(a2, a3, domain2)); + + // Comparing two Books with no domains + BEAST_EXPECT( + Book(a2, a3, std::nullopt) <= Book(a2, a3, std::nullopt)); + BEAST_EXPECT( + Book(a2, a3, std::nullopt) >= Book(a2, a3, std::nullopt)); + + // Test case where domain1 is less than domain2 + BEAST_EXPECT(Book(a2, a3, domain1) <= Book(a2, a3, domain2)); + BEAST_EXPECT(Book(a2, a3, domain2) >= Book(a2, a3, domain1)); + + // Test case where domain2 is equal to domain1 + BEAST_EXPECT(Book(a2, a3, domain1) >= Book(a2, a3, domain1)); + BEAST_EXPECT(Book(a2, a3, domain1) <= Book(a2, a3, domain1)); + + // More test cases involving a4 (with domain2) + + // Comparing Book with domain2 (a4) to a Book with domain1 + BEAST_EXPECT(Book(a2, a3, domain1) < Book(a3, a4, domain2)); + BEAST_EXPECT(Book(a3, a4, domain2) > Book(a2, a3, domain1)); + + // Comparing Book with domain2 (a4) to a Book with no domain + BEAST_EXPECT(Book(a3, a4, domain2) > Book(a2, a3, std::nullopt)); + BEAST_EXPECT(Book(a2, a3, std::nullopt) < Book(a3, a4, domain2)); + + // Comparing Book with domain2 (a4) to a Book with the same domain + BEAST_EXPECT(Book(a3, a4, domain2) == Book(a3, a4, domain2)); + + // Comparing Book with domain2 (a4) to a Book with domain1 + BEAST_EXPECT(Book(a2, a3, domain1) < Book(a3, a4, domain2)); + BEAST_EXPECT(Book(a3, a4, domain2) > Book(a2, a3, domain1)); + } std::hash hash; @@ -336,18 +528,99 @@ public: // log << std::hex << hash (Book (a3, a4)); // log << std::hex << hash (Book (a3, a4)); - BEAST_EXPECT(hash(Book(a1, a2)) == hash(Book(a1, a2))); - BEAST_EXPECT(hash(Book(a1, a3)) == hash(Book(a1, a3))); - BEAST_EXPECT(hash(Book(a1, a4)) == hash(Book(a1, a4))); - BEAST_EXPECT(hash(Book(a2, a3)) == hash(Book(a2, a3))); - BEAST_EXPECT(hash(Book(a2, a4)) == hash(Book(a2, a4))); - BEAST_EXPECT(hash(Book(a3, a4)) == hash(Book(a3, a4))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) == + hash(Book(a1, a2, std::nullopt))); + BEAST_EXPECT( + hash(Book(a1, a3, std::nullopt)) == + hash(Book(a1, a3, std::nullopt))); + BEAST_EXPECT( + hash(Book(a1, a4, std::nullopt)) == + hash(Book(a1, a4, std::nullopt))); + BEAST_EXPECT( + hash(Book(a2, a3, std::nullopt)) == + hash(Book(a2, a3, std::nullopt))); + BEAST_EXPECT( + hash(Book(a2, a4, std::nullopt)) == + hash(Book(a2, a4, std::nullopt))); + BEAST_EXPECT( + hash(Book(a3, a4, std::nullopt)) == + hash(Book(a3, a4, std::nullopt))); - BEAST_EXPECT(hash(Book(a1, a2)) != hash(Book(a1, a3))); - BEAST_EXPECT(hash(Book(a1, a2)) != hash(Book(a1, a4))); - BEAST_EXPECT(hash(Book(a1, a2)) != hash(Book(a2, a3))); - BEAST_EXPECT(hash(Book(a1, a2)) != hash(Book(a2, a4))); - BEAST_EXPECT(hash(Book(a1, a2)) != hash(Book(a3, a4))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) != + hash(Book(a1, a3, std::nullopt))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) != + hash(Book(a1, a4, std::nullopt))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) != + hash(Book(a2, a3, std::nullopt))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) != + hash(Book(a2, a4, std::nullopt))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) != + hash(Book(a3, a4, std::nullopt))); + + // Books with domain + BEAST_EXPECT( + hash(Book(a1, a2, domain1)) == hash(Book(a1, a2, domain1))); + BEAST_EXPECT( + hash(Book(a1, a3, domain1)) == hash(Book(a1, a3, domain1))); + BEAST_EXPECT( + hash(Book(a1, a4, domain1)) == hash(Book(a1, a4, domain1))); + BEAST_EXPECT( + hash(Book(a2, a3, domain1)) == hash(Book(a2, a3, domain1))); + BEAST_EXPECT( + hash(Book(a2, a4, domain1)) == hash(Book(a2, a4, domain1))); + BEAST_EXPECT( + hash(Book(a3, a4, domain1)) == hash(Book(a3, a4, domain1))); + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) == + hash(Book(a1, a2, std::nullopt))); + + // Comparing Books with domain1 vs no domain + BEAST_EXPECT( + hash(Book(a1, a2, std::nullopt)) != hash(Book(a1, a2, domain1))); + BEAST_EXPECT( + hash(Book(a1, a3, std::nullopt)) != hash(Book(a1, a3, domain1))); + BEAST_EXPECT( + hash(Book(a1, a4, std::nullopt)) != hash(Book(a1, a4, domain1))); + BEAST_EXPECT( + hash(Book(a2, a3, std::nullopt)) != hash(Book(a2, a3, domain1))); + BEAST_EXPECT( + hash(Book(a2, a4, std::nullopt)) != hash(Book(a2, a4, domain1))); + BEAST_EXPECT( + hash(Book(a3, a4, std::nullopt)) != hash(Book(a3, a4, domain1))); + + // Books with domain1 but different Issues + BEAST_EXPECT( + hash(Book(a1, a2, domain1)) != hash(Book(a1, a3, domain1))); + BEAST_EXPECT( + hash(Book(a1, a2, domain1)) != hash(Book(a1, a4, domain1))); + BEAST_EXPECT( + hash(Book(a2, a3, domain1)) != hash(Book(a2, a4, domain1))); + BEAST_EXPECT( + hash(Book(a1, a2, domain1)) != hash(Book(a2, a3, domain1))); + BEAST_EXPECT( + hash(Book(a2, a4, domain1)) != hash(Book(a3, a4, domain1))); + BEAST_EXPECT( + hash(Book(a3, a4, domain1)) != hash(Book(a1, a4, domain1))); + + // Books with domain1 and domain2 + BEAST_EXPECT( + hash(Book(a1, a2, domain1)) != hash(Book(a1, a2, domain2))); + BEAST_EXPECT( + hash(Book(a1, a3, domain1)) != hash(Book(a1, a3, domain2))); + BEAST_EXPECT( + hash(Book(a1, a4, domain1)) != hash(Book(a1, a4, domain2))); + BEAST_EXPECT( + hash(Book(a2, a3, domain1)) != hash(Book(a2, a3, domain2))); + BEAST_EXPECT( + hash(Book(a2, a4, domain1)) != hash(Book(a2, a4, domain2))); + BEAST_EXPECT( + hash(Book(a3, a4, domain1)) != hash(Book(a3, a4, domain2))); } //-------------------------------------------------------------------------- @@ -362,8 +635,16 @@ public: AccountID const i2(2); Issue const a1(c1, i1); Issue const a2(c2, i2); - Book const b1(a1, a2); - Book const b2(a2, a1); + Book const b1(a1, a2, std::nullopt); + Book const b2(a2, a1, std::nullopt); + + uint256 const domain1{1}; + uint256 const domain2{2}; + + Book const b1_d1(a1, a2, domain1); + Book const b2_d1(a2, a1, domain1); + Book const b1_d2(a1, a2, domain2); + Book const b2_d2(a2, a1, domain2); { Set c; @@ -375,11 +656,11 @@ public: if (!BEAST_EXPECT(c.size() == 2)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a1)) == 0)) + if (!BEAST_EXPECT(c.erase(Book(a1, a1, std::nullopt)) == 0)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a2)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a1, a2, std::nullopt)) == 1)) return; - if (!BEAST_EXPECT(c.erase(Book(a2, a1)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a2, a1, std::nullopt)) == 1)) return; if (!BEAST_EXPECT(c.empty())) return; @@ -395,11 +676,11 @@ public: if (!BEAST_EXPECT(c.size() == 2)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a1)) == 0)) + if (!BEAST_EXPECT(c.erase(Book(a1, a1, std::nullopt)) == 0)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a2)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a1, a2, std::nullopt)) == 1)) return; - if (!BEAST_EXPECT(c.erase(Book(a2, a1)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a2, a1, std::nullopt)) == 1)) return; if (!BEAST_EXPECT(c.empty())) return; @@ -413,6 +694,66 @@ public: return; #endif } + + { + Set c; + + c.insert(b1_d1); + if (!BEAST_EXPECT(c.size() == 1)) + return; + c.insert(b2_d1); + if (!BEAST_EXPECT(c.size() == 2)) + return; + c.insert(b1_d2); + if (!BEAST_EXPECT(c.size() == 3)) + return; + c.insert(b2_d2); + if (!BEAST_EXPECT(c.size() == 4)) + return; + + // Try removing non-existent elements + if (!BEAST_EXPECT(c.erase(Book(a2, a2, domain1)) == 0)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.size() == 2)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, domain2)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, domain2)) == 1)) + return; + if (!BEAST_EXPECT(c.empty())) + return; + } + + { + Set c; + + c.insert(b1); + c.insert(b2); + c.insert(b1_d1); + c.insert(b2_d1); + if (!BEAST_EXPECT(c.size() == 4)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, std::nullopt)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, std::nullopt)) == 1)) + return; + if (!BEAST_EXPECT(c.size() == 2)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.empty())) + return; + } } template @@ -425,8 +766,16 @@ public: AccountID const i2(2); Issue const a1(c1, i1); Issue const a2(c2, i2); - Book const b1(a1, a2); - Book const b2(a2, a1); + Book const b1(a1, a2, std::nullopt); + Book const b2(a2, a1, std::nullopt); + + uint256 const domain1{1}; + uint256 const domain2{2}; + + Book const b1_d1(a1, a2, domain1); + Book const b2_d1(a2, a1, domain1); + Book const b1_d2(a1, a2, domain2); + Book const b2_d2(a2, a1, domain2); // typename Map::value_type value_type; // std::pair value_type; @@ -443,11 +792,11 @@ public: if (!BEAST_EXPECT(c.size() == 2)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a1)) == 0)) + if (!BEAST_EXPECT(c.erase(Book(a1, a1, std::nullopt)) == 0)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a2)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a1, a2, std::nullopt)) == 1)) return; - if (!BEAST_EXPECT(c.erase(Book(a2, a1)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a2, a1, std::nullopt)) == 1)) return; if (!BEAST_EXPECT(c.empty())) return; @@ -465,11 +814,77 @@ public: if (!BEAST_EXPECT(c.size() == 2)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a1)) == 0)) + if (!BEAST_EXPECT(c.erase(Book(a1, a1, std::nullopt)) == 0)) return; - if (!BEAST_EXPECT(c.erase(Book(a1, a2)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a1, a2, std::nullopt)) == 1)) return; - if (!BEAST_EXPECT(c.erase(Book(a2, a1)) == 1)) + if (!BEAST_EXPECT(c.erase(Book(a2, a1, std::nullopt)) == 1)) + return; + if (!BEAST_EXPECT(c.empty())) + return; + } + + { + Map c; + + c.insert(std::make_pair(b1_d1, 10)); + if (!BEAST_EXPECT(c.size() == 1)) + return; + c.insert(std::make_pair(b2_d1, 20)); + if (!BEAST_EXPECT(c.size() == 2)) + return; + c.insert(std::make_pair(b1_d2, 30)); + if (!BEAST_EXPECT(c.size() == 3)) + return; + c.insert(std::make_pair(b2_d2, 40)); + if (!BEAST_EXPECT(c.size() == 4)) + return; + + // Try removing non-existent elements + if (!BEAST_EXPECT(c.erase(Book(a2, a2, domain1)) == 0)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.size() == 2)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, domain2)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, domain2)) == 1)) + return; + if (!BEAST_EXPECT(c.empty())) + return; + } + + { + Map c; + + c.insert(std::make_pair(b1, 1)); + c.insert(std::make_pair(b2, 2)); + c.insert(std::make_pair(b1_d1, 3)); + c.insert(std::make_pair(b2_d1, 4)); + if (!BEAST_EXPECT(c.size() == 4)) + return; + + // Try removing non-existent elements + if (!BEAST_EXPECT(c.erase(Book(a1, a1, domain1)) == 0)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a2, domain2)) == 0)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, std::nullopt)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, std::nullopt)) == 1)) + return; + if (!BEAST_EXPECT(c.size() == 2)) + return; + + if (!BEAST_EXPECT(c.erase(Book(a1, a2, domain1)) == 1)) + return; + if (!BEAST_EXPECT(c.erase(Book(a2, a1, domain1)) == 1)) return; if (!BEAST_EXPECT(c.empty())) return; @@ -556,6 +971,10 @@ public: testBookSets(); testBookMaps(); + + // --- + testIssueDomainSets(); + testIssueDomainMaps(); } }; 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/BookChanges_test.cpp b/src/test/rpc/BookChanges_test.cpp index 95997538d7..1f059c2bf7 100644 --- a/src/test/rpc/BookChanges_test.cpp +++ b/src/test/rpc/BookChanges_test.cpp @@ -18,6 +18,10 @@ //============================================================================== #include +#include + +#include "xrpl/beast/unit_test/suite.h" +#include "xrpl/protocol/jss.h" namespace ripple { namespace test { @@ -83,14 +87,59 @@ public: // == 3); } + void + testDomainOffer() + { + testcase("Domain Offer"); + using namespace jtx; + + FeatureBitset const all{ + jtx::supported_amendments() | featurePermissionedDomains | + featureCredentials | featurePermissionedDEX}; + + Env env(*this, all); + PermissionedDEX permDex(env); + auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] = + permDex; + + auto wsc = makeWSClient(env.app().config()); + + env(offer(alice, XRP(10), USD(10)), domain(domainID)); + env.close(); + + env(pay(bob, carol, USD(10)), + path(~USD), + sendmax(XRP(10)), + domain(domainID)); + env.close(); + + std::string const txHash{ + env.tx()->getJson(JsonOptions::none)[jss::hash].asString()}; + + Json::Value const txResult = env.rpc("tx", txHash)[jss::result]; + auto const ledgerIndex = txResult[jss::ledger_index].asInt(); + + Json::Value jvParams; + jvParams[jss::ledger_index] = ledgerIndex; + + auto jv = wsc->invoke("book_changes", jvParams); + auto jrr = jv[jss::result]; + + BEAST_EXPECT(jrr[jss::changes].size() == 1); + BEAST_EXPECT( + jrr[jss::changes][0u][jss::domain].asString() == + to_string(domainID)); + } + void run() override { testConventionalLedgerInputStrings(); testLedgerInputDefaultBehavior(); - // Note: Other aspects of the book_changes rpc are fertile grounds for - // unit-testing purposes. It can be included in future work + testDomainOffer(); + // Note: Other aspects of the book_changes rpc are fertile grounds + // for unit-testing purposes. It can be included in future work } }; diff --git a/src/test/rpc/Book_test.cpp b/src/test/rpc/Book_test.cpp index 79e3f940f8..0ec36eca53 100644 --- a/src/test/rpc/Book_test.cpp +++ b/src/test/rpc/Book_test.cpp @@ -22,6 +22,8 @@ #include #include +#include +#include #include namespace ripple { @@ -30,10 +32,14 @@ namespace test { class Book_test : public beast::unit_test::suite { std::string - getBookDir(jtx::Env& env, Issue const& in, Issue const& out) + getBookDir( + jtx::Env& env, + Issue const& in, + Issue const& out, + std::optional const& domain = std::nullopt) { std::string dir; - auto uBookBase = getBookBase({in, out}); + auto uBookBase = getBookBase({in, out, domain}); auto uBookEnd = getQualityNext(uBookBase); auto view = env.closed(); auto key = view->succ(uBookBase, uBookEnd); @@ -1657,6 +1663,19 @@ public: "Unneeded field 'taker_gets.issuer' " "for XRP currency specification."); } + { + Json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker_pays][jss::currency] = "USD"; + jvParams[jss::taker_pays][jss::issuer] = gw.human(); + jvParams[jss::taker_gets][jss::currency] = "EUR"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + jvParams[jss::domain] = "badString"; + auto const jrr = env.rpc( + "json", "book_offers", to_string(jvParams))[jss::result]; + BEAST_EXPECT(jrr[jss::error] == "domainMalformed"); + BEAST_EXPECT(jrr[jss::error_message] == "Unable to parse domain."); + } } void @@ -1711,6 +1730,273 @@ public: (asAdmin ? RPC::Tuning::bookOffers.rdefault : 0u)); } + void + testTrackDomainOffer() + { + testcase("TrackDomainOffer"); + using namespace jtx; + + FeatureBitset const all{ + jtx::supported_amendments() | featurePermissionedDomains | + featureCredentials | featurePermissionedDEX}; + + Env env(*this, all); + PermissionedDEX permDex(env); + auto const alice = permDex.alice; + auto const bob = permDex.bob; + auto const carol = permDex.carol; + auto const domainID = permDex.domainID; + auto const gw = permDex.gw; + auto const USD = permDex.USD; + + auto wsc = makeWSClient(env.app().config()); + + env(offer(alice, XRP(10), USD(10)), domain(domainID)); + env.close(); + + auto checkBookOffers = [&](Json::Value const& jrr) { + BEAST_EXPECT(jrr[jss::offers].isArray()); + BEAST_EXPECT(jrr[jss::offers].size() == 1); + auto const jrOffer = jrr[jss::offers][0u]; + BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human()); + BEAST_EXPECT( + jrOffer[sfBookDirectory.fieldName] == + getBookDir(env, XRP, USD.issue(), domainID)); + BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0"); + BEAST_EXPECT(jrOffer[jss::Flags] == 0); + BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer); + BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0"); + BEAST_EXPECT( + jrOffer[jss::TakerGets] == + USD(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jrOffer[jss::TakerPays] == + XRP(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jrOffer[sfDomainID.jsonName].asString() == to_string(domainID)); + }; + + // book_offers: open book doesn't return offer + { + Json::Value jvParams; + jvParams[jss::taker] = env.master.human(); + jvParams[jss::taker_pays][jss::currency] = "XRP"; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker_gets][jss::currency] = "USD"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + + auto jv = wsc->invoke("book_offers", jvParams); + auto jrr = jv[jss::result]; + BEAST_EXPECT(jrr[jss::offers].isArray()); + BEAST_EXPECT(jrr[jss::offers].size() == 0); + } + + auto checkSubBooks = [&](Json::Value const& jv) { + BEAST_EXPECT( + jv[jss::result].isMember(jss::offers) && + jv[jss::result][jss::offers].size() == 1); + BEAST_EXPECT( + jv[jss::result][jss::offers][0u][jss::TakerGets] == + USD(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jv[jss::result][jss::offers][0u][jss::TakerPays] == + XRP(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jv[jss::result][jss::offers][0u][sfDomainID.jsonName] + .asString() == to_string(domainID)); + }; + + // book_offers: requesting domain book returns hybrid offer + { + Json::Value jvParams; + jvParams[jss::taker] = env.master.human(); + jvParams[jss::taker_pays][jss::currency] = "XRP"; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker_gets][jss::currency] = "USD"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + jvParams[jss::domain] = to_string(domainID); + + auto jv = wsc->invoke("book_offers", jvParams); + auto jrr = jv[jss::result]; + checkBookOffers(jrr); + } + + // subscribe to domain book should return domain offer + { + Json::Value books; + books[jss::books] = Json::arrayValue; + { + auto& j = books[jss::books].append(Json::objectValue); + j[jss::snapshot] = true; + j[jss::taker_pays][jss::currency] = "XRP"; + j[jss::taker_gets][jss::currency] = "USD"; + j[jss::taker_gets][jss::issuer] = gw.human(); + j[jss::domain] = to_string(domainID); + } + + auto jv = wsc->invoke("subscribe", books); + if (!BEAST_EXPECT(jv[jss::status] == "success")) + return; + checkSubBooks(jv); + } + + // subscribe to open book should not return domain offer + { + Json::Value books; + books[jss::books] = Json::arrayValue; + { + auto& j = books[jss::books].append(Json::objectValue); + j[jss::snapshot] = true; + j[jss::taker_pays][jss::currency] = "XRP"; + j[jss::taker_gets][jss::currency] = "USD"; + j[jss::taker_gets][jss::issuer] = gw.human(); + } + + auto jv = wsc->invoke("subscribe", books); + if (!BEAST_EXPECT(jv[jss::status] == "success")) + return; + BEAST_EXPECT( + jv[jss::result].isMember(jss::offers) && + jv[jss::result][jss::offers].size() == 0); + } + } + + void + testTrackHybridOffer() + { + testcase("TrackHybridOffer"); + using namespace jtx; + + FeatureBitset const all{ + jtx::supported_amendments() | featurePermissionedDomains | + featureCredentials | featurePermissionedDEX}; + + Env env(*this, all); + PermissionedDEX permDex(env); + auto const alice = permDex.alice; + auto const bob = permDex.bob; + auto const carol = permDex.carol; + auto const domainID = permDex.domainID; + auto const gw = permDex.gw; + auto const USD = permDex.USD; + + auto wsc = makeWSClient(env.app().config()); + + env(offer(alice, XRP(10), USD(10)), + domain(domainID), + txflags(tfHybrid)); + env.close(); + + auto checkBookOffers = [&](Json::Value const& jrr) { + BEAST_EXPECT(jrr[jss::offers].isArray()); + BEAST_EXPECT(jrr[jss::offers].size() == 1); + auto const jrOffer = jrr[jss::offers][0u]; + BEAST_EXPECT(jrOffer[sfAccount.fieldName] == alice.human()); + BEAST_EXPECT( + jrOffer[sfBookDirectory.fieldName] == + getBookDir(env, XRP, USD.issue(), domainID)); + BEAST_EXPECT(jrOffer[sfBookNode.fieldName] == "0"); + BEAST_EXPECT(jrOffer[jss::Flags] == lsfHybrid); + BEAST_EXPECT(jrOffer[sfLedgerEntryType.fieldName] == jss::Offer); + BEAST_EXPECT(jrOffer[sfOwnerNode.fieldName] == "0"); + BEAST_EXPECT( + jrOffer[jss::TakerGets] == + USD(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jrOffer[jss::TakerPays] == + XRP(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jrOffer[sfDomainID.jsonName].asString() == to_string(domainID)); + BEAST_EXPECT(jrOffer[sfAdditionalBooks.jsonName].size() == 1); + }; + + // book_offers: open book returns hybrid offer + { + Json::Value jvParams; + jvParams[jss::taker] = env.master.human(); + jvParams[jss::taker_pays][jss::currency] = "XRP"; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker_gets][jss::currency] = "USD"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + + auto jv = wsc->invoke("book_offers", jvParams); + auto jrr = jv[jss::result]; + checkBookOffers(jrr); + } + + auto checkSubBooks = [&](Json::Value const& jv) { + BEAST_EXPECT( + jv[jss::result].isMember(jss::offers) && + jv[jss::result][jss::offers].size() == 1); + BEAST_EXPECT( + jv[jss::result][jss::offers][0u][jss::TakerGets] == + USD(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jv[jss::result][jss::offers][0u][jss::TakerPays] == + XRP(10).value().getJson(JsonOptions::none)); + BEAST_EXPECT( + jv[jss::result][jss::offers][0u][sfDomainID.jsonName] + .asString() == to_string(domainID)); + }; + + // book_offers: requesting domain book returns hybrid offer + { + Json::Value jvParams; + jvParams[jss::taker] = env.master.human(); + jvParams[jss::taker_pays][jss::currency] = "XRP"; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::taker_gets][jss::currency] = "USD"; + jvParams[jss::taker_gets][jss::issuer] = gw.human(); + jvParams[jss::domain] = to_string(domainID); + + auto jv = wsc->invoke("book_offers", jvParams); + auto jrr = jv[jss::result]; + checkBookOffers(jrr); + } + + // subscribe to domain book should return hybrid offer + { + Json::Value books; + books[jss::books] = Json::arrayValue; + { + auto& j = books[jss::books].append(Json::objectValue); + j[jss::snapshot] = true; + j[jss::taker_pays][jss::currency] = "XRP"; + j[jss::taker_gets][jss::currency] = "USD"; + j[jss::taker_gets][jss::issuer] = gw.human(); + j[jss::domain] = to_string(domainID); + } + + auto jv = wsc->invoke("subscribe", books); + if (!BEAST_EXPECT(jv[jss::status] == "success")) + return; + checkSubBooks(jv); + + // RPC unsubscribe + auto unsubJv = wsc->invoke("unsubscribe", books); + if (wsc->version() == 2) + BEAST_EXPECT(unsubJv[jss::status] == "success"); + } + + // subscribe to open book should return hybrid offer + { + Json::Value books; + books[jss::books] = Json::arrayValue; + { + auto& j = books[jss::books].append(Json::objectValue); + j[jss::snapshot] = true; + j[jss::taker_pays][jss::currency] = "XRP"; + j[jss::taker_gets][jss::currency] = "USD"; + j[jss::taker_gets][jss::issuer] = gw.human(); + } + + auto jv = wsc->invoke("subscribe", books); + if (!BEAST_EXPECT(jv[jss::status] == "success")) + return; + checkSubBooks(jv); + } + } + void run() override { @@ -1728,6 +2014,8 @@ public: testBookOfferErrors(); testBookOfferLimits(true); testBookOfferLimits(false); + testTrackDomainOffer(); + testTrackHybridOffer(); } }; diff --git a/src/test/rpc/GatewayBalances_test.cpp b/src/test/rpc/GatewayBalances_test.cpp index 249d4f892f..7e9273d25e 100644 --- a/src/test/rpc/GatewayBalances_test.cpp +++ b/src/test/rpc/GatewayBalances_test.cpp @@ -252,7 +252,10 @@ public: { using namespace jtx; auto const sa = supported_amendments(); - for (auto feature : {sa - featureFlowCross, sa}) + for (auto feature : + {sa - featureFlowCross - featurePermissionedDEX, + sa - featurePermissionedDEX, + sa}) { testGWB(feature); testGWBApiVersions(feature); diff --git a/src/test/rpc/JSONRPC_test.cpp b/src/test/rpc/JSONRPC_test.cpp index cd26758c1f..1612d1b455 100644 --- a/src/test/rpc/JSONRPC_test.cpp +++ b/src/test/rpc/JSONRPC_test.cpp @@ -2041,6 +2041,28 @@ static constexpr TxnTestData txnTestArray[] = { "Cannot specify differing 'Amount' and 'DeliverMax'", "Cannot specify differing 'Amount' and 'DeliverMax'", "Cannot specify differing 'Amount' and 'DeliverMax'"}}}, + {"Payment cannot specify bad DomainID.", + __LINE__, + R"({ + "command": "doesnt_matter", + "account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "secret": "masterpassphrase", + "debug_signing": 0, + "tx_json": { + "Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", + "Amount": "1000000000", + "Destination": "rnUy2SHTrB9DubsPmkJZUXTf5FcNDGrYEA", + "Fee": 50, + "Sequence": 0, + "SigningPubKey": "", + "TransactionType": "Payment", + "DomainID": "invalid", + } +})", + {{"Unable to parse 'DomainID'.", + "Unable to parse 'DomainID'.", + "Unable to parse 'DomainID'.", + "Unable to parse 'DomainID'."}}}, {"Minimal delegated transaction.", __LINE__, @@ -2132,6 +2154,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 +2928,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/NoRipple_test.cpp b/src/test/rpc/NoRipple_test.cpp index 5c41f25128..42c86b34bb 100644 --- a/src/test/rpc/NoRipple_test.cpp +++ b/src/test/rpc/NoRipple_test.cpp @@ -294,7 +294,8 @@ public: }; using namespace jtx; auto const sa = supported_amendments(); - withFeatsTests(sa - featureFlowCross); + withFeatsTests(sa - featureFlowCross - featurePermissionedDEX); + withFeatsTests(sa - featurePermissionedDEX); withFeatsTests(sa); } }; 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/test/rpc/Subscribe_test.cpp b/src/test/rpc/Subscribe_test.cpp index 3d1b425422..32296c5d0a 100644 --- a/src/test/rpc/Subscribe_test.cpp +++ b/src/test/rpc/Subscribe_test.cpp @@ -1300,6 +1300,60 @@ public: } } + void + testSubBookChanges() + { + testcase("SubBookChanges"); + using namespace jtx; + using namespace std::chrono_literals; + FeatureBitset const all{ + jtx::supported_amendments() | featurePermissionedDomains | + featureCredentials | featurePermissionedDEX}; + + Env env(*this, all); + PermissionedDEX permDex(env); + auto const alice = permDex.alice; + auto const bob = permDex.bob; + auto const carol = permDex.carol; + auto const domainID = permDex.domainID; + auto const gw = permDex.gw; + auto const USD = permDex.USD; + + auto wsc = makeWSClient(env.app().config()); + + Json::Value streams; + streams[jss::streams] = Json::arrayValue; + streams[jss::streams][0u] = "book_changes"; + + auto jv = wsc->invoke("subscribe", streams); + if (!BEAST_EXPECT(jv[jss::status] == "success")) + return; + env(offer(alice, XRP(10), USD(10)), + domain(domainID), + txflags(tfHybrid)); + env.close(); + + env(pay(bob, carol, USD(5)), + path(~USD), + sendmax(XRP(5)), + domain(domainID)); + env.close(); + + BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { + if (jv[jss::changes].size() != 1) + return false; + + auto const jrOffer = jv[jss::changes][0u]; + return (jv[jss::changes][0u][jss::domain]).asString() == + strHex(domainID) && + jrOffer[jss::currency_a].asString() == "XRP_drops" && + jrOffer[jss::volume_a].asString() == "5000000" && + jrOffer[jss::currency_b].asString() == + "rHUKYAZyUFn8PCZWbPfwHfbVQXTYrYKkHb/USD" && + jrOffer[jss::volume_b].asString() == "5"; + })); + } + void run() override { @@ -1318,6 +1372,7 @@ public: testSubErrors(false); testSubByUrl(); testHistoryTxStream(); + testSubBookChanges(); } }; diff --git a/src/xrpld/app/ledger/OrderBookDB.cpp b/src/xrpld/app/ledger/OrderBookDB.cpp index b8a7b54008..433a993772 100644 --- a/src/xrpld/app/ledger/OrderBookDB.cpp +++ b/src/xrpld/app/ledger/OrderBookDB.cpp @@ -89,6 +89,8 @@ OrderBookDB::update(std::shared_ptr const& ledger) decltype(allBooks_) allBooks; decltype(xrpBooks_) xrpBooks; + decltype(domainBooks_) domainBooks; + decltype(xrpDomainBooks_) xrpDomainBooks; allBooks.reserve(allBooks_.size()); xrpBooks.reserve(xrpBooks_.size()); @@ -120,10 +122,16 @@ OrderBookDB::update(std::shared_ptr const& ledger) book.in.account = sle->getFieldH160(sfTakerPaysIssuer); book.out.currency = sle->getFieldH160(sfTakerGetsCurrency); book.out.account = sle->getFieldH160(sfTakerGetsIssuer); + book.domain = (*sle)[~sfDomainID]; - allBooks[book.in].insert(book.out); + if (book.domain) + domainBooks_[{book.in, *book.domain}].insert(book.out); + else + allBooks[book.in].insert(book.out); - if (isXRP(book.out)) + if (book.domain && isXRP(book.out)) + xrpDomainBooks.insert({book.in, *book.domain}); + else if (isXRP(book.out)) xrpBooks.insert(book.in); ++cnt; @@ -160,6 +168,8 @@ OrderBookDB::update(std::shared_ptr const& ledger) std::lock_guard sl(mLock); allBooks_.swap(allBooks); xrpBooks_.swap(xrpBooks); + domainBooks_.swap(domainBooks); + xrpDomainBooks_.swap(xrpDomainBooks); } app_.getLedgerMaster().newOrderBookDB(); @@ -172,47 +182,77 @@ OrderBookDB::addOrderBook(Book const& book) std::lock_guard sl(mLock); - allBooks_[book.in].insert(book.out); + if (book.domain) + domainBooks_[{book.in, *book.domain}].insert(book.out); + else + allBooks_[book.in].insert(book.out); - if (toXRP) + if (book.domain && toXRP) + xrpDomainBooks_.insert({book.in, *book.domain}); + else if (toXRP) xrpBooks_.insert(book.in); } // return list of all orderbooks that want this issuerID and currencyID std::vector -OrderBookDB::getBooksByTakerPays(Issue const& issue) +OrderBookDB::getBooksByTakerPays( + Issue const& issue, + std::optional const& domain) { std::vector ret; { std::lock_guard sl(mLock); - if (auto it = allBooks_.find(issue); it != allBooks_.end()) - { - ret.reserve(it->second.size()); + auto getBooks = [&](auto const& container, auto const& key) { + if (auto it = container.find(key); it != container.end()) + { + auto const& books = it->second; + ret.reserve(books.size()); - for (auto const& gets : it->second) - ret.push_back(Book(issue, gets)); - } + for (auto const& gets : books) + ret.emplace_back(issue, gets, domain); + } + }; + + if (!domain) + getBooks(allBooks_, issue); + else + getBooks(domainBooks_, std::make_pair(issue, *domain)); } return ret; } int -OrderBookDB::getBookSize(Issue const& issue) +OrderBookDB::getBookSize( + Issue const& issue, + std::optional const& domain) { std::lock_guard sl(mLock); - if (auto it = allBooks_.find(issue); it != allBooks_.end()) - return static_cast(it->second.size()); + + if (!domain) + { + if (auto it = allBooks_.find(issue); it != allBooks_.end()) + return static_cast(it->second.size()); + } + else + { + if (auto it = domainBooks_.find({issue, *domain}); + it != domainBooks_.end()) + return static_cast(it->second.size()); + } + return 0; } bool -OrderBookDB::isBookToXRP(Issue const& issue) +OrderBookDB::isBookToXRP(Issue const& issue, std::optional domain) { std::lock_guard sl(mLock); - return xrpBooks_.count(issue) > 0; + if (domain) + return xrpDomainBooks_.contains({issue, *domain}); + return xrpBooks_.contains(issue); } BookListeners::pointer @@ -228,7 +268,8 @@ OrderBookDB::makeBookListeners(Book const& book) mListeners[book] = ret; XRPL_ASSERT( getBookListeners(book) == ret, - "ripple::OrderBookDB::makeBookListeners : result roundtrip lookup"); + "ripple::OrderBookDB::makeBookListeners : result roundtrip " + "lookup"); } return ret; @@ -278,7 +319,8 @@ OrderBookDB::processTxn( { auto listeners = getBookListeners( {data->getFieldAmount(sfTakerGets).issue(), - data->getFieldAmount(sfTakerPays).issue()}); + data->getFieldAmount(sfTakerPays).issue(), + (*data)[~sfDomainID]}); if (listeners) listeners->publish(jvObj, havePublished); } diff --git a/src/xrpld/app/ledger/OrderBookDB.h b/src/xrpld/app/ledger/OrderBookDB.h index bc36f8a301..89c20b7074 100644 --- a/src/xrpld/app/ledger/OrderBookDB.h +++ b/src/xrpld/app/ledger/OrderBookDB.h @@ -25,8 +25,10 @@ #include #include +#include #include +#include namespace ripple { @@ -46,15 +48,19 @@ public: /** @return a list of all orderbooks that want this issuerID and currencyID. */ std::vector - getBooksByTakerPays(Issue const&); + getBooksByTakerPays( + Issue const&, + std::optional const& domain = std::nullopt); /** @return a count of all orderbooks that want this issuerID and currencyID. */ int - getBookSize(Issue const&); + getBookSize( + Issue const&, + std::optional const& domain = std::nullopt); bool - isBookToXRP(Issue const&); + isBookToXRP(Issue const&, std::optional domain = std::nullopt); BookListeners::pointer getBookListeners(Book const&); @@ -74,9 +80,15 @@ private: // Maps order books by "issue in" to "issue out": hardened_hash_map> allBooks_; + hardened_hash_map, hardened_hash_set> + domainBooks_; + // does an order book to XRP exist hash_set xrpBooks_; + // does an order book to XRP exist + hash_set> xrpDomainBooks_; + std::recursive_mutex mLock; using BookToListenersMap = hash_map; 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/PermissionedDEXHelpers.cpp b/src/xrpld/app/misc/PermissionedDEXHelpers.cpp new file mode 100644 index 0000000000..4251ac1519 --- /dev/null +++ b/src/xrpld/app/misc/PermissionedDEXHelpers.cpp @@ -0,0 +1,88 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 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 + +namespace ripple { +namespace permissioned_dex { + +bool +accountInDomain( + ReadView const& view, + AccountID const& account, + Domain const& domainID) +{ + auto const sleDomain = view.read(keylet::permissionedDomain(domainID)); + if (!sleDomain) + return false; + + // domain owner is in the domain + if (sleDomain->getAccountID(sfOwner) == account) + return true; + + auto const& credentials = sleDomain->getFieldArray(sfAcceptedCredentials); + + bool const inDomain = std::any_of( + credentials.begin(), credentials.end(), [&](auto const& credential) { + auto const sleCred = view.read(keylet::credential( + account, credential[sfIssuer], credential[sfCredentialType])); + if (!sleCred || !sleCred->isFlag(lsfAccepted)) + return false; + + return !credentials::checkExpired( + sleCred, view.info().parentCloseTime); + }); + + return inDomain; +} + +bool +offerInDomain( + ReadView const& view, + uint256 const& offerID, + Domain const& domainID, + beast::Journal j) +{ + auto const sleOffer = view.read(keylet::offer(offerID)); + + // The following are defensive checks that should never happen, since this + // function is used to check against the order book offers, which should not + // have any of the following wrong behavior + if (!sleOffer) + return false; // LCOV_EXCL_LINE + if (!sleOffer->isFieldPresent(sfDomainID)) + return false; // LCOV_EXCL_LINE + if (sleOffer->getFieldH256(sfDomainID) != domainID) + return false; // LCOV_EXCL_LINE + + if (sleOffer->isFlag(lsfHybrid) && + !sleOffer->isFieldPresent(sfAdditionalBooks)) + { + JLOG(j.error()) << "Hybrid offer " << offerID + << " missing AdditionalBooks field"; + return false; // LCOV_EXCL_LINE + } + + return accountInDomain(view, sleOffer->getAccountID(sfAccount), domainID); +} + +} // namespace permissioned_dex + +} // namespace ripple diff --git a/src/xrpld/app/misc/PermissionedDEXHelpers.h b/src/xrpld/app/misc/PermissionedDEXHelpers.h new file mode 100644 index 0000000000..1b3a0323fd --- /dev/null +++ b/src/xrpld/app/misc/PermissionedDEXHelpers.h @@ -0,0 +1,43 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 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. +*/ +//============================================================================== + +#pragma once +#include + +namespace ripple { +namespace permissioned_dex { + +// Check if an account is in a permissioned domain +[[nodiscard]] bool +accountInDomain( + ReadView const& view, + AccountID const& account, + Domain const& domainID); + +// Check if an offer is in the permissioned domain +[[nodiscard]] bool +offerInDomain( + ReadView const& view, + uint256 const& offerID, + Domain const& domainID, + beast::Journal j); + +} // namespace permissioned_dex + +} // namespace ripple 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/paths/Flow.cpp b/src/xrpld/app/paths/Flow.cpp index 08f8ec3f25..3b14b8b968 100644 --- a/src/xrpld/app/paths/Flow.cpp +++ b/src/xrpld/app/paths/Flow.cpp @@ -64,6 +64,7 @@ flow( OfferCrossing offerCrossing, std::optional const& limitQuality, std::optional const& sendMax, + std::optional const& domainID, beast::Journal j, path::detail::FlowDebugInfo* flowDebugInfo) { @@ -98,6 +99,7 @@ flow( ownerPaysTransferFee, offerCrossing, ammContext, + domainID, j); if (toStrandsTer != tesSUCCESS) diff --git a/src/xrpld/app/paths/Flow.h b/src/xrpld/app/paths/Flow.h index 048b8785f1..659f180484 100644 --- a/src/xrpld/app/paths/Flow.h +++ b/src/xrpld/app/paths/Flow.h @@ -66,6 +66,7 @@ flow( OfferCrossing offerCrossing, std::optional const& limitQuality, std::optional const& sendMax, + std::optional const& domainID, beast::Journal j, path::detail::FlowDebugInfo* flowDebugInfo = nullptr); diff --git a/src/xrpld/app/paths/PathRequest.cpp b/src/xrpld/app/paths/PathRequest.cpp index ed090d25aa..8a88e774d0 100644 --- a/src/xrpld/app/paths/PathRequest.cpp +++ b/src/xrpld/app/paths/PathRequest.cpp @@ -438,6 +438,21 @@ PathRequest::parseJson(Json::Value const& jvParams) if (jvParams.isMember(jss::id)) jvId = jvParams[jss::id]; + if (jvParams.isMember(jss::domain)) + { + uint256 num; + if (!jvParams[jss::domain].isString() || + !num.parseHex(jvParams[jss::domain].asString())) + { + jvStatus = rpcError(rpcDOMAIN_MALFORMED); + return PFR_PJ_INVALID; + } + else + { + domain = num; + } + } + return PFR_PJ_NOCHANGE; } @@ -484,6 +499,7 @@ PathRequest::getPathFinder( std::nullopt, dst_amount, saSendMax, + domain, app_); if (pathfinder->findPaths(level, continueCallback)) pathfinder->computePathRanks(max_paths_, continueCallback); @@ -581,6 +597,7 @@ PathRequest::findPaths( *raDstAccount, // --> Account to deliver to. *raSrcAccount, // --> Account sending from. ps, // --> Path set. + domain, // --> Domain. app_.logs(), &rcInput); @@ -601,6 +618,7 @@ PathRequest::findPaths( *raDstAccount, // --> Account to deliver to. *raSrcAccount, // --> Account sending from. ps, // --> Path set. + domain, // --> Domain. app_.logs()); if (rc.result() != tesSUCCESS) diff --git a/src/xrpld/app/paths/PathRequest.h b/src/xrpld/app/paths/PathRequest.h index e480c2b812..aea0e564fb 100644 --- a/src/xrpld/app/paths/PathRequest.h +++ b/src/xrpld/app/paths/PathRequest.h @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -156,6 +157,8 @@ private: std::set sciSourceCurrencies; std::map mContext; + std::optional domain; + bool convert_all_; std::recursive_mutex mIndexLock; diff --git a/src/xrpld/app/paths/Pathfinder.cpp b/src/xrpld/app/paths/Pathfinder.cpp index e02c3ed089..74a33ec917 100644 --- a/src/xrpld/app/paths/Pathfinder.cpp +++ b/src/xrpld/app/paths/Pathfinder.cpp @@ -166,6 +166,7 @@ Pathfinder::Pathfinder( std::optional const& uSrcIssuer, STAmount const& saDstAmount, std::optional const& srcAmount, + std::optional const& domain, Application& app) : mSrcAccount(uSrcAccount) , mDstAccount(uDstAccount) @@ -184,6 +185,7 @@ Pathfinder::Pathfinder( 0, true))) , convert_all_(convertAllCheck(mDstAmount)) + , mDomain(domain) , mLedger(cache->getLedger()) , mRLCache(cache) , app_(app) @@ -372,6 +374,7 @@ Pathfinder::getPathLiquidity( mDstAccount, mSrcAccount, pathSet, + mDomain, app_.logs(), &rcInput); // If we can't get even the minimum liquidity requested, we're done. @@ -392,6 +395,7 @@ Pathfinder::getPathLiquidity( mDstAccount, mSrcAccount, pathSet, + mDomain, app_.logs(), &rcInput); @@ -431,6 +435,7 @@ Pathfinder::computePathRanks( mDstAccount, mSrcAccount, STPathSet(), + mDomain, app_.logs(), &rcInput); @@ -741,7 +746,7 @@ Pathfinder::getPathsOut( if (!bFrozen) { - count = app_.getOrderBookDB().getBookSize(issue); + count = app_.getOrderBookDB().getBookSize(issue, mDomain); if (auto const lines = mRLCache->getRippleLines(account, direction)) { @@ -1128,7 +1133,8 @@ Pathfinder::addLink( { // to XRP only if (!bOnXRP && - app_.getOrderBookDB().isBookToXRP({uEndCurrency, uEndIssuer})) + app_.getOrderBookDB().isBookToXRP( + {uEndCurrency, uEndIssuer}, mDomain)) { STPathElement pathElement( STPathElement::typeCurrency, @@ -1142,7 +1148,7 @@ Pathfinder::addLink( { bool bDestOnly = (addFlags & afOB_LAST) != 0; auto books = app_.getOrderBookDB().getBooksByTakerPays( - {uEndCurrency, uEndIssuer}); + {uEndCurrency, uEndIssuer}, mDomain); JLOG(j_.trace()) << books.size() << " books found from this currency/issuer"; diff --git a/src/xrpld/app/paths/Pathfinder.h b/src/xrpld/app/paths/Pathfinder.h index 973fda8855..ea3928dff4 100644 --- a/src/xrpld/app/paths/Pathfinder.h +++ b/src/xrpld/app/paths/Pathfinder.h @@ -48,6 +48,7 @@ public: std::optional const& uSrcIssuer, STAmount const& dstAmount, std::optional const& srcAmount, + std::optional const& domain, Application& app); Pathfinder(Pathfinder const&) = delete; Pathfinder& @@ -205,6 +206,7 @@ private: been removed. */ STAmount mRemainingAmount; bool convert_all_; + std::optional mDomain; std::shared_ptr mLedger; std::unique_ptr m_loadEvent; diff --git a/src/xrpld/app/paths/RippleCalc.cpp b/src/xrpld/app/paths/RippleCalc.cpp index c783bb8e9f..4e472e07c8 100644 --- a/src/xrpld/app/paths/RippleCalc.cpp +++ b/src/xrpld/app/paths/RippleCalc.cpp @@ -53,6 +53,8 @@ RippleCalc::rippleCalculate( // A set of paths that are included in the transaction that we'll // explore for liquidity. STPathSet const& spsPaths, + + std::optional const& domainID, Logs& l, Input const* const pInputs) { @@ -110,6 +112,7 @@ RippleCalc::rippleCalculate( OfferCrossing::no, limitQuality, sendMax, + domainID, j, nullptr); } diff --git a/src/xrpld/app/paths/RippleCalc.h b/src/xrpld/app/paths/RippleCalc.h index 45f68725cc..09de7334e8 100644 --- a/src/xrpld/app/paths/RippleCalc.h +++ b/src/xrpld/app/paths/RippleCalc.h @@ -111,6 +111,8 @@ public: // A set of paths that are included in the transaction that we'll // explore for liquidity. STPathSet const& spsPaths, + + std::optional const& domainID, Logs& l, Input const* const pInputs = nullptr); diff --git a/src/xrpld/app/paths/RippleLineCache.h b/src/xrpld/app/paths/RippleLineCache.h index 5a3188c810..6196211a70 100644 --- a/src/xrpld/app/paths/RippleLineCache.h +++ b/src/xrpld/app/paths/RippleLineCache.h @@ -104,7 +104,7 @@ private: struct Hash { - explicit Hash() = default; + Hash() = default; std::size_t operator()(AccountKey const& key) const noexcept diff --git a/src/xrpld/app/paths/detail/BookStep.cpp b/src/xrpld/app/paths/detail/BookStep.cpp index 4024ca190d..8d20a9900c 100644 --- a/src/xrpld/app/paths/detail/BookStep.cpp +++ b/src/xrpld/app/paths/detail/BookStep.cpp @@ -93,7 +93,7 @@ protected: public: BookStep(StrandContext const& ctx, Issue const& in, Issue const& out) : maxOffersToConsume_(getMaxOffersToConsume(ctx)) - , book_(in, out) + , book_(in, out, ctx.domainID) , strandSrc_(ctx.strandSrc) , strandDst_(ctx.strandDst) , prevStep_(ctx.prevStep) @@ -837,6 +837,10 @@ BookStep::forEachOffer( // At any payment engine iteration, AMM offer can only be consumed once. auto tryAMM = [&](std::optional const& lobQuality) -> bool { + // amm doesn't support domain yet + if (book_.domain) + return true; + // If offer crossing then use either LOB quality or nullopt // to prevent AMM being blocked by a lower quality LOB. auto const qualityThreshold = [&]() -> std::optional { diff --git a/src/xrpld/app/paths/detail/PaySteps.cpp b/src/xrpld/app/paths/detail/PaySteps.cpp index 99f212d548..aa9e21e182 100644 --- a/src/xrpld/app/paths/detail/PaySteps.cpp +++ b/src/xrpld/app/paths/detail/PaySteps.cpp @@ -142,6 +142,7 @@ toStrand( bool ownerPaysTransferFee, OfferCrossing offerCrossing, AMMContext& ammContext, + std::optional const& domainID, beast::Journal j) { if (isXRP(src) || isXRP(dst) || !isConsistent(deliver) || @@ -279,6 +280,7 @@ toStrand( seenDirectIssues, seenBookOuts, ammContext, + domainID, j}; }; @@ -476,6 +478,7 @@ toStrands( bool ownerPaysTransferFee, OfferCrossing offerCrossing, AMMContext& ammContext, + std::optional const& domainID, beast::Journal j) { std::vector result; @@ -502,6 +505,7 @@ toStrands( ownerPaysTransferFee, offerCrossing, ammContext, + domainID, j); auto const ter = sp.first; auto& strand = sp.second; @@ -546,6 +550,7 @@ toStrands( ownerPaysTransferFee, offerCrossing, ammContext, + domainID, j); auto ter = sp.first; auto& strand = sp.second; @@ -592,6 +597,7 @@ StrandContext::StrandContext( std::array, 2>& seenDirectIssues_, boost::container::flat_set& seenBookOuts_, AMMContext& ammContext_, + std::optional const& domainID_, beast::Journal j_) : view(view_) , strandSrc(strandSrc_) @@ -608,6 +614,7 @@ StrandContext::StrandContext( , seenDirectIssues(seenDirectIssues_) , seenBookOuts(seenBookOuts_) , ammContext(ammContext_) + , domainID(domainID_) , j(j_) { } diff --git a/src/xrpld/app/paths/detail/Steps.h b/src/xrpld/app/paths/detail/Steps.h index bb9abf6545..0fcdc85fe1 100644 --- a/src/xrpld/app/paths/detail/Steps.h +++ b/src/xrpld/app/paths/detail/Steps.h @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -388,6 +389,7 @@ normalizePath( owner @param offerCrossing false -> payment; true -> offer crossing @param ammContext counts iterations with AMM offers + @param domainID the domain that order books will use @param j Journal for logging messages @return Error code and constructed Strand */ @@ -403,6 +405,7 @@ toStrand( bool ownerPaysTransferFee, OfferCrossing offerCrossing, AMMContext& ammContext, + std::optional const& domainID, beast::Journal j); /** @@ -427,6 +430,7 @@ toStrand( owner @param offerCrossing false -> payment; true -> offer crossing @param ammContext counts iterations with AMM offers + @param domainID the domain that order books will use @param j Journal for logging messages @return error code and collection of strands */ @@ -443,6 +447,7 @@ toStrands( bool ownerPaysTransferFee, OfferCrossing offerCrossing, AMMContext& ammContext, + std::optional const& domainID, beast::Journal j); /// @cond INTERNAL @@ -553,6 +558,7 @@ struct StrandContext */ boost::container::flat_set& seenBookOuts; AMMContext& ammContext; + std::optional domainID; // the domain the order book will use beast::Journal const j; /** StrandContext constructor. */ @@ -574,6 +580,7 @@ struct StrandContext boost::container::flat_set& seenBookOuts_, ///< For detecting book loops AMMContext& ammContext_, + std::optional const& domainID, beast::Journal j_); ///< Journal for logging }; 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/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index 0f14fb3ac6..14bc8f7cda 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -323,7 +323,7 @@ applyCreate( << amount2; auto addOrderBook = [&](Issue const& issueIn, Issue const& issueOut, std::uint64_t uRate) { - Book const book{issueIn, issueOut}; + Book const book{issueIn, issueOut, std::nullopt}; auto const dir = keylet::quality(keylet::book(book), uRate); if (auto const bookExisted = static_cast(sb.read(dir)); !bookExisted) 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..b60942487d --- /dev/null +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -0,0 +1,481 @@ +//------------------------------------------------------------------------------ +/* + 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; +} + +bool +Batch::isEnabled(PreflightContext const& ctx) +{ + return ctx.rules.enabled(featureBatch); +} + +std::uint32_t +Batch::getFlagsMask(PreflightContext const& ctx) +{ + return tfBatchMask; +} + +/** + * @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::doPreflight(PreflightContext const& ctx) +{ + auto const parentBatchId = ctx.tx.getTransactionID(); + auto const outerAccount = ctx.tx.getAccountID(sfAccount); + auto const flags = ctx.tx.getFlags(); + + 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 = detail::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..858a3d9c3d --- /dev/null +++ b/src/xrpld/app/tx/detail/Batch.h @@ -0,0 +1,61 @@ +//------------------------------------------------------------------------------ +/* + 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 bool + isEnabled(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + + static NotTEC + checkSign(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index 5ab84f0524..02babcfbe5 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -443,6 +443,7 @@ CashCheck::doApply() OfferCrossing::no, std::nullopt, sleCheck->getFieldAmount(sfSendMax), + std::nullopt, // check does not support domain viewJ); if (result.result() != tesSUCCESS) diff --git a/src/xrpld/app/tx/detail/CreateOffer.cpp b/src/xrpld/app/tx/detail/CreateOffer.cpp index ab6c0c1fbb..815bad994d 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.cpp +++ b/src/xrpld/app/tx/detail/CreateOffer.cpp @@ -18,16 +18,20 @@ //============================================================================== #include +#include #include #include #include +#include #include #include #include +#include +#include +#include namespace ripple { - TxConsequences CreateOffer::makeTxConsequences(PreflightContext const& ctx) { @@ -39,10 +43,26 @@ CreateOffer::makeTxConsequences(PreflightContext const& ctx) return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)}; } +bool +CreateOffer::isEnabled(PreflightContext const& ctx) +{ + // Permissioned offers should use the PE (which must be enabled by + // featureFlowCross amendment) + if (ctx.rules.enabled(featurePermissionedDEX) && + !ctx.rules.enabled(featureFlowCross)) + return false; + + return (!ctx.tx.isFieldPresent(sfDomainID)) || + ctx.rules.enabled(featurePermissionedDEX); +} + std::uint32_t CreateOffer::getFlagsMask(PreflightContext const& ctx) { - return tfOfferCreateMask; + if (ctx.rules.enabled(featurePermissionedDEX) && + ctx.tx.isFieldPresent(sfDomainID)) + return tfOfferCreateMask; + return tfOfferCreateMask | tfHybrid; } NotTEC @@ -52,7 +72,6 @@ CreateOffer::doPreflight(PreflightContext const& ctx) auto& j = ctx.j; std::uint32_t const uTxFlags = tx.getFlags(); - bool const bImmediateOrCancel(uTxFlags & tfImmediateOrCancel); bool const bFillOrKill(uTxFlags & tfFillOrKill); @@ -195,6 +214,15 @@ CreateOffer::preclaim(PreclaimContext const& ctx) return result; } + // if domain is specified, make sure that domain exists and the offer create + // is part of the domain + if (ctx.tx.isFieldPresent(sfDomainID)) + { + if (!permissioned_dex::accountInDomain( + ctx.view, id, ctx.tx[sfDomainID])) + return tecNO_PERMISSION; + } + return tesSUCCESS; } @@ -364,7 +392,7 @@ CreateOffer::bridged_cross( OfferStream offers_direct( view, view_cancel, - Book(taker.issue_in(), taker.issue_out()), + Book(taker.issue_in(), taker.issue_out(), std::nullopt), when, stepCounter_, j_); @@ -372,7 +400,7 @@ CreateOffer::bridged_cross( OfferStream offers_leg1( view, view_cancel, - Book(taker.issue_in(), xrpIssue()), + Book(taker.issue_in(), xrpIssue(), std::nullopt), when, stepCounter_, j_); @@ -380,7 +408,7 @@ CreateOffer::bridged_cross( OfferStream offers_leg2( view, view_cancel, - Book(xrpIssue(), taker.issue_out()), + Book(xrpIssue(), taker.issue_out(), std::nullopt), when, stepCounter_, j_); @@ -548,7 +576,7 @@ CreateOffer::direct_cross( OfferStream offers( view, view_cancel, - Book(taker.issue_in(), taker.issue_out()), + Book(taker.issue_in(), taker.issue_out(), std::nullopt), when, stepCounter_, j_); @@ -705,7 +733,8 @@ std::pair CreateOffer::flowCross( PaymentSandbox& psb, PaymentSandbox& psbCancel, - Amounts const& takerAmount) + Amounts const& takerAmount, + std::optional const& domainID) { try { @@ -802,6 +831,7 @@ CreateOffer::flowCross( offerCrossing, threshold, sendMax, + domainID, j_); // If stale offers were found remove them. @@ -904,13 +934,18 @@ CreateOffer::flowCross( } std::pair -CreateOffer::cross(Sandbox& sb, Sandbox& sbCancel, Amounts const& takerAmount) +CreateOffer::cross( + Sandbox& sb, + Sandbox& sbCancel, + Amounts const& takerAmount, + std::optional const& domainID) { if (sb.rules().enabled(featureFlowCross)) { PaymentSandbox psbFlow{&sb}; PaymentSandbox psbCancelFlow{&sbCancel}; - auto const ret = flowCross(psbFlow, psbCancelFlow, takerAmount); + auto const ret = + flowCross(psbFlow, psbCancelFlow, takerAmount, domainID); psbFlow.apply(sb); psbCancelFlow.apply(sbCancel); return ret; @@ -947,6 +982,54 @@ CreateOffer::preCompute() return Transactor::preCompute(); } +TER +CreateOffer::applyHybrid( + Sandbox& sb, + std::shared_ptr sleOffer, + Keylet const& offerKey, + STAmount const& saTakerPays, + STAmount const& saTakerGets, + std::function)> const& setDir) +{ + if (!sleOffer->isFieldPresent(sfDomainID)) + return tecINTERNAL; // LCOV_EXCL_LINE + + // set hybrid flag + sleOffer->setFlag(lsfHybrid); + + // if offer is hybrid, need to also place into open offer dir + Book const book{saTakerPays.issue(), saTakerGets.issue(), std::nullopt}; + + auto dir = + keylet::quality(keylet::book(book), getRate(saTakerGets, saTakerPays)); + bool const bookExists = sb.exists(dir); + + auto const bookNode = sb.dirAppend(dir, offerKey, [&](SLE::ref sle) { + // don't set domainID on the directory object since this directory is + // for open book + setDir(sle, std::nullopt); + }); + + if (!bookNode) + { + JLOG(j_.debug()) + << "final result: failed to add hybrid offer to open book"; + return tecDIR_FULL; // LCOV_EXCL_LINE + } + + STArray bookArr(sfAdditionalBooks, 1); + auto bookInfo = STObject::makeInnerObject(sfBook); + bookInfo.setFieldH256(sfBookDirectory, dir.key); + bookInfo.setFieldU64(sfBookNode, *bookNode); + bookArr.push_back(std::move(bookInfo)); + + if (!bookExists) + ctx_.app.getOrderBookDB().addOrderBook(book); + + sleOffer->setFieldArray(sfAdditionalBooks, bookArr); + return tesSUCCESS; +} + std::pair CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) { @@ -958,9 +1041,11 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) bool const bImmediateOrCancel(uTxFlags & tfImmediateOrCancel); bool const bFillOrKill(uTxFlags & tfFillOrKill); bool const bSell(uTxFlags & tfSell); + bool const bHybrid(uTxFlags & tfHybrid); auto saTakerPays = ctx_.tx[sfTakerPays]; auto saTakerGets = ctx_.tx[sfTakerGets]; + auto const domainID = ctx_.tx[~sfDomainID]; auto const cancelSequence = ctx_.tx[~sfOfferSequence]; @@ -1077,7 +1162,8 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) stream << " out: " << format_amount(takerAmount.out); } - std::tie(result, place_offer) = cross(sb, sbCancel, takerAmount); + std::tie(result, place_offer) = + cross(sb, sbCancel, takerAmount, domainID); // We expect the implementation of cross to succeed // or give a tec. @@ -1219,21 +1305,39 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) adjustOwnerCount(sb, sleCreator, 1, viewJ); JLOG(j_.trace()) << "adding to book: " << to_string(saTakerPays.issue()) - << " : " << to_string(saTakerGets.issue()); + << " : " << to_string(saTakerGets.issue()) + << (domainID ? (" : " + to_string(*domainID)) : ""); - Book const book{saTakerPays.issue(), saTakerGets.issue()}; + Book const book{saTakerPays.issue(), saTakerGets.issue(), domainID}; // Add offer to order book, using the original rate // before any crossing occured. + // + // Regular offer - BookDirectory points to open directory + // + // Domain offer (w/o hyrbid) - BookDirectory points to domain + // directory + // + // Hybrid domain offer - BookDirectory points to domain directory, + // and AdditionalBooks field stores one entry that points to the open + // directory auto dir = keylet::quality(keylet::book(book), uRate); bool const bookExisted = static_cast(sb.peek(dir)); - auto const bookNode = sb.dirAppend(dir, offer_index, [&](SLE::ref sle) { + auto setBookDir = [&](SLE::ref sle, + std::optional const& maybeDomain) { sle->setFieldH160(sfTakerPaysCurrency, saTakerPays.issue().currency); sle->setFieldH160(sfTakerPaysIssuer, saTakerPays.issue().account); sle->setFieldH160(sfTakerGetsCurrency, saTakerGets.issue().currency); sle->setFieldH160(sfTakerGetsIssuer, saTakerGets.issue().account); sle->setFieldU64(sfExchangeRate, uRate); + if (maybeDomain) + sle->setFieldH256(sfDomainID, *maybeDomain); + }; + + auto const bookNode = sb.dirAppend(dir, offer_index, [&](SLE::ref sle) { + // sets domainID on book directory if it's a domain offer + setBookDir(sle, domainID); }); if (!bookNode) @@ -1256,6 +1360,18 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) sleOffer->setFlag(lsfPassive); if (bSell) sleOffer->setFlag(lsfSell); + if (domainID) + sleOffer->setFieldH256(sfDomainID, *domainID); + + // if it's a hybrid offer, set hybrid flag, and create an open dir + if (bHybrid) + { + auto const res = applyHybrid( + sb, sleOffer, offer_index, saTakerPays, saTakerGets, setBookDir); + if (res != tesSUCCESS) + return {res, true}; // LCOV_EXCL_LINE + } + sb.insert(sleOffer); if (!bookExisted) diff --git a/src/xrpld/app/tx/detail/CreateOffer.h b/src/xrpld/app/tx/detail/CreateOffer.h index d654c14042..cff1022a6b 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.h +++ b/src/xrpld/app/tx/detail/CreateOffer.h @@ -44,6 +44,9 @@ public: static TxConsequences makeTxConsequences(PreflightContext const& ctx); + static bool + isEnabled(PreflightContext const& ctx); + static std::uint32_t getFlagsMask(PreflightContext const& ctx); @@ -124,18 +127,32 @@ private: flowCross( PaymentSandbox& psb, PaymentSandbox& psbCancel, - Amounts const& takerAmount); + Amounts const& takerAmount, + std::optional const& domainID); // Temporary // This is a central location that invokes both versions of cross // so the results can be compared. Eventually this layer will be // removed once flowCross is determined to be stable. std::pair - cross(Sandbox& sb, Sandbox& sbCancel, Amounts const& takerAmount); + cross( + Sandbox& sb, + Sandbox& sbCancel, + Amounts const& takerAmount, + std::optional const& domainID); static std::string format_amount(STAmount const& amount); + TER + applyHybrid( + Sandbox& sb, + std::shared_ptr sleOffer, + Keylet const& offer_index, + STAmount const& saTakerPays, + STAmount const& saTakerGets, + std::function)> const& setDir); + private: // What kind of offer we are placing CrossType cross_type_; diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index 284c5f27eb..a56870c1b9 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -1640,6 +1640,89 @@ ValidPermissionedDomain::finalize( (sleStatus_[1] ? check(*sleStatus_[1], j) : true); } +void +ValidPermissionedDEX::visitEntry( + bool, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (after && after->getType() == ltDIR_NODE) + { + if (after->isFieldPresent(sfDomainID)) + domains_.insert(after->getFieldH256(sfDomainID)); + } + + if (after && after->getType() == ltOFFER) + { + if (after->isFieldPresent(sfDomainID)) + domains_.insert(after->getFieldH256(sfDomainID)); + else + regularOffers_ = true; + + // if a hybrid offer is missing domain or additional book, there's + // something wrong + if (after->isFlag(lsfHybrid) && + (!after->isFieldPresent(sfDomainID) || + !after->isFieldPresent(sfAdditionalBooks) || + after->getFieldArray(sfAdditionalBooks).size() > 1)) + badHybrids_ = true; + } +} + +bool +ValidPermissionedDEX::finalize( + STTx const& tx, + TER const result, + XRPAmount const, + ReadView const& view, + beast::Journal const& j) +{ + auto const txType = tx.getTxnType(); + if ((txType != ttPAYMENT && txType != ttOFFER_CREATE) || + result != tesSUCCESS) + return true; + + // For each offercreate transaction, check if + // permissioned offers are valid + if (txType == ttOFFER_CREATE && badHybrids_) + { + JLOG(j.fatal()) << "Invariant failed: hybrid offer is malformed"; + return false; + } + + if (!tx.isFieldPresent(sfDomainID)) + return true; + + auto const domain = tx.getFieldH256(sfDomainID); + + if (!view.exists(keylet::permissionedDomain(domain))) + { + JLOG(j.fatal()) << "Invariant failed: domain doesn't exist"; + return false; + } + + // for both payment and offercreate, there shouldn't be another domain + // that's different from the domain specified + for (auto const& d : domains_) + { + if (d != domain) + { + JLOG(j.fatal()) << "Invariant failed: transaction" + " consumed wrong domains"; + return false; + } + } + + if (regularOffers_) + { + JLOG(j.fatal()) << "Invariant failed: domain transaction" + " affected regular offers"; + return false; + } + + return true; +} + //------------------------------------------------------------------------------ void diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 7f55cd73a6..f7af0da725 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.h +++ b/src/xrpld/app/tx/detail/InvariantCheck.h @@ -28,6 +28,7 @@ #include #include +#include namespace ripple { @@ -625,6 +626,28 @@ public: beast::Journal const&); }; +class ValidPermissionedDEX +{ + bool regularOffers_ = false; + bool badHybrids_ = false; + hash_set domains_; + +public: + void + visitEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&); + + bool + finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const&); +}; + /** * @brief Invariants: Some fields are unmodifiable * @@ -765,6 +788,7 @@ using InvariantChecks = std::tuple< ValidClawback, ValidMPTIssuance, ValidPermissionedDomain, + ValidPermissionedDEX, NoModifiedUnmodifiableFields, ValidPseudoAccounts, ValidLoanBroker, diff --git a/src/xrpld/app/tx/detail/OfferStream.cpp b/src/xrpld/app/tx/detail/OfferStream.cpp index 7640cca206..55993f5c5f 100644 --- a/src/xrpld/app/tx/detail/OfferStream.cpp +++ b/src/xrpld/app/tx/detail/OfferStream.cpp @@ -17,10 +17,13 @@ */ //============================================================================== +#include #include +#include #include #include +#include namespace ripple { @@ -288,6 +291,17 @@ TOfferStreamBase::step() continue; } + if (entry->isFieldPresent(sfDomainID) && + !permissioned_dex::offerInDomain( + view_, entry->key(), entry->getFieldH256(sfDomainID), j_)) + { + JLOG(j_.trace()) + << "Removing offer no longer in domain " << entry->key(); + permRmOffer(entry->key()); + offer_ = TOffer{}; + continue; + } + // Calculate owner funds ownerFunds_ = accountFundsHelper( view_, diff --git a/src/xrpld/app/tx/detail/Payment.cpp b/src/xrpld/app/tx/detail/Payment.cpp index 1b6573181f..c062abad1c 100644 --- a/src/xrpld/app/tx/detail/Payment.cpp +++ b/src/xrpld/app/tx/detail/Payment.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -67,8 +68,14 @@ getMaxSourceAmount( bool Payment::isEnabled(PreflightContext const& ctx) { - return !ctx.tx.isFieldPresent(sfCredentialIDs) || - ctx.rules.enabled(featureCredentials); + if (ctx.tx.isFieldPresent(sfCredentialIDs) && + !ctx.rules.enabled(featureCredentials)) + return false; + if (ctx.tx.isFieldPresent(sfDomainID) && + !ctx.rules.enabled(featurePermissionedDEX)) + return false; + + return true; } std::uint32_t @@ -360,6 +367,17 @@ Payment::preclaim(PreclaimContext const& ctx) !isTesSuccess(err)) return err; + if (ctx.tx.isFieldPresent(sfDomainID)) + { + if (!permissioned_dex::accountInDomain( + ctx.view, ctx.tx[sfAccount], ctx.tx[sfDomainID])) + return tecNO_PERMISSION; + + if (!permissioned_dex::accountInDomain( + ctx.view, ctx.tx[sfDestination], ctx.tx[sfDomainID])) + return tecNO_PERMISSION; + } + return tesSUCCESS; } @@ -461,6 +479,7 @@ Payment::doApply() dstAccountID, account_, ctx_.tx.getFieldPathSet(sfPaths), + ctx_.tx[~sfDomainID], ctx_.app.logs(), &rcInput); // VFALCO NOTE We might not need to apply, depending diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index f24ddd38cd..659ff41f12 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -211,7 +211,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 0cbd3c0fcc..5d3dca1c81 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -43,6 +43,13 @@ namespace ripple { NotTEC preflight0(PreflightContext const& ctx, std::uint32_t flagMask) { + 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; @@ -157,6 +164,14 @@ preflight1(PreflightContext const& ctx, std::uint32_t flagMask) 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; } @@ -210,7 +225,7 @@ 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; } @@ -219,18 +234,6 @@ preflight2(PreflightContext const& ctx) //------------------------------------------------------------------------------ -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) , sink_(ctx.journal, to_short_string(ctx.tx.getTransactionID()) + " ") @@ -318,6 +321,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; @@ -624,11 +637,11 @@ Transactor::apply() NotTEC Transactor::checkSign( PreclaimContext const& ctx, - AccountID const& id, + AccountID const& idAccount, STObject const& sigObject) { { - auto const sle = ctx.view.read(keylet::account(id)); + auto const sle = ctx.view.read(keylet::account(idAccount)); if (ctx.view.rules().enabled(featureLendingProtocol) && isPseudoAccount(sle)) @@ -637,19 +650,53 @@ Transactor::checkSign( // added under, and it doesn't justify another amendment return tefBAD_AUTH; } - if (ctx.flags & tapDRY_RUN) - { - // This code must be different for `simulate` - // Since the public key may be empty even for single signing - if (sigObject.isFieldPresent(sfSigners)) - return checkMultiSign(ctx, id, sigObject); - return checkSingleSign(ctx, id, sigObject); - } - // If the pk is empty, then we must be multi-signing. - if (sigObject.getFieldVL(sfSigningPubKey).empty()) - return checkMultiSign(ctx, id, sigObject); - return checkSingleSign(ctx, id, sigObject); + // Ignore signature check on batch inner transactions + if (sigObject.isFlag(tfInnerBatchTxn) && + ctx.view.rules().enabled(featureBatch)) + { + // Defensive Check: These values are also checked in Batch::preflight + if (sigObject.isFieldPresent(sfTxnSignature) || + !sigObject.getFieldVL(sfSigningPubKey).empty() || + sigObject.isFieldPresent(sfSigners)) + { + return temINVALID_FLAG; // LCOV_EXCL_LINE + } + return tesSUCCESS; + } + + // If the pk is empty and not simulate or simulate and signers, + // then we must be multi-signing. + if ((ctx.flags & tapDRY_RUN && sigObject.isFieldPresent(sfSigners)) || + (!(ctx.flags & tapDRY_RUN) && + sigObject.getFieldVL(sfSigningPubKey).empty())) + { + return checkMultiSign(ctx, idAccount, sigObject); + } + + // Check Single Sign + auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey); + // 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 idSigner = pkSigner.empty() + ? idAccount + : calcAccountID(PublicKey(makeSlice(pkSigner))); + auto const sleAccount = ctx.view.read(keylet::account(idAccount)); + if (!sleAccount) + return terNO_ACCOUNT; + + return checkSingleSign(ctx, idSigner, idAccount, sleAccount); } NotTEC @@ -661,34 +708,57 @@ Transactor::checkSign(PreclaimContext const& ctx) return checkSign(ctx, idAccount, ctx.tx); } -// TODO generalize +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()) + { + if (ret = checkMultiSign(ctx, idAccount, signer); + !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(ctx, idSigner, idAccount, sleAccount); + !isTesSuccess(ret)) + return ret; + } + } + return ret; +} + NotTEC Transactor::checkSingleSign( PreclaimContext const& ctx, + AccountID const& idSigner, AccountID const& idAccount, - STObject const& sigObject) + std::shared_ptr sleAccount) { - // Check that the value in the signing key slot is a public key. - auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey); - 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 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))); bool const isMasterDisabled = sleAccount->isFlag(lsfDisableMaster); if (ctx.view.rules().enabled(fixMasterKeyAsRegularKey)) @@ -855,7 +925,8 @@ Transactor::checkMultiSign( // 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 = + ctx.view.read(keylet::account(txSignerAcctID)); if (signingAcctIDFromPubKey == txSignerAcctID) { @@ -995,7 +1066,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) { @@ -1003,9 +1078,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) @@ -1115,7 +1191,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 cf57017e3e..e5e932f566 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -38,14 +38,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; @@ -58,8 +82,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( @@ -68,14 +93,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& @@ -143,6 +193,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); @@ -256,8 +309,9 @@ private: static NotTEC checkSingleSign( PreclaimContext const& ctx, - AccountID const& id, - STObject const& sigObject); + AccountID const& idSigner, + AccountID const& idAccount, + std::shared_ptr sleAccount); static NotTEC checkMultiSign( PreclaimContext const& ctx, diff --git a/src/xrpld/app/tx/detail/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index fa188c45c5..93619e620c 100644 --- a/src/xrpld/app/tx/detail/XChainBridge.cpp +++ b/src/xrpld/app/tx/detail/XChainBridge.cpp @@ -511,6 +511,7 @@ transferHelper( /*offer crossing*/ OfferCrossing::no, /*limit quality*/ std::nullopt, /*sendmax*/ std::nullopt, + /*domain id*/ std::nullopt, j); if (auto const r = result.result(); 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 476021cbd6..fbb117c372 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -68,7 +68,6 @@ with_txn_type(TxType txnType, F&& f) #undef TRANSACTION #pragma pop_macro("TRANSACTION") - default: throw UnknownTxnType(txnType); } @@ -191,6 +190,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) { @@ -284,7 +299,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}}}; } } @@ -298,18 +334,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 @@ -320,8 +369,10 @@ preclaim( preflightResult.ter, preflightResult.tx, preflightResult.flags, + preflightResult.parentBatchId, preflightResult.j); } + try { if (ctx->preflightResult != tesSUCCESS) @@ -330,7 +381,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}; } } @@ -363,6 +414,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 b55a2b4e8e..a3fbbdea55 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 d90f7a1ced..521e1d11ab 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -37,7 +37,6 @@ #include #include -#include #include #include @@ -1060,8 +1059,8 @@ AccountID pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey) { // This number must not be changed without an amendment - constexpr int maxAccountAttempts = 256; - for (auto i = 0; i < maxAccountAttempts; ++i) + constexpr std::uint16_t maxAccountAttempts = 256; + for (std::uint16_t i = 0; i < maxAccountAttempts; ++i) { ripesha_hasher rsh; auto const hash = sha512Half(i, view.info().parentHash, pseudoOwnerKey); @@ -1546,6 +1545,27 @@ offerDelete(ApplyView& view, std::shared_ptr const& sle, beast::Journal j) return tefBAD_LEDGER; } + if (sle->isFieldPresent(sfAdditionalBooks)) + { + XRPL_ASSERT( + sle->isFlag(lsfHybrid) && sle->isFieldPresent(sfDomainID), + "ripple::offerDelete : should be a hybrid domain offer"); + + auto const& additionalBookDirs = sle->getFieldArray(sfAdditionalBooks); + + for (auto const& bookDir : additionalBookDirs) + { + auto const& dirIndex = bookDir.getFieldH256(sfBookDirectory); + auto const& dirNode = bookDir.getFieldU64(sfBookNode); + + if (!view.dirRemove( + keylet::page(dirIndex), dirNode, offerIndex, false)) + { + return tefBAD_LEDGER; // LCOV_EXCL_LINE + } + } + } + adjustOwnerCount(view, view.peek(keylet::account(owner)), -1, j); view.erase(sle); @@ -2453,8 +2473,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) { @@ -2465,14 +2496,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/BookChanges.h b/src/xrpld/rpc/BookChanges.h index c87fa0ccf4..9d94e80b82 100644 --- a/src/xrpld/rpc/BookChanges.h +++ b/src/xrpld/rpc/BookChanges.h @@ -49,13 +49,13 @@ computeBookChanges(std::shared_ptr const& lpAccepted) std::map< std::string, std::tuple< - STAmount, // side A volume - STAmount, // side B volume - STAmount, // high rate - STAmount, // low rate - STAmount, // open rate - STAmount // close rate - >> + STAmount, // side A volume + STAmount, // side B volume + STAmount, // high rate + STAmount, // low rate + STAmount, // open rate + STAmount, // close rate + std::optional>> // optional: domain id tally; for (auto& tx : lpAccepted->txs) @@ -148,6 +148,8 @@ computeBookChanges(std::shared_ptr const& lpAccepted) else ss << p << "|" << g; + std::optional domain = finalFields[~sfDomainID]; + std::string key{ss.str()}; if (tally.find(key) == tally.end()) @@ -157,8 +159,8 @@ computeBookChanges(std::shared_ptr const& lpAccepted) rate, // high rate, // low rate, // open - rate // close - }; + rate, // close + domain}; else { // increment volume @@ -173,7 +175,8 @@ computeBookChanges(std::shared_ptr const& lpAccepted) if (std::get<3>(entry) > rate) // low std::get<3>(entry) = rate; - std::get<5>(entry) = rate; // close + std::get<5>(entry) = rate; // close + std::get<6>(entry) = domain; // domain } } } @@ -211,6 +214,10 @@ computeBookChanges(std::shared_ptr const& lpAccepted) inner[jss::low] = to_string(std::get<3>(entry.second).iou()); inner[jss::open] = to_string(std::get<4>(entry.second).iou()); inner[jss::close] = to_string(std::get<5>(entry.second).iou()); + + std::optional const domain = std::get<6>(entry.second); + if (domain) + inner[jss::domain] = to_string(*domain); } return jvObj; diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 9387aba505..175fd84c9b 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -40,6 +40,7 @@ #include #include +#include namespace ripple { namespace RPC { @@ -222,6 +223,22 @@ checkPayment( rpcINVALID_PARAMS, "Cannot specify both 'tx_json.Paths' and 'build_path'"); + std::optional domain; + if (tx_json.isMember(sfDomainID.jsonName)) + { + uint256 num; + if (!tx_json[sfDomainID.jsonName].isString() || + !num.parseHex(tx_json[sfDomainID.jsonName].asString())) + { + return RPC::make_error( + rpcDOMAIN_MALFORMED, "Unable to parse 'DomainID'."); + } + else + { + domain = num; + } + } + if (!tx_json.isMember(jss::Paths) && params.isMember(jss::build_path)) { STAmount sendMax; @@ -260,6 +277,7 @@ checkPayment( sendMax.issue().account, amount, std::nullopt, + domain, app); if (pf.findPaths(app.config().PATH_SEARCH_OLD)) { @@ -464,9 +482,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 +764,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 +792,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 +823,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/BookOffers.cpp b/src/xrpld/rpc/handlers/BookOffers.cpp index bede01b927..df4712209c 100644 --- a/src/xrpld/rpc/handlers/BookOffers.cpp +++ b/src/xrpld/rpc/handlers/BookOffers.cpp @@ -172,6 +172,22 @@ doBookOffers(RPC::JsonContext& context) return RPC::invalid_field_error(jss::taker); } + std::optional domain; + if (context.params.isMember(jss::domain)) + { + uint256 num; + if (!context.params[jss::domain].isString() || + !num.parseHex(context.params[jss::domain].asString())) + { + return RPC::make_error( + rpcDOMAIN_MALFORMED, "Unable to parse domain."); + } + else + { + domain = num; + } + } + if (pay_currency == get_currency && pay_issuer == get_issuer) { JLOG(context.j.info()) << "taker_gets same as taker_pays."; @@ -190,7 +206,7 @@ doBookOffers(RPC::JsonContext& context) context.netOps.getBookPage( lpLedger, - {{pay_currency, pay_issuer}, {get_currency, get_issuer}}, + {{pay_currency, pay_issuer}, {get_currency, get_issuer}, domain}, takerID ? *takerID : beast::zero, bProof, limit, 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 diff --git a/src/xrpld/rpc/handlers/Subscribe.cpp b/src/xrpld/rpc/handlers/Subscribe.cpp index deac6e18ad..e71d973b7b 100644 --- a/src/xrpld/rpc/handlers/Subscribe.cpp +++ b/src/xrpld/rpc/handlers/Subscribe.cpp @@ -305,6 +305,20 @@ doSubscribe(RPC::JsonContext& context) return rpcError(rpcBAD_ISSUER); } + if (j.isMember(jss::domain)) + { + uint256 domain; + if (!j[jss::domain].isString() || + !domain.parseHex(j[jss::domain].asString())) + { + return rpcError(rpcDOMAIN_MALFORMED); + } + else + { + book.domain = domain; + } + } + if (!isConsistent(book)) { JLOG(context.j.warn()) << "Bad market: " << book; diff --git a/src/xrpld/rpc/handlers/Unsubscribe.cpp b/src/xrpld/rpc/handlers/Unsubscribe.cpp index c890de593a..f512840c86 100644 --- a/src/xrpld/rpc/handlers/Unsubscribe.cpp +++ b/src/xrpld/rpc/handlers/Unsubscribe.cpp @@ -230,6 +230,20 @@ doUnsubscribe(RPC::JsonContext& context) return rpcError(rpcBAD_MARKET); } + if (jv.isMember(jss::domain)) + { + uint256 domain; + if (!jv[jss::domain].isString() || + !domain.parseHex(jv[jss::domain].asString())) + { + return rpcError(rpcDOMAIN_MALFORMED); + } + else + { + book.domain = domain; + } + } + context.netOps.unsubBook(ispSub->getSeq(), book); // both_sides is deprecated.