This commit is contained in:
Denis Angell
2026-05-14 06:57:17 +02:00
parent e635557235
commit 315d1fdb06
122 changed files with 10912 additions and 1388 deletions

View File

@@ -1,3 +1,15 @@
/** @file
* Transaction Engine Result (TER) code taxonomy for the XRP Ledger.
*
* Defines the six result-code enumerations (tel/tem/tef/ter/tes/tec),
* the strongly-typed `TERSubset<Trait>` wrapper that enforces which
* categories are permitted in a given context, the `NotTEC` and `TER`
* aliases, comparison operators, and lookup utilities.
*
* Every code value is part of the wire protocol: numeric values are
* stored in ledger metadata and consumed by `ripple-binary-codec`.
* @see https://xrpl.org/transaction-results.html
*/
#pragma once
// NOLINTBEGIN(readability-identifier-naming)
@@ -12,15 +24,29 @@
namespace xrpl {
// See https://xrpl.org/transaction-results.html
//
// "Transaction Engine Result"
// or Transaction ERror.
//
/** Underlying integer type shared by all TER code enumerations.
*
* Using a named typedef allows `TERSubset` to store a plain `int`
* without naming a specific enum, and lets `TERtoInt` overloads share
* a single return type that triggers the comparison-operator SFINAE.
*
* @see https://xrpl.org/transaction-results.html
*/
using TERUnderlyingType = int;
//------------------------------------------------------------------------------
/** Local-error result codes (range 399..300).
*
* A `tel` result means this node alone rejected the transaction; the
* decision is not propagated to the network. The transaction is not
* forwarded to peers and no fee check is performed. Common causes:
* fee below the local minimum, or path counts that exceed node-local
* limits. These codes are only valid during non-consensus processing.
*
* @note Numeric values are stable and encoded in `ripple-binary-codec`.
* Never renumber or remove existing enumerators.
*/
// Protocol-critical, mixed with custom TER wrapper type, hundreds of usages
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum TELcodes : TERUnderlyingType {
@@ -28,11 +54,6 @@ enum TELcodes : TERUnderlyingType {
// Exact numbers are used in ripple-binary-codec:
// https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json
// Use tokens.
// -399 .. -300: L Local error (transaction fee inadequate, exceeds local
// limit) Only valid during non-consensus processing. Implications:
// - Not forwarded
// - No fee check
telLOCAL_ERROR = -399,
telBAD_DOMAIN,
telBAD_PATH_COUNT,
@@ -54,6 +75,15 @@ enum TELcodes : TERUnderlyingType {
//------------------------------------------------------------------------------
/** Malformed-transaction result codes (range 299..200).
*
* A `tem` result means the transaction is structurally corrupt and
* cannot succeed in any possible ledger state. The transaction is
* rejected without being applied or forwarded, and no fee is charged.
*
* @note Numeric values are stable and encoded in `ripple-binary-codec`.
* Never renumber or remove existing enumerators.
*/
// Protocol-critical, mixed with custom TER wrapper type, hundreds of usages
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum TEMcodes : TERUnderlyingType {
@@ -61,15 +91,6 @@ enum TEMcodes : TERUnderlyingType {
// Exact numbers are used in ripple-binary-codec:
// https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json
// Use tokens.
// -299 .. -200: M Malformed (bad signature)
// Causes:
// - Transaction corrupt.
// Implications:
// - Not applied
// - Not forwarded
// - Reject
// - Cannot succeed in any imagined ledger.
temMALFORMED = -299,
temBAD_AMOUNT,
@@ -106,8 +127,8 @@ enum TEMcodes : TERUnderlyingType {
temCANNOT_PREAUTH_SELF,
temINVALID_COUNT,
temUNCERTAIN, // An internal intermediate result; should never be returned.
temUNKNOWN, // An internal intermediate result; should never be returned.
temUNCERTAIN, ///< Internal sentinel — in the process of determining a result; never returned to callers.
temUNKNOWN, ///< Internal sentinel — logic not yet implemented; never returned to callers.
temSEQ_AND_TICKET,
temBAD_NFTOKEN_TRANSFER_FEE,
@@ -132,6 +153,17 @@ enum TEMcodes : TERUnderlyingType {
//------------------------------------------------------------------------------
/** Failure result codes (range 199..100).
*
* A `tef` result means the transaction cannot be applied because of the
* current ledger state (e.g., sequence already used, bad signature, or
* an unexpected C++ exception). The transaction is not applied, not
* forwarded, and no fee is charged. Unlike `tem`, a `tef` transaction
* could theoretically succeed in a different ledger state.
*
* @note Numeric values are stable and encoded in `ripple-binary-codec`.
* Never renumber or remove existing enumerators.
*/
// Protocol-critical, mixed with custom TER wrapper type, hundreds of usages
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum TEFcodes : TERUnderlyingType {
@@ -139,19 +171,6 @@ enum TEFcodes : TERUnderlyingType {
// Exact numbers are used in ripple-binary-codec:
// https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json
// Use tokens.
// -199 .. -100: F
// Failure (sequence number previously used)
//
// Causes:
// - Transaction cannot succeed because of ledger state.
// - Unexpected ledger state.
// - C++ exception.
//
// Implications:
// - Not applied
// - Not forwarded
// - Could succeed in an imagined ledger.
tefFAILURE = -199,
tefALREADY,
tefBAD_ADD_AUTH,
@@ -178,6 +197,18 @@ enum TEFcodes : TERUnderlyingType {
//------------------------------------------------------------------------------
/** Retry result codes (range 99..1).
*
* A `ter` result means the transaction cannot succeed right now, but
* might succeed after other transactions are applied — for example,
* if the sequence number is too high or there are insufficient funds
* for the fee. The transaction is not applied and leaves a sequence
* gap that can block later transactions. It may be held in the
* transaction queue (`terQUEUED`) to retry when fee levels drop.
*
* @note Numeric values are stable and encoded in `ripple-binary-codec`.
* Never renumber or remove existing enumerators.
*/
// Protocol-critical, mixed with custom TER wrapper type, hundreds of usages
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum TERcodes : TERUnderlyingType {
@@ -185,23 +216,6 @@ enum TERcodes : TERUnderlyingType {
// Exact numbers are used in ripple-binary-codec:
// https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/enums/definitions.json
// Use tokens.
// -99 .. -1: R Retry
// sequence too high, no funds for txn fee, originating -account
// non-existent
//
// Cause:
// Prior application of another, possibly non-existent, transaction could
// allow this transaction to succeed.
//
// Implications:
// - Not applied
// - May be forwarded
// - Results indicating the txn was forwarded: terQUEUED
// - All others are not forwarded.
// - Might succeed later
// - Hold
// - Makes hole in sequence which jams transactions.
terRETRY = -99,
terFUNDS_SPENT, // DEPRECATED.
terINSUF_FEE_B, // Can't pay fee, therefore don't burden network.
@@ -224,57 +238,50 @@ enum TERcodes : TERUnderlyingType {
//------------------------------------------------------------------------------
/** Success result code (value 0).
*
* `tesSUCCESS` is the sole member: the transaction was applied to the
* ledger and forwarded to peers. Its numeric value (0) is stored in
* ledger metadata and must never change.
*
* @note `TERSubset::operator bool()` returns `false` for this code
* (success = falsy), mirroring the conventional C error-code idiom.
*/
// Protocol-critical, mixed with custom TER wrapper type, hundreds of usages
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum TEScodes : TERUnderlyingType {
// Note: Exact number must stay stable. This code is stored by value
// in metadata for historic transactions.
// 0: S Success (success)
// Causes:
// - Success.
// Implications:
// - Applied
// - Forwarded
tesSUCCESS = 0
};
//------------------------------------------------------------------------------
/** Fee-claim result codes (range 100..255).
*
* A `tec` result means the fee is consumed and the sequence number is
* spent, but no other effect is applied to the ledger. The transaction
* is still applied and forwarded to peers. Typical causes: a payment
* with no valid path, or a transaction that is logically invalid but
* well-formed enough to charge a fee.
*
* When `tapRETRY` is set during application, `tec` codes are demoted
* to `terRETRY` so the transaction can be retried rather than
* consuming the sequence number.
*
* @note **DO NOT CHANGE THESE NUMBERS.** They are stored by value in
* ledger metadata and parsed by external tools such as
* `ripple-binary-codec`. Append new codes; never renumber or remove.
* @note Naming convention: use `tecNO_ENTRY` when the primary ledger
* object targeted by the transaction is missing; use
* `tecOBJECT_NOT_FOUND` when an auxiliary object required to
* complete the transaction cannot be found.
*/
// Protocol-critical, mixed with custom TER wrapper type, hundreds of usages
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum TECcodes : TERUnderlyingType {
// Note: Exact numbers must stay stable. These codes are stored by
// value in metadata for historic transactions.
// 100 .. 255 C
// Claim fee only (ripple transaction with no good paths, pay to
// non-existent account, no path)
//
// Causes:
// - Success, but does not achieve optimal result.
// - Invalid transaction or no effect, but claim fee to use the sequence
// number.
//
// Implications:
// - Applied
// - Forwarded
//
// Only allowed as a return code of appliedTransaction when !tapRETRY.
// Otherwise, treated as terRETRY.
//
// DO NOT CHANGE THESE NUMBERS: They appear in ledger meta data.
//
// Note:
// tecNO_ENTRY is often used interchangeably with tecOBJECT_NOT_FOUND.
// While there does not seem to be a clear rule which to use when, the
// following guidance will help to keep errors consistent with the
// majority of (but not all) transaction types:
// - tecNO_ENTRY : cannot find the primary ledger object on which the
// transaction is being attempted
// - tecOBJECT_NOT_FOUND : cannot find the additional object(s) needed to
// complete the transaction
tecCLAIM = 100,
tecPATH_PARTIAL = 101,
tecUNFUNDED_ADD = 102, // Unused legacy code
@@ -362,37 +369,56 @@ enum TECcodes : TERUnderlyingType {
//------------------------------------------------------------------------------
// For generic purposes, a free function that returns the value of a TE*codes.
/** Convert a TEL/TEM/TEF/TER/TES/TEC code to its underlying integer value.
*
* These overloads form the single conversion point used by `TERSubset`,
* comparison operators, and any code that needs to inspect the raw
* integer. A free-function overload set is used rather than an explicit
* conversion operator on `TERSubset` to prevent silent implicit
* conversions in constructor-initialization contexts (e.g., `Status(TER)`
* would compile silently even with `explicit` — a named function does not).
*
* A matching `friend` overload for `TERSubset<Trait>` is defined inside
* the class template; the six overloads below cover the raw enum types.
*
* @param v The enum code to convert.
* @return The underlying `int` value of `v`.
*/
constexpr TERUnderlyingType
TERtoInt(TELcodes v)
{
return safeCast<TERUnderlyingType>(v);
}
/** @copydoc TERtoInt(TELcodes) */
constexpr TERUnderlyingType
TERtoInt(TEMcodes v)
{
return safeCast<TERUnderlyingType>(v);
}
/** @copydoc TERtoInt(TELcodes) */
constexpr TERUnderlyingType
TERtoInt(TEFcodes v)
{
return safeCast<TERUnderlyingType>(v);
}
/** @copydoc TERtoInt(TELcodes) */
constexpr TERUnderlyingType
TERtoInt(TERcodes v)
{
return safeCast<TERUnderlyingType>(v);
}
/** @copydoc TERtoInt(TELcodes) */
constexpr TERUnderlyingType
TERtoInt(TEScodes v)
{
return safeCast<TERUnderlyingType>(v);
}
/** @copydoc TERtoInt(TELcodes) */
constexpr TERUnderlyingType
TERtoInt(TECcodes v)
{
@@ -400,15 +426,37 @@ TERtoInt(TECcodes v)
}
//------------------------------------------------------------------------------
// Template class that is specific to selected ranges of error codes. The
// Trait tells std::enable_if which ranges are allowed.
/** Strongly-typed wrapper around a TER integer that restricts which
* result-code categories may be implicitly assigned or constructed.
*
* The `Trait` policy class template determines which `TE*codes` enum
* types are accepted. A specialization of `Trait<T>` that inherits from
* `std::true_type` permits `T`; one inheriting from `std::false_type`
* rejects it at compile time via `std::enable_if`. This provides
* category-level type safety without runtime overhead.
*
* Two concrete aliases are provided:
* - `NotTEC` — permits `tel`, `tem`, `tef`, `ter`, `tes`; **excludes
* `tec`**. Used as the return type of `preflight()` to prevent fee-theft
* via unsigned transactions (see `CanCvtToNotTEC`).
* - `TER` — permits all six categories including `tec` and `NotTEC`
* (widening assignment from `NotTEC` to `TER` is always valid).
*
* Default-constructs to `tesSUCCESS`. Truthy (`operator bool`) when the
* stored code is anything other than `tesSUCCESS`.
*
* @tparam Trait A class template whose specializations for each `TE*codes`
* type inherit from `std::true_type` (allowed) or `std::false_type`
* (disallowed).
*/
template <template <typename> class Trait>
class TERSubset
{
TERUnderlyingType code_;
public:
// Constructors
/** Default-constructs to `tesSUCCESS`. */
constexpr TERSubset() : code_(tesSUCCESS)
{
}
@@ -421,13 +469,30 @@ private:
}
public:
/** Construct from a raw integer, bypassing the Trait type check.
*
* This escape hatch is intended for deserialization contexts (e.g.,
* reconstructing a `TER` from ledger metadata) where the integer
* originates from a validated source. Prefer enum-typed construction
* everywhere else.
*
* @param from The raw integer code to wrap.
* @return A `TERSubset` holding `from`.
*/
static constexpr TERSubset
fromInt(int from)
{
return TERSubset(from);
}
// Trait tells enable_if which types are allowed for construction.
/** Construct from any `TE*codes` enum type permitted by `Trait`.
*
* The constructor is disabled via `std::enable_if_t` for enum types
* whose `Trait` specialization inherits from `std::false_type`, turning
* category violations into hard compile errors.
*
* @param rhs The source enum value.
*/
template <
typename T,
typename = std::enable_if_t<Trait<std::remove_cv_t<std::remove_reference_t<T>>>::value>>
@@ -435,13 +500,19 @@ public:
{
}
// Assignment
constexpr TERSubset&
operator=(TERSubset const& rhs) = default;
constexpr TERSubset&
operator=(TERSubset&& rhs) = default;
// Trait tells enable_if which types are allowed for assignment.
/** Assign from any `TE*codes` enum type permitted by `Trait`.
*
* Disabled via `std::enable_if_t` for categories not allowed by
* `Trait`, matching the construction constraint.
*
* @param rhs The source enum value.
* @return `*this`.
*/
template <typename T>
constexpr auto
operator=(T rhs) -> std::enable_if_t<Trait<T>::value, TERSubset&>
@@ -450,43 +521,46 @@ public:
return *this;
}
// Conversion to bool.
/** Return `true` when the code is anything other than `tesSUCCESS`.
*
* Mirrors conventional C error-code semantics: a falsy result is
* success, a truthy result means something went wrong. Use
* `isTesSuccess()` for the positive sense.
*/
explicit
operator bool() const
{
return code_ != tesSUCCESS;
}
// Conversion to json::Value allows assignment to json::Objects
// without casting.
/** Implicit conversion to `json::Value` for use in JSON object assembly.
*
* Allows `jsonObj["result"] = ter;` without an explicit cast.
*/
operator json::Value() const
{
return json::Value{code_};
}
// Streaming operator.
/** Stream the raw integer code to `os`. */
friend std::ostream&
operator<<(std::ostream& os, TERSubset const& rhs)
{
return os << rhs.code_;
}
// Return the underlying value. Not a member so similarly named free
// functions can do the same work for the enums.
//
// It's worth noting that an explicit conversion operator was considered
// and rejected. Consider this case, taken from Status.h
//
// class Status {
// int code_;
// public:
// Status (TER ter)
// : code_ (ter) {}
// }
//
// This code compiles with no errors or warnings if TER has an explicit
// (unnamed) conversion to int. To avoid silent conversions like these
// we provide (only) a named conversion.
/** Return the underlying integer value of this result code.
*
* Implemented as a named `friend` free function rather than an
* `explicit` conversion operator. An explicit operator would still
* allow silent conversion in constructor-initialization contexts
* (e.g., `Status(TER ter) : code_(ter) {}` compiles without warning
* even with `explicit`). A named function forces the conversion to
* be visible at every call site.
*
* @param v The `TERSubset` to extract from.
* @return The raw `int` code.
*/
friend constexpr TERUnderlyingType
TERtoInt(TERSubset v)
{
@@ -494,8 +568,16 @@ public:
}
};
// Comparison operators.
// Only enabled if both arguments return int if TERtiInt is called with them.
/** @name TER comparison operators
*
* Heterogeneous comparisons across any combination of raw `TE*codes`
* enum types and `TERSubset` wrappers. Each operator is enabled only
* when both operands have a `TERtoInt` overload returning `int`, so
* unrelated types (e.g., plain `int`) are excluded by SFINAE — no
* accidental numeric comparisons.
*
* @{
*/
template <typename L, typename R>
constexpr auto
operator==(L const& lhs, R const& rhs) -> std::enable_if_t<
@@ -549,17 +631,23 @@ operator>=(L const& lhs, R const& rhs) -> std::enable_if_t<
{
return TERtoInt(lhs) >= TERtoInt(rhs);
}
/** @} */
//------------------------------------------------------------------------------
// Use traits to build a TERSubset that can convert from any of the TE*codes
// enums *except* TECcodes: NotTEC
// NOTE: NotTEC is useful for codes returned by preflight in transactors.
// Preflight checks occur prior to signature checking. If preflight returned
// a tec code, then a malicious user could submit a transaction with a very
// large fee and have that fee charged against an account without using that
// account's valid signature.
/** Trait that permits `tel`, `tem`, `tef`, `ter`, and `tes` but
* explicitly **excludes** `TECcodes` for use with `NotTEC`.
*
* The exclusion of `tec` is a security invariant. `preflight()`
* executes before signature verification. If it could return a `tec`
* code, a malicious actor could craft a transaction with a very large
* fee and have that fee deducted from an account without supplying a
* valid signature. Restricting `preflight` return types to `NotTEC`
* makes this class of fee-theft structurally impossible at compile time.
*
* @tparam FROM The candidate source type; only the five non-tec enums
* yield `std::true_type`.
*/
template <typename FROM>
class CanCvtToNotTEC : public std::false_type
{
@@ -585,12 +673,27 @@ class CanCvtToNotTEC<TEScodes> : public std::true_type
{
};
/** A `TERSubset` restricted to non-`tec` result categories.
*
* Use as the return type of `preflight()` and any function that must
* not be allowed to claim a fee. Assigning a `TECcodes` value to a
* `NotTEC` variable is a compile-time error. A `NotTEC` can be
* widened to a `TER` without a cast.
*/
using NotTEC = TERSubset<CanCvtToNotTEC>;
//------------------------------------------------------------------------------
// Use traits to build a TERSubset that can convert from any of the TE*codes
// enums as well as from NotTEC.
/** Trait that permits all six result-code categories plus `NotTEC` for
* use with the `TER` alias.
*
* The `NotTEC` specialization enables the widening assignment from
* `NotTEC` to `TER` that is required in contexts where a function
* returning `NotTEC` passes its result to code expecting `TER`.
*
* @tparam FROM The candidate source type; all six `TE*codes` enums
* and `NotTEC` yield `std::true_type`.
*/
template <typename FROM>
class CanCvtToTER : public std::false_type
{
@@ -624,35 +727,68 @@ class CanCvtToTER<NotTEC> : public std::true_type
{
};
// TER allows all of the subsets.
/** A `TERSubset` that accepts all six result-code categories.
*
* This is the general result type used throughout the transaction engine
* for `doApply()` and most ledger-application code. Use `NotTEC` for
* `preflight()` return types where `tec` codes must be excluded.
*/
using TER = TERSubset<CanCvtToTER>;
//------------------------------------------------------------------------------
/** Return `true` if `x` is a local-error (`tel`) code (399..300).
*
* @param x The code to test; accepts any `TER`-compatible value.
* @return `true` iff `x` falls in the `tel` range.
*/
inline bool
isTelLocal(TER x) noexcept
{
return (x >= telLOCAL_ERROR && x < temMALFORMED);
}
/** Return `true` if `x` is a malformed-transaction (`tem`) code (299..200).
*
* @param x The code to test.
* @return `true` iff `x` falls in the `tem` range.
*/
inline bool
isTemMalformed(TER x) noexcept
{
return (x >= temMALFORMED && x < tefFAILURE);
}
/** Return `true` if `x` is a failure (`tef`) code (199..100).
*
* @param x The code to test.
* @return `true` iff `x` falls in the `tef` range.
*/
inline bool
isTefFailure(TER x) noexcept
{
return (x >= tefFAILURE && x < terRETRY);
}
/** Return `true` if `x` is a retry (`ter`) code (99..1).
*
* @param x The code to test.
* @return `true` iff `x` falls in the `ter` range.
*/
inline bool
isTerRetry(TER x) noexcept
{
return (x >= terRETRY && x < tesSUCCESS);
}
/** Return `true` if `x` is `tesSUCCESS` (0).
*
* Relies on `TERSubset::operator bool()` returning `false` for
* `tesSUCCESS` (the only falsy value), so this is equivalent to `!x`.
*
* @param x The code to test.
* @return `true` iff `x == tesSUCCESS`.
*/
inline bool
isTesSuccess(TER x) noexcept
{
@@ -660,24 +796,75 @@ isTesSuccess(TER x) noexcept
return !(x);
}
/** Return `true` if `x` is a fee-claim (`tec`) code (≥ 100).
*
* Any value at or above `tecCLAIM` (100) is treated as a fee-claim
* result regardless of whether it is a recognized code.
*
* @param x The code to test.
* @return `true` iff `x >= tecCLAIM`.
*/
inline bool
isTecClaim(TER x) noexcept
{
return ((x) >= tecCLAIM);
}
/** Return the complete registry mapping every known TER code to its
* symbolic token and human-readable description.
*
* The map is keyed by the underlying integer value and maps to a pair
* of C-string literals: `{token, description}`. Token strings match
* the enum identifier exactly (generated via preprocessor stringification).
* The registry is a Meyers singleton — initialized once on first call,
* thread-safe per C++11, and immutable thereafter.
*
* @return A `const` reference to the singleton map, valid for the
* lifetime of the process.
*/
std::unordered_map<TERUnderlyingType, std::pair<char const* const, char const* const>> const&
transResults();
/** Look up the token and human-readable description for a TER code.
*
* @param code The TER result code to look up.
* @param token On success, populated with the symbolic token string
* (e.g., `"tecNO_DST"`). Unchanged on failure.
* @param text On success, populated with the English description.
* Unchanged on failure.
* @return `true` if `code` is a registered code; `false` otherwise.
*/
bool
transResultInfo(TER code, std::string& token, std::string& text);
/** Return the symbolic token string for a TER code.
*
* @param code The TER result code to look up.
* @return The token string (e.g., `"tecNO_DST"`) for known codes, or
* `"-"` if `code` is not in the registry.
*/
std::string
transToken(TER code);
/** Return the human-readable description for a TER code.
*
* @param code The TER result code to look up.
* @return The English description string for known codes, or `"-"` if
* `code` is not in the registry.
*/
std::string
transHuman(TER code);
/** Convert a symbolic token string to the corresponding TER code.
*
* Provides the reverse direction of `transToken()`. The reverse map is
* built lazily as a function-local static on the first call by inverting
* the primary registry; the two maps stay in sync automatically.
*
* @param token The symbolic token string to look up (e.g., `"tecNO_DST"`).
* @return The matching `TER` wrapped in `std::optional`, or
* `std::nullopt` if `token` is not a recognized code name.
*/
std::optional<TER>
transCode(std::string const& token);

View File

@@ -1,3 +1,17 @@
/** @file
* Canonical, single-source-of-truth definitions for every transaction flag in
* the XRPL protocol.
*
* Flag values are embedded in signed transactions and therefore form part of
* the consensus protocol. Altering any constant without a coordinated amendment
* and special handling will cause a hard fork.
*
* The file uses three X-macro instantiations of a single `XMACRO` table to
* emit, from one authoritative list, the flag value constants, the per-type
* validation masks, and the Meyer's-singleton getter functions consumed by the
* `server_definitions` RPC endpoint.
*/
#pragma once
// NOLINTBEGIN(readability-identifier-naming)
@@ -36,14 +50,33 @@ namespace xrpl {
@ingroup protocol
*/
/** Underlying integer type for all transaction flag bitmasks. */
using FlagValue = std::uint32_t;
// Universal Transaction flags:
// --- Universal Transaction flags ---
/** Require that the transaction signature use the canonical (low-S) ECDSA
* form. The network now enforces this unconditionally, but the flag must
* remain defined so that historical transactions that set it remain valid. */
inline constexpr FlagValue tfFullyCanonicalSig = 0x80000000;
/** Marks a transaction as an inner member of a `Batch` transaction.
*
* Set by the batch submitter on every inner transaction; the outer `Batch`
* wrapper must NOT carry this flag (enforced by `tfBatchMask` and the
* compile-time `static_assert` below). */
inline constexpr FlagValue tfInnerBatchTxn = 0x40000000;
/** Bitwise OR of all universal flags; occupies the high 8 bits of `Flags`. */
inline constexpr FlagValue tfUniversal = tfFullyCanonicalSig | tfInnerBatchTxn;
/** Complement of `tfUniversal`; ANDing an unknown `Flags` value with this mask
* isolates any transaction-type-specific bits. */
inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
// The push/pop guards protect any caller that has its own macros with the same
// short names (XMACRO, TO_VALUE, etc.) from having them clobbered when this
// header is included.
#pragma push_macro("XMACRO")
#pragma push_macro("TO_VALUE")
#pragma push_macro("VALUE_TO_MAP")
@@ -70,16 +103,36 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
// clang-format off
#undef ALL_TX_FLAGS
// XMACRO parameters:
// - TRANSACTION: handles the transaction name, its flags, and mask adjustment
// - TF_FLAG: defines a new flag constant
// - TF_FLAG2: references an existing flag constant (no new definition)
// - MASK_ADJ: specifies flags to add back to the mask (making them invalid for this tx type)
//
// Note: MASK_ADJ is used when a universal flag should be invalid for a specific transaction.
// For example, Batch uses MASK_ADJ(tfInnerBatchTxn) because the outer Batch transaction
// must not have tfInnerBatchTxn set (only inner transactions should have it).
//
/** Master X-macro table of all per-transaction-type flag groups.
*
* This macro is the single source of truth for every flag in the system.
* It is instantiated three times with different argument bindings to produce:
* 1. Inline `constexpr FlagValue tf*` declarations.
* 2. Inline `constexpr FlagValue tf*Mask` validation masks.
* 3. `inline FlagMap const& get*Flags()` Meyer's-singleton getters consumed
* by the `server_definitions` RPC endpoint.
*
* @param TRANSACTION Macro invoked once per transaction type; receives the
* type name, the expansion of its flag list, and the mask adjustment.
* @param TF_FLAG Declares a new flag constant unique to this transaction
* type (or the first transaction that defines a shared constant).
* @param TF_FLAG2 References an already-declared flag constant; suppresses
* redeclaration. Used when two transaction types share a numeric value
* (e.g., `tfLPToken` is declared by `AMMDeposit` and referenced by
* `AMMWithdraw`).
* @param MASK_ADJ Specifies additional bits to OR back into the generated
* mask, making those bits invalid for this transaction type even though
* they are otherwise universal. `Batch` uses `MASK_ADJ(tfInnerBatchTxn)`
* because the outer wrapper must not carry that flag; all other entries
* use `MASK_ADJ(0)`.
*
* @note To add a new flag: add a `TF_FLAG(name, value)` row inside the
* appropriate `TRANSACTION(...)` block and nowhere else. The value,
* mask, and getter are all derived automatically.
*
* @warning Flag values are protocol-stable. Changing or reusing a numeric
* value without an amendment causes a hard fork.
*/
// TODO: Consider rewriting this using reflection in C++26 or later. Alternatively this could be a DSL processed by a script at build time.
#define XMACRO(TRANSACTION, TF_FLAG, TF_FLAG2, MASK_ADJ) \
TRANSACTION(AccountSet, \
@@ -218,39 +271,38 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal;
// clang-format on
// Create all the flag values.
//
// example:
// inline constexpr FlagValue tfAccountSetRequireDestTag = 0x00010000;
// --- Instantiation 1: emit `inline constexpr FlagValue tf* = 0x...;` ---
// TF_FLAG → declares a new constant.
// TF_FLAG2 → no-op (constant already declared by a prior TRANSACTION block).
// Example output:
// inline constexpr FlagValue tfRequireDestTag = 0x00010000;
#define TO_VALUE(name, value) inline constexpr FlagValue name = value;
#define NULL_NAME(name, values, maskAdj) values
#define NULL_OUTPUT(name, value)
#define NULL_MASK_ADJ(value)
XMACRO(NULL_NAME, TO_VALUE, NULL_OUTPUT, NULL_MASK_ADJ)
// Create masks for each transaction type that has flags.
//
// example:
// inline constexpr FlagValue tfAccountSetMask = ~(tfUniversal | tfRequireDestTag |
// tfOptionalDestTag | tfRequireAuth | tfOptionalAuth | tfDisallowXRP | tfAllowXRP);
//
// The mask adjustment (maskAdj) allows adding flags back to the mask, making them invalid.
// For example, Batch uses MASK_ADJ(tfInnerBatchTxn) to reject tfInnerBatchTxn on outer Batch.
// --- Instantiation 2: emit `inline constexpr FlagValue tf*Mask` ---
// Each mask is the bitwise complement of (tfUniversal | all valid flags for
// this tx type) OR'd with any MASK_ADJ bits. A transaction whose `Flags`
// field ANDed with the mask is non-zero is rejected as carrying unknown or
// forbidden flags.
// Example output:
// inline constexpr FlagValue tfAccountSetMask =
// ~(tfUniversal | tfRequireDestTag | tfOptionalDestTag | ...);
#define TO_MASK(name, values, maskAdj) \
inline constexpr FlagValue tf##name##Mask = ~(tfUniversal values) | (maskAdj);
#define VALUE_TO_MASK(name, value) | name
#define MASK_ADJ_TO_MASK(value) value
XMACRO(TO_MASK, VALUE_TO_MASK, VALUE_TO_MASK, MASK_ADJ_TO_MASK)
// Verify that tfBatchMask correctly rejects tfInnerBatchTxn.
// The outer Batch transaction must NOT have tfInnerBatchTxn set; only inner transactions should
// have it.
// Compile-time invariants for the MASK_ADJ(tfInnerBatchTxn) mechanism:
// The outer Batch transaction rejects tfInnerBatchTxn (bit must appear in its
// mask); all other transaction types allow it so inner transactions can carry
// the flag legally.
static_assert(
(tfBatchMask & tfInnerBatchTxn) == tfInnerBatchTxn,
"tfBatchMask must include tfInnerBatchTxn to reject it on outer Batch");
// Verify that other transaction masks correctly allow tfInnerBatchTxn.
// Inner transactions need tfInnerBatchTxn to be valid, so these masks must not reject it.
static_assert(
(tfPaymentMask & tfInnerBatchTxn) == 0,
"tfPaymentMask must not reject tfInnerBatchTxn");
@@ -258,19 +310,13 @@ static_assert(
(tfAccountSetMask & tfInnerBatchTxn) == 0,
"tfAccountSetMask must not reject tfInnerBatchTxn");
// Create getter functions for each set of flags using Meyer's singleton pattern.
// This avoids static initialization order fiasco while still providing efficient access.
// This is used below in `getAllTxFlags()` to generate the server_definitions RPC
// output.
//
// example:
// inline FlagMap const& getAccountSetFlags() {
// static FlagMap const flags = {
// {"tfRequireDestTag", 0x00010000},
// {"tfOptionalDestTag", 0x00020000},
// ...};
// return flags;
// }
// --- Instantiation 3: emit `inline FlagMap const& get*Flags()` getters ---
// Each function initialises a local static on first call (Meyer's singleton)
// and returns a reference to it on every subsequent call. The map is keyed
// by flag name string and valued by the numeric FlagValue. These are
// aggregated by getAllTxFlags() and served to clients via server_definitions.
/** Maps flag names to their numeric values for a single transaction type. */
using FlagMap = std::map<std::string, FlagValue>;
#define VALUE_TO_MAP(name, value) {#name, value},
#define TO_MAP(name, values, maskAdj) \
@@ -281,6 +327,14 @@ using FlagMap = std::map<std::string, FlagValue>;
}
XMACRO(TO_MAP, VALUE_TO_MAP, VALUE_TO_MAP, NULL_MASK_ADJ)
/** Returns the universal transaction flags by name.
*
* The returned map contains `tfFullyCanonicalSig` and `tfInnerBatchTxn`.
* It is initialised once (Meyer's singleton) and safe to call from any
* thread after static initialisation completes.
*
* @return A reference to the singleton `FlagMap` for universal flags.
*/
inline FlagMap const&
getUniversalFlags()
{
@@ -289,18 +343,21 @@ getUniversalFlags()
return flags;
}
// Create a getter function for all transaction flag maps using Meyer's singleton pattern.
// This is used to generate the server_definitions RPC output.
//
// example:
// inline FlagMapPairList const& getAllTxFlags() {
// static FlagMapPairList const flags = {
// {"AccountSet", getAccountSetFlags()},
// ...};
// return flags;
// }
/** Ordered list of `{transaction-type-name, FlagMap}` pairs covering every
* transaction type and the universal flag group. Consumed by the
* `server_definitions` RPC endpoint so clients can discover the protocol's
* flag vocabulary at runtime. */
using FlagMapPairList = std::vector<std::pair<std::string, FlagMap>>;
#define ALL_TX_FLAGS(name, values, maskAdj) {#name, get##name##Flags()},
/** Returns all per-transaction-type flag maps, prefixed by the universal group.
*
* Initialised once (Meyer's singleton). The first entry is always
* `{"universal", getUniversalFlags()}`; subsequent entries follow the
* declaration order in `XMACRO`.
*
* @return A reference to the singleton `FlagMapPairList`.
*/
inline FlagMapPairList const&
getAllTxFlags()
{
@@ -334,68 +391,142 @@ getAllTxFlags()
#pragma pop_macro("NULL_MASK_ADJ")
#pragma pop_macro("MASK_ADJ_TO_MASK")
// Additional transaction masks and combos
// --- Additional composite masks ---
/** Validation mask for `Payment` transactions that involve MPTokens.
*
* MPToken payments support only `tfPartialPayment`; all other
* transaction-type-specific bits are rejected. */
inline constexpr FlagValue tfMPTPaymentMask = ~(tfUniversal | tfPartialPayment);
/** Validation mask for `TrustSet` transactions submitted under a granular
* delegation permission.
*
* Only `tfSetfAuth`, `tfSetFreeze`, and `tfClearFreeze` are permitted when
* the `TrustlineUnfreeze` permission applies; any other flags cause the
* transactor to return `terNO_DELEGATE_PERMISSION`. */
inline constexpr FlagValue tfTrustSetPermissionMask =
~(tfUniversal | tfSetfAuth | tfSetFreeze | tfClearFreeze);
// MPTokenIssuanceCreate MutableFlags:
// Indicating specific fields or flags may be changed after issuance.
// --- MPTokenIssuanceCreate mutable-flag declarations (tmf* prefix) ---
// These alias the corresponding lsmf* ledger-state mutable-flag values from
// LedgerFormats.h so that the same numeric bit can be stored verbatim on the
// MPTokenIssuance object without a translation step. Each flag, when set on
// the creation transaction, means the named property may be updated by a
// subsequent MPTokenIssuanceSet transaction.
/** Permits the `CanLock` property to be changed after issuance. */
inline constexpr FlagValue tmfMPTCanMutateCanLock = lsmfMPTCanMutateCanLock;
/** Permits the `RequireAuth` property to be changed after issuance. */
inline constexpr FlagValue tmfMPTCanMutateRequireAuth = lsmfMPTCanMutateRequireAuth;
/** Permits the `CanEscrow` property to be changed after issuance. */
inline constexpr FlagValue tmfMPTCanMutateCanEscrow = lsmfMPTCanMutateCanEscrow;
/** Permits the `CanTrade` property to be changed after issuance. */
inline constexpr FlagValue tmfMPTCanMutateCanTrade = lsmfMPTCanMutateCanTrade;
/** Permits the `CanTransfer` property to be changed after issuance. */
inline constexpr FlagValue tmfMPTCanMutateCanTransfer = lsmfMPTCanMutateCanTransfer;
/** Permits the `CanClawback` property to be changed after issuance. */
inline constexpr FlagValue tmfMPTCanMutateCanClawback = lsmfMPTCanMutateCanClawback;
/** Permits the metadata URI to be changed after issuance. */
inline constexpr FlagValue tmfMPTCanMutateMetadata = lsmfMPTCanMutateMetadata;
/** Permits the transfer fee to be changed after issuance. */
inline constexpr FlagValue tmfMPTCanMutateTransferFee = lsmfMPTCanMutateTransferFee;
/** Validation mask for the `MutableFlags` field of `MPTokenIssuanceCreate`.
*
* Any bit outside the recognised `tmfMPTCanMutate*` set causes
* `MPTokenIssuanceCreate::preflight` to return `temINVALID_FLAG`.
* A value of zero is also rejected — at least one mutable property must be
* declared. */
inline constexpr FlagValue tmfMPTokenIssuanceCreateMutableMask =
~(tmfMPTCanMutateCanLock | tmfMPTCanMutateRequireAuth | tmfMPTCanMutateCanEscrow |
tmfMPTCanMutateCanTrade | tmfMPTCanMutateCanTransfer | tmfMPTCanMutateCanClawback |
tmfMPTCanMutateMetadata | tmfMPTCanMutateTransferFee);
// MPTokenIssuanceSet MutableFlags:
// Set or Clear flags.
// --- MPTokenIssuanceSet mutable-flag Set/Clear pairs ---
// Each property has two complementary flags: one to enable and one to disable
// the property in an existing MPTokenIssuance object. Setting both bits in the
// same transaction is a logical error and is rejected by the transactor.
/** Enable the `CanLock` property on an existing MPTokenIssuance. */
inline constexpr FlagValue tmfMPTSetCanLock = 0x00000001;
/** Disable the `CanLock` property on an existing MPTokenIssuance. */
inline constexpr FlagValue tmfMPTClearCanLock = 0x00000002;
/** Enable the `RequireAuth` property on an existing MPTokenIssuance. */
inline constexpr FlagValue tmfMPTSetRequireAuth = 0x00000004;
/** Disable the `RequireAuth` property on an existing MPTokenIssuance. */
inline constexpr FlagValue tmfMPTClearRequireAuth = 0x00000008;
/** Enable the `CanEscrow` property on an existing MPTokenIssuance. */
inline constexpr FlagValue tmfMPTSetCanEscrow = 0x00000010;
/** Disable the `CanEscrow` property on an existing MPTokenIssuance. */
inline constexpr FlagValue tmfMPTClearCanEscrow = 0x00000020;
/** Enable the `CanTrade` property on an existing MPTokenIssuance. */
inline constexpr FlagValue tmfMPTSetCanTrade = 0x00000040;
/** Disable the `CanTrade` property on an existing MPTokenIssuance. */
inline constexpr FlagValue tmfMPTClearCanTrade = 0x00000080;
/** Enable the `CanTransfer` property on an existing MPTokenIssuance. */
inline constexpr FlagValue tmfMPTSetCanTransfer = 0x00000100;
/** Disable the `CanTransfer` property on an existing MPTokenIssuance. */
inline constexpr FlagValue tmfMPTClearCanTransfer = 0x00000200;
/** Enable the `CanClawback` property on an existing MPTokenIssuance. */
inline constexpr FlagValue tmfMPTSetCanClawback = 0x00000400;
/** Disable the `CanClawback` property on an existing MPTokenIssuance. */
inline constexpr FlagValue tmfMPTClearCanClawback = 0x00000800;
/** Validation mask for the `MutableFlags` field of `MPTokenIssuanceSet`.
*
* Any bit outside the recognised `tmfMPTSet*` / `tmfMPTClear*` set causes
* `MPTokenIssuanceSet::preflight` to return `temINVALID_FLAG`.
* A zero value is also rejected. */
inline constexpr FlagValue tmfMPTokenIssuanceSetMutableMask = ~(
tmfMPTSetCanLock | tmfMPTClearCanLock | tmfMPTSetRequireAuth | tmfMPTClearRequireAuth |
tmfMPTSetCanEscrow | tmfMPTClearCanEscrow | tmfMPTSetCanTrade | tmfMPTClearCanTrade |
tmfMPTSetCanTransfer | tmfMPTClearCanTransfer | tmfMPTSetCanClawback | tmfMPTClearCanClawback);
// Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between accounts allowed a
// TrustLine to be added to the issuer of that token without explicit permission from that issuer.
// This was enabled by minting the NFToken with the tfTrustLine flag set.
// --- NFTokenMint backward-compatibility mask variants ---
// Three mask variants exist to accommodate two amendment-gated changes to the
// set of valid NFTokenMint flags:
//
// That capability could be used to attack the NFToken issuer.
// It would be possible for two accounts to trade the NFToken back and forth building up any number
// of TrustLines on the issuer, increasing the issuer's reserve without bound.
// 1. fixRemoveNFTokenAutoTrustLine — closed a reserve-exhaustion attack where
// two accounts could endlessly trade an NFToken, forcing unbounded trust
// lines onto the issuer. After this amendment, tfTrustLine is forbidden.
// 2. featureDynamicNFT — adds tfMutable, allowing the token URI to be updated
// after minting.
//
// The fixRemoveNFTokenAutoTrustLine amendment disables minting with the tfTrustLine flag as a way
// to prevent the attack. But until the amendment passes we still need to keep the old behavior
// available.
inline constexpr FlagValue tfTrustLine = 0x00000004; // needed for backwards compatibility
// Nodes processing historical ledger data must still accept tfTrustLine on pre-
// amendment mints, which is why the constant and the old mask remain defined.
/** NFTokenMint flag that once allowed automatic trust-line creation on the
* issuer. Forbidden after the `fixRemoveNFTokenAutoTrustLine` amendment;
* retained only for historical ledger replay. */
inline constexpr FlagValue tfTrustLine = 0x00000004;
/** Baseline `NFTokenMint` validation mask (post `fixRemoveNFTokenAutoTrustLine`,
* pre `featureDynamicNFT`). Rejects `tfTrustLine` and `tfMutable`. */
inline constexpr FlagValue tfNFTokenMintMaskWithoutMutable =
~(tfUniversal | tfBurnable | tfOnlyXRP | tfTransferable);
/** `NFTokenMint` validation mask for ledgers before `fixRemoveNFTokenAutoTrustLine`.
* Allows `tfTrustLine` in addition to the standard flags. */
inline constexpr FlagValue tfNFTokenMintOldMask = ~(~tfNFTokenMintMaskWithoutMutable | tfTrustLine);
// if featureDynamicNFT enabled then new flag allowing mutable URI available.
/** `NFTokenMint` validation mask for ledgers before `fixRemoveNFTokenAutoTrustLine`
* but after `featureDynamicNFT` — allows both `tfTrustLine` and `tfMutable`. */
inline constexpr FlagValue tfNFTokenMintOldMaskWithMutable = ~(~tfNFTokenMintOldMask | tfMutable);
/** Union of all mutually-exclusive `AMMWithdraw` mode flags.
*
* The transactor checks `std::popcount(flags & tfWithdrawSubTx) == 1` to
* ensure exactly one withdrawal mode is selected; zero or more than one
* causes `temMALFORMED`. */
inline constexpr FlagValue tfWithdrawSubTx = tfLPToken | tfSingleAsset | tfTwoAsset |
tfOneAssetLPToken | tfLimitLPToken | tfWithdrawAll | tfOneAssetWithdrawAll;
/** Union of all mutually-exclusive `AMMDeposit` mode flags.
*
* The transactor checks `std::popcount(flags & tfDepositSubTx) == 1` to
* ensure exactly one deposit mode is selected; zero or more than one
* causes `temMALFORMED`. */
inline constexpr FlagValue tfDepositSubTx =
tfLPToken | tfSingleAsset | tfTwoAsset | tfOneAssetLPToken | tfLimitLPToken | tfTwoAssetIfEmpty;
@@ -403,7 +534,21 @@ inline constexpr FlagValue tfDepositSubTx =
#pragma push_macro("ACCOUNTSET_FLAG_TO_VALUE")
#pragma push_macro("ACCOUNTSET_FLAG_TO_MAP")
// AccountSet SetFlag/ClearFlag values
/** X-macro table of `AccountSet` `SetFlag`/`ClearFlag` integer values.
*
* These are **small integers** (117), not bitmasks, and are carried in the
* `SetFlag` or `ClearFlag` field of an `AccountSet` transaction rather than
* in the `Flags` bitmask. Value 11 is reserved for the Hooks amendment
* (`asfTshCollect`) and is intentionally absent here.
*
* The macro is instantiated twice: once to declare inline constants and once
* to build the map returned by `getAsfFlagMap()`.
*
* @param ASF_FLAG Receives `(name, integer_value)` for each `asf*` constant.
*
* @warning These values are protocol-stable; changing them breaks existing
* signed `AccountSet` transactions.
*/
#define ACCOUNTSET_FLAGS(ASF_FLAG) \
ASF_FLAG(asfRequireDest, 1) \
ASF_FLAG(asfRequireAuth, 2) \
@@ -429,6 +574,14 @@ inline constexpr FlagValue tfDepositSubTx =
ACCOUNTSET_FLAGS(ACCOUNTSET_FLAG_TO_VALUE)
/** Returns all `AccountSet` `SetFlag`/`ClearFlag` values by name.
*
* The map keys are the `asf*` constant names; values are the corresponding
* small integers. Initialised once (Meyer's singleton) and consumed by the
* `server_definitions` RPC endpoint alongside `getAllTxFlags()`.
*
* @return A reference to the singleton `asf*` flag map.
*/
inline std::map<std::string, FlagValue> const&
getAsfFlagMap()
{

View File

@@ -6,34 +6,30 @@
namespace xrpl {
/** Transaction type identifiers.
These are part of the binary message format.
@ingroup protocol
*/
/** Transaction type identifiers
Each ledger object requires a unique type identifier, which is stored
within the object itself; this makes it possible to iterate the entire
ledger and determine each object's type and verify that the object you
retrieved from a given hash matches the expected type.
@warning Since these values are included in transactions, which are signed
objects, and used by the code to determine the type of transaction
being invoked, they are part of the protocol. **Changing them
should be avoided because without special handling, this will
result in a hard fork.**
@note When retiring types, the specific values should not be removed but
should be marked as [[deprecated]]. This is to avoid accidental
reuse of identifiers.
@todo The C++ language does not enable checking for duplicate values
here. If it becomes possible then we should do this.
@ingroup protocol
*/
/** Wire-format identifiers for every XRPL transaction type.
*
* The numeric values are generated by the X-macro expansion of
* `transactions.macro` and are embedded inside signed transaction objects.
* They are therefore **immutable protocol surface**: a validator that
* assigns a different meaning to a given numeric value cannot participate
* in consensus with its peers, which would cause a hard fork.
*
* Deprecated types (`TtNicknameSet`, `TtContract`, `TtSpinalTap`) are
* retained as tombstoned, `[[deprecated]]`-annotated enumerators so that
* their numeric slots can never be accidentally reused by a future
* transaction type — doing so would cause historical ledger data to be
* misclassified.
*
* @warning Do not change any existing enumerator value. Numeric IDs are
* part of the signed binary format and altering them constitutes
* a consensus-breaking protocol change.
*
* @note The language cannot detect duplicate enumerator values at compile
* time; duplicate-ID protection is enforced instead at static-init time
* by `TxFormats::TxFormats()` via `KnownFormats::add()`.
*
* @ingroup protocol
*/
// clang-format off
// Protocol-critical, hundreds of usages
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
@@ -64,20 +60,76 @@ enum TxType : std::uint16_t
};
// clang-format on
/** Manages the list of known transaction formats.
/** Singleton registry mapping every XRPL transaction type to its field schema.
*
* `TxFormats` inherits `KnownFormats<TxType, TxFormats>`, which stores one
* `Item` per transaction type. Each `Item` pairs the type's human-readable
* name and `TxType` discriminant with an `SOTemplate` — the ordered list of
* `SOElement` descriptors specifying which SFields the transaction accepts
* and whether each is required, optional, or default.
*
* The singleton is constructed lazily on the first call to `getInstance()`.
* During construction, `transactions.macro` is expanded via an X-macro to
* call `KnownFormats::add()` once per transaction type. Registering a
* duplicate `TxType` value causes an immediate `logicError()` (process
* abort), making collisions visible at startup rather than at request time.
*
* Every transaction's `SOTemplate` is formed by merging the type-specific
* fields from `transactions.macro` with the universal common fields returned
* by `getCommonFields()`. The resulting schema is consulted by the
* deserializer and validator for every transaction that enters the ledger.
*
* @note This class is non-copyable. Obtain the single instance via
* `getInstance()`.
*
* @see KnownFormats, TxType, SOTemplate
*/
class TxFormats : public KnownFormats<TxType, TxFormats>
{
private:
/** Create the object.
This will load the object with all the known transaction formats.
*/
/** Populate the registry with every known transaction format.
*
* Called exactly once by `getInstance()`. Expands `transactions.macro`
* and registers each transaction type via `KnownFormats::add()`.
*/
TxFormats();
public:
/** Return the process-wide singleton registry of all transaction formats.
*
* The registry is constructed on the first call via a function-local
* static, guaranteeing thread-safe initialization under C++11 rules for
* local statics. The same reference is returned on every subsequent
* call.
*
* @return A stable `const` reference to the singleton `TxFormats`
* instance.
*/
static TxFormats const&
getInstance();
/** Return the fields shared by every XRPL transaction, regardless of type.
*
* The returned vector is merged with each transaction's unique fields
* inside `KnownFormats::add()` to form the complete `SOTemplate` for
* that type. Required entries are `sfTransactionType`, `sfAccount`,
* `sfSequence`, `sfFee`, and `sfSigningPubKey`; a transaction missing
* any of them fails template validation.
*
* Notable optional fields and their roles:
* - `sfTicketSequence` — consume a pre-issued ticket instead of the
* account's current sequence, enabling out-of-order submission.
* - `sfSigners` — multi-signature array; coexists with `sfSigningPubKey`
* because single-sig and multi-sig are orthogonal format modes.
* - `sfNetworkID` — distinguishes sidechain transactions from mainnet
* ones at the wire level.
* - `sfDelegate` — authorizes a second account to act on behalf of the
* fee-paying account.
*
* @return A stable `const` reference to the static common-field vector.
* The vector is initialized on the first call and lives for the
* lifetime of the process.
*/
static std::vector<SOElement> const&
getCommonFields();
};

View File

@@ -11,71 +11,245 @@
namespace xrpl {
/** Metadata record for a single transaction applied to a closed XRPL ledger.
*
* Every validated transaction carries a metadata blob alongside the
* transaction itself. This class owns that metadata: the result code, the
* transaction's ordinal position within the ledger, and the `AffectedNodes`
* array — one entry per ledger object that was created, modified, or deleted.
*
* The primary lifecycle is: construct empty via `TxMeta(txid, ledger)`,
* accumulate node records through `setAffectedNode` / `getAffectedNode` (both
* called by `ApplyStateTable`), then finalize with a single call to
* `addRaw()`. Calling `getAsObject()` or `getJson()` before `addRaw()` will
* trigger an assertion because `result_` still holds the sentinel value 255.
*
* The two deserializing constructors handle the opposite direction: loading
* existing metadata from raw bytes (database / wire) or from an already-parsed
* `STObject` (ledger replay, RPC).
*
* @note `TxMeta` is a passive data container. All change-detection and
* field-diff logic lives in `ApplyStateTable::apply()`, which drives the
* accumulation protocol.
* @note `addRaw()` sorts `nodes_` by `sfLedgerIndex` before serializing.
* This sort is consensus-critical: validators must produce byte-identical
* metadata blobs for the SHAMap hash to agree.
*/
class TxMeta
{
public:
/** Construct an empty metadata shell for a transaction being applied.
*
* Initializes `result_` to 255 and `index_` to `UINT32_MAX` as sentinels
* indicating the object has not yet been finalized. Pre-reserves 32 slots
* in `nodes_` to absorb typical transaction complexity without
* reallocation. Must be finalized by `addRaw()` before serialization.
*
* @param transactionID Hash of the transaction this metadata describes.
* @param ledger Sequence number of the ledger in which the
* transaction is included.
*/
TxMeta(uint256 const& transactionID, std::uint32_t ledger);
/** Deserialize metadata from a raw byte blob.
*
* Wraps `vec` in a `SerialIter`, materializes an `STObject` tagged
* `sfMetadata`, and extracts `sfTransactionResult`, `sfTransactionIndex`,
* and `sfAffectedNodes`. Also reads optional `sfDeliveredAmount` and
* `sfParentBatchID` via `setAdditionalFields`. Used when loading existing
* metadata from the ledger database or from the wire.
*
* @param txID Hash of the transaction this metadata describes.
* @param ledger Sequence number of the containing ledger.
* @param vec Raw serialized metadata bytes.
*/
TxMeta(uint256 const& txID, std::uint32_t ledger, Blob const&);
/** Construct from an already-parsed STObject.
*
* Extracts `sfTransactionResult`, `sfTransactionIndex`, and
* `sfAffectedNodes` from `obj`, plus any optional fields. Used during
* ledger replay and in RPC code paths where the metadata has already been
* deserialized into the `STObject` type hierarchy.
*
* @param txID Hash of the transaction this metadata describes.
* @param ledger Sequence number of the containing ledger.
* @param obj Fully deserialized metadata object (e.g. `sfMetadata`).
*/
TxMeta(uint256 const& txID, std::uint32_t ledger, STObject const&);
/** Return the hash of the transaction this metadata record describes. */
[[nodiscard]] uint256 const&
getTxID() const
{
return transactionID_;
}
/** Return the sequence number of the ledger that included this transaction. */
[[nodiscard]] std::uint32_t
getLgrSeq() const
{
return ledgerSeq_;
}
/** Return the raw integer representation of the transaction result code.
*
* Holds 255 until `addRaw()` has been called; callers that need a typed
* result should prefer `getResultTER()`.
*
* @return Raw `TER` integer value (0 = tesSUCCESS, 101255 = tec codes).
*/
[[nodiscard]] int
getResult() const
{
return result_;
}
/** Return the transaction result as a typed `TER` code.
*
* @note Returns a meaningless value if called before `addRaw()`.
*/
[[nodiscard]] TER
getResultTER() const
{
return TER::fromInt(result_);
}
/** Return the transaction's ordinal position within its ledger.
*
* Holds `UINT32_MAX` until `addRaw()` has been called.
*/
[[nodiscard]] std::uint32_t
getIndex() const
{
return index_;
}
/** Register or reclassify a ledger entry in the affected-nodes list.
*
* If a node with the given key already exists, its field name and
* `sfLedgerEntryType` are updated in place — allowing a caller to
* upgrade an entry from `sfModifiedNode` to `sfDeletedNode`, for
* example. Otherwise a new entry is appended to `nodes_`.
*
* @param node The `sfLedgerIndex` key of the affected ledger entry.
* @param type Node category: `sfCreatedNode`, `sfModifiedNode`, or
* `sfDeletedNode`.
* @param nodeType The `sfLedgerEntryType` value (e.g. `ltACCOUNT_ROOT`).
*/
void
setAffectedNode(uint256 const&, SField const& type, std::uint16_t nodeType);
/** Retrieve or create the metadata node for a live ledger entry.
*
* Finds the node whose `sfLedgerIndex` matches `node->key()`, creating a
* new entry (copying the entry type from the live SLE) if none exists.
* Used by `ApplyStateTable` to attach `sfPreviousFields`, `sfFinalFields`,
* or `sfNewFields` sub-objects after the node has been registered via
* `setAffectedNode`.
*
* @param node Live ledger entry whose metadata node is requested.
* @param type Node category used when a new entry must be created.
* @return Mutable reference to the matching `STObject` in `nodes_`.
*/
STObject&
getAffectedNode(SLE::ref node, SField const& type); // create if needed
/** Retrieve the metadata node for a ledger-entry key, asserting it exists.
*
* Used after `setAffectedNode` has guaranteed registration of the key.
* Calls `UNREACHABLE` in debug builds and throws in release if the key is
* absent — absence indicates a programming error, not a data error.
*
* @param node The `sfLedgerIndex` key to look up.
* @return Mutable reference to the matching `STObject` in `nodes_`.
* @throws std::runtime_error If the node is not found (should never occur
* in correct callers).
*/
STObject&
getAffectedNode(uint256 const&);
/** Return a list of accounts affected by this transaction */
/** Return the set of every account implicated by this transaction's metadata.
*
* Scans `sfNewFields` for created nodes and `sfFinalFields` for modified
* and deleted nodes, extracting `AccountID` values from `STAccount`
* fields, from the issuers embedded in `sfLowLimit`, `sfHighLimit`,
* `sfTakerPays`, and `sfTakerGets` amounts, and from the issuer encoded in
* `sfMPTokenIssuanceID`.
*
* @note The behavior must remain identical to that of the JavaScript
* `Meta#getAffectedAccounts` method for cross-platform consistency.
* @return Sorted, deduplicated flat set of affected `AccountID` values.
*/
[[nodiscard]] boost::container::flat_set<AccountID>
getAffectedAccounts() const;
/** Serialize the metadata as JSON via the underlying `STObject`.
*
* Delegates to `getAsObject().getJson(p)`. Must not be called before
* `addRaw()` finalizes `result_`.
*
* @param p JSON formatting options.
* @return JSON representation of the complete metadata object.
*/
[[nodiscard]] json::Value
getJson(JsonOptions p) const
{
return getAsObject().getJson(p);
}
/** Finalize and append the metadata bytes to a Serializer.
*
* Records `result` and `index`, then sorts `nodes_` by `sfLedgerIndex`
* before calling `getAsObject().add(s)`. The sort is consensus-critical:
* nodes are accumulated in execution order, which is not deterministic
* across implementations; sorting by key ensures all validators produce
* byte-identical metadata blobs for the same transaction.
*
* Must be called exactly once per `TxMeta` instance, before any call to
* `getAsObject()` or `getJson()`.
*
* @param s Serializer to append the metadata bytes to.
* @param result The `TER` outcome of the transaction.
* @param index The transaction's ordinal position within the closed ledger.
*/
void
addRaw(Serializer&, TER, std::uint32_t index);
/** Assemble the complete metadata as an `STObject`.
*
* Constructs an `sfTransactionMetaData` object containing
* `sfTransactionResult`, `sfTransactionIndex`, `sfAffectedNodes`, and
* optionally `sfDeliveredAmount` and `sfParentBatchID`.
*
* @note Asserts `result_ != 255`; call `addRaw()` first.
* @return Fully populated `STObject` tagged `sfTransactionMetaData`.
*/
[[nodiscard]] STObject
getAsObject() const;
/** Return a mutable reference to the raw affected-nodes array. */
STArray&
getNodes()
{
return nodes_;
}
/** Return a read-only reference to the raw affected-nodes array. */
[[nodiscard]] STArray const&
getNodes() const
{
return nodes_;
}
/** Extract and store optional fields from an existing metadata object.
*
* Reads `sfDeliveredAmount` and `sfParentBatchID` from `obj` when
* present. Called by the deserializing constructors immediately after
* extracting the required fields.
*
* @param obj Source object from which to read optional metadata fields.
*/
void
setAdditionalFields(STObject const& obj)
{
@@ -86,18 +260,40 @@ public:
parentBatchID_ = obj.getFieldH256(sfParentBatchID);
}
/** Return the optional delivered amount recorded for this transaction.
*
* Present only for payment transactions where the amount actually
* delivered may differ from the `Amount` field (e.g. partial payments).
* Absent for all other transaction types.
*/
[[nodiscard]] std::optional<STAmount> const&
getDeliveredAmount() const
{
return deliveredAmount_;
}
/** Set the optional delivered amount for a payment transaction.
*
* Called by `ApplyStateTable` after the payment engine resolves path
* execution. Pass `std::nullopt` to clear a previously set value.
*
* @param amount The actual amount delivered, or `std::nullopt` to clear.
*/
void
setDeliveredAmount(std::optional<STAmount> const& amount)
{
deliveredAmount_ = amount;
}
/** Set the optional parent Batch transaction ID.
*
* Links this transaction's metadata to the enclosing Batch transaction
* (Batch amendment). Propagated from `ApplyContext`, which receives it
* from the outermost batch-processing layer.
*
* @param id The hash of the parent Batch transaction, or `std::nullopt`
* if this transaction is not part of a batch.
*/
void
setParentBatchID(std::optional<uint256> const& id)
{

View File

@@ -2,6 +2,47 @@
namespace xrpl {
enum class TxSearched { All, Some, Unknown };
/** Coverage completeness of a transaction search over the node's local ledger history.
*
* When a client queries for a transaction by hash and the transaction is not
* found, the result is ambiguous: the transaction may genuinely not exist in
* the requested range, or the node may simply lack some of those ledgers
* locally. This enum is the discriminant that resolves that ambiguity.
*
* It is the alternative type in the `std::variant` returned by
* `TransactionMaster::fetch()` and `RelationalDatabase::getTransaction()`:
* when no transaction is found, the variant holds a `TxSearched` value
* encoding both the "not found" signal and the confidence level of that
* determination.
*
* The RPC layer collapses `All` and `Some` into a boolean `searched_all`
* field in the JSON response, and suppresses the field entirely when the
* state is `Unknown`.
*
* @see TransactionMaster::fetch()
* @see RelationalDatabase::getTransaction()
*/
enum class TxSearched {
/** The node searched its entire local history for the requested ledger
* range and confirmed the transaction is absent. The search was
* exhaustive; a client can be confident the transaction was not
* included in that range.
*/
All,
/** The node attempted the search but its local ledger store is
* incomplete for the requested range — some ledgers were missing from
* the database. Absence of the transaction is therefore inconclusive.
*/
Some,
/** Coverage information is unavailable. This occurs when no ledger
* range was specified, a deserialization error occurred, or the
* database layer could not determine search coverage. The
* `searched_all` field is suppressed in the JSON response when this
* state is active.
*/
Unknown,
};
} // namespace xrpl

View File

@@ -1,3 +1,19 @@
/** @file
* Defines the strongly-typed fixed-width integer identifiers used throughout
* the XRPL protocol layer.
*
* Rather than passing bare `uint160` or `uint256` values, this header creates
* a family of mutually-incompatible types via the tag-parameter mechanism of
* `BaseUInt<Bits, Tag>`. A `Currency`, a `NodeID`, and an `AccountID` are
* all 160-bit values at the hardware level but are distinct C++ types, so the
* compiler rejects accidental substitutions at compile time. The tag classes
* themselves contribute no data members and no runtime overhead.
*
* Also declares the three sentinel `Currency` values (`xrpCurrency`,
* `noCurrency`, `badCurrency`) and the string ↔ `Currency` conversion
* functions that implement the XRPL wire-format currency encoding.
*/
#pragma once
#include <xrpl/basics/UnorderedContainers.h>
@@ -8,18 +24,38 @@
namespace xrpl {
namespace detail {
/** Phantom tag type that makes `Currency` a distinct strong type.
*
* Passed as the second template argument to `BaseUInt<160, Tag>` so that
* a 160-bit currency value cannot be silently used where a `NodeID` or raw
* hash is expected, and vice versa. The class has no data members or
* behaviour; it exists only to produce a unique template instantiation.
*/
class CurrencyTag
{
public:
explicit CurrencyTag() = default;
};
/** Phantom tag type that makes `Directory` a distinct strong type.
*
* Passed as the second template argument to `BaseUInt<256, Tag>`. Without
* this tag, a `Directory` and an untagged `uint256` would share the same
* type and be freely interchangeable. The class has no data members or
* behaviour.
*/
class DirectoryTag
{
public:
explicit DirectoryTag() = default;
};
/** Phantom tag type that makes `NodeID` a distinct strong type.
*
* Passed as the second template argument to `BaseUInt<160, Tag>` so that
* a 160-bit validator node ID cannot be silently used where a `Currency` or
* `AccountID` is expected. The class has no data members or behaviour.
*/
class NodeIDTag
{
public:
@@ -28,66 +64,170 @@ public:
} // namespace detail
/** Directory is an index into the directory of offer books.
The last 64 bits of this are the quality. */
/** A 256-bit index into the DEX offer-book directory.
*
* The last 64 bits of this value encode the order-book quality (in big-endian
* byte order), so that the natural `SHAMap` sort order over directory keys
* matches price-ascending order. The `detail::DirectoryTag` phantom type
* makes this a distinct C++ type from untagged `uint256`.
*
* @see keylet::quality(), Quality
*/
using Directory = BaseUInt<256, detail::DirectoryTag>;
/** Currency is a hash representing a specific currency. */
/** A 160-bit identifier for an XRP Ledger currency.
*
* Distinct from `NodeID` and `AccountID` at the C++ type level, even though
* all three are 160-bit values. The canonical encoding of the native asset
* (XRP) is the all-zero value; use `xrpCurrency()` and `isXRP()` rather than
* constructing zero directly. ISO-style 3-character codes occupy bytes 1214
* of the 20-byte wire representation, with all other bytes zero.
*
* @see xrpCurrency(), noCurrency(), badCurrency(), isXRP(), to_string(),
* toCurrency()
*/
using Currency = BaseUInt<160, detail::CurrencyTag>;
/** NodeID is a 160-bit hash representing one node. */
/** A 160-bit identifier for a validator node.
*
* Distinct from `Currency` and `AccountID` at the C++ type level via the
* `detail::NodeIDTag` phantom parameter. Derived from the node's public key
* via SHA-256 + RIPEMD-160 (the same derivation as `AccountID`).
*/
using NodeID = BaseUInt<160, detail::NodeIDTag>;
/** MPTID is a 192-bit value representing MPT Issuance ID,
* which is a concatenation of a 32-bit sequence (big endian)
* and a 160-bit account */
/** A 192-bit identifier for an MPT (Multi-Purpose Token) issuance.
*
* Composed of a 32-bit sequence number (big-endian) in the high bits followed
* by the 160-bit issuer `AccountID`. No tag parameter is used because no
* other 192-bit type exists in the protocol; the added friction of an empty
* tag class is not justified when there is nothing to confuse it with.
*/
using MPTID = BaseUInt<192>;
/** Domain is a 256-bit hash representing a specific domain. */
/** A 256-bit generic domain hash.
*
* No tag parameter is used because there is no other untagged 256-bit type it
* could be confused with in the same contexts; see `Directory` for the tagged
* 256-bit counterpart.
*/
using Domain = BaseUInt<256>;
/** XRP currency. */
/** Return the canonical encoding of XRP, the ledger's native asset.
*
* The all-zero 160-bit value (`beast::kZERO`) is the protocol-defined
* representation of XRP. `isXRP(Currency const&)` tests equality against
* this value. The returned reference points to a function-local static and
* is valid for the lifetime of the process.
*
* @return A `const` reference to the singleton zero `Currency`.
* @see isXRP(Currency const&), noCurrency(), badCurrency()
*/
Currency const&
xrpCurrency();
/** A placeholder for empty currencies. */
/** Return a placeholder sentinel used when no currency is meaningful.
*
* Holds the value `1`. Data structures that require a `Currency` slot but
* have no actual currency to store use this sentinel rather than zero (which
* would be mistaken for XRP). The returned reference is to a function-local
* static valid for the process lifetime.
*
* @return A `const` reference to the singleton `Currency{1}`.
* @see xrpCurrency(), badCurrency()
*/
Currency const&
noCurrency();
/** We deliberately disallow the currency that looks like "XRP" because too
many people were using it instead of the correct XRP currency. */
/** Return the poisoned sentinel for the "XRP as ISO code" misuse.
*
* Early adopters sometimes encoded XRP using the three-letter ISO code
* `"XRP"` (byte value `0x5852500000000000` placed at bytes 1214), rather
* than the correct all-zero form. This sentinel lets code paths detect and
* reject that mistake explicitly. `to_string()` on this value produces a
* 40-character hex string, not `"XRP"`, making the anomaly visible. The
* returned reference is to a function-local static valid for the process
* lifetime.
*
* @return A `const` reference to the singleton `Currency{0x5852500000000000}`.
* @see xrpCurrency(), noCurrency(), toCurrency()
*/
Currency const&
badCurrency();
/** Test whether a `Currency` represents the native XRP asset.
*
* @param c The currency to test.
* @return `true` if `c` equals `beast::kZERO` (i.e., equals `xrpCurrency()`).
*/
inline bool
isXRP(Currency const& c)
{
return c == beast::kZERO;
}
/** Returns "", "XRP", or three letter ISO code. */
/** Convert a `Currency` to its canonical string representation.
*
* Applies a priority-ordered decision tree:
* - Zero value → `"XRP"` (native asset).
* - `noCurrency()` → `"1"`.
* - All non-ISO bytes zero and the 3-character code in the allowed character
* set (alphanumeric plus `<>(){}[]|?!@#$%^&*`) and not equal to `"XRP"` →
* the three-character string.
* - Everything else → a 40-character uppercase hex string.
*
* @param c The currency value to convert.
* @return A string in one of the four forms described above.
* @note A currency whose ISO bytes spell `"XRP"` but whose surrounding bytes
* are non-zero is returned as hex, making the anomaly visible rather than
* silently aliasing to the native currency.
*/
std::string
to_string(Currency const& c);
/** Tries to convert a string to a Currency, returns true on success.
@note This function will return success if the resulting currency is
badCurrency(). This legacy behavior is unfortunate; changing this
will require very careful checking everywhere and may mean having
to rewrite some unit test code.
*/
/** Parse a string into a `Currency`, reporting success via the return value.
*
* Accepted forms:
* - Empty string or `"XRP"` → sets `currency` to `xrpCurrency()` (all zero).
* - Exactly 3 characters from the XRPL ISO character set → zero-pads and
* writes the code at byte offset 12 of `currency`.
* - 40-character hex string → parsed directly into `currency`.
* - Anything else → returns `false` and leaves `currency` unmodified.
*
* @param currency Output: receives the parsed `Currency` on success.
* @param code The string to parse.
* @return `true` if parsing succeeded; `false` otherwise.
* @note Returns `true` even when the result equals `badCurrency()`. This
* legacy behaviour is preserved to avoid breaking existing callers;
* validate the output value separately if `badCurrency()` must be
* excluded.
*/
bool
toCurrency(Currency&, std::string const&);
/** Tries to convert a string to a Currency, returns noCurrency() on failure.
@note This function can return badCurrency(). This legacy behavior is
unfortunate; changing this will require very careful checking
everywhere and may mean having to rewrite some unit test code.
*/
/** Parse a string into a `Currency`, returning `noCurrency()` on failure.
*
* Convenience wrapper around `toCurrency(Currency&, std::string const&)`.
* Prefer the two-argument overload when the caller must distinguish a parse
* failure from a legitimately absent currency.
*
* @param code The string to parse.
* @return The parsed `Currency`, or `noCurrency()` if the string is invalid.
* @note Can return `badCurrency()` for hex input that encodes that sentinel.
* This legacy behaviour is preserved; callers should validate the result
* if they need to exclude `badCurrency()`.
*/
Currency
toCurrency(std::string const&);
/** Write the string representation of a `Currency` to an output stream.
*
* Delegates to `to_string(Currency const&)`.
*
* @param os The stream to write to.
* @param x The currency to format.
* @return `os`, to allow chaining.
*/
inline std::ostream&
operator<<(std::ostream& os, Currency const& x)
{
@@ -99,24 +239,48 @@ operator<<(std::ostream& os, Currency const& x)
namespace std {
/** `std::hash` specialization for `xrpl::Currency`.
*
* Inherits the `hasher` inner type provided by `BaseUInt`, enabling
* `Currency` to be used directly as a key in `std::unordered_map` and
* `std::unordered_set`.
*/
template <>
struct hash<xrpl::Currency> : xrpl::Currency::hasher
{
hash() = default;
};
/** `std::hash` specialization for `xrpl::NodeID`.
*
* Inherits the `hasher` inner type provided by `BaseUInt`, enabling
* `NodeID` to be used directly as a key in `std::unordered_map` and
* `std::unordered_set`.
*/
template <>
struct hash<xrpl::NodeID> : xrpl::NodeID::hasher
{
hash() = default;
};
/** `std::hash` specialization for `xrpl::Directory`.
*
* Inherits the `hasher` inner type provided by `BaseUInt`, enabling
* `Directory` to be used directly as a key in `std::unordered_map` and
* `std::unordered_set`.
*/
template <>
struct hash<xrpl::Directory> : xrpl::Directory::hasher
{
hash() = default;
};
/** `std::hash` specialization for `xrpl::uint256`.
*
* Inherits the `hasher` inner type provided by `BaseUInt`. Included here
* because `uint256` is used in the same contexts as `Currency`, `NodeID`,
* and `Directory`, even though it is not exclusively defined in this file.
*/
template <>
struct hash<xrpl::uint256> : xrpl::uint256::hasher
{

View File

@@ -1,3 +1,12 @@
/** @file
* Type-safe unit arithmetic for the XRP Ledger.
*
* Defines the `ValueUnit<UnitTag, T>` phantom-typed wrapper and its concrete
* aliases (`FeeLevel64`, `Bips32`, etc.) that prevent silent mixing of
* conceptually different integer quantities (drops, fee levels, basis points).
* Also provides `mulDiv`, an overflow-safe scaled-arithmetic helper that
* preserves unit constraints across cross-type computations.
*/
#pragma once
#include <xrpl/basics/safe_cast.h>
@@ -16,35 +25,59 @@ namespace xrpl {
namespace unit {
/** "drops" are the smallest divisible amount of XRP. This is what most
of the code uses. */
/** Tag type identifying a quantity measured in drops (the smallest XRP unit).
*
* Used as the `UnitTag` parameter of `ValueUnit` to produce drop-valued
* types. `XRPAmount` (defined in `XRPAmount.h`) is the primary alias.
*/
struct dropTag;
/** "fee levels" are used by the transaction queue to compare the relative
cost of transactions that require different levels of effort to process.
See also: src/xrpld/app/misc/FeeEscalation.md#fee-level */
/** Tag type identifying a fee-level quantity used by the transaction queue.
*
* Fee levels are dimensionless ratios that let the queue compare the relative
* cost of transactions with different processing effort.
* @see src/xrpld/app/misc/FeeEscalation.md#fee-level
*/
struct feelevelTag;
/** unitless values are plain scalars wrapped in a ValueUnit. They are
used for calculations in this header. */
/** Tag type for plain scalars participating in unit-checked arithmetic.
*
* Raw `uint64_t` arguments passed to `mulDiv` are wrapped in
* `ValueUnit<unitlessTag, uint64_t>` via `unit::scalar()` so the inner
* `mulDivU` implementation only needs to handle the typed case.
*/
struct unitlessTag;
/** Units to represent basis points (bips) and 1/10 basis points */
/** Tag type identifying a quantity measured in basis points (1/100 of 1%). */
class BipsTag;
/** Tag type identifying a quantity measured in tenth-basis-points (1/1000 of 1%). */
class TenthBipsTag;
// These names don't have to be too descriptive, because we're in the "unit"
// namespace.
/** Concept satisfied by any `ValueUnit` specialization.
*
* Requires the nested `unit_type` and `value_type` aliases that every
* `ValueUnit` exposes. Used as the base constraint for all other
* unit-system concepts.
*
* @tparam T The type to check.
*/
template <class T>
concept Valid = std::is_class_v<T> && std::is_object_v<typename T::unit_type> &&
std::is_object_v<typename T::value_type>;
/** `Usable` is checked to ensure that only values with
known valid type tags can be used (sometimes transparently) in
non-unit contexts. At the time of implementation, this includes
all known tags, but more may be added in the future, and they
should not be added automatically unless determined to be
appropriate.
*/
/** Concept satisfied by `ValueUnit` types whose tag is an approved known unit.
*
* Acts as an explicit whitelist: new tags do not automatically gain access
* to JSON serialisation (`jsonClipped`) or other non-unit-domain operations.
* Currently admits `dropTag`, `feelevelTag`, `unitlessTag`, `BipsTag`, and
* `TenthBipsTag`.
*
* @tparam T The type to check.
*/
template <class T>
concept Usable = Valid<T> &&
(std::is_same_v<typename T::unit_type, feelevelTag> ||
@@ -53,21 +86,73 @@ concept Usable = Valid<T> &&
std::is_same_v<typename T::unit_type, BipsTag> ||
std::is_same_v<typename T::unit_type, TenthBipsTag>);
/** Concept satisfied when `Other` is an arithmetic type convertible to `VU`'s
* value type.
*
* Enables cross-scalar arithmetic on a `ValueUnit` (e.g. multiplying a
* `FeeLevel64` by a raw `uint64_t`).
*
* @tparam Other The scalar type to check.
* @tparam VU The `ValueUnit` specialization to check compatibility with.
*/
template <class Other, class VU>
concept Compatible =
Valid<VU> && std::is_arithmetic_v<Other> && std::is_arithmetic_v<typename VU::value_type> &&
std::is_convertible_v<Other, typename VU::value_type>;
/** Concept satisfied when `T` is an integral type. */
template <class T>
concept Integral = std::is_integral_v<T>;
/** Concept satisfied when a `ValueUnit`'s value type is integral.
*
* Required for modulo (`%=`) and cast operations.
*
* @tparam VU The `ValueUnit` specialization to check.
*/
template <class VU>
concept IntegralValue = Integral<typename VU::value_type>;
/** Concept satisfied when two `ValueUnit` types share the same unit tag and
* both have integral value types.
*
* Guards `safe_cast` and `unsafe_cast` between `ValueUnit` specializations
* (e.g. `FeeLevel<uint32_t>` to `FeeLevel64`).
*
* @tparam VU1 Source `ValueUnit` type.
* @tparam VU2 Destination `ValueUnit` type.
*/
template <class VU1, class VU2>
concept CastableValue = IntegralValue<VU1> && IntegralValue<VU2> &&
std::is_same_v<typename VU1::unit_type, typename VU2::unit_type>;
/** Phantom-typed wrapper that attaches a compile-time unit tag to an
* arithmetic value, preventing silent mixing of incompatible quantities.
*
* The `UnitTag` type is never instantiated; it only serves as a type-system
* discriminator. Arithmetic operators are generated by `boost::operators`
* mixins from a small set of explicitly defined primitives:
*
* - **Addition / subtraction** between two `ValueUnit` of the same unit, or
* between a `ValueUnit` and a raw scalar, preserve the unit.
* - **Multiplication** by a raw scalar preserves the unit (commutativity is
* handled by a `friend operator*`).
* - **Division** of two same-unit `ValueUnit`s returns the raw `value_type`
* (a dimensionless ratio); division by a scalar preserves the unit.
* - **Negation** is compile-time-rejected for unsigned value types via
* `static_assert`.
*
* A `ValueUnit<Unit, Wide>` can be implicitly constructed from
* `ValueUnit<Unit, Narrow>` when `SafeToCast<Narrow, Wide>` is satisfied
* (same-sign widening). The reverse requires `safe_cast` or `unsafe_cast`.
*
* Integrates with `beast::Zero`: an explicit constructor from `beast::Zero`
* and `signum()` let the beast zero-comparison machinery generate all six
* relational operators against zero without a temporary allocation.
*
* @tparam UnitTag An empty tag type (e.g. `dropTag`, `feelevelTag`).
* @tparam T The underlying arithmetic value type.
*/
template <class UnitTag, class T>
class ValueUnit : private boost::totally_ordered<ValueUnit<UnitTag, T>>,
private boost::additive<ValueUnit<UnitTag, T>>,
@@ -89,10 +174,12 @@ public:
constexpr ValueUnit&
operator=(ValueUnit const& other) = default;
/** Construct a zero-valued unit, integrating with `beast::Zero`. */
constexpr explicit ValueUnit(beast::Zero) : value_(0)
{
}
/** Assign zero, integrating with `beast::Zero`. */
constexpr ValueUnit&
operator=(beast::Zero)
{
@@ -100,10 +187,20 @@ public:
return *this;
}
/** Construct from a raw value of the underlying arithmetic type.
*
* Explicit to prevent accidental construction from unrelated integers.
*
* @param value The raw value to wrap.
*/
constexpr explicit ValueUnit(value_type value) : value_(value)
{
}
/** Assign from a raw value of the underlying arithmetic type.
*
* @param value The raw value to wrap.
*/
constexpr ValueUnit&
operator=(value_type value)
{
@@ -111,9 +208,16 @@ public:
return *this;
}
/** Instances with the same unit, and a type that is
"safe" to convert to this one can be converted
implicitly */
/** Implicit widening conversion from a narrower same-unit `ValueUnit`.
*
* Enabled only when `SafeToCast<Other, value_type>` is satisfied, i.e.
* same signedness and `value_type` is at least as wide as `Other`.
* This allows e.g. `FeeLevel<uint32_t>` to widen to `FeeLevel64`
* without an explicit cast, mirroring ordinary C++ arithmetic widening.
*
* @tparam Other The narrower underlying type of the source.
* @param value The source `ValueUnit` to convert from.
*/
template <Compatible<ValueUnit> Other>
constexpr ValueUnit(ValueUnit<unit_type, Other> const& value)
requires SafeToCast<Other, value_type>
@@ -121,12 +225,23 @@ public:
{
}
/** Add a raw scalar to this unit value, preserving the unit.
*
* @param rhs Scalar to add.
* @return A new `ValueUnit` of the same unit.
*/
constexpr ValueUnit
operator+(value_type const& rhs) const
{
return ValueUnit{value_ + rhs};
}
/** Add a `ValueUnit` to a raw scalar (commutative form).
*
* @param lhs Scalar left-hand side.
* @param rhs `ValueUnit` right-hand side.
* @return A new `ValueUnit` of the same unit.
*/
friend constexpr ValueUnit
operator+(value_type lhs, ValueUnit const& rhs)
{
@@ -134,12 +249,25 @@ public:
return rhs + lhs;
}
/** Subtract a raw scalar from this unit value, preserving the unit.
*
* @param rhs Scalar to subtract.
* @return A new `ValueUnit` of the same unit.
*/
constexpr ValueUnit
operator-(value_type const& rhs) const
{
return ValueUnit{value_ - rhs};
}
/** Subtract a `ValueUnit` from a raw scalar.
*
* Computed as `lhs + (-rhs)` so that addition commutativity applies.
*
* @param lhs Scalar left-hand side.
* @param rhs `ValueUnit` to subtract.
* @return A new `ValueUnit` of the same unit.
*/
friend constexpr ValueUnit
operator-(value_type lhs, ValueUnit const& rhs)
{
@@ -148,12 +276,23 @@ public:
return -rhs + lhs;
}
/** Scale this unit value by a raw scalar, preserving the unit.
*
* @param rhs Scalar multiplier.
* @return A new `ValueUnit` of the same unit.
*/
constexpr ValueUnit
operator*(value_type const& rhs) const
{
return ValueUnit{value_ * rhs};
}
/** Scale a `ValueUnit` by a raw scalar (commutative form).
*
* @param lhs Scalar left-hand side.
* @param rhs `ValueUnit` right-hand side.
* @return A new `ValueUnit` of the same unit.
*/
friend constexpr ValueUnit
operator*(value_type lhs, ValueUnit const& rhs)
{
@@ -161,6 +300,13 @@ public:
return rhs * lhs;
}
/** Divide two same-unit values to produce a dimensionless raw ratio.
*
* Same-unit / same-unit = unitless scalar (e.g. drops / drops = 1).
*
* @param rhs The divisor.
* @return The raw `value_type` quotient.
*/
constexpr value_type
operator/(ValueUnit const& rhs) const
{
@@ -209,6 +355,11 @@ public:
return *this;
}
/** Modulo-assign by a raw scalar. Only available when value type is integral.
*
* @tparam Transparent Deduced; constrains the overload to integral value types.
* @param rhs The divisor for the modulo operation.
*/
template <Integral Transparent = value_type>
ValueUnit&
operator%=(value_type const& rhs)
@@ -217,6 +368,12 @@ public:
return *this;
}
/** Negate this value.
*
* @note Fails to compile with a `static_assert` when `T` is unsigned,
* preventing silent integer wrapping on types like `uint64_t`.
* @return A new `ValueUnit` holding `-value_`.
*/
ValueUnit
operator-() const
{
@@ -256,14 +413,23 @@ public:
return value_ < other.value_;
}
/** Returns true if the amount is not zero */
/** Returns `true` when the stored value is non-zero.
*
* Allows `if (amount)` idiom analogous to pointer truthiness.
*/
explicit constexpr
operator bool() const noexcept
{
return value_ != 0;
}
/** Return the sign of the amount */
/** Returns the sign of the stored value as -1, 0, or +1.
*
* Used by the `beast::Zero` comparison infrastructure to generate all
* six relational operators against `zero` without a temporary allocation.
*
* @return -1 if negative, 0 if zero, +1 if positive.
*/
[[nodiscard]] constexpr int
signum() const noexcept
{
@@ -272,7 +438,15 @@ public:
return value_ ? 1 : 0;
}
/** Returns the number of drops */
/** Returns the raw underlying value.
*
* Named `fee()` for historical reasons; intended to migrate to a
* dedicated tagged-fee class.
*
* @note Prefer `value()` in generic code. This accessor exists for
* call sites that treat a `ValueUnit` specifically as a fee amount.
* @return The raw `value_type`.
*/
// TODO: Move this to a new class, maybe with the old "TaggedFee" name
[[nodiscard]] constexpr value_type
fee() const
@@ -280,6 +454,15 @@ public:
return value_;
}
/** Express this value as a decimal fraction of a reference value.
*
* Used to produce load-factor metrics for the JSON RPC layer (e.g.
* `openLedgerFeeLevel / referenceFeeLevel`).
*
* @tparam Other The underlying type of the reference (must share this unit).
* @param reference The denominator value; behaviour is undefined if zero.
* @return `static_cast<double>(value_) / reference.value()`.
*/
template <class Other>
[[nodiscard]] constexpr double
decimalFromReference(ValueUnit<unit_type, Other> reference) const
@@ -287,10 +470,19 @@ public:
return static_cast<double>(value_) / reference.value();
}
// `Usable` is checked to ensure that only values with
// known valid type tags can be converted to JSON. At the time
// of implementation, that includes all known tags, but more may
// be added in the future.
/** Convert this value to a `Json::Value`, clamping to the JSON integer range.
*
* For integral value types, the result is clamped to
* `[numeric_limits<json::Int>::min(), numeric_limits<json::UInt>::max()]`
* to avoid JSON serialization overflow (JSON integers are 32-bit on many
* platforms). For floating-point value types, the value is returned
* as-is.
*
* Only available for `Usable` unit tags (explicit whitelist), so that
* internal intermediate types cannot be accidentally exposed via RPC.
*
* @return A `Json::Value` holding the clamped integer or double.
*/
[[nodiscard]] json::Value
jsonClipped() const
requires Usable<ValueUnit>
@@ -315,16 +507,20 @@ public:
}
}
/** Returns the underlying value. Code SHOULD NOT call this
function unless the type has been abstracted away,
e.g. in a templated function.
*/
/** Returns the underlying raw value.
*
* @note Prefer named accessors at call sites where the unit is known.
* This function is intended for generic (templated) code where the
* unit type has been abstracted away.
* @return The stored `value_type`.
*/
[[nodiscard]] constexpr value_type
value() const
{
return value_;
}
/** Read a raw value from a stream into this `ValueUnit`. */
friend std::istream&
operator>>(std::istream& s, ValueUnit& val)
{
@@ -333,7 +529,11 @@ public:
}
};
// Output Values as just their numeric value.
/** Write the raw numeric value of a `ValueUnit` to an output stream.
*
* The unit tag is not written; the output is identical to streaming the
* underlying `value_type`.
*/
template <class Char, class Traits, class UnitTag, class T>
std::basic_ostream<Char, Traits>&
operator<<(std::basic_ostream<Char, Traits>& os, ValueUnit<UnitTag, T> const& q)
@@ -341,6 +541,13 @@ operator<<(std::basic_ostream<Char, Traits>& os, ValueUnit<UnitTag, T> const& q)
return os << q.value();
}
/** Convert a `ValueUnit` to a `std::string` representation of its raw value.
*
* Equivalent to `std::to_string(amount.value())`.
*
* @param amount The value to convert.
* @return A string representation of the underlying value.
*/
template <class UnitTag, class T>
std::string
to_string(ValueUnit<UnitTag, T> const& amount)
@@ -348,27 +555,80 @@ to_string(ValueUnit<UnitTag, T> const& amount)
return std::to_string(amount.value());
}
/** Concept satisfied by a `ValueUnit` whose value type is convertible to
* `uint64_t`. Constrains `mulDiv` source arguments.
*
* @tparam Source The `ValueUnit` type to check.
*/
template <class Source>
concept muldivSource =
Valid<Source> && std::is_convertible_v<typename Source::value_type, std::uint64_t>;
/** Concept satisfied by a `ValueUnit` that can also receive the result of a
* `mulDiv` computation.
*
* In addition to satisfying `muldivSource`, the destination type must accept
* a `uint64_t` (the intermediate quotient) and its value type must be at
* least 64 bits wide.
*
* @tparam Dest The candidate destination `ValueUnit` type.
*/
template <class Dest>
concept muldivDest = muldivSource<Dest> && // Dest is also a source
std::is_convertible_v<std::uint64_t, typename Dest::value_type> &&
sizeof(typename Dest::value_type) >= sizeof(std::uint64_t);
/** Concept satisfied when two `ValueUnit` types are both valid `mulDiv`
* sources with the same unit tag.
*
* Ensures the numerator (`value`) and denominator (`div`) of a `mulDiv`
* call are dimensionally compatible.
*
* @tparam Source2 The type of the divisor argument.
* @tparam Source1 The type of the value argument.
*/
template <class Source2, class Source1>
concept muldivSources = muldivSource<Source1> && muldivSource<Source2> &&
std::is_same_v<typename Source1::unit_type, typename Source2::unit_type>;
/** Concept satisfied when a `mulDiv` call is well-typed: both source
* arguments share a unit tag and the destination satisfies `muldivDest`.
*
* Source and `Dest` may share the same unit (same-unit scaling).
*
* @tparam Dest The result `ValueUnit` type.
* @tparam Source1 The value argument type.
* @tparam Source2 The divisor argument type.
*/
template <class Dest, class Source1, class Source2>
concept muldivable = muldivSources<Source1, Source2> && muldivDest<Dest>;
// Source and Dest can be the same by default
/** Concept satisfied when a `mulDiv` overload may reorder arguments
* (commuted form).
*
* Extends `muldivable` with the additional constraint that `Dest`'s unit
* tag differs from the source unit tags. This prevents the commuted
* overload from being ambiguous with the non-commuted one when all three
* types share the same unit.
*
* @tparam Dest The result `ValueUnit` type.
* @tparam Source1 One source argument type.
* @tparam Source2 The other source argument type.
*/
template <class Dest, class Source1, class Source2>
concept muldivCommutable = muldivable<Dest, Source1, Source2> &&
!std::is_same_v<typename Source1::unit_type, typename Dest::unit_type>;
/** Wrap a raw scalar in a `unitlessTag` `ValueUnit` for use with `mulDivU`.
*
* Raw integers cannot be passed directly to `mulDivU`, which requires typed
* arguments. `scalar()` provides the necessary wrapping without introducing
* a named unit.
*
* @tparam T The scalar arithmetic type.
* @param value The value to wrap.
* @return `ValueUnit<unitlessTag, T>{value}`.
*/
template <class T>
ValueUnit<unitlessTag, T>
scalar(T value)
@@ -376,6 +636,27 @@ scalar(T value)
return ValueUnit<unitlessTag, T>{value};
}
/** Core overflow-safe multiply-then-divide for typed `ValueUnit` arguments.
*
* Computes `(value * mul) / div` using a `boost::multiprecision::uint128_t`
* intermediate to prevent overflow on the product. Two fast paths avoid the
* 128-bit multiply when `value == div` (returns `mul`) or `mul == div`
* (returns `value` after a range check).
*
* All inputs must be non-negative; negative inputs trigger `XRPL_ASSERT` in
* addition to returning `std::nullopt`.
*
* The public `mulDiv` overloads in the `xrpl` namespace forward to this
* function after normalising raw scalars with `unit::scalar()`.
*
* @tparam Source1 Value and divisor unit type (must share a unit tag).
* @tparam Source2 Divisor unit type (same constraint as Source1).
* @tparam Dest Result unit type; must satisfy `muldivable<Source1, Source2>`.
* @param value The value to scale.
* @param mul The multiplier (in `Dest`'s unit).
* @param div The divisor (in `Source`'s unit).
* @return The scaled result, or `std::nullopt` on overflow or negative input.
*/
template <class Source1, class Source2, unit::muldivable<Source1, Source2> Dest>
std::optional<Dest>
mulDivU(Source1 value, Dest mul, Source2 div)
@@ -422,22 +703,69 @@ mulDivU(Source1 value, Dest mul, Source2 div)
} // namespace unit
// Fee Levels
/** Fee level as a `ValueUnit` parameterised on the underlying arithmetic type.
*
* Fee levels are dimensionless ratios used by the transaction queue to compare
* relative transaction cost independent of processing effort.
*
* @tparam T The underlying arithmetic type (`uint64_t`, `double`, etc.).
*/
template <class T>
using FeeLevel = unit::ValueUnit<unit::feelevelTag, T>;
/** Fee level backed by a 64-bit unsigned integer. The primary fee-level type
* used by `TxQ` and the load-fee tracker. */
using FeeLevel64 = FeeLevel<std::uint64_t>;
/** Fee level backed by a `double`. Used where a fractional fee-level ratio
* must be represented (e.g. load-factor RPC fields). */
using FeeLevelDouble = FeeLevel<double>;
// Basis points (Bips)
/** Basis-point quantity parameterised on the underlying type.
*
* One basis point = 1/100 of one percent.
*
* @tparam T The underlying arithmetic type.
*/
template <class T>
using Bips = unit::ValueUnit<unit::BipsTag, T>;
/** Basis points as a 16-bit unsigned integer. */
using Bips16 = Bips<std::uint16_t>;
/** Basis points as a 32-bit unsigned integer. */
using Bips32 = Bips<std::uint32_t>;
/** Tenth-of-a-basis-point quantity parameterised on the underlying type.
*
* One tenth-bip = 1/1000 of one percent. Used for interest rates and
* management fees in the lending protocol where basis-point granularity
* is insufficient.
*
* @tparam T The underlying arithmetic type.
*/
template <class T>
using TenthBips = unit::ValueUnit<unit::TenthBipsTag, T>;
/** Tenth-basis-points as a 16-bit unsigned integer. */
using TenthBips16 = TenthBips<std::uint16_t>;
/** Tenth-basis-points as a 32-bit unsigned integer. */
using TenthBips32 = TenthBips<std::uint32_t>;
/** Compute `(value * mul) / div` with overflow protection, preserving unit types.
*
* Delegates to `unit::mulDivU`. Both `value` and `div` must share a unit
* tag; `mul` carries the destination unit. All inputs must be non-negative.
*
* @tparam Source1 Type of `value` and `div` (same unit tag required).
* @tparam Source2 Type of `div` (same unit tag as Source1).
* @tparam Dest Type of `mul` and the return value.
* @param value The value to scale.
* @param mul The multiplier in `Dest`'s unit.
* @param div The divisor in `Source`'s unit.
* @return The scaled result, or `std::nullopt` on overflow or negative input.
*/
template <class Source1, class Source2, unit::muldivable<Source1, Source2> Dest>
std::optional<Dest>
mulDiv(Source1 value, Dest mul, Source2 div)
@@ -445,6 +773,21 @@ mulDiv(Source1 value, Dest mul, Source2 div)
return unit::mulDivU(value, mul, div);
}
/** Compute `(value * mul) / div` with the `value` and `mul` arguments swapped
* (commuted form for cross-unit conversions).
*
* This overload is selected when `Dest`'s unit tag differs from the source
* unit tags, avoiding ambiguity with the non-commuted form. It reorders
* the arguments to `unit::mulDivU` rather than duplicating the implementation.
*
* @tparam Source1 Type of `mul` (same unit tag as Source2).
* @tparam Source2 Type of `div` (same unit tag as Source1).
* @tparam Dest Type of `value` and the return value (different unit from sources).
* @param value The value to scale (in `Dest`'s unit).
* @param mul The multiplier (in the source unit).
* @param div The divisor (in the source unit).
* @return The scaled result, or `std::nullopt` on overflow or negative input.
*/
template <class Source1, class Source2, unit::muldivCommutable<Source1, Source2> Dest>
std::optional<Dest>
mulDiv(Dest value, Source1 mul, Source2 div)
@@ -453,6 +796,17 @@ mulDiv(Dest value, Source1 mul, Source2 div)
return unit::mulDivU(mul, value, div);
}
/** Compute `(value * mul) / div` when both scalars are raw `uint64_t`.
*
* Wraps `value` and `div` in `unit::scalar()` so the typed `mulDivU`
* path handles them uniformly.
*
* @tparam Dest The typed destination unit.
* @param value Raw scalar numerator.
* @param mul Typed multiplier (carries the destination unit).
* @param div Raw scalar divisor.
* @return The scaled result, or `std::nullopt` on overflow.
*/
template <unit::muldivDest Dest>
std::optional<Dest>
mulDiv(std::uint64_t value, Dest mul, std::uint64_t div)
@@ -462,6 +816,15 @@ mulDiv(std::uint64_t value, Dest mul, std::uint64_t div)
return unit::mulDivU(unit::scalar(value), mul, unit::scalar(div));
}
/** Compute `(value * mul) / div` when both scalars are raw `uint64_t`
* and `value` carries the destination unit (commuted form).
*
* @tparam Dest The typed destination unit.
* @param value Typed value (carries the destination unit).
* @param mul Raw scalar multiplier.
* @param div Raw scalar divisor.
* @return The scaled result, or `std::nullopt` on overflow.
*/
template <unit::muldivDest Dest>
std::optional<Dest>
mulDiv(Dest value, std::uint64_t mul, std::uint64_t div)
@@ -470,6 +833,19 @@ mulDiv(Dest value, std::uint64_t mul, std::uint64_t div)
return mulDiv(mul, value, div);
}
/** Compute `(value * mul) / div` returning a raw `uint64_t` when `mul` is
* a raw scalar.
*
* Wraps `mul` in `unit::scalar()` and unwraps the typed result, allowing
* callers to stay in the untyped domain for the output.
*
* @tparam Source1 Type of `value` (must satisfy `muldivSource`).
* @tparam Source2 Type of `div` (same unit tag as Source1).
* @param value Typed numerator value.
* @param mul Raw scalar multiplier.
* @param div Typed divisor (same unit as `value`).
* @return The raw quotient, or `std::nullopt` on overflow or negative input.
*/
template <unit::muldivSource Source1, unit::muldivSources<Source1> Source2>
std::optional<std::uint64_t>
mulDiv(Source1 value, std::uint64_t mul, Source2 div)
@@ -484,6 +860,16 @@ mulDiv(Source1 value, std::uint64_t mul, Source2 div)
return unitresult->value();
}
/** Compute `(value * mul) / div` returning a raw `uint64_t` when `value` is
* a raw scalar (commuted form of the scalar-`mul` overload).
*
* @tparam Source1 Type of `mul` (must satisfy `muldivSource`).
* @tparam Source2 Type of `div` (same unit tag as Source1).
* @param value Raw scalar numerator.
* @param mul Typed multiplier.
* @param div Typed divisor (same unit as `mul`).
* @return The raw quotient, or `std::nullopt` on overflow or negative input.
*/
template <unit::muldivSource Source1, unit::muldivSources<Source1> Source2>
std::optional<std::uint64_t>
mulDiv(std::uint64_t value, Source1 mul, Source2 div)
@@ -492,6 +878,17 @@ mulDiv(std::uint64_t value, Source1 mul, Source2 div)
return mulDiv(mul, value, div);
}
/** Checked narrowing or widening cast between two `ValueUnit`s of the same unit.
*
* Unwraps `s`, applies the scalar `safeCast` to the underlying value, and
* rewraps the result in `Dest`. Requires that both types share a unit tag
* and have integral value types (`CastableValue` concept).
*
* @tparam Dest The destination `ValueUnit` type.
* @tparam Src The source `ValueUnit` type (same unit tag, integral value).
* @param s The source value.
* @return `Dest` holding the safely cast value.
*/
template <unit::IntegralValue Dest, unit::CastableValue<Dest> Src>
constexpr Dest
safeCast(Src s) noexcept
@@ -500,6 +897,15 @@ safeCast(Src s) noexcept
return Dest{safeCast<typename Dest::value_type>(s.value())};
}
/** Checked cast from a raw integral scalar into a `ValueUnit`.
*
* Applies the scalar `safeCast` to `s` and wraps the result in `Dest`.
*
* @tparam Dest The destination `ValueUnit` type (must have integral value type).
* @tparam Src An integral scalar type.
* @param s The raw scalar to cast.
* @return `Dest` holding the safely cast value.
*/
template <unit::IntegralValue Dest, unit::Integral Src>
constexpr Dest
safeCast(Src s) noexcept
@@ -508,6 +914,17 @@ safeCast(Src s) noexcept
return Dest{safeCast<typename Dest::value_type>(s)};
}
/** Unchecked narrowing cast between two `ValueUnit`s of the same unit.
*
* Equivalent to `safeCast` but asserts at compile time that the cast is not
* trivially safe, preventing misuse as a drop-in replacement for `safeCast`.
* Use only when the value is known by contract to fit in `Dest`.
*
* @tparam Dest The destination `ValueUnit` type.
* @tparam Src The source `ValueUnit` type (same unit tag, integral value).
* @param s The source value.
* @return `Dest` holding the unsafely cast value.
*/
template <unit::IntegralValue Dest, unit::CastableValue<Dest> Src>
constexpr Dest
unsafeCast(Src s) noexcept
@@ -516,6 +933,15 @@ unsafeCast(Src s) noexcept
return Dest{unsafeCast<typename Dest::value_type>(s.value())};
}
/** Unchecked cast from a raw integral scalar into a `ValueUnit`.
*
* Use only when the value is known by contract to fit in `Dest`'s value type.
*
* @tparam Dest The destination `ValueUnit` type (must have integral value type).
* @tparam Src An integral scalar type.
* @param s The raw scalar to cast.
* @return `Dest` holding the unsafely cast value.
*/
template <unit::IntegralValue Dest, unit::Integral Src>
constexpr Dest
unsafeCast(Src s) noexcept

View File

@@ -1,3 +1,20 @@
/** @file
* Type system foundation for XRPL's cross-chain bridge attestation protocol.
*
* Witness servers attest to cross-chain transfer events by constructing and
* signing objects from the `Attestations` namespace, then submitting them to
* the destination chain. The destination chain stores a stripped, signature-
* free form directly in ledger objects. Two parallel hierarchies serve these
* roles:
*
* - `Attestations::AttestationClaim` / `AttestationCreateAccount` — full
* witness-submitted proofs including raw signature bytes.
* - `XChainClaimAttestation` / `XChainCreateAccountAttestation` — on-ledger
* storage forms retaining only the signer key identity and event fields.
*
* @see STXChainBridge
*/
#pragma once
#include <xrpl/basics/Buffer.h>
@@ -22,25 +39,52 @@ namespace xrpl {
namespace Attestations {
/** Common fields shared by all witness-server attestation types.
*
* Holds the signer identity, raw signature, source account, amount, reward
* account, and transfer direction for a single cross-chain event attestation.
* Subclasses add the event-specific identifier (`claimID` or `createCount`)
* and supply the virtual `message()` implementation that determines the exact
* bytes that were signed.
*
* @note This type carries the raw `signature` bytes and is only used in the
* witness-submission path. On-ledger storage uses the signature-free
* `XChainClaimAttestation` / `XChainCreateAccountAttestation` types.
*/
struct AttestationBase
{
// Account associated with the public key
/** Account ID associated with the signer's public key. */
AccountID attestationSignerAccount;
// Public key from the witness server attesting to the event
/** Public key used by the witness server to sign the attestation. */
PublicKey publicKey;
// Signature from the witness server attesting to the event
/** Raw cryptographic signature over the canonical message bytes. */
Buffer signature;
// Account on the sending chain that triggered the event (sent the
// transaction)
/** Account on the sending chain that originated the cross-chain transfer. */
AccountID sendingAccount;
// Amount transferred on the sending chain
/** Amount transferred on the sending chain. */
STAmount sendingAmount;
// Account on the destination chain that collects a share of the attestation
// reward
/** Destination-chain account that collects this witness's reward share. */
AccountID rewardAccount;
// Amount was transferred on the locking chain
/** `true` if the transfer was initiated on the locking chain. */
bool wasLockingChainSend;
/** Construct from individual field values supplied by the witness server.
*
* @param attestationSignerAccount Account ID for the signer's public key.
* @param publicKey Witness server's public key.
* @param signature Pre-computed signature over the canonical message.
* @param sendingAccount Source account on the sending chain.
* @param sendingAmount Amount transferred on the sending chain.
* @param rewardAccount Destination-chain account receiving the reward share.
* @param wasLockingChainSend `true` if the transfer originated on the
* locking chain.
*/
explicit AttestationBase(
AccountID attestationSignerAccount,
PublicKey const& publicKey,
@@ -57,34 +101,122 @@ struct AttestationBase
AttestationBase&
operator=(AttestationBase const&) = default;
// verify that the signature attests to the data.
/** Cryptographically verify the witness signature against the stored fields.
*
* Re-derives the canonical message bytes via the virtual `message()` call,
* then checks them against `publicKey` and `signature`.
*
* @param bridge The bridge this attestation relates to; included in the
* signed payload to scope the proof to a specific bridge instance.
* @return `true` if the signature is valid.
*/
[[nodiscard]] bool
verify(STXChainBridge const& bridge) const;
protected:
/** Deserialize from a ledger `STObject`. */
explicit AttestationBase(STObject const& o);
/** Deserialize from a JSON value.
*
* @throws std::runtime_error if any required field is missing or has the
* wrong type.
*/
explicit AttestationBase(json::Value const& v);
/** Compare all fields of two `AttestationBase` instances, including signer
* identity and raw signature bytes.
*
* Used by subclass `operator==` to test whether two attestations are
* identical in every respect. Compare with `sameEventHelper`, which
* intentionally excludes signer fields.
*
* @param lhs Left-hand attestation.
* @param rhs Right-hand attestation.
* @return `true` if every base field matches.
*/
[[nodiscard]] static bool
equalHelper(AttestationBase const& lhs, AttestationBase const& rhs);
/** Check whether two attestations witness the same cross-chain event,
* ignoring signer identity.
*
* Two attestations from different witnesses for the same transfer share
* identical `sendingAccount`, `sendingAmount`, and `wasLockingChainSend`
* values. Signer fields are excluded so that distinct witnesses
* corroborating the same event can be counted toward quorum.
*
* @param lhs Left-hand attestation.
* @param rhs Right-hand attestation.
* @return `true` if the event-identity fields match.
*/
[[nodiscard]] static bool
sameEventHelper(AttestationBase const& lhs, AttestationBase const& rhs);
/** Populate `o` with the base attestation fields shared by both claim types.
*
* Subclass `toSTObject()` implementations call this before setting their
* own type-specific fields.
*
* @param o The `STObject` to populate.
*/
void
addHelper(STObject& o) const;
private:
/** Produce the canonical byte sequence that the witness signed.
*
* Pure virtual: each subclass encodes a different set of fields to prevent
* the base class from silently producing an incomplete message. The static
* overloads on each subclass expose the same logic for callers that want to
* verify a signature without constructing a full attestation object.
*
* @param bridge Bridge context bound into the signed payload.
* @return Serialized bytes suitable for passing to `xrpl::verify()`.
*/
[[nodiscard]] virtual std::vector<std::uint8_t>
message(STXChainBridge const& bridge) const = 0;
};
// Attest to a regular cross-chain transfer
/** Full witness-server attestation for a regular cross-chain transfer.
*
* Extends `AttestationBase` with the claim-specific `claimID` (a monotonic
* counter on the bridge that prevents replay) and an optional destination
* account override on the receiving chain.
*
* Two constructors support the two call sites: one accepts a pre-computed
* `Buffer signature` (used on the destination chain when verifying incoming
* attestations), and one accepts a `SecretKey` and signs inline (used by
* witness servers and test harnesses generating attestations from scratch).
*/
struct AttestationClaim : AttestationBase
{
/** Monotonic bridge counter that scopes this claim to a specific transfer,
* preventing replay across different events on the same bridge. */
std::uint64_t claimID;
/** Optional destination account on the receiving chain. Witnesses may
* attest to the same transfer with different `dst` opinions; the
* three-state `AttestationMatch` distinguishes these cases during quorum
* collection. */
std::optional<AccountID> dst;
/** Construct from individual field values with a pre-computed signature.
*
* Used on the destination chain when receiving and verifying attestations
* submitted by witness servers.
*
* @param attestationSignerAccount Account ID for the signer's public key.
* @param publicKey Witness server's public key.
* @param signature Pre-computed signature over the canonical message.
* @param sendingAccount Source account on the sending chain.
* @param sendingAmount Amount transferred on the sending chain.
* @param rewardAccount Destination-chain account receiving the reward share.
* @param wasLockingChainSend `true` if the transfer originated on the
* locking chain.
* @param claimId Bridge claim counter for this transfer.
* @param dst Optional destination account override on the receiving chain.
*/
explicit AttestationClaim(
AccountID attestationSignerAccount,
PublicKey const& publicKey,
@@ -96,6 +228,26 @@ struct AttestationClaim : AttestationBase
std::uint64_t claimId,
std::optional<AccountID> const& dst);
/** Construct and immediately sign using `secretKey`.
*
* Derives the canonical message bytes from all supplied fields and
* `bridge`, then signs them. Intended for witness servers and test
* harnesses that generate attestations from scratch.
*
* @param bridge Bridge context included in the signed payload; binds the
* attestation to a specific bridge instance.
* @param attestationSignerAccount Account ID for the signer's public key.
* @param publicKey Witness server's public key.
* @param secretKey Signing key; the resulting signature is stored in
* `AttestationBase::signature`.
* @param sendingAccount Source account on the sending chain.
* @param sendingAmount Amount transferred on the sending chain.
* @param rewardAccount Destination-chain account receiving the reward share.
* @param wasLockingChainSend `true` if the transfer originated on the
* locking chain.
* @param claimId Bridge claim counter for this transfer.
* @param dst Optional destination account override on the receiving chain.
*/
explicit AttestationClaim(
STXChainBridge const& bridge,
AccountID attestationSignerAccount,
@@ -108,16 +260,57 @@ struct AttestationClaim : AttestationBase
std::uint64_t claimId,
std::optional<AccountID> const& dst);
/** Deserialize from a ledger `STObject`. */
explicit AttestationClaim(STObject const& o);
/** Deserialize from a JSON value.
*
* @throws std::runtime_error if any required field is missing or has the
* wrong type.
*/
explicit AttestationClaim(json::Value const& v);
/** Serialize this attestation to an `STObject` for inclusion in a
* transaction or `STArray`.
*
* @return An inner object tagged
* `sfXChainClaimAttestationCollectionElement` containing all claim
* attestation fields.
*/
[[nodiscard]] STObject
toSTObject() const;
// return true if the two attestations attest to the same thing
/** Check whether `rhs` witnesses the same cross-chain claim event,
* ignoring signer identity fields.
*
* Both the base event fields and the claim-specific `claimID` and `dst`
* must agree. Two attestations satisfying this condition came from
* different witnesses for the same transfer and each count toward quorum.
*
* @param rhs The attestation to compare against.
* @return `true` if both attestations describe the same claim event.
*/
[[nodiscard]] bool
sameEvent(AttestationClaim const& rhs) const;
/** Produce the canonical bytes that a witness signs for a claim attestation.
*
* Fields are serialized in `SField` sort order so that independent
* implementations (e.g., Python witness servers) produce byte-for-byte
* identical output. The `bridge` is embedded in the payload, scoping
* the proof to a specific bridge instance so it cannot be replayed on
* another.
*
* @param bridge Bridge context scoping the proof.
* @param sendingAccount Source account on the sending chain.
* @param sendingAmount Amount transferred on the sending chain.
* @param rewardAccount Destination-chain account receiving the reward share.
* @param wasLockingChainSend `true` if the transfer originated on the
* locking chain.
* @param claimID Monotonic counter from the bridge that prevents replay.
* @param dst Optional destination override on the issuing chain.
* @return Serialized bytes suitable for signing or verification.
*/
[[nodiscard]] static std::vector<std::uint8_t>
message(
STXChainBridge const& bridge,
@@ -128,6 +321,10 @@ struct AttestationClaim : AttestationBase
std::uint64_t claimID,
std::optional<AccountID> const& dst);
/** Check that `sendingAmount` is a legal network amount.
*
* @return `true` if the amount is valid for wire transmission.
*/
[[nodiscard]] bool
validAmounts() const;
@@ -139,6 +336,11 @@ private:
operator==(AttestationClaim const& lhs, AttestationClaim const& rhs);
};
/** Comparator that orders `AttestationClaim` objects by their `claimID`.
*
* Used to sort or binary-search a collection of claim attestations by the
* bridge's monotonic sequence counter.
*/
struct CmpByClaimID
{
bool
@@ -148,22 +350,58 @@ struct CmpByClaimID
}
};
// Attest to a cross-chain transfer that creates an account
/** Full witness-server attestation for a cross-chain account-creation transfer.
*
* Extends `AttestationBase` with the account-creationspecific `createCount`
* (the value of `XChainAccountCreateCount` on the sending-chain bridge at
* event time), the destination account to create, and the total size of the
* witness reward pool. Unlike `AttestationClaim`, the destination is not
* optional — account-creation attestations always specify the account to
* create on the receiving chain.
*
* As with `AttestationClaim`, two constructors support pre-computed signatures
* (verification path) and inline signing (witness server / test harness path).
*/
struct AttestationCreateAccount : AttestationBase
{
// createCount on the sending chain. This is the value of the `CreateCount`
// field of the bridge on the sending chain when the transaction was
// executed.
/** Value of `XChainAccountCreateCount` on the sending-chain bridge when
* the account-creation transaction was executed. Scopes this attestation
* to a specific event and prevents replay. */
std::uint64_t createCount;
// Account to create on the destination chain
/** Account to create on the destination chain. */
AccountID toCreate;
// Total amount of the reward pool
/** Total amount of the witness reward pool for this event. */
STAmount rewardAmount;
/** Deserialize from a ledger `STObject`. */
explicit AttestationCreateAccount(STObject const& o);
/** Deserialize from a JSON value.
*
* @throws std::runtime_error if any required field is missing or has the
* wrong type.
*/
explicit AttestationCreateAccount(json::Value const& v);
/** Construct from individual field values with a pre-computed signature.
*
* Used on the destination chain when receiving and verifying attestations
* submitted by witness servers.
*
* @param attestationSignerAccount Account ID for the signer's public key.
* @param publicKey Witness server's public key.
* @param signature Pre-computed signature over the canonical message.
* @param sendingAccount Source account on the sending chain.
* @param sendingAmount Amount transferred on the sending chain.
* @param rewardAmount Total witness reward pool for this event.
* @param rewardAccount Destination-chain account receiving the reward share.
* @param wasLockingChainSend `true` if the transfer originated on the
* locking chain.
* @param createCount Bridge account-creation counter for this event.
* @param toCreate Account to create on the destination chain.
*/
explicit AttestationCreateAccount(
AccountID attestationSignerAccount,
PublicKey const& publicKey,
@@ -176,6 +414,27 @@ struct AttestationCreateAccount : AttestationBase
std::uint64_t createCount,
AccountID const& toCreate);
/** Construct and immediately sign using `secretKey`.
*
* Derives the canonical message bytes from all supplied fields and
* `bridge`, then signs them. Intended for witness servers and test
* harnesses that generate account-creation attestations from scratch.
*
* @param bridge Bridge context included in the signed payload; binds the
* attestation to a specific bridge instance.
* @param attestationSignerAccount Account ID for the signer's public key.
* @param publicKey Witness server's public key.
* @param secretKey Signing key; the resulting signature is stored in
* `AttestationBase::signature`.
* @param sendingAccount Source account on the sending chain.
* @param sendingAmount Amount transferred on the sending chain.
* @param rewardAmount Total witness reward pool for this event.
* @param rewardAccount Destination-chain account receiving the reward share.
* @param wasLockingChainSend `true` if the transfer originated on the
* locking chain.
* @param createCount Bridge account-creation counter for this event.
* @param toCreate Account to create on the destination chain.
*/
explicit AttestationCreateAccount(
STXChainBridge const& bridge,
AccountID attestationSignerAccount,
@@ -189,16 +448,54 @@ struct AttestationCreateAccount : AttestationBase
std::uint64_t createCount,
AccountID const& toCreate);
/** Serialize this attestation to an `STObject` for inclusion in a
* transaction or `STArray`.
*
* @return An inner object tagged
* `sfXChainCreateAccountAttestationCollectionElement` containing all
* account-creation attestation fields.
*/
[[nodiscard]] STObject
toSTObject() const;
// return true if the two attestations attest to the same thing
/** Check whether `rhs` witnesses the same cross-chain account-creation
* event, ignoring signer identity fields.
*
* The base event fields plus `createCount`, `toCreate`, and `rewardAmount`
* must all agree. Two attestations satisfying this condition came from
* different witnesses for the same transfer and each count toward quorum.
*
* @param rhs The attestation to compare against.
* @return `true` if both attestations describe the same account-creation
* event.
*/
[[nodiscard]] bool
sameEvent(AttestationCreateAccount const& rhs) const;
friend bool
operator==(AttestationCreateAccount const& lhs, AttestationCreateAccount const& rhs);
/** Produce the canonical bytes that a witness signs for an account-creation
* attestation.
*
* Fields are serialized in `SField` sort order so that independent
* implementations (e.g., Python witness servers) produce byte-for-byte
* identical output. The `bridge` is embedded in the payload, scoping
* the proof to a specific bridge instance.
*
* @param bridge Bridge context scoping the proof.
* @param sendingAccount Source account on the sending chain.
* @param sendingAmount Amount transferred on the sending chain.
* @param rewardAmount Total size of the witness reward pool for this event.
* @param rewardAccount Destination-chain account receiving this witness's
* reward share.
* @param wasLockingChainSend `true` if the transfer originated on the
* locking chain.
* @param createCount Value of `XChainAccountCreateCount` on the sending-
* chain bridge at the time of the event; prevents replay.
* @param dst Account to create on the destination chain.
* @return Serialized bytes suitable for signing or verification.
*/
[[nodiscard]] static std::vector<std::uint8_t>
message(
STXChainBridge const& bridge,
@@ -210,6 +507,11 @@ struct AttestationCreateAccount : AttestationBase
std::uint64_t createCount,
AccountID const& dst);
/** Check that both `sendingAmount` and `rewardAmount` are legal network
* amounts.
*
* @return `true` if both amounts are valid for wire transmission.
*/
[[nodiscard]] bool
validAmounts() const;
@@ -218,6 +520,11 @@ private:
message(STXChainBridge const& bridge) const override;
};
/** Comparator that orders `AttestationCreateAccount` objects by `createCount`.
*
* Used to sort or binary-search a collection of account-creation attestations
* by the bridge's monotonic account-creation sequence counter.
*/
struct CmpByCreateCount
{
bool
@@ -229,40 +536,113 @@ struct CmpByCreateCount
}; // namespace Attestations
// Result when checking when two attestation match.
/** Three-state result of comparing a stored ledger attestation against incoming
* fields.
*
* The extra granularity beyond a simple boolean is required because
* `XChainClaim` transactions allow the user to specify their own destination
* account, so the system must distinguish "everything matches" from "same
* transfer, different destination":
*
* - `XChainAddClaimAttestation` requires `Match` — witnesses must agree on
* the destination.
* - `XChainClaim` (user-specified destination) also accepts `MatchExceptDst`.
*/
enum class AttestationMatch {
// One of the fields doesn't match, and it isn't the dst field
/** At least one non-destination field differs; the attestations describe
* different events or amounts. */
NonDstMismatch,
// all of the fields match, except the dst field
/** All fields agree except the optional destination account. */
MatchExceptDst,
// all of the fields match
/** All fields, including the destination, agree completely. */
Match
};
/** Ledger-stored form of a regular cross-chain transfer attestation.
*
* This is the signature-free projection of `Attestations::AttestationClaim`
* that lives inside `XChainOwnedClaimID` ledger entries. The raw `Buffer
* signature` and full `sendingAccount` are not persisted; only the signer's
* `keyAccount` and `publicKey`, the event amount, reward account, transfer
* direction, and optional destination are retained.
*
* The `TSignedAttestation` typedef links this type back to its full
* counterpart. The converting constructor from `TSignedAttestation` is the
* canonical way to project a submitted attestation into its on-ledger form.
*
* @see AttestationMatch
*/
struct XChainClaimAttestation
{
/** Full witness-submitted attestation type that this ledger form is derived
* from. */
using TSignedAttestation = Attestations::AttestationClaim;
/** `SField` naming the `STArray` that holds these entries in ledger
* objects (`sfXChainClaimAttestations`). */
static SField const& arrayFieldName;
/** Account ID corresponding to the signer's public key. */
AccountID keyAccount;
/** Witness server's public key. */
PublicKey publicKey;
/** Amount transferred on the sending chain. */
STAmount amount;
/** Destination-chain account that collects this witness's reward share. */
AccountID rewardAccount;
/** `true` if the transfer was initiated on the locking chain. */
bool wasLockingChainSend;
/** Optional destination account on the receiving chain. */
std::optional<AccountID> dst;
/** Fields used to compare a stored attestation against an incoming one
* during quorum matching.
*
* Constructed from either a full `TSignedAttestation` or explicit values;
* holds only the event-describing fields that `match()` inspects.
*/
struct MatchFields
{
/** Amount transferred on the sending chain. */
STAmount amount;
/** `true` if the transfer was initiated on the locking chain. */
bool wasLockingChainSend;
/** Optional destination account on the receiving chain. */
std::optional<AccountID> dst;
/** Project from a full signing-side attestation.
*
* @param att Full witness-submitted attestation to extract from.
*/
MatchFields(TSignedAttestation const& att);
/** Construct from explicit values.
*
* @param a Amount transferred on the sending chain.
* @param b `true` if the transfer originated on the locking chain.
* @param d Optional destination account.
*/
MatchFields(STAmount a, bool b, std::optional<AccountID> const& d)
: amount{std::move(a)}, wasLockingChainSend{b}, dst{d}
{
}
};
/** Construct from explicit field values.
*
* @param keyAccount Account ID corresponding to the signer's public key.
* @param publicKey Witness server's public key.
* @param amount Amount transferred on the sending chain.
* @param rewardAccount Destination-chain account receiving the reward share.
* @param wasLockingChainSend `true` if the transfer originated on the
* locking chain.
* @param dst Optional destination account on the receiving chain.
*/
explicit XChainClaimAttestation(
AccountID const& keyAccount,
PublicKey const& publicKey,
@@ -271,6 +651,20 @@ struct XChainClaimAttestation
bool wasLockingChainSend,
std::optional<AccountID> const& dst);
/** Construct from `STAccount`-wrapped field values, unwrapping the
* `AccountID` values.
*
* Convenience overload used when deserializing from an `STObject` whose
* account fields are already typed as `STAccount`.
*
* @param keyAccount Wrapped account ID for the signer's public key.
* @param publicKey Witness server's public key.
* @param amount Amount transferred on the sending chain.
* @param rewardAccount Wrapped destination-chain reward account.
* @param wasLockingChainSend `true` if the transfer originated on the
* locking chain.
* @param dst Optional wrapped destination account.
*/
explicit XChainClaimAttestation(
STAccount const& keyAccount,
PublicKey const& publicKey,
@@ -279,15 +673,40 @@ struct XChainClaimAttestation
bool wasLockingChainSend,
std::optional<STAccount> const& dst);
/** Project a full `AttestationClaim` into its ledger-storage form,
* dropping the raw signature.
*
* @param claimAtt The full witness-submitted attestation to convert.
*/
explicit XChainClaimAttestation(TSignedAttestation const& claimAtt);
/** Deserialize from a ledger `STObject`. */
explicit XChainClaimAttestation(STObject const& o);
/** Deserialize from a JSON value.
*
* @throws std::runtime_error if any required field is missing or has the
* wrong type.
*/
explicit XChainClaimAttestation(json::Value const& v);
/** Determine how closely this stored attestation matches the supplied fields.
*
* Returns `Match` when all fields agree, `MatchExceptDst` when only the
* destination differs, and `NonDstMismatch` when a core event field
* (amount or direction) does not agree.
*
* @param rhs The fields from the incoming attestation or claim request.
* @return `Match`, `MatchExceptDst`, or `NonDstMismatch`.
*/
[[nodiscard]] AttestationMatch
match(MatchFields const& rhs) const;
/** Serialize this ledger-stored attestation to an `STObject`.
*
* @return An inner object tagged `sfXChainClaimProofSig` containing the
* ledger-side claim attestation fields (no raw signature).
*/
[[nodiscard]] STObject
toSTObject() const;
@@ -295,29 +714,87 @@ struct XChainClaimAttestation
operator==(XChainClaimAttestation const& lhs, XChainClaimAttestation const& rhs);
};
/** Ledger-stored form of a cross-chain account-creation attestation.
*
* This is the signature-free projection of
* `Attestations::AttestationCreateAccount` that lives inside
* `XChainOwnedCreateAccountClaimID` ledger entries. Unlike the claim
* variant, the destination account (`dst`) is mandatory — account-creation
* attestations always specify the account to create on the receiving chain.
*
* The `TSignedAttestation` typedef links this type back to its full
* counterpart. The converting constructor from `TSignedAttestation` is the
* canonical way to project a submitted attestation into its on-ledger form.
*
* @see AttestationMatch
*/
struct XChainCreateAccountAttestation
{
/** Full witness-submitted attestation type that this ledger form is derived
* from. */
using TSignedAttestation = Attestations::AttestationCreateAccount;
/** `SField` naming the `STArray` that holds these entries in ledger
* objects (`sfXChainCreateAccountAttestations`). */
static SField const& arrayFieldName;
/** Account ID corresponding to the signer's public key. */
AccountID keyAccount;
/** Witness server's public key. */
PublicKey publicKey;
/** Amount transferred on the sending chain. */
STAmount amount;
/** Total witness reward pool for this account-creation event. */
STAmount rewardAmount;
/** Destination-chain account that collects this witness's reward share. */
AccountID rewardAccount;
/** `true` if the transfer was initiated on the locking chain. */
bool wasLockingChainSend;
/** Account to create on the destination chain. */
AccountID dst;
/** Fields used to compare a stored attestation against an incoming one
* during quorum matching.
*
* For account-creation attestations, `amount`, `rewardAmount`, and
* `wasLockingChainSend` are all checked before the destination is
* compared.
*/
struct MatchFields
{
/** Amount transferred on the sending chain. */
STAmount amount;
/** Total witness reward pool for this event. */
STAmount rewardAmount;
/** `true` if the transfer was initiated on the locking chain. */
bool wasLockingChainSend;
/** Account to create on the destination chain. */
AccountID dst;
/** Project from a full signing-side attestation.
*
* @param att Full witness-submitted attestation to extract from.
*/
MatchFields(TSignedAttestation const& att);
};
/** Construct from explicit field values.
*
* @param keyAccount Account ID corresponding to the signer's public key.
* @param publicKey Witness server's public key.
* @param amount Amount transferred on the sending chain.
* @param rewardAmount Total witness reward pool for this event.
* @param rewardAccount Destination-chain account receiving the reward share.
* @param wasLockingChainSend `true` if the transfer originated on the
* locking chain.
* @param dst Account to create on the destination chain.
*/
explicit XChainCreateAccountAttestation(
AccountID const& keyAccount,
PublicKey const& publicKey,
@@ -327,15 +804,41 @@ struct XChainCreateAccountAttestation
bool wasLockingChainSend,
AccountID const& dst);
/** Project a full `AttestationCreateAccount` into its ledger-storage form,
* dropping the raw signature.
*
* @param claimAtt The full witness-submitted attestation to convert.
*/
explicit XChainCreateAccountAttestation(TSignedAttestation const& claimAtt);
/** Deserialize from a ledger `STObject`. */
explicit XChainCreateAccountAttestation(STObject const& o);
/** Deserialize from a JSON value.
*
* @throws std::runtime_error if any required field is missing or has the
* wrong type.
*/
explicit XChainCreateAccountAttestation(json::Value const& v);
/** Serialize this ledger-stored attestation to an `STObject`.
*
* @return An inner object tagged `sfXChainCreateAccountProofSig`
* containing the ledger-side account-creation attestation fields
* (no raw signature).
*/
[[nodiscard]] STObject
toSTObject() const;
/** Determine how closely this stored attestation matches the supplied fields.
*
* Returns `Match` when all fields agree, `MatchExceptDst` when only the
* destination account differs, and `NonDstMismatch` when a core event
* field (amount, reward amount, or direction) does not agree.
*
* @param rhs The fields from the incoming attestation or claim request.
* @return `Match`, `MatchExceptDst`, or `NonDstMismatch`.
*/
[[nodiscard]] AttestationMatch
match(MatchFields const& rhs) const;
@@ -345,23 +848,39 @@ struct XChainCreateAccountAttestation
XChainCreateAccountAttestation const& rhs);
};
// Attestations from witness servers for a particular claim ID and bridge.
// Only one attestation per signature is allowed.
/** Container for a collection of witness-server attestations for a single
* bridge event.
*
* Wraps a `std::vector<TAttestation>` and adds serialization to/from
* `STArray` and `Json::Value`. At most `kMAX_ATTESTATIONS` (256) entries
* are accepted at either parse path; this cap prevents memory exhaustion from
* crafted oversized arrays while remaining far above any realistic quorum
* size.
*
* The destructor is `protected` to prevent slicing: the two concrete
* subclasses (`XChainClaimAttestations` and `XChainCreateAccountAttestations`)
* add no virtual methods, so accidental deletion through a base pointer would
* silently be incorrect.
*
* @tparam TAttestation Either `XChainClaimAttestation` or
* `XChainCreateAccountAttestation`.
*/
template <class TAttestation>
class XChainAttestationsBase
{
public:
/** Underlying storage type for the attestation collection. */
using AttCollection = std::vector<TAttestation>;
private:
// Set a max number of allowed attestations to limit the amount of memory
// allocated and processing time. This number is much larger than the actual
// number of attestation a server would ever expect.
/** Maximum number of attestations accepted from either `STArray` or JSON
* input. Far above practical quorum sizes; guards against memory
* exhaustion from maliciously crafted messages. */
static constexpr std::uint32_t kMAX_ATTESTATIONS = 256;
AttCollection attestations_;
protected:
// Prevent slicing to the base class
/** Protected destructor prevents slicing through a base-class pointer. */
~XChainAttestationsBase() = default;
public:
@@ -370,45 +889,88 @@ public:
XChainAttestationsBase&
operator=(XChainAttestationsBase const& rhs) = default;
/** Construct from a pre-built attestation vector, taking ownership.
*
* @param sigs Attestation collection to move into this container.
*/
explicit XChainAttestationsBase(AttCollection&& sigs);
/** Deserialize from a JSON value containing an `"attestations"` array.
*
* @throws std::runtime_error if `v` is not a JSON object, if the array
* exceeds `kMAX_ATTESTATIONS`, or if any element fails to
* deserialize.
*/
explicit XChainAttestationsBase(json::Value const& v);
/** Deserialize from an `STArray` read out of a ledger object.
*
* @throws std::runtime_error if `arr` contains more than
* `kMAX_ATTESTATIONS` elements.
*/
explicit XChainAttestationsBase(STArray const& arr);
/** Serialize the collection to an `STArray` for storage in a ledger object.
*
* @return An `STArray` tagged with `TAttestation::arrayFieldName` whose
* elements are the `STObject` representations of each attestation.
*/
[[nodiscard]] STArray
toSTArray() const;
/** @return Const iterator to the first attestation. */
[[nodiscard]] typename AttCollection::const_iterator
begin() const;
/** @return Const iterator past the last attestation. */
[[nodiscard]] typename AttCollection::const_iterator
end() const;
/** @return Mutable iterator to the first attestation. */
typename AttCollection::iterator
begin();
/** @return Mutable iterator past the last attestation. */
typename AttCollection::iterator
end();
/** Remove all attestations satisfying predicate `f`.
*
* @tparam F Unary predicate type; called with `TAttestation const&`.
* @param f Predicate; returns `true` for attestations to remove.
* @return Number of attestations removed.
*/
template <class F>
std::size_t
eraseIf(F&& f);
/** @return Number of attestations in the collection. */
[[nodiscard]] std::size_t
size() const;
/** @return `true` if the collection contains no attestations. */
[[nodiscard]] bool
empty() const;
/** @return Read-only reference to the underlying attestation vector. */
[[nodiscard]] AttCollection const&
attestations() const;
/** Append an attestation to the collection.
*
* @tparam T Must be convertible to `TAttestation`.
* @param att Attestation to append (forwarded).
*/
template <class T>
void
emplaceBack(T&& att);
};
/** Compare two attestation collections for equality.
*
* @return `true` if both containers hold identical attestations in the same
* order.
*/
template <class TAttestation>
[[nodiscard]] inline bool
operator==(
@@ -455,12 +1017,25 @@ XChainAttestationsBase<TAttestation>::empty() const
return attestations_.empty();
}
/** Typed container for ledger-stored claim attestations.
*
* Thin named wrapper around `XChainAttestationsBase<XChainClaimAttestation>`
* that provides a distinct type for the type system without adding behavior.
* All constructors are inherited from the base.
*/
class XChainClaimAttestations final : public XChainAttestationsBase<XChainClaimAttestation>
{
using TBase = XChainAttestationsBase<XChainClaimAttestation>;
using TBase::TBase;
};
/** Typed container for ledger-stored account-creation attestations.
*
* Thin named wrapper around
* `XChainAttestationsBase<XChainCreateAccountAttestation>` that provides a
* distinct type for the type system without adding behavior. All constructors
* are inherited from the base.
*/
class XChainCreateAccountAttestations final
: public XChainAttestationsBase<XChainCreateAccountAttestation>
{

View File

@@ -1,3 +1,11 @@
/** @file
* Defines XRPAmount, the canonical type-safe representation of XRP in drops.
*
* One XRP equals exactly 1,000,000 drops. All XRP quantities in the ledger
* are stored and computed as integer drop counts to avoid floating-point
* imprecision. The companion constant `kDROPS_PER_XRP` and the
* `mulRatio()` helper are defined here as well.
*/
#pragma once
#include <xrpl/basics/Number.h>
@@ -16,13 +24,35 @@
namespace xrpl {
/** Type-safe representation of an XRP quantity in drops.
*
* All XRP values are stored as integer drop counts (1 XRP = 1,000,000 drops).
* The class derives comparison, addition, and subtraction operators from
* `boost::operators` mixins; only `operator==`, `operator<`, `operator+=`,
* and `operator-=` are implemented directly, and the rest are generated.
* Mixed arithmetic with raw `std::int64_t` literals is supported for fee
* and reserve computations.
*
* Negative drop values are representable and used in internal calculations;
* callers are responsible for ensuring ledger amounts are non-negative where
* required.
*
* @note Satisfies the `unit::Valid` and `unit::Usable` concepts from
* `Units.h` via the `unit_type` / `value_type` member typedefs, but
* is not a `ValueUnit<>` specialization. Its value accessor is named
* `drops()`, not `value()`.
* @see IOUAmount, MPTAmount, STAmount, mulRatio()
*/
class XRPAmount : private boost::totally_ordered<XRPAmount>,
private boost::additive<XRPAmount>,
private boost::equality_comparable<XRPAmount, std::int64_t>,
private boost::additive<XRPAmount, std::int64_t>
{
public:
/** Unit tag satisfying the `unit::Valid` / `unit::Usable` concepts. */
using unit_type = unit::dropTag;
/** Underlying integer type holding the drop count. */
using value_type = std::int64_t;
private:
@@ -34,15 +64,28 @@ public:
constexpr XRPAmount&
operator=(XRPAmount const& other) = default;
// Round to nearest, even on tie.
/** Construct from a `Number`, rounding to nearest, with ties to even.
*
* Used when high-precision `Number` arithmetic produces a result that
* must be expressed as an integer drop count, e.g. computed fees and
* reserves that may not fall exactly on integer boundaries.
*
* @param x The `Number` value to convert.
*/
explicit XRPAmount(Number const& x) : XRPAmount(static_cast<value_type>(x))
{
}
/** Construct a zero-drop amount from a `beast::Zero` sentinel.
*
* Enables use in generic contexts that pass `beast::zero` without
* knowing the concrete amount type, e.g. `XRPAmount x = beast::zero`.
*/
constexpr XRPAmount(beast::Zero) : drops_(0)
{
}
/** Assign zero drops from a `beast::Zero` sentinel. */
constexpr XRPAmount&
operator=(beast::Zero)
{
@@ -50,10 +93,20 @@ public:
return *this;
}
/** Construct from a raw drop count.
*
* Explicit to prevent silent integer-to-XRPAmount coercions.
*
* @param drops The amount in drops.
*/
constexpr explicit XRPAmount(value_type drops) : drops_(drops)
{
}
/** Assign from a raw drop count.
*
* @param drops The amount in drops.
*/
XRPAmount&
operator=(value_type drops)
{
@@ -61,12 +114,21 @@ public:
return *this;
}
/** Scale by a scalar integer, returning a new amount.
*
* Multiplication is exact (no rounding); overflow is unchecked.
* Use `mulRatio()` for ratio-based scaling with overflow detection.
*
* @param rhs The integer multiplier.
* @return The scaled amount.
*/
constexpr XRPAmount
operator*(value_type const& rhs) const
{
return XRPAmount{drops_ * rhs};
}
/** Scale by a scalar integer (commutative overload). */
friend constexpr XRPAmount
operator*(value_type lhs, XRPAmount const& rhs)
{
@@ -109,6 +171,7 @@ public:
return *this;
}
/** Negate the amount (unary minus). */
XRPAmount
operator-() const
{
@@ -140,12 +203,24 @@ public:
return drops_ != 0;
}
/** Implicitly convert to `Number` for use in high-precision arithmetic.
*
* Non-explicit so that `XRPAmount` values participate transparently in
* `Number`-based expressions used throughout the payment engine.
*/
operator Number() const noexcept
{
return drops();
}
/** Return the sign of the amount */
/** Return the sign of the amount as -1, 0, or +1.
*
* Follows the mathematical signum convention. Used widely in path and
* transaction validation to detect non-positive amounts without branching
* on the raw value.
*
* @return -1 if negative, 0 if zero, +1 if positive.
*/
[[nodiscard]] constexpr int
signum() const noexcept
{
@@ -161,9 +236,29 @@ public:
return drops_;
}
/** Convert the drop count to a decimal XRP quantity.
*
* Divides by `kDROPS_PER_XRP` (1,000,000) using floating-point
* arithmetic. The result is an approximation; do not use for
* ledger-critical calculations.
*
* @return The equivalent XRP amount as a `double`.
*/
[[nodiscard]] constexpr double
decimalXRP() const;
/** Safely narrow the drop count to a smaller integer type.
*
* Returns `std::nullopt` if the value cannot be represented in `Dest`:
* - value exceeds `std::numeric_limits<Dest>::max()`, or
* - `Dest` is unsigned and the drop count is negative, or
* - `Dest` is signed and the value is below `Dest::lowest()`.
*
* Primarily used when serializing fee fields that use 32-bit wire types.
*
* @tparam Dest The target integer type.
* @return The drop count as `Dest`, or `std::nullopt` on range error.
*/
template <class Dest>
[[nodiscard]] std::optional<Dest>
dropsAs() const
@@ -177,6 +272,12 @@ public:
return static_cast<Dest>(drops_);
}
/** Safely narrow the drop count, returning a default on range error.
*
* @tparam Dest The target integer type.
* @param defaultValue Value to return when the drop count is out of range.
* @return The drop count as `Dest`, or `defaultValue` on range error.
*/
template <class Dest>
Dest
dropsAs(Dest defaultValue) const
@@ -184,6 +285,12 @@ public:
return dropsAs<Dest>().value_or(defaultValue);
}
/** Safely narrow the drop count, using another `XRPAmount` as the fallback.
*
* @tparam Dest The target integer type.
* @param defaultValue `XRPAmount` whose drop count is returned on range error.
* @return The drop count as `Dest`, or `defaultValue.drops()` on range error.
*/
template <class Dest>
[[nodiscard]] Dest
dropsAs(XRPAmount defaultValue) const
@@ -191,9 +298,14 @@ public:
return dropsAs<Dest>().value_or(defaultValue.drops());
}
/* Clips a 64-bit value to a 32-bit JSON number. It is only used
* in contexts that don't expect the value to ever approach
* the 32-bit limits (i.e. fees and reserves).
/** Saturate-cast the drop count to a 32-bit JSON integer.
*
* Clamps values outside `[json::Int::min(), json::Int::max()]` to the
* respective bound rather than throwing or wrapping. Only valid in
* contexts where the value is never expected to approach 32-bit limits,
* specifically fees and reserves.
*
* @return A `json::Value` containing the (possibly clamped) drop count.
*/
[[nodiscard]] json::Value
jsonClipped() const
@@ -222,6 +334,7 @@ public:
return drops_;
}
/** Read a drop count from a stream into this amount. */
friend std::istream&
operator>>(std::istream& s, XRPAmount& val)
{
@@ -229,6 +342,13 @@ public:
return s;
}
/** Return the smallest representable positive XRP amount (1 drop).
*
* Used as a dust threshold: offers whose effective in- or out-amount
* does not exceed this value are considered stale and removed.
*
* @return `XRPAmount{1}`.
*/
static XRPAmount
minPositiveAmount()
{
@@ -236,7 +356,12 @@ public:
}
};
/** Number of drops per 1 XRP */
/** Number of drops per 1 XRP (1,000,000).
*
* Declared as `constexpr XRPAmount` rather than a plain integer so it
* participates in the type system and prevents inadvertent mixing with
* unrelated integer values.
*/
constexpr XRPAmount kDROPS_PER_XRP{1'000'000};
constexpr double
@@ -245,7 +370,7 @@ XRPAmount::decimalXRP() const
return static_cast<double>(drops_) / kDROPS_PER_XRP.drops();
}
// Output XRPAmount as just the drops value.
/** Write the drop count of an `XRPAmount` to an output stream. */
template <class Char, class Traits>
std::basic_ostream<Char, Traits>&
operator<<(std::basic_ostream<Char, Traits>& os, XRPAmount const& q)
@@ -253,12 +378,31 @@ operator<<(std::basic_ostream<Char, Traits>& os, XRPAmount const& q)
return os << q.drops();
}
/** Return a decimal string representation of the drop count. */
inline std::string
to_string(XRPAmount const& amount)
{
return std::to_string(amount.drops());
}
/** Scale an XRPAmount by the rational factor `num / den` with controlled rounding.
*
* Uses a 128-bit intermediate product to avoid overflow when multiplying a
* 64-bit drop count by a 32-bit numerator. After division, sign-aware
* rounding is applied: for a positive amount with a nonzero remainder,
* `roundUp=true` increments the result by one drop; for a negative amount,
* `roundUp=false` decrements (moves toward more negative), maintaining
* symmetric semantics across signs.
*
* @param amt The amount to scale.
* @param num Numerator of the scaling ratio (32-bit, treated as unsigned).
* @param den Denominator of the scaling ratio (must be non-zero).
* @param roundUp When `true`, round away from zero (positive) or toward
* more-negative (negative) on a fractional remainder.
* @return The scaled amount.
* @throws std::runtime_error if `den == 0`.
* @throws std::overflow_error if the result exceeds `XRPAmount::value_type` range.
*/
inline XRPAmount
mulRatio(XRPAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp)
{

View File

@@ -1,3 +1,14 @@
/** @file
* Type-erased variant storage for all XRPL serialized field types.
*
* Declares `STVar`, the small-object-optimized container that `STObject`
* uses to store heterogeneous `STBase`-derived values by value. Also
* declares the tag types and the `makeStvar<T>()` factory.
*
* @see xrpl::detail::STVar
* @see xrpl::STObject
*/
#pragma once
#include <xrpl/protocol/SField.h>
@@ -9,32 +20,92 @@
namespace xrpl::detail {
/** Tag type that selects the default-valued construction path in `STVar`.
*
* Pass the global `gDefaultObject` singleton to `STVar(DefaultObjectT,
* SField)` to construct a field whose value is the type default. The
* explicit constructor prevents accidental implicit use.
*/
struct DefaultObjectT
{
explicit DefaultObjectT() = default;
};
/** Tag type that selects the absent-field (not-present) construction path in
* `STVar`.
*
* Pass the global `gNonPresentObject` singleton to
* `STVar(NonPresentObjectT, SField)` to create a bare `STBase` sentinel
* representing a schema slot that carries no value in a particular object
* instance (`STI_NOTPRESENT`). The explicit constructor prevents accidental
* implicit use.
*/
struct NonPresentObjectT
{
explicit NonPresentObjectT() = default;
};
/** Global sentinel for default-valued object construction.
*
* Passed as the first argument to `STVar(DefaultObjectT, SField)`.
*/
extern DefaultObjectT gDefaultObject;
/** Global sentinel for absent-field object construction.
*
* Passed as the first argument to `STVar(NonPresentObjectT, SField)`.
*/
extern NonPresentObjectT gNonPresentObject;
// Concept to constrain STVar constructors, which
// instantiate ST* types from SerializedTypeID
/** Concept constraining the variadic argument lists accepted by `constructST`.
*
* Permits exactly two calling forms:
* - `(SField)` — default-constructs the concrete `ST*` type.
* - `(SerialIter, SField)` — deserializes the concrete `ST*` type from a
* wire-format stream.
*
* Any other argument combination is a compile-time error, preventing misuse
* of the template dispatch switch.
*/
template <typename... Args>
concept ValidConstructSTArgs =
(std::is_same_v<std::tuple<std::remove_cvref_t<Args>...>, std::tuple<SField>> ||
std::is_same_v<std::tuple<std::remove_cvref_t<Args>...>, std::tuple<SerialIter, SField>>);
// "variant" that can hold any type of serialized object
// and includes a small-object allocation optimization.
/** Type-erased, small-object-optimized container for any XRPL serialized type.
*
* `STVar` is the storage primitive underlying `STObject`: every field in a
* transaction or ledger entry is stored as a `detail::STVar` inside
* `STObject`'s field vector. Because fields span roughly two dozen concrete
* `STBase` subclasses, `STVar` provides uniform value semantics over the
* polymorphic hierarchy.
*
* **Small-object optimization:** objects whose `sizeof` fits within
* `kMAX_SIZE` (72 bytes) are placement-new'd directly into the inline
* aligned-storage buffer `d_`, avoiding a heap allocation. Larger types
* (e.g., `STPathSet`, `STObject`, `STArray`) fall back to `new`. The
* `onHeap()` predicate distinguishes the two cases by comparing `p_`
* against `&d_`.
*
* **Move semantics:** moving a heap-resident object is a zero-copy pointer
* transfer. Moving an inline object requires calling the virtual
* `STBase::move()` to relocate it into the new buffer, because the source
* buffer address becomes invalid once the source `STVar` is destroyed.
*
* **Depth guard:** the deserialization constructor enforces a maximum nesting
* depth of 10 for `STObject`/`STArray` containers to prevent stack
* exhaustion from malformed or malicious wire data.
*
* @note `STVar` is an internal implementation detail (`xrpl::detail`). Public
* callers interact with fields through `STObject` and its proxy accessors.
* @see STBase
* @see STObject
*/
class STVar
{
private:
// The largest "small object" we can accommodate
/** Inline buffer size in bytes; objects at or below this threshold are
* stored in-place rather than on the heap. */
static std::size_t constexpr kMAX_SIZE = 72;
std::aligned_storage<kMAX_SIZE>::type d_ = {};
@@ -49,45 +120,102 @@ public:
STVar&
operator=(STVar&& rhs);
/** Move-construct from a bare `STBase` rvalue.
*
* Delegates to `STBase::move()`, which places the object into `d_` if
* it fits or heap-allocates it otherwise. The source object is in a
* valid-but-unspecified state after this call.
*
* @param t The `STBase`-derived rvalue to move into this container.
*/
STVar(STBase&& t) // NOLINT(cppcoreguidelines-rvalue-reference-param-not-moved)
{
p_ = t.move(kMAX_SIZE, &d_);
}
/** Copy-construct from a bare `STBase` lvalue.
*
* Delegates to `STBase::copy()`, which places a copy of @p t into `d_`
* if it fits or heap-allocates it otherwise.
*
* @param t The `STBase`-derived object to copy into this container.
*/
STVar(STBase const& t)
{
p_ = t.copy(kMAX_SIZE, &d_);
}
/** Construct a default-valued field.
*
* Creates an instance of the concrete `ST*` type identified by
* `name.fieldType`, initialized to that type's default value. Pass
* `gDefaultObject` as the tag argument.
*
* @param name The field descriptor; `name.fieldType` selects the
* concrete subtype to construct.
*/
STVar(DefaultObjectT, SField const& name);
/** Construct an absent-field sentinel.
*
* Creates a bare `STBase` with `getSType() == STI_NOTPRESENT`,
* representing a schema slot that has no value in this particular
* object instance. Pass `gNonPresentObject` as the tag argument.
*
* @param name The field that is absent.
*/
STVar(NonPresentObjectT, SField const& name);
/** Deserialize a field from a wire-format byte stream.
*
* Dispatches to the appropriate `ST*` deserialization constructor based
* on `name.fieldType`. For `STObject` and `STArray`, the current
* `depth` is incremented before recursing into nested `STVar`
* construction to enforce the nesting limit.
*
* @param sit Iterator positioned at the start of the serialized value.
* @param name The field descriptor; `name.fieldType` selects the type.
* @param depth Current nesting depth; must not exceed 10.
* @throws std::runtime_error if `depth` exceeds 10.
*/
STVar(SerialIter& sit, SField const& name, int depth = 0);
/** Return a mutable reference to the contained `STBase` object. */
STBase&
get()
{
return *p_;
}
/** Dereference to a mutable `STBase` reference. */
STBase&
operator*()
{
return get();
}
/** Arrow operator for mutable member access on the contained object. */
STBase*
operator->()
{
return &get();
}
/** Return a const reference to the contained `STBase` object. */
[[nodiscard]] STBase const&
get() const
{
return *p_;
}
/** Dereference to a const `STBase` reference. */
STBase const&
operator*() const
{
return get();
}
/** Arrow operator for const member access on the contained object. */
STBase const*
operator->() const
{
@@ -106,6 +234,15 @@ private:
void
destroy();
/** Placement-new a `T` into the inline buffer or the heap.
*
* If `sizeof(T) > kMAX_SIZE`, allocates via `new`. Otherwise,
* placement-news directly into `d_`. Sets `p_` to point at the result.
*
* @tparam T The concrete `STBase` subtype to construct.
* @tparam Args Constructor argument types forwarded to `T`.
* @param args Arguments forwarded to the `T` constructor.
*/
template <class T, class... Args>
void
construct(Args&&... args)
@@ -120,15 +257,34 @@ private:
}
}
/** Construct requested Serializable Type according to id.
* The variadic args are: (SField), or (SerialIter, SField).
* depth is ignored in former case.
/** Dispatch construction of the concrete `ST*` type identified by `id`.
*
* Maps every `SerializedTypeID` to the appropriate `construct<T>()` call.
* For `STObject` and `STArray`, forwards `depth` so the nesting limit
* propagates into their own recursive `STVar` construction.
* `STI_NOTPRESENT` always constructs a bare `STBase` regardless of
* `args`.
*
* @tparam Args Constrained by `ValidConstructSTArgs` to `(SField)` or
* `(SerialIter, SField)`.
* @param id Selects the concrete `ST*` subtype.
* @param depth Current nesting depth; forwarded only to `STObject` and
* `STArray` constructors.
* @param args Construction arguments forwarded to the selected type.
* @throws std::runtime_error if `id` is not a recognised
* `SerializedTypeID`.
*/
template <typename... Args>
requires ValidConstructSTArgs<Args...>
void
constructST(SerializedTypeID id, int depth, Args&&... arg);
/** Return `true` if the contained object was allocated on the heap.
*
* Compares `p_` against the address of the inline buffer `d_`. Used by
* the move constructor and destructor to select between pointer-steal
* and virtual-move / explicit-destruct paths respectively.
*/
[[nodiscard]] bool
onHeap() const
{
@@ -136,6 +292,19 @@ private:
}
};
/** Construct an `STVar` holding a concrete `T` directly, bypassing type-ID
* dispatch.
*
* Use this factory in well-typed contexts where the exact `STBase` subclass
* is already known, avoiding the overhead of the `constructST` switch. The
* small-object optimization applies: `T` is placement-new'd into the inline
* buffer when `sizeof(T) <= 72`, otherwise heap-allocated.
*
* @tparam T The concrete `STBase` subtype to store.
* @tparam Args Constructor argument types forwarded to `T`.
* @param args Arguments forwarded to the `T` constructor.
* @return An `STVar` owning the newly constructed `T` instance.
*/
template <class T, class... Args>
inline STVar
makeStvar(Args&&... args)
@@ -145,12 +314,28 @@ makeStvar(Args&&... args)
return st;
}
/** Value equality: delegates to `STBase::isEquivalent()`.
*
* Compares field values but not field names, consistent with `STBase`
* semantics — two fields of equal value but different names are considered
* equivalent at the `STVar` level.
*
* @param lhs Left-hand operand.
* @param rhs Right-hand operand.
* @return `true` if the contained values are equivalent.
*/
inline bool
operator==(STVar const& lhs, STVar const& rhs)
{
return lhs.get().isEquivalent(rhs.get());
}
/** Value inequality: opposite of `operator==`.
*
* @param lhs Left-hand operand.
* @param rhs Right-hand operand.
* @return `true` if the contained values are not equivalent.
*/
inline bool
operator!=(STVar const& lhs, STVar const& rhs)
{

View File

@@ -13,21 +13,65 @@
namespace xrpl {
/** Idiomatic return type for the Base58 codec.
*
* Pairs a success value of type `T` with a `std::error_code` drawn from
* `TokenCodecErrc`. Using `boost::outcome` rather than exceptions keeps the
* codec allocation-free and gives callers fine-grained error inspection
* without try/catch overhead.
*
* @tparam T The success value type.
*/
template <class T>
using Result = boost::outcome_v2::result<T, std::error_code>;
#ifndef _MSC_VER
/** @file
* Arithmetic primitives for XRPL's fast Base58 codec.
*
* The fast path achieves 1015× the throughput of the reference
* Bitcoin-derived algorithm by using base 58¹⁰ as an intermediate
* representation. Because 58¹⁰ = 430,804,206,899,405,824 fits in a
* `uint64_t`, the codec operates on far fewer, much larger "digits",
* dramatically reducing inner-loop iterations.
*
* All functions in this namespace rely on GCC/Clang's `unsigned __int128`
* extension. The entire namespace is therefore excluded on MSVC (`#ifndef
* _MSC_VER`); `tokens.cpp` falls back to the reference implementation on
* Windows.
*/
namespace b58_fast::detail {
// This optimizes to what hand written asm would do (single divide)
/** Compute quotient and remainder in a single hardware divide instruction.
*
* A combined `divRem` call allows the compiler to emit one `divq`
* instruction rather than two, avoiding the cost of a redundant division.
*
* @param a The dividend.
* @param b The divisor.
* @return A tuple `{a / b, a % b}`.
*/
[[nodiscard]] inline std::tuple<std::uint64_t, std::uint64_t>
divRem(std::uint64_t a, std::uint64_t b)
{
return {a / b, a % b};
}
// This optimizes to what hand written asm would do (single multiply)
/** Multiply two 64-bit values and add a carry term without overflow.
*
* Uses `unsigned __int128` as a 128-bit intermediate so that GCC/Clang can
* fuse the operation into a single `mulq` instruction. This is the
* per-limb step of hardware long-multiplication, used by
* `inplaceBigintMul` to scale a multi-precision integer by a scalar.
*
* @param a First multiplicand.
* @param b Second multiplicand.
* @param carry Carry value accumulated from a previous limb (may be zero).
* @return A tuple `{low64, high64}` where `low64` is the new coefficient
* (the low 64 bits of the 128-bit product) and `high64` is the carry
* to propagate to the next limb.
*/
[[nodiscard]] inline std::tuple<std::uint64_t, std::uint64_t>
carryingMul(std::uint64_t a, std::uint64_t b, std::uint64_t carry)
{
@@ -37,6 +81,16 @@ carryingMul(std::uint64_t a, std::uint64_t b, std::uint64_t carry)
return {c & 0xffff'ffff'ffff'ffff, c >> 64};
}
/** Add two 64-bit values and detect carry into the 65th bit.
*
* Uses `unsigned __int128` as a 128-bit intermediate so that GCC/Clang
* generates a single `addq`/`adcq` pair. Used by `inplaceBigintAdd` to
* ripple carry through the limbs of a multi-precision integer.
*
* @param a First addend.
* @param b Second addend.
* @return A tuple `{sum & 0xFFFF…F, carry}` where `carry` is 0 or 1.
*/
[[nodiscard]] inline std::tuple<std::uint64_t, std::uint64_t>
carryingAdd(std::uint64_t a, std::uint64_t b)
{
@@ -46,11 +100,27 @@ carryingAdd(std::uint64_t a, std::uint64_t b)
return {c & 0xffff'ffff'ffff'ffff, c >> 64};
}
// Add a u64 to a "big uint" value inplace.
// The bigint value is stored with the smallest coefficients first
// (i.e a[0] is the 2^0 coefficient, a[n] is the 2^(64*n) coefficient)
// panics if overflows (this is a specialized adder for b58 decoding.
// it should never overflow).
/** Add a `uint64_t` scalar to a multi-precision big integer in place.
*
* The big integer is stored as a `span` of `uint64_t` limbs in
* **little-endian limb order**: `a[0]` holds the 2⁰ coefficient,
* `a[n]` holds the 2⁶⁴ⁿ coefficient. The scalar is added to `a[0]` and
* any resulting carry is rippled forward through subsequent limbs.
*
* Early return is the common case: once carry becomes zero the function
* stops walking the span, so short carries (spanning one or two limbs) are
* essentially free.
*
* @param a The big integer to modify, with limbs in ascending power order.
* Must contain at least 2 elements (one data limb + one carry limb).
* @param b The scalar value to add.
* @return `TokenCodecErrc::Success` on success.
* `TokenCodecErrc::InputTooSmall` if `a.size() <= 1`.
* `TokenCodecErrc::OverflowAdd` if carry propagates past the end of
* `a`; this represents a programming invariant violation — valid
* Base58 inputs are never large enough to trigger it.
* @note `[[nodiscard]]` — the caller must inspect the return value.
*/
[[nodiscard]] inline TokenCodecErrc
inplaceBigintAdd(std::span<std::uint64_t> a, std::uint64_t b)
{
@@ -77,6 +147,26 @@ inplaceBigintAdd(std::span<std::uint64_t> a, std::uint64_t b)
return TokenCodecErrc::Success;
}
/** Multiply a multi-precision big integer by a `uint64_t` scalar in place.
*
* Limbs are stored in **little-endian limb order** (same convention as
* `inplaceBigintAdd`). The function multiplies every limb except the last
* by `b`, propagating carry forward. The final carry is written into
* `a[last_index]`, which must be zero on entry — it acts as a reserved
* overflow slot.
*
* `tokens.cpp` always passes a span one element larger than the live
* portion so that this overflow slot is always available.
*
* @param a The big integer to scale in place. `a[a.size()-1]` must be zero
* on entry; it receives the overflow carry on exit.
* @param b The scalar multiplier.
* @return `TokenCodecErrc::Success` on success.
* `TokenCodecErrc::InputTooSmall` if `a` is empty.
* `TokenCodecErrc::InputTooLarge` if `a[last]` is non-zero on entry,
* indicating the value would overflow the allocated limb array.
* @note `[[nodiscard]]` — the caller must inspect the return value.
*/
[[nodiscard]] inline TokenCodecErrc
inplaceBigintMul(std::span<std::uint64_t> a, std::uint64_t b)
{
@@ -100,8 +190,29 @@ inplaceBigintMul(std::span<std::uint64_t> a, std::uint64_t b)
return TokenCodecErrc::Success;
}
// divide a "big uint" value inplace and return the mod
// numerator is stored so smallest coefficients come first
/** Divide a multi-precision big integer in place by a scalar, returning the remainder.
*
* Performs long division from the most significant limb down, maintaining a
* running `prevRem`. At each step it forms a 128-bit dividend
* `(prevRem << 64) | numerator[i]` and divides it by `divisor` using an
* inner lambda that keeps the `unsigned __int128` logic localized.
* `XRPL_ASSERT` guards verify that neither quotient nor remainder
* overflows 64 bits (guaranteed by the invariant that `prevRem < divisor`
* at every step).
*
* Limbs are stored in **little-endian limb order**: `numerator[0]` is the
* 2⁰ coefficient, `numerator[n]` is the 2⁶⁴ⁿ coefficient. On return,
* `numerator` is overwritten with the quotient in the same layout.
*
* This is the workhorse of the encoding path: `tokens.cpp` calls it in a
* loop to extract successive base-58¹⁰ coefficients from the binary
* representation of the value being encoded.
*
* @param numerator The big integer to divide, modified in place to hold
* the quotient. An empty span is treated as zero (returns 0).
* @param divisor The scalar divisor. Behavior is undefined if zero.
* @return The remainder of the division (`numerator % divisor`).
*/
[[nodiscard]] inline std::uint64_t
inplaceBigintDivRem(std::span<uint64_t> numerator, std::uint64_t divisor)
{
@@ -149,9 +260,20 @@ inplaceBigintDivRem(std::span<uint64_t> numerator, std::uint64_t divisor)
return prevRem;
}
// convert from base 58^10 to base 58
// put largest coeffs first
// the `_be` suffix stands for "big endian"
/** Decompose a single base-58¹⁰ coefficient into 10 base-58 digits, most significant first.
*
* Repeatedly divides `input` by 58, filling the result array from the back
* so that the most significant digit occupies index 0 (big-endian digit
* order). The caller (`tokens.cpp`) maps each digit through
* `alphabetForward[]` to produce the final Base58 character.
*
* @param input A value in [0, 58¹⁰). The precondition `input < 58¹⁰` is
* checked by `XRPL_ASSERT`; a value at or above this bound indicates a
* logic error in the preceding division step.
* @return An array of 10 `uint8_t` values in [0, 58), ordered
* most-significant digit first.
* @note `[[nodiscard]]` — the caller must use the returned digit array.
*/
[[nodiscard]] inline std::array<std::uint8_t, 10>
b5810ToB58Be(std::uint64_t input)
{

View File

@@ -1,13 +1,50 @@
/** @file
* Process-wide singleton accessor for the libsecp256k1 context.
*
* This header is an implementation detail of the XRPL protocol layer.
* External code should use `SecretKey` and `PublicKey` rather than
* including this header directly.
*/
#pragma once
#include <secp256k1.h>
namespace xrpl {
/** Returns the process-wide, read-only libsecp256k1 context.
*
* The context is created on the first call with both
* `SECP256K1_CONTEXT_VERIFY` and `SECP256K1_CONTEXT_SIGN` flags so that
* the same instance can service signing (in `SecretKey.cpp`) and
* verification (in `PublicKey.cpp`) without any additional setup. It is
* destroyed when the process exits.
*
* The function template with a defaulted, unused type parameter is a
* standard C++ idiom for defining a function that owns a `static` local
* variable in a header file without violating the One Definition Rule
* (ODR). The `static Holder` is initialized exactly once regardless of
* how many translation units include this header.
*
* @tparam Unused Defaulted to `void`; callers should never supply a
* value. The parameter exists solely to satisfy the ODR.
* @return A `const` pointer to the shared `secp256k1_context`. The
* `const` qualifier matches the secp256k1 library's thread-safety
* contract: concurrent reads through a `const*` require no locking.
* @note Do not call `secp256k1_context_destroy` on the returned pointer;
* lifetime is managed internally by the `Holder` RAII wrapper.
*/
template <class = void>
secp256k1_context const*
secp256k1Context()
{
/** RAII owner for the shared `secp256k1_context`.
*
* Allocates the context with both `SECP256K1_CONTEXT_VERIFY` and
* `SECP256K1_CONTEXT_SIGN` on construction, and destroys it on
* destruction. The single `static` instance inside
* `secp256k1Context()` lives for the duration of the process.
*/
struct Holder
{
secp256k1_context* impl;

View File

@@ -1,41 +1,101 @@
/** @file
* Error taxonomy for the XRPL Base58Check token codec.
*
* Defines `TokenCodecErrc` and wires it into the `std::error_code`
* machinery so that codec failures compose cleanly with `B58Result<T>`
* (`Expected<T, std::error_code>`) without exceptions or casts.
*/
#pragma once
#include <system_error>
namespace xrpl {
/** Error codes produced by the Base58Check token codec.
*
* Every distinct failure mode the encode/decode pipeline can signal is
* represented here. `Success` is zero so that a default-constructed
* `std::error_code` built from this enum compares equal to no-error.
*
* The enumerators fall into four groups:
* - **Size** (`InputTooLarge`, `InputTooSmall`, `OutputTooSmall`): bounds
* violations detected before or during a conversion pass.
* - **Alphabet** (`BadB58Character`, `InvalidEncodingChar`): invalid bytes
* in a Base58 string, distinguished by context — a character outside the
* 58-symbol alphabet vs. one that is invalid for the specific encoding.
* - **Semantic** (`MismatchedTokenType`, `MismatchedChecksum`): structurally
* valid input that fails XRPL-specific validation — wrong one-byte type
* prefix or a four-byte checksum mismatch.
* - **Arithmetic** (`OverflowAdd`): bignum addition overflow during the
* multi-precision fast-path decode, indicating the encoded value exceeds
* the expected output width. Valid Base58Check inputs never trigger this;
* it surfaces only on malformed or adversarial input.
*/
enum class TokenCodecErrc {
Success = 0,
InputTooLarge,
InputTooSmall,
BadB58Character,
OutputTooSmall,
MismatchedTokenType,
MismatchedChecksum,
InvalidEncodingChar,
OverflowAdd,
Unknown,
Success = 0, /**< Conversion succeeded. Zero value is the
`std::error_code` no-error sentinel. */
InputTooLarge, /**< Input buffer exceeds the maximum accepted size. */
InputTooSmall, /**< Input buffer is shorter than the minimum required. */
BadB58Character, /**< A byte in the input is not in the Base58 alphabet. */
OutputTooSmall, /**< Caller-supplied output buffer is too small for the result. */
MismatchedTokenType, /**< Decoded one-byte token-type prefix does not match
the expected `TokenType`. */
MismatchedChecksum, /**< Decoded four-byte checksum does not match the
recomputed value; the token is corrupt or truncated. */
InvalidEncodingChar, /**< A character is syntactically invalid for this
encoding context (distinct from `BadB58Character`). */
OverflowAdd, /**< Bignum addition overflowed the fixed-size limb array;
the encoded value is too large for the output type. */
Unknown, /**< Catch-all for unrecognised internal error states. */
};
} // namespace xrpl
namespace std {
/** Opt `TokenCodecErrc` into the `std::error_code` implicit-conversion protocol.
*
* With this specialisation a bare `TokenCodecErrc` enumerator can be stored
* in or compared against a `std::error_code` without an explicit
* `make_error_code` call, enabling natural use in `Unexpected(errc)` and
* `ec == TokenCodecErrc::X` comparisons.
*/
template <>
struct is_error_code_enum<xrpl::TokenCodecErrc> : true_type
{
};
} // namespace std
namespace xrpl {
namespace detail {
/** `std::error_category` singleton for the Base58Check codec error domain.
*
* Provides the stable category name `"TokenCodecError"` and human-readable
* messages for every `TokenCodecErrc` enumerator. Declared `final` — only
* one category exists for this error domain.
*
* Obtain the singleton via `tokenCodecErrcCategory()` rather than
* constructing this class directly.
*/
class TokenCodecErrcCategory : public std::error_category
{
public:
// Return a short descriptive name for the category
/** Return the stable name of this error category. */
[[nodiscard]] char const*
name() const noexcept final
{
return "TokenCodecError";
}
// Return what each enum means in text
/** Translate a `TokenCodecErrc` integer value into a human-readable string.
*
* @param c An integer cast of a `TokenCodecErrc` enumerator.
* @return A short English description of the error condition; returns
* `"unknown"` for unrecognised values.
*/
[[nodiscard]] std::string
message(int c) const final
{
@@ -64,8 +124,18 @@ public:
}
}
};
} // namespace detail
/** Return the `TokenCodecErrc` error category singleton.
*
* Uses a function-local static to guarantee thread-safe, zero-overhead
* initialisation (C++11 magic-static semantics). Called by
* `make_error_code` and by the implicit `std::error_code` conversion
* enabled through `is_error_code_enum`.
*
* @return A `const` reference to the single `TokenCodecErrcCategory` instance.
*/
inline xrpl::detail::TokenCodecErrcCategory const&
tokenCodecErrcCategory()
{
@@ -73,9 +143,21 @@ tokenCodecErrcCategory()
return kC;
}
/** Construct a `std::error_code` from a `TokenCodecErrc` enumerator.
*
* This is the ADL-located overload required by the `std::error_code`
* implicit-conversion protocol. Combined with the `is_error_code_enum`
* specialisation, it allows `TokenCodecErrc` values to be returned
* directly wherever a `std::error_code` is expected — including inside
* `Unexpected(errc)` expressions in `B58Result<T>` return sites.
*
* @param e The codec error enumerator to wrap.
* @return A `std::error_code` pairing `e` with `tokenCodecErrcCategory()`.
*/
inline std::error_code
make_error_code(xrpl::TokenCodecErrc e)
{
return {static_cast<int>(e), tokenCodecErrcCategory()};
}
} // namespace xrpl

View File

@@ -9,21 +9,32 @@
namespace xrpl {
/** Message digest functions used in the codebase
@note These are modeled to meet the requirements of `Hasher` in the
`hash_append` interface, discussed in proposal:
N3980 "Types Don't Know #"
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html
*/
/** @file
* Cryptographic digest primitives for the XRPL protocol layer.
*
* Defines all hasher structs used to compute ledger object identifiers,
* transaction IDs, account addresses, and signing payloads. Every type
* satisfies the `Hasher` concept from N3980 ("Types Don't Know #"), enabling
* `beast::hash_append` to drive any of them generically.
*
* @see https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3980.html
*/
//------------------------------------------------------------------------------
/** RIPEMD-160 digest
@note This uses the OpenSSL implementation
*/
/** RIPEMD-160 hasher backed by the OpenSSL `RIPEMD160_CTX` implementation.
*
* Satisfies the `Hasher` concept: feed data via `operator()`, then extract the
* 20-byte digest with `explicit operator result_type()`.
*
* The OpenSSL context is stored in an opaque `char ctx_[96]` buffer so that
* this header never needs to include any OpenSSL headers. A `static_assert` in
* the constructor verifies the buffer size matches `sizeof(RIPEMD160_CTX)` at
* compile time; if an OpenSSL upgrade changes the struct size the build fails
* rather than silently corrupting memory.
*
* @note Prefer the `ripemd160_hasher` alias over this name at call sites.
*/
struct OpensslRipemd160Hasher
{
public:
@@ -33,9 +44,15 @@ public:
OpensslRipemd160Hasher();
/** Feed bytes into the running digest.
*
* @param data Pointer to the input bytes.
* @param size Number of bytes to consume.
*/
void
operator()(void const* data, std::size_t size) noexcept;
/** Finalize and return the 20-byte RIPEMD-160 digest. */
explicit
operator result_type() noexcept;
@@ -43,10 +60,19 @@ private:
char ctx_[96]{};
};
/** SHA-512 digest
@note This uses the OpenSSL implementation
*/
/** SHA-512 hasher backed by the OpenSSL `SHA512_CTX` implementation.
*
* Satisfies the `Hasher` concept: feed data via `operator()`, then extract the
* 64-byte digest with `explicit operator result_type()`.
*
* Like `OpensslRipemd160Hasher`, the context is stored in an opaque
* `char ctx_[216]` buffer to avoid exposing OpenSSL headers, with a
* compile-time size check in the constructor.
*
* @note Prefer the `sha512_hasher` alias over this name at call sites. Most
* callers should use `sha512_half_hasher` / `sha512Half()` instead, which
* truncate the output to the 256-bit XRPL canonical form.
*/
struct OpensslSha512Hasher
{
public:
@@ -56,9 +82,15 @@ public:
OpensslSha512Hasher();
/** Feed bytes into the running digest.
*
* @param data Pointer to the input bytes.
* @param size Number of bytes to consume.
*/
void
operator()(void const* data, std::size_t size) noexcept;
/** Finalize and return the 64-byte SHA-512 digest. */
explicit
operator result_type() noexcept;
@@ -66,10 +98,17 @@ private:
char ctx_[216]{};
};
/** SHA-256 digest
@note This uses the OpenSSL implementation
*/
/** SHA-256 hasher backed by the OpenSSL `SHA256_CTX` implementation.
*
* Satisfies the `Hasher` concept: feed data via `operator()`, then extract the
* 32-byte digest with `explicit operator result_type()`.
*
* Like `OpensslRipemd160Hasher`, the context is stored in an opaque
* `char ctx_[112]` buffer to avoid exposing OpenSSL headers, with a
* compile-time size check in the constructor.
*
* @note Prefer the `sha256_hasher` alias over this name at call sites.
*/
struct OpensslSha256Hasher
{
public:
@@ -79,9 +118,15 @@ public:
OpensslSha256Hasher();
/** Feed bytes into the running digest.
*
* @param data Pointer to the input bytes.
* @param size Number of bytes to consume.
*/
void
operator()(void const* data, std::size_t size) noexcept;
/** Finalize and return the 32-byte SHA-256 digest. */
explicit
operator result_type() noexcept;
@@ -91,27 +136,34 @@ private:
//------------------------------------------------------------------------------
/** Implementation-neutral alias for the RIPEMD-160 hasher. */
using ripemd160_hasher = OpensslRipemd160Hasher;
/** Implementation-neutral alias for the SHA-256 hasher. */
using sha256_hasher = OpensslSha256Hasher;
/** Implementation-neutral alias for the SHA-512 hasher. */
using sha512_hasher = OpensslSha512Hasher;
//------------------------------------------------------------------------------
/** Returns the RIPEMD-160 digest of the SHA256 hash of the message.
This operation is used to compute the 160-bit identifier
representing an XRPL account, from a message. Typically the
message is the public key of the account - which is not
stored in the account root.
The same computation is used regardless of the cryptographic
scheme implied by the public key. For example, the public key
may be an ed25519 public key or a secp256k1 public key. Support
for new cryptographic systems may be added, using the same
formula for calculating the account identifier.
Meets the requirements of Hasher (in hash_append)
*/
/** Hasher that computes RIPEMD-160(SHA-256(msg)) — the XRPL account ID formula.
*
* This is the Bitcoin-lineage two-pass construction used throughout XRPL to
* derive a 160-bit `AccountID` from a public key. Data is accumulated into a
* `sha256_hasher`; on conversion, SHA-256 is finalized and its 32-byte output
* is immediately fed into a fresh `ripemd160_hasher` — no intermediate buffer
* escapes the function.
*
* The formula is deliberately key-type-agnostic: both secp256k1 and Ed25519
* public keys are hashed the same way, decoupling account addresses from the
* underlying cryptographic scheme. Future key types can be added without
* changing the address derivation.
*
* Satisfies the `Hasher` concept (N3980 `hash_append` interface).
*
* @see calcAccountID() in AccountID.h
*/
struct RipeshaHasher
{
private:
@@ -122,12 +174,20 @@ public:
using result_type = std::array<std::uint8_t, 20>;
/** Feed bytes into the running SHA-256 accumulator.
*
* @param data Pointer to the input bytes.
* @param size Number of bytes to consume.
*/
void
operator()(void const* data, std::size_t size) noexcept
{
h_(data, size);
}
/** Finalize SHA-256, hash its output through RIPEMD-160, and return the
* 20-byte account ID digest.
*/
explicit
operator result_type() noexcept
{
@@ -142,11 +202,26 @@ public:
namespace detail {
/** Returns the SHA512-Half digest of a message.
The SHA512-Half is the first 256 bits of the
SHA-512 digest of the message.
*/
/** SHA-512-Half hasher: computes SHA-512 and returns the first 256 bits as a
* `uint256`.
*
* SHA-512 is used (rather than SHA-256) because it is faster on 64-bit
* hardware due to wider register operations, while truncating to 256 bits
* still provides strong security. This is the dominant hash construction
* across XRPL's protocol layer — transaction IDs, ledger node hashes, signing
* payloads, and manifest digests all use it.
*
* `kENDIAN = big` ensures that when `beast::hash_append` serializes multi-byte
* integers before feeding them to this hasher, the bytes are in network
* (big-endian) order — matching the XRPL wire format.
*
* @tparam Secure When `true`, the destructor calls `secure_erase()` on the
* internal SHA-512 context to prevent sensitive key material from
* lingering in memory. When `false`, the destructor is a no-op with zero
* overhead. Use `sha512_half_hasher_s` (Secure=true) when hashing
* private keys or seed material; use `sha512_half_hasher` (Secure=false)
* for all other ledger computations.
*/
template <bool Secure>
struct BasicSha512HalfHasher
{
@@ -163,12 +238,18 @@ public:
erase(std::integral_constant<bool, Secure>{});
}
/** Feed bytes into the running SHA-512 accumulator.
*
* @param data Pointer to the input bytes.
* @param size Number of bytes to consume.
*/
void
operator()(void const* data, std::size_t size) noexcept
{
h_(data, size);
}
/** Finalize SHA-512 and return the first 256 bits as a `uint256`. */
explicit
operator result_type() noexcept
{
@@ -191,14 +272,41 @@ private:
} // namespace detail
/** Standard SHA-512-Half hasher for ledger computations.
*
* The destructor is a no-op. Use this for transaction IDs, ledger node
* hashes, and any non-sensitive protocol payload. Use `sha512_half_hasher_s`
* when hashing key or seed material.
*/
using sha512_half_hasher = detail::BasicSha512HalfHasher<false>;
// secure version
/** Secure SHA-512-Half hasher that zeroes internal state on destruction.
*
* Identical to `sha512_half_hasher` except the destructor calls
* `secure_erase()` on the embedded SHA-512 context, preventing sensitive
* material from remaining in memory after the hasher goes out of scope.
*/
using sha512_half_hasher_s = detail::BasicSha512HalfHasher<true>;
//------------------------------------------------------------------------------
/** Returns the SHA512-Half of a series of objects. */
/** Compute the SHA-512-Half of one or more objects and return a `uint256`.
*
* Constructs a `sha512_half_hasher`, drives it with `beast::hash_append` over
* all arguments in order, and returns the 256-bit result. Because
* `hash_append` is overloaded for all XRPL protocol types — including
* `HashPrefix`, `STObject`, `Serializer`, and primitive integers — a single
* call can serialize and hash an entire transaction or ledger node.
*
* Always prepend a `HashPrefix` constant as the first argument to enforce
* domain separation and prevent cross-context hash collisions.
*
* @param args One or more `hash_append`-compatible values to hash in order.
* @return The first 256 bits of the SHA-512 digest of the serialized input.
*
* @see sha512HalfS() for the secure variant that zeroes internal state.
* @see HashPrefix.h for the domain-separation prefix constants.
*/
template <class... Args>
sha512_half_hasher::result_type
sha512Half(Args const&... args)
@@ -209,12 +317,19 @@ sha512Half(Args const&... args)
return static_cast<typename sha512_half_hasher::result_type>(h);
}
/** Returns the SHA512-Half of a series of objects.
Postconditions:
Temporary memory storing copies of
input messages will be cleared.
*/
/** Compute the SHA-512-Half of one or more objects, zeroing internal state
* after extraction.
*
* Identical to `sha512Half()` except the internal `sha512_half_hasher_s`
* context is erased via `secure_erase()` on destruction, preventing
* sensitive material (e.g., a private key or seed) from lingering on the
* stack after the call returns.
*
* @param args One or more `hash_append`-compatible values to hash in order.
* @return The first 256 bits of the SHA-512 digest of the serialized input.
*
* @see sha512Half() for the non-secure, lower-overhead variant.
*/
template <class... Args>
sha512_half_hasher_s::result_type
sha512HalfS(Args const&... args)

View File

@@ -1,5 +1,22 @@
#pragma once
/** @file
* Typed JSON extraction with SField keys and structured exceptions.
*
* Provides `json::getOrThrow<T>` and `json::getOptional<T>`: replacements for
* raw `Json::Value` access that throw structured exceptions instead of
* silently returning defaults or coercing types. The key type is
* `xrpl::SField` rather than a plain string, tying every lookup to a declared
* XRPL protocol field and eliminating magic string literals.
*
* @note Additional specializations for `xrpl::AccountID`, `xrpl::PublicKey`,
* and `xrpl::STAmount` are defined in their respective headers, following
* the same pattern established here. The primary consumer is
* `XChainAttestations.cpp`, which calls `getOrThrow` in constructor
* initializer lists to reject partially-initialized attestation objects
* before they reach validation logic.
*/
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/contract.h>
@@ -11,13 +28,29 @@
#include <optional>
namespace json {
/** Exception thrown when a required JSON key is absent from the object.
*
* The diagnostic message is constructed lazily on the first call to `what()`
* to avoid a string allocation at the throw site.
*/
struct JsonMissingKeyError : std::exception
{
/** The missing key name, pointing into the `StaticString` storage. */
char const* const key;
/** Lazily-populated message returned by `what()`. */
mutable std::string msg;
/** Construct with the name of the missing key. */
JsonMissingKeyError(json::StaticString const& k) : key{k.cStr()}
{
}
/** Return a human-readable description of the missing key.
*
* The message string is built on the first call and cached in `msg`.
*/
char const*
what() const noexcept override
{
@@ -29,15 +62,32 @@ struct JsonMissingKeyError : std::exception
}
};
/** Exception thrown when a JSON key is present but its value has the wrong type.
*
* The diagnostic message is constructed lazily on the first call to `what()`
* to avoid a string allocation at the throw site.
*/
struct JsonTypeMismatchError : std::exception
{
/** The key whose value had an unexpected type. */
char const* const key;
/** Human-readable name of the expected type (e.g., `"string"`, `"uint64"`). */
std::string const expectedType;
/** Lazily-populated message returned by `what()`. */
mutable std::string msg;
/** Construct with the key name and the name of the expected type. */
JsonTypeMismatchError(json::StaticString const& k, std::string et)
: key{k.cStr()}, expectedType{std::move(et)}
{
}
/** Return a human-readable description of the type mismatch.
*
* The message string is built on the first call and cached in `msg`.
*/
char const*
what() const noexcept override
{
@@ -50,6 +100,28 @@ struct JsonTypeMismatchError : std::exception
}
};
/** Extract a typed value from a JSON object using an XRPL protocol field key.
*
* The key is taken from `field.getJsonName()`, a `Json::StaticString` that
* avoids dynamic allocation in the JSON library's hash map and ties every
* lookup to a declared XRPL protocol field.
*
* This primary template is intentionally unimplementable: the
* `static_assert` fires for any `T` that lacks an explicit specialization,
* turning unsupported types into a compile error rather than a runtime
* surprise. Specializations exist for `std::string`, `bool`,
* `std::uint64_t`, and `xrpl::Buffer` in this header, and for
* `xrpl::AccountID`, `xrpl::PublicKey`, and `xrpl::STAmount` in their
* respective headers.
*
* @tparam T The target C++ type. Must have an explicit specialization.
* @param v The JSON object to read from.
* @param field The SField identifying the key to look up.
* @return The extracted and type-checked value.
* @throws JsonMissingKeyError if the key is absent from `v`.
* @throws JsonTypeMismatchError if the key is present but the value cannot
* be interpreted as `T`.
*/
template <class T>
T
getOrThrow(json::Value const& v, xrpl::SField const& field)
@@ -57,6 +129,17 @@ getOrThrow(json::Value const& v, xrpl::SField const& field)
static_assert(sizeof(T) == -1, "This function must be specialized");
}
/** Extract a UTF-8 string from a JSON object field.
*
* No type coercion is applied: the value must be a JSON string (`isString()`).
* Numeric values are not accepted.
*
* @param v The JSON object to read from.
* @param field The SField identifying the key to look up.
* @return The string value.
* @throws JsonMissingKeyError if the key is absent.
* @throws JsonTypeMismatchError if the value is not a JSON string.
*/
template <>
inline std::string
getOrThrow(json::Value const& v, xrpl::SField const& field)
@@ -72,7 +155,20 @@ getOrThrow(json::Value const& v, xrpl::SField const& field)
return inner.asString();
}
// Note, this allows integer numeric fields to act as bools
/** Extract a boolean from a JSON object field.
*
* Accepts either a native JSON boolean or any integral value, where any
* non-zero integer maps to `true`. This mirrors a common XRPL convention
* where boolean-semantic fields such as `sfWasLockingChainSend` are encoded
* as `0`/`1` integers in JSON rather than as JSON `true`/`false`.
*
* @param v The JSON object to read from.
* @param field The SField identifying the key to look up.
* @return The boolean value.
* @throws JsonMissingKeyError if the key is absent.
* @throws JsonTypeMismatchError if the value is neither a JSON boolean nor an
* integral type.
*/
template <>
inline bool
getOrThrow(json::Value const& v, xrpl::SField const& field)
@@ -90,6 +186,26 @@ getOrThrow(json::Value const& v, xrpl::SField const& field)
return inner.asInt() != 0;
}
/** Extract a 64-bit unsigned integer from a JSON object field.
*
* Three source formats are accepted, in order of preference:
* 1. Native JSON unsigned integer (`isUInt()`).
* 2. Signed JSON integer that is non-negative (negative values throw).
* 3. Hex-encoded string, parsed via `std::from_chars` in base 16.
*
* The hex string path exists because 64-bit values exceed JavaScript's
* safe integer range (`2^53 - 1`), so some XRPL JSON producers encode
* large integers as hex strings. A partial parse — where `from_chars`
* succeeds but does not consume the entire string — is treated as a type
* error.
*
* @param v The JSON object to read from.
* @param field The SField identifying the key to look up.
* @return The extracted value as `std::uint64_t`.
* @throws JsonMissingKeyError if the key is absent.
* @throws JsonTypeMismatchError if the value is a negative integer, a string
* that is not valid hex, or a type that does not match any accepted form.
*/
template <>
inline std::uint64_t
getOrThrow(json::Value const& v, xrpl::SField const& field)
@@ -111,7 +227,6 @@ getOrThrow(json::Value const& v, xrpl::SField const& field)
if (inner.isString())
{
auto const s = inner.asString();
// parse as hex
std::uint64_t val = 0;
auto [p, ec] = std::from_chars(s.data(), s.data() + s.size(), val, 16);
@@ -123,6 +238,23 @@ getOrThrow(json::Value const& v, xrpl::SField const& field)
Throw<JsonTypeMismatchError>(key, "uint64");
}
/** Extract a raw byte buffer from a JSON object field encoded as a hex string.
*
* Delegates to `getOrThrow<std::string>` to fetch the raw hex string, then
* decodes it with `strUnHex`. A decode failure (e.g., odd length, non-hex
* characters) throws `JsonTypeMismatchError`.
*
* @note There is a conceptual mismatch between `xrpl::Buffer` (raw bytes) and
* the `STBlob` wire type. This specialization bridges that gap for fields
* like `sfSignature` when deserializing witness-server JSON.
*
* @param v The JSON object to read from.
* @param field The SField identifying the key to look up.
* @return The decoded byte buffer.
* @throws JsonMissingKeyError if the key is absent.
* @throws JsonTypeMismatchError if the value is not a string or contains
* invalid hex data.
*/
template <>
inline xrpl::Buffer
getOrThrow(json::Value const& v, xrpl::SField const& field)
@@ -137,7 +269,24 @@ getOrThrow(json::Value const& v, xrpl::SField const& field)
Throw<JsonTypeMismatchError>(field.getJsonName(), "Buffer");
}
// This function may be used by external projects (like the witness server).
/** Extract a typed value from a JSON object, returning `std::nullopt` on any
* error.
*
* Wraps `getOrThrow<T>` in a catch-all handler so that a missing key, a type
* mismatch, or any other exception simply yields `std::nullopt`. The
* catch-all is intentional: the caller's only question is whether the field is
* present and valid, not which specific error occurred.
*
* @note This function is part of the public API consumed by external projects
* such as the witness server, which runs outside the rippled process and
* needs to parse XRPL JSON payloads without depending on rippled internals.
*
* @tparam T The target C++ type. Must have a `getOrThrow` specialization.
* @param v The JSON object to read from.
* @param field The SField identifying the key to look up.
* @return The extracted value, or `std::nullopt` if the field is absent or
* cannot be decoded as `T`.
*/
template <class T>
std::optional<T>
getOptional(json::Value const& v, xrpl::SField const& field)

View File

@@ -1,26 +1,80 @@
/** @file
* Centralized registry of every JSON key name used in the XRPL rippled
* implementation.
*
* Rather than scattering string literals across hundreds of translation
* units, this header declares each key exactly once as a
* `constexpr json::StaticString` inside `xrpl::jss`. Any code that
* builds or inspects a `json::Value` — RPC handlers, ledger serializers,
* transaction processors, network-operation code — includes this header
* and writes `jss::account_data` instead of the raw string literal
* `"account_data"`.
*
* ## Naming conventions
*
* - **PascalCase** names (`Account`, `Amount`, `TransactionType`, …) are
* canonical transaction and ledger-entry field names defined by the XRPL
* wire protocol. Their casing is part of the protocol specification.
* - **snake_case** names (`account_data`, `ledger_index`, …) belong to the
* RPC API layer — the JSON objects exchanged over HTTP or WebSocket.
*
* ## Trailing comment legend
*
* Each declaration carries a compact trailing comment:
* - `in:` — an RPC handler reads this field from its input `json::Value`
* - `out:` — an RPC handler writes this field into its response
* - `field:` — a protocol-level field of a transaction or ledger entry
* - `RPC:` — part of the RPC request/response envelope
* - `error:` — part of the standard error-response shape
*
* ## ODR safety
*
* Every declaration uses `constexpr`, giving each variable internal
* linkage in C++17 and later. Including this header from many `.cpp`
* files does not violate the One Definition Rule.
*
* ## Transaction and ledger-entry names
*
* The tail of the file uses X-macros to derive `jss::Payment`,
* `jss::EscrowCreate`, `jss::Offer`, and every other transaction or
* ledger-entry type name directly from the canonical macro tables in
* `detail/transactions.macro` and `detail/ledger_entries.macro`.
* Adding a new type to those tables automatically registers its name
* here — the registry stays in sync by construction.
*/
#pragma once
#include <xrpl/json/json_value.h>
/** Centralized registry of every JSON key name used in the XRPL rippled
* implementation.
*
* Each identifier is a `constexpr json::StaticString` wrapping a
* string literal from the binary's read-only data segment. Indexing a
* `json::Value` with a `StaticString` stores only the pointer — no heap
* allocation — because the library knows the pointed-to string has static
* lifetime. This makes the efficient path the default for all named
* fields.
*
* @see jss.h for the full list of constants and their trailing comments.
*/
namespace xrpl::jss {
// NOLINTBEGIN(readability-identifier-naming)
// JSON static strings
/** Declares a `constexpr json::StaticString` whose identifier and string
* value are identical.
*
* The `#x` stringification ensures the C++ name and the JSON key can never
* silently diverge: renaming the identifier is a compile error at every
* usage site. The macro is `#undef`-ed at the end of the namespace block
* so it does not leak into surrounding translation-unit scope.
*
* @param x An unquoted C++ identifier that becomes both the variable name
* and the underlying JSON key string.
*/
#define JSS(x) constexpr ::json::StaticString x(#x)
/* These "StaticString" field names are used instead of string literals to
optimize the performance of accessing properties of json::Value objects.
Most strings have a trailing comment. Here is the legend:
in: Read by the given RPC handler from its `json::Value` parameter.
out: Assigned by the given RPC handler in the `json::Value` it returns.
field: A field of at least one type of transaction.
RPC: Common properties of RPC requests and responses.
error: Common properties of RPC error responses.
*/
JSS(AL_size); // out: GetCounts
JSS(AL_hit_rate); // out: GetCounts
JSS(AcceptedCredentials); // out: AccountObjects
@@ -678,6 +732,12 @@ JSS(warnings); // out: server_info, server_state
JSS(workers); //
JSS(write_load); // out: GetCounts
// --- X-macro expansion: transaction and ledger-entry type names ---
// Redefines TRANSACTION and LEDGER_ENTRY momentarily to emit a JSS()
// declaration for each type name registered in the canonical macro tables.
// Adding a new entry to transactions.macro or ledger_entries.macro
// automatically registers its name here — no manual update required.
#pragma push_macro("TRANSACTION")
#undef TRANSACTION
@@ -693,6 +753,10 @@ JSS(write_load); // out: GetCounts
#pragma push_macro("LEDGER_ENTRY_DUPLICATE")
#undef LEDGER_ENTRY_DUPLICATE
// LEDGER_ENTRY emits both the internal name and the rpcName alias.
// LEDGER_ENTRY_DUPLICATE emits only the rpcName to avoid re-declaring
// the identifier when a single type has two registered names (e.g.,
// DepositPreauth appears as both a transaction type and a ledger entry).
#define LEDGER_ENTRY(tag, value, name, rpcName, ...) \
JSS(name); \
JSS(rpcName);

View File

@@ -1,12 +1,35 @@
#pragma once
// Some versions of protobuf generate code that will produce errors during
// compilation. See https://github.com/google/protobuf/issues/549 for more
// details. We work around this by undefining this macro.
//
/** @file
* Safe include shim for the XRPL peer-to-peer protobuf definitions.
*
* All code that needs access to the XRPL wire-protocol types (the
* `protocol::MessageType` enum, `TMTransaction`, `TMProposeSet`,
* `TMValidation`, `TMSquelch`, and the rest of the peer message
* hierarchy) must include this header rather than
* `<xrpl/proto/xrpl.pb.h>` directly. The indirection exists to
* resolve a macro conflict that would otherwise produce cryptic build
* failures on affected platforms.
*
* Some versions of `protoc` emit C++ that uses `TYPE_BOOL` as a plain
* identifier. Certain platform headers (notably parts of the Windows
* SDK) define `TYPE_BOOL` as a preprocessor macro, so when
* `xrpl.pb.h` is compiled in a translation unit where that macro is
* live the preprocessor expands it mid-compilation and corrupts the
* generated symbol names. The `#ifdef` guard before the `#undef` is
* deliberate: it keeps the file a no-op on platforms that never define
* the macro, avoiding compilers that warn on `#undef` of an undefined
* name.
*
* @note The `TYPE_BOOL` workaround is technical debt. It should be
* removed once the project upgrades to a `protoc` version that no
* longer generates code conflicting with that macro name.
*
* @see https://github.com/google/protobuf/issues/549
*/
// TODO: Remove this after the protoc we use is upgraded to not generate
// code that conflicts with the TYPE_BOOL macro.
#ifdef TYPE_BOOL
#undef TYPE_BOOL
#endif

View File

@@ -1,3 +1,16 @@
/** @file
* Binary layout and field accessors for XRPL NFToken identifiers.
*
* Every `NFTokenID` on the ledger is a 256-bit big-endian structure that
* packs six fields: flags (2 bytes), transfer fee (2 bytes), issuer
* AccountID (20 bytes), ciphered taxon (4 bytes), and serial number
* (4 bytes). This file is the single source of truth for that layout;
* all other subsystems that need to inspect a token ID call the accessors
* defined here rather than duplicating the byte-offset arithmetic.
*
* @see nftPageMask.h for the `kPAGE_MASK` constant that isolates the low
* 96 bits used as the NFTokenPage sort key.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
@@ -11,30 +24,84 @@
namespace xrpl::nft {
// Separate taxons from regular integers.
/** Phantom type tag that makes `Taxon` a distinct type from plain integers.
*
* This empty struct exists solely to instantiate `TaggedInteger` so that
* the compiler rejects accidental integer/taxon mixups at call sites where
* taxon and serial number (both `uint32_t` underneath) appear side by side.
*/
struct TaxonTag
{
};
/** Strongly-typed wrapper for an NFT taxon value.
*
* A taxon is an issuer-assigned 32-bit category label embedded in every
* NFTokenID. The compiler distinguishes `Taxon` from raw `uint32_t`, so a
* serial number cannot be passed where a taxon is expected. Use `toTaxon()`
* and `toUInt32()` to cross the type boundary explicitly.
*/
using Taxon = TaggedInteger<std::uint32_t, TaxonTag>;
/** Convert a plain `uint32_t` to a `Taxon`.
*
* @param i The raw taxon value obtained from a transaction field or other
* integer source.
* @return A `Taxon` wrapping `i`.
*/
inline Taxon
toTaxon(std::uint32_t i)
{
return static_cast<Taxon>(i);
}
/** Convert a `Taxon` to a plain `uint32_t`.
*
* @param t The taxon to unwrap.
* @return The underlying integer value.
*/
inline std::uint32_t
toUInt32(Taxon t)
{
return static_cast<std::uint32_t>(t);
}
/** The issuer may burn the token even when it is held by another account. */
constexpr std::uint16_t const kFLAG_BURNABLE = 0x0001;
/** The token may only be bought or sold for XRP, not IOUs. */
constexpr std::uint16_t const kFLAG_ONLY_XRP = 0x0002;
/** Accepting a transfer of this token may open trust lines on the recipient's
* account to receive IOU royalty payments.
*
* @note Under amendment `fixEnforceNFTokenTrustline`, transfers with a
* non-zero transfer fee denominated in an IOU are rejected with
* `tecNO_LINE` unless this flag is set or the NFT issuer is also the
* IOU issuer.
*/
constexpr std::uint16_t const kFLAG_CREATE_TRUST_LINES = 0x0004;
/** The token may be transferred to accounts other than the issuer. */
constexpr std::uint16_t const kFLAG_TRANSFERABLE = 0x0008;
/** The token's URI may be updated after minting via `NFTokenModify`.
*
* Because flags are baked into bytes 01 of the token ID, this flag — like
* all flags — is immutable after minting. `NFTokenModify` reads it directly
* from the token ID via `getFlags()` and returns `tecNO_PERMISSION` if it
* is absent.
*/
constexpr std::uint16_t const kFLAG_MUTABLE = 0x0010;
/** Extract the flags field from an NFTokenID.
*
* Reads bytes 01 (big-endian) of `id` and converts to host byte order.
* Flags are immutable after minting because they are part of the token ID.
*
* @param id A 256-bit NFTokenID as stored on the ledger.
* @return The 16-bit flags bitmask; compare against the `kFLAG_*` constants.
*/
inline std::uint16_t
getFlags(uint256 const& id)
{
@@ -43,6 +110,16 @@ getFlags(uint256 const& id)
return boost::endian::big_to_native(flags);
}
/** Extract the transfer fee from an NFTokenID.
*
* Reads bytes 23 (big-endian) of `id` and converts to host byte order.
* The fee is expressed in basis points; 50000 represents 50%. Use
* `nft::transferFeeAsRate()` (in `Rate.h`) to convert to a `Rate` suitable
* for `multiply()`.
*
* @param id A 256-bit NFTokenID as stored on the ledger.
* @return Transfer fee in basis points (050000).
*/
inline std::uint16_t
getTransferFee(uint256 const& id)
{
@@ -51,6 +128,15 @@ getTransferFee(uint256 const& id)
return boost::endian::big_to_native(fee);
}
/** Extract the serial number from an NFTokenID.
*
* Reads bytes 2831 (big-endian) of `id` and converts to host byte order.
* The serial is the issuer's mint sequence at the time the token was created
* and is unique within a single issuer's token space.
*
* @param id A 256-bit NFTokenID as stored on the ledger.
* @return The 32-bit serial number.
*/
inline std::uint32_t
getSerial(uint256 const& id)
{
@@ -59,6 +145,38 @@ getSerial(uint256 const& id)
return boost::endian::big_to_native(seq);
}
/** Compute the ciphered taxon stored in an NFTokenID for a given mint.
*
* To prevent tokens minted under the same taxon from clustering in the same
* `NFTokenPage` objects (a ledger-write hotspot), the stored taxon is XORed
* with a scramble value derived from the mint sequence number via a Linear
* Congruential Generator:
*
* @code
* ciphered = taxon ^ (384160001 * tokenSeq + 2459)
* @endcode
*
* The LCG multiplier (384160001 ≡ 1 mod 4) and addend (2459, odd) satisfy
* the Hull-Dobell theorem, guaranteeing a full period over 2³² when
* arithmetic wraps naturally on `uint32_t`. Because `tokenSeq` advances
* monotonically and the issuer cannot choose it freely, successive mints
* under the same taxon land in very different positions in the page sort
* order, distributing load.
*
* Since XOR is its own inverse, calling this function a second time with the
* stored ciphered value recovers the original taxon — no separate decipher
* function is needed. `getTaxon()` relies on this property.
*
* @param tokenSeq The issuer's account sequence at the time of minting
* (the serial number that will be embedded in the token ID).
* @param taxon The issuer-supplied taxon to cipher.
* @return The ciphered taxon value to embed in bytes 2427 of the token ID.
*
* @note **Protocol-stable constants.** The LCG parameters 384160001 and
* 2459 are embedded in every NFTokenID ever minted. Changing them is
* a consensus-breaking change that requires an amendment and a way to
* distinguish token IDs generated under the old scheme.
*/
inline Taxon
cipheredTaxon(std::uint32_t tokenSeq, Taxon taxon)
{
@@ -83,6 +201,16 @@ cipheredTaxon(std::uint32_t tokenSeq, Taxon taxon)
return taxon ^ toTaxon(((384160001 * tokenSeq) + 2459));
}
/** Extract and decode the taxon from an NFTokenID.
*
* Reads bytes 2427 (big-endian) to obtain the stored ciphered taxon, then
* deciphers it by calling `cipheredTaxon()` a second time with the serial
* number from bytes 2831. Because XOR is its own inverse, this cancels the
* original scramble and returns the issuer-supplied taxon value.
*
* @param id A 256-bit NFTokenID as stored on the ledger.
* @return The original issuer-assigned taxon, not the ciphered value.
*/
inline Taxon
getTaxon(uint256 const& id)
{
@@ -95,6 +223,14 @@ getTaxon(uint256 const& id)
return cipheredTaxon(getSerial(id), toTaxon(taxon));
}
/** Extract the issuer AccountID from an NFTokenID.
*
* Reads bytes 423 of `id`, which hold the 20-byte issuer address in
* big-endian order as packed by `NFTokenMint::createNFTokenID()`.
*
* @param id A 256-bit NFTokenID as stored on the ledger.
* @return The 20-byte AccountID of the token's issuer.
*/
inline AccountID
getIssuer(uint256 const& id)
{

View File

@@ -6,8 +6,35 @@
namespace xrpl::nft {
// NFT directory pages order their contents based only on the low 96 bits of
// the NFToken value. This mask provides easy access to the necessary mask.
/** Bitmask that separates the owner-identity region of an NFToken page key
* from its token-ordering region.
*
* Every `NFTokenID` encodes the issuing `AccountID` in its high 160 bits and
* token-specific data (taxon + sequence) in its low 96 bits. Ledger page keys
* for `ltNFTOKEN_PAGE` SLEs fuse those two regions into a single `uint256`.
* `kPAGE_MASK` — 96 bits of `1` in the least-significant position, 160 bits
* of `0` above — is the canonical tool for splitting them:
*
* - `token & kPAGE_MASK` → low 96 bits (token-ordering portion)
* - `key & ~kPAGE_MASK` → high 160 bits (owner AccountID portion)
*
* Typical uses:
* - **Page key construction** (`Indexes.cpp`): `(k.key & ~kPAGE_MASK) + (token & kPAGE_MASK)`
* - **Max-page sentinel** (`keylet::nftpage_max`): initialize the full `uint256`
* to `kPAGE_MASK` (all-ones in the low 96 bits), then overwrite the high
* bytes with the owner's `AccountID` to form the lexicographically largest
* page key for that owner.
* - **RPC pagination** (`AccountNFTs.cpp`, `AccountObjects.cpp`): validate
* that a client-supplied marker or `entryIndex` belongs to the correct page.
* - **Sort-order comparisons** (`NFTokenHelpers.cpp`): tokens with different
* low-96-bit values belong to different pages.
* - **Invariant checks** (`NFTInvariant.cpp`): verify that a page being deleted
* carries the all-ones sentinel in its low 96 bits.
*
* @note The value is `constexpr`, resolved entirely at compile time via
* `base_uint`'s `std::string_view` constructor, so including this header
* has no runtime cost and raises no ODR concerns.
*/
uint256 constexpr kPAGE_MASK(
std::string_view("0000000000000000000000000000000000000000ffffffffffffffffffffffff"));

View File

@@ -6,7 +6,22 @@
namespace xrpl {
/** Serialize an object to a blob. */
/** Serialize a protocol object to its canonical wire-format byte sequence.
*
* Constructs a `Serializer` pre-reserved to 256 bytes, invokes
* `o.add(s)` to write the object's canonical binary encoding, and returns
* a copy of the accumulated buffer. The returned `Blob` is independently
* owned by the caller — no lifetime dependency on the transient `Serializer`
* is created.
*
* @tparam Object Any type that implements `void add(Serializer&) const` —
* the canonical serialization interface defined by `STBase` and
* honoured by every concrete protocol type (`STTx`, `STLedgerEntry`,
* transaction-metadata objects, etc.).
* @param o The object to serialize.
* @return A `Blob` (`std::vector<uint8_t>`) containing the complete
* wire-format encoding of `o`.
*/
template <class Object>
Blob
serializeBlob(Object const& o)
@@ -16,7 +31,19 @@ serializeBlob(Object const& o)
return s.peekData();
}
/** Serialize an object to a hex string. */
/** Serialize an `STObject` to an uppercase hex string of its wire encoding.
*
* Convenience wrapper around `serializeBlob` + `strHex`, covering the
* common RPC pattern of rendering a transaction, ledger entry, or metadata
* object as a hex string for a JSON response (e.g. `tx_blob`, `meta_blob`,
* `data` fields). The concrete `STObject` parameter — rather than a
* template — avoids unnecessary instantiation overhead for this
* narrow but frequent use case.
*
* @param o The `STObject` (or `STObject`-derived type) to serialize.
* @return An uppercase hex string of the canonical wire encoding of `o`.
* @see serializeBlob
*/
inline std::string
serializeHex(STObject const& o)
{

View File

@@ -1,3 +1,36 @@
/** @file
* Umbrella header for the XRPL Serialized Type (ST) system.
*
* Including this header makes the complete ST type vocabulary available in a
* single line. Every type that can appear inside a transaction, ledger entry,
* or consensus message is reachable transitively:
*
* - **Field registry** — `SField`, `SOTemplate`, `SOEStyle`, and the full set
* of named fields (`sfAmount`, `sfDestination`, …) via `SField.h`.
* - **Polymorphic root** — `STBase` and the `detail::STVar` small-buffer
* helper via `STBase.h`.
* - **Scalar leaves** — `STInteger<T>` / `STUInt8` … `STUInt64` / `STInt32`
* (`STInteger.h`); `STBitString<Bits>` / `STUInt128` … `STUInt256`
* (`STBitString.h`); `STBlob` (`STBlob.h`); `STAccount` (`STAccount.h`).
* - **Composite containers** — `STObject` with `SOTemplate` enforcement and
* typed proxy accessors (`STObject.h`); `STArray` of inner `STObject`
* values (`STArray.h`); `STVector256` for multi-hash fields (`STVector256.h`).
* - **Domain-specific types** — `STAmount` (XRP drops or IOU amount),
* `STNumber` (high-precision asset-contextual value), `STPathSet` (payment
* path graphs), `STIssue`, `STCurrency`.
* - **High-level protocol objects** — `STTx` (signed transaction with
* cached ID and type), `STLedgerEntry` / `SLE` (keyed ledger state entry),
* `STValidation` (consensus vote with lazy signature cache).
* - **JSON boundary** — `STParsedJSON` for deserializing a `Json::Value` tree
* into an `STObject` or `STArray`.
*
* @note Code that works with only one or two ST primitives should prefer the
* individual headers to keep compile-time dependencies minimal. Include
* `st.h` when a translation unit genuinely needs the full protocol-object
* vocabulary (transaction processing, serialization, RPC formatting, etc.).
*
* @see STBase.h, STObject.h, STTx.h, STLedgerEntry.h, STValidation.h
*/
#pragma once
#include <xrpl/protocol/SField.h>

View File

@@ -1,3 +1,27 @@
/** @file
* Public interface for Base58Check encoding and decoding of XRPL
* cryptographic identifiers.
*
* Raw byte sequences — account IDs, node keys, seeds — are converted to and
* from the human-readable strings that appear in XRPL transactions and client
* APIs (e.g., `rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh` for an account address).
*
* Every encoded token follows the wire layout:
* @code
* [ type byte (1) ][ raw payload (N) ][ checksum (4) ]
* @endcode
* where the checksum is the first four bytes of SHA-256(SHA-256(type ‖ payload)).
*
* Three namespaces expose encoding/decoding:
* - `xrpl::` — public API; dispatches to `b58_fast` on GCC/Clang, `b58_ref`
* on MSVC.
* - `xrpl::b58_ref` — portable O(n²) reference implementation (all compilers).
* - `xrpl::b58_fast` — 1015× faster implementation using GCC's
* `unsigned __int128`; guarded by `#ifndef _MSC_VER`.
*
* @see https://xrpl.org/base58-encodings.html
*/
#pragma once
#include <xrpl/basics/Expected.h>
@@ -12,91 +36,308 @@
namespace xrpl {
/** Result type for fallible Base58Check codec operations.
*
* Holds either a value of type `T` on success, or a `std::error_code`
* (wrapping a `TokenCodecErrc` enumerator) on failure. Marked
* `[[nodiscard]]` via `Expected` so callers cannot silently ignore errors.
*
* @tparam T The success-path value type (e.g., `std::span<std::uint8_t>`).
* @see TokenCodecErrc
*/
template <class T>
using B58Result = Expected<T, std::error_code>;
/** One-byte version prefixes for each XRPL identifier category.
*
* The prefix byte is prepended to the raw payload before Base58Check
* encoding. It causes the resulting string to begin with a
* category-specific letter in the XRPL alphabet — e.g., account addresses
* start with `'r'` and family seeds start with `'s'` — providing a
* recognisable visual cue without decoding.
*
* `None` and `FamilyGenerator` are reserved and currently unused; their
* numeric values must not be reassigned to new token categories.
*
* @note These values are protocol-stable and must never be changed. All
* Base58Check-encoded XRPL identifiers in existence carry one of these
* bytes as their first decoded byte.
*/
enum class TokenType : std::uint8_t {
None = 1, // unused
NodePublic = 28,
NodePrivate = 32,
AccountID = 0,
AccountPublic = 35,
AccountSecret = 34,
FamilyGenerator = 41, // unused
FamilySeed = 33
None = 1, ///< Reserved; unused.
NodePublic = 28, ///< Validator/peer public key.
NodePrivate = 32, ///< Validator/peer private key.
AccountID = 0, ///< Classic account address (20-byte hash).
AccountPublic = 35, ///< Account public key.
AccountSecret = 34, ///< Account private key.
FamilyGenerator = 41, ///< Reserved; unused.
FamilySeed = 33 ///< Key-generation seed (16 bytes).
};
/** Decode a Base58Check string into a typed XRPL value.
*
* The overload with no explicit `TokenType` is used when the target type
* `T` carries its own implicit token type (e.g., `parseBase58<AccountID>`).
* The overload that accepts a `TokenType` is used when the type byte must
* be supplied by the caller (e.g., `parseBase58<PublicKey>(TokenType::NodePublic, s)`).
*
* No definition is provided in this header. Explicit template
* specialisations for each concrete XRPL type live alongside that type's
* own implementation (e.g., `AccountID.cpp`, `PublicKey.cpp`).
*
* @tparam T The target XRPL type to parse into.
* @param s The Base58Check-encoded string to decode.
* @return The decoded value, or `std::nullopt` if the string is invalid,
* has the wrong token type, or fails its checksum.
*/
template <class T>
[[nodiscard]] std::optional<T>
parseBase58(std::string const& s);
/** Decode a Base58Check string with an explicit token type into a typed XRPL value.
*
* @tparam T The target XRPL type to parse into.
* @param type The expected one-byte token-type prefix. Decoding fails if
* the decoded prefix does not match.
* @param s The Base58Check-encoded string to decode.
* @return The decoded value, or `std::nullopt` on any error.
*/
template <class T>
[[nodiscard]] std::optional<T>
parseBase58(TokenType type, std::string const& s);
/** Encode data in Base58Check format using XRPL alphabet
For details on the format see
https://xrpl.org/base58-encodings.html#base58-encodings
@param type The type of token to encode.
@param token Pointer to the data to encode.
@param size The size of the data to encode.
@return the encoded token.
*/
/** Encode data in Base58Check format using the XRPL alphabet.
*
* Prepends `type` as a one-byte version prefix, appends a 4-byte
* SHA-256(SHA-256(type ‖ data)) checksum, then Base58-encodes the
* concatenation.
*
* On non-MSVC platforms this dispatches to `b58_fast::encodeBase58Token`;
* on MSVC it falls back to `b58_ref::encodeBase58Token`.
*
* @param type The token category prefix byte.
* @param token Pointer to the raw payload bytes.
* @param size Number of payload bytes.
* @return The Base58Check-encoded string.
*
* @see https://xrpl.org/base58-encodings.html
*/
[[nodiscard]] std::string
encodeBase58Token(TokenType type, void const* token, std::size_t size);
/** Decode a Base58Check string and validate its token type and checksum.
*
* Returns the raw payload bytes (without the type prefix or checksum) as a
* string, or an empty string if the input is malformed, the token type does
* not match `type`, or the checksum fails.
*
* On non-MSVC platforms this dispatches to `b58_fast::decodeBase58Token`;
* on MSVC it falls back to `b58_ref::decodeBase58Token`.
*
* @param s The Base58Check-encoded string to decode.
* @param type The expected one-byte token-type prefix.
* @return The raw decoded payload, or an empty string on any error.
* @note Error detail is lost on failure; prefer the span-based
* `b58_fast::decodeBase58Token` overload where typed errors matter.
*/
[[nodiscard]] std::string
decodeBase58Token(std::string const& s, TokenType type);
/** Portable O(n²) reference implementation of the XRPL Base58Check codec.
*
* Adapted from Bitcoin Core. Performs direct base-256 ↔ base-58 digit
* conversion without any compiler extensions, making it available on all
* platforms including MSVC. For large payloads the O(n²) cost is
* measurable; prefer `b58_fast` where available.
*/
namespace b58_ref {
// The reference version does not use gcc extensions (int128 in particular)
/** Encode data in Base58Check format using the XRPL alphabet.
*
* Portable reference implementation; does not use `__int128` or other
* compiler extensions.
*
* @param type The token category prefix byte.
* @param token Pointer to the raw payload bytes.
* @param size Number of payload bytes.
* @return The Base58Check-encoded string.
*/
[[nodiscard]] std::string
encodeBase58Token(TokenType type, void const* token, std::size_t size);
/** Decode a Base58Check string and validate its token type and checksum.
*
* Portable reference implementation.
*
* @param s The Base58Check-encoded string to decode.
* @param type The expected one-byte token-type prefix.
* @return The raw decoded payload, or an empty string on any error.
*/
[[nodiscard]] std::string
decodeBase58Token(std::string const& s, TokenType type);
/** Raw base-conversion primitives, exposed for unit testing only.
*
* These functions operate on byte spans without any XRPL framing (no
* token-type prefix and no checksum), allowing the numeric conversion to
* be verified in isolation.
*/
namespace detail {
// Expose detail functions for unit tests only
/** Encode raw bytes into a Base58 string using the XRPL alphabet.
*
* Performs the O(n²) base-256 → base-58 conversion. No token-type prefix
* or checksum is applied.
*
* @param message Pointer to the input bytes.
* @param size Number of input bytes.
* @param temp Caller-supplied scratch buffer; must be at least `size` bytes.
* @param tempSize Size of the scratch buffer in bytes.
* @return The Base58-encoded string.
* @note Exposed for unit testing only; production code should call
* `b58_ref::encodeBase58Token`.
*/
std::string
encodeBase58(void const* message, std::size_t size, void* temp, std::size_t tempSize);
/** Decode a raw Base58 string into bytes using the XRPL alphabet.
*
* Performs the base-58 → base-256 conversion without validating any token
* prefix or checksum.
*
* @param s The Base58-encoded string to decode.
* @return The decoded bytes, or an empty string if any character is outside
* the XRPL Base58 alphabet.
* @note Exposed for unit testing only; production code should call
* `b58_ref::decodeBase58Token`.
*/
std::string
decodeBase58(std::string const& s);
} // namespace detail
} // namespace b58_ref
#ifndef _MSC_VER
/** 1015× faster Base58Check codec using GCC/Clang's `unsigned __int128`.
*
* The algorithm routes through an intermediate base-58^10 representation.
* `58^10 = 430,804,206,899,405,824` fits in a 64-bit register, so groups of
* ten base-58 digits can be processed as a single 64-bit word. The
* expensive multi-precision arithmetic is then performed on far fewer, larger
* coefficients. The three-stage pipeline is:
* @code
* base 58 → base 58^10 → base 2^64 → base 2^8
* @endcode
* Conversions between bases that are powers of one another are trivial
* concatenations; only the middle hop requires multi-precision work.
*
* The span-based overloads avoid heap allocation in the hot path. The
* `std::string`-returning overloads are provided for API compatibility but
* each require one allocation.
*
* @note Not available on MSVC, which lacks `unsigned __int128`.
*/
namespace b58_fast {
// Use the fast version (10-15x faster) is using gcc extensions (int128 in
// particular)
/** Encode data in Base58Check format into a caller-supplied buffer.
*
* Prepends `tokenType` as a one-byte version prefix, appends a 4-byte
* checksum, and Base58-encodes the result into `out`. No heap allocation.
*
* @param tokenType The token category prefix byte.
* @param input The raw payload bytes to encode.
* @param out Buffer to receive the Base58Check-encoded bytes. Must be
* large enough to hold the encoded output.
* @return On success, a sub-span of `out` covering the encoded bytes.
* On failure, a `std::error_code` wrapping a `TokenCodecErrc` value
* (e.g., `OutputTooSmall` if `out` is insufficient).
*/
[[nodiscard]] B58Result<std::span<std::uint8_t>>
encodeBase58Token(
TokenType tokenType,
std::span<std::uint8_t const> input,
std::span<std::uint8_t> out);
/** Decode a Base58Check string into a caller-supplied buffer.
*
* Validates the token-type prefix and 4-byte checksum before writing to
* `outBuf`. No heap allocation.
*
* @param type The expected one-byte token-type prefix.
* @param s The Base58Check-encoded string to decode.
* @param outBuf Buffer to receive the raw decoded payload bytes. Must be
* large enough for the payload (payload size = decoded size 5).
* @return On success, a sub-span of `outBuf` covering the decoded bytes.
* On failure, a `std::error_code` wrapping a `TokenCodecErrc`
* (e.g., `MismatchedTokenType`, `MismatchedChecksum`, `OutputTooSmall`,
* or `BadB58Character`).
*/
[[nodiscard]] B58Result<std::span<std::uint8_t>>
decodeBase58Token(TokenType type, std::string_view s, std::span<std::uint8_t> outBuf);
// This interface matches the old interface, but requires additional allocation
/** Encode data in Base58Check format, returning a `std::string`.
*
* Legacy-compatible overload matching the `b58_ref` API. Requires one
* heap allocation for the returned string.
*
* @param type The token category prefix byte.
* @param token Pointer to the raw payload bytes.
* @param size Number of payload bytes.
* @return The Base58Check-encoded string.
*/
[[nodiscard]] std::string
encodeBase58Token(TokenType type, void const* token, std::size_t size);
// This interface matches the old interface, but requires additional allocation
/** Decode a Base58Check string, returning a `std::string`.
*
* Legacy-compatible overload matching the `b58_ref` API. Requires one
* heap allocation for the returned string. Error detail is lost on
* failure; prefer the span-based overload where typed errors matter.
*
* @param s The Base58Check-encoded string to decode.
* @param type The expected one-byte token-type prefix.
* @return The raw decoded payload, or an empty string on any error.
*/
[[nodiscard]] std::string
decodeBase58Token(std::string const& s, TokenType type);
/** Raw base-conversion primitives, exposed for unit testing only.
*
* These functions perform the numeric base conversion without any XRPL
* framing (no token-type prefix and no checksum), enabling isolated testing
* of the fast-path arithmetic.
*/
namespace detail {
// Expose detail functions for unit tests only
/** Convert big-endian base-256 bytes to a big-endian Base58 byte sequence.
*
* Uses the three-stage `b256 → b58^10 → b2^64 → b58` pipeline. No token
* prefix or checksum is applied.
*
* @param input The big-endian base-256 bytes to convert.
* @param out Buffer to receive the Base58-encoded bytes.
* @return On success, a sub-span of `out` covering the result.
* On failure, a `std::error_code` (e.g., `OutputTooSmall`).
* @note Exposed for unit testing only.
*/
B58Result<std::span<std::uint8_t>>
b256ToB58Be(std::span<std::uint8_t const> input, std::span<std::uint8_t> out);
/** Convert a big-endian Base58 byte sequence to big-endian base-256 bytes.
*
* Uses the three-stage `b58 → b58^10 → b2^64 → b256` pipeline. No token
* prefix or checksum validation is performed.
*
* @param input The Base58-encoded string to decode.
* @param out Buffer to receive the big-endian base-256 bytes.
* @return On success, a sub-span of `out` covering the result.
* On failure, a `std::error_code` (e.g., `BadB58Character`,
* `OutputTooSmall`, or `OverflowAdd`).
* @note Exposed for unit testing only.
*/
B58Result<std::span<std::uint8_t>>
b58ToB256Be(std::string_view input, std::span<std::uint8_t> out);
} // namespace detail
} // namespace b58_fast

View File

@@ -4,6 +4,24 @@
namespace xrpl {
/**
* Transactor for `ttNFTOKEN_CREATE_OFFER` (type code 27).
*
* Places a buy or sell offer for an NFT into the ledger. When `tfSellNFToken`
* is set the submitter is offering to sell an NFT they own; when unset they
* are offering to buy an NFT currently held by the account named in
* `sfOwner`. The distinction drives which account's NFT directory is searched
* in `preclaim` and which reserve is charged in `doApply`.
*
* All three pipeline phases delegate almost entirely to free functions in
* `NFTokenHelpers` (`tokenOfferCreatePreflight`, `tokenOfferCreatePreclaim`,
* `tokenOfferCreateApply`) that are shared with `NFTokenMint`, which can
* create a sell offer atomically at mint time. This sharing ensures the two
* transactors cannot drift apart on offer-creation rules.
*
* @note `ConsequencesFactory{Normal}` means a fee is charged on failure; this
* transaction does not block later transactions from the same account.
*/
class NFTokenCreateOffer : public Transactor
{
public:
@@ -13,24 +31,98 @@ public:
{
}
/**
* Returns the flag mask for this transaction type (`tfNFTokenCreateOfferMask`).
*
* The framework passes this mask to `preflight1`, which rejects any
* transaction whose flags include bits not defined for
* `ttNFTOKEN_CREATE_OFFER`. This guards against clients setting
* unrecognized bits that could acquire meaning under future amendments.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/**
* Stateless structural validation — no ledger access.
*
* Extracts NFT flags embedded in `sfNFTokenID` (e.g. `lsfBurnable`,
* `lsfOnlyXRP`, `lsfTransferable`) then delegates to
* `nft::tokenOfferCreatePreflight()`, which validates the offer amount,
* optional destination and expiration, ownership rules, and transaction
* flags.
*
* @return `tesSUCCESS` if the transaction is structurally valid; a
* `tem*` code otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/**
* Read-only ledger checks after signature verification.
*
* Performs two checks before delegating to the shared helper:
*
* 1. **Expiration guard** — rejects with `tecEXPIRED` if the offer's
* `sfExpiration` has already passed the current ledger close time.
* This cannot be done in `preflight` because close time is ledger
* state.
* 2. **Token ownership** — for sell offers, confirms `sfAccount` holds
* the NFT; for buy offers, confirms `sfOwner` holds it. Returns
* `tecNO_ENTRY` if the token is absent from the expected directory.
*
* After these two checks, delegates to
* `nft::tokenOfferCreatePreclaim()` for business-logic validation
* (transfer fee eligibility, destination account existence, etc.).
*
* @return `tesSUCCESS` on success; `tecEXPIRED`, `tecNO_ENTRY`, or
* another `tec*`/`ter*` code on failure.
*/
static TER
preclaim(PreclaimContext const& ctx);
/**
* Creates the `NFTokenOffer` ledger object and inserts it into the
* appropriate buy or sell directory.
*
* Delegates entirely to `nft::tokenOfferCreateApply()`, passing all
* transaction fields plus `preFeeBalance_` (the submitter's XRP balance
* before fee deduction) for reserve checking.
*
* @return `tesSUCCESS` on success; a `tec*` code if the offer object
* cannot be created (e.g. `tecINSUFFICIENT_RESERVE`).
*/
TER
doApply() override;
/**
* Per-entry invariant visitor hook.
*
* No transaction-specific invariants are currently enforced; this is a
* no-op placeholder for future work.
*
* @param isDelete `true` if the entry is being deleted.
* @param before The SLE state before the transaction, or `nullptr`.
* @param after The SLE state after the transaction, or `nullptr`.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/**
* Post-execution invariant finalizer.
*
* No transaction-specific invariants are currently enforced; always
* returns `true`. Placeholder for future work.
*
* @param tx The transaction being applied.
* @param result The `TER` code produced by `doApply`.
* @param fee The fee charged, in drops.
* @param view The ledger view after the transaction.
* @param j Journal for diagnostic logging.
* @return `true` — all invariants pass (none are currently defined).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -6,6 +6,24 @@
namespace xrpl {
/** Transactor for the `NFTokenMint` transaction type.
*
* Creates a new non-fungible token on the XRP Ledger and records it in the
* issuer's on-ledger NFToken page structure. Optionally, in the same atomic
* step, it can create an initial sell offer for the newly minted token when
* `featureNFTokenMintOffer` is active and the transaction includes offer
* fields (`sfAmount`, optionally `sfDestination` and `sfExpiration`).
*
* The offer-creation helpers (`nft::tokenOfferCreatePreflight`,
* `nft::tokenOfferCreatePreclaim`, `nft::tokenOfferCreateApply`) are shared
* with `NFTokenCreateOffer` to keep offer-validation logic consistent whether
* an offer is created standalone or bundled into a mint. The embedded-offer
* path always creates a sell offer — atomic mint-and-buy-offer is not
* supported.
*
* @note `kCONSEQUENCES_FACTORY` is `Normal`; minting imposes no extraordinary
* sequencing constraints on other transactions from the same account.
*/
class NFTokenMint : public Transactor
{
public:
@@ -15,27 +33,137 @@ public:
{
}
/** Gate the embedded-offer sub-feature during preflight.
*
* Returns `false` (blocking the transaction) if the transaction contains
* any of `sfAmount`, `sfDestination`, or `sfExpiration` — the fields used
* to bundle an initial sell offer — and `featureNFTokenMintOffer` is not
* enabled on the current network. Without this guard a mint-and-offer
* transaction could activate on a network that has not voted for the
* combined flow.
*
* @param ctx Preflight context; no ledger access is performed.
* @return `true` if the transaction is compatible with active amendments;
* `false` if offer fields are present but `featureNFTokenMintOffer`
* is not yet enabled.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Return the valid transaction-flag bitmask for the current amendment set.
*
* The set of legal flags is amendment-sensitive in two ways:
* - `tfTrustLine` was permanently disabled by `fixRemoveNFTokenAutoTrustLine`
* to close a denial-of-service vector: two cooperating accounts could trade
* an NFT in a loop, each transfer creating a new trustline on the issuer
* and growing the issuer's reserve without bound.
* - `tfMutable` (mutable-URI NFTs) is added by `featureDynamicNFT`.
*
* The returned mask is one of four combinations depending on which of these
* two amendments are active.
*
* @param ctx Preflight context used to query active amendments.
* @return A bitmask of `tf*` flags that are legal for this transaction.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Stateless validation of `NFTokenMint` transaction fields.
*
* Checks mint-specific field constraints:
* - `sfTransferFee` must not exceed `maxTransferFee`; if non-zero,
* `tfTransferable` must also be set (a transfer fee on a non-transferable
* token is contradictory).
* - `sfIssuer`, when present, must not equal `sfAccount` — the
* authorized-minter pattern requires distinct minter and issuer accounts.
* - `sfURI`, when present, must be non-empty and within `maxTokenURILength`.
* - If offer fields are present, `sfAmount` is mandatory; offer field
* validity is then delegated to `nft::tokenOfferCreatePreflight()`.
*
* @param ctx Preflight context; no ledger access is performed.
* @return `tesSUCCESS` if all fields are valid; a `tem*` code describing
* the first constraint violated.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Read-only ledger validation for the `NFTokenMint` transaction.
*
* When `sfIssuer` is present (authorized-minter path), reads the issuer's
* `AccountRoot` and verifies that its `sfNFTokenMinter` field matches the
* signing account. A missing issuer account returns `tecNO_ISSUER`; a
* mismatch returns `tecNO_PERMISSION`.
*
* If offer fields are present, also invokes `nft::tokenOfferCreatePreclaim()`
* to check offer-specific ledger conditions (expiry, trustline authorisation,
* deep-freeze status, etc.).
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if all checks pass; `tecNO_ISSUER` if the issuer
* account does not exist; `tecNO_PERMISSION` if the signing account is
* not the designated minter; or any error propagated from
* `nft::tokenOfferCreatePreclaim()`.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the `NFTokenMint` transaction to the mutable ledger view.
*
* Execution proceeds in up to four steps:
* 1. **Sequence bookkeeping**: On the issuer's `AccountRoot`,
* `sfFirstNFTokenSequence` is seeded on the first mint (set to the
* issuer's current account sequence, decremented by one when the issuer
* submits the transaction directly with a sequence number because the
* sequence has already been pre-incremented by this point).
* `sfMintedNFTokens` is incremented; overflow returns
* `tecMAX_SEQUENCE_REACHED`.
* 2. **Token insertion**: The NFToken `STObject` is assembled with the
* computed ID and optional URI, then placed into the issuer's NFToken
* page structure via `nft::insertToken()`.
* 3. **Optional sell offer**: If `sfAmount` is present,
* `nft::tokenOfferCreateApply()` creates a sell offer atomically.
* 4. **Reserve check**: Performed only when the owner count increased
* relative to its pre-mint value — packing NFTs into an existing page
* does not raise the owner count and therefore does not require a
* reserve top-up.
*
* @return `tesSUCCESS` on success; `tecMAX_SEQUENCE_REACHED` if the
* issuer's NFT sequence counter would overflow; `tecINSUFFICIENT_RESERVE`
* if a new page or sell offer was created and the issuer cannot cover
* the increased reserve; or any error from `nft::insertToken()` or
* `nft::tokenOfferCreateApply()`.
*/
TER
doApply() override;
/** Per-entry invariant visitor for `NFTokenMint`.
*
* Tracks NFToken page and NFT issuance count changes to verify that
* minting results in exactly the expected ledger mutations.
*
* @param isDelete True if the entry is being deleted.
* @param before SLE state before the transaction (null for insertions).
* @param after SLE state after the transaction (null for deletions).
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-apply invariant finalizer for `NFTokenMint`.
*
* Verifies that the net NFT count change across all visited entries is
* consistent with a successful mint (+1 net token created).
*
* @param tx The transaction being applied.
* @param result The TER result from `doApply()`.
* @param fee The fee charged for this transaction.
* @param view Read-only view of the ledger after apply.
* @param j Journal for diagnostic logging.
* @return `true` if the invariant holds; `false` if an unexpected NFT
* count discrepancy is detected.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -44,7 +172,36 @@ public:
ReadView const& view,
beast::Journal const& j) override;
// Public to support unit tests.
/** Construct the 32-byte big-endian NFToken ID from its component fields.
*
* The layout is:
*
* | Bytes | Content |
* |-------|----------------------------------|
* | 01 | Flags (2 bytes, big-endian) |
* | 23 | Transfer fee (2 bytes, big-endian)|
* | 423 | Issuer `AccountID` (20 bytes) |
* | 2427 | Ciphered taxon (4 bytes, big-endian) |
* | 2831 | Token sequence number (4 bytes, big-endian) |
*
* The taxon is scrambled via `nft::cipheredTaxon()` before packing:
* `taxon ^ ((384160001 * tokenSeq) + 2459)`. This linear congruential
* transform — a permutation of the 32-bit integer space by the Hull-Dobell
* theorem — distributes tokens with the same taxon across different NFToken
* pages, avoiding page hot-spots that would degrade lookup and deletion
* performance. The constants are **protocol-frozen**: changing them would
* corrupt interpretation of existing token IDs and require a new amendment.
*
* Exposed publicly to enable unit testing without a full transaction context.
*
* @param flags Two-byte flag field (e.g., `nft::kFLAG_TRANSFERABLE`).
* @param fee Transfer fee in units of 1/100,000 of a percent (050,000).
* @param issuer The issuer's 20-byte `AccountID`.
* @param taxon The unscrambled taxon value assigned by the issuer.
* @param tokenSeq The per-issuer mint sequence number used as the cipher key
* and stored in the final 4 bytes of the ID.
* @return A `uint256` token ID encoding all five fields.
*/
static uint256
createNFTokenID(
std::uint16_t flags,

View File

@@ -4,6 +4,21 @@
namespace xrpl {
/** Transactor for the `NFTokenModify` transaction type.
*
* Updates the URI metadata of an existing, mutable NFT on the XRP Ledger.
* Mutability is a permanent, at-mint choice encoded directly in the 256-bit
* token ID via `nft::kFLAG_MUTABLE`; tokens minted without that flag cannot
* be modified under any circumstances.
*
* Both the original issuer and a designated authorized minter (the account
* recorded in the issuer's `sfNFTokenMinter` field) may submit this
* transaction. Any other account receives `tecNO_PERMISSION`.
*
* @note `kCONSEQUENCES_FACTORY` is `Normal`; URI modification imposes no
* extraordinary sequencing constraints on other transactions from the
* same account.
*/
class NFTokenModify : public Transactor
{
public:
@@ -13,21 +28,91 @@ public:
{
}
/** Stateless validation of `NFTokenModify` transaction fields.
*
* Performs two pure-data checks without consulting ledger state:
* - If `sfOwner` is present, it must not equal `sfAccount`. The field is
* only meaningful when a designated minter is acting on behalf of the
* actual owner; a self-referential value is malformed.
* - If `sfURI` is present, it must be non-empty and no longer than
* `kMAX_TOKEN_URI_LENGTH` (256 bytes). A present-but-empty URI is
* rejected as ambiguous. Omitting `sfURI` entirely is valid and signals
* that the URI should not be changed.
*
* @param ctx Preflight context; no ledger access is performed.
* @return `tesSUCCESS` if all fields are valid; `temMALFORMED` if
* `sfOwner` equals `sfAccount`, or if `sfURI` is present but empty
* or exceeds the maximum length.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Read-only ledger validation for the `NFTokenModify` transaction.
*
* Resolves the effective owner (`sfOwner` if present, otherwise
* `sfAccount`) and then performs the following checks in order:
* 1. The NFT identified by `sfNFTokenID` must exist in the owner's token
* directory; absent tokens return `tecNO_ENTRY`.
* 2. The NFT's `nft::kFLAG_MUTABLE` bit (encoded in the token ID) must be
* set; immutable tokens return `tecNO_PERMISSION`.
* 3. If the signing account is not the original issuer (extracted from the
* token ID via `nft::getIssuer`), the issuer's `AccountRoot` must have
* `sfNFTokenMinter` pointing to the signing account; any other account
* returns `tecNO_PERMISSION`.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if all checks pass; `tecNO_ENTRY` if the NFT does
* not exist; `tecNO_PERMISSION` if the token is immutable or the
* caller is neither the issuer nor the designated minter;
* `tecINTERNAL` if the issuer's `AccountRoot` cannot be read (ledger
* corruption sentinel, excluded from coverage).
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the `NFTokenModify` transaction to the mutable ledger view.
*
* Resolves the effective owner (`sfOwner` if present, otherwise
* `sfAccount`) and delegates all ledger writes to
* `nft::changeTokenURI()`, which locates the correct `NFTokenPage` SLE
* and updates the stored URI in place. Passing `ctx_.tx[~sfURI]` (an
* optional) allows the helper to distinguish an explicit URI update from
* an omitted field.
*
* @return The `TER` result from `nft::changeTokenURI()`, which is
* `tesSUCCESS` on a successful write or an error code if the page
* cannot be located.
*/
TER
doApply() override;
/** Per-entry invariant visitor for `NFTokenModify`.
*
* No transaction-specific invariants are currently enforced; the override
* is a placeholder for future work.
*
* @param isDelete True if the entry is being deleted.
* @param before SLE state before the transaction (null for insertions).
* @param after SLE state after the transaction (null for deletions).
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-apply invariant finalizer for `NFTokenModify`.
*
* No transaction-specific invariants are currently enforced; always
* returns `true`. The override is a placeholder for future work.
*
* @param tx The transaction being applied.
* @param result The TER result from `doApply()`.
* @param fee The fee charged for this transaction.
* @param view Read-only view of the ledger after apply.
* @param j Journal for diagnostic logging.
* @return Always `true`.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,15 +4,18 @@
namespace xrpl {
/**
Price Oracle is a system that acts as a bridge between
a blockchain network and the external world, providing off-chain price data
to decentralized applications (dApps) on the blockchain. This implementation
conforms to the requirements specified in the XLS-47d.
The OracleDelete transactor implements the deletion of Oracle objects.
*/
/** Transactor for the `OracleDelete` transaction type (XLS-47d).
*
* Removes a Price Oracle object from the ledger. Price Oracles bridge the
* blockchain and external world by publishing off-chain asset prices on-chain
* for consumption by decentralised applications.
*
* This transactor pairs with `OracleSet`, which handles creation and updates.
* Deletion needs no field validation — all meaningful checks are deferred to
* `preclaim`. The static `deleteOracle` helper exposes the core mutation logic
* so that `AccountDelete` can clean up oracles during account removal without
* constructing a full transactor instance.
*/
class OracleDelete : public Transactor
{
public:
@@ -22,21 +25,58 @@ public:
{
}
/** Stateless preflight validation.
*
* A delete request carries only an `sfOracleDocumentID`; there is nothing
* to validate without ledger state, so this always returns `tesSUCCESS`.
* All substantive checks occur in `preclaim`.
*
* @param ctx Preflight context (no ledger access).
* @return `tesSUCCESS` unconditionally.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Read-only ledger validation.
*
* Confirms that the oracle identified by `sfOracleDocumentID` exists at
* `keylet::oracle(account, documentId)` and that its `sfOwner` field
* matches the submitting account. Because the keylet embeds the account,
* an ownership mismatch is structurally unreachable through normal flow
* (the branch is `LCOV_EXCL`).
*
* @param ctx Preclaim context (read-only ledger access).
* @return `tesSUCCESS` if the oracle exists and is owned by the submitter;
* `tecNO_ENTRY` if the oracle does not exist;
* `terNO_ACCOUNT` if the submitting account is missing (unreachable
* under normal conditions — account existence is enforced earlier).
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the oracle deletion to the mutable ledger view.
*
* Peeks the oracle SLE and delegates immediately to `deleteOracle`.
* Returns `tecINTERNAL` if the SLE cannot be found (unreachable under
* normal conditions — `preclaim` already confirmed existence).
*
* @return `tesSUCCESS` on success; `tecINTERNAL` if the oracle SLE is
* unexpectedly missing.
*/
TER
doApply() override;
/** Per-entry invariant visitor (no-op; reserved for future use). */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-transaction invariant check (no-op; reserved for future use).
*
* @return `true` unconditionally.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -45,6 +85,32 @@ public:
ReadView const& view,
beast::Journal const& j) override;
/** Remove a Price Oracle SLE from the ledger and adjust owner reserves.
*
* Exposed as a static helper so that `AccountDelete` (and any other
* transactor that must sweep owned objects) can invoke oracle deletion
* without constructing an `OracleDelete` transactor instance — the same
* pattern as `Transactor::ticketDelete`.
*
* Owner-reserve adjustment follows XLS-47d: oracles with more than five
* `sfPriceDataSeries` entries occupy **two** reserve slots when created,
* so `-2` is passed to `adjustOwnerCount` for those; all others use `-1`.
* This threshold must stay in sync with the creation logic in `OracleSet`.
*
* Deletion order: `dirRemove` from the owner directory → `adjustOwnerCount`
* → `view.erase`. Erasing before `dirRemove` would lose the cached page
* index stored in `sfOwnerNode`.
*
* @param view Mutable apply view.
* @param sle The oracle SLE to delete; must not be null.
* @param account Account that owns the oracle.
* @param j Journal for diagnostic logging.
* @return `tesSUCCESS` on success;
* `tefBAD_LEDGER` if removal from the owner directory fails (signals
* ledger corruption — `LCOV_EXCL`);
* `tecINTERNAL` if `sle` is null or the owner `AccountRoot` is missing
* (`LCOV_EXCL`).
*/
static TER
deleteOracle(
ApplyView& view,

View File

@@ -4,15 +4,18 @@
namespace xrpl {
/**
Price Oracle is a system that acts as a bridge between
a blockchain network and the external world, providing off-chain price data
to decentralized applications (dApps) on the blockchain. This implementation
conforms to the requirements specified in the XLS-47d.
The OracleSet transactor implements creating or updating Oracle objects.
*/
/** Transactor for the `OracleSet` transaction type (XLS-47d).
*
* Handles both creation and in-place update of Price Oracle ledger objects.
* A Price Oracle bridges off-chain data sources (e.g., exchange price feeds)
* and on-ledger consumers by publishing timestamped token-pair prices under
* an `(account, sfOracleDocumentID)` key. The counterpart transactor,
* `OracleDelete`, handles removal.
*
* @note Owner-count reserve follows a tiered model: oracles with more than
* five `sfPriceDataSeries` entries consume **two** reserve slots; all
* others consume one. This threshold is shared with `OracleDelete`.
*/
class OracleSet : public Transactor
{
public:
@@ -22,21 +25,103 @@ public:
{
}
/** Stateless structural validation (no ledger access).
*
* Checks that `sfPriceDataSeries` is non-empty and within
* `kMAX_ORACLE_DATA_SERIES`, and that the optional string fields
* `sfProvider`, `sfURI`, and `sfAssetClass` — when present — are
* non-empty and within their respective maximum lengths.
*
* Deeper token-pair consistency is deferred to `preclaim`, which can
* compare against the existing oracle SLE.
*
* @param ctx Preflight context (no ledger access).
* @return `temARRAY_EMPTY` if `sfPriceDataSeries` is empty;
* `temARRAY_TOO_LARGE` if it exceeds `kMAX_ORACLE_DATA_SERIES`;
* `temMALFORMED` if any optional string field violates its length
* bounds; `tesSUCCESS` otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Read-only ledger validation; distinguishes create from update.
*
* **Timestamp freshness.** `sfLastUpdateTime` is stored in XRPL epoch
* seconds (offset from Unix epoch by `kEPOCH_OFFSET`). The converted
* Unix timestamp must lie within `±kMAX_LAST_UPDATE_TIME_DELTA` of the
* ledger's `closeTime`. On update it must also be strictly greater than
* the existing oracle's `sfLastUpdateTime`.
*
* **Create.** If no oracle SLE exists for `(account, sfOracleDocumentID)`,
* `sfProvider` and `sfAssetClass` are mandatory, and any entry in
* `sfPriceDataSeries` that lacks `sfAssetPrice` is malformed (there is
* nothing to delete from a non-existent object).
*
* **Update.** If the oracle SLE exists, `sfProvider` and `sfAssetClass`
* may be omitted; if supplied they must match the stored values (these
* fields are immutable after creation). Entries without `sfAssetPrice`
* signal deletion; a deletion request for a pair not in the current
* oracle returns `tecTOKEN_PAIR_NOT_FOUND`. Duplicate pairs within a
* single transaction are rejected.
*
* **Reserve.** The account balance is checked against the reserve
* required by the resulting pair count (using the tiered 1-or-2-slot
* model) before the fee is deducted.
*
* @param ctx Preclaim context (read-only ledger access).
* @return `tesSUCCESS` on success;
* `terNO_ACCOUNT` if the submitting account does not exist;
* `tecINVALID_UPDATE_TIME` if the timestamp is out of the freshness
* window or is not strictly newer than the existing oracle's
* timestamp on update;
* `tecTOKEN_PAIR_NOT_FOUND` if a deletion targets a pair absent from
* the current oracle;
* `tecARRAY_EMPTY` if the resolved pair set after merging is empty;
* `tecARRAY_TOO_LARGE` if the resolved pair set exceeds
* `kMAX_ORACLE_DATA_SERIES`;
* `tecINSUFFICIENT_RESERVE` if the account cannot cover the reserve;
* `temMALFORMED` for structural violations (duplicate pairs,
* self-referential base/quote, mismatched immutable fields, or
* a deletion entry in a creation context).
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the oracle create or update to the mutable ledger view.
*
* **Update path.** Loads all existing pairs into a
* `std::map<pair<Currency,Currency>, STObject>` keyed by `tokenPairKey`,
* applies the transaction's entries (delete / update-in-place / insert),
* and serialises the result back as `sfPriceDataSeries`. Owner-count is
* adjusted for any tier change. `sfURI` and `sfLastUpdateTime` are
* updated unconditionally. Under `fixIncludeKeyletFields`, `sfOracleDocumentID`
* is back-filled onto older objects that predate the amendment.
*
* **Create path.** Constructs a new SLE, inserts it into the owner
* directory, and increments the owner count by 1 or 2 per the tiered
* model. Under `fixPriceOracleOrder`, the initial `sfPriceDataSeries`
* is sorted via the same map approach as the update path; without the
* amendment, raw transaction order is preserved (legacy behaviour).
*
* @return `tesSUCCESS` on success;
* `tecDIR_FULL` if the owner directory is full (create path only);
* `tefINTERNAL` if the account SLE cannot be peeked for owner-count
* adjustment (unreachable under normal conditions).
*/
TER
doApply() override;
/** Per-entry invariant visitor (no-op; reserved for future work). */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-transaction invariant check (no-op; reserved for future work).
*
* @return `true` unconditionally.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,6 +4,20 @@
namespace xrpl {
/** Transactor for the `DepositPreauth` transaction type.
*
* Manages the whitelist of accounts and credential-bearing parties permitted
* to send payments to an account that has enabled `lsfDepositAuth`. Without a
* preauthorization entry, any inbound payment to such an account fails.
*
* Supports four mutually exclusive operations, selected by which single field
* is present in the transaction: `sfAuthorize`, `sfUnauthorize`,
* `sfAuthorizeCredentials`, and `sfUnauthorizeCredentials`. The credential
* variants are gated on the `featureCredentials` amendment.
*
* @note The `ConsequencesFactory` is `Normal` — this transaction does not
* block other transactions from the same account in the fee queue.
*/
class DepositPreauth : public Transactor
{
public:
@@ -13,24 +27,83 @@ public:
{
}
/** Gate credential-based operations on the `featureCredentials` amendment.
*
* Returns `false` (causing `invokePreflight` to emit `temDISABLED`) when
* `sfAuthorizeCredentials` or `sfUnauthorizeCredentials` is present but
* the `featureCredentials` amendment is not yet active on the ledger.
*
* @param ctx Preflight context carrying the transaction and active rules.
* @return `true` if the transaction is permitted to proceed to `preflight`;
* `false` if a required amendment is disabled.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Stateless validation of field presence and basic semantic correctness.
*
* Enforces that exactly one of `sfAuthorize`, `sfUnauthorize`,
* `sfAuthorizeCredentials`, or `sfUnauthorizeCredentials` is present,
* returning `temMALFORMED` otherwise. For account-based operations,
* validates that the target `AccountID` is non-zero and that the
* authorizing account is not attempting to preauthorize itself
* (`temCANNOT_PREAUTH_SELF`). For credential-based operations, delegates
* array validation to `credentials::checkArray`.
*
* @param ctx Preflight context; no ledger view is available.
* @return `tesSUCCESS` on valid input, or a `tem*` code describing the
* specific malformation.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Read-only ledger checks for authorization and de-authorization.
*
* For `sfAuthorize`: verifies the target account exists (`tecNO_TARGET`)
* and that no duplicate `DepositPreauth` entry already exists for the pair
* (`tecDUPLICATE`). For `sfUnauthorize`: verifies the entry being removed
* is present (`tecNO_ENTRY`). Credential variants additionally verify that
* every credential issuer is a live account (`tecNO_ISSUER`), and sort the
* credential set canonically before computing the ledger key to ensure the
* duplicate check is order-independent.
*
* @param ctx Preclaim context providing a read-only ledger view.
* @return `tesSUCCESS`, or a `tec*`/`tef*` code if a ledger-state
* constraint is violated.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the preauthorization change to the ledger.
*
* For authorization operations (`sfAuthorize` / `sfAuthorizeCredentials`),
* checks the owner reserve against `preFeeBalance_` (the balance before
* fee deduction), creates a `DepositPreauth` SLE, inserts it into the
* ledger and the account's owner directory, and increments the owner count.
* For de-authorization, delegates to `removeFromLedger`. Credentials are
* re-sorted before being written to the SLE to preserve canonical order.
*
* @return `tesSUCCESS` on success, `tecINSUFFICIENT_RESERVE` if the
* account lacks sufficient reserve to own another entry, or a `tef*`
* code on internal ledger inconsistency.
* @note Reserve is checked against `preFeeBalance_` so that an account
* may spend its base reserve on fees while still being rejected for
* creating a new owned object it cannot afford.
*/
TER
doApply() override;
/** No-op invariant visitor; transaction-specific invariants not yet defined. */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** No-op invariant finalizer; transaction-specific invariants not yet defined.
*
* @return Always `true`.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -39,7 +112,21 @@ public:
ReadView const& view,
beast::Journal const& j) override;
// Interface used by AccountDelete
/** Remove a `DepositPreauth` SLE from the ledger by its index.
*
* Locates the entry, removes it from the owner's directory using the
* `sfOwnerNode` page cached in the SLE, decrements the owner count, and
* erases the entry. Called both from `doApply` (for `sfUnauthorize` /
* `sfUnauthorizeCredentials`) and from `AccountDelete` when cleaning up
* all owned objects before deleting an account.
*
* @param view Mutable ledger view on which to operate.
* @param delIndex Ledger key of the `DepositPreauth` entry to remove.
* @param j Journal for diagnostic logging.
* @return `tesSUCCESS` on success; `tecNO_ENTRY` if the SLE is missing;
* `tefBAD_LEDGER` if the owner-directory removal fails (indicates
* ledger corruption — should be unreachable under normal operation).
*/
static TER
removeFromLedger(ApplyView& view, uint256 const& delIndex, beast::Journal j);
};

View File

@@ -4,48 +4,211 @@
namespace xrpl {
/**
* Transactor for the `ttPAYMENT` transaction type.
*
* Handles three structurally distinct execution paths within a single protocol
* transaction type: direct XRP-to-XRP transfers, direct MPToken (MPT) transfers
* (pre-`featureMPTokensV2`), and cross-currency path-based payments routed
* through `path::RippleCalc`. The branching is an implementation detail hidden
* from the serialized transaction format.
*
* Pipeline stages follow the standard `Transactor` framework:
* `checkExtraFeatures` → `preflight` → `preflight2` → `preclaim` → `doApply`.
* All stages except `doApply` are static; compile-time dispatch via
* `invokePreflight<Payment>` rather than vtable.
*
* @note `ConsequencesFactory` is `Custom` because the maximum XRP spend depends
* on `sfSendMax` vs. `sfAmount` — the framework invokes `makeTxConsequences`
* to obtain a conservative upper bound for queue ordering.
*/
class Payment : public Transactor
{
/* The largest number of paths we allow */
/** Maximum number of paths allowed in `sfPaths`. */
static std::size_t const kMAX_PATH_SIZE = 6;
/* The longest path we allow */
/** Maximum number of steps permitted in a single path. */
static std::size_t const kMAX_PATH_LENGTH = 8;
public:
/**
* Signals to the framework that this transactor computes its own
* `TxConsequences` via `makeTxConsequences` rather than using the
* standard normal-fee or blocker logic.
*/
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Custom;
explicit Payment(ApplyContext& ctx) : Transactor(ctx)
{
}
/**
* Computes the maximum XRP that this transaction could consume.
*
* If `sfSendMax` is present and denominated in XRP, its value is the
* upper bound. Otherwise `sfAmount` is used if it is native. If neither
* field is XRP, the maximum is zero (no XRP is spent beyond the fee).
* The result is used by the transaction queue for consequence ordering.
*
* @param ctx Stateless preflight context providing the raw transaction.
* @return `TxConsequences` carrying the computed maximum XRP spend.
*/
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
/**
* Gates optional transaction fields behind their required amendments.
*
* Returns `false` (suppressing the transaction) if:
* - `sfCredentialIDs` is present but `featureCredentials` is not enabled, or
* - `sfDomainID` is present but `featurePermissionedDEX` is not enabled.
*
* @param ctx Preflight context including the active rule set.
* @return `true` if all present optional fields are supported; `false` otherwise.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/**
* Returns the set of transaction flags that are valid for this payment.
*
* For MPT-denominated payments when `featureMPTokensV2` is not active,
* only `tfPartialPayment` is permitted beyond the universal flags. Once
* `MPTokensV2` is active, the full `tfPaymentMask` (which includes
* `tfLimitQuality`, `tfNoRippleDirect`, etc.) becomes valid because MPT
* payments can then participate in path-finding.
*
* @param ctx Preflight context providing the transaction and active rules.
* @return Bitmask of flags that must be zero for a well-formed transaction.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/**
* Performs stateless structural validation of the payment fields.
*
* Checks performed (no ledger access):
* - MPT-denominated `sfAmount` requires `featureMPTokensV1`.
* - XRP direct payments must not carry `sfSendMax`, `sfPaths`,
* `tfPartialPayment`, `tfLimitQuality`, or `tfNoRippleDirect` — each
* produces a distinct `temBAD_SEND_XRP_*` error for client diagnostics.
* - Pre-`MPTokensV2` MPT payments must not carry `sfPaths`.
* - Self-payments without `sfPaths` are rejected as `temREDUNDANT`;
* self-payments with paths are allowed (arbitrage cycle).
* - When `sfDeliverMin` is set, `tfPartialPayment` must be set, the amount
* must be positive, its asset must match `sfAmount`, and it must not
* exceed `sfAmount`.
* - Credential fields are structurally validated via `credentials::checkFields`.
*
* @param ctx Preflight context; no ledger view is available.
* @return `tesSUCCESS` if structurally valid, or a `tem*` error code.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/**
* Validates delegation permission for the payment when `sfDelegate` is set.
*
* Two-tier check:
* 1. Full permission: if the `DelegateObject` SLE grants blanket `ttPAYMENT`
* permission, the payment is allowed unconditionally.
* 2. Granular permissions: `PaymentMint` permits a delegate to issue tokens
* where the delegating account is the asset issuer; `PaymentBurn` permits
* sending tokens back to their issuer. Both grants apply only to *direct*
* payments — any payment with `sfPaths` set, or where `sfSendMax` refers
* to a different asset than `sfAmount`, is denied. This prevents granular
* delegation from being used to manipulate multi-hop paths.
*
* If `sfDelegate` is absent, returns `tesSUCCESS` immediately.
*
* @param view Read-only ledger view used to look up the delegate SLE.
* @param tx The transaction being validated.
* @return `tesSUCCESS` if permitted, `terNO_DELEGATE_PERMISSION` otherwise.
*/
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
/**
* Validates ledger-state preconditions for the payment.
*
* Checks performed (read-only ledger access):
* - Non-native payments to a non-existent destination fail `tecNO_DST`;
* only XRP can fund a new account.
* - Partial payments cannot create accounts (`telNO_DST_PARTIAL`).
* - XRP amount below base reserve cannot create an account (`tecNO_DST_INSUF_XRP`).
* - Destination with `lsfRequireDestTag` requires `sfDestinationTag`
* (`tecDST_TAG_NEEDED`).
* - Path complexity: more than `kMAX_PATH_SIZE` (6) paths, or any path
* exceeding `kMAX_PATH_LENGTH` (8) steps, is rejected `telBAD_PATH_COUNT`.
* - Credential validity is checked via `credentials::valid`.
* - If `sfDomainID` is present, both sender and destination must be members
* of the specified permissioned domain (`tecNO_PERMISSION`).
*
* @param ctx Preclaim context providing a read-only ledger view.
* @return `tesSUCCESS`, or a `tec*`/`tel*`/`ter*` error code.
*/
static TER
preclaim(PreclaimContext const& ctx);
/**
* Executes the payment and mutates ledger state.
*
* Branches into one of three execution paths based on asset type and flags:
*
* **RippleCalc (path-based):** Any payment with `sfPaths`, `sfSendMax`, or
* a non-native destination amount (except direct MPT pre-`MPTokensV2`)
* routes through `path::RippleCalc::rippleCalculate` inside a
* `PaymentSandbox`. The sandbox is applied atomically only if the
* calculation succeeds. `ter*` retry codes from `RippleCalc` are promoted
* to `tecPATH_DRY` to ensure fee collection and deter path-spam.
* `ctx_.deliver()` records the actual delivered amount when it differs from
* the requested amount (enabling the `DeliveredAmount` metadata field).
*
* **Direct MPT (pre-`featureMPTokensV2`):** Bypasses `RippleCalc`. Checks
* `requireAuth`, `canTransfer`, and frozen state, then applies the transfer
* rate. If `tfPartialPayment` is set and the sender cannot cover the full
* transfer-rate-adjusted cost, the delivered amount is scaled down and
* checked against `sfDeliverMin`. `fixMPTDeliveredAmount` gates whether
* `ctx_.deliver()` is called for partial or rate-adjusted deliveries.
*
* **Direct XRP:** Validates that `preFeeBalance_` covers the destination
* amount plus the reserve (adjusted for whether the source account or a
* delegate pays the fee). Rejects pseudo-account recipients. Enforces
* deposit pre-authorization with a small-balance bypass to prevent accounts
* from becoming permanently wedged. Updates both `sfBalance` fields directly.
*
* @return `tesSUCCESS` on success, or a `tec*` error code on failure.
*/
TER
doApply() override;
/**
* Invariant visitor called once per modified SLE.
*
* Currently a no-op placeholder for future transaction-specific invariants.
*
* @param isDelete `true` if the SLE is being erased.
* @param before SLE state before the transaction, or `nullptr` if new.
* @param after SLE state after the transaction, or `nullptr` if deleted.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/**
* Finalizes transaction-specific invariant checks after all SLEs are visited.
*
* Currently a no-op placeholder; always returns `true`.
*
* @param tx The transaction that was applied.
* @param result The TER code returned by `doApply`.
* @param fee The fee charged in drops.
* @param view Read-only view of the ledger after the transaction.
* @param j Journal for diagnostic logging.
* @return `true` if all invariants pass; `false` to veto the transaction.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,6 +4,25 @@
namespace xrpl {
/** Transactor for the PaymentChannelClaim transaction type.
*
* Payment channels are off-ledger scaling constructs: a sender locks XRP
* into an on-ledger `ltPAYCHAN` object via `PaymentChannelCreate`, then
* issues incrementally larger signed authorization vouchers off-ledger.
* Each voucher encodes a new *cumulative* `sfBalance` — "the receiver may
* now claim up to N drops total from this channel." When either party
* wants to settle, they submit a `PaymentChannelClaim` transaction. This
* transactor handles all settlement scenarios: balance claims, voluntary
* close, expiry-triggered close, and expiry removal (`tfRenew`).
*
* @note `kCONSEQUENCES_FACTORY` is `Normal` (not `Custom` as in
* `PaymentChannelCreate` / `PaymentChannelFund`) because a Claim only
* redistributes already-locked XRP and cannot introduce new XRP
* obligations for the submitting account.
*
* @see PaymentChannelCreate — opens the channel and locks capacity.
* @see PaymentChannelFund — adds XRP to an existing channel.
*/
class PaymentChannelClaim : public Transactor
{
public:
@@ -13,27 +32,145 @@ public:
{
}
/** Gate optional fields on their governing amendments.
*
* Rejects the transaction with `temDISABLED` if `sfCredentialIDs` is
* present but the `featureCredentials` amendment is not yet active.
* Called by the framework before `preflight`.
*
* @param ctx Preflight context providing the transaction and active rules.
* @return `true` if all optional fields are permitted under current rules;
* `false` to abort with `temDISABLED`.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Return the set of legal flag bits for this transaction type.
*
* The base-class `preflight1` enforces this mask before invoking the
* transactor-specific `preflight`, so unknown flags are rejected early.
*
* @param ctx Preflight context (unused; present for framework
* compatibility).
* @return `tfPaymentChannelClaimMask`.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Stateless validation of the transaction fields.
*
* Checks that can be performed without ledger access, in order:
* - `sfBalance` and `sfAmount`, when present, must be positive XRP;
* `sfBalance` must not exceed `sfAmount`.
* - `tfClose` and `tfRenew` are mutually exclusive.
* - If `sfSignature` is present, both `sfPublicKey` and `sfBalance`
* must also be present (a bare signature is malformed).
* - The off-channel payment voucher signature is verified here against
* the transaction-supplied public key via
* `serializePayChanAuthorization`. This is distinct from the
* transaction's own signature (verified by the framework in
* `preflight2`); the voucher authorizes the receiver to claim funds,
* not the transaction itself.
* - Credential field structure is validated via
* `credentials::checkFields`.
*
* @note The public key is not yet matched against the channel's stored
* key here because `preflight` has no ledger access; that check
* occurs in `doApply`.
*
* @param ctx Preflight context.
* @return `tesSUCCESS` on success, or a `tem*` code describing the
* specific malformation.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validate credentials against live ledger state.
*
* Called only when `featureCredentials` is enabled. Delegates to the
* base-class no-op otherwise. Uses `credentials::valid()` to check
* that any `sfCredentialIDs` entries exist in the ledger and are not
* expired. Credential *cleanup* (removing expired objects) is deferred
* to `verifyDepositPreauth` in `doApply`.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS`, or a `ter*`/`tec*` code if credentials are
* invalid or expired.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Settle the payment channel on the ledger.
*
* Executes up to four independent behaviors, evaluated in order:
*
* 1. **Expiry check (highest priority).** If the ledger's
* `parentCloseTime` has passed either `sfCancelAfter` or
* `sfExpiration`, `closeChannel()` is called immediately regardless
* of other fields or flags. A claim transaction can thus serve as
* the trigger that closes an expired channel even when no XRP
* transfer is requested.
*
* 2. **Permission check.** Only the channel source (`sfAccount`) or
* destination (`sfDestination`) may interact with the channel.
*
* 3. **Balance claim** (when `sfBalance` is present). Transfers the
* incremental difference `reqBalance - chanBalance` to the
* destination. `sfBalance` is cumulative: the actual XRP moved is
* only the new portion beyond what was already claimed. The
* destination must supply `sfSignature`; the source does not need
* one. The supplied public key is matched against the channel's
* stored `sfPublicKey`. `verifyDepositPreauth` is called before
* the transfer to honor any DepositPreauth restrictions.
*
* 4. **Renew** (`tfRenew`). Clears `sfExpiration`, retracting a
* previously issued close request. Only the source may renew.
*
* 5. **Close** (`tfClose`). If the destination is closing, or the
* channel is fully drained (`sfBalance == sfAmount`), the channel
* closes immediately via `closeChannel()`. If the source is
* closing a partially-funded channel, `sfExpiration` is set to
* `parentCloseTime + sfSettleDelay`, giving the destination a
* guaranteed settlement window; a sooner existing expiration is
* never overwritten.
*
* @return `tesSUCCESS` on success, or a `tec*`/`tem*` code:
* - `tecNO_TARGET` — channel object not found.
* - `tecNO_PERMISSION` — submitter is neither source nor
* destination.
* - `tecUNFUNDED_PAYMENT` — requested balance does not exceed the
* already-settled balance (nothing new to transfer), or exceeds
* the channel's total funded capacity.
* - `temBAD_SIGNATURE` — destination attempted a claim without
* supplying a signature.
* - `temBAD_SIGNER` — supplied public key does not match the
* channel's stored key.
* - `tecNO_DST` — destination account not found.
*/
TER
doApply() override;
/** Per-entry invariant hook (no-op; reserved for future use).
*
* @param isDelete Whether the entry is being deleted.
* @param before SLE state before the transaction.
* @param after SLE state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-transaction invariant check (no-op; reserved for future use).
*
* @param tx The applied transaction.
* @param result The TER returned by `doApply`.
* @param fee The fee deducted.
* @param view Read-only view of the resulting ledger state.
* @param j Journal for diagnostic logging.
* @return Always `true` (no invariants enforced yet).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,6 +4,27 @@
namespace xrpl {
/**
* Transactor for opening a unidirectional XRP payment channel (`ttPAYCHAN_CREATE`).
*
* Payment channels let two parties exchange a stream of XRP micropayments
* off-ledger via signed claim messages, while committing only the open and
* close operations to the global ledger. This transactor handles the first
* on-ledger step: locking the sender's XRP into a new `PayChannel` SLE and
* registering it in both parties' owner directories.
*
* Pipeline contract:
* - `makeTxConsequences` — reports the full `sfAmount` as consumed XRP so the
* transaction queue accurately prices the account's locked funds.
* - `preflight` — field-level validation only; no ledger access.
* - `preclaim` — read-only ledger checks (reserve, destination existence,
* permission flags, pseudo-account guard).
* - `doApply` — constructs the `PayChannel` SLE, inserts dual directory
* entries, and debits the sender's balance.
*
* `ConsequencesFactory` is `Custom` because the channel locks the full
* `sfAmount` of XRP beyond just the fee, unlike a `Normal` transactor.
*/
class PaymentChannelCreate : public Transactor
{
public:
@@ -13,24 +34,118 @@ public:
{
}
/**
* Report the worst-case XRP consumed so the transaction queue can reserve
* the right amount.
*
* Returns a `TxConsequences` whose consumed XRP is `ctx.tx[sfAmount].xrp()`
* — the full channel funding, not merely the fee. This distinction matters
* for pending-transaction accounting: the account cannot spend the escrowed
* XRP while this transaction awaits inclusion.
*
* @param ctx Preflight context providing access to the transaction fields.
* @return `TxConsequences` reflecting fee + full channel amount.
*/
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
/**
* Validate transaction fields before any ledger access.
*
* Enforces three structural invariants:
* 1. `sfAmount` must be a positive XRP value; IOUs are not permitted in
* payment channels.
* 2. `sfAccount` must differ from `sfDestination`; self-funded channels
* are rejected with `temDST_IS_SRC`.
* 3. `sfPublicKey` must parse as a known key type (secp256k1 or Ed25519);
* this key signs off-ledger claims and cannot be changed after creation.
*
* @param ctx Preflight context providing the transaction and current rules.
* @return `tesSUCCESS`, or a `tem*` error code.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/**
* Validate ledger state before mutating it.
*
* Checks (in order):
* - Sender account exists.
* - Sender balance covers the new owner reserve (`tecINSUFFICIENT_RESERVE`)
* and the full channel amount on top of that (`tecUNFUNDED`). The two
* codes are intentionally distinct: `tecINSUFFICIENT_RESERVE` means the
* account cannot hold another ledger object at all; `tecUNFUNDED` means
* it can hold the object but cannot fund it at the requested level.
* - Destination account exists (`tecNO_DST`).
* - Destination has not set `lsfDisallowIncomingPayChan` (`tecNO_PERMISSION`).
* - Destination tag is present when required by `lsfRequireDestTag`
* (`tecDST_TAG_NEEDED`).
* - Destination is not a pseudo-account (`tecNO_PERMISSION`). This check
* is not amendment-gated because pseudo-account discriminator fields are
* themselves only written under amendment guards, so the check's
* behaviour automatically tracks the active amendments.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS`, or a `tec*`/`ter*` error code.
*/
static TER
preclaim(PreclaimContext const& ctx);
/**
* Construct the `PayChannel` SLE and commit all ledger mutations.
*
* Performs the following steps in order:
* 1. **Expiry check** (under `fixPayChanCancelAfter`): if `sfCancelAfter`
* is present and already past the ledger's `parentCloseTime`, returns
* `tecEXPIRED`. This runs in `doApply`, not `preclaim`, because the
* canonical close time is only fully settled at apply time.
* 2. **Channel SLE construction**: creates a `PayChannel` object at
* `keylet::payChan(account, dst, tx.getSeqValue())`. Using the
* sequence-or-ticket value as part of the key makes the channel ID
* deterministic before the transaction lands — off-ledger parties can
* reference the channel in signed claims before confirmation. The SLE
* captures total escrowed funds (`sfAmount`), running paid balance
* initialised to zero (`sfBalance`), both account IDs, settle delay,
* signing public key, and optional cancel-after/tag fields. Under
* `fixIncludeKeyletFields`, `sfSequence` is also stored so the keylet
* can be reconstructed from the SLE alone.
* 3. **Dual directory registration**: inserts the channel into the sender's
* owner directory (`sfOwnerNode`) and the recipient's owner directory
* (`sfDestinationNode`), enabling efficient removal on closure.
* 4. **Balance and owner count update**: decrements sender `sfBalance` by
* the channel amount and calls `adjustOwnerCount(..., +1, ...)` to
* raise the sender's minimum reserve.
*
* @return `tesSUCCESS` on success, or a `tef*`/`tec*` code on failure.
*/
TER
doApply() override;
/**
* Per-entry invariant visitor (reserved for future transaction-specific
* invariant checks; currently a no-op).
*
* @param isDelete True if the entry is being deleted.
* @param before SLE state before the transaction, or null if new.
* @param after SLE state after the transaction, or null if deleted.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/**
* Post-apply invariant finalizer (reserved for future transaction-specific
* invariant checks; currently always returns `true`).
*
* @param tx The applied transaction.
* @param result The TER returned by `doApply`.
* @param fee The fee charged for this transaction.
* @param view Read-only view of the ledger after application.
* @param j Journal for diagnostic logging.
* @return `true` if all invariants pass; `false` to veto the result.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,6 +4,20 @@
namespace xrpl {
/** Adds XRP to an existing payment channel and optionally extends its expiration.
*
* One of three transactors in the payment channel lifecycle (alongside
* `PaymentChannelCreate` and `PaymentChannelClaim`). The channel owner may
* top up the escrowed XRP balance without closing and reopening the channel,
* and may independently extend the voluntary `sfExpiration` deadline.
*
* `ConsequencesFactory` is `Custom` because the consumed XRP is the full
* `sfAmount` being deposited — not merely the fee — and the transaction queue
* must price it accordingly.
*
* No `preclaim` override is defined; all meaningful ledger preconditions are
* checked efficiently inside `doApply` once the channel SLE is in hand.
*/
class PaymentChannelFund : public Transactor
{
public:
@@ -13,21 +27,86 @@ public:
{
}
/** Report the worst-case XRP consumed so the transaction queue can reserve
* the right amount.
*
* Returns a `TxConsequences` whose consumed XRP equals `ctx.tx[sfAmount].xrp()`
* — the full deposit, not merely the fee. Pending-transaction accounting
* treats this XRP as unavailable until the transaction is included or dropped.
*
* @param ctx Preflight context providing access to the transaction fields.
* @return `TxConsequences` reflecting fee + full deposit amount.
*/
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
/** Validate transaction fields before any ledger access.
*
* Enforces two structural rules:
* 1. `sfAmount` must be XRP; IOU amounts are not permitted in payment channels.
* 2. `sfAmount` must be strictly positive.
*
* Everything else is deferred to `doApply`.
*
* @param ctx Preflight context providing the transaction and current rules.
* @return `tesSUCCESS`, or `temBAD_AMOUNT` if the amount is invalid.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Mutate ledger state to fund the channel and/or extend its expiration.
*
* Executes the following guard sequence:
* 1. **Channel existence** — looks up the `ltPAYCHAN` SLE by `sfChannel`;
* returns `tecNO_ENTRY` if absent.
* 2. **Expiry short-circuit** — if `parentCloseTime` has passed either
* `sfCancelAfter` or `sfExpiration`, calls `closeChannel` and returns its
* result. This ensures a stale funding attempt performs useful cleanup
* rather than silently failing.
* 3. **Ownership enforcement** — only the account that originally opened the
* channel may fund it; any other submitter receives `tecNO_PERMISSION`.
* 4. **Expiration extension** — if `sfExpiration` is present in the
* transaction, the new value must be at least `parentCloseTime +
* sfSettleDelay` (floored by the existing expiration if that is earlier),
* ensuring the destination retains time to claim; violates return
* `temBAD_EXPIRATION`.
* 5. **Reserve check** — owner balance must cover `accountReserve(ownerCount)`;
* falls short returns `tecINSUFFICIENT_RESERVE`.
* 6. **Balance check** — owner balance must also cover the deposit on top of
* the reserve; falls short returns `tecUNFUNDED`.
* 7. **Destination existence** — the channel's destination account must still
* exist; if deleted, returns `tecNO_DST` to prevent locking XRP into an
* unclaimable channel.
*
* On success, `sfAmount` on the channel SLE is incremented and the owner's
* `sfBalance` is decremented by the same amount.
*
* @return `tesSUCCESS` on success, or a `tec*`/`tem*`/`tef*` error code.
*/
TER
doApply() override;
/** Per-entry invariant visitor (no-op; reserved for future invariant checks).
*
* @param isDelete True if the entry is being deleted.
* @param before SLE state before the transaction, or null if new.
* @param after SLE state after the transaction, or null if deleted.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-apply invariant finalizer (no-op; reserved for future invariant checks).
*
* @param tx The applied transaction.
* @param result The TER returned by `doApply`.
* @param fee The fee charged for this transaction.
* @param view Read-only view of the ledger after application.
* @param j Journal for diagnostic logging.
* @return Always `true`; no transaction-specific invariants are checked yet.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,31 +4,122 @@
namespace xrpl {
/**
* Transactor that removes a permissioned domain from the ledger.
*
* A permissioned domain constrains access to certain ledger operations (AMM,
* DEX) to accounts that satisfy a configured set of credentials. This
* transactor is the controlled teardown path: the domain owner deletes the
* domain SLE, removes it from the owner directory, and recovers the base
* reserve that was locked at creation time.
*
* Unlike `PermissionedDomainSet`, this class intentionally omits a
* `checkExtraFeatures` override. The base-class implementation returns `true`
* unconditionally, so deletion is always permitted once the domain object
* exists — objects must never be stranded by an amendment state change.
*
* Pipeline: `preflight` (structural check) → `preclaim` (existence + ownership)
* → `doApply` (directory removal, owner-count decrement, SLE erasure).
*/
class PermissionedDomainDelete : public Transactor
{
public:
/**
* Consequences factory type for this transactor.
*
* `Normal` indicates that the worst-case impact on the submitting account's
* spendable balance is the transaction fee alone. Deleting a domain returns
* a reserve increment but does not transfer XRP between parties in an
* unbounded way, so no special queuing treatment is required.
*/
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
explicit PermissionedDomainDelete(ApplyContext& ctx) : Transactor(ctx)
{
}
/**
* Stateless preflight validation — no ledger access.
*
* Confirms that `sfDomainID` is non-zero. A zero value is structurally
* malformed; all ledger-state checks (existence, ownership) are deferred
* to `preclaim` so they can influence fee-claiming behaviour.
*
* @param ctx The preflight context containing the transaction.
* @return `temMALFORMED` if `sfDomainID` is zero; `tesSUCCESS` otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/**
* Read-only ledger validation — existence and ownership checks.
*
* Resolves `sfDomainID` to a `PermissionedDomain` SLE via
* `keylet::permissionedDomain` and verifies two conditions:
* - The domain exists in the current ledger view.
* - The submitting account (`sfAccount`) is the domain's `sfOwner`.
*
* Placing these checks here rather than in `doApply` ensures that an
* unauthorized or nonexistent-domain delete still results in a fee claim.
*
* @param ctx The preclaim context with read-only ledger view.
* @return `tecNO_ENTRY` if the domain does not exist;
* `tecNO_PERMISSION` if the submitter is not the domain owner;
* `tesSUCCESS` otherwise.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Attempt to delete the Permissioned Domain. */
/**
* Apply the domain deletion to the mutable ledger view.
*
* Performs three mutations in order:
* 1. `view().dirRemove()` — removes the SLE from the owner's directory,
* which is required for correct reserve accounting. Failure here
* indicates ledger corruption and returns `tefBAD_LEDGER`.
* 2. `adjustOwnerCount(view(), ownerSle, -1, j)` — decrements the owner's
* `sfOwnerCount`, releasing the base reserve locked at creation.
* 3. `view().erase(slePd)` — removes the `PermissionedDomain` SLE.
*
* @return `tefBAD_LEDGER` if the owner-directory entry cannot be removed
* (ledger corruption; marked `LCOV_EXCL` as unreachable in
* production); `tesSUCCESS` otherwise.
*/
TER
doApply() override;
/**
* Per-entry invariant visitor — currently a no-op placeholder.
*
* Called once for each SLE modified by this transaction. No
* domain-deletion-specific invariants are enforced at the entry level yet.
*
* @param isDelete `true` if the entry is being deleted.
* @param before SLE state before the transaction (may be null for new
* entries).
* @param after SLE state after the transaction (may be null for
* deletions).
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/**
* Post-transaction invariant check — currently a no-op placeholder.
*
* Called once after all `visitInvariantEntry` calls for this transaction.
* No domain-deletion-specific invariants are enforced yet; always returns
* `true`.
*
* @param tx The transaction being applied.
* @param result The TER result from `doApply`.
* @param fee The fee charged for this transaction.
* @param view Read-only view of the post-transaction ledger state.
* @param j Journal for diagnostic logging.
* @return `true` unconditionally (no invariant violations possible yet).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,6 +4,18 @@
namespace xrpl {
/** Transactor for the `ttPERMISSIONED_DOMAIN_SET` transaction.
*
* Handles both creation of a new `PermissionedDomain` ledger object and
* in-place modification of an existing one. The presence of `sfDomainID` in
* the transaction selects the update path; its absence triggers creation.
*
* Permissioned Domains define a named set of accepted credential types
* (issuer + credential-type pairs). Other protocol features can scope access
* to holders of those credentials without enumerating individual accounts.
*
* Gated on the `featureCredentials` amendment via `checkExtraFeatures`.
*/
class PermissionedDomainSet : public Transactor
{
public:
@@ -13,25 +25,103 @@ public:
{
}
/** Gate this transaction type on the `featureCredentials` amendment.
*
* Called by `invokePreflight` before `preflight`. Returns `false` (and
* causes `temDISABLED`) when the amendment is not yet active, preventing
* domain creation or modification on pre-amendment ledgers.
*
* @param ctx The preflight context carrying the current rule set.
* @return `true` if `featureCredentials` is enabled; `false` otherwise.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Stateless validation of transaction fields.
*
* Delegates to `credentials::checkArray` to enforce that
* `sfAcceptedCredentials` is non-empty, within
* `kMAX_PERMISSIONED_DOMAIN_CREDENTIALS_ARRAY_SIZE` (10), and
* structurally well-formed. Also rejects a zero-valued `sfDomainID` —
* a zero hash is not a valid domain identifier.
*
* No ledger state is accessed here; all checks are purely against the
* transaction's own fields.
*
* @param ctx The preflight context.
* @return `tesSUCCESS` on success; `temMALFORMED` for a zero
* `sfDomainID`; a `tem` code from `checkArray` for invalid
* credential structure.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Read-only ledger state checks.
*
* Verifies:
* - The submitting account exists on ledger (guard against internal
* inconsistency; returns `tefINTERNAL` if missing).
* - Every `sfIssuer` in `sfAcceptedCredentials` has a live `AccountRoot`
* on ledger; a domain referencing a non-existent issuer would be
* permanently unresolvable (`tecNO_ISSUER`).
* - For update operations (`sfDomainID` present): the domain SLE exists
* (`tecNO_ENTRY`) and is owned by the submitting account
* (`tecNO_PERMISSION`).
*
* @param ctx The preclaim context carrying the read-only ledger view.
* @return `tesSUCCESS`, `tefINTERNAL`, `tecNO_ISSUER`, `tecNO_ENTRY`,
* or `tecNO_PERMISSION`.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Attempt to create the Permissioned Domain. */
/** Apply the transaction, creating or updating a PermissionedDomain SLE.
*
* Canonicalises `sfAcceptedCredentials` via `credentials::makeSorted`
* before writing, ensuring deterministic storage order regardless of
* submission order.
*
* **Update path** (`sfDomainID` present): replaces the existing domain
* SLE's credential array in-place; no reserve change occurs.
*
* **Create path** (`sfDomainID` absent): checks that the account's XRP
* balance covers `accountReserve(ownerCount + 1)`, allocates a new SLE
* keyed by `keylet::permissionedDomain(account_, sfSequence)`, inserts it
* into the owner directory, and increments the owner count by one.
*
* The transaction sequence number is used as the domain's key
* differentiator, making each domain globally unique without a separate
* ID-generation step.
*
* @return `tesSUCCESS`; `tecINSUFFICIENT_RESERVE` if the account cannot
* cover the new reserve; `tecDIR_FULL` if the owner directory has no
* space; `tefINTERNAL` (unreachable in normal operation) if the owner
* or domain SLE is unexpectedly absent.
*/
TER
doApply() override;
/** Per-entry invariant visitor — currently a no-op (future work).
*
* @param isDelete True when the entry is being deleted.
* @param before The SLE state before the transaction.
* @param after The SLE state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-transaction invariant finalization — currently a no-op (future work).
*
* @param tx The applied transaction.
* @param result The TER code returned by `doApply`.
* @param fee The fee charged.
* @param view The post-transaction read view.
* @param j Journal for diagnostic logging.
* @return Always `true`; no transaction-specific invariants are checked yet.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -6,39 +6,192 @@
namespace xrpl {
/** Transactor for the `ttBATCH` transaction type.
*
* Bundles two to eight inner XRPL transactions into a single outer
* transaction with one of four configurable execution policies
* (`tfAllOrNothing`, `tfOnlyOne`, `tfUntilFailure`, `tfIndependent`).
* This lets operations that belong together — such as a DEX offer paired
* with a trust-line establishment — be submitted composably without the
* race conditions and ordering problems of independent submissions.
*
* The outer transaction handles fee deduction and sequence/ticket
* consumption via the base-class `apply()` machinery. Inner transaction
* execution is delegated to `applyBatchTransactions()` in `apply.cpp`,
* which is called by `applyTransaction()` only after the outer apply
* succeeds. Each inner transaction runs in its own `perTxBatchView`
* sandbox layered over a shared `wholeBatchView`; `wholeBatchView` is
* merged into the authoritative ledger view only if at least one inner
* transaction was applied.
*
* @note Vault (`ttVAULT_*`), Loan Broker (`ttLOAN_BROKER_*`), and Loan
* (`ttLOAN_*`) transaction types are forbidden as inner transactions
* and are enumerated in `kDISABLED_TX_TYPES`.
*/
class Batch : public Transactor
{
public:
/** Uses standard consequence analysis; does not unconditionally block
* the transaction queue the way a `Blocker`-factory transactor does.
*/
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
explicit Batch(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Compute the total base fee for a Batch transaction.
*
* The fee is additive:
* - One base fee for the outer Batch transaction itself.
* - One base fee per inner transaction (preventing Batch from being a
* cost-avoidance mechanism relative to submitting transactions
* individually).
* - One base fee per entry in `sfBatchSigners` (mirrors the multi-sign
* fee structure used elsewhere in the protocol).
*
* Overflow is checked at every accumulation step. If any addition would
* overflow, `INITIAL_XRP` is returned as a sentinel — consistent with
* the defensive pattern used across the XRPL fee pipeline.
*
* @param view The current ledger view, used to obtain the base fee.
* @param tx The Batch transaction being evaluated.
* @return The total base fee in drops, or `INITIAL_XRP` on overflow.
*/
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
/** Return the set of flags valid on the outer Batch transaction.
*
* Returns `tfBatchMask`, which admits exactly the four mutually
* exclusive execution-policy flags and rejects `tfInnerBatchTxn` on
* the outer transaction (only inner transactions carry that flag).
*
* @param ctx Preflight context (unused beyond satisfying the static
* interface; the mask is unconditional).
* @return The bitmask of valid outer-Batch flags.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Validate the structural integrity of a Batch transaction.
*
* Runs before signature verification and has no ledger access.
* Checks (in order):
* 1. Exactly one execution-policy flag is set (enforced via
* `std::popcount`).
* 2. `sfRawTransactions` contains at least 2 and at most
* `maxBatchTxCount` (8) entries.
* 3. Each inner transaction: is not a `ttBATCH` (no nesting), is not
* a disabled type (`kDISABLED_TX_TYPES`), carries `tfInnerBatchTxn`,
* has an empty `sfSigningPubKey` and no `sfTxnSignature`/`sfSigners`,
* has a zero XRP fee, and passes its own `xrpl::preflight()` call
* with the `tapBATCH` flag and the outer batch's ID as
* `parentBatchId`.
* 4. Each inner transaction has either a non-zero `sfSequence` or an
* `sfTicketSequence`, but not both.
* 5. For `tfAllOrNothing` and `tfUntilFailure` modes, duplicate
* sequence or ticket values across inner transactions from the same
* account are rejected (these modes commit or abort as a unit, so
* consuming the same account slot twice would be incoherent).
*
* @param ctx The preflight context for the outer Batch transaction.
* @return `tesSUCCESS` if the batch is structurally valid; a `tem*`
* code otherwise (e.g., `temINVALID_INNER_BATCH` for disabled
* types, `temINVALID_FLAG` for multiple policy flags).
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validate batch-signer authorization after the outer signature is
* verified.
*
* Called by the framework only after the outer transaction's own
* cryptographic signature has been accepted, granting confidence that
* the submitter is who they claim to be. This hook:
* 1. Builds `requiredSigners` — every inner-transaction account that
* differs from the outer account, plus any `sfCounterparty` fields
* that differ from the outer account.
* 2. Validates `sfBatchSigners` against that set with a bidirectional
* pass: extraneous signers (not in `requiredSigners`) and missing
* signers (in `requiredSigners` but absent from `sfBatchSigners`)
* both fail, as do duplicates. The outer account may not appear in
* `sfBatchSigners`.
* 3. Calls `ctx.tx.checkBatchSign()` to verify the cryptographic
* signatures of all batch signers.
*
* @param ctx The preflight context, guaranteed to be post-outer-sig.
* @return `tesSUCCESS` if all batch signers are valid and sufficient;
* a `tem*` code otherwise.
*/
static NotTEC
preflightSigValidated(PreflightContext const& ctx);
/** Verify signatures at the preclaim stage, where ledger state is
* available.
*
* Runs two checks in sequence:
* 1. `Transactor::checkSign` — validates the outer account's
* signature (regular key, master key, or signer list).
* 2. `Transactor::checkBatchSign` — re-validates batch-signer
* credentials against on-ledger `RegularKey`/`SignerList` objects,
* which are only accessible in preclaim.
*
* The split from `preflightSigValidated` is intentional: preflight
* can only check cryptographic correctness; preclaim can additionally
* confirm on-ledger authorization state.
*
* @param ctx The preclaim context, providing a read-only ledger view.
* @return `tesSUCCESS` if both checks pass; a `tef*` code otherwise.
*/
static NotTEC
checkSign(PreclaimContext const& ctx);
/** Apply the outer Batch transaction.
*
* Returns `tesSUCCESS` immediately. The outer Batch itself writes
* nothing to the ledger beyond the fee deduction and sequence/ticket
* consumption handled by the base-class `apply()` method. Inner
* transaction execution is performed by `applyBatchTransactions()` in
* `apply.cpp`, invoked by `applyTransaction()` after this method
* returns.
*
* @return Always `tesSUCCESS`.
*/
TER
doApply() override;
/** Invariant visitor for Batch — currently a no-op.
*
* Inner-transaction invariant checking is handled by the individual
* inner transactors in their own apply contexts. The outer Batch
* transaction does not directly mutate ledger objects beyond the
* base-class fee/sequence handling.
*
* @param isDelete `true` if the entry is being deleted.
* @param before The SLE state before the transaction; may be null
* for newly created entries.
* @param after The SLE state after the transaction; may be null for
* deleted entries.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Finalize invariant checks for Batch — currently a no-op.
*
* Returns `true` unconditionally. Inner-transaction invariants are
* finalized within each inner transactor's own apply context.
*
* @param tx The Batch transaction.
* @param result The TER code produced by `doApply`.
* @param fee The fee deducted from the submitting account.
* @param view A read-only view of the ledger after application.
* @param j Journal for diagnostic logging.
* @return Always `true` (no outer-Batch invariant violations possible).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -47,6 +200,15 @@ public:
ReadView const& view,
beast::Journal const& j) override;
/** Transaction types that may not appear as inner transactions in a
* Batch.
*
* Vault (`ttVAULT_*`), Loan Broker (`ttLOAN_BROKER_*`), and Loan
* (`ttLOAN_*`) families are blocked because their multi-ledger-object
* state machines are difficult to reason about under batch atomicity
* semantics. `preflight` rejects any inner transaction whose type
* appears here with `temINVALID_INNER_BATCH`.
*/
static constexpr auto kDISABLED_TX_TYPES = std::to_array<TxType>({
ttVAULT_CREATE,
ttVAULT_SET,

View File

@@ -4,6 +4,23 @@
namespace xrpl {
/** Applies consensus-driven, system-level protocol mutations to a closed ledger.
*
* `Change` handles three distinct pseudo-transaction types — `ttAMENDMENT`,
* `ttFEE`, and `ttUNL_MODIFY` — under one class, dispatching in `doApply()`
* to `applyAmendment()`, `applyFee()`, or `applyUNLModify()` based on the
* transaction type.
*
* These transactions are synthesized internally during ledger close and are
* never submitted by real accounts. Accordingly, `Transactor::invokePreflight<Change>`
* is fully specialized (in `Change.cpp`) to bypass the normal amendment-gating,
* flag-mask, and signature-validation sequence, performing only `preflight0`
* plus zero-account/zero-fee/zero-sequence checks.
*
* @note `calculateBaseFee()` unconditionally returns zero — pseudo-transactions
* are not subject to fee markets or reserve checks.
* @see EnableAmendment, SetFee, UNLModify
*/
class Change : public Transactor
{
public:
@@ -13,17 +30,33 @@ public:
{
}
/** Dispatch to `applyAmendment()`, `applyFee()`, or `applyUNLModify()`.
*
* The `default` branch is `UNREACHABLE`; `preclaim()` rejects unknown
* transaction types before `doApply()` is ever called.
*
* @return `tesSUCCESS` on success, a `tef*` code on protocol-level
* constraint violations, or `temINVALID_FLAG` for conflicting amendment
* flags.
*/
TER
doApply() override;
/** Assert that the synthesized account field is the zero account. */
void
preCompute() override;
/** No-op; `Change` has no transaction-specific invariant entries yet. */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** No-op; `Change` has no transaction-specific invariant finalization yet.
*
* @return Always `true`.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -32,28 +65,90 @@ public:
ReadView const& view,
beast::Journal const& j) override;
/** Return zero; pseudo-transactions carry no fee.
*
* @return `XRPAmount{0}` unconditionally.
*/
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx)
{
return XRPAmount{0};
}
/** Validate that this transaction is being applied to a closed ledger and
* enforce format compatibility for `ttFEE` transactions.
*
* Returns `temINVALID` when the view is open, because the open/closed
* distinction is not available during preflight. For `ttFEE`, the set of
* required and forbidden fields differs depending on whether `featureXRPFees`
* is active: the legacy format uses fee-unit integers (`sfBaseFee`,
* `sfReferenceFeeUnits`, `sfReserveBase`, `sfReserveIncrement`), while the
* new format uses drop amounts (`sfBaseFeeDrops`, `sfReserveBaseDrops`,
* `sfReserveIncrementDrops`). Mixing field sets returns `temMALFORMED` or
* `temDISABLED`. `ttAMENDMENT` and `ttUNL_MODIFY` require no extra checks
* here; unknown types return `temUNKNOWN`.
*
* @param ctx Read-only context including the ledger view and transaction.
* @return `tesSUCCESS`, `temINVALID`, `temMALFORMED`, `temDISABLED`, or
* `temUNKNOWN`.
*/
static TER
preclaim(PreclaimContext const& ctx);
private:
/** Record a `tfGotMajority` / `tfLostMajority` event, or activate an
* amendment on the ledger.
*
* Writes to the `keylet::amendments()` SLE. On activation, calls
* `AmendmentTable::enable()` and, if the amendment is unsupported by this
* node, invokes `NetworkOPs::setAmendmentBlocked()` to halt consensus
* participation.
*
* @return `tesSUCCESS`, `tefALREADY` (duplicate majority record or removal
* of a non-existent majority), or `temINVALID_FLAG` (both majority flags
* set simultaneously).
*/
TER
applyAmendment();
/** Write the new fee schedule into the `keylet::fees()` SLE.
*
* Under `featureXRPFees`, sets the drop-denominated fields and explicitly
* removes the legacy fee-unit fields via `makeFieldAbsent` to prevent stale
* data from persisting. Without the feature, the legacy fields are written
* and the drop fields remain absent.
*
* @return `tesSUCCESS` always.
*/
TER
applyFee();
/** Nominate a validator for disabling or re-enabling in the Negative UNL.
*
* Only valid on flag ledgers (`isFlagLedger(view().seq())`); returns
* `tefFAILURE` on any other ledger sequence. Enforces these consistency
* invariants on `keylet::negativeUNL()`:
* - At most one pending `sfValidatorToDisable` and one `sfValidatorToReEnable`.
* - A validator cannot be nominated for disabling if it is already in the
* Negative UNL.
* - A validator cannot be nominated for re-enabling if it is not in the
* Negative UNL.
* - The same validator cannot simultaneously occupy both pending slots.
*
* @return `tesSUCCESS` on success, `tefFAILURE` for any format error or
* invariant violation.
*/
TER
applyUNLModify();
};
/** Semantic alias for `Change` when dispatching amendment activation pseudo-transactions. */
using EnableAmendment = Change;
/** Semantic alias for `Change` when dispatching fee-schedule update pseudo-transactions. */
using SetFee = Change;
/** Semantic alias for `Change` when dispatching Negative UNL modification pseudo-transactions. */
using UNLModify = Change;
} // namespace xrpl

View File

@@ -1,40 +1,141 @@
/** @file
* Declares the LedgerStateFix transactor for the `ttLEDGER_STATE_FIX`
* transaction type, a protocol-level maintenance mechanism that corrects
* corrupted or inconsistent ledger state.
*
* Gated on the `fixNFTokenPageLinks` amendment. New repair operations may
* be introduced in future amendments by extending the `FixType` enum.
*/
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
/** Repairs corrupted ledger state via a protocol-level maintenance transaction.
*
* `LedgerStateFix` implements `ttLEDGER_STATE_FIX`, which allows network
* participants to correct inconsistent ledger entries. The specific repair
* operation is selected by the `sfLedgerFixType` field; currently the only
* supported type is `NfTokenPageLink`, which heals broken forward/backward
* links in an account's NFToken page chain.
*
* The fee is one owner reserve (same as `AccountDelete`) to deter frivolous
* submissions. The fee is non-refundable even when no repair is needed.
*
* @note Amendment gating is handled by the framework before `preflight` is
* called; no manual amendment check is required inside the lifecycle
* methods.
*/
class LedgerStateFix : public Transactor
{
public:
/** Identifies which ledger-repair operation the transaction performs.
*
* The `sfLedgerFixType` field in the transaction carries one of these
* values as a `uint16_t`. All three lifecycle methods dispatch on this
* enum; unknown values are rejected at `preflight` with
* `tefINVALID_LEDGER_FIX_TYPE`.
*/
enum class FixType : std::uint16_t {
/** Repair broken forward/backward links between NFToken page SLEs
* in an account's NFToken directory.
*
* Requires `sfOwner` to be present in the transaction.
*/
NfTokenPageLink = 1,
};
/** Not a queue blocker; later transactions from the same account may
* proceed without waiting for this one to be applied.
*/
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
explicit LedgerStateFix(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Validate the transaction's static fields.
*
* Checks that `sfLedgerFixType` maps to a known `FixType`. For
* `NfTokenPageLink`, also verifies that `sfOwner` is present.
*
* @param ctx The preflight context; no ledger access is available here.
* @return `tesSUCCESS` on success; `tefINVALID_LEDGER_FIX_TYPE` if the
* fix type is unrecognised; `temINVALID` if a required field is
* absent for the selected fix type.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Compute the fee for this transaction.
*
* Returns one owner reserve, matching the pricing strategy used by
* `AccountDelete`. The elevated fee serves as an economic deterrent
* against frivolous repair submissions.
*
* @param view The current read-only ledger view.
* @param tx The transaction being evaluated.
* @return The fee in drops (one owner reserve).
*/
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
/** Verify ledger preconditions before applying the repair.
*
* For `NfTokenPageLink`, confirms that the account identified by
* `sfOwner` exists in the ledger. This check is deferred to preclaim
* (rather than preflight) because ledger state may change between
* submission and application.
*
* @param ctx The preclaim context, providing read-only ledger access.
* @return `tesSUCCESS` if preconditions are satisfied;
* `tecOBJECT_NOT_FOUND` if the owner account does not exist;
* `tecINTERNAL` (unreachable in practice) if an unknown fix type
* reaches this stage despite passing preflight.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Execute the ledger repair operation.
*
* For `NfTokenPageLink`, calls `nft::repairNFTokenDirectoryLinks` to
* walk the owner's NFToken page chain and correct any broken links.
*
* @return `tesSUCCESS` if the repair completed; `tecFAILED_PROCESSING`
* if the repair helper could not make progress; `tecINTERNAL`
* (unreachable in practice) if an unknown fix type reaches this
* stage despite passing preflight.
*/
TER
doApply() override;
/** No-op: no transaction-specific invariants are currently defined.
*
* Reserved for future use. Accumulates nothing.
*
* @param isDelete True if the entry was erased.
* @param before The SLE state before the transaction.
* @param after The SLE state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** No-op: no transaction-specific invariants are currently defined.
*
* Reserved for future use. Always returns `true`.
*
* @param tx The transaction that was applied.
* @param result The tentative TER from `doApply`.
* @param fee The fee consumed.
* @param view The post-apply ledger view.
* @param j Journal for diagnostics.
* @return Always `true` (no invariants to check yet).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,68 +4,153 @@
namespace xrpl {
/** Transactor for the `TicketCreate` transaction type.
*
* Mints one or more `Ticket` ledger objects in a single transaction.
* Tickets reserve a contiguous block of future account sequence numbers,
* allowing the owner to submit transactions out-of-order or in parallel
* without violating the normal strict sequence increment requirement.
*
* Uses `ConsequencesFactoryType::Custom` so that `makeTxConsequences` can
* report the exact number of sequence numbers consumed (equal to
* `sfTicketCount`), enabling the transaction queue to correctly order
* transactions that reference the newly minted ticket sequences.
*
* @note `TicketCreate` is the only transactor that advances an account's
* `sfSequence` by more than one in a single application.
*/
class TicketCreate : public Transactor
{
public:
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Custom;
/** Minimum number of tickets that may be created in one transaction.
*
* A count of zero is nonsensical and is rejected by `preflight` with
* `temINVALID_COUNT`.
*/
constexpr static std::uint32_t kMIN_VALID_COUNT = 1;
// A note on how the maxValidCount was determined. The goal is for
// a single TicketCreate transaction to not use more compute power than
// a single compute-intensive Payment.
//
// Timing was performed using a MacBook Pro laptop and a release build
// with asserts off. 20 measurements were taken of each of the Payment
// and TicketCreate transactions and averaged to get timings.
//
// For the example compute-intensive Payment a Discrepancy unit test
// unit test Payment with 3 paths was chosen. With all the latest
// amendments enabled, that Payment::doApply() operation took, on
// average, 1.25 ms.
//
// Using that same test set up creating 250 Tickets in a single
// TicketCreate::doApply() in a unit test took, on average, 1.21 ms.
//
// So, for the moment, a single transaction creating 250 Tickets takes
// about the same compute time as a single compute-intensive payment.
//
// October 2018.
/** Maximum number of tickets that may be created in one transaction.
*
* Calibrated so that a single `TicketCreate` consuming this many tickets
* takes roughly the same validator CPU as a compute-intensive three-path
* `Payment`. Benchmarked on a MacBook Pro release build (asserts off):
* 250 tickets averaged 1.21 ms vs. 1.25 ms for the reference Payment.
* Exceeding this value is rejected by `preflight` with `temINVALID_COUNT`.
*
* @note Benchmark performed October 2018.
*/
constexpr static std::uint32_t kMAX_VALID_COUNT = 250;
// The maximum number of Tickets an account may hold. If a
// TicketCreate would cause an account to own more than this many
// tickets, then the TicketCreate will fail.
//
// The number was chosen arbitrarily and is an effort toward avoiding
// ledger-stuffing with Tickets.
/** Maximum number of tickets an account may hold at one time.
*
* If a `TicketCreate` would push the account's net ticket inventory
* (existing + added consumed) above this threshold, `preclaim` rejects
* the transaction with `tecDIR_FULL`. The limit is an anti-ledger-stuffing
* measure and was chosen to match `kMAX_VALID_COUNT`.
*/
constexpr static std::uint32_t kMAX_TICKET_THRESHOLD = 250;
explicit TicketCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Build the `TxConsequences` for a `TicketCreate` transaction.
*
* Reports `sfTicketCount` as the number of sequence numbers consumed so
* the transaction queue can correctly evaluate ordering constraints for
* any later transaction that references one of the newly reserved ticket
* sequences.
*
* @param ctx Preflight context providing access to the transaction fields.
* @return A `TxConsequences` object encoding the multi-sequence claim.
*/
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
/** Enforce constraints beyond those of the Transactor base class. */
/** Stateless validation of `sfTicketCount`.
*
* Reads `sfTicketCount` from the transaction and rejects values outside
* `[kMIN_VALID_COUNT, kMAX_VALID_COUNT]`. No ledger state is accessed.
*
* @param ctx Preflight context providing access to the transaction fields.
* @return `tesSUCCESS` if the count is in range; `temINVALID_COUNT`
* otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Enforce constraints beyond those of the Transactor base class. */
/** State-aware validation of the account's ticket inventory.
*
* Computes the net ticket delta after applying this transaction:
* `curTicketCount + addedTickets consumedTickets`. The `consumedTickets`
* term is 1 when the transaction itself was submitted using a ticket
* (i.e. `getSeqProxy().isTicket()` is true), otherwise 0.
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` if the net delta stays within
* `kMAX_TICKET_THRESHOLD`; `tecDIR_FULL` if the threshold would be
* exceeded; `terNO_ACCOUNT` if the submitting account does not exist.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Precondition: fee collection is likely. Attempt to create ticket(s). */
/** Create the requested tickets and update account state.
*
* Execution steps:
* 1. Checks the account's pre-fee balance against the reserve required
* for `ticketCount` additional owned objects, using `preFeeBalance_`
* so the account may dip into its reserve to pay the fee.
* 2. Reads `sfSequence` from the account root (already incremented by the
* transaction machinery) as the first ticket sequence number, then
* sanity-checks that `txSeq == firstTicketSeq 1` (or `txSeq == 0`
* for ticket-submitted transactions).
* 3. Creates one `Ticket` SLE per requested ticket, inserts each into the
* ledger, and links it into the account's owner directory, storing the
* directory page in `sfOwnerNode`.
* 4. Advances `sfSequence` by `ticketCount` — the only transactor in XRPL
* that increments the account sequence by more than one.
* 5. Increments `sfTicketCount` on the account root and calls
* `adjustOwnerCount` with `+ticketCount` to update the reserve
* obligation.
*
* @return `tesSUCCESS` on success; `tecINSUFFICIENT_RESERVE` if the
* account cannot cover the reserve for the new tickets; `tecDIR_FULL`
* if the owner directory is full; `tefINTERNAL` for ledger
* corruption (should not occur in practice).
*/
TER
doApply() override;
/** Per-entry invariant visitor (reserved for future use).
*
* No transaction-specific invariants are currently enforced. The override
* exists as a placeholder for future checks on `Ticket` SLE mutations.
*
* @param isDelete True if the entry is being deleted.
* @param before SLE state before the transaction, or nullptr if new.
* @param after SLE state after the transaction, or nullptr if deleted.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-transaction invariant check (reserved for future use).
*
* No transaction-specific invariants are currently enforced. Always
* returns `true`.
*
* @param tx The applied transaction.
* @param result The TER produced by `doApply`.
* @param fee The fee charged.
* @param view Read-only view of the resulting ledger state.
* @param j Journal for diagnostic logging.
* @return `true` unconditionally; future implementations may return
* `false` to signal invariant violations.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,6 +4,21 @@
namespace xrpl {
/** Executes a ttCLAWBACK transaction on the XRP Ledger.
*
* Allows an issuer to reclaim tokens from a holder's account, supporting
* both IOU trust-line tokens and Multi-Purpose Tokens (MPT). The amount
* recovered is capped at the holder's spendable balance; any freeze state
* the issuer may have applied to the account is bypassed for this purpose.
*
* Clawback is rejected against AMM liquidity-pool accounts (use
* `AMMClawback` instead) and, when `featureSingleAssetVault` is active,
* against pseudo-accounts.
*
* @note For IOU tokens the holder identity is encoded inside `sfAmount`'s
* issuer field; `sfHolder` must be absent. For MPT tokens `sfHolder`
* carries the holder account and must be present.
*/
class Clawback : public Transactor
{
public:
@@ -13,21 +28,84 @@ public:
{
}
/** Validate the structural correctness of the clawback transaction fields.
*
* Dispatches to a template specialisation based on the asset type carried
* in `sfAmount` (IOU `Issue` or `MPTIssue`). For IOUs, rejects if
* `sfHolder` is present, if the amount is XRP or non-positive, or if the
* issuer and holder are the same account. For MPTs, additionally gates on
* the `featureMPTokensV1` amendment and requires `sfHolder` to be present
* and the amount to be within `maxMPTokenAmount`.
*
* @param ctx Stateless preflight context carrying the transaction and rules.
* @return `tesSUCCESS` if the transaction is structurally valid; a `tem*`
* error code otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Verify ledger preconditions required for clawback to succeed.
*
* Confirms that both the issuer and holder accounts exist, that the holder
* is neither an AMM account nor (when `featureSingleAssetVault` is active)
* a pseudo-account, and then dispatches to the asset-type-specific helper.
*
* For IOU tokens the helper checks that `lsfAllowTrustLineClawback` is set
* on the issuer and that `lsfNoFreeze` is not set, that the trust line
* exists with the correct balance orientation, and that the holder's
* spendable balance is non-zero. For MPT tokens it checks `lsfMPTCanClawback`
* on the issuance object, that the caller is the issuance's issuer, that the
* holder's `MPToken` object exists, and that the spendable balance is
* non-zero.
*
* @param ctx Read-only preclaim context providing the current ledger view.
* @return `tesSUCCESS` on success; `tecNO_PERMISSION`, `tecNO_LINE`,
* `tecAMM_ACCOUNT`, `tecPSEUDO_ACCOUNT`, `tecOBJECT_NOT_FOUND`,
* `tecINSUFFICIENT_FUNDS`, or `terNO_ACCOUNT` on failure.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the clawback, moving tokens from the holder back to the issuer.
*
* Calls `directSendNoFee` to transfer `min(spendableBalance, sfAmount)`
* tokens without applying any transfer fee. The spendable balance is
* computed via `accountHolds` with `fhIGNORE_FREEZE`, so clawback
* succeeds up to the available balance even when the account is frozen.
*
* @return `tesSUCCESS` on success; `tecINTERNAL` if an internal
* consistency check fails (should not occur in practice).
*/
TER
doApply() override;
/** Per-entry invariant visitor hook for the clawback transactor.
*
* Currently a no-op; reserved for future transaction-specific invariant
* checks.
*
* @param isDelete True if the entry is being deleted.
* @param before The SLE state before the transaction.
* @param after The SLE state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Invariant finalisation hook for the clawback transactor.
*
* Currently a no-op that always returns `true`; reserved for future
* transaction-specific invariant checks.
*
* @param tx The transaction being applied.
* @param result The TER result code from `doApply`.
* @param fee The fee charged for the transaction.
* @param view The ledger view after the transaction.
* @param j Journal for diagnostic logging.
* @return Always `true`; no violations are currently checked.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,15 +4,57 @@
namespace xrpl {
/** Arguments for the core MPToken authorization helper.
*
* Bundles the inputs required by `authorizeMPToken()` (declared in
* `MPTokenHelpers.h`) so that the helper can be called from contexts outside
* the `MPTokenAuthorize` transactor — for example, `VaultCreate` and
* `VaultDeposit` reuse it to implicitly authorize vault pseudo-accounts
* without executing a full transaction.
*
* @see authorizeMPToken
*/
struct MPTAuthorizeArgs
{
/** Submitting account's XRP balance before fee deduction; used for the
* reserve check when creating a new `MPToken` object. */
XRPAmount const& priorBalance;
/** 192-bit identifier of the target MPT issuance. */
MPTID const& mptIssuanceID;
/** Account that submitted the transaction (holder when `holderID` is
* absent; issuer when `holderID` is present). */
AccountID const& account;
/** Transaction flags; `tfMPTUnauthorize` selects the
* delete/deauthorize path. */
std::uint32_t flags{};
/** When set, `account` is the issuer acting on behalf of this holder;
* when absent, `account` itself is the holder. */
std::optional<AccountID> holderID;
};
/** Transactor for the `MPTokenAuthorize` transaction (ttMPTOKEN_AUTHORIZE, opcode 57).
*
* Manages the bidirectional authorization handshake that governs who may hold a
* given Multi-Purpose Token (MPT) issuance. A single transaction type, differentiated
* by the presence of the optional `sfHolder` field, handles two distinct roles:
*
* - **Holder path** (`sfHolder` absent): the submitting account opts in to hold the
* issuance (creates an `MPToken` SLE), or opts out (deletes it via `tfMPTUnauthorize`).
* - **Issuer path** (`sfHolder` present): the issuer grants or revokes the
* `lsfMPTAuthorized` flag on the named holder's existing `MPToken` SLE. Only
* meaningful when the issuance carries `lsfMPTRequireAuth`.
*
* The protocol enforces a holder-first, issuer-second handshake: a holder must opt in
* before the issuer can authorize them. Gated behind the `featureMPTokensV1` amendment.
* Delegable to a credentialed delegate via the `mustAuthorizeMPT` privilege.
*
* @note `ConsequencesFactory` is `Normal` — a well-formed transaction that fails
* in preclaim still consumes its sequence number and fee.
*/
class MPTokenAuthorize : public Transactor
{
public:
@@ -22,24 +64,102 @@ public:
{
}
/** Return the bitmask of valid transaction flags for this type.
*
* Reports `tfMPTokenAuthorizeMask` to the `preflight1()` framework so that
* unexpected flag bits are rejected before field validation begins.
*
* @param ctx Preflight context (unused; present for interface uniformity).
* @return The set of flag bits valid for `MPTokenAuthorize` transactions.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Stateless field validation — rejects degenerate self-authorization.
*
* The only check performed here is that `sfAccount != sfHolder`; an account
* authorizing itself is a client bug with no valid interpretation. All
* amendment gating is handled upstream by `invokePreflight<T>()` via the
* `Permission` registry, so this method does not touch ledger or amendment state.
*
* @param ctx Preflight context providing the transaction and rules.
* @return `tesSUCCESS` or `temMALFORMED` if `sfAccount == sfHolder`.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Ledger-state validation against a read-only view.
*
* Branches on the presence of `sfHolder`:
*
* **Holder path** (`sfHolder` absent — submitter is the holder):
* - With `tfMPTUnauthorize`: confirms the `MPToken` SLE exists, that both
* `sfMPTAmount` and `sfLockedAmount` are zero (`tecHAS_OBLIGATIONS` if not),
* and (when `featureSingleAssetVault` is active) that `lsfMPTLocked` is not
* set on the token (`tecNO_PERMISSION`).
* - Without `tfMPTUnauthorize`: confirms the issuance exists, that the submitter
* is not the issuer (`tecNO_PERMISSION`), and that no `MPToken` SLE already
* exists (`tecDUPLICATE`).
*
* **Issuer path** (`sfHolder` present — submitter is the issuer):
* - Confirms the holder account exists (`tecNO_DST`).
* - Confirms the issuance exists (`tecOBJECT_NOT_FOUND`).
* - Confirms the submitter is the issuance's issuer (`tecNO_PERMISSION`).
* - Confirms `lsfMPTRequireAuth` is set on the issuance (`tecNO_AUTH`);
* granting auth on a non-auth issuance is meaningless.
* - Confirms the holder has already created their `MPToken` entry (`tecOBJECT_NOT_FOUND`),
* enforcing the holder-first handshake.
* - Rejects pseudo-accounts (vault and loan broker) as holders (`tecNO_PERMISSION`),
* because pseudo-accounts are implicitly always authorized by the protocol.
*
* @param ctx Preclaim context providing the read-only ledger view and transaction.
* @return `tesSUCCESS`, or one of the `tec`/`tef` codes described above.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the authorization change to the ledger.
*
* Delegates entirely to `authorizeMPToken()` from `MPTokenHelpers.h`, passing
* `preFeeBalance_` (the XRP balance snapshot taken by the base class before fee
* deduction) along with the transaction fields. All ledger mutations — creating
* or deleting the `MPToken` SLE, adjusting owner directory entries, and toggling
* `lsfMPTAuthorized` — are performed inside that helper.
*
* @return `tesSUCCESS`, `tecINSUFFICIENT_RESERVE`, or a `tef` code on
* invariant violations inside `authorizeMPToken`.
*/
TER
doApply() override;
/** Accumulate per-entry data for transaction-specific invariant checks.
*
* Currently a no-op stub; no transaction-specific invariants are defined yet.
* The method exists to satisfy the pure-virtual interface on `Transactor` and
* serves as the extension point for future post-apply checks.
*
* @param isDelete True if the entry was erased.
* @param before Entry state before the transaction (nullptr for new entries).
* @param after Entry state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Finalize transaction-specific invariant checks.
*
* Currently a no-op stub that always returns `true`; no transaction-specific
* invariants are defined yet. Satisfies the pure-virtual interface on `Transactor`.
*
* @param tx The transaction being applied.
* @param result The tentative TER result.
* @param fee The fee consumed.
* @param view Read-only ledger view after the transaction.
* @param j Logging sink.
* @return Always `true` (no invariants to fail).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -6,24 +6,84 @@
namespace xrpl {
/** Input parameters for creating a new MPTokenIssuance ledger object.
*
* This aggregate decouples the creation logic from the transaction context so
* that `MPTokenIssuanceCreate::create()` can be called as a side effect by
* other transactors — most notably `VaultCreate`, which mints a share-token
* issuance when creating a single-asset vault — without constructing a full
* `ApplyContext`.
*
* @note When `priorBalance` is present, `create()` enforces the owner-reserve
* requirement. Callers that manage the reserve externally (e.g. vault
* pseudo-account creation) should pass `std::nullopt` to skip the check.
*/
struct MPTCreateArgs
{
/** Account balance before fee deduction. When set, `create()` returns
* `tecINSUFFICIENT_RESERVE` if the balance cannot cover the new reserve
* increment. Pass `std::nullopt` to skip the reserve check. */
std::optional<XRPAmount> priorBalance;
/** Issuing account; combined with `sequence` to derive the deterministic
* `MPTID` via `makeMptID()`. */
AccountID const& account;
/** Transaction sequence value used with `account` to compute the `MPTID`.
* Stored in the issuance SLE as `sfSequence`. */
std::uint32_t sequence = 0;
/** Transaction flags, stripped of universal bits (`~tfUniversal`) before
* being written to `sfFlags` on the issuance SLE. */
std::uint32_t flags = 0;
/** Optional supply cap for the issuance. Must be positive and at most
* `maxMPTokenAmount` (0x7FFF_FFFF_FFFF_FFFF). Absent means no cap. */
std::optional<std::uint64_t> maxAmount =
std::nullopt; // NOLINT(readability-redundant-member-init)
/** Optional decimal-scale exponent stored in `sfAssetScale` on the SLE. */
std::optional<std::uint8_t> assetScale =
std::nullopt; // NOLINT(readability-redundant-member-init)
/** Per-transfer fee in basis points. Must be ≤ `maxTransferFee`
* (50,000). A non-zero value requires `tfMPTCanTransfer` to be set in
* `flags`. */
std::optional<std::uint16_t> transferFee =
std::nullopt; // NOLINT(readability-redundant-member-init)
/** Arbitrary binary metadata stored in `sfMPTokenMetadata`. When
* present must be non-empty and at most `maxMPTokenMetadataLength`
* (1,024) bytes. */
std::optional<Slice> const& metadata{};
/** Optional permissioned-domain ID. When set the issuance is private
* and `tfMPTRequireAuth` must be set in `flags`. Requires both
* `featurePermissionedDomains` and `featureSingleAssetVault`. */
std::optional<uint256> domainId = std::nullopt; // NOLINT(readability-redundant-member-init)
/** Optional bitmask of flags that may be changed after issuance. When
* present must be non-zero and contain only bits within
* `tmfMPTokenIssuanceCreateMutableMask`. Requires `featureDynamicMPT`. */
std::optional<std::uint32_t> mutableFlags =
std::nullopt; // NOLINT(readability-redundant-member-init)
};
/** Transactor for the `ttMPTOKEN_ISSUANCE_CREATE` transaction type.
*
* Creates a new `MPTokenIssuance` ledger object that anchors all subsequent
* holder balances for a Multi-Party Token (MPT). MPTs carry richer metadata
* than classic trust-line IOUs: optional supply caps, decimal scaling,
* configurable transfer fees, arbitrary binary metadata, and
* permissioned-domain constraints.
*
* The core creation logic lives in the static `create()` method rather than
* directly in `doApply()`, allowing `VaultCreate` (and future transactors)
* to mint an MPT issuance as a side effect by constructing `MPTCreateArgs`
* directly, without a full transaction context.
*
* @see MPTCreateArgs
*/
class MPTokenIssuanceCreate : public Transactor
{
public:
@@ -33,24 +93,93 @@ public:
{
}
/** Gate optional field categories on the amendments that enable them.
*
* Returns `false` (causing `invokePreflight` to return `temDISABLED`) if:
* - `sfDomainID` is present but neither `featurePermissionedDomains` nor
* `featureSingleAssetVault` is enabled, or
* - `sfMutableFlags` is present but `featureDynamicMPT` is not enabled.
*
* @param ctx The preflight context.
* @return `true` if all present optional fields are covered by active
* amendments; `false` otherwise.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Returns the flag mask for this transaction type.
*
* `invokePreflight` passes this mask to `preflight1`, which rejects any
* transaction whose `sfFlags` field contains bits outside the mask.
*
* @param ctx The preflight context (unused; present for interface
* uniformity).
* @return `tfMPTokenIssuanceCreateMask`.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Validate semantic constraints on the transaction fields.
*
* Enforces four rules:
* 1. If `sfMutableFlags` is present it must be non-zero and must not
* contain bits outside `tmfMPTokenIssuanceCreateMutableMask`
* (returns `temINVALID_FLAG`).
* 2. A non-zero `sfTransferFee` (max 50,000 basis points) requires
* `tfMPTCanTransfer` to also be set (returns `temMALFORMED` or
* `temBAD_TRANSFER_FEE`).
* 3. A zero `sfDomainID` is malformed; a non-zero domain ID requires
* `tfMPTRequireAuth` (returns `temMALFORMED`).
* 4. `sfMPTokenMetadata` must be non-empty and ≤ 1,024 bytes;
* `sfMaximumAmount`, if present, must be positive and ≤
* `maxMPTokenAmount` (returns `temMALFORMED`).
*
* @param ctx The preflight context.
* @return `tesSUCCESS` if all checks pass; a `tem*` error code otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Apply the transaction: package fields into `MPTCreateArgs` and delegate
* to `create()`.
*
* Passes `preFeeBalance_` as `priorBalance` so that `create()` enforces
* the reserve requirement against the pre-fee snapshot.
*
* @return `tesSUCCESS` on success, or the `TER` embedded in the
* `Unexpected` result from `create()`.
*/
TER
doApply() override;
/** Accumulate per-entry data for transaction-specific invariant checks.
*
* Currently a no-op stub; no transaction-specific invariants are defined
* yet. The method satisfies the `Transactor` interface and serves as the
* extension point for future checks.
*
* @param isDelete True if the entry was erased.
* @param before Entry state before the transaction (nullptr for new entries).
* @param after Entry state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Finalize transaction-specific invariant checks.
*
* Currently a no-op stub that always returns `true`. Satisfies the
* `Transactor` interface; reserved for future per-transaction invariants.
*
* @param tx The transaction being applied.
* @param result The tentative TER result.
* @param fee The fee consumed.
* @param view Read-only ledger view after the transaction.
* @param j Logging sink.
* @return Always `true` (no invariants to fail).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -59,6 +188,34 @@ public:
ReadView const& view,
beast::Journal const& j) override;
/** Create an `MPTokenIssuance` SLE from the supplied parameters.
*
* This is the single authoritative path for inserting a new MPT issuance
* into the ledger. It is called directly by `doApply()` and may also be
* called by other transactors (e.g. `VaultCreate`) that need to mint a
* share-token issuance as a side effect.
*
* Sequence of operations:
* 1. If `args.priorBalance` is set, verifies the account can cover the
* new owner-reserve increment.
* 2. Inserts an entry in the issuer's owner directory via
* `view.dirInsert()`.
* 3. Constructs the `MPTokenIssuance` SLE with mandatory fields
* (`sfFlags`, `sfIssuer`, `sfOutstandingAmount = 0`, `sfOwnerNode`,
* `sfSequence`) and populates all optional fields present in `args`.
* 4. Calls `adjustOwnerCount()` to increment the account's reserve
* obligation.
*
* @param view Mutable ledger view to write the new SLE into.
* @param journal Logging sink.
* @param args Creation parameters; see `MPTCreateArgs`.
* @return On success, the new `MPTID` (deterministically derived from
* `args.sequence` and `args.account` via `makeMptID()`). On failure,
* an `Unexpected` wrapping one of:
* - `tecINTERNAL` — issuer account SLE not found (ledger corruption).
* - `tecINSUFFICIENT_RESERVE` — `priorBalance` too low.
* - `tecDIR_FULL` — owner directory page exhausted.
*/
static Expected<MPTID, TER>
create(ApplyView& view, beast::Journal journal, MPTCreateArgs const& args);
};

View File

@@ -1,9 +1,30 @@
/** @file
* Declares the `MPTokenIssuanceDestroy` transactor (`ttMPTOKEN_ISSUANCE_DESTROY`).
*
* Handles permanent deletion of an MPToken issuance object from the ledger,
* reclaiming the issuer's owner-count slot and removing the entry from the
* owner directory. Gated behind `featureMPTokensV1`; delegable via the
* `destroyMPTIssuance` privilege.
*/
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
/** Transactor for the `ttMPTOKEN_ISSUANCE_DESTROY` transaction type (opcode 55).
*
* Permanently removes an `MPTokenIssuance` ledger object when no tokens remain
* in circulation (`sfOutstandingAmount == 0`) and none are locked
* (`sfLockedAmount == 0`), protecting any holders from having live balances
* orphaned. On success the issuer's owner count is decremented by one.
*
* All substantive validation lives in `preclaim`; `preflight` is intentionally
* trivial because every guard requires reading current ledger state.
*
* @see MPTokenIssuanceCreate
*/
class MPTokenIssuanceDestroy : public Transactor
{
public:
@@ -13,21 +34,81 @@ public:
{
}
/** Static validation phase — unconditionally returns `tesSUCCESS`.
*
* Every meaningful precondition for this transaction requires ledger
* state that is unavailable at preflight time. Field-level checks
* (valid fee, sequence, well-formed signature) are handled by the
* framework's `preflight0`/`preflight1` layers before this method runs.
*
* @param ctx The preflight context (unused).
* @return `tesSUCCESS` always.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Read-only ledger validation — enforces all destruction preconditions.
*
* Reads the `MPTokenIssuance` SLE identified by `sfMPTokenIssuanceID` and
* enforces three invariants in order:
* 1. The issuance object must exist (`tecOBJECT_NOT_FOUND` if absent).
* 2. `sfIssuer` must match the transaction submitter (`tecNO_PERMISSION`
* if it does not) — only the issuer may destroy their own issuance.
* 3. `sfOutstandingAmount` must be zero, and `sfLockedAmount` (if present)
* must also be zero (`tecHAS_OBLIGATIONS` if either is non-zero).
*
* @param ctx The preclaim context providing a read-only ledger view.
* @return `tesSUCCESS` if all guards pass; `tecOBJECT_NOT_FOUND`,
* `tecNO_PERMISSION`, or `tecHAS_OBLIGATIONS` otherwise.
* @note A non-zero `sfLockedAmount` when `sfOutstandingAmount` is already
* zero would indicate corrupted ledger state unreachable through normal
* transaction flow; the check is present as a defensive backstop.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Execute the ledger mutation: erase the issuance SLE and update accounting.
*
* Sequence of operations:
* 1. Peeks the `MPTokenIssuance` SLE via `view().peek(keylet::mptIssuance(...))`.
* 2. Removes the entry from the issuer's owner directory via `view().dirRemove()`.
* 3. Erases the SLE with `view().erase()`.
* 4. Decrements the issuer's owner count by one via `adjustOwnerCount()`.
*
* @return `tesSUCCESS` on success; `tefBAD_LEDGER` if the owner-directory
* removal fails (indicates ledger corruption, not user error).
*/
TER
doApply() override;
/** Accumulate per-entry data for transaction-specific invariant checks.
*
* Currently a no-op stub; no transaction-specific invariants are defined
* yet. Satisfies the `Transactor` interface and serves as the extension
* point for future checks.
*
* @param isDelete True if the entry was erased.
* @param before Entry state before the transaction (nullptr for new entries).
* @param after Entry state after the transaction (nullptr for deleted entries).
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Finalize transaction-specific invariant checks.
*
* Currently a no-op stub that always returns `true`. Satisfies the
* `Transactor` interface; reserved for future per-transaction invariants.
*
* @param tx The transaction being applied.
* @param result The tentative TER result.
* @param fee The fee consumed.
* @param view Read-only ledger view after the transaction.
* @param j Logging sink.
* @return Always `true` (no invariants to fail).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,6 +4,22 @@
namespace xrpl {
/** Transactor for `ttMPTOKEN_ISSUANCE_SET` transactions.
*
* Handles two distinct operations on MPTokenIssuance ledger objects:
* 1. Toggling the locked state of the whole issuance or an individual
* holder's `MPToken` slot (via `tfMPTLock` / `tfMPTUnlock`).
* 2. Mutating post-creation properties of the issuance — `sfMutableFlags`,
* `sfTransferFee`, `sfMPTokenMetadata`, and `sfDomainID` — when the
* `featureDynamicMPT` amendment is active.
*
* Uses compile-time (static) polymorphism: the three pipeline entry points
* (`checkExtraFeatures`, `preflight`, `preclaim`) are static methods driven
* by `Transactor::invokePreflight<T>` rather than virtual dispatch.
* `ConsequencesFactory{Normal}` applies standard fee consequences; this
* transaction does not unconditionally block later transactions from the
* same account.
*/
class MPTokenIssuanceSet : public Transactor
{
public:
@@ -13,30 +29,163 @@ public:
{
}
/** Gate on amendment requirements not captured by the global tx-to-feature
* map.
*
* Returns `false` (causing `invokePreflight` to return `temDISABLED`)
* if `sfDomainID` is present in the transaction but either
* `featurePermissionedDomains` or `featureSingleAssetVault` is not yet
* enabled. All other transactions pass unconditionally.
*
* @param ctx Preflight context providing access to transaction fields
* and active amendment rules.
* @return `true` if the transaction may proceed, `false` to abort with
* `temDISABLED`.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Return the valid flag-bit mask for this transaction type.
*
* Delegates to `tfMPTokenIssuanceSetMask`, which covers `tfMPTLock` and
* `tfMPTUnlock`. `preflight1` uses this mask to reject transactions that
* set any undefined flag bits.
*
* @param ctx Preflight context (unused; present for framework uniformity).
* @return Bitmask of all flag bits this transaction legitimately owns.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Stateless semantic validation (no ledger access).
*
* Enforces the following invariants:
* - `sfDomainID` and `sfHolder` are mutually exclusive.
* - `tfMPTLock` and `tfMPTUnlock` cannot both be set.
* - The submitting account cannot equal `sfHolder`.
*
* When `featureDynamicMPT` is active, additionally:
* - Mutation fields (`sfMutableFlags`, `sfMPTokenMetadata`,
* `sfTransferFee`) cannot coexist with `sfHolder` or non-universal
* tx flags.
* - Setting a non-zero `sfTransferFee` while clearing `tmfMPTClearCanTransfer`
* is rejected.
* - `sfMutableFlags` must be non-zero and must not include unknown bits
* (validated against `tmfMPTokenIssuanceSetMutableMask`).
*
* When either `featureSingleAssetVault` or `featureDynamicMPT` is
* enabled, a transaction that changes nothing (zero flags, no domain,
* no mutation fields) is rejected as `temMALFORMED`.
*
* @param ctx Preflight context with the transaction and active rules.
* @return `tesSUCCESS` on valid input; a `tem*` code on any violation.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validate delegate authorization for lock/unlock operations.
*
* If `sfDelegate` is absent the call is a no-op and returns
* `tesSUCCESS`. Otherwise the delegate SLE is loaded and permissions
* are checked in two tiers:
* 1. Broad transaction-level permission via `checkTxPermission()`.
* 2. Granular fall-back: `MPTokenIssuanceLock` required for `tfMPTLock`,
* `MPTokenIssuanceUnlock` required for `tfMPTUnlock`.
*
* @param view Read-only ledger view used to load the delegate SLE.
* @param tx The transaction being validated.
* @return `tesSUCCESS` if authorized; `terNO_DELEGATE_PERMISSION` if the
* delegate lacks the required permission.
*/
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
/** Read-only ledger-state validation (runs after signature verification).
*
* Checks that:
* - The target `MPTokenIssuance` object exists and the submitter is its
* issuer.
* - If `sfHolder` is present: the holder account exists and their
* `MPToken` slot for this issuance exists.
* - If `sfDomainID` is present: the issuance has `lsfMPTRequireAuth` set,
* and the referenced `PermissionedDomain` object exists (unless the
* zero sentinel is supplied to clear the domain link).
* - Lock/unlock operations respect the `lsfMPTCanLock` flag: under older
* rules the flag must be present; under `featureSingleAssetVault` or
* `featureDynamicMPT` the flag is only required when the tx actually
* requests a lock or unlock.
* - For `featureDynamicMPT` mutations: each flag the transaction tries to
* set or clear must be listed in `sfMutableFlags` on the ledger object
* (table-driven via `kMPT_MUTABILITY_FLAGS`); similarly,
* `sfMPTokenMetadata` and `sfTransferFee` mutations require
* `lsmfMPTCanMutateMetadata` and `lsmfMPTCanMutateTransferFee`
* respectively. Setting a non-zero `sfTransferFee` additionally
* requires `lsfMPTCanTransfer` to already be set on the object.
*
* @param ctx Preclaim context providing read-only access to the ledger.
* @return `tesSUCCESS` on success; `tecOBJECT_NOT_FOUND`,
* `tecNO_PERMISSION`, `tecNO_AUTH`, `tecNO_ENTRY`, or another
* `tec*` / `ter*` code on failure.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the transaction's mutations to the ledger.
*
* Peeks at the `MPTokenIssuance` SLE (or the holder's `MPToken` SLE
* when `sfHolder` is present) and applies the following changes as
* directed by the transaction flags and fields:
* - `tfMPTLock` / `tfMPTUnlock`: sets or clears `lsfMPTLocked`.
* - `sfMutableFlags`: iterates `kMPT_MUTABILITY_FLAGS` and sets/clears
* each `canMutateFlag` on the issuance as directed.
* - When `tmfMPTClearCanTransfer` is applied, `sfTransferFee` is
* explicitly removed to keep the ledger internally consistent.
* - `sfTransferFee`: stored absent when zero (field uses `soeDEFAULT`
* semantics), present otherwise.
* - `sfMPTokenMetadata`: an empty blob removes the field entirely.
* - `sfDomainID`: zero sentinel removes the field; any other value sets
* it.
*
* All failure paths should have been caught in preflight/preclaim; if
* `doApply` reaches an unexpected state it returns `tecINTERNAL`.
*
* @return `tesSUCCESS` on success; `tecINTERNAL` on an unexpected error.
*/
TER
doApply() override;
/** Per-entry invariant hook (currently a no-op).
*
* Called by the invariant-checker framework for each SLE modified by
* this transaction. No transaction-specific invariants are registered
* yet; this override exists as a placeholder for future work.
*
* @param isDelete `true` if the entry was deleted.
* @param before SLE state before the transaction (may be null for new
* entries).
* @param after SLE state after the transaction (may be null for
* deletions).
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-apply invariant finalizer (currently a no-op).
*
* Called once after all `visitInvariantEntry` calls for this
* transaction. No transaction-specific invariants are registered yet;
* always returns `true`. This override exists as a placeholder for
* future work.
*
* @param tx The applied transaction.
* @param result The `TER` result of `doApply`.
* @param fee Fee charged for this transaction.
* @param view Read-only view of the ledger after application.
* @param j Journal for diagnostic logging.
* @return `true` (invariant trivially satisfied).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -5,6 +5,27 @@
namespace xrpl {
/** Transactor for `TrustSet` transactions.
*
* A `TrustSet` transaction creates, modifies, or implicitly deletes a
* *trust line* — the bilateral `RippleState` ledger entry that permits two
* accounts to carry a non-XRP IOU balance in a specific currency. Without a
* trust line, no IOU balance can exist between the two parties.
*
* The class participates in the standard three-phase pipeline declared by
* `Transactor`:
* - `getFlagsMask` / `preflight` — stateless validation (no ledger access).
* - `checkPermission` — delegate-authority check against a
* read-only view.
* - `preclaim` — read-only ledger checks.
* - `doApply` — mutable ledger writes.
*
* Static methods use compile-time name hiding rather than virtual dispatch;
* `Transactor::invokePreflight<TrustSet>` resolves them at compile time.
* `ConsequencesFactoryType::Normal` is used — the transaction does not block
* subsequent transactions from the same account and needs no custom
* consequence computation.
*/
class TrustSet : public Transactor
{
public:
@@ -14,27 +35,179 @@ public:
{
}
/** Return the set of flags valid for a `TrustSet` transaction.
*
* Returns `tfTrustSetMask`, which is the union of `tfSetfAuth`,
* `tfSetNoRipple`, `tfClearNoRipple`, `tfSetFreeze`, `tfClearFreeze`,
* `tfSetDeepFreeze`, and `tfClearDeepFreeze`. `preflight1` uses this mask
* to reject transactions that set any flag outside the union before any
* ledger state is touched. The deep-freeze bits are included in the mask
* unconditionally; `preflight` enforces the `featureDeepFreeze` amendment
* guard separately at runtime.
*
* @param ctx Preflight context (unused; present for interface uniformity).
* @return Bitmask of all flags permitted on a `TrustSet` transaction.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Validate a `TrustSet` transaction without accessing ledger state.
*
* Checks performed (all return `tem` codes, so no fee is charged on
* failure):
* - If `featureDeepFreeze` is not enabled, `tfSetDeepFreeze` and
* `tfClearDeepFreeze` are rejected with `temINVALID_FLAG`.
* - `sfLimitAmount` must be a legal non-native amount (`temBAD_AMOUNT`).
* - `sfLimitAmount` must not be denominated in XRP (`temBAD_LIMIT`).
* - The currency must not be the XRP pseudo-currency sentinel
* (`temBAD_CURRENCY`).
* - The limit must be non-negative (`temBAD_LIMIT`).
* - The issuer field must be a real, non-zero account ID
* (`temDST_NEEDED`).
*
* @param ctx Immutable preflight context carrying the transaction and
* active ledger rules.
* @return `tesSUCCESS` on success, or a `tem` error code.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Validate delegate authority for a `TrustSet` transaction.
*
* Called only when `sfDelegate` is present on the transaction. Locates
* the `Delegate` ledger object for `(sfAccount, sfDelegate)` and first
* checks full transaction-type permission via `checkTxPermission`. If
* that passes, the entire transaction is allowed. Otherwise, the method
* falls through to the following **granular permission** checks:
*
* - Any flag in `tfTrustSetPermissionMask` (everything except
* `tfSetfAuth`, `tfSetFreeze`, and `tfClearFreeze`) → denied.
* - `sfQualityIn` or `sfQualityOut` present → denied (delegates cannot
* adjust quality settings under granular grants).
* - Trust line does not yet exist → denied (granular grants cannot
* create new trust lines).
* - `tfSetfAuth` set without `TrustlineAuthorize` permission → denied.
* - `tfSetFreeze` set without `TrustlineFreeze` permission → denied.
* - `tfClearFreeze` set without `TrustlineUnfreeze` permission → denied.
* - `sfLimitAmount` differs from the stored limit → denied (granular
* delegates cannot modify credit limits).
*
* @param view Read-only ledger view for looking up delegate objects and
* the existing trust line.
* @param tx The transaction being evaluated.
* @return `tesSUCCESS` if the delegate is authorised, or
* `terNO_DELEGATE_PERMISSION` otherwise.
*/
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
/** Read-only ledger checks before state mutation.
*
* Checks (in order):
* 1. **Auth flag guard**: `tfSetfAuth` is only valid when the issuing
* account has `lsfRequireAuth` set (`tefNO_AUTH_REQUIRED`).
* 2. **Self-trust**: sender and destination (issuer) must differ
* (`temDST_IS_SRC`).
* 3. **Destination existence**: when AMM or `featureSingleAssetVault` is
* enabled the destination account must exist (`tecNO_DST`).
* 4. **`lsfDisallowIncomingTrustline`**: if the destination has opted out
* of incoming trust lines, the transaction is blocked (`tecNO_PERMISSION`)
* unless `fixDisallowIncomingV1` is enabled *and* a trust line between
* the parties already exists (modification of an existing relationship
* is always permitted regardless of the opt-out).
* 5. **Pseudo-account restrictions**:
* - AMM pseudo-accounts: new trust lines are only permitted when the
* currency matches the pool's LP token and the pool has non-zero
* liquidity; existing lines may always be modified.
* - Vault and loan-broker pseudo-accounts: modification of an existing
* line is permitted; creation of a new line is not.
* - Any other pseudo-account type → `tecPSEUDO_ACCOUNT`.
* 6. **Deep-freeze invariants** (when `featureDeepFreeze` is active):
* - `lsfNoFreeze` on the sender's account blocks any freeze action.
* - Setting and clearing any freeze flag in the same transaction is
* rejected.
* - A post-transaction simulation via `computeFreezeFlags` ensures
* deep-freeze cannot be set without a co-occurring normal freeze.
*
* @param ctx Read-only preclaim context with ledger view and transaction.
* @return `tesSUCCESS`, or an appropriate `tec`/`tef`/`tem` error.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the `TrustSet` transaction to the mutable ledger view.
*
* **Reserve policy**: accounts with fewer than two owned objects are
* exempt from the incremental reserve requirement when creating a new
* trust line. This allows gateways to fund new users with exactly the
* base reserve — without having to pad for the trust-line reserve — so
* the user cannot pocket the surplus.
*
* **Existing trust line** (`RippleState` SLE found):
* - Account IDs are compared numerically to assign the "low" and "high"
* side; all field accesses (`sfLowLimit`/`sfHighLimit`,
* `sfLowQualityIn`/`sfHighQualityIn`, etc.) use the appropriate side.
* - Quality values equal to `QUALITY_ONE` are stored as zero to maintain
* canonical representation.
* - The owner-reserve flags (`lsfLowReserve`/`lsfHighReserve`) are
* recomputed from scratch after every change; `adjustOwnerCount` is
* called with ±1 whenever either flag transitions.
* - If both sides of the trust line reach fully default state (zero limit,
* zero quality, no special flags, no outstanding balance), `trustDelete`
* is called to remove the SLE and decrement both owner counts (automatic
* garbage collection).
* - If a reserve increase would be required but `preFeeBalance_` falls
* short of the post-creation reserve threshold, the transaction fails
* with `tecINSUF_RESERVE_LINE`.
*
* **No existing trust line**:
* - Submitting all-default parameters on a non-existent line returns
* `tecNO_LINE_REDUNDANT` immediately.
* - If the sender's XRP balance is below the reserve needed to hold one
* more object, the transaction fails with `tecNO_LINE_INSUF_RESERVE`.
* - Otherwise, `trustCreate` initialises a new `RippleState` SLE with
* the supplied limit, quality, and flag values.
*
* @return `tesSUCCESS`, `tecINSUF_RESERVE_LINE`, `tecNO_LINE_REDUNDANT`,
* `tecNO_LINE_INSUF_RESERVE`, `tecNO_DST`, `tecNO_PERMISSION`,
* or `tefINTERNAL` on unexpected ledger corruption.
*/
TER
doApply() override;
/** Accumulate per-SLE state for transaction-specific invariant checks.
*
* Called once per modified ledger entry before `finalizeInvariants`.
* The current implementation is a no-op placeholder; `TrustSet` does
* not yet define transaction-specific post-conditions beyond those
* enforced by the protocol-level invariant checkers.
*
* @param isDelete `true` if the entry was erased.
* @param before Entry state before the transaction (`nullptr` if
* the entry was newly created).
* @param after Entry state after the transaction (not guaranteed to
* be `nullptr` on deletion; use `isDelete` instead).
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Verify transaction-specific post-conditions after all entries are visited.
*
* Called once after every modified ledger entry has been passed to
* `visitInvariantEntry`. Returns `false` to fail the transaction with
* `tecINVARIANT_FAILED`. The current implementation always returns
* `true` (placeholder for future transaction-specific invariants).
*
* @param tx The applied transaction.
* @param result Tentative `TER` result prior to invariant checking.
* @param fee Fee consumed by the transaction.
* @param view Read-only view of the final ledger state.
* @param j Journal for logging invariant failures.
* @return `true` if all transaction-specific invariants pass.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,6 +4,32 @@
namespace xrpl {
/** Implements the VaultClawback transaction for forcibly recovering assets or
* shares held inside a Vault ledger object.
*
* The transaction operates in one of two mutually exclusive modes, determined
* at runtime by comparing the submitting account against vault roles:
*
* - **Share-burn mode** (vault owner, share MPT targeted): burns all of a
* holder's vault shares when the vault holds zero assets. This is the
* recovery mechanism for stuck vaults where a lending protocol has absorbed
* all assets and suffered a total loss, leaving outstanding shares with no
* underlying value. Partial burns are not permitted.
*
* - **Asset-clawback mode** (asset issuer, vault asset targeted): recovers
* the vault's underlying IOU or MPT assets from the vault pseudo-account
* and proportionally destroys the corresponding holder shares. XRP vaults
* are excluded; IOU vaults require `lsfAllowTrustLineClawback` (and not
* `lsfNoFreeze`) on the issuer's account root; MPT vaults require
* `lsfMPTCanClawback` on the issuance.
*
* @note When the asset issuer and vault owner are the same account, the
* transaction is ambiguous and `sfAmount` must be supplied explicitly;
* omitting it returns `tecWRONG_ASSET`.
*
* @see VaultWithdraw (holder-initiated counterpart that also converts shares
* to assets, but requires no external authority)
*/
class VaultClawback : public Transactor
{
public:
@@ -13,21 +39,83 @@ public:
{
}
/** Validate transaction fields before accessing ledger state.
*
* Rejects a zero/empty `sfVaultID`. If `sfAmount` is present it must be
* non-negative and must not be XRP, since XRP clawback is categorically
* forbidden. A zero `sfAmount` is valid and means "all".
*
* @param ctx Preflight context carrying the transaction and rules.
* @return `tesSUCCESS` on success, `temMALFORMED` for an empty vault ID
* or an XRP amount, `temBAD_AMOUNT` for a negative amount.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Verify authorization and resolve the operating mode against ledger state.
*
* Loads the vault SLE and share MPT issuance, then resolves the effective
* clawback amount via the file-local `clawbackAmount()` helper (an absent
* `sfAmount` becomes "all shares" for the vault owner, or "vault asset"
* for the issuer).
*
* For **share-burn mode**: enforces that the vault has shares outstanding
* but both `sfAssetsTotal` and `sfAssetsAvailable` are zero, and that a
* non-zero explicit amount equals the holder's entire share balance.
*
* For **asset-clawback mode**: confirms the submitter is the vault's
* asset issuer (not the holder), the asset is not XRP, and the appropriate
* clawback flags are set (`lsfAllowTrustLineClawback` without `lsfNoFreeze`
* for IOU; `lsfMPTCanClawback` for MPT).
*
* @param ctx Preclaim context providing read-only ledger access.
* @return `tesSUCCESS` on success, `tecNO_ENTRY` if the vault is missing,
* `tecWRONG_ASSET` if the amount is ambiguous or targets an unrelated
* asset, `tecNO_PERMISSION` for authorization failures,
* `tecLIMIT_EXCEEDED` if the vault owner attempts a partial share burn,
* `tefINTERNAL` if the share MPT issuance is unexpectedly absent.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Execute the clawback, mutating vault and holder balances.
*
* In **share-burn mode**, sets `sharesDestroyed` to the holder's full
* share balance; no assets move. In **asset-clawback mode**, delegates
* to `assetsToClawback()` to compute the (shares, assets) pair.
*
* Mutation sequence:
* 1. Decrements vault `sfAssetsTotal` and `sfAssetsAvailable`.
* 2. Transfers `sharesDestroyed` shares from holder to vault pseudo-account
* (transfer fee waived).
* 3. Attempts to remove the holder's now-empty MPToken entry via
* `removeEmptyHolding()`; tolerates `tecHAS_OBLIGATIONS` silently.
* 4. If `assetsRecovered > 0`, transfers assets from the vault
* pseudo-account to the submitting issuer (transfer fee waived) and
* confirms the vault's asset balance has not gone negative.
* 5. Calls `associateAsset` to re-round stored numeric fields to the
* vault asset's precision.
*
* @return `tesSUCCESS` on success, `tecPRECISION_LOSS` if rounding
* produced zero shares to destroy, `tecPATH_DRY` on arithmetic
* overflow in the share/asset conversion, `tefINTERNAL` on
* unexpected ledger-corruption conditions.
*/
TER
doApply() override;
/** Per-entry invariant hook; no transaction-specific checks are registered yet. */
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Post-transaction invariant finalization; no transaction-specific checks
* are registered yet.
*
* @return Always `true`.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,
@@ -37,6 +125,31 @@ public:
beast::Journal const& j) override;
private:
/** Compute the (assetsRecovered, sharesDestroyed) pair for asset-clawback mode.
*
* When `clawbackAmount` is zero ("all"), converts the holder's entire
* share balance to assets via `sharesToAssetsWithdraw`. When non-zero,
* performs a double-pass: assets → shares (via `assetsToSharesWithdraw`),
* then shares → assets, to account correctly for integer truncation in
* the MPT share representation.
*
* If the resulting `assetsRecovered` would exceed `sfAssetsAvailable`
* (possible when assets are partially deployed to a lending protocol),
* the method clamps to `assetsAvailable` and recomputes shares with
* `TruncateShares::Yes` so the re-conversion cannot overshoot the cap.
*
* A legacy pre-`fixSecurity3_1_3` code path is preserved for ledger
* replay: zero-amount clawback on old rules skips the `assetsAvailable`
* clamp, reproducing the original (incorrect) outcome.
*
* @param vault Mutable vault SLE used for exchange-rate and cap reads.
* @param sleShareIssuance Read-only share MPT issuance SLE.
* @param holder Account whose shares are being destroyed.
* @param clawbackAmount Resolved asset amount to recover; zero means all.
* @return `(assetsRecovered, sharesDestroyed)` on success, or an
* `Unexpected` `TER`: `tecPATH_DRY` on arithmetic overflow,
* `tecINTERNAL` on asset-mismatch or conversion helper failure.
*/
Expected<std::pair<STAmount, STAmount>, TER>
assetsToClawback(
std::shared_ptr<SLE> const& vault,

View File

@@ -4,36 +4,160 @@
namespace xrpl {
/** Transactor for the `ttVAULT_CREATE` transaction type.
*
* Instantiates a new Vault on the ledger: a pooled-asset construct that holds
* a designated asset inside a synthetic pseudo-account and issues share tokens
* (via `MPTokenIssuance`) to depositors in proportion to their contribution.
*
* The three-phase pipeline is:
* - `checkExtraFeatures` / `preflight` — amendment gating and field validation
* (no ledger access)
* - `preclaim` — read-only checks on asset validity, freeze state, and domain
* - `doApply` — creates the Vault SLE, its pseudo-account, the asset holding,
* and the share `MPTokenIssuance`; increments owner count by 2
*
* @note `sfScale` is only meaningful for IOU assets; it is rejected for XRP
* and MPT assets in `preflight`.
* @note `sfDomainID` is only valid when `tfVaultPrivate` is set; it also
* requires the `PermissionedDomains` amendment.
* @see VaultDeposit, VaultWithdraw, VaultSet, VaultClawback, VaultDelete
*/
class VaultCreate : public Transactor
{
public:
/** Standard fee and sequence consequences; does not block queued transactions. */
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
explicit VaultCreate(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Gate the transaction on required amendments.
*
* Returns `false` (producing `temDISABLED`) if `featureMPTokensV1` is not
* active, or if `sfDomainID` is present and `featurePermissionedDomains`
* is not active. Called by `invokePreflight<VaultCreate>` before
* `preflight1`, so disabled transactions are rejected before any field
* parsing.
*
* @param ctx Preflight context.
* @return `true` if the required amendments are active; `false` otherwise.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Return the bitmask of valid flag bits for this transaction type.
*
* Returns `tfVaultCreateMask`, which permits `tfVaultPrivate` and
* `tfVaultShareNonTransferable`. Any flag bits outside this mask
* cause `preflight0` to return `temINVALID_FLAG`.
*
* @param ctx Preflight context (unused).
* @return Bitmask of permitted flag bits.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
/** Perform stateless, ledger-free field validation.
*
* Validates the following constraints (returning `temMALFORMED` on
* violation):
* - `sfData`, if present, must not exceed `kMAX_DATA_PAYLOAD_LENGTH`.
* - `sfWithdrawalPolicy`, if present, must equal
* `kVAULT_STRATEGY_FIRST_COME_FIRST_SERVE` (the only supported strategy).
* - `sfDomainID`, if present, must be non-zero and `tfVaultPrivate` must
* be set (domain-scoped access applies only to private vaults).
* - `sfAssetsMaximum`, if present, must not be negative.
* - `sfMPTokenMetadata`, if present, must be non-empty and within
* `kMAX_MP_TOKEN_METADATA_LENGTH`.
* - `sfScale`, if present, is only valid for IOU assets (rejected for XRP
* and MPT) and must not exceed `kVAULT_MAXIMUM_IOU_SCALE`.
*
* @param ctx Preflight context carrying the transaction and active rules.
* @return `tesSUCCESS` or `temMALFORMED`.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Perform read-only ledger checks after signature verification.
*
* Checks, in order (returning the first failing code):
* - `canAddHolding`: the asset can be held (e.g., MPT issuance exists).
* - Pseudo-account issuer rejection: if the asset's issuer is a
* pseudo-account (e.g., an AMM LP token or another vault's share MPT),
* returns `tecWRONG_ASSET`. Such assets cannot be clawed back if needed.
* - Freeze check: returns `tecFROZEN` (IOU) or `tecLOCKED` (MPT) if the
* asset is frozen for the vault owner.
* - Domain existence: if `sfDomainID` is present, the referenced
* `PermissionedDomain` object must exist; returns `tecOBJECT_NOT_FOUND`
* otherwise.
* - Address collision: derives the pseudo-account address from the vault
* keylet; returns `terADDRESS_COLLISION` if the derivation yields zero.
*
* @param ctx Preclaim context carrying a read-only ledger view.
* @return `tesSUCCESS`, `tecWRONG_ASSET`, `tecFROZEN`, `tecLOCKED`,
* `tecOBJECT_NOT_FOUND`, `terADDRESS_COLLISION`, or a code from
* `canAddHolding`.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the vault creation to the mutable ledger view.
*
* Executes the following steps atomically:
* 1. Links a new Vault SLE into the owner's directory via `dirLink`.
* 2. Increments the owner count by 2 (vault + pseudo-account), then checks
* `preFeeBalance_` against the new reserve requirement; returns
* `tecINSUFFICIENT_RESERVE` if insufficient. The increment-before-check
* order is intentional: the account must be able to afford both objects.
* 3. Creates the pseudo-account via `createPseudoAccount` keyed from the
* vault's object ID with `sfVaultID` as the discriminator field.
* 4. Adds an empty asset holding on the pseudo-account via `addEmptyHolding`
* (either an `MPToken` or a `TrustLine`/`RippleState`).
* 5. Creates the share `MPTokenIssuance` on the pseudo-account at sequence 1
* via `MPTokenIssuanceCreate::create`. The `tfVaultShareNonTransferable`
* flag suppresses `lsfMPTCanEscrow | lsfMPTCanTrade | lsfMPTCanTransfer`;
* `tfVaultPrivate` sets `lsfMPTRequireAuth`.
* 6. Populates and inserts the Vault SLE with all fields.
* 7. Explicitly authorizes the vault creator's `MPToken` via
* `authorizeMPToken`. For private vaults, also authorizes the
* pseudo-account itself (so it can hold its own share tokens internally).
* 8. Calls `associateAsset` on the vault SLE — this must be the final
* write to round stored numeric values to the asset's precision.
*
* @return `tesSUCCESS` or a `tec*` / `ter*` error.
*/
TER
doApply() override;
/** Accumulate per-entry data for transaction-specific invariant checks.
*
* Currently a no-op placeholder; no transaction-specific invariants are
* defined for `VaultCreate` yet.
*
* @param isDelete `true` if the entry was erased.
* @param before Entry state before the transaction (nullptr if new).
* @param after Entry state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Evaluate transaction-specific post-conditions after all entries are visited.
*
* Currently a no-op placeholder that always returns `true`; no
* transaction-specific invariants are defined for `VaultCreate` yet.
*
* @param tx The transaction being applied.
* @param result Tentative TER result.
* @param fee Fee consumed by the transaction.
* @param view Read-only ledger view after the transaction.
* @param j Journal for logging invariant failures.
* @return `true` (all invariants pass).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -1,9 +1,34 @@
/** @file
* Declares the `VaultDelete` transactor, which tears down a Single-Sided AMM
* vault and all of its associated ledger objects.
*/
#pragma once
#include <xrpl/tx/Transactor.h>
namespace xrpl {
/** Transactor for the `ttVAULT_DELETE` transaction type.
*
* Performs a full multi-object teardown of a vault that has been fully
* drained by its depositors. The teardown sequence is:
* 1. Remove the pseudo-account's zero-balance asset holding.
* 2. Remove the vault owner's MPToken for share issuance (if any).
* 3. Erase the share MPTokenIssuance and its directory entry.
* 4. Erase the vault pseudo-account.
* 5. Remove the vault SLE from the owner's directory and decrement the
* owner count by 2 (one for the vault object, one for the pseudo-account).
*
* Deletion is only possible once every depositor has withdrawn: `preclaim`
* enforces that `sfAssetsAvailable`, `sfAssetsTotal`, and the share
* issuance's `sfOutstandingAmount` are all zero before allowing `doApply`
* to run. This makes `VaultDelete` the terminal state of the vault lifecycle
* and the strict inverse of `VaultCreate`.
*
* @note This transactor is gated on the `featureSingleAssetVault` amendment
* and is not delegable (`Delegation::NotDelegable`).
*/
class VaultDelete : public Transactor
{
public:
@@ -13,21 +38,89 @@ public:
{
}
/** Stateless preflight validation.
*
* Confirms that `sfVaultID` is non-zero. A zero vault ID cannot
* correspond to any ledger object and is rejected immediately.
*
* @return `temMALFORMED` if `sfVaultID` is zero; `tesSUCCESS` otherwise.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Read-only ledger checks before state mutation.
*
* Enforces the three-invariant guard that makes safe deletion possible:
* - The submitting account must be the vault `sfOwner`.
* - `sfAssetsAvailable` on the vault must be zero.
* - `sfAssetsTotal` on the vault must be zero.
* - `sfOutstandingAmount` on the share MPTokenIssuance must be zero.
*
* Any outstanding assets or shares indicate depositors have not yet
* withdrawn; the vault cannot be deleted while it holds funds.
*
* @return `tecNO_ENTRY` if the vault does not exist; `tecNO_PERMISSION`
* if the submitter is not the vault owner or the share issuance owner
* is mismatched; `tecHAS_OBLIGATIONS` if any asset or share balance
* is non-zero; `tecOBJECT_NOT_FOUND` if the share issuance SLE is
* missing; `tesSUCCESS` otherwise.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Execute the vault teardown, mutating ledger state.
*
* Removes five categories of ledger objects in dependency order.
* Defensive `tecHAS_OBLIGATIONS` guards (marked `LCOV_EXCL`) verify the
* pseudo-account reaches a clean state (zero balance, zero owner count,
* no directory) before it is erased; these guards protect against bugs
* in earlier cleanup steps and are not reachable through valid
* transaction sequences. Conditions that indicate ledger corruption
* (missing pseudo-account, mismatched issuance owner) return
* `tefBAD_LEDGER` or `tefINTERNAL` rather than a fee-claiming `tec`
* code.
*
* @return `tesSUCCESS` on complete teardown; a `tef` code if ledger
* corruption is detected; a `tec` code from `removeEmptyHolding` if
* a holding cannot be removed.
*/
TER
doApply() override;
/** Per-SLE visitor called by the invariant checker framework.
*
* Accumulates per-entry accounting needed by `finalizeInvariants`.
* Called once for each SLE that was modified, inserted, or deleted
* during `doApply`.
*
* @param isDelete `true` if the SLE was erased.
* @param before SLE state before the transaction; `nullptr` for
* newly inserted entries.
* @param after SLE state after the transaction; `nullptr` for
* deleted entries.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Terminal invariant check called after all entries have been visited.
*
* Validates that the net effect of the transaction satisfies the
* `ValidVault` invariant (asset/share conservation, immutable field
* constraints). Returns `false` and logs diagnostics if any violation
* is detected, causing the framework to escalate to
* `tecINVARIANT_FAILED`.
*
* @param tx The transaction being applied.
* @param result The TER returned by `doApply`.
* @param fee The fee charged for the transaction.
* @param view Read-only view of the post-apply ledger state.
* @param j Journal for diagnostic logging.
* @return `true` if all invariants pass; `false` if any violation is
* detected.
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,30 +4,144 @@
namespace xrpl {
/** Transactor for the `ttVAULT_DEPOSIT` transaction type.
*
* Deposits assets into a Vault ledger object and mints vault-share MPTokens
* for the depositor in exchange — the primary liquidity-provision operation
* for on-ledger asset pools.
*
* The three-phase pipeline is:
* - `preflight` — rejects a zero `sfVaultID` and any non-positive `sfAmount`
* without accessing the ledger
* - `preclaim` — read-only checks: vault existence, asset type match, freeze
* state, and — for private vaults — domain-credential authorization
* - `doApply` — mints shares via a two-step exchange calculation, updates
* `sfAssetsTotal`/`sfAssetsAvailable`, enforces the vault asset cap, and
* issues two `accountSend` calls (assets in, shares out), both with
* `WaiveTransferFee::Yes` to preserve exchange-rate accuracy
*
* @note Transfer fees are intentionally waived on both legs. Allowing them
* would corrupt the `sfAssetsTotal` accounting and break the share
* exchange rate for all depositors.
* @note Private vault access is governed by `DomainID` credentials on the
* share MPT issuance, not by standard MPT issuer authorization, because
* the share issuer is a pseudo-account that cannot proactively authorize
* holders.
* @see VaultWithdraw, VaultCreate, VaultHelpers.h
*/
class VaultDeposit : public Transactor
{
public:
/** Standard fee and sequence consequences; does not block queued transactions. */
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
explicit VaultDeposit(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Perform stateless, ledger-free field validation.
*
* Checks:
* - `sfVaultID` must be non-zero; returns `temMALFORMED` otherwise.
* - `sfAmount` must be positive; returns `temBAD_AMOUNT` otherwise.
*
* @param ctx Preflight context carrying the transaction and active rules.
* @return `tesSUCCESS`, `temMALFORMED`, or `temBAD_AMOUNT`.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Perform read-only ledger checks after signature verification.
*
* Validates the following constraints in order, returning the first
* failing code:
* - Vault existence: the object identified by `sfVaultID` must exist;
* returns `tecNO_ENTRY` otherwise.
* - Asset match: `sfAmount` must carry the same asset type as the vault's
* `sfAsset` field; returns `tecWRONG_ASSET` otherwise.
* - Transferability: the vault asset must be transferable from the
* depositor to the vault pseudo-account; returns the result of
* `canTransfer` otherwise.
* - Freeze / lock: returns `tecFROZEN` (IOU) or `tecLOCKED` (MPT) if the
* deposited asset is frozen for the depositor, and `tecLOCKED` if the
* vault's share MPT is locked for the depositor.
* - Private vault authorization: if `lsfVaultPrivate` is set and the
* depositor is not the vault owner, the vault's share MPT issuance must
* carry a `sfDomainID`; `credentials::validDomain` must succeed (or
* return only `tecEXPIRED`, which is suppressed here so that `doApply`
* can delete the expired credentials as a side effect); returns
* `tecNO_AUTH` if no `sfDomainID` is present.
* - Sufficient balance: the depositor must hold at least `sfAmount` of the
* vault asset; returns `tecINSUFFICIENT_FUNDS` otherwise.
*
* @param ctx Preclaim context carrying a read-only ledger view.
* @return `tesSUCCESS`, `tecNO_ENTRY`, `tecWRONG_ASSET`, `tecFROZEN`,
* `tecLOCKED`, `tecNO_AUTH`, `tecINSUFFICIENT_FUNDS`, or a code from
* `canTransfer` / `requireAuth`.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the vault deposit to the mutable ledger view.
*
* Executes the following steps atomically:
* 1. Ensures the depositor holds (or creates) an MPToken for vault shares
* via `enforceMPTokenAuthorization` (private vault, non-owner) or
* `authorizeMPToken` (public vault or vault owner). For the vault owner
* of a private vault, also authorizes the pseudo-account itself.
* 2. Computes `sharesCreated` via `assetsToSharesDeposit()` (truncated to
* integral MPT units); returns `tecPRECISION_LOSS` if the result rounds
* to zero, preventing dust deposits.
* 3. Back-computes the exact asset cost via `sharesToAssetsDeposit()` to
* guarantee `assetsDeposited <= sfAmount`; any sub-share-unit remainder
* is effectively returned to the depositor.
* 4. Updates `sfAssetsTotal` and `sfAssetsAvailable` on the vault SLE,
* then enforces the vault's `sfAssetsMaximum` cap; returns
* `tecLIMIT_EXCEEDED` if the cap would be breached.
* 5. Transfers `assetsDeposited` from the depositor to the vault
* pseudo-account via `accountSend` with `WaiveTransferFee::Yes`.
* 6. Transfers `sharesCreated` from the vault pseudo-account to the
* depositor via `accountSend` with `WaiveTransferFee::Yes`.
* 7. Calls `associateAsset` on the vault SLE — this must be the final
* write to round stored numeric values to the asset's precision.
*
* If `assetsToSharesDeposit` or `sharesToAssetsDeposit` throws
* `std::overflow_error` (e.g., due to a large scale factor combined with
* large balances), the exception is caught and `tecPATH_DRY` is returned.
*
* @return `tesSUCCESS`, `tecPRECISION_LOSS`, `tecLIMIT_EXCEEDED`,
* `tecPATH_DRY`, or a `tec*` / `ter*` code from the underlying helpers.
*/
TER
doApply() override;
/** Accumulate per-entry data for transaction-specific invariant checks.
*
* Currently a no-op placeholder; no transaction-specific invariants are
* defined for `VaultDeposit` yet.
*
* @param isDelete `true` if the entry was erased.
* @param before Entry state before the transaction (nullptr if new).
* @param after Entry state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Evaluate transaction-specific post-conditions after all entries are visited.
*
* Currently a no-op placeholder that always returns `true`; no
* transaction-specific invariants are defined for `VaultDeposit` yet.
*
* @param tx The transaction being applied.
* @param result Tentative TER result.
* @param fee Fee consumed by the transaction.
* @param view Read-only ledger view after the transaction.
* @param j Journal for logging invariant failures.
* @return `true` (all invariants pass).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,33 +4,142 @@
namespace xrpl {
/** Transactor for the `ttVAULT_SET` transaction type.
*
* Allows the vault owner to modify the mutable properties of an existing
* vault after creation. The three writable fields are: `sfData` (arbitrary
* off-chain metadata), `sfAssetsMaximum` (deposit cap), and `sfDomainID`
* (permissioned-domain gate for private vaults). All other vault properties
* are fixed at `VaultCreate` time and cannot be changed.
*
* The three-phase pipeline is:
* - `checkExtraFeatures` / `preflight` — amendment gating and field
* validation (no ledger access)
* - `preclaim` — read-only ownership and domain checks
* - `doApply` — updates the Vault SLE and, for domain changes, the
* associated `MPTokenIssuance` SLE
*
* @note `sfDomainID` can only be set on vaults that were created with
* `lsfVaultPrivate`. A zero value clears an existing domain restriction;
* it is not possible to retroactively restrict a public vault.
* @note The vault SLE is always marked dirty via `view().update(vault)` even
* when only the issuance SLE changes — this gives the `ValidVault`
* invariant checker a signal that a `VaultSet` occurred.
* @see VaultCreate, VaultDeposit, VaultWithdraw, VaultClawback, VaultDelete
*/
class VaultSet : public Transactor
{
public:
/** Standard fee and sequence consequences; does not block queued transactions. */
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
explicit VaultSet(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Gate the transaction on required amendments.
*
* Returns `false` (producing `temDISABLED`) if `sfDomainID` is present
* in the transaction but `featurePermissionedDomains` is not yet active.
* Called by `invokePreflight<VaultSet>` before `preflight1`, keeping the
* domain-feature availability check separate from per-field branching in
* `preflight`.
*
* @param ctx Preflight context.
* @return `true` if all required amendments are active; `false` otherwise.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
/** Perform stateless, ledger-free field validation.
*
* Validates the following constraints (returning `temMALFORMED` on any
* violation):
* - `sfVaultID` must be non-zero.
* - `sfData`, if present, must be non-empty and at most
* `kMAX_DATA_PAYLOAD_LENGTH` bytes.
* - `sfAssetsMaximum`, if present, must be non-negative.
* - At least one of `sfData`, `sfAssetsMaximum`, or `sfDomainID` must be
* present; a transaction that sets none of these fields is rejected as a
* no-op to prevent fee-burning with no effect.
*
* @param ctx Preflight context carrying the transaction and active rules.
* @return `tesSUCCESS` or `temMALFORMED`.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Perform read-only ledger checks after signature verification.
*
* Checks, in order (returning the first failing code):
* - The vault ledger object for `sfVaultID` must exist; returns
* `tecNO_ENTRY` if absent.
* - The submitting account must be the vault's `sfOwner`; returns
* `tecNO_PERMISSION` otherwise.
* - The vault's `MPTokenIssuance` SLE must exist; returns `tefINTERNAL`
* if missing (a defensive guard marked `LCOV_EXCL_*` because
* `VaultCreate` and the invariant checker prevent this state).
* - If `sfDomainID` is being set, `lsfVaultPrivate` must be active on the
* vault; returns `tecNO_PERMISSION` for public vaults.
* - A non-zero `sfDomainID` must resolve to an existing
* `PermissionedDomain` object; returns `tecOBJECT_NOT_FOUND` if not.
*
* @param ctx Preclaim context carrying a read-only ledger view.
* @return `tesSUCCESS`, `tecNO_ENTRY`, `tecNO_PERMISSION`,
* `tecOBJECT_NOT_FOUND`, or `tefINTERNAL`.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply vault configuration changes to the mutable ledger view.
*
* Applies updates in order:
* - `sfData`: written directly to the Vault SLE if present.
* - `sfAssetsMaximum`: written to the Vault SLE if present; returns
* `tecLIMIT_EXCEEDED` if a non-zero cap would be lower than the
* vault's current `sfAssetsTotal`.
* - `sfDomainID`: written to or removed from the `MPTokenIssuance` SLE
* (not the Vault SLE) because domain enforcement for MPT-based vaults
* lives at the issuance level. A zero value calls `makeFieldAbsent` to
* clear the restriction.
*
* The Vault SLE is always marked dirty via `view().update(vault)` even
* when only the issuance changed, so the `ValidVault` invariant checker
* can observe the operation. `associateAsset` is called last to re-round
* stored numeric values to the asset's precision.
*
* @return `tesSUCCESS`, `tecLIMIT_EXCEEDED`, or `tefINTERNAL`.
*/
TER
doApply() override;
/** Accumulate per-entry data for transaction-specific invariant checks.
*
* Currently a no-op placeholder; no transaction-specific invariants are
* defined for `VaultSet` yet.
*
* @param isDelete `true` if the entry was erased.
* @param before Entry state before the transaction (nullptr if new).
* @param after Entry state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Evaluate transaction-specific post-conditions after all entries are visited.
*
* Currently a no-op placeholder that always returns `true`; no
* transaction-specific invariants are defined for `VaultSet` yet.
*
* @param tx The transaction being applied.
* @param result Tentative TER result.
* @param fee Fee consumed by the transaction.
* @param view Read-only ledger view after the transaction.
* @param j Journal for logging invariant failures.
* @return `true` (all invariants pass).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -4,30 +4,150 @@
namespace xrpl {
/** Transactor for the `ttVAULT_WITHDRAW` transaction type.
*
* Redeems vault-share MPTokens for the underlying pooled asset — the inverse
* of `VaultDeposit`. The submitter specifies `sfAmount` in either the vault's
* underlying asset or its share MPT; the complementary quantity is computed
* via the vault's current share-to-asset exchange rate.
*
* The three-phase pipeline is:
* - `preflight` — rejects a zero `sfVaultID`, a non-positive `sfAmount`, and
* a zero explicit `sfDestination` without accessing the ledger.
* - `preclaim` — read-only checks: vault existence, asset denomination,
* transferability, withdrawal limits (with share-to-asset conversion when
* `fixSecurity3_1_3` is active), destination authorization, and freeze state.
* - `doApply` — burns shares, decrements `sfAssetsTotal`/`sfAssetsAvailable`,
* and credits the underlying asset to the destination account.
*
* @note Possession of vault shares is treated as standing authorization to
* withdraw. The `lsfVaultPrivate` flag is intentionally not re-checked
* in `doApply`; holding a share proves prior authorized deposit.
* @note Transfer fees are waived when returning shares to the vault
* pseudo-account (`WaiveTransferFee::Yes`) because shares are internal
* bookkeeping tokens, not economic transfers.
* @see VaultDeposit, VaultCreate, VaultHelpers.h
*/
class VaultWithdraw : public Transactor
{
public:
/** Standard fee and sequence consequences; does not block queued transactions. */
static constexpr auto kCONSEQUENCES_FACTORY = ConsequencesFactoryType::Normal;
explicit VaultWithdraw(ApplyContext& ctx) : Transactor(ctx)
{
}
/** Perform stateless, ledger-free field validation.
*
* Checks:
* - `sfVaultID` must be non-zero; returns `temMALFORMED` otherwise.
* - `sfAmount` must be positive; returns `temBAD_AMOUNT` otherwise.
* - If `sfDestination` is present, it must be non-zero; returns
* `temMALFORMED` otherwise.
*
* @param ctx Preflight context carrying the transaction and active rules.
* @return `tesSUCCESS`, `temMALFORMED`, or `temBAD_AMOUNT`.
*/
static NotTEC
preflight(PreflightContext const& ctx);
/** Perform read-only ledger checks after signature verification.
*
* Validates the following constraints in order, returning the first
* failing code:
* - Vault existence: the object identified by `sfVaultID` must exist;
* returns `tecNO_ENTRY` otherwise.
* - Asset denomination: `sfAmount` must be denominated in either the
* vault's `sfAsset` or its share MPT; returns `tecWRONG_ASSET` otherwise.
* - Transferability: the vault asset must be transferable from the vault
* pseudo-account to the destination; returns the result of `canTransfer`
* otherwise.
* - Withdrawal policy: only `vaultStrategyFirstComeFirstServe` is
* supported; any other value returns `tefINTERNAL` (invariant violation).
* - Withdrawal limits: when `fixSecurity3_1_3` is active and the amount is
* share-denominated, shares are first converted to an equivalent asset
* amount before calling `canWithdraw`. Pre-amendment, share-denominated
* requests bypassed this check entirely. Overflow during conversion
* returns `tecPATH_DRY`.
* - Destination authorization: `WeakAuth` (trust line or MPToken may be
* created in `doApply`) when withdrawing to `sfAccount`; `StrongAuth`
* (trust line or MPToken must already exist) when withdrawing to a third
* party.
* - Freeze / lock: returns a freeze error if the vault asset is frozen for
* the destination, or if the vault's share MPT is frozen for the
* submitting account.
*
* @param ctx Preclaim context carrying a read-only ledger view.
* @return `tesSUCCESS`, `tecNO_ENTRY`, `tecWRONG_ASSET`, `tecPATH_DRY`,
* `tefINTERNAL`, `tecFROZEN`, `tecLOCKED`, or a code from
* `canTransfer` / `canWithdraw` / `requireAuth` / `checkFrozen`.
*/
static TER
preclaim(PreclaimContext const& ctx);
/** Apply the vault withdrawal to the mutable ledger view.
*
* Executes the following steps atomically:
* 1. Resolves the vault and share issuance SLEs with write access.
* 2. If `sfAmount` is asset-denominated: calls `assetsToSharesWithdraw` to
* find `sharesRedeemed`, returns `tecPRECISION_LOSS` if the result is
* zero (dust prevention), then back-computes `assetsWithdrawn` via
* `sharesToAssetsWithdraw`. If `sfAmount` is share-denominated: uses the
* amount directly as `sharesRedeemed` and derives `assetsWithdrawn`.
* 3. Returns `tecPATH_DRY` if conversion throws `std::overflow_error`.
* 4. Verifies the submitter holds at least `sharesRedeemed` via
* `accountHolds`; returns `tecINSUFFICIENT_FUNDS` if not.
* 5. Checks `sfAssetsAvailable` (not the raw pseudo-account balance)
* against `assetsWithdrawn`; returns `tecINSUFFICIENT_FUNDS` if
* insufficient. Using `sfAssetsAvailable` correctly excludes assets
* pledged to external lending positions.
* 6. Decrements both `sfAssetsTotal` and `sfAssetsAvailable` on the vault
* by `assetsWithdrawn`.
* 7. Transfers `sharesRedeemed` from the submitter back to the vault
* pseudo-account via `accountSend` with `WaiveTransferFee::Yes`.
* 8. Attempts to remove the submitter's now-empty share MPToken via
* `removeEmptyHolding`; `tecHAS_OBLIGATIONS` (balance non-zero) is
* silently tolerated. Skipped when the submitter is the vault owner.
* 9. Calls `doWithdraw` to credit `assetsWithdrawn` from the vault
* pseudo-account to the destination account.
* 10. Calls `associateAsset` on the vault SLE — must be the final write to
* round stored numeric values to the asset's precision.
*
* @return `tesSUCCESS`, `tecPRECISION_LOSS`, `tecPATH_DRY`,
* `tecINSUFFICIENT_FUNDS`, or a `tec*` / `ter*` code from the
* underlying helpers.
*/
TER
doApply() override;
/** Accumulate per-entry data for transaction-specific invariant checks.
*
* Currently a no-op placeholder; no transaction-specific invariants are
* defined for `VaultWithdraw` yet.
*
* @param isDelete `true` if the entry was erased.
* @param before Entry state before the transaction (nullptr if new).
* @param after Entry state after the transaction.
*/
void
visitInvariantEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after) override;
/** Evaluate transaction-specific post-conditions after all entries are visited.
*
* Currently a no-op placeholder that always returns `true`; no
* transaction-specific invariants are defined for `VaultWithdraw` yet.
*
* @param tx The transaction being applied.
* @param result Tentative TER result.
* @param fee Fee consumed by the transaction.
* @param view Read-only ledger view after the transaction.
* @param j Journal for logging invariant failures.
* @return `true` (all invariants pass).
*/
[[nodiscard]] bool
finalizeInvariants(
STTx const& tx,

View File

@@ -1,3 +1,16 @@
/** @file
* Concrete implementations of the RIPEMD-160, SHA-256, and SHA-512 hashers.
*
* Each hasher stores its OpenSSL context in an opaque `char ctx_[]` buffer
* declared in the header so that `digest.h` does not need to expose any
* OpenSSL headers to its consumers. The constructors here recover the real
* OpenSSL type via `reinterpret_cast`, which is only safe when the buffer
* size exactly matches `sizeof` the target struct. A `static_assert` in
* each constructor enforces that invariant at compile time: if an OpenSSL
* upgrade ever changes a context struct's size, the build breaks here rather
* than silently corrupting memory at runtime.
*/
#include <xrpl/protocol/digest.h>
#include <openssl/ripemd.h>
@@ -9,6 +22,10 @@ namespace xrpl {
OpensslRipemd160Hasher::OpensslRipemd160Hasher()
{
// Compile-time firewall: ctx_ must be exactly sizeof(RIPEMD160_CTX) bytes
// for the reinterpret_cast below to be safe. If an OpenSSL upgrade ever
// changes the size of RIPEMD160_CTX, this assert fires and the hardcoded
// buffer size in digest.h must be updated to match.
static_assert(sizeof(decltype(OpensslRipemd160Hasher::ctx_)) == sizeof(RIPEMD160_CTX), "");
auto const ctx = reinterpret_cast<RIPEMD160_CTX*>(ctx_);
RIPEMD160_Init(ctx);
@@ -34,6 +51,9 @@ operator result_type() noexcept
OpensslSha512Hasher::OpensslSha512Hasher()
{
// Same opaque-buffer safety check as OpensslRipemd160Hasher: ctx_ must
// match sizeof(SHA512_CTX). Update the buffer size in digest.h if this
// assert fires after an OpenSSL upgrade.
static_assert(sizeof(decltype(OpensslSha512Hasher::ctx_)) == sizeof(SHA512_CTX), "");
auto const ctx = reinterpret_cast<SHA512_CTX*>(ctx_);
SHA512_Init(ctx);
@@ -59,6 +79,9 @@ operator result_type() noexcept
OpensslSha256Hasher::OpensslSha256Hasher()
{
// Same opaque-buffer safety check as OpensslRipemd160Hasher: ctx_ must
// match sizeof(SHA256_CTX). Update the buffer size in digest.h if this
// assert fires after an OpenSSL upgrade.
static_assert(sizeof(decltype(OpensslSha256Hasher::ctx_)) == sizeof(SHA256_CTX), "");
auto const ctx = reinterpret_cast<SHA256_CTX*>(ctx_);
SHA256_Init(ctx);

View File

@@ -1,5 +1,34 @@
//
/* The base58 encoding & decoding routines in the b58_ref namespace are taken
/**
* @file tokens.cpp
* @brief Base58Check encoding and decoding for XRPL identifiers.
*
* This file is the single source of truth for XRPL's Base58Check scheme —
* the algorithm that converts raw binary account IDs, key pairs, and seeds
* into the human-readable strings users interact with every day
* (e.g., `rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh`).
*
* Every encoded XRPL identifier follows the wire layout:
* @code
* [ type byte (1) ][ raw payload (N) ][ checksum (4) ]
* @endcode
* The checksum is the first four bytes of SHA-256(SHA-256(type || payload)),
* identical to Bitcoin's checksum design.
*
* Two internal implementations are provided and selected at compile time:
* - `b58_ref` — portable O(n²) algorithm adapted from Bitcoin Core.
* - `b58_fast` — 10-15× faster path using GCC's `unsigned __int128` extension;
* unavailable on MSVC, which falls back to `b58_ref`.
*
* The fast algorithm stages the conversion as:
* @code
* base 58 → base 58^10 → base 2^64 → base 2^8
* @endcode
* `58^10 = 430804206899405824` is the largest power of 58 that fits in a
* 64-bit register, enabling the first hop to use simple 64-bit arithmetic.
* The multi-precision second hop then operates on far fewer, larger
* coefficients, dramatically reducing total work.
*
* The base58 encoding & decoding routines in the b58_ref namespace are taken
* from Bitcoin but have been modified from the original.
*
* Copyright (c) 2014 The Bitcoin Core developers
@@ -122,9 +151,22 @@ coefficients sizes greatly speeds up the multi-precision computations.
namespace xrpl {
/** The 58-character XRPL Base58 alphabet.
*
* Deliberately chosen so that an `AccountID` of all-zero bytes encodes to a
* string beginning with `'r'`, letting users and validators visually
* recognize a classic XRPL account address without decoding.
*/
static constexpr char const* kALPHABET_FORWARD =
"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz";
/** Compile-time reverse lookup table for the XRPL Base58 alphabet.
*
* Maps each ASCII byte value to its 0-based index in `kALPHABET_FORWARD`,
* or `-1` if the character is not a valid Base58 digit. Used by the
* decoder to convert encoded characters back to numeric coefficients
* without a linear search.
*/
static constexpr std::array<int, 256> const kALPHABET_REVERSE = []() {
std::array<int, 256> map{};
for (auto& m : map)
@@ -134,6 +176,14 @@ static constexpr std::array<int, 256> const kALPHABET_REVERSE = []() {
return map;
}();
/** Hash a raw buffer with the given Hasher.
*
* @tparam Hasher A type satisfying the XRP hasher concept — default-constructible,
* callable with `(void const*, size_t)`, and convertible to its `result_type`.
* @param data Pointer to the bytes to hash.
* @param size Number of bytes to hash.
* @return The hash digest.
*/
template <class Hasher>
static typename Hasher::result_type
digest(void const* data, std::size_t size) noexcept
@@ -143,6 +193,16 @@ digest(void const* data, std::size_t size) noexcept
return static_cast<typename Hasher::result_type>(h);
}
/** Hash a fixed-size byte array with the given Hasher.
*
* Convenience overload for `std::array<T, N>` where `sizeof(T) == 1`.
*
* @tparam Hasher A type satisfying the XRP hasher concept.
* @tparam T Byte-sized element type.
* @tparam N Array length.
* @param v The byte array to hash.
* @return The hash digest.
*/
template <class Hasher, class T, std::size_t N, class = std::enable_if_t<sizeof(T) == 1>>
static typename Hasher::result_type
digest(std::array<T, N> const& v)
@@ -150,7 +210,17 @@ digest(std::array<T, N> const& v)
return digest<Hasher>(v.data(), v.size());
}
// Computes a double digest (e.g. digest of the digest)
/** Compute the hash of a hash (double-digest).
*
* Applies the Hasher twice: first to produce an intermediate digest from
* `args`, then hashes that digest again. The result is the same as
* `digest<Hasher>(digest<Hasher>(args...))`.
*
* @tparam Hasher A type satisfying the XRP hasher concept.
* @tparam Args Arguments forwarded to the inner `digest` overload.
* @param args Data arguments passed to the inner digest computation.
* @return The double-digest result.
*/
template <class Hasher, class... Args>
static typename Hasher::result_type
digest2(Args const&... args)
@@ -174,6 +244,19 @@ checksum(void* out, void const* message, std::size_t size)
std::memcpy(out, h.data(), 4);
}
/** Encode a raw payload as a Base58Check XRPL token string.
*
* Prepends the `type` byte, appends a 4-byte double-SHA256 checksum, then
* Base58-encodes the result using the XRPL alphabet. Dispatches to the
* fast (`b58_fast`) implementation on non-MSVC compilers, and to the
* portable reference implementation (`b58_ref`) on MSVC.
*
* @param type The `TokenType` version byte that prefixes the encoded form
* (e.g., `TokenType::AccountID = 0` → addresses start with `'r'`).
* @param token Pointer to the raw payload bytes to encode.
* @param size Number of payload bytes.
* @return The Base58Check-encoded string, or an empty string on error.
*/
[[nodiscard]] std::string
encodeBase58Token(TokenType type, void const* token, std::size_t size)
{
@@ -184,6 +267,22 @@ encodeBase58Token(TokenType type, void const* token, std::size_t size)
#endif
}
/** Decode a Base58Check XRPL token string and return the raw payload.
*
* Decodes the Base58 string, then validates:
* 1. The decoded length is at least 6 bytes (1 type + 1 payload min + 4 checksum).
* 2. The leading type byte matches `type`.
* 3. The trailing 4-byte checksum is correct.
*
* Dispatches to the fast (`b58_fast`) implementation on non-MSVC compilers,
* and to the portable reference implementation (`b58_ref`) on MSVC.
*
* @param s The Base58Check-encoded string to decode.
* @param type The expected `TokenType` version byte.
* @return The raw payload (type byte and checksum stripped), or an empty
* string if the input is invalid, the type does not match, or the
* checksum fails.
*/
[[nodiscard]] std::string
decodeBase58Token(std::string const& s, TokenType type)
{
@@ -198,6 +297,21 @@ namespace b58_ref {
namespace detail {
/** Encode a raw byte buffer as a Base58 string (reference O(n²) algorithm).
*
* Uses the Bitcoin-derived algorithm: for each input byte the working
* base-58 buffer is multiplied by 256 and the byte added as carry.
* Leading zero bytes map to the first alphabet character (`'r'`) to
* preserve bijectivity.
*
* @param message Pointer to the data to encode.
* @param size Number of bytes in `message`.
* @param temp Caller-supplied scratch buffer; must be at least
* `size * 138 / 100 + 1` bytes (derived from log(256)/log(58) ≈ 1.38).
* @param tempSize Size of `temp` in bytes.
* @return The Base58-encoded string.
* @note Exposed in the header for unit-testing comparison against the fast path.
*/
std::string
encodeBase58(void const* message, std::size_t size, void* temp, std::size_t tempSize)
{
@@ -245,6 +359,18 @@ encodeBase58(void const* message, std::size_t size, void* temp, std::size_t temp
return str;
}
/** Decode a Base58 string to raw bytes (reference O(n²) algorithm).
*
* Reverses `encodeBase58`: for each input character the working base-256
* buffer is multiplied by 58 and the digit value added as carry.
* Leading `'r'` characters (index 0 in the alphabet) map back to leading
* zero bytes.
*
* @param s The Base58-encoded string to decode.
* @return The decoded byte string, or an empty string if any character is
* not in the XRPL alphabet or if the input exceeds 64 characters.
* @note Exposed in the header for unit-testing comparison against the fast path.
*/
std::string
decodeBase58(std::string const& s)
{
@@ -293,6 +419,18 @@ decodeBase58(std::string const& s)
} // namespace detail
/** Encode a raw payload as a Base58Check XRPL token string (reference implementation).
*
* Constructs the wire layout `[type][payload][checksum]` in a stack-allocated
* buffer, then delegates to `detail::encodeBase58`. The scratch buffer is
* sized at `expanded * 3` bytes, which is a safe upper bound derived from
* `log(256) / log(58) ≈ 1.38` plus one character of slack.
*
* @param type The `TokenType` version byte to prepend.
* @param token Pointer to the payload bytes.
* @param size Number of payload bytes.
* @return The Base58Check-encoded string.
*/
std::string
encodeBase58Token(TokenType type, void const* token, std::size_t size)
{
@@ -306,8 +444,6 @@ encodeBase58Token(TokenType type, void const* token, std::size_t size)
boost::container::small_vector<std::uint8_t, 1024> buf(bufsize);
// Lay the data out as
// <type><token><checksum>
buf[0] = safeCast<std::underlying_type_t<TokenType>>(type);
if (size != 0u)
std::memcpy(buf.data() + 1, token, size);
@@ -316,6 +452,16 @@ encodeBase58Token(TokenType type, void const* token, std::size_t size)
return detail::encodeBase58(buf.data(), expanded, buf.data() + expanded, bufsize - expanded);
}
/** Decode a Base58Check XRPL token string and return the raw payload (reference implementation).
*
* Validates decoded length (≥ 6 bytes), the leading type byte, and the
* trailing 4-byte checksum. Returns an empty string on any mismatch.
*
* @param s The Base58Check-encoded string to decode.
* @param type The expected `TokenType` version byte.
* @return The raw payload with the type byte and checksum stripped, or an
* empty string if validation fails.
*/
std::string
decodeBase58Token(std::string const& s, TokenType type)
{
@@ -345,7 +491,33 @@ decodeBase58Token(std::string const& s, TokenType type)
// meantime MS falls back to the slower reference implementation)
namespace b58_fast {
namespace detail {
// Note: both the input and output will be BIG ENDIAN
/** Encode big-endian binary data as a big-endian Base58 byte sequence (fast path).
*
* Implements the three-hop conversion `base 256 → base 2^64 → base 58^10 → base 58`
* using GCC's `unsigned __int128` for carry-free multi-precision arithmetic:
*
* 1. The input bytes are loaded into at most 5 `uint64_t` limbs (little-endian
* coefficient order), representing the value in base 2^64.
* 2. The 2^64 limb array is repeatedly divided by `58^10 = 430804206899405824`
* via `inplaceBigintDivRem` to extract base-58^10 coefficients.
* 3. Each base-58^10 coefficient is expanded into 10 base-58 digits via
* `b5810ToB58Be` and mapped through `kALPHABET_FORWARD`.
*
* Leading zero bytes in `input` each emit the alphabet's first character
* (`'r'`) to preserve the bijective encoding property.
*
* @note Both `input` and `out` are in big-endian byte order.
* @note Maximum valid `input` is 38 bytes (33-byte public key + 1 type + 4 checksum).
* Larger inputs return `TokenCodecErrc::InputTooLarge`.
*
* @param input The big-endian binary data to encode.
* @param out Caller-supplied output buffer; must be large enough for the
* encoded result (≈ ceil(len×log(256)/log(58)) + leading-zero count).
* Returns `TokenCodecErrc::OutputTooSmall` if insufficient.
* @return On success, a subspan of `out` containing the encoded Base58 bytes.
* On failure, an unexpected `TokenCodecErrc` error code.
*/
B58Result<std::span<std::uint8_t>>
b256ToB58Be(std::span<std::uint8_t const> input, std::span<std::uint8_t> out)
{
@@ -466,7 +638,35 @@ b256ToB58Be(std::span<std::uint8_t const> input, std::span<std::uint8_t> out)
return out.subspan(0, outIndex);
}
// Note the input is BIG ENDIAN (some fn in this module use little endian)
/** Decode a big-endian Base58 string to big-endian binary data (fast path).
*
* Reverses `b256ToB58Be` using the same three-hop strategy in reverse:
* `base 58 → base 58^10 → base 2^64 → base 2^8`:
*
* 1. The input string is partitioned into chunks of 10 characters
* (with a possible shorter partial chunk at the start). Each chunk is
* accumulated into a `uint64_t` base-58^10 coefficient using simple
* 64-bit arithmetic.
* 2. The base-58^10 coefficients synthesize a multi-precision value stored
* as `uint64_t` limbs via `inplaceBigintMul` (×58^10) and
* `inplaceBigintAdd` (+coefficient) for each chunk.
* 3. The `uint64_t` limbs are written out as big-endian bytes, suppressing
* leading zeros from the most-significant limb.
*
* Leading `'r'` characters (alphabet index 0) map back to leading zero bytes.
*
* @note The input string is big-endian (most significant Base58 digit first).
* @note Maximum input length is 52 characters (encoding up to 38 bytes);
* larger inputs return `TokenCodecErrc::InputTooLarge`.
* @note `out` must be at least 8 bytes; returns `TokenCodecErrc::OutputTooSmall`
* if the buffer is too small to hold the decoded result.
*
* @param input The Base58-encoded string to decode (big-endian digit order).
* @param out Caller-supplied output buffer for the decoded bytes.
* @return On success, a subspan of `out` containing the decoded big-endian bytes.
* On failure, an unexpected `TokenCodecErrc` error code (e.g.,
* `InvalidEncodingChar` for characters not in the XRPL alphabet).
*/
B58Result<std::span<std::uint8_t>>
b58ToB256Be(std::string_view input, std::span<std::uint8_t> out)
{
@@ -604,6 +804,21 @@ b58ToB256Be(std::string_view input, std::span<std::uint8_t> out)
}
} // namespace detail
/** Encode a raw payload as a Base58Check XRPL token (fast, span-based overload).
*
* Builds the wire layout `[tokenType][input][checksum(4)]` in a 128-byte
* stack buffer, then delegates to `detail::b256ToB58Be` to perform the
* base conversion. The output is written directly into the caller-supplied
* `out` span, avoiding heap allocation.
*
* @param tokenType The `TokenType` version byte to prepend.
* @param input The raw payload bytes to encode. Must be non-empty and
* at most 123 bytes (128 buffer 5 bytes for type + checksum).
* @param out Caller-supplied output buffer for the encoded Base58 bytes.
* @return On success, a subspan of `out` containing the encoded result.
* On failure, an unexpected `TokenCodecErrc` (`InputTooLarge`,
* `InputTooSmall`, or `OutputTooSmall`).
*/
B58Result<std::span<std::uint8_t>>
encodeBase58Token(
TokenType tokenType,
@@ -620,21 +835,33 @@ encodeBase58Token(
{
return Unexpected(TokenCodecErrc::InputTooSmall);
}
// <type (1 byte)><token (input len)><checksum (4 bytes)>
buf[0] = static_cast<std::uint8_t>(tokenType);
// buf[1..=input.len()] = input;
memcpy(&buf[1], input.data(), input.size());
size_t const checksumI = input.size() + 1;
// buf[checksum_i..checksum_i + 4] = checksum
checksum(buf.data() + checksumI, buf.data(), checksumI);
std::span<std::uint8_t const> const b58Span(buf.data(), input.size() + 5);
return detail::b256ToB58Be(b58Span, out);
}
// Convert from base 58 to base 256, largest coefficients first
// The input is encoded in XRPL format, with the token in the first
// byte and the checksum in the last four bytes.
// The decoded base 256 value does not include the token type or checksum.
// It is an error if the token type or checksum does not match.
/** Decode a Base58Check XRPL token and return the raw payload (fast, span-based overload).
*
* Converts the Base58 string to binary via `detail::b58ToB256Be`, then
* applies three-layer validation:
* 1. Decoded length ≥ 6 bytes (1 type + at least 1 payload byte + 4 checksum).
* 2. Leading type byte matches `type`.
* 3. Trailing 4-byte double-SHA256 checksum is correct.
*
* Only the interior payload (type byte and checksum stripped) is copied into
* `outBuf` on success.
*
* @param type The expected `TokenType` version byte.
* @param s The Base58Check-encoded string to decode (largest digit first).
* @param outBuf Caller-supplied buffer for the decoded payload bytes.
* @return On success, a subspan of `outBuf` containing the decoded payload.
* On failure, an unexpected `TokenCodecErrc` (`InputTooSmall`,
* `MismatchedTokenType`, `MismatchedChecksum`, `OutputTooSmall`,
* `InvalidEncodingChar`, or `InputTooLarge`).
*/
B58Result<std::span<std::uint8_t>>
decodeBase58Token(TokenType type, std::string_view s, std::span<std::uint8_t> outBuf)
{
@@ -670,16 +897,22 @@ decodeBase58Token(TokenType type, std::string_view s, std::span<std::uint8_t> ou
return outBuf.subspan(0, outSize);
}
/** Encode a raw payload as a Base58Check XRPL token string (fast, legacy string overload).
*
* Bridges the zero-allocation span-based API to the legacy `std::string`
* interface expected by callers that pre-date the span overload. Pre-allocates
* 128 bytes — well above the theoretical maximum of ≈46 Base58 characters for
* a 33-byte public key — to avoid reallocation.
*
* @param type The `TokenType` version byte to prepend.
* @param token Pointer to the raw payload bytes.
* @param size Number of payload bytes.
* @return The Base58Check-encoded string, or an empty string on any error.
*/
[[nodiscard]] std::string
encodeBase58Token(TokenType type, void const* token, std::size_t size)
{
std::string sr;
// The largest object encoded as base58 is 33 bytes; This will be encoded in
// at most ceil(log(2^256,58)) bytes, or 46 bytes. 128 is plenty (and
// there's not real benefit making it smaller). Note that 46 bytes may be
// encoded in more than 46 base58 chars. Since decode uses 64 as the
// over-allocation, this function uses 128 (again, over-allocation assuming
// 2 base 58 char per byte)
sr.resize(128);
std::span<std::uint8_t> const outSp(reinterpret_cast<std::uint8_t*>(sr.data()), sr.size());
std::span<std::uint8_t const> const inSp(reinterpret_cast<std::uint8_t const*>(token), size);
@@ -690,12 +923,21 @@ encodeBase58Token(TokenType type, void const* token, std::size_t size)
return sr;
}
/** Decode a Base58Check XRPL token string and return the raw payload (fast, legacy string overload).
*
* Bridges the zero-allocation span-based API to the legacy `std::string`
* interface. Pre-allocates 64 bytes — sufficient for any valid XRPL token
* payload — to avoid reallocation.
*
* @param s The Base58Check-encoded string to decode.
* @param type The expected `TokenType` version byte.
* @return The raw payload with type byte and checksum stripped, or an empty
* string on any error (invalid character, type mismatch, checksum failure, etc.).
*/
[[nodiscard]] std::string
decodeBase58Token(std::string const& s, TokenType type)
{
std::string sr;
// The largest object encoded as base58 is 33 bytes; 64 is plenty (and
// there's no benefit making it smaller)
sr.resize(64);
std::span<std::uint8_t> const outSp(reinterpret_cast<std::uint8_t*>(sr.data()), sr.size());
auto r = b58_fast::decodeBase58Token(type, s, outSp);

View File

@@ -157,7 +157,6 @@ OfferCreate::preflight(PreflightContext const& ctx)
JLOG(j.debug()) << "Malformed offer: redundant (IOU for IOU)";
return temREDUNDANT;
}
// We don't allow a non-native currency to use the currency code XRP.
if (badAsset() == uPaysAsset || badAsset() == uGetsAsset)
{
JLOG(j.debug()) << "Malformed offer: bad currency";
@@ -200,7 +199,8 @@ OfferCreate::preclaim(PreclaimContext const& ctx)
return tecFROZEN;
}
// Allow unfunded MPT for issuer (OutstandingAmount >= MaximumAmount)
// MPT issuers may place offers even when OutstandingAmount >= MaximumAmount
// (they have no "balance" to be funded from).
if ((!saTakerGets.holds<MPTIssue>() || saTakerGets.getIssuer() != id) &&
accountFunds(
ctx.view,
@@ -214,8 +214,6 @@ OfferCreate::preclaim(PreclaimContext const& ctx)
return tecUNFUNDED_OFFER;
}
// This can probably be simplified to make sure that you cancel sequences
// before the transaction sequence number.
if (cancelSequence && (uAccountSequence <= *cancelSequence))
{
JLOG(ctx.j.debug()) << "uAccountSequenceNext=" << uAccountSequence
@@ -230,7 +228,6 @@ OfferCreate::preclaim(PreclaimContext const& ctx)
return tecEXPIRED;
}
// Make sure that we are authorized to hold what the taker will pay us.
if (!saTakerPays.native())
{
auto result = checkAcceptAsset(ctx.view, ctx.flags, id, ctx.j, uPaysAsset);
@@ -238,8 +235,6 @@ OfferCreate::preclaim(PreclaimContext const& ctx)
return result;
}
// if domain is specified, make sure that domain exists and the offer create
// is part of the domain
if (ctx.tx.isFieldPresent(sfDomainID))
{
if (!permissioned_dex::accountInDomain(ctx.view, id, ctx.tx[sfDomainID]))
@@ -262,7 +257,6 @@ OfferCreate::checkAcceptAsset(
beast::Journal const j,
Asset const& asset)
{
// Only valid for custom currencies
XRPL_ASSERT(!isXRP(asset), "xrpl::OfferCreate::checkAcceptAsset : input is not XRP");
auto const issuerAccount = view.read(keylet::account(asset.getIssuer()));
@@ -275,9 +269,8 @@ OfferCreate::checkAcceptAsset(
return ((flags & TapRetry) != 0u) ? TER{terNO_ACCOUNT} : TER{tecNO_ISSUER};
}
// An account cannot create a trustline to itself, so no line can exist
// to be frozen. Additionally, an issuer can always accept its own
// issuance.
// An issuer always accepts its own issuance; no trust line can exist
// between an account and itself, so no freeze check is needed either.
if (asset.getIssuer() == id)
return tesSUCCESS;
@@ -293,10 +286,8 @@ OfferCreate::checkAcceptAsset(
return ((flags & TapRetry) != 0u) ? TER{terNO_LINE} : TER{tecNO_LINE};
}
// Entries have a canonical representation, determined by a
// lexicographical "greater than" comparison employing
// strict weak ordering. Determine which entry we need to
// access.
// Trust-line entries use canonical account ordering (id > issuer
// selects the "low" side fields).
bool const canonicalGt(id > issuer);
bool const isAuthorized(
@@ -318,8 +309,8 @@ OfferCreate::checkAcceptAsset(
return tesSUCCESS;
}
// There's no difference which side enacted deep freeze, accepting
// tokens shouldn't be possible.
// Either side can enact deep freeze; once set, token movement is
// prohibited regardless of which side initiated it.
bool const deepFrozen =
((*trustLine)[sfFlags] & (lsfLowDeepFreeze | lsfHighDeepFreeze)) != 0u;
@@ -331,8 +322,8 @@ OfferCreate::checkAcceptAsset(
return tesSUCCESS;
},
[&](MPTIssue const& issue) -> TER {
// WeakAuth - don't check if MPToken exists since it's created
// if needed.
// WeakAuth: an MPToken holder entry need not pre-exist — it will
// be created lazily if the account has the right authorization.
return requireAuth(view, issue, id, AuthType::WeakAuth);
});
}
@@ -539,10 +530,8 @@ OfferCreate::applyHybrid(
if (!sleOffer->isFieldPresent(sfDomainID))
return tecINTERNAL; // LCOV_EXCL_LINE
// set hybrid flag
sleOffer->setFlag(lsfHybrid);
// if offer is hybrid, need to also place into open offer dir
Book const book{saTakerPays.asset(), saTakerGets.asset(), std::nullopt};
auto dir = keylet::quality(keylet::kBOOK(book), getRate(saTakerGets, saTakerPays));
@@ -722,8 +711,6 @@ OfferCreate::applyGuts(Sandbox& sb, Sandbox& sbCancel)
if (takerAmount != placeOffer)
crossed = true;
// The offer that we need to place after offer crossing should
// never be negative. If it is, something went very very wrong.
if (placeOffer.in < kZERO || placeOffer.out < kZERO)
{
JLOG(j_.fatal()) << "Cross left offer negative!"

View File

@@ -1,3 +1,12 @@
/** @file
* Implementation of the `DIDDelete` transactor, which removes a
* Decentralized Identifier (`ltDID`) ledger entry owned by the submitting
* account. See `DIDDelete.h` for the full interface contract.
*
* The `deleteSLE(ApplyView&, ...)` overload is intentionally reachable
* from other transactors (notably `AccountDelete`) that need to clean up
* an owned DID without constructing a full transactor context.
*/
#include <xrpl/tx/transactors/did/DIDDelete.h>
#include <xrpl/basics/Log.h>
@@ -43,7 +52,6 @@ DIDDelete::deleteSLE(
AccountID const owner,
beast::Journal j)
{
// Remove object from owner directory
if (!view.dirRemove(keylet::ownerDir(owner), (*sle)[sfOwnerNode], sle->key(), true))
{
// LCOV_EXCL_START
@@ -59,7 +67,6 @@ DIDDelete::deleteSLE(
adjustOwnerCount(view, sleOwner, -1, j);
view.update(sleOwner);
// Remove object from ledger
view.erase(sle);
return tesSUCCESS;
}

View File

@@ -1,3 +1,15 @@
/** @file
* Implementation of the `DIDSet` transactor, which creates or updates a
* Decentralized Identifier (DID) ledger object (`ltDID`) owned by the
* submitting account. The DID object stores up to three independently-optional
* blob fields — `sfURI`, `sfDIDDocument`, and `sfData` — each capped at 256
* bytes. The implementation conforms to the W3C DID v1.0 specification
* (https://www.w3.org/TR/did-core/).
*
* @note The symmetric removal path lives in `DIDDelete.cpp`. The file-local
* `addSLE` helper mirrors `DIDDelete::deleteSLE` in reverse: reserve
* check → insert → dirInsert → adjustOwnerCount(+1).
*/
#include <xrpl/tx/transactors/did/DIDSet.h>
#include <xrpl/core/ServiceRegistry.h>
@@ -23,20 +35,25 @@
namespace xrpl {
/*
DID
======
Decentralized Identifiers (DIDs) are a new type of identifier that enable
verifiable, self-sovereign digital identity and are designed to be
compatible with any distributed ledger or network. This implementation
conforms to the requirements specified in the DID v1.0 specification
currently recommended by the W3C Credentials Community Group
(https://www.w3.org/TR/did-core/).
*/
//------------------------------------------------------------------------------
/** Validate DID transaction fields before any ledger state is read.
*
* Enforces two emptiness invariants:
* 1. At least one of `sfURI`, `sfDIDDocument`, or `sfData` must be present in
* the transaction. A transaction with none of them is meaningless.
* 2. All three fields may not be simultaneously present but empty. A client
* sending `URI=""`, `DIDDocument=""`, `Data=""` is attempting to wipe an
* existing DID via `DIDSet` rather than `DIDDelete`; the protocol blocks
* this by treating "all-present, all-empty" as equivalent to no payload.
*
* Also enforces per-field byte-length limits via the `isTooLong` lambda, which
* uses the `~sField` (optional) accessor so absent fields are silently skipped.
*
* @param ctx Preflight context containing the raw transaction fields.
* @return `temEMPTY_DID` if no non-empty field payload is provided;
* `temMALFORMED` if any present field exceeds its maximum byte length
* (`kMAX_DIDURI_LENGTH`, `kMAX_DID_DOCUMENT_LENGTH`, or
* `kMAX_DID_DATA_LENGTH`); `tesSUCCESS` otherwise.
*/
NotTEC
DIDSet::preflight(PreflightContext const& ctx)
{
@@ -63,6 +80,31 @@ DIDSet::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
/** Insert a new owner-tracked SLE into the ledger with all required bookkeeping.
*
* Performs the standard three-step sequence for creating any owned ledger
* object: reserve check, object insertion, and directory linkage.
*
* 1. **Reserve check**: verifies `sfBalance ≥ accountReserve(ownerCount + 1)`
* before touching anything. The check uses the pre-insertion owner count so
* that `tecINSUFFICIENT_RESERVE` is returned cleanly with no partial state.
* 2. **Insertion**: calls `ctx.view().insert(sle)` to add the object to the
* ledger view.
* 3. **Directory linkage**: calls `dirInsert` to register the object's key in
* the account's owner directory and stores the returned page index in
* `sfOwnerNode` on the SLE. That cached index enables O(1) removal by
* `DIDDelete::deleteSLE` via `dirRemove`. Increments `sfOwnerCount` on the
* account root and writes the updated account SLE back.
*
* @param ctx Apply context providing the mutable ledger view and journal.
* @param sle The freshly constructed SLE to insert; must not yet be present
* in the view.
* @param owner Account that will own the new object.
* @return `tecINSUFFICIENT_RESERVE` if the account balance is below the new
* reserve threshold; `tecDIR_FULL` if the owner directory has no room
* (unreachable in practice — marked `LCOV_EXCL_LINE`); `tesSUCCESS` on
* success.
*/
static TER
addSLE(ApplyContext& ctx, std::shared_ptr<SLE> const& sle, AccountID const& owner)
{
@@ -70,7 +112,6 @@ addSLE(ApplyContext& ctx, std::shared_ptr<SLE> const& sle, AccountID const& owne
if (!sleAccount)
return tefINTERNAL; // LCOV_EXCL_LINE
// Check reserve availability for new object creation
{
auto const balance = STAmount((*sleAccount)[sfBalance]).xrp();
auto const reserve = ctx.view().fees().accountReserve((*sleAccount)[sfOwnerCount] + 1);
@@ -79,10 +120,8 @@ addSLE(ApplyContext& ctx, std::shared_ptr<SLE> const& sle, AccountID const& owne
return tecINSUFFICIENT_RESERVE;
}
// Add ledger object to ledger
ctx.view().insert(sle);
// Add ledger object to owner's page
{
auto page =
ctx.view().dirInsert(keylet::ownerDir(owner), sle->key(), describeOwnerDir(owner));
@@ -96,13 +135,45 @@ addSLE(ApplyContext& ctx, std::shared_ptr<SLE> const& sle, AccountID const& owne
return tesSUCCESS;
}
/** Apply the DID create-or-update to the ledger.
*
* Dispatches to one of two paths based on whether a DID SLE already exists
* for the submitting account (keyed by `keylet::did(account_)`).
*
* **Update path** (SLE exists): applies a local `update` lambda to each of
* `sfURI`, `sfDIDDocument`, and `sfData`. The lambda is intentionally
* asymmetric: an absent transaction field is a no-op (leaves the stored value
* unchanged), an empty field actively removes the attribute from the SLE via
* `makeFieldAbsent` (surgical clear), and a non-empty field overwrites the
* stored value. After all three updates, the code re-checks whether all fields
* are now absent — an update that clears the last remaining attribute produces
* an empty DID, which is rejected with `tecEMPTY_DID`. Note the `tec` code
* rather than `tem`: the fee is still charged because preflight already passed.
*
* **Create path** (no SLE exists): constructs a fresh `ltDID` SLE, sets
* `sfAccount`, and copies non-empty transaction fields via a `set` lambda
* (absent or empty fields are skipped). Under `fixEmptyDID`, if all three
* fields end up absent in the new SLE the function returns `tecEMPTY_DID`
* before inserting. Without that amendment, the create path allowed an SLE
* where every payload field was absent (because `preflight` only caught the
* "all-present and all-empty" case, not the "all-present but some non-empty
* strings that happen to be zero-length" edge). Finally, `addSLE` is called
* to validate the owner reserve, insert the SLE, link it into the owner
* directory, and increment `sfOwnerCount`.
*
* @return `tesSUCCESS` on success; `tecEMPTY_DID` if the resulting DID would
* carry no attributes; `tecINSUFFICIENT_RESERVE` if the account cannot
* cover the incremental owner reserve (create path only); `tecDIR_FULL`
* if the owner directory cannot accommodate the new entry (create path
* only, unreachable in practice).
*/
TER
DIDSet::doApply()
{
// Edit ledger object if it already exists
Keylet const didKeylet = keylet::did(account_);
if (auto const sleDID = ctx_.view().peek(didKeylet))
{
// Update path: absent field = no-op, empty = remove, non-empty = replace.
auto update = [&](auto const& sField) {
if (auto const field = ctx_.tx[~sField])
{
@@ -129,7 +200,7 @@ DIDSet::doApply()
return tesSUCCESS;
}
// Create new ledger object otherwise
// Create path: build a fresh SLE and populate non-empty fields only.
auto const sleDID = std::make_shared<SLE>(didKeylet);
(*sleDID)[sfAccount] = account_;
@@ -141,6 +212,10 @@ DIDSet::doApply()
set(sfURI);
set(sfDIDDocument);
set(sfData);
// fixEmptyDID closes the preflight gap: reject creation of an SLE where
// every payload field ended up absent (e.g. all values were empty strings
// that passed the "not all three present-and-empty" preflight check).
if (ctx_.view().rules().enabled(fixEmptyDID) && !sleDID->isFieldPresent(sfURI) &&
!sleDID->isFieldPresent(sfDIDDocument) && !sleDID->isFieldPresent(sfData))
{
@@ -150,19 +225,28 @@ DIDSet::doApply()
return addSLE(ctx_, sleDID, account_);
}
/** Invariant visitor stub — no DID-specific per-entry invariants are enforced.
*
* The global invariant framework (`ValidAMM`, `XRPNotCreated`, etc.) already
* covers the properties that matter for DID objects. This override exists only
* to satisfy the `Transactor` interface contract.
*/
void
DIDSet::visitInvariantEntry(
bool,
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
{
// No transaction-specific invariants yet (future work).
}
/** Invariant finalizer stub — no DID-specific post-transaction invariants are
* enforced.
*
* @return `true` always.
*/
bool
DIDSet::finalizeInvariants(STTx const&, TER, XRPAmount, ReadView const&, beast::Journal const&)
{
// No transaction-specific invariants yet (future work).
return true;
}

View File

@@ -0,0 +1,23 @@
/** @file
* Build-system anchor for the escrow transactor module.
*
* This file is intentionally empty. All escrow logic is distributed across
* three sibling translation units:
*
* - `EscrowCreate.cpp` — locks XRP or tokens into a new escrow SLE, with
* optional time-based release and/or a crypto-condition fulfillment
* requirement. Contains the canonical module-level comment for the entire
* escrow subsystem.
* - `EscrowFinish.cpp` — releases escrowed funds to the destination after
* time and/or fulfillment conditions are met; validates and caches
* crypto-conditions via the hash router.
* - `EscrowCancel.cpp` — returns escrowed funds to the originator after the
* cancel-after deadline has passed; no fee is applied on return.
*
* The file exists as a structural artifact of the build system: either
* CMake required a translation unit when escrow logic was first introduced,
* or a refactor that split a monolithic `Escrow.cpp` into three specialized
* transactors left this source behind.
*
* @see EscrowCreate.h, EscrowFinish.h, EscrowCancel.h
*/

View File

@@ -1,3 +1,12 @@
/** @file
* Implementation of the `EscrowCancel` transactor.
*
* Handles the expiry-refund path of the escrow lifecycle: once the ledger's
* parent close time has advanced past `sfCancelAfter`, any party may submit
* an `EscrowCancel` to return the locked funds to the original escrow creator.
* Supports XRP escrows (original design) and token escrows (IOU / MPT) gated
* behind `featureTokenEscrow`.
*/
#include <xrpl/tx/transactors/escrow/EscrowCancel.h>
#include <xrpl/basics/Log.h>
@@ -34,6 +43,18 @@ EscrowCancel::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
/** Read-only authorization check for non-XRP escrow cancellations.
*
* Primary template — no body; only the `Issue` and `MPTIssue` full
* specializations are defined. Called via `std::visit` on the `Asset` variant
* held by the escrow's `sfAmount`.
*
* @tparam T Asset type; must be `Issue` (IOU) or `MPTIssue` (MPT).
* @param ctx Read-only preclaim context providing ledger access.
* @param account Escrow creator (`sfAccount` on the escrow SLE).
* @param amount Escrowed token amount; its asset identifies the issuer.
* @return `tesSUCCESS` if cancellation may proceed; a `tec` error otherwise.
*/
template <ValidIssueType T>
static TER
escrowCancelPreclaimHelper(
@@ -41,6 +62,18 @@ escrowCancelPreclaimHelper(
AccountID const& account,
STAmount const& amount);
/** IOU specialization: verifies the escrow owner is still authorized by the issuer.
*
* Authorization must remain valid at cancel time, not just at creation time.
* The `issuer == account` case is impossible by construction but is guarded
* defensively and marked unreachable for coverage purposes.
*
* @param ctx Read-only preclaim context.
* @param account Escrow creator (`sfAccount` on the escrow SLE).
* @param amount Escrowed IOU amount.
* @return `tesSUCCESS` if authorized; `tecINTERNAL` if issuer equals account
* (unreachable); a `requireAuth` error if authorization was revoked.
*/
template <>
TER
escrowCancelPreclaimHelper<Issue>(
@@ -49,17 +82,31 @@ escrowCancelPreclaimHelper<Issue>(
STAmount const& amount)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecINTERNAL
if (issuer == account)
return tecINTERNAL; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(ctx.view, amount.get<Issue>(), account); !isTesSuccess(ter))
return ter;
return tesSUCCESS;
}
/** MPT specialization: verifies the MPT issuance still exists and the escrow
* owner remains authorized to hold it.
*
* Unlike the IOU path, an MPT issuance can be deleted while an escrow
* referencing it is still open; this check surfaces that condition early.
* Authorization uses `AuthType::WeakAuth` because MPTs operate under a less
* strict per-transaction approval model than IOU trust lines.
*
* @param ctx Read-only preclaim context.
* @param account Escrow creator (`sfAccount` on the escrow SLE).
* @param amount Escrowed MPT amount.
* @return `tesSUCCESS` if authorized; `tecINTERNAL` if issuer equals account
* (unreachable); `tecOBJECT_NOT_FOUND` if the MPT issuance was
* deleted after the escrow was created; a `requireAuth` error if
* weak authorization is denied.
*/
template <>
TER
escrowCancelPreclaimHelper<MPTIssue>(
@@ -68,18 +115,14 @@ escrowCancelPreclaimHelper<MPTIssue>(
STAmount const& amount)
{
AccountID const issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecINTERNAL
if (issuer == account)
return tecINTERNAL; // LCOV_EXCL_LINE
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
@@ -130,17 +173,15 @@ EscrowCancel::doApply()
auto const now = ctx_.view().header().parentCloseTime;
// No cancel time specified: can't execute at all.
// Escrows with no sfCancelAfter can never be cancelled.
if (!(*slep)[~sfCancelAfter])
return tecNO_PERMISSION;
// Too soon: can't execute before the cancel time.
if (!after(now, (*slep)[sfCancelAfter]))
return tecNO_PERMISSION;
AccountID const account = (*slep)[sfAccount];
// Remove escrow from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
@@ -152,7 +193,6 @@ EscrowCancel::doApply()
}
}
// Remove escrow from recipient's owner directory, if present.
if (auto const optPage = (*slep)[~sfDestinationNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir((*slep)[sfDestination]), *optPage, k.key, true))
@@ -167,7 +207,6 @@ EscrowCancel::doApply()
auto const sle = ctx_.view().peek(keylet::account(account));
STAmount const amount = slep->getFieldAmount(sfAmount);
// Transfer amount back to the owner
if (isXRP(amount))
{
(*sle)[sfBalance] = (*sle)[sfBalance] + amount;
@@ -197,7 +236,7 @@ EscrowCancel::doApply()
!isTesSuccess(ret))
return ret; // LCOV_EXCL_LINE
// Remove escrow from issuers owner directory, if present.
// Non-XRP escrows also carry an sfIssuerNode directory entry.
if (auto const optPage = (*slep)[~sfIssuerNode]; optPage)
{
if (!ctx_.view().dirRemove(keylet::ownerDir(issuer), *optPage, k.key, true))
@@ -213,7 +252,6 @@ EscrowCancel::doApply()
adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal);
ctx_.view().update(sle);
// Remove escrow from ledger
ctx_.view().erase(slep);
return tesSUCCESS;

View File

@@ -1,3 +1,22 @@
/** @file
* Implements the EscrowCreate transactor, which locks XRP or token (IOU/MPT)
* funds in a conditional escrow ledger object. The escrow can later be
* released by `EscrowFinish` or reclaimed by `EscrowCancel`.
*
* Locked funds are completely unavailable to either party until one of those
* outcomes occurs. Time-based unlocks and crypto-conditions are both
* supported unlock mechanisms. Escrows may also carry an expiry after which
* only `EscrowCancel` is permitted.
*
* Token escrow (`Issue`/`MPTIssue`) is gated on `featureTokenEscrow`.
*
* @see https://xrpl.org/escrowcreate.html
* @see https://xrpl.org/escrowfinish.html
* @see https://xrpl.org/escrowcancel.html
* @see EscrowFinish
* @see EscrowCancel
*/
#include <xrpl/tx/transactors/escrow/EscrowCreate.h>
#include <xrpl/basics/Log.h>
@@ -38,40 +57,6 @@
namespace xrpl {
/*
Escrow
======
Escrow is a feature of the XRP Ledger that allows you to send conditional
XRP payments. These conditional payments, called escrows, set aside XRP and
deliver it later when certain conditions are met. Conditions to successfully
finish an escrow include time-based unlocks and crypto-conditions. Escrows
can also be set to expire if not finished in time.
The XRP set aside in an escrow is locked up. No one can use or destroy the
XRP until the escrow has been successfully finished or canceled. Before the
expiration time, only the intended receiver can get the XRP. After the
expiration time, the XRP can only be returned to the sender.
For more details on escrow, including examples, diagrams and more please
visit https://xrpl.org/escrow.html
For details on specific transactions, including fields and validation rules
please see:
`EscrowCreate`
--------------
See: https://xrpl.org/escrowcreate.html
`EscrowFinish`
--------------
See: https://xrpl.org/escrowfinish.html
`EscrowCancel`
--------------
See: https://xrpl.org/escrowcancel.html
*/
//------------------------------------------------------------------------------
TxConsequences
@@ -81,10 +66,30 @@ EscrowCreate::makeTxConsequences(PreflightContext const& ctx)
return TxConsequences{ctx.tx, isXRP(amount) ? amount.xrp() : beast::kZERO};
}
/** Stateless preflight checks specific to each non-XRP asset type.
*
* Specialised for `Issue` (IOU) and `MPTIssue` (MPT). Called from
* `EscrowCreate::preflight` after `featureTokenEscrow` has been confirmed
* active. Validates amount polarity, magnitude, and asset-type constraints
* without touching the ledger.
*
* @tparam T `Issue` or `MPTIssue`.
* @param ctx stateless preflight context.
* @return `tesSUCCESS`, or a `tem*` error code.
*/
template <ValidIssueType T>
static NotTEC
escrowCreatePreflightHelper(PreflightContext const& ctx);
/** IOU-specific preflight helper.
*
* Rejects native (XRP) amounts, non-positive values, and the bad-currency
* sentinel. The native guard prevents XRP amounts from reaching this path
* via an `Issue`-typed specialisation.
*
* @param ctx stateless preflight context.
* @return `temBAD_AMOUNT` for invalid amounts; `tesSUCCESS` otherwise.
*/
template <>
NotTEC
escrowCreatePreflightHelper<Issue>(PreflightContext const& ctx)
@@ -99,6 +104,17 @@ escrowCreatePreflightHelper<Issue>(PreflightContext const& ctx)
return tesSUCCESS;
}
/** MPT-specific preflight helper.
*
* In addition to the checks common to all non-XRP assets (non-native, positive
* value), also requires `featureMPTokensV1` to be active and enforces the
* `kMAX_MP_TOKEN_AMOUNT` ceiling on the raw MPT balance.
*
* @param ctx stateless preflight context.
* @return `temDISABLED` if `featureMPTokensV1` is inactive;
* `temBAD_AMOUNT` for out-of-range or non-positive amounts;
* `tesSUCCESS` otherwise.
*/
template <>
NotTEC
escrowCreatePreflightHelper<MPTIssue>(PreflightContext const& ctx)
@@ -113,6 +129,7 @@ escrowCreatePreflightHelper<MPTIssue>(PreflightContext const& ctx)
return tesSUCCESS;
}
/** @copydoc EscrowCreate::preflight */
NotTEC
EscrowCreate::preflight(PreflightContext const& ctx)
{
@@ -134,20 +151,18 @@ EscrowCreate::preflight(PreflightContext const& ctx)
return temBAD_AMOUNT;
}
// We must specify at least one timeout value
// At least one of sfCancelAfter or sfFinishAfter must be present;
// an escrow with neither has no defined lifecycle.
if (!ctx.tx[~sfCancelAfter] && !ctx.tx[~sfFinishAfter])
return temBAD_EXPIRATION;
// If both finish and cancel times are specified then the cancel time must
// be strictly after the finish time.
if (ctx.tx[~sfCancelAfter] && ctx.tx[~sfFinishAfter] &&
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
return temBAD_EXPIRATION;
// In the absence of a FinishAfter, the escrow can be finished
// immediately, which can be confusing. When creating an escrow,
// we want to ensure that either a FinishAfter time is explicitly
// specified or a completion condition is attached.
// Without sfFinishAfter the escrow could be completed immediately on
// creation, which is almost certainly a mistake. Require sfCondition
// as an explicit unlock mechanism when no finish time is given.
if (!ctx.tx[~sfFinishAfter] && !ctx.tx[~sfCondition])
return temMALFORMED;
@@ -168,6 +183,20 @@ EscrowCreate::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
/** Read-only ledger checks specific to each non-XRP asset type during preclaim.
*
* Specialised for `Issue` (IOU) and `MPTIssue` (MPT). Called from
* `EscrowCreate::preclaim` after confirming `featureTokenEscrow` is active.
* Validates issuer permissions, sender/destination authorisation, freeze
* status, and spendable balance against the live ledger state.
*
* @tparam T `Issue` or `MPTIssue`.
* @param ctx read-only preclaim context.
* @param account sender's `AccountID`.
* @param dest destination's `AccountID`.
* @param amount escrowed amount.
* @return `tesSUCCESS`, or a `tec*` error code.
*/
template <ValidIssueType T>
static TER
escrowCreatePreclaimHelper(
@@ -176,6 +205,35 @@ escrowCreatePreclaimHelper(
AccountID const& dest,
STAmount const& amount);
/** IOU-specific preclaim helper.
*
* Enforces the following in order:
* - Issuer and sender must differ (no self-escrow of issued credit).
* - Issuer must have `lsfAllowTrustLineLocking` set — token escrow is
* opt-in for issuers.
* - A trust line between sender and issuer must exist.
* - Trust line balance polarity must match the XRP Ledger address-ordering
* convention (positive balance → issuer > account; negative → issuer <
* account).
* - Both sender and destination must satisfy `requireAuth` when the issuer
* mandates authorisation.
* - Neither the sender's nor the destination's trust line may be frozen.
* - Sender's spendable balance (checked with `fhIGNORE_FREEZE` to obtain the
* raw balance) must be positive and cover `amount`.
* - `canAdd` precision guard: the amount must be addable back to the balance
* without precision loss when the escrow later finishes.
*
* @param ctx read-only preclaim context.
* @param account sender's `AccountID`.
* @param dest destination's `AccountID`.
* @param amount escrowed IOU amount.
* @return `tecNO_PERMISSION` if issuer == account or flags/auth fail;
* `tecNO_ISSUER` if the issuer account does not exist;
* `tecNO_LINE` if no trust line exists; `tecFROZEN` if either
* party is frozen; `tecINSUFFICIENT_FUNDS` if balance is
* insufficient; `tecPRECISION_LOSS` on arithmetic edge cases;
* `tesSUCCESS` otherwise.
*/
template <>
TER
escrowCreatePreclaimHelper<Issue>(
@@ -186,67 +244,85 @@ escrowCreatePreclaimHelper<Issue>(
{
Issue const& issue = amount.get<Issue>();
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the lsfAllowTrustLineLocking is not enabled, return tecNO_PERMISSION
auto const sleIssuer = ctx.view.read(keylet::account(issuer));
if (!sleIssuer)
return tecNO_ISSUER;
if (!sleIssuer->isFlag(lsfAllowTrustLineLocking))
return tecNO_PERMISSION;
// If the account does not have a trustline to the issuer, return tecNO_LINE
auto const sleRippleState = ctx.view.read(keylet::line(account, issuer, issue.currency));
if (!sleRippleState)
return tecNO_LINE;
STAmount const balance = (*sleRippleState)[sfBalance];
// If balance is positive, issuer must have higher address than account
// Trust line balance polarity encodes address ordering: positive balance
// means issuer has the higher address; negative means issuer has the lower.
if (balance > beast::kZERO && issuer < account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If balance is negative, issuer must have lower address than account
if (balance < beast::kZERO && issuer > account)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the issuer has requireAuth set, check if the account is authorized
if (auto const ter = requireAuth(ctx.view, issue, account); !isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(ctx.view, issue, dest); !isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecFROZEN
if (isFrozen(ctx.view, account, issue))
return tecFROZEN;
// If the issuer has frozen the destination, return tecFROZEN
if (isFrozen(ctx.view, dest, issue))
return tecFROZEN;
STAmount const spendableAmount = accountHolds(
ctx.view, account, issue.currency, issuer, FreezeHandling::IgnoreFreeze, ctx.j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZERO)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
// If the amount is not addable to the balance, return tecPRECISION_LOSS
// Guard against precision loss that would occur when the locked amount is
// returned to the sender's trust line balance on finish.
if (!canAdd(spendableAmount, amount))
return tecPRECISION_LOSS;
return tesSUCCESS;
}
/** MPT-specific preclaim helper.
*
* Mirrors the IOU helper in structure but applies MPT-specific rules:
* - Issuer and sender must differ.
* - The `MPTokenIssuance` object must exist and carry `lsfMPTCanEscrow`.
* - The sender must hold an `MPToken` object for this issuance.
* - Both sender and destination must pass `requireAuth` (WeakAuth) when the
* issuance mandates authorisation.
* - Lock/freeze status is checked with `isFrozen`; violations return
* `tecLOCKED` (not `tecFROZEN`) — the deliberate distinction reflects the
* different freeze semantics for MPTs vs. IOU trust lines.
* - Transfer eligibility is confirmed via `canTransfer`.
* - Sender's spendable MPT balance must be positive and cover `amount`.
*
* @note Unlike the IOU path there is no `canAdd` precision check here because
* MPT arithmetic uses integer semantics without IOU rounding concerns.
*
* @param ctx read-only preclaim context.
* @param account sender's `AccountID`.
* @param dest destination's `AccountID`.
* @param amount escrowed MPT amount.
* @return `tecNO_PERMISSION` if issuer == account or flags/auth fail;
* `tecOBJECT_NOT_FOUND` if the issuance or sender's `MPToken`
* is absent; `tecLOCKED` if either party is frozen/locked;
* `tecINSUFFICIENT_FUNDS` if balance is insufficient;
* `tesSUCCESS` otherwise.
*/
template <>
TER
escrowCreatePreclaimHelper<MPTIssue>(
@@ -256,51 +332,40 @@ escrowCreatePreclaimHelper<MPTIssue>(
STAmount const& amount)
{
AccountID const issuer = amount.getIssuer();
// If the issuer is the same as the account, return tecNO_PERMISSION
if (issuer == account)
return tecNO_PERMISSION;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the lsfMPTCanEscrow is not enabled, return tecNO_PERMISSION
if (!sleIssuance->isFlag(lsfMPTCanEscrow))
return tecNO_PERMISSION;
// If the issuer is not the same as the issuer of the mpt, return
// tecNO_PERMISSION
if (sleIssuance->getAccountID(sfIssuer) != issuer)
return tecNO_PERMISSION; // LCOV_EXCL_LINE
// If the account does not have the mpt, return tecOBJECT_NOT_FOUND
if (!ctx.view.exists(keylet::mptoken(issuanceKey.key, account)))
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the account is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, account, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has requireAuth set, check if the destination is
// authorized
if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has frozen the account, return tecLOCKED
// MPT freeze violations use tecLOCKED rather than tecFROZEN to distinguish
// MPT lock semantics from IOU global/per-line freeze semantics.
if (isFrozen(ctx.view, account, mptIssue))
return tecLOCKED;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(ctx.view, dest, mptIssue))
return tecLOCKED;
// If the mpt cannot be transferred, return tecNO_AUTH
if (auto const ter = canTransfer(ctx.view, mptIssue, account, dest); !isTesSuccess(ter))
return ter;
@@ -312,18 +377,16 @@ escrowCreatePreclaimHelper<MPTIssue>(
AuthHandling::IgnoreAuth,
ctx.j);
// If the balance is less than or equal to 0, return tecINSUFFICIENT_FUNDS
if (spendableAmount <= beast::kZERO)
return tecINSUFFICIENT_FUNDS;
// If the spendable amount is less than the amount, return
// tecINSUFFICIENT_FUNDS
if (spendableAmount < amount)
return tecINSUFFICIENT_FUNDS;
return tesSUCCESS;
}
/** @copydoc EscrowCreate::preclaim */
TER
EscrowCreate::preclaim(PreclaimContext const& ctx)
{
@@ -358,6 +421,21 @@ EscrowCreate::preclaim(PreclaimContext const& ctx)
return tesSUCCESS;
}
/** Debit the sender's token balance when creating a non-XRP escrow.
*
* Specialised for `Issue` (IOU) and `MPTIssue` (MPT). Performs the
* asset-type-specific ledger mutation that removes funds from the sender's
* spendable balance and places them under escrow control.
*
* @tparam T `Issue` or `MPTIssue`.
* @param view mutable apply view.
* @param issuer token issuer's `AccountID`.
* @param sender sender's `AccountID`.
* @param amount escrowed amount.
* @param journal logging journal.
* @return `tesSUCCESS`, `tecINTERNAL` (defensive; should never fire),
* or any error propagated from the underlying transfer helper.
*/
template <ValidIssueType T>
static TER
escrowLockApplyHelper(
@@ -367,6 +445,22 @@ escrowLockApplyHelper(
STAmount const& amount,
beast::Journal journal);
/** IOU locking: transfer tokens fee-free from sender back to issuer.
*
* Conceptually retires the tokens from circulation; they will be re-issued
* to the destination by `EscrowFinish`. `directSendNoFee` is used so no
* transfer fee is charged at lock time (the fee, if any, is charged on
* finish using the rate snapshotted in `sfTransferRate`).
*
* @param view mutable apply view.
* @param issuer token issuer's `AccountID`.
* @param sender sender's `AccountID`.
* @param amount escrowed IOU amount.
* @param journal logging journal.
* @return `tecINTERNAL` if issuer == sender (defensive, should never
* fire in production); otherwise the result of
* `directSendNoFee`.
*/
template <>
TER
escrowLockApplyHelper<Issue>(
@@ -376,7 +470,8 @@ escrowLockApplyHelper<Issue>(
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
// Defensive: preclaim already rejected issuer == sender; tecINTERNAL
// here indicates an internal inconsistency rather than a user error.
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -387,6 +482,22 @@ escrowLockApplyHelper<Issue>(
return tesSUCCESS;
}
/** MPT locking: atomically move tokens into the sender's locked-amount field.
*
* `lockEscrowMPT` decrements `sfMPTAmount` and increments `sfLockedAmount`
* on the sender's `MPToken` SLE, and increments `sfLockedAmount` on the
* `MPTokenIssuance` SLE. Ownership of the tokens does not change — no
* issuer directory entry is needed because the issuance object tracks the
* total locked supply directly.
*
* @param view mutable apply view.
* @param issuer token issuer's `AccountID`.
* @param sender sender's `AccountID`.
* @param amount escrowed MPT amount.
* @param journal logging journal.
* @return `tecINTERNAL` if issuer == sender (defensive); otherwise
* the result of `lockEscrowMPT`.
*/
template <>
TER
escrowLockApplyHelper<MPTIssue>(
@@ -396,7 +507,7 @@ escrowLockApplyHelper<MPTIssue>(
STAmount const& amount,
beast::Journal journal)
{
// Defensive: Issuer cannot create an escrow
// Defensive: see escrowLockApplyHelper<Issue> comment above.
if (issuer == sender)
return tecINTERNAL; // LCOV_EXCL_LINE
@@ -406,11 +517,16 @@ escrowLockApplyHelper<MPTIssue>(
return tesSUCCESS;
}
/** @copydoc EscrowCreate::doApply */
TER
EscrowCreate::doApply()
{
auto const closeTime = ctx_.view().header().parentCloseTime;
// Re-check time bounds against the current ledger's parentCloseTime.
// A transaction that was valid at preflight may arrive in a ledger where
// the expiry has already passed; creating an already-expired escrow must
// be rejected here rather than persisted.
if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter]))
return tecNO_PERMISSION;
@@ -421,23 +537,22 @@ EscrowCreate::doApply()
if (!sle)
return tefINTERNAL; // LCOV_EXCL_LINE
// Check reserve and funds availability
STAmount const amount{ctx_.tx[sfAmount]};
// Reserve check: the new escrow SLE adds one to the owner count.
auto const reserve = ctx_.view().fees().accountReserve((*sle)[sfOwnerCount] + 1);
auto const balance = sle->getFieldAmount(sfBalance).xrp();
if (balance < reserve)
return tecINSUFFICIENT_RESERVE;
// Check reserve and funds availability
// For XRP escrows the sender's balance must also cover the locked principal.
if (isXRP(amount))
{
if (balance < reserve + STAmount(amount).xrp())
return tecUNFUNDED;
}
// Check destination account
{
auto const sled = ctx_.view().read(keylet::account(ctx_.tx[sfDestination]));
if (!sled)
@@ -446,8 +561,8 @@ EscrowCreate::doApply()
return tecDST_TAG_NEEDED;
}
// Create escrow in ledger. Note that we use the value from the
// sequence or ticket. For more explanation see comments in SeqProxy.h.
// The escrow keylet is derived from the sender's account and the sequence
// (or ticket) value; see SeqProxy.h for the rationale.
Keylet const escrowKeylet = keylet::escrow(account_, ctx_.tx.getSeqValue());
auto const slep = std::make_shared<SLE>(escrowKeylet);
(*slep)[sfAmount] = amount;
@@ -459,11 +574,17 @@ EscrowCreate::doApply()
(*slep)[~sfFinishAfter] = ctx_.tx[~sfFinishAfter];
(*slep)[~sfDestinationTag] = ctx_.tx[~sfDestinationTag];
// fixIncludeKeyletFields: store sfSequence in the SLE so that
// EscrowFinish/EscrowCancel can reconstruct the keylet without external
// input, enabling more robust cross-ledger object navigation.
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))
{
(*slep)[sfSequence] = ctx_.tx.getSeqValue();
}
// Snapshot the issuer's transfer rate at creation time. EscrowFinish reads
// this stored rate to compute the correct delivery amount; the issuer cannot
// retroactively change the rate that applies to the locked funds.
if (ctx_.view().rules().enabled(featureTokenEscrow) && !isXRP(amount))
{
auto const xferRate = transferRate(ctx_.view(), amount);
@@ -473,7 +594,7 @@ EscrowCreate::doApply()
ctx_.view().insert(slep);
// Add escrow to sender's owner directory
// Register the escrow in the sender's owner directory unconditionally.
{
auto page = ctx_.view().dirInsert(
keylet::ownerDir(account_), escrowKeylet, describeOwnerDir(account_));
@@ -482,7 +603,7 @@ EscrowCreate::doApply()
(*slep)[sfOwnerNode] = *page;
}
// If it's not a self-send, add escrow to recipient's owner directory.
// Register in the destination's owner directory for non-self-escrows.
AccountID const dest = ctx_.tx[sfDestination];
if (dest != account_)
{
@@ -506,7 +627,8 @@ EscrowCreate::doApply()
(*slep)[sfIssuerNode] = *page;
}
// Deduct owner's balance
// Debit the sender. XRP is deducted directly from sfBalance; token amounts
// are handled by asset-type-specific helpers via std::visit.
if (isXRP(amount))
{
(*sle)[sfBalance] = (*sle)[sfBalance] - amount;
@@ -524,7 +646,6 @@ EscrowCreate::doApply()
}
}
// increment owner count
adjustOwnerCount(ctx_.view(), sle, 1, ctx_.journal);
ctx_.view().update(sle);
return tesSUCCESS;

View File

@@ -36,14 +36,40 @@
namespace xrpl {
// During an EscrowFinish, the transaction must specify both
// a condition and a fulfillment. We track whether that
// fulfillment matches and validates the condition.
/** HashRouter flag indicating the crypto-condition fulfillment is invalid.
*
* Set in `preflightSigValidated` (and re-set in `doApply` if the cache has
* expired) when `checkCondition` returns false. When this bit is present on
* a transaction's flags, `doApply` immediately returns
* `tecCRYPTOCONDITION_ERROR` without repeating the cryptographic work.
*/
constexpr HashRouterFlags kSF_CF_INVALID = HashRouterFlags::PRIVATE5;
/** HashRouter flag indicating the crypto-condition fulfillment is valid.
*
* Set in `preflightSigValidated` (and re-set in `doApply` if the cache has
* expired) when `checkCondition` returns true. `doApply` reads this bit to
* confirm that the fulfillment was already verified before proceeding with
* the remaining condition checks against the escrow SLE.
*/
constexpr HashRouterFlags kSF_CF_VALID = HashRouterFlags::PRIVATE6;
//------------------------------------------------------------------------------
/** Verify that a crypto-condition fulfillment satisfies a condition.
*
* Deserializes both the condition (`c`) and the fulfillment (`f`) using the
* `cryptoconditions` library and calls `validate`. Returns `false` if
* either field fails to deserialize. This is the single call site for all
* crypto-condition verification in the escrow finish pipeline; its result is
* cached in the `HashRouter` under `kSF_CF_VALID`/`kSF_CF_INVALID` to avoid
* repeating the work across multiple passes of the same transaction.
*
* @param f raw bytes of the PREIMAGE-SHA-256 fulfillment.
* @param c raw bytes of the condition originally stored in the escrow SLE.
* @return `true` if the fulfillment validates the condition; `false` if
* either field fails to deserialize or validation fails.
*/
static bool
checkCondition(Slice f, Slice c)
{
@@ -137,6 +163,23 @@ escrowFinishPreclaimHelper(
AccountID const& dest,
STAmount const& amount);
/** IOU specialization: read-only eligibility check for an escrow destination.
*
* Verifies that `dest` is permitted to receive the IOU held in escrow:
* - If the issuer equals the destination the check trivially passes (the
* issuer cannot be unauthorized or frozen against itself).
* - Otherwise `requireAuth` is called; if the issuer has `lsfRequireAuth`
* set and `dest` is not authorized on the trust line, the check fails.
* - Then `isDeepFrozen` is consulted; deep-freeze on the trust line blocks
* the finish regardless of authorization.
*
* @param ctx read-only preclaim context.
* @param dest the escrow destination account.
* @param amount the escrowed IOU amount (carries the issuer and currency).
* @return `tesSUCCESS` if the destination may receive the asset;
* `tecNO_AUTH` if `requireAuth` is set and the trust line is
* unauthorized; `tecFROZEN` if the trust line is deep-frozen.
*/
template <>
TER
escrowFinishPreclaimHelper<Issue>(
@@ -145,21 +188,37 @@ escrowFinishPreclaimHelper<Issue>(
STAmount const& amount)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the account, return tesSUCCESS
if (issuer == dest)
return tesSUCCESS;
// If the issuer has requireAuth set, check if the destination is authorized
if (auto const ter = requireAuth(ctx.view, amount.get<Issue>(), dest); !isTesSuccess(ter))
return ter;
// If the issuer has deep frozen the destination, return tecFROZEN
if (isDeepFrozen(ctx.view, dest, amount.get<Issue>().currency, amount.getIssuer()))
return tecFROZEN;
return tesSUCCESS;
}
/** MPT specialization: read-only eligibility check for an escrow destination.
*
* Verifies that `dest` is permitted to receive the MPToken held in escrow:
* - If the issuer equals the destination the check trivially passes.
* - The MPT issuance object must exist on the ledger; a missing issuance
* means the asset was destroyed after the escrow was created.
* - `requireAuth` is called with `AuthType::WeakAuth`, which requires an
* `MPToken` holder entry to exist but does not require the issuer to have
* explicitly authorized it (less strict than IOU `requireAuth`).
* - `isFrozen` checks for an MPT-level lock on the destination's holding.
*
* @param ctx read-only preclaim context.
* @param dest the escrow destination account.
* @param amount the escrowed MPT amount (carries the MPT issuance ID).
* @return `tesSUCCESS` if the destination may receive the asset;
* `tecOBJECT_NOT_FOUND` if the MPT issuance no longer exists;
* a `requireAuth` error code if `WeakAuth` fails;
* `tecLOCKED` if the destination's MPToken holding is frozen.
*/
template <>
TER
escrowFinishPreclaimHelper<MPTIssue>(
@@ -168,24 +227,19 @@ escrowFinishPreclaimHelper<MPTIssue>(
STAmount const& amount)
{
AccountID const& issuer = amount.getIssuer();
// If the issuer is the same as the dest, return tesSUCCESS
if (issuer == dest)
return tesSUCCESS;
// If the mpt does not exist, return tecOBJECT_NOT_FOUND
auto const issuanceKey = keylet::mptIssuance(amount.get<MPTIssue>().getMptID());
auto const sleIssuance = ctx.view.read(issuanceKey);
if (!sleIssuance)
return tecOBJECT_NOT_FOUND;
// If the issuer has requireAuth set, check if the destination is
// authorized
auto const& mptIssue = amount.get<MPTIssue>();
if (auto const ter = requireAuth(ctx.view, mptIssue, dest, AuthType::WeakAuth);
!isTesSuccess(ter))
return ter;
// If the issuer has frozen the destination, return tecLOCKED
if (isFrozen(ctx.view, dest, mptIssue))
return tecLOCKED;
@@ -239,8 +293,6 @@ EscrowFinish::doApply()
return tecNO_TARGET;
}
// If a cancel time is present, a finish operation should only succeed prior
// to that time.
auto const now = ctx_.view().header().parentCloseTime;
// Too soon: can't execute before the finish time
@@ -251,7 +303,6 @@ EscrowFinish::doApply()
if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter]))
return tecNO_PERMISSION;
// Check cryptocondition fulfillment
{
auto const id = ctx_.tx.getTransactionID();
auto flags = ctx_.registry.get().getHashRouter().getFlags(id);
@@ -282,12 +333,9 @@ EscrowFinish::doApply()
// LCOV_EXCL_STOP
}
// If the check failed, then simply return an error
// and don't look at anything else.
if (any(flags & kSF_CF_INVALID))
return tecCRYPTOCONDITION_ERROR;
// Check against condition in the ledger entry:
auto const cond = (*slep)[~sfCondition];
// If a condition wasn't specified during creation,
@@ -316,7 +364,6 @@ EscrowFinish::doApply()
AccountID const account = (*slep)[sfAccount];
// Remove escrow from owner directory
{
auto const page = (*slep)[sfOwnerNode];
if (!ctx_.view().dirRemove(keylet::ownerDir(account), page, k.key, true))
@@ -341,7 +388,6 @@ EscrowFinish::doApply()
}
STAmount const amount = slep->getFieldAmount(sfAmount);
// Transfer amount to destination
if (isXRP(amount))
{
(*sled)[sfBalance] = (*sled)[sfBalance] + amount;
@@ -389,12 +435,10 @@ EscrowFinish::doApply()
ctx_.view().update(sled);
// Adjust source owner count
auto const sle = ctx_.view().peek(keylet::account(account));
adjustOwnerCount(ctx_.view(), sle, -1, ctx_.journal);
ctx_.view().update(sle);
// Remove escrow from ledger
ctx_.view().erase(slep);
return tesSUCCESS;
}

View File

@@ -1,3 +1,17 @@
/** @file
* Implements `LoanBrokerCoverClawback`: the transaction that lets the
* original asset issuer forcibly reclaim cover funds from a loan broker's
* pseudo-account, subject to a minimum cover-to-debt ratio enforced via
* asymmetric rounding.
*
* The broker may be identified either explicitly via `sfLoanBrokerID` or
* implicitly by encoding the pseudo-account as the IOU issuer in `sfAmount`.
* XRP assets and MPTs cannot use the implicit path.
*
* @see LoanBrokerCoverDeposit structural inverse — adds cover, same
* `WaiveTransferFee::Yes` convention
* @see LoanBrokerCoverWithdraw voluntary cover removal by broker owner
*/
#include <xrpl/tx/transactors/lending/LoanBrokerCoverClawback.h>
#include <xrpl/basics/Expected.h>
@@ -83,6 +97,28 @@ LoanBrokerCoverClawback::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
/** Resolve the target broker's ledger ID from the transaction.
*
* Supports two identification modes:
* - **Explicit**: `sfLoanBrokerID` is present in the transaction.
* - **Implicit**: `sfAmount` is an IOU whose `issuer` field is the broker's
* pseudo-account; the function reads that account's SLE and extracts its
* `sfLoanBrokerID` field.
*
* The implicit path exists because callers may naturally express the target
* using the trust-line issuer convention (pseudo-account as issuer) without
* knowing the broker's object ID up front. `preflight` ensures the implicit
* path is only reached when `sfAmount` holds an IOU.
*
* @param view Read-only ledger view used to look up the pseudo-account SLE.
* @param tx The transaction being applied.
* @return The broker's `uint256` object ID on success, or:
* - `tecINTERNAL` if the transaction state is inconsistent with what
* `preflight` should have rejected (unreachable in practice).
* - `tecNO_ENTRY` if the IOU issuer account does not exist.
* - `tecOBJECT_NOT_FOUND` if the IOU issuer account exists but is not
* a loan-broker pseudo-account.
*/
Expected<uint256, TER>
determineBrokerID(ReadView const& view, STTx const& tx)
{
@@ -126,6 +162,29 @@ determineBrokerID(ReadView const& view, STTx const& tx)
// Or tecWRONG_ASSET?
}
/** Normalize the clawback asset to use the submitting account as issuer.
*
* IOU trust lines are bidirectional: the same line can be expressed with
* either endpoint as the `issuer` field. This function canonicalises the
* asset so that comparisons against the vault's `sfAsset` (which always
* uses the actual issuer account) work correctly regardless of which
* perspective the submitter used when constructing `sfAmount`.
*
* MPT assets are returned as-is because MPTs have no trust-line issuer
* ambiguity.
*
* @param view Read-only ledger view (currently unused;
* reserved for future validation).
* @param account The submitting account (the asset issuer).
* @param brokerPseudoAccountID The broker pseudo-account ID resolved by
* `determineBrokerID`.
* @param amount The `sfAmount` from the transaction, which may
* encode the asset from either the issuer's or pseudo-account's
* perspective.
* @return The canonical `Asset` with `account` as issuer on success, or
* `tecWRONG_ASSET` if the IOU issuer matches neither `account` nor
* `brokerPseudoAccountID`.
*/
Expected<Asset, TER>
determineAsset(
ReadView const& view,
@@ -156,6 +215,29 @@ determineAsset(
return Unexpected(tecWRONG_ASSET);
}
/** Compute the maximum amount the issuer may claw back while respecting the
* broker's minimum cover floor.
*
* The floor is `sfDebtTotal × sfCoverRateMinimum` (in tenth-basis-points),
* computed with ceiling rounding so the ledger never permits cover to drop
* below the contractual minimum. The remaining surplus
* (`sfCoverAvailable minRequiredCover`) is then rounded down. Asymmetric
* rounding is deliberate: ceiling on the required minimum, floor on the
* available surplus.
*
* When `amount` is absent or zero the function returns the full surplus
* (`maxClawAmount`). When `amount` is provided but exceeds the surplus it
* is silently capped — the submitter always receives at most what the floor
* allows, without needing to know the exact number in advance.
*
* @param sleBroker The broker SLE supplying `sfDebtTotal`,
* `sfCoverRateMinimum`, and `sfCoverAvailable`.
* @param vaultAsset Canonical vault asset used to type the returned
* `STAmount` (avoids pseudo-account issuer ambiguity).
* @param amount Requested claw amount, or absent/zero for "take all".
* @return The effective claw amount capped to `maxClawAmount`, or
* `tecINSUFFICIENT_FUNDS` if cover is already at or below the floor.
*/
Expected<STAmount, TER>
determineClawAmount(
SLE const& sleBroker,
@@ -163,11 +245,9 @@ determineClawAmount(
std::optional<STAmount> const& amount)
{
auto const maxClawAmount = [&]() {
// Always round the minimum required up
NumberRoundModeGuard const mg1(Number::RoundingMode::Upward);
auto const minRequiredCover =
tenthBipsOfValue(sleBroker[sfDebtTotal], TenthBips32(sleBroker[sfCoverRateMinimum]));
// The subtraction probably won't round, but round down if it does.
NumberRoundModeGuard const mg2(Number::RoundingMode::Downward);
return sleBroker[sfCoverAvailable] - minRequiredCover;
}();
@@ -185,22 +265,44 @@ determineClawAmount(
return STAmount{vaultAsset, magnitude};
}
/** Asset-type-specific permission check for the vault asset issuer.
*
* Dispatched from `preclaim` via `std::visit` on the vault asset variant.
* Each specialisation enforces the clawback-permission model appropriate to
* its asset type: `Issue` checks IOU account flags; `MPTIssue` checks the
* `MPTIssuance` SLE flags.
*
* @tparam T Asset type: `Issue` or `MPTIssue`.
* @param ctx Preclaim context with read-only ledger view.
* @param sleIssuer The issuer's `AccountRoot` SLE.
* @param clawAmount The effective claw amount (used by the MPT path to derive
* the issuance keylet).
* @return `tesSUCCESS` if the issuer holds the required permission flag, or
* `tecNO_PERMISSION` / `tecOBJECT_NOT_FOUND` / `tecINTERNAL` otherwise.
*/
template <ValidIssueType T>
static TER
preclaimHelper(PreclaimContext const& ctx, SLE const& sleIssuer, STAmount const& clawAmount);
/** IOU specialisation: enforces `lsfAllowTrustLineClawback` and absence of
* `lsfNoFreeze` on the issuer account, mirroring the standard XRPL IOU
* clawback permission model.
*/
template <>
TER
preclaimHelper<Issue>(PreclaimContext const& ctx, SLE const& sleIssuer, STAmount const& clawAmount)
{
// If AllowTrustLineClawback is not set or NoFreeze is set, return no
// permission
if (!(sleIssuer.isFlag(lsfAllowTrustLineClawback)) || (sleIssuer.isFlag(lsfNoFreeze)))
return tecNO_PERMISSION;
return tesSUCCESS;
}
/** MPT specialisation: looks up the `MPTIssuance` SLE and checks
* `lsfMPTCanClawback`. Also asserts (redundantly) that the issuance's
* `sfIssuer` matches the submitting account — a consistency guard that
* should be unreachable given upstream checks.
*/
template <>
TER
preclaimHelper<MPTIssue>(
@@ -261,8 +363,6 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}
// Only the issuer of the vault asset can claw it back from the broker's
// cover funds.
if (vaultAsset.getIssuer() != account)
{
JLOG(ctx.j.warn()) << "Account is not the issuer of the vault asset.";
@@ -291,9 +391,6 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
}
STAmount const& clawAmount = *findClawAmount;
// Explicitly check the balance of the trust line / MPT to make sure the
// balance is actually there. It should always match `sfCoverAvailable`, so
// if there isn't, this is an internal error.
if (accountHolds(
ctx.view,
brokerPseudoAccountID,
@@ -303,7 +400,6 @@ LoanBrokerCoverClawback::preclaim(PreclaimContext const& ctx)
ctx.j) < clawAmount)
return tecINTERNAL; // tecINSUFFICIENT_FUNDS; LCOV_EXCL_LINE
// Check if the vault asset issuer has the correct flags
auto const sleIssuer = ctx.view.read(keylet::account(vaultAsset.getIssuer()));
if (!sleIssuer)
{
@@ -349,13 +445,11 @@ LoanBrokerCoverClawback::doApply()
if (clawAmount.native())
return tecINTERNAL; // LCOV_EXCL_LINE
// Decrease the LoanBroker's CoverAvailable by Amount
sleBroker->at(sfCoverAvailable) -= clawAmount;
view().update(sleBroker);
associateAsset(*sleBroker, vaultAsset);
// Transfer assets from pseudo-account to depositor.
return accountSend(view(), brokerPseudoID, account, clawAmount, j_, WaiveTransferFee::Yes);
}

View File

@@ -1,3 +1,17 @@
/** @file
* Implementation of `LoanBrokerCoverDeposit`, the transactor that lets a
* `LoanBroker` owner inject collateral into the broker's pseudo-account.
*
* Cover is the loss-absorption buffer the lending protocol (XLS-66) requires
* each broker to maintain relative to its outstanding loan debt
* (`sfDebtTotal`). Depositing cover increases `sfCoverAvailable` and the
* pseudo-account's on-ledger asset balance atomically.
*
* The three symmetrical cover-management transactors are:
* - `LoanBrokerCoverDeposit` — increases cover (this file)
* - `LoanBrokerCoverWithdraw` — decreases cover (enforces minimum ratio)
* - `LoanBrokerCoverClawback` — issuer reclaims frozen cover assets
*/
#include <xrpl/tx/transactors/lending/LoanBrokerCoverDeposit.h>
#include <xrpl/basics/Log.h>
@@ -74,16 +88,12 @@ LoanBrokerCoverDeposit::preclaim(PreclaimContext const& ctx)
return tecWRONG_ASSET;
auto const pseudoAccountID = sleBroker->at(sfAccount);
// Cannot transfer a non-transferable Asset
if (auto const ret = canTransfer(ctx.view, vaultAsset, account, pseudoAccountID))
return ret;
// Cannot transfer a frozen Asset
if (auto const ret = checkFrozen(ctx.view, account, vaultAsset))
return ret;
// Pseudo-account cannot receive if asset is deep frozen
if (auto const ret = checkDeepFrozen(ctx.view, pseudoAccountID, vaultAsset))
return ret;
// Cannot transfer unauthorized asset
if (auto const ret = requireAuth(ctx.view, vaultAsset, account, AuthType::StrongAuth))
return ret;
@@ -120,11 +130,9 @@ LoanBrokerCoverDeposit::doApply()
auto const brokerPseudoID = broker->at(sfAccount);
// Transfer assets from depositor to pseudo-account.
if (auto ter = accountSend(view(), account_, brokerPseudoID, amount, j_, WaiveTransferFee::Yes))
return ter;
// Increase the LoanBroker's CoverAvailable by Amount
broker->at(sfCoverAvailable) += amount;
view().update(broker);

View File

@@ -1,3 +1,17 @@
/** @file
* Implementation of the `LoanBrokerCoverWithdraw` transactor (XLS-66).
*
* This transactor is the only sanctioned path by which a broker owner can
* reclaim idle cover capital from the broker's pseudo-account. It enforces
* the minimum cover ratio — `roundUp(tenthBipsOfValue(sfDebtTotal,
* sfCoverRateMinimum))` — before permitting any withdrawal, ensuring
* outstanding loans remain adequately backed. Asset movement is delegated
* to `doWithdraw` in `View.h`, which handles pseudo-account settlement
* logic common across vault-adjacent withdrawal operations.
*
* @see LoanBrokerCoverDeposit symmetric deposit path (increments cover)
* @see LoanBrokerCoverClawback forced cover reclaim by the vault asset issuer
*/
#include <xrpl/tx/transactors/lending/LoanBrokerCoverWithdraw.h>
#include <xrpl/basics/Log.h>
@@ -23,12 +37,25 @@
namespace xrpl {
/** Delegates amendment gating to `checkLendingProtocolDependencies`.
*
* Returns `false` (causing `invokePreflight` to return `temDISABLED`) if
* the lending protocol amendment or any of its dependencies is not yet
* active on the ledger.
*/
bool
LoanBrokerCoverWithdraw::checkExtraFeatures(PreflightContext const& ctx)
{
return checkLendingProtocolDependencies(ctx.rules, ctx.tx);
}
/** Stateless field validation — no ledger access.
*
* Rejects structural nonsense before any state is consulted: a zero broker
* ID cannot map to a valid SLE, a non-positive amount is never a meaningful
* withdrawal, and a present-but-zero `sfDestination` would silently route
* funds to a black-hole address.
*/
NotTEC
LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx)
{
@@ -53,6 +80,30 @@ LoanBrokerCoverWithdraw::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
/** Read-only ledger checks for a cover withdrawal.
*
* Layers of enforcement (in order):
* - Destination must not be a pseudo-account.
* - `LoanBroker` SLE must exist and be owned by the submitter.
* - The broker's vault must exist (missing vault → `tefBAD_LEDGER`; excluded
* from coverage because the broker cannot outlive its vault in a well-formed
* ledger).
* - `sfAmount` asset must match the vault's `sfAsset`.
* - Asset transferability: `canTransfer` from the broker pseudo-account to
* the destination.
* - Third-party destination: upgrades to `StrongAuth` and calls `canWithdraw`
* (checks account existence, deposit-auth, tag requirements, trust limits).
* Withdrawing to self requires only `WeakAuth`, so the owner need not
* pre-establish a trust relationship with their own account.
* - Freeze checks against the pseudo-account (source) and destination, unless
* the destination is the asset issuer.
* - Minimum cover: `roundUp(tenthBipsOfValue(sfDebtTotal,
* sfCoverRateMinimum))` must remain after the withdrawal. Upward rounding
* is enforced via `NumberRoundModeGuard` to prevent rounding dust from
* under-collateralizing outstanding loans.
* - `accountHolds` confirms the pseudo-account's actual balance covers the
* requested amount, guarding against any divergence from `sfCoverAvailable`.
*/
TER
LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
{
@@ -93,46 +144,41 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
if (amount.asset() != vaultAsset)
return tecWRONG_ASSET;
// The broker's pseudo-account is the source of funds.
// The broker's pseudo-account is the source of funds, not the submitter.
auto const pseudoAccountID = sleBroker->at(sfAccount);
// Cannot transfer a non-transferable Asset
if (auto const ret = canTransfer(ctx.view, vaultAsset, pseudoAccountID, dstAcct))
return ret;
// Withdrawal to a 3rd party destination account is essentially a transfer.
// Enforce all the usual asset transfer checks.
// Withdrawal to a 3rd party destination is functionally a transfer:
// enforce full consent checks and require StrongAuth (existing trust line
// or MPToken).
AuthType authType = AuthType::WeakAuth;
if (account != dstAcct)
{
if (auto const ret = canWithdraw(ctx.view, tx))
return ret;
// The destination account must have consented to receive the asset by
// creating a RippleState or MPToken
authType = AuthType::StrongAuth;
}
// Destination MPToken must exist (if asset is an MPT)
if (auto const ter = requireAuth(ctx.view, vaultAsset, dstAcct, authType))
return ter;
// Check for freezes, unless sending directly to the issuer
// Freeze checks are skipped when sending directly to the asset issuer.
if (dstAcct != vaultAsset.getIssuer())
{
// Cannot send a frozen Asset
if (auto const ret = checkFrozen(ctx.view, pseudoAccountID, vaultAsset))
return ret;
// Destination account cannot receive if asset is deep frozen
if (auto const ret = checkDeepFrozen(ctx.view, dstAcct, vaultAsset))
return ret;
}
auto const coverAvail = sleBroker->at(sfCoverAvailable);
// Cover Rate is in 1/10 bips units
auto const currentDebtTotal = sleBroker->at(sfDebtTotal);
auto const minimumCover = [&]() {
// Always round the minimum required up.
// Applies to `tenthBipsOfValue` as well as `roundToAsset`.
// Always round the minimum required up — applies to both
// `tenthBipsOfValue` and `roundToAsset` — so fractional dust cannot
// be used to erode the collateral buffer below the required ratio.
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
return roundToAsset(
vaultAsset,
@@ -156,6 +202,19 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
return tesSUCCESS;
}
/** Executes the cover withdrawal against the mutable ledger view.
*
* Decrements `sfCoverAvailable` on the `LoanBroker` SLE, then calls
* `associateAsset` to re-round all `STNumber` fields to asset precision
* (required after any numeric mutation on an SLE that holds asset-denominated
* values). The actual token movement from the broker pseudo-account to the
* destination is performed by `doWithdraw`.
*
* @note `associateAsset` must be the last call before `doWithdraw`; failing
* to call it produces silent precision corruption in the serialized SLE.
* @note The broker and vault peek/read failures return `tecINTERNAL` and are
* excluded from coverage — preclaim guarantees both objects exist.
*/
TER
LoanBrokerCoverWithdraw::doApply()
{
@@ -177,7 +236,6 @@ LoanBrokerCoverWithdraw::doApply()
auto const brokerPseudoID = *broker->at(sfAccount);
// Decrease the LoanBroker's CoverAvailable by Amount
broker->at(sfCoverAvailable) -= amount;
view().update(broker);
@@ -186,6 +244,7 @@ LoanBrokerCoverWithdraw::doApply()
return doWithdraw(view(), tx, account_, dstAcct, brokerPseudoID, preFeeBalance_, amount, j_);
}
/** No-op: no per-entry invariants are defined for this transaction type yet. */
void
LoanBrokerCoverWithdraw::visitInvariantEntry(
bool,
@@ -195,6 +254,10 @@ LoanBrokerCoverWithdraw::visitInvariantEntry(
// No transaction-specific invariants yet (future work).
}
/** No-op: no post-conditions are defined for this transaction type yet.
*
* @return `true` unconditionally.
*/
bool
LoanBrokerCoverWithdraw::finalizeInvariants(
STTx const&,

View File

@@ -1,3 +1,14 @@
/** @file
* Implements `LoanBrokerDelete`, the teardown transactor for a `LoanBroker`
* and its associated pseudo-account.
*
* Deletion is a strict multi-phase process: preclaim verifies that all loans
* are closed and any residual debt rounds to zero, then doApply removes both
* directory entries, refunds cover capital, clears the holding, and erases
* both the pseudo-account SLE and the broker SLE — decrementing the human
* owner's `sfOwnerCount` by exactly two to balance the two-unit increment
* performed by `LoanBrokerSet`.
*/
#include <xrpl/tx/transactors/lending/LoanBrokerDelete.h>
#include <xrpl/basics/Log.h>
@@ -95,8 +106,6 @@ LoanBrokerDelete::preclaim(PreclaimContext const& ctx)
}
auto const coverAvailable = STAmount{asset, sleBroker->at(sfCoverAvailable)};
// If there are assets in the cover, broker will receive them on deletion.
// So we need to check if the broker owner is deep frozen for that asset.
if (coverAvailable > beast::kZERO)
{
if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset))
@@ -116,7 +125,6 @@ LoanBrokerDelete::doApply()
auto const brokerID = tx[sfLoanBrokerID];
// Delete the loan broker
auto broker = view().peek(keylet::loanbroker(brokerID));
if (!broker)
return tefBAD_LEDGER; // LCOV_EXCL_LINE

View File

@@ -1,3 +1,9 @@
/** @file
* Implements `LoanBrokerSet`, the upsert transactor for `LoanBroker` ledger
* objects (XLS-66). A single transaction type handles both creation (when
* `sfLoanBrokerID` is absent) and configuration updates (when present).
* See `LoanBrokerSet.h` for the full interface and behavioral contract.
*/
#include <xrpl/tx/transactors/lending/LoanBrokerSet.h>
#include <xrpl/basics/Log.h>
@@ -51,8 +57,6 @@ LoanBrokerSet::preflight(PreflightContext const& ctx)
if (tx.isFieldPresent(sfLoanBrokerID))
{
// Fixed fields can not be specified if we're modifying an existing
// LoanBroker Object
if (tx.isFieldPresent(sfManagementFeeRate) || tx.isFieldPresent(sfCoverRateMinimum) ||
tx.isFieldPresent(sfCoverRateLiquidation))
return temINVALID;
@@ -70,7 +74,6 @@ LoanBrokerSet::preflight(PreflightContext const& ctx)
{
auto const minimumZero = tx[~sfCoverRateMinimum].value_or(0) == 0;
auto const liquidationZero = tx[~sfCoverRateLiquidation].value_or(0) == 0;
// Both must be zero or non-zero.
if (minimumZero != liquidationZero)
{
return temINVALID;
@@ -112,8 +115,6 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
if (auto const brokerID = tx[~sfLoanBrokerID])
{
// Updating an existing Broker
auto const sleBroker = ctx.view.read(keylet::loanbroker(*brokerID));
if (!sleBroker)
{
@@ -133,7 +134,6 @@ LoanBrokerSet::preclaim(PreclaimContext const& ctx)
if (auto const debtMax = tx[~sfDebtMaximum])
{
// Can't reduce the debt maximum below the current total debt
auto const currentDebtTotal = sleBroker->at(sfDebtTotal);
if (*debtMax != 0 && *debtMax < currentDebtTotal)
{
@@ -177,7 +177,6 @@ LoanBrokerSet::doApply()
if (auto const brokerID = tx[~sfLoanBrokerID])
{
// Modify an existing LoanBroker
auto broker = view.peek(keylet::loanbroker(*brokerID));
if (!broker)
{
@@ -205,7 +204,6 @@ LoanBrokerSet::doApply()
}
else
{
// Create a new LoanBroker pointing back to the given Vault
auto const vaultID = tx[sfVaultID];
auto const sleVault = view.read(keylet::vault(vaultID));
if (!sleVault)
@@ -252,7 +250,6 @@ LoanBrokerSet::doApply()
if (auto ter = addEmptyHolding(view, pseudoId, preFeeBalance_, sleVault->at(sfAsset), j_))
return ter;
// Initialize data fields:
broker->at(sfSequence) = sequence;
broker->at(sfVaultID) = vaultID;
broker->at(sfOwner) = account_;

View File

@@ -1,3 +1,20 @@
/** @file LoanDelete.cpp
* Implements the `LoanDelete` transactor for the XRP Ledger lending protocol
* (XLS-66).
*
* The teardown sequence requires two ordered steps:
* 1. All `Loan` objects for a broker are deleted via `LoanDelete` — each call
* decrements `sfOwnerCount` on the `LoanBroker` SLE.
* 2. Once `sfOwnerCount` reaches zero the `LoanBroker` itself may be removed
* via `LoanBrokerDelete`.
*
* A critical edge case handled here: accumulated sub-precision rounding dust
* in `sfDebtTotal` is forgiven when the last loan is deleted. No future
* payment path can reduce it further, and leaving a non-zero `sfDebtTotal`
* would permanently block `LoanBrokerDelete` (which requires that the debt
* round to zero). The `XRPL_ASSERT_PARTS` guard confirms the residual is
* already representationally zero before the assignment.
*/
#include <xrpl/tx/transactors/lending/LoanDelete.h>
#include <xrpl/basics/Log.h>
@@ -97,23 +114,23 @@ LoanDelete::doApply()
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultAsset = vaultSle->at(sfAsset);
// Remove LoanID from Directory of the LoanBroker pseudo-account.
if (!view.dirRemove(
keylet::ownerDir(brokerPseudoAccount), loanSle->at(sfLoanBrokerNode), loanID, false))
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Remove LoanID from Directory of the Borrower.
if (!view.dirRemove(keylet::ownerDir(borrower), loanSle->at(sfOwnerNode), loanID, false))
return tefBAD_LEDGER; // LCOV_EXCL_LINE
// Delete the Loan object
view.erase(loanSle);
// Decrement the LoanBroker's owner count.
// The broker's owner count is solely for the number of outstanding loans,
// and is distinct from the broker's pseudo-account's owner count
// The broker's sfOwnerCount tracks outstanding loans only — it is distinct
// from the broker's pseudo-account owner count, which governs XRP reserves.
// LoanBrokerDelete decrements the pseudo-account count by two; LoanDelete
// only touches this broker-level count.
adjustOwnerCount(view, brokerSle, -1, j_);
// If there are no loans left, then any remaining debt must be forgiven,
// because there is no other way to pay it back.
// If no loans remain, forgive any residual sfDebtTotal. Rounding dust
// accumulates over many payment cycles and cannot be recovered once there
// are no more loans to repay against. Leaving a non-zero value would
// permanently block LoanBrokerDelete, which requires debt to round to zero.
if (brokerSle->at(sfOwnerCount) == 0)
{
auto debtTotalProxy = brokerSle->at(sfDebtTotal);
@@ -130,10 +147,12 @@ LoanDelete::doApply()
debtTotalProxy = 0;
}
}
// Decrement the borrower's owner count
adjustOwnerCount(view, borrowerSle, -1, j_);
// These associations shouldn't do anything, but do them just to be safe
// associateAsset is a lending-transactor convention: STNumber / STTakesAsset
// fields carry asset-precision metadata that must remain consistent. Even on
// deletion paths where no write-back of these fields is expected, the
// defensive call ensures correctness if the convention ever changes.
associateAsset(*loanSle, vaultAsset);
associateAsset(*brokerSle, vaultAsset);
associateAsset(*vaultSle, vaultAsset);

View File

@@ -1,3 +1,12 @@
/** @file LoanManage.cpp
* Implements the `LoanManage` transactor (ttLOAN_MANAGE, XLS-66).
*
* Manages the credit-quality lifecycle of a loan — impairment, impairment
* reversal, and formal default — on behalf of the `LoanBroker` that issued
* the loan. Multi-object accounting across the `Loan`, `LoanBroker`, and
* `Vault` ledger entries is the core concern. Payment processing lives in
* `LoanPay`; loan creation lives in `LoanSet`.
*/
#include <xrpl/tx/transactors/lending/LoanManage.h>
#include <xrpl/basics/Log.h>
@@ -77,12 +86,9 @@ LoanManage::preclaim(PreclaimContext const& ctx)
JLOG(ctx.j.warn()) << "Loan does not exist.";
return tecNO_ENTRY;
}
// Impairment only allows certain transitions.
// 1. Once it's in default, it can't be changed.
// 2. It can get worse: unimpaired -> impaired -> default
// or unimpaired -> default
// 3. It can get better: impaired -> unimpaired
// 4. If it's in a state, it can't be put in that state again.
// State-machine transitions: normal→impaired, normal→default,
// impaired→normal, impaired→default. Default is terminal; re-impair and
// un-impair-of-normal are blocked.
if (loanSle->isFlag(lsfLoanDefault))
{
JLOG(ctx.j.warn()) << "Loan is in default. A defaulted loan can not be modified.";
@@ -129,20 +135,30 @@ LoanManage::preclaim(PreclaimContext const& ctx)
return tesSUCCESS;
}
/** Compute the amount the vault is owed by a loan (XLS-66 §3.2.3.2).
*
* The vault is owed principal plus its share of interest; management fees
* accrue entirely to the broker. `sfInterestOutstanding` is not stored
* directly, so it is derived:
*
* ```
* InterestOutstanding = TotalValueOutstanding
* - PrincipalOutstanding
* - ManagementFeeOutstanding
*
* owedToVault = PrincipalOutstanding + InterestOutstanding
* = TotalValueOutstanding - ManagementFeeOutstanding
* ```
*
* This identity drives both the impairment paper-loss and the default
* settlement amount.
*
* @param loanSle Read-only SLE for the Loan object.
* @return The amount the vault is owed, expressed as a `Number`.
*/
static Number
owedToVault(SLE::ref loanSle)
{
// Spec section 3.2.3.2, defines the default amount as
//
// DefaultAmount = (Loan.PrincipalOutstanding + Loan.InterestOutstanding)
//
// Loan.InterestOutstanding is not stored directly on ledger.
// It is computed as
//
// Loan.TotalValueOutstanding - Loan.PrincipalOutstanding -
// Loan.ManagementFeeOutstanding
//
// Add that to the original formula, and you get this:
return loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfManagementFeeOutstanding);
}
@@ -155,18 +171,15 @@ LoanManage::defaultLoan(
Asset const& vaultAsset,
beast::Journal j)
{
// Calculate the amount of the Default that First-Loss Capital covers:
std::int32_t const loanScale = loanSle->at(sfLoanScale);
auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal);
Number const totalDefaultAmount = owedToVault(loanSle);
// Apply the First-Loss Capital to the Default Amount
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
TenthBips32 const coverRateLiquidation{brokerSle->at(sfCoverRateLiquidation)};
auto const defaultCovered = [&]() {
// Always round the minimum required up.
// Always round upward — the broker must not under-cover the vault.
NumberRoundModeGuard const mg(Number::RoundingMode::Upward);
auto const minimumCover = tenthBipsOfValue(brokerDebtTotalProxy.value(), coverRateMinimum);
// Round the liquidation amount up, too
@@ -186,8 +199,6 @@ LoanManage::defaultLoan(
auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered;
// Update the Vault object:
// The vault may be at a different scale than the loan. Reduce rounding
// errors during the accounting by rounding some of the values to that
// scale.
@@ -209,8 +220,7 @@ LoanManage::defaultLoan(
auto const vaultDefaultRounded = roundToAsset(
vaultAsset, vaultDefaultAmount, vaultScale, Number::RoundingMode::Downward);
vaultTotalProxy -= vaultDefaultRounded;
// Increase the Asset Available of the Vault by liquidated First-Loss
// Capital and any unclaimed funds amount:
// Add back the first-loss capital recovered from the broker.
vaultAvailableProxy += defaultCovered;
if (*vaultAvailableProxy > *vaultTotalProxy && !vaultAsset.integral())
{
@@ -239,7 +249,7 @@ LoanManage::defaultLoan(
// LCOV_EXCL_STOP
}
// The loss has been realized
// If previously impaired, convert the recorded paper loss to realized.
if (loanSle->isFlag(lsfLoanImpaired))
{
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
@@ -256,12 +266,8 @@ LoanManage::defaultLoan(
view.update(vaultSle);
}
// Update the LoanBroker object:
{
// Decrease the Debt of the LoanBroker:
adjustImpreciseNumber(brokerDebtTotalProxy, -totalDefaultAmount, vaultAsset, vaultScale);
// Decrease the First-Loss Capital Cover Available:
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
if (coverAvailableProxy < defaultCovered)
{
@@ -274,20 +280,20 @@ LoanManage::defaultLoan(
view.update(brokerSle);
}
// Update the Loan object:
loanSle->setFlag(lsfLoanDefault);
loanSle->at(sfTotalValueOutstanding) = 0;
loanSle->at(sfPaymentRemaining) = 0;
loanSle->at(sfPrincipalOutstanding) = 0;
loanSle->at(sfManagementFeeOutstanding) = 0;
// Zero out the next due date. Since it's default, it'll be removed from
// the object.
// Zero the due date — a defaulted loan has no future payment schedule;
// a zero value causes the field to be removed from the serialised object.
loanSle->at(sfNextPaymentDueDate) = 0;
view.update(loanSle);
// Return funds from the LoanBroker pseudo-account to the
// Vault pseudo-account:
// Transfer the covered amount from the broker pseudo-account to the
// vault pseudo-account. WaiveTransferFee::Yes is required because neither
// account is a regular user account.
return accountSend(
view,
brokerSle->at(sfAccount),
@@ -308,11 +314,9 @@ LoanManage::impairLoan(
Number const lossUnrealized = owedToVault(loanSle);
// The vault may be at a different scale than the loan. Reduce rounding
// errors during the accounting by rounding some of the values to that
// scale.
// errors during the accounting by rounding values to the vault's scale.
auto const vaultScale = getAssetsTotalScale(vaultSle);
// Update the Vault object(set "paper loss")
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
adjustImpreciseNumber(vaultLossUnrealizedProxy, lossUnrealized, vaultAsset, vaultScale);
if (vaultLossUnrealizedProxy > vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable))
@@ -325,13 +329,13 @@ LoanManage::impairLoan(
}
view.update(vaultSle);
// Update the Loan object
loanSle->setFlag(lsfLoanImpaired);
auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate);
if (!hasExpired(view, loanNextDueProxy))
{
// loan payment is not yet late -
// move the next payment due date to now
// Payment is not yet overdue — accelerate the due date to now so the
// grace period clock starts immediately rather than from the original
// scheduled date.
loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
}
view.update(loanSle);
@@ -348,11 +352,9 @@ LoanManage::unimpairLoan(
beast::Journal j)
{
// The vault may be at a different scale than the loan. Reduce rounding
// errors during the accounting by rounding some of the values to that
// scale.
// errors during the accounting by rounding values to the vault's scale.
auto const vaultScale = getAssetsTotalScale(vaultSle);
// Update the Vault object(clear "paper loss")
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
Number const lossReversed = owedToVault(loanSle);
if (vaultLossUnrealizedProxy < lossReversed)
@@ -362,24 +364,24 @@ LoanManage::unimpairLoan(
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
// Reverse the "paper loss"
adjustImpreciseNumber(vaultLossUnrealizedProxy, -lossReversed, vaultAsset, vaultScale);
view.update(vaultSle);
// Update the Loan object
loanSle->clearFlag(lsfLoanImpaired);
auto const paymentInterval = loanSle->at(sfPaymentInterval);
auto const normalPaymentDueDate =
std::max(loanSle->at(sfPreviousPaymentDueDate), loanSle->at(sfStartDate)) + paymentInterval;
if (!hasExpired(view, normalPaymentDueDate))
{
// loan was unimpaired within the payment interval
// Original due date is still in the future — restore the amortization
// schedule unchanged so the borrower loses no payment window.
loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate;
}
else
{
// loan was unimpaired after the original payment due date
// Original due date has passed — give the borrower a fresh interval
// from now rather than leaving the loan immediately overdue.
loanSle->at(sfNextPaymentDueDate) =
view.parentCloseTime().time_since_epoch().count() + paymentInterval;
}
@@ -410,15 +412,14 @@ LoanManage::doApply()
auto const vaultAsset = vaultSle->at(sfAsset);
auto const result = [&]() -> TER {
// Valid flag combinations are checked in preflight. No flags is valid -
// just a noop.
// Mutual exclusivity of flags is enforced in preflight. No flags is a
// valid no-op form (e.g., used to trigger associateAsset post-amendment).
if (tx.isFlag(tfLoanDefault))
return defaultLoan(view, loanSle, brokerSle, vaultSle, vaultAsset, j_);
if (tx.isFlag(tfLoanImpair))
return impairLoan(view, loanSle, vaultSle, vaultAsset, j_);
if (tx.isFlag(tfLoanUnimpair))
return unimpairLoan(view, loanSle, vaultSle, vaultAsset, j_);
// NoOp, as described above.
return tesSUCCESS;
}();

View File

@@ -1,3 +1,17 @@
/** @file
* Implementation of the `LoanPay` transactor for the XRPL Lending Protocol
* (XLS-66).
*
* Covers the full transaction pipeline — `checkExtraFeatures`, `getFlagsMask`,
* `preflight`, `calculateBaseFee`, `preclaim`, and `doApply` — for the
* `ttLOAN_PAY` transaction type. `doApply` is the algorithmic core: it
* unwinds any prior impairment, runs the amortization arithmetic via
* `loanMakePayment`, routes the broker service fee to either the broker
* owner or the first-loss cover pool, and atomically transfers funds from
* the borrower to the vault pseudo-account and the broker payee.
*
* See `LoanPay.h` for the class contract and per-method behavioral docs.
*/
#include <xrpl/tx/transactors/lending/LoanPay.h>
#include <xrpl/basics/Expected.h>
@@ -70,6 +84,26 @@ LoanPay::preflight(PreflightContext const& ctx)
return tesSUCCESS;
}
/** Fee scaling for multi-installment loan payments.
*
* Late and full payments always return a single base fee — they perform a
* bounded, fixed amount of work regardless of `sfAmount`.
*
* For regular and overpayment types the method reads the loan → loanbroker →
* vault hierarchy to derive `regularPayment = roundedPeriodicPayment +
* serviceFee`, then estimates `numPayments = amount / regularPayment`. One
* base-fee unit is charged per `kLOAN_PAYMENTS_PER_FEE_INCREMENT` estimated
* payments (minimum 1, rounded up). When `fixSecurity3_1_3` is active the
* result is capped at `kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION /
* kLOAN_PAYMENTS_PER_FEE_INCREMENT` fee increments — matching the hard
* processing cap that `loanMakePayment` enforces at apply time, so the fee
* is never charged for work that cannot happen.
*
* Overpayments use upward rounding for the payment estimate (conservative
* billing); regular payments use downward rounding. If any ledger object
* in the chain is missing or the asset does not match, the method falls back
* to `normalCost` and defers the error to `preclaim`.
*/
XRPAmount
LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
{
@@ -277,6 +311,59 @@ LoanPay::preclaim(PreclaimContext const& ctx)
return tesSUCCESS;
}
/** Atomic loan repayment: state mutation, accounting, and fund transfer.
*
* Execution proceeds in five sequential phases:
*
* **1. Impairment reversal.** If `lsfLoanImpaired` is set,
* `LoanManage::unimpairLoan` is called first. It reverses the paper loss
* recorded in the vault's `sfLossUnrealized` and recalculates
* `sfNextPaymentDueDate`. This must precede `loanMakePayment` because the
* payment classification (late vs. on-time) depends on the corrected due date.
*
* **2. Payment computation.** `loanMakePayment` runs the XLS-66 amortization
* arithmetic and modifies `loanSle` in-place, returning a `LoanPaymentParts`
* breakdown: `principalPaid`, `interestPaid`, `feePaid`, and `valueChange`.
* `valueChange` is negative for overpayments (extra principal reduction) and
* positive for late/full payments (penalty interest accrued).
*
* **3. Broker fee routing.** The service fee is sent to the broker owner's
* account when cover available ≥ `tenthBipsOfValue(debtTotal, coverRateMinimum)`
* (rounded up) AND the owner is neither deep-frozen nor unauthorized. The
* upward rounding is conservative: it ensures `sfCoverAvailable` never dips
* below the theoretical minimum, protecting the broker's solvency. If the
* owner cannot receive funds, the fee accumulates in the broker pseudo-account
* (first-loss cover pool). Only when both the owner and the pseudo-account are
* deep-frozen does the transaction fail.
*
* **4. Scale-reconciled accounting.** The vault and loan may operate at
* different numeric scales. `totalPaidToVaultRounded` is rounded down to the
* vault scale (preventing over-crediting). The broker's `sfDebtTotal` is
* adjusted via `adjustImpreciseNumber`, which re-rounds to vault scale and
* floors at zero so accumulated rounding drift across multiple loans never
* produces a negative debt balance. `sfAssetsTotal` is adjusted by
* `valueChange`, while `sfAssetsAvailable` is adjusted by
* `totalPaidToVaultRounded`; the invariant `available ≤ total` is asserted
* (and guarded with `tecINTERNAL` as a last-resort failsafe).
*
* **5. Atomic fund transfer.** A single `accountSendMulti` call moves funds:
* borrower → vault pseudo-account (`totalPaidToVaultRounded`) and borrower →
* broker payee (`totalPaidToBroker`). Transfer fees are waived
* (`WaiveTransferFee::Yes`) because the amortization schedule dictates exact
* amounts. If the broker is also the transaction submitter, `addEmptyHolding`
* recreates a token holding the broker may have previously deleted.
*
* @note In debug builds two `#if !NDEBUG` blocks verify fund conservation and
* that `sfAssetsAvailable` tracks the vault pseudo-account's actual balance
* before and after the transfer. These are too expensive for release builds;
* `XRPL_ASSERT_PARTS` macros serve as lightweight release-build postcondition
* guards.
*
* @return `tesSUCCESS` on success; `tefBAD_LEDGER` if any required SLE is
* missing (implies ledger corruption); a `tec` code from `loanMakePayment`
* or `LoanManage::unimpairLoan` on computation failure; `tecINTERNAL` if
* the `assetsAvailable ≤ assetsTotal` invariant is violated post-accounting.
*/
TER
LoanPay::doApply()
{

View File

@@ -1,3 +1,12 @@
/** @file
* Implements the `LoanSet` transactor for the XLS-66 on-ledger lending protocol.
*
* `LoanSet` is the entry point for creating a new loan: it disburses principal
* from a vault's pool of assets to a borrower, records the full amortization
* schedule as a `Loan` ledger entry, and updates the `LoanBroker` and `Vault`
* SLEs accordingly. Subsequent payments are handled by `LoanPay`; teardown by
* `LoanDelete`. All financial math is delegated to `LendingHelpers.cpp`.
*/
#include <xrpl/tx/transactors/lending/LoanSet.h>
#include <xrpl/basics/Log.h>
@@ -92,7 +101,6 @@ LoanSet::preflight(PreflightContext const& ctx)
if (!validNumericMinimum(tx[~*field]))
return temINVALID;
}
// Principal Requested is required
auto const p = tx[sfPrincipalRequested];
if (p <= 0)
return temINVALID;
@@ -207,6 +215,16 @@ LoanSet::getValueFields()
return kVALUE_FIELDS;
}
/** Return the current ledger close time as a raw 32-bit second count.
*
* Used both as `sfStartDate` on the newly-created `Loan` SLE and as the base
* value for the schedule-overflow arithmetic in `preclaim`. The result is
* directly comparable to `sfNextPaymentDueDate`, which is also stored as
* `std::uint32_t` seconds since the XRPL epoch.
*
* @param view Read-only ledger view from which the close time is taken.
* @return Close time in seconds since the XRPL epoch.
*/
static std::uint32_t
getStartDate(ReadView const& view)
{
@@ -568,11 +586,9 @@ LoanSet::doApply()
WaiveTransferFee::Yes))
return ter;
// Get shortcuts to the loan property values
auto const startDate = getStartDate(view);
auto loanSequenceProxy = brokerSle->at(sfLoanSequence);
// Create the loan
auto loan = std::make_shared<SLE>(keylet::loan(brokerID, *loanSequenceProxy));
// Prevent copy/paste errors
@@ -582,14 +598,12 @@ LoanSet::doApply()
loan->at(field) = tx[field].value_or(defValue);
};
// Set required and fixed tx fields
loan->at(sfLoanScale) = properties.loanScale;
loan->at(sfStartDate) = startDate;
loan->at(sfPaymentInterval) = paymentInterval;
loan->at(sfLoanSequence) = *loanSequenceProxy;
loan->at(sfLoanBrokerID) = brokerID;
loan->at(sfBorrower) = borrower;
// Set all other transaction fields directly from the transaction
if (tx.isFlag(tfLoanOverpayment))
loan->setFlag(lsfLoanOverpayment);
setLoanField(~sfLoanOriginationFee);
@@ -602,7 +616,6 @@ LoanSet::doApply()
setLoanField(~sfCloseInterestRate);
setLoanField(~sfOverpaymentInterestRate);
setLoanField(~sfGracePeriod, kDEFAULT_GRACE_PERIOD);
// Set dynamic / computed fields to their initial values
loan->at(sfPrincipalOutstanding) = principalRequested;
loan->at(sfPeriodicPayment) = properties.periodicPayment;
loan->at(sfTotalValueOutstanding) = properties.loanState.valueOutstanding;
@@ -612,7 +625,6 @@ LoanSet::doApply()
loan->at(sfPaymentRemaining) = paymentTotal;
view.insert(loan);
// Update the balances in the vault
vaultAvailableProxy -= principalRequested;
vaultTotalProxy += state.interestDue;
XRPL_ASSERT_PARTS(
@@ -621,7 +633,6 @@ LoanSet::doApply()
"assets available must not be greater than assets outstanding");
view.update(vaultSle);
// Update the balances in the loan broker
adjustImpreciseNumber(brokerSle->at(sfDebtTotal), newDebtDelta, vaultAsset, vaultScale);
// The broker's owner count is solely for the number of outstanding loans,
// and is distinct from the broker's pseudo-account's owner count
@@ -633,10 +644,8 @@ LoanSet::doApply()
return tecMAX_SEQUENCE_REACHED;
view.update(brokerSle);
// Put the loan into the pseudo-account's directory
if (auto const ter = dirLink(view, brokerPseudo, loan, sfLoanBrokerNode))
return ter;
// Borrower is the owner of the loan
if (auto const ter = dirLink(view, borrower, loan, sfOwnerNode))
return ter;

View File

@@ -1,3 +1,11 @@
/** @file
* Implementation of the `NFTokenAcceptOffer` transactor.
*
* This is the most financially complex NFT transaction: up to four parties
* (buyer, seller, royalty issuer, broker) may receive funds in a single
* atomic apply. See the class and method documentation in
* `NFTokenAcceptOffer.h` for the full behavioral contract.
*/
#include <xrpl/tx/transactors/nft/NFTokenAcceptOffer.h>
#include <xrpl/basics/Log.h>
@@ -33,12 +41,9 @@ NFTokenAcceptOffer::preflight(PreflightContext const& ctx)
auto const bo = ctx.tx[~sfNFTokenBuyOffer];
auto const so = ctx.tx[~sfNFTokenSellOffer];
// At least one of these MUST be specified
if (!bo && !so)
return temMALFORMED;
// The `BrokerFee` field must not be present in direct mode but may be
// present and greater than zero in brokered mode.
if (auto const bf = ctx.tx[~sfNFTokenBrokerFee])
{
if (!bo || !so)
@@ -68,12 +73,12 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
if (hasExpired(ctx.view, (*offerSLE)[~sfExpiration]))
{
// Before fixSecurity3_1_3 amendment, expired offers caused tecEXPIRED in preclaim,
// leaving them on ledger forever. After the amendment, we allow expired offers to
// reach doApply() where they get deleted and tecEXPIRED is returned.
// Before fixSecurity3_1_3, returning tecEXPIRED here left the
// expired offer stranded on the ledger (doApply was never reached).
// After the amendment, we pass the expired SLE through so doApply
// can delete it before returning tecEXPIRED.
if (!ctx.view.rules().enabled(fixSecurity3_1_3))
return {nullptr, tecEXPIRED};
// Amendment enabled: return the expired offer to be handled in doApply.
}
if ((*offerSLE)[sfAmount].negative())
@@ -93,43 +98,31 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
if (bo && so)
{
// Brokered mode:
// The two offers being brokered must be for the same token:
if ((*bo)[sfNFTokenID] != (*so)[sfNFTokenID])
return tecNFTOKEN_BUY_SELL_MISMATCH;
// The two offers being brokered must be for the same asset:
if ((*bo)[sfAmount].asset() != (*so)[sfAmount].asset())
return tecNFTOKEN_BUY_SELL_MISMATCH;
// The two offers may not form a loop. A broker may not sell the
// token to the current owner of the token.
// Reject self-trades: a broker may not facilitate a sale from an
// account to itself.
if (((*bo)[sfOwner] == (*so)[sfOwner]))
return tecCANT_ACCEPT_OWN_NFTOKEN_OFFER;
// Ensure that the buyer is willing to pay at least as much as the
// seller is requesting:
if ((*so)[sfAmount] > (*bo)[sfAmount])
return tecINSUFFICIENT_PAYMENT;
// The destination must be whoever is submitting the tx if the buyer
// specified it
// In brokered mode the submitter is the broker; both offers'
// Destination (if set) must name the broker.
if (auto const dest = bo->at(~sfDestination); dest && *dest != ctx.tx[sfAccount])
{
return tecNO_PERMISSION;
}
// The destination must be whoever is submitting the tx if the seller
// specified it
if (auto const dest = so->at(~sfDestination); dest && *dest != ctx.tx[sfAccount])
{
return tecNO_PERMISSION;
}
// The broker can specify an amount that represents their cut; if they
// have, ensure that the seller will get at least as much as they want
// to get *after* this fee is accounted for (but before the issuer's
// cut, if any).
// The broker fee is deducted from the buyer's amount first; what
// remains must still satisfy the seller's ask (issuer royalty is
// computed on the post-broker remainder in doApply).
if (auto const brokerFee = ctx.tx[~sfNFTokenBrokerFee])
{
if (brokerFee->asset() != (*bo)[sfAmount].asset())
@@ -162,38 +155,31 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
if (((*bo)[sfFlags] & lsfSellNFToken) == lsfSellNFToken)
return tecNFTOKEN_OFFER_TYPE_MISMATCH;
// An account can't accept an offer it placed:
if ((*bo)[sfOwner] == ctx.tx[sfAccount])
return tecCANT_ACCEPT_OWN_NFTOKEN_OFFER;
// If not in bridged mode, the account must own the token:
// In direct mode the submitter must own the token they are selling.
if (!so && !nft::findToken(ctx.view, ctx.tx[sfAccount], (*bo)[sfNFTokenID]))
return tecNO_PERMISSION;
// If not in bridged mode...
if (!so)
{
// If the offer has a Destination field, the acceptor must be the
// Destination.
if (auto const dest = bo->at(~sfDestination);
dest.has_value() && *dest != ctx.tx[sfAccount])
return tecNO_PERMISSION;
}
// The account offering to buy must have funds:
//
// After this amendment, we allow an IOU issuer to buy an NFT with their
// own currency
// An IOU issuer may buy an NFT with their own currency; accountFunds
// handles this by treating the issuer's own IOU as zero-cost.
auto const needed = bo->at(sfAmount);
if (accountFunds(ctx.view, (*bo)[sfOwner], needed, FreezeHandling::ZeroIfFrozen, ctx.j) <
needed)
return tecINSUFFICIENT_FUNDS;
// Check that the account accepting the buy offer (he's selling the NFT)
// is allowed to receive IOUs. Also check that this offer's creator is
// authorized. But we need to exclude the case when the transaction is
// created by the broker.
// Trust-line checks for the IOU payment: buyer must be authorized and
// the seller (direct mode submitter) must be authorized and not deep-
// frozen. Skipped in brokered mode the broker is not the IOU recipient.
if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2) && !needed.native())
{
auto res = nft::checkTrustlineAuthorized(
@@ -221,47 +207,31 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
if (((*so)[sfFlags] & lsfSellNFToken) != lsfSellNFToken)
return tecNFTOKEN_OFFER_TYPE_MISMATCH;
// An account can't accept an offer it placed:
if ((*so)[sfOwner] == ctx.tx[sfAccount])
return tecCANT_ACCEPT_OWN_NFTOKEN_OFFER;
// The seller must own the token.
if (!nft::findToken(ctx.view, (*so)[sfOwner], (*so)[sfNFTokenID]))
return tecNO_PERMISSION;
// If not in bridged mode...
if (!bo)
{
// If the offer has a Destination field, the acceptor must be the
// Destination.
if (auto const dest = so->at(~sfDestination);
dest.has_value() && *dest != ctx.tx[sfAccount])
return tecNO_PERMISSION;
}
// The account offering to buy must have funds:
auto const needed = so->at(sfAmount);
if (!bo)
{
// After this amendment, we allow buyers to buy with their own
// issued currency.
//
// In the case of brokered mode, this check is essentially
// redundant, since we have already confirmed that buy offer is >
// than the sell offer, and that the buyer can cover the buy
// offer.
//
// We also _must not_ check the tx submitter in brokered
// mode, because then we are confirming that the broker can
// cover what the buyer will pay, which doesn't make sense, causes
// an unnecessary tec, and is also resolved with this amendment.
// In brokered mode this check is skipped: the buyer's solvency is
// already confirmed via the buy-offer check, and checking the
// broker's balance here would produce a spurious tecINSUFFICIENT_FUNDS.
if (accountFunds(
ctx.view, ctx.tx[sfAccount], needed, FreezeHandling::ZeroIfFrozen, ctx.j) <
needed)
return tecINSUFFICIENT_FUNDS;
}
// Make sure that we are allowed to hold what the taker will pay us.
if (!needed.native())
{
if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2))
@@ -287,8 +257,8 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
}
}
// Additional checks are required in case a minter set a transfer fee for
// this nftoken
// When a transfer fee is set on the token, the issuer is an additional
// IOU payment recipient and must pass trust-line hygiene checks.
auto const& offer = bo ? bo : so;
if (!offer)
{
@@ -302,16 +272,16 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
if (nft::getTransferFee(tokenID) != 0 && !amount.native())
{
// Fix a bug where the transfer of an NFToken with a transfer fee could
// give the NFToken issuer an undesired trust line.
// Issuer doesn't need a trust line to accept their own currency.
// fixEnforceNFTokenTrustline: if the token lacks kFLAG_CREATE_TRUST_LINES
// and no trust line already exists between the issuer and the IOU issuer,
// the transfer would silently create an unintended trust line for the royalty
// payment. Reject unless the NFT issuer is also the IOU issuer (self-payment).
if (ctx.view.rules().enabled(fixEnforceNFTokenTrustline) &&
(nft::getFlags(tokenID) & nft::kFLAG_CREATE_TRUST_LINES) == 0 &&
nftMinter != amount.getIssuer() &&
!ctx.view.read(keylet::line(nftMinter, amount.get<Issue>())))
return tecNO_LINE;
// Check that the issuer is allowed to receive IOUs.
if (ctx.view.rules().enabled(fixEnforceNFTokenTrustlineV2))
{
auto res = nft::checkTrustlineAuthorized(
@@ -332,17 +302,15 @@ NFTokenAcceptOffer::preclaim(PreclaimContext const& ctx)
TER
NFTokenAcceptOffer::pay(AccountID const& from, AccountID const& to, STAmount const& amount)
{
// This should never happen, but it's easy and quick to check.
if (amount < beast::kZERO)
return tecINTERNAL; // LCOV_EXCL_LINE
auto const result = accountSend(view(), from, to, amount, j_);
// If any payment causes a non-IOU-issuer to have a negative balance,
// or an IOU-issuer to have a positive balance in their own currency,
// we know that something went wrong. This was originally found in the
// context of IOU transfer fees. Since there are several payouts in this tx,
// just confirm that the end state is OK.
// IOU transfer fees can leave the sender with a small negative or the
// receiver (when they are the IOU issuer) with a positive balance in their
// own currency. Check both parties after every payment to catch these
// edge cases before they corrupt the ledger.
if (!isTesSuccess(result))
return result;
if (accountFunds(view(), from, amount, FreezeHandling::ZeroIfFrozen, j_).signum() < 0)
@@ -375,19 +343,11 @@ NFTokenAcceptOffer::transferNFToken(
auto const insertRet = nft::insertToken(view(), buyer, std::move(tokenAndPage->token));
// if fixNFTokenReserve is enabled, check if the buyer has sufficient
// reserve to own a new object, if their OwnerCount changed.
//
// There was an issue where the buyer accepts a sell offer, the ledger
// didn't check if the buyer has enough reserve, meaning that buyer can get
// NFTs free of reserve.
if (view().rules().enabled(fixNFTokenReserve))
{
// To check if there is sufficient reserve, we cannot use preFeeBalance_
// because NFT is sold for a price. So we must use the balance after
// the deduction of the potential offer price. A small caveat here is
// that the balance has already deducted the transaction fee, meaning
// that the reserve requirement is a few drops higher.
// Use the buyer's current post-purchase balance, not preFeeBalance_.
// The tx fee has already been deducted, so the effective reserve bar
// is a few drops higher than the owner-count formula alone would imply.
auto const buyerBalance = sleBuyer->getFieldAmount(sfBalance);
auto const buyerOwnerCountAfter = sleBuyer->getFieldU32(sfOwnerCount);
@@ -414,11 +374,12 @@ NFTokenAcceptOffer::acceptOffer(std::shared_ptr<SLE> const& offer)
if (auto amount = offer->getFieldAmount(sfAmount); amount != beast::kZERO)
{
// Calculate the issuer's cut from this sale, if any:
if (auto const fee = nft::getTransferFee(nftokenID); fee != 0)
{
auto const cut = multiply(amount, nft::transferFeeAsRate(fee));
// Skip the royalty when either party is the issuer: paying the
// issuer their own cut would be a no-op or nonsensical self-transfer.
if (auto const issuer = nft::getIssuer(nftokenID);
cut != beast::kZERO && seller != issuer && buyer != issuer)
{
@@ -428,12 +389,10 @@ NFTokenAcceptOffer::acceptOffer(std::shared_ptr<SLE> const& offer)
}
}
// Send the remaining funds to the seller of the NFT
if (auto const r = pay(buyer, seller, amount); !isTesSuccess(r))
return r;
}
// Now transfer the NFT:
return transferNFToken(buyer, seller, nftokenID);
}
@@ -450,8 +409,8 @@ NFTokenAcceptOffer::doApply()
auto bo = loadToken(ctx_.tx[~sfNFTokenBuyOffer]);
auto so = loadToken(ctx_.tx[~sfNFTokenSellOffer]);
// With fixSecurity3_1_3 amendment, check for expired offers and delete them, returning
// tecEXPIRED. This ensures expired offers are properly cleaned up from the ledger.
// Under fixSecurity3_1_3, expired offers are passed through preclaim so we
// can delete them here, preventing them from becoming permanently stranded.
if (view().rules().enabled(fixSecurity3_1_3))
{
bool foundExpired = false;
@@ -501,7 +460,6 @@ NFTokenAcceptOffer::doApply()
// LCOV_EXCL_STOP
}
// Bridging two different offers
if (bo && so)
{
AccountID const buyer = (*bo)[sfOwner];
@@ -509,23 +467,15 @@ NFTokenAcceptOffer::doApply()
auto const nftokenID = (*so)[sfNFTokenID];
// The amount is what the buyer of the NFT pays:
STAmount amount = (*bo)[sfAmount];
// Three different folks may be paid. The order of operations is
// important.
// Payment ordering in brokered mode is strict and critical to correctness:
// 1. Broker receives their fee from the buyer's full amount.
// 2. Issuer royalty is computed on what remains after the broker cut.
// 3. Seller receives the final remainder.
//
// o The broker is paid the cut they requested.
// o The issuer's cut is calculated from what remains after the
// broker is paid. The issuer can take up to 50% of the remainder.
// o Finally, the seller gets whatever is left.
//
// It is important that the issuer's cut be calculated after the
// broker's portion is already removed. Calculating the issuer's
// cut before the broker's cut is removed can result in more money
// being paid out than the seller authorized. That would be bad!
// Send the broker the amount they requested.
// Computing the issuer's royalty before removing the broker fee would
// allow total disbursements to exceed what the buyer authorised.
if (auto const cut = ctx_.tx[~sfNFTokenBrokerFee]; cut && cut.value() != beast::kZERO)
{
if (auto const r = pay(buyer, account_, cut.value()); !isTesSuccess(r))
@@ -534,7 +484,6 @@ NFTokenAcceptOffer::doApply()
amount -= cut.value();
}
// Calculate the issuer's cut, if any.
if (auto const fee = nft::getTransferFee(nftokenID); amount != beast::kZERO && fee != 0)
{
auto cut = multiply(amount, nft::transferFeeAsRate(fee));
@@ -548,14 +497,12 @@ NFTokenAcceptOffer::doApply()
}
}
// And send whatever remains to the seller.
if (amount > beast::kZERO)
{
if (auto const r = pay(buyer, seller, amount); !isTesSuccess(r))
return r;
}
// Now transfer the NFT:
return transferNFToken(buyer, seller, nftokenID);
}

View File

@@ -1,3 +1,18 @@
/** @file
* Implementation of the `NFTokenBurn` transactor.
*
* Permanently destroys an NFToken by removing it from the owner's
* NFTokenPage, incrementing the issuer's `sfBurnedNFTokens` counter, and
* deleting associated buy/sell offers up to `kMAX_DELETABLE_TOKEN_OFFER_ENTRIES`.
*
* Permission model (enforced in `preclaim`):
* - The **token owner** may always burn their own token.
* - A **non-owner** requires the token's `nft::kFLAG_BURNABLE` bit to be set
* and must be either the issuer or the issuer's `sfNFTokenMinter` delegate.
*
* The issuer is read directly from the packed 256-bit `sfNFTokenID` via
* `nft::getIssuer()` — no secondary ledger lookup is needed to resolve it.
*/
#include <xrpl/tx/transactors/nft/NFTokenBurn.h>
#include <xrpl/beast/utility/Journal.h>
@@ -36,8 +51,6 @@ NFTokenBurn::preclaim(PreclaimContext const& ctx)
if (!nft::findToken(ctx.view, owner, ctx.tx[sfNFTokenID]))
return tecNO_ENTRY;
// The owner of a token can always burn it, but the issuer can only
// do so if the token is marked as burnable.
if (auto const account = ctx.tx[sfAccount]; owner != account)
{
if ((nft::getFlags(ctx.tx[sfNFTokenID]) & nft::kFLAG_BURNABLE) == 0)
@@ -59,7 +72,6 @@ NFTokenBurn::preclaim(PreclaimContext const& ctx)
TER
NFTokenBurn::doApply()
{
// Remove the token, effectively burning it:
auto const ret = nft::removeToken(
view(),
ctx_.tx.isFieldPresent(sfOwner) ? ctx_.tx.getAccountID(sfOwner)
@@ -76,10 +88,9 @@ NFTokenBurn::doApply()
view().update(issuer);
}
// Delete up to 500 offers in total.
// Because the number of sell offers is likely to be less than
// the number of buy offers, we prioritize the deletion of sell
// offers in order to clean up sell offer directory
// Sell offers are processed first: their directory is typically smaller,
// so clearing them first maximises the chance of a complete cleanup
// within the single-transaction 500-offer budget.
std::size_t const deletedSellOffers = nft::removeTokenOffersWithLimit(
view(), keylet::nftSells(ctx_.tx[sfNFTokenID]), kMAX_DELETABLE_TOKEN_OFFER_ENTRIES);

View File

@@ -1,3 +1,15 @@
/** @file
* Implements the `NFTokenCancelOffer` transactor, which removes one or more
* outstanding `NFTokenOffer` ledger objects in a single atomic transaction.
*
* The three phases follow the standard transactor pattern: `preflight`
* performs stateless list validation (size bounds, no duplicates), `preclaim`
* checks per-offer cancellation rights on a read-only view, and `doApply`
* calls `nft::deleteTokenOffer` to remove each offer and release its reserve.
* Both `preclaim` and `doApply` silently skip offers that no longer exist,
* making the operation idempotent when an offer is consumed between submission
* and application.
*/
#include <xrpl/tx/transactors/nft/NFTokenCancelOffer.h>
#include <xrpl/basics/Log.h>
@@ -47,25 +59,18 @@ NFTokenCancelOffer::preclaim(PreclaimContext const& ctx)
auto ret = std::ranges::find_if(ids, [&ctx, &account](uint256 const& id) {
auto const offer = ctx.view.read(keylet::child(id));
// If id is not in the ledger we assume the offer was consumed
// before we got here.
if (!offer)
return false;
// If id is in the ledger but is not an NFTokenOffer, then
// they have no permission.
if (offer->getType() != ltNFTOKEN_OFFER)
return true;
// Anyone can cancel, if expired
if (hasExpired(ctx.view, (*offer)[~sfExpiration]))
return false;
// The owner can always cancel
if ((*offer)[sfOwner] == account)
return false;
// The recipient can always cancel
if (auto const dest = (*offer)[~sfDestination]; dest == account)
return false;

View File

@@ -1,3 +1,20 @@
/**
* @file NFTokenCreateOffer.cpp
* @brief Transactor implementation for `ttNFTOKEN_CREATE_OFFER` (type 27).
*
* This file is intentionally thin: it acts as an adapter that extracts fields
* from the transaction context and forwards them to the three shared helpers
* declared in `NFTokenHelpers.h` — `tokenOfferCreatePreflight`,
* `tokenOfferCreatePreclaim`, and `tokenOfferCreateApply`. The same helpers
* are reused by `NFTokenMint`, which can create a sell offer atomically at
* mint time, ensuring the two transactors cannot drift apart on offer-creation
* rules.
*
* The only logic that lives here rather than in the shared helpers is the
* buy/sell token-ownership check in `preclaim` (which must switch on
* `tfSellNFToken` to choose the right account's NFT directory) and the
* pre-expiration guard (which requires the current ledger close time).
*/
#include <xrpl/tx/transactors/nft/NFTokenCreateOffer.h>
#include <xrpl/basics/base_uint.h>
@@ -31,7 +48,6 @@ NFTokenCreateOffer::preflight(PreflightContext const& ctx)
auto const nftFlags = nft::getFlags(ctx.tx[sfNFTokenID]);
// Use implementation shared with NFTokenMint
if (NotTEC notTec = nft::tokenOfferCreatePreflight(
ctx.tx[sfAccount],
ctx.tx[sfAmount],
@@ -60,7 +76,6 @@ NFTokenCreateOffer::preclaim(PreclaimContext const& ctx)
ctx.view, ctx.tx[((txFlags & tfSellNFToken) != 0u) ? sfAccount : sfOwner], nftokenID))
return tecNO_ENTRY;
// Use implementation shared with NFTokenMint
return nft::tokenOfferCreatePreclaim(
ctx.view,
ctx.tx[sfAccount],
@@ -77,7 +92,6 @@ NFTokenCreateOffer::preclaim(PreclaimContext const& ctx)
TER
NFTokenCreateOffer::doApply()
{
// Use implementation shared with NFTokenMint
return nft::tokenOfferCreateApply(
view(),
ctx_.tx[sfAccount],
@@ -97,7 +111,6 @@ NFTokenCreateOffer::visitInvariantEntry(
std::shared_ptr<SLE const> const&,
std::shared_ptr<SLE const> const&)
{
// No transaction-specific invariants yet (future work).
}
bool
@@ -108,7 +121,6 @@ NFTokenCreateOffer::finalizeInvariants(
ReadView const&,
beast::Journal const&)
{
// No transaction-specific invariants yet (future work).
return true;
}

View File

@@ -1,3 +1,11 @@
/** @file
* Implementation of the NFTokenMint transactor.
*
* Handles the full lifecycle of NFT creation: stateless field validation
* (`preflight`), read-only ledger checks (`preclaim`), and state mutations
* (`doApply`). Also provides `createNFTokenID`, the canonical algorithm for
* constructing the globally unique 256-bit NFToken identifier.
*/
#include <xrpl/tx/transactors/nft/NFTokenMint.h>
#include <xrpl/basics/Expected.h>
@@ -32,12 +40,33 @@
namespace xrpl {
/** Extract the NFToken-flag half of the transaction flags word.
*
* NFToken flags are packed into the lower 16 bits of the 32-bit transaction
* flags field. The upper 16 bits are reserved for transaction-level flags
* (e.g., `tfTransferable`, `tfTrustLine`). This helper isolates the token
* flags for storage in the NFToken ID and for passing to offer helpers.
*
* @param txFlags The raw 32-bit flags from `STTx::getFlags()`.
* @return The low 16 bits cast to `uint16_t`.
*/
static std::uint16_t
extractNFTokenFlagsFromTxFlags(std::uint32_t txFlags)
{
return static_cast<std::uint16_t>(txFlags & 0x0000FFFF);
}
/** Return true if the transaction includes any embedded-offer fields.
*
* The presence of `sfAmount`, `sfDestination`, or `sfExpiration` signals
* that the minter wants to create an initial sell offer atomically with the
* mint. Used by `checkExtraFeatures` to gate the sub-feature and by
* `preflight`/`preclaim` to decide whether to invoke the shared offer
* validation path.
*
* @param ctx Preflight context providing access to the transaction fields.
* @return `true` if at least one offer-related field is present.
*/
static bool
hasOfferFields(PreflightContext const& ctx)
{
@@ -54,23 +83,10 @@ NFTokenMint::checkExtraFeatures(PreflightContext const& ctx)
std::uint32_t
NFTokenMint::getFlagsMask(PreflightContext const& ctx)
{
// Prior to fixRemoveNFTokenAutoTrustLine, transfer of an NFToken between
// accounts allowed a TrustLine to be added to the issuer of that token
// without explicit permission from that issuer. This was enabled by
// minting the NFToken with the tfTrustLine flag set.
//
// That capability could be used to attack the NFToken issuer. It
// would be possible for two accounts to trade the NFToken back and forth
// building up any number of TrustLines on the issuer, increasing the
// issuer's reserve without bound.
//
// The fixRemoveNFTokenAutoTrustLine amendment disables minting with the
// tfTrustLine flag as a way to prevent the attack. But until the
// amendment passes we still need to keep the old behavior available.
// Four combinations: {trustLine disabled, trustLine enabled} x {mutable URI, no mutable URI}
std::uint32_t const nfTokenMintMask = [&]() -> std::uint32_t {
if (ctx.rules.enabled(fixRemoveNFTokenAutoTrustLine))
{
// if featureDynamicNFT enabled then new flag allowing mutable URI available
return ctx.rules.enabled(featureDynamicNFT) ? tfNFTokenMintMask
: tfNFTokenMintMaskWithoutMutable;
}
@@ -89,13 +105,12 @@ NFTokenMint::preflight(PreflightContext const& ctx)
if (f > kMAX_TRANSFER_FEE)
return temBAD_NFTOKEN_TRANSFER_FEE;
// If a non-zero TransferFee is set then the tfTransferable flag
// must also be set.
// A transfer fee on a non-transferable token is contradictory:
// there is no transfer that could ever collect it.
if (f > 0u && !ctx.tx.isFlag(tfTransferable))
return temMALFORMED;
}
// An issuer must only be set if the tx is executed by the minter
if (auto iss = ctx.tx[~sfIssuer]; iss == ctx.tx[sfAccount])
return temMALFORMED;
@@ -107,14 +122,11 @@ NFTokenMint::preflight(PreflightContext const& ctx)
if (hasOfferFields(ctx))
{
// The Amount field must be present if either the Destination or
// Expiration fields are present.
if (!ctx.tx.isFieldPresent(sfAmount))
return temMALFORMED;
// Rely on the common code shared with NFTokenCreateOffer to
// do the validation. We pass tfSellNFToken as the transaction flags
// because a Mint is only allowed to create a sell offer.
// Pass tfSellNFToken because a mint-bundled offer is always a sell
// offer; buy offers cannot be created atomically at mint time.
if (NotTEC notTec = nft::tokenOfferCreatePreflight(
ctx.tx[sfAccount],
ctx.tx[sfAmount],
@@ -139,14 +151,8 @@ NFTokenMint::createNFTokenID(
nft::Taxon taxon,
std::uint32_t tokenSeq)
{
// An issuer may issue several NFTs with the same taxon; to ensure that NFTs
// are spread across multiple pages we lightly mix the taxon up by using the
// sequence (which is not under the issuer's direct control) as the seed for
// a simple linear congruential generator. cipheredTaxon() does this work.
taxon = nft::cipheredTaxon(tokenSeq, taxon);
// The values are packed inside a 32-byte buffer, so we need to make sure
// that the endianess is fixed.
flags = boost::endian::native_to_big(flags);
fee = boost::endian::native_to_big(fee);
taxon = nft::toTaxon(boost::endian::native_to_big(nft::toUInt32(taxon)));
@@ -156,8 +162,6 @@ NFTokenMint::createNFTokenID(
auto ptr = buf.data();
// This code is awkward but the idea is to pack these values into a single
// 256-bit value that uniquely identifies this NFT.
std::memcpy(ptr, &flags, sizeof(flags));
ptr += sizeof(flags);
@@ -182,8 +186,6 @@ NFTokenMint::createNFTokenID(
TER
NFTokenMint::preclaim(PreclaimContext const& ctx)
{
// The issuer of the NFT may or may not be the account executing this
// transaction. Check that and verify that this is allowed:
if (auto issuer = ctx.tx[~sfIssuer])
{
auto const sle = ctx.view.read(keylet::account(*issuer));
@@ -197,13 +199,10 @@ NFTokenMint::preclaim(PreclaimContext const& ctx)
if (ctx.tx.isFieldPresent(sfAmount))
{
// The Amount field says create an offer for the minted token.
if (hasExpired(ctx.view, ctx.tx[~sfExpiration]))
return tecEXPIRED;
// Rely on the common code shared with NFTokenCreateOffer to
// do the validation. We pass tfSellNFToken as the transaction flags
// because a Mint is only allowed to create a sell offer.
// Pass tfSellNFToken: mint-bundled offers are always sell offers.
if (TER const ter = nft::tokenOfferCreatePreclaim(
ctx.view,
ctx.tx[sfAccount],
@@ -228,25 +227,17 @@ NFTokenMint::doApply()
auto const root = view().peek(keylet::account(issuer));
if (root == nullptr)
{
// Should not happen. Checked in preclaim.
return Unexpected(tecNO_ISSUER);
// Should not happen — checked in preclaim.
return Unexpected(tecNO_ISSUER); // LCOV_EXCL_LINE
}
// If the issuer hasn't minted an NFToken before we must add a
// FirstNFTokenSequence field to the issuer's AccountRoot. The
// value of the FirstNFTokenSequence must equal the issuer's
// current account sequence.
//
// There are three situations:
// o If the first token is being minted by the issuer and
// * If the transaction consumes a Sequence number, then the
// Sequence has been pre-incremented by the time we get here in
// doApply. We must decrement the value in the Sequence field.
// * Otherwise the transaction uses a Ticket so the Sequence has
// not been pre-incremented. We use the Sequence value as is.
// o The first token is being minted by an authorized minter. In
// this case the issuer's Sequence field has been left untouched.
// We use the issuer's Sequence value as is.
// Bootstrap sfFirstNFTokenSequence on the very first mint.
// By the time doApply runs, the account sequence has already been
// pre-incremented for direct-sequence transactions, so we subtract 1
// to recover the pre-tx value. Ticket-based transactions do not
// pre-increment the issuer's sequence, nor do authorized-minter
// transactions (where the issuer is a different account entirely), so
// in those two cases we use the sequence value as-is.
if (!root->isFieldPresent(sfFirstNFTokenSequence))
{
std::uint32_t const acctSeq = root->at(sfSequence);
@@ -262,12 +253,10 @@ NFTokenMint::doApply()
if ((*root)[sfMintedNFTokens] == 0u)
return Unexpected(tecMAX_SEQUENCE_REACHED);
// Get the unique sequence number of this token by
// sfFirstNFTokenSequence + sfMintedNFTokens
// tokenSeq = sfFirstNFTokenSequence + sfMintedNFTokens (before increment)
std::uint32_t const offset = (*root)[sfFirstNFTokenSequence];
std::uint32_t const tokenSeq = offset + mintedNftCnt;
// Check for more overflow cases
if (tokenSeq + 1u == 0u || tokenSeq < offset)
return Unexpected(tecMAX_SEQUENCE_REACHED);
@@ -281,13 +270,12 @@ NFTokenMint::doApply()
std::uint32_t const ownerCountBefore =
view().read(keylet::account(account_))->getFieldU32(sfOwnerCount);
// Assemble the new NFToken.
SOTemplate const* nfTokenTemplate =
InnerObjectFormats::getInstance().findSOTemplateBySField(sfNFToken);
if (nfTokenTemplate == nullptr)
{
// Should never happen.
// Should never happen — sfNFToken is registered at startup.
return tecINTERNAL; // LCOV_EXCL_LINE
}
@@ -311,9 +299,7 @@ NFTokenMint::doApply()
if (ctx_.tx.isFieldPresent(sfAmount))
{
// Rely on the common code shared with NFTokenCreateOffer to create
// the offer. We pass tfSellNFToken as the transaction flags
// because a Mint is only allowed to create a sell offer.
// Pass tfSellNFToken: mint-bundled offers are always sell offers.
if (TER const ter = nft::tokenOfferCreateApply(
view(),
ctx_.tx[sfAccount],
@@ -328,10 +314,9 @@ NFTokenMint::doApply()
return ter;
}
// Only check the reserve if the owner count actually changed. This
// allows NFTs to be added to the page (and burn fees) without
// requiring the reserve to be met each time. The reserve is
// only managed when a new NFT page or sell offer is added.
// Reserve is only checked when the owner count increased (new page or sell
// offer created). Packing tokens into an existing page does not allocate a
// new ledger object and therefore imposes no incremental reserve cost.
if (auto const ownerCountAfter =
view().read(keylet::account(account_))->getFieldU32(sfOwnerCount);
ownerCountAfter > ownerCountBefore)

View File

@@ -1,3 +1,12 @@
/** @file
* Implementation of the `NFTokenModify` transactor, which updates the
* metadata URI of a mutable NFT without burning and re-minting it.
*
* Heavy lifting is delegated to `nft::changeTokenURI` in
* `NFTokenHelpers.cpp`; this file is responsible only for the three-phase
* validation pipeline (`preflight` → `preclaim` → `doApply`) and the
* no-op invariant hooks.
*/
#include <xrpl/tx/transactors/nft/NFTokenModify.h>
#include <xrpl/basics/base_uint.h>
@@ -42,11 +51,9 @@ NFTokenModify::preclaim(PreclaimContext const& ctx)
if (!nft::findToken(ctx.view, owner, ctx.tx[sfNFTokenID]))
return tecNO_ENTRY;
// Check if the NFT is mutable
if ((nft::getFlags(ctx.tx[sfNFTokenID]) & nft::kFLAG_MUTABLE) == 0)
return tecNO_PERMISSION;
// Verify permissions for the issuer
if (AccountID const issuer = nft::getIssuer(ctx.tx[sfNFTokenID]); issuer != account)
{
auto const sle = ctx.view.read(keylet::account(issuer));

View File

@@ -1,3 +1,11 @@
/** @file
* Implementation of the `OracleDelete` transactor (XLS-47d).
*
* Removes a Price Oracle (`ltORACLE`) ledger object, reverting the owner
* reserve that was allocated when the oracle was created by `OracleSet`.
* The `deleteOracle` static helper is also the entry point used by
* `AccountDelete` when sweeping owned objects during account removal.
*/
#include <xrpl/tx/transactors/oracle/OracleDelete.h>
#include <xrpl/basics/Log.h>
@@ -17,12 +25,25 @@
namespace xrpl {
/** Unconditionally succeeds.
*
* A delete request has no stateless properties to validate — there are no
* field ranges, array size limits, or flag combinations that can be checked
* without ledger state. All meaningful validation is deferred to `preclaim`.
*/
NotTEC
OracleDelete::preflight(PreflightContext const& ctx)
{
return tesSUCCESS;
}
/** Validates that the target oracle exists and is owned by the submitter.
*
* The oracle keylet is derived from `(account, sfOracleDocumentID)`, so a
* successful read at that key is already proof of ownership; the explicit
* `sfOwner` comparison that follows is a defensive invariant and is excluded
* from coverage metrics (`LCOV_EXCL`).
*/
TER
OracleDelete::preclaim(PreclaimContext const& ctx)
{
@@ -39,7 +60,8 @@ OracleDelete::preclaim(PreclaimContext const& ctx)
if (ctx.tx.getAccountID(sfAccount) != sle->getAccountID(sfOwner))
{
// this can't happen because of the above check
// Unreachable: the oracle keylet embeds the account ID, so a
// successful read above is sufficient proof of ownership.
// LCOV_EXCL_START
JLOG(ctx.j.debug()) << "Oracle Delete: invalid account.";
return tecINTERNAL;
@@ -48,6 +70,26 @@ OracleDelete::preclaim(PreclaimContext const& ctx)
return tesSUCCESS;
}
/** Remove the oracle SLE from the ledger and reclaim the owner reserve.
*
* Deletion must follow a strict three-step order:
*
* 1. `dirRemove` — reads `sfOwnerNode` (the directory page index stored by
* `OracleSet::doApply` at creation time) from the live SLE. Erasing the
* SLE first would lose this pointer, making O(1) directory removal
* impossible. Failure here returns `tefBAD_LEDGER`, indicating ledger
* corruption rather than a user error.
*
* 2. `adjustOwnerCount` — decrements the owner reserve by `-2` if the oracle
* currently holds more than five `sfPriceDataSeries` entries, or by `-1`
* otherwise. This mirrors the creation logic in `OracleSet::doApply` and
* ensures the returned reserve equals the reserve originally taken.
* The threshold is read from the live SLE rather than the transaction to
* keep it immune to a caller passing a mismatched count.
*
* 3. `view.erase` — removes the SLE. After this call the SLE pointer is
* invalid; no further reads from it are safe.
*/
TER
OracleDelete::deleteOracle(
ApplyView& view,
@@ -70,6 +112,8 @@ OracleDelete::deleteOracle(
if (!sleOwner)
return tecINTERNAL; // LCOV_EXCL_LINE
// Mirror the +1/+2 increment in OracleSet::doApply: oracles with more than
// 5 price data series occupy two reserve slots, all others occupy one.
auto const count = sle->getFieldArray(sfPriceDataSeries).size() > 5 ? -2 : -1;
adjustOwnerCount(view, sleOwner, count, j);
@@ -79,6 +123,12 @@ OracleDelete::deleteOracle(
return tesSUCCESS;
}
/** Fetches the oracle SLE and delegates to `deleteOracle`.
*
* The `peek` returning null is unreachable under normal conditions because
* `preclaim` already confirmed the oracle exists. The `tecINTERNAL` sentinel
* is a fault-detection guard excluded from coverage metrics (`LCOV_EXCL`).
*/
TER
OracleDelete::doApply()
{

View File

@@ -1,3 +1,19 @@
/** @file
* Implementation of the `OracleSet` transactor (XLS-47d).
*
* Creates or updates an `ltORACLE` ledger object that stores off-chain
* price-feed data — base/quote token-pair prices, optional scaling factors,
* and a freshness timestamp — making external market data available to
* on-ledger consumers.
*
* Two amendments alter behaviour:
* - **`fixPriceOracleOrder`**: on creation, sorts `sfPriceDataSeries` into
* a canonical lexicographic order so all validators produce identical SLEs
* regardless of client submission order.
* - **`fixIncludeKeyletFields`**: back-fills `sfOracleDocumentID` into
* existing SLEs that predate the amendment, making the object
* self-describing without external context.
*/
#include <xrpl/tx/transactors/oracle/OracleSet.h>
#include <xrpl/basics/chrono.h>
@@ -29,6 +45,16 @@
namespace xrpl {
/** Extract a canonical `(BaseAsset, QuoteAsset)` currency pair from a
* `sfPriceData` entry or equivalent STObject, for use as a map/set key.
*
* Factoring this out ensures the key-extraction logic is identical in both
* `preclaim` (validation) and `doApply` (mutation), preventing subtle
* divergence bugs.
*
* @param pair An STObject containing at least `sfBaseAsset` and `sfQuoteAsset`.
* @return A `std::pair<Currency, Currency>` suitable for ordered containers.
*/
static inline std::pair<Currency, Currency>
tokenPairKey(STObject const& pair)
{
@@ -66,8 +92,6 @@ OracleSet::preclaim(PreclaimContext const& ctx)
if (!sleSetter)
return terNO_ACCOUNT; // LCOV_EXCL_LINE
// lastUpdateTime must be within maxLastUpdateTimeDelta seconds
// of the last closed ledger
using namespace std::chrono;
std::size_t const closeTime =
duration_cast<seconds>(ctx.view.header().closeTime.time_since_epoch()).count();
@@ -84,10 +108,9 @@ OracleSet::preclaim(PreclaimContext const& ctx)
auto const sle =
ctx.view.read(keylet::oracle(ctx.tx.getAccountID(sfAccount), ctx.tx[sfOracleDocumentID]));
// token pairs to add/update
// Entries with sfAssetPrice go into `pairs` (create/update); entries
// without sfAssetPrice go into `pairsDel` (deletion request).
std::set<std::pair<Currency, Currency>> pairs;
// token pairs to delete. if a token pair doesn't include
// the price then this pair should be deleted from the object.
std::set<std::pair<Currency, Currency>> pairsDel;
for (auto const& entry : ctx.tx.getFieldArray(sfPriceDataSeries))
{
@@ -112,9 +135,8 @@ OracleSet::preclaim(PreclaimContext const& ctx)
}
}
// Lambda is used to check if the value of a field, passed
// in the transaction, is equal to the value of that field
// in the on-ledger object.
// Returns true if `field` is absent from the transaction or matches the
// on-ledger value — enforcing immutability of sfProvider / sfAssetClass.
auto isConsistent = [&ctx, &sle](auto const& field) {
auto const v = ctx.tx[~field];
return !v || *v == (*sle)[field];
@@ -123,10 +145,6 @@ OracleSet::preclaim(PreclaimContext const& ctx)
std::int8_t adjustReserve = 0;
if (sle)
{
// update
// Account is the Owner since we can get sle
// lastUpdateTime must be more recent than the previous one
if (ctx.tx[sfLastUpdateTime] <= (*sle)[sfLastUpdateTime])
return tecINVALID_UPDATE_TIME;
@@ -157,8 +175,6 @@ OracleSet::preclaim(PreclaimContext const& ctx)
}
else
{
// create
if (!ctx.tx.isFieldPresent(sfProvider) || !ctx.tx.isFieldPresent(sfAssetClass))
return temMALFORMED;
adjustReserve = pairs.size() > 5 ? 2 : 1;
@@ -179,6 +195,18 @@ OracleSet::preclaim(PreclaimContext const& ctx)
return tesSUCCESS;
}
/** Convenience wrapper around the ledger-helper `adjustOwnerCount` that
* peeks the submitting account's SLE from `ctx` and delegates to the
* standard helper.
*
* The `false` return path is unreachable in practice: `preclaim` already
* verified the account exists via `terNO_ACCOUNT`.
*
* @param ctx The apply context for the current transaction.
* @param count Signed reserve-slot delta to apply (positive = increase).
* @return `true` on success; `false` if the account SLE cannot be peeked
* (dead code, annotated `LCOV_EXCL_LINE`).
*/
static bool
adjustOwnerCount(ApplyContext& ctx, int count)
{
@@ -191,6 +219,16 @@ adjustOwnerCount(ApplyContext& ctx, int count)
return false; // LCOV_EXCL_LINE
}
/** Apply the canonical `sfPriceData` inner-object SOTemplate to `obj`.
*
* XRPL's serialization layer requires every inner object to declare its
* field set via an `SOTemplate` before fields can be written to it.
* Without this call, `setField*` operations on a freshly constructed
* `STObject{sfPriceData}` would behave unpredictably.
*
* @param obj A freshly constructed or pre-existing `STObject` that will
* hold `sfPriceData` fields. Modified in place.
*/
static void
setPriceDataInnerObjTemplate(STObject& obj)
{
@@ -215,12 +253,11 @@ OracleSet::doApply()
if (auto sle = ctx_.view().peek(oracleID))
{
// update
// the token pair that doesn't have their price updated will not
// include neither price nor scale in the updated PriceDataSeries
// Update path: build a keyed map of the current pairs, apply the
// transaction's deltas (delete / update-in-place / insert), then
// serialise the result back. Pairs not mentioned by this transaction
// carry over with their existing price and scale unchanged.
std::map<std::pair<Currency, Currency>, STObject> pairs;
// collect current token pairs
for (auto const& entry : sle->getFieldArray(sfPriceDataSeries))
{
STObject priceData{sfPriceData};
@@ -230,25 +267,21 @@ OracleSet::doApply()
pairs.emplace(tokenPairKey(entry), std::move(priceData));
}
auto const oldCount = pairs.size() > 5 ? 2 : 1;
// update/add/delete pairs
for (auto const& entry : ctx_.tx.getFieldArray(sfPriceDataSeries))
{
auto const key = tokenPairKey(entry);
if (!entry.isFieldPresent(sfAssetPrice))
{
// delete token pair
pairs.erase(key);
}
else if (auto iter = pairs.find(key); iter != pairs.end())
{
// update the price
iter->second.setFieldU64(sfAssetPrice, entry.getFieldU64(sfAssetPrice));
if (entry.isFieldPresent(sfScale))
iter->second.setFieldU8(sfScale, entry.getFieldU8(sfScale));
}
else
{
// add a token pair with the price
STObject priceData{sfPriceData};
populatePriceData(priceData, entry);
pairs.emplace(key, std::move(priceData));
@@ -276,8 +309,10 @@ OracleSet::doApply()
}
else
{
// create
// Create path: allocate a new SLE, populate all fields, sort the
// sfPriceDataSeries canonically under fixPriceOracleOrder (pre-fix
// behaviour preserves raw transaction order for history consistency),
// insert into the owner directory, and increment the owner count.
sle = std::make_shared<SLE>(oracleID);
sle->setAccountID(sfOwner, ctx_.tx.getAccountID(sfAccount));
if (ctx_.view().rules().enabled(fixIncludeKeyletFields))

View File

@@ -1,3 +1,19 @@
/** @file
* Implementation of the `DepositPreauth` transactor.
*
* Manages `DepositPreauth` ledger entries, which form the allow-list that
* permits specific accounts or credential-bearing parties to send payments
* to an account that has enabled `lsfDepositAuth`. A single transaction
* performs exactly one of four mutually exclusive operations selected by
* whichever of `sfAuthorize`, `sfUnauthorize`, `sfAuthorizeCredentials`, or
* `sfUnauthorizeCredentials` is present.
*
* The credential-based operations are gated on `featureCredentials` and use
* a canonically sorted representation of the credential set for both the
* keylet derivation and ledger storage, ensuring order-independent lookups.
*
* @see DepositPreauth.h for the class declaration and full interface docs.
*/
#include <xrpl/tx/transactors/payment/DepositPreauth.h>
#include <xrpl/basics/Log.h>
@@ -98,7 +114,6 @@ DepositPreauth::preclaim(PreclaimContext const& ctx)
{
AccountID const account(ctx.tx[sfAccount]);
// Determine which operation we're performing: authorizing or unauthorizing.
if (ctx.tx.isFieldPresent(sfAuthorize))
{
// Verify that the Authorize account is present in the ledger.

View File

@@ -1,3 +1,15 @@
/**
* @file Payment.cpp
* @brief Implementation of the `ttPAYMENT` transactor.
*
* A single transaction type covers three structurally distinct execution paths:
* direct XRP-to-XRP transfers, direct MPToken (MPT) transfers (pre-`featureMPTokensV2`),
* and cross-currency / path-based payments routed through `path::RippleCalc`.
* The branching lives entirely in `doApply()`; the serialized transaction format
* is the same for all three cases.
*
* See `Payment.h` for the full class documentation and per-method contracts.
*/
#include <xrpl/tx/transactors/payment/Payment.h>
#include <xrpl/basics/Log.h>
@@ -59,6 +71,21 @@ Payment::makeTxConsequences(PreflightContext const& ctx)
return TxConsequences{ctx.tx, calculateMaxXRPSpend(ctx.tx)};
}
/**
* Computes the maximum amount the sender is willing to spend.
*
* When `sfSendMax` is present it is returned as-is. When it is absent
* and the destination asset is an XRP or MPT, the destination amount
* itself is the ceiling. For IOU destinations the amount is re-expressed
* with the *sender's* account as the issuer, because IOU trust lines are
* scoped to the issuer: the "same" currency from two different issuers is
* not fungible and must be tracked separately.
*
* @param account The sending account, used to substitute the IOU issuer.
* @param dstAmount The requested destination amount (`sfAmount`).
* @param sendMax The optional `sfSendMax` field from the transaction.
* @return The effective maximum source amount.
*/
STAmount
getMaxSourceAmount(
AccountID const& account,
@@ -187,8 +214,8 @@ Payment::preflight(PreflightContext const& ctx)
}
if (account == dstAccountID && equalTokens(srcAsset, dstAsset) && !hasPaths)
{
// You're signing yourself a payment.
// If hasPaths is true, you might be trying some arbitrage.
// A self-payment with no paths is always redundant. With paths the
// sender may be attempting an arbitrage cycle, which is permitted.
JLOG(j.trace()) << "Malformed transaction: "
<< "Redundant payment from " << to_string(account)
<< " to self without path for " << to_string(dstAsset);
@@ -196,35 +223,30 @@ Payment::preflight(PreflightContext const& ctx)
}
if (xrpDirect && hasMax)
{
// Consistent but redundant transaction.
JLOG(j.trace()) << "Malformed transaction: "
<< "SendMax specified for XRP to XRP.";
return temBAD_SEND_XRP_MAX;
}
if ((xrpDirect || (!mpTokensV2 && isDstMPT)) && hasPaths)
{
// XRP is sent without paths.
JLOG(j.trace()) << "Malformed transaction: "
<< "Paths specified for XRP to XRP or MPT to MPT.";
return temBAD_SEND_XRP_PATHS;
}
if (xrpDirect && partialPaymentAllowed)
{
// Consistent but redundant transaction.
JLOG(j.trace()) << "Malformed transaction: "
<< "Partial payment specified for XRP to XRP.";
return temBAD_SEND_XRP_PARTIAL;
}
if ((xrpDirect || (!mpTokensV2 && isDstMPT)) && limitQuality)
{
// Consistent but redundant transaction.
JLOG(j.trace()) << "Malformed transaction: "
<< "Limit quality specified for XRP to XRP or MPT to MPT.";
return temBAD_SEND_XRP_LIMIT;
}
if ((xrpDirect || (!mpTokensV2 && isDstMPT)) && !defaultPathsAllowed)
{
// Consistent but redundant transaction.
JLOG(j.trace()) << "Malformed transaction: "
<< "No ripple direct specified for XRP to XRP or MPT to MPT.";
return temBAD_SEND_XRP_NO_DIRECT;
@@ -310,7 +332,6 @@ Payment::checkPermission(ReadView const& view, STTx const& tx)
TER
Payment::preclaim(PreclaimContext const& ctx)
{
// Ripple if source or destination is non-native or if there are paths.
std::uint32_t const txFlags = ctx.tx.getFlags();
bool const partialPaymentAllowed = (txFlags & tfPartialPayment) != 0u;
auto const hasPaths = ctx.tx.isFieldPresent(sfPaths);
@@ -324,36 +345,31 @@ Payment::preclaim(PreclaimContext const& ctx)
if (!sleDst)
{
// Destination account does not exist.
if (!dstAmount.native())
{
JLOG(ctx.j.trace()) << "Delay transaction: Destination account does not exist.";
// Another transaction could create the account and then this
// transaction would succeed.
// tec (not tem) because another transaction could create the
// account first, after which this one would succeed.
return tecNO_DST;
}
if (ctx.view.open() && partialPaymentAllowed)
{
// You cannot fund an account with a partial payment.
// Make retry work smaller, by rejecting this.
// Partial payments cannot create accounts; use tel (not tec) to
// make retry cheaper by dropping the transaction early.
JLOG(ctx.j.trace()) << "Delay transaction: Partial payment not "
"allowed to create account.";
// Another transaction could create the account and then this
// transaction would succeed.
return telNO_DST_PARTIAL;
}
if (dstAmount < STAmount(ctx.view.fees().reserve))
{
// accountReserve is the minimum amount that an account can have.
// Reserve is not scaled by load.
// Reserve is not load-scaled; dstAmount must meet the base
// account reserve to fund the new account root.
JLOG(ctx.j.trace()) << "Delay transaction: Destination account does not exist. "
<< "Insufficent payment to create account.";
// TODO: de-dupe
// Another transaction could create the account and then this
// transaction would succeed.
return tecNO_DST_INSUF_XRP;
}
}
@@ -361,17 +377,15 @@ Payment::preclaim(PreclaimContext const& ctx)
((sleDst->getFlags() & lsfRequireDestTag) != 0u) &&
!ctx.tx.isFieldPresent(sfDestinationTag))
{
// The tag is basically account-specific information we don't
// understand, but we can require someone to fill it in.
// We didn't make this test for a newly-formed account because there's
// no way for this field to be set.
// The destination tag is opaque to the protocol but lets the
// destination owner require senders to provide one (e.g. for
// exchange sub-account routing). The check is skipped for
// newly-formed accounts because lsfRequireDestTag can't be set yet.
JLOG(ctx.j.trace()) << "Malformed transaction: DestinationTag required.";
return tecDST_TAG_NEEDED;
}
// Payment with at least one intermediate step and uses transitive balances.
if ((hasPaths || sendMax || !dstAmount.native()) && ctx.view.open())
{
STPathSet const& paths = ctx.tx.getFieldPathSet(sfPaths);
@@ -405,7 +419,6 @@ Payment::doApply()
{
auto const deliverMin = ctx_.tx[~sfDeliverMin];
// Ripple if source or destination is non-native or if there are paths.
std::uint32_t const txFlags = ctx_.tx.getFlags();
bool const partialPaymentAllowed = (txFlags & tfPartialPayment) != 0u;
bool const limitQuality = (txFlags & tfLimitQuality) != 0u;
@@ -421,13 +434,11 @@ Payment::doApply()
JLOG(j_.trace()) << "maxSourceAmount=" << maxSourceAmount.getFullText()
<< " dstAmount=" << dstAmount.getFullText();
// Open a ledger for editing.
auto const k = keylet::account(dstAccountID);
SLE::pointer sleDst = view().peek(k);
if (!sleDst)
{
// Create the account.
sleDst = std::make_shared<SLE>(k);
sleDst->setAccountID(sfAccount, dstAccountID);
sleDst->setFieldU32(sfSequence, view().seq());
@@ -437,9 +448,8 @@ Payment::doApply()
}
else
{
// Tell the engine that we are intending to change the destination
// account. The source account gets always charged a fee so it's always
// marked as modified.
// Mark the destination as modified so the engine tracks it; the source
// is always modified because a fee is always deducted.
view().update(sleDst);
}
@@ -450,14 +460,10 @@ Payment::doApply()
if (ripple)
{
// XRPL payment with at least one intermediate step and uses
// transitive balances.
// An account that requires authorization has two ways to get an
// IOU Payment in:
// 1. If Account == Destination, or
// 2. If Account is deposit preauthorized by destination.
// An account that requires deposit authorization has two ways to
// receive an IOU payment:
// 1. Account == Destination, or
// 2. Account is deposit-preauthorized by the destination.
if (auto err = verifyDepositPreauth(
ctx_.tx, ctx_.view(), account_, dstAccountID, sleDst, ctx_.journal);
!isTesSuccess(err))
@@ -483,9 +489,6 @@ Payment::doApply()
ctx_.tx[~sfDomainID],
ctx_.registry,
&rcInput);
// VFALCO NOTE We might not need to apply, depending
// on the TER. But always applying *should*
// be safe.
pv.apply(ctx_.rawView());
}
@@ -505,10 +508,9 @@ Payment::doApply()
auto terResult = rc.result();
// Because of its overhead, if RippleCalc
// fails with a retry code, claim a fee
// instead. Maybe the user will be more
// careful with their path spec next time.
// Promote ter* retry codes to tecPATH_DRY so a fee is charged.
// Running the path engine has non-trivial cost; charging a fee
// discourages users from submitting poorly-constructed path specs.
if (isTerRetry(terResult))
terResult = tecPATH_DRY;
return terResult;
@@ -535,35 +537,27 @@ Payment::doApply()
auto const& issuer = mptIssue.getIssuer();
// Transfer rate
Rate rate{QUALITY_ONE};
// Payment between the holders
if (account_ != issuer && dstAccountID != issuer)
{
// If globally/individually locked then
// - can't send between holders
// - holder can send back to issuer
// - issuer can send to holder
// Freeze checks apply only between holders; issuers can always
// send to holders and holders can always return to issuers even
// when the issuance is globally or individually locked.
if (isAnyFrozen(view(), {account_, dstAccountID}, mptIssue))
return tecLOCKED;
// Get the rate for a payment between the holders.
rate = transferRate(view(), mptIssue.getMptID());
}
// Amount to deliver.
STAmount amountDeliver = dstAmount;
// Factor in the transfer rate.
// No rounding. It'll change once MPT integrated into DEX.
// No rounding here — rounding semantics will change once MPT is
// integrated into the DEX path engine.
STAmount requiredMaxSourceAmount = multiply(dstAmount, rate);
// Send more than the account wants to pay or less than
// the account wants to deliver (if no SendMax).
// Adjust the amount to deliver.
if (partialPaymentAllowed && requiredMaxSourceAmount > maxSourceAmount)
{
requiredMaxSourceAmount = maxSourceAmount;
// No rounding. It'll change once MPT integrated into DEX.
// No rounding — same note as above.
amountDeliver = divide(maxSourceAmount, rate);
}
@@ -577,9 +571,9 @@ Payment::doApply()
{
pv.apply(ctx_.rawView());
// If the actual amount delivered is different from the original
// amount due to partial payment or transfer fee, we need to update
// DeliveredAmount using the actual delivered amount
// Record actual delivered amount for the DeliveredAmount metadata
// field when it differs from sfAmount (partial pay or transfer fee).
// Gated on fixMPTDeliveredAmount, mirroring the IOU payment pattern.
if (view().rules().enabled(fixMPTDeliveredAmount) && amountDeliver != dstAmount)
ctx_.deliver(amountDeliver);
}
@@ -599,20 +593,13 @@ Payment::doApply()
if (!sleSrc)
return tefINTERNAL; // LCOV_EXCL_LINE
// ownerCount is the number of entries in this ledger for this
// account that require a reserve.
auto const ownerCount = sleSrc->getFieldU32(sfOwnerCount);
// This is the total reserve in drops.
auto const reserve = view().fees().accountReserve(ownerCount);
// In a delegated payment, the fee payer is the delegated account,
// not the source account (account_).
// In a delegated payment the fee is charged to the delegate, not to
// account_. When account_ IS the fee payer it must also cover the fee
// amount, so minRequiredFunds is max(reserve, fee) rather than just reserve.
bool const accountIsPayer = (ctx_.tx.getFeePayer() == account_);
// preFeeBalance_ is the balance on the source account (account_) BEFORE the fees
// were charged. If source account is the fee payer, it must also cover the fee.
// The final spend may use the reserve to cover fees.
auto const minRequiredFunds =
accountIsPayer ? std::max(reserve, ctx_.tx.getFieldAmount(sfFee).xrp()) : reserve;
@@ -635,28 +622,17 @@ Payment::doApply()
if (isPseudoAccount(sleDst))
return tecNO_PERMISSION;
// The source account does have enough money. Make sure the
// source account has authority to deposit to the destination.
// An account that requires authorization has three ways to get an XRP
// Payment in:
// 1. If Account == Destination, or
// 2. If Account is deposit preauthorized by destination, or
// 3. If the destination's XRP balance is
// a. less than or equal to the base reserve and
// b. the deposit amount is less than or equal to the base reserve,
// then we allow the deposit.
// An account with lsfDepositAuth set has three ways to receive XRP:
// 1. Account == Destination, or
// 2. Account is deposit-preauthorized by the destination, or
// 3. Both the destination's current balance AND the payment amount
// are ≤ the base reserve (Rule 3 / small-balance bypass).
//
// Rule 3 is designed to keep an account from getting wedged
// in an unusable state if it sets the lsfDepositAuth flag and
// then consumes all of its XRP. Without the rule if an
// account with lsfDepositAuth set spent all of its XRP, it
// would be unable to acquire more XRP required to pay fees.
//
// We choose the base reserve as our bound because it is
// a small number that seldom changes but is always sufficient
// to get the account un-wedged.
// Get the base reserve.
// Rule 3 prevents an account from becoming permanently wedged: if an
// account sets lsfDepositAuth and then spends all its XRP it would be
// unable to pay the fee required to unset the flag. The base reserve
// is the bound because it is small, seldom changes, and is always
// enough to fund the account-management transaction needed to recover.
XRPAmount const dstReserve{view().fees().reserve};
if (dstAmount > dstReserve || sleDst->getFieldAmount(sfBalance) > dstReserve)
@@ -667,11 +643,11 @@ Payment::doApply()
return err;
}
// Do the arithmetic for the transfer and make the ledger change.
sleSrc->setFieldAmount(sfBalance, sleSrc->getFieldAmount(sfBalance) - dstAmount);
sleDst->setFieldAmount(sfBalance, sleDst->getFieldAmount(sfBalance) + dstAmount);
// Re-arm the password change fee if we can and need to.
// Clear lsfPasswordSpent if set — legacy flag from the original
// password-based account creation flow; receiving XRP re-enables it.
if ((sleDst->getFlags() & lsfPasswordSpent) != 0u)
sleDst->clearFlag(lsfPasswordSpent);

View File

@@ -752,12 +752,18 @@ doFeature(RPC::JsonContext&);
json::Value
doGetCounts(RPC::JsonContext&);
/** Dump internal object state to the log (ADMIN).
/** Serialize the application's `beast::PropertyStream` tree to JSON (ADMIN).
*
* Triggers a diagnostic print of subsystem internals for offline analysis.
* Returns a live introspection snapshot of every subsystem that has
* registered itself as a `PropertyStream::Source` child of `Application`.
* An optional dot-delimited path string in `params[0]` targets a specific
* named sub-source; a missing or malformed parameter falls back silently to
* the full tree. Always succeeds — never returns an error response.
*
* @param context RPC dispatch context for this call.
* @return JSON object confirming the print was triggered.
* @param context RPC dispatch context; optional path filter read from
* `context.params[jss::params][0u]`.
* @return JSON object containing the serialized property-stream tree.
* @see JsonPropertyStream
*/
json::Value
doPrint(RPC::JsonContext&);

View File

@@ -8,10 +8,46 @@
namespace xrpl {
// {
// ledger_hash : <ledger>
// ledger_index : <ledger_index>
// }
/** @file
* Admin RPC handler that locates a historical ledger by hash or sequence,
* acquiring it from the network if it is not already cached locally.
*/
/** Implement the `ledger_request` admin RPC command.
*
* Declares a heavy resource cost immediately, then delegates all ledger
* resolution and acquisition logic to `RPC::getOrAcquireLedger()`. On
* success the response contains a compact header-level JSON representation
* of the ledger (no transactions, no state entries) together with the
* `ledger_index` field.
*
* Exactly one of `ledger_hash` (64-char hex string) or `ledger_index`
* (positive integer) must be present in `params`; providing both or neither
* is a `rpcBAD_PARAM` error. Shortcut strings (`current`, `closed`,
* `validated`) are not accepted — this command targets specific historical
* ledgers rather than live chain state.
*
* When the requested ledger is not locally cached, `getOrAcquireLedger`
* calls `InboundLedgers::acquire()` to initiate a peer-to-peer fetch. While
* that fetch is in progress the returned error JSON contains an `"acquiring"`
* field; callers should poll until the ledger becomes available.
*
* @param context JSON-RPC context; `params` must contain exactly one of
* `ledger_hash` or `ledger_index`.
* @return On success, a JSON object with `ledger_index` and a nested
* `ledger` block containing the ledger header fields. On failure, an
* error JSON object; may include an `"acquiring"` field when a network
* fetch is pending.
* @note `context.loadType` is set to `kFEE_HEAVY_BURDEN_RPC` (weight 3000)
* before any work is done so the resource manager can rate-limit or
* deprioritize the caller regardless of whether the call ultimately
* triggers a network fetch.
* @note For the index-based path, the node's validated ledger must not be
* stale (older than `Tuning::kMAX_VALIDATED_LEDGER_AGE`); API v1 returns
* `rpcNO_CURRENT` on staleness, API v2+ returns `rpcNOT_SYNCED`.
* @note Non-admin callers receive an HTTP 403 response rather than a JSON
* error; the result object will be null in that case.
*/
json::Value
doLedgerRequest(RPC::JsonContext& context)
{

View File

@@ -1,3 +1,8 @@
/** @file
* Implements the `validation_create` admin RPC command, which generates a
* secp256k1 key pair for use as a validator's signing identity.
*/
#include <xrpld/rpc/Context.h>
#include <xrpl/json/json_value.h>
@@ -14,6 +19,20 @@
namespace xrpl {
/** Resolve the seed for validator key generation from RPC params.
*
* If `params` contains a `secret` field, interprets it via
* `parseGenericSeed()`, which accepts Base58 family seeds, passphrase
* strings (hashed via SHA-512 Half to 128 bits), and RFC 1751 mnemonic
* word lists. If `secret` is absent, generates 128 bits of
* cryptographically secure random entropy via `randomSeed()`.
*
* @param params The JSON request parameters object.
* @return The resolved `Seed` on success, or `std::nullopt` if `secret`
* was provided but could not be parsed by `parseGenericSeed()`.
* @note The `Seed` destructor zeroes its 16-byte buffer, so key material
* does not linger in memory after the returned object is destroyed.
*/
static std::optional<Seed>
validationSeed(json::Value const& params)
{
@@ -23,12 +42,37 @@ validationSeed(json::Value const& params)
return parseGenericSeed(params[jss::secret].asString());
}
// {
// secret: <string> // optional
// }
//
// This command requires Role::ADMIN access because it makes
// no sense to ask an untrusted server for this.
/** Handle the `validation_create` RPC command.
*
* Generates a secp256k1 key pair suitable for use as a validator's signing
* identity. The returned object contains four fields covering the full range
* of operational needs:
*
* - `validation_public_key` — Base58 `NodePublic` token; published in UNLs
* and trusted by peers during consensus.
* - `validation_private_key` — Base58 `NodePrivate` token; stored locally
* to sign validation messages.
* - `validation_seed` — Base58 `FamilySeed` token; used in the
* `[validation_seed]` config entry (older deployment style).
* - `validation_key` — RFC 1751 mnemonic; human-readable seed backup.
*
* When `secret` is omitted, a random seed is generated. When `secret` is
* provided, it is parsed via `parseGenericSeed()` (Base58, passphrase, or
* RFC 1751). The `NodePublic`/`NodePrivate` token types are structurally
* distinct from `AccountPublic`/`AccountPrivate`, making it impossible at
* the Base58 layer to confuse a validator key for an account key.
*
* @param context The RPC dispatch context; `context.params` may contain an
* optional `secret` field (string).
* @return A JSON object with the four key-material fields on success, or an
* `rpcBAD_SEED` error object if the supplied `secret` could not be
* parsed.
* @note This command is restricted to `Role::ADMIN`. Asking an untrusted
* server to generate a validator seed would allow that server to log
* or substitute a seed it controls, subverting the validator's identity.
* @see doWalletPropose() for account key generation (supports ed25519,
* entropy warnings, and account ID derivation).
*/
json::Value
doValidationCreate(RPC::JsonContext& context)
{

View File

@@ -1,3 +1,10 @@
/** @file
* Implements the `wallet_propose` admin RPC command.
*
* Generates a complete XRPL account identity — seed, key pair, and account
* address — from a caller-supplied or randomly-generated seed. Exposed only
* to admin-role clients; never reachable by untrusted users.
*/
#include <xrpld/rpc/handlers/admin/keygen/WalletPropose.h>
#include <xrpld/rpc/Context.h>
@@ -22,12 +29,23 @@
namespace xrpl {
/** Estimate the Shannon entropy of a string in bits.
*
* Computes the per-symbol Shannon entropy from character frequencies and
* multiplies by the string length to yield a total bit estimate. The result
* is floored to be conservative — callers use this to gate brute-force
* vulnerability warnings, so over-estimating entropy would suppress a
* legitimate warning.
*
* @param input The string whose entropy is estimated.
* @return Estimated entropy in bits, floored to the nearest integer.
* @note This is a statistical estimate, not a cryptographic strength
* measurement. It is only meaningful for human-chosen passphrases;
* a randomly-generated seed will always score well regardless.
*/
double
estimateEntropy(std::string const& input)
{
// First, we calculate the Shannon entropy. This gives
// the average number of bits per symbol that we would
// need to encode the input.
std::map<int, double> freq;
for (auto const& c : input)
@@ -42,21 +60,65 @@ estimateEntropy(std::string const& input)
se += (x)*log2(x);
}
// We multiply it by the length, to get an estimate of
// the number of bits in the input. We floor because it
// is better to be conservative.
// Floor because it is better to be conservative when warning about
// low-entropy passphrases.
return std::floor(-se * input.length());
}
// {
// passphrase: <string>
// }
/** RPC dispatch entry point for the `wallet_propose` command.
*
* Extracts the request parameters from the JSON context and delegates to
* `walletPropose()`. Separating dispatch from logic allows the core
* function to be called directly in tests without a full RPC context.
*
* Accepts an optional `passphrase`, `seed`, or `seed_hex` field (mutually
* exclusive); and an optional `key_type` ("secp256k1" or "ed25519").
*
* @param context The RPC dispatch context carrying `params`.
* @return A JSON object with key-generation results, or an error object.
*/
json::Value
doWalletPropose(RPC::JsonContext& context)
{
return walletPropose(context.params);
}
/** Generate a complete XRPL account identity from parameters.
*
* Resolves a seed via a priority-ordered chain: XrplLib-encoded Ed25519 seed
* detection (in `passphrase` or `seed` fields) → `getSeedFromRPC()` for
* explicit `passphrase` / `seed` / `seed_hex` → `randomSeed()` as the
* fallback. Key type defaults to `secp256k1` for historical compatibility
* when none is specified.
*
* On success, returns a JSON object with seven fields:
* - `master_seed` — base58-encoded seed (canonical backup format)
* - `master_seed_hex` — raw hex of the seed bytes
* - `master_key` — RFC 1751 mnemonic encoding
* - `account_id` — base58check account address
* - `public_key` — base58-encoded public key with AccountPublic prefix
* - `public_key_hex` — raw hex public key
* - `key_type` — resolved algorithm name ("secp256k1" or "ed25519")
*
* A `warning` field is appended when a user-supplied passphrase is detected
* that is not itself an encoding of the seed: a strong warning when entropy
* is below 80 bits, and a softer advisory otherwise (any brain-wallet
* derivation is weaker than a random seed). The warning is suppressed when
* the passphrase matches the seed's own 1751, base58, or hex encoding, and
* when a XrplLib-encoded seed is detected.
*
* @param params JSON object containing optional fields: `key_type`,
* `passphrase`, `seed`, `seed_hex`. The three seed-input fields are
* mutually exclusive; supplying more than one yields an error from
* `getSeedFromRPC()`.
* @return A JSON result object on success, or a JSON error object on failure.
* @note Supplying a XrplLib-encoded Ed25519 seed together with
* `key_type: "secp256k1"` returns `rpcBAD_SEED` rather than silently
* deriving the wrong key. XrplLib seeds are unambiguously Ed25519.
* @see walletPropose() is called directly by `KeyGeneration_test.cpp` to
* validate known constant vectors for both key types and both warning
* tiers.
*/
json::Value
walletPropose(json::Value const& params)
{
@@ -77,9 +139,10 @@ walletPropose(json::Value const& params)
return rpcError(RpcInvalidParams);
}
// XrplLib encodes seed used to generate an Ed25519 wallet in a
// non-standard way. While we never encode seeds that way, we try
// to detect such keys to avoid user confusion.
// XrplLib encodes seeds for Ed25519 wallets with a non-standard base58
// prefix (0xE1 0x4B), producing an 18-byte form rippled never emits.
// Detect it in passphrase/seed fields first so we can enforce the
// Ed25519-only constraint and avoid confusing users who migrate keys.
{
if (params.isMember(jss::passphrase))
{
@@ -140,17 +203,20 @@ walletPropose(json::Value const& params)
obj[jss::key_type] = to_string(*keyType);
obj[jss::public_key_hex] = strHex(publicKey);
// If a passphrase was specified, and it was hashed and used as a seed
// run a quick entropy check and add an appropriate warning, because
// "brain wallets" can be easily attacked.
// Warn about brain-wallet weakness when a user passphrase was used.
// Skip if the passphrase is just an alternate encoding of the seed
// itself — the user is passing a valid seed as a string, not inventing
// a memorable phrase, so entropy is already adequate.
if (!libSeed && params.isMember(jss::passphrase))
{
auto const passphrase = params[jss::passphrase].asString();
if (passphrase != seed1751 && passphrase != seedBase58 && passphrase != seedHex)
{
// 80 bits of entropy isn't bad, but it's better to
// err on the side of caution and be conservative.
// 80 bits is the threshold: below it the passphrase is
// acutely vulnerable to brute-force; at or above it we still
// warn because any deterministic derivation is weaker than a
// truly random seed.
if (estimateEntropy(passphrase) < 80.0)
{
obj[jss::warning] =

View File

@@ -4,6 +4,41 @@
namespace xrpl {
/** Generate a complete XRPL account identity from caller-supplied or random seed material.
*
* Implements the core logic of the `wallet_propose` admin RPC command without
* requiring a full RPC dispatch context, so it can be called directly from
* unit tests. The companion `doWalletPropose()` adapter in `WalletPropose.cpp`
* simply forwards `context.params` here.
*
* Seed resolution order:
* 1. XrplLib-encoded Ed25519 seed detected in `passphrase` or `seed` fields
* (non-standard 0xE1 0x4B base58 prefix). Forces `key_type` to `ed25519`;
* returns `rpcBAD_SEED` if the caller explicitly requested a conflicting type.
* 2. Explicit `passphrase`, `seed`, or `seed_hex` field, parsed via
* `getSeedFromRPC()`. The three fields are mutually exclusive.
* 3. `randomSeed()` — used when no seed material is provided at all.
*
* Key type defaults to `secp256k1` when unspecified.
*
* On success the returned JSON object contains: `master_seed` (base58),
* `master_seed_hex`, `master_key` (RFC 1751 mnemonic), `account_id` (base58check
* address), `public_key` (base58 with AccountPublic prefix), `public_key_hex`,
* and `key_type`. A `warning` field is appended when the caller supplied a
* passphrase that is not itself an encoding of the seed: strong ("vulnerable to
* brute-force attacks") when Shannon-entropy estimate is below 80 bits, softer
* advisory otherwise.
*
* @param params JSON object with optional fields: `key_type` ("secp256k1" or
* "ed25519"), `passphrase`, `seed`, `seed_hex`. Providing more than one of
* the three seed-input fields yields an error from `getSeedFromRPC()`.
* @return A JSON result object on success, or a JSON error object on failure.
* @note The passphrase entropy warning is suppressed when the passphrase value
* matches the seed's own base58, hex, or RFC 1751 encoding — the caller is
* presenting an existing seed as a string, not a brain-wallet phrase.
* @see `KeyGeneration_test.cpp` exercises this function directly with known
* constant vectors for both key types and both warning tiers.
*/
json::Value
walletPropose(json::Value const& params);

View File

@@ -1,3 +1,12 @@
/** @file
* Implements `doLogLevel`, the handler for the `log_level` admin RPC command.
*
* Operators use this command at runtime to query or change log verbosity
* thresholds across all logging partitions — or a single named one — without
* restarting the node. The companion command `logrotate` (implemented in
* `LogRotate.cpp`) completes the set of runtime log-management primitives.
*/
#include <xrpld/app/main/Application.h>
#include <xrpld/rpc/Context.h>
@@ -15,13 +24,43 @@
namespace xrpl {
/** Query or set runtime log-verbosity thresholds (ADMIN).
*
* Dispatches across three modes based on the shape of the request:
*
* - **Query** (no `severity` field): returns a `levels` JSON object whose
* `base` key holds the global threshold and whose remaining keys are the
* per-partition thresholds for every named subsystem (e.g. `Ledger`,
* `Consensus`).
* - **Global set** (`severity` only, no `partition`): sets the global base
* threshold; all partitions without an individual override are filtered at
* this level from that point forward.
* - **Partition set** (`severity` + `partition`): sets the threshold on a
* single named partition. The name `"base"` (case-insensitive) is an alias
* for the global threshold and is handled identically to the global-set
* mode.
*
* **Severity conversion:** the raw string is validated by `Logs::fromString()`
* and converted to `beast::severities::Severity` via `Logs::toSeverity()`.
* An unrecognised string causes an immediate `rpcINVALID_PARAMS` error rather
* than silently clamping to a default.
*
* @param context RPC dispatch context carrying `params` and `app` references.
* @return In query mode, a JSON object with a `levels` key. In set modes, an
* empty JSON object on success, or an `rpcINVALID_PARAMS` error object for
* an unrecognised severity string.
*
* @note Unknown partition names are silently accepted: `Logs::get()` creates a
* new sink on demand, so a misspelled partition name creates an isolated
* threshold entry rather than returning an error.
* @note The `"base"` partition alias comparison uses `boost::iequals`, matching
* the case-insensitive storage key comparison used internally by `Logs`.
*/
json::Value
doLogLevel(RPC::JsonContext& context)
{
// log_level
if (not context.params.isMember(jss::severity))
{
// get log severities
json::Value ret(json::ValueType::Object);
json::Value lev(json::ValueType::Object);
@@ -40,18 +79,14 @@ doLogLevel(RPC::JsonContext& context)
if (not severity.has_value())
return rpcError(RpcInvalidParams);
// log_level severity
if (not context.params.isMember(jss::partition))
{
// set base log threshold
context.app.getLogs().threshold(*severity);
return json::ValueType::Object;
}
// log_level partition severity base?
if (context.params.isMember(jss::partition))
{
// set partition threshold
std::string const partition(context.params[jss::partition].asString());
if (boost::iequals(partition, "base"))

View File

@@ -1,3 +1,13 @@
/** @file
* Implements `doLogRotate`, the handler for the `logrotate` admin RPC command.
*
* Rotating the log file allows external tools such as `logrotate(8)` to rename
* or truncate the on-disk file while the process keeps running; the handler
* closes and reopens the file descriptor so subsequent writes go to the new
* path. The companion command `log_level` (implemented in `LogLevel.cpp`)
* completes the set of runtime log-management primitives.
*/
#include <xrpld/app/main/Application.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/detail/Handler.h>
@@ -8,6 +18,23 @@
namespace xrpl {
/** Rotate both the performance log and the application log (ADMIN).
*
* Closes and reopens the application log file at its configured path, which
* is the standard interop pattern for `logrotate(8)`: the external tool renames
* or truncates the file first, then signals the process to reopen it. The
* performance log is rotated via `PerfLog::rotate()` beforehand.
*
* No request parameters are consumed; the handler delegates entirely to the
* two logging subsystems and returns their status message.
*
* @param context RPC dispatch context carrying the `app` reference.
* @return A JSON object whose value is the status string returned by
* `Logs::rotate()` (e.g., `"The log file has been rotated"`).
*
* @note Access is enforced by the RPC framework before this function is
* called; the handler itself performs no role or condition checks.
*/
json::Value
doLogRotate(RPC::JsonContext& context)
{

View File

@@ -1,3 +1,8 @@
/** @file
* Implements the `connect` admin RPC handler, which instructs the node to
* open an outbound peer connection to a caller-specified IP address and port.
*/
#include <xrpld/app/main/Application.h>
#include <xrpld/core/Config.h>
#include <xrpld/overlay/Overlay.h>
@@ -15,10 +20,31 @@
namespace xrpl {
// {
// ip: <string>,
// port: <number>
// }
/** Initiate a manual outbound peer connection (ADMIN).
*
* Validates the caller-supplied `ip` and optional `port`, then delegates to
* `Overlay::connect()`, which is non-blocking — the TCP handshake proceeds
* asynchronously and the response is returned immediately.
*
* Request fields:
* - `ip` (string, required) — IPv4 or IPv6 address of the target peer.
* - `port` (integer, optional) — target port; defaults to `kDEFAULT_PEER_PORT`
* (2459) when absent.
*
* @param context RPC dispatch context carrying the request parameters and
* application references.
* @return A JSON string value confirming the connection attempt, or a JSON
* error object if validation fails.
*
* @note Returns `rpcNOT_SYNCED` when the node is running in standalone mode,
* where peer connections are not meaningful.
* @note If `ip` does not parse as a valid address, `Overlay::connect()` is
* silently skipped — the response still reads "attempting connection …"
* with no indication that the address was rejected. DNS names are not
* resolved.
* @note The port value is not range-checked; values outside `[1, 65535]` are
* passed through to the overlay unchanged.
*/
// XXX Might allow domain for manual connections.
json::Value
doConnect(RPC::JsonContext& context)

View File

@@ -14,6 +14,42 @@
namespace xrpl {
/** Insert or replace a peer reservation (admin: `peer_reservations_add`).
*
* A peer reservation gives a specific XRPL node a guaranteed inbound
* connection slot in the overlay, even when the general pool is full.
* This handler is the write path: it validates the caller-supplied node
* public key, optionally records a human-readable description, and
* delegates to `PeerReservationTable::insertOrAssign` which updates both
* the in-memory registry and the backing SQL table under a single lock.
*
* Validation is performed in three layers before any state is mutated:
* 1. Presence — `public_key` must be present.
* 2. Type — `public_key` (and `description`, if present) must be a string.
* 3. Cryptographic parse — `public_key` must decode as a NodePublic base58
* key via `parseBase58<PublicKey>`. Only base58 is accepted (not hex),
* as a deliberate design choice to eliminate ambiguous-format bugs.
*
* The response object is always a JSON `{}`. If a prior reservation existed
* for the same node key it is returned under `jss::previous` (serialized by
* `PeerReservation::toJson()`), giving callers idempotent-safe confirmation
* of what was displaced. A fresh insert returns an empty object with no
* `previous` field.
*
* @note The inline parameter-extraction code intentionally avoids a shared
* helper that returns `Json::Value`. Copying whole JSON objects to
* propagate error codes is expensive and clutters call sites. The right
* abstraction is an error monad (`std::expected`-style
* `optional<T, Error>`); the code uses direct early-returns as the
* practical stand-in until such an abstraction is adopted.
*
* @param context RPC dispatch context carrying `params` and the live
* `Application` reference.
* @return A JSON object, optionally containing a `"previous"` field with
* the reservation that was replaced.
* @throws soci::soci_error Propagated from `PeerReservationTable::insertOrAssign`
* if the underlying SQL write fails.
*/
json::Value
doPeerReservationsAdd(RPC::JsonContext& context)
{
@@ -22,24 +58,9 @@ doPeerReservationsAdd(RPC::JsonContext& context)
if (!params.isMember(jss::public_key))
return RPC::missingFieldError(jss::public_key);
// Returning JSON from every function ruins any attempt to encapsulate
// the pattern of "get field F as type T, and diagnose an error if it is
// missing or malformed":
// - It is costly to copy whole JSON objects around just to check whether an
// error code is present.
// - It is not as easy to read when cluttered by code to pack and unpack the
// JSON object.
// - It is not as easy to write when you have to include all the packing and
// unpacking code.
// Exceptions would be easier to use, but have a terrible cost for control
// flow. An error monad is purpose-built for this situation; it is
// essentially an optional (the "maybe monad" in Haskell) with a non-unit
// type for the failure case to capture more information.
if (!params[jss::public_key].isString())
return RPC::expectedFieldError(jss::public_key, "a string");
// Same for the pattern of "if field F is present, make sure it has type T
// and get it".
std::string desc;
if (params.isMember(jss::description))
{
@@ -48,8 +69,6 @@ doPeerReservationsAdd(RPC::JsonContext& context)
desc = params[jss::description].asString();
}
// channel_verify takes a key in both base58 and hex.
// @nikb prefers that we take only base58.
std::optional<PublicKey> optPk =
parseBase58<PublicKey>(TokenType::NodePublic, params[jss::public_key].asString());
if (!optPk)

View File

@@ -12,12 +12,45 @@
namespace xrpl {
/** Remove a peer reservation by node public key (admin: `peer_reservations_del`).
*
* Deletes the reserved peer slot held by the node identified by `public_key`,
* freeing that slot to be filled by the general overlay pool. Both the
* in-memory registry and the backing SQL table are updated atomically under
* `PeerReservationTable`'s internal mutex, so the deletion is immediately
* visible to concurrent peer-connection logic and survives a restart.
*
* Validation is performed in three layers before any state is mutated:
* 1. Presence — `public_key` must be present in `params`.
* 2. Type — `public_key` must be a JSON string.
* 3. Cryptographic parse — `public_key` must decode as a NodePublic base58
* key via `parseBase58<PublicKey>(TokenType::NodePublic, ...)`. The
* `NodePublic` token type is required to prevent account keys or other
* base58-encoded types from being accepted.
*
* If the key is valid but no reservation existed, the operation is a no-op
* and the response is an empty JSON object — the handler does not treat a
* missing reservation as an error, making it safe to call idempotently from
* scripts or retry loops.
*
* @note Parameter extraction is duplicated from `doPeerReservationsAdd` by
* design. A shared helper returning `Json::Value` would copy whole error
* objects on failure; the right abstraction is an error monad
* (`std::expected`-style), used here as explicit early-returns until
* that abstraction is adopted.
*
* @param context RPC dispatch context carrying `params` and the live
* `Application` reference.
* @return A JSON object, optionally containing a `"previous"` field with
* the reservation that was removed (serialized by
* `PeerReservation::toJson()`). Returns an empty object `{}` when no
* reservation existed for the given key.
*/
json::Value
doPeerReservationsDel(RPC::JsonContext& context)
{
auto const& params = context.params;
// We repeat much of the parameter parsing from `doPeerReservationsAdd`.
if (!params.isMember(jss::public_key))
return RPC::missingFieldError(jss::public_key);
if (!params[jss::public_key].isString())

View File

@@ -6,12 +6,27 @@
namespace xrpl {
/** Return a snapshot of all configured peer reservations (ADMIN).
*
* Accepts no input parameters. Delegates to `PeerReservationTable::list()`,
* which performs a mutex-guarded copy of the internal unordered set and
* returns it sorted, guaranteeing deterministic output on every call.
* Each reservation is serialized via `PeerReservation::toJson()`, which
* encodes the node public key as a base58 string and omits the
* `description` field when empty.
*
* An empty reservation table produces `{"reservations": []}` rather than
* an error, because `PeerReservationTable::load()` always succeeds even
* when the backing SQLite table has no rows.
*
* @param context RPC dispatch context; `context.params` is not read.
* @return JSON object of the form `{"reservations": [...]}` where each
* element is a `PeerReservation` serialized by `toJson()`.
*/
json::Value
doPeerReservationsList(RPC::JsonContext& context)
{
auto const& reservations = context.app.getPeerReservations().list();
// Enumerate the reservations in context.app.getPeerReservations()
// as a json::Value.
json::Value result{json::ValueType::Object};
json::Value& jaReservations = result[jss::reservations] = json::ValueType::Array;
for (auto const& reservation : reservations)

View File

@@ -1,3 +1,11 @@
/** @file
* Implements the `peers` admin RPC handler.
*
* Aggregates two distinct views of the network into a single JSON response:
* the real-time overlay snapshot (every TCP-connected peer) and the cluster
* status table (trusted cluster members with their self-reported load).
*/
#include <xrpld/app/main/Application.h>
#include <xrpld/core/TimeKeeper.h>
#include <xrpld/overlay/Cluster.h>
@@ -16,6 +24,35 @@
namespace xrpl {
/** Respond to the `peers` admin RPC command.
*
* Builds a JSON object with two top-level keys:
*
* - `peers`: snapshot of every active overlay connection, sourced from
* `Overlay::json()`. Each entry carries a `track` field whose value
* is `"diverged"` (peer is on a different chain) or `"unknown"`
* (consensus not yet established); converged peers omit the field.
*
* - `cluster`: one entry per configured cluster member (keyed by
* base58-encoded node public key), excluding the local node. Each
* entry may carry:
* - `tag` — human-readable name from `rippled.cfg`, omitted when empty.
* - `fee` — load fee as a floating-point multiplier over `loadBase`,
* omitted when equal to `loadBase` or zero (i.e., normal load).
* - `age` — seconds since the member last broadcast its status,
* omitted when the member has never reported; clamped to 0 on
* clock-skew (reportTime >= now).
*
* **API v1 compatibility**: when `context.apiVersion == 1` the handler
* post-processes the peers array in-place, injecting a `sanity` field
* alongside each `track` field: `"diverged"` → `"insane"`,
* `"unknown"` → `"unknown"`. Converged peers (no `track` field) receive
* no `sanity` annotation under either version.
*
* @param context RPC dispatch context providing access to `Overlay`,
* `Cluster`, `TimeKeeper`, and `LoadFeeTrack`.
* @return JSON object with `peers` array and `cluster` object.
*/
json::Value
doPeers(RPC::JsonContext& context)
{
@@ -23,7 +60,6 @@ doPeers(RPC::JsonContext& context)
jvResult[jss::peers] = context.app.getOverlay().json();
// Legacy support
if (context.apiVersion == 1)
{
for (auto& p : jvResult[jss::peers])

View File

@@ -11,6 +11,43 @@
namespace xrpl {
/** Implements the `ledger_accept` admin RPC command.
*
* Forces the node to close the current open ledger and advance to the next
* one without waiting for peer votes or the normal consensus timer. This is
* only valid in standalone mode, where no validator peers participate and
* ledger progression must be triggered on demand — for example, by the
* `jtx` integration-test harness between transaction submissions.
*
* If the node is not in standalone mode the response contains
* `{"error": "notStandAlone"}` and no state is mutated.
*
* When standalone mode is confirmed the handler acquires
* `Application::getMasterMutex()` (a recursive mutex that serialises all
* open-ledger and consensus-engine mutations) before delegating to
* `NetworkOPs::acceptLedger()`. That method drives a synthetic consensus
* round via `mConsensus.simulate()`, bypassing real peer participation and
* producing a fully validated ledger as if consensus had concluded normally,
* including fee adjustments and any pending open-ledger transactions.
*
* The response on success contains a single field `ledger_current_index`
* holding the sequence number of the ledger now open for new transactions
* (one past the ledger just closed). Test frameworks use this value to
* anchor subsequent state queries with confidence that the triggered
* transactions have been processed.
*
* @note This handler accepts no JSON parameters beyond the implicit RPC
* envelope; validation here is purely a mode-enforcement check rather
* than input sanitisation.
* @note Registered in Handler.cpp with `Role::ADMIN` and
* `Condition::NeedsCurrentLedger`, so it is unreachable for
* non-admin connections and when the ledger is too stale.
*
* @param context The RPC dispatch context providing app services, config,
* `netOps`, and `ledgerMaster`.
* @return A JSON object with `ledger_current_index` on success, or
* `{"error": "notStandAlone"}` when not in standalone mode.
*/
json::Value
doLedgerAccept(RPC::JsonContext& context)
{

View File

@@ -10,6 +10,31 @@ namespace RPC {
struct JsonContext;
} // namespace RPC
/** Implements the `stop` admin RPC command.
*
* Signals the application to begin a graceful shutdown, then immediately
* returns a confirmation message to the caller. The shutdown itself proceeds
* asynchronously after this handler returns — the response is queued for
* delivery before teardown begins, so the caller reliably receives
* `"<name> server stopping"` even though the server is about to exit.
*
* The delegate, `Application::signalStop("RPC")`, sets a C++20
* `std::atomic_flag` exactly once (idempotent under concurrent callers) and
* unblocks the `run()` loop. The `"RPC"` reason string is written to the
* warning log, letting operators distinguish an administrative RPC shutdown
* from a SIGTERM or an internal fault condition.
*
* Permission enforcement is handled entirely by the dispatch layer:
* the handler is registered with `Role::ADMIN` and `NO_CONDITION`, so it is
* unreachable for non-admin connections and does not require the server to
* hold a current ledger — a node that is still syncing can be stopped via
* RPC just as readily as a fully-synced one.
*
* @param context The RPC dispatch context providing access to the
* `Application` instance whose `signalStop()` is invoked.
* @return A JSON object with a `"message"` field containing
* `"<systemName> server stopping"`.
*/
json::Value
doStop(RPC::JsonContext& context)
{

View File

@@ -23,12 +23,51 @@
namespace xrpl {
// {
// secret_key: <signing_secret_key>
// key_type: optional; either ed25519 or secp256k1 (default to secp256k1)
// channel_id: 256-bit channel id
// drops: 64-bit uint (as string)
// }
/** Sign a payment channel authorization (`channel_authorize`).
*
* Produces a cryptographic signature that a channel recipient can present
* in a `PaymentChannelClaim` transaction to redeem up to `amount` drops
* without requiring the channel owner to be online at redemption time.
*
* The signed payload is constructed by `serializePayChanAuthorization`:
* `HashPrefix::paymentChannelClaim` (4-byte domain separator `'CLM\0'`) +
* 32-byte channel ID + 8-byte drops in big-endian. The domain separator
* prevents the signature from being reinterpreted as authorization for any
* other XRPL data structure. The resulting binary signature is returned as a
* hex string in `"signature"`.
*
* **Access control:** The handler is restricted to `Role::ADMIN` callers
* or nodes with `[signing_support]` enabled in their configuration. Public
* nodes reject this call by default to avoid exposing key material on
* internet-facing endpoints.
*
* **Key material:** Accepts `secret`, `passphrase`, `seed`, or `seed_hex`,
* optionally paired with `key_type` (`"secp256k1"` or `"ed25519"`). When
* `key_type` is absent and `secret` is also absent, a `missingFieldError`
* for `secret` is returned immediately, providing a clear error for legacy
* clients that omit both fields. When `key_type` is present, `secret` alone
* is rejected — the caller must use one of the explicit seed encodings.
*
* **Amount encoding:** `amount` must be a decimal-integer string (not a JSON
* number) to avoid precision loss for values that exceed JavaScript's safe
* integer range (~10^17 drops ≈ max XRP supply).
*
* @param context RPC dispatch context carrying `params`, `role`, and `app`.
* @return A JSON object with key `"signature"` containing the hex-encoded
* signature, or an error object on any validation failure.
*
* @note Required params: `channel_id` (64-hex-char uint256), `amount`
* (decimal string, drops), and at least one of `secret` / `key_type`.
* Optional: `key_type` (`"secp256k1"` default, or `"ed25519"`),
* `passphrase`, `seed`, `seed_hex`.
* @note Error codes: `rpcNOT_SUPPORTED` (signing disabled), `missingField`
* (missing required param), `rpcCHANNEL_MALFORMED` (bad channel ID),
* `rpcCHANNEL_AMT_MALFORMED` (non-string or unparseable amount),
* `rpcINTERNAL` (unexpected exception from `sign()`; unreachable under
* normal conditions — excluded from coverage via `LCOV_EXCL`).
* @see doChannelVerify, serializePayChanAuthorization,
* RPC::keypairForSignature
*/
json::Value
doChannelAuthorize(RPC::JsonContext& context)
{
@@ -44,9 +83,8 @@ doChannelAuthorize(RPC::JsonContext& context)
return RPC::missingFieldError(p);
}
// Compatibility if a key type isn't specified. If it is, the
// keypairForSignature code will validate parameters and return
// the appropriate error.
// Early guard for legacy clients that supply neither field; when key_type
// is present, keypairForSignature handles the error itself.
if (!params.isMember(jss::key_type) && !params.isMember(jss::secret))
return RPC::missingFieldError(jss::secret);

View File

@@ -10,10 +10,37 @@
namespace xrpl {
// {
// tx_json: <object>,
// secret: <secret>
// }
/** Sign a transaction and return the signed blob without submitting it.
*
* Implements the `sign` JSON-RPC command. Enforces the server's signing
* access policy, tags the request as a heavy resource burden, then delegates
* all cryptographic and structural work — key derivation, field auto-fill
* (`Fee`, `Sequence`), serialization, and ECDSA/Ed25519 signing — to
* `RPC::transactionSign`. The response always carries a `deprecated` field
* steering callers toward client-side signing tools.
*
* Access is granted when either condition holds:
* - The caller holds `Role::ADMIN` (trusted local/credentialed connection), or
* - The server is explicitly configured with `[signing_support] = 1`.
* Both failing returns `rpcNOT_SUPPORTED`. The default configuration denies
* non-admin callers, so a server inadvertently exposed to the internet will
* not act as a signing oracle.
*
* @param context RPC dispatch envelope carrying `params` (must include
* `tx_json` and a key field: `secret`, `seed`, `seed_hex`, or
* `passphrase`), the negotiated API version, the caller's role, and
* a reference to the application.
* @return JSON object containing `tx_blob` and `tx_json` on success, or an
* error object on failure. A `deprecated` field is always present.
* @note `fail_hard` is read defensively via `isMember()` before `asBool()`
* to handle its absence safely; the flag is forwarded to
* `transactionSign` where it governs signing-pipeline error treatment.
* @note Signing is rejected if the most recently validated ledger is older
* than `Tuning::kMAX_VALIDATED_LEDGER_AGE` (2 minutes), preventing
* transactions from being built against stale ledger state.
* @see RPC::transactionSign
* @see doSignFor
*/
json::Value
doSign(RPC::JsonContext& context)
{

View File

@@ -10,11 +10,40 @@
namespace xrpl {
// {
// tx_json: <object>,
// account: <signing account>
// secret: <secret of signing account>
// }
/** Contribute one co-signer's signature to a multi-signed transaction.
*
* Implements the `sign_for` JSON-RPC command. Enforces the server's signing
* access policy, tags the request as a heavy resource burden, then delegates
* all cryptographic work — key derivation, signature computation over the
* serialized transaction, and injection of the resulting `Signer` entry into
* the `Signers` array — to `RPC::transactionSignFor`. The response always
* carries a `deprecated` field steering callers toward client-side tools.
*
* Access is granted when either condition holds:
* - The caller holds `Role::ADMIN` (trusted local/credentialed connection), or
* - The server is explicitly configured with `[signing_support] = 1`.
* Both failing returns `rpcNOT_SUPPORTED`. This two-level design separates
* routing permission (set in the handler table as `Role::USER`) from feature
* permission (enforced here), so public nodes never act as signing oracles
* even if inadvertently exposed to the internet.
*
* @param context RPC dispatch envelope carrying `params` (must include
* `tx_json`, `account` identifying whose signature slot is being filled,
* and a key field: `secret`, `seed`, `seed_hex`, or `passphrase`),
* the negotiated API version, the caller's role, and a reference to
* the application.
* @return JSON object containing the updated `tx_json` (with the new `Signer`
* entry appended) and `tx_blob` on success, or an error object on
* failure. A `deprecated` field is always present on success.
* @note Unlike `doSign`, `fail_hard` is read via `asBool()` directly without
* an `isMember()` guard. `Json::Value` returns `false` for absent fields,
* so the result is correct, but the inconsistency with the sibling
* handler is worth noting for future maintenance.
* @note `tx_json` must carry pre-filled `Sequence` and `SigningPubKey` fields;
* `transactionSignFor` does not auto-fill them, unlike single-signing.
* @see RPC::transactionSignFor
* @see doSign
*/
json::Value
doSignFor(RPC::JsonContext& context)
{

View File

@@ -1,3 +1,12 @@
/** @file
* Implements the `consensus_info` admin RPC handler.
*
* Registered in Handler.cpp as `Role::ADMIN` with `NO_CONDITION`, so the
* handler runs regardless of sync state and is unreachable by non-admin
* connections. The command accepts no parameters; any extra arguments are
* rejected by the RPC framework before dispatch.
*/
#include <xrpld/rpc/Context.h>
#include <xrpl/json/json_value.h>
@@ -6,6 +15,17 @@
namespace xrpl {
/** Snapshot the live consensus engine state for an admin caller.
*
* Delegates to `NetworkOPs::getConsensusInfo()`, which calls
* `Consensus::getJson(true)` — the verbose path that includes the current
* consensus phase, dispute sets, proposal counts, and timing fields in
* addition to the summary fields present in the non-verbose form.
*
* @param context RPC dispatch envelope carrying the `NetworkOPs` reference.
* @return JSON object with a single `"info"` key whose value is the verbose
* consensus snapshot.
*/
json::Value
doConsensusInfo(RPC::JsonContext& context)
{

View File

@@ -1,3 +1,11 @@
/** @file
* Handler for the `fetch_info` admin RPC command.
*
* Exposes the internal state of the inbound ledger fetch subsystem and
* optionally clears recorded fetch failures, allowing operators to diagnose
* and recover from stalled ledger acquisition.
*/
#include <xrpld/rpc/Context.h>
#include <xrpl/json/json_value.h>
@@ -6,6 +14,31 @@
namespace xrpl {
/** Handle the `fetch_info` admin RPC command.
*
* Returns a snapshot of the inbound ledger fetch subsystem's current state.
* If the `"clear"` parameter is present and true, failed-fetch records are
* wiped first via `InboundLedgers::clearFailures()` so that previously
* abandoned ledgers become eligible for re-acquisition. The snapshot
* returned under `"info"` always reflects the post-clear state, making a
* single `{"clear": true}` call both an action and a confirmation.
*
* Registered in `Handler.cpp` as `"fetch_info"` with `Role::ADMIN` and
* `Condition::NoCondition`.
*
* @param context RPC dispatch envelope; `context.params` may contain an
* optional boolean `"clear"` field. `context.netOps` is the
* `NetworkOPs` abstraction through which the fetch subsystem is reached.
* @return A JSON object with:
* - `"clear"` (boolean, present only when the clear action was taken)
* — echoes `true` to confirm that failed-fetch records were wiped.
* - `"info"` (object) — the fetch-state snapshot from
* `InboundLedgers::getInfo()`.
* @note The `"clear"` field is read with an `isMember` guard before
* `asBool()` to avoid a type-coercion exception when the field is
* absent; unexpected non-boolean values are handled by `Json::Value`'s
* built-in coercion, consistent with sibling handlers in this directory.
*/
json::Value
doFetchInfo(RPC::JsonContext& context)
{

View File

@@ -1,3 +1,11 @@
/** @file
* Implements the `get_counts` admin RPC command.
*
* Provides a single-call snapshot of runtime health: live object reference
* counts, relational database disk usage, multiple cache hit rates, node
* store write pressure, and formatted uptime.
*/
#include <xrpld/app/ledger/InboundLedgers.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/main/Application.h>
@@ -17,6 +25,24 @@
namespace xrpl {
/** Append one time unit to an uptime string and consume it from the remainder.
*
* Divides `seconds` by `unitVal`, appends the count and unit name to `text`
* (with a comma separator if `text` is non-empty and an "s" plural suffix
* when count > 1), then subtracts the consumed duration from `seconds`.
* Does nothing when the unit count is zero, so units with no contribution
* are silently omitted from the output.
*
* @param text Accumulator string for the formatted uptime result.
* @param seconds Remaining uptime; modified in place by subtracting the
* portion consumed by this unit.
* @param unitName Singular name of the time unit (e.g. "hour").
* @param unitVal Duration value of one unit (e.g. `std::chrono::hours{1}`).
*
* @note This function is intended to be called in descending unit order
* (years → days → hours → minutes → seconds). Each call strips the
* largest remaining unit, so later calls never double-count earlier ones.
*/
static void
textTime(
std::string& text,
@@ -42,6 +68,31 @@ textTime(
text += "s";
}
/** Aggregate runtime diagnostics and cache statistics into a JSON object.
*
* Collects and returns a snapshot of the node's internal state, including:
* - Live object reference counts from `CountedObjects` (filtered by
* `minObjectCount` to suppress low-population types);
* - Relational database disk usage (`dbKBTotal`, `dbKBLedger`,
* `dbKBTransaction`) and local transaction queue depth, when
* `config().useTxTables()` is enabled;
* - Node store write pressure (`write_load`) and historical ledger fetch
* rate (`historical_perminute`);
* - Cache hit rates for SLEs, the ledger cache, and the `AcceptedLedger`
* cache, plus `NodeFamily` SHAMap tree-node cache sizes;
* - Formatted human-readable uptime string;
* - Additional node store counters appended by
* `NodeStore::Database::getCountsJson`.
*
* This function is factored out of the RPC handler so it can be called by
* non-RPC paths (e.g. `OverlayImpl::getServerCounts` for the `/crawl`
* endpoint) without constructing a fake `RPC::JsonContext`.
*
* @param app The running application instance.
* @param minObjectCount Minimum live-instance threshold for including an
* object type in the output. Types with fewer live instances are omitted.
* @return JSON object containing the aggregated diagnostic fields.
*/
json::Value
getCountsJson(Application& app, int minObjectCount)
{
@@ -107,9 +158,19 @@ getCountsJson(Application& app, int minObjectCount)
return ret;
}
// {
// min_count: <number> // optional, defaults to 10
// }
/** Handle the `get_counts` admin RPC request.
*
* Reads the optional `min_count` parameter (default: 10) and delegates to
* `getCountsJson`. The default of 10 acts as a noise filter, suppressing
* object types with very few live instances that are rarely relevant in
* production diagnostics.
*
* @param context RPC dispatch context; `context.params` may contain
* `min_count` (unsigned integer). A missing, non-numeric, or negative
* value silently coerces to 0, causing all object types to be reported.
* @return JSON object of aggregated runtime diagnostics.
* @see getCountsJson
*/
json::Value
doGetCounts(RPC::JsonContext& context)
{

View File

@@ -1,9 +1,49 @@
/** @file
* Declares `getCountsJson`, the shared diagnostic snapshot function for the
* `get_counts` admin RPC command and the `/crawl` overlay endpoint.
*
* The function is factored out of `GetCounts.cpp` so that non-RPC subsystems
* (specifically `OverlayImpl::getServerCounts`) can obtain the same runtime
* health payload without depending on `RPC::JsonContext` or the full RPC
* dispatch machinery — only on `Application&`.
*/
#pragma once
#include <xrpld/app/main/Application.h>
namespace xrpl {
/** Aggregate runtime diagnostics and cache statistics into a JSON object.
*
* Returns a broad snapshot of the node's internal health, including:
* - Live object reference counts from `CountedObjects` (types with fewer
* than `minObjectCount` live instances are omitted to reduce noise);
* - Relational database disk usage (`dbKBTotal`, `dbKBLedger`,
* `dbKBTransaction`) and local transaction queue depth (`local_txs`) — only
* present when `config().useTxTables()` is enabled;
* - Node store write pressure (`write_load`) and historical ledger fetch
* rate (`historical_perminute`);
* - Cache hit rates for SLEs, the ledger cache, and `AcceptedLedger`, plus
* `NodeFamily` SHAMap tree-node cache sizes;
* - Human-readable formatted uptime string;
* - Additional counters appended by `NodeStore::Database::getCountsJson`.
*
* This function is the shared implementation used by both the `doGetCounts`
* RPC handler (default `minObjectCount` of 10, overridable via `min_count`)
* and `OverlayImpl::getServerCounts` (hard-coded threshold of 10) for the
* `/crawl` endpoint, without requiring either caller to construct a fake
* `RPC::JsonContext`.
*
* @param app The running application instance.
* @param minObjectCount Minimum live-instance threshold for including a
* tracked object type in the output. Pass 0 to report all types.
* @return JSON object containing the aggregated diagnostic fields.
*
* @note `local_txs` and the `dbKB*` fields are absent when
* `config().useTxTables()` is false (e.g. reporting nodes that do not
* maintain a transaction database).
*/
json::Value
getCountsJson(Application& app, int minObjectCount);

View File

@@ -1,3 +1,15 @@
/** @file
* Admin RPC handler that serializes the application's `beast::PropertyStream`
* tree to JSON.
*
* `Application` is the root of a named, hierarchical tree of diagnostic
* sources — every major subsystem registers itself as a child. `doPrint`
* drives a `JsonPropertyStream` sink over that tree to produce a runtime
* introspection snapshot. New subsystems that register as
* `PropertyStream::Source` children are automatically included without any
* change to this handler.
*/
#include <xrpld/app/main/Application.h>
#include <xrpld/rpc/Context.h>
@@ -7,6 +19,29 @@
namespace xrpl {
/** Serialize the application's full `beast::PropertyStream` tree to JSON.
*
* Drives a `JsonPropertyStream` sink over the `Application` property-stream
* tree and returns the resulting JSON object as the RPC response. An optional
* dot-delimited path filter may be supplied via
* `params[0]` (e.g. `"ledgermaster"` or `"consensus.*"`) to target a
* specific named sub-source; if the path ends with `*` the subtree is dumped
* recursively. If the parameter is absent or malformed the entire tree is
* returned.
*
* Serialization safety is provided by `beast::PropertyStream::Source::write()`,
* which acquires a `std::recursive_mutex` on each source node before
* iterating its children. This handler acquires no additional locks.
*
* @param context RPC dispatch context; `context.app` is the stream root.
* An optional path filter is read from
* `context.params[jss::params][0u]` as a string; all other shapes are
* silently ignored and the full tree is serialized instead.
* @return JSON object containing the serialized property-stream tree (or
* the targeted sub-tree when a valid path filter is provided).
* @note This handler always succeeds — a malformed or mistyped path filter
* produces a full-tree dump rather than an error response.
*/
json::Value
doPrint(RPC::JsonContext& context)
{

Some files were not shown because too many files have changed in this diff Show More