From 7724cca384487bf4cdc17fa34ff8d5372a54b110 Mon Sep 17 00:00:00 2001 From: Scott Schurr Date: Thu, 18 Oct 2018 18:43:02 -0700 Subject: [PATCH] Implement enhanced Ticket support: Tickets are a mechanism to allow for the "out-of-order" execution of transactions on the XRP Ledger. This commit, if merged, reworks the existing support for tickets and introduces support for 'ticket batching', completing the feature set needed for tickets. The code is gated under the newly-introduced `TicketBatch` amendment and the `Tickets` amendment, which is not presently active on the network, is being removed. The specification for this change can be found at: https://github.com/xrp-community/standards-drafts/issues/16 --- Builds/CMake/RippledCore.cmake | 3 +- cfg/rippled-example.cfg | 8 - src/ripple/app/ledger/LedgerMaster.h | 13 +- src/ripple/app/ledger/impl/LedgerMaster.cpp | 8 +- src/ripple/app/ledger/impl/LedgerToJson.cpp | 15 +- src/ripple/app/ledger/impl/LocalTxs.cpp | 36 +- src/ripple/app/misc/CanonicalTXSet.cpp | 105 +- src/ripple/app/misc/CanonicalTXSet.h | 77 +- src/ripple/app/misc/FeeEscalation.md | 2 - src/ripple/app/misc/NetworkOPs.cpp | 9 +- src/ripple/app/misc/TxQ.h | 228 ++- src/ripple/app/misc/impl/LoadFeeTrack.cpp | 4 +- src/ripple/app/misc/impl/TxQ.cpp | 1507 ++++++++------ src/ripple/app/tx/README.md | 2 - src/ripple/app/tx/applySteps.h | 205 +- src/ripple/app/tx/impl/CancelCheck.cpp | 4 +- src/ripple/app/tx/impl/CancelCheck.h | 2 + src/ripple/app/tx/impl/CancelOffer.h | 2 + src/ripple/app/tx/impl/CancelTicket.cpp | 95 - src/ripple/app/tx/impl/CancelTicket.h | 45 - src/ripple/app/tx/impl/CashCheck.cpp | 4 +- src/ripple/app/tx/impl/CashCheck.h | 2 + src/ripple/app/tx/impl/Change.cpp | 4 +- src/ripple/app/tx/impl/Change.h | 2 + src/ripple/app/tx/impl/CreateCheck.cpp | 6 +- src/ripple/app/tx/impl/CreateCheck.h | 2 + src/ripple/app/tx/impl/CreateOffer.cpp | 22 +- src/ripple/app/tx/impl/CreateOffer.h | 7 +- src/ripple/app/tx/impl/CreateTicket.cpp | 159 +- src/ripple/app/tx/impl/CreateTicket.h | 43 + src/ripple/app/tx/impl/DeleteAccount.cpp | 23 +- src/ripple/app/tx/impl/DeleteAccount.h | 8 +- src/ripple/app/tx/impl/DepositPreauth.h | 2 + src/ripple/app/tx/impl/Escrow.cpp | 17 +- src/ripple/app/tx/impl/Escrow.h | 10 +- src/ripple/app/tx/impl/PayChan.cpp | 20 +- src/ripple/app/tx/impl/PayChan.h | 12 + src/ripple/app/tx/impl/Payment.cpp | 23 +- src/ripple/app/tx/impl/Payment.h | 6 +- src/ripple/app/tx/impl/SetAccount.cpp | 35 +- src/ripple/app/tx/impl/SetAccount.h | 6 +- src/ripple/app/tx/impl/SetRegularKey.h | 8 +- src/ripple/app/tx/impl/SetSignerList.cpp | 18 +- src/ripple/app/tx/impl/SetSignerList.h | 11 +- src/ripple/app/tx/impl/SetTrust.h | 2 + src/ripple/app/tx/impl/Transactor.cpp | 255 ++- src/ripple/app/tx/impl/Transactor.h | 36 +- src/ripple/app/tx/impl/applySteps.cpp | 446 +++-- src/ripple/ledger/ReadView.h | 8 +- src/ripple/proto/org/xrpl/rpc/v1/common.proto | 15 + .../org/xrpl/rpc/v1/get_account_info.proto | 14 +- .../org/xrpl/rpc/v1/ledger_objects.proto | 26 +- .../proto/org/xrpl/rpc/v1/transaction.proto | 13 +- src/ripple/protocol/Feature.h | 9 +- src/ripple/protocol/Indexes.h | 9 +- src/ripple/protocol/SField.h | 3 +- src/ripple/protocol/STObject.h | 7 + src/ripple/protocol/STTx.h | 14 +- src/ripple/protocol/SeqProxy.h | 170 ++ src/ripple/protocol/TER.h | 9 +- src/ripple/protocol/TxFormats.h | 2 +- src/ripple/protocol/impl/Feature.cpp | 8 +- src/ripple/protocol/impl/Indexes.cpp | 22 +- src/ripple/protocol/impl/LedgerFormats.cpp | 7 +- src/ripple/protocol/impl/SField.cpp | 6 +- src/ripple/protocol/impl/STTx.cpp | 20 +- src/ripple/protocol/impl/TER.cpp | 3 + src/ripple/protocol/impl/TxFormats.cpp | 29 +- src/ripple/protocol/jss.h | 7 +- src/ripple/rpc/handlers/AccountInfo.cpp | 87 +- src/ripple/rpc/handlers/Fee1.cpp | 9 +- src/ripple/rpc/handlers/LedgerEntry.cpp | 25 + src/ripple/rpc/impl/GRPCHelpers.cpp | 136 +- src/ripple/rpc/impl/GRPCHelpers.h | 2 +- src/ripple/rpc/impl/RPCHelpers.cpp | 2 +- src/ripple/rpc/impl/TransactionSign.cpp | 19 +- src/test/app/AccountDelete_test.cpp | 66 +- src/test/app/Check_test.cpp | 103 + src/test/app/DepositAuth_test.cpp | 37 +- src/test/app/Escrow_test.cpp | 161 +- src/test/app/Flow_test.cpp | 30 +- src/test/app/MultiSign_test.cpp | 47 +- src/test/app/Offer_test.cpp | 260 ++- src/test/app/PayChan_test.cpp | 253 ++- src/test/app/SetRegularKey_test.cpp | 53 + src/test/app/SetTrust_test.cpp | 35 + src/test/app/Ticket_test.cpp | 986 ++++++--- src/test/app/TxQ_test.cpp | 1756 ++++++++++++++--- src/test/jtx/Env_test.cpp | 11 +- src/test/jtx/impl/ticket.cpp | 26 +- src/test/jtx/ticket.h | 64 +- src/test/protocol/SeqProxy_test.cpp | 240 +++ src/test/rpc/AccountObjects_test.cpp | 7 +- src/test/rpc/AccountSet_test.cpp | 41 + src/test/rpc/AccountTx_test.cpp | 56 +- src/test/rpc/Fee_test.cpp | 11 +- src/test/rpc/LedgerData_test.cpp | 4 +- src/test/rpc/LedgerRPC_test.cpp | 121 +- src/test/rpc/NoRippleCheck_test.cpp | 9 +- src/test/rpc/RobustTransaction_test.cpp | 2 + src/test/rpc/Submit_test.cpp | 5 +- 101 files changed, 6337 insertions(+), 2287 deletions(-) delete mode 100644 src/ripple/app/tx/impl/CancelTicket.cpp delete mode 100644 src/ripple/app/tx/impl/CancelTicket.h create mode 100644 src/ripple/protocol/SeqProxy.h create mode 100644 src/test/protocol/SeqProxy_test.cpp diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 83598bd61..97d48bde8 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -237,6 +237,7 @@ install ( src/ripple/protocol/STVector256.h src/ripple/protocol/SecretKey.h src/ripple/protocol/Seed.h + src/ripple/protocol/SeqProxy.h src/ripple/protocol/Serializer.h src/ripple/protocol/Sign.h src/ripple/protocol/SystemParameters.h @@ -411,7 +412,6 @@ target_sources (rippled PRIVATE src/ripple/app/tx/impl/BookTip.cpp src/ripple/app/tx/impl/CancelCheck.cpp src/ripple/app/tx/impl/CancelOffer.cpp - src/ripple/app/tx/impl/CancelTicket.cpp src/ripple/app/tx/impl/CashCheck.cpp src/ripple/app/tx/impl/Change.cpp src/ripple/app/tx/impl/CreateCheck.cpp @@ -889,6 +889,7 @@ target_sources (rippled PRIVATE src/test/protocol/STValidation_test.cpp src/test/protocol/SecretKey_test.cpp src/test/protocol/Seed_test.cpp + src/test/protocol/SeqProxy_test.cpp src/test/protocol/TER_test.cpp src/test/protocol/digest_test.cpp src/test/protocol/types_test.cpp diff --git a/cfg/rippled-example.cfg b/cfg/rippled-example.cfg index b2b3c03f3..907eb0eef 100644 --- a/cfg/rippled-example.cfg +++ b/cfg/rippled-example.cfg @@ -506,14 +506,6 @@ # than the original transaction's fee, or meet the current open # ledger fee to be considered. Default: 25. # -# multi_txn_percent = -# -# If a client submits multiple transactions (different sequence -# numbers), later transactions must pay a fee at least -# percent higher than the transaction with the previous sequence -# number. -# Default: -90. -# # minimum_escalation_multiplier = # # At ledger close time, the median fee level of the transactions diff --git a/src/ripple/app/ledger/LedgerMaster.h b/src/ripple/app/ledger/LedgerMaster.h index 8c3f81254..ea4ba5763 100644 --- a/src/ripple/app/ledger/LedgerMaster.h +++ b/src/ripple/app/ledger/LedgerMaster.h @@ -148,14 +148,13 @@ public: void applyHeldTransactions(); - /** Get all the transactions held for a particular account. - This is normally called when a transaction for that - account is successfully applied to the open - ledger so those transactions can be resubmitted without - waiting for ledger close. + /** Get the next transaction held for a particular account if any. + This is normally called when a transaction for that account is + successfully applied to the open ledger so the next transaction + can be resubmitted without waiting for ledger close. */ - std::vector> - pruneHeldTransactions(AccountID const& account, std::uint32_t const seq); + std::shared_ptr + popAcctTransaction(std::shared_ptr const& tx); /** Get a ledger's hash by sequence number using the cache */ diff --git a/src/ripple/app/ledger/impl/LedgerMaster.cpp b/src/ripple/app/ledger/impl/LedgerMaster.cpp index c72f3349b..4b07d7d9e 100644 --- a/src/ripple/app/ledger/impl/LedgerMaster.cpp +++ b/src/ripple/app/ledger/impl/LedgerMaster.cpp @@ -543,14 +543,12 @@ LedgerMaster::applyHeldTransactions() mHeldTransactions.reset(app_.openLedger().current()->info().parentHash); } -std::vector> -LedgerMaster::pruneHeldTransactions( - AccountID const& account, - std::uint32_t const seq) +std::shared_ptr +LedgerMaster::popAcctTransaction(std::shared_ptr const& tx) { std::lock_guard sl(m_mutex); - return mHeldTransactions.prune(account, seq); + return mHeldTransactions.popAcctTransaction(tx); } LedgerIndex diff --git a/src/ripple/app/ledger/impl/LedgerToJson.cpp b/src/ripple/app/ledger/impl/LedgerToJson.cpp index c681f7406..1a683e32e 100644 --- a/src/ripple/app/ledger/impl/LedgerToJson.cpp +++ b/src/ripple/app/ledger/impl/LedgerToJson.cpp @@ -228,15 +228,12 @@ fillJsonQueue(Object& json, LedgerFill const& fill) txJson[jss::fee_level] = to_string(tx.feeLevel); if (tx.lastValid) txJson[jss::LastLedgerSequence] = *tx.lastValid; - if (tx.consequences) - { - txJson[jss::fee] = to_string(tx.consequences->fee); - auto spend = tx.consequences->potentialSpend + tx.consequences->fee; - txJson[jss::max_spend_drops] = to_string(spend); - auto authChanged = - tx.consequences->category == TxConsequences::blocker; - txJson[jss::auth_change] = authChanged; - } + + txJson[jss::fee] = to_string(tx.consequences.fee()); + auto const spend = + tx.consequences.potentialSpend() + tx.consequences.fee(); + txJson[jss::max_spend_drops] = to_string(spend); + txJson[jss::auth_change] = tx.consequences.isBlocker(); txJson[jss::account] = to_string(tx.account); txJson["retries_remaining"] = tx.retriesRemaining; diff --git a/src/ripple/app/ledger/impl/LocalTxs.cpp b/src/ripple/app/ledger/impl/LocalTxs.cpp index d7b9cf548..afee5e2d4 100644 --- a/src/ripple/app/ledger/impl/LocalTxs.cpp +++ b/src/ripple/app/ledger/impl/LocalTxs.cpp @@ -63,7 +63,7 @@ public: , m_expire(index + holdLedgers) , m_id(txn->getTransactionID()) , m_account(txn->getAccountID(sfAccount)) - , m_seq(txn->getSequence()) + , m_seqProxy(txn->getSeqProxy()) { if (txn->isFieldPresent(sfLastLedgerSequence)) m_expire = @@ -76,10 +76,10 @@ public: return m_id; } - std::uint32_t - getSeq() const + SeqProxy + getSeqProxy() const { - return m_seq; + return m_seqProxy; } bool @@ -105,7 +105,7 @@ private: LedgerIndex m_expire; uint256 m_id; AccountID m_account; - std::uint32_t m_seq; + SeqProxy m_seqProxy; }; //------------------------------------------------------------------------------ @@ -138,7 +138,6 @@ public: for (auto const& it : m_txns) tset.insert(it.getTX()); } - return tset; } @@ -156,11 +155,28 @@ public: if (view.txExists(txn.getID())) return true; - std::shared_ptr sle = - view.read(keylet::account(txn.getAccount())); - if (!sle) + AccountID const acctID = txn.getAccount(); + auto const sleAcct = view.read(keylet::account(acctID)); + + if (!sleAcct) return false; - return sle->getFieldU32(sfSequence) > txn.getSeq(); + + SeqProxy const acctSeq = + SeqProxy::sequence(sleAcct->getFieldU32(sfSequence)); + SeqProxy const seqProx = txn.getSeqProxy(); + + if (seqProx.isSeq()) + return acctSeq > seqProx; // Remove tefPAST_SEQ + + if (seqProx.isTicket() && acctSeq.value() <= seqProx.value()) + // Keep ticket from the future. Note, however, that the + // transaction will not be held indefinitely since LocalTxs + // will only hold a transaction for a maximum of 5 ledgers. + return false; + + // Ticket should have been created by now. Remove if ticket + // does not exist. + return !view.exists(keylet::ticket(acctID, seqProx)); }); } diff --git a/src/ripple/app/misc/CanonicalTXSet.cpp b/src/ripple/app/misc/CanonicalTXSet.cpp index 6d7b0d796..a9fcd17f0 100644 --- a/src/ripple/app/misc/CanonicalTXSet.cpp +++ b/src/ripple/app/misc/CanonicalTXSet.cpp @@ -18,80 +18,25 @@ //============================================================================== #include -#include namespace ripple { bool -CanonicalTXSet::Key::operator<(Key const& rhs) const +operator<(CanonicalTXSet::Key const& lhs, CanonicalTXSet::Key const& rhs) { - if (mAccount < rhs.mAccount) + if (lhs.account_ < rhs.account_) return true; - if (mAccount > rhs.mAccount) + if (lhs.account_ > rhs.account_) return false; - if (mSeq < rhs.mSeq) + if (lhs.seqProxy_ < rhs.seqProxy_) return true; - if (mSeq > rhs.mSeq) + if (lhs.seqProxy_ > rhs.seqProxy_) return false; - return mTXid < rhs.mTXid; -} - -bool -CanonicalTXSet::Key::operator>(Key const& rhs) const -{ - if (mAccount > rhs.mAccount) - return true; - - if (mAccount < rhs.mAccount) - return false; - - if (mSeq > rhs.mSeq) - return true; - - if (mSeq < rhs.mSeq) - return false; - - return mTXid > rhs.mTXid; -} - -bool -CanonicalTXSet::Key::operator<=(Key const& rhs) const -{ - if (mAccount < rhs.mAccount) - return true; - - if (mAccount > rhs.mAccount) - return false; - - if (mSeq < rhs.mSeq) - return true; - - if (mSeq > rhs.mSeq) - return false; - - return mTXid <= rhs.mTXid; -} - -bool -CanonicalTXSet::Key::operator>=(Key const& rhs) const -{ - if (mAccount > rhs.mAccount) - return true; - - if (mAccount < rhs.mAccount) - return false; - - if (mSeq > rhs.mSeq) - return true; - - if (mSeq < rhs.mSeq) - return false; - - return mTXid >= rhs.mTXid; + return lhs.txId_ < rhs.txId_; } uint256 @@ -108,28 +53,36 @@ CanonicalTXSet::insert(std::shared_ptr const& txn) { map_.insert(std::make_pair( Key(accountKey(txn->getAccountID(sfAccount)), - txn->getSequence(), + txn->getSeqProxy(), txn->getTransactionID()), txn)); } -std::vector> -CanonicalTXSet::prune(AccountID const& account, std::uint32_t const seq) +std::shared_ptr +CanonicalTXSet::popAcctTransaction(std::shared_ptr const& tx) { - auto effectiveAccount = accountKey(account); + // Determining the next viable transaction for an account with Tickets: + // + // 1. Prioritize transactions with Sequences over transactions with + // Tickets. + // + // 2. Don't worry about consecutive Sequence numbers. Creating Tickets + // can introduce a discontinuity in Sequence numbers. + // + // 3. After handling all transactions with Sequences, return Tickets + // with the lowest Ticket ID first. + std::shared_ptr result; + uint256 const effectiveAccount{accountKey(tx->getAccountID(sfAccount))}; - Key keyLow(effectiveAccount, seq, beast::zero); - Key keyHigh(effectiveAccount, seq + 1, beast::zero); + Key const after(effectiveAccount, tx->getSeqProxy(), beast::zero); + auto const itrNext{map_.lower_bound(after)}; + if (itrNext != map_.end() && + itrNext->first.getAccount() == effectiveAccount) + { + result = std::move(itrNext->second); + map_.erase(itrNext); + } - auto range = boost::make_iterator_range( - map_.lower_bound(keyLow), map_.lower_bound(keyHigh)); - auto txRange = boost::adaptors::transform( - range, [](auto const& p) { return p.second; }); - - std::vector> result( - txRange.begin(), txRange.end()); - - map_.erase(range.begin(), range.end()); return result; } diff --git a/src/ripple/app/misc/CanonicalTXSet.h b/src/ripple/app/misc/CanonicalTXSet.h index f85bd3fd0..d0dfd97e3 100644 --- a/src/ripple/app/misc/CanonicalTXSet.h +++ b/src/ripple/app/misc/CanonicalTXSet.h @@ -22,6 +22,7 @@ #include #include +#include namespace ripple { @@ -29,7 +30,7 @@ namespace ripple { "Canonical" refers to the order in which transactions are applied. - - Puts transactions from the same account in sequence order + - Puts transactions from the same account in SeqProxy order */ // VFALCO TODO rename to SortedTxSet @@ -39,43 +40,65 @@ private: class Key { public: - Key(uint256 const& account, std::uint32_t seq, uint256 const& id) - : mAccount(account), mTXid(id), mSeq(seq) + Key(uint256 const& account, SeqProxy seqProx, uint256 const& id) + : account_(account), txId_(id), seqProxy_(seqProx) { } - bool - operator<(Key const& rhs) const; - bool - operator>(Key const& rhs) const; - bool - operator<=(Key const& rhs) const; - bool - operator>=(Key const& rhs) const; + friend bool + operator<(Key const& lhs, Key const& rhs); - bool - operator==(Key const& rhs) const + inline friend bool + operator>(Key const& lhs, Key const& rhs) { - return mTXid == rhs.mTXid; + return rhs < lhs; } - bool - operator!=(Key const& rhs) const + + inline friend bool + operator<=(Key const& lhs, Key const& rhs) { - return mTXid != rhs.mTXid; + return !(lhs > rhs); + } + + inline friend bool + operator>=(Key const& lhs, Key const& rhs) + { + return !(lhs < rhs); + } + + inline friend bool + operator==(Key const& lhs, Key const& rhs) + { + return lhs.txId_ == rhs.txId_; + } + + inline friend bool + operator!=(Key const& lhs, Key const& rhs) + { + return !(lhs == rhs); + } + + uint256 const& + getAccount() const + { + return account_; } uint256 const& getTXID() const { - return mTXid; + return txId_; } private: - uint256 mAccount; - uint256 mTXid; - std::uint32_t mSeq; + uint256 account_; + uint256 txId_; + SeqProxy seqProxy_; }; + friend bool + operator<(Key const& lhs, Key const& rhs); + // Calculate the salted key for the given account uint256 accountKey(AccountID const& account); @@ -92,10 +115,16 @@ public: void insert(std::shared_ptr const& txn); - std::vector> - prune(AccountID const& account, std::uint32_t const seq); + // Pops the next transaction on account that follows seqProx in the + // sort order. Normally called when a transaction is successfully + // applied to the open ledger so the next transaction can be resubmitted + // without waiting for ledger close. + // + // The return value is often null, when an account has no more + // transactions. + std::shared_ptr + popAcctTransaction(std::shared_ptr const& tx); - // VFALCO TODO remove this function void reset(LedgerHash const& salt) { diff --git a/src/ripple/app/misc/FeeEscalation.md b/src/ripple/app/misc/FeeEscalation.md index 65dffceb8..4e1951931 100644 --- a/src/ripple/app/misc/FeeEscalation.md +++ b/src/ripple/app/misc/FeeEscalation.md @@ -130,8 +130,6 @@ transactions in the queue for that account, it will be considered for the queue if it meets these additional criteria: * the account has fewer than [10](#other-constants) transactions already in the queue. - * it pays a [fee level](#fee-level) that is greater than 10% of the - fee level for the transaction with the previous sequence number, * all other queued transactions for that account, in the case where they spend the maximum possible XRP, leave enough XRP balance to pay the fee, diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index 15161d642..113e5276e 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -1365,13 +1365,12 @@ NetworkOPsImp::apply(std::unique_lock& batchLock) << "Transaction is now included in open ledger"; e.transaction->setStatus(INCLUDED); - auto txCur = e.transaction->getSTransaction(); - for (auto const& tx : m_ledgerMaster.pruneHeldTransactions( - txCur->getAccountID(sfAccount), - txCur->getSequence() + 1)) + auto const& txCur = e.transaction->getSTransaction(); + auto const txNext = m_ledgerMaster.popAcctTransaction(txCur); + if (txNext) { std::string reason; - auto const trans = sterilize(*tx); + auto const trans = sterilize(*txNext); auto t = std::make_shared(trans, reason, app_); submit_held.emplace_back(t, false, false, FailHard::no); t->setApplying(); diff --git a/src/ripple/app/misc/TxQ.h b/src/ripple/app/misc/TxQ.h index 64cb544da..748032cfc 100644 --- a/src/ripple/app/misc/TxQ.h +++ b/src/ripple/app/misc/TxQ.h @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012-14 Ripple Labs Inc. + Copyright (c) 2012-19 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 @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -81,7 +82,7 @@ public: std::size_t queueSizeMin = 2000; /** Extra percentage required on the fee level of a queued transaction to replace that transaction with another - with the same sequence number. + with the same SeqProxy. If queued transaction for account "Alice" with seq 45 has a fee level of 512, a replacement transaction for @@ -89,19 +90,6 @@ public: 512 * (1 + 0.25) = 640 to be considered. */ std::uint32_t retrySequencePercent = 25; - /** Extra percentage required on the fee level of a - queued transaction to queue the transaction with - the next sequence number. - - If queued transaction for account "Alice" with seq 45 - has a fee level of 512, a transaction with seq 46 must - have a fee level of at least - 512 * (1 + -0.90) = 51.2 ~= 52 to - be considered. - - @todo eahennis. Can we remove the multi tx factor? - */ - std::int32_t multiTxnPercent = -90; /// Minimum value of the escalation multiplier, regardless /// of the prior ledger's median fee level. FeeLevel64 minimumEscalationMultiplier = baseLevel * 500; @@ -160,15 +148,6 @@ public: processed. */ std::uint32_t minimumLastLedgerBuffer = 2; - /** So we don't deal with "infinite" fee levels, treat - any transaction with a 0 base fee (i.e. SetRegularKey - password recovery) as having this fee level. - Should the network behavior change in the future such - that these transactions are unable to be processed, - we can make this more complicated. But avoid - bikeshedding for now. - */ - FeeLevel64 zeroBaseFeeTransactionFeeLevel{256000}; /// Use standalone mode behavior. bool standAlone = false; }; @@ -202,40 +181,48 @@ public: FeeLevel64 openLedgerFeeLevel; }; - /** - Structure returned by @ref TxQ::getAccountTxs to describe - transactions in the queue for an account. - */ - struct AccountTxDetails - { - /// Default constructor - explicit AccountTxDetails() = default; - - /// Fee level of the queued transaction - FeeLevel64 feeLevel; - /// LastValidLedger field of the queued transaction, if any - boost::optional lastValid; - /** Potential @ref TxConsequences of applying the queued transaction - to the open ledger, if known. - - @note `consequences` is lazy-computed, so may not be known at any - given time. - */ - boost::optional consequences; - }; - /** Structure that describes a transaction in the queue waiting to be applied to the current open ledger. A collection of these is returned by @ref TxQ::getTxs. */ - struct TxDetails : AccountTxDetails + struct TxDetails { - /// Default constructor - explicit TxDetails() = default; + /// Full initialization + TxDetails( + FeeLevel64 feeLevel_, + boost::optional const& lastValid_, + TxConsequences const& consequences_, + AccountID const& account_, + SeqProxy seqProxy_, + std::shared_ptr const& txn_, + int retriesRemaining_, + TER preflightResult_, + boost::optional lastResult_) + : feeLevel(feeLevel_) + , lastValid(lastValid_) + , consequences(consequences_) + , account(account_) + , seqProxy(seqProxy_) + , txn(txn_) + , retriesRemaining(retriesRemaining_) + , preflightResult(preflightResult_) + , lastResult(lastResult_) + { + } + /// Fee level of the queued transaction + FeeLevel64 feeLevel; + /// LastValidLedger field of the queued transaction, if any + boost::optional lastValid; + /** Potential @ref TxConsequences of applying the queued transaction + to the open ledger. + */ + TxConsequences consequences; /// The account the transaction is queued for AccountID account; + /// SeqProxy of the transaction + SeqProxy seqProxy; /// The full transaction std::shared_ptr txn; /** Number of times the transactor can return a retry / `ter` result @@ -315,6 +302,10 @@ public: void processClosedLedger(Application& app, ReadView const& view, bool timeLeap); + /** Return the next sequence that would go in the TxQ for an account. */ + SeqProxy + nextQueuableSeq(std::shared_ptr const& sleAccount) const; + /** Returns fee metrics in reference fee level units. */ Metrics @@ -344,10 +335,10 @@ public: /** Returns information about the transactions currently in the queue for the account. - @returns Empty `map` if the - account has no transactions in the queue. + @returns Empty `vector` if the account has no transactions + in the queue. */ - std::map + std::vector getAccountTxs(AccountID const& account, ReadView const& view) const; /** Returns information about all transactions currently @@ -367,6 +358,12 @@ public: doRPC(Application& app) const; private: + // Implementation for nextQueuableSeq(). The passed lock must be held. + SeqProxy + nextQueuableSeqImpl( + std::shared_ptr const& sleAccount, + std::lock_guard const&) const; + /** Track and use the fee escalation metrics of the current open ledger. Does the work of scaling fees @@ -473,18 +470,18 @@ private: will be sensible (e.g. there won't be any underflows or overflows), but the level will be higher than actually required. - @note A "series" is a set of transactions for the same account - with sequential sequence numbers. In the context of this - function, the series is already in the queue, and the series - starts with the account's current sequence number. This - function is called by @ref tryClearAccountQueue to figure - out if a newly submitted transaction is paying enough to - get all of the queued transactions plus itself out of the - queue and into the open ledger while accounting for the - escalating fee as each one is processed. The idea is that - if a series of transactions are taking too long to get out - of the queue, a user can "rescue" them without having to - resubmit each one with an individually higher fee. + @note A "series" is a set of transactions for the same account. + In the context of this function, the series is already in + the queue, and the series starts with the account's current + sequence number. This function is called by + @ref tryClearAccountQueueUpThruTx to figure out if a newly + submitted transaction is paying enough to get all of the queued + transactions plus itself out of the queue and into the open + ledger while accounting for the escalating fee as each one + is processed. The idea is that if a series of transactions + are taking too long to get out of the queue, a user can + "rescue" them without having to resubmit each one with an + individually higher fee. @param view Current open / working ledger. (May be a sandbox.) @param extraCount Number of additional transactions to count as @@ -519,23 +516,18 @@ private: /// The complete transaction. std::shared_ptr txn; - /// Potential @ref TxConsequences of applying this transaction - /// to the open ledger. - boost::optional consequences; - /// Computed fee level that the transaction will pay. FeeLevel64 const feeLevel; /// Transaction ID. TxID const txID; - /// Prior transaction ID (`sfAccountTxnID` field). - boost::optional priorTxID; /// Account submitting the transaction. AccountID const account; /// Expiration ledger for the transaction /// (`sfLastLedgerSequence` field). - boost::optional lastValid; - /// Transaction sequence number (`sfSequence` field). - TxSeq const sequence; + boost::optional const lastValid; + /// Transaction SeqProxy number + /// (`sfSequence` or `sfTicketSequence` field). + SeqProxy const seqProxy; /** A transaction at the front of the queue will be given several attempts to succeed before being dropped from @@ -594,6 +586,30 @@ private: /// Attempt to apply the queued transaction to the open ledger. std::pair apply(Application& app, OpenView& view, beast::Journal j); + + /// Potential @ref TxConsequences of applying this transaction + /// to the open ledger. + TxConsequences const& + consequences() const + { + return pfresult->consequences; + } + + /// Return a TxDetails based on contained information. + TxDetails + getTxDetails() const + { + return { + feeLevel, + lastValid, + consequences(), + account, + seqProxy, + txn, + retriesRemaining, + pfresult->ter, + lastResult}; + } }; /// Used for sorting @ref MaybeTx by `feeLevel` @@ -612,12 +628,12 @@ private: }; /** Used to represent an account to the queue, and stores the - transactions queued for that account by sequence. + transactions queued for that account by SeqProxy. */ class TxQAccount { public: - using TxMap = std::map; + using TxMap = std::map; /// The account AccountID const account; @@ -658,19 +674,47 @@ private: return !getTxnCount(); } + /// Find the entry in transactions that precedes seqProx, if one does. + TxMap::const_iterator + getPrevTx(SeqProxy seqProx) const; + /// Add a transaction candidate to this account for queuing MaybeTx& add(MaybeTx&&); - /** Remove the candidate with given sequence number from this + /** Remove the candidate with given SeqProxy value from this account. @return Whether a candidate was removed */ bool - remove(TxSeq const& sequence); + remove(SeqProxy seqProx); }; + // Helper function returns requiredFeeLevel. + FeeLevel64 + getRequiredFeeLevel( + OpenView& view, + ApplyFlags flags, + FeeMetrics::Snapshot const& metricsSnapshot, + std::lock_guard const& lock) const; + + // Helper function for TxQ::apply. If a transaction's fee is high enough, + // attempt to directly apply that transaction to the ledger. + std::optional> + tryDirectApply( + Application& app, + OpenView& view, + std::shared_ptr const& tx, + ApplyFlags flags, + beast::Journal j); + + // Helper function that removes a replaced entry in _byFee. + boost::optional + removeFromByFee( + boost::optional const& replacedTxIter, + std::shared_ptr const& tx); + using FeeHook = boost::intrusive::member_hook< MaybeTx, boost::intrusive::set_member_hook<>, @@ -726,13 +770,15 @@ private: /** Checks if the indicated transaction fits the conditions for being stored in the queue. */ - bool + TER canBeHeld( STTx const&, ApplyFlags const, OpenView const&, - AccountMap::iterator, - boost::optional); + std::shared_ptr const& sleAccount, + AccountMap::iterator const&, + boost::optional const&, + std::lock_guard const& lock); /// Erase and return the next entry in byFee_ (lower fee level) FeeMultiSet::iterator_type erase(FeeMultiSet::const_iterator_type); @@ -750,11 +796,12 @@ private: TxQAccount::TxMap::const_iterator end); /** - All-or-nothing attempt to try to apply all the queued txs for - `accountIter` up to and including `tx`. + All-or-nothing attempt to try to apply the queued txs for + `accountIter` up to and including `tx`. Transactions following + `tx` are not cleared. */ std::pair - tryClearAccountQueue( + tryClearAccountQueueUpThruTx( Application& app, OpenView& view, STTx const& tx, @@ -775,16 +822,23 @@ TxQ::Setup setup_TxQ(Config const&); template -std::pair +XRPAmount toDrops(FeeLevel const& level, XRPAmount const& baseFee) { - return mulDiv(level, baseFee, TxQ::baseLevel); + if (auto const drops = mulDiv(level, baseFee, TxQ::baseLevel); drops.first) + return drops.second; + + return XRPAmount(STAmount::cMaxNativeN); } -inline std::pair +inline FeeLevel64 toFeeLevel(XRPAmount const& drops, XRPAmount const& baseFee) { - return mulDiv(drops, TxQ::baseLevel, baseFee); + if (auto const feeLevel = mulDiv(drops, TxQ::baseLevel, baseFee); + feeLevel.first) + return feeLevel.second; + + return FeeLevel64(std::numeric_limits::max()); } } // namespace ripple diff --git a/src/ripple/app/misc/impl/LoadFeeTrack.cpp b/src/ripple/app/misc/impl/LoadFeeTrack.cpp index efe3b83b3..01445d4e5 100644 --- a/src/ripple/app/misc/impl/LoadFeeTrack.cpp +++ b/src/ripple/app/misc/impl/LoadFeeTrack.cpp @@ -41,7 +41,7 @@ LoadFeeTrack::raiseLocalFee() if (++raiseCount_ < 2) return false; - std::uint32_t origFee = localTxnLoadFee_; + std::uint32_t const origFee = localTxnLoadFee_; // make sure this fee takes effect if (localTxnLoadFee_ < remoteTxnLoadFee_) @@ -65,7 +65,7 @@ bool LoadFeeTrack::lowerLocalFee() { std::lock_guard sl(lock_); - std::uint32_t origFee = localTxnLoadFee_; + std::uint32_t const origFee = localTxnLoadFee_; raiseCount_ = 0; // Reduce slowly diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index db858a908..d3f12f39d 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2012, 2013 Ripple Labs Inc. + Copyright (c) 2012, 2013, 2019 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 @@ -36,21 +36,31 @@ namespace ripple { ////////////////////////////////////////////////////////////////////////// static FeeLevel64 -getFeeLevelPaid( - STTx const& tx, - FeeLevel64 baseRefLevel, - XRPAmount refTxnCostDrops, - TxQ::Setup const& setup) +getFeeLevelPaid(ReadView const& view, STTx const& tx) { - if (refTxnCostDrops == 0) - // If nothing is required, or the cost is 0, - // the level is effectively infinite. - return setup.zeroBaseFeeTransactionFeeLevel; + auto const [baseFee, effectiveFeePaid] = [&view, &tx]() { + XRPAmount baseFee = view.fees().toDrops(calculateBaseFee(view, tx)); + XRPAmount feePaid = tx[sfFee].xrp(); - // If the math overflows, return the clipped - // result blindly. This is very unlikely to ever - // happen. - return mulDiv(tx[sfFee].xrp(), baseRefLevel, refTxnCostDrops).second; + // If baseFee is 0 then the cost of a basic transaction is free. + XRPAmount const ref = baseFee.signum() > 0 + ? XRPAmount{0} + : calculateDefaultBaseFee(view, tx); + return std::pair{baseFee + ref, feePaid + ref}; + }(); + + assert(baseFee.signum() > 0); + if (effectiveFeePaid.signum() <= 0 || baseFee.signum() <= 0) + { + return FeeLevel64(0); + } + + if (std::pair const feeLevelPaid = + mulDiv(effectiveFeePaid, TxQ::baseLevel, baseFee); + feeLevelPaid.first) + return feeLevelPaid.second; + + return FeeLevel64(std::numeric_limits::max()); } static boost::optional @@ -84,10 +94,7 @@ TxQ::FeeMetrics::update( auto const size = std::distance(txBegin, txEnd); feeLevels.reserve(size); std::for_each(txBegin, txEnd, [&](auto const& tx) { - auto const baseFee = - view.fees().toDrops(calculateBaseFee(view, *tx.first)).second; - feeLevels.push_back( - getFeeLevelPaid(*tx.first, baseLevel, baseFee, setup)); + feeLevels.push_back(getFeeLevelPaid(view, *tx.first)); }); std::sort(feeLevels.begin(), feeLevels.end()); assert(size == feeLevels.size()); @@ -182,21 +189,41 @@ TxQ::FeeMetrics::scaleFeeLevel(Snapshot const& snapshot, OpenView const& view) namespace detail { -static std::pair -sumOfFirstSquares(std::size_t x) +constexpr static std::pair +sumOfFirstSquares(std::size_t xIn) { // sum(n = 1->x) : n * n = x(x + 1)(2x + 1) / 6 + // We expect that size_t == std::uint64_t but, just in case, guarantee + // we lose no bits. + std::uint64_t x{xIn}; + // If x is anywhere on the order of 2^^21, it's going // to completely dominate the computation and is likely // enough to overflow that we're just going to assume // it does. If we have anywhere near 2^^21 transactions // in a ledger, this is the least of our problems. if (x >= (1 << 21)) - return std::make_pair(false, std::numeric_limits::max()); - return std::make_pair(true, (x * (x + 1) * (2 * x + 1)) / 6); + return {false, std::numeric_limits::max()}; + return {true, (x * (x + 1) * (2 * x + 1)) / 6}; } +// Unit tests for sumOfSquares() +static_assert(sumOfFirstSquares(1).first == true); +static_assert(sumOfFirstSquares(1).second == 1); + +static_assert(sumOfFirstSquares(2).first == true); +static_assert(sumOfFirstSquares(2).second == 5); + +static_assert(sumOfFirstSquares(0x1FFFFF).first == true, ""); +static_assert(sumOfFirstSquares(0x1FFFFF).second == 0x2AAAA8AAAAB00000ul, ""); + +static_assert(sumOfFirstSquares(0x200000).first == false, ""); +static_assert( + sumOfFirstSquares(0x200000).second == + std::numeric_limits::max(), + ""); + } // namespace detail std::pair @@ -250,15 +277,12 @@ TxQ::MaybeTx::MaybeTx( , feeLevel(feeLevel_) , txID(txID_) , account(txn_->getAccountID(sfAccount)) - , sequence(txn_->getSequence()) + , lastValid(getLastLedgerSequence(*txn_)) + , seqProxy(txn_->getSeqProxy()) , retriesRemaining(retriesAllowed) , flags(flags_) , pfresult(pfresult_) { - lastValid = getLastLedgerSequence(*txn); - - if (txn->isFieldPresent(sfAccountTxnID)) - priorTxID = txn->getFieldH256(sfAccountTxnID); } std::pair @@ -290,12 +314,23 @@ TxQ::TxQAccount::TxQAccount(const AccountID& account_) : account(account_) { } -auto -TxQ::TxQAccount::add(MaybeTx&& txn) -> MaybeTx& +TxQ::TxQAccount::TxMap::const_iterator +TxQ::TxQAccount::getPrevTx(SeqProxy seqProx) const { - auto sequence = txn.sequence; + // Find the entry that is greater than or equal to the new transaction, + // then decrement the iterator. + auto sameOrPrevIter = transactions.lower_bound(seqProx); + if (sameOrPrevIter != transactions.begin()) + --sameOrPrevIter; + return sameOrPrevIter; +} - auto result = transactions.emplace(sequence, std::move(txn)); +TxQ::MaybeTx& +TxQ::TxQAccount::add(MaybeTx&& txn) +{ + auto const seqProx = txn.seqProxy; + + auto result = transactions.emplace(seqProx, std::move(txn)); assert(result.second); assert(&result.first->second != &txn); @@ -303,9 +338,9 @@ TxQ::TxQAccount::add(MaybeTx&& txn) -> MaybeTx& } bool -TxQ::TxQAccount::remove(TxSeq const& sequence) +TxQ::TxQAccount::remove(SeqProxy seqProx) { - return transactions.erase(sequence) != 0; + return transactions.erase(seqProx) != 0; } ////////////////////////////////////////////////////////////////////////// @@ -329,63 +364,70 @@ TxQ::isFull() const return maxSize_ && byFee_.size() >= (*maxSize_ * fillPercentage / 100); } -bool +TER TxQ::canBeHeld( STTx const& tx, ApplyFlags const flags, OpenView const& view, - AccountMap::iterator accountIter, - boost::optional replacementIter) + std::shared_ptr const& sleAccount, + AccountMap::iterator const& accountIter, + boost::optional const& replacementIter, + std::lock_guard const& lock) { - // PreviousTxnID is deprecated and should never be used + // PreviousTxnID is deprecated and should never be used. // AccountTxnID is not supported by the transaction - // queue yet, but should be added in the future + // queue yet, but should be added in the future. // tapFAIL_HARD transactions are never held - bool canBeHeld = !tx.isFieldPresent(sfPreviousTxnID) && - !tx.isFieldPresent(sfAccountTxnID) && !(flags & tapFAIL_HARD); - if (canBeHeld) + if (tx.isFieldPresent(sfPreviousTxnID) || + tx.isFieldPresent(sfAccountTxnID) || (flags & tapFAIL_HARD)) + return telCAN_NOT_QUEUE; + { - /* To be queued and relayed, the transaction needs to - promise to stick around for long enough that it has - a realistic chance of getting into a ledger. - */ + // To be queued and relayed, the transaction needs to + // promise to stick around for long enough that it has + // a realistic chance of getting into a ledger. auto const lastValid = getLastLedgerSequence(tx); - canBeHeld = !lastValid || - *lastValid >= view.info().seq + setup_.minimumLastLedgerBuffer; + if (lastValid && + *lastValid < view.info().seq + setup_.minimumLastLedgerBuffer) + return telCAN_NOT_QUEUE; } - if (canBeHeld) - { - /* Limit the number of transactions an individual account - can queue. Mitigates the lost cost of relaying should - an early one fail or get dropped. - */ - // Allow if the account is not in the queue at all - canBeHeld = accountIter == byAccount_.end(); + // Allow if the account is not in the queue at all. + if (accountIter == byAccount_.end()) + return tesSUCCESS; - if (!canBeHeld) - { - // Allow this tx to replace another one - canBeHeld = replacementIter.is_initialized(); - } + // Allow this tx to replace another one. + if (replacementIter) + return tesSUCCESS; - if (!canBeHeld) - { - // Allow if there are fewer than the limit - canBeHeld = - accountIter->second.getTxnCount() < setup_.maximumTxnPerAccount; - } + // Allow if there are fewer than the limit. + TxQAccount const& txQAcct = accountIter->second; + if (txQAcct.getTxnCount() < setup_.maximumTxnPerAccount) + return tesSUCCESS; - if (!canBeHeld) - { - // Allow if the transaction goes in front of any - // queued transactions. Enables recovery of open - // ledger transactions, and stuck transactions. - auto const tSeq = tx.getSequence(); - canBeHeld = tSeq < accountIter->second.transactions.rbegin()->first; - } - } - return canBeHeld; + // If we get here the queue limit is exceeded. Only allow if this + // transaction fills the _first_ sequence hole for the account. + auto const txSeqProx = tx.getSeqProxy(); + if (txSeqProx.isTicket()) + // Tickets always follow sequence-based transactions, so a ticket + // cannot unblock a sequence-based transaction. + return telCAN_NOT_QUEUE_FULL; + + // This is the next queuable sequence-based SeqProxy for the account. + SeqProxy const nextQueuable = nextQueuableSeqImpl(sleAccount, lock); + if (txSeqProx != nextQueuable) + // The provided transaction does not fill the next open sequence gap. + return telCAN_NOT_QUEUE_FULL; + + // Make sure they are not just topping off the account's queued + // sequence-based transactions. + if (auto const nextTxIter = txQAcct.transactions.upper_bound(nextQueuable); + nextTxIter != txQAcct.transactions.end() && nextTxIter->first.isSeq()) + // There is a next transaction and it is sequence based. They are + // filling a real gap. Allow it. + return tesSUCCESS; + + return telCAN_NOT_QUEUE_FULL; } auto @@ -393,12 +435,12 @@ TxQ::erase(TxQ::FeeMultiSet::const_iterator_type candidateIter) -> FeeMultiSet::iterator_type { auto& txQAccount = byAccount_.at(candidateIter->account); - auto const sequence = candidateIter->sequence; + auto const seqProx = candidateIter->seqProxy; auto const newCandidateIter = byFee_.erase(candidateIter); // Now that the candidate has been removed from the // intrusive list remove it from the TxQAccount // so the memory can be freed. - auto const found = txQAccount.remove(sequence); + auto const found = txQAccount.remove(seqProx); (void)found; assert(found); @@ -411,32 +453,46 @@ TxQ::eraseAndAdvance(TxQ::FeeMultiSet::const_iterator_type candidateIter) { auto& txQAccount = byAccount_.at(candidateIter->account); auto const accountIter = - txQAccount.transactions.find(candidateIter->sequence); + txQAccount.transactions.find(candidateIter->seqProxy); assert(accountIter != txQAccount.transactions.end()); - assert(accountIter == txQAccount.transactions.begin()); + + // Note that sequence-based transactions must be applied in sequence order + // from smallest to largest. But ticket-based transactions can be + // applied in any order. + assert( + candidateIter->seqProxy.isTicket() || + accountIter == txQAccount.transactions.begin()); assert(byFee_.iterator_to(accountIter->second) == candidateIter); auto const accountNextIter = std::next(accountIter); - /* Check if the next transaction for this account has the - next sequence number, and a higher fee level, which means - we skipped it earlier, and need to try it again. - Edge cases: If the next account tx has a lower fee level, - it's going to be later in the fee queue, so we haven't - skipped it yet. - If the next tx has an equal fee level, it was either - submitted later, so it's also going to be later in the - fee queue, OR the current was resubmitted to bump up - the fee level, and we have skipped that next tx. In - the latter case, continue through the fee queue anyway - to head off potential ordering manipulation problems. - */ + + // Check if the next transaction for this account has a greater + // SeqProxy, and a higher fee level, which means we skipped it + // earlier, and need to try it again. + // + // Edge cases: + // o If the next account tx has a lower fee level, it's going to be + // later in the fee queue, so we haven't skipped it yet. + // + // o If the next tx has an equal fee level, it was... + // + // * EITHER submitted later, so it's also going to be later in the + // fee queue, + // + // * OR the current was resubmitted to bump up the fee level, and + // we have skipped that next tx. + // + // In the latter case, continue through the fee queue anyway + // to head off potential ordering manipulation problems. auto const feeNextIter = std::next(candidateIter); bool const useAccountNext = accountNextIter != txQAccount.transactions.end() && - accountNextIter->first == candidateIter->sequence + 1 && + accountNextIter->first > candidateIter->seqProxy && (feeNextIter == byFee_.end() || accountNextIter->second.feeLevel > feeNextIter->feeLevel); + auto const candidateNextIter = byFee_.erase(candidateIter); txQAccount.transactions.erase(accountIter); + return useAccountNext ? byFee_.iterator_to(accountNextIter->second) : candidateNextIter; } @@ -455,7 +511,7 @@ TxQ::erase( } std::pair -TxQ::tryClearAccountQueue( +TxQ::tryClearAccountQueueUpThruTx( Application& app, OpenView& view, STTx const& tx, @@ -468,22 +524,21 @@ TxQ::tryClearAccountQueue( FeeMetrics::Snapshot const& metricsSnapshot, beast::Journal j) { - auto const tSeq = tx.getSequence(); + SeqProxy const tSeqProx{tx.getSeqProxy()}; assert(beginTxIter != accountIter->second.transactions.end()); - auto const aSeq = beginTxIter->first; + + // This check is only concerned with the range from + // [aSeqProxy, tSeqProxy) + auto endTxIter = accountIter->second.transactions.lower_bound(tSeqProx); + auto const dist = std::distance(beginTxIter, endTxIter); auto const requiredTotalFeeLevel = FeeMetrics::escalatedSeriesFeeLevel( - metricsSnapshot, view, txExtraCount, tSeq - aSeq + 1); - /* If the computation for the total manages to overflow (however extremely - unlikely), then there's no way we can confidently verify if the queue - can be cleared. - */ + metricsSnapshot, view, txExtraCount, dist + 1); + // If the computation for the total manages to overflow (however extremely + // unlikely), then there's no way we can confidently verify if the queue + // can be cleared. if (!requiredTotalFeeLevel.first) - return std::make_pair(telINSUF_FEE_P, false); - - // Unlike multiTx, this check is only concerned with the range - // from [aSeq, tSeq) - auto endTxIter = accountIter->second.transactions.lower_bound(tSeq); + return {telINSUF_FEE_P, false}; auto const totalFeeLevelPaid = std::accumulate( beginTxIter, @@ -495,7 +550,7 @@ TxQ::tryClearAccountQueue( // This transaction did not pay enough, so fall back to the normal process. if (totalFeeLevelPaid < requiredTotalFeeLevel.second) - return std::make_pair(telINSUF_FEE_P, false); + return {telINSUF_FEE_P, false}; // This transaction paid enough to clear out the queue. // Attempt to apply the queued transactions. @@ -508,10 +563,30 @@ TxQ::tryClearAccountQueue( // moot. --it->second.retriesRemaining; it->second.lastResult = txResult.first; + + // In TxQ::apply we note that it's possible for a transaction with + // a ticket to both be in the queue and in the ledger. And, while + // we're in TxQ::apply, it's too expensive to filter those out. + // + // So here in tryClearAccountQueueUpThruTx we just received a batch of + // queued transactions. And occasionally one of those is a ticketed + // transaction that is both in the queue and in the ledger. When + // that happens the queued transaction returns tefNO_TICKET. + // + // The transaction that returned tefNO_TICKET can never succeed + // and we'd like to get it out of the queue as soon as possible. + // The easiest way to do that from here is to treat the transaction + // as though it succeeded and attempt to clear the remaining + // transactions in the account queue. Then, if clearing the account + // is successful, we will have removed any ticketed transactions + // that can never succeed. + if (txResult.first == tefNO_TICKET) + continue; + if (!txResult.second) { // Transaction failed to apply. Fall back to the normal process. - return std::make_pair(txResult.first, false); + return {txResult.first, false}; } } // Apply the current tx. Because the state of the view has been changed @@ -525,74 +600,126 @@ TxQ::tryClearAccountQueue( endTxIter = erase(accountIter->second, beginTxIter, endTxIter); // If `tx` is replacing a queued tx, delete that one, too. if (endTxIter != accountIter->second.transactions.end() && - endTxIter->first == tSeq) + endTxIter->first == tSeqProx) erase(accountIter->second, endTxIter, std::next(endTxIter)); } return txResult; } -/* - How the decision to apply, queue, or reject is made: - 1. Does `preflight` indicate that the tx is valid? - No: Return the `TER` from `preflight`. Stop. - Yes: Continue to next step. - 2. Is there already a tx for the same account with the - same sequence number in the queue? - Yes: Is `txn`'s fee `retrySequencePercent` higher than the - queued transaction's fee? And is this the last tx - in the queue for that account, or are both txs - non-blockers? - Yes: Remove the queued transaction. Continue to next - step. - No: Reject `txn` with `telCAN_NOT_QUEUE_FEE`. Stop. - No: Continue to next step. - 3. Does this tx have the expected sequence number for the - account? - Yes: Continue to next step. - No: Are all the intervening sequence numbers also in the - queue? - No: Continue to the next step. (We expect the next - step to return `terPRE_SEQ`, but won't short - circuit that logic.) - Yes: Is the fee more than `multiTxnPercent` higher - than the previous tx? - No: Reject with `telINSUF_FEE_P`. Stop. - Yes: Are any of the prior sequence txs blockers? - Yes: Reject with `telCAN_NOT_QUEUE_BLOCKED`. Stop. - No: Are the fees in-flight of the other - queued txs >= than the account - balance or minimum account reserve? - Yes: Reject with `telCAN_NOT_QUEUE_BALANCE`. Stop. - No: Create a throwaway sandbox `View`. Modify - the account's sequence number to match - the tx (avoid `terPRE_SEQ`), and decrease - the account balance by the total fees and - maximum spend of the other in-flight txs. - Continue to the next step. - 4. Does `preclaim` indicate that the account is likely to claim - a fee (using the throwaway sandbox `View` created above, - if appropriate)? - No: Return the `TER` from `preclaim`. Stop. - Yes: Continue to the next step. - 5. Did we create a throwaway sandbox `View`? - Yes: Continue to the next step. - No: Is the `txn`s fee level >= the required fee level? - Yes: `txn` can be applied to the open ledger. Pass - it to `doApply()` and return that result. - No: Continue to the next step. - 6. Can the tx be held in the queue? (See TxQ::canBeHeld). - No: Reject `txn` with `telCAN_NOT_QUEUE_FULL` - if not. Stop. - Yes: Continue to the next step. - 7. Is the queue full? - No: Continue to the next step. - Yes: Is the `txn`'s fee level higher than the end / - lowest fee level item's fee level? - Yes: Remove the end item. Continue to the next step. - No: Reject `txn` with a low fee TER code. - 8. Put `txn` in the queue. -*/ +// Overview of considerations for when a transaction is accepted into the TxQ: +// +// These rules apply to the transactions in the queue owned by a single +// account. Briefly, the primary considerations are: +// +// 1. Is the new transaction blocking? +// 2. Is there an expiration gap in the account's sequence-based transactions? +// 3. Does the new transaction replace one that is already in the TxQ? +// 4. Is the transaction's sequence or ticket value acceptable for this account? +// 5. Is the transaction likely to claim a fee? +// 6. Is the queue full? +// +// Here are more details. +// +// 1. A blocking transaction is one that would change the validity of following +// transactions for the issuing account. Examples of blocking transactions +// include SetRegularKey and SignerListSet. +// +// A blocking transaction can only be added to the queue for an account if: +// +// a. The queue for that account is empty, or +// +// b. The blocking transaction replaces the only transaction in the +// account's queue. +// +// While a blocker is in the account's queue no additional transactions +// can be added to the queue. +// +// As a consequence, any blocker is always alone in the account's queue. +// +// 2. Transactions are given unique identifiers using either Sequence numbers +// or Tickets. In general, sequence numbers in the queue are expected to +// start with the account root sequence and increment from there. There +// are two exceptions: +// +// a. Sequence holes left by ticket creation. If a transaction creates +// more than one ticket, then the account sequence number will jump +// by the number of tickets created. These holes are fine. +// +// b. Sequence gaps left by transaction expiration. If transactions stay +// in the queue long enough they may expire. If that happens it leaves +// gaps in the sequence numbers held by the queue. These gaps are +// important because, if left in place, they will block any later +// sequence-based transactions in the queue from working. Remember, +// for any given account sequence numbers must be used consecutively +// (with the exception of ticket-induced holes). +// +// 3. Transactions in the queue may be replaced. If a transaction in the +// queue has the same SeqProxy as the incoming transaction, then the +// transaction in the queue will be replaced if the following conditions +// are met: +// +// a. The replacement must provide a fee that is at least 1.25 times the +// fee of the transaction it is replacing. +// +// b. If the transaction being replaced has a sequence number, then +// the transaction may not be after any expiration-based sequence +// gaps in the account's queue. +// +// c. A replacement that is a blocker is only allowed if the transaction +// it replaces is the only transaction in the account's queue. +// +// 4. The transaction that is not a replacement must have an acceptable +// sequence or ticket ID: +// +// Sequence: For a given account's queue configuration there is at most +// one sequence number that is acceptable to the queue for that account. +// The rules are: +// +// a. If there are no sequence-based transactions in the queue and the +// candidate transaction has a sequence number, that value must match +// the account root's sequence. +// +// b. If there are sequence-based transactions in the queue for that +// account and there are no expiration-based gaps, then the candidate's +// sequence number must belong at the end of the list of sequences. +// +// c. If there are expiration-based gaps in the sequence-based +// transactions in the account's queue, then the candidate's sequence +// value must go precisely at the front of the first gap. +// +// Ticket: If there are no blockers or sequence gaps in the account's +// queue, then there are many tickets that are acceptable to the queue +// for that account. The rules are: +// +// a. If there are no blockers in the account's queue and the ticket +// required by the transaction is in the ledger then the transaction +// may be added to the account's queue. +// +// b. If there is a ticket-based blocker in the account's queue then +// that blocker can be replaced. +// +// Note that it is not sufficient for the transaction that would create +// the necessary ticket to be in the account's queue. The required ticket +// must already be in the ledger. This avoids problems that can occur if +// a ticket-creating transaction enters the queue but expires out of the +// queue before its tickets are created. +// +// 5. The transaction must be likely to claim a fee. In general that is +// checked by having preclaim return a tes or tec code. +// +// Extra work is done here to account for funds that other transactions +// in the queue remove from the account. +// +// 6. The queue must not be full. +// +// a. Each account can queue up to a maximum of 10 transactions. Beyond +// that transactions are rejected. There is an exception for this case +// when filling expiration-based sequence gaps. +// +// b. The entire queue also has a (dynamic) maximum size. Transactions +// beyond that limit are rejected. +// std::pair TxQ::apply( Application& app, @@ -601,9 +728,18 @@ TxQ::apply( ApplyFlags flags, beast::Journal j) { - auto const account = (*tx)[sfAccount]; - auto const transactionID = tx->getTransactionID(); - auto const tSeq = tx->getSequence(); + // 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)) + return *directApplied; + + // If we get past tryDirectApply() without returning then we expect + // one of the following to occur: + // + // o We will decide the transaction is unlikely to claim a fee. + // 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. @@ -611,121 +747,168 @@ TxQ::apply( if (pfresult.ter != tesSUCCESS) return {pfresult.ter, false}; - struct MultiTxn + // 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)}; + auto const sleAccount = view.read(accountKey); + if (!sleAccount) + return {terNO_ACCOUNT, false}; + + // If the transaction needs a Ticket is that Ticket in the ledger? + SeqProxy const acctSeqProx = SeqProxy::sequence((*sleAccount)[sfSequence]); + SeqProxy const txSeqProx = tx->getSeqProxy(); + if (txSeqProx.isTicket() && + !view.exists(keylet::ticket(account, txSeqProx))) { - explicit MultiTxn() = default; + if (txSeqProx.value() < acctSeqProx.value()) + // The ticket number is low enough that it should already be + // in the ledger if it were ever going to exist. + return {tefNO_TICKET, false}; - boost::optional applyView; - boost::optional openView; - - TxQAccount::TxMap::iterator nextTxIter; - - XRPAmount fee = beast::zero; - XRPAmount potentialSpend = beast::zero; - bool includeCurrentFee = false; - }; - - boost::optional multiTxn; - boost::optional consequences; - boost::optional replacedItemDeleteIter; + // We don't queue transactions that use Tickets unless + // we can find the Ticket in the ledger. + return {terPRE_TICKET, false}; + } std::lock_guard lock(mutex_); - auto const metricsSnapshot = feeMetrics_.getSnapshot(); + // accountIter is not const because it may be updated further down. + AccountMap::iterator accountIter = byAccount_.find(account); + bool const accountIsInQueue = accountIter != byAccount_.end(); - // We may need the base fee for multiple transactions - // or transaction replacement, so just pull it up now. - // TODO: Do we want to avoid doing it again during - // preclaim? - auto const baseFee = - view.fees().toDrops(calculateBaseFee(view, *tx)).second; - auto const feeLevelPaid = getFeeLevelPaid(*tx, baseLevel, baseFee, setup_); - auto const requiredFeeLevel = [&]() { - auto feeLevel = FeeMetrics::scaleFeeLevel(metricsSnapshot, view); - if ((flags & tapPREFER_QUEUE) && byFee_.size()) + // _If_ the account is in the queue, then ignore any sequence-based + // queued transactions that slipped into the ledger while we were not + // watching. This does actually happen in the wild, but it's uncommon. + // + // Note that we _don't_ ignore queued ticket-based transactions that + // slipped into the ledger while we were not watching. It would be + // desirable to do so, but the measured cost was too high since we have + // to individually check each queued ticket against the ledger. + struct TxIter + { + TxIter( + TxQAccount::TxMap::iterator first_, + TxQAccount::TxMap::iterator end_) + : first(first_), end(end_) { - return std::max(feeLevel, byFee_.begin()->feeLevel); } - return feeLevel; + + TxQAccount::TxMap::iterator first; + TxQAccount::TxMap::iterator end; + }; + + boost::optional const txIter = + [accountIter, + accountIsInQueue, + acctSeqProx]() -> boost::optional { + if (!accountIsInQueue) + return {}; + + // Find the first transaction in the queue that we might apply. + TxQAccount::TxMap& acctTxs = accountIter->second.transactions; + TxQAccount::TxMap::iterator const firstIter = + acctTxs.lower_bound(acctSeqProx); + + if (firstIter == acctTxs.end()) + // Even though there may be transactions in the queue, there are + // none that we should pay attention to. + return {}; + + return {TxIter{firstIter, acctTxs.end()}}; }(); - auto accountIter = byAccount_.find(account); - bool const accountExists = accountIter != byAccount_.end(); + auto const acctTxCount{ + !txIter ? 0 : std::distance(txIter->first, txIter->end)}; - // Is there a transaction for the same account with the - // same sequence number already in the queue? - if (accountExists) + // Is tx a blocker? If so there are very limited conditions when it + // is allowed in the TxQ: + // 1. If the account's queue is empty or + // 2. If the blocker replaces the only entry in the account's queue. + auto const transactionID = tx->getTransactionID(); + if (pfresult.consequences.isBlocker()) { - auto& txQAcct = accountIter->second; - auto existingIter = txQAcct.transactions.find(tSeq); - if (existingIter != txQAcct.transactions.end()) + if (acctTxCount > 1) { + // A blocker may not be co-resident with other transactions in + // the account's queue. + JLOG(j_.trace()) + << "Rejecting blocker transaction " << transactionID + << ". Account has other queued transactions."; + return {telCAN_NOT_QUEUE_BLOCKS, false}; + } + if (acctTxCount == 1 && (txSeqProx != txIter->first->first)) + { + // The blocker is not replacing the lone queued transaction. + JLOG(j_.trace()) + << "Rejecting blocker transaction " << transactionID + << ". Blocker does not replace lone queued transaction."; + return {telCAN_NOT_QUEUE_BLOCKS, false}; + } + } + + // If the transaction is intending to replace a transaction in the queue + // identify the one that might be replaced. + auto replacedTxIter = [accountIsInQueue, &accountIter, txSeqProx]() + -> boost::optional { + if (accountIsInQueue) + { + TxQAccount& txQAcct = accountIter->second; + if (auto const existingIter = txQAcct.transactions.find(txSeqProx); + existingIter != txQAcct.transactions.end()) + return existingIter; + } + return {}; + }(); + + // We may need the base fee for multiple transactions or transaction + // replacement, so just pull it up now. + auto const metricsSnapshot = feeMetrics_.getSnapshot(); + auto const feeLevelPaid = getFeeLevelPaid(view, *tx); + auto const requiredFeeLevel = + getRequiredFeeLevel(view, flags, metricsSnapshot, lock); + + // Is there a blocker already in the account's queue? If so, don't + // allow additional transactions in the queue. + if (acctTxCount > 0) + { + // Allow tx to replace a blocker. Otherwise, if there's a + // blocker, we can't queue tx. + // + // We only need to check if txIter->first is a blocker because we + // require that a blocker be alone in the account's queue. + if (acctTxCount == 1 && + txIter->first->second.consequences().isBlocker() && + (txIter->first->first != txSeqProx)) + { + return {telCAN_NOT_QUEUE_BLOCKED, false}; + } + + // Is there a transaction for the same account with the same + // SeqProxy already in the queue? If so we may replace the + // existing entry with this new transaction. + if (replacedTxIter) + { + // We are attempting to replace a transaction in the queue. + // // Is the current transaction's fee higher than // the queued transaction's fee + a percentage + TxQAccount::TxMap::iterator const& existingIter = *replacedTxIter; auto requiredRetryLevel = increase( existingIter->second.feeLevel, setup_.retrySequencePercent); JLOG(j_.trace()) << "Found transaction in queue for account " << account - << " with sequence number " << tSeq << " new txn fee level is " + << " with " << txSeqProx << " new txn fee level is " << feeLevelPaid << ", old txn fee level is " << existingIter->second.feeLevel << ", new txn needs fee level of " << requiredRetryLevel; - if (feeLevelPaid > requiredRetryLevel || - (existingIter->second.feeLevel < requiredFeeLevel && - feeLevelPaid >= requiredFeeLevel && - existingIter == txQAcct.transactions.begin())) + if (feeLevelPaid > requiredRetryLevel) { - /* Either the fee is high enough to retry or - the prior txn is the first for this account, and - could not get into the open ledger, but this one can. - */ - - /* A normal tx can't be replaced by a blocker, unless it's - the last tx in the queue for the account. - */ - if (std::next(existingIter) != txQAcct.transactions.end()) - { - // Normally, only the last tx in the queue will have - // !consequences, but an expired transaction can be - // replaced, and that replacement won't have it set, - // and that's ok. - if (!existingIter->second.consequences) - existingIter->second.consequences.emplace( - calculateConsequences( - *existingIter->second.pfresult)); - - if (existingIter->second.consequences->category == - TxConsequences::normal) - { - assert(!consequences); - consequences.emplace(calculateConsequences(pfresult)); - if (consequences->category == TxConsequences::blocker) - { - // Can't replace a normal transaction in the - // middle of the queue with a blocker. - JLOG(j_.trace()) << "Ignoring blocker transaction " - << transactionID - << " in favor of normal queued " - << existingIter->second.txID; - return {telCAN_NOT_QUEUE_BLOCKS, false}; - } - } - } - - // Remove the queued transaction and continue + // Continue, leaving the queued transaction marked for removal. + // DO NOT REMOVE if the new tx fails, because there may + // be other txs dependent on it in the queue. JLOG(j_.trace()) << "Removing transaction from queue " << existingIter->second.txID << " in favor of " << transactionID; - // Then save the queued tx to remove from the queue if - // the new tx succeeds or gets queued. DO NOT REMOVE - // if the new tx fails, because there may be other txs - // dependent on it in the queue. - auto deleteIter = byFee_.iterator_to(existingIter->second); - assert(deleteIter != byFee_.end()); - assert(&existingIter->second == &*deleteIter); - assert(deleteIter->sequence == tSeq); - assert(deleteIter->account == txQAcct.account); - replacedItemDeleteIter = deleteIter; } else { @@ -738,186 +921,221 @@ TxQ::apply( } } - // If there are other transactions in the queue - // for this account, account for that before the pre-checks, - // so we don't get a false terPRE_SEQ. - if (accountExists) + struct MultiTxn { - if (auto const sle = view.read(keylet::account(account)); sle) + ApplyViewImpl applyView; + OpenView openView; + + MultiTxn(OpenView& view, ApplyFlags flags) + : applyView(&view, flags), openView(&applyView) { - auto& txQAcct = accountIter->second; - auto const aSeq = (*sle)[sfSequence]; + } + }; - if (aSeq < tSeq) - { - // If the transaction is queueable, create the multiTxn - // object to hold the info we need to adjust for - // prior txns. Otherwise, let preclaim fail as if - // we didn't have the queue at all. - if (canBeHeld( - *tx, flags, view, accountIter, replacedItemDeleteIter)) - multiTxn.emplace(); - } + boost::optional multiTxn; - if (multiTxn) + if (acctTxCount == 0) + { + // There are no queued transactions for this account. If the + // transaction has a sequence make sure it's valid (tickets + // are checked elsewhere). + if (txSeqProx.isSeq()) + { + if (acctSeqProx > txSeqProx) + return {tefPAST_SEQ, false}; + if (acctSeqProx < txSeqProx) + return {terPRE_SEQ, false}; + } + } + else + { + // There are probably other transactions in the queue for this + // account. Make sure the new transaction can work with the others + // in the queue. + TxQAccount const& txQAcct = accountIter->second; + + if (acctSeqProx > txSeqProx) + return {tefPAST_SEQ, false}; + + // Determine if we need a multiTxn object. Assuming the account + // is in the queue, there are two situations where we need to + // build multiTx: + // 1. If there are two or more transactions in the account's queue, or + // 2. If the account has a single queue entry, we may still need + // multiTxn, but only if that lone entry will not be replaced by tx. + bool requiresMultiTxn = false; + if (acctTxCount > 1 || !replacedTxIter) + { + // If the transaction is queueable, create the multiTxn + // object to hold the info we need to adjust for prior txns. + TER const ter{canBeHeld( + *tx, + flags, + view, + sleAccount, + accountIter, + replacedTxIter, + lock)}; + if (!isTesSuccess(ter)) + return {ter, false}; + + requiresMultiTxn = true; + } + + if (requiresMultiTxn) + { + // See if adding this entry to the queue makes sense. + // + // o Transactions with sequences should start with the + // account's Sequence. + // + // o Additional transactions with Sequences should + // follow preceding sequence-based transactions with no + // gaps (except for those required by CreateTicket + // transactions). + + // Find the entry in the queue that precedes the new + // transaction, if one does. + TxQAccount::TxMap::const_iterator const prevIter = + txQAcct.getPrevTx(txSeqProx); + + // Does the new transaction go to the front of the queue? + // This can happen if: + // o A transaction in the queue with a Sequence expired, or + // o The current first thing in the queue has a Ticket and + // * The tx has a Ticket that precedes it or + // * txSeqProx == acctSeqProx. + assert(prevIter != txIter->end); + if (prevIter == txIter->end || txSeqProx < prevIter->first) { - /* See if the queue has entries for all the - seq's in [aSeq, tSeq). Total up all the - consequences while we're checking. If one - turns up missing or is a blocker, abort. - */ - multiTxn->nextTxIter = txQAcct.transactions.find(aSeq); - auto workingIter = multiTxn->nextTxIter; - auto workingSeq = aSeq; - for (; workingIter != txQAcct.transactions.end(); - ++workingIter, ++workingSeq) + // The first Sequence number in the queue must be the + // account's sequence. + if (txSeqProx.isSeq()) { - if (workingSeq < tSeq && workingIter->first != workingSeq) - { - // If any transactions are missing before `tx`, abort. - multiTxn.reset(); - break; - } - if (workingIter->first == tSeq - 1) - { - // Is the current transaction's fee higher than - // the previous transaction's fee + a percentage - auto requiredMultiLevel = increase( - workingIter->second.feeLevel, - setup_.multiTxnPercent); - - if (feeLevelPaid <= requiredMultiLevel) - { - // Drop the current transaction - JLOG(j_.trace()) - << "Ignoring transaction " << transactionID - << ". Needs fee level of " << - - requiredMultiLevel << ". Only paid " - << feeLevelPaid; - return {telINSUF_FEE_P, false}; - } - } - if (workingIter->first == tSeq) - { - // If we're replacing this transaction, don't - // count it. - assert(replacedItemDeleteIter); - multiTxn->includeCurrentFee = std::next(workingIter) != - txQAcct.transactions.end(); - continue; - } - if (!workingIter->second.consequences) - workingIter->second.consequences.emplace( - calculateConsequences( - *workingIter->second.pfresult)); - // Don't worry about the blocker status of txs - // after the current. - if (workingIter->first < tSeq && - workingIter->second.consequences->category == - TxConsequences::blocker) - { - // Drop the current transaction, because it's - // blocked by workingIter. - JLOG(j_.trace()) - << "Ignoring transaction " << transactionID - << ". A blocker-type transaction " - << "is in the queue."; - return {telCAN_NOT_QUEUE_BLOCKED, false}; - } - multiTxn->fee += workingIter->second.consequences->fee; - multiTxn->potentialSpend += - workingIter->second.consequences->potentialSpend; + if (txSeqProx < acctSeqProx) + return {tefPAST_SEQ, false}; + else if (txSeqProx > acctSeqProx) + return {terPRE_SEQ, false}; } - if (workingSeq < tSeq) - // Transactions are missing before `tx`. - multiTxn.reset(); } - - if (multiTxn) + else if (!replacedTxIter) { - /* Check if the total fees in flight are greater - than the account's current balance, or the - minimum reserve. If it is, then there's a risk - that the fees won't get paid, so drop this - transaction with a telCAN_NOT_QUEUE_BALANCE result. - TODO: Decide whether to count the current txn fee - in this limit if it's the last transaction for - this account. Currently, it will not count, - for the same reason that it is not checked on - the first transaction. - Assume: Minimum account reserve is 20 XRP. - Example 1: If I have 1,000,000 XRP, I can queue - a transaction with a 1,000,000 XRP fee. In - the meantime, some other transaction may - lower my balance (eg. taking an offer). When - the transaction executes, I will either - spend the 1,000,000 XRP, or the transaction - will get stuck in the queue with a - `terINSUF_FEE_B`. - Example 2: If I have 1,000,000 XRP, and I queue - 10 transactions with 0.1 XRP fee, I have 1 XRP - in flight. I can now queue another tx with a - 999,999 XRP fee. When the first 10 execute, - they're guaranteed to pay their fee, because - nothing can eat into my reserve. The last - transaction, again, will either spend the - 999,999 XRP, or get stuck in the queue. - Example 3: If I have 1,000,000 XRP, and I queue - 7 transactions with 3 XRP fee, I have 21 XRP - in flight. I can not queue any more transactions, - no matter how small or large the fee. - Transactions stuck in the queue are mitigated by - LastLedgerSeq and MaybeTx::retriesRemaining. - */ - auto const balance = (*sle)[sfBalance].xrp(); - /* Get the minimum possible reserve. If fees exceed - this amount, the transaction can't be queued. - Considering that typical fees are several orders - of magnitude smaller than any current or expected - future reserve, this calculation is simpler than - trying to figure out the potential changes to - the ownerCount that may occur to the account - as a result of these transactions, and removes - any need to account for other transactions that - may affect the owner count while these are queued. - */ - auto const reserve = view.fees().accountReserve(0); - auto totalFee = multiTxn->fee; - if (multiTxn->includeCurrentFee) - totalFee += (*tx)[sfFee].xrp(); - if (totalFee >= balance || totalFee >= reserve) - { - // Drop the current transaction - JLOG(j_.trace()) << "Ignoring transaction " << transactionID - << ". Total fees in flight too high."; - return {telCAN_NOT_QUEUE_BALANCE, false}; - } - - // Create the test view from the current view - multiTxn->applyView.emplace(&view, flags); - multiTxn->openView.emplace(&*multiTxn->applyView); - - auto const sleBump = - multiTxn->applyView->peek(keylet::account(account)); - if (!sleBump) - return {tefINTERNAL, false}; - - auto const potentialTotalSpend = multiTxn->fee + - std::min(balance - std::min(balance, reserve), - multiTxn->potentialSpend); - assert(potentialTotalSpend > XRPAmount{0}); - sleBump->setFieldAmount( - sfBalance, balance - potentialTotalSpend); - sleBump->setFieldU32(sfSequence, tSeq); + // The current transaction is not replacing a transaction + // in the queue. So apparently there's a transaction in + // front of this one in the queue. Make sure the current + // transaction fits in proper sequence order with the + // previous transaction or is a ticket. + if (txSeqProx.isSeq() && + nextQueuableSeqImpl(sleAccount, lock) != txSeqProx) + return {telCAN_NOT_QUEUE, false}; } + + // Sum fees and spending for all of the queued transactions + // so we know how much to remove from the account balance + // for the trial preclaim. + XRPAmount potentialSpend = beast::zero; + XRPAmount totalFee = beast::zero; + for (auto iter = txIter->first; iter != txIter->end; ++iter) + { + // If we're replacing this transaction don't include + // the replaced transaction's XRP spend. Otherwise add + // it to potentialSpend. + if (iter->first != txSeqProx) + { + totalFee += iter->second.consequences().fee(); + potentialSpend += + iter->second.consequences().potentialSpend(); + } + else if (std::next(iter) != txIter->end) + { + // The fee for the candidate transaction _should_ be + // counted if it's replacing a transaction in the middle + // of the queue. + totalFee += pfresult.consequences.fee(); + potentialSpend += pfresult.consequences.potentialSpend(); + } + } + + /* Check if the total fees in flight are greater + than the account's current balance, or the + minimum reserve. If it is, then there's a risk + that the fees won't get paid, so drop this + transaction with a telCAN_NOT_QUEUE_BALANCE result. + Assume: Minimum account reserve is 20 XRP. + Example 1: If I have 1,000,000 XRP, I can queue + a transaction with a 1,000,000 XRP fee. In + the meantime, some other transaction may + lower my balance (eg. taking an offer). When + the transaction executes, I will either + spend the 1,000,000 XRP, or the transaction + will get stuck in the queue with a + `terINSUF_FEE_B`. + Example 2: If I have 1,000,000 XRP, and I queue + 10 transactions with 0.1 XRP fee, I have 1 XRP + in flight. I can now queue another tx with a + 999,999 XRP fee. When the first 10 execute, + they're guaranteed to pay their fee, because + nothing can eat into my reserve. The last + transaction, again, will either spend the + 999,999 XRP, or get stuck in the queue. + Example 3: If I have 1,000,000 XRP, and I queue + 7 transactions with 3 XRP fee, I have 21 XRP + in flight. I can not queue any more transactions, + no matter how small or large the fee. + Transactions stuck in the queue are mitigated by + LastLedgerSeq and MaybeTx::retriesRemaining. + */ + auto const balance = (*sleAccount)[sfBalance].xrp(); + /* Get the minimum possible reserve. If fees exceed + this amount, the transaction can't be queued. + Considering that typical fees are several orders + of magnitude smaller than any current or expected + future reserve, this calculation is simpler than + trying to figure out the potential changes to + the ownerCount that may occur to the account + as a result of these transactions, and removes + any need to account for other transactions that + may affect the owner count while these are queued. + */ + auto const reserve = view.fees().accountReserve(0); + if (totalFee >= balance || totalFee >= reserve) + { + // Drop the current transaction + JLOG(j_.trace()) << "Ignoring transaction " << transactionID + << ". Total fees in flight too high."; + return {telCAN_NOT_QUEUE_BALANCE, false}; + } + + // Create the test view from the current view. + multiTxn.emplace(view, flags); + + auto const sleBump = multiTxn->applyView.peek(accountKey); + if (!sleBump) + return {tefINTERNAL, false}; + + // Subtract the fees and XRP spend from all of the other + // transactions in the queue. That prevents a transaction + // inserted in the middle from fouling up later transactions. + auto const potentialTotalSpend = totalFee + + std::min(balance - std::min(balance, reserve), potentialSpend); + assert(potentialTotalSpend > XRPAmount{0}); + sleBump->setFieldAmount(sfBalance, balance - potentialTotalSpend); } } // See if the transaction is likely to claim a fee. - assert(!multiTxn || multiTxn->openView); - auto const pcresult = - preclaim(pfresult, app, multiTxn ? *multiTxn->openView : view); + // + // We assume that if the transaction survives preclaim(), then it + // is likely to claim a fee. However we can't allow preclaim to + // check the sequence/ticket. Transactions in the queue may be + // responsible for increasing the sequence, and mocking those up + // is non-trivially expensive. + // + // Note that earlier code has already verified that the sequence/ticket + // is valid. So we use a special entry point that runs all of the + // preclaim checks with the exception of the sequence check. + auto const pcresult = ForTxQ::preclaimWithoutSeqCheck( + pfresult, app, multiTxn ? multiTxn->openView : view); if (!pcresult.likelyToClaimFee) return {pcresult.ter, false}; @@ -930,38 +1148,36 @@ TxQ::apply( << " to get in the open ledger, which has " << view.txCount() << " entries."; - /* Quick heuristic check to see if it's worth checking that this - tx has a high enough fee to clear all the txs in the queue. - 1) Transaction is trying to get into the open ledger - 2) Must be an account already in the queue. - 3) Must be have passed the multiTxn checks (tx is not the next + /* Quick heuristic check to see if it's worth checking that this tx has + a high enough fee to clear all the txs in front of it in the queue. + 1) Transaction is trying to get into the open ledger. + 2) Transaction must be Sequence-based. + 3) Must be an account already in the queue. + 4) Must be have passed the multiTxn checks (tx is not the next account seq, the skipped seqs are in the queue, the reserve doesn't get exhausted, etc). - 4) The next transaction must not have previously tried and failed + 5) The next transaction must not have previously tried and failed to apply to an open ledger. - 5) Tx must be paying more than just the required fee level to + 6) Tx must be paying more than just the required fee level to get itself into the queue. - 6) Fee level must be escalated above the default (if it's not, + 7) Fee level must be escalated above the default (if it's not, then the first tx _must_ have failed to process in `accept` for some other reason. Tx is allowed to queue in case conditions change, but don't waste the effort to clear). - 7) Tx is not a 0-fee / free transaction, regardless of fee level. */ - if (!(flags & tapPREFER_QUEUE) && accountExists && + if (!(flags & tapPREFER_QUEUE) && txSeqProx.isSeq() && txIter && multiTxn.is_initialized() && - multiTxn->nextTxIter->second.retriesRemaining == - MaybeTx::retriesAllowed && - feeLevelPaid > requiredFeeLevel && requiredFeeLevel > baseLevel && - baseFee != 0) + txIter->first->second.retriesRemaining == MaybeTx::retriesAllowed && + feeLevelPaid > requiredFeeLevel && requiredFeeLevel > baseLevel) { OpenView sandbox(open_ledger, &view, view.rules()); - auto result = tryClearAccountQueue( + auto result = tryClearAccountQueueUpThruTx( app, sandbox, *tx, accountIter, - multiTxn->nextTxIter, + txIter->first, feeLevelPaid, pfresult, view.txCount(), @@ -971,47 +1187,31 @@ TxQ::apply( if (result.second) { sandbox.apply(view); - /* Can't erase(*replacedItemDeleteIter) here because success + /* Can't erase (*replacedTxIter) here because success implies that it has already been deleted. */ return result; } } - // Can transaction go in open ledger? - if (!multiTxn && feeLevelPaid >= requiredFeeLevel) - { - // Transaction fee is sufficient to go in open ledger immediately - - JLOG(j_.trace()) << "Applying transaction " << transactionID - << " to open ledger."; - - auto const [txnResult, didApply] = doApply(pcresult, app, view); - - JLOG(j_.trace()) << "New transaction " << transactionID - << (didApply ? " applied successfully with " - : " failed with ") - << transToken(txnResult); - - if (didApply && replacedItemDeleteIter) - erase(*replacedItemDeleteIter); - return {txnResult, didApply}; - } - // If `multiTxn` has a value, then `canBeHeld` has already been verified - if (!multiTxn && - !canBeHeld(*tx, flags, view, accountIter, replacedItemDeleteIter)) + if (!multiTxn) { - // Bail, transaction cannot be held - JLOG(j_.trace()) << "Transaction " << transactionID - << " can not be held"; - return {telCAN_NOT_QUEUE, false}; + TER const ter{canBeHeld( + *tx, flags, view, sleAccount, accountIter, replacedTxIter, lock)}; + if (!isTesSuccess(ter)) + { + // Bail, transaction cannot be held + JLOG(j_.trace()) + << "Transaction " << transactionID << " cannot be held"; + return {ter, false}; + } } // If the queue is full, decide whether to drop the current // transaction or the last transaction for the account with // the lowest fee. - if (!replacedItemDeleteIter && isFull()) + if (!replacedTxIter && isFull()) { auto lastRIter = byFee_.rbegin(); if (lastRIter->account == account) @@ -1036,16 +1236,17 @@ TxQ::apply( endAccount.transactions.begin(), endAccount.transactions.end(), std::pair(0, 0), - [&](auto const& total, auto const& txn) { + [&](auto const& total, + auto const& txn) -> std::pair { // Check for overflow. auto next = txn.second.feeLevel / endAccount.transactions.size(); auto mod = txn.second.feeLevel % endAccount.transactions.size(); if (total.first >= max - next || total.second >= max - mod) - return std::make_pair(max, FeeLevel64{0}); - return std::make_pair( - total.first + next, total.second + mod); + return {max, FeeLevel64{0}}; + + return {total.first + next, total.second + mod}; }); return endTotal.first + endTotal.second / endAccount.transactions.size(); @@ -1073,9 +1274,12 @@ TxQ::apply( } // Hold the transaction in the queue. - if (replacedItemDeleteIter) - erase(*replacedItemDeleteIter); - if (!accountExists) + if (replacedTxIter) + { + replacedTxIter = removeFromByFee(replacedTxIter, tx); + } + + if (!accountIsInQueue) { // Create a new TxQAccount object and add the byAccount lookup. bool created; @@ -1097,17 +1301,12 @@ TxQ::apply( auto& candidate = accountIter->second.add( {tx, transactionID, feeLevelPaid, flags, pfresult}); - /* Normally we defer figuring out the consequences until - something later requires us to, but if we know the - consequences now, save them for later. - */ - if (consequences) - candidate.consequences.emplace(*consequences); + // Then index it into the byFee lookup. byFee_.insert(candidate); JLOG(j_.debug()) << "Added transaction " << candidate.txID << " with result " << transToken(pfresult.ter) << " from " - << (accountExists ? "existing" : "new") << " account " + << (accountIsInQueue ? "existing" : "new") << " account " << candidate.account << " to queue." << " Flags: " << flags; @@ -1208,15 +1407,18 @@ TxQ::accept(Application& app, OpenView& view) std::lock_guard lock(mutex_); - auto const metricSnapshot = feeMetrics_.getSnapshot(); + auto const metricsSnapshot = feeMetrics_.getSnapshot(); for (auto candidateIter = byFee_.begin(); candidateIter != byFee_.end();) { auto& account = byAccount_.at(candidateIter->account); - if (candidateIter->sequence > account.transactions.begin()->first) + auto const beginIter = account.transactions.begin(); + if (candidateIter->seqProxy.isSeq() && + candidateIter->seqProxy > beginIter->first) { - // This is not the first transaction for this account, so skip it. - // It can not succeed yet. + // There is a sequence transaction at the front of the queue and + // candidate has a later sequence, so skip this candidate. We + // need to process sequence-based transactions in sequence order. JLOG(j_.trace()) << "Skipping queued transaction " << candidateIter->txID << " from account " << candidateIter->account @@ -1225,7 +1427,7 @@ TxQ::accept(Application& app, OpenView& view) continue; } auto const requiredFeeLevel = - FeeMetrics::scaleFeeLevel(metricSnapshot, view); + getRequiredFeeLevel(view, tapNONE, metricsSnapshot, lock); auto const feeLevelPaid = candidateIter->feeLevel; JLOG(j_.trace()) << "Queued transaction " << candidateIter->txID << " from account " << candidateIter->account @@ -1233,8 +1435,6 @@ TxQ::accept(Application& app, OpenView& view) << " needs at least " << requiredFeeLevel; if (feeLevelPaid >= requiredFeeLevel) { - auto firstTxn = candidateIter->txn; - JLOG(j_.trace()) << "Applying queued transaction " << candidateIter->txID << " to open ledger."; @@ -1280,25 +1480,46 @@ TxQ::accept(Application& app, OpenView& view) if (account.dropPenalty && account.transactions.size() > 1 && isFull<95>()) { - /* The queue is close to full, this account has multiple - txs queued, and this account has had a transaction - fail. Even though we're giving this transaction another - chance, chances are it won't recover. So we don't make - things worse, drop the _last_ transaction for this - account. - */ - auto dropRIter = account.transactions.rbegin(); - assert(dropRIter->second.account == candidateIter->account); - JLOG(j_.warn()) << "Queue is nearly full, and transaction " - << candidateIter->txID << " failed with " - << transToken(txnResult) - << ". Removing last item of account " - << account.account; - auto endIter = byFee_.iterator_to(dropRIter->second); - assert(endIter != candidateIter); - erase(endIter); + // The queue is close to full, this account has multiple + // txs queued, and this account has had a transaction + // fail. + if (candidateIter->seqProxy.isTicket()) + { + // Since the failed transaction has a ticket, order + // doesn't matter. Drop this one. + JLOG(j_.warn()) + << "Queue is nearly full, and transaction " + << candidateIter->txID << " failed with " + << transToken(txnResult) + << ". Removing ticketed tx from account " + << account.account; + candidateIter = eraseAndAdvance(candidateIter); + } + else + { + // Even though we're giving this transaction another + // chance, chances are it won't recover. To avoid + // making things worse, drop the _last_ transaction for + // this account. + auto dropRIter = account.transactions.rbegin(); + assert( + dropRIter->second.account == + candidateIter->account); + + JLOG(j_.warn()) + << "Queue is nearly full, and transaction " + << candidateIter->txID << " failed with " + << transToken(txnResult) + << ". Removing last item from account " + << account.account; + auto endIter = byFee_.iterator_to(dropRIter->second); + if (endIter != candidateIter) + erase(endIter); + ++candidateIter; + } } - ++candidateIter; + else + ++candidateIter; } } else @@ -1310,6 +1531,176 @@ TxQ::accept(Application& app, OpenView& view) return ledgerChanged; } +// Public entry point for nextQueuableSeq(). +// +// Acquires a lock and calls the implementation. +SeqProxy +TxQ::nextQueuableSeq(std::shared_ptr const& sleAccount) const +{ + std::lock_guard lock(mutex_); + return nextQueuableSeqImpl(sleAccount, lock); +} + +// The goal is to return a SeqProxy for a sequence that will fill the next +// available hole in the queue for the passed in account. +// +// If there are queued transactions for the account then the first viable +// sequence number, that is not used by a transaction in the queue, must +// be found and returned. +SeqProxy +TxQ::nextQueuableSeqImpl( + std::shared_ptr const& sleAccount, + std::lock_guard const&) const +{ + // If the account is not in the ledger or a non-account was passed + // then return zero. We have no idea. + if (!sleAccount || sleAccount->getType() != ltACCOUNT_ROOT) + return SeqProxy::sequence(0); + + SeqProxy const acctSeqProx = SeqProxy::sequence((*sleAccount)[sfSequence]); + + // If the account is not in the queue then acctSeqProx is good enough. + auto const accountIter = byAccount_.find((*sleAccount)[sfAccount]); + if (accountIter == byAccount_.end() || + accountIter->second.transactions.empty()) + return acctSeqProx; + + TxQAccount::TxMap const& acctTxs = accountIter->second.transactions; + + // Ignore any sequence-based queued transactions that slipped into the + // ledger while we were not watching. This does actually happen in the + // wild, but it's uncommon. + TxQAccount::TxMap::const_iterator txIter = acctTxs.lower_bound(acctSeqProx); + + if (txIter == acctTxs.end() || !txIter->first.isSeq() || + txIter->first != acctSeqProx) + // Either... + // o There are no queued sequence-based transactions equal to or + // following acctSeqProx or + // o acctSeqProx is not currently in the queue. + // So acctSeqProx is as good as it gets. + return acctSeqProx; + + // There are sequence-based transactions queued that follow acctSeqProx. + // Locate the first opening to put a transaction into. + SeqProxy attempt = txIter->second.consequences().followingSeq(); + while (++txIter != acctTxs.cend()) + { + if (attempt < txIter->first) + break; + + attempt = txIter->second.consequences().followingSeq(); + } + return attempt; +} + +FeeLevel64 +TxQ::getRequiredFeeLevel( + OpenView& view, + ApplyFlags flags, + FeeMetrics::Snapshot const& metricsSnapshot, + std::lock_guard const& lock) const +{ + FeeLevel64 const feeLevel = + FeeMetrics::scaleFeeLevel(metricsSnapshot, view); + + if ((flags & tapPREFER_QUEUE) && !byFee_.empty()) + return std::max(feeLevel, byFee_.begin()->feeLevel); + + return feeLevel; +} + +std::optional> +TxQ::tryDirectApply( + Application& app, + OpenView& view, + std::shared_ptr const& tx, + ApplyFlags flags, + beast::Journal j) +{ + auto const account = (*tx)[sfAccount]; + auto const sleAccount = view.read(keylet::account(account)); + + // Don't attempt to direct apply if the account is not in the ledger. + if (!sleAccount) + return {}; + + SeqProxy const acctSeqProx = SeqProxy::sequence((*sleAccount)[sfSequence]); + SeqProxy const txSeqProx = tx->getSeqProxy(); + + // Can only directly apply if the transaction sequence matches the account + // sequence or if the transaction uses a ticket. + if (txSeqProx.isSeq() && txSeqProx != acctSeqProx) + return {}; + + FeeLevel64 const requiredFeeLevel = [this, &view, flags]() { + std::lock_guard lock(mutex_); + return getRequiredFeeLevel( + view, flags, feeMetrics_.getSnapshot(), lock); + }(); + + // If the transaction's fee is high enough we may be able to put the + // transaction straight into the ledger. + FeeLevel64 const feeLevelPaid = getFeeLevelPaid(view, *tx); + + if (feeLevelPaid >= requiredFeeLevel) + { + // Attempt to apply the transaction directly. + auto const transactionID = tx->getTransactionID(); + JLOG(j_.trace()) << "Applying transaction " << transactionID + << " to open ledger."; + + auto const [txnResult, didApply] = + ripple::apply(app, view, *tx, flags, j); + + JLOG(j_.trace()) << "New transaction " << transactionID + << (didApply ? " applied successfully with " + : " failed with ") + << transToken(txnResult); + + if (didApply) + { + // If the applied transaction replaced a transaction in the + // queue then remove the replaced transaction. + std::lock_guard lock(mutex_); + + AccountMap::iterator accountIter = byAccount_.find(account); + if (accountIter != byAccount_.end()) + { + TxQAccount& txQAcct = accountIter->second; + if (auto const existingIter = + txQAcct.transactions.find(txSeqProx); + existingIter != txQAcct.transactions.end()) + { + removeFromByFee(existingIter, tx); + } + } + } + return {std::pair(txnResult, didApply)}; + } + return {}; +} + +boost::optional +TxQ::removeFromByFee( + boost::optional const& replacedTxIter, + std::shared_ptr const& tx) +{ + if (replacedTxIter && tx) + { + // If the transaction we're holding replaces a transaction in the + // queue, remove the transaction that is being replaced. + auto deleteIter = byFee_.iterator_to((*replacedTxIter)->second); + assert(deleteIter != byFee_.end()); + assert(&(*replacedTxIter)->second == &*deleteIter); + assert(deleteIter->seqProxy == tx->getSeqProxy()); + assert(deleteIter->account == (*tx)[sfAccount]); + + erase(deleteIter); + } + return boost::none; +} + TxQ::Metrics TxQ::getMetrics(OpenView const& view) const { @@ -1342,91 +1733,50 @@ TxQ::getTxRequiredFeeAndSeq( std::lock_guard lock(mutex_); auto const snapshot = feeMetrics_.getSnapshot(); - auto const baseFee = - view.fees().toDrops(calculateBaseFee(view, *tx)).second; + auto const baseFee = view.fees().toDrops(calculateBaseFee(view, *tx)); auto const fee = FeeMetrics::scaleFeeLevel(snapshot, view); - auto const accountSeq = [&view, &account]() -> std::uint32_t { - auto const sle = view.read(keylet::account(account)); - if (sle) - return (*sle)[sfSequence]; - return 0; - }(); + auto const sle = view.read(keylet::account(account)); - auto availableSeq = accountSeq; - - if (auto iter{byAccount_.find(account)}; iter != byAccount_.end()) - { - auto& txQAcct = iter->second; - for (auto const& [seq, _] : txQAcct.transactions) - { - (void)_; - if (seq >= availableSeq) - availableSeq = seq + 1; - } - } + std::uint32_t const accountSeq = sle ? (*sle)[sfSequence] : 0; + std::uint32_t const availableSeq = nextQueuableSeqImpl(sle, lock).value(); return {mulDiv(fee, baseFee, baseLevel).second, accountSeq, availableSeq}; } -auto +std::vector TxQ::getAccountTxs(AccountID const& account, ReadView const& view) const - -> std::map { + std::vector result; + std::lock_guard lock(mutex_); - auto accountIter = byAccount_.find(account); + AccountMap::const_iterator const accountIter{byAccount_.find(account)}; + if (accountIter == byAccount_.end() || accountIter->second.transactions.empty()) - return {}; - - std::map result; + return result; + result.reserve(accountIter->second.transactions.size()); for (auto const& tx : accountIter->second.transactions) { - result.emplace(tx.first, [&] { - AccountTxDetails resultTx; - resultTx.feeLevel = tx.second.feeLevel; - if (tx.second.lastValid) - resultTx.lastValid.emplace(*tx.second.lastValid); - if (tx.second.consequences) - resultTx.consequences.emplace(*tx.second.consequences); - return resultTx; - }()); + result.emplace_back(tx.second.getTxDetails()); } return result; } -auto -TxQ::getTxs(ReadView const& view) const -> std::vector +std::vector +TxQ::getTxs(ReadView const& view) const { + std::vector result; + std::lock_guard lock(mutex_); - if (byFee_.empty()) - return {}; - - std::vector result; result.reserve(byFee_.size()); for (auto const& tx : byFee_) - { - result.emplace_back([&] { - TxDetails resultTx; - resultTx.feeLevel = tx.feeLevel; - if (tx.lastValid) - resultTx.lastValid.emplace(*tx.lastValid); - if (tx.consequences) - resultTx.consequences.emplace(*tx.consequences); - resultTx.account = tx.account; - resultTx.txn = tx.txn; - resultTx.retriesRemaining = tx.retriesRemaining; - BOOST_ASSERT(tx.pfresult); - resultTx.preflightResult = tx.pfresult->ter; - if (tx.lastResult) - resultTx.lastResult.emplace(*tx.lastResult); - return resultTx; - }()); - } + result.emplace_back(tx.getTxDetails()); + return result; } @@ -1461,16 +1811,13 @@ TxQ::doRPC(Application& app) const auto const baseFee = view->fees().base; auto& drops = ret[jss::drops] = Json::Value(); - // Don't care about the overflow flags drops[jss::base_fee] = - to_string(toDrops(metrics.referenceFeeLevel, baseFee).second); + to_string(toDrops(metrics.referenceFeeLevel, baseFee)); drops[jss::minimum_fee] = - to_string(toDrops(metrics.minProcessingFeeLevel, baseFee).second); - drops[jss::median_fee] = - to_string(toDrops(metrics.medFeeLevel, baseFee).second); + to_string(toDrops(metrics.minProcessingFeeLevel, baseFee)); + drops[jss::median_fee] = to_string(toDrops(metrics.medFeeLevel, baseFee)); drops[jss::open_ledger_fee] = to_string( - toDrops(metrics.openLedgerFeeLevel - FeeLevel64{1}, baseFee).second + - 1); + toDrops(metrics.openLedgerFeeLevel - FeeLevel64{1}, baseFee) + 1); return ret; } @@ -1485,7 +1832,6 @@ setup_TxQ(Config const& config) set(setup.ledgersInQueue, "ledgers_in_queue", section); set(setup.queueSizeMin, "minimum_queue_size", section); set(setup.retrySequencePercent, "retry_sequence_percent", section); - set(setup.multiTxnPercent, "multi_txn_percent", section); set(setup.minimumEscalationMultiplier, "minimum_escalation_multiplier", section); @@ -1541,9 +1887,6 @@ setup_TxQ(Config const& config) set(setup.maximumTxnPerAccount, "maximum_txn_per_account", section); set(setup.minimumLastLedgerBuffer, "minimum_last_ledger_buffer", section); - set(setup.zeroBaseFeeTransactionFeeLevel, - "zero_basefee_transaction_feelevel", - section); setup.standAlone = config.standalone(); return setup; diff --git a/src/ripple/app/tx/README.md b/src/ripple/app/tx/README.md index 3c0d59379..f5d94e128 100644 --- a/src/ripple/app/tx/README.md +++ b/src/ripple/app/tx/README.md @@ -209,5 +209,3 @@ I believe that the disadvantages outweigh the advantages, but debate is welcome. ### CreateTicket ### - -### CancelTicket ### diff --git a/src/ripple/app/tx/applySteps.h b/src/ripple/app/tx/applySteps.h index 38be91beb..b9e731697 100644 --- a/src/ripple/app/tx/applySteps.h +++ b/src/ripple/app/tx/applySteps.h @@ -27,6 +27,7 @@ namespace ripple { class Application; class STTx; +class TxQ; /** Return true if the transaction can claim a fee (tec), and the `ApplyFlags` do not allow soft failures. @@ -37,6 +38,109 @@ isTecClaimHardFail(TER ter, ApplyFlags flags) return isTecClaim(ter) && !(flags & tapRETRY); } +/** Class describing the consequences to the account + of applying a transaction if the transaction consumes + the maximum XRP allowed. +*/ +class TxConsequences +{ +public: + /// Describes how the transaction affects subsequent + /// transactions + enum Category { + /// Moves currency around, creates offers, etc. + normal = 0, + /// Affects the ability of subsequent transactions + /// to claim a fee. Eg. `SetRegularKey` + blocker + }; + +private: + /// Describes how the transaction affects subsequent + /// transactions + bool isBlocker_; + /// Transaction fee + XRPAmount fee_; + /// Does NOT include the fee. + XRPAmount potentialSpend_; + /// SeqProxy of transaction. + SeqProxy seqProx_; + /// Number of sequences consumed. + std::uint32_t sequencesConsumed_; + +public: + // Constructor if preflight returns a value other than tesSUCCESS. + // Asserts if tesSUCCESS is passed. + explicit TxConsequences(NotTEC pfresult); + + /// Constructor if the STTx has no notable consequences for the TxQ. + explicit TxConsequences(STTx const& tx); + + /// Constructor for a blocker. + TxConsequences(STTx const& tx, Category category); + + /// Constructor for an STTx that may consume more XRP than the fee. + TxConsequences(STTx const& tx, XRPAmount potentialSpend); + + /// Constructor for an STTx that consumes more than the usual sequences. + TxConsequences(STTx const& tx, std::uint32_t sequencesConsumed); + + /// Copy constructor + TxConsequences(TxConsequences const&) = default; + /// Copy assignment operator + TxConsequences& + operator=(TxConsequences const&) = default; + /// Move constructor + TxConsequences(TxConsequences&&) = default; + /// Move assignment operator + TxConsequences& + operator=(TxConsequences&&) = default; + + /// Fee + XRPAmount + fee() const + { + return fee_; + } + + /// Potential Spend + XRPAmount const& + potentialSpend() const + { + return potentialSpend_; + } + + /// SeqProxy + SeqProxy + seqProxy() const + { + return seqProx_; + } + + /// Sequences consumed + std::uint32_t + sequencesConsumed() const + { + return sequencesConsumed_; + } + + /// Returns true if the transaction is a blocker. + bool + isBlocker() const + { + return isBlocker_; + } + + // Return the SeqProxy that would follow this. + SeqProxy + followingSeq() const + { + SeqProxy following = seqProx_; + following.advanceBy(sequencesConsumed()); + return following; + } +}; + /** Describes the results of the `preflight` check @note All members are const to make it more difficult @@ -50,6 +154,8 @@ public: STTx const& tx; /// From the input - the rules Rules const rules; + /// Consequences of the transaction + TxConsequences const consequences; /// From the input - the flags ApplyFlags const flags; /// From the input - the journal @@ -60,12 +166,15 @@ public: /// Constructor template - PreflightResult(Context const& ctx_, NotTEC ter_) + PreflightResult( + Context const& ctx_, + std::pair const& result) : tx(ctx_.tx) , rules(ctx_.rules) + , consequences(result.second) , flags(ctx_.flags) , j(ctx_.j) - , ter(ter_) + , ter(result.first) { } @@ -117,53 +226,6 @@ public: operator=(PreclaimResult const&) = delete; }; -/** Structure describing the consequences to the account - of applying a transaction if the transaction consumes - the maximum XRP allowed. - - @see calculateConsequences -*/ -struct TxConsequences -{ - /// Describes how the transaction affects subsequent - /// transactions - enum ConsequenceCategory { - /// Moves currency around, creates offers, etc. - normal = 0, - /// Affects the ability of subsequent transactions - /// to claim a fee. Eg. `SetRegularKey` - blocker - }; - - /// Describes how the transaction affects subsequent - /// transactions - ConsequenceCategory const category; - /// Transaction fee - XRPAmount const fee; - /// Does NOT include the fee. - XRPAmount const potentialSpend; - - /// Constructor - TxConsequences( - ConsequenceCategory const category_, - XRPAmount const fee_, - XRPAmount const spend_) - : category(category_), fee(fee_), potentialSpend(spend_) - { - } - - /// Constructor - TxConsequences(TxConsequences const&) = default; - /// Deleted copy assignment operator - TxConsequences& - operator=(TxConsequences const&) = delete; - /// Constructor - TxConsequences(TxConsequences&&) = default; - /// Deleted copy assignment operator - TxConsequences& - operator=(TxConsequences&&) = delete; -}; - /** Gate a transaction based on static information. The transaction is checked against all possible @@ -222,6 +284,29 @@ preclaim( Application& app, OpenView const& view); +// There are two special entry points that are only intended for use by the +// TxQ. To help enforce that this class contains the two functions as +// static members. +class ForTxQ +{ +private: + friend TxQ; + + // This entry point runs all of the preclaim checks with the lone exception + // of verifying Sequence or Ticket validity. This allows the TxQ to + // perform its own similar checks without needing to construct a bogus view. + static PreclaimResult + preclaimWithoutSeqCheck( + PreflightResult const& preflightResult, + Application& app, + OpenView const& view); + + // Checks the sequence number explicitly. Used by the TxQ in the case + // where the preclaim sequence number check was skipped earlier. + static TER + seqCheck(OpenView& view, STTx const& tx, beast::Journal j); +}; + /** Compute only the expected base fee for a transaction. Base fees are transaction specific, so any calculation @@ -241,23 +326,19 @@ preclaim( FeeUnit64 calculateBaseFee(ReadView const& view, STTx const& tx); -/** Determine the XRP balance consequences if a transaction - consumes the maximum XRP allowed. +/** Return the minimum fee that an "ordinary" transaction would pay. - @pre The transaction has been checked - and validated using `preflight` + When computing the FeeLevel for a transaction the TxQ sometimes needs + the know what an "ordinary" or reference transaction would be required + to pay. - @param preflightResult The result of a previous - call to `preflight` for the transaction. + @param view The current open ledger. + @param tx The transaction so the correct multisigner count is used. - @return A `TxConsequences` object containing the "worst - case" consequences of applying this transaction to - a ledger. - - @see TxConsequences + @return The base fee in XRPAmount. */ -TxConsequences -calculateConsequences(PreflightResult const& preflightResult); +XRPAmount +calculateDefaultBaseFee(ReadView const& view, STTx const& tx); /** Apply a prechecked transaction to an OpenView. diff --git a/src/ripple/app/tx/impl/CancelCheck.cpp b/src/ripple/app/tx/impl/CancelCheck.cpp index 1b62d9f72..5e7050d86 100644 --- a/src/ripple/app/tx/impl/CancelCheck.cpp +++ b/src/ripple/app/tx/impl/CancelCheck.cpp @@ -108,7 +108,7 @@ CancelCheck::doApply() if (!view().dirRemove( keylet::ownerDir(dstId), page, sleCheck->key(), true)) { - JLOG(j_.warn()) << "Unable to delete check from destination."; + JLOG(j_.fatal()) << "Unable to delete check from destination."; return tefBAD_LEDGER; } } @@ -117,7 +117,7 @@ CancelCheck::doApply() if (!view().dirRemove( keylet::ownerDir(srcId), page, sleCheck->key(), true)) { - JLOG(j_.warn()) << "Unable to delete check from owner."; + JLOG(j_.fatal()) << "Unable to delete check from owner."; return tefBAD_LEDGER; } } diff --git a/src/ripple/app/tx/impl/CancelCheck.h b/src/ripple/app/tx/impl/CancelCheck.h index cec434c8a..a7e0f6e5d 100644 --- a/src/ripple/app/tx/impl/CancelCheck.h +++ b/src/ripple/app/tx/impl/CancelCheck.h @@ -27,6 +27,8 @@ namespace ripple { class CancelCheck : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + explicit CancelCheck(ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/CancelOffer.h b/src/ripple/app/tx/impl/CancelOffer.h index 36efdeb27..32aee3353 100644 --- a/src/ripple/app/tx/impl/CancelOffer.h +++ b/src/ripple/app/tx/impl/CancelOffer.h @@ -30,6 +30,8 @@ namespace ripple { class CancelOffer : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + explicit CancelOffer(ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/CancelTicket.cpp b/src/ripple/app/tx/impl/CancelTicket.cpp deleted file mode 100644 index c69cd2107..000000000 --- a/src/ripple/app/tx/impl/CancelTicket.cpp +++ /dev/null @@ -1,95 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2014 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 - -namespace ripple { - -NotTEC -CancelTicket::preflight(PreflightContext const& ctx) -{ - if (!ctx.rules.enabled(featureTickets)) - return temDISABLED; - - if (ctx.tx.getFlags() & tfUniversalMask) - return temINVALID_FLAG; - - auto const ret = preflight1(ctx); - if (!isTesSuccess(ret)) - return ret; - - return preflight2(ctx); -} - -TER -CancelTicket::doApply() -{ - uint256 const ticketId = ctx_.tx.getFieldH256(sfTicketID); - - // VFALCO This is highly suspicious, we're requiring that the - // transaction provide the return value of getTicketIndex? - SLE::pointer sleTicket = view().peek(keylet::ticket(ticketId)); - - if (!sleTicket) - return tecNO_ENTRY; - - auto const ticket_owner = sleTicket->getAccountID(sfAccount); - - bool authorized = account_ == ticket_owner; - - // The target can also always remove a ticket - if (!authorized && sleTicket->isFieldPresent(sfTarget)) - authorized = (account_ == sleTicket->getAccountID(sfTarget)); - - // And finally, anyone can remove an expired ticket - if (!authorized && sleTicket->isFieldPresent(sfExpiration)) - { - using tp = NetClock::time_point; - using d = tp::duration; - auto const expiration = tp{d{sleTicket->getFieldU32(sfExpiration)}}; - - if (view().parentCloseTime() >= expiration) - authorized = true; - } - - if (!authorized) - return tecNO_PERMISSION; - - std::uint64_t const hint(sleTicket->getFieldU64(sfOwnerNode)); - - if (!ctx_.view().dirRemove( - keylet::ownerDir(ticket_owner), hint, ticketId, false)) - { - return tefBAD_LEDGER; - } - - auto viewJ = ctx_.app.journal("View"); - adjustOwnerCount( - view(), view().peek(keylet::account(ticket_owner)), -1, viewJ); - ctx_.view().erase(sleTicket); - - return tesSUCCESS; -} - -} // namespace ripple diff --git a/src/ripple/app/tx/impl/CancelTicket.h b/src/ripple/app/tx/impl/CancelTicket.h deleted file mode 100644 index 9cf2bc7dc..000000000 --- a/src/ripple/app/tx/impl/CancelTicket.h +++ /dev/null @@ -1,45 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2014 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_CANCELTICKET_H_INCLUDED -#define RIPPLE_TX_CANCELTICKET_H_INCLUDED - -#include -#include -#include - -namespace ripple { - -class CancelTicket : public Transactor -{ -public: - explicit CancelTicket(ApplyContext& ctx) : Transactor(ctx) - { - } - - static NotTEC - preflight(PreflightContext const& ctx); - - TER - doApply() override; -}; - -} // namespace ripple - -#endif diff --git a/src/ripple/app/tx/impl/CashCheck.cpp b/src/ripple/app/tx/impl/CashCheck.cpp index 753ecf8a7..9ab6e21e8 100644 --- a/src/ripple/app/tx/impl/CashCheck.cpp +++ b/src/ripple/app/tx/impl/CashCheck.cpp @@ -391,7 +391,7 @@ CashCheck::doApply() if (!ctx_.view().dirRemove( keylet::ownerDir(account_), page, sleCheck->key(), true)) { - JLOG(j_.warn()) << "Unable to delete check from destination."; + JLOG(j_.fatal()) << "Unable to delete check from destination."; return tefBAD_LEDGER; } } @@ -401,7 +401,7 @@ CashCheck::doApply() if (!ctx_.view().dirRemove( keylet::ownerDir(srcId), page, sleCheck->key(), true)) { - JLOG(j_.warn()) << "Unable to delete check from owner."; + JLOG(j_.fatal()) << "Unable to delete check from owner."; return tefBAD_LEDGER; } } diff --git a/src/ripple/app/tx/impl/CashCheck.h b/src/ripple/app/tx/impl/CashCheck.h index c8c863d3e..3acc3204a 100644 --- a/src/ripple/app/tx/impl/CashCheck.h +++ b/src/ripple/app/tx/impl/CashCheck.h @@ -27,6 +27,8 @@ namespace ripple { class CashCheck : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + explicit CashCheck(ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/Change.cpp b/src/ripple/app/tx/impl/Change.cpp index 4589b00de..8c34f532d 100644 --- a/src/ripple/app/tx/impl/Change.cpp +++ b/src/ripple/app/tx/impl/Change.cpp @@ -58,7 +58,8 @@ Change::preflight(PreflightContext const& ctx) return temBAD_SIGNATURE; } - if (ctx.tx.getSequence() != 0 || ctx.tx.isFieldPresent(sfPreviousTxnID)) + if (ctx.tx.getFieldU32(sfSequence) != 0 || + ctx.tx.isFieldPresent(sfPreviousTxnID)) { JLOG(ctx.j.warn()) << "Change: Bad sequence"; return temBAD_SEQUENCE; @@ -116,7 +117,6 @@ Change::doApply() void Change::preCompute() { - account_ = ctx_.tx.getAccountID(sfAccount); assert(account_ == beast::zero); } diff --git a/src/ripple/app/tx/impl/Change.h b/src/ripple/app/tx/impl/Change.h index 9f7927841..acd21837e 100644 --- a/src/ripple/app/tx/impl/Change.h +++ b/src/ripple/app/tx/impl/Change.h @@ -32,6 +32,8 @@ namespace ripple { class Change : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + explicit Change(ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/CreateCheck.cpp b/src/ripple/app/tx/impl/CreateCheck.cpp index edba73c70..509bd2a3e 100644 --- a/src/ripple/app/tx/impl/CreateCheck.cpp +++ b/src/ripple/app/tx/impl/CreateCheck.cpp @@ -183,11 +183,13 @@ CreateCheck::doApply() return tecINSUFFICIENT_RESERVE; } - AccountID const dstAccountId{ctx_.tx[sfDestination]}; - std::uint32_t const seq{ctx_.tx.getSequence()}; + // Note that we we use the value from the sequence or ticket as the + // Check sequence. For more explanation see comments in SeqProxy.h. + std::uint32_t const seq = ctx_.tx.getSeqProxy().value(); auto sleCheck = std::make_shared(keylet::check(account_, seq)); sleCheck->setAccountID(sfAccount, account_); + AccountID const dstAccountId = ctx_.tx[sfDestination]; sleCheck->setAccountID(sfDestination, dstAccountId); sleCheck->setFieldU32(sfSequence, seq); sleCheck->setFieldAmount(sfSendMax, ctx_.tx[sfSendMax]); diff --git a/src/ripple/app/tx/impl/CreateCheck.h b/src/ripple/app/tx/impl/CreateCheck.h index aa2639bff..537b230eb 100644 --- a/src/ripple/app/tx/impl/CreateCheck.h +++ b/src/ripple/app/tx/impl/CreateCheck.h @@ -27,6 +27,8 @@ namespace ripple { class CreateCheck : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + explicit CreateCheck(ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/CreateOffer.cpp b/src/ripple/app/tx/impl/CreateOffer.cpp index f0fa487cb..e7006c4cc 100644 --- a/src/ripple/app/tx/impl/CreateOffer.cpp +++ b/src/ripple/app/tx/impl/CreateOffer.cpp @@ -29,12 +29,15 @@ namespace ripple { -XRPAmount -CreateOffer::calculateMaxSpend(STTx const& tx) +TxConsequences +CreateOffer::makeTxConsequences(PreflightContext const& ctx) { - auto const& saTakerGets = tx[sfTakerGets]; + auto calculateMaxXRPSpend = [](STTx const& tx) -> XRPAmount { + auto const& amount{tx[sfTakerGets]}; + return amount.native() ? amount.xrp() : beast::zero; + }; - return saTakerGets.native() ? saTakerGets.xrp() : beast::zero; + return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)}; } NotTEC @@ -1143,10 +1146,9 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) auto const cancelSequence = ctx_.tx[~sfOfferSequence]; - // FIXME understand why we use SequenceNext instead of current transaction - // sequence to determine the transaction. Why is the offer sequence - // number insufficient? - auto const uSequence = ctx_.tx.getSequence(); + // Note that we we use the value from the sequence or ticket as the + // offer sequence. For more explanation see comments in SeqProxy.h. + auto const offerSequence = ctx_.tx.getSeqProxy().value(); // This is the original rate of the offer, and is the rate at which // it will be placed, even if crossing offers change the amounts that @@ -1373,7 +1375,7 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) } // We need to place the remainder of the offer into its order book. - auto const offer_index = keylet::offer(account_, uSequence); + auto const offer_index = keylet::offer(account_, offerSequence); // Add offer to owner's directory. auto const ownerNode = sb.dirInsert( @@ -1415,7 +1417,7 @@ CreateOffer::applyGuts(Sandbox& sb, Sandbox& sbCancel) auto sleOffer = std::make_shared(offer_index); sleOffer->setAccountID(sfAccount, account_); - sleOffer->setFieldU32(sfSequence, uSequence); + sleOffer->setFieldU32(sfSequence, offerSequence); sleOffer->setFieldH256(sfBookDirectory, dir.key); sleOffer->setFieldAmount(sfTakerPays, saTakerPays); sleOffer->setFieldAmount(sfTakerGets, saTakerGets); diff --git a/src/ripple/app/tx/impl/CreateOffer.h b/src/ripple/app/tx/impl/CreateOffer.h index e7838e0f1..597ee22c0 100644 --- a/src/ripple/app/tx/impl/CreateOffer.h +++ b/src/ripple/app/tx/impl/CreateOffer.h @@ -34,15 +34,16 @@ class Sandbox; class CreateOffer : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + /** Construct a Transactor subclass that creates an offer in the ledger. */ explicit CreateOffer(ApplyContext& ctx) : Transactor(ctx), stepCounter_(1000, j_) { } - /** Override default behavior provided by Transactor base class. */ - static XRPAmount - calculateMaxSpend(STTx const& tx); + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); /** Enforce constraints beyond those of the Transactor base class. */ static NotTEC diff --git a/src/ripple/app/tx/impl/CreateTicket.cpp b/src/ripple/app/tx/impl/CreateTicket.cpp index 4d47ae59e..6e074d958 100644 --- a/src/ripple/app/tx/impl/CreateTicket.cpp +++ b/src/ripple/app/tx/impl/CreateTicket.cpp @@ -23,109 +23,138 @@ #include #include #include +#include namespace ripple { +TxConsequences +CreateTicket::makeTxConsequences(PreflightContext const& ctx) +{ + // Create TxConsequences identifying the number of sequences consumed. + return TxConsequences{ctx.tx, ctx.tx[sfTicketCount]}; +} + NotTEC CreateTicket::preflight(PreflightContext const& ctx) { - if (!ctx.rules.enabled(featureTickets)) + if (!ctx.rules.enabled(featureTicketBatch)) return temDISABLED; if (ctx.tx.getFlags() & tfUniversalMask) return temINVALID_FLAG; - auto const ret = preflight1(ctx); - if (!isTesSuccess(ret)) - return ret; + if (std::uint32_t const count = ctx.tx[sfTicketCount]; + count < minValidCount || count > maxValidCount) + return temINVALID_COUNT; - if (ctx.tx.isFieldPresent(sfExpiration)) - { - if (ctx.tx.getFieldU32(sfExpiration) == 0) - { - JLOG(ctx.j.warn()) << "Malformed transaction: bad expiration"; - return temBAD_EXPIRATION; - } - } + if (NotTEC const ret{preflight1(ctx)}; !isTesSuccess(ret)) + return ret; return preflight2(ctx); } +TER +CreateTicket::preclaim(PreclaimContext const& ctx) +{ + auto const id = ctx.tx[sfAccount]; + auto const sleAccountRoot = ctx.view.read(keylet::account(id)); + if (!sleAccountRoot) + return terNO_ACCOUNT; + + // Make sure the TicketCreate would not cause the account to own + // too many tickets. + std::uint32_t const curTicketCount = + (*sleAccountRoot)[~sfTicketCount].value_or(0u); + std::uint32_t const addedTickets = ctx.tx[sfTicketCount]; + std::uint32_t const consumedTickets = + ctx.tx.getSeqProxy().isTicket() ? 1u : 0u; + + // Note that unsigned integer underflow can't currently happen because + // o curTicketCount >= 0 + // o addedTickets >= 1 + // o consumedTickets <= 1 + // So in the worst case addedTickets == consumedTickets and the + // computation yields curTicketCount. + if (curTicketCount + addedTickets - consumedTickets > maxTicketThreshold) + return tecDIR_FULL; + + return tesSUCCESS; +} + TER CreateTicket::doApply() { - auto const sle = view().peek(keylet::account(account_)); - if (!sle) + SLE::pointer const sleAccountRoot = view().peek(keylet::account(account_)); + if (!sleAccountRoot) return tefINTERNAL; - // A ticket counts against the reserve of the issuing account, but we + // Each ticket counts against the reserve of the issuing account, but we // check the starting balance because we want to allow dipping into the // reserve to pay fees. + std::uint32_t const ticketCount = ctx_.tx[sfTicketCount]; { - auto const reserve = - view().fees().accountReserve(sle->getFieldU32(sfOwnerCount) + 1); + XRPAmount const reserve = view().fees().accountReserve( + sleAccountRoot->getFieldU32(sfOwnerCount) + ticketCount); if (mPriorBalance < reserve) return tecINSUFFICIENT_RESERVE; } - NetClock::time_point expiration{}; + beast::Journal viewJ{ctx_.app.journal("View")}; - if (ctx_.tx.isFieldPresent(sfExpiration)) + // The starting ticket sequence is the same as the current account + // root sequence. Before we got here to doApply(), the transaction + // machinery already incremented the account root sequence if that + // was appropriate. + std::uint32_t const firstTicketSeq = (*sleAccountRoot)[sfSequence]; + + // Sanity check that the transaction machinery really did already + // increment the account root Sequence. + if (std::uint32_t const txSeq = ctx_.tx[sfSequence]; + txSeq != 0 && txSeq != (firstTicketSeq - 1)) + return tefINTERNAL; + + for (std::uint32_t i = 0; i < ticketCount; ++i) { - expiration = - NetClock::time_point(NetClock::duration(ctx_.tx[sfExpiration])); + std::uint32_t const curTicketSeq = firstTicketSeq + i; - if (view().parentCloseTime() >= expiration) - return tesSUCCESS; + SLE::pointer sleTicket = std::make_shared( + ltTICKET, getTicketIndex(account_, curTicketSeq)); + + sleTicket->setAccountID(sfAccount, account_); + sleTicket->setFieldU32(sfTicketSequence, curTicketSeq); + view().insert(sleTicket); + + auto const page = dirAdd( + view(), + keylet::ownerDir(account_), + sleTicket->key(), + false, + describeOwnerDir(account_), + viewJ); + + JLOG(j_.trace()) << "Creating ticket " << to_string(sleTicket->key()) + << ": " << (page ? "success" : "failure"); + + if (!page) + return tecDIR_FULL; + + sleTicket->setFieldU64(sfOwnerNode, *page); } - SLE::pointer sleTicket = std::make_shared( - ltTICKET, getTicketIndex(account_, ctx_.tx.getSequence())); - sleTicket->setAccountID(sfAccount, account_); - sleTicket->setFieldU32(sfSequence, ctx_.tx.getSequence()); - if (expiration != NetClock::time_point{}) - sleTicket->setFieldU32( - sfExpiration, expiration.time_since_epoch().count()); - view().insert(sleTicket); + // Update the record of the number of Tickets this account owns. + std::uint32_t const oldTicketCount = + (*(sleAccountRoot))[~sfTicketCount].value_or(0u); - if (ctx_.tx.isFieldPresent(sfTarget)) - { - AccountID const target_account(ctx_.tx.getAccountID(sfTarget)); + sleAccountRoot->setFieldU32(sfTicketCount, oldTicketCount + ticketCount); - SLE::pointer sleTarget = view().peek(keylet::account(target_account)); + // Every added Ticket counts against the creator's reserve. + adjustOwnerCount(view(), sleAccountRoot, ticketCount, viewJ); - // Destination account does not exist. - if (!sleTarget) - return tecNO_TARGET; + // TicketCreate is the only transaction that can cause an account root's + // Sequence field to increase by more than one. October 2018. + sleAccountRoot->setFieldU32(sfSequence, firstTicketSeq + ticketCount); - // The issuing account is the default account to which the ticket - // applies so don't bother saving it if that's what's specified. - if (target_account != account_) - sleTicket->setAccountID(sfTarget, target_account); - } - - auto viewJ = ctx_.app.journal("View"); - - auto const page = dirAdd( - view(), - keylet::ownerDir(account_), - sleTicket->key(), - false, - describeOwnerDir(account_), - viewJ); - - JLOG(j_.trace()) << "Creating ticket " << to_string(sleTicket->key()) - << ": " << (page ? "success" : "failure"); - - if (!page) - return tecDIR_FULL; - - sleTicket->setFieldU64(sfOwnerNode, *page); - - // If we succeeded, the new entry counts against the - // creator's reserve. - adjustOwnerCount(view(), sle, 1, viewJ); return tesSUCCESS; } diff --git a/src/ripple/app/tx/impl/CreateTicket.h b/src/ripple/app/tx/impl/CreateTicket.h index e927cbd2f..c1909ddf7 100644 --- a/src/ripple/app/tx/impl/CreateTicket.h +++ b/src/ripple/app/tx/impl/CreateTicket.h @@ -30,13 +30,56 @@ namespace ripple { class CreateTicket : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + + constexpr static std::uint32_t minValidCount = 1; + + // A note on how the maxValidCount was determined. The goal is for + // a single TicketCreate transaction to not use more compute power than + // a single compute-intensive Payment. + // + // Timing was performed using a MacBook Pro laptop and a release build + // with asserts off. 20 measurements were taken of each of the Payment + // and TicketCreate transactions and averaged to get timings. + // + // For the example compute-intensive Payment a Discrepancy unit test + // unit test Payment with 3 paths was chosen. With all the latest + // amendments enabled, that Payment::doApply() operation took, on + // average, 1.25 ms. + // + // Using that same test set up creating 250 Tickets in a single + // CreateTicket::doApply() in a unit test took, on average, 1.21 ms. + // + // So, for the moment, a single transaction creating 250 Tickets takes + // about the same compute time as a single compute-intensive payment. + // + // October 2018. + constexpr static std::uint32_t maxValidCount = 250; + + // The maximum number of Tickets an account may hold. If a + // TicketCreate would cause an account to own more than this many + // tickets, then the TicketCreate will fail. + // + // The number was chosen arbitrarily and is an effort toward avoiding + // ledger-stuffing with Tickets. + constexpr static std::uint32_t maxTicketThreshold = 250; + explicit CreateTicket(ApplyContext& ctx) : Transactor(ctx) { } + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + + /** Enforce constraints beyond those of the Transactor base class. */ static NotTEC preflight(PreflightContext const& ctx); + /** Enforce constraints beyond those of the Transactor base class. */ + static TER + preclaim(PreclaimContext const& ctx); + + /** Precondition: fee collection is likely. Attempt to create ticket(s). */ TER doApply() override; }; diff --git a/src/ripple/app/tx/impl/DeleteAccount.cpp b/src/ripple/app/tx/impl/DeleteAccount.cpp index 7f9787142..1936dc4a0 100644 --- a/src/ripple/app/tx/impl/DeleteAccount.cpp +++ b/src/ripple/app/tx/impl/DeleteAccount.cpp @@ -100,7 +100,19 @@ removeSignersFromLedger( std::shared_ptr const& sleDel, beast::Journal j) { - return SetSignerList::removeFromLedger(app, view, account); + return SetSignerList::removeFromLedger(app, view, account, j); +} + +TER +removeTicketFromLedger( + Application&, + ApplyView& view, + AccountID const& account, + uint256 const& delIndex, + std::shared_ptr const&, + beast::Journal j) +{ + return Transactor::ticketDelete(view, account, delIndex, j); } TER @@ -115,9 +127,9 @@ removeDepositPreauthFromLedger( return DepositPreauth::removeFromLedger(app, view, delIndex, j); } -// Return nullptr if the LedgerEntryType represents an obligation that can't be -// deleted Otherwise return the pointer to the function that can delete the -// non-obligation +// Return nullptr if the LedgerEntryType represents an obligation that can't +// be deleted. Otherwise return the pointer to the function that can delete +// the non-obligation DeleterFuncPtr nonObligationDeleter(LedgerEntryType t) { @@ -127,7 +139,8 @@ nonObligationDeleter(LedgerEntryType t) return offerDelete; case ltSIGNER_LIST: return removeSignersFromLedger; - // case ltTICKET: return ???; + case ltTICKET: + return removeTicketFromLedger; case ltDEPOSIT_PREAUTH: return removeDepositPreauthFromLedger; default: diff --git a/src/ripple/app/tx/impl/DeleteAccount.h b/src/ripple/app/tx/impl/DeleteAccount.h index d7c563526..c01991ca7 100644 --- a/src/ripple/app/tx/impl/DeleteAccount.h +++ b/src/ripple/app/tx/impl/DeleteAccount.h @@ -29,6 +29,8 @@ namespace ripple { class DeleteAccount : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Blocker}; + // Set a reasonable upper limit on the number of deletable directory // entries an account may have before we decide the account can't be // deleted. @@ -41,12 +43,6 @@ public: { } - static bool - affectsSubsequentTransactionAuth(STTx const& tx) - { - return true; - } - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/ripple/app/tx/impl/DepositPreauth.h b/src/ripple/app/tx/impl/DepositPreauth.h index 2b73d1cce..8111c2156 100644 --- a/src/ripple/app/tx/impl/DepositPreauth.h +++ b/src/ripple/app/tx/impl/DepositPreauth.h @@ -27,6 +27,8 @@ namespace ripple { class DepositPreauth : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + explicit DepositPreauth(ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/Escrow.cpp b/src/ripple/app/tx/impl/Escrow.cpp index 30a74c138..e033c989a 100644 --- a/src/ripple/app/tx/impl/Escrow.cpp +++ b/src/ripple/app/tx/impl/Escrow.cpp @@ -90,10 +90,10 @@ after(NetClock::time_point now, std::uint32_t mark) return now.time_since_epoch().count() > mark; } -XRPAmount -EscrowCreate::calculateMaxSpend(STTx const& tx) +TxConsequences +EscrowCreate::makeTxConsequences(PreflightContext const& ctx) { - return tx[sfAmount].xrp(); + return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; } NotTEC @@ -229,9 +229,10 @@ EscrowCreate::doApply() return tecNO_TARGET; } - // Create escrow in ledger - auto const slep = - std::make_shared(keylet::escrow(account, (*sle)[sfSequence] - 1)); + // Create escrow in ledger. Note that we we use the value from the + // sequence or ticket. For more explanation see comments in SeqProxy.h. + auto const slep = std::make_shared( + keylet::escrow(account, ctx_.tx.getSeqProxy().value())); (*slep)[sfAmount] = ctx_.tx[sfAmount]; (*slep)[sfAccount] = account; (*slep)[~sfCondition] = ctx_.tx[~sfCondition]; @@ -480,6 +481,7 @@ EscrowFinish::doApply() if (!ctx_.view().dirRemove( keylet::ownerDir(account), page, k.key, true)) { + JLOG(j_.fatal()) << "Unable to delete Escrow from owner."; return tefBAD_LEDGER; } } @@ -490,6 +492,7 @@ EscrowFinish::doApply() if (!ctx_.view().dirRemove( keylet::ownerDir(destID), *optPage, k.key, true)) { + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; return tefBAD_LEDGER; } } @@ -561,6 +564,7 @@ EscrowCancel::doApply() if (!ctx_.view().dirRemove( keylet::ownerDir(account), page, k.key, true)) { + JLOG(j_.fatal()) << "Unable to delete Escrow from owner."; return tefBAD_LEDGER; } } @@ -574,6 +578,7 @@ EscrowCancel::doApply() k.key, true)) { + JLOG(j_.fatal()) << "Unable to delete Escrow from recipient."; return tefBAD_LEDGER; } } diff --git a/src/ripple/app/tx/impl/Escrow.h b/src/ripple/app/tx/impl/Escrow.h index bc915bdcb..20e57c85f 100644 --- a/src/ripple/app/tx/impl/Escrow.h +++ b/src/ripple/app/tx/impl/Escrow.h @@ -27,12 +27,14 @@ namespace ripple { class EscrowCreate : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + explicit EscrowCreate(ApplyContext& ctx) : Transactor(ctx) { } - static XRPAmount - calculateMaxSpend(STTx const& tx); + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); @@ -46,6 +48,8 @@ public: class EscrowFinish : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + explicit EscrowFinish(ApplyContext& ctx) : Transactor(ctx) { } @@ -65,6 +69,8 @@ public: class EscrowCancel : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + explicit EscrowCancel(ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/PayChan.cpp b/src/ripple/app/tx/impl/PayChan.cpp index a8bc20cb5..ebbd0064d 100644 --- a/src/ripple/app/tx/impl/PayChan.cpp +++ b/src/ripple/app/tx/impl/PayChan.cpp @@ -162,6 +162,12 @@ closeChannel( //------------------------------------------------------------------------------ +TxConsequences +PayChanCreate::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; +} + NotTEC PayChanCreate::preflight(PreflightContext const& ctx) { @@ -236,9 +242,12 @@ PayChanCreate::doApply() auto const dst = ctx_.tx[sfDestination]; - // Create PayChan in ledger + // Create PayChan in ledger. + // + // Note that we we use the value from the sequence or ticket as the + // payChan sequence. For more explanation see comments in SeqProxy.h. auto const slep = std::make_shared( - keylet::payChan(account, dst, (*sle)[sfSequence] - 1)); + keylet::payChan(account, dst, ctx_.tx.getSeqProxy().value())); // Funds held in this channel (*slep)[sfAmount] = ctx_.tx[sfAmount]; // Amount channel has already paid @@ -292,6 +301,12 @@ PayChanCreate::doApply() //------------------------------------------------------------------------------ +TxConsequences +PayChanFund::makeTxConsequences(PreflightContext const& ctx) +{ + return TxConsequences{ctx.tx, ctx.tx[sfAmount].xrp()}; +} + NotTEC PayChanFund::preflight(PreflightContext const& ctx) { @@ -429,6 +444,7 @@ PayChanClaim::preflight(PreflightContext const& ctx) Keylet const k(ltPAYCHAN, ctx.tx[sfPayChannel]); if (!publicKeyType(ctx.tx[sfPublicKey])) return temMALFORMED; + PublicKey const pk(ctx.tx[sfPublicKey]); Serializer msg; serializePayChanAuthorization(msg, k.key, authAmt); diff --git a/src/ripple/app/tx/impl/PayChan.h b/src/ripple/app/tx/impl/PayChan.h index f82a7841f..9fe4b8419 100644 --- a/src/ripple/app/tx/impl/PayChan.h +++ b/src/ripple/app/tx/impl/PayChan.h @@ -27,10 +27,15 @@ namespace ripple { class PayChanCreate : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + explicit PayChanCreate(ApplyContext& ctx) : Transactor(ctx) { } + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); @@ -46,10 +51,15 @@ public: class PayChanFund : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + explicit PayChanFund(ApplyContext& ctx) : Transactor(ctx) { } + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); + static NotTEC preflight(PreflightContext const& ctx); @@ -62,6 +72,8 @@ public: class PayChanClaim : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + explicit PayChanClaim(ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/Payment.cpp b/src/ripple/app/tx/impl/Payment.cpp index 982c846ca..55fffca93 100644 --- a/src/ripple/app/tx/impl/Payment.cpp +++ b/src/ripple/app/tx/impl/Payment.cpp @@ -30,18 +30,19 @@ namespace ripple { // See https://ripple.com/wiki/Transaction_Format#Payment_.280.29 -XRPAmount -Payment::calculateMaxSpend(STTx const& tx) +TxConsequences +Payment::makeTxConsequences(PreflightContext const& ctx) { - if (tx.isFieldPresent(sfSendMax)) - { - auto const& sendMax = tx[sfSendMax]; - return sendMax.native() ? sendMax.xrp() : beast::zero; - } - /* If there's no sfSendMax in XRP, and the sfAmount isn't - in XRP, then the transaction can not send XRP. */ - auto const& saDstAmount = tx.getFieldAmount(sfAmount); - return saDstAmount.native() ? saDstAmount.xrp() : beast::zero; + auto calculateMaxXRPSpend = [](STTx const& tx) -> XRPAmount { + STAmount const maxAmount = + tx.isFieldPresent(sfSendMax) ? tx[sfSendMax] : tx[sfAmount]; + + // If there's no sfSendMax in XRP, and the sfAmount isn't + // in XRP, then the transaction does not spend XRP. + return maxAmount.native() ? maxAmount.xrp() : beast::zero; + }; + + return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)}; } NotTEC diff --git a/src/ripple/app/tx/impl/Payment.h b/src/ripple/app/tx/impl/Payment.h index 33d0cac0c..9c2f35472 100644 --- a/src/ripple/app/tx/impl/Payment.h +++ b/src/ripple/app/tx/impl/Payment.h @@ -38,12 +38,14 @@ class Payment : public Transactor static std::size_t const MaxPathLength = 8; public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + explicit Payment(ApplyContext& ctx) : Transactor(ctx) { } - static XRPAmount - calculateMaxSpend(STTx const& tx); + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/ripple/app/tx/impl/SetAccount.cpp b/src/ripple/app/tx/impl/SetAccount.cpp index 0ee305d34..c965457fd 100644 --- a/src/ripple/app/tx/impl/SetAccount.cpp +++ b/src/ripple/app/tx/impl/SetAccount.cpp @@ -29,23 +29,30 @@ namespace ripple { -bool -SetAccount::affectsSubsequentTransactionAuth(STTx const& tx) +TxConsequences +SetAccount::makeTxConsequences(PreflightContext const& ctx) { - auto const uTxFlags = tx.getFlags(); - if (uTxFlags & (tfRequireAuth | tfOptionalAuth)) - return true; + // The SetAccount may be a blocker, but only if it sets or clears + // specific account flags. + auto getTxConsequencesCategory = [](STTx const& tx) { + if (std::uint32_t const uTxFlags = tx.getFlags(); + uTxFlags & (tfRequireAuth | tfOptionalAuth)) + return TxConsequences::blocker; - auto const uSetFlag = tx[~sfSetFlag]; - if (uSetFlag && - (*uSetFlag == asfRequireAuth || *uSetFlag == asfDisableMaster || - *uSetFlag == asfAccountTxnID)) - return true; + if (auto const uSetFlag = tx[~sfSetFlag]; uSetFlag && + (*uSetFlag == asfRequireAuth || *uSetFlag == asfDisableMaster || + *uSetFlag == asfAccountTxnID)) + return TxConsequences::blocker; - auto const uClearFlag = tx[~sfClearFlag]; - return uClearFlag && - (*uClearFlag == asfRequireAuth || *uClearFlag == asfDisableMaster || - *uClearFlag == asfAccountTxnID); + if (auto const uClearFlag = tx[~sfClearFlag]; uClearFlag && + (*uClearFlag == asfRequireAuth || *uClearFlag == asfDisableMaster || + *uClearFlag == asfAccountTxnID)) + return TxConsequences::blocker; + + return TxConsequences::normal; + }; + + return TxConsequences{ctx.tx, getTxConsequencesCategory(ctx.tx)}; } NotTEC diff --git a/src/ripple/app/tx/impl/SetAccount.h b/src/ripple/app/tx/impl/SetAccount.h index 37bd02dda..cb37c6ecd 100644 --- a/src/ripple/app/tx/impl/SetAccount.h +++ b/src/ripple/app/tx/impl/SetAccount.h @@ -34,12 +34,14 @@ class SetAccount : public Transactor static std::size_t const DOMAIN_BYTES_MAX = 256; public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Custom}; + explicit SetAccount(ApplyContext& ctx) : Transactor(ctx) { } - static bool - affectsSubsequentTransactionAuth(STTx const& tx); + static TxConsequences + makeTxConsequences(PreflightContext const& ctx); static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/ripple/app/tx/impl/SetRegularKey.h b/src/ripple/app/tx/impl/SetRegularKey.h index 12b882ae0..53d832c17 100644 --- a/src/ripple/app/tx/impl/SetRegularKey.h +++ b/src/ripple/app/tx/impl/SetRegularKey.h @@ -30,16 +30,12 @@ namespace ripple { class SetRegularKey : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Blocker}; + explicit SetRegularKey(ApplyContext& ctx) : Transactor(ctx) { } - static bool - affectsSubsequentTransactionAuth(STTx const& tx) - { - return true; - } - static NotTEC preflight(PreflightContext const& ctx); diff --git a/src/ripple/app/tx/impl/SetSignerList.cpp b/src/ripple/app/tx/impl/SetSignerList.cpp index 8ae7cd31f..f27fbcc79 100644 --- a/src/ripple/app/tx/impl/SetSignerList.cpp +++ b/src/ripple/app/tx/impl/SetSignerList.cpp @@ -177,7 +177,8 @@ removeSignersFromLedger( ApplyView& view, Keylet const& accountKeylet, Keylet const& ownerDirKeylet, - Keylet const& signerListKeylet) + Keylet const& signerListKeylet, + beast::Journal j) { // We have to examine the current SignerList so we know how much to // reduce the OwnerCount. @@ -203,6 +204,7 @@ removeSignersFromLedger( if (!view.dirRemove(ownerDirKeylet, hint, signerListKeylet.key, false)) { + JLOG(j.fatal()) << "Unable to delete SignerList from owner."; return tefBAD_LEDGER; } @@ -221,14 +223,15 @@ TER SetSignerList::removeFromLedger( Application& app, ApplyView& view, - AccountID const& account) + AccountID const& account, + beast::Journal j) { auto const accountKeylet = keylet::account(account); auto const ownerDirKeylet = keylet::ownerDir(account); auto const signerListKeylet = keylet::signers(account); return removeSignersFromLedger( - app, view, accountKeylet, ownerDirKeylet, signerListKeylet); + app, view, accountKeylet, ownerDirKeylet, signerListKeylet, j); } NotTEC @@ -299,7 +302,12 @@ SetSignerList::replaceSignerList() // old signer list. May reduce the reserve, so this is done before // checking the reserve. if (TER const ter = removeSignersFromLedger( - ctx_.app, view(), accountKeylet, ownerDirKeylet, signerListKeylet)) + ctx_.app, + view(), + accountKeylet, + ownerDirKeylet, + signerListKeylet, + j_)) return ter; auto const sle = view().peek(accountKeylet); @@ -373,7 +381,7 @@ SetSignerList::destroySignerList() auto const ownerDirKeylet = keylet::ownerDir(account_); auto const signerListKeylet = keylet::signers(account_); return removeSignersFromLedger( - ctx_.app, view(), accountKeylet, ownerDirKeylet, signerListKeylet); + ctx_.app, view(), accountKeylet, ownerDirKeylet, signerListKeylet, j_); } void diff --git a/src/ripple/app/tx/impl/SetSignerList.h b/src/ripple/app/tx/impl/SetSignerList.h index 0434a2a85..345d59402 100644 --- a/src/ripple/app/tx/impl/SetSignerList.h +++ b/src/ripple/app/tx/impl/SetSignerList.h @@ -48,16 +48,12 @@ private: std::vector signers_; public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Blocker}; + explicit SetSignerList(ApplyContext& ctx) : Transactor(ctx) { } - static bool - affectsSubsequentTransactionAuth(STTx const& tx) - { - return true; - } - static NotTEC preflight(PreflightContext const& ctx); @@ -71,7 +67,8 @@ public: removeFromLedger( Application& app, ApplyView& view, - AccountID const& account); + AccountID const& account, + beast::Journal j); private: static std::tuple< diff --git a/src/ripple/app/tx/impl/SetTrust.h b/src/ripple/app/tx/impl/SetTrust.h index df5c6552c..259ed0774 100644 --- a/src/ripple/app/tx/impl/SetTrust.h +++ b/src/ripple/app/tx/impl/SetTrust.h @@ -31,6 +31,8 @@ namespace ripple { class SetTrust : public Transactor { public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + explicit SetTrust(ApplyContext& ctx) : Transactor(ctx) { } diff --git a/src/ripple/app/tx/impl/Transactor.cpp b/src/ripple/app/tx/impl/Transactor.cpp index 0a5c7ff7f..4810e4a78 100644 --- a/src/ripple/app/tx/impl/Transactor.cpp +++ b/src/ripple/app/tx/impl/Transactor.cpp @@ -55,6 +55,14 @@ preflight0(PreflightContext const& ctx) NotTEC preflight1(PreflightContext const& ctx) { + // This is inappropriate in preflight0, because only Change transactions + // skip this function, and those do not allow an sfTicketSequence field. + if (ctx.tx.getSeqProxy().isTicket() && + !ctx.rules.enabled(featureTicketBatch)) + { + return temMALFORMED; + } + auto const ret = preflight0(ctx); if (!isTesSuccess(ret)) return ret; @@ -82,6 +90,16 @@ preflight1(PreflightContext const& ctx) return temBAD_SIGNATURE; } + // An AccountTxnID field constrains transaction ordering more than the + // Sequence field. Tickets, on the other hand, reduce ordering + // constraints. Because Tickets and AccountTxnID work against one + // another the combination is unsupported and treated as malformed. + // + // We return temINVALID for such transactions. + if (ctx.tx.getSeqProxy().isTicket() && + ctx.tx.isFieldPresent(sfAccountTxnID)) + return temINVALID; + return tesSUCCESS; } @@ -113,7 +131,8 @@ PreflightContext::PreflightContext( //------------------------------------------------------------------------------ -Transactor::Transactor(ApplyContext& ctx) : ctx_(ctx), j_(ctx.journal) +Transactor::Transactor(ApplyContext& ctx) + : ctx_(ctx), j_(ctx.journal), account_(ctx.tx.getAccountID(sfAccount)) { } @@ -135,12 +154,6 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx) return baseFee + (signerCount * baseFee); } -XRPAmount -Transactor::calculateFeePaid(STTx const& tx) -{ - return tx[sfFee].xrp(); -} - XRPAmount Transactor::minimumFee( Application& app, @@ -151,16 +164,13 @@ Transactor::minimumFee( return scaleFeeLoad(baseFee, app.getFeeTrack(), fees, flags & tapUNLIMITED); } -XRPAmount -Transactor::calculateMaxSpend(STTx const& tx) -{ - return beast::zero; -} - TER Transactor::checkFee(PreclaimContext const& ctx, FeeUnit64 baseFee) { - auto const feePaid = calculateFeePaid(ctx.tx); + if (!ctx.tx[sfFee].native()) + return temBAD_FEE; + + auto const feePaid = ctx.tx[sfFee].xrp(); if (!isLegalAmount(feePaid) || feePaid < beast::zero) return temBAD_FEE; @@ -206,7 +216,7 @@ Transactor::checkFee(PreclaimContext const& ctx, FeeUnit64 baseFee) TER Transactor::payFee() { - auto const feePaid = calculateFeePaid(ctx_.tx); + auto const feePaid = ctx_.tx[sfFee].xrp(); auto const sle = view().peek(keylet::account(account_)); if (!sle) @@ -224,7 +234,68 @@ Transactor::payFee() } NotTEC -Transactor::checkSeq(PreclaimContext const& ctx) +Transactor::checkSeqProxy( + ReadView const& view, + STTx const& tx, + beast::Journal j) +{ + auto const id = tx.getAccountID(sfAccount); + + auto const sle = view.read(keylet::account(id)); + + if (!sle) + { + JLOG(j.trace()) + << "applyTransaction: delay: source account does not exist " + << toBase58(tx.getAccountID(sfAccount)); + return terNO_ACCOUNT; + } + + SeqProxy const t_seqProx = tx.getSeqProxy(); + SeqProxy const a_seq = SeqProxy::sequence((*sle)[sfSequence]); + + if (t_seqProx.isSeq() && t_seqProx != a_seq) + { + if (a_seq < t_seqProx) + { + JLOG(j.trace()) << "applyTransaction: has future sequence number " + << "a_seq=" << a_seq << " t_seq=" << t_seqProx; + return terPRE_SEQ; + } + // It's an already-used sequence number. + JLOG(j.trace()) << "applyTransaction: has past sequence number " + << "a_seq=" << a_seq << " t_seq=" << t_seqProx; + return tefPAST_SEQ; + } + else if (t_seqProx.isTicket()) + { + // Bypass the type comparison. Apples and oranges. + if (a_seq.value() <= t_seqProx.value()) + { + // If the Ticket number is greater than or equal to the + // account sequence there's the possibility that the + // transaction to create the Ticket has not hit the ledger + // yet. Allow a retry. + JLOG(j.trace()) << "applyTransaction: has future ticket id " + << "a_seq=" << a_seq << " t_seq=" << t_seqProx; + return terPRE_TICKET; + } + + // Transaction can never succeed if the Ticket is not in the ledger. + if (!view.exists(keylet::ticket(id, t_seqProx))) + { + JLOG(j.trace()) + << "applyTransaction: ticket already used or never created " + << "a_seq=" << a_seq << " t_seq=" << t_seqProx; + return tefNO_TICKET; + } + } + + return tesSUCCESS; +} + +NotTEC +Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx) { auto const id = ctx.tx.getAccountID(sfAccount); @@ -238,27 +309,6 @@ Transactor::checkSeq(PreclaimContext const& ctx) return terNO_ACCOUNT; } - std::uint32_t const t_seq = ctx.tx.getSequence(); - std::uint32_t const a_seq = sle->getFieldU32(sfSequence); - - if (t_seq != a_seq) - { - if (a_seq < t_seq) - { - JLOG(ctx.j.trace()) - << "applyTransaction: has future sequence number " - << "a_seq=" << a_seq << " t_seq=" << t_seq; - return terPRE_SEQ; - } - - if (ctx.view.txExists(ctx.tx.getTransactionID())) - return tefALREADY; - - JLOG(ctx.j.trace()) << "applyTransaction: has past sequence number " - << "a_seq=" << a_seq << " t_seq=" << t_seq; - return tefPAST_SEQ; - } - if (ctx.tx.isFieldPresent(sfAccountTxnID) && (sle->getFieldH256(sfAccountTxnID) != ctx.tx.getFieldH256(sfAccountTxnID))) @@ -268,29 +318,87 @@ Transactor::checkSeq(PreclaimContext const& ctx) (ctx.view.seq() > ctx.tx.getFieldU32(sfLastLedgerSequence))) return tefMAX_LEDGER; + if (ctx.view.txExists(ctx.tx.getTransactionID())) + return tefALREADY; + return tesSUCCESS; } -void -Transactor::setSeq() +TER +Transactor::consumeSeqProxy(SLE::pointer const& sleAccount) { - auto const sle = view().peek(keylet::account(account_)); - if (!sle) - return; + assert(sleAccount); + SeqProxy const seqProx = ctx_.tx.getSeqProxy(); + if (seqProx.isSeq()) + { + // Note that if this transaction is a TicketCreate, then + // the transaction will modify the account root sfSequence + // yet again. + sleAccount->setFieldU32(sfSequence, seqProx.value() + 1); + return tesSUCCESS; + } + return ticketDelete( + view(), account_, getTicketIndex(account_, seqProx), j_); +} - std::uint32_t const t_seq = ctx_.tx.getSequence(); +// Remove a single Ticket from the ledger. +TER +Transactor::ticketDelete( + ApplyView& view, + AccountID const& account, + uint256 const& ticketIndex, + beast::Journal j) +{ + // Delete the Ticket, adjust the account root ticket count, and + // reduce the owner count. + SLE::pointer const sleTicket = view.peek(keylet::ticket(ticketIndex)); + if (!sleTicket) + { + JLOG(j.fatal()) << "Ticket disappeared from ledger."; + return tefBAD_LEDGER; + } - sle->setFieldU32(sfSequence, t_seq + 1); + std::uint64_t const page{(*sleTicket)[sfOwnerNode]}; + if (!view.dirRemove(keylet::ownerDir(account), page, ticketIndex, true)) + { + JLOG(j.fatal()) << "Unable to delete Ticket from owner."; + return tefBAD_LEDGER; + } - if (sle->isFieldPresent(sfAccountTxnID)) - sle->setFieldH256(sfAccountTxnID, ctx_.tx.getTransactionID()); + // Update the account root's TicketCount. If the ticket count drops to + // zero remove the (optional) field. + auto sleAccount = view.peek(keylet::account(account)); + if (!sleAccount) + { + JLOG(j.fatal()) << "Could not find Ticket owner account root."; + return tefBAD_LEDGER; + } + + if (auto ticketCount = (*sleAccount)[~sfTicketCount]) + { + if (*ticketCount == 1) + sleAccount->makeFieldAbsent(sfTicketCount); + else + ticketCount = *ticketCount - 1; + } + else + { + JLOG(j.fatal()) << "TicketCount field missing from account root."; + return tefBAD_LEDGER; + } + + // Update the Ticket owner's reserve. + adjustOwnerCount(view, sleAccount, -1, j); + + // Remove Ticket from ledger. + view.erase(sleTicket); + return tesSUCCESS; } // check stuff before you bother to lock the ledger void Transactor::preCompute() { - account_ = ctx_.tx.getAccountID(sfAccount); assert(account_ != beast::zero); } @@ -309,16 +417,20 @@ Transactor::apply() if (sle) { - mPriorBalance = STAmount((*sle)[sfBalance]).xrp(); + mPriorBalance = STAmount{(*sle)[sfBalance]}.xrp(); mSourceBalance = mPriorBalance; - setSeq(); - - auto result = payFee(); - + TER result = consumeSeqProxy(sle); if (result != tesSUCCESS) return result; + result = payFee(); + if (result != tesSUCCESS) + return result; + + if (sle->isFieldPresent(sfAccountTxnID)) + sle->setFieldH256(sfAccountTxnID, ctx_.tx.getTransactionID()); + view().update(sle); } @@ -590,7 +702,7 @@ removeUnfundedOffers( } /** Reset the context, discarding any changes made and adjust the fee */ -XRPAmount +std::pair Transactor::reset(XRPAmount fee) { ctx_.discard(); @@ -600,7 +712,7 @@ Transactor::reset(XRPAmount fee) 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 beast::zero; + return {tefINTERNAL, beast::zero}; auto const balance = txnAcct->getFieldAmount(sfBalance).xrp(); @@ -613,13 +725,19 @@ Transactor::reset(XRPAmount fee) fee = balance; // Since we reset the context, we need to charge the fee and update - // the account's sequence number again. + // the account's sequence number (or consume the Ticket) again. + // + // If for some reason we are unable to consume the ticket or sequence + // then the ledger is corrupted. Rather than make things worse we + // reject the transaction. txnAcct->setFieldAmount(sfBalance, balance - fee); - txnAcct->setFieldU32(sfSequence, ctx_.tx.getSequence() + 1); + TER const ter{consumeSeqProxy(txnAcct)}; + assert(isTesSuccess(ter)); - view().update(txnAcct); + if (isTesSuccess(ter)) + view().update(txnAcct); - return fee; + return {ter, fee}; } //------------------------------------------------------------------------------ @@ -699,15 +817,21 @@ Transactor::operator()() }); } - // Reset the context, potentially adjusting the fee - fee = reset(fee); + // Reset the context, potentially adjusting the fee. + { + auto const resetResult = reset(fee); + if (!isTesSuccess(resetResult.first)) + result = resetResult.first; + + fee = resetResult.second; + } // If necessary, remove any offers found unfunded during processing if ((result == tecOVERSIZE) || (result == tecKILLED)) removeUnfundedOffers( view(), removedOffers, ctx_.app.journal("View")); - applied = true; + applied = isTecClaim(result); } if (applied) @@ -720,11 +844,16 @@ Transactor::operator()() { // if invariants checking failed again, reset the context and // attempt to only claim a fee. - fee = reset(fee); + auto const resetResult = reset(fee); + if (!isTesSuccess(resetResult.first)) + result = resetResult.first; + + fee = resetResult.second; // Check invariants again to ensure the fee claiming doesn't // violate invariants. - result = ctx_.checkInvariants(result, fee); + if (isTesSuccess(result) || isTecClaim(result)) + result = ctx_.checkInvariants(result, fee); } // We ran through the invariant checker, which can, in some cases, diff --git a/src/ripple/app/tx/impl/Transactor.h b/src/ripple/app/tx/impl/Transactor.h index b32dcf789..c66e25551 100644 --- a/src/ripple/app/tx/impl/Transactor.h +++ b/src/ripple/app/tx/impl/Transactor.h @@ -80,7 +80,7 @@ public: operator=(PreclaimContext const&) = delete; }; -struct TxConsequences; +class TxConsequences; struct PreflightResult; class Transactor @@ -89,7 +89,7 @@ protected: ApplyContext& ctx_; beast::Journal const j_; - AccountID account_; + AccountID const account_; XRPAmount mPriorBalance; // Balance before fees. XRPAmount mSourceBalance; // Balance after fees. @@ -99,6 +99,7 @@ protected: operator=(Transactor const&) = delete; public: + enum ConsequencesFactoryType { Normal, Blocker, Custom }; /** Process the transaction. */ std::pair operator()(); @@ -126,7 +127,10 @@ public: */ static NotTEC - checkSeq(PreclaimContext const& ctx); + checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j); + + static NotTEC + checkPriorTxAndLastLedger(PreclaimContext const& ctx); static TER checkFee(PreclaimContext const& ctx, FeeUnit64 baseFee); @@ -138,18 +142,6 @@ public: static FeeUnit64 calculateBaseFee(ReadView const& view, STTx const& tx); - static bool - affectsSubsequentTransactionAuth(STTx const& tx) - { - return false; - } - - static XRPAmount - calculateFeePaid(STTx const& tx); - - static XRPAmount - calculateMaxSpend(STTx const& tx); - static TER preclaim(PreclaimContext const& ctx) { @@ -159,6 +151,14 @@ public: } ///////////////////////////////////////////////////// + // Interface used by DeleteAccount + static TER + ticketDelete( + ApplyView& view, + AccountID const& account, + uint256 const& ticketIndex, + beast::Journal j); + protected: TER apply(); @@ -188,11 +188,11 @@ protected: ApplyFlags flags); private: - XRPAmount + std::pair reset(XRPAmount fee); - void - setSeq(); + TER + consumeSeqProxy(SLE::pointer const& sleAccount); TER payFee(); static NotTEC diff --git a/src/ripple/app/tx/impl/applySteps.cpp b/src/ripple/app/tx/impl/applySteps.cpp index 8ebfd6d3c..1e6e35d9b 100644 --- a/src/ripple/app/tx/impl/applySteps.cpp +++ b/src/ripple/app/tx/impl/applySteps.cpp @@ -21,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -39,68 +38,120 @@ namespace ripple { -static NotTEC +// Templates so preflight does the right thing with T::ConsequencesFactory. +// +// This could be done more easily using if constexpr, but Visual Studio +// 2017 doesn't handle if constexpr correctly. So once we're no longer +// building with Visual Studio 2017 we can consider replacing the four +// templates with a single template function that uses if constexpr. +// +// For Transactor::Normal +template < + class T, + std::enable_if_t = 0> +TxConsequences +consequences_helper(PreflightContext const& ctx) +{ + return TxConsequences(ctx.tx); +}; + +// For Transactor::Blocker +template < + class T, + std::enable_if_t = 0> +TxConsequences +consequences_helper(PreflightContext const& ctx) +{ + return TxConsequences(ctx.tx, TxConsequences::blocker); +}; + +// For Transactor::Custom +template < + class T, + std::enable_if_t = 0> +TxConsequences +consequences_helper(PreflightContext const& ctx) +{ + return T::makeTxConsequences(ctx); +}; + +template +std::pair +invoke_preflight_helper(PreflightContext const& ctx) +{ + auto const tec = T::preflight(ctx); + return { + tec, + isTesSuccess(tec) ? consequences_helper(ctx) : TxConsequences{tec}}; +} + +static std::pair invoke_preflight(PreflightContext const& ctx) { switch (ctx.tx.getTxnType()) { - case ttACCOUNT_SET: - return SetAccount ::preflight(ctx); - case ttCHECK_CANCEL: - return CancelCheck ::preflight(ctx); - case ttCHECK_CASH: - return CashCheck ::preflight(ctx); - case ttCHECK_CREATE: - return CreateCheck ::preflight(ctx); - case ttDEPOSIT_PREAUTH: - return DepositPreauth ::preflight(ctx); - case ttOFFER_CANCEL: - return CancelOffer ::preflight(ctx); - case ttOFFER_CREATE: - return CreateOffer ::preflight(ctx); - case ttESCROW_CREATE: - return EscrowCreate ::preflight(ctx); - case ttESCROW_FINISH: - return EscrowFinish ::preflight(ctx); - case ttESCROW_CANCEL: - return EscrowCancel ::preflight(ctx); - case ttPAYCHAN_CLAIM: - return PayChanClaim ::preflight(ctx); - case ttPAYCHAN_CREATE: - return PayChanCreate ::preflight(ctx); - case ttPAYCHAN_FUND: - return PayChanFund ::preflight(ctx); - case ttPAYMENT: - return Payment ::preflight(ctx); - case ttREGULAR_KEY_SET: - return SetRegularKey ::preflight(ctx); - case ttSIGNER_LIST_SET: - return SetSignerList ::preflight(ctx); - case ttTICKET_CANCEL: - return CancelTicket ::preflight(ctx); - case ttTICKET_CREATE: - return CreateTicket ::preflight(ctx); - case ttTRUST_SET: - return SetTrust ::preflight(ctx); case ttACCOUNT_DELETE: - return DeleteAccount ::preflight(ctx); + return invoke_preflight_helper(ctx); + case ttACCOUNT_SET: + return invoke_preflight_helper(ctx); + case ttCHECK_CANCEL: + return invoke_preflight_helper(ctx); + case ttCHECK_CASH: + return invoke_preflight_helper(ctx); + case ttCHECK_CREATE: + return invoke_preflight_helper(ctx); + case ttDEPOSIT_PREAUTH: + return invoke_preflight_helper(ctx); + case ttOFFER_CANCEL: + return invoke_preflight_helper(ctx); + case ttOFFER_CREATE: + return invoke_preflight_helper(ctx); + case ttESCROW_CREATE: + return invoke_preflight_helper(ctx); + case ttESCROW_FINISH: + return invoke_preflight_helper(ctx); + case ttESCROW_CANCEL: + return invoke_preflight_helper(ctx); + case ttPAYCHAN_CLAIM: + return invoke_preflight_helper(ctx); + case ttPAYCHAN_CREATE: + return invoke_preflight_helper(ctx); + case ttPAYCHAN_FUND: + return invoke_preflight_helper(ctx); + case ttPAYMENT: + return invoke_preflight_helper(ctx); + case ttREGULAR_KEY_SET: + return invoke_preflight_helper(ctx); + case ttSIGNER_LIST_SET: + return invoke_preflight_helper(ctx); + case ttTICKET_CREATE: + return invoke_preflight_helper(ctx); + case ttTRUST_SET: + return invoke_preflight_helper(ctx); case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: - return Change ::preflight(ctx); + return invoke_preflight_helper(ctx); default: assert(false); - return temUNKNOWN; + return {temUNKNOWN, TxConsequences{temUNKNOWN}}; } } +// The TxQ needs to be able to bypass checking the Sequence or Ticket +// The enum provides a self-documenting way to do that +enum class SeqCheck : bool { + no = false, + yes = true, +}; + /* invoke_preclaim uses name hiding to accomplish compile-time polymorphism of (presumably) static class functions for Transactor and derived classes. */ template static TER -invoke_preclaim(PreclaimContext const& ctx) +invoke_preclaim(PreclaimContext const& ctx, SeqCheck seqChk) { // If the transactor requires a valid account and the transaction doesn't // list one, preflight will have already a flagged a failure. @@ -108,7 +159,16 @@ invoke_preclaim(PreclaimContext const& ctx) if (id != beast::zero) { - TER result = T::checkSeq(ctx); + TER result{tesSUCCESS}; + if (seqChk == SeqCheck::yes) + { + result = T::checkSeqProxy(ctx.view, ctx.tx, ctx.j); + + if (result != tesSUCCESS) + return result; + } + + result = T::checkPriorTxAndLastLedger(ctx); if (result != tesSUCCESS) return result; @@ -128,54 +188,112 @@ invoke_preclaim(PreclaimContext const& ctx) } static TER -invoke_preclaim(PreclaimContext const& ctx) +invoke_preclaim(PreclaimContext const& ctx, SeqCheck seqChk) { switch (ctx.tx.getTxnType()) { - case ttACCOUNT_SET: - return invoke_preclaim(ctx); - case ttCHECK_CANCEL: - return invoke_preclaim(ctx); - case ttCHECK_CASH: - return invoke_preclaim(ctx); - case ttCHECK_CREATE: - return invoke_preclaim(ctx); - case ttDEPOSIT_PREAUTH: - return invoke_preclaim(ctx); - case ttOFFER_CANCEL: - return invoke_preclaim(ctx); - case ttOFFER_CREATE: - return invoke_preclaim(ctx); - case ttESCROW_CREATE: - return invoke_preclaim(ctx); - case ttESCROW_FINISH: - return invoke_preclaim(ctx); - case ttESCROW_CANCEL: - return invoke_preclaim(ctx); - case ttPAYCHAN_CLAIM: - return invoke_preclaim(ctx); - case ttPAYCHAN_CREATE: - return invoke_preclaim(ctx); - case ttPAYCHAN_FUND: - return invoke_preclaim(ctx); - case ttPAYMENT: - return invoke_preclaim(ctx); - case ttREGULAR_KEY_SET: - return invoke_preclaim(ctx); - case ttSIGNER_LIST_SET: - return invoke_preclaim(ctx); - case ttTICKET_CANCEL: - return invoke_preclaim(ctx); - case ttTICKET_CREATE: - return invoke_preclaim(ctx); - case ttTRUST_SET: - return invoke_preclaim(ctx); case ttACCOUNT_DELETE: - return invoke_preclaim(ctx); + return invoke_preclaim(ctx, seqChk); + case ttACCOUNT_SET: + return invoke_preclaim(ctx, seqChk); + case ttCHECK_CANCEL: + return invoke_preclaim(ctx, seqChk); + case ttCHECK_CASH: + return invoke_preclaim(ctx, seqChk); + case ttCHECK_CREATE: + return invoke_preclaim(ctx, seqChk); + case ttDEPOSIT_PREAUTH: + return invoke_preclaim(ctx, seqChk); + case ttOFFER_CANCEL: + return invoke_preclaim(ctx, seqChk); + case ttOFFER_CREATE: + return invoke_preclaim(ctx, seqChk); + case ttESCROW_CREATE: + return invoke_preclaim(ctx, seqChk); + case ttESCROW_FINISH: + return invoke_preclaim(ctx, seqChk); + case ttESCROW_CANCEL: + return invoke_preclaim(ctx, seqChk); + case ttPAYCHAN_CLAIM: + return invoke_preclaim(ctx, seqChk); + case ttPAYCHAN_CREATE: + return invoke_preclaim(ctx, seqChk); + case ttPAYCHAN_FUND: + return invoke_preclaim(ctx, seqChk); + case ttPAYMENT: + return invoke_preclaim(ctx, seqChk); + case ttREGULAR_KEY_SET: + return invoke_preclaim(ctx, seqChk); + case ttSIGNER_LIST_SET: + return invoke_preclaim(ctx, seqChk); + case ttTICKET_CREATE: + return invoke_preclaim(ctx, seqChk); + case ttTRUST_SET: + return invoke_preclaim(ctx, seqChk); case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: - return invoke_preclaim(ctx); + return invoke_preclaim(ctx, seqChk); + default: + assert(false); + return temUNKNOWN; + } +} + +template +static TER +invoke_seqCheck(ReadView const& view, STTx const& tx, beast::Journal j) +{ + return T::checkSeqProxy(view, tx, j); +} + +TER +ForTxQ::seqCheck(OpenView& view, STTx const& tx, beast::Journal j) +{ + switch (tx.getTxnType()) + { + case ttACCOUNT_DELETE: + return invoke_seqCheck(view, tx, j); + case ttACCOUNT_SET: + return invoke_seqCheck(view, tx, j); + case ttCHECK_CANCEL: + return invoke_seqCheck(view, tx, j); + case ttCHECK_CASH: + return invoke_seqCheck(view, tx, j); + case ttCHECK_CREATE: + return invoke_seqCheck(view, tx, j); + case ttDEPOSIT_PREAUTH: + return invoke_seqCheck(view, tx, j); + case ttOFFER_CANCEL: + return invoke_seqCheck(view, tx, j); + case ttOFFER_CREATE: + return invoke_seqCheck(view, tx, j); + case ttESCROW_CREATE: + return invoke_seqCheck(view, tx, j); + case ttESCROW_FINISH: + return invoke_seqCheck(view, tx, j); + case ttESCROW_CANCEL: + return invoke_seqCheck(view, tx, j); + case ttPAYCHAN_CLAIM: + return invoke_seqCheck(view, tx, j); + case ttPAYCHAN_CREATE: + return invoke_seqCheck(view, tx, j); + case ttPAYCHAN_FUND: + return invoke_seqCheck(view, tx, j); + case ttPAYMENT: + return invoke_seqCheck(view, tx, j); + case ttREGULAR_KEY_SET: + return invoke_seqCheck(view, tx, j); + case ttSIGNER_LIST_SET: + return invoke_seqCheck(view, tx, j); + case ttTICKET_CREATE: + return invoke_seqCheck(view, tx, j); + case ttTRUST_SET: + return invoke_seqCheck(view, tx, j); + case ttAMENDMENT: + case ttFEE: + case ttUNL_MODIFY: + return invoke_seqCheck(view, tx, j); default: assert(false); return temUNKNOWN; @@ -187,6 +305,8 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) { switch (tx.getTxnType()) { + case ttACCOUNT_DELETE: + return DeleteAccount::calculateBaseFee(view, tx); case ttACCOUNT_SET: return SetAccount::calculateBaseFee(view, tx); case ttCHECK_CANCEL: @@ -219,14 +339,10 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) return SetRegularKey::calculateBaseFee(view, tx); case ttSIGNER_LIST_SET: return SetSignerList::calculateBaseFee(view, tx); - case ttTICKET_CANCEL: - return CancelTicket::calculateBaseFee(view, tx); case ttTICKET_CREATE: return CreateTicket::calculateBaseFee(view, tx); case ttTRUST_SET: return SetTrust::calculateBaseFee(view, tx); - case ttACCOUNT_DELETE: - return DeleteAccount::calculateBaseFee(view, tx); case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: @@ -237,75 +353,43 @@ invoke_calculateBaseFee(ReadView const& view, STTx const& tx) } } -template -static TxConsequences -invoke_calculateConsequences(STTx const& tx) +TxConsequences::TxConsequences(NotTEC pfresult) + : isBlocker_(false) + , fee_(beast::zero) + , potentialSpend_(beast::zero) + , seqProx_(SeqProxy::sequence(0)) + , sequencesConsumed_(0) { - auto const category = T::affectsSubsequentTransactionAuth(tx) - ? TxConsequences::blocker - : TxConsequences::normal; - auto const feePaid = T::calculateFeePaid(tx); - auto const maxSpend = T::calculateMaxSpend(tx); - - return {category, feePaid, maxSpend}; + assert(!isTesSuccess(pfresult)); } -static TxConsequences -invoke_calculateConsequences(STTx const& tx) +TxConsequences::TxConsequences(STTx const& tx) + : isBlocker_(false) + , fee_( + tx[sfFee].native() && !tx[sfFee].negative() ? tx[sfFee].xrp() + : beast::zero) + , potentialSpend_(beast::zero) + , seqProx_(tx.getSeqProxy()) + , sequencesConsumed_(tx.getSeqProxy().isSeq() ? 1 : 0) { - switch (tx.getTxnType()) - { - case ttACCOUNT_SET: - return invoke_calculateConsequences(tx); - case ttCHECK_CANCEL: - return invoke_calculateConsequences(tx); - case ttCHECK_CASH: - return invoke_calculateConsequences(tx); - case ttCHECK_CREATE: - return invoke_calculateConsequences(tx); - case ttDEPOSIT_PREAUTH: - return invoke_calculateConsequences(tx); - case ttOFFER_CANCEL: - return invoke_calculateConsequences(tx); - case ttOFFER_CREATE: - return invoke_calculateConsequences(tx); - case ttESCROW_CREATE: - return invoke_calculateConsequences(tx); - case ttESCROW_FINISH: - return invoke_calculateConsequences(tx); - case ttESCROW_CANCEL: - return invoke_calculateConsequences(tx); - case ttPAYCHAN_CLAIM: - return invoke_calculateConsequences(tx); - case ttPAYCHAN_CREATE: - return invoke_calculateConsequences(tx); - case ttPAYCHAN_FUND: - return invoke_calculateConsequences(tx); - case ttPAYMENT: - return invoke_calculateConsequences(tx); - case ttREGULAR_KEY_SET: - return invoke_calculateConsequences(tx); - case ttSIGNER_LIST_SET: - return invoke_calculateConsequences(tx); - case ttTICKET_CANCEL: - return invoke_calculateConsequences(tx); - case ttTICKET_CREATE: - return invoke_calculateConsequences(tx); - case ttTRUST_SET: - return invoke_calculateConsequences(tx); - case ttACCOUNT_DELETE: - return invoke_calculateConsequences(tx); - case ttAMENDMENT: - case ttFEE: - case ttUNL_MODIFY: - [[fallthrough]]; - default: - assert(false); - return { - TxConsequences::blocker, - Transactor::calculateFeePaid(tx), - beast::zero}; - } +} + +TxConsequences::TxConsequences(STTx const& tx, Category category) + : TxConsequences(tx) +{ + isBlocker_ = (category == blocker); +} + +TxConsequences::TxConsequences(STTx const& tx, XRPAmount potentialSpend) + : TxConsequences(tx) +{ + potentialSpend_ = potentialSpend; +} + +TxConsequences::TxConsequences(STTx const& tx, std::uint32_t sequencesConsumed) + : TxConsequences(tx) +{ + sequencesConsumed_ = sequencesConsumed; } static std::pair @@ -313,6 +397,10 @@ invoke_apply(ApplyContext& ctx) { switch (ctx.tx.getTxnType()) { + case ttACCOUNT_DELETE: { + DeleteAccount p(ctx); + return p(); + } case ttACCOUNT_SET: { SetAccount p(ctx); return p(); @@ -377,10 +465,6 @@ invoke_apply(ApplyContext& ctx) SetSignerList p(ctx); return p(); } - case ttTICKET_CANCEL: { - CancelTicket p(ctx); - return p(); - } case ttTICKET_CREATE: { CreateTicket p(ctx); return p(); @@ -389,10 +473,6 @@ invoke_apply(ApplyContext& ctx) SetTrust p(ctx); return p(); } - case ttACCOUNT_DELETE: { - DeleteAccount p(ctx); - return p(); - } case ttAMENDMENT: case ttFEE: case ttUNL_MODIFY: { @@ -421,15 +501,15 @@ preflight( catch (std::exception const& e) { JLOG(j.fatal()) << "apply: " << e.what(); - return {pfctx, tefEXCEPTION}; + return {pfctx, {tefEXCEPTION, TxConsequences{tx}}}; } } -PreclaimResult -preclaim( +PreclaimResult static preclaim( PreflightResult const& preflightResult, Application& app, - OpenView const& view) + OpenView const& view, + SeqCheck seqChk) { boost::optional ctx; if (preflightResult.rules != view.rules()) @@ -462,7 +542,7 @@ preclaim( { if (ctx->preflightResult != tesSUCCESS) return {*ctx, ctx->preflightResult}; - return {*ctx, invoke_preclaim(*ctx)}; + return {*ctx, invoke_preclaim(*ctx, seqChk)}; } catch (std::exception const& e) { @@ -471,22 +551,34 @@ preclaim( } } +PreclaimResult +preclaim( + PreflightResult const& preflightResult, + Application& app, + OpenView const& view) +{ + return preclaim(preflightResult, app, view, SeqCheck::yes); +} + +PreclaimResult +ForTxQ::preclaimWithoutSeqCheck( + PreflightResult const& preflightResult, + Application& app, + OpenView const& view) +{ + return preclaim(preflightResult, app, view, SeqCheck::no); +} + FeeUnit64 calculateBaseFee(ReadView const& view, STTx const& tx) { return invoke_calculateBaseFee(view, tx); } -TxConsequences -calculateConsequences(PreflightResult const& preflightResult) +XRPAmount +calculateDefaultBaseFee(ReadView const& view, STTx const& tx) { - assert(preflightResult.ter == tesSUCCESS); - if (preflightResult.ter != tesSUCCESS) - return { - TxConsequences::blocker, - Transactor::calculateFeePaid(preflightResult.tx), - beast::zero}; - return invoke_calculateConsequences(preflightResult.tx); + return view.fees().toDrops(Transactor::calculateBaseFee(view, tx)); } std::pair diff --git a/src/ripple/ledger/ReadView.h b/src/ripple/ledger/ReadView.h index 1e8052cc3..a2b6f6f99 100644 --- a/src/ripple/ledger/ReadView.h +++ b/src/ripple/ledger/ReadView.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -67,10 +68,13 @@ struct Fees return reserve + ownerCount * increment; } - std::pair + XRPAmount toDrops(FeeUnit64 const& fee) const { - return mulDiv(base, fee, units); + if (auto const resultPair = mulDiv(base, fee, units); resultPair.first) + return resultPair.second; + + return XRPAmount(STAmount::cMaxNativeN); } }; diff --git a/src/ripple/proto/org/xrpl/rpc/v1/common.proto b/src/ripple/proto/org/xrpl/rpc/v1/common.proto index 2920f42c7..ebfa208de 100644 --- a/src/ripple/proto/org/xrpl/rpc/v1/common.proto +++ b/src/ripple/proto/org/xrpl/rpc/v1/common.proto @@ -174,6 +174,21 @@ message TickSize uint32 value = 1; } +message Ticket +{ + uint32 value = 1; +} + +message TicketCount +{ + uint32 value = 1; +} + +message TicketSequence +{ + uint32 value = 1; +} + message TransferRate { uint32 value = 1; diff --git a/src/ripple/proto/org/xrpl/rpc/v1/get_account_info.proto b/src/ripple/proto/org/xrpl/rpc/v1/get_account_info.proto index 9a2a877cd..fe571b4f3 100644 --- a/src/ripple/proto/org/xrpl/rpc/v1/get_account_info.proto +++ b/src/ripple/proto/org/xrpl/rpc/v1/get_account_info.proto @@ -47,7 +47,7 @@ message GetAccountInfoResponse } // Aggregate data about queued transactions -// Next field: 7 +// Next field: 11 message QueueData { uint32 txn_count = 1; @@ -61,10 +61,18 @@ message QueueData XRPDropsAmount max_spend_drops_total = 5; repeated QueuedTransaction transactions = 6; + + uint32 lowest_ticket = 7; + + uint32 highest_ticket = 8; + + uint32 sequence_count = 9; + + uint32 ticket_count = 10; } // Data about a single queued transaction -// Next field: 7 +// Next field: 8 message QueuedTransaction { bool auth_change = 1; @@ -78,4 +86,6 @@ message QueuedTransaction Sequence sequence = 5; LastLedgerSequence last_ledger_sequence = 6; + + Ticket ticket = 7; } diff --git a/src/ripple/proto/org/xrpl/rpc/v1/ledger_objects.proto b/src/ripple/proto/org/xrpl/rpc/v1/ledger_objects.proto index b24635fa3..fe07228a0 100644 --- a/src/ripple/proto/org/xrpl/rpc/v1/ledger_objects.proto +++ b/src/ripple/proto/org/xrpl/rpc/v1/ledger_objects.proto @@ -6,7 +6,7 @@ option java_multiple_files = true; import "org/xrpl/rpc/v1/common.proto"; -// Next field: 14 +// Next field: 15 message LedgerObject { oneof object @@ -24,10 +24,11 @@ message LedgerObject RippleState ripple_state = 11; SignerList signer_list = 12; NegativeUNL negative_unl = 13; + TicketObject ticket = 14; } } -// Next field: 14 +// Next field: 15 enum LedgerEntryType { LEDGER_ENTRY_TYPE_UNSPECIFIED = 0; @@ -44,9 +45,10 @@ enum LedgerEntryType LEDGER_ENTRY_TYPE_RIPPLE_STATE = 11; LEDGER_ENTRY_TYPE_SIGNER_LIST = 12; LEDGER_ENTRY_TYPE_NEGATIVE_UNL = 13; + LEDGER_ENTRY_TYPE_TICKET = 14; } -// Next field: 15 +// Next field: 16 message AccountRoot { Account account = 1; @@ -76,6 +78,8 @@ message AccountRoot TickSize tick_size = 13; TransferRate transfer_rate = 14; + + TicketCount ticket_count = 15; } // Next field: 4 @@ -332,6 +336,22 @@ message SignerList SignerQuorum signer_quorum = 7; } +// Next field: 7 +message TicketObject +{ + Flags flags = 1; + + Account account = 2; + + OwnerNode owner_node = 3; + + PreviousTransactionID previous_transaction_id = 4; + + PreviousTransactionLedgerSequence previous_transaction_ledger_sequence = 5; + + TicketSequence ticket_sequence = 6; +} + // Next field: 5 message NegativeUNL { diff --git a/src/ripple/proto/org/xrpl/rpc/v1/transaction.proto b/src/ripple/proto/org/xrpl/rpc/v1/transaction.proto index 7f896a9a4..081f22e9e 100644 --- a/src/ripple/proto/org/xrpl/rpc/v1/transaction.proto +++ b/src/ripple/proto/org/xrpl/rpc/v1/transaction.proto @@ -9,7 +9,7 @@ import "org/xrpl/rpc/v1/amount.proto"; import "org/xrpl/rpc/v1/account.proto"; // A message encompassing all transaction types -// Next field: 30 +// Next field: 32 message Transaction { Account account = 1; @@ -55,8 +55,9 @@ message Transaction SignerListSet signer_list_set = 28; - TrustSet trust_set = 29; + TicketCreate ticket_create = 30; + TrustSet trust_set = 29; } SigningPublicKey signing_public_key = 5; @@ -74,6 +75,8 @@ message Transaction repeated Signer signers = 11; AccountTransactionID account_transaction_id = 12; + + TicketSequence ticket_sequence = 31; } // Next field: 4 @@ -308,6 +311,12 @@ message SignerListSet repeated SignerEntry signer_entries = 2; } +// Next field: 2 +message TicketCreate +{ + TicketCount count = 1; +} + // Next field: 4 message TrustSet { diff --git a/src/ripple/protocol/Feature.h b/src/ripple/protocol/Feature.h index 0eb79df5b..5f9970423 100644 --- a/src/ripple/protocol/Feature.h +++ b/src/ripple/protocol/Feature.h @@ -70,8 +70,7 @@ namespace detail { class FeatureCollections { static constexpr char const* const featureNames[] = { - "MultiSign", // Unconditionally supported. - "Tickets", + "MultiSign", // Unconditionally supported. "TrustSetAuth", // Unconditionally supported. "FeeEscalation", // Unconditionally supported. "OwnerPaysFee", @@ -113,7 +112,9 @@ class FeatureCollections // payment check "HardenedValidations", "fixAmendmentMajorityCalc", // Fix Amendment majority calculation - "NegativeUNL"}; + "NegativeUNL", + "TicketBatch"}; + std::vector features; boost::container::flat_map featureToIndex; boost::container::flat_map nameToFeature; @@ -343,7 +344,6 @@ foreachFeature(FeatureBitset bs, F&& f) f(bitsetIndexToFeature(i)); } -extern uint256 const featureTickets; extern uint256 const featureOwnerPaysFee; extern uint256 const featureFlow; extern uint256 const featureCompareTakerFlowCross; @@ -370,6 +370,7 @@ extern uint256 const fix1781; extern uint256 const featureHardenedValidations; extern uint256 const fixAmendmentMajorityCalc; extern uint256 const featureNegativeUNL; +extern uint256 const featureTicketBatch; } // namespace ripple diff --git a/src/ripple/protocol/Indexes.h b/src/ripple/protocol/Indexes.h index 6f2bbf992..411f4db24 100644 --- a/src/ripple/protocol/Indexes.h +++ b/src/ripple/protocol/Indexes.h @@ -32,6 +32,7 @@ namespace ripple { +class SeqProxy; /** Keylet computation funclets. Entries in the ledger are located using 256-bit locators. The locators are @@ -152,7 +153,10 @@ struct ticket_t explicit ticket_t() = default; Keylet - operator()(AccountID const& id, std::uint32_t seq) const; + operator()(AccountID const& id, std::uint32_t ticketSeq) const; + + Keylet + operator()(AccountID const& id, SeqProxy ticketSeq) const; Keylet operator()(uint256 const& key) const @@ -238,6 +242,9 @@ getQuality(uint256 const& uBase); uint256 getTicketIndex(AccountID const& account, std::uint32_t uSequence); +uint256 +getTicketIndex(AccountID const& account, SeqProxy ticketSeq); + } // namespace ripple #endif diff --git a/src/ripple/protocol/SField.h b/src/ripple/protocol/SField.h index c4f86056a..97d749914 100644 --- a/src/ripple/protocol/SField.h +++ b/src/ripple/protocol/SField.h @@ -390,6 +390,8 @@ extern SF_U32 const sfCancelAfter; extern SF_U32 const sfFinishAfter; extern SF_U32 const sfSignerListID; extern SF_U32 const sfSettleDelay; +extern SF_U32 const sfTicketCount; +extern SF_U32 const sfTicketSequence; // 64-bit integers extern SF_U64 const sfIndexNext; @@ -429,7 +431,6 @@ extern SF_U256 const sfBookDirectory; extern SF_U256 const sfInvoiceID; extern SF_U256 const sfNickname; extern SF_U256 const sfAmendment; -extern SF_U256 const sfTicketID; extern SF_U256 const sfDigest; extern SF_U256 const sfPayChannel; extern SF_U256 const sfConsensusHash; diff --git a/src/ripple/protocol/STObject.h b/src/ripple/protocol/STObject.h index 490de42ce..9cd407efc 100644 --- a/src/ripple/protocol/STObject.h +++ b/src/ripple/protocol/STObject.h @@ -199,6 +199,13 @@ private: return !(lhs == rhs); } + // Emulate boost::optional::value_or + value_type + value_or(value_type val) const + { + return engaged() ? this->value() : val; + } + OptionalProxy& operator=(boost::none_t const&); OptionalProxy& diff --git a/src/ripple/protocol/STTx.h b/src/ripple/protocol/STTx.h index f4e631d7f..cecbd2c0a 100644 --- a/src/ripple/protocol/STTx.h +++ b/src/ripple/protocol/STTx.h @@ -23,9 +23,9 @@ #include #include #include +#include #include #include -#include #include namespace ripple { @@ -113,16 +113,8 @@ public: return getFieldVL(sfSigningPubKey); } - std::uint32_t - getSequence() const - { - return getFieldU32(sfSequence); - } - void - setSequence(std::uint32_t seq) - { - return setFieldU32(sfSequence, seq); - } + SeqProxy + getSeqProxy() const; boost::container::flat_set getMentionedAccounts() const; diff --git a/src/ripple/protocol/SeqProxy.h b/src/ripple/protocol/SeqProxy.h new file mode 100644 index 000000000..e7a89561c --- /dev/null +++ b/src/ripple/protocol/SeqProxy.h @@ -0,0 +1,170 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2018 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_PROTOCOL_SEQ_PROXY_H_INCLUDED +#define RIPPLE_PROTOCOL_SEQ_PROXY_H_INCLUDED + +#include +#include + +namespace ripple { + +/** A type that represents either a sequence value or a ticket value. + + We use the value() of a SeqProxy in places where a sequence was used + before. An example of this is the sequence of an Offer stored in the + ledger. We do the same thing with the in-ledger identifier of a + Check, Payment Channel, and Escrow. + + Why is this safe? If we use the SeqProxy::value(), how do we know that + each ledger entry will be unique? + + There are two components that make this safe: + + 1. A "TicketCreate" transaction carefully avoids creating a ticket + that corresponds with an already used Sequence or Ticket value. + The transactor does this by referring to the account root's + sequence number. Creating the ticket advances the account root's + sequence number so the same ticket (or sequence) value cannot be + used again. + + 2. When a "TicketCreate" transaction creates a batch of tickets it advances + the account root sequence to one past the largest created ticket. + + Therefore all tickets in a batch other than the first may never have + the same value as a sequence on that same account. And since a ticket + may only be used once there will never be any duplicates within this + account. +*/ +class SeqProxy +{ +public: + enum Type : std::uint8_t { seq = 0, ticket }; + +private: + std::uint32_t value_; + Type type_; + +public: + constexpr explicit SeqProxy(Type t, std::uint32_t v) : value_{v}, type_{t} + { + } + + SeqProxy(SeqProxy const& other) = default; + + SeqProxy& + operator=(SeqProxy const& other) = default; + + /** Factory function to return a sequence-based SeqProxy */ + static constexpr SeqProxy + sequence(std::uint32_t v) + { + return SeqProxy{Type::seq, v}; + } + + constexpr std::uint32_t + value() const + { + return value_; + } + + constexpr bool + isSeq() const + { + return type_ == seq; + } + + constexpr bool + isTicket() const + { + return type_ == ticket; + } + + // Occasionally it is convenient to be able to increase the value_ + // of a SeqProxy. But it's unusual. So, rather than putting in an + // addition operator, you must invoke the method by name. That makes + // if more difficult to invoke accidentally. + SeqProxy& + advanceBy(std::uint32_t amount) + { + value_ += amount; + return *this; + } + + // Comparison + // + // The comparison is designed specifically so _all_ Sequence + // representations sort in front of Ticket representations. This + // is true even if the Ticket value() is less that the Sequence + // value(). + // + // This somewhat surprising sort order has benefits for transaction + // processing. It guarantees that transactions creating Tickets are + // sorted in from of transactions that consume Tickets. + friend constexpr bool + operator==(SeqProxy lhs, SeqProxy rhs) + { + if (lhs.type_ != rhs.type_) + return false; + return (lhs.value() == rhs.value()); + } + + friend constexpr bool + operator!=(SeqProxy lhs, SeqProxy rhs) + { + return !(lhs == rhs); + } + + friend constexpr bool + operator<(SeqProxy lhs, SeqProxy rhs) + { + if (lhs.type_ != rhs.type_) + return lhs.type_ < rhs.type_; + return lhs.value() < rhs.value(); + } + + friend constexpr bool + operator>(SeqProxy lhs, SeqProxy rhs) + { + return rhs < lhs; + } + + friend constexpr bool + operator>=(SeqProxy lhs, SeqProxy rhs) + { + return !(lhs < rhs); + } + + friend constexpr bool + operator<=(SeqProxy lhs, SeqProxy rhs) + { + return !(lhs > rhs); + } + + friend std::ostream& + operator<<(std::ostream& os, SeqProxy seqProx) + { + os << (seqProx.isSeq() ? "sequence " : "ticket "); + os << seqProx.value(); + return os; + } +}; +} // namespace ripple + +#endif diff --git a/src/ripple/protocol/TER.h b/src/ripple/protocol/TER.h index a30bd794a..c43a5ad53 100644 --- a/src/ripple/protocol/TER.h +++ b/src/ripple/protocol/TER.h @@ -60,7 +60,7 @@ enum TELcodes : TERUnderlyingType { telCAN_NOT_QUEUE_BLOCKS, telCAN_NOT_QUEUE_BLOCKED, telCAN_NOT_QUEUE_FEE, - telCAN_NOT_QUEUE_FULL + telCAN_NOT_QUEUE_FULL, }; //------------------------------------------------------------------------------ @@ -113,10 +113,11 @@ enum TEMcodes : TERUnderlyingType { temBAD_TICK_SIZE, temINVALID_ACCOUNT_ID, temCANNOT_PREAUTH_SELF, + temINVALID_COUNT, // An intermediate result used internally, should never be returned. temUNCERTAIN, - temUNKNOWN + temUNKNOWN, }; //------------------------------------------------------------------------------ @@ -158,6 +159,7 @@ enum TEFcodes : TERUnderlyingType { tefBAD_AUTH_MASTER, tefINVARIANT_FAILED, tefTOO_BIG, + tefNO_TICKET, }; //------------------------------------------------------------------------------ @@ -195,7 +197,8 @@ enum TERcodes : TERUnderlyingType { // burden network. terLAST, // DEPRECATED. terNO_RIPPLE, // Rippling not allowed - terQUEUED // Transaction is being held in TxQ until fee drops + terQUEUED, // Transaction is being held in TxQ until fee drops + terPRE_TICKET, // Ticket is not yet in ledger but might be on its way }; //------------------------------------------------------------------------------ diff --git a/src/ripple/protocol/TxFormats.h b/src/ripple/protocol/TxFormats.h index 5ff3cf3ee..a9d4d94f1 100644 --- a/src/ripple/protocol/TxFormats.h +++ b/src/ripple/protocol/TxFormats.h @@ -44,7 +44,7 @@ enum TxType { ttOFFER_CANCEL = 8, no_longer_used = 9, ttTICKET_CREATE = 10, - ttTICKET_CANCEL = 11, + // = 11, // open ttSIGNER_LIST_SET = 12, ttPAYCHAN_CREATE = 13, ttPAYCHAN_FUND = 14, diff --git a/src/ripple/protocol/impl/Feature.cpp b/src/ripple/protocol/impl/Feature.cpp index e18e1260d..7025117aa 100644 --- a/src/ripple/protocol/impl/Feature.cpp +++ b/src/ripple/protocol/impl/Feature.cpp @@ -92,7 +92,6 @@ detail::supportedAmendments() // Removing them will cause servers to become amendment blocked. static std::vector const supported{ "MultiSign", // Unconditionally supported. - // "Tickets", "TrustSetAuth", // Unconditionally supported. "FeeEscalation", // Unconditionally supported. // "OwnerPaysFee", @@ -132,7 +131,8 @@ detail::supportedAmendments() "fix1781", "HardenedValidations", "fixAmendmentMajorityCalc", - //"NegativeUNL" // Commented out to prevent automatic enablement + //"NegativeUNL", // Commented out to prevent automatic enablement + //"TicketBatch", // Commented out to prevent automatic enablement }; return supported; } @@ -160,7 +160,6 @@ bitsetIndexToFeature(size_t i) // clang-format off uint256 const - featureTickets = *getRegisteredFeature("Tickets"), featureOwnerPaysFee = *getRegisteredFeature("OwnerPaysFee"), featureFlow = *getRegisteredFeature("Flow"), featureCompareTakerFlowCross = *getRegisteredFeature("CompareTakerFlowCross"), @@ -186,7 +185,8 @@ uint256 const fix1781 = *getRegisteredFeature("fix1781"), featureHardenedValidations = *getRegisteredFeature("HardenedValidations"), fixAmendmentMajorityCalc = *getRegisteredFeature("fixAmendmentMajorityCalc"), - featureNegativeUNL = *getRegisteredFeature("NegativeUNL"); + featureNegativeUNL = *getRegisteredFeature("NegativeUNL"), + featureTicketBatch = *getRegisteredFeature("TicketBatch"); // The following amendments have been active for at least two years. Their // pre-amendment code has been removed and the identifiers are deprecated. diff --git a/src/ripple/protocol/impl/Indexes.cpp b/src/ripple/protocol/impl/Indexes.cpp index acb3ef14e..20174d978 100644 --- a/src/ripple/protocol/impl/Indexes.cpp +++ b/src/ripple/protocol/impl/Indexes.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -107,10 +108,17 @@ getQuality(uint256 const& uBase) } uint256 -getTicketIndex(AccountID const& account, std::uint32_t uSequence) +getTicketIndex(AccountID const& account, std::uint32_t ticketSeq) { return indexHash( - LedgerNameSpace::TICKET, account, std::uint32_t(uSequence)); + LedgerNameSpace::TICKET, account, std::uint32_t(ticketSeq)); +} + +uint256 +getTicketIndex(AccountID const& account, SeqProxy ticketSeq) +{ + assert(ticketSeq.isTicket()); + return getTicketIndex(account, ticketSeq.value()); } //------------------------------------------------------------------------------ @@ -238,9 +246,15 @@ next_t::operator()(Keylet const& k) const } Keylet -ticket_t::operator()(AccountID const& id, std::uint32_t seq) const +ticket_t::operator()(AccountID const& id, std::uint32_t ticketSeq) const { - return {ltTICKET, getTicketIndex(id, seq)}; + return {ltTICKET, getTicketIndex(id, ticketSeq)}; +} + +Keylet +ticket_t::operator()(AccountID const& id, SeqProxy ticketSeq) const +{ + return {ltTICKET, getTicketIndex(id, ticketSeq)}; } // This function is presently static, since it's never accessed from anywhere diff --git a/src/ripple/protocol/impl/LedgerFormats.cpp b/src/ripple/protocol/impl/LedgerFormats.cpp index e6941803a..b6feb3823 100644 --- a/src/ripple/protocol/impl/LedgerFormats.cpp +++ b/src/ripple/protocol/impl/LedgerFormats.cpp @@ -53,6 +53,7 @@ LedgerFormats::LedgerFormats() {sfTransferRate, soeOPTIONAL}, {sfDomain, soeOPTIONAL}, {sfTickSize, soeOPTIONAL}, + {sfTicketCount, soeOPTIONAL}, }, commonFields); @@ -155,10 +156,10 @@ LedgerFormats::LedgerFormats() ltTICKET, { {sfAccount, soeREQUIRED}, - {sfSequence, soeREQUIRED}, {sfOwnerNode, soeREQUIRED}, - {sfTarget, soeOPTIONAL}, - {sfExpiration, soeOPTIONAL}, + {sfTicketSequence, soeREQUIRED}, + {sfPreviousTxnID, soeREQUIRED}, + {sfPreviousTxnLgrSeq, soeREQUIRED}, }, commonFields); diff --git a/src/ripple/protocol/impl/SField.cpp b/src/ripple/protocol/impl/SField.cpp index 0717db309..3f0454b31 100644 --- a/src/ripple/protocol/impl/SField.cpp +++ b/src/ripple/protocol/impl/SField.cpp @@ -117,6 +117,8 @@ SF_U32 const sfCancelAfter(access, STI_UINT32, 36, "CancelAfter"); SF_U32 const sfFinishAfter(access, STI_UINT32, 37, "FinishAfter"); SF_U32 const sfSignerListID(access, STI_UINT32, 38, "SignerListID"); SF_U32 const sfSettleDelay(access, STI_UINT32, 39, "SettleDelay"); +SF_U32 const sfTicketCount(access, STI_UINT32, 40, "TicketCount"); +SF_U32 const sfTicketSequence(access, STI_UINT32, 41, "TicketSequence"); // 64-bit integers SF_U64 const sfIndexNext(access, STI_UINT64, 1, "IndexNext"); @@ -162,7 +164,7 @@ SF_U256 const sfBookDirectory(access, STI_HASH256, 16, "BookDirectory"); SF_U256 const sfInvoiceID(access, STI_HASH256, 17, "InvoiceID"); SF_U256 const sfNickname(access, STI_HASH256, 18, "Nickname"); SF_U256 const sfAmendment(access, STI_HASH256, 19, "Amendment"); -SF_U256 const sfTicketID(access, STI_HASH256, 20, "TicketID"); +// 20 is currently unused SF_U256 const sfDigest(access, STI_HASH256, 21, "Digest"); SF_U256 const sfPayChannel(access, STI_HASH256, 22, "Channel"); SF_U256 const sfConsensusHash(access, STI_HASH256, 23, "ConsensusHash"); @@ -234,7 +236,7 @@ SF_Account const sfDestination(access, STI_ACCOUNT, 3, "Destination"); SF_Account const sfIssuer(access, STI_ACCOUNT, 4, "Issuer"); SF_Account const sfAuthorize(access, STI_ACCOUNT, 5, "Authorize"); SF_Account const sfUnauthorize(access, STI_ACCOUNT, 6, "Unauthorize"); -SF_Account const sfTarget(access, STI_ACCOUNT, 7, "Target"); +// 7 is currently unused SF_Account const sfRegularKey(access, STI_ACCOUNT, 8, "RegularKey"); // path set diff --git a/src/ripple/protocol/impl/STTx.cpp b/src/ripple/protocol/impl/STTx.cpp index d5f468b66..14bffddea 100644 --- a/src/ripple/protocol/impl/STTx.cpp +++ b/src/ripple/protocol/impl/STTx.cpp @@ -160,6 +160,22 @@ STTx::getSignature() const } } +SeqProxy +STTx::getSeqProxy() const +{ + std::uint32_t const seq{getFieldU32(sfSequence)}; + if (seq != 0) + return SeqProxy::sequence(seq); + + boost::optional const ticketSeq{operator[]( + ~sfTicketSequence)}; + if (!ticketSeq) + // No TicketSequence specified. Return the Sequence, whatever it is. + return SeqProxy::sequence(seq); + + return SeqProxy{SeqProxy::ticket, *ticketSeq}; +} + void STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey) { @@ -250,8 +266,8 @@ STTx::getMetaSQL( return str( boost::format(bfTrans) % to_string(getTransactionID()) % - format->getName() % toBase58(getAccountID(sfAccount)) % getSequence() % - inLedger % status % rTxn % escapedMetaData); + format->getName() % toBase58(getAccountID(sfAccount)) % + getFieldU32(sfSequence) % inLedger % status % rTxn % escapedMetaData); } std::pair diff --git a/src/ripple/protocol/impl/TER.cpp b/src/ripple/protocol/impl/TER.cpp index f496711dd..81b90da77 100644 --- a/src/ripple/protocol/impl/TER.cpp +++ b/src/ripple/protocol/impl/TER.cpp @@ -100,6 +100,7 @@ transResults() MAKE_ERROR(tefBAD_AUTH_MASTER, "Auth for unclaimed account needs correct master key."), MAKE_ERROR(tefINVARIANT_FAILED, "Fee claim violated invariants for the transaction."), MAKE_ERROR(tefTOO_BIG, "Transaction affects too many items."), + MAKE_ERROR(tefNO_TICKET, "Ticket is not in ledger."), MAKE_ERROR(telLOCAL_ERROR, "Local failure."), MAKE_ERROR(telBAD_DOMAIN, "Domain too long."), @@ -150,6 +151,7 @@ transResults() MAKE_ERROR(temBAD_TICK_SIZE, "Malformed: Tick size out of range."), MAKE_ERROR(temINVALID_ACCOUNT_ID, "Malformed: A field contains an invalid account ID."), MAKE_ERROR(temCANNOT_PREAUTH_SELF, "Malformed: An account may not preauthorize itself."), + MAKE_ERROR(temINVALID_COUNT, "Malformed: Count field outside valid range."), MAKE_ERROR(terRETRY, "Retry transaction."), MAKE_ERROR(terFUNDS_SPENT, "DEPRECATED."), @@ -162,6 +164,7 @@ transResults() MAKE_ERROR(terPRE_SEQ, "Missing/inapplicable prior transaction."), MAKE_ERROR(terOWNERS, "Non-zero owner count."), MAKE_ERROR(terQUEUED, "Held until escalated fee drops."), + {terPRE_TICKET, {"terPRE_TICKET", "Ticket is not yet in ledger."}}, MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."), }; diff --git a/src/ripple/protocol/impl/TxFormats.cpp b/src/ripple/protocol/impl/TxFormats.cpp index 24a8ef197..dd94c09c1 100644 --- a/src/ripple/protocol/impl/TxFormats.cpp +++ b/src/ripple/protocol/impl/TxFormats.cpp @@ -54,6 +54,7 @@ TxFormats::TxFormats() {sfSetFlag, soeOPTIONAL}, {sfClearFlag, soeOPTIONAL}, {sfTickSize, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -63,6 +64,7 @@ TxFormats::TxFormats() {sfLimitAmount, soeOPTIONAL}, {sfQualityIn, soeOPTIONAL}, {sfQualityOut, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -73,6 +75,7 @@ TxFormats::TxFormats() {sfTakerGets, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, {sfOfferSequence, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -80,6 +83,7 @@ TxFormats::TxFormats() ttOFFER_CANCEL, { {sfOfferSequence, soeREQUIRED}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -87,6 +91,7 @@ TxFormats::TxFormats() ttREGULAR_KEY_SET, { {sfRegularKey, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -100,6 +105,7 @@ TxFormats::TxFormats() {sfInvoiceID, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, {sfDeliverMin, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -112,6 +118,7 @@ TxFormats::TxFormats() {sfCancelAfter, soeOPTIONAL}, {sfFinishAfter, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -122,6 +129,7 @@ TxFormats::TxFormats() {sfOfferSequence, soeREQUIRED}, {sfFulfillment, soeOPTIONAL}, {sfCondition, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -130,6 +138,7 @@ TxFormats::TxFormats() { {sfOwner, soeREQUIRED}, {sfOfferSequence, soeREQUIRED}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -164,15 +173,8 @@ TxFormats::TxFormats() add(jss::TicketCreate, ttTICKET_CREATE, { - {sfTarget, soeOPTIONAL}, - {sfExpiration, soeOPTIONAL}, - }, - commonFields); - - add(jss::TicketCancel, - ttTICKET_CANCEL, - { - {sfTicketID, soeREQUIRED}, + {sfTicketCount, soeREQUIRED}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -183,6 +185,7 @@ TxFormats::TxFormats() { {sfSignerQuorum, soeREQUIRED}, {sfSignerEntries, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -195,6 +198,7 @@ TxFormats::TxFormats() {sfPublicKey, soeREQUIRED}, {sfCancelAfter, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -204,6 +208,7 @@ TxFormats::TxFormats() {sfPayChannel, soeREQUIRED}, {sfAmount, soeREQUIRED}, {sfExpiration, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -215,6 +220,7 @@ TxFormats::TxFormats() {sfBalance, soeOPTIONAL}, {sfSignature, soeOPTIONAL}, {sfPublicKey, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -226,6 +232,7 @@ TxFormats::TxFormats() {sfExpiration, soeOPTIONAL}, {sfDestinationTag, soeOPTIONAL}, {sfInvoiceID, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -235,6 +242,7 @@ TxFormats::TxFormats() {sfCheckID, soeREQUIRED}, {sfAmount, soeOPTIONAL}, {sfDeliverMin, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -242,6 +250,7 @@ TxFormats::TxFormats() ttCHECK_CANCEL, { {sfCheckID, soeREQUIRED}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -250,6 +259,7 @@ TxFormats::TxFormats() { {sfDestination, soeREQUIRED}, {sfDestinationTag, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); @@ -258,6 +268,7 @@ TxFormats::TxFormats() { {sfAuthorize, soeOPTIONAL}, {sfUnauthorize, soeOPTIONAL}, + {sfTicketSequence, soeOPTIONAL}, }, commonFields); } diff --git a/src/ripple/protocol/jss.h b/src/ripple/protocol/jss.h index 1df4bf7fc..cf1f43354 100644 --- a/src/ripple/protocol/jss.h +++ b/src/ripple/protocol/jss.h @@ -94,7 +94,6 @@ JSS(SigningPubKey); // field. JSS(TakerGets); // field. JSS(TakerPays); // field. JSS(Ticket); // ledger type. -JSS(TicketCancel); // transaction type. JSS(TicketCreate); // transaction type. JSS(TxnSignature); // field. JSS(TransactionType); // in: TransactionSign. @@ -260,6 +259,7 @@ JSS(have_header); // out: InboundLedger JSS(have_state); // out: InboundLedger JSS(have_transactions); // out: InboundLedger JSS(highest_sequence); // out: AccountInfo +JSS(highest_ticket); // out: AccountInfo JSS(historical_perminute); // historical_perminute. JSS(hostid); // out: NetworkOPs JSS(hotwallet); // in: GatewayBalances @@ -271,7 +271,6 @@ JSS(inbound); // out: PeerImp JSS(index); // in: LedgerEntry, DownloadShard // out: STLedgerEntry, // LedgerEntry, TxHistory, LedgerData - // field JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo JSS(internal_command); // in: Internal JSS(invalid_API_version); // out: Many, when a request has an invalid @@ -337,6 +336,7 @@ JSS(local); // out: resource/Logic.h JSS(local_txs); // out: GetCounts JSS(local_static_keys); // out: ValidatorList JSS(lowest_sequence); // out: AccountInfo +JSS(lowest_ticket); // out: AccountInfo JSS(majority); // out: RPC feature JSS(manifest); // out: ValidatorInfo, Manifest JSS(marker); // in/out: AccountTx, AccountOffers, @@ -472,6 +472,7 @@ JSS(seq); // in: LedgerEntry; // out: NetworkOPs, RPCSub, AccountOffers, // ValidatorList, ValidatorInfo, Manifest JSS(seqNum); // out: LedgerToJson +JSS(sequence_count); // out: AccountInfo JSS(server_state); // out: NetworkOPs JSS(server_state_duration_us); // out: NetworkOPs JSS(server_status); // out: NetworkOPs @@ -513,6 +514,8 @@ JSS(taker_pays); // in: Subscribe, Unsubscribe, BookOffers JSS(taker_pays_funded); // out: NetworkOPs JSS(threshold); // in: Blacklist JSS(ticket); // in: AccountObjects +JSS(ticket_count); // out: AccountInfo +JSS(ticket_seq); // in: LedgerEntry JSS(time); JSS(timeouts); // out: InboundLedger JSS(traffic); // out: Overlay diff --git a/src/ripple/rpc/handlers/AccountInfo.cpp b/src/ripple/rpc/handlers/AccountInfo.cpp index 4244f7057..d8d638b0a 100644 --- a/src/ripple/rpc/handlers/AccountInfo.cpp +++ b/src/ripple/rpc/handlers/AccountInfo.cpp @@ -123,52 +123,79 @@ doAccountInfo(RPC::JsonContext& context) { jvQueueData[jss::txn_count] = static_cast(txs.size()); - jvQueueData[jss::lowest_sequence] = txs.begin()->first; - jvQueueData[jss::highest_sequence] = txs.rbegin()->first; auto& jvQueueTx = jvQueueData[jss::transactions]; jvQueueTx = Json::arrayValue; - boost::optional anyAuthChanged(false); - boost::optional totalSpend(0); + std::uint32_t seqCount = 0; + std::uint32_t ticketCount = 0; + boost::optional lowestSeq; + boost::optional highestSeq; + boost::optional lowestTicket; + boost::optional highestTicket; + bool anyAuthChanged = false; + XRPAmount totalSpend(0); - for (auto const& [txSeq, txDetails] : txs) + // We expect txs to be returned sorted by SeqProxy. Verify + // that with a couple of asserts. + SeqProxy prevSeqProxy = SeqProxy::sequence(0); + for (auto const& tx : txs) { Json::Value jvTx = Json::objectValue; - jvTx[jss::seq] = txSeq; - jvTx[jss::fee_level] = to_string(txDetails.feeLevel); - if (txDetails.lastValid) - jvTx[jss::LastLedgerSequence] = *txDetails.lastValid; - if (txDetails.consequences) + if (tx.seqProxy.isSeq()) { - jvTx[jss::fee] = to_string(txDetails.consequences->fee); - auto spend = txDetails.consequences->potentialSpend + - txDetails.consequences->fee; - jvTx[jss::max_spend_drops] = to_string(spend); - if (totalSpend) - *totalSpend += spend; - auto authChanged = txDetails.consequences->category == - TxConsequences::blocker; - if (authChanged) - anyAuthChanged.emplace(authChanged); - jvTx[jss::auth_change] = authChanged; + assert(prevSeqProxy < tx.seqProxy); + prevSeqProxy = tx.seqProxy; + jvTx[jss::seq] = tx.seqProxy.value(); + ++seqCount; + if (!lowestSeq) + lowestSeq = tx.seqProxy.value(); + highestSeq = tx.seqProxy.value(); } else { - if (anyAuthChanged && !*anyAuthChanged) - anyAuthChanged.reset(); - totalSpend.reset(); + assert(prevSeqProxy < tx.seqProxy); + prevSeqProxy = tx.seqProxy; + jvTx[jss::ticket] = tx.seqProxy.value(); + ++ticketCount; + if (!lowestTicket) + lowestTicket = tx.seqProxy.value(); + highestTicket = tx.seqProxy.value(); } + jvTx[jss::fee_level] = to_string(tx.feeLevel); + if (tx.lastValid) + jvTx[jss::LastLedgerSequence] = *tx.lastValid; + + jvTx[jss::fee] = to_string(tx.consequences.fee()); + auto const spend = tx.consequences.potentialSpend() + + tx.consequences.fee(); + jvTx[jss::max_spend_drops] = to_string(spend); + totalSpend += spend; + bool const authChanged = tx.consequences.isBlocker(); + if (authChanged) + anyAuthChanged = authChanged; + jvTx[jss::auth_change] = authChanged; + jvQueueTx.append(std::move(jvTx)); } - if (anyAuthChanged) - jvQueueData[jss::auth_change_queued] = *anyAuthChanged; - if (totalSpend) - jvQueueData[jss::max_spend_drops_total] = - to_string(*totalSpend); + if (seqCount) + jvQueueData[jss::sequence_count] = seqCount; + if (ticketCount) + jvQueueData[jss::ticket_count] = ticketCount; + if (lowestSeq) + jvQueueData[jss::lowest_sequence] = *lowestSeq; + if (highestSeq) + jvQueueData[jss::highest_sequence] = *highestSeq; + if (lowestTicket) + jvQueueData[jss::lowest_ticket] = *lowestTicket; + if (highestTicket) + jvQueueData[jss::highest_ticket] = *highestTicket; + + jvQueueData[jss::auth_change_queued] = anyAuthChanged; + jvQueueData[jss::max_spend_drops_total] = to_string(totalSpend); } else jvQueueData[jss::txn_count] = 0u; @@ -259,7 +286,7 @@ doAccountInfoGrpc( "requested queue but ledger is not open"}; return {result, errorStatus}; } - auto const txs = + std::vector const txs = context.app.getTxQ().getAccountTxs(accountID, *ledger); org::xrpl::rpc::v1::QueueData& queueData = *result.mutable_queue_data(); diff --git a/src/ripple/rpc/handlers/Fee1.cpp b/src/ripple/rpc/handlers/Fee1.cpp index 3f06cb515..f28f9b447 100644 --- a/src/ripple/rpc/handlers/Fee1.cpp +++ b/src/ripple/rpc/handlers/Fee1.cpp @@ -72,15 +72,14 @@ doFeeGrpc(RPC::GRPCContext& context) org::xrpl::rpc::v1::Fee& fee = *reply.mutable_fee(); auto const baseFee = view->fees().base; fee.mutable_base_fee()->set_drops( - toDrops(metrics.referenceFeeLevel, baseFee).second.drops()); + toDrops(metrics.referenceFeeLevel, baseFee).drops()); fee.mutable_minimum_fee()->set_drops( - toDrops(metrics.minProcessingFeeLevel, baseFee).second.drops()); + toDrops(metrics.minProcessingFeeLevel, baseFee).drops()); fee.mutable_median_fee()->set_drops( - toDrops(metrics.medFeeLevel, baseFee).second.drops()); + toDrops(metrics.medFeeLevel, baseFee).drops()); fee.mutable_open_ledger_fee()->set_drops( - (toDrops(metrics.openLedgerFeeLevel - FeeLevel64{1}, baseFee).second + - 1) + (toDrops(metrics.openLedgerFeeLevel - FeeLevel64{1}, baseFee) + 1) .drops()); return {reply, status}; } diff --git a/src/ripple/rpc/handlers/LedgerEntry.cpp b/src/ripple/rpc/handlers/LedgerEntry.cpp index a521d2724..6d0fbbdb0 100644 --- a/src/ripple/rpc/handlers/LedgerEntry.cpp +++ b/src/ripple/rpc/handlers/LedgerEntry.cpp @@ -266,6 +266,31 @@ doLedgerEntry(RPC::JsonContext& context) } } } + else if (context.params.isMember(jss::ticket)) + { + expectedType = ltTICKET; + if (!context.params[jss::ticket].isObject()) + { + uNodeIndex.SetHex(context.params[jss::ticket].asString()); + } + else if ( + !context.params[jss::ticket].isMember(jss::account) || + !context.params[jss::ticket].isMember(jss::ticket_seq) || + !context.params[jss::ticket][jss::ticket_seq].isIntegral()) + { + jvResult[jss::error] = "malformedRequest"; + } + else + { + auto const id = parseBase58( + context.params[jss::ticket][jss::account].asString()); + if (!id) + jvResult[jss::error] = "malformedAddress"; + else + uNodeIndex = getTicketIndex( + *id, context.params[jss::ticket][jss::ticket_seq].asUInt()); + } + } else { jvResult[jss::error] = "unknownOption"; diff --git a/src/ripple/rpc/impl/GRPCHelpers.cpp b/src/ripple/rpc/impl/GRPCHelpers.cpp index 237242741..00d6af0f9 100644 --- a/src/ripple/rpc/impl/GRPCHelpers.cpp +++ b/src/ripple/rpc/impl/GRPCHelpers.cpp @@ -416,6 +416,15 @@ populateSignerQuorum(T& to, STObject const& from) populateProtoPrimitive( [&to]() { return to.mutable_signer_quorum(); }, from, sfSignerQuorum); } + +template +void +populateTicketCount(T& to, STObject const& from) +{ + populateProtoPrimitive( + [&to]() { return to.mutable_count(); }, from, sfTicketCount); +} + template void populateLimitAmount(T& to, STObject const& from) @@ -737,6 +746,16 @@ populateSignerListID(T& to, STObject const& from) [&to]() { return to.mutable_signer_list_id(); }, from, sfSignerListID); } +template +void +populateTicketSequence(T& to, STObject const& from) +{ + populateProtoPrimitive( + [&to]() { return to.mutable_ticket_sequence(); }, + from, + sfTicketSequence); +} + template void populateHashes(T& to, STObject const& from) @@ -1143,6 +1162,12 @@ convert(org::xrpl::rpc::v1::SignerListSet& to, STObject const& from) populateSignerEntries(to, from); } +void +convert(org::xrpl::rpc::v1::TicketCreate& to, STObject const& from) +{ + populateTicketCount(to, from); +} + void convert(org::xrpl::rpc::v1::TrustSet& to, STObject const& from) { @@ -1474,6 +1499,22 @@ convert(org::xrpl::rpc::v1::NegativeUNL& to, STObject const& from) populateFlags(to, from); } +void +convert(org::xrpl::rpc::v1::TicketObject& to, STObject const& from) +{ + populateAccount(to, from); + + populateFlags(to, from); + + populateOwnerNode(to, from); + + populatePreviousTransactionID(to, from); + + populatePreviousTransactionLedgerSequence(to, from); + + populateTicketSequence(to, from); +} + void setLedgerEntryType( org::xrpl::rpc::v1::AffectedNode& proto, @@ -1533,6 +1574,10 @@ setLedgerEntryType( proto.set_ledger_entry_type( org::xrpl::rpc::v1::LEDGER_ENTRY_TYPE_NEGATIVE_UNL); break; + case ltTICKET: + proto.set_ledger_entry_type( + org::xrpl::rpc::v1::LEDGER_ENTRY_TYPE_TICKET); + break; } } @@ -1581,6 +1626,9 @@ convert(T& to, STObject& from, std::uint16_t type) case ltNEGATIVE_UNL: RPC::convert(*to.mutable_negative_unl(), from); break; + case ltTICKET: + RPC::convert(*to.mutable_ticket(), from); + break; } } @@ -1698,55 +1746,72 @@ convert(org::xrpl::rpc::v1::Meta& to, std::shared_ptr const& from) void convert( org::xrpl::rpc::v1::QueueData& to, - std::map const& from) + std::vector const& from) { if (!from.empty()) { to.set_txn_count(from.size()); - to.set_lowest_sequence(from.begin()->first); - to.set_highest_sequence(from.rbegin()->first); - boost::optional anyAuthChanged(false); - boost::optional totalSpend(0); + std::uint32_t seqCount = 0; + std::uint32_t ticketCount = 0; + boost::optional lowestSeq; + boost::optional highestSeq; + boost::optional lowestTicket; + boost::optional highestTicket; + bool anyAuthChanged = false; + XRPAmount totalSpend(0); - for (auto const& [txSeq, txDetails] : from) + for (auto const& tx : from) { org::xrpl::rpc::v1::QueuedTransaction& qt = *to.add_transactions(); - qt.mutable_sequence()->set_value(txSeq); - qt.set_fee_level(txDetails.feeLevel.fee()); - if (txDetails.lastValid) - qt.mutable_last_ledger_sequence()->set_value( - *txDetails.lastValid); - - if (txDetails.consequences) + if (tx.seqProxy.isSeq()) { - qt.mutable_fee()->set_drops( - txDetails.consequences->fee.drops()); - auto spend = txDetails.consequences->potentialSpend + - txDetails.consequences->fee; - qt.mutable_max_spend_drops()->set_drops(spend.drops()); - if (totalSpend) - *totalSpend += spend; - auto authChanged = - txDetails.consequences->category == TxConsequences::blocker; - if (authChanged) - anyAuthChanged.emplace(authChanged); - qt.set_auth_change(authChanged); + qt.mutable_sequence()->set_value(tx.seqProxy.value()); + ++seqCount; + if (!lowestSeq) + lowestSeq = tx.seqProxy.value(); + highestSeq = tx.seqProxy.value(); } else { - if (anyAuthChanged && !*anyAuthChanged) - anyAuthChanged.reset(); - totalSpend.reset(); + qt.mutable_ticket()->set_value(tx.seqProxy.value()); + ++ticketCount; + if (!lowestTicket) + lowestTicket = tx.seqProxy.value(); + highestTicket = tx.seqProxy.value(); } + + qt.set_fee_level(tx.feeLevel.fee()); + if (tx.lastValid) + qt.mutable_last_ledger_sequence()->set_value(*tx.lastValid); + + qt.mutable_fee()->set_drops(tx.consequences.fee().drops()); + auto const spend = + tx.consequences.potentialSpend() + tx.consequences.fee(); + qt.mutable_max_spend_drops()->set_drops(spend.drops()); + totalSpend += spend; + bool const authChanged = tx.consequences.isBlocker(); + if (authChanged) + anyAuthChanged = true; + qt.set_auth_change(authChanged); } - if (anyAuthChanged) - to.set_auth_change_queued(*anyAuthChanged); - if (totalSpend) - to.mutable_max_spend_drops_total()->set_drops( - (*totalSpend).drops()); + if (seqCount) + to.set_sequence_count(seqCount); + if (ticketCount) + to.set_ticket_count(ticketCount); + if (lowestSeq) + to.set_lowest_sequence(*lowestSeq); + if (highestSeq) + to.set_highest_sequence(*highestSeq); + if (lowestTicket) + to.set_lowest_ticket(*lowestTicket); + if (highestTicket) + to.set_highest_ticket(*highestTicket); + + to.set_auth_change_queued(anyAuthChanged); + to.mutable_max_spend_drops_total()->set_drops(totalSpend.drops()); } } @@ -1779,6 +1844,8 @@ convert( populateSigners(to, fromObj); + populateTicketSequence(to, fromObj); + auto type = safe_cast(fromObj.getFieldU16(sfTransactionType)); switch (type) @@ -1837,6 +1904,9 @@ convert( case TxType::ttACCOUNT_DELETE: convert(*to.mutable_account_delete(), fromObj); break; + case TxType::ttTICKET_CREATE: + convert(*to.mutable_ticket_create(), fromObj); + break; default: break; } diff --git a/src/ripple/rpc/impl/GRPCHelpers.h b/src/ripple/rpc/impl/GRPCHelpers.h index fcf0bfd1a..c5fb669b1 100644 --- a/src/ripple/rpc/impl/GRPCHelpers.h +++ b/src/ripple/rpc/impl/GRPCHelpers.h @@ -42,7 +42,7 @@ convert(org::xrpl::rpc::v1::Meta& to, std::shared_ptr const& from); void convert( org::xrpl::rpc::v1::QueueData& to, - std::map const& from); + std::vector const& from); void convert( diff --git a/src/ripple/rpc/impl/RPCHelpers.cpp b/src/ripple/rpc/impl/RPCHelpers.cpp index dbc86774d..2a8e18eed 100644 --- a/src/ripple/rpc/impl/RPCHelpers.cpp +++ b/src/ripple/rpc/impl/RPCHelpers.cpp @@ -800,7 +800,7 @@ chooseLedgerEntryType(Json::Value const& params) std::pair result{RPC::Status::OK, ltINVALID}; if (params.isMember(jss::type)) { - static std::array, 13> const + static constexpr std::array, 13> types{ {{jss::account, ltACCOUNT_ROOT}, {jss::amendments, ltAMENDMENTS}, diff --git a/src/ripple/rpc/impl/TransactionSign.cpp b/src/ripple/rpc/impl/TransactionSign.cpp index 0cceca142..7556b5693 100644 --- a/src/ripple/rpc/impl/TransactionSign.cpp +++ b/src/ripple/rpc/impl/TransactionSign.cpp @@ -450,20 +450,7 @@ transactionPreProcessImpl( return rpcError(rpcSRC_ACT_NOT_FOUND); } - - auto seq = (*sle)[sfSequence]; - auto const queued = - app.getTxQ().getAccountTxs(srcAddressID, *ledger); - // If the account has any txs in the TxQ, skip those sequence - // numbers (accounting for possible gaps). - for (auto const& tx : queued) - { - if (tx.first == seq) - ++seq; - else if (tx.first > seq) - break; - } - tx_json[jss::Sequence] = seq; + tx_json[jss::Sequence] = app.getTxQ().nextQueuableSeq(sle).value(); } if (!tx_json.isMember(jss::Flags)) @@ -741,9 +728,7 @@ checkFee( auto const metrics = txQ.getMetrics(*ledger); auto const baseFee = ledger->fees().base; auto escalatedFee = - toDrops(metrics.openLedgerFeeLevel - FeeLevel64{1}, baseFee) - .second + - 1; + toDrops(metrics.openLedgerFeeLevel - FeeLevel64(1), baseFee) + 1; fee = std::max(fee, escalatedFee); } diff --git a/src/test/app/AccountDelete_test.cpp b/src/test/app/AccountDelete_test.cpp index 5e571fce6..71fff4523 100644 --- a/src/test/app/AccountDelete_test.cpp +++ b/src/test/app/AccountDelete_test.cpp @@ -110,7 +110,7 @@ public: testcase("Basics"); - Env env{*this}; + Env env{*this, supported_amendments() | featureTicketBatch}; Account const alice("alice"); Account const becky("becky"); Account const carol("carol"); @@ -144,11 +144,13 @@ public: env(trust(becky, gw["USD"](1000))); env.close(); - // Give carol a deposit preauthorization, an offer, and a signer list. - // Even with all that she's still deletable. + // Give carol a deposit preauthorization, an offer, a ticket, + // and a signer list. Even with all that she's still deletable. env(deposit::auth(carol, becky)); std::uint32_t const carolOfferSeq{env.seq(carol)}; env(offer(carol, gw["USD"](51), XRP(51))); + std::uint32_t const carolTicketSeq{env.seq(carol) + 1}; + env(ticket::create(carol, 1)); env(signers(carol, 1, {{alice, 1}, {becky, 1}})); // Deleting should fail with TOO_SOON, which is a relatively @@ -200,13 +202,15 @@ public: auto const carolOldBalance{env.balance(carol)}; // Verify that Carol's account, directory, deposit - // preauthorization, offer, and signer list exist. + // preauthorization, offer, ticket, and signer list exist. BEAST_EXPECT(env.closed()->exists(keylet::account(carol.id()))); BEAST_EXPECT(env.closed()->exists(keylet::ownerDir(carol.id()))); BEAST_EXPECT(env.closed()->exists( keylet::depositPreauth(carol.id(), becky.id()))); BEAST_EXPECT( env.closed()->exists(keylet::offer(carol.id(), carolOfferSeq))); + BEAST_EXPECT(env.closed()->exists( + keylet::ticket(carol.id(), carolTicketSeq))); BEAST_EXPECT(env.closed()->exists(keylet::signers(carol.id()))); // Delete carol's account even with stuff in her directory. Show @@ -222,6 +226,8 @@ public: keylet::depositPreauth(carol.id(), becky.id()))); BEAST_EXPECT(!env.closed()->exists( keylet::offer(carol.id(), carolOfferSeq))); + BEAST_EXPECT(!env.closed()->exists( + keylet::ticket(carol.id(), carolTicketSeq))); BEAST_EXPECT(!env.closed()->exists(keylet::signers(carol.id()))); // Verify that Carol's XRP, minus the fee, was transferred to becky. @@ -775,6 +781,57 @@ public: } } + void + testWithTickets() + { + testcase("With Tickets"); + + using namespace test::jtx; + + Account const alice{"alice"}; + Account const bob{"bob"}; + + Env env{*this, supported_amendments() | featureTicketBatch}; + env.fund(XRP(100000), alice, bob); + env.close(); + + // bob grabs as many tickets as he is allowed to have. + std::uint32_t const ticketSeq{env.seq(bob) + 1}; + env(ticket::create(bob, 250)); + env.close(); + env.require(owners(bob, 250)); + + { + std::shared_ptr closed{env.closed()}; + BEAST_EXPECT(closed->exists(keylet::account(bob.id()))); + for (std::uint32_t i = 0; i < 250; ++i) + { + BEAST_EXPECT( + closed->exists(keylet::ticket(bob.id(), ticketSeq + i))); + } + } + + // Close enough ledgers to be able to delete bob's account. + incLgrSeqForAccDel(env, bob); + + // bob deletes his account using a ticket. bob's account and all + // of his tickets should be removed from the ledger. + auto const acctDelFee{drops(env.current()->fees().increment)}; + auto const bobOldBalance{env.balance(bob)}; + env(acctdelete(bob, alice), ticket::use(ticketSeq), fee(acctDelFee)); + verifyDeliveredAmount(env, bobOldBalance - acctDelFee); + env.close(); + { + std::shared_ptr closed{env.closed()}; + BEAST_EXPECT(!closed->exists(keylet::account(bob.id()))); + for (std::uint32_t i = 0; i < 250; ++i) + { + BEAST_EXPECT( + !closed->exists(keylet::ticket(bob.id(), ticketSeq + i))); + } + } + } + void run() override { @@ -786,6 +843,7 @@ public: testTooManyOffers(); testImplicitlyCreatedTrustline(); testBalanceTooSmallForFee(); + testWithTickets(); } }; diff --git a/src/test/app/Check_test.cpp b/src/test/app/Check_test.cpp index b555c7d3a..fb5f6301a 100644 --- a/src/test/app/Check_test.cpp +++ b/src/test/app/Check_test.cpp @@ -1746,6 +1746,108 @@ class Check_test : public beast::unit_test::suite testEnable(supported_amendments(), true); } + void + testWithTickets() + { + testcase("With Tickets"); + + using namespace test::jtx; + + Account const gw{"gw"}; + Account const alice{"alice"}; + Account const bob{"bob"}; + IOU const USD{gw["USD"]}; + + Env env{*this, supported_amendments() | featureTicketBatch}; + env.fund(XRP(1000), gw, alice, bob); + env.close(); + + // alice and bob grab enough tickets for all of the following + // transactions. Note that once the tickets are acquired alice's + // and bob's account sequence numbers should not advance. + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + std::uint32_t const aliceSeq{env.seq(alice)}; + + std::uint32_t bobTicketSeq{env.seq(bob) + 1}; + env(ticket::create(bob, 10)); + std::uint32_t const bobSeq{env.seq(bob)}; + + env.close(); + env.require(owners(alice, 10)); + env.require(owners(bob, 10)); + + // alice gets enough USD to write a few checks. + env(trust(alice, USD(1000)), ticket::use(aliceTicketSeq++)); + env(trust(bob, USD(1000)), ticket::use(bobTicketSeq++)); + env.close(); + env.require(owners(alice, 10)); + env.require(owners(bob, 10)); + + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + env(pay(gw, alice, USD(900))); + env.close(); + + // alice creates four checks; two XRP, two IOU. Bob will cash + // one of each and cancel one of each. + uint256 const chkIdXrp1{getCheckIndex(alice, aliceTicketSeq)}; + env(check::create(alice, bob, XRP(200)), ticket::use(aliceTicketSeq++)); + + uint256 const chkIdXrp2{getCheckIndex(alice, aliceTicketSeq)}; + env(check::create(alice, bob, XRP(300)), ticket::use(aliceTicketSeq++)); + + uint256 const chkIdUsd1{getCheckIndex(alice, aliceTicketSeq)}; + env(check::create(alice, bob, USD(200)), ticket::use(aliceTicketSeq++)); + + uint256 const chkIdUsd2{getCheckIndex(alice, aliceTicketSeq)}; + env(check::create(alice, bob, USD(300)), ticket::use(aliceTicketSeq++)); + + env.close(); + // Alice used four tickets but created four checks. + env.require(owners(alice, 10)); + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(checksOnAccount(env, alice).size() == 4); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + + env.require(owners(bob, 10)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + // Bob cancels two of alice's checks. + env(check::cancel(bob, chkIdXrp1), ticket::use(bobTicketSeq++)); + env(check::cancel(bob, chkIdUsd2), ticket::use(bobTicketSeq++)); + env.close(); + + env.require(owners(alice, 8)); + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(checksOnAccount(env, alice).size() == 2); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + + env.require(owners(bob, 8)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + // Bob cashes alice's two remaining checks. + env(check::cash(bob, chkIdXrp2, XRP(300)), ticket::use(bobTicketSeq++)); + env(check::cash(bob, chkIdUsd1, USD(200)), ticket::use(bobTicketSeq++)); + env.close(); + + env.require(owners(alice, 6)); + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(checksOnAccount(env, alice).size() == 0); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + env.require(balance(alice, USD(700))); + env.require(balance(alice, drops(699'999'940))); + + env.require(owners(bob, 6)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + env.require(balance(bob, USD(200))); + env.require(balance(bob, drops(1'299'999'940))); + } + public: void run() override @@ -1761,6 +1863,7 @@ public: testCancelValid(); testCancelInvalid(); testFix1623Enable(); + testWithTickets(); } }; diff --git a/src/test/app/DepositAuth_test.cpp b/src/test/app/DepositAuth_test.cpp index 6097ffafb..ea3413ff2 100644 --- a/src/test/app/DepositAuth_test.cpp +++ b/src/test/app/DepositAuth_test.cpp @@ -431,6 +431,38 @@ struct DepositPreauth_test : public beast::unit_test::suite env.require(owners(alice, 0)); env.require(owners(becky, 0)); } + { + // Verify that an account can be preauthorized and unauthorized + // using tickets. + Env env(*this, supported_amendments() | featureTicketBatch); + env.fund(XRP(10000), alice, becky); + env.close(); + + env(ticket::create(alice, 2)); + std::uint32_t const aliceSeq{env.seq(alice)}; + env.close(); + env.require(tickets(alice, 2)); + + // Consume the tickets from biggest seq to smallest 'cuz we can. + std::uint32_t aliceTicketSeq{env.seq(alice)}; + + // Add a DepositPreauth to alice. + env(deposit::auth(alice, becky), ticket::use(--aliceTicketSeq)); + env.close(); + // Alice uses a ticket but gains a preauth entry. + env.require(tickets(alice, 1)); + env.require(owners(alice, 2)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + env.require(owners(becky, 0)); + + // Remove a DepositPreauth from alice. + env(deposit::unauth(alice, becky), ticket::use(--aliceTicketSeq)); + env.close(); + env.require(tickets(alice, 0)); + env.require(owners(alice, 0)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + env.require(owners(becky, 0)); + } } void @@ -697,8 +729,9 @@ struct DepositPreauth_test : public beast::unit_test::suite { testEnable(); testInvalid(); - testPayment(jtx::supported_amendments() - featureDepositPreauth); - testPayment(jtx::supported_amendments()); + auto const supported{jtx::supported_amendments() | featureTicketBatch}; + testPayment(supported - featureDepositPreauth); + testPayment(supported); } }; diff --git a/src/test/app/Escrow_test.cpp b/src/test/app/Escrow_test.cpp index c5fc7dbec..74d488d9c 100644 --- a/src/test/app/Escrow_test.cpp +++ b/src/test/app/Escrow_test.cpp @@ -1462,10 +1462,9 @@ struct Escrow_test : public beast::unit_test::suite tapNONE, env.journal); BEAST_EXPECT(pf.ter == tesSUCCESS); - auto const conseq = calculateConsequences(pf); - BEAST_EXPECT(conseq.category == TxConsequences::normal); - BEAST_EXPECT(conseq.fee == drops(10)); - BEAST_EXPECT(conseq.potentialSpend == XRP(1000)); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(10)); + BEAST_EXPECT(pf.consequences.potentialSpend() == XRP(1000)); } { @@ -1477,10 +1476,9 @@ struct Escrow_test : public beast::unit_test::suite tapNONE, env.journal); BEAST_EXPECT(pf.ter == tesSUCCESS); - auto const conseq = calculateConsequences(pf); - BEAST_EXPECT(conseq.category == TxConsequences::normal); - BEAST_EXPECT(conseq.fee == drops(10)); - BEAST_EXPECT(conseq.potentialSpend == XRP(0)); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(10)); + BEAST_EXPECT(pf.consequences.potentialSpend() == XRP(0)); } { @@ -1492,10 +1490,148 @@ struct Escrow_test : public beast::unit_test::suite tapNONE, env.journal); BEAST_EXPECT(pf.ter == tesSUCCESS); - auto const conseq = calculateConsequences(pf); - BEAST_EXPECT(conseq.category == TxConsequences::normal); - BEAST_EXPECT(conseq.fee == drops(10)); - BEAST_EXPECT(conseq.potentialSpend == XRP(0)); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(10)); + BEAST_EXPECT(pf.consequences.potentialSpend() == XRP(0)); + } + } + + void + testEscrowWithTickets() + { + testcase("Escrow with tickets"); + + using namespace jtx; + using namespace std::chrono; + Account const alice{"alice"}; + Account const bob{"bob"}; + + { + // Create escrow and finish using tickets. + Env env(*this, supported_amendments() | featureTicketBatch); + env.fund(XRP(5000), alice, bob); + env.close(); + + // alice creates a ticket. + std::uint32_t const aliceTicket{env.seq(alice) + 1}; + env(ticket::create(alice, 1)); + + // bob creates a bunch of tickets because he will be burning + // through them with tec transactions. Just because we can + // we'll use them up starting from largest and going smaller. + constexpr static std::uint32_t bobTicketCount{20}; + env(ticket::create(bob, bobTicketCount)); + env.close(); + std::uint32_t bobTicket{env.seq(bob)}; + env.require(tickets(alice, 1)); + env.require(tickets(bob, bobTicketCount)); + + // Note that from here on all transactions use tickets. No account + // root sequences should change. + std::uint32_t const aliceRootSeq{env.seq(alice)}; + std::uint32_t const bobRootSeq{env.seq(bob)}; + + // alice creates an escrow that can be finished in the future + auto const ts = env.now() + 97s; + + std::uint32_t const escrowSeq = aliceTicket; + env(escrow(alice, bob, XRP(1000)), + finish_time(ts), + ticket::use(aliceTicket)); + BEAST_EXPECT(env.seq(alice) == aliceRootSeq); + env.require(tickets(alice, 0)); + env.require(tickets(bob, bobTicketCount)); + + // Advance the ledger, verifying that the finish won't complete + // prematurely. Note that each tec consumes one of bob's tickets. + for (; env.now() < ts; env.close()) + { + env(finish(bob, alice, escrowSeq), + fee(1500), + ticket::use(--bobTicket), + ter(tecNO_PERMISSION)); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + } + + // bob tries to re-use a ticket, which is rejected. + env(finish(bob, alice, escrowSeq), + fee(1500), + ticket::use(bobTicket), + ter(tefNO_TICKET)); + + // bob uses one of his remaining tickets. Success! + env(finish(bob, alice, escrowSeq), + fee(1500), + ticket::use(--bobTicket)); + env.close(); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + } + + { + // Create escrow and cancel using tickets. + Env env(*this, supported_amendments() | featureTicketBatch); + env.fund(XRP(5000), alice, bob); + env.close(); + + // alice creates a ticket. + std::uint32_t const aliceTicket{env.seq(alice) + 1}; + env(ticket::create(alice, 1)); + + // bob creates a bunch of tickets because he will be burning + // through them with tec transactions. + constexpr std::uint32_t bobTicketCount{20}; + std::uint32_t bobTicket{env.seq(bob) + 1}; + env(ticket::create(bob, bobTicketCount)); + env.close(); + env.require(tickets(alice, 1)); + env.require(tickets(bob, bobTicketCount)); + + // Note that from here on all transactions use tickets. No account + // root sequences should change. + std::uint32_t const aliceRootSeq{env.seq(alice)}; + std::uint32_t const bobRootSeq{env.seq(bob)}; + + // alice creates an escrow that can be finished in the future. + auto const ts = env.now() + 117s; + + std::uint32_t const escrowSeq = aliceTicket; + env(escrow(alice, bob, XRP(1000)), + condition(cb1), + cancel_time(ts), + ticket::use(aliceTicket)); + BEAST_EXPECT(env.seq(alice) == aliceRootSeq); + env.require(tickets(alice, 0)); + env.require(tickets(bob, bobTicketCount)); + + // Advance the ledger, verifying that the cancel won't complete + // prematurely. + for (; env.now() < ts; env.close()) + { + env(cancel(bob, alice, escrowSeq), + fee(1500), + ticket::use(bobTicket++), + ter(tecNO_PERMISSION)); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + } + + // Verify that a finish won't work anymore. + env(finish(bob, alice, escrowSeq), + condition(cb1), + fulfillment(fb1), + fee(1500), + ticket::use(bobTicket++), + ter(tecNO_PERMISSION)); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + + // Verify that the cancel succeeds. + env(cancel(bob, alice, escrowSeq), + fee(1500), + ticket::use(bobTicket++)); + env.close(); + BEAST_EXPECT(env.seq(bob) == bobRootSeq); + + // Verify that bob actually consumed his tickets. + env.require(tickets(bob, env.seq(bob) - bobTicket)); } } @@ -1512,6 +1648,7 @@ struct Escrow_test : public beast::unit_test::suite testEscrowConditions(); testMetaAndOwnership(); testConsequences(); + testEscrowWithTickets(); } }; diff --git a/src/test/app/Flow_test.cpp b/src/test/app/Flow_test.cpp index b6d496787..e9d2e89bc 100644 --- a/src/test/app/Flow_test.cpp +++ b/src/test/app/Flow_test.cpp @@ -1350,6 +1350,31 @@ struct Flow_test : public beast::unit_test::suite } } + void + testTicketPay(FeatureBitset features) + { + testcase("Payment with ticket"); + using namespace jtx; + + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + Env env(*this, features); + BEAST_EXPECT(features[featureTicketBatch]); + + env.fund(XRP(10000), alice); + + // alice creates a ticket for the payment. + std::uint32_t const ticketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 1)); + + // Make a payment using the ticket. + env(pay(alice, bob, XRP(1000)), ticket::use(ticketSeq)); + env.close(); + env.require(balance(bob, XRP(1000))); + env.require(balance(alice, XRP(9000) - drops(20))); + } + void testWithFeats(FeatureBitset features) { @@ -1370,6 +1395,7 @@ struct Flow_test : public beast::unit_test::suite testUnfundedOffer(features); testReexecuteDirectStep(features); testSelfPayLowQualityOffer(features); + testTicketPay(features); } void @@ -1381,7 +1407,7 @@ struct Flow_test : public beast::unit_test::suite testRIPD1449(); using namespace jtx; - auto const sa = supported_amendments(); + auto const sa = supported_amendments() | featureTicketBatch; testWithFeats(sa - featureFlowCross); testWithFeats(sa); testEmptyStrand(sa); @@ -1394,7 +1420,7 @@ struct Flow_manual_test : public Flow_test run() override { using namespace jtx; - auto const all = supported_amendments(); + auto const all = supported_amendments() | featureTicketBatch; FeatureBitset const flowCross{featureFlowCross}; FeatureBitset const f1513{fix1513}; diff --git a/src/test/app/MultiSign_test.cpp b/src/test/app/MultiSign_test.cpp index ee6417270..433d0685a 100644 --- a/src/test/app/MultiSign_test.cpp +++ b/src/test/app/MultiSign_test.cpp @@ -1480,6 +1480,50 @@ public: env.require(owners(daria, 0)); } + void + test_signersWithTickets(FeatureBitset features) + { + testcase("Signers With Tickets"); + + using namespace jtx; + Env env{*this, features}; + Account const alice{"alice", KeyType::ed25519}; + env.fund(XRP(2000), alice); + env.close(); + + // If featureTicketBatch is not enabled expect massive failures. + BEAST_EXPECT(features[featureTicketBatch]); + + // Create a few tickets that alice can use up. + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 20)); + env.close(); + std::uint32_t const aliceSeq = env.seq(alice); + + // Attach phantom signers to alice using a ticket. + env(signers(alice, 1, {{bogie, 1}, {demon, 1}}), + ticket::use(aliceTicketSeq++)); + env.close(); + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + + // This should work. + auto const baseFee = env.current()->fees().base; + env(noop(alice), + msig(bogie, demon), + fee(3 * baseFee), + ticket::use(aliceTicketSeq++)); + env.close(); + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + + // Should also be able to remove the signer list using a ticket. + env(signers(alice, jtx::none), ticket::use(aliceTicketSeq++)); + env.close(); + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + } + void testAll(FeatureBitset features) { @@ -1499,13 +1543,14 @@ public: test_noMultiSigners(features); test_multisigningMultisigner(features); test_signForHash(features); + test_signersWithTickets(features); } void run() override { using namespace jtx; - auto const all = supported_amendments(); + auto const all = supported_amendments() | featureTicketBatch; // The reserve required on a signer list changes based on. // featureMultiSignReserve. Test both with and without. diff --git a/src/test/app/Offer_test.cpp b/src/test/app/Offer_test.cpp index a3cc0dbc6..5ef89d690 100644 --- a/src/test/app/Offer_test.cpp +++ b/src/test/app/Offer_test.cpp @@ -5134,6 +5134,260 @@ public: BEAST_EXPECT(++it == offers.end()); } + // Helper function that returns offers on an account sorted by sequence. + static std::vector> + sortedOffersOnAccount(jtx::Env& env, jtx::Account const& acct) + { + std::vector> offers{ + offersOnAccount(env, acct)}; + std::sort( + offers.begin(), + offers.end(), + [](std::shared_ptr const& rhs, + std::shared_ptr const& lhs) { + return (*rhs)[sfSequence] < (*lhs)[sfSequence]; + }); + return offers; + } + + void + testTicketOffer(FeatureBitset features) + { + testcase("Ticket Offers"); + + using namespace jtx; + + // Should be called with TicketBatch enabled. + BEAST_EXPECT(features[featureTicketBatch]); + + // Two goals for this test. + // + // o Verify that offers can be created using tickets. + // + // o Show that offers in the _same_ order book remain in + // chronological order regardless of sequence/ticket numbers. + Env env{*this, features}; + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + auto const bob = Account{"bob"}; + auto const USD = gw["USD"]; + + env.fund(XRP(10000), gw, alice, bob); + env.close(); + + env(trust(alice, USD(1000))); + env(trust(bob, USD(1000))); + env.close(); + + env(pay(gw, alice, USD(200))); + env.close(); + + // Create four offers from the same account with identical quality + // so they go in the same order book. Each offer goes in a different + // ledger so the chronology is clear. + std::uint32_t const offerId_0{env.seq(alice)}; + env(offer(alice, XRP(50), USD(50))); + env.close(); + + // Create two tickets. + std::uint32_t const ticketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 2)); + env.close(); + + // Create another sequence-based offer. + std::uint32_t const offerId_1{env.seq(alice)}; + BEAST_EXPECT(offerId_1 == offerId_0 + 4); + env(offer(alice, XRP(50), USD(50))); + env.close(); + + // Create two ticket based offers in reverse order. + std::uint32_t const offerId_2{ticketSeq + 1}; + env(offer(alice, XRP(50), USD(50)), ticket::use(offerId_2)); + env.close(); + + // Create the last offer. + std::uint32_t const offerId_3{ticketSeq}; + env(offer(alice, XRP(50), USD(50)), ticket::use(offerId_3)); + env.close(); + + // Verify that all of alice's offers are present. + { + auto offers = sortedOffersOnAccount(env, alice); + BEAST_EXPECT(offers.size() == 4); + BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId_0); + BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerId_3); + BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerId_2); + BEAST_EXPECT(offers[3]->getFieldU32(sfSequence) == offerId_1); + env.require(balance(alice, USD(200))); + env.require(owners(alice, 5)); + } + + // Cross alice's first offer. + env(offer(bob, USD(50), XRP(50))); + env.close(); + + // Verify that the first offer alice created was consumed. + { + auto offers = sortedOffersOnAccount(env, alice); + BEAST_EXPECT(offers.size() == 3); + BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId_3); + BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerId_2); + BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerId_1); + } + + // Cross alice's second offer. + env(offer(bob, USD(50), XRP(50))); + env.close(); + + // Verify that the second offer alice created was consumed. + { + auto offers = sortedOffersOnAccount(env, alice); + BEAST_EXPECT(offers.size() == 2); + BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId_3); + BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerId_2); + } + + // Cross alice's third offer. + env(offer(bob, USD(50), XRP(50))); + env.close(); + + // Verify that the third offer alice created was consumed. + { + auto offers = sortedOffersOnAccount(env, alice); + BEAST_EXPECT(offers.size() == 1); + BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerId_3); + } + + // Cross alice's last offer. + env(offer(bob, USD(50), XRP(50))); + env.close(); + + // Verify that the third offer alice created was consumed. + { + auto offers = sortedOffersOnAccount(env, alice); + BEAST_EXPECT(offers.size() == 0); + } + env.require(balance(alice, USD(0))); + env.require(owners(alice, 1)); + env.require(balance(bob, USD(200))); + env.require(owners(bob, 1)); + } + + void + testTicketCancelOffer(FeatureBitset features) + { + testcase("Ticket Cancel Offers"); + + using namespace jtx; + + // Should be called with TicketBatch enabled. + BEAST_EXPECT(features[featureTicketBatch]); + + // Verify that offers created with or without tickets can be canceled + // by transactions with or without tickets. + Env env{*this, features}; + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + auto const USD = gw["USD"]; + + env.fund(XRP(10000), gw, alice); + env.close(); + + env(trust(alice, USD(1000))); + env.close(); + env.require(owners(alice, 1), tickets(alice, 0)); + + env(pay(gw, alice, USD(200))); + env.close(); + + // Create the first of four offers using a sequence. + std::uint32_t const offerSeqId_0{env.seq(alice)}; + env(offer(alice, XRP(50), USD(50))); + env.close(); + env.require(owners(alice, 2), tickets(alice, 0)); + + // Create four tickets. + std::uint32_t const ticketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 4)); + env.close(); + env.require(owners(alice, 6), tickets(alice, 4)); + + // Create the second (also sequence-based) offer. + std::uint32_t const offerSeqId_1{env.seq(alice)}; + BEAST_EXPECT(offerSeqId_1 == offerSeqId_0 + 6); + env(offer(alice, XRP(50), USD(50))); + env.close(); + + // Create the third (ticket-based) offer. + std::uint32_t const offerTixId_0{ticketSeq + 1}; + env(offer(alice, XRP(50), USD(50)), ticket::use(offerTixId_0)); + env.close(); + + // Create the last offer. + std::uint32_t const offerTixId_1{ticketSeq}; + env(offer(alice, XRP(50), USD(50)), ticket::use(offerTixId_1)); + env.close(); + + // Verify that all of alice's offers are present. + { + auto offers = sortedOffersOnAccount(env, alice); + BEAST_EXPECT(offers.size() == 4); + BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerSeqId_0); + BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerTixId_1); + BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerTixId_0); + BEAST_EXPECT(offers[3]->getFieldU32(sfSequence) == offerSeqId_1); + env.require(balance(alice, USD(200))); + env.require(owners(alice, 7)); + } + + // Use a ticket to cancel an offer created with a sequence. + env(offer_cancel(alice, offerSeqId_0), ticket::use(ticketSeq + 2)); + env.close(); + + // Verify that offerSeqId_0 was canceled. + { + auto offers = sortedOffersOnAccount(env, alice); + BEAST_EXPECT(offers.size() == 3); + BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerTixId_1); + BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerTixId_0); + BEAST_EXPECT(offers[2]->getFieldU32(sfSequence) == offerSeqId_1); + } + + // Use a ticket to cancel an offer created with a ticket. + env(offer_cancel(alice, offerTixId_0), ticket::use(ticketSeq + 3)); + env.close(); + + // Verify that offerTixId_0 was canceled. + { + auto offers = sortedOffersOnAccount(env, alice); + BEAST_EXPECT(offers.size() == 2); + BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerTixId_1); + BEAST_EXPECT(offers[1]->getFieldU32(sfSequence) == offerSeqId_1); + } + + // All of alice's tickets should now be used up. + env.require(owners(alice, 3), tickets(alice, 0)); + + // Use a sequence to cancel an offer created with a ticket. + env(offer_cancel(alice, offerTixId_1)); + env.close(); + + // Verify that offerTixId_1 was canceled. + { + auto offers = sortedOffersOnAccount(env, alice); + BEAST_EXPECT(offers.size() == 1); + BEAST_EXPECT(offers[0]->getFieldU32(sfSequence) == offerSeqId_1); + } + + // Use a sequence to cancel an offer created with a sequence. + env(offer_cancel(alice, offerSeqId_1)); + env.close(); + + // Verify that offerSeqId_1 was canceled. + // All of alice's tickets should now be used up. + env.require(owners(alice, 1), tickets(alice, 0), offers(alice, 0)); + } + void testFalseAssert() { @@ -5209,13 +5463,15 @@ public: testSelfAuth(features); testDeletedOfferIssuer(features); testTickSize(features); + testTicketOffer(features); + testTicketCancelOffer(features); } void run() override { using namespace jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{supported_amendments() | featureTicketBatch}; FeatureBitset const flowCross{featureFlowCross}; FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval}; @@ -5233,7 +5489,7 @@ class Offer_manual_test : public Offer_test run() override { using namespace jtx; - FeatureBitset const all{supported_amendments()}; + FeatureBitset const all{supported_amendments() | featureTicketBatch}; FeatureBitset const flowCross{featureFlowCross}; FeatureBitset const f1513{fix1513}; FeatureBitset const takerDryOffer{fixTakerDryOfferRemoval}; diff --git a/src/test/app/PayChan_test.cpp b/src/test/app/PayChan_test.cpp index f2281181a..602063017 100644 --- a/src/test/app/PayChan_test.cpp +++ b/src/test/app/PayChan_test.cpp @@ -34,14 +34,11 @@ struct PayChan_test : public beast::unit_test::suite { static uint256 channel( - ReadView const& view, jtx::Account const& account, - jtx::Account const& dst) + jtx::Account const& dst, + std::uint32_t seqProxyValue) { - auto const sle = view.read(keylet::account(account)); - if (!sle) - return beast::zero; - auto const k = keylet::payChan(account, dst, (*sle)[sfSequence] - 1); + auto const k = keylet::payChan(account, dst, seqProxyValue); return k.key; } @@ -190,8 +187,8 @@ struct PayChan_test : public beast::unit_test::suite env.fund(XRP(10000), alice, bob); auto const pk = alice.pk(); auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), settleDelay, pk)); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); @@ -224,7 +221,11 @@ struct PayChan_test : public beast::unit_test::suite env(create(alice, alice, XRP(1000), settleDelay, pk), ter(temDST_IS_SRC)); // invalid channel - env(fund(alice, channel(*env.current(), alice, "noAccount"), XRP(1000)), + + env(fund( + alice, + channel(alice, "noAccount", env.seq(alice) - 1), + XRP(1000)), ter(tecNO_ENTRY)); // not enough funds env(create(alice, bob, XRP(10000), settleDelay, pk), ter(tecUNFUNDED)); @@ -366,13 +367,8 @@ struct PayChan_test : public beast::unit_test::suite NetClock::time_point const cancelAfter = env.current()->info().parentCloseTime + 3600s; auto const channelFunds = XRP(1000); + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); - auto const chan = channel(*env.current(), alice, bob); - if (!chan) - { - fail(); - return; - } BEAST_EXPECT(channelExists(*env.current(), chan)); env.close(cancelAfter); { @@ -403,8 +399,8 @@ struct PayChan_test : public beast::unit_test::suite NetClock::time_point const cancelAfter = env.current()->info().parentCloseTime + 3600s; auto const channelFunds = XRP(1000); + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(channelExists(*env.current(), chan)); // third party close before cancelAfter env(claim(carol, chan), txflags(tfClose), ter(tecNO_PERMISSION)); @@ -435,8 +431,8 @@ struct PayChan_test : public beast::unit_test::suite auto const minExpiration = closeTime + settleDelay; NetClock::time_point const cancelAfter = closeTime + 7200s; auto const channelFunds = XRP(1000); + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk, cancelAfter)); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(channelExists(*env.current(), chan)); BEAST_EXPECT(!channelExpiration(*env.current(), chan)); // Owner closes, will close after settleDelay @@ -499,8 +495,8 @@ struct PayChan_test : public beast::unit_test::suite NetClock::time_point const settleTimepoint = env.current()->info().parentCloseTime + settleDelay; auto const channelFunds = XRP(1000); + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk)); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(channelExists(*env.current(), chan)); // Owner closes, will close after settleDelay env(claim(alice, chan), txflags(tfClose)); @@ -557,8 +553,8 @@ struct PayChan_test : public beast::unit_test::suite auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk)); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(channelExists(*env.current(), chan)); // Owner tries to close channel, but it will remain open (settle delay) env(claim(alice, chan), txflags(tfClose)); @@ -592,8 +588,8 @@ struct PayChan_test : public beast::unit_test::suite auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk)); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(channelExists(*env.current(), chan)); // Owner tries to close channel, but it will remain open (settle delay) env(claim(alice, chan), txflags(tfClose)); @@ -648,9 +644,9 @@ struct PayChan_test : public beast::unit_test::suite Env env(*this, supported_amendments() - featureDepositAuth); env.fund(XRP(10000), alice, bob); env(fset(bob, asfDisallowXRP)); + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), 3600s, alice.pk()), ter(tecNO_TARGET)); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(!channelExists(*env.current(), chan)); } { @@ -659,8 +655,8 @@ struct PayChan_test : public beast::unit_test::suite Env env(*this, supported_amendments()); env.fund(XRP(10000), alice, bob); env(fset(bob, asfDisallowXRP)); + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), 3600s, alice.pk())); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(channelExists(*env.current(), chan)); } @@ -669,8 +665,8 @@ struct PayChan_test : public beast::unit_test::suite // (channel is created before disallow xrp is set) Env env(*this, supported_amendments() - featureDepositAuth); env.fund(XRP(10000), alice, bob); + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), 3600s, alice.pk())); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(channelExists(*env.current(), chan)); env(fset(bob, asfDisallowXRP)); @@ -683,8 +679,8 @@ struct PayChan_test : public beast::unit_test::suite // since it is just advisory. Env env(*this, supported_amendments()); env.fund(XRP(10000), alice, bob); + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), 3600s, alice.pk())); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(channelExists(*env.current(), chan)); env(fset(bob, asfDisallowXRP)); @@ -709,13 +705,18 @@ struct PayChan_test : public beast::unit_test::suite auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); - env(create(alice, bob, channelFunds, settleDelay, pk), - ter(tecDST_TAG_NEEDED)); - BEAST_EXPECT(!channelExists( - *env.current(), channel(*env.current(), alice, bob))); - env(create(alice, bob, channelFunds, settleDelay, pk, boost::none, 1)); - BEAST_EXPECT( - channelExists(*env.current(), channel(*env.current(), alice, bob))); + { + auto const chan = channel(alice, bob, env.seq(alice)); + env(create(alice, bob, channelFunds, settleDelay, pk), + ter(tecDST_TAG_NEEDED)); + BEAST_EXPECT(!channelExists(*env.current(), chan)); + } + { + auto const chan = channel(alice, bob, env.seq(alice)); + env(create( + alice, bob, channelFunds, settleDelay, pk, boost::none, 1)); + BEAST_EXPECT(channelExists(*env.current(), chan)); + } } void @@ -738,10 +739,10 @@ struct PayChan_test : public beast::unit_test::suite auto const pk = alice.pk(); auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); @@ -856,11 +857,11 @@ struct PayChan_test : public beast::unit_test::suite auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); + auto const chan1 = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk)); - auto const chan1 = channel(*env.current(), alice, bob); BEAST_EXPECT(channelExists(*env.current(), chan1)); + auto const chan2 = channel(alice, bob, env.seq(alice)); env(create(alice, bob, channelFunds, settleDelay, pk)); - auto const chan2 = channel(*env.current(), alice, bob); BEAST_EXPECT(channelExists(*env.current(), chan2)); BEAST_EXPECT(chan1 != chan2); } @@ -880,9 +881,9 @@ struct PayChan_test : public beast::unit_test::suite auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); + auto const chan1Str = to_string(channel(alice, bob, env.seq(alice))); env(create(alice, bob, channelFunds, settleDelay, pk)); env.close(); - auto const chan1Str = to_string(channel(*env.current(), alice, bob)); { auto const r = env.rpc("account_channels", alice.human(), bob.human()); @@ -904,9 +905,9 @@ struct PayChan_test : public beast::unit_test::suite BEAST_EXPECT(r[jss::result][jss::channels].size() == 0); BEAST_EXPECT(r[jss::result][jss::validated]); } + auto const chan2Str = to_string(channel(alice, bob, env.seq(alice))); env(create(alice, bob, channelFunds, settleDelay, pk)); env.close(); - auto const chan2Str = to_string(channel(*env.current(), alice, bob)); { auto const r = env.rpc("account_channels", alice.human(), bob.human()); @@ -1088,9 +1089,9 @@ struct PayChan_test : public beast::unit_test::suite auto const pk = alice.pk(); auto const settleDelay = 3600s; auto const channelFunds = XRP(1000); + auto const chan1Str = to_string(channel(alice, bob, env.seq(alice))); env(create(alice, bob, channelFunds, settleDelay, pk)); env.close(); - auto const chan1Str = to_string(channel(*env.current(), alice, bob)); std::string chan1PkStr; { auto const r = @@ -1117,9 +1118,9 @@ struct PayChan_test : public beast::unit_test::suite BEAST_EXPECT(r[jss::result][jss::channels].size() == 0); BEAST_EXPECT(r[jss::result][jss::validated]); } + auto const chan2Str = to_string(channel(alice, bob, env.seq(alice))); env(create(alice, bob, channelFunds, settleDelay, pk)); env.close(); - auto const chan2Str = to_string(channel(*env.current(), alice, bob)); { auto const r = env.rpc("account_channels", alice.human(), bob.human()); @@ -1254,13 +1255,12 @@ struct PayChan_test : public beast::unit_test::suite } { // Try to explicitly specify secp256k1 and Ed25519 keys: + auto const chan = + to_string(channel(charlie, alice, env.seq(charlie))); env(create( charlie, alice, channelFunds, settleDelay, charlie.pk())); env.close(); - auto const chan = - to_string(channel(*env.current(), charlie, alice)); - std::string cpk; { auto const r = @@ -1434,8 +1434,8 @@ struct PayChan_test : public beast::unit_test::suite boost::optional cancelAfter; { + auto const chan = to_string(channel(alice, bob, env.seq(alice))); env(create(alice, bob, channelFunds, settleDelay, pk)); - auto const chan = to_string(channel(*env.current(), alice, bob)); auto const r = env.rpc("account_channels", alice.human(), bob.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); @@ -1446,6 +1446,7 @@ struct PayChan_test : public beast::unit_test::suite } { std::uint32_t dstTag = 42; + auto const chan = to_string(channel(alice, carol, env.seq(alice))); env(create( alice, carol, @@ -1454,7 +1455,6 @@ struct PayChan_test : public beast::unit_test::suite pk, cancelAfter, dstTag)); - auto const chan = to_string(channel(*env.current(), alice, carol)); auto const r = env.rpc("account_channels", alice.human(), carol.human()); BEAST_EXPECT(r[jss::result][jss::channels].size() == 1); @@ -1480,6 +1480,7 @@ struct PayChan_test : public beast::unit_test::suite auto const pk = alice.pk(); auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); auto jv = create(alice, bob, XRP(1000), settleDelay, pk); auto const pkHex = strHex(pk.slice()); jv["PublicKey"] = pkHex.substr(2, pkHex.size() - 2); @@ -1494,7 +1495,6 @@ struct PayChan_test : public beast::unit_test::suite jv["PublicKey"] = pkHex; env(jv); - auto const chan = channel(*env.current(), alice, bob); auto const authAmt = XRP(100); auto const sig = signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); @@ -1689,9 +1689,9 @@ struct PayChan_test : public beast::unit_test::suite // Create a channel from alice to bob auto const pk = alice.pk(); auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); @@ -1781,9 +1781,9 @@ struct PayChan_test : public beast::unit_test::suite // Create a channel from alice to bob auto const pk = alice.pk(); auto const settleDelay = 100s; + auto const chan = channel(alice, bob, env.seq(alice)); env(create(alice, bob, XRP(1000), settleDelay, pk)); env.close(); - auto const chan = channel(*env.current(), alice, bob); BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); @@ -1878,6 +1878,168 @@ struct PayChan_test : public beast::unit_test::suite } } + void + testUsingTickets() + { + testcase("using tickets"); + using namespace jtx; + using namespace std::literals::chrono_literals; + Env env(*this, supported_amendments() | featureTicketBatch); + auto const alice = Account("alice"); + auto const bob = Account("bob"); + auto USDA = alice["USD"]; + env.fund(XRP(10000), alice, bob); + + // alice and bob grab enough tickets for all of the following + // transactions. Note that once the tickets are acquired alice's + // and bob's account sequence numbers should not advance. + std::uint32_t aliceTicketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 10)); + std::uint32_t const aliceSeq{env.seq(alice)}; + + std::uint32_t bobTicketSeq{env.seq(bob) + 1}; + env(ticket::create(bob, 10)); + std::uint32_t const bobSeq{env.seq(bob)}; + + auto const pk = alice.pk(); + auto const settleDelay = 100s; + auto const chan = channel(alice, bob, aliceTicketSeq); + + env(create(alice, bob, XRP(1000), settleDelay, pk), + ticket::use(aliceTicketSeq++)); + + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == XRP(0)); + BEAST_EXPECT(channelAmount(*env.current(), chan) == XRP(1000)); + + { + auto const preAlice = env.balance(alice); + env(fund(alice, chan, XRP(1000)), ticket::use(aliceTicketSeq++)); + + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + + auto const feeDrops = env.current()->fees().base; + BEAST_EXPECT(env.balance(alice) == preAlice - XRP(1000) - feeDrops); + } + + auto chanBal = channelBalance(*env.current(), chan); + auto chanAmt = channelAmount(*env.current(), chan); + BEAST_EXPECT(chanBal == XRP(0)); + BEAST_EXPECT(chanAmt == XRP(2000)); + + { + // No signature needed since the owner is claiming + auto const preBob = env.balance(bob); + auto const delta = XRP(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + XRP(100); + assert(reqBal <= chanAmt); + env(claim(alice, chan, reqBal, authAmt), + ticket::use(aliceTicketSeq++)); + + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob) == preBob + delta); + chanBal = reqBal; + } + { + // Claim with signature + auto preBob = env.balance(bob); + auto const delta = XRP(500); + auto const reqBal = chanBal + delta; + auto const authAmt = reqBal + XRP(100); + assert(reqBal <= chanAmt); + auto const sig = + signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()), + ticket::use(bobTicketSeq++)); + + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == reqBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + auto const feeDrops = env.current()->fees().base; + BEAST_EXPECT(env.balance(bob) == preBob + delta - feeDrops); + chanBal = reqBal; + + // claim again + preBob = env.balance(bob); + // A transaction that generates a tec still consumes its ticket. + env(claim(bob, chan, reqBal, authAmt, Slice(sig), alice.pk()), + ticket::use(bobTicketSeq++), + ter(tecUNFUNDED_PAYMENT)); + + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob) == preBob - feeDrops); + } + { + // Try to claim more than authorized + auto const preBob = env.balance(bob); + STAmount const authAmt = chanBal + XRP(500); + STAmount const reqAmt = authAmt + drops(1); + assert(reqAmt <= chanAmt); + // Note that since claim() returns a tem (neither tec nor tes), + // the ticket is not consumed. So we don't increment bobTicket. + auto const sig = + signClaimAuth(alice.pk(), alice.sk(), chan, authAmt); + env(claim(bob, chan, reqAmt, authAmt, Slice(sig), alice.pk()), + ticket::use(bobTicketSeq), + ter(temBAD_AMOUNT)); + + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + BEAST_EXPECT(env.balance(bob) == preBob); + } + + // Dst tries to fund the channel + env(fund(bob, chan, XRP(1000)), + ticket::use(bobTicketSeq++), + ter(tecNO_PERMISSION)); + + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + BEAST_EXPECT(channelBalance(*env.current(), chan) == chanBal); + BEAST_EXPECT(channelAmount(*env.current(), chan) == chanAmt); + + { + // Dst closes channel + auto const preAlice = env.balance(alice); + auto const preBob = env.balance(bob); + env(claim(bob, chan), + txflags(tfClose), + ticket::use(bobTicketSeq++)); + + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + + BEAST_EXPECT(!channelExists(*env.current(), chan)); + auto const feeDrops = env.current()->fees().base; + auto const delta = chanAmt - chanBal; + assert(delta > beast::zero); + BEAST_EXPECT(env.balance(alice) == preAlice + delta); + BEAST_EXPECT(env.balance(bob) == preBob - feeDrops); + } + env.require(tickets(alice, env.seq(alice) - aliceTicketSeq)); + BEAST_EXPECT(env.seq(alice) == aliceSeq); + env.require(tickets(bob, env.seq(bob) - bobTicketSeq)); + BEAST_EXPECT(env.seq(bob) == bobSeq); + } + void run() override { @@ -1899,6 +2061,7 @@ struct PayChan_test : public beast::unit_test::suite testMalformedPK(); testMetaAndOwnership(); testAccountDelete(); + testUsingTickets(); } }; diff --git a/src/test/app/SetRegularKey_test.cpp b/src/test/app/SetRegularKey_test.cpp index 19fb1fcc0..effbaace0 100644 --- a/src/test/app/SetRegularKey_test.cpp +++ b/src/test/app/SetRegularKey_test.cpp @@ -198,6 +198,58 @@ public: env(jv, ter(temINVALID_FLAG)); } + void + testTicketRegularKey() + { + using namespace test::jtx; + + testcase("Ticket regular key"); + Env env{*this, supported_amendments() | featureTicketBatch}; + Account const alice{"alice", KeyType::ed25519}; + env.fund(XRP(1000), alice); + env.close(); + + // alice makes herself some tickets. + env(ticket::create(alice, 4)); + env.close(); + std::uint32_t ticketSeq{env.seq(alice)}; + + // Make sure we can give a regular key using a ticket. + Account const alie{"alie", KeyType::secp256k1}; + env(regkey(alice, alie), ticket::use(--ticketSeq)); + env.close(); + + // Disable alice's master key using a ticket. + env(fset(alice, asfDisableMaster), + sig(alice), + ticket::use(--ticketSeq)); + env.close(); + + // alice should be able to sign using the regular key but not the + // master key. + std::uint32_t const aliceSeq{env.seq(alice)}; + env(noop(alice), sig(alice), ter(tefMASTER_DISABLED)); + env(noop(alice), sig(alie), ter(tesSUCCESS)); + env.close(); + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + + // Re-enable the master key using a ticket. + env(fclear(alice, asfDisableMaster), + sig(alie), + ticket::use(--ticketSeq)); + env.close(); + + // Disable the regular key using a ticket. + env(regkey(alice, disabled), sig(alie), ticket::use(--ticketSeq)); + env.close(); + + // alice should be able to sign using the master key but not the + // regular key. + env(noop(alice), sig(alice), ter(tesSUCCESS)); + env(noop(alice), sig(alie), ter(tefBAD_AUTH)); + env.close(); + } + void run() override { @@ -207,6 +259,7 @@ public: testDisableRegularKeyAfterFix(); testPasswordSpent(); testUniversalMask(); + testTicketRegularKey(); } }; diff --git a/src/test/app/SetTrust_test.cpp b/src/test/app/SetTrust_test.cpp index 28beab308..7c6d0fccd 100644 --- a/src/test/app/SetTrust_test.cpp +++ b/src/test/app/SetTrust_test.cpp @@ -106,6 +106,40 @@ public: } } + void + testTicketSetTrust() + { + testcase("SetTrust using a ticket"); + + using namespace jtx; + + // Verify that TrustSet transactions can use tickets. + Env env{*this, supported_amendments() | featureTicketBatch}; + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + auto const USD = gw["USD"]; + + env.fund(XRP(10000), gw, alice); + env.close(); + + // Cannot pay alice without a trustline. + env(pay(gw, alice, USD(200)), ter(tecPATH_DRY)); + env.close(); + + // Create a ticket. + std::uint32_t const ticketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 1)); + env.close(); + + // Use that ticket to create a trust line. + env(trust(alice, USD(1000)), ticket::use(ticketSeq)); + env.close(); + + // Now the payment succeeds. + env(pay(gw, alice, USD(200))); + env.close(); + } + Json::Value trust_explicit_amt(jtx::Account const& a, STAmount const& amt) { @@ -223,6 +257,7 @@ public: // true, true case doesn't matter since creating a trustline ledger // entry requires reserve from the creator // independent of hi/low account ids for endpoints + testTicketSetTrust(); testMalformedTransaction(); testModifyQualityOfTrustline(false, false); testModifyQualityOfTrustline(false, true); diff --git a/src/test/app/Ticket_test.cpp b/src/test/app/Ticket_test.cpp index 936c54d8c..a311bb305 100644 --- a/src/test/app/Ticket_test.cpp +++ b/src/test/app/Ticket_test.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -25,83 +26,355 @@ namespace ripple { class Ticket_test : public beast::unit_test::suite { - static auto constexpr idOne = - "00000000000000000000000000000000" - "00000000000000000000000000000001"; - - /// @brief validate metadata for a create/cancel ticket transaction and - /// return the 3 or 4 nodes that make-up the metadata (AffectedNodes) + /// @brief Validate metadata for a successful CreateTicket transaction. /// - /// @param env current jtx env (meta will be extracted from it) - /// - /// @param other_target flag to indicate whether a Target different - /// from the Account was specified for the ticket (when created) - /// - /// @param expiration flag to indicate a cancellation with expiration which - /// causes two of the affected nodes to be swapped (in order). - /// - /// @retval std::array size 4 of json object values representing - /// each meta node entry. When the transaction was a cancel with differing - /// target and account, there will be 4 complete items, otherwise the last - /// entry will be an empty object - auto - checkTicketMeta( - test::jtx::Env& env, - bool other_target = false, - bool expiration = false) + /// @param env current jtx env (tx and meta are extracted using it) + void + checkTicketCreateMeta(test::jtx::Env& env) { using namespace std::string_literals; - auto const& tx = env.tx()->getJson(JsonOptions::none); - bool is_cancel = tx[jss::TransactionType] == jss::TicketCancel; - auto const& jvm = env.meta()->getJson(JsonOptions::none); - std::array retval; - - // these are the affected nodes that we expect for - // a few different scenarios. - // tuple is index, field name, and label (LedgerEntryType) - std::vector> - expected_nodes; - - if (is_cancel && other_target) + Json::Value const& tx{env.tx()->getJson(JsonOptions::none)}; { - expected_nodes = { - {0, sfModifiedNode.fieldName, jss::AccountRoot}, - {expiration ? 2 : 1, - sfModifiedNode.fieldName, - jss::AccountRoot}, - {expiration ? 1 : 2, sfDeletedNode.fieldName, jss::Ticket}, - {3, sfDeletedNode.fieldName, jss::DirectoryNode}}; - } - else - { - expected_nodes = { - {0, sfModifiedNode.fieldName, jss::AccountRoot}, - {1, - is_cancel ? sfDeletedNode.fieldName : sfCreatedNode.fieldName, - jss::Ticket}, - {2, - is_cancel ? sfDeletedNode.fieldName : sfCreatedNode.fieldName, - jss::DirectoryNode}}; + std::string const txType = + tx[sfTransactionType.jsonName].asString(); + + if (!BEAST_EXPECTS( + txType == jss::TicketCreate, + "Unexpected TransactionType: "s + txType)) + return; } - BEAST_EXPECT(jvm.isMember(sfAffectedNodes.fieldName)); - BEAST_EXPECT(jvm[sfAffectedNodes.fieldName].isArray()); + std::uint32_t const count = {tx[sfTicketCount.jsonName].asUInt()}; + if (!BEAST_EXPECTS( + count >= 1, + "Unexpected ticket count: "s + std::to_string(count))) + return; + + std::uint32_t const txSeq = {tx[sfSequence.jsonName].asUInt()}; + std::string const account = tx[sfAccount.jsonName].asString(); + + Json::Value const& metadata = env.meta()->getJson(JsonOptions::none); + if (!BEAST_EXPECTS( + metadata.isMember(sfTransactionResult.jsonName) && + metadata[sfTransactionResult.jsonName].asString() == + "tesSUCCESS", + "Not metadata for successful TicketCreate.")) + return; + + BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName)); + BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].isArray()); + + bool directoryChanged = false; + std::uint32_t acctRootFinalSeq = {0}; + std::vector ticketSeqs; + ticketSeqs.reserve(count); + for (Json::Value const& node : metadata[sfAffectedNodes.jsonName]) + { + if (node.isMember(sfModifiedNode.jsonName)) + { + Json::Value const& modified = node[sfModifiedNode.jsonName]; + std::string const entryType = + modified[sfLedgerEntryType.jsonName].asString(); + if (entryType == jss::AccountRoot) + { + auto const& previousFields = + modified[sfPreviousFields.jsonName]; + auto const& finalFields = modified[sfFinalFields.jsonName]; + { + // Verify the account root Sequence did the right thing. + std::uint32_t const prevSeq = + previousFields[sfSequence.jsonName].asUInt(); + + acctRootFinalSeq = + finalFields[sfSequence.jsonName].asUInt(); + + if (txSeq == 0) + { + // Transaction used a TicketSequence. + BEAST_EXPECT(acctRootFinalSeq == prevSeq + count); + } + else + { + // Transaction used a (plain) Sequence. + BEAST_EXPECT(prevSeq == txSeq); + BEAST_EXPECT( + acctRootFinalSeq == prevSeq + count + 1); + } + } + + std::uint32_t const consumedTickets = { + txSeq == 0u ? 1u : 0u}; + + // If... + // 1. The TicketCount is 1 and + // 2. A ticket was consumed by the ticket create, then + // 3. The final TicketCount did not change, so the + // previous TicketCount is not reported. + // But, since the count did not change, we know it equals + // the final Ticket count. + bool const unreportedPrevTicketCount = { + count == 1 && txSeq == 0}; + + // Verify the OwnerCount did the right thing + if (unreportedPrevTicketCount) + { + // The number of Tickets should not have changed, so + // the previous OwnerCount should not be reported. + BEAST_EXPECT( + !previousFields.isMember(sfOwnerCount.jsonName)); + } + else + { + // Verify the OwnerCount did the right thing. + std::uint32_t const prevCount = { + previousFields[sfOwnerCount.jsonName].asUInt()}; + + std::uint32_t const finalCount = { + finalFields[sfOwnerCount.jsonName].asUInt()}; + + BEAST_EXPECT( + prevCount + count - consumedTickets == finalCount); + } + + // Verify TicketCount metadata. + BEAST_EXPECT(finalFields.isMember(sfTicketCount.jsonName)); + + if (unreportedPrevTicketCount) + { + // The number of Tickets should not have changed, so + // the previous TicketCount should not be reported. + BEAST_EXPECT( + !previousFields.isMember(sfTicketCount.jsonName)); + } + else + { + // If the TicketCount was previously present it + // should have been greater than zero. + std::uint32_t const startCount = { + previousFields.isMember(sfTicketCount.jsonName) + ? previousFields[sfTicketCount.jsonName] + .asUInt() + : 0u}; + + BEAST_EXPECT( + (startCount == 0u) ^ + previousFields.isMember(sfTicketCount.jsonName)); + + BEAST_EXPECT( + startCount + count - consumedTickets == + finalFields[sfTicketCount.jsonName]); + } + } + else if (entryType == jss::DirectoryNode) + { + directoryChanged = true; + } + else + { + fail( + "Unexpected modified node: "s + entryType, + __FILE__, + __LINE__); + } + } + else if (node.isMember(sfCreatedNode.jsonName)) + { + Json::Value const& created = node[sfCreatedNode.jsonName]; + std::string const entryType = + created[sfLedgerEntryType.jsonName].asString(); + if (entryType == jss::Ticket) + { + auto const& newFields = created[sfNewFields.jsonName]; + + BEAST_EXPECT( + newFields[sfAccount.jsonName].asString() == account); + ticketSeqs.push_back( + newFields[sfTicketSequence.jsonName].asUInt()); + } + else if (entryType == jss::DirectoryNode) + { + directoryChanged = true; + } + else + { + fail( + "Unexpected created node: "s + entryType, + __FILE__, + __LINE__); + } + } + else if (node.isMember(sfDeletedNode.jsonName)) + { + Json::Value const& deleted = node[sfDeletedNode.jsonName]; + std::string const entryType = + deleted[sfLedgerEntryType.jsonName].asString(); + + if (entryType == jss::Ticket) + { + // Verify the transaction's Sequence == 0. + BEAST_EXPECT(txSeq == 0); + + // Verify the account of the deleted ticket. + auto const& finalFields = deleted[sfFinalFields.jsonName]; + BEAST_EXPECT( + finalFields[sfAccount.jsonName].asString() == account); + + // Verify the deleted ticket has the right TicketSequence. + BEAST_EXPECT( + finalFields[sfTicketSequence.jsonName].asUInt() == + tx[sfTicketSequence.jsonName].asUInt()); + } + } + else + { + fail( + "Unexpected node type in TicketCreate metadata.", + __FILE__, + __LINE__); + } + } + BEAST_EXPECT(directoryChanged); + + // Verify that all the expected Tickets were created. + BEAST_EXPECT(ticketSeqs.size() == count); + std::sort(ticketSeqs.begin(), ticketSeqs.end()); BEAST_EXPECT( - jvm[sfAffectedNodes.fieldName].size() == expected_nodes.size()); + std::adjacent_find(ticketSeqs.begin(), ticketSeqs.end()) == + ticketSeqs.end()); + BEAST_EXPECT(*ticketSeqs.rbegin() == acctRootFinalSeq - 1); + } + + /// @brief Validate metadata for a ticket using transaction. + /// + /// The transaction may have been successful or failed with a tec. + /// + /// @param env current jtx env (tx and meta are extracted using it) + void + checkTicketConsumeMeta(test::jtx::Env& env) + { + Json::Value const& tx{env.tx()->getJson(JsonOptions::none)}; + + // Verify that the transaction includes a TicketSequence. + + // Capture that TicketSequence. + // Capture the Account from the transaction + + // Verify that metadata indicates a tec or a tesSUCCESS. + + // Walk affected nodes: + // + // For each deleted node, see if it is a Ticket node. If it is + // a Ticket Node being deleted, then assert that the... + // + // Account == the transaction Account && + // TicketSequence == the transaction TicketSequence + // + // If a modified node is an AccountRoot, see if it is the transaction + // Account. If it is then verify the TicketCount decreased by one. + // If the old TicketCount was 1, then the TicketCount field should be + // removed from the final fields of the AccountRoot. + // + // After looking at all nodes verify that exactly one Ticket node + // was deleted. + BEAST_EXPECT(tx[sfSequence.jsonName].asUInt() == 0); + std::string const account{tx[sfAccount.jsonName].asString()}; + if (!BEAST_EXPECTS( + tx.isMember(sfTicketSequence.jsonName), + "Not metadata for a ticket consuming transaction.")) + return; + + std::uint32_t const ticketSeq{tx[sfTicketSequence.jsonName].asUInt()}; + + Json::Value const& metadata{env.meta()->getJson(JsonOptions::none)}; + if (!BEAST_EXPECTS( + metadata.isMember(sfTransactionResult.jsonName), + "Metadata is missing TransactionResult.")) + return; - // verify the actual metadata against the expected - for (auto const& it : expected_nodes) { - auto const& idx = std::get<0>(it); - auto const& field = std::get<1>(it); - auto const& type = std::get<2>(it); - BEAST_EXPECT(jvm[sfAffectedNodes.fieldName][idx].isMember(field)); - retval[idx] = jvm[sfAffectedNodes.fieldName][idx][field]; - BEAST_EXPECT(retval[idx][sfLedgerEntryType.fieldName] == type); + std::string const transactionResult{ + metadata[sfTransactionResult.jsonName].asString()}; + if (!BEAST_EXPECTS( + transactionResult == "tesSUCCESS" || + transactionResult.compare(0, 3, "tec") == 0, + transactionResult + " neither tesSUCCESS nor tec")) + return; } - return retval; + BEAST_EXPECT(metadata.isMember(sfAffectedNodes.jsonName)); + BEAST_EXPECT(metadata[sfAffectedNodes.jsonName].isArray()); + + bool acctRootFound{false}; + std::uint32_t acctRootSeq{0}; + int ticketsRemoved{0}; + for (Json::Value const& node : metadata[sfAffectedNodes.jsonName]) + { + if (node.isMember(sfModifiedNode.jsonName)) + { + Json::Value const& modified{node[sfModifiedNode.jsonName]}; + std::string const entryType = + modified[sfLedgerEntryType.jsonName].asString(); + if (entryType == "AccountRoot" && + modified[sfFinalFields.jsonName][sfAccount.jsonName] + .asString() == account) + { + acctRootFound = true; + + auto const& previousFields = + modified[sfPreviousFields.jsonName]; + auto const& finalFields = modified[sfFinalFields.jsonName]; + + acctRootSeq = finalFields[sfSequence.jsonName].asUInt(); + + // Check that the TicketCount was present and decremented + // by 1. If it decremented to zero, then the field should + // be gone. + if (!BEAST_EXPECTS( + previousFields.isMember(sfTicketCount.jsonName), + "AccountRoot previous is missing TicketCount")) + return; + + std::uint32_t const prevTicketCount = + previousFields[sfTicketCount.jsonName].asUInt(); + + BEAST_EXPECT(prevTicketCount > 0); + if (prevTicketCount == 1) + BEAST_EXPECT( + !finalFields.isMember(sfTicketCount.jsonName)); + else + BEAST_EXPECT( + finalFields.isMember(sfTicketCount.jsonName) && + finalFields[sfTicketCount.jsonName].asUInt() == + prevTicketCount - 1); + } + } + else if (node.isMember(sfDeletedNode.jsonName)) + { + Json::Value const& deleted{node[sfDeletedNode.jsonName]}; + std::string const entryType{ + deleted[sfLedgerEntryType.jsonName].asString()}; + + if (entryType == jss::Ticket) + { + // Verify the account of the deleted ticket. + BEAST_EXPECT( + deleted[sfFinalFields.jsonName][sfAccount.jsonName] + .asString() == account); + + // Verify the deleted ticket has the right TicketSequence. + BEAST_EXPECT( + deleted[sfFinalFields.jsonName] + [sfTicketSequence.jsonName] + .asUInt() == ticketSeq); + + ++ticketsRemoved; + } + } + } + BEAST_EXPECT(acctRootFound); + BEAST_EXPECT(ticketsRemoved == 1); + BEAST_EXPECT(ticketSeq < acctRootSeq); } void @@ -110,108 +383,179 @@ class Ticket_test : public beast::unit_test::suite testcase("Feature Not Enabled"); using namespace test::jtx; - Env env{*this, FeatureBitset{}}; + Env env{*this, supported_amendments() - featureTicketBatch}; - env(ticket::create(env.master), ter(temDISABLED)); - env(ticket::cancel(env.master, idOne), ter(temDISABLED)); - } + env(ticket::create(env.master, 1), ter(temDISABLED)); + env.close(); + env.require(owners(env.master, 0), tickets(env.master, 0)); - void - testTicketCancelNonexistent() - { - testcase("Cancel Nonexistent"); + env(noop(env.master), ticket::use(1), ter(temMALFORMED)); - using namespace test::jtx; - Env env{*this, supported_amendments().set(featureTickets)}; - env(ticket::cancel(env.master, idOne), ter(tecNO_ENTRY)); + // Close enough ledgers that the previous transactions are no + // longer retried. + for (int i = 0; i < 8; ++i) + env.close(); + + env.enableFeature(featureTicketBatch); + env.close(); + env.require(owners(env.master, 0), tickets(env.master, 0)); + + std::uint32_t ticketSeq{env.seq(env.master) + 1}; + env(ticket::create(env.master, 2)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(env.master, 2), tickets(env.master, 2)); + + env(noop(env.master), ticket::use(ticketSeq++)); + checkTicketConsumeMeta(env); + env.close(); + env.require(owners(env.master, 1), tickets(env.master, 1)); + + env(fset(env.master, asfDisableMaster), + ticket::use(ticketSeq++), + ter(tecNO_ALTERNATIVE_KEY)); + checkTicketConsumeMeta(env); + env.close(); + env.require(owners(env.master, 0), tickets(env.master, 0)); } void testTicketCreatePreflightFail() { - testcase("Create/Cancel Ticket with Bad Fee, Fail Preflight"); + testcase("Create Tickets that fail Preflight"); using namespace test::jtx; - Env env{*this, supported_amendments().set(featureTickets)}; + Env env{*this, supported_amendments() | featureTicketBatch}; - env(ticket::create(env.master), fee(XRP(-1)), ter(temBAD_FEE)); - env(ticket::cancel(env.master, idOne), fee(XRP(-1)), ter(temBAD_FEE)); + Account const master{env.master}; + + // Exercise boundaries on count. + env(ticket::create(master, 0), ter(temINVALID_COUNT)); + env(ticket::create(master, 251), ter(temINVALID_COUNT)); + + // Exercise fees. + std::uint32_t const ticketSeq_A{env.seq(master) + 1}; + env(ticket::create(master, 1), fee(XRP(10))); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(master, 1), tickets(master, 1)); + + env(ticket::create(master, 1), fee(XRP(-1)), ter(temBAD_FEE)); + + // Exercise flags. + std::uint32_t const ticketSeq_B{env.seq(master) + 1}; + env(ticket::create(master, 1), txflags(tfFullyCanonicalSig)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(master, 2), tickets(master, 2)); + + env(ticket::create(master, 1), txflags(tfSell), ter(temINVALID_FLAG)); + env.close(); + env.require(owners(master, 2), tickets(master, 2)); + + // We successfully created 1 ticket earlier. Verify that we can + // create 250 tickets in one shot. We must consume one ticket first. + env(noop(master), ticket::use(ticketSeq_A)); + checkTicketConsumeMeta(env); + env.close(); + env.require(owners(master, 1), tickets(master, 1)); + + env(ticket::create(master, 250), ticket::use(ticketSeq_B)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(master, 250), tickets(master, 250)); } void - testTicketCreateNonexistent() + testTicketCreatePreclaimFail() { - testcase("Create Tickets with Nonexistent Accounts"); + testcase("Create Tickets that fail Preclaim"); using namespace test::jtx; - Env env{*this, supported_amendments().set(featureTickets)}; - Account alice{"alice"}; - env.memoize(alice); + { + // Create tickets on a non-existent account. + Env env{*this, supported_amendments() | featureTicketBatch}; + Account alice{"alice"}; + env.memoize(alice); - env(ticket::create(env.master, alice), ter(tecNO_TARGET)); + env(ticket::create(alice, 1), + json(jss::Sequence, 1), + ter(terNO_ACCOUNT)); + } + { + // Exceed the threshold where tickets can no longer be + // added to an account. + Env env{*this, supported_amendments() | featureTicketBatch}; + Account alice{"alice"}; - env(ticket::create(alice, env.master), - json(jss::Sequence, 1), - ter(terNO_ACCOUNT)); - } + env.fund(XRP(100000), alice); - void - testTicketToSelf() - { - testcase("Create Tickets with Same Account and Target"); + std::uint32_t ticketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 250)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(alice, 250), tickets(alice, 250)); - using namespace test::jtx; - Env env{*this, supported_amendments().set(featureTickets)}; + // Note that we can add one more ticket while consuming a ticket + // because the final result is still 250 tickets. + env(ticket::create(alice, 1), ticket::use(ticketSeq + 0)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(alice, 250), tickets(alice, 250)); - env(ticket::create(env.master, env.master)); - auto cr = checkTicketMeta(env); - auto const& jticket = cr[1]; + // Adding one more ticket will exceed the threshold. + env(ticket::create(alice, 2), + ticket::use(ticketSeq + 1), + ter(tecDIR_FULL)); + env.close(); + env.require(owners(alice, 249), tickets(alice, 249)); - BEAST_EXPECT( - jticket[sfLedgerIndex.fieldName] == - "7F58A0AE17775BA3404D55D406DD1C2E91EADD7AF3F03A26877BCE764CCB75E3"); - BEAST_EXPECT( - jticket[sfNewFields.fieldName][jss::Account] == env.master.human()); - BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Sequence] == 1); - // verify that there's no `Target` saved in the ticket - BEAST_EXPECT( - !jticket[sfNewFields.fieldName].isMember(sfTarget.fieldName)); - } + // Now we can successfully add one more ticket. + env(ticket::create(alice, 2), ticket::use(ticketSeq + 2)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(alice, 250), tickets(alice, 250)); - void - testTicketCancelByCreator() - { - testcase("Create Ticket and Then Cancel by Creator"); + // Since we're at 250, we can't add another ticket using a + // sequence. + env(ticket::create(alice, 1), ter(tecDIR_FULL)); + env.close(); + env.require(owners(alice, 250), tickets(alice, 250)); + } + { + // Explore exceeding the ticket threshold from another angle. + Env env{*this, supported_amendments() | featureTicketBatch}; + Account alice{"alice"}; - using namespace test::jtx; - Env env{*this, supported_amendments().set(featureTickets)}; + env.fund(XRP(100000), alice); + env.close(); - // create and verify - env(ticket::create(env.master)); - auto cr = checkTicketMeta(env); - auto const& jacct = cr[0]; - auto const& jticket = cr[1]; - BEAST_EXPECT( - jacct[sfPreviousFields.fieldName][sfOwnerCount.fieldName] == 0); - BEAST_EXPECT( - jacct[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 1); - BEAST_EXPECT( - jticket[sfNewFields.fieldName][jss::Sequence] == - jacct[sfPreviousFields.fieldName][jss::Sequence]); - BEAST_EXPECT( - jticket[sfLedgerIndex.fieldName] == - "7F58A0AE17775BA3404D55D406DD1C2E91EADD7AF3F03A26877BCE764CCB75E3"); - BEAST_EXPECT( - jticket[sfNewFields.fieldName][jss::Account] == env.master.human()); + std::uint32_t ticketSeq_AB{env.seq(alice) + 1}; + env(ticket::create(alice, 2)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(alice, 2), tickets(alice, 2)); - // cancel - env(ticket::cancel( - env.master, jticket[sfLedgerIndex.fieldName].asString())); - auto crd = checkTicketMeta(env); - auto const& jacctd = crd[0]; - BEAST_EXPECT(jacctd[sfFinalFields.fieldName][jss::Sequence] == 3); - BEAST_EXPECT( - jacctd[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 0); + // Adding 250 tickets (while consuming one) will exceed the + // threshold. + env(ticket::create(alice, 250), + ticket::use(ticketSeq_AB + 0), + ter(tecDIR_FULL)); + env.close(); + env.require(owners(alice, 1), tickets(alice, 1)); + + // Adding 250 tickets (without consuming one) will exceed the + // threshold. + env(ticket::create(alice, 250), ter(tecDIR_FULL)); + env.close(); + env.require(owners(alice, 1), tickets(alice, 1)); + + // Alice can now add 250 tickets while consuming one. + env(ticket::create(alice, 250), ticket::use(ticketSeq_AB + 1)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(alice, 250), tickets(alice, 250)); + } } void @@ -220,179 +564,255 @@ class Ticket_test : public beast::unit_test::suite testcase("Create Ticket Insufficient Reserve"); using namespace test::jtx; - Env env{*this, supported_amendments().set(featureTickets)}; + Env env{*this, supported_amendments() | featureTicketBatch}; Account alice{"alice"}; - env.fund(env.current()->fees().accountReserve(0), alice); + // Fund alice not quite enough to make the reserve for a Ticket. + env.fund(env.current()->fees().accountReserve(1) - drops(1), alice); env.close(); - env(ticket::create(alice), ter(tecINSUFFICIENT_RESERVE)); + env(ticket::create(alice, 1), ter(tecINSUFFICIENT_RESERVE)); + env.close(); + env.require(owners(alice, 0), tickets(alice, 0)); + + // Give alice enough to exactly meet the reserve for one Ticket. + env( + pay(env.master, + alice, + env.current()->fees().accountReserve(1) - env.balance(alice))); + env.close(); + + env(ticket::create(alice, 1)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(alice, 1), tickets(alice, 1)); + + // Give alice not quite enough to make the reserve for a total of + // 250 Tickets. + env( + pay(env.master, + alice, + env.current()->fees().accountReserve(250) - drops(1) - + env.balance(alice))); + env.close(); + + // alice doesn't quite have the reserve for a total of 250 + // Tickets, so the transaction fails. + env(ticket::create(alice, 249), ter(tecINSUFFICIENT_RESERVE)); + env.close(); + env.require(owners(alice, 1), tickets(alice, 1)); + + // Give alice enough so she can make the reserve for all 250 + // Tickets. + env(pay( + env.master, + alice, + env.current()->fees().accountReserve(250) - env.balance(alice))); + env.close(); + + std::uint32_t const ticketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 249)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(alice, 250), tickets(alice, 250)); + BEAST_EXPECT(ticketSeq + 249 == env.seq(alice)); } void - testTicketCancelByTarget() + testUsingTickets() { - testcase("Create Ticket and Then Cancel by Target"); + testcase("Using Tickets"); using namespace test::jtx; - Env env{*this, supported_amendments().set(featureTickets)}; + Env env{*this, supported_amendments() | featureTicketBatch}; Account alice{"alice"}; env.fund(XRP(10000), alice); env.close(); - // create and verify - env(ticket::create(env.master, alice)); - auto cr = checkTicketMeta(env, true); - auto const& jacct = cr[0]; - auto const& jticket = cr[1]; - BEAST_EXPECT( - jacct[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 1); - BEAST_EXPECT(jticket[sfLedgerEntryType.fieldName] == jss::Ticket); - BEAST_EXPECT( - jticket[sfLedgerIndex.fieldName] == - "C231BA31A0E13A4D524A75F990CE0D6890B800FF1AE75E51A2D33559547AC1A2"); - BEAST_EXPECT( - jticket[sfNewFields.fieldName][jss::Account] == env.master.human()); - BEAST_EXPECT( - jticket[sfNewFields.fieldName][sfTarget.fieldName] == - alice.human()); - BEAST_EXPECT(jticket[sfNewFields.fieldName][jss::Sequence] == 2); + // Successfully create tickets (using a sequence) + std::uint32_t const ticketSeq_AB{env.seq(alice) + 1}; + env(ticket::create(alice, 2)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(alice, 2), tickets(alice, 2)); + BEAST_EXPECT(ticketSeq_AB + 2 == env.seq(alice)); - // cancel using the target account - env(ticket::cancel(alice, jticket[sfLedgerIndex.fieldName].asString())); - auto crd = checkTicketMeta(env, true); - auto const& jacctd = crd[0]; - auto const& jdir = crd[2]; - BEAST_EXPECT( - jacctd[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 0); - BEAST_EXPECT( - jdir[sfLedgerIndex.fieldName] == jticket[sfLedgerIndex.fieldName]); - BEAST_EXPECT( - jdir[sfFinalFields.fieldName][jss::Account] == env.master.human()); - BEAST_EXPECT( - jdir[sfFinalFields.fieldName][sfTarget.fieldName] == alice.human()); - BEAST_EXPECT(jdir[sfFinalFields.fieldName][jss::Flags] == 0); - BEAST_EXPECT( - jdir[sfFinalFields.fieldName][sfOwnerNode.fieldName] == - "0000000000000000"); - BEAST_EXPECT(jdir[sfFinalFields.fieldName][jss::Sequence] == 2); - } + // You can use a ticket to create one ticket ... + std::uint32_t const ticketSeq_C{env.seq(alice)}; + env(ticket::create(alice, 1), ticket::use(ticketSeq_AB + 0)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(alice, 2), tickets(alice, 2)); + BEAST_EXPECT(ticketSeq_C + 1 == env.seq(alice)); - void - testTicketWithExpiration() - { - testcase("Create Ticket with Future Expiration"); + // ... you can use a ticket to create multiple tickets ... + std::uint32_t const ticketSeq_DE{env.seq(alice)}; + env(ticket::create(alice, 2), ticket::use(ticketSeq_AB + 1)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(alice, 3), tickets(alice, 3)); + BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice)); - using namespace test::jtx; - Env env{*this, supported_amendments().set(featureTickets)}; + // ... and you can use a ticket for other things. + env(noop(alice), ticket::use(ticketSeq_DE + 0)); + checkTicketConsumeMeta(env); + env.close(); + env.require(owners(alice, 2), tickets(alice, 2)); + BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice)); - // create and verify - using namespace std::chrono_literals; - uint32_t expire = - (env.timeKeeper().closeTime() + 60s).time_since_epoch().count(); - env(ticket::create(env.master, expire)); - auto cr = checkTicketMeta(env); - auto const& jacct = cr[0]; - auto const& jticket = cr[1]; - BEAST_EXPECT( - jacct[sfPreviousFields.fieldName][sfOwnerCount.fieldName] == 0); - BEAST_EXPECT( - jacct[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 1); - BEAST_EXPECT( - jticket[sfNewFields.fieldName][jss::Sequence] == - jacct[sfPreviousFields.fieldName][jss::Sequence]); - BEAST_EXPECT( - jticket[sfNewFields.fieldName][sfExpiration.fieldName] == expire); - } + env(pay(alice, env.master, XRP(20)), ticket::use(ticketSeq_DE + 1)); + checkTicketConsumeMeta(env); + env.close(); + env.require(owners(alice, 1), tickets(alice, 1)); + BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice)); - void - testTicketZeroExpiration() - { - testcase("Create Ticket with Zero Expiration"); + env(trust(alice, env.master["USD"](20)), ticket::use(ticketSeq_C)); + checkTicketConsumeMeta(env); + env.close(); + env.require(owners(alice, 1), tickets(alice, 0)); + BEAST_EXPECT(ticketSeq_DE + 2 == env.seq(alice)); - using namespace test::jtx; - Env env{*this, supported_amendments().set(featureTickets)}; - - // create and verify - env(ticket::create(env.master, 0u), ter(temBAD_EXPIRATION)); - } - - void - testTicketWithPastExpiration() - { - testcase("Create Ticket with Past Expiration"); - - using namespace test::jtx; - Env env{*this, supported_amendments().set(featureTickets)}; - - env.timeKeeper().adjustCloseTime(days{2}); + // Attempt to use a ticket that has already been used. + env(noop(alice), ticket::use(ticketSeq_C), ter(tefNO_TICKET)); env.close(); - // create and verify - uint32_t expire = 60; - env(ticket::create(env.master, expire)); - // in the case of past expiration, we only get - // one meta node entry returned - auto const& jvm = env.meta()->getJson(JsonOptions::none); - BEAST_EXPECT(jvm.isMember(sfAffectedNodes.fieldName)); - BEAST_EXPECT(jvm[sfAffectedNodes.fieldName].isArray()); - BEAST_EXPECT(jvm[sfAffectedNodes.fieldName].size() == 1); - BEAST_EXPECT(jvm[sfAffectedNodes.fieldName][0u].isMember( - sfModifiedNode.fieldName)); - auto const& jacct = - jvm[sfAffectedNodes.fieldName][0u][sfModifiedNode.fieldName]; - BEAST_EXPECT(jacct[sfLedgerEntryType.fieldName] == jss::AccountRoot); - BEAST_EXPECT( - jacct[sfFinalFields.fieldName][jss::Account] == env.master.human()); + // Attempt to use a ticket from the future. + std::uint32_t const ticketSeq_F{env.seq(alice) + 1}; + env(noop(alice), ticket::use(ticketSeq_F), ter(terPRE_TICKET)); + env.close(); + + // Now create the ticket. The retry will consume the new ticket. + env(ticket::create(alice, 1)); + checkTicketCreateMeta(env); + env.close(); + env.require(owners(alice, 1), tickets(alice, 0)); + BEAST_EXPECT(ticketSeq_F + 1 == env.seq(alice)); + + // Try a transaction that combines consuming a ticket with + // AccountTxnID. + std::uint32_t const ticketSeq_G{env.seq(alice) + 1}; + env(ticket::create(alice, 1)); + checkTicketCreateMeta(env); + env.close(); + + env(noop(alice), + ticket::use(ticketSeq_G), + json(R"({"AccountTxnID": "0"})"), + ter(temINVALID)); + env.close(); + env.require(owners(alice, 2), tickets(alice, 1)); } void - testTicketAllowExpiration() + testTransactionDatabaseWithTickets() { - testcase("Create Ticket and Allow to Expire"); + // The Transaction database keeps each transaction's sequence number + // in an entry (called "FromSeq"). Until the introduction of tickets + // each sequence stored for a given account would always be unique. + // With the advent of tickets there could be lots of entries + // with zero. + // + // We really don't expect those zeros to cause any problems since + // there are no indexes that use "FromSeq". But it still seems + // prudent to exercise this a bit to see if tickets cause any obvious + // harm. + testcase("Transaction Database With Tickets"); using namespace test::jtx; - Env env{*this, supported_amendments().set(featureTickets)}; - - // create and verify - uint32_t expire = (env.timeKeeper().closeTime() + std::chrono::hours{3}) - .time_since_epoch() - .count(); - env(ticket::create(env.master, expire)); - auto cr = checkTicketMeta(env); - auto const& jacct = cr[0]; - auto const& jticket = cr[1]; - BEAST_EXPECT( - jacct[sfPreviousFields.fieldName][sfOwnerCount.fieldName] == 0); - BEAST_EXPECT( - jacct[sfFinalFields.fieldName][sfOwnerCount.fieldName] == 1); - BEAST_EXPECT( - jticket[sfNewFields.fieldName][sfExpiration.fieldName] == expire); - BEAST_EXPECT( - jticket[sfLedgerIndex.fieldName] == - "7F58A0AE17775BA3404D55D406DD1C2E91EADD7AF3F03A26877BCE764CCB75E3"); - + Env env{*this, supported_amendments() | featureTicketBatch}; Account alice{"alice"}; + env.fund(XRP(10000), alice); env.close(); - // now try to cancel with alice account, which should not work - auto jv = - ticket::cancel(alice, jticket[sfLedgerIndex.fieldName].asString()); - env(jv, ter(tecNO_PERMISSION)); + // Lambda that returns the hash of the most recent transaction. + auto getTxID = [&env, this]() -> uint256 { + std::shared_ptr tx{env.tx()}; + if (!BEAST_EXPECTS(tx, "Transaction not found")) + Throw("Invalid transaction ID"); - // advance the ledger time to as to trigger expiration - env.timeKeeper().adjustCloseTime(days{3}); + return tx->getTransactionID(); + }; + + // A note about the metadata created by these transactions. + // + // We _could_ check the metadata on these transactions. However + // checking the metadata has the side effect of advancing the ledger. + // So if we check the metadata we don't get to look at several + // transactions in the same ledger. Therefore a specific choice was + // made to not check the metadata on these transactions. + + // Successfully create several tickets (using a sequence). + std::uint32_t ticketSeq{env.seq(alice)}; + static constexpr std::uint32_t ticketCount{10}; + env(ticket::create(alice, ticketCount)); + uint256 const txHash_1{getTxID()}; + + // Just for grins use the tickets in reverse from largest to smallest. + ticketSeq += ticketCount; + env(noop(alice), ticket::use(--ticketSeq)); + uint256 const txHash_2{getTxID()}; + + env(pay(alice, env.master, XRP(200)), ticket::use(--ticketSeq)); + uint256 const txHash_3{getTxID()}; + + env(deposit::auth(alice, env.master), ticket::use(--ticketSeq)); + uint256 const txHash_4{getTxID()}; + + // Close the ledger so we look at transactions from a couple of + // different ledgers. env.close(); - // now try again - the cancel succeeds because ticket has expired - env(jv); - auto crd = checkTicketMeta(env, true, true); - auto const& jticketd = crd[1]; - BEAST_EXPECT( - jticketd[sfFinalFields.fieldName][sfExpiration.fieldName] == - expire); + env(pay(alice, env.master, XRP(300)), ticket::use(--ticketSeq)); + uint256 const txHash_5{getTxID()}; + + env(pay(alice, env.master, XRP(400)), ticket::use(--ticketSeq)); + uint256 const txHash_6{getTxID()}; + + env(deposit::unauth(alice, env.master), ticket::use(--ticketSeq)); + uint256 const txHash_7{getTxID()}; + + env(noop(alice), ticket::use(--ticketSeq)); + uint256 const txHash_8{getTxID()}; + + env.close(); + + // Checkout what's in the Transaction database. We go straight + // to the database. Most of our interfaces cache transactions + // in memory. So if we use normal interfaces we would get the + // transactions from memory rather than from the database. + + // Lambda to verify a transaction pulled from the Transaction database. + auto checkTxFromDB = [&env, this]( + uint256 const& txID, + std::uint32_t ledgerSeq, + std::uint32_t txSeq, + boost::optional ticketSeq, + TxType txType) { + error_code_i txErrCode{rpcSUCCESS}; + std::shared_ptr const tx{ + Transaction::load(txID, env.app(), txErrCode)}; + BEAST_EXPECT(txErrCode == rpcSUCCESS); + BEAST_EXPECT(tx->getLedger() == ledgerSeq); + + std::shared_ptr const& sttx{tx->getSTransaction()}; + BEAST_EXPECT((*sttx)[sfSequence] == txSeq); + if (ticketSeq) + BEAST_EXPECT((*sttx)[sfTicketSequence] == *ticketSeq); + BEAST_EXPECT((*sttx)[sfTransactionType] == txType); + }; + + // txID ledgerSeq txSeq ticketSeq txType + checkTxFromDB(txHash_1, 4, 4, {}, ttTICKET_CREATE); + checkTxFromDB(txHash_2, 4, 0, 13, ttACCOUNT_SET); + checkTxFromDB(txHash_3, 4, 0, 12, ttPAYMENT); + checkTxFromDB(txHash_4, 4, 0, 11, ttDEPOSIT_PREAUTH); + + checkTxFromDB(txHash_5, 5, 0, 10, ttPAYMENT); + checkTxFromDB(txHash_6, 5, 0, 9, ttPAYMENT); + checkTxFromDB(txHash_7, 5, 0, 8, ttDEPOSIT_PREAUTH); + checkTxFromDB(txHash_8, 5, 0, 7, ttACCOUNT_SET); } public: @@ -400,17 +820,11 @@ public: run() override { testTicketNotEnabled(); - testTicketCancelNonexistent(); testTicketCreatePreflightFail(); - testTicketCreateNonexistent(); - testTicketToSelf(); - testTicketCancelByCreator(); + testTicketCreatePreclaimFail(); testTicketInsufficientReserve(); - testTicketCancelByTarget(); - testTicketWithExpiration(); - testTicketZeroExpiration(); - testTicketWithPastExpiration(); - testTicketAllowExpiration(); + testUsingTickets(); + testTransactionDatabaseWithTickets(); } }; diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index c666d0855..17661d0ae 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -84,8 +84,7 @@ class TxQ_test : public beast::unit_test::suite auto metrics = env.app().getTxQ().getMetrics(view); // Don't care about the overflow flag - return fee( - toDrops(metrics.openLedgerFeeLevel, view.fees().base).second + 1); + return fee(toDrops(metrics.openLedgerFeeLevel, view.fees().base) + 1); } static std::unique_ptr @@ -100,7 +99,6 @@ class TxQ_test : public beast::unit_test::suite 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("zero_basefee_transaction_feelevel", "100000000000"); section.set("normal_consensus_increase_percent", "0"); for (auto const& [k, v] : extraTxQ) @@ -141,13 +139,12 @@ class TxQ_test : public beast::unit_test::suite env.close(); // The ledger after the flag ledger creates all the // fee (1) and amendment (supportedAmendments().size()) - // pseudotransactions. They all have 0 fee, which is - // treated as a high fee level by the queue, so the - // medianFeeLevel is 100000000000. + // pseudotransactions. The queue treats the fees on these + // transactions as though they are ordinary transactions. auto const flagPerLedger = 1 + ripple::detail::supportedAmendments().size(); auto const flagMaxQueue = ledgersInQueue * flagPerLedger; - checkMetrics(env, 0, flagMaxQueue, 0, flagPerLedger, 256, 100000000000); + checkMetrics(env, 0, flagMaxQueue, 0, flagPerLedger, 256); // Pad a couple of txs with normal fees so the median comes // back down to normal @@ -170,13 +167,13 @@ class TxQ_test : public beast::unit_test::suite public: void - testQueue() + testQueueSeq() { using namespace jtx; using namespace std::chrono; + testcase("queue sequence"); Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); - auto& txq = env.app().getTxQ(); auto alice = Account("alice"); auto bob = Account("bob"); @@ -186,6 +183,7 @@ public: auto fred = Account("fred"); auto gwen = Account("gwen"); auto hank = Account("hank"); + auto iris = Account("iris"); auto queued = ter(terQUEUED); @@ -330,6 +328,26 @@ public: env.close(); checkMetrics(env, 0, 10, 2, 5, 256); + ////////////////////////////////////////////////////////////// + + // Attempt to put a transaction in the queue for an account + // that is not yet funded. + env.memoize(iris); + + env(noop(alice)); + env(noop(bob)); + env(noop(charlie)); + env(noop(daria)); + env(pay(alice, iris, XRP(1000)), queued); + env(noop(iris), seq(1), fee(20), ter(terNO_ACCOUNT)); + checkMetrics(env, 1, 10, 6, 5, 256); + + env.close(); + checkMetrics(env, 0, 12, 1, 6, 256); + + env.require(balance(iris, XRP(1000))); + BEAST_EXPECT(env.seq(iris) == 11); + ////////////////////////////////////////////////////////////// // Cleanup: @@ -337,7 +355,7 @@ public: // we can be sure that there's one in the queue when the // test ends and the TxQ is destructed. - auto metrics = txq.getMetrics(*env.current()); + auto metrics = env.app().getTxQ().getMetrics(*env.current()); BEAST_EXPECT(metrics.txCount == 0); // Stuff the ledger. @@ -359,10 +377,271 @@ public: 256); } + void + testQueueTicket() + { + using namespace jtx; + testcase("queue ticket"); + + Env env( + *this, + makeConfig({{"minimum_txn_in_ledger_standalone", "3"}}), + supported_amendments() | featureTicketBatch); + + auto alice = Account("alice"); + + auto queued = ter(terQUEUED); + + BEAST_EXPECT(env.current()->fees().base == 10); + + checkMetrics(env, 0, boost::none, 0, 3, 256); + + // Fund alice and then fill the ledger. + env.fund(XRP(50000), noripple(alice)); + env(noop(alice)); + env(noop(alice)); + env(noop(alice)); + checkMetrics(env, 0, boost::none, 4, 3, 256); + + ////////////////////////////////////////////////////////////////// + + // Alice requests tickets, but that transaction is queued. So + // Alice can't queue ticketed transactions yet. + std::uint32_t const tkt1{env.seq(alice) + 1}; + env(ticket::create(alice, 250), seq(tkt1 - 1), queued); + + 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(env, 1, boost::none, 4, 3, 256); + + env.close(); + env.require(owners(alice, 250), tickets(alice, 250)); + checkMetrics(env, 0, 8, 1, 4, 256); + BEAST_EXPECT(env.seq(alice) == tkt1 + 250); + + ////////////////////////////////////////////////////////////////// + + // Unlike queued sequence-based transactions, ticket-based + // transactions _do_ move out of the queue largest fee first, + // even within one account, since they can be applied in any order. + // Demonstrate that. + + // Fill the ledger so we can start queuing things. + env(noop(alice), ticket::use(tkt1 + 1), fee(11)); + env(noop(alice), ticket::use(tkt1 + 2), fee(12)); + env(noop(alice), ticket::use(tkt1 + 3), fee(13)); + env(noop(alice), ticket::use(tkt1 + 4), fee(14)); + env(noop(alice), ticket::use(tkt1 + 5), fee(15), queued); + env(noop(alice), ticket::use(tkt1 + 6), fee(16), queued); + env(noop(alice), ticket::use(tkt1 + 7), fee(17), queued); + env(noop(alice), ticket::use(tkt1 + 8), fee(18), queued); + env(noop(alice), ticket::use(tkt1 + 9), fee(19), queued); + env(noop(alice), ticket::use(tkt1 + 10), fee(20), queued); + env(noop(alice), ticket::use(tkt1 + 11), fee(21), queued); + env(noop(alice), ticket::use(tkt1 + 12), fee(22), queued); + env(noop(alice), + ticket::use(tkt1 + 13), + fee(23), + ter(telCAN_NOT_QUEUE_FULL)); + checkMetrics(env, 8, 8, 5, 4, 385); + + // Check which of the queued transactions got into the ledger by + // attempting to replace them. + // o Get tefNO_TICKET if the ticket has already been used. + // o Get telCAN_NOT_QUEUE_FEE if the transaction is still in the queue. + env.close(); + env.require(owners(alice, 240), tickets(alice, 240)); + + // These 4 went straight to the ledger: + env(noop(alice), ticket::use(tkt1 + 1), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 2), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 3), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 4), ter(tefNO_TICKET)); + + // These two are still in the TxQ: + env(noop(alice), ticket::use(tkt1 + 5), ter(telCAN_NOT_QUEUE_FEE)); + env(noop(alice), ticket::use(tkt1 + 6), ter(telCAN_NOT_QUEUE_FEE)); + + // These six were moved from the queue into the open ledger + // since those with the highest fees go first. + env(noop(alice), ticket::use(tkt1 + 7), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 8), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 9), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 10), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 11), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 12), ter(tefNO_TICKET)); + + // This last one was moved from the local transactions into + // the queue. + env(noop(alice), ticket::use(tkt1 + 13), ter(telCAN_NOT_QUEUE_FEE)); + + checkMetrics(env, 3, 10, 6, 5, 256); + + ////////////////////////////////////////////////////////////////// + + // Do some experiments with putting sequence-based transactions + // into the queue while there are ticket-based transactions + // already in the queue. + + // Alice still has three ticket-based transactions in the queue. + // The fee is escalated so unless we pay a sufficient fee + // transactions will go straight to the queue. + std::uint32_t const nextSeq{env.seq(alice)}; + env(noop(alice), seq(nextSeq + 1), ter(terPRE_SEQ)); + env(noop(alice), seq(nextSeq - 1), ter(tefPAST_SEQ)); + env(noop(alice), seq(nextSeq + 0), queued); + + // Now that nextSeq is in the queue, we should be able to queue + // nextSeq + 1. + env(noop(alice), seq(nextSeq + 1), queued); + + // Fill the queue with sequence-based transactions. When the + // ledger closes we should find the three ticket-based + // transactions gone from the queue (because they had the + // highest fee). Then the earliest of the sequence-based + // transactions should also be gone from the queue. + env(noop(alice), seq(nextSeq + 2), queued); + env(noop(alice), seq(nextSeq + 3), queued); + env(noop(alice), seq(nextSeq + 4), queued); + 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(env, 10, 10, 6, 5, 257); + + // Check which of the queued transactions got into the ledger by + // attempting to replace them. + // o Get tefNo_TICKET if the ticket has already been used. + // o Get tefPAST_SEQ if the sequence moved out of the queue. + // o Get telCAN_NOT_QUEUE_FEE if the transaction is still in + // the queue. + env.close(); + env.require(owners(alice, 237), tickets(alice, 237)); + + // The four ticket-based transactions went out first, since + // they paid the highest fee. + env(noop(alice), ticket::use(tkt1 + 4), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 5), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 12), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 13), ter(tefNO_TICKET)); + + // Three of the sequence-based transactions also moved out of + // the queue. + env(noop(alice), seq(nextSeq + 1), ter(tefPAST_SEQ)); + env(noop(alice), seq(nextSeq + 2), ter(tefPAST_SEQ)); + env(noop(alice), seq(nextSeq + 3), ter(tefPAST_SEQ)); + env(noop(alice), seq(nextSeq + 4), ter(telCAN_NOT_QUEUE_FEE)); + env(noop(alice), seq(nextSeq + 5), ter(telCAN_NOT_QUEUE_FEE)); + env(noop(alice), seq(nextSeq + 6), ter(telCAN_NOT_QUEUE_FEE)); + env(noop(alice), seq(nextSeq + 7), ter(telCAN_NOT_QUEUE_FEE)); + + checkMetrics(env, 4, 12, 7, 6, 256); + BEAST_EXPECT(env.seq(alice) == nextSeq + 4); + + ////////////////////////////////////////////////////////////////// + + // We haven't yet shown that ticket-based transactions can be added + // to the queue in any order. We should do that... + std::uint32_t tkt250 = tkt1 + 249; + env(noop(alice), ticket::use(tkt250 - 0), fee(30), queued); + env(noop(alice), ticket::use(tkt1 + 14), fee(29), queued); + env(noop(alice), ticket::use(tkt250 - 1), fee(28), queued); + env(noop(alice), ticket::use(tkt1 + 15), fee(27), queued); + env(noop(alice), ticket::use(tkt250 - 2), fee(26), queued); + env(noop(alice), ticket::use(tkt1 + 16), fee(25), queued); + env(noop(alice), + ticket::use(tkt250 - 3), + fee(24), + ter(telCAN_NOT_QUEUE_FULL)); + env(noop(alice), + ticket::use(tkt1 + 17), + fee(23), + ter(telCAN_NOT_QUEUE_FULL)); + env(noop(alice), + ticket::use(tkt250 - 4), + fee(22), + ter(telCAN_NOT_QUEUE_FULL)); + env(noop(alice), + ticket::use(tkt1 + 18), + fee(21), + ter(telCAN_NOT_QUEUE_FULL)); + + checkMetrics(env, 10, 12, 7, 6, 256); + + env.close(); + env.require(owners(alice, 231), tickets(alice, 231)); + + // These three ticket-based transactions escaped the queue. + env(noop(alice), ticket::use(tkt1 + 14), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 15), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt1 + 16), ter(tefNO_TICKET)); + + // But these four ticket-based transactions are in the queue + // now; they moved into the TxQ from local transactions. + env(noop(alice), ticket::use(tkt250 - 3), ter(telCAN_NOT_QUEUE_FEE)); + env(noop(alice), ticket::use(tkt1 + 17), ter(telCAN_NOT_QUEUE_FEE)); + env(noop(alice), ticket::use(tkt250 - 4), ter(telCAN_NOT_QUEUE_FEE)); + env(noop(alice), ticket::use(tkt1 + 18), ter(telCAN_NOT_QUEUE_FEE)); + + // These three ticket-based transactions also escaped the queue. + env(noop(alice), ticket::use(tkt250 - 2), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt250 - 1), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt250 - 0), ter(tefNO_TICKET)); + + // These sequence-based transactions escaped the queue. + env(noop(alice), seq(nextSeq + 4), ter(tefPAST_SEQ)); + env(noop(alice), seq(nextSeq + 5), ter(tefPAST_SEQ)); + + // But these sequence-based transactions are still stuck in the queue. + env(noop(alice), seq(nextSeq + 6), ter(telCAN_NOT_QUEUE_FEE)); + env(noop(alice), seq(nextSeq + 7), ter(telCAN_NOT_QUEUE_FEE)); + + BEAST_EXPECT(env.seq(alice) == nextSeq + 6); + checkMetrics(env, 6, 14, 8, 7, 256); + + ////////////////////////////////////////////////////////////////// + + // Since we still have two ticket-based transactions in the queue + // let's try replacing them. + + // 26 drops is less than 21 * 1.25 + env(noop(alice), + ticket::use(tkt1 + 18), + fee(26), + ter(telCAN_NOT_QUEUE_FEE)); + + // 27 drops is more than 21 * 1.25 + env(noop(alice), ticket::use(tkt1 + 18), fee(27), queued); + + // 27 drops is less than 22 * 1.25 + env(noop(alice), + ticket::use(tkt250 - 4), + fee(27), + ter(telCAN_NOT_QUEUE_FEE)); + + // 28 drops is more than 22 * 1.25 + env(noop(alice), ticket::use(tkt250 - 4), fee(28), queued); + + env.close(); + env.require(owners(alice, 227), tickets(alice, 227)); + + // Verify that all remaining transactions made it out of the TxQ. + env(noop(alice), ticket::use(tkt1 + 18), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt250 - 4), ter(tefNO_TICKET)); + env(noop(alice), seq(nextSeq + 4), ter(tefPAST_SEQ)); + env(noop(alice), seq(nextSeq + 5), ter(tefPAST_SEQ)); + env(noop(alice), seq(nextSeq + 6), ter(tefPAST_SEQ)); + env(noop(alice), seq(nextSeq + 7), ter(tefPAST_SEQ)); + + BEAST_EXPECT(env.seq(alice) == nextSeq + 8); + checkMetrics(env, 0, 16, 6, 8, 256); + } + void testTecResult() { using namespace jtx; + testcase("queue tec"); Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "2"}})); @@ -399,6 +678,7 @@ public: { using namespace jtx; using namespace std::chrono; + testcase("local tx retry"); Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "2"}})); @@ -428,7 +708,7 @@ public: checkMetrics(env, 1, boost::none, 3, 2, 256); // Alice - sequence is too far ahead, so won't queue. - env(noop(alice), seq(env.seq(alice) + 2), ter(terPRE_SEQ)); + env(noop(alice), seq(env.seq(alice) + 2), ter(telCAN_NOT_QUEUE)); checkMetrics(env, 1, boost::none, 3, 2, 256); // Bob with really high fee - applies @@ -455,6 +735,7 @@ public: { using namespace jtx; using namespace std::chrono; + testcase("last ledger sequence"); Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "2"}})); @@ -501,19 +782,18 @@ public: auto& txQ = env.app().getTxQ(); auto aliceStat = txQ.getAccountTxs(alice.id(), *env.current()); BEAST_EXPECT(aliceStat.size() == 1); - BEAST_EXPECT(aliceStat.begin()->second.feeLevel == FeeLevel64{256}); + BEAST_EXPECT(aliceStat.begin()->feeLevel == FeeLevel64{256}); BEAST_EXPECT( - aliceStat.begin()->second.lastValid && - *aliceStat.begin()->second.lastValid == 8); - BEAST_EXPECT(!aliceStat.begin()->second.consequences); + aliceStat.begin()->lastValid && + *aliceStat.begin()->lastValid == 8); + BEAST_EXPECT(!aliceStat.begin()->consequences.isBlocker()); auto bobStat = txQ.getAccountTxs(bob.id(), *env.current()); BEAST_EXPECT(bobStat.size() == 1); BEAST_EXPECT( - bobStat.begin()->second.feeLevel == - FeeLevel64{7000 * 256 / 10}); - BEAST_EXPECT(!bobStat.begin()->second.lastValid); - BEAST_EXPECT(!bobStat.begin()->second.consequences); + bobStat.begin()->feeLevel == FeeLevel64{7000 * 256 / 10}); + BEAST_EXPECT(!bobStat.begin()->lastValid); + BEAST_EXPECT(!bobStat.begin()->consequences.isBlocker()); auto noStat = txQ.getAccountTxs(Account::master.id(), *env.current()); @@ -552,11 +832,13 @@ public: // though one of felicia's is still in the queue. checkMetrics(env, 1, 10, 6, 5, 256, 700 * 256); BEAST_EXPECT(env.seq(alice) == 3); + BEAST_EXPECT(env.seq(felicia) == 7); env.close(); // And now the queue is empty checkMetrics(env, 0, 12, 1, 6, 256, 800 * 256); BEAST_EXPECT(env.seq(alice) == 3); + BEAST_EXPECT(env.seq(felicia) == 8); } void @@ -564,6 +846,7 @@ public: { using namespace jtx; using namespace std::chrono; + testcase("zero transaction fee"); Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "2"}})); @@ -590,86 +873,78 @@ public: env(noop(bob), queued); checkMetrics(env, 1, boost::none, 3, 2, 256); - // Even though this transaction has a 0 fee, - // SetRegularKey::calculateBaseFee indicates this is - // a "free" transaction, so it has an "infinite" fee - // level and goes into the open ledger. - env(regkey(alice, bob), fee(0)); - checkMetrics(env, 1, boost::none, 4, 2, 256); + // Since Alice's queue is empty this blocker can go into her queue. + env(regkey(alice, bob), fee(0), queued); + checkMetrics(env, 2, boost::none, 3, 2, 256); // Close out this ledger so we can get a maxsize env.close(); - checkMetrics(env, 0, 8, 1, 4, 256); + checkMetrics(env, 0, 6, 2, 3, 256); - fillQueue(env, bob); - checkMetrics(env, 0, 8, 5, 4, 256); + fillQueue(env, alice); + checkMetrics(env, 0, 6, 4, 3, 256); - auto feeBob = 30; - auto seqBob = env.seq(bob); + auto feeAlice = 30; + auto seqAlice = env.seq(alice); for (int i = 0; i < 4; ++i) { - env(noop(bob), fee(feeBob), seq(seqBob), queued); - feeBob = (feeBob + 1) * 125 / 100; - ++seqBob; + env(noop(alice), fee(feeAlice), seq(seqAlice), queued); + feeAlice = (feeAlice + 1) * 125 / 100; + ++seqAlice; } - checkMetrics(env, 4, 8, 5, 4, 256); + checkMetrics(env, 4, 6, 4, 3, 256); - // This transaction also has an "infinite" fee level, - // but since bob has txns in the queue, it gets queued. - env(regkey(bob, alice), fee(0), seq(seqBob), queued); - ++seqBob; - checkMetrics(env, 5, 8, 5, 4, 256); + // Bob adds a zero fee blocker to his queue. + auto const seqBob = env.seq(bob); + env(regkey(bob, alice), fee(0), queued); + checkMetrics(env, 5, 6, 4, 3, 256); - // Unfortunately bob can't get any more txns into - // the queue, because of the multiTxnPercent. - // TANSTAAFL - env(noop(bob), fee(XRP(100)), seq(seqBob), ter(telINSUF_FEE_P)); - - // Carol fills the queue, but can't kick out any - // transactions. - auto feeCarol = feeBob; + // Carol fills the queue. + auto feeCarol = feeAlice; auto seqCarol = env.seq(carol); - for (int i = 0; i < 3; ++i) + for (int i = 0; i < 4; ++i) { env(noop(carol), fee(feeCarol), seq(seqCarol), queued); feeCarol = (feeCarol + 1) * 125 / 100; ++seqCarol; } - checkMetrics(env, 8, 8, 5, 4, 3 * 256 + 1); + checkMetrics(env, 6, 6, 4, 3, 3 * 256 + 1); - // Carol doesn't submit high enough to beat Bob's - // average fee. (Which is ~144,115,188,075,855,907 - // because of the zero fee txn.) - env(noop(carol), - fee(feeCarol), - seq(seqCarol), - ter(telCAN_NOT_QUEUE_FULL)); + // Carol submits high enough to beat Bob's average fee which kicks + // out Bob's queued transaction. However Bob's transaction stays + // in the localTx queue, so it will return to the TxQ next time + // around. + env(noop(carol), fee(feeCarol), seq(seqCarol), ter(terQUEUED)); env.close(); - // Some of Bob's transactions stay in the queue, - // and Carol's low fee tx is reapplied from the - // Local Txs. - checkMetrics(env, 3, 10, 6, 5, 256); - BEAST_EXPECT(env.seq(bob) == seqBob - 2); - BEAST_EXPECT(env.seq(carol) == seqCarol); + // Some of Alice's transactions stay in the queue. Bob's + // transaction returns to the TxQ. + checkMetrics(env, 5, 8, 5, 4, 256); + BEAST_EXPECT(env.seq(alice) == seqAlice - 4); + BEAST_EXPECT(env.seq(bob) == seqBob); + BEAST_EXPECT(env.seq(carol) == seqCarol + 1); env.close(); - checkMetrics(env, 0, 12, 4, 6, 256); + // The remaining queued transactions flush through to the ledger. + checkMetrics(env, 0, 10, 5, 5, 256); + BEAST_EXPECT(env.seq(alice) == seqAlice); BEAST_EXPECT(env.seq(bob) == seqBob + 1); BEAST_EXPECT(env.seq(carol) == seqCarol + 1); env.close(); - checkMetrics(env, 0, 12, 0, 6, 256); + checkMetrics(env, 0, 10, 0, 5, 256); + BEAST_EXPECT(env.seq(alice) == seqAlice); BEAST_EXPECT(env.seq(bob) == seqBob + 1); BEAST_EXPECT(env.seq(carol) == seqCarol + 1); } void - testPreclaimFailures() + testFailInPreclaim() { using namespace jtx; Env env(*this, makeConfig()); + testcase("fail in preclaim"); auto alice = Account("alice"); auto bob = Account("bob"); @@ -688,9 +963,10 @@ public: } void - testQueuedFailure() + testQueuedTxFails() { using namespace jtx; + testcase("queued tx fails"); Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "2"}})); @@ -741,6 +1017,7 @@ public: testMultiTxnPerAccount() { using namespace jtx; + testcase("multi tx per account"); Env env( *this, @@ -776,10 +1053,10 @@ public: auto charlieSeq = env.seq(charlie); // Alice - try to queue a second transaction, but leave a gap - env(noop(alice), seq(aliceSeq + 2), fee(100), ter(terPRE_SEQ)); + env(noop(alice), seq(aliceSeq + 2), fee(100), ter(telCAN_NOT_QUEUE)); checkMetrics(env, 1, initQueueMax, 4, 3, 256); - // Alice - queue a second transaction. Yay. + // Alice - queue a second transaction. Yay! env(noop(alice), seq(aliceSeq + 1), fee(13), queued); checkMetrics(env, 2, initQueueMax, 4, 3, 256); @@ -837,19 +1114,16 @@ public: auto const& baseFee = env.current()->fees().base; auto seq = env.seq(alice); BEAST_EXPECT(aliceStat.size() == 7); - for (auto const& [txSeq, details] : aliceStat) + for (auto const& tx : aliceStat) { - BEAST_EXPECT(txSeq == seq); + BEAST_EXPECT(tx.seqProxy.isSeq() && tx.seqProxy.value() == seq); + BEAST_EXPECT(tx.feeLevel == toFeeLevel(fee, baseFee)); + BEAST_EXPECT(tx.lastValid); BEAST_EXPECT( - details.feeLevel == toFeeLevel(fee, baseFee).second); - BEAST_EXPECT(details.lastValid); - BEAST_EXPECT( - (details.consequences && - details.consequences->fee == drops(fee) && - details.consequences->potentialSpend == drops(0) && - details.consequences->category == - TxConsequences::normal) || - txSeq == env.seq(alice) + 6); + (tx.consequences.fee() == drops(fee) && + tx.consequences.potentialSpend() == drops(0) && + !tx.consequences.isBlocker()) || + tx.seqProxy.value() == env.seq(alice) + 6); ++seq; } } @@ -878,7 +1152,7 @@ public: // 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(terPRE_SEQ)); + env(noop(alice), seq(aliceSeq), fee(aliceFee), ter(telCAN_NOT_QUEUE)); checkMetrics(env, 8, 8, 5, 4, 513); // Alice wants this tx more than the dropped tx, @@ -967,12 +1241,45 @@ public: env(noop(bob), seq(bobSeq + i), queued); checkMetrics(env, 10, 12, 7, 6, 256); // Bob hit the single account limit - env(noop(bob), seq(bobSeq + 10), ter(terPRE_SEQ)); + env(noop(bob), seq(bobSeq + 10), ter(telCAN_NOT_QUEUE_FULL)); checkMetrics(env, 10, 12, 7, 6, 256); // Bob can replace one of the earlier txs regardless // of the limit env(noop(bob), seq(bobSeq + 5), fee(20), queued); checkMetrics(env, 10, 12, 7, 6, 256); + + // Try to replace a middle item in the queue + // with enough fee to bankrupt bob and make the + // later transactions unable to pay their fees + std::int64_t bobFee = + env.le(bob)->getFieldAmount(sfBalance).xrp().drops() - (9 * 10 - 1); + env(noop(bob), + seq(bobSeq + 5), + fee(bobFee), + ter(telCAN_NOT_QUEUE_BALANCE)); + checkMetrics(env, 10, 12, 7, 6, 256); + + // 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 + // first six transactions. + // + // The attempt fails because the sum of bob's fees now exceeds the + // (artificially lowered to 200 drops) account reserve. + bobFee = + env.le(bob)->getFieldAmount(sfBalance).xrp().drops() - (9 * 10); + env(noop(bob), + seq(bobSeq + 5), + fee(bobFee), + ter(telCAN_NOT_QUEUE_BALANCE)); + checkMetrics(env, 10, 12, 7, 6, 256); + + // Close the ledger and verify that the queued transactions succeed + // and bob has the right ending balance. + env.close(); + checkMetrics(env, 3, 14, 8, 7, 256); + env.close(); + checkMetrics(env, 0, 16, 3, 8, 256); + env.require(balance(bob, drops(499'999'999'750))); } void @@ -980,6 +1287,7 @@ public: { using namespace jtx; using namespace std::chrono; + testcase("tie breaking"); Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "4"}})); @@ -1117,6 +1425,7 @@ public: testAcctTxnID() { using namespace jtx; + testcase("acct tx id"); Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "1"}})); @@ -1156,6 +1465,7 @@ public: { using namespace jtx; using namespace std::string_literals; + testcase("maximum tx"); { Env env( @@ -1251,6 +1561,7 @@ public: testUnexpectedBalanceChange() { using namespace jtx; + testcase("unexpected balance change"); Env env( *this, @@ -1344,11 +1655,10 @@ public: } void - testBlockers() + testBlockersSeq() { using namespace jtx; - - Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); + testcase("blockers sequence"); auto alice = Account("alice"); auto bob = Account("bob"); @@ -1357,61 +1667,286 @@ public: auto queued = ter(terQUEUED); + Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); + BEAST_EXPECT(env.current()->fees().base == 10); checkMetrics(env, 0, boost::none, 0, 3, 256); env.fund(XRP(50000), noripple(alice, bob)); env.memoize(charlie); - env.memoize(daria); + checkMetrics(env, 0, boost::none, 2, 3, 256); + { + // Cannot put a blocker in an account's queue if that queue + // already holds two or more (non-blocker) entries. + + // Fill up the open ledger + env(noop(alice)); + // Set a regular key just to clear the password spent flag + env(regkey(alice, charlie)); + checkMetrics(env, 0, boost::none, 4, 3, 256); + + // Put two "normal" txs in the queue + auto const aliceSeq = env.seq(alice); + env(noop(alice), seq(aliceSeq + 0), queued); + env(noop(alice), seq(aliceSeq + 1), queued); + + // Can't replace either queued transaction with a blocker + env(fset(alice, asfAccountTxnID), + seq(aliceSeq + 0), + fee(20), + ter(telCAN_NOT_QUEUE_BLOCKS)); + + env(regkey(alice, bob), + seq(aliceSeq + 1), + fee(20), + ter(telCAN_NOT_QUEUE_BLOCKS)); + + // Can't append a blocker to the queue. + env(signers(alice, 2, {{bob}, {charlie}, {daria}}), + seq(aliceSeq + 2), + fee(20), + ter(telCAN_NOT_QUEUE_BLOCKS)); + + // Other accounts are not affected + env(noop(bob), queued); + checkMetrics(env, 3, boost::none, 4, 3, 256); + + // Drain the queue. + env.close(); + checkMetrics(env, 0, 8, 4, 4, 256); + } + { + // Replace a lone non-blocking tx with a blocker. + + // Fill up the open ledger and put just one entry in the TxQ. + env(noop(alice)); + + auto const aliceSeq = env.seq(alice); + env(noop(alice), seq(aliceSeq + 0), queued); + + // Since there's only one entry in the queue we can replace + // that entry with a blocker. + env(regkey(alice, bob), seq(aliceSeq + 0), fee(20), queued); + + // Now that there's a blocker in the queue we can't append to + // the queue. + env(noop(alice), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BLOCKED)); + + // Other accounts are unaffected. + env(noop(bob), queued); + + // We can replace the blocker with a different blocker. + env(signers(alice, 2, {{bob}, {charlie}, {daria}}), + seq(aliceSeq + 0), + fee(26), + queued); + + // Prove that the queue is still blocked. + env(noop(alice), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BLOCKED)); + + // We can replace the blocker with a non-blocker. Then we can + // successfully append to the queue. + env(noop(alice), seq(aliceSeq + 0), fee(33), queued); + env(noop(alice), seq(aliceSeq + 1), queued); + + // Drain the queue. + env.close(); + checkMetrics(env, 0, 10, 3, 5, 256); + } + { + // Put a blocker in an empty queue. + + // Fill up the open ledger and put a blocker as Alice's first + // entry in the (empty) TxQ. + env(noop(alice)); + env(noop(alice)); + env(noop(alice)); + + auto const aliceSeq = env.seq(alice); + env(fset(alice, asfAccountTxnID), seq(aliceSeq + 0), queued); + + // Since there's a blocker in the queue we can't append to + // the queue. + env(noop(alice), seq(aliceSeq + 1), ter(telCAN_NOT_QUEUE_BLOCKED)); + + // Other accounts are unaffected. + env(noop(bob), queued); + + // We can replace the blocker with a non-blocker. Then we can + // successfully append to the queue. + env(noop(alice), seq(aliceSeq + 0), fee(20), queued); + env(noop(alice), seq(aliceSeq + 1), queued); + + // Drain the queue. + env.close(); + checkMetrics(env, 0, 12, 3, 6, 256); + } + } + + void + testBlockersTicket() + { + using namespace jtx; + testcase("blockers ticket"); + + auto alice = Account("alice"); + auto bob = Account("bob"); + auto charlie = Account("charlie"); + auto daria = Account("daria"); + + auto queued = ter(terQUEUED); + + Env env( + *this, + makeConfig({{"minimum_txn_in_ledger_standalone", "3"}}), + supported_amendments() | featureTicketBatch); + + BEAST_EXPECT(env.current()->fees().base == 10); + + checkMetrics(env, 0, boost::none, 0, 3, 256); + + env.fund(XRP(50000), noripple(alice, bob)); + env.memoize(charlie); + checkMetrics(env, 0, boost::none, 2, 3, 256); - // Fill up the open ledger - env(noop(alice)); - // Set a regular key just to clear the password spent flag - env(regkey(alice, charlie)); - checkMetrics(env, 0, boost::none, 4, 3, 256); + std::uint32_t tkt{env.seq(alice) + 1}; + { + // Cannot put a blocker in an account's queue if that queue + // already holds two or more (non-blocker) entries. - // Put some "normal" txs in the queue - auto aliceSeq = env.seq(alice); - env(noop(alice), queued); - env(noop(alice), seq(aliceSeq + 1), queued); - env(noop(alice), seq(aliceSeq + 2), queued); + // Fill up the open ledger + env(ticket::create(alice, 250), seq(tkt - 1)); + // Set a regular key just to clear the password spent flag + env(regkey(alice, charlie)); + checkMetrics(env, 0, boost::none, 4, 3, 256); - // Can't replace the first tx with a blocker - env(fset(alice, asfAccountTxnID), - fee(20), - ter(telCAN_NOT_QUEUE_BLOCKS)); - // Can't replace the second / middle tx with a blocker - env(regkey(alice, bob), - seq(aliceSeq + 1), - fee(20), - ter(telCAN_NOT_QUEUE_BLOCKS)); - env(signers(alice, 2, {{bob}, {charlie}, {daria}}), - fee(20), - seq(aliceSeq + 1), - ter(telCAN_NOT_QUEUE_BLOCKS)); - // CAN replace the last tx with a blocker - env(signers(alice, 2, {{bob}, {charlie}, {daria}}), - fee(20), - seq(aliceSeq + 2), - queued); - env(regkey(alice, bob), seq(aliceSeq + 2), fee(30), queued); + // Put two "normal" txs in the queue + auto const aliceSeq = env.seq(alice); + env(noop(alice), ticket::use(tkt + 2), queued); + env(noop(alice), ticket::use(tkt + 1), queued); - // Can't queue up any more transactions after the blocker - env(noop(alice), seq(aliceSeq + 3), ter(telCAN_NOT_QUEUE_BLOCKED)); + // Can't replace either queued transaction with a blocker + env(fset(alice, asfAccountTxnID), + ticket::use(tkt + 1), + fee(20), + ter(telCAN_NOT_QUEUE_BLOCKS)); - // Other accounts are not affected - env(noop(bob), queued); + env(regkey(alice, bob), + ticket::use(tkt + 2), + fee(20), + ter(telCAN_NOT_QUEUE_BLOCKS)); - // Can replace the txs before the blocker - env(noop(alice), fee(14), queued); + // Can't append a blocker to the queue. + env(signers(alice, 2, {{bob}, {charlie}, {daria}}), + fee(20), + ter(telCAN_NOT_QUEUE_BLOCKS)); - // Can replace the blocker itself - env(noop(alice), seq(aliceSeq + 2), fee(40), queued); + env(signers(alice, 2, {{bob}, {charlie}, {daria}}), + ticket::use(tkt + 0), + fee(20), + ter(telCAN_NOT_QUEUE_BLOCKS)); - // And now there's no block. - env(noop(alice), seq(aliceSeq + 3), queued); + // Other accounts are not affected + env(noop(bob), queued); + checkMetrics(env, 3, boost::none, 4, 3, 256); + + // Drain the queue and local transactions. + env.close(); + checkMetrics(env, 0, 8, 5, 4, 256); + + // Show that the local transactions have flushed through as well. + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + env(noop(alice), ticket::use(tkt + 0), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt + 1), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt + 2), ter(tefNO_TICKET)); + tkt += 3; + } + { + // Replace a lone non-blocking tx with a blocker. + + // Put just one entry in the TxQ. + auto const aliceSeq = env.seq(alice); + env(noop(alice), ticket::use(tkt + 0), queued); + + // Since there's an entry in the queue we cannot append a + // blocker to the account's queue. + env(regkey(alice, bob), fee(20), ter(telCAN_NOT_QUEUE_BLOCKS)); + env(regkey(alice, bob), + ticket::use(tkt + 1), + fee(20), + ter(telCAN_NOT_QUEUE_BLOCKS)); + + // However we can _replace_ that lone entry with a blocker. + env(regkey(alice, bob), ticket::use(tkt + 0), fee(20), queued); + + // Now that there's a blocker in the queue we can't append to + // the queue. + env(noop(alice), ter(telCAN_NOT_QUEUE_BLOCKED)); + env(noop(alice), + ticket::use(tkt + 1), + ter(telCAN_NOT_QUEUE_BLOCKED)); + + // Other accounts are unaffected. + env(noop(bob), queued); + + // We can replace the blocker with a different blocker. + env(signers(alice, 2, {{bob}, {charlie}, {daria}}), + ticket::use(tkt + 0), + fee(26), + queued); + + // Prove that the queue is still blocked. + env(noop(alice), ter(telCAN_NOT_QUEUE_BLOCKED)); + env(noop(alice), + ticket::use(tkt + 1), + ter(telCAN_NOT_QUEUE_BLOCKED)); + + // We can replace the blocker with a non-blocker. Then we can + // successfully append to the queue. + env(noop(alice), ticket::use(tkt + 0), fee(33), queued); + env(noop(alice), ticket::use(tkt + 1), queued); + env(noop(alice), seq(aliceSeq), queued); + + // Drain the queue. + env.close(); + checkMetrics(env, 0, 10, 4, 5, 256); + + // Show that the local transactions have flushed through as well. + BEAST_EXPECT(env.seq(alice) == aliceSeq + 1); + env(noop(alice), ticket::use(tkt + 0), ter(tefNO_TICKET)); + env(noop(alice), ticket::use(tkt + 1), ter(tefNO_TICKET)); + tkt += 2; + } + { + // Put a blocker in an empty queue. + + // Fill up the open ledger and put a blocker as Alice's first + // entry in the (empty) TxQ. + env(noop(alice)); + env(noop(alice)); + + env(fset(alice, asfAccountTxnID), ticket::use(tkt + 2), queued); + + // Since there's a blocker in the queue we can't append to + // the queue. + env(noop(alice), + ticket::use(tkt + 1), + ter(telCAN_NOT_QUEUE_BLOCKED)); + + // Other accounts are unaffected. + env(noop(bob), queued); + + // We can replace the blocker with a non-blocker. Then we can + // successfully append to the queue. + env(noop(alice), ticket::use(tkt + 2), fee(20), queued); + env(noop(alice), ticket::use(tkt + 1), queued); + + // Drain the queue. + env.close(); + checkMetrics(env, 0, 12, 3, 6, 256); + } } void @@ -1773,7 +2308,9 @@ public: { using namespace jtx; using namespace std::chrono; - Env env(*this, supported_amendments().set(featureTickets)); + testcase("consequences"); + + Env env(*this, supported_amendments() | featureTicketBatch); auto const alice = Account("alice"); env.memoize(alice); env.memoize("bob"); @@ -1791,10 +2328,9 @@ public: tapNONE, env.journal); BEAST_EXPECT(pf.ter == tesSUCCESS); - auto const conseq = calculateConsequences(pf); - BEAST_EXPECT(conseq.category == TxConsequences::normal); - BEAST_EXPECT(conseq.fee == drops(10)); - BEAST_EXPECT(conseq.potentialSpend == XRP(0)); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(10)); + BEAST_EXPECT(pf.consequences.potentialSpend() == XRP(0)); } { @@ -1809,15 +2345,13 @@ public: tapNONE, env.journal); BEAST_EXPECT(pf.ter == tesSUCCESS); - auto const conseq = calculateConsequences(pf); - BEAST_EXPECT(conseq.category == TxConsequences::normal); - BEAST_EXPECT(conseq.fee == drops(10)); - BEAST_EXPECT(conseq.potentialSpend == XRP(0)); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(10)); + BEAST_EXPECT(pf.consequences.potentialSpend() == XRP(0)); } { - auto const jtx = - env.jt(ticket::create(alice, "bob", 60), seq(1), fee(10)); + auto const jtx = env.jt(ticket::create(alice, 1), seq(1), fee(10)); auto const pf = preflight( env.app(), env.current()->rules(), @@ -1825,36 +2359,123 @@ public: tapNONE, env.journal); BEAST_EXPECT(pf.ter == tesSUCCESS); - auto const conseq = calculateConsequences(pf); - BEAST_EXPECT(conseq.category == TxConsequences::normal); - BEAST_EXPECT(conseq.fee == drops(10)); - BEAST_EXPECT(conseq.potentialSpend == XRP(0)); + BEAST_EXPECT(!pf.consequences.isBlocker()); + BEAST_EXPECT(pf.consequences.fee() == drops(10)); + BEAST_EXPECT(pf.consequences.potentialSpend() == XRP(0)); } + } - { - Json::Value cancelTicket; - cancelTicket[jss::Account] = alice.human(); - cancelTicket["TicketID"] = to_string(uint256()); - cancelTicket[jss::TransactionType] = jss::TicketCancel; - auto const jtx = env.jt(cancelTicket, seq(1), fee(10)); - auto const pf = preflight( - env.app(), - env.current()->rules(), - *jtx.stx, - tapNONE, - env.journal); - BEAST_EXPECT(pf.ter == tesSUCCESS); - auto const conseq = calculateConsequences(pf); - BEAST_EXPECT(conseq.category == TxConsequences::normal); - BEAST_EXPECT(conseq.fee == drops(10)); - BEAST_EXPECT(conseq.potentialSpend == XRP(0)); - } + void + testAcctInQueueButEmpty() + { + // It is possible for an account to be present in the queue but have + // no queued transactions. This has been the source of at least one + // bug where an insufficiently informed developer assumed that if an + // account was present in the queue then it also had at least one + // queued transaction. + // + // This test does touch testing to verify that, at least, that bug + // is addressed. + using namespace jtx; + testcase("acct in queue but empty"); + + auto alice = Account("alice"); + auto bob = Account("bob"); + auto charlie = Account("charlie"); + + auto queued = ter(terQUEUED); + + Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); + + BEAST_EXPECT(env.current()->fees().base == 10); + + checkMetrics(env, 0, boost::none, 0, 3, 256); + + // Fund accounts while the fee is cheap so they all apply. + env.fund(XRP(50000), noripple(alice, bob, charlie)); + checkMetrics(env, 0, boost::none, 3, 3, 256); + + // Alice - no fee change yet + env(noop(alice)); + checkMetrics(env, 0, boost::none, 4, 3, 256); + + // Bob with really high fee - applies + env(noop(bob), openLedgerFee(env)); + checkMetrics(env, 0, boost::none, 5, 3, 256); + + // Charlie with low fee: queued + env(noop(charlie), fee(1000), queued); + checkMetrics(env, 1, boost::none, 5, 3, 256); + + env.close(); + // Verify that the queued transaction was applied + checkMetrics(env, 0, 10, 1, 5, 256); + + ///////////////////////////////////////////////////////////////// + + // Stuff the ledger and queue so we can verify that + // stuff gets kicked out. + env(noop(bob), fee(1000)); + env(noop(bob), fee(1000)); + env(noop(bob), fee(1000)); + env(noop(bob), fee(1000)); + env(noop(bob), fee(1000)); + checkMetrics(env, 0, 10, 6, 5, 256); + + // Use explicit fees so we can control which txn + // will get dropped + // This one gets into the queue, but gets dropped when the + // higher fee one is added later. + std::uint32_t const charlieSeq{env.seq(charlie)}; + env(noop(charlie), fee(15), seq(charlieSeq), queued); + + // These stay in the queue. + std::uint32_t aliceSeq{env.seq(alice)}; + std::uint32_t bobSeq{env.seq(bob)}; + + env(noop(alice), fee(16), seq(aliceSeq++), queued); + env(noop(bob), fee(16), seq(bobSeq++), queued); + env(noop(alice), fee(17), seq(aliceSeq++), queued); + env(noop(bob), fee(17), seq(bobSeq++), queued); + env(noop(alice), fee(18), seq(aliceSeq++), queued); + env(noop(bob), fee(19), seq(bobSeq++), queued); + env(noop(alice), fee(20), seq(aliceSeq++), queued); + env(noop(bob), fee(20), seq(bobSeq++), queued); + env(noop(alice), fee(21), seq(aliceSeq++), queued); + + // Queue is full now. + checkMetrics(env, 10, 10, 6, 5, 385); + + // Try to add another transaction with the default (low) fee, + // it should fail because the queue is full. + env(noop(alice), seq(aliceSeq++), ter(telCAN_NOT_QUEUE_FULL)); + + // Add another transaction, with a higher fee, + // not high enough to get into the ledger, but high + // enough to get into the queue (and kick Charlie's out) + env(noop(bob), fee(22), seq(bobSeq++), queued); + + ///////////////////////////////////////////////////////// + + // That was the setup for the actual test :-). Now make + // sure we get the right results if we try to add a + // transaction for Charlie (who's in the queue, but has no queued + // transactions) with the wrong sequence numbers. + // + // Charlie is paying a high enough fee to go straight into the + // ledger in order to get into the vicinity of an assert which + // should no longer fire :-). + env(noop(charlie), fee(8000), seq(charlieSeq - 1), ter(tefPAST_SEQ)); + env(noop(charlie), fee(8000), seq(charlieSeq + 1), ter(terPRE_SEQ)); + env(noop(charlie), fee(8000), seq(charlieSeq), ter(tesSUCCESS)); } void testRPC() { using namespace jtx; + testcase("rpc"); + Env env(*this); auto fee = env.rpc("fee"); @@ -1928,6 +2549,7 @@ public: the '22 in the queue did not have consequences. */ using namespace jtx; + testcase("expiration replacement"); Env env( *this, @@ -1985,26 +2607,23 @@ public: // Because aliceSeq is missing, aliceSeq + 1 fails env(noop(alice), seq(aliceSeq + 1), ter(terPRE_SEQ)); - // Queue up a new aliceSeq tx. - // This will only do some of the multiTx validation to - // improve the chances that the orphaned txs can be - // recovered. Because the cost of relaying the later txs - // has already been paid, this tx could potentially be a - // blocker. - env(fset(alice, asfAccountTxnID), seq(aliceSeq), ter(terQUEUED)); - checkMetrics(env, 3, 40, 5, 4, 256); + // Cannot fill the gap with a blocker since Alice's queue is not empty. + env(fset(alice, asfAccountTxnID), + seq(aliceSeq), + ter(telCAN_NOT_QUEUE_BLOCKS)); + checkMetrics(env, 2, 40, 5, 4, 256); - // Even though consequences were not computed, we can replace it. + // However we can fill the gap with a non-blocker. env(noop(alice), seq(aliceSeq), fee(20), ter(terQUEUED)); checkMetrics(env, 3, 40, 5, 4, 256); - // Queue up a new aliceSeq + 1 tx. - // This tx will also only do some of the multiTx validation. - env(fset(alice, asfAccountTxnID), seq(aliceSeq + 1), ter(terQUEUED)); - checkMetrics(env, 4, 40, 5, 4, 256); + // 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(env, 3, 40, 5, 4, 256); - // Even though consequences were not computed, we can replace it, - // too. + // Queue up a non-blocker replacement for aliceSeq + 1. env(noop(alice), seq(aliceSeq + 1), fee(20), ter(terQUEUED)); checkMetrics(env, 4, 40, 5, 4, 256); @@ -2016,6 +2635,166 @@ public: BEAST_EXPECT(env.seq(alice) == aliceSeq + 4); } + void + testFullQueueGapFill() + { + // This test focuses on which gaps in queued transactions are + // allowed to be filled even when the account's queue is full. + using namespace jtx; + testcase("full queue gap handling"); + + Env env( + *this, + makeConfig( + {{"minimum_txn_in_ledger_standalone", "1"}, + {"ledgers_in_queue", "10"}, + {"maximum_txn_per_account", "11"}}), + supported_amendments() | featureTicketBatch); + + // Alice will have the gaps. Bob will keep the queue busy with + // high fee transactions so alice's transactions can expire to leave + // gaps. + auto const alice = Account("alice"); + auto const bob = Account("bob"); + + env.fund(XRP(500000), noripple(alice, bob)); + checkMetrics(env, 0, boost::none, 2, 1, 256); + + auto const aliceSeq = env.seq(alice); + BEAST_EXPECT(env.current()->info().seq == 3); + + // Start by procuring tickets for alice to use to keep her queue full + // without affecting the sequence gap that will appear later. + env(ticket::create(alice, 11), + seq(aliceSeq + 0), + fee(201), + ter(terQUEUED)); + env(noop(alice), + seq(aliceSeq + 11), + json(R"({"LastLedgerSequence":11})"), + ter(terQUEUED)); + env(noop(alice), + seq(aliceSeq + 12), + json(R"({"LastLedgerSequence":11})"), + ter(terQUEUED)); + env(noop(alice), + seq(aliceSeq + 13), + json(R"({"LastLedgerSequence":11})"), + ter(terQUEUED)); + env(noop(alice), + seq(aliceSeq + 14), + json(R"({"LastLedgerSequence":11})"), + ter(terQUEUED)); + env(noop(alice), + seq(aliceSeq + 15), + json(R"({"LastLedgerSequence":11})"), + ter(terQUEUED)); + env(noop(alice), + seq(aliceSeq + 16), + json(R"({"LastLedgerSequence": 5})"), + ter(terQUEUED)); + env(noop(alice), + seq(aliceSeq + 17), + json(R"({"LastLedgerSequence": 5})"), + ter(terQUEUED)); + env(noop(alice), + seq(aliceSeq + 18), + json(R"({"LastLedgerSequence": 5})"), + ter(terQUEUED)); + env(noop(alice), + seq(aliceSeq + 19), + json(R"({"LastLedgerSequence":11})"), + ter(terQUEUED)); + checkMetrics(env, 10, boost::none, 2, 1, 256); + + auto const bobSeq = env.seq(bob); + // Ledger 4 gets 2 from bob and 1 from alice, + // Ledger 5 gets 4 from bob, + // Ledger 6 gets 5 from bob. + for (int i = 0; i < 2 + 4 + 5; ++i) + { + env(noop(bob), seq(bobSeq + i), fee(200), ter(terQUEUED)); + } + checkMetrics(env, 10 + 2 + 4 + 5, boost::none, 2, 1, 256); + // Close ledger 3 + env.close(); + checkMetrics(env, 9 + 4 + 5, 20, 3, 2, 256); + BEAST_EXPECT(env.seq(alice) == aliceSeq + 12); + + // Close ledger 4 + env.close(); + checkMetrics(env, 9 + 5, 30, 4, 3, 256); + BEAST_EXPECT(env.seq(alice) == aliceSeq + 12); + + // Close ledger 5 + env.close(); + // Three of Alice's txs expired. + checkMetrics(env, 6, 40, 5, 4, 256); + BEAST_EXPECT(env.seq(alice) == aliceSeq + 12); + + // Top off Alice's queue again using Tickets so the sequence gap is + // unaffected. + env(noop(alice), ticket::use(aliceSeq + 1), ter(terQUEUED)); + env(noop(alice), ticket::use(aliceSeq + 2), ter(terQUEUED)); + env(noop(alice), ticket::use(aliceSeq + 3), ter(terQUEUED)); + 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(env, 11, 40, 5, 4, 256); + + // Even though alice's queue is full we can still slide in a couple + // more transactions because she has a sequence gap. But we + // can only install a transaction that fills the bottom of the gap. + // Explore that... + + // Verify that we can't queue a sequence-based transaction that + // follows a gap. + env(noop(alice), seq(aliceSeq + 20), ter(telCAN_NOT_QUEUE_FULL)); + + // Verify that the transaction in front of the gap is still present + // by attempting to replace it without a sufficient fee. + env(noop(alice), seq(aliceSeq + 15), ter(telCAN_NOT_QUEUE_FEE)); + + // We can't queue a transaction into the middle of the gap. It must + // go at the front. + env(noop(alice), seq(aliceSeq + 18), ter(telCAN_NOT_QUEUE_FULL)); + env(noop(alice), seq(aliceSeq + 17), ter(telCAN_NOT_QUEUE_FULL)); + + // Successfully put this transaction into the front of the gap. + env(noop(alice), seq(aliceSeq + 16), ter(terQUEUED)); + + // Still can't put a sequence-based transaction at the end of the gap. + env(noop(alice), seq(aliceSeq + 18), ter(telCAN_NOT_QUEUE_FULL)); + + // But we can still fill the gap from the front. + env(noop(alice), seq(aliceSeq + 17), ter(terQUEUED)); + + // Finally we can fill in the entire gap. + env(noop(alice), seq(aliceSeq + 18), ter(terQUEUED)); + checkMetrics(env, 14, 40, 5, 4, 256); + + // Verify that nothing can be added now that the gap is filled. + env(noop(alice), seq(aliceSeq + 20), ter(telCAN_NOT_QUEUE_FULL)); + + // Close ledger 6. That removes 6 of alice's transactions, + // but alice adds one more transaction at seq(aliceSeq + 20) so + // we only see a reduction by 5. + env.close(); + checkMetrics(env, 9, 50, 6, 5, 256); + BEAST_EXPECT(env.seq(alice) == aliceSeq + 16); + + // Close ledger 7. That should remove 7 more of alice's transactions. + env.close(); + checkMetrics(env, 2, 60, 7, 6, 256); + BEAST_EXPECT(env.seq(alice) == aliceSeq + 19); + + // Close one last ledger to see all of alice's transactions moved + // into the ledger. + env.close(); + checkMetrics(env, 0, 70, 2, 7, 256); + BEAST_EXPECT(env.seq(alice) == aliceSeq + 21); + } + void testSignAndSubmitSequence() { @@ -2053,23 +2832,22 @@ public: checkMetrics(env, 5, boost::none, 7, 6, 256); { auto aliceStat = txQ.getAccountTxs(alice.id(), *env.current()); - auto seq = aliceSeq; + SeqProxy seq = SeqProxy::sequence(aliceSeq); BEAST_EXPECT(aliceStat.size() == 5); for (auto const& tx : aliceStat) { - BEAST_EXPECT(tx.first == seq); - BEAST_EXPECT(tx.second.feeLevel == FeeLevel64{25600}); - if (seq == aliceSeq + 2) + BEAST_EXPECT(tx.seqProxy == seq); + BEAST_EXPECT(tx.feeLevel == FeeLevel64{25600}); + if (seq.value() == aliceSeq + 2) { BEAST_EXPECT( - tx.second.lastValid && - *tx.second.lastValid == lastLedgerSeq); + tx.lastValid && *tx.lastValid == lastLedgerSeq); } else { - BEAST_EXPECT(!tx.second.lastValid); + BEAST_EXPECT(!tx.lastValid); } - ++seq; + seq.advanceBy(1); } } // Put some txs in the queue for bob. @@ -2112,9 +2890,9 @@ public: if (seq == aliceSeq + 2) ++seq; - BEAST_EXPECT(tx.first == seq); - BEAST_EXPECT(tx.second.feeLevel == FeeLevel64{25600}); - BEAST_EXPECT(!tx.second.lastValid); + BEAST_EXPECT(tx.seqProxy.isSeq() && tx.seqProxy.value() == seq); + BEAST_EXPECT(tx.feeLevel == FeeLevel64{25600}); + BEAST_EXPECT(!tx.lastValid); ++seq; } } @@ -2127,9 +2905,9 @@ public: BEAST_EXPECT(aliceStat.size() == 5); for (auto const& tx : aliceStat) { - BEAST_EXPECT(tx.first == seq); - BEAST_EXPECT(tx.second.feeLevel == FeeLevel64{25600}); - BEAST_EXPECT(!tx.second.lastValid); + BEAST_EXPECT(tx.seqProxy.isSeq() && tx.seqProxy.value() == seq); + BEAST_EXPECT(tx.feeLevel == FeeLevel64{25600}); + BEAST_EXPECT(!tx.lastValid); ++seq; } } @@ -2151,6 +2929,8 @@ public: testAccountInfo() { using namespace jtx; + testcase("account info"); + Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); Env_ss envs(env); @@ -2241,8 +3021,10 @@ public: queue_data[jss::highest_sequence] == data[jss::Sequence].asUInt() + queue_data[jss::txn_count].asUInt() - 1); - BEAST_EXPECT(!queue_data.isMember(jss::auth_change_queued)); - BEAST_EXPECT(!queue_data.isMember(jss::max_spend_drops_total)); + BEAST_EXPECT(queue_data.isMember(jss::auth_change_queued)); + BEAST_EXPECT(queue_data[jss::auth_change_queued] == false); + BEAST_EXPECT(queue_data.isMember(jss::max_spend_drops_total)); + BEAST_EXPECT(queue_data[jss::max_spend_drops_total] == "400"); BEAST_EXPECT(queue_data.isMember(jss::transactions)); auto const& queued = queue_data[jss::transactions]; BEAST_EXPECT(queued.size() == queue_data[jss::txn_count]); @@ -2253,32 +3035,29 @@ public: BEAST_EXPECT(item[jss::fee_level] == "2560"); BEAST_EXPECT(!item.isMember(jss::LastLedgerSequence)); - if (i == queued.size() - 1) - { - BEAST_EXPECT(!item.isMember(jss::fee)); - BEAST_EXPECT(!item.isMember(jss::max_spend_drops)); - BEAST_EXPECT(!item.isMember(jss::auth_change)); - } - else - { - BEAST_EXPECT(item.isMember(jss::fee)); - BEAST_EXPECT(item[jss::fee] == "100"); - BEAST_EXPECT(item.isMember(jss::max_spend_drops)); - BEAST_EXPECT(item[jss::max_spend_drops] == "100"); - BEAST_EXPECT(item.isMember(jss::auth_change)); - BEAST_EXPECT(!item[jss::auth_change].asBool()); - } + BEAST_EXPECT(item.isMember(jss::fee)); + BEAST_EXPECT(item[jss::fee] == "100"); + BEAST_EXPECT(item.isMember(jss::max_spend_drops)); + BEAST_EXPECT(item[jss::max_spend_drops] == "100"); + BEAST_EXPECT(item.isMember(jss::auth_change)); + BEAST_EXPECT(item[jss::auth_change].asBool() == false); } } - // Queue up a blocker + // Drain the queue so we can queue up a blocker. + env.close(); + checkMetrics(env, 0, 8, 4, 4, 256); + + // Fill the ledger and then queue up a blocker. + envs(noop(alice), seq(none))(submitParams); + envs( fset(alice, asfAccountTxnID), fee(100), seq(none), json(jss::LastLedgerSequence, 10), ter(terQUEUED))(submitParams); - checkMetrics(env, 5, 6, 4, 3, 256); + checkMetrics(env, 1, 8, 5, 4, 256); { auto const info = env.rpc("json", "account_info", withQueue); @@ -2291,7 +3070,7 @@ public: auto const& queue_data = result[jss::queue_data]; BEAST_EXPECT(queue_data.isObject()); BEAST_EXPECT(queue_data.isMember(jss::txn_count)); - BEAST_EXPECT(queue_data[jss::txn_count] == 5); + BEAST_EXPECT(queue_data[jss::txn_count] == 1); BEAST_EXPECT(queue_data.isMember(jss::lowest_sequence)); BEAST_EXPECT( queue_data[jss::lowest_sequence] == data[jss::Sequence]); @@ -2300,8 +3079,10 @@ public: queue_data[jss::highest_sequence] == data[jss::Sequence].asUInt() + queue_data[jss::txn_count].asUInt() - 1); - BEAST_EXPECT(!queue_data.isMember(jss::auth_change_queued)); - BEAST_EXPECT(!queue_data.isMember(jss::max_spend_drops_total)); + BEAST_EXPECT(queue_data.isMember(jss::auth_change_queued)); + BEAST_EXPECT(queue_data[jss::auth_change_queued] == true); + BEAST_EXPECT(queue_data.isMember(jss::max_spend_drops_total)); + BEAST_EXPECT(queue_data[jss::max_spend_drops_total] == "100"); BEAST_EXPECT(queue_data.isMember(jss::transactions)); auto const& queued = queue_data[jss::transactions]; BEAST_EXPECT(queued.size() == queue_data[jss::txn_count]); @@ -2310,23 +3091,21 @@ public: auto const& item = queued[i]; BEAST_EXPECT(item[jss::seq] == data[jss::Sequence].asInt() + i); BEAST_EXPECT(item[jss::fee_level] == "2560"); + BEAST_EXPECT(item.isMember(jss::fee)); + BEAST_EXPECT(item[jss::fee] == "100"); + BEAST_EXPECT(item.isMember(jss::max_spend_drops)); + BEAST_EXPECT(item[jss::max_spend_drops] == "100"); + BEAST_EXPECT(item.isMember(jss::auth_change)); if (i == queued.size() - 1) { - BEAST_EXPECT(!item.isMember(jss::fee)); - BEAST_EXPECT(!item.isMember(jss::max_spend_drops)); - BEAST_EXPECT(!item.isMember(jss::auth_change)); + BEAST_EXPECT(item[jss::auth_change].asBool() == true); BEAST_EXPECT(item.isMember(jss::LastLedgerSequence)); BEAST_EXPECT(item[jss::LastLedgerSequence] == 10); } else { - BEAST_EXPECT(item.isMember(jss::fee)); - BEAST_EXPECT(item[jss::fee] == "100"); - BEAST_EXPECT(item.isMember(jss::max_spend_drops)); - BEAST_EXPECT(item[jss::max_spend_drops] == "100"); - BEAST_EXPECT(item.isMember(jss::auth_change)); - BEAST_EXPECT(!item[jss::auth_change].asBool()); + BEAST_EXPECT(item[jss::auth_change].asBool() == false); BEAST_EXPECT(!item.isMember(jss::LastLedgerSequence)); } } @@ -2334,7 +3113,7 @@ public: envs(noop(alice), fee(100), seq(none), ter(telCAN_NOT_QUEUE_BLOCKED))( submitParams); - checkMetrics(env, 5, 6, 4, 3, 256); + checkMetrics(env, 1, 8, 5, 4, 256); { auto const info = env.rpc("json", "account_info", withQueue); @@ -2347,7 +3126,7 @@ public: auto const& queue_data = result[jss::queue_data]; BEAST_EXPECT(queue_data.isObject()); BEAST_EXPECT(queue_data.isMember(jss::txn_count)); - BEAST_EXPECT(queue_data[jss::txn_count] == 5); + BEAST_EXPECT(queue_data[jss::txn_count] == 1); BEAST_EXPECT(queue_data.isMember(jss::lowest_sequence)); BEAST_EXPECT( queue_data[jss::lowest_sequence] == data[jss::Sequence]); @@ -2359,7 +3138,7 @@ public: BEAST_EXPECT(queue_data.isMember(jss::auth_change_queued)); BEAST_EXPECT(queue_data[jss::auth_change_queued].asBool()); BEAST_EXPECT(queue_data.isMember(jss::max_spend_drops_total)); - BEAST_EXPECT(queue_data[jss::max_spend_drops_total] == "500"); + BEAST_EXPECT(queue_data[jss::max_spend_drops_total] == "100"); BEAST_EXPECT(queue_data.isMember(jss::transactions)); auto const& queued = queue_data[jss::transactions]; BEAST_EXPECT(queued.size() == queue_data[jss::txn_count]); @@ -2402,9 +3181,9 @@ public: } env.close(); - checkMetrics(env, 1, 8, 5, 4, 256); + checkMetrics(env, 0, 10, 2, 5, 256); env.close(); - checkMetrics(env, 0, 10, 1, 5, 256); + checkMetrics(env, 0, 10, 0, 5, 256); { auto const info = env.rpc("json", "account_info", withQueue); @@ -2429,6 +3208,8 @@ public: testServerInfo() { using namespace jtx; + testcase("server info"); + Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); Env_ss envs(env); @@ -2688,6 +3469,7 @@ public: testServerSubscribe() { using namespace jtx; + testcase("server subscribe"); Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); @@ -2832,6 +3614,7 @@ public: testClearQueuedAccountTxs() { using namespace jtx; + testcase("clear queued acct txs"); Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); auto alice = Account("alice"); @@ -2858,7 +3641,7 @@ public: metrics.medFeeLevel * totalFactor / (metrics.txPerLedger * metrics.txPerLedger), env.current()->fees().base) - .second.drops(); + .drops(); // Subtract the fees already paid result -= alreadyPaid; // round up @@ -2968,12 +3751,12 @@ public: auto const aliceQueue = env.app().getTxQ().getAccountTxs(alice.id(), *env.current()); BEAST_EXPECT(aliceQueue.size() == 2); - auto seq = aliceSeq; + SeqProxy seq = SeqProxy::sequence(aliceSeq); for (auto const& tx : aliceQueue) { - BEAST_EXPECT(tx.first == seq); - BEAST_EXPECT(tx.second.feeLevel == FeeLevel64{2560}); - ++seq; + BEAST_EXPECT(tx.seqProxy == seq); + BEAST_EXPECT(tx.feeLevel == FeeLevel64{2560}); + seq.advanceBy(1); } // Close the ledger to clear the queue @@ -3038,6 +3821,7 @@ public: { using namespace jtx; using namespace std::chrono_literals; + testcase("scaling"); { Env env( @@ -3172,31 +3956,541 @@ public: } } + void + testInLedgerSeq() + { + // Test the situation where a transaction with an account and + // sequence that's in the queue also appears in the ledger. + // + // Normally this situation can only happen on a network + // when a transaction gets validated by most of the network, + // but one or more nodes have that transaction (or a different + // transaction with the same sequence) queued. And, yes, this + // situation has been observed (rarely) in the wild. + testcase("Sequence in queue and open ledger"); + using namespace jtx; + + Env env(*this, makeConfig({{"minimum_txn_in_ledger_standalone", "3"}})); + + auto const alice = Account("alice"); + + auto const queued = ter(terQUEUED); + + BEAST_EXPECT(env.current()->fees().base == 10); + + checkMetrics(env, 0, boost::none, 0, 3, 256); + + // Create account + env.fund(XRP(50000), noripple(alice)); + checkMetrics(env, 0, boost::none, 1, 3, 256); + + fillQueue(env, alice); + checkMetrics(env, 0, boost::none, 4, 3, 256); + + // Queue a transaction + auto const aliceSeq = env.seq(alice); + env(noop(alice), queued); + checkMetrics(env, 1, boost::none, 4, 3, 256); + + // Now, apply a (different) transaction directly + // to the open ledger, bypassing the queue + // (This requires calling directly into the open ledger, + // which won't work if unit tests are separated to only + // be callable via RPC.) + env.app().openLedger().modify([&](OpenView& view, beast::Journal j) { + auto const tx = + env.jt(noop(alice), seq(aliceSeq), openLedgerFee(env)); + auto const result = + ripple::apply(env.app(), view, *tx.stx, tapUNLIMITED, j); + BEAST_EXPECT(result.first == tesSUCCESS && result.second); + return result.second; + }); + // the queued transaction is still there + checkMetrics(env, 1, boost::none, 5, 3, 256); + + // 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), openLedgerFee(env)); + checkMetrics(env, 1, boost::none, 6, 3, 256); + // 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(env, 3, boost::none, 6, 3, 256); + + // Now close the ledger. One of the queued transactions + // (aliceSeq) should be dropped. + env.close(); + checkMetrics(env, 0, 12, 2, 6, 256); + } + + void + testInLedgerTicket() + { + // Test the situation where a transaction with an account and + // ticket that's in the queue also appears in the ledger. + // + // Although this situation has not (yet) been observed in the wild, + // it is a direct analogy to the previous sequence based test. So + // there is no reason to not expect to see it in the wild. + testcase("Ticket in queue and open ledger"); + using namespace jtx; + + Env env( + *this, + makeConfig({{"minimum_txn_in_ledger_standalone", "3"}}), + supported_amendments() | featureTicketBatch); + + auto alice = Account("alice"); + + auto queued = ter(terQUEUED); + + BEAST_EXPECT(env.current()->fees().base == 10); + + checkMetrics(env, 0, boost::none, 0, 3, 256); + + // Create account + env.fund(XRP(50000), noripple(alice)); + checkMetrics(env, 0, boost::none, 1, 3, 256); + + // Create tickets + std::uint32_t const tktSeq0{env.seq(alice) + 1}; + env(ticket::create(alice, 4)); + + // Fill the queue so the next transaction will be queued. + fillQueue(env, alice); + checkMetrics(env, 0, boost::none, 4, 3, 256); + + // Queue a transaction with a ticket. Leave an unused ticket + // on either side. + env(noop(alice), ticket::use(tktSeq0 + 1), queued); + checkMetrics(env, 1, boost::none, 4, 3, 256); + + // Now, apply a (different) transaction directly + // to the open ledger, bypassing the queue + // (This requires calling directly into the open ledger, + // which won't work if unit tests are separated to only + // be callable via RPC.) + env.app().openLedger().modify([&](OpenView& view, beast::Journal j) { + auto const tx = env.jt( + noop(alice), ticket::use(tktSeq0 + 1), openLedgerFee(env)); + auto const result = + ripple::apply(env.app(), view, *tx.stx, tapUNLIMITED, j); + BEAST_EXPECT(result.first == tesSUCCESS && result.second); + return result.second; + }); + // the queued transaction is still there + checkMetrics(env, 1, boost::none, 5, 3, 256); + + // 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), openLedgerFee(env)); + checkMetrics(env, 1, boost::none, 6, 3, 256); + + // 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), openLedgerFee(env)); + checkMetrics(env, 1, boost::none, 7, 3, 256); + + // 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(env, 2, boost::none, 7, 3, 256); + + // Here we try to force the queued transactions into the ledger by + // adding one more queued (ticketed) transaction that pays enough + // so fee averaging kicks in. It doesn't work. It only succeeds in + // forcing just the one ticketed transaction into the ledger. + // + // The fee averaging functionality makes sense for sequence-based + // transactions because if there are several sequence-based + // transactions queued, the transactions in front must go into the + // ledger before the later ones can go in. + // + // Fee averaging does not make sense with tickets. Every ticketed + // 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(1))); + checkMetrics(env, 2, boost::none, 8, 3, 256); + + // Now close the ledger. One of the queued transactions + // (the one with tktSeq0 + 1) should be dropped. + env.close(); + checkMetrics(env, 0, 16, 1, 8, 256); + } + + void + testReexecutePreflight() + { + // The TxQ caches preflight results. But there are situations where + // that cache must become invalidated, like if amendments change. + // + // This test puts transactions into the TxQ and then enables an + // amendment. We won't really see much interesting here in the unit + // test, but the code that checks for cache invalidation should be + // exercised. You can see that in improved code coverage, + testcase("Re-execute preflight"); + using namespace jtx; + + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const daria("daria"); + Account const ellie("ellie"); + Account const fiona("fiona"); + + auto cfg = makeConfig( + {{"minimum_txn_in_ledger_standalone", "1"}, + {"ledgers_in_queue", "5"}, + {"maximum_txn_per_account", "10"}}, + {{"account_reserve", "200"}, {"owner_reserve", "50"}}); + + Env env(*this, std::move(cfg)); + + env.fund(XRP(10000), alice); + env.close(); + env.fund(XRP(10000), bob); + env.close(); + env.fund(XRP(10000), carol); + env.close(); + env.fund(XRP(10000), daria); + env.close(); + env.fund(XRP(10000), ellie); + env.close(); + env.fund(XRP(10000), fiona); + env.close(); + + // Close ledgers until the amendments show up. + int i = 0; + for (i = 0; i <= 257; ++i) + { + env.close(); + if (!getMajorityAmendments(*env.closed()).empty()) + break; + } + + // Now wait 2 weeks modulo 256 ledgers for the amendments to be + // enabled. Speed the process by closing ledgers every 80 minutes, + // which should get us to just past 2 weeks after 256 ledgers. + using namespace std::chrono_literals; + auto closeDuration = 80min; + for (i = 0; i <= 255; ++i) + env.close(closeDuration); + + // We're very close to the flag ledger. Fill the ledger. + fillQueue(env, alice); + checkMetrics(env, 0, 195, 40, 39, 256); + + // Fill everyone's queues. + auto seqAlice = env.seq(alice); + auto seqBob = env.seq(bob); + auto seqCarol = env.seq(carol); + auto seqDaria = env.seq(daria); + auto seqEllie = env.seq(ellie); + auto seqFiona = env.seq(fiona); + for (int i = 0; i < 10; ++i) + { + env(noop(alice), seq(seqAlice++), ter(terQUEUED)); + env(noop(bob), seq(seqBob++), ter(terQUEUED)); + env(noop(carol), seq(seqCarol++), ter(terQUEUED)); + env(noop(daria), seq(seqDaria++), ter(terQUEUED)); + env(noop(ellie), seq(seqEllie++), ter(terQUEUED)); + env(noop(fiona), seq(seqFiona++), ter(terQUEUED)); + } + checkMetrics(env, 60, 195, 40, 39, 256); + + // The next close should cause the in-ledger amendments to change. + // Alice's queued transactions have a cached PreflightResult + // that resulted from running against the Rules in the previous + // ledger. Since the amendments change in this newest ledger + // The TxQ must re-run preflight using the new rules. + // + // These particular amendments don't impact any of the queued + // transactions, so we won't see any change in the transaction + // outcomes. But code coverage is affected. + env.close(closeDuration); + checkMetrics(env, 19, 200, 41, 40, 256); + BEAST_EXPECT(env.seq(alice) == seqAlice - 3); + BEAST_EXPECT(env.seq(bob) == seqBob - 3); + BEAST_EXPECT(env.seq(carol) == seqCarol - 3); + BEAST_EXPECT(env.seq(daria) == seqDaria - 3); + BEAST_EXPECT(env.seq(ellie) == seqEllie - 3); + BEAST_EXPECT(env.seq(fiona) == seqFiona - 4); + + env.close(closeDuration); + checkMetrics(env, 0, 205, 19, 41, 256); + BEAST_EXPECT(env.seq(alice) == seqAlice); + BEAST_EXPECT(env.seq(bob) == seqBob); + BEAST_EXPECT(env.seq(carol) == seqCarol); + BEAST_EXPECT(env.seq(daria) == seqDaria); + BEAST_EXPECT(env.seq(ellie) == seqEllie); + BEAST_EXPECT(env.seq(fiona) == seqFiona); + } + + void + testQueueFullDropPenalty() + { + // If... + // o The queue is close to full, + // o An account has multiple txs queued, and + // o That same account has a transaction fail + // Then drop the last transaction for the account if possible. + // + // Verify that happens. + testcase("Queue full drop penalty"); + using namespace jtx; + + // Because we're looking at a phenomenon that occurs when the TxQ + // is at 95% capacity or greater, we need to have lots of entries + // in the queue. You can't even see 95% capacity unless there are + // 20 entries in the queue. + Account const alice("alice"); + Account const bob("bob"); + Account const carol("carol"); + Account const daria("daria"); + Account const ellie("ellie"); + Account const fiona("fiona"); + + // We'll be using fees to control which entries leave the queue in + // which order. There's no "lowFee" -- that's the default fee from + // the unit test. + auto const medFee = drops(15); + auto const hiFee = drops(1000); + + auto cfg = makeConfig( + {{"minimum_txn_in_ledger_standalone", "5"}, + {"ledgers_in_queue", "5"}, + {"maximum_txn_per_account", "30"}, + {"minimum_queue_size", "50"}}); + + Env env( + *this, std::move(cfg), supported_amendments() | featureTicketBatch); + + // The noripple is to reduce the number of transactions required to + // fund the accounts. There is no rippling in this test. + env.fund(XRP(10000), noripple(alice, bob, carol, daria, ellie, fiona)); + env.close(); + + // Get bob some tickets. + std::uint32_t const bobTicketSeq = env.seq(bob) + 1; + env(ticket::create(bob, 10)); + env.close(); + + // Get the dropPenalty flag set on alice and bob by having one + // of their transactions expire out of the queue. To start out + // alice fills the ledger. + fillQueue(env, alice); + checkMetrics(env, 0, 50, 7, 6, 256); + + // Now put a few transactions into alice's queue, including one that + // will expire out soon. + auto seqAlice = env.seq(alice); + auto const seqSaveAlice = seqAlice; + env(noop(alice), + seq(seqAlice++), + json(R"({"LastLedgerSequence": 7})"), + ter(terQUEUED)); + env(noop(alice), seq(seqAlice++), ter(terQUEUED)); + env(noop(alice), seq(seqAlice++), ter(terQUEUED)); + BEAST_EXPECT(env.seq(alice) == seqSaveAlice); + + // Similarly for bob, but bob uses tickets in his transactions. + // The drop penalty works a little differently with tickets. + env(noop(bob), + ticket::use(bobTicketSeq + 0), + json(R"({"LastLedgerSequence": 7})"), + ter(terQUEUED)); + env(noop(bob), ticket::use(bobTicketSeq + 1), ter(terQUEUED)); + env(noop(bob), ticket::use(bobTicketSeq + 2), ter(terQUEUED)); + + // Fill the queue with higher fee transactions so alice's and + // bob's transactions are stuck in the queue. + auto seqCarol = env.seq(carol); + auto seqDaria = env.seq(daria); + auto seqEllie = env.seq(ellie); + auto seqFiona = env.seq(fiona); + for (int i = 0; i < 7; ++i) + { + env(noop(carol), seq(seqCarol++), fee(medFee), ter(terQUEUED)); + env(noop(daria), seq(seqDaria++), fee(medFee), ter(terQUEUED)); + env(noop(ellie), seq(seqEllie++), fee(medFee), ter(terQUEUED)); + env(noop(fiona), seq(seqFiona++), fee(medFee), ter(terQUEUED)); + } + + checkMetrics(env, 34, 50, 7, 6, 256); + env.close(); + checkMetrics(env, 26, 50, 8, 7, 256); + + // Re-fill the queue so alice and bob stay stuck. + for (int i = 0; i < 3; ++i) + { + env(noop(carol), seq(seqCarol++), fee(medFee), ter(terQUEUED)); + env(noop(daria), seq(seqDaria++), fee(medFee), ter(terQUEUED)); + env(noop(ellie), seq(seqEllie++), fee(medFee), ter(terQUEUED)); + env(noop(fiona), seq(seqFiona++), fee(medFee), ter(terQUEUED)); + } + checkMetrics(env, 38, 50, 8, 7, 256); + env.close(); + checkMetrics(env, 29, 50, 9, 8, 256); + + // One more time... + for (int i = 0; i < 3; ++i) + { + env(noop(carol), seq(seqCarol++), fee(medFee), ter(terQUEUED)); + env(noop(daria), seq(seqDaria++), fee(medFee), ter(terQUEUED)); + env(noop(ellie), seq(seqEllie++), fee(medFee), ter(terQUEUED)); + env(noop(fiona), seq(seqFiona++), fee(medFee), ter(terQUEUED)); + } + checkMetrics(env, 41, 50, 9, 8, 256); + env.close(); + checkMetrics(env, 29, 50, 10, 9, 256); + + // 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 + // their accounts. + // + // This also means that alice has a sequence gap in her transactions, + // and thus can't queue any more. + env(noop(alice), seq(seqAlice), fee(hiFee), ter(telCAN_NOT_QUEUE)); + + // Once again, fill the queue almost to the brim. + for (int i = 0; i < 4; ++i) + { + env(noop(carol), seq(seqCarol++), ter(terQUEUED)); + env(noop(daria), seq(seqDaria++), ter(terQUEUED)); + env(noop(ellie), seq(seqEllie++), ter(terQUEUED)); + env(noop(fiona), seq(seqFiona++), ter(terQUEUED)); + } + env(noop(carol), seq(seqCarol++), ter(terQUEUED)); + env(noop(daria), seq(seqDaria++), ter(terQUEUED)); + env(noop(ellie), seq(seqEllie++), ter(terQUEUED)); + checkMetrics(env, 48, 50, 10, 9, 256); + + // Now induce a fee jump which should cause all the transactions + // in the queue to fail with telINSUF_FEE_P. + // + // *NOTE* raiseLocalFee() is tricky to use since the local fee is + // asynchronously lowered by LoadManager. Here we're just + // pushing the local fee up really high and then hoping that we + // outrace LoadManager undoing our work. + for (int i = 0; i < 10; ++i) + env.app().getFeeTrack().raiseLocalFee(); + + // Now close the ledger, which will attempt to process alice's + // and bob's queued transactions. + // 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(env, 46, 50, 0, 10, 256); + + // Run the local fee back down. + while (env.app().getFeeTrack().lowerLocalFee()) + ; + + // bob fills the ledger so it's easier to probe the TxQ. + fillQueue(env, bob); + checkMetrics(env, 46, 50, 11, 10, 256); + + // 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. + // + // 0. The transaction that used to be first in her queue expired + // out two env.close() calls back. That left a gap in alice's + // queue which has not been filled yet. + // + // 1. The first transaction in the queue failed to apply because + // of the sequence gap. But it is retained in the queue. + // + // 2. The last (second) transaction in alice's queue was removed + // as "punishment"... + // a) For already having a transaction expire out of her queue, and + // b) For just now having a queued transaction fail on apply() + // because of the sequence gap. + // + // Verify that none of alice's queued transactions actually applied to + // her account. + BEAST_EXPECT(env.seq(alice) == seqSaveAlice); + seqAlice = seqSaveAlice; + + // Verify that there's a gap at the front of alice's queue by + // queuing another low fee transaction into that spot. + env(noop(alice), seq(seqAlice++), ter(terQUEUED)); + + // Verify that the first entry in alice's queue is still there + // by trying to replace it and having that fail. + env(noop(alice), seq(seqAlice++), ter(telCAN_NOT_QUEUE_FEE)); + + // Verify that the last transaction in alice's queue was removed by + // appending to her queue with a very low fee. + env(noop(alice), seq(seqAlice++), ter(terQUEUED)); + + // Before the close() bob had two transactions in his queue. + // We now expect him to have one. Here's the state of bob's queue. + // + // 0. The transaction that used to be first in his queue expired out + // two env.close() calls back. That is how the dropPenalty flag + // got set on bob's queue. + // + // 1. Since bob's remaining transactions all have the same fee, the + // TxQ attempted to apply bob's second transaction to the ledger, + // but the fee was too low. So the TxQ threw that transaction + // (not bob's last transaction) out of the queue. + // + // 2. The last of bob's transactions remains in the TxQ. + + // Verify that bob's first transaction was removed from the queue + // by queueing another low fee transaction into that spot. + env(noop(bob), ticket::use(bobTicketSeq + 0), ter(terQUEUED)); + + // Verify that bob's second transaction was removed from the queue + // by queueing another low fee transaction into that spot. + env(noop(bob), ticket::use(bobTicketSeq + 1), ter(terQUEUED)); + + // Verify that the last entry in bob's queue is still there + // by trying to replace it and having that fail. + env(noop(bob), + ticket::use(bobTicketSeq + 2), + ter(telCAN_NOT_QUEUE_FEE)); + } + void run() override { - testQueue(); + testQueueSeq(); + testQueueTicket(); + testTecResult(); testLocalTxRetry(); testLastLedgerSeq(); testZeroFeeTxn(); - testPreclaimFailures(); - testQueuedFailure(); + testFailInPreclaim(); + testQueuedTxFails(); testMultiTxnPerAccount(); testTieBreaking(); testAcctTxnID(); testMaximum(); testUnexpectedBalanceChange(); - testBlockers(); + testBlockersSeq(); + testBlockersTicket(); testInFlightBalance(); testConsequences(); + testAcctInQueueButEmpty(); testRPC(); testExpirationReplacement(); + testFullQueueGapFill(); testSignAndSubmitSequence(); testAccountInfo(); testServerInfo(); testServerSubscribe(); testClearQueuedAccountTxs(); testScaling(); + testInLedgerSeq(); + testInLedgerTicket(); + testReexecutePreflight(); + testQueueFullDropPenalty(); } }; diff --git a/src/test/jtx/Env_test.cpp b/src/test/jtx/Env_test.cpp index fc67317d6..370cfbcac 100644 --- a/src/test/jtx/Env_test.cpp +++ b/src/test/jtx/Env_test.cpp @@ -480,20 +480,15 @@ public: { using namespace jtx; // create syntax - ticket::create("alice", "bob"); - ticket::create("alice", 60); - ticket::create("alice", "bob", 60); - ticket::create("alice", 60, "bob"); + ticket::create("alice", 1); { - Env env(*this, supported_amendments().set(featureTickets)); + Env env(*this, supported_amendments() | featureTicketBatch); env.fund(XRP(10000), "alice"); env(noop("alice"), require(owners("alice", 0), tickets("alice", 0))); - env(ticket::create("alice"), + env(ticket::create("alice", 1), require(owners("alice", 1), tickets("alice", 1))); - env(ticket::create("alice"), - require(owners("alice", 2), tickets("alice", 2))); } } diff --git a/src/test/jtx/impl/ticket.cpp b/src/test/jtx/impl/ticket.cpp index 31d02d758..951c62ab7 100644 --- a/src/test/jtx/impl/ticket.cpp +++ b/src/test/jtx/impl/ticket.cpp @@ -26,34 +26,22 @@ namespace jtx { namespace ticket { -namespace detail { - Json::Value -create( - Account const& account, - boost::optional const& target, - boost::optional const& expire) +create(Account const& account, std::uint32_t count) { Json::Value jv; jv[jss::Account] = account.human(); jv[jss::TransactionType] = jss::TicketCreate; - if (expire) - jv["Expiration"] = *expire; - if (target) - jv["Target"] = target->human(); + jv[sfTicketCount.jsonName] = count; return jv; } -} // namespace detail - -Json::Value -cancel(Account const& account, std::string const& ticketId) +void +use::operator()(Env&, JTx& jt) const { - Json::Value jv; - jv[jss::TransactionType] = jss::TicketCancel; - jv[jss::Account] = account.human(); - jv["TicketID"] = ticketId; - return jv; + jt.fill_seq = false; + jt[sfSequence.jsonName] = 0u; + jt[sfTicketSequence.jsonName] = ticketSeq_; } } // namespace ticket diff --git a/src/test/jtx/ticket.h b/src/test/jtx/ticket.h index 9906e00d7..2ddf295f9 100644 --- a/src/test/jtx/ticket.h +++ b/src/test/jtx/ticket.h @@ -39,62 +39,24 @@ namespace jtx { /** Ticket operations */ namespace ticket { -namespace detail { - +/** Create one of more tickets */ Json::Value -create( - Account const& account, - boost::optional const& target, - boost::optional const& expire); +create(Account const& account, std::uint32_t count); -inline void -create_arg( - boost::optional& opt, - boost::optional&, - Account const& value) +/** Set a ticket sequence on a JTx. */ +class use { - opt = value; -} +private: + std::uint32_t ticketSeq_; -inline void -create_arg( - boost::optional&, - boost::optional& opt, - std::uint32_t value) -{ - opt = value; -} +public: + use(std::uint32_t ticketSeq) : ticketSeq_{ticketSeq} + { + } -template -void -create_args( - boost::optional& account_opt, - boost::optional& expire_opt, - Arg const& arg, - Args const&... args) -{ - create_arg(account_opt, expire_opt, arg); - if constexpr (sizeof...(args)) - create_args(account_opt, expire_opt, args...); -} - -} // namespace detail - -/** Create a ticket */ -template -Json::Value -create(Account const& account, Args const&... args) -{ - boost::optional target; - boost::optional expire; - if constexpr (sizeof...(args) > 0) - detail::create_args(target, expire, args...); - return detail::create(account, target, expire); -} - -/** Cancel a ticket */ -Json::Value -cancel(Account const& account, std::string const& ticketId); + void + operator()(Env&, JTx& jt) const; +}; } // namespace ticket diff --git a/src/test/protocol/SeqProxy_test.cpp b/src/test/protocol/SeqProxy_test.cpp new file mode 100644 index 000000000..99a29dfe0 --- /dev/null +++ b/src/test/protocol/SeqProxy_test.cpp @@ -0,0 +1,240 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2018 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 { + +struct SeqProxy_test : public beast::unit_test::suite +{ + // Exercise value(), isSeq(), and isTicket(). + static constexpr bool + expectValues(SeqProxy seqProx, std::uint32_t value, SeqProxy::Type type) + { + bool const expectSeq{type == SeqProxy::seq}; + return (seqProx.value() == value) && (seqProx.isSeq() == expectSeq) && + (seqProx.isTicket() == !expectSeq); + } + + // Exercise all SeqProxy comparison operators expecting lhs < rhs. + static constexpr bool + expectLt(SeqProxy lhs, SeqProxy rhs) + { + return (lhs < rhs) && (lhs <= rhs) && (!(lhs == rhs)) && (lhs != rhs) && + (!(lhs >= rhs)) && (!(lhs > rhs)); + } + + // Exercise all SeqProxy comparison operators expecting lhs == rhs. + static constexpr bool + expectEq(SeqProxy lhs, SeqProxy rhs) + { + return (!(lhs < rhs)) && (lhs <= rhs) && (lhs == rhs) && + (!(lhs != rhs)) && (lhs >= rhs) && (!(lhs > rhs)); + } + + // Exercise all SeqProxy comparison operators expecting lhs > rhs. + static constexpr bool + expectGt(SeqProxy lhs, SeqProxy rhs) + { + return (!(lhs < rhs)) && (!(lhs <= rhs)) && (!(lhs == rhs)) && + (lhs != rhs) && (lhs >= rhs) && (lhs > rhs); + } + + // Verify streaming. + bool + streamTest(SeqProxy seqProx) + { + std::string const type{seqProx.isSeq() ? "sequence" : "ticket"}; + std::string const value{std::to_string(seqProx.value())}; + + std::stringstream ss; + ss << seqProx; + std::string str{ss.str()}; + + return str.find(type) == 0 && str[type.size()] == ' ' && + str.find(value) == (type.size() + 1); + } + + void + run() override + { + // While SeqProxy supports values of zero, they are not + // expected in the wild. Nevertheless they are tested here. + // But so are values of 1, which are expected to occur in the wild. + static constexpr std::uint32_t uintMax{ + std::numeric_limits::max()}; + static constexpr SeqProxy::Type seq{SeqProxy::seq}; + static constexpr SeqProxy::Type ticket{SeqProxy::ticket}; + + static constexpr SeqProxy seqZero{seq, 0}; + static constexpr SeqProxy seqSmall{seq, 1}; + static constexpr SeqProxy seqMid0{seq, 2}; + static constexpr SeqProxy seqMid1{seqMid0}; + static constexpr SeqProxy seqBig{seq, uintMax}; + + static constexpr SeqProxy ticZero{ticket, 0}; + static constexpr SeqProxy ticSmall{ticket, 1}; + static constexpr SeqProxy ticMid0{ticket, 2}; + static constexpr SeqProxy ticMid1{ticMid0}; + static constexpr SeqProxy ticBig{ticket, uintMax}; + + // Verify operation of value(), isSeq() and isTicket(). + static_assert(expectValues(seqZero, 0, seq), ""); + static_assert(expectValues(seqSmall, 1, seq), ""); + static_assert(expectValues(seqMid0, 2, seq), ""); + static_assert(expectValues(seqMid1, 2, seq), ""); + static_assert(expectValues(seqBig, uintMax, seq), ""); + + static_assert(expectValues(ticZero, 0, ticket), ""); + static_assert(expectValues(ticSmall, 1, ticket), ""); + static_assert(expectValues(ticMid0, 2, ticket), ""); + static_assert(expectValues(ticMid1, 2, ticket), ""); + static_assert(expectValues(ticBig, uintMax, ticket), ""); + + // Verify expected behavior of comparison operators. + static_assert(expectEq(seqZero, seqZero), ""); + static_assert(expectLt(seqZero, seqSmall), ""); + static_assert(expectLt(seqZero, seqMid0), ""); + static_assert(expectLt(seqZero, seqMid1), ""); + static_assert(expectLt(seqZero, seqBig), ""); + static_assert(expectLt(seqZero, ticZero), ""); + static_assert(expectLt(seqZero, ticSmall), ""); + static_assert(expectLt(seqZero, ticMid0), ""); + static_assert(expectLt(seqZero, ticMid1), ""); + static_assert(expectLt(seqZero, ticBig), ""); + + static_assert(expectGt(seqSmall, seqZero), ""); + static_assert(expectEq(seqSmall, seqSmall), ""); + static_assert(expectLt(seqSmall, seqMid0), ""); + static_assert(expectLt(seqSmall, seqMid1), ""); + static_assert(expectLt(seqSmall, seqBig), ""); + static_assert(expectLt(seqSmall, ticZero), ""); + static_assert(expectLt(seqSmall, ticSmall), ""); + static_assert(expectLt(seqSmall, ticMid0), ""); + static_assert(expectLt(seqSmall, ticMid1), ""); + static_assert(expectLt(seqSmall, ticBig), ""); + + static_assert(expectGt(seqMid0, seqZero), ""); + static_assert(expectGt(seqMid0, seqSmall), ""); + static_assert(expectEq(seqMid0, seqMid0), ""); + static_assert(expectEq(seqMid0, seqMid1), ""); + static_assert(expectLt(seqMid0, seqBig), ""); + static_assert(expectLt(seqMid0, ticZero), ""); + static_assert(expectLt(seqMid0, ticSmall), ""); + static_assert(expectLt(seqMid0, ticMid0), ""); + static_assert(expectLt(seqMid0, ticMid1), ""); + static_assert(expectLt(seqMid0, ticBig), ""); + + static_assert(expectGt(seqMid1, seqZero), ""); + static_assert(expectGt(seqMid1, seqSmall), ""); + static_assert(expectEq(seqMid1, seqMid0), ""); + static_assert(expectEq(seqMid1, seqMid1), ""); + static_assert(expectLt(seqMid1, seqBig), ""); + static_assert(expectLt(seqMid1, ticZero), ""); + static_assert(expectLt(seqMid1, ticSmall), ""); + static_assert(expectLt(seqMid1, ticMid0), ""); + static_assert(expectLt(seqMid1, ticMid1), ""); + static_assert(expectLt(seqMid1, ticBig), ""); + + static_assert(expectGt(seqBig, seqZero), ""); + static_assert(expectGt(seqBig, seqSmall), ""); + static_assert(expectGt(seqBig, seqMid0), ""); + static_assert(expectGt(seqBig, seqMid1), ""); + static_assert(expectEq(seqBig, seqBig), ""); + static_assert(expectLt(seqBig, ticZero), ""); + static_assert(expectLt(seqBig, ticSmall), ""); + static_assert(expectLt(seqBig, ticMid0), ""); + static_assert(expectLt(seqBig, ticMid1), ""); + static_assert(expectLt(seqBig, ticBig), ""); + + static_assert(expectGt(ticZero, seqZero), ""); + static_assert(expectGt(ticZero, seqSmall), ""); + static_assert(expectGt(ticZero, seqMid0), ""); + static_assert(expectGt(ticZero, seqMid1), ""); + static_assert(expectGt(ticZero, seqBig), ""); + static_assert(expectEq(ticZero, ticZero), ""); + static_assert(expectLt(ticZero, ticSmall), ""); + static_assert(expectLt(ticZero, ticMid0), ""); + static_assert(expectLt(ticZero, ticMid1), ""); + static_assert(expectLt(ticZero, ticBig), ""); + + static_assert(expectGt(ticSmall, seqZero), ""); + static_assert(expectGt(ticSmall, seqSmall), ""); + static_assert(expectGt(ticSmall, seqMid0), ""); + static_assert(expectGt(ticSmall, seqMid1), ""); + static_assert(expectGt(ticSmall, seqBig), ""); + static_assert(expectGt(ticSmall, ticZero), ""); + static_assert(expectEq(ticSmall, ticSmall), ""); + static_assert(expectLt(ticSmall, ticMid0), ""); + static_assert(expectLt(ticSmall, ticMid1), ""); + static_assert(expectLt(ticSmall, ticBig), ""); + + static_assert(expectGt(ticMid0, seqZero), ""); + static_assert(expectGt(ticMid0, seqSmall), ""); + static_assert(expectGt(ticMid0, seqMid0), ""); + static_assert(expectGt(ticMid0, seqMid1), ""); + static_assert(expectGt(ticMid0, seqBig), ""); + static_assert(expectGt(ticMid0, ticZero), ""); + static_assert(expectGt(ticMid0, ticSmall), ""); + static_assert(expectEq(ticMid0, ticMid0), ""); + static_assert(expectEq(ticMid0, ticMid1), ""); + static_assert(expectLt(ticMid0, ticBig), ""); + + static_assert(expectGt(ticMid1, seqZero), ""); + static_assert(expectGt(ticMid1, seqSmall), ""); + static_assert(expectGt(ticMid1, seqMid0), ""); + static_assert(expectGt(ticMid1, seqMid1), ""); + static_assert(expectGt(ticMid1, seqBig), ""); + static_assert(expectGt(ticMid1, ticZero), ""); + static_assert(expectGt(ticMid1, ticSmall), ""); + static_assert(expectEq(ticMid1, ticMid0), ""); + static_assert(expectEq(ticMid1, ticMid1), ""); + static_assert(expectLt(ticMid1, ticBig), ""); + + static_assert(expectGt(ticBig, seqZero), ""); + static_assert(expectGt(ticBig, seqSmall), ""); + static_assert(expectGt(ticBig, seqMid0), ""); + static_assert(expectGt(ticBig, seqMid1), ""); + static_assert(expectGt(ticBig, seqBig), ""); + static_assert(expectGt(ticBig, ticZero), ""); + static_assert(expectGt(ticBig, ticSmall), ""); + static_assert(expectGt(ticBig, ticMid0), ""); + static_assert(expectGt(ticBig, ticMid1), ""); + static_assert(expectEq(ticBig, ticBig), ""); + + // Verify streaming. + BEAST_EXPECT(streamTest(seqZero)); + BEAST_EXPECT(streamTest(seqSmall)); + BEAST_EXPECT(streamTest(seqMid0)); + BEAST_EXPECT(streamTest(seqMid1)); + BEAST_EXPECT(streamTest(seqBig)); + BEAST_EXPECT(streamTest(ticZero)); + BEAST_EXPECT(streamTest(ticSmall)); + BEAST_EXPECT(streamTest(ticMid0)); + BEAST_EXPECT(streamTest(ticMid1)); + BEAST_EXPECT(streamTest(ticBig)); + } +}; + +BEAST_DEFINE_TESTSUITE(SeqProxy, protocol, ripple); + +} // namespace ripple diff --git a/src/test/rpc/AccountObjects_test.cpp b/src/test/rpc/AccountObjects_test.cpp index f60beedaf..8f77b6ce9 100644 --- a/src/test/rpc/AccountObjects_test.cpp +++ b/src/test/rpc/AccountObjects_test.cpp @@ -349,8 +349,7 @@ public: Account const gw{"gateway"}; auto const USD = gw["USD"]; - // Test for ticket account objects when they are supported. - Env env(*this, supported_amendments().set(featureTickets)); + Env env(*this, supported_amendments() | featureTicketBatch); // Make a lambda we can use to get "account_objects" easily. auto acct_objs = [&env](Account const& acct, char const* type) { @@ -504,7 +503,7 @@ public: BEAST_EXPECT(entry[sfSignerWeight.jsonName].asUInt() == 7); } // Create a Ticket for gw. - env(ticket::create(gw, gw)); + env(ticket::create(gw, 1)); env.close(); { // Find the ticket. @@ -514,7 +513,7 @@ public: auto const& ticket = resp[jss::result][jss::account_objects][0u]; BEAST_EXPECT(ticket[sfAccount.jsonName] == gw.human()); BEAST_EXPECT(ticket[sfLedgerEntryType.jsonName] == jss::Ticket); - BEAST_EXPECT(ticket[sfSequence.jsonName].asUInt() == 11); + BEAST_EXPECT(ticket[sfTicketSequence.jsonName].asUInt() == 12); } { // See how "deletion_blockers_only" handles gw's directory. diff --git a/src/test/rpc/AccountSet_test.cpp b/src/test/rpc/AccountSet_test.cpp index ed9c46d12..b9745f3ec 100644 --- a/src/test/rpc/AccountSet_test.cpp +++ b/src/test/rpc/AccountSet_test.cpp @@ -495,6 +495,46 @@ public: BEAST_EXPECT(!dirIsEmpty(*env.closed(), keylet::ownerDir(alice))); env(fset(alice, asfRequireAuth), ter(tecOWNERS)); + + // Remove the signer list. After that asfRequireAuth should succeed. + env(signers(alice, test::jtx::none)); + env.close(); + BEAST_EXPECT(dirIsEmpty(*env.closed(), keylet::ownerDir(alice))); + + env(fset(alice, asfRequireAuth)); + } + + void + testTicket() + { + using namespace test::jtx; + Env env(*this, supported_amendments() | featureTicketBatch); + Account const alice("alice"); + + env.fund(XRP(10000), alice); + env.close(); + + std::uint32_t const ticketSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 1)); + env.close(); + env.require(owners(alice, 1), tickets(alice, 1)); + + // Try using a ticket that alice doesn't have. + env(noop(alice), ticket::use(ticketSeq + 1), ter(terPRE_TICKET)); + env.close(); + env.require(owners(alice, 1), tickets(alice, 1)); + + // Actually use alice's ticket. Note that if a transaction consumes + // a ticket then the account's sequence number does not advance. + std::uint32_t const aliceSeq{env.seq(alice)}; + env(noop(alice), ticket::use(ticketSeq)); + env.close(); + env.require(owners(alice, 0), tickets(alice, 0)); + BEAST_EXPECT(aliceSeq == env.seq(alice)); + + // Try re-using a ticket that alice already used. + env(noop(alice), ticket::use(ticketSeq), ter(tefNO_TICKET)); + env.close(); } void @@ -512,6 +552,7 @@ public: testBadInputs(); testRequireAuthWithDir(); testTransferRate(); + testTicket(); } }; diff --git a/src/test/rpc/AccountTx_test.cpp b/src/test/rpc/AccountTx_test.cpp index aa40d6c0b..c6c168c60 100644 --- a/src/test/rpc/AccountTx_test.cpp +++ b/src/test/rpc/AccountTx_test.cpp @@ -252,7 +252,7 @@ class AccountTx_test : public beast::unit_test::suite using namespace test::jtx; using namespace std::chrono_literals; - Env env(*this); + Env env(*this, supported_amendments() | featureTicketBatch); Account const alice{"alice"}; Account const alie{"alie"}; Account const gw{"gw"}; @@ -399,10 +399,15 @@ class AccountTx_test : public beast::unit_test::suite env(check::cancel(alice, aliceCheckId), sig(alie)); env.close(); } + { + // Deposit preauthorization with a Ticket. + std::uint32_t const tktSeq{env.seq(alice) + 1}; + env(ticket::create(alice, 1), sig(alie)); + env.close(); - // Deposit preauthorization. - env(deposit::auth(alice, gw), sig(alie)); - env.close(); + env(deposit::auth(alice, gw), ticket::use(tktSeq), sig(alie)); + env.close(); + } // Setup is done. Look at the transactions returned by account_tx. Json::Value params; @@ -423,27 +428,28 @@ class AccountTx_test : public beast::unit_test::suite // be returned in the reverse order of application to the ledger. static const NodeSanity sanity[]{ // txType, created, deleted, modified - { 0, jss::DepositPreauth, {jss::DepositPreauth}, {}, {jss::AccountRoot, jss::DirectoryNode}}, - { 1, jss::CheckCancel, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, - { 2, jss::CheckCash, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, - { 3, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, - { 4, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, - { 5, jss::PaymentChannelClaim, {}, {jss::PayChannel}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, - { 6, jss::PaymentChannelFund, {}, {}, {jss::AccountRoot, jss::PayChannel }}, - { 7, jss::PaymentChannelCreate, {jss::PayChannel}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, - { 8, jss::EscrowCancel, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}}, - { 9, jss::EscrowFinish, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}}, - { 10, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}}, - { 11, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}}, - { 12, jss::SignerListSet, {jss::SignerList}, {}, {jss::AccountRoot, jss::DirectoryNode}}, - { 13, jss::OfferCancel, {}, {jss::Offer, jss::DirectoryNode}, {jss::AccountRoot, jss::DirectoryNode}}, - { 14, jss::OfferCreate, {jss::Offer, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::DirectoryNode}}, - { 15, jss::TrustSet, {jss::RippleState, jss::DirectoryNode, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::AccountRoot}}, - { 16, jss::SetRegularKey, {}, {}, {jss::AccountRoot}}, - { 17, jss::Payment, {}, {}, {jss::AccountRoot, jss::AccountRoot}}, - { 18, jss::AccountSet, {}, {}, {jss::AccountRoot}}, - { 19, jss::AccountSet, {}, {}, {jss::AccountRoot}}, - { 20, jss::Payment, {jss::AccountRoot}, {}, {jss::AccountRoot}}, + {0, jss::DepositPreauth, {jss::DepositPreauth}, {jss::Ticket}, {jss::AccountRoot, jss::DirectoryNode}}, + {1, jss::TicketCreate, {jss::Ticket}, {}, {jss::AccountRoot, jss::DirectoryNode}}, + {2, jss::CheckCancel, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, + {3, jss::CheckCash, {}, {jss::Check}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, + {4, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, + {5, jss::CheckCreate, {jss::Check}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, + {6, jss::PaymentChannelClaim, {}, {jss::PayChannel}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, + {7, jss::PaymentChannelFund, {}, {}, {jss::AccountRoot, jss::PayChannel}}, + {8, jss::PaymentChannelCreate, {jss::PayChannel}, {}, {jss::AccountRoot, jss::AccountRoot, jss::DirectoryNode, jss::DirectoryNode}}, + {9, jss::EscrowCancel, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}}, + {10, jss::EscrowFinish, {}, {jss::Escrow}, {jss::AccountRoot, jss::DirectoryNode}}, + {11, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}}, + {12, jss::EscrowCreate, {jss::Escrow}, {}, {jss::AccountRoot, jss::DirectoryNode}}, + {13, jss::SignerListSet, {jss::SignerList}, {}, {jss::AccountRoot, jss::DirectoryNode}}, + {14, jss::OfferCancel, {}, {jss::Offer, jss::DirectoryNode}, {jss::AccountRoot, jss::DirectoryNode}}, + {15, jss::OfferCreate, {jss::Offer, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::DirectoryNode}}, + {16, jss::TrustSet, {jss::RippleState, jss::DirectoryNode, jss::DirectoryNode}, {}, {jss::AccountRoot, jss::AccountRoot}}, + {17, jss::SetRegularKey, {}, {}, {jss::AccountRoot}}, + {18, jss::Payment, {}, {}, {jss::AccountRoot, jss::AccountRoot}}, + {19, jss::AccountSet, {}, {}, {jss::AccountRoot}}, + {20, jss::AccountSet, {}, {}, {jss::AccountRoot}}, + {21, jss::Payment, {jss::AccountRoot}, {}, {jss::AccountRoot}}, }; // clang-format on diff --git a/src/test/rpc/Fee_test.cpp b/src/test/rpc/Fee_test.cpp index 98f8541bc..635a879f5 100644 --- a/src/test/rpc/Fee_test.cpp +++ b/src/test/rpc/Fee_test.cpp @@ -114,17 +114,14 @@ class Fee_test : public beast::unit_test::suite auto const baseFee = view->fees().base; BEAST_EXPECT( fee.base_fee().drops() == - toDrops(metrics.referenceFeeLevel, baseFee).second); + toDrops(metrics.referenceFeeLevel, baseFee)); BEAST_EXPECT( fee.minimum_fee().drops() == - toDrops(metrics.minProcessingFeeLevel, baseFee).second); + toDrops(metrics.minProcessingFeeLevel, baseFee)); BEAST_EXPECT( - fee.median_fee().drops() == - toDrops(metrics.medFeeLevel, baseFee).second); + fee.median_fee().drops() == toDrops(metrics.medFeeLevel, baseFee)); auto openLedgerFee = - toDrops(metrics.openLedgerFeeLevel - FeeLevel64{1}, baseFee) - .second + - 1; + toDrops(metrics.openLedgerFeeLevel - FeeLevel64{1}, baseFee) + 1; BEAST_EXPECT(fee.open_ledger_fee().drops() == openLedgerFee.drops()); } diff --git a/src/test/rpc/LedgerData_test.cpp b/src/test/rpc/LedgerData_test.cpp index b0d3c5a72..05e0876c6 100644 --- a/src/test/rpc/LedgerData_test.cpp +++ b/src/test/rpc/LedgerData_test.cpp @@ -312,7 +312,7 @@ public: Env env{ *this, envconfig(validator, ""), - supported_amendments().set(featureTickets)}; + supported_amendments() | featureTicketBatch}; Account const gw{"gateway"}; auto const USD = gw["USD"]; @@ -338,7 +338,7 @@ public: } env(signers( Account{"bob0"}, 1, {{Account{"bob1"}, 1}, {Account{"bob2"}, 1}})); - env(ticket::create(env.master)); + env(ticket::create(env.master, 1)); { Json::Value jv; diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index ee007e32a..2992e0f15 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -393,7 +393,7 @@ class LedgerRPC_test : public beast::unit_test::suite void testLedgerEntryDepositPreauth() { - testcase("ledger_entry Request Directory"); + testcase("ledger_entry Request DepositPreauth"); using namespace test::jtx; Env env{*this}; Account const alice{"alice"}; @@ -1074,6 +1074,124 @@ class LedgerRPC_test : public beast::unit_test::suite } } + void + testLedgerEntryTicket() + { + testcase("ledger_entry Request Ticket"); + using namespace test::jtx; + Env env{*this, supported_amendments() | featureTicketBatch}; + env.close(); + + // Create two tickets. + std::uint32_t const tkt1{env.seq(env.master) + 1}; + env(ticket::create(env.master, 2)); + env.close(); + + std::string const ledgerHash{to_string(env.closed()->info().hash)}; + // Request four tickets: one before the first one we created, the + // two created tickets, and the ticket that would come after the + // last created ticket. + { + // Not a valid ticket requested by index. + Json::Value jvParams; + jvParams[jss::ticket] = + to_string(getTicketIndex(env.master, tkt1 - 1)); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", ""); + } + { + // First real ticket requested by index. + Json::Value jvParams; + jvParams[jss::ticket] = to_string(getTicketIndex(env.master, tkt1)); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][sfLedgerEntryType.jsonName] == jss::Ticket); + BEAST_EXPECT(jrr[jss::node][sfTicketSequence.jsonName] == tkt1); + } + { + // Second real ticket requested by account and sequence. + Json::Value jvParams; + jvParams[jss::ticket] = Json::objectValue; + jvParams[jss::ticket][jss::account] = env.master.human(); + jvParams[jss::ticket][jss::ticket_seq] = tkt1 + 1; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + BEAST_EXPECT( + jrr[jss::node][jss::index] == + to_string(getTicketIndex(env.master, tkt1 + 1))); + } + { + // Not a valid ticket requested by account and sequence. + Json::Value jvParams; + jvParams[jss::ticket] = Json::objectValue; + jvParams[jss::ticket][jss::account] = env.master.human(); + jvParams[jss::ticket][jss::ticket_seq] = tkt1 + 2; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "entryNotFound", ""); + } + { + // Request a ticket using an account root entry. + Json::Value jvParams; + jvParams[jss::ticket] = to_string(keylet::account(env.master).key); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedRequest", ""); + } + { + // Malformed account entry. + Json::Value jvParams; + jvParams[jss::ticket] = Json::objectValue; + + std::string const badAddress = makeBadAddress(env.master.human()); + jvParams[jss::ticket][jss::account] = badAddress; + jvParams[jss::ticket][jss::ticket_seq] = env.seq(env.master) - 1; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedAddress", ""); + } + { + // Malformed ticket object. Missing account member. + Json::Value jvParams; + jvParams[jss::ticket] = Json::objectValue; + jvParams[jss::ticket][jss::ticket_seq] = env.seq(env.master) - 1; + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedRequest", ""); + } + { + // Malformed ticket object. Missing seq member. + Json::Value jvParams; + jvParams[jss::ticket] = Json::objectValue; + jvParams[jss::ticket][jss::account] = env.master.human(); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedRequest", ""); + } + { + // Malformed ticket object. Non-integral seq member. + Json::Value jvParams; + jvParams[jss::ticket] = Json::objectValue; + jvParams[jss::ticket][jss::account] = env.master.human(); + jvParams[jss::ticket][jss::ticket_seq] = + std::to_string(env.seq(env.master) - 1); + jvParams[jss::ledger_hash] = ledgerHash; + Json::Value const jrr = env.rpc( + "json", "ledger_entry", to_string(jvParams))[jss::result]; + checkErrorValue(jrr, "malformedRequest", ""); + } + } + void testLedgerEntryUnknownOption() { @@ -1560,6 +1678,7 @@ public: testLedgerEntryOffer(); testLedgerEntryPayChan(); testLedgerEntryRippleState(); + testLedgerEntryTicket(); testLedgerEntryUnknownOption(); testLookupLedger(); testNoQueue(); diff --git a/src/test/rpc/NoRippleCheck_test.cpp b/src/test/rpc/NoRippleCheck_test.cpp index 5b05b1dcd..73934899e 100644 --- a/src/test/rpc/NoRippleCheck_test.cpp +++ b/src/test/rpc/NoRippleCheck_test.cpp @@ -297,23 +297,20 @@ class NoRippleCheckLimits_test : public beast::unit_test::suite seq(autofill), fee(toDrops( txq.getMetrics(*env.current()).openLedgerFeeLevel, - baseFee) - .second + + baseFee) + 1), sig(autofill)); env(fset(gw, asfDefaultRipple), seq(autofill), fee(toDrops( txq.getMetrics(*env.current()).openLedgerFeeLevel, - baseFee) - .second + + baseFee) + 1), sig(autofill)); env(trust(alice, gw["USD"](10)), fee(toDrops( txq.getMetrics(*env.current()).openLedgerFeeLevel, - baseFee) - .second + + baseFee) + 1)); env.close(); } diff --git a/src/test/rpc/RobustTransaction_test.cpp b/src/test/rpc/RobustTransaction_test.cpp index 497c878c9..37b16c58d 100644 --- a/src/test/rpc/RobustTransaction_test.cpp +++ b/src/test/rpc/RobustTransaction_test.cpp @@ -2,9 +2,11 @@ /* This file is part of rippled: https://github.com/ripple/rippled Copyright (c) 2012, 2013 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 diff --git a/src/test/rpc/Submit_test.cpp b/src/test/rpc/Submit_test.cpp index 2c7e2707b..bd8047372 100644 --- a/src/test/rpc/Submit_test.cpp +++ b/src/test/rpc/Submit_test.cpp @@ -185,8 +185,9 @@ public: { return; } - BEAST_EXPECT(client.reply.engine_result().result() == "tefALREADY"); - BEAST_EXPECT(client.reply.engine_result_code() == -198); + BEAST_EXPECT( + client.reply.engine_result().result() == "tefPAST_SEQ"); + BEAST_EXPECT(client.reply.engine_result_code() == -190); } }