From 7f52249e407e362e2c3bbcb4decfc62df0efe718 Mon Sep 17 00:00:00 2001 From: Edward Hennis Date: Thu, 2 Jun 2016 20:12:26 -0400 Subject: [PATCH] Change fee escalation algorithms (RIPD-1177): * Minimum factor 256*500, don't multiply by base fee * Change autofill fee behavior to pay the open ledger fee. ** Experimental options: x-assume-tx - assume more transactions in the open queue when computing escalated fee, x-queue-okay - if true and escalated fee is over limit, try with load fee. * Port of 75af4ed. --- src/ripple/app/misc/FeeEscalation.md | 45 +++-- src/ripple/app/misc/NetworkOPs.cpp | 2 +- src/ripple/app/misc/TxQ.h | 12 +- src/ripple/app/misc/impl/TxQ.cpp | 43 ++-- src/ripple/app/tests/TxQ_test.cpp | 83 ++++---- src/ripple/rpc/handlers/SignFor.cpp | 3 +- src/ripple/rpc/handlers/SignHandler.cpp | 3 +- src/ripple/rpc/handlers/Submit.cpp | 1 - src/ripple/rpc/handlers/SubmitMultiSigned.cpp | 1 - src/ripple/rpc/impl/TransactionSign.cpp | 54 +++++- src/ripple/rpc/impl/TransactionSign.h | 12 +- src/ripple/rpc/tests/JSONRPC.test.cpp | 183 ++++++++++++++++-- src/ripple/rpc/tests/Subscribe.test.cpp | 2 +- 13 files changed, 316 insertions(+), 128 deletions(-) diff --git a/src/ripple/app/misc/FeeEscalation.md b/src/ripple/app/misc/FeeEscalation.md index 3cc34ac647..211e6efcdf 100644 --- a/src/ripple/app/misc/FeeEscalation.md +++ b/src/ripple/app/misc/FeeEscalation.md @@ -35,30 +35,30 @@ consensus process, but will be at least [5](#other-constants). or the number of transactions in the validated ledger. 3. Once there are more transactions in the open ledger than indicated by the limit, the required fee level jumps drastically. - * The formula is `( baseFeeLevel * lastLedgerMedianFeeLevel * + * The formula is `( lastLedgerMedianFeeLevel * TransactionsInOpenLedger^2 / limit^2 )`, and returns a [fee level](#fee-level). 4. That may still be pretty small, but as more transactions get into the ledger, the fee level increases exponentially. * For example, if the limit is 6, and the median fee is minimal, and assuming all [reference transactions](#reference-transaction), - the 7th transaction only requires a [level](#fee-level) of about 174,000 + the 8th transaction only requires a [level](#fee-level) of about 174,000 or about 6800 drops, but the 20th transaction requires a [level](#fee-level) of about - 1,422,000 or about 56,000 drops. + 1,283,000 or about 50,000 drops. 5. Finally, as each ledger closes, the median fee level of that ledger is computed and used as `lastLedgerMedianFeeLevel` (with a -[minimum value of 500](#other-constants)) +[minimum value of 128,000](#other-constants)) in the fee escalation formula for the next open ledger. * Continuing the example above, if ledger consensus completes with only those 20 transactions, and all of those transactions paid the minimum required fee at each step, the limit will be adjusted from - 6 to 20, and the `lastLedgerMedianFeeLevel` will be about 393,000, - which is 15,000 drops for a + 6 to 20, and the `lastLedgerMedianFeeLevel` will be about 322,000, + which is 12,600 drops for a [reference transaction](#reference-transaction). * This will cause the first 21 transactions only require 10 drops, but the 22nd transaction will require - a level of about 110,000,000 or about 4.3 million drops (4.3XRP). + a level of about 355,000 or about 13,800 drops. * This example assumes a cold-start scenario, with a single, possibly malicious, user willing to pay arbitrary amounts to get transactions @@ -80,13 +80,14 @@ allows legitimate users to continue submitting transactions during high traffic periods, and give those transactions a much better chance to succeed. -1. If an incoming transaction meets the base [fee level](#fee-level), -but does not have a high enough [fee level](#fee-level) to immediately -go into the open ledger, it is instead put into the queue and broadcast -to peers. Each peer will then make an independent decision about whether -to put the transaction into its open ledger or the queue. In principle, -peers with identical open ledgers will come to identical decisions. Any -discrepancies will be resolved as usual during consensus. +1. If an incoming transaction meets both the base [fee +level](#fee-level) and the load fee minimum, but does not have a high +enough [fee level](#fee-level) to immediately go into the open ledger, +it is instead put into the queue and broadcast to peers. Each peer will +then make an independent decision about whether to put the transaction +into its open ledger or the queue. In principle, peers with identical +open ledgers will come to identical decisions. Any discrepancies will be +resolved as usual during consensus. 2. When consensus completes, the open ledger limit is adjusted, and the required [fee level](#fee-level) drops back to the base [fee level](#fee-level). Before the ledger is made available to @@ -105,10 +106,11 @@ but in practice, either times](#other-constants), * its last ledger sequence number will expire, * the user will replace it by submitting another transaction with the same - sequence number and at least a 25% higher fee, or + sequence number and at least a [25% higher fee](#other-constants), or * it will get dropped when the queue fills up with more valuable transactions. The size limit is computed dynamically, and can hold transactions for - the next [20 ledgers](#other-constants). + the next [20 ledgers](#other-constants). The lower the transaction's + fee, the more likely that it will get dropped if the network is busy. If a transaction is submitted for an account with one or more transactions already in the queue, and a sequence number that is sequential with the other @@ -222,6 +224,13 @@ the lifespan of the transaction, and giving a queued transaction a chance to get processed out of the queue before getting discarded, particularly since it may have dependent transactions also in the queue, which will never succeed if this one is discarded. +* *Replaced transaction fee increase*. Any transaction in the queue can be +replaced by another transaction with the same sequence number and at +least a 25% higher fee level. The 25% increase is intended to cover the +resource cost incurred by broadcasting the original transaction to the +network. This value was chosen experimentally, and can easily change in +the future. + ### `fee` command **The `fee` RPC and WebSocket command is still experimental, and may @@ -250,13 +259,13 @@ Result format: "reference_level" : "256", // level of a reference transaction. Always 256. "minimum_level" : "256", // minimum fee level to get into the queue. If >256, indicates the queue is full. "median_level" : "281600", // lastLedgerMedianFeeLevel used in escalation calculations. - "open_ledger_level" : "82021944" // minimum fee level to get into the open ledger immediately. + "open_ledger_level" : "320398" // minimum fee level to get into the open ledger immediately. }, "drops" : { "base_fee" : "10", // base fee of a reference transaction in drops. "minimum_fee" : "10", // minimum drops to get a reference transaction into the queue. If >base_fee, indicates the queue is full. "median_fee" : "11000", // drop equivalent of "median_level" for a reference transaction. - "open_ledger_fee" : "3203982" // minimum drops to get a reference transaction into the open ledger immediately. + "open_ledger_fee" : "12516" // minimum drops to get a reference transaction into the open ledger immediately. } } } diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index f3dcfa06a2..e32edc923e 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -2093,7 +2093,7 @@ Json::Value NetworkOPsImp::getServerInfo (bool human, bool admin) info[jss::load] = m_job_queue.getJson (); auto const escalationMetrics = app_.getTxQ().getMetrics( - app_, *app_.openLedger().current()); + app_.config(), *app_.openLedger().current()); if (!human) { info[jss::load_base] = app_.getFeeTrack ().getLoadBase (); diff --git a/src/ripple/app/misc/TxQ.h b/src/ripple/app/misc/TxQ.h index c24fa4273f..e0388abffc 100644 --- a/src/ripple/app/misc/TxQ.h +++ b/src/ripple/app/misc/TxQ.h @@ -51,13 +51,15 @@ class Application; class TxQ { public: + static constexpr std::uint64_t baseLevel = 256; + struct Setup { std::size_t ledgersInQueue = 20; std::uint32_t retrySequencePercent = 25; // TODO: eahennis. Can we remove the multi tx factor? std::int32_t multiTxnPercent = -90; - std::uint32_t minimumEscalationMultiplier = 500; + std::uint32_t minimumEscalationMultiplier = baseLevel * 500; std::uint32_t minimumTxnInLedger = 5; std::uint32_t minimumTxnInLedgerSA = 1000; std::uint32_t targetTxnInLedger = 50; @@ -128,7 +130,8 @@ public: /** Returns fee metrics in reference fee level units. */ boost::optional - getMetrics(Application& app, OpenView const& view) const; + getMetrics(Config const& config, OpenView const& view, + std::uint32_t txCountPadding = 0) const; /** Packages up fee metrics for the `fee` RPC command. */ @@ -161,9 +164,6 @@ private: std::mutex mutable lock_; - public: - static constexpr std::uint64_t baseLevel = 256; - public: FeeMetrics(Setup const& setup, beast::Journal j) : minimumTxnCount_(setup.standAlone ? @@ -212,7 +212,7 @@ private: } std::uint64_t - scaleFeeLevel(OpenView const& view) const; + scaleFeeLevel(OpenView const& view, std::uint32_t txCountPadding = 0) const; }; // Alternate name: MaybeTx diff --git a/src/ripple/app/misc/impl/TxQ.cpp b/src/ripple/app/misc/impl/TxQ.cpp index b183180b25..44ea36f72f 100644 --- a/src/ripple/app/misc/impl/TxQ.cpp +++ b/src/ripple/app/misc/impl/TxQ.cpp @@ -154,12 +154,11 @@ TxQ::FeeMetrics::update(Application& app, } std::uint64_t -TxQ::FeeMetrics::scaleFeeLevel(OpenView const& view) const +TxQ::FeeMetrics::scaleFeeLevel(OpenView const& view, + std::uint32_t txCountPadding) const { - auto fee = baseLevel; - // Transactions in the open ledger so far - auto const current = view.txCount(); + auto const current = view.txCount() + txCountPadding; std::size_t target; std::uint32_t multiplier; @@ -177,11 +176,11 @@ TxQ::FeeMetrics::scaleFeeLevel(OpenView const& view) const { // Compute escalated fee level // Don't care about the overflow flag - fee = mulDiv(fee, current * current * - multiplier, target * target).second; + return mulDiv(multiplier, current * current, + target * target).second; } - return fee; + return baseLevel; } TxQ::MaybeTx::MaybeTx( @@ -481,7 +480,7 @@ TxQ::apply(Application& app, OpenView& view, // preclaim? auto const baseFee = calculateBaseFee(app, view, *tx, j); auto const feeLevelPaid = getFeeLevelPaid(*tx, - feeMetrics_.baseLevel, baseFee, setup_); + baseLevel, baseFee, setup_); auto const requiredFeeLevel = feeMetrics_.scaleFeeLevel(view); auto accountIter = byAccount_.find(account); @@ -758,7 +757,7 @@ TxQ::apply(Application& app, OpenView& view, return{ pcresult.ter, false }; // Too low of a fee should get caught by preclaim - assert(feeLevelPaid >= feeMetrics_.baseLevel); + assert(feeLevelPaid >= baseLevel); JLOG(j_.trace()) << "Transaction " << transactionID << @@ -1118,12 +1117,13 @@ TxQ::accept(Application& app, } auto -TxQ::getMetrics(Application& app, OpenView const& view) const - -> boost::optional +TxQ::getMetrics(Config const& config, OpenView const& view, + std::uint32_t txCountPadding) const + -> boost::optional { auto const allowEscalation = (view.rules().enabled(featureFeeEscalation, - app.config().features)); + config.features)); if (!allowEscalation) return boost::none; @@ -1135,11 +1135,11 @@ TxQ::getMetrics(Application& app, OpenView const& view) const result.txQMaxSize = maxSize_; result.txInLedger = view.txCount(); result.txPerLedger = feeMetrics_.getTxnsExpected(); - result.referenceFeeLevel = feeMetrics_.baseLevel; + result.referenceFeeLevel = baseLevel; result.minFeeLevel = isFull() ? byFee_.rbegin()->feeLevel + 1 : - feeMetrics_.baseLevel; + baseLevel; result.medFeeLevel = feeMetrics_.getEscalationMultiplier(); - result.expFeeLevel = feeMetrics_.scaleFeeLevel(view); + result.expFeeLevel = feeMetrics_.scaleFeeLevel(view, txCountPadding); return result; } @@ -1150,8 +1150,8 @@ TxQ::doRPC(Application& app) const using std::to_string; auto const view = app.openLedger().current(); - auto const metrics = getMetrics(app, *view); - assert(metrics); + auto const metrics = getMetrics(app.config(), *view); + if (!metrics) return{}; @@ -1183,9 +1183,14 @@ TxQ::doRPC(Application& app) const drops[jss::median_fee] = to_string(mulDiv( metrics->medFeeLevel, baseFee, metrics->referenceFeeLevel).second); - drops[jss::open_ledger_fee] = to_string(mulDiv( + auto escalatedFee = mulDiv( metrics->expFeeLevel, baseFee, - metrics->referenceFeeLevel).second); + metrics->referenceFeeLevel).second; + if (mulDiv(escalatedFee, metrics->referenceFeeLevel, + baseFee).second < metrics->expFeeLevel) + ++escalatedFee; + + drops[jss::open_ledger_fee] = to_string(escalatedFee); return ret; } diff --git a/src/ripple/app/tests/TxQ_test.cpp b/src/ripple/app/tests/TxQ_test.cpp index 7fb676b612..af59d360b5 100644 --- a/src/ripple/app/tests/TxQ_test.cpp +++ b/src/ripple/app/tests/TxQ_test.cpp @@ -45,10 +45,10 @@ class TxQ_test : public beast::unit_test::suite std::size_t expectedInLedger, std::size_t expectedPerLedger, std::uint64_t expectedMinFeeLevel, - std::uint64_t expectedMedFeeLevel = 500) + std::uint64_t expectedMedFeeLevel = 256 * 500) { - auto optMetrics = env.app().getTxQ().getMetrics(env.app(), - *env.current()); + auto optMetrics = env.app().getTxQ().getMetrics( + env.app().config(), *env.current()); if (!expect(optMetrics)) return; auto& metrics = *optMetrics; @@ -60,10 +60,9 @@ class TxQ_test : public beast::unit_test::suite expect(metrics.minFeeLevel == expectedMinFeeLevel, "minFeeLevel"); expect(metrics.medFeeLevel == expectedMedFeeLevel, "medFeeLevel"); auto expectedCurFeeLevel = expectedInLedger > expectedPerLedger ? - metrics.referenceFeeLevel * expectedMedFeeLevel * - expectedInLedger * expectedInLedger / - (expectedPerLedger * expectedPerLedger) : - metrics.referenceFeeLevel; + expectedMedFeeLevel * expectedInLedger * expectedInLedger / + (expectedPerLedger * expectedPerLedger) : + metrics.referenceFeeLevel; expect(metrics.expFeeLevel == expectedCurFeeLevel, "expFeeLevel"); } @@ -72,7 +71,7 @@ class TxQ_test : public beast::unit_test::suite jtx::Env& env, jtx::Account const& account) { - auto metrics = env.app().getTxQ().getMetrics(env.app(), + auto metrics = env.app().getTxQ().getMetrics(env.app().config(), *env.current()); if (!expect(metrics)) return; @@ -86,7 +85,7 @@ class TxQ_test : public beast::unit_test::suite using namespace jtx; auto const& view = *env.current(); - auto metrics = env.app().getTxQ().getMetrics(env.app(), + auto metrics = env.app().getTxQ().getMetrics(env.app().config(), view); if (!expect(metrics)) return fee(none); @@ -222,7 +221,7 @@ public: // Close again with a simulated time leap to // reset the escalation limit down to minimum env.close(env.now() + 5s, 10000ms); - checkMetrics(env, 0, 16, 0, 3, 256, 76928); + checkMetrics(env, 0, 16, 0, 3, 256); // Then close once more without the time leap // to reset the queue maxsize down to minimum env.close(); @@ -232,10 +231,10 @@ public: // Stuff the ledger and queue so we can verify that // stuff gets kicked out. - env(noop(hank)); - env(noop(gwen)); - env(noop(fred)); - env(noop(elmo)); + env(noop(hank), fee(7000)); + env(noop(gwen), fee(7000)); + env(noop(fred), fee(7000)); + env(noop(elmo), fee(7000)); checkMetrics(env, 0, 6, 4, 3, 256); // Use explicit fees so we can control which txn @@ -270,7 +269,7 @@ public: // point, daria's transaction that was dropped from the queue // is put back in. Neat. env.close(); - checkMetrics(env, 2, 8, 5, 4, 256); + checkMetrics(env, 2, 8, 5, 4, 256, 256 * 700); env.close(); checkMetrics(env, 0, 10, 2, 5, 256); @@ -282,7 +281,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.app(), *env.current()); + auto metrics = txq.getMetrics(env.app().config(), *env.current()); expect(metrics->txCount == 0, "txCount"); // Stuff the ledger. @@ -399,50 +398,50 @@ public: queued); // Queue items with higher fees to force the previous // txn to wait. - env(noop(bob), fee(20), queued); - env(noop(charlie), fee(20), queued); - env(noop(daria), fee(20), queued); - env(noop(edgar), fee(20), queued); + env(noop(bob), fee(7000), queued); + env(noop(charlie), fee(7000), queued); + env(noop(daria), fee(7000), queued); + env(noop(edgar), fee(7000), queued); checkMetrics(env, 5, boost::none, 3, 2, 256); env.close(); checkMetrics(env, 1, 6, 4, 3, 256); // Keep alice's transaction waiting. - env(noop(bob), fee(20), queued); - env(noop(charlie), fee(20), queued); - env(noop(daria), fee(20), queued); - env(noop(edgar), fee(20), queued); - env(noop(felicia), fee(20), queued); + env(noop(bob), fee(7000), queued); + env(noop(charlie), fee(7000), queued); + env(noop(daria), fee(7000), queued); + env(noop(edgar), fee(7000), queued); + env(noop(felicia), fee(7000), queued); checkMetrics(env, 6, 6, 4, 3, 257); env.close(); // alice's transaction is still hanging around - checkMetrics(env, 1, 8, 5, 4, 256, 512); + checkMetrics(env, 1, 8, 5, 4, 256, 700 * 256); expect(env.seq(alice) == 1); // Keep alice's transaction waiting. - env(noop(bob), fee(20), queued); - env(noop(charlie), fee(20), queued); - env(noop(daria), fee(20), queued); - env(noop(daria), fee(20), seq(env.seq(daria) + 1), + env(noop(bob), fee(8000), queued); + env(noop(charlie), fee(8000), queued); + env(noop(daria), fee(8000), queued); + env(noop(daria), fee(8000), seq(env.seq(daria) + 1), queued); - env(noop(edgar), fee(20), queued); - env(noop(felicia), fee(20), queued); - env(noop(felicia), fee(20), seq(env.seq(felicia) + 1), + env(noop(edgar), fee(8000), queued); + env(noop(felicia), fee(8000), queued); + env(noop(felicia), fee(8000), seq(env.seq(felicia) + 1), queued); - checkMetrics(env, 8, 8, 5, 4, 257, 512); + checkMetrics(env, 8, 8, 5, 4, 257, 700 * 256); env.close(); // alice's transaction expired without getting // into the ledger, so her transaction is gone, // though one of felicia's is still in the queue. - checkMetrics(env, 1, 10, 6, 5, 256, 512); + checkMetrics(env, 1, 10, 6, 5, 256, 700 * 256); expect(env.seq(alice) == 1); env.close(); // And now the queue is empty - checkMetrics(env, 0, 12, 1, 6, 256, 512); + checkMetrics(env, 0, 12, 1, 6, 256, 800 * 256); expect(env.seq(alice) == 1); } @@ -544,12 +543,12 @@ public: env.close(); - checkMetrics(env, 0, 12, 4, 6, 256, 1600); + checkMetrics(env, 0, 12, 4, 6, 256); expect(env.seq(bob) == seqBob + 1); expect(env.seq(carol) == seqCarol + 1); env.close(); - checkMetrics(env, 0, 12, 0, 6, 256, 2739); + checkMetrics(env, 0, 12, 0, 6, 256); expect(env.seq(bob) == seqBob + 1); expect(env.seq(carol) == seqCarol + 1); } @@ -824,10 +823,10 @@ public: env.close(); // All of Alice's transactions applied. - checkMetrics(env, 0, 12, 4, 6, 256, 640); + checkMetrics(env, 0, 12, 4, 6, 256); env.close(); - checkMetrics(env, 0, 12, 0, 6, 256, 1203); + checkMetrics(env, 0, 12, 0, 6, 256); // Alice is broke env.require(balance(alice, XRP(0))); @@ -980,7 +979,7 @@ public: auto alice = Account("alice"); - expect(!env.app().getTxQ().getMetrics(env.app(), + expect(!env.app().getTxQ().getMetrics(env.app().config(), *env.current())); env.fund(XRP(50000), noripple(alice)); @@ -992,7 +991,7 @@ public: env(noop(alice), fee(30)); env.close(); - expect(!env.app().getTxQ().getMetrics(env.app(), + expect(!env.app().getTxQ().getMetrics(env.app().config(), *env.current())); } diff --git a/src/ripple/rpc/handlers/SignFor.cpp b/src/ripple/rpc/handlers/SignFor.cpp index 655d170db8..1495312140 100755 --- a/src/ripple/rpc/handlers/SignFor.cpp +++ b/src/ripple/rpc/handlers/SignFor.cpp @@ -50,8 +50,7 @@ Json::Value doSignFor (RPC::Context& context) failType, context.role, context.ledgerMaster.getValidatedLedgerAge(), - context.app, - context.ledgerMaster.getCurrentLedger()); + context.app); } } // ripple diff --git a/src/ripple/rpc/handlers/SignHandler.cpp b/src/ripple/rpc/handlers/SignHandler.cpp index 1b77895b35..aad1535de3 100644 --- a/src/ripple/rpc/handlers/SignHandler.cpp +++ b/src/ripple/rpc/handlers/SignHandler.cpp @@ -43,8 +43,7 @@ Json::Value doSign (RPC::Context& context) failType, context.role, context.ledgerMaster.getValidatedLedgerAge(), - context.app, - context.ledgerMaster.getCurrentLedger()); + context.app); } } // ripple diff --git a/src/ripple/rpc/handlers/Submit.cpp b/src/ripple/rpc/handlers/Submit.cpp index 82340d9d72..582f80f9a7 100644 --- a/src/ripple/rpc/handlers/Submit.cpp +++ b/src/ripple/rpc/handlers/Submit.cpp @@ -55,7 +55,6 @@ Json::Value doSubmit (RPC::Context& context) context.role, context.ledgerMaster.getValidatedLedgerAge(), context.app, - context.ledgerMaster.getCurrentLedger(), RPC::getProcessTxnFn (context.netOps)); } diff --git a/src/ripple/rpc/handlers/SubmitMultiSigned.cpp b/src/ripple/rpc/handlers/SubmitMultiSigned.cpp index 3430ffe36e..743036bab5 100644 --- a/src/ripple/rpc/handlers/SubmitMultiSigned.cpp +++ b/src/ripple/rpc/handlers/SubmitMultiSigned.cpp @@ -50,7 +50,6 @@ Json::Value doSubmitMultiSigned (RPC::Context& context) context.role, context.ledgerMaster.getValidatedLedgerAge(), context.app, - context.ledgerMaster.getCurrentLedger(), RPC::getProcessTxnFn (context.netOps)); } diff --git a/src/ripple/rpc/impl/TransactionSign.cpp b/src/ripple/rpc/impl/TransactionSign.cpp index 7e0a258a49..bf12d97f3a 100644 --- a/src/ripple/rpc/impl/TransactionSign.cpp +++ b/src/ripple/rpc/impl/TransactionSign.cpp @@ -20,8 +20,10 @@ #include #include #include +#include #include #include +#include #include #include // Validity::Valid #include @@ -31,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -339,7 +342,7 @@ transactionPreProcessImpl ( SigningForParams& signingArgs, std::chrono::seconds validatedLedgerAge, Application& app, - std::shared_ptr const& ledger) + std::shared_ptr const& ledger) { auto j = app.journal ("RPCHandler"); @@ -393,6 +396,7 @@ transactionPreProcessImpl ( verify && signingArgs.editFields(), app.config(), app.getFeeTrack(), + app.getTxQ(), ledger); if (RPC::contains_error (err)) @@ -627,7 +631,8 @@ Json::Value checkFee ( bool doAutoFill, Config const& config, LoadFeeTrack const& feeTrack, - std::shared_ptr const& ledger) + TxQ const& txQ, + std::shared_ptr const& ledger) { Json::Value& tx (request[jss::tx_json]); if (tx.isMember (jss::Fee)) @@ -670,14 +675,42 @@ Json::Value checkFee ( std::uint64_t const feeDefault = config.TRANSACTION_FEE_BASE; // Administrative and identified endpoints are exempt from local fees. - std::uint64_t const fee = + std::uint64_t const loadFee = feeTrack.scaleFeeLoad (feeDefault, ledger->fees().base, ledger->fees().units, isUnlimited (role)); + std::uint64_t fee = loadFee; + if (ledger->rules().enabled(featureFeeEscalation, + config.features)) + { + auto const assumeTx = request.isMember("x-assume-tx") && + request["x-assume-tx"].isConvertibleTo(Json::uintValue) ? + request["x-assume-tx"].asUInt() : 0; + auto const metrics = txQ.getMetrics(config, *ledger, assumeTx); + assert(metrics); + if(metrics) + { + auto const baseFee = ledger->fees().base; + auto escalatedFee = mulDiv( + metrics->expFeeLevel, baseFee, + metrics->referenceFeeLevel).second; + if (mulDiv(escalatedFee, metrics->referenceFeeLevel, + baseFee).second < metrics->expFeeLevel) + ++escalatedFee; + fee = std::max(fee, escalatedFee); + } + } auto const limit = mulDivThrow(feeTrack.scaleFeeBase ( feeDefault, ledger->fees().base, ledger->fees().units), mult, div); + if (fee > limit && fee != loadFee && + request.isMember("x-queue-okay") && + request["x-queue-okay"].isBool() && + request["x-queue-okay"].asBool()) + { + fee = loadFee; + } if (fee > limit) { std::stringstream ss; @@ -698,11 +731,11 @@ Json::Value transactionSign ( NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, - Application& app, - std::shared_ptr const& ledger) + Application& app) { using namespace detail; + auto const& ledger = app.openLedger().current(); auto j = app.journal ("RPCHandler"); JLOG (j.debug()) << "transactionSign: " << jvRequest; @@ -733,11 +766,11 @@ Json::Value transactionSubmit ( Role role, std::chrono::seconds validatedLedgerAge, Application& app, - std::shared_ptr const& ledger, ProcessTransactionFn const& processTransaction) { using namespace detail; + auto const& ledger = app.openLedger().current(); auto j = app.journal ("RPCHandler"); JLOG (j.debug()) << "transactionSubmit: " << jvRequest; @@ -860,9 +893,9 @@ Json::Value transactionSignFor ( NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, - Application& app, - std::shared_ptr const& ledger) + Application& app) { + auto const& ledger = app.openLedger().current(); auto j = app.journal ("RPCHandler"); JLOG (j.debug()) << "transactionSignFor: " << jvRequest; @@ -975,9 +1008,9 @@ Json::Value transactionSubmitMultiSigned ( Role role, std::chrono::seconds validatedLedgerAge, Application& app, - std::shared_ptr const& ledger, ProcessTransactionFn const& processTransaction) { + auto const& ledger = app.openLedger().current(); auto j = app.journal ("RPCHandler"); JLOG (j.debug()) << "transactionSubmitMultiSigned: " << jvRequest; @@ -1018,7 +1051,8 @@ Json::Value transactionSubmitMultiSigned ( { Json::Value err = checkFee ( - jvRequest, role, false, app.config(), app.getFeeTrack(), ledger); + jvRequest, role, false, app.config(), app.getFeeTrack(), + app.getTxQ(), ledger); if (RPC::contains_error(err)) return err; diff --git a/src/ripple/rpc/impl/TransactionSign.h b/src/ripple/rpc/impl/TransactionSign.h index d3f14989ee..660f42e869 100644 --- a/src/ripple/rpc/impl/TransactionSign.h +++ b/src/ripple/rpc/impl/TransactionSign.h @@ -30,6 +30,7 @@ namespace ripple { class Application; class LoadFeeTrack; class Transaction; +class TxQ; namespace RPC { @@ -66,7 +67,8 @@ Json::Value checkFee ( bool doAutoFill, Config const& config, LoadFeeTrack const& feeTrack, - std::shared_ptr const& ledger); + TxQ const& txQ, + std::shared_ptr const& ledger); // Return a std::function<> that calls NetworkOPs::processTransaction. using ProcessTransactionFn = @@ -88,8 +90,7 @@ Json::Value transactionSign ( NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, - Application& app, - std::shared_ptr const& ledger); + Application& app); /** Returns a Json::objectValue. */ Json::Value transactionSubmit ( @@ -98,7 +99,6 @@ Json::Value transactionSubmit ( Role role, std::chrono::seconds validatedLedgerAge, Application& app, - std::shared_ptr const& ledger, ProcessTransactionFn const& processTransaction); /** Returns a Json::objectValue. */ @@ -107,8 +107,7 @@ Json::Value transactionSignFor ( NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, - Application& app, - std::shared_ptr const& ledger); + Application& app); /** Returns a Json::objectValue. */ Json::Value transactionSubmitMultiSigned ( @@ -117,7 +116,6 @@ Json::Value transactionSubmitMultiSigned ( Role role, std::chrono::seconds validatedLedgerAge, Application& app, - std::shared_ptr const& ledger, ProcessTransactionFn const& processTransaction); } // RPC diff --git a/src/ripple/rpc/tests/JSONRPC.test.cpp b/src/ripple/rpc/tests/JSONRPC.test.cpp index ad368ca8c5..177735a825 100644 --- a/src/ripple/rpc/tests/JSONRPC.test.cpp +++ b/src/ripple/rpc/tests/JSONRPC.test.cpp @@ -18,6 +18,7 @@ //============================================================================== #include +#include #include #include #include @@ -1841,10 +1842,8 @@ public: void testAutoFillFees () { test::jtx::Env env(*this); - std::shared_ptr ledger = - std::make_shared(create_genesis, - env.app().config(), env.app().family()); - LoadFeeTrack const feeTrack; + auto ledger = env.current(); + LoadFeeTrack const& feeTrack = env.app().getFeeTrack(); { Json::Value req; @@ -1852,9 +1851,12 @@ public: "{ \"fee_mult_max\" : 1, \"tx_json\" : { } } ", req); Json::Value result = checkFee (req, Role::ADMIN, true, - env.app().config(), feeTrack, ledger); + env.app().config(), feeTrack, + env.app().getTxQ(), ledger); expect (! RPC::contains_error (result), "Legal checkFee"); + expect(req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == 10); } { @@ -1864,9 +1866,12 @@ public: "\"tx_json\" : { } } ", req); Json::Value result = checkFee(req, Role::ADMIN, true, - env.app().config(), feeTrack, ledger); + env.app().config(), feeTrack, + env.app().getTxQ(), ledger); expect(!RPC::contains_error(result), "Legal checkFee"); + expect(req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == 10); } { @@ -1875,9 +1880,11 @@ public: "{ \"fee_mult_max\" : 0, \"tx_json\" : { } } ", req); Json::Value result = checkFee (req, Role::ADMIN, true, - env.app().config(), feeTrack, ledger); + env.app().config(), feeTrack, + env.app().getTxQ(), ledger); expect (RPC::contains_error (result), "Invalid checkFee"); + expect(!req[jss::tx_json].isMember(jss::Fee)); } { @@ -1889,9 +1896,11 @@ public: "\"tx_json\" : { } } ", req); Json::Value result = checkFee(req, Role::ADMIN, true, - env.app().config(), feeTrack, ledger); + env.app().config(), feeTrack, + env.app().getTxQ(), ledger); expect(RPC::contains_error(result), "Invalid checkFee"); + expect(!req[jss::tx_json].isMember(jss::Fee)); } { @@ -1901,9 +1910,11 @@ public: "\"tx_json\" : { } } ", req); Json::Value result = checkFee(req, Role::ADMIN, true, - env.app().config(), feeTrack, ledger); + env.app().config(), feeTrack, + env.app().getTxQ(), ledger); expect(RPC::contains_error(result), "Invalid checkFee"); + expect(!req[jss::tx_json].isMember(jss::Fee)); } { @@ -1913,12 +1924,153 @@ public: "\"tx_json\" : { } } ", req); Json::Value result = checkFee(req, Role::ADMIN, true, - env.app().config(), feeTrack, ledger); + env.app().config(), feeTrack, + env.app().getTxQ(), ledger); expect(RPC::contains_error(result), "Divide by 0"); + expect(!req[jss::tx_json].isMember(jss::Fee)); } } + static + std::unique_ptr + makeConfig() + { + auto p = std::make_unique(); + test::setupConfigForUnitTests(*p); + auto& section = p->section("transaction_queue"); + section.set("minimum_txn_in_ledger_standalone", "3"); + return p; + } + + void testAutoFillEscalatedFees () + { + test::jtx::Env env(*this, makeConfig(), + test::jtx::features(featureFeeEscalation)); + LoadFeeTrack const& feeTrack = env.app().getFeeTrack(); + + { + // 1: high mult, no queue, no pad + Json::Value req; + Json::Reader ().parse ( + "{ \"fee_mult_max\" : 1000, \"tx_json\" : { } } ", req); + Json::Value result = + checkFee (req, Role::ADMIN, true, + env.app().config(), feeTrack, + env.app().getTxQ(), env.current()); + + expect (! RPC::contains_error (result), "Legal checkFee"); + expect(req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == 10); + } + + { + // 2: high mult, can queue, no pad + Json::Value req; + Json::Reader().parse( + "{ \"fee_mult_max\" : 1000, \"x-queue-okay\" : true \"tx_json\" : { } } ", req); + Json::Value result = + checkFee(req, Role::ADMIN, true, + env.app().config(), feeTrack, + env.app().getTxQ(), env.current()); + + expect(!RPC::contains_error(result), "Legal checkFee"); + expect(req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == 10); + } + + { + // 3: high mult, no queue, 4 pad + Json::Value req; + Json::Reader().parse( + "{ \"fee_mult_max\" : 1000, \"x-assume-tx\" : 4, \"tx_json\" : { } } ", req); + Json::Value result = + checkFee(req, Role::ADMIN, true, + env.app().config(), feeTrack, + env.app().getTxQ(), env.current()); + + expect(!RPC::contains_error(result), "Legal checkFee"); + expect(req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == 8889); + } + + { + // 4: high mult, can queue, 4 pad + Json::Value req; + Json::Reader().parse( + "{ \"fee_mult_max\" : 1000, \"x-assume-tx\" : 4, \"x-queue-okay\" : true \"tx_json\" : { } } ", req); + Json::Value result = + checkFee(req, Role::ADMIN, true, + env.app().config(), feeTrack, + env.app().getTxQ(), env.current()); + + expect(!RPC::contains_error(result), "Legal checkFee"); + expect(req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == 8889); + } + + /////////////////// + { + // 5: low mult, no queue, no pad + Json::Value req; + Json::Reader().parse( + "{ \"fee_mult_max\" : 5, \"tx_json\" : { } } ", req); + Json::Value result = + checkFee(req, Role::ADMIN, true, + env.app().config(), feeTrack, + env.app().getTxQ(), env.current()); + + expect(!RPC::contains_error(result), "Legal checkFee"); + expect(req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == 10); + } + + { + // 6: low mult, can queue, no pad + Json::Value req; + Json::Reader().parse( + "{ \"fee_mult_max\" : 5, \"x-queue-okay\" : true \"tx_json\" : { } } ", req); + Json::Value result = + checkFee(req, Role::ADMIN, true, + env.app().config(), feeTrack, + env.app().getTxQ(), env.current()); + + expect(!RPC::contains_error(result), "Legal checkFee"); + expect(req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == 10); + } + + { + // 7: low mult, no queue, 4 pad + Json::Value req; + Json::Reader().parse( + "{ \"fee_mult_max\" : 5, \"x-assume-tx\" : 4, \"tx_json\" : { } } ", req); + Json::Value result = + checkFee(req, Role::ADMIN, true, + env.app().config(), feeTrack, + env.app().getTxQ(), env.current()); + + expect(RPC::contains_error(result), "Invalid checkFee"); + expect(!req[jss::tx_json].isMember(jss::Fee)); + } + + { + // 8: : low mult, can queue, 4 pad + Json::Value req; + Json::Reader().parse( + "{ \"fee_mult_max\" : 5, \"x-assume-tx\" : 4, \"x-queue-okay\" : true \"tx_json\" : { } } ", req); + Json::Value result = + checkFee(req, Role::ADMIN, true, + env.app().config(), feeTrack, + env.app().getTxQ(), env.current()); + + expect(!RPC::contains_error(result), "Legal checkFee"); + expect(req[jss::tx_json].isMember(jss::Fee) && + req[jss::tx_json][jss::Fee] == 10); + } + + } + // A function that can be called as though it would process a transaction. static void fakeProcessTransaction ( std::shared_ptr&, bool, bool, NetworkOPs::FailHard) @@ -1951,8 +2103,6 @@ public: env(pay(g, env.master, USD(50))); env.close(); - auto const ledger = env.current(); - ProcessTransactionFn processTxn = fakeProcessTransaction; // A list of all the functions we want to test. @@ -1961,8 +2111,7 @@ public: NetworkOPs::FailHard failType, Role role, std::chrono::seconds validatedLedgerAge, - Application& app, - std::shared_ptr const& ledger); + Application& app); using submitFunc = Json::Value (*) ( Json::Value params, @@ -1970,7 +2119,6 @@ public: Role role, std::chrono::seconds validatedLedgerAge, Application& app, - std::shared_ptr const& ledger, ProcessTransactionFn const& processTransaction); using TestStuff = @@ -2010,8 +2158,7 @@ public: NetworkOPs::FailHard::yes, testRole, 1s, - env.app(), - ledger); + env.app()); } else { @@ -2023,7 +2170,6 @@ public: testRole, 1s, env.app(), - ledger, processTxn); } @@ -2044,6 +2190,7 @@ public: void run () { testAutoFillFees (); + testAutoFillEscalatedFees (); testTransactionRPC (); } }; diff --git a/src/ripple/rpc/tests/Subscribe.test.cpp b/src/ripple/rpc/tests/Subscribe.test.cpp index 0a21312d30..6d1996330d 100644 --- a/src/ripple/rpc/tests/Subscribe.test.cpp +++ b/src/ripple/rpc/tests/Subscribe.test.cpp @@ -313,7 +313,7 @@ public: jv[jss::flags] == (vfFullyCanonicalSig | STValidation::kFullFlag) && jv[jss::full] == true && - jv[jss::load_fee] == 256000 && + !jv.isMember(jss::load_fee) && jv[jss::signature] && jv[jss::signing_time]; }));