From 72657294462ebff63ee226c3cbb3fba75f8a9058 Mon Sep 17 00:00:00 2001 From: Edward Hennis Date: Mon, 13 Feb 2017 21:21:28 -0500 Subject: [PATCH] TxQ full queue RPC info (RIPD-1404): * RPC `ledger` command returns all queue entries in "queue_data" when requesting open ledger, and including boolean "queue: true". * Includes queue state. e.g.: fee_level, retries, last_result, tx. * Respects "expand" and "binary" parameters for the txs. * Remove some unused code. --- src/ripple/app/ledger/LedgerToJson.h | 21 +- src/ripple/app/ledger/impl/LedgerToJson.cpp | 132 ++++++++---- src/ripple/app/misc/TxQ.h | 22 +- src/ripple/app/misc/impl/TxQ.cpp | 67 +++++- src/ripple/rpc/RPCHandler.h | 3 - src/ripple/rpc/handlers/AccountInfo.cpp | 10 +- src/ripple/rpc/handlers/LedgerHandler.cpp | 43 ++-- src/ripple/rpc/handlers/LedgerHandler.h | 3 +- src/ripple/rpc/impl/Handler.cpp | 1 - src/ripple/rpc/impl/Handler.h | 1 - src/ripple/rpc/impl/RPCHandler.cpp | 30 --- src/ripple/rpc/impl/TransactionSign.cpp | 15 +- src/test/app/TxQ_test.cpp | 141 ++++++------- src/test/rpc/LedgerRPC_test.cpp | 214 ++++++++++++++++++++ 14 files changed, 499 insertions(+), 204 deletions(-) diff --git a/src/ripple/app/ledger/LedgerToJson.h b/src/ripple/app/ledger/LedgerToJson.h index 78bf6a3e50..d115bd4db2 100644 --- a/src/ripple/app/ledger/LedgerToJson.h +++ b/src/ripple/app/ledger/LedgerToJson.h @@ -21,6 +21,7 @@ #define RIPPLE_APP_LEDGER_LEDGERTOJSON_H_INCLUDED #include +#include #include #include #include @@ -36,12 +37,27 @@ struct LedgerFill { } + LedgerFill(ReadView const& l, int o, + std::vector q) + : ledger(l) + , options(o) + , txQueue(q) + { + } + enum Options { - dumpTxrp = 1, dumpState = 2, expand = 4, full = 8, binary = 16, - ownerFunds = 32}; + dumpTxrp = 1, + dumpState = 2, + expand = 4, + full = 8, + binary = 16, + ownerFunds = 32, + dumpQueue = 64 + }; ReadView const& ledger; int options; + std::vector txQueue; }; /** Given a Ledger and options, fill a Json::Object or Json::Value with a @@ -49,7 +65,6 @@ struct LedgerFill */ void addJson(Json::Value&, LedgerFill const&); -void addJson(Json::Object&, LedgerFill const&); /** Return a new Json::Value representing the ledger with given options.*/ Json::Value getJson (LedgerFill const&); diff --git a/src/ripple/app/ledger/impl/LedgerToJson.cpp b/src/ripple/app/ledger/impl/LedgerToJson.cpp index 67c94d5850..3ec9316072 100644 --- a/src/ripple/app/ledger/impl/LedgerToJson.cpp +++ b/src/ripple/app/ledger/impl/LedgerToJson.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include namespace ripple { @@ -50,7 +51,7 @@ void fillJson(Object& json, bool closed, LedgerInfo const& info, bool bFull) { json[jss::closed] = true; } - else if (!bFull) + else if (! bFull) { json[jss::closed] = false; return; @@ -95,6 +96,50 @@ void fillJsonBinary(Object& json, bool closed, LedgerInfo const& info) } } +Json::Value fillJsonTx (LedgerFill const& fill, + bool bBinary, bool bExpanded, + std::pair, + std::shared_ptr> const i) +{ + if (! bExpanded) + { + return to_string(i.first->getTransactionID()); + } + else + { + Json::Value txJson{ Json::objectValue }; + if (bBinary) + { + txJson[jss::tx_blob] = serializeHex(*i.first); + if (i.second) + txJson[jss::meta] = serializeHex(*i.second); + } + else + { + copyFrom(txJson, i.first->getJson(0)); + if (i.second) + txJson[jss::metaData] = i.second->getJson(0); + } + + if ((fill.options & LedgerFill::ownerFunds) && + i.first->getTxnType() == ttOFFER_CREATE) + { + auto const account = i.first->getAccountID(sfAccount); + auto const amount = i.first->getFieldAmount(sfTakerGets); + + // If the offer create is not self funded then add the + // owner balance + if (account != amount.getIssuer()) + { + auto const ownerFunds = accountFunds(fill.ledger, + account, amount, fhIGNORE_FREEZE, beast::Journal()); + txJson[jss::owner_funds] = ownerFunds.getText (); + } + } + return txJson; + } +} + template void fillJsonTx (Object& json, LedgerFill const& fill) { @@ -106,42 +151,7 @@ void fillJsonTx (Object& json, LedgerFill const& fill) { for (auto& i: fill.ledger.txs) { - if (! bExpanded) - { - txns.append(to_string(i.first->getTransactionID())); - } - else - { - auto&& txJson = appendObject(txns); - if (bBinary) - { - txJson[jss::tx_blob] = serializeHex(*i.first); - if (i.second) - txJson[jss::meta] = serializeHex(*i.second); - } - else - { - copyFrom(txJson, i.first->getJson(0)); - if (i.second) - txJson[jss::metaData] = i.second->getJson(0); - } - - if ((fill.options & LedgerFill::ownerFunds) && - i.first->getTxnType() == ttOFFER_CREATE) - { - auto const account = i.first->getAccountID(sfAccount); - auto const amount = i.first->getFieldAmount(sfTakerGets); - - // If the offer create is not self funded then add the - // owner balance - if (account != amount.getIssuer()) - { - auto const ownerFunds = accountFunds(fill.ledger, - account, amount, fhIGNORE_FREEZE, beast::Journal()); - txJson[jss::owner_funds] = ownerFunds.getText (); - } - } - } + txns.append(fillJsonTx(fill, bBinary, bExpanded, i)); } } catch (std::exception const&) @@ -173,6 +183,42 @@ void fillJsonState(Object& json, LedgerFill const& fill) } } +template +void fillJsonQueue(Object& json, LedgerFill const& fill) +{ + auto&& queueData = Json::setArray(json, jss::queue_data); + auto bBinary = isBinary(fill); + auto bExpanded = isExpanded(fill); + + for (auto const& tx : fill.txQueue) + { + auto&& txJson = appendObject(queueData); + 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::account] = to_string(tx.account); + txJson["retries_remaining"] = tx.retriesRemaining; + txJson["preflight_result"] = transToken(tx.preflightResult); + if (tx.lastResult) + txJson["last_result"] = transToken(*tx.lastResult); + + txJson[jss::tx] = fillJsonTx(fill, bBinary, bExpanded, + std::make_pair(tx.txn, nullptr)); + } +} + template void fillJson (Object& json, LedgerFill const& fill) { @@ -180,9 +226,9 @@ void fillJson (Object& json, LedgerFill const& fill) // Is there a way to report this back? auto bFull = isFull(fill); if (isBinary(fill)) - fillJsonBinary(json, !fill.ledger.open(), fill.ledger.info()); + fillJsonBinary(json, ! fill.ledger.open(), fill.ledger.info()); else - fillJson(json, !fill.ledger.open(), fill.ledger.info(), bFull); + fillJson(json, ! fill.ledger.open(), fill.ledger.info(), bFull); if (bFull || fill.options & LedgerFill::dumpTxrp) fillJsonTx(json, fill); @@ -193,17 +239,13 @@ void fillJson (Object& json, LedgerFill const& fill) } // namespace -void addJson (Json::Object& json, LedgerFill const& fill) -{ - auto&& object = Json::addObject (json, jss::ledger); - fillJson (object, fill); -} - void addJson (Json::Value& json, LedgerFill const& fill) { auto&& object = Json::addObject (json, jss::ledger); fillJson (object, fill); + if ((fill.options & LedgerFill::dumpQueue) && !fill.txQueue.empty()) + fillJsonQueue(json, fill); } Json::Value getJson (LedgerFill const& fill) diff --git a/src/ripple/app/misc/TxQ.h b/src/ripple/app/misc/TxQ.h index 348cab25a7..bf3218097a 100644 --- a/src/ripple/app/misc/TxQ.h +++ b/src/ripple/app/misc/TxQ.h @@ -96,6 +96,15 @@ public: boost::optional consequences; }; + struct TxDetails : AccountTxDetails + { + AccountID account; + std::shared_ptr txn; + int retriesRemaining; + TER preflightResult; + boost::optional lastResult; + }; + TxQ(Setup const& setup, beast::Journal j); @@ -149,9 +158,19 @@ public: amendment is not enabled, OR if the account has no transactions in the queue. */ - boost::optional> + std::map getAccountTxs(AccountID const& account, ReadView const& view) const; + /** Returns information about all transactions currently + in the queue. + + @returns Uninitialized @ref optional if the FeeEscalation + amendment is not enabled, OR if there are no transactions + in the queue. + */ + std::vector + getTxs(ReadView const& view) const; + /** Packages up fee metrics for the `fee` RPC command. */ Json::Value @@ -267,6 +286,7 @@ private: int retriesRemaining; TxSeq const sequence; ApplyFlags const flags; + boost::optional lastResult; // Invariant: pfresult is never allowed to be empty. The // boost::optional is leveraged to allow `emplace`d // construction and replacement without a copy diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index fb4a1c8456..bdbcc882c2 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -492,6 +492,7 @@ TxQ::tryClearAccountQueue(Application& app, OpenView& view, // succeeds, the MaybeTx will be destructed, so it'll be // moot. --it->second.retriesRemaining; + it->second.lastResult = txResult.first; if (!txResult.second) { // Transaction failed to apply. Fall back to the normal process. @@ -1268,6 +1269,7 @@ TxQ::accept(Application& app, candidateIter->retriesRemaining = 1; else --candidateIter->retriesRemaining; + candidateIter->lastResult = txnResult; if (account.dropPenalty && account.transactions.size() > 1 && isFull<95>()) { @@ -1331,34 +1333,79 @@ TxQ::getMetrics(OpenView const& view, std::uint32_t txCountPadding) const auto TxQ::getAccountTxs(AccountID const& account, ReadView const& view) const - -> boost::optional> + -> std::map { auto const allowEscalation = (view.rules().enabled(featureFeeEscalation)); if (!allowEscalation) - return boost::none; + return {}; std::lock_guard lock(mutex_); auto accountIter = byAccount_.find(account); if (accountIter == byAccount_.end() || accountIter->second.transactions.empty()) - return boost::none; + return {}; - std::map result; + std::map result; for (auto const& tx : accountIter->second.transactions) { - auto& resultTx = result[tx.first]; - 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); + 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; + }()); } return result; } +auto +TxQ::getTxs(ReadView const& view) const +-> std::vector +{ + auto const allowEscalation = + (view.rules().enabled(featureFeeEscalation)); + if (!allowEscalation) + return {}; + + 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; + }()); + } + return result; +} + + Json::Value TxQ::doRPC(Application& app) const { diff --git a/src/ripple/rpc/RPCHandler.h b/src/ripple/rpc/RPCHandler.h index e535033309..1dbcee49ff 100644 --- a/src/ripple/rpc/RPCHandler.h +++ b/src/ripple/rpc/RPCHandler.h @@ -33,9 +33,6 @@ struct Context; /** Execute an RPC command and store the results in a Json::Value. */ Status doCommand (RPC::Context&, Json::Value&); -/** Execute an RPC command and store the results in an std::string. */ -void executeRPC (RPC::Context&, std::string&); - Role roleRequired (std::string const& method ); } // RPC diff --git a/src/ripple/rpc/handlers/AccountInfo.cpp b/src/ripple/rpc/handlers/AccountInfo.cpp index ac171aafbb..98965dbaff 100644 --- a/src/ripple/rpc/handlers/AccountInfo.cpp +++ b/src/ripple/rpc/handlers/AccountInfo.cpp @@ -117,11 +117,11 @@ Json::Value doAccountInfo (RPC::Context& context) auto const txs = context.app.getTxQ().getAccountTxs( accountID, *ledger); - if (txs && !txs->empty()) + if (!txs.empty()) { - jvQueueData[jss::txn_count] = static_cast(txs->size()); - jvQueueData[jss::lowest_sequence] = txs->begin()->first; - jvQueueData[jss::highest_sequence] = txs->rbegin()->first; + 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; @@ -129,7 +129,7 @@ Json::Value doAccountInfo (RPC::Context& context) boost::optional anyAuthChanged(false); boost::optional totalSpend(0); - for (auto const& tx : *txs) + for (auto const& tx : txs) { Json::Value jvTx = Json::objectValue; diff --git a/src/ripple/rpc/handlers/LedgerHandler.cpp b/src/ripple/rpc/handlers/LedgerHandler.cpp index 808691e9c1..857617a051 100644 --- a/src/ripple/rpc/handlers/LedgerHandler.cpp +++ b/src/ripple/rpc/handlers/LedgerHandler.cpp @@ -42,27 +42,29 @@ Status LedgerHandler::check() bool needsLedger = params.isMember (jss::ledger) || params.isMember (jss::ledger_hash) || params.isMember (jss::ledger_index); - if (!needsLedger) + if (! needsLedger) return Status::OK; if (auto s = lookupLedger (ledger_, context_, result_)) return s; - bool bFull = params[jss::full].asBool(); - bool bTransactions = params[jss::transactions].asBool(); - bool bAccounts = params[jss::accounts].asBool(); - bool bExpand = params[jss::expand].asBool(); - bool bBinary = params[jss::binary].asBool(); + bool const full = params[jss::full].asBool(); + bool const transactions = params[jss::transactions].asBool(); + bool const accounts = params[jss::accounts].asBool(); + bool const expand = params[jss::expand].asBool(); + bool const binary = params[jss::binary].asBool(); bool const owner_funds = params[jss::owner_funds].asBool(); + bool const queue = params[jss::queue].asBool(); - options_ = (bFull ? LedgerFill::full : 0) - | (bExpand ? LedgerFill::expand : 0) - | (bTransactions ? LedgerFill::dumpTxrp : 0) - | (bAccounts ? LedgerFill::dumpState : 0) - | (bBinary ? LedgerFill::binary : 0) - | (owner_funds ? LedgerFill::ownerFunds : 0); + options_ = (full ? LedgerFill::full : 0) + | (expand ? LedgerFill::expand : 0) + | (transactions ? LedgerFill::dumpTxrp : 0) + | (accounts ? LedgerFill::dumpState : 0) + | (binary ? LedgerFill::binary : 0) + | (owner_funds ? LedgerFill::ownerFunds : 0) + | (queue ? LedgerFill::dumpQueue : 0); - if (bFull || bAccounts) + if (full || accounts) { // Until some sane way to get full ledgers has been implemented, // disallow retrieving all state nodes. @@ -70,13 +72,24 @@ Status LedgerHandler::check() return rpcNO_PERMISSION; if (context_.app.getFeeTrack().isLoadedLocal() && - !isUnlimited (context_.role)) + ! isUnlimited (context_.role)) { return rpcTOO_BUSY; } - context_.loadType = bBinary ? Resource::feeMediumBurdenRPC : + context_.loadType = binary ? Resource::feeMediumBurdenRPC : Resource::feeHighBurdenRPC; } + if (queue) + { + if (! ledger_ || ! ledger_->open()) + { + // It doesn't make sense to request the queue + // with a non-existant or closed/validated ledger. + return rpcINVALID_PARAMS; + } + + queueTxs_ = context_.app.getTxQ().getTxs(*ledger_); + } return Status::OK; } diff --git a/src/ripple/rpc/handlers/LedgerHandler.h b/src/ripple/rpc/handlers/LedgerHandler.h index d642263c39..a2306b7c04 100644 --- a/src/ripple/rpc/handlers/LedgerHandler.h +++ b/src/ripple/rpc/handlers/LedgerHandler.h @@ -73,6 +73,7 @@ public: private: Context& context_; std::shared_ptr ledger_; + std::vector queueTxs_; Json::Value result_; int options_ = 0; }; @@ -88,7 +89,7 @@ void LedgerHandler::writeResult (Object& value) if (ledger_) { Json::copyFrom (value, result_); - addJson (value, {*ledger_, options_}); + addJson (value, {*ledger_, options_, queueTxs_}); } else { diff --git a/src/ripple/rpc/impl/Handler.cpp b/src/ripple/rpc/impl/Handler.cpp index c49820adf5..21851610ef 100644 --- a/src/ripple/rpc/impl/Handler.cpp +++ b/src/ripple/rpc/impl/Handler.cpp @@ -92,7 +92,6 @@ class HandlerTable { h.valueMethod_ = &handle; h.role_ = HandlerImpl::role(); h.condition_ = HandlerImpl::condition(); - h.objectMethod_ = &handle; table_[HandlerImpl::name()] = h; }; diff --git a/src/ripple/rpc/impl/Handler.h b/src/ripple/rpc/impl/Handler.h index 62102912b3..947aeac5a6 100644 --- a/src/ripple/rpc/impl/Handler.h +++ b/src/ripple/rpc/impl/Handler.h @@ -48,7 +48,6 @@ struct Handler Method valueMethod_; Role role_; RPC::Condition condition_; - Method objectMethod_; }; const Handler* getHandler (std::string const&); diff --git a/src/ripple/rpc/impl/RPCHandler.cpp b/src/ripple/rpc/impl/RPCHandler.cpp index d0bcc342b6..acb5caf94f 100644 --- a/src/ripple/rpc/impl/RPCHandler.cpp +++ b/src/ripple/rpc/impl/RPCHandler.cpp @@ -265,36 +265,6 @@ Status doCommand ( return rpcUNKNOWN_COMMAND; } -/** Execute an RPC command and store the results in a string. */ -void executeRPC ( - RPC::Context& context, std::string& output) -{ - boost::optional handler; - if (auto error = fillHandler (context, handler)) - { - auto wo = Json::stringWriterObject (output); - auto&& sub = Json::addObject (*wo, jss::result); - inject_error (error, sub); - } - else if (auto method = handler->objectMethod_) - { - auto wo = Json::stringWriterObject (output); - getResult (context, method, *wo, handler->name_); - } - else if (auto method = handler->valueMethod_) - { - auto object = Json::Value (Json::objectValue); - getResult (context, method, object, handler->name_); - output = to_string (object); - } - else - { - // Can't ever get here. - assert (false); - Throw ("RPC handler with no method"); - } -} - Role roleRequired (std::string const& method) { auto handler = RPC::getHandler(method); diff --git a/src/ripple/rpc/impl/TransactionSign.cpp b/src/ripple/rpc/impl/TransactionSign.cpp index 3be99b6f51..01eb9bfec9 100644 --- a/src/ripple/rpc/impl/TransactionSign.cpp +++ b/src/ripple/rpc/impl/TransactionSign.cpp @@ -434,14 +434,13 @@ transactionPreProcessImpl ( *ledger); // If the account has any txs in the TxQ, skip those sequence // numbers (accounting for possible gaps). - if(queued) - for(auto const& tx : *queued) - { - if (tx.first == seq) - ++seq; - else if (tx.first > seq) - break; - } + for(auto const& tx : queued) + { + if (tx.first == seq) + ++seq; + else if (tx.first > seq) + break; + } tx_json[jss::Sequence] = seq; } diff --git a/src/test/app/TxQ_test.cpp b/src/test/app/TxQ_test.cpp index 0a8a8a1685..4ec73be202 100644 --- a/src/test/app/TxQ_test.cpp +++ b/src/test/app/TxQ_test.cpp @@ -447,27 +447,21 @@ public: { auto& txQ = env.app().getTxQ(); auto aliceStat = txQ.getAccountTxs(alice.id(), *env.current()); - if (BEAST_EXPECT(aliceStat)) - { - BEAST_EXPECT(aliceStat->size() == 1); - BEAST_EXPECT(aliceStat->begin()->second.feeLevel == 256); - BEAST_EXPECT(aliceStat->begin()->second.lastValid && - *aliceStat->begin()->second.lastValid == 8); - BEAST_EXPECT(!aliceStat->begin()->second.consequences); - } + BEAST_EXPECT(aliceStat.size() == 1); + BEAST_EXPECT(aliceStat.begin()->second.feeLevel == 256); + BEAST_EXPECT(aliceStat.begin()->second.lastValid && + *aliceStat.begin()->second.lastValid == 8); + BEAST_EXPECT(!aliceStat.begin()->second.consequences); auto bobStat = txQ.getAccountTxs(bob.id(), *env.current()); - if (BEAST_EXPECT(bobStat)) - { - BEAST_EXPECT(bobStat->size() == 1); - BEAST_EXPECT(bobStat->begin()->second.feeLevel = 7000 * 256 / 10); - BEAST_EXPECT(!bobStat->begin()->second.lastValid); - BEAST_EXPECT(!bobStat->begin()->second.consequences); - } + BEAST_EXPECT(bobStat.size() == 1); + BEAST_EXPECT(bobStat.begin()->second.feeLevel == 7000 * 256 / 10); + BEAST_EXPECT(!bobStat.begin()->second.lastValid); + BEAST_EXPECT(!bobStat.begin()->second.consequences); auto noStat = txQ.getAccountTxs(Account::master.id(), *env.current()); - BEAST_EXPECT(!noStat); + BEAST_EXPECT(noStat.empty()); } env.close(); @@ -792,21 +786,18 @@ public: auto aliceStat = txQ.getAccountTxs(alice.id(), *env.current()); std::int64_t fee = 20; auto seq = env.seq(alice); - if (BEAST_EXPECT(aliceStat)) + BEAST_EXPECT(aliceStat.size() == 7); + for (auto const& tx : aliceStat) { - BEAST_EXPECT(aliceStat->size() == 7); - for (auto const& tx : *aliceStat) - { - BEAST_EXPECT(tx.first == seq); - BEAST_EXPECT(tx.second.feeLevel == mulDiv(fee, 256, 10).second); - BEAST_EXPECT(tx.second.lastValid); - BEAST_EXPECT((tx.second.consequences && - tx.second.consequences->fee == drops(fee) && - tx.second.consequences->potentialSpend == drops(0) && - tx.second.consequences->category == TxConsequences::normal) || - tx.first == env.seq(alice) + 6); - ++seq; - } + BEAST_EXPECT(tx.first == seq); + BEAST_EXPECT(tx.second.feeLevel == mulDiv(fee, 256, 10).second); + BEAST_EXPECT(tx.second.lastValid); + BEAST_EXPECT((tx.second.consequences && + tx.second.consequences->fee == drops(fee) && + tx.second.consequences->potentialSpend == drops(0) && + tx.second.consequences->category == TxConsequences::normal) || + tx.first == env.seq(alice) + 6); + ++seq; } } @@ -1823,25 +1814,22 @@ public: checkMetrics(env, 5, boost::none, 7, 6, 256); { auto aliceStat = txQ.getAccountTxs(alice.id(), *env.current()); - if (BEAST_EXPECT(aliceStat)) + auto seq = aliceSeq; + BEAST_EXPECT(aliceStat.size() == 5); + for (auto const& tx : aliceStat) { - auto seq = aliceSeq; - BEAST_EXPECT(aliceStat->size() == 5); - for (auto const& tx : *aliceStat) + BEAST_EXPECT(tx.first == seq); + BEAST_EXPECT(tx.second.feeLevel == 25600); + if(seq == aliceSeq + 2) { - BEAST_EXPECT(tx.first == seq); - BEAST_EXPECT(tx.second.feeLevel == 25600); - if(seq == aliceSeq + 2) - { - BEAST_EXPECT(tx.second.lastValid && - *tx.second.lastValid == lastLedgerSeq); - } - else - { - BEAST_EXPECT(!tx.second.lastValid); - } - ++seq; + BEAST_EXPECT(tx.second.lastValid && + *tx.second.lastValid == lastLedgerSeq); } + else + { + BEAST_EXPECT(!tx.second.lastValid); + } + ++seq; } } // Put some txs in the queue for bob. @@ -1870,27 +1858,24 @@ public: { // Bob has nothing left in the queue. auto bobStat = txQ.getAccountTxs(bob.id(), *env.current()); - BEAST_EXPECT(!bobStat); + BEAST_EXPECT(bobStat.empty()); } // Verify alice's tx got dropped as we BEAST_EXPECT, and that there's // a gap in her queued txs. { auto aliceStat = txQ.getAccountTxs(alice.id(), *env.current()); - if (BEAST_EXPECT(aliceStat)) + auto seq = aliceSeq; + BEAST_EXPECT(aliceStat.size() == 4); + for (auto const& tx : aliceStat) { - auto seq = aliceSeq; - BEAST_EXPECT(aliceStat->size() == 4); - for (auto const& tx : *aliceStat) - { - // Skip over the missing one. - if (seq == aliceSeq + 2) - ++seq; - - BEAST_EXPECT(tx.first == seq); - BEAST_EXPECT(tx.second.feeLevel == 25600); - BEAST_EXPECT(!tx.second.lastValid); + // Skip over the missing one. + if (seq == aliceSeq + 2) ++seq; - } + + BEAST_EXPECT(tx.first == seq); + BEAST_EXPECT(tx.second.feeLevel == 25600); + BEAST_EXPECT(!tx.second.lastValid); + ++seq; } } // Now, fill the gap. @@ -1898,17 +1883,14 @@ public: checkMetrics(env, 5, 18, 10, 9, 256); { auto aliceStat = txQ.getAccountTxs(alice.id(), *env.current()); - if (BEAST_EXPECT(aliceStat)) + auto seq = aliceSeq; + BEAST_EXPECT(aliceStat.size() == 5); + for (auto const& tx : aliceStat) { - auto seq = aliceSeq; - BEAST_EXPECT(aliceStat->size() == 5); - for (auto const& tx : *aliceStat) - { - BEAST_EXPECT(tx.first == seq); - BEAST_EXPECT(tx.second.feeLevel == 25600); - BEAST_EXPECT(!tx.second.lastValid); - ++seq; - } + BEAST_EXPECT(tx.first == seq); + BEAST_EXPECT(tx.second.feeLevel == 25600); + BEAST_EXPECT(!tx.second.lastValid); + ++seq; } } @@ -1917,11 +1899,11 @@ public: { // Bob's data has been cleaned up. auto bobStat = txQ.getAccountTxs(bob.id(), *env.current()); - BEAST_EXPECT(!bobStat); + BEAST_EXPECT(bobStat.empty()); } { auto aliceStat = txQ.getAccountTxs(alice.id(), *env.current()); - BEAST_EXPECT(!aliceStat); + BEAST_EXPECT(aliceStat.empty()); } } @@ -2723,16 +2705,13 @@ public: checkMetrics(env, 2, 24, 16, 12, 256); auto const aliceQueue = env.app().getTxQ().getAccountTxs( alice.id(), *env.current()); - if (BEAST_EXPECT(aliceQueue)) + BEAST_EXPECT(aliceQueue.size() == 2); + auto seq = aliceSeq; + for (auto const& tx : aliceQueue) { - BEAST_EXPECT(aliceQueue->size() == 2); - auto seq = aliceSeq; - for (auto const& tx : *aliceQueue) - { - BEAST_EXPECT(tx.first == seq); - BEAST_EXPECT(tx.second.feeLevel == 2560); - ++seq; - } + BEAST_EXPECT(tx.first == seq); + BEAST_EXPECT(tx.second.feeLevel == 2560); + ++seq; } // Close the ledger to clear the queue diff --git a/src/test/rpc/LedgerRPC_test.cpp b/src/test/rpc/LedgerRPC_test.cpp index 4714a66bbd..e5d6316af5 100644 --- a/src/test/rpc/LedgerRPC_test.cpp +++ b/src/test/rpc/LedgerRPC_test.cpp @@ -18,7 +18,9 @@ //============================================================================== #include +#include #include +#include #include #include #include @@ -110,6 +112,15 @@ class LedgerRPC_test : public beast::unit_test::suite checkErrorValue(jrr, "lgrNotFound", "ledgerNotFound"); } + { + // Request queue for closed ledger + Json::Value jvParams; + jvParams[jss::ledger_index] = "validated"; + jvParams[jss::queue] = true; + auto const jrr = env.rpc ( "json", "ledger", to_string(jvParams) ) [jss::result]; + checkErrorValue(jrr, "invalidParams", "Invalid parameters."); + } + } void testLedgerCurrent() @@ -440,6 +451,207 @@ class LedgerRPC_test : public beast::unit_test::suite } + void testNoQueue() + { + testcase("Ledger with queueing disabled"); + using namespace test::jtx; + Env env{ *this }; + + Json::Value jv; + jv[jss::ledger_index] = "current"; + jv[jss::queue] = true; + jv[jss::expand] = true; + + auto jrr = env.rpc("json", "ledger", to_string(jv))[jss::result]; + BEAST_EXPECT(! jrr.isMember(jss::queue_data)); + } + + void testQueue() + { + testcase("Ledger with Queued Transactions"); + using namespace test::jtx; + Env env{ *this, []() + { + auto p = std::make_unique(); + test::setupConfigForUnitTests(*p); + auto& section = p->section("transaction_queue"); + section.set("minimum_txn_in_ledger_standalone", "3"); + return p; + }(), + features(featureFeeEscalation) }; + + Json::Value jv; + jv[jss::ledger_index] = "current"; + jv[jss::queue] = true; + jv[jss::expand] = true; + + Account const alice{ "alice" }; + Account const bob{ "bob" }; + Account const charlie{ "charlie" }; + Account const daria{ "daria" }; + env.fund(XRP(10000), alice); + env.fund(XRP(10000), bob); + env.close(); + env.fund(XRP(10000), charlie); + env.fund(XRP(10000), daria); + env.close(); + + auto jrr = env.rpc("json", "ledger", to_string(jv))[jss::result]; + BEAST_EXPECT(! jrr.isMember(jss::queue_data)); + + // Fill the open ledger + for (;;) + { + auto metrics = env.app().getTxQ().getMetrics(*env.current()); + if (! BEAST_EXPECT(metrics)) + break; + if (metrics->expFeeLevel > metrics->minFeeLevel) + break; + env(noop(alice)); + } + + BEAST_EXPECT(env.current()->info().seq == 5); + // Put some txs in the queue + // Alice + auto aliceSeq = env.seq(alice); + env(pay(alice, "george", XRP(1000)), json(R"({"LastLedgerSequence":7})"), + ter(terQUEUED)); + env(offer(alice, XRP(50000), alice["USD"](5000)), seq(aliceSeq + 1), + ter(terQUEUED)); + env(noop(alice), seq(aliceSeq + 2), ter(terQUEUED)); + // Bob + auto batch = [&env](Account a) + { + auto aSeq = env.seq(a); + // Enough fee to get in front of alice in the queue + for (int i = 0; i < 10; ++i) + { + env(noop(a), fee(1000 + i), seq(aSeq + i), ter(terQUEUED)); + } + }; + batch(bob); + // Charlie + batch(charlie); + // Daria + batch(daria); + + jrr = env.rpc("json", "ledger", to_string(jv))[jss::result]; + BEAST_EXPECT(jrr[jss::queue_data].size() == 33); + + // Close enough ledgers so that alice's first tx expires. + env.close(); + env.close(); + env.close(); + BEAST_EXPECT(env.current()->info().seq == 8); + + jrr = env.rpc("json", "ledger", to_string(jv))[jss::result]; + BEAST_EXPECT(jrr[jss::queue_data].size() == 11); + + env.close(); + + jrr = env.rpc("json", "ledger", to_string(jv))[jss::result]; + std::string txid1; + std::string txid2; + if (BEAST_EXPECT(jrr[jss::queue_data].size() == 2)) + { + auto const& txj = jrr[jss::queue_data][0u]; + BEAST_EXPECT(txj[jss::account] == alice.human()); + BEAST_EXPECT(txj[jss::fee_level] == "256"); + BEAST_EXPECT(txj["preflight_result"] == "tesSUCCESS"); + BEAST_EXPECT(txj["retries_remaining"] == 10); + BEAST_EXPECT(txj.isMember(jss::tx)); + auto const& tx = txj[jss::tx]; + BEAST_EXPECT(tx[jss::Account] == alice.human()); + BEAST_EXPECT(tx[jss::TransactionType] == "OfferCreate"); + txid1 = tx[jss::hash].asString(); + } + + env.close(); + + jv[jss::expand] = false; + + jrr = env.rpc("json", "ledger", to_string(jv))[jss::result]; + if (BEAST_EXPECT(jrr[jss::queue_data].size() == 2)) + { + auto const& txj = jrr[jss::queue_data][0u]; + BEAST_EXPECT(txj[jss::account] == alice.human()); + BEAST_EXPECT(txj[jss::fee_level] == "256"); + BEAST_EXPECT(txj["preflight_result"] == "tesSUCCESS"); + BEAST_EXPECT(txj["retries_remaining"] == 9); + BEAST_EXPECT(txj["last_result"] == "terPRE_SEQ"); + BEAST_EXPECT(txj.isMember(jss::tx)); + BEAST_EXPECT(txj[jss::tx] == txid1); + } + + env.close(); + + jv[jss::expand] = true; + jv[jss::binary] = true; + + jrr = env.rpc("json", "ledger", to_string(jv))[jss::result]; + if (BEAST_EXPECT(jrr[jss::queue_data].size() == 2)) + { + auto const& txj = jrr[jss::queue_data][0u]; + BEAST_EXPECT(txj[jss::account] == alice.human()); + BEAST_EXPECT(txj[jss::fee_level] == "256"); + BEAST_EXPECT(txj["preflight_result"] == "tesSUCCESS"); + BEAST_EXPECT(txj["retries_remaining"] == 8); + BEAST_EXPECT(txj["last_result"] == "terPRE_SEQ"); + BEAST_EXPECT(txj.isMember(jss::tx)); + BEAST_EXPECT(txj[jss::tx].isMember(jss::tx_blob)); + + auto const& txj2 = jrr[jss::queue_data][1u]; + BEAST_EXPECT(txj2[jss::account] == alice.human()); + BEAST_EXPECT(txj2[jss::fee_level] == "256"); + BEAST_EXPECT(txj2["preflight_result"] == "tesSUCCESS"); + BEAST_EXPECT(txj2["retries_remaining"] == 10); + BEAST_EXPECT(! txj2.isMember("last_result")); + BEAST_EXPECT(txj2.isMember(jss::tx)); + BEAST_EXPECT(txj2[jss::tx].isMember(jss::tx_blob)); + } + + for (int i = 0; i != 9; ++i) + { + env.close(); + } + + jv[jss::expand] = false; + jv[jss::binary] = false; + + jrr = env.rpc("json", "ledger", to_string(jv))[jss::result]; + if (BEAST_EXPECT(jrr[jss::queue_data].size() == 1)) + { + auto const& txj = jrr[jss::queue_data][0u]; + BEAST_EXPECT(txj[jss::account] == alice.human()); + BEAST_EXPECT(txj[jss::fee_level] == "256"); + BEAST_EXPECT(txj["preflight_result"] == "tesSUCCESS"); + BEAST_EXPECT(txj["retries_remaining"] == 1); + BEAST_EXPECT(txj["last_result"] == "terPRE_SEQ"); + BEAST_EXPECT(txj.isMember(jss::tx)); + BEAST_EXPECT(txj[jss::tx] != txid1); + txid2 = txj[jss::tx].asString(); + } + + jv[jss::full] = true; + + jrr = env.rpc("json", "ledger", to_string(jv))[jss::result]; + if (BEAST_EXPECT(jrr[jss::queue_data].size() == 1)) + { + auto const& txj = jrr[jss::queue_data][0u]; + BEAST_EXPECT(txj[jss::account] == alice.human()); + BEAST_EXPECT(txj[jss::fee_level] == "256"); + BEAST_EXPECT(txj["preflight_result"] == "tesSUCCESS"); + BEAST_EXPECT(txj["retries_remaining"] == 1); + BEAST_EXPECT(txj["last_result"] == "terPRE_SEQ"); + BEAST_EXPECT(txj.isMember(jss::tx)); + auto const& tx = txj[jss::tx]; + BEAST_EXPECT(tx[jss::Account] == alice.human()); + BEAST_EXPECT(tx[jss::TransactionType] == "AccountSet"); + BEAST_EXPECT(tx[jss::hash] == txid2); + } + + } + public: void run () { @@ -454,6 +666,8 @@ public: testNotFoundAccountRoot(); testAccountRootFromIndex(); testLookupLedger(); + testNoQueue(); + testQueue(); } };