Add Batch feature (XLS-56) (#5060)

- Specification: [XRPLF/XRPL-Standards 56](https://github.com/XRPLF/XRPL-Standards/blob/master/XLS-0056d-batch/README.md)
- Amendment: `Batch`
- Implements execution of multiple transactions within a single batch transaction with four execution modes: `tfAllOrNothing`, `tfOnlyOne`, `tfUntilFailure`, and `tfIndependent`.
- Enables atomic multi-party transactions where multiple accounts can participate in a single batch, with up to 8 inner transactions and 8 batch signers per batch transaction.
- Inner transactions use `tfInnerBatchTxn` flag with zero fees, no signature, and empty signing public key.
- Inner transactions are applied after the outer batch succeeds via the `applyBatchTransactions` function in apply.cpp.
- Network layer prevents relay of transactions with `tfInnerBatchTxn` flag - each peer applies inner transactions locally from the batch.
- Batch transactions are excluded from AccountDelegate permissions but inner transactions retain full delegation support.
- Metadata includes `ParentBatchID` linking inner transactions to their containing batch for traceability and auditing.
- Extended STTx with batch-specific signature verification methods and added protocol structures (`sfRawTransactions`, `sfBatchSigners`).
This commit is contained in:
Denis Angell
2025-05-23 21:53:53 +02:00
committed by GitHub
parent 40ce8a8833
commit 2a61aee562
69 changed files with 6400 additions and 744 deletions

View File

@@ -0,0 +1,37 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/STVector256.h>
#include <xrpl/protocol/Serializer.h>
namespace ripple {
inline void
serializeBatch(
Serializer& msg,
std::uint32_t const& flags,
std::vector<uint256> const& txids)
{
msg.add32(HashPrefix::batch);
msg.add32(flags);
msg.add32(std::uint32_t(txids.size()));
for (auto const& txid : txids)
msg.addBitString(txid);
}
} // namespace ripple

View File

@@ -88,6 +88,9 @@ enum class HashPrefix : std::uint32_t {
/** Credentials signature */
credential = detail::make_hash_prefix('C', 'R', 'D'),
/** Batch */
batch = detail::make_hash_prefix('B', 'C', 'H'),
};
template <class Hasher>

View File

@@ -169,6 +169,9 @@ std::size_t constexpr maxTrim = 25;
*/
std::size_t constexpr permissionMaxSize = 10;
/** The maximum number of transactions that can be in a batch. */
std::size_t constexpr maxBatchTxCount = 8;
} // namespace ripple
#endif

View File

@@ -125,10 +125,16 @@ public:
@return `true` if valid signature. If invalid, the error message string.
*/
enum class RequireFullyCanonicalSig : bool { no, yes };
Expected<void, std::string>
checkSign(RequireFullyCanonicalSig requireCanonicalSig, Rules const& rules)
const;
Expected<void, std::string>
checkBatchSign(
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules) const;
// SQL Functions with metadata.
static std::string const&
getMetaSQLInsertReplaceHeader();
@@ -144,6 +150,9 @@ public:
char status,
std::string const& escapedMetaData) const;
std::vector<uint256>
getBatchTransactionIDs() const;
private:
Expected<void, std::string>
checkSingleSign(RequireFullyCanonicalSig requireCanonicalSig) const;
@@ -153,12 +162,24 @@ private:
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules) const;
Expected<void, std::string>
checkBatchSingleSign(
STObject const& batchSigner,
RequireFullyCanonicalSig requireCanonicalSig) const;
Expected<void, std::string>
checkBatchMultiSign(
STObject const& batchSigner,
RequireFullyCanonicalSig requireCanonicalSig,
Rules const& rules) const;
STBase*
copy(std::size_t n, void* buf) const override;
STBase*
move(std::size_t n, void* buf) override;
friend class detail::STVar;
mutable std::vector<uint256> batch_txn_ids_;
};
bool

View File

@@ -139,8 +139,8 @@ enum TEMcodes : TERUnderlyingType {
temARRAY_EMPTY,
temARRAY_TOO_LARGE,
temBAD_TRANSFER_FEE,
temINVALID_INNER_BATCH,
};
//------------------------------------------------------------------------------
@@ -645,37 +645,37 @@ using TER = TERSubset<CanCvtToTER>;
//------------------------------------------------------------------------------
inline bool
isTelLocal(TER x)
isTelLocal(TER x) noexcept
{
return ((x) >= telLOCAL_ERROR && (x) < temMALFORMED);
return (x >= telLOCAL_ERROR && x < temMALFORMED);
}
inline bool
isTemMalformed(TER x)
isTemMalformed(TER x) noexcept
{
return ((x) >= temMALFORMED && (x) < tefFAILURE);
return (x >= temMALFORMED && x < tefFAILURE);
}
inline bool
isTefFailure(TER x)
isTefFailure(TER x) noexcept
{
return ((x) >= tefFAILURE && (x) < terRETRY);
return (x >= tefFAILURE && x < terRETRY);
}
inline bool
isTerRetry(TER x)
isTerRetry(TER x) noexcept
{
return ((x) >= terRETRY && (x) < tesSUCCESS);
return (x >= terRETRY && x < tesSUCCESS);
}
inline bool
isTesSuccess(TER x)
isTesSuccess(TER x) noexcept
{
return ((x) == tesSUCCESS);
return (x == tesSUCCESS);
}
inline bool
isTecClaim(TER x)
isTecClaim(TER x) noexcept
{
return ((x) >= tecCLAIM);
}

View File

@@ -58,7 +58,8 @@ namespace ripple {
// clang-format off
// Universal Transaction flags:
constexpr std::uint32_t tfFullyCanonicalSig = 0x80000000;
constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig;
constexpr std::uint32_t tfInnerBatchTxn = 0x40000000;
constexpr std::uint32_t tfUniversal = tfFullyCanonicalSig | tfInnerBatchTxn;
constexpr std::uint32_t tfUniversalMask = ~tfUniversal;
// AccountSet flags:
@@ -97,6 +98,7 @@ constexpr std::uint32_t tfPassive = 0x00010000;
constexpr std::uint32_t tfImmediateOrCancel = 0x00020000;
constexpr std::uint32_t tfFillOrKill = 0x00040000;
constexpr std::uint32_t tfSell = 0x00080000;
constexpr std::uint32_t tfOfferCreateMask =
~(tfUniversal | tfPassive | tfImmediateOrCancel | tfFillOrKill | tfSell);
@@ -239,6 +241,20 @@ constexpr std::uint32_t const tfVaultPrivate = 0x00010000;
static_assert(tfVaultPrivate == lsfVaultPrivate);
constexpr std::uint32_t const tfVaultShareNonTransferable = 0x00020000;
constexpr std::uint32_t const tfVaultCreateMask = ~(tfUniversal | tfVaultPrivate | tfVaultShareNonTransferable);
// Batch Flags:
constexpr std::uint32_t tfAllOrNothing = 0x00010000;
constexpr std::uint32_t tfOnlyOne = 0x00020000;
constexpr std::uint32_t tfUntilFailure = 0x00040000;
constexpr std::uint32_t tfIndependent = 0x00080000;
/**
* @note If nested Batch transactions are supported in the future, the tfInnerBatchTxn flag
* will need to be removed from this mask to allow Batch transaction to be inside
* the sfRawTransactions array.
*/
constexpr std::uint32_t const tfBatchMask =
~(tfUniversal | tfAllOrNothing | tfOnlyOne | tfUntilFailure | tfIndependent) | tfInnerBatchTxn;
// clang-format on
} // namespace ripple

View File

@@ -46,7 +46,10 @@ private:
CtorHelper);
public:
TxMeta(uint256 const& transactionID, std::uint32_t ledger);
TxMeta(
uint256 const& transactionID,
std::uint32_t ledger,
std::optional<uint256> parentBatchId = std::nullopt);
TxMeta(uint256 const& txID, std::uint32_t ledger, Blob const&);
TxMeta(uint256 const& txID, std::uint32_t ledger, std::string const&);
TxMeta(uint256 const& txID, std::uint32_t ledger, STObject const&);
@@ -130,6 +133,27 @@ public:
return static_cast<bool>(mDelivered);
}
void
setParentBatchId(uint256 const& parentBatchId)
{
mParentBatchId = parentBatchId;
}
uint256
getParentBatchId() const
{
XRPL_ASSERT(
hasParentBatchId(),
"ripple::TxMeta::getParentBatchId : non-null batch id");
return *mParentBatchId;
}
bool
hasParentBatchId() const
{
return static_cast<bool>(mParentBatchId);
}
private:
uint256 mTransactionID;
std::uint32_t mLedger;
@@ -137,6 +161,7 @@ private:
int mResult;
std::optional<STAmount> mDelivered;
std::optional<uint256> mParentBatchId;
STArray mNodes;
};

View File

@@ -32,6 +32,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(Batch, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(SingleAssetVault, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegation, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (PayChanCancelAfter, Supported::yes, VoteBehavior::DefaultNo)

View File

@@ -195,6 +195,7 @@ TYPED_SFIELD(sfHookNamespace, UINT256, 32)
TYPED_SFIELD(sfHookSetTxnID, UINT256, 33)
TYPED_SFIELD(sfDomainID, UINT256, 34)
TYPED_SFIELD(sfVaultID, UINT256, 35)
TYPED_SFIELD(sfParentBatchID, UINT256, 36)
// number (common)
TYPED_SFIELD(sfNumber, NUMBER, 1)
@@ -357,6 +358,8 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
UNTYPED_SFIELD(sfRawTransaction, OBJECT, 34)
UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35)
// array of objects (common)
// ARRAY/1 is reserved for end of array
@@ -388,3 +391,5 @@ UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26)
UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27)
UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28)
UNTYPED_SFIELD(sfPermissions, ARRAY, 29)
UNTYPED_SFIELD(sfRawTransactions, ARRAY, 30)
UNTYPED_SFIELD(sfBatchSigners, ARRAY, 31, SField::sMD_Default, SField::notSigning)

View File

@@ -514,6 +514,12 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback, Delegation::delegatable, ({
{sfAmount, soeOPTIONAL, soeMPTSupported},
}))
/** This transaction type batches together transactions. */
TRANSACTION(ttBATCH, 71, Batch, Delegation::notDelegatable, ({
{sfRawTransactions, soeREQUIRED},
{sfBatchSigners, soeOPTIONAL},
}))
/** This system-generated transaction type is used to update the status of the various amendments.
For details, see: https://xrpl.org/amendments.html
@@ -548,4 +554,3 @@ TRANSACTION(ttUNL_MODIFY, 102, UNLModify, Delegation::notDelegatable, ({
{sfLedgerSequence, soeREQUIRED},
{sfUNLModifyValidator, soeREQUIRED},
}))

View File

@@ -83,6 +83,8 @@ JSS(PriceDataSeries); // field.
JSS(PriceData); // field.
JSS(Provider); // field.
JSS(QuoteAsset); // in: Oracle.
JSS(RawTransaction); // in: Batch
JSS(RawTransactions); // in: Batch
JSS(SLE_hit_rate); // out: GetCounts.
JSS(Scale); // field.
JSS(SettleDelay); // in: TransactionSign