diff --git a/include/xrpl/tx/transactors/system/Batch.h b/include/xrpl/tx/transactors/system/Batch.h index 85d28ab341..6d6f0ca92b 100644 --- a/include/xrpl/tx/transactors/system/Batch.h +++ b/include/xrpl/tx/transactors/system/Batch.h @@ -7,7 +7,7 @@ namespace xrpl { class Batch : public Transactor { public: - static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Blocker; explicit Batch(ApplyContext& ctx) : Transactor(ctx) { diff --git a/src/libxrpl/tx/apply.cpp b/src/libxrpl/tx/apply.cpp index cbe84a8482..4eb9b39780 100644 --- a/src/libxrpl/tx/apply.cpp +++ b/src/libxrpl/tx/apply.cpp @@ -166,6 +166,9 @@ applyBatchTransactions( // If the transaction should be applied push its changes to the // whole-batch view. + // NOTE: each inner tx is individually capped at kOversizeMetaDataCap; + // there is no aggregate cap here. Bounded by kMaxBatchTxCount * cap, + // which standalone txns can already produce in one ledger. if (ret.applied && (isTesSuccess(ret.ter) || isTecClaim(ret.ter))) perTxBatchView.apply(batchView); diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index a786c1d505..0ca123cd6d 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -4561,6 +4561,89 @@ class Batch_test : public beast::unit_test::Suite env(noop(carol), Ter(terQUEUED)); checkMetrics(*this, env, 1, std::nullopt, 3, 2); } + + // A Batch is a TxQ blocker (it can advance the account sequence by + // more than one via an inner TicketCreate or multiple inner txns), + // so it must be alone in the account's queue. Once a Batch is queued + // no follow-on transaction from the same account can be appended. + { + 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 (blocker) as alice's lone queue entry. + 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 cannot be queued behind it. + env(noop(alice), Seq(aliceSeq + 1), Ter(telCAN_NOT_QUEUE_BLOCKED)); + + // Other accounts are unaffected. + env(noop(carol), Ter(terQUEUED)); + } + + // A Batch (blocker) cannot be queued when the account already holds + // other queued (non-blocker) transactions. + { + test::jtx::Env env{ + *this, + makeSmallQueueConfig({{Keys::kMinimumTxnInLedgerStandalone, "2"}}), + features, + nullptr, + beast::Severity::Error}; + + auto alice = Account("alice"); + auto bob = Account("bob"); + + env.fund(XRP(10000), noripple(alice, bob)); + 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); + + // Queue two normal transactions for alice. + env(noop(alice), Seq(aliceSeq + 0), Ter(terQUEUED)); + env(noop(alice), Seq(aliceSeq + 1), Ter(terQUEUED)); + + // The Batch (blocker) cannot join a non-empty account queue. + 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(telCAN_NOT_QUEUE_BLOCKS)); + } } void