Merge remote-tracking branch 'XRPLF/develop' into ximinez/directory

* XRPLF/develop:
  refactor: Retire InnerObjTemplate fix (7368)
  fix: Disable AMM creation with Vault shares (7666)
  test: Add tests for TMProofPathResponse and TMReplayDeltaResponse invalid hash/key sizes (7593)
  ci: [DEPENDABOT] bump actions/setup-python from 6.2.0 to 6.3.0 (7657)
  build: Don't reuse binaries between different C++ versions (7681)
  chore: Update pre-commit hooks && actions (7686)
  feat: Add an invariant to ensure object deletion also deletes its pseudo-account (7445)
  feat: Add Batch (XLS-56) V1_1 (6446)
  feat: Introduce lending 1.1 amendment and add `MemoData` field to `VaultDelete` transaction (6324)
This commit is contained in:
Ed Hennis
2026-07-01 16:10:38 -04:00
66 changed files with 2554 additions and 1242 deletions

View File

@@ -19,12 +19,14 @@ enum class HashRouterFlags : std::uint16_t {
HELD = 0x08, // Held by LedgerMaster after potential processing failure
TRUSTED = 0x10, // Comes from a trusted source
// Private flags (used internally in apply.cpp)
// Do not attempt to read, set, or reuse.
// Private flags. Each group is owned by one file; do not read, set, or
// reuse a flag outside the file noted.
// Used in apply.cpp
PRIVATE1 = 0x0100,
PRIVATE2 = 0x0200,
PRIVATE3 = 0x0400,
PRIVATE4 = 0x0800,
// Used in EscrowFinish.cpp
PRIVATE5 = 0x1000,
PRIVATE6 = 0x2000
};

View File

@@ -28,7 +28,7 @@ inline constexpr struct OpenLedgerT
/** Batch view construction tag.
Views constructed with this tag are part of a stack of views
used during batch transaction applied.
used during batch transaction application.
*/
inline constexpr struct BatchViewT
{

View File

@@ -23,6 +23,15 @@ namespace xrpl {
[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue);
/** Returns true if @p account's MPToken for @p mptIssue carries the
* individual-lock flag (lsfMPTLocked).
*
* @warning This checks only the raw per-holder lock bit. It does **not**
* perform the transitive vault pseudo-account check: if @p mptIssue is a
* vault share whose underlying asset is frozen, this function returns false.
* Call @ref isFrozen instead when determining whether an account may send or
* receive tokens — it combines isIndividualFrozen, isGlobalFrozen, and
* isVaultPseudoAccountFrozen into a single complete check. */
[[nodiscard]] bool
isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);

View File

@@ -144,19 +144,18 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
*
* Otherwise checks, in order:
* 1. If the asset is globally frozen the remaining checks are redundant.
* 2. For MPT shares: The pseudo-account's vault share must not be transitively frozen via its
* underlying asset.
* 3. The pseudo-account's trustline / MPToken must not be frozen for sending.
* 4. Skipped when submitter == dst (self-withdrawal); a regular freeze should not prevent
* recovering one's own funds.
* 5. The destination must not be deep-frozen (cannot receive under any circumstance).
* 2. The pseudo-account's trustline / MPToken must not be individually frozen for sending.
* 3. The submitter's trustline / MPToken must not be individually frozen. Skipped when
* submitter == dst (self-withdrawal) so a regular freeze does not prevent recovering one's own
* funds. (Enforced as defensive code; no current caller exercises a frozen submitter ≠ dst.)
* 4. The destination must not be deep-frozen.
*
* For IOUs a regular individual freeze on the withdrawer does NOT block self-withdrawal; only deep
* freeze does. For MPTs "locked" is equivalent to deep-frozen, so locked MPT holders are always
* For IOUs a regular individual freeze on the submitter does NOT block self-withdrawal; only deep
* freeze does. For MPTs "locked" is equivalent to deep-frozen, so locked MPT holders are always
* blocked.
*
* @param view Ledger view to read freeze state from.
* @param srcAcct Pseudo-account the funds are withdrawn from (sender).
* @param pseudoAcct Pseudo-account the funds are withdrawn from (sender).
* @param submitterAcct Account that submitted the withdrawal transaction.
* @param dstAcct Account receiving the withdrawn funds.
* @param asset Asset being withdrawn.
@@ -166,7 +165,7 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
[[nodiscard]] TER
checkWithdrawFreeze(
ReadView const& view,
AccountID const& srcAcct,
AccountID const& pseudoAcct,
AccountID const& submitterAcct,
AccountID const& dstAcct,
Asset const& asset);
@@ -175,20 +174,17 @@ checkWithdrawFreeze(
* Checks freeze compliance for depositing an asset into a pseudo-account (e.g. Vault, AMM,
* LoanBroker).
*
*
* Checks, in order:
* 1. If the asset is globally frozen the remaining checks are redundant.
* 2. For MPT shares: the pseudo-account's vault share must not be transitively frozen via its
* underlying asset (returns tecLOCKED).
* 3. The depositor must not be individually frozen. Skipped when srcAcct is the asset issuer,
* since the issuer can always send its own asset.
* 4. The pseudo-account must not be individually frozen for the asset. Unlike regular accounts,
* 2. The depositor must not be individually frozen for the asset. Skipped when srcAcct is the
* asset issuer, since the issuer can always send its own asset.
* 3. The pseudo-account must not be individually frozen for the asset. Unlike regular accounts,
* pseudo-accounts cannot receive deposits under a regular freeze because the deposited funds
* could not later be withdrawn.
*
* @param view Ledger view to read freeze state from.
* @param srcAcct Depositor sending the funds.
* @param dstAcct Pseudo-account receiving the deposit.
* @param pseudoAcct Pseudo-account receiving the deposit.
* @param asset Asset being deposited.
* @return tesSUCCESS if the deposit is permitted, otherwise a freeze result
* (tecFROZEN for IOUs, tecLOCKED for MPTs).
@@ -197,7 +193,7 @@ checkWithdrawFreeze(
checkDepositFreeze(
ReadView const& view,
AccountID const& srcAcct,
AccountID const& dstAcct,
AccountID const& pseudoAcct,
Asset const& asset);
//------------------------------------------------------------------------------

View File

@@ -1,5 +1,6 @@
#pragma once
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/STVector256.h>
#include <xrpl/protocol/Serializer.h>
@@ -7,9 +8,16 @@
namespace xrpl {
inline void
serializeBatch(Serializer& msg, std::uint32_t const& flags, std::vector<uint256> const& txids)
serializeBatch(
Serializer& msg,
AccountID const& outerAccount,
std::uint32_t outerSeqValue,
std::uint32_t const& flags,
std::vector<uint256> const& txids)
{
msg.add32(HashPrefix::Batch);
msg.addBitString(outerAccount);
msg.add32(outerSeqValue);
msg.add32(flags);
msg.add32(std::uint32_t(txids.size()));
for (auto const& txid : txids)

View File

@@ -311,6 +311,9 @@ constexpr std::size_t kPermissionMaxSize = 10;
/** The maximum number of transactions that can be in a batch. */
constexpr std::size_t kMaxBatchTxCount = 8;
/** The maximum number of batch signers. */
constexpr std::size_t kMaxBatchSigners = kMaxBatchTxCount * 3;
/** Length of a secp256k1 scalar in bytes. */
constexpr std::size_t kEcScalarLength = kMPT_SCALAR_SIZE;

View File

@@ -217,6 +217,11 @@ public:
[[nodiscard]] AccountID
getAccountID(SField const& field) const;
/** The account responsible for the fee and authorization: the delegate when
sfDelegate is present, otherwise the account. */
[[nodiscard]] AccountID
getFeePayer() const;
[[nodiscard]] Blob
getFieldVL(SField const& field) const;
[[nodiscard]] STAmount const&

View File

@@ -12,6 +12,7 @@
#include <expected>
#include <functional>
#include <optional>
namespace xrpl {
@@ -51,51 +52,48 @@ public:
STTx(TxType type, std::function<void(STObject&)> assembler);
// STObject functions.
SerializedTypeID
[[nodiscard]] SerializedTypeID
getSType() const override;
std::string
[[nodiscard]] std::string
getFullText() const override;
// Outer transaction functions / signature functions.
static Blob
getSignature(STObject const& sigObject);
Blob
[[nodiscard]] Blob
getSignature() const
{
return getSignature(*this);
}
uint256
[[nodiscard]] uint256
getSigningHash() const;
TxType
[[nodiscard]] TxType
getTxnType() const;
Blob
[[nodiscard]] Blob
getSigningPubKey() const;
SeqProxy
[[nodiscard]] SeqProxy
getSeqProxy() const;
/** Returns the first non-zero value of (Sequence, TicketSequence). */
std::uint32_t
[[nodiscard]] std::uint32_t
getSeqValue() const;
AccountID
getFeePayer() const;
boost::container::flat_set<AccountID>
[[nodiscard]] boost::container::flat_set<AccountID>
getMentionedAccounts() const;
uint256
[[nodiscard]] uint256
getTransactionID() const;
json::Value
[[nodiscard]] json::Value
getJson(JsonOptions options) const override;
json::Value
[[nodiscard]] json::Value
getJson(JsonOptions options, bool binary) const;
void
@@ -108,27 +106,27 @@ public:
@param rules The current ledger rules.
@return `true` if valid signature. If invalid, the error message string.
*/
std::expected<void, std::string>
[[nodiscard]] std::expected<void, std::string>
checkSign(Rules const& rules) const;
std::expected<void, std::string>
[[nodiscard]] std::expected<void, std::string>
checkBatchSign(Rules const& rules) const;
// SQL Functions with metadata.
static std::string const&
getMetaSQLInsertReplaceHeader();
std::string
[[nodiscard]] std::string
getMetaSQL(std::uint32_t inLedger, std::string const& escapedMetaData) const;
std::string
[[nodiscard]] std::string
getMetaSQL(
Serializer rawTxn,
std::uint32_t inLedger,
TxnSql status,
std::string const& escapedMetaData) const;
std::vector<uint256> const&
[[nodiscard]] std::vector<uint256> const&
getBatchTransactionIDs() const;
private:
@@ -138,28 +136,31 @@ private:
Will be *this more often than not.
@return `true` if valid signature. If invalid, the error message string.
*/
std::expected<void, std::string>
[[nodiscard]] std::expected<void, std::string>
checkSign(Rules const& rules, STObject const& sigObject) const;
std::expected<void, std::string>
[[nodiscard]] std::expected<void, std::string>
checkSingleSign(STObject const& sigObject) const;
std::expected<void, std::string>
[[nodiscard]] std::expected<void, std::string>
checkMultiSign(Rules const& rules, STObject const& sigObject) const;
std::expected<void, std::string>
[[nodiscard]] std::expected<void, std::string>
checkBatchSingleSign(STObject const& batchSigner) const;
std::expected<void, std::string>
[[nodiscard]] std::expected<void, std::string>
checkBatchMultiSign(STObject const& batchSigner, Rules const& rules) const;
void
buildBatchTxnIds();
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> batchTxnIds_;
std::optional<std::vector<uint256>> batchTxnIds_;
};
bool

View File

@@ -175,6 +175,8 @@ enum TEFcodes : TERUnderlyingType {
tefNO_TICKET,
tefNFTOKEN_IS_NOT_TRANSFERABLE,
tefINVALID_LEDGER_FIX_TYPE,
tefNO_DST_PARTIAL,
tefBAD_PATH_COUNT,
};
//------------------------------------------------------------------------------

View File

@@ -16,12 +16,13 @@
// Keep it sorted in reverse chronological order.
XRPL_FEATURE(DefragDirectories, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(ConfidentialTransfer, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(BatchV1_1, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocolV1_1, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(ConfidentialTransfer, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_3_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_2_0, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(MPTokensV2, Supported::No, VoteBehavior::DefaultNo)
XRPL_FIX (Cleanup3_1_3, Supported::Yes, VoteBehavior::DefaultYes)
XRPL_FIX (BatchInnerSigs, Supported::No, VoteBehavior::DefaultNo)
XRPL_FEATURE(LendingProtocol, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionDelegationV1_1, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (DirectoryLimit, Supported::Yes, VoteBehavior::DefaultNo)
@@ -35,7 +36,6 @@ XRPL_FEATURE(TokenEscrow, Supported::Yes, VoteBehavior::DefaultN
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
@@ -59,7 +59,6 @@ XRPL_FIX (XChainRewardRounding, Supported::Yes, VoteBehavior::DefaultNo
XRPL_FIX (EmptyDID, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PriceOracle, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (AMMOverflowOffer, Supported::Yes, VoteBehavior::DefaultYes)
XRPL_FIX (InnerObjTemplate, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (FillOrKill, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(DID, Supported::Yes, VoteBehavior::DefaultNo)
XRPL_FIX (DisallowIncomingV1, Supported::Yes, VoteBehavior::DefaultNo)
@@ -102,6 +101,7 @@ XRPL_RETIRE_FIX(1623)
XRPL_RETIRE_FIX(1781)
XRPL_RETIRE_FIX(AmendmentMajorityCalc)
XRPL_RETIRE_FIX(CheckThreading)
XRPL_RETIRE_FIX(InnerObjTemplate)
XRPL_RETIRE_FIX(MasterKeyAsRegularKey)
XRPL_RETIRE_FIX(NonFungibleTokensV1_2)
XRPL_RETIRE_FIX(NFTokenRemint)

View File

@@ -889,6 +889,7 @@ TRANSACTION(ttVAULT_DELETE, 67, VaultDelete,
MustDeleteAcct | DestroyMptIssuance | MustModifyVault,
({
{sfVaultID, SoeRequired},
{sfMemoData, SoeOptional},
}))
/** This transaction trades assets for shares with a vault. */
@@ -939,7 +940,7 @@ TRANSACTION(ttVAULT_CLAWBACK, 70, VaultClawback,
#endif
TRANSACTION(ttBATCH, 71, Batch,
Delegation::NotDelegable,
featureBatch,
featureBatchV1_1,
NoPriv,
({
{sfRawTransactions, SoeRequired},

View File

@@ -20,7 +20,7 @@ class BatchBuilder;
*
* Type: ttBATCH (71)
* Delegable: Delegation::NotDelegable
* Amendment: featureBatch
* Amendment: featureBatchV1_1
* Privileges: NoPriv
*
* Immutable wrapper around STTx providing type-safe field access.

View File

@@ -57,6 +57,32 @@ public:
{
return this->tx_->at(sfVaultID);
}
/**
* @brief Get sfMemoData (SoeOptional)
* @return The field value, or std::nullopt if not present.
*/
[[nodiscard]]
protocol_autogen::Optional<SF_VL::type::value_type>
getMemoData() const
{
if (hasMemoData())
{
return this->tx_->at(sfMemoData);
}
return std::nullopt;
}
/**
* @brief Check if sfMemoData is present.
* @return True if the field is present, false otherwise.
*/
[[nodiscard]]
bool
hasMemoData() const
{
return this->tx_->isFieldPresent(sfMemoData);
}
};
/**
@@ -112,6 +138,17 @@ public:
return *this;
}
/**
* @brief Set sfMemoData (SoeOptional)
* @return Reference to this builder for method chaining.
*/
VaultDeleteBuilder&
setMemoData(std::decay_t<typename SF_VL::type::value_type> const& value)
{
object_[sfMemoData] = value;
return *this;
}
/**
* @brief Build and return the VaultDelete wrapper.
* @param publicKey The public key for signing.

View File

@@ -24,6 +24,10 @@ public:
ApplyFlags flags,
beast::Journal journal = beast::Journal{beast::Journal::getNullSink()});
// Convenience constructor used only by tests that build an ApplyContext
// directly (e.g. invariant checks). Production always uses the parentBatchId
// constructor above; this one fixes parentBatchId to std::nullopt and so is
// never valid for a batch inner (hence the TapBatch assert).
explicit ApplyContext(
ServiceRegistry& registry,
OpenView& base,
@@ -124,7 +128,7 @@ private:
ApplyFlags flags_;
std::optional<ApplyViewImpl> view_;
// The ID of the batch transaction we are executing under, if seated.
// The ID of the batch transaction we are executing under, if set.
std::optional<uint256 const> parentBatchId_;
};

View File

@@ -180,9 +180,6 @@ public:
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);
@@ -373,7 +370,12 @@ protected:
std::optional<uint256 const> const& parentBatchId,
AccountID const& idAccount,
STObject const& sigObject,
beast::Journal const j);
beast::Journal const j,
// A batch may carry an inner from an account that an earlier inner
// creates, so the signer account need not exist yet; when it does not,
// only its own master key may authorize it. Normal transactions require
// the account to already exist.
bool permitUncreatedAccount = false);
// Base class always returns true
static bool
@@ -413,25 +415,8 @@ protected:
std::optional<T> value,
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});
private:
static NotTEC
checkPermission(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType>& heldGranularPermissions);
std::pair<TER, XRPAmount>
reset(XRPAmount fee);
TER
consumeSeqProxy(SLE::pointer const& sleAccount);
TER
payFee();
std::tuple<TER, XRPAmount, bool>
processPersistentChanges(TER result, XRPAmount fee);
// Signature-authorization helpers. protected so the Batch transactor can
// reuse them when validating each BatchSigner in Batch::checkBatchSign.
static NotTEC
checkSingleSign(
ReadView const& view,
@@ -448,6 +433,24 @@ private:
STObject const& sigObject,
beast::Journal const j);
private:
static NotTEC
checkPermission(
ReadView const& view,
STTx const& tx,
std::unordered_set<GranularPermissionType>& heldGranularPermissions);
std::pair<TER, XRPAmount>
reset(XRPAmount fee);
TER
consumeSeqProxy(SLE::pointer const& sleAccount);
TER
payFee();
std::tuple<TER, XRPAmount, bool>
processPersistentChanges(TER result, XRPAmount fee);
void trapTransaction(uint256) const;
/** Performs early sanity checks on the account and fee fields.

View File

@@ -375,16 +375,35 @@ public:
*/
class ValidAmounts
{
std::vector<std::shared_ptr<SLE const>> afterEntries_;
std::vector<SLE::const_pointer> afterEntries_;
public:
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool, SLE::const_ref, SLE::const_ref);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/*
* Verify that when an object with an associated pseudo-account is deleted,
* its pseudo-account is also deleted.
*
* The reverse (pseudo-account deleted → object deleted) is enforced by
* AccountRootsDeletedClean via getPseudoAccountFields().
*/
class ObjectHasPseudoAccount
{
public:
void
visitEntry(bool, SLE::const_ref, SLE::const_ref);
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
private:
std::vector<SLE::const_pointer> deletedObjSles_;
};
// additional invariant checks can be declared above and then added to this
// tuple
using InvariantChecks = std::tuple<
@@ -416,7 +435,8 @@ using InvariantChecks = std::tuple<
ValidConfidentialMPToken,
ValidMPTPayment,
ValidAmounts,
ValidMPTTransfer>;
ValidMPTTransfer,
ObjectHasPseudoAccount>;
/**
* @brief get a tuple of all invariant checks

View File

@@ -1,7 +1,5 @@
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/tx/Transactor.h>
namespace xrpl {
@@ -61,6 +59,12 @@ public:
ttLOAN_MANAGE,
ttLOAN_PAY,
});
private:
// Skips signature verification for inner txns, so keep it private: it must
// only be reached through Batch::checkSign.
static NotTEC
checkBatchSign(PreclaimContext const& ctx);
};
} // namespace xrpl