Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions include/xrpl/protocol/detail/features.macro
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@

// Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order.

XRPL_FEATURE(BatchV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (ExpiredNFTokenOfferRemoval, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (BatchInnerSigs, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)
Expand All @@ -32,7 +31,6 @@ XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo
XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionedDEX, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Batch, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(SingleAssetVault, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo)
// Check flags in Credential transactions
Expand Down
2 changes: 1 addition & 1 deletion include/xrpl/protocol/detail/transactions.macro
Original file line number Diff line number Diff line change
Expand Up @@ -918,7 +918,7 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback,
#endif
TRANSACTION(ttBATCH, 71, Batch,
Delegation::notDelegable,
featureBatch,
featureBatchV1_1,
noPriv,
({
{sfRawTransactions, soeREQUIRED},
Expand Down
21 changes: 10 additions & 11 deletions include/xrpl/tx/Transactor.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,6 @@ class Transactor
static NotTEC
checkSign(PreclaimContext const& ctx);

static NotTEC
checkBatchSign(PreclaimContext const& ctx);

// Returns the fee in fee units, not scaled for load.
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
Expand Down Expand Up @@ -293,14 +290,7 @@ class Transactor
std::optional<T> value,
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});

private:
std::pair<TER, XRPAmount>
reset(XRPAmount fee);

TER
consumeSeqProxy(SLE::pointer const& sleAccount);
TER
payFee();
protected:
static NotTEC
checkSingleSign(
ReadView const& view,
Expand All @@ -316,6 +306,15 @@ class Transactor
STObject const& sigObject,
beast::Journal const j);

private:
std::pair<TER, XRPAmount>
reset(XRPAmount fee);

TER
consumeSeqProxy(SLE::pointer const& sleAccount);
TER
payFee();

void trapTransaction(uint256) const;

/** Performs early sanity checks on the account and fee fields.
Expand Down
3 changes: 3 additions & 0 deletions include/xrpl/tx/transactors/Batch.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class Batch : public Transactor
static NotTEC
preflightSigValidated(PreflightContext const& ctx);

static NotTEC
checkBatchSign(PreclaimContext const& ctx);

static NotTEC
checkSign(PreclaimContext const& ctx);

Expand Down
8 changes: 5 additions & 3 deletions src/libxrpl/protocol/STTx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ STTx::checkBatchSign(Rules const& rules) const
JLOG(debugLog().fatal()) << "not a batch transaction";
return Unexpected("Not a batch transaction.");
}
if (!isFieldPresent(sfBatchSigners))
return Unexpected("Missing BatchSigners field.");
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

sfBatchSigners is marked soeOPTIONAL for ttBATCH (see transactions.macro), but STTx::checkBatchSign() now returns an error when the field is missing. That makes the method semantics surprising for callers that may reasonably want to validate a batch txn regardless of whether it includes BatchSigners (e.g., batches where all inner txns are from the outer account). Consider treating a missing sfBatchSigners as success (no signatures to validate), or alternatively make sfBatchSigners required at the transaction format level if the field must always be present in V1_1.

Suggested change
return Unexpected("Missing BatchSigners field.");
return {};

Copilot uses AI. Check for mistakes.
STArray const& signers{getFieldArray(sfBatchSigners)};
for (auto const& signer : signers)
{
Expand All @@ -292,9 +294,8 @@ STTx::checkBatchSign(Rules const& rules) const
}
catch (std::exception const& e)
{
JLOG(debugLog().error()) << "Batch signature check failed: " << e.what();
return Unexpected(std::string("Internal batch signature check failure: ") + e.what());
}
return Unexpected("Internal batch signature check failure.");
}

Json::Value
Expand Down Expand Up @@ -416,6 +417,7 @@ STTx::checkBatchSingleSign(STObject const& batchSigner) const
{
Serializer msg;
serializeBatch(msg, getFlags(), getBatchTransactionIDs());
finishMultiSigningData(batchSigner.getAccountID(sfAccount), msg);
return singleSignHelper(batchSigner, msg.slice());
}

Expand Down Expand Up @@ -488,7 +490,7 @@ multiSignHelper(
if (!validSig)
return Unexpected(
std::string("Invalid signature on account ") + toBase58(accountID) +
errorWhat.value_or("") + ".");
(errorWhat ? ": " + *errorWhat : "") + ".");
}
// All signatures verified.
return {};
Expand Down
56 changes: 6 additions & 50 deletions src/libxrpl/tx/Transactor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,12 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask)
if (ctx.tx.getSeqProxy().isTicket() && ctx.tx.isFieldPresent(sfAccountTxnID))
return temINVALID;

if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch))
if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatchV1_1))
return temINVALID_FLAG;

XRPL_ASSERT(
ctx.tx.isFlag(tfInnerBatchTxn) == ctx.parentBatchId.has_value() ||
!ctx.rules.enabled(featureBatch),
!ctx.rules.enabled(featureBatchV1_1),
"Inner batch transaction must have a parent batch ID.");

return tesSUCCESS;
Expand All @@ -196,13 +196,13 @@ Transactor::preflight2(PreflightContext const& ctx)
return *ret;

// It should be impossible for the InnerBatchTxn flag to be set without
// featureBatch being enabled
// featureBatchV1_1 being enabled
XRPL_ASSERT_PARTS(
!ctx.tx.isFlag(tfInnerBatchTxn) || ctx.rules.enabled(featureBatch),
!ctx.tx.isFlag(tfInnerBatchTxn) || ctx.rules.enabled(featureBatchV1_1),
"xrpl::Transactor::preflight2",
"InnerBatch flag only set if feature enabled");
// Skip signature check on batch inner transactions
if (ctx.tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch))
if (ctx.tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatchV1_1))
return tesSUCCESS;
// Do not add any checks after this point that are relevant for
// batch inner transactions. They will be skipped.
Expand Down Expand Up @@ -647,7 +647,7 @@ Transactor::checkSign(

auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey);
// Ignore signature check on batch inner transactions
if (parentBatchId && view.rules().enabled(featureBatch))
if (parentBatchId && view.rules().enabled(featureBatchV1_1))
{
// Defensive Check: These values are also checked in Batch::preflight
if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() ||
Expand Down Expand Up @@ -699,50 +699,6 @@ Transactor::checkSign(PreclaimContext const& ctx)
return checkSign(ctx.view, ctx.flags, ctx.parentBatchId, idAccount, ctx.tx, ctx.j);
}

NotTEC
Transactor::checkBatchSign(PreclaimContext const& ctx)
{
NotTEC ret = tesSUCCESS;
STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)};
for (auto const& signer : signers)
{
auto const idAccount = signer.getAccountID(sfAccount);

Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey);
if (pkSigner.empty())
{
if (ret = checkMultiSign(ctx.view, ctx.flags, idAccount, signer, ctx.j);
!isTesSuccess(ret))
return ret;
}
else
{
// LCOV_EXCL_START
if (!publicKeyType(makeSlice(pkSigner)))
return tefBAD_AUTH;
// LCOV_EXCL_STOP

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

// A batch can include transactions from an un-created account ONLY
// when the account master key is the signer
if (!sleAccount)
{
if (idAccount != idSigner)
return tefBAD_AUTH;

return tesSUCCESS;
}

if (ret = checkSingleSign(ctx.view, idSigner, idAccount, sleAccount, ctx.j);
!isTesSuccess(ret))
return ret;
}
}
return ret;
}

NotTEC
Transactor::checkSingleSign(
ReadView const& view,
Expand Down
19 changes: 1 addition & 18 deletions src/libxrpl/tx/apply.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,12 @@ checkValidity(HashRouter& router, STTx const& tx, Rules const& rules)
auto const flags = router.getFlags(id);

// Ignore signature check on batch inner transactions
if (tx.isFlag(tfInnerBatchTxn) && rules.enabled(featureBatch))
if (tx.isFlag(tfInnerBatchTxn) && rules.enabled(featureBatchV1_1))
{
// Defensive Check: These values are also checked in Batch::preflight
if (tx.isFieldPresent(sfTxnSignature) || !tx.getSigningPubKey().empty() ||
tx.isFieldPresent(sfSigners))
return {Validity::SigBad, "Malformed: Invalid inner batch transaction."};

// This block should probably have never been included in the
// original `Batch` implementation. An inner transaction never
// has a valid signature.
bool const neverValid = rules.enabled(fixBatchInnerSigs);
if (!neverValid)
{
std::string reason;
if (!passesLocalChecks(tx, reason))
{
router.setFlags(id, SF_LOCALBAD);
return {Validity::SigGoodOnly, reason};
}

router.setFlags(id, SF_SIGGOOD);
return {Validity::Valid, ""};
}
}

if (any(flags & SF_SIGBAD))
Expand Down
80 changes: 75 additions & 5 deletions src/libxrpl/tx/transactors/Batch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,18 @@
if (signer.isFieldPresent(sfTxnSignature))
signerCount += 1;
else if (signer.isFieldPresent(sfSigners))
signerCount += signer.getFieldArray(sfSigners).size();
{
auto const& nestedSigners = signer.getFieldArray(sfSigners);
// LCOV_EXCL_START
if (nestedSigners.size() > STTx::maxMultiSigners)
{
JLOG(debugLog().error())
<< "BatchTrace: Nested Signers array exceeds max entries.";
return XRPAmount{INITIAL_XRP};
}
// LCOV_EXCL_STOP
signerCount += nestedSigners.size();
}
}
}

Expand Down Expand Up @@ -205,6 +216,14 @@
return temARRAY_TOO_LARGE;
}

if (ctx.tx.isFieldPresent(sfBatchSigners) &&
ctx.tx.getFieldArray(sfBatchSigners).size() > maxBatchTxCount)
{
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]:"
<< "signers array exceeds 8 entries.";
return temARRAY_TOO_LARGE;
}

// Validation Inner Batch Txns
std::unordered_set<uint256> uniqueHashes;
std::unordered_map<AccountID, std::unordered_set<std::uint32_t>> accountSeqTicket;
Expand Down Expand Up @@ -426,7 +445,7 @@
if (requiredSigners.erase(signerAccount) == 0)
{
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
<< "no account signature for inner txn.";
<< "extra signer provided: " << signerAccount;
return temBAD_SIGNER;
}
}
Expand All @@ -451,6 +470,54 @@
return tesSUCCESS;
}

NotTEC
Batch::checkBatchSign(PreclaimContext const& ctx)
{
NotTEC ret = tesSUCCESS;
STArray const& signers{ctx.tx.getFieldArray(sfBatchSigners)};
for (auto const& signer : signers)
{
auto const idAccount = signer.getAccountID(sfAccount);

Blob const& pkSigner = signer.getFieldVL(sfSigningPubKey);
if (pkSigner.empty())
{
if (ret = checkMultiSign(ctx.view, ctx.flags, idAccount, signer, ctx.j);
!isTesSuccess(ret))
return ret;
}
else
{
// LCOV_EXCL_START
if (!publicKeyType(makeSlice(pkSigner)))
return tefBAD_AUTH;
// LCOV_EXCL_STOP

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

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

Check warning on line 502 in src/libxrpl/tx/transactors/Batch.cpp

View check run for this annotation

Codecov / codecov/patch

src/libxrpl/tx/transactors/Batch.cpp#L502

Added line #L502 was not covered by tests

if (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
}
}
}
return ret;
}

/**
* @brief Checks the validity of signatures for a batch transaction.
*
Expand All @@ -459,7 +526,7 @@
* corresponding error code.
*
* Next, it verifies the batch-specific signature requirements by calling
* Transactor::checkBatchSign. If this check fails, it also returns the
* Batch::checkBatchSign. If this check fails, it also returns the
* corresponding error code.
*
* If both checks succeed, the function returns tesSUCCESS.
Expand All @@ -474,8 +541,11 @@
if (auto ret = Transactor::checkSign(ctx); !isTesSuccess(ret))
return ret;

if (auto ret = Transactor::checkBatchSign(ctx); !isTesSuccess(ret))
return ret;
if (ctx.tx.isFieldPresent(sfBatchSigners))
{
if (auto ret = checkBatchSign(ctx); !isTesSuccess(ret))
return ret;
}

return tesSUCCESS;
}
Expand Down
2 changes: 1 addition & 1 deletion src/libxrpl/tx/transactors/Lending/LoanSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ LoanSet::preflight(PreflightContext const& ctx)
auto const& tx = ctx.tx;

// Special case for Batch inner transactions
if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatch) &&
if (tx.isFlag(tfInnerBatchTxn) && ctx.rules.enabled(featureBatchV1_1) &&
!tx.isFieldPresent(sfCounterparty))
{
auto const parentBatchId = ctx.parentBatchId.value_or(uint256{0});
Expand Down
Loading
Loading