Skip to content

feat: Batch (XLS-56) V1_1#6446

Open
dangell7 wants to merge 59 commits into
developfrom
dangell7/batch-v1
Open

feat: Batch (XLS-56) V1_1#6446
dangell7 wants to merge 59 commits into
developfrom
dangell7/batch-v1

Conversation

@dangell7

@dangell7 dangell7 commented Feb 26, 2026

Copy link
Copy Markdown
Collaborator

High Level Overview of Change

Feature

  • New Batch transactor and featureBatchV1_1 amendment (69084a6ff5)
  • Outer account ID added to batch serialization (359a94b766)
  • Test suite + jtx batch helpers (aafe4b6201, plus tests bundled with the fixes below)

Security / audit fixes

These came out of the security audit and harden authorization and input-bounding:

  • Bind signer signatures to the outer account & sequence (7618b726b3) — serializeBatch now hashes in the outer Account and Sequence, and multi-sign data includes the batch signer account. Prevents a batch signature from being lifted and replayed against a different outer account/sequence.
  • Reject tfInnerBatchTxn unconditionally from the network (6393ac8c47) — inner-batch flag is now refused in NetworkOPs regardless of amendment state, closing a window where the gate depended on featureBatchV1_1 being enabled.
  • Enforce strictly-ascending, unique batch signers (477532edfe) — signers must be sorted by AccountID; duplicate/unsorted arrays return temBAD_SIGNER. Removes ambiguity and a malleability vector in the signer set.
  • Bound sfRawTransactions before eager txn-id hashing (d90fc44c0b) — skip hashing when the raw array exceeds maxBatchTxCount, preventing unbounded work on oversized input.
  • Skip inner batch txns during ledger replay (c27613e1df) — BuildLedger no longer re-applies inner transactions on replay.
  • Defensive bound on nested Signers array (c729a26dd3) — calculateBaseFee caps nested signers at STTx::maxMultiSigners.
  • Additional defensive checks in Transactor (871d60f910, fix: BatchV_1 add defensive checks #6719).

Context of Change

XLS spec: https://github.com/XRPLF/XRPL-Standards/blob/master/XLS-0056-batch/README.md

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking change that only restructures code)
  • Performance (increase or change in throughput and/or latency)
  • Tests (you added tests for code that already exists, or your new feature included in this PR)
  • Documentation update
  • Chore (no impact to binary, e.g. .gitignore, formatting, dropping support for older tooling)
  • Release

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

Edit: Fixed spec URL

Copilot AI review requested due to automatic review settings February 26, 2026 22:40
@dangell7 dangell7 changed the title Batch (XLS-56) V1_1 feat: Batch (XLS-56) V1_1 Feb 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Batch transaction implementation to a new amendment name/version (“BatchV1_1”), adjusts batch signature construction/verification, and refreshes the related validation paths and unit tests.

Changes:

  • Renames/gates Batch functionality under featureBatchV1_1 (replacing prior featureBatch / removing fixBatchInnerSigs usage).
  • Updates batch signature message construction to include finishMultiSigningData for batch signers and strengthens signer validation to ensure all signers are checked.
  • Refactors batch signer authorization checks from Transactor into Batch, and updates/extends tests accordingly.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/xrpld/overlay/detail/PeerImp.cpp Updates overlay comments to reference featureBatchV1_1.
src/xrpld/app/misc/NetworkOPs.cpp Switches network submission/processing checks to featureBatchV1_1.
src/test/rpc/Feature_test.cpp Updates feature name expectation for the Batch amendment.
src/test/jtx/impl/batch.cpp Adjusts batch signer signature construction (finishMultiSigningData).
src/test/app/Batch_test.cpp Updates tests to use featureBatchV1_1, removes fixBatchInnerSigs branching, and adds a regression test ensuring all signers are validated.
src/libxrpl/tx/transactors/Lending/LoanSet.cpp Switches inner-batch gating to featureBatchV1_1.
src/libxrpl/tx/transactors/Batch.cpp Adds bounds checks, improves signer diagnostics, and introduces Batch::checkBatchSign used from Batch::checkSign.
src/libxrpl/tx/apply.cpp Updates inner-batch handling to key off featureBatchV1_1 and removes legacy inner-sig behavior.
src/libxrpl/tx/Transactor.cpp Switches inner-batch gating to featureBatchV1_1 and removes Transactor::checkBatchSign.
src/libxrpl/protocol/STTx.cpp Tightens batch signature verification flow; updates single-sign batch digest construction and error reporting.
include/xrpl/tx/transactors/Batch.h Declares Batch::checkBatchSign.
include/xrpl/tx/Transactor.h Removes checkBatchSign API and adjusts access to allow Batch to reuse signing helpers.
include/xrpl/protocol/detail/transactions.macro Gates ttBATCH on featureBatchV1_1.
include/xrpl/protocol/detail/features.macro Introduces BatchV1_1 and removes prior Batch-related amendment entries.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/libxrpl/protocol/STTx.cpp Outdated
@codecov

codecov Bot commented Feb 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.57364% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.0%. Comparing base (50fdb38) to head (d031a22).
⚠️ Report is 4 commits behind head on develop.

Files with missing lines Patch % Lines
src/libxrpl/tx/transactors/payment/Payment.cpp 70.0% 3 Missing ⚠️
src/libxrpl/tx/Transactor.cpp 77.8% 2 Missing ⚠️
src/libxrpl/tx/transactors/system/Batch.cpp 96.9% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           develop   #6446    +/-   ##
========================================
  Coverage     82.0%   82.0%            
========================================
  Files         1007    1007            
  Lines        76888   77070   +182     
  Branches      8971    8973     +2     
========================================
+ Hits         63042   63212   +170     
- Misses       13837   13849    +12     
  Partials         9       9            
Files with missing lines Coverage Δ
include/xrpl/core/HashRouter.h 100.0% <ø> (ø)
include/xrpl/ledger/OpenView.h 100.0% <ø> (ø)
include/xrpl/protocol/Batch.h 100.0% <100.0%> (ø)
include/xrpl/protocol/Protocol.h 100.0% <ø> (ø)
include/xrpl/protocol/STTx.h 100.0% <ø> (ø)
include/xrpl/protocol/TER.h 100.0% <ø> (ø)
include/xrpl/protocol/detail/transactions.macro 100.0% <ø> (ø)
include/xrpl/protocol_autogen/transactions/Batch.h 100.0% <ø> (ø)
include/xrpl/tx/ApplyContext.h 100.0% <ø> (ø)
include/xrpl/tx/Transactor.h 100.0% <ø> (ø)
... and 13 more

... and 15 files with indirect coverage changes

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mvadari mvadari requested review from mvadari and ximinez February 27, 2026 18:43
Comment thread src/libxrpl/tx/transactors/system/Batch.cpp
Comment thread src/libxrpl/tx/transactors/system/Batch.cpp Outdated
Comment thread src/libxrpl/tx/transactors/Batch.cpp Outdated
Comment thread src/libxrpl/protocol/STTx.cpp
Comment thread src/libxrpl/tx/transactors/system/Batch.cpp Outdated
Comment thread src/libxrpl/protocol/STTx.cpp Outdated
Comment thread src/libxrpl/tx/transactors/system/Batch.cpp Outdated
@mvadari mvadari mentioned this pull request Mar 19, 2026
13 tasks
@dangell7 dangell7 force-pushed the dangell7/batch-v1 branch from 07c3143 to 70752f8 Compare March 19, 2026 18:30
Comment thread src/libxrpl/protocol/STTx.cpp
Comment thread src/libxrpl/protocol/STTx.cpp

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

Review by Claude Sonnet 4.6 · Prompt: V15

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues.

Review by Claude Sonnet 4.6 · Prompt: V15

@github-actions

Copy link
Copy Markdown

All conflicts have been resolved. Assigned reviewers can now start or resume their review.

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One historical security issue called out inline (now fixed in this MR) — signature binding in checkBatchSingleSign previously excluded Account and Sequence, enabling cross-account replay. The fix is present in this diff; comment is for audit traceability.

Review by Claude Sonnet 4.6 · Prompt: V15

Comment thread src/libxrpl/protocol/STTx.cpp
Comment thread src/xrpld/app/misc/detail/TxQ.cpp Outdated
Comment on lines +1289 to +1290
if (tx->getTxnType() == ttBATCH)
return {telCAN_NOT_QUEUE, false};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a PR that does the same if it's a delegate txn. #7640

The check should probably be moved to canBeHeld as well

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the check into canBeHeld, matching #7640's delegate check. Put it above the existing checks so #7640 still merges clean. resolved: 188bde223f

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ship it

Review by Claude Sonnet 4.6 · Prompt: V15

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All clear


Review by ReviewBot 🤖

Review by Claude Sonnet 4.6 · Prompt: V15

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

Review by Claude Sonnet 4.6 · Prompt: V15

shawnxie999
shawnxie999 previously approved these changes Jun 26, 2026

@shawnxie999 shawnxie999 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, the amendment needs to be toggled to Supported::Yes but that can be done separately

mvadari
mvadari previously approved these changes Jun 26, 2026

@ximinez ximinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another partial review. This one covers the only changes since my last review.

// this guard only protects release builds where asserts are no-ops.
if (getTxnType() != ttBATCH)
{
JLOG(debugLog().fatal()) << "not a batch transaction";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You didn't necessarily have to remove the log message, but I don't have an opinion one way or the other whether you should put it back.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving the log removed, no strong reason either way. Thanks.

Comment thread src/libxrpl/protocol/STTx.cpp Outdated
Comment thread src/libxrpl/protocol/STTx.cpp
Comment thread src/libxrpl/protocol/STTx.cpp Outdated
Comment on lines +744 to +747
// sfRawTransactions only appears on a Batch.
XRPL_ASSERT(
safeCast<TxType>(st.getFieldU16(sfTransactionType)) == ttBATCH,
"xrpl::isBatchRawTransactionOkay : not a batch transaction");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Assertion descriptions are supposed to describe the desired state. So this should be ... : batch transaction. (UNREACHABLE is the opposite. That description should describe what is wrong.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, the description now states the desired condition: "batch transaction". Swept the sibling batch asserts too. resolved: d031a22027

Comment thread src/libxrpl/tx/transactors/payment/Payment.cpp
Comment thread src/libxrpl/tx/transactors/system/Batch.cpp Outdated
<< "invalid batch txn signature: " << sigResult.error();
return temBAD_SIGNATURE;
}
router.setFlags(parentBatchId, kSfBatchSigGood);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably cache failures, too. That way, if the same transaction is resubmitted (or even retried locally), you won't waste the time to check the batch signatures again.

You could do that with a new PRIVATE8, or I think even better would be to reuse HashRouterFlags::BAD. The advantage of BAD is that if the transaction is seen again (before the entry expires), it'll get stopped and dropped well before getting into the transaction engine.

Note: NetworkOPsImp::apply sets BAD if the transaction engine returns a tem code, so ignore this suggestion.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, NetworkOPs already sets BAD on a tem result, so keeping it good-only. Thanks.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block should go before the inner signature checking (immediately after the signer matching loop). It's way cheaper than checking signatures, and more readable, IMHO.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, moved the required-signer count check before the signature check so it fails fast. resolved: d031a22027

Comment thread src/libxrpl/tx/Transactor.cpp Outdated
Comment on lines -249 to -256
if (ctx.tx.isFlag(tfInnerBatchTxn))
{
if (!ctx.rules.enabled(featureBatchV1_1))
return temINVALID_FLAG;
if (!ctx.parentBatchId.has_value())
return temINVALID_INNER_BATCH;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any harm in keeping these checks? They seem like a good defense in depth mechanism.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, restored them as defense in depth, with a comment noting the preflight1 copy. resolved: d031a22027

Comment thread src/xrpld/app/misc/detail/TxQ.cpp Outdated
Comment on lines +393 to +397
// 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.
if (tx.getTxnType() == ttBATCH)
return telCAN_NOT_QUEUE;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: The TxQ's sequence model can handle the account sequence advancing by more than one. See TicketCreate. The reason to exclude Batch is that it cannot model one transaction changing the sequence number for more than one account. For a single account consuming more than one sequence, you could customize the TxConsequences generator, but that has to be more definitive, too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the correction. Updated the comment: a Batch changes sequence numbers for multiple accounts, which the per-account model can't forecast. resolved: d031a22027

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 3 comments.

Comment thread src/libxrpl/tx/transactors/payment/Payment.cpp
Comment thread src/libxrpl/tx/transactors/payment/Payment.cpp
Comment on lines +744 to +748
// sfRawTransactions only appears on a Batch.
XRPL_ASSERT(
safeCast<TxType>(st.getFieldU16(sfTransactionType)) == ttBATCH,
"xrpl::isBatchRawTransactionOkay : not a batch transaction");

@dangell7 dangell7 dismissed stale reviews from mvadari and shawnxie999 via d031a22 June 26, 2026 23:55

@ximinez ximinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial review.

I need to pick up at src/test/app/Batch_test.cpp, though I'll probably skip over the tests to get through the first pass.

Comment on lines +572 to 601
void
STTx::buildBatchTxnIds()
{
// Precondition: the template must have been applied first, so the fields
// (including sfRawTransactions) are canonical before the inner txns are
// hashed. The constructors call this immediately after applying the
// template; isFree() being false confirms a template is set.
XRPL_ASSERT(!isFree(), "STTx::buildBatchTxnIds : template not applied");
if (getTxnType() != ttBATCH || !isFieldPresent(sfRawTransactions))
return;

auto const& raw = getFieldArray(sfRawTransactions);

if (raw.size() > kMaxBatchTxCount)
return;

batchTxnIds_.reserve(raw.size());
for (STObject const& rb : raw)
batchTxnIds_.push_back(rb.getHash(HashPrefix::TransactionId));
}

std::vector<uint256> const&
STTx::getBatchTransactionIDs() const
{
XRPL_ASSERT(getTxnType() == ttBATCH, "STTx::getBatchTransactionIDs : not a batch transaction");
XRPL_ASSERT(
!getFieldArray(sfRawTransactions).empty(),
"STTx::getBatchTransactionIDs : empty raw transactions");

// The list of inner ids is built once, then reused on subsequent calls.
// After the list is built, it must always have the same size as the array
// `sfRawTransactions`. The assert below verifies that.
if (batchTxnIds_.empty())
{
for (STObject const& rb : getFieldArray(sfRawTransactions))
batchTxnIds_.push_back(rb.getHash(HashPrefix::TransactionId));
}

!batchTxnIds_.empty(), "STTx::getBatchTransactionIDs : batch transaction IDs not built");
XRPL_ASSERT(
batchTxnIds_.size() == getFieldArray(sfRawTransactions).size(),
"STTx::getBatchTransactionIDs : batch transaction IDs size mismatch");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a transaction with too many batch signers or no batch signers calls buildBatchTxnIds(), you end up with an empty batchTxnIds_ collection.

If later, that transaction calls getBatchTransactionIDs(), it will fail one of these assertions. Right now, the code is structured so that getBatchTransactionIDs() won't be called until after the signer matching has happened... (Wait, is that why you deferred the checking until Batch::preflight?) ...but that could change down the road, and may not be detected until user input crashes a node running debug. It's fragile.

Here's how to fix it:

  1. Change the declaration to std::optional<std::vector<uint256>> batchTxnIds_;.
  2. In buildBatchTxnIds(), right after you check that the type is ttBATCH, call batchTxnIds_.emplace().
  3. Change the places that read it to use -> instead of ..
  4. Either change the return type of getBatchTransactionIDs() to match batchTxnIds_, and handle the case where it's unseated, or check whether batchTxnIds_ is seated before calling it.
    a. Change the assertions inside getBatchTransactionIDs() as appropriate based on what you did in the previous step.
  5. Move the call to checkBatchSign from Batch::preflightSigValidated to STTx::checkSign(Rules const&). Gate it on if (batchTxnIds_) or something like that, not on type == ttBATCH.
  6. Get rid of the now unnecessary HashRouterFlags::PRIVATE7 and kSfBatchSigGood.
  7. Tada! The batch signature checking happens with the rest of the signature checking, is cached with the rest of the signature checking, is out of the transaction engine, and is out of the serialized path processing sets of transactions.

This is a blocker.

Comment thread src/libxrpl/protocol/STTx.cpp Outdated
Comment on lines +744 to +747
// sfRawTransactions only appears on a Batch.
XRPL_ASSERT(
safeCast<TxType>(st.getFieldU16(sfTransactionType)) == ttBATCH,
"xrpl::isBatchRawTransactionOkay : not a batch transaction");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is called by passesLocalChecks, which is called from checkValidity, which is called from potentially unverified user and peer input. Thus, I don't think it's impossible with malicious or malformed input.

Replace this assert to

// sfRawTransactions only appears on a Batch.
if(safeCast<TxType>(st.getFieldU16(sfTransactionType)) != ttBATCH)
	return false;

I have considered that applyTemplate will fail for any transaction with sfRawTransactions that is of a type that doesn't specify sfRawTransactions. And only Batch has sfRawTransactions, but I don't think that is sufficiently fool- and future-proof, particularly when checking it this way is so simple.

JLOG(debugLog().error()) << "BatchTrace: XRPAmount overflow in signerFees calculation.";
return XRPAmount{kInitialXrp};
}
if (txnFees + signerFees > maxAmount - batchBase)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Micro-optimization: Instead of computing txnFees + signerFees twice, define auto const innerFees = txnFees + signerFees. Use it here, and in the final return calculation.

Optional

requiredSigners.insert(innerAccount);
// A delegated inner is signed by the delegate, not the account holder,
// so the delegate is the required signer when present.
AccountID const authorizer = rb[~sfDelegate].value_or(rb[sfAccount]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
AccountID const authorizer = rb[~sfDelegate].value_or(rb[sfAccount]);
AccountID const authorizer = rb.getFeePayer();

We've got the utility functions - we need to use them.

{
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
<< "extra signer provided: " << signerAccount;
return temBAD_SIGNER;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I think this might also indicate a missing signer. It might be worth tweaking the log message to say, "missing signer or extra signer provided: " << signerAccount. I don't think it's necessarily worth checking which is which.

else
{
if (!publicKeyType(makeSlice(pkSigner)))
return tefBAD_AUTH; // LCOV_EXCL_LINE

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: This could be checked in preflightSigValidated.

Comment on lines +528 to +562
auto const idAccount = signer.getAccountID(sfAccount);
Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey);
if (pkSigner.empty())
{
if (auto const ret = checkMultiSign(ctx.view, ctx.flags, idAccount, signer, ctx.j);
!isTesSuccess(ret))
return ret;
}
else
{
if (!publicKeyType(makeSlice(pkSigner)))
return tefBAD_AUTH; // LCOV_EXCL_LINE

auto const idSigner = calcAccountID(PublicKey(makeSlice(pkSigner)));
auto const sleAccount = ctx.view.read(keylet::account(idAccount));

if (sleAccount)
{
if (isPseudoAccount(sleAccount))
return tefBAD_AUTH; // LCOV_EXCL_LINE

if (auto const ret =
checkSingleSign(ctx.view, idSigner, idAccount, sleAccount, ctx.j);
!isTesSuccess(ret))
return ret;
}
else
{
if (idAccount != idSigner)
return tefBAD_AUTH;

// A batch can include transactions from an un-created account ONLY
// when the account master key is the signer
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% certain, but I think this whole loop can be replaced by Transactor::checkSign:

        auto const idAccount = signer.getAccountID(sfAccount);
		if (auto const ret = checkSign(ctx.view, ctx.flags, ctx.parentBatchId, idAccount, signer, ctx.j;
			!isTesSuccess(ret))
			return ret;

Reduce code duplication, prevent the different implementations from drifting, know for certain that they're both following the same rules, etc.

Also, change the isPseudoAccount check at the top of Transactor::checkSign to

        if ((view.rules().enabled(featureLendingProtocol)
			|| view.rules().enabled(featureBatchV1_1)
			|| view.rules().enabled(fixCleanup3_3_0)) && isPseudoAccount(sle))
        {
            // Pseudo-accounts can't sign transactions. This check is gated on
            // a few different amendments so that it can take effect as soon as
			// any of them are activated.
            return tefBAD_AUTH;
        }

Comment thread src/libxrpl/tx/apply.cpp
return {Validity::Valid, ""};
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but I don't think we should ever be calling checkValidity on an inner transaction now. If so, replace this whole thing with:

    if (tx.isFlag(tfInnerBatchTxn))
    {
            router.setFlags(id, kSfSigbad);
            return {Validity::SigBad, "Batch inner transactions are never considered validly signed."};
    }

Note that, similar to NetworkOPs and PeerImp, we don't need to gate the check on the amendment. If the tx has this flag, it's never valid.

Comment thread src/libxrpl/tx/Transactor.cpp Outdated
Comment on lines 247 to 250
// Skip signature check on batch inner transactions. The inner-batch flag
// and parentBatchId checks are in preflight1.
if (ctx.tx.isFlag(tfInnerBatchTxn))
return tesSUCCESS;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like, unlike the places where tfInnerBatchTxn is never valid, so don't check the amendment, this is returning a success result. And even though we shouldn't get here without the amendment being enabled, it would be a good defensive check to just make sure. ctx.tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatchV1_1).

@xrplf-ai-reviewer xrplf-ai-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One informational note on the replay-prevention fix — see inline.

Review by Claude Sonnet 4.6 · Prompt: V15

Serializer msg;
serializeBatch(msg, getFlags(), getBatchTransactionIDs());
serializeBatch(
msg, getAccountID(sfAccount), getSeqValue(), getFlags(), getBatchTransactionIDs());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirms the security fix: serializeBatch now binds signatures to the outer Account and Sequence, and finishMultiSigningData includes the batch signer account — preventing cross-account/sequence signature replay.

@github-actions

Copy link
Copy Markdown

This PR has conflicts, please resolve them in order for the PR to be reviewed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants