mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 00:36:48 +00:00
part 3
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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** (1–17), 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()
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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, 101–255 = 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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 12–14
|
||||
* 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 12–14), 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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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-creation–specific `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>
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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 10–15× 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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 0–1 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 0–1 (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 2–3 (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 (0–50000).
|
||||
*/
|
||||
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 28–31 (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 24–27 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 24–27 (big-endian) to obtain the stored ciphered taxon, then
|
||||
* deciphers it by calling `cipheredTaxon()` a second time with the serial
|
||||
* number from bytes 28–31. 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 4–23 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)
|
||||
{
|
||||
|
||||
@@ -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"));
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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` — 10–15× 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
|
||||
/** 10–15× 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 |
|
||||
* |-------|----------------------------------|
|
||||
* | 0–1 | Flags (2 bytes, big-endian) |
|
||||
* | 2–3 | Transfer fee (2 bytes, big-endian)|
|
||||
* | 4–23 | Issuer `AccountID` (20 bytes) |
|
||||
* | 24–27 | Ciphered taxon (4 bytes, big-endian) |
|
||||
* | 28–31 | 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 (0–50,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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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&,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}();
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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&);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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] =
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user