fix: reject batch transactions from the tx queue

This commit is contained in:
Denis Angell
2026-06-26 10:21:38 -04:00
parent c45c07f8b5
commit afa4d9a0ad
2 changed files with 37 additions and 72 deletions

View File

@@ -4531,7 +4531,10 @@ class Batch_test : public beast::unit_test::Suite
using namespace test::jtx;
using namespace std::literals;
// only outer batch transactions are counter towards the queue size
// A Batch is never queued. Under open-ledger congestion a Batch that
// pays only its base fee cannot apply directly and, unlike an ordinary
// transaction, is rejected outright rather than held in the queue. A
// Batch that pays the escalated open-ledger fee applies directly.
{
test::jtx::Env env{
*this,
@@ -4550,7 +4553,7 @@ class Batch_test : public beast::unit_test::Suite
env.fund(XRP(10000), noripple(carol));
env.close(env.now() + 5s, 10000ms);
// Fill the ledger
// Fill the open ledger so escalation is active.
env(noop(alice));
env(noop(alice));
env(noop(alice));
@@ -4563,28 +4566,23 @@ class Batch_test : public beast::unit_test::Suite
auto const bobSeq = env.seq(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
// Queue Batch
{
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob),
Ter(terQUEUED));
}
// Paying only the base fee, the Batch cannot enter the congested
// open ledger and is rejected rather than queued.
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob),
Ter(telCAN_NOT_QUEUE));
checkMetrics(*this, env, 2, std::nullopt, 3, 2);
// The Batch was not queued; only carol's transaction is queued.
checkMetrics(*this, env, 1, std::nullopt, 3, 2);
// Replace Queued Batch
{
env(batch::outer(alice, aliceSeq, openLedgerFee(env, batchFee), tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob),
Ter(tesSUCCESS));
env.close();
}
checkMetrics(*this, env, 0, 12, 1, 6);
// Paying the escalated open-ledger fee, the Batch applies directly.
env(batch::outer(alice, aliceSeq, openLedgerFee(env, batchFee), tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob),
Ter(tesSUCCESS));
}
// inner batch transactions are counter towards the ledger tx count
@@ -4630,54 +4628,9 @@ class Batch_test : public beast::unit_test::Suite
checkMetrics(*this, env, 1, std::nullopt, 3, 2);
}
// A Batch is not a TxQ blocker: it queues like an ordinary tx, so a
// follow-on transaction from the same account can be appended behind
// it. (The TxQ forecasts the account advancing by one; if the batch's
// own inners consume further sequences, a stale follow-on simply fails
// on apply - a soft, account-local effect.)
{
test::jtx::Env env{
*this,
makeSmallQueueConfig({{Keys::kMinimumTxnInLedgerStandalone, "2"}}),
features,
nullptr,
beast::Severity::Error};
auto alice = Account("alice");
auto bob = Account("bob");
auto carol = Account("carol");
env.fund(XRP(10000), noripple(alice, bob));
env.close(env.now() + 5s, 10000ms);
env.fund(XRP(10000), noripple(carol));
env.close(env.now() + 5s, 10000ms);
// Fill the open ledger so subsequent transactions queue.
env(noop(alice));
env(noop(alice));
env(noop(alice));
checkMetrics(*this, env, 0, std::nullopt, 3, 2);
auto const aliceSeq = env.seq(alice);
auto const bobSeq = env.seq(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
// Queue the Batch.
env(batch::outer(alice, aliceSeq, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 1),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob),
Ter(terQUEUED));
// A follow-on transaction from alice can now be queued behind it.
env(noop(alice), Seq(aliceSeq + 1), Ter(terQUEUED));
// Other accounts are unaffected.
env(noop(carol), Ter(terQUEUED));
}
// A Batch can be queued even when the account already holds other
// queued transactions (it is not a blocker).
// A Batch is never queued, so it also cannot sit behind the account's
// already-queued transactions: with a sequence gap it cannot apply
// directly and is rejected rather than queued.
{
test::jtx::Env env{
*this,
@@ -4703,15 +4656,20 @@ class Batch_test : public beast::unit_test::Suite
// Queue two normal transactions for alice.
env(noop(alice), Seq(aliceSeq + 0), Ter(terQUEUED));
env(noop(alice), Seq(aliceSeq + 1), Ter(terQUEUED));
checkMetrics(*this, env, 2, std::nullopt, 3, 2);
// The Batch can join the non-empty account queue.
// The Batch's sequence sits behind the queued transactions, so it
// cannot apply directly and is rejected rather than queued.
auto const bobSeq = env.seq(bob);
auto const batchFee = batch::calcBatchFee(env, 1, 2);
env(batch::outer(alice, aliceSeq + 2, batchFee, tfAllOrNothing),
batch::Inner(pay(alice, bob, XRP(10)), aliceSeq + 3),
batch::Inner(pay(bob, alice, XRP(5)), bobSeq),
batch::Sig(bob),
Ter(terQUEUED));
Ter(telCAN_NOT_QUEUE));
// The two queued transactions are untouched.
checkMetrics(*this, env, 2, std::nullopt, 3, 2);
}
}

View File

@@ -1282,6 +1282,13 @@ TxQ::apply(
}
}
// A Batch is never queued: it can advance the account sequence by more
// than one, which the TxQ's single-sequence forecast cannot model. It must
// apply straight to the open ledger or not at all. (Fee and sequence errors
// are already returned above; only an otherwise-queueable Batch reaches here.)
if (tx->getTxnType() == ttBATCH)
return {telCAN_NOT_QUEUE, false};
// Hold the transaction in the queue.
if (replacedTxIter)
{