mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-04 11:15:56 +00:00
Clear queue for account with high fee tx (RIPD-1246):
* If an account has any transactions in the transaction queue, submitting a transaction that covers the differences to the open ledger fee level for prior queued transactions plus itself will cause all those transactions to be applied to the open ledger. * tel failures in `TxQ::accept` will leave tx in the queue to retry later.
This commit is contained in:
committed by
Nik Bougalis
parent
b0704b47e8
commit
c1b8efb7af
@@ -176,10 +176,10 @@ private:
|
||||
// before escalation kicks in.
|
||||
std::size_t txnsExpected_;
|
||||
// Minimum value of escalationMultiplier.
|
||||
std::uint32_t const minimumMultiplier_;
|
||||
std::uint64_t const minimumMultiplier_;
|
||||
// Based on the median fee of the LCL. Used
|
||||
// when fee escalation kicks in.
|
||||
std::uint32_t escalationMultiplier_;
|
||||
std::uint64_t escalationMultiplier_;
|
||||
beast::Journal j_;
|
||||
|
||||
std::mutex mutable lock_;
|
||||
@@ -223,7 +223,7 @@ private:
|
||||
return txnsExpected_;
|
||||
}
|
||||
|
||||
std::uint32_t
|
||||
std::uint64_t
|
||||
getEscalationMultiplier() const
|
||||
{
|
||||
std::lock_guard <std::mutex> sl(lock_);
|
||||
@@ -233,6 +233,19 @@ private:
|
||||
|
||||
std::uint64_t
|
||||
scaleFeeLevel(OpenView const& view, std::uint32_t txCountPadding = 0) const;
|
||||
|
||||
/**
|
||||
Returns the total fee level for all transactions in a series.
|
||||
Assumes that there are already more than txnsExpected_ txns in
|
||||
the view. If there aren't, the math still works, but the level
|
||||
will be high.
|
||||
|
||||
Returns: A `std::pair` as returned from `mulDiv` indicating whether
|
||||
the calculation result is safe.
|
||||
*/
|
||||
std::pair<bool, std::uint64_t>
|
||||
escalatedSeriesFeeLevel(OpenView const& view, std::size_t extraCount,
|
||||
std::size_t seriesSize) const;
|
||||
};
|
||||
|
||||
class MaybeTx
|
||||
@@ -302,7 +315,19 @@ private:
|
||||
AccountID const account;
|
||||
// Sequence number will be used as the key.
|
||||
TxMap transactions;
|
||||
/* If this account has had any transaction retry more than
|
||||
`retriesAllowed` times so that it was dropped from the
|
||||
queue, then all other transactions for this account will
|
||||
be given at most 2 attempts before being removed. Helps
|
||||
prevent wasting resources on retries that are more likely
|
||||
to fail.
|
||||
*/
|
||||
bool retryPenalty = false;
|
||||
/* If this account has had any transaction fail or expire,
|
||||
then when the queue is nearly full, transactions from
|
||||
this account will be discarded. Helps prevent the queue
|
||||
from getting filled and wedged.
|
||||
*/
|
||||
bool dropPenalty = false;
|
||||
|
||||
public:
|
||||
@@ -365,6 +390,22 @@ private:
|
||||
// is higher), or next entry in byFee_ (lower fee level).
|
||||
// Used to get the next "applyable" MaybeTx for accept().
|
||||
FeeMultiSet::iterator_type eraseAndAdvance(FeeMultiSet::const_iterator_type);
|
||||
// Erase a range of items, based on TxQAccount::TxMap iterators
|
||||
TxQAccount::TxMap::iterator
|
||||
erase(TxQAccount& txQAccount, TxQAccount::TxMap::const_iterator begin,
|
||||
TxQAccount::TxMap::const_iterator end);
|
||||
|
||||
/*
|
||||
All-or-nothing attempt to try to apply all the queued txs for `accountIter`
|
||||
up to and including `tx`.
|
||||
*/
|
||||
std::pair<TER, bool>
|
||||
tryClearAccountQueue(Application& app, OpenView& view,
|
||||
STTx const& tx, AccountMap::iterator const& accountIter,
|
||||
TxQAccount::TxMap::iterator, std::uint64_t feeLevelPaid,
|
||||
PreflightResult const& pfresult,
|
||||
std::size_t const txExtraCount, ApplyFlags flags,
|
||||
beast::Journal j);
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -81,17 +81,17 @@ TxQ::FeeMetrics::update(Application& app,
|
||||
ReadView const& view, bool timeLeap,
|
||||
TxQ::Setup const& setup)
|
||||
{
|
||||
std::vector<uint64_t> feeLevels;
|
||||
std::size_t txnsExpected;
|
||||
std::size_t mimimumTx;
|
||||
std::uint32_t escalationMultiplier;
|
||||
std::uint64_t escalationMultiplier;
|
||||
{
|
||||
std::lock_guard <std::mutex> sl(lock_);
|
||||
feeLevels.reserve(txnsExpected_);
|
||||
txnsExpected = txnsExpected_;
|
||||
mimimumTx = minimumTxnCount_;
|
||||
escalationMultiplier = escalationMultiplier_;
|
||||
}
|
||||
std::vector<uint64_t> feeLevels;
|
||||
feeLevels.reserve(txnsExpected);
|
||||
for (auto const& tx : view.txs)
|
||||
{
|
||||
auto const baseFee = calculateBaseFee(app, view,
|
||||
@@ -161,15 +161,14 @@ TxQ::FeeMetrics::scaleFeeLevel(OpenView const& view,
|
||||
// Transactions in the open ledger so far
|
||||
auto const current = view.txCount() + txCountPadding;
|
||||
|
||||
std::size_t target;
|
||||
std::uint32_t multiplier;
|
||||
auto const params = [&]
|
||||
{
|
||||
std::lock_guard <std::mutex> sl(lock_);
|
||||
|
||||
// Target number of transactions allowed
|
||||
target = txnsExpected_;
|
||||
multiplier = escalationMultiplier_;
|
||||
}
|
||||
return std::make_pair(txnsExpected_,
|
||||
escalationMultiplier_);
|
||||
}();
|
||||
auto const target = params.first;
|
||||
auto const multiplier = params.second;
|
||||
|
||||
// Once the open ledger bypasses the target,
|
||||
// escalate the fee quickly.
|
||||
@@ -184,6 +183,71 @@ TxQ::FeeMetrics::scaleFeeLevel(OpenView const& view,
|
||||
return baseLevel;
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
static
|
||||
std::pair<bool, std::uint64_t>
|
||||
sumOfFirstSquares(std::size_t x)
|
||||
{
|
||||
// sum(n = 1->x) : n * n = x(x + 1)(2x + 1) / 6
|
||||
|
||||
// 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<std::uint64_t>::max());
|
||||
return std::make_pair(true, (x * (x + 1) * (2 * x + 1)) / 6);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::pair<bool, std::uint64_t>
|
||||
TxQ::FeeMetrics::escalatedSeriesFeeLevel(OpenView const& view,
|
||||
std::size_t extraCount, std::size_t seriesSize) const
|
||||
{
|
||||
/* Transactions in the open ledger so far.
|
||||
AKA Transactions that will be in the open ledger when
|
||||
the first tx in the series is attempted.
|
||||
*/
|
||||
auto const current = view.txCount() + extraCount;
|
||||
/* Transactions that will be in the open ledger when
|
||||
the last tx in the series is attempted.
|
||||
*/
|
||||
auto const last = current + seriesSize - 1;
|
||||
|
||||
auto const params = [&] {
|
||||
std::lock_guard <std::mutex> sl(lock_);
|
||||
return std::make_pair(txnsExpected_,
|
||||
escalationMultiplier_);
|
||||
}();
|
||||
auto const target = params.first;
|
||||
auto const multiplier = params.second;
|
||||
|
||||
assert(current > target);
|
||||
|
||||
/* Calculate (apologies for the terrible notation)
|
||||
sum(n = current -> last) : multiplier * n * n / (target * target)
|
||||
multiplier / (target * target) * (sum(n = current -> last) : n * n)
|
||||
multiplier / (target * target) * ((sum(n = 1 -> last) : n * n) -
|
||||
(sum(n = 1 -> current - 1) : n * n))
|
||||
*/
|
||||
auto const sumNlast = detail::sumOfFirstSquares(last);
|
||||
auto const sumNcurrent = detail::sumOfFirstSquares(current - 1);
|
||||
// because `last` is bigger, if either sum overflowed, then
|
||||
// `sumNlast` definitely overflowed. Also the odds of this
|
||||
// are nearly nil.
|
||||
if (!sumNlast.first)
|
||||
return sumNlast;
|
||||
auto const totalFeeLevel = mulDiv(multiplier,
|
||||
sumNlast.second - sumNcurrent.second, target * target);
|
||||
|
||||
return totalFeeLevel;
|
||||
}
|
||||
|
||||
|
||||
TxQ::MaybeTx::MaybeTx(
|
||||
std::shared_ptr<STTx const> const& txn_,
|
||||
TxID const& txID_, std::uint64_t feeLevel_,
|
||||
@@ -321,12 +385,12 @@ TxQ::erase(TxQ::FeeMultiSet::const_iterator_type candidateIter)
|
||||
-> FeeMultiSet::iterator_type
|
||||
{
|
||||
auto& txQAccount = byAccount_.at(candidateIter->account);
|
||||
auto sequence = candidateIter->sequence;
|
||||
auto newCandidateIter = byFee_.erase(candidateIter);
|
||||
auto const sequence = candidateIter->sequence;
|
||||
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 found = txQAccount.remove(sequence);
|
||||
auto const found = txQAccount.remove(sequence);
|
||||
(void)found;
|
||||
assert(found);
|
||||
|
||||
@@ -338,16 +402,16 @@ TxQ::eraseAndAdvance(TxQ::FeeMultiSet::const_iterator_type candidateIter)
|
||||
-> FeeMultiSet::iterator_type
|
||||
{
|
||||
auto& txQAccount = byAccount_.at(candidateIter->account);
|
||||
auto accountIter = txQAccount.transactions.find(
|
||||
auto const accountIter = txQAccount.transactions.find(
|
||||
candidateIter->sequence);
|
||||
assert(accountIter != txQAccount.transactions.end());
|
||||
assert(accountIter == txQAccount.transactions.begin());
|
||||
assert(byFee_.iterator_to(accountIter->second) == candidateIter);
|
||||
auto accountNextIter = std::next(accountIter);
|
||||
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 acccount tx has a lower fee level,
|
||||
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
|
||||
@@ -357,12 +421,12 @@ TxQ::eraseAndAdvance(TxQ::FeeMultiSet::const_iterator_type candidateIter)
|
||||
the latter case, continue through the fee queue anyway
|
||||
to head off potential ordering manipulation problems.
|
||||
*/
|
||||
auto feeNextIter = std::next(candidateIter);
|
||||
bool useAccountNext = accountNextIter != txQAccount.transactions.end() &&
|
||||
auto const feeNextIter = std::next(candidateIter);
|
||||
bool const useAccountNext = accountNextIter != txQAccount.transactions.end() &&
|
||||
accountNextIter->first == candidateIter->sequence + 1 &&
|
||||
(feeNextIter == byFee_.end() ||
|
||||
accountNextIter->second.feeLevel > feeNextIter->feeLevel);
|
||||
auto candidateNextIter = byFee_.erase(candidateIter);
|
||||
auto const candidateNextIter = byFee_.erase(candidateIter);
|
||||
txQAccount.transactions.erase(accountIter);
|
||||
return useAccountNext ?
|
||||
byFee_.iterator_to(accountNextIter->second) :
|
||||
@@ -370,6 +434,89 @@ TxQ::eraseAndAdvance(TxQ::FeeMultiSet::const_iterator_type candidateIter)
|
||||
|
||||
}
|
||||
|
||||
auto
|
||||
TxQ::erase(TxQ::TxQAccount& txQAccount,
|
||||
TxQ::TxQAccount::TxMap::const_iterator begin,
|
||||
TxQ::TxQAccount::TxMap::const_iterator end)
|
||||
-> TxQAccount::TxMap::iterator
|
||||
{
|
||||
for (auto it = begin; it != end; ++it)
|
||||
{
|
||||
byFee_.erase(byFee_.iterator_to(it->second));
|
||||
}
|
||||
return txQAccount.transactions.erase(begin, end);
|
||||
}
|
||||
|
||||
std::pair<TER, bool>
|
||||
TxQ::tryClearAccountQueue(Application& app, OpenView& view,
|
||||
STTx const& tx, TxQ::AccountMap::iterator const& accountIter,
|
||||
TxQAccount::TxMap::iterator beginTxIter, std::uint64_t feeLevelPaid,
|
||||
PreflightResult const& pfresult, std::size_t const txExtraCount,
|
||||
ApplyFlags flags, beast::Journal j)
|
||||
{
|
||||
auto const tSeq = tx.getSequence();
|
||||
assert(beginTxIter != accountIter->second.transactions.end());
|
||||
auto const aSeq = beginTxIter->first;
|
||||
|
||||
auto const requiredTotalFeeLevel = feeMetrics_.escalatedSeriesFeeLevel(
|
||||
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.
|
||||
*/
|
||||
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);
|
||||
|
||||
auto const totalFeeLevelPaid = std::accumulate(beginTxIter, endTxIter,
|
||||
feeLevelPaid,
|
||||
[](auto const& total, auto const& tx)
|
||||
{
|
||||
return total + tx.second.feeLevel;
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
// This transaction paid enough to clear out the queue.
|
||||
// Attempt to apply the queued transactions.
|
||||
for (auto it = beginTxIter; it != endTxIter; ++it)
|
||||
{
|
||||
auto txResult = it->second.apply(app, view);
|
||||
// Succeed or fail, use up a retry, because if the overall
|
||||
// process fails, we want the attempt to count. If it all
|
||||
// succeeds, the MaybeTx will be destructed, so it'll be
|
||||
// moot.
|
||||
--it->second.retriesRemaining;
|
||||
if (!txResult.second)
|
||||
{
|
||||
// Transaction failed to apply. Fall back to the normal process.
|
||||
return std::make_pair(txResult.first, false);
|
||||
}
|
||||
}
|
||||
// Apply the current tx. Because the state of the view has been changed
|
||||
// by the queued txs, we also need to preclaim again.
|
||||
auto const pcresult = preclaim(pfresult, app, view);
|
||||
auto txResult = doApply(pcresult, app, view);
|
||||
|
||||
if (txResult.second)
|
||||
{
|
||||
// All of the queued transactions applied, so remove them from the queue.
|
||||
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)
|
||||
erase(accountIter->second, endTxIter, std::next(endTxIter));
|
||||
}
|
||||
|
||||
return txResult;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
How the decision to apply, queue, or reject is made:
|
||||
0. Is `featureFeeEscalation` enabled?
|
||||
@@ -451,7 +598,7 @@ TxQ::apply(Application& app, OpenView& view,
|
||||
|
||||
auto const account = (*tx)[sfAccount];
|
||||
auto const transactionID = tx->getTransactionID();
|
||||
auto const t_seq = tx->getSequence();
|
||||
auto const tSeq = tx->getSequence();
|
||||
|
||||
// See if the transaction is valid, properly formed,
|
||||
// etc. before doing potentially expensive queue
|
||||
@@ -466,6 +613,8 @@ TxQ::apply(Application& app, OpenView& view,
|
||||
boost::optional<ApplyViewImpl> applyView;
|
||||
boost::optional<OpenView> openView;
|
||||
|
||||
TxQAccount::TxMap::iterator nextTxIter;
|
||||
|
||||
XRPAmount fee = beast::zero;
|
||||
XRPAmount potentialSpend = beast::zero;
|
||||
bool includeCurrentFee = false;
|
||||
@@ -494,7 +643,7 @@ TxQ::apply(Application& app, OpenView& view,
|
||||
if (accountExists)
|
||||
{
|
||||
auto& txQAcct = accountIter->second;
|
||||
auto existingIter = txQAcct.transactions.find(t_seq);
|
||||
auto existingIter = txQAcct.transactions.find(tSeq);
|
||||
if (existingIter != txQAcct.transactions.end())
|
||||
{
|
||||
// Is the current transaction's fee higher than
|
||||
@@ -503,7 +652,7 @@ TxQ::apply(Application& app, OpenView& view,
|
||||
existingIter->second.feeLevel,
|
||||
setup_.retrySequencePercent);
|
||||
JLOG(j_.trace()) << "Found transaction in queue for account " <<
|
||||
account << " with sequence number " << t_seq <<
|
||||
account << " with sequence number " << tSeq <<
|
||||
" new txn fee level is " << feeLevelPaid <<
|
||||
", old txn fee level is " <<
|
||||
existingIter->second.feeLevel <<
|
||||
@@ -568,7 +717,7 @@ TxQ::apply(Application& app, OpenView& view,
|
||||
auto deleteIter = byFee_.iterator_to(existingIter->second);
|
||||
assert(deleteIter != byFee_.end());
|
||||
assert(&existingIter->second == &*deleteIter);
|
||||
assert(deleteIter->sequence == t_seq);
|
||||
assert(deleteIter->sequence == tSeq);
|
||||
assert(deleteIter->account == txQAcct.account);
|
||||
replacedItemDeleteIter = deleteIter;
|
||||
}
|
||||
@@ -595,9 +744,9 @@ TxQ::apply(Application& app, OpenView& view,
|
||||
if (sle)
|
||||
{
|
||||
auto& txQAcct = accountIter->second;
|
||||
auto const a_seq = (*sle)[sfSequence];
|
||||
auto const aSeq = (*sle)[sfSequence];
|
||||
|
||||
if (a_seq < t_seq)
|
||||
if (aSeq < tSeq)
|
||||
{
|
||||
// If the transaction is queueable, create the multiTxn
|
||||
// object to hold the info we need to adjust for
|
||||
@@ -610,23 +759,24 @@ TxQ::apply(Application& app, OpenView& view,
|
||||
if (multiTxn)
|
||||
{
|
||||
/* See if the queue has entries for all the
|
||||
seq's in [a_seq, t_seq). Total up 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.
|
||||
*/
|
||||
auto workingIter = txQAcct.transactions.find(a_seq);
|
||||
auto workingSeq = a_seq;
|
||||
multiTxn->nextTxIter = txQAcct.transactions.find(aSeq);
|
||||
auto workingIter = multiTxn->nextTxIter;
|
||||
auto workingSeq = aSeq;
|
||||
for (; workingIter != txQAcct.transactions.end();
|
||||
++workingIter, ++workingSeq)
|
||||
{
|
||||
if (workingSeq < t_seq &&
|
||||
if (workingSeq < tSeq &&
|
||||
workingIter->first != workingSeq)
|
||||
{
|
||||
// If any transactions are missing before `tx`, abort.
|
||||
multiTxn.reset();
|
||||
break;
|
||||
}
|
||||
if (workingIter->first == t_seq - 1)
|
||||
if (workingIter->first == tSeq - 1)
|
||||
{
|
||||
// Is the current transaction's fee higher than
|
||||
// the previous transaction's fee + a percentage
|
||||
@@ -648,7 +798,7 @@ TxQ::apply(Application& app, OpenView& view,
|
||||
return{ telINSUF_FEE_P, false };
|
||||
}
|
||||
}
|
||||
if (workingIter->first == t_seq)
|
||||
if (workingIter->first == tSeq)
|
||||
{
|
||||
// If we're replacing this transaction, don't
|
||||
// count it.
|
||||
@@ -664,7 +814,7 @@ TxQ::apply(Application& app, OpenView& view,
|
||||
*workingIter->second.pfresult));
|
||||
// Don't worry about the blocker status of txs
|
||||
// after the current.
|
||||
if (workingIter->first < t_seq &&
|
||||
if (workingIter->first < tSeq &&
|
||||
workingIter->second.consequences->category ==
|
||||
TxConsequences::blocker)
|
||||
{
|
||||
@@ -682,7 +832,7 @@ TxQ::apply(Application& app, OpenView& view,
|
||||
multiTxn->potentialSpend +=
|
||||
workingIter->second.consequences->potentialSpend;
|
||||
}
|
||||
if (workingSeq < t_seq)
|
||||
if (workingSeq < tSeq)
|
||||
// Transactions are missing before `tx`.
|
||||
multiTxn.reset();
|
||||
}
|
||||
@@ -748,7 +898,7 @@ TxQ::apply(Application& app, OpenView& view,
|
||||
sleBump->setFieldAmount(sfBalance,
|
||||
balance - (multiTxn->fee +
|
||||
multiTxn->potentialSpend));
|
||||
sleBump->setFieldU32(sfSequence, t_seq);
|
||||
sleBump->setFieldU32(sfSequence, tSeq);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -771,6 +921,42 @@ TxQ::apply(Application& app, OpenView& view,
|
||||
" 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) Must be an account already in the queue.
|
||||
2) 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).
|
||||
3) The next transaction must not have previously tried and failed
|
||||
to apply to an open ledger.
|
||||
4) Tx must be paying more than just the required fee level to
|
||||
get itself into the queue.
|
||||
5) 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).
|
||||
6) Tx is not a 0-fee / free transaction, regardless of fee level.
|
||||
*/
|
||||
if (accountExists && multiTxn.is_initialized() &&
|
||||
multiTxn->nextTxIter->second.retriesRemaining == MaybeTx::retriesAllowed &&
|
||||
feeLevelPaid > requiredFeeLevel &&
|
||||
requiredFeeLevel > baseLevel && baseFee != 0)
|
||||
{
|
||||
OpenView sandbox(open_ledger, &view, view.rules());
|
||||
|
||||
auto result = tryClearAccountQueue(app, sandbox, *tx, accountIter,
|
||||
multiTxn->nextTxIter, feeLevelPaid, pfresult, view.txCount(),
|
||||
flags, j);
|
||||
if (result.second)
|
||||
{
|
||||
sandbox.apply(view);
|
||||
/* Can't erase(*replacedItemDeleteIter) here because success
|
||||
implies that it has already been deleted.
|
||||
*/
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Can transaction go in open ledger?
|
||||
if (!multiTxn && feeLevelPaid >= requiredFeeLevel)
|
||||
{
|
||||
@@ -1064,7 +1250,7 @@ TxQ::accept(Application& app,
|
||||
ledgerChanged = true;
|
||||
}
|
||||
else if (isTefFailure(txnResult) || isTemMalformed(txnResult) ||
|
||||
isTelLocal(txnResult) || candidateIter->retriesRemaining <= 0)
|
||||
candidateIter->retriesRemaining <= 0)
|
||||
{
|
||||
if (candidateIter->retriesRemaining <= 0)
|
||||
account.retryPenalty = true;
|
||||
|
||||
@@ -28,9 +28,10 @@
|
||||
#include <ripple/protocol/ErrorCodes.h>
|
||||
#include <ripple/protocol/Feature.h>
|
||||
#include <ripple/protocol/JsonFields.h>
|
||||
#include <ripple/protocol/STTx.h>
|
||||
#include <ripple/protocol/st.h>
|
||||
#include <ripple/test/jtx.h>
|
||||
#include <ripple/test/jtx/ticket.h>
|
||||
#include <boost/optional.hpp>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
@@ -112,6 +113,48 @@ class TxQ_test : public beast::unit_test::suite
|
||||
return p;
|
||||
}
|
||||
|
||||
void
|
||||
initFee(jtx::Env& env, std::size_t expectedInLedger, std::uint32_t base,
|
||||
std::uint32_t units, std::uint32_t reserve, std::uint32_t increment)
|
||||
{
|
||||
// Change the reserve fee so we can create an account
|
||||
// with a lower balance.
|
||||
STTx feeTx(ttFEE,
|
||||
[&](auto& obj)
|
||||
{
|
||||
obj[sfAccount] = AccountID();
|
||||
obj[sfLedgerSequence] = env.current()->info().seq;
|
||||
obj[sfBaseFee] = base;
|
||||
obj[sfReferenceFeeUnits] = units;
|
||||
obj[sfReserveBase] = reserve;
|
||||
obj[sfReserveIncrement] = increment;
|
||||
});
|
||||
|
||||
auto const changed = env.app().openLedger().modify(
|
||||
[&](OpenView& view, beast::Journal j)
|
||||
{
|
||||
auto const result = ripple::apply(
|
||||
env.app(), view, feeTx,
|
||||
tapNONE, j);
|
||||
BEAST_EXPECT(result.second && result.first ==
|
||||
tesSUCCESS);
|
||||
return result.second;
|
||||
});
|
||||
BEAST_EXPECT(changed);
|
||||
// Pad a couple of txs to keep the median at the default
|
||||
env(noop(env.master));
|
||||
env(noop(env.master));
|
||||
// Close the ledger with a delay to force the TxQ stats
|
||||
// to stay at the default.
|
||||
env.close(env.now() + 5s, 10000ms);
|
||||
checkMetrics(env, 0, boost::none, 0, expectedInLedger, 256);
|
||||
auto const fees = env.current()->fees();
|
||||
BEAST_EXPECT(fees.base == base);
|
||||
BEAST_EXPECT(fees.units == units);
|
||||
BEAST_EXPECT(fees.reserve == reserve);
|
||||
BEAST_EXPECT(fees.increment == increment);
|
||||
}
|
||||
|
||||
public:
|
||||
void testQueue()
|
||||
{
|
||||
@@ -676,8 +719,11 @@ public:
|
||||
|
||||
checkMetrics(env, 0, boost::none, 0, 3, 256);
|
||||
|
||||
initFee(env, 3, 10, 10, 200, 50);
|
||||
|
||||
// Create several accounts while the fee is cheap so they all apply.
|
||||
env.fund(XRP(50000), noripple(alice, bob, charlie, daria));
|
||||
env.fund(drops(2000), noripple(alice));
|
||||
env.fund(XRP(500000), noripple(bob, charlie, daria));
|
||||
checkMetrics(env, 0, boost::none, 4, 3, 256);
|
||||
|
||||
// Alice - price starts exploding: held
|
||||
@@ -734,7 +780,7 @@ public:
|
||||
BEAST_EXPECT(env.seq(charlie) == charlieSeq + 1);
|
||||
|
||||
// Alice - fill up the queue
|
||||
std::int64_t aliceFee = 10;
|
||||
std::int64_t aliceFee = 20;
|
||||
aliceSeq = env.seq(alice);
|
||||
auto lastLedgerSeq = env.current()->info().seq + 2;
|
||||
for (auto i = 0; i < 7; i++)
|
||||
@@ -742,15 +788,14 @@ public:
|
||||
env(noop(alice), seq(aliceSeq),
|
||||
json(jss::LastLedgerSequence, lastLedgerSeq + i),
|
||||
fee(aliceFee), queued);
|
||||
aliceFee = (aliceFee + 1) * 125 / 100;
|
||||
++aliceSeq;
|
||||
}
|
||||
checkMetrics(env, 8, 8, 5, 4, 257);
|
||||
checkMetrics(env, 8, 8, 5, 4, 513);
|
||||
{
|
||||
auto& txQ = env.app().getTxQ();
|
||||
auto aliceStat = txQ.getAccountTxs(alice.id(),
|
||||
env.app().config(), *env.current());
|
||||
std::int64_t fee = 10;
|
||||
std::int64_t fee = 20;
|
||||
auto seq = env.seq(alice);
|
||||
if (BEAST_EXPECT(aliceStat))
|
||||
{
|
||||
@@ -766,7 +811,6 @@ public:
|
||||
tx.second.consequences->category == TxConsequences::normal) ||
|
||||
tx.first == env.seq(alice) + 6);
|
||||
++seq;
|
||||
fee = (fee + 1) * 125 / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -777,60 +821,46 @@ public:
|
||||
env(noop(alice), seq(aliceSeq),
|
||||
json(jss::LastLedgerSequence, lastLedgerSeq + 7),
|
||||
fee(aliceFee), ter(telCAN_NOT_QUEUE));
|
||||
checkMetrics(env, 8, 8, 5, 4, 257);
|
||||
checkMetrics(env, 8, 8, 5, 4, 513);
|
||||
|
||||
// Charlie - try to add another item to the queue,
|
||||
// which fails because fee is lower than Alice's
|
||||
// queued average.
|
||||
env(noop(charlie), fee(24), ter(telINSUF_FEE_P));
|
||||
checkMetrics(env, 8, 8, 5, 4, 257);
|
||||
env(noop(charlie), fee(19), ter(telINSUF_FEE_P));
|
||||
checkMetrics(env, 8, 8, 5, 4, 513);
|
||||
|
||||
// Charlie - add another item to the queue, which
|
||||
// causes Alice's last txn to drop
|
||||
env(noop(charlie), fee(30), queued);
|
||||
checkMetrics(env, 8, 8, 5, 4, 257);
|
||||
checkMetrics(env, 8, 8, 5, 4, 513);
|
||||
|
||||
// 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));
|
||||
checkMetrics(env, 8, 8, 5, 4, 257);
|
||||
checkMetrics(env, 8, 8, 5, 4, 513);
|
||||
|
||||
// Alice wants this tx more than the dropped tx,
|
||||
// so resubmits with higher fee, but the queue
|
||||
// is full, and her account is the cheapest.
|
||||
env(noop(alice), seq(aliceSeq - 1),
|
||||
fee(aliceFee), ter(telCAN_NOT_QUEUE));
|
||||
checkMetrics(env, 8, 8, 5, 4, 257);
|
||||
checkMetrics(env, 8, 8, 5, 4, 513);
|
||||
|
||||
// Try to replace a middle item in the queue
|
||||
// without enough fee.
|
||||
aliceSeq = env.seq(alice) + 2;
|
||||
aliceFee = 21;
|
||||
aliceFee = 25;
|
||||
env(noop(alice), seq(aliceSeq),
|
||||
fee(aliceFee), ter(telINSUF_FEE_P));
|
||||
checkMetrics(env, 8, 8, 5, 4, 257);
|
||||
checkMetrics(env, 8, 8, 5, 4, 513);
|
||||
|
||||
// Replace a middle item from the queue successfully
|
||||
++aliceFee;
|
||||
env(noop(alice), seq(aliceSeq),
|
||||
fee(aliceFee), queued);
|
||||
checkMetrics(env, 8, 8, 5, 4, 257);
|
||||
|
||||
// Try to replace the next item in the queue
|
||||
// without enough fee.
|
||||
++aliceSeq;
|
||||
aliceFee = aliceFee * 125 / 100;
|
||||
env(noop(alice), seq(aliceSeq),
|
||||
fee(aliceFee), ter(telINSUF_FEE_P));
|
||||
checkMetrics(env, 8, 8, 5, 4, 257);
|
||||
|
||||
// Replace a middle item from the queue successfully
|
||||
++aliceFee;
|
||||
env(noop(alice), seq(aliceSeq),
|
||||
fee(aliceFee), queued);
|
||||
checkMetrics(env, 8, 8, 5, 4, 257);
|
||||
checkMetrics(env, 8, 8, 5, 4, 513);
|
||||
|
||||
env.close();
|
||||
// Alice's transactions processed, along with
|
||||
@@ -845,7 +875,7 @@ public:
|
||||
// more than the minimum reserve in flight before the
|
||||
// last queued transaction
|
||||
aliceFee = env.le(alice)->getFieldAmount(sfBalance).xrp().drops()
|
||||
- (121);
|
||||
- (59);
|
||||
env(noop(alice), seq(aliceSeq),
|
||||
fee(aliceFee), ter(telCAN_NOT_QUEUE));
|
||||
checkMetrics(env, 4, 10, 6, 5, 256);
|
||||
@@ -1144,15 +1174,18 @@ public:
|
||||
|
||||
auto queued = ter(terQUEUED);
|
||||
|
||||
initFee(env, 3, 10, 10, 200, 50);
|
||||
|
||||
BEAST_EXPECT(env.current()->fees().base == 10);
|
||||
|
||||
checkMetrics(env, 0, boost::none, 0, 3, 256);
|
||||
|
||||
env.fund(XRP(50000), noripple(alice, bob));
|
||||
env.fund(drops(5000), noripple(alice));
|
||||
env.fund(XRP(50000), noripple(bob));
|
||||
checkMetrics(env, 0, boost::none, 2, 3, 256);
|
||||
auto USD = bob["USD"];
|
||||
|
||||
env(offer(alice, USD(5000), XRP(50000)), require(owners(alice, 1)));
|
||||
env(offer(alice, USD(5000), drops(5000)), require(owners(alice, 1)));
|
||||
checkMetrics(env, 0, boost::none, 3, 3, 256);
|
||||
|
||||
env.close();
|
||||
@@ -1163,25 +1196,25 @@ public:
|
||||
checkMetrics(env, 0, 6, 4, 3, 256);
|
||||
|
||||
// Queue up a couple of transactions, plus one
|
||||
// really expensive one.
|
||||
// more expensive one.
|
||||
auto aliceSeq = env.seq(alice);
|
||||
env(noop(alice), seq(aliceSeq++), queued);
|
||||
env(noop(alice), seq(aliceSeq++), queued);
|
||||
env(noop(alice), seq(aliceSeq++), queued);
|
||||
env(noop(alice), fee(XRP(1000)),
|
||||
env(noop(alice), fee(drops(1000)),
|
||||
seq(aliceSeq), queued);
|
||||
checkMetrics(env, 4, 6, 4, 3, 256);
|
||||
|
||||
// This offer should take Alice's offer
|
||||
// up to Alice's reserve.
|
||||
env(offer(bob, XRP(50000), USD(5000)),
|
||||
openLedgerFee(env), require(balance("alice", XRP(250)),
|
||||
env(offer(bob, drops(5000), USD(5000)),
|
||||
openLedgerFee(env), require(balance(alice, drops(250)),
|
||||
owners(alice, 1), lines(alice, 1)));
|
||||
checkMetrics(env, 4, 6, 5, 3, 256);
|
||||
|
||||
// Try adding a new transaction.
|
||||
// Too many fees in flight.
|
||||
env(noop(alice), fee(XRP(2000)), seq(aliceSeq+1),
|
||||
env(noop(alice), fee(drops(200)), seq(aliceSeq+1),
|
||||
ter(telCAN_NOT_QUEUE));
|
||||
checkMetrics(env, 4, 6, 5, 3, 256);
|
||||
|
||||
@@ -1189,11 +1222,11 @@ public:
|
||||
// take a fee, except the last one.
|
||||
env.close();
|
||||
checkMetrics(env, 1, 10, 3, 5, 256);
|
||||
env.require(balance(alice, XRP(250) - drops(30)));
|
||||
env.require(balance(alice, drops(250 - 30)));
|
||||
|
||||
// Still can't add a new transaction for Alice,
|
||||
// no matter the fee.
|
||||
env(noop(alice), fee(XRP(2000)), seq(aliceSeq + 1),
|
||||
env(noop(alice), fee(drops(200)), seq(aliceSeq + 1),
|
||||
ter(telCAN_NOT_QUEUE));
|
||||
checkMetrics(env, 1, 10, 3, 5, 256);
|
||||
|
||||
@@ -1671,8 +1704,8 @@ public:
|
||||
void testExpirationReplacement()
|
||||
{
|
||||
/* This test is based on a reported regression where a
|
||||
replacement candidate transaction found it's replacee
|
||||
did not have `consequences` set
|
||||
replacement candidate transaction found the tx it was trying
|
||||
to replace did not have `consequences` set
|
||||
|
||||
Hypothesis: The queue had '22 through '25. At some point(s),
|
||||
both the original '22 and '23 expired and were removed from
|
||||
@@ -2202,7 +2235,6 @@ public:
|
||||
}
|
||||
{
|
||||
auto const server_state = env.rpc("server_state");
|
||||
log << server_state;
|
||||
auto const& state = server_state[jss::result][jss::state];
|
||||
BEAST_EXPECT(state.isMember(jss::load_factor) &&
|
||||
state[jss::load_factor] == 256);
|
||||
@@ -2248,7 +2280,6 @@ public:
|
||||
}
|
||||
{
|
||||
auto const server_state = env.rpc("server_state");
|
||||
log << server_state;
|
||||
auto const& state = server_state[jss::result][jss::state];
|
||||
BEAST_EXPECT(state.isMember(jss::load_factor) &&
|
||||
state[jss::load_factor] == 227555);
|
||||
@@ -2284,7 +2315,6 @@ public:
|
||||
}
|
||||
{
|
||||
auto const server_state = env.rpc("server_state");
|
||||
log << server_state;
|
||||
auto const& state = server_state[jss::result][jss::state];
|
||||
BEAST_EXPECT(state.isMember(jss::load_factor) &&
|
||||
state[jss::load_factor] == 256000);
|
||||
@@ -2332,7 +2362,6 @@ public:
|
||||
}
|
||||
{
|
||||
auto const server_state = env.rpc("server_state");
|
||||
log << server_state;
|
||||
auto const& state = server_state[jss::result][jss::state];
|
||||
BEAST_EXPECT(state.isMember(jss::load_factor) &&
|
||||
state[jss::load_factor] == 227555);
|
||||
@@ -2376,7 +2405,6 @@ public:
|
||||
}
|
||||
{
|
||||
auto const server_state = env.rpc("server_state");
|
||||
log << server_state;
|
||||
auto const& state = server_state[jss::result][jss::state];
|
||||
BEAST_EXPECT(state.isMember(jss::load_factor) &&
|
||||
state[jss::load_factor] >= 320 &&
|
||||
@@ -2398,6 +2426,213 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void testClearQueuedAccountTxs()
|
||||
{
|
||||
using namespace jtx;
|
||||
|
||||
Env env(*this,
|
||||
makeConfig({ { "minimum_txn_in_ledger_standalone", "3" } }),
|
||||
features(featureFeeEscalation));
|
||||
auto alice = Account("alice");
|
||||
auto bob = Account("bob");
|
||||
|
||||
checkMetrics(env, 0, boost::none, 0, 3, 256);
|
||||
env.fund(XRP(50000000), alice, bob);
|
||||
|
||||
fillQueue(env, alice);
|
||||
|
||||
auto calcTotalFee = [&](
|
||||
std::int64_t alreadyPaid, boost::optional<std::size_t> numToClear = boost::none)
|
||||
-> std::uint64_t {
|
||||
auto totalFactor = 0;
|
||||
auto const metrics = env.app ().getTxQ ().getMetrics (
|
||||
env.app ().config (), *env.current ());
|
||||
if (!numToClear)
|
||||
numToClear.emplace(metrics->txCount + 1);
|
||||
for (int i = 0; i < *numToClear; ++i)
|
||||
{
|
||||
auto inLedger = metrics->txInLedger + i;
|
||||
totalFactor += inLedger * inLedger;
|
||||
}
|
||||
auto result =
|
||||
mulDiv (metrics->medFeeLevel * totalFactor /
|
||||
(metrics->txPerLedger * metrics->txPerLedger),
|
||||
env.current ()->fees ().base, metrics->referenceFeeLevel)
|
||||
.second;
|
||||
// Subtract the fees already paid
|
||||
result -= alreadyPaid;
|
||||
// round up
|
||||
++result;
|
||||
return result;
|
||||
};
|
||||
|
||||
testcase("straightfoward positive case");
|
||||
{
|
||||
// Queue up some transactions at a too-low fee.
|
||||
auto aliceSeq = env.seq(alice);
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
env(noop(alice), fee(100), seq(aliceSeq++), ter(terQUEUED));
|
||||
}
|
||||
|
||||
// Queue up a transaction paying the open ledger fee
|
||||
// This will be the first tx to call the operative function,
|
||||
// but it won't succeed.
|
||||
env(noop(alice), openLedgerFee(env), seq(aliceSeq++),
|
||||
ter(terQUEUED));
|
||||
|
||||
checkMetrics(env, 3, boost::none, 4, 3, 256);
|
||||
|
||||
// Figure out how much it would cost to cover all the
|
||||
// queued txs + itself
|
||||
std::uint64_t totalFee1 = calcTotalFee (100 * 2 + 8889);
|
||||
--totalFee1;
|
||||
|
||||
BEAST_EXPECT(totalFee1 == 60911);
|
||||
// Submit a transaction with that fee. It will get queued
|
||||
// because the fee level calculation rounds down. This is
|
||||
// the edge case test.
|
||||
env(noop(alice), fee(totalFee1), seq(aliceSeq++),
|
||||
ter(terQUEUED));
|
||||
|
||||
checkMetrics(env, 4, boost::none, 4, 3, 256);
|
||||
|
||||
// Now repeat the process including the new tx
|
||||
// and avoiding the rounding error
|
||||
std::uint64_t const totalFee2 = calcTotalFee (100 * 2 + 8889 + 60911);
|
||||
BEAST_EXPECT(totalFee2 == 35556);
|
||||
// Submit a transaction with that fee. It will succeed.
|
||||
env(noop(alice), fee(totalFee2), seq(aliceSeq++));
|
||||
|
||||
checkMetrics(env, 0, boost::none, 9, 3, 256);
|
||||
}
|
||||
|
||||
testcase("replace last tx with enough to clear queue");
|
||||
{
|
||||
// Queue up some transactions at a too-low fee.
|
||||
auto aliceSeq = env.seq(alice);
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
env(noop(alice), fee(100), seq(aliceSeq++), ter(terQUEUED));
|
||||
}
|
||||
|
||||
// Queue up a transaction paying the open ledger fee
|
||||
// This will be the first tx to call the operative function,
|
||||
// but it won't succeed.
|
||||
env(noop(alice), openLedgerFee(env), seq(aliceSeq++),
|
||||
ter(terQUEUED));
|
||||
|
||||
checkMetrics(env, 3, boost::none, 9, 3, 256);
|
||||
|
||||
// Figure out how much it would cost to cover all the
|
||||
// queued txs + itself
|
||||
auto const metrics = env.app ().getTxQ ().getMetrics (
|
||||
env.app ().config (), *env.current ());
|
||||
std::uint64_t const totalFee =
|
||||
calcTotalFee (100 * 2, metrics->txCount);
|
||||
BEAST_EXPECT(totalFee == 167578);
|
||||
// Replacing the last tx with the large fee succeeds.
|
||||
--aliceSeq;
|
||||
env(noop(alice), fee(totalFee), seq(aliceSeq++));
|
||||
|
||||
// The queue is clear
|
||||
checkMetrics(env, 0, boost::none, 12, 3, 256);
|
||||
|
||||
env.close();
|
||||
checkMetrics(env, 0, 24, 0, 12, 256);
|
||||
}
|
||||
|
||||
testcase("replace middle tx with enough to clear queue");
|
||||
{
|
||||
fillQueue(env, alice);
|
||||
// Queue up some transactions at a too-low fee.
|
||||
auto aliceSeq = env.seq(alice);
|
||||
for (int i = 0; i < 5; ++i)
|
||||
{
|
||||
env(noop(alice), fee(100), seq(aliceSeq++), ter(terQUEUED));
|
||||
}
|
||||
|
||||
checkMetrics(env, 5, 24, 13, 12, 256);
|
||||
|
||||
// Figure out how much it would cost to cover 3 txns
|
||||
std::uint64_t const totalFee = calcTotalFee(100 * 2, 3);
|
||||
BEAST_EXPECT(totalFee == 20287);
|
||||
// Replacing the last tx with the large fee succeeds.
|
||||
aliceSeq -= 3;
|
||||
env(noop(alice), fee(totalFee), seq(aliceSeq++));
|
||||
|
||||
checkMetrics(env, 2, 24, 16, 12, 256);
|
||||
auto const aliceQueue = env.app().getTxQ().getAccountTxs(
|
||||
alice.id(), env.app().config(), *env.current());
|
||||
if (BEAST_EXPECT(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;
|
||||
}
|
||||
}
|
||||
|
||||
// Close the ledger to clear the queue
|
||||
env.close();
|
||||
checkMetrics(env, 0, 32, 2, 16, 256);
|
||||
}
|
||||
|
||||
testcase("clear queue failure (load)");
|
||||
{
|
||||
fillQueue(env, alice);
|
||||
// Queue up some transactions at a too-low fee.
|
||||
auto aliceSeq = env.seq(alice);
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
env(noop(alice), fee(200), seq(aliceSeq++), ter(terQUEUED));
|
||||
}
|
||||
for (int i = 0; i < 2; ++i)
|
||||
{
|
||||
env(noop(alice), fee(22), seq(aliceSeq++), ter(terQUEUED));
|
||||
}
|
||||
|
||||
checkMetrics(env, 4, 32, 17, 16, 256);
|
||||
|
||||
// Figure out how much it would cost to cover all the txns
|
||||
// + 1
|
||||
std::uint64_t const totalFee = calcTotalFee (200 * 2 + 22 * 2);
|
||||
BEAST_EXPECT(totalFee == 35006);
|
||||
// This fee should be enough, but oh no! Server load went up!
|
||||
auto& feeTrack = env.app().getFeeTrack();
|
||||
auto const origFee = feeTrack.getRemoteFee();
|
||||
feeTrack.setRemoteFee(origFee * 5);
|
||||
// Instead the tx gets queued, and all of the queued
|
||||
// txs stay in the queue.
|
||||
env(noop(alice), fee(totalFee), seq(aliceSeq++), ter(terQUEUED));
|
||||
|
||||
// The original last transaction is still in the queue
|
||||
checkMetrics(env, 5, 32, 17, 16, 256);
|
||||
|
||||
// With high load, some of the txs stay in the queue
|
||||
env.close();
|
||||
checkMetrics(env, 3, 34, 2, 17, 256);
|
||||
|
||||
// Load drops back down
|
||||
feeTrack.setRemoteFee(origFee);
|
||||
|
||||
// Because of the earlier failure, alice can not clear the queue,
|
||||
// no matter how high the fee
|
||||
fillQueue(env, bob);
|
||||
checkMetrics(env, 3, 34, 18, 17, 256);
|
||||
|
||||
env(noop(alice), fee(XRP(1)), seq(aliceSeq++), ter(terQUEUED));
|
||||
checkMetrics(env, 4, 34, 18, 17, 256);
|
||||
|
||||
// With normal load, those txs get into the ledger
|
||||
env.close();
|
||||
checkMetrics(env, 0, 36, 4, 18, 256);
|
||||
}
|
||||
}
|
||||
|
||||
void run()
|
||||
{
|
||||
testQueue();
|
||||
@@ -2420,6 +2655,7 @@ public:
|
||||
testSignAndSubmitSequence();
|
||||
testAccountInfo();
|
||||
testServerInfo();
|
||||
testClearQueuedAccountTxs();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user