Transaction queue and fee escalation (RIPD-598):

The first few transactions are added to the open ledger at
the base fee (ie. 10 drops).  Once enough transactions are
added, the required fee will jump dramatically. If additional
transactions are added, the fee will grow exponentially.

Transactions that don't have a high enough fee to be applied to
the ledger are added to the queue in order from highest fee to
lowest. Whenever a new ledger is accepted as validated, transactions
are first applied from the queue to the open ledger in fee order
until either all transactions are applied or the fee again jumps
too high for the remaining transactions.

Current implementation is restricted to one transaction in the
queue per account. Some groundwork has been laid to expand in
the future.

Note that this fee logic escalates independently of the load-based
fee logic (ie. LoadFeeTrack). Submitted transactions must meet
the load fee to be considered for the queue, and must meet both
fees to be put into open ledger.
This commit is contained in:
Edward Hennis
2015-07-10 20:00:21 -04:00
parent dc1276efa3
commit 9329aafe53
39 changed files with 2454 additions and 421 deletions

View File

@@ -0,0 +1,368 @@
//------------------------------------------------------------------------------
/*
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
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 <ripple/app/main/Application.h>
#include <ripple/app/misc/TxQ.h>
#include <ripple/app/ledger/LedgerConsensus.h>
#include <ripple/core/LoadFeeTrack.h>
#include <ripple/basics/Log.h>
#include <ripple/basics/TestSuite.h>
#include <ripple/protocol/JsonFields.h>
#include <ripple/protocol/STTx.h>
#include <ripple/test/jtx.h>
namespace ripple {
namespace test {
class TxQ_test : public TestSuite
{
void
checkMetrics(
jtx::Env& env,
std::size_t expectedCount,
boost::optional<std::size_t> expectedMaxCount,
std::size_t expectedInLedger,
std::size_t expectedPerLedger,
std::uint64_t expectedMinFeeLevel,
std::uint64_t expectedMedFeeLevel)
{
auto metrics = env.app().getTxQ().getMetrics(*env.open());
expect(metrics.referenceFeeLevel == 256, "referenceFeeLevel");
expect(metrics.txCount == expectedCount, "txCount");
expect(metrics.txQMaxSize == expectedMaxCount, "txQMaxSize");
expect(metrics.txInLedger == expectedInLedger, "txInLedger");
expect(metrics.txPerLedger == expectedPerLedger, "txPerLedger");
expect(metrics.minFeeLevel == expectedMinFeeLevel, "minFeeLevel");
expect(metrics.medFeeLevel == expectedMedFeeLevel, "medFeeLevel");
auto expectedCurFeeLevel = expectedInLedger > expectedPerLedger ?
metrics.referenceFeeLevel * expectedMedFeeLevel *
expectedInLedger * expectedInLedger /
(expectedPerLedger * expectedPerLedger) :
metrics.referenceFeeLevel;
expect(metrics.expFeeLevel == expectedCurFeeLevel, "expFeeLevel");
}
void
close(jtx::Env& env, size_t expectedTxSetSize, bool timeLeap = false)
{
{
auto const view = env.open();
expect(view->txCount() == expectedTxSetSize, "TxSet size mismatch");
// Update fee computations.
// Note implementing this way assumes that everything
// in the open ledger _will_ make it into the closed
// ledger, but for metrics that's probably good enough.
env.app().getTxQ().processValidatedLedger(
env.app(), *view, timeLeap, tapENABLE_TESTING);
}
env.close(
[&](OpenView& view, beast::Journal j)
{
// Stuff the ledger with transactions from the queue.
return env.app().getTxQ().accept(env.app(), view,
tapENABLE_TESTING);
}
);
}
void
submit(jtx::Env& env, jtx::JTx const& jt)
{
// Env checks this, but this test shouldn't
// generate any malformed txns.
expect(jt.stx);
bool didApply;
TER ter;
env.openLedger.modify(
[&](OpenView& view, beast::Journal j)
{
std::tie(ter, didApply) =
env.app().getTxQ().apply(env.app(),
view, jt.stx, tapENABLE_TESTING,
env.journal);
return didApply;
}
);
env.postconditions(jt, ter, didApply);
}
static
std::unique_ptr<Config const>
makeConfig()
{
auto p = std::make_unique<Config>();
setupConfigForUnitTests(*p);
auto& section = p->section("transaction_queue");
section.set("ledgers_in_queue", "2");
section.set("min_ledgers_to_compute_size_limit", "3");
section.set("max_ledger_counts_to_store", "100");
section.set("retry_sequence_percent", "125");
return std::unique_ptr<Config const>(p.release());
}
public:
void run()
{
using namespace jtx;
Env env(*this, makeConfig());
auto& txq = env.app().getTxQ();
txq.setMinimumTx(3);
auto alice = Account("alice");
auto bob = Account("bob");
auto charlie = Account("charlie");
auto daria = Account("daria");
auto elmo = Account("elmo");
auto fred = Account("fred");
auto gwen = Account("gwen");
auto hank = Account("hank");
auto queued = ter(terQUEUED);
expectEquals(env.open()->fees().base, 10);
checkMetrics(env, 0, boost::none, 0, 3, 256, 500);
// Create several accounts while the fee is cheap so they all apply.
env.fund(XRP(50000), noripple(alice, bob, charlie, daria));
checkMetrics(env, 0, boost::none, 4, 3, 256, 500);
// Alice - price starts exploding: held
submit(env,
env.jt(noop(alice), queued));
checkMetrics(env, 1, boost::none, 4, 3, 256, 500);
// Alice - Alice is already in the queue, so can't hold.
submit(env,
env.jt(noop(alice), seq(env.seq(alice) + 1),
ter(telINSUF_FEE_P)));
checkMetrics(env, 1, boost::none, 4, 3, 256, 500);
auto openLedgerFee =
[&]()
{
return fee(txq.openLedgerFee(*env.open()));
};
// Alice's next transaction -
// fails because the item in the TxQ hasn't applied.
submit(env,
env.jt(noop(alice), openLedgerFee(),
seq(env.seq(alice) + 1), ter(terPRE_SEQ)));
checkMetrics(env, 1, boost::none, 4, 3, 256, 500);
// Bob with really high fee - applies
submit(env,
env.jt(noop(bob), openLedgerFee()));
checkMetrics(env, 1, boost::none, 5, 3, 256, 500);
// Daria with low fee: hold
submit(env,
env.jt(noop(daria), fee(1000), queued));
checkMetrics(env, 2, boost::none, 5, 3, 256, 500);
close(env, 5);
// Verify that the held transactions got applied
auto lastMedian = 500;
checkMetrics(env, 0, 10, 2, 5, 256, lastMedian);
//////////////////////////////////////////////////////////////
// Make some more accounts. We'll need them later to abuse the queue.
env.fund(XRP(50000), noripple(elmo, fred, gwen, hank));
checkMetrics(env, 0, 10, 6, 5, 256, lastMedian);
// Now get a bunch of transactions held.
submit(env,
env.jt(noop(alice), fee(12), queued));
checkMetrics(env, 1, 10, 6, 5, 256, lastMedian);
submit(env,
env.jt(noop(bob), fee(10), queued)); // won't clear the queue
submit(env,
env.jt(noop(charlie), fee(20), queued));
submit(env,
env.jt(noop(daria), fee(15), queued));
submit(env,
env.jt(noop(elmo), fee(11), queued));
submit(env,
env.jt(noop(fred), fee(19), queued));
submit(env,
env.jt(noop(gwen), fee(16), queued));
submit(env,
env.jt(noop(hank), fee(18), queued));
checkMetrics(env, 8, 10, 6, 5, 256, lastMedian);
close(env, 6);
// Verify that the held transactions got applied
lastMedian = 500;
checkMetrics(env, 1, 12, 7, 6, 256, lastMedian);
// Bob's transaction is still stuck in the queue.
//////////////////////////////////////////////////////////////
// Hank sends another txn
submit(env,
env.jt(noop(hank), fee(10), queued));
// But he's not going to leave it in the queue
checkMetrics(env, 2, 12, 7, 6, 256, lastMedian);
// Hank sees his txn got held and bumps the fee,
// but doesn't even bump it enough to requeue
submit(env,
env.jt(noop(hank), fee(11), ter(telINSUF_FEE_P)));
checkMetrics(env, 2, 12, 7, 6, 256, lastMedian);
// Hank sees his txn got held and bumps the fee,
// enough to requeue, but doesn't bump it enough to
// apply to the ledger
submit(env,
env.jt(noop(hank), fee(6000), queued));
// But he's not going to leave it in the queue
checkMetrics(env, 2, 12, 7, 6, 256, lastMedian);
// Hank sees his txn got held and bumps the fee,
// high enough to get into the open ledger, because
// he doesn't want to wait.
submit(env,
env.jt(noop(hank), openLedgerFee()));
checkMetrics(env, 1, 12, 8, 6, 256, lastMedian);
// Hank then sends another, less important txn
// (In addition to the metrics, this will verify that
// the original txn got removed.)
submit(env,
env.jt(noop(hank), fee(6000), queued));
checkMetrics(env, 2, 12, 8, 6, 256, lastMedian);
close(env, 8);
// Verify that bob and hank's txns were applied
lastMedian = 500;
checkMetrics(env, 0, 16, 2, 8, 256, lastMedian);
// Close again with a simulated time leap to
// reset the escalation limit down to minimum
lastMedian = 76928;
close(env, 2, true);
checkMetrics(env, 0, 16, 0, 3, 256, lastMedian);
// Then close once more without the time leap
// to reset the queue maxsize down to minimum
lastMedian = 500;
close(env, 0);
checkMetrics(env, 0, 6, 0, 3, 256, lastMedian);
//////////////////////////////////////////////////////////////
// At this point, the queue should have a limit of 6.
// Stuff the ledger and queue so we can verify that
// stuff gets kicked out.
submit(env,
env.jt(noop(hank)));
submit(env,
env.jt(noop(gwen)));
submit(env,
env.jt(noop(fred)));
submit(env,
env.jt(noop(elmo)));
checkMetrics(env, 0, 6, 4, 3, 256, lastMedian);
// Use explicit fees so we can control which txn
// will get dropped
submit(env,
env.jt(noop(alice), fee(20), queued));
submit(env,
env.jt(noop(hank), fee(19), queued));
submit(env,
env.jt(noop(gwen), fee(18), queued));
submit(env,
env.jt(noop(fred), fee(17), queued));
submit(env,
env.jt(noop(elmo), fee(16), queued));
// This one gets into the queue, but gets dropped when the
// higher fee one is added later.
submit(env,
env.jt(noop(daria), fee(15), queued));
// Queue is full now.
checkMetrics(env, 6, 6, 4, 3, 385, lastMedian);
// Try to add another transaction with the default (low) fee,
// it should fail because the queue is full.
submit(env,
env.jt(noop(charlie), ter(telINSUF_FEE_P)));
// 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 somebody out)
submit(env,
env.jt(noop(charlie), fee(100), queued));
// Queue is still full, of course, but the min fee has gone up
checkMetrics(env, 6, 6, 4, 3, 410, lastMedian);
close(env, 4);
lastMedian = 500;
checkMetrics(env, 1, 8, 5, 4, 256, lastMedian);
lastMedian = 500;
close(env, 5);
checkMetrics(env, 0, 10, 1, 5, 256, lastMedian);
//////////////////////////////////////////////////////////////
// Cleanup:
// Create a few more transactions, so that
// 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.open());
expect(metrics.txCount == 0, "txCount");
auto txnsNeeded = metrics.txPerLedger - metrics.txInLedger;
// Stuff the ledger.
for (int i = 0; i <= txnsNeeded; ++i)
{
submit(env,
env.jt(noop(env.master)));
}
// Queue one straightforward transaction
submit(env,
env.jt(noop(env.master), fee(20), queued));
++metrics.txCount;
checkMetrics(env, metrics.txCount,
metrics.txQMaxSize, metrics.txPerLedger + 1,
metrics.txPerLedger,
256, lastMedian);
}
};
BEAST_DEFINE_TESTSUITE(TxQ,app,ripple);
}
}