From d25915ca1dd5d25d8a12909d251b165ff6b404ca Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Thu, 8 Jan 2026 08:48:39 -0500 Subject: [PATCH] fix: Reorder Batch Preflight Errors (#6176) This change fixes https://github.com/XRPLF/rippled/issues/6058. --- src/test/app/Batch_test.cpp | 32 +++++++++++++++++++++++++++++++ src/xrpld/app/tx/detail/Batch.cpp | 20 +++++++++---------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index 1dac56d2d9..328cb63dbe 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -420,6 +420,38 @@ class Batch_test : public beast::unit_test::suite env.close(); } + // temBAD_FEE: Inner txn with negative fee + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1); + tx1[jss::Fee] = "-1"; + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(2)), seq + 2), + ter(temBAD_FEE)); + env.close(); + } + + // temBAD_FEE: Inner txn with non-integer fee + { + auto const seq = env.seq(alice); + auto const batchFee = batch::calcBatchFee(env, 0, 2); + auto tx1 = batch::inner(pay(alice, bob, XRP(1)), seq + 1); + tx1[jss::Fee] = "1.5"; + try + { + env(batch::outer(alice, seq, batchFee, tfAllOrNothing), + tx1, + batch::inner(pay(alice, bob, XRP(2)), seq + 2)); + fail("Expected parse_error for fractional fee"); + } + catch (jtx::parse_error const&) + { + BEAST_EXPECT(true); + } + } + // temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence // and TicketSequence. { diff --git a/src/xrpld/app/tx/detail/Batch.cpp b/src/xrpld/app/tx/detail/Batch.cpp index 8c1c515021..87c6907cc4 100644 --- a/src/xrpld/app/tx/detail/Batch.cpp +++ b/src/xrpld/app/tx/detail/Batch.cpp @@ -324,6 +324,16 @@ Batch::preflight(PreflightContext const& ctx) } } + // Check that the Fee is native asset (XRP) and zero + if (auto const fee = stx.getFieldAmount(sfFee); + !fee.native() || fee.xrp() != beast::zero) + { + JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " + << "inner txn must have a fee of 0. " + << "txID: " << hash; + return temBAD_FEE; + } + auto const innerAccount = stx.getAccountID(sfAccount); if (auto const preflightResult = ripple::preflight( ctx.app, ctx.rules, parentBatchId, stx, tapBATCH, ctx.j); @@ -336,16 +346,6 @@ Batch::preflight(PreflightContext const& ctx) return temINVALID_INNER_BATCH; } - // Check that the fee is zero - if (auto const fee = stx.getFieldAmount(sfFee); - !fee.native() || fee.xrp() != beast::zero) - { - JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " - << "inner txn must have a fee of 0. " - << "txID: " << hash; - return temBAD_FEE; - } - // Check that Sequence and TicketSequence are not both present if (stx.isFieldPresent(sfTicketSequence) && stx.getFieldU32(sfSequence) != 0)