From 70371a4344a22439f3a24b9fc69a3c2576425774 Mon Sep 17 00:00:00 2001 From: Olek <115580134+oleks-rip@users.noreply.github.com> Date: Wed, 21 May 2025 13:28:18 -0400 Subject: [PATCH 01/14] Fix initializer list initialization for GCC-15 (#5443) --- include/xrpl/beast/hash/uhash.h | 2 +- include/xrpl/beast/net/IPEndpoint.h | 4 ++-- include/xrpl/protocol/AccountID.h | 2 +- include/xrpl/protocol/Book.h | 8 ++++---- include/xrpl/protocol/UintTypes.h | 8 ++++---- include/xrpl/resource/detail/Key.h | 2 +- src/xrpld/app/paths/RippleLineCache.h | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) 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/Book.h b/include/xrpl/protocol/Book.h index 0a04deb277..0fcff0df80 100644 --- a/include/xrpl/protocol/Book.h +++ b/include/xrpl/protocol/Book.h @@ -104,7 +104,7 @@ private: boost::base_from_member, 1>; public: - explicit hash() = default; + hash() = default; using value_type = std::size_t; using argument_type = ripple::Issue; @@ -131,7 +131,7 @@ private: hasher m_hasher; public: - explicit hash() = default; + hash() = default; using value_type = std::size_t; using argument_type = ripple::Book; @@ -154,7 +154,7 @@ namespace boost { template <> struct hash : std::hash { - explicit hash() = default; + hash() = default; using Base = std::hash; // VFALCO NOTE broken in vs2012 @@ -164,7 +164,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/UintTypes.h b/include/xrpl/protocol/UintTypes.h index 9a7284158e..d6cdc9350e 100644 --- a/include/xrpl/protocol/UintTypes.h +++ b/include/xrpl/protocol/UintTypes.h @@ -119,25 +119,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/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/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 From 7713ff8c5c18f9f814e0cce61772af972333bf8f Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Thu, 22 May 2025 15:43:41 +0100 Subject: [PATCH 02/14] Add codecov badge, raise .codecov.yml thresholds (#5428) --- .codecov.yml | 4 ++-- README.md | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) 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/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. From 40ce8a883343bdc20e767998cee7f3b02421047c Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Fri, 23 May 2025 15:05:36 +0100 Subject: [PATCH 03/14] fix: Fix pseudo-account ID calculation (#5447) Before #5224, the pseudoaccount ID was calculated using prefix expressed in `std::uint16_t`. The refactoring to move the pseudoaccount ID calculation to View.cpp had accidentally changed the prefix type to `int` (derived from `auto i = 0`) which in turn changed the length of the input to `sha512Half` from 2 bytes to 4, altering the result. This resulted in a different ID of the pseudoaccount calculated from the function after the refactoring, breaking the ledger. This impacts AMMCreate, even when the `SingleAssetVault` amendment is not active. This change restores the prefix type to `std::uint16_t`. --- src/xrpld/ledger/detail/View.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xrpld/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index d248d37e18..aa6e2dda8f 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -1056,8 +1056,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); From 2a61aee5620725f8249e3066616ef6e20c99c857 Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Fri, 23 May 2025 21:53:53 +0200 Subject: [PATCH 04/14] Add Batch feature (XLS-56) (#5060) - Specification: [XRPLF/XRPL-Standards 56](https://github.com/XRPLF/XRPL-Standards/blob/master/XLS-0056d-batch/README.md) - Amendment: `Batch` - Implements execution of multiple transactions within a single batch transaction with four execution modes: `tfAllOrNothing`, `tfOnlyOne`, `tfUntilFailure`, and `tfIndependent`. - Enables atomic multi-party transactions where multiple accounts can participate in a single batch, with up to 8 inner transactions and 8 batch signers per batch transaction. - Inner transactions use `tfInnerBatchTxn` flag with zero fees, no signature, and empty signing public key. - Inner transactions are applied after the outer batch succeeds via the `applyBatchTransactions` function in apply.cpp. - Network layer prevents relay of transactions with `tfInnerBatchTxn` flag - each peer applies inner transactions locally from the batch. - Batch transactions are excluded from AccountDelegate permissions but inner transactions retain full delegation support. - Metadata includes `ParentBatchID` linking inner transactions to their containing batch for traceability and auditing. - Extended STTx with batch-specific signature verification methods and added protocol structures (`sfRawTransactions`, `sfBatchSigners`). --- include/xrpl/protocol/Batch.h | 37 + include/xrpl/protocol/HashPrefix.h | 3 + include/xrpl/protocol/Protocol.h | 3 + include/xrpl/protocol/STTx.h | 21 + include/xrpl/protocol/TER.h | 24 +- include/xrpl/protocol/TxFlags.h | 18 +- include/xrpl/protocol/TxMeta.h | 27 +- include/xrpl/protocol/detail/features.macro | 1 + include/xrpl/protocol/detail/sfields.macro | 5 + .../xrpl/protocol/detail/transactions.macro | 7 +- include/xrpl/protocol/jss.h | 2 + src/libxrpl/protocol/InnerObjectFormats.cpp | 7 + src/libxrpl/protocol/STTx.cpp | 253 +- src/libxrpl/protocol/TER.cpp | 1 + src/libxrpl/protocol/TxMeta.cpp | 16 +- src/test/app/AccountDelete_test.cpp | 29 - src/test/app/Batch_test.cpp | 3860 +++++++++++++++++ src/test/app/Delegate_test.cpp | 1 + src/test/app/MultiSign_test.cpp | 28 +- src/test/app/TxQ_test.cpp | 760 ++-- src/test/jtx.h | 2 + src/test/jtx/SignerUtils.h | 56 + src/test/jtx/TestHelpers.h | 106 +- src/test/jtx/acctdelete.h | 11 + src/test/jtx/batch.h | 169 + src/test/jtx/impl/AMM.cpp | 1 - src/test/jtx/impl/TestHelpers.cpp | 6 - src/test/jtx/impl/acctdelete.cpp | 23 + src/test/jtx/impl/batch.cpp | 154 + src/test/jtx/impl/check.cpp | 3 - src/test/jtx/impl/creds.cpp | 5 - src/test/jtx/impl/dids.cpp | 3 - src/test/jtx/impl/ledgerStateFixes.cpp | 1 - src/test/jtx/impl/multisign.cpp | 11 - src/test/jtx/impl/pay.cpp | 2 +- src/test/jtx/impl/txflags.cpp | 2 +- src/test/jtx/impl/xchain_bridge.cpp | 8 - src/test/jtx/multisign.h | 42 +- src/test/rpc/AccountLines_test.cpp | 2 - src/test/rpc/AccountObjects_test.cpp | 3 - src/test/rpc/AccountTx_test.cpp | 5 - src/test/rpc/JSONRPC_test.cpp | 122 + src/test/rpc/LedgerData_test.cpp | 2 - src/test/rpc/LedgerEntry_test.cpp | 1 - src/test/rpc/Simulate_test.cpp | 31 + src/xrpld/app/ledger/detail/BuildLedger.cpp | 14 +- src/xrpld/app/ledger/detail/OpenLedger.cpp | 14 + src/xrpld/app/misc/NetworkOPs.cpp | 49 +- src/xrpld/app/misc/detail/TxQ.cpp | 14 +- src/xrpld/app/tx/applySteps.h | 18 + src/xrpld/app/tx/detail/ApplyContext.cpp | 8 +- src/xrpld/app/tx/detail/ApplyContext.h | 28 +- src/xrpld/app/tx/detail/Batch.cpp | 482 ++ src/xrpld/app/tx/detail/Batch.h | 55 + src/xrpld/app/tx/detail/SetAccount.cpp | 2 +- src/xrpld/app/tx/detail/Transactor.cpp | 218 +- src/xrpld/app/tx/detail/Transactor.h | 73 +- src/xrpld/app/tx/detail/apply.cpp | 134 +- src/xrpld/app/tx/detail/applySteps.cpp | 71 +- src/xrpld/ledger/ApplyView.h | 3 + src/xrpld/ledger/ApplyViewImpl.h | 1 + src/xrpld/ledger/OpenView.h | 30 +- src/xrpld/ledger/detail/ApplyStateTable.cpp | 5 +- src/xrpld/ledger/detail/ApplyStateTable.h | 1 + src/xrpld/ledger/detail/ApplyViewImpl.cpp | 3 +- src/xrpld/ledger/detail/OpenView.cpp | 6 +- src/xrpld/overlay/detail/PeerImp.cpp | 25 + src/xrpld/rpc/detail/TransactionSign.cpp | 11 +- src/xrpld/rpc/handlers/Simulate.cpp | 5 + 69 files changed, 6400 insertions(+), 744 deletions(-) create mode 100644 include/xrpl/protocol/Batch.h create mode 100644 src/test/app/Batch_test.cpp create mode 100644 src/test/jtx/SignerUtils.h create mode 100644 src/test/jtx/batch.h create mode 100644 src/test/jtx/impl/batch.cpp create mode 100644 src/xrpld/app/tx/detail/Batch.cpp create mode 100644 src/xrpld/app/tx/detail/Batch.h diff --git a/include/xrpl/protocol/Batch.h b/include/xrpl/protocol/Batch.h new file mode 100644 index 0000000000..1388bbd2f1 --- /dev/null +++ b/include/xrpl/protocol/Batch.h @@ -0,0 +1,37 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +namespace ripple { + +inline void +serializeBatch( + Serializer& msg, + std::uint32_t const& flags, + std::vector const& txids) +{ + msg.add32(HashPrefix::batch); + msg.add32(flags); + msg.add32(std::uint32_t(txids.size())); + for (auto const& txid : txids) + msg.addBitString(txid); +} + +} // namespace ripple \ No newline at end of file diff --git a/include/xrpl/protocol/HashPrefix.h b/include/xrpl/protocol/HashPrefix.h index ab825658e8..7e486af4c0 100644 --- a/include/xrpl/protocol/HashPrefix.h +++ b/include/xrpl/protocol/HashPrefix.h @@ -88,6 +88,9 @@ enum class HashPrefix : std::uint32_t { /** Credentials signature */ credential = detail::make_hash_prefix('C', 'R', 'D'), + + /** Batch */ + batch = detail::make_hash_prefix('B', 'C', 'H'), }; template diff --git a/include/xrpl/protocol/Protocol.h b/include/xrpl/protocol/Protocol.h index 49bad8a076..898fd06fbd 100644 --- a/include/xrpl/protocol/Protocol.h +++ b/include/xrpl/protocol/Protocol.h @@ -169,6 +169,9 @@ std::size_t constexpr maxTrim = 25; */ std::size_t constexpr permissionMaxSize = 10; +/** The maximum number of transactions that can be in a batch. */ +std::size_t constexpr maxBatchTxCount = 8; + } // namespace ripple #endif diff --git a/include/xrpl/protocol/STTx.h b/include/xrpl/protocol/STTx.h index b00495bf76..f0d2157283 100644 --- a/include/xrpl/protocol/STTx.h +++ b/include/xrpl/protocol/STTx.h @@ -125,10 +125,16 @@ public: @return `true` if valid signature. If invalid, the error message string. */ enum class RequireFullyCanonicalSig : bool { no, yes }; + Expected checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; + Expected + checkBatchSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const; + // SQL Functions with metadata. static std::string const& getMetaSQLInsertReplaceHeader(); @@ -144,6 +150,9 @@ public: char status, std::string const& escapedMetaData) const; + std::vector + getBatchTransactionIDs() const; + private: Expected checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const; @@ -153,12 +162,24 @@ private: RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules) const; + Expected + checkBatchSingleSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig) const; + + Expected + checkBatchMultiSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const; + STBase* copy(std::size_t n, void* buf) const override; STBase* move(std::size_t n, void* buf) override; friend class detail::STVar; + mutable std::vector batch_txn_ids_; }; bool diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index b87bc3f8a4..4483d6251a 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -139,8 +139,8 @@ enum TEMcodes : TERUnderlyingType { temARRAY_EMPTY, temARRAY_TOO_LARGE, - temBAD_TRANSFER_FEE, + temINVALID_INNER_BATCH, }; //------------------------------------------------------------------------------ @@ -645,37 +645,37 @@ using TER = TERSubset; //------------------------------------------------------------------------------ inline bool -isTelLocal(TER x) +isTelLocal(TER x) noexcept { - return ((x) >= telLOCAL_ERROR && (x) < temMALFORMED); + return (x >= telLOCAL_ERROR && x < temMALFORMED); } inline bool -isTemMalformed(TER x) +isTemMalformed(TER x) noexcept { - return ((x) >= temMALFORMED && (x) < tefFAILURE); + return (x >= temMALFORMED && x < tefFAILURE); } inline bool -isTefFailure(TER x) +isTefFailure(TER x) noexcept { - return ((x) >= tefFAILURE && (x) < terRETRY); + return (x >= tefFAILURE && x < terRETRY); } inline bool -isTerRetry(TER x) +isTerRetry(TER x) noexcept { - return ((x) >= terRETRY && (x) < tesSUCCESS); + return (x >= terRETRY && x < tesSUCCESS); } inline bool -isTesSuccess(TER x) +isTesSuccess(TER x) noexcept { - return ((x) == tesSUCCESS); + return (x == tesSUCCESS); } inline bool -isTecClaim(TER x) +isTecClaim(TER x) noexcept { return ((x) >= tecCLAIM); } diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 505000cfd6..31c3ffa205 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -58,7 +58,8 @@ namespace ripple { // clang-format off // Universal Transaction flags: constexpr std::uint32_t tfFullyCanonicalSig = 0x80000000; -constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig; +constexpr std::uint32_t tfInnerBatchTxn = 0x40000000; +constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig | tfInnerBatchTxn; constexpr std::uint32_t tfUniversalMask = ~tfUniversal; // AccountSet flags: @@ -97,6 +98,7 @@ constexpr std::uint32_t tfPassive = 0x00010000; constexpr std::uint32_t tfImmediateOrCancel = 0x00020000; constexpr std::uint32_t tfFillOrKill = 0x00040000; constexpr std::uint32_t tfSell = 0x00080000; + constexpr std::uint32_t tfOfferCreateMask = ~(tfUniversal | tfPassive | tfImmediateOrCancel | tfFillOrKill | tfSell); @@ -239,6 +241,20 @@ constexpr std::uint32_t const tfVaultPrivate = 0x00010000; static_assert(tfVaultPrivate == lsfVaultPrivate); constexpr std::uint32_t const tfVaultShareNonTransferable = 0x00020000; constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate | tfVaultShareNonTransferable); + +// Batch Flags: +constexpr std::uint32_t tfAllOrNothing = 0x00010000; +constexpr std::uint32_t tfOnlyOne = 0x00020000; +constexpr std::uint32_t tfUntilFailure = 0x00040000; +constexpr std::uint32_t tfIndependent = 0x00080000; +/** + * @note If nested Batch transactions are supported in the future, the tfInnerBatchTxn flag + * will need to be removed from this mask to allow Batch transaction to be inside + * the sfRawTransactions array. + */ +constexpr std::uint32_t const tfBatchMask = + ~(tfUniversal | tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent) | tfInnerBatchTxn; + // clang-format on } // namespace ripple diff --git a/include/xrpl/protocol/TxMeta.h b/include/xrpl/protocol/TxMeta.h index 9422d697ca..02fde2ffe5 100644 --- a/include/xrpl/protocol/TxMeta.h +++ b/include/xrpl/protocol/TxMeta.h @@ -46,7 +46,10 @@ private: CtorHelper); public: - TxMeta(uint256 const& transactionID, std::uint32_t ledger); + TxMeta( + uint256 const& transactionID, + std::uint32_t ledger, + std::optional parentBatchId = std::nullopt); TxMeta(uint256 const& txID, std::uint32_t ledger, Blob const&); TxMeta(uint256 const& txID, std::uint32_t ledger, std::string const&); TxMeta(uint256 const& txID, std::uint32_t ledger, STObject const&); @@ -130,6 +133,27 @@ public: return static_cast(mDelivered); } + void + setParentBatchId(uint256 const& parentBatchId) + { + mParentBatchId = parentBatchId; + } + + uint256 + getParentBatchId() const + { + XRPL_ASSERT( + hasParentBatchId(), + "ripple::TxMeta::getParentBatchId : non-null batch id"); + return *mParentBatchId; + } + + bool + hasParentBatchId() const + { + return static_cast(mParentBatchId); + } + private: uint256 mTransactionID; std::uint32_t mLedger; @@ -137,6 +161,7 @@ private: int mResult; std::optional mDelivered; + std::optional mParentBatchId; STArray mNodes; }; diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index 3be0fd426c..e61d3a8005 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -32,6 +32,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo) XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 63bc52de6a..dbef597ea0 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -195,6 +195,7 @@ TYPED_SFIELD(sfHookNamespace, UINT256, 32) TYPED_SFIELD(sfHookSetTxnID, UINT256, 33) TYPED_SFIELD(sfDomainID, UINT256, 34) TYPED_SFIELD(sfVaultID, UINT256, 35) +TYPED_SFIELD(sfParentBatchID, UINT256, 36) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -357,6 +358,8 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30) UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31) UNTYPED_SFIELD(sfPriceData, OBJECT, 32) UNTYPED_SFIELD(sfCredential, OBJECT, 33) +UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) +UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -388,3 +391,5 @@ UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26) UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27) UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) UNTYPED_SFIELD(sfPermissions, ARRAY, 29) +UNTYPED_SFIELD(sfRawTransactions, ARRAY, 30) +UNTYPED_SFIELD(sfBatchSigners, ARRAY, 31, SField::sMD_Default, SField::notSigning) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 0f614df692..5d5faae505 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -514,6 +514,12 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::delegatable, ({ {sfAmount, soeOPTIONAL, soeMPTSupported}, })) +/** This transaction type batches together transactions. */ +TRANSACTION(ttBATCH, 71, Batch, Delegation::notDelegatable, ({ + {sfRawTransactions, soeREQUIRED}, + {sfBatchSigners, soeOPTIONAL}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html @@ -548,4 +554,3 @@ TRANSACTION(ttUNL_MODIFY, 102, UNLModify, Delegation::notDelegatable, ({ {sfLedgerSequence, soeREQUIRED}, {sfUNLModifyValidator, soeREQUIRED}, })) - diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index de3560d7f9..9dff4cc4f3 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -83,6 +83,8 @@ JSS(PriceDataSeries); // field. JSS(PriceData); // field. JSS(Provider); // field. JSS(QuoteAsset); // in: Oracle. +JSS(RawTransaction); // in: Batch +JSS(RawTransactions); // in: Batch JSS(SLE_hit_rate); // out: GetCounts. JSS(Scale); // field. JSS(SettleDelay); // in: TransactionSign diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index ecfca9743d..3f3b1e00c0 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -158,6 +158,13 @@ InnerObjectFormats::InnerObjectFormats() add(sfPermission.jsonName.c_str(), sfPermission.getCode(), {{sfPermissionValue, soeREQUIRED}}); + + add(sfBatchSigner.jsonName.c_str(), + sfBatchSigner.getCode(), + {{sfAccount, soeREQUIRED}, + {sfSigningPubKey, soeOPTIONAL}, + {sfTxnSignature, soeOPTIONAL}, + {sfSigners, soeOPTIONAL}}); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/STTx.cpp b/src/libxrpl/protocol/STTx.cpp index 7b6b4c1ee2..ee26dd69de 100644 --- a/src/libxrpl/protocol/STTx.cpp +++ b/src/libxrpl/protocol/STTx.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -262,6 +265,42 @@ STTx::checkSign( return Unexpected("Internal signature check failure."); } +Expected +STTx::checkBatchSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + try + { + XRPL_ASSERT( + getTxnType() == ttBATCH, + "STTx::checkBatchSign : not a batch transaction"); + if (getTxnType() != ttBATCH) + { + JLOG(debugLog().fatal()) << "not a batch transaction"; + return Unexpected("Not a batch transaction."); + } + STArray const& signers{getFieldArray(sfBatchSigners)}; + for (auto const& signer : signers) + { + Blob const& signingPubKey = signer.getFieldVL(sfSigningPubKey); + auto const result = signingPubKey.empty() + ? checkBatchMultiSign(signer, requireCanonicalSig, rules) + : checkBatchSingleSign(signer, requireCanonicalSig); + + if (!result) + return result; + } + return {}; + } + catch (std::exception const& e) + { + JLOG(debugLog().error()) + << "Batch signature check failed: " << e.what(); + } + return Unexpected("Internal batch signature check failure."); +} + Json::Value STTx::getJson(JsonOptions options) const { @@ -341,79 +380,90 @@ STTx::getMetaSQL( getFieldU32(sfSequence) % inLedger % status % rTxn % escapedMetaData); } -Expected -STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const +static Expected +singleSignHelper( + STObject const& signer, + Slice const& data, + bool const fullyCanonical) { // We don't allow both a non-empty sfSigningPubKey and an sfSigners. // That would allow the transaction to be signed two ways. So if both // fields are present the signature is invalid. - if (isFieldPresent(sfSigners)) + if (signer.isFieldPresent(sfSigners)) return Unexpected("Cannot both single- and multi-sign."); bool validSig = false; try { - bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || - (requireCanonicalSig == RequireFullyCanonicalSig::yes); - - auto const spk = getFieldVL(sfSigningPubKey); - + auto const spk = signer.getFieldVL(sfSigningPubKey); if (publicKeyType(makeSlice(spk))) { - Blob const signature = getFieldVL(sfTxnSignature); - Blob const data = getSigningData(*this); - + Blob const signature = signer.getFieldVL(sfTxnSignature); validSig = verify( PublicKey(makeSlice(spk)), - makeSlice(data), + data, makeSlice(signature), fullyCanonical); } } catch (std::exception const&) { - // Assume it was a signature failure. validSig = false; } - if (validSig == false) + + if (!validSig) return Unexpected("Invalid signature."); - // Signature was verified. + return {}; } Expected -STTx::checkMultiSign( - RequireFullyCanonicalSig requireCanonicalSig, - Rules const& rules) const +STTx::checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const +{ + auto const data = getSigningData(*this); + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes); + return singleSignHelper(*this, makeSlice(data), fullyCanonical); +} + +Expected +STTx::checkBatchSingleSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig) const +{ + Serializer msg; + serializeBatch(msg, getFlags(), getBatchTransactionIDs()); + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == STTx::RequireFullyCanonicalSig::yes); + return singleSignHelper(batchSigner, msg.slice(), fullyCanonical); +} + +Expected +multiSignHelper( + STObject const& signerObj, + bool const fullyCanonical, + std::function makeMsg, + Rules const& rules) { // Make sure the MultiSigners are present. Otherwise they are not // attempting multi-signing and we just have a bad SigningPubKey. - if (!isFieldPresent(sfSigners)) + if (!signerObj.isFieldPresent(sfSigners)) return Unexpected("Empty SigningPubKey."); // We don't allow both an sfSigners and an sfTxnSignature. Both fields // being present would indicate that the transaction is signed both ways. - if (isFieldPresent(sfTxnSignature)) + if (signerObj.isFieldPresent(sfTxnSignature)) return Unexpected("Cannot both single- and multi-sign."); - STArray const& signers{getFieldArray(sfSigners)}; + STArray const& signers{signerObj.getFieldArray(sfSigners)}; // There are well known bounds that the number of signers must be within. - if (signers.size() < minMultiSigners || - signers.size() > maxMultiSigners(&rules)) + if (signers.size() < STTx::minMultiSigners || + signers.size() > STTx::maxMultiSigners(&rules)) return Unexpected("Invalid Signers array size."); - // We can ease the computational load inside the loop a bit by - // pre-constructing part of the data that we hash. Fill a Serializer - // with the stuff that stays constant from signature to signature. - Serializer const dataStart{startMultiSigningData(*this)}; - // We also use the sfAccount field inside the loop. Get it once. - auto const txnAccountID = getAccountID(sfAccount); - - // Determine whether signatures must be full canonical. - bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || - (requireCanonicalSig == RequireFullyCanonicalSig::yes); + auto const txnAccountID = signerObj.getAccountID(sfAccount); // Signers must be in sorted order by AccountID. AccountID lastAccountID(beast::zero); @@ -441,18 +491,13 @@ STTx::checkMultiSign( bool validSig = false; try { - Serializer s = dataStart; - finishMultiSigningData(accountID, s); - auto spk = signer.getFieldVL(sfSigningPubKey); - if (publicKeyType(makeSlice(spk))) { Blob const signature = signer.getFieldVL(sfTxnSignature); - validSig = verify( PublicKey(makeSlice(spk)), - s.slice(), + makeMsg(accountID).slice(), makeSlice(signature), fullyCanonical); } @@ -471,6 +516,90 @@ STTx::checkMultiSign( return {}; } +Expected +STTx::checkBatchMultiSign( + STObject const& batchSigner, + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == RequireFullyCanonicalSig::yes); + + // We can ease the computational load inside the loop a bit by + // pre-constructing part of the data that we hash. Fill a Serializer + // with the stuff that stays constant from signature to signature. + Serializer dataStart; + serializeBatch(dataStart, getFlags(), getBatchTransactionIDs()); + return multiSignHelper( + batchSigner, + fullyCanonical, + [&dataStart](AccountID const& accountID) mutable -> Serializer { + Serializer s = dataStart; + finishMultiSigningData(accountID, s); + return s; + }, + rules); +} + +Expected +STTx::checkMultiSign( + RequireFullyCanonicalSig requireCanonicalSig, + Rules const& rules) const +{ + bool const fullyCanonical = (getFlags() & tfFullyCanonicalSig) || + (requireCanonicalSig == RequireFullyCanonicalSig::yes); + + // We can ease the computational load inside the loop a bit by + // pre-constructing part of the data that we hash. Fill a Serializer + // with the stuff that stays constant from signature to signature. + Serializer dataStart = startMultiSigningData(*this); + return multiSignHelper( + *this, + fullyCanonical, + [&dataStart](AccountID const& accountID) mutable -> Serializer { + Serializer s = dataStart; + finishMultiSigningData(accountID, s); + return s; + }, + rules); +} + +/** + * @brief Retrieves a batch of transaction IDs from the STTx. + * + * This function returns a vector of transaction IDs by extracting them from + * the field array `sfRawTransactions` within the STTx. If the batch + * transaction IDs have already been computed and cached in `batch_txn_ids_`, + * it returns the cached vector. Otherwise, it computes the transaction IDs, + * caches them, and then returns the vector. + * + * @return A vector of `uint256` containing the batch transaction IDs. + * + * @note The function asserts that the `sfRawTransactions` field array is not + * empty and that the size of the computed batch transaction IDs matches the + * size of the `sfRawTransactions` field array. + */ +std::vector +STTx::getBatchTransactionIDs() const +{ + XRPL_ASSERT( + getTxnType() == ttBATCH, + "STTx::getBatchTransactionIDs : not a batch transaction"); + XRPL_ASSERT( + getFieldArray(sfRawTransactions).size() != 0, + "STTx::getBatchTransactionIDs : empty raw transactions"); + if (batch_txn_ids_.size() != 0) + return batch_txn_ids_; + + for (STObject const& rb : getFieldArray(sfRawTransactions)) + batch_txn_ids_.push_back(rb.getHash(HashPrefix::transactionID)); + + XRPL_ASSERT( + batch_txn_ids_.size() == getFieldArray(sfRawTransactions).size(), + "STTx::getBatchTransactionIDs : batch transaction IDs size mismatch"); + return batch_txn_ids_; +} + //------------------------------------------------------------------------------ static bool @@ -606,6 +735,42 @@ invalidMPTAmountInTx(STObject const& tx) return false; } +static bool +isRawTransactionOkay(STObject const& st, std::string& reason) +{ + if (!st.isFieldPresent(sfRawTransactions)) + return true; + + if (st.isFieldPresent(sfBatchSigners) && + st.getFieldArray(sfBatchSigners).size() > maxBatchTxCount) + { + reason = "Batch Signers array exceeds max entries."; + return false; + } + + auto const& rawTxns = st.getFieldArray(sfRawTransactions); + if (rawTxns.size() > maxBatchTxCount) + { + reason = "Raw Transactions array exceeds max entries."; + return false; + } + for (STObject raw : rawTxns) + { + try + { + TxType const tt = + safe_cast(raw.getFieldU16(sfTransactionType)); + raw.applyTemplate(getTxFormat(tt)->getSOTemplate()); + } + catch (std::exception const& e) + { + reason = e.what(); + return false; + } + } + return true; +} + bool passesLocalChecks(STObject const& st, std::string& reason) { @@ -630,6 +795,9 @@ passesLocalChecks(STObject const& st, std::string& reason) return false; } + if (!isRawTransactionOkay(st, reason)) + return false; + return true; } @@ -645,10 +813,13 @@ sterilize(STTx const& stx) bool isPseudoTx(STObject const& tx) { - auto t = tx[~sfTransactionType]; + auto const t = tx[~sfTransactionType]; + if (!t) return false; - auto tt = safe_cast(*t); + + auto const tt = safe_cast(*t); + return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY; } diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index 943a0e601b..68125fab83 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -217,6 +217,7 @@ transResults() MAKE_ERROR(temARRAY_EMPTY, "Malformed: Array is empty."), MAKE_ERROR(temARRAY_TOO_LARGE, "Malformed: Array is too large."), MAKE_ERROR(temBAD_TRANSFER_FEE, "Malformed: Transfer fee is outside valid range."), + MAKE_ERROR(temINVALID_INNER_BATCH, "Malformed: Invalid inner batch transaction."), MAKE_ERROR(terRETRY, "Retry transaction."), MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."), diff --git a/src/libxrpl/protocol/TxMeta.cpp b/src/libxrpl/protocol/TxMeta.cpp index d9a9f0db87..2083fc8eaf 100644 --- a/src/libxrpl/protocol/TxMeta.cpp +++ b/src/libxrpl/protocol/TxMeta.cpp @@ -56,6 +56,9 @@ TxMeta::TxMeta( if (obj.isFieldPresent(sfDeliveredAmount)) setDeliveredAmount(obj.getFieldAmount(sfDeliveredAmount)); + + if (obj.isFieldPresent(sfParentBatchID)) + setParentBatchId(obj.getFieldH256(sfParentBatchID)); } TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj) @@ -76,6 +79,9 @@ TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj) if (obj.isFieldPresent(sfDeliveredAmount)) setDeliveredAmount(obj.getFieldAmount(sfDeliveredAmount)); + + if (obj.isFieldPresent(sfParentBatchID)) + setParentBatchId(obj.getFieldH256(sfParentBatchID)); } TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, Blob const& vec) @@ -91,11 +97,15 @@ TxMeta::TxMeta( { } -TxMeta::TxMeta(uint256 const& transactionID, std::uint32_t ledger) +TxMeta::TxMeta( + uint256 const& transactionID, + std::uint32_t ledger, + std::optional parentBatchId) : mTransactionID(transactionID) , mLedger(ledger) , mIndex(static_cast(-1)) , mResult(255) + , mParentBatchId(parentBatchId) , mNodes(sfAffectedNodes) { mNodes.reserve(32); @@ -231,6 +241,10 @@ TxMeta::getAsObject() const metaData.emplace_back(mNodes); if (hasDeliveredAmount()) metaData.setFieldAmount(sfDeliveredAmount, getDeliveredAmount()); + + if (hasParentBatchId()) + metaData.setFieldH256(sfParentBatchID, getParentBatchId()); + return metaData; } diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index 4ae18d9d28..03283e4611 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -28,12 +28,6 @@ namespace test { class AccountDelete_test : public beast::unit_test::suite { private: - std::uint32_t - openLedgerSeq(jtx::Env& env) - { - return env.current()->seq(); - } - // Helper function that verifies the expected DeliveredAmount is present. // // NOTE: the function _infers_ the transaction to operate on by calling @@ -83,26 +77,6 @@ private: return jv; }; - // Close the ledger until the ledger sequence is large enough to close - // the account. If margin is specified, close the ledger so `margin` - // more closes are needed - void - incLgrSeqForAccDel( - jtx::Env& env, - jtx::Account const& acc, - std::uint32_t margin = 0) - { - int const delta = [&]() -> int { - if (env.seq(acc) + 255 > openLedgerSeq(env)) - return env.seq(acc) - openLedgerSeq(env) + 255 - margin; - return 0; - }(); - BEAST_EXPECT(margin == 0 || delta >= 0); - for (int i = 0; i < delta; ++i) - env.close(); - BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255 - margin); - } - public: void testBasics() @@ -368,7 +342,6 @@ public: NetClock::time_point const& cancelAfter) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -398,7 +371,6 @@ public: [](Account const& account, Account const& from, std::uint32_t seq) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCancel; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); jv[sfOwner.jsonName] = from.human(); jv[sfOfferSequence.jsonName] = seq; @@ -536,7 +508,6 @@ public: auto payChanClaim = [&]() { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelClaim; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = alice.human(); jv[sfChannel.jsonName] = to_string(payChanKey.key); jv[sfBalance.jsonName] = diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp new file mode 100644 index 0000000000..6874a42c9e --- /dev/null +++ b/src/test/app/Batch_test.cpp @@ -0,0 +1,3860 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace ripple { +namespace test { + +class Batch_test : public beast::unit_test::suite +{ + struct TestLedgerData + { + int index; + std::string txType; + std::string result; + std::string txHash; + std::optional batchID; + }; + + struct TestBatchData + { + std::string result; + std::string txHash; + }; + + Json::Value + getTxByIndex(Json::Value const& jrr, int const index) + { + for (auto const& txn : jrr[jss::result][jss::ledger][jss::transactions]) + { + if (txn[jss::metaData][sfTransactionIndex.jsonName] == index) + return txn; + } + return {}; + } + + Json::Value + getLastLedger(jtx::Env& env) + { + Json::Value params; + params[jss::ledger_index] = env.closed()->seq(); + params[jss::transactions] = true; + params[jss::expand] = true; + return env.rpc("json", "ledger", to_string(params)); + } + + void + validateInnerTxn( + jtx::Env& env, + std::string const& batchID, + TestLedgerData const& ledgerResult) + { + Json::Value const jrr = env.rpc("tx", ledgerResult.txHash)[jss::result]; + BEAST_EXPECT(jrr[sfTransactionType.jsonName] == ledgerResult.txType); + BEAST_EXPECT( + jrr[jss::meta][sfTransactionResult.jsonName] == + ledgerResult.result); + BEAST_EXPECT(jrr[jss::meta][sfParentBatchID.jsonName] == batchID); + } + + void + validateClosedLedger( + jtx::Env& env, + std::vector const& ledgerResults) + { + auto const jrr = getLastLedger(env); + auto const transactions = + jrr[jss::result][jss::ledger][jss::transactions]; + BEAST_EXPECT(transactions.size() == ledgerResults.size()); + for (TestLedgerData const& ledgerResult : ledgerResults) + { + auto const txn = getTxByIndex(jrr, ledgerResult.index); + BEAST_EXPECT(txn[jss::hash].asString() == ledgerResult.txHash); + BEAST_EXPECT(txn.isMember(jss::metaData)); + Json::Value const meta = txn[jss::metaData]; + BEAST_EXPECT( + txn[sfTransactionType.jsonName] == ledgerResult.txType); + BEAST_EXPECT( + meta[sfTransactionResult.jsonName] == ledgerResult.result); + if (ledgerResult.batchID) + validateInnerTxn(env, *ledgerResult.batchID, ledgerResult); + } + } + + template + std::pair, std::string> + submitBatch(jtx::Env& env, TER const& result, Args&&... args) + { + auto batchTxn = env.jt(std::forward(args)...); + env(batchTxn, jtx::ter(result)); + + auto const ids = batchTxn.stx->getBatchTransactionIDs(); + std::vector txIDs; + for (auto const& id : ids) + txIDs.push_back(strHex(id)); + TxID const batchID = batchTxn.stx->getTransactionID(); + return std::make_pair(txIDs, strHex(batchID)); + } + + static uint256 + getCheckIndex(AccountID const& account, std::uint32_t uSequence) + { + return keylet::check(account, uSequence).key; + } + + static std::unique_ptr + makeSmallQueueConfig( + std::map extraTxQ = {}, + std::map extraVoting = {}) + { + auto p = test::jtx::envconfig(); + auto& section = p->section("transaction_queue"); + section.set("ledgers_in_queue", "2"); + section.set("minimum_queue_size", "2"); + section.set("min_ledgers_to_compute_size_limit", "3"); + section.set("max_ledger_counts_to_store", "100"); + section.set("retry_sequence_percent", "25"); + section.set("normal_consensus_increase_percent", "0"); + + for (auto const& [k, v] : extraTxQ) + section.set(k, v); + + return p; + } + + auto + openLedgerFee(jtx::Env& env, XRPAmount const& batchFee) + { + using namespace jtx; + + auto const& view = *env.current(); + auto metrics = env.app().getTxQ().getMetrics(view); + return toDrops(metrics.openLedgerFeeLevel, batchFee) + 1; + } + + void + testEnable(FeatureBitset features) + { + testcase("enabled"); + + using namespace test::jtx; + using namespace std::literals; + + for (bool const withBatch : {true, false}) + { + auto const amend = withBatch ? features : features - featureBatch; + test::jtx::Env env{*this, envconfig(), amend}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + // ttBatch + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const txResult = + withBatch ? ter(tesSUCCESS) : ter(temDISABLED); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + txResult); + env.close(); + } + + // tfInnerBatchTxn + // If the feature is disabled, the transaction fails with + // temINVALID_FLAG If the feature is enabled, the transaction fails + // early in checkValidity() + { + auto const txResult = + withBatch ? ter(telENV_RPC_FAILED) : ter(temINVALID_FLAG); + env(pay(alice, bob, XRP(1)), + txflags(tfInnerBatchTxn), + txResult); + env.close(); + } + + env.close(); + } + } + + void + testPreflight(FeatureBitset features) + { + testcase("preflight"); + + using namespace test::jtx; + using namespace std::literals; + + //---------------------------------------------------------------------- + // preflight + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + // temBAD_FEE: preflight1 + { + env(batch::outer(alice, env.seq(alice), XRP(-1), tfAllOrNothing), + ter(temBAD_FEE)); + env.close(); + } + + // DEFENSIVE: temINVALID_FLAG: Batch: inner batch flag. + // ACTUAL: telENV_RPC_FAILED: checkValidity() + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfInnerBatchTxn), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temINVALID_FLAG: Batch: invalid flags. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfDisallowXRP), + ter(temINVALID_FLAG)); + env.close(); + } + + // temINVALID_FLAG: Batch: too many flags. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + txflags(tfAllOrNothing | tfOnlyOne), + ter(temINVALID_FLAG)); + env.close(); + } + + // temARRAY_EMPTY: Batch: txns array must have at least 2 entries. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + ter(temARRAY_EMPTY)); + env.close(); + } + + // temARRAY_EMPTY: Batch: txns array must have at least 2 entries. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 0); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + ter(temARRAY_EMPTY)); + env.close(); + } + + // DEFENSIVE: temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries. + // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay() + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 9); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + batch::inner(pay(alice, bob, XRP(1)), seq + 3), + batch::inner(pay(alice, bob, XRP(1)), seq + 4), + batch::inner(pay(alice, bob, XRP(1)), seq + 5), + batch::inner(pay(alice, bob, XRP(1)), seq + 6), + batch::inner(pay(alice, bob, XRP(1)), seq + 7), + batch::inner(pay(alice, bob, XRP(1)), seq + 8), + batch::inner(pay(alice, bob, XRP(1)), seq + 9), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate Txn found. + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(10)), seq + 1)); + + env(jt.jv, batch::sig(bob), ter(temREDUNDANT)); + env.close(); + } + + // temINVALID: Batch: batch cannot have inner batch txn. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner( + batch::outer(alice, seq, batchFee, tfAllOrNothing), seq), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + ter(temINVALID)); + env.close(); + } + + // temINVALID_FLAG: Batch: inner txn must have the + // tfInnerBatchTxn flag. + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1[jss::Flags] = 0; + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(temINVALID_FLAG)); + env.close(); + } + + // temBAD_SIGNATURE: Batch: inner txn cannot include TxnSignature. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto jt = env.jt(pay(alice, bob, XRP(1))); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(jt.jv, seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + ter(temBAD_SIGNATURE)); + env.close(); + } + + // temBAD_SIGNER: Batch: inner txn cannot include Signers. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = pay(alice, bob, XRP(1)); + tx1[sfSigners.jsonName] = Json::arrayValue; + tx1[sfSigners.jsonName][0U][sfSigner.jsonName] = Json::objectValue; + tx1[sfSigners.jsonName][0U][sfSigner.jsonName][sfAccount.jsonName] = + alice.human(); + tx1[sfSigners.jsonName][0U][sfSigner.jsonName] + [sfSigningPubKey.jsonName] = strHex(alice.pk()); + tx1[sfSigners.jsonName][0U][sfSigner.jsonName] + [sfTxnSignature.jsonName] = "DEADBEEF"; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(tx1, seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + ter(temBAD_SIGNER)); + env.close(); + } + + // temBAD_REGKEY: Batch: inner txn must include empty + // SigningPubKey. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1); + tx1[jss::SigningPubKey] = strHex(alice.pk()); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(1)), seq + 2)); + + env(jt.jv, ter(temBAD_REGKEY)); + env.close(); + } + + // temINVALID_INNER_BATCH: Batch: inner txn preflight failed. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // amount can't be negative + batch::inner(pay(alice, bob, XRP(-1)), seq + 2), + ter(temINVALID_INNER_BATCH)); + env.close(); + } + + // temBAD_FEE: Batch: inner txn must have a fee of 0. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1); + tx1[jss::Fee] = to_string(env.current()->fees().base); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + ter(temBAD_FEE)); + env.close(); + } + + // temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence + // and TicketSequence. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(pay(alice, bob, XRP(1)), 0, 1); + tx1[jss::Sequence] = seq + 1; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + ter(temSEQ_AND_TICKET)); + env.close(); + } + + // temSEQ_AND_TICKET: Batch: inner txn must have either Sequence or + // TicketSequence. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + ter(temSEQ_AND_TICKET)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate sequence found: + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 1), + ter(temREDUNDANT)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate ticket found: + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, seq + 1), + batch::inner(pay(alice, bob, XRP(2)), 0, seq + 1), + ter(temREDUNDANT)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate ticket & sequence found: + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 1), + ter(temREDUNDANT)); + env.close(); + } + + // DEFENSIVE: temARRAY_TOO_LARGE: Batch: signers array exceeds 8 + // entries. + // ACTUAL: telENV_RPC_FAILED: isRawTransactionOkay() + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 9, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(5)), seq + 2), + batch::sig( + bob, + carol, + alice, + bob, + carol, + alice, + bob, + carol, + alice, + alice), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temBAD_SIGNER: Batch: signer cannot be the outer account + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::sig(alice, bob), + ter(temBAD_SIGNER)); + env.close(); + } + + // temREDUNDANT: Batch: duplicate signer found + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::sig(bob, bob), + ter(temREDUNDANT)); + env.close(); + } + + // temBAD_SIGNER: Batch: no account signature for inner txn. + // Note: Extra signature by bob + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(5)), seq + 2), + batch::sig(bob), + ter(temBAD_SIGNER)); + env.close(); + } + + // temBAD_SIGNER: Batch: no account signature for inner txn. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::sig(carol), + ter(temBAD_SIGNER)); + env.close(); + } + + // temBAD_SIGNATURE: Batch: invalid batch txn signature. + { + auto const seq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto jt = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq)); + + Serializer msg; + serializeBatch( + msg, tfAllOrNothing, jt.stx->getBatchTransactionIDs()); + auto const sig = ripple::sign(bob.pk(), bob.sk(), msg.slice()); + jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName] + [sfAccount.jsonName] = bob.human(); + jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName] + [sfSigningPubKey.jsonName] = strHex(alice.pk()); + jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName] + [sfTxnSignature.jsonName] = + strHex(Slice{sig.data(), sig.size()}); + + env(jt.jv, ter(temBAD_SIGNATURE)); + env.close(); + } + + // temBAD_SIGNER: Batch: invalid batch signers. + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::inner(pay(carol, alice, XRP(5)), env.seq(carol)), + batch::sig(bob), + ter(temBAD_SIGNER)); + env.close(); + } + } + + void + testPreclaim(FeatureBitset features) + { + testcase("preclaim"); + + using namespace test::jtx; + using namespace std::literals; + + //---------------------------------------------------------------------- + // preclaim + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const elsa = Account("elsa"); + auto const frank = Account("frank"); + auto const phantom = Account("phantom"); + env.memoize(phantom); + + env.fund(XRP(10000), alice, bob, carol, dave, elsa, frank); + env.close(); + + //---------------------------------------------------------------------- + // checkSign.checkSingleSign + + // tefBAD_AUTH: Bob is not authorized to sign for Alice + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(20)), seq + 2), + sig(bob), + ter(tefBAD_AUTH)); + env.close(); + } + + //---------------------------------------------------------------------- + // checkBatchSign.checkMultiSign + + // tefNOT_MULTI_SIGNING: SignersList not enabled + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {dave, carol}), + ter(tefNOT_MULTI_SIGNING)); + env.close(); + } + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + env(signers(bob, 2, {{carol, 1}, {dave, 1}, {elsa, 1}})); + env.close(); + + // tefBAD_SIGNATURE: Account not in SignersList + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, frank}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefBAD_SIGNATURE: Wrong publicKey type + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, Account("dave", KeyType::ed25519)}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefMASTER_DISABLED: Master key disabled + { + env(regkey(elsa, frank)); + env(fset(elsa, asfDisableMaster), sig(elsa)); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, elsa}), + ter(tefMASTER_DISABLED)); + env.close(); + } + + // tefBAD_SIGNATURE: Signer does not exist + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, phantom}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefBAD_SIGNATURE: Signer has not enabled RegularKey + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + Account const davo{"davo", KeyType::ed25519}; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, Reg{dave, davo}}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefBAD_SIGNATURE: Wrong RegularKey Set + { + env(regkey(dave, frank)); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + Account const davo{"davo", KeyType::ed25519}; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, Reg{dave, davo}}), + ter(tefBAD_SIGNATURE)); + env.close(); + } + + // tefBAD_QUORUM + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 2, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol}), + ter(tefBAD_QUORUM)); + env.close(); + } + + // tesSUCCESS: BatchSigners.Signers + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 3, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, dave}), + ter(tesSUCCESS)); + env.close(); + } + + // tesSUCCESS: Multisign + BatchSigners.Signers + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 4, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(bob, alice, XRP(5)), env.seq(bob)), + batch::msig(bob, {carol, dave}), + msig(bob, carol), + ter(tesSUCCESS)); + env.close(); + } + + //---------------------------------------------------------------------- + // checkBatchSign.checkSingleSign + + // tefBAD_AUTH: Inner Account is not signer + { + auto const ledSeq = env.current()->seq(); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, phantom, XRP(1000)), seq + 1), + batch::inner(noop(phantom), ledSeq), + batch::sig(Reg{phantom, carol}), + ter(tefBAD_AUTH)); + env.close(); + } + + // tefBAD_AUTH: Account is not signer + { + auto const ledSeq = env.current()->seq(); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1000)), seq + 1), + batch::inner(noop(bob), ledSeq), + batch::sig(Reg{bob, carol}), + ter(tefBAD_AUTH)); + env.close(); + } + + // tesSUCCESS: Signed With Regular Key + { + env(regkey(bob, carol)); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(bob, alice, XRP(2)), env.seq(bob)), + batch::sig(Reg{bob, carol}), + ter(tesSUCCESS)); + env.close(); + } + + // tesSUCCESS: Signed With Master Key + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(bob, alice, XRP(2)), env.seq(bob)), + batch::sig(bob), + ter(tesSUCCESS)); + env.close(); + } + + // tefMASTER_DISABLED: Signed With Master Key Disabled + { + env(regkey(bob, carol)); + env(fset(bob, asfDisableMaster), sig(bob)); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(bob, alice, XRP(2)), env.seq(bob)), + batch::sig(bob), + ter(tefMASTER_DISABLED)); + env.close(); + } + } + + void + testBadRawTxn(FeatureBitset features) + { + testcase("bad raw txn"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + + // Invalid: sfTransactionType + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::TransactionType); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + + // Invalid: sfAccount + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::Account); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + + // Invalid: sfSequence + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::Sequence); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + + // Invalid: sfFee + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::Fee); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + + // Invalid: sfSigningPubKey + { + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const seq = env.seq(alice); + auto tx1 = batch::inner(pay(alice, bob, XRP(10)), seq + 1); + tx1.removeMember(jss::SigningPubKey); + auto jt = env.jtnofill( + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(10)), seq + 2)); + + env(jt.jv, batch::sig(bob), ter(telENV_RPC_FAILED)); + env.close(); + } + } + + void + testBadSequence(FeatureBitset features) + { + testcase("bad sequence"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + // Invalid: Alice Sequence is a past sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq - 10), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Alice Sequence is a future sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq + 10), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Bob Sequence is a past sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq - 10), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Bob Sequence is a future sequence + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq + 10), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + + // Invalid: Outer and Inner Sequence are the same + { + auto const preAliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobSeq = env.seq(bob); + auto const preBob = env.balance(bob); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, preAliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), preAliceSeq), + batch::inner(pay(bob, alice, XRP(5)), preBobSeq), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + + // Alice pays fee & Bob should not be affected. + BEAST_EXPECT(env.seq(alice) == preAliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.seq(bob) == preBobSeq); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + } + + void + testBadOuterFee(FeatureBitset features) + { + testcase("bad outer fee"); + + using namespace test::jtx; + using namespace std::literals; + + // Bad Fee Without Signer + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 0, 2) + auto const batchFee = batch::calcBatchFee(env, 0, 1); + auto const aliceSeq = env.seq(alice); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(15)), aliceSeq + 2), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee With MultiSign + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 2, 2) + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const aliceSeq = env.seq(alice); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(15)), aliceSeq + 2), + msig(bob, carol), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee With MultiSign + BatchSigners + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 3, 2) + auto const batchFee = batch::calcBatchFee(env, 2, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + msig(bob, carol), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee With MultiSign + BatchSigners.Signers + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + env(signers(alice, 2, {{bob, 1}, {carol, 1}})); + env.close(); + + env(signers(bob, 2, {{alice, 1}, {carol, 1}})); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 4, 2) + auto const batchFee = batch::calcBatchFee(env, 3, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::msig(bob, {alice, carol}), + msig(bob, carol), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee With BatchSigners + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + // Bad Fee: Should be batch::calcBatchFee(env, 1, 2) + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + ter(telINSUF_FEE_P)); + env.close(); + } + + // Bad Fee Dynamic Fee Calculation + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + auto const ammCreate = + [&alice](STAmount const& amount, STAmount const& amount2) { + Json::Value jv; + jv[jss::Account] = alice.human(); + jv[jss::Amount] = amount.getJson(JsonOptions::none); + jv[jss::Amount2] = amount2.getJson(JsonOptions::none); + jv[jss::TradingFee] = 0; + jv[jss::TransactionType] = jss::AMMCreate; + return jv; + }; + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(ammCreate(XRP(10), USD(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(10)), seq + 2), + ter(telINSUF_FEE_P)); + env.close(); + } + } + + void + testCalculateBaseFee(FeatureBitset features) + { + testcase("calculate base fee"); + + using namespace test::jtx; + using namespace std::literals; + + // telENV_RPC_FAILED: Batch: txns array exceeds 8 entries. + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 9); + auto const aliceSeq = env.seq(alice); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temARRAY_TOO_LARGE: Batch: txns array exceeds 8 entries. + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 9); + auto const aliceSeq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq)); + + env.app().openLedger().modify( + [&](OpenView& view, beast::Journal j) { + auto const result = + ripple::apply(env.app(), view, *jt.stx, tapNONE, j); + BEAST_EXPECT( + !result.applied && result.ter == temARRAY_TOO_LARGE); + return result.applied; + }); + } + + // telENV_RPC_FAILED: Batch: signers array exceeds 8 entries. + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 9, 2); + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(5)), aliceSeq + 2), + batch::sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob), + ter(telENV_RPC_FAILED)); + env.close(); + } + + // temARRAY_TOO_LARGE: Batch: signers array exceeds 8 entries. + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 9); + auto const aliceSeq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(5)), aliceSeq + 2), + batch::sig(bob, bob, bob, bob, bob, bob, bob, bob, bob, bob)); + + env.app().openLedger().modify( + [&](OpenView& view, beast::Journal j) { + auto const result = + ripple::apply(env.app(), view, *jt.stx, tapNONE, j); + BEAST_EXPECT( + !result.applied && result.ter == temARRAY_TOO_LARGE); + return result.applied; + }); + } + } + + void + testAllOrNothing(FeatureBitset features) + { + testcase("all or nothing"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + // all + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // tec failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequence + BEAST_EXPECT(env.seq(alice) == seq + 1); + + // Alice pays Fee; Bob should not be affected + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // tef failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tefNO_AUTH_REQUIRED: trustline auth is not required + batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequence + BEAST_EXPECT(env.seq(alice) == seq + 1); + + // Alice pays Fee; Bob should not be affected + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // ter failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // terPRE_TICKET: ticket does not exist + batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequence + BEAST_EXPECT(env.seq(alice) == seq + 1); + + // Alice pays Fee; Bob should not be affected + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + } + + void + testOnlyOne(FeatureBitset features) + { + testcase("only one"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, dave, gw); + env.close(); + + // all transactions fail + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 1), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 2), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tecUNFUNDED_PAYMENT", txIDs[0], batchID}, + {2, "Payment", "tecUNFUNDED_PAYMENT", txIDs[1], batchID}, + {3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // first transaction fails + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tecUNFUNDED_PAYMENT", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // tec failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 2), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // tef failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + // tefNO_AUTH_REQUIRED: trustline auth is not required + batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1)); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // ter failure + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + // terPRE_TICKET: ticket does not exist + batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(1)); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + // tec (tecKILLED) error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 6); + + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfOnlyOne), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 1), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 2), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 3), + batch::inner(pay(alice, bob, XRP(100)), seq + 4), + batch::inner(pay(alice, carol, XRP(100)), seq + 5), + batch::inner(pay(alice, dave, XRP(100)), seq + 6)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "OfferCreate", "tecKILLED", txIDs[0], batchID}, + {2, "OfferCreate", "tecKILLED", txIDs[1], batchID}, + {3, "OfferCreate", "tecKILLED", txIDs[2], batchID}, + {4, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(100) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol); + } + } + + void + testUntilFailure(FeatureBitset features) + { + testcase("until failure"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const dave = Account("dave"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, dave, gw); + env.close(); + + // first transaction fails + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 1), + batch::inner(pay(alice, bob, XRP(1)), seq + 2), + batch::inner(pay(alice, bob, XRP(2)), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tecUNFUNDED_PAYMENT", txIDs[0], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // all transactions succeed + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + batch::inner(pay(alice, bob, XRP(3)), seq + 3), + batch::inner(pay(alice, bob, XRP(4)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[2], batchID}, + {4, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 5); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(10) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(10)); + } + + // tec error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // tef error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // tefNO_AUTH_REQUIRED: trustline auth is not required + batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // ter error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // terPRE_TICKET: ticket does not exist + batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // tec (tecKILLED) error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfUntilFailure), + batch::inner(pay(alice, bob, XRP(100)), seq + 1), + batch::inner(pay(alice, carol, XRP(100)), seq + 2), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 3), + batch::inner(pay(alice, dave, XRP(100)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "OfferCreate", "tecKILLED", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100)); + } + } + + void + testIndependent(FeatureBitset features) + { + testcase("independent"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + + // multiple transactions fail + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 2), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tecUNFUNDED_PAYMENT", txIDs[1], batchID}, + {3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID}, + {4, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 5); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(4) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(4)); + } + + // tec error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // tecUNFUNDED_PAYMENT: alice does not have enough XRP + batch::inner(pay(alice, bob, XRP(9999)), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 4)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tecUNFUNDED_PAYMENT", txIDs[2], batchID}, + {4, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 5); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(6) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(6)); + } + + // tef error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // tefNO_AUTH_REQUIRED: trustline auth is not required + batch::inner(trust(alice, USD(1000), tfSetfAuth), seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6)); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(6)); + } + + // ter error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 4); + auto const seq = env.seq(alice); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + // terPRE_TICKET: ticket does not exist + batch::inner(trust(alice, USD(1000), tfSetfAuth), 0, seq + 3), + batch::inner(pay(alice, bob, XRP(3)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[3], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 4); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee - XRP(6)); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(6)); + } + + // tec (tecKILLED) error + { + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 3); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(100)), seq + 1), + batch::inner(pay(alice, carol, XRP(100)), seq + 2), + batch::inner( + offer( + alice, + alice["USD"](100), + XRP(100), + tfImmediateOrCancel), + seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "OfferCreate", "tecKILLED", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(200) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(100)); + BEAST_EXPECT(env.balance(carol) == preCarol + XRP(100)); + } + } + + void + testInnerSubmitRPC(FeatureBitset features) + { + testcase("inner submit rpc"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + auto submitAndValidate = [&](Slice const& slice) { + auto const jrr = env.rpc("submit", strHex(slice))[jss::result]; + BEAST_EXPECT( + jrr[jss::status] == "error" && + jrr[jss::error] == "invalidTransaction" && + jrr[jss::error_exception] == + "fails local checks: Malformed: Invalid inner batch " + "transaction."); + env.close(); + }; + + // Invalid RPC Submission: TxnSignature + // - has `TxnSignature` field + // - has no `SigningPubKey` field + // - has no `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice)); + txn[sfTxnSignature] = "DEADBEEF"; + STParsedJSONObject parsed("test", txn.getTxn()); + Serializer s; + parsed.object->add(s); + submitAndValidate(s.slice()); + } + + // Invalid RPC Submission: SigningPubKey + // - has no `TxnSignature` field + // - has `SigningPubKey` field + // - has no `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice)); + txn[sfSigningPubKey] = strHex(alice.pk()); + STParsedJSONObject parsed("test", txn.getTxn()); + Serializer s; + parsed.object->add(s); + submitAndValidate(s.slice()); + } + + // Invalid RPC Submission: Signers + // - has no `TxnSignature` field + // - has empty `SigningPubKey` field + // - has `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice)); + txn[sfSigners] = Json::arrayValue; + STParsedJSONObject parsed("test", txn.getTxn()); + Serializer s; + parsed.object->add(s); + submitAndValidate(s.slice()); + } + + // Invalid RPC Submission: tfInnerBatchTxn + // - has no `TxnSignature` field + // - has empty `SigningPubKey` field + // - has no `Signers` field + // - has `tfInnerBatchTxn` flag + { + auto txn = batch::inner(pay(alice, bob, XRP(1)), env.seq(alice)); + STParsedJSONObject parsed("test", txn.getTxn()); + Serializer s; + parsed.object->add(s); + auto const jrr = env.rpc("submit", strHex(s.slice()))[jss::result]; + BEAST_EXPECT( + jrr[jss::status] == "success" && + jrr[jss::engine_result] == "temINVALID_FLAG"); + + env.close(); + } + } + + void + testAccountActivation(FeatureBitset features) + { + testcase("account activation"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice); + env.close(); + env.memoize(bob); + + auto const preAlice = env.balance(alice); + auto const ledSeq = env.current()->seq(); + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1000)), seq + 1), + batch::inner(fset(bob, asfAllowTrustLineClawback), ledSeq), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "AccountSet", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 2); + + // Bob consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == ledSeq + 1); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - batchFee); + BEAST_EXPECT(env.balance(bob) == XRP(1000)); + } + + void + testAccountSet(FeatureBitset features) + { + testcase("account set"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(noop(alice), seq + 1); + std::string domain = "example.com"; + tx1[sfDomain] = strHex(domain); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(1)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "AccountSet", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT( + sle->getFieldVL(sfDomain) == Blob(domain.begin(), domain.end())); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + } + + void + testAccountDelete(FeatureBitset features) + { + testcase("account delete"); + + using namespace test::jtx; + using namespace std::literals; + + // tfIndependent: account delete success + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + incLgrSeqForAccDel(env, alice); + for (int i = 0; i < 5; ++i) + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2) + + env.current()->fees().increment; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(acctdelete(alice, bob), seq + 2), + // terNO_ACCOUNT: alice does not exist + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "AccountDelete", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice does not exist; Bob receives Alice's XRP + BEAST_EXPECT(!env.le(keylet::account(alice))); + BEAST_EXPECT(env.balance(bob) == preBob + (preAlice - batchFee)); + } + + // tfIndependent: account delete fails + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + incLgrSeqForAccDel(env, alice); + for (int i = 0; i < 5; ++i) + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + env.trust(bob["USD"](1000), alice); + env.close(); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2) + + env.current()->fees().increment; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfIndependent), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + // tecHAS_OBLIGATIONS: alice has obligations + batch::inner(acctdelete(alice, bob), seq + 2), + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "AccountDelete", "tecHAS_OBLIGATIONS", txIDs[1], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice does not exist; Bob receives XRP + BEAST_EXPECT(env.le(keylet::account(alice))); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // tfAllOrNothing: account delete fails + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + incLgrSeqForAccDel(env, alice); + for (int i = 0; i < 5; ++i) + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2) + + env.current()->fees().increment; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(acctdelete(alice, bob), seq + 2), + // terNO_ACCOUNT: alice does not exist + batch::inner(pay(alice, bob, XRP(2)), seq + 3)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + + // Alice still exists; Bob is unchanged + BEAST_EXPECT(env.le(keylet::account(alice))); + BEAST_EXPECT(env.balance(bob) == preBob); + } + } + + void + testObjectCreateSequence(FeatureBitset features) + { + testcase("object create w/ sequence"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + // success + { + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + uint256 const chkID{getCheckIndex(bob, env.seq(bob))}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(check::create(bob, alice, USD(10)), bobSeq), + batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq + 1), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "CheckCash", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + + // Alice pays Fee; Bob XRP Unchanged + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + + // Alice pays USD & Bob receives USD + BEAST_EXPECT( + env.balance(alice, USD.issue()) == preAliceUSD + USD(10)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10)); + } + + // failure + { + env(fset(alice, asfRequireDest)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + uint256 const chkID{getCheckIndex(bob, env.seq(bob))}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfIndependent), + // tecDST_TAG_NEEDED - alice has enabled asfRequireDest + batch::inner(check::create(bob, alice, USD(10)), bobSeq), + batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq + 1), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tecDST_TAG_NEEDED", txIDs[0], batchID}, + {2, "CheckCash", "tecNO_ENTRY", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + + // Bob consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + + // Alice pays Fee; Bob XRP Unchanged + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + + // Alice pays USD & Bob receives USD + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD); + } + } + + void + testObjectCreateTicket(FeatureBitset features) + { + testcase("object create w/ ticket"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 1, 3); + uint256 const chkID{getCheckIndex(bob, bobSeq + 1)}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(ticket::create(bob, 10), bobSeq), + batch::inner(check::create(bob, alice, USD(10)), 0, bobSeq + 1), + batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq + 1), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "TicketCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "CheckCreate", "tesSUCCESS", txIDs[1], batchID}, + {3, "CheckCash", "tesSUCCESS", txIDs[2], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + BEAST_EXPECT(env.seq(bob) == bobSeq + 10 + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD + USD(10)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10)); + } + + void + testObjectCreate3rdParty(FeatureBitset features) + { + testcase("object create w/ 3rd party"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + + env.trust(USD(1000), alice, bob); + env(pay(gw, alice, USD(100))); + env(pay(gw, bob, USD(100))); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const carolSeq = env.seq(carol); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + auto const preAliceUSD = env.balance(alice, USD.issue()); + auto const preBobUSD = env.balance(bob, USD.issue()); + + auto const batchFee = batch::calcBatchFee(env, 2, 2); + uint256 const chkID{getCheckIndex(bob, env.seq(bob))}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(carol, carolSeq, batchFee, tfAllOrNothing), + batch::inner(check::create(bob, alice, USD(10)), bobSeq), + batch::inner(check::cash(alice, chkID, USD(10)), aliceSeq), + batch::sig(alice, bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "CheckCash", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + BEAST_EXPECT(env.seq(carol) == carolSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice); + BEAST_EXPECT(env.balance(bob) == preBob); + BEAST_EXPECT(env.balance(carol) == preCarol - batchFee); + BEAST_EXPECT(env.balance(alice, USD.issue()) == preAliceUSD + USD(10)); + BEAST_EXPECT(env.balance(bob, USD.issue()) == preBobUSD - USD(10)); + } + + void + testTickets(FeatureBitset features) + { + { + testcase("tickets outer"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq + 0), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + ticket::use(aliceTicketSeq)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 9); + BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 9); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + { + testcase("tickets inner"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq), + batch::inner(pay(alice, bob, XRP(2)), 0, aliceTicketSeq + 1)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8); + BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + { + testcase("tickets outer inner"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq), + ticket::use(aliceTicketSeq)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + auto const sle = env.le(keylet::account(alice)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle->getFieldU32(sfOwnerCount) == 8); + BEAST_EXPECT(sle->getFieldU32(sfTicketCount) == 8); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + } + + void + testSequenceOpenLedger(FeatureBitset features) + { + testcase("sequence open ledger"); + + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + + // Before Batch Txn w/ retry following ledger + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. Because the + // terPRE_SEQ is outside of the batch this noop transaction will ge + // reapplied in the following ledger + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const carolSeq = env.seq(carol); + + // AccountSet Txn + auto const noopTxn = env.jt(noop(alice), seq(aliceSeq + 2)); + auto const noopTxnID = to_string(noopTxn.stx->getTransactionID()); + env(noopTxn, ter(terPRE_SEQ)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(carol, carolSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + batch::sig(alice)); + env.close(); + + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger contains noop txn + std::vector testCases = { + {0, "AccountSet", "tesSUCCESS", noopTxnID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + } + + // Before Batch Txn w/ same sequence + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // AccountSet Txn + auto const noopTxn = env.jt(noop(alice), seq(aliceSeq + 1)); + env(noopTxn, ter(terPRE_SEQ)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 2)); + env.close(); + + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + + // After Batch Txn w/ same sequence + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 2)); + + auto const noopTxn = env.jt(noop(alice), seq(aliceSeq + 1)); + auto const noopTxnID = to_string(noopTxn.stx->getTransactionID()); + env(noopTxn, ter(tesSUCCESS)); + env.close(); + + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + + // Outer Batch terPRE_SEQ + { + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const carolSeq = env.seq(carol); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + terPRE_SEQ, + batch::outer(carol, carolSeq + 1, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), aliceSeq), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + batch::sig(alice)); + + // AccountSet Txn + auto const noopTxn = env.jt(noop(carol), seq(carolSeq)); + auto const noopTxnID = to_string(noopTxn.stx->getTransactionID()); + env(noopTxn, ter(tesSUCCESS)); + env.close(); + + { + std::vector testCases = { + {0, "AccountSet", "tesSUCCESS", noopTxnID, std::nullopt}, + {1, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {2, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger contains no transactions + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + } + + void + testTicketsOpenLedger(FeatureBitset features) + { + testcase("tickets open ledger"); + + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + // Before Batch Txn w/ same ticket + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // AccountSet Txn + auto const noopTxn = + env.jt(noop(alice), ticket::use(aliceTicketSeq + 1)); + auto const noopTxnID = to_string(noopTxn.stx->getTransactionID()); + env(noopTxn, ter(tesSUCCESS)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq), + ticket::use(aliceTicketSeq)); + env.close(); + + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + + // After Batch Txn w/ same ticket + { + // IMPORTANT: The batch txn is applied first, then the noop txn. + // Because of this ordering, the noop txn is not applied and is + // overwritten by the payment in the batch transaction. + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + batch::inner(pay(alice, bob, XRP(2)), aliceSeq), + ticket::use(aliceTicketSeq)); + + // AccountSet Txn + auto const noopTxn = + env.jt(noop(alice), ticket::use(aliceTicketSeq + 1)); + env(noopTxn); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + } + + void + testObjectsOpenLedger(FeatureBitset features) + { + testcase("objects open ledger"); + + using namespace test::jtx; + using namespace std::literals; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + // Consume Object Before Batch Txn + { + // IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY + // because the create transaction has not been applied because the + // batch will run in the close ledger process. The batch will be + // allied and then retry this transaction in the current ledger. + + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // CheckCash Txn + uint256 const chkID{getCheckIndex(alice, aliceSeq)}; + auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10))); + auto const objTxnID = to_string(objTxn.stx->getTransactionID()); + env(objTxn, ter(tecNO_ENTRY)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(check::create(alice, bob, XRP(10)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + ticket::use(aliceTicketSeq)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "CheckCash", "tesSUCCESS", objTxnID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + env.close(); + { + // next ledger is empty + std::vector testCases = {}; + validateClosedLedger(env, testCases); + } + } + + // Create Object Before Batch Txn + { + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + + // CheckCreate Txn + uint256 const chkID{getCheckIndex(alice, aliceSeq)}; + auto const objTxn = env.jt(check::create(alice, bob, XRP(10))); + auto const objTxnID = to_string(objTxn.stx->getTransactionID()); + env(objTxn, ter(tesSUCCESS)); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(check::cash(bob, chkID, XRP(10)), bobSeq), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + ticket::use(aliceTicketSeq), + batch::sig(bob)); + + env.close(); + { + std::vector testCases = { + {0, "CheckCreate", "tesSUCCESS", objTxnID, std::nullopt}, + {1, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {2, "CheckCash", "tesSUCCESS", txIDs[0], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + } + } + + // After Batch Txn + { + // IMPORTANT: The initial result of `CheckCash` is tecNO_ENTRY + // because the create transaction has not been applied because the + // batch will run in the close ledger process. The batch will be + // applied and then retry this transaction in the current ledger. + + test::jtx::Env env{*this, envconfig()}; + env.fund(XRP(10000), alice, bob); + env.close(); + + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + env.close(); + + auto const aliceSeq = env.seq(alice); + + // Batch Txn + auto const batchFee = batch::calcBatchFee(env, 0, 2); + uint256 const chkID{getCheckIndex(alice, aliceSeq)}; + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, 0, batchFee, tfAllOrNothing), + batch::inner(check::create(alice, bob, XRP(10)), aliceSeq), + batch::inner(pay(alice, bob, XRP(1)), 0, aliceTicketSeq + 1), + ticket::use(aliceTicketSeq)); + + // CheckCash Txn + auto const objTxn = env.jt(check::cash(bob, chkID, XRP(10))); + auto const objTxnID = to_string(objTxn.stx->getTransactionID()); + env(objTxn, ter(tecNO_ENTRY)); + + env.close(); + { + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "CheckCreate", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + {3, "CheckCash", "tesSUCCESS", objTxnID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + } + } + + void + testPseudoTxn(FeatureBitset features) + { + testcase("pseudo txn with tfInnerBatchTxn"); + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + STTx const stx = STTx(ttAMENDMENT, [&](auto& obj) { + obj.setAccountID(sfAccount, AccountID()); + obj.setFieldH256(sfAmendment, uint256(2)); + obj.setFieldU32(sfLedgerSequence, env.seq(alice)); + obj.setFieldU32(sfFlags, tfInnerBatchTxn); + }); + + std::string reason; + BEAST_EXPECT(isPseudoTx(stx)); + BEAST_EXPECT(!passesLocalChecks(stx, reason)); + BEAST_EXPECT(reason == "Cannot submit pseudo transactions."); + env.app().openLedger().modify([&](OpenView& view, beast::Journal j) { + auto const result = ripple::apply(env.app(), view, stx, tapNONE, j); + BEAST_EXPECT(!result.applied && result.ter == temINVALID_FLAG); + return result.applied; + }); + } + + void + testOpenLedger(FeatureBitset features) + { + testcase("batch open ledger"); + // IMPORTANT: When a transaction is submitted outside of a batch and + // another transaction is part of the batch, the batch might fail + // because the sequence is out of order. This is because the canonical + // order of transactions is determined by the account first. So in this + // case, alice's batch comes after bobs self submitted transaction even + // though the payment was submitted after the batch. + + using namespace test::jtx; + using namespace std::literals; + + test::jtx::Env env{*this, envconfig()}; + XRPAmount const baseFee = env.current()->fees().base; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(10000), alice, bob); + env.close(); + + env(noop(bob), ter(tesSUCCESS)); + env.close(); + + auto const aliceSeq = env.seq(alice); + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const bobSeq = env.seq(bob); + + // Alice Pays Bob (Open Ledger) + auto const payTxn1 = env.jt(pay(alice, bob, XRP(10)), seq(aliceSeq)); + auto const payTxn1ID = to_string(payTxn1.stx->getTransactionID()); + env(payTxn1, ter(tesSUCCESS)); + + // Alice & Bob Atomic Batch + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq + 1, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 2), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob)); + + // Bob pays Alice (Open Ledger) + auto const payTxn2 = env.jt(pay(bob, alice, XRP(5)), seq(bobSeq + 1)); + auto const payTxn2ID = to_string(payTxn2.stx->getTransactionID()); + env(payTxn2, ter(terPRE_SEQ)); + env.close(); + + std::vector testCases = { + {0, "Payment", "tesSUCCESS", payTxn1ID, std::nullopt}, + {1, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {2, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {3, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + env.close(); + { + // next ledger includes the payment txn + std::vector testCases = { + {0, "Payment", "tesSUCCESS", payTxn2ID, std::nullopt}, + }; + validateClosedLedger(env, testCases); + } + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == aliceSeq + 3); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(bob) == bobSeq + 2); + + // Alice pays XRP & Fee; Bob receives XRP & pays Fee + BEAST_EXPECT( + env.balance(alice) == preAlice - XRP(10) - batchFee - baseFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(10) - baseFee); + } + + void + testBatchTxQueue(FeatureBitset features) + { + testcase("batch tx queue"); + + using namespace test::jtx; + using namespace std::literals; + + // only outer batch transactions are counter towards the queue size + { + test::jtx::Env env{ + *this, + makeSmallQueueConfig( + {{"minimum_txn_in_ledger_standalone", "2"}}), + nullptr, + beast::severities::kError}; + + auto alice = Account("alice"); + auto bob = Account("bob"); + auto carol = Account("carol"); + + // Fund across several ledgers so the TxQ metrics stay restricted. + env.fund(XRP(10000), noripple(alice, bob)); + env.close(env.now() + 5s, 10000ms); + env.fund(XRP(10000), noripple(carol)); + env.close(env.now() + 5s, 10000ms); + + // Fill the ledger + env(noop(alice)); + env(noop(alice)); + env(noop(alice)); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); + + env(noop(carol), ter(terQUEUED)); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + + // Queue Batch + { + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + ter(terQUEUED)); + } + + checkMetrics(*this, env, 2, std::nullopt, 3, 2); + + // Replace Queued Batch + { + env(batch::outer( + alice, + aliceSeq, + openLedgerFee(env, batchFee), + tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + ter(tesSUCCESS)); + env.close(); + } + + checkMetrics(*this, env, 0, 12, 1, 6); + } + + // inner batch transactions are counter towards the ledger tx count + { + test::jtx::Env env{ + *this, + makeSmallQueueConfig( + {{"minimum_txn_in_ledger_standalone", "2"}}), + nullptr, + beast::severities::kError}; + + auto alice = Account("alice"); + auto bob = Account("bob"); + auto carol = Account("carol"); + + // Fund across several ledgers so the TxQ metrics stay restricted. + env.fund(XRP(10000), noripple(alice, bob)); + env.close(env.now() + 5s, 10000ms); + env.fund(XRP(10000), noripple(carol)); + env.close(env.now() + 5s, 10000ms); + + // Fill the ledger leaving room for 1 queued transaction + env(noop(alice)); + env(noop(alice)); + checkMetrics(*this, env, 0, std::nullopt, 2, 2); + + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + auto const batchFee = batch::calcBatchFee(env, 1, 2); + + // Batch Successful + { + env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), aliceSeq + 1), + batch::inner(pay(bob, alice, XRP(5)), bobSeq), + batch::sig(bob), + ter(tesSUCCESS)); + } + + checkMetrics(*this, env, 0, std::nullopt, 3, 2); + + env(noop(carol), ter(terQUEUED)); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); + } + } + + void + testBatchNetworkOps(FeatureBitset features) + { + testcase("batch network ops"); + + using namespace test::jtx; + using namespace std::literals; + + Env env( + *this, + envconfig(), + features, + nullptr, + beast::severities::kDisabled); + + auto alice = Account("alice"); + auto bob = Account("bob"); + env.fund(XRP(10000), alice, bob); + env.close(); + + auto submitTx = [&](std::uint32_t flags) -> uint256 { + auto jt = env.jt(pay(alice, bob, XRP(1)), txflags(flags)); + Serializer s; + jt.stx->add(s); + env.app().getOPs().submitTransaction(jt.stx); + return jt.stx->getTransactionID(); + }; + + auto processTxn = [&](std::uint32_t flags) -> uint256 { + auto jt = env.jt(pay(alice, bob, XRP(1)), txflags(flags)); + Serializer s; + jt.stx->add(s); + std::string reason; + auto transaction = + std::make_shared(jt.stx, reason, env.app()); + env.app().getOPs().processTransaction( + transaction, false, true, NetworkOPs::FailHard::yes); + return transaction->getID(); + }; + + // Validate: NetworkOPs::submitTransaction() + { + // Submit a tx with tfInnerBatchTxn + uint256 const txBad = submitTx(tfInnerBatchTxn); + BEAST_EXPECT(env.app().getHashRouter().getFlags(txBad) == 0); + } + + // Validate: NetworkOPs::processTransaction() + { + uint256 const txid = processTxn(tfInnerBatchTxn); + // HashRouter::getFlags() should return SF_BAD + BEAST_EXPECT(env.app().getHashRouter().getFlags(txid) == SF_BAD); + } + } + + void + testBatchDelegate(FeatureBitset features) + { + testcase("batch delegate"); + + using namespace test::jtx; + using namespace std::literals; + + // delegated non atomic inner + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env(delegate::set(alice, bob, {"Payment"})); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + auto tx = batch::inner(pay(alice, bob, XRP(1)), seq + 1); + tx[jss::Delegate] = bob.human(); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx, + batch::inner(pay(alice, bob, XRP(2)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(3) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(3)); + } + + // delegated atomic inner + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const carol = Account("carol"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, carol, gw); + env.close(); + + env(delegate::set(bob, carol, {"Payment"})); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + auto const preCarol = env.balance(carol); + + auto const batchFee = batch::calcBatchFee(env, 1, 2); + auto const aliceSeq = env.seq(alice); + auto const bobSeq = env.seq(bob); + + auto tx = batch::inner(pay(bob, alice, XRP(1)), bobSeq); + tx[jss::Delegate] = carol.human(); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing), + tx, + batch::inner(pay(alice, bob, XRP(2)), aliceSeq + 1), + batch::sig(bob)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "Payment", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + BEAST_EXPECT(env.seq(alice) == aliceSeq + 2); + BEAST_EXPECT(env.seq(bob) == bobSeq + 1); + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(1)); + // NOTE: Carol would normally pay the fee for delegated txns, but + // because the batch is atomic, the fee is paid by the batch + BEAST_EXPECT(env.balance(carol) == preCarol); + } + + // delegated non atomic inner (AccountSet) + { + test::jtx::Env env{*this, envconfig()}; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto const gw = Account("gw"); + auto const USD = gw["USD"]; + env.fund(XRP(10000), alice, bob, gw); + env.close(); + + env(delegate::set(alice, bob, {"AccountDomainSet"})); + env.close(); + + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + + auto tx = batch::inner(noop(alice), seq + 1); + std::string const domain = "example.com"; + tx[sfDomain.jsonName] = strHex(domain); + tx[jss::Delegate] = bob.human(); + auto const [txIDs, batchID] = submitBatch( + env, + tesSUCCESS, + batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx, + batch::inner(pay(alice, bob, XRP(2)), seq + 2)); + env.close(); + + std::vector testCases = { + {0, "Batch", "tesSUCCESS", batchID, std::nullopt}, + {1, "AccountSet", "tesSUCCESS", txIDs[0], batchID}, + {2, "Payment", "tesSUCCESS", txIDs[1], batchID}, + }; + validateClosedLedger(env, testCases); + + // Alice consumes sequences (# of txns) + BEAST_EXPECT(env.seq(alice) == seq + 3); + + // Alice pays XRP & Fee; Bob receives XRP + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(2) - batchFee); + BEAST_EXPECT(env.balance(bob) == preBob + XRP(2)); + } + } + + void + testWithFeats(FeatureBitset features) + { + testEnable(features); + testPreflight(features); + testPreclaim(features); + testBadRawTxn(features); + testBadSequence(features); + testBadOuterFee(features); + testCalculateBaseFee(features); + testAllOrNothing(features); + testOnlyOne(features); + testUntilFailure(features); + testIndependent(features); + testInnerSubmitRPC(features); + testAccountActivation(features); + testAccountSet(features); + testAccountDelete(features); + testObjectCreateSequence(features); + testObjectCreateTicket(features); + testObjectCreate3rdParty(features); + testTickets(features); + testSequenceOpenLedger(features); + testTicketsOpenLedger(features); + testObjectsOpenLedger(features); + testPseudoTxn(features); + testOpenLedger(features); + testBatchTxQueue(features); + testBatchNetworkOps(features); + testBatchDelegate(features); + } + +public: + void + run() override + { + using namespace test::jtx; + auto const sa = supported_amendments(); + testWithFeats(sa); + } +}; + +BEAST_DEFINE_TESTSUITE(Batch, app, ripple); + +} // namespace test +} // namespace ripple diff --git a/src/test/app/Delegate_test.cpp b/src/test/app/Delegate_test.cpp index 5136627148..ca173a6993 100644 --- a/src/test/app/Delegate_test.cpp +++ b/src/test/app/Delegate_test.cpp @@ -231,6 +231,7 @@ class Delegate_test : public beast::unit_test::suite ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"UNLModify"}), ter(tecNO_PERMISSION)); env(delegate::set(gw, alice, {"SetFee"}), ter(tecNO_PERMISSION)); + env(delegate::set(gw, alice, {"Batch"}), ter(tecNO_PERMISSION)); } } diff --git a/src/test/app/MultiSign_test.cpp b/src/test/app/MultiSign_test.cpp index b24c7ca39e..8c1880c1a0 100644 --- a/src/test/app/MultiSign_test.cpp +++ b/src/test/app/MultiSign_test.cpp @@ -460,7 +460,7 @@ public: // Attempt a multisigned transaction that meets the quorum. auto const baseFee = env.current()->fees().base; std::uint32_t aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{cheri, cher}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{cheri, cher}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -480,7 +480,7 @@ public: BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{becky, beck}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{becky, beck}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -488,7 +488,7 @@ public: aliceSeq = env.seq(alice); env(noop(alice), fee(3 * baseFee), - msig(msig::Reg{becky, beck}, msig::Reg{cheri, cher})); + msig(Reg{becky, beck}, Reg{cheri, cher})); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); } @@ -783,12 +783,12 @@ public: BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{cheri, cher}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{cheri, cher}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); aliceSeq = env.seq(alice); - env(noop(alice), msig(msig::Reg{daria, dari}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{daria, dari}), fee(2 * baseFee)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -801,7 +801,7 @@ public: aliceSeq = env.seq(alice); env(noop(alice), fee(5 * baseFee), - msig(becky, msig::Reg{cheri, cher}, msig::Reg{daria, dari}, jinni)); + msig(becky, Reg{cheri, cher}, Reg{daria, dari}, jinni)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -820,7 +820,7 @@ public: aliceSeq = env.seq(alice); env(noop(alice), fee(9 * baseFee), - msig(becky, msig::Reg{cheri, cher}, msig::Reg{daria, dari}, jinni)); + msig(becky, Reg{cheri, cher}, Reg{daria, dari}, jinni)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -828,7 +828,7 @@ public: aliceSeq = env.seq(alice); env(noop(alice), fee(5 * baseFee), - msig(becky, cheri, msig::Reg{daria, dari}, jinni)); + msig(becky, cheri, Reg{daria, dari}, jinni)); env.close(); BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -853,8 +853,8 @@ public: fee(9 * baseFee), msig( becky, - msig::Reg{cheri, cher}, - msig::Reg{daria, dari}, + Reg{cheri, cher}, + Reg{daria, dari}, haunt, jinni, phase, @@ -1349,7 +1349,7 @@ public: // Becky cannot 2-level multisign for alice. 2-level multisigning // is not supported. env(noop(alice), - msig(msig::Reg{becky, bogie}), + msig(Reg{becky, bogie}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); @@ -1358,7 +1358,7 @@ public: // not yet enabled. Account const beck{"beck", KeyType::ed25519}; env(noop(alice), - msig(msig::Reg{becky, beck}), + msig(Reg{becky, beck}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); @@ -1368,13 +1368,13 @@ public: env(regkey(becky, beck), msig(demon), fee(2 * baseFee)); env.close(); - env(noop(alice), msig(msig::Reg{becky, beck}), fee(2 * baseFee)); + env(noop(alice), msig(Reg{becky, beck}), fee(2 * baseFee)); env.close(); // The presence of becky's regular key does not influence whether she // can 2-level multisign; it still won't work. env(noop(alice), - msig(msig::Reg{becky, demon}), + msig(Reg{becky, demon}), fee(2 * baseFee), ter(tefBAD_SIGNATURE)); env.close(); diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 7b69cee1ce..947640495d 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -42,97 +43,6 @@ class TxQPosNegFlows_test : public beast::unit_test::suite static constexpr FeeLevel64 baseFeeLevel{256}; static constexpr FeeLevel64 minEscalationFeeLevel = baseFeeLevel * 500; - void - checkMetrics( - int line, - jtx::Env& env, - std::size_t expectedCount, - std::optional expectedMaxCount, - std::size_t expectedInLedger, - std::size_t expectedPerLedger, - std::uint64_t expectedMinFeeLevel = baseFeeLevel.fee(), - std::uint64_t expectedMedFeeLevel = minEscalationFeeLevel.fee()) - { - FeeLevel64 const expectedMin{expectedMinFeeLevel}; - FeeLevel64 const expectedMed{expectedMedFeeLevel}; - auto const metrics = env.app().getTxQ().getMetrics(*env.current()); - using namespace std::string_literals; - - metrics.referenceFeeLevel == baseFeeLevel - ? pass() - : fail( - "reference: "s + - std::to_string(metrics.referenceFeeLevel.value()) + "/" + - std::to_string(baseFeeLevel.value()), - __FILE__, - line); - - metrics.txCount == expectedCount - ? pass() - : fail( - "txCount: "s + std::to_string(metrics.txCount) + "/" + - std::to_string(expectedCount), - __FILE__, - line); - - metrics.txQMaxSize == expectedMaxCount - ? pass() - : fail( - "txQMaxSize: "s + - std::to_string(metrics.txQMaxSize.value_or(0)) + "/" + - std::to_string(expectedMaxCount.value_or(0)), - __FILE__, - line); - - metrics.txInLedger == expectedInLedger - ? pass() - : fail( - "txInLedger: "s + std::to_string(metrics.txInLedger) + "/" + - std::to_string(expectedInLedger), - __FILE__, - line); - - metrics.txPerLedger == expectedPerLedger - ? pass() - : fail( - "txPerLedger: "s + std::to_string(metrics.txPerLedger) + "/" + - std::to_string(expectedPerLedger), - __FILE__, - line); - - metrics.minProcessingFeeLevel == expectedMin - ? pass() - : fail( - "minProcessingFeeLevel: "s + - std::to_string(metrics.minProcessingFeeLevel.value()) + - "/" + std::to_string(expectedMin.value()), - __FILE__, - line); - - metrics.medFeeLevel == expectedMed - ? pass() - : fail( - "medFeeLevel: "s + - std::to_string(metrics.medFeeLevel.value()) + "/" + - std::to_string(expectedMed.value()), - __FILE__, - line); - - auto const expectedCurFeeLevel = expectedInLedger > expectedPerLedger - ? expectedMed * expectedInLedger * expectedInLedger / - (expectedPerLedger * expectedPerLedger) - : metrics.referenceFeeLevel; - - metrics.openLedgerFeeLevel == expectedCurFeeLevel - ? pass() - : fail( - "openLedgerFeeLevel: "s + - std::to_string(metrics.openLedgerFeeLevel.value()) + "/" + - std::to_string(expectedCurFeeLevel.value()), - __FILE__, - line); - } - void fillQueue(jtx::Env& env, jtx::Account const& account) { @@ -244,7 +154,7 @@ class TxQPosNegFlows_test : public beast::unit_test::suite // transactions as though they are ordinary transactions. auto const flagPerLedger = 1 + ripple::detail::numUpVotedAmendments(); auto const flagMaxQueue = ledgersInQueue * flagPerLedger; - checkMetrics(__LINE__, env, 0, flagMaxQueue, 0, flagPerLedger); + checkMetrics(*this, env, 0, flagMaxQueue, 0, flagPerLedger); // Pad a couple of txs with normal fees so the median comes // back down to normal @@ -255,7 +165,7 @@ class TxQPosNegFlows_test : public beast::unit_test::suite // metrics to reset to defaults, EXCEPT the maxQueue size. using namespace std::chrono_literals; env.close(env.now() + 5s, 10000ms); - checkMetrics(__LINE__, env, 0, flagMaxQueue, 0, expectedPerLedger); + checkMetrics(*this, env, 0, flagMaxQueue, 0, expectedPerLedger); auto const fees = env.current()->fees(); BEAST_EXPECT(fees.base == XRPAmount{base}); BEAST_EXPECT(fees.reserve == XRPAmount{reserve}); @@ -287,37 +197,37 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Create several accounts while the fee is cheap so they all apply. env.fund(XRP(50000), noripple(alice, bob, charlie, daria)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Alice - price starts exploding: held env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 3); + checkMetrics(*this, env, 1, std::nullopt, 4, 3); // Bob with really high fee - applies env(noop(bob), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 5, 3); + checkMetrics(*this, env, 1, std::nullopt, 5, 3); // Daria with low fee: hold env(noop(daria), fee(baseFee * 100), queued); - checkMetrics(__LINE__, env, 2, std::nullopt, 5, 3); + checkMetrics(*this, env, 2, std::nullopt, 5, 3); env.close(); // Verify that the held transactions got applied - checkMetrics(__LINE__, env, 0, 10, 2, 5); + checkMetrics(*this, env, 0, 10, 2, 5); ////////////////////////////////////////////////////////////// // Make some more accounts. We'll need them later to abuse the queue. env.fund(XRP(50000), noripple(elmo, fred, gwen, hank)); - checkMetrics(__LINE__, env, 0, 10, 6, 5); + checkMetrics(*this, env, 0, 10, 6, 5); // Now get a bunch of transactions held. env(noop(alice), fee(baseFee * 1.2), queued); - checkMetrics(__LINE__, env, 1, 10, 6, 5); + checkMetrics(*this, env, 1, 10, 6, 5); env(noop(bob), fee(baseFee), queued); // won't clear the queue env(noop(charlie), fee(baseFee * 2), queued); @@ -326,11 +236,11 @@ public: env(noop(fred), fee(baseFee * 1.9), queued); env(noop(gwen), fee(baseFee * 1.6), queued); env(noop(hank), fee(baseFee * 1.8), queued); - checkMetrics(__LINE__, env, 8, 10, 6, 5); + checkMetrics(*this, env, 8, 10, 6, 5); env.close(); // Verify that the held transactions got applied - checkMetrics(__LINE__, env, 1, 12, 7, 6); + checkMetrics(*this, env, 1, 12, 7, 6); // Bob's transaction is still stuck in the queue. @@ -339,45 +249,45 @@ public: // Hank sends another txn env(noop(hank), fee(baseFee), queued); // But he's not going to leave it in the queue - checkMetrics(__LINE__, env, 2, 12, 7, 6); + checkMetrics(*this, env, 2, 12, 7, 6); // Hank sees his txn got held and bumps the fee, // but doesn't even bump it enough to requeue env(noop(hank), fee(baseFee * 1.1), ter(telCAN_NOT_QUEUE_FEE)); - checkMetrics(__LINE__, env, 2, 12, 7, 6); + checkMetrics(*this, env, 2, 12, 7, 6); // Hank sees his txn got held and bumps the fee, // enough to requeue, but doesn't bump it enough to // apply to the ledger env(noop(hank), fee(baseFee * 600), queued); // But he's not going to leave it in the queue - checkMetrics(__LINE__, env, 2, 12, 7, 6); + checkMetrics(*this, env, 2, 12, 7, 6); // Hank sees his txn got held and bumps the fee, // high enough to get into the open ledger, because // he doesn't want to wait. env(noop(hank), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, 12, 8, 6); + checkMetrics(*this, env, 1, 12, 8, 6); // Hank then sends another, less important txn // (In addition to the metrics, this will verify that // the original txn got removed.) env(noop(hank), fee(baseFee * 2), queued); - checkMetrics(__LINE__, env, 2, 12, 8, 6); + checkMetrics(*this, env, 2, 12, 8, 6); env.close(); // Verify that bob and hank's txns were applied - checkMetrics(__LINE__, env, 0, 16, 2, 8); + checkMetrics(*this, env, 0, 16, 2, 8); // Close again with a simulated time leap to // reset the escalation limit down to minimum env.close(env.now() + 5s, 10000ms); - checkMetrics(__LINE__, env, 0, 16, 0, 3); + checkMetrics(*this, env, 0, 16, 0, 3); // Then close once more without the time leap // to reset the queue maxsize down to minimum env.close(); - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); ////////////////////////////////////////////////////////////// @@ -390,7 +300,7 @@ public: env(noop(gwen), fee(largeFee)); env(noop(fred), fee(largeFee)); env(noop(elmo), fee(largeFee)); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); // Use explicit fees so we can control which txn // will get dropped @@ -406,7 +316,7 @@ public: // Queue is full now. // clang-format off - checkMetrics(__LINE__, env, 6, 6, 4, 3, txFeeLevelByAccount(env, daria) + 1); + checkMetrics(*this, env, 6, 6, 4, 3, txFeeLevelByAccount(env, daria) + 1); // clang-format on // Try to add another transaction with the default (low) fee, // it should fail because the queue is full. @@ -419,7 +329,7 @@ public: // Queue is still full, of course, but the min fee has gone up // clang-format off - checkMetrics(__LINE__, env, 6, 6, 4, 3, txFeeLevelByAccount(env, elmo) + 1); + checkMetrics(*this, env, 6, 6, 4, 3, txFeeLevelByAccount(env, elmo) + 1); // clang-format on // Close out the ledger, the transactions are accepted, the @@ -428,11 +338,11 @@ public: // is put back in. Neat. env.close(); // clang-format off - checkMetrics(__LINE__, env, 2, 8, 5, 4, baseFeeLevel.fee(), calcMedFeeLevel(FeeLevel64{baseFeeLevel.fee() * largeFeeMultiplier})); + checkMetrics(*this, env, 2, 8, 5, 4, baseFeeLevel.fee(), calcMedFeeLevel(FeeLevel64{baseFeeLevel.fee() * largeFeeMultiplier})); // clang-format on env.close(); - checkMetrics(__LINE__, env, 0, 10, 2, 5); + checkMetrics(*this, env, 0, 10, 2, 5); ////////////////////////////////////////////////////////////// @@ -446,10 +356,10 @@ public: env(noop(daria)); env(pay(alice, iris, XRP(1000)), queued); env(noop(iris), seq(1), fee(baseFee * 2), ter(terNO_ACCOUNT)); - checkMetrics(__LINE__, env, 1, 10, 6, 5); + checkMetrics(*this, env, 1, 10, 6, 5); env.close(); - checkMetrics(__LINE__, env, 0, 12, 1, 6); + checkMetrics(*this, env, 0, 12, 1, 6); env.require(balance(iris, XRP(1000))); BEAST_EXPECT(env.seq(iris) == 11); @@ -475,7 +385,7 @@ public: ++metrics.txCount; checkMetrics( - __LINE__, + *this, env, metrics.txCount, metrics.txQMaxSize, @@ -496,14 +406,14 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Fund alice and then fill the ledger. env.fund(XRP(50000), noripple(alice)); env(noop(alice)); env(noop(alice)); env(noop(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); ////////////////////////////////////////////////////////////////// @@ -515,11 +425,11 @@ public: env(noop(alice), ticket::use(tkt1 - 2), ter(tefNO_TICKET)); env(noop(alice), ticket::use(tkt1 - 1), ter(terPRE_TICKET)); env.require(owners(alice, 0), tickets(alice, 0)); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 3); + checkMetrics(*this, env, 1, std::nullopt, 4, 3); env.close(); env.require(owners(alice, 250), tickets(alice, 250)); - checkMetrics(__LINE__, env, 0, 8, 1, 4); + checkMetrics(*this, env, 0, 8, 1, 4); BEAST_EXPECT(env.seq(alice) == tkt1 + 250); ////////////////////////////////////////////////////////////////// @@ -547,7 +457,7 @@ public: ticket::use(tkt1 + 13), fee(baseFee * 2.3), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, expectedMinFeeLevel); + checkMetrics(*this, env, 8, 8, 5, 4, expectedMinFeeLevel); // Check which of the queued transactions got into the ledger by // attempting to replace them. @@ -579,7 +489,7 @@ public: // the queue. env(noop(alice), ticket::use(tkt1 + 13), ter(telCAN_NOT_QUEUE_FEE)); - checkMetrics(__LINE__, env, 3, 10, 6, 5); + checkMetrics(*this, env, 3, 10, 6, 5); ////////////////////////////////////////////////////////////////// @@ -610,7 +520,7 @@ public: env(noop(alice), seq(nextSeq + 5), queued); env(noop(alice), seq(nextSeq + 6), queued); env(noop(alice), seq(nextSeq + 7), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 10, 10, 6, 5, 257); + checkMetrics(*this, env, 10, 10, 6, 5, 257); // Check which of the queued transactions got into the ledger by // attempting to replace them. @@ -638,7 +548,7 @@ public: env(noop(alice), seq(nextSeq + 6), ter(telCAN_NOT_QUEUE_FEE)); env(noop(alice), seq(nextSeq + 7), ter(telCAN_NOT_QUEUE_FEE)); - checkMetrics(__LINE__, env, 4, 12, 7, 6); + checkMetrics(*this, env, 4, 12, 7, 6); BEAST_EXPECT(env.seq(alice) == nextSeq + 4); ////////////////////////////////////////////////////////////////// @@ -669,7 +579,7 @@ public: fee(baseFee * 2.1), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); env.close(); env.require(owners(alice, 231), tickets(alice, 231)); @@ -700,7 +610,7 @@ public: env(noop(alice), seq(nextSeq + 7), ter(telCAN_NOT_QUEUE_FEE)); BEAST_EXPECT(env.seq(alice) == nextSeq + 6); - checkMetrics(__LINE__, env, 6, 14, 8, 7); + checkMetrics(*this, env, 6, 14, 8, 7); ////////////////////////////////////////////////////////////////// @@ -739,7 +649,7 @@ public: env(noop(alice), seq(nextSeq + 7), ter(tefPAST_SEQ)); BEAST_EXPECT(env.seq(alice) == nextSeq + 8); - checkMetrics(__LINE__, env, 0, 16, 6, 8); + checkMetrics(*this, env, 0, 16, 6, 8); } void @@ -754,28 +664,28 @@ public: auto gw = Account("gw"); auto USD = gw["USD"]; - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); // Create accounts env.fund(XRP(50000), noripple(alice, gw)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 2); + checkMetrics(*this, env, 0, std::nullopt, 2, 2); env.close(); - checkMetrics(__LINE__, env, 0, 4, 0, 2); + checkMetrics(*this, env, 0, 4, 0, 2); // Alice creates an unfunded offer while the ledger is not full env(offer(alice, XRP(1000), USD(1000)), ter(tecUNFUNDED_OFFER)); - checkMetrics(__LINE__, env, 0, 4, 1, 2); + checkMetrics(*this, env, 0, 4, 1, 2); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 4, 3, 2); + checkMetrics(*this, env, 0, 4, 3, 2); // Alice creates an unfunded offer that goes in the queue env(offer(alice, XRP(1000), USD(1000)), ter(terQUEUED)); - checkMetrics(__LINE__, env, 1, 4, 3, 2); + checkMetrics(*this, env, 1, 4, 3, 2); // The offer comes out of the queue env.close(); - checkMetrics(__LINE__, env, 0, 6, 1, 3); + checkMetrics(*this, env, 0, 6, 1, 3); } void @@ -794,44 +704,44 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); // Create several accounts while the fee is cheap so they all apply. env.fund(XRP(50000), noripple(alice, bob, charlie)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); // Future transaction for Alice - fails env(noop(alice), fee(openLedgerCost(env)), seq(env.seq(alice) + 1), ter(terPRE_SEQ)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); // Current transaction for Alice: held env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 3, 2); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); // Alice - sequence is too far ahead, so won't queue. env(noop(alice), seq(env.seq(alice) + 2), ter(telCAN_NOT_QUEUE)); - checkMetrics(__LINE__, env, 1, std::nullopt, 3, 2); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); // Bob with really high fee - applies env(noop(bob), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 2); + checkMetrics(*this, env, 1, std::nullopt, 4, 2); // Daria with low fee: hold env(noop(charlie), fee(baseFee * 100), queued); - checkMetrics(__LINE__, env, 2, std::nullopt, 4, 2); + checkMetrics(*this, env, 2, std::nullopt, 4, 2); // Alice with normal fee: hold env(noop(alice), seq(env.seq(alice) + 1), queued); - checkMetrics(__LINE__, env, 3, std::nullopt, 4, 2); + checkMetrics(*this, env, 3, std::nullopt, 4, 2); env.close(); // Verify that the held transactions got applied // Alice's bad transaction applied from the // Local Txs. - checkMetrics(__LINE__, env, 0, 8, 4, 4); + checkMetrics(*this, env, 0, 8, 4, 4); } void @@ -853,7 +763,7 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); // Fund across several ledgers so the TxQ metrics stay restricted. env.fund(XRP(1000), noripple(alice, bob)); @@ -863,11 +773,11 @@ public: env.fund(XRP(1000), noripple(edgar, felicia)); env.close(env.now() + 5s, 10000ms); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); env(noop(bob)); env(noop(charlie)); env(noop(daria)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); BEAST_EXPECT(env.current()->info().seq == 6); // Fail to queue an item with a low LastLedgerSeq @@ -886,7 +796,7 @@ public: env(noop(charlie), fee(largeFee), queued); env(noop(daria), fee(largeFee), queued); env(noop(edgar), fee(largeFee), queued); - checkMetrics(__LINE__, env, 5, std::nullopt, 3, 2); + checkMetrics(*this, env, 5, std::nullopt, 3, 2); { auto& txQ = env.app().getTxQ(); auto aliceStat = txQ.getAccountTxs(alice.id()); @@ -910,7 +820,7 @@ public: } env.close(); - checkMetrics(__LINE__, env, 1, 6, 4, 3); + checkMetrics(*this, env, 1, 6, 4, 3); // Keep alice's transaction waiting. env(noop(bob), fee(largeFee), queued); @@ -918,12 +828,12 @@ public: env(noop(daria), fee(largeFee), queued); env(noop(edgar), fee(largeFee), queued); env(noop(felicia), fee(largeFee - 1), queued); - checkMetrics(__LINE__, env, 6, 6, 4, 3, 257); + checkMetrics(*this, env, 6, 6, 4, 3, 257); env.close(); // alice's transaction is still hanging around // clang-format off - checkMetrics(__LINE__, env, 1, 8, 5, 4, baseFeeLevel.fee(), baseFeeLevel.fee() * largeFeeMultiplier); + checkMetrics(*this, env, 1, 8, 5, 4, baseFeeLevel.fee(), baseFeeLevel.fee() * largeFeeMultiplier); // clang-format on BEAST_EXPECT(env.seq(alice) == 3); @@ -938,7 +848,7 @@ public: env(noop(edgar), fee(anotherLargeFee), queued); env(noop(felicia), fee(anotherLargeFee - 1), queued); env(noop(felicia), fee(anotherLargeFee - 1), seq(env.seq(felicia) + 1), queued); - checkMetrics(__LINE__, env, 8, 8, 5, 4, baseFeeLevel.fee() + 1, baseFeeLevel.fee() * largeFeeMultiplier); + checkMetrics(*this, env, 8, 8, 5, 4, baseFeeLevel.fee() + 1, baseFeeLevel.fee() * largeFeeMultiplier); // clang-format on env.close(); @@ -946,7 +856,7 @@ public: // into the ledger, so her transaction is gone, // though one of felicia's is still in the queue. // clang-format off - checkMetrics(__LINE__, env, 1, 10, 6, 5, baseFeeLevel.fee(), baseFeeLevel.fee() * largeFeeMultiplier); + checkMetrics(*this, env, 1, 10, 6, 5, baseFeeLevel.fee(), baseFeeLevel.fee() * largeFeeMultiplier); // clang-format on BEAST_EXPECT(env.seq(alice) == 3); BEAST_EXPECT(env.seq(felicia) == 7); @@ -954,7 +864,7 @@ public: env.close(); // And now the queue is empty // clang-format off - checkMetrics(__LINE__, env, 0, 12, 1, 6, baseFeeLevel.fee(), baseFeeLevel.fee() * anotherLargeFeeMultiplier); + checkMetrics(*this, env, 0, 12, 1, 6, baseFeeLevel.fee(), baseFeeLevel.fee() * anotherLargeFeeMultiplier); // clang-format on BEAST_EXPECT(env.seq(alice) == 3); BEAST_EXPECT(env.seq(felicia) == 8); @@ -976,7 +886,7 @@ public: auto queued = ter(terQUEUED); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); // Fund across several ledgers so the TxQ metrics stay restricted. env.fund(XRP(1000), noripple(alice, bob)); @@ -988,21 +898,21 @@ public: env(noop(alice)); env(noop(alice)); env(noop(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); env(noop(bob), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 3, 2); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); // Since Alice's queue is empty this blocker can go into her queue. env(regkey(alice, bob), fee(0), queued); - checkMetrics(__LINE__, env, 2, std::nullopt, 3, 2); + checkMetrics(*this, env, 2, std::nullopt, 3, 2); // Close out this ledger so we can get a maxsize env.close(); - checkMetrics(__LINE__, env, 0, 6, 2, 3); + checkMetrics(*this, env, 0, 6, 2, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); constexpr auto aliceFeeMultiplier = 3; auto feeAlice = baseFee * aliceFeeMultiplier; @@ -1013,12 +923,12 @@ public: feeAlice = (feeAlice + 1) * 125 / 100; ++seqAlice; } - checkMetrics(__LINE__, env, 4, 6, 4, 3); + checkMetrics(*this, env, 4, 6, 4, 3); // Bob adds a zero fee blocker to his queue. auto const seqBob = env.seq(bob); env(regkey(bob, alice), fee(0), queued); - checkMetrics(__LINE__, env, 5, 6, 4, 3); + checkMetrics(*this, env, 5, 6, 4, 3); // Carol fills the queue. auto feeCarol = feeAlice; @@ -1030,7 +940,7 @@ public: ++seqCarol; } // clang-format off - checkMetrics( __LINE__, env, 6, 6, 4, 3, baseFeeLevel.fee() * aliceFeeMultiplier + 1); + checkMetrics(*this, env, 6, 6, 4, 3, baseFeeLevel.fee() * aliceFeeMultiplier + 1); // clang-format on // Carol submits high enough to beat Bob's average fee which kicks @@ -1042,20 +952,20 @@ public: env.close(); // Some of Alice's transactions stay in the queue. Bob's // transaction returns to the TxQ. - checkMetrics(__LINE__, env, 5, 8, 5, 4); + checkMetrics(*this, env, 5, 8, 5, 4); BEAST_EXPECT(env.seq(alice) == seqAlice - 4); BEAST_EXPECT(env.seq(bob) == seqBob); BEAST_EXPECT(env.seq(carol) == seqCarol + 1); env.close(); // The remaining queued transactions flush through to the ledger. - checkMetrics(__LINE__, env, 0, 10, 5, 5); + checkMetrics(*this, env, 0, 10, 5, 5); BEAST_EXPECT(env.seq(alice) == seqAlice); BEAST_EXPECT(env.seq(bob) == seqBob + 1); BEAST_EXPECT(env.seq(carol) == seqCarol + 1); env.close(); - checkMetrics(__LINE__, env, 0, 10, 0, 5); + checkMetrics(*this, env, 0, 10, 0, 5); BEAST_EXPECT(env.seq(alice) == seqAlice); BEAST_EXPECT(env.seq(bob) == seqBob + 1); BEAST_EXPECT(env.seq(carol) == seqCarol + 1); @@ -1101,19 +1011,19 @@ public: auto queued = ter(terQUEUED); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); env.fund(XRP(1000), noripple(alice, bob)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 2); + checkMetrics(*this, env, 0, std::nullopt, 2, 2); // Fill the ledger env(noop(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 2); + checkMetrics(*this, env, 0, std::nullopt, 3, 2); // Put a transaction in the queue env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 3, 2); + checkMetrics(*this, env, 1, std::nullopt, 3, 2); // Now cheat, and bypass the queue. { @@ -1131,12 +1041,12 @@ public: }); env.postconditions(jt, parsed); } - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 2); + checkMetrics(*this, env, 1, std::nullopt, 4, 2); env.close(); // Alice's queued transaction failed in TxQ::accept // with tefPAST_SEQ - checkMetrics(__LINE__, env, 0, 8, 0, 4); + checkMetrics(*this, env, 0, 8, 0, 4); } void @@ -1158,7 +1068,7 @@ public: auto queued = ter(terQUEUED); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // ledgers in queue is 2 because of makeConfig auto const initQueueMax = initFee(env, 3, 2, 10, 200, 50); @@ -1166,11 +1076,11 @@ public: // Create several accounts while the fee is cheap so they all apply. env.fund(drops(2000), noripple(alice)); env.fund(XRP(500000), noripple(bob, charlie, daria)); - checkMetrics(__LINE__, env, 0, initQueueMax, 4, 3); + checkMetrics(*this, env, 0, initQueueMax, 4, 3); // Alice - price starts exploding: held env(noop(alice), fee(11), queued); - checkMetrics(__LINE__, env, 1, initQueueMax, 4, 3); + checkMetrics(*this, env, 1, initQueueMax, 4, 3); auto aliceSeq = env.seq(alice); auto bobSeq = env.seq(bob); @@ -1178,28 +1088,28 @@ public: // Alice - try to queue a second transaction, but leave a gap env(noop(alice), seq(aliceSeq + 2), fee(100), ter(telCAN_NOT_QUEUE)); - checkMetrics(__LINE__, env, 1, initQueueMax, 4, 3); + checkMetrics(*this, env, 1, initQueueMax, 4, 3); // Alice - queue a second transaction. Yay! env(noop(alice), seq(aliceSeq + 1), fee(13), queued); - checkMetrics(__LINE__, env, 2, initQueueMax, 4, 3); + checkMetrics(*this, env, 2, initQueueMax, 4, 3); // Alice - queue a third transaction. Yay. env(noop(alice), seq(aliceSeq + 2), fee(17), queued); - checkMetrics(__LINE__, env, 3, initQueueMax, 4, 3); + checkMetrics(*this, env, 3, initQueueMax, 4, 3); // Bob - queue a transaction env(noop(bob), queued); - checkMetrics(__LINE__, env, 4, initQueueMax, 4, 3); + checkMetrics(*this, env, 4, initQueueMax, 4, 3); // Bob - queue a second transaction env(noop(bob), seq(bobSeq + 1), fee(50), queued); - checkMetrics(__LINE__, env, 5, initQueueMax, 4, 3); + checkMetrics(*this, env, 5, initQueueMax, 4, 3); // Charlie - queue a transaction, with a higher fee // than default env(noop(charlie), fee(15), queued); - checkMetrics(__LINE__, env, 6, initQueueMax, 4, 3); + checkMetrics(*this, env, 6, initQueueMax, 4, 3); BEAST_EXPECT(env.seq(alice) == aliceSeq); BEAST_EXPECT(env.seq(bob) == bobSeq); @@ -1208,7 +1118,7 @@ public: env.close(); // Verify that all of but one of the queued transactions // got applied. - checkMetrics(__LINE__, env, 1, 8, 5, 4); + checkMetrics(*this, env, 1, 8, 5, 4); // Verify that the stuck transaction is Bob's second. // Even though it had a higher fee than Alice's and @@ -1230,7 +1140,7 @@ public: queued); ++aliceSeq; } - checkMetrics(__LINE__, env, 8, 8, 5, 4, 513); + checkMetrics(*this, env, 8, 8, 5, 4, 513); { auto& txQ = env.app().getTxQ(); auto aliceStat = txQ.getAccountTxs(alice.id()); @@ -1261,24 +1171,24 @@ public: json(jss::LastLedgerSequence, lastLedgerSeq + 7), fee(aliceFee), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 513); + checkMetrics(*this, env, 8, 8, 5, 4, 513); // Charlie - try to add another item to the queue, // which fails because fee is lower than Alice's // queued average. env(noop(charlie), fee(19), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 513); + checkMetrics(*this, env, 8, 8, 5, 4, 513); // Charlie - add another item to the queue, which // causes Alice's last txn to drop env(noop(charlie), fee(30), queued); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); // Alice - now attempt to add one more to the queue, // which fails because the last tx was dropped, so // there is no complete chain. env(noop(alice), seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); // Alice wants this tx more than the dropped tx, // so resubmits with higher fee, but the queue @@ -1287,7 +1197,7 @@ public: seq(aliceSeq - 1), fee(aliceFee), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); // Try to replace a middle item in the queue // without enough fee. @@ -1297,18 +1207,18 @@ public: seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE_FEE)); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); // Replace a middle item from the queue successfully ++aliceFee; env(noop(alice), seq(aliceSeq), fee(aliceFee), queued); - checkMetrics(__LINE__, env, 8, 8, 5, 4, 538); + checkMetrics(*this, env, 8, 8, 5, 4, 538); env.close(); // Alice's transactions processed, along with // Charlie's, and the lost one is replayed and // added back to the queue. - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); aliceSeq = env.seq(alice) + 1; @@ -1322,18 +1232,18 @@ public: seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); // Try to spend more than Alice can afford with all the other txs. aliceSeq += 2; env(noop(alice), seq(aliceSeq), fee(aliceFee), ter(terINSUF_FEE_B)); - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); // Replace the last queued item with a transaction that will // bankrupt Alice --aliceFee; env(noop(alice), seq(aliceSeq), fee(aliceFee), queued); - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); // Alice - Attempt to queue a last transaction, but it // fails because the fee in flight is too high, before @@ -1344,14 +1254,14 @@ public: seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 4, 10, 6, 5); + checkMetrics(*this, env, 4, 10, 6, 5); env.close(); // All of Alice's transactions applied. - checkMetrics(__LINE__, env, 0, 12, 4, 6); + checkMetrics(*this, env, 0, 12, 4, 6); env.close(); - checkMetrics(__LINE__, env, 0, 12, 0, 6); + checkMetrics(*this, env, 0, 12, 0, 6); // Alice is broke env.require(balance(alice, XRP(0))); @@ -1361,17 +1271,17 @@ public: // account limit (10) txs. fillQueue(env, bob); bobSeq = env.seq(bob); - checkMetrics(__LINE__, env, 0, 12, 7, 6); + checkMetrics(*this, env, 0, 12, 7, 6); for (int i = 0; i < 10; ++i) env(noop(bob), seq(bobSeq + i), queued); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Bob hit the single account limit env(noop(bob), seq(bobSeq + 10), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Bob can replace one of the earlier txs regardless // of the limit env(noop(bob), seq(bobSeq + 5), fee(20), queued); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Try to replace a middle item in the queue // with enough fee to bankrupt bob and make the @@ -1382,7 +1292,7 @@ public: seq(bobSeq + 5), fee(bobFee), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Attempt to replace a middle item in the queue with enough fee // to bankrupt bob, and also to use fee averaging to clear out the @@ -1396,14 +1306,14 @@ public: seq(bobSeq + 5), fee(bobFee), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 10, 12, 7, 6); + checkMetrics(*this, env, 10, 12, 7, 6); // Close the ledger and verify that the queued transactions succeed // and bob has the right ending balance. env.close(); - checkMetrics(__LINE__, env, 3, 14, 8, 7); + checkMetrics(*this, env, 3, 14, 8, 7); env.close(); - checkMetrics(__LINE__, env, 0, 16, 3, 8); + checkMetrics(*this, env, 0, 16, 3, 8); env.require(balance(bob, drops(499'999'999'750))); } @@ -1431,20 +1341,20 @@ public: BEAST_EXPECT(env.current()->fees().base == 10); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 4); + checkMetrics(*this, env, 0, std::nullopt, 0, 4); // Create several accounts while the fee is cheap so they all apply. env.fund(XRP(50000), noripple(alice, bob, charlie, daria)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 4); + checkMetrics(*this, env, 0, std::nullopt, 4, 4); env.close(); - checkMetrics(__LINE__, env, 0, 8, 0, 4); + checkMetrics(*this, env, 0, 8, 0, 4); env.fund(XRP(50000), noripple(elmo, fred, gwen, hank)); - checkMetrics(__LINE__, env, 0, 8, 4, 4); + checkMetrics(*this, env, 0, 8, 4, 4); env.close(); - checkMetrics(__LINE__, env, 0, 8, 0, 4); + checkMetrics(*this, env, 0, 8, 0, 4); ////////////////////////////////////////////////////////////// @@ -1455,7 +1365,7 @@ public: env(noop(gwen)); env(noop(fred)); env(noop(elmo)); - checkMetrics(__LINE__, env, 0, 8, 5, 4); + checkMetrics(*this, env, 0, 8, 5, 4); auto aliceSeq = env.seq(alice); auto bobSeq = env.seq(bob); @@ -1482,7 +1392,7 @@ public: // Queue is full now. Minimum fee now reflects the // lowest fee in the queue. auto minFeeLevel = txFeeLevelByAccount(env, alice); - checkMetrics(__LINE__, env, 8, 8, 5, 4, minFeeLevel + 1); + checkMetrics(*this, env, 8, 8, 5, 4, minFeeLevel + 1); // Try to add another transaction with the default (low) fee, // it should fail because it can't replace the one already @@ -1495,13 +1405,13 @@ public: env(noop(charlie), fee(100), seq(charlieSeq + 1), queued); // Queue is still full. - checkMetrics(__LINE__, env, 8, 8, 5, 4, minFeeLevel + 1); + checkMetrics(*this, env, 8, 8, 5, 4, minFeeLevel + 1); // Six txs are processed out of the queue into the ledger, // leaving two txs. The dropped tx is retried from localTxs, and // put back into the queue. env.close(); - checkMetrics(__LINE__, env, 3, 10, 6, 5); + checkMetrics(*this, env, 3, 10, 6, 5); // This next test should remain unchanged regardless of // transaction ordering @@ -1587,7 +1497,7 @@ public: env(noop(gwen), seq(gwenSeq + qTxCount1[gwen.id()]++), fee(15), queued); minFeeLevel = txFeeLevelByAccount(env, gwen) + 1; - checkMetrics(__LINE__, env, 10, 10, 6, 5, minFeeLevel); + checkMetrics(*this, env, 10, 10, 6, 5, minFeeLevel); // Add another transaction, with a higher fee, // Not high enough to get into the ledger, but high @@ -1597,13 +1507,13 @@ public: seq(aliceSeq + qTxCount1[alice.id()]++), queued); - checkMetrics(__LINE__, env, 10, 10, 6, 5, minFeeLevel); + checkMetrics(*this, env, 10, 10, 6, 5, minFeeLevel); // Seven txs are processed out of the queue, leaving 3. One // dropped tx is retried from localTxs, and put back into the // queue. env.close(); - checkMetrics(__LINE__, env, 4, 12, 7, 6); + checkMetrics(*this, env, 4, 12, 7, 6); // Refresh the queue counts auto qTxCount2 = getTxsQueued(); @@ -1668,13 +1578,13 @@ public: auto alice = Account("alice"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 1); + checkMetrics(*this, env, 0, std::nullopt, 0, 1); env.fund(XRP(50000), noripple(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 1, 1); + checkMetrics(*this, env, 0, std::nullopt, 1, 1); env(fset(alice, asfAccountTxnID)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 1); + checkMetrics(*this, env, 0, std::nullopt, 2, 1); // Immediately after the fset, the sfAccountTxnID field // is still uninitialized, so preflight succeeds here, @@ -1683,14 +1593,14 @@ public: json(R"({"AccountTxnID": "0"})"), ter(telCAN_NOT_QUEUE)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 1); + checkMetrics(*this, env, 0, std::nullopt, 2, 1); env.close(); // The failed transaction is retried from LocalTx // and succeeds. - checkMetrics(__LINE__, env, 0, 4, 1, 2); + checkMetrics(*this, env, 0, 4, 1, 2); env(noop(alice)); - checkMetrics(__LINE__, env, 0, 4, 2, 2); + checkMetrics(*this, env, 0, 4, 2, 2); env(noop(alice), json(R"({"AccountTxnID": "0"})"), ter(tefWRONG_PRIOR)); } @@ -1714,10 +1624,10 @@ public: auto alice = Account("alice"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 2); + checkMetrics(*this, env, 0, std::nullopt, 0, 2); env.fund(XRP(50000), noripple(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 1, 2); + checkMetrics(*this, env, 0, std::nullopt, 1, 2); FeeLevel64 medFeeLevel; for (int i = 0; i < 10; ++i) @@ -1737,12 +1647,12 @@ public: env(noop(alice), fee(cost)); } - checkMetrics(__LINE__, env, 0, std::nullopt, 11, 2); + checkMetrics(*this, env, 0, std::nullopt, 11, 2); env.close(); // If not for the maximum, the per ledger would be 11. // clang-format off - checkMetrics(__LINE__, env, 0, 10, 0, 5, baseFeeLevel.fee(), calcMedFeeLevel(medFeeLevel)); + checkMetrics(*this, env, 0, 10, 0, 5, baseFeeLevel.fee(), calcMedFeeLevel(medFeeLevel)); // clang-format on } @@ -1831,22 +1741,22 @@ public: // ledgers in queue is 2 because of makeConfig auto const initQueueMax = initFee(env, 3, 2, 10, 200, 50); - checkMetrics(__LINE__, env, 0, initQueueMax, 0, 3); + checkMetrics(*this, env, 0, initQueueMax, 0, 3); env.fund(drops(5000), noripple(alice)); env.fund(XRP(50000), noripple(bob)); - checkMetrics(__LINE__, env, 0, initQueueMax, 2, 3); + checkMetrics(*this, env, 0, initQueueMax, 2, 3); auto USD = bob["USD"]; env(offer(alice, USD(5000), drops(5000)), require(owners(alice, 1))); - checkMetrics(__LINE__, env, 0, initQueueMax, 3, 3); + checkMetrics(*this, env, 0, initQueueMax, 3, 3); env.close(); - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); // Fill up the ledger fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); // Queue up a couple of transactions, plus one // more expensive one. @@ -1855,7 +1765,7 @@ public: env(noop(alice), seq(aliceSeq++), queued); env(noop(alice), seq(aliceSeq++), queued); env(noop(alice), fee(drops(1000)), seq(aliceSeq), queued); - checkMetrics(__LINE__, env, 4, 6, 4, 3); + checkMetrics(*this, env, 4, 6, 4, 3); // This offer should take Alice's offer // up to Alice's reserve. @@ -1863,7 +1773,7 @@ public: fee(openLedgerCost(env)), require( balance(alice, drops(250)), owners(alice, 1), lines(alice, 1))); - checkMetrics(__LINE__, env, 4, 6, 5, 3); + checkMetrics(*this, env, 4, 6, 5, 3); // Try adding a new transaction. // Too many fees in flight. @@ -1871,12 +1781,12 @@ public: fee(drops(200)), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 4, 6, 5, 3); + checkMetrics(*this, env, 4, 6, 5, 3); // Close the ledger. All of Alice's transactions // take a fee, except the last one. env.close(); - checkMetrics(__LINE__, env, 1, 10, 3, 5); + checkMetrics(*this, env, 1, 10, 3, 5); env.require(balance(alice, drops(250 - 30))); // Still can't add a new transaction for Alice, @@ -1885,7 +1795,7 @@ public: fee(drops(200)), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 1, 10, 3, 5); + checkMetrics(*this, env, 1, 10, 3, 5); /* At this point, Alice's transaction is indefinitely stuck in the queue. Eventually it will either @@ -1897,13 +1807,13 @@ public: for (int i = 0; i < 9; ++i) { env.close(); - checkMetrics(__LINE__, env, 1, 10, 0, 5); + checkMetrics(*this, env, 1, 10, 0, 5); } // And Alice's transaction expires (via the retry limit, // not LastLedgerSequence). env.close(); - checkMetrics(__LINE__, env, 0, 10, 0, 5); + checkMetrics(*this, env, 0, 10, 0, 5); } void @@ -1922,11 +1832,11 @@ public: Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000), noripple(alice, bob)); env.memoize(charlie); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 3); + checkMetrics(*this, env, 0, std::nullopt, 2, 3); { // Cannot put a blocker in an account's queue if that queue // already holds two or more (non-blocker) entries. @@ -1935,7 +1845,7 @@ public: env(noop(alice)); // Set a regular key just to clear the password spent flag env(regkey(alice, charlie)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Put two "normal" txs in the queue auto const aliceSeq = env.seq(alice); @@ -1961,11 +1871,11 @@ public: // Other accounts are not affected env(noop(bob), queued); - checkMetrics(__LINE__, env, 3, std::nullopt, 4, 3); + checkMetrics(*this, env, 3, std::nullopt, 4, 3); // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 8, 4, 4); + checkMetrics(*this, env, 0, 8, 4, 4); } { // Replace a lone non-blocking tx with a blocker. @@ -2006,7 +1916,7 @@ public: // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 10, 3, 5); + checkMetrics(*this, env, 0, 10, 3, 5); } { // Put a blocker in an empty queue. @@ -2034,7 +1944,7 @@ public: // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 12, 3, 6); + checkMetrics(*this, env, 0, 12, 3, 6); } } @@ -2054,12 +1964,12 @@ public: Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000), noripple(alice, bob)); env.memoize(charlie); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 3); + checkMetrics(*this, env, 0, std::nullopt, 2, 3); std::uint32_t tkt{env.seq(alice) + 1}; { @@ -2070,7 +1980,7 @@ public: env(ticket::create(alice, 250), seq(tkt - 1)); // Set a regular key just to clear the password spent flag env(regkey(alice, charlie)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Put two "normal" txs in the queue auto const aliceSeq = env.seq(alice); @@ -2100,11 +2010,11 @@ public: // Other accounts are not affected env(noop(bob), queued); - checkMetrics(__LINE__, env, 3, std::nullopt, 4, 3); + checkMetrics(*this, env, 3, std::nullopt, 4, 3); // Drain the queue and local transactions. env.close(); - checkMetrics(__LINE__, env, 0, 8, 5, 4); + checkMetrics(*this, env, 0, 8, 5, 4); // Show that the local transactions have flushed through as well. BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -2166,7 +2076,7 @@ public: // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 10, 4, 5); + checkMetrics(*this, env, 0, 10, 4, 5); // Show that the local transactions have flushed through as well. BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); @@ -2200,7 +2110,7 @@ public: // Drain the queue. env.close(); - checkMetrics(__LINE__, env, 0, 12, 3, 6); + checkMetrics(*this, env, 0, 12, 3, 6); } } @@ -2232,10 +2142,10 @@ public: auto limit = 3; - checkMetrics(__LINE__, env, 0, initQueueMax, 0, limit); + checkMetrics(*this, env, 0, initQueueMax, 0, limit); env.fund(XRP(50000), noripple(alice, charlie), gw); - checkMetrics(__LINE__, env, 0, initQueueMax, limit + 1, limit); + checkMetrics(*this, env, 0, initQueueMax, limit + 1, limit); auto USD = gw["USD"]; auto BUX = gw["BUX"]; @@ -2250,16 +2160,16 @@ public: // If this offer crosses, all of alice's // XRP will be taken (except the reserve). env(offer(alice, BUX(5000), XRP(50000)), queued); - checkMetrics(__LINE__, env, 1, initQueueMax, limit + 1, limit); + checkMetrics(*this, env, 1, initQueueMax, limit + 1, limit); // But because the reserve is protected, another // transaction will be allowed to queue env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, initQueueMax, limit + 1, limit); + checkMetrics(*this, env, 2, initQueueMax, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // But once we close the ledger, we find alice // has plenty of XRP, because the offer didn't @@ -2271,7 +2181,7 @@ public: ////////////////////////////////////////// // Offer with high XRP out and high total fee blocks later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2279,12 +2189,12 @@ public: // Alice creates an offer with a fee of half the reserve env(offer(alice, BUX(5000), XRP(50000)), fee(drops(100)), queued); - checkMetrics(__LINE__, env, 1, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 1, limit * 2, limit + 1, limit); // Alice creates another offer with a fee // that brings the total to just shy of the reserve env(noop(alice), fee(drops(99)), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); // So even a noop will look like alice // doesn't have the balance to pay the fee @@ -2292,11 +2202,11 @@ public: fee(drops(51)), seq(aliceSeq + 2), ter(terINSUF_FEE_B)); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 3, limit); + checkMetrics(*this, env, 0, limit * 2, 3, limit); // But once we close the ledger, we find alice // has plenty of XRP, because the offer didn't @@ -2308,7 +2218,7 @@ public: ////////////////////////////////////////// // Offer with high XRP out and super high fee blocks later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2317,7 +2227,7 @@ public: // Alice creates an offer with a fee larger than the reserve // This one can queue because it's the first in the queue for alice env(offer(alice, BUX(5000), XRP(50000)), fee(drops(300)), queued); - checkMetrics(__LINE__, env, 1, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 1, limit * 2, limit + 1, limit); // So even a noop will look like alice // doesn't have the balance to pay the fee @@ -2325,11 +2235,11 @@ public: fee(drops(51)), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BALANCE)); - checkMetrics(__LINE__, env, 1, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 1, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // But once we close the ledger, we find alice // has plenty of XRP, because the offer didn't @@ -2341,7 +2251,7 @@ public: ////////////////////////////////////////// // Offer with low XRP out allows later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2351,11 +2261,11 @@ public: // And later transactions are just fine env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // But once we close the ledger, we find alice // has plenty of XRP, because the offer didn't @@ -2367,7 +2277,7 @@ public: ////////////////////////////////////////// // Large XRP payment doesn't block later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2380,11 +2290,11 @@ public: // But because the reserve is protected, another // transaction will be allowed to queue env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // But once we close the ledger, we find alice // still has most of her balance, because the @@ -2394,7 +2304,7 @@ public: ////////////////////////////////////////// // Small XRP payment allows later txs fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2405,11 +2315,11 @@ public: // And later transactions are just fine env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // The payment succeeds env.require( @@ -2420,19 +2330,19 @@ public: auto const amount = USD(500000); env(trust(alice, USD(50000000))); env(trust(charlie, USD(50000000))); - checkMetrics(__LINE__, env, 0, limit * 2, 4, limit); + checkMetrics(*this, env, 0, limit * 2, 4, limit); // Close so we don't have to deal // with tx ordering in consensus. env.close(); env(pay(gw, alice, amount)); - checkMetrics(__LINE__, env, 0, limit * 2, 1, limit); + checkMetrics(*this, env, 0, limit * 2, 1, limit); // Close so we don't have to deal // with tx ordering in consensus. env.close(); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2445,11 +2355,11 @@ public: // But that's fine, because it doesn't affect // alice's XRP balance (other than the fee, of course). env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // So once we close the ledger, alice has her // XRP balance, but her USD balance went to charlie. @@ -2469,7 +2379,7 @@ public: env.close(); fillQueue(env, charlie); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2485,11 +2395,11 @@ public: // But because the reserve is protected, another // transaction will be allowed to queue env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // So once we close the ledger, alice sent a payment // to charlie using only a portion of her XRP balance @@ -2504,7 +2414,7 @@ public: // Small XRP to IOU payment allows later txs. fillQueue(env, charlie); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2519,11 +2429,11 @@ public: // And later transactions are just fine env(noop(alice), seq(aliceSeq + 1), queued); - checkMetrics(__LINE__, env, 2, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 2, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 2, limit); + checkMetrics(*this, env, 0, limit * 2, 2, limit); // So once we close the ledger, alice sent a payment // to charlie using only a portion of her XRP balance @@ -2540,7 +2450,7 @@ public: env.close(); fillQueue(env, charlie); - checkMetrics(__LINE__, env, 0, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 0, limit * 2, limit + 1, limit); aliceSeq = env.seq(alice); aliceBal = env.balance(alice); @@ -2550,11 +2460,11 @@ public: env(noop(alice), seq(aliceSeq + 1), ter(terINSUF_FEE_B)); BEAST_EXPECT(env.balance(alice) == drops(30)); - checkMetrics(__LINE__, env, 1, limit * 2, limit + 1, limit); + checkMetrics(*this, env, 1, limit * 2, limit + 1, limit); env.close(); ++limit; - checkMetrics(__LINE__, env, 0, limit * 2, 1, limit); + checkMetrics(*this, env, 0, limit * 2, 1, limit); BEAST_EXPECT(env.balance(alice) == drops(5)); } @@ -2639,27 +2549,27 @@ public: Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); auto const baseFee = env.current()->fees().base.drops(); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Fund accounts while the fee is cheap so they all apply. env.fund(XRP(50000), noripple(alice, bob, charlie)); - checkMetrics(__LINE__, env, 0, std::nullopt, 3, 3); + checkMetrics(*this, env, 0, std::nullopt, 3, 3); // Alice - no fee change yet env(noop(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Bob with really high fee - applies env(noop(bob), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 0, std::nullopt, 5, 3); + checkMetrics(*this, env, 0, std::nullopt, 5, 3); // Charlie with low fee: queued env(noop(charlie), fee(baseFee * 100), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 5, 3); + checkMetrics(*this, env, 1, std::nullopt, 5, 3); env.close(); // Verify that the queued transaction was applied - checkMetrics(__LINE__, env, 0, 10, 1, 5); + checkMetrics(*this, env, 0, 10, 1, 5); ///////////////////////////////////////////////////////////////// @@ -2670,7 +2580,7 @@ public: env(noop(bob), fee(baseFee * 100)); env(noop(bob), fee(baseFee * 100)); env(noop(bob), fee(baseFee * 100)); - checkMetrics(__LINE__, env, 0, 10, 6, 5); + checkMetrics(*this, env, 0, 10, 6, 5); // Use explicit fees so we can control which txn // will get dropped @@ -2695,7 +2605,7 @@ public: env(noop(alice), fee(baseFee * 2.1), seq(aliceSeq++), queued); // Queue is full now. - checkMetrics(__LINE__, env, 10, 10, 6, 5, expectedFeeLevel + 1); + checkMetrics(*this, env, 10, 10, 6, 5, expectedFeeLevel + 1); // Try to add another transaction with the default (low) fee, // it should fail because the queue is full. @@ -2825,7 +2735,7 @@ public: auto const bob = Account("bob"); env.fund(XRP(500000), noripple(alice, bob)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 1); + checkMetrics(*this, env, 0, std::nullopt, 2, 1); auto const aliceSeq = env.seq(alice); BEAST_EXPECT(env.current()->info().seq == 3); @@ -2845,7 +2755,7 @@ public: seq(aliceSeq + 3), json(R"({"LastLedgerSequence":11})"), ter(terQUEUED)); - checkMetrics(__LINE__, env, 4, std::nullopt, 2, 1); + checkMetrics(*this, env, 4, std::nullopt, 2, 1); auto const bobSeq = env.seq(bob); // Ledger 4 gets 3, // Ledger 5 gets 4, @@ -2854,17 +2764,17 @@ public: { env(noop(bob), seq(bobSeq + i), fee(baseFee * 20), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 4 + 3 + 4 + 5, std::nullopt, 2, 1); + checkMetrics(*this, env, 4 + 3 + 4 + 5, std::nullopt, 2, 1); // Close ledger 3 env.close(); - checkMetrics(__LINE__, env, 4 + 4 + 5, 20, 3, 2); + checkMetrics(*this, env, 4 + 4 + 5, 20, 3, 2); // Close ledger 4 env.close(); - checkMetrics(__LINE__, env, 4 + 5, 30, 4, 3); + checkMetrics(*this, env, 4 + 5, 30, 4, 3); // Close ledger 5 env.close(); // Alice's first two txs expired. - checkMetrics(__LINE__, env, 2, 40, 5, 4); + checkMetrics(*this, env, 2, 40, 5, 4); // Because aliceSeq is missing, aliceSeq + 1 fails env(noop(alice), seq(aliceSeq + 1), ter(terPRE_SEQ)); @@ -2873,27 +2783,27 @@ public: env(fset(alice, asfAccountTxnID), seq(aliceSeq), ter(telCAN_NOT_QUEUE_BLOCKS)); - checkMetrics(__LINE__, env, 2, 40, 5, 4); + checkMetrics(*this, env, 2, 40, 5, 4); // However we can fill the gap with a non-blocker. env(noop(alice), seq(aliceSeq), fee(baseFee * 2), ter(terQUEUED)); - checkMetrics(__LINE__, env, 3, 40, 5, 4); + checkMetrics(*this, env, 3, 40, 5, 4); // Attempt to queue up a new aliceSeq + 1 tx that's a blocker. env(fset(alice, asfAccountTxnID), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BLOCKS)); - checkMetrics(__LINE__, env, 3, 40, 5, 4); + checkMetrics(*this, env, 3, 40, 5, 4); // Queue up a non-blocker replacement for aliceSeq + 1. env(noop(alice), seq(aliceSeq + 1), fee(baseFee * 2), ter(terQUEUED)); - checkMetrics(__LINE__, env, 4, 40, 5, 4); + checkMetrics(*this, env, 4, 40, 5, 4); // Close ledger 6 env.close(); // We expect that all of alice's queued tx's got into // the open ledger. - checkMetrics(__LINE__, env, 0, 50, 4, 5); + checkMetrics(*this, env, 0, 50, 4, 5); BEAST_EXPECT(env.seq(alice) == aliceSeq + 4); } @@ -2927,7 +2837,7 @@ public: auto const bob = Account("bob"); env.fund(XRP(500000), noripple(alice, bob)); - checkMetrics(__LINE__, env, 0, std::nullopt, 2, 1); + checkMetrics(*this, env, 0, std::nullopt, 2, 1); auto const aliceSeq = env.seq(alice); BEAST_EXPECT(env.current()->info().seq == 3); @@ -2974,7 +2884,7 @@ public: seq(aliceSeq + 19), json(R"({"LastLedgerSequence":11})"), ter(terQUEUED)); - checkMetrics(__LINE__, env, 10, std::nullopt, 2, 1); + checkMetrics(*this, env, 10, std::nullopt, 2, 1); auto const bobSeq = env.seq(bob); // Ledger 4 gets 2 from bob and 1 from alice, @@ -2984,21 +2894,21 @@ public: { env(noop(bob), seq(bobSeq + i), fee(baseFee * 20), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 10 + 2 + 4 + 5, std::nullopt, 2, 1); + checkMetrics(*this, env, 10 + 2 + 4 + 5, std::nullopt, 2, 1); // Close ledger 3 env.close(); - checkMetrics(__LINE__, env, 9 + 4 + 5, 20, 3, 2); + checkMetrics(*this, env, 9 + 4 + 5, 20, 3, 2); BEAST_EXPECT(env.seq(alice) == aliceSeq + 12); // Close ledger 4 env.close(); - checkMetrics(__LINE__, env, 9 + 5, 30, 4, 3); + checkMetrics(*this, env, 9 + 5, 30, 4, 3); BEAST_EXPECT(env.seq(alice) == aliceSeq + 12); // Close ledger 5 env.close(); // Three of Alice's txs expired. - checkMetrics(__LINE__, env, 6, 40, 5, 4); + checkMetrics(*this, env, 6, 40, 5, 4); BEAST_EXPECT(env.seq(alice) == aliceSeq + 12); // Top off Alice's queue again using Tickets so the sequence gap is @@ -3009,7 +2919,7 @@ public: env(noop(alice), ticket::use(aliceSeq + 4), ter(terQUEUED)); env(noop(alice), ticket::use(aliceSeq + 5), ter(terQUEUED)); env(noop(alice), ticket::use(aliceSeq + 6), ter(telCAN_NOT_QUEUE_FULL)); - checkMetrics(__LINE__, env, 11, 40, 5, 4); + checkMetrics(*this, env, 11, 40, 5, 4); // Even though alice's queue is full we can still slide in a couple // more transactions because she has a sequence gap. But we @@ -3040,7 +2950,7 @@ public: // Finally we can fill in the entire gap. env(noop(alice), seq(aliceSeq + 18), ter(terQUEUED)); - checkMetrics(__LINE__, env, 14, 40, 5, 4); + checkMetrics(*this, env, 14, 40, 5, 4); // Verify that nothing can be added now that the gap is filled. env(noop(alice), seq(aliceSeq + 20), ter(telCAN_NOT_QUEUE_FULL)); @@ -3049,18 +2959,18 @@ public: // but alice adds some more transaction(s) so expectedCount // may not reduce to 8. env.close(); - checkMetrics(__LINE__, env, 9, 50, 6, 5); + checkMetrics(*this, env, 9, 50, 6, 5); BEAST_EXPECT(env.seq(alice) == aliceSeq + 15); // Close ledger 7. That should remove 4 more of alice's transactions. env.close(); - checkMetrics(__LINE__, env, 2, 60, 7, 6); + checkMetrics(*this, env, 2, 60, 7, 6); BEAST_EXPECT(env.seq(alice) == aliceSeq + 19); // Close one last ledger to see all of alice's transactions moved // into the ledger, including the tickets env.close(); - checkMetrics(__LINE__, env, 0, 70, 2, 7); + checkMetrics(*this, env, 0, 70, 2, 7); BEAST_EXPECT(env.seq(alice) == aliceSeq + 21); } @@ -3079,7 +2989,7 @@ public: env.fund(XRP(100000), alice, bob); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 7, 6); + checkMetrics(*this, env, 0, std::nullopt, 7, 6); // Queue up several transactions for alice sign-and-submit auto const aliceSeq = env.seq(alice); @@ -3100,7 +3010,7 @@ public: noop(alice), fee(baseFee * 100), seq(none), ter(terQUEUED))( submitParams); } - checkMetrics(__LINE__, env, 5, std::nullopt, 7, 6); + checkMetrics(*this, env, 5, std::nullopt, 7, 6); { auto aliceStat = txQ.getAccountTxs(alice.id()); SeqProxy seq = SeqProxy::sequence(aliceSeq); @@ -3126,25 +3036,25 @@ public: // Give them a higher fee so they'll beat alice's. for (int i = 0; i < 8; ++i) envs(noop(bob), fee(baseFee * 200), seq(none), ter(terQUEUED))(); - checkMetrics(__LINE__, env, 13, std::nullopt, 7, 6); + checkMetrics(*this, env, 13, std::nullopt, 7, 6); env.close(); - checkMetrics(__LINE__, env, 5, 14, 8, 7); + checkMetrics(*this, env, 5, 14, 8, 7); // Put some more txs in the queue for bob. // Give them a higher fee so they'll beat alice's. fillQueue(env, bob); for (int i = 0; i < 9; ++i) envs(noop(bob), fee(baseFee * 200), seq(none), ter(terQUEUED))(); - checkMetrics(__LINE__, env, 14, 14, 8, 7, 25601); + checkMetrics(*this, env, 14, 14, 8, 7, 25601); env.close(); // Put some more txs in the queue for bob. // Give them a higher fee so they'll beat alice's. fillQueue(env, bob); for (int i = 0; i < 10; ++i) envs(noop(bob), fee(baseFee * 200), seq(none), ter(terQUEUED))(); - checkMetrics(__LINE__, env, 15, 16, 9, 8); + checkMetrics(*this, env, 15, 16, 9, 8); env.close(); - checkMetrics(__LINE__, env, 4, 18, 10, 9); + checkMetrics(*this, env, 4, 18, 10, 9); { // Bob has nothing left in the queue. auto bobStat = txQ.getAccountTxs(bob.id()); @@ -3172,7 +3082,7 @@ public: // Now, fill the gap. envs(noop(alice), fee(baseFee * 100), seq(none), ter(terQUEUED))( submitParams); - checkMetrics(__LINE__, env, 5, 18, 10, 9); + checkMetrics(*this, env, 5, 18, 10, 9); { auto aliceStat = txQ.getAccountTxs(alice.id()); auto seq = aliceSeq; @@ -3187,7 +3097,7 @@ public: } env.close(); - checkMetrics(__LINE__, env, 0, 20, 5, 10); + checkMetrics(*this, env, 0, 20, 5, 10); { // Bob's data has been cleaned up. auto bobStat = txQ.getAccountTxs(bob.id()); @@ -3246,10 +3156,10 @@ public: BEAST_EXPECT(!queue_data.isMember(jss::max_spend_drops_total)); BEAST_EXPECT(!queue_data.isMember(jss::transactions)); } - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3278,7 +3188,7 @@ public: submitParams); envs(noop(alice), fee(baseFee * 10), seq(none), ter(terQUEUED))( submitParams); - checkMetrics(__LINE__, env, 4, 6, 4, 3); + checkMetrics(*this, env, 4, 6, 4, 3); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3330,7 +3240,7 @@ public: // Drain the queue so we can queue up a blocker. env.close(); - checkMetrics(__LINE__, env, 0, 8, 4, 4); + checkMetrics(*this, env, 0, 8, 4, 4); // Fill the ledger and then queue up a blocker. envs(noop(alice), seq(none))(submitParams); @@ -3341,7 +3251,7 @@ public: seq(none), json(jss::LastLedgerSequence, 10), ter(terQUEUED))(submitParams); - checkMetrics(__LINE__, env, 1, 8, 5, 4); + checkMetrics(*this, env, 1, 8, 5, 4); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3405,7 +3315,7 @@ public: fee(baseFee * 10), seq(none), ter(telCAN_NOT_QUEUE_BLOCKED))(submitParams); - checkMetrics(__LINE__, env, 1, 8, 5, 4); + checkMetrics(*this, env, 1, 8, 5, 4); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3483,9 +3393,9 @@ public: } env.close(); - checkMetrics(__LINE__, env, 0, 10, 2, 5); + checkMetrics(*this, env, 0, 10, 2, 5); env.close(); - checkMetrics(__LINE__, env, 0, 10, 0, 5); + checkMetrics(*this, env, 0, 10, 0, 5); { auto const info = env.rpc("json", "account_info", withQueue); @@ -3555,10 +3465,10 @@ public: state[jss::load_factor_fee_reference] == 256); } - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); auto aliceSeq = env.seq(alice); auto submitParams = Json::Value(Json::objectValue); @@ -3568,7 +3478,7 @@ public: fee(baseFee * 10), seq(aliceSeq + i), ter(terQUEUED))(submitParams); - checkMetrics(__LINE__, env, 4, 6, 4, 3); + checkMetrics(*this, env, 4, 6, 4, 3); { auto const server_info = env.rpc("server_info"); @@ -3794,7 +3704,7 @@ public: // Fund the first few accounts at non escalated fee env.fund(XRP(50000), noripple(a, b, c, d)); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // First transaction establishes the messaging using namespace std::chrono_literals; @@ -3844,7 +3754,7 @@ public: jv[jss::load_factor_fee_reference] == 256; })); - checkMetrics(__LINE__, env, 0, 8, 0, 4); + checkMetrics(*this, env, 0, 8, 0, 4); // Fund then next few accounts at non escalated fee env.fund(XRP(50000), noripple(e, f, g, h, i)); @@ -3858,7 +3768,7 @@ public: env(noop(e), fee(baseFee), queued); env(noop(f), fee(baseFee), queued); env(noop(g), fee(baseFee), queued); - checkMetrics(__LINE__, env, 7, 8, 5, 4); + checkMetrics(*this, env, 7, 8, 5, 4); // Last transaction escalates the fee BEAST_EXPECT(wsc->findMsg(5s, [&](auto const& jv) { @@ -3928,7 +3838,7 @@ public: auto alice = Account("alice"); auto bob = Account("bob"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000000), alice, bob); fillQueue(env, alice); @@ -3982,7 +3892,7 @@ public: seq(aliceSeq++), ter(terQUEUED)); - checkMetrics(__LINE__, env, 3, std::nullopt, 4, 3); + checkMetrics(*this, env, 3, std::nullopt, 4, 3); // Figure out how much it would cost to cover all the // queued txs + itself @@ -3994,7 +3904,7 @@ public: // the edge case test. env(noop(alice), fee(totalFee), seq(aliceSeq++), ter(terQUEUED)); - checkMetrics(__LINE__, env, 4, std::nullopt, 4, 3); + checkMetrics(*this, env, 4, std::nullopt, 4, 3); // Now repeat the process including the new tx // and avoiding the rounding error @@ -4004,7 +3914,7 @@ public: // Submit a transaction with that fee. It will succeed. env(noop(alice), fee(totalFee), seq(aliceSeq++)); - checkMetrics(__LINE__, env, 0, std::nullopt, 9, 3); + checkMetrics(*this, env, 0, std::nullopt, 9, 3); } testcase("replace last tx with enough to clear queue"); @@ -4029,7 +3939,7 @@ public: seq(aliceSeq++), ter(terQUEUED)); - checkMetrics(__LINE__, env, 3, std::nullopt, 9, 3); + checkMetrics(*this, env, 3, std::nullopt, 9, 3); // Figure out how much it would cost to cover all the // queued txs + itself @@ -4041,10 +3951,10 @@ public: env(noop(alice), fee(totalFee), seq(aliceSeq++)); // The queue is clear - checkMetrics(__LINE__, env, 0, std::nullopt, 12, 3); + checkMetrics(*this, env, 0, std::nullopt, 12, 3); env.close(); - checkMetrics(__LINE__, env, 0, 24, 0, 12); + checkMetrics(*this, env, 0, 24, 0, 12); } testcase("replace middle tx with enough to clear queue"); @@ -4060,7 +3970,7 @@ public: ter(terQUEUED)); } - checkMetrics(__LINE__, env, 5, 24, 13, 12); + checkMetrics(*this, env, 5, 24, 13, 12); // Figure out how much it would cost to cover 3 txns uint64_t const totalFee = calcTotalFee(baseFee * 10 * 2, 3); @@ -4068,7 +3978,7 @@ public: aliceSeq -= 3; env(noop(alice), fee(totalFee), seq(aliceSeq++)); - checkMetrics(__LINE__, env, 2, 24, 16, 12); + checkMetrics(*this, env, 2, 24, 16, 12); auto const aliceQueue = env.app().getTxQ().getAccountTxs(alice.id()); BEAST_EXPECT(aliceQueue.size() == 2); @@ -4083,7 +3993,7 @@ public: // Close the ledger to clear the queue env.close(); - checkMetrics(__LINE__, env, 0, 32, 2, 16); + checkMetrics(*this, env, 0, 32, 2, 16); } testcase("clear queue failure (load)"); @@ -4109,7 +4019,7 @@ public: totalPaid += baseFee * 2.2; } - checkMetrics(__LINE__, env, 4, 32, 17, 16); + checkMetrics(*this, env, 4, 32, 17, 16); // Figure out how much it would cost to cover all the txns // + 1 @@ -4123,11 +4033,11 @@ public: env(noop(alice), fee(totalFee), seq(aliceSeq++), ter(terQUEUED)); // The original last transaction is still in the queue - checkMetrics(__LINE__, env, 5, 32, 17, 16); + checkMetrics(*this, env, 5, 32, 17, 16); // With high load, some of the txs stay in the queue env.close(); - checkMetrics(__LINE__, env, 3, 34, 2, 17); + checkMetrics(*this, env, 3, 34, 2, 17); // Load drops back down feeTrack.setRemoteFee(origFee); @@ -4135,14 +4045,14 @@ public: // Because of the earlier failure, alice can not clear the queue, // no matter how high the fee fillQueue(env, bob); - checkMetrics(__LINE__, env, 3, 34, 18, 17); + checkMetrics(*this, env, 3, 34, 18, 17); env(noop(alice), fee(XRP(1)), seq(aliceSeq++), ter(terQUEUED)); - checkMetrics(__LINE__, env, 4, 34, 18, 17); + checkMetrics(*this, env, 4, 34, 18, 17); // With normal load, those txs get into the ledger env.close(); - checkMetrics(__LINE__, env, 0, 36, 4, 18); + checkMetrics(*this, env, 0, 36, 4, 18); } } @@ -4164,77 +4074,77 @@ public: {"maximum_txn_per_account", "200"}})); auto alice = Account("alice"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000000), alice); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); auto seqAlice = env.seq(alice); auto txCount = 140; for (int i = 0; i < txCount; ++i) env(noop(alice), seq(seqAlice++), ter(terQUEUED)); - checkMetrics(__LINE__, env, txCount, std::nullopt, 4, 3); + checkMetrics(*this, env, txCount, std::nullopt, 4, 3); // Close a few ledgers successfully, so the limit grows env.close(); // 4 + 25% = 5 txCount -= 6; - checkMetrics(__LINE__, env, txCount, 10, 6, 5, 257); + checkMetrics(*this, env, txCount, 10, 6, 5, 257); env.close(); // 6 + 25% = 7 txCount -= 8; - checkMetrics(__LINE__, env, txCount, 14, 8, 7, 257); + checkMetrics(*this, env, txCount, 14, 8, 7, 257); env.close(); // 8 + 25% = 10 txCount -= 11; - checkMetrics(__LINE__, env, txCount, 20, 11, 10, 257); + checkMetrics(*this, env, txCount, 20, 11, 10, 257); env.close(); // 11 + 25% = 13 txCount -= 14; - checkMetrics(__LINE__, env, txCount, 26, 14, 13, 257); + checkMetrics(*this, env, txCount, 26, 14, 13, 257); env.close(); // 14 + 25% = 17 txCount -= 18; - checkMetrics(__LINE__, env, txCount, 34, 18, 17, 257); + checkMetrics(*this, env, txCount, 34, 18, 17, 257); env.close(); // 18 + 25% = 22 txCount -= 23; - checkMetrics(__LINE__, env, txCount, 44, 23, 22, 257); + checkMetrics(*this, env, txCount, 44, 23, 22, 257); env.close(); // 23 + 25% = 28 txCount -= 29; - checkMetrics(__LINE__, env, txCount, 56, 29, 28); + checkMetrics(*this, env, txCount, 56, 29, 28); // From 3 expected to 28 in 7 "fast" ledgers. // Close the ledger with a delay. env.close(env.now() + 5s, 10000ms); txCount -= 15; - checkMetrics(__LINE__, env, txCount, 56, 15, 14); + checkMetrics(*this, env, txCount, 56, 15, 14); // Close the ledger with a delay. env.close(env.now() + 5s, 10000ms); txCount -= 8; - checkMetrics(__LINE__, env, txCount, 56, 8, 7); + checkMetrics(*this, env, txCount, 56, 8, 7); // Close the ledger with a delay. env.close(env.now() + 5s, 10000ms); txCount -= 4; - checkMetrics(__LINE__, env, txCount, 56, 4, 3); + checkMetrics(*this, env, txCount, 56, 4, 3); // From 28 expected back down to 3 in 3 "slow" ledgers. // Confirm the minimum sticks env.close(env.now() + 5s, 10000ms); txCount -= 4; - checkMetrics(__LINE__, env, txCount, 56, 4, 3); + checkMetrics(*this, env, txCount, 56, 4, 3); BEAST_EXPECT(!txCount); } @@ -4250,35 +4160,35 @@ public: {"maximum_txn_per_account", "200"}})); auto alice = Account("alice"); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); env.fund(XRP(50000000), alice); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); auto seqAlice = env.seq(alice); auto txCount = 43; for (int i = 0; i < txCount; ++i) env(noop(alice), seq(seqAlice++), ter(terQUEUED)); - checkMetrics(__LINE__, env, txCount, std::nullopt, 4, 3); + checkMetrics(*this, env, txCount, std::nullopt, 4, 3); // Close a few ledgers successfully, so the limit grows env.close(); // 4 + 150% = 10 txCount -= 11; - checkMetrics(__LINE__, env, txCount, 20, 11, 10, 257); + checkMetrics(*this, env, txCount, 20, 11, 10, 257); env.close(); // 11 + 150% = 27 txCount -= 28; - checkMetrics(__LINE__, env, txCount, 54, 28, 27); + checkMetrics(*this, env, txCount, 54, 28, 27); // From 3 expected to 28 in 7 "fast" ledgers. // Close the ledger with a delay. env.close(env.now() + 5s, 10000ms); txCount -= 4; - checkMetrics(__LINE__, env, txCount, 54, 4, 3); + checkMetrics(*this, env, txCount, 54, 4, 3); // From 28 expected back down to 3 in 3 "slow" ledgers. @@ -4306,19 +4216,19 @@ public: auto const queued = ter(terQUEUED); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Create account env.fund(XRP(50000), noripple(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 1, 3); + checkMetrics(*this, env, 0, std::nullopt, 1, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Queue a transaction auto const aliceSeq = env.seq(alice); env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 3); + checkMetrics(*this, env, 1, std::nullopt, 4, 3); // Now, apply a (different) transaction directly // to the open ledger, bypassing the queue @@ -4334,23 +4244,23 @@ public: return result.applied; }); // the queued transaction is still there - checkMetrics(__LINE__, env, 1, std::nullopt, 5, 3); + checkMetrics(*this, env, 1, std::nullopt, 5, 3); // The next transaction should be able to go into the open // ledger, even though aliceSeq is queued. In earlier incarnations // of the TxQ this would cause an assert. env(noop(alice), seq(aliceSeq + 1), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 6, 3); + checkMetrics(*this, env, 1, std::nullopt, 6, 3); // Now queue a couple more transactions to make sure // they succeed despite aliceSeq being queued env(noop(alice), seq(aliceSeq + 2), queued); env(noop(alice), seq(aliceSeq + 3), queued); - checkMetrics(__LINE__, env, 3, std::nullopt, 6, 3); + checkMetrics(*this, env, 3, std::nullopt, 6, 3); // Now close the ledger. One of the queued transactions // (aliceSeq) should be dropped. env.close(); - checkMetrics(__LINE__, env, 0, 12, 2, 6); + checkMetrics(*this, env, 0, 12, 2, 6); } void @@ -4371,11 +4281,11 @@ public: auto queued = ter(terQUEUED); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // Create account env.fund(XRP(50000), noripple(alice)); - checkMetrics(__LINE__, env, 0, std::nullopt, 1, 3); + checkMetrics(*this, env, 0, std::nullopt, 1, 3); // Create tickets std::uint32_t const tktSeq0{env.seq(alice) + 1}; @@ -4383,12 +4293,12 @@ public: // Fill the queue so the next transaction will be queued. fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, std::nullopt, 4, 3); + checkMetrics(*this, env, 0, std::nullopt, 4, 3); // Queue a transaction with a ticket. Leave an unused ticket // on either side. env(noop(alice), ticket::use(tktSeq0 + 1), queued); - checkMetrics(__LINE__, env, 1, std::nullopt, 4, 3); + checkMetrics(*this, env, 1, std::nullopt, 4, 3); // Now, apply a (different) transaction directly // to the open ledger, bypassing the queue @@ -4406,25 +4316,25 @@ public: return result.applied; }); // the queued transaction is still there - checkMetrics(__LINE__, env, 1, std::nullopt, 5, 3); + checkMetrics(*this, env, 1, std::nullopt, 5, 3); // The next (sequence-based) transaction should be able to go into // the open ledger, even though tktSeq0 is queued. Note that this // sequence-based transaction goes in front of the queued // transaction, so the queued transaction is left in the queue. env(noop(alice), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 6, 3); + checkMetrics(*this, env, 1, std::nullopt, 6, 3); // We should be able to do the same thing with a ticket that goes // if front of the queued transaction. This one too will leave // the queued transaction in place. env(noop(alice), ticket::use(tktSeq0 + 0), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 1, std::nullopt, 7, 3); + checkMetrics(*this, env, 1, std::nullopt, 7, 3); // We have one ticketed transaction in the queue. We should able // to add another to the queue. env(noop(alice), ticket::use(tktSeq0 + 2), queued); - checkMetrics(__LINE__, env, 2, std::nullopt, 7, 3); + checkMetrics(*this, env, 2, std::nullopt, 7, 3); // Here we try to force the queued transactions into the ledger by // adding one more queued (ticketed) transaction that pays enough @@ -4440,12 +4350,12 @@ public: // transaction is equally capable of going into the ledger independent // of all other ticket- or sequence-based transactions. env(noop(alice), ticket::use(tktSeq0 + 3), fee(XRP(10))); - checkMetrics(__LINE__, env, 2, std::nullopt, 8, 3); + checkMetrics(*this, env, 2, std::nullopt, 8, 3); // Now close the ledger. One of the queued transactions // (the one with tktSeq0 + 1) should be dropped. env.close(); - checkMetrics(__LINE__, env, 0, 16, 1, 8); + checkMetrics(*this, env, 0, 16, 1, 8); } void @@ -4496,7 +4406,7 @@ public: env.close(); env.fund(XRP(10000), fiona); env.close(); - checkMetrics(__LINE__, env, 0, 10, 0, 2); + checkMetrics(*this, env, 0, 10, 0, 2); // Close ledgers until the amendments show up. int i = 0; @@ -4508,7 +4418,7 @@ public: } auto expectedPerLedger = ripple::detail::numUpVotedAmendments() + 1; checkMetrics( - __LINE__, env, 0, 5 * expectedPerLedger, 0, expectedPerLedger); + *this, env, 0, 5 * expectedPerLedger, 0, expectedPerLedger); // Now wait 2 weeks modulo 256 ledgers for the amendments to be // enabled. Speed the process by closing ledgers every 80 minutes, @@ -4524,7 +4434,7 @@ public: // We're very close to the flag ledger. Fill the ledger. fillQueue(env, alice); checkMetrics( - __LINE__, + *this, env, 0, 5 * expectedPerLedger, @@ -4575,7 +4485,7 @@ public: } std::size_t expectedInQueue = 60; checkMetrics( - __LINE__, + *this, env, expectedInQueue, 5 * expectedPerLedger, @@ -4602,7 +4512,7 @@ public: expectedInLedger -= expectedInQueue; ++expectedPerLedger; checkMetrics( - __LINE__, + *this, env, expectedInQueue, 5 * expectedPerLedger, @@ -4689,7 +4599,7 @@ public: // of their transactions expire out of the queue. To start out // alice fills the ledger. fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 50, 7, 6); + checkMetrics(*this, env, 0, 50, 7, 6); // Now put a few transactions into alice's queue, including one that // will expire out soon. @@ -4735,9 +4645,9 @@ public: env(noop(fiona), seq(seqFiona++), fee(--feeDrops), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 34, 50, 7, 6); + checkMetrics(*this, env, 34, 50, 7, 6); env.close(); - checkMetrics(__LINE__, env, 26, 50, 8, 7); + checkMetrics(*this, env, 26, 50, 8, 7); // Re-fill the queue so alice and bob stay stuck. feeDrops = medFee; @@ -4748,9 +4658,9 @@ public: env(noop(ellie), seq(seqEllie++), fee(--feeDrops), ter(terQUEUED)); env(noop(fiona), seq(seqFiona++), fee(--feeDrops), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 38, 50, 8, 7); + checkMetrics(*this, env, 38, 50, 8, 7); env.close(); - checkMetrics(__LINE__, env, 29, 50, 9, 8); + checkMetrics(*this, env, 29, 50, 9, 8); // One more time... feeDrops = medFee; @@ -4761,9 +4671,9 @@ public: env(noop(ellie), seq(seqEllie++), fee(--feeDrops), ter(terQUEUED)); env(noop(fiona), seq(seqFiona++), fee(--feeDrops), ter(terQUEUED)); } - checkMetrics(__LINE__, env, 41, 50, 9, 8); + checkMetrics(*this, env, 41, 50, 9, 8); env.close(); - checkMetrics(__LINE__, env, 29, 50, 10, 9); + checkMetrics(*this, env, 29, 50, 10, 9); // Finally the stage is set. alice's and bob's transactions expired // out of the queue which caused the dropPenalty flag to be set on @@ -4785,7 +4695,7 @@ public: env(noop(carol), seq(seqCarol++), fee(--feeDrops), ter(terQUEUED)); env(noop(daria), seq(seqDaria++), fee(--feeDrops), ter(terQUEUED)); env(noop(ellie), seq(seqEllie++), fee(--feeDrops), ter(terQUEUED)); - checkMetrics(__LINE__, env, 48, 50, 10, 9); + checkMetrics(*this, env, 48, 50, 10, 9); // Now induce a fee jump which should cause all the transactions // in the queue to fail with telINSUF_FEE_P. @@ -4802,7 +4712,7 @@ public: // o The _last_ transaction should be dropped from alice's queue. // o The first failing transaction should be dropped from bob's queue. env.close(); - checkMetrics(__LINE__, env, 46, 50, 0, 10); + checkMetrics(*this, env, 46, 50, 0, 10); // Run the local fee back down. while (env.app().getFeeTrack().lowerLocalFee()) @@ -4810,7 +4720,7 @@ public: // bob fills the ledger so it's easier to probe the TxQ. fillQueue(env, bob); - checkMetrics(__LINE__, env, 46, 50, 11, 10); + checkMetrics(*this, env, 46, 50, 11, 10); // Before the close() alice had two transactions in her queue. // We now expect her to have one. Here's the state of alice's queue. @@ -4928,7 +4838,7 @@ public: env.close(); - checkMetrics(__LINE__, env, 0, 50, 4, 6); + checkMetrics(*this, env, 0, 50, 4, 6); } { @@ -4989,7 +4899,7 @@ public: // The ticket transactions that didn't succeed or get queued succeed // this time because the tickets got consumed when the offers came // out of the queue - checkMetrics(__LINE__, env, 0, 50, 8, 7); + checkMetrics(*this, env, 0, 50, 8, 7); } } @@ -5010,7 +4920,7 @@ public: {"account_reserve", "0"}, {"owner_reserve", "0"}})); - checkMetrics(__LINE__, env, 0, std::nullopt, 0, 3); + checkMetrics(*this, env, 0, std::nullopt, 0, 3); // ledgers in queue is 2 because of makeConfig auto const initQueueMax = initFee(env, 3, 2, 0, 0, 0); @@ -5056,34 +4966,34 @@ public: } } - checkMetrics(__LINE__, env, 0, initQueueMax, 0, 3); + checkMetrics(*this, env, 0, initQueueMax, 0, 3); // The noripple is to reduce the number of transactions required to // fund the accounts. There is no rippling in this test. env.fund(XRP(100000), noripple(alice)); - checkMetrics(__LINE__, env, 0, initQueueMax, 1, 3); + checkMetrics(*this, env, 0, initQueueMax, 1, 3); env.close(); - checkMetrics(__LINE__, env, 0, 6, 0, 3); + checkMetrics(*this, env, 0, 6, 0, 3); fillQueue(env, alice); - checkMetrics(__LINE__, env, 0, 6, 4, 3); + checkMetrics(*this, env, 0, 6, 4, 3); env(noop(alice), fee(openLedgerCost(env))); - checkMetrics(__LINE__, env, 0, 6, 5, 3); + checkMetrics(*this, env, 0, 6, 5, 3); auto aliceSeq = env.seq(alice); env(noop(alice), queued); - checkMetrics(__LINE__, env, 1, 6, 5, 3); + checkMetrics(*this, env, 1, 6, 5, 3); env(noop(alice), seq(aliceSeq + 1), fee(10), queued); - checkMetrics(__LINE__, env, 2, 6, 5, 3); + checkMetrics(*this, env, 2, 6, 5, 3); { auto const fee = env.rpc("fee"); @@ -5126,7 +5036,7 @@ public: env.close(); - checkMetrics(__LINE__, env, 0, 10, 2, 5); + checkMetrics(*this, env, 0, 10, 2, 5); } void diff --git a/src/test/jtx.h b/src/test/jtx.h index 6b73ca63ec..2e4764a403 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -31,8 +31,10 @@ #include #include #include +#include #include #include +#include #include #include #include diff --git a/src/test/jtx/SignerUtils.h b/src/test/jtx/SignerUtils.h new file mode 100644 index 0000000000..7b1ae5007c --- /dev/null +++ b/src/test/jtx/SignerUtils.h @@ -0,0 +1,56 @@ +#ifndef RIPPLE_TEST_JTX_SIGNERUTILS_H_INCLUDED +#define RIPPLE_TEST_JTX_SIGNERUTILS_H_INCLUDED + +#include + +#include + +namespace ripple { +namespace test { +namespace jtx { + +struct Reg +{ + Account acct; + Account sig; + + Reg(Account const& masterSig) : acct(masterSig), sig(masterSig) + { + } + + Reg(Account const& acct_, Account const& regularSig) + : acct(acct_), sig(regularSig) + { + } + + Reg(char const* masterSig) : acct(masterSig), sig(masterSig) + { + } + + Reg(char const* acct_, char const* regularSig) + : acct(acct_), sig(regularSig) + { + } + + bool + operator<(Reg const& rhs) const + { + return acct < rhs.acct; + } +}; + +// Utility function to sort signers +inline void +sortSigners(std::vector& signers) +{ + std::sort( + signers.begin(), signers.end(), [](Reg const& lhs, Reg const& rhs) { + return lhs.acct < rhs.acct; + }); +} + +} // namespace jtx +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 534419494d..ae46ea4fe3 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -31,6 +32,14 @@ #include +#if (defined(__clang_major__) && __clang_major__ < 15) +#include +using source_location = std::experimental::source_location; +#else +#include +using std::source_location; +#endif + namespace ripple { namespace test { namespace jtx { @@ -445,7 +454,6 @@ create(A const& account, A const& dest, STAmount const& sendMax) jv[sfSendMax.jsonName] = sendMax.getJson(JsonOptions::none); jv[sfDestination.jsonName] = to_string(dest); jv[sfTransactionType.jsonName] = jss::CheckCreate; - jv[sfFlags.jsonName] = tfUniversal; return jv; } // clang-format on @@ -461,6 +469,102 @@ create( } // namespace check +static constexpr FeeLevel64 baseFeeLevel{256}; +static constexpr FeeLevel64 minEscalationFeeLevel = baseFeeLevel * 500; + +template +void +checkMetrics( + Suite& test, + jtx::Env& env, + std::size_t expectedCount, + std::optional expectedMaxCount, + std::size_t expectedInLedger, + std::size_t expectedPerLedger, + std::uint64_t expectedMinFeeLevel = baseFeeLevel.fee(), + std::uint64_t expectedMedFeeLevel = minEscalationFeeLevel.fee(), + source_location const location = source_location::current()) +{ + int line = location.line(); + char const* file = location.file_name(); + FeeLevel64 const expectedMin{expectedMinFeeLevel}; + FeeLevel64 const expectedMed{expectedMedFeeLevel}; + auto const metrics = env.app().getTxQ().getMetrics(*env.current()); + using namespace std::string_literals; + + metrics.referenceFeeLevel == baseFeeLevel + ? test.pass() + : test.fail( + "reference: "s + + std::to_string(metrics.referenceFeeLevel.value()) + "/" + + std::to_string(baseFeeLevel.value()), + file, + line); + + metrics.txCount == expectedCount + ? test.pass() + : test.fail( + "txCount: "s + std::to_string(metrics.txCount) + "/" + + std::to_string(expectedCount), + file, + line); + + metrics.txQMaxSize == expectedMaxCount + ? test.pass() + : test.fail( + "txQMaxSize: "s + std::to_string(metrics.txQMaxSize.value_or(0)) + + "/" + std::to_string(expectedMaxCount.value_or(0)), + file, + line); + + metrics.txInLedger == expectedInLedger + ? test.pass() + : test.fail( + "txInLedger: "s + std::to_string(metrics.txInLedger) + "/" + + std::to_string(expectedInLedger), + file, + line); + + metrics.txPerLedger == expectedPerLedger + ? test.pass() + : test.fail( + "txPerLedger: "s + std::to_string(metrics.txPerLedger) + "/" + + std::to_string(expectedPerLedger), + file, + line); + + metrics.minProcessingFeeLevel == expectedMin + ? test.pass() + : test.fail( + "minProcessingFeeLevel: "s + + std::to_string(metrics.minProcessingFeeLevel.value()) + "/" + + std::to_string(expectedMin.value()), + file, + line); + + metrics.medFeeLevel == expectedMed + ? test.pass() + : test.fail( + "medFeeLevel: "s + std::to_string(metrics.medFeeLevel.value()) + + "/" + std::to_string(expectedMed.value()), + file, + line); + + auto const expectedCurFeeLevel = expectedInLedger > expectedPerLedger + ? expectedMed * expectedInLedger * expectedInLedger / + (expectedPerLedger * expectedPerLedger) + : metrics.referenceFeeLevel; + + metrics.openLedgerFeeLevel == expectedCurFeeLevel + ? test.pass() + : test.fail( + "openLedgerFeeLevel: "s + + std::to_string(metrics.openLedgerFeeLevel.value()) + "/" + + std::to_string(expectedCurFeeLevel.value()), + file, + line); +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/acctdelete.h b/src/test/jtx/acctdelete.h index 98a23c6de2..21d00cb727 100644 --- a/src/test/jtx/acctdelete.h +++ b/src/test/jtx/acctdelete.h @@ -23,6 +23,8 @@ #include #include +#include + namespace ripple { namespace test { namespace jtx { @@ -31,6 +33,15 @@ namespace jtx { Json::Value acctdelete(Account const& account, Account const& dest); +// Close the ledger until the ledger sequence is large enough to close +// the account. If margin is specified, close the ledger so `margin` +// more closes are needed +void +incLgrSeqForAccDel( + jtx::Env& env, + jtx::Account const& acc, + std::uint32_t margin = 0); + } // namespace jtx } // namespace test diff --git a/src/test/jtx/batch.h b/src/test/jtx/batch.h new file mode 100644 index 0000000000..ab235c293f --- /dev/null +++ b/src/test/jtx/batch.h @@ -0,0 +1,169 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TEST_JTX_BATCH_H_INCLUDED +#define RIPPLE_TEST_JTX_BATCH_H_INCLUDED + +#include +#include +#include +#include +#include + +#include + +#include "test/jtx/SignerUtils.h" + +#include +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +/** Batch operations */ +namespace batch { + +/** Calculate Batch Fee. */ +XRPAmount +calcBatchFee( + jtx::Env const& env, + uint32_t const& numSigners, + uint32_t const& txns = 0); + +/** Batch. */ +Json::Value +outer( + jtx::Account const& account, + uint32_t seq, + STAmount const& fee, + std::uint32_t flags); + +/** Adds a new Batch Txn on a JTx and autofills. */ +class inner +{ +private: + Json::Value txn_; + std::uint32_t seq_; + std::optional ticket_; + +public: + inner( + Json::Value const& txn, + std::uint32_t const& sequence, + std::optional const& ticket = std::nullopt, + std::optional const& fee = std::nullopt) + : txn_(txn), seq_(sequence), ticket_(ticket) + { + txn_[jss::SigningPubKey] = ""; + txn_[jss::Sequence] = seq_; + txn_[jss::Fee] = "0"; + txn_[jss::Flags] = txn_[jss::Flags].asUInt() | tfInnerBatchTxn; + + // Optionally set ticket sequence + if (ticket_.has_value()) + { + txn_[jss::Sequence] = 0; + txn_[sfTicketSequence.jsonName] = *ticket_; + } + } + + void + operator()(Env&, JTx& jtx) const; + + Json::Value& + operator[](Json::StaticString const& key) + { + return txn_[key]; + } + + void + removeMember(Json::StaticString const& key) + { + txn_.removeMember(key); + } + + Json::Value const& + getTxn() const + { + return txn_; + } +}; + +/** Set a batch signature on a JTx. */ +class sig +{ +public: + std::vector signers; + + sig(std::vector signers_) : signers(std::move(signers_)) + { + sortSigners(signers); + } + + template + requires std::convertible_to + explicit sig(AccountType&& a0, Accounts&&... aN) + : signers{std::forward(a0), std::forward(aN)...} + { + sortSigners(signers); + } + + void + operator()(Env&, JTx& jt) const; +}; + +/** Set a batch nested multi-signature on a JTx. */ +class msig +{ +public: + Account master; + std::vector signers; + + msig(Account const& masterAccount, std::vector signers_) + : master(masterAccount), signers(std::move(signers_)) + { + sortSigners(signers); + } + + template + requires std::convertible_to + explicit msig( + Account const& masterAccount, + AccountType&& a0, + Accounts&&... aN) + : master(masterAccount) + , signers{std::forward(a0), std::forward(aN)...} + { + sortSigners(signers); + } + + void + operator()(Env&, JTx& jt) const; +}; + +} // namespace batch + +} // namespace jtx + +} // namespace test +} // namespace ripple + +#endif diff --git a/src/test/jtx/impl/AMM.cpp b/src/test/jtx/impl/AMM.cpp index 3482e7e867..6345253584 100644 --- a/src/test/jtx/impl/AMM.cpp +++ b/src/test/jtx/impl/AMM.cpp @@ -821,7 +821,6 @@ pay(Account const& account, AccountID const& to, STAmount const& amount) jv[jss::Amount] = amount.getJson(JsonOptions::none); jv[jss::Destination] = to_string(to); jv[jss::TransactionType] = jss::Payment; - jv[jss::Flags] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index e5b136e9c0..cb8141b9f3 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -219,7 +219,6 @@ escrow(AccountID const& account, AccountID const& to, STAmount const& amount) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv[jss::Destination] = to_string(to); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -231,7 +230,6 @@ finish(AccountID const& account, AccountID const& from, std::uint32_t seq) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowFinish; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv[sfOwner.jsonName] = to_string(from); jv[sfOfferSequence.jsonName] = seq; @@ -243,7 +241,6 @@ cancel(AccountID const& account, Account const& from, std::uint32_t seq) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCancel; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv[sfOwner.jsonName] = from.human(); jv[sfOfferSequence.jsonName] = seq; @@ -264,7 +261,6 @@ create( { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv[jss::Destination] = to_string(to); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -286,7 +282,6 @@ fund( { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelFund; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv[sfChannel.fieldName] = to_string(channel); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -306,7 +301,6 @@ claim( { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelClaim; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = to_string(account); jv["Channel"] = to_string(channel); if (amount) diff --git a/src/test/jtx/impl/acctdelete.cpp b/src/test/jtx/impl/acctdelete.cpp index 842eea7fc2..acce912d46 100644 --- a/src/test/jtx/impl/acctdelete.cpp +++ b/src/test/jtx/impl/acctdelete.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include @@ -36,6 +37,28 @@ acctdelete(jtx::Account const& account, jtx::Account const& dest) return jv; } +// Close the ledger until the ledger sequence is large enough to close +// the account. If margin is specified, close the ledger so `margin` +// more closes are needed +void +incLgrSeqForAccDel(jtx::Env& env, jtx::Account const& acc, std::uint32_t margin) +{ + using namespace jtx; + auto openLedgerSeq = [](jtx::Env& env) -> std::uint32_t { + return env.current()->seq(); + }; + + int const delta = [&]() -> int { + if (env.seq(acc) + 255 > openLedgerSeq(env)) + return env.seq(acc) - openLedgerSeq(env) + 255 - margin; + return 0; + }(); + env.test.BEAST_EXPECT(margin == 0 || delta >= 0); + for (int i = 0; i < delta; ++i) + env.close(); + env.test.BEAST_EXPECT(openLedgerSeq(env) == env.seq(acc) + 255 - margin); +} + } // namespace jtx } // namespace test } // namespace ripple diff --git a/src/test/jtx/impl/batch.cpp b/src/test/jtx/impl/batch.cpp new file mode 100644 index 0000000000..055ed3fb55 --- /dev/null +++ b/src/test/jtx/impl/batch.cpp @@ -0,0 +1,154 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace ripple { +namespace test { +namespace jtx { + +namespace batch { + +XRPAmount +calcBatchFee( + test::jtx::Env const& env, + uint32_t const& numSigners, + uint32_t const& txns) +{ + XRPAmount const feeDrops = env.current()->fees().base; + return ((numSigners + 2) * feeDrops) + feeDrops * txns; +} + +// Batch. +Json::Value +outer( + jtx::Account const& account, + uint32_t seq, + STAmount const& fee, + std::uint32_t flags) +{ + Json::Value jv; + jv[jss::TransactionType] = jss::Batch; + jv[jss::Account] = account.human(); + jv[jss::RawTransactions] = Json::Value{Json::arrayValue}; + jv[jss::Sequence] = seq; + jv[jss::Flags] = flags; + jv[jss::Fee] = to_string(fee); + return jv; +} + +void +inner::operator()(Env& env, JTx& jt) const +{ + auto const index = jt.jv[jss::RawTransactions].size(); + Json::Value& batchTransaction = jt.jv[jss::RawTransactions][index]; + + // Initialize the batch transaction + batchTransaction = Json::Value{}; + batchTransaction[jss::RawTransaction] = txn_; +} + +void +sig::operator()(Env& env, JTx& jt) const +{ + auto const mySigners = signers; + std::optional st; + try + { + // required to cast the STObject to STTx + jt.jv[jss::SigningPubKey] = ""; + st = parse(jt.jv); + } + catch (parse_error const&) + { + env.test.log << pretty(jt.jv) << std::endl; + Rethrow(); + } + STTx const& stx = STTx{std::move(*st)}; + auto& js = jt[sfBatchSigners.getJsonName()]; + for (std::size_t i = 0; i < mySigners.size(); ++i) + { + auto const& e = mySigners[i]; + auto& jo = js[i][sfBatchSigner.getJsonName()]; + jo[jss::Account] = e.acct.human(); + jo[jss::SigningPubKey] = strHex(e.sig.pk().slice()); + + Serializer msg; + serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs()); + auto const sig = ripple::sign( + *publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice()); + jo[sfTxnSignature.getJsonName()] = + strHex(Slice{sig.data(), sig.size()}); + } +} + +void +msig::operator()(Env& env, JTx& jt) const +{ + auto const mySigners = signers; + std::optional st; + try + { + // required to cast the STObject to STTx + jt.jv[jss::SigningPubKey] = ""; + st = parse(jt.jv); + } + catch (parse_error const&) + { + env.test.log << pretty(jt.jv) << std::endl; + Rethrow(); + } + STTx const& stx = STTx{std::move(*st)}; + auto& bs = jt[sfBatchSigners.getJsonName()]; + auto const index = jt[sfBatchSigners.jsonName].size(); + auto& bso = bs[index][sfBatchSigner.getJsonName()]; + bso[jss::Account] = master.human(); + bso[jss::SigningPubKey] = ""; + auto& is = bso[sfSigners.getJsonName()]; + for (std::size_t i = 0; i < mySigners.size(); ++i) + { + auto const& e = mySigners[i]; + auto& iso = is[i][sfSigner.getJsonName()]; + iso[jss::Account] = e.acct.human(); + iso[jss::SigningPubKey] = strHex(e.sig.pk().slice()); + + Serializer msg; + serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs()); + finishMultiSigningData(e.acct.id(), msg); + auto const sig = ripple::sign( + *publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice()); + iso[sfTxnSignature.getJsonName()] = + strHex(Slice{sig.data(), sig.size()}); + } +} + +} // namespace batch + +} // namespace jtx +} // namespace test +} // namespace ripple diff --git a/src/test/jtx/impl/check.cpp b/src/test/jtx/impl/check.cpp index f5aa76658c..831bc900e7 100644 --- a/src/test/jtx/impl/check.cpp +++ b/src/test/jtx/impl/check.cpp @@ -37,7 +37,6 @@ cash(jtx::Account const& dest, uint256 const& checkId, STAmount const& amount) jv[sfAmount.jsonName] = amount.getJson(JsonOptions::none); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCash; - jv[sfFlags.jsonName] = tfUniversal; return jv; } @@ -53,7 +52,6 @@ cash( jv[sfDeliverMin.jsonName] = atLeast.value.getJson(JsonOptions::none); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCash; - jv[sfFlags.jsonName] = tfUniversal; return jv; } @@ -65,7 +63,6 @@ cancel(jtx::Account const& dest, uint256 const& checkId) jv[sfAccount.jsonName] = dest.human(); jv[sfCheckID.jsonName] = to_string(checkId); jv[sfTransactionType.jsonName] = jss::CheckCancel; - jv[sfFlags.jsonName] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/creds.cpp b/src/test/jtx/impl/creds.cpp index f29bc45e20..eae3b9501b 100644 --- a/src/test/jtx/impl/creds.cpp +++ b/src/test/jtx/impl/creds.cpp @@ -39,8 +39,6 @@ create( jv[jss::Account] = issuer.human(); jv[jss::Subject] = subject.human(); - - jv[jss::Flags] = tfUniversal; jv[sfCredentialType.jsonName] = strHex(credType); return jv; @@ -57,8 +55,6 @@ accept( jv[jss::Account] = subject.human(); jv[jss::Issuer] = issuer.human(); jv[sfCredentialType.jsonName] = strHex(credType); - jv[jss::Flags] = tfUniversal; - return jv; } @@ -75,7 +71,6 @@ deleteCred( jv[jss::Subject] = subject.human(); jv[jss::Issuer] = issuer.human(); jv[sfCredentialType.jsonName] = strHex(credType); - jv[jss::Flags] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/dids.cpp b/src/test/jtx/impl/dids.cpp index 67a523403c..1b443a5d9d 100644 --- a/src/test/jtx/impl/dids.cpp +++ b/src/test/jtx/impl/dids.cpp @@ -35,7 +35,6 @@ set(jtx::Account const& account) Json::Value jv; jv[jss::TransactionType] = jss::DIDSet; jv[jss::Account] = to_string(account.id()); - jv[jss::Flags] = tfUniversal; return jv; } @@ -45,7 +44,6 @@ setValid(jtx::Account const& account) Json::Value jv; jv[jss::TransactionType] = jss::DIDSet; jv[jss::Account] = to_string(account.id()); - jv[jss::Flags] = tfUniversal; jv[sfURI.jsonName] = strHex(std::string{"uri"}); return jv; } @@ -56,7 +54,6 @@ del(jtx::Account const& account) Json::Value jv; jv[jss::TransactionType] = jss::DIDDelete; jv[jss::Account] = to_string(account.id()); - jv[jss::Flags] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/ledgerStateFixes.cpp b/src/test/jtx/impl/ledgerStateFixes.cpp index 8c78069191..b7df78dd11 100644 --- a/src/test/jtx/impl/ledgerStateFixes.cpp +++ b/src/test/jtx/impl/ledgerStateFixes.cpp @@ -39,7 +39,6 @@ nftPageLinks(jtx::Account const& acct, jtx::Account const& owner) jv[sfLedgerFixType.jsonName] = LedgerStateFix::nfTokenPageLink; jv[sfOwner.jsonName] = owner.human(); jv[sfTransactionType.jsonName] = jss::LedgerStateFix; - jv[sfFlags.jsonName] = tfUniversal; return jv; } diff --git a/src/test/jtx/impl/multisign.cpp b/src/test/jtx/impl/multisign.cpp index a802528247..6ed6df6804 100644 --- a/src/test/jtx/impl/multisign.cpp +++ b/src/test/jtx/impl/multisign.cpp @@ -65,17 +65,6 @@ signers(Account const& account, none_t) //------------------------------------------------------------------------------ -msig::msig(std::vector signers_) : signers(std::move(signers_)) -{ - // Signatures must be applied in sorted order. - std::sort( - signers.begin(), - signers.end(), - [](msig::Reg const& lhs, msig::Reg const& rhs) { - return lhs.acct.id() < rhs.acct.id(); - }); -} - void msig::operator()(Env& env, JTx& jt) const { diff --git a/src/test/jtx/impl/pay.cpp b/src/test/jtx/impl/pay.cpp index 82fe910e9b..d1d994059e 100644 --- a/src/test/jtx/impl/pay.cpp +++ b/src/test/jtx/impl/pay.cpp @@ -35,7 +35,7 @@ pay(AccountID const& account, AccountID const& to, AnyAmount amount) jv[jss::Amount] = amount.value.getJson(JsonOptions::none); jv[jss::Destination] = to_string(to); jv[jss::TransactionType] = jss::Payment; - jv[jss::Flags] = tfUniversal; + jv[jss::Flags] = tfFullyCanonicalSig; return jv; } Json::Value diff --git a/src/test/jtx/impl/txflags.cpp b/src/test/jtx/impl/txflags.cpp index 77c46f35b3..12c9cfeb83 100644 --- a/src/test/jtx/impl/txflags.cpp +++ b/src/test/jtx/impl/txflags.cpp @@ -28,7 +28,7 @@ namespace jtx { void txflags::operator()(Env&, JTx& jt) const { - jt[jss::Flags] = v_ /*| tfUniversal*/; + jt[jss::Flags] = v_ /*| tfFullyCanonicalSig*/; } } // namespace jtx diff --git a/src/test/jtx/impl/xchain_bridge.cpp b/src/test/jtx/impl/xchain_bridge.cpp index c63734ee8f..86e9deda7c 100644 --- a/src/test/jtx/impl/xchain_bridge.cpp +++ b/src/test/jtx/impl/xchain_bridge.cpp @@ -84,7 +84,6 @@ bridge_create( minAccountCreate->getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainCreateBridge; - jv[jss::Flags] = tfUniversal; return jv; } @@ -107,7 +106,6 @@ bridge_modify( minAccountCreate->getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainModifyBridge; - jv[jss::Flags] = tfUniversal; return jv; } @@ -126,7 +124,6 @@ xchain_create_claim_id( jv[sfOtherChainSource.getJsonName()] = otherChainSource.human(); jv[jss::TransactionType] = jss::XChainCreateClaimID; - jv[jss::Flags] = tfUniversal; return jv; } @@ -148,7 +145,6 @@ xchain_commit( jv[sfOtherChainDestination.getJsonName()] = dst->human(); jv[jss::TransactionType] = jss::XChainCommit; - jv[jss::Flags] = tfUniversal; return jv; } @@ -169,7 +165,6 @@ xchain_claim( jv[sfAmount.getJsonName()] = amt.value.getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainClaim; - jv[jss::Flags] = tfUniversal; return jv; } @@ -191,7 +186,6 @@ sidechain_xchain_account_create( reward.value.getJson(JsonOptions::none); jv[jss::TransactionType] = jss::XChainAccountCreateCommit; - jv[jss::Flags] = tfUniversal; return jv; } @@ -242,7 +236,6 @@ claim_attestation( result[sfDestination.getJsonName()] = toBase58(*dst); result[jss::TransactionType] = jss::XChainAddClaimAttestation; - result[jss::Flags] = tfUniversal; return result; } @@ -297,7 +290,6 @@ create_account_attestation( rewardAmount.value.getJson(JsonOptions::none); result[jss::TransactionType] = jss::XChainAddAccountCreateAttestation; - result[jss::Flags] = tfUniversal; return result; } diff --git a/src/test/jtx/multisign.h b/src/test/jtx/multisign.h index 6bcb1a671c..1fed895c6d 100644 --- a/src/test/jtx/multisign.h +++ b/src/test/jtx/multisign.h @@ -21,6 +21,7 @@ #define RIPPLE_TEST_JTX_MULTISIGN_H_INCLUDED #include +#include #include #include #include @@ -65,48 +66,19 @@ signers(Account const& account, none_t); class msig { public: - struct Reg - { - Account acct; - Account sig; - - Reg(Account const& masterSig) : acct(masterSig), sig(masterSig) - { - } - - Reg(Account const& acct_, Account const& regularSig) - : acct(acct_), sig(regularSig) - { - } - - Reg(char const* masterSig) : acct(masterSig), sig(masterSig) - { - } - - Reg(char const* acct_, char const* regularSig) - : acct(acct_), sig(regularSig) - { - } - - bool - operator<(Reg const& rhs) const - { - return acct < rhs.acct; - } - }; - std::vector signers; -public: - msig(std::vector signers_); + msig(std::vector signers_) : signers(std::move(signers_)) + { + sortSigners(signers); + } template requires std::convertible_to explicit msig(AccountType&& a0, Accounts&&... aN) - : msig{std::vector{ - std::forward(a0), - std::forward(aN)...}} + : signers{std::forward(a0), std::forward(aN)...} { + sortSigners(signers); } void diff --git a/src/test/rpc/AccountLines_test.cpp b/src/test/rpc/AccountLines_test.cpp index 6e6f0def19..42acea4111 100644 --- a/src/test/rpc/AccountLines_test.cpp +++ b/src/test/rpc/AccountLines_test.cpp @@ -580,7 +580,6 @@ public: STAmount const& amount) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); @@ -596,7 +595,6 @@ public: PublicKey const& pk) { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index b723095aeb..7a48db73bd 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -698,7 +698,6 @@ public: // gw creates an escrow that we can look for in the ledger. Json::Value jvEscrow; jvEscrow[jss::TransactionType] = jss::EscrowCreate; - jvEscrow[jss::Flags] = tfUniversal; jvEscrow[jss::Account] = gw.human(); jvEscrow[jss::Destination] = gw.human(); jvEscrow[jss::Amount] = XRP(100).value().getJson(JsonOptions::none); @@ -912,7 +911,6 @@ public: // for. Json::Value jvPayChan; jvPayChan[jss::TransactionType] = jss::PaymentChannelCreate; - jvPayChan[jss::Flags] = tfUniversal; jvPayChan[jss::Account] = gw.human(); jvPayChan[jss::Destination] = alice.human(); jvPayChan[jss::Amount] = @@ -938,7 +936,6 @@ public: // gw creates a DID that we can look for in the ledger. Json::Value jvDID; jvDID[jss::TransactionType] = jss::DIDSet; - jvDID[jss::Flags] = tfUniversal; jvDID[jss::Account] = gw.human(); jvDID[sfURI.jsonName] = strHex(std::string{"uri"}); env(jvDID); diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index 9af3fdcb61..6e25c26e58 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -458,7 +458,6 @@ class AccountTx_test : public beast::unit_test::suite STAmount const& amount) { Json::Value escro; escro[jss::TransactionType] = jss::EscrowCreate; - escro[jss::Flags] = tfUniversal; escro[jss::Account] = account.human(); escro[jss::Destination] = to.human(); escro[jss::Amount] = amount.getJson(JsonOptions::none); @@ -487,7 +486,6 @@ class AccountTx_test : public beast::unit_test::suite { Json::Value escrowFinish; escrowFinish[jss::TransactionType] = jss::EscrowFinish; - escrowFinish[jss::Flags] = tfUniversal; escrowFinish[jss::Account] = alice.human(); escrowFinish[sfOwner.jsonName] = alice.human(); escrowFinish[sfOfferSequence.jsonName] = escrowFinishSeq; @@ -496,7 +494,6 @@ class AccountTx_test : public beast::unit_test::suite { Json::Value escrowCancel; escrowCancel[jss::TransactionType] = jss::EscrowCancel; - escrowCancel[jss::Flags] = tfUniversal; escrowCancel[jss::Account] = alice.human(); escrowCancel[sfOwner.jsonName] = alice.human(); escrowCancel[sfOfferSequence.jsonName] = escrowCancelSeq; @@ -510,7 +507,6 @@ class AccountTx_test : public beast::unit_test::suite std::uint32_t payChanSeq{env.seq(alice)}; Json::Value payChanCreate; payChanCreate[jss::TransactionType] = jss::PaymentChannelCreate; - payChanCreate[jss::Flags] = tfUniversal; payChanCreate[jss::Account] = alice.human(); payChanCreate[jss::Destination] = gw.human(); payChanCreate[jss::Amount] = @@ -527,7 +523,6 @@ class AccountTx_test : public beast::unit_test::suite { Json::Value payChanFund; payChanFund[jss::TransactionType] = jss::PaymentChannelFund; - payChanFund[jss::Flags] = tfUniversal; payChanFund[jss::Account] = alice.human(); payChanFund[sfChannel.jsonName] = payChanIndex; payChanFund[jss::Amount] = diff --git a/src/test/rpc/JSONRPC_test.cpp b/src/test/rpc/JSONRPC_test.cpp index cd26758c1f..22c7dfd1dc 100644 --- a/src/test/rpc/JSONRPC_test.cpp +++ b/src/test/rpc/JSONRPC_test.cpp @@ -2132,6 +2132,127 @@ public: result[jss::result][jss::request][jss::command] == "bad_command"); } + void + testAutoFillFails() + { + testcase("autofill fails"); + using namespace test::jtx; + + // test batch raw transactions max size + { + Env env(*this); + auto ledger = env.current(); + auto const& feeTrack = env.app().getFeeTrack(); + Json::Value req; + Account const alice("alice"); + Account const bob("bob"); + env.fund(XRP(100000), alice); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(1)), seq + 1), + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + batch::inner(pay(alice, bob, XRP(3)), seq + 3), + batch::inner(pay(alice, bob, XRP(4)), seq + 4), + batch::inner(pay(alice, bob, XRP(5)), seq + 5), + batch::inner(pay(alice, bob, XRP(6)), seq + 6), + batch::inner(pay(alice, bob, XRP(7)), seq + 7), + batch::inner(pay(alice, bob, XRP(8)), seq + 8), + batch::inner(pay(alice, bob, XRP(9)), seq + 9)); + + jt.jv.removeMember(jss::Fee); + jt.jv.removeMember(jss::TxnSignature); + req[jss::tx_json] = jt.jv; + Json::Value result = checkFee( + req, + Role::ADMIN, + true, + env.app().config(), + feeTrack, + env.app().getTxQ(), + env.app()); + BEAST_EXPECT(result.size() == 0); + BEAST_EXPECT( + req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == + env.current()->fees().base.jsonClipped()); + } + + // test signers max size + { + Env env(*this); + auto ledger = env.current(); + auto const& feeTrack = env.app().getFeeTrack(); + Json::Value req; + Account const alice("alice"); + Account const bob("bob"); + env.fund(XRP(100000), alice, bob); + env.close(); + + auto jt = env.jtnofill( + noop(alice), + msig( + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice, + alice)); + + req[jss::tx_json] = jt.jv; + Json::Value result = checkFee( + req, + Role::ADMIN, + true, + env.app().config(), + feeTrack, + env.app().getTxQ(), + env.app()); + BEAST_EXPECT(result.size() == 0); + BEAST_EXPECT( + req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == + env.current()->fees().base.jsonClipped()); + } + } + void testAutoFillFees() { @@ -2785,6 +2906,7 @@ public: run() override { testBadRpcCommand(); + testAutoFillFails(); testAutoFillFees(); testAutoFillEscalatedFees(); testAutoFillNetworkID(); diff --git a/src/test/rpc/LedgerData_test.cpp b/src/test/rpc/LedgerData_test.cpp index b56cb241dd..c2b22efc00 100644 --- a/src/test/rpc/LedgerData_test.cpp +++ b/src/test/rpc/LedgerData_test.cpp @@ -369,7 +369,6 @@ public: { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = Account{"bob5"}.human(); jv[jss::Destination] = Account{"bob6"}.human(); jv[jss::Amount] = XRP(50).value().getJson(JsonOptions::none); @@ -383,7 +382,6 @@ public: { Json::Value jv; jv[jss::TransactionType] = jss::PaymentChannelCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = Account{"bob6"}.human(); jv[jss::Destination] = Account{"bob7"}.human(); jv[jss::Amount] = XRP(100).value().getJson(JsonOptions::none); diff --git a/src/test/rpc/LedgerEntry_test.cpp b/src/test/rpc/LedgerEntry_test.cpp index cb6f6d45e2..83232f79c8 100644 --- a/src/test/rpc/LedgerEntry_test.cpp +++ b/src/test/rpc/LedgerEntry_test.cpp @@ -1259,7 +1259,6 @@ class LedgerEntry_test : public beast::unit_test::suite NetClock::time_point const& cancelAfter) { Json::Value jv; jv[jss::TransactionType] = jss::EscrowCreate; - jv[jss::Flags] = tfUniversal; jv[jss::Account] = account.human(); jv[jss::Destination] = to.human(); jv[jss::Amount] = amount.getJson(JsonOptions::none); diff --git a/src/test/rpc/Simulate_test.cpp b/src/test/rpc/Simulate_test.cpp index f27f0c2915..a4360ccc8b 100644 --- a/src/test/rpc/Simulate_test.cpp +++ b/src/test/rpc/Simulate_test.cpp @@ -465,6 +465,36 @@ class Simulate_test : public beast::unit_test::suite } } + void + testInvalidTransactionType() + { + testcase("Invalid transaction type"); + + using namespace jtx; + + Env env(*this); + + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(1000000), alice, bob); + env.close(); + + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto const seq = env.seq(alice); + auto jt = env.jtnofill( + batch::outer(alice, env.seq(alice), batchFee, tfAllOrNothing), + batch::inner(pay(alice, bob, XRP(10)), seq + 1), + batch::inner(pay(alice, bob, XRP(10)), seq + 1)); + + jt.jv.removeMember(jss::TxnSignature); + Json::Value params; + params[jss::tx_json] = jt.jv; + auto const resp = env.rpc("json", "simulate", to_string(params)); + BEAST_EXPECT(resp[jss::result][jss::error] == "notImpl"); + BEAST_EXPECT( + resp[jss::result][jss::error_message] == "Not implemented."); + } + void testSuccessfulTransaction() { @@ -1081,6 +1111,7 @@ public: { testParamErrors(); testFeeError(); + testInvalidTransactionType(); testSuccessfulTransaction(); testTransactionNonTecFailure(); testTransactionTecFailure(); diff --git a/src/xrpld/app/ledger/detail/BuildLedger.cpp b/src/xrpld/app/ledger/detail/BuildLedger.cpp index 954507a006..4305426753 100644 --- a/src/xrpld/app/ledger/detail/BuildLedger.cpp +++ b/src/xrpld/app/ledger/detail/BuildLedger.cpp @@ -208,11 +208,17 @@ buildLedger( applyTransactions(app, built, txns, failedTxns, accum, j); if (!txns.empty() || !failedTxns.empty()) - JLOG(j.debug()) << "Applied " << applied << " transactions; " - << failedTxns.size() << " failed and " - << txns.size() << " will be retried."; + JLOG(j.debug()) + << "Applied " << applied << " transactions; " + << failedTxns.size() << " failed and " << txns.size() + << " will be retried. " + << "Total transactions in ledger (including Inner Batch): " + << accum.txCount(); else - JLOG(j.debug()) << "Applied " << applied << " transactions."; + JLOG(j.debug()) + << "Applied " << applied << " transactions. " + << "Total transactions in ledger (including Inner Batch): " + << accum.txCount(); }); } diff --git a/src/xrpld/app/ledger/detail/OpenLedger.cpp b/src/xrpld/app/ledger/detail/OpenLedger.cpp index 86a3b4b840..2c98caaa6d 100644 --- a/src/xrpld/app/ledger/detail/OpenLedger.cpp +++ b/src/xrpld/app/ledger/detail/OpenLedger.cpp @@ -26,6 +26,8 @@ #include #include +#include + #include namespace ripple { @@ -120,6 +122,18 @@ OpenLedger::accept( { auto const& tx = txpair.first; auto const txId = tx->getTransactionID(); + + // skip batch txns + // LCOV_EXCL_START + if (tx->isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch)) + { + XRPL_ASSERT( + txpair.second && txpair.second->isFieldPresent(sfParentBatchID), + "Inner Batch transaction missing sfParentBatchID"); + continue; + } + // LCOV_EXCL_STOP + if (auto const toSkip = app.getHashRouter().shouldRelay(txId)) { JLOG(j_.debug()) << "Relaying recovered tx " << txId; diff --git a/src/xrpld/app/misc/NetworkOPs.cpp b/src/xrpld/app/misc/NetworkOPs.cpp index d87dea3c52..c8197b2219 100644 --- a/src/xrpld/app/misc/NetworkOPs.cpp +++ b/src/xrpld/app/misc/NetworkOPs.cpp @@ -64,6 +64,7 @@ #include #include #include +#include #include #include #include @@ -1190,6 +1191,15 @@ NetworkOPsImp::submitTransaction(std::shared_ptr const& iTrans) return; } + // Enforce Network bar for batch txn + if (iTrans->isFlag(tfInnerBatchTxn) && + m_ledgerMaster.getValidatedRules().enabled(featureBatch)) + { + JLOG(m_journal.error()) + << "Submitted transaction invalid: tfInnerBatchTxn flag present."; + return; + } + // this is an asynchronous interface auto const trans = sterilize(*iTrans); @@ -1249,15 +1259,25 @@ NetworkOPsImp::preProcessTransaction(std::shared_ptr& transaction) return false; } + auto const view = m_ledgerMaster.getCurrentLedger(); + + // This function is called by several different parts of the codebase + // under no circumstances will we ever accept an inner txn within a batch + // txn from the network. + auto const sttx = *transaction->getSTransaction(); + if (sttx.isFlag(tfInnerBatchTxn) && view->rules().enabled(featureBatch)) + { + transaction->setStatus(INVALID); + transaction->setResult(temINVALID_FLAG); + app_.getHashRouter().setFlags(transaction->getID(), SF_BAD); + return false; + } + // NOTE eahennis - I think this check is redundant, // but I'm not 100% sure yet. // If so, only cost is looking up HashRouter flags. - auto const view = m_ledgerMaster.getCurrentLedger(); - auto const [validity, reason] = checkValidity( - app_.getHashRouter(), - *transaction->getSTransaction(), - view->rules(), - app_.config()); + auto const [validity, reason] = + checkValidity(app_.getHashRouter(), sttx, view->rules(), app_.config()); XRPL_ASSERT( validity == Validity::Valid, "ripple::NetworkOPsImp::processTransaction : valid validity"); @@ -1659,13 +1679,17 @@ NetworkOPsImp::apply(std::unique_lock& batchLock) { auto const toSkip = app_.getHashRouter().shouldRelay(e.transaction->getID()); - - if (toSkip) + if (auto const sttx = *(e.transaction->getSTransaction()); + toSkip && + // Skip relaying if it's an inner batch txn and batch + // feature is enabled + !(sttx.isFlag(tfInnerBatchTxn) && + newOL->rules().enabled(featureBatch))) { protocol::TMTransaction tx; Serializer s; - e.transaction->getSTransaction()->add(s); + sttx.add(s); tx.set_rawtransaction(s.data(), s.size()); tx.set_status(protocol::tsCURRENT); tx.set_receivetimestamp( @@ -1677,7 +1701,7 @@ NetworkOPsImp::apply(std::unique_lock& batchLock) } } - if (validatedLedgerIndex) + if (!isTemMalformed(e.result) && validatedLedgerIndex) { auto [fee, accountSeq, availableSeq] = app_.getTxQ().getTxRequiredFeeAndSeq( @@ -3020,6 +3044,11 @@ NetworkOPsImp::pubProposedTransaction( std::shared_ptr const& transaction, TER result) { + // never publish an inner txn inside a batch txn + if (transaction->isFlag(tfInnerBatchTxn) && + ledger->rules().enabled(featureBatch)) + return; + MultiApiJson jvObj = transJson(transaction, result, false, ledger, std::nullopt); diff --git a/src/xrpld/app/misc/detail/TxQ.cpp b/src/xrpld/app/misc/detail/TxQ.cpp index adf96d0e14..6924dae6c8 100644 --- a/src/xrpld/app/misc/detail/TxQ.cpp +++ b/src/xrpld/app/misc/detail/TxQ.cpp @@ -737,6 +737,13 @@ TxQ::apply( STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; + // See if the transaction is valid, properly formed, + // etc. before doing potentially expensive queue + // replace and multi-transaction operations. + auto const pfresult = preflight(app, view.rules(), *tx, flags, j); + if (pfresult.ter != tesSUCCESS) + return {pfresult.ter, false}; + // See if the transaction paid a high enough fee that it can go straight // into the ledger. if (auto directApplied = tryDirectApply(app, view, tx, flags, j)) @@ -749,13 +756,6 @@ TxQ::apply( // o The transaction paid a high enough fee that fee averaging will apply. // o The transaction will be queued. - // See if the transaction is valid, properly formed, - // etc. before doing potentially expensive queue - // replace and multi-transaction operations. - auto const pfresult = preflight(app, view.rules(), *tx, flags, j); - if (pfresult.ter != tesSUCCESS) - return {pfresult.ter, false}; - // If the account is not currently in the ledger, don't queue its tx. auto const account = (*tx)[sfAccount]; Keylet const accountKey{keylet::account(account)}; diff --git a/src/xrpld/app/tx/applySteps.h b/src/xrpld/app/tx/applySteps.h index 2a5557ff4b..ec7180e263 100644 --- a/src/xrpld/app/tx/applySteps.h +++ b/src/xrpld/app/tx/applySteps.h @@ -165,6 +165,8 @@ struct PreflightResult public: /// From the input - the transaction STTx const& tx; + /// From the input - the batch identifier, if part of a batch + std::optional const parentBatchId; /// From the input - the rules Rules const rules; /// Consequences of the transaction @@ -183,6 +185,7 @@ public: Context const& ctx_, std::pair const& result) : tx(ctx_.tx) + , parentBatchId(ctx_.parentBatchId) , rules(ctx_.rules) , consequences(result.second) , flags(ctx_.flags) @@ -210,6 +213,8 @@ public: ReadView const& view; /// From the input - the transaction STTx const& tx; + /// From the input - the batch identifier, if part of a batch + std::optional const parentBatchId; /// From the input - the flags ApplyFlags const flags; /// From the input - the journal @@ -217,6 +222,7 @@ public: /// Intermediate transaction result TER const ter; + /// Success flag - whether the transaction is likely to /// claim a fee bool const likelyToClaimFee; @@ -226,6 +232,7 @@ public: PreclaimResult(Context const& ctx_, TER ter_) : view(ctx_.view) , tx(ctx_.tx) + , parentBatchId(ctx_.parentBatchId) , flags(ctx_.flags) , j(ctx_.j) , ter(ter_) @@ -255,6 +262,7 @@ public: @return A `PreflightResult` object containing, among other things, the `TER` code. */ +/** @{ */ PreflightResult preflight( Application& app, @@ -263,6 +271,16 @@ preflight( ApplyFlags flags, beast::Journal j); +PreflightResult +preflight( + Application& app, + Rules const& rules, + uint256 const& parentBatchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j); +/** @} */ + /** Gate a transaction based on static ledger information. The transaction is checked against all possible diff --git a/src/xrpld/app/tx/detail/ApplyContext.cpp b/src/xrpld/app/tx/detail/ApplyContext.cpp index 71fe246f15..79cbb7f40d 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.cpp +++ b/src/xrpld/app/tx/detail/ApplyContext.cpp @@ -29,6 +29,7 @@ namespace ripple { ApplyContext::ApplyContext( Application& app_, OpenView& base, + std::optional const& parentBatchId, STTx const& tx_, TER preclaimResult_, XRPAmount baseFee_, @@ -41,7 +42,11 @@ ApplyContext::ApplyContext( , journal(journal_) , base_(base) , flags_(flags) + , parentBatchId_(parentBatchId) { + XRPL_ASSERT( + parentBatchId.has_value() == ((flags_ & tapBATCH) == tapBATCH), + "Parent Batch ID should be set if batch apply flag is set"); view_.emplace(&base_, flags_); } @@ -54,7 +59,8 @@ ApplyContext::discard() std::optional ApplyContext::apply(TER ter) { - return view_->apply(base_, tx, ter, flags_ & tapDRY_RUN, journal); + return view_->apply( + base_, tx, ter, parentBatchId_, flags_ & tapDRY_RUN, journal); } std::size_t diff --git a/src/xrpld/app/tx/detail/ApplyContext.h b/src/xrpld/app/tx/detail/ApplyContext.h index 715d4ea471..720d0aeea3 100644 --- a/src/xrpld/app/tx/detail/ApplyContext.h +++ b/src/xrpld/app/tx/detail/ApplyContext.h @@ -39,11 +39,34 @@ public: explicit ApplyContext( Application& app, OpenView& base, + std::optional const& parentBatchId, STTx const& tx, TER preclaimResult, XRPAmount baseFee, ApplyFlags flags, - beast::Journal = beast::Journal{beast::Journal::getNullSink()}); + beast::Journal journal = beast::Journal{beast::Journal::getNullSink()}); + + explicit ApplyContext( + Application& app, + OpenView& base, + STTx const& tx, + TER preclaimResult, + XRPAmount baseFee, + ApplyFlags flags, + beast::Journal journal = beast::Journal{beast::Journal::getNullSink()}) + : ApplyContext( + app, + base, + std::nullopt, + tx, + preclaimResult, + baseFee, + flags, + journal) + { + XRPL_ASSERT( + (flags & tapBATCH) == 0, "Batch apply flag should not be set"); + } Application& app; STTx const& tx; @@ -131,6 +154,9 @@ private: OpenView& base_; ApplyFlags flags_; std::optional view_; + + // The ID of the batch transaction we are executing under, if seated. + std::optional parentBatchId_; }; } // namespace ripple diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp new file mode 100644 index 0000000000..dcac889a5a --- /dev/null +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -0,0 +1,482 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace ripple { + +/** + * @brief Calculates the total base fee for a batch transaction. + * + * This function computes the required base fee for a batch transaction, + * including the base fee for the batch itself, the sum of base fees for + * all inner transactions, and additional fees for each batch signer. + * It performs overflow checks and validates the structure of the batch + * and its signers. + * + * @param view The ledger view providing fee and state information. + * @param tx The batch transaction to calculate the fee for. + * @return XRPAmount The total base fee required for the batch transaction. + * + * @throws std::overflow_error If any fee calculation would overflow the + * XRPAmount type. + * @throws std::length_error If the number of inner transactions or signers + * exceeds the allowed maximum. + * @throws std::invalid_argument If an inner transaction is itself a batch + * transaction. + */ +XRPAmount +Batch::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + XRPAmount const maxAmount{ + std::numeric_limits::max()}; + + // batchBase: view.fees().base for batch processing + default base fee + XRPAmount const baseFee = Transactor::calculateBaseFee(view, tx); + + // LCOV_EXCL_START + if (baseFee > maxAmount - view.fees().base) + throw std::overflow_error("XRPAmount overflow"); + // LCOV_EXCL_STOP + + XRPAmount const batchBase = view.fees().base + baseFee; + + // Calculate the Inner Txn Fees + XRPAmount txnFees{0}; + if (tx.isFieldPresent(sfRawTransactions)) + { + auto const& txns = tx.getFieldArray(sfRawTransactions); + + XRPL_ASSERT( + txns.size() <= maxBatchTxCount, + "Raw Transactions array exceeds max entries."); + + // LCOV_EXCL_START + if (txns.size() > maxBatchTxCount) + throw std::length_error( + "Raw Transactions array exceeds max entries"); + // LCOV_EXCL_STOP + + for (STObject txn : txns) + { + STTx const stx = STTx{std::move(txn)}; + + XRPL_ASSERT( + stx.getTxnType() != ttBATCH, "Inner Batch transaction found."); + + // LCOV_EXCL_START + if (stx.getTxnType() == ttBATCH) + throw std::invalid_argument("Inner Batch transaction found"); + // LCOV_EXCL_STOP + + auto const fee = ripple::calculateBaseFee(view, stx); + // LCOV_EXCL_START + if (txnFees > maxAmount - fee) + throw std::overflow_error("XRPAmount overflow"); + // LCOV_EXCL_STOP + txnFees += fee; + } + } + + // Calculate the Signers/BatchSigners Fees + std::int32_t signerCount = 0; + if (tx.isFieldPresent(sfBatchSigners)) + { + auto const& signers = tx.getFieldArray(sfBatchSigners); + XRPL_ASSERT( + signers.size() <= maxBatchTxCount, + "Batch Signers array exceeds max entries."); + + // LCOV_EXCL_START + if (signers.size() > maxBatchTxCount) + throw std::length_error("Batch Signers array exceeds max entries"); + // LCOV_EXCL_STOP + + for (STObject const& signer : signers) + { + if (signer.isFieldPresent(sfTxnSignature)) + signerCount += 1; + else if (signer.isFieldPresent(sfSigners)) + signerCount += signer.getFieldArray(sfSigners).size(); + } + } + + // LCOV_EXCL_START + if (signerCount > 0 && view.fees().base > maxAmount / signerCount) + throw std::overflow_error("XRPAmount overflow"); + // LCOV_EXCL_STOP + + XRPAmount signerFees = signerCount * view.fees().base; + + // LCOV_EXCL_START + if (signerFees > maxAmount - txnFees) + throw std::overflow_error("XRPAmount overflow"); + if (txnFees + signerFees > maxAmount - batchBase) + throw std::overflow_error("XRPAmount overflow"); + // LCOV_EXCL_STOP + + // 10 drops per batch signature + sum of inner tx fees + batchBase + return signerFees + txnFees + batchBase; +} + +/** + * @brief Performs preflight validation checks for a Batch transaction. + * + * This function validates the structure and contents of a Batch transaction + * before it is processed. It ensures that the Batch feature is enabled, + * checks for valid flags, validates the number and uniqueness of inner + * transactions, and enforces correct signing and fee requirements. + * + * The following validations are performed: + * - The Batch feature must be enabled in the current rules. + * - Only one of the mutually exclusive batch flags must be set. + * - The batch must contain at least two and no more than the maximum allowed + * inner transactions. + * - Each inner transaction must: + * - Be unique within the batch. + * - Not itself be a Batch transaction. + * - Have the tfInnerBatchTxn flag set. + * - Not include a TxnSignature or Signers field. + * - Have an empty SigningPubKey. + * - Pass its own preflight checks. + * - Have a fee of zero. + * - Have either Sequence or TicketSequence set, but not both or neither. + * - Not duplicate Sequence or TicketSequence values for the same account (for + * certain flags). + * - Validates that all required inner transaction accounts are present in the + * batch signers array, and that all batch signers are unique and not the outer + * account. + * - Verifies the batch signature if batch signers are present. + * + * @param ctx The PreflightContext containing the transaction and environment. + * @return NotTEC Returns tesSUCCESS if all checks pass, or an appropriate error + * code otherwise. + */ +NotTEC +Batch::preflight(PreflightContext const& ctx) +{ + if (!ctx.rules.enabled(featureBatch)) + return temDISABLED; + + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) + return ret; + + auto const parentBatchId = ctx.tx.getTransactionID(); + auto const outerAccount = ctx.tx.getAccountID(sfAccount); + auto const flags = ctx.tx.getFlags(); + + if (flags & tfBatchMask) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "invalid flags."; + return temINVALID_FLAG; + } + + if (std::popcount( + flags & + (tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent)) != 1) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "too many flags."; + return temINVALID_FLAG; + } + + auto const& rawTxns = ctx.tx.getFieldArray(sfRawTransactions); + if (rawTxns.size() <= 1) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "txns array must have at least 2 entries."; + return temARRAY_EMPTY; + } + + if (rawTxns.size() > maxBatchTxCount) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:" + << "txns array exceeds 8 entries."; + return temARRAY_TOO_LARGE; + } + + // Validation Inner Batch Txns + std::unordered_set requiredSigners; + std::unordered_set uniqueHashes; + std::unordered_map> + accountSeqTicket; + for (STObject rb : rawTxns) + { + STTx const stx = STTx{std::move(rb)}; + auto const hash = stx.getTransactionID(); + if (!uniqueHashes.emplace(hash).second) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "duplicate Txn found. " + << "txID: " << hash; + return temREDUNDANT; + } + + if (stx.getFieldU16(sfTransactionType) == ttBATCH) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "batch cannot have an inner batch txn. " + << "txID: " << hash; + return temINVALID; + } + + if (!(stx.getFlags() & tfInnerBatchTxn)) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have the tfInnerBatchTxn flag. " + << "txID: " << hash; + return temINVALID_FLAG; + } + + if (stx.isFieldPresent(sfTxnSignature)) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn cannot include TxnSignature. " + << "txID: " << hash; + return temBAD_SIGNATURE; + } + + if (stx.isFieldPresent(sfSigners)) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn cannot include Signers. " + << "txID: " << hash; + return temBAD_SIGNER; + } + + if (!stx.getSigningPubKey().empty()) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn SigningPubKey must be empty. " + << "txID: " << hash; + return temBAD_REGKEY; + } + + auto const innerAccount = stx.getAccountID(sfAccount); + if (auto const preflightResult = ripple::preflight( + ctx.app, ctx.rules, parentBatchId, stx, tapBATCH, ctx.j); + preflightResult.ter != tesSUCCESS) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn preflight failed: " + << transHuman(preflightResult.ter) << " " + << "txID: " << hash; + return temINVALID_INNER_BATCH; + } + + // Check that the fee is zero + if (auto const fee = stx.getFieldAmount(sfFee); + !fee.native() || fee.xrp() != beast::zero) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have a fee of 0. " + << "txID: " << hash; + return temBAD_FEE; + } + + // Check that Sequence and TicketSequence are not both present + if (stx.isFieldPresent(sfTicketSequence) && + stx.getFieldU32(sfSequence) != 0) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have exactly one of Sequence and " + "TicketSequence. " + << "txID: " << hash; + return temSEQ_AND_TICKET; + } + + // Verify that either Sequence or TicketSequence is present + if (!stx.isFieldPresent(sfTicketSequence) && + stx.getFieldU32(sfSequence) == 0) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have either Sequence or " + "TicketSequence. " + << "txID: " << hash; + return temSEQ_AND_TICKET; + } + + // Duplicate sequence and ticket checks + if (flags & (tfAllOrNothing | tfUntilFailure)) + { + if (auto const seq = stx.getFieldU32(sfSequence); seq != 0) + { + if (!accountSeqTicket[innerAccount].insert(seq).second) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "duplicate sequence found: " + << "txID: " << hash; + return temREDUNDANT; + } + } + + if (stx.isFieldPresent(sfTicketSequence)) + { + if (auto const ticket = stx.getFieldU32(sfTicketSequence); + !accountSeqTicket[innerAccount].insert(ticket).second) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "duplicate ticket found: " + << "txID: " << hash; + return temREDUNDANT; + } + } + } + + // If the inner account is the same as the outer account, do not add the + // inner account to the required signers set. + if (innerAccount != outerAccount) + requiredSigners.insert(innerAccount); + } + + // LCOV_EXCL_START + if (auto const ret = preflight2(ctx); !isTesSuccess(ret)) + return ret; + // LCOV_EXCL_STOP + + // Validation Batch Signers + std::unordered_set batchSigners; + if (ctx.tx.isFieldPresent(sfBatchSigners)) + { + STArray const& signers = ctx.tx.getFieldArray(sfBatchSigners); + + // Check that the batch signers array is not too large. + if (signers.size() > maxBatchTxCount) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "signers array exceeds 8 entries."; + return temARRAY_TOO_LARGE; + } + + // Add batch signers to the set to ensure all signer accounts are + // unique. Meanwhile, remove signer accounts from the set of inner + // transaction accounts (`requiredSigners`). By the end of the loop, + // `requiredSigners` should be empty, indicating that all inner + // accounts are matched with signers. + for (auto const& signer : signers) + { + AccountID const signerAccount = signer.getAccountID(sfAccount); + if (signerAccount == outerAccount) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "signer cannot be the outer account: " << signerAccount; + return temBAD_SIGNER; + } + + if (!batchSigners.insert(signerAccount).second) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "duplicate signer found: " << signerAccount; + return temREDUNDANT; + } + + // Check that the batch signer is in the required signers set. + // Remove it if it does, as it can be crossed off the list. + if (requiredSigners.erase(signerAccount) == 0) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "no account signature for inner txn."; + return temBAD_SIGNER; + } + } + + // Check the batch signers signatures. + auto const sigResult = ctx.tx.checkBatchSign( + STTx::RequireFullyCanonicalSig::yes, ctx.rules); + + if (!sigResult) + { + JLOG(ctx.j.debug()) + << "BatchTrace[" << parentBatchId << "]: " + << "invalid batch txn signature: " << sigResult.error(); + return temBAD_SIGNATURE; + } + } + + if (!requiredSigners.empty()) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "invalid batch signers."; + return temBAD_SIGNER; + } + return tesSUCCESS; +} + +/** + * @brief Checks the validity of signatures for a batch transaction. + * + * This method first verifies the standard transaction signature by calling + * Transactor::checkSign. If the signature is not valid it returns the + * corresponding error code. + * + * Next, it verifies the batch-specific signature requirements by calling + * Transactor::checkBatchSign. If this check fails, it also returns the + * corresponding error code. + * + * If both checks succeed, the function returns tesSUCCESS. + * + * @param ctx The PreclaimContext containing transaction and environment data. + * @return NotTEC Returns tesSUCCESS if all signature checks pass, or an error + * code otherwise. + */ +NotTEC +Batch::checkSign(PreclaimContext const& ctx) +{ + if (auto ret = Transactor::checkSign(ctx); !isTesSuccess(ret)) + return ret; + + if (auto ret = Transactor::checkBatchSign(ctx); !isTesSuccess(ret)) + return ret; + + return tesSUCCESS; +} + +/** + * @brief Applies the outer batch transaction. + * + * This method is responsible for applying the outer batch transaction. + * The inner transactions within the batch are applied separately in the + * `applyBatchTransactions` method after the outer transaction is processed. + * + * @return TER Returns tesSUCCESS to indicate successful application of the + * outer batch transaction. + */ +TER +Batch::doApply() +{ + return tesSUCCESS; +} + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/Batch.h b/src/xrpld/app/tx/detail/Batch.h new file mode 100644 index 0000000000..211bce0589 --- /dev/null +++ b/src/xrpld/app/tx/detail/Batch.h @@ -0,0 +1,55 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2024 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_BATCH_H_INCLUDED +#define RIPPLE_TX_BATCH_H_INCLUDED + +#include +#include + +#include +#include + +namespace ripple { + +class Batch : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit Batch(ApplyContext& ctx) : Transactor(ctx) + { + } + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static NotTEC + checkSign(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/SetAccount.cpp b/src/xrpld/app/tx/detail/SetAccount.cpp index 599819151a..0c16182ed8 100644 --- a/src/xrpld/app/tx/detail/SetAccount.cpp +++ b/src/xrpld/app/tx/detail/SetAccount.cpp @@ -214,7 +214,7 @@ SetAccount::checkPermission(ReadView const& view, STTx const& tx) // AccountSet transaction. If any delegated account is trying to // update the flag on behalf of another account, it is not // authorized. - if (uSetFlag != 0 || uClearFlag != 0 || uTxFlags != tfFullyCanonicalSig) + if (uSetFlag != 0 || uClearFlag != 0 || uTxFlags & tfUniversalMask) return tecNO_PERMISSION; if (tx.isFieldPresent(sfEmailHash) && diff --git a/src/xrpld/app/tx/detail/Transactor.cpp b/src/xrpld/app/tx/detail/Transactor.cpp index baba7d131e..cc82f7c3ca 100644 --- a/src/xrpld/app/tx/detail/Transactor.cpp +++ b/src/xrpld/app/tx/detail/Transactor.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include namespace ripple { @@ -42,6 +43,13 @@ namespace ripple { NotTEC preflight0(PreflightContext const& ctx) { + if (isPseudoTx(ctx.tx) && ctx.tx.isFlag(tfInnerBatchTxn)) + { + JLOG(ctx.j.warn()) << "Pseudo transactions cannot contain the " + "tfInnerBatchTxn flag."; + return temINVALID_FLAG; + } + if (!isPseudoTx(ctx.tx) || ctx.tx.isFieldPresent(sfNetworkID)) { uint32_t nodeNID = ctx.app.config().NETWORK_ID; @@ -136,6 +144,14 @@ preflight1(PreflightContext const& ctx) ctx.tx.isFieldPresent(sfAccountTxnID)) return temINVALID; + if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch)) + return temINVALID_FLAG; + + XRPL_ASSERT( + ctx.tx.isFlag(tfInnerBatchTxn) == ctx.parentBatchId.has_value() || + !ctx.rules.enabled(featureBatch), + "Inner batch transaction must have a parent batch ID."); + return tesSUCCESS; } @@ -176,25 +192,13 @@ preflight2(PreflightContext const& ctx) if (sigValid.first == Validity::SigBad) { JLOG(ctx.j.debug()) << "preflight2: bad signature. " << sigValid.second; - return temINVALID; + return temINVALID; // LCOV_EXCL_LINE } return tesSUCCESS; } //------------------------------------------------------------------------------ -PreflightContext::PreflightContext( - Application& app_, - STTx const& tx_, - Rules const& rules_, - ApplyFlags flags_, - beast::Journal j_) - : app(app_), tx(tx_), rules(rules_), flags(flags_), j(j_) -{ -} - -//------------------------------------------------------------------------------ - Transactor::Transactor(ApplyContext& ctx) : ctx_(ctx), j_(ctx.journal), account_(ctx.tx.getAccountID(sfAccount)) { @@ -251,6 +255,16 @@ Transactor::checkFee(PreclaimContext const& ctx, XRPAmount baseFee) return temBAD_FEE; auto const feePaid = ctx.tx[sfFee].xrp(); + + if (ctx.flags & tapBATCH) + { + if (feePaid == beast::zero) + return tesSUCCESS; + + JLOG(ctx.j.trace()) << "Batch: Fee must be zero."; + return temBAD_FEE; // LCOV_EXCL_LINE + } + if (!isLegalAmount(feePaid) || feePaid < beast::zero) return temBAD_FEE; @@ -557,51 +571,113 @@ Transactor::apply() NotTEC Transactor::checkSign(PreclaimContext const& ctx) { - if (ctx.flags & tapDRY_RUN) + // Ignore signature check on batch inner transactions + if (ctx.tx.isFlag(tfInnerBatchTxn) && + ctx.view.rules().enabled(featureBatch)) { - // This code must be different for `simulate` - // Since the public key may be empty even for single signing - if (ctx.tx.isFieldPresent(sfSigners)) - return checkMultiSign(ctx); - return checkSingleSign(ctx); + // Defensive Check: These values are also checked in Batch::preflight + if (ctx.tx.isFieldPresent(sfTxnSignature) || + !ctx.tx.getSigningPubKey().empty() || + ctx.tx.isFieldPresent(sfSigners)) + { + return temINVALID_FLAG; // LCOV_EXCL_LINE + } + return tesSUCCESS; } - // If the pk is empty, then we must be multi-signing. - if (ctx.tx.getSigningPubKey().empty()) - return checkMultiSign(ctx); - return checkSingleSign(ctx); -} + auto const idAccount = ctx.tx[~sfDelegate].value_or(ctx.tx[sfAccount]); -NotTEC -Transactor::checkSingleSign(PreclaimContext const& ctx) -{ - // Check that the value in the signing key slot is a public key. + // If the pk is empty and not simulate or simulate and signers, + // then we must be multi-signing. + if ((ctx.flags & tapDRY_RUN && ctx.tx.isFieldPresent(sfSigners)) || + (!(ctx.flags & tapDRY_RUN) && ctx.tx.getSigningPubKey().empty())) + { + STArray const& txSigners(ctx.tx.getFieldArray(sfSigners)); + return checkMultiSign(ctx.view, idAccount, txSigners, ctx.flags, ctx.j); + } + + // Check Single Sign auto const pkSigner = ctx.tx.getSigningPubKey(); + // This ternary is only needed to handle `simulate` + XRPL_ASSERT( + (ctx.flags & tapDRY_RUN) || !pkSigner.empty(), + "ripple::Transactor::checkSingleSign : non-empty signer or simulation"); + if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(pkSigner))) { JLOG(ctx.j.trace()) << "checkSingleSign: signing public key type is unknown"; return tefBAD_AUTH; // FIXME: should be better error! } - - // Look up the account. - auto const idAccount = ctx.tx.isFieldPresent(sfDelegate) - ? ctx.tx.getAccountID(sfDelegate) - : ctx.tx.getAccountID(sfAccount); + auto const idSigner = pkSigner.empty() + ? idAccount + : calcAccountID(PublicKey(makeSlice(pkSigner))); auto const sleAccount = ctx.view.read(keylet::account(idAccount)); if (!sleAccount) return terNO_ACCOUNT; - // This ternary is only needed to handle `simulate` - XRPL_ASSERT( - (ctx.flags & tapDRY_RUN) || !pkSigner.empty(), - "ripple::Transactor::checkSingleSign : non-empty signer or simulation"); - auto const idSigner = pkSigner.empty() - ? idAccount - : calcAccountID(PublicKey(makeSlice(pkSigner))); + return checkSingleSign( + idSigner, idAccount, sleAccount, ctx.view.rules(), ctx.j); +} + +NotTEC +Transactor::checkBatchSign(PreclaimContext const& ctx) +{ + NotTEC ret = tesSUCCESS; + STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)}; + for (auto const& signer : signers) + { + auto const idAccount = signer.getAccountID(sfAccount); + + Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey); + if (pkSigner.empty()) + { + STArray const& txSigners(signer.getFieldArray(sfSigners)); + if (ret = checkMultiSign( + ctx.view, idAccount, txSigners, ctx.flags, ctx.j); + !isTesSuccess(ret)) + return ret; + } + else + { + // LCOV_EXCL_START + if (!publicKeyType(makeSlice(pkSigner))) + return tefBAD_AUTH; + // LCOV_EXCL_STOP + + auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner))); + auto const sleAccount = ctx.view.read(keylet::account(idAccount)); + + // A batch can include transactions from an un-created account ONLY + // when the account master key is the signer + if (!sleAccount) + { + if (idAccount != idSigner) + return tefBAD_AUTH; + + return tesSUCCESS; + } + + if (ret = checkSingleSign( + idSigner, idAccount, sleAccount, ctx.view.rules(), ctx.j); + !isTesSuccess(ret)) + return ret; + } + } + return ret; +} + +NotTEC +Transactor::checkSingleSign( + AccountID const& idSigner, + AccountID const& idAccount, + std::shared_ptr sleAccount, + Rules const& rules, + beast::Journal j) +{ bool const isMasterDisabled = sleAccount->isFlag(lsfDisableMaster); - if (ctx.view.rules().enabled(fixMasterKeyAsRegularKey)) + if (rules.enabled(fixMasterKeyAsRegularKey)) { // Signed with regular key. if ((*sleAccount)[~sfRegularKey] == idSigner) @@ -638,16 +714,14 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) else if (sleAccount->isFieldPresent(sfRegularKey)) { // Signing key does not match master or regular key. - JLOG(ctx.j.trace()) - << "checkSingleSign: Not authorized to use account."; + JLOG(j.trace()) << "checkSingleSign: Not authorized to use account."; return tefBAD_AUTH; } else { // No regular key on account and signing key does not match master key. // FIXME: Why differentiate this case from tefBAD_AUTH? - JLOG(ctx.j.trace()) - << "checkSingleSign: Not authorized to use account."; + JLOG(j.trace()) << "checkSingleSign: Not authorized to use account."; return tefBAD_AUTH_MASTER; } @@ -655,18 +729,20 @@ Transactor::checkSingleSign(PreclaimContext const& ctx) } NotTEC -Transactor::checkMultiSign(PreclaimContext const& ctx) +Transactor::checkMultiSign( + ReadView const& view, + AccountID const& id, + STArray const& txSigners, + ApplyFlags const& flags, + beast::Journal j) { - auto const id = ctx.tx.isFieldPresent(sfDelegate) - ? ctx.tx.getAccountID(sfDelegate) - : ctx.tx.getAccountID(sfAccount); // Get mTxnAccountID's SignerList and Quorum. std::shared_ptr sleAccountSigners = - ctx.view.read(keylet::signers(id)); + view.read(keylet::signers(id)); // If the signer list doesn't exist the account is not multi-signing. if (!sleAccountSigners) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid: Not a multi-signing account."; return tefNOT_MULTI_SIGNING; } @@ -681,12 +757,11 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) "ripple::Transactor::checkMultiSign : signer list ID is 0"); auto accountSigners = - SignerEntries::deserialize(*sleAccountSigners, ctx.j, "ledger"); + SignerEntries::deserialize(*sleAccountSigners, j, "ledger"); if (!accountSigners) return accountSigners.error(); // Get the array of transaction signers. - STArray const& txSigners(ctx.tx.getFieldArray(sfSigners)); // Walk the accountSigners performing a variety of checks and see if // the quorum is met. @@ -705,7 +780,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) { if (++iter == accountSigners->end()) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account."; return tefBAD_SIGNATURE; } @@ -713,7 +788,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) if (iter->account != txSignerAcctID) { // The SigningAccount is not in the SignerEntries. - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Invalid SigningAccount.Account."; return tefBAD_SIGNATURE; } @@ -723,16 +798,16 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // public key. auto const spk = txSigner.getFieldVL(sfSigningPubKey); - if (!(ctx.flags & tapDRY_RUN) && !publicKeyType(makeSlice(spk))) + if (!(flags & tapDRY_RUN) && !publicKeyType(makeSlice(spk))) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "checkMultiSign: signing public key type is unknown"; return tefBAD_SIGNATURE; } // This ternary is only needed to handle `simulate` XRPL_ASSERT( - (ctx.flags & tapDRY_RUN) || !spk.empty(), + (flags & tapDRY_RUN) || !spk.empty(), "ripple::Transactor::checkMultiSign : non-empty signer or " "simulation"); AccountID const signingAcctIDFromPubKey = spk.empty() @@ -764,7 +839,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // In any of these cases we need to know whether the account is in // the ledger. Determine that now. - auto sleTxSignerRoot = ctx.view.read(keylet::account(txSignerAcctID)); + auto const sleTxSignerRoot = view.read(keylet::account(txSignerAcctID)); if (signingAcctIDFromPubKey == txSignerAcctID) { @@ -777,7 +852,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) if (signerAccountFlags & lsfDisableMaster) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Signer:Account lsfDisableMaster."; return tefMASTER_DISABLED; } @@ -789,21 +864,21 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // Public key must hash to the account's regular key. if (!sleTxSignerRoot) { - JLOG(ctx.j.trace()) << "applyTransaction: Non-phantom signer " - "lacks account root."; + JLOG(j.trace()) << "applyTransaction: Non-phantom signer " + "lacks account root."; return tefBAD_SIGNATURE; } if (!sleTxSignerRoot->isFieldPresent(sfRegularKey)) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Account lacks RegularKey."; return tefBAD_SIGNATURE; } if (signingAcctIDFromPubKey != sleTxSignerRoot->getAccountID(sfRegularKey)) { - JLOG(ctx.j.trace()) + JLOG(j.trace()) << "applyTransaction: Account doesn't match RegularKey."; return tefBAD_SIGNATURE; } @@ -815,8 +890,7 @@ Transactor::checkMultiSign(PreclaimContext const& ctx) // Cannot perform transaction if quorum is not met. if (weightSum < sleAccountSigners->getFieldU32(sfSignerQuorum)) { - JLOG(ctx.j.trace()) - << "applyTransaction: Signers failed to meet quorum."; + JLOG(j.trace()) << "applyTransaction: Signers failed to meet quorum."; return tefBAD_QUORUM; } @@ -904,7 +978,11 @@ removeDeletedTrustLines( } } -/** Reset the context, discarding any changes made and adjust the fee */ +/** Reset the context, discarding any changes made and adjust the fee. + + @param fee The transaction fee to be charged. + @return A pair containing the transaction result and the actual fee charged. + */ std::pair Transactor::reset(XRPAmount fee) { @@ -912,9 +990,10 @@ Transactor::reset(XRPAmount fee) auto const txnAcct = view().peek(keylet::account(ctx_.tx.getAccountID(sfAccount))); + + // The account should never be missing from the ledger. But if it + // is missing then we can't very well charge it a fee, can we? if (!txnAcct) - // The account should never be missing from the ledger. But if it - // is missing then we can't very well charge it a fee, can we? return {tefINTERNAL, beast::zero}; auto const payerSle = ctx_.tx.isFieldPresent(sfDelegate) @@ -1024,7 +1103,6 @@ Transactor::operator()() { // If the tapFAIL_HARD flag is set, a tec result // must not do anything - ctx_.discard(); applied = false; } diff --git a/src/xrpld/app/tx/detail/Transactor.h b/src/xrpld/app/tx/detail/Transactor.h index 4956f021df..42d4861a63 100644 --- a/src/xrpld/app/tx/detail/Transactor.h +++ b/src/xrpld/app/tx/detail/Transactor.h @@ -37,14 +37,38 @@ public: STTx const& tx; Rules const rules; ApplyFlags flags; + std::optional parentBatchId; beast::Journal const j; + PreflightContext( + Application& app_, + STTx const& tx_, + uint256 parentBatchId_, + Rules const& rules_, + ApplyFlags flags_, + beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) + : app(app_) + , tx(tx_) + , rules(rules_) + , flags(flags_) + , parentBatchId(parentBatchId_) + , j(j_) + { + XRPL_ASSERT( + (flags_ & tapBATCH) == tapBATCH, "Batch apply flag should be set"); + } + PreflightContext( Application& app_, STTx const& tx_, Rules const& rules_, ApplyFlags flags_, - beast::Journal j_); + beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) + : app(app_), tx(tx_), rules(rules_), flags(flags_), j(j_) + { + XRPL_ASSERT( + (flags_ & tapBATCH) == 0, "Batch apply flag should not be set"); + } PreflightContext& operator=(PreflightContext const&) = delete; @@ -57,8 +81,9 @@ public: Application& app; ReadView const& view; TER preflightResult; - STTx const& tx; ApplyFlags flags; + STTx const& tx; + std::optional const parentBatchId; beast::Journal const j; PreclaimContext( @@ -67,14 +92,39 @@ public: TER preflightResult_, STTx const& tx_, ApplyFlags flags_, + std::optional parentBatchId_, beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) : app(app_) , view(view_) , preflightResult(preflightResult_) - , tx(tx_) , flags(flags_) + , tx(tx_) + , parentBatchId(parentBatchId_) , j(j_) { + XRPL_ASSERT( + parentBatchId.has_value() == ((flags_ & tapBATCH) == tapBATCH), + "Parent Batch ID should be set if batch apply flag is set"); + } + + PreclaimContext( + Application& app_, + ReadView const& view_, + TER preflightResult_, + STTx const& tx_, + ApplyFlags flags_, + beast::Journal j_ = beast::Journal{beast::Journal::getNullSink()}) + : PreclaimContext( + app_, + view_, + preflightResult_, + tx_, + flags_, + std::nullopt, + j_) + { + XRPL_ASSERT( + (flags_ & tapBATCH) == 0, "Batch apply flag should not be set"); } PreclaimContext& @@ -139,6 +189,9 @@ public: static NotTEC checkSign(PreclaimContext const& ctx); + static NotTEC + checkBatchSign(PreclaimContext const& ctx); + // Returns the fee in fee units, not scaled for load. static XRPAmount calculateBaseFee(ReadView const& view, STTx const& tx); @@ -200,9 +253,19 @@ private: TER payFee(); static NotTEC - checkSingleSign(PreclaimContext const& ctx); + checkSingleSign( + AccountID const& idSigner, + AccountID const& idAccount, + std::shared_ptr sleAccount, + Rules const& rules, + beast::Journal j); static NotTEC - checkMultiSign(PreclaimContext const& ctx); + checkMultiSign( + ReadView const& view, + AccountID const& idAccount, + STArray const& txSigners, + ApplyFlags const& flags, + beast::Journal j); void trapTransaction(uint256) const; }; diff --git a/src/xrpld/app/tx/detail/apply.cpp b/src/xrpld/app/tx/detail/apply.cpp index 615fd6a92d..889a520032 100644 --- a/src/xrpld/app/tx/detail/apply.cpp +++ b/src/xrpld/app/tx/detail/apply.cpp @@ -23,6 +23,7 @@ #include #include +#include namespace ripple { @@ -43,6 +44,28 @@ checkValidity( { auto const id = tx.getTransactionID(); auto const flags = router.getFlags(id); + + // Ignore signature check on batch inner transactions + if (tx.isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch)) + { + // Defensive Check: These values are also checked in Batch::preflight + if (tx.isFieldPresent(sfTxnSignature) || + !tx.getSigningPubKey().empty() || tx.isFieldPresent(sfSigners)) + return { + Validity::SigBad, + "Malformed: Invalid inner batch transaction."}; + + std::string reason; + if (!passesLocalChecks(tx, reason)) + { + router.setFlags(id, SF_LOCALBAD); + return {Validity::SigGoodOnly, reason}; + } + + router.setFlags(id, SF_SIGGOOD); + return {Validity::Valid, ""}; + } + if (flags & SF_SIGBAD) // Signature is known bad return {Validity::SigBad, "Transaction has bad signature."}; @@ -106,6 +129,16 @@ forceValidity(HashRouter& router, uint256 const& txid, Validity validity) router.setFlags(txid, flags); } +template +ApplyResult +apply(Application& app, OpenView& view, PreflightChecks&& preflightChecks) +{ + STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; + NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; + + return doApply(preclaim(preflightChecks(), app, view), app, view); +} + ApplyResult apply( Application& app, @@ -114,12 +147,89 @@ apply( ApplyFlags flags, beast::Journal j) { - STAmountSO stAmountSO{view.rules().enabled(fixSTAmountCanonicalize)}; - NumberSO stNumberSO{view.rules().enabled(fixUniversalNumber)}; + return apply(app, view, [&]() mutable { + return preflight(app, view.rules(), tx, flags, j); + }); +} - auto pfresult = preflight(app, view.rules(), tx, flags, j); - auto pcresult = preclaim(pfresult, app, view); - return doApply(pcresult, app, view); +ApplyResult +apply( + Application& app, + OpenView& view, + uint256 const& parentBatchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j) +{ + return apply(app, view, [&]() mutable { + return preflight(app, view.rules(), parentBatchId, tx, flags, j); + }); +} + +static bool +applyBatchTransactions( + Application& app, + OpenView& batchView, + STTx const& batchTxn, + beast::Journal j) +{ + XRPL_ASSERT( + batchTxn.getTxnType() == ttBATCH && + batchTxn.getFieldArray(sfRawTransactions).size() != 0, + "Batch transaction missing sfRawTransactions"); + + auto const parentBatchId = batchTxn.getTransactionID(); + auto const mode = batchTxn.getFlags(); + + auto applyOneTransaction = + [&app, &j, &parentBatchId, &batchView](STTx&& tx) { + OpenView perTxBatchView(batch_view, batchView); + + auto const ret = + apply(app, perTxBatchView, parentBatchId, tx, tapBATCH, j); + XRPL_ASSERT( + ret.applied == (isTesSuccess(ret.ter) || isTecClaim(ret.ter)), + "Inner transaction should not be applied"); + + JLOG(j.debug()) << "BatchTrace[" << parentBatchId + << "]: " << tx.getTransactionID() << " " + << (ret.applied ? "applied" : "failure") << ": " + << transToken(ret.ter); + + // If the transaction should be applied push its changes to the + // whole-batch view. + if (ret.applied && (isTesSuccess(ret.ter) || isTecClaim(ret.ter))) + perTxBatchView.apply(batchView); + + return ret; + }; + + int applied = 0; + + for (STObject rb : batchTxn.getFieldArray(sfRawTransactions)) + { + auto const result = applyOneTransaction(STTx{std::move(rb)}); + XRPL_ASSERT( + result.applied == + (isTesSuccess(result.ter) || isTecClaim(result.ter)), + "Outer Batch failure, inner transaction should not be applied"); + + if (result.applied) + ++applied; + + if (!isTesSuccess(result.ter)) + { + if (mode & tfAllOrNothing) + return false; + + if (mode & tfUntilFailure) + break; + } + else if (mode & tfOnlyOne) + break; + } + + return applied != 0; } ApplyTransactionResult @@ -141,10 +251,22 @@ applyTransaction( try { auto const result = apply(app, view, txn, flags, j); + if (result.applied) { JLOG(j.debug()) - << "Transaction applied: " << transHuman(result.ter); + << "Transaction applied: " << transToken(result.ter); + + // The batch transaction was just applied; now we need to apply + // its inner transactions as necessary. + if (isTesSuccess(result.ter) && txn.getTxnType() == ttBATCH) + { + OpenView wholeBatchView(batch_view, view); + + if (applyBatchTransactions(app, wholeBatchView, txn, j)) + wholeBatchView.apply(view); + } + return ApplyTransactionResult::Success; } diff --git a/src/xrpld/app/tx/detail/applySteps.cpp b/src/xrpld/app/tx/detail/applySteps.cpp index 5e8c125e83..34259ebef0 100644 --- a/src/xrpld/app/tx/detail/applySteps.cpp +++ b/src/xrpld/app/tx/detail/applySteps.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -104,7 +105,6 @@ with_txn_type(TxType txnType, F&& f) #undef TRANSACTION #pragma pop_macro("TRANSACTION") - default: throw UnknownTxnType(txnType); } @@ -227,6 +227,22 @@ invoke_preclaim(PreclaimContext const& ctx) } } +/** + * @brief Calculates the base fee for a given transaction. + * + * This function determines the base fee required for the specified transaction + * by invoking the appropriate fee calculation logic based on the transaction + * type. It uses a type-dispatch mechanism to select the correct calculation + * method. + * + * @param view The ledger view to use for fee calculation. + * @param tx The transaction for which the base fee is to be calculated. + * @return The calculated base fee as an XRPAmount. + * + * @throws std::exception If an error occurs during fee calculation, including + * but not limited to unknown transaction types or internal errors, the function + * logs an error and returns an XRPAmount of zero. + */ static XRPAmount invoke_calculateBaseFee(ReadView const& view, STTx const& tx) { @@ -320,7 +336,28 @@ preflight( } catch (std::exception const& e) { - JLOG(j.fatal()) << "apply: " << e.what(); + JLOG(j.fatal()) << "apply (preflight): " << e.what(); + return {pfctx, {tefEXCEPTION, TxConsequences{tx}}}; + } +} + +PreflightResult +preflight( + Application& app, + Rules const& rules, + uint256 const& parentBatchId, + STTx const& tx, + ApplyFlags flags, + beast::Journal j) +{ + PreflightContext const pfctx(app, tx, parentBatchId, rules, flags, j); + try + { + return {pfctx, invoke_preflight(pfctx)}; + } + catch (std::exception const& e) + { + JLOG(j.fatal()) << "apply (preflight): " << e.what(); return {pfctx, {tefEXCEPTION, TxConsequences{tx}}}; } } @@ -334,18 +371,31 @@ preclaim( std::optional ctx; if (preflightResult.rules != view.rules()) { - auto secondFlight = preflight( - app, - view.rules(), - preflightResult.tx, - preflightResult.flags, - preflightResult.j); + auto secondFlight = [&]() { + if (preflightResult.parentBatchId) + return preflight( + app, + view.rules(), + preflightResult.parentBatchId.value(), + preflightResult.tx, + preflightResult.flags, + preflightResult.j); + + return preflight( + app, + view.rules(), + preflightResult.tx, + preflightResult.flags, + preflightResult.j); + }(); + ctx.emplace( app, view, secondFlight.ter, secondFlight.tx, secondFlight.flags, + secondFlight.parentBatchId, secondFlight.j); } else @@ -356,8 +406,10 @@ preclaim( preflightResult.ter, preflightResult.tx, preflightResult.flags, + preflightResult.parentBatchId, preflightResult.j); } + try { if (ctx->preflightResult != tesSUCCESS) @@ -366,7 +418,7 @@ preclaim( } catch (std::exception const& e) { - JLOG(ctx->j.fatal()) << "apply: " << e.what(); + JLOG(ctx->j.fatal()) << "apply (preclaim): " << e.what(); return {*ctx, tefEXCEPTION}; } } @@ -399,6 +451,7 @@ doApply(PreclaimResult const& preclaimResult, Application& app, OpenView& view) ApplyContext ctx( app, view, + preclaimResult.parentBatchId, preclaimResult.tx, preclaimResult.ter, calculateBaseFee(view, preclaimResult.tx), diff --git a/src/xrpld/ledger/ApplyView.h b/src/xrpld/ledger/ApplyView.h index 1abff33be0..1e4a5a112a 100644 --- a/src/xrpld/ledger/ApplyView.h +++ b/src/xrpld/ledger/ApplyView.h @@ -42,6 +42,9 @@ enum ApplyFlags : std::uint32_t { // Transaction came from a privileged source tapUNLIMITED = 0x400, + // Transaction is executing as part of a batch + tapBATCH = 0x800, + // Transaction shouldn't be applied // Signatures shouldn't be checked tapDRY_RUN = 0x1000 diff --git a/src/xrpld/ledger/ApplyViewImpl.h b/src/xrpld/ledger/ApplyViewImpl.h index 1c282565b1..d170cf71ff 100644 --- a/src/xrpld/ledger/ApplyViewImpl.h +++ b/src/xrpld/ledger/ApplyViewImpl.h @@ -58,6 +58,7 @@ public: OpenView& to, STTx const& tx, TER ter, + std::optional parentBatchId, bool isDryRun, beast::Journal j); diff --git a/src/xrpld/ledger/OpenView.h b/src/xrpld/ledger/OpenView.h index ecc618e185..a1fa195a69 100644 --- a/src/xrpld/ledger/OpenView.h +++ b/src/xrpld/ledger/OpenView.h @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -39,13 +40,21 @@ namespace ripple { Views constructed with this tag will have the rules of open ledgers applied during transaction processing. -*/ -struct open_ledger_t + */ +inline constexpr struct open_ledger_t { - explicit open_ledger_t() = default; -}; + explicit constexpr open_ledger_t() = default; +} open_ledger{}; -extern open_ledger_t const open_ledger; +/** Batch view construction tag. + + Views constructed with this tag are part of a stack of views + used during batch transaction applied. + */ +inline constexpr struct batch_view_t +{ + explicit constexpr batch_view_t() = default; +} batch_view{}; //------------------------------------------------------------------------------ @@ -97,6 +106,10 @@ private: ReadView const* base_; detail::RawStateTable items_; std::shared_ptr hold_; + + /// In batch mode, the number of transactions already executed. + std::size_t baseTxCount_ = 0; + bool open_ = true; public: @@ -142,7 +155,6 @@ public: The tx list starts empty and will contain all newly inserted tx. */ - /** @{ */ OpenView( open_ledger_t, ReadView const* base, @@ -156,7 +168,11 @@ public: : OpenView(open_ledger, &*base, rules, base) { } - /** @} */ + + OpenView(batch_view_t, OpenView& base) : OpenView(std::addressof(base)) + { + baseTxCount_ = base.txCount(); + } /** Construct a new last closed ledger. diff --git a/src/xrpld/ledger/detail/ApplyStateTable.cpp b/src/xrpld/ledger/detail/ApplyStateTable.cpp index c11a72d782..2a740093d9 100644 --- a/src/xrpld/ledger/detail/ApplyStateTable.cpp +++ b/src/xrpld/ledger/detail/ApplyStateTable.cpp @@ -116,6 +116,7 @@ ApplyStateTable::apply( STTx const& tx, TER ter, std::optional const& deliver, + std::optional const& parentBatchId, bool isDryRun, beast::Journal j) { @@ -126,9 +127,11 @@ ApplyStateTable::apply( std::optional metadata; if (!to.open() || isDryRun) { - TxMeta meta(tx.getTransactionID(), to.seq()); + TxMeta meta(tx.getTransactionID(), to.seq(), parentBatchId); + if (deliver) meta.setDeliveredAmount(*deliver); + Mods newMod; for (auto& item : items_) { diff --git a/src/xrpld/ledger/detail/ApplyStateTable.h b/src/xrpld/ledger/detail/ApplyStateTable.h index b1bac733fc..5a2e0bcf54 100644 --- a/src/xrpld/ledger/detail/ApplyStateTable.h +++ b/src/xrpld/ledger/detail/ApplyStateTable.h @@ -72,6 +72,7 @@ public: STTx const& tx, TER ter, std::optional const& deliver, + std::optional const& parentBatchId, bool isDryRun, beast::Journal j); diff --git a/src/xrpld/ledger/detail/ApplyViewImpl.cpp b/src/xrpld/ledger/detail/ApplyViewImpl.cpp index 74b71c8324..3fd9478b54 100644 --- a/src/xrpld/ledger/detail/ApplyViewImpl.cpp +++ b/src/xrpld/ledger/detail/ApplyViewImpl.cpp @@ -31,10 +31,11 @@ ApplyViewImpl::apply( OpenView& to, STTx const& tx, TER ter, + std::optional parentBatchId, bool isDryRun, beast::Journal j) { - return items_.apply(to, tx, ter, deliver_, isDryRun, j); + return items_.apply(to, tx, ter, deliver_, parentBatchId, isDryRun, j); } std::size_t diff --git a/src/xrpld/ledger/detail/OpenView.cpp b/src/xrpld/ledger/detail/OpenView.cpp index 5c62d8cef8..73e502a5e2 100644 --- a/src/xrpld/ledger/detail/OpenView.cpp +++ b/src/xrpld/ledger/detail/OpenView.cpp @@ -23,8 +23,6 @@ namespace ripple { -open_ledger_t const open_ledger{}; - class OpenView::txs_iter_impl : public txs_type::iter_base { private: @@ -124,7 +122,7 @@ OpenView::OpenView(ReadView const* base, std::shared_ptr hold) std::size_t OpenView::txCount() const { - return txs_.size(); + return baseTxCount_ + txs_.size(); } void @@ -269,7 +267,7 @@ OpenView::rawTxInsert( std::forward_as_tuple(key), std::forward_as_tuple(txn, metaData)); if (!result.second) - LogicError("rawTxInsert: duplicate TX id" + to_string(key)); + LogicError("rawTxInsert: duplicate TX id: " + to_string(key)); } } // namespace ripple diff --git a/src/xrpld/overlay/detail/PeerImp.cpp b/src/xrpld/overlay/detail/PeerImp.cpp index bca2cfd8c7..cb3a7a69f5 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 @@ -1282,6 +1283,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; @@ -2838,6 +2851,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) < diff --git a/src/xrpld/rpc/detail/TransactionSign.cpp b/src/xrpld/rpc/detail/TransactionSign.cpp index 9387aba505..a4454c6e8a 100644 --- a/src/xrpld/rpc/detail/TransactionSign.cpp +++ b/src/xrpld/rpc/detail/TransactionSign.cpp @@ -464,9 +464,6 @@ transactionPreProcessImpl( hasTicketSeq ? 0 : app.getTxQ().nextQueuableSeq(sle).value(); } - if (!tx_json.isMember(jss::Flags)) - tx_json[jss::Flags] = tfFullyCanonicalSig; - if (!tx_json.isMember(jss::NetworkID)) { auto const networkId = app.config().NETWORK_ID; @@ -749,6 +746,7 @@ transactionFormatResultImpl(Transaction::pointer tpTrans, unsigned apiVersion) [[nodiscard]] static XRPAmount getTxFee(Application const& app, Config const& config, Json::Value tx) { + auto const& ledger = app.openLedger().current(); // autofilling only needed in this function so that the `STParsedJSONObject` // parsing works properly it should not be modifying the actual `tx` object if (!tx.isMember(jss::Fee)) @@ -776,6 +774,9 @@ getTxFee(Application const& app, Config const& config, Json::Value tx) if (!tx[jss::Signers].isArray()) return config.FEES.reference_fee; + if (tx[jss::Signers].size() > STTx::maxMultiSigners(&ledger->rules())) + return config.FEES.reference_fee; + // check multi-signed signers for (auto& signer : tx[jss::Signers]) { @@ -804,6 +805,10 @@ getTxFee(Application const& app, Config const& config, Json::Value tx) try { STTx const& stTx = STTx(std::move(parsed.object.value())); + std::string reason; + if (!passesLocalChecks(stTx, reason)) + return config.FEES.reference_fee; + return calculateBaseFee(*app.openLedger().current(), stTx); } catch (std::exception& e) diff --git a/src/xrpld/rpc/handlers/Simulate.cpp b/src/xrpld/rpc/handlers/Simulate.cpp index 5f69c203ff..3c175883c5 100644 --- a/src/xrpld/rpc/handlers/Simulate.cpp +++ b/src/xrpld/rpc/handlers/Simulate.cpp @@ -342,6 +342,11 @@ doSimulate(RPC::JsonContext& context) return jvResult; } + if (stTx->getTxnType() == ttBATCH) + { + return RPC::make_error(rpcNOT_IMPL); + } + std::string reason; auto transaction = std::make_shared(stTx, reason, context.app); // Actually run the transaction through the transaction processor From 8b9e21e3f5735b22ba106e3033c7f4c10fea3606 Mon Sep 17 00:00:00 2001 From: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com> Date: Tue, 27 May 2025 19:32:25 +0100 Subject: [PATCH 05/14] docs: Update build instructions for Ubuntu 22.04+ (#5292) --- docs/build/environment.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 From cd777f79efd7e57224cd740e7c893b7fc26b9015 Mon Sep 17 00:00:00 2001 From: "Elliot." Date: Tue, 27 May 2025 12:11:13 -0700 Subject: [PATCH 06/14] docs: add -j $(nproc) to BUILD.md (#5288) This improves build times. --- BUILD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From cae5294b4e38e3c01c92fa5f32a59e6f15ad16f9 Mon Sep 17 00:00:00 2001 From: Bart Date: Tue, 27 May 2025 16:03:23 -0400 Subject: [PATCH 07/14] chore: Rename docs job (#5398) --- .github/workflows/doxygen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From be668ee26d6a9cc4e7b885e3c3cbb2fc7da7119b Mon Sep 17 00:00:00 2001 From: Michael Legleux Date: Tue, 27 May 2025 13:46:25 -0700 Subject: [PATCH 08/14] chore: Update CPP ref source (#5453) --- cmake/RippledDocs.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 \ From d71ce51901409c710735c4ceddcb7b319c14f8ef Mon Sep 17 00:00:00 2001 From: Vito Tumas <5780819+Tapanito@users.noreply.github.com> Date: Wed, 28 May 2025 12:30:03 +0200 Subject: [PATCH 09/14] feat: improve squelching configuration (#5438) This commit introduces the following changes: * Renames `vp_enable config` option to `vp_base_squelch_enable` to enable squelching for validators. * Removes `vp_squelch` config option which was used to configure whether to send squelch messages to peers or not. With this flag removed, if squelching is enabled, squelch messages will be sent. This was an option used for debugging. * Introduces a temporary `vp_base_squelch_max_trusted_peers` config option to change the max number of peers who are selected as validator message sources. This is a temporary option, which will be removed once a good value is found. * Adds a traffic counter to count the number of times peers ignored squelch messages and kept sending messages for squelched validators. * Moves the decision whether squelching is enabled and ready into Slot.h. --- src/test/overlay/compression_test.cpp | 11 +- src/test/overlay/reduce_relay_test.cpp | 205 +++++++++++++++++--- src/xrpld/core/Config.h | 25 ++- src/xrpld/core/detail/Config.cpp | 46 ++++- src/xrpld/overlay/Slot.h | 131 ++++++++++--- src/xrpld/overlay/detail/ConnectAttempt.cpp | 2 +- src/xrpld/overlay/detail/Handshake.cpp | 2 +- src/xrpld/overlay/detail/Handshake.h | 7 +- src/xrpld/overlay/detail/OverlayImpl.cpp | 22 ++- src/xrpld/overlay/detail/PeerImp.cpp | 40 ++-- src/xrpld/overlay/detail/PeerImp.h | 27 +-- src/xrpld/overlay/detail/TrafficCount.h | 4 + 12 files changed, 387 insertions(+), 135 deletions(-) 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/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/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 cb3a7a69f5..68894fb234 100644 --- a/src/xrpld/overlay/detail/PeerImp.cpp +++ b/src/xrpld/overlay/detail/PeerImp.cpp @@ -113,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() @@ -1733,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); @@ -2381,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); @@ -3005,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(), @@ -3040,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, @@ -3506,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}}, From 9e1fe9a85e800b11bd82e594eff5198f64c57c3c Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Wed, 28 May 2025 15:28:18 +0100 Subject: [PATCH 10/14] Fix: Improve handling of expired credentials in `VaultDeposit` (#5452) This change returns `tecEXPIRED` from VaultDeposit to allow the Transactor to remove the expired credentials. --- src/test/app/Vault_test.cpp | 96 ++++++++++++++++++++---- src/xrpld/app/misc/CredentialHelpers.cpp | 6 +- src/xrpld/ledger/detail/View.cpp | 19 ++++- 3 files changed, 100 insertions(+), 21 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 67cc3812df..5aab737669 100644 --- a/src/test/app/Vault_test.cpp +++ b/src/test/app/Vault_test.cpp @@ -2273,6 +2273,8 @@ class Vault_test : public beast::unit_test::suite env(pay(issuer, owner, asset(500))); env.trust(asset(1000), depositor); env(pay(issuer, depositor, asset(500))); + env.trust(asset(1000), charlie); + env(pay(issuer, charlie, asset(5))); env.close(); auto [tx, keylet] = vault.create( @@ -2362,7 +2364,7 @@ class Vault_test : public beast::unit_test::suite env(credentials::create(depositor, credIssuer1, credType)); env(credentials::accept(depositor, credIssuer1, credType)); env(credentials::create(charlie, credIssuer1, credType)); - env(credentials::accept(charlie, credIssuer1, credType)); + // charlie's credential not accepted env.close(); auto credSle = env.le(credKeylet); BEAST_EXPECT(credSle != nullptr); @@ -2376,7 +2378,7 @@ class Vault_test : public beast::unit_test::suite tx = vault.deposit( {.depositor = charlie, .id = keylet.key, .amount = asset(50)}); - env(tx, ter{tecINSUFFICIENT_FUNDS}); + env(tx, ter{tecNO_AUTH}); env.close(); } @@ -2384,6 +2386,8 @@ class Vault_test : public beast::unit_test::suite testcase("private vault depositor lost authorization"); env(credentials::deleteCred( credIssuer1, depositor, credIssuer1, credType)); + env(credentials::deleteCred( + credIssuer1, charlie, credIssuer1, credType)); env.close(); auto credSle = env.le(credKeylet); BEAST_EXPECT(credSle == nullptr); @@ -2396,18 +2400,84 @@ class Vault_test : public beast::unit_test::suite env.close(); } - { - testcase("private vault depositor new authorization"); - env(credentials::create(depositor, credIssuer2, credType)); - env(credentials::accept(depositor, credIssuer2, credType)); - env.close(); + auto const shares = [&env, keylet = keylet, this]() -> Asset { + auto const vault = env.le(keylet); + BEAST_EXPECT(vault != nullptr); + return MPTIssue(vault->at(sfShareMPTID)); + }(); - auto tx = vault.deposit( - {.depositor = depositor, - .id = keylet.key, - .amount = asset(50)}); - env(tx); - env.close(); + { + testcase("private vault expired authorization"); + uint32_t const closeTime = env.current() + ->info() + .parentCloseTime.time_since_epoch() + .count(); + { + auto tx0 = + credentials::create(depositor, credIssuer2, credType); + tx0[sfExpiration] = closeTime + 20; + env(tx0); + tx0 = credentials::create(charlie, credIssuer2, credType); + tx0[sfExpiration] = closeTime + 20; + env(tx0); + env.close(); + + env(credentials::accept(depositor, credIssuer2, credType)); + env(credentials::accept(charlie, credIssuer2, credType)); + env.close(); + } + + { + auto tx1 = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(50)}); + env(tx1); + env.close(); + + auto const tokenKeylet = keylet::mptoken( + shares.get().getMptID(), depositor.id()); + BEAST_EXPECT(env.le(tokenKeylet) != nullptr); + } + + { + // time advance + env.close(); + env.close(); + env.close(); + + auto const credsKeylet = + credentials::keylet(depositor, credIssuer2, credType); + BEAST_EXPECT(env.le(credsKeylet) != nullptr); + + auto tx2 = vault.deposit( + {.depositor = depositor, + .id = keylet.key, + .amount = asset(1)}); + env(tx2, ter{tecEXPIRED}); + env.close(); + + BEAST_EXPECT(env.le(credsKeylet) == nullptr); + } + + { + auto const credsKeylet = + credentials::keylet(charlie, credIssuer2, credType); + BEAST_EXPECT(env.le(credsKeylet) != nullptr); + auto const tokenKeylet = keylet::mptoken( + shares.get().getMptID(), charlie.id()); + BEAST_EXPECT(env.le(tokenKeylet) == nullptr); + + auto tx3 = vault.deposit( + {.depositor = charlie, + .id = keylet.key, + .amount = asset(2)}); + env(tx3, ter{tecEXPIRED}); + + env.close(); + BEAST_EXPECT(env.le(credsKeylet) == nullptr); + BEAST_EXPECT(env.le(tokenKeylet) == nullptr); + } } { diff --git a/src/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/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index aa6e2dda8f..e9499a287a 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -2391,8 +2391,19 @@ enforceMPTokenAuthorization( auto const keylet = keylet::mptoken(mptIssuanceID, account); auto const sleToken = view.read(keylet); // NOTE: might be null auto const maybeDomainID = sleIssuance->at(~sfDomainID); - bool const authorizedByDomain = maybeDomainID.has_value() && - verifyValidDomain(view, account, *maybeDomainID, j) == tesSUCCESS; + bool expired = false; + bool const authorizedByDomain = [&]() -> bool { + // NOTE: defensive here, shuld be checked in preclaim + if (!maybeDomainID.has_value()) + return false; // LCOV_EXCL_LINE + + auto const ter = verifyValidDomain(view, account, *maybeDomainID, j); + if (isTesSuccess(ter)) + return true; + if (ter == tecEXPIRED) + expired = true; + return false; + }(); if (!authorizedByDomain && sleToken == nullptr) { @@ -2403,14 +2414,14 @@ enforceMPTokenAuthorization( // 3. Account has all expired credentials (deleted in verifyValidDomain) // // Either way, return tecNO_AUTH and there is nothing else to do - return tecNO_AUTH; + return expired ? tecEXPIRED : tecNO_AUTH; } else if (!authorizedByDomain && maybeDomainID.has_value()) { // Found an MPToken but the account is not authorized and we expect // it to have been authorized by the domain. This could be because the // credentials used to create the MPToken have expired or been deleted. - return tecNO_AUTH; + return expired ? tecEXPIRED : tecNO_AUTH; } else if (!authorizedByDomain) { From 05105743e94f90100a5edae14d46d8b7e4af8b59 Mon Sep 17 00:00:00 2001 From: Mayukha Vadari Date: Thu, 29 May 2025 12:28:09 -0400 Subject: [PATCH 11/14] chore[tests]: improve env.meta usage (#5457) This commit changes the ledger close in env.meta to be conditional on if it hasn't already been closed (i.e. the current ledger doesn't have any transactions in it). This change will make it a bit easier to use, as it will still work if you close the ledger outside of this usage. Previously, if you accidentally closed the ledger outside of the meta function, it would segfault and it was incredibly difficult to debug. --- src/test/jtx/Env.h | 17 ++++++++++------- src/test/jtx/impl/Env.cpp | 7 ++++++- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/test/jtx/Env.h b/src/test/jtx/Env.h index de6b83362d..2b5397b903 100644 --- a/src/test/jtx/Env.h +++ b/src/test/jtx/Env.h @@ -589,13 +589,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/impl/Env.cpp b/src/test/jtx/impl/Env.cpp index ac00d3eed1..e45042e310 100644 --- a/src/test/jtx/impl/Env.cpp +++ b/src/test/jtx/impl/Env.cpp @@ -446,7 +446,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; } From dacecd24ba7949b6fea4b63186bf6789c5e3f9ff Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Thu, 29 May 2025 21:53:31 +0100 Subject: [PATCH 12/14] Fix unit build error (#5459) This change fixes the issue that there is a `using namespace` statement inside a namespace scope. --- src/test/app/Vault_test.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/test/app/Vault_test.cpp b/src/test/app/Vault_test.cpp index 5aab737669..ccac0e2819 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 Date: Fri, 30 May 2025 14:46:08 +0200 Subject: [PATCH 13/14] docs: update example keyserver host in SECURITY.md (#5460) --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 0a34b5c691d3a205146a706b460473b3bfb84205 Mon Sep 17 00:00:00 2001 From: Shawn Xie <35279399+shawnxie999@users.noreply.github.com> Date: Fri, 30 May 2025 13:24:48 -0400 Subject: [PATCH 14/14] Add support for XLS-81 Permissioned DEX (#5404) Modified transactions: - OfferCreate - Payment Modified RPCs: - book_changes - subscribe - book_offers - ripple_path_find - path_find Spec: https://github.com/XRPLF/XRPL-Standards/pull/281 --- include/xrpl/protocol/Book.h | 40 +- include/xrpl/protocol/ErrorCodes.h | 5 +- include/xrpl/protocol/LedgerFormats.h | 1 + include/xrpl/protocol/TxFlags.h | 4 +- include/xrpl/protocol/UintTypes.h | 3 + include/xrpl/protocol/detail/features.macro | 1 + .../xrpl/protocol/detail/ledger_entries.macro | 3 + include/xrpl/protocol/detail/sfields.macro | 2 + .../xrpl/protocol/detail/transactions.macro | 2 + src/libxrpl/protocol/Book.cpp | 2 +- src/libxrpl/protocol/ErrorCodes.cpp | 3 +- src/libxrpl/protocol/Indexes.cpp | 19 +- src/libxrpl/protocol/InnerObjectFormats.cpp | 7 + src/test/app/AMMExtended_test.cpp | 1 + src/test/app/CrossingLimits_test.cpp | 7 +- src/test/app/DeliverMin_test.cpp | 4 +- src/test/app/Discrepancy_test.cpp | 3 +- src/test/app/Flow_test.cpp | 15 +- src/test/app/Freeze_test.cpp | 8 +- src/test/app/Offer_test.cpp | 39 +- src/test/app/Path_test.cpp | 971 ++++++++-- src/test/app/PayStrand_test.cpp | 40 +- src/test/app/PermissionedDEX_test.cpp | 1595 +++++++++++++++++ src/test/app/SetAuth_test.cpp | 3 +- src/test/app/TheoreticalQuality_test.cpp | 1 + src/test/app/TrustAndBalance_test.cpp | 3 +- src/test/jtx.h | 2 + src/test/jtx/domain.h | 45 + src/test/jtx/impl/domain.cpp | 36 + src/test/jtx/impl/paths.cpp | 15 + src/test/jtx/impl/permissioned_dex.cpp | 85 + src/test/jtx/permissioned_dex.h | 51 + src/test/ledger/BookDirs_test.cpp | 18 +- src/test/ledger/Directory_test.cpp | 6 +- src/test/ledger/Invariants_test.cpp | 231 ++- src/test/ledger/PaymentSandbox_test.cpp | 3 +- src/test/protocol/Issue_test.cpp | 489 ++++- src/test/rpc/BookChanges_test.cpp | 53 +- src/test/rpc/Book_test.cpp | 292 ++- src/test/rpc/GatewayBalances_test.cpp | 5 +- src/test/rpc/JSONRPC_test.cpp | 22 + src/test/rpc/NoRipple_test.cpp | 3 +- src/test/rpc/Subscribe_test.cpp | 55 + src/xrpld/app/ledger/OrderBookDB.cpp | 78 +- src/xrpld/app/ledger/OrderBookDB.h | 18 +- src/xrpld/app/misc/PermissionedDEXHelpers.cpp | 88 + src/xrpld/app/misc/PermissionedDEXHelpers.h | 43 + src/xrpld/app/paths/Flow.cpp | 2 + src/xrpld/app/paths/Flow.h | 1 + src/xrpld/app/paths/PathRequest.cpp | 18 + src/xrpld/app/paths/PathRequest.h | 3 + src/xrpld/app/paths/Pathfinder.cpp | 12 +- src/xrpld/app/paths/Pathfinder.h | 2 + src/xrpld/app/paths/RippleCalc.cpp | 3 + src/xrpld/app/paths/RippleCalc.h | 2 + src/xrpld/app/paths/detail/BookStep.cpp | 6 +- src/xrpld/app/paths/detail/PaySteps.cpp | 7 + src/xrpld/app/paths/detail/Steps.h | 7 + src/xrpld/app/tx/detail/AMMCreate.cpp | 2 +- src/xrpld/app/tx/detail/CashCheck.cpp | 1 + src/xrpld/app/tx/detail/CreateOffer.cpp | 141 +- src/xrpld/app/tx/detail/CreateOffer.h | 18 +- src/xrpld/app/tx/detail/InvariantCheck.cpp | 83 + src/xrpld/app/tx/detail/InvariantCheck.h | 26 +- src/xrpld/app/tx/detail/OfferStream.cpp | 14 + src/xrpld/app/tx/detail/Payment.cpp | 17 + src/xrpld/app/tx/detail/XChainBridge.cpp | 1 + src/xrpld/ledger/detail/View.cpp | 22 +- src/xrpld/rpc/BookChanges.h | 27 +- src/xrpld/rpc/detail/TransactionSign.cpp | 18 + src/xrpld/rpc/handlers/BookOffers.cpp | 18 +- src/xrpld/rpc/handlers/Subscribe.cpp | 14 + src/xrpld/rpc/handlers/Unsubscribe.cpp | 14 + 73 files changed, 4591 insertions(+), 308 deletions(-) create mode 100644 src/test/app/PermissionedDEX_test.cpp create mode 100644 src/test/jtx/domain.h create mode 100644 src/test/jtx/impl/domain.cpp create mode 100644 src/test/jtx/impl/permissioned_dex.cpp create mode 100644 src/test/jtx/permissioned_dex.h create mode 100644 src/xrpld/app/misc/PermissionedDEXHelpers.cpp create mode 100644 src/xrpld/app/misc/PermissionedDEXHelpers.h diff --git a/include/xrpl/protocol/Book.h b/include/xrpl/protocol/Book.h index 0fcff0df80..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 } /** @} */ @@ -126,9 +146,11 @@ 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: hash() = default; @@ -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; } }; 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/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 3edd656213..58ebbe69cc 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/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 31c3ffa205..80f6a78727 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -98,9 +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; diff --git a/include/xrpl/protocol/UintTypes.h b/include/xrpl/protocol/UintTypes.h index d6cdc9350e..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(); diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index e61d3a8005..61828d4758 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -32,6 +32,7 @@ // If you add an amendment here, then do not forget to increment `numFeatures` // in include/xrpl/protocol/Feature.h. +XRPL_FEATURE(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) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index a902b32026..06fe9d45bd 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -188,6 +188,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. @@ -249,6 +250,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 dbef597ea0..2053ac94bb 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -360,6 +360,7 @@ UNTYPED_SFIELD(sfPriceData, OBJECT, 32) UNTYPED_SFIELD(sfCredential, OBJECT, 33) UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34) UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) +UNTYPED_SFIELD(sfBook, OBJECT, 36) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -375,6 +376,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) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 5d5faae505..6992410e4c 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -38,6 +38,7 @@ TRANSACTION(ttPAYMENT, 0, Payment, Delegation::delegatable, ({ {sfDestinationTag, soeOPTIONAL}, {sfDeliverMin, soeOPTIONAL, soeMPTSupported}, {sfCredentialIDs, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL}, })) /** This transaction type creates an escrow object. */ @@ -93,6 +94,7 @@ TRANSACTION(ttOFFER_CREATE, 7, OfferCreate, Delegation::delegatable, ({ {sfTakerGets, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, {sfOfferSequence, soeOPTIONAL}, + {sfDomainID, soeOPTIONAL}, })) /** This transaction type cancels existing offers to trade one asset for another. */ 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 2426092d13..486945992a 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -117,12 +117,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 3f3b1e00c0..2de5e6624e 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -165,6 +165,13 @@ InnerObjectFormats::InnerObjectFormats() {sfSigningPubKey, soeOPTIONAL}, {sfTxnSignature, soeOPTIONAL}, {sfSigners, soeOPTIONAL}}); + + add(sfBook.jsonName, + sfBook.getCode(), + { + {sfBookDirectory, soeREQUIRED}, + {sfBookNode, soeREQUIRED}, + }); } InnerObjectFormats const& 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/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/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/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/jtx.h b/src/test/jtx.h index 2e4764a403..fa67780cbd 100644 --- a/src/test/jtx.h +++ b/src/test/jtx.h @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,7 @@ #include #include #include +#include #include #include #include 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/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/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/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/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 7ceb76504d..76fcb34c7f 100644 --- a/src/test/ledger/Invariants_test.cpp +++ b/src/test/ledger/Invariants_test.cpp @@ -1046,6 +1046,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() { @@ -1153,36 +1177,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 { @@ -1201,12 +1204,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 { @@ -1236,12 +1239,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 { @@ -1271,12 +1274,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 { @@ -1300,6 +1303,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}); + } + public: void run() override @@ -1318,6 +1490,7 @@ public: testValidNewAccountRoot(); testNFTokenPageInvariants(); testPermissionedDomainInvariants(); + testPermissionedDEX(); } }; 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/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/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 22c7dfd1dc..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__, 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/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/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/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/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/detail/AMMCreate.cpp b/src/xrpld/app/tx/detail/AMMCreate.cpp index 95cb5bf2e6..f0ccc6f298 100644 --- a/src/xrpld/app/tx/detail/AMMCreate.cpp +++ b/src/xrpld/app/tx/detail/AMMCreate.cpp @@ -329,7 +329,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/CashCheck.cpp b/src/xrpld/app/tx/detail/CashCheck.cpp index cccda83a68..0f1d08689c 100644 --- a/src/xrpld/app/tx/detail/CashCheck.cpp +++ b/src/xrpld/app/tx/detail/CashCheck.cpp @@ -451,6 +451,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 d9bd57ec3c..7ccecd7a47 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) { @@ -42,6 +46,16 @@ CreateOffer::makeTxConsequences(PreflightContext const& ctx) NotTEC CreateOffer::preflight(PreflightContext const& ctx) { + if (ctx.tx.isFieldPresent(sfDomainID) && + !ctx.rules.enabled(featurePermissionedDEX)) + return temDISABLED; + + // Permissioned offers should use the PE (which must be enabled by + // featureFlowCross amendment) + if (ctx.rules.enabled(featurePermissionedDEX) && + !ctx.rules.enabled(featureFlowCross)) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -56,6 +70,12 @@ CreateOffer::preflight(PreflightContext const& ctx) return temINVALID_FLAG; } + if (!ctx.rules.enabled(featurePermissionedDEX) && tx.isFlag(tfHybrid)) + return temINVALID_FLAG; + + if (tx.isFlag(tfHybrid) && !tx.isFieldPresent(sfDomainID)) + return temINVALID_FLAG; + bool const bImmediateOrCancel(uTxFlags & tfImmediateOrCancel); bool const bFillOrKill(uTxFlags & tfFillOrKill); @@ -198,6 +218,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; } @@ -367,7 +396,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_); @@ -375,7 +404,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_); @@ -383,7 +412,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_); @@ -551,7 +580,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_); @@ -708,7 +737,8 @@ std::pair CreateOffer::flowCross( PaymentSandbox& psb, PaymentSandbox& psbCancel, - Amounts const& takerAmount) + Amounts const& takerAmount, + std::optional const& domainID) { try { @@ -805,6 +835,7 @@ CreateOffer::flowCross( offerCrossing, threshold, sendMax, + domainID, j_); // If stale offers were found remove them. @@ -907,13 +938,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; @@ -950,6 +986,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) { @@ -961,9 +1045,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]; @@ -1080,7 +1166,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. @@ -1222,21 +1309,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) @@ -1259,6 +1364,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 35808c78fe..9b35062d8a 100644 --- a/src/xrpld/app/tx/detail/CreateOffer.h +++ b/src/xrpld/app/tx/detail/CreateOffer.h @@ -121,18 +121,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 aa1464ec2a..729f69a03b 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -1580,4 +1580,87 @@ 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; +} + } // namespace ripple diff --git a/src/xrpld/app/tx/detail/InvariantCheck.h b/src/xrpld/app/tx/detail/InvariantCheck.h index 6819780114..fdde8427fb 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 { @@ -618,6 +619,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&); +}; + // additional invariant checks can be declared above and then added to this // tuple using InvariantChecks = std::tuple< @@ -637,7 +660,8 @@ using InvariantChecks = std::tuple< NFTokenCountTracking, ValidClawback, ValidMPTIssuance, - ValidPermissionedDomain>; + ValidPermissionedDomain, + ValidPermissionedDEX>; /** * @brief get a tuple of all invariant checks 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 a97e472841..b597af570a 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 @@ -71,6 +72,10 @@ Payment::preflight(PreflightContext const& ctx) !ctx.rules.enabled(featureCredentials)) return temDISABLED; + if (ctx.tx.isFieldPresent(sfDomainID) && + !ctx.rules.enabled(featurePermissionedDEX)) + return temDISABLED; + if (auto const ret = preflight1(ctx); !isTesSuccess(ret)) return ret; @@ -357,6 +362,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; } @@ -458,6 +474,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/XChainBridge.cpp b/src/xrpld/app/tx/detail/XChainBridge.cpp index 5fa03557e5..6ca049ee66 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/ledger/detail/View.cpp b/src/xrpld/ledger/detail/View.cpp index e9499a287a..3978d26e56 100644 --- a/src/xrpld/ledger/detail/View.cpp +++ b/src/xrpld/ledger/detail/View.cpp @@ -37,7 +37,6 @@ #include #include -#include #include #include @@ -1484,6 +1483,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); 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 a4454c6e8a..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)) { 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/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.