Compare commits

...

9 Commits

Author SHA1 Message Date
Denis Angell
200f93a287 Merge branch 'develop' into dangell7/batch-v1 2026-02-26 23:41:34 +01:00
Denis Angell
90d2eb839a reverse negated else 2026-02-26 22:27:24 +01:00
Denis Angell
359a94b766 add account id to batch serialization 2026-02-26 15:12:49 +01:00
Denis Angell
5e39c1d80c clang-format 2026-02-26 15:12:49 +01:00
Denis Angell
25abc8ffae minor adjustments 2026-02-26 14:03:13 +01:00
Denis Angell
a669dcec87 misc review fix
- add early `sfBatchSigners` size check
- fix log nomenclature
2026-02-26 14:03:01 +01:00
Denis Angell
4f5a3241de move checkBatchSign 2026-02-26 14:03:01 +01:00
Denis Angell
c729a26dd3 defensive check for nested Signers array size 2026-02-26 14:02:52 +01:00
Denis Angell
69084a6ff5 feature BatchV1_1 2026-02-26 14:02:51 +01:00
14 changed files with 169 additions and 119 deletions

View File

@@ -15,10 +15,9 @@
// Add new amendments to the top of this list. // Add new amendments to the top of this list.
// Keep it sorted in reverse chronological order. // Keep it sorted in reverse chronological order.
XRPL_FEATURE(BatchV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PermissionedDomainInvariant, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (ExpiredNFTokenOfferRemoval, 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(LendingProtocol, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo) XRPL_FEATURE(PermissionDelegationV1_1, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (DirectoryLimit, Supported::yes, VoteBehavior::DefaultNo)
@@ -32,7 +31,6 @@ XRPL_FEATURE(TokenEscrow, Supported::yes, VoteBehavior::DefaultNo
XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (EnforceNFTokenTrustlineV2, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (AMMv1_3, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionedDEX, 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_FEATURE(SingleAssetVault, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo) XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo)
// Check flags in Credential transactions // Check flags in Credential transactions

View File

@@ -918,7 +918,7 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback,
#endif #endif
TRANSACTION(ttBATCH, 71, Batch, TRANSACTION(ttBATCH, 71, Batch,
Delegation::notDelegable, Delegation::notDelegable,
featureBatch, featureBatchV1_1,
noPriv, noPriv,
({ ({
{sfRawTransactions, soeREQUIRED}, {sfRawTransactions, soeREQUIRED},

View File

@@ -162,9 +162,6 @@ public:
static NotTEC static NotTEC
checkSign(PreclaimContext const& ctx); checkSign(PreclaimContext const& ctx);
static NotTEC
checkBatchSign(PreclaimContext const& ctx);
// Returns the fee in fee units, not scaled for load. // Returns the fee in fee units, not scaled for load.
static XRPAmount static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx); calculateBaseFee(ReadView const& view, STTx const& tx);
@@ -293,14 +290,7 @@ protected:
std::optional<T> value, std::optional<T> value,
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{}); unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});
private: protected:
std::pair<TER, XRPAmount>
reset(XRPAmount fee);
TER
consumeSeqProxy(SLE::pointer const& sleAccount);
TER
payFee();
static NotTEC static NotTEC
checkSingleSign( checkSingleSign(
ReadView const& view, ReadView const& view,
@@ -316,6 +306,15 @@ private:
STObject const& sigObject, STObject const& sigObject,
beast::Journal const j); beast::Journal const j);
private:
std::pair<TER, XRPAmount>
reset(XRPAmount fee);
TER
consumeSeqProxy(SLE::pointer const& sleAccount);
TER
payFee();
void trapTransaction(uint256) const; void trapTransaction(uint256) const;
/** Performs early sanity checks on the account and fee fields. /** Performs early sanity checks on the account and fee fields.

View File

@@ -27,6 +27,9 @@ public:
static NotTEC static NotTEC
preflightSigValidated(PreflightContext const& ctx); preflightSigValidated(PreflightContext const& ctx);
static NotTEC
checkBatchSign(PreclaimContext const& ctx);
static NotTEC static NotTEC
checkSign(PreclaimContext const& ctx); checkSign(PreclaimContext const& ctx);

View File

@@ -278,6 +278,8 @@ STTx::checkBatchSign(Rules const& rules) const
JLOG(debugLog().fatal()) << "not a batch transaction"; JLOG(debugLog().fatal()) << "not a batch transaction";
return Unexpected("Not a batch transaction."); return Unexpected("Not a batch transaction.");
} }
if (!isFieldPresent(sfBatchSigners))
return Unexpected("Missing BatchSigners field.");
STArray const& signers{getFieldArray(sfBatchSigners)}; STArray const& signers{getFieldArray(sfBatchSigners)};
for (auto const& signer : signers) for (auto const& signer : signers)
{ {
@@ -292,9 +294,8 @@ STTx::checkBatchSign(Rules const& rules) const
} }
catch (std::exception const& e) 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 Json::Value
@@ -416,6 +417,7 @@ STTx::checkBatchSingleSign(STObject const& batchSigner) const
{ {
Serializer msg; Serializer msg;
serializeBatch(msg, getFlags(), getBatchTransactionIDs()); serializeBatch(msg, getFlags(), getBatchTransactionIDs());
finishMultiSigningData(batchSigner.getAccountID(sfAccount), msg);
return singleSignHelper(batchSigner, msg.slice()); return singleSignHelper(batchSigner, msg.slice());
} }
@@ -488,7 +490,7 @@ multiSignHelper(
if (!validSig) if (!validSig)
return Unexpected( return Unexpected(
std::string("Invalid signature on account ") + toBase58(accountID) + std::string("Invalid signature on account ") + toBase58(accountID) +
errorWhat.value_or("") + "."); (errorWhat ? ": " + *errorWhat : "") + ".");
} }
// All signatures verified. // All signatures verified.
return {}; return {};

View File

@@ -175,12 +175,12 @@ Transactor::preflight1(PreflightContext const& ctx, std::uint32_t flagMask)
if (ctx.tx.getSeqProxy().isTicket() && ctx.tx.isFieldPresent(sfAccountTxnID)) if (ctx.tx.getSeqProxy().isTicket() && ctx.tx.isFieldPresent(sfAccountTxnID))
return temINVALID; return temINVALID;
if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatch)) if (ctx.tx.isFlag(tfInnerBatchTxn) && !ctx.rules.enabled(featureBatchV1_1))
return temINVALID_FLAG; return temINVALID_FLAG;
XRPL_ASSERT( XRPL_ASSERT(
ctx.tx.isFlag(tfInnerBatchTxn) == ctx.parentBatchId.has_value() || 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."); "Inner batch transaction must have a parent batch ID.");
return tesSUCCESS; return tesSUCCESS;
@@ -196,13 +196,13 @@ Transactor::preflight2(PreflightContext const& ctx)
return *ret; return *ret;
// It should be impossible for the InnerBatchTxn flag to be set without // It should be impossible for the InnerBatchTxn flag to be set without
// featureBatch being enabled // featureBatchV1_1 being enabled
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
!ctx.tx.isFlag(tfInnerBatchTxn) || ctx.rules.enabled(featureBatch), !ctx.tx.isFlag(tfInnerBatchTxn) || ctx.rules.enabled(featureBatchV1_1),
"xrpl::Transactor::preflight2", "xrpl::Transactor::preflight2",
"InnerBatch flag only set if feature enabled"); "InnerBatch flag only set if feature enabled");
// Skip signature check on batch inner transactions // 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; return tesSUCCESS;
// Do not add any checks after this point that are relevant for // Do not add any checks after this point that are relevant for
// batch inner transactions. They will be skipped. // batch inner transactions. They will be skipped.
@@ -647,7 +647,7 @@ Transactor::checkSign(
auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey); auto const pkSigner = sigObject.getFieldVL(sfSigningPubKey);
// Ignore signature check on batch inner transactions // 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 // Defensive Check: These values are also checked in Batch::preflight
if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() || if (sigObject.isFieldPresent(sfTxnSignature) || !pkSigner.empty() ||
@@ -699,50 +699,6 @@ Transactor::checkSign(PreclaimContext const& ctx)
return checkSign(ctx.view, ctx.flags, ctx.parentBatchId, idAccount, ctx.tx, ctx.j); 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 NotTEC
Transactor::checkSingleSign( Transactor::checkSingleSign(
ReadView const& view, ReadView const& view,

View File

@@ -24,29 +24,12 @@ checkValidity(HashRouter& router, STTx const& tx, Rules const& rules)
auto const flags = router.getFlags(id); auto const flags = router.getFlags(id);
// Ignore signature check on batch inner transactions // 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 // Defensive Check: These values are also checked in Batch::preflight
if (tx.isFieldPresent(sfTxnSignature) || !tx.getSigningPubKey().empty() || if (tx.isFieldPresent(sfTxnSignature) || !tx.getSigningPubKey().empty() ||
tx.isFieldPresent(sfSigners)) tx.isFieldPresent(sfSigners))
return {Validity::SigBad, "Malformed: Invalid inner batch transaction."}; 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)) if (any(flags & SF_SIGBAD))

View File

@@ -107,7 +107,18 @@ Batch::calculateBaseFee(ReadView const& view, STTx const& tx)
if (signer.isFieldPresent(sfTxnSignature)) if (signer.isFieldPresent(sfTxnSignature))
signerCount += 1; signerCount += 1;
else if (signer.isFieldPresent(sfSigners)) 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();
}
} }
} }
@@ -205,6 +216,14 @@ Batch::preflight(PreflightContext const& ctx)
return temARRAY_TOO_LARGE; 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 // Validation Inner Batch Txns
std::unordered_set<uint256> uniqueHashes; std::unordered_set<uint256> uniqueHashes;
std::unordered_map<AccountID, std::unordered_set<std::uint32_t>> accountSeqTicket; std::unordered_map<AccountID, std::unordered_set<std::uint32_t>> accountSeqTicket;
@@ -426,7 +445,7 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
if (requiredSigners.erase(signerAccount) == 0) if (requiredSigners.erase(signerAccount) == 0)
{ {
JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: " JLOG(ctx.j.debug()) << "BatchTrace[" << parentBatchId << "]: "
<< "no account signature for inner txn."; << "extra signer provided: " << signerAccount;
return temBAD_SIGNER; return temBAD_SIGNER;
} }
} }
@@ -451,6 +470,54 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
return tesSUCCESS; 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;
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. * @brief Checks the validity of signatures for a batch transaction.
* *
@@ -459,7 +526,7 @@ Batch::preflightSigValidated(PreflightContext const& ctx)
* corresponding error code. * corresponding error code.
* *
* Next, it verifies the batch-specific signature requirements by calling * 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. * corresponding error code.
* *
* If both checks succeed, the function returns tesSUCCESS. * If both checks succeed, the function returns tesSUCCESS.
@@ -474,8 +541,11 @@ Batch::checkSign(PreclaimContext const& ctx)
if (auto ret = Transactor::checkSign(ctx); !isTesSuccess(ret)) if (auto ret = Transactor::checkSign(ctx); !isTesSuccess(ret))
return ret; return ret;
if (auto ret = Transactor::checkBatchSign(ctx); !isTesSuccess(ret)) if (ctx.tx.isFieldPresent(sfBatchSigners))
return ret; {
if (auto ret = checkBatchSign(ctx); !isTesSuccess(ret))
return ret;
}
return tesSUCCESS; return tesSUCCESS;
} }

View File

@@ -26,7 +26,7 @@ LoanSet::preflight(PreflightContext const& ctx)
auto const& tx = ctx.tx; auto const& tx = ctx.tx;
// Special case for Batch inner transactions // 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)) !tx.isFieldPresent(sfCounterparty))
{ {
auto const parentBatchId = ctx.parentBatchId.value_or(uint256{0}); auto const parentBatchId = ctx.parentBatchId.value_or(uint256{0});

View File

@@ -141,14 +141,11 @@ class Batch_test : public beast::unit_test::suite
using namespace test::jtx; using namespace test::jtx;
using namespace std::literals; using namespace std::literals;
bool const withInnerSigFix = features[fixBatchInnerSigs];
for (bool const withBatch : {true, false}) for (bool const withBatch : {true, false})
{ {
testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled") testcase << "enabled: Batch " << (withBatch ? "enabled" : "disabled");
<< ", Inner Sig Fix: " << (withInnerSigFix ? "enabled" : "disabled");
auto const amend = withBatch ? features : features - featureBatch; auto const amend = withBatch ? features : features - featureBatchV1_1;
test::jtx::Env env{*this, amend}; test::jtx::Env env{*this, amend};
@@ -553,6 +550,7 @@ class Batch_test : public beast::unit_test::suite
Serializer msg; Serializer msg;
serializeBatch(msg, tfAllOrNothing, jt.stx->getBatchTransactionIDs()); serializeBatch(msg, tfAllOrNothing, jt.stx->getBatchTransactionIDs());
finishMultiSigningData(bob.id(), msg);
auto const sig = xrpl::sign(bob.pk(), bob.sk(), msg.slice()); auto const sig = xrpl::sign(bob.pk(), bob.sk(), msg.slice());
jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfAccount.jsonName] = jt.jv[sfBatchSigners.jsonName][0u][sfBatchSigner.jsonName][sfAccount.jsonName] =
bob.human(); bob.human();
@@ -1405,7 +1403,7 @@ class Batch_test : public beast::unit_test::suite
env.close(); env.close();
} }
// temARRAY_TOO_LARGE: Batch: signers array exceeds 8 entries. // temARRAY_TOO_LARGE: Batch preflight: signers array exceeds 8 entries.
{ {
test::jtx::Env env{*this, features}; test::jtx::Env env{*this, features};
@@ -2191,22 +2189,16 @@ class Batch_test : public beast::unit_test::suite
void void
doTestInnerSubmitRPC(FeatureBitset features, bool withBatch) doTestInnerSubmitRPC(FeatureBitset features, bool withBatch)
{ {
bool const withInnerSigFix = features[fixBatchInnerSigs]; std::string const testName =
std::string("inner submit rpc: batch ") + (withBatch ? "enabled" : "disabled") + ": ";
std::string const testName = [&]() { auto const amend = withBatch ? features : features - featureBatchV1_1;
std::stringstream ss;
ss << "inner submit rpc: batch " << (withBatch ? "enabled" : "disabled")
<< ", inner sig fix: " << (withInnerSigFix ? "enabled" : "disabled") << ": ";
return ss.str();
}();
auto const amend = withBatch ? features : features - featureBatch;
using namespace test::jtx; using namespace test::jtx;
using namespace std::literals; using namespace std::literals;
test::jtx::Env env{*this, amend}; test::jtx::Env env{*this, amend};
if (!BEAST_EXPECT(amend[featureBatch] == withBatch)) if (!BEAST_EXPECT(amend[featureBatchV1_1] == withBatch))
return; return;
auto const alice = Account("alice"); auto const alice = Account("alice");
@@ -2328,8 +2320,7 @@ class Batch_test : public beast::unit_test::suite
s.slice(), s.slice(),
__LINE__, __LINE__,
"fails local checks: Empty SigningPubKey.", "fails local checks: Empty SigningPubKey.",
"fails local checks: Empty SigningPubKey.", "fails local checks: Empty SigningPubKey.");
withBatch && !withInnerSigFix);
} }
// Invalid RPC Submission: tfInnerBatchTxn pseudo-transaction // Invalid RPC Submission: tfInnerBatchTxn pseudo-transaction
@@ -2340,7 +2331,7 @@ class Batch_test : public beast::unit_test::suite
{ {
STTx amendTx(ttAMENDMENT, [seq = env.closed()->header().seq + 1](auto& obj) { STTx amendTx(ttAMENDMENT, [seq = env.closed()->header().seq + 1](auto& obj) {
obj.setAccountID(sfAccount, AccountID()); obj.setAccountID(sfAccount, AccountID());
obj.setFieldH256(sfAmendment, fixBatchInnerSigs); obj.setFieldH256(sfAmendment, featureBatchV1_1);
obj.setFieldU32(sfLedgerSequence, seq); obj.setFieldU32(sfLedgerSequence, seq);
obj.setFieldU32(sfFlags, tfInnerBatchTxn); obj.setFieldU32(sfFlags, tfInnerBatchTxn);
}); });
@@ -2352,8 +2343,7 @@ class Batch_test : public beast::unit_test::suite
"Pseudo-transaction", "Pseudo-transaction",
s.slice(), s.slice(),
__LINE__, __LINE__,
withInnerSigFix ? "fails local checks: Empty SigningPubKey." "fails local checks: Empty SigningPubKey.",
: "fails local checks: Cannot submit pseudo transactions.",
"fails local checks: Empty SigningPubKey."); "fails local checks: Empty SigningPubKey.");
} }
} }
@@ -2414,6 +2404,53 @@ class Batch_test : public beast::unit_test::suite
BEAST_EXPECT(env.balance(bob) == XRP(1000)); BEAST_EXPECT(env.balance(bob) == XRP(1000));
} }
void
testCheckAllSignatures(FeatureBitset features)
{
testcase("check all signatures");
using namespace test::jtx;
using namespace std::literals;
// Verifies that checkBatchSign validates all signers even when an
// unfunded account (signed with its master key) appears first in the
// sorted signer list. A funded account with an invalid signature must
// still be rejected with tefBAD_AUTH.
test::jtx::Env env{*this, features};
auto const alice = Account("alice");
// "aaa" sorts before other accounts alphabetically, ensuring the
// unfunded account is checked first in the sorted signer list
auto const unfunded = Account("aaa");
auto const carol = Account("carol");
env.fund(XRP(10000), alice, carol);
env.close();
// Verify sort order: unfunded.id() < carol.id()
BEAST_EXPECT(unfunded.id() < carol.id());
auto const seq = env.seq(alice);
auto const ledSeq = env.current()->seq();
auto const batchFee = batch::calcBatchFee(env, 2, 3);
// The batch includes:
// 1. alice pays unfunded (to create unfunded's account)
// 2. unfunded does a noop (signed by unfunded's master key - valid)
// 3. carol pays alice (signed by alice's key - INVALID since alice is
// not carol's regular key)
//
// checkBatchSign must validate all signers regardless of order.
// This must fail with tefBAD_AUTH.
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
batch::inner(pay(alice, unfunded, XRP(100)), seq + 1),
batch::inner(noop(unfunded), ledSeq),
batch::inner(pay(carol, alice, XRP(1000)), env.seq(carol)),
batch::sig(unfunded, Reg{carol, alice}),
ter(tefBAD_AUTH));
env.close();
}
void void
testAccountSet(FeatureBitset features) testAccountSet(FeatureBitset features)
{ {
@@ -4331,6 +4368,7 @@ class Batch_test : public beast::unit_test::suite
testIndependent(features); testIndependent(features);
testInnerSubmitRPC(features); testInnerSubmitRPC(features);
testAccountActivation(features); testAccountActivation(features);
testCheckAllSignatures(features);
testAccountSet(features); testAccountSet(features);
testAccountDelete(features); testAccountDelete(features);
testLoan(features); testLoan(features);
@@ -4356,7 +4394,6 @@ public:
{ {
using namespace test::jtx; using namespace test::jtx;
auto const sa = testable_amendments(); auto const sa = testable_amendments();
testWithFeats(sa - fixBatchInnerSigs);
testWithFeats(sa); testWithFeats(sa);
} }
}; };

View File

@@ -74,6 +74,7 @@ sig::operator()(Env& env, JTx& jt) const
Serializer msg; Serializer msg;
serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs()); serializeBatch(msg, stx.getFlags(), stx.getBatchTransactionIDs());
finishMultiSigningData(e.acct.id(), msg);
auto const sig = xrpl::sign(*publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice()); auto const sig = xrpl::sign(*publicKeyType(e.sig.pk().slice()), e.sig.sk(), msg.slice());
jo[sfTxnSignature.getJsonName()] = strHex(Slice{sig.data(), sig.size()}); jo[sfTxnSignature.getJsonName()] = strHex(Slice{sig.data(), sig.size()});
} }

View File

@@ -116,7 +116,7 @@ class Feature_test : public beast::unit_test::suite
// or removed, swap out for any other feature. // or removed, swap out for any other feature.
BEAST_EXPECT( BEAST_EXPECT(
featureToName(fixRemoveNFTokenAutoTrustLine) == "fixRemoveNFTokenAutoTrustLine"); featureToName(fixRemoveNFTokenAutoTrustLine) == "fixRemoveNFTokenAutoTrustLine");
BEAST_EXPECT(featureToName(featureBatch) == "Batch"); BEAST_EXPECT(featureToName(featureBatchV1_1) == "BatchV1_1");
BEAST_EXPECT(featureToName(featureDID) == "DID"); BEAST_EXPECT(featureToName(featureDID) == "DID");
BEAST_EXPECT(featureToName(fixIncludeKeyletFields) == "fixIncludeKeyletFields"); BEAST_EXPECT(featureToName(fixIncludeKeyletFields) == "fixIncludeKeyletFields");
BEAST_EXPECT(featureToName(featureTokenEscrow) == "TokenEscrow"); BEAST_EXPECT(featureToName(featureTokenEscrow) == "TokenEscrow");

View File

@@ -1119,7 +1119,8 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
} }
// Enforce Network bar for batch txn // Enforce Network bar for batch txn
if (iTrans->isFlag(tfInnerBatchTxn) && m_ledgerMaster.getValidatedRules().enabled(featureBatch)) if (iTrans->isFlag(tfInnerBatchTxn) &&
m_ledgerMaster.getValidatedRules().enabled(featureBatchV1_1))
{ {
JLOG(m_journal.error()) << "Submitted transaction invalid: tfInnerBatchTxn flag present."; JLOG(m_journal.error()) << "Submitted transaction invalid: tfInnerBatchTxn flag present.";
return; return;
@@ -1185,7 +1186,7 @@ NetworkOPsImp::preProcessTransaction(std::shared_ptr<Transaction>& transaction)
// under no circumstances will we ever accept an inner txn within a batch // under no circumstances will we ever accept an inner txn within a batch
// txn from the network. // txn from the network.
auto const sttx = *transaction->getSTransaction(); auto const sttx = *transaction->getSTransaction();
if (sttx.isFlag(tfInnerBatchTxn) && view->rules().enabled(featureBatch)) if (sttx.isFlag(tfInnerBatchTxn) && view->rules().enabled(featureBatchV1_1))
{ {
transaction->setStatus(INVALID); transaction->setStatus(INVALID);
transaction->setResult(temINVALID_FLAG); transaction->setResult(temINVALID_FLAG);

View File

@@ -1291,7 +1291,7 @@ PeerImp::handleTransaction(
// Charge strongly for attempting to relay a txn with tfInnerBatchTxn // Charge strongly for attempting to relay a txn with tfInnerBatchTxn
// LCOV_EXCL_START // LCOV_EXCL_START
/* /*
There is no need to check whether the featureBatch amendment is There is no need to check whether the featureBatchV1_1 amendment is
enabled. enabled.
* If the `tfInnerBatchTxn` flag is set, and the amendment is * If the `tfInnerBatchTxn` flag is set, and the amendment is
@@ -2740,7 +2740,7 @@ PeerImp::checkTransaction(
// charge strongly for relaying batch txns // charge strongly for relaying batch txns
// LCOV_EXCL_START // LCOV_EXCL_START
/* /*
There is no need to check whether the featureBatch amendment is There is no need to check whether the featureBatchV1_1 amendment is
enabled. enabled.
* If the `tfInnerBatchTxn` flag is set, and the amendment is * If the `tfInnerBatchTxn` flag is set, and the amendment is