Compare commits

..

1 Commits

Author SHA1 Message Date
Denis Angell
88794a1ea9 docs: add Doxygen comments across xrpl and xrpld
Bulk documentation pass covering 702 C++ source files in src/libxrpl, src/xrpld, and
include/xrpl. Adds class, function, parameter, and invariant docs per
docs/DOCUMENTATION_STANDARDS.md.

Squashed from the original three-part series (part 1 / part 2 / part 3) to avoid
merge-conflict noise when rebasing the work onto current develop.
2026-05-14 10:20:15 +02:00
705 changed files with 91287 additions and 15213 deletions

View File

@@ -7,12 +7,48 @@
namespace xrpl {
/** Read the entire contents of a file into a string.
*
* Resolves `sourcePath` to its canonical (absolute, symlink-free) form before
* opening it, which prevents TOCTOU races between path resolution and the open.
* When `maxSize` is supplied and the file exceeds that byte count, `ec` is set
* to `boost::system::errc::file_too_large` and an empty string is returned
* without reading any data.
*
* All errors — non-existent path, permission denial, size exceeded, open
* failure, and mid-read I/O error — are reported through `ec`. The function
* never throws.
*
* @param ec Output error code; set on any failure, left unchanged on success.
* @param sourcePath Path to the file to read; must exist and be resolvable.
* @param maxSize Optional upper bound on file size in bytes. If the file is
* larger, `ec` is set to `errc::file_too_large` and `{}` is returned.
* @return The full file contents on success, or an empty string on any error.
* @note EOF during the single-pass read is not an error; only `bad()` (hardware
* or stream-corruption failure) triggers an error code after the read.
*/
std::string
getFileContents(
boost::system::error_code& ec,
boost::filesystem::path const& sourcePath,
std::optional<std::size_t> maxSize = std::nullopt);
/** Write a string to a file, creating or truncating it as necessary.
*
* Opens `destPath` with `std::ios::out | std::ios::trunc`, so any existing
* content is discarded and the file is created if it does not yet exist.
* This is a full replacement, not an atomic rename-and-swap; callers that
* require crash-safe writes must implement that at a higher level.
*
* All errors — open failure and mid-write I/O error — are reported through
* `ec`. The function never throws.
*
* @param ec Output error code; set on any failure, left unchanged on success.
* @param destPath Path to the destination file; parent directory must exist.
* @param contents Data to write; written in a single `<<` operation.
* @note Unlike `getFileContents`, this function does not call `canonical()`
* because the destination file may not yet exist.
*/
void
writeFileContents(
boost::system::error_code& ec,

View File

@@ -1,3 +1,18 @@
/** @file
* RFC 1751 mnemonic encoding for 128-bit XRPL wallet seeds.
*
* Declares the `RFC1751` utility class, which encodes and decodes 128-bit
* binary keys as sequences of short English words drawn from a 2048-word
* dictionary. Each word represents exactly 11 bits; a 64-bit block maps
* to 6 words (64 data bits + 2 parity bits = 66 bits). A full 128-bit key
* therefore encodes as 12 words in two back-to-back 6-word groups.
*
* The primary consumer is `Seed.cpp`, which uses the codec to produce
* human-readable wallet seed mnemonics. `NetworkOPs.cpp` also uses
* `getWordFromBlob` to derive a stable short label for the local node's
* public key in log output.
*/
#pragma once
#include <string>
@@ -5,39 +20,183 @@
namespace xrpl {
/** XRPL adaptation of the RFC 1751 128-bit mnemonic key codec.
*
* Converts 128-bit binary keys to and from sequences of 12 English words
* using a fixed 2048-word dictionary. The dictionary is split at index 571:
* words 0570 have 13 characters; words 5712047 are all exactly 4
* characters. This property is exploited internally to halve binary-search
* range during decoding.
*
* All methods are static; this class is a pure stateless namespace and is
* never instantiated.
*
* @note `Seed.cpp` reverses the 16 seed bytes before passing them to
* `getEnglishFromKey` and after receiving them from `getKeyFromEnglish`
* to satisfy the RFC's big-endian byte-order convention.
*/
class RFC1751
{
public:
/** Decode a 12-word mnemonic string into a 128-bit binary key.
*
* Splits @p strHuman on whitespace (multiple spaces are collapsed),
* validates and normalises each word via `standard()`, looks each one
* up in the dictionary, and packs the resulting 11-bit indices into two
* 8-byte binary halves. Each half carries a 2-bit parity check computed
* from the 64 data bits; the decode fails with `-2` if the recomputed
* parity does not match.
*
* @param strKey Output parameter; set to the 16-byte binary key on
* success. Unchanged on any failure return.
* @param strHuman 12 space-separated words to decode. Leading and
* trailing whitespace is trimmed before splitting.
* @return 1 success — @p strKey holds the decoded 16-byte key.
* @return 0 a word was not found in the dictionary.
* @return -1 malformed input: word count ≠ 12, or a word exceeds 4
* characters.
* @return -2 all words are valid but the 2-bit parity check failed,
* indicating a transcription error.
*
* @note The four distinct return codes must not be collapsed; `-2`
* (parity failure) implies the words themselves were individually
* valid and is a different diagnostic than `0` (unknown word).
*/
static int
getKeyFromEnglish(std::string& strKey, std::string const& strHuman);
/** Encode a 128-bit binary key as 12 space-separated English words.
*
* Encodes the first 8 bytes of @p strKey as 6 words and the next 8
* bytes as a further 6 words, then joins the two groups with a single
* space. A 2-bit parity value is appended to each 64-bit block before
* encoding to support transcription-error detection on decode.
*
* Encoding is lossless and cannot fail for valid 16-byte input; no
* return code is needed.
*
* @param strHuman Output parameter; receives the 12-word mnemonic string.
* @param strKey The 16-byte (128-bit) binary key to encode. Behaviour
* is undefined if fewer than 16 bytes are provided.
*/
static void
getEnglishFromKey(std::string& strHuman, std::string const& strKey);
/** Chooses a single dictionary word from the data.
This is not particularly secure but it can be useful to provide
a unique name for something given a GUID or fixed data. We use
it to turn the pubkey_node into an easily remembered and identified
4 character string.
*/
/** Map arbitrary binary data to a single dictionary word.
*
* Applies the Jenkins one-at-a-time hash to the input bytes, then
* indexes into the 2048-word dictionary using the hash modulo 2048.
* The result is a stable, reproducible label for the input data.
*
* @param blob Pointer to the input data.
* @param bytes Number of bytes to hash.
* @return A single uppercase dictionary word of 14 characters.
*
* @note This function is **not** cryptographically secure. It is
* intended only for producing human-readable identifiers, such as
* the `shroudedHostId` label derived from a node's public key in
* `NetworkOPs.cpp`.
*/
static std::string
getWordFromBlob(void const* blob, size_t bytes);
private:
/** Read up to 11 bits from a byte array at an arbitrary bit offset.
*
* Assembles up to 3 adjacent bytes into a 24-bit window, shifts right
* to align the target field, and masks to @p length bits. Works across
* byte boundaries. The output buffer for the 66-bit block (64 data +
* 2 parity) must be at least 9 bytes.
*
* @param s Source byte array (at least ⌈(start + length) / 8⌉ + 1
* bytes long; 9 bytes for the full 66-bit block).
* @param start First bit to read (0-based). Must be ≥ 0.
* @param length Number of bits to read. Must satisfy 0 ≤ length ≤ 11
* and start + length ≤ 66.
* @return The extracted value, right-justified and zero-extended.
*/
static unsigned long
extract(char const* s, int start, int length);
/** Encode an 8-byte binary block as 6 space-separated dictionary words.
*
* Appends a 9th byte carrying a 2-bit parity value (sum of all 32
* two-bit pairs in the 64-bit payload, placed at bits 6465), then
* calls `extract()` at six 11-bit offsets to obtain dictionary indices.
*
* @param strHuman Output; receives the 6-word space-separated string.
* @param strData Exactly 8 bytes of binary data to encode.
*/
static void
btoe(std::string& strHuman, std::string const& strData);
/** Write up to 11 bits into a byte array at an arbitrary bit offset.
*
* ORs the bit field into the target bytes; the output buffer must be
* zero-initialised before the first call because this function
* accumulates bits with bitwise OR rather than assignment.
*
* @param s Target byte array (must be zero-initialised).
* @param x Value to insert (only the low @p length bits are used).
* @param start First destination bit (0-based). Must be ≥ 0.
* @param length Number of bits to write. Must satisfy 0 ≤ length ≤ 11
* and start + length ≤ 66.
*/
static void
insert(char* s, int x, int start, int length);
/** Normalise a mnemonic word for dictionary lookup.
*
* Applies three in-place transformations to tolerate common
* handwriting and OCR ambiguities: lowercased letters are uppercased,
* `'1'` is replaced by `'L'`, `'0'` by `'O'`, and `'5'` by `'S'`.
*
* @param strWord Word to normalise in place.
*/
static void
standard(std::string& strWord);
/** Binary-search the dictionary within a given index range.
*
* The dictionary is sorted, and its first 571 entries (indices 0570)
* are words of 13 characters while the remaining 1477 (indices
* 5712047) are all exactly 4 characters. Callers restrict the range
* based on word length to halve the search space.
*
* @param strWord Word to search for (must already be normalised via
* `standard()`).
* @param iMin Inclusive lower bound of the search range.
* @param iMax Exclusive upper bound of the search range.
* @return The dictionary index of @p strWord, or -1 if not found.
*/
static int
wsrch(std::string const& strWord, int iMin, int iMax);
/** Decode 6 mnemonic words into an 8-byte binary block.
*
* Normalises each word, looks it up via `wsrch()`, packs the resulting
* 11-bit indices into a 9-byte buffer using `insert()`, then validates
* the 2-bit parity stored at bit offset 64.
*
* @param strData Output; receives the 8 decoded data bytes on success.
* Unchanged on any failure return.
* @param vsHuman Exactly 6 words to decode. Returns -1 immediately
* if the vector does not contain exactly 6 elements, or if any
* word is longer than 4 characters.
* @return 1 success.
* @return 0 a word was not found in the dictionary.
* @return -1 wrong word count or word exceeds 4 characters.
* @return -2 parity mismatch.
*/
static int
etob(std::string& strData, std::vector<std::string> vsHuman);
/** The 2048-word mnemonic dictionary, sorted ascending.
*
* Indices 0570 contain words of 13 characters; indices 5712047
* contain words of exactly 4 characters. This structural split is
* relied upon by `wsrch()` to restrict binary-search ranges.
*/
static char const* dictionary[];
};

View File

@@ -4,14 +4,38 @@
namespace xrpl {
/** A cryptographically secure random number engine
/** @file
* Cryptographically secure pseudo-random number engine and singleton accessor.
*
* Every piece of key material in the XRP Ledger — wallet seeds, secret keys,
* nonces, session identifiers — is generated through `CsprngEngine`. The class
* is a thin, type-safe C++ wrapper around OpenSSL's `RAND_bytes` that provides
* thread safety and satisfies the C++ *UniformRandomNumberEngine* named
* requirement, allowing it to be used directly with standard-library facilities
* such as `std::uniform_int_distribution` and `beast::rngfill`.
*/
The engine is thread-safe (it uses a lock to serialize
access) and will, automatically, mix in some randomness
from std::random_device.
Meets the requirements of UniformRandomNumberEngine
*/
/** Cryptographically secure random number engine backed by OpenSSL.
*
* Wraps OpenSSL's `RAND_bytes` to provide randomness to the rest of the
* codebase without any caller needing to touch OpenSSL directly. Satisfies
* the C++ *UniformRandomNumberEngine* named requirement (`result_type`,
* `operator()()`, `min()`, `max()`), so it plugs directly into
* `std::uniform_int_distribution`, `beast::rngfill`, and similar utilities.
*
* Thread safety is version-conditioned at compile time: on OpenSSL ≥ 1.1.0
* built with thread support, `RAND_bytes` is internally thread-safe and the
* per-call mutex acquisition is elided on the hot path. On older OpenSSL the
* mutex is always held. Entropy mixing (`mixEntropy`) always holds the mutex
* regardless of OpenSSL version because `RAND_add` modifies shared pool state.
*
* Copy and move operations are deleted. The engine holds a `std::mutex`, is
* backed by a global OpenSSL PRNG pool, and must be accessed as a singleton.
* Copying would produce a second object with no coherent relationship to that
* shared state. Use `cryptoPrng()` to obtain the singleton reference.
*
* @see cryptoPrng()
*/
class CsprngEngine
{
private:
@@ -28,29 +52,92 @@ public:
CsprngEngine&
operator=(CsprngEngine&&) = delete;
/** Construct and eagerly seed the engine.
*
* Calls `RAND_poll()` to harvest OS entropy (e.g., `/dev/urandom` on
* Linux, `CryptGenRandom` on Windows) before any bytes are generated.
* Although OpenSSL seeds itself lazily on first use, polling eagerly
* surfaces seeding failures at startup rather than during key generation.
*
* @throw std::runtime_error if `RAND_poll()` fails.
*/
CsprngEngine();
/** Destroy the engine, releasing OpenSSL PRNG state on older runtimes.
*
* Calls `RAND_cleanup()` only for OpenSSL versions older than 1.1.0.
* Modern OpenSSL manages cleanup internally via `atexit`; calling
* `RAND_cleanup()` on those versions is unnecessary and was removed.
*/
~CsprngEngine();
/** Mix entropy into the pool */
/** Stir additional entropy into the OpenSSL random pool.
*
* Reads 128 values from `std::random_device` and passes them to
* `RAND_add` with an entropy estimate of zero. The caller-supplied
* buffer, if provided, is also added with a zero entropy estimate.
* The zero estimate is deliberate: on some platforms `std::random_device`
* may fall back to a software PRNG, so claiming zero entropy ensures
* OpenSSL's internal seeding threshold is never prematurely satisfied by
* potentially weak input. The data is still mixed into the pool.
*
* Called periodically from `Application.cpp` to stir in fresh OS entropy
* during the node's lifetime. May also be called with caller-supplied
* high-quality entropy from a hardware RNG or other trusted source.
*
* @param buffer Optional pointer to additional entropy material to mix in.
* Ignored if `nullptr` or if `count` is zero.
* @param count Number of bytes at `buffer` to mix in.
*/
void
mixEntropy(void* buffer = nullptr, std::size_t count = 0);
/** Generate a random integer */
/** Generate a single random `result_type` value.
*
* Delegates to the buffer-fill overload with `sizeof(result_type)` bytes,
* sharing the same validation and error-handling path.
*
* @return A uniformly distributed random `std::uint64_t`.
* @throw std::runtime_error if the underlying `RAND_bytes` call fails
* (e.g., entropy pool exhausted). This is an unrecoverable condition;
* the exception is not caught by callers such as `randomSecretKey()`.
*/
result_type
operator()();
/** Fill a buffer with the requested amount of random data */
/** Fill a buffer with cryptographically secure random bytes.
*
* On OpenSSL ≥ 1.1.0 (built with thread support) the call to `RAND_bytes`
* is internally thread-safe and the mutex is elided at compile time. On
* older OpenSSL the mutex is held for the duration of the call.
*
* @param ptr Pointer to the buffer to fill; must not be `nullptr` when
* `count` is non-zero.
* @param count Number of random bytes to write into `ptr`.
* @throw std::runtime_error ("CSPRNG: Insufficient entropy") if
* `RAND_bytes` returns anything other than 1. Generating key material
* from an exhausted pool is a security failure, so the exception
* propagates and halts the operation.
*/
void
operator()(void* ptr, std::size_t count);
/* The smallest possible value that can be returned */
/** Return the smallest value that `operator()()` can produce.
*
* Required by the *UniformRandomNumberEngine* named requirement.
* Always returns `std::numeric_limits<result_type>::min()`.
*/
static constexpr result_type
min()
{
return std::numeric_limits<result_type>::min();
}
/* The largest possible value that can be returned */
/** Return the largest value that `operator()()` can produce.
*
* Required by the *UniformRandomNumberEngine* named requirement.
* Always returns `std::numeric_limits<result_type>::max()`.
*/
static constexpr result_type
max()
{
@@ -58,14 +145,23 @@ public:
}
};
/** The default cryptographically secure PRNG
Use this when you need to generate random numbers or
data that will be used for encryption or passed into
cryptographic routines.
This meets the requirements of UniformRandomNumberEngine
*/
/** Return a reference to the process-wide cryptographically secure PRNG.
*
* Use this whenever random numbers or bytes are needed for cryptographic
* purposes: key generation, seed creation, nonce production, or any value
* passed into a cryptographic routine. The returned engine satisfies the
* C++ *UniformRandomNumberEngine* requirement and can be used directly with
* `std::uniform_int_distribution`, `beast::rngfill`, and similar utilities.
*
* The singleton is a Meyers-static local; C++11 guarantees thread-safe
* one-time construction, so the first call from any thread safely initialises
* the engine exactly once. Every caller shares the same OpenSSL PRNG pool.
*
* @return Reference to the process-wide `CsprngEngine` singleton.
* @note Never copy or store the returned reference by value — the deleted
* copy/move operations on `CsprngEngine` prevent this at compile time.
* @see CsprngEngine
*/
CsprngEngine&
cryptoPrng();

View File

@@ -1,23 +1,38 @@
/** @file
* Declares `xrpl::secureErase`, the canonical primitive for wiping
* sensitive key material from memory in a way that survives compiler
* dead-store elimination.
*/
#pragma once
#include <cstddef>
namespace xrpl {
/** Attempts to clear the given blob of memory.
The underlying implementation of this function takes pains to
attempt to outsmart the compiler from optimizing the clearing
away. Please note that, despite that, remnants of content may
remain floating around in memory as well as registers, caches
and more.
For a more in-depth discussion of the subject please see the
below posts by Colin Percival:
http://www.daemonology.net/blog/2014-09-04-how-to-zero-a-buffer.html
http://www.daemonology.net/blog/2014-09-06-zeroing-buffers-is-insufficient.html
*/
/** Best-effort wipe of a memory region containing sensitive data.
*
* Overwrites `bytes` bytes starting at `dest` using `OPENSSL_cleanse`,
* which employs volatile writes or memory barriers to prevent the compiler
* from eliminating the store as a dead write. The function is defined in a
* separate translation unit (`secure_erase.cpp`) so the call is always
* opaque to the optimizer at the call site, reinforcing the effect.
*
* Use this instead of `memset` whenever clearing key material, seeds, or
* derived intermediates. The canonical pattern is to call it in destructors
* and immediately after copying raw key bytes into their final owner object.
*
* @param dest Pointer to the memory region to wipe. Must not be null and
* must point to at least `bytes` bytes of writable memory.
* @param bytes Number of bytes to overwrite.
*
* @note This is a best-effort mitigation, not a guarantee of complete
* erasure. Register contents, CPU caches, and other micro-architectural
* state are outside its reach. For a thorough discussion of the
* inherent limits see Colin Percival's analysis:
* http://www.daemonology.net/blog/2014-09-04-how-to-zero-a-buffer.html
* http://www.daemonology.net/blog/2014-09-06-zeroing-buffers-is-insufficient.html
*/
void
secureErase(void* dest, std::size_t bytes);

View File

@@ -10,66 +10,133 @@
namespace xrpl {
/**
A transaction that is in a closed ledger.
Description
An accepted ledger transaction contains additional information that the
server needs to tell clients about the transaction. For example,
- The transaction in JSON form
- Which accounts are affected
* This is used by InfoSub to report to clients
- Cached stuff
*/
/** Immutable snapshot of a transaction accepted into a closed ledger.
*
* Constructed once from the closed ledger view, the serialized transaction,
* and its raw metadata; all downstream representations — JSON payload, binary
* metadata blob, affected-account set — are fully materialized at construction
* time and never recomputed.
*
* The pre-built `json_` payload is consumed directly by
* `NetworkOPsImp::pubValidatedTransaction()` and `pubAccountTransaction()`
* for WebSocket subscription delivery; `rawMeta_` (via `getEscMeta()`) feeds
* SQL `INSERT`/`REPLACE` statements in the relational transaction database.
*
* `CountedObject` inheritance exposes live-instance telemetry useful for
* detecting accumulation under load or slow subscriber drain holding ledger
* snapshots open longer than expected.
*
* @note Immutable after construction; safe to share across threads without
* additional locking.
* @note The ledger passed to the constructor must be closed (not open).
* Constructing from an open ledger aborts in debug builds.
* @see AcceptedLedger
*/
class AcceptedLedgerTx : public CountedObject<AcceptedLedgerTx>
{
public:
/** Construct and fully materialize a closed-ledger transaction snapshot.
*
* Parses metadata into a `TxMeta`, serializes raw metadata bytes, builds
* the complete JSON payload (transaction, meta, raw_meta, result, affected
* accounts), and — for non-self-funded `ttOFFER_CREATE` transactions —
* annotates the JSON with `owner_funds` queried from `accountFunds()` with
* freeze and auth checks bypassed. This avoids a later ledger round-trip
* when delivering to order-book subscribers.
*
* @param ledger The closed ledger that accepted this transaction. Must not
* be open; the constructor asserts `!ledger->open()` in debug builds.
* @param txn The serialized transaction object.
* @param met The raw metadata `STObject` produced during transaction apply.
*/
AcceptedLedgerTx(
std::shared_ptr<ReadView const> const& ledger,
std::shared_ptr<STTx const> const&,
std::shared_ptr<STObject const> const&);
/** Returns the serialized transaction. */
[[nodiscard]] std::shared_ptr<STTx const> const&
getTxn() const
{
return txn_;
}
/** Returns the parsed transaction metadata, including affected nodes and
* result code.
*/
[[nodiscard]] TxMeta const&
getMeta() const
{
return meta_;
}
/** Returns the set of accounts affected by this transaction.
*
* Stored as a `flat_set` for cache-friendly iteration during subscription
* fan-out in `pubAccountTransaction()`.
*/
[[nodiscard]] boost::container::flat_set<AccountID> const&
getAffected() const
{
return affected_;
}
/** Returns the transaction's unique identifier (SHA-512 half of the
* canonical serialization).
*/
[[nodiscard]] TxID
getTransactionID() const
{
return txn_->getTransactionID();
}
/** Returns the transaction type (e.g., `ttOFFER_CREATE`, `ttPAYMENT`). */
[[nodiscard]] TxType
getTxnType() const
{
return txn_->getTxnType();
}
/** Returns the transaction result code as recorded in metadata. */
[[nodiscard]] TER
getResult() const
{
return meta_.getResultTER();
}
/** Returns the transaction's ordinal position within the closed ledger.
*
* This is `TxMeta::getIndex()` — the transaction's sequence number within
* the ledger's ordered transaction set, not the account sequence number.
*/
[[nodiscard]] std::uint32_t
getTxnSeq() const
{
return meta_.getIndex();
}
/** Returns the raw metadata formatted as an escaped SQL blob literal.
*
* Formats `rawMeta_` via `sqlBlobLiteral()` for direct embedding in SQL
* `INSERT`/`REPLACE` statements (see `STTx::getMetaSQL()` in `Node.cpp`).
*
* @return SQL blob literal string suitable for verbatim inclusion in a
* SQL statement.
* @note Asserts that `rawMeta_` is non-empty. An empty blob indicates
* upstream ledger corruption; every accepted transaction must carry
* metadata.
*/
[[nodiscard]] std::string
getEscMeta() const;
/** Returns the pre-built JSON envelope for WebSocket subscription delivery.
*
* The object contains `transaction`, `meta`, `raw_meta` (hex), `result`
* (human-readable TER string), and `affected` (base58 account array).
* For non-self-funded `ttOFFER_CREATE` transactions, `transaction` also
* contains `owner_funds` — the account's spendable balance of the offered
* asset at acceptance time, computed with freeze and auth checks bypassed.
*/
[[nodiscard]] json::Value const&
getJson() const
{

View File

@@ -14,56 +14,172 @@ namespace xrpl {
class ServiceRegistry;
/** The amendment table stores the list of enabled and potential amendments.
Individuals amendments are voted on by validators during the consensus
process.
*/
/** Tracks enabled and pending amendments and coordinates validator voting.
*
* Each protocol change (amendment) must achieve an 80% supermajority of
* trusted validators for `majorityTime` before it activates. This class
* manages the full lifecycle: registration of supported amendments, vote
* aggregation across flag ledgers, pseudo-transaction injection at consensus
* time, and detection of "amendment blocked" conditions where the network has
* enabled a feature this node does not support.
*
* The interface is split into two layers. The pure virtual methods form the
* internal API that the concrete implementation satisfies, operating on
* pre-extracted amendment sets. Two concrete non-virtual adapter methods
* (`doValidatedLedger(shared_ptr<ReadView>)` and
* `doVoting(shared_ptr<ReadView>, ...)`) read amendment state from a
* `ReadView` and delegate to the pure-virtual overloads, keeping the
* implementation independent of the ledger view layer.
*
* @note Amendment voting is only meaningful at flag ledgers (multiples of
* 256). Use `needValidatedLedger` to gate the more expensive
* `doValidatedLedger` call.
* @see Feature.h for `VoteBehavior` and `majorityAmendments_t`
*/
class AmendmentTable
{
public:
/** Metadata for a single registered amendment.
*
* Bundles the human-readable name, canonical 256-bit hash, and compiled-in
* vote preference for one amendment. Non-default-constructible: every
* instance must carry all three fields.
*
* @note Amendments with `VoteBehavior::Obsolete` are still registered so
* the node remains amendment-unblocked if the network enables them, but
* the node will never emit votes for them and their vote behavior cannot
* be overridden by config.
*/
struct FeatureInfo
{
FeatureInfo() = delete;
/** Construct a FeatureInfo with all required fields.
*
* @param n Human-readable amendment name (e.g., "OwnerPaysFee").
* @param f Canonical 256-bit amendment hash used in ledger state and
* validations.
* @param v Compiled-in voting preference (`DefaultYes`, `DefaultNo`,
* or `Obsolete`).
*/
FeatureInfo(std::string n, uint256 const& f, VoteBehavior v)
: name(std::move(n)), feature(f), vote(v)
{
}
/** Human-readable name of the amendment. */
std::string const name;
/** Canonical 256-bit amendment identifier used throughout the ledger. */
uint256 const feature;
/** Compiled-in voting preference for this amendment. */
VoteBehavior const vote;
};
virtual ~AmendmentTable() = default;
/** Look up an amendment's 256-bit hash by its human-readable name.
*
* @param name The amendment name to look up (case-sensitive).
* @return The amendment's `uint256` hash, or a zero value if no
* amendment with that name is registered.
*/
[[nodiscard]] virtual uint256
find(std::string const& name) const = 0;
/** Suppress this node's vote for an amendment.
*
* Changes the amendment's vote from Up to Down regardless of the
* compiled-in `VoteBehavior`. May be called on amendments not in the
* supported list; an entry is created if one does not exist. The new
* state is persisted to the wallet database.
*
* @param amendment The 256-bit amendment hash to veto.
* @return `true` if the vote state changed (was Up, now Down);
* `false` if the amendment was already Down-voted or Obsolete.
*/
virtual bool
veto(uint256 const& amendment) = 0;
/** Remove a previously applied veto for an amendment.
*
* Reverts the amendment's vote from Down back to Up. The change is
* persisted to the wallet database. Has no effect if the amendment
* was never vetoed, does not exist, or has `VoteBehavior::Obsolete`
* (Obsolete amendments cannot be unvetoed).
*
* @param amendment The 256-bit amendment hash to un-veto.
* @return `true` if the vote state changed (was Down, now Up);
* `false` if the amendment was not in the Down state.
*/
virtual bool
unVeto(uint256 const& amendment) = 0;
/** Mark an amendment as enabled in the local amendment table.
*
* Directly flips the amendment's enabled flag. Called by
* `doValidatedLedger` when ledger state confirms the amendment is active.
* If the amendment is not in the supported list, `hasUnsupportedEnabled()`
* will subsequently return `true`.
*
* @param amendment The 256-bit amendment hash to enable.
* @return `true` if the amendment was not already enabled;
* `false` if it was already in the enabled state.
*/
virtual bool
enable(uint256 const& amendment) = 0;
/** Return whether an amendment is currently active on the network.
*
* @param amendment The 256-bit amendment hash to query.
* @return `true` if the amendment has been enabled via `enable()` or
* through ledger validation; `false` otherwise.
*/
[[nodiscard]] virtual bool
isEnabled(uint256 const& amendment) const = 0;
/** Return whether this node's software knows about and supports an amendment.
*
* @param amendment The 256-bit amendment hash to query.
* @return `true` if the amendment was included in the `supported` list
* passed to `makeAmendmentTable`; `false` for unknown amendments.
*/
[[nodiscard]] virtual bool
isSupported(uint256 const& amendment) const = 0;
/**
* @brief returns true if one or more amendments on the network
* have been enabled that this server does not support
/** Return whether any network-enabled amendment is unsupported by this node.
*
* @return true if an unsupported feature is enabled on the network
* When this returns `true`, the node is "amendment blocked" — it is
* executing ledger rules it does not fully implement. The application
* layer should warn operators and eventually halt participation.
*
* @return `true` if at least one enabled amendment is not in this node's
* supported list.
*/
[[nodiscard]] virtual bool
hasUnsupportedEnabled() const = 0;
/** Return the projected activation time of the earliest unsupported amendment.
*
* Scans amendments currently holding validator supermajority that are not
* supported by this node and returns the time at which the earliest such
* amendment is expected to activate (`majorityTime` after it first
* achieved supermajority). Updated by `doValidatedLedger`.
*
* @return The projected activation time of the first unsupported amendment
* that has achieved majority, or `std::nullopt` if no unsupported
* amendment is approaching activation.
*/
[[nodiscard]] virtual std::optional<NetClock::time_point>
firstUnsupportedExpected() const = 0;
/** Serialize all known amendments to JSON for RPC responses.
*
* @param isAdmin `true` to include sensitive or operator-only fields.
* @return A `json::Value` object containing the full amendment list with
* status, vote, and majority information for each entry.
*/
[[nodiscard]] virtual json::Value
getJson(bool isAdmin) const = 0;
@@ -71,7 +187,17 @@ public:
[[nodiscard]] virtual json::Value
getJson(uint256 const& amendment, bool isAdmin) const = 0;
/** Called when a new fully-validated ledger is accepted. */
/** Update amendment state from a newly validated ledger.
*
* Adapter that extracts `enabledAmendments` and `majorityAmendments` from
* `lastValidatedLedger` via `getEnabledAmendments()` and
* `getMajorityAmendments()`, then delegates to the pure-virtual
* `doValidatedLedger(LedgerIndex, set, majorityAmendments_t)` overload.
* The call is skipped entirely when `needValidatedLedger` returns `false`.
*
* @param lastValidatedLedger The most recently validated ledger. Amendment
* state is read from this view; the ledger sequence gates the update.
*/
void
doValidatedLedger(std::shared_ptr<ReadView const> const& lastValidatedLedger)
{
@@ -84,24 +210,77 @@ public:
}
}
/** Called to determine whether the amendment logic needs to process
a new validated ledger. (If it could have changed things.)
*/
/** Return whether the amendment table needs to process a given ledger sequence.
*
* Amendment voting state only changes at flag ledgers (every 256 ledgers).
* This gate avoids the cost of extracting and processing amendment state
* for the vast majority of validated ledgers that cannot affect voting
* outcomes.
*
* @param seq The sequence number of the validated ledger being considered.
* @return `true` if `seq` crosses a new 256-ledger flag boundary relative
* to the last processed sequence; `false` if no change is possible.
*/
[[nodiscard]] virtual bool
needValidatedLedger(LedgerIndex seq) const = 0;
/** Update internal amendment state from pre-extracted ledger data.
*
* Enables all amendments in `enabled`, then scans `majority` for
* unsupported amendments approaching activation and updates the
* `firstUnsupportedExpected` projection accordingly. Errors are logged for
* each unsupported amendment that has reached supermajority.
*
* @param ledgerSeq Sequence number of the validated ledger.
* @param enabled Set of amendment hashes currently active in the ledger.
* @param majority Map of amendment hash → time of first observed
* supermajority for amendments that have crossed the voting threshold
* but are not yet enabled.
*/
virtual void
doValidatedLedger(
LedgerIndex ledgerSeq,
std::set<uint256> const& enabled,
majorityAmendments_t const& majority) = 0;
// Called when the set of trusted validators changes.
/** Notify the table that the set of trusted validators has changed.
*
* Updates the internal per-validator vote cache: existing records are
* preserved for validators that remain trusted; new validators are
* initialized with empty votes; validators no longer in the UNL have
* their records discarded. Vote history is NOT reset — this preserves
* the anti-flapping behavior that prevents an amendment from appearing to
* oscillate across the 80% threshold as validators come and go.
*
* @param allTrusted The complete current set of trusted validator public keys.
*/
virtual void
trustChanged(hash_set<PublicKey> const& allTrusted) = 0;
// Called by the consensus code when we need to
// inject pseudo-transactions
/** Compute amendment actions for the current consensus round.
*
* Aggregates amendment votes from `valSet` against the current ledger
* state, applying the anti-flapping policy that retains the last known
* vote from each trusted validator for up to 24 hours. For each amendment
* whose vote state has changed relative to the ledger, produces an action
* entry:
* - `tfGotMajority` — validators have supermajority; ledger does not yet
* record it.
* - `tfLostMajority` — validators have lost supermajority; ledger still
* records it.
* - `0` — supermajority has been held for `majorityTime`; enable now.
*
* @param rules Protocol rules in effect for the ledger being built.
* @param closeTime Parent ledger's close time, used to evaluate whether
* `majorityTime` has elapsed since first supermajority.
* @param enabledAmendments Set of amendment hashes already active.
* @param majorityAmendments Map of amendment hash → time first achieving
* supermajority, for amendments not yet enabled.
* @param valSet Validations from the previous ledger; each carries the
* set of amendments the issuing validator supports.
* @return A map from amendment hash to action flag for each amendment
* requiring a pseudo-transaction in the initial consensus position.
*/
virtual std::map<uint256, std::uint32_t>
doVoting(
Rules const& rules,
@@ -110,15 +289,27 @@ public:
majorityAmendments_t const& majorityAmendments,
std::vector<std::shared_ptr<STValidation>> const& valSet) = 0;
// Called by the consensus code when we need to
// add feature entries to a validation
/** Return the amendment hashes this node wishes to vote for.
*
* Called when building a `STValidation` message. Returns all amendments
* that this node supports, has Up-voted, and that are not already active
* in the ledger. The result is sorted.
*
* @param enabled The set of amendment hashes currently enabled in the
* ledger; enabled amendments are excluded from the returned set.
* @return Sorted vector of amendment hashes this node wants to vote for.
*/
[[nodiscard]] virtual std::vector<uint256>
doValidation(std::set<uint256> const& enabled) const = 0;
// The set of amendments to enable in the genesis ledger
// This will return all known, non-vetoed amendments.
// If we ever have two amendments that should not both be
// enabled at the same time, we should ensure one is vetoed.
/** Return all non-vetoed amendments desired for a genesis ledger.
*
* Equivalent to `doValidation({})` — returns every supported, Up-voted
* amendment since none are enabled yet. If two amendments must not both be
* enabled simultaneously, one must be vetoed before calling this.
*
* @return All known, supported, non-vetoed amendment hashes.
*/
[[nodiscard]] virtual std::vector<uint256>
getDesired() const = 0;
@@ -128,6 +319,25 @@ public:
// implementation. These APIs will merge when the view code
// supports a full ledger API
/** Run the amendment voting pipeline and inject pseudo-transactions.
*
* Adapter for the consensus engine. Extracts amendment state from
* `lastClosedLedger`, delegates to the pure-virtual `doVoting` overload
* to determine required actions, then builds a signed-less `STTx` of type
* `ttAMENDMENT` for each action and inserts it into `initialPosition`
* as a `TnTransactionNm` node. These pseudo-transactions are not user
* transactions; they are injected directly into the consensus-agreed
* transaction set so validators can process them at flag-ledger close.
*
* @param lastClosedLedger The most recently closed ledger; supplies
* rules, parent close time, enabled amendments, and majority state.
* @param parentValidations Validations received for the parent ledger;
* each carries the voting validator's amendment preferences.
* @param initialPosition The SHAMap being built as the node's initial
* consensus position; amendment pseudo-transactions are added here.
* @param j Journal for debug logging of injected
* pseudo-transactions.
*/
void
doVoting(
std::shared_ptr<ReadView const> const& lastClosedLedger,
@@ -169,6 +379,30 @@ public:
}
};
/** Create the concrete AmendmentTable implementation.
*
* Registers all supported amendments, applies config-forced enables and
* vetoes, and loads any persisted vote overrides from the wallet database.
* Config entries in `enabled` and `vetoed` are ignored if the wallet database
* already contains a `FeatureVotes` table — the database is the authoritative
* source for persisted vote state.
*
* @param registry Service registry used to access the wallet database for
* persisting vote state.
* @param majorityTime Duration a supermajority must be continuously held
* before an amendment is enabled (typically two weeks on mainnet).
* @param supported All amendments compiled into this build, each with its
* `VoteBehavior`. Amendments absent from this list are treated as
* unsupported; enabling them sets `hasUnsupportedEnabled()`.
* @param enabled Config section (`[amendments]`) listing amendment IDs
* to force-enable; applied only when the wallet database has no
* `FeatureVotes` table.
* @param vetoed Config section (`[veto_amendments]`) listing amendment
* IDs to suppress votes for; applied only when the wallet database has no
* `FeatureVotes` table.
* @param journal Journal for logging during initialization.
* @return Owning pointer to the constructed `AmendmentTable`.
*/
std::unique_ptr<AmendmentTable>
makeAmendmentTable(
ServiceRegistry& registry,

View File

@@ -1,3 +1,14 @@
/** @file
* Defines `ApplyView`, the writable ledger view used during transaction
* application, and the `ApplyFlags` bitmask that configures each apply pass.
*
* All state mutations produced by a transaction — trust-line updates, offer
* creation, account creation, fee destruction — flow through `ApplyView`.
* Changes are journaled and may be committed to the parent view or discarded
* atomically, enabling transactional rollback at every layer of the view
* hierarchy.
*/
#pragma once
#include <xrpl/basics/safe_cast.h>
@@ -7,30 +18,54 @@
namespace xrpl {
/** Bitmask of flags that configure how a transaction apply pass behaves.
*
* Carried through every transaction-application call site. Multiple flags
* may be combined with `operator|`. All bitwise operators use `safeCast`
* to prevent silent conversion to the underlying integer type.
*
* @note Correctness and commutativity of `operator|` and `operator&` are
* verified by `static_assert` at compile time, guarding against future
* value collisions.
*/
// Bitwise flag enum with existing operator overloads
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum ApplyFlags : std::uint32_t {
/** No flags set; default processing. */
TapNone = 0x00,
// This is a local transaction with the
// fail_hard flag set.
/** Transaction originated locally with `fail_hard` set.
*
* The engine must not retry; a hard failure that claims fees is
* produced instead of a soft retry.
*/
TapFailHard = 0x10,
// This is not the transaction's last pass
// Transaction can be retried, soft failures allowed
/** This is not the transaction's final pass.
*
* Soft failures (insufficient balance, wrong sequence) are allowed
* because the transaction may succeed in a later pass.
*/
TapRetry = 0x20,
// Transaction came from a privileged source
/** Transaction arrived from a trusted, privileged source.
*
* Certain per-transaction limits are relaxed (e.g., path count).
*/
TapUnlimited = 0x400,
// Transaction is executing as part of a batch
/** Transaction is being processed as part of a batch transaction. */
TapBatch = 0x800,
// Transaction shouldn't be applied
// Signatures shouldn't be checked
/** Dry-run simulation: apply the transaction without committing state.
*
* Signature checks are skipped. A full `TxMeta` is still produced so
* callers can inspect the outcome. Used by the `simulate` RPC handler.
*/
TapDryRun = 0x1000
};
/** Combine two `ApplyFlags` values. */
constexpr ApplyFlags
operator|(ApplyFlags const& lhs, ApplyFlags const& rhs)
{
@@ -42,6 +77,7 @@ operator|(ApplyFlags const& lhs, ApplyFlags const& rhs)
static_assert((TapFailHard | TapRetry) == safeCast<ApplyFlags>(0x30u), "ApplyFlags operator |");
static_assert((TapRetry | TapFailHard) == safeCast<ApplyFlags>(0x30u), "ApplyFlags operator |");
/** Mask `ApplyFlags` values, retaining only the bits present in both operands. */
constexpr ApplyFlags
operator&(ApplyFlags const& lhs, ApplyFlags const& rhs)
{
@@ -53,6 +89,7 @@ operator&(ApplyFlags const& lhs, ApplyFlags const& rhs)
static_assert((TapFailHard & TapRetry) == TapNone, "ApplyFlags operator &");
static_assert((TapRetry & TapFailHard) == TapNone, "ApplyFlags operator &");
/** Invert all bits of an `ApplyFlags` value. */
constexpr ApplyFlags
operator~(ApplyFlags const& flags)
{
@@ -61,6 +98,7 @@ operator~(ApplyFlags const& flags)
static_assert(~TapRetry == safeCast<ApplyFlags>(0xFFFFFFDFu), "ApplyFlags operator ~");
/** Set-assign `ApplyFlags` bits from `rhs` into `lhs`. */
inline ApplyFlags
operator|=(ApplyFlags& lhs, ApplyFlags const& rhs)
{
@@ -68,6 +106,7 @@ operator|=(ApplyFlags& lhs, ApplyFlags const& rhs)
return lhs;
}
/** Clear `ApplyFlags` bits in `lhs` that are absent from `rhs`. */
inline ApplyFlags
operator&=(ApplyFlags& lhs, ApplyFlags const& rhs)
{
@@ -77,47 +116,40 @@ operator&=(ApplyFlags& lhs, ApplyFlags const& rhs)
//------------------------------------------------------------------------------
/** Writeable view to a ledger, for applying a transaction.
This refinement of ReadView provides an interface where
the SLE can be "checked out" for modifications and put
back in an updated or removed state. Also added is an
interface to provide contextual information necessary
to calculate the results of transaction processing,
including the metadata if the view is later applied to
the parent (using an interface in the derived class).
The context info also includes values from the base
ledger such as sequence number and the network time.
This allows implementations to journal changes made to
the state items in a ledger, with the option to apply
those changes to the base or discard the changes without
affecting the base.
Typical usage is to call read() for non-mutating
operations.
For mutating operations the sequence is as follows:
// Add a new value
v.insert(sle);
// Check out a value for modification
sle = v.peek(k);
// Indicate that changes were made
v.update(sle)
// Or, erase the value
v.erase(sle)
The invariant is that insert, update, and erase may not
be called with any SLE which belongs to different view.
*/
/** Writable view of a ledger used during transaction application.
*
* Extends `ReadView` with a checkout-modify-commit protocol: callers
* `peek()` an SLE to obtain a mutable handle, mutate it in place, then
* call `update()` (or `erase()`) to journal the change. `insert()` adds
* entries that were never checked out. All deltas are buffered; calling
* `apply()` on the concrete subclass flushes them to the parent view.
* Discarding the view without calling `apply()` abandons all changes.
*
* Also exposes directory management (`dirAppend`, `dirInsert`, `dirRemove`,
* `dirDelete`) and virtual payment hooks (`creditHookIOU`, `creditHookMPT`,
* `issuerSelfDebitHookMPT`, `adjustOwnerCountHook`) that `PaymentSandbox`
* overrides to prevent double-spend within a multi-hop payment path.
*
* @invariant `update()` and `erase()` must be called with an SLE obtained
* from `peek()` on **the same view instance**. Passing an SLE across
* view boundaries is undefined behavior, because each view journals its
* own deltas independently.
*/
class ApplyView : public ReadView
{
private:
/** Add an entry to a directory using the specified insert strategy */
/** Insert a key into the directory, routing to append-tail or
* sorted-insert logic based on `preserveOrder`.
*
* @param preserveOrder if `true`, append to tail (offer-book order);
* if `false`, insert in sorted position within each page.
* @param directory keylet of the directory root page.
* @param key the `uint256` key to insert.
* @param describe callback invoked on each newly allocated page SLE to
* brand it with type-specific fields (e.g., `sfOwner`).
* @return the 0-based page index where the key was stored, or
* `std::nullopt` if the page counter overflowed.
*/
std::optional<std::uint64_t>
dirAdd(
bool preserveOrder,
@@ -128,92 +160,86 @@ private:
public:
ApplyView() = default;
/** Returns the tx apply flags.
Flags can affect the outcome of transaction
processing. For example, transactions applied
to an open ledger generate "local" failures,
while transactions applied to the consensus
ledger produce hard failures (and claim a fee).
*/
/** Return the flags that govern this transaction apply pass.
*
* Flags shape engine behavior: `TapRetry` allows soft failures,
* `TapFailHard` demands a fee-claiming hard failure, `TapDryRun`
* suppresses state commits, and `TapUnlimited` relaxes per-tx limits.
*
* @return the `ApplyFlags` bitmask for this view.
*/
[[nodiscard]] virtual ApplyFlags
flags() const = 0;
/** Prepare to modify the SLE associated with key.
Effects:
Gives the caller ownership of a modifiable
SLE associated with the specified key.
The returned SLE may be used in a subsequent
call to erase or update.
The SLE must not be passed to any other ApplyView.
@return `nullptr` if the key is not present
*/
/** Check out a ledger entry for in-place mutation.
*
* Returns an owning `shared_ptr<SLE>` whose contents may be freely
* modified. The caller must pass the same pointer back to `update()`
* or `erase()` on **this** view instance when done; passing it to any
* other `ApplyView` is undefined behavior.
*
* @param k keylet identifying the entry.
* @return a mutable handle to the SLE, or `nullptr` if `k` is not
* present in this view.
*/
virtual std::shared_ptr<SLE>
peek(Keylet const& k) = 0;
/** Remove a peeked SLE.
Requirements:
`sle` was obtained from prior call to peek()
on this instance of the RawView.
Effects:
The key is no longer associated with the SLE.
*/
/** Remove an entry previously checked out with `peek()`.
*
* Journals a delete delta so the entry is absent when this view's
* changes are later committed.
*
* @param sle a pointer obtained from `peek()` on this view instance.
*
* @note The key is taken from the SLE's own key field.
*/
virtual void
erase(std::shared_ptr<SLE> const& sle) = 0;
/** Insert a new state SLE
Requirements:
`sle` was not obtained from any calls to
peek() on any instances of RawView.
The SLE's key must not already exist.
Effects:
The key in the state map is associated
with the SLE.
The RawView acquires ownership of the shared_ptr.
@note The key is taken from the SLE
*/
/** Insert a brand-new ledger entry that has no prior existence in this view.
*
* The SLE must not have been obtained from `peek()`. Its key must not
* already exist in this view. The view takes ownership of the
* `shared_ptr`.
*
* @param sle the new entry to insert.
*
* @note The key is taken from the SLE's own key field.
*/
virtual void
insert(std::shared_ptr<SLE> const& sle) = 0;
/** Indicate changes to a peeked SLE
Requirements:
The SLE's key must exist.
`sle` was obtained from prior call to peek()
on this instance of the RawView.
Effects:
The SLE is updated
@note The key is taken from the SLE
*/
/** @{ */
/** Journal modifications to a checked-out ledger entry.
*
* Signals to the underlying delta table that the entry has changed and
* its new state must be written when this view's changes are committed.
* The entry's key must already exist.
*
* @param sle a pointer obtained from `peek()` on this view instance.
*
* @note The key is taken from the SLE's own key field.
*/
virtual void
update(std::shared_ptr<SLE> const& sle) = 0;
//--------------------------------------------------------------------------
// Called when a credit is made to an account
// This is required to support PaymentSandbox
/** Notification hook invoked whenever an IOU credit is made to an account.
*
* The default implementation is a no-op; `PaymentSandbox` overrides it to
* record the credit in its `DeferredCredits` table so that subsequent
* `balanceHookIOU` calls subtract in-path credits from reported balances,
* preventing circular paths from manufacturing liquidity.
*
* @param from the debited account (sender side of the trust line).
* @param to the credited account (receiver side of the trust line).
* @param amount the IOU amount being credited; must hold an `Issue`.
* @param preCreditBalance the sender's trust-line balance before the credit.
*
* @note The `XRPL_ASSERT` in the default body verifies that `amount` holds
* an `Issue`; it fires in debug builds if the wrong asset type is passed.
*/
virtual void
creditHookIOU(
AccountID const& from,
@@ -224,6 +250,23 @@ public:
XRPL_ASSERT(amount.holds<Issue>(), "creditHookIOU: amount is for Issue");
}
/** Notification hook invoked whenever an MPT credit is made to an account.
*
* The default implementation is a no-op; `PaymentSandbox` overrides it to
* record the credit in its `DeferredCredits` table, enabling the same
* double-spend prevention as `creditHookIOU` but for MPT trust lines.
*
* @param from the debited account.
* @param to the credited account.
* @param amount the MPT amount being credited; must hold an `MPTIssue`.
* @param preCreditBalanceHolder the holder's MPT balance before the credit.
* @param preCreditBalanceIssuer the issuer's `OutstandingAmount` before the
* credit (signed to accommodate transient overflow).
*
* @note The `XRPL_ASSERT` in the default body verifies that `amount` holds
* an `MPTIssue`; it fires in debug builds if the wrong asset type is
* passed.
*/
virtual void
creditHookMPT(
AccountID const& from,
@@ -235,67 +278,66 @@ public:
XRPL_ASSERT(amount.holds<MPTIssue>(), "creditHookMPT: amount is for MPTIssue");
}
/** Facilitate tracking of MPT sold by an issuer owning MPT sell offer.
* Unlike IOU, MPT doesn't have bi-directional relationship with an issuer,
* where a trustline limits an amount that can be issued to a holder.
* Consequently, the credit step (last MPTEndpointStep or
* BookStep buying MPT) might temporarily overflow OutstandingAmount.
* Limiting of a step's output amount in this case is delegated to
* the next step (in rev order). The next step always redeems when a holder
* account sells MPT (first MPTEndpointStep or BookStep selling MPT).
* In this case the holder account is only limited by the step's output
* and it's available funds since it's transferring the funds from one
* account to another account and doesn't change OutstandingAmount.
* This doesn't apply to an offer owned by an issuer.
* In this case the issuer sells or self debits and is increasing
* OutstandingAmount. Ability to issue is limited by the issuer
* originally available funds less already self sold MPT amounts (MPT sell
* offer).
* Consider an example:
* - GW creates MPT(USD) with 1,000USD MaximumAmount.
* - GW pays 950USD to A1.
* - A1 creates an offer 100XRP(buy)/100USD(sell).
* - GW creates an offer 100XRP(buy)/100USD(sell).
* - A2 pays 200USD to A3 with sendMax of 200XRP.
* Since the payment engine executes payments in reverse,
* OutstandingAmount overflows in MPTEndpointStep: 950 + 200 = 1,150USD.
* BookStep first consumes A1 offer. This reduces OutstandingAmount
* by 100USD: 1,150 - 100 = 1,050USD. GW offer can only be partially
* consumed because the initial available amount is 50USD = 1,000 - 950.
* BookStep limits it's output to 150USD. This in turn limits A3's send
* amount to 150XRP: A1 buys 100XRP and sells 100USD to A3. This doesn't
* change OutstandingAmount. GW buys 50XRP and sells 50USD to A3. This
* changes OutstandingAmount to 1,000USD.
/** Notification hook for MPT issuer self-debit via an owned sell offer.
*
* Unlike IOU trust lines, MPT has no bi-directional issuer↔holder
* relationship that caps issuance. When the payment engine processes a
* sell offer owned by the MPT issuer (in reverse order), it tentatively
* credits the holder first, which can transiently push `OutstandingAmount`
* beyond `MaximumAmount`. A subsequent step then redeems MPT from the
* issuer, restoring `OutstandingAmount`. The hook lets `PaymentSandbox`
* track the issuer's cumulative self-debit so that `balanceHookSelfIssueMPT`
* can cap available-to-issue at `origBalance selfDebit` across the entire
* payment rather than trusting the transient ledger state.
*
* The default implementation is a no-op.
*
* @param issue the MPT issuance being self-debited.
* @param amount the quantity the issuer is selling (debiting to self).
* @param origBalance the issuer's `OutstandingAmount` at the start of the
* payment, before any path steps executed.
*/
virtual void
issuerSelfDebitHookMPT(MPTIssue const& issue, std::uint64_t amount, std::int64_t origBalance)
{
}
// Called when the owner count changes
// This is required to support PaymentSandbox
/** Notification hook invoked when an account's owner count changes.
*
* The default implementation is a no-op; `PaymentSandbox` overrides it to
* record the high-water owner count for each account touched during the
* payment, so that reserve checks reflect the peak count rather than the
* instantaneous count at any single path step.
*
* @param account the account whose owner count is changing.
* @param cur the owner count before the change.
* @param next the owner count after the change.
*/
virtual void
adjustOwnerCountHook(AccountID const& account, std::uint32_t cur, std::uint32_t next)
{
}
/** Append an entry to a directory
Entries in the directory will be stored in order of insertion, i.e. new
entries will always be added at the tail end of the last page.
@param directory the base of the directory
@param key the entry to insert
@param describe callback to add required entries to a new page
@return a \c std::optional which, if insertion was successful,
will contain the page number in which the item was stored.
@note this function may create a page (including a root page), if no
page with space is available. This function will only fail if the
page counter exceeds the protocol-defined maximum number of
allowable pages.
*/
/** Append an entry to a directory, preserving insertion order.
*
* New entries are always placed at the tail of the last page, maintaining
* chronological ordering within an offer-book directory. This ordering
* is relied upon during offer matching: earlier offers at the same quality
* have priority.
*
* @param directory keylet of the directory root (page 0).
* @param key keylet of the entry to append; must be of type `ltOFFER`.
* @param describe callback invoked on each newly allocated page SLE to
* brand it with type-specific fields.
* @return the 0-based page index where the entry was stored, or
* `std::nullopt` if the page counter overflowed the protocol maximum.
*
* @note Only `ltOFFER` entries may be appended; passing any other keylet
* type triggers `UNREACHABLE` and returns `std::nullopt`. Use
* `dirInsert` for non-offer entries.
* @note A root page is created automatically if the directory does not yet
* exist. New pages are linked into the chain as needed.
*/
/** @{ */
std::optional<std::uint64_t>
dirAppend(
@@ -318,23 +360,24 @@ public:
}
/** @} */
/** Insert an entry to a directory
Entries in the directory will be stored in a semi-random order, but
each page will be maintained in sorted order.
@param directory the base of the directory
@param key the entry to insert
@param describe callback to add required entries to a new page
@return a \c std::optional which, if insertion was successful,
will contain the page number in which the item was stored.
@note this function may create a page (including a root page), if no
page with space is available.this function will only fail if the
page counter exceeds the protocol-defined maximum number of
allowable pages.
*/
/** Insert an entry into a directory, maintaining per-page sorted order.
*
* Each individual page is kept in sorted key order, but entries may span
* multiple pages so the overall directory is only loosely ordered.
* Because legacy pages may not be sorted, each touched page is re-sorted
* before the new key is binary-inserted. Used for account-owned object
* directories (offers owned by an account, escrows, etc.).
*
* @param directory keylet of the directory root (page 0).
* @param key the `uint256` key to insert.
* @param describe callback invoked on each newly allocated page SLE to
* brand it with type-specific fields.
* @return the 0-based page index where the entry was stored, or
* `std::nullopt` if the page counter overflowed the protocol maximum.
*
* @note A root page is created automatically if the directory does not yet
* exist. New pages are allocated and linked as needed.
*/
/** @{ */
std::optional<std::uint64_t>
dirInsert(
@@ -345,6 +388,10 @@ public:
return dirAdd(false, directory, key, describe);
}
/** @copydoc dirInsert(Keylet const&, uint256 const&, std::function<void(std::shared_ptr<SLE> const&)> const&)
*
* Convenience overload that extracts the `uint256` key from `key.key`.
*/
std::optional<std::uint64_t>
dirInsert(
Keylet const& directory,
@@ -355,25 +402,37 @@ public:
}
/** @} */
/** Remove an entry from a directory
@param directory the base of the directory
@param page the page number for this page
@param key the entry to remove
@param keepRoot if deleting the last entry, don't
delete the root page (i.e. the directory itself).
@return \c true if the entry was found and deleted and
\c false otherwise.
@note This function will remove zero or more pages from the directory;
the root page will not be deleted even if it is empty, unless
\p keepRoot is not set and the directory is empty.
*/
/** Remove a single entry from a directory and collapse any resulting
* empty non-root pages.
*
* After the key is removed, if the containing page becomes empty:
* - Non-root pages are unlinked and erased from the ledger.
* - The root page (page 0) is never erased unless `keepRoot` is `false`
* and the entire directory is now empty.
* - Legacy empty trailing pages left by older code are cleaned up
* opportunistically when the root page is touched.
*
* @param directory keylet of the directory root (page 0).
* @param page the 0-based page index that contains `key`; obtained from
* the page number stored in the owning ledger entry.
* @param key the `uint256` key to remove.
* @param keepRoot if `true`, preserve the root page even if it becomes
* empty after the removal (the directory anchor remains in the ledger).
* @return `true` if the entry was found and removed; `false` if the page
* or the key was not found.
*
* @note Throws `std::logic_error` if the directory linked-list pointers
* are found to be inconsistent (broken chain); this indicates ledger
* corruption and should never occur under normal operation.
*/
/** @{ */
bool
dirRemove(Keylet const& directory, std::uint64_t page, uint256 const& key, bool keepRoot);
/** @copydoc dirRemove(Keylet const&, std::uint64_t, uint256 const&, bool)
*
* Convenience overload that extracts the `uint256` key from `key.key`.
*/
bool
dirRemove(Keylet const& directory, std::uint64_t page, Keylet const& key, bool keepRoot)
{
@@ -381,31 +440,67 @@ public:
}
/** @} */
/** Remove the specified directory, invoking the callback for every node. */
/** Delete every page of a directory, invoking a callback for each key.
*
* Traverses the entire linked-list chain starting from page 0, erases
* each page SLE, and calls `callback` once per key stored in the
* directory. Callers are responsible for cleaning up the objects
* referenced by those keys before or after this call.
*
* @param directory keylet of the directory root (page 0).
* @param callback function called with each `uint256` key found in the
* directory before the page is erased.
* @return `true` if the root page was found and the directory was deleted;
* `false` if the root page does not exist.
*/
bool
dirDelete(Keylet const& directory, std::function<void(uint256 const&)> const&);
/** Remove the specified directory, if it is empty.
@param directory the identifier of the directory node to be deleted
@return \c true if the directory was found and was successfully deleted
\c false otherwise.
@note The function should only be called with the root entry (i.e. with
the first page) of a directory.
*/
/** Delete the root page of a directory if and only if it is empty.
*
* Verifies that both `sfIndexes` is empty and the linked-list pointers
* indicate no other pages remain. Legacy empty trailing pages (a known
* edge case from older code) are cleaned up as a side effect before the
* emptiness check.
*
* @param directory keylet of the directory root page (`ltDIR_NODE`);
* must identify page 0 (the root).
* @return `true` if the directory was empty and was successfully erased;
* `false` if the directory was not found, contained entries, or had
* non-empty sub-pages.
*
* @note Throws `std::logic_error` if the directory linked-list pointers
* are inconsistent; this indicates ledger corruption.
*/
bool
emptyDirDelete(Keylet const& directory);
};
namespace directory {
/** Helper functions for managing low-level directory operations.
These are not part of the ApplyView interface.
Don't use them unless you really, really know what you're doing.
Instead use dirAdd, dirInsert, etc.
/** Low-level primitives for building and modifying paged ledger directories.
*
* These helpers implement the individual steps of the directory linked-list
* protocol: root creation, tail-page discovery, key insertion, and page
* allocation. They are exposed so that specialised callers (tests, tooling)
* can exercise individual steps, but **transaction processors must always
* go through `ApplyView::dirAppend` / `dirInsert` / `dirRemove`** instead.
*
* @warning Do not call these directly unless you fully understand the
* directory invariants and page-linking protocol.
*/
namespace directory {
/** Allocate and insert the root page (page 0) for a new directory.
*
* Creates a fresh `ltDIR_NODE` SLE at `directory`, sets `sfRootIndex`,
* calls `describe` to brand it, stores `key` as the first `sfIndexes`
* entry, and inserts it into the view.
*
* @param view the writable ledger view.
* @param directory keylet for the root page.
* @param key the first key to store in the new directory.
* @param describe callback to set type-specific fields on the root SLE.
* @return `0` — the root page index.
*/
std::uint64_t
createRoot(
ApplyView& view,
@@ -413,9 +508,37 @@ createRoot(
uint256 const& key,
std::function<void(std::shared_ptr<SLE> const&)> const& describe);
/** Locate the last used page in a directory by following `sfIndexPrevious`
* from the root.
*
* The root's `sfIndexPrevious` field always points to the tail page (O(1)
* append guarantee). If it is 0 the root itself is the tail.
*
* @param view the writable ledger view.
* @param directory keylet of the directory root.
* @param start the root SLE (already peeked by the caller).
* @return a tuple of `(pageIndex, pageSLE, sfIndexes)` for the tail page.
* @throws std::logic_error if the back-pointer chain is broken.
*/
auto
findPreviousPage(ApplyView& view, Keylet const& directory, SLE::ref start);
/** Insert a key into the `sfIndexes` vector of an existing page SLE and
* commit the change via `view.update()`.
*
* If `preserveOrder` is `true`, the key is appended at the end (offer-book
* order). If `false`, the page is sorted first (to handle legacy unsorted
* pages), then the key is binary-inserted. Double-insertion throws.
*
* @param view the writable ledger view.
* @param node the page SLE to modify (must have been obtained via `peek()`).
* @param page the 0-based page index of `node`.
* @param preserveOrder `true` to append; `false` to sort-then-insert.
* @param indexes the current `sfIndexes` vector (mutated in place).
* @param key the key to insert.
* @return the page index (`page`) where the key was stored.
* @throws std::logic_error if `key` is already present in `indexes`.
*/
std::uint64_t
insertKey(
ApplyView& view,
@@ -425,6 +548,26 @@ insertKey(
STVector256& indexes,
uint256 const& key);
/** Allocate a new trailing page, link it into the directory chain, and
* store the first key in it.
*
* The new page number is computed as `page + 1`; unsigned wraparound to 0
* (verified by `static_assert`) signals overflow and causes `std::nullopt`
* to be returned. The `fixDirectoryLimit` amendment lifts the legacy
* per-directory page cap.
*
* @param view the writable ledger view.
* @param page the current last-page index (new page will be `page + 1`).
* @param node the current last-page SLE; its `sfIndexNext` is updated.
* @param nextPage reserved for future mid-chain insertion; must be `0`.
* @param next the root SLE; its `sfIndexPrevious` is updated to point to
* the new tail.
* @param key the first key to store on the new page.
* @param directory keylet of the directory root.
* @param describe callback to brand the new page SLE.
* @return the new page index, or `std::nullopt` on overflow or page-count
* limit violation.
*/
std::optional<std::uint64_t>
insertPage(
ApplyView& view,

View File

@@ -7,12 +7,25 @@
namespace xrpl {
/** Editable, discardable view that can build metadata for one tx.
Iteration of the tx map is delegated to the base.
@note Presented as ApplyView to clients.
*/
/** Per-transaction scratch-pad view that buffers ledger mutations and
* constructs `TxMeta` on commit.
*
* `ApplyViewImpl` is the concrete view handed to every `Transactor`
* during the apply phase. It sits at the top of the view hierarchy:
* `ReadView` → `ApplyView` → `detail::ApplyViewBase` → `ApplyViewImpl`.
* All ledger mutations are buffered in the inherited `items_`
* (`ApplyStateTable`) and are not visible to the parent `OpenView`
* until `apply()` is called. If the transaction fails, the view is
* discarded and `base_` is left unchanged.
*
* The object is move-constructible but neither copyable nor
* move-assignable, ensuring that at most one instance can commit a
* given transaction's buffered state.
*
* @note `base_` is held as a raw `const*` (not a shared pointer) for
* performance. The caller must ensure the underlying view outlives
* this object.
*/
class ApplyViewImpl final : public detail::ApplyViewBase
{
public:
@@ -24,14 +37,47 @@ public:
operator=(ApplyViewImpl const&) = delete;
ApplyViewImpl(ApplyViewImpl&&) = default;
/** Construct a transaction apply view over an existing read view.
*
* @param base The underlying ledger state to read from. Must remain
* valid for the lifetime of this object.
* @param flags Apply-phase control flags (e.g., `tapRETRY`,
* `tapDRY_RUN`, `tapBATCH`) that influence commit behavior and
* the metadata produced by `apply()`.
*/
ApplyViewImpl(ReadView const* base, ApplyFlags flags);
/** Apply the transaction.
After a call to `apply`, the only valid
operation on this object is to call the
destructor.
*/
/** Flush buffered mutations to `to` and produce transaction metadata.
*
* Delegates to `ApplyStateTable::apply()`, which drains every
* pending insert, modify, and erase into `to` and builds the
* `TxMeta` record — including `sfCreatedNode`, `sfModifiedNode`,
* and `sfDeletedNode` entries with `sfPreviousFields`/`sfFinalFields`
* — for the closed ledger. If `isDryRun` is `true`, metadata is
* computed and returned but state changes are suppressed, supporting
* fee-simulation paths without side effects.
*
* When `parentBatchId` is set (i.e., `tapBATCH` is active), the
* generated metadata records the parent batch transaction ID so
* individual results can be traced back to their enclosing batch.
*
* @param to The target open view that accumulates all
* committed transaction changes for the current ledger round.
* @param tx The transaction being applied.
* @param ter The final result code; recorded in metadata.
* @param parentBatchId The ID of the enclosing `ttBATCH` transaction,
* or `std::nullopt` for standalone transactions.
* @param isDryRun If `true`, produce metadata without mutating `to`.
* @param j Journal for diagnostic logging.
* @return The `TxMeta` for closed-ledger commits and dry-run
* evaluation; `std::nullopt` when `to` is still open and
* `isDryRun` is `false`.
*
* @note After this call returns, the only valid operation on this
* object is destruction. The internal `ApplyStateTable` is
* drained and must not be accessed again.
*/
std::optional<TxMeta>
apply(
OpenView& to,
@@ -41,25 +87,50 @@ public:
bool isDryRun,
beast::Journal j);
/** Set the amount of currency delivered.
This value is used when generating metadata
for payments, to set the DeliveredAmount field.
If the amount is not specified, the field is
excluded from the resulting metadata.
*/
/** Record the amount delivered by a payment transaction.
*
* Stores `amount` so that `ApplyStateTable::apply()` can write the
* `sfDeliveredAmount` field into the resulting `TxMeta`. The
* delivered amount can differ from the send amount in cross-currency
* or partial-payment scenarios. If never called, `sfDeliveredAmount`
* is omitted from the metadata.
*
* Must be called before `apply()` to take effect.
*
* @param amount The currency amount actually received by the destination.
*/
void
deliver(STAmount const& amount)
{
deliver_ = amount;
}
/** Get the number of modified entries
/** Return the number of pending write-intent entries.
*
* Counts only entries with an `Erase`, `Insert`, or `Modify` action;
* cache-only reads are excluded. Used by `ApplyContext::size()` to
* support batch-processing decisions before committing.
*
* @return Count of SLE mutations buffered since construction or the
* last `discard()`.
*/
std::size_t
size();
/** Visit modified entries
/** Iterate every pending write-intent entry, invoking a callback per entry.
*
* Delegates to `ApplyStateTable::visit()`. `Cache`-only reads are
* skipped. Used by invariant checkers and batch-processing logic to
* inspect accumulated changes before deciding whether to commit them.
*
* @param target The open view used to fetch pre-change SLE snapshots
* for `Erase` and `Modify` entries.
* @param func Callback invoked once per pending entry with:
* - `key` — ledger index of the entry.
* - `isDelete` — `true` if the entry is being erased.
* - `before` — the SLE state before this transaction (`nullptr`
* for insertions).
* - `after` — the pending SLE state (`nullptr` for deletions).
*/
void
visit(

View File

@@ -4,6 +4,25 @@
namespace xrpl {
/** A range-based view over all offers in a single order-book direction.
*
* The XRPL DEX stores offers in a two-level ledger directory structure: a
* *book* groups offers for one currency pair in one direction, and within
* the book each quality level (encoded exchange rate) has its own directory
* page. `BookDirs` presents this multi-level structure as a flat sequence of
* `SLE` objects, letting callers iterate every offer with a standard
* range-for loop without reasoning about quality boundaries or directory
* pagination.
*
* Construction eagerly locates the first quality directory via
* `ReadView::succ` and loads the first page with `cdirFirst`; subsequent
* advancement is handled lazily by `const_iterator::operator++`.
*
* @note The `ReadView` passed at construction must outlive both the
* `BookDirs` object and any iterators derived from it. Iterators hold
* raw pointers to the view.
* @see Dir for single-directory iteration (e.g., NFTokenOffer pages).
*/
class BookDirs
{
private:
@@ -19,15 +38,49 @@ public:
class const_iterator; // NOLINT(readability-identifier-naming)
using value_type = std::shared_ptr<SLE const>;
/** Construct a `BookDirs` range over all offers in `book` as seen by `view`.
*
* Finds the first quality directory in the book's key-space via
* `view.succ` and positions the internal state at the first offer.
* If the book is empty, `begin() == end()` immediately.
*
* @param view The ledger view to read from; must remain valid for the
* lifetime of this object and all derived iterators.
* @param book The currency pair and direction defining the order book.
*/
BookDirs(ReadView const&, Book const&);
/** Return an iterator positioned at the first offer in the book.
*
* If the book is empty the returned iterator compares equal to `end()`.
*/
[[nodiscard]] const_iterator
begin() const;
/** Return the past-the-end sentinel iterator for this book. */
[[nodiscard]] const_iterator
end() const;
};
/** Forward iterator over offers in an order book, crossing quality boundaries.
*
* Advances through all offers in a book by walking pages within each quality
* directory via `cdirNext`, then locating the next quality directory via
* `ReadView::succ` when a quality is exhausted. Dereference re-reads the
* current offer SLE from the view and caches it until `operator++` clears
* the cache.
*
* **End-sentinel encoding:** the end iterator and an exhausted begin iterator
* share identical state — `entry_ == 0`, `cur_key_ == key_`, and
* `index_ == beast::zero`. `operator++` explicitly resets to this state when
* no further quality directory exists, which is how loop termination is
* detected.
*
* @note Default-constructed iterators have a null `view_` and compare
* unequal to everything, including each other. They are valid only as
* placeholders; dereferencing them is undefined behaviour.
* @note Only `BookDirs` may construct iterators in valid, non-default states.
*/
class BookDirs::const_iterator // NOLINT(readability-identifier-naming)
{
public:
@@ -37,35 +90,91 @@ public:
using difference_type = std::ptrdiff_t;
using iterator_category = std::forward_iterator_tag;
/** Construct a default (placeholder) iterator with a null view.
*
* Required by the `ForwardIterator` concept. The resulting iterator
* compares unequal to all other iterators and must not be dereferenced
* or incremented.
*/
const_iterator() = default;
/** Return true if both iterators refer to the same offer position.
*
* Equality is determined by comparing `entry_`, `cur_key_`, and
* `index_`. If either iterator has a null view, returns false.
*
* @note Comparing iterators from different `BookDirs` instances
* (different views or roots) triggers an assertion in debug builds.
*/
bool
operator==(const_iterator const& other) const;
/** Return true if the iterators do not refer to the same offer position. */
bool
operator!=(const_iterator const& other) const
{
return !(*this == other);
}
/** Return a reference to the current offer SLE.
*
* Reads the offer SLE from the view on first access and caches the
* result; subsequent dereferences of the same position return the cached
* value. The cache is cleared by `operator++`.
*
* @note Asserts that `index_` is non-zero; dereferencing the end
* iterator or a default-constructed iterator is undefined behaviour.
*/
reference
operator*() const;
/** Return a pointer to the current offer SLE.
*
* Equivalent to `&**this`. Safe to use with `->` because `operator*`
* stores the result in a `mutable` cache member whose lifetime matches
* the iterator.
*/
pointer
operator->() const
{
return &**this;
}
/** Advance to the next offer in the book and return this iterator.
*
* First attempts to advance within the current quality directory via
* `cdirNext`. If that quality is exhausted, uses `ReadView::succ` to
* find the next quality directory and positions at its first offer via
* `cdirFirst`. If no further quality directory exists, resets to the
* end-sentinel state. Clears the dereference cache.
*
* @note Asserts that the iterator is not already at the end position
* (i.e. `index_` must be non-zero) before advancing.
*/
const_iterator&
operator++();
/** Post-increment: advance and return a copy of the pre-increment state. */
const_iterator
operator++(int);
private:
friend class BookDirs;
/** Construct a valid iterator anchored to `view`, `root`, and `dirKey`.
*
* Only `BookDirs` calls this constructor. `dirKey` becomes both `key_`
* (the end-sentinel anchor) and the initial `cur_key_`. Additional
* fields (`next_quality_`, `sle_`, `entry_`, `index_`) are populated by
* `BookDirs::begin()` for the begin iterator; the end iterator leaves
* them at their zero-initialised defaults.
*
* @param view The ledger view; must outlive this iterator.
* @param root The root key of the book's quality key-space; must be
* non-zero.
* @param dirKey The key of the first quality directory, or `beast::zero`
* if the book is empty.
*/
const_iterator(ReadView const& view, uint256 const& root, uint256 const& dirKey)
: view_(&view), root_(root), key_(dirKey), cur_key_(dirKey)
{

View File

@@ -8,35 +8,71 @@
namespace xrpl {
/** Listen to public/subscribe messages from a book. */
/** Per-book fan-out layer for WebSocket order-book subscriptions.
*
* One instance exists for each `Book` (currency pair) that has at least one
* active subscriber. `OrderBookDB` owns and looks up instances via
* `getBookListeners()` / `makeBookListeners()`; callers hold references
* through the `pointer` alias.
*
* Subscribers are stored as `InfoSub::wptr` (weak pointers) so that
* `BookListeners` does not extend the lifetime of the connection object.
* Dead entries are pruned lazily inside `publish()` when the weak pointer
* can no longer be locked.
*
* All three public methods take `lock_` for their full duration, including
* across the `p->send()` calls in `publish()`. This favours correctness over
* throughput on high-subscriber-count books.
*/
class BookListeners
{
public:
/** Shared-ownership handle used by `OrderBookDB` and callers. */
using pointer = std::shared_ptr<BookListeners>;
BookListeners() = default;
/** Add a new subscription for this book
/** Register a subscriber for this book.
*
* Stores a weak pointer to @p sub, keyed by its sequence number, so that
* subsequent `publish()` calls deliver notifications to it.
*
* @param sub The subscriber to add; must not be null.
*/
void
addSubscriber(InfoSub::ref sub);
/** Stop publishing to a subscriber
/** Unregister a subscriber by sequence number.
*
* Removes the entry whose key equals @p sub. If no such entry exists
* (e.g. the subscriber was already pruned by a `publish()` call after
* disconnect), this is a no-op.
*
* @param sub Sequence number returned by `InfoSub::getSeq()` for the
* subscriber to remove.
*/
void
removeSubscriber(std::uint64_t sub);
/** Publish a transaction to subscribers
Publish a transaction to clients subscribed to changes on this book.
Uses havePublished to prevent sending duplicate transactions to clients
that have subscribed to multiple books.
@param jvObj JSON transaction data to publish
@param havePublished InfoSub sequence numbers that have already
published this transaction.
*/
/** Deliver a transaction notification to all live subscribers.
*
* Iterates over the internal listener map and, for each live subscriber,
* attempts to insert its sequence number into @p havePublished. If the
* insertion succeeds (i.e. this subscriber has not yet received this
* transaction from another book), the version-appropriate JSON is
* dispatched via `InfoSub::send()`.
*
* Dead weak pointers (subscribers that have disconnected) are erased
* in-place during the scan, providing lazy GC without a separate sweep.
*
* @param jvObj Version-indexed JSON built once upstream; each subscriber
* receives the slice matching its negotiated API version.
* @param havePublished Per-transaction set of subscriber sequence numbers
* that have already been notified. Shared across every
* `BookListeners::publish()` call for the same transaction so that a
* client subscribed to multiple affected books receives the message
* only once. Passed by reference and mutated in-place.
*/
void
publish(MultiApiJson const& jvObj, hash_set<std::uint64_t>& havePublished);

View File

@@ -1,3 +1,12 @@
/** @file
* Process-wide cache of deserialized ledger state entries (SLEs).
*
* Declares `CachedSLEs`, a named alias for the `TaggedCache` instantiation
* that backs the two-level SLE read cache used by `CachedView`. Any future
* change to the underlying container's key hasher, pointer policy, or mutex
* type can be made here without touching consumers.
*/
#pragma once
#include <xrpl/basics/TaggedCache.h>
@@ -5,5 +14,32 @@
#include <xrpl/protocol/STLedgerEntry.h>
namespace xrpl {
/** Process-wide, thread-safe cache of immutable ledger state entries (SLEs).
*
* Maps the cryptographic digest of a serialized SLE (`uint256`) to the
* deserialized `SLE const` object, allowing multiple read paths to share a
* single in-memory representation without re-deserializing from disk.
*
* The `SLE const` mapped type enforces at compile time that stored objects
* are never mutated through the cache, satisfying `TaggedCache`'s requirement
* that callers must not modify stored objects unless they hold a lock over all
* cache operations. This makes cached entries safe to share across threads
* without additional per-object locking.
*
* The key is the on-disk hash (digest) of the serialized entry — not an
* account ID or keylet — which integrates directly with `DigestAwareReadView`.
* `CachedView` delegates `read()` calls to `CachedSLEs::fetch(digest, ...)`,
* falling through to the underlying store only on a miss.
*
* The application-wide instance is constructed with a target size of `0`
* (no fixed count limit) and a one-minute expiration window.
* `TaggedCache::sweep()` is called periodically to demote strong references
* to weak references and eventually reclaim memory.
*
* @see CachedView
* @see TaggedCache
*/
using CachedSLEs = TaggedCache<uint256, SLE const>;
} // namespace xrpl

View File

@@ -1,3 +1,16 @@
/** @file
* Transparent two-level caching layer over a `DigestAwareReadView`.
*
* Declares `detail::CachedViewImpl` (non-template caching logic) and the
* public template `CachedView<Base>`, which adds `shared_ptr` ownership of
* the wrapped view. The canonical instantiation `CachedLedger` (defined in
* `Ledger.h`) wraps the immutable closed ledger that serves as the base for
* transaction application.
*
* @see CachedSLEs
* @see CachedLedger
*/
#pragma once
#include <xrpl/basics/hardened_hash.h>
@@ -11,12 +24,43 @@ namespace xrpl {
namespace detail {
/** Non-template base class that implements SLE caching over a `DigestAwareReadView`.
*
* All caching logic is compiled once here, avoiding template-instantiation bloat
* in `CachedView<Base>`. The class maintains two complementary caches:
*
* - **`map_`** — a per-instance `unordered_map` from ledger key (`uint256`) to
* SLE digest. Once a key has been resolved to its content hash, subsequent
* reads skip the SHAMap traversal. Uses `HardenedHash<>` to resist
* hash-flood attacks from adversarially crafted ledger keys.
* - **`cache_`** — a reference to an externally owned, process-wide `CachedSLEs`
* (`TaggedCache<uint256, SLE const>`) keyed by digest. Multiple views over
* different ledgers share this cache; if two ledgers carry an unchanged SLE,
* only one deserialized copy lives in memory.
*
* `mutex_` guards `map_` only; it is deliberately *not* held across
* `base_.digest()` or `base_.read()` calls so that concurrent readers are not
* serialized through SHAMap traversal or deserialization. Two threads may both
* call `base_.digest()` for the same key on a cold miss — this is safe because
* `base_` is an immutable ledger snapshot.
*
* Copy and assignment are deleted; a cached view always represents a unique,
* coherent window onto a specific ledger snapshot.
*
* @note All `ReadView` and `DigestAwareReadView` pass-through methods delegate
* directly to `base_`; only `exists()` and `read()` go through the cache.
*/
class CachedViewImpl : public DigestAwareReadView
{
private:
DigestAwareReadView const& base_;
CachedSLEs& cache_;
std::mutex mutable mutex_;
/** Per-instance map from ledger key to SLE digest.
*
* Uses `HardenedHash<>` to prevent adversarial hash-bucket flooding from
* network-visible ledger keys (account IDs, object types).
*/
std::unordered_map<key_type, uint256, HardenedHash<>> mutable map_;
public:
@@ -25,6 +69,13 @@ public:
CachedViewImpl&
operator=(CachedViewImpl const&) = delete;
/** Construct over an existing `DigestAwareReadView` and a shared SLE cache.
*
* @param base The underlying immutable view to cache reads against.
* The caller is responsible for ensuring `base` outlives this object;
* `CachedView<Base>` satisfies this by holding the owning `shared_ptr`.
* @param cache The process-wide SLE cache shared across all views.
*/
CachedViewImpl(DigestAwareReadView const* base, CachedSLEs& cache) : base_(*base), cache_(cache)
{
}
@@ -33,9 +84,30 @@ public:
// ReadView
//
/** Returns `true` if an SLE exists for the given keylet.
*
* Delegates to `read(k) != nullptr`; benefits from caching on repeated
* calls for the same key.
*/
bool
exists(Keylet const& k) const override;
/** Return the SLE associated with the keylet, going through both cache levels.
*
* The lookup sequence is:
* 1. Check `map_` for a known digest (under `mutex_`).
* 2. If absent, call `base_.digest(k.key)` outside the lock.
* 3. Pass the digest to `cache_.fetch()`, which deserializes from `base_`
* only on a shared-cache miss.
* 4. Populate `map_` on a cold miss (re-acquires `mutex_`).
* 5. Validate the SLE type with `k.check(*sle)`.
*
* Hit/miss statistics are tracked via `CountedObjects` counters
* `CachedView::hit`, `CachedView::hitExpired`, and `CachedView::miss`.
*
* @return The matching `SLE const`, or `nullptr` if the key is absent or
* the stored type does not match the keylet's expected type.
*/
std::shared_ptr<SLE const>
read(Keylet const& k) const override;
@@ -124,10 +196,25 @@ public:
} // namespace detail
/** Wraps a DigestAwareReadView to provide caching.
@tparam Base A subclass of DigestAwareReadView
*/
/** Transparent caching layer over a `DigestAwareReadView`.
*
* Wraps a `shared_ptr<Base const>` to ensure the underlying view remains alive
* for the lifetime of this object, then delegates all caching logic to
* `detail::CachedViewImpl`. The `static_assert` enforces that `Base` satisfies
* the `DigestAwareReadView` contract required for two-level caching.
*
* The production instantiation is `CachedLedger = CachedView<Ledger>`, used
* by `OpenLedger::create()` to wrap the closed ledger that forms the base for
* each round of transaction application.
*
* Copy and assignment are deleted; each `CachedView` instance is the sole
* owner of its per-instance key→digest `map_`.
*
* @tparam Base A type derived from `DigestAwareReadView`.
*
* @see detail::CachedViewImpl
* @see CachedSLEs
*/
template <class Base>
class CachedView : public detail::CachedViewImpl
{
@@ -144,15 +231,27 @@ public:
CachedView&
operator=(CachedView const&) = delete;
/** Construct a caching view over a shared immutable ledger snapshot.
*
* @param base Shared ownership of the underlying view; must not be null.
* @param cache Process-wide SLE cache shared across all `CachedView`
* instances. Must outlive this object.
*/
CachedView(std::shared_ptr<Base const> const& base, CachedSLEs& cache)
: CachedViewImpl(base.get(), cache), sp_(base)
{
}
/** Returns the base type.
@note This breaks encapsulation and bypasses the cache.
*/
/** Return the underlying view, bypassing both cache levels.
*
* @note This breaks encapsulation: callers interact with the
* `DigestAwareReadView` directly, skipping both the per-instance
* key→digest `map_` and the shared `CachedSLEs`. Use only when the
* full `Base` type (e.g. `Ledger`) is needed and cannot be expressed
* through the `ReadView` interface alone.
*
* @return A const shared pointer to the wrapped `Base` instance.
*/
std::shared_ptr<Base const> const&
base() const
{

View File

@@ -7,17 +7,51 @@
namespace xrpl {
/** Holds transactions which were deferred to the next pass of consensus.
"Canonical" refers to the order in which transactions are applied.
- Puts transactions from the same account in SeqProxy order
*/
/** Ordered transaction queue for deterministic consensus application.
*
* Holds transactions deferred from a previous ledger-building pass and
* re-applies them in the next pass. The "canonical" in the name is the
* ordering guarantee: given the same input transaction set and the same
* salt, every validator iterates and applies transactions in identical
* sequence, which is required for Byzantine fault-tolerant ledger
* agreement.
*
* Ordering is three-level (implemented in `Key::operator<`):
* 1. Salted account ID — groups all transactions from the same account.
* 2. `SeqProxy` — within an account, sequence-based transactions sort
* before ticket-based ones, preserving the dependency that a ticket
* creator must apply before ticket consumers.
* 3. Transaction ID — tiebreaker within the same account and sequence.
*
* @note Account keys are XORed with a `LedgerHash` salt at construction
* (and via `reset()`) so that no actor can mine account addresses to
* achieve a persistent early-sort advantage across ledger rounds.
*
* @note Inherits from `CountedObject<CanonicalTXSet>` for diagnostic
* memory-pressure accounting; the instance count is queryable via
* `CountedObjects::getInstance().getCounts()` and has no effect on
* behavior.
*
* Usage in `BuildLedger.cpp`: `applyTransactions()` iterates this set in
* map order across multiple passes, erasing each transaction on success or
* definitive failure and leaving retryable ones in place for the next pass.
*/
// VFALCO TODO rename to SortedTxSet
class CanonicalTXSet : public CountedObject<CanonicalTXSet>
{
private:
/** Sort key for the internal transaction map.
*
* Holds a salted account identifier, a `SeqProxy`, and the transaction
* hash. The three-level `operator<` groups transactions by account, then
* orders within an account by `SeqProxy` (sequences before tickets), then
* breaks ties by transaction ID.
*
* `operator==` compares only `txId_` — identity is the transaction hash
* alone, independent of account or sequence context. This asymmetry is
* intentional: iterator-based `erase` must not conflate distinct
* transactions that happen to share account/sequence metadata.
*/
class Key
{
public:
@@ -47,6 +81,14 @@ private:
return !(lhs < rhs);
}
/** Tests equality by transaction ID only.
*
* Deliberately asymmetric with `operator<`: two keys with different
* account/sequence values but the same `txId_` compare equal. This
* keeps iterator-based removal (`erase`) safe — the map's ordering
* key is account+seq+id, but uniqueness is solely the transaction
* hash.
*/
friend bool
operator==(Key const& lhs, Key const& rhs)
{
@@ -59,12 +101,14 @@ private:
return !(lhs == rhs);
}
/** Returns the salted account identifier used as the primary sort key. */
[[nodiscard]] uint256 const&
getAccount() const
{
return account_;
}
/** Returns the transaction hash. */
[[nodiscard]] uint256 const&
getTXID() const
{
@@ -80,7 +124,14 @@ private:
friend bool
operator<(Key const& lhs, Key const& rhs);
// Calculate the salted key for the given account
/** Computes the salted sort key for an account.
*
* Copies the 20-byte `AccountID` into a zeroed `uint256`, then XORs the
* result with `salt_`. The XOR prevents an attacker from mining account
* addresses with low byte values to gain a persistent ordering advantage:
* because `salt_` changes each ledger round, the effective sort position
* of any account is randomized per round.
*/
uint256
accountKey(AccountID const& account);
@@ -88,23 +139,59 @@ public:
using const_iterator = std::map<Key, std::shared_ptr<STTx const>>::const_iterator;
public:
/** Constructs the set with the given ledger hash as the account-key salt.
*
* @param saltHash Hash of the current ledger (or consensus map); used to
* randomize per-round account sort positions. Pass `uint256{}` when a
* stable, unsalted ordering is acceptable (e.g., `LocalTxs`).
*/
explicit CanonicalTXSet(LedgerHash const& saltHash) : salt_(saltHash)
{
}
/** Inserts a transaction into the set.
*
* Constructs a `Key` from the transaction's salted account ID, `SeqProxy`,
* and transaction hash, then inserts the `(Key, tx)` pair into the map.
* Duplicate inserts (same transaction hash) are silently ignored by the
* underlying `std::map`.
*
* @param txn The signed transaction to enqueue.
*/
void
insert(std::shared_ptr<STTx const> const& txn);
// Pops the next transaction on account that follows seqProx in the
// sort order. Normally called when a transaction is successfully
// applied to the open ledger so the next transaction can be resubmitted
// without waiting for ledger close.
//
// The return value is often null, when an account has no more
// transactions.
/** Pops and returns the next eligible transaction for the same account.
*
* After `tx` has been successfully applied to the open ledger, call this
* method to retrieve and remove the immediately-following transaction for
* the same account, if one exists and is eligible. A transaction is
* eligible if it either:
* - uses a ticket (tickets may be applied regardless of sequence gaps), or
* - has a sequence number exactly one greater than `tx`'s sequence.
*
* The search uses `lower_bound` on a synthetic key whose `txId_` is
* `beast::zero` (which sorts before any real transaction ID) to locate the
* first map entry past `tx`'s position. If that entry belongs to a
* different account, or its sequence constraint is not satisfied, the
* method returns `nullptr`.
*
* @param tx The just-applied transaction whose account and sequence
* establish the search anchor.
* @return The next eligible transaction (removed from the set), or
* `nullptr` if no suitable successor exists.
*/
std::shared_ptr<STTx const>
popAcctTransaction(std::shared_ptr<STTx const> const& tx);
/** Resets the set for a new ledger round.
*
* Installs a fresh salt and clears all transactions, allowing the same
* `CanonicalTXSet` instance to be reused across rounds without
* reallocating the underlying container.
*
* @param salt New ledger hash to use as the account-key salt.
*/
void
reset(LedgerHash const& salt)
{
@@ -112,35 +199,54 @@ public:
map_.clear();
}
/** Erases the transaction at `it` and returns an iterator to the next element.
*
* Supports in-place removal during iteration, as used by `applyTransactions()`
* in `BuildLedger.cpp` when a transaction succeeds or definitively fails.
*
* @param it A valid iterator into this set.
* @return Iterator to the element following the removed one.
*/
const_iterator
erase(const_iterator const& it)
{
return map_.erase(it);
}
/** Returns an iterator to the first transaction in canonical order. */
[[nodiscard]] const_iterator
begin() const
{
return map_.begin();
}
/** Returns a past-the-end iterator. */
[[nodiscard]] const_iterator
end() const
{
return map_.end();
}
/** Returns the number of transactions currently in the set. */
[[nodiscard]] size_t
size() const
{
return map_.size();
}
/** Returns `true` if the set contains no transactions. */
[[nodiscard]] bool
empty() const
{
return map_.empty();
}
/** Returns the salt hash that identifies this set's ordering context.
*
* Callers use this for logging the transaction set identity alongside
* the ledger close time (e.g., `RCLConsensus` logs `retriableTxs.key()`
* when building the canonical set from the consensus map).
*/
[[nodiscard]] uint256 const&
key() const
{
@@ -150,7 +256,9 @@ public:
private:
std::map<Key, std::shared_ptr<STTx const>> map_;
// Used to salt the accounts so people can't mine for low account numbers
// XORed into each account's sort key to prevent mining for low account
// numbers that would gain a persistent ordering advantage. Refreshed each
// ledger round via reset().
uint256 salt_;
};

View File

@@ -5,18 +5,22 @@
namespace xrpl {
/** A class that simplifies iterating ledger directory pages
The Dir class provides a forward iterator for walking through
the uint256 values contained in ledger directories.
The Dir class also allows accelerated directory walking by
stepping directly from one page to the next using the next_page()
member function.
As of July 2024, the Dir class is only being used with NFTokenOffer
directories and for unit tests.
*/
/** Read-only range adaptor for a paged ledger directory (`ltDIR_NODE`).
*
* A ledger directory is a linked list of `DirectoryNode` SLEs, each holding
* a `STVector256` (`sfIndexes`) of 256-bit keys pointing to other ledger
* objects. `Dir` wraps that structure in a C++ forward-iterable range,
* hiding page-chasing and SLE loading behind `begin()`/`end()`.
*
* Construction reads the root page eagerly but loads no entry SLEs;
* per-entry loading is deferred to `operator*()`. The class is used
* with NFTokenOffer directories (`keylet::nft_buys()`, `keylet::nft_sells()`)
* and in unit tests with owner directories (`keylet::ownerDir()`).
*
* @note Callers that only need per-page counts (not per-entry SLEs) should
* use `nextPage()` as the loop increment and `pageSize()` for counting,
* which avoids the per-entry `ReadView::read()` calls entirely.
*/
class Dir
{
private:
@@ -27,17 +31,57 @@ private:
public:
class ConstIterator;
/** `shared_ptr<SLE const>`, matching `ReadView::read()`'s return type. */
using value_type = std::shared_ptr<SLE const>;
/** Construct a range over the directory rooted at `key` in `view`.
*
* Reads the root `DirectoryNode` SLE immediately and caches its
* `sfIndexes`. If the root page is absent the range is empty.
*
* @param view The ledger view to read from; must outlive this object.
* @param key Keylet of the directory root page.
*/
Dir(ReadView const&, Keylet const&);
/** Return an iterator to the first entry of the directory.
*
* If the root page is missing or its `sfIndexes` is empty, the returned
* iterator compares equal to `end()`.
*
* @return A `ConstIterator` positioned at the first directory entry,
* or `end()` if the directory is empty.
*/
[[nodiscard]] ConstIterator
begin() const;
/** Return a past-the-end sentinel iterator.
*
* The sentinel has `page_.key == root_.key` and `index_ == beast::zero`.
* An iterator reaches this state when `nextPage()` finds `sfIndexNext == 0`
* on the last `DirectoryNode` page.
*
* @return A past-the-end `ConstIterator`.
*/
[[nodiscard]] ConstIterator
end() const;
};
/** Forward iterator over entries in a paged ledger directory.
*
* Each dereference lazily loads the ledger object pointed to by the current
* directory entry key via `ReadView::read(keylet::child(index_))`. The result
* is cached in `cache_` and cleared on every advance, including page
* transitions.
*
* Equality compares `page_.key` and `index_`. Two iterators are equal when
* both fields match; comparing iterators from different views or roots is
* undefined (asserted in debug builds).
*
* @note Advancing an iterator that is already at `end()` is undefined.
* Always guard with `it != dir.end()` before incrementing.
*/
class Dir::ConstIterator
{
public:
@@ -47,42 +91,113 @@ public:
using difference_type = std::ptrdiff_t;
using iterator_category = std::forward_iterator_tag;
/** Return true if both iterators point to the same directory entry.
*
* Returns `false` if either view pointer is null. Asserts in debug builds
* that both iterators share the same view and root keylet.
*
* @param other The iterator to compare against.
* @return `true` if `page_.key` and `index_` match in both iterators.
*/
bool
operator==(ConstIterator const& other) const;
/** Return true if the iterators do not point to the same directory entry.
*
* @param other The iterator to compare against.
* @return `!(*this == other)`.
*/
bool
operator!=(ConstIterator const& other) const
{
return !(*this == other);
}
/** Load and return the ledger object for the current directory entry.
*
* The result is cached after the first call and reused on subsequent
* dereferences of the same position. The cache is cleared on every
* advance (including page transitions).
*
* @return `shared_ptr<SLE const>` to the referenced ledger object,
* or `nullptr` if the object is not present in the view.
*/
reference
operator*() const;
/** Return a pointer to the current entry's `shared_ptr<SLE const>`.
*
* @return Pointer to the cached SLE shared pointer.
*/
pointer
operator->() const
{
return &**this;
}
/** Advance to the next directory entry, crossing page boundaries as needed.
*
* When the end of the current page's `sfIndexes` is reached, calls
* `nextPage()` to load the subsequent `DirectoryNode`. If no next page
* exists the iterator converges to the `end()` sentinel.
*
* @return Reference to this iterator after advancement.
*/
ConstIterator&
operator++();
/** Post-increment: return a copy of this iterator, then advance.
*
* @return Copy of the iterator before advancement.
*/
ConstIterator
operator++(int);
/** Jump directly to the first entry of the next `DirectoryNode` page.
*
* Reads `sfIndexNext` from the current page SLE. If the value is zero
* (last page), the iterator is set to the `end()` sentinel. Otherwise,
* loads `keylet::page(root_, sfIndexNext)` and positions the iterator
* at the beginning of that page's `sfIndexes`.
*
* This method is public so callers can skip an entire page without
* loading individual entries — useful when only the per-page count is
* needed (see `pageSize()`).
*
* @return Reference to this iterator, now positioned at the start of the
* next page, or at `end()` if the directory is exhausted.
*/
ConstIterator&
nextPage();
/** Return the number of entries on the current page.
*
* Reports `sfIndexes.size()` for the currently loaded `DirectoryNode`
* without reading any entry SLEs. Combined with `nextPage()` as a loop
* increment, this enables O(pages) offer-count checks instead of
* O(entries).
*
* @return Number of `uint256` entries in the current page's `sfIndexes`.
*/
std::size_t
pageSize();
/** Return the keylet of the currently loaded `DirectoryNode` page.
*
* @return `Keylet` identifying the current page SLE.
*/
Keylet const&
page() const
{
return page_;
}
/** Return the `uint256` key of the current directory entry.
*
* Equal to `beast::zero` when the iterator is at `end()`.
*
* @return The current entry's 256-bit ledger object key.
*/
uint256
index() const
{

View File

@@ -1,3 +1,9 @@
/** @file
* Declares the Ledger class — the central data structure of the XRP Ledger
* daemon — together with supporting types for genesis ledger construction
* and the CachedLedger alias.
*/
#pragma once
#include <xrpl/basics/CountedObject.h>
@@ -20,44 +26,58 @@ class TransactionMaster;
class SqliteStatement;
/** Tag type used to select the genesis-ledger constructor of Ledger.
*
* Pass the singleton `kCREATE_GENESIS` constant to construct ledger
* sequence 1. The explicit constructor prevents accidental conversions.
*/
struct CreateGenesisT
{
explicit CreateGenesisT() = default;
};
/** Singleton tag constant passed to the genesis-ledger constructor. */
extern CreateGenesisT const kCREATE_GENESIS;
/** Holds a ledger.
The ledger is composed of two SHAMaps. The state map holds all of the
ledger entries such as account roots and order books. The tx map holds
all of the transactions and associated metadata that made it into that
particular ledger. Most of the operations on a ledger are concerned
with the state map.
This can hold just the header, a partial set of data, or the entire set
of data. It all depends on what is in the corresponding SHAMap entry.
Various functions are provided to populate or depopulate the caches that
the object holds references to.
Ledgers are constructed as either mutable or immutable.
1) If you are the sole owner of a mutable ledger, you can do whatever you
want with no need for locks.
2) If you have an immutable ledger, you cannot ever change it, so no need
for locks.
3) Mutable ledgers cannot be shared.
@note Presented to clients as ReadView
@note Calls virtuals in the constructor, so marked as final
*/
/** Immutable or mutable snapshot of the XRP Ledger at a single sequence number.
*
* A Ledger owns two SHAMap Merkleradix trees: `stateMap_` (all account
* state — account roots, trust lines, offers, escrows, amendments, fee
* settings, etc.) and `txMap_` (every transaction together with its
* execution metadata that produced this ledger's state).
*
* **Mutable/immutable lifecycle:**
* - A freshly constructed ledger begins mutable; it must not be shared
* across threads while mutable.
* - After `setImmutable()` is called the ledger hashes are finalised,
* both SHAMaps are locked, and the object may be shared freely without
* any locking. Any attempt to mutate the SHAMaps after this point will
* assert.
* - `setAccepted()` is the standard close-time + `setImmutable()` sequence
* used after consensus.
*
* The class inherits `DigestAwareReadView` (read + per-entry digest),
* `TxsRawView` (raw state and transaction mutation), and
* `CountedObject<Ledger>` (intrusive diagnostics). It is marked `final`
* because constructors call virtual functions through `setup()`.
*
* @note Presented to most callers through the `ReadView` interface.
* @note `txMap_` and `stateMap_` are declared `mutable` to allow
* `setFull()` and iterator operations in `const` contexts without
* compromising the logical-constness contract.
* @see CachedLedger — the standard shareable form used at rest.
*/
class Ledger final : public std::enable_shared_from_this<Ledger>,
public DigestAwareReadView,
public TxsRawView,
public CountedObject<Ledger>
{
public:
/** Copying and moving are prohibited.
*
* Ledger objects are always owned through `std::shared_ptr`. Shared
* ownership combined with the mutable-→-immutable transition makes
* value-semantic copies unsafe and unnecessary.
*/
Ledger(Ledger const&) = delete;
Ledger&
operator=(Ledger const&) = delete;
@@ -66,20 +86,22 @@ public:
Ledger&
operator=(Ledger&&) = delete;
/** Create the Genesis ledger.
The Genesis ledger contains a single account whose
AccountID is generated with a Generator using the seed
computed from the string "masterpassphrase" and ordinal
zero.
The account has an XRP balance equal to the total amount
of XRP in the system. No more XRP than the amount which
starts in this account can ever exist, with amounts
used to pay fees being destroyed.
Amendments specified are enabled in the genesis ledger
*/
/** Construct ledger sequence 1 (the genesis ledger).
*
* Seeds a single master account whose `AccountID` is derived
* deterministically from the seed of `"masterpassphrase"`, credits it
* with `kINITIAL_XRP` drops, inserts the `sfAmendments` SLE for any
* pre-enabled amendments, and inserts the fee schedule SLE using either
* drop-native fields (`sfBaseFeeDrops`, etc.) when `featureXRPFees` is
* among `amendments`, or legacy integer fields otherwise. Ends with
* `setImmutable()`.
*
* @param rules Protocol rules in effect at genesis.
* @param fees Initial fee schedule (base fee, reserve, increment).
* @param amendments Amendments that are enabled from ledger 1 onward.
* Determines which fee-field format is used for the genesis fee SLE.
* @param family Node-store family that owns the SHAMap backing storage.
*/
Ledger(
CreateGenesisT,
Rules rules,
@@ -87,15 +109,37 @@ public:
std::vector<uint256> const& amendments,
Family& family);
/** Construct an immutable header-only placeholder ledger.
*
* Creates SHAMaps initialised with the root hashes from `info` but does
* not attempt to fetch SHAMap nodes from the node store. The canonical
* ledger hash is computed immediately from the header fields. Used for
* skeleton or partial ledgers reconstructed from database metadata.
*
* @param info Fully populated ledger header (must include root hashes).
* @param rules Protocol rules in effect for this ledger.
* @param family Node-store family for the underlying SHAMaps.
*/
Ledger(LedgerHeader const& info, Rules rules, Family& family);
/** Used for ledgers loaded from JSON files
@param acquire If true, acquires the ledger if not found locally
@note The fees parameter provides default values, but setup() may
override them from the ledger state if fee-related SLEs exist.
*/
/** Restore a ledger from its header, fetching SHAMap roots from the node store.
*
* Constructs both SHAMaps with the root hashes from `info` and calls
* `fetchRoot()` on each. If either root is absent from the node store,
* `loaded` is set to `false`; when `acquire` is also `true`, async
* acquisition is triggered via `family.missingNodeAcquireByHash()`.
* The resulting ledger is always immutable.
*
* @param info Ledger header, including `txHash` and `accountHash` roots.
* @param loaded Set to `false` on return if either SHAMap root was missing.
* @param acquire If `true`, trigger async node acquisition when `loaded`
* would be set to `false`.
* @param rules Protocol rules in effect for this ledger.
* @param fees Default fee values; `setup()` will override these from the
* on-ledger fee SLE if one exists.
* @param family Node-store family for the underlying SHAMaps.
* @param j Journal for missing-root warnings.
*/
Ledger(
LedgerHeader const& info,
bool& loaded,
@@ -105,15 +149,33 @@ public:
Family& family,
beast::Journal j);
/** Create a new ledger following a previous ledger
The ledger will have the sequence number that
follows previous, and have
parentCloseTime == previous.closeTime.
*/
/** Create the next mutable ledger in the chain following `previous`.
*
* The new ledger has sequence `previous.seq() + 1`. Its `stateMap_`
* is a copy-on-write snapshot of `previous.stateMap_` so state changes
* do not affect the closed parent. Its `txMap_` starts empty (a fresh
* SHAMap for the new round's transactions). `parentCloseTime` is set
* to `previous.closeTime`; the close-time resolution is advanced via
* `getNextLedgerTimeResolution`.
*
* @param previous The preceding closed ledger; must be immutable.
* @param closeTime Proposed close time for the new ledger.
*/
Ledger(Ledger const& previous, NetClock::time_point closeTime);
// used for database ledgers
/** Construct a mutable empty ledger for database reconstruction.
*
* Creates an empty, mutable ledger at `ledgerSeq` and calls `setup()`
* to initialise `fees_` and `rules_` from any state entries that may
* already exist. Used when the node store needs to rebuild a ledger
* from raw DB data outside the normal consensus flow.
*
* @param ledgerSeq Target ledger sequence number.
* @param closeTime Close time to record in the ledger header.
* @param rules Protocol rules for this ledger.
* @param fees Initial fee schedule (may be overridden by `setup()`).
* @param family Node-store family for the underlying SHAMaps.
*/
Ledger(
std::uint32_t ledgerSeq,
NetClock::time_point closeTime,
@@ -127,66 +189,118 @@ public:
// ReadView
//
/** Always returns `false`; Ledger objects are never open. */
bool
open() const override
{
return false;
}
/** Returns the ledger header (sequence, hashes, close time, drops, etc.). */
LedgerHeader const&
header() const override
{
return header_;
}
/** Overwrite the in-memory ledger header wholesale.
*
* Used during ledger reconstruction from external data before the
* ledger is made immutable. Do not call on an immutable ledger.
*
* @param info New header to install.
*/
void
setLedgerInfo(LedgerHeader const& info)
{
header_ = info;
}
/** Returns the fee schedule parsed from the on-ledger fee SLE. */
Fees const&
fees() const override
{
return fees_;
}
/** Returns the protocol rules in effect for this ledger. */
Rules const&
rules() const override
{
return rules_;
}
/** Returns `true` if the state map contains an entry matching `k`.
*
* @param k Keylet identifying the ledger entry (type + key).
*/
bool
exists(Keylet const& k) const override;
/** Returns `true` if the state map contains an entry at the raw key.
*
* @param key 256-bit SHAMap key to look up (no type check).
*/
bool
exists(uint256 const& key) const;
/** Find the smallest state-map key strictly greater than `key`.
*
* @param key Lower bound (exclusive) for the search.
* @param last If set, keys >= `last` are not returned.
* @return The next key, or `std::nullopt` if none exists in range.
*/
std::optional<uint256>
succ(uint256 const& key, std::optional<uint256> const& last = std::nullopt) const override;
/** Deserialize and return the state entry identified by `k`.
*
* Checks the keylet type against the deserialized SLE; returns
* `nullptr` if the key is missing or the type check fails.
*
* @param k Keylet specifying the key and expected ledger-entry type.
* @return Shared pointer to the immutable SLE, or `nullptr`.
*/
std::shared_ptr<SLE const>
read(Keylet const& k) const override;
/** Return a begin iterator over all state-map entries. */
std::unique_ptr<SlesType::iter_base>
slesBegin() const override;
/** Return a past-the-end iterator over all state-map entries. */
std::unique_ptr<SlesType::iter_base>
slesEnd() const override;
/** Return an iterator to the first state-map entry with key > `key`. */
std::unique_ptr<SlesType::iter_base>
slesUpperBound(uint256 const& key) const override;
/** Return a begin iterator over all transaction-map entries.
*
* @note Transactions are yielded with metadata for closed ledgers and
* without metadata for open ledgers (always closed for `Ledger`).
*/
std::unique_ptr<TxsType::iter_base>
txsBegin() const override;
/** Return a past-the-end iterator over all transaction-map entries. */
std::unique_ptr<TxsType::iter_base>
txsEnd() const override;
/** Returns `true` if the transaction map contains an entry for `key`. */
bool
txExists(uint256 const& key) const override;
/** Deserialize and return the transaction (plus metadata) for `key`.
*
* For a closed ledger both the `STTx` and the `STObject` metadata are
* returned. Returns an empty pair if the key is not present.
*
* @param key Transaction ID to look up.
* @return Pair of `(STTx const*, STObject const*)` shared pointers;
* either or both may be null on miss.
*/
tx_type
txRead(key_type const& key) const override;
@@ -194,6 +308,17 @@ public:
// DigestAwareReadView
//
/** Return the Merkle hash of the state-map leaf at `key`.
*
* Used by `CachedView` to detect whether a cached SLE is stale.
* Returns `std::nullopt` if no entry exists at `key`.
*
* @note The current implementation loads the SHAMap item from the node
* store as a side-effect; see the inline comment in `Ledger.cpp`.
*
* @param key 256-bit state-map key to hash.
* @return The leaf node hash, or `std::nullopt` if absent.
*/
std::optional<digest_type>
digest(key_type const& key) const override;
@@ -201,18 +326,53 @@ public:
// RawView
//
/** Remove the state entry whose key matches `sle->key()`.
*
* Calls `logicError` if the key does not exist in the state map.
*
* @param sle Entry to remove; only the key is used.
*/
void
rawErase(std::shared_ptr<SLE> const& sle) override;
/** Insert a new state entry for `sle`.
*
* Serializes the SLE and adds it to the state SHAMap. Calls
* `logicError` if an entry with the same key already exists.
*
* @param sle Entry to insert; must not already be present.
*/
void
rawInsert(std::shared_ptr<SLE> const& sle) override;
/** Remove the state entry at the raw key `key`.
*
* Overload for callers that hold only the key rather than an SLE.
* Calls `logicError` if the key does not exist.
*
* @param key 256-bit state-map key of the entry to remove.
*/
void
rawErase(uint256 const& key);
/** Replace (overwrite) an existing state entry with `sle`.
*
* Serializes the SLE and updates the state SHAMap in place. Calls
* `logicError` if no entry exists at `sle->key()`.
*
* @param sle Replacement entry; key must already be present.
*/
void
rawReplace(std::shared_ptr<SLE> const& sle) override;
/** Burn `fee` drops from the ledger's total XRP supply.
*
* Implements XRPL's deflationary model: transaction fees are
* permanently destroyed rather than redistributed. Decrements
* `header_.drops` directly.
*
* @param fee Amount to deduct from the total coin supply.
*/
void
rawDestroyXRP(XRPAmount const& fee) override
{
@@ -223,6 +383,17 @@ public:
// TxsRawView
//
/** Append a transaction + metadata blob to the transaction map.
*
* Encodes `txn` and `metaData` as two back-to-back variable-length
* fields and inserts the result at `key`. Asserts that `metaData`
* is non-null (open ledgers must not call this). Calls `logicError`
* if `key` is already present (duplicate transaction).
*
* @param key Transaction ID (SHAMap key).
* @param txn Serialized transaction blob.
* @param metaData Serialized transaction metadata blob; must be non-null.
*/
void
rawTxInsert(
uint256 const& key,
@@ -231,37 +402,66 @@ public:
//--------------------------------------------------------------------------
/** Mark this ledger as validated by the network.
*
* Sets `header_.validated = true`. This is a local-node annotation
* only; it does not affect the consensus hash or any on-ledger state.
*/
void
setValidated() const
{
header_.validated = true;
}
/** Finalise timing fields and transition this ledger to immutable.
*
* Records `closeTime`, `closeResolution`, and the close-flag
* (`kS_LCF_NO_CONSENSUS_TIME` when `correctCloseTime` is `false`),
* then delegates to `setImmutable()`.
*
* @pre `!open()` — the ledger must already be closed.
*
* @param closeTime Agreed consensus close time.
* @param closeResolution Resolution used to bin the close time.
* @param correctCloseTime `true` if consensus agreed on the close time;
* `false` sets the no-consensus-time flag in the header.
*/
void
setAccepted(
NetClock::time_point closeTime,
NetClock::duration closeResolution,
bool correctCloseTime);
/** Compute hashes and lock the ledger against further mutation.
*
* When `rehash` is `true` (the default): computes `header_.txHash`
* and `header_.accountHash` from the respective SHAMap roots, then
* computes the canonical ledger hash via `calculateLedgerHash()`.
* Regardless of `rehash`, sets `immutable_ = true`, calls
* `setImmutable()` on both SHAMaps, and calls `setup()` to populate
* `fees_` and `rules_` from the state map.
*
* @param rehash If `false`, skip hash computation (used when the
* hashes are already known, e.g. on load from the database).
*/
void
setImmutable(bool rehash = true);
/** Returns `true` if `setImmutable()` has been called on this ledger. */
bool
isImmutable() const
{
return immutable_;
}
/* Mark this ledger as "should be full".
"Full" is metadata property of the ledger, it indicates
that the local server wants all the corresponding nodes
in durable storage.
This is marked `const` because it reflects metadata
and not data that is in common with other nodes on the
network.
*/
/** Tell the node store to retain all SHAMap nodes for this ledger.
*
* "Full" is a local storage policy: when set, the node store will keep
* all state-map and transaction-map nodes for this ledger in durable
* storage rather than evicting them. Declared `const` because fullness
* is node-local metadata — two nodes holding the same ledger may differ
* on this property without affecting consensus.
*/
void
setFull() const
{
@@ -271,145 +471,283 @@ public:
stateMap_.setLedgerSeq(header_.seq);
}
/** Overwrite the total XRP supply recorded in the ledger header.
*
* Used when building ledgers from external data sources (e.g. JSON
* import) before the ledger is made immutable.
*
* @param totDrops New total supply in drops.
*/
void
setTotalDrops(std::uint64_t totDrops)
{
header_.drops = totDrops;
}
/** Returns a read-only reference to the state SHAMap. */
SHAMap const&
stateMap() const
{
return stateMap_;
}
/** Returns a mutable reference to the state SHAMap.
*
* @note Only valid while the ledger is mutable.
*/
SHAMap&
stateMap()
{
return stateMap_;
}
/** Returns a read-only reference to the transaction SHAMap. */
SHAMap const&
txMap() const
{
return txMap_;
}
/** Returns a mutable reference to the transaction SHAMap.
*
* @note Only valid while the ledger is mutable.
*/
SHAMap&
txMap()
{
return txMap_;
}
// returns false on error
/** Serialize `sle` and add it directly to the state SHAMap.
*
* Convenience wrapper used during ledger construction from external
* data sources. Unlike `rawInsert`, this does not assert on failure.
*
* @param sle State entry to serialize and insert.
* @return `true` on success; `false` if the key already exists or the
* underlying `SHAMap::addItem` call fails.
*/
bool
addSLE(SLE const& sle);
//--------------------------------------------------------------------------
/** Update the two-tier skip list stored in the state map.
*
* The skip list enables O(1) historical hash lookup. This method
* maintains two SLEs:
* - `keylet::skip(prevIndex)` — a permanent record written for every
* 256-aligned predecessor sequence; stores up to 256 ancestor hashes.
* - `keylet::skip()` — a rolling window of the 256 most recent parent
* hashes; oldest entry is evicted when the list is full.
*
* Must be called on a mutable ledger before `setImmutable()`.
*/
void
updateSkipList();
/** Verify that every SHAMap node for this ledger is reachable.
*
* Walks both the state map and the transaction map and collects missing
* node reports. Logs the first missing node of each type to `j`.
*
* @param j Journal to receive missing-node diagnostics.
* @param parallel If `true`, walks the state map using parallel
* traversal (faster on multi-core hardware).
* @return `true` if both maps are fully present; `false` if any nodes
* are missing.
*/
bool
walkLedger(beast::Journal j, bool parallel = false) const;
/** Perform basic sanity checks on the ledger header vs. SHAMap hashes.
*
* Verifies that `header_.hash`, `header_.accountHash`, and
* `header_.txHash` are all non-zero and that the account and
* transaction hashes match the actual SHAMap roots.
*
* @return `true` if all checks pass.
*/
bool
isSensible() const;
/** Assert internal SHAMap invariants for both the state and tx maps.
*
* Delegates to `SHAMap::invariants()` on each map. Intended for
* debug-build integrity checks.
*/
void
invariants() const;
/** Release copy-on-write sharing of SHAMap nodes.
*
* After a copy-on-write snapshot is made (e.g. in the successor
* constructor), internal SHAMap nodes may be shared between the parent
* and child ledgers. Calling `unshare()` on the mutable child forces
* a deep copy so the two trees are fully independent.
*/
void
unshare() const;
/**
* get Negative UNL validators' master public keys
/** Read the current set of Negative UNL validators from the state map.
*
* @return the public keys
* The Negative UNL is a consensus mechanism that temporarily removes
* chronically offline validators without breaking liveness. This
* method reads the `sfDisabledValidators` array from the
* `keylet::negativeUNL()` SLE.
*
* @return Master public keys of all currently disabled validators;
* empty if no Negative UNL entry exists or it has no members.
*/
hash_set<PublicKey>
negativeUNL() const;
/**
* get the to be disabled validator's master public key if any
/** Return the validator scheduled for disabling at the next flag ledger.
*
* @return the public key if any
* Reads `sfValidatorToDisable` from the Negative UNL SLE, if present.
*
* @return The validator's master public key, or `std::nullopt` if none
* is pending.
*/
std::optional<PublicKey>
validatorToDisable() const;
/**
* get the to be re-enabled validator's master public key if any
/** Return the validator scheduled for re-enabling at the next flag ledger.
*
* @return the public key if any
* Reads `sfValidatorToReEnable` from the Negative UNL SLE, if present.
*
* @return The validator's master public key, or `std::nullopt` if none
* is pending.
*/
std::optional<PublicKey>
validatorToReEnable() const;
/**
* update the Negative UNL ledger component.
* @note must be called at and only at flag ledgers
* must be called before applying UNLModify Tx
/** Apply the pending Negative UNL changes recorded in the state map.
*
* Promotes `sfValidatorToDisable` into `sfDisabledValidators` and
* removes `sfValidatorToReEnable` from that array. If the resulting
* disabled set is empty, the entire Negative UNL SLE is deleted.
*
* @note Must be called exactly once per flag ledger (sequence divisible
* by 256) and *before* any `UNLModify` transaction is applied.
*/
void
updateNegativeUNL();
/** Returns true if the ledger is a flag ledger */
/** Returns `true` if this is a flag ledger (sequence divisible by 256).
*
* Flag ledgers carry out amendment votes, fee votes, and Negative UNL
* updates. These actions must not occur on non-flag ledgers.
*/
bool
isFlagLedger() const;
/** Returns true if the ledger directly precedes a flag ledger */
/** Returns `true` if this ledger directly precedes a flag ledger.
*
* Voting ledgers (flagSeq 1) are where validators cast their
* amendment and fee preferences before the flag-ledger processing pass.
*/
bool
isVotingLedger() const;
/** Deserialize and return a mutable SLE at keylet `k`.
*
* Unlike `read()`, the returned SLE is not `const` and may be passed
* to `rawReplace()` or `rawErase()`. Returns `nullptr` if the key
* is absent or the keylet type check fails.
*
* @note The caller must use the returned pointer only with the same
* `Ledger` instance; crossing to another view is a `LogicError`.
*
* @param k Keylet identifying the entry.
* @return Mutable SLE, or `nullptr` if not found.
*/
std::shared_ptr<SLE>
peek(Keylet const& k) const;
private:
/** SHAMap-backed iterator implementation for `ReadView::sles`. */
class SlesIterImpl;
/** SHAMap-backed iterator implementation for `ReadView::txs`.
*
* Deserializes with metadata for closed ledgers, without for open ones.
*/
class TxsIterImpl;
/** Populate `fees_` and `rules_` from the current state map.
*
* Reads `keylet::fees()` and applies the fee fields to `fees_`, then
* rebuilds `rules_` via `makeRulesGivenLedger`. Returns `false` if a
* `SHAMapMissingNode` is caught or if the fee SLE contains an illegal
* combination of old and new fee fields; otherwise returns `true`.
*
* @note Called by every constructor and by `setImmutable()`.
*/
bool
setup();
/** @brief Deserialize a SHAMapItem containing a single STTx.
/** Deserialize a SHAMapItem containing a single `STTx`.
*
* @param item The SHAMapItem to deserialize.
* @return A shared pointer to the deserialized transaction.
* @throw May throw on deserialization error.
* Used by `TxsIterImpl` for open ledgers (no metadata).
*
* @param item The SHAMap leaf to deserialize.
* @return Shared pointer to the deserialized transaction.
* @throw May throw on deserialization error.
*/
static std::shared_ptr<STTx const>
deserializeTx(SHAMapItem const& item);
/** @brief Deserialize a SHAMapItem containing STTx + STObject metadata.
/** Deserialize a SHAMapItem containing an `STTx` followed by `STObject` metadata.
*
* The SHAMapItem must contain two variable length serialization objects.
* The item must encode two back-to-back variable-length fields: the
* serialized transaction blob first, then the metadata blob.
*
* @param item The SHAMapItem to deserialize.
* @return A pair containing shared pointers to the deserialized transaction
* and metadata.
* @throw May throw on deserialization error.
* @param item The SHAMap leaf to deserialize.
* @return Pair of shared pointers to the transaction and its metadata.
* @throw May throw on deserialization error.
*/
static std::pair<std::shared_ptr<STTx const>, std::shared_ptr<STObject const>>
deserializeTxPlusMeta(SHAMapItem const& item);
/** `true` after `setImmutable()` has been called; mutations are forbidden. */
bool immutable_;
// A SHAMap containing the transactions associated with this ledger.
/** Merkleradix tree of transactions + metadata keyed by transaction ID.
*
* Declared `mutable` so `setFull()` and iterator accessors can be
* called in `const` contexts without violating logical immutability.
*/
SHAMap mutable txMap_;
// A SHAMap containing the state objects for this ledger.
/** Merkleradix tree of all ledger state entries (SLEs) keyed by their
* 256-bit key.
*
* Declared `mutable` for the same reason as `txMap_`.
*/
SHAMap mutable stateMap_;
// Protects fee variables
/** Guards `fees_` during the narrow mutable window before `setImmutable()`
* completes; not held on the read path once the ledger is immutable.
*/
std::mutex mutable mutex_;
Fees fees_;
Rules rules_;
LedgerHeader header_;
beast::Journal j_;
Fees fees_; /**< Fee schedule parsed from the on-ledger fee SLE. */
Rules rules_; /**< Protocol rules derived from enabled amendments. */
LedgerHeader header_; /**< Sequence, hashes, close time, coin supply, etc. */
beast::Journal j_; /**< Journal for constructor and `setup()` diagnostics. */
};
/** A ledger wrapped in a CachedView. */
/** Standard shareable ledger type used at rest in most of the server.
*
* `CachedView<Ledger>` layers an `unordered_map` in front of the raw
* `Ledger`, caching deserialized SLEs by key so that frequently accessed
* state entries are not repeatedly deserialized from the SHAMap. This is
* the type that callers such as the transaction engine and RPC handlers
* typically hold, not a raw `Ledger`.
*
* @see CachedView
*/
using CachedLedger = CachedView<Ledger>;
} // namespace xrpl

View File

@@ -1,3 +1,15 @@
/** @file
* Ledger close-time resolution binning and monotonicity enforcement.
*
* Provides compile-time constants and three header-only template functions
* that translate raw wall-clock observations into canonical, network-agreed
* close timestamps written into every immutable ledger record. The binning
* approach lets validators with imperfectly synchronized clocks converge on
* a single close time without requiring a global time source.
*
* @see getNextLedgerTimeResolution, roundCloseTime, effCloseTime
*/
#pragma once
#include <xrpl/basics/chrono.h>
@@ -7,11 +19,18 @@
namespace xrpl {
/** Possible ledger close time resolutions.
Values should not be duplicated.
@see getNextLedgerTimeResolution
*/
/** Ordered ladder of candidate close-time bin sizes, in seconds.
*
* The six values — 10, 20, 30, 60, 90, 120 seconds — form a strictly
* increasing sequence. `getNextLedgerTimeResolution` traverses this array
* to coarsen (move toward index 5) on disagreement and to refine (move
* toward index 0) on agreement. The array order directly encodes the
* coarser/finer direction; no separate mapping is needed.
*
* Values must be unique and sorted in ascending order.
*
* @see getNextLedgerTimeResolution
*/
std::chrono::seconds constexpr kLEDGER_POSSIBLE_TIME_RESOLUTIONS[] = {
std::chrono::seconds{10},
std::chrono::seconds{20},
@@ -20,41 +39,77 @@ std::chrono::seconds constexpr kLEDGER_POSSIBLE_TIME_RESOLUTIONS[] = {
std::chrono::seconds{90},
std::chrono::seconds{120}};
//! Initial resolution of ledger close time.
/** Default close-time resolution used for all ordinary (non-genesis) ledgers.
*
* Equal to `kLEDGER_POSSIBLE_TIME_RESOLUTIONS[2]` (30 seconds). Every
* consensus round starts from this resolution and adjusts based on prior
* agreement history via `getNextLedgerTimeResolution`.
*/
auto constexpr kLEDGER_DEFAULT_TIME_RESOLUTION = kLEDGER_POSSIBLE_TIME_RESOLUTIONS[2];
//! Close time resolution in genesis ledger
/** Close-time resolution used exclusively for the genesis ledger.
*
* Equal to `kLEDGER_POSSIBLE_TIME_RESOLUTIONS[0]` (10 seconds), the finest
* available bin. There is no prior-ledger disagreement history at genesis,
* so the finest resolution is chosen as the starting point.
*/
auto constexpr kLEDGER_GENESIS_TIME_RESOLUTION = kLEDGER_POSSIBLE_TIME_RESOLUTIONS[0];
//! How often we increase the close time resolution (in numbers of ledgers)
/** Number of ledgers between successive close-time resolution refinements.
*
* When the prior ledger reached close-time consensus, the resolution moves
* one step finer only every 8th ledger. This conservative cadence avoids
* prematurely tightening the bin size after a brief period of agreement,
* which could immediately reintroduce disagreements on slightly skewed clocks.
*
* @see getNextLedgerTimeResolution, kDECREASE_LEDGER_TIME_RESOLUTION_EVERY
*/
auto constexpr kINCREASE_LEDGER_TIME_RESOLUTION_EVERY = 8;
//! How often we decrease the close time resolution (in numbers of ledgers)
/** Number of ledgers between successive close-time resolution coarsenings.
*
* When the prior ledger failed to reach close-time consensus, the resolution
* moves one step coarser on every ledger (value = 1). This aggressive
* back-off quickly finds a bin size that absorbs the validators' clock skew,
* deliberately asymmetric with the slower refinement cadence.
*
* @see getNextLedgerTimeResolution, kINCREASE_LEDGER_TIME_RESOLUTION_EVERY
*/
auto constexpr kDECREASE_LEDGER_TIME_RESOLUTION_EVERY = 1;
/** Calculates the close time resolution for the specified ledger.
The XRPL protocol uses binning to represent time intervals using only one
timestamp. This allows servers to derive a common time for the next ledger,
without the need for perfectly synchronized clocks.
The time resolution (i.e. the size of the intervals) is adjusted dynamically
based on what happened in the last ledger, to try to avoid disagreements.
@param previousResolution the resolution used for the prior ledger
@param previousAgree whether consensus agreed on the close time of the prior
ledger
@param ledgerSeq the sequence number of the new ledger
@pre previousResolution must be a valid bin
from @ref kLEDGER_POSSIBLE_TIME_RESOLUTIONS
@tparam Rep Type representing number of ticks in std::chrono::duration
@tparam Period An std::ratio representing tick period in
std::chrono::duration
@tparam Seq Unsigned integer-like type corresponding to the ledger sequence
number. It should be comparable to 0 and support modular
division. Built-in and tagged_integers are supported.
*/
/** Compute the close-time resolution to use for the next ledger.
*
* Implements the adaptive binning policy: if the prior ledger failed to
* reach close-time consensus the bin size is coarsened (every ledger,
* per `kDECREASE_LEDGER_TIME_RESOLUTION_EVERY`); if it succeeded the bin
* size is refined (every 8th ledger, per
* `kINCREASE_LEDGER_TIME_RESOLUTION_EVERY`). Both adjustments saturate at
* the boundaries of `kLEDGER_POSSIBLE_TIME_RESOLUTIONS` rather than
* wrapping. The two rules are mutually exclusive — only one fires per call.
*
* Called by the consensus engine at the start of every round to set
* `closeResolution_`, which is then used for the full round's close-time
* voting and embedded in the accepted ledger.
*
* @param previousResolution The close-time resolution used for the prior
* ledger; must be one of the values in
* `kLEDGER_POSSIBLE_TIME_RESOLUTIONS`.
* @param previousAgree Whether the network agreed on the prior ledger's
* close time (true = finer bins are safe to try).
* @param ledgerSeq Sequence number of the ledger being built; must be
* non-zero. Used for the modulo-based rate-limiting of each direction.
* @return The resolution to apply for the new ledger, chosen from
* `kLEDGER_POSSIBLE_TIME_RESOLUTIONS`.
*
* @pre `previousResolution` is an element of `kLEDGER_POSSIBLE_TIME_RESOLUTIONS`.
* @pre `ledgerSeq != Seq{0}`.
*
* @tparam Rep Tick-count type of the `std::chrono::duration`.
* @tparam Period `std::ratio` tick period of the `std::chrono::duration`.
* @tparam Seq Unsigned integer-like type for the ledger sequence number;
* supports `operator%` and comparison with `Seq{0}`. Both built-in
* integers and XRPL `tagged_integer` wrappers are accepted.
*/
template <class Rep, class Period, class Seq>
std::chrono::duration<Rep, Period>
getNextLedgerTimeResolution(
@@ -65,7 +120,6 @@ getNextLedgerTimeResolution(
XRPL_ASSERT(ledgerSeq != Seq{0}, "xrpl::getNextLedgerTimeResolution : valid ledger sequence");
using namespace std::chrono;
// Find the current resolution:
auto iter = std::find(
std::begin(kLEDGER_POSSIBLE_TIME_RESOLUTIONS),
std::end(kLEDGER_POSSIBLE_TIME_RESOLUTIONS),
@@ -78,16 +132,12 @@ getNextLedgerTimeResolution(
if (iter == std::end(kLEDGER_POSSIBLE_TIME_RESOLUTIONS))
return previousResolution;
// If we did not previously agree, we try to decrease the resolution to
// improve the chance that we will agree now.
if (!previousAgree && (ledgerSeq % Seq{kDECREASE_LEDGER_TIME_RESOLUTION_EVERY} == Seq{0}))
{
if (++iter != std::end(kLEDGER_POSSIBLE_TIME_RESOLUTIONS))
return *iter;
}
// If we previously agreed, we try to increase the resolution to determine
// if we can continue to agree.
if (previousAgree && (ledgerSeq % Seq{kINCREASE_LEDGER_TIME_RESOLUTION_EVERY} == Seq{0}))
{
if (iter-- != std::begin(kLEDGER_POSSIBLE_TIME_RESOLUTIONS))
@@ -97,13 +147,26 @@ getNextLedgerTimeResolution(
return previousResolution;
}
/** Calculates the close time for a ledger, given a close time resolution.
@param closeTime The time to be rounded
@param closeResolution The resolution
@return @b closeTime rounded to the nearest multiple of @b closeResolution.
Rounds up if @b closeTime is midway between multiples of @b closeResolution.
*/
/** Round a ledger close time to the nearest bin boundary.
*
* Bins are aligned to multiples of `closeResolution` measured from the
* clock epoch (`time_since_epoch()`), so any two validators computing this
* on the same raw time will produce the same result regardless of local
* state — a correctness prerequisite for network agreement. Ties (a time
* exactly at the midpoint between two boundaries) round up to the later bin.
*
* A default-constructed `time_point{}` (the epoch sentinel signalling no
* agreed close time) is returned unchanged without any rounding.
*
* @param closeTime The raw close-time observation to round.
* @param closeResolution The bin size; must be positive and non-zero.
* @return `closeTime` rounded to the nearest epoch-anchored multiple of
* `closeResolution`, or `closeTime` unmodified if it equals
* `time_point{}`.
*
* @note Called by `effCloseTime` and also directly by the consensus engine
* via `asCloseTime()` to canonicalize individual peer proposals.
*/
template <class Clock, class Duration, class Rep, class Period>
std::chrono::time_point<Clock, Duration>
roundCloseTime(
@@ -118,15 +181,30 @@ roundCloseTime(
return closeTime - (closeTime.time_since_epoch() % closeResolution);
}
/** Calculate the effective ledger close time
After adjusting the ledger close time based on the current resolution, also
ensure it is sufficiently separated from the prior close time.
@param closeTime The raw ledger close time
@param resolution The current close time resolution
@param priorCloseTime The close time of the prior ledger
*/
/** Compute the effective close time for a ledger, enforcing monotonicity.
*
* Rounds `closeTime` via `roundCloseTime`, then clamps the result to be
* strictly greater than `priorCloseTime`. The clamp (`priorCloseTime + 1s`)
* handles the edge case where a very fast close would otherwise produce a
* rounded time equal to or earlier than the prior ledger's close time,
* violating the invariant that ledger timestamps increase strictly along the
* chain. When the rounded value is already later than `priorCloseTime`, it
* passes through unchanged.
*
* A default-constructed `closeTime` (the epoch sentinel for "no agreed close
* time") is returned unchanged without rounding or clamping.
*
* @param closeTime The raw close-time observation for this ledger.
* @param resolution The bin size for this round's close-time voting.
* @param priorCloseTime The effective close time of the preceding ledger;
* used as the strict lower bound.
* @return `max(roundCloseTime(closeTime, resolution), priorCloseTime + 1s)`,
* or `closeTime` unmodified if it equals `time_point{}`.
*
* @note Example edge cases (30 s bins, priorCloseTime = 0 s):
* - `effCloseTime(10s, 30s, 0s)` → `1s` (rounded = 0s, clamped to 1s)
* - `effCloseTime(16s, 30s, 0s)` → `30s` (rounded = 30s, passes through)
*/
template <class Clock, class Duration, class Rep, class Period>
std::chrono::time_point<Clock, Duration>
effCloseTime(

View File

@@ -14,21 +14,29 @@
namespace xrpl {
/** Open ledger construction tag.
Views constructed with this tag will have the
rules of open ledgers applied during transaction
processing.
/** Tag type for constructing an open-ledger view.
*
* Pass `kOPEN_LEDGER` to the `OpenView` constructor to build a fresh open
* ledger on top of a base. The header sequence is incremented, `parentHash`
* and `parentCloseTime` are derived from the base, and `validated`/`accepted`
* flags are cleared. Rules are supplied explicitly by the caller.
*
* @see kOPEN_LEDGER
*/
inline constexpr struct OpenLedgerT
{
explicit constexpr OpenLedgerT() = default;
} kOPEN_LEDGER{};
/** Batch view construction tag.
Views constructed with this tag are part of a stack of views
used during batch transaction applied.
/** Tag type for constructing a batch-mode view.
*
* Pass `kBATCH_VIEW` to the `OpenView` constructor when building a child view
* during batch transaction processing. The child wraps an existing `OpenView`
* and captures its current transaction count as `baseTxCount_`, so that
* `txCount()` ordinals remain globally unique and monotonically increasing
* within the enclosing ledger regardless of how many sub-views are stacked.
*
* @see kBATCH_VIEW
*/
inline constexpr struct BatchViewT
{
@@ -37,10 +45,31 @@ inline constexpr struct BatchViewT
//------------------------------------------------------------------------------
/** Writable ledger view that accumulates state and tx changes.
@note Presented as ReadView to clients.
*/
/** Mutable ledger view used during transaction processing.
*
* Implements the delta-accumulation pattern: holds an immutable base
* `ReadView` (typically the most recent closed ledger) and records all SLE
* mutations and inserted transactions as a pending diff on top of it.
* Nothing is written through to the base until `apply()` is called, making
* it safe to discard changes on failure.
*
* State-object mutations are buffered in `items_` (`RawStateTable`). All
* `ReadView` queries merge the base and the pending diff transparently, so
* the apparent ledger state is always consistent. Transaction records are
* kept in `txs_` (a PMR `std::map`); open ledgers omit metadata while
* closed representations include it.
*
* Both maps are backed by a 256 KB `monotonic_buffer_resource` for O(1)
* amortised allocation with no per-element heap overhead. The resource is
* a `unique_ptr` so move-construction maintains stable addressing for the
* maps' `polymorphic_allocator` raw pointers.
*
* @note Move assignment and copy assignment are deleted; only move
* construction and copy construction are available.
* @note Callers holding `ReadView const*` see a coherent read-only snapshot
* that merges base state and pending modifications without needing to
* know whether the ledger is settled.
*/
class OpenView final : public ReadView, public TxsRawView
{
private:
@@ -98,145 +127,249 @@ public:
OpenView(OpenView&&) = default;
/** Construct a shallow copy.
Effects:
Creates a new object with a copy of
the modification state table.
The objects managed by shared pointers are
not duplicated but shared between instances.
Since the SLEs are immutable, calls on the
RawView interface cannot break invariants.
*/
/** Construct a copy of this view with a fresh PMR arena.
*
* The modification state table (`items_`) and transaction map (`txs_`)
* are copied into a newly allocated 256 KB monotonic buffer. `shared_ptr`
* members (SLEs, `hold_`) are shared with the source — they are not
* deep-copied — which is safe because SLEs are immutable once published.
*/
OpenView(OpenView const&);
/** Construct an open ledger view.
Effects:
The sequence number is set to the
sequence number of parent plus one.
The parentCloseTime is set to the
closeTime of parent.
If `hold` is not nullptr, retains
ownership of a copy of `hold` until
the MetaView is destroyed.
Calls to rules() will return the
rules provided on construction.
The tx list starts empty and will contain
all newly inserted tx.
*/
/** Construct a fresh open ledger view on top of a closed base.
*
* The header is derived from `base`: sequence is incremented by one,
* `parentCloseTime` is set to the base close time, `parentHash` is set
* to the base hash, and `validated`/`accepted` flags are cleared.
* The transaction list starts empty.
*
* @param base The most recent closed ledger; must outlive this view
* unless `hold` is provided.
* @param rules Rules governing this open ledger; may differ from what
* the base recorded.
* @param hold Optional shared pointer keeping `base`'s backing object
* alive for the lifetime of this view.
*/
OpenView(
OpenLedgerT,
ReadView const* base,
Rules rules,
std::shared_ptr<void const> hold = nullptr);
/** Convenience overload that keeps the base alive via shared ownership.
*
* Equivalent to the three-argument `OpenLedgerT` constructor, but takes
* a `shared_ptr` so the caller need not manage lifetime separately.
*
* @param rules Rules governing this open ledger.
* @param base Shared pointer to the closed base ledger.
*/
OpenView(OpenLedgerT, Rules const& rules, std::shared_ptr<ReadView const> const& base)
: OpenView(kOPEN_LEDGER, &*base, rules, base)
{
}
/** Construct a batch child view on top of an existing open ledger.
*
* Wraps `base` as a read-through fallback and snapshots its current
* `txCount()` into `baseTxCount_`. This ensures that `txCount()` on this
* child continues from where the parent left off, preserving monotonically
* increasing apply-ordinals in transaction metadata.
*
* @param base The parent `OpenView` to wrap; must outlive this child.
*/
OpenView(BatchViewT, OpenView& base) : OpenView(std::addressof(base))
{
baseTxCount_ = base.txCount();
}
/** Construct a new last closed ledger.
Effects:
The LedgerHeader is copied from the base.
The rules are inherited from the base.
The tx list starts empty and will contain
all newly inserted tx.
*/
/** Construct a view representing a last-closed ledger.
*
* Copies the `LedgerHeader` and `Rules` directly from `base`, and
* inherits its `open_` flag — so if the base was a closed ledger, this
* view will also report itself as closed. The transaction list starts
* empty.
*
* @param base The source ledger; must outlive this view unless `hold`
* is provided.
* @param hold Optional shared pointer keeping `base`'s backing object
* alive for the lifetime of this view.
*/
OpenView(ReadView const* base, std::shared_ptr<void const> hold = nullptr);
/** Returns true if this reflects an open ledger. */
/** Returns true if this view represents an open (not yet closed) ledger. */
bool
open() const override
{
return open_;
}
/** Return the number of tx inserted since creation.
This is used to set the "apply ordinal"
when calculating transaction metadata.
*/
/** Return the total number of transactions applied since ledger construction.
*
* Computed as `baseTxCount_ + txs_.size()`. In batch mode `baseTxCount_`
* captures the parent view's count at the time this child was constructed,
* so ordinals are globally unique and monotonically increasing even when
* child views are committed incrementally.
*
* @return Number of transactions, used as the apply ordinal in metadata.
*/
std::size_t
txCount() const;
/** Apply changes. */
/** Commit all accumulated changes to the target view.
*
* Replays every buffered SLE mutation (`items_`) into `to` via
* `RawStateTable::apply`, then iterates `txs_` and calls
* `to.rawTxInsert()` for each transaction. The typical call site is
* `ApplyViewImpl::apply()`, which applies a per-transaction sandbox into
* the enclosing `OpenView`; later the `OpenView` itself is applied into
* the final ledger object.
*
* @param to The target view that receives all mutations and transactions.
*/
void
apply(TxsRawView& to) const;
// ReadView
/** @return The current ledger header (sequence, hashes, close times). */
LedgerHeader const&
header() const override;
/** @return The fee schedule inherited from the base ledger. */
Fees const&
fees() const override;
/** @return The amendment rules supplied at construction or inherited from base. */
Rules const&
rules() const override;
/** Check whether a ledger entry exists, merging base state and pending diff.
*
* @param k Keylet identifying the entry.
* @return `true` if the entry exists in the merged view.
*/
bool
exists(Keylet const& k) const override;
/** Return the smallest key strictly greater than `key` in the merged view.
*
* @param key The lower bound (exclusive) to search from.
* @param last Optional upper bound (inclusive); search is bounded to
* `[key+1, last]` when provided.
* @return The next key, or `std::nullopt` if none exists in range.
*/
std::optional<key_type>
succ(key_type const& key, std::optional<key_type> const& last = std::nullopt) const override;
/** Read a ledger entry from the merged view (base + pending diff).
*
* @param k Keylet identifying the entry.
* @return Shared pointer to the immutable SLE, or `nullptr` if absent.
*/
std::shared_ptr<SLE const>
read(Keylet const& k) const override;
/** @return Iterator to the first SLE in the merged state map. */
std::unique_ptr<SlesType::iter_base>
slesBegin() const override;
/** @return Past-the-end iterator for the merged state map. */
std::unique_ptr<SlesType::iter_base>
slesEnd() const override;
/** @return Iterator to the first SLE whose key is > `key` in the merged map.
*
* @param key The exclusive lower bound.
*/
std::unique_ptr<SlesType::iter_base>
slesUpperBound(uint256 const& key) const override;
/** @return Iterator to the first transaction in this view's tx map.
*
* @note For open ledgers the iterator will not deserialize metadata;
* for closed-ledger views it will.
*/
std::unique_ptr<TxsType::iter_base>
txsBegin() const override;
/** @return Past-the-end iterator for this view's tx map. */
std::unique_ptr<TxsType::iter_base>
txsEnd() const override;
/** Check whether a transaction is present in this view's tx map.
*
* @param key The transaction ID.
* @return `true` if the transaction was inserted into this view.
*/
bool
txExists(key_type const& key) const override;
/** Read a transaction from this view, falling back to the base.
*
* @param key The transaction ID.
* @return Pair of `(STTx, optional metadata STObject)`; both pointers are
* null if the transaction is not found in this view or the base.
*/
tx_type
txRead(key_type const& key) const override;
// RawView
/** Buffer a deletion of an existing state item.
*
* Delegates to `RawStateTable::erase`. The entry will be removed from
* the merged view immediately and will not appear in subsequent reads.
*
* @param sle The SLE to erase; its key is extracted from the object.
*/
void
rawErase(std::shared_ptr<SLE> const& sle) override;
/** Buffer an insertion of a new state item.
*
* Delegates to `RawStateTable::insert`. The key must not already exist
* in the merged view.
*
* @param sle The new SLE to insert; its key is extracted from the object.
*/
void
rawInsert(std::shared_ptr<SLE> const& sle) override;
/** Buffer a replacement of an existing state item.
*
* Delegates to `RawStateTable::replace`. The key must already exist in
* the merged view.
*
* @param sle The replacement SLE; its key is extracted from the object.
*/
void
rawReplace(std::shared_ptr<SLE> const& sle) override;
/** Record destruction of XRP (burned as transaction fees).
*
* Delegates to `RawStateTable::destroyXRP`. The destroyed amount
* accumulates in the state table and is flushed to the target on `apply()`.
*
* @param fee The amount of XRP to destroy.
*/
void
rawDestroyXRP(XRPAmount const& fee) override;
// TxsRawView
/** Record a transaction in this view's transaction map.
*
* For open ledgers `metaData` is typically `nullptr`; for closed-ledger
* representations it carries the serialized `TxMeta`.
*
* @param key The transaction ID (must be unique within this view).
* @param txn Serialized transaction blob.
* @param metaData Serialized transaction metadata, or `nullptr` for open
* ledger entries.
* @throws std::logic_error if `key` is already present in this view's
* tx map. Duplicate transaction IDs are a hard invariant violation.
*/
void
rawTxInsert(
key_type const& key,

View File

@@ -14,75 +14,122 @@
namespace xrpl {
/** Tracks order books in the ledger.
This interface provides access to order book information, including:
- Which order books exist in the ledger
- Querying order books by issue
- Managing order book subscriptions
The order book database is updated as ledgers are accepted and provides
efficient lookup of order book information for pathfinding and client
subscriptions.
*/
/** Pure abstract index of all active order books across the ledger.
*
* An order book is a directed trading pair — a set of open `ltOFFER` entries
* sharing the same "taker pays" (`in`) and "taker gets" (`out`) assets.
* Because pathfinding and client subscriptions both need fast lookups of
* which markets exist, this index is maintained separately from ledger state.
*
* The interface lives in the public ledger layer; the concrete implementation
* (`OrderBookDBImpl`) is instantiated via `makeOrderBookDb()` and injected
* through the service registry, keeping heavy implementation details out of
* consumer headers.
*
* @note All internal maps are guarded by a `std::recursive_mutex`. The
* expensive full-ledger scan in `setup()` builds new maps outside the
* lock and swaps them in a brief critical section, so reader calls are
* only briefly blocked rather than held for the duration of a full ledger
* traversal.
*/
class OrderBookDB
{
public:
virtual ~OrderBookDB() = default;
/** Initialize or update the order book database with a new ledger.
This method should be called when a new ledger is accepted to update
the order book database with the current state of all order books.
@param ledger The ledger to scan for order books
*/
/** Notify the database that a new ledger has been accepted.
*
* Triggers a throttled full-ledger scan when needed. The scan is skipped
* if the new ledger is within 25,600 sequences ahead of the last scanned
* ledger (incremental updates from `processTxn` keep the index current)
* or within 16 sequences behind it (small reorg). Outside these windows
* a full scan is scheduled — synchronously in standalone mode, or as a
* background job queue task otherwise. The scan rebuilds the book maps in
* local variables then swaps them under a lock to minimise reader
* contention.
*
* @param ledger The accepted ledger to evaluate; the scan reads every
* `ltDIR_NODE` with an `sfExchangeRate` field and every `ltAMM`
* object to rebuild the in-memory book maps.
*/
virtual void
setup(std::shared_ptr<ReadView const> const& ledger) = 0;
/** Add an order book to track.
@param book The order book to add
*/
/** Register a single order book without triggering a full ledger scan.
*
* Used to record a newly discovered book incrementally — for example,
* when a new offer type is seen in `processTxn` before the next scheduled
* full `setup()` scan.
*
* @param book The directed trading pair to register.
*/
virtual void
addOrderBook(Book const& book) = 0;
/** Get all order books that want a specific issue.
Returns a list of all order books where the taker pays the specified
issue. This is useful for pathfinding to find all possible next hops
from a given currency.
@param asset The asset to search for
@param domain Optional domain restriction for the order book
@return Vector of books that want this issue
*/
/** Return all order books whose "taker pays" side is @p asset.
*
* The primary pathfinding query: given an asset a sender currently holds,
* enumerate every market where that asset can be spent. The pathfinding
* engine calls this at each hop to discover possible next steps toward
* the destination currency.
*
* @param asset The asset the taker pays (the "in" side of the book).
* @param domain If provided, restricts results to books scoped to that
* permissioned domain; if absent, returns only global books.
* @return All `Book` objects with @p asset as their `in` side.
*/
virtual std::vector<Book>
getBooksByTakerPays(Asset const& asset, std::optional<Domain> const& domain = std::nullopt) = 0;
/** Get the count of order books that want a specific issue.
@param asset The asset to search for
@param domain Optional domain restriction for the order book
@return Number of books that want this issue
*/
/** Return the number of distinct "taker gets" assets available for @p asset.
*
* Used as a breadth-limiting heuristic by the pathfinding engine: a large
* count signals a liquid hub currency; a small count may not warrant
* deeper exploration.
*
* @param asset The asset the taker pays.
* @param domain If provided, counts only books in that permissioned domain;
* if absent, counts only global books.
* @return The number of order books whose "in" side matches @p asset.
*/
virtual int
getBookSize(Asset const& asset, std::optional<Domain> const& domain = std::nullopt) = 0;
/** Check if an order book to XRP exists for the given issue.
@param asset The asset to check
@param domain Optional domain restriction for the order book
@return true if a book from this issue to XRP exists
*/
/** Return whether any order book exists that sells @p asset for XRP.
*
* The implementation maintains a dedicated O(1) set (`xrpBooks_` /
* `xrpDomainBooks_`) so this check does not scan `allBooks_`. Pathfinding
* uses it to identify assets that can be liquidated directly to XRP
* without an intermediate hop.
*
* @param asset The asset the taker pays.
* @param domain If provided, checks the permissioned-domain book set;
* if absent, checks the global book set.
* @return `true` if a book with @p asset as "in" and XRP as "out" exists.
*/
virtual bool
isBookToXRP(Asset const& asset, std::optional<Domain> const& domain = std::nullopt) = 0;
/**
* Process a transaction for order book tracking.
* @param ledger The ledger the transaction was applied to
* @param alTx The transaction to process
* @param jvObj The JSON object of the transaction
/** Fan out a closed-ledger transaction to all relevant book subscribers.
*
* Walks the transaction's metadata nodes looking for `ltOFFER` entries
* that were created, modified, or deleted and extracts their `TakerGets`
* and `TakerPays` fields. For each affected offer, the reversed book
* (`TakerGets` → `TakerPays`) is looked up in the listeners map and, if
* subscribers exist, `BookListeners::publish()` is called.
*
* Deduplication is handled via a `hash_set<uint64_t> havePublished` local
* to each call: a subscriber whose sequence number is already in the set
* will not receive a second copy of the same transaction, even if multiple
* of its subscribed books were touched.
*
* @note Only called for transactions with result `tesSUCCESS`.
*
* @param ledger The closed ledger the transaction was applied to.
* @param alTx The fully materialised transaction-in-ledger projection,
* including metadata.
* @param jvObj Version-indexed JSON representation of the transaction,
* built once upstream and dispatched to subscribers by API version.
*/
virtual void
processTxn(
@@ -90,18 +137,30 @@ public:
AcceptedLedgerTx const& alTx,
MultiApiJson const& jvObj) = 0;
/**
* Get the book listeners for a book.
* @param book The book to get the listeners for
* @return The book listeners for the book
/** Return the listener set for @p book, or `nullptr` if none exists.
*
* Used when unsubscribing: a `nullptr` result means no entry needs to be
* updated. Avoids creating empty `BookListeners` objects for every book
* that passes through the system.
*
* @param book The directed trading pair to look up.
* @return Shared pointer to the existing `BookListeners` for @p book, or
* `nullptr` if no subscribers are registered.
*/
virtual BookListeners::pointer
getBookListeners(Book const&) = 0;
/**
* Create a new book listeners for a book.
* @param book The book to create the listeners for
* @return The new book listeners for the book
/** Return the listener set for @p book, creating it on demand.
*
* Used when subscribing: if no `BookListeners` entry exists for the book,
* one is created and inserted into the map before returning.
*
* @note Internally calls `getBookListeners()` under the same lock,
* which is why the implementation uses a `std::recursive_mutex`.
*
* @param book The directed trading pair to look up or create.
* @return Shared pointer to the (possibly newly created) `BookListeners`
* for @p book; never `nullptr`.
*/
virtual BookListeners::pointer
makeBookListeners(Book const&) = 0;

View File

@@ -14,10 +14,35 @@ namespace detail {
// VFALCO TODO Inline this implementation
// into the PaymentSandbox class itself
/** Bookkeeping ledger for credits deferred during payment execution.
*
* Tracks every credit applied through a `PaymentSandbox` so that
* balance queries can subtract those credits before reporting available
* funds. This prevents circular-path liquidity: a credit arriving at an
* intermediate account mid-payment cannot be re-spent by an earlier step
* in the same path.
*
* Two separate tables are maintained: `creditsIOU_` for IOU trust-line
* transfers (keyed by canonical `(lowAccount, highAccount, currency)`) and
* `creditsMPT_` for MPT issuances (keyed by `MPTID`). Owner-count
* maximums are stored in `ownerCounts_`.
*
* @note This class is an implementation detail of `PaymentSandbox` and is
* not intended for direct use by other components.
*/
class DeferredCredits
{
private:
using KeyIOU = std::tuple<AccountID, AccountID, Currency>;
/** Per-trust-line record of accumulated debits and the pre-credit balance.
*
* Debits are split by canonical endpoint: `lowAcctDebits` accumulates
* amounts sent by the account whose `AccountID` is lower; `highAcctDebits`
* accumulates amounts sent by the other endpoint. `lowAcctOrigBalance`
* holds the low-account's balance at the moment the first credit was
* recorded; it is never overwritten by subsequent credits.
*/
struct ValueIOU
{
explicit ValueIOU() = default;
@@ -26,41 +51,52 @@ private:
STAmount lowAcctOrigBalance;
};
/** Per-holder MPT debit record.
*
* `debit` accumulates the total MPT amount sent by this holder during
* the payment. `origBalance` is the holder's balance at the time the
* first debit was recorded; it is never overwritten by subsequent debits.
*/
struct HolderValueMPT
{
HolderValueMPT() = default;
// Debit to issuer
std::uint64_t debit = 0;
std::uint64_t origBalance = 0;
};
/** Per-issuance MPT record aggregating credits and self-debits.
*
* `holders` tracks per-holder debit entries. `credit` accumulates the
* total amount issued (i.e. credited to holders) during the payment.
* `origBalance` holds the issuer's `OutstandingAmount` at the time the
* first entry was recorded; it is never overwritten.
*
* `selfDebit` handles the case where the MPT issuer owns a sell offer.
* Because the payment engine runs in reverse, crediting a holder first
* can transiently push `OutstandingAmount` above `MaximumAmount`. When
* the issuer's own sell offer is consumed in a later (reversed) step,
* the available issuance capacity must be reduced by the offer amount.
* `selfDebit` accumulates those offer amounts so that
* `balanceHookSelfIssueMPT` can correctly cap available issuance.
*/
struct IssuerValueMPT
{
IssuerValueMPT() = default;
std::map<AccountID, HolderValueMPT> holders;
// Credit to holder
std::uint64_t credit = 0;
// OutstandingAmount might overflow when MPTs are credited to a holder.
// Consider A1 paying 100MPT to A2 and A1 already having maximum MPTs.
// Since the payment engine executes a payment in revers, A2 is
// credited first and OutstandingAmount is going to be equal
// to MaximumAmount + 100MPT. In the next step A1 redeems 100MPT
// to the issuer and OutstandingAmount balances out.
std::int64_t origBalance = 0;
// Self debit on offer selling MPT. Since the payment engine executes
// a payment in reverse, a crediting/buying step may overflow
// OutstandingAmount. A sell MPT offer owned by a holder can redeem any
// amount up to the offer's amount and holder's available funds,
// balancing out OutstandingAmount. But if the offer's owner is issuer
// then it issues more MPT. In this case the available amount to issue
// is the initial issuer's available amount less all offer sell amounts
// by the issuer. This is self-debit, where the offer's owner,
// issuer in this case, debits to self.
std::uint64_t selfDebit = 0;
};
using AdjustmentMPT = IssuerValueMPT;
public:
/** Query result for a single IOU trust-line adjustment.
*
* Oriented from the perspective of the `main` account passed to
* `adjustmentsIOU()`: `debits` is what `main` has sent, `credits` is
* what `main` has received, and `origBalance` is `main`'s balance
* before the first credit in this sandbox was recorded.
*/
struct AdjustmentIOU
{
AdjustmentIOU(STAmount d, STAmount c, STAmount b)
@@ -72,14 +108,44 @@ public:
STAmount origBalance;
};
// Get the adjustments for the balance between main and other.
// Returns the debits, credits and the original balance
/** Return the accumulated debit/credit adjustments for an IOU trust line.
*
* The result is oriented from `main`'s perspective: `debits` contains
* what `main` has sent to `other`, `credits` contains what `other` has
* sent to `main`, and `origBalance` is `main`'s balance at the time the
* first credit for this pair was recorded.
*
* @param main The account whose perspective determines orientation.
* @param other The counterparty account.
* @param currency The currency of the trust line.
* @return Adjustment record, or `std::nullopt` if no credits have been
* recorded for this pair in this sandbox.
*/
[[nodiscard]] std::optional<AdjustmentIOU>
adjustmentsIOU(AccountID const& main, AccountID const& other, Currency const& currency) const;
/** Return the accumulated MPT adjustments for a given issuance.
*
* @param mptID The unique identifier of the MPT issuance.
* @return Adjustment record, or `std::nullopt` if no credits have been
* recorded for this issuance in this sandbox.
*/
[[nodiscard]] std::optional<AdjustmentMPT>
adjustmentsMPT(MPTID const& mptID) const;
/** Record an IOU credit from `sender` to `receiver`.
*
* On the first call for a given `(sender, receiver, currency)` triple the
* pre-credit sender balance is saved as the original balance. Subsequent
* calls for the same triple accumulate debits without overwriting the
* original balance.
*
* @param sender Account sending the credit.
* @param receiver Account receiving the credit.
* @param amount Non-negative IOU amount being transferred.
* @param preCreditSenderBalance Sender's balance immediately before
* this credit is applied; only stored on the first call.
*/
void
creditIOU(
AccountID const& sender,
@@ -87,6 +153,20 @@ public:
STAmount const& amount,
STAmount const& preCreditSenderBalance);
/** Record an MPT credit from `sender` to `receiver`.
*
* Distinguishes between issuer-to-holder transfers (which increment the
* aggregate `credit` counter) and holder-to-issuer redemptions (which
* increment the per-holder `debit` counter). The original balances are
* stored only on the first call for each holder/issuance combination.
*
* @param sender Account sending the MPT.
* @param receiver Account receiving the MPT.
* @param amount Non-negative MPT amount being transferred.
* @param preCreditBalanceHolder Holder's MPT balance before this credit.
* @param preCreditBalanceIssuer Issuer's `OutstandingAmount` before this
* credit; only stored on the first call for this issuance.
*/
void
creditMPT(
AccountID const& sender,
@@ -95,22 +175,61 @@ public:
std::uint64_t preCreditBalanceHolder,
std::int64_t preCreditBalanceIssuer);
/** Record an MPT self-debit incurred by the issuer via a sell offer.
*
* When the issuer owns a sell offer and it is consumed, the payment
* engine (running in reverse) may have already credited a holder,
* pushing `OutstandingAmount` transiently above `MaximumAmount`. This
* call registers the offer amount as a self-debit so that
* `balanceHookSelfIssueMPT` can cap available issuance correctly.
*
* @param issue The MPT issuance involved.
* @param amount Amount of the issuer's sell offer that was consumed.
* @param origBalance Issuer's `OutstandingAmount` before this entry; only
* stored on the first call for this issuance.
*/
void
issuerSelfDebitMPT(MPTIssue const& issue, std::uint64_t amount, std::int64_t origBalance);
/** Record an owner-count transition for `account`.
*
* Stores the maximum of `cur` and `next`, and takes the maximum with any
* previously recorded value. Because payments only ever decrease owner
* counts, the highest observed count is the conservative bound that
* prevents a transient low count from bypassing reserve checks mid-payment.
*
* @param id Account whose owner count is changing.
* @param cur Current owner count before the transition.
* @param next Owner count after the transition.
*/
void
ownerCount(AccountID const& id, std::uint32_t cur, std::uint32_t next);
// Get the adjusted owner count. Since DeferredCredits is meant to be used
// in payments, and payments only decrease owner counts, return the max
// remembered owner count.
/** Return the maximum owner count observed for `account` in this sandbox.
*
* Since payments only decrease owner counts, the maximum is the correct
* conservative bound for reserve checks.
*
* @param id Account to query.
* @return The peak owner count, or `std::nullopt` if no transition has
* been recorded for this account.
*/
[[nodiscard]] std::optional<std::uint32_t>
ownerCount(AccountID const& id) const;
/** Merge this sandbox's deferred credits into a parent sandbox.
*
* Debit accumulators and self-debit fields are summed; original balances
* are never overwritten (the parent's earlier record takes precedence).
* Owner-count maximums are taken across both sandboxes.
*
* @param to The parent `DeferredCredits` table to merge into.
*/
void
apply(DeferredCredits& to);
private:
/** Produce a canonical `KeyIOU` by ordering the two accounts. */
static KeyIOU
makeKeyIOU(AccountID const& a1, AccountID const& a2, Currency const& currency);
@@ -123,18 +242,29 @@ private:
//------------------------------------------------------------------------------
/** A wrapper which makes credits unavailable to balances.
This is used for payments and pathfinding, so that consuming
liquidity from a path never causes portions of that path or
other paths to gain liquidity.
The behavior of certain free functions in the ApplyView API
will change via the balanceHook and creditHook overrides
of PaymentSandbox.
@note Presented as ApplyView to clients
*/
/** Speculative ledger view that hides in-flight credits from balance queries.
*
* The XRPL payment engine processes multi-hop paths where value flows through
* chains of trust lines, order books, and AMM pools. Without a guard, a
* credit arriving at an intermediate account mid-path could immediately
* appear as spendable liquidity for a later step in the same path — allowing
* phantom value to be created. `PaymentSandbox` prevents this by intercepting
* every credit via the hook protocol defined in `ApplyView` and recording it
* in a `DeferredCredits` table. Balance queries then subtract those deferred
* credits so freshly-received funds are invisible to outgoing transfer checks
* until the entire transaction commits.
*
* `PaymentSandbox` can be stacked: constructing one on top of another via the
* pointer constructors creates a child sandbox whose deferred credits chain to
* the parent. The pathfinding engine uses this to evaluate each candidate
* strand in a disposable child, committing to the parent only on success.
*
* @note When constructing on top of an existing `PaymentSandbox`, you **must**
* use the explicit pointer constructors. Using the plain `ApplyView*`
* constructor would bypass deferred-credit propagation and break invariants.
*
* @note Presented as `ApplyView` to clients.
*/
class PaymentSandbox final : public detail::ApplyViewBase
{
public:
@@ -147,27 +277,40 @@ public:
PaymentSandbox(PaymentSandbox&&) = default;
/** Construct a root payment sandbox over a read-only base view.
*
* @param base The underlying ledger state to layer mutations on top of.
* @param flags Transaction-processing flags forwarded to `ApplyViewBase`.
*/
PaymentSandbox(ReadView const* base, ApplyFlags flags) : ApplyViewBase(base, flags)
{
}
/** Construct a payment sandbox over an existing `ApplyView`.
*
* Inherits the flags of the base view. Use the explicit pointer
* constructors instead if `base` is itself a `PaymentSandbox`.
*
* @param base The mutable view to build on top of.
*/
PaymentSandbox(ApplyView const* base) : ApplyViewBase(base, base->flags())
{
}
/** Construct on top of existing PaymentSandbox.
The changes are pushed to the parent when
apply() is called.
@param parent A non-null pointer to the parent.
@note A pointer is used to prevent confusion
with copy construction.
*/
// VFALCO If we are constructing on top of a PaymentSandbox,
// or a PaymentSandbox-derived class, we MUST go through
// one of these constructors or invariants will be broken.
/** Construct a child payment sandbox on top of an existing `PaymentSandbox`.
*
* The child's deferred-credit table chains to the parent so that balance
* adjustments aggregate correctly across the sandbox stack. Changes are
* not visible in the parent until `apply(PaymentSandbox&)` is called.
*
* @param parent Non-null pointer to the parent sandbox. A pointer is
* used rather than a reference to prevent confusion with copy
* construction.
*
* @note This overload set **must** be used whenever building on top of
* a `PaymentSandbox` or derived class. The plain `ApplyView*`
* constructor does not propagate deferred credits.
*/
/** @{ */
explicit PaymentSandbox(PaymentSandbox const* base)
: ApplyViewBase(base, base->flags()), ps_(base)
@@ -179,17 +322,67 @@ public:
}
/** @} */
/** Return the IOU balance adjusted for deferred credits.
*
* Walks the sandbox chain (this → parent → … ) and accumulates total
* debits from all ancestor tables. Returns
* `min(amount, origBalance - totalDebits, minObservedBalance)` to
* handle edge cases where rounding in the deferred table could otherwise
* overestimate usable funds. A computed negative XRP result is clamped
* to zero (it is not an error — it arises when a large credit is
* followed by the same debit within the path).
*
* @param account The account whose perspective determines orientation.
* @param issuer The IOU issuer (doubles as the currency issuer).
* @param amount The raw balance as reported by the underlying ledger.
* @return Adjusted balance with deferred credits hidden.
*/
[[nodiscard]] STAmount
balanceHookIOU(AccountID const& account, AccountID const& issuer, STAmount const& amount)
const override;
/** Return the MPT holder or issuer balance adjusted for deferred credits.
*
* Walks the sandbox chain accumulating per-holder debits (if `account`
* is a holder) or the aggregate issuer credit (if `account` is the
* issuer). Returns `min(amount, origBalance - totalAdjustment,
* minObservedBalance)`, clamped to zero.
*
* @param account The account being queried (holder or issuer).
* @param issue The MPT issuance.
* @param amount The raw balance as reported by the underlying ledger.
* @return Adjusted balance with deferred credits hidden.
*/
[[nodiscard]] STAmount
balanceHookMPT(AccountID const& account, MPTIssue const& issue, std::int64_t amount)
const override;
/** Return the issuer's available MPT issuance capacity, net of self-debits.
*
* When the issuer owns sell offers and the payment engine (running in
* reverse) has already consumed some of them, those amounts are recorded
* as self-debits. This hook caps available issuance at
* `origOutstandingAmount - totalSelfDebits`, returning zero if the result
* is non-positive.
*
* @param issue The MPT issuance.
* @param amount The raw `OutstandingAmount` from the underlying ledger.
* @return Available issuance capacity after subtracting self-debits.
*/
[[nodiscard]] STAmount
balanceHookSelfIssueMPT(MPTIssue const& issue, std::int64_t amount) const override;
/** Record an IOU credit in the deferred-credits table.
*
* Called by ledger mutation helpers at every IOU transfer. The recorded
* debit is used by `balanceHookIOU` to hide this credit from future
* balance queries within the same payment path.
*
* @param from Account sending the credit.
* @param to Account receiving the credit.
* @param amount Non-negative IOU amount being transferred.
* @param preCreditBalance Sender's balance immediately before this credit.
*/
void
creditHookIOU(
AccountID const& from,
@@ -197,6 +390,19 @@ public:
STAmount const& amount,
STAmount const& preCreditBalance) override;
/** Record an MPT credit in the deferred-credits table.
*
* Called by ledger mutation helpers at every MPT transfer. The recorded
* debit is used by `balanceHookMPT` to hide this credit from future
* balance queries within the same payment path.
*
* @param from Account sending the MPT.
* @param to Account receiving the MPT.
* @param amount Non-negative MPT amount being transferred.
* @param preCreditBalanceHolder Holder's MPT balance before this credit.
* @param preCreditBalanceIssuer Issuer's `OutstandingAmount` before this
* credit.
*/
void
creditHookMPT(
AccountID const& from,
@@ -205,22 +411,60 @@ public:
std::uint64_t preCreditBalanceHolder,
std::int64_t preCreditBalanceIssuer) override;
/** Record an MPT issuer self-debit arising from a consumed sell offer.
*
* Called when the MPT issuer's own sell offer is consumed during
* payment processing. Accumulates the offer amount in the
* `DeferredCredits` self-debit field so that `balanceHookSelfIssueMPT`
* can correctly limit further issuance capacity.
*
* @param issue The MPT issuance.
* @param amount Amount consumed from the issuer's sell offer.
* @param origBalance Issuer's `OutstandingAmount` before this entry.
*/
void
issuerSelfDebitHookMPT(MPTIssue const& issue, std::uint64_t amount, std::int64_t origBalance)
override;
/** Record an owner-count transition for reserve-check purposes.
*
* Stores the maximum of `cur` and `next` in the deferred-credits table.
* Because payments only decrease owner counts, the peak value is the
* conservative bound that prevents a transient low count from bypassing
* reserve checks mid-payment.
*
* @param account Account whose owner count is changing.
* @param cur Owner count before the transition.
* @param next Owner count after the transition.
*/
void
adjustOwnerCountHook(AccountID const& account, std::uint32_t cur, std::uint32_t next) override;
/** Return the peak owner count observed for `account` in this sandbox chain.
*
* Walks the sandbox chain and returns the maximum recorded count across
* all ancestors, or `count` if no transition has been recorded.
*
* @param account Account to query.
* @param count Baseline count from the underlying ledger.
* @return The peak owner count seen across the sandbox chain.
*/
[[nodiscard]] std::uint32_t
ownerCountHook(AccountID const& account, std::uint32_t count) const override;
/** Apply changes to base view.
`to` must contain contents identical to the parent
view passed upon construction, else undefined
behavior will result.
*/
/** Commit changes to a base view.
*
* The two overloads serve different commit targets:
* - `apply(RawView&)` is the terminal commit: asserts this sandbox has
* no parent (`ps_ == nullptr`) and flushes the state journal to the
* raw ledger. The `RawView` must contain state identical to the view
* passed at construction, otherwise behavior is undefined.
* - `apply(PaymentSandbox&)` asserts that `&to == ps_` (you can only
* apply to your direct parent) and propagates both the state journal
* and the deferred-credits table into the parent sandbox.
*
* @param to The target view to flush changes into.
*/
/** @{ */
void
apply(RawView& to);
@@ -229,6 +473,10 @@ public:
apply(PaymentSandbox& to);
/** @} */
/** Return the amount of XRP destroyed (as fees) during this payment.
*
* Delegates to `items_.dropsDestroyed()`. Distinct from transferred XRP.
*/
[[nodiscard]] XRPAmount
xrpDestroyed() const;

View File

@@ -8,12 +8,49 @@
namespace xrpl {
/** Keeps track of which ledgers haven't been fully saved.
During the ledger building process this collection will keep
track of those ledgers that are being built but have not yet
been completely written.
*/
/** Coordination primitive tracking validated ledgers not yet fully written to
* the SQLite relational database.
*
* When a validated ledger is being persisted, there is a window in which it
* exists in memory but its index entries are incomplete on disk. Any code that
* reports the "validated range" of ledgers to peers or clients must exclude
* these in-progress sequences; otherwise it could direct a requester to query
* a partially-written row.
*
* ## Internal state machine
*
* The internal map encodes three observable states per ledger sequence:
*
* | Map state | Meaning |
* |----------------------------|--------------------------------------------|
* | key absent | Not pending; safe for DB queries |
* | key present, value `false` | Registered/dispatched, write not started |
* | key present, value `true` | A thread is actively writing to SQLite |
*
* The canonical "finished" state is key-absent; `finishWork()` erases the
* entry (rather than resetting the flag) so that `pending()` and the blocking
* loop in `shouldWork()` use absence as the termination condition.
*
* ## Typical call sequence
*
* 1. `pendSaveValidated()` calls `shouldWork(seq, isSynchronous)` to either
* claim a fresh entry or "steal" a registered-but-unstarted one.
* 2. `saveValidatedLedger()` calls `startWork(seq)` to atomically flip the
* flag from `false` → `true`. A `false` return means another thread won
* the race; the caller logs "Save aborted" and exits early.
* 3. `saveValidatedLedger()` calls `finishWork(seq)` after the DB write
* completes, waking any synchronous waiters.
* 4. `LedgerMaster::getValidatedRange()` calls `getSnapshot()` to trim the
* reported min/max validated range, excluding any in-progress sequences.
*
* This class is a pure coordination primitive. It does not own a thread pool
* or `JobQueue`; all scheduling policy lives in `pendSaveValidated()`.
*
* @note Thread-safe. All methods acquire `mutex_` internally. The synchronous
* blocking path in `shouldWork()` re-acquires the lock after each
* `await_.wait()` and re-checks in a loop because `notify_all()` can
* wake multiple waiters simultaneously.
*/
class PendingSaves
{
private:
@@ -22,12 +59,18 @@ private:
std::condition_variable await_;
public:
/** Start working on a ledger
This is called prior to updating the SQLite indexes.
@return 'true' if work should be done
*/
/** Atomically claim the right to begin writing a ledger to the database.
*
* Flips the map entry for @p seq from `false` to `true`, signalling that
* a thread is actively writing to SQLite. This must be called after
* `shouldWork()` returns `true` and before the DB write begins.
*
* @param seq Ledger sequence number to claim.
* @return `true` if this caller successfully claimed the write; `false` if
* the entry is absent (write already completed) or already `true`
* (another thread started it first). A `false` return is the caller's
* signal to abort with a "Save aborted" log and return early.
*/
bool
startWork(LedgerIndex seq)
{
@@ -45,12 +88,14 @@ public:
return true;
}
/** Finish working on a ledger
This is called after updating the SQLite indexes.
The tracking of the work in progress is removed and
threads awaiting completion are notified.
*/
/** Mark a ledger's database write as complete and wake any waiters.
*
* Erases the entry for @p seq from the map — key-absent is the canonical
* "done" state — then calls `notify_all()` so any synchronous caller
* blocked in `shouldWork()` can re-evaluate.
*
* @param seq Ledger sequence number whose write has completed.
*/
void
finishWork(LedgerIndex seq)
{
@@ -60,7 +105,14 @@ public:
await_.notify_all();
}
/** Return `true` if a ledger is in the progress of being saved. */
/** Return `true` if @p seq has a pending or in-progress database write.
*
* A `true` result means the sequence appears in the map (either
* dispatched-but-not-started or actively writing). Callers use this to
* avoid re-dispatching a save that is already in flight.
*
* @param seq Ledger sequence number to test.
*/
bool
pending(LedgerIndex seq)
{
@@ -68,14 +120,34 @@ public:
return map_.contains(seq);
}
/** Check if a ledger should be dispatched
Called to determine whether work should be done or
dispatched. If work is already in progress and the
call is synchronous, wait for work to be completed.
@return 'true' if work should be done or dispatched
*/
/** Determine whether the caller should proceed with (or wait for) a save.
*
* This is the entry point for `pendSaveValidated()`. It implements the
* full dispatch/steal/wait decision:
*
* - **Not present**: Inserts `(seq, false)` and returns `true` — the
* caller owns the work.
* - **Present as `false`** (registered, unstarted):
* - Asynchronous caller: returns `false` (already dispatched; skip).
* - Synchronous caller: returns `true`, stealing the work before any
* thread can claim it via `startWork()`.
* - **Present as `true`** (write in progress):
* - Asynchronous caller: unreachable in practice; the `!isSynchronous`
* branch returns `false` before reaching the wait.
* - Synchronous caller: blocks on `await_` in a `do/while` loop,
* re-checking after each `notify_all()` from `finishWork()`, until
* the entry disappears (write complete).
*
* @param seq Ledger sequence number to check or register.
* @param isSynchronous `true` if the caller requires the write to be
* complete before returning; `false` if dispatch-once is sufficient.
* @return `true` if the caller should perform (or has stolen) the write;
* `false` if the work is already dispatched or complete.
*
* @note The blocking synchronous path re-acquires `mutex_` after each
* wake-up and loops because `notify_all()` may unblock multiple
* waiters; only one will find the entry absent.
*/
bool
shouldWork(LedgerIndex seq, bool isSynchronous)
{
@@ -108,12 +180,20 @@ public:
} while (true);
}
/** Get a snapshot of the pending saves
Each entry in the returned map corresponds to a ledger
that is in progress or dispatched. The boolean indicates
whether work is currently in progress.
*/
/** Return a point-in-time copy of the pending-saves map.
*
* Used by `LedgerMaster::getValidatedRange()` to trim the reported
* min/max validated-ledger range: any sequence present in the snapshot —
* regardless of whether its flag is `false` (dispatched) or `true`
* (writing) — is excluded from the range to avoid directing peers to
* query a partially-written DB row.
*
* The returned map is a value copy taken under `mutex_`; the caller may
* iterate it freely without holding any lock.
*
* @return A snapshot of `map_`, where each key is an in-flight ledger
* sequence and each value is `false` (unstarted) or `true` (active).
*/
std::map<LedgerIndex, bool>
getSnapshot() const
{

View File

@@ -6,10 +6,29 @@
namespace xrpl {
/** Interface for ledger entry changes.
Subclasses allow raw modification of ledger entries.
*/
/** Low-level write surface for committing ledger state mutations.
*
* Defines the three-operation contract (`rawErase`, `rawInsert`,
* `rawReplace`) plus an XRP-burn hook (`rawDestroyXRP`) that together
* represent the minimal interface a backing store must provide to absorb
* flushed changes from a sandbox.
*
* `detail::RawStateTable::apply(RawView&)` is the canonical driver:
* it iterates its buffered erase/insert/replace actions and dispatches
* each through the corresponding method here, so flushing logic is written
* once and any concrete target — a finalising `Ledger`, an `OpenView`, or
* another sandbox — implements the contract without exposing checkout
* semantics.
*
* The "raw" prefix is a semantic contract: these methods perform no
* precondition checking, no journaling, and no ownership tracking.
* They are the trusted commit surface, not the API that transaction
* logic should call directly.
*
* @note The copy constructor is defaulted (subclasses may need to snapshot
* state), but copy assignment is deleted to prevent silent cross-type
* assignment through the base interface.
*/
class RawView
{
public:
@@ -19,66 +38,79 @@ public:
RawView&
operator=(RawView const&) = delete;
/** Delete an existing state item.
The SLE is provided so the implementation
can calculate metadata.
*/
/** Unconditionally remove an existing state entry.
*
* The full SLE (not just its key) is passed so that implementations
* can compute metadata such as changes to owner count or the type of
* the deleted object.
*
* @param sle The ledger entry to remove. The key is derived from
* the SLE itself; the entry must exist in the backing store.
*/
virtual void
rawErase(std::shared_ptr<SLE> const& sle) = 0;
/** Unconditionally insert a state item.
Requirements:
The key must not already exist.
Effects:
The key is associated with the SLE.
@note The key is taken from the SLE
*/
/** Unconditionally insert a new state entry.
*
* The key is read from the SLE rather than passed separately,
* which prevents key/value mismatches at the call site.
*
* @param sle The ledger entry to insert. The key must not already
* exist in the backing store.
*/
virtual void
rawInsert(std::shared_ptr<SLE> const& sle) = 0;
/** Unconditionally replace a state item.
Requirements:
The key must exist.
Effects:
The key is associated with the SLE.
@note The key is taken from the SLE
*/
/** Unconditionally overwrite an existing state entry.
*
* The key is read from the SLE rather than passed separately,
* which prevents key/value mismatches at the call site.
*
* @param sle The replacement ledger entry. The key must already
* exist in the backing store.
*/
virtual void
rawReplace(std::shared_ptr<SLE> const& sle) = 0;
/** Destroy XRP.
This is used to pay for transaction fees.
*/
/** Permanently remove XRP drops from the ledger supply.
*
* XRPL burns transaction fees rather than redistributing them.
* This method is the accounting hook for that burn: separating it
* from `rawErase` keeps fee accounting explicit and auditable.
*
* @param fee The quantity of XRP drops to destroy.
*/
virtual void
rawDestroyXRP(XRPAmount const& fee) = 0;
};
//------------------------------------------------------------------------------
/** Interface for changing ledger entries with transactions.
Allows raw modification of ledger entries and insertion
of transactions into the transaction map.
*/
/** Extends `RawView` with the ability to insert transactions into the
* ledger's transaction map.
*
* The split between `RawView` (state-only writes) and `TxsRawView`
* (state plus transaction map) is architecturally significant.
* `detail::ApplyViewBase` — the sandbox used during transaction
* processing — only needs `RawView`: sandboxes accumulate state
* mutations but do not independently maintain a transaction map.
* `OpenView`, by contrast, inherits both `ReadView` and `TxsRawView`
* because it is the accumulation point for an open ledger round and
* must track both the growing state diff and the applied-transaction
* set.
*/
class TxsRawView : public RawView
{
public:
/** Add a transaction to the tx map.
Closed ledgers must have metadata,
while open ledgers omit metadata.
*/
/** Insert a serialized transaction into the ledger's transaction map.
*
* @param key The transaction's map key (typically its hash).
* @param txn Serialized transaction blob; must not be null.
* @param metaData Serialized transaction metadata, or null for open
* ledgers. Closed ledgers must supply metadata; open ledgers must
* pass null because consensus has not yet produced execution
* results.
*/
virtual void
rawTxInsert(
ReadView::key_type const& key,

View File

@@ -1,3 +1,17 @@
/** @file
* Defines the foundational read-only ledger view interface.
*
* `ReadView` is the base of the entire ledger view hierarchy. Every concrete
* ledger representation — finalized `Ledger`, in-progress `OpenView`, apply-time
* `Sandbox`, or payment-path `PaymentSandbox` — exposes its state through this
* interface. Code that only reads ledger data can operate on any view type without
* knowing the concrete implementation.
*
* `DigestAwareReadView` extends `ReadView` with per-entry cryptographic digests,
* used by `CachedView` for efficient cache invalidation and by `makeRulesGivenLedger`
* to detect amendment changes between ledger closes.
*/
#pragma once
#include <xrpl/basics/chrono.h>
@@ -21,21 +35,43 @@ namespace xrpl {
//------------------------------------------------------------------------------
/** A view into a ledger.
This interface provides read access to state
and transaction items. There is no checkpointing
or calculation of metadata.
*/
/** Pure abstract read-only interface to a ledger.
*
* Exposes two conceptually distinct maps: the **state map** (SLEs keyed by
* `uint256`) and the **transaction map** (committed transactions with metadata).
* Concrete implementations include `Ledger` (finalized), `OpenView` (in-progress),
* `Sandbox` (discardable apply-time copy), and `PaymentSandbox` (payment engine).
*
* @note Copy and move constructors explicitly re-initialize `sles` and `txs`
* with `*this`. Both members store a raw pointer to their owning view; a
* default memberwise copy would leave them pointing at the source object.
* Assignment operators are deleted for the same reason.
*/
class ReadView
{
public:
/** Pair of transaction and its associated metadata object.
*
* The metadata `STObject` is empty for open ledgers, since metadata is
* only finalized at ledger close time.
*/
using tx_type = std::pair<std::shared_ptr<STTx const>, std::shared_ptr<STObject const>>;
/** Raw key type for state-map and transaction-map lookups. */
using key_type = uint256;
/** Shared ownership handle to a non-modifiable state entry. */
using mapped_type = std::shared_ptr<SLE const>;
/** STL-compatible forward range over the ledger state map.
*
* Iterates all SLEs present in this view. Backed by type-erased
* `ReadViewFwdIter` so the same interface works across SHAMap-backed,
* delta-list, and sandbox views. `upperBound` enables sub-range scans
* without a full traversal.
*
* @note Visiting every state entry can be expensive as the ledger grows.
*/
struct SlesType : detail::ReadViewFwdRange<std::shared_ptr<SLE const>>
{
explicit SlesType(ReadView const& view);
@@ -43,13 +79,20 @@ public:
begin() const;
[[nodiscard]] Iterator
end() const;
/** Returns an iterator to the first SLE whose key is strictly greater than @p key. */
[[nodiscard]] Iterator
upperBound(key_type const& key) const;
};
/** STL-compatible forward range over the ledger transaction map.
*
* Iterates all `tx_type` pairs (transaction + metadata) present in
* this view. For open ledgers the metadata member of each pair is empty.
*/
struct TxsType : detail::ReadViewFwdRange<tx_type>
{
explicit TxsType(ReadView const& view);
/** Returns `true` when the transaction map contains no entries. */
[[nodiscard]] bool
empty() const;
[[nodiscard]] Iterator
@@ -65,92 +108,118 @@ public:
ReadView&
operator=(ReadView const& other) = delete;
/** Constructs the view and binds `sles` and `txs` to `*this`. */
ReadView() : sles(*this), txs(*this)
{
}
/** Copy-constructs the view, re-binding `sles` and `txs` to `*this`.
*
* @note The `sles` and `txs` members store a pointer to their owning
* view. They are explicitly re-initialized here to point at the new
* object, not at `other`.
*/
ReadView(ReadView const& other) : sles(*this), txs(*this)
{
}
/** Move-constructs the view, re-binding `sles` and `txs` to `*this`.
*
* @note Same aliasing concern as the copy constructor; `sles` and `txs`
* are explicitly re-initialized to point at the new object.
*/
ReadView(ReadView&& other) : sles(*this), txs(*this)
{
}
/** Returns information about the ledger. */
/** Returns the immutable header fields for this ledger.
*
* All non-virtual convenience accessors (`seq()`, `parentCloseTime()`)
* delegate here, keeping the virtual dispatch surface minimal.
*/
[[nodiscard]] virtual LedgerHeader const&
header() const = 0;
/** Returns true if this reflects an open ledger. */
/** Returns `true` if this view reflects an open (not yet closed) ledger. */
[[nodiscard]] virtual bool
open() const = 0;
/** Returns the close time of the previous ledger. */
/** Returns the close time of the previous (parent) ledger. */
[[nodiscard]] NetClock::time_point
parentCloseTime() const
{
return header().parentCloseTime;
}
/** Returns the sequence number of the base ledger. */
/** Returns the sequence number of this ledger. */
[[nodiscard]] LedgerIndex
seq() const
{
return header().seq;
}
/** Returns the fees for the base ledger. */
/** Returns the fee schedule in effect for this ledger. */
[[nodiscard]] virtual Fees const&
fees() const = 0;
/** Returns the tx processing rules. */
/** Returns the amendment rules active for this ledger. */
[[nodiscard]] virtual Rules const&
rules() const = 0;
/** Determine if a state item exists.
@note This can be more efficient than calling read.
@return `true` if a SLE is associated with the
specified key.
*/
/** Returns `true` if a state entry matching the keylet is present.
*
* The `Keylet` bundles a raw `uint256` key with its `LedgerEntryType`,
* allowing implementations to reject type mismatches without deserializing
* the entry. This makes `exists` more efficient than calling `read` when
* only presence is needed.
*
* @param k The keylet (key + expected entry type) to probe.
* @return `true` if an SLE with the given key and type exists.
*/
[[nodiscard]] virtual bool
exists(Keylet const& k) const = 0;
/** Return the key of the next state item.
This returns the key of the first state item
whose key is greater than the specified key. If
no such key is present, std::nullopt is returned.
If `last` is engaged, returns std::nullopt when
the key returned would be outside the open
interval (key, last).
*/
/** Returns the smallest state-map key strictly greater than @p key.
*
* Enables ordered range scans of the SHAMap without deserializing entries.
* If @p last is set, the search is bounded to the open interval
* `(key, last)` — any candidate key outside that range causes
* `std::nullopt` to be returned instead.
*
* @param key The key to search above.
* @param last Optional exclusive upper bound for the result.
* @return The next key, or `std::nullopt` if none exists within bounds.
*/
[[nodiscard]] virtual std::optional<key_type>
succ(key_type const& key, std::optional<key_type> const& last = std::nullopt) const = 0;
/** Return the state item associated with a key.
Effects:
If the key exists, gives the caller ownership
of the non-modifiable corresponding SLE.
@note While the returned SLE is `const` from the
perspective of the caller, it can be changed
by other callers through raw operations.
@return `nullptr` if the key is not present or
if the type does not match.
*/
/** Returns a read-only handle to the state entry identified by @p k.
*
* Gives the caller shared ownership of a non-modifiable SLE. The `const`
* qualifier reflects this caller's view; the underlying object may be
* mutated through `ApplyView` in another code path.
*
* @param k The keylet (key + expected entry type) to look up.
* @return Shared pointer to the SLE, or `nullptr` if the key is absent
* or the ledger entry type does not match the keylet.
*/
[[nodiscard]] virtual std::shared_ptr<SLE const>
read(Keylet const& k) const = 0;
// Accounts in a payment are not allowed to use assets acquired during that
// payment. The PaymentSandbox tracks the debits, credits, and owner count
// changes that accounts make during a payment. `balanceHookIOU` adjusts
// balances so newly acquired assets are not counted toward the balance.
// This is required to support PaymentSandbox.
/** Adjusts an IOU balance to exclude assets acquired during the current payment.
*
* The payment engine executes paths in reverse (destination-first), which
* means an account may be credited before it has redeemed the corresponding
* asset. Accounts must not spend assets acquired within the same payment.
* `PaymentSandbox` overrides this hook to subtract deferred credits recorded
* in its `DeferredCredits` table. The default implementation returns
* @p amount unchanged, making the hook zero-cost for non-payment views.
*
* @param account The account whose balance is being queried.
* @param issuer The IOU issuer.
* @param amount The raw IOU balance (must hold `Issue`).
* @return The effective spendable balance after deducting deferred credits.
*/
[[nodiscard]] virtual STAmount
balanceHookIOU(AccountID const& account, AccountID const& issuer, STAmount const& amount) const
{
@@ -159,71 +228,113 @@ public:
return amount;
}
// balanceHookMPT adjusts balances so newly acquired assets are not counted
// toward the balance.
/** Adjusts an MPT balance to exclude assets acquired during the current payment.
*
* Mirrors `balanceHookIOU` for MPT-denominated amounts. `PaymentSandbox`
* overrides this hook; the default implementation wraps @p amount in an
* `STAmount` and returns it unchanged.
*
* @param account The account whose balance is being queried.
* @param issue The MPT issuance.
* @param amount The raw MPT balance as a signed 64-bit integer.
* @return The effective spendable balance after deducting deferred credits.
*/
[[nodiscard]] virtual STAmount
balanceHookMPT(AccountID const& account, MPTIssue const& issue, std::int64_t amount) const
{
return STAmount{issue, amount};
}
// An offer owned by an issuer and selling MPT is limited by the issuer's
// funds available to issue, which are originally available funds less
// already self sold MPT amounts (MPT sell offer). This hook is used
// by issuerFundsToSelfIssue() function.
/** Adjusts the available issuance capacity for an issuer selling their own MPT.
*
* An issuer's sell-offer for their own MPT is limited by their remaining
* issuance capacity (i.e., `MaximumAmount - OutstandingAmount`), reduced
* by any MPT already committed to self-issued sell offers during this payment.
* `PaymentSandbox` overrides this hook to track that self-debit; the default
* returns @p amount unchanged. Used by `issuerFundsToSelfIssue()`.
*
* @param issue The MPT issuance.
* @param amount The raw available-issuance amount.
* @return The effective capacity after accounting for in-flight self-sold amounts.
*/
[[nodiscard]] virtual STAmount
balanceHookSelfIssueMPT(MPTIssue const& issue, std::int64_t amount) const
{
return STAmount{issue, amount};
}
// Accounts in a payment are not allowed to use assets acquired during that
// payment. The PaymentSandbox tracks the debits, credits, and owner count
// changes that accounts make during a payment. `ownerCountHook` adjusts the
// ownerCount so it returns the max value of the ownerCount so far.
// This is required to support PaymentSandbox.
/** Returns the effective owner count, adjusted for in-payment reserve changes.
*
* A payment could temporarily free reserves by consuming offers in intermediate
* steps, making it appear that an account has fewer owner-count obligations.
* `PaymentSandbox` overrides this hook to return the maximum owner count seen
* so far during the payment, preventing reserve-bypass exploits. The default
* implementation returns @p count unchanged.
*
* @param account The account being queried.
* @param count The current owner count from ledger state.
* @return The high-water-mark owner count for reserve purposes.
*/
[[nodiscard]] virtual std::uint32_t
ownerCountHook(AccountID const& account, std::uint32_t count) const
{
return count;
}
// used by the implementation
/** Returns a heap-allocated iterator positioned at the start of the state map.
*
* Called by `SlesType::begin()`; not intended for direct use by callers.
*/
[[nodiscard]] virtual std::unique_ptr<SlesType::iter_base>
slesBegin() const = 0;
// used by the implementation
/** Returns a heap-allocated sentinel iterator for the state map.
*
* Called by `SlesType::end()`; not intended for direct use by callers.
*/
[[nodiscard]] virtual std::unique_ptr<SlesType::iter_base>
slesEnd() const = 0;
// used by the implementation
/** Returns a heap-allocated iterator to the first SLE whose key is strictly greater than @p key.
*
* Called by `SlesType::upperBound()`; not intended for direct use by callers.
*/
[[nodiscard]] virtual std::unique_ptr<SlesType::iter_base>
slesUpperBound(key_type const& key) const = 0;
// used by the implementation
/** Returns a heap-allocated iterator positioned at the start of the transaction map.
*
* Called by `TxsType::begin()`; not intended for direct use by callers.
*/
[[nodiscard]] virtual std::unique_ptr<TxsType::iter_base>
txsBegin() const = 0;
// used by the implementation
/** Returns a heap-allocated sentinel iterator for the transaction map.
*
* Called by `TxsType::end()`; not intended for direct use by callers.
*/
[[nodiscard]] virtual std::unique_ptr<TxsType::iter_base>
txsEnd() const = 0;
/** Returns `true` if a tx exists in the tx map.
A tx exists in the map if it is part of the
base ledger, or if it is a newly inserted tx.
*/
/** Returns `true` if a transaction with the given key exists in the tx map.
*
* A transaction is present if it is part of the base ledger or was
* inserted into this view's delta since the base.
*
* @param key The transaction hash to probe.
*/
[[nodiscard]] virtual bool
txExists(key_type const& key) const = 0;
/** Read a transaction from the tx map.
If the view represents an open ledger,
the metadata object will be empty.
@return A pair of nullptr if the
key is not found in the tx map.
*/
/** Returns the transaction and its metadata for the given key.
*
* For open ledgers the metadata `STObject` in the returned pair will be
* empty, since metadata is only finalized at close time.
*
* @param key The transaction hash to look up.
* @return A `tx_type` pair where both pointers are `nullptr` if the key
* is not found in the transaction map.
*/
[[nodiscard]] virtual tx_type
txRead(key_type const& key) const = 0;
@@ -231,20 +342,29 @@ public:
// Memberspaces
//
/** Iterable range of ledger state items.
@note Visiting each state entry in the ledger can
become quite expensive as the ledger grows.
*/
/** Iterable range over all state entries (SLEs) in this view.
*
* @note Full traversal can be expensive on a large ledger. Use
* `upperBound` or `succ` for targeted sub-range scans.
*/
SlesType sles;
// The range of transactions
/** Iterable range over all transactions in this view. */
TxsType txs;
};
//------------------------------------------------------------------------------
/** ReadView that associates keys with digests. */
/** Extension of `ReadView` that provides per-entry cryptographic digests.
*
* `Ledger` implements this interface cheaply by reading the hash directly
* from the SHAMap trie node without deserializing the leaf entry. Sandboxes
* and delta-views do not expose digests, which is why this capability is a
* separate subclass rather than part of `ReadView`.
*
* Used by `CachedView` for two-level cache invalidation and by
* `makeRulesGivenLedger` to detect amendments changes across ledger closes.
*/
class DigestAwareReadView : public ReadView
{
public:
@@ -253,19 +373,48 @@ public:
DigestAwareReadView() = default;
DigestAwareReadView(DigestAwareReadView const&) = default;
/** Return the digest associated with the key.
@return std::nullopt if the item does not exist.
*/
/** Returns the cryptographic hash of the serialized state entry at @p key.
*
* Implementations may return this without fully deserializing the entry.
*
* @param key The raw state-map key to query.
* @return The entry's digest, or `std::nullopt` if no entry exists at that key.
*/
[[nodiscard]] virtual std::optional<digest_type>
digest(key_type const& key) const = 0;
};
//------------------------------------------------------------------------------
/** Constructs the active amendment `Rules` from a closed ledger, updating from existing rules.
*
* Reads the `sfAmendments` field from the ledger's amendments object and passes
* its digest to the `Rules` constructor so that `Rules` can detect unchanged
* amendments between successive ledger closes without re-parsing. Requires a
* `DigestAwareReadView` because the optimization depends on querying the entry
* hash directly. Falls back to a default `Rules` object if the amendments object
* is absent.
*
* @param ledger The closed ledger to read amendments from.
* @param current The current rules object; its internal preset set is forwarded
* to the new `Rules` instance.
* @return A `Rules` object reflecting the amendments active in @p ledger.
* @see makeRulesGivenLedger(DigestAwareReadView const&, std::unordered_set<uint256, beast::Uhash<>> const&)
*/
Rules
makeRulesGivenLedger(DigestAwareReadView const& ledger, Rules const& current);
/** Constructs the active amendment `Rules` from a closed ledger using an explicit preset set.
*
* Identical behavior to the `Rules const& current` overload but accepts
* the preset set directly. Used during initialization before a prior `Rules`
* object is available.
*
* @param ledger The closed ledger to read amendments from.
* @param presets The set of always-enabled amendment flags to seed the rules object.
* @return A `Rules` object reflecting the amendments active in @p ledger.
* @see makeRulesGivenLedger(DigestAwareReadView const&, Rules const&)
*/
Rules
makeRulesGivenLedger(
DigestAwareReadView const& ledger,

View File

@@ -5,12 +5,41 @@
namespace xrpl {
/** Discardable, editable view to a ledger.
The sandbox inherits the flags of the base.
@note Presented as ApplyView to clients.
*/
/** Discardable staging layer for ledger mutations within a single transaction.
*
* `Sandbox` accumulates ledger changes in a private write buffer inherited
* from `detail::ApplyViewBase` without touching the underlying ledger. The
* caller decides at the end of the operation whether to commit — by calling
* `apply()` — or to discard — by letting the sandbox go out of scope. This
* eliminates the need for explicit rollback: on failure, destruction of the
* sandbox is sufficient.
*
* The typical pattern used by transactors:
* @code
* Sandbox sb(&ctx_.view());
* auto const result = doWork(sb, ...);
* if (result == tesSUCCESS)
* sb.apply(ctx_.rawView());
* @endcode
*
* `Sandbox` is the minimal concrete subclass of `ApplyViewBase`: it adds
* only constructors and `apply()`. It does not produce `TxMeta` (that is
* `ApplyViewImpl`'s responsibility) and does not track deferred credits (that
* is `PaymentSandbox`'s responsibility). Use `Sandbox` whenever a transactor
* or helper needs a safe, atomic scratchpad without those heavier features.
*
* The sandbox always inherits the `ApplyFlags` of its base view, so
* dry-run, no-check-sign, and similar execution-context properties propagate
* correctly through nested sandboxes without re-specification.
*
* Not copyable or move-assignable; move-constructible only. This enforces
* single ownership of the change buffer.
*
* @see detail::ApplyViewBase for the full `ApplyView`/`RawView` interface.
* @see ApplyViewImpl for the outermost commit path that also builds `TxMeta`.
* @see PaymentSandbox for the variant that prevents within-payment
* double-counting of credits.
*/
class Sandbox : public detail::ApplyViewBase
{
public:
@@ -23,14 +52,46 @@ public:
Sandbox(Sandbox&&) = default;
/** Construct over any read-only ledger snapshot with explicit flags.
*
* @param base Non-owning pointer to the underlying ledger state; must
* outlive this sandbox. All reads that bypass the change buffer
* are forwarded here.
* @param flags Per-transaction policy flags (e.g. `tapDRY_RUN`,
* `tapNO_CHECK_SIGN`) governing this apply pass.
*/
Sandbox(ReadView const* base, ApplyFlags flags) : ApplyViewBase(base, flags)
{
}
/** Construct over an existing `ApplyView`, inheriting its flags.
*
* Convenience form used when stacking a `Sandbox` on top of another
* mutable view (including another `Sandbox` or a `PaymentSandbox`).
* Flags are copied from the parent so that execution-context properties
* such as `tapDRY_RUN` propagate without the caller re-specifying them.
*
* @param base Non-owning pointer to the parent mutable view; must
* outlive this sandbox.
*/
Sandbox(ApplyView const* base) : Sandbox(base, base->flags())
{
}
/** Commit all buffered changes to a target `RawView`.
*
* Replays every insert, modify, and erase action accumulated in the
* internal change buffer against `to`, atomically promoting the tentative
* mutations into the target. After this call the buffer is reset; the
* sandbox must not be used again.
*
* If the caller decides the operation failed, simply do not call `apply()`
* — destroying the sandbox discards all buffered changes without touching
* the target view.
*
* @param to The target `RawView` to receive the committed mutations;
* typically `ctx_.rawView()` at the outermost transactor boundary.
*/
void
apply(RawView& to)
{

View File

@@ -19,6 +19,13 @@
namespace xrpl {
/** Controls whether `cleanupOnAccountDelete()` adjusts the directory iterator
* after a deletion.
*
* When `No`, the iterator position is decremented to compensate for the
* element shift caused by the deletion. When `Yes`, the entry was
* intentionally left in place by the deleter, so no adjustment is made.
*/
enum class SkipEntry : bool { No = false, Yes };
//------------------------------------------------------------------------------
@@ -51,7 +58,21 @@ enum class SkipEntry : bool { No = false, Yes };
[[nodiscard]] bool
hasExpired(ReadView const& view, std::optional<std::uint32_t> const& exp);
// Note, depth parameter is used to limit the recursion depth
/** Determines whether a vault pseudo-account's MPT share token is indirectly
* frozen because the vault's underlying asset is frozen.
*
* Traverses: MPT issuance → issuer account root → vault object → vault asset,
* then delegates to `isAnyFrozen()`. Returns `false` immediately if the
* `featureSingleAssetVault` amendment is not enabled.
*
* @param view The ledger state to inspect.
* @param account The account whose holdings are being queried.
* @param mptShare The MPT share token issued by the vault pseudo-account.
* @param depth Recursion depth guard; returns `true` (conservatively frozen)
* if `depth >= kMAX_ASSET_CHECK_DEPTH`.
* @return `true` if the underlying asset is frozen for `account`; `false`
* otherwise or if the amendment is not enabled.
*/
[[nodiscard]] bool
isVaultPseudoAccountFrozen(
ReadView const& view,
@@ -59,6 +80,17 @@ isVaultPseudoAccountFrozen(
MPTIssue const& mptShare,
int depth);
/** Determines whether LP tokens for an AMM pool are frozen for an account.
*
* LP tokens are considered frozen if *either* constituent asset of the pool
* is frozen for `account`.
*
* @param view The ledger state to inspect.
* @param account The account whose holdings are being queried.
* @param asset The first asset of the AMM pool.
* @param asset2 The second asset of the AMM pool.
* @return `true` if either `asset` or `asset2` is frozen for `account`.
*/
[[nodiscard]] bool
isLPTokenFrozen(
ReadView const& view,
@@ -66,50 +98,94 @@ isLPTokenFrozen(
Asset const& asset,
Asset const& asset2);
// Return the list of enabled amendments
/** Returns the set of amendment hashes currently enabled on the ledger.
*
* Reads from the singleton `keylet::amendments()` SLE. If no amendments
* SLE exists or none are yet enabled, returns an empty set.
*
* @param view The ledger state to query.
* @return A `std::set<uint256>` containing every enabled amendment hash.
*/
[[nodiscard]] std::set<uint256>
getEnabledAmendments(ReadView const& view);
// Return a map of amendments that have achieved majority
/** Maps amendment hashes to the `NetClock::time_point` at which each first
* achieved validator supermajority. Used by the amendment governance process
* to enforce the two-week waiting period before activation.
*/
using majorityAmendments_t = std::map<uint256, NetClock::time_point>;
/** Returns amendments that have achieved validator supermajority but are not
* yet enabled.
*
* Reads the `sfMajorities` array from the singleton `keylet::amendments()`
* SLE and converts each entry's `sfCloseTime` to a `NetClock::time_point`.
* Returns an empty map if no SLE exists or no majority amendments are pending.
*
* @param view The ledger state to query.
* @return A `majorityAmendments_t` mapping each amendment hash to the time
* at which it first achieved supermajority.
*/
[[nodiscard]] majorityAmendments_t
getMajorityAmendments(ReadView const& view);
/** Return the hash of a ledger by sequence.
The hash is retrieved by looking up the "skip list"
in the passed ledger. As the skip list is limited
in size, if the requested ledger sequence number is
out of the range of ledgers represented in the skip
list, then std::nullopt is returned.
@return The hash of the ledger with the
given sequence number or std::nullopt.
*/
/** Returns the hash of a past ledger by sequence number via the skip list.
*
* Implements a three-tier lookup:
* 1. **Trivial**: `seq == ledger.seq()` → returns the ledger's own hash;
* `seq == ledger.seq() - 1` → returns `parentHash` directly.
* 2. **Within 256**: Reads the rolling `keylet::skip()` object, which stores
* the hashes of the previous ≤ 256 ledgers, and indexes by offset.
* 3. **Aligned deep history**: For sequences that are multiples of 256, reads
* the permanent `LedgerHashes` page at `keylet::skip(seq)` and indexes into
* it. Non-aligned sequences beyond the 256-ledger rolling window are not
* reachable and return `std::nullopt`.
*
* @param ledger The view from whose skip list the search starts.
* @param seq The target ledger sequence number.
* @param journal Used to log warnings when the skip list is incomplete or the
* requested sequence is out of range.
* @return The hash of ledger `seq`, or `std::nullopt` if it cannot be
* determined from the available skip-list data.
*/
[[nodiscard]] std::optional<uint256>
hashOfSeq(ReadView const& ledger, LedgerIndex seq, beast::Journal journal);
/** Find a ledger index from which we could easily get the requested ledger
The index that we return should meet two requirements:
1) It must be the index of a ledger that has the hash of the ledger
we are looking for. This means that its sequence must be equal to
greater than the sequence that we want but not more than 256 greater
since each ledger contains the hashes of the 256 previous ledgers.
2) Its hash must be easy for us to find. This means it must be 0 mod 256
because every such ledger is permanently enshrined in a LedgerHashes
page which we can easily retrieve via the skip list.
*/
/** Computes the nearest 256-aligned ledger sequence ≥ `requested`.
*
* Every ledger whose sequence is a multiple of 256 permanently stores a
* `LedgerHashes` page (`keylet::skip(seq)`) containing the hashes of
* the preceding 256 ledgers. That page is retrievable via the skip list,
* making it the ideal starting point for resolving an arbitrary past hash.
* The expression `(requested + 255) & (~255)` rounds up to the next 256
* boundary in a single instruction.
*
* @param requested The target ledger sequence number.
* @return The smallest value ≥ `requested` that is divisible by 256.
*/
inline LedgerIndex
getCandidateLedger(LedgerIndex requested)
{
return (requested + 255) & (~255);
}
/** Return false if the test ledger is provably incompatible
with the valid ledger, that is, they could not possibly
both be valid. Use the first form if you have both ledgers,
use the second form if you have not acquired the valid ledger yet
*/
/** Returns `false` if `testLedger` is provably on a different chain than
* `validLedger`.
*
* Uses `hashOfSeq()` to walk the skip list of whichever ledger is later and
* confirms that the earlier ledger's hash appears in that list. A mismatch
* proves a fork. When the skip list is incomplete or the sequences are too
* far apart to compare, the function conservatively returns `true` (cannot
* prove incompatibility). Diagnostic lines are written to `s` on mismatch.
*
* Use this overload when both ledger objects are available.
*
* @param validLedger The authoritative ledger.
* @param testLedger The candidate ledger being verified.
* @param s Journal stream for diagnostic messages on mismatch.
* @param reason Short label prepended to log messages for context.
* @return `false` if a fork is proven; `true` otherwise.
*/
[[nodiscard]] bool
areCompatible(
ReadView const& validLedger,
@@ -117,6 +193,19 @@ areCompatible(
beast::Journal::Stream& s,
char const* reason);
/** Returns `false` if `testLedger` is provably on a different chain than the
* ledger identified by `(validHash, validIndex)`.
*
* Use this overload when the authoritative ledger object has not been fully
* loaded but its identity is known from consensus.
*
* @param validHash Hash of the authoritative ledger.
* @param validIndex Sequence number of the authoritative ledger.
* @param testLedger The candidate ledger being verified.
* @param s Journal stream for diagnostic messages on mismatch.
* @param reason Short label prepended to log messages for context.
* @return `false` if a fork is proven; `true` otherwise.
*/
[[nodiscard]] bool
areCompatible(
uint256 const& validHash,
@@ -131,6 +220,19 @@ areCompatible(
//
//------------------------------------------------------------------------------
/** Inserts an SLE into an account's owner directory and records the page.
*
* Calls `view.dirInsert()` to append `object` to `owner`'s owner directory,
* then writes the assigned page number back into `object`'s `node` field.
*
* @param view The mutable ledger view.
* @param owner The account whose owner directory receives the entry.
* @param object The SLE being linked; updated in-place with the page number.
* @param node The field on `object` that receives the directory page number;
* defaults to `sfOwnerNode`.
* @return `tecDIR_FULL` if the owner directory has no room; `tesSUCCESS`
* otherwise.
*/
[[nodiscard]] TER
dirLink(
ApplyView& view,
@@ -138,19 +240,30 @@ dirLink(
std::shared_ptr<SLE>& object,
SF_UINT64 const& node = sfOwnerNode);
/** Checks that can withdraw funds from an object to itself or a destination.
/** Checks whether funds can be withdrawn from `from` to `to` given a
* pre-fetched destination SLE.
*
* The receiver may be either the submitting account (sfAccount) or a different
* destination account (sfDestination).
* This is the innermost overload; use it when the caller already holds `toSle`
* to avoid a redundant ledger read. Rules enforced in order:
* - `toSle` must be non-null (destination account must exist).
* - If `lsfRequireDestTag` is set, `hasDestinationTag` must be `true` even
* for self-sends.
* - If `from == to`, succeed immediately.
* - If `lsfDepositAuth` is set, `from` must have a pre-authorized
* `DepositPreauth` entry under `to`.
* - For IOU amounts, the withdrawal must not push `to` past its trust-line
* credit limit. MPT transfers skip this check because they move existing
* supply rather than creating new tokens.
*
* - Checks that the receiver account exists.
* - If the receiver requires a destination tag, check that one exists, even
* if withdrawing to self.
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it.
* - Checks that the receiver will not exceed the limit (IOU trustline limit
* or MPT MaximumAmount).
* @param view Ledger state to query.
* @param from Source account (e.g., vault or broker pseudo-account).
* @param to Destination account.
* @param toSle Pre-fetched SLE for `to`; may be null.
* @param amount Asset and quantity being transferred.
* @param hasDestinationTag Whether the transaction includes `sfDestinationTag`.
* @return `tesSUCCESS`, or a `tec` code: `tecNO_DST` (account absent),
* `tecDST_TAG_NEEDED` (tag missing), `tecNO_PERMISSION` (deposit auth
* denied), or `tecNO_LINE` (IOU limit exceeded).
*/
[[nodiscard]] TER
canWithdraw(
@@ -161,19 +274,17 @@ canWithdraw(
STAmount const& amount,
bool hasDestinationTag);
/** Checks that can withdraw funds from an object to itself or a destination.
/** Checks whether funds can be withdrawn from `from` to `to`.
*
* The receiver may be either the submitting account (sfAccount) or a different
* destination account (sfDestination).
* Looks up the destination account SLE and delegates to the six-argument
* overload. See that overload for the full rule set.
*
* - Checks that the receiver account exists.
* - If the receiver requires a destination tag, check that one exists, even
* if withdrawing to self.
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it.
* - Checks that the receiver will not exceed the limit (IOU trustline limit
* or MPT MaximumAmount).
* @param view Ledger state to query.
* @param from Source account.
* @param to Destination account.
* @param amount Asset and quantity being transferred.
* @param hasDestinationTag Whether the transaction includes `sfDestinationTag`.
* @return `tesSUCCESS` or a `tec` code; see the six-argument overload.
*/
[[nodiscard]] TER
canWithdraw(
@@ -183,23 +294,45 @@ canWithdraw(
STAmount const& amount,
bool hasDestinationTag);
/** Checks that can withdraw funds from an object to itself or a destination.
/** Checks whether the withdrawal described by `tx` is permitted.
*
* The receiver may be either the submitting account (sfAccount) or a different
* destination account (sfDestination).
* Extracts `sfAccount`, `sfDestination` (defaults to `sfAccount` when absent),
* `sfAmount`, and the presence of `sfDestinationTag` from the transaction, then
* delegates to the five-argument overload. Intended for use in preclaim.
*
* - Checks that the receiver account exists.
* - If the receiver requires a destination tag, check that one exists, even
* if withdrawing to self.
* - If withdrawing to self, succeed.
* - If not, checks if the receiver requires deposit authorization, and if
* the sender has it.
* - Checks that the receiver will not exceed the limit (IOU trustline limit
* or MPT MaximumAmount).
* @param view Ledger state to query.
* @param tx The withdrawal transaction (e.g., `VaultWithdraw` or
* `LoanBrokerCoverWithdraw`).
* @return `tesSUCCESS` or a `tec` code; see the six-argument overload.
*/
[[nodiscard]] TER
canWithdraw(ReadView const& view, STTx const& tx);
/** Executes the physical asset transfer from a pseudo-account to a destination.
*
* When `dstAcct == senderAcct` (self-withdrawal), calls `addEmptyHolding()`
* to lazily create a trust line or MPToken record if one does not already
* exist (`tecDUPLICATE` is silently tolerated). For third-party
* destinations, calls `verifyDepositPreauth()` to enforce deposit
* authorisation and prune any expired credential objects as a side-effect.
*
* Before transferring, asserts via `accountHolds()` that `sourceAcct` holds
* at least `amount`; a shortfall surfaces as `tefINTERNAL` rather than an
* overdraft. On success, calls `accountSend()` with `WaiveTransferFee::Yes`.
*
* @param view The mutable ledger view.
* @param tx The originating transaction (used by `verifyDepositPreauth`).
* @param senderAcct The transaction submitter / withdrawal beneficiary.
* @param dstAcct The account that will receive the funds.
* @param sourceAcct The pseudo-account (vault, loan broker) holding the funds.
* @param priorBalance The XRP balance of `senderAcct` before the transaction,
* used for reserve calculation when creating an empty holding.
* @param amount The asset and quantity to transfer.
* @param j Journal for diagnostic logging.
* @return `tesSUCCESS` on success; `tefINTERNAL` if the source has
* insufficient balance; any TER propagated from `verifyDepositPreauth` or
* `accountSend` otherwise.
*/
[[nodiscard]] TER
doWithdraw(
ApplyView& view,
@@ -211,18 +344,41 @@ doWithdraw(
STAmount const& amount,
beast::Journal j);
/** Deleter function prototype. Returns the status of the entry deletion
* (if should not be skipped) and if the entry should be skipped. The status
* is always tesSUCCESS if the entry should be skipped.
/** Callback invoked by `cleanupOnAccountDelete()` for each owner-directory entry.
*
* Returns a pair:
* - `TER` — `tesSUCCESS` if the entry was handled or intentionally skipped;
* any other code aborts the cleanup loop immediately.
* - `SkipEntry` — `Yes` if the entry was left in place (iterator must not be
* decremented); `No` if the entry was removed (iterator must be decremented
* to compensate for the index shift).
*
* The `TER` value is always `tesSUCCESS` when `SkipEntry` is `Yes`.
*/
using EntryDeleter = std::function<
std::pair<TER, SkipEntry>(LedgerEntryType, uint256 const&, std::shared_ptr<SLE>&)>;
/** Cleanup owner directory entries on account delete.
* Used for a regular and AMM accounts deletion. The caller
* has to provide the deleter function, which handles details of
* specific account-owned object deletion.
* @return tecINCOMPLETE indicates maxNodesToDelete
* are deleted and there remains more nodes to delete.
/** Iterates an account's owner directory and removes entries via `deleter`.
*
* Used by `DeleteAccount` and AMM account deletion. Traversal uses the
* `dirFirst`/`dirNext` exposed-cursor pattern; after each successful removal
* the cursor is decremented by one to compensate for the index shift that
* occurs when an element is erased mid-iteration. When the deleter leaves an
* entry in place (`SkipEntry::Yes`), the cursor is not adjusted.
*
* When `maxNodesToDelete` is supplied and the limit is reached before the
* directory is empty, `tecINCOMPLETE` is returned, signaling the caller that
* the account-delete transaction must be retried in a future ledger.
*
* @param view Mutable ledger view.
* @param ownerDirKeylet Keylet of the account's owner directory root.
* @param deleter Callback invoked once per directory entry.
* @param j Journal for diagnostic logging.
* @param maxNodesToDelete Optional cap on entries processed per call.
* When absent, all entries are consumed in a single invocation.
* @return `tesSUCCESS` when the directory is fully processed;
* `tecINCOMPLETE` if `maxNodesToDelete` is exhausted with entries
* remaining; `tefBAD_LEDGER` if a ledger invariant is violated.
*/
[[nodiscard]] TER
cleanupOnAccountDelete(

View File

@@ -1,3 +1,9 @@
/** @file
* Declares `ApplyStateTable`, the per-transaction write-staging buffer used
* by all `ApplyView`/`ApplyViewImpl` instances. This is an implementation
* detail of `ApplyViewBase` and is not intended for direct use by transactors.
*/
#pragma once
#include <xrpl/beast/utility/Journal.h>
@@ -12,18 +18,36 @@
namespace xrpl::detail {
// Helper class that buffers modifications
/** Write-staging buffer for a single transaction's ledger mutations.
*
* Every SLE touched by a transaction is recorded here — keyed by its
* `uint256` ledger key — along with an `Action` tag that tracks whether
* the entry was merely read (`Cache`), newly created (`Insert`), mutated
* (`Modify`), or scheduled for removal (`Erase`). On success the buffer
* is flushed atomically to the underlying view; on failure the table is
* simply discarded.
*
* The class is the core member of `ApplyViewBase` and backs all
* `ApplyView`/`ApplyViewImpl` instances that transactors receive.
*
* @note Not copyable. Move-constructible only to support placement inside
* `ApplyViewBase` during construction.
* @note `erase()` and `update()` enforce pointer-identity: the caller
* must pass the exact `shared_ptr` returned by `peek()` on this same
* table instance. Crossing views is a `LogicError`.
*/
class ApplyStateTable
{
public:
using key_type = ReadView::key_type;
private:
/** Lifecycle state of a buffered ledger entry. */
enum class Action {
Cache,
Erase,
Insert,
Modify,
Cache, /**< Read from base; no write intent yet. */
Erase, /**< Scheduled for deletion from the base view. */
Insert, /**< New object not yet in the base view. */
Modify, /**< Existing object with pending mutations. */
};
using items_t = std::map<key_type, std::pair<Action, std::shared_ptr<SLE>>>;
@@ -41,9 +65,48 @@ public:
ApplyStateTable&
operator=(ApplyStateTable const&) = delete;
/** Flush all pending mutations to a raw view without generating metadata.
*
* Maps each buffered action to a raw write on `to`: `Cache` entries
* are skipped; `Erase` → `rawErase`; `Insert` → `rawInsert`;
* `Modify` → `rawReplace`. Also forwards the accumulated
* `dropsDestroyed_` to `to.rawDestroyXRP()`.
*
* Used when committing a sandbox or nested view back to its parent.
*
* @param to The target raw view to receive the mutations.
*/
void
apply(RawView& to) const;
/** Flush mutations to an open view, generating `TxMeta` for closed ledgers.
*
* For closed ledgers (`!to.open()`) or dry-run mode (`isDryRun`),
* builds full `TxMeta` — classifying every pending item as
* `sfCreatedNode`, `sfModifiedNode`, or `sfDeletedNode` — and
* populates `sfPreviousFields`/`sfFinalFields`/`sfNewFields` using
* `SField` metadata flags. Threads `sfPreviousTxnID`/
* `sfPreviousTxnLgrSeq` onto affected account roots and trust-line
* endpoints.
*
* In dry-run mode the metadata is produced but state changes and the
* raw tx insert are suppressed — supporting fee simulation without
* side effects.
*
* A `sfModifiedNode` whose buffered content is byte-for-byte equal to
* the original is silently omitted from the metadata.
*
* @param to The open view to commit into.
* @param tx The transaction being applied.
* @param ter The transaction result code; recorded in the metadata.
* @param deliver Optional delivered amount annotation for the metadata.
* @param parentBatchId Optional batch parent ID for the metadata.
* @param isDryRun If true, produce metadata but suppress state mutations.
* @param j Journal for diagnostic logging.
* @return The generated `TxMeta` when `!to.open() || isDryRun`;
* `std::nullopt` when the view is open and `isDryRun` is false
* (live open-ledger apply, no metadata needed).
*/
std::optional<TxMeta>
apply(
OpenView& to,
@@ -54,21 +117,88 @@ public:
bool isDryRun,
beast::Journal j);
/** Test whether a ledger object exists, accounting for pending changes.
*
* Returns `false` for objects pending `Erase`; returns `true` for
* objects buffered as `Cache`, `Insert`, or `Modify`; falls back to
* `base.exists(k)` for keys not yet in the buffer.
*
* @param base The underlying read view (base ledger state).
* @param k The keylet identifying the object to test.
* @return `true` if the object will exist after the pending changes.
*/
[[nodiscard]] bool
exists(ReadView const& base, Keylet const& k) const;
/** Find the smallest key strictly greater than `key` that will exist
* after applying pending changes, up to but not including `last`.
*
* Merges two sorted key spaces: the base ledger (skipping keys
* pending deletion) and the local `items_` map (skipping erased
* entries). Returns whichever candidate is smaller.
*
* @param base The underlying read view supplying the base key space.
* @param key The starting key (exclusive lower bound).
* @param last Optional exclusive upper bound; if the result reaches
* or exceeds `last`, `std::nullopt` is returned.
* @return The next live key, or `std::nullopt` if none exists in
* range.
*/
[[nodiscard]] std::optional<key_type>
succ(ReadView const& base, key_type const& key, std::optional<key_type> const& last) const;
/** Read a ledger object as an immutable snapshot, accounting for
* pending changes.
*
* Returns `nullptr` for objects pending `Erase` or whose keylet
* check fails; returns the buffered SLE for `Cache`, `Insert`, and
* `Modify` entries; falls back to `base.read(k)` for unknown keys.
*
* @param base The underlying read view.
* @param k The keylet identifying the object.
* @return A `const`-qualified `shared_ptr` to the SLE, or `nullptr`
* if the object does not exist or the keylet check fails.
*/
[[nodiscard]] std::shared_ptr<SLE const>
read(ReadView const& base, Keylet const& k) const;
/** Obtain a mutable handle to a ledger object, loading it on first
* access.
*
* If the key is not yet in the buffer, reads from `base` and stores
* a private copy under `Action::Cache`. Subsequent calls return the
* same `shared_ptr`. Returns `nullptr` for erased objects or when the
* object does not exist in `base`.
*
* The returned pointer is the exact instance that must be passed to
* `update()` or `erase()` — pointer identity is enforced.
*
* @param base The underlying read view.
* @param k The keylet identifying the object.
* @return A mutable `shared_ptr` to the buffered SLE, or `nullptr`.
*/
std::shared_ptr<SLE>
peek(ReadView const& base, Keylet const& k);
/** Count pending mutations (Erase, Insert, Modify), excluding cache-only reads.
*
* @return The number of entries with a write-intent action.
*/
[[nodiscard]] std::size_t
size() const;
/** Invoke a callback for every pending write-intent entry.
*
* Calls `func` once for each `Erase`, `Insert`, or `Modify` entry in
* the buffer. `Cache`-only entries are skipped. The `before` snapshot
* is read from `base` on each call; `after` is the buffered SLE.
*
* @param base The underlying read view used to fetch the pre-change
* snapshots for `Erase` and `Modify` entries.
* @param func Callback invoked as
* `func(key, isDelete, before, after)`. `before` is `nullptr`
* for `Insert`; `after` is the pending SLE in all cases.
*/
void
visit(
ReadView const& base,
@@ -78,25 +208,95 @@ public:
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)> const& func) const;
/** Mark a buffered object for deletion.
*
* Transitions the action from `Cache` or `Modify` to `Erase`. If the
* object was previously `Insert`ed within this same transaction, the
* entry is removed entirely (net-zero effect on the base). Calling on
* an unknown key or a different `shared_ptr` than the one returned by
* `peek()` is a `LogicError`.
*
* @param base The underlying read view (used for key lookup context).
* @param sle The exact `shared_ptr` previously obtained from `peek()`.
* @throws std::logic_error If the key is not in the buffer, the
* pointer does not match, or the entry is already erased.
*/
void
erase(ReadView const& base, std::shared_ptr<SLE> const& sle);
/** Mark an object for deletion without enforcing pointer identity.
*
* Behaves like `erase()` but accepts any SLE with the matching key —
* the caller-provided pointer replaces whatever is stored. Used by
* `ApplyViewBase` for raw-level operations that bypass the ownership
* protocol enforced by `erase()`.
*
* @param base The underlying read view (used for key lookup context).
* @param sle An SLE whose key identifies the object to erase.
* @throws std::logic_error If the object is already pending erasure.
*/
void
rawErase(ReadView const& base, std::shared_ptr<SLE> const& sle);
/** Stage a new ledger object for insertion.
*
* Records the SLE under `Action::Insert`. If the key was previously
* erased within this same transaction, the action is collapsed to
* `Action::Modify` (insert-after-erase = replace). Attempting to
* insert over an existing `Cache`, `Insert`, or `Modify` entry is
* a `LogicError`.
*
* @param base The underlying read view (used for key lookup context).
* @param sle The new SLE to insert.
* @throws std::logic_error If the key already exists with a
* non-erase action.
*/
void
insert(ReadView const& base, std::shared_ptr<SLE> const& sle);
/** Promote a cached or new SLE to a definitive write.
*
* Requires the exact `shared_ptr` returned by `peek()`. Transitions
* `Cache` → `Modify`; `Insert` and `Modify` are left unchanged
* (already write-intent). Calling on an erased or unknown entry is a
* `LogicError`.
*
* @param base The underlying read view (used for key lookup context).
* @param sle The exact `shared_ptr` previously obtained from `peek()`.
* @throws std::logic_error If the key is missing, the pointer does not
* match, or the entry is already erased.
*/
void
update(ReadView const& base, std::shared_ptr<SLE> const& sle);
/** Unconditionally overwrite the buffered SLE for a given key.
*
* Records the SLE under `Action::Modify`, replacing any existing
* `Cache` or `Insert` entry with the supplied pointer. Calling on an
* erased entry is a `LogicError`. Unlike `update()`, does not enforce
* pointer identity — the caller supplies a fresh SLE.
*
* @param base The underlying read view (used for key lookup context).
* @param sle The SLE to store.
* @throws std::logic_error If the key is currently pending erasure.
*/
void
replace(ReadView const& base, std::shared_ptr<SLE> const& sle);
/** Record XRP drops destroyed by fees within this transaction's scope.
*
* Accumulates into `dropsDestroyed_`, which is forwarded to
* `RawView::rawDestroyXRP()` on `apply()`.
*
* @param fee The amount of XRP to permanently remove from circulation.
*/
void
destroyXRP(XRPAmount const& fee);
// For debugging
/** Return the total XRP drops marked for destruction so far.
*
* @return Reference to the accumulated destroyed-drops counter.
*/
[[nodiscard]] XRPAmount const&
dropsDestroyed() const
{
@@ -104,17 +304,74 @@ public:
}
private:
/** Scratch map used during threading to track SLEs modified solely by
* metadata updates (i.e., objects whose only change is the addition of
* `sfPreviousTxnID`/`sfPreviousTxnLgrSeq` fields). These are kept
* separate from `items_` to avoid promoting cache entries to
* `Action::Modify` for transactional purposes.
*/
using Mods = hash_map<key_type, std::shared_ptr<SLE>>;
/** Update an SLE's thread fields and record the previous tx link in metadata.
*
* Calls `sle->thread(txID, lgrSeq, ...)` to update `sfPreviousTxnID`
* and `sfPreviousTxnLgrSeq` in place. If there was a preceding
* transaction, adds those old fields to the affected node in `meta`
* so the chain of transactions is visible in on-ledger metadata.
*
* @param meta The `TxMeta` object being built for the current transaction.
* @param sle The account-root SLE to thread.
*/
static void
threadItem(TxMeta& meta, std::shared_ptr<SLE> const& to);
/** Retrieve an SLE for threading modification, using `mods` as a cache.
*
* Checks `mods` first, then `items_` (returning non-cache entries
* directly), then falls back to `base`. Objects found in `items_` as
* `Action::Cache` are copied into `mods` so that threading-only
* mutations do not promote them to `Action::Modify` in the primary
* table. Returns `nullptr` when threading to a deleted or nonexistent
* account (e.g., an expired Escrow destination), which is legal.
*
* @param base The underlying read view.
* @param key The ledger key of the SLE to retrieve.
* @param mods Scratch map of threading-only modifications.
* @param j Journal for warnings about missing or deleted targets.
* @return A mutable SLE, or `nullptr` if the account does not exist.
*/
std::shared_ptr<SLE>
getForMod(ReadView const& base, key_type const& key, Mods& mods, beast::Journal j);
/** Thread the current transaction to a specific account's root SLE.
*
* Looks up the account root via `getForMod()` and calls `threadItem()`
* on it. Logs a warning and returns without error if the account does
* not exist (e.g., destination of a deleted Escrow or PayChannel).
*
* @param base The underlying read view.
* @param meta The `TxMeta` object being built.
* @param to The account whose root SLE should be threaded.
* @param mods Scratch map of threading-only modifications.
* @param j Journal for warnings about missing targets.
*/
void
threadTx(ReadView const& base, TxMeta& meta, AccountID const& to, Mods& mods, beast::Journal j);
/** Thread the current transaction to all owner accounts of a ledger entry.
*
* Dispatches by `LedgerEntryType`:
* - `ltACCOUNT_ROOT`: no-op (threading to self is handled by the caller).
* - `ltRIPPLE_STATE`: threads to both the low-limit and high-limit account.
* - All others: threads to `sfAccount` if present, and to `sfDestination`
* if present.
*
* @param base The underlying read view.
* @param meta The `TxMeta` object being built.
* @param sle The ledger entry whose owner accounts should be threaded.
* @param mods Scratch map of threading-only modifications.
* @param j Journal for warnings about missing targets.
*/
void
threadOwners(
ReadView const& base,

View File

@@ -1,3 +1,14 @@
/** @file
* Declares `ApplyViewBase`, the abstract concrete base class shared by all
* buffered mutable ledger views used during transaction application.
*
* `ApplyViewBase` lives in `xrpl::detail` to signal that it is internal
* infrastructure; transaction processing code works with `ApplyView` or
* `ApplyViewImpl` references. The three concrete subclasses —
* `ApplyViewImpl`, `Sandbox`, and `PaymentSandbox` — are the only types
* that need to reach into this layer directly.
*/
#pragma once
#include <xrpl/ledger/ApplyView.h>
@@ -7,6 +18,24 @@
namespace xrpl::detail {
/** Concrete base for buffered mutable ledger views.
*
* Implements the full `ApplyView` and `RawView` interfaces on top of two
* members: a read-only pointer to the base ledger snapshot (`base_`) and
* an `ApplyStateTable` change buffer (`items_`). Queries that need
* awareness of pending mutations (e.g. `exists`, `read`, `peek`) merge
* `items_` with `base_`; purely structural queries (`header`, `fees`,
* `rules`) and SLE iterators bypass `items_` and forward directly to
* `base_`.
*
* Not copyable; move-constructible only. Subclasses (`ApplyViewImpl`,
* `Sandbox`) supply lifecycle logic such as `apply()`.
*
* @note The `erase()` and `update()` methods enforce pointer identity: the
* caller must pass the exact `shared_ptr` returned by `peek()` on
* **this** view instance. Passing an SLE obtained from a different view
* results in a `LogicError`.
*/
class ApplyViewBase : public ApplyView, public RawView
{
public:
@@ -19,85 +48,254 @@ public:
ApplyViewBase(ApplyViewBase&&) = default;
/** Construct over an existing read-only ledger snapshot.
*
* @param base Non-owning pointer to the base ledger state; must
* outlive this view. All reads that bypass the change buffer
* are forwarded here.
* @param flags Per-transaction policy flags (retry mode, dry-run,
* unlimited, etc.) that are carried through the apply pass and
* exposed via `flags()`.
*/
ApplyViewBase(ReadView const* base, ApplyFlags flags);
// ReadView
/** @return `true` if the underlying view represents an open ledger. */
[[nodiscard]] bool
open() const override;
/** @return The ledger header from the base snapshot. */
[[nodiscard]] LedgerHeader const&
header() const override;
/** @return The fee schedule from the base snapshot. */
[[nodiscard]] Fees const&
fees() const override;
/** @return The amendment rules from the base snapshot. */
[[nodiscard]] Rules const&
rules() const override;
/** Test whether a ledger object exists, accounting for pending changes.
*
* Returns `false` for objects pending erasure, `true` for objects
* buffered as inserted or modified, and delegates to `base_` for
* keys not yet in the change buffer.
*
* @param k Keylet identifying the object.
* @return `true` if the object will exist after the pending changes.
*/
[[nodiscard]] bool
exists(Keylet const& k) const override;
/** Find the next live key after `key`, accounting for pending changes.
*
* Merges the base key space (skipping keys pending deletion) with the
* local change buffer (skipping erased entries) and returns the smaller
* candidate key that is less than `last`.
*
* @param key Exclusive lower bound.
* @param last Optional exclusive upper bound.
* @return The next live key, or `std::nullopt` if none exists in range.
*/
[[nodiscard]] std::optional<key_type>
succ(key_type const& key, std::optional<key_type> const& last = std::nullopt) const override;
/** Read a ledger object as an immutable snapshot, accounting for pending
* changes.
*
* Returns `nullptr` for objects pending erasure or when the keylet check
* fails; returns the buffered SLE for inserted/modified entries; falls
* back to `base_` for unknown keys.
*
* @param k Keylet identifying the object.
* @return A `const`-qualified handle to the SLE, or `nullptr`.
*/
[[nodiscard]] std::shared_ptr<SLE const>
read(Keylet const& k) const override;
/** @name SLE iterators (base snapshot only)
*
* These iterators forward directly to `base_` and do **not** reflect
* pending insertions or deletions in the change buffer. This is
* intentional: the apply phase never needs to iterate its own buffered
* writes, and bypassing the buffer keeps SLE traversal consistent with
* the base ledger snapshot.
*/
/** @{ */
[[nodiscard]] std::unique_ptr<SlesType::iter_base>
slesBegin() const override;
[[nodiscard]] std::unique_ptr<SlesType::iter_base>
slesEnd() const override;
/** Return an iterator to the first SLE whose key is not less than `key`,
* drawn from the base snapshot only.
*
* @param key The lower-bound key for the search.
* @return An iterator into the base SLE map at or after `key`.
*/
[[nodiscard]] std::unique_ptr<SlesType::iter_base>
slesUpperBound(uint256 const& key) const override;
/** @} */
/** @name Transaction-map accessors (forwarded to base snapshot) */
/** @{ */
[[nodiscard]] std::unique_ptr<TxsType::iter_base>
txsBegin() const override;
[[nodiscard]] std::unique_ptr<TxsType::iter_base>
txsEnd() const override;
/** Test whether a transaction exists in the base snapshot's tx-map.
*
* @param key The transaction ID to look up.
* @return `true` if the transaction is present in the base ledger's
* transaction map.
*/
[[nodiscard]] bool
txExists(key_type const& key) const override;
/** Read a transaction and its metadata from the base snapshot's tx-map.
*
* @param key The transaction ID to retrieve.
* @return A pair of `(STTx, STObject metadata)` for the transaction,
* or `{nullptr, nullptr}` if not found.
*/
[[nodiscard]] tx_type
txRead(key_type const& key) const override;
/** @} */
// ApplyView
/** Return the flags governing this transaction apply pass.
*
* @return The `ApplyFlags` bitmask set at construction.
*/
[[nodiscard]] ApplyFlags
flags() const override;
/** Check out a ledger entry for in-place mutation.
*
* Loads the entry into the change buffer on first access (tagged
* `Cache`). Returns the same `shared_ptr` on subsequent calls.
* The returned pointer must later be passed to `update()` or `erase()`
* on **this** view instance to record the intended change.
*
* @param k Keylet identifying the entry.
* @return A mutable handle to the buffered SLE, or `nullptr` if the
* entry does not exist (including if it is pending erasure).
*/
std::shared_ptr<SLE>
peek(Keylet const& k) override;
/** Stage a deletion for a checked-out entry.
*
* Transitions the buffer entry from `Cache` or `Modify` to `Erase`.
* If the entry was inserted within this same transaction, it is removed
* entirely (net-zero effect on the base).
*
* @param sle The exact `shared_ptr` previously returned by `peek()`
* on this view instance.
* @throws std::logic_error If the pointer does not match the buffered
* entry or the entry is already erased.
*/
void
erase(std::shared_ptr<SLE> const& sle) override;
/** Stage a new ledger entry for insertion.
*
* Records the SLE under `Action::Insert`. If the key was previously
* erased within this transaction the action is collapsed to `Modify`.
*
* @param sle The new entry; its key must not already exist in the view.
* @throws std::logic_error If the key already exists with a non-erase
* action in the buffer.
*/
void
insert(std::shared_ptr<SLE> const& sle) override;
/** Promote a checked-out entry to a definitive write.
*
* Transitions the buffer action from `Cache` to `Modify`; `Insert` and
* `Modify` entries are left unchanged (already write-intent).
*
* @param sle The exact `shared_ptr` previously returned by `peek()`
* on this view instance.
* @throws std::logic_error If the pointer does not match, the entry is
* erased, or the key is unknown.
*/
void
update(std::shared_ptr<SLE> const& sle) override;
// RawView
/** Erase a ledger entry without enforcing pointer-identity ownership.
*
* Bypasses the `peek()`-pointer ownership check enforced by `erase()`.
* Used by `RawView` callers (e.g. `Sandbox::apply`) that flush changes
* from another view's table and cannot satisfy the same-instance
* invariant.
*
* @param sle An SLE whose key identifies the object to erase.
* @throws std::logic_error If the object is already pending erasure.
*/
void
rawErase(std::shared_ptr<SLE> const& sle) override;
/** Insert a ledger entry via the same validated path as `insert()`.
*
* Despite being a raw-tier operation, this method calls the same
* `items_.insert()` that the high-level `insert()` uses; the
* distinction is that callers from the `RawView` flush path are not
* required to have obtained the SLE from `peek()`.
*
* @param sle The new entry to stage for insertion.
* @throws std::logic_error If the key already exists with a non-erase
* action.
*/
void
rawInsert(std::shared_ptr<SLE> const& sle) override;
/** Unconditionally overwrite the buffered SLE for an existing key.
*
* Records the SLE under `Action::Modify`, replacing any `Cache` or
* `Insert` entry. Unlike `update()`, does not enforce pointer identity.
*
* @param sle The SLE to store; its key must exist in this view.
* @throws std::logic_error If the key is currently pending erasure.
*/
void
rawReplace(std::shared_ptr<SLE> const& sle) override;
/** Record XRP drops destroyed by fees within this transaction's scope.
*
* Accumulates into the change buffer and is forwarded to the parent
* view's `rawDestroyXRP()` when the buffer is committed.
*
* @param feeDrops The amount of XRP to permanently remove from
* circulation.
*/
void
rawDestroyXRP(XRPAmount const& feeDrops) override;
protected:
/** Per-transaction policy flags set at construction; exposed via `flags()`. */
ApplyFlags flags_;
/** Non-owning pointer to the base ledger snapshot.
*
* All reads that do not need awareness of pending changes are forwarded
* here. The pointed-to view must outlive this object.
*/
ReadView const* base_;
/** Change buffer accumulating per-transaction ledger mutations.
*
* Maps each touched `uint256` key to an `(Action, SLE)` pair. Flushed
* to the parent view atomically on `apply()`; discarded on destruction.
*/
detail::ApplyStateTable items_;
};

View File

@@ -11,27 +11,73 @@
namespace xrpl::detail {
// Helper class that buffers raw modifications
/** In-memory write buffer that accumulates SLE mutations before flushing them
* to a backing `RawView`.
*
* Every mutable ledger view (`OpenView`, and indirectly `ApplyStateTable`)
* embeds a `RawStateTable` as its delta accumulator. The three mutation
* methods — `erase`, `insert`, and `replace` — apply a state-machine
* collapse so the map stays minimal: insert-then-erase cancels out entirely;
* erase-then-insert upgrades to replace; and illegal sequences (double-erase,
* double-insert) throw `std::logic_error`. `read`, `exists`, and `succ`
* overlay the pending delta transparently onto the supplied base `ReadView`,
* so callers always see a coherent merged state. Once a transaction succeeds,
* `apply()` flushes the buffer to the target `RawView` in a single pass.
*
* The `items_` map uses a `boost::container::pmr::monotonic_buffer_resource`
* with a 256 KB initial arena for O(1) amortised allocation during the burst
* of mutations that constitute a single transaction round. Because the
* resource cannot be shared or assigned, copy construction allocates a fresh
* resource and deep-copies the map; move construction transfers the
* `unique_ptr` directly. Both assignment operators are deleted.
*
* XRP fee destruction is tracked separately in `dropsDestroyed_` and
* replayed as a single `rawDestroyXRP` call during `apply()`.
*
* @note This class is an internal implementation detail of `OpenView`.
* Transaction logic should not interact with it directly; use the
* `RawView` interface instead.
* @see OpenView, RawView
*/
class RawStateTable
{
public:
using key_type = ReadView::key_type;
// Initial size for the monotonic_buffer_resource used for allocations
// The size was chosen from the old `qalloc` code (which this replaces).
// It is unclear how the size initially chosen in qalloc.
/** Initial arena size for the PMR monotonic buffer resource.
*
* Inherited from the legacy `qalloc` scheme this replaced. The 256 KB
* budget covers the typical per-transaction working set without triggering
* heap growth for the common case.
*/
static constexpr size_t kINITIAL_BUFFER_SIZE = kilobytes(256);
/** Construct an empty table with a fresh 256 KB monotonic arena. */
RawStateTable()
: monotonic_resource_{std::make_unique<boost::container::pmr::monotonic_buffer_resource>(
kINITIAL_BUFFER_SIZE)}
, items_{monotonic_resource_.get()} {};
/** Copy-construct by allocating a fresh monotonic arena and copying items.
*
* The SLE `shared_ptr` values in `items_` are shared with the source —
* not deep-copied — which is safe because SLEs are immutable once
* published. `dropsDestroyed_` is copied verbatim.
*
* @param rhs The source table to copy.
*/
RawStateTable(RawStateTable const& rhs)
: monotonic_resource_{std::make_unique<boost::container::pmr::monotonic_buffer_resource>(
kINITIAL_BUFFER_SIZE)}
, items_{rhs.items_, monotonic_resource_.get()}
, dropsDestroyed_{rhs.dropsDestroyed_} {};
/** Move-construct by transferring the monotonic resource and items map.
*
* After the move, the source table is left in a valid but empty state.
* The `unique_ptr` transfer preserves the stable address that `items_`'
* `polymorphic_allocator` holds.
*/
RawStateTable(RawStateTable&&) = default;
RawStateTable&
@@ -39,48 +85,166 @@ public:
RawStateTable&
operator=(RawStateTable const&) = delete;
/** Flush all buffered mutations to a backing `RawView`.
*
* First calls `to.rawDestroyXRP(dropsDestroyed_)` to replay accumulated
* fee burns, then iterates `items_` and dispatches each pending action
* to the corresponding `rawErase`, `rawInsert`, or `rawReplace` method.
* The table is not cleared after apply; this object should be discarded
* or destroyed once flushed.
*
* @param to The target `RawView` that receives all buffered mutations.
*/
void
apply(RawView& to) const;
/** Test whether an SLE exists, overlaying the pending delta onto `base`.
*
* Checks the pending buffer first: a pending erase returns `false`; a
* pending insert or replace returns `true` only if `k.check()` passes
* (type-tag validation). Falls through to `base.exists(k)` when the key
* has no pending action.
*
* @param base The underlying read-only ledger state.
* @param k The keylet specifying key and expected SLE type.
* @return `true` if the entry exists and its type satisfies `k.check()`.
*/
[[nodiscard]] bool
exists(ReadView const& base, Keylet const& k) const;
/** Find the smallest key strictly greater than `key` in the merged state.
*
* Runs two parallel searches: (1) walks `base.succ()` repeatedly,
* skipping any base key that has a pending `Action::Erase`; (2) scans
* `items_` forward from `key` for the first non-erase entry. Returns
* the lower of the two candidates. If `last` is given and the result is
* `>= last`, returns `std::nullopt` (half-open range semantics).
*
* @param base The underlying read-only ledger state.
* @param key Exclusive lower bound; the search begins strictly after this.
* @param last Optional exclusive upper bound; `std::nullopt` means unbounded.
* @return The next existing key, or `std::nullopt` if none is in range.
*/
[[nodiscard]] std::optional<key_type>
succ(ReadView const& base, key_type const& key, std::optional<key_type> const& last) const;
/** Stage an SLE deletion, applying state-machine transition rules.
*
* Transitions on the key's existing pending action:
* - None → records `Action::Erase`.
* - `Insert` → removes the entry entirely (net-zero; base is unaffected).
* - `Replace` → downgrades to `Action::Erase`.
* - `Erase` → `LogicError` (double-delete).
*
* @param sle The ledger entry to stage for deletion; key is taken from the SLE.
* @throws std::logic_error if the key already has a pending erase.
*/
void
erase(std::shared_ptr<SLE> const& sle);
/** Stage an SLE creation, applying state-machine transition rules.
*
* Transitions on the key's existing pending action:
* - None → records `Action::Insert`.
* - `Erase` → upgrades to `Action::Replace` (delete-then-recreate in
* the same transaction batch).
* - `Insert` → `LogicError` (duplicate insert).
* - `Replace` → `LogicError` (key already present in the delta).
*
* @param sle The new ledger entry to stage; key is taken from the SLE.
* @throws std::logic_error if the key is already pending insert or replace.
*/
void
insert(std::shared_ptr<SLE> const& sle);
/** Stage an SLE field update, applying state-machine transition rules.
*
* Transitions on the key's existing pending action:
* - None → records `Action::Replace`.
* - `Insert` → updates the stored SLE pointer; preserves `Insert`
* because from the base's perspective the key is still being created.
* - `Replace` → updates the stored SLE pointer.
* - `Erase` → `LogicError` (cannot replace a deleted key).
*
* @param sle The updated ledger entry to stage; key is taken from the SLE.
* @throws std::logic_error if the key has a pending erase.
*/
void
replace(std::shared_ptr<SLE> const& sle);
/** Read an SLE, overlaying the pending delta onto `base`.
*
* Checks the buffer first: a pending erase returns `nullptr`; a pending
* insert or replace returns the buffered SLE if `k.check()` passes
* (guards against type mismatches at the same key). Falls through to
* `base.read(k)` when the key has no pending action.
*
* @param base The underlying read-only ledger state.
* @param k The keylet specifying key and expected SLE type.
* @return The SLE if it exists and the type matches, otherwise `nullptr`.
*/
[[nodiscard]] std::shared_ptr<SLE const>
read(ReadView const& base, Keylet const& k) const;
/** Accumulate XRP drops to destroy at `apply()` time.
*
* Drops are not forwarded individually; they accumulate in
* `dropsDestroyed_` and are replayed as a single `rawDestroyXRP` call in
* `apply()`, keeping fee-burn accounting atomic with the rest of the flush.
*
* @param fee The quantity of XRP drops to add to the accumulated burn total.
*/
void
destroyXRP(XRPAmount const& fee);
/** Return a begin iterator for the merged SLE range over `base` and the delta.
*
* The returned iterator implements the two-pointer merge defined by
* `SlesIterImpl`: pending inserts appear in sorted position, pending
* erases are hidden, and pending replaces shadow the base entry.
*
* @param base The underlying read-only ledger state to merge with.
* @return A heap-allocated `iter_base` positioned at the first merged SLE.
*/
[[nodiscard]] std::unique_ptr<ReadView::SlesType::iter_base>
slesBegin(ReadView const& base) const;
/** Return an end sentinel for the merged SLE range over `base` and the delta.
*
* @param base The underlying read-only ledger state to merge with.
* @return A heap-allocated `iter_base` positioned past the last merged SLE.
*/
[[nodiscard]] std::unique_ptr<ReadView::SlesType::iter_base>
slesEnd(ReadView const& base) const;
/** Return an iterator to the first merged SLE with key strictly greater
* than `key`.
*
* @param base The underlying read-only ledger state to merge with.
* @param key Exclusive lower bound for the search.
* @return A heap-allocated `iter_base` positioned at the first qualifying SLE.
*/
[[nodiscard]] std::unique_ptr<ReadView::SlesType::iter_base>
slesUpperBound(ReadView const& base, uint256 const& key) const;
private:
/** Pending mutation kind for an entry in `items_`. */
enum class Action {
Erase,
Insert,
Replace,
Erase, /**< Entry is scheduled for deletion. */
Insert, /**< Entry is being created; does not yet exist in the base. */
Replace, /**< Entry exists in the base and has been modified. */
};
/** Private iterator class that merges base-view SLEs with the pending
* delta; defined in the `.cpp`. */
class SlesIterImpl;
/** Pairs a pending `Action` with the SLE it acts on.
*
* Stored as the mapped value in `items_`. The SLE pointer is always
* non-null; for `Erase` it is the last version written before the
* deletion was staged (used by `RawView::rawErase`).
*/
struct SleAction
{
Action action;
@@ -99,11 +263,17 @@ private:
SleAction,
std::less<key_type>,
boost::container::pmr::polymorphic_allocator<std::pair<key_type const, SleAction>>>;
// monotonic_resource_ must outlive `items_`. Make a pointer so it may be
// easily moved.
std::unique_ptr<boost::container::pmr::monotonic_buffer_resource> monotonic_resource_;
/** Ordered map from ledger key to pending mutation; backed by the
* monotonic arena for O(1) amortised node allocation. */
items_t items_;
/** Accumulated XRP drops burned by fees; replayed as one `rawDestroyXRP`
* call during `apply()`. */
XRPAmount dropsDestroyed_{0};
};

View File

@@ -1,3 +1,13 @@
/** @file
* Type-erased forward-iterator infrastructure for `ReadView` traversal.
*
* Defines `ReadViewFwdIter` (the abstract iterator interface) and
* `ReadViewFwdRange` (the STL-compatible range wrapper) that together let
* any `ReadView` subclass expose its state and transaction maps through a
* single, stable iterator type. Callers interact indirectly via
* `ReadView::sles` and `ReadView::txs`; this header is internal plumbing.
*/
#pragma once
#include <cstddef>
@@ -10,8 +20,18 @@ class ReadView;
namespace detail {
// A type-erased ForwardIterator
//
/** Abstract base defining the four primitive operations of a type-erased forward iterator.
*
* Each concrete `ReadView` implementation provides a private subclass of
* this template and hands heap-allocated instances to `ReadViewFwdRange::Iterator`
* via the factory methods `slesBegin()`, `slesEnd()`, `slesUpperBound()`,
* `txsBegin()`, and `txsEnd()` on `ReadView`. Callers never interact with
* this class directly.
*
* @tparam ValueType The element type yielded by the iterator —
* `std::shared_ptr<SLE const>` for state-map iteration or
* `ReadView::tx_type` for transaction-map iteration.
*/
template <class ValueType>
class ReadViewFwdIter
{
@@ -27,21 +47,57 @@ public:
virtual ~ReadViewFwdIter() = default;
/** Returns a heap-allocated deep copy of this iterator.
*
* Provides value-semantics copy for the owning `unique_ptr` wrapper.
* Each concrete subclass must return a new instance of itself in the
* same position.
*
* @return A `unique_ptr` to a fresh copy of this iterator instance.
*/
[[nodiscard]] virtual std::unique_ptr<ReadViewFwdIter>
copy() const = 0;
/** Returns `true` if this iterator denotes the same position as @p impl.
*
* Both iterators must be over the same underlying view; mixing iterators
* from different views produces undefined behavior.
*
* @param impl The other iterator to compare against.
* @return `true` when both iterators point to the same element (or both
* are end sentinels).
*/
[[nodiscard]] virtual bool
equal(ReadViewFwdIter const& impl) const = 0;
/** Advances this iterator to the next element in the sequence. */
virtual void
increment() = 0;
/** Returns the element at the current iterator position.
*
* @return The current `ValueType` value. The result is cached by the
* wrapping `Iterator` so repeated dereferences are inexpensive.
* @throw May throw if the underlying view operation fails.
*/
[[nodiscard]] virtual value_type
dereference() const = 0;
};
// A range using type-erased ForwardIterator
//
/** STL-compatible forward range backed by a type-erased iterator.
*
* Wraps a `ReadViewFwdIter<ValueType>` behind a regular value-type iterator
* so that callers can write range-for loops over any `ReadView` subclass
* without knowing the concrete iterator type. Virtual dispatch is hidden
* inside the `impl_` pointer; the public `Iterator` API is fully inlined.
*
* `ReadView::SlesType` and `ReadView::TxsType` inherit from this template;
* application code should use those types rather than instantiating
* `ReadViewFwdRange` directly.
*
* @tparam ValueType The element type — must be noexcept-move-constructible
* so that `Iterator` move operations are noexcept.
*/
template <class ValueType>
class ReadViewFwdRange
{
@@ -53,6 +109,18 @@ public:
"ReadViewFwdRange move and move assign constructors should be "
"noexcept");
/** STL forward iterator over a `ReadViewFwdRange`.
*
* Value-type wrapper around a heap-allocated `iter_base`. Copy uses
* `iter_base::copy()` for a polymorphic deep clone; move transfers
* ownership of the `unique_ptr` without allocation and is `noexcept`.
* Dereference results are cached in `cache_` and cleared on advance,
* amortizing the cost of repeated `*it` or `it->` calls in tight loops.
*
* @note Comparing iterators from different views triggers an
* `XRPL_ASSERT` in debug builds. The `view_` pointer is carried
* solely for this cross-view sanity check.
*/
class Iterator
{
public:
@@ -66,43 +134,127 @@ public:
using iterator_category = std::forward_iterator_tag;
/** Constructs a singular (default) iterator.
*
* A default-constructed iterator is not dereferenceable and must
* not be incremented. It compares equal only to other
* default-constructed iterators.
*/
Iterator() = default;
/** Copy-constructs an independent iterator at the same position.
*
* Calls `iter_base::copy()` to deep-clone the polymorphic
* implementation, producing a new iterator that advances
* independently of @p other.
*
* @param other The iterator to clone.
*/
Iterator(Iterator const& other);
/** Move-constructs an iterator, transferring ownership of the impl.
*
* @param other The iterator to move from; left in a valid but
* singular state.
*/
Iterator(Iterator&& other) noexcept;
// Used by the implementation
/** Constructs an iterator from a raw view pointer and a polymorphic impl.
*
* Used exclusively by `ReadView`'s factory methods (`slesBegin()`,
* `slesEnd()`, etc.). Not intended for direct use by callers.
*
* @param view The owning view; stored only for cross-view assertion.
* @param impl The heap-allocated concrete iterator; ownership is
* transferred to this object.
*/
explicit Iterator(ReadView const* view, std::unique_ptr<iter_base> impl);
/** Copy-assigns from another iterator at the same position.
*
* Deep-clones via `iter_base::copy()`.
*
* @param other The iterator to copy.
* @return `*this`.
*/
Iterator&
operator=(Iterator const& other);
/** Move-assigns from another iterator.
*
* @param other The iterator to move from; left in a valid but
* singular state.
* @return `*this`.
*/
Iterator&
operator=(Iterator&& other) noexcept;
/** Returns `true` if both iterators denote the same position.
*
* Delegates to `iter_base::equal()`. Two null `impl_` pointers also
* compare equal (both are end sentinels / default-constructed).
*
* @param other The iterator to compare against.
* @return `true` when both iterators are at the same element.
* @note Asserts in debug builds that both iterators belong to the
* same view. Comparing iterators from different views is
* undefined behaviour.
*/
bool
operator==(Iterator const& other) const;
/** Returns `true` if the iterators denote different positions.
*
* @param other The iterator to compare against.
* @return `true` when the iterators are not at the same element.
*/
bool
operator!=(Iterator const& other) const;
/** Returns a reference to the current element.
*
* The result is cached after the first call; subsequent calls before
* the next `operator++` return the cached value at no extra cost.
*
* @return A `const` reference to the current `ValueType`.
* @throw May throw if the underlying `iter_base::dereference()` call fails.
*/
// Can throw
reference
operator*() const;
/** Returns a pointer to the current element.
*
* Delegates to `operator*()` so caching and exception behaviour are
* identical to that of the dereference operator.
*
* @return A `const` pointer to the current `ValueType`.
* @throw May throw if the underlying `iter_base::dereference()` call fails.
*/
// Can throw
pointer
operator->() const;
/** Advances the iterator and clears the dereference cache.
*
* @return `*this` after advancing to the next element.
*/
Iterator&
operator++();
/** Returns a copy of the current iterator, then advances.
*
* @return An iterator to the element before the advance.
*/
Iterator
operator++(int);
private:
/** Owning view; compared in `operator==` to catch cross-view misuse. */
ReadView const* view_ = nullptr;
/** Heap-allocated polymorphic iterator; null for the end sentinel. */
std::unique_ptr<iter_base> impl_{};
/** One-slot dereference cache; cleared on each advance. */
std::optional<value_type> mutable cache_;
};
@@ -118,11 +270,19 @@ public:
ReadViewFwdRange&
operator=(ReadViewFwdRange const&) = default;
/** Constructs a range bound to @p view.
*
* The range stores a raw pointer to the view. The view must outlive
* the range and any iterators derived from it.
*
* @param view The `ReadView` whose factory methods supply iterators.
*/
explicit ReadViewFwdRange(ReadView const& view) : view_(&view)
{
}
protected:
/** The view whose factory methods supply concrete `iter_base` instances. */
ReadView const* view_;
};

View File

@@ -1,3 +1,22 @@
/** @file
* Mathematical and operational backbone of the XRPL Automated Market Maker.
*
* Provides every computation needed to run a constant-product AMM pool:
* LP token minting and burning (XLS-30d Equations 3, 4, 7, 8), spot-price
* quality alignment against the central limit order book, swap execution with
* rigorous directional rounding, and ledger-state helpers for pool balance
* queries and AMM account lifecycle management.
*
* All arithmetic observes the pool invariant:
* @code
* sqrt(poolAsset1 × poolAsset2) >= LPTokenBalance
* @endcode
* Rounding is always directed to keep the pool at least as large as required.
* The `fixAMMv1_1` amendment introduced per-step directional rounding for
* swaps; `fixAMMv1_3` extended this discipline to LP token and
* deposit/withdrawal formulas. Pre-amendment paths are preserved for
* historic ledger replay.
*/
#pragma once
#include <xrpl/basics/Expected.h>
@@ -22,6 +41,17 @@ namespace xrpl {
namespace detail {
/** Scale @p amount down by 99.99% as a last-resort quality rescue.
*
* When the rounded offer from `getAMMOfferStartWithTakerGets` or
* `getAMMOfferStartWithTakerPays` still falls below the target quality due
* to XRP integer-drop discretization, this function shrinks it by 0.01%
* (rounding toward zero) so the resulting offer quality meets or exceeds
* the target without generating an implausibly small trade.
*
* @param amount The offer side (takerGets or takerPays) to reduce.
* @return The reduced amount, or zero if already at zero.
*/
Number
reduceOffer(auto const& amount)
{
@@ -34,22 +64,41 @@ reduceOffer(auto const& amount)
} // namespace detail
/** Direction tag used throughout deposit/withdrawal and rounding helpers.
*
* Passed to functions that behave asymmetrically between deposit (LP tokens
* rounded down, assets rounded up) and withdrawal (LP tokens rounded up,
* assets rounded down) to preserve the pool invariant.
*/
enum class IsDeposit : bool { No = false, Yes = true };
/** Calculate LP Tokens given AMM pool reserves.
* @param asset1 AMM one side of the pool reserve
* @param asset2 AMM another side of the pool reserve
* @return LP Tokens as IOU
/** Compute the initial LP token supply for a newly seeded AMM pool.
*
* Uses the geometric mean `sqrt(asset1 × asset2)`, which sets the
* pool invariant to equality at creation: `sqrt(asset1 × asset2) == LPTokens`.
* Under `fixAMMv1_3` the result is rounded downward so the pool starts
* with a slight surplus, preserving the invariant.
*
* @param asset1 Balance of the first pool asset.
* @param asset2 Balance of the second pool asset.
* @param lptIssue Asset descriptor identifying the LP token currency/issuer.
* @return Initial LP token amount as an IOU `STAmount`.
*/
STAmount
ammLPTokens(STAmount const& asset1, STAmount const& asset2, Asset const& lptIssue);
/** Calculate LP Tokens given asset's deposit amount.
* @param asset1Balance current AMM asset1 balance
* @param asset1Deposit requested asset1 deposit amount
* @param lptAMMBalance AMM LPT balance
* @param tfee trading fee in basis points
* @return tokens
/** LP tokens minted for a single-asset deposit (XLS-30d Equation 3).
*
* A single-sided deposit is economically equivalent to a proportional
* deposit plus a fee-bearing swap; the fee is embedded via `feeMult` and
* `feeMultHalf`. Under `fixAMMv1_3` the final multiplication is rounded
* downward so fewer tokens are issued, preserving the pool invariant.
*
* @param asset1Balance Current pool balance of the asset being deposited.
* @param asset1Deposit Amount being deposited.
* @param lptAMMBalance Current total LP token supply.
* @param tfee Trading fee in basis points (e.g. 1000 = 1%).
* @return LP tokens to mint for the depositor.
*/
STAmount
lpTokensOut(
@@ -58,12 +107,19 @@ lpTokensOut(
STAmount const& lptAMMBalance,
std::uint16_t tfee);
/** Calculate asset deposit given LP Tokens.
* @param asset1Balance current AMM asset1 balance
* @param lpTokens LP Tokens
* @param lptAMMBalance AMM LPT balance
* @param tfee trading fee in basis points
* @return
/** Asset deposit required to receive a given number of LP tokens (XLS-30d Equation 4).
*
* Inverse of `lpTokensOut`: solves Equation 3 for the deposit amount given a
* desired token output. The solution is a quadratic whose positive root is
* found via `solveQuadraticEq`. Under `fixAMMv1_3` the result is rounded
* upward so the depositor contributes slightly more, preserving the pool
* invariant.
*
* @param asset1Balance Current pool balance of the asset to deposit.
* @param lptAMMBalance Current total LP token supply.
* @param lpTokens Desired LP token amount.
* @param tfee Trading fee in basis points.
* @return Asset amount the depositor must contribute.
*/
STAmount
ammAssetIn(
@@ -72,13 +128,18 @@ ammAssetIn(
STAmount const& lpTokens,
std::uint16_t tfee);
/** Calculate LP Tokens given asset's withdraw amount. Return 0
* if can't calculate.
* @param asset1Balance current AMM asset1 balance
* @param asset1Withdraw requested asset1 withdraw amount
* @param lptAMMBalance AMM LPT balance
* @param tfee trading fee in basis points
* @return tokens out amount
/** LP tokens to burn for a single-asset withdrawal (XLS-30d Equation 7).
*
* Computes how many LP tokens must be redeemed to withdraw a specified asset
* amount. Returns zero if the inputs make calculation impossible. Under
* `fixAMMv1_3` the final multiplication is rounded upward so more tokens must
* be burned, preserving the pool invariant.
*
* @param asset1Balance Current pool balance of the asset being withdrawn.
* @param asset1Withdraw Requested withdrawal amount.
* @param lptAMMBalance Current total LP token supply.
* @param tfee Trading fee in basis points.
* @return LP tokens the withdrawer must burn, or zero if the calculation fails.
*/
STAmount
lpTokensIn(
@@ -87,12 +148,18 @@ lpTokensIn(
STAmount const& lptAMMBalance,
std::uint16_t tfee);
/** Calculate asset withdrawal by tokens
* @param assetBalance balance of the asset being withdrawn
* @param lptAMMBalance total AMM Tokens balance
* @param lpTokens LP Tokens balance
* @param tfee trading fee in basis points
* @return calculated asset amount
/** Asset returned when burning a given number of LP tokens (XLS-30d Equation 8).
*
* Inverse of `lpTokensIn`: solves Equation 7 for the withdrawal amount given
* the token burn. Under `fixAMMv1_3` the final multiplication is rounded
* downward so the withdrawer receives slightly less, preserving the pool
* invariant.
*
* @param assetBalance Current pool balance of the asset to withdraw.
* @param lptAMMBalance Current total LP token supply.
* @param lpTokens LP tokens being burned.
* @param tfee Trading fee in basis points.
* @return Asset amount returned to the withdrawer.
*/
STAmount
ammAssetOut(
@@ -101,12 +168,19 @@ ammAssetOut(
STAmount const& lpTokens,
std::uint16_t tfee);
/** Check if the relative distance between the qualities
* is within the requested distance.
* @param calcQuality calculated quality
* @param reqQuality requested quality
* @param dist requested relative distance
* @return true if within dist, false otherwise
/** Check whether two `Quality` values are within a relative tolerance.
*
* `Quality` has no subtraction operator, so the comparison is performed via
* `Quality::rate()`, which returns the *inverse* of quality (output/input).
* The formula `(min.rate - max.rate) / min.rate < dist` is equivalent to
* the standard `(max - min) / max < dist` after accounting for the inversion.
* Used in `changeSpotPriceQuality` to suppress trace-level errors when the
* quality mismatch is within one part in ten million (1e-7).
*
* @param calcQuality Computed quality.
* @param reqQuality Target quality.
* @param dist Maximum acceptable relative distance (e.g. `Number(1, -7)`).
* @return `true` if the two qualities are within @p dist of each other.
*/
inline bool
withinRelativeDistance(Quality const& calcQuality, Quality const& reqQuality, Number const& dist)
@@ -120,12 +194,18 @@ withinRelativeDistance(Quality const& calcQuality, Quality const& reqQuality, Nu
return ((min.rate() - max.rate()) / min.rate()) < dist;
}
/** Check if the relative distance between the amounts
* is within the requested distance.
* @param calc calculated amount
* @param req requested amount
* @param dist requested relative distance
* @return true if within dist, false otherwise
/** Check whether two numeric amounts are within a relative tolerance.
*
* Computes `(max - min) / max` and tests that it is less than @p dist.
* Accepted for `STAmount`, `IOUAmount`, `XRPAmount`, `MPTAmount`, and
* `Number`. Used alongside the `Quality` overload to emit quality-mismatch
* errors only when the discrepancy is truly significant.
*
* @tparam Amt Amount type; constrained to the five types listed above.
* @param calc Computed amount.
* @param req Target amount.
* @param dist Maximum acceptable relative distance.
* @return `true` if the two amounts are within @p dist of each other.
*/
template <typename Amt>
requires(
@@ -141,34 +221,49 @@ withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist)
return ((max - min) / max) < dist;
}
/** Solve quadratic equation to find takerGets or takerPays. Round
* to minimize the amount in order to maximize the quality.
/** Smallest positive root of `a·x² + b·x + c = 0`, used to minimize offer size.
*
* Uses the numerically stable "citardauq" formula (Blinn 2006): when `b > 0`
* it computes `2c / (-b - sqrt(d))` instead of the standard
* `(-b + sqrt(d)) / 2a`, avoiding catastrophic cancellation when the two
* terms in the numerator are nearly equal. Minimizing the root maximizes
* offer quality in `getAMMOfferStartWithTakerGets` / `getAMMOfferStartWithTakerPays`.
*
* @param a Quadratic coefficient.
* @param b Linear coefficient.
* @param c Constant term.
* @return The smallest positive root, or `std::nullopt` if the discriminant
* is negative (no real solution) or the root is non-positive.
*/
std::optional<Number>
solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c);
/** Generate AMM offer starting with takerGets when AMM pool
* from the payment perspective is IOU(in)/XRP(out)
* Equations:
* Spot Price Quality after the offer is consumed:
* Qsp = (O - o) / (I + i) -- equation (1)
* where O is poolPays, I is poolGets, o is takerGets, i is takerPays
* Swap out:
* i = (I * o) / (O - o) * f -- equation (2)
* where f is (1 - tfee/100000), tfee is in basis points
* Effective price targetQuality:
* Qep = o / i -- equation (3)
* There are two scenarios to consider
* A) Qsp = Qep. Substitute i in (1) with (2) and solve for o
* and Qsp = targetQuality(Qt):
* o**2 + o * (I * Qt * (1 - 1 / f) - 2 * O) + O**2 - Qt * I * O = 0
* B) Qep = Qsp. Substitute i in (3) with (2) and solve for o
* and Qep = targetQuality(Qt):
* o = O - I * Qt / f
* Since the scenario is not known a priori, both A and B are solved and
* the lowest value of o is takerGets. takerPays is calculated with
* swap out eq (2). If o is less or equal to 0 then the offer can't
* be generated.
/** Generate a synthetic AMM offer whose quality matches @p targetQuality,
* starting from takerGets (XRP out, IOU in).
*
* Used when the pool pays XRP (IOU-in / XRP-out). Starting from the XRP
* side ensures that rounding XRP down to integer drops improves rather than
* degrades offer quality (post-`fixAMMv1_1` behavior).
*
* Two binding constraints are solved and the smaller takerGets is chosen:
* - Scenario A — post-swap spot price equals @p targetQuality:
* `o² + o·(I·Qt·(1 - 1/f) - 2·O) + O² - Qt·I·O = 0`
* - Scenario B — effective offer price equals @p targetQuality:
* `o = O - I·Qt / f`
*
* where `O = poolPays`, `I = poolGets`, `f = feeMult(tfee)`.
* takerPays is then derived from the swap-out equation. If the resulting
* offer quality is still below @p targetQuality after rounding, a 99.99%
* rescale via `detail::reduceOffer` is attempted.
*
* @tparam TIn Asset type flowing into the pool (IOU side).
* @tparam TOut Asset type flowing out of the pool (XRP side).
* @param pool Current AMM pool balances (`in` = poolGets, `out` = poolPays).
* @param targetQuality Desired offer quality (CLOB best quality).
* @param tfee Trading fee in basis points.
* @return Seated `{takerPays, takerGets}` amounts, or `std::nullopt` if a
* valid offer cannot be generated (e.g. target quality unreachable at
* current fee).
*/
template <typename TIn, typename TOut>
std::optional<TAmounts<TIn, TOut>>
@@ -214,28 +309,30 @@ getAMMOfferStartWithTakerGets(
return amounts;
}
/** Generate AMM offer starting with takerPays when AMM pool
* from the payment perspective is XRP(in)/IOU(out) or IOU(in)/IOU(out).
* Equations:
* Spot Price Quality after the offer is consumed:
* Qsp = (O - o) / (I + i) -- equation (1)
* where O is poolPays, I is poolGets, o is takerGets, i is takerPays
* Swap in:
* o = (O * i * f) / (I + i * f) -- equation (2)
* where f is (1 - tfee/100000), tfee is in basis points
* Effective price quality:
* Qep = o / i -- equation (3)
* There are two scenarios to consider
* A) Qsp = Qep. Substitute o in (1) with (2) and solve for i
* and Qsp = targetQuality(Qt):
* i**2 * f + i * I * (1 + f) + I**2 - I * O / Qt = 0
* B) Qep = Qsp. Substitute i in (3) with (2) and solve for i
* and Qep = targetQuality(Qt):
* i = O / Qt - I / f
* Since the scenario is not known a priori, both A and B are solved and
* the lowest value of i is takerPays. takerGets is calculated with
* swap in eq (2). If i is less or equal to 0 then the offer can't
* be generated.
/** Generate a synthetic AMM offer whose quality matches @p targetQuality,
* starting from takerPays (XRP in, or IOU/IOU).
*
* Used for XRP-in/IOU-out and IOU/IOU pools. Starting from the XRP
* side (takerPays) under `fixAMMv1_1` keeps rounding effects favorable.
*
* Two binding constraints are solved and the smaller takerPays is chosen:
* - Scenario A — post-swap spot price equals @p targetQuality:
* `i²·f + i·I·(1+f) + I² - I·O/Qt = 0`
* - Scenario B — effective offer price equals @p targetQuality:
* `i = O/Qt - I/f`
*
* where `O = poolPays`, `I = poolGets`, `f = feeMult(tfee)`.
* takerGets is then derived from the swap-in equation. If the resulting
* offer quality is still below @p targetQuality after rounding, a 99.99%
* rescale via `detail::reduceOffer` is attempted.
*
* @tparam TIn Asset type flowing into the pool.
* @tparam TOut Asset type flowing out of the pool.
* @param pool Current AMM pool balances (`in` = poolGets, `out` = poolPays).
* @param targetQuality Desired offer quality (CLOB best quality).
* @param tfee Trading fee in basis points.
* @return Seated `{takerPays, takerGets}` amounts, or `std::nullopt` if a
* valid offer cannot be generated.
*/
template <typename TIn, typename TOut>
std::optional<TAmounts<TIn, TOut>>
@@ -281,21 +378,34 @@ getAMMOfferStartWithTakerPays(
return amounts;
}
/** Generate AMM offer so that either updated Spot Price Quality (SPQ)
* is equal to LOB quality (in this case AMM offer quality is
* better than LOB quality) or AMM offer is equal to LOB quality
* (in this case SPQ is better than LOB quality).
* Pre-amendment code calculates takerPays first. If takerGets is XRP,
* it is rounded down, which results in worse offer quality than
* LOB quality, and the offer might fail to generate.
* Post-amendment code calculates the XRP offer side first. The result
* is rounded down, which makes the offer quality better.
* It might not be possible to match either SPQ or AMM offer to LOB
* quality. This generally happens at higher fees.
* @param pool AMM pool balances
* @param quality requested quality
* @param tfee trading fee in basis points
* @return seated in/out amounts if the quality can be changed
/** Generate a synthetic AMM offer that aligns the pool's spot price with a CLOB quality.
*
* The payment engine calls this when it encounters both AMM pools and order
* book offers for the same currency pair. The resulting offer has a quality
* such that either the post-swap spot price equals @p quality (AMM offer
* quality is better) or the offer's effective price equals @p quality (the
* post-swap spot price is better) — whichever produces the smaller offer.
*
* Amendment behavior:
* - Pre-`fixAMMv1_1`: always solves for takerPays first; rounding down XRP
* takerGets can push quality below target, causing the offer to be rejected.
* - Post-`fixAMMv1_1`: solves for the XRP side first (takerGets when pool pays
* XRP, takerPays otherwise) so XRP rounding improves rather than degrades
* quality. Falls back to `detail::reduceOffer` if quality is still below
* target after rounding.
*
* A quality mismatch larger than 1e-7 is logged at `j.error()` level; smaller
* mismatches are trace-only.
*
* @tparam TIn Asset type flowing into the pool.
* @tparam TOut Asset type flowing out of the pool.
* @param pool Current AMM pool balances.
* @param quality Target quality (best CLOB offer quality for this pair).
* @param tfee Trading fee in basis points.
* @param rules Current ledger rules (for amendment checks).
* @param j Journal for diagnostic logging.
* @return Seated `{takerPays, takerGets}` amounts, or `std::nullopt` if the
* quality cannot be achieved (generally at high fees).
*/
template <typename TIn, typename TOut>
std::optional<TAmounts<TIn, TOut>>
@@ -398,26 +508,26 @@ changeSpotPriceQuality(
return amounts;
}
/** AMM pool invariant - the product (A * B) after swap in/out has to remain
* at least the same: (A + in) * (B - out) >= A * B
* XRP round-off may result in a smaller product after swap in/out.
* To address this:
* - if on swapIn the out is XRP then the amount is round-off
* downward, making the product slightly larger since out
* value is reduced.
* - if on swapOut the in is XRP then the amount is round-off
* upward, making the product slightly larger since in
* value is increased.
*/
// --- Swap-in / Swap-out ---
/** Swap assetIn into the pool and swap out a proportional amount
* of the other asset. Implements AMM Swap in.
* @see [XLS30d:AMM
* Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78)
* @param pool current AMM pool balances
* @param assetIn amount to swap in
* @param tfee trading fee in basis points
* @return
/** Deposit @p assetIn into the pool and receive a proportional amount of the
* other asset (AMM Swap in, XLS-30d).
*
* Formula: `out = pool.out - (pool.in × pool.out) / (pool.in + assetIn × feeMult(tfee))`
*
* Pool invariant: `(pool.in + assetIn) × (pool.out - out) >= pool.in × pool.out`.
* XRP integer rounding can violate this; post-`fixAMMv1_1` each sub-expression
* has an explicitly directed rounding mode so the pool retains a tiny surplus.
* The output is always rounded downward so the trader receives less, not more.
*
* @tparam TIn Asset type deposited (poolGets side).
* @tparam TOut Asset type received (poolPays side).
* @param pool Current AMM pool balances.
* @param assetIn Amount being deposited into the pool.
* @param tfee Trading fee in basis points.
* @return Amount of the output asset the trader receives; zero if the pool
* denominator is non-positive.
* @see [XLS-30d AMM Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78)
*/
template <typename TIn, typename TOut>
TOut
@@ -476,14 +586,23 @@ swapAssetIn(TAmounts<TIn, TOut> const& pool, TIn const& assetIn, std::uint16_t t
Number::RoundingMode::Downward);
}
/** Swap assetOut out of the pool and swap in a proportional amount
* of the other asset. Implements AMM Swap out.
* @see [XLS30d:AMM
* Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78)
* @param pool current AMM pool balances
* @param assetOut amount to swap out
* @param tfee trading fee in basis points
* @return
/** Withdraw @p assetOut from the pool and compute the required input asset (AMM Swap out, XLS-30d).
*
* Formula: `in = ((pool.in × pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee)`
*
* The input is always rounded upward so the trader pays at least what the
* pool needs to maintain its invariant. Post-`fixAMMv1_1` each intermediate
* step is individually directed; if the pool denominator is non-positive (i.e.
* @p assetOut >= the entire pool), the maximum representable `TIn` is returned.
*
* @tparam TIn Asset type deposited (poolGets side).
* @tparam TOut Asset type withdrawn (poolPays side).
* @param pool Current AMM pool balances.
* @param assetOut Amount being withdrawn from the pool.
* @param tfee Trading fee in basis points.
* @return Amount of the input asset the trader must pay; `toMaxAmount<TIn>`
* if the requested output would exhaust the pool.
* @see [XLS-30d AMM Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78)
*/
template <typename TIn, typename TOut>
TIn
@@ -542,35 +661,46 @@ swapAssetOut(TAmounts<TIn, TOut> const& pool, TOut const& assetOut, std::uint16_
Number::RoundingMode::Upward);
}
/** Return square of n.
*/
/** Return `n²`. */
Number
square(Number const& n);
/** Adjust LP tokens to deposit/withdraw.
* Amount type keeps 16 digits. Maintaining the LP balance by adding
* deposited tokens or subtracting withdrawn LP tokens from LP balance
* results in losing precision in LP balance. I.e. the resulting LP balance
* is less than the actual sum of LP tokens. To adjust for this, subtract
* old tokens balance from the new one for deposit or vice versa for
* withdraw to cancel out the precision loss.
* @param lptAMMBalance LPT AMM Balance
* @param lpTokens LP tokens to deposit or withdraw
* @param isDeposit Yes if deposit, No if withdraw
/** Adjust LP tokens to account for 16-digit precision loss in the running balance.
*
* Adding newly-minted tokens to an already-large `lptAMMBalance` can lose
* significance in the least-significant digit: the stored balance advances
* by less than `lpTokens`. This function round-trips through the 16-digit
* representation by computing `(balance + tokens) - balance` (deposit) or
* `(tokens - balance) + balance` (withdraw), returning the value that will
* actually be committed to the ledger. Result is forced downward to ensure
* the adjusted tokens do not exceed the requested tokens.
*
* @param lptAMMBalance Current total LP token supply stored on the AMM SLE.
* @param lpTokens Tokens being minted or burned.
* @param isDeposit `IsDeposit::Yes` for deposit, `IsDeposit::No` for withdrawal.
* @return Adjusted token amount that exactly matches the representable delta
* in the 16-digit balance.
*/
STAmount
adjustLPTokens(STAmount const& lptAMMBalance, STAmount const& lpTokens, IsDeposit isDeposit);
/** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if
* the adjusted LP tokens are less than the provided LP tokens.
* @param amountBalance asset1 pool balance
* @param amount asset1 to deposit or withdraw
* @param amount2 asset2 to deposit or withdraw
* @param lptAMMBalance LPT AMM Balance
* @param lpTokens LP tokens to deposit or withdraw
* @param tfee trading fee in basis points
* @param isDeposit Yes if deposit, No if withdraw
* @return
/** Adjust deposit/withdrawal asset amounts to match the precision-corrected LP token count.
*
* Calls `adjustLPTokens()` to compute the representable token delta. If the
* adjusted count is less than @p lpTokens, the corresponding asset amounts are
* scaled down so the ledger does not grant assets that exceed what the LP token
* math supports. A no-op when `fixAMMv1_3` is active because `getRoundedLPTokens`
* already incorporates the precision adjustment.
*
* @param amountBalance Current pool balance of the primary asset.
* @param amount Primary asset amount to deposit or withdraw.
* @param amount2 Secondary asset amount for two-sided operations; `std::nullopt`
* for single-asset operations.
* @param lptAMMBalance Current total LP token supply.
* @param lpTokens Calculated LP tokens before precision adjustment.
* @param tfee Trading fee in basis points.
* @param isDeposit `IsDeposit::Yes` for deposit, `IsDeposit::No` for withdrawal.
* @return Tuple of `(adjustedAmount, adjustedAmount2, adjustedLPTokens)`.
*/
std::tuple<STAmount, std::optional<STAmount>, STAmount>
adjustAmountsByLPTokens(
@@ -582,17 +712,46 @@ adjustAmountsByLPTokens(
std::uint16_t tfee,
IsDeposit isDeposit);
/** Positive solution for quadratic equation:
* x = (-b + sqrt(b**2 + 4*a*c))/(2*a)
/** Positive root of `a·x² + b·x + c = 0` using the standard formula.
*
* Computes `x = (-b + sqrt(b² - 4·a·c)) / (2·a)`. Used by `ammAssetIn`
* to invert Equation 4; the discriminant is guaranteed non-negative by the
* deposit formula's domain.
*
* @param a Quadratic coefficient.
* @param b Linear coefficient.
* @param c Constant term.
* @return The positive root.
*/
Number
solveQuadraticEq(Number const& a, Number const& b, Number const& c);
/** Multiply @p amount by @p frac with an explicitly directed rounding mode.
*
* Installs @p rm for both the `Number` multiplication and the subsequent
* `toSTAmount` conversion so that rounding is applied once at the final step,
* not accumulated through intermediates. This is the building block for all
* `fixAMMv1_3` directional-rounding paths.
*
* @param amount Base `STAmount` to scale.
* @param frac Scaling factor.
* @param rm Rounding mode to apply at the final conversion step.
* @return `amount × frac` rounded according to @p rm, expressed in the same
* asset as @p amount.
*/
STAmount
multiply(STAmount const& amount, Number const& frac, Number::RoundingMode rm);
namespace detail {
/** Select the LP token rounding direction that preserves the pool invariant.
*
* Deposit: round downward (fewer tokens minted → pool worth more per token).
* Withdraw: round upward (more tokens burned → pool retains slightly more).
*
* @param isDeposit Direction of the operation.
* @return `Downward` for deposit, `Upward` for withdrawal.
*/
inline Number::RoundingMode
getLPTokenRounding(IsDeposit isDeposit)
{
@@ -602,6 +761,14 @@ getLPTokenRounding(IsDeposit isDeposit)
: Number::RoundingMode::Upward;
}
/** Select the asset rounding direction that preserves the pool invariant.
*
* Deposit: round upward (depositor pays slightly more → pool is larger).
* Withdraw: round downward (withdrawer receives slightly less → pool retains).
*
* @param isDeposit Direction of the operation.
* @return `Upward` for deposit, `Downward` for withdrawal.
*/
inline Number::RoundingMode
getAssetRounding(IsDeposit isDeposit)
{
@@ -613,10 +780,19 @@ getAssetRounding(IsDeposit isDeposit)
} // namespace detail
/** Round AMM equal deposit/withdrawal amount. Deposit/withdrawal formulas
* calculate the amount as a fractional value of the pool balance. The rounding
* takes place on the last step of multiplying the balance by the fraction if
* AMMv1_3 is enabled.
/** Compute a proportional asset amount with amendment-gated directional rounding.
*
* Used for two-sided (equal) deposit/withdrawal where the asset amount is
* `balance × frac`. Under `fixAMMv1_3` the final multiplication is rounded
* via `detail::getAssetRounding` (upward on deposit, downward on withdraw).
* Without the amendment the result uses the current ambient rounding mode.
*
* @tparam A Type of @p frac; either `STAmount` or `Number`.
* @param rules Current ledger rules.
* @param balance Pool balance of the asset.
* @param frac Fraction of the pool balance to apply.
* @param isDeposit Direction; controls rounding when `fixAMMv1_3` is active.
* @return `balance × frac` rounded to preserve the pool invariant.
*/
template <typename A>
STAmount
@@ -637,14 +813,20 @@ getRoundedAsset(Rules const& rules, STAmount const& balance, A const& frac, IsDe
return multiply(balance, frac, rm);
}
/** Round AMM single deposit/withdrawal amount.
* The lambda's are used to delay evaluation until the function
* is executed so that the calculation is not done twice. noRoundCb() is
* called if AMMv1_3 is disabled. Otherwise, the rounding is set and
* the amount is:
* isDeposit is Yes - the balance multiplied by productCb()
* isDeposit is No - the result of productCb(). The rounding is
* the same for all calculations in productCb()
/** Compute a single-asset deposit/withdrawal amount with amendment-gated rounding.
*
* The callback form defers evaluation to avoid computing the formula twice:
* - Without `fixAMMv1_3`: calls `noRoundCb()` and converts without directed rounding.
* - With `fixAMMv1_3`, deposit: calls `multiply(balance, productCb(), rm)`.
* - With `fixAMMv1_3`, withdrawal: installs @p rm globally and calls `productCb()`
* so every arithmetic step inside the callback shares the same rounding direction.
*
* @param rules Current ledger rules.
* @param noRoundCb Produces the unrounded result (pre-amendment path).
* @param balance Pool balance of the asset.
* @param productCb Produces the rounding fraction (post-amendment path).
* @param isDeposit Direction; controls which rounding mode is selected.
* @return Rounded asset amount preserving the pool invariant.
*/
STAmount
getRoundedAsset(
@@ -654,12 +836,18 @@ getRoundedAsset(
std::function<Number()> const& productCb,
IsDeposit isDeposit);
/** Round AMM deposit/withdrawal LPToken amount. Deposit/withdrawal formulas
* calculate the lptokens as a fractional value of the AMM total lptokens.
* The rounding takes place on the last step of multiplying the balance by
* the fraction if AMMv1_3 is enabled. The tokens are then
* adjusted to factor in the loss in precision (we only keep 16 significant
* digits) when adding the lptokens to the balance.
/** Compute a proportional LP token amount with amendment-gated rounding and precision adjustment.
*
* Used for two-sided (equal) deposit/withdrawal. Under `fixAMMv1_3` the
* multiplication `balance × frac` is rounded via `detail::getLPTokenRounding`,
* then `adjustLPTokens` corrects for the 16-digit precision loss introduced
* when adding the result to the running LP token balance.
*
* @param rules Current ledger rules.
* @param balance Current total LP token supply.
* @param frac Fraction of the pool's LP supply to mint or burn.
* @param isDeposit Direction; controls rounding and sign of the adjustment.
* @return LP token amount after rounding and precision correction.
*/
STAmount
getRoundedLPTokens(
@@ -668,16 +856,22 @@ getRoundedLPTokens(
Number const& frac,
IsDeposit isDeposit);
/** Round AMM single deposit/withdrawal LPToken amount.
* The lambda's are used to delay evaluation until the function is executed
* so that the calculations are not done twice.
* noRoundCb() is called if AMMv1_3 is disabled. Otherwise, the rounding is set
* and the lptokens are:
* if isDeposit is Yes - the result of productCb(). The rounding is
* the same for all calculations in productCb()
* if isDeposit is No - the balance multiplied by productCb()
* The lptokens are then adjusted to factor in the loss in precision
* (we only keep 16 significant digits) when adding the lptokens to the balance.
/** Compute a single-asset LP token amount with amendment-gated rounding and precision adjustment.
*
* The callback form avoids evaluating the formula twice:
* - Without `fixAMMv1_3`: calls `noRoundCb()` with no directed rounding.
* - With `fixAMMv1_3`, deposit: installs the LP rounding mode globally and
* calls `productCb()` (all arithmetic inside shares the direction).
* - With `fixAMMv1_3`, withdrawal: calls `multiply(lptAMMBalance, productCb(), rm)`.
* In all post-amendment cases, `adjustLPTokens` then corrects for 16-digit
* precision loss in the running LP balance.
*
* @param rules Current ledger rules.
* @param noRoundCb Produces the unrounded result (pre-amendment path).
* @param lptAMMBalance Current total LP token supply.
* @param productCb Produces the rounding fraction (post-amendment path).
* @param isDeposit Direction; controls rounding mode selection.
* @return LP token amount after rounding and precision correction.
*/
STAmount
getRoundedLPTokens(
@@ -687,16 +881,21 @@ getRoundedLPTokens(
std::function<Number()> const& productCb,
IsDeposit isDeposit);
/* Next two functions adjust asset in/out amount to factor in the adjusted
* lptokens. The lptokens are calculated from the asset in/out. The lptokens are
* then adjusted to factor in the loss in precision. The adjusted lptokens might
* be less than the initially calculated tokens. Therefore, the asset in/out
* must be adjusted. The rounding might result in the adjusted amount being
* greater than the original asset in/out amount. If this happens,
* then the original amount is reduced by the difference in the adjusted amount
* and the original amount. The actual tokens and the actual adjusted amount
* are then recalculated. The minimum of the original and the actual
* adjusted amount is returned.
/** Adjust a single-asset deposit amount to match the precision-corrected LP token count.
*
* Under `fixAMMv1_3`: computes `ammAssetIn(balance, lptAMMBalance, tokens, tfee)`.
* If rounding causes the derived asset amount to exceed @p amount, the deposit is
* reduced by the overshoot and both tokens and asset are recomputed, then the minimum
* of original and adjusted amounts is returned. Before the amendment, returns the
* inputs unchanged.
*
* @param rules Current ledger rules.
* @param balance Pool balance of the asset being deposited.
* @param amount Requested deposit amount.
* @param lptAMMBalance Current total LP token supply.
* @param tokens LP token count before precision adjustment.
* @param tfee Trading fee in basis points.
* @return `{adjustedTokens, adjustedAmount}` pair.
*/
std::pair<STAmount, STAmount>
adjustAssetInByTokens(
@@ -706,6 +905,23 @@ adjustAssetInByTokens(
STAmount const& lptAMMBalance,
STAmount const& tokens,
std::uint16_t tfee);
/** Adjust a single-asset withdrawal amount to match the precision-corrected LP token count.
*
* Under `fixAMMv1_3`: computes `ammAssetOut(balance, lptAMMBalance, tokens, tfee)`.
* If rounding causes the derived asset amount to exceed @p amount, the withdrawal is
* reduced by the overshoot and both tokens and asset are recomputed, then the minimum
* of original and adjusted amounts is returned. Before the amendment, returns the
* inputs unchanged.
*
* @param rules Current ledger rules.
* @param balance Pool balance of the asset being withdrawn.
* @param amount Requested withdrawal amount.
* @param lptAMMBalance Current total LP token supply.
* @param tokens LP token count before precision adjustment.
* @param tfee Trading fee in basis points.
* @return `{adjustedTokens, adjustedAmount}` pair.
*/
std::pair<STAmount, STAmount>
adjustAssetOutByTokens(
Rules const& rules,
@@ -715,8 +931,20 @@ adjustAssetOutByTokens(
STAmount const& tokens,
std::uint16_t tfee);
/** Find a fraction of tokens after the tokens are adjusted. The fraction
* is used to adjust equal deposit/withdraw amount.
/** Recompute the LP token fraction after precision adjustment.
*
* Under `fixAMMv1_3` the precision-adjusted token count may differ from the
* originally requested count, so the fraction `tokens / lptAMMBalance` must
* be recomputed from the adjusted value before it is used to scale equal
* deposit/withdrawal amounts. Returns @p frac unchanged when `fixAMMv1_3`
* is inactive (the precision adjustment has not yet been applied).
*
* @param rules Current ledger rules.
* @param lptAMMBalance Current total LP token supply.
* @param tokens Precision-adjusted LP token count.
* @param frac Original fraction before adjustment.
* @return Adjusted fraction `tokens / lptAMMBalance`, or @p frac if
* `fixAMMv1_3` is not active.
*/
Number
adjustFracByTokens(
@@ -725,7 +953,19 @@ adjustFracByTokens(
STAmount const& tokens,
Number const& frac);
/** Get AMM pool balances.
/** Read the AMM's current pool asset balances from the ledger.
*
* Delegates to `accountHolds` for each asset, respecting freeze and
* authorization policy. Does not read the LP token balance.
*
* @param view Ledger state to query.
* @param ammAccountID AccountID of the AMM's pseudo-account.
* @param asset1 First pool asset.
* @param asset2 Second pool asset.
* @param freezeHandling Whether to enforce freeze restrictions.
* @param authHandling Whether to enforce authorization restrictions.
* @param j Journal for diagnostic logging.
* @return `{balance1, balance2}` pair in the same asset order as the inputs.
*/
std::pair<STAmount, STAmount>
ammPoolHolds(
@@ -737,9 +977,23 @@ ammPoolHolds(
AuthHandling authHandling,
beast::Journal const j);
/** Get AMM pool and LP token balances. If both optIssue are
* provided then they are used as the AMM token pair issues.
* Otherwise the missing issues are fetched from ammSle.
/** Read the AMM's pool balances and total LP token supply from the ledger.
*
* When both optional assets are provided they are validated against the AMM
* SLE's stored pair and used as the query order; providing only one resolves
* the counterpart from `ammSle`. If neither is provided, the canonical order
* from `ammSle` is used. An invalid asset pair (mismatched with the AMM SLE)
* indicates a corrupted AMM object and returns `tecAMM_INVALID_TOKENS`.
*
* @param view Ledger state to query.
* @param ammSle The AMM's `ltAMM` SLE.
* @param optAsset1 Optional first asset override.
* @param optAsset2 Optional second asset override.
* @param freezeHandling Whether to enforce freeze restrictions.
* @param authHandling Whether to enforce authorization restrictions.
* @param j Journal for diagnostic logging.
* @return `{balance1, balance2, lpTokenBalance}` on success, or
* `Unexpected(tecAMM_INVALID_TOKENS)` if the asset pair is invalid.
*/
Expected<std::tuple<STAmount, STAmount, STAmount>, TER>
ammHolds(
@@ -751,7 +1005,21 @@ ammHolds(
AuthHandling authHandling,
beast::Journal const j);
/** Get the balance of LP tokens.
/** Read an LP's token balance from its direct trustline with the AMM account.
*
* Intentionally bypasses `accountHolds` — that function would also check
* whether the AMM's underlying pool assets are frozen (under
* `fixFrozenLPTokenTransfer`), which is incorrect policy for LP token balance
* queries. Only the LP token trustline's own freeze flag is checked.
* Trust-line orientation: raw `sfBalance` is negated when `lpAccount > ammAccount`.
*
* @param view Ledger state to query.
* @param asset1 First pool asset (used to derive the LP token currency).
* @param asset2 Second pool asset.
* @param ammAccount AccountID of the AMM's pseudo-account (LP token issuer).
* @param lpAccount AccountID of the liquidity provider.
* @param j Journal for diagnostic logging.
* @return The LP's token balance, or zero if the trustline is absent or frozen.
*/
STAmount
ammLPHolds(
@@ -762,6 +1030,17 @@ ammLPHolds(
AccountID const& lpAccount,
beast::Journal const j);
/** Read an LP's token balance using the asset pair stored in @p ammSle.
*
* Convenience overload; extracts `sfAsset`, `sfAsset2`, and `sfAccount` from
* @p ammSle and delegates to the five-parameter `ammLPHolds`.
*
* @param view Ledger state to query.
* @param ammSle The AMM's `ltAMM` SLE.
* @param lpAccount AccountID of the liquidity provider.
* @param j Journal for diagnostic logging.
* @return The LP's token balance, or zero if the trustline is absent or frozen.
*/
STAmount
ammLPHolds(
ReadView const& view,
@@ -769,25 +1048,72 @@ ammLPHolds(
AccountID const& lpAccount,
beast::Journal const j);
/** Get AMM trading fee for the given account. The fee is discounted
* if the account is the auction slot owner or one of the slot's authorized
* accounts.
/** Get the effective AMM trading fee for @p account.
*
* Returns the auction slot's `sfDiscountedFee` if the slot is unexpired and
* @p account is either the slot owner or one of up to four authorized accounts;
* otherwise returns the AMM's global `sfTradingFee`. Expiration is compared
* against the ledger's `parentCloseTime` (the slot stores
* `parentCloseTime + TOTAL_TIME_SLOT_SECS` at creation, i.e. 24 hours).
*
* @param view Ledger state providing the current close time.
* @param ammSle The AMM's `ltAMM` SLE.
* @param account The account whose fee rate is needed.
* @return Fee rate in basis points (01000).
*/
std::uint16_t
getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account);
/** Returns total amount held by AMM for the given token.
/** Read the AMM account's raw pool-asset balance, bypassing balance hooks.
*
* Unlike `accountHolds`, this function does not invoke `balanceHookIOU` or
* `balanceHookMPT`, so the result is unaffected by `PaymentSandbox`
* deferred-credit accounting. Used when the AMM needs its own unmodified
* balance for math, not for payment routing. Returns zero if the trustline
* or MPToken object is absent or frozen.
*
* @param view Ledger state to query.
* @param ammAccountID AccountID of the AMM's pseudo-account.
* @param asset The pool asset to query (IOU, XRP, or MPT).
* @return The raw balance, or zero if unavailable.
*/
STAmount
ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const& asset);
/** Delete trustlines to AMM. If all trustlines are deleted then
* AMM object and account are deleted. Otherwise tecINCOMPLETE is returned.
/** Remove all ledger objects owned by the AMM and, if successful, delete the AMM itself.
*
* Deletion is ordered: IOU trustlines first, then MPToken objects, then the
* AMM SLE and its `AccountRoot`. Because each ledger transaction has a bounded
* work budget, not all trustlines may be removable in one call; in that case
* `tecINCOMPLETE` is returned and the caller must submit additional transactions
* to finish. The AMM can be re-deposited while deletion is incomplete.
*
* @param view Sandbox for applying state changes.
* @param asset First pool asset (used to locate the AMM keylet).
* @param asset2 Second pool asset.
* @param j Journal for diagnostic logging.
* @return `tesSUCCESS` on full deletion, `tecINCOMPLETE` if trustlines remain,
* or `tecINTERNAL` for unexpected ledger inconsistencies.
*/
TER
deleteAMMAccount(Sandbox& view, Asset const& asset, Asset const& asset2, beast::Journal j);
/** Initialize Auction and Voting slots and set the trading/discounted fee.
/** Initialize the vote slot and auction slot on a new or re-created AMM.
*
* Called on both `AMMCreate` and on `AMMDeposit` when the pool was previously
* drained to zero. Sets up:
* - One vote entry for @p account with full weight (`kVOTE_WEIGHT_SCALE_FACTOR`).
* - An auction slot owned by @p account, expiring in 24 hours, at zero price.
* - `sfDiscountedFee` = `tfee / kAUCTION_SLOT_DISCOUNTED_FEE_FRACTION`.
* - Absent-field canonicalization: fee fields are removed if their value is zero.
* - Under `fixCleanup3_2_0`, stale `sfAuthAccounts` from any previous slot owner
* are cleared.
*
* @param view Apply-view for the current transaction.
* @param ammSle The AMM's `ltAMM` SLE (modified in place).
* @param account The creator/re-depositor receiving the slot.
* @param lptAsset The LP token asset descriptor (used as the `sfPrice` currency).
* @param tfee Trading fee in basis points to set.
*/
void
initializeFeeAuctionVote(
@@ -797,16 +1123,41 @@ initializeFeeAuctionVote(
Asset const& lptAsset,
std::uint16_t tfee);
/** Return true if the Liquidity Provider is the only AMM provider, false
* otherwise. Return tecINTERNAL if encountered an unexpected condition,
* for instance Liquidity Provider has more than one LPToken trustline.
/** Determine whether @p lpAccount is the sole remaining liquidity provider.
*
* Walks the AMM account's owner directory (up to 10 pages, covering at most
* 4 objects) counting LPToken trustlines, pool-asset trustlines, MPToken
* objects, and the AMM SLE itself. Any second LPToken trustline belonging to
* a different account returns `false` immediately.
*
* @param view Ledger state to query.
* @param ammIssue The LP token issue (currency + AMM account as issuer).
* @param lpAccount AccountID of the candidate sole LP.
* @return `true` if @p lpAccount is the only LP, `false` if other LPs exist,
* or `Unexpected(tecINTERNAL)` for any unexpected directory state
* (e.g. more than one LPToken trustline for @p lpAccount).
*/
Expected<bool, TER>
isOnlyLiquidityProvider(ReadView const& view, Issue const& ammIssue, AccountID const& lpAccount);
/** Due to rounding, the LPTokenBalance of the last LP might
* not match the LP's trustline balance. If it's within the tolerance,
* update LPTokenBalance to match the LP's trustline balance.
/** Reconcile the AMM's `sfLPTokenBalance` with the last LP's trustline balance.
*
* Accumulated rounding over the life of the pool can cause the AMM's running
* `sfLPTokenBalance` to differ slightly from the sole LP's trustline balance.
* This function:
* 1. Confirms @p account is the only remaining LP via `isOnlyLiquidityProvider`.
* 2. If so, verifies the discrepancy is within 0.1% (tolerance `1e-3`).
* 3. If within tolerance, updates `sfLPTokenBalance` to @p lpTokens so the
* final withdrawal leaves the AMM in a fully consistent state.
*
* @param sb Sandbox for applying the balance correction.
* @param lpTokens The last LP's actual trustline balance.
* @param ammSle The AMM's `ltAMM` SLE (updated in place if correction applied).
* @param account AccountID of the candidate sole LP.
* @return `true` if the balance was reconciled or no adjustment was needed
* (other LPs exist), `Unexpected(tecAMM_INVALID_TOKENS)` if the
* discrepancy exceeds tolerance, or `Unexpected(tecINTERNAL)` on an
* unexpected directory error.
*/
Expected<bool, TER>
verifyAndAdjustLPTokenBalance(

View File

@@ -1,3 +1,12 @@
/** @file
* Free functions for querying and mutating `ltACCOUNT_ROOT` ledger entries.
*
* Provides the canonical helpers for freeze-state queries, spendable XRP
* balance, owner-count bookkeeping, transfer fees, destination-tag
* enforcement, and the creation and detection of pseudo-accounts (AMM,
* Vault, LoanBroker). Almost every transaction processor depends on at
* least one function here.
*/
#pragma once
#include <xrpl/basics/Expected.h>
@@ -15,26 +24,60 @@
namespace xrpl {
/** Check if the issuer has the global freeze flag set.
@param issuer The account to check
@return true if the account has global freeze set
*/
/** Check whether an IOU issuer has the global freeze flag active.
*
* XRP is never frozen; this function returns `false` immediately for the XRP
* account. For any other issuer it reads `lsfGlobalFreeze` from the
* account root. Missing accounts are treated as non-frozen.
*
* @param view The read-only ledger view to query.
* @param issuer The account whose freeze state is to be checked.
* @return `true` if `issuer` is a non-XRP account with `lsfGlobalFreeze` set;
* `false` otherwise.
*/
[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, AccountID const& issuer);
// Calculate liquid XRP balance for an account.
// This function may be used to calculate the amount of XRP that
// the holder is able to freely spend. It subtracts reserve requirements.
//
// ownerCountAdj adjusts the owner count in case the caller calculates
// before ledger entries are added or removed. Positive to add, negative
// to subtract.
//
// @param ownerCountAdj positive to add to count, negative to reduce count.
/** Compute the spendable XRP balance for an account after reserve deduction.
*
* Queries the account's current balance and owner count through the view's
* virtual hook methods (`balanceHookIOU`, `ownerCountHook`) so that
* `PaymentSandbox` can overlay uncommitted in-flight changes without any
* branching here. The reserve is then subtracted; if the balance is below
* the reserve, the function returns zero rather than a negative amount.
*
* Pseudo-accounts (AMM, Vault, LoanBroker) bypass the reserve calculation
* entirely and receive the full balance as spendable XRP, because they
* cannot submit transactions and must never be blocked by reserve checks.
*
* @param view The ledger view to query.
* @param id The account whose liquid XRP balance is computed.
* @param ownerCountAdj Signed delta applied to `sfOwnerCount` before the
* reserve is calculated. Pass a positive value when the caller is about
* to add ledger entries; pass a negative value when entries are about to
* be removed. This lets callers reason about post-mutation availability
* before the state is committed to the view.
* @param j Journal for trace-level diagnostics.
* @return The spendable XRP amount, clamped to zero from below.
*/
[[nodiscard]] XRPAmount
xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j);
/** Adjust the owner count up or down. */
/** Increment or decrement `sfOwnerCount` on an account SLE and notify the view.
*
* Delegates to a file-static helper that clamps the result to
* `[0, UINT32_MAX]`, logging at `fatal` severity if either bound would be
* exceeded — silent wrapping of the `uint32_t` field would corrupt ledger
* state. After clamping, `view.adjustOwnerCountHook()` is called before the
* new value is written; `PaymentSandbox` overrides that hook to track the
* high-water-mark count, ensuring subsequent `ownerCountHook` reads use the
* most conservative value seen during the payment.
*
* @param view The mutable view on which the SLE update is recorded.
* @param sle The account SLE to adjust; a null pointer is silently ignored.
* @param amount Signed delta to apply to `sfOwnerCount`; must be non-zero.
* @param j Journal for fatal-level diagnostics on overflow or underflow.
*/
void
adjustOwnerCount(
ApplyView& view,
@@ -42,45 +85,89 @@ adjustOwnerCount(
std::int32_t amount,
beast::Journal j);
/** Returns IOU issuer transfer fee as Rate. Rate specifies
* the fee as fractions of 1 billion. For example, 1% transfer rate
* is represented as 1,010,000,000.
* @param issuer The IOU issuer
/** Return the IOU transfer fee for an issuer as a `Rate` value.
*
* `Rate` expresses the fee as a fraction of one billion, so a 1% fee is
* represented as 1,010,000,000. If the issuer account does not exist or
* has not set `sfTransferRate`, `parityRate` (no fee, i.e., 1,000,000,000)
* is returned — callers never need to handle a null case.
*
* @param view The ledger view to query.
* @param issuer The IOU issuer whose transfer fee is requested.
* @return The issuer's `Rate`, or `parityRate` if none is configured.
*/
[[nodiscard]] Rate
transferRate(ReadView const& view, AccountID const& issuer);
/** Generate a pseudo-account address from a pseudo owner key.
@param pseudoOwnerKey The key to generate the address from
@return The generated account ID
*/
/** Derive a collision-free pseudo-account `AccountID` from an owner key.
*
* Iterates up to 256 attempts. Each attempt hashes a counter, the parent
* ledger's hash, and `pseudoOwnerKey` through `sha512Half` then
* `ripesha_hasher` (RIPEMD-160(SHA-256(...))). The parent-hash component
* prevents precomputation of collisions. The first candidate address that
* has no existing `AccountRoot` in `view` is returned.
*
* @param view The ledger view used to check for address collisions.
* @param pseudoOwnerKey The 256-bit key identifying the pseudo-account owner
* (e.g., the AMM or Vault object ID).
* @return A collision-free `AccountID`, or `beast::kZERO` if all 256
* attempts collided. `createPseudoAccount` propagates exhaustion as
* `tecDUPLICATE`.
* @note The 256-attempt cap is consensus-critical and must not be changed
* without an amendment, as it determines the pseudo-account address space.
*/
AccountID
pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey);
/** Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account
if set.
The list is constructed during initialization and is const after that.
Pseudo-account designator fields MUST be maintained by including the
SField::sMD_PseudoAccount flag in the SField definition.
*/
/** Return the singleton list of `SField`s that designate a pseudo-account.
*
* Built once at first call by scanning the `ltACCOUNT_ROOT` `SOTemplate`
* from `LedgerFormats` and selecting every field whose `SField::sMD_PseudoAccount`
* metadata bit is set. Currently includes `sfAMMID`, `sfVaultID`, and
* `sfLoanBrokerID`. The discovery is fully data-driven: adding a new
* pseudo-account type requires only tagging its key field with
* `SField::sMD_PseudoAccount` in `sfields.macro` — no manual registration
* here is needed.
*
* @return A const reference to the cached vector of pseudo-account fields.
* @note Non-active amendments are harmless: the corresponding field will
* never be set in practice, so the list remains correct regardless of
* which amendments are enabled.
*/
[[nodiscard]] std::vector<SField const*> const&
getPseudoAccountFields();
/** Returns true if and only if sleAcct is a pseudo-account or specific
pseudo-accounts in pseudoFieldFilter.
Returns false if sleAcct is:
- NOT a pseudo-account OR
- NOT a ltACCOUNT_ROOT OR
- null pointer
*/
/** Determine whether an SLE is a pseudo-account (optionally of a specific type).
*
* Returns `true` only when all three conditions hold: `sleAcct` is non-null,
* its ledger-entry type is `ltACCOUNT_ROOT`, and at least one pseudo-account
* designator field (from `getPseudoAccountFields()`) is present. When
* `pseudoFieldFilter` is non-empty, only fields in the filter are considered,
* allowing callers to distinguish AMM pseudo-accounts from Vault
* pseudo-accounts.
*
* @param sleAcct The SLE to inspect; may be null.
* @param pseudoFieldFilter Optional subset of pseudo-account fields to match
* against. An empty set (the default) matches any pseudo-account field.
* @return `true` if `sleAcct` is a pseudo-account (of a type in the filter
* when one is provided); `false` otherwise.
*/
[[nodiscard]] bool
isPseudoAccount(
std::shared_ptr<SLE const> sleAcct,
std::set<SField const*> const& pseudoFieldFilter = {});
/** Convenience overload that reads the account from the view. */
/** Convenience overload that looks up the account from a `ReadView`.
*
* Reads the `AccountRoot` for `accountId` via `keylet::account()` and
* delegates to the SLE overload.
*
* @param view The ledger view to query.
* @param accountId The account address to look up.
* @param pseudoFieldFilter Optional field filter forwarded to the SLE overload.
* @return `true` if the account exists and is a pseudo-account matching the
* filter; `false` otherwise.
*/
[[nodiscard]] inline bool
isPseudoAccount(
ReadView const& view,
@@ -90,29 +177,48 @@ isPseudoAccount(
return isPseudoAccount(view.read(keylet::account(accountId)), pseudoFieldFilter);
}
/**
* Create pseudo-account, storing pseudoOwnerKey into ownerField.
/** Create a protocol-owned pseudo-account `AccountRoot` SLE.
*
* The list of valid ownerField is maintained in AccountRootHelpers.cpp and
* the caller to this function must perform necessary amendment check(s)
* before using a field. The amendment check is **not** performed in
* createPseudoAccount.
* Derives a collision-free address via `pseudoAccountAddress()`, constructs
* an `AccountRoot` with zero balance, `lsfDisableMaster | lsfDefaultRipple |
* lsfDepositAuth`, and stores `pseudoOwnerKey` in `ownerField`. When
* `featureSingleAssetVault` or `featureLendingProtocol` is enabled,
* `sfSequence` is set to `0`; otherwise it is set to the current ledger
* sequence. The zero sequence makes pseudo-accounts visually distinguishable
* and provides an extra barrier against accidental transaction submission.
*
* In debug builds, an `XRPL_ASSERT` fires if `ownerField` does not carry the
* `SField::sMD_PseudoAccount` flag, catching misuse at development time.
*
* @param view The mutable ledger view into which the new SLE is
* inserted.
* @param pseudoOwnerKey The 256-bit key of the owning object (e.g., the AMM
* or Vault ledger entry key); stored in `ownerField` on the new SLE.
* @param ownerField The back-link field written on the new SLE; must be
* one of the fields returned by `getPseudoAccountFields()`.
* @return The newly created SLE on success, or `tecDUPLICATE` if all 256
* address derivation attempts collided.
* @note Amendment checks are the **caller's** responsibility. This function
* is amendment-neutral by design; callers such as `VaultCreate` and
* `LoanBrokerSet` must gate on the relevant feature flag before invoking.
*/
[[nodiscard]] Expected<std::shared_ptr<SLE>, TER>
createPseudoAccount(
ApplyView& view,
uint256 const& pseudoOwnerKey,
SField const& ownerField,
std::uint32_t additionalFlags = 0);
createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey, SField const& ownerField);
[[nodiscard]] bool
isBlackholed(ReadView const& view, std::shared_ptr<SLE const> const& sle);
/** Checks the destination and tag.
- Checks that the SLE is not null.
- If the SLE requires a destination tag, checks that there is a tag.
*/
/** Validate a payment destination SLE and its destination-tag requirement.
*
* Returns `tecNO_DST` if `toSle` is null (the destination account does not
* exist), and `tecDST_TAG_NEEDED` if the destination has set
* `lsfRequireDestTag` but the transaction supplies no tag. Returns
* `tesSUCCESS` otherwise.
*
* @param toSle The destination account SLE; may be null.
* @param hasDestinationTag `true` if the transaction includes a destination
* tag field.
* @return `tecNO_DST`, `tecDST_TAG_NEEDED`, or `tesSUCCESS`.
* @note The ledger enforces the *presence* of a tag but never interprets its
* value; semantics (e.g., exchange user IDs) are opaque to the protocol.
*/
[[nodiscard]] TER
checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag);

View File

@@ -1,3 +1,17 @@
/** @file
* Central contract for credential and deposit pre-authorization logic.
*
* Included by every fund-transfer transactor (Payment, EscrowFinish,
* PaymentChannelClaim, VaultDeposit) that must honor destination-account
* access controls.
*
* Functions divide along the preclaim / doApply boundary:
* - `xrpl::credentials::*` — read-only checks safe to call from preclaim.
* - `xrpl::verifyDepositPreauth` / `xrpl::verifyValidDomain` — mutating
* counterparts that must be called from doApply when the corresponding
* preclaim function succeeds, so that expired credential objects are
* physically deleted from the ledger as a side effect.
*/
#pragma once
#include <xrpl/basics/Log.h>
@@ -13,57 +27,225 @@
namespace xrpl {
namespace credentials {
// These function will be used by the code that use DepositPreauth / Credentials
// (and any future pre-authorization modes) as part of authorization (all the
// transfer funds transactions)
// Check if credential sfExpiration field has passed ledger's parentCloseTime
/** Test whether a credential SLE has passed its expiration time.
*
* Reads `sfExpiration` from @p sleCredential, defaulting to
* `std::numeric_limits<uint32_t>::max()` when the field is absent, so
* credentials with no expiration field never expire.
*
* @param sleCredential The credential SLE to inspect.
* @param closed The parent ledger's close time. Must be a
* NetClock epoch value — do not pass wall-clock time.
* @return `true` if the credential has expired, `false` otherwise.
*/
bool
checkExpired(SLE const& sleCredential, NetClock::time_point const& closed);
// Actually remove a credentials object from the ledger
/** Remove a credential SLE and its entries from both owner directories.
*
* A credential is indexed in two owner directories — the issuer's and the
* subject's. Reserve-count accounting depends on acceptance state:
* - Before acceptance (`lsfAccepted` unset): only the issuer holds the
* reserve; only the issuer's count is decremented.
* - After acceptance with distinct accounts: the subject holds the reserve
* and its count is decremented.
* - When issuer and subject are the same account, only one directory
* removal is performed.
*
* @note Paths indicating ledger corruption (missing account SLE, failed
* `dirRemove`) are marked `LCOV_EXCL` and are unreachable under normal
* operation.
*
* @param view Mutable ledger view through which the SLE is erased.
* @param sleCredential The credential SLE to delete; must not be null.
* @param j Journal for fatal-level error logging.
* @return `tesSUCCESS` on success; `tecNO_ENTRY` if @p sleCredential is
* null; `tecINTERNAL` or `tefBAD_LEDGER` on internal directory
* inconsistency.
*/
[[nodiscard]] TER
deleteSLE(ApplyView& view, std::shared_ptr<SLE> const& sleCredential, beast::Journal j);
// Amendment and parameters checks for sfCredentialIDs field
/** Validate the `sfCredentialIDs` field of a transaction at preflight time.
*
* Enforces non-empty, at most `kMAX_CREDENTIALS_ARRAY_SIZE` entries, and no
* duplicate hashes. Returns `tesSUCCESS` immediately when `sfCredentialIDs`
* is absent, as credentials are optional for most transaction types.
*
* @param tx The transaction under preflight validation.
* @param j Journal for trace-level malformed-transaction logging.
* @return `tesSUCCESS` if the field is absent or valid; `temMALFORMED` if
* the array is empty, too large, or contains duplicates.
*/
NotTEC
checkFields(STTx const& tx, beast::Journal j);
// Accessing the ledger to check if provided credentials are valid. Do not use
// in doApply (only in preclaim) since it does not remove expired credentials.
// If you call it in preclaim, you also must call verifyDepositPreauth in
// doApply
/** Verify that all credentials in a transaction exist, are owned by the
* sender, and have been accepted — for use in preclaim only.
*
* Checks each ID in `sfCredentialIDs`: the SLE must exist, its `sfSubject`
* must equal @p src, and `lsfAccepted` must be set. Expiration is
* deliberately not checked here; expired credentials are deleted in doApply
* by `verifyDepositPreauth` or `verifyValidDomain`.
*
* @note If this returns `tesSUCCESS` in preclaim, the caller must invoke
* `verifyDepositPreauth` in doApply to garbage-collect any credentials
* that expire before the enclosing transaction applies.
*
* @param tx The transaction whose `sfCredentialIDs` field is inspected.
* @param view Read-only ledger view for SLE lookups.
* @param src The account that must own every listed credential.
* @param j Journal for trace-level logging.
* @return `tesSUCCESS` if `sfCredentialIDs` is absent or all credentials are
* valid; `tecBAD_CREDENTIALS` if any credential is missing, belongs to a
* different account, or has not been accepted.
*/
TER
valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal j);
// Check if subject has any credential maching the given domain. If you call it
// in preclaim and it returns tecEXPIRED, you should call verifyValidDomain in
// doApply. This will ensure that expired credentials are deleted.
/** Check whether @p subject holds a live, accepted credential for a
* permissioned domain — for use in preclaim only.
*
* Reads the `PermissionedDomain` SLE, iterates its `sfAcceptedCredentials`
* array, and looks up the corresponding credential SLE for @p subject.
* A credential qualifies when it exists, has not expired, and carries
* `lsfAccepted`.
*
* Because a `ReadView` is immutable, expired credentials cannot be deleted
* here. The function returns `tecEXPIRED` when all matching credentials
* are expired — signaling the caller that the condition may resolve in
* doApply where `verifyValidDomain` will physically remove them.
*
* @note If this returns `tecEXPIRED` in preclaim, the caller must invoke
* `verifyValidDomain` in doApply so that expired objects are
* garbage-collected even if the transaction ultimately fails.
*
* @param view Read-only ledger view.
* @param domainID Key of the `PermissionedDomain` SLE to check against.
* @param subject Account that must hold a qualifying credential.
* @return `tesSUCCESS` if a live accepted credential exists; `tecEXPIRED`
* if only expired credentials were found; `tecNO_AUTH` if no matching
* credential exists; `tecOBJECT_NOT_FOUND` if the domain does not exist.
*/
TER
validDomain(ReadView const& view, uint256 domainID, AccountID const& subject);
// This function is only called when we about to return tecNO_PERMISSION
// because all the checks for the DepositPreauth authorization failed.
/** Check whether a set of credential IDs matches a credential-set
* `DepositPreauth` entry for the destination account.
*
* Builds a sorted `std::set<std::pair<AccountID, Slice>>` of
* `(issuer, credentialType)` pairs from @p credIDs and tests for the
* existence of the corresponding `keylet::depositPreauth(dst, sorted)`.
* The sorted representation matches the canonical key used at
* `DepositPreauth` creation time.
*
* @note Credential existence is assumed to have been confirmed in preclaim.
* A missing SLE here indicates an internal consistency error.
* @note `Slice` members in the internal sorted set are non-owning views
* into SLE storage. A `lifeExtender` vector keeps the SLEs alive for
* the duration of the lookup.
*
* @param view Read-only ledger view for SLE and keylet lookups.
* @param credIDs The `sfCredentialIDs` vector from the transaction.
* @param dst The destination account whose `DepositPreauth` is checked.
* @return `tesSUCCESS` if a matching `DepositPreauth` object exists;
* `tecNO_PERMISSION` if none exists; `tefINTERNAL` if a credential SLE
* is unexpectedly missing or a duplicate pair is encountered.
*/
TER
authorizedDepositPreauth(ReadView const& view, STVector256 const& ctx, AccountID const& dst);
// Sort credentials array, return empty set if there are duplicates
/** Build a sorted `(issuer, credentialType)` set from a credentials array.
*
* Produces the canonical representation used to key `DepositPreauth`
* objects. Each element of @p credentials must carry `sfIssuer` and
* `sfCredentialType`.
*
* @param credentials An `STArray` of credential pairs, as stored in a
* `DepositPreauth` or `PermissionedDomainSet` transaction.
* @return A sorted set of `(AccountID, Slice)` pairs; an empty set if any
* duplicate `(issuer, credentialType)` pair is detected.
*/
std::set<std::pair<AccountID, Slice>>
makeSorted(STArray const& credentials);
// Check credentials array passed to DepositPreauth/PermissionedDomainSet
// transactions
/** Validate a credential array in `DepositPreauth` or
* `PermissionedDomainSet` transactions at preflight time.
*
* Credentials in these transactions are `(issuer, credentialType)` pairs
* rather than object hashes. Enforces: non-empty; at most @p maxSize
* entries; valid issuer `AccountID`; `sfCredentialType` length in
* `[1, kMAX_CREDENTIAL_TYPE_LENGTH]` bytes; and no logical duplicates
* (detected via `sha512Half(issuer, credentialType)`).
*
* @param credentials The `STArray` of credential pairs to validate.
* @param maxSize Maximum permitted array length (caller-supplied per
* transaction type).
* @param j Journal for trace-level malformed-transaction logging.
* @return `tesSUCCESS` if all entries are valid; `temARRAY_EMPTY`,
* `temARRAY_TOO_LARGE`, `temINVALID_ACCOUNT_ID`, or `temMALFORMED`
* on the first constraint violation found.
*/
NotTEC
checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j);
} // namespace credentials
// Check expired credentials and for credentials maching DomainID of the ledger
// object
/** Enforce domain-credential authorization in doApply, deleting expired
* credentials as a side effect.
*
* The doApply counterpart to `credentials::validDomain`. Collects all
* credential SLEs for @p account that match the `sfAcceptedCredentials`
* list of the `PermissionedDomain` at @p domainID, calls
* `credentials::removeExpired` to physically delete any that have expired,
* then re-checks whether at least one live, accepted credential remains.
*
* The two-pass design (collect → expire → re-validate) ensures expired
* objects are garbage-collected even when the surrounding transaction
* ultimately fails.
*
* @param view Mutable ledger view; expired credential SLEs are erased.
* @param account Account whose credentials are being verified.
* @param domainID Key of the `PermissionedDomain` SLE.
* @param j Journal for trace/error logging.
* @return `tesSUCCESS` if a live accepted credential for the domain exists;
* `tecEXPIRED` if only expired credentials were found; `tecNO_PERMISSION`
* if no matching credential exists; `tecOBJECT_NOT_FOUND` if the domain
* SLE is missing; or a propagated `TER` error from `removeExpired` under
* `fixCleanup3_1_3`.
*/
TER
verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j);
// Check expired credentials and for existing DepositPreauth ledger object
/** Enforce deposit pre-authorization in doApply, deleting expired credentials
* as a side effect.
*
* Called by Payment, EscrowFinish, and PaymentChannelClaim when the
* destination account has `lsfDepositAuth` set. Authorization succeeds
* when any of the following hold:
* - `src == dst` (self-payments are always allowed).
* - `keylet::depositPreauth(dst, src)` exists (account-level pre-auth).
* - A credential-set `DepositPreauth` object exists for the credentials
* submitted via `sfCredentialIDs` (via `credentials::authorizedDepositPreauth`).
*
* If `sfCredentialIDs` is present, `credentials::removeExpired` is called
* unconditionally before the authorization tests. If any credential was
* expired, `tecEXPIRED` is returned immediately without attempting
* authorization.
*
* @param tx The transaction under doApply; may carry `sfCredentialIDs`.
* @param view Mutable ledger view; expired credential SLEs may be erased.
* @param src The sending account.
* @param dst The destination account.
* @param sleDst The destination account's SLE, used to test `lsfDepositAuth`.
* If null, `lsfDepositAuth` is treated as unset and the function returns
* `tesSUCCESS`.
* @param j Journal for trace/error logging.
* @return `tesSUCCESS` if authorized or `lsfDepositAuth` is not set;
* `tecEXPIRED` if submitted credentials have expired;
* `tecNO_PERMISSION` if no matching pre-authorization exists; or a
* propagated error from `removeExpired` or `authorizedDepositPreauth`.
*/
TER
verifyDepositPreauth(
STTx const& tx,

View File

@@ -1,3 +1,12 @@
/** @file
* Runtime enforcement helpers for the XRPL delegate account system.
*
* Transactors call these two functions in sequence during permission
* validation: `checkTxPermission` for the broad transaction-type gate,
* then `loadGranularPermission` when a more restrictive, field-level
* check is needed. The permission schema and encoding convention live
* in `xrpl/protocol/Permissions.h`.
*/
#pragma once
#include <xrpl/protocol/Permissions.h>
@@ -7,24 +16,64 @@
namespace xrpl {
/**
* Check if the delegate account has permission to execute the transaction.
* @param delegate The delegate account.
* @param tx The transaction that the delegate account intends to execute.
* @return tesSUCCESS if the transaction is allowed, terNO_DELEGATE_PERMISSION
* if not.
/** Determine whether a delegate relationship grants blanket permission for
* a transaction type.
*
* Scans the `sfPermissions` array of the `ltDELEGATE` ledger entry for an
* element whose `sfPermissionValue` equals `tx.getTxnType() + 1` — the
* transaction-level encoding used on-ledger. Returns `tesSUCCESS` on the
* first match, or `terNO_DELEGATE_PERMISSION` if no match is found.
*
* A null `delegate` pointer is treated as a missing ledger entry and
* returns `terNO_DELEGATE_PERMISSION` immediately.
*
* The result is `NotTEC` (no `tec` fee-claim codes) because the two
* meaningful outcomes are `tesSUCCESS` and `terNO_DELEGATE_PERMISSION`.
* The `ter` (retry) code is intentional: the `ltDELEGATE` object could be
* updated in a subsequent ledger, so an identical transaction may succeed
* in the future without modification.
*
* @param delegate Immutable `ltDELEGATE` SLE obtained via `view.read()`;
* may be null, in which case `terNO_DELEGATE_PERMISSION` is returned.
* @param tx The transaction whose type is being checked.
* @return `tesSUCCESS` if the delegate holds a transaction-level permission
* for `tx`'s type; `terNO_DELEGATE_PERMISSION` otherwise.
* @note Callers should resolve the SLE via `keylet::delegate(account,
* delegate)` and pass it directly. If the SLE is absent from the
* ledger, `view.read()` returns null and the guard here handles it.
* @see loadGranularPermission — for fine-grained per-flag enforcement when
* this function returns `terNO_DELEGATE_PERMISSION`.
*/
NotTEC
checkTxPermission(std::shared_ptr<SLE const> const& delegate, STTx const& tx);
/**
* Load the granular permissions granted to the delegate account for the
* specified transaction type
* @param delegate The delegate account.
* @param type Used to determine which granted granular permissions to load,
* based on the transaction type.
* @param granularPermissions Granted granular permissions tied to the
* transaction type.
/** Populate a set with all granular sub-operation permissions the delegate
* holds for a given transaction type.
*
* Walks the `sfPermissions` array of the `ltDELEGATE` ledger entry. For
* each element, it casts the `sfPermissionValue` to `GranularPermissionType`
* and asks `Permission::getInstance().getGranularTxType()` whether that
* granular type belongs to `type`. Matching values are inserted into
* `granularPermissions`.
*
* A null `delegate` pointer is a silent no-op; the output set is left
* unchanged.
*
* The set is caller-owned and passed by reference so transactors can declare
* it on the stack, avoiding heap allocation. Callers may also accumulate
* results from multiple calls if needed.
*
* @param delegate Immutable `ltDELEGATE` SLE; may be null (no-op).
* @param type The transaction type whose granular permissions should be
* collected (e.g., `ttTRUST_SET`, `ttPAYMENT`).
* @param granularPermissions Output set populated with every
* `GranularPermissionType` the delegate holds that maps to `type`.
* @note This function is the second stage of a two-step check. Call
* `checkTxPermission` first; only invoke this when that returns
* `terNO_DELEGATE_PERMISSION` and the transaction type supports
* granular flags. Calling it unconditionally wastes a full scan of
* the permissions array on the common case.
* @see checkTxPermission — for the broad transaction-type gate.
*/
void
loadGranularPermission(

View File

@@ -1,3 +1,22 @@
/** @file
* Traversal utilities for ledger directory nodes (`ltDIR_NODE`).
*
* A directory is a linked list of pages (`SLE` of type `ltDIR_NODE`),
* where each page holds an `sfIndexes` field (`STVector256`) of child
* ledger-entry keys and an `sfIndexNext` field that chains to the next
* page. Owner directories track every object an account holds; order-
* book directories track standing offers at a given quality.
*
* This header provides:
* - A const-aware template core (`detail::internalDirFirst` /
* `detail::internalDirNext`) that unifies the read and write traversal
* paths at compile time.
* - A deprecated step-iterator API (`cdirFirst`, `cdirNext`, `dirFirst`,
* `dirNext`) used only where cursor patching during deletion is required.
* - Higher-level callback iterators (`forEachItem`, `forEachItemAfter`)
* for exhaustive and paginated walks.
* - `dirIsEmpty` and `describeOwnerDir` utility helpers.
*/
#pragma once
#include <xrpl/beast/utility/instrumentation.h>
@@ -15,6 +34,32 @@ namespace xrpl {
namespace detail {
/** Advance a directory cursor to the next entry, crossing page boundaries.
*
* When the cursor has consumed all entries in the current page, the function
* follows `sfIndexNext` to load the next page and tail-calls itself to yield
* the first entry of that page in a single logical step. If `sfIndexNext` is
* zero the directory is exhausted: `entry` is zeroed and `false` is returned.
*
* The `if constexpr` branch selects `view.read()` when `N` is `SLE const`
* (read-only traversal via `ReadView`) and `view.peek()` when `N` is `SLE`
* (mutable traversal via `ApplyView`), keeping both paths in one template.
*
* @tparam V A view type derived from `ReadView`.
* @tparam N Either `SLE` (mutable) or `SLE const` (read-only).
* @param view The ledger view to query pages from.
* @param root The 256-bit key of the directory's root (anchor) page.
* @param page In/out: the current page SLE; updated when a page boundary
* is crossed.
* @param index In/out: the zero-based cursor within `page->sfIndexes`;
* incremented to point past the entry that was just returned.
* @param entry Out: the key of the current entry on success; zeroed on
* end-of-directory.
* @return `true` if an entry was produced; `false` if the directory is
* exhausted.
* @note An `XRPL_ASSERT` fires in instrumented builds if `index` exceeds
* the page's entry count, indicating a corrupted cursor.
*/
template <
class V,
class N,
@@ -64,6 +109,23 @@ internalDirNext(
return true;
}
/** Initialise a directory cursor at the first entry of the root page.
*
* Loads the root page via `view.read()` (when `N` is `SLE const`) or
* `view.peek()` (when `N` is `SLE`), resets the index to zero, then
* delegates to `internalDirNext` to yield the first entry.
*
* @tparam V A view type derived from `ReadView`.
* @tparam N Either `SLE` (mutable) or `SLE const` (read-only).
* @param view The ledger view to query pages from.
* @param root The 256-bit key of the directory's root (anchor) page.
* @param page Out: set to the root page SLE on success; unchanged if the
* root page is absent.
* @param index Out: set to zero before delegating to `internalDirNext`.
* @param entry Out: the key of the first entry on success.
* @return `true` if the directory has at least one entry; `false` if the
* root page is absent or the directory is empty.
*/
template <
class V,
class N,
@@ -119,6 +181,24 @@ cdirFirst(
unsigned int& index,
uint256& entry);
/** Returns the first entry in the directory, advancing the index.
*
* Mutable overload of `cdirFirst` for use with `ApplyView`. Yields a
* `shared_ptr<SLE>` obtained via `view.peek()`, allowing the caller to
* modify the page SLE if required.
*
* @deprecated Prefer the `Dir` range adaptor or `forEachItem` for new
* code. Use this overload only when cursor patching during deletion
* is required (see `cleanupOnAccountDelete` in `View.cpp`).
*
* @param view The mutable view against which to operate.
* @param root The 256-bit key of the directory's root page.
* @param page Out: set to the root page SLE obtained via `peek()`.
* @param index Out: set to the cursor position within `page->sfIndexes`.
* @param entry Out: the key of the first directory entry.
* @return `true` if the directory has at least one entry; `false`
* otherwise.
*/
bool
dirFirst(
ApplyView& view,
@@ -151,6 +231,31 @@ cdirNext(
unsigned int& index,
uint256& entry);
/** Advances the mutable directory cursor to the next entry.
*
* Mutable overload of `cdirNext` for use with `ApplyView`. Page
* transitions are handled transparently: when `index` reaches the end
* of the current page, `sfIndexNext` is followed and the cursor is reset
* to the first entry of the new page.
*
* @deprecated Prefer the `Dir` range adaptor or `forEachItem` for new
* code. The primary use case for this function is cursor patching
* during deletion: `cleanupOnAccountDelete` (in `View.cpp`) decrements
* `index` after each deletion so the cursor stays aligned as entries
* shift — a technique that relies on the cursor being externally
* accessible.
*
* @param view The mutable view against which to operate.
* @param root The 256-bit key of the directory's root page.
* @param page In/out: the current page SLE; updated on page boundary
* crossing.
* @param index In/out: the cursor position within `page->sfIndexes`;
* incremented past the returned entry.
* @param entry Out: the key of the current entry on success; zeroed when
* the directory is exhausted.
* @return `true` if an entry was produced; `false` if the directory is
* exhausted.
*/
bool
dirNext(
ApplyView& view,
@@ -160,19 +265,61 @@ dirNext(
uint256& entry);
/** @} */
/** Iterate all items in the given directory. */
/** Exhaustively walk every entry in a directory, invoking a callback for each.
*
* Iterates all pages of the directory in `sfIndexNext` chain order, calling
* `f` with the materialised child SLE for every key in `sfIndexes`. The
* child SLE is obtained via `view.read(keylet::child(key))` and may be
* `nullptr` if the referenced entry is absent from the view; the callback
* must handle that case. Iteration terminates when `sfIndexNext` is zero or
* a page SLE is missing; there is no early-exit mechanism.
*
* @param view The read-only ledger view to query.
* @param root Keylet of the directory's root page; must have type
* `ltDIR_NODE`.
* @param f Callback invoked with each child SLE (possibly `nullptr`).
* @note An `XRPL_ASSERT` fires in instrumented builds if `root.type` is
* not `ltDIR_NODE`; in release builds the function returns silently.
*/
void
forEachItem(
ReadView const& view,
Keylet const& root,
std::function<void(std::shared_ptr<SLE const> const&)> const& f);
/** Iterate all items after an item in the given directory.
@param after The key of the item to start after
@param hint The directory page containing `after`
@param limit The maximum number of items to return
@return `false` if the iteration failed
*/
/** Paginated directory walk, delivering items that follow a cursor key.
*
* Supports cursor-based pagination as used by RPC handlers such as
* `account_offers`, `account_lines`, and `account_channels`. When
* `after` is non-zero the function first attempts to jump to the `hint`
* page (the page the client last saw) to avoid re-scanning all prior
* pages; if the hint does not contain `after`, it falls back to a linear
* scan from the root. Once the cursor is located, subsequent entries are
* delivered to `f` until `limit` is reached or the directory is exhausted.
*
* The callback `f` returns `bool`: `true` to continue (and decrement the
* limit counter), `false` to stop immediately regardless of the remaining
* limit. Callers conventionally request `limit + 1` items and infer a
* non-empty next page when exactly `limit + 1` items are delivered.
*
* @param view The read-only ledger view to query.
* @param root Keylet of the directory's root page; must have type
* `ltDIR_NODE`.
* @param after Cursor key: only entries that follow this key in directory
* order are delivered. Pass `uint256()` (zero) to start from the
* beginning, in which case the function always returns `true`.
* @param hint Page number expected to contain `after`; used as a fast-
* path optimisation. Ignored when `after` is zero or when the hint
* page does not actually contain `after`.
* @param limit Maximum number of `true`-returning callback invocations
* before the walk stops.
* @param f Callback invoked for each qualifying child SLE (possibly
* `nullptr` if the key is absent). Return `true` to continue
* iteration; `false` to stop early.
* @return `true` if `after` was found (or `after` is zero); `false` if
* the cursor key was never located, indicating a stale or invalid
* marker that callers should surface as a pagination error.
*/
bool
forEachItemAfter(
ReadView const& view,
@@ -182,7 +329,15 @@ forEachItemAfter(
unsigned int limit,
std::function<bool(std::shared_ptr<SLE const> const&)> const& f);
/** Iterate all items in an account's owner directory. */
/** Exhaustively walk every entry in an account's owner directory.
*
* Convenience overload that resolves `id` to `keylet::ownerDir(id)` and
* forwards to `forEachItem(view, Keylet, f)`.
*
* @param view The read-only ledger view to query.
* @param id The account whose owner directory should be iterated.
* @param f Callback invoked with each child SLE (possibly `nullptr`).
*/
inline void
forEachItem(
ReadView const& view,
@@ -192,12 +347,22 @@ forEachItem(
forEachItem(view, keylet::ownerDir(id), f);
}
/** Iterate all items after an item in an owner directory.
@param after The key of the item to start after
@param hint The directory page containing `after`
@param limit The maximum number of items to return
@return `false` if the iteration failed
*/
/** Paginated walk of an account's owner directory after a cursor key.
*
* Convenience overload that resolves `id` to `keylet::ownerDir(id)` and
* forwards to `forEachItemAfter(view, Keylet, after, hint, limit, f)`.
*
* @param view The read-only ledger view to query.
* @param id The account whose owner directory should be iterated.
* @param after Cursor key; pass `uint256()` (zero) to start from the
* beginning.
* @param hint Page number expected to contain `after`.
* @param limit Maximum number of `true`-returning callback invocations.
* @param f Callback invoked for each qualifying child SLE. Return `true`
* to continue; `false` to stop early.
* @return `true` if `after` was found (or is zero); `false` if the cursor
* was never located.
*/
inline bool
forEachItemAfter(
ReadView const& view,
@@ -210,13 +375,36 @@ forEachItemAfter(
return forEachItemAfter(view, keylet::ownerDir(id), after, hint, limit, f);
}
/** Returns `true` if the directory is empty
@param key The key of the directory
*/
/** Returns `true` if the directory contains no entries.
*
* An empty `sfIndexes` array on the root page is necessary but not
* sufficient: the root is an anchor page and may have an empty index
* while `sfIndexNext` still points to a populated subsequent page. Both
* conditions — empty `sfIndexes` *and* `sfIndexNext == 0` — must hold
* before declaring the directory empty. A missing root SLE is also
* treated as empty.
*
* @param view The read-only ledger view to query.
* @param k Keylet of the directory's root page.
* @return `true` if the directory has no entries or does not exist;
* `false` otherwise.
*/
[[nodiscard]] bool
dirIsEmpty(ReadView const& view, Keylet const& k);
/** Returns a function that sets the owner on a directory SLE */
/** Returns a callback that stamps a new directory page with its owner account.
*
* The returned `std::function<void(SLE::ref)>` sets `sfOwner = account` on
* the newly allocated `ltDIR_NODE` SLE. It is passed as the `describe`
* argument to `ApplyView::dirInsert` throughout the codebase (e.g.,
* `RippleStateHelpers.cpp`, `PaymentChannelCreate.cpp`) and is invoked only
* when `dirInsert` actually allocates a fresh overflow page, keeping the
* owning account ID out of the generic insertion logic.
*
* @param account The `AccountID` to record as `sfOwner` on each new page.
* @return A callable suitable for the `describe` parameter of
* `ApplyView::dirInsert`.
*/
[[nodiscard]] std::function<void(SLE::ref)>
describeOwnerDir(AccountID const& account);

View File

@@ -1,3 +1,13 @@
/** @file
* Token-delivery helper for IOU and MPT escrow resolution.
*
* Implements `escrowUnlockApplyHelper`, the single function responsible for
* crediting the appropriate account when an IOU or MPT escrow is finished
* (`EscrowFinish`) or cancelled (`EscrowCancel`) under `featureTokenEscrow`.
* The function is specialised once for `Issue` (IOU trust-line path) and once
* for `MPTIssue` (MPToken path); callers reach the correct specialisation via
* `std::visit` on the `Asset` variant, with zero runtime dispatch overhead.
*/
#pragma once
#include <xrpl/basics/Log.h>
@@ -13,6 +23,33 @@
namespace xrpl {
/** Credit an account with tokens held in escrow, applying transfer-fee logic.
*
* Primary template — no body is provided. Only the `Issue` and `MPTIssue`
* full specialisations are defined. Callers should invoke via `std::visit`
* on an `Asset` variant so the compiler selects the correct specialisation
* at compile time.
*
* @tparam T Asset type; must satisfy `ValidIssueType` (`Issue` or `MPTIssue`).
* @param view Mutable ledger view on which state changes are applied.
* @param lockedRate Transfer rate snapshotted at escrow creation time.
* Pass `kPARITY_RATE` for cancellations (return to sender, no fee).
* @param sleDest SLE for the destination account (`receiver`); used for
* owner-count and reserve checks when auto-creating a trust line or
* MPToken holding object.
* @param xrpBalance Pre-fee XRP balance of the destination account; compared
* against the incremental reserve required to create a new holding object.
* @param amount Escrowed token amount (face value locked at escrow creation).
* @param issuer Token issuer.
* @param sender Escrow creator / original token sender.
* @param receiver Account that will receive the unlocked tokens.
* @param createAsset When `true`, auto-creates a trust line or MPToken object
* for `receiver` if one does not already exist. Callers set this only
* when the transaction submitter is also the beneficiary, preserving
* account sovereignty over directory entries.
* @param journal Logging sink.
* @return `tesSUCCESS` on success, or a `tec` error code on failure.
*/
template <ValidIssueType T>
TER
escrowUnlockApplyHelper(
@@ -27,6 +64,40 @@ escrowUnlockApplyHelper(
bool createAsset,
beast::Journal journal);
/** IOU trust-line specialisation of `escrowUnlockApplyHelper`.
*
* Delivers IOU tokens from a finished or cancelled escrow to `receiver`,
* optionally creating the trust line and applying the snapshotted transfer
* fee.
*
* **Issuer short-circuits.** `sender == issuer` returns `tecINTERNAL` (an
* issuer cannot be an escrow originator for their own obligation).
* `receiver == issuer` returns `tesSUCCESS` immediately — delivery to the
* issuer is a redemption handled by the calling transactor at the balance
* level.
*
* **Trust line creation.** When `createAsset` is `true` and no trust line
* exists, one is created with a zero balance and zero limit via `trustCreate`.
* The `sfDefaultRipple` flag is inherited from `sleDest`. Reserve is checked
* first; insufficient reserve returns `tecNO_LINE_INSUF_RESERVE`. When
* `createAsset` is `false` and no line exists, returns `tecNO_LINE`.
*
* **Transfer fee.** The effective rate is `min(lockedRate, currentRate)`,
* protecting the receiver from a rate increase during the escrow lifetime.
* The fee is deducted *from* `amount` (not added on top), so `receiver` gets
* `amount - fee`. When neither party is the issuer and the rate differs from
* `kPARITY_RATE`, the check against the trust-line limit uses `finalAmt`.
*
* **Limit check.** When `createAsset` is `false`, the post-transfer balance
* is compared to `receiver`'s trust-line limit; `tecLIMIT_EXCEEDED` is
* returned if the delivery would exceed it. This check is skipped when
* `createAsset` is `true` because a freshly created line has a zero limit
* and would always fail it spuriously.
*
* @note This function is reached via `std::visit` on an `Asset` variant in
* `EscrowFinish` and `EscrowCancel`. `EscrowCancel` always passes
* `kPARITY_RATE` so no fee is charged on the return-to-sender path.
*/
template <>
inline TER
escrowUnlockApplyHelper<Issue>(
@@ -41,12 +112,6 @@ escrowUnlockApplyHelper<Issue>(
bool createAsset,
beast::Journal journal)
{
if (view.rules().enabled(fixTokenEscrowV1_1))
{
if (!sleDest || sleDest->getType() != ltACCOUNT_ROOT)
return tecINTERNAL;
}
Issue const& issue = amount.get<Issue>();
Keylet const trustLineKey = keylet::line(receiver, issue);
bool const recvLow = issuer > receiver;
@@ -76,21 +141,21 @@ escrowUnlockApplyHelper<Issue>(
initialBalance.get<Issue>().account = noAccount();
if (TER const ter = trustCreate(
view, // payment sandbox
recvLow, // is dest low?
issuer, // source
receiver, // destination
trustLineKey.key, // ledger index
sleDest, // Account to add to
false, // authorize account
(sleDest->getFlags() & lsfDefaultRipple) == 0, //
false, // freeze trust line
false, // deep freeze trust line
initialBalance, // zero initial balance
Issue(currency, receiver), // limit of zero
0, // quality in
0, // quality out
journal); // journal
view,
recvLow,
issuer,
receiver,
trustLineKey.key,
sleDest,
false,
(sleDest->getFlags() & lsfDefaultRipple) == 0,
false,
false,
initialBalance,
Issue(currency, receiver),
0,
0,
journal);
!isTesSuccess(ter))
{
return ter; // LCOV_EXCL_LINE
@@ -103,57 +168,43 @@ escrowUnlockApplyHelper<Issue>(
return tecNO_LINE;
auto const xferRate = transferRate(view, amount);
// update if issuer rate is less than locked rate
// Cap to the lower of the snapshotted and current rate to protect the receiver.
if (xferRate < lockedRate)
lockedRate = xferRate;
// Transfer Rate only applies when:
// 1. Issuer is not involved in the transfer (senderIssuer or
// receiverIssuer)
// 2. The locked rate is different from the parity rate
// NOTE: Transfer fee in escrow works a bit differently from a normal
// payment. In escrow, the fee is deducted from the locked/sending amount,
// whereas in a normal payment, the transfer fee is taken on top of the
// sending amount.
// Fee is deducted from `amount` (not added on top): finalAmt = amount - fee.
// No fee when either party is the issuer, or when lockedRate == kPARITY_RATE.
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != kPARITY_RATE)
{
// compute transfer fee, if any
auto const xferFee =
amount.value() - divideRound(amount, lockedRate, amount.get<Issue>(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
// validate the line limit if the account submitting txn is not the receiver
// of the funds
// Limit check skipped when createAsset is true (freshly created line has
// zero limit and would always fail spuriously).
if (!createAsset)
{
auto const sleRippleState = view.peek(trustLineKey);
if (!sleRippleState)
return tecINTERNAL; // LCOV_EXCL_LINE
// if the issuer is the high, then we use the low limit
// otherwise we use the high limit
// recvLow true → receiver is low side → use sfLowLimit; else sfHighLimit.
STAmount const lineLimit =
sleRippleState->getFieldAmount(recvLow ? sfLowLimit : sfHighLimit);
STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance);
// flip the sign of the line balance if the issuer is not high
if (!recvLow)
lineBalance.negate();
// add the final amount to the line balance
lineBalance += finalAmt;
// if the transfer would exceed the line limit return tecLIMIT_EXCEEDED
if (lineLimit < lineBalance)
return tecLIMIT_EXCEEDED;
}
// if destination is not the issuer then transfer funds
if (!receiverIssuer)
{
auto const ter = directSendNoFee(view, issuer, receiver, finalAmt, true, journal);
@@ -163,6 +214,32 @@ escrowUnlockApplyHelper<Issue>(
return tesSUCCESS;
}
/** MPT specialisation of `escrowUnlockApplyHelper`.
*
* Delivers MPT tokens from a finished or cancelled escrow to `receiver`,
* optionally creating an MPToken holding object and applying the snapshotted
* transfer fee.
*
* **MPToken creation.** When `createAsset` is `true`, `receiver` is not the
* issuer, and no MPToken SLE exists for this issuance, one is created via
* `createMPToken` and the owner count is incremented. Insufficient reserve
* returns `tecINSUFFICIENT_RESERVE`. If no MPToken exists after the creation
* attempt (and `receiver` is not the issuer), returns `tecNO_PERMISSION`.
*
* **Transfer fee.** Identical to the `Issue` path: effective rate is
* `min(lockedRate, currentRate)`, fee is deducted *from* `amount`, and no
* fee is applied when either party is the issuer or the rate is parity.
*
* **`fixTokenEscrowV1` bug fix.** The gross amount passed to `unlockEscrowMPT`
* (used to reduce `sfOutstandingAmount`) is `amount` when the amendment is
* enabled, and `finalAmt` otherwise. Without the fix, the outstanding supply
* is only reduced by the net delivered amount, silently retaining the fee
* portion; with the fix, the full face value is removed from circulation and
* the fee is burned from the outstanding supply.
*
* @note `EscrowCancel` passes `kPARITY_RATE` so no fee is charged when
* tokens are returned to the original sender.
*/
template <>
inline TER
escrowUnlockApplyHelper<MPTIssue>(
@@ -177,12 +254,6 @@ escrowUnlockApplyHelper<MPTIssue>(
bool createAsset,
beast::Journal journal)
{
if (view.rules().enabled(fixTokenEscrowV1_1))
{
if (!sleDest || sleDest->getType() != ltACCOUNT_ROOT)
return tecINTERNAL;
}
bool const senderIssuer = issuer == sender;
bool const receiverIssuer = issuer == receiver;
@@ -201,7 +272,6 @@ escrowUnlockApplyHelper<MPTIssue>(
return ter; // LCOV_EXCL_LINE
}
// update owner count.
adjustOwnerCount(view, sleDest, 1, journal);
}
@@ -209,25 +279,16 @@ escrowUnlockApplyHelper<MPTIssue>(
return tecNO_PERMISSION;
auto const xferRate = transferRate(view, amount);
// update if issuer rate is less than locked rate
// Cap to the lower of the snapshotted and current rate to protect the receiver.
if (xferRate < lockedRate)
lockedRate = xferRate;
// Transfer Rate only applies when:
// 1. Issuer is not involved in the transfer (senderIssuer or
// receiverIssuer)
// 2. The locked rate is different from the parity rate
// NOTE: Transfer fee in escrow works a bit differently from a normal
// payment. In escrow, the fee is deducted from the locked/sending amount,
// whereas in a normal payment, the transfer fee is taken on top of the
// sending amount.
// Fee is deducted from `amount` (not added on top): finalAmt = amount - fee.
// No fee when either party is the issuer, or when lockedRate == kPARITY_RATE.
auto finalAmt = amount;
if ((!senderIssuer && !receiverIssuer) && lockedRate != kPARITY_RATE)
{
// compute transfer fee, if any
auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
// compute balance to transfer
finalAmt = amount.value() - xferFee;
}
return unlockEscrowMPT(

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,16 @@
/** @file
* MPT-specific ledger helper declarations.
*
* Declares the MPT counterpart to `RippleStateHelpers.h`. The asset-agnostic
* `TokenHelpers.h` dispatchers route `MPTIssue`-typed calls here via
* `std::visit` on the `Asset` variant. In addition to the functions that
* mirror IOU trust-line semantics (freeze, transfer rate, holding lifecycle,
* authorization), this header exposes operations with no IOU equivalent:
* escrow accounting, DEX permission gating, supply-overflow arithmetic, and
* the two-phase authorization protocol specific to MPT.
*
* @see RippleStateHelpers.h, TokenHelpers.h
*/
#pragma once
#include <xrpl/beast/utility/Journal.h>
@@ -20,15 +33,65 @@ namespace xrpl {
//
//------------------------------------------------------------------------------
/** Check whether an entire MPT issuance is globally frozen.
*
* Reads the `MPTokenIssuance` SLE and tests `lsfMPTLocked`. A missing
* issuance SLE is treated as unfrozen.
*
* @param view The ledger state to query.
* @param mptIssue The MPT issuance to check.
* @return `true` if `lsfMPTLocked` is set on the issuance; `false` otherwise.
*/
[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue);
/** Check whether a specific account's MPToken holding is individually frozen.
*
* Reads the per-holder `MPToken` SLE and tests `lsfMPTLocked`. Returns
* `false` if no `MPToken` SLE exists for the account (i.e., the account
* holds no balance for this issuance).
*
* @param view The ledger state to query.
* @param account The account whose holding is checked.
* @param mptIssue The MPT issuance to check against.
* @return `true` if the account's `MPToken` carries `lsfMPTLocked`;
* `false` otherwise.
*/
[[nodiscard]] bool
isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);
/** Check whether an account's access to an MPT issuance is frozen by any tier.
*
* Applies three checks in order: global issuance lock (`isGlobalFrozen`),
* per-account holding lock (`isIndividualFrozen`), and vault pseudo-account
* freeze (`isVaultPseudoAccountFrozen`). Short-circuits on the first match.
*
* @param view The ledger state to query.
* @param account The account to check.
* @param mptIssue The MPT issuance to check against.
* @param depth Recursion depth guard forwarded to `isVaultPseudoAccountFrozen`;
* bounds pathological nested-vault configurations (currently unreachable
* in practice, but defended against up to `maxAssetCheckDepth`).
* @return `true` if any freeze tier applies; `false` otherwise.
*/
[[nodiscard]] bool
isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue, int depth = 0);
/** Check whether any account in a set is frozen for an MPT issuance.
*
* Sequences checks across separate passes to minimize cost: the global freeze
* is tested once and short-circuits immediately; individual per-account locks
* are checked for every account before the more expensive vault
* pseudo-account recursion begins.
*
* @param view The ledger state to query.
* @param accounts The set of accounts to check.
* @param mptIssue The MPT issuance to check against.
* @param depth Recursion depth guard forwarded to `isVaultPseudoAccountFrozen`.
* @return `true` if the global freeze is set, or if any account carries an
* individual freeze, or if any account is a frozen vault pseudo-account;
* `false` otherwise.
*/
[[nodiscard]] bool
isAnyFrozen(
ReadView const& view,
@@ -42,10 +105,18 @@ isAnyFrozen(
//
//------------------------------------------------------------------------------
/** Returns MPT transfer fee as Rate. Rate specifies
* the fee as fractions of 1 billion. For example, 1% transfer rate
* is represented as 1,010,000,000.
* @param issuanceID MPTokenIssuanceID of MPTTokenIssuance object
/** Convert the `sfTransferFee` field of an MPT issuance to the XRPL `Rate` type.
*
* `sfTransferFee` is a `uint16` in the range 050,000 representing 050%
* (units of 0.001%). The encoding maps to `Rate` via
* `1,000,000,000 + (10,000 × fee)`, so a 50,000 field value becomes
* `1,500,000,000` (50% surcharge over the gross). When `sfTransferFee` is
* absent, `parityRate` (1,000,000,000 — no fee) is returned.
*
* @param view The ledger state to query.
* @param issuanceID The `MPTokenIssuanceID` of the issuance.
* @return The transfer rate as a `Rate` value; `parityRate` when no fee is
* configured or the issuance SLE is absent.
*/
[[nodiscard]] Rate
transferRate(ReadView const& view, MPTID const& issuanceID);
@@ -56,6 +127,18 @@ transferRate(ReadView const& view, MPTID const& issuanceID);
//
//------------------------------------------------------------------------------
/** Read-only pre-check: verify that an independent holding can be created.
*
* Validates two preconditions before `addEmptyHolding` mutates the ledger:
* the `MPTokenIssuance` must exist, and it must carry `lsfMPTCanTransfer`.
* Tokens without `lsfMPTCanTransfer` can only move directly between the
* issuer and counterparties, making independent holdings meaningless.
*
* @param view The ledger state to query.
* @param mptIssue The MPT issuance the caller wants to hold.
* @return `tesSUCCESS`, `tecOBJECT_NOT_FOUND` if the issuance SLE is absent,
* or `tecNO_AUTH` if `lsfMPTCanTransfer` is not set.
*/
[[nodiscard]] TER
canAddHolding(ReadView const& view, MPTIssue const& mptIssue);
@@ -65,6 +148,33 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue);
//
//------------------------------------------------------------------------------
/** Core MPToken SLE lifecycle function — create, delete, or toggle authorization.
*
* Behavior depends on `holderID`:
* - `holderID` absent (`nullopt`): `account` is the holder. Without
* `tfMPTUnauthorize`, a new zero-balance `MPToken` SLE is created and
* inserted into the owner directory; the XRP reserve is enforced when
* `ownerCount >= 2` (same policy as trust lines). With `tfMPTUnauthorize`,
* the existing SLE is erased and the owner count decremented.
* - `holderID` set: `account` must be the issuance's issuer. The function
* toggles `lsfMPTAuthorized` on the holder's existing `MPToken` SLE.
*
* @param view The mutable ledger state.
* @param priorBalance XRP balance before this transaction; used only for the
* reserve check when creating a new holding (`holderID` absent and
* `tfMPTUnauthorize` not set).
* @param mptIssuanceID The issuance being authorized or deauthorized.
* @param account Submitting account: the holder (when `holderID` is absent)
* or the issuer (when `holderID` is set).
* @param journal Logging sink.
* @param flags Transaction flags; `tfMPTUnauthorize` selects the
* delete/deauthorize path.
* @param holderID When set, `account` is the issuer and this is the holder
* whose `lsfMPTAuthorized` flag is toggled.
* @return `tesSUCCESS`, `tecINSUFFICIENT_RESERVE` if reserves are too low,
* `tecDUPLICATE` if the holding already exists, or a `tef` code on
* invariant violations.
*/
[[nodiscard]] TER
authorizeMPToken(
ApplyView& view,
@@ -75,12 +185,31 @@ authorizeMPToken(
std::uint32_t flags = 0,
std::optional<AccountID> holderID = std::nullopt);
/** Check if the account lacks required authorization for MPT.
/** Preclaim (read-only) authorization check for an MPT holding.
*
* requireAuth check is recursive for MPT shares in a vault, descending to
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
* purely defensive, as we currently do not allow such vaults to be created.
* WeakAuth intentionally allows missing MPTokens under MPToken V2.
* Issuers are always authorized. When `featureSingleAssetVault` is active,
* vault and `LoanBroker` pseudo-accounts are implicitly authorized, and the
* check recurses into the vault's underlying asset (bounded by `depth`
* vs. `kMAX_ASSET_CHECK_DEPTH`). Domain-based authorization via
* `credentials::validDomain` takes precedence over `lsfMPTAuthorized` when
* `sfDomainID` is present on the issuance — a passing domain check succeeds
* even if no `MPToken` SLE exists.
*
* `WeakAuth` intentionally permits a missing `MPToken` SLE; used in MPToken
* V2 flows where the SLE is created on demand during apply.
*
* @note The recursion through vault assets is purely defensive; the ledger
* does not currently permit nested-vault MPT configurations.
* @param view The ledger state to query (read-only; called in preclaim).
* @param mptIssue The MPT issuance being accessed.
* @param account The account requesting access.
* @param authType Controls leniency toward missing `MPToken` SLEs;
* `WeakAuth` allows a missing SLE, `StrongAuth`/`Legacy` require it.
* @param depth Current recursion depth; guards against theoretical infinite
* recursion through nested vault configurations.
* @return `tesSUCCESS` if authorized, `tecOBJECT_NOT_FOUND` if the issuance
* is absent, `tecNO_AUTH` if authorization fails, or `tecEXPIRED` if
* domain credentials have expired.
*/
[[nodiscard]] TER
requireAuth(
@@ -90,11 +219,25 @@ requireAuth(
AuthType authType = AuthType::Legacy,
int depth = 0);
/** Enforce account has MPToken to match its authorization.
/** Enforce account has MPToken to match its authorization (doApply phase).
*
* Called from doApply - it will check for expired (and delete if found any)
* credentials matching DomainID set in MPTokenIssuance. Must be called if
* requireAuth(...MPTIssue...) returned tesSUCCESS or tecEXPIRED in preclaim.
* Must be called when `requireAuth` returned `tesSUCCESS` or `tecEXPIRED`
* during preclaim. Re-checks authorization and, if a `sfDomainID` is set on
* the issuance, runs `verifyValidDomain` (which deletes expired credentials
* as a side effect). When domain authorization succeeds but the account has
* no `MPToken` SLE, one is created on the fly using `priorBalance` for the
* XRP reserve check.
*
* @note Must not be called for the issuer account.
* @param view The mutable ledger state (called in doApply).
* @param mptIssuanceID The issuance being accessed.
* @param account The holder account; must not be the issuer.
* @param priorBalance XRP balance before this transaction; used when lazily
* allocating a new `MPToken` SLE for domain-authorized holders.
* @param j Logging sink.
* @return `tesSUCCESS`, `tecNO_AUTH` if not authorized, `tecEXPIRED` if
* credentials have expired, or `tecINSUFFICIENT_RESERVE` if the reserve
* check fails during on-demand SLE creation.
*/
[[nodiscard]] TER
enforceMPTokenAuthorization(
@@ -104,9 +247,20 @@ enforceMPTokenAuthorization(
XRPAmount const& priorBalance,
beast::Journal j);
/** Check if the destination account is allowed
* to receive MPT. Return tecNO_AUTH if it doesn't
* and tesSUCCESS otherwise.
/** Check whether a transfer between two accounts is permitted by the issuance.
*
* When `lsfMPTCanTransfer` is absent, third-party transfers are blocked.
* Transfers where either `from` or `to` is the issuer are always allowed,
* mirroring the IOU trust-line policy that lets issuers send and receive
* their own tokens unconditionally.
*
* @param view The ledger state to query.
* @param mptIssue The MPT issuance involved in the transfer.
* @param from The sending account.
* @param to The receiving account.
* @return `tesSUCCESS` if the transfer is permitted, `tecOBJECT_NOT_FOUND`
* if the issuance SLE is absent, or `tecNO_AUTH` if `lsfMPTCanTransfer`
* is unset and neither endpoint is the issuer.
*/
[[nodiscard]] TER
canTransfer(
@@ -115,8 +269,16 @@ canTransfer(
AccountID const& from,
AccountID const& to);
/** Check if Asset can be traded on DEX. return tecNO_PERMISSION
* if it doesn't and tesSUCCESS otherwise.
/** Check whether an asset may be traded on the DEX.
*
* Dispatches via `asset.visit`: XRP and IOU assets always succeed; for MPT,
* reads the issuance SLE and checks `lsfMPTCanTrade`.
*
* @param view The ledger state to query.
* @param asset The asset to check; non-MPT assets always pass.
* @return `tesSUCCESS` if trading is permitted, `tecOBJECT_NOT_FOUND` if
* the MPT issuance SLE is absent, or `tecNO_PERMISSION` if
* `lsfMPTCanTrade` is not set.
*/
[[nodiscard]] TER
canTrade(ReadView const& view, Asset const& asset);
@@ -127,6 +289,24 @@ canTrade(ReadView const& view, Asset const& asset);
//
//------------------------------------------------------------------------------
/** Create a zero-balance `MPToken` holding for `accountID`.
*
* Short-circuits to `tesSUCCESS` when the caller is the issuer — issuers
* never hold a `MPToken` SLE for their own issuance. For all other accounts,
* delegates to `authorizeMPToken`, which enforces the XRP reserve requirement
* and inserts the SLE into the owner directory. Returns `tefINTERNAL` if the
* issuance SLE is missing or globally locked (invariant violations).
*
* @param view The mutable ledger state.
* @param accountID The account requesting the holding.
* @param priorBalance XRP balance before this transaction; forwarded to
* `authorizeMPToken` for the reserve check.
* @param mptIssue The MPT issuance to hold.
* @param journal Logging sink.
* @return `tesSUCCESS`, `tecDUPLICATE` if a holding already exists,
* `tecINSUFFICIENT_RESERVE` if reserves are too low, or `tefINTERNAL`
* on issuance-state invariant violations.
*/
[[nodiscard]] TER
addEmptyHolding(
ApplyView& view,
@@ -135,6 +315,23 @@ addEmptyHolding(
MPTIssue const& mptIssue,
beast::Journal journal);
/** Delete a zero-balance `MPToken` holding.
*
* Requires `sfMPTAmount` to be zero and, when `fixCleanup3_1_3` is enabled,
* `sfLockedAmount` to be zero as well; returns `tecHAS_OBLIGATIONS` otherwise.
* When `accountID` is the issuer and no `MPToken` SLE exists, returns
* `tesSUCCESS` immediately — the normal issuer state. Otherwise delegates to
* `authorizeMPToken` with `tfMPTUnauthorize` to erase the SLE and decrement
* the owner count.
*
* @param view The mutable ledger state.
* @param accountID The account whose holding is being removed.
* @param mptIssue The MPT issuance.
* @param journal Logging sink.
* @return `tesSUCCESS`, `tecOBJECT_NOT_FOUND` if no holding exists (and
* caller is not the issuer), or `tecHAS_OBLIGATIONS` if the holding
* carries a non-zero balance or locked amount.
*/
[[nodiscard]] TER
removeEmptyHolding(
ApplyView& view,
@@ -148,6 +345,22 @@ removeEmptyHolding(
//
//------------------------------------------------------------------------------
/** Move MPT funds from a holder's spendable balance into escrow.
*
* Decrements `sfMPTAmount` and increments `sfLockedAmount` on the sender's
* `MPToken` SLE, then increments `sfLockedAmount` on the `MPTokenIssuance`
* SLE. `sfOutstandingAmount` on the issuance is deliberately left unchanged —
* escrowed tokens remain outstanding until the escrow completes and the
* recipient actually receives them. All arithmetic is guarded by
* `canSubtract`/`canAdd`.
*
* @param view The mutable ledger state.
* @param uGrantorID The account placing tokens in escrow; must not be the issuer.
* @param saAmount The MPT amount to lock; must be a valid `MPTIssue` amount.
* @param j Logging sink.
* @return `tesSUCCESS`, or a `tec`/`tef` error if the issuance or `MPToken`
* SLE is missing, the sender is the issuer, or an arithmetic guard fires.
*/
TER
lockEscrowMPT(
ApplyView& view,
@@ -155,6 +368,28 @@ lockEscrowMPT(
STAmount const& saAmount,
beast::Journal j);
/** Release MPT funds from escrow and credit the recipient.
*
* Decrements `sfLockedAmount` on both the sender's `MPToken` SLE and the
* `MPTokenIssuance` SLE by `grossAmount`. Then, depending on the receiver:
* - Receiver is a third party: `sfMPTAmount` on the receiver's `MPToken` is
* incremented by `netAmount`.
* - Receiver is the issuer: `sfOutstandingAmount` on the issuance is
* decremented by `netAmount` — tokens return to the issuer and retire.
* When `fixTokenEscrowV1` is enabled and `grossAmount > netAmount`, the fee
* difference is additionally subtracted from `sfOutstandingAmount` because
* the fee tokens are effectively burned. All arithmetic is guarded by
* `canSubtract`/`canAdd`.
*
* @param view The mutable ledger state.
* @param uGrantorID The escrow grantor; must not be the issuer.
* @param uGranteeID The escrow grantee (may be the issuer).
* @param netAmount The MPT amount credited to the receiver after fees.
* @param grossAmount The MPT amount unlocked from escrow (>= `netAmount`).
* @param j Logging sink.
* @return `tesSUCCESS`, or a `tec`/`tef` error on missing SLEs or
* arithmetic guard failure.
*/
TER
unlockEscrowMPT(
ApplyView& view,
@@ -164,6 +399,18 @@ unlockEscrowMPT(
STAmount const& grossAmount,
beast::Journal j);
/** Low-level primitive: insert a new `MPToken` SLE and link it into the owner directory.
*
* Inserts the SLE unconditionally without checking for duplicates, enforcing
* reserves, or verifying issuance validity. Callers must perform those checks
* before invoking this function.
*
* @param view The mutable ledger state.
* @param mptIssuanceID The issuance the token belongs to.
* @param account The account that will own the `MPToken`.
* @param flags Initial `sfFlags` value for the new SLE.
* @return `tesSUCCESS`, or `tecDIR_FULL` if the owner directory is full.
*/
TER
createMPToken(
ApplyView& view,
@@ -171,6 +418,21 @@ createMPToken(
AccountID const& account,
std::uint32_t const flags);
/** Idempotently ensure a `MPToken` holding exists for `holder`.
*
* Succeeds immediately if `holder` is the issuer or if the `MPToken` SLE
* already exists. Otherwise calls `createMPToken` and increments the owner
* count. Suitable for apply-phase callers that need to auto-create a holding
* without the full reserve and issuance validity checks performed by
* `addEmptyHolding`.
*
* @param view The mutable ledger state.
* @param mptIssue The MPT issuance the holder will hold.
* @param holder The account to receive the holding.
* @param j Logging sink.
* @return `tesSUCCESS`, `tecDIR_FULL` if the owner directory is full, or
* `tecINTERNAL` if the holder's account SLE is missing.
*/
TER
checkCreateMPT(
xrpl::ApplyView& view,
@@ -184,25 +446,62 @@ checkCreateMPT(
//
//------------------------------------------------------------------------------
// MaximumAmount doesn't exceed 2**63-1
/** Return the configured supply cap for an MPT issuance.
*
* Returns `sfMaximumAmount` when present, or `kMAX_MP_TOKEN_AMOUNT` (2^631)
* when the field is absent, representing an uncapped issuance. The result is
* always non-negative and fits in a `std::int64_t`.
*
* @param sleIssuance The `MPTokenIssuance` SLE to query.
* @return The maximum allowed outstanding amount.
*/
std::int64_t
maxMPTAmount(SLE const& sleIssuance);
// OutstandingAmount may overflow and available amount might be negative.
// But available amount is always <= |MaximumAmount - OutstandingAmount|.
/** Compute remaining issuance headroom from a pre-read SLE.
*
* Returns `maxMPTAmount(sleIssuance) - sfOutstandingAmount`. May transiently
* be negative when the payment engine is processing a path step that
* temporarily exceeds `MaximumAmount` under `AllowMPTOverflow::Yes`.
*
* @param sleIssuance The `MPTokenIssuance` SLE to query.
* @return Headroom as a signed 64-bit integer; may be negative.
*/
std::int64_t
availableMPTAmount(SLE const& sleIssuance);
/** Compute remaining issuance headroom by reading the SLE from the view.
*
* Convenience overload that performs the SLE lookup. Throws
* `std::runtime_error` if the issuance SLE is absent — a missing issuance at
* this call site indicates a ledger consistency failure rather than a user
* error.
*
* @param view The ledger state to query.
* @param mptID The `MPTID` of the issuance.
* @return Headroom as a signed 64-bit integer; may be negative.
* @throws std::runtime_error if the `MPTokenIssuance` SLE is absent.
*/
std::int64_t
availableMPTAmount(ReadView const& view, MPTID const& mptID);
/** Checks for two types of OutstandingAmount overflow during a send operation.
* 1. **Direct directSendNoFee (Overflow: No):** A true overflow check when
* `OutstandingAmount > MaximumAmount`. This threshold is used for direct
* directSendNoFee transactions that bypass the payment engine.
* 2. **accountSend & Payment Engine (Overflow: Yes):** A temporary overflow
* check when `OutstandingAmount > UINT64_MAX`. This higher threshold is used
* for `accountSend` and payments processed via the payment engine.
/** Check whether crediting `sendAmount` would overflow the outstanding supply.
*
* Two distinct overflow thresholds are applied based on `allowOverflow`:
* 1. **`AllowMPTOverflow::No` (direct send):** Enforces the strict cap
* `OutstandingAmount + sendAmount ≤ MaximumAmount`. Used by
* `directSendNoFee` transactions that bypass the payment engine.
* 2. **`AllowMPTOverflow::Yes` (payment engine):** Raises the effective
* ceiling to `UINT64_MAX` to allow transient in-flight values that exceed
* `MaximumAmount` during path routing. A matching redemption step in the
* same transaction collapses the overshoot before settlement.
*
* @param sendAmount The proposed additional issuance; must be non-negative.
* @param outstandingAmount Current `sfOutstandingAmount` from the issuance SLE.
* @param maximumAmount The configured cap (`sfMaximumAmount` or
* `kMAX_MP_TOKEN_AMOUNT`).
* @param allowOverflow Selects which ceiling to apply.
* @return `true` if adding `sendAmount` would exceed the applicable limit.
*/
bool
isMPTOverflow(
@@ -211,18 +510,33 @@ isMPTOverflow(
std::int64_t maximumAmount,
AllowMPTOverflow allowOverflow);
/**
* Determine funds available for an issuer to sell in an issuer owned offer.
* Issuing step, which could be either MPTEndPointStep last step or BookStep's
* TakerPays may overflow OutstandingAmount. Redeeming step, in BookStep's
* TakerGets redeems the offer's owner funds, essentially balancing out
* the overflow, unless the offer's owner is the issuer.
/** Determine funds available for an issuer to sell in an issuer-owned DEX offer.
*
* During an issuing step (outbound from the issuer), the issuer's
* "available" balance is the remaining issuance headroom (`availableMPTAmount`)
* adjusted by `balanceHookSelfIssueMPT` to account for any amount already
* sold within the same payment. Without this hook, offer-crossing could
* allow the issuer to exceed `sfMaximumAmount` across parallel paths in the
* same transaction.
*
* @param view The ledger state to query.
* @param issue The MPT issuance for which to compute issuer funds.
* @return The effective amount the issuer can sell; zero if the issuance SLE
* is absent.
*/
[[nodiscard]] STAmount
issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue);
/** Facilitate tracking of MPT sold by an issuer owning MPT sell offer.
* See ApplyView::issuerSelfDebitHookMPT().
/** Track MPT sold by an issuer that owns an MPT sell offer.
*
* Records the cumulative amount sold during the current payment step so that
* subsequent calls to `issuerFundsToSelfIssue` return a correctly reduced
* available balance. Delegates to `ApplyView::issuerSelfDebitHookMPT` after
* computing the current issuance headroom.
*
* @param view The mutable ledger state.
* @param issue The MPT issuance being sold.
* @param amount The additional amount sold in this step.
*/
void
issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amount);
@@ -233,9 +547,26 @@ issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amo
//
//------------------------------------------------------------------------------
/* Return true if a transaction is allowed for the specified MPT/account. The
* function checks MPTokenIssuance and MPToken objects flags to determine if the
* transaction is allowed.
/** Comprehensive MPT transaction permission check for DEX and payment types.
*
* Verifies in order: the issuer account exists, the `MPTokenIssuance` SLE
* exists, the issuance is not globally locked (`lsfMPTLocked`), the
* `lsfMPTCanTrade` flag is set, and — for non-issuer accounts — that
* `lsfMPTCanTransfer` is set and the account's own `MPToken` is not
* individually locked. A missing `MPToken` SLE for a non-issuer is treated
* as passing: some transaction types create the `MPToken` on demand and
* perform their own missing-token checks.
*
* @note Must not be called with `txType == ttPAYMENT`; use the payment-engine
* path's own checks for payments.
* @param v The ledger state to query.
* @param tx The transaction type being gated.
* @param asset The asset involved; non-MPT assets always succeed.
* @param accountID The account initiating the transaction.
* @return `tesSUCCESS`, `tecOBJECT_NOT_FOUND` if the issuance is absent,
* `tecNO_ISSUER` if the issuer account is gone, `tecLOCKED` if the
* issuance or account is frozen, or `tecNO_PERMISSION` if trading or
* transfer is not permitted.
*/
TER
checkMPTTxAllowed(ReadView const& v, TxType tx, Asset const& asset, AccountID const& accountID);

View File

@@ -1,3 +1,20 @@
/**
* @file NFTokenHelpers.h
* @brief Core helpers for NFT paged-directory and offer management.
*
* Declares all mutable and read-only operations on the NFToken paged-directory
* structure and offer queues. Every transaction that touches an NFToken —
* minting, burning, transferring, or creating/cancelling offers — calls these
* helpers rather than manipulating ledger state directly.
*
* @note NFTs are packed into doubly-linked `ltNFTOKEN_PAGE` SLEs, each
* holding up to `kDIR_MAX_TOKENS_PER_PAGE` (32) tokens sorted by
* `compareTokens()`. Tokens sharing the same low-96-bit masked value
* (issuer + taxon) are *equivalent* and must be collocated on the same
* page. Page key invariant: every token's low 96 bits are strictly less
* than the low 96 bits of its enclosing page key.
*/
#pragma once
#include <xrpl/basics/Log.h>
@@ -13,18 +30,48 @@
namespace xrpl::nft {
/** Delete up to a specified number of offers from the specified token offer
* directory. */
* directory.
*
* Iterates the directory page-by-page, deleting offers in reverse index order
* within each page. Reverse iteration is required because `sfIndexes` is
* vector-backed and forward deletion would corrupt the remaining indices.
* Stops as soon as `maxDeletableOffers` offers have been removed.
*
* @param view The apply view to mutate.
* @param directory Keylet of the NFT buy or sell offer directory to drain.
* @param maxDeletableOffers Maximum number of offers to remove in this call.
* @return The number of offers actually deleted.
* @note Returns 0 immediately if `maxDeletableOffers` is 0. Used by
* `NFTokenBurn` to drain open offers within the per-transaction
* deletion cap (`maxDeletableTokenOfferEntries`).
*/
std::size_t
removeTokenOffersWithLimit(
ApplyView& view,
Keylet const& directory,
std::size_t maxDeletableOffers);
/** Finds the specified token in the owner's token directory. */
/** Finds the specified token in the owner's token directory.
*
* Read-only traversal: locates the `ltNFTOKEN_PAGE` candidate via `succ()`
* and searches the page's `sfNFTokens` array for a matching `sfNFTokenID`.
*
* @param view The read-only view to query.
* @param owner The account whose NFT directory is searched.
* @param nftokenID The 256-bit NFT identifier to look up.
* @return The matching token `STObject`, or `std::nullopt` if not found.
* @see findTokenAndPage for the mutable overload that also returns the page.
*/
std::optional<STObject>
findToken(ReadView const& view, AccountID const& owner, uint256 const& nftokenID);
/** Finds the token in the owner's token directory. Returns token and page. */
/** Token and its containing page, returned by `findTokenAndPage()`.
*
* Bundles the located token `STObject` with the mutable `shared_ptr<SLE>`
* page so callers can modify the token in place without a second ledger
* traversal. The page pointer must be used exclusively on the same
* `ApplyView` that produced it.
*/
struct TokenAndPage
{
STObject token;
@@ -35,17 +82,81 @@ struct TokenAndPage
{
}
};
/** Finds the token in the owner's token directory and returns it with its page.
*
* Mutable traversal via `ApplyView::peek()`. Returns both the token
* `STObject` and the `shared_ptr<SLE>` page so that callers such as
* `NFTokenAcceptOffer` can pass the page directly to `removeToken()`,
* avoiding a redundant page lookup.
*
* @param view The apply view to query (mutable; uses `peek()`).
* @param owner The account whose NFT directory is searched.
* @param nftokenID The 256-bit NFT identifier to look up.
* @return A `TokenAndPage` containing the token and its page, or
* `std::nullopt` if the token is not found.
* @see findToken for the read-only alternative that returns only the token.
*/
std::optional<TokenAndPage>
findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftokenID);
/** Insert the token in the owner's token directory. */
/** Insert the token in the owner's token directory.
*
* Locates or creates the appropriate `ltNFTOKEN_PAGE` via `getPageForToken()`.
* If the target page is full, it is split to make room; each split increments
* the owner's reserve count. Tokens are kept sorted within a page by
* `compareTokens()` (low 96-bit key first, full ID as tiebreaker).
*
* @param view The apply view to mutate.
* @param owner The account that will own the token.
* @param nft The token `STObject` to insert; must contain `sfNFTokenID`.
* @return `tesSUCCESS` on success, or `tecNO_SUITABLE_NFTOKEN_PAGE` if the
* target page is entirely filled with equivalent tokens (same low 96-bit
* key) and no split is possible.
*/
TER
insertToken(ApplyView& view, AccountID owner, STObject&& nft);
/** Remove the token from the owner's token directory. */
/** Remove the token from the owner's token directory.
*
* Page-discovery overload: locates the containing `ltNFTOKEN_PAGE` via
* `succ()` and then delegates to the two-argument form. Use this when
* the caller does not already hold a page reference.
*
* After erasure, attempts to merge the affected page with its neighbours;
* each successful merge credits one reserve. If the page becomes empty it
* is unlinked and erased.
*
* @param view The apply view to mutate.
* @param owner The account that currently holds the token.
* @param nftokenID The 256-bit NFT identifier to remove.
* @return `tesSUCCESS`, or `tecNO_ENTRY` if the page or token cannot be
* found.
* @see removeToken(ApplyView&, AccountID const&, uint256 const&, shared_ptr<SLE> const&)
* for the overload that skips the page lookup.
*/
TER
removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID);
/** Remove the token from the owner's token directory using a pre-located page.
*
* Caller-supplied page overload: skips the `succ()`-based page lookup when
* the caller already holds the page (e.g., from `findTokenAndPage()`).
* The `page` pointer must have been obtained from the same `ApplyView`
* instance.
*
* Under the `fixNFTokenPageLinks` amendment, if the emptied page is the final
* anchor page (`nftpage_max`), its contents are replaced with those of the
* previous page and the now-empty previous page is erased, preserving the
* invariant that the last page always has the stable sentinel key.
*
* @param view The apply view to mutate.
* @param owner The account that currently holds the token.
* @param nftokenID The 256-bit NFT identifier to remove.
* @param page The mutable SLE page known to contain the token.
* @return `tesSUCCESS`, or `tecNO_ENTRY` if the token is not found on the
* supplied page.
*/
TER
removeToken(
ApplyView& view,
@@ -53,28 +164,74 @@ removeToken(
uint256 const& nftokenID,
std::shared_ptr<SLE> const& page);
/** Deletes the given token offer.
An offer is tracked in two separate places:
- The token's 'buy' directory, if it's a buy offer; or
- The token's 'sell' directory, if it's a sell offer; and
- The owner directory of the account that placed the offer.
The offer also consumes one incremental reserve.
/** Deletes the given token offer and removes it from both tracking directories.
*
* An offer is tracked in two separate places:
* - The token's `nft_buys` directory, if it is a buy offer; or
* - The token's `nft_sells` directory, if it is a sell offer; and
* - The owner's owner directory.
*
* Both directory entries are removed, the owner's reserve count is
* decremented by one, and the offer SLE is erased.
*
* @param view The apply view to mutate.
* @param offer The SLE for the offer to delete; must be of type
* `ltNFTOKEN_OFFER`.
* @return `true` if the offer was successfully deleted; `false` if the SLE
* is not of type `ltNFTOKEN_OFFER` or if a directory removal fails,
* acting as a type-safety guard.
*/
bool
deleteTokenOffer(ApplyView& view, std::shared_ptr<SLE> const& offer);
/** Repairs the links in an NFTokenPage directory.
Returns true if a repair took place, otherwise false.
*/
/** Repairs the links in an NFToken page directory.
*
* Walks the entire `ltNFTOKEN_PAGE` chain for the owner and corrects any
* broken `sfNextPageMin` / `sfPreviousPageMin` links. If the final page does
* not have the expected `nftpage_max` sentinel key, its contents are migrated
* to a newly created SLE with the correct key, the old SLE is erased, and the
* chain is relinked. Owner count is unchanged by this operation because the
* page count is preserved.
*
* Intended to be called by the `LedgerStateFix` transaction on accounts with
* known directory corruption.
*
* @param view The apply view to mutate.
* @param owner The account whose NFToken page directory is to be repaired.
* @return `true` if any correction was applied; `false` if the directory was
* already consistent.
*/
bool
repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner);
/** Ordering predicate for NFToken IDs within and across pages.
*
* Sorts first by the low 96 bits of each ID (the `pageMask` region that
* determines page placement), then by the full 256-bit value as a
* tiebreaker. This ensures deterministic ordering for tokens that share
* the same low 96-bit prefix (equivalent tokens) and must co-reside on
* a single page.
*
* @param a First NFToken ID.
* @param b Second NFToken ID.
* @return `true` if `a` sorts before `b`.
*/
bool
compareTokens(uint256 const& a, uint256 const& b);
/** Modify the URI of an existing NFToken in the owner's directory.
*
* Locates the token's page and updates the `sfURI` field in the token's
* `STObject` within the page's `sfNFTokens` array. If `uri` is
* `std::nullopt`, the `sfURI` field is removed from the token.
*
* @param view The apply view to mutate.
* @param owner The account that owns the token.
* @param nftokenID The 256-bit NFT identifier whose URI is to be changed.
* @param uri The new URI value, or `std::nullopt` to clear the URI.
* @return `tesSUCCESS` on success, or `tecINTERNAL` if the page or token
* cannot be located (indicates ledger inconsistency).
*/
TER
changeTokenURI(
ApplyView& view,
@@ -82,7 +239,33 @@ changeTokenURI(
uint256 const& nftokenID,
std::optional<xrpl::Slice> const& uri);
/** Preflight checks shared by NFTokenCreateOffer and NFTokenMint */
/** Preflight checks shared by NFTokenCreateOffer and NFTokenMint.
*
* Validates offer parameters that require no ledger access: negative or
* zero amounts (buy offers must carry a non-zero amount), zero IOU amounts,
* zero expiration, and malformed `owner`/`destination` combinations.
* A buy offer must supply `owner` (the targeted token holder); a sell offer
* must not (the seller is implicit). Neither party may designate itself as
* the destination.
*
* Defaults (`owner = nullopt`, `txFlags = tfSellNFToken`) allow
* `NFTokenMint` to reuse this path with minimal adaptation.
*
* @param acctID Account executing the transaction.
* @param amount The offer amount; must be non-negative and, for buy offers,
* non-zero and non-zero for IOUs.
* @param dest Optional destination account that may exclusively accept the
* offer; must not equal `acctID`.
* @param expiration Optional offer expiration; must not be zero.
* @param nftFlags The flags field of the NFToken being offered.
* @param rules Current ledger rule set used for amendment checks.
* @param owner For buy offers, the account that currently holds the token;
* must be absent for sell offers.
* @param txFlags Transaction flags; `tfSellNFToken` distinguishes sell from
* buy.
* @return `tesSUCCESS` if all static checks pass, or a `temXXX` error code
* indicating which parameter is invalid.
*/
NotTEC
tokenOfferCreatePreflight(
AccountID const& acctID,
@@ -94,7 +277,37 @@ tokenOfferCreatePreflight(
std::optional<AccountID> const& owner = std::nullopt,
std::uint32_t txFlags = tfSellNFToken);
/** Preclaim checks shared by NFTokenCreateOffer and NFTokenMint */
/** Preclaim checks shared by NFTokenCreateOffer and NFTokenMint.
*
* Accesses the ledger to validate conditions that cannot be checked
* statically:
* - For non-XRP offers on tokens without `flagCreateTrustLines`, verifies
* that the NFT issuer's trust line for the IOU exists and is not frozen.
* Under `featureNFTokenMintOffer`, an issuer selling their own currency is
* exempt from this check.
* - Enforces `flagTransferable`: if absent and the transacting account is
* neither the issuer nor the current `sfNFTokenMinter`, returns
* `tefNFTOKEN_IS_NOT_TRANSFERABLE`.
* - For buy offers, verifies the account currently has sufficient funds.
* - Verifies `dest` and `owner` accounts exist and have not set
* `lsfDisallowIncomingNFTokenOffer`.
* - Under `fixEnforceNFTokenTrustlineV2`, calls `checkTrustlineAuthorized()`
* to reject offers backed by unauthorized trust lines that carry a balance.
*
* @param view The read-only ledger view.
* @param acctID Account executing the transaction.
* @param nftIssuer Issuer encoded in the NFToken ID.
* @param amount The offer amount.
* @param dest Optional restricted destination account.
* @param nftFlags The flags field of the NFToken being offered.
* @param xferFee Transfer fee encoded in the NFToken ID (basis points).
* @param j Journal for diagnostic logging.
* @param owner For buy offers, the account that currently holds the token.
* @param txFlags Transaction flags; `tfSellNFToken` distinguishes sell from
* buy.
* @return `tesSUCCESS` if all ledger-state checks pass, or a `tecXXX` /
* `tefXXX` error code.
*/
TER
tokenOfferCreatePreclaim(
ReadView const& view,
@@ -108,7 +321,28 @@ tokenOfferCreatePreclaim(
std::optional<AccountID> const& owner = std::nullopt,
std::uint32_t txFlags = tfSellNFToken);
/** doApply implementation shared by NFTokenCreateOffer and NFTokenMint */
/** doApply implementation shared by NFTokenCreateOffer and NFTokenMint.
*
* Reserves XRP for the new `ltNFTOKEN_OFFER` object, inserts the offer into
* the account's owner directory and into the token's buy or sell directory
* (determined by `tfSellNFToken` in `txFlags`), constructs the SLE with the
* supplied fields, and increments the owner count.
*
* @param view The apply view to mutate.
* @param acctID Account executing the transaction.
* @param amount The offer amount.
* @param dest Optional restricted destination account.
* @param expiration Optional expiration time for the offer.
* @param seqProxy Sequence or ticket proxy used to derive the offer keylet.
* @param nftokenID The 256-bit ID of the NFToken being offered.
* @param priorBalance The account's XRP balance before the transaction fee
* was deducted; used to verify the reserve requirement.
* @param j Journal for diagnostic logging.
* @param txFlags Transaction flags; `tfSellNFToken` controls offer direction.
* @return `tesSUCCESS` on success, `tecINSUFFICIENT_RESERVE` if the account
* cannot cover the new object reserve, or `tecDIR_FULL` if either
* directory is at capacity.
*/
TER
tokenOfferCreateApply(
ApplyView& view,
@@ -122,6 +356,25 @@ tokenOfferCreateApply(
beast::Journal j,
std::uint32_t txFlags = tfSellNFToken);
/** Verify that an account is authorized to hold a given IOU trust line.
*
* Only active under the `fixEnforceNFTokenTrustlineV2` amendment; returns
* `tesSUCCESS` unconditionally when the amendment is not enabled.
*
* When active, checks that if the IOU issuer requires authorization
* (`lsfRequireAuth`), the trust line between `id` and the issuer exists and
* carries the appropriate `lsfLowAuth` / `lsfHighAuth` flag. The issuer
* account is always considered authorized to hold its own issuance.
*
* @param view The read-only ledger view.
* @param id The account whose authorization is being verified.
* @param j Journal for diagnostic logging.
* @param issue The IOU issue (currency + issuer) to check; must not be XRP.
* @return `tesSUCCESS` if authorized, `tecNO_ISSUER` if the issuer account
* does not exist, `tecNO_LINE` if the required trust line is absent, or
* `tecNO_AUTH` if the trust line exists but is not authorized.
* @note Only valid for custom (non-XRP) currencies; asserts otherwise.
*/
TER
checkTrustlineAuthorized(
ReadView const& view,
@@ -129,6 +382,26 @@ checkTrustlineAuthorized(
beast::Journal const j,
Issue const& issue);
/** Verify that an IOU trust line is not deep-frozen for a given account.
*
* Only active under the `featureDeepFreeze` amendment; returns
* `tesSUCCESS` unconditionally when the amendment is not enabled.
*
* When active, checks whether the trust line between `id` and the IOU issuer
* carries either `lsfLowDeepFreeze` or `lsfHighDeepFreeze`. Either side
* enacting deep freeze blocks token receipt, regardless of which party set it.
* The issuer account is always permitted to accept its own issuance; accounts
* with no trust line are treated as not frozen.
*
* @param view The read-only ledger view.
* @param id The account whose deep-freeze status is being checked.
* @param j Journal for diagnostic logging.
* @param issue The IOU issue (currency + issuer) to check; must not be XRP.
* @return `tesSUCCESS` if not deep-frozen or if no trust line exists,
* `tecNO_ISSUER` if the issuer account does not exist, or `tecFROZEN`
* if the trust line is deep-frozen.
* @note Only valid for custom (non-XRP) currencies; asserts otherwise.
*/
TER
checkTrustlineDeepFrozen(
ReadView const& view,

View File

@@ -9,18 +9,38 @@
namespace xrpl {
/** Delete an offer.
Requirements:
The offer must exist.
The caller must have already checked permissions.
@param view The ApplyView to modify.
@param sle The offer to delete.
@param j Journal for logging.
@return tesSUCCESS on success, otherwise an error code.
*/
/** Remove an offer and its directory back-references from the ledger.
*
* Performs the full teardown sequence atomically within the transaction
* buffer: removes the offer from the owner's directory, removes it from
* the order-book quality directory, decrements the owner's reserve count,
* and erases the SLE. For hybrid offers (flagged `lsfHybrid`) that
* participate in one or more Permissioned DEX domains, each entry in
* `sfAdditionalBooks` is also removed from its domain-specific book
* directory before the owner-count adjustment and erasure.
*
* If `sle` is null the function returns `tesSUCCESS` immediately,
* allowing callers to pass the result of a failed `peek()` without
* a pre-check (defensive against double-delete within one batch).
*
* @pre The offer SLE must exist in the ledger and both its
* `sfOwnerNode` and `sfBookNode` back-references must be valid.
* @pre The caller must have already verified that the submitting
* account is authorized to delete this offer; this function
* performs no ownership or permission check.
*
* @param view The `ApplyView` transaction buffer to modify.
* @param sle The offer SLE to delete. May be null (treated as no-op).
* @param j Journal for diagnostic logging.
*
* @return `tesSUCCESS` on success, or `tefBAD_LEDGER` if a directory
* back-reference is missing (invariant violation; should not occur
* in a well-formed ledger).
*
* @note `[[nodiscard]]` is intentionally absent: `BookTip` and payment
* path callers do not always inspect the return value, and enforcing
* the attribute would have broken compilation across the engine.
*/
// [[nodiscard]] // nodiscard commented out so Flow, BookTip and others compile.
TER
offerDelete(ApplyView& view, std::shared_ptr<SLE> const& sle, beast::Journal j);

View File

@@ -7,6 +7,35 @@
namespace xrpl {
/** Tear down a payment channel and return unspent XRP to its source account.
*
* Performs four ledger mutations in order:
* 1. Removes the channel from the source's owner directory (`sfOwnerNode`).
* 2. Conditionally removes the channel from the destination's owner directory
* (`sfDestinationNode`) — the field is absent on older channel objects that
* predate destination-directory tracking, so its presence is tested before
* the removal attempt.
* 3. Credits the unspent balance (`sfAmount - sfBalance`) back to the source
* account. `sfAmount` is the total XRP escrowed; `sfBalance` is the
* cumulative amount already paid to the destination.
* 4. Decrements the source's owner count and erases the `ltPAYCHAN` SLE.
*
* Called by both `PaymentChannelClaim` and `PaymentChannelFund` whenever a
* channel must be closed — on expiry (`cancelAfter`/`expiration` elapsed), on
* an explicit `tfClose` flag, or when the channel is fully drained.
*
* @param slep The `ltPAYCHAN` SLE to close; must satisfy
* `sfAmount >= sfBalance` (asserted).
* @param view The apply view through which all ledger mutations are made.
* @param key The ledger key of the channel SLE (used for directory removal).
* @param j Journal for fatal-level diagnostic messages on internal errors.
* @return `tesSUCCESS` on the normal path; `tefBAD_LEDGER` if an owner
* directory removal fails (indicates corrupted ledger state);
* `tefINTERNAL` if the source account SLE cannot be found.
* @note The `tefBAD_LEDGER` and `tefINTERNAL` branches are annotated
* `LCOV_EXCL` — they guard against ledger corruption that cannot occur
* during correct operation.
*/
TER
closeChannel(
std::shared_ptr<SLE> const& slep,

View File

@@ -1,13 +1,90 @@
/**
* @file PermissionedDEXHelpers.h
* @brief Domain membership predicates for the Permissioned DEX.
*
* Declares the two authorization gatekeepers used by `xrpl::permissioned_dex`
* to enforce credential-based access control on restricted order books.
* Both functions are called from transaction preclaim logic and from live
* order-book traversal in `OfferStream`.
*/
#pragma once
#include <xrpl/ledger/View.h>
namespace xrpl::permissioned_dex {
// Check if an account is in a permissioned domain
/**
* @brief Test whether an account currently qualifies as a member of a
* permissioned domain.
*
* Resolves the `PermissionedDomain` ledger object identified by @p domainID
* and applies a two-tier membership test:
*
* 1. **Owner shortcut** — the domain's `sfOwner` is always considered a member,
* avoiding a bootstrap problem where the owner couldn't trade in their own
* domain.
* 2. **Credential scan** — for all other accounts, the function iterates
* `sfAcceptedCredentials` and returns `true` as soon as it finds a
* credential issued to @p account that (a) carries the `lsfAccepted` flag
* and (b) has not expired according to `credentials::checkExpired` evaluated
* against the ledger's `parentCloseTime`.
*
* Expiry is evaluated against `parentCloseTime` (not wall time) so that all
* validators reach the same deterministic result regardless of local clock skew.
*
* @param view The read-only ledger view to query.
* @param account The account whose domain membership is being tested.
* @param domainID The identifier of the `PermissionedDomain` ledger object.
* @return `true` if @p account is the domain owner or holds at least one
* accepted, non-expired credential listed in the domain; `false` if the
* domain object does not exist, or if no qualifying credential is found.
*
* @note Called from `OfferCreate` preclaim (rejects with `tecNO_PERMISSION` if
* `false`) and twice from `Payment` preclaim — once for the sender, once for
* the destination — since a domain payment requires both parties to be
* members. Also called internally by `offerInDomain`.
*/
[[nodiscard]] bool
accountInDomain(ReadView const& view, AccountID const& account, Domain const& domainID);
// Check if an offer is in the permissioned domain
/**
* @brief Test whether a specific offer is still legitimately part of a
* permissioned domain at the time it is being consumed.
*
* Called by `OfferStream` during order-book traversal to handle the race
* between offer creation and subsequent credential expiry. An offer that was
* valid when placed may become invalid if the owner's credentials expire before
* the offer is matched. When this function returns `false`, `OfferStream`
* removes the offer from the book immediately (`permRmOffer`) instead of
* matching it.
*
* The function performs the following checks in order:
* - Offer SLE must exist (defensive; should not occur in a well-formed book).
* - Offer must carry `sfDomainID` (defensive; should not occur).
* - `sfDomainID` must match @p domainID (defensive; should not occur).
* - **Post-`fixCleanup3_1_3`**: a hybrid offer (`lsfHybrid`) must have
* `sfAdditionalBooks` present with exactly one entry; a violation is logged
* as an error and `false` is returned.
* - **Pre-`fixCleanup3_1_3`**: a hybrid offer must have `sfAdditionalBooks`
* present (size is not validated).
* - Delegates the final membership check to `accountInDomain` for the offer's
* owner (`sfAccount`).
*
* The three defensive checks are marked `LCOV_EXCL_LINE`; they guard against
* invariant violations that cannot occur under normal operation but are retained
* as safety nets.
*
* @param view The read-only ledger view to query.
* @param offerID The hash identifier of the offer SLE to validate.
* @param domainID The permissioned domain the offer is expected to belong to.
* @param j Journal used to log an error if a hybrid offer has a missing
* or malformed `sfAdditionalBooks` field.
* @return `true` if the offer passes all structural checks and its owner is
* currently a member of @p domainID; `false` otherwise.
*
* @note The `fixCleanup3_1_3` amendment tightens hybrid-offer validation from
* a presence-only check on `sfAdditionalBooks` to a presence-plus-size-one
* check. Both code paths must be preserved for deterministic historic replay.
*/
[[nodiscard]] bool
offerInDomain(
ReadView const& view,

View File

@@ -1,3 +1,23 @@
/** @file
* IOU trustline (RippleState) operations for the XRP Ledger.
*
* Declares every ledger operation that reads from or writes to a
* `RippleState` (trustline) SLE: credit-limit and balance queries,
* freeze checks, trustline lifecycle, IOU issuance/redemption,
* authorization and rippling enforcement, zero-balance holding
* management, and AMM-specific cleanup.
*
* This file is the IOU-specific leaf of the token helper layer.
* Asset-agnostic callers should go through the dispatchers in
* `TokenHelpers.h`, which branch on `Issue` vs `MPTIssue` and
* delegate here for the IOU path.
*
* @note The trustline orientation invariant is pervasive here:
* `sfLowLimit` always belongs to the account whose `AccountID`
* compares less; `sfHighLimit` to the other. Every function
* applies this flip internally — callers supply `(account, issuer)`
* and receive results in account-centric terms.
*/
#pragma once
#include <xrpl/beast/utility/Journal.h>
@@ -10,27 +30,29 @@
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/TER.h>
//------------------------------------------------------------------------------
//
// RippleState (Trustline) helpers
//
//------------------------------------------------------------------------------
// --- RippleState (Trustline) helpers ---
namespace xrpl {
//------------------------------------------------------------------------------
//
// Credit functions (from Credit.h)
//
//------------------------------------------------------------------------------
// --- Credit queries ---
/** Calculate the maximum amount of IOUs that an account can hold
@param view the ledger to check against.
@param account the account of interest.
@param issuer the issuer of the IOU.
@param currency the IOU to check.
@return The maximum amount that can be held.
*/
/** Read the maximum IOU balance that @p account has authorised @p issuer to
* carry on their behalf.
*
* Reads `sfLowLimit` or `sfHighLimit` from the trustline depending on
* which side `account` occupies (low if `account < issuer`). The issuer
* field of the returned amount is rewritten to `account` so the result is
* safe to consume without knowing the binary-ordering of the two accounts.
* Returns a zero-valued `STAmount` (with the correct issue) if no trustline
* exists.
*
* @param view Read-only ledger view to query.
* @param account The account whose credit limit is requested.
* @param issuer The IOU issuer.
* @param currency The currency of the trustline.
* @return The credit limit expressed from @p account's perspective, or zero
* if no trustline exists.
*/
/** @{ */
STAmount
creditLimit(
@@ -39,16 +61,35 @@ creditLimit(
AccountID const& issuer,
Currency const& currency);
/** Convenience wrapper returning the credit limit as `IOUAmount`.
*
* @param v Read-only ledger view to query.
* @param acc The account whose credit limit is requested.
* @param iss The IOU issuer.
* @param cur The currency of the trustline.
* @return The credit limit as `IOUAmount`, or zero if no trustline exists.
* @see creditLimit
*/
IOUAmount
creditLimit2(ReadView const& v, AccountID const& acc, AccountID const& iss, Currency const& cur);
/** @} */
/** Returns the amount of IOUs issued by issuer that are held by an account
@param view the ledger to check against.
@param account the account of interest.
@param issuer the issuer of the IOU.
@param currency the IOU to check.
*/
/** Read the IOU balance that @p account currently holds.
*
* `sfBalance` is stored in "low-account-sends-to-high-account" orientation.
* When `account` is the high side the stored value is negated before being
* returned, so callers always receive a balance expressed as "how much of
* this currency does @p account hold", regardless of which slot they occupy
* on the trustline. Returns zero (with the correct issue) if no trustline
* exists.
*
* @param view Read-only ledger view to query.
* @param account The account whose balance is requested.
* @param issuer The IOU issuer.
* @param currency The currency of the trustline.
* @return The balance expressed from @p account's perspective, or zero if
* no trustline exists.
*/
/** @{ */
STAmount
creditBalance(
@@ -58,12 +99,20 @@ creditBalance(
Currency const& currency);
/** @} */
//------------------------------------------------------------------------------
//
// Freeze checking (IOU-specific)
//
//------------------------------------------------------------------------------
// --- Freeze checks (IOU-specific) ---
/** Check whether @p issuer has individually frozen @p account's trustline.
*
* Inspects only the issuer's side flag (`lsfLowFreeze`/`lsfHighFreeze`) on
* the trustline. Does **not** check the issuer's global freeze flag — use
* `isFrozen` for that combined check. Always returns `false` for XRP.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param currency The IOU currency.
* @param issuer The IOU issuer.
* @return `true` if the issuer has set a line-level freeze on this account.
*/
[[nodiscard]] bool
isIndividualFrozen(
ReadView const& view,
@@ -71,12 +120,34 @@ isIndividualFrozen(
Currency const& currency,
AccountID const& issuer);
/** Convenience overload accepting an `Issue`.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param issue The IOU issue (currency + issuer).
* @return `true` if the issuer has set a line-level freeze on this account.
* @see isIndividualFrozen(ReadView const&, AccountID const&, Currency const&,
* AccountID const&)
*/
[[nodiscard]] inline bool
isIndividualFrozen(ReadView const& view, AccountID const& account, Issue const& issue)
{
return isIndividualFrozen(view, account, issue.currency, issue.account);
}
/** Check whether @p account is frozen for @p currency issued by @p issuer.
*
* Returns `true` if either the issuer's `AccountRoot` has `lsfGlobalFreeze`
* set, or the issuer has frozen this specific trustline (`lsfLowFreeze` /
* `lsfHighFreeze`). Always returns `false` for XRP or when
* `account == issuer`. This is the check used by payment paths.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param currency The IOU currency.
* @param issuer The IOU issuer.
* @return `true` if the account cannot move this IOU due to any freeze.
*/
[[nodiscard]] bool
isFrozen(
ReadView const& view,
@@ -84,20 +155,52 @@ isFrozen(
Currency const& currency,
AccountID const& issuer);
/** Convenience overload accepting an `Issue`.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param issue The IOU issue (currency + issuer).
* @return `true` if the account cannot move this IOU due to any freeze.
* @see isFrozen(ReadView const&, AccountID const&, Currency const&,
* AccountID const&)
*/
[[nodiscard]] inline bool
isFrozen(ReadView const& view, AccountID const& account, Issue const& issue)
{
return isFrozen(view, account, issue.currency, issue.account);
}
// Overload with depth parameter for uniformity with MPTIssue version.
// The depth parameter is ignored for IOUs since they don't have vault recursion.
/** Overload accepting a depth parameter for interface uniformity with MPT.
*
* IOUs do not have vault-level recursion, so the `depth` argument is
* unconditionally ignored.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param issue The IOU issue (currency + issuer).
* @return `true` if the account cannot move this IOU due to any freeze.
*/
[[nodiscard]] inline bool
isFrozen(ReadView const& view, AccountID const& account, Issue const& issue, int /*depth*/)
{
return isFrozen(view, account, issue);
}
/** Check whether @p account is deep-frozen for @p currency issued by
* @p issuer.
*
* Deep-freeze (`lsfHighDeepFreeze` / `lsfLowDeepFreeze`) is a stricter
* condition than ordinary freeze: it prevents both sending *and* receiving
* the currency. Always returns `false` for XRP, and always returns `false`
* when `issuer == account` (an issuer cannot deep-freeze their own balance
* with themselves).
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param currency The IOU currency.
* @param issuer The IOU issuer.
* @return `true` if the deep-freeze flag is set on either side of the line.
*/
[[nodiscard]] bool
isDeepFrozen(
ReadView const& view,
@@ -105,6 +208,18 @@ isDeepFrozen(
Currency const& currency,
AccountID const& issuer);
/** Convenience overload accepting an `Issue`, with an optional depth parameter
* for interface uniformity with the MPT equivalent.
*
* The `depth` argument is unconditionally ignored for IOUs.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param issue The IOU issue (currency + issuer).
* @return `true` if the deep-freeze flag is set on either side of the line.
* @see isDeepFrozen(ReadView const&, AccountID const&, Currency const&,
* AccountID const&)
*/
[[nodiscard]] inline bool
isDeepFrozen(
ReadView const& view,
@@ -115,22 +230,63 @@ isDeepFrozen(
return isDeepFrozen(view, account, issue.currency, issue.account);
}
/** Convert a deep-freeze check into a `TER` result.
*
* Convenience wrapper for transactor preflight code that returns
* `tecFROZEN` if the account is deep-frozen and `tesSUCCESS` otherwise.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param issue The IOU issue (currency + issuer).
* @return `tecFROZEN` if deep-frozen, `tesSUCCESS` otherwise.
*/
[[nodiscard]] inline TER
checkDeepFrozen(ReadView const& view, AccountID const& account, Issue const& issue)
{
return isDeepFrozen(view, account, issue) ? (TER)tecFROZEN : (TER)tesSUCCESS;
}
//------------------------------------------------------------------------------
//
// Trust line operations
//
//------------------------------------------------------------------------------
// --- Trust line lifecycle ---
/** Create a trust line
This can set an initial balance.
*/
/** Create a new `RippleState` (trustline) SLE and insert it into both owner
* directories.
*
* This is the lowest-level entry point for trustline creation. It is called
* directly by `TrustSet` transactors and indirectly by `issueIOU` when the
* destination has no existing line.
*
* The function writes all trustline fields — limits, quality in/out, balance,
* and flag bits — using side-aware field selectors (`sfLowLimit`/`sfHighLimit`
* etc.) derived from `bSrcHigh`. The peer account's `lsfNoRipple` flag is
* initialised from the peer's `lsfDefaultRipple` setting (absent means
* noRipple is on by default).
*
* @param view Mutable ledger view.
* @param bSrcHigh `true` if `uSrcAccountID` occupies the "high" slot
* (i.e., `uSrcAccountID > uDstAccountID`).
* @param uSrcAccountID The account whose limit and flags are being
* configured.
* @param uDstAccountID The peer account on the other side of the line.
* @param uIndex Pre-calculated keylet key for the new SLE.
* @param sleAccount The `AccountRoot` SLE for the account being set
* (used to adjust owner count); must not be null.
* @param bAuth If `true`, set the authorization flag on the source
* side of the line.
* @param bNoRipple If `true`, set `lsfNoRipple` on the source side.
* @param bFreeze If `true`, set the freeze flag on the source side.
* @param bDeepFreeze If `true`, set the deep-freeze flag on the source
* side.
* @param saBalance Initial balance from the source account's
* perspective; the issuer field must be `noAccount()`.
* @param saLimit Credit limit for the source account; the issuer
* field must be `uSrcAccountID`.
* @param uQualityIn Quality-in override (0 = default/no override).
* @param uQualityOut Quality-out override (0 = default/no override).
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success, `tecDIR_FULL` if either owner directory
* is at capacity, `tecNO_TARGET` if the peer account does not exist,
* or `tefINTERNAL` if `sleAccount` is null or has a mismatched ID.
*/
[[nodiscard]] TER
trustCreate(
ApplyView& view,
@@ -151,6 +307,21 @@ trustCreate(
std::uint32_t uQualityOut,
beast::Journal j);
/** Delete a `RippleState` (trustline) SLE and remove its directory backlinks.
*
* Removes the SLE from both the low and high owner directories using the
* `sfLowNode`/`sfHighNode` deletion hints stored inside the SLE itself,
* then erases the SLE from the view.
*
* @param view Mutable ledger view.
* @param sleRippleState The trustline SLE to delete; must be obtained
* from `view.peek()`.
* @param uLowAccountID The account occupying the low slot.
* @param uHighAccountID The account occupying the high slot.
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success, `tefBAD_LEDGER` if either directory
* removal fails (indicating ledger corruption).
*/
[[nodiscard]] TER
trustDelete(
ApplyView& view,
@@ -159,12 +330,30 @@ trustDelete(
AccountID const& uHighAccountID,
beast::Journal j);
//------------------------------------------------------------------------------
//
// IOU issuance/redemption
//
//------------------------------------------------------------------------------
// --- IOU issuance/redemption ---
/** Issue IOUs from @p issue.account to @p account, adjusting the trustline
* balance.
*
* Debits the issuer's side of the trustline and credits the receiver. After
* adjusting the balance, calls the internal `updateTrustLine` helper: if the
* sender's balance crosses zero and seven specific cleanup conditions are met
* (zero limit, no freeze, etc.), the sender's reserve is released and the
* line may be deleted via `trustDelete`.
*
* If no trustline exists for the receiver, one is created via `trustCreate`,
* inheriting the receiver's `lsfDefaultRipple` setting for the initial
* `lsfNoRipple` state. Always invokes `view.creditHookIOU()` after mutating
* the balance.
*
* @param view Mutable ledger view.
* @param account The account receiving the IOUs (must not be the issuer).
* @param amount The amount to issue; its `Issue` must match @p issue.
* @param issue Identifies the currency and issuer.
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success, or a `tef`/`tec` code propagated from
* `trustCreate` or `trustDelete` if an error occurs.
*/
[[nodiscard]] TER
issueIOU(
ApplyView& view,
@@ -173,6 +362,26 @@ issueIOU(
Issue const& issue,
beast::Journal j);
/** Redeem IOUs held by @p account back toward the issuer, adjusting the
* trustline balance.
*
* The mirror image of `issueIOU`: credits the issuer and debits the holder.
* After adjusting the balance, calls `updateTrustLine` for the same
* automatic cleanup logic. Always invokes `view.creditHookIOU()` after
* mutating the balance.
*
* Unlike `issueIOU`, a missing trustline is treated as a fatal internal
* error (`tefINTERNAL`) because it is impossible to redeem a balance on a
* line that does not exist.
*
* @param view Mutable ledger view.
* @param account The account redeeming IOUs (must not be the issuer).
* @param amount The amount to redeem; its `Issue` must match @p issue.
* @param issue Identifies the currency and issuer.
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success, `tefINTERNAL` if no trustline exists,
* or a `tef`/`tec` code from `trustDelete` if cleanup triggers an error.
*/
[[nodiscard]] TER
redeemIOU(
ApplyView& view,
@@ -181,28 +390,30 @@ redeemIOU(
Issue const& issue,
beast::Journal j);
//------------------------------------------------------------------------------
//
// Authorization and transfer checks (IOU-specific)
//
//------------------------------------------------------------------------------
// --- Authorization and transfer checks (IOU-specific) ---
/** Check if the account lacks required authorization.
/** Check whether @p account is authorized to hold the IOU described by
* @p issue.
*
* Return tecNO_AUTH or tecNO_LINE if it does
* and tesSUCCESS otherwise.
* Behaviour depends on @p authType:
* - **`StrongAuth`**: Returns `tecNO_LINE` immediately if no trustline
* exists. If the issuer has `lsfRequireAuth` and the line exists but is
* not authorized, returns `tecNO_AUTH`.
* - **`WeakAuth`** / **`Legacy`** (equivalent for IOUs): Returns
* `tecNO_AUTH` if `lsfRequireAuth` is set, the line exists, but is not
* authorized. Returns `tecNO_LINE` if auth is required and no line
* exists. If `lsfRequireAuth` is not set, returns `tesSUCCESS` even when
* no line exists — appropriate for payment path-finding where a line may
* be created on the fly.
*
* If StrongAuth then return tecNO_LINE if the RippleState doesn't exist. Return
* tecNO_AUTH if lsfRequireAuth is set on the issuer's AccountRoot, and the
* RippleState does exist, and the RippleState is not authorized.
* Always returns `tesSUCCESS` for XRP or when `account == issue.account`.
*
* If WeakAuth then return tecNO_AUTH if lsfRequireAuth is set, and the
* RippleState exists, and is not authorized. Return tecNO_LINE if
* lsfRequireAuth is set and the RippleState doesn't exist. Consequently, if
* WeakAuth and lsfRequireAuth is *not* set, this function will return
* tesSUCCESS even if RippleState does *not* exist.
*
* The default "Legacy" auth type is equivalent to WeakAuth.
* @param view Read-only ledger view.
* @param issue The IOU to check authorization for.
* @param account The account to check.
* @param authType Authorization strictness; defaults to `AuthType::Legacy`
* (equivalent to `WeakAuth` for IOUs).
* @return `tesSUCCESS`, `tecNO_AUTH`, or `tecNO_LINE`.
*/
[[nodiscard]] TER
requireAuth(
@@ -211,21 +422,53 @@ requireAuth(
AccountID const& account,
AuthType authType = AuthType::Legacy);
/** Check if the destination account is allowed
* to receive IOU. Return terNO_RIPPLE if rippling is
* disabled on both sides and tesSUCCESS otherwise.
/** Check whether an IOU can be transferred between @p from and @p to via the
* issuer's trustlines.
*
* Returns `tesSUCCESS` unconditionally when either endpoint is the issuer,
* or when the IOU is native (XRP). For third-party transfers, returns
* `terNO_RIPPLE` only when both the `from` and the `to` trustlines have
* `lsfNoRipple` set on the issuer's side, blocking rippling through. If a
* trustline does not exist for a given account, the issuer's
* `lsfDefaultRipple` flag is consulted as a fallback preference.
*
* @param view Read-only ledger view.
* @param issue The IOU (identifies the issuer and currency).
* @param from The sending account.
* @param to The receiving account.
* @return `tesSUCCESS` if the transfer is permitted, `terNO_RIPPLE` if
* rippling is disabled on both sides.
*/
[[nodiscard]] TER
canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, AccountID const& to);
//------------------------------------------------------------------------------
//
// Empty holding operations (IOU-specific)
//
//------------------------------------------------------------------------------
// --- Empty holding operations (IOU-specific) ---
/// Any transactors that call addEmptyHolding() in doApply must call
/// canAddHolding() in preflight with the same View and Asset
/** Create a zero-balance trustline for @p accountID, reserving the destination
* slot before any funds arrive.
*
* Used by transactors (e.g., DEX limit orders) that need to guarantee a
* destination line exists before settlement. Checks that @p accountID can
* cover the increased owner-count reserve before calling `trustCreate`.
*
* Returns `tesSUCCESS` immediately for XRP or when `accountID` is the
* issuer. Returns `tecDUPLICATE` if the trustline already exists.
*
* @note Any transactor that calls this function in `doApply` **must** call
* `canAddHolding()` (declared in `TokenHelpers.h`) in `preflight` with
* the same view and asset to validate the reserve precondition.
*
* @param view Mutable ledger view.
* @param accountID The account that will hold the IOU.
* @param priorBalance The account's XRP balance before the current
* transaction, used to test reserve sufficiency.
* @param issue The IOU to create a holding for.
* @param journal Journal for trace/debug logging.
* @return `tesSUCCESS` on success; `tecFROZEN` if the issuer is globally
* frozen; `tecNO_LINE_INSUF_RESERVE` if the account cannot afford the
* reserve; `tecDUPLICATE` if the line already exists; or a `tec`/`tef`
* code from `trustCreate`.
*/
[[nodiscard]] TER
addEmptyHolding(
ApplyView& view,
@@ -234,6 +477,20 @@ addEmptyHolding(
Issue const& issue,
beast::Journal journal);
/** Delete a zero-balance trustline previously created by `addEmptyHolding`.
*
* Validates that the balance is actually zero before deletion. Adjusts
* owner counts for both the low and high sides if their reserve flags are
* set, then calls `trustDelete`.
*
* @param view Mutable ledger view.
* @param accountID The account whose holding line should be removed.
* @param issue The IOU identifying the trustline to remove.
* @param journal Journal for trace/debug logging.
* @return `tesSUCCESS` on success; `tecHAS_OBLIGATIONS` if the balance is
* non-zero; `tecOBJECT_NOT_FOUND` if no line exists (and the account
* is not the issuer); or a `tef`/`tec` code from `trustDelete`.
*/
[[nodiscard]] TER
removeEmptyHolding(
ApplyView& view,
@@ -241,9 +498,27 @@ removeEmptyHolding(
Issue const& issue,
beast::Journal journal);
/** Delete trustline to AMM. The passed `sle` must be obtained from a prior
* call to view.peek(). Fail if neither side of the trustline is AMM or
* if ammAccountID is seated and is not one of the trustline's side.
/** Delete a trustline owned by an AMM pool account during AMM withdrawal.
*
* Validates that:
* - @p sleState is a non-null `ltRIPPLE_STATE` SLE.
* - Exactly one of the two trustline endpoints is an AMM account
* (identified by the presence of `sfAMMID` in the `AccountRoot`).
* - If @p ammAccountID is provided, it matches one of the endpoints.
*
* On success, calls `trustDelete` and decrements the owner count of the
* non-AMM side.
*
* @param view Mutable ledger view.
* @param sleState The `ltRIPPLE_STATE` SLE to delete; must be obtained
* from `view.peek()`.
* @param ammAccountID If provided, the expected AMM account ID; the
* function returns `terNO_AMM` if neither endpoint matches.
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success; `tecINTERNAL` if the SLE is null, has
* the wrong type, if both sides are AMM, or if the reserve flag is
* unexpectedly absent; `terNO_AMM` if neither endpoint is an AMM or
* the optional ID does not match; or a `tef` code from `trustDelete`.
*/
[[nodiscard]] TER
deleteAMMTrustLine(
@@ -252,8 +527,19 @@ deleteAMMTrustLine(
std::optional<AccountID> const& ammAccountID,
beast::Journal j);
/** Delete AMMs MPToken. The passed `sle` must be obtained from a prior
* call to view.peek().
/** Delete an AMM account's `MPToken` SLE during AMM withdrawal.
*
* Removes the `MPToken` SLE from @p ammAccountID's owner directory and
* erases it from the view. The caller is responsible for any balance
* assertions before invoking this function.
*
* @param view Mutable ledger view.
* @param sleMPT The `MPToken` SLE to delete; must be obtained from
* `view.peek()`.
* @param ammAccountID The AMM account that owns the `MPToken`.
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success, `tefBAD_LEDGER` if the directory removal
* fails (indicating ledger corruption).
*/
[[nodiscard]] TER
deleteAMMMPToken(

View File

@@ -1,3 +1,20 @@
/** @file
* Asset-agnostic dispatcher layer for all token operations on the XRP Ledger.
*
* This header is the unified entry point for token operations that must work
* across XRPL's three asset classes: XRP, IOU (trust-line-based), and MPT
* (Multi-Party Token). It sits between transaction-processing code that wants
* to be asset-agnostic and the two type-specific leaf modules:
* `RippleStateHelpers.h` for IOU trust lines and `MPTokenHelpers.h` for
* `MPToken`/`MPTokenIssuance` objects.
*
* Callers pass an `Asset` — a `std::variant<Issue, MPTIssue>` — and the
* functions here dispatch via `std::visit` or `Asset::visit` to the correct
* lower-level function, returning consistent result types (`STAmount`, `TER`,
* `bool`) regardless of asset kind. Adding a new asset type requires only
* extending the `Asset` variant and the branches here, not modifying call
* sites.
*/
#pragma once
#include <xrpl/beast/utility/Journal.h>
@@ -20,30 +37,83 @@ namespace xrpl {
//
//------------------------------------------------------------------------------
/** Controls the treatment of frozen account balances */
enum class FreezeHandling { IgnoreFreeze, ZeroIfFrozen };
/** Controls the treatment of unauthorized MPT balances */
enum class AuthHandling { IgnoreAuth, ZeroIfUnauthorized };
/** Controls whether to include the account's full spendable balance */
enum class SpendableHandling { SimpleBalance, FullBalance };
enum class WaiveTransferFee : bool { No = false, Yes };
/** Controls whether accountSend is allowed to overflow OutstandingAmount **/
enum class AllowMPTOverflow : bool { No = false, Yes };
/* Check if MPToken (for MPT) or trust line (for IOU) exists:
* - StrongAuth - before checking if authorization is required
* - WeakAuth
* for MPT - after checking lsfMPTRequireAuth flag
* for IOU - do not check if trust line exists
* - Legacy
* for MPT - before checking lsfMPTRequireAuth flag i.e. same as StrongAuth
* for IOU - do not check if trust line exists i.e. same as WeakAuth
/** Controls how a frozen balance is reported by balance-query functions.
*
* Use `ZeroIfFrozen` in payment paths where a frozen balance must not be
* spent. Use `IgnoreFreeze` in cleanup paths that need the real value
* regardless of freeze state.
*/
enum class AuthType { StrongAuth, WeakAuth, Legacy };
enum class FreezeHandling {
IgnoreFreeze, /**< Return the actual balance even if the holding is frozen. */
ZeroIfFrozen /**< Return zero when the holding is frozen (the spendable amount). */
};
/** Controls how an unauthorized MPT balance is reported by balance-query functions.
*
* Parallel to `FreezeHandling` but for MPT authorization. Use
* `ZeroIfUnauthorized` when computing the amount an account may legally spend.
*/
enum class AuthHandling {
IgnoreAuth, /**< Return the actual balance even if the MPToken is unauthorized. */
ZeroIfUnauthorized /**< Return zero when the MPToken is not authorized. */
};
/** Controls whether `accountHolds` reports simple or full spendable balance.
*
* - `SimpleBalance`: the amount the account can spend without going into
* debt, i.e. the raw trustline balance (negated to account-centric terms)
* for IOU, or the `sfMPTAmount` for MPT.
* - `FullBalance`: for IOU, also includes the peer's credit limit so the
* account can borrow up to that limit; for the IOU issuer, returns
* `STAmount::kMAX_VALUE`; for the MPT issuer, returns
* `MaximumAmount - OutstandingAmount`.
*/
enum class SpendableHandling {
SimpleBalance, /**< Balance the account can spend without going into debt. */
FullBalance /**< Full spendable balance including borrowable credit or issuance capacity. */
};
/** Controls whether the transfer fee is skipped during a send operation.
*
* Typed as `enum class : bool` to prevent accidental transposition with
* other boolean parameters at call sites.
*/
enum class WaiveTransferFee : bool {
No = false, /**< Apply the normal transfer fee. */
Yes /**< Skip the transfer fee entirely. */
};
/** Controls whether `accountSend` permits `OutstandingAmount` to transiently
* exceed `MaximumAmount` during MPT payment-engine routing.
*
* The payment engine issues tokens first (raising `OutstandingAmount`) and
* redeems them in the same transaction (lowering it back). `Yes` raises the
* overflow ceiling to `UINT64_MAX` for that transient window. Direct sends
* use `No` and enforce the strict `MaximumAmount` cap.
*/
enum class AllowMPTOverflow : bool {
No = false, /**< Enforce the strict MaximumAmount cap. */
Yes /**< Allow transient overflow up to UINT64_MAX during routing. */
};
/** Encodes the three-way authorization-strictness contract.
*
* Determines how `requireAuth` behaves when checking whether an account may
* hold or interact with a token:
* - `StrongAuth` checks that the holding object (trust line or `MPToken`)
* exists *before* asking whether authorization is set. Returns `tecNO_LINE`
* immediately if no holding exists.
* - `WeakAuth` skips the existence check, returning `tesSUCCESS` when
* authorization is not required even if no holding exists. Appropriate for
* payment path-finding where a line may be created on the fly.
* - `Legacy` maps to `StrongAuth` for MPT and `WeakAuth` for IOU, preserving
* historical behavior at existing call sites.
*/
enum class AuthType {
StrongAuth, /**< Existence of the holding object is verified first. */
WeakAuth, /**< Holding existence is not required when auth is not needed. */
Legacy /**< StrongAuth for MPT; WeakAuth for IOU (historical default). */
};
//------------------------------------------------------------------------------
//
@@ -51,35 +121,126 @@ enum class AuthType { StrongAuth, WeakAuth, Legacy };
//
//------------------------------------------------------------------------------
/** Check whether the issuer of @p asset has activated a global freeze.
*
* Dispatches to the IOU or MPT leaf based on the runtime type of @p asset.
* A global freeze on the issuer's `AccountRoot` blocks all holders
* simultaneously.
*
* @param view Read-only ledger view.
* @param asset The asset to test.
* @return `true` if the issuer has a global freeze in effect.
*/
[[nodiscard]] bool
isGlobalFrozen(ReadView const& view, Asset const& asset);
/** Check whether @p account has an individual freeze on @p asset.
*
* Dispatches to the IOU or MPT leaf based on the runtime type of @p asset.
* For IOU, checks the issuer's per-line freeze flag. For MPT, checks the
* `lsfMPTLocked` flag on the `MPToken` SLE. Does not check global freeze.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param asset The asset to test.
* @return `true` if the issuer has set an individual freeze on this account.
*/
[[nodiscard]] bool
isIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset);
/**
* isFrozen check is recursive for MPT shares in a vault, descending to
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
* purely defensive, as we currently do not allow such vaults to be created.
/** Check whether @p account is frozen for @p asset (global or individual).
*
* Returns `true` if either `isGlobalFrozen` or `isIndividualFrozen` is true
* for the given account and asset. Dispatches to the typed IOU or MPT leaf
* via `std::visit`.
*
* The `depth` parameter enables recursive vault checking: if @p asset is an
* MPT backed by a vault, the vault's underlying asset is checked up to
* `maxAssetCheckDepth` levels deep.
*
* @note Recursion is purely defensive. The ledger currently does not allow
* nested vaults to be created, so `depth > 0` should not occur in
* practice.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param asset The asset to test.
* @param depth Current recursion depth for vault checking; defaults to 0.
* @return `true` if the account cannot move this asset due to any freeze.
*/
[[nodiscard]] bool
isFrozen(ReadView const& view, AccountID const& account, Asset const& asset, int depth = 0);
/** Convert a freeze check on an IOU to a `TER`.
*
* Returns `tecFROZEN` if `isFrozen` is true for the given account and issue,
* `tesSUCCESS` otherwise.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param issue The IOU to test.
* @return `tecFROZEN` if frozen, `tesSUCCESS` otherwise.
*/
[[nodiscard]] TER
checkFrozen(ReadView const& view, AccountID const& account, Issue const& issue);
/** Convert a freeze check on an MPT to a `TER`.
*
* Returns `tecLOCKED` (not `tecFROZEN`) if `isFrozen` is true for the given
* account and MPT issuance, `tesSUCCESS` otherwise. The distinct error code
* reflects the separate protocol semantics of MPT locking vs IOU freezing.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param mptIssue The MPT issuance to test.
* @return `tecLOCKED` if frozen/locked, `tesSUCCESS` otherwise.
*/
[[nodiscard]] TER
checkFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);
/** Convert a freeze check on any asset to a `TER`.
*
* Dispatches to `checkFrozen(…, Issue)` or `checkFrozen(…, MPTIssue)` based
* on the runtime type of @p asset, returning the type-appropriate error code
* (`tecFROZEN` for IOU, `tecLOCKED` for MPT).
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param asset The asset to test.
* @return `tecFROZEN` (IOU) or `tecLOCKED` (MPT) if frozen, `tesSUCCESS`
* otherwise.
*/
[[nodiscard]] TER
checkFrozen(ReadView const& view, AccountID const& account, Asset const& asset);
/** Check whether any account in @p accounts is frozen for @p issue.
*
* Iterates the list and returns `true` on the first frozen account. Used to
* check both sides (taker and maker) of an offer with a single call.
*
* @param view Read-only ledger view.
* @param accounts The accounts to test, e.g. `{takerID, makerID}`.
* @param issue The IOU to test.
* @return `true` if any account in the list is frozen for @p issue.
*/
[[nodiscard]] bool
isAnyFrozen(
ReadView const& view,
std::initializer_list<AccountID> const& accounts,
Issue const& issue);
/** Check whether any account in @p accounts is frozen for @p asset.
*
* Asset-dispatching overload. Delegates to the IOU or MPT leaf for each
* account in the list. The `depth` parameter passes through to `isFrozen`
* for vault-backed MPT recursion.
*
* @param view Read-only ledger view.
* @param accounts The accounts to test.
* @param asset The asset to test.
* @param depth Recursion depth for vault checking; defaults to 0.
* @return `true` if any account in the list is frozen for @p asset.
*/
[[nodiscard]] bool
isAnyFrozen(
ReadView const& view,
@@ -87,6 +248,22 @@ isAnyFrozen(
Asset const& asset,
int depth = 0);
/** Check whether @p account is deep-frozen for @p mptIssue.
*
* For MPT, deep-freeze semantics are identical to regular freeze: a frozen
* MPT holder cannot send or receive. This function delegates to
* `isFrozen(view, account, mptIssue, depth)`.
*
* @note For IOU, deep-freeze is a distinct state (`lsfDeepFreeze`) where the
* holder cannot send but can still receive. See `isDeepFrozen` in
* `RippleStateHelpers.h` for IOU-specific semantics.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param mptIssue The MPT issuance to test.
* @param depth Recursion depth for vault checking; defaults to 0.
* @return `true` if the account is frozen/locked for this MPT.
*/
[[nodiscard]] bool
isDeepFrozen(
ReadView const& view,
@@ -94,17 +271,51 @@ isDeepFrozen(
MPTIssue const& mptIssue,
int depth = 0);
/**
* isFrozen check is recursive for MPT shares in a vault, descending to
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
* purely defensive, as we currently do not allow such vaults to be created.
/** Check whether @p account is deep-frozen for @p asset.
*
* Dispatches to the IOU or MPT leaf via `std::visit`. For MPT, deep-freeze
* is equivalent to regular freeze. For IOU, checks the `lsfDeepFreeze` flag,
* which prevents sending but allows receiving.
*
* The `depth` parameter enables recursive vault checking up to
* `maxAssetCheckDepth` levels.
*
* @note Recursion is purely defensive — nested vaults cannot currently be
* created on the ledger.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param asset The asset to test.
* @param depth Recursion depth for vault checking; defaults to 0.
* @return `true` if the account is deep-frozen for @p asset.
*/
[[nodiscard]] bool
isDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset, int depth = 0);
/** Convert a deep-freeze check on an MPT to a `TER`.
*
* Returns `tecLOCKED` if `isDeepFrozen` is true, `tesSUCCESS` otherwise.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param mptIssue The MPT issuance to test.
* @return `tecLOCKED` if deep-frozen, `tesSUCCESS` otherwise.
*/
[[nodiscard]] TER
checkDeepFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);
/** Convert a deep-freeze check on any asset to a `TER`.
*
* Dispatches to `checkDeepFrozen(…, Issue)` (`tecFROZEN`) or
* `checkDeepFrozen(…, MPTIssue)` (`tecLOCKED`) based on the runtime type of
* @p asset.
*
* @param view Read-only ledger view.
* @param account The account to test.
* @param asset The asset to test.
* @return `tecFROZEN` (IOU) or `tecLOCKED` (MPT) if deep-frozen,
* `tesSUCCESS` otherwise.
*/
[[nodiscard]] TER
checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset);
@@ -114,19 +325,31 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
//
//------------------------------------------------------------------------------
// Returns the amount an account can spend.
//
// If shSIMPLE_BALANCE is specified, this is the amount the account can spend
// without going into debt.
//
// If shFULL_BALANCE is specified, this is the amount the account can spend
// total. Specifically:
// * The account can go into debt if using a trust line, and the other side has
// a non-zero limit.
// * If the account is the asset issuer the limit is defined by the asset /
// issuance.
//
// <-- saAmount: amount of currency held by account. May be negative.
/** Return the amount that @p account can spend of the given currency/issuer.
*
* This is the canonical implementation. All other `accountHolds` overloads
* ultimately delegate here for the IOU path.
*
* - For XRP: returns `xrpLiquid(view, account, 0, j)` (reserve-adjusted).
* - For IOU with `shFULL_BALANCE` when `account == issuer`: returns
* `STAmount::kMAX_VALUE` — the issuer has effectively unlimited issuance
* capacity.
* - For IOU otherwise: reads the trust-line balance from the ledger,
* negating it to account-centric terms. If `shFULL_BALANCE` is specified,
* also adds the peer's credit limit so the account can draw down that
* credit. Returns zero if the line is frozen (when `ZeroIfFrozen`) or does
* not exist.
*
* @param view Read-only ledger view.
* @param account The account whose balance is queried.
* @param currency The IOU currency.
* @param issuer The IOU issuer.
* @param zeroIfFrozen Whether to return zero for frozen balances.
* @param j Journal for trace logging.
* @param includeFullBalance Whether to include borrowable credit or max
* issuance capacity; defaults to `SimpleBalance`.
* @return The spendable balance, which may be negative (e.g. trust-line debt).
*/
[[nodiscard]] STAmount
accountHolds(
ReadView const& view,
@@ -137,6 +360,19 @@ accountHolds(
beast::Journal j,
SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance);
/** Return the spendable balance of an IOU for @p account.
*
* Convenience adapter over the `(Currency, AccountID)` overload, extracting
* the currency and issuer from @p issue.
*
* @param view Read-only ledger view.
* @param account The account whose balance is queried.
* @param issue The IOU (currency + issuer).
* @param zeroIfFrozen Whether to return zero for frozen balances.
* @param j Journal for trace logging.
* @param includeFullBalance Balance mode; defaults to `SimpleBalance`.
* @return The spendable balance from @p account's perspective.
*/
[[nodiscard]] STAmount
accountHolds(
ReadView const& view,
@@ -146,6 +382,29 @@ accountHolds(
beast::Journal j,
SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance);
/** Return the spendable balance of an MPT for @p account.
*
* - For the MPT issuer with `shFULL_BALANCE`: returns
* `MaximumAmount - OutstandingAmount` (available issuance capacity) via
* `availableMPTAmount`.
* - For regular holders: reads `sfMPTAmount` from the `MPToken` SLE. Returns
* zero if: the `MPToken` SLE does not exist; the token is frozen and
* `ZeroIfFrozen` is set; or the token is unauthorized and
* `ZeroIfUnauthorized` is set (with `featureSingleAssetVault` gating the
* precise auth-check path).
* - Under `featureMPTokensV2`, the result passes through
* `view.balanceHookMPT` to allow `PaymentSandbox` deferred-credit
* interception.
*
* @param view Read-only ledger view.
* @param account The account whose balance is queried.
* @param mptIssue The MPT issuance.
* @param zeroIfFrozen Whether to zero the balance when frozen/locked.
* @param zeroIfUnauthorized Whether to zero the balance when unauthorized.
* @param j Journal for trace logging.
* @param includeFullBalance Balance mode; defaults to `SimpleBalance`.
* @return The spendable MPT balance, or zero per the policy flags above.
*/
[[nodiscard]] STAmount
accountHolds(
ReadView const& view,
@@ -156,6 +415,22 @@ accountHolds(
beast::Journal j,
SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance);
/** Return the spendable balance of any asset for @p account.
*
* Asset-dispatching overload. Delegates to the `Issue` overload (which
* ignores `zeroIfUnauthorized`) or the `MPTIssue` overload based on the
* runtime type of @p asset.
*
* @param view Read-only ledger view.
* @param account The account whose balance is queried.
* @param asset The asset to query.
* @param zeroIfFrozen Whether to zero the balance when frozen.
* @param zeroIfUnauthorized Whether to zero the balance when unauthorized
* (MPT only; ignored for IOU).
* @param j Journal for trace logging.
* @param includeFullBalance Balance mode; defaults to `SimpleBalance`.
* @return The spendable balance per the policy flags.
*/
[[nodiscard]] STAmount
accountHolds(
ReadView const& view,
@@ -166,11 +441,29 @@ accountHolds(
beast::Journal j,
SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance);
// Returns the amount an account can spend of the currency type saDefault, or
// returns saDefault if this account is the issuer of the currency in
// question. Should be used in favor of accountHolds when questioning how much
// an account can spend while also allowing currency issuers to spend
// unlimited amounts of their own currency (since they can always issue more).
/** Return how much of @p saDefault's currency @p id can fund, treating the
* issuer as having unlimited supply of their own currency.
*
* For IOU: if `id == saDefault.getIssuer()`, returns `saDefault` directly —
* the issuer can always fund an offer for their own currency up to whatever
* amount they specify. Otherwise delegates to `accountHolds` with
* `SimpleBalance`.
*
* This is the correct semantic for offer matching; prefer `accountFunds` over
* `accountHolds` when asking "can this account fund this offer?".
*
* @note `saDefault` must hold an `Issue` (not MPT). Use the `AuthHandling`
* overload for asset-agnostic callers.
*
* @param view Read-only ledger view.
* @param id The account to query.
* @param saDefault The amount (currency + issuer) to check fundability
* for.
* @param freezeHandling Whether to zero the balance when frozen.
* @param j Journal for trace logging.
* @return `saDefault` if @p id is the issuer; otherwise the trust-line
* balance, zeroed per @p freezeHandling.
*/
[[nodiscard]] STAmount
accountFunds(
ReadView const& view,
@@ -179,7 +472,22 @@ accountFunds(
FreezeHandling freezeHandling,
beast::Journal j);
// Overload with AuthHandling to support IOU and MPT.
/** Asset-agnostic overload of `accountFunds` supporting both IOU and MPT.
*
* For IOU: delegates to the `FreezeHandling`-only overload above.
* For MPT: delegates to `accountHolds` with `shFULL_BALANCE`, which
* returns the issuer's available issuance capacity or the holder's
* `sfMPTAmount`.
*
* @param view Read-only ledger view.
* @param id The account to query.
* @param saDefault The amount (currency/asset + issuer) to check.
* @param freezeHandling Whether to zero the balance when frozen.
* @param authHandling Whether to zero the balance when unauthorized (MPT
* only).
* @param j Journal for trace logging.
* @return The fundable balance per the policy flags.
*/
[[nodiscard]] STAmount
accountFunds(
ReadView const& view,
@@ -189,9 +497,15 @@ accountFunds(
AuthHandling authHandling,
beast::Journal j);
/** Returns the transfer fee as Rate based on the type of token
* @param view The ledger view
* @param amount The amount to transfer
/** Return the transfer fee for the asset embedded in @p amount.
*
* Dispatches on `amount.asset()`: for IOU, reads the issuer's transfer rate
* from their `AccountRoot`; for MPT, reads the `sfTransferFee` field from
* the `MPTokenIssuance` SLE. Both paths return a `Rate` (parts-per-billion).
*
* @param view Read-only ledger view.
* @param amount The amount whose asset determines which fee to look up.
* @return The transfer fee as a `Rate`, or `parityRate` if no fee is set.
*/
[[nodiscard]] Rate
transferRate(ReadView const& view, STAmount const& amount);
@@ -202,9 +516,42 @@ transferRate(ReadView const& view, STAmount const& amount);
//
//------------------------------------------------------------------------------
/** Check whether a new holding object (trust line or MPToken) can be created.
*
* For IOU: verifies that the issuer's `AccountRoot` has `lsfDefaultRipple`
* set; returns `terNO_RIPPLE` if not, `terNO_ACCOUNT` if the issuer does not
* exist, `tesSUCCESS` for XRP. For MPT: delegates to the MPT-specific check.
*
* @note This function is read-only (takes `ReadView`) and is intended to be
* called during `preflight`. Any transactor that calls `addEmptyHolding`
* in `doApply` must call this function in `preflight` first.
*
* @param view Read-only ledger view.
* @param asset The asset for which a holding would be created.
* @return `tesSUCCESS` if a holding can be added; `terNO_RIPPLE`,
* `terNO_ACCOUNT`, or an MPT-specific error otherwise.
*/
[[nodiscard]] TER
canAddHolding(ReadView const& view, Asset const& asset);
/** Create an empty holding object (trust line or MPToken) for @p accountID.
*
* Dispatches to `addEmptyHolding(…, Issue)` or `addEmptyHolding(…, MPTIssue)`
* based on the runtime type of @p asset. The holding is created with zero
* balance and consumes an owner-count reserve slot.
*
* @note The caller must have invoked `canAddHolding` in `preflight` with the
* same view and asset to validate preconditions before calling this.
*
* @param view Mutable ledger view.
* @param accountID The account that will hold the asset.
* @param priorBalance The account's XRP balance before this transaction,
* used to test reserve sufficiency.
* @param asset The asset to create a holding for.
* @param journal Journal for trace/debug logging.
* @return `tesSUCCESS` on success, or a `tec`/`tef` error from the
* type-specific leaf.
*/
[[nodiscard]] TER
addEmptyHolding(
ApplyView& view,
@@ -213,6 +560,21 @@ addEmptyHolding(
Asset const& asset,
beast::Journal journal);
/** Delete a zero-balance holding object (trust line or MPToken) for @p accountID.
*
* Dispatches to `removeEmptyHolding(…, Issue)` or
* `removeEmptyHolding(…, MPTIssue)` based on the runtime type of @p asset.
* The holding must have a zero balance; a non-zero balance returns
* `tecHAS_OBLIGATIONS`.
*
* @param view Mutable ledger view.
* @param accountID The account whose holding should be removed.
* @param asset The asset identifying the holding to remove.
* @param journal Journal for trace/debug logging.
* @return `tesSUCCESS` on success; `tecHAS_OBLIGATIONS` if the balance is
* non-zero; `tecOBJECT_NOT_FOUND` if no holding exists; or a `tec`/`tef`
* error from the type-specific leaf.
*/
[[nodiscard]] TER
removeEmptyHolding(
ApplyView& view,
@@ -226,6 +588,25 @@ removeEmptyHolding(
//
//------------------------------------------------------------------------------
/** Check whether @p account is authorized to hold or interact with @p asset.
*
* Dispatches to `requireAuth(…, Issue, …)` or `requireAuth(…, MPTIssue, …)`
* based on the runtime type of @p asset.
*
* - `StrongAuth`: verifies the holding object exists first; returns
* `tecNO_LINE` (IOU) or `tecNO_AUTH` (MPT) if absent.
* - `WeakAuth`: skips the existence check; returns success if authorization
* is not required even when no holding exists.
* - `Legacy`: maps to `StrongAuth` for MPT and `WeakAuth` for IOU to
* preserve historical behavior.
*
* @param view Read-only ledger view.
* @param asset The asset to check authorization for.
* @param account The account to check.
* @param authType Authorization strictness; defaults to `AuthType::Legacy`.
* @return `tesSUCCESS`, `tecNO_AUTH`, or `tecNO_LINE` depending on the asset
* type and authorization state.
*/
[[nodiscard]] TER
requireAuth(
ReadView const& view,
@@ -233,6 +614,20 @@ requireAuth(
AccountID const& account,
AuthType authType = AuthType::Legacy);
/** Check whether @p asset can be transferred from @p from to @p to.
*
* Dispatches to the IOU or MPT leaf. For IOU, checks rippling flags on the
* trustlines (returns `terNO_RIPPLE` if both sides block rippling). For MPT,
* checks `lsfMPTCanTransfer` on the issuance and the destination's
* authorization state.
*
* @param view Read-only ledger view.
* @param asset The asset to transfer.
* @param from The sending account.
* @param to The receiving account.
* @return `tesSUCCESS` if the transfer is permitted, or an asset-specific
* error (`terNO_RIPPLE`, `tecNO_AUTH`, etc.) otherwise.
*/
[[nodiscard]] TER
canTransfer(ReadView const& view, Asset const& asset, AccountID const& from, AccountID const& to);
@@ -242,14 +637,29 @@ canTransfer(ReadView const& view, Asset const& asset, AccountID const& from, Acc
//
//------------------------------------------------------------------------------
// Direct send w/o fees:
// - Redeeming IOUs and/or sending sender's own IOUs.
// - Create trust line of needed.
// --> bCheckIssuer : normally require issuer to be involved.
// [[nodiscard]] // nodiscard commented out so DirectStep.cpp compiles.
/** Calls static directSendNoFeeIOU if saAmount represents Issue.
* Calls static directSendNoFeeMPT if saAmount represents MPTIssue.
/** Send @p saAmount directly without applying transfer fees or limit checks.
*
* Used for IOU redemption, intra-issuer transfers, and MPT moves where the
* issuer is one of the endpoints. Dispatches to `directSendNoFeeIOU` for
* IOU and `directSendNoFeeMPT` for MPT.
*
* For IOU, @p bCheckIssuer controls whether the function asserts that the
* issuer is one of the endpoints. For MPT, the issuer check is not performed
* (`bCheckIssuer` must be `false` for MPT).
*
* @note This function is intentionally **not** marked `[[nodiscard]]` for
* compatibility with `DirectStep.cpp`, which discards the return value in
* certain control paths. All other callers should inspect the result.
*
* @param view Mutable ledger view.
* @param uSenderID The sending account.
* @param uReceiverID The receiving account.
* @param saAmount The amount to send; its asset determines the dispatch.
* @param bCheckIssuer If `true` (IOU only), asserts that the issuer is one
* of the endpoints. Must be `false` for MPT.
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success, or a `tec`/`tef` error from the
* type-specific leaf.
*/
TER
directSendNoFee(
@@ -260,8 +670,30 @@ directSendNoFee(
bool bCheckIssuer,
beast::Journal j);
/** Calls static accountSendIOU if saAmount represents Issue.
* Calls static accountSendMPT if saAmount represents MPTIssue.
/** Send @p saAmount from @p from to @p to, applying transfer fees when
* applicable.
*
* This is the main asset-transfer entry point for transactors. Dispatches to
* `accountSendIOU` or `accountSendMPT` based on the asset type embedded in
* @p saAmount. Transfer fees are applied unless `WaiveTransferFee::Yes` is
* passed.
*
* The `allowOverflow` flag is forwarded to the MPT path only and controls
* whether `OutstandingAmount` may transiently exceed `MaximumAmount` during
* the two-phase issue-then-redeem structure used by the payment engine. Direct
* sends should use `AllowMPTOverflow::No`.
*
* @param view Mutable ledger view.
* @param from The sending account.
* @param to The receiving account.
* @param saAmount The amount to send.
* @param j Journal for trace/debug logging.
* @param waiveFee Whether to skip the transfer fee; defaults to `No`.
* @param allowOverflow Whether MPT OutstandingAmount may transiently exceed
* MaximumAmount; defaults to `No`. Use `Yes` only in payment-engine
* routing.
* @return `tesSUCCESS` on success, or a `tec`/`tef` error from the
* type-specific leaf.
*/
[[nodiscard]] TER
accountSend(
@@ -273,12 +705,34 @@ accountSend(
WaiveTransferFee waiveFee = WaiveTransferFee::No,
AllowMPTOverflow allowOverflow = AllowMPTOverflow::No);
/** A vector of (receiver, amount) pairs used by `accountSendMulti`. */
using MultiplePaymentDestinations = std::vector<std::pair<AccountID, Number>>;
/** Like accountSend, except one account is sending multiple payments (with the
* same asset!) simultaneously
/** Send the same @p asset from @p senderID to multiple @p receivers in one
* atomic operation.
*
* Calls static accountSendMultiIOU if saAmount represents Issue.
* Calls static accountSendMultiMPT if saAmount represents MPTIssue.
* Dispatches to `accountSendMultiIOU` or `accountSendMultiMPT` based on
* @p asset. Batching avoids repeated round-trips through the ledger state for
* the sender's balance and the issuance's `OutstandingAmount` field.
*
* For MPT, the `fixCleanup3_1_3` amendment switches the aggregate
* `MaximumAmount` check from a per-iteration stale-snapshot check (pre-fix)
* to an exact `uint64_t` running-total check (post-fix) to prevent precision
* loss at 19-digit magnitudes near `kMAX_MP_TOKEN_AMOUNT`.
*
* @note `receivers.size()` must be greater than 1 (asserted).
*
* @param view Mutable ledger view.
* @param senderID The account sending the asset.
* @param asset The asset to send (must match the type of all receiver
* amounts).
* @param receivers List of (AccountID, Number) destination pairs. All amounts
* must be non-negative. Sender-equals-receiver entries are silently
* skipped.
* @param j Journal for trace/debug logging.
* @param waiveFee Whether to skip transfer fees; defaults to `No`.
* @return `tesSUCCESS` on success, or a `tec`/`tef` error from the
* type-specific leaf.
*/
[[nodiscard]] TER
accountSendMulti(
@@ -289,6 +743,23 @@ accountSendMulti(
beast::Journal j,
WaiveTransferFee waiveFee = WaiveTransferFee::No);
/** Transfer XRP directly between two accounts without reserve or fee checks.
*
* XRP has no trust lines, no transfer fees, and no authorization model, so
* it bypasses the Asset-dispatch path entirely. Both @p from and @p to must
* be non-zero and distinct. Returns `telFAILED_PROCESSING` (open ledger) or
* `tecFAILED_PROCESSING` (closed ledger) if the sender's balance is
* insufficient.
*
* @param view Mutable ledger view.
* @param from The sending account; must not be `beast::kZERO`.
* @param to The receiving account; must not be `beast::kZERO`.
* @param amount The XRP amount to transfer; must be native (XRP).
* @param j Journal for trace/debug logging.
* @return `tesSUCCESS` on success; `telFAILED_PROCESSING` or
* `tecFAILED_PROCESSING` if balance is insufficient; `tefINTERNAL` if
* either account SLE cannot be found.
*/
[[nodiscard]] TER
transferXRP(
ApplyView& view,

View File

@@ -1,3 +1,16 @@
/** @file
* Pure arithmetic helpers for the XLS-65d Single-Sided Vault feature.
*
* Each function converts between the two token types a vault manages:
* the underlying *asset* (XRP, IOU, or MPT that depositors contribute) and
* vault *shares* (an MPT representing proportional ownership). Because MPT
* values are always integers every function makes an explicit rounding
* decision — and those decisions differ between the deposit and withdrawal
* paths to protect vault solvency.
*
* These functions are stateless and side-effect-free; all ledger mutations
* are the caller's responsibility.
*/
#pragma once
#include <xrpl/protocol/STAmount.h>
@@ -8,53 +21,105 @@
namespace xrpl {
/** From the perspective of a vault, return the number of shares to give
depositor when they offer a fixed amount of assets. Note, since shares are
MPT, this number is integral and always truncated in this calculation.
@param vault The vault SLE.
@param issuance The MPTokenIssuance SLE for the vault's shares.
@param assets The amount of assets to convert.
@return The number of shares, or nullopt on error.
*/
/** Compute the shares minted when a depositor offers a fixed asset amount.
*
* Uses `sfAssetsTotal` from `vault` directly, *without* subtracting
* `sfLossUnrealized`. Unrealized losses are a risk borne by existing
* shareholders, not a discount for new depositors.
*
* **Bootstrap case**: when `sfAssetsTotal == 0` the result is
* `assets × 10^sfScale` (truncated), establishing the initial exchange rate.
* The non-bootstrap result is `(sfOutstandingAmount × assets) / sfAssetsTotal`,
* always truncated — depositors always receive a whole number of shares, never
* more than the assets strictly warrant.
*
* @note The deposit transactor calls this first, then back-calculates the
* true asset cost via `sharesToAssetsDeposit()` to ensure it never
* extracts more than the depositor offered.
* @throws std::overflow_error if `sfScale` is large enough to overflow
* XRPL's `Number` type; callers should catch and return `tecPATH_DRY`.
*
* @param vault The vault SLE; must contain `sfAsset`, `sfAssetsTotal`,
* `sfScale`, and `sfShareMPTID`.
* @param issuance The MPTokenIssuance SLE for the vault's share token;
* must contain `sfOutstandingAmount`.
* @param assets The asset amount to convert; must be non-negative and
* must match `vault->at(sfAsset)`.
* @return The integral share amount, or `nullopt` if `assets` is negative
* or its asset type does not match the vault.
*/
[[nodiscard]] std::optional<STAmount>
assetsToSharesDeposit(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& assets);
/** From the perspective of a vault, return the number of assets to take from
depositor when they receive a fixed amount of shares. Note, since shares are
MPT, they are always an integral number.
@param vault The vault SLE.
@param issuance The MPTokenIssuance SLE for the vault's shares.
@param shares The amount of shares to convert.
@return The number of assets, or nullopt on error.
*/
/** Compute the asset cost for a depositor who will receive a fixed share amount.
*
* This is the inverse of `assetsToSharesDeposit()` and is used in the second
* step of the deposit calculation: after truncating the forward direction to
* determine how many whole shares are created, the transactor calls this
* function to derive the exact asset amount to collect.
*
* Uses `sfAssetsTotal` directly, without subtracting `sfLossUnrealized`,
* matching the deposit-path convention.
*
* **Bootstrap case**: when `sfAssetsTotal == 0` the result uses `sfScale` to
* reverse the bootstrap formula applied by `assetsToSharesDeposit()`.
*
* @throws std::overflow_error if `sfScale` is large enough to overflow
* XRPL's `Number` type; callers should catch and return `tecPATH_DRY`.
*
* @param vault The vault SLE.
* @param issuance The MPTokenIssuance SLE for the vault's share token.
* @param shares The share amount to convert; must be non-negative and must
* match `vault->at(sfShareMPTID)`.
* @return The asset amount, or `nullopt` if `shares` is negative or its
* asset type does not match the vault's share MPT.
*/
[[nodiscard]] std::optional<STAmount>
sharesToAssetsDeposit(
std::shared_ptr<SLE const> const& vault,
std::shared_ptr<SLE const> const& issuance,
STAmount const& shares);
/** Controls whether to truncate shares instead of rounding. */
/** Controls whether to truncate (floor) the share result instead of rounding.
*
* `No` (the default) rounds to nearest, ensuring the vault is never
* shortchanged when computing shares to redeem for a fixed asset withdrawal.
* `Yes` applies floor truncation, used when the caller explicitly needs
* conservative (depositor-favoring) rounding.
*/
enum class TruncateShares : bool { No = false, Yes = true };
/** From the perspective of a vault, return the number of shares to demand from
the depositor when they ask to withdraw a fixed amount of assets. Since
shares are MPT this number is integral, and it will be rounded to nearest
unless explicitly requested to be truncated instead.
@param vault The vault SLE.
@param issuance The MPTokenIssuance SLE for the vault's shares.
@param assets The amount of assets to convert.
@param truncate Whether to truncate instead of rounding.
@return The number of shares, or nullopt on error.
*/
/** Compute the shares a withdrawer must redeem to receive a fixed asset amount.
*
* Unlike the deposit path, this function subtracts `sfLossUnrealized` from
* `sfAssetsTotal` before computing the exchange rate. Withdrawers receive fewer
* assets per share when the vault has recorded unrealized losses, preventing
* early withdrawers from exiting at inflated prices at the expense of remaining
* holders.
*
* The result is rounded to nearest by default (`TruncateShares::No`), ensuring
* the vault is not shortchanged. The withdraw transactor then back-calculates
* the actual assets delivered via `sharesToAssetsWithdraw()` for a precise
* two-step computation.
*
* If `sfAssetsTotal - sfLossUnrealized == 0` (fully insolvent vault), returns
* a zero-valued `STAmount` rather than dividing by zero.
*
* @throws std::overflow_error if arithmetic overflows XRPL's `Number` type;
* callers should catch and return `tecPATH_DRY`.
*
* @param vault The vault SLE; must contain `sfAsset`, `sfAssetsTotal`,
* `sfLossUnrealized`, and `sfShareMPTID`.
* @param issuance The MPTokenIssuance SLE for the vault's share token.
* @param assets The asset amount to convert; must be non-negative and must
* match `vault->at(sfAsset)`.
* @param truncate Whether to truncate instead of rounding to nearest.
* @return The integral share amount, or `nullopt` if `assets` is negative or
* its asset type does not match the vault.
*/
[[nodiscard]] std::optional<STAmount>
assetsToSharesWithdraw(
std::shared_ptr<SLE const> const& vault,
@@ -62,16 +127,25 @@ assetsToSharesWithdraw(
STAmount const& assets,
TruncateShares truncate = TruncateShares::No);
/** From the perspective of a vault, return the number of assets to give the
depositor when they redeem a fixed amount of shares. Note, since shares are
MPT, they are always an integral number.
@param vault The vault SLE.
@param issuance The MPTokenIssuance SLE for the vault's shares.
@param shares The amount of shares to convert.
@return The number of assets, or nullopt on error.
*/
/** Compute the assets delivered when a withdrawer redeems a fixed share amount.
*
* Like `assetsToSharesWithdraw()`, this function subtracts `sfLossUnrealized`
* from `sfAssetsTotal` before computing the exchange rate, so withdrawers
* bear their proportional share of any recorded losses.
*
* If `sfAssetsTotal - sfLossUnrealized == 0` (fully insolvent vault), returns
* a zero-valued `STAmount` rather than dividing by zero.
*
* @throws std::overflow_error if arithmetic overflows XRPL's `Number` type;
* callers should catch and return `tecPATH_DRY`.
*
* @param vault The vault SLE.
* @param issuance The MPTokenIssuance SLE for the vault's share token.
* @param shares The share amount to convert; must be non-negative and must
* match `vault->at(sfShareMPTID)`.
* @return The asset amount, or `nullopt` if `shares` is negative or its
* asset type does not match the vault's share MPT.
*/
[[nodiscard]] std::optional<STAmount>
sharesToAssetsWithdraw(
std::shared_ptr<SLE const> const& vault,

View File

@@ -6,33 +6,50 @@
namespace xrpl::NodeStore {
/** A backend used for the NodeStore.
The NodeStore uses a swappable backend so that other database systems
can be tried. Different databases may offer various features such
as improved performance, fault tolerant or distributed storage, or
all in-memory operation.
A given instance of a backend is fixed to a particular key size.
*/
/** Pure abstract storage interface for the NodeStore persistence layer.
*
* Every ledger object (account states, transactions, ledger headers) is a
* `NodeObject` keyed by its 256-bit hash. `Backend` defines the narrow
* interface that lets the `Database` layer remain independent of the
* underlying engine — NuDB, RocksDB, or an in-memory store for tests all
* satisfy this contract identically.
*
* A backend instance is fixed to a particular key size (always 32 bytes in
* practice, matching `NodeObject::keyBytes`) at construction.
*
* **Concurrency contract**: `fetch()` and `store()` will be called
* concurrently by multiple threads; implementations must be internally
* thread-safe for these two operations. `storeBatch()` and `forEach()` are
* never called concurrently with each other or with other writes.
*
* **Lifecycle**: Construction is separated from initialization via `open()`.
* Backends are never constructed directly — use `Factory::createInstance()`
* dispatched through `Manager`.
*
* @see Factory, Manager, Database
*/
class Backend
{
public:
/** Destroy the backend.
All open files are closed and flushed. If there are batched writes
or other tasks scheduled, they will be completed before this call
returns.
*/
*
* All open files are closed and flushed. Any batched writes or scheduled
* tasks complete before this returns, so dropping a `unique_ptr<Backend>`
* cannot silently discard data.
*/
virtual ~Backend() = default;
/** Get the human-readable name of this backend.
This is used for diagnostic output.
*/
/** Return the human-readable name of this backend, used in diagnostics. */
virtual std::string
getName() = 0;
/** Get the block size for backends that support it
/** Return the storage block size, if the backend has a meaningful one.
*
* NuDB organizes data into fixed-size blocks; callers that care about
* I/O alignment or prefetch granularity can query this without
* downcasting. Backends with no block concept return `std::nullopt`.
*
* @return Block size in bytes, or `std::nullopt` if not applicable.
*/
[[nodiscard]] virtual std::optional<std::size_t>
getBlockSize() const
@@ -40,25 +57,37 @@ public:
return std::nullopt;
}
/** Open the backend.
@param createIfMissing Create the database files if necessary.
This allows the caller to catch exceptions.
*/
/** Open the backend, optionally creating the database if absent.
*
* Separating `open()` from the constructor allows I/O errors to be
* caught without wrapping constructors in try/catch.
*
* @param createIfMissing If `true`, create the database files when they
* do not exist. Pass `false` to fail fast on a missing database.
* @throws implementation-defined exception on I/O or database errors.
*/
virtual void
open(bool createIfMissing = true) = 0;
/** Returns true is the database is open.
*/
/** Return `true` if the backend is currently open. */
virtual bool
isOpen() = 0;
/** Open the backend.
@param createIfMissing Create the database files if necessary.
@param appType Deterministic appType used to create a backend.
@param uid Deterministic uid used to create a backend.
@param salt Deterministic salt used to create a backend.
@throws std::runtime_error is function is called not for NuDB backend.
*/
/** Open the backend with deterministic NuDB header parameters.
*
* This overload exists exclusively to support NuDB's header-level
* application identification (appnum, uid, salt). It enables shard
* databases to be created with reproducible identifiers.
*
* @param createIfMissing Create the database files if they do not exist.
* @param appType Application-defined type tag embedded in the NuDB header.
* @param uid Deterministic unique identifier for this database instance.
* @param salt Deterministic salt value used during NuDB database creation.
* @throws std::runtime_error for every backend except NuDB, as this
* capability is not part of the general interface.
* @note Non-NuDB backends inherit a default implementation that always
* throws, clearly advertising that the capability is unavailable.
*/
virtual void
open(bool createIfMissing, uint64_t appType, uint64_t uid, uint64_t salt)
{
@@ -66,75 +95,137 @@ public:
"Deterministic appType/uid/salt not supported by backend " + getName());
}
/** Close the backend.
This allows the caller to catch exceptions.
*/
/** Close the backend, flushing any pending writes.
*
* Separating `close()` from the destructor allows the caller to catch
* and handle I/O exceptions explicitly.
*/
virtual void
close() = 0;
/** Fetch a single object.
If the object is not found or an error is encountered, the
result will indicate the condition.
@note This will be called concurrently.
@param hash The hash of the object.
@param pObject [out] The created object if successful.
@return The result of the operation.
*/
/** Fetch a single object by its 256-bit hash.
*
* On success, `*pObject` is set to the retrieved `NodeObject`. On any
* non-`Ok` outcome, `*pObject` is left unchanged (or reset).
*
* @note Called concurrently by multiple threads; implementations must
* be thread-safe for this operation.
* @param hash The 256-bit hash key identifying the object.
* @param pObject Output parameter; receives the fetched object on success.
* @return `Status::Ok` on success, `Status::NotFound` if the key is
* absent, `Status::DataCorrupt` if the stored blob fails validation,
* or another `Status` value on backend or unknown errors.
*/
virtual Status
fetch(uint256 const& hash, std::shared_ptr<NodeObject>* pObject) = 0;
/** Fetch a batch synchronously. */
/** Fetch a batch of objects by their 256-bit hashes.
*
* Amortizes round-trip or I/O overhead when prefetching sets of related
* objects. The returned vector is parallel to `hashes`: a null
* `shared_ptr` at position `i` indicates the object at `hashes[i]` was
* not found or could not be retrieved.
*
* @param hashes Ordered list of 256-bit hash keys to fetch.
* @return A pair of (results vector, aggregate Status). Each element in
* the results vector is the fetched object, or an empty
* `shared_ptr` if the corresponding hash was not found.
*/
virtual std::pair<std::vector<std::shared_ptr<NodeObject>>, Status>
fetchBatch(std::vector<uint256> const& hashes) = 0;
/** Store a single object.
Depending on the implementation this may happen immediately
or deferred using a scheduled task.
@note This will be called concurrently.
@param object The object to store.
*/
*
* Depending on the implementation, the write may be synchronous or
* deferred to a scheduled task (e.g., via `BatchWriter`). Either way,
* the object is guaranteed to be durable before the backend is destroyed.
*
* @note Called concurrently by multiple threads; implementations must
* be thread-safe for this operation.
* @param object The `NodeObject` to persist.
*/
virtual void
store(std::shared_ptr<NodeObject> const& object) = 0;
/** Store a group of objects.
@note This function will not be called concurrently with
itself or @ref store.
*/
/** Store a group of objects as a batch.
*
* More efficient than repeated `store()` calls for backends that
* support atomic or coalesced multi-key writes (e.g., RocksDB
* `WriteBatch`). The entire batch is treated as a single unit.
*
* @note Never called concurrently with itself or with `store()`.
* @param batch The collection of `NodeObject`s to persist.
*/
virtual void
storeBatch(Batch const& batch) = 0;
/** Flush all previously submitted stores to durable storage.
*
* Provides an explicit durability barrier: after `sync()` returns,
* all objects passed to `store()` or `storeBatch()` before the call
* are guaranteed to be on disk. Backends backed by a write-ahead log
* (e.g., RocksDB) may implement this as a no-op.
*/
virtual void
sync() = 0;
/** Visit every object in the database
This is usually called during import.
@note This routine will not be called concurrently with itself
or other methods.
@see import
*/
/** Invoke a callback for every object stored in the backend.
*
* Typically used during database import or migration. Because it closes
* and reopens the underlying database (NuDB), it must not be called
* while concurrent reads or writes are in flight.
*
* @note Never called concurrently with itself or with any other method.
* @param f Callback invoked once per stored object; receives a
* `shared_ptr<NodeObject>` for each entry in the database.
* @see importInternal
*/
virtual void
forEach(std::function<void(std::shared_ptr<NodeObject>)> f) = 0;
/** Estimate the number of write operations pending. */
/** Return an estimate of the number of pending write operations.
*
* Used by the `Database` layer for back-pressure and diagnostic
* reporting. The value is advisory; implementations may return 0 if
* writes are always synchronous (e.g., NuDB).
*
* @return Approximate count of writes not yet flushed to storage.
*/
virtual int
getWriteLoad() = 0;
/** Remove contents on disk upon destruction. */
/** Schedule the backend's on-disk files for deletion on destruction.
*
* After this call, the next `close()` (including the one in the
* destructor) removes all database files from the filesystem. Used by
* temporary databases — unit tests and ephemeral shard stores — that
* require automatic cleanup without external management.
*/
virtual void
setDeletePath() = 0;
/** Perform consistency checks on database.
/** Perform an offline consistency check of the stored data.
*
* This method is implemented only by NuDBBackend. It is not yet called
* anywhere, but it might be a good idea to one day call it at startup to
* avert a crash.
* Closes and reopens the database around the check, so it must not be
* called while I/O is in progress. Currently implemented only by
* `NuDBBackend`; all other backends inherit a no-op.
*
* @note Not yet called at startup, but could one day be invoked at
* launch to detect on-disk corruption before it causes a crash.
*/
virtual void
verify()
{
}
/** Returns the number of file descriptors the backend expects to need. */
/** Return the number of file descriptors this backend expects to consume.
*
* The `Database` base class aggregates these values across all open
* backends and exposes the total so the process can pre-check against
* the OS file descriptor limit before opening any databases.
*
* @return Expected file descriptor count (e.g., 3 for NuDB, 0 for Null).
*/
[[nodiscard]] virtual int
fdRequired() const = 0;
};

View File

@@ -1,3 +1,14 @@
/** @file
* Abstract base class for the NodeStore persistence layer.
*
* Defines the full public contract for node object storage: async and
* synchronous fetch, store, import, and diagnostics. Concrete subclasses
* (`DatabaseNodeImp`, `DatabaseRotatingImp`) implement the private virtual
* `fetchNodeObject()` and `forEach()` hooks; all instrumentation (timing,
* counters, scheduler callbacks) is applied in this base class and cannot
* be bypassed.
*/
#pragma once
#include <xrpl/basics/BasicConfig.h>
@@ -12,100 +23,159 @@
namespace xrpl::NodeStore {
/** Persistency layer for NodeObject
A Node is a ledger object which is uniquely identified by a key, which is
the 256-bit hash of the body of the node. The payload is a variable length
block of serialized data.
All ledger data is stored as node objects and as such, needs to be persisted
between launches. Furthermore, since the set of node objects will in
general be larger than the amount of available memory, purged node objects
which are later accessed must be retrieved from the node store.
@see NodeObject
*/
/** Persistence layer for NodeObject records.
*
* Every ledger datum — account states, transactions, ledger headers — is
* stored as a `NodeObject` keyed by the 256-bit hash of its payload. Because
* the total object set typically exceeds available memory, any hash absent
* from the in-memory cache must be fetched from disk through this class.
*
* `Database` owns the async read thread pool and all performance counters.
* The public non-virtual `fetchNodeObject()` wraps the private pure-virtual
* one, applying timing, hit/miss accounting, and `Scheduler::onFetch()`
* callbacks — so no subclass can escape the instrumentation.
*
* **Shutdown ordering**: Derived classes **must** call `stop()` in their own
* destructors before the base destructor runs. Worker threads invoke the
* virtual `fetchNodeObject()` through a subclass vtable; if the derived
* object is destroyed before all threads have exited, a waking thread will
* call through a dangling vtable entry (undefined behaviour). The base
* destructor calls `stop()` only as a last-resort safety net.
*
* @see NodeObject, Backend, Scheduler, DatabaseNodeImp, DatabaseRotatingImp
*/
class Database
{
public:
Database() = delete;
/** Construct the node store.
@param scheduler The scheduler to use for performing asynchronous tasks.
@param readThreads The number of asynchronous read threads to create.
@param config The configuration settings
@param journal Destination for logging output.
*/
/** Construct the node store and start the async read thread pool.
*
* Validates configuration parameters, then spawns `readThreads` detached
* worker threads. Threads are controlled by `readStopping_`; `stop()`
* spin-waits (≤ 30 s) until `readThreads_` reaches zero.
*
* @param scheduler Task scheduler for async I/O dispatch and telemetry
* callbacks; must outlive this object.
* @param readThreads Number of prefetch worker threads to create; clamped
* to at least 1.
* @param config `[node_db]` config section; reads `earliest_seq` (default
* `kXRP_LEDGER_EARLIEST_SEQ`, must be ≥ 1) and `rq_bundle` (default 4,
* clamped [1, 64]).
* @param j Logging sink.
* @throws std::runtime_error if `earliest_seq` < 1 or `rq_bundle` is
* outside [1, 64].
*/
Database(Scheduler& scheduler, int readThreads, Section const& config, beast::Journal j);
/** Destroy the node store.
All pending operations are completed, pending writes flushed,
and files closed before this returns.
*/
*
* Calls `stop()` as a safety net to drain the read queue and wait for all
* worker threads to exit. Derived classes **must** call `stop()` in their
* own destructors first — worker threads invoke the pure-virtual
* `fetchNodeObject()` through the subclass vtable, which is already gone
* by the time this base destructor runs.
*/
virtual ~Database();
/** Retrieve the name associated with this backend.
This is used for diagnostics and may not reflect the actual path
or paths used by the underlying backend.
*/
/** Return the name of the underlying backend for diagnostics.
*
* The returned string may not reflect the actual on-disk path when
* multiple backends are in use (e.g. `DatabaseRotatingImp`).
*
* @return A human-readable backend identifier.
*/
virtual std::string
getName() const = 0;
/** Import objects from another database. */
/** Bulk-import all objects from another database into this one.
*
* Iterates every `NodeObject` in @p source and writes it to this
* database's backend. Implementations typically delegate to
* `importInternal()`. Large databases may take significant time.
*
* @param source The source database to read from; must remain valid
* and quiescent (no concurrent writes) for the duration of the call.
*/
virtual void
importDatabase(Database& source) = 0;
/** Retrieve the estimated number of pending write operations.
This is used for diagnostics.
*/
/** Return the estimated number of pending write operations.
*
* Used for backpressure diagnostics; the value is approximate and may
* change immediately after it is read.
*
* @return Pending write count, or 0 if the backend does not batch writes.
*/
virtual std::int32_t
getWriteLoad() const = 0;
/** Store the object.
The caller's Blob parameter is overwritten.
@param type The type of object.
@param data The payload of the object. The caller's
variable is overwritten.
@param hash The 256-bit hash of the payload data.
@param ledgerSeq The sequence of the ledger the object belongs to.
@return `true` if the object was stored?
*/
/** Persist a node object to the backend.
*
* Takes ownership of @p data (the caller's `Blob` is consumed). The object
* is keyed by @p hash; backends are content-addressed, so storing an object
* whose hash already exists is a no-op (same key → same data).
*
* @param type The semantic type of the object (ledger, account node, etc.).
* @param data Serialized payload; moved into the backend — caller's variable
* is left in a valid but unspecified state.
* @param hash 256-bit hash of @p data. The caller is responsible for
* correctness; the hash is not re-verified by the store.
* @param ledgerSeq The ledger sequence this object belongs to; used by
* rotating backends to route writes to the correct physical file.
*/
virtual void
store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t ledgerSeq) = 0;
/* Check if two ledgers are in the same database
If these two sequence numbers map to the same database,
the result of a fetch with either sequence number would
be identical.
@param s1 The first sequence number
@param s2 The second sequence number
@return 'true' if both ledgers would be in the same DB
*/
/** Return whether two ledger sequence numbers resolve to the same backend.
*
* When this returns `true`, a fetch with either sequence number will
* reach the same physical storage and yield identical results. The async
* thread pool uses this to avoid redundant backend reads when multiple
* callbacks for the same hash were registered with different sequence
* numbers.
*
* `DatabaseNodeImp` always returns `true` (single backend).
* `DatabaseRotatingImp` returns `false` when the sequences straddle a
* rotation boundary.
*
* @param s1 First ledger sequence number.
* @param s2 Second ledger sequence number.
* @return `true` if both sequences map to the same physical backend.
*/
virtual bool
isSameDB(std::uint32_t s1, std::uint32_t s2) = 0;
/** Flush any buffered writes to durable storage.
*
* Called by maintenance paths (e.g. ledger close) to ensure consistency.
* Not latency-sensitive; implementations may hold locks for the full call.
*/
virtual void
sync() = 0;
/** Fetch a node object.
If the object is known to be not in the database, isn't found in the
database during the fetch, or failed to load correctly during the fetch,
`nullptr` is returned.
@note This can be called concurrently.
@param hash The key of the object to retrieve.
@param ledgerSeq The sequence of the ledger where the object is stored.
@param fetchType the type of fetch, synchronous or asynchronous.
@return The object, or nullptr if it couldn't be retrieved.
*/
/** Fetch a node object by hash, recording timing and hit/miss metrics.
*
* This is the public entry point for all node lookups. It wraps the
* private pure-virtual `fetchNodeObject(hash, seq, FetchReport&, duplicate)`
* using the Template Method pattern: timing, atomic counters, and
* `Scheduler::onFetch()` are applied here and cannot be bypassed by
* subclasses.
*
* Returns `nullptr` if the object is absent, could not be decoded, or the
* backend encountered an error.
*
* @note Thread-safe; may be called concurrently from any thread.
* @param hash 256-bit content hash of the desired object.
* @param ledgerSeq Ledger sequence that owns this object; used by rotating
* backends to select the correct physical file. Defaults to 0.
* @param fetchType `FetchType::Synchronous` (default) or
* `FetchType::Async` when called from the async worker pool.
* @param duplicate When `true`, the object is also written into the
* writable backend after being found in the archive backend
* (`DatabaseRotatingImp` promotion path). Defaults to `false`.
* @return The requested `NodeObject`, or `nullptr` on miss or error.
*/
std::shared_ptr<NodeObject>
fetchNodeObject(
uint256 const& hash,
@@ -113,75 +183,124 @@ public:
FetchType fetchType = FetchType::Synchronous,
bool duplicate = false);
/** Fetch an object without waiting.
If I/O is required to determine whether or not the object is present,
`false` is returned. Otherwise, `true` is returned and `object` is set
to refer to the object, or `nullptr` if the object is not present.
If I/O is required, the I/O is scheduled and `true` is returned
@note This can be called concurrently.
@param hash The key of the object to retrieve
@param ledgerSeq The sequence of the ledger where the
object is stored.
@param callback Callback function when read completes
*/
/** Schedule a non-blocking background fetch for a node object.
*
* Enqueues a `(hash, ledgerSeq, callback)` entry in the async read map.
* Multiple calls for the same hash are coalesced: a single backend read
* satisfies all registered callbacks. If `isStopping()` is `true` at the
* time of the call, the request is silently discarded and the callback
* will never fire.
*
* @note Thread-safe; may be called concurrently from any thread.
* @param hash 256-bit content hash of the desired object.
* @param ledgerSeq Ledger sequence that owns this object; passed through
* to `isSameDB()` for multi-sequence coalescing.
* @param callback Invoked on a worker thread with the fetched
* `NodeObject`, or `nullptr` on miss or error.
*/
virtual void
asyncFetch(
uint256 const& hash,
std::uint32_t ledgerSeq,
std::function<void(std::shared_ptr<NodeObject> const&)>&& callback);
/** Gather statistics pertaining to read and write activities.
*
* @param obj Json object reference into which to place counters.
*/
// --- Performance counters (all lock-free atomic reads) ---
/** Return the total number of objects written since construction. */
std::uint64_t
getStoreCount() const
{
return storeCount_;
}
/** Return the total number of fetch attempts (hits + misses). */
std::uint32_t
getFetchTotalCount() const
{
return fetchTotalCount_;
}
/** Return the number of fetch attempts that found the requested object. */
std::uint32_t
getFetchHitCount() const
{
return fetchHitCount_;
}
/** Return the cumulative byte count of all stored objects. */
std::uint64_t
getStoreSize() const
{
return storeSz_;
}
/** Return the cumulative byte count of all successfully fetched objects. */
std::uint32_t
getFetchSize() const
{
return fetchSz_;
}
/** Populate a JSON object with read/write diagnostics for `get_counts` RPC.
*
* Snapshots the async read queue depth (under `readLock_`) and then reads
* thread counts, request bundle size, and all atomic counters without
* holding any lock. The resulting fields include: `read_queue`,
* `read_threads_total`, `read_threads_running`, `read_request_bundle`,
* `node_writes`, `node_reads_total`, `node_reads_hit`,
* `node_written_bytes`, `node_read_bytes`, `node_reads_duration_us`.
*
* @param obj A JSON object to populate; must satisfy `obj.isObject()`.
*/
void
getCountsJson(json::Value& obj);
/** Returns the number of file descriptors the database expects to need */
/** Return the number of file descriptors this database expects to hold open.
*
* Aggregated from the underlying backend(s). Used by the application to
* check that the process file-descriptor limit is sufficient before
* opening backends. Inaccurate values cause silent failures when the
* limit is exceeded.
*
* @return File descriptor count, or 0 if not set by the subclass.
*/
int
fdRequired() const
{
return fdRequired_;
}
/** Begin orderly shutdown of the async read thread pool.
*
* Sets `readStopping_`, clears the pending `read_` queue, broadcasts on
* `readCondVar_`, then spin-yields until `readThreads_` reaches zero.
* An assertion fires if shutdown takes longer than 30 seconds.
*
* Idempotent: a second call after shutdown has already completed is a
* no-op. Derived classes must call this in their own destructors before
* their data members are torn down.
*/
virtual void
stop();
/** Return whether `stop()` has been called.
*
* Uses a relaxed atomic load — only the flag value is observed; no
* ordering is imposed on surrounding operations.
*
* @return `true` once `stop()` has been invoked.
*/
bool
isStopping() const;
/** @return The earliest ledger sequence allowed
/** Return the earliest ledger sequence this database will serve.
*
* Configured via `earliest_seq` in `[node_db]`; defaults to
* `kXRP_LEDGER_EARLIEST_SEQ` (32570 on the main network). The value is
* constant after construction. Only unit tests or alternate networks
* should set this below the default.
*
* @return The minimum valid ledger sequence number, always ≥ 1.
*/
[[nodiscard]] std::uint32_t
earliestLedgerSeq() const noexcept
@@ -190,26 +309,34 @@ public:
}
protected:
beast::Journal const j_;
Scheduler& scheduler_;
beast::Journal const j_; ///< Logging sink; set at construction.
Scheduler& scheduler_; ///< Task scheduler for async dispatch and telemetry.
/** Number of file descriptors consumed by the underlying backend(s).
* Subclasses set this in their constructors; read by `fdRequired()`.
*/
int fdRequired_{0};
std::atomic<std::uint32_t> fetchHitCount_{0};
std::atomic<std::uint32_t> fetchSz_{0};
std::atomic<std::uint32_t> fetchHitCount_{0}; ///< Fetches that returned a non-null object.
std::atomic<std::uint32_t> fetchSz_{0}; ///< Cumulative bytes returned by successful fetches.
// The default is XRP_LEDGER_EARLIEST_SEQ (32570) to match the XRP ledger
// network's earliest allowed ledger sequence. Can be set through the
// configuration file using the 'earliest_seq' field under the 'node_db'
// stanza. If specified, the value must be greater than zero.
// Only unit tests or alternate
// networks should change this value.
/** Minimum ledger sequence this store will serve; constant after construction.
* Defaults to `kXRP_LEDGER_EARLIEST_SEQ` (32570). Must be ≥ 1.
*/
std::uint32_t const earliestLedgerSeq_;
// The maximum number of requests a thread extracts from the queue in an
// attempt to minimize the overhead of mutex acquisition. This is an
// advanced tunable, via the config file. The default value is 4.
/** Maximum number of read-queue entries extracted per mutex acquisition.
* Amortises lock overhead under load. Configured via `rq_bundle` in
* `[node_db]`; clamped to [1, 64]; defaults to 4.
*/
int const requestBundle_;
/** Update store counters after a successful batch write.
*
* @param count Number of objects written.
* @param sz Total byte size of those objects.
* @note Asserts `count <= sz` — byte total must be ≥ item count.
*/
void
storeStats(std::uint64_t count, std::uint64_t sz)
{
@@ -218,10 +345,32 @@ protected:
storeSz_ += sz;
}
// Called by the public import function
/** Bulk-import all objects from @p srcDB into @p dstBackend.
*
* Iterates @p srcDB via `forEach()`, accumulates objects into batches of
* `kBATCH_WRITE_PREALLOCATION_SIZE`, and flushes each batch with
* `dstBackend.storeBatch()`. Byte statistics are recorded via
* `storeStats()` after each flush. On exception, logs the error and
* returns early without aborting the overall import.
*
* Called by subclass `importDatabase()` implementations.
*
* @param dstBackend Destination backend; must be open and writable.
* @param srcDB Source database; iterated sequentially — no concurrent
* writes to @p srcDB should occur during the call.
*/
void
importInternal(Backend& dstBackend, Database& srcDB);
/** Merge externally-collected fetch metrics into the atomic counters.
*
* Used by subclasses that perform their own batched reads (e.g. import
* paths) and need to credit the counters in bulk rather than per-object.
*
* @param fetches Number of fetch attempts to add to `fetchTotalCount_`.
* @param hits Number of successful fetches to add to `fetchHitCount_`.
* @param duration Elapsed microseconds to add to `fetchDurationUs_`.
*/
void
updateFetchMetrics(uint64_t fetches, uint64_t hits, uint64_t duration)
{
@@ -231,26 +380,51 @@ protected:
}
private:
std::atomic<std::uint64_t> storeCount_{0};
std::atomic<std::uint64_t> storeSz_{0};
std::atomic<std::uint64_t> fetchTotalCount_{0};
std::atomic<std::uint64_t> fetchDurationUs_{0};
std::atomic<std::uint64_t> storeDurationUs_{0};
// --- Write-side atomic counters ---
std::atomic<std::uint64_t> storeCount_{0}; ///< Total objects stored.
std::atomic<std::uint64_t> storeSz_{0}; ///< Total bytes stored.
std::atomic<std::uint64_t> storeDurationUs_{0}; ///< Cumulative store duration (µs); reserved.
mutable std::mutex readLock_;
std::condition_variable readCondVar_;
// --- Fetch-side atomic counters (incremented by the public fetchNodeObject wrapper) ---
std::atomic<std::uint64_t> fetchTotalCount_{0}; ///< Total fetch attempts.
std::atomic<std::uint64_t> fetchDurationUs_{0}; ///< Cumulative fetch duration (µs).
// reads to do
// --- Async read-queue state (all guarded by readLock_ except atomic members) ---
mutable std::mutex readLock_; ///< Guards `read_` and `readCondVar_`.
std::condition_variable readCondVar_; ///< Wakes worker threads when `read_` is non-empty or stopping.
/** Pending async read requests, keyed by hash.
*
* Each map entry holds all `(ledgerSeq, callback)` pairs registered for a
* given hash. Multiple calls to `asyncFetch()` with the same hash are
* coalesced here so that a single backend read services all callbacks.
*/
std::map<
uint256,
std::vector<
std::pair<std::uint32_t, std::function<void(std::shared_ptr<NodeObject> const&)>>>>
read_;
std::atomic<bool> readStopping_ = false;
std::atomic<int> readThreads_ = 0;
std::atomic<int> runningThreads_ = 0;
std::atomic<bool> readStopping_ = false; ///< Set by `stop()`; workers exit when observed.
std::atomic<int> readThreads_ = 0; ///< Count of live worker threads; reaches 0 on full stop.
std::atomic<int> runningThreads_ = 0; ///< Threads currently active (not blocked on condvar).
/** Backend fetch hook — the Template Method target.
*
* Called exclusively by the public non-virtual `fetchNodeObject()` wrapper,
* which applies timing and metrics around this call. Subclasses must
* implement this and may not call the public wrapper from within it.
*
* @param hash 256-bit content hash to look up.
* @param ledgerSeq Ledger sequence, used by rotating backends to select
* the correct physical file.
* @param fetchReport Mutable report populated by the implementation;
* the public wrapper reads `fetchReport.wasFound` and `elapsed`.
* @param duplicate When `true`, if the object is found in the archive
* backend it should also be written back to the writable backend
* (promotion path for `DatabaseRotatingImp`).
* @return The fetched `NodeObject`, or `nullptr` on miss or error.
*/
virtual std::shared_ptr<NodeObject>
fetchNodeObject(
uint256 const& hash,
@@ -258,16 +432,26 @@ private:
FetchReport& fetchReport,
bool duplicate) = 0;
/** Visit every object in the database
This is usually called during import.
@note This routine will not be called concurrently with itself
or other methods.
@see import
*/
/** Iterate every object in the database and invoke @p f for each one.
*
* Used exclusively by `importInternal()`. Implementations may close and
* reopen the underlying store (e.g. NuDB) and are not safe for concurrent
* access; the caller must ensure no other reads or writes occur during
* iteration.
*
* @note Never called concurrently with itself or other methods.
* @param f Callback invoked with each `NodeObject`; must not be null.
*/
virtual void
forEach(std::function<void(std::shared_ptr<NodeObject>)> f) = 0;
/** Worker thread body for the async read pool.
*
* Loops waiting on `readCondVar_`, extracts up to `requestBundle_` entries
* from `read_` per lock acquisition, and dispatches each to the private
* `fetchNodeObject()`. Handles multi-sequence coalescing via `isSameDB()`.
* Exits when `readStopping_` is observed, then decrements `readThreads_`.
*/
void
threadEntry();
};

View File

@@ -1,17 +1,47 @@
/** @file
* Abstract interface extending `Database` with a two-backend rotation
* operation for online ledger history deletion.
*/
#pragma once
#include <xrpl/nodestore/Database.h>
namespace xrpl::NodeStore {
/* This class has two key-value store Backend objects for persisting SHAMap
* records. This facilitates online deletion of data. New backends are
* rotated in. Old ones are rotated out and deleted.
/** Abstract seam for the two-backend rotation scheme that enables online
* deletion of ledger history without taking the node offline.
*
* The concrete subclass `DatabaseRotatingImp` maintains two physical
* `Backend` objects: a *writable* backend that receives all current writes
* and an *archive* backend holding older data. When enough new history has
* accumulated, `SHAMapStoreImp` calls `rotate()` to atomically promote the
* writable backend to the archive role, install a fresh writable backend,
* and discard the old archive — all without interrupting read or write
* traffic.
*
* `DatabaseRotating` carries no state; it extends `Database` solely with
* the `rotate()` pure-virtual method. Components that drive rotation
* (currently only `SHAMapStoreImp`) hold a `DatabaseRotating*` pointer,
* keeping the rotation mechanism decoupled from storage format.
*
* @see DatabaseRotatingImp, Database, SHAMapStoreImp
*/
class DatabaseRotating : public Database
{
public:
/** Construct the rotating database and start the async read thread pool.
*
* Delegates entirely to `Database(scheduler, readThreads, config,
* journal)`. The two physical backends are supplied when constructing
* the concrete `DatabaseRotatingImp` subclass.
*
* @param scheduler Task scheduler for async I/O dispatch and telemetry;
* must outlive this object.
* @param readThreads Number of prefetch worker threads to create.
* @param config `[node_db]` config section forwarded to `Database`.
* @param journal Logging sink.
*/
DatabaseRotating(
Scheduler& scheduler,
int readThreads,
@@ -21,13 +51,37 @@ public:
{
}
/** Rotates the backends.
@param newBackend New writable backend
@param f A function executed after the rotation outside of lock. The
values passed to f will be the new backend database names _after_
rotation.
*/
/** Atomically replace the current writable backend with @p newBackend.
*
* Performs a three-step pointer swap under the implementation's internal
* mutex:
* 1. Mark the current archive backend for directory deletion and stash it
* in a local `shared_ptr` to keep it alive past the lock release.
* 2. Demote the current writable backend to the archive role.
* 3. Install @p newBackend as the new writable backend.
*
* After releasing the lock, @p f is called with the new backend names.
* Only after @p f returns does the old archive `shared_ptr` go out of
* scope and its on-disk files are removed. This sequencing is
* **crash-safe**: if the process dies between the pointer swap and @p f
* completing, both directory sets still exist on disk and can be
* recovered from the SQL state database on restart.
*
* Concurrent fetches already in flight hold `shared_ptr` references to
* the old backends; reference counting keeps those backends alive until
* all in-flight I/O completes.
*
* @param newBackend Freshly created, opened backend that becomes the new
* writable store; ownership is transferred.
* @param f Callback invoked after the in-memory swap completes but
* before the old archive is deleted, and outside the implementation
* mutex. Receives two names post-rotation: @p writableName is the
* name of @p newBackend, and @p archiveName is the name of the former
* writable backend now serving as the archive. `SHAMapStoreImp` uses
* @p f to durably persist the new backend names and `lastRotated`
* ledger sequence to a SQL state database, creating an atomic
* checkpoint for crash recovery.
*/
virtual void
rotate(
std::unique_ptr<NodeStore::Backend>&& newBackend,

View File

@@ -4,16 +4,64 @@
namespace xrpl::NodeStore {
/** Simple NodeStore Scheduler that just performs the tasks synchronously. */
/** Null-object implementation of @ref Scheduler for tests and offline import.
*
* Satisfies the full `Scheduler` interface contract while doing the minimum
* possible work: every task is executed immediately on the calling thread, and
* the two performance-reporting hooks are no-ops. There is no thread pool, no
* queue, and no statistics collection.
*
* The `Scheduler` contract explicitly permits running a task on the calling
* thread, so `DummyScheduler` is always correct — it differs from a
* production scheduler only in latency and throughput characteristics.
*
* **Effect on `BatchWriter`**: Because `scheduleTask` flushes the batch
* inline before returning, batching is effectively disabled. This is
* acceptable for import and test workloads but would degrade performance
* under normal ledger-processing load.
*
* **Typical call sites**:
* - `Application.cpp` — transient scheduler for the source database during
* node-startup `doImport`; sequential offline migration makes async
* scheduling unnecessary.
* - `Backend_test.cpp`, `Database_test.cpp`, `Timing_test.cpp`,
* `NuDBFactory_test.cpp`, `shamap/common.h` — test fixtures use this to
* obtain deterministic, single-threaded execution without the teardown
* complexity of a real async scheduler.
*
* @see Scheduler
* @see BatchWriter
*/
class DummyScheduler : public Scheduler
{
public:
DummyScheduler() = default;
~DummyScheduler() override = default;
/** Execute @p task synchronously on the calling thread.
*
* Calls `task.performScheduledTask()` directly and returns only after
* the task completes. With `BatchWriter` as the consumer, this causes
* the pending write batch to be flushed inline, disabling asynchronous
* batching.
*
* @param task The task to execute; must remain valid for the duration
* of the call.
*/
void
scheduleTask(Task& task) override;
/** No-op performance hook — fetch telemetry is not collected.
*
* @param report Ignored.
*/
void
onFetch(FetchReport const& report) override;
/** No-op performance hook — batch-write telemetry is not collected.
*
* @param report Ignored.
*/
void
onBatchWrite(BatchWriteReport const& report) override;
};

View File

@@ -9,24 +9,55 @@
namespace xrpl::NodeStore {
/** Base class for backend factories. */
/** Abstract factory for constructing pluggable NodeStore `Backend` instances.
*
* Each concrete subclass wraps one storage engine (NuDB, RocksDB, memory,
* null). Subclasses register themselves with the `Manager` singleton at
* program startup by calling `Manager::insert(*this)` from their constructor,
* typically via a module-level `register*Factory()` free function that holds
* the factory as a function-local static. `Manager::find()` then resolves the
* `type=` configuration string to the matching factory by name.
*
* @note Factory objects are stored as raw (non-owning) pointers in
* `ManagerImp`. Concrete factories registered as function-local statics
* have program lifetime and must outlive the `Manager`.
*
* @see Backend, Manager
*/
class Factory
{
public:
virtual ~Factory() = default;
/** Retrieve the name of this factory. */
/** Return the configuration type string that identifies this backend.
*
* The returned name is used as the lookup key by `Manager::find()`,
* which compares case-insensitively against the `type=` value in the
* `[node_db]` config section (e.g., `"NuDB"`, `"RocksDB"`, `"memory"`).
*
* @return The backend type name (e.g., `"NuDB"`).
*/
[[nodiscard]] virtual std::string
getName() const = 0;
/** Create an instance of this factory's backend.
@param keyBytes The fixed number of bytes per key.
@param parameters A set of key/value configuration pairs.
@param burstSize Backend burst size in bytes.
@param scheduler The scheduler to use for running tasks.
@return A pointer to the Backend object.
*/
/** Construct a Backend from configuration, without a shared NuDB context.
*
* The returned backend has not yet been opened; the caller must invoke
* `Backend::open()` before performing any I/O. In production, this is
* done by `ManagerImp::makeDatabase()`.
*
* @param keyBytes Fixed width of every storage key in bytes. Always 32
* (SHA-512 Half) in production; may differ in tests.
* @param parameters Key/value pairs from the `[node_db]` config section,
* supplying backend-specific settings such as `path` and
* `nudb_block_size`.
* @param burstSize Maximum bytes the backend may buffer before flushing.
* Flows directly into NuDB's `db_.set_burst()` after open; other
* backends may use or ignore it.
* @param scheduler Async task dispatcher for background write jobs.
* @param journal Logging sink for backend diagnostics.
* @return An unopened, uniquely-owned Backend instance.
*/
virtual std::unique_ptr<Backend>
createInstance(
size_t keyBytes,
@@ -35,15 +66,24 @@ public:
Scheduler& scheduler,
beast::Journal journal) = 0;
/** Create an instance of this factory's backend.
@param keyBytes The fixed number of bytes per key.
@param parameters A set of key/value configuration pairs.
@param burstSize Backend burst size in bytes.
@param scheduler The scheduler to use for running tasks.
@param context The context used by database.
@return A pointer to the Backend object.
*/
/** Construct a Backend sharing an existing NuDB I/O context.
*
* This overload is provided for NuDB backends that share a `nudb::context`
* thread pool across multiple backends (e.g., the rotating database used
* for shard imports). Non-NuDB factories inherit a default implementation
* that returns an empty `unique_ptr`, signaling to `ManagerImp` that this
* backend does not use a NuDB context; the caller falls back to the
* context-free overload in that case.
*
* @param keyBytes Fixed width of every storage key in bytes.
* @param parameters Key/value pairs from the `[node_db]` config section.
* @param burstSize Maximum bytes the backend may buffer before flushing.
* @param scheduler Async task dispatcher for background write jobs.
* @param context Shared NuDB I/O thread pool. Ignored by non-NuDB backends.
* @param journal Logging sink for backend diagnostics.
* @return An unopened Backend, or an empty `unique_ptr` if this factory
* does not support the NuDB context overload.
*/
virtual std::unique_ptr<Backend>
createInstance(
size_t keyBytes,

View File

@@ -5,7 +5,28 @@
namespace xrpl::NodeStore {
/** Singleton for managing NodeStore factories and back ends. */
/** Abstract interface for the NodeStore backend registry and factory.
*
* `Manager` maps the `type=` string from `[node_db]` in `xrpld.cfg` to the
* concrete `Backend` implementation that implements it, and exposes the two
* construction entry points the rest of the application needs: `makeBackend()`
* for a raw storage engine and `makeDatabase()` for a fully-wired `Database`.
*
* The concrete implementation is `ManagerImp`, a Meyers singleton returned by
* `instance()`. Its constructor eagerly registers the four built-in backends
* (NuDB, RocksDB, memory, null). Additional backends may be registered at
* runtime via `insert()`. The abstract base class is exposed here so callers
* depend only on the interface without being coupled to `ManagerImp` or its
* dependencies.
*
* All registry operations (`insert`, `erase`, `find`) are protected by an
* internal mutex and are safe to call concurrently.
*
* @note Copy construction and copy assignment are deleted — there is exactly
* one manager for the lifetime of the process.
*
* @see Factory, Backend, Database
*/
class Manager
{
public:
@@ -15,26 +36,81 @@ public:
Manager&
operator=(Manager const&) = delete;
/** Returns the instance of the manager singleton. */
/** Return the process-wide Manager singleton.
*
* Delegates to `ManagerImp::instance()`, which uses a Meyers static local
* for thread-safe, once-only initialization under C++11 and later. The
* four built-in backends are registered before the reference is returned
* for the first time.
*
* @return A reference to the singleton `ManagerImp`.
*/
static Manager&
instance();
/** Add a factory. */
/** Register a backend factory with the manager.
*
* After insertion, `find(factory.getName())` will return `&factory`. The
* call is protected by a mutex and safe to make concurrently. The manager
* stores a non-owning pointer; the caller is responsible for ensuring the
* factory outlives the manager (function-local statics are the idiomatic
* approach).
*
* @param factory The factory to register. Must remain alive for the
* lifetime of the manager.
*/
virtual void
insert(Factory& factory) = 0;
/** Remove a factory. */
/** Deregister a previously inserted backend factory.
*
* Removes `factory` from the internal list. The call is protected by a
* mutex. Passing a pointer that was never inserted triggers an
* `XRPL_ASSERT`.
*
* @note Built-in backend factories registered by `ManagerImp`'s
* constructor are intentionally never erased: because static-storage
* destruction order across translation units is undefined, calling
* `erase()` from a `Factory` destructor could invoke a destroyed
* `ManagerImp`. The built-in factories use function-local statics
* that outlive the manager.
*
* @param factory The factory to remove. Must have been previously passed
* to `insert()`.
*/
virtual void
erase(Factory& factory) = 0;
/** Return a pointer to the matching factory if it exists.
@param name The name to match, performed case-insensitive.
@return `nullptr` if a match was not found.
*/
/** Look up a factory by its type name.
*
* Comparison is case-insensitive (via `boost::iequals`), so `"NuDB"`,
* `"nudb"`, and `"NUDB"` all resolve to the same factory. The call is
* protected by a mutex.
*
* @param name The backend type name to search for (e.g., `"NuDB"`).
* @return Pointer to the matching `Factory`, or `nullptr` if none found.
*/
virtual Factory*
find(std::string const& name) = 0;
/** Create a backend. */
/** Construct an unopened Backend from configuration parameters.
*
* Reads the `type` key from `parameters`, resolves it to a registered
* `Factory` via `find()`, and delegates to `Factory::createInstance()`.
* The returned backend has not yet been opened; the caller must invoke
* `Backend::open()` before performing any I/O (this is done automatically
* by `makeDatabase()`).
*
* @param parameters Key/value pairs from the `[node_db]` config section.
* Must contain a `type` key naming a registered backend.
* @param burstSize Maximum bytes the backend may buffer before flushing.
* @param scheduler Async task dispatcher for background write jobs.
* @param journal Logging sink for backend diagnostics.
* @return A uniquely-owned, unopened Backend instance.
* @throws std::runtime_error If the `type` key is absent or names an
* unrecognised backend, with a message directing the operator to
* check `xrpld.cfg`.
*/
virtual std::unique_ptr<Backend>
makeBackend(
Section const& parameters,
@@ -42,34 +118,25 @@ public:
Scheduler& scheduler,
beast::Journal journal) = 0;
/** Construct a NodeStore database.
The parameters are key value pairs passed to the backend. The
'type' key must exist, it defines the choice of backend. Most
backends also require a 'path' field.
Some choices for 'type' are:
HyperLevelDB, LevelDBFactory, SQLite, MDB
If the fastBackendParameter is omitted or empty, no ephemeral database
is used. If the scheduler parameter is omitted or unspecified, a
synchronous scheduler is used which performs all tasks immediately on
the caller's thread.
@note If the database cannot be opened or created, an exception is
thrown.
@param name A diagnostic label for the database.
@param burstSize Backend burst size in bytes.
@param scheduler The scheduler to use for performing asynchronous tasks.
@param readThreads The number of async read threads to create
@param backendParameters The parameter string for the persistent
backend.
@param fastBackendParameters [optional] The parameter string for the
ephemeral backend.
@return The opened database.
*/
/** Construct and open a fully-wired Database backed by a single backend.
*
* Calls `makeBackend()` to create and open the backend, then wraps it in
* a `DatabaseNodeImp` which adds an async read-thread pool and the full
* `Database` API. The `backendParameters` section must contain a `type`
* key naming a registered backend; most backends also require a `path`
* key. Currently registered built-in types are: `NuDB`, `RocksDB`,
* `memory`, `none`.
*
* @param burstSize Maximum bytes the backend may buffer before flushing.
* @param scheduler Async task dispatcher for read and write jobs.
* @param readThreads Number of threads in the async read pool.
* @param backendParameters Key/value pairs for the persistent backend,
* including at minimum a `type` key.
* @param journal Logging sink for database diagnostics.
* @return A uniquely-owned, open Database ready for I/O.
* @throws std::runtime_error If the backend cannot be created or opened,
* or if the `type` key is missing or unrecognised.
*/
virtual std::unique_ptr<Database>
makeDatabase(
std::size_t burstSize,

View File

@@ -1,72 +1,126 @@
/** @file
* Defines `NodeObject`, the atomic storage unit of the XRPL node store.
*
* Every piece of ledger state — account tree nodes, transaction tree nodes,
* and ledger headers — is stored and retrieved as a `NodeObject`. The class
* is a pure value type: a type tag, a 256-bit hash key, and a raw binary
* blob. Higher layers (SHAMap, ledger, serialization) are responsible for
* interpreting the blob's contents.
*
* `NodeObject` lives in the `xrpl` namespace rather than `xrpl::NodeStore`
* so that the SHAMap layer, ledger subsystem, and serialization paths can
* consume it without pulling in the full nodestore backend API.
*/
#pragma once
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/CountedObject.h>
#include <xrpl/basics/base_uint.h>
// VFALCO NOTE Intentionally not in the NodeStore namespace
namespace xrpl {
/** The types of node objects. */
/** Identifies the kind of data stored in a `NodeObject`.
*
* The integer values are part of the on-disk format (written by
* `EncodedBlob` and read by `DecodedBlob`), so they must not be changed.
* Value 2 is a historical gap left by a removed type and must remain
* unused. `Dummy` (512) is deliberately outside the contiguous valid range
* so it cannot be confused with a legitimate type by accident or by
* off-by-one arithmetic; it is used as a cache sentinel meaning "confirmed
* missing".
*/
enum class NodeObjectType : std::uint32_t {
Unknown = 0,
Ledger = 1,
AccountNode = 3,
TransactionNode = 4,
Dummy = 512 // an invalid or missing object
Unknown = 0, /**< Type not yet determined or not applicable. */
Ledger = 1, /**< Serialized ledger header. */
// Value 2 intentionally absent — historical removal; do not reuse.
AccountNode = 3, /**< SHAMap node from an account-state tree. */
TransactionNode = 4, /**< SHAMap node from a transaction tree. */
Dummy = 512 /**< Sentinel for a confirmed-missing cache entry; not a real object. */
};
/** A simple object that the Ledger uses to store entries.
NodeObjects are comprised of a type, a hash, and a blob.
They can be uniquely identified by the hash, which is a half-SHA512 of
the blob. The blob is a variable length block of serialized data. The
type identifies what the blob contains.
@note No checking is performed to make sure the hash matches the data.
@see SHAMap
*/
/** Immutable storage unit carrying a type tag, a 256-bit hash key, and a
* raw binary payload.
*
* `NodeObject` is the payload type at every level of the nodestore stack:
* `Backend::fetch()` produces instances; `Backend::store()` and
* `Backend::storeBatch()` consume them; `Database` caches shared pointers
* to them. All three data members are `const` — once constructed the
* object never changes, which is correct for content-addressed storage.
*
* Instances must be created exclusively through `createObject()`. Direct
* construction is blocked via the `PrivateAccess` tag idiom (see below).
* All shared references are `std::shared_ptr<NodeObject>`; ownership is
* always shared, never transferred.
*
* Inherits `CountedObject<NodeObject>` to maintain a global atomic
* live-instance count that feeds the `get_counts` diagnostic RPC.
*
* @note The hash is accepted on trust — no verification that it matches
* the payload is performed here. Correctness is enforced at higher
* layers (SHAMap traversal, ledger validation).
* @see SHAMap
*/
class NodeObject : public CountedObject<NodeObject>
{
public:
/** Size in bytes of the hash key used to identify a `NodeObject`. */
static constexpr std::size_t kKEY_BYTES = 32;
private:
// This hack is used to make the constructor effectively private
// except for when we use it in the call to make_shared.
// There's no portable way to make make_shared<> a friend work.
/** Tag type that makes the public constructor effectively private.
*
* `std::make_shared` requires the constructor it calls to be
* accessible, so the constructor cannot be `private`. Instead, it
* takes a `PrivateAccess` argument. Because `PrivateAccess` itself is
* a private nested type, only code inside `NodeObject` (i.e.,
* `createObject`) can construct one — achieving the same effect.
*/
struct PrivateAccess
{
explicit PrivateAccess() = default;
};
public:
// This constructor is private, use createObject instead.
/** Constructs a `NodeObject`; use `createObject()` instead.
*
* The `PrivateAccess` parameter is intentionally inaccessible to
* external callers; it exists solely to satisfy `std::make_shared`.
*/
NodeObject(NodeObjectType type, Blob&& data, uint256 const& hash, PrivateAccess);
/** Create an object from fields.
The caller's variable is modified during this call. The
underlying storage for the Blob is taken over by the NodeObject.
@param type The type of object.
@param ledgerIndex The ledger in which this object appears.
@param data A buffer containing the payload. The caller's variable
is overwritten.
@param hash The 256-bit hash of the payload data.
*/
/** Create a `NodeObject`, transferring ownership of the payload buffer.
*
* The caller's `data` buffer is moved into the new object; after this
* call `data` is in a valid but unspecified state. No copy of the
* payload is made.
*
* @param type The kind of ledger data the payload represents.
* @param data Raw serialized payload; ownership is transferred to the
* returned object.
* @param hash 256-bit hash that uniquely identifies this object in the
* node store. Must be the correct hash of `data` — no verification
* is performed.
* @return A `shared_ptr` to the newly created, immutable `NodeObject`.
*/
static std::shared_ptr<NodeObject>
createObject(NodeObjectType type, Blob&& data, uint256 const& hash);
/** Returns the type of this object. */
/** Returns the type tag indicating what kind of ledger data this object
* holds.
*/
[[nodiscard]] NodeObjectType
getType() const;
/** Returns the hash of the data. */
/** Returns the 256-bit hash that identifies this object in the node
* store.
*
* @note The hash is not verified against the payload at construction
* time; callers must ensure consistency at higher layers.
*/
[[nodiscard]] uint256 const&
getHash() const;
/** Returns the underlying data. */
/** Returns the raw serialized payload stored in this object. */
[[nodiscard]] Blob const&
getData() const;

View File

@@ -6,59 +6,120 @@
namespace xrpl::NodeStore {
enum class FetchType { Synchronous, Async };
/** Distinguishes how a node-object fetch was initiated.
*
* Used by `FetchReport` to let the `Scheduler` route telemetry to the
* correct load-tracking bucket (`jtNS_SYNC_READ` vs `jtNS_ASYNC_READ`
* in production).
*/
enum class FetchType {
Synchronous, /**< Fetch was issued on the caller's thread and awaited inline. */
Async /**< Fetch was queued and completed on a background read thread. */
};
/** Contains information about a fetch operation. */
/** Performance telemetry for a single completed node-object fetch.
*
* Created on the stack immediately before a fetch and passed to
* `Scheduler::onFetch()` once the fetch returns. `fetchType` is fixed at
* construction; `elapsed` and `wasFound` are filled in afterwards.
*
* @see Scheduler::onFetch
*/
struct FetchReport
{
/** Construct a report for a fetch of the given type.
*
* @param fetchType Whether the fetch was synchronous or asynchronous;
* stored as a `const` member and cannot be changed after construction.
*/
explicit FetchReport(FetchType fetchType) : fetchType(fetchType)
{
}
std::chrono::milliseconds elapsed{};
FetchType const fetchType;
bool wasFound = false;
std::chrono::milliseconds elapsed{}; /**< Wall-clock duration of the fetch; zero-initialized. */
FetchType const fetchType; /**< Sync or async; set at construction. */
bool wasFound = false; /**< True if the object was present in the backend. */
};
/** Contains information about a batch write operation. */
/** Performance telemetry for a single completed batch write.
*
* Constructed by `BatchWriter` after each flush and passed to
* `Scheduler::onBatchWrite()`. Both fields must be filled in by the caller
* before the report is forwarded.
*
* @see Scheduler::onBatchWrite
*/
struct BatchWriteReport
{
explicit BatchWriteReport() = default;
std::chrono::milliseconds elapsed;
int writeCount;
std::chrono::milliseconds elapsed; /**< Wall-clock duration of the batch flush. */
int writeCount; /**< Number of `NodeObject`s written in this batch. */
};
/** Scheduling for asynchronous backend activity
For improved performance, a backend has the option of performing writes
in batches. These writes can be scheduled using the provided scheduler
object.
@see BatchWriter
*/
/** Scheduling and telemetry interface for NodeStore backend activity.
*
* Decouples backend write batching and I/O instrumentation from any
* particular threading strategy. A `Scheduler` implementation may run a
* submitted task synchronously on the calling thread (as `DummyScheduler`
* does) or post it to a thread pool (as `NodeStoreScheduler` does via the
* application `JobQueue`). The same backend code is correct under either
* policy.
*
* The interface serves two orthogonal purposes that share one injection
* point: *work dispatch* (`scheduleTask`) and *telemetry ingestion*
* (`onFetch`, `onBatchWrite`). Concrete implementations may ignore the
* telemetry hooks entirely or forward them to a load-balancing subsystem.
*
* @note `scheduleTask` takes `task` by non-const reference rather than by
* value or smart pointer. `BatchWriter` implements `Task` privately and
* manages its own lifetime, so no heap allocation is required for the
* common write-batching case. Callers must ensure the task object
* remains valid until `performScheduledTask()` returns.
*
* @see BatchWriter
* @see DummyScheduler
*/
class Scheduler
{
public:
virtual ~Scheduler() = default;
/** Schedules a task.
Depending on the implementation, the task may be invoked either on
the current thread of execution, or an unspecified
implementation-defined foreign thread.
*/
/** Dispatch a task for execution.
*
* The scheduler may call `task.performScheduledTask()` on the current
* thread before returning, or post the task to an unspecified foreign
* thread. Both behaviours are valid; callers must not assume which will
* occur. The task object must remain valid until `performScheduledTask()`
* returns.
*
* @param task The deferred work to execute; typically a `BatchWriter`
* flush. Passed by reference — ownership is not transferred.
*/
virtual void
scheduleTask(Task& task) = 0;
/** Reports completion of a fetch
Allows the scheduler to monitor the node store's performance
*/
/** Telemetry hook called after each node-object fetch completes.
*
* Allows the scheduler to record I/O latency and hit/miss statistics.
* This is a pure reporting path with no effect on control flow; backends
* call it unconditionally after every fetch, whether or not the object
* was found.
*
* @param report Timing, fetch type, and hit/miss outcome for the
* completed fetch.
*/
virtual void
onFetch(FetchReport const& report) = 0;
/** Reports the completion of a batch write
Allows the scheduler to monitor the node store's performance
*/
/** Telemetry hook called after each batch write completes.
*
* Allows the scheduler to record write throughput. Called by
* `BatchWriter` after each flush, with `report.writeCount` reflecting
* the number of objects flushed in that batch.
*
* @param report Elapsed time and object count for the completed batch.
*/
virtual void
onBatchWrite(BatchWriteReport const& report) = 0;
};

View File

@@ -1,15 +1,53 @@
/** @file
* Defines the `Task` abstract interface for NodeStore scheduled work units.
*
* Any piece of deferred backend work (e.g., a `BatchWriter` flush) inherits
* from `Task` and implements `performScheduledTask()`. The `Scheduler`
* interface accepts a `Task&` and decides *where* and *when* to invoke it,
* decoupling the work unit from any knowledge of threads or job queues.
*/
#pragma once
namespace xrpl::NodeStore {
/** Derived classes perform scheduled tasks. */
/** Pure command-pattern base for NodeStore deferred backend work.
*
* A `Task` is the minimal callable token the scheduling system needs: a single
* `performScheduledTask()` entry point and a virtual destructor. Concrete work
* units inherit from this struct (typically privately, as `BatchWriter` does)
* and are submitted to `Scheduler::scheduleTask()`.
*
* The scheduling contract is intentionally loose: `Scheduler::scheduleTask()`
* may invoke the task synchronously on the calling thread (as `DummyScheduler`
* does for tests) or post it to an unspecified foreign thread (as
* `NodeStoreScheduler` does via the application `JobQueue`). Concrete `Task`
* implementations must be safe under either policy.
*
* The interface is deliberately as small as possible. A richer alternative
* such as `std::function` or `std::unique_ptr<Task>` would impose a heap
* allocation on every scheduled operation and couple the interface to a
* specific ownership model. With this design, `BatchWriter` can implement
* `Task` privately and pass `*this` to `scheduleTask()` — no extra allocation
* needed, and lifetime management stays entirely within `BatchWriter`.
*
* @see Scheduler
* @see BatchWriter
* @see DummyScheduler
*/
struct Task
{
virtual ~Task() = default;
/** Performs the task.
The call may take place on a foreign thread.
*/
/** Execute the deferred work represented by this task.
*
* Called by the `Scheduler` either synchronously on the submitting thread
* or asynchronously on a foreign thread, depending on the scheduler
* implementation. Implementations must tolerate either calling context.
*
* The object must remain valid and unmodified from the time it is passed
* to `Scheduler::scheduleTask()` until this method returns.
*/
virtual void
performScheduledTask() = 0;
};

View File

@@ -1,3 +1,13 @@
/** @file
* Shared vocabulary types for the xrpl::NodeStore subsystem.
*
* This header sits at the base of the NodeStore include hierarchy and is
* pulled in by every other NodeStore interface header. It defines only the
* primitives that all participants — backends, the async database layer, and
* callers — must agree on: the operation status codes, the batch container
* alias, and the batch-size policy constants.
*/
#pragma once
#include <xrpl/nodestore/NodeObject.h>
@@ -6,29 +16,57 @@
namespace xrpl::NodeStore {
// This is only used to pre-allocate the array for
// batch objects and does not affect the amount written.
//
/** Initial capacity hint for a `Batch` vector and the backpressure threshold
* in `BatchWriter::store`.
*
* `BatchWriter` reserves this many slots on construction and re-reserves after
* each flush to avoid repeated allocations. `BatchWriter::store` also blocks
* when `writeSet_` reaches this size, providing backpressure against producers
* that outrun the flush thread. This value does not cap how many objects can
* ultimately be written in a single pass.
*/
static constexpr auto kBATCH_WRITE_PREALLOCATION_SIZE = 256;
// This sets a limit on the maximum number of writes
// in a batch. Actual usage can be twice this since
// we have a new batch growing as we write the old.
//
/** Maximum number of objects flushed in a single batch write.
*
* Once a batch accumulates this many objects it is handed off to the backend.
* Because a new batch begins accumulating while the previous one is being
* written to disk (double-buffer pattern), peak in-flight memory for pending
* objects can reach approximately twice this limit.
*/
static constexpr auto kBATCH_WRITE_LIMIT_SIZE = 65536;
/** Return codes from Backend operations. */
/** Return codes from `Backend` fetch and store operations.
*
* Values 099 are reserved for the standard codes defined here. Backend
* implementations that need additional error distinctions must use values
* starting at `CustomCode` (100) to avoid collisions.
*/
enum class Status {
Ok = 0,
NotFound = 1,
DataCorrupt = 2,
Unknown = 3,
BackendError = 4,
Ok = 0, /**< Operation completed successfully. */
NotFound = 1, /**< Key is not present in the store. */
DataCorrupt = 2, /**< Stored blob failed integrity validation. */
Unknown = 3, /**< An unclassified error occurred. */
BackendError = 4, /**< The underlying storage backend reported an error. */
/** First value available for backend-defined extended error codes.
* Backend implementations may define their own codes as
* `static_cast<int>(Status::CustomCode) + N` without colliding with the
* standard range (099).
*/
CustomCode = 100
};
/** A batch of NodeObjects to write at once. */
/** A collection of `NodeObject`s to be written together in a single batch.
*
* Using a named alias rather than spelling out the type at every call site
* means that a change to the container type or ownership model propagates
* from this single definition. The `shared_ptr` element type reflects that
* individual `NodeObject` instances may be concurrently referenced by
* in-memory caches and the write pipeline at the same time.
*
* @see Backend::storeBatch
*/
using Batch = std::vector<std::shared_ptr<NodeObject>>;
} // namespace xrpl::NodeStore

View File

@@ -9,18 +9,46 @@
namespace xrpl::NodeStore {
/** Batch-writing assist logic.
The batch writes are performed with a scheduled task. Use of the
class it not required. A backend can implement its own write batching,
or skip write batching if doing so yields a performance benefit.
@see Scheduler
*/
/** Coalesces individual NodeObject writes into batches for NodeStore backends.
*
* Individual key-value store writes carry per-operation overhead (system
* call, WAL append, compaction pressure). `BatchWriter` amortises that cost
* by accumulating objects in an internal buffer and flushing them as a single
* batch via a `Scheduler`-dispatched task. Use of this class is optional —
* a backend may implement its own batching strategy or skip batching entirely.
*
* The class privately inherits `Task`, turning itself into a schedulable unit
* of work with no additional heap allocation. The actual write is delegated
* to a `Callback` (typically the owning backend), keeping storage-engine
* specifics out of the batching logic.
*
* **Thread safety**: `store()` and `getWriteLoad()` are safe to call
* concurrently from multiple threads. The flush task may run on the calling
* thread (synchronous scheduler) or a background thread (async scheduler);
* the recursive mutex design is safe under both policies.
*
* **Backpressure**: `store()` blocks when the pending buffer reaches
* `kBATCH_WRITE_LIMIT_SIZE` (65,536 objects), preventing unbounded memory
* growth when disk I/O cannot keep pace with producers. Peak in-flight
* memory can reach approximately twice this limit due to the double-buffer
* swap pattern (one batch being written while the next accumulates).
*
* @see Scheduler
* @see Backend
*/
class BatchWriter : private Task
{
public:
/** This callback does the actual writing. */
/** Pure interface through which `BatchWriter` delivers a completed batch.
*
* The concrete backend (e.g., `RocksDBBackend`) inherits both `Backend`
* and `BatchWriter::Callback`, implementing `writeBatch` to forward the
* batch to the underlying storage engine. This indirection keeps batching
* logic storage-agnostic.
*
* `writeBatch` is invoked outside the internal mutex, so implementations
* may perform blocking I/O without serialising concurrent `store()` calls.
*/
struct Callback
{
virtual ~Callback() = default;
@@ -29,49 +57,111 @@ public:
Callback&
operator=(Callback const&) = delete;
/** Flush a completed batch to the storage engine.
*
* Called by `BatchWriter` once per scheduled flush, with the lock
* already released. The implementation must persist every object in
* `batch` before returning.
*
* @param batch The collection of `NodeObject`s to write. Objects in
* the batch may be concurrently referenced by in-memory caches.
*/
virtual void
writeBatch(Batch const& batch) = 0;
};
/** Create a batch writer. */
/** Construct a `BatchWriter` tied to the given sink and scheduler.
*
* Pre-allocates the internal write buffer to avoid repeated small
* reallocations during normal operation.
*
* @param callback The sink that receives each flushed `Batch` via
* `Callback::writeBatch()`. Typically the owning backend. Must
* outlive this `BatchWriter`.
* @param scheduler The scheduler used to dispatch the flush task. May be
* a synchronous `DummyScheduler` (tests and bulk import) or the
* production async scheduler; both are supported.
*/
BatchWriter(Callback& callback, Scheduler& scheduler);
/** Destroy a batch writer.
Anything pending in the batch is written out before this returns.
*/
/** Destroy the `BatchWriter`, draining any pending writes first.
*
* Blocks until all accumulated objects have been flushed to the
* `Callback`. No objects passed to `store()` are silently abandoned.
*/
~BatchWriter() override;
/** Store the object.
This will add to the batch and initiate a scheduled task to
write the batch out.
*/
/** Enqueue a `NodeObject` for the next scheduled batch flush.
*
* Appends `object` to the internal accumulation buffer and, if no flush
* task is already outstanding, schedules one via the `Scheduler`.
* Subsequent `store()` calls before the flush fires piggyback on the
* single in-flight task.
*
* @param object The `NodeObject` to persist.
* @note Blocks the caller when the buffer reaches `kBATCH_WRITE_LIMIT_SIZE`
* (65,536 objects) until the in-flight batch is fully written. This
* backpressure prevents unbounded memory growth when disk I/O falls
* behind producers.
*/
void
store(std::shared_ptr<NodeObject> const& object);
/** Get an estimate of the amount of writing I/O pending. */
/** Return a conservative estimate of pending write I/O.
*
* Returns the larger of the item count currently being written to the
* backend and the item count waiting for the next scheduled flush.
* Taking the maximum reflects pressure in both the in-flight and
* accumulating phases, giving callers a meaningful load signal for
* scheduling decisions.
*
* @return Estimated number of `NodeObject`s awaiting or undergoing write.
*/
int
getWriteLoad();
private:
/** `Task` entry-point; delegates to the internal `writeBatch()`. */
void
performScheduledTask() override;
/** Drain accumulated objects to the backend using the double-buffer swap.
*
* Holds the lock only long enough to swap the internal buffer with a
* local vector (O(1)), then releases the lock before calling
* `Callback::writeBatch()`. Loops until no objects remain after a swap,
* then clears `writePending_` and notifies any blocked `store()` callers.
*/
void
writeBatch();
/** Block until any in-flight flush has completed.
*
* Waits on the condition variable until `writePending_` is false.
* Called by the destructor to guarantee no pending objects are abandoned
* on teardown.
*/
void
waitForWriting();
private:
/** Recursive to allow synchronous schedulers that invoke `writeBatch()`
* on the same thread as `store()` or `waitForWriting()`. */
using LockType = std::recursive_mutex;
/** Required by `LockType`; `std::condition_variable` only works with
* `std::mutex`. */
using CondvarType = std::condition_variable_any;
Callback& callback_;
Scheduler& scheduler_;
LockType writeMutex_;
CondvarType writeCondition_;
/** Item count of the batch currently being written; used by `getWriteLoad()`. */
int writeLoad_{0};
/** True when a flush task has been scheduled but not yet completed. */
bool writePending_{false};
/** Accumulation buffer; swapped out atomically inside `writeBatch()`. */
Batch writeSet_;
};

View File

@@ -1,3 +1,13 @@
/** @file
* Single-backend concrete implementation of the NodeStore `Database` interface.
*
* `DatabaseNodeImp` is the standard node-store path for deployments that keep
* all ledger objects in one persistent key/value backend (NuDB, RocksDB, etc.).
* It adapts the thin `Backend` interface onto the richer `Database` contract
* — async read pool, telemetry, and scheduler callbacks — all of which live in
* the base class and cannot be bypassed.
*/
#pragma once
#include <xrpl/basics/TaggedCache.h>
@@ -6,6 +16,25 @@
namespace xrpl::NodeStore {
/** Single-backend implementation of the NodeStore `Database` interface.
*
* Wraps exactly one `Backend` (NuDB, RocksDB, Memory, Null) and serves all
* ledger objects regardless of their ledger sequence number. This is the
* standard deployment path; the two-backend rotation variant is
* `DatabaseRotatingImp`.
*
* Every public method is a thin delegation: to `backend_` for storage
* operations and to base-class helpers for async dispatch, telemetry, and
* bulk import. No business logic lives here.
*
* **Shutdown ordering**: The destructor calls `stop()` to drain all pending
* async reads and wait for worker threads to exit before releasing `backend_`.
* Worker threads invoke the virtual `fetchNodeObject()` hook; if `backend_`
* were released while a thread was active, it would dereference a dangling
* pointer.
*
* @see Database, DatabaseRotatingImp, Backend
*/
class DatabaseNodeImp : public Database
{
public:
@@ -14,6 +43,21 @@ public:
DatabaseNodeImp&
operator=(DatabaseNodeImp const&) = delete;
/** Construct the database and start the async read thread pool.
*
* Asserts that @p backend is non-null, then delegates to the `Database`
* base constructor which spawns `readThreads` detached worker threads.
*
* @param scheduler Task scheduler for async I/O dispatch and telemetry;
* must outlive this object.
* @param readThreads Number of async prefetch threads; clamped to at least 1
* by the base constructor.
* @param backend Open, non-null backend to use for all storage; shared
* ownership is assumed.
* @param config `[node_db]` config section; forwarded to `Database`
* for `earliest_seq` and `rq_bundle` parsing.
* @param j Logging sink.
*/
DatabaseNodeImp(
Scheduler& scheduler,
int readThreads,
@@ -28,48 +72,126 @@ public:
"backend");
}
/** Drain pending I/O and release the backend.
*
* Calls `stop()` to wait for all async read worker threads to exit before
* `backend_` is destroyed. This must happen in the derived destructor
* because worker threads call the virtual `fetchNodeObject()` hook, which
* dereferences `backend_`.
*/
~DatabaseNodeImp() override
{
stop();
}
/** Return the name of the underlying backend for diagnostics.
*
* @return The backend's human-readable identifier (e.g. the on-disk path).
*/
std::string
getName() const override
{
return backend_->getName();
}
/** Return the estimated number of pending write operations in the backend.
*
* Approximate; the value may change immediately after it is read.
*
* @return Pending write count, or 0 if the backend does not batch writes.
*/
std::int32_t
getWriteLoad() const override
{
return backend_->getWriteLoad();
}
/** Bulk-import all objects from @p source into this database's backend.
*
* Delegates to `importInternal()`, which iterates @p source via `forEach()`
* and stores objects in batches. Large source databases may take significant
* time; no concurrent writes to @p source should occur during the call.
*
* @param source The database to read from; must remain open and quiescent.
*/
void
importDatabase(Database& source) override
{
importInternal(*backend_.get(), source);
}
/** Persist a node object to the backend.
*
* Updates store telemetry, wraps the payload in a `NodeObject`, and
* forwards to the backend. The ledger sequence parameter is part of the
* `Database` contract but is ignored here — a single backend holds objects
* from all ledger sequences.
*
* @param type Type tag for the object (ledger, account node, etc.).
* @param data Serialized payload; ownership is transferred — the caller's
* variable is left in a valid but unspecified state.
* @param hash 256-bit content-address key. The caller is responsible for
* correctness; the hash is not re-verified.
*/
void
store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t) override;
/** Report whether two ledger sequence numbers map to the same backend.
*
* Always returns `true` for `DatabaseNodeImp` because there is exactly one
* backend: every sequence number resolves to the same physical store. This
* allows the async read pool to coalesce duplicate hash requests that carry
* different sequence numbers without issuing a second backend read.
*
* @return `true` unconditionally.
*/
bool
isSameDB(std::uint32_t, std::uint32_t) override
{
// only one database
return true;
}
/** Flush any buffered writes to durable storage.
*
* Delegates directly to `backend_->sync()`. Not latency-sensitive;
* typically called on ledger close or maintenance paths.
*/
void
sync() override
{
backend_->sync();
}
/** Synchronously fetch a batch of node objects by hash.
*
* Calls `backend_->fetchBatch()` directly, bypassing the async read queue.
* Enforces a positional contract: the returned vector is always the same
* length as @p hashes, with null entries for objects not found. Missing
* objects are logged at `error` level. Wall-clock elapsed time is reported
* via `updateFetchMetrics()`; per-slot hit counts are not tracked here and
* remain the caller's responsibility.
*
* @note The batch-level `Status` from the backend is discarded; object
* availability is inferred entirely from null vs. non-null slots.
* @param hashes Ordered list of 256-bit keys to retrieve.
* @return Vector of the same length as @p hashes; null entries indicate
* objects absent from the backend.
*/
std::vector<std::shared_ptr<NodeObject>>
fetchBatch(std::vector<uint256> const& hashes);
/** Schedule a non-blocking background fetch for a single node object.
*
* Forwards unconditionally to `Database::asyncFetch()`, which coalesces
* duplicate hash requests and dispatches callbacks from the worker thread
* pool. No per-backend routing is needed for the single-backend case.
*
* @param hash 256-bit key of the object to retrieve.
* @param ledgerSeq Ledger sequence the object belongs to; forwarded for
* hash-coalescing decisions via `isSameDB()`.
* @param callback Invoked on a worker thread with the fetched `NodeObject`,
* or `nullptr` on miss or error.
*/
void
asyncFetch(
uint256 const& hash,
@@ -77,13 +199,37 @@ public:
std::function<void(std::shared_ptr<NodeObject> const&)>&& callback) override;
private:
// Persistent key/value storage
/** The single persistent key/value backend that holds all ledger objects. */
std::shared_ptr<Backend> backend_;
/** Template Method hook called by the base-class public `fetchNodeObject()`.
*
* Delegates to `backend_->fetch()` with structured error logging:
* `Status::Ok` and `Status::NotFound` are silent; `Status::DataCorrupt`
* logs at `fatal`; any other code logs at `warn`. Exceptions from the
* backend are logged at `fatal` then re-raised via `Rethrow()`. Sets
* `fetchReport.wasFound = true` on a hit to feed the base-class metric.
* The ledger sequence parameter is accepted by the signature but unused.
*
* @param hash 256-bit key to look up.
* @param fetchReport Mutable report; `wasFound` is set on a hit.
* @param duplicate Whether this fetch was deduplicated from another
* in-flight request for the same hash; unused in this implementation.
* @return The fetched `NodeObject`, or `nullptr` on miss or error.
* @throws Any exception propagated from `backend_->fetch()` after logging.
*/
std::shared_ptr<NodeObject>
fetchNodeObject(uint256 const& hash, std::uint32_t, FetchReport& fetchReport, bool duplicate)
override;
/** Iterate every object in the backend and invoke @p f for each one.
*
* Used exclusively by `importInternal()` for bulk export. Delegates
* directly to `backend_->forEach()`. Not safe for concurrent access with
* reads or writes; see `Backend::forEach()` for details.
*
* @param f Callback invoked with each `NodeObject`; must not be null.
*/
void
forEach(std::function<void(std::shared_ptr<NodeObject>)> f) override
{

View File

@@ -6,6 +6,24 @@
namespace xrpl::NodeStore {
/** Concrete two-backend node store that enables online deletion of old ledger data.
*
* Maintains a _writable_ backend (receives all new stores) and an _archive_
* backend (holds older data). The `SHAMapStore` sweep thread drives rotations:
* when the configured deletion horizon is reached it calls `rotate()`, which
* atomically promotes the current writable to archive, installs a fresh backend
* as the new writable, and schedules the old archive for deletion.
*
* All public methods follow a capture-under-lock / use-outside-lock pattern:
* the mutex protects only the `shared_ptr` swap, not the backend I/O. This
* keeps unrelated readers and writers concurrent during disk operations.
*
* **Thread safety**: all public methods are safe to call from any thread
* concurrently. `stop()` must be called in the derived destructor before
* the base `Database` destructor tears down the async read pool.
*
* @see DatabaseRotating, Database, SHAMapStoreImp
*/
class DatabaseRotatingImp : public DatabaseRotating
{
public:
@@ -14,6 +32,20 @@ public:
DatabaseRotatingImp&
operator=(DatabaseRotatingImp const&) = delete;
/** Construct the rotating database and initialise the async read pool.
*
* Both backends must already be open. Their `fdRequired()` values are
* accumulated into `fdRequired_` so the application can pre-validate the
* process file-descriptor limit before any I/O begins.
*
* @param scheduler Task scheduler for async dispatch and telemetry;
* must outlive this object.
* @param readThreads Number of async read worker threads to spawn.
* @param writableBackend The backend that receives all new stores.
* @param archiveBackend The backend holding older (pre-rotation) data.
* @param config `[node_db]` config section forwarded to `Database`.
* @param j Logging sink.
*/
DatabaseRotatingImp(
Scheduler& scheduler,
int readThreads,
@@ -22,48 +54,166 @@ public:
Section const& config,
beast::Journal j);
/** Destroy the rotating database.
*
* Calls `stop()` before the base destructor so that async worker threads
* stop invoking the virtual `fetchNodeObject()` while derived data members
* are still valid.
*/
~DatabaseRotatingImp() override
{
stop();
}
/** Atomically swap in a new writable backend, demoting the current one.
*
* The rotation sequence under the mutex is:
* 1. Mark the existing archive backend for on-disk deletion, move it into
* a local to extend its lifetime past the callback.
* 2. Promote the current writable backend to become the new archive.
* 3. Install @p newBackend as the writable backend.
*
* The lock is released before @p f is called. This ordering is critical:
* the callback (in production, `SHAMapStoreImp`) persists the new backend
* names to a SQLite state database. The old archive `shared_ptr` remains
* alive on the stack until after @p f returns, so the archive directory is
* deleted only after the persistent state has been updated — making the
* rotation crash-safe.
*
* @param newBackend Freshly prepared backend to install as the new writable.
* Ownership is transferred; the caller's pointer is null on return.
* @param f Callback invoked after the swap, outside the mutex.
* Receives the new writable name and the new archive name (the former
* writable). Must persist these names to durable storage before
* returning so the node can recover the correct layout after a crash.
* @note The callback is invoked outside the mutex, so other methods
* (including `getName()` and even `rotate()`) may be called from within
* @p f without deadlocking. Re-entering `rotate()` from @p f is
* technically safe but should never occur in production code.
*/
void
rotate(
std::unique_ptr<NodeStore::Backend>&& newBackend,
std::function<void(std::string const& writableName, std::string const& archiveName)> const&
f) override;
/** Return the name of the current writable backend.
*
* Acquires the mutex to take a consistent snapshot of `writableBackend_`.
*
* @return A human-readable identifier for the writable backend.
*/
std::string
getName() const override;
/** Return the estimated pending write count from the writable backend.
*
* Acquires the mutex to snapshot `writableBackend_`, then queries it
* outside the lock.
*
* @return Pending write count; 0 if the backend does not batch writes.
*/
std::int32_t
getWriteLoad() const override;
/** Bulk-import all objects from @p source into the current writable backend.
*
* Snapshots `writableBackend_` under the mutex, then delegates to
* `importInternal()`. A rotation that occurs concurrently will not affect
* the import — it continues writing to the backend that was writable when
* it started.
*
* @param source Source database to read from; must remain valid and
* quiescent (no concurrent writes) for the duration of the call.
*/
void
importDatabase(Database& source) override;
/** Return `true`, since both backends form a single logical namespace.
*
* The async read pool calls this to decide whether two in-flight fetches
* for the same hash (with different ledger sequence numbers) can share a
* single backend read. Because the rotating store presents one logical
* keyspace across both tiers, this always returns `true`.
*
* @return Always `true`.
*/
bool
isSameDB(std::uint32_t, std::uint32_t) override
{
// rotating store acts as one logical database
return true;
}
/** Store a node object in the current writable backend.
*
* Snapshots `writableBackend_` under the mutex, constructs a `NodeObject`
* from the supplied data, then writes it outside the lock. The ledger
* sequence parameter is accepted for interface compatibility but ignored —
* all writes always go to the current writable backend regardless of age.
*
* @param type Semantic type of the object.
* @param data Serialized payload; moved into the backend.
* @param hash 256-bit content hash; not re-verified.
* @param ledgerSeq Ignored; present for `Database` interface compatibility.
*/
void
store(NodeObjectType type, Blob&& data, uint256 const& hash, std::uint32_t) override;
/** Flush the writable backend to durable storage.
*
* Holds the mutex for the entire sync call. Acceptable because this is a
* maintenance path, not a latency-sensitive read/write path.
*/
void
sync() override;
private:
/** Active backend; receives all new `store()` calls. */
std::shared_ptr<Backend> writableBackend_;
/** Read-only backend holding data from before the last rotation. */
std::shared_ptr<Backend> archiveBackend_;
/** Guards swaps of `writableBackend_` and `archiveBackend_`.
* Held only for pointer capture or swap — never across I/O.
*/
mutable std::mutex mutex_;
/** Two-tier fetch with optional archive-to-writable promotion.
*
* Snapshots both backend pointers under the mutex, then tries the writable
* backend first. On a miss, tries the archive backend. If the object is
* found in the archive and @p duplicate is `true`, the writable pointer is
* refreshed under the mutex (to handle a concurrent rotation) and the
* object is written back into the current writable tier.
*
* Backend errors are handled conservatively: `DataCorrupt` is logged at
* fatal severity and returns `nullptr` (cache miss); unknown status codes
* are logged at warning level; exceptions are logged and rethrown via
* `Rethrow()`.
*
* @param hash 256-bit content hash of the desired object.
* @param ledgerSeq Ignored; accepted for `Database` virtual interface.
* @param fetchReport Out-param; `wasFound` is set to `true` on a hit.
* @param duplicate When `true`, a hit in the archive is promoted to
* the writable backend.
* @return The found `NodeObject`, or `nullptr` on miss or error.
*/
std::shared_ptr<NodeObject>
fetchNodeObject(uint256 const& hash, std::uint32_t, FetchReport& fetchReport, bool duplicate)
override;
/** Visit every object in both backends sequentially.
*
* Snapshots both backend pointers under the mutex, then calls
* `writable->forEach(f)` followed by `archive->forEach(f)` outside the
* lock. Used by `importInternal()` during bulk import.
*
* @param f Callable invoked with each `NodeObject`; must not call any
* method that acquires `mutex_` to avoid deadlock.
* @note Not safe to call concurrently with `rotate()` or other writes if
* the backend's `for_each` re-opens the database (e.g. NuDB).
*/
void
forEach(std::function<void(std::shared_ptr<NodeObject>)> f) override;
};

View File

@@ -4,30 +4,84 @@
namespace xrpl::NodeStore {
/** Parsed key/value blob into NodeObject components.
This will extract the information required to construct a NodeObject. It
also does consistency checking and returns the result, so it is possible
to determine if the data is corrupted without throwing an exception. Not
all forms of corruption are detected so further analysis will be needed
to eliminate false negatives.
@note This defines the database format of a NodeObject!
*/
/** Deserializes a raw backend key/value buffer into the components of a
* `NodeObject`.
*
* This is the read-direction half of the NodeStore on-disk format, paired
* with `EncodedBlob`. Together they define the canonical binary schema for
* persisted node objects; any format change must be reflected in both classes.
*
* On-disk layout (canonical reference):
* - Bytes 07: Unused prefix. Historically stored a ledger index; written
* as eight zero bytes today and silently ignored on read.
* - Byte 8: `NodeObjectType` discriminant (one-byte enum value).
* - Bytes 9+: Raw serialized object payload.
*
* Validation is intentionally minimal and non-throwing: the constructor sets
* an internal success flag rather than raising an exception, allowing callers
* to handle corruption gracefully (see `wasOk()`). Not all corruption is
* detected — this is a fast sanity check, not a cryptographic integrity proof.
*
* `DecodedBlob` holds non-owning pointers into the caller-supplied buffers;
* the backing storage must remain valid until `createObject()` is called or
* the `DecodedBlob` is destroyed.
*
* @note This class defines the database format of a `NodeObject`.
* @see EncodedBlob for the write-direction counterpart.
*/
class DecodedBlob
{
public:
/** Construct the decoded blob from raw data. */
/** Parse a raw backend buffer into its constituent NodeObject fields.
*
* Validates the on-disk layout without performing any heap allocation.
* `key_` and `objectData_` are set to non-owning pointers into the
* caller-supplied buffers; the actual payload copy is deferred to
* `createObject()`. The caller must keep both buffers alive for the
* lifetime of this object.
*
* Parsing succeeds (`wasOk()` returns `true`) only when `valueBytes > 9`
* and the type byte at offset 8 is one of the four recognised values:
* `hotUNKNOWN`, `hotLEDGER`, `hotACCOUNT_NODE`, or `hotTRANSACTION_NODE`.
* `hotDUMMY` (value 512) and any unrecognised byte leave the object in a
* failed state without throwing.
*
* @param key Pointer to the 32-byte hash that was used as the
* storage key; not validated or dereferenced here.
* @param value Pointer to the raw value buffer retrieved from the
* backend.
* @param valueBytes Total byte length of `value`. Values of 9 or fewer
* bytes produce a failed parse.
*/
DecodedBlob(void const* key, void const* value, int valueBytes);
/** Determine if the decoding was successful. */
/** Returns `true` if the constructor successfully parsed a well-formed
* buffer with a recognised `NodeObjectType`.
*
* Must be checked before calling `createObject()`. Calling `createObject()`
* on a failed `DecodedBlob` fires `XRPL_ASSERT` in debug builds.
*/
[[nodiscard]] bool
wasOk() const noexcept
{
return success_;
}
/** Create a NodeObject from this data. */
/** Allocate and return a `NodeObject` from the previously parsed fields.
*
* Copies the payload slice into an owning `Blob` and reconstructs the
* full hash key from the stored pointer. This is the only heap allocation
* in the decode path. The returned `NodeObject` owns its data
* independently, so the caller may release the backend fetch buffer
* immediately after this call returns.
*
* @pre `wasOk()` must return `true`. Calling this on a failed parse fires
* `XRPL_ASSERT` in debug builds; in release builds a null
* `shared_ptr` is returned as a defensive fallback.
* @return A fully constructed `NodeObject`, or `nullptr` if the parse had
* failed (release-build defensive path — callers must always check
* `wasOk()` first).
*/
std::shared_ptr<NodeObject>
createObject();

View File

@@ -11,51 +11,80 @@
namespace xrpl::NodeStore {
/** Convert a NodeObject from in-memory to database format.
The (suboptimal) database format consists of:
- 8 prefix bytes which will typically be 0, but don't assume that's the
case; earlier versions of the code would use these bytes to store the
ledger index either once or twice.
- A single byte denoting the type of the object.
- The payload.
@note This class is typically instantiated on the stack, so the size of
the object does not matter as much as it normally would since the
allocation is, effectively, free.
We leverage that fact to preallocate enough memory to handle most
payloads as part of this object, eliminating the need for dynamic
allocation. As of this writing ~94% of objects require fewer than
1024 payload bytes.
/** Serializes a `NodeObject` to the binary wire format expected by storage
* backends (NuDB, RocksDB).
*
* This is the write-direction half of the NodeStore on-disk format, paired
* with `DecodedBlob`. Together they define the canonical binary schema for
* persisted node objects; any format change must be reflected in both classes.
*
* On-disk layout (canonical reference):
* - Bytes 07: Eight zero bytes. Historically stored the ledger index;
* zeroed since that field was removed. Readers must not assume all
* zeros — older databases may contain non-zero values here.
* - Byte 8: `NodeObjectType` cast to a single `uint8_t`.
* - Bytes 9+: Raw serialized payload from `NodeObject::getData()`.
*
* The 32-byte `uint256` hash is the storage key and is kept separate from
* the value payload. `getKey()` and `getData()` expose these two pieces as
* `void const*` pointers suitable for direct hand-off to NuDB or RocksDB
* slice APIs.
*
* Instances are intended to be constructed immediately before a backend
* insert call and destroyed immediately after, keeping any heap-allocated
* overflow buffer alive for exactly as long as needed.
*
* @note This class is non-copyable. `ptr_` is a `const` raw pointer whose
* ownership is conditional: it points into the inline `payload_` buffer
* when the serialized size fits within 1033 bytes (~94% of real objects),
* and into a heap buffer otherwise. Copying would require duplicating
* that conditional ownership, so no copy or move constructor is provided.
*
* @see DecodedBlob for the read-direction counterpart.
*/
class EncodedBlob
{
/** The 32-byte key of the serialized object. */
/** Storage key: the object's 32-byte `uint256` hash. */
std::array<std::uint8_t, 32> key_{};
/** A pre-allocated buffer for the serialized object.
The buffer is large enough for the 9 byte prefix and at least
1024 more bytes. The precise size is calculated automatically
at compile time so as to avoid wasting space on padding bytes.
/** Inline stack buffer covering the 9-byte header plus up to 1024 bytes
* of payload.
*
* Sized at compile time via `boost::alignment::align_up` to the next
* `uint32_t`-aligned boundary, eliminating any trailing padding that a
* naive `9 + 1024` array would incur. When `size_` does not exceed this
* array's capacity, `ptr_` aliases `payload_.data()` and no heap
* allocation occurs.
*/
std::array<std::uint8_t, boost::alignment::align_up(9 + 1024, alignof(std::uint32_t))>
payload_{};
/** The size of the serialized data. */
/** Total byte length of the serialized value (header + payload). */
std::uint32_t size_;
/** A pointer to the serialized data.
This may point to the pre-allocated buffer (if it is sufficiently
large) or to a dynamically allocated buffer.
/** Pointer to the serialized value buffer.
*
* Set once at construction and never changed (`const`). Points into
* `payload_` when `size_ <= payload_.size()`, or into a heap allocation
* otherwise. The destructor uses `ptr_ != payload_.data()` to decide
* whether to `delete[]`.
*/
std::uint8_t* const ptr_;
public:
/** Serialize `obj` into the on-disk wire format.
*
* Fills `key_` with the object's hash, writes the 9-byte header into
* `ptr_`, then copies the payload. If the total serialized size exceeds
* the inline `payload_` buffer capacity the constructor heap-allocates
* an exact-fit buffer; otherwise the inline buffer is used directly.
*
* @param obj The node object to serialize. Must be non-null: a null
* `shared_ptr` fires `XRPL_ASSERT` in debug builds and throws
* `std::runtime_error` in all builds.
* @throws std::runtime_error if `obj` is null.
*/
explicit EncodedBlob(std::shared_ptr<NodeObject> const& obj)
: size_([&obj]() {
XRPL_ASSERT(obj, "xrpl::NodeStore::EncodedBlob::EncodedBlob : non-null input");
@@ -73,6 +102,14 @@ public:
std::copy_n(obj->getHash().data(), obj->getHash().size(), key_.data());
}
/** Releases any heap-allocated overflow buffer.
*
* If `ptr_` points outside `payload_` (i.e., a heap buffer was
* allocated because the serialized size exceeded 1033 bytes), the buffer
* is freed with `delete[]`. An `XRPL_ASSERT` verifies that the pointer
* and size fields are mutually consistent before the free, catching any
* state drift that would otherwise cause a double-free or memory leak.
*/
~EncodedBlob()
{
XRPL_ASSERT(
@@ -85,18 +122,41 @@ public:
delete[] ptr_;
}
/** Returns a pointer to the 32-byte storage key (the object's hash).
*
* The pointer is valid for the lifetime of this `EncodedBlob` and may
* be passed directly to NuDB or RocksDB key-slice APIs.
*
* @return `void const*` pointing to the 32-byte key buffer.
*/
[[nodiscard]] void const*
getKey() const noexcept
{
return static_cast<void const*>(key_.data());
}
/** Returns the total byte length of the serialized value buffer.
*
* This is `obj->getData().size() + 9`: nine header bytes (eight
* zero-prefix bytes plus the type byte) followed by the raw payload.
*
* @return Byte count of the buffer returned by `getData()`.
*/
[[nodiscard]] std::size_t
getSize() const noexcept
{
return size_;
}
/** Returns a pointer to the serialized value buffer.
*
* The buffer layout is: eight zero bytes, one `NodeObjectType` byte,
* then the raw object payload. The pointer is valid for the lifetime of
* this `EncodedBlob` and may be passed directly to NuDB compression
* helpers or RocksDB value-slice APIs.
*
* @return `void const*` pointing to `getSize()` bytes of serialized data.
*/
[[nodiscard]] void const*
getData() const noexcept
{

View File

@@ -1,9 +1,37 @@
/** @file
* Declares `ManagerImp`, the concrete Meyers-singleton implementation of the
* NodeStore `Manager` interface, hidden in `detail/` as an implementation
* detail not intended for direct use outside the nodestore subsystem.
*/
#pragma once
#include <xrpl/nodestore/Manager.h>
namespace xrpl::NodeStore {
/** Concrete singleton implementation of the NodeStore backend registry.
*
* `ManagerImp` maintains a runtime registry of `Factory` objects and
* orchestrates `Backend` and `Database` construction from configuration data.
* The four built-in backends (NuDB, RocksDB, Memory, Null) are registered
* during construction by calling their respective `register*Factory` free
* functions, each of which holds a function-local static `Factory` that
* self-registers via `insert()`. This avoids relying on global-variable
* destruction order across translation units, which would be undefined
* behaviour if a `Factory` destructor called `erase()` after `ManagerImp`
* had already been destroyed.
*
* The registry is a `std::vector<Factory*>` of non-owning pointers protected
* by a `std::mutex`. Ownership of each `Factory` remains with the static
* storage managed by the `register*Factory` functions, which are guaranteed
* to outlive this singleton.
*
* @note Callers outside the nodestore subsystem should use `Manager::instance()`
* rather than `ManagerImp::instance()` to avoid depending on this
* implementation-detail type.
*
* @see Manager, Factory, DatabaseNodeImp
*/
class ManagerImp : public Manager
{
private:
@@ -11,25 +39,55 @@ private:
std::vector<Factory*> list_;
public:
/** Return the process-wide ManagerImp singleton.
*
* Uses a Meyers function-local static for thread-safe, once-only
* construction under C++11 and later. All four built-in backend factories
* are registered before the reference is returned for the first time.
*
* @return Reference to the single ManagerImp instance.
*/
static ManagerImp&
instance();
/** Throw a user-facing error when the backend configuration is absent or
* names an unrecognised type.
*
* Both the missing-`type`-key and the unrecognised-type code paths in
* `makeBackend()` converge on this helper so the operator-facing message
* is consistent.
*
* @throws std::runtime_error Always — message directs the operator to add
* or correct the `[node_db]` section in `xrpld.cfg`.
*/
static void
missingBackend();
/** Register all built-in backend factories.
*
* Calls `registerNuDBFactory`, `registerRocksDBFactory`,
* `registerNullFactory`, and `registerMemoryFactory`. Each function
* creates a function-local static `Factory` that calls `insert()` on this
* manager. The function-local-static lifetime guarantee ensures all
* factories are destroyed before this `ManagerImp`.
*/
ManagerImp();
~ManagerImp() override = default;
/** @copydoc Manager::find */
Factory*
find(std::string const& name) override;
/** @copydoc Manager::insert */
void
insert(Factory& factory) override;
/** @copydoc Manager::erase */
void
erase(Factory& factory) override;
/** @copydoc Manager::makeBackend */
std::unique_ptr<Backend>
makeBackend(
Section const& parameters,
@@ -37,6 +95,7 @@ public:
Scheduler& scheduler,
beast::Journal journal) override;
/** @copydoc Manager::makeDatabase */
std::unique_ptr<Database>
makeDatabase(
std::size_t burstSize,

View File

@@ -1,3 +1,33 @@
/** @file
* Compression codec for NodeStore blobs written to and read from NuDB.
*
* Every `NodeObject` value stored in the NuDB backend passes through either
* `nodeobjectCompress` or `nodeobjectDecompress`. The on-disk format is a
* leading varint type tag followed by a type-specific payload:
*
* | Tag | Format |
* |-----|--------|
* | 0 | Uncompressed (legacy; readable but never written) |
* | 1 | LZ4-compressed payload |
* | 2 | Sparse inner-node (16-bit presence bitmask + non-zero hashes) |
* | 3 | Full inner-node (all 16 hashes, no bitmask) |
*
* SHAMap inner nodes (exactly 525 bytes with `HashPrefix::InnerNode`) receive
* a specialized encoding that out-performs LZ4 on their typical hash density.
* All other objects are LZ4-compressed (type 1). The codec reconstructs inner
* nodes with `index`, `unused`, and `kind` fields zeroed, so those fields are
* not preserved across a round-trip.
*
* All functions follow the `BufferFactory` pattern: callers supply a callable
* `void*(std::size_t)` that allocates output memory. The codec never frees
* memory; ownership remains with the caller's factory object.
*
* @note This header is an implementation detail of the NuDB backend and the
* NodeStore import tool. It is not part of the public NodeStore API.
*
* @see nodeobjectCompress, nodeobjectDecompress, filterInner
*/
#pragma once
// Disable lz4 deprecation warning due to incompatibility with clang attributes
@@ -19,6 +49,25 @@
namespace xrpl::NodeStore {
/** Decompress an LZ4-compressed blob produced by `lz4Compress`.
*
* Reads a leading varint that encodes the original uncompressed size,
* allocates exactly that many bytes via `bf`, then calls
* `LZ4_decompress_safe` into the allocated buffer.
*
* @tparam BufferFactory Callable with signature `void*(std::size_t n)` that
* allocates `n` bytes and returns a pointer to them. The codec does not
* free this memory; lifetime is governed by the caller.
* @param in Pointer to the compressed input buffer (varint prefix + LZ4 data).
* @param inSize Number of bytes at `in`.
* @param bf Factory used to allocate the decompressed output buffer.
* @return Pair of (pointer to decompressed data, decompressed byte count).
* The pointer is the buffer returned by `bf`.
* @throws std::runtime_error if `inSize` would overflow `int`, if the leading
* varint is missing or occupies the entire buffer, if the decompressed
* size would overflow `int`, or if `LZ4_decompress_safe` returns a byte
* count that does not match the expected output size.
*/
template <class BufferFactory>
std::pair<void const*, std::size_t>
lz4Decompress(void const* in, std::size_t inSize, BufferFactory&& bf)
@@ -48,6 +97,24 @@ lz4Decompress(void const* in, std::size_t inSize, BufferFactory&& bf)
return {out, outSize};
}
/** Compress a raw blob using LZ4 and prepend the uncompressed size as a varint.
*
* Allocates a single output buffer via `bf` sized for the varint prefix plus
* `LZ4_compressBound(inSize)` bytes (worst-case LZ4 output), then writes the
* varint followed by the compressed payload. The returned size reflects the
* actual compressed size, not the worst-case bound.
*
* @tparam BufferFactory Callable with signature `void*(std::size_t n)` that
* allocates `n` bytes and returns a pointer to them. The codec does not
* free this memory; lifetime is governed by the caller.
* @param in Pointer to the uncompressed input data.
* @param inSize Number of bytes at `in`.
* @param bf Factory used to allocate the output buffer.
* @return Pair of (pointer to compressed output, compressed byte count
* including the varint prefix). The pointer is the buffer returned by `bf`.
* @throws std::runtime_error if `LZ4_compress_default` returns 0 (compression
* failure).
*/
template <class BufferFactory>
std::pair<void const*, std::size_t>
lz4Compress(void const* in, std::size_t inSize, BufferFactory&& bf)
@@ -69,17 +136,29 @@ lz4Compress(void const* in, std::size_t inSize, BufferFactory&& bf)
return result;
}
//------------------------------------------------------------------------------
/*
object types:
0 = Uncompressed
1 = lz4 compressed
2 = inner node compressed
3 = full inner node
*/
/** Decompress a NodeStore blob encoded by `nodeobjectCompress`.
*
* Reads the leading varint type tag and dispatches to the appropriate decoder:
* - Type 0: uncompressed legacy data — returned as a non-owning view into `in`.
* - Type 1: delegates to `lz4Decompress`.
* - Type 2: sparse inner-node — reads a 16-bit bitmask, then reconstructs a
* 525-byte SHAMap inner-node blob with only the non-zero child hashes
* filled in and `index`/`unused`/`kind` fields zeroed.
* - Type 3: full inner-node — reads all 512 bytes of child hashes directly and
* reconstructs the 525-byte blob with metadata fields zeroed.
*
* @tparam BufferFactory Callable with signature `void*(std::size_t n)` that
* allocates `n` bytes and returns a pointer to them. Not invoked for type 0
* (the returned pointer into `in` is valid only as long as `in` is alive).
* @param in Pointer to the encoded input buffer.
* @param inSize Number of bytes at `in`.
* @param bf Factory used to allocate decoded output for types 13.
* @return Pair of (pointer to decoded data, decoded byte count).
* @throws std::runtime_error if the type varint is missing, if any size check
* fails during inner-node reconstruction, or if the type tag is unrecognized.
* @note For type 0 the returned pointer aliases `in`; for types 13 it points
* into the buffer supplied by `bf`.
*/
template <class BufferFactory>
std::pair<void const*, std::size_t>
nodeobjectDecompress(void const* in, std::size_t inSize, BufferFactory&& bf)
@@ -184,6 +263,14 @@ nodeobjectDecompress(void const* in, std::size_t inSize, BufferFactory&& bf)
return result;
}
/** Return a pointer to a zero-initialized 32-byte static buffer.
*
* Used by `nodeobjectCompress` as a sentinel to detect empty child-hash
* slots in a SHAMap inner node via `memcmp`. The buffer is function-local
* static so it is initialized exactly once and lives for the process lifetime.
*
* @return Pointer to a 32-byte buffer whose contents are all zero bytes.
*/
template <class = void>
void const*
zero32()
@@ -192,6 +279,31 @@ zero32()
return kV.data();
}
/** Compress a raw NodeStore blob into the NodeStore on-disk wire format.
*
* Detects SHAMap inner nodes (exactly 525 bytes with `HashPrefix::InnerNode`
* at byte offset 9) and applies a specialized encoding:
* - Sparse (type 2): fewer than 16 child slots occupied — stores a 16-bit
* presence bitmask (bit 0x8000 = slot 0) followed by only the non-zero
* hashes packed contiguously.
* - Full (type 3): all 16 slots occupied — stores all 512 hash bytes directly,
* skipping the bitmask.
*
* All other blobs are LZ4-compressed (type 1) via `lz4Compress`. Type 0
* (uncompressed) is never written; the `kCODEC_TYPE` constant is fixed at 1.
*
* @tparam BufferFactory Callable with signature `void*(std::size_t n)` that
* allocates `n` bytes and returns a pointer to them. The codec does not
* free this memory; lifetime is governed by the caller.
* @param in Pointer to the uncompressed NodeStore blob.
* @param inSize Number of bytes at `in`.
* @param bf Factory used to allocate the encoded output buffer.
* @return Pair of (pointer to encoded data, encoded byte count).
* @throws std::runtime_error if LZ4 compression fails.
* @note Inner-node reconstruction zeros `index`, `unused`, and `kind` fields,
* so those fields are not preserved across a compress/decompress round-trip.
* Call `filterInner` on the source blob before round-trip verification.
*/
template <class BufferFactory>
std::pair<void const*, std::size_t>
nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf)
@@ -199,7 +311,6 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf)
using std::runtime_error;
using namespace nudb::detail;
// Check for inner node v1
if (inSize == 525)
{
istream is(in, inSize);
@@ -228,7 +339,6 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf)
std::pair<void const*, std::size_t> result;
if (n < 16)
{
// 2 = v1 inner node compressed
auto const type = 2U;
auto const vs = sizeVarint(type);
result.second = vs + field<std::uint16_t>::size + // mask
@@ -241,7 +351,6 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf)
write(os, vh.data(), n * 32);
return result;
}
// 3 = full v1 inner node
auto const type = 3U;
auto const vs = sizeVarint(type);
result.second = vs + (n * 32); // hashes
@@ -261,7 +370,6 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf)
std::pair<void const*, std::size_t> result;
switch (kCODEC_TYPE)
{
// case 0 was uncompressed data; we always compress now.
case 1: // lz4
{
std::uint8_t* p = nullptr;
@@ -280,17 +388,29 @@ nodeobjectCompress(void const* in, std::size_t inSize, BufferFactory&& bf)
return result;
}
// Modifies an inner node to erase the ledger
// sequence and type information so the codec
// verification can pass.
//
/** Normalize an inner-node blob in place before codec round-trip verification.
*
* `nodeobjectCompress` reconstructs inner nodes with `index`, `unused`, and
* `kind` zeroed (those fields are not stored on disk). Comparing a raw source
* blob against the decompressed output would therefore fail unless the source
* is first normalized by zeroing the same fields. This function performs that
* normalization in place.
*
* The function is a no-op for any blob that is not exactly 525 bytes or does
* not carry the `HashPrefix::InnerNode` marker at byte offset 9.
*
* @param in Pointer to the blob to normalize. Modified in place when the blob
* is identified as a SHAMap inner node.
* @param inSize Number of bytes at `in`.
* @note This function is used by the NodeStore import tool prior to calling
* `nodeobjectCompress` so that the verification `memcmp` succeeds.
*/
template <class = void>
void
filterInner(void* in, std::size_t inSize)
{
using namespace nudb::detail;
// Check for inner node
if (inSize == 525)
{
istream is(in, inSize);

View File

@@ -1,3 +1,23 @@
/** @file
* Variable-length integer (varint) encoding for the NodeStore serialization
* layer.
*
* Provides a base-127 variant of the Protocol Buffers LEB128 varint format.
* Small values (0126) occupy exactly one byte; larger values expand up to
* 10 bytes for a full 64-bit quantity. Used by `codec.h` for two purposes:
* the one-byte object-type discriminant prefix on every stored blob, and the
* decompressed-size prefix that precedes LZ4-compressed payloads.
*
* @note The encoding uses base-127, not the standard base-128, so the byte
* value `0x7F` never appears as a payload byte. The continuation flag
* remains bit 7 (`0x80`), matching the structural appearance of protobuf
* varints.
*
* @note All multi-definition functions (`readVarint`, `writeVarint`) are
* function templates with a defaulted `<class = void>` parameter solely
* to satisfy the ODR when the header is included in multiple translation
* units. They carry no template behaviour beyond that.
*/
#pragma once
#include <nudb/detail/stream.hpp>
@@ -7,30 +27,60 @@
namespace xrpl::NodeStore {
// This is a variant of the base128 varint format from
// google protocol buffers:
// https://developers.google.com/protocol-buffers/docs/encoding#varints
// field tag
/** Tag type used to select the varint overloads of `read` and `write`.
*
* Pass as the explicit template argument at call sites:
* @code
* read<varint>(is, u);
* write<varint>(os, type);
* @endcode
* The tag distinguishes these overloads from NuDB's built-in typed
* `read`/`write` functions for `uint8_t`, `uint16_t`, etc.
*/
struct varint;
// Metafuncton to return largest
// possible size of T represented as varint.
// T must be unsigned
/** Compile-time upper bound on the encoded byte width of type `T`.
*
* `kMAX` is the maximum number of bytes that any value of unsigned type `T`
* can occupy when encoded as a base-127 varint. Use it to allocate
* stack-local buffers without dynamic allocation:
* @code
* std::array<std::uint8_t, varint_traits<std::size_t>::kMAX> buf{};
* @endcode
*
* @tparam T An unsigned integer type. Instantiation with a signed type is
* disabled via SFINAE.
*/
template <class T, bool = std::is_unsigned_v<T>>
struct varint_traits;
/** Specialisation enabled for unsigned types. */
template <class T>
struct varint_traits<T, true>
{
explicit varint_traits() = default;
/** Maximum encoded byte count for type `T` under base-127 encoding. */
static std::size_t constexpr kMAX = (8 * sizeof(T) + 6) / 7;
};
// Returns: Number of bytes consumed or 0 on error,
// if the buffer was too small or t overflowed.
//
/** Decode a base-127 varint from a raw byte buffer.
*
* Scans `buf` for continuation bytes (bit 7 set), then decodes using
* Horner's method from most-significant to least-significant byte so that
* `t = t * 127 + (byte & 0x7F)` reconstructs the original value.
*
* @param buf Pointer to the first byte of the encoded varint.
* @param buflen Number of bytes available in `buf`.
* @param t Output parameter set to the decoded value on success;
* unmodified on error.
* @return Number of bytes consumed from `buf`, or `0` on error. Error
* conditions: `buflen == 0`, the continuation chain extends past
* `buflen`, or arithmetic overflow during accumulation.
* @note The zero value is handled as a special case because the
* overflow guard (`t <= t0`) would otherwise trigger spuriously when
* `t` remains zero after processing a single zero byte.
*/
template <class = void>
std::size_t
readVarint(void const* buf, std::size_t buflen, std::size_t& t)
@@ -67,6 +117,16 @@ readVarint(void const* buf, std::size_t buflen, std::size_t& t)
return used;
}
/** Compute the encoded byte width of `v` without writing anything.
*
* Mirrors the byte count that `writeVarint` would return for the same value.
* Use this to pre-compute output buffer sizes before encoding.
*
* @tparam T An unsigned integer type.
* @param v The value whose encoded size is needed.
* @return Number of bytes required to encode `v` as a base-127 varint
* (always >= 1).
*/
template <class T, std::enable_if_t<std::is_unsigned_v<T>>* = nullptr>
std::size_t
sizeVarint(T v)
@@ -80,6 +140,17 @@ sizeVarint(T v)
return n;
}
/** Encode `v` into the buffer at `p0` as a base-127 varint.
*
* Writes bytes in least-significant-first order. Each byte carries a 7-bit
* payload in bits 06 (range 0126); bit 7 is set on all bytes except the
* last, signalling that more bytes follow.
*
* @param p0 Destination buffer. Must have capacity of at least
* `sizeVarint(v)` bytes; no bounds check is performed.
* @param v The value to encode.
* @return Number of bytes written (same as `sizeVarint(v)`).
*/
template <class = void>
std::size_t
writeVarint(void* p0, std::size_t v)
@@ -97,8 +168,17 @@ writeVarint(void* p0, std::size_t v)
return p - reinterpret_cast<std::uint8_t*>(p0);
}
// input stream
/** Read a varint from a NuDB input stream into `u`.
*
* Advances the stream one byte at a time until a byte without the
* continuation bit is consumed, then delegates to `readVarint` over the
* accumulated span.
*
* @tparam T Must be `varint`; the tag selects this overload over NuDB's
* built-in typed `read` functions.
* @param is The NuDB input stream to read from.
* @param u Output parameter set to the decoded value.
*/
template <class T, std::enable_if_t<std::is_same_v<T, varint>>* = nullptr>
void
read(nudb::detail::istream& is, std::size_t& u)
@@ -110,8 +190,16 @@ read(nudb::detail::istream& is, std::size_t& u)
readVarint(p0, p1 - p0, u);
}
// output stream
/** Write `t` as a varint into a NuDB output stream.
*
* Reserves exactly `sizeVarint(t)` bytes in the stream and encodes `t`
* directly into that region via `writeVarint`.
*
* @tparam T Must be `varint`; the tag selects this overload over NuDB's
* built-in typed `write` functions.
* @param os The NuDB output stream to write into.
* @param t The value to encode.
*/
template <class T, std::enable_if_t<std::is_same_v<T, varint>>* = nullptr>
void
write(nudb::detail::ostream& os, std::size_t t)

View File

@@ -1,3 +1,13 @@
/** @file
* Protocol-level constants, LP-token identity derivation, input-validation
* helpers, and fee-conversion utilities for the XRP Ledger Automated Market
* Maker (AMM) feature.
*
* Every AMM transactor (`AMMCreate`, `AMMDeposit`, `AMMWithdraw`, `AMMBid`,
* `AMMVote`) and `AMMHelpers.h` depend on this header as the single
* authoritative source for numeric parameter encoding and preflight checks.
*/
#pragma once
#include <xrpl/basics/Number.h>
@@ -8,40 +18,116 @@
namespace xrpl {
/** Maximum trading fee, in tenths of a basis point.
*
* Fee integers are in the range `[0, kTRADING_FEE_THRESHOLD]` where
* 1 unit = 0.001% (1/10 bps) and 1000 = 1%.
*/
std::uint16_t constexpr kTRADING_FEE_THRESHOLD = 1000; // 1%
// Auction slot
// --- Auction slot parameters ---
/** Duration of a single auction slot window, in seconds (24 hours). */
std::uint32_t constexpr kTOTAL_TIME_SLOT_SECS = 24 * 3600;
/** Number of equal time intervals the 24-hour auction window is divided into.
*
* The slot index (019) determines how much of the bid price is refunded to
* the outgoing holder when a new bidder takes over mid-window.
*/
std::uint16_t constexpr kAUCTION_SLOT_TIME_INTERVALS = 20;
/** Maximum number of additional accounts a slot holder may authorise to trade
* at the discounted fee.
*/
std::uint16_t constexpr kAUCTION_SLOT_MAX_AUTH_ACCOUNTS = 4;
/** Divisor used to convert a fee integer to the fee fraction `f`.
*
* `f = tfee / kAUCTION_SLOT_FEE_SCALE_FACTOR`. Chosen so that
* `kTRADING_FEE_THRESHOLD / kAUCTION_SLOT_FEE_SCALE_FACTOR == 0.01` (1%).
*/
std::uint32_t constexpr kAUCTION_SLOT_FEE_SCALE_FACTOR = 100000;
/** Denominator for the slot holder's discounted fee.
*
* The effective fee for a slot holder is `tradingFee / kAUCTION_SLOT_DISCOUNTED_FEE_FRACTION`.
*/
std::uint32_t constexpr kAUCTION_SLOT_DISCOUNTED_FEE_FRACTION = 10;
/** Denominator used to compute the minimum bid price for the auction slot.
*
* Minimum bid = `lptAMMBalance × tradingFee / kAUCTION_SLOT_MIN_FEE_FRACTION`.
*/
std::uint32_t constexpr kAUCTION_SLOT_MIN_FEE_FRACTION = 25;
/** Duration of one auction slot interval, in seconds (72 minutes).
*
* Derived as `kTOTAL_TIME_SLOT_SECS / kAUCTION_SLOT_TIME_INTERVALS`.
*/
std::uint32_t constexpr kAUCTION_SLOT_INTERVAL_DURATION =
kTOTAL_TIME_SLOT_SECS / kAUCTION_SLOT_TIME_INTERVALS;
// Votes
// --- Fee-governance vote parameters ---
/** Maximum number of simultaneous fee-vote records in an AMM object. */
std::uint16_t constexpr kVOTE_MAX_SLOTS = 8;
/** Scale factor for LP vote weights.
*
* Each LP's proportional vote weight is stored as an integer in
* `[0, kVOTE_WEIGHT_SCALE_FACTOR]`, avoiding division until the
* weighted-average fee is computed.
*/
std::uint32_t constexpr kVOTE_WEIGHT_SCALE_FACTOR = 100000;
class STObject;
class STAmount;
class Rules;
/** Calculate Liquidity Provider Token (LPT) Currency.
/** Derive the deterministic LP token `Currency` code for an asset pair.
*
* The two assets are sorted canonically before hashing, so
* `ammLPTCurrency(a, b) == ammLPTCurrency(b, a)` for any asset pair.
* The resulting 20-byte currency has `0x03` as its first byte (the AMM
* currency sentinel), followed by 19 bytes taken from
* `sha512Half(canonicalId(min), canonicalId(max))`. For IOU/XRP assets the
* canonical identifier is the `Currency` field; for MPT assets it is the
* `MPTID`.
*
* @param asset1 One of the two pool assets.
* @param asset2 The other pool asset.
* @return A `Currency` value that uniquely identifies the LP token for this
* pair on the ledger and is distinct from any normal IOU or XRP currency.
*/
Currency
ammLPTCurrency(Asset const& asset1, Asset const& asset2);
/** Calculate LPT Issue from AMM asset pair.
/** Construct the full LP token `Issue` (currency + issuer) for an asset pair.
*
* Combines the deterministic currency from `ammLPTCurrency` with the AMM
* account's `AccountID` to produce the `Issue` that `STAmount` operations
* require.
*
* @param asset1 One of the two pool assets.
* @param asset2 The other pool asset.
* @param ammAccountID The `AccountID` of the AMM ledger object.
* @return An `Issue` identifying the LP token for this AMM pool.
*/
Issue
ammLPTIssue(Asset const& asset1, Asset const& asset2, AccountID const& ammAccountID);
/** Validate the amount.
* If validZero is false and amount is beast::zero then invalid amount.
* Return error code if invalid amount.
* If pair then validate amount's issue matches one of the pair's issue.
/** Validate an `STAmount` for use in an AMM transaction (preflight check).
*
* Delegates asset-level validation to `invalidAMMAsset`, then additionally
* rejects negative values and, unless `validZero` is true, zero values.
*
* @param amount The amount to validate.
* @param pair When provided, the amount's asset must match one of the
* two assets in the pair; otherwise `temBAD_AMM_TOKENS` is returned.
* @param validZero If `false` (the default), a zero amount is rejected with
* `temBAD_AMOUNT`.
* @return `tesSUCCESS` if valid; a `tem*` error code otherwise.
*/
NotTEC
invalidAMMAmount(
@@ -49,30 +135,81 @@ invalidAMMAmount(
std::optional<std::pair<Asset, Asset>> const& pair = std::nullopt,
bool validZero = false);
/** Validate a single asset for use in an AMM transaction (preflight check).
*
* - MPT assets with a zero issuer → `temBAD_MPT`.
* - XRP with a non-zero issuer → `temBAD_ISSUER`.
* - Malformed currency codes → `temBAD_CURRENCY`.
* - Asset not matching either element of `pair` (when provided) → `temBAD_AMM_TOKENS`.
*
* @param asset The asset to validate.
* @param pair When provided, `asset` must equal `pair->first` or
* `pair->second`; used to confirm the asset belongs to a specific pool.
* @return `tesSUCCESS` if valid; a `tem*` error code otherwise.
*/
NotTEC
invalidAMMAsset(
Asset const& asset,
std::optional<std::pair<Asset, Asset>> const& pair = std::nullopt);
/** Validate a pair of assets for use in an AMM transaction (preflight check).
*
* Rejects identical assets (`temBAD_AMM_TOKENS`) before delegating each
* asset to `invalidAMMAsset`.
*
* @param asset1 First asset of the pair.
* @param asset2 Second asset of the pair.
* @param pair When provided, each asset must match one element of this
* known-good pair; passed through to `invalidAMMAsset`.
* @return `tesSUCCESS` if valid; a `tem*` error code otherwise.
*/
NotTEC
invalidAMMAssetPair(
Asset const& asset1,
Asset const& asset2,
std::optional<std::pair<Asset, Asset>> const& pair = std::nullopt);
/** Get time slot of the auction slot.
/** Compute the zero-based time-slot index for an active auction slot.
*
* Derives the slot start from `auctionSlot[sfExpiration] - kTOTAL_TIME_SLOT_SECS`,
* then integer-divides elapsed seconds by `kAUCTION_SLOT_INTERVAL_DURATION`.
* Returns `std::nullopt` when `current` is before the slot start or at or
* after `sfExpiration`, indicating the slot has expired or has not yet begun.
*
* @param current Current ledger time (NetClock seconds).
* @param auctionSlot The `STObject` representing the AMM's auction slot;
* must contain `sfExpiration`.
* @return Slot index in `[0, kAUCTION_SLOT_TIME_INTERVALS)`, or
* `std::nullopt` if the slot is not currently active.
* @note An `XRPL_ASSERT` fires if `sfExpiration < kTOTAL_TIME_SLOT_SECS`,
* which is considered an impossible ledger state.
*/
std::optional<std::uint8_t>
ammAuctionTimeSlot(std::uint64_t current, STObject const& auctionSlot);
/** Return true if required AMM amendments are enabled
/** Return true if the network has enabled both AMM amendments.
*
* Requires both `featureAMM` and `fixUniversalNumber`. The second
* amendment is a hard dependency: AMM arithmetic relies on the corrected
* high-precision numeric library introduced by `fixUniversalNumber`, and
* allowing AMM transactions on networks without it would cause overflow or
* precision loss in intermediate swap calculations.
*
* @param rules Snapshot of currently enabled amendments.
* @return `true` only when both `featureAMM` and `fixUniversalNumber` are
* active.
*/
bool
ammEnabled(Rules const&);
/** Convert to the fee from the basis points
* @param tfee trading fee in {0, 1000}
* 1 = 1/10bps or 0.001%, 1000 = 1%
/** Convert a trading fee integer to the fee fraction `f`.
*
* Divides `tfee` by `kAUCTION_SLOT_FEE_SCALE_FACTOR` (100,000) to produce
* the dimensionless fraction used in swap arithmetic. At the maximum fee
* `kTRADING_FEE_THRESHOLD = 1000`, `getFee` returns `0.01` (1%).
*
* @param tfee Trading fee integer in `[0, kTRADING_FEE_THRESHOLD]`.
* @return Fee fraction `f = tfee / 100000`.
*/
inline Number
getFee(std::uint16_t tfee)
@@ -80,8 +217,15 @@ getFee(std::uint16_t tfee)
return Number{tfee} / kAUCTION_SLOT_FEE_SCALE_FACTOR;
}
/** Get fee multiplier (1 - tfee)
* @tfee trading fee in basis points
/** Compute the full-fee swap multiplier `(1 - f)`.
*
* Applied to the input amount when the complete trading fee is charged,
* i.e. during ordinary swaps. In `AMMDeposit`, this is the `f1` factor
* in the single-asset constant-product formula.
*
* @param tfee Trading fee integer in `[0, kTRADING_FEE_THRESHOLD]`.
* @return `1 - getFee(tfee)`.
* @see feeMultHalf
*/
inline Number
feeMult(std::uint16_t tfee)
@@ -89,8 +233,15 @@ feeMult(std::uint16_t tfee)
return 1 - getFee(tfee);
}
/** Get fee multiplier (1 - tfee / 2)
* @tfee trading fee in basis points
/** Compute the half-fee swap multiplier `(1 - f/2)`.
*
* Used during single-asset deposits where only half the implied fee is
* deducted. In `AMMDeposit`, the combined factor is
* `f2 = feeMultHalf(tfee) / feeMult(tfee)`.
*
* @param tfee Trading fee integer in `[0, kTRADING_FEE_THRESHOLD]`.
* @return `1 - getFee(tfee) / 2`.
* @see feeMult
*/
inline Number
feeMultHalf(std::uint16_t tfee)

View File

@@ -1,3 +1,8 @@
/** @file
* Defines the AccountID type, serialization helpers, sentinel constants,
* and the optional base58 encoding cache for XRP Ledger account identities.
*/
#pragma once
#include <xrpl/protocol/tokens.h>
@@ -16,6 +21,12 @@ namespace xrpl {
namespace detail {
/** Phantom tag type that makes AccountID a distinct strong type.
*
* Passed as the second template argument to `BaseUInt<160, Tag>` so that
* a 160-bit account hash cannot be silently used where a raw hash or node ID
* is expected, and vice versa. The class has no data members or behaviour.
*/
class AccountIDTag
{
public:
@@ -24,47 +35,109 @@ public:
} // namespace detail
/** A 160-bit unsigned that uniquely identifies an account. */
/** A 160-bit identifier that uniquely addresses an XRP Ledger account.
*
* Stored as five `uint32_t` values in big-endian byte order — a layout
* that is part of the binary serialization protocol and cannot be changed.
* Derived from a public key via SHA-256 + RIPEMD-160 (`calcAccountID()`).
*
* The phantom tag `detail::AccountIDTag` makes this a distinct C++ type,
* preventing accidental mixing with other 160-bit quantities at compile time.
*
* @see calcAccountID(), toBase58(), parseBase58<AccountID>()
*/
using AccountID = BaseUInt<160, detail::AccountIDTag>;
/** Convert AccountID to base58 checked string */
/** Encode an AccountID as a Base58Check string.
*
* Prepends `TokenType::AccountID` (value 0) before encoding. When the
* global cache has been initialised via `initAccountIdCache()`, the result
* is served from the cache to avoid repeated SHA-256 checksum computation.
*
* @param v The account identifier to encode.
* @return The Base58Check-encoded string (always 2534 printable characters).
* @see initAccountIdCache(), parseBase58<AccountID>()
*/
std::string
toBase58(AccountID const& v);
/** Parse AccountID from checked, base58 string.
@return std::nullopt if a parse error occurs
*/
/** Decode a Base58Check string into an AccountID.
*
* Validates the `TokenType::AccountID` prefix and requires the decoded
* payload to be exactly 20 bytes. Input that fails either check returns
* `std::nullopt` rather than throwing, because external input is frequently
* untrusted.
*
* @param s The Base58Check-encoded account string to parse.
* @return The decoded AccountID, or `std::nullopt` on any parse failure.
* @see toBase58()
*/
template <>
std::optional<AccountID>
parseBase58(std::string const& s);
/** Compute AccountID from public key.
The account ID is computed as the 160-bit hash of the
public key data. This excludes the version byte and
guard bytes included in the base58 representation.
*/
/** Compute the AccountID for a public key using SHA-256 + RIPEMD-160.
*
* Applies `RipeshaHasher` to the raw public-key bytes (no version byte).
* The double-hash matches Bitcoin's derivation: SHA-256 prevents
* length-extension attacks, and RIPEMD-160 is considered safe at 160 bits.
* XRPL adopted the scheme to avoid any claim of weaker security relative
* to Bitcoin.
*
* @note Declaration lives in `PublicKey.h`; the implementation is in
* `AccountID.cpp`.
*/
// VFALCO In PublicKey.h for now
// AccountID
// calcAccountID (PublicKey const& pk);
/** A special account that's used as the "issuer" for XRP. */
/** Return the canonical XRP issuer sentinel: the all-zero AccountID.
*
* Used as the issuer field in XRP `STAmount` values. Code that needs to
* test whether an amount is native XRP should prefer checking the native
* flag or the currency directly rather than comparing the issuer against
* this value — see the deprecated `isXRP(AccountID)` overload.
*
* @return A function-local static `AccountID` equal to `beast::kZERO`.
* Returned by `const&` to avoid copies; lifetime is the process lifetime.
*/
AccountID const&
xrpAccount();
/** A placeholder for empty accounts. */
/** Return the "no account" sentinel: `AccountID(1)`.
*
* Used as a placeholder in offer and trust-line fields that have no
* meaningful account value (e.g., an uninitialized or absent counterparty).
* Distinct from `xrpAccount()` (all zeros) so the two sentinels cannot
* be confused.
*
* @return A function-local static `AccountID` with value 1.
* Returned by `const&` to avoid copies; lifetime is the process lifetime.
*/
AccountID const&
noAccount();
/** Convert hex or base58 string to AccountID.
@return `true` if the parsing was successful.
*/
/** Parse a hex or Base58Check string into an AccountID.
*
* Tries hex first (`parseHex`), then falls back to Base58Check. Used
* in legacy configuration parsing where the encoding is not guaranteed.
*
* @param issuer Output: receives the parsed AccountID on success.
* @param s The hex (40 chars) or Base58Check string to parse.
* @return `true` if parsing succeeded and `issuer` was written.
* @deprecated Prefer `parseBase58<AccountID>()` for user-facing input.
*/
// DEPRECATED
bool
toIssuer(AccountID&, std::string const&);
/** Test whether an AccountID equals the XRP issuer sentinel (all zeros).
*
* @param c The account identifier to test.
* @return `true` if `c` equals `beast::kZERO` (i.e., equals `xrpAccount()`).
* @deprecated Check the currency field or the native/integral flag instead;
* relying on the zero-account-as-issuer convention is a leaky abstraction.
*/
// DEPRECATED Should be checking the currency or native flag
inline bool
isXRP(AccountID const& c)
@@ -72,6 +145,12 @@ isXRP(AccountID const& c)
return c == beast::kZERO;
}
/** Convert an AccountID to its Base58Check string representation.
*
* @param account The account identifier to convert.
* @return The Base58Check-encoded string.
* @deprecated Use `toBase58()` directly.
*/
// DEPRECATED
inline std::string
to_string(AccountID const& account)
@@ -79,6 +158,14 @@ to_string(AccountID const& account)
return toBase58(account);
}
/** Write the Base58Check encoding of an AccountID to an output stream.
*
* @param os The stream to write to.
* @param x The account identifier to encode.
* @return `os`, to allow chaining.
* @deprecated Prefer explicit `toBase58()` calls; stream output silently
* invokes Base58 encoding and can be surprising in logging contexts.
*/
// DEPRECATED
inline std::ostream&
operator<<(std::ostream& os, AccountID const& x)
@@ -87,17 +174,22 @@ operator<<(std::ostream& os, AccountID const& x)
return os;
}
/** Initialize the global cache used to map AccountID to base58 conversions.
The cache is optional and need not be initialized. But because conversion
is expensive (it requires a SHA-256 operation) in most cases the overhead
of the cache is worth the benefit.
@param count The number of entries the cache should accommodate. Zero will
disable the cache, releasing any memory associated with it.
@note The function will only initialize the cache the first time it is
invoked. Subsequent invocations do nothing.
/** Initialize the global AccountID → Base58Check encoding cache.
*
* Base58Check encoding requires a SHA-256 checksum on every call, which is
* expensive at transaction-processing throughput. The cache uses a
* direct-mapped open-addressing table with 64 spinlocks packed into a single
* `atomic<uint64_t>` (via `PackedSpinlock`) to allow concurrent access with
* minimal memory overhead. The index hash is `hardened_hash<>` (DoS-
* resistant seeded hash) to prevent crafted workloads from degrading lookups.
*
* The cache is strictly optional: if never initialised, `toBase58()` falls
* through to `encodeBase58Token` on every call.
*
* @param count The number of cache slots to allocate. Pass 0 to leave the
* cache disabled (no-op if already disabled).
* @note This function initialises the cache at most once. Subsequent calls
* with any `count` value are silently ignored.
*/
void
initAccountIdCache(std::size_t count);
@@ -106,6 +198,21 @@ initAccountIdCache(std::size_t count);
//------------------------------------------------------------------------------
namespace json {
/** Extract and parse an AccountID from a JSON object field.
*
* Reads `field` from `v` as a string, then decodes it as a Base58Check
* account address. Throws `JsonTypeMismatchError` if the field is absent,
* not a string, or not a valid AccountID encoding — the same error type
* raised for any other JSON type mismatch, enabling uniform error handling
* in RPC and transaction-parsing code.
*
* @param v The JSON object to read from.
* @param field The SField identifying the key to look up.
* @return The decoded AccountID.
* @throws JsonTypeMismatchError if the field is missing, not a string, or
* cannot be decoded as a valid Base58Check AccountID.
*/
template <>
inline xrpl::AccountID
getOrThrow(json::Value const& v, xrpl::SField const& field)
@@ -123,6 +230,15 @@ getOrThrow(json::Value const& v, xrpl::SField const& field)
namespace std {
/** `std::hash` specialization for AccountID, delegating to `hardened_hash<>`.
*
* Maintains compatibility with standard-library unordered containers that
* key on `AccountID`. The underlying hasher uses a random seed (DoS-
* resistant), so hash values differ across process restarts.
*
* @deprecated Prefer `beast::uhash` or XRPL's hardened unordered containers
* (`UnorderedMap`, `UnorderedSet`) for new code.
*/
// DEPRECATED
// VFALCO Use beast::uhash or a hardened container
template <>

View File

@@ -1,5 +1,17 @@
#pragma once
/** @file
* Conversion utilities between the four XRPL amount representations.
*
* The protocol defines four amount types, each optimized for a different
* concern: `XRPAmount` (integer drops), `MPTAmount` (integer MPT units),
* `IOUAmount` (normalized floating-point), and `STAmount` (wire-level union
* over all three). Generic algorithms — AMM pricing, pathfinding, offer
* crossing — need to work across all four without duplicating logic. This
* header provides the glue: inline conversion functions that move freely
* between representations. No arithmetic or business logic lives here.
*/
#include <xrpl/protocol/IOUAmount.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STAmount.h>
@@ -9,6 +21,20 @@
namespace xrpl {
/** Wrap an `IOUAmount` in a serializable `STAmount` tagged with the given asset.
*
* `IOUAmount` stores a signed mantissa; `STAmount` stores an unsigned mantissa
* with a separate sign bit. This overload performs that split manually and
* constructs via `STAmount::Unchecked()` to skip re-canonicalization —
* `IOUAmount` is already normalized so re-canonicalizing would be wasted work.
*
* @param iou The IOU value to wrap.
* @param asset The asset identity to embed; must hold an `Issue` (not XRP
* or MPT) — verified by assertion.
* @return An `STAmount` encoding the same value and sign as `iou`.
* @note Passing an XRP or MPT asset silently produces wrong data in release
* builds; the assertion catches this only in debug builds.
*/
inline STAmount
toSTAmount(IOUAmount const& iou, Asset const& asset)
{
@@ -18,12 +44,26 @@ toSTAmount(IOUAmount const& iou, Asset const& asset)
return STAmount(asset, umant, iou.exponent(), isNeg, STAmount::Unchecked());
}
/** Wrap an `IOUAmount` in an `STAmount` with a placeholder `noIssue()` asset.
*
* Convenience overload for contexts where the true asset identity is not
* available at the call site. The resulting `STAmount` carries `noIssue()`
* as its asset tag and should not be used in wire serialization.
*
* @param iou The IOU value to wrap.
* @return An `STAmount` encoding the value of `iou` with `noIssue()` asset.
*/
inline STAmount
toSTAmount(IOUAmount const& iou)
{
return toSTAmount(iou, noIssue());
}
/** Wrap an `XRPAmount` in a serializable `STAmount`.
*
* @param xrp The XRP drop count to wrap; may be negative.
* @return A native `STAmount` encoding the same value and sign as `xrp`.
*/
inline STAmount
toSTAmount(XRPAmount const& xrp)
{
@@ -32,6 +72,16 @@ toSTAmount(XRPAmount const& xrp)
return STAmount(umant, isNeg);
}
/** Wrap an `XRPAmount` in an `STAmount` given an explicit `Asset`.
*
* Exists to give generic code a uniform `toSTAmount(amount, asset)` call
* signature; delegates immediately to the asset-less overload after asserting
* that `asset` is XRP.
*
* @param xrp The XRP drop count to wrap.
* @param asset Must be the XRP asset — verified by assertion.
* @return A native `STAmount` encoding the same value as `xrp`.
*/
inline STAmount
toSTAmount(XRPAmount const& xrp, Asset const& asset)
{
@@ -39,12 +89,25 @@ toSTAmount(XRPAmount const& xrp, Asset const& asset)
return toSTAmount(xrp);
}
/** Wrap an `MPTAmount` in an `STAmount` with a placeholder `noMPT()` asset.
*
* @param mpt The MPT unit count to wrap.
* @return An `STAmount` encoding the value of `mpt` with `noMPT()` asset.
*/
inline STAmount
toSTAmount(MPTAmount const& mpt)
{
return STAmount(mpt, noMPT());
}
/** Wrap an `MPTAmount` in an `STAmount` tagged with the given MPT asset.
*
* @param mpt The MPT unit count to wrap.
* @param asset The asset identity to embed; must hold an `MPTIssue` —
* verified by assertion.
* @return An `STAmount` encoding the value of `mpt` with the given
* `MPTIssue` identity.
*/
inline STAmount
toSTAmount(MPTAmount const& mpt, Asset const& asset)
{
@@ -52,10 +115,24 @@ toSTAmount(MPTAmount const& mpt, Asset const& asset)
return STAmount(mpt, asset.get<MPTIssue>());
}
/** Primary template for `STAmount` → lean-type extraction; intentionally deleted.
*
* Calling `toAmount<T>(stamt)` with an unsupported `T` is a hard compile
* error rather than a linker error or silent mis-conversion. Only the
* explicit specializations below (`STAmount`, `IOUAmount`, `XRPAmount`,
* `MPTAmount`) are valid.
*
* @tparam T Target amount type.
*/
template <class T>
T
toAmount(STAmount const& amt) = delete;
/** Identity conversion: return the `STAmount` unchanged.
*
* @param amt The `STAmount` to return.
* @return `amt` unchanged.
*/
template <>
inline STAmount
toAmount<STAmount>(STAmount const& amt)
@@ -63,6 +140,16 @@ toAmount<STAmount>(STAmount const& amt)
return amt;
}
/** Extract the IOU value from an `STAmount` as an `IOUAmount`.
*
* Reconstitutes the signed mantissa (STAmount stores it unsigned + sign bit)
* and constructs an `IOUAmount` directly without re-canonicalization.
*
* @param amt The source `STAmount`; must not be a native XRP amount —
* verified by assertion. Mantissa must fit in `int64_t` — verified
* by assertion.
* @return An `IOUAmount` with the same signed mantissa and exponent.
*/
template <>
inline IOUAmount
toAmount<IOUAmount>(STAmount const& amt)
@@ -77,6 +164,13 @@ toAmount<IOUAmount>(STAmount const& amt)
return IOUAmount(sMant, amt.exponent());
}
/** Extract the XRP drop count from an `STAmount` as an `XRPAmount`.
*
* @param amt The source `STAmount`; must be a native XRP amount —
* verified by assertion. Mantissa must fit in `int64_t` — verified
* by assertion.
* @return An `XRPAmount` holding the signed drop count.
*/
template <>
inline XRPAmount
toAmount<XRPAmount>(STAmount const& amt)
@@ -91,6 +185,20 @@ toAmount<XRPAmount>(STAmount const& amt)
return XRPAmount(sMant);
}
/** Extract the MPT unit count from an `STAmount` as an `MPTAmount`.
*
* MPT amounts are integers: the exponent must be exactly 0 and the
* mantissa must not exceed `kMAX_MP_TOKEN_AMOUNT`. Both constraints are
* checked in debug builds (assertion) and in release builds (exception),
* because a violation indicates data corruption or a ledger encoding bug
* that should surface loudly rather than silently truncate.
*
* @param amt The source `STAmount`; must hold an `MPTIssue`, have exponent
* 0, and mantissa ≤ `kMAX_MP_TOKEN_AMOUNT`.
* @return An `MPTAmount` holding the signed unit count.
* @throws std::runtime_error if `amt.exponent() != 0` or
* `amt.mantissa() > kMAX_MP_TOKEN_AMOUNT`.
*/
template <>
inline MPTAmount
toAmount<MPTAmount>(STAmount const& amt)
@@ -106,10 +214,24 @@ toAmount<MPTAmount>(STAmount const& amt)
return MPTAmount(sMant);
}
/** Primary template for `IOUAmount` → same-type extraction; intentionally deleted.
*
* Only the `IOUAmount` identity specialization below is valid.
*
* @tparam T Target amount type.
*/
template <class T>
T
toAmount(IOUAmount const& amt) = delete;
/** Identity conversion: return the `IOUAmount` unchanged.
*
* Allows generic code to call `toAmount<IOUAmount>(iouValue)` without
* branching on whether the source is already the target type.
*
* @param amt The `IOUAmount` to return.
* @return `amt` unchanged.
*/
template <>
inline IOUAmount
toAmount<IOUAmount>(IOUAmount const& amt)
@@ -117,10 +239,24 @@ toAmount<IOUAmount>(IOUAmount const& amt)
return amt;
}
/** Primary template for `XRPAmount` → same-type extraction; intentionally deleted.
*
* Only the `XRPAmount` identity specialization below is valid.
*
* @tparam T Target amount type.
*/
template <class T>
T
toAmount(XRPAmount const& amt) = delete;
/** Identity conversion: return the `XRPAmount` unchanged.
*
* Allows generic code to call `toAmount<XRPAmount>(xrpValue)` without
* branching on whether the source is already the target type.
*
* @param amt The `XRPAmount` to return.
* @return `amt` unchanged.
*/
template <>
inline XRPAmount
toAmount<XRPAmount>(XRPAmount const& amt)
@@ -128,10 +264,24 @@ toAmount<XRPAmount>(XRPAmount const& amt)
return amt;
}
/** Primary template for `MPTAmount` → same-type extraction; intentionally deleted.
*
* Only the `MPTAmount` identity specialization below is valid.
*
* @tparam T Target amount type.
*/
template <class T>
T
toAmount(MPTAmount const& amt) = delete;
/** Identity conversion: return the `MPTAmount` unchanged.
*
* Allows generic code to call `toAmount<MPTAmount>(mptValue)` without
* branching on whether the source is already the target type.
*
* @param amt The `MPTAmount` to return.
* @return `amt` unchanged.
*/
template <>
inline MPTAmount
toAmount<MPTAmount>(MPTAmount const& amt)
@@ -139,6 +289,27 @@ toAmount<MPTAmount>(MPTAmount const& amt)
return amt;
}
/** Convert a `Number` intermediate result to a typed amount, applying a
* caller-specified rounding mode for XRP.
*
* Used by AMM pricing and pathfinding after performing arithmetic in
* `Number` space. The rounding mode override is applied **only for XRP**:
* XRP is an integer count of drops, so converting a rational intermediate
* requires a deterministic rounding decision. IOU and MPT types handle
* normalization internally and do not require external rounding control.
* `SaveNumberRoundMode` restores the previous thread-local rounding mode
* on destruction, even if the conversion throws.
*
* @tparam T Target amount type: `IOUAmount`, `XRPAmount`, `MPTAmount`, or
* `STAmount`. Any other type is a compile error.
* @param asset The asset identity for the result; must be consistent with
* `T` (e.g., XRP asset with `XRPAmount`).
* @param n The intermediate `Number` value to convert.
* @param mode Rounding mode applied when `T` is `XRPAmount` or when
* `T` is `STAmount` with an XRP asset. Defaults to the current
* thread-local rounding mode.
* @return The converted amount of type `T`.
*/
template <typename T>
T
toAmount(Asset const& asset, Number const& n, Number::RoundingMode mode = Number::getround())
@@ -172,6 +343,17 @@ toAmount(Asset const& asset, Number const& n, Number::RoundingMode mode = Number
}
}
/** Return the maximum representable value for a given amount type and asset.
*
* Dispatches at compile time on `T`. For `STAmount` the result depends on
* the runtime asset: XRP uses `kMAX_NATIVE_N` drops; IOU uses
* `(kMAX_VALUE, kMAX_OFFSET)`; MPT uses `kMAX_MP_TOKEN_AMOUNT`.
*
* @tparam T Target amount type: `IOUAmount`, `XRPAmount`, `MPTAmount`, or
* `STAmount`. Any other type is a compile error.
* @param asset The asset identity; consulted only when `T` is `STAmount`.
* @return The maximum representable value of type `T`.
*/
template <typename T>
T
toMaxAmount(Asset const& asset)
@@ -205,12 +387,38 @@ toMaxAmount(Asset const& asset)
}
}
/** Convert a `Number` intermediate to an `STAmount` with a given asset and rounding mode.
*
* Thin wrapper around `toAmount<STAmount>(asset, n, mode)` provided so
* callers that always work with `STAmount` can use a non-template name.
*
* @param asset The asset identity for the result.
* @param n The intermediate `Number` value to convert.
* @param mode Rounding mode applied when `asset` is XRP. Defaults to the
* current thread-local rounding mode.
* @return An `STAmount` encoding `n` tagged with `asset`.
* @see toAmount
*/
inline STAmount
toSTAmount(Asset const& asset, Number const& n, Number::RoundingMode mode = Number::getround())
{
return toAmount<STAmount>(asset, n, mode);
}
/** Return a placeholder `Asset` for a given amount type.
*
* For `STAmount` this delegates to `amt.asset()` and returns the true asset.
* For lean types — `IOUAmount`, `XRPAmount`, `MPTAmount` — which do not
* carry asset identity, a sentinel is returned: `noIssue()`, `xrpIssue()`,
* or `noMPT()` respectively. Callers such as AMM helpers use this to
* produce an `Asset` argument for a subsequent `toAmount<T>(asset, ...)` call
* where the true asset is known from context.
*
* @tparam T Source amount type. Any other type is a compile error.
* @param amt The amount whose asset identity is requested.
* @return The true `Asset` for `STAmount`; a type-appropriate sentinel
* for lean types.
*/
template <typename T>
Asset
getAsset(T const& amt)
@@ -238,6 +446,19 @@ getAsset(T const& amt)
}
}
/** Extract a typed value from an `STAmount` by delegating to the
* appropriate accessor.
*
* Dispatches at compile time: `IOUAmount` → `a.iou()`, `XRPAmount` →
* `a.xrp()`, `MPTAmount` → `a.mpt()`, `STAmount` → identity. The
* `static_assert` in the else branch uses a type-dependent expression
* so it fires only when the unsupported branch is actually instantiated,
* not on every parse of the template.
*
* @tparam T Target lean type or `STAmount`. Any other type is a compile error.
* @param a The source `STAmount`.
* @return The value of `a` expressed as type `T`.
*/
template <typename T>
constexpr T
get(STAmount const& a)

View File

@@ -8,43 +8,76 @@
#include <type_traits>
#include <utility>
namespace xrpl {
/**
* API version numbers used in later API versions
* @file ApiVersion.h
* @brief Single source of truth for the XRPL RPC API versioning scheme.
*
* Requests with a version number in the range
* [apiMinimumSupportedVersion, apiMaximumSupportedVersion]
* are supported.
* Defines the compile-time integer constants that bound the accepted API
* version range, JSON parsing and serialization helpers that enforce those
* bounds at the RPC ingress point, and compile-time iteration templates
* (`forApiVersions`, `forAllApiVersions`) that let the rest of the codebase
* generate version-aware code paths without runtime switches.
*
* If [beta_rpc_api] is enabled in config, the version numbers
* in the range [apiMinimumSupportedVersion, apiBetaVersion]
* are supported.
*
* Network Requests without explicit version numbers use
* apiVersionIfUnspecified. apiVersionIfUnspecified is 1,
* because all the RPC requests with a version >= 2 must
* explicitly specify the version in the requests.
* Note that apiVersionIfUnspecified will be lower than
* apiMinimumSupportedVersion when we stop supporting API
* version 1.
*
* Command line Requests use apiCommandLineVersion.
* The versioning constants dictate the size and index mapping of every
* `MultiApiJson` array in the system — changing them automatically adjusts
* every data structure that stores per-version output.
*/
namespace xrpl {
namespace RPC {
/**
* @brief Typed version-constant factory.
*
* Produces an `std::integral_constant<unsigned, Version>` tag for the given
* version number. Using a distinct type per version allows overload resolution
* and `if constexpr` branching at compile time while still implicitly decaying
* to `unsigned` in arithmetic and comparison contexts.
*
* @tparam Version The API version number to encode as a type.
*/
template <unsigned int Version>
constexpr static std::integral_constant<unsigned, Version> kAPI_VERSION = {};
/** Sentinel returned by `getAPIVersionNumber()` when parsing fails or the
* supplied version falls outside the supported range. Callers that receive
* this value must reject the request before any handler dispatch. */
constexpr static auto kAPI_INVALID_VERSION = kAPI_VERSION<0>;
/** Oldest API version still accepted from network clients. Requests with a
* lower version are rejected; the floor advances when old versions are
* retired. */
constexpr static auto kAPI_MINIMUM_SUPPORTED_VERSION = kAPI_VERSION<1>;
/** Newest stable API version. Network requests are capped here unless the
* `[beta_rpc_api]` configuration flag is set, in which case
* `kAPI_BETA_VERSION` becomes the effective ceiling. */
constexpr static auto kAPI_MAXIMUM_SUPPORTED_VERSION = kAPI_VERSION<2>;
/** Implicit version assigned when a request omits the `api_version` field.
* Fixed at 1 because any request at version 2 or above must carry an
* explicit field; omitting it is treated as a version-1 request rather than
* an error. This constant will fall below `kAPI_MINIMUM_SUPPORTED_VERSION`
* once version-1 support is retired. */
constexpr static auto kAPI_VERSION_IF_UNSPECIFIED = kAPI_VERSION<1>;
constexpr static auto kAPI_COMMAND_LINE_VERSION = kAPI_VERSION<1>; // TODO Bump to 2 later
/** Version used for command-line invocations.
* @note TODO: bump to 2 in a future release. */
constexpr static auto kAPI_COMMAND_LINE_VERSION = kAPI_VERSION<1>;
/** Experimental version gated behind the `[beta_rpc_api]` configuration flag.
* Completely invisible to clients connecting to a production node that has
* not opted in. */
constexpr static auto kAPI_BETA_VERSION = kAPI_VERSION<3>;
/** Absolute ceiling for template range loops; always equal to
* `kAPI_BETA_VERSION`. Drives the size of `MultiApiJson` arrays and the
* upper bound of `forAllApiVersions`. */
constexpr static auto kAPI_MAXIMUM_VALID_VERSION = kAPI_BETA_VERSION;
// --- Version-range invariants (load-bearing; update assertions when bumping
// any constant above) ---
static_assert(kAPI_INVALID_VERSION < kAPI_MINIMUM_SUPPORTED_VERSION);
static_assert(
kAPI_VERSION_IF_UNSPECIFIED >= kAPI_MINIMUM_SUPPORTED_VERSION &&
@@ -56,6 +89,28 @@ static_assert(kAPI_MAXIMUM_SUPPORTED_VERSION >= kAPI_MINIMUM_SUPPORTED_VERSION);
static_assert(kAPI_BETA_VERSION >= kAPI_MAXIMUM_SUPPORTED_VERSION);
static_assert(kAPI_MAXIMUM_VALID_VERSION >= kAPI_MAXIMUM_SUPPORTED_VERSION);
/**
* @brief Populate the `version` sub-object in an RPC response.
*
* The output format diverges by negotiated version to maintain backwards
* compatibility:
* - **Version 1** (legacy): emits `first`, `good`, and `last` as semver
* strings (e.g. `"1.0.0"`). Static `SemanticVersion` objects are used to
* avoid repeated string parsing on every call.
* - **Version 2+**: emits `first` as the minimum supported version integer
* and `last` as either `kAPI_BETA_VERSION` or `kAPI_MAXIMUM_SUPPORTED_VERSION`
* depending on `betaEnabled`.
*
* The primary consumer is `VersionHandler` in
* `src/xrpld/rpc/handlers/server_info/Version.h`.
*
* @param parent The JSON object into which the `version` key is written.
* @param apiVersion The negotiated API version for the current request; must
* not be `kAPI_INVALID_VERSION`.
* @param betaEnabled Whether the `[beta_rpc_api]` configuration flag is set,
* which extends the reported `last` version to include the
* beta version.
*/
inline void
setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled)
{
@@ -65,7 +120,7 @@ setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled)
if (apiVersion == kAPI_VERSION_IF_UNSPECIFIED)
{
// API version numbers used in API version 1
// Legacy semver-string format required by API version 1 clients.
static beast::SemanticVersion const kFIRST_VERSION{"1.0.0"};
static beast::SemanticVersion const kGOOD_VERSION{"1.0.0"};
static beast::SemanticVersion const kLAST_VERSION{"1.0.0"};
@@ -82,18 +137,28 @@ setVersion(json::Value& parent, unsigned int apiVersion, bool betaEnabled)
}
/**
* Retrieve the api version number from the json value
* @brief Extract and validate the API version from an incoming RPC request.
*
* Note that APIInvalidVersion will be returned if
* 1) the version number field has a wrong format
* 2) the version number retrieved is out of the supported range
* 3) the version number is unspecified and
* APIVersionIfUnspecified is out of the supported range
* Called at the RPC ingress point (`ServerHandler.cpp`) on every HTTP and
* WebSocket request before handler dispatch. The function inspects the
* top-level `api_version` field of `jv`:
* - If the field is absent, returns `kAPI_VERSION_IF_UNSPECIFIED`.
* - If the field is present but not an integer, returns `kAPI_INVALID_VERSION`.
* - If the integer value falls outside
* `[kAPI_MINIMUM_SUPPORTED_VERSION, maxVersion]`, returns
* `kAPI_INVALID_VERSION`.
* - Otherwise returns the integer value directly.
*
* @param jv a Json value that may or may not specify
* the api version number
* @param betaEnabled if the beta API version is enabled
* @return the api version number
* Callers must treat a `kAPI_INVALID_VERSION` return as a signal to reject
* the request immediately with an appropriate error.
*
* @param jv The top-level JSON object of the incoming request.
* @param betaEnabled When `false`, the effective ceiling is
* `kAPI_MAXIMUM_SUPPORTED_VERSION`; when `true`, the ceiling
* extends to `kAPI_BETA_VERSION`. Reflects the
* `BETA_RPC_API` configuration flag of the serving node.
* @return The negotiated API version, or `kAPI_INVALID_VERSION` if the
* request must be rejected.
*/
inline unsigned int
getAPIVersionNumber(json::Value const& jv, bool betaEnabled)
@@ -125,6 +190,39 @@ getAPIVersionNumber(json::Value const& jv, bool betaEnabled)
} // namespace RPC
/**
* @brief Invoke a callable once for each API version in `[MinVer, MaxVer]`,
* passing the version as a distinct `std::integral_constant` type.
*
* The range is expanded into a parameter pack at compile time via
* `std::make_index_sequence`, and the callable is called once per version in
* order. Because each invocation receives a different type
* (`std::integral_constant<unsigned, N>`), the callable may use
* `if constexpr (Version >= 2)` to eliminate dead branches at compile time
* rather than relying on a runtime switch.
*
* The C++20 `requires` clause enforces three constraints statically:
* - `MaxVer >= MinVer` (non-empty range),
* - `MinVer >= kAPI_MINIMUM_SUPPORTED_VERSION` (floor bound),
* - `MaxVer <= kAPI_MAXIMUM_VALID_VERSION` (ceiling bound).
* A caller that attempts to iterate outside the known valid range fails to
* compile rather than producing a runtime out-of-bounds error.
*
* @note The `NOLINTBEGIN/NOLINTEND` block suppresses a spurious
* `bugprone-use-after-move` warning that clang-tidy raises on the fold
* expression when `Args` contains move-only types; the fold is safe
* because perfect-forwarding within a comma-expression does not actually
* move from the same argument twice.
*
* @tparam MinVer First version in the iteration range (inclusive).
* @tparam MaxVer Last version in the iteration range (inclusive).
* @tparam Fn Callable type; must be invocable with
* `(std::integral_constant<unsigned, V>, Args&&...)` for
* every `V` in `[MinVer, MaxVer]`.
* @tparam Args Additional arguments forwarded verbatim to each invocation.
* @param fn The callable to invoke for each version.
* @param args Additional arguments forwarded to each invocation of `fn`.
*/
template <unsigned MinVer, unsigned MaxVer, typename Fn, typename... Args>
void
forApiVersions(Fn const& fn, Args&&... args)
@@ -146,6 +244,28 @@ forApiVersions(Fn const& fn, Args&&... args)
}(std::make_index_sequence<kSIZE>{});
}
/**
* @brief Invoke a callable once for every supported API version
* (`[kAPI_MINIMUM_SUPPORTED_VERSION, kAPI_MAXIMUM_VALID_VERSION]`).
*
* Thin wrapper around `forApiVersions` that fixes the range to the full set
* of known versions (currently 13). This is the standard way to:
* - Run a test scenario against every version in CI.
* - Populate all slots of a `MultiApiJson` fan-out in a single pass (e.g.
* `NetworkOPs.cpp` uses it to build per-subscriber data when notifying of
* new transactions, calling `insertDeliverMax` only for versions where that
* field is defined).
*
* Each invocation of `fn` receives a distinct
* `std::integral_constant<unsigned, N>` type for the version, enabling
* compile-time branching inside the lambda body.
*
* @tparam Fn Callable type; must satisfy the constraints of
* `forApiVersions` for the full version range.
* @tparam Args Additional arguments forwarded verbatim to each invocation.
* @param fn The callable to invoke for each version.
* @param args Additional arguments forwarded to each invocation of `fn`.
*/
template <typename Fn, typename... Args>
void
forAllApiVersions(Fn const& fn, Args&&... args)

View File

@@ -1,3 +1,18 @@
/**
* @file Asset.h
* @brief Unified asset abstraction for XRP, IOU, and MPT value types.
*
* Introduces the `Asset` type, a `std::variant<Issue, MPTIssue>` wrapper that
* represents all three kinds of transferable value on the XRP Ledger: native
* XRP, IOU issued currencies, and Multi-Purpose Tokens (MPT). `Issue` covers
* both XRP and IOU (distinguished by `Issue::native()`), so the variant has
* two arms but three logical asset kinds.
*
* Conversions *to* `Asset` are implicit (from `Issue`, `MPTIssue`, or `MPTID`)
* to preserve backward compatibility with legacy `Issue`-taking APIs.
* Conversions *out* are explicit via `get<TIss>()` or guarded with `holds<TIss>()`.
*/
#pragma once
#include <xrpl/basics/Number.h>
@@ -11,6 +26,19 @@ namespace xrpl {
class STAmount;
/**
* @brief Empty tag type encoding an amount's numeric kind as a template
* parameter.
*
* Carries no data; its sole purpose is to convey compile-time type information
* through a runtime `std::variant`. Code that needs to dispatch on the numeric
* kind of an `Asset` calls `Asset::getAmountType()`, which returns a
* `std::variant<AmountType<XRPAmount>, AmountType<IOUAmount>,
* AmountType<MPTAmount>>`, and then `std::visit`s over it to select the
* correct templated path.
*
* @tparam T Must be one of `XRPAmount`, `IOUAmount`, or `MPTAmount`.
*/
template <typename T>
requires(
std::is_same_v<T, XRPAmount> || std::is_same_v<T, IOUAmount> ||
@@ -20,13 +48,26 @@ struct AmountType
using amount_type = T;
};
/* Used to check for an asset with either badCurrency()
* or MPT with 0 account.
/**
* @brief Sentinel tag used to test whether an `Asset` holds an invalid value.
*
* An `Asset` is "bad" when it holds an `Issue` whose currency equals
* `badCurrency()`, or an `MPTIssue` whose issuer equals `xrpAccount()` (the
* zero-account sentinel). Use `operator==(BadAsset const&, Asset const&)` or
* compare against `badAsset()` rather than inspecting the sub-type directly.
*
* This pattern avoids a separate validity flag or `std::optional<Asset>`:
* invalid states are represented as well-known sentinel values.
*/
struct BadAsset
{
};
/**
* @brief Returns a reference to the singleton `BadAsset` sentinel.
*
* Prefer `badAsset() == myAsset` over constructing a temporary `BadAsset{}`.
*/
inline BadAsset const&
badAsset()
{
@@ -34,16 +75,30 @@ badAsset()
return kA;
}
/* Asset is an abstraction of three different issue types: XRP, IOU, MPT.
* For historical reasons, two issue types XRP and IOU are wrapped in Issue
* type. Many functions and classes there were first written for Issue
* have been rewritten for Asset.
/**
* @brief Unified representation of an XRP Ledger asset: XRP, IOU, or MPT.
*
* Wraps `std::variant<Issue, MPTIssue>`. Because `Issue` already encodes both
* XRP (via `Issue::native()`) and IOU, the variant has two arms but three
* logical asset kinds. Value semantics and `constexpr` comparisons are
* preserved — no vtables, no heap allocation.
*
* Implicit conversions *from* `Issue`, `MPTIssue`, and `MPTID` allow callers
* to pass those types anywhere an `Asset` is expected. Extraction of the
* concrete sub-type is explicit: guard with `holds<TIss>()` then call
* `get<TIss>()`, or use `visit()` for exhaustive dispatch.
*
* `STAmount` stores an `Asset` as its type-identity half and delegates
* `native()`, `integral()`, `holds<>()`, and `get<>()` directly to it.
*/
class Asset
{
public:
/** Underlying storage type: one of `Issue` (XRP or IOU) or `MPTIssue`. */
using value_type = std::variant<Issue, MPTIssue>;
/** Currency or MPTID, depending on the active arm. */
using token_type = std::variant<Currency, MPTID>;
/** Runtime amount-kind discriminant returned by `getAmountType()`. */
using AmtType =
std::variant<AmountType<XRPAmount>, AmountType<IOUAmount>, AmountType<MPTAmount>>;
@@ -51,66 +106,176 @@ private:
value_type issue_;
public:
/** Constructs a default (XRP) asset. */
Asset() = default;
/** Conversions to Asset are implicit and conversions to specific issue
* type are explicit. This design facilitates the use of Asset.
/**
* @brief Constructs an Asset from an `Issue` (XRP or IOU).
*
* Implicit to preserve backward compatibility with APIs that previously
* accepted `Issue` directly.
*
* @param issue The XRP or IOU issue to wrap.
*/
Asset(Issue const& issue) : issue_(issue)
{
}
/**
* @brief Constructs an Asset from an `MPTIssue`.
*
* Implicit so callers can pass an `MPTIssue` wherever `Asset` is expected.
*
* @param mptIssue The MPT issuance to wrap.
*/
Asset(MPTIssue const& mptIssue) : issue_(mptIssue)
{
}
/**
* @brief Constructs an Asset from a raw `MPTID`.
*
* Convenience implicit conversion that wraps the issuance ID in an
* `MPTIssue` before storing it.
*
* @param issuanceID The 192-bit MPT issuance identifier.
*/
Asset(MPTID const& issuanceID) : issue_(MPTIssue{issuanceID})
{
}
/**
* @brief Returns the issuer of this asset.
*
* For XRP, returns the zero `AccountID` (no real issuer). For IOU, returns
* the issuing account. For MPT, returns the sequence-owner encoded in the
* MPTID.
*/
[[nodiscard]] AccountID const&
getIssuer() const;
/**
* @brief Returns a const reference to the active sub-type.
*
* @tparam TIss `Issue` or `MPTIssue`.
* @throws std::logic_error if the asset does not hold `TIss`. Guard with
* `holds<TIss>()` before calling, or use `visit()` for exhaustive
* dispatch.
*/
template <ValidIssueType TIss>
constexpr TIss const&
get() const;
/**
* @brief Returns a mutable reference to the active sub-type.
*
* @tparam TIss `Issue` or `MPTIssue`.
* @throws std::logic_error if the asset does not hold `TIss`.
*/
template <ValidIssueType TIss>
TIss&
get();
/**
* @brief Tests whether the asset currently holds the given sub-type.
*
* @tparam TIss `Issue` or `MPTIssue`.
* @return `true` if the active arm matches `TIss`.
*/
template <ValidIssueType TIss>
[[nodiscard]] constexpr bool
holds() const;
/**
* @brief Returns a human-readable string identifying the asset.
*
* Delegates to the underlying `Issue` or `MPTIssue` text representation.
*/
[[nodiscard]] std::string
getText() const;
/**
* @brief Returns a const reference to the underlying `variant` storage.
*
* Prefer `visit()` or `get<TIss>()` for type-safe access; this accessor
* is available for callers that must interact with the variant directly.
*/
[[nodiscard]] constexpr value_type const&
value() const;
/**
* @brief Returns the currency token identity of this asset.
*
* For XRP and IOU assets, returns the `Currency`. For MPT assets, returns
* the `MPTID`. Useful when identity must be compared independently of the
* issuer.
*/
[[nodiscard]] constexpr token_type
token() const;
/**
* @brief Serializes the asset into a JSON value.
*
* For IOU: emits `currency` and `issuer` keys (no issuer for XRP).
* For MPT: emits `mpt_issuance_id`.
*
* @param jv Output JSON object; populated in place.
*/
void
setJson(json::Value& jv) const;
/**
* @brief Constructs an `STAmount` from this asset and a raw numeric value.
*
* Convenience operator enabling concise amount construction:
* `myAsset(someNumber)`. The `Number` is interpreted according to the
* asset's kind (XRP drops, IOU mantissa/exponent, MPT integer).
*
* @param n The numeric value to associate with this asset.
* @return An `STAmount` holding this asset and the given value.
*/
STAmount
operator()(Number const&) const;
/**
* @brief Returns a tag-variant encoding the runtime amount kind.
*
* The returned variant holds one of `AmountType<XRPAmount>`,
* `AmountType<IOUAmount>`, or `AmountType<MPTAmount>`. `std::visit` over
* this result to select the correct templated arithmetic path without
* inspecting the asset sub-type manually.
*/
[[nodiscard]] constexpr AmtType
getAmountType() const;
// Custom, generic visit implementation
/**
* @brief Applies a set of lambdas to the active `Issue` or `MPTIssue` arm.
*
* Combines the provided callables into a single overload set using
* `detail::visit` (the `CombineVisitors` trick from `Concepts.h`) and
* forwards to `std::visit` over the internal variant. Example:
* @code
* asset.visit(
* [](Issue const& issue) { / * XRP or IOU * / },
* [](MPTIssue const& mpt) { / * MPT * / });
* @endcode
*
* @tparam Visitors Callable types whose signatures cover `Issue` and `MPTIssue`.
* @return The return value of the matching visitor.
*/
template <typename... Visitors>
constexpr auto
visit(Visitors&&... visitors) const -> decltype(auto)
{
// Simple delegation to the reusable utility, passing the internal
// variant data.
return detail::visit(issue_, std::forward<Visitors>(visitors)...);
}
/**
* @brief Returns `true` if and only if the asset is native XRP.
*
* MPT always returns `false`; IOU always returns `false`; only the XRP
* arm of `Issue` returns `true`.
*/
[[nodiscard]] constexpr bool
native() const
{
@@ -119,6 +284,14 @@ public:
[&](MPTIssue const&) { return false; });
}
/**
* @brief Returns `true` if the asset has an integer (non-fractional) amount
* representation.
*
* Both XRP (drops) and MPT amounts are always whole numbers. IOU amounts
* use a floating-point mantissa/exponent encoding and are not integral.
* This distinction affects serialization and arithmetic rounding.
*/
[[nodiscard]] bool
integral() const
{
@@ -127,32 +300,83 @@ public:
[&](MPTIssue const&) { return true; });
}
/**
* @brief Equality: `true` when both assets hold the same sub-type and
* compare equal within that sub-type.
*
* Cross-type comparisons (e.g., `Issue` vs `MPTIssue`) always return
* `false`. For IOU, both currency and issuer must match. Use
* `equalTokens()` to compare ignoring issuer.
*/
friend constexpr bool
operator==(Asset const& lhs, Asset const& rhs);
/**
* @brief Total order over assets for use in sorted containers.
*
* When both assets hold the same variant arm, ordering is delegated to
* that arm's natural `<=>`. When arms differ, `Issue` sorts greater than
* `MPTIssue` (an arbitrary but stable convention).
*/
friend constexpr std::weak_ordering
operator<=>(Asset const& lhs, Asset const& rhs);
/**
* @brief Tests whether the asset holds an `Issue` with the given currency.
*
* Returns `false` for any `MPTIssue` asset regardless of `lhs`.
*
* @param lhs The currency to compare against.
* @param rhs The asset to inspect.
*/
friend constexpr bool
operator==(Currency const& lhs, Asset const& rhs);
// rhs is either badCurrency() or MPT issuer is 0
/**
* @brief Tests whether the asset represents an invalid (sentinel) value.
*
* Returns `true` when `rhs` holds an `Issue` with `badCurrency()`, or an
* `MPTIssue` whose issuer is `xrpAccount()` (the zero-account sentinel).
*
* @param lhs Unused sentinel tag; use `badAsset()` as the left operand.
* @param rhs The asset to test.
*/
friend constexpr bool
operator==(BadAsset const& lhs, Asset const& rhs);
/** Return true if both assets refer to the same currency (regardless of
* issuer) or MPT issuance. Otherwise return false.
/**
* @brief Returns `true` if both assets refer to the same token type,
* regardless of issuer.
*
* For `Issue`-vs-`Issue` comparisons only the `Currency` field is checked;
* issuers are ignored. For `MPTIssue`-vs-`MPTIssue` the full `MPTID` is
* compared (issuer is already encoded in the ID, so there is no
* issuer-free concept). Cross-type comparisons always return `false`.
*
* Used in path-finding and offer-matching where token type must match but
* trust lines from different issuers in the same currency are acceptable.
*/
friend constexpr bool
equalTokens(Asset const& lhs, Asset const& rhs);
};
/** @brief `true` when `TIss` is `Issue`. Helper for `operator<=>`. */
template <ValidIssueType TIss>
constexpr bool kIS_ISSUE_V = std::is_same_v<TIss, Issue>;
/** @brief `true` when `TIss` is `MPTIssue`. Helper for `operator<=>`. */
template <ValidIssueType TIss>
constexpr bool kIS_MPTISSUE_V = std::is_same_v<TIss, MPTIssue>;
/**
* @brief Converts an asset to a `json::Value` representation.
*
* For IOU: produces `{currency, issuer}` (no issuer key for XRP).
* For MPT: produces `{mpt_issuance_id}`.
*
* @param asset The asset to serialize.
* @return A `json::Value` object describing the asset.
*/
inline json::Value
toJson(Asset const& asset)
{
@@ -293,21 +517,65 @@ equalTokens(Asset const& lhs, Asset const& rhs)
rhs.issue_);
}
/**
* @brief Returns `true` if the asset is native XRP.
*
* Thin wrapper around `Asset::native()` for readability at call sites.
*
* @param asset The asset to test.
*/
inline bool
isXRP(Asset const& asset)
{
return asset.native();
}
/**
* @brief Returns a human-readable string representation of an asset.
*
* Delegates to the underlying `Issue` or `MPTIssue` text form. Suitable for
* logging and error messages; not for wire serialization.
*
* @param asset The asset to stringify.
* @return A descriptive string identifying the asset.
*/
std::string
to_string(Asset const& asset);
/**
* @brief Validates that a JSON object encodes a well-formed asset.
*
* Enforces the protocol rule that an asset JSON object must contain exactly
* one of `currency` or `mpt_issuance_id`, but not both.
*
* @param jv The JSON value to validate.
* @return `true` if the JSON represents a valid asset; `false` otherwise.
*/
bool
validJSONAsset(json::Value const& jv);
/**
* @brief Parses an `Asset` from a JSON value.
*
* Accepts either `{currency[, issuer]}` for XRP/IOU or
* `{mpt_issuance_id}` for MPT. Throws on malformed input.
*
* @param jv The JSON object describing the asset.
* @return The parsed `Asset`.
* @throws std::runtime_error (or equivalent) if `jv` is not a valid asset.
*/
Asset
assetFromJson(json::Value const& jv);
/**
* @brief Returns `true` if the asset's internal fields are mutually consistent.
*
* For XRP/IOU assets, delegates to `Issue::isConsistent()` which checks that
* XRP has no account component. MPT assets are always considered consistent.
* Less strict than `validAsset()` — does not reject sentinel currencies.
*
* @param asset The asset to check.
*/
inline bool
isConsistent(Asset const& asset)
{
@@ -316,6 +584,15 @@ isConsistent(Asset const& asset)
[](MPTIssue const&) { return true; });
}
/**
* @brief Returns `true` if the asset is a well-formed, non-sentinel value.
*
* Stricter than `isConsistent()`: additionally rejects `badCurrency()` for
* IOU/XRP assets and the zero-issuer sentinel for MPT assets. Use this to
* validate user-provided or deserialized assets before operating on them.
*
* @param asset The asset to validate.
*/
inline bool
validAsset(Asset const& asset)
{
@@ -324,6 +601,17 @@ validAsset(Asset const& asset)
[](MPTIssue const& issue) { return issue.getIssuer() != xrpAccount(); });
}
/**
* @brief Appends an asset's hash contribution to a Hasher.
*
* Enables `Asset` as a key in `beast::uhash`-based and `std::unordered_*`
* containers. Dispatches to the active arm's own `hash_append` specialization,
* so `Issue` and `MPTIssue` assets produce distinct hash domains.
*
* @tparam Hasher A `beast::hash_append`-compatible hasher type.
* @param h The hasher to accumulate into.
* @param r The asset to hash.
*/
template <class Hasher>
void
hash_append(Hasher& h, Asset const& r)
@@ -334,6 +622,15 @@ hash_append(Hasher& h, Asset const& r)
[&](MPTIssue const& issue) { hash_append(h, issue); });
}
/**
* @brief Stream-inserts a human-readable asset description.
*
* Equivalent to `os << to_string(x)`. Intended for logging and diagnostics.
*
* @param os The output stream.
* @param x The asset to write.
* @return `os`, for chaining.
*/
std::ostream&
operator<<(std::ostream& os, Asset const& x);

View File

@@ -1,9 +1,50 @@
/** @file
* Defines the canonical wire-format serialization for batch signing payloads.
*
* A batch payload is the exact byte sequence that every co-signer of a
* `ttBATCH` transaction signs and that validators verify. The format is
* protocol-stable: any reordering of the four serialized fields would
* invalidate all previously issued batch signatures.
*/
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/STVector256.h>
#include <xrpl/protocol/Serializer.h>
namespace xrpl {
/** Serialize the signable payload for a batch transaction.
*
* Appends four fields to `msg` in a fixed order:
* 1. `HashPrefix::batch` — 4-byte domain separator that places batch hashes
* in their own hash-space, preventing cross-type signature collisions.
* 2. `flags` — the outer batch transaction's execution-policy flags (e.g.
* `tfAllOrNothing`). Signing over the flags ensures a signer cannot have
* the execution policy changed after they have committed.
* 3. `txids.size()` — the inner-transaction count as a `uint32_t`. Explicit
* serialization of the count prevents an adversary from extending or
* truncating the list without invalidating signatures.
* 4. Each `uint256` in `txids` — the hash of each inner transaction, in
* order. Signers commit to the exact set of inner transactions by ID.
*
* Both `checkBatchSingleSign()` and `checkBatchMultiSign()` in `STTx.cpp`
* call this function to build the verification payload, and test signing
* helpers do the same, so signing and verification share a single
* serialization path. For multi-sign, `serializeBatch()` is called once
* and `finishMultiSigningData()` appends each per-signer account ID suffix
* without re-serializing the inner transaction list.
*
* @param msg Serializer that receives the batch payload bytes. The caller
* is responsible for passing the resulting `msg.slice()` to the
* appropriate signature primitive.
* @param flags The `uint32_t` flags field of the outer batch transaction,
* as returned by `STTx::getFlags()`.
* @param txids Ordered list of inner-transaction IDs, as returned by
* `STTx::getBatchTransactionIDs()`.
*
* @note `HashPrefix::batch` is a protocol constant. Changing it would
* invalidate all existing batch signatures and requires an amendment.
*/
inline void
serializeBatch(Serializer& msg, std::uint32_t const& flags, std::vector<uint256> const& txids)
{

View File

@@ -1,3 +1,9 @@
/** @file
* Defines the `Book` type — the identity of an XRPL DEX order book — together
* with `std::hash` and `boost::hash` specializations for `Issue`, `MPTIssue`,
* `Asset`, and `Book` needed by unordered containers throughout the codebase.
*/
#pragma once
#include <xrpl/basics/CountedObject.h>
@@ -8,34 +14,110 @@
namespace xrpl {
/** Specifies an order book.
The order book is a pair of Issues called in and out.
@see Issue.
*/
/** Identity of an XRPL order book: a directed pair of assets.
*
* An order book is the set of all open offers to exchange one asset for
* another in a specific direction. `in` is the asset a taker spends;
* `out` is the asset a taker receives. Because `Asset` is a variant of
* `Issue` and `MPTIssue`, a `Book` can represent any combination of XRP,
* IOU, and MPT asset classes.
*
* When `domain` is set, the book is scoped to a `PermissionedDomain`
* ledger entry identified by that `uint256` index. A domain-scoped book
* and the corresponding open book are distinct even when their `in`/`out`
* assets are identical — equality, ordering, and hashing all include
* `domain`.
*
* Inherits `CountedObject<Book>` for diagnostic instance counting only;
* this has no effect on protocol logic.
*
* @invariant A well-formed book satisfies `isConsistent(*this)`:
* both legs are individually consistent and `in != out`.
* @see isConsistent, reversed
*/
class Book final : public CountedObject<Book>
{
public:
/** The asset being spent by the taker (offered). */
Asset in;
/** The asset being received by the taker (wanted). */
Asset out;
/** Optional permissioned-domain scope for this order book.
*
* When present, the `uint256` is the ledger index of a
* `PermissionedDomain` object that gates participation. Absent means
* the book is open to all accounts.
*/
std::optional<uint256> domain;
Book() = default;
/** Construct a Book from explicit asset legs and an optional domain.
*
* @param in Asset being spent by the taker.
* @param out Asset being received by the taker.
* @param domain Ledger index of the `PermissionedDomain` that scopes
* this book, or `std::nullopt` for an open book.
*/
Book(Asset const& in, Asset const& out, std::optional<uint256> const& domain)
: in(in), out(out), domain(domain)
{
}
};
/** Check that a Book is self-consistent.
*
* A book is consistent when both `in` and `out` are individually consistent
* (e.g., no XRP currency paired with a non-XRP issuer) and `in != out`.
* A book with identical legs would represent trading an asset against itself
* and would cause infinite-loop offer matching.
*
* @note `book.domain` is not validated here; semantic validity of the domain
* identifier belongs to higher-level transaction processing.
* @param book The order book to validate.
* @return `true` if both legs are individually consistent and `in != out`.
*/
bool
isConsistent(Book const& book);
/** Format a Book as a human-readable string for logging and diagnostics.
*
* Produces a string of the form `"<in>-><out>"` using the `to_string`
* representations of each asset leg. The arrow makes directionality
* explicit. This format is not part of the wire protocol.
*
* @param book The order book to convert.
* @return A string of the form `"<in>-><out>"`.
*/
std::string
to_string(Book const& book);
/** Write a Book to an output stream in diagnostic form.
*
* Delegates to `to_string(book)`.
*
* @param os The output stream to write to.
* @param x The order book to write.
* @return `os`, to allow chaining.
*/
std::ostream&
operator<<(std::ostream& os, Book const& x);
/** Append a Book to a cryptographic hash state (beast hash pipeline).
*
* Hashes `in` and `out` unconditionally, then appends `domain` only when
* present. A book with a domain and one without — even with identical
* asset legs — therefore produce different digests. This matters for
* ledger index derivation in `Indexes.cpp`, where the presence of a domain
* conditionally changes the hash inputs used to locate the book's offer
* directory.
*
* @tparam Hasher A beast-compatible hash accumulator.
* @param h The hash state to append to.
* @param b The order book whose fields are appended.
*/
template <class Hasher>
void
hash_append(Hasher& h, Book const& b)
@@ -46,11 +128,26 @@ hash_append(Hasher& h, Book const& b)
hash_append(h, *(b.domain));
}
/** Return the mirror-image order book with `in` and `out` swapped.
*
* Preserves `domain` unchanged — a domain-scoped market is the same market
* when viewed from either direction. Used by the Subscribe/Unsubscribe RPC
* handlers when a client requests the `both` flag so that updates from both
* the bid and ask sides are delivered.
*
* @param book The order book to reverse.
* @return A new `Book` with `in` and `out` exchanged and `domain` unchanged.
*/
Book
reversed(Book const& book);
/** Equality comparison. */
/** @{ */
/** Test two Books for equality.
*
* Two books are equal only when `in`, `out`, and `domain` all compare equal.
* A domain-scoped book is never equal to an open book with the same asset
* legs.
*/
[[nodiscard]] constexpr bool
operator==(Book const& lhs, Book const& rhs)
{
@@ -58,8 +155,19 @@ operator==(Book const& lhs, Book const& rhs)
}
/** @} */
/** Strict weak ordering. */
/** @{ */
/** Three-way comparison establishing a strict weak ordering over Books.
*
* Orders first by `in`, then by `out`, then by `domain`. An absent domain
* compares less than any present domain. This ordering is used by sorted
* containers such as subscription routing tables and by `BookDirs` traversal
* to iterate over offer directories deterministically.
*
* @note The optional comparison is performed manually (rather than relying
* on the standard library's spaceship support for `optional`) to
* guarantee a `std::weak_ordering` return type consistent with the
* `Asset` spaceship result.
*/
[[nodiscard]] constexpr std::weak_ordering
operator<=>(Book const& lhs, Book const& rhs)
{
@@ -68,15 +176,15 @@ operator<=>(Book const& lhs, Book const& rhs)
if (auto const c{lhs.out <=> rhs.out}; c != 0)
return c;
// Manually compare optionals
// Manually compare optionals: absent domain sorts before any present domain.
if (lhs.domain && rhs.domain)
return *lhs.domain <=> *rhs.domain; // Compare values if both exist
return *lhs.domain <=> *rhs.domain;
if (!lhs.domain && rhs.domain)
return std::weak_ordering::less; // Empty is considered less
return std::weak_ordering::less;
if (lhs.domain && !rhs.domain)
return std::weak_ordering::greater; // Non-empty is greater
return std::weak_ordering::greater;
return std::weak_ordering::equivalent; // Both are empty
return std::weak_ordering::equivalent;
}
/** @} */
@@ -86,6 +194,13 @@ operator<=>(Book const& lhs, Book const& rhs)
namespace std {
/** `std::hash` specialization for `xrpl::Issue`.
*
* Combines the currency hash with the account (issuer) hash via
* `boost::hash_combine`. For XRP, the issuer field is ignored because
* all XRP issuers are equivalent by protocol definition, ensuring that
* all representations of XRP hash identically.
*/
template <>
struct hash<xrpl::Issue> : private boost::base_from_member<std::hash<xrpl::Currency>, 0>,
private boost::base_from_member<std::hash<xrpl::AccountID>, 1>
@@ -110,6 +225,12 @@ public:
}
};
/** `std::hash` specialization for `xrpl::MPTIssue`.
*
* Hashes only the 192-bit `MPTID` (32-bit sequence number concatenated with
* the 160-bit issuer account), which is the canonical unique identifier for
* an MPT issuance.
*/
template <>
struct hash<xrpl::MPTIssue> : private boost::base_from_member<std::hash<xrpl::MPTID>, 0>
{
@@ -130,6 +251,11 @@ public:
}
};
/** `std::hash` specialization for `xrpl::Asset`.
*
* Visits the underlying variant and dispatches to the appropriate
* `std::hash<Issue>` or `std::hash<MPTIssue>` specialization.
*/
template <>
struct hash<xrpl::Asset>
{
@@ -163,6 +289,14 @@ public:
//------------------------------------------------------------------------------
/** `std::hash` specialization for `xrpl::Book`.
*
* Seeds from `hash(in)`, combines `hash(out)` via `boost::hash_combine`,
* then conditionally combines `hash(domain)` when a domain is present.
* A domain-scoped book and an otherwise-identical open book therefore
* produce distinct hash values, which is required for correct keying in
* subscription routing tables and offer-directory indexes.
*/
template <>
struct hash<xrpl::Book>
{
@@ -198,6 +332,16 @@ public:
namespace boost {
/** `boost::hash` adapter for `xrpl::Issue`.
*
* Inherits `std::hash<xrpl::Issue>` so that Boost.Unordered and
* Boost.MultiIndex containers resolve the same hash function as standard
* unordered containers, avoiding divergence between the two hash registries.
*
* @note Constructor inheritance (`using Base::Base`) is omitted because it
* was broken in Visual Studio 2012; an explicit defaulted constructor
* is provided instead.
*/
template <>
struct hash<xrpl::Issue> : std::hash<xrpl::Issue>
{
@@ -208,6 +352,11 @@ struct hash<xrpl::Issue> : std::hash<xrpl::Issue>
// using Base::Base; // inherit ctors
};
/** `boost::hash` adapter for `xrpl::MPTIssue`.
*
* Delegates to `std::hash<xrpl::MPTIssue>` so that Boost containers use
* the same hash logic as standard containers.
*/
template <>
struct hash<xrpl::MPTIssue> : std::hash<xrpl::MPTIssue>
{
@@ -216,6 +365,11 @@ struct hash<xrpl::MPTIssue> : std::hash<xrpl::MPTIssue>
using Base = std::hash<xrpl::MPTIssue>;
};
/** `boost::hash` adapter for `xrpl::Asset`.
*
* Delegates to `std::hash<xrpl::Asset>` so that Boost containers use the
* same variant-dispatching hash logic as standard containers.
*/
template <>
struct hash<xrpl::Asset> : std::hash<xrpl::Asset>
{
@@ -224,6 +378,15 @@ struct hash<xrpl::Asset> : std::hash<xrpl::Asset>
using Base = std::hash<xrpl::Asset>;
};
/** `boost::hash` adapter for `xrpl::Book`.
*
* Delegates to `std::hash<xrpl::Book>` so that Boost containers use the
* same domain-aware hash logic as standard containers.
*
* @note Constructor inheritance (`using Base::Base`) is omitted because it
* was broken in Visual Studio 2012; an explicit defaulted constructor
* is provided instead.
*/
template <>
struct hash<xrpl::Book> : std::hash<xrpl::Book>
{

View File

@@ -1,76 +1,135 @@
/** @file
* Version identity and wire-encoding utilities for the xrpld binary.
*
* Owns the canonical SemVer version string, the composite `systemName-version`
* identifier used in HTTP headers and peer-protocol handshakes, and a compact
* 64-bit encoding that lets validators compare software versions during
* consensus via a plain integer comparison — no string parsing required at
* runtime.
*
* At every flag ledger (every 256 ledgers) `RCLConsensus` writes the result of
* `getEncodedVersion()` into `sfServerVersion` in each validation message it
* broadcasts. `LedgerMaster` then inspects incoming validations, calling
* `isXrpldVersion()` and `isNewerVersion()` to count how many UNL validators
* are running a newer build, which can surface an upgrade notification.
*/
#pragma once
#include <cstdint>
#include <string>
/** Versioning information for this build. */
/** Version identity, wire encoding, and peer-comparison utilities for xrpld.
*
* @deprecated The `BuildInfo` sub-namespace is expected to be dissolved; these
* utilities will eventually be promoted directly into `xrpl`.
*/
// VFALCO The namespace is deprecated
namespace xrpl::BuildInfo {
/** Server version.
Follows the Semantic Versioning Specification:
http://semver.org/
*/
/** Return the canonical SemVer version string for this build.
*
* The result is memoized; the initializer runs exactly once (C++11
* thread-safe static-init guarantee). On first call the hard-coded
* `versionString` constant is round-tripped through `beast::SemanticVersion`:
* if it fails to parse, or if its canonical re-serialization differs from the
* original, `LogicError` is thrown and the process terminates. This acts as a
* start-up invariant check — a malformed version constant is caught
* immediately rather than producing silently wrong encoded integers.
*
* In `DEBUG` or sanitizer builds, SemVer build metadata (commit hash,
* `DEBUG`, sanitizer names) is appended as a `+`-separated suffix, e.g.
* `"3.2.0-b0+abc1234.DEBUG"`.
*
* @return a reference to the cached, validated version string (e.g.
* `"3.2.0-b0"`).
* @throw LogicError on first call if `versionString` is malformed or
* not in canonical SemVer form.
*/
std::string const&
getVersionString();
/** Full server version string.
This includes the name of the server. It is used in the peer
protocol hello message and also the headers of some HTTP replies.
*/
/** Return the composite `systemName-version` string for this build.
*
* Prepends `systemName()` (always `"xrpld"`) to `getVersionString()`,
* separated by `"-"`, e.g. `"xrpld-3.2.0-b0"`. This string appears verbatim
* in the `User-Agent` and `Server` HTTP headers during peer-protocol
* handshakes and in all HTTP responses from the JSON-RPC server.
*
* @return a reference to the cached composite version string.
*/
std::string const&
getFullVersionString();
/** Encode an arbitrary server software version in a 64-bit integer.
The general format is:
........-........-........-........-........-........-........-........
XXXXXXXX-XXXXXXXX-YYYYYYYY-YYYYYYYY-YYYYYYYY-YYYYYYYY-YYYYYYYY-YYYYYYYY
X: 16 bits identifying the particular implementation
Y: 48 bits of data specific to the implementation
The xrpld-specific format (implementation ID is: 0x18 0x3B) is:
00011000-00111011-MMMMMMMM-mmmmmmmm-pppppppp-TTNNNNNN-00000000-00000000
M: 8-bit major version (0-255)
m: 8-bit minor version (0-255)
p: 8-bit patch version (0-255)
T: 11 if neither an RC nor a beta
10 if an RC
01 if a beta
N: 6-bit rc/beta number (1-63)
@param the version string
@return the encoded version in a 64-bit integer
*/
/** Encode a SemVer string into the 64-bit wire format used in `sfServerVersion`.
*
* The resulting integer has the following bit layout:
*
* ```
* [63:48] implementation identifier (0x183B for xrpld)
* [47:40] major version (8 bits, 0-255)
* [39:32] minor version (8 bits, 0-255)
* [31:24] patch version (8 bits, 0-255)
* [23:22] pre-release type (0b11 = release, 0b10 = RC, 0b01 = beta)
* [21:16] pre-release number (6 bits, 0-63; 0 for releases)
* [15:0] reserved zeros
* ```
*
* The pre-release type bits are deliberately ordered so that a plain integer
* comparison on the full `uint64_t` yields correct semantic ordering:
* release (`0b11`) > RC (`0b10`) > beta (`0b01`). A malformed pre-release
* identifier (missing number, non-numeric suffix, number out of range
* [0, 63]) silently yields zero for bits [23:16], which sorts below any
* recognizable pre-release type. If `versionStr` does not parse as valid
* SemVer at all, the return value contains only the xrpld fingerprint
* (`0x183B`) in bits [63:48] and zeros elsewhere.
*
* @param versionStr a SemVer-formatted version string (e.g. `"3.2.0-b0"`).
* @return the packed version as a `uint64_t`; see bit layout above.
*/
std::uint64_t
encodeSoftwareVersion(std::string_view versionStr);
/** Returns this server's version packed in a 64-bit integer. */
/** Return this node's own encoded version, cached from `getVersionString()`.
*
* Calls `encodeSoftwareVersion(getVersionString())` exactly once and caches
* the result as a function-local static. This value is written into
* `sfServerVersion` in every validation message emitted on flag ledgers by
* `RCLConsensus`.
*
* @return the cached 64-bit encoded version for this build.
*/
std::uint64_t
getEncodedVersion();
/** Check if the encoded software version is an xrpld software version.
@param version another node's encoded software version
@return true if the version is an xrpld software version, false otherwise
*/
/** Return true if `version` carries the xrpld implementation fingerprint.
*
* Checks only the upper 16 bits against the xrpld identifier `0x183B`. This
* must be called before any numeric comparison of version values: a non-xrpld
* peer could advertise an arbitrarily large integer that would otherwise
* appear "newer", so `isNewerVersion()` calls this guard unconditionally.
*
* @param version an encoded software version read from `sfServerVersion`.
* @return true iff the upper 16 bits of `version` equal `0x183B`.
*/
bool
isXrpldVersion(std::uint64_t version);
/** Check if the version is newer than the local node's xrpld software
version.
@param version another node's encoded software version
@return true if the version is newer than the local node's xrpld software
version, false otherwise.
@note This function only understands version numbers that are generated by
xrpld. Please see the encodeSoftwareVersion() function for detail.
*/
/** Return true if `version` represents a strictly newer xrpld release than
* this node.
*
* Guards against non-xrpld peers by calling `isXrpldVersion()` first: any
* value whose upper 16 bits differ from `0x183B` unconditionally returns
* false, regardless of its numeric magnitude. For confirmed xrpld versions,
* a plain integer comparison is sufficient because `encodeSoftwareVersion()`
* places major, minor, patch, and release-type bits in descending order of
* significance.
*
* @param version an encoded software version read from `sfServerVersion`.
* @return true iff `version` is an xrpld version strictly greater than
* `getEncodedVersion()`.
* @see isXrpldVersion(), encodeSoftwareVersion()
*/
bool
isNewerVersion(std::uint64_t version);

View File

@@ -1,3 +1,12 @@
/** @file
* Compile-time type vocabulary for the XRPL protocol layer.
*
* Centralises all C++20 concept definitions that constrain the three payment
* asset families (XRP, IOU, and MPT) and provides the `detail::CombineVisitors`
* utility used by `Asset::visit()` and `PathAsset::visit()`. Keeping every
* constraint in one place means that adding a new asset family requires
* updates in a single file; the compiler propagates errors to every call site.
*/
#pragma once
#include <xrpl/protocol/UintTypes.h>
@@ -14,20 +23,73 @@ class IOUAmount;
class XRPAmount;
class MPTAmount;
/** Constrains the three numeric types used as individual payment-step quantities.
*
* `EitherAmount` (the type-erased amount carrier used by the path-finding
* engine) restricts its constructor, `holds<T>()`, and `get<T>()` with this
* concept, so the compiler rejects any attempt to store or query an amount
* type outside the sanctioned set at instantiation time.
*
* @tparam A One of `XRPAmount`, `IOUAmount`, or `MPTAmount`.
*/
template <typename A>
concept StepAmount =
std::is_same_v<A, XRPAmount> || std::is_same_v<A, IOUAmount> || std::is_same_v<A, MPTAmount>;
/** Constrains template parameters to the two issue types held by `Asset`.
*
* Gates `Asset::get<T>()` and `Asset::holds<T>()` to exactly `Issue` and
* `MPTIssue` — the two alternatives of `Asset`'s internal
* `std::variant<Issue, MPTIssue>`. Also used by the `kIS_ISSUE_V` and
* `kIS_MPTISSUE_V` boolean constants in `Asset.h` that drive `if constexpr`
* branches in comparison operators.
*
* @tparam TIss `Issue` or `MPTIssue`.
*/
template <typename TIss>
concept ValidIssueType = std::is_same_v<TIss, Issue> || std::is_same_v<TIss, MPTIssue>;
/** Constrains template parameters to any type convertible to a known asset representation.
*
* Broader than `ValidIssueType`: uses `is_convertible_v` rather than
* `is_same_v`, so it accepts any type with an implicit conversion path to
* `Asset`, `Issue`, `MPTIssue`, or `MPTID`. This enables generic code that
* accepts any "asset-like" value without requiring callers to normalise to a
* canonical form first.
*
* @tparam A Any type implicitly convertible to `Asset`, `Issue`, `MPTIssue`, or `MPTID`.
*/
template <typename A>
concept AssetType = std::is_convertible_v<A, Asset> || std::is_convertible_v<A, Issue> ||
std::is_convertible_v<A, MPTIssue> || std::is_convertible_v<A, MPTID>;
/** Constrains template parameters to the two token-identity types used in payment paths.
*
* `PathAsset` carries only the currency/token specifier inside a payment path
* element — it explicitly does not carry issuer information. `Currency` covers
* both XRP (the zero currency) and IOU tokens; `MPTID` covers MPT issuances.
* This concept gates `PathAsset::get<T>()`, `PathAsset::holds<T>()`, and the
* `kIS_CURRENCY_V`/`kIS_MPTID_V` helper constants in `PathAsset.h`.
*
* @tparam T `Currency` or `MPTID`.
*/
template <typename T>
concept ValidPathAsset = (std::is_same_v<T, Currency> || std::is_same_v<T, MPTID>);
/** Constrains a pair of step-amount types to a legal DEX trading pair.
*
* Both sides must independently be one of `XRPAmount`, `IOUAmount`, or
* `MPTAmount`, but the XRP/XRP combination is structurally illegal on the
* XRPL order book — an offer cannot have both `TakerPays` and `TakerGets`
* denominated in XRP. `OfferStream::shouldRmSmallIncreasedQOffer()` uses this
* concept to encode that invariant at the type system level rather than as a
* runtime assertion.
*
* @tparam TTakerPays The amount type for what the taker pays; must satisfy `StepAmount`.
* @tparam TTakerGets The amount type for what the taker receives; must satisfy `StepAmount`.
* @note The constraint is equivalent to: both sides are valid step-amount types
* AND NOT (TTakerPays == XRPAmount AND TTakerGets == XRPAmount).
*/
template <class TTakerPays, class TTakerGets>
concept ValidTaker =
((std::is_same_v<TTakerPays, IOUAmount> || std::is_same_v<TTakerPays, XRPAmount> ||
@@ -38,46 +100,70 @@ concept ValidTaker =
namespace detail {
// This template combines multiple callable objects (lambdas) into a single
// object that std::visit can use for overload resolution.
/** Combines multiple callable objects (lambdas) into a single overload set for `std::visit`.
*
* Implements the classical *overloaded* pattern: by inheriting from every
* lambda type and pulling each `operator()` into the derived scope, this
* struct becomes a single callable that overload-resolution can dispatch
* correctly based on the active variant alternative at runtime.
*
* Prefer constructing instances via `makeCombineVisitors()` rather than
* direct construction; the factory applies `std::decay_t` and uses function
* template argument deduction, which is more portable than CTAD for variadic
* class templates.
*
* @tparam Ts Lambda (or other callable) types to merge into one overload set.
* @see makeCombineVisitors
*/
template <typename... Ts>
struct CombineVisitors : Ts...
{
// Bring all operator() overloads from base classes into this scope.
// It's the mechanism that makes the CombineVisitors struct function
// as a single callable object with multiple overloads.
using Ts::operator()...;
// Perfect forwarding constructor to correctly initialize the base class
// lambdas
/** Initialises each base-class lambda by perfect-forwarding its argument. */
constexpr CombineVisitors(Ts&&... ts) : Ts(std::forward<Ts>(ts))...
{
}
};
// This function forces function template argument deduction, which is more
// robust than class template argument deduction (CTAD) via the deduction guide.
/** Creates a `CombineVisitors` from a pack of callables.
*
* Preferred over a CTAD deduction guide because function template argument
* deduction handles parameter packs more robustly than class-template
* argument deduction (CTAD) across compilers. `std::decay_t` strips
* references and cv-qualifiers from lambda types before they become base
* classes, ensuring the inherited `operator()` calls have the correct value
* categories.
*
* @tparam Ts Callable types; typically lambdas.
* @param ts Callables to combine.
* @return A `CombineVisitors<std::decay_t<Ts>...>` holding all overloads.
*/
template <typename... Ts>
constexpr CombineVisitors<std::decay_t<Ts>...>
makeCombineVisitors(Ts&&... ts)
{
// std::decay_t<Ts> is used to remove references/constness from the lambda
// types before they are passed as template arguments to the CombineVisitors
// struct.
return CombineVisitors<std::decay_t<Ts>...>{std::forward<Ts>(ts)...};
}
// This function takes ANY variant and ANY number of visitors, and performs the
// visit. It is the reusable core logic.
/** Visits a variant with a set of per-alternative callables.
*
* Combines `visitors...` into a single overload set via `makeCombineVisitors`
* and delegates to `std::visit`. This is the reusable core called by
* `Asset::visit()` and `PathAsset::visit()`; callers should go through those
* member functions rather than invoking this directly.
*
* @tparam Variant A `std::variant` specialisation.
* @tparam Visitors Callable types, one per variant alternative.
* @param v The variant to dispatch on.
* @param visitors Callables covering each alternative of `v`.
* @return The return value of the selected visitor.
*/
template <typename Variant, typename... Visitors>
constexpr auto
visit(Variant&& v, Visitors&&... visitors) -> decltype(auto)
{
// Use the function template helper instead of raw CTAD.
auto visitorSet = makeCombineVisitors(std::forward<Visitors>(visitors)...);
// Delegate to std::visit, perfectly forwarding the variant and the visitor
// set.
return std::visit(visitorSet, std::forward<Variant>(v));
}

View File

@@ -1,5 +1,16 @@
#pragma once
/** @file
* Single source of truth for every RPC error the XRPL node can emit.
*
* Defines the stable numeric code space (`ErrorCodeI`), a parallel warning
* code space (`WarningCodeI`), the `ErrorInfo` struct that binds each code
* to a token and HTTP status, and the complete vocabulary of JSON helpers
* used by RPC handlers to produce well-formed error responses. Every
* component that rejects an RPC call — from malformed-parameter checks to
* ledger-not-found conditions — funnels through this file.
*/
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/jss.h>
@@ -7,158 +18,174 @@ namespace xrpl {
// VFALCO NOTE These are outside the RPC namespace
// NOTE: Although the precise numeric values of these codes were never
// intended to be stable, several API endpoints include the numeric values.
// Some users came to rely on the values, meaning that renumbering would be
// a breaking change for those users.
//
// We therefore treat the range of values as stable although they are not
// and are subject to change.
//
// Please only append to this table. Do not "fill-in" gaps and do not re-use
// or repurpose error code values.
/** Numeric codes for every named RPC error the XRPL node can return.
*
* Values are used as machine-readable error identifiers in RPC responses
* (the `error_code` field). They were never formally promised to be stable,
* but real API consumers depend on them, so the range is now treated as
* **append-only**: new codes go at the end, gaps are never filled, and
* retired values are commented out rather than reassigned.
*
* Codes are grouped thematically (general failures, networking, ledger
* state, malformed commands, bad parameters, internal errors) to guide
* maintainers when choosing where a new code belongs.
*
* `RpcLast` must always equal the highest assigned code; the compile-time
* validation in `ErrorCodes.cpp` enforces this and will fail to compile if
* it is not updated when a new code is added.
*
* @note `RpcUnknown` (-1) is returned by `getErrorInfo()` for any code
* that falls outside the range `(RpcSuccess, RpcLast]`.
*/
// Protocol-wide, 50+ files
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum ErrorCodeI {
// -1 represents codes not listed in this enumeration
RpcUnknown = -1,
RpcUnknown = -1, /**< Sentinel for out-of-range or unrecognised codes. */
RpcSuccess = 0,
RpcSuccess = 0, /**< No error. */
RpcBadSyntax = 1,
RpcJsonRpc = 2,
RpcForbidden = 3,
// General failures
RpcBadSyntax = 1, /**< Request could not be parsed as valid JSON-RPC. */
RpcJsonRpc = 2, /**< JSON-RPC transport-level error. */
RpcForbidden = 3, /**< Credentials rejected. */
RpcWrongNetwork = 4,
RpcWrongNetwork = 4, /**< Request arrived on the wrong network. */
// Misc failure
// unused 5,
RpcNoPermission = 6,
RpcNoEvents = 7,
RpcNoPermission = 6, /**< Caller lacks permission for this command. */
RpcNoEvents = 7, /**< Transport does not support event subscriptions. */
// unused 8,
RpcTooBusy = 9,
RpcSlowDown = 10,
RpcHighFee = 11,
RpcNotEnabled = 12,
RpcNotReady = 13,
RpcAmendmentBlocked = 14,
RpcTooBusy = 9, /**< Server load too high to serve the request now. */
RpcSlowDown = 10, /**< Caller is sending requests too rapidly. */
RpcHighFee = 11, /**< Current fee exceeds the caller's stated limit. */
RpcNotEnabled = 12, /**< Feature not enabled in the server's configuration. */
RpcNotReady = 13, /**< Server is not yet ready to handle this request. */
RpcAmendmentBlocked = 14, /**< Node needs an upgrade; amendment-blocked. */
// Networking
RpcNoClosed = 15,
RpcNoCurrent = 16,
RpcNoNetwork = 17,
RpcNotSynced = 18,
RpcNoClosed = 15, /**< Closed ledger is unavailable. */
RpcNoCurrent = 16, /**< Current ledger is unavailable. */
RpcNoNetwork = 17, /**< Not synced to the network. */
RpcNotSynced = 18, /**< Not synced to the network. */
// Ledger state
RpcActNotFound = 19,
RpcActNotFound = 19, /**< Specified account does not exist in the ledger. */
// unused 20,
RpcLgrNotFound = 21,
RpcLgrNotValidated = 22,
RpcMasterDisabled = 23,
RpcLgrNotFound = 21, /**< Requested ledger does not exist. */
RpcLgrNotValidated = 22, /**< Requested ledger exists but has not yet been validated. */
RpcMasterDisabled = 23, /**< Master key is disabled on this account. */
// unused 24,
// unused 25,
// unused 26,
// unused 27,
// unused 28,
RpcTxnNotFound = 29,
RpcInvalidHotwallet = 30,
RpcTxnNotFound = 29, /**< Transaction not found. */
RpcInvalidHotwallet = 30, /**< Specified hotwallet address is invalid. */
// Malformed command
RpcInvalidParams = 31,
RpcUnknownCommand = 32,
RpcNoPfRequest = 33,
RpcInvalidParams = 31, /**< One or more request parameters are invalid. */
RpcUnknownCommand = 32, /**< The requested command is not recognised. */
RpcNoPfRequest = 33, /**< No pathfinding request is currently in progress. */
// Bad parameter
// NOT USED DO NOT USE AGAIN rpcACT_BITCOIN = 34,
RpcActMalformed = 35,
RpcAlreadyMultisig = 36,
RpcAlreadySingleSig = 37,
RpcActMalformed = 35, /**< Account address is malformed. */
RpcAlreadyMultisig = 36, /**< Account is already set up for multi-signing. */
RpcAlreadySingleSig = 37, /**< Account is already single-signed. */
// unused 38,
// unused 39,
RpcBadFeature = 40,
RpcBadIssuer = 41,
RpcBadMarket = 42,
RpcBadSecret = 43,
RpcBadSeed = 44,
RpcChannelMalformed = 45,
RpcChannelAmtMalformed = 46,
RpcCommandMissing = 47,
RpcDstActMalformed = 48,
RpcDstActMissing = 49,
RpcDstActNotFound = 50,
RpcDstAmtMalformed = 51,
RpcDstAmtMissing = 52,
RpcDstIsrMalformed = 53,
RpcBadFeature = 40, /**< Unknown or invalid amendment feature. */
RpcBadIssuer = 41, /**< Issuer account address is malformed. */
RpcBadMarket = 42, /**< Requested order-book does not exist. */
RpcBadSecret = 43, /**< Secret key does not match the specified account. */
RpcBadSeed = 44, /**< Seed value is disallowed. */
RpcChannelMalformed = 45, /**< Payment channel identifier is malformed. */
RpcChannelAmtMalformed = 46, /**< Payment channel amount is malformed. */
RpcCommandMissing = 47, /**< Request object is missing the command field. */
RpcDstActMalformed = 48, /**< Destination account address is malformed. */
RpcDstActMissing = 49, /**< Destination account was not provided. */
RpcDstActNotFound = 50, /**< Destination account does not exist in the ledger. */
RpcDstAmtMalformed = 51, /**< Destination amount, currency, or issuer is malformed. */
RpcDstAmtMissing = 52, /**< Destination amount, currency, or issuer was not provided. */
RpcDstIsrMalformed = 53, /**< Destination issuer is malformed. */
// unused 54,
// unused 55,
// unused 56,
RpcLgrIdxsInvalid = 57,
RpcLgrIdxMalformed = 58,
RpcLgrIdxsInvalid = 57, /**< Ledger index range is invalid. */
RpcLgrIdxMalformed = 58, /**< Individual ledger index is malformed. */
// unused 59,
// unused 60,
// unused 61,
RpcPublicMalformed = 62,
RpcSigningMalformed = 63,
RpcSendmaxMalformed = 64,
RpcSrcActMalformed = 65,
RpcSrcActMissing = 66,
RpcSrcActNotFound = 67,
RpcDelegateActNotFound = 68,
RpcSrcCurMalformed = 69,
RpcSrcIsrMalformed = 70,
RpcStreamMalformed = 71,
RpcAtxDeprecated = 72,
RpcPublicMalformed = 62, /**< Public key is malformed. */
RpcSigningMalformed = 63, /**< Transaction signing data is malformed. */
RpcSendmaxMalformed = 64, /**< SendMax amount is malformed. */
RpcSrcActMalformed = 65, /**< Source account address is malformed. */
RpcSrcActMissing = 66, /**< Source account was not provided. */
RpcSrcActNotFound = 67, /**< Source account does not exist in the ledger. */
RpcDelegateActNotFound = 68, /**< Delegate account does not exist in the ledger. */
RpcSrcCurMalformed = 69, /**< Source currency is malformed. */
RpcSrcIsrMalformed = 70, /**< Source issuer is malformed. */
RpcStreamMalformed = 71, /**< Subscription stream specification is malformed. */
RpcAtxDeprecated = 72, /**< Deprecated API endpoint; use the current API. */
// Internal error (should never happen)
RpcInternal = 73, // Generic internal error.
RpcNotImpl = 74,
RpcNotSupported = 75,
RpcBadKeyType = 76,
RpcDbDeserialization = 77,
RpcExcessiveLgrRange = 78,
RpcInvalidLgrRange = 79,
RpcExpiredValidatorList = 80,
RpcInternal = 73, /**< Generic internal server error. */
RpcNotImpl = 74, /**< Feature not yet implemented. */
RpcNotSupported = 75, /**< Operation not supported by this server. */
RpcBadKeyType = 76, /**< Key type is not supported. */
RpcDbDeserialization = 77, /**< Failed to deserialize an object from the database. */
RpcExcessiveLgrRange = 78, /**< Requested ledger range exceeds the 1000-ledger limit. */
RpcInvalidLgrRange = 79, /**< Requested ledger range bounds are logically invalid. */
RpcExpiredValidatorList = 80, /**< Validator list has expired; node needs an updated UNL. */
// unused = 90,
// DEPRECATED. New code must not use this value.
RpcReportingUnsupported = 91,
RpcReportingUnsupported = 91, /**< @deprecated Reporting-mode-only command sent to a non-reporting node. */
RpcObjectNotFound = 92,
RpcObjectNotFound = 92, /**< Requested ledger object was not found. */
// AMM
RpcIssueMalformed = 93,
RpcIssueMalformed = 93, /**< AMM asset issue specification is malformed. */
// Oracle
RpcOracleMalformed = 94,
RpcOracleMalformed = 94, /**< Oracle request is malformed. */
// deposit_authorized + credentials
RpcBadCredentials = 95,
RpcBadCredentials = 95, /**< Credentials do not exist, are not accepted, or have expired. */
// Simulate
RpcTxSigned = 96,
RpcTxSigned = 96, /**< Simulate rejected a pre-signed transaction. */
// Pathfinding
RpcDomainMalformed = 97,
RpcDomainMalformed = 97, /**< Domain field is malformed. */
// ledger_entry
RpcEntryNotFound = 98,
RpcUnexpectedLedgerType = 99,
RpcEntryNotFound = 98, /**< Requested ledger entry was not found. */
RpcUnexpectedLedgerType = 99, /**< Ledger entry type does not match the request. */
RpcLast = RpcUnexpectedLedgerType // rpcLAST should always equal the last code.
RpcLast = RpcUnexpectedLedgerType /**< Sentinel: always equal to the highest assigned code. */
};
/** Codes returned in the `warnings` array of certain RPC commands.
These values need to remain stable.
*/
/** Numeric codes returned in the `warnings` array of certain RPC responses.
*
* Warning codes appear alongside a successful result (not in the top-level
* `error` field) and inform the caller of advisory conditions such as
* imminent amendment blocking or a deprecated field being used.
*
* Values start at 1001 to be clearly distinct from `ErrorCodeI` values and
* must remain **stable** — external implementations such as Clio hardcode
* specific values (notably `WarnRpcFieldsDeprecated = 2004`).
*/
// Protocol-wide, 50+ files
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum WarningCodeI {
WarnRpcUnsupportedMajority = 1001,
WarnRpcAmendmentBlocked = 1002,
WarnRpcExpiredValidatorList = 1003,
WarnRpcUnsupportedMajority = 1001, /**< A non-default amendment has gained majority support. */
WarnRpcAmendmentBlocked = 1002, /**< Node is amendment-blocked and needs an upgrade. */
WarnRpcExpiredValidatorList = 1003, /**< Validator list has expired. */
// unused = 1004
WarnRpcFieldsDeprecated = 2004, // xrpld needs to maintain
// compatibility with Clio on this code.
WarnRpcFieldsDeprecated = 2004, /**< Request used one or more deprecated fields.
* @note Value must stay fixed at 2004; Clio hardcodes it. */
};
//------------------------------------------------------------------------------
@@ -167,144 +194,298 @@ enum WarningCodeI {
namespace RPC {
/** Maps an rpc error code to its token, default message, and HTTP status. */
/** Binds an `ErrorCodeI` to its human-readable token, default message, and HTTP status.
*
* Instances are stored in a compile-time-validated array in `ErrorCodes.cpp`
* and returned by reference from `getErrorInfo()`. `Json::StaticString` fields
* hold raw `const char*` pointers to string literals, avoiding heap allocation
* when the token is written into a JSON response.
*
* The `http_status` field drives load-balancer failover semantics: errors that
* indicate a node is temporarily unable to serve (e.g., amendment-blocked,
* too-busy) use 5xx/429 so a proxy can redirect to a healthy peer; client-fault
* errors use 4xx; everything else defaults to 200 for backward compatibility.
*/
struct ErrorInfo
{
// Default ctor needed to produce an empty std::array during constexpr eval.
/** Default-constructs an unknown-error entry.
*
* Required so that `std::array<ErrorInfo, N>` can be value-initialised
* during `constexpr` evaluation of the lookup table.
*/
constexpr ErrorInfo()
: code(RpcUnknown), token("unknown"), message("An unknown error code."), http_status(200)
{
}
/** Constructs an `ErrorInfo` with HTTP status defaulting to 200.
*
* @param code The `ErrorCodeI` value this entry represents.
* @param token Short machine-readable string token (e.g., `"invalidParams"`).
* @param message Default human-readable error message.
*/
constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message)
: code(code), token(token), message(message), http_status(200)
{
}
/** Constructs an `ErrorInfo` with an explicit HTTP status.
*
* @param code The `ErrorCodeI` value this entry represents.
* @param token Short machine-readable string token.
* @param message Default human-readable error message.
* @param httpStatus HTTP status code returned to clients and load balancers.
*/
constexpr ErrorInfo(ErrorCodeI code, char const* token, char const* message, int httpStatus)
: code(code), token(token), message(message), http_status(httpStatus)
{
}
ErrorCodeI code;
json::StaticString token;
json::StaticString message;
int http_status;
ErrorCodeI code; /**< Numeric error code. */
json::StaticString token; /**< Short machine-readable string token (e.g., `"invalidParams"`). */
json::StaticString message; /**< Default human-readable error message. */
int http_status; /**< HTTP status for this error; 200 unless overridden. */
};
/** Returns an ErrorInfo that reflects the error code. */
/** Look up the `ErrorInfo` for a given error code.
*
* Performs a single bounds check followed by a direct array subscript —
* O(1) with no hash table or binary search.
*
* @param code The error code to look up.
* @return A `const` reference to the matching `ErrorInfo`, or to an
* internal unknown-error sentinel if @p code is outside the range
* `(RpcSuccess, RpcLast]`.
*/
ErrorInfo const&
getErrorInfo(ErrorCodeI code);
/** Add or update the json update to reflect the error code. */
/** Stamp `error`, `error_code`, and `error_message` fields onto a JSON object.
*
* Uses the default message registered for @p code. Any existing values for
* those three fields are overwritten.
*
* @param code The RPC error code whose metadata to inject.
* @param json The JSON object to mutate.
*/
/** @{ */
void
injectError(ErrorCodeI code, json::Value& json);
/** Stamp `error`, `error_code`, and `error_message` fields onto a JSON object,
* replacing the default message with a caller-supplied string.
*
* The machine-readable `error` token and numeric `error_code` are taken from
* the registry; only `error_message` is overridden, enabling context-specific
* diagnostics (e.g., naming the exact malformed field) while keeping the
* stable fields intact.
*
* @param code The RPC error code whose token and numeric code to inject.
* @param message Context-specific human-readable message.
* @param json The JSON object to mutate.
*/
void
injectError(ErrorCodeI code, std::string const& message, json::Value& json);
/** @} */
/** Returns a new json object that reflects the error code. */
/** Construct a fresh JSON error object for the given code.
*
* Convenience wrapper around `injectError` for handlers that build a response
* from scratch rather than annotating an existing object.
*
* @param code The RPC error code.
* @return A new `Json::Value` object with `error`, `error_code`, and
* `error_message` populated from the registry.
*/
/** @{ */
json::Value
makeError(ErrorCodeI code);
/** Construct a fresh JSON error object with a caller-supplied message.
*
* @param code The RPC error code.
* @param message Context-specific message written to `error_message`.
* @return A new `Json::Value` object with `error` and `error_code` from
* the registry and `error_message` set to @p message.
*/
json::Value
makeError(ErrorCodeI code, std::string const& message);
/** @} */
/** Returns a new json object that indicates invalid parameters. */
/** @{ */
/** Construct an `rpcINVALID_PARAMS` error object with a caller-supplied message.
*
* Thin wrapper around `makeError(RpcInvalidParams, message)` used by the
* field-error helper family below to avoid repetitive code at every
* parameter-validation site.
*
* @param message Human-readable description of the parameter problem.
* @return A new `Json::Value` error object for `RpcInvalidParams`.
*/
inline json::Value
makeParamError(std::string const& message)
{
return makeError(RpcInvalidParams, message);
}
/** Format a "missing field" diagnostic string.
*
* @param name The field name that was absent.
* @return The string `"Missing field '<name>'."`.
*/
inline std::string
missingFieldMessage(std::string const& name)
{
return "Missing field '" + name + "'.";
}
/** Return an `rpcINVALID_PARAMS` error for a missing required field.
*
* @param name The name of the missing field.
* @return A new JSON error object with a "Missing field" message.
*/
inline json::Value
missingFieldError(std::string const& name)
{
return makeParamError(missingFieldMessage(name));
}
/** @copydoc missingFieldError(std::string const&)
*
* @param name The name of the missing field as a `Json::StaticString`.
*/
inline json::Value
missingFieldError(json::StaticString name)
{
return missingFieldError(std::string(name));
}
/** Format a "field is not an object" diagnostic string.
*
* @param name The field name that was expected to be an object.
* @return The string `"Invalid field '<name>', not object."`.
*/
inline std::string
objectFieldMessage(std::string const& name)
{
return "Invalid field '" + name + "', not object.";
}
/** Return an `rpcINVALID_PARAMS` error for a field that must be an object.
*
* @param name The name of the field with the wrong type.
* @return A new JSON error object with a "not object" message.
*/
inline json::Value
objectFieldError(std::string const& name)
{
return makeParamError(objectFieldMessage(name));
}
/** @copydoc objectFieldError(std::string const&)
*
* @param name The field name as a `Json::StaticString`.
*/
inline json::Value
objectFieldError(json::StaticString name)
{
return objectFieldError(std::string(name));
}
/** Format a generic "invalid field" diagnostic string.
*
* @param name The field name that was invalid.
* @return The string `"Invalid field '<name>'."`.
*/
inline std::string
invalidFieldMessage(std::string const& name)
{
return "Invalid field '" + name + "'.";
}
/** @copydoc invalidFieldMessage(std::string const&)
*
* @param name The field name as a `Json::StaticString`.
*/
inline std::string
invalidFieldMessage(json::StaticString name)
{
return invalidFieldMessage(std::string(name));
}
/** Return an `rpcINVALID_PARAMS` error for a field that failed generic validation.
*
* @param name The name of the invalid field.
* @return A new JSON error object with an "Invalid field" message.
*/
inline json::Value
invalidFieldError(std::string const& name)
{
return makeParamError(invalidFieldMessage(name));
}
/** @copydoc invalidFieldError(std::string const&)
*
* @param name The field name as a `Json::StaticString`.
*/
inline json::Value
invalidFieldError(json::StaticString name)
{
return invalidFieldError(std::string(name));
}
/** Format a "field has wrong type" diagnostic string.
*
* @param name The field name.
* @param type The expected type description (e.g., `"unsigned integer"`).
* @return The string `"Invalid field '<name>', not <type>."`.
*/
inline std::string
expectedFieldMessage(std::string const& name, std::string const& type)
{
return "Invalid field '" + name + "', not " + type + ".";
}
/** @copydoc expectedFieldMessage(std::string const&, std::string const&)
*
* @param name The field name as a `Json::StaticString`.
* @param type The expected type description.
*/
inline std::string
expectedFieldMessage(json::StaticString name, std::string const& type)
{
return expectedFieldMessage(std::string(name), type);
}
/** Return an `rpcINVALID_PARAMS` error for a field whose value has the wrong type.
*
* @param name The name of the field with the wrong type.
* @param type The expected type description (e.g., `"unsigned integer"`).
* @return A new JSON error object with a "not <type>" message.
*/
inline json::Value
expectedFieldError(std::string const& name, std::string const& type)
{
return makeParamError(expectedFieldMessage(name, type));
}
/** @copydoc expectedFieldError(std::string const&, std::string const&)
*
* @param name The field name as a `Json::StaticString`.
* @param type The expected type description.
*/
inline json::Value
expectedFieldError(json::StaticString name, std::string const& type)
{
return expectedFieldError(std::string(name), type);
}
/** Return an `rpcINVALID_PARAMS` error for commands that require a validator node.
*
* Used by the handful of commands (e.g., `validator_info`) that are only
* meaningful when the local node is a validator.
*
* @return A new JSON error object with the message `"not a validator"`.
*/
inline json::Value
notValidatorError()
{
@@ -313,17 +494,47 @@ notValidatorError()
/** @} */
/** Returns `true` if the json contains an rpc error specification. */
/** Return `true` if @p json represents an RPC error response.
*
* The canonical test used throughout the RPC layer to distinguish error
* responses from successful ones. Only the presence of the `"error"` key
* is checked; the specific code is not inspected.
*
* @param json The JSON value to probe.
* @return `true` if @p json is an object containing an `"error"` member.
* @see getErrorInfo() for code-level branching on a known error.
*/
bool
containsError(json::Value const& json);
/** Returns http status that corresponds to the error code. */
/** Return the HTTP status integer associated with an error code.
*
* Used by the HTTP transport layer when constructing response headers.
* HTTP status assignments follow load-balancer failover semantics: transient
* server-side conditions (amendment-blocked, too-busy, not-synced) use 5xx
* or 429 so proxies can retry on a healthy peer; client-fault errors use
* 4xx; codes with no explicit assignment default to 200.
*
* @param code The RPC error code.
* @return HTTP status integer (e.g., 200, 400, 403, 503).
*/
int
errorCodeHttpStatus(ErrorCodeI code);
} // namespace RPC
/** Returns a single string with the contents of an RPC error. */
/** Concatenate the `error` token and `error_message` from a JSON error value.
*
* Convenience helper for producing logging and diagnostic strings from an
* already-constructed RPC error object.
*
* @param jv A `Json::Value` that must contain an RPC error
* (i.e., `RPC::containsError(jv)` is `true`).
* @return The `error` token string concatenated with the `error_message`
* string, with no separator.
* @note An `XRPL_ASSERT` fires in debug builds if @p jv does not contain
* an error, making misuse diagnosable early.
*/
std::string
rpcErrorString(json::Value const& jv);

View File

@@ -64,23 +64,37 @@
namespace xrpl {
// Feature names must not exceed this length (in characters, excluding the null terminator).
/** Maximum allowed length of a feature name in characters, excluding the null terminator. */
static constexpr std::size_t kMAX_FEATURE_NAME_SIZE = 63;
// Reserve this exact feature-name length (in characters/bytes, excluding the null terminator)
// so that a 32-byte uint256 (for example, in WASM or other interop contexts) can be used
// as a compact, fixed-size feature selector without conflicting with human-readable names.
/** Feature-name length (in bytes, excluding the null terminator) reserved for
* raw `uint256` hash selectors.
*
* A `uint256` is 32 bytes. Allowing a human-readable name that is exactly 32
* characters long would create an ambiguous namespace collision with compact
* feature selectors used in WASM or other interop contexts. Names of this
* exact length are rejected at compile time by `validFeatureNameSize()`.
*/
static constexpr std::size_t kRESERVED_FEATURE_NAME_SIZE = 32;
// Both validFeatureNameSize and validFeatureName are consteval functions that can be used in
// static_asserts to validate feature names at compile time. They are only used inside
// enforceValidFeatureName in Feature.cpp, but are exposed here for testing. The expected
// parameter `auto fn` is a constexpr lambda which returns a const char*, making it available
// for compile-time evaluation. Read more in https://accu.org/journals/overload/30/172/wu/
/** Validate a feature name's length at compile time.
*
* Returns `true` iff the name produced by `fn` satisfies both:
* - length ≤ `kMAX_FEATURE_NAME_SIZE` (63 characters), and
* - length ≠ `kRESERVED_FEATURE_NAME_SIZE` (32 characters).
*
* The parameter `fn` must be a `constexpr` lambda returning `const char*`,
* which makes the string literal available for compile-time evaluation.
* See https://accu.org/journals/overload/30/172/wu/ for the idiom.
*
* @param fn A `consteval`-compatible nullary callable returning `const char*`.
* @return `true` if the name length is valid, `false` otherwise.
* @note `std::strlen` is not `constexpr`; a manual loop computes the length.
*/
consteval auto
validFeatureNameSize(auto fn) -> bool
{
constexpr char const* kN = fn();
// Note, std::strlen is not constexpr, we need to implement our own here.
constexpr std::size_t kLEN = [](auto n) {
std::size_t ret = 0;
for (auto ptr = n; *ptr != '\0'; ret++, ++ptr)
@@ -91,14 +105,22 @@ validFeatureNameSize(auto fn) -> bool
kLEN <= kMAX_FEATURE_NAME_SIZE;
}
/** Validate that a feature name contains only printable ASCII characters.
*
* Returns `true` iff every character in the name produced by `fn` has value
* ≥ 0x20 and the high bit (0x80) clear. Rejects:
* - Control characters (below 0x20, e.g. `\t`, `\n`).
* - Non-ASCII bytes (high bit set), which appear in UTF-8 multibyte sequences
* and Unicode identifiers that C++ technically permits but that are visually
* confusable with ASCII characters (e.g. Greek Capital Alpha vs. `'A'`).
*
* @param fn A `consteval`-compatible nullary callable returning `const char*`.
* @return `true` if all characters are printable ASCII, `false` otherwise.
*/
consteval auto
validFeatureName(auto fn) -> bool
{
constexpr char const* kN = fn();
// Prevent the use of visually confusable characters and enforce that feature names
// are always valid ASCII. This is needed because C++ allows Unicode identifiers.
// Characters below 0x20 are nonprintable control characters, and characters with the 0x80 bit
// set are non-ASCII (e.g. UTF-8 encoding of Unicode), so both are disallowed.
for (auto ptr = kN; *ptr != '\0'; ++ptr)
{
if (*ptr & 0x80 || *ptr < 0x20)
@@ -107,10 +129,48 @@ validFeatureName(auto fn) -> bool
return true;
}
enum class VoteBehavior : int { Obsolete = -1, DefaultNo = 0, DefaultYes = 1 };
enum class AmendmentSupport : int { Retired = -1, Supported = 0, Unsupported = 1 };
/** Controls whether this server votes for an amendment it supports.
*
* Governs the server's default stance during the amendment voting round.
* The winning value for most amendments progresses from `DefaultNo`
* (governance decides timing) to optionally `DefaultYes` (critical fixes),
* and then to `Obsolete` if the amendment is abandoned without activating.
*/
enum class VoteBehavior : int {
Obsolete = -1, /**< Amendment supported but no longer voted for; retained
* for ledger compatibility only. */
DefaultNo = 0, /**< Server supports but abstains by default; external
* governance decides when to activate. */
DefaultYes = 1, /**< Server actively votes for activation; reserved for
* critical bug fixes after off-chain consensus. */
};
/** All amendments libxrpl knows about. */
/** Records how well this build understands a given amendment.
*
* Used by `allAmendments()` to report the full picture of what the server
* knows about each amendment, including retired ones whose conditional code
* has been removed.
*/
enum class AmendmentSupport : int {
Retired = -1, /**< Conditional code removed; amendment remains registered
* so nodes stay amendment-compatible with old ledgers. */
Supported = 0, /**< Amendment is recognized and the server may vote for it. */
Unsupported = 1, /**< Amendment is known but this build does not implement it. */
};
/** Return every amendment this build has ever known about, including retired ones.
*
* Maps each amendment's string name to its `AmendmentSupport` status:
* `Supported` (recognized and votable), `Unsupported` (declared but not
* implemented by this build), or `Retired` (conditional code removed,
* retained for ledger compatibility). The returned reference is stable for
* the process lifetime.
*
* @return A sorted map of amendment name → `AmendmentSupport`.
* @note This function must only be called after static initialization
* completes. Calling it during static initialization of another
* translation unit risks querying before the registry is sealed.
*/
std::map<std::string, AmendmentSupport> const&
allAmendments();
@@ -132,10 +192,16 @@ namespace detail {
#define XRPL_RETIRE_FIX(name) +1
// NOLINTEND(bugprone-macro-parentheses)
// This value SHOULD be equal to the number of amendments registered in
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
/** Compile-time upper bound on the total number of registered amendments.
*
* Used as the `std::bitset` template parameter for `FeatureBitset`. SHOULD
* equal the actual count of entries in `features.macro`, but MAY be larger
* (reserving headroom for future additions). MUST NOT be less than the actual
* count — a `LogicError` on startup verifies this.
*
* @note This is a ceiling, not an exact count. Do not use it as an iteration
* bound or to infer the number of active amendments.
*/
static constexpr std::size_t kNUM_FEATURES =
(0 +
#include <xrpl/protocol/detail/features.macro>
@@ -150,40 +216,110 @@ static constexpr std::size_t kNUM_FEATURES =
#undef XRPL_FEATURE
#pragma pop_macro("XRPL_FEATURE")
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated
ledger */
/** Return amendments this build supports and their default vote stance.
*
* Maps each supported amendment's name to its `VoteBehavior`. An amendment
* appearing here is recognized by this build; whether it is actually active
* depends on the `Rules` derived from the validated ledger's Amendments
* object. Retired amendments (`VoteBehavior::Obsolete`) appear here but are
* not voted for.
*
* @return A sorted map of amendment name → `VoteBehavior`.
*/
std::map<std::string, VoteBehavior> const&
supportedAmendments();
/** Amendments that this server won't vote for by default.
This function is only used in unit tests.
*/
/** Return the count of supported amendments this server will NOT vote for.
*
* Includes both `VoteBehavior::DefaultNo` and `VoteBehavior::Obsolete`
* entries. Used in unit tests to verify the vote-tally invariant:
* `numDownVotedAmendments() + numUpVotedAmendments() == supportedAmendments().size()`.
*
* @return Count of amendments this server abstains from or treats as obsolete.
*/
std::size_t
numDownVotedAmendments();
/** Amendments that this server will vote for by default.
This function is only used in unit tests.
*/
/** Return the count of supported amendments this server will vote for.
*
* Counts only `VoteBehavior::DefaultYes` entries. Used in unit tests to
* verify the vote-tally invariant alongside `numDownVotedAmendments()`.
*
* @return Count of amendments this server actively votes to activate.
*/
std::size_t
numUpVotedAmendments();
} // namespace detail
/** Look up a registered amendment by name and return its on-chain identifier.
*
* @param name The amendment's string name (e.g. `"Checks"`).
* @return The `uint256` hash computed as `sha512Half(name)`, or `std::nullopt`
* if no amendment with that name has been registered.
* @note Feature names are case-sensitive. Querying an unknown name returns
* `nullopt`; it does not throw.
*/
std::optional<uint256>
getRegisteredFeature(std::string const& name);
/** Translate an amendment's `uint256` identifier to its `FeatureBitset` bit position.
*
* This is the hot-path translation used by every `FeatureBitset` operation.
* The result is stable for the process lifetime because the registry is sealed
* before any calls can be made.
*
* @param f A registered amendment identifier.
* @return The zero-based bit index within `FeatureBitset`.
* @throws LogicError if `f` is not a registered amendment.
*/
size_t
featureToBitsetIndex(uint256 const& f);
/** Translate a `FeatureBitset` bit position back to the amendment's `uint256`.
*
* Inverse of `featureToBitsetIndex()`. Used by `foreachFeature()` to convert
* set bits back into identifiers for callers.
*
* @param i A zero-based bit index within `FeatureBitset`.
* @return The `uint256` hash of the amendment registered at that position.
* @throws LogicError if `i` is out of bounds (≥ the number of registered amendments).
*/
uint256
bitsetIndexToFeature(size_t i);
/** Return the human-readable name for an amendment, or its hex representation.
*
* Useful for diagnostics and logging when a `uint256` amendment ID needs to be
* displayed.
*
* @param f The amendment identifier to look up.
* @return The registered string name (e.g. `"Checks"`), or `to_string(f)` if
* `f` is not in the registry.
*/
std::string
featureToName(uint256 const& f);
/** A set of active amendments, represented as a bitset indexed by amendment ID.
*
* Wraps `std::bitset<detail::kNUM_FEATURES>` and replaces integer-index access
* with `uint256`-based access. Externally every amendment is a `uint256` hash;
* internally `featureToBitsetIndex()` maps it to a compact sequential bit
* position, so all set operations run in O(1).
*
* The full suite of bitwise operators is provided for set algebra:
* - `operator&` — intersection (features enabled in both sets)
* - `operator|` — union (features enabled in either set)
* - `operator^` — symmetric difference
* - `operator-` — **set difference** (`lhs & ~rhs`), used in amendment voting
* to compute "amendments I support that are not yet enabled"
*
* Overloads accepting a bare `uint256` on either side construct a temporary
* single-element `FeatureBitset` for the operation.
*
* @see foreachFeature() to iterate all set bits.
* @see Rules::enabled() for the per-transaction query path.
*/
class FeatureBitset : private std::bitset<detail::kNUM_FEATURES>
{
using base = std::bitset<detail::kNUM_FEATURES>;
@@ -215,13 +351,30 @@ public:
using base::to_ullong;
using base::to_ulong;
/** Construct an empty feature set (no amendments enabled). */
FeatureBitset() = default;
/** Construct from a raw `std::bitset`, asserting no bits are lost.
*
* @param b A bitset whose bit layout matches the amendment registry's
* insertion order. Intended for internal use (e.g. bitwise operators).
*/
explicit FeatureBitset(base const& b) : base(b)
{
XRPL_ASSERT(b.count() == count(), "xrpl::FeatureBitset::FeatureBitset(base) : count match");
}
/** Construct from one or more amendment identifiers.
*
* Each `uint256` is translated to its bitset position via
* `featureToBitsetIndex()`. Asserts that all supplied features are
* distinct (the resulting count equals the number of arguments).
*
* @param f First amendment identifier.
* @param fs Additional amendment identifiers (variadic).
* @throws LogicError (via `featureToBitsetIndex`) if any identifier is
* not registered.
*/
template <class... Fs>
explicit FeatureBitset(uint256 const& f, Fs&&... fs)
{
@@ -232,6 +385,16 @@ public:
"sizeof... do match");
}
/** Construct from any range of `uint256` amendment identifiers.
*
* Iterates `fs` and sets the corresponding bit for each element.
* Asserts that the resulting popcount equals `fs.size()` (all distinct).
*
* @tparam Col A range whose elements are convertible to `uint256`.
* @param fs A collection of amendment identifiers.
* @throws LogicError (via `featureToBitsetIndex`) if any identifier is
* not registered.
*/
template <class Col>
explicit FeatureBitset(Col const& fs)
{
@@ -243,18 +406,35 @@ public:
"size do match");
}
/** Return a reference to the bit corresponding to amendment `f`.
*
* @param f A registered amendment identifier.
* @throws LogicError if `f` is not registered.
*/
auto
operator[](uint256 const& f)
{
return base::operator[](featureToBitsetIndex(f));
}
/** Return the value of the bit corresponding to amendment `f`.
*
* @param f A registered amendment identifier.
* @throws LogicError if `f` is not registered.
*/
auto
operator[](uint256 const& f) const
{
return base::operator[](featureToBitsetIndex(f));
}
/** Set (or clear) the bit for amendment `f`.
*
* @param f A registered amendment identifier.
* @param value `true` to enable the amendment, `false` to disable.
* @return `*this`, for chaining.
* @throws LogicError if `f` is not registered.
*/
FeatureBitset&
set(uint256 const& f, bool value = true)
{
@@ -262,6 +442,12 @@ public:
return *this;
}
/** Clear the bit for amendment `f`.
*
* @param f A registered amendment identifier.
* @return `*this`, for chaining.
* @throws LogicError if `f` is not registered.
*/
FeatureBitset&
reset(uint256 const& f)
{
@@ -269,6 +455,12 @@ public:
return *this;
}
/** Toggle the bit for amendment `f`.
*
* @param f A registered amendment identifier.
* @return `*this`, for chaining.
* @throws LogicError if `f` is not registered.
*/
FeatureBitset&
flip(uint256 const& f)
{
@@ -276,6 +468,7 @@ public:
return *this;
}
/** Intersect this set with `rhs` in-place. */
FeatureBitset&
operator&=(FeatureBitset const& rhs)
{
@@ -283,6 +476,7 @@ public:
return *this;
}
/** Union this set with `rhs` in-place. */
FeatureBitset&
operator|=(FeatureBitset const& rhs)
{
@@ -290,79 +484,95 @@ public:
return *this;
}
/** Return the complement: every registered amendment NOT in this set. */
FeatureBitset
operator~() const
{
return FeatureBitset{base::operator~()};
}
/** Return the intersection of two feature sets. */
friend FeatureBitset
operator&(FeatureBitset const& lhs, FeatureBitset const& rhs)
{
return FeatureBitset{static_cast<base const&>(lhs) & static_cast<base const&>(rhs)};
}
/** Return the intersection of a feature set and a single amendment. */
friend FeatureBitset
operator&(FeatureBitset const& lhs, uint256 const& rhs)
{
return lhs & FeatureBitset{rhs};
}
/** Return the intersection of a single amendment and a feature set. */
friend FeatureBitset
operator&(uint256 const& lhs, FeatureBitset const& rhs)
{
return FeatureBitset{lhs} & rhs;
}
/** Return the union of two feature sets. */
friend FeatureBitset
operator|(FeatureBitset const& lhs, FeatureBitset const& rhs)
{
return FeatureBitset{static_cast<base const&>(lhs) | static_cast<base const&>(rhs)};
}
/** Return the union of a feature set and a single amendment. */
friend FeatureBitset
operator|(FeatureBitset const& lhs, uint256 const& rhs)
{
return lhs | FeatureBitset{rhs};
}
/** Return the union of a single amendment and a feature set. */
friend FeatureBitset
operator|(uint256 const& lhs, FeatureBitset const& rhs)
{
return FeatureBitset{lhs} | rhs;
}
/** Return the symmetric difference of two feature sets. */
friend FeatureBitset
operator^(FeatureBitset const& lhs, FeatureBitset const& rhs)
{
return FeatureBitset{static_cast<base const&>(lhs) ^ static_cast<base const&>(rhs)};
}
/** Return the symmetric difference of a feature set and a single amendment. */
friend FeatureBitset
operator^(FeatureBitset const& lhs, uint256 const& rhs)
{
return lhs ^ FeatureBitset{rhs};
}
/** Return the symmetric difference of a single amendment and a feature set. */
friend FeatureBitset
operator^(uint256 const& lhs, FeatureBitset const& rhs)
{
return FeatureBitset{lhs} ^ rhs;
}
// set difference
/** Return the set difference: amendments in `lhs` that are not in `rhs` (`lhs & ~rhs`).
*
* Used in amendment voting to compute "amendments this server supports
* that have not yet been enabled on the network".
*/
friend FeatureBitset
operator-(FeatureBitset const& lhs, FeatureBitset const& rhs)
{
return lhs & ~rhs;
}
/** Return the set difference of a feature set minus a single amendment. */
friend FeatureBitset
operator-(FeatureBitset const& lhs, uint256 const& rhs)
{
return lhs - FeatureBitset{rhs};
}
/** Return the set difference: a single amendment minus all amendments in `rhs`. */
friend FeatureBitset
operator-(uint256 const& lhs, FeatureBitset const& rhs)
{
@@ -370,6 +580,16 @@ public:
}
};
/** Invoke a callback for each amendment enabled in `bs`.
*
* Iterates all bit positions in `bs`, translates each set bit back to its
* `uint256` amendment identifier via `bitsetIndexToFeature()`, and passes it
* to `f`. Unset bits are skipped.
*
* @tparam F A callable accepting a single `uint256 const&` argument.
* @param bs The feature set to iterate.
* @param f Callback invoked once per enabled amendment.
*/
template <class F>
void
foreachFeature(FeatureBitset bs, F&& f)
@@ -381,6 +601,13 @@ foreachFeature(FeatureBitset bs, F&& f)
}
}
// --- Amendment identifier declarations ---
//
// A second X-macro pass over features.macro declares one `extern uint256 const`
// variable per active amendment (e.g. `featureChecks`, `fixAMMOverflowOffer`).
// These are the identifiers used throughout the codebase in
// `rules.enabled(featureName)` calls. Retired entries expand to nothing because
// their conditional code has been removed.
#pragma push_macro("XRPL_FEATURE")
#undef XRPL_FEATURE
#pragma push_macro("XRPL_FIX")

View File

@@ -4,24 +4,56 @@
namespace xrpl {
// Deprecated constant for backwards compatibility with pre-XRPFees amendment.
// This was the reference fee units used in the old fee calculation.
/** Reference fee cost in abstract "fee units" used before the XRPFees amendment.
*
* Prior to `featureXRPFees`, transaction costs were expressed in fee units
* rather than drops; a reference transaction cost 10 fee units and the actual
* drop cost was determined by multiplying by a per-ledger scaling factor.
* After the amendment, fees are expressed natively in drops via `XRPAmount`.
*
* This constant is retained as a compatibility shim: it is written into
* `sfReferenceFeeUnits` in validation objects and `fee_ref` in JSON
* subscription messages when `featureXRPFees` is not active, preserving the
* legacy wire format consumed by older clients.
*/
inline constexpr std::uint32_t kFEE_UNITS_DEPRECATED = 10;
/** Reflects the fee settings for a particular ledger.
The fees are always the same for any transactions applied
to a ledger. Changes to fees occur in between ledgers.
*/
/** Snapshot of a ledger's fee schedule.
*
* Packages the three economically significant fee parameters — transaction
* cost, base account reserve, and per-object reserve increment — into a
* single value-semantic aggregate. Obtained via `ReadView::fees()` so that
* transactors, preflight checks, and RPC handlers can query fee parameters
* without knowing the concrete view type.
*
* @invariant Fee parameters are constant within a ledger; changes take
* effect only at the next ledger boundary and are driven by validator
* fee-vote consensus updating the `FeeSettings` SLE.
*/
struct Fees
{
/** @brief Cost of a reference transaction in drops. */
/** Minimum fee for a reference transaction, in drops.
*
* Transactions paying fewer drops than this value are rejected.
* Zero-initialized so that a default-constructed `Fees` acts as a
* safe placeholder in tests or before a ledger is loaded.
*/
XRPAmount base{0};
/** @brief Minimum XRP an account must hold to exist on the ledger. */
/** Minimum XRP balance every account must hold simply to exist, in drops.
*
* An account whose balance falls below its total reserve (see
* `accountReserve()`) becomes reserve-deficient and cannot send payments.
*/
XRPAmount reserve{0};
/** @brief Additional XRP reserve required per owned ledger object. */
/** Additional reserve required for each ledger object owned by an account,
* in drops.
*
* Applies to trust lines, offers, escrows, NFT tokens, and other objects
* that consume shared ledger state. Multiplied by `ownerCount` in
* `accountReserve()` to produce the total per-object reserve charge.
*/
XRPAmount increment{0};
explicit Fees() = default;
@@ -29,16 +61,31 @@ struct Fees
Fees&
operator=(Fees const&) = default;
/** Construct a fee schedule from explicit drop amounts.
*
* @param base Minimum fee for a reference transaction, in drops.
* @param reserve Base account reserve, in drops.
* @param increment Per-owned-object reserve increment, in drops.
*/
Fees(XRPAmount base, XRPAmount reserve, XRPAmount increment)
: base(base), reserve(reserve), increment(increment)
{
}
/** Returns the account reserve given the owner count, in drops.
The reserve is calculated as the reserve base plus
the reserve increment times the number of increments.
*/
/** Compute the total XRP reserve an account must hold, in drops.
*
* Applies the formula `reserve + ownerCount * increment`. Callers
* checking whether an account can afford a *new* object should pass
* `ownerCount + 1` — the post-creation count — so the check accounts
* for the incremental cost of the object being created.
*
* @note Pseudo-accounts (AMM, Vault, LoanBroker) are exempt from
* reserves; their callers bypass this method entirely.
*
* @param ownerCount Number of ledger objects currently owned by the
* account (from `sfOwnerCount` on the `AccountRoot` SLE).
* @return Total required balance in drops.
*/
[[nodiscard]] XRPAmount
accountReserve(std::size_t ownerCount) const
{

View File

@@ -1,3 +1,12 @@
/** @file
* Protocol hash domain separation via 4-byte prefixes.
*
* Every XRPL hashing context prepends a `HashPrefix` constant to its input
* so that two structurally different objects that share identical serialized
* bytes can never collide in hash space. See `HashPrefix` for the full list
* of contexts and `hash_append` for the N3980-compatible integration point.
*/
#pragma once
#include <xrpl/beast/hash/hash_append.h>
@@ -8,6 +17,19 @@ namespace xrpl {
namespace detail {
/** Pack three ASCII characters into the high 24 bits of a `uint32_t`.
*
* The resulting value has the form `(a << 24) | (b << 16) | (c << 8)`,
* leaving the low byte as zero. The trailing zero acts as an implicit
* separator and prevents any prefix from coinciding with a valid 1- or
* 2-byte byte sequence. The ASCII mnemonics make prefixes self-documenting
* in hex dumps (e.g. `TransactionId` appears as `0x54584E00`, i.e. `TXN\0`).
*
* @param a First character of the 3-letter mnemonic.
* @param b Second character of the 3-letter mnemonic.
* @param c Third character of the 3-letter mnemonic.
* @return A `constexpr` `uint32_t` suitable for use as a `HashPrefix` value.
*/
constexpr std::uint32_t
makeHashPrefix(char a, char b, char c)
{
@@ -17,58 +39,121 @@ makeHashPrefix(char a, char b, char c)
} // namespace detail
/** Prefix for hashing functions.
These prefixes are inserted before the source material used to generate
various hashes. This is done to put each hash in its own "space." This way,
two different types of objects with the same binary data will produce
different hashes.
Each prefix is a 4-byte value with the last byte set to zero and the first
three bytes formed from the ASCII equivalent of some arbitrary string. For
example "TXN".
@note Hash prefixes are part of the protocol; you cannot, arbitrarily,
change the type or the value of any of these without causing breakage.
*/
/** 4-byte domain-separation sentinels prepended to every XRPL hash input.
*
* Each enumerator identifies a distinct hashing context. Prepending the
* prefix ensures that two objects from different contexts with byte-for-byte
* identical serializations always produce different digests, closing a class
* of hash-collision attacks at the protocol layer.
*
* The prefix is consumed in one of two ways depending on the call site:
* - **Serializer prefix** (`s.add32(HashPrefix::TxSign)`): writes the raw
* `uint32_t` into a `Serializer` buffer before appending signing fields.
* - **`hash_append` composition** (`hash_append(h, HashPrefix::InnerNode)`):
* feeds the 4-byte value directly into a streaming hasher, avoiding an
* intermediate buffer.
*
* Both produce the same 4-byte prefix at position zero of the hash input.
*
* @note Hash prefixes are protocol-immutable. Changing the mnemonic letters
* or the numeric value of any enumerator breaks consensus and cross-node
* compatibility irreversibly.
*/
enum class HashPrefix : std::uint32_t {
/** transaction plus signature to give transaction ID */
/** Canonical transaction ID: SHA-512/2 of `TXN\0` followed by the
* transaction bytes including its signature field.
* Distinct from `TxSign` (which excludes the signature) so that signing
* payloads and transaction IDs operate in separate hash namespaces.
*/
TransactionId = detail::makeHashPrefix('T', 'X', 'N'),
/** transaction plus metadata */
/** Transaction-plus-metadata leaf node in the transaction SHAMap
* (`SND\0`). Used by `SHAMapTxPlusMetaLeafNode` to hash a transaction
* together with its execution metadata. Distinct from `TransactionId`
* so a raw transaction and its annotated form can never collide.
*/
TxNode = detail::makeHashPrefix('S', 'N', 'D'),
/** account state */
/** Account-state leaf node in the SHAMap (`MLN\0`). Used by
* `SHAMapAccountStateLeafNode` when computing or verifying the hash of
* a single ledger-state entry.
*/
LeafNode = detail::makeHashPrefix('M', 'L', 'N'),
/** inner node in V1 tree */
/** SHAMap inner (branch) node (`MIN\0`). Used by `SHAMapInnerNode`
* when hashing the 16 child-hash slots of a branch node. Distinct from
* `LeafNode` so inner-node hashes never collide with leaf-node hashes.
*/
InnerNode = detail::makeHashPrefix('M', 'I', 'N'),
/** ledger master data for signing */
/** Ledger header signing payload (`LWR\0`). Prepended to the serialized
* ledger header before computing the ledger hash that validators sign and
* that serves as the canonical ledger identifier.
*/
LedgerMaster = detail::makeHashPrefix('L', 'W', 'R'),
/** inner transaction to sign */
/** Single-signature transaction signing payload (`STX\0`). Prepended to
* the serialized transaction body (with signing fields, without the
* signature itself) before a regular key or master key signs. A
* `TxSign` blob cannot be replayed as a `TxMultiSign` contribution
* because the two prefixes produce different digests.
*/
TxSign = detail::makeHashPrefix('S', 'T', 'X'),
/** inner transaction to multi-sign */
/** Multi-signature transaction signing payload (`SMT\0`). Prepended to
* the serialized transaction body plus the signer's `AccountID` suffix
* before each individual signer's key signs. Distinct from `TxSign` to
* prevent a single-sig blob from being replayed as a multi-sig share.
*/
TxMultiSign = detail::makeHashPrefix('S', 'M', 'T'),
/** validation for signing */
/** Validator validation message signing payload (`VAL\0`). Used by
* `STValidation::getSigningHash` to produce the digest that a validator
* signs when asserting agreement on a ledger.
*/
Validation = detail::makeHashPrefix('V', 'A', 'L'),
/** proposal for signing */
/** Consensus proposal signing payload (`PRP\0`). Used by
* `ConsensusProposal` and `RCLCxPeerPos` when signing or verifying a
* peer's position on a candidate ledger during the consensus round.
*/
Proposal = detail::makeHashPrefix('P', 'R', 'P'),
/** Manifest */
/** Validator manifest signing payload (`MAN\0`). Used by the manifest
* system to sign and verify the binding between a validator's master key
* and its rotating ephemeral signing key.
*/
Manifest = detail::makeHashPrefix('M', 'A', 'N'),
/** Payment Channel Claim */
/** Off-ledger payment channel claim payload (`CLM\0`). Prepended to the
* channel ID and authorized amount before the channel owner signs an
* off-ledger claim that a counterparty can later submit on-chain.
*/
PaymentChannelClaim = detail::makeHashPrefix('C', 'L', 'M'),
/** Batch */
/** Batch transaction signing payload (`BCH\0`). Prepended to the outer
* batch flags, inner transaction count, and list of inner transaction
* IDs before signing a batch. See `serializeBatch()` in `Batch.h`.
*/
Batch = detail::makeHashPrefix('B', 'C', 'H'),
};
/** Feed a `HashPrefix` into a N3980-compatible streaming hasher.
*
* Casts the prefix to its underlying `uint32_t` representation and forwards
* it to `beast::hash_append`, allowing a `HashPrefix` to be composed with
* other arguments in a single variadic `sha512Half` call:
* @code
* sha512Half(HashPrefix::transactionID, data)
* @endcode
* No temporary allocation or explicit serialization step is required; the
* 4-byte prefix is fed directly into the running digest state.
*
* @tparam Hasher A type satisfying the N3980 `hash_append` protocol
* (e.g. `sha512_half_hasher`).
* @param h The hasher instance to update.
* @param hp The prefix value to append.
*/
template <class Hasher>
void
hash_append(Hasher& h, HashPrefix const& hp) noexcept

View File

@@ -11,16 +11,35 @@
namespace xrpl {
/** Floating point representation of amounts with high dynamic range
Amounts are stored as a normalized signed mantissa and an exponent. The
range of the normalized exponent is [-96,80] and the range of the absolute
value of the normalized mantissa is [1000000000000000, 9999999999999999].
Arithmetic operations can throw std::overflow_error during normalization
if the amount exceeds the largest representable amount, but underflows
will silently truncate to zero.
*/
/** Fixed-precision decimal floating-point type for IOU (non-native) balances.
*
* Encodes a value as `mantissa × 10^exponent` using a 64-bit signed mantissa
* and an integer exponent. Canonical form requires the absolute value of the
* mantissa to lie in `[10^15, 10^161]` (i.e., `[1000000000000000,
* 9999999999999999]`) and the exponent to lie in `[-96, 80]`. These bounds
* match the on-wire limits in `STAmount`, so a normalized `IOUAmount` is
* always serializable.
*
* Zero is the sentinel `{mantissa=0, exponent=-100}`. The exponent `-100` is
* chosen to be below the minimum representable non-zero exponent (`-96`), so
* that numeric ordering via the exponent field correctly places zero below
* the smallest positive amount.
*
* Arithmetic operations can throw `std::overflow_error` during normalization
* if the result exceeds the largest representable amount; underflows silently
* truncate to zero. This asymmetry is intentional: overflow indicates a
* programming error, while sub-minimum amounts arise naturally from interest
* calculations and must degrade gracefully.
*
* The class privately inherits `boost::totally_ordered` and
* `boost::additive` to derive the full set of comparison and binary
* arithmetic operators from the handful of hand-written primitives
* (`operator==`, `operator<`, `operator+=`, `operator-`).
*
* @note Normalization has two code paths selected by `getSTNumberSwitchover()`:
* the legacy in-place loop and the modern path delegating to
* `Number::normalizeToRange`. Use `NumberSO` to scope either path.
*/
class IOUAmount : private boost::totally_ordered<IOUAmount>, private boost::additive<IOUAmount>
{
private:
@@ -30,39 +49,124 @@ private:
exponent_type exponent_{};
/** Adjusts the mantissa and exponent to the proper range.
This can throw if the amount cannot be normalized, or is larger than
the largest value that can be represented as an IOU amount. Amounts
that are too small to be represented normalize to 0.
*/
*
* Scales the mantissa up (multiply by 10, decrement exponent) or down
* (divide by 10, increment exponent) until the absolute value of the
* mantissa is in `[10^15, 10^161]` and the exponent is in `[-96, 80]`.
*
* Which algorithm is used depends on `getSTNumberSwitchover()`: when
* false, a legacy digit-by-digit loop; when true (the default), delegates
* to `Number::normalizeToRange`.
*
* @throws std::overflow_error if the value is too large to be represented.
* @note Underflow silently rounds to zero rather than throwing.
*/
void
normalize();
/** Convert a `Number` to an `IOUAmount` by fitting its mantissa into
* the IOU `10^15` precision range via `Number::normalizeToRange`.
*
* @param number The `Number` value to convert.
* @return The nearest representable `IOUAmount`.
*/
static IOUAmount
fromNumber(Number const& number);
public:
/** Default-constructs a zero amount (`{mantissa=0, exponent=0}`).
*
* @note The raw fields are zero-initialized but `normalize()` is not
* called; use `IOUAmount{beast::kZERO}` to get the canonical zero
* sentinel `{mantissa=0, exponent=-100}`.
*/
IOUAmount() = default;
/** Construct from a `Number`, fitting its mantissa into IOU precision.
*
* @param other The `Number` to convert. Delegates to `fromNumber()`.
*/
explicit IOUAmount(Number const& other);
/** Construct the canonical zero sentinel `{mantissa=0, exponent=-100}`. */
IOUAmount(beast::Zero);
/** Construct from raw mantissa and exponent, then normalize.
*
* @param mantissa The signed mantissa; sign determines the amount's sign.
* @param exponent The power-of-ten exponent.
* @throws std::overflow_error if the value cannot be normalized to the
* representable range after scaling.
*/
IOUAmount(mantissa_type mantissa, exponent_type exponent);
/** Reset to the canonical zero sentinel `{mantissa=0, exponent=-100}`.
*
* The exponent `-100` ensures zero sorts below the smallest positive
* amount whose minimum exponent is `-96`.
*/
IOUAmount& operator=(beast::Zero);
/** Implicit conversion to `Number`.
*
* Constructs `Number{mantissa_, exponent_}`, bridging the legacy IOU
* type into the modern arithmetic layer. Conversion to `Number` is
* intentionally implicit; the reverse (from `Number`) is explicit.
*/
operator Number() const;
/** Add another amount in-place.
*
* When `getSTNumberSwitchover()` is true, routes through
* `Number` arithmetic for correct handling across the two normalization
* regimes. Otherwise, performs manual exponent alignment.
*
* @param other The amount to add.
* @return Reference to `*this` after normalization.
* @throws std::overflow_error if the result exceeds the representable range.
*/
IOUAmount&
operator+=(IOUAmount const& other);
/** Subtract another amount in-place.
*
* Implemented as `*this += -other`.
*
* @param other The amount to subtract.
* @return Reference to `*this` after normalization.
* @throws std::overflow_error if the result exceeds the representable range.
*/
IOUAmount&
operator-=(IOUAmount const& other);
/** Negate the amount without calling `normalize()`.
*
* Flips the sign of the mantissa. Safe because the negation of a
* normalized value is also normalized; negating zero leaves the
* `{0, -100}` sentinel unchanged.
*
* @return A new `IOUAmount` with the same magnitude and opposite sign.
*/
IOUAmount
operator-() const;
/** Returns true if both amounts have identical mantissa and exponent.
*
* Valid because every non-zero value has a unique canonical
* representation after normalization, and zero is always `{0, -100}`.
*
* @param other The amount to compare.
*/
bool
operator==(IOUAmount const& other) const;
/** Returns true if this amount is strictly less than `other`.
*
* Delegates to `Number` comparison, which handles the zero sentinel and
* cross-regime comparisons correctly.
*
* @param other The amount to compare against.
*/
bool
operator<(IOUAmount const& other) const;
@@ -74,12 +178,26 @@ public:
[[nodiscard]] int
signum() const noexcept;
/** Return the raw (normalized) exponent.
*
* The value is in `[-96, 80]` for non-zero amounts, or `-100` for zero.
*/
[[nodiscard]] exponent_type
exponent() const noexcept;
/** Return the raw (normalized) signed mantissa.
*
* For non-zero amounts, the absolute value is in `[10^15, 10^161]`.
* Zero returns `0`.
*/
[[nodiscard]] mantissa_type
mantissa() const noexcept;
/** Return the smallest representable positive `IOUAmount`.
*
* Corresponds to `{mantissa = 10^15, exponent = -96}`, the lower-left
* corner of the normalized canonical range.
*/
static IOUAmount
minPositiveAmount();
@@ -104,8 +222,6 @@ inline IOUAmount::IOUAmount(mantissa_type mantissa, exponent_type exponent)
inline IOUAmount&
IOUAmount::operator=(beast::Zero)
{
// The -100 is used to allow 0 to sort less than small positive values
// which will have a large negative exponent.
mantissa_ = 0;
exponent_ = -100;
return *this;
@@ -168,29 +284,73 @@ IOUAmount::mantissa() const noexcept
return mantissa_;
}
/** Format an `IOUAmount` as a human-readable decimal string.
*
* Produces integer notation when the exponent is non-negative (e.g. `"2e20"`
* for very large values), or decimal notation for fractional amounts (e.g.
* `"0.025"`). Scientific notation is used when the decimal form would be
* impractical.
*
* @param amount The amount to format.
* @return A decimal string representation of `amount`.
*/
std::string
to_string(IOUAmount const& amount);
/* Return num*amt/den
This function keeps more precision than computing
num*amt, storing the result in an IOUAmount, then
dividing by den.
*/
/** Compute `amt × num / den` with higher precision than sequential operations.
*
* Intermediate products are held in a 128-bit unsigned integer to avoid
* overflow when multiplying a 64-bit mantissa by a 32-bit numerator. The
* quotient and remainder are then rescaled to fit the 64-bit IOU mantissa
* range, with the rounding direction determined by `roundUp`.
*
* @param amt The base amount to scale.
* @param num Numerator of the scaling ratio; may be zero to produce zero.
* @param den Denominator of the scaling ratio; must not be zero.
* @param roundUp When true, rounds toward positive infinity for positive
* results and toward negative infinity for negative results (directed
* rounding semantics). When false, rounds toward zero.
* @return The scaled amount `amt × num / den`.
* @throws std::overflow_error if the result exceeds the representable range.
* @throws std::domain_error (or similar) if `den` is zero.
*/
IOUAmount
mulRatio(IOUAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp);
// Since many uses of the number class do not have access to a ledger,
// getSTNumberSwitchover needs to be globally accessible.
/** Return the current coroutine-local STNumber switchover flag.
*
* When true, `IOUAmount::normalize()` and `operator+=` use the modern
* `Number`-based code path; when false, the legacy in-place loop is used.
* The flag is stored in a `LocalValue<bool>` so each coroutine has its own
* independent copy. Globally accessible because most callers do not have
* access to a ledger/rules context.
*
* @return true if the `Number` normalization path is active.
* @see setSTNumberSwitchover, NumberSO
*/
bool
getSTNumberSwitchover();
/** Set the coroutine-local STNumber switchover flag.
*
* Prefer the `NumberSO` RAII guard for scoped changes.
*
* @param v true to enable the `Number` normalization path; false for legacy.
* @see getSTNumberSwitchover, NumberSO
*/
void
setSTNumberSwitchover(bool v);
/** RAII class to set and restore the Number switchover.
/** RAII guard that temporarily overrides the coroutine-local STNumber
* switchover flag and restores the previous value on destruction.
*
* Construct with `true` to force the `Number`-based normalization path, or
* `false` to force the legacy loop. Useful in tests and ledger-replay code
* that must exercise a specific path without global side effects.
*
* @note Non-copyable; intended for stack-only use.
* @see getSTNumberSwitchover, setSTNumberSwitchover
*/
class NumberSO
{
bool saved_;

View File

@@ -1,3 +1,18 @@
/** @file
* Single authoritative source for computing the 256-bit ledger-state addresses
* of every object type in the XRP Ledger.
*
* All key derivations use "tagged hashing": a `sha512Half` over a type-specific
* `LedgerNameSpace` discriminator prepended to the object's identifying
* parameters. This prevents cross-type key collisions even when two object
* types share identical parameter values. The namespace discriminators are
* protocol-immutable; changing them constitutes a hard fork.
*
* The primary API is the `xrpl::keylet` namespace, whose functions return
* `Keylet` values pairing a 256-bit key with its expected `LedgerEntryType`.
* Free functions below the namespace (`getBookBase`, `getQuality`, etc.) are
* deprecated predecessors retained for backward compatibility.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
@@ -17,84 +32,141 @@
namespace xrpl {
class SeqProxy;
/** Keylet computation functions.
Entries in the ledger are located using 256-bit locators. The locators are
calculated using a wide range of parameters specific to the entry whose
locator we are calculating (e.g. an account's locator is derived from the
account's address, whereas the locator for an offer is derived from the
account and the offer sequence.)
To enhance type safety during lookup and make the code more robust, we use
keylets, which contain not only the locator of the object but also the type
of the object being referenced.
These functions each return a type-specific keylet.
*/
/** Keylet computation functions for every XRPL ledger object type.
*
* Entries in the ledger are located using 256-bit keys derived by hashing
* object-specific parameters under a type-specific namespace discriminator.
* Each function in this namespace returns a `Keylet` — a pair of the derived
* key and the expected `LedgerEntryType` — enabling type-safe ledger lookups
* that catch category errors at retrieval time via `Keylet::check()`.
*
* @note All namespace discriminator values are part of the consensus protocol
* and must never be changed. Adding a new keylet function requires
* assigning a new, previously unused discriminator character.
*/
namespace keylet {
/** AccountID root */
/** Return the keylet for an AccountRoot ledger entry.
*
* @param id The account address.
* @return Keylet typed `ltACCOUNT_ROOT`.
*/
Keylet
account(AccountID const& id) noexcept;
/** The index of the amendment table */
/** Return the keylet for the singleton amendments table.
*
* The amendments object has no parameters; its key is computed once and
* returned as a reference to a function-local static (Meyers singleton).
*
* @return Reference to a static `Keylet` typed `ltAMENDMENTS`.
*/
Keylet const&
amendments() noexcept;
/** Any item that can be in an owner dir. */
/** Return a wildcard keylet for any item that can appear in an owner directory.
*
* Uses `ltCHILD` so that `Keylet::check()` accepts any entry type —
* useful when iterating a directory without knowing the contained type.
*
* @param key Raw 256-bit ledger key of the directory child entry.
* @return Keylet typed `ltCHILD`.
*/
Keylet
child(uint256 const& key) noexcept;
/** The index of the "short" skip list
The "short" skip list is a node (at a fixed index) that holds the hashes
of ledgers since the last flag ledger. It will contain, at most, 256 hashes.
*/
/** Return the keylet for the "short" ledger-hash skip list.
*
* The short skip list is a singleton object holding the hashes of ledgers
* since the last flag ledger (at most 256 entries). Its key is computed
* once and returned as a reference to a function-local static.
*
* @return Reference to a static `Keylet` typed `ltLEDGER_HASHES`.
*/
Keylet const&
skip() noexcept;
/** The index of the long skip for a particular ledger range.
The "long" skip list is a node that holds the hashes of (up to) 256 flag
ledgers.
It can be used to efficiently skip back to any ledger using only two hops:
the first hop gets the "long" skip list for the ledger it wants to retrieve
and uses it to get the hash of the flag ledger whose short skip list will
contain the hash of the requested ledger.
*/
/** Return the keylet for a "long" ledger-hash skip list page.
*
* Each long skip list page stores hashes of up to 256 flag ledgers within
* a 65536-ledger range. Together with the short skip list, any historical
* ledger can be located in at most two hops: one to the long skip list for
* the target range, one to the short skip list around the target ledger.
*
* @param ledger Any ledger index within the desired 65536-ledger range;
* only the upper 16 bits determine the page key.
* @return Keylet typed `ltLEDGER_HASHES` for the corresponding skip-list page.
*/
Keylet
skip(LedgerIndex ledger) noexcept;
/** The (fixed) index of the object containing the ledger fees. */
/** Return the keylet for the singleton fee-settings object.
*
* Its key is computed once and returned as a reference to a function-local
* static (Meyers singleton).
*
* @return Reference to a static `Keylet` typed `ltFEE_SETTINGS`.
*/
Keylet const&
fees() noexcept;
/** The (fixed) index of the object containing the ledger negativeUNL. */
/** Return the keylet for the singleton negative-UNL object.
*
* Its key is computed once and returned as a reference to a function-local
* static (Meyers singleton).
*
* @return Reference to a static `Keylet` typed `ltNEGATIVE_UNL`.
*/
Keylet const&
negativeUNL() noexcept;
/** The beginning of an order book */
/** Functor that returns the root keylet for an order book directory.
*
* The returned keylet encodes quality 0 in the last 8 bytes of the key,
* making it the floor of the book's range in the SHAMap. Use `kBOOK`
* (the pre-constructed singleton instance) rather than constructing directly.
*
* @see keylet::quality
*/
struct BookT
{
explicit BookT() = default;
/** Return the keylet for the root directory page of @p b.
*
* @param b Order book specifying the in/out asset pair and optional domain.
* @return Keylet typed `ltDIR_NODE` with quality 0 in the last 8 bytes.
*/
Keylet
operator()(Book const& b) const;
};
static BookT const kBOOK{};
/** The index of a trust line for a given currency
Note that a trustline is *shared* between two accounts (commonly referred
to as the issuer and the holder); if Alice sets up a trust line to Bob for
BTC, and Bob trusts Alice for BTC, here is only a single BTC trust line
between them.
*/
/** Return the keylet for a trust line (RippleState) between two accounts.
*
* A trust line is a bilateral ledger object shared by both accounts. The
* two account IDs are sorted before hashing so that `line(Alice, Bob, USD)`
* and `line(Bob, Alice, USD)` produce the same key.
*
* @note `id0 == id1` is permitted (TrustSet may look up and delete malformed
* self-trust lines); the absence of a strict inequality assert is intentional.
*
* @param id0 One account on the trust line.
* @param id1 The other account on the trust line.
* @param currency Currency of the trust line.
* @return Keylet typed `ltRIPPLE_STATE`.
*/
/** @{ */
Keylet
line(AccountID const& id0, AccountID const& id1, Currency const& currency) noexcept;
/** Return the keylet for the trust line between @p id and the issuer of @p issue.
*
* @param id One of the two accounts on the trust line.
* @param issue Issue whose account and currency identify the trust line.
* @return Keylet typed `ltRIPPLE_STATE`.
*/
inline Keylet
line(AccountID const& id, Issue const& issue) noexcept
{
@@ -102,11 +174,21 @@ line(AccountID const& id, Issue const& issue) noexcept
}
/** @} */
/** An offer from an account */
/** Return the keylet for an offer placed by an account.
*
* @param id Account that placed the offer.
* @param seq Sequence number of the OfferCreate transaction.
* @return Keylet typed `ltOFFER`.
*/
/** @{ */
Keylet
offer(AccountID const& id, std::uint32_t seq) noexcept;
/** Return a typed keylet for an offer from its pre-computed key.
*
* @param key Pre-computed 256-bit offer key.
* @return Keylet typed `ltOFFER`.
*/
inline Keylet
offer(uint256 const& key) noexcept
{
@@ -114,31 +196,77 @@ offer(uint256 const& key) noexcept
}
/** @} */
/** The initial directory page for a specific quality */
/** Return the keylet for an order-book directory page at a specific quality.
*
* Writes @p q as a big-endian 64-bit value into the last 8 bytes of the
* book's base key. Because `uint256` keys sort as big-endian integers in
* the SHAMap, adjacent quality levels occupy adjacent addresses, enabling
* O(1) price-level iteration without a secondary index.
*
* @param k Base keylet for the order book (must be `ltDIR_NODE`).
* @param q 64-bit quality value (inverted exchange rate) to embed.
* @return Keylet typed `ltDIR_NODE` with @p q encoded in the last 8 bytes.
*/
Keylet
quality(Keylet const& k, std::uint64_t q) noexcept;
/** The directory for the next lower quality */
/** Functor that advances a book-directory keylet to the next quality level.
*
* Adds a unit to the 64-bit quality field embedded in the last 8 bytes of
* the key, stepping to the directory for the next higher quality tier in
* the same order book. Use `kNEXT` (the pre-constructed singleton instance)
* rather than constructing directly.
*
* @see keylet::quality
*/
struct NextT
{
explicit NextT() = default;
/** Return the keylet for the next quality tier above @p k.
*
* @param k A directory keylet (must be `ltDIR_NODE`) whose last 8 bytes
* encode a quality value.
* @return Keylet typed `ltDIR_NODE` with quality incremented by 1.
*/
Keylet
operator()(Keylet const& k) const;
};
static NextT const kNEXT{};
/** A ticket belonging to an account */
/** Functor that computes ticket keylets.
*
* Use `kTICKET` (the pre-constructed singleton instance) rather than
* constructing directly.
*/
struct TicketT
{
explicit TicketT() = default;
/** Return the keylet for a ticket owned by @p id.
*
* @param id Owner of the ticket.
* @param ticketSeq Sequence number consumed when the ticket was created.
* @return Keylet typed `ltTICKET`.
*/
Keylet
operator()(AccountID const& id, std::uint32_t ticketSeq) const;
/** Return the keylet for a ticket owned by @p id, resolved via a SeqProxy.
*
* @param id Owner of the ticket.
* @param ticketSeq SeqProxy in ticket mode; asserts if it represents a
* plain sequence number.
* @return Keylet typed `ltTICKET`.
*/
Keylet
operator()(AccountID const& id, SeqProxy ticketSeq) const;
/** Return a typed keylet for a ticket from its pre-computed key.
*
* @param key Pre-computed 256-bit ticket key.
* @return Keylet typed `ltTICKET`.
*/
Keylet
operator()(uint256 const& key) const
{
@@ -147,15 +275,29 @@ struct TicketT
};
static TicketT const kTICKET{};
/** A SignerList */
/** Return the keylet for an account's multi-signature signer list.
*
* @param account Account whose signer list is being addressed.
* @return Keylet typed `ltSIGNER_LIST` for page 0 (the only allocated page).
*/
Keylet
signers(AccountID const& account) noexcept;
/** A Check */
/** Return the keylet for a Check issued by an account.
*
* @param id Account that created the check (via CheckCreate).
* @param seq Sequence number of the CheckCreate transaction.
* @return Keylet typed `ltCHECK`.
*/
/** @{ */
Keylet
check(AccountID const& id, std::uint32_t seq) noexcept;
/** Return a typed keylet for a check from its pre-computed key.
*
* @param key Pre-computed 256-bit check key.
* @return Keylet typed `ltCHECK`.
*/
inline Keylet
check(uint256 const& key) noexcept
{
@@ -163,16 +305,45 @@ check(uint256 const& key) noexcept
}
/** @} */
/** A DepositPreauth */
/** Return the keylet for a deposit pre-authorization record.
*
* Two overloads exist for the two pre-authorization modes — account-to-account
* and credential-set — which hash under distinct namespace discriminators to
* prevent key collisions even when the `owner` is identical.
*/
/** @{ */
/** Return the keylet for a single-account deposit pre-authorization.
*
* @param owner Account granting the pre-authorization.
* @param preauthorized Account being pre-authorized to deposit.
* @return Keylet typed `ltDEPOSIT_PREAUTH`.
*/
Keylet
depositPreauth(AccountID const& owner, AccountID const& preauthorized) noexcept;
/** Return the keylet for a credential-set deposit pre-authorization.
*
* Each credential in @p authCreds is hashed individually as
* `sha512Half(issuer, credentialType)`; the resulting hashes are then passed
* to the outer hash under the `DepositPreauthCredentials` namespace, which
* is distinct from the account-to-account `DepositPreauth` namespace.
* Because `authCreds` is a `std::set`, iteration order is deterministic and
* the key is stable regardless of insertion order.
*
* @param owner Account granting the pre-authorization.
* @param authCreds Sorted set of (issuer AccountID, credentialType) pairs.
* @return Keylet typed `ltDEPOSIT_PREAUTH`.
*/
Keylet
depositPreauth(
AccountID const& owner,
std::set<std::pair<AccountID, Slice>> const& authCreds) noexcept;
/** Return a typed keylet for a deposit pre-auth entry from its pre-computed key.
*
* @param key Pre-computed 256-bit deposit-preauth key.
* @return Keylet typed `ltDEPOSIT_PREAUTH`.
*/
inline Keylet
depositPreauth(uint256 const& key) noexcept
{
@@ -182,19 +353,48 @@ depositPreauth(uint256 const& key) noexcept
//------------------------------------------------------------------------------
/** Any ledger entry */
/** Return a keylet for any ledger entry without type enforcement.
*
* Uses `ltANY` so `Keylet::check()` accepts any entry type. Intended for
* low-level read paths that need to fetch an entry before its type is known.
*
* @param key Raw 256-bit ledger key.
* @return Keylet typed `ltANY`.
*/
Keylet
unchecked(uint256 const& key) noexcept;
/** The root page of an account's directory */
/** Return the keylet for the root page of an account's owner directory.
*
* The owner directory lists all objects owned by the account (offers, trust
* lines, escrows, etc.). Subsequent pages beyond page 0 are keyed via
* `keylet::page`.
*
* @param id Account whose owner directory is being addressed.
* @return Keylet typed `ltDIR_NODE`.
*/
Keylet
ownerDir(AccountID const& id) noexcept;
/** A page in a directory */
/** Return the keylet for a specific page within a directory.
*
* Page 0 is stored at the root key itself; pages 1+ are stored at keys
* derived by hashing the root key with the page index.
*
* @param root 256-bit key of the directory's root page.
* @param index Zero-based page index; 0 returns the root key unchanged.
* @return Keylet typed `ltDIR_NODE`.
*/
/** @{ */
Keylet
page(uint256 const& root, std::uint64_t index = 0) noexcept;
/** Return the keylet for a specific page within a directory, from a root keylet.
*
* @param root Keylet of the directory's root page (must be `ltDIR_NODE`).
* @param index Zero-based page index.
* @return Keylet typed `ltDIR_NODE`.
*/
inline Keylet
page(Keylet const& root, std::uint64_t index = 0) noexcept
{
@@ -203,176 +403,477 @@ page(Keylet const& root, std::uint64_t index = 0) noexcept
}
/** @} */
/** An escrow entry */
/** Return the keylet for an escrow conditional payment.
*
* @param src Account that created the escrow.
* @param seq Sequence number of the EscrowCreate transaction.
* @return Keylet typed `ltESCROW`.
*/
Keylet
escrow(AccountID const& src, std::uint32_t seq) noexcept;
/** A PaymentChannel */
/** Return the keylet for an XRP payment channel.
*
* @param src Funding (source) account.
* @param dst Receiving (destination) account.
* @param seq Sequence number of the PaymentChannelCreate transaction.
* @return Keylet typed `ltPAYCHAN`.
*/
Keylet
payChan(AccountID const& src, AccountID const& dst, std::uint32_t seq) noexcept;
/** NFT page keylets
Unlike objects whose ledger identifiers are produced by hashing data,
NFT page identifiers are composite identifiers, consisting of the owner's
160-bit AccountID, followed by a 96-bit value that determines which NFT
tokens are candidates for that page.
/** NFT page keylets.
*
* Unlike other ledger objects whose keys are produced by hashing, NFT page
* keys are composite values: the high 160 bits hold the owner's `AccountID`
* and the low 96 bits are a range tag derived from an NFToken ID. This
* composite structure enables bounded range scans over all of an owner's NFT
* pages in the SHAMap without a linked-list traversal.
*/
/** @{ */
/** A keylet for the owner's first possible NFT page. */
/** Return the keylet for the owner's lowest possible NFT page (low 96 bits = 0).
*
* This is the floor of the owner's page range. It is normally impossible to
* create an actual NFT page at this key, but it is used in invariant tests
* to exercise the full page range.
*
* @param owner Account that owns the NFT collection.
* @return Keylet typed `ltNFTOKEN_PAGE` with low 96 bits all zero.
*/
Keylet
nftpageMin(AccountID const& owner);
/** A keylet for the owner's last possible NFT page. */
/** Return the keylet for the owner's highest possible NFT page (low 96 bits = all ones).
*
* Together with `nftpageMin`, this defines the closed interval covering every
* NFT page belonging to this owner.
*
* @param owner Account that owns the NFT collection.
* @return Keylet typed `ltNFTOKEN_PAGE` with low 96 bits all one.
*/
Keylet
nftpageMax(AccountID const& owner);
/** Return the keylet for the NFT page that should contain @p token.
*
* Preserves the owner prefix from @p k (high 160 bits) and replaces the
* range tag (low 96 bits) with the corresponding bits of @p token masked
* by `nft::pageMask`.
*
* @param k An NFT page keylet for the same owner (must be `ltNFTOKEN_PAGE`).
* @param token 256-bit NFToken ID whose low 96 bits determine the target page.
* @return Keylet typed `ltNFTOKEN_PAGE` for the page whose range covers @p token.
*/
Keylet
nftpage(Keylet const& k, uint256 const& token);
/** @} */
/** An offer from an account to buy or sell an NFT */
/** Return the keylet for an NFToken buy or sell offer.
*
* @param owner Account that created the offer.
* @param seq Sequence number of the NFTokenCreateOffer transaction.
* @return Keylet typed `ltNFTOKEN_OFFER`.
*/
/** @{ */
Keylet
nftoffer(AccountID const& owner, std::uint32_t seq);
/** Return a typed keylet for an NFToken offer from its pre-computed key.
*
* @param offer Pre-computed 256-bit NFToken offer key.
* @return Keylet typed `ltNFTOKEN_OFFER`.
*/
inline Keylet
nftoffer(uint256 const& offer)
{
return {ltNFTOKEN_OFFER, offer};
}
/** @} */
/** The directory of buy offers for the specified NFT */
/** Return the keylet for the directory of buy offers for an NFToken.
*
* @param id 256-bit NFToken ID.
* @return Keylet typed `ltDIR_NODE` for the buy-offer directory.
*/
Keylet
nftBuys(uint256 const& id) noexcept;
/** The directory of sell offers for the specified NFT */
/** Return the keylet for the directory of sell offers for an NFToken.
*
* @param id 256-bit NFToken ID.
* @return Keylet typed `ltDIR_NODE` for the sell-offer directory.
*/
Keylet
nftSells(uint256 const& id) noexcept;
/** AMM entry */
/** Return the keylet for an AMM pool, keyed by its two pooled assets.
*
* The two assets are sorted via `std::minmax` before hashing, so
* `amm(A, B)` and `amm(B, A)` always produce the same keylet.
* All four combinations of `Issue`/`MPTIssue` asset pairs are supported.
*
* @param issue1 One of the two pooled assets.
* @param issue2 The other pooled asset.
* @return Keylet typed `ltAMM`.
*/
/** @{ */
Keylet
amm(Asset const& issue1, Asset const& issue2) noexcept;
/** Return the keylet for an AMM pool from a pre-computed 256-bit AMM ID.
*
* Use this overload when the AMM ID is already available (e.g. stored in
* `sfAMMID` on another SLE) to avoid redundant hashing.
*
* @param amm Pre-computed 256-bit AMM identifier.
* @return Keylet typed `ltAMM`.
*/
Keylet
amm(uint256 const& amm) noexcept;
/** @} */
/** A keylet for Delegate object */
/** Return the keylet for a delegation grant from one account to another.
*
* @param account Account granting delegated authority.
* @param authorizedAccount Account receiving the delegated authority.
* @return Keylet typed `ltDELEGATE`.
*/
Keylet
delegate(AccountID const& account, AccountID const& authorizedAccount) noexcept;
/** Return the keylet for a cross-chain bridge object.
*
* A door account may host multiple bridges. The key encodes the door
* account and the currency appropriate to @p chainType, ensuring at most
* one bridge per currency per side.
*
* @param bridge Bridge descriptor containing door accounts and issues.
* @param chainType Selects whether to key on the locking or issuing chain side.
* @return Keylet typed `ltBRIDGE`.
*/
Keylet
bridge(STXChainBridge const& bridge, STXChainBridge::ChainType chainType);
// `seq` is stored as `sfXChainClaimID` in the object
/** Return the keylet for a cross-chain claim ID.
*
* The key encodes the full bridge identity plus the sequential claim ID
* stored as `sfXChainClaimID` in the object.
*
* @param bridge Bridge descriptor.
* @param seq Sequential claim ID (`sfXChainClaimID`).
* @return Keylet typed `ltXCHAIN_OWNED_CLAIM_ID`.
*/
Keylet
xChainClaimID(STXChainBridge const& bridge, std::uint64_t seq);
// `seq` is stored as `sfXChainAccountCreateCount` in the object
/** Return the keylet for a cross-chain create-account claim ID.
*
* Analogous to `xChainClaimID` but for the create-account workflow. The
* sequential counter is stored as `sfXChainAccountCreateCount` in the object.
*
* @param bridge Bridge descriptor.
* @param seq Sequential create-account claim ID (`sfXChainAccountCreateCount`).
* @return Keylet typed `ltXCHAIN_OWNED_CREATE_ACCOUNT_CLAIM_ID`.
*/
Keylet
xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq);
/** Return the keylet for an account's DID (Decentralized Identifier) document.
*
* @param account Account that owns the DID.
* @return Keylet typed `ltDID`.
*/
Keylet
did(AccountID const& account) noexcept;
/** Return the keylet for a price oracle owned by an account.
*
* An account may own multiple oracles distinguished by unique document IDs.
*
* @param account Account that owns the oracle.
* @param documentID Application-defined identifier distinguishing oracles
* within the same account (`sfOracleDocumentID`).
* @return Keylet typed `ltORACLE`.
*/
Keylet
oracle(AccountID const& account, std::uint32_t const& documentID) noexcept;
/** Return the keylet for a verifiable credential.
*
* @param subject Account the credential was issued to.
* @param issuer Account that issued the credential.
* @param credType Application-defined credential type byte string.
* @return Keylet typed `ltCREDENTIAL`.
*/
/** @{ */
Keylet
credential(AccountID const& subject, AccountID const& issuer, Slice const& credType) noexcept;
/** Return a typed keylet for a credential from its pre-computed key.
*
* @param key Pre-computed 256-bit credential key.
* @return Keylet typed `ltCREDENTIAL`.
*/
inline Keylet
credential(uint256 const& key) noexcept
{
return {ltCREDENTIAL, key};
}
/** @} */
/** Return the keylet for an MPT issuance, identified by sequence and issuer.
*
* Constructs the `MPTID` from @p seq and @p issuer via `makeMptID`, then
* delegates to the `MPTID` overload.
*
* @param seq Issuer's account sequence number at the time of issuance creation.
* @param issuer Account that created the issuance.
* @return Keylet typed `ltMPTOKEN_ISSUANCE`.
*/
/** @{ */
Keylet
mptIssuance(std::uint32_t seq, AccountID const& issuer) noexcept;
/** Return the keylet for an MPT issuance from a pre-built MPTID.
*
* @param issuanceID 192-bit MPT issuance identifier (see `makeMptID`).
* @return Keylet typed `ltMPTOKEN_ISSUANCE`.
*/
Keylet
mptIssuance(MPTID const& issuanceID) noexcept;
/** Return a typed keylet for an MPT issuance from its pre-computed key.
*
* @param issuanceKey Pre-computed 256-bit issuance key.
* @return Keylet typed `ltMPTOKEN_ISSUANCE`.
*/
inline Keylet
mptIssuance(uint256 const& issuanceKey)
{
return {ltMPTOKEN_ISSUANCE, issuanceKey};
}
/** @} */
/** Return the keylet for a holder's MPToken balance entry.
*
* MPToken entries are keyed under the `MPToken` namespace by hashing the
* issuance's 256-bit ledger key together with the holder's `AccountID`.
* This naturally groups all token balances under their issuance in the
* SHAMap hash space.
*
* @param issuanceID 192-bit MPTID identifying the issuance.
* @param holder Account holding the MPToken balance.
* @return Keylet typed `ltMPTOKEN`.
*/
/** @{ */
Keylet
mptoken(MPTID const& issuanceID, AccountID const& holder) noexcept;
/** Return a typed keylet for an MPToken entry from its pre-computed key.
*
* @param mptokenKey Pre-computed 256-bit MPToken key.
* @return Keylet typed `ltMPTOKEN`.
*/
inline Keylet
mptoken(uint256 const& mptokenKey)
{
return {ltMPTOKEN, mptokenKey};
}
/** Return the keylet for a holder's MPToken balance entry, identified by issuance key.
*
* Use this overload when the issuance's 256-bit ledger key is already available
* to avoid redundant hashing through `makeMptID` and `mptIssuance`.
*
* @param issuanceKey 256-bit key of the `MPTokenIssuance` SLE.
* @param holder Account holding the MPToken balance.
* @return Keylet typed `ltMPTOKEN`.
*/
Keylet
mptoken(uint256 const& issuanceKey, AccountID const& holder) noexcept;
/** @} */
/** Return the keylet for a single-asset vault.
*
* @param owner Account that created the vault.
* @param seq Sequence number of the VaultCreate transaction.
* @return Keylet typed `ltVAULT`.
*/
/** @{ */
Keylet
vault(AccountID const& owner, std::uint32_t seq) noexcept;
/** Return a typed keylet for a vault from its pre-computed key.
*
* @param vaultKey Pre-computed 256-bit vault key.
* @return Keylet typed `ltVAULT`.
*/
inline Keylet
vault(uint256 const& vaultKey)
{
return {ltVAULT, vaultKey};
}
/** @} */
/** Return the keylet for a loan broker created by an account.
*
* @param owner Account that created the loan broker.
* @param seq Sequence number of the LoanBrokerCreate transaction.
* @return Keylet typed `ltLOAN_BROKER`.
*/
/** @{ */
Keylet
loanbroker(AccountID const& owner, std::uint32_t seq) noexcept;
/** Return a typed keylet for a loan broker from its pre-computed key.
*
* @param key Pre-computed 256-bit loan broker key.
* @return Keylet typed `ltLOAN_BROKER`.
*/
inline Keylet
loanbroker(uint256 const& key)
{
return {ltLOAN_BROKER, key};
}
/** @} */
/** Return the keylet for an individual loan issued by a loan broker.
*
* @param loanBrokerID 256-bit key of the parent `LoanBroker` SLE.
* @param loanSeq Sequential loan number assigned by the broker.
* @return Keylet typed `ltLOAN`.
*/
/** @{ */
Keylet
loan(uint256 const& loanBrokerID, std::uint32_t loanSeq) noexcept;
/** Return a typed keylet for a loan from its pre-computed key.
*
* @param key Pre-computed 256-bit loan key.
* @return Keylet typed `ltLOAN`.
*/
inline Keylet
loan(uint256 const& key)
{
return {ltLOAN, key};
}
/** @} */
/** Return the keylet for a permissioned domain owned by an account.
*
* @param account Account that created the permissioned domain.
* @param seq Sequence number of the PermissionedDomainSet transaction.
* @return Keylet typed `ltPERMISSIONED_DOMAIN`.
*/
/** @{ */
Keylet
permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;
/** Return the keylet for a permissioned domain from its pre-computed ID.
*
* Use this overload when the domain ID is already known (e.g. stored in
* `sfDomainID` on another SLE) to avoid recomputing the hash.
*
* @param domainID Pre-computed 256-bit domain key.
* @return Keylet typed `ltPERMISSIONED_DOMAIN`.
*/
Keylet
permissionedDomain(uint256 const& domainID) noexcept;
/** @} */
} // namespace keylet
// Everything below is deprecated and should be removed in favor of keylets:
/** Return the base 256-bit key for an order book directory (deprecated).
*
* The returned key has quality 0 embedded in its last 8 bytes. Prefer
* `keylet::kBOOK(book)` for new code, which wraps this result in a typed keylet.
*
* @param book Order book identifying the in/out asset pair and optional domain.
* @return Raw 256-bit key for the book's root directory.
* @deprecated Use `keylet::kBOOK(book)` instead.
*/
uint256
getBookBase(Book const& book);
/** Advance a book-directory key to the next quality level (deprecated).
*
* Adds a unit to the 64-bit quality field embedded in the last 8 bytes of
* @p uBase. Prefer `keylet::kNEXT(k)` for new code.
*
* @param uBase A book-directory key, typically from `getBookBase`.
* @return Key with quality incremented by 1.
* @deprecated Use `keylet::kNEXT(k)` instead.
*/
uint256
getQualityNext(uint256 const& uBase);
/** Extract the 64-bit quality value from a book-directory key (deprecated).
*
* Reads the last 8 bytes of @p uBase as a big-endian `uint64_t`, exploiting
* `base_uint`'s big-endian internal layout.
*
* @param uBase A book-directory key produced by `getBookBase` or `keylet::quality`.
* @return The 64-bit quality (inverted exchange rate) embedded in the key.
* @deprecated Callers should use `keylet::quality` for construction instead.
*/
// VFALCO This name could be better
std::uint64_t
getQuality(uint256 const& uBase);
/** Return the 256-bit ledger key for a ticket (deprecated).
*
* @param account Owner of the ticket.
* @param uSequence Sequence number consumed when the ticket was created.
* @return Raw key under the `Ticket` namespace.
* @deprecated Use `keylet::kTICKET(account, seq)` instead.
*/
uint256
getTicketIndex(AccountID const& account, std::uint32_t uSequence);
/** Return the 256-bit ledger key for a ticket from a SeqProxy (deprecated).
*
* @param account Owner of the ticket.
* @param ticketSeq SeqProxy in ticket mode.
* @return Raw key under the `Ticket` namespace.
* @deprecated Use `keylet::kTICKET(account, ticketSeq)` instead.
*/
uint256
getTicketIndex(AccountID const& account, SeqProxy ticketSeq);
/** Descriptor binding a keylet factory to its expected ledger-entry type name.
*
* Used exclusively by invariant tests (`Invariants_test.cpp`) to enumerate
* keylet functions and verify that the objects they address have the correct
* ledger entry type. Not part of the production ledger-access API.
*
* @tparam KeyletParams Parameter types of the wrapped keylet factory function.
*/
template <class... KeyletParams>
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init)
struct KeyletDesc
{
/** Keylet factory function for one ledger object type. */
std::function<Keylet(KeyletParams...)> function;
/** Expected `LedgerEntryType` name as a JSON static string, used to
* validate the type of the SLE retrieved at the computed key. */
json::StaticString expectedLEName;
/** Whether to include this keylet in invariant test coverage. */
bool includeInTests{};
};
// This list should include all of the keylet functions that take a single
// AccountID parameter.
/** All keylet functions that accept a single `AccountID` parameter.
*
* This array drives invariant tests that verify the ledger-entry type of
* the object addressed by each keylet function. When adding a new single-
* `AccountID` keylet, add an entry here so invariant tests automatically
* exercise it.
*
* @note `nftpageMin` is listed even though creating an actual NFT page at
* that key is normally impossible — the invariant checker tests for it
* regardless.
*/
std::array<KeyletDesc<AccountID const&>, 6> const kDIRECT_ACCOUNT_KEYLETS{
{{.function = &keylet::account, .expectedLEName = jss::AccountRoot, .includeInTests = false},
{.function = &keylet::ownerDir, .expectedLEName = jss::DirectoryNode, .includeInTests = true},
@@ -383,6 +884,17 @@ std::array<KeyletDesc<AccountID const&>, 6> const kDIRECT_ACCOUNT_KEYLETS{
{.function = &keylet::nftpageMax, .expectedLEName = jss::NFTokenPage, .includeInTests = true},
{.function = &keylet::did, .expectedLEName = jss::DID, .includeInTests = true}}};
/** Construct a 192-bit MPT issuance identifier from a sequence number and issuer.
*
* Packs a big-endian 32-bit @p sequence into the first 4 bytes of the `MPTID`,
* followed by the 20-byte @p account. The explicit endian conversion ensures
* canonical byte order for on-ledger storage and byte-by-byte comparison.
*
* @param sequence The issuer's account sequence number at issuance creation
* (stored as `sfSequence` in the `MPTokenIssuance` SLE).
* @param account The issuing account.
* @return 192-bit `MPTID` uniquely addressing this MPT issuance.
*/
MPTID
makeMptID(std::uint32_t sequence, AccountID const& account);

View File

@@ -4,20 +4,66 @@
namespace xrpl {
/** Manages the list of known inner object formats.
/** Singleton registry of field schemas for all XRPL inner object types.
*
* Inner objects are the structured sub-objects that appear nested inside
* transactions and ledger entries — for example, `sfSigner`, `sfSignerEntry`,
* `sfNFToken`, `sfAuctionSlot`, and `sfPriceData`. This registry plays the
* same role for those nested objects as `TxFormats` plays for top-level
* transactions: it maps each inner object's `SField` code to an `SOTemplate`
* that declares which child fields are `soeREQUIRED`, `soeOPTIONAL`, or
* `soeDEFAULT`.
*
* The key type is `int` (the integer field code returned by
* `SField::getCode()`) rather than a dedicated enum, because inner objects
* are already identified by their `SField` descriptors in wire format.
*
* The registry is complete and immutable after the first call to
* `getInstance()`. Duplicate key registration triggers a `LogicError` at
* static-init time. The returned `const&` from `getInstance()` is safe for
* concurrent reads without additional locking.
*
* @see TxFormats, LedgerFormats, SOTemplate
*/
class InnerObjectFormats : public KnownFormats<int, InnerObjectFormats>
{
private:
/** Create the object.
This will load the object with all the known inner object formats.
*/
/** Register all known inner object schemas.
*
* Each `add()` call maps an `SField`'s JSON name and integer field code
* to an `SOTemplate` that specifies the required, optional, and default
* child fields. The `SField` code doubles as the registry key, so no
* separate enumeration is needed.
*/
InnerObjectFormats();
public:
/** Return the process-wide singleton instance.
*
* Initialized on first call via a Meyer's function-local static; safe
* for concurrent access after construction. The object is immutable
* after it is returned for the first time.
*
* @return A `const` reference to the singleton registry.
*/
static InnerObjectFormats const&
getInstance();
/** Look up the field schema for a structured inner object.
*
* Translates an `SField` to its registered `SOTemplate` by matching on
* the field's integer code. The returned pointer is stable for the
* lifetime of the process; callers may cache it safely.
*
* Called by `STObject::makeInnerObject()` (amendment-gated on
* `fixInnerObjTemplate` / `fixInnerObjTemplate2`) and by
* `STObject::applyTemplateFromSField()` to enforce field-presence rules
* during construction and deserialization.
*
* @param sField The `SField` identifying the inner object type.
* @return A pointer to the matching `SOTemplate`, or `nullptr` if
* `sField` is not a registered inner object type.
*/
[[nodiscard]] SOTemplate const*
findSOTemplateBySField(SField const& sField) const;
};

View File

@@ -6,36 +6,113 @@
namespace xrpl {
/** A currency issued by an account.
@see Currency, AccountID, Issue, Book
*/
/** Identifies a specific currency as issued by a specific account.
*
* `Issue` is the minimal token identity tuple in the XRPL type system: a
* 160-bit `Currency` paired with a 160-bit `AccountID`. It is the building
* block for trust lines, order books, offer matching, and AMM pools.
*
* XRP is represented as a special-case `Issue` whose `currency` and
* `account` fields both carry their respective zero/sentinel values
* (`xrpCurrency()` and `xrpAccount()`). The equality and ordering
* operators ignore `account` when `currency` is XRP, so all XRP issues
* form a single equivalence class even if `account` carries a stale value.
*
* @note `Issue` is a peer to `MPTIssue`; both satisfy the `IssueType`
* concept in `Concepts.h` and can be held inside an `Asset` variant.
* @see MPTIssue, Asset, Book
*/
class Issue
{
public:
/** The 160-bit currency code. `xrpCurrency()` (all-zero) denotes XRP. */
Currency currency;
/** The account that issued this currency.
*
* Meaningful only for non-XRP issues. For XRP issues this field
* should carry `xrpAccount()` (all-zero); `isConsistent()` enforces
* that invariant, but the comparison operators are lenient and ignore
* this field when `currency` is XRP.
*/
AccountID account;
Issue() = default;
/** Constructs an issue from an explicit currency and issuer account.
*
* @param c The currency code.
* @param a The issuing account. Pass `xrpAccount()` when `c` is
* `xrpCurrency()`; use `isConsistent()` to verify the pair.
*/
Issue(Currency const& c, AccountID const& a) : currency(c), account(a)
{
}
/** Returns the issuing account.
*
* Provides a uniform accessor shared with `MPTIssue`, enabling generic
* algorithms to retrieve the issuer without a type dispatch. For XRP
* issues the returned value is `xrpAccount()` (all-zero).
*
* @return A reference to `account`.
*/
[[nodiscard]] AccountID const&
getIssuer() const
{
return account;
}
/** Returns a human-readable diagnostic string of the form
* `currency[/account]`.
*
* For XRP, only the currency string is returned. For IOU issues the
* account is appended after a slash, substituting `"0"` for
* `xrpAccount()` and `"1"` for `noAccount()` so structurally
* inconsistent issues are detectable in logs.
*
* @note Field order is `currency/account`, which is the reverse of
* `to_string(Issue)`. Use this for logging; use `setJson()` for
* canonical wire output.
* @return Diagnostic string; never empty.
*/
[[nodiscard]] std::string
getText() const;
/** Writes the canonical JSON representation into an existing object.
*
* Always sets `jv["currency"]`. Sets `jv["issuer"]` as a Base58Check
* account string only for non-XRP issues; XRP omits `"issuer"`
* entirely, which is the authoritative form expected by transaction
* JSON, RPC responses, and the binary codec.
*
* @param jv Output JSON object; existing keys are not cleared.
*/
void
setJson(json::Value& jv) const;
/** Returns `true` if this issue represents XRP, the native asset.
*
* Implemented as a full equality comparison against `xrpIssue()`.
* The underlying `operator==` short-circuits on `currency` alone for
* XRP, so `account` is not consulted.
*
* @return `true` iff `*this == xrpIssue()`.
*/
[[nodiscard]] bool
native() const;
/** Returns `true` if amounts of this issue are stored as integers.
*
* For `Issue`, only XRP uses integer (drop) representation; all IOU
* currencies use mantissa/exponent floating-point. Delegates entirely
* to `native()`.
*
* @note `MPTIssue::integral()` always returns `true`. The shared
* method name allows generic code to query integer-vs-float
* semantics without a type dispatch.
* @return `true` iff `native()`.
*/
[[nodiscard]] bool
integral() const;
@@ -43,21 +120,92 @@ public:
operator<=>(Issue const& lhs, Issue const& rhs);
};
/** Returns `true` if `ac.currency` and `ac.account` agree on XRP-ness.
*
* A well-formed XRP issue must carry `xrpCurrency()` and `xrpAccount()`
* (both all-zero). A well-formed IOU issue must carry a non-zero currency
* and a non-zero account. Cross-contamination — XRP currency with a real
* account, or a real currency with the XRP account sentinel — silently
* corrupts amount comparisons and offer-book matching.
*
* @note The equality and ordering operators are intentionally more lenient
* than this check; they ignore `account` whenever `currency` is XRP.
* Call `isConsistent()` on any `Issue` sourced from external input.
* @param ac The issue to validate.
* @return `true` iff `isXRP(ac.currency) == isXRP(ac.account)`.
*/
bool
isConsistent(Issue const& ac);
/** Returns a string of the form `account/currency`, or just the currency
* for XRP issues.
*
* @note Field order is `account/currency`, which is the reverse of
* `Issue::getText()` (`currency/account`). Both formats are in active
* use in different parts of the codebase; this one matches
* offer-book log lines and stream output.
* @param ac The issue to render.
* @return A non-empty string identifying the issue.
*/
std::string
to_string(Issue const& ac);
/** Returns the canonical wire-format JSON representation of an issue.
*
* Convenience wrapper around `Issue::setJson()`. The returned object
* contains a `"currency"` field and, for non-XRP issues, an `"issuer"`
* field holding the Base58Check-encoded account.
*
* @param is The issue to serialise.
* @return A new JSON object representing the issue.
*/
json::Value
toJson(Issue const& is);
/** Parses and validates an `Issue` from a JSON object.
*
* Performs layered validation in strict order:
* 1. `v` must be a JSON object.
* 2. `mpt_issuance_id` must be absent — its presence means the caller
* has accidentally routed MPT data into the wrong parser.
* 3. `"currency"` must be a string that parses to neither `badCurrency()`
* nor `noCurrency()`.
* 4. For XRP currency, `"issuer"` must be absent.
* 5. For non-XRP currencies, `"issuer"` must be a valid Base58Check
* account string.
*
* @param v The JSON value to parse; must be a JSON object.
* @return The parsed `Issue`.
* @throws std::runtime_error if `v` is not an object, or if
* `mpt_issuance_id` is present.
* @throws Json::error if any field is missing, the wrong type, or carries
* an invalid currency code or account string.
* @see toJson for the inverse operation.
*/
Issue
issueFromJson(json::Value const& v);
/** Writes the issue to a stream using the `to_string(Issue)` format.
*
* @param os The output stream.
* @param x The issue to write.
* @return `os`.
*/
std::ostream&
operator<<(std::ostream& os, Issue const& x);
/** Appends both `currency` and `account` to the hasher unconditionally.
*
* @note The XRP special case from `operator==` (ignoring `account` when
* `currency` is XRP) is deliberately not applied here. Consistent
* data — ensured by `isConsistent()` at ingestion — guarantees that
* XRP issues always carry `xrpAccount()`, so hashes are stable.
* Hashing an inconsistent XRP issue could produce a hash that matches
* equality but diverges from a canonical XRP issue's hash.
* @tparam Hasher A type satisfying the `beast::hash_append` concept.
* @param h The hasher to append to.
* @param r The issue whose fields are appended.
*/
template <class Hasher>
void
hash_append(Hasher& h, Issue const& r)
@@ -66,17 +214,35 @@ hash_append(Hasher& h, Issue const& r)
hash_append(h, r.currency, r.account);
}
/** Equality comparison. */
/** @{ */
/** Returns `true` if two issues represent the same asset.
*
* Currencies are compared first. When both currencies are XRP (all-zero),
* the `account` field is ignored — all XRP issues are equal regardless of
* any stale or partially-constructed account value. For IOU issues both
* fields must match exactly.
*
* @param lhs Left-hand issue.
* @param rhs Right-hand issue.
* @return `true` iff the two issues identify the same asset.
*/
[[nodiscard]] constexpr bool
operator==(Issue const& lhs, Issue const& rhs)
{
return (lhs.currency == rhs.currency) && (isXRP(lhs.currency) || lhs.account == rhs.account);
}
/** @} */
/** Strict weak ordering. */
/** @{ */
/** Provides a strict weak ordering over `Issue` values.
*
* Sorts by `currency` first. When currencies are equal and the currency is
* XRP, `std::weak_ordering::equivalent` is returned immediately so that all
* XRP issues form a single equivalence class regardless of the `account`
* field. For IOU issues with equal currencies, `account` is the
* tiebreaker.
*
* @param lhs Left-hand issue.
* @param rhs Right-hand issue.
* @return A `std::weak_ordering` value consistent with `operator==`.
*/
[[nodiscard]] constexpr std::weak_ordering
operator<=>(Issue const& lhs, Issue const& rhs)
{
@@ -88,11 +254,18 @@ operator<=>(Issue const& lhs, Issue const& rhs)
return (lhs.account <=> rhs.account);
}
/** @} */
//------------------------------------------------------------------------------
/** Returns an asset specifier that represents XRP. */
/** Returns the canonical `Issue` sentinel that represents XRP.
*
* The returned instance holds `xrpCurrency()` and `xrpAccount()` (both
* all-zero 160-bit values). The singleton is initialised once and returned
* by `const&`, which is thread-safe under C++11 guaranteed-initialisation
* semantics.
*
* @return A reference to the process-lifetime XRP issue singleton.
*/
inline Issue const&
xrpIssue()
{
@@ -100,7 +273,13 @@ xrpIssue()
return kISSUE;
}
/** Returns an asset specifier that represents no account and currency. */
/** Returns an `Issue` sentinel that represents the absence of an issue.
*
* Holds `noCurrency()` and `noAccount()`. Used in contexts where a
* missing or invalid issue must be represented without `std::optional`.
*
* @return A reference to the process-lifetime "no issue" singleton.
*/
inline Issue const&
noIssue()
{
@@ -108,6 +287,14 @@ noIssue()
return kISSUE;
}
/** Returns `true` if `issue` represents XRP, the native asset.
*
* Thin wrapper over `issue.native()`, providing the naming convention
* used throughout the codebase for XRP detection at all abstraction levels.
*
* @param issue The issue to test.
* @return `true` iff `issue.native()`.
*/
inline bool
isXRP(Issue const& issue)
{

View File

@@ -1,3 +1,12 @@
/** @file
* Discriminant enum and conversion utilities for XRPL's two signature schemes.
*
* Every key-management function in the protocol (generation, signing,
* verification) accepts a `KeyType` to select between the secp256k1 and
* ed25519 algorithms. This header is included by virtually every
* cryptographic interface in the protocol layer.
*/
#pragma once
#include <optional>
@@ -5,11 +14,38 @@
namespace xrpl {
/** Selects the cryptographic signature algorithm for a key pair.
*
* The XRPL supports two independent signature schemes: the Bitcoin-lineage
* secp256k1 elliptic curve and the modern ed25519 Edwards curve. The
* explicit integer values provide stable identifiers for internal storage and
* configuration that maps an integer to a key type, even though `KeyType`
* itself is not serialized directly on the wire.
*
* The choice of algorithm is self-describing in serialized key material:
* secp256k1 public keys begin with a compressed-point prefix byte (`0x02` or
* `0x03`), while ed25519 public keys carry a sentinel byte `0xED`.
*
* @see publicKeyType for recovering the algorithm from a serialized public key.
*/
enum class KeyType {
Secp256k1 = 0,
Ed25519 = 1,
Secp256k1 = 0, /**< Bitcoin-lineage elliptic curve; XRPL uses a custom
seed-to-key-pair derivation path. */
Ed25519 = 1, /**< Modern Edwards curve; uses a direct derivation from the
seed with no intermediate generator step. */
};
/** Parse a canonical string name into a `KeyType`.
*
* Recognises exactly `"secp256k1"` and `"ed25519"` (lower-case). Returns an
* empty optional for any other input rather than throwing, so callers at
* configuration-parse or RPC-request boundaries can compose the result with
* their own error-reporting logic without requiring exception handling.
*
* @param s The string to parse.
* @return The corresponding `KeyType`, or `std::nullopt` if `s` is not a
* recognised algorithm name.
*/
inline std::optional<KeyType>
keyTypeFromString(std::string const& s)
{
@@ -22,6 +58,17 @@ keyTypeFromString(std::string const& s)
return {};
}
/** Return the canonical lower-case string name for a `KeyType`.
*
* Returns `"INVALID"` — rather than `nullptr` or undefined behaviour — for
* any value that matches neither known enumerator. This defensive path is
* reachable because C++ permits arbitrary integers to be cast to an
* `enum class`, so a corrupt or adversarially crafted value must not cause
* unsafe access in logging or diagnostic paths.
*
* @param type The key type to convert.
* @return `"secp256k1"`, `"ed25519"`, or `"INVALID"`.
*/
inline char const*
to_string(KeyType type)
{
@@ -34,6 +81,18 @@ to_string(KeyType type)
return "INVALID";
}
/** Write a `KeyType` to a stream as its canonical string name.
*
* Templated on `Stream` rather than fixed to `std::ostream` so the operator
* works with Beast logging streams, test-harness formatters, and any other
* stream-like type without coupling this header to a concrete stream
* hierarchy.
*
* @tparam Stream Any type that supports `operator<<(char const*)`.
* @param s The destination stream.
* @param type The key type to write.
* @return `s`, to allow chaining.
*/
template <class Stream>
inline Stream&
operator<<(Stream& s, KeyType type)

View File

@@ -7,24 +7,72 @@ namespace xrpl {
class STLedgerEntry;
/** A pair of SHAMap key and LedgerEntryType.
A Keylet identifies both a key in the state map
and its ledger entry type.
@note Keylet is a portmanteau of the words key
and LET, an acronym for LedgerEntryType.
*/
/** Bundles the 256-bit SHAMap locator of a ledger object with its expected
* `LedgerEntryType`, making ledger lookups type-safe by construction.
*
* The name is a portmanteau of "key" and "LET" (LedgerEntryType). Callers
* never build a `Keylet` by hand — they use one of the factory functions in
* the `keylet::` namespace (see `Indexes.h`), each of which encapsulates the
* correct SHA-512Half derivation for a specific object type and returns a
* `Keylet` already annotated with the matching `LedgerEntryType`.
*
* The ledger view's `read()` method accepts a `Keylet` and calls `check()`
* before returning an entry, so an incorrect type annotation surfaces at
* the access point rather than silently yielding a mistyped object.
*
* Two sentinel types participate in the matching protocol but are never
* stored on-ledger:
* - `ltANY` — wildcard; bypasses type checking entirely (used by
* `keylet::unchecked`).
* - `ltCHILD` — matches any entry that is not a directory node, reflecting
* the semantics of owner-directory children.
*
* @see keylet namespace in `Indexes.h` for all factory functions.
*/
struct Keylet
{
/** 256-bit SHAMap key that locates the ledger entry in the state tree. */
uint256 key;
/** Expected `LedgerEntryType` of the entry at `key`.
*
* May be the sentinel `ltANY` (wildcard) or `ltCHILD` (any non-directory
* entry); all other values are concrete on-ledger types whose numeric
* identities are protocol-stable and consensus-critical.
*/
LedgerEntryType type;
/** Constructs a keylet from an explicit type and key.
*
* Prefer the factory functions in the `keylet::` namespace over calling
* this constructor directly; they ensure the correct key derivation
* formula is used for each ledger object type.
*
* @param type The expected `LedgerEntryType` of the addressed entry.
* @param key The 256-bit SHAMap key of the entry.
*/
Keylet(LedgerEntryType type, uint256 const& key) : key(key), type(type)
{
}
/** Returns true if the SLE matches the type */
/** Validates that a deserialized ledger entry corresponds to this keylet.
*
* Applies a three-tier match ordered from most-permissive to most-strict:
* - `ltANY`: always returns `true`; the caller bears full responsibility
* for type safety.
* - `ltCHILD`: returns `true` for any entry whose concrete type is not
* `ltDIR_NODE`. Directory nodes are structural bookkeeping objects; a
* directory child is definitionally something other than a directory.
* - Concrete type: requires both `sle.getType() == type` and
* `sle.key() == key`. This is the common case and gives the strongest
* safety guarantee.
*
* @param sle The deserialized ledger entry retrieved from the state map.
* @return `true` if `sle` legitimately corresponds to this keylet.
* @note `sle` must not itself carry `ltANY` or `ltCHILD` as its stored
* type; those are query-side sentinels, not real on-ledger types.
* An `XRPL_ASSERT` enforces this precondition in debug builds.
*/
[[nodiscard]] bool
check(STLedgerEntry const&) const;
};

View File

@@ -1,3 +1,13 @@
/**
* @file KnownFormats.h
* @brief Template base for XRPL protocol format registries.
*
* Declares `KnownFormats<KeyType, Derived>`, the shared infrastructure used
* by `TxFormats`, `LedgerFormats`, and `InnerObjectFormats` to register and
* look up the field schemas (`SOTemplate`) for every transaction type, ledger
* object type, and inner object type in the protocol.
*/
#pragma once
#include <xrpl/basics/contract.h>
@@ -11,22 +21,60 @@
namespace xrpl {
/** Manages a list of known formats.
Each format has a name, an associated KeyType (typically an enumeration),
and a predefined @ref SOElement.
@tparam KeyType The type of key identifying the format.
*/
/** Registry of protocol format schemas, keyed by a wire-protocol discriminant.
*
* Each concrete registry (transaction, ledger entry, inner object) inherits
* from this template and populates it with one `Item` per recognized format.
* At runtime the serialization and validation layers look up `Item` instances
* via `findByType()` or `findTypeByName()` to obtain the `SOTemplate` that
* governs which fields are required, optional, or default for that format.
*
* Concrete subclasses are singletons constructed during static initialization;
* registering the same `KeyType` value twice is a programming error caught via
* `logicError()` (process abort) at startup rather than at request time.
*
* @note `begin()` / `end()` expose the raw `forward_list` for use in tests.
* They are not part of the normal lookup API.
*
* @tparam KeyType Integral or enum type whose values are the wire-protocol
* discriminants for this family of formats (e.g. `TxType`,
* `LedgerEntryType`, or `int` for inner objects). The `static_assert`
* inside `Item`'s constructor enforces this constraint at compile time.
* @tparam Derived The concrete subclass (CRTP). Used solely to embed the
* subclass name in diagnostic messages via `beast::typeName<Derived>()`.
*
* @see TxFormats, LedgerFormats, InnerObjectFormats, SOTemplate
*/
template <class KeyType, class Derived>
class KnownFormats
{
public:
/** A known format.
/** A registered protocol format: name, wire-key, and field schema.
*
* Each `Item` bundles the human-readable format name (e.g. `"Payment"`),
* its `KeyType` discriminant (the integer embedded in the wire protocol),
* and the `SOTemplate` that specifies every field's presence requirement.
* `Item` instances are owned by `KnownFormats` and are never moved after
* construction; callers may hold `Item const*` pointers indefinitely.
*/
class Item
{
public:
/** Construct a format item and merge unique and common field lists.
*
* `uniqueFields` are specific to this format; `commonFields` are
* shared across all formats in the registry (e.g. ledger metadata
* fields). Both lists are forwarded into the `SOTemplate`.
*
* @note A `static_assert` enforces at compile time that `KeyType` is
* integral or an enum, preventing accidental use of arbitrary
* types as wire-protocol discriminants.
*
* @param name Human-readable format name (e.g. `"Payment"`).
* @param type Wire-protocol discriminant value.
* @param uniqueFields Fields specific to this format.
* @param commonFields Fields shared by all formats in this registry.
*/
Item(
char const* name,
KeyType type,
@@ -36,13 +84,13 @@ public:
, name_(name)
, type_(type)
{
// Verify that KeyType is appropriate.
// KeyType must map directly to a wire integer value.
static_assert(
std::is_enum_v<KeyType> || std::is_integral_v<KeyType>,
"KnownFormats KeyType must be integral or enum.");
}
/** Retrieve the name of the format.
/** Return the human-readable name of this format (e.g. `"Payment"`).
*/
[[nodiscard]] std::string const&
getName() const
@@ -50,7 +98,11 @@ public:
return name_;
}
/** Retrieve the transaction type this format represents.
/** Return the wire-protocol discriminant identifying this format.
*
* The returned value is the `KeyType` constant that was supplied at
* registration time — for example `ttPayment` for a transaction
* format, or `ltOFFER` for a ledger entry format.
*/
[[nodiscard]] KeyType
getType() const
@@ -58,6 +110,13 @@ public:
return type_;
}
/** Return the field schema for this format.
*
* The `SOTemplate` enumerates every field that may appear in a
* serialized object of this type, together with its `SOEStyle`
* (`soeREQUIRED`, `soeOPTIONAL`, or `soeDEFAULT`). The returned
* reference is stable for the lifetime of the process.
*/
[[nodiscard]] SOTemplate const&
getSOTemplate() const
{
@@ -70,32 +129,40 @@ public:
KeyType const type_;
};
/** Create the known formats object.
Derived classes will load the object with all the known formats.
*/
private:
/** Construct the registry and capture the concrete subclass name.
*
* The subclass name (obtained via `beast::typeName<Derived>()`) is
* stored in `name_` and prepended to diagnostic messages emitted by
* `findTypeByName()`, enabling errors like
* `"TxFormats: Unknown format name 'BadName'"`.
*
* Only `Derived` may construct this base (enforced by the `friend`
* declaration and the private access specifier).
*/
KnownFormats() : name_(beast::typeName<Derived>())
{
}
public:
/** Destroy the known formats object.
The defined formats are deleted.
*/
virtual ~KnownFormats() = default;
KnownFormats(KnownFormats const&) = delete;
KnownFormats&
operator=(KnownFormats const&) = delete;
/** Retrieve the type for a format specified by name.
If the format name is unknown, an exception is thrown.
@param name The name of the type.
@return The type.
*/
/** Return the wire-protocol key for a format looked up by name.
*
* Intended for use when parsing externally supplied strings (e.g. JSON
* RPC input or configuration files). An unknown name is treated as a
* recoverable application-level error and is reported as a
* `std::runtime_error` whose message includes the registry name and the
* (possibly truncated to 32 characters) unrecognized name.
*
* @param name The human-readable format name to look up.
* @return The `KeyType` value registered under that name.
* @throws std::runtime_error If `name` is not registered in this
* registry.
*/
[[nodiscard]] KeyType
findTypeByName(std::string const& name) const
{
@@ -106,7 +173,16 @@ public:
name.substr(0, std::min(name.size(), std::size_t(32))) + "'");
}
/** Retrieve a format based on its type.
/** Return the `Item` registered for the given wire-protocol key, or
* `nullptr` if no such format has been registered.
*
* Returns `nullptr` on a miss so that callers in internal code paths can
* handle an absent format with an idiomatic null check rather than a
* caught exception. Contrast with `findTypeByName()`, which throws for
* unknown names supplied from external input.
*
* @param type The wire-protocol discriminant to look up.
* @return Pointer to the matching `Item`, or `nullptr`.
*/
[[nodiscard]] Item const*
findByType(KeyType type) const
@@ -117,13 +193,21 @@ public:
return itr->second;
}
// begin() and end() are provided for testing purposes.
/** Return an iterator to the first registered `Item`.
*
* @note Exposed for testing only; do not rely on iteration order, which
* reflects reverse-registration sequence due to `emplace_front`.
*/
[[nodiscard]] typename std::forward_list<Item>::const_iterator
begin() const
{
return formats_.begin();
}
/** Return a past-the-end iterator for the registered `Item` sequence.
*
* @note Exposed for testing only.
*/
[[nodiscard]] typename std::forward_list<Item>::const_iterator
end() const
{
@@ -131,7 +215,14 @@ public:
}
protected:
/** Retrieve a format based on its name.
/** Return the `Item` registered under the given name, or `nullptr`.
*
* Protected so that external callers are directed to the public
* `findTypeByName()`, which enforces the exception-on-miss contract for
* externally supplied names.
*
* @param name The human-readable format name to look up.
* @return Pointer to the matching `Item`, or `nullptr`.
*/
[[nodiscard]] Item const*
findByName(std::string const& name) const
@@ -142,15 +233,28 @@ protected:
return itr->second;
}
/** Add a new format.
@param name The name of this format.
@param type The type of this format.
@param uniqueFields A std::vector of unique fields
@param commonFields A std::vector of common fields
@return The created format.
*/
/** Register a new format with this registry.
*
* Creates an `Item` by combining `uniqueFields` (specific to this
* format) and `commonFields` (shared across all formats in the registry)
* into a single `SOTemplate`. The new `Item` is inserted at the front
* of the owning `forward_list` to preserve pointer stability, then
* indexed by both name and type in the two `flat_map` lookup tables.
*
* Registering a `type` value that is already present is a programming
* error: `logicError()` (process abort) is called immediately, making
* the failure visible at static-initialization time before any requests
* are served.
*
* @param name Human-readable format name (e.g. `"Payment"`).
* @param type Wire-protocol discriminant; must be unique within
* this registry.
* @param uniqueFields Fields specific to this format.
* @param commonFields Fields shared by all formats in this registry;
* defaults to empty.
* @return A stable `const` reference to the newly created
* `Item`.
*/
Item const&
add(char const* name,
KeyType type,
@@ -174,14 +278,22 @@ protected:
}
private:
/** Concrete subclass name, captured at construction for diagnostic messages. */
std::string name_;
// One of the situations where a std::forward_list is useful. We want to
// store each Item in a place where its address won't change. So a node-
// based container is appropriate. But we don't need searchability.
/** Owning store for all registered `Item` instances.
*
* `std::forward_list` is used because node-based containers never
* relocate existing elements, keeping `Item` addresses stable after
* insertion. The `flat_map` indices below store raw pointers into this
* list; pointer stability is therefore a hard requirement.
*/
std::forward_list<Item> formats_{};
/** Name-to-item index for O(log n) lookup by human-readable format name. */
boost::container::flat_map<std::string, Item const*> names_{};
/** Type-to-item index for O(log n) lookup by wire-protocol discriminant. */
boost::container::flat_map<KeyType, Item const*> types_{};
friend Derived;
};

View File

@@ -1,3 +1,27 @@
/** @file
* Authoritative registry for every object type that can live in the XRP Ledger.
*
* Defines three tightly-coupled, protocol-level artifacts:
*
* 1. `LedgerEntryType` — the `uint16_t` wire discriminants stored inside every
* serialized ledger object.
* 2. `LedgerSpecificFlags` / per-object flag accessor functions / `getAllLedgerFlags()`
* — flag bitmasks that modify ledger object behavior, together with Meyer's-singleton
* accessors consumed by the `server_definitions` RPC endpoint.
* 3. `LedgerFormats` — the singleton registry that maps each `LedgerEntryType` to its
* `SOTemplate` (field presence schema).
*
* The `ledger_entries.macro` X-macro file is the single source of truth for all
* per-type data; this header and `LedgerFormats.cpp` each include it with a different
* macro definition to derive the enum and the format registration from the same table.
*
* @warning All numeric values defined here are embedded in serialized ledger objects
* and transmitted over the wire. Changing them without corresponding amendment
* machinery causes a hard fork.
*
* @ingroup protocol
*/
#pragma once
// NOLINTBEGIN(readability-identifier-naming)
@@ -9,28 +33,35 @@
#include <vector>
namespace xrpl {
/** Identifiers for on-ledger objects.
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 stored inside objects stored on the ledger they are part of the
protocol.
**Changing them should be avoided because without special handling, this will result in a hard
fork.**
@note Values outside this range may be used internally by the code for various purposes, but
attempting to use such values to identify on-ledger objects will result in an invariant failure.
@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
*/
/** Numeric type identifiers for every object type that can exist in the XRP Ledger.
*
* Each ledger object embeds its `LedgerEntryType` in the serialized form; this allows
* the ledger layer to determine an object's type during iteration and to verify that
* a hash lookup returned the expected kind of object.
*
* The concrete values are generated by the `ledger_entries.macro` X-macro, which is
* the single source of truth for (tag, value, name, fields) tuples across the enum,
* the `LedgerFormats` constructor, and any auto-generated protocol bindings.
*
* Beyond the macro-generated members, two sentinel pseudo-types (`ltANY`, `ltCHILD`)
* are defined manually for use in keylet lookups where the precise object type is
* unknown or irrelevant.
*
* @warning These values are stored in serialized ledger objects and are part of the
* protocol. Changing them without special amendment handling causes a hard fork.
*
* @note Values outside the known range may be used internally, but passing them to
* ledger-object APIs will result in an invariant failure.
*
* @note When retiring an entry type, mark its enumerator `[[deprecated]]` rather than
* removing it. Removal would free the numeric slot for accidental reuse.
*
* @todo C++ enums cannot enforce uniqueness of values at compile time; duplicate IDs
* can silently coexist. If the language gains that capability it should be used here.
*
* @ingroup protocol
*/
// Protocol-critical, hundreds of usages
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum LedgerEntryType : std::uint16_t {
@@ -96,16 +127,29 @@ enum LedgerEntryType : std::uint16_t {
0x0067,
};
/** Ledger object flags.
These flags are specified in ledger objects and modify their behavior.
@warning Ledger object flags form part of the protocol.
**Changing them should be avoided because without special handling, this will result in a hard
fork.**
@ingroup protocol
*/
/** Flat enum of all per-object flag bitmasks across every ledger entry type.
*
* Each enumerator is a named bit constant (e.g. `lsfRequireDestTag`, `lsfGlobalFreeze`)
* that modifies the behavior of a specific ledger object type. The constants are
* generated via the `XMACRO` / `TO_VALUE` pass below, which strips object-type grouping
* and collects every flag name and value into a single flat enum.
*
* @note `LSF_FLAG2` is used when the same bit value appears in more than one object
* type (currently `lsfMPTLocked = 0x00000001` shared between `MPTokenIssuance` and
* `MPToken`). The second occurrence is silently omitted from this enum via the
* `NULL_OUTPUT` helper to avoid a duplicate-enumerator warning, while still appearing
* in the per-object flag maps returned by the getter functions below.
*
* @note Most object types use flag bits starting at `0x00010000`, reserving the low 16
* bits for future use. `DirNode`, `NFTokenOffer`, and the MPToken family deviate
* from this convention and use the low-order bits — a legacy of their original
* feature designs.
*
* @warning These values are stored in serialized ledger objects and form part of the
* protocol. Changing them without amendment machinery causes a hard fork.
*
* @ingroup protocol
*/
#pragma push_macro("XMACRO")
#pragma push_macro("TO_VALUE")
#pragma push_macro("VALUE_TO_MAP")
@@ -222,19 +266,17 @@ enum LedgerEntryType : std::uint16_t {
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum LedgerSpecificFlags : std::uint32_t { XMACRO(NULL_NAME, TO_VALUE, NULL_OUTPUT) };
// 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 `getAllLedgerFlags()` to generate the server_definitions RPC output.
//
// example:
// inline LedgerFlagMap const& getAccountRootFlags() {
// static LedgerFlagMap const flags = {
// {"lsfPasswordSpent", 0x00010000},
// {"lsfRequireDestTag", 0x00020000},
// ...};
// return flags;
// }
/** Map from flag name string to its `uint32_t` bitmask value for a single ledger object type.
*
* Each entry has the form `{"lsfFlagName", 0xXXXXXXXX}`. Per-object maps are returned
* by the `get<ObjectType>Flags()` inline functions generated below and are aggregated by
* `getAllLedgerFlags()` for the `server_definitions` RPC response.
*/
using LedgerFlagMap = std::map<std::string, std::uint32_t>;
// Per-object flag getter functions: getAccountRootFlags(), getOfferFlags(), etc.
// Each returns a const LedgerFlagMap& initialized once via Meyer's singleton.
// See getAllLedgerFlags() for the aggregating accessor.
#define VALUE_TO_MAP(name, value) {#name, value},
#define TO_MAP(name, values) \
inline LedgerFlagMap const& get##name##Flags() \
@@ -244,16 +286,17 @@ using LedgerFlagMap = std::map<std::string, std::uint32_t>;
}
XMACRO(TO_MAP, VALUE_TO_MAP, VALUE_TO_MAP)
// Create a getter function for all ledger flag maps using Meyer's singleton pattern.
// This is used to generate the server_definitions RPC output.
//
// example:
// inline std::vector<std::pair<std::string, LedgerFlagMap>> const& getAllLedgerFlags() {
// static std::vector<std::pair<std::string, LedgerFlagMap>> const flags = {
// {"AccountRoot", getAccountRootFlags()},
// ...};
// return flags;
// }
/** Return the flags for all ledger object types, keyed by object type name.
*
* Aggregates every per-object `LedgerFlagMap` (produced by the `get<ObjectType>Flags()`
* inline functions above) into a single vector, where each element is a pair of
* `(object-type-name, flag-map)`. The vector is initialized once via Meyer's singleton.
*
* This function is the sole data source for the `server_definitions` RPC endpoint,
* which exposes the complete ledger flag catalogue to external API consumers.
*
* @return A stable `const` reference to the process-wide flag catalogue.
*/
#define ALL_LEDGER_FLAGS(name, values) {#name, get##name##Flags()},
inline std::vector<std::pair<std::string, LedgerFlagMap>> const&
getAllLedgerFlags()
@@ -280,21 +323,64 @@ getAllLedgerFlags()
//------------------------------------------------------------------------------
/** Holds the list of known ledger entry formats.
/** Singleton registry mapping every `LedgerEntryType` to its canonical field schema.
*
* Inherits from `KnownFormats<LedgerEntryType, LedgerFormats>` (CRTP), which provides
* O(log n) lookup by type and by name, duplicate-registration detection, and stable
* `Item` pointer identity.
*
* The registry is populated once, during static initialization, via the private
* constructor's X-macro pass over `ledger_entries.macro`. Every registered entry
* receives an `SOTemplate` built from its type-specific fields plus the three
* common fields returned by `getCommonFields()` (`sfLedgerIndex`, `sfLedgerEntryType`,
* `sfFlags`).
*
* Callers in the serialization, deserialization, and invariant-checking layers
* access the registry through `getInstance()` to look up schemas by type.
*
* @see KnownFormats, LedgerEntryType, SOTemplate
* @ingroup protocol
*/
class LedgerFormats : public KnownFormats<LedgerEntryType, LedgerFormats>
{
private:
/** Create the object.
This will load the object with all the known ledger formats.
*/
/** Populate the registry with all known ledger entry formats.
*
* Uses an X-macro pass over `ledger_entries.macro`, registering each entry type
* by calling `KnownFormats::add()` with the entry's name, `LedgerEntryType`
* discriminant, type-specific fields, and the common fields from `getCommonFields()`.
*
* If `ledger_entries.macro` contains a duplicate numeric type ID, `add()` calls
* `logicError()` (process abort) during static initialization rather than silently
* corrupting the registry.
*/
LedgerFormats();
public:
/** Return the process-wide `LedgerFormats` singleton.
*
* Uses a function-local static (Meyer's singleton) for thread-safe, once-only
* initialization guaranteed by C++11. The first call constructs the registry and
* registers every known ledger entry type; subsequent calls return the same instance.
*
* @return A `const` reference to the global `LedgerFormats` instance.
*/
static LedgerFormats const&
getInstance();
// Fields shared by all ledger entry formats:
/** Return the three fields that every ledger entry must carry.
*
* The common fields are:
* - `sfLedgerIndex` (`soeOPTIONAL`) — key of the entry in the SHAMap.
* - `sfLedgerEntryType` (`soeREQUIRED`) — wire discriminant; must always be present.
* - `sfFlags` (`soeREQUIRED`) — object flag bitmask; must always be present.
*
* These fields are injected into every `SOTemplate` by the constructor, so they do
* not need to be listed in each entry type's individual field set in `ledger_entries.macro`.
* The vector is initialized once on first call (function-local static).
*
* @return A stable `const` reference to the common-fields vector.
*/
static std::vector<SOElement> const&
getCommonFields();
};

View File

@@ -1,3 +1,13 @@
/** @file
* Defines `LedgerHeader`, the compact canonical summary of a single XRP
* Ledger, together with the serialization, deserialization, and hash
* calculation functions that operate on it.
*
* Every ledger — open, closed, or validated — is identified and
* authenticated through this structure. The serialized form is
* protocol-immutable: 118 bytes without the trailing hash, 150 with it.
*/
#pragma once
#include <xrpl/basics/Slice.h>
@@ -9,7 +19,23 @@
namespace xrpl {
/** Information about the notional ledger backing the view. */
/** Canonical metadata block that identifies a single XRP Ledger.
*
* Fields are split into two groups: those valid for all ledgers (including
* open ones that are still accumulating transactions) and those that are
* only meaningful once the ledger is closed (transaction set finalized).
*
* `LedgerHeader` is embedded inside the `ReadView`/`ApplyView` hierarchy
* and is accessible via `view.info()`. The struct is intentionally small so
* it can be cheaply copied, compared, and transmitted without loading the
* full account-state SHAMap.
*
* @note `validated` is `mutable` because it transitions one-way from
* `false` to `true` and must remain settable even on `const`-qualified
* ledger objects. This is a known design wart.
*
* @see calculateLedgerHash, addRaw, deserializeHeader
*/
struct LedgerHeader
{
explicit LedgerHeader() = default;
@@ -18,7 +44,12 @@ struct LedgerHeader
// For all ledgers
//
/** Monotonically increasing sequence number that identifies this ledger's
* position in the chain. */
LedgerIndex seq = 0;
/** Close time of the parent (previous) ledger, in `NetClock` seconds
* (epoch: 2000-01-01 00:00:00 UTC). */
NetClock::time_point parentCloseTime;
//
@@ -26,53 +57,158 @@ struct LedgerHeader
//
// Closed means "tx set already determined"
/** This ledger's own identity hash, computed by `calculateLedgerHash`.
* Meaningful only after the ledger is closed. */
uint256 hash = beast::kZERO;
/** SHAMap root hash of the transaction set for this ledger. */
uint256 txHash = beast::kZERO;
/** SHAMap root hash of the account-state tree after applying this
* ledger's transaction set. */
uint256 accountHash = beast::kZERO;
/** Hash of the immediately preceding ledger; links this ledger into the
* chain and is included in the signed hash. */
uint256 parentHash = beast::kZERO;
/** Total XRP in existence at this ledger, in drops (1 XRP = 10^6 drops). */
XRPAmount drops = beast::kZERO;
// If validated is false, it means "not yet validated."
// Once validated is true, it will never be set false at a later time.
// VFALCO TODO Make this not mutable
/** Whether this ledger has been confirmed by a quorum of validators.
* Transitions one-way from `false` to `true`; never reverts.
* Declared `mutable` so it can be set on `const`-qualified objects. */
bool mutable validated = false;
/** Whether this node has accepted the ledger's transaction set,
* independent of network-wide validation. */
bool accepted = false;
// flags indicating how this ledger close took place
/** Bitmask of close-time flags produced by the consensus round.
* The only defined bit is `kS_LCF_NO_CONSENSUS_TIME` (0x01).
* Serialized as a single `uint8_t`; only the low 8 bits are meaningful. */
int closeFlags = 0;
// the resolution for this ledger close time (2-120 seconds)
/** Granularity to which the close time was rounded, in seconds (2120).
* Determined by the consensus algorithm and stored per-ledger. */
NetClock::duration closeTimeResolution = {};
// For closed ledgers, the time the ledger
// closed. For open ledgers, the time the ledger
// will close if there's no transactions.
//
/** For closed ledgers: the time at which the ledger closed, in
* `NetClock` seconds. For open ledgers: the projected close time if
* no transactions arrive. */
NetClock::time_point closeTime;
};
// ledger close flags
/** Close-flag bit set when consensus could not agree on a close time.
*
* Written into `LedgerHeader::closeFlags` during `Ledger::setAccepted()`
* when the `correctCloseTime` argument is `false`. Queried via
* `getCloseAgree()`.
*/
static std::uint32_t const kS_LCF_NO_CONSENSUS_TIME = 0x01;
/** Return `true` if the consensus round agreed on a close time for this
* ledger.
*
* Returns `false` when `kS_LCF_NO_CONSENSUS_TIME` is set in
* `info.closeFlags`, which indicates the validator set was unable to
* reach agreement (e.g., significant clock skew or a very small validator
* set). Callers that require a reliable close time — such as the ledger
* replay subsystem — must guard on this value.
*
* @param info The ledger header to inspect.
* @return `true` if `kS_LCF_NO_CONSENSUS_TIME` is not set; `false`
* otherwise.
*/
inline bool
getCloseAgree(LedgerHeader const& info)
{
return (info.closeFlags & kS_LCF_NO_CONSENSUS_TIME) == 0;
}
/** Append the ledger header to a serializer in canonical network byte order.
*
* Field order (protocol-immutable): `seq` (32-bit), `drops` (64-bit),
* `parentHash`, `txHash`, `accountHash` (each 256-bit), `parentCloseTime`,
* `closeTime` (each 32-bit epoch seconds), `closeTimeResolution` (8-bit),
* `closeFlags` (8-bit). Total: 118 bytes. When `includeHash` is `true`,
* `hash` is appended as an additional 32 bytes.
*
* The hash is omitted by default to avoid circularity: it is derived from
* all other fields and must not be part of the input to
* `calculateLedgerHash`. Pass `includeHash = true` when persisting to the
* node store or transmitting over the wire so receivers can skip
* recomputing it.
*
* @note The field order here must exactly mirror `calculateLedgerHash`.
* They are not mechanically linked; a divergence silently breaks
* consensus-level hash agreement across the network.
* @note `validated` and `accepted` are runtime-only flags and are not
* written to the serializer.
*
* @param info The ledger header to serialize.
* @param s Accumulator that receives the serialized bytes.
* @param includeHash If `true`, append `info.hash` after all other fields.
*/
void
addRaw(LedgerHeader const&, Serializer&, bool includeHash = false);
/** Deserialize a ledger header from a byte array. */
/** Deserialize a ledger header from a raw byte buffer.
*
* Reads fields in the same order that `addRaw` writes them. Time fields
* are raw 32-bit epoch counts (seconds since 2000-01-01 00:00:00 UTC)
* and are restored to typed `NetClock::time_point` values. The runtime-only
* fields `validated` and `accepted` are left at their default values.
*
* No semantic validation is performed beyond what `SerialIter` enforces.
* The caller is responsible for verifying that the deserialized `hash`
* matches `calculateLedgerHash` before trusting the data.
*
* @param data View over the raw bytes to deserialize.
* @param hasHash If `true`, read a trailing 256-bit value into
* `LedgerHeader::hash`. Must match how the header was serialized.
* @return A populated `LedgerHeader`.
* @throws std::runtime_error (via `SerialIter`) if @p data is shorter
* than the expected field sequence.
*/
LedgerHeader
deserializeHeader(Slice data, bool hasHash = false);
/** Deserialize a ledger header (prefixed with 4 bytes) from a byte array. */
/** Deserialize a ledger header that is preceded by a 4-byte prefix.
*
* Skips the leading `HashPrefix` tag that is prepended when a ledger
* header is stored in the node database or transmitted in a peer-protocol
* message, then delegates to `deserializeHeader`.
*
* @param data View over the raw bytes, including the 4-byte prefix.
* @param hasHash Forwarded to `deserializeHeader`; see its documentation.
* @return A populated `LedgerHeader` as returned by `deserializeHeader`.
* @throws std::runtime_error (via `SerialIter`) if the buffer after
* skipping the prefix is too short.
*/
LedgerHeader
deserializePrefixedHeader(Slice data, bool hasHash = false);
/** Calculate the hash of a ledger header. */
/** Compute the canonical 256-bit identity hash for a ledger header.
*
* Feeds all header fields (except `hash` itself, `validated`, and
* `accepted`) into `sha512Half` — the first 256 bits of a SHA-512 digest
* — prepended with `HashPrefix::LedgerMaster` (`LWR\0`). The four-byte
* prefix provides hash-domain separation, preventing collisions with
* hashes computed over other XRPL object types.
*
* Each field is explicitly cast to its protocol-defined wire width before
* hashing, preventing silent integer widening from diverging from the
* network.
*
* @note The field order and widths here must exactly mirror `addRaw`.
* They are not mechanically linked; a divergence causes this node to
* compute hashes that disagree with the rest of the network.
*
* @param info The ledger header to hash.
* @return The 256-bit canonical ledger hash.
*/
uint256
calculateLedgerHash(LedgerHeader const& info);

View File

@@ -2,20 +2,57 @@
namespace xrpl {
/**
* @brief Enumeration of ledger shortcuts for specifying which ledger to use.
/** Symbolic names for the three canonical XRPL ledger states.
*
* These shortcuts provide a convenient way to reference commonly used ledgers
* without needing to specify their exact hash or sequence number.
* The XRPL consensus model maintains three distinct ledger states at any
* point in time. Rather than requiring callers to pass magic strings
* (`"current"`, `"closed"`, `"validated"`) or ad-hoc integer sentinels,
* `LedgerShortcut` gives the type system a precise vocabulary for expressing
* ledger-selection intent without a specific sequence number or hash.
*
* In `RPCLedgerHelpers.cpp`, `lookupLedger` parsing maps the JSON strings
* `"current"`, `"closed"`, and `"validated"` onto the corresponding enum
* values before dispatching to the appropriate `getLedger` overload. The
* `AccountTx` RPC handler performs the same mapping when processing the
* `ledger_index` field. The gRPC adapter maps protobuf shortcut constants to
* these values as well.
*
* `LedgerShortcut` also participates as one arm of
* `RelationalDatabase::LedgerSpecifier` — a
* `std::variant<LedgerRange, LedgerShortcut, LedgerSequence, LedgerHash>` —
* allowing symbolic ledger names to flow through the database query layer via
* `std::visit` dispatch without special-case handling.
*
* @note The scoped `enum class` form prevents implicit integer conversions and
* namespace pollution, both of which are hazards in a codebase that also
* works extensively with raw integer ledger sequence numbers.
*/
enum class LedgerShortcut {
/** The current working ledger (open, not yet closed) */
/** The open, in-progress ledger still accumulating new transactions.
*
* This ledger has not been closed or validated, so its contents may
* change. Results derived from it are not final and may be rolled back
* during a reorganisation or consensus failure.
*/
Current,
/** The most recently closed ledger (may not be validated) */
/** The most recently closed ledger; stable in structure but not yet
* consensus-validated.
*
* No new transactions are accepted into this ledger, but the network has
* not yet confirmed it as the authoritative chain tip. It is more stable
* than `Current` but still not suitable for finality guarantees.
*/
Closed,
/** The most recently validated ledger */
/** The most recently validated ledger; the fully consensus-confirmed chain
* tip.
*
* This is the only state considered immutable and trustworthy for finality
* purposes. An RPC node that cannot provide a fresh validated ledger
* (i.e., it is stale) will return an error rather than serve potentially
* incorrect data.
*/
Validated
};

View File

@@ -1,3 +1,8 @@
/** @file
* Defines MPTAmount, the canonical signed-integer amount type for
* Multi-Purpose Tokens (MPTs) on the XRP Ledger.
*/
#pragma once
#include <xrpl/basics/Number.h>
@@ -13,12 +18,40 @@
namespace xrpl {
/** Typed signed-integer quantity for Multi-Purpose Tokens (MPTs).
*
* MPT balances are plain whole-unit integers — no mantissa/exponent pair,
* no sub-unit naming — capped at `maxMPTokenAmount` (INT64_MAX) by the
* protocol. The class sits alongside `XRPAmount` and `IOUAmount` as one
* of the three concrete amount types that satisfy the `StepAmount` concept
* used by the payment-path and DEX engines.
*
* Arithmetic operators are composed via Boost.Operators (CRTP):
* - `boost::totally_ordered<MPTAmount>` — synthesizes `!=`, `>`, `>=`,
* `<=` from the declared `==` and `<`.
* - `boost::additive<MPTAmount>` — synthesizes binary `+`/`-` from
* `+=`/`-=`.
* - `boost::equality_comparable<MPTAmount, int64_t>` — heterogeneous `!=`
* from `operator==(value_type)`.
* - `boost::additive<MPTAmount, int64_t>` — heterogeneous `+`/`-` with
* raw integers.
*
* Out-of-line `+=`, `-=`, `operator-()`, `==`, and `<` perform no overflow
* detection; callers are responsible for keeping balances in range through
* the ledger constraint machinery. The safe multiplication path
* (`mulRatio`) uses 128-bit intermediates and throws on overflow.
*
* @note `value_` is `protected` (not `private`) to allow subclassing
* without exposing the raw integer to unrelated code. No subclasses
* exist in the current codebase.
*/
class MPTAmount : private boost::totally_ordered<MPTAmount>,
private boost::additive<MPTAmount>,
private boost::equality_comparable<MPTAmount, std::int64_t>,
private boost::additive<MPTAmount, std::int64_t>
{
public:
/** Underlying integer type; matches `XRPAmount::value_type`. */
using value_type = std::int64_t;
protected:
@@ -27,57 +60,149 @@ protected:
public:
MPTAmount() = default;
constexpr MPTAmount(MPTAmount const& other) = default;
/** Construct a zero amount from the `beast::Zero` sentinel.
*
* Allows idiomatic zero-initialization via `beast::zero` in generic
* code that is templated on amount type.
*/
constexpr MPTAmount(beast::Zero);
constexpr MPTAmount&
operator=(MPTAmount const& other) = default;
// Round to nearest, even on tie.
/** Construct from a `Number`, rounding to nearest with ties to even.
*
* Provides implicit compatibility with XRPL's high-precision arithmetic
* type. The rounding mode matches IEEE 754 default (round-half-to-even).
*
* @param x The `Number` value to convert.
*/
explicit MPTAmount(Number const& x) : MPTAmount(static_cast<value_type>(x))
{
}
/** Construct from a raw `int64_t` value.
*
* Explicit to prevent accidental implicit conversion from integers.
* The caller is responsible for ensuring `value` does not exceed
* `maxMPTokenAmount` (INT64_MAX).
*
* @param value The integer amount in whole MPT units.
*/
constexpr explicit MPTAmount(value_type value);
/** Assign the `beast::Zero` sentinel, setting the amount to zero. */
constexpr MPTAmount& operator=(beast::Zero);
/** Add `other` to this amount in place.
*
* No overflow detection is performed; callers must ensure the result
* remains within `int64_t` range.
*
* @param other The amount to add.
* @return Reference to `*this` after addition.
*/
MPTAmount&
operator+=(MPTAmount const& other);
/** Subtract `other` from this amount in place.
*
* No overflow detection is performed; callers must ensure the result
* remains within `int64_t` range.
*
* @param other The amount to subtract.
* @return Reference to `*this` after subtraction.
*/
MPTAmount&
operator-=(MPTAmount const& other);
/** Return the arithmetic negation of this amount.
*
* Used where a credit and a debit are expressed as equal-magnitude
* amounts of opposite sign before being applied to the ledger.
* Negating `INT64_MIN` is undefined behavior; callers must avoid it.
*
* @return A new `MPTAmount` equal to `-value_`.
*/
MPTAmount
operator-() const;
/** Test equality with another `MPTAmount`.
*
* Together with `operator<`, satisfies `boost::totally_ordered`,
* from which `!=`, `>`, `<=`, and `>=` are synthesized.
*
* @param other The amount to compare against.
* @return `true` if both amounts hold the same integer value.
*/
bool
operator==(MPTAmount const& other) const;
/** Test equality with a raw `int64_t` value.
*
* Allows expressions like `amt == 0` without constructing a temporary.
* `boost::equality_comparable<MPTAmount, int64_t>` synthesizes the
* mixed-type `!=` from this overload.
*
* @param other The raw integer value to compare against.
* @return `true` if `value_` equals `other`.
*/
bool
operator==(value_type other) const;
/** Return `true` if this amount is strictly less than `other`.
*
* The single total-order primitive from which `boost::totally_ordered`
* derives `>`, `<=`, and `>=`. Signed comparison gives correct
* semantics for negative balances.
*
* @param other The amount to compare against.
* @return `true` if `value_` is strictly less than `other.value_`.
*/
bool
operator<(MPTAmount const& other) const;
/** Returns true if the amount is not zero */
/** Returns true if the amount is not zero. */
explicit constexpr
operator bool() const noexcept;
/** Implicit conversion to `Number` for use in high-precision arithmetic.
*
* Allows `MPTAmount` to be passed anywhere a `Number` is expected —
* arithmetic operations, rounding, and comparisons — without an explicit
* cast. The reverse direction (construction from `Number`) is explicit.
*/
operator Number() const noexcept
{
return value();
}
/** Return the sign of the amount */
/** Return the sign of the amount.
*
* @return `-1` if negative, `0` if zero, `1` if positive.
*/
[[nodiscard]] constexpr int
signum() const noexcept;
/** Returns the underlying value. Code SHOULD NOT call this
function unless the type has been abstracted away,
e.g. in a templated function.
*/
/** Return the underlying integer value.
*
* Code SHOULD NOT call this function unless the type has been abstracted
* away, e.g. in a templated function. Prefer operating on `MPTAmount`
* directly to keep arithmetic in the typed domain.
*
* @return The raw `int64_t` balance in whole MPT units.
*/
[[nodiscard]] constexpr value_type
value() const;
/** Return the smallest positive MPT amount (one indivisible unit).
*
* Provides a uniform factory interface shared with `XRPAmount` and
* `IOUAmount` so generic payment-path code can obtain the minimum
* step size without knowing the concrete amount type.
*
* @return `MPTAmount{1}`.
*/
static MPTAmount
minPositiveAmount();
};
@@ -98,14 +223,12 @@ MPTAmount::operator=(beast::Zero)
return *this;
}
/** Returns true if the amount is not zero */
constexpr MPTAmount::
operator bool() const noexcept
{
return value_ != 0;
}
/** Return the sign of the amount */
constexpr int
MPTAmount::signum() const noexcept
{
@@ -114,17 +237,13 @@ MPTAmount::signum() const noexcept
return (value_ != 0) ? 1 : 0;
}
/** Returns the underlying value. Code SHOULD NOT call this
function unless the type has been abstracted away,
e.g. in a templated function.
*/
constexpr MPTAmount::value_type
MPTAmount::value() const
{
return value_;
}
// Output MPTAmount as just the value.
/** Stream an `MPTAmount` as its raw integer value. */
template <class Char, class Traits>
std::basic_ostream<Char, Traits>&
operator<<(std::basic_ostream<Char, Traits>& os, MPTAmount const& q)
@@ -132,12 +251,35 @@ operator<<(std::basic_ostream<Char, Traits>& os, MPTAmount const& q)
return os << q.value();
}
/** Return the decimal string representation of an `MPTAmount`. */
inline std::string
to_string(MPTAmount const& amount)
{
return std::to_string(amount.value());
}
/** Compute `amt * num / den` with configurable rounding direction.
*
* The intermediate product is computed in 128-bit arithmetic to avoid
* overflow when multiplying a 63-bit MPT balance by a 32-bit numerator
* (up to 95 bits required). After division, any remainder is resolved
* based on the sign of `amt` and `roundUp`:
* - Positive amounts round up when `roundUp` is `true`.
* - Negative amounts round away from zero (more negative) when `roundUp`
* is `false`.
*
* Used for fee and reserve calculations that apply percentage-style ratios
* to MPT amounts.
*
* @param amt The base amount to scale.
* @param num Numerator of the ratio (32-bit unsigned).
* @param den Denominator of the ratio (32-bit unsigned, must be > 0).
* @param roundUp If `true`, round the result toward positive infinity;
* if `false`, round toward negative infinity.
* @return The scaled `MPTAmount`.
* @throws std::runtime_error If `den` is zero.
* @throws std::overflow_error If the result exceeds `INT64_MAX`.
*/
inline MPTAmount
mulRatio(MPTAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp)
{

View File

@@ -5,9 +5,23 @@
namespace xrpl {
/* Adapt MPTID to provide the same interface as Issue. Enables using static
* polymorphism by Asset and other classes. MPTID is a 192-bit concatenation
* of a 32-bit account sequence and a 160-bit account id.
/** Identifies a Multi-Purpose Token issuance, adapting `MPTID` to mirror
* the public interface of `Issue`.
*
* `MPTIssue` wraps a single 192-bit `MPTID` (32-bit big-endian sequence
* concatenated with a 160-bit `AccountID`) and exposes the same accessors as
* `Issue` — `getIssuer()`, `getText()`, `setJson()`, `native()`, and
* `integral()` — allowing `Asset` and any template code constrained by
* `ValidIssueType` to treat MPTs and IOUs uniformly.
*
* Key semantic differences from `Issue`:
* - `native()` always returns `false` (MPTs are never the native currency).
* - `integral()` always returns `true` (MPT amounts are 64-bit integers,
* unlike IOUs which use multi-precision rational arithmetic).
* - Equality and ordering compare the full 192-bit `MPTID`, with no
* special-case equivalence class for any sentinel value.
*
* @see Issue, Asset, MPTID
*/
class MPTIssue
{
@@ -17,27 +31,80 @@ private:
public:
MPTIssue() = default;
/** Constructs an MPTIssue from a pre-formed 192-bit issuance identifier.
*
* @param issuanceID The packed MPTID (32-bit sequence ‖ 160-bit AccountID).
*/
MPTIssue(MPTID const& issuanceID);
/** Constructs an MPTIssue from the raw components of an MPTID.
*
* Delegates to `xrpl::makeMptID(sequence, account)` to assemble the
* packed MPTID, saving callers from invoking that helper explicitly.
*
* @param sequence The issuer's account sequence number at the time of
* issuance.
* @param account The AccountID of the issuer.
*/
MPTIssue(std::uint32_t sequence, AccountID const& account);
/** Implicit conversion to the underlying `MPTID`.
*
* Allows an `MPTIssue` to be passed wherever a raw `MPTID` is expected
* without an explicit cast.
*
* @return A reference to the underlying `MPTID`, valid for the lifetime
* of this object.
*/
operator MPTID const&() const
{
return mptID_;
}
/** Extracts the issuer's `AccountID` from the packed `MPTID`.
*
* `MPTID` lays out 4 bytes of sequence followed immediately by 20 bytes
* of `AccountID`. This method returns a reference into that buffer by
* pointer-casting past the leading 4 bytes. A `static_assert` on the
* total size of `MPTID` guards against layout changes breaking this
* assumption at compile time.
*
* @return A reference to the `AccountID` embedded in the `MPTID`.
* Valid for the lifetime of this `MPTIssue` object.
* @note The free function `getMPTIssuer()` achieves the same extraction
* via `std::bit_cast` and returns by value, avoiding any
* lifetime dependency on the source object.
*/
[[nodiscard]] AccountID const&
getIssuer() const;
/** Returns the underlying `MPTID`.
*
* @return A reference to the 192-bit issuance identifier, valid for the
* lifetime of this object.
*/
[[nodiscard]] constexpr MPTID const&
getMptID() const
{
return mptID_;
}
/** Returns the hex string representation of the underlying `MPTID`.
*
* @return Uppercase hex encoding of the 192-bit issuance identifier.
*/
[[nodiscard]] std::string
getText() const;
/** Writes the issuance identifier into a JSON object under the key
* `mpt_issuance_id`.
*
* Serializes the `MPTID` as a hex string. Contrasted with
* `Issue::setJson()`, which writes separate `currency` and `issuer` keys.
*
* @param jv Output JSON object to write into; existing keys are not
* cleared.
*/
void
setJson(json::Value& jv) const;
@@ -47,12 +114,30 @@ public:
friend constexpr std::weak_ordering
operator<=>(MPTIssue const& lhs, MPTIssue const& rhs);
/** Returns `false`; MPTs are never the native asset (XRP).
*
* Mirrors `Issue::native()` so that generic code can query XRP-ness
* without a type dispatch. `Asset::getAmountType()` relies on this flag
* to select `XRPAmount` vs `IOUAmount` vs `MPTAmount` at compile time.
*
* @return Always `false`.
*/
static bool
native()
{
return false;
}
/** Returns `true`; MPT amounts are stored as 64-bit integers.
*
* Mirrors the naming of `Issue::integral()` so that generic code can
* distinguish integer (drop/MPT) amounts from multi-precision IOU
* amounts without a type dispatch. Unlike `Issue::integral()`, this is
* unconditionally `true` — all MPTs use integer arithmetic regardless of
* the token configuration.
*
* @return Always `true`.
*/
static bool
integral()
{
@@ -60,19 +145,44 @@ public:
}
};
/** Returns `true` if two `MPTIssue` instances represent the same issuance.
*
* Delegates to the full 192-bit comparison of the underlying `MPTID`s.
* Both the sequence number and the issuer `AccountID` must match; there is
* no partial-equality exemption as there is in `Issue::operator==` for XRP.
*
* @param lhs Left-hand issuance.
* @param rhs Right-hand issuance.
* @return `true` iff both `MPTID`s are bitwise equal.
*/
constexpr bool
operator==(MPTIssue const& lhs, MPTIssue const& rhs)
{
return lhs.mptID_ == rhs.mptID_;
}
/** Provides a strict weak ordering over `MPTIssue` values.
*
* Delegates to the 192-bit lexicographic comparison of the underlying
* `MPTID`s. The ordering is consistent with `operator==`: two issuances are
* equivalent iff their full `MPTID`s are identical.
*
* @param lhs Left-hand issuance.
* @param rhs Right-hand issuance.
* @return A `std::weak_ordering` value consistent with `operator==`.
*/
constexpr std::weak_ordering
operator<=>(MPTIssue const& lhs, MPTIssue const& rhs)
{
return lhs.mptID_ <=> rhs.mptID_;
}
/** MPT is a non-native token.
/** Returns `false`; an `MPTID` never identifies the native XRP asset.
*
* Provides the same naming convention as `isXRP(Issue)`, allowing call
* sites to test XRP-ness uniformly across both issue types.
*
* @return Always `false`.
*/
inline bool
isXRP(MPTID const&)
@@ -80,6 +190,21 @@ isXRP(MPTID const&)
return false;
}
/** Extracts the issuer `AccountID` from a `MPTID` by value.
*
* Copies the 20 bytes that follow the leading 4-byte sequence field into a
* temporary array, then uses `std::bit_cast` to reinterpret them as an
* `AccountID`. The `static_assert` on the total size of `MPTID` ensures the
* layout assumption holds; if the type ever gains padding the build fails.
* `std::bit_cast` is typically optimized to nothing in the final assembly.
*
* @param mptid The 192-bit issuance identifier to extract from.
* @return The `AccountID` embedded in bytes 423 of `mptid`.
* @note Use `MPTIssue::getIssuer()` when a zero-copy reference into an
* existing `MPTIssue` object is sufficient. The rvalue overloads of
* this function are deleted to prevent dangling references from
* temporaries.
*/
inline AccountID
getMPTIssuer(MPTID const& mptid)
{
@@ -93,12 +218,21 @@ getMPTIssuer(MPTID const& mptid)
return std::bit_cast<AccountID>(bytes);
}
// Disallow temporary
// Deleted to prevent a dangling-reference bug: if a temporary MPTID were
// accepted, the returned AccountID const& would immediately dangle.
AccountID const&
getMPTIssuer(MPTID const&&) = delete;
AccountID const&
getMPTIssuer(MPTID&&) = delete;
/** Returns the `MPTID` sentinel representing "no MPT".
*
* Encodes `{ sequence=0, account=noAccount() }` — all-zero bits.
* Mirrors `noIssue()` in `Issue.h` for use in contexts where a
* missing or invalid MPT must be represented without `std::optional`.
*
* @return The all-zero 192-bit sentinel `MPTID`.
*/
inline MPTID
noMPT()
{
@@ -106,6 +240,15 @@ noMPT()
return kMPT.getMptID();
}
/** Returns the `MPTID` sentinel representing a structurally invalid MPT.
*
* Encodes `{ sequence=0, account=xrpAccount() }` — sequence zero with the
* XRP account address as issuer, which is a conventionally invalid issuer
* for MPTs. `Asset`'s `BadAsset` comparison detects this sentinel by
* checking `getIssuer() == xrpAccount()`.
*
* @return The sentinel `MPTID` whose issuer is `xrpAccount()`.
*/
inline MPTID
badMPT()
{
@@ -113,6 +256,15 @@ badMPT()
return kMPT.getMptID();
}
/** Appends the underlying `MPTID` to a hasher.
*
* Plugs `MPTIssue` into the Beast hashing framework, enabling use in
* Beast-aware hash maps and sets.
*
* @tparam Hasher A type satisfying the `beast::hash_append` concept.
* @param h The hasher to append to.
* @param r The issuance whose `MPTID` is appended.
*/
template <class Hasher>
void
hash_append(Hasher& h, MPTIssue const& r)
@@ -121,15 +273,49 @@ hash_append(Hasher& h, MPTIssue const& r)
hash_append(h, r.getMptID());
}
/** Returns the canonical wire-format JSON representation of an MPT issuance.
*
* Convenience wrapper around `MPTIssue::setJson()`. The returned object
* contains a single `mpt_issuance_id` field with the hex-encoded `MPTID`.
*
* @param mptIssue The issuance to serialize.
* @return A JSON object of the form `{"mpt_issuance_id": "<hex>"}`.
*/
json::Value
toJson(MPTIssue const& mptIssue);
/** Returns the hex string representation of an MPT issuance.
*
* @param mptIssue The issuance to convert.
* @return Uppercase hex encoding of the underlying 192-bit `MPTID`.
*/
std::string
to_string(MPTIssue const& mptIssue);
/** Parses an MPT issuance from a JSON object.
*
* Validates in strict order: `v` must be a JSON object; `currency` and
* `issuer` keys must be absent (their presence indicates IOU data routed
* to the wrong parser); `mpt_issuance_id` must be a string containing a
* valid 48-character hex-encoded `MPTID`.
*
* @param jv The JSON value to parse; must be an object.
* @return The parsed `MPTIssue`.
* @throws std::runtime_error if `jv` is not a JSON object, or if `currency`
* or `issuer` keys are present.
* @throws json::Error if `mpt_issuance_id` is absent, not a string, or not
* a valid 192-bit hex value.
* @see toJson for the inverse operation.
*/
MPTIssue
mptIssueFromJson(json::Value const& jv);
/** Writes the hex representation of an MPT issuance to a stream.
*
* @param os The output stream.
* @param x The issuance to write.
* @return `os`, to allow chaining.
*/
std::ostream&
operator<<(std::ostream& os, MPTIssue const& x);
@@ -137,6 +323,13 @@ operator<<(std::ostream& os, MPTIssue const& x);
namespace std {
/** Specializes `std::hash` for `xrpl::MPTID`, delegating to the type's own
* hasher.
*
* Enables `MPTID` to be used directly as a key in `std::unordered_map`,
* `std::unordered_set`, and similar standard containers without wrapping
* in `MPTIssue`.
*/
template <>
struct hash<xrpl::MPTID> : xrpl::MPTID::hasher
{

View File

@@ -1,3 +1,12 @@
/** @file
* Holds one pre-built `Json::Value` per supported API version so that a
* single ledger event can be delivered to subscribers speaking different API
* versions without re-serializing on every send.
*
* The public alias `xrpl::MultiApiJson` binds the template to the live
* version range `[kAPI_MINIMUM_SUPPORTED_VERSION, kAPI_MAXIMUM_VALID_VERSION]`.
*/
#pragma once
#include <xrpl/beast/utility/instrumentation.h>
@@ -14,6 +23,15 @@
namespace xrpl {
namespace detail {
/** Variable template that is `true` only for lvalue-reference-qualified
* `std::integral_constant` specializations (both cv-variants).
*
* Used as the building block for the `some_integral_constant` concept, which
* disambiguates the compile-time and runtime overloads of `VisitorT::operator()`.
*
* @tparam T The type to test.
*/
template <typename T>
constexpr bool kIS_INTEGRAL_CONSTANT = false;
template <typename I, auto A>
@@ -21,35 +39,94 @@ constexpr bool kIS_INTEGRAL_CONSTANT<std::integral_constant<I, A>&> = true;
template <typename I, auto A>
constexpr bool kIS_INTEGRAL_CONSTANT<std::integral_constant<I, A> const&> = true;
/** Concept satisfied only by `std::integral_constant` specializations (lvalue refs).
*
* Used in `requires` clauses on `VisitorT::operator()` to prevent the
* runtime-`unsigned` overloads from being selected when a compile-time
* constant is passed, avoiding otherwise-ambiguous partial ordering.
*
* @tparam T The type to constrain.
*/
template <typename T>
concept some_integral_constant = detail::kIS_INTEGRAL_CONSTANT<T&>;
// This class is designed to wrap a collection of _almost_ identical json::Value
// objects, indexed by version (i.e. there is some mapping of version to object
// index). It is used e.g. when we need to publish JSON data to users supporting
// different API versions. We allow manipulation and inspection of all objects
// at once with `isMember` and `set`, and also individual inspection and updates
// of an object selected by the user by version, using `visitor_t` nested type.
/** Holds one `Json::Value` per API version in a fixed-size array, enabling
* single-pass event serialization for multi-version subscriber delivery.
*
* When an XRPL server event (e.g., a validated transaction) must be published
* to subscribers that may speak different API versions, re-serializing or
* branching inside the send path would add latency proportional to subscriber
* count. `MultiApiJson` amortizes version-specific transformations to once per
* event: callers construct the object from a common base `Json::Value`, apply
* per-version mutations via `visit`, and then each subscriber's delivery path
* calls `visit(apiVersion, sender)` to pick the pre-built slot cheaply.
*
* The array has `MaxVer + 1 - MinVer` elements; version `v` maps to index
* `v - MinVer`. `set` and `isMember` operate across all slots; `visit`
* operates on a single slot selected by version.
*
* @note Prefer the `xrpl::MultiApiJson` type alias over instantiating this
* template directly. Direct instantiation is intended for tests only; all
* production code should use the alias, which is bound to the live version
* constants and automatically tracks any future version-range changes.
*
* @tparam MinVer Minimum (inclusive) supported API version.
* @tparam MaxVer Maximum (inclusive) supported API version.
*/
template <unsigned MinVer, unsigned MaxVer>
struct MultiApiJson
{
static_assert(MinVer <= MaxVer);
/** Returns `true` if `v` falls within `[MinVer, MaxVer]`.
*
* Used by `VisitorT` to guard against out-of-range version accesses.
* @param v The API version number to test.
* @return `true` iff `v` is a valid slot index.
*/
static constexpr auto
valid(unsigned int v) noexcept -> bool
{
return v >= MinVer && v <= MaxVer;
}
/** Maps an API version number to its zero-based array slot.
*
* Out-of-range values below `MinVer` clamp to 0 rather than underflowing;
* the caller is responsible for checking `valid(v)` before trusting the
* result. Values above `MaxVer` are not clamped — `valid()` must be used
* to guard against those.
*
* @param v The API version number to map.
* @return The corresponding index into `val`.
*/
static constexpr auto
index(unsigned int v) noexcept -> std::size_t
{
return (v < MinVer) ? 0 : static_cast<std::size_t>(v - MinVer);
}
/** Number of API version slots stored; equals `MaxVer + 1 - MinVer`. */
constexpr static std::size_t kSIZE = MaxVer + 1 - MinVer;
/** The per-version JSON values, indexed by `index(version)`.
*
* Public to allow direct slot access in tests and for `VisitorT` (which
* is a friend via the `static constexpr` data member). Production callers
* should use `set`, `isMember`, and `visit` rather than indexing directly.
*/
std::array<json::Value, kSIZE> val = {};
/** Constructs the object, optionally copy-initializing every slot.
*
* When `init` is the default (null) `Json::Value`, all slots remain
* default-initialized (null). When a non-null value is supplied, every
* slot is copy-initialized to it. The common pattern in `NetworkOPs.cpp`
* is to pass a shared base object and then apply per-version mutations
* via `visit`.
*
* @param init Base value to copy into every slot; omit for null slots.
*/
explicit MultiApiJson(json::Value const& init = {})
{
if (init == json::Value{})
@@ -58,6 +135,16 @@ struct MultiApiJson
v = init;
}
/** Writes a key-value pair into every slot simultaneously.
*
* Use for fields that are identical across all API versions — the majority
* of transaction fields. Cheaper than calling `visit` once per version for
* shared data. The `requires` clause restricts `v` to types from which
* `Json::Value` can be constructed, preventing silent misuse.
*
* @param key The JSON object key to set.
* @param v The value to assign; must be constructible to `Json::Value`.
*/
void
set(char const* key, auto const& v)
requires std::constructible_from<json::Value, decltype(v)>
@@ -66,8 +153,26 @@ struct MultiApiJson
a[key] = v;
}
enum class IsMemberResult : int { None = 0, Some, All };
/** Tri-state result of `isMember`: indicates how many version slots contain a key.
*
* Scoped to `MultiApiJson` rather than a separate class enum deliberately —
* the struct is narrow enough to serve as its own scope for this result.
*/
enum class IsMemberResult : int {
None = 0, /**< No slot contains the key. */
Some, /**< At least one but not all slots contain the key. */
All /**< Every slot contains the key. */
};
/** Queries how many version slots contain the given JSON key.
*
* Useful for asserting that version-specific mutations were (or were not)
* applied before delivery. `NetworkOPs` uses it in assertions to verify
* that certain fields are never set on a freshly-constructed object.
*
* @param key The JSON object key to look up in each slot.
* @return `IsMemberResult::None`, `Some`, or `All`.
*/
[[nodiscard]] IsMemberResult
isMember(char const* key) const
{
@@ -83,6 +188,33 @@ struct MultiApiJson
return count < kSIZE ? IsMemberResult::Some : IsMemberResult::All;
}
/** Stateless callable that routes invocations to the correct version slot.
*
* Provides four `operator()` overloads split along two axes:
*
* 1. **Compile-time version** (`std::integral_constant<unsigned, V>`):
* the version is checked with `static_assert`; the JSON reference and
* optional extra arguments are forwarded to `fn` at compile time.
*
* 2. **Runtime version** (any type convertible to `unsigned` that is
* *not* an `integral_constant`): the version is checked with
* `XRPL_ASSERT`; the `some_integral_constant` concept in the `requires`
* clause prevents these overloads from being selected when a
* compile-time constant is passed, resolving the otherwise-ambiguous
* partial ordering.
*
* Each axis is further split by whether extra arguments are forwarded to
* `fn` after the `Json::Value` (and possibly the version value). This
* matches the calling convention of `forAllApiVersions`/`forApiVersions`,
* which pass each version as an `integral_constant` plus any extra args
* bound at the call site.
*
* `const`-propagation is automatic: the JSON reference passed to `fn`
* mirrors the `const`-ness of the `Json&` parameter.
*
* @note Exposed as `kVISITOR` to allow direct testing; prefer `visit()`
* for all production call sites.
*/
static constexpr struct VisitorT final
{
// integral_constant version, extra arguments
@@ -145,6 +277,19 @@ struct MultiApiJson
}
} kVISITOR = {};
/** Returns a closure that dispatches `kVISITOR` for this object (mutable).
*
* The returned callable captures `this` and forwards all arguments to
* `kVISITOR`. This form is composable with `forAllApiVersions` and
* `forApiVersions`: those utilities iterate the version range at compile
* time, passing each version as an `integral_constant`. The closure
* satisfies that calling convention exactly, so
* `forAllApiVersions(obj.visit(), lambda)` iterates every version with a
* single consistent lambda without any per-version conditional logic.
*
* @return A lambda `(auto... args) -> auto` that calls
* `kVISITOR(*this, args...)`.
*/
auto
visit()
{
@@ -155,6 +300,16 @@ struct MultiApiJson
{ return kVISITOR(*self, std::forward<decltype(args)>(args)...); };
}
/** Returns a closure that dispatches `kVISITOR` for this object (const).
*
* Identical to the mutable overload but captures `this` as `const`,
* propagating const-ness through to the `Json::Value` reference passed to
* the callable. Used when the caller only needs to read the pre-built JSON
* (e.g., subscriber delivery in `BookListeners::publish`).
*
* @return A lambda `(auto... args) -> auto` that calls
* `kVISITOR(*this, args...)` on the const object.
*/
[[nodiscard]] auto
visit() const
{
@@ -165,6 +320,20 @@ struct MultiApiJson
{ return kVISITOR(*self, std::forward<decltype(args)>(args)...); };
}
/** Directly invokes `kVISITOR` for a single version (mutable).
*
* Equivalent to `visit()(args...)` but avoids the closure allocation.
* Typical usage:
* ```cpp
* jvObj.visit(RPC::kAPI_VERSION<1>, [](Json::Value& jv) {
* jv["ledger_index"] = std::to_string(jv["ledger_index"].asInt());
* });
* ```
*
* @param args Version (compile-time or runtime) followed by a callable
* and any extra arguments accepted by `kVISITOR`.
* @return The return value of the callable.
*/
template <typename... Args>
auto
visit(Args... args) -> std::invoke_result_t<VisitorT, MultiApiJson&, Args...>
@@ -174,6 +343,20 @@ struct MultiApiJson
return kVISITOR(*this, std::forward<decltype(args)>(args)...);
}
/** Directly invokes `kVISITOR` for a single version (const).
*
* Const counterpart of the mutable `visit(args...)` overload. Used when
* the JSON slot must not be mutated — for example in the subscriber
* delivery path where each subscriber picks its pre-built slot:
* ```cpp
* jvObj.visit(subscriber->getApiVersion(),
* [&](Json::Value const& jv) { subscriber->send(jv, true); });
* ```
*
* @param args Version (compile-time or runtime) followed by a callable
* and any extra arguments accepted by `kVISITOR`.
* @return The return value of the callable.
*/
template <typename... Args>
[[nodiscard]] auto
visit(Args... args) const -> std::invoke_result_t<VisitorT, MultiApiJson const&, Args...>
@@ -186,7 +369,15 @@ struct MultiApiJson
} // namespace detail
// Wrapper for Json for all supported API versions.
/** Holds one pre-built `Json::Value` per currently supported API version.
*
* Bound to `[kAPI_MINIMUM_SUPPORTED_VERSION, kAPI_MAXIMUM_VALID_VERSION]`
* (currently versions 13), so the concrete type stores exactly three
* `Json::Value` objects. Changing those constants automatically resizes
* every `MultiApiJson` instance in the server.
*
* @see detail::MultiApiJson for the full behavioral contract.
*/
using MultiApiJson =
detail::MultiApiJson<RPC::kAPI_MINIMUM_SUPPORTED_VERSION, RPC::kAPI_MAXIMUM_VALID_VERSION>;

View File

@@ -1,3 +1,20 @@
/** @file
* Aggregator entry point for injecting synthetic NFT fields into RPC
* transaction responses.
*
* "Synthetic" fields (`nftoken_ids`, `nftoken_id`, `offer_id`) are derived
* at query time from the ledger state changes recorded in `TxMeta`; they are
* not stored on-chain. Callers invoke a single function here rather than
* calling the individual NFT inserters directly, keeping call sites from
* accumulating an ever-growing list of per-type injector calls as new NFT
* transaction types are added.
*
* The underlying extraction helpers (`insertNFTokenID`, `insertNFTokenOfferID`)
* live in `NFTokenID.h` and `NFTokenOfferID.h` under the broader `xrpl::`
* namespace so that Clio (the XRPL History API server) can call those helpers
* directly without the `xrpl::RPC` coupling imposed by this header.
*/
#pragma once
#include <xrpl/json/json_forwards.h>
@@ -8,13 +25,44 @@
namespace xrpl::RPC {
/**
Adds common synthetic fields to transaction-related JSON responses
@{
/** Enrich a transaction JSON response with NFT-derived synthetic fields.
*
* Delegates to two independent inserters, in order:
*
* - `insertNFTokenID` — adds `nftoken_id` (for `NFTokenMint` and
* `NFTokenAcceptOffer`) or `nftoken_ids` (for `NFTokenCancelOffer`) by
* diffing the NFToken arrays across all affected ledger nodes recorded in
* the transaction metadata.
* - `insertNFTokenOfferID` — adds `offer_id` for `NFTokenCreateOffer` (and
* mints that include an immediate sell offer) by locating the newly created
* `NFTokenOffer` node and extracting its `sfLedgerIndex`.
*
* Both delegates gate themselves on transaction type and `tesSUCCESS`, so
* this function is safe to call for any transaction type: non-NFT
* transactions produce no output.
*
* Synthetic fields are written into `response[jss::meta]`. The `meta`
* sub-object should already be populated by the caller (e.g., via
* `TxMeta::getJson`) before this function is invoked — consistent with the
* call-site pattern in `Tx.cpp`, `Simulate.cpp`, `AccountTx.cpp`, and
* `NetworkOPs.cpp`, where this call appears alongside `insertDeliveredAmount`
* and `insertMPTokenIssuanceID` as part of a fixed metadata-enrichment
* sequence.
*
* @param response Top-level RPC response object. Synthetic fields are
* written into its `meta` sub-object, which is created on demand if
* absent.
* @param transaction The executed transaction. A null pointer is handled
* gracefully by the delegates (no-op).
* @param transactionMeta Read-only view of the transaction's metadata used to
* diff ledger node states and locate newly created objects.
*
* @see xrpl::insertNFTokenID, xrpl::insertNFTokenOfferID
*/
void
insertNFTSyntheticInJson(json::Value&, std::shared_ptr<STTx const> const&, TxMeta const&);
/** @} */
insertNFTSyntheticInJson(
json::Value& response,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta);
} // namespace xrpl::RPC

View File

@@ -1,3 +1,16 @@
/** @file
* Helpers that reconstruct NFToken identities from transaction metadata
* and inject them into RPC JSON responses as synthetic fields.
*
* Raw ledger metadata records before/after state of `NFTokenPage` objects
* but does not directly annotate which token was created or consumed. The
* functions below bridge that gap. They are free (non-static) functions so
* that Clio (the XRPL History API server) can link against them directly
* and perform the same enrichment without duplicating the logic.
*
* @see NFTokenOfferID.h for the analogous helpers for `NFTokenOffer` IDs.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
@@ -11,28 +24,100 @@
namespace xrpl {
/**
Add a `nftoken_ids` field to the `meta` output parameter.
The field is only added to successful NFTokenMint, NFTokenAcceptOffer,
and NFTokenCancelOffer transactions.
Helper functions are not static because they can be used by Clio.
@{
/** Returns true if this transaction could have produced or consumed an NFToken.
*
* Acts as a cheap early-exit guard for all downstream extraction logic.
* A transaction qualifies only when it is one of the three NFT types
* (`ttNFTOKEN_MINT`, `ttNFTOKEN_ACCEPT_OFFER`, `ttNFTOKEN_CANCEL_OFFER`)
* and its result code is `tesSUCCESS`. A failed transaction cannot have
* mutated any NFToken page, so metadata diffing would be meaningless.
*
* @param serializedTx The executed transaction; a null pointer yields
* false immediately.
* @param transactionMeta Metadata from the same transaction, used to
* check the result code.
* @return True only when `serializedTx` is non-null, its type is one of
* the three NFT transaction types, and the result is `tesSUCCESS`.
*/
bool
canHaveNFTokenID(std::shared_ptr<STTx const> const& serializedTx, TxMeta const& transactionMeta);
/** Recovers the ID of the NFToken added by a mint transaction.
*
* `ttNFTOKEN_MINT` metadata records the full token arrays of every
* affected `NFTokenPage` in `sfPreviousFields` and `sfFinalFields` but
* does not tag the newly inserted entry. This function recovers it by
* set-difference: it accumulates token IDs from all previous states into
* `prevIDs` and all final states into `finalIDs`, then uses
* `std::mismatch` to locate the first entry present in `finalIDs` but
* absent from `prevIDs`. Because `NFTokenPage` entries are stored in
* sorted order by token ID, both vectors are already ordered and
* `std::mismatch` finds the insertion point in linear time without
* additional sorting.
*
* @note When a mint causes an existing page to split, the linked-list
* rewiring may produce a `sfModifiedNode` for a third page whose
* `sfPreviousFields` contain only pointer updates (`NextPageMin` /
* `PreviousPageMin`) with no `sfNFTokens` array. Such nodes are
* skipped silently; without this guard the size invariant below
* would incorrectly fail for legitimate mints.
*
* @param transactionMeta Metadata from a `ttNFTOKEN_MINT` transaction.
* @return The `uint256` ID of the newly minted token, or `std::nullopt`
* if `finalIDs.size() != prevIDs.size() + 1` (tokens are minted one
* at a time) or if `std::mismatch` unexpectedly reaches the end of
* `finalIDs`.
*/
std::optional<uint256>
getNFTokenIDFromPage(TxMeta const& transactionMeta);
/** Collects the NFToken IDs referenced by deleted `NFTokenOffer` objects.
*
* Both `ttNFTOKEN_ACCEPT_OFFER` and `ttNFTOKEN_CANCEL_OFFER` delete one
* or more `ltNFTOKEN_OFFER` ledger entries. Each deleted offer's
* `sfFinalFields` carries the `sfNFTokenID` it was created for, so the
* token identity is recoverable without set-difference arithmetic.
* Results are sorted and deduplicated because a single cancel transaction
* can target multiple offers that reference the same underlying NFT.
*
* @param transactionMeta Metadata from a `ttNFTOKEN_ACCEPT_OFFER` or
* `ttNFTOKEN_CANCEL_OFFER` transaction.
* @return Sorted, deduplicated vector of `uint256` NFToken IDs recovered
* from all deleted offer nodes; empty if no qualifying deletions are
* found.
*/
std::vector<uint256>
getNFTokenIDFromDeletedOffer(TxMeta const& transactionMeta);
/** Injects synthetic NFToken ID field(s) into an RPC transaction response.
*
* Calls `canHaveNFTokenID` first; returns immediately without modifying
* `response` if the transaction is ineligible or extraction yields nothing.
* When eligible, dispatches by transaction type:
*
* - `ttNFTOKEN_MINT` — writes `jss::nftoken_id` (single string) derived
* from `getNFTokenIDFromPage`.
* - `ttNFTOKEN_ACCEPT_OFFER` — writes `jss::nftoken_id` (single string,
* first element) derived from `getNFTokenIDFromDeletedOffer`.
* - `ttNFTOKEN_CANCEL_OFFER` — writes `jss::nftoken_ids` (JSON array of
* all deduplicated IDs) derived from `getNFTokenIDFromDeletedOffer`.
*
* The singular/plural field-name distinction reflects a real semantic
* difference: accept and mint affect exactly one NFT, while cancel can
* affect many.
*
* @param response The JSON object to enrich; fields are written
* directly into it. The caller is responsible for scoping this to
* the `jss::meta` sub-object of the full response.
* @param transaction The executed transaction. A null pointer is
* handled gracefully via `canHaveNFTokenID`.
* @param transactionMeta Read-only metadata used for eligibility checking
* and token ID extraction.
*/
void
insertNFTokenID(
json::Value& response,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta);
/** @} */
} // namespace xrpl

View File

@@ -1,3 +1,20 @@
/** @file
* Helpers that recover the ledger index of a newly created `NFTokenOffer`
* from transaction metadata and inject it into RPC JSON responses as a
* synthetic `offer_id` field.
*
* The XRPL transaction format records only inputs; the ledger index of a
* newly created offer object appears solely in the `CreatedNode` entries of
* the transaction metadata. The three functions below encapsulate the scan
* once so that every API consumer — rippled RPC handlers and Clio alike —
* can obtain the offer ID without walking `AffectedNodes` manually. All
* three functions are free (non-static) so that Clio can call them directly
* without duplicating the logic.
*
* @see NFTokenID.h for the analogous helpers that inject `nftoken_id` /
* `nftoken_ids` for mint, accept-offer, and cancel-offer operations.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
@@ -10,26 +27,76 @@
namespace xrpl {
/**
Add an `offer_id` field to the `meta` output parameter.
The field is only added to successful NFTokenCreateOffer transactions.
Helper functions are not static because they can be used by Clio.
@{
/** Determine whether a transaction can have an NFToken offer ID.
*
* Acts as a cheap pre-filter before the metadata scan in
* `getOfferIDFromCreatedOffer`. Three conditions must all hold:
*
* 1. `serializedTx` is non-null.
* 2. The transaction type is `ttNFTOKEN_CREATE_OFFER`, **or** it is
* `ttNFTOKEN_MINT` with `sfAmount` present (a mint that simultaneously
* creates an immediate-sale offer).
* 3. The transaction succeeded (`tesSUCCESS`). A failed transaction never
* modifies the ledger, so no offer object can exist in the metadata.
*
* @param serializedTx The transaction to inspect. A null `shared_ptr`
* is handled safely and causes the function to return `false`.
* @param transactionMeta Metadata whose result code is checked for success.
* @return `true` only when all three conditions are satisfied, indicating
* that a subsequent call to `getOfferIDFromCreatedOffer` may yield a
* value.
*/
bool
canHaveNFTokenOfferID(
std::shared_ptr<STTx const> const& serializedTx,
TxMeta const& transactionMeta);
/** Extract the ledger index of the NFToken offer created by a transaction.
*
* Scans the `AffectedNodes` array in `transactionMeta` for a `CreatedNode`
* whose `sfLedgerEntryType` is `ltNFTOKEN_OFFER`. Modified and deleted
* nodes are skipped. The first qualifying node's `sfLedgerIndex` is
* returned; because at most one `NFTokenOffer` can be created per
* transaction, the loop exits immediately on the first match.
*
* @param transactionMeta Read-only transaction metadata to scan.
* @return The `uint256` ledger index of the newly created offer, or
* `std::nullopt` if no `CreatedNode` of type `ltNFTOKEN_OFFER` is
* found. Absence is a plausible non-exceptional condition (e.g., when
* processing historical or externally sourced transactions with
* incomplete metadata), not an error.
*
* @note Callers that have already performed their own eligibility checks
* (e.g., Clio) may call this function directly without first calling
* `canHaveNFTokenOfferID`.
*/
std::optional<uint256>
getOfferIDFromCreatedOffer(TxMeta const& transactionMeta);
/** Inject the NFToken offer ID into a JSON response as `jss::offer_id`.
*
* Composes `canHaveNFTokenOfferID` and `getOfferIDFromCreatedOffer`:
* returns immediately without touching `response` if the transaction is
* ineligible or the metadata contains no created offer node. When an offer
* ID is successfully extracted, it is written into `response[jss::offer_id]`
* as a hex string.
*
* The primary call site is `xrpl::RPC::insertNFTSyntheticInJson`, which
* passes `response[jss::meta]` as the target so that `offer_id` appears
* inside the `meta` sub-object alongside the raw node data.
*
* @param response The JSON object to enrich; `jss::offer_id` is
* written directly into it on success. The caller is responsible for
* scoping this to `jss::meta` of the full response.
* @param transaction The executed transaction. A null pointer is
* handled gracefully (no-op) via `canHaveNFTokenOfferID`.
* @param transactionMeta Read-only transaction metadata used to locate the
* created `NFTokenOffer` node.
*/
void
insertNFTokenOfferID(
json::Value& response,
std::shared_ptr<STTx const> const& transaction,
TxMeta const& transactionMeta);
/** @} */
} // namespace xrpl

View File

@@ -1,3 +1,12 @@
/** @file
* Defines `PathAsset`, the token identifier for a single hop in an XRPL
* payment path, and its associated free functions.
*
* `PathAsset` holds `std::variant<Currency, MPTID>` — just the *which
* currency or MPT* component of a path element, without the issuer.
* This is narrower than `Asset` (`std::variant<Issue, MPTIssue>`) because
* `STPathElement` records the issuer in a separate field.
*/
#pragma once
#include <xrpl/protocol/Asset.h>
@@ -5,7 +14,19 @@
namespace xrpl {
/* Represent STPathElement's asset, which can be Currency or MPTID.
/** Token identifier for a single hop within an XRPL payment path.
*
* Holds `std::variant<Currency, MPTID>` — the *which currency/MPT* component
* of a path element, without the issuer. Issuers are stored separately in
* `STPathElement::mIssuerID` because payment-path serialization records them
* as independent fields; folding them into `PathAsset` would duplicate data
* and complicate encoding.
*
* This is intentionally narrower than `Asset`, which pairs a currency or MPTID
* with its issuer. `PathAsset` carries only the identifier half. Use
* `PathAsset(Asset const&)` to project an `Asset` down to a `PathAsset`.
*
* @see Asset, STPathElement, ValidPathAsset
*/
class PathAsset
{
@@ -14,36 +35,83 @@ private:
public:
PathAsset() = default;
// Enables comparing Asset and PathAsset
/** Construct a PathAsset by projecting an Asset, discarding the issuer.
*
* For an `Issue`-bearing `Asset`, retains the `Currency`. For an
* `MPTIssue`-bearing `Asset`, retains the `MPTID`. This enables direct
* comparison between the richer `Asset` type and the path-element
* representation without manually extracting the identifier.
*
* @param asset The full asset to project.
*/
PathAsset(Asset const& asset);
/** Construct a PathAsset representing an XRP or IOU currency. */
PathAsset(Currency const& currency) : easset_(currency)
{
}
/** Construct a PathAsset representing an MPT issuance. */
PathAsset(MPTID const& mpt) : easset_(mpt)
{
}
/** Return whether the active alternative is exactly `T`.
*
* @tparam T `Currency` or `MPTID` (enforced by `ValidPathAsset`).
* @return `true` if the held alternative is `T`, `false` otherwise.
*/
template <ValidPathAsset T>
[[nodiscard]] constexpr bool
holds() const;
/** Return whether this path asset represents native XRP.
*
* A `Currency` alternative delegates to `xrpl::isXRP(currency)`. An
* `MPTID` alternative always returns `false` — MPT can never be native.
*
* @return `true` if the held currency is the XRP zero-currency sentinel.
*/
[[nodiscard]] constexpr bool
isXRP() const;
/** Return a const reference to the held value of type `T`.
*
* @tparam T `Currency` or `MPTID` (enforced by `ValidPathAsset`).
* @return A reference to the active alternative.
* @throws std::runtime_error if the active alternative is not `T`. Call
* `holds<T>()` or dispatch through `visit()` to avoid this.
*/
template <ValidPathAsset T>
T const&
get() const;
/** Return a const reference to the underlying variant.
*
* Provides direct access to `std::variant<Currency, MPTID>` for callers
* that need to pass it to `std::visit` or store it without going through
* the member `visit()` wrapper.
*
* @return The internal variant holding `Currency` or `MPTID`.
*/
[[nodiscard]] constexpr std::variant<Currency, MPTID> const&
value() const;
// Custom, generic visit implementation
/** Visit the active alternative with a set of per-type callables.
*
* Combines `visitors...` into a single overload set via
* `detail::CombineVisitors` and forwards to `std::visit`. Both
* alternatives (`Currency` and `MPTID`) must be covered.
*
* @tparam Visitors Callable types, one per alternative.
* @param visitors Callables to dispatch to; typically lambdas.
* @return The return value of the selected visitor.
*/
template <typename... Visitors>
constexpr auto
visit(Visitors&&... visitors) const -> decltype(auto)
{
// Simple delegation to the reusable utility, passing the internal
// variant data.
return detail::visit(easset_, std::forward<Visitors>(visitors)...);
}
@@ -51,9 +119,23 @@ public:
operator==(PathAsset const& lhs, PathAsset const& rhs);
};
/** True when `PA` is `Currency`, false when `PA` is `MPTID`.
*
* Compile-time predicate for `if constexpr` branches in generic code that
* must distinguish XRP/IOU paths from MPT paths.
*
* @tparam PA `Currency` or `MPTID` (enforced by `ValidPathAsset`).
*/
template <ValidPathAsset PA>
constexpr bool kIS_CURRENCY_V = std::is_same_v<PA, Currency>;
/** True when `PA` is `MPTID`, false when `PA` is `Currency`.
*
* Compile-time predicate for `if constexpr` branches in generic code that
* must distinguish MPT paths from XRP/IOU paths.
*
* @tparam PA `Currency` or `MPTID` (enforced by `ValidPathAsset`).
*/
template <ValidPathAsset PA>
constexpr bool kIS_MPTID_V = std::is_same_v<PA, MPTID>;
@@ -94,6 +176,17 @@ PathAsset::isXRP() const
[](MPTID const&) { return false; });
}
/** Compare two PathAssets for equality.
*
* Two `PathAsset` values are equal only when both hold the same alternative
* type *and* the contained values are equal. A `Currency` and an `MPTID`
* are never equal even if their raw bytes coincide, preventing cross-type
* false positives.
*
* @param lhs Left-hand operand.
* @param rhs Right-hand operand.
* @return `true` if both hold the same type and equal value, `false` otherwise.
*/
constexpr bool
operator==(PathAsset const& lhs, PathAsset const& rhs)
{
@@ -112,6 +205,16 @@ operator==(PathAsset const& lhs, PathAsset const& rhs)
rhs.value());
}
/** Append a PathAsset's value to a hash state.
*
* Dispatches to the appropriate `hash_append` overload for the active
* alternative (`Currency` or `MPTID`), enabling `PathAsset` to be used as
* a key in hash-based containers built on the `beast::uhash` infrastructure.
*
* @tparam Hasher A type satisfying the `beast::hash_append` Hasher concept.
* @param h The hash accumulator to append to.
* @param pathAsset The path asset whose value is appended.
*/
template <typename Hasher>
void
hash_append(Hasher& h, PathAsset const& pathAsset)
@@ -119,15 +222,41 @@ hash_append(Hasher& h, PathAsset const& pathAsset)
std::visit([&]<ValidPathAsset T>(T const& e) { hash_append(h, e); }, pathAsset.value());
}
/** Return whether a PathAsset represents native XRP.
*
* Free-function wrapper for `PathAsset::isXRP()`, provided for symmetry
* with the `isXRP()` overloads for `Currency`, `Asset`, and `STAmount`.
*
* @param asset The path asset to test.
* @return `true` if `asset` holds the XRP zero-currency sentinel.
*/
inline bool
isXRP(PathAsset const& asset)
{
return asset.isXRP();
}
/** Produce a human-readable string identifying a PathAsset.
*
* Dispatches to `to_string(Currency const&)` or `to_string(MPTID const&)`
* depending on the active alternative. For a `Currency` this yields the ISO
* 4217 ticker or `"XRP"`; for an `MPTID` it yields the base-58 encoded token
* identifier.
*
* @param asset The path asset to stringify.
* @return A descriptive string identifying the currency or MPT issuance.
*/
std::string
to_string(PathAsset const& asset);
/** Stream-insert a human-readable description of a PathAsset.
*
* Equivalent to `os << to_string(x)`. Intended for logging and diagnostics.
*
* @param os The output stream to write to.
* @param x The path asset to write.
* @return `os`, for chaining.
*/
std::ostream&
operator<<(std::ostream& os, PathAsset const& x);

View File

@@ -1,3 +1,13 @@
/** @file
* Canonical serialization for payment channel claim authorizations.
*
* Defines the single function that all three call sites — channel
* authorization (RPC), channel verification (RPC), and on-ledger
* claim validation (transaction engine) — must use to build the
* signed payload. Centralizing this here ensures that a signature
* produced off-ledger is always accepted on-ledger.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
@@ -7,6 +17,34 @@
namespace xrpl {
/** Serialize the signing payload for a payment channel claim authorization.
*
* Writes exactly three fields into @p msg in a protocol-defined order:
* the `HashPrefix::paymentChannelClaim` domain-separation tag (4 bytes),
* the 256-bit channel keylet @p key, and the authorized cumulative amount
* @p amt as a 64-bit drop count. The resulting byte sequence is what the
* channel sender signs and what the recipient or ledger verifies.
*
* This function is the single source of truth for the signed payload layout.
* It is called identically by `channel_authorize` (RPC), `channel_verify`
* (RPC), and `PaymentChannelClaim` preflight (transaction engine). Any drift
* between those sites would cause off-ledger signatures to fail on-ledger
* validation.
*
* @param msg Serializer to append the payload fields into. The caller is
* responsible for constructing the `Serializer` and, after this call,
* passing `msg.slice()` to the sign or verify primitive.
* @param key The 256-bit keylet of the payment channel ledger object. Binds
* the authorization to exactly one channel so it cannot be replayed
* against a different channel.
* @param amt The authorized cumulative ceiling in drops. The on-ledger claim
* validator rejects any claim whose running total exceeds this value.
*
* @note The `HashPrefix::paymentChannelClaim` tag (`'C','L','M',0x00`) is
* protocol-immutable. Changing it would invalidate all existing payment
* channel authorizations.
* @see HashPrefix::paymentChannelClaim
*/
inline void
serializePayChanAuthorization(Serializer& msg, uint256 const& key, XRPAmount const& amt)
{

View File

@@ -1,3 +1,17 @@
/** @file
* Central definition of XRPL's account-delegation permission system,
* used by the `DelegateSet` transaction type.
*
* Two numeric ranges partition the `sfPermissionValue` field stored
* on-ledger:
* - **Transaction-level** (≤ `UINT16_MAX`): `TxType + 1`, granting
* authority over an entire transaction type.
* - **Granular** (> `UINT16_MAX`, minimum 65537): covers a specific
* sub-operation within a transaction type (e.g., freezing a trustline
* without being able to authorize it).
*
* The `Permission` singleton is the runtime authority for both ranges.
*/
#pragma once
#include <xrpl/protocol/Rules.h>
@@ -9,12 +23,21 @@
#include <unordered_map>
namespace xrpl {
/**
* We have both transaction type permissions and granular type permissions.
* Since we will reuse the TransactionFormats to parse the Transaction
* Permissions, only the GranularPermissionType is defined here. To prevent
* conflicts with TxType, the GranularPermissionType is always set to a value
* greater than the maximum value of uint16.
/** Granular sub-operation permission values used by the delegation system.
*
* Each enumerator targets a specific capability within a parent transaction
* type, enabling fine-grained delegation without granting broad transaction-
* level authority. For example, `TrustlineFreeze` delegates only the ability
* to freeze a trustline via `ttTRUST_SET`, not to authorize or unfreeze.
*
* All values are greater than `UINT16_MAX` (minimum 65537), which keeps them
* numerically disjoint from transaction-level permissions (≤ `UINT16_MAX`).
* This invariant is asserted at startup inside the `Permission` constructor.
*
* Generated from `detail/permissions.macro` via the X-macro pattern. Adding
* a new sub-operation requires only a single `PERMISSION(...)` entry in that
* file.
*/
// Macro-generated, complex
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
@@ -30,27 +53,67 @@ enum GranularPermissionType : std::uint32_t {
#pragma pop_macro("PERMISSION")
};
/** Indicates whether a transaction type may be delegated in bulk via
* a transaction-level `DelegateSet` permission.
*
* The policy for each `TxType` is encoded in `detail/transactions.macro`
* as the `delegable` parameter of every `TRANSACTION(...)` entry.
* Sensitive types such as `ttACCOUNT_SET` and `ttREGULAR_KEY_SET` are
* `NotDelegable`; most operational types are `Delegable`.
*
* @note Bare enumerators (`xrpl::Delegable` / `xrpl::NotDelegable`) are
* required by preprocessor expansions in tests and macro-generated
* code; `enum class` would break that usage.
*/
// Injected bare enumerators (xrpl::delegable / xrpl::notDelegable) are required by preprocessor
// tricks in tests and macro-generated code; enum class would break that.
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum Delegation { Delegable, NotDelegable };
/** Central authority for XRPL's account-delegation permission system.
*
* A Meyer's singleton populated at first call to `getInstance()`. Its
* constructor expands `transactions.macro` and `permissions.macro` to build
* five immutable lookup maps covering every known transaction type and
* granular sub-operation. After construction the maps are never mutated,
* so all concurrent read access from transaction-processing threads is safe
* without synchronization.
*
* The two principal call sites are:
* - `DelegateSet::preflight()` — calls `isDelegable()` to validate each
* `sfPermissionValue` before it is written on-ledger.
* - `DelegateUtils` / transactors — call `getGranularTxType()` and related
* helpers to enforce granular limits at execution time.
*/
class Permission
{
private:
Permission();
/** Maps each `TxType` to the amendment required to use it, or `uint256{}` if none. */
std::unordered_map<std::uint16_t, uint256> txFeatureMap_;
/** Maps each `TxType` to its `Delegable` / `NotDelegable` policy tag. */
std::unordered_map<std::uint16_t, Delegation> delegableTx_;
/** Maps granular permission name strings to their `GranularPermissionType` values. */
std::unordered_map<std::string, GranularPermissionType> granularPermissionMap_;
/** Maps `GranularPermissionType` values to their name strings (inverse of `granularPermissionMap_`). */
std::unordered_map<GranularPermissionType, std::string> granularNameMap_;
/** Maps each `GranularPermissionType` to its parent `TxType`. */
std::unordered_map<GranularPermissionType, TxType> granularTxTypeMap_;
public:
/** Returns the process-wide singleton instance.
*
* Initialized on first call via a function-local `static`; C++11
* guarantees thread-safe initialization. The instance is never mutated
* after construction.
*
* @return A `const` reference to the singleton `Permission` object.
*/
static Permission const&
getInstance();
@@ -58,29 +121,125 @@ public:
Permission&
operator=(Permission const&) = delete;
/** Resolves a raw `sfPermissionValue` to its human-readable name.
*
* Checks the granular permission table first (values > `UINT16_MAX`).
* If unrecognized there, decodes the value as a transaction-level
* permission (`value - 1` = `TxType`) and delegates to `TxFormats` for
* the canonical name. Used by `STUInt32::getText()` and
* `STUInt32::getJson()` to render any `sfPermissionValue` as a string
* instead of a raw number.
*
* @param value Raw `sfPermissionValue` from the ledger.
* @return The permission name, or `std::nullopt` if `value` is not
* recognized as either a granular or transaction-level permission.
*/
[[nodiscard]] std::optional<std::string>
getPermissionName(std::uint32_t const value) const;
/** Looks up the numeric wire value of a granular permission by name.
*
* Used when deserializing `sfPermissionValue` from JSON (e.g., during
* `DelegateSet` preflight or RPC input parsing) to convert a
* human-readable name like `"TrustlineFreeze"` back to its `uint32_t`
* representation.
*
* @param name Case-sensitive granular permission name.
* @return The corresponding `uint32_t` wire value, or `std::nullopt` if
* `name` is not a known granular permission.
*/
[[nodiscard]] std::optional<std::uint32_t>
getGranularValue(std::string const& name) const;
/** Looks up the name of a granular permission by its enum value.
*
* Inverse of `getGranularValue`; used when serializing a granular
* permission value to human-readable output.
*
* @param value A `GranularPermissionType` enum value.
* @return The permission name string, or `std::nullopt` if `value` is
* not a known granular permission.
*/
[[nodiscard]] std::optional<std::string>
getGranularName(GranularPermissionType const& value) const;
/** Returns the parent transaction type for a granular permission.
*
* Multiple granular permissions share the same parent `TxType`; for
* example, `TrustlineAuthorize`, `TrustlineFreeze`, and
* `TrustlineUnfreeze` all map to `ttTRUST_SET`. Used by `isDelegable()`
* and execution-time helpers to locate the relevant transactor context
* and required amendment for a granular sub-operation.
*
* @param gpType A `GranularPermissionType` enum value.
* @return The parent `TxType`, or `std::nullopt` if `gpType` is not a
* known granular permission.
*/
[[nodiscard]] std::optional<TxType>
getGranularTxType(GranularPermissionType const& gpType) const;
/** Returns the amendment required to use a transaction type, if any.
*
* A `uint256{}` stored in `txFeatureMap_` means the transaction type
* requires no enabling amendment. In that case `std::nullopt` is
* returned, signalling that the type is unconditionally available.
*
* @param txType A recognized transaction type.
* @return A const reference to the required amendment hash wrapped in
* `std::optional`, or `std::nullopt` if no amendment is required.
* @note Asserts in debug builds that `txType` is present in
* `txFeatureMap_`. Passing an unregistered `TxType` is a
* programming error (a transaction missing from `transactions.macro`).
*/
[[nodiscard]] std::optional<std::reference_wrapper<uint256 const>>
getTxFeature(TxType txType) const;
/** Determines whether a permission value may appear in a `DelegateSet`
* transaction under the current ledger rules.
*
* The check differs by permission kind:
* - **Granular** (value > `UINT16_MAX`): accepted whenever the value
* resolves to a known `GranularPermissionType`; no further gate is
* applied because granular permissions are inherently narrow.
* - **Transaction-level** (value ≤ `UINT16_MAX`): accepted only when the
* decoded `TxType` is recognized, its required amendment is currently
* enabled in `rules` (or no amendment is required), and the type is
* marked `Delegable` in `transactions.macro`.
*
* @param permissionValue Raw `sfPermissionValue` to validate.
* @param rules Active amendment rules for the current ledger.
* @return `true` if the permission may be granted, `false` otherwise.
* @note The amendment check prevents a transaction type from being
* delegated before the ledger feature that introduces it is live,
* even if the macro table already includes it.
*/
[[nodiscard]] bool
isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const;
// for tx level permission, permission value is equal to tx type plus one
/** Converts a `TxType` to its transaction-level permission value.
*
* Transaction-level permissions are encoded as `TxType + 1`. The `+1`
* offset ensures zero is never a valid permission value and keeps the
* entire range within `uint16` (transaction-level permissions ≤
* `UINT16_MAX`).
*
* @param type A transaction type.
* @return The corresponding `uint32_t` permission value (`TxType + 1`).
* @see permissionToTxType
*/
static uint32_t
txToPermissionType(TxType const& type);
// tx type value is permission value minus one
/** Converts a transaction-level permission value back to its `TxType`.
*
* Inverse of `txToPermissionType`. Callers must verify that `value` is
* in the transaction-level range (≤ `UINT16_MAX`) before calling; this
* function performs no range check.
*
* @param value A transaction-level permission value (`TxType + 1`).
* @return The decoded `TxType` (`value - 1`).
* @see txToPermissionType
*/
static TxType
permissionToTxType(uint32_t const& value);
};

View File

@@ -1,3 +1,16 @@
/** @file
* Canonical source of XRPL protocol constants and boundary predicates.
*
* Every hard-coded numeric limit that, if changed silently, would cause a
* **hard fork** — a ledger-state disagreement between nodes running different
* software versions — is defined here. All constants are `constexpr` and
* therefore available at compile time with zero runtime overhead.
*
* @note Changing any value in this file without pairing the change with an
* amendment-gated detection mechanism will split the network.
*
* @ingroup protocol
*/
#pragma once
#include <xrpl/basics/ByteUtilities.h>
@@ -8,100 +21,182 @@
namespace xrpl {
/** Protocol specific constants.
This information is, implicitly, part of the protocol.
@note Changing these values without adding code to the
server to detect "pre-change" and "post-change"
will result in a hard fork.
@ingroup protocol
*/
/** Smallest legal byte size of a transaction. */
/** Smallest legal serialized size of a transaction, in bytes.
*
* Transactions below this threshold are trivially malformed and are rejected
* before deserialization begins.
*/
std::size_t constexpr kTX_MIN_SIZE_BYTES = 32;
/** Largest legal byte size of a transaction. */
/** Largest legal serialized size of a transaction, in bytes.
*
* The 1 MB cap protects node memory and network bandwidth. Transactions
* exceeding this limit are rejected on receipt without further processing.
*/
std::size_t constexpr kTX_MAX_SIZE_BYTES = megabytes(1);
/** The maximum number of unfunded offers to delete at once */
/** Maximum number of unfunded offers that may be removed in a single
* transaction pass.
*
* Unfunded-offer cleanup is opportunistic: stale offers are removed as a
* side-effect of offer placement. Capping the count keeps the worst-case
* execution time of a single transaction predictable.
*
* @note The asymmetry with `kEXPIRED_OFFER_REMOVE_LIMIT` (1000 vs 256)
* reflects that unfunded-offer removal was designed to handle larger
* batches; expired offers are discovered through a different, narrower
* path.
*/
std::size_t constexpr kUNFUNDED_OFFER_REMOVE_LIMIT = 1000;
/** The maximum number of expired offers to delete at once */
/** Maximum number of expired offers that may be removed in a single
* transaction pass.
*
* @see kUNFUNDED_OFFER_REMOVE_LIMIT for the rationale behind the asymmetric
* cap.
*/
std::size_t constexpr kEXPIRED_OFFER_REMOVE_LIMIT = 256;
/** The maximum number of metadata entries allowed in one transaction */
/** Maximum number of metadata entries a single transaction may produce.
*
* When a transaction would exceed this cap the transactor returns
* `tecOVERSIZE`, triggering a controlled teardown that applies the fee
* and rolls back ledger mutations rather than allowing unbounded metadata
* growth.
*/
std::size_t constexpr kOVERSIZE_META_DATA_CAP = 5200;
/** The maximum number of entries per directory page */
/** Maximum number of entries per owner-directory or offer-directory page.
*
* Keeping pages small bounds the work required to traverse a directory:
* each page hop visits at most 32 entries.
*/
std::size_t constexpr kDIR_NODE_MAX_ENTRIES = 32;
/** The maximum number of pages allowed in a directory
Made obsolete by fixDirectoryLimit amendment.
*/
/** Historical maximum number of pages in a single directory.
*
* This limit was enforced before the `fixDirectoryLimit` amendment.
* Post-amendment, directories may grow beyond 262 144 pages; this
* constant is retained for pre-amendment replay correctness.
*
* @note Pre-amendment code returns `tecDIR_FULL` when this limit is
* reached. Post-amendment, only unsigned-integer overflow can
* produce a null page index.
*/
std::uint64_t constexpr kDIR_NODE_MAX_PAGES = 262144;
/** The maximum number of items in an NFT page */
/** Maximum number of NFToken entries per NFT directory page. */
std::size_t constexpr kDIR_MAX_TOKENS_PER_PAGE = 32;
/** The maximum number of owner directory entries for account to be deletable */
/** Maximum number of owner-directory entries an account may hold and still
* be eligible for deletion via `AccountDelete`.
*
* Accounts with more than 1000 directory entries cannot be deleted; this
* protects against unbounded cleanup work within a single transaction.
*/
std::size_t constexpr kMAX_DELETABLE_DIR_ENTRIES = 1000;
/** The maximum number of token offers that can be canceled at once */
/** Maximum number of NFToken offers that may be cancelled in a single
* `NFTokenCancelOffer` transaction.
*/
std::size_t constexpr kMAX_TOKEN_OFFER_CANCEL_COUNT = 500;
/** The maximum number of offers in an offer directory for NFT to be burnable */
/** Maximum number of NFToken offers that must be cleaned up before an NFT
* can be burned.
*
* An NFT with more than 500 live offers cannot be burned until the excess
* offers are cancelled first.
*/
std::size_t constexpr kMAX_DELETABLE_TOKEN_OFFER_ENTRIES = 500;
/** The maximum token transfer fee allowed.
Token transfer fees can range from 0% to 50% and are specified in tenths of
a basis point; that is a value of 1000 represents a transfer fee of 1% and
a value of 10000 represents a transfer fee of 10%.
Note that for extremely low transfer fees values, it is possible that the
calculated fee will be 0.
/** Maximum NFToken transfer fee, expressed in tenths of a basis point.
*
* Transfer fees range from 0% to 50%. A value of 1 000 represents 1% and
* a value of 50 000 represents 50%. For very low fee values the computed
* fee amount may round down to zero drops.
*/
std::uint16_t constexpr kMAX_TRANSFER_FEE = 50000;
/** There are 10,000 basis points (bips) in 100%.
/** Number of basis points (bips) in 100% (unity).
*
* Basis points represent 0.01%.
* One basis point equals 0.01%. To compute the share of a value `X`
* corresponding to `B` bips, use `X * B / kBIPS_PER_UNITY`. To convert
* a whole-percentage `P` to bips, use `P * kBIPS_PER_UNITY / 100`
* (or simply call `percentageToBips(P)`).
*
* Given a value X, to find the amount for B bps,
* use X * B / bipsPerUnity
* Example: 10% coverage on 999 XRP (999 000 000 drops) =
* `999'000'000 * 1'000 / 10'000` = 99 900 000 drops.
*
* Example: If a loan broker has 999 XRP of debt, and must maintain 1,000 bps of
* that debt as cover (10%), then the minimum cover amount is 999,000,000 drops
* * 1000 / bipsPerUnity = 99,900,00 drops or 99.9 XRP.
*
* Given a percentage P, to find the number of bps that percentage represents,
* use P * bipsPerUnity.
*
* Example: 50% is 0.50 * bipsPerUnity = 5,000 bps.
* All ledger fee and rate arithmetic uses integer bips to guarantee
* bit-identical results across all validator platforms.
*/
Bips32 constexpr kBIPS_PER_UNITY(100 * 100);
static_assert(kBIPS_PER_UNITY == Bips32{10'000});
/** Number of tenth-basis-points in 100% (unity).
*
* One tenth-basis-point equals 0.001%. Use `percentageToTenthBips(P)`
* to convert a whole percentage, or `tenthBipsOfValue(value, rate)` to
* apply a rate to a value.
*/
TenthBips32 constexpr kTENTH_BIPS_PER_UNITY(kBIPS_PER_UNITY.value() * 10);
static_assert(kTENTH_BIPS_PER_UNITY == TenthBips32(100'000));
/** Convert a whole-percentage value to a strongly-typed `Bips32`.
*
* Uses integer division; fractional basis points are truncated.
*
* @param percentage An integer percentage in [0, 100].
* @return The equivalent number of basis points as a `Bips32`.
*/
constexpr Bips32
percentageToBips(std::uint32_t percentage)
{
return Bips32(percentage * kBIPS_PER_UNITY.value() / 100);
}
/** Convert a whole-percentage value to a strongly-typed `TenthBips32`.
*
* Uses integer division; fractional tenth-bips are truncated.
*
* @param percentage An integer percentage in [0, 100].
* @return The equivalent number of tenth-basis-points as a `TenthBips32`.
*/
constexpr TenthBips32
percentageToTenthBips(std::uint32_t percentage)
{
return TenthBips32(percentage * kTENTH_BIPS_PER_UNITY.value() / 100);
}
/** Compute the basis-point share of a value using integer arithmetic.
*
* Calculates `value * bips / kBIPS_PER_UNITY` without floating point,
* guaranteeing deterministic results on all platforms.
*
* @tparam T Numeric type of the value (must support `*` and `/`).
* @tparam TBips Underlying storage type of the `Bips` wrapper.
* @param value The base amount to take a share of.
* @param bips The rate in basis points.
* @return The share of `value` at the given rate, truncated toward zero.
*/
template <typename T, class TBips>
constexpr T
bipsOfValue(T value, Bips<TBips> bips)
{
return value * bips.value() / kBIPS_PER_UNITY.value();
}
/** Compute the tenth-basis-point share of a value using integer arithmetic.
*
* Calculates `value * bips / kTENTH_BIPS_PER_UNITY` without floating
* point, guaranteeing deterministic results on all platforms.
*
* @tparam T Numeric type of the value (must support `*` and `/`).
* @tparam TBips Underlying storage type of the `TenthBips` wrapper.
* @param value The base amount to take a share of.
* @param bips The rate in tenth-basis-points.
* @return The share of `value` at the given rate, truncated toward zero.
*/
template <typename T, class TBips>
constexpr T
tenthBipsOfValue(T value, TenthBips<TBips> bips)
@@ -109,202 +204,293 @@ tenthBipsOfValue(T value, TenthBips<TBips> bips)
return value * bips.value() / kTENTH_BIPS_PER_UNITY.value();
}
/** Rate and limit constants specific to the on-ledger lending protocol. */
namespace Lending {
/** The maximum management fee rate allowed by a loan broker in 1/10 bips.
Valid values are between 0 and 10% inclusive.
*/
/** Maximum management fee a LoanBroker may charge, in tenth-basis-points.
*
* Valid values are in [0, 10%]. Stored as `TenthBips16` (fits in
* `uint16_t`) because 10 000 < 65 535.
*/
TenthBips16 constexpr kMAX_MANAGEMENT_FEE_RATE(
unsafeCast<std::uint16_t>(percentageToTenthBips(10).value()));
static_assert(kMAX_MANAGEMENT_FEE_RATE == TenthBips16(std::uint16_t(10'000u)));
/** The maximum coverage rate required of a loan broker in 1/10 bips.
Valid values are between 0 and 100% inclusive.
*/
/** Maximum coverage rate a LoanBroker must maintain, in tenth-basis-points.
*
* The coverage rate specifies the minimum fraction of outstanding loan
* debt that the broker must hold as collateral. Valid values are in
* [0, 100%].
*/
TenthBips32 constexpr kMAX_COVER_RATE = percentageToTenthBips(100);
static_assert(kMAX_COVER_RATE == TenthBips32(100'000u));
/** The maximum overpayment fee on a loan in 1/10 bips.
*
Valid values are between 0 and 100% inclusive.
*/
/** Maximum overpayment fee on a loan, in tenth-basis-points.
*
* Applied when a borrower pays more than the scheduled amount. Valid
* values are in [0, 100%].
*/
TenthBips32 constexpr kMAX_OVERPAYMENT_FEE = percentageToTenthBips(100);
static_assert(kMAX_OVERPAYMENT_FEE == TenthBips32(100'000u));
/** Annualized interest rate of the Loan in 1/10 bips.
/** Maximum annualized interest rate on a Loan, in tenth-basis-points.
*
* Valid values are between 0 and 100% inclusive.
* Valid values are in [0, 100%].
*/
TenthBips32 constexpr kMAX_INTEREST_RATE = percentageToTenthBips(100);
static_assert(kMAX_INTEREST_RATE == TenthBips32(100'000u));
/** The maximum premium added to the interest rate for late payments on a loan
* in 1/10 bips.
/** Maximum late-payment interest premium on a Loan, in tenth-basis-points.
*
* Valid values are between 0 and 100% inclusive.
* This rate is added to the base interest rate when payments are overdue.
* Valid values are in [0, 100%].
*/
TenthBips32 constexpr kMAX_LATE_INTEREST_RATE = percentageToTenthBips(100);
static_assert(kMAX_LATE_INTEREST_RATE == TenthBips32(100'000u));
/** The maximum close interest rate charged for repaying a loan early in 1/10
* bips.
/** Maximum early-repayment (close) interest rate on a Loan, in
* tenth-basis-points.
*
* Valid values are between 0 and 100% inclusive.
* Charged when a borrower repays a loan ahead of schedule. Valid values
* are in [0, 100%].
*/
TenthBips32 constexpr kMAX_CLOSE_INTEREST_RATE = percentageToTenthBips(100);
static_assert(kMAX_CLOSE_INTEREST_RATE == TenthBips32(100'000u));
/** The maximum overpayment interest rate charged on loan overpayments in 1/10
* bips.
/** Maximum overpayment interest rate charged on loan overpayments, in
* tenth-basis-points.
*
* Valid values are between 0 and 100% inclusive.
* Valid values are in [0, 100%].
*/
TenthBips32 constexpr kMAX_OVERPAYMENT_INTEREST_RATE = percentageToTenthBips(100);
static_assert(kMAX_OVERPAYMENT_INTEREST_RATE == TenthBips32(100'000u));
/** LoanPay transaction cost will be one base fee per X combined payments
/** Number of loan payments per base-fee increment charged by `LoanPay`.
*
* The number of payments is estimated based on the Amount paid and the Loan's
* Fixed Payment size. Overpayments (indicated with the tfLoanOverpayment flag)
* count as one more payment.
* The fee is estimated from the transaction `Amount` divided by the
* loan's fixed payment size. Overpayments (flagged with
* `tfLoanOverpayment`) count as one additional payment in the estimate.
* One base fee unit is charged for every 5 estimated payments.
*
* This number was chosen arbitrarily, but should not be changed once released
* without an amendment
* @note This value was chosen arbitrarily and is amendment-locked once
* released: changing it without an amendment would alter the fee
* schedule for existing `LoanPay` transactions.
* @see kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION
*/
static constexpr int kLOAN_PAYMENTS_PER_FEE_INCREMENT = 5;
/** Maximum number of combined payments that a LoanPay transaction will process
/** Hard cap on the number of combined payments processed by one `LoanPay`.
*
* This limit is enforced during the loan payment process, and thus is not
* estimated. If the limit is hit, no further payments or overpayments will be
* processed, no matter how much of the transaction Amount is left, but the
* transaction will succeed with the payments that have been processed up to
* that point.
* This limit is enforced during execution, not during fee estimation.
* When the cap is reached the transaction succeeds with the payments
* processed so far; any remaining `Amount` is not applied.
*
* This limit is independent of loanPaymentsPerFeeIncrement, so a transaction
* could potentially be charged for many more payments than actually get
* processed. Users should take care not to submit a transaction paying more
* than loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment. Because
* overpayments are charged as a payment, if submitting
* loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment, users should not
* set the tfLoanOverpayment flag.
* Because the fee is based on the *estimated* payment count (derived from
* `Amount / PeriodicPayment`) and the cap is enforced on the *actual*
* count, a transaction can be charged for more payments than it processes.
* Submitters should not exceed
* `kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION * Loan.PeriodicPayment` in
* `Amount`, and should omit `tfLoanOverpayment` if paying exactly that
* much.
*
* Even though they're independent, loanMaximumPaymentsPerTransaction should be
* a multiple of loanPaymentsPerFeeIncrement.
*
* This number was chosen arbitrarily, but should not be changed once released
* without an amendment
* @note `kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION` must remain a multiple
* of `kLOAN_PAYMENTS_PER_FEE_INCREMENT`; this invariant is checked
* at startup via `static_assert` in LoanPay.cpp. Both values are
* amendment-locked once released.
*/
static constexpr int kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION = 100;
} // namespace Lending
/** The maximum length of a URI inside an NFT */
/** Maximum byte length of a URI stored in an NFToken. */
std::size_t constexpr kMAX_TOKEN_URI_LENGTH = 256;
/** The maximum length of a Data element inside a DID */
/** Maximum byte length of the `Data` field (DID document) in a DID object. */
std::size_t constexpr kMAX_DID_DOCUMENT_LENGTH = 256;
/** The maximum length of a URI inside a DID */
/** Maximum byte length of the `URI` field in a DID object. */
std::size_t constexpr kMAX_DIDURI_LENGTH = 256;
/** The maximum length of an Attestation inside a DID */
/** Maximum byte length of the `Attestation` field in a DID object. */
std::size_t constexpr kMAX_DID_DATA_LENGTH = 256;
/** The maximum length of a domain */
/** Maximum byte length of an account `Domain` field. */
std::size_t constexpr kMAX_DOMAIN_LENGTH = 256;
/** The maximum length of a URI inside a Credential */
/** Maximum byte length of the `URI` field in a Credential object. */
std::size_t constexpr kMAX_CREDENTIAL_URI_LENGTH = 256;
/** The maximum length of a CredentialType inside a Credential */
/** Maximum byte length of the `CredentialType` field in a Credential object.
*
* Narrower than the 256-byte default to keep credential-type strings
* human-readable and prevent abuse of the type field as an arbitrary blob.
*/
std::size_t constexpr kMAX_CREDENTIAL_TYPE_LENGTH = 64;
/** The maximum number of credentials can be passed in array */
/** Maximum number of credentials that may appear in a transaction's
* `Credentials` array.
*/
std::size_t constexpr kMAX_CREDENTIALS_ARRAY_SIZE = 8;
/** The maximum number of credentials can be passed in array for permissioned
* domain */
/** Maximum number of credentials that a permissioned domain may reference. */
std::size_t constexpr kMAX_PERMISSIONED_DOMAIN_CREDENTIALS_ARRAY_SIZE = 10;
/** The maximum length of MPTokenMetadata */
/** Maximum byte length of the `MPTokenMetadata` field on an MPTokenIssuance. */
std::size_t constexpr kMAX_MP_TOKEN_METADATA_LENGTH = 1024;
/** The maximum amount of MPTokenIssuance */
/** Maximum quantity representable by an MPToken amount field.
*
* Equal to `INT64_MAX` (2^63 1). The `static_assert` below guarantees
* that the XRPL `Number` type can represent every valid MPToken quantity
* without overflow.
*/
std::uint64_t constexpr kMAX_MP_TOKEN_AMOUNT = 0x7FFF'FFFF'FFFF'FFFFull;
static_assert(Number::kMAX_REP >= kMAX_MP_TOKEN_AMOUNT);
/** The maximum length of Data payload */
/** Maximum byte length of the `Data` payload field. */
std::size_t constexpr kMAX_DATA_PAYLOAD_LENGTH = 256;
/** Vault withdrawal policies */
/** Vault withdrawal policy: first-come, first-served.
*
* The numeric value 1 is the wire-stable identifier for this strategy;
* it must not change once released.
*/
std::uint8_t constexpr kVAULT_STRATEGY_FIRST_COME_FIRST_SERVE = 1;
/** Default IOU scale factor for a Vault */
/** Default IOU-to-share scale exponent for a Vault.
*
* When no explicit scale is specified at Vault creation the scale
* defaults to 6, meaning one IOU unit maps to 10^6 shares. This
* applies only to IOU-backed vaults; native-asset and MPT vaults always
* use scale 0.
*/
std::uint8_t constexpr kVAULT_DEFAULT_IOU_SCALE = 6;
/** Maximum scale factor for a Vault. The number is chosen to ensure that
1 IOU can be always converted to shares.
10^19 > maxMPTokenAmount (2^64-1) > 10^18 */
/** Maximum IOU-to-share scale exponent for a Vault.
*
* Chosen so that exactly one IOU unit can always be converted to at
* least one share: 10^19 > `kMAX_MP_TOKEN_AMOUNT` (≈ 2^63) > 10^18.
* Preflight rejects any `VaultCreate` that specifies a scale above this
* value with `temMALFORMED`. Applies only to IOU-backed vaults.
*/
std::uint8_t constexpr kVAULT_MAXIMUM_IOU_SCALE = 18;
/** Maximum recursion depth for vault shares being put as an asset inside
* another vault; counted from 0 */
/** Maximum recursion depth when checking whether a vault's asset is itself
* backed by another vault.
*
* Counted from 0, so a depth of 5 permits at most 6 levels of nesting.
* This prevents pathological chains from consuming unbounded stack space
* during asset-validation traversal.
*/
std::uint8_t constexpr kMAX_ASSET_CHECK_DEPTH = 5;
/** A ledger index. */
/** Ledger sequence number type.
*
* A named alias for `uint32_t` that makes function signatures
* self-documenting wherever ledger positions are passed.
*/
using LedgerIndex = std::uint32_t;
/** Number of ledgers between consecutive flag-ledger boundaries.
*
* Every 256 ledgers the network applies accumulated validator votes for
* fee adjustments, reserve requirements, amendment activation, and
* Negative UNL reliability scoring. Both `isFlagLedger()` and
* `isVotingLedger()` test `seq % kFLAG_LEDGER_INTERVAL == 0`; the
* semantic distinction between the two predicates is resolved by callers
* via a `+1` offset on the sequence number they pass.
*
* @note This constant is an implicit part of the wire protocol. Changing
* it without an amendment-gated migration path will cause a hard fork.
*/
std::uint32_t constexpr kFLAG_LEDGER_INTERVAL = 256;
/** Returns true if the given ledgerIndex is a voting ledgerIndex */
/** Return `true` if @p seq is a voting ledger.
*
* Semantically, this asks: "will the ledger built *on top of* `seq`
* be a flag ledger?" Callers therefore pass `seq + 1` (the sequence of
* the ledger currently being assembled). `RCLConsensus` uses this
* predicate to decide whether to inject Negative UNL pseudo-transactions
* for the new consensus round.
*
* The arithmetic is identical to `isFlagLedger`; the two names exist to
* make the `+1` offset explicit at each call site without embedding it
* inside these functions.
*
* @param seq The ledger index to test (typically the previous ledger's
* sequence plus one).
* @return `true` if `seq % kFLAG_LEDGER_INTERVAL == 0`.
* @see isFlagLedger
*/
bool
isVotingLedger(LedgerIndex seq);
/** Returns true if the given ledgerIndex is a flag ledgerIndex */
/** Return `true` if @p seq is a flag ledger.
*
* A flag ledger is any ledger whose sequence number is an exact multiple
* of `kFLAG_LEDGER_INTERVAL` (256). It is the ledger in which fee-vote
* and amendment pseudo-transactions are applied, and in which Negative
* UNL reliability updates take effect. `Change::doApply` and
* `FeeVoteImpl` gate their parameter-update logic on this predicate.
*
* Callers pass the ledger's **own** sequence number to ask "has this
* ledger already crossed the boundary?", as opposed to `isVotingLedger`,
* which is called with `seq + 1`.
*
* @param seq The ledger index to test.
* @return `true` if `seq % kFLAG_LEDGER_INTERVAL == 0`.
* @see isVotingLedger
*/
bool
isFlagLedger(LedgerIndex seq);
/** A transaction identifier.
The value is computed as the hash of the
canonicalized, serialized transaction object.
*/
/** Transaction identifier type.
*
* A 256-bit hash computed over the canonicalized, serialized transaction
* object using `HashPrefix::transactionID` as the domain separator.
*/
using TxID = uint256;
/** The maximum number of trustlines to delete as part of AMM account
* deletion cleanup.
/** Maximum number of AMM trust lines that may be deleted as part of an
* AMM account-deletion cleanup pass.
*/
std::uint16_t constexpr kMAX_DELETABLE_AMM_TRUST_LINES = 512;
/** The maximum length of a URI inside an Oracle */
/** Maximum byte length of the `URI` field in an Oracle object. */
std::size_t constexpr kMAX_ORACLE_URI = 256;
/** The maximum length of a Provider inside an Oracle */
/** Maximum byte length of the `Provider` field in an Oracle object. */
std::size_t constexpr kMAX_ORACLE_PROVIDER = 256;
/** The maximum size of a data series array inside an Oracle */
/** Maximum number of price data-series entries in an Oracle object. */
std::size_t constexpr kMAX_ORACLE_DATA_SERIES = 10;
/** The maximum length of a SymbolClass inside an Oracle */
/** Maximum byte length of the `SymbolClass` field in an Oracle object. */
std::size_t constexpr kMAX_ORACLE_SYMBOL_CLASS = 16;
/** The maximum allowed time difference between lastUpdateTime and the time
of the last closed ledger
*/
/** Maximum allowed age of an Oracle price update, in seconds.
*
* `OracleSet` rejects updates whose `LastUpdateTime` differs from the
* last-closed-ledger close time by more than 300 seconds (5 minutes).
*/
std::size_t constexpr kMAX_LAST_UPDATE_TIME_DELTA = 300;
/** The maximum price scaling factor
*/
/** Maximum price-scaling exponent accepted in an Oracle object. */
std::size_t constexpr kMAX_PRICE_SCALE = 20;
/** The maximum percentage of outliers to trim
/** Maximum percentage of outlier data points to trim in Oracle price
* aggregation.
*/
std::size_t constexpr kMAX_TRIM = 25;
/** The maximum number of delegate permissions an account can grant
*/
/** Maximum number of granular delegate permissions an account may grant. */
std::size_t constexpr kPERMISSION_MAX_SIZE = 10;
/** The maximum number of transactions that can be in a batch. */
/** Maximum number of inner transactions in a single Batch transaction.
*
* Enforced during preflight; batches exceeding this count are rejected.
* The limit directly bounds the worst-case compute cost for batch
* signature validation and fee calculation.
*/
std::size_t constexpr kMAX_BATCH_TX_COUNT = 8;
} // namespace xrpl

View File

@@ -16,35 +16,31 @@
namespace xrpl {
/** A public key.
Public keys are used in the public-key cryptography
system used to verify signatures attached to messages.
The format of the public key is XRPL specific,
information needed to determine the cryptosystem
parameters used is stored inside the key.
As of this writing two systems are supported:
secp256k1
ed25519
secp256k1 public keys consist of a 33 byte
compressed public key, with the lead byte equal
to 0x02 or 0x03.
The ed25519 public keys consist of a 1 byte
prefix constant 0xED, followed by 32 bytes of
public key data.
*/
/** Immutable 33-byte value type holding an XRPL public key.
*
* Supports both secp256k1 and Ed25519 cryptosystems. The lead byte acts as
* a self-describing type tag — `0x02`/`0x03` for secp256k1 compressed keys,
* `0xED` for Ed25519 keys (an XRPL-specific prefix that pads the native
* 32-byte Ed25519 key to the common 33-byte size). This uniform encoding
* allows `publicKeyType()` to identify the algorithm in O(1) from the raw
* bytes alone, with no external metadata.
*
* The default constructor is deleted; the only construction path is from a
* `Slice`. If the slice does not represent a recognized key format,
* construction calls `LogicError` (process termination) rather than
* throwing — an invalid key at this point indicates a programming error,
* not a recoverable runtime condition. Any live `PublicKey` object is
* therefore always well-formed and algorithm-identified.
*
* The implicit conversion to `Slice` is intentional: it lets `PublicKey`
* flow into serialization and hashing APIs without explicit casting.
*/
class PublicKey
{
protected:
// All the constructed public keys are valid, non-empty and contain 33
// bytes of data.
/** Uniform storage size in bytes for all supported key types. */
static constexpr std::size_t kSIZE = 33;
std::uint8_t buf_[kSIZE]{}; // should be large enough
std::uint8_t buf_[kSIZE]{};
public:
using const_iterator = std::uint8_t const*;
@@ -56,72 +52,89 @@ public:
PublicKey&
operator=(PublicKey const& other);
/** Create a public key.
Preconditions:
publicKeyType(slice) != std::nullopt
*/
/** Construct from a raw byte slice.
*
* Copies exactly 33 bytes from `slice` after verifying that the bytes
* represent a recognized key format (secp256k1 or Ed25519). Calls
* `LogicError` — terminating the process — if the slice is undersized
* or does not pass `publicKeyType()`.
*
* @param slice Raw bytes to construct from; must satisfy
* `publicKeyType(slice) != std::nullopt`.
* @note Use `publicKeyType()` to validate untrusted input before
* constructing; `parseBase58<PublicKey>` does this automatically
* for Base58-encoded keys.
*/
explicit PublicKey(Slice const& slice);
/** Return a pointer to the raw 33-byte key buffer. */
[[nodiscard]] std::uint8_t const*
data() const noexcept
{
return buf_;
}
/** Return the fixed size of all `PublicKey` objects (always 33). */
static std::size_t
size() noexcept
{
return kSIZE;
}
/** Return an iterator to the first byte of the key buffer. */
[[nodiscard]] const_iterator
begin() const noexcept
{
return buf_;
}
/** Return a const iterator to the first byte of the key buffer. */
[[nodiscard]] const_iterator
cbegin() const noexcept
{
return buf_;
}
/** Return an iterator past the last byte of the key buffer. */
[[nodiscard]] const_iterator
end() const noexcept
{
return buf_ + kSIZE;
}
/** Return a const iterator past the last byte of the key buffer. */
[[nodiscard]] const_iterator
cend() const noexcept
{
return buf_ + kSIZE;
}
/** Return a `Slice` view over the 33-byte key buffer. */
[[nodiscard]] Slice
slice() const noexcept
{
return {buf_, kSIZE};
}
/** Implicit conversion to `Slice` for use with serialization APIs. */
operator Slice() const noexcept
{
return slice();
}
};
/** Print the public key to a stream.
*/
/** Write the public key as a hex string to a stream. */
std::ostream&
operator<<(std::ostream& os, PublicKey const& pk);
/** Return `true` if both keys hold identical 33-byte representations. */
inline bool
operator==(PublicKey const& lhs, PublicKey const& rhs)
{
return std::memcmp(lhs.data(), rhs.data(), rhs.size()) == 0;
}
/** Return `true` if `lhs` is lexicographically less than `rhs`. */
inline bool
operator<(PublicKey const& lhs, PublicKey const& rhs)
{
@@ -129,6 +142,15 @@ operator<(PublicKey const& lhs, PublicKey const& rhs)
lhs.data(), lhs.data() + lhs.size(), rhs.data(), rhs.data() + rhs.size());
}
/** Feed the raw 33-byte key into a hash algorithm.
*
* Enables `PublicKey` to be used as a key in unordered containers via
* `boost::hash` or any other `hash_append`-compatible hasher.
*
* @tparam Hasher A `hash_append`-compatible hasher type.
* @param h The hasher to feed bytes into.
* @param pk The key whose bytes are appended.
*/
template <class Hasher>
void
hash_append(Hasher& h, PublicKey const& pk)
@@ -136,6 +158,13 @@ hash_append(Hasher& h, PublicKey const& pk)
h(pk.data(), pk.size());
}
/** Serialization bridge between `STBlob` fields and `PublicKey` values.
*
* This specialization plugs `PublicKey` into XRPL's typed serialization
* framework. It allows `get<PublicKey>` and `set<PublicKey>` on `STBlob`
* fields in serialized ledger objects and transactions without any
* conversion boilerplate at call sites.
*/
template <>
struct STExchange<STBlob, PublicKey>
{
@@ -143,12 +172,14 @@ struct STExchange<STBlob, PublicKey>
using value_type = PublicKey;
/** Read a `PublicKey` from an `STBlob` field into `t`. */
static void
get(std::optional<value_type>& t, STBlob const& u)
{
t.emplace(Slice(u.data(), u.size()));
}
/** Write a `PublicKey` into a new `STBlob` for the given field. */
static std::unique_ptr<STBlob>
set(SField const& f, PublicKey const& t)
{
@@ -158,55 +189,86 @@ struct STExchange<STBlob, PublicKey>
//------------------------------------------------------------------------------
/** Encode a public key as a Base58Check string with a token-type prefix.
*
* @param type The `TokenType` prefix to use (e.g. `TokenType::NodePublic`
* for validator keys, `TokenType::AccountPublic` for signing keys).
* @param pk The key to encode.
* @return The Base58Check-encoded string.
*/
inline std::string
toBase58(TokenType type, PublicKey const& pk)
{
return encodeBase58Token(type, pk.data(), pk.size());
}
/** Decode a Base58Check-encoded public key.
*
* Validates the token-type prefix and that the decoded bytes represent a
* recognized key format. Safe to call on untrusted input.
*
* @param type The expected `TokenType` prefix.
* @param s The Base58Check-encoded string to decode.
* @return A `PublicKey` on success, or `std::nullopt` if the string is
* malformed, uses the wrong token type, or the decoded bytes are not
* a valid secp256k1 or Ed25519 key.
*/
template <>
std::optional<PublicKey>
parseBase58(TokenType type, std::string const& s);
enum class ECDSACanonicality { Canonical, FullyCanonical };
/** Canonicality level of a DER-encoded secp256k1 ECDSA signature.
*
* For any signed message, both `(R, S)` and `(R, G-S)` are mathematically
* valid ECDSA signatures (where G is the secp256k1 curve order). Accepting
* both enables transaction malleability attacks. XRPL prevents this by
* requiring *fully canonical* signatures — where `S ≤ G-S` — for new
* transactions.
*/
enum class ECDSACanonicality {
/** Both R and S are in `[1, G)` with no redundant zero padding, but
* `S > G/2`. Structurally valid; may be accepted in legacy contexts. */
Canonical,
/** Both R and S are in `[1, G)` and `S ≤ G-S`, making the signature
* unique and immune to the malleability flip. Required for new XRPL
* transactions. */
FullyCanonical
};
/** Determines the canonicality of a signature.
A canonical signature is in its most reduced form.
For example the R and S components do not contain
additional leading zeroes. However, even in
canonical form, (R,S) and (R,G-S) are both
valid signatures for message M.
Therefore, to prevent malleability attacks we
define a fully canonical signature as one where:
R < G - S
where G is the curve order.
This routine returns std::nullopt if the format
of the signature is invalid (for example, the
points are encoded incorrectly).
@return std::nullopt if the signature fails
validity checks.
@note Only the format of the signature is checked,
no verification cryptography is performed.
*/
/** Determine the canonicality of a DER-encoded secp256k1 ECDSA signature.
*
* Validates the DER structure (`0x30 <len> 0x02 <R> 0x02 <S>`), checks
* that R and S are properly encoded integers (no negative encoding, no
* redundant zero padding), and compares them against the secp256k1 curve
* order G. Returns `FullyCanonical` when `S ≤ G-S`, `Canonical` when
* `S > G-S` but the signature is otherwise structurally sound.
*
* @param sig DER-encoded ECDSA signature to examine.
* @return `ECDSACanonicality::FullyCanonical` if `S ≤ G-S`,
* `ECDSACanonicality::Canonical` if `S > G-S` but structurally valid,
* or `std::nullopt` if the encoding is malformed (wrong header bytes,
* invalid integer components, R or S outside the curve order, or
* trailing bytes present).
* @note Only the structure and canonicality of the encoding are checked;
* no cryptographic verification is performed.
*/
std::optional<ECDSACanonicality>
ecdsaCanonicality(Slice const& sig);
/** Returns the type of public key.
@return std::nullopt If the public key does not
represent a known type.
*/
/** Determine the algorithm encoded in a public key.
*
* Uses the lead byte as a self-describing type tag: `0xED` → Ed25519;
* `0x02`/`0x03` → secp256k1 compressed. Any other lead byte, or a slice
* that is not exactly 33 bytes, is unrecognized.
*
* @return The detected `KeyType`, or `std::nullopt` if the bytes do not
* match a known key format.
*/
/** @{ */
[[nodiscard]] std::optional<KeyType>
publicKeyType(Slice const& slice);
/** @copydoc publicKeyType(Slice const&) */
[[nodiscard]] inline std::optional<KeyType>
publicKeyType(PublicKey const& publicKey)
{
@@ -214,7 +276,24 @@ publicKeyType(PublicKey const& publicKey)
}
/** @} */
/** Verify a secp256k1 signature on the digest of a message. */
/** Verify a secp256k1 ECDSA signature against a pre-computed digest.
*
* Validates DER structure and canonicality before calling libsecp256k1.
* When `mustBeFullyCanonical` is `false` and the signature is merely
* canonical (S > G/2), the S component is normalized to its low form via
* `secp256k1_ecdsa_signature_normalize` before verification — preserving
* backward compatibility without accepting truly malformed encodings.
*
* @param publicKey A secp256k1 public key. Passing an Ed25519 key calls
* `LogicError` (programming error).
* @param digest The 256-bit digest over which the signature was produced.
* @param sig DER-encoded ECDSA signature.
* @param mustBeFullyCanonical If `true` (default), reject signatures where
* `S > G/2`. If `false`, accept them after S normalization.
* @return `true` if the signature is cryptographically valid for the given
* key and digest; `false` for any structural, canonicality, or
* cryptographic failure.
*/
[[nodiscard]] bool
verifyDigest(
PublicKey const& publicKey,
@@ -222,22 +301,65 @@ verifyDigest(
Slice const& sig,
bool mustBeFullyCanonical = true) noexcept;
/** Verify a signature on a message.
With secp256k1 signatures, the data is first hashed with
SHA512-Half, and the resulting digest is signed.
*/
/** Verify a signature over a raw message for either supported key type.
*
* Dispatches on the cryptosystem detected from `publicKey`:
* - **secp256k1**: hashes `m` with SHA512-Half (256-bit digest) and
* delegates to `verifyDigest` with `mustBeFullyCanonical = true`.
* - **Ed25519**: checks that the signature scalar S is below the Ed25519
* subgroup order, then calls the underlying `ed25519_sign_open` library
* after stripping the XRPL-specific `0xED` prefix byte that the library
* does not understand.
*
* @param publicKey The public key to verify against.
* @param m The message that was signed (raw bytes, not pre-hashed).
* @param sig The signature to verify.
* @return `true` if the signature is valid; `false` for any failure
* including unrecognized key type, non-canonical signature, or
* cryptographic mismatch.
*/
[[nodiscard]] bool
verify(PublicKey const& publicKey, Slice const& m, Slice const& sig) noexcept;
/** Calculate the 160-bit node ID from a node public key. */
/** Derive the 160-bit node identity from a public key.
*
* Applies RIPEMD-160(SHA-256(pubkey)) to produce the `NodeID` used in the
* peer-to-peer layer for validator routing and consensus tracking.
*
* @param pk The validator's public key (secp256k1 or Ed25519).
* @return The 160-bit `NodeID` identifying the validator on the network.
*/
NodeID
calcNodeID(PublicKey const&);
/** Derive the 160-bit on-ledger account address from a public key.
*
* Applies RIPEMD-160(SHA-256(pubkey)) — the same algorithm used in
* Bitcoin — to produce the `AccountID` that identifies the account on the
* XRP Ledger.
*
* @param pk The account's public key.
* @return The `AccountID` corresponding to `pk`.
* @note The implementation lives in `AccountID.cpp` rather than
* `PublicKey.cpp` due to header dependency ordering constraints.
*/
// VFALCO This belongs in AccountID.h but
// is here because of header issues
AccountID
calcAccountID(PublicKey const& pk);
/** Format a human-readable peer fingerprint for diagnostic logging.
*
* Produces a string of the form
* `"IP Address: <addr>[, Public Key: <NodePublic>][, Id: <id>]"` suitable
* for audit and connection-lifecycle log messages.
*
* @param address The peer's IP endpoint (always included).
* @param publicKey The peer's node public key, encoded as `NodePublic`
* Base58; omitted if not yet known (e.g., before the handshake).
* @param id An optional session identifier string; omitted if absent.
* @return A formatted fingerprint string.
*/
inline std::string
getFingerprint(
beast::IP::Endpoint const& address,
@@ -260,7 +382,22 @@ getFingerprint(
//------------------------------------------------------------------------------
namespace json {
/** Deserialize a `PublicKey` from a JSON field value.
*
* Accepts three formats in order:
* 1. Lowercase hex string of the raw 33-byte key.
* 2. `NodePublic` Base58Check encoding (validator keys).
* 3. `AccountPublic` Base58Check encoding (signing keys).
*
* This covers the variety of formats that appear in RPC requests and
* configuration files.
*
* @param v The JSON object to read from.
* @param field The field whose value is decoded.
* @return The decoded `PublicKey`.
* @throws `JsonTypeMismatchError` if the field value does not match any
* recognized format.
*/
template <>
inline xrpl::PublicKey
getOrThrow(json::Value const& v, xrpl::SField const& field)

View File

@@ -1,3 +1,14 @@
/** @file
* Defines `Quality` and `TAmounts`, the core exchange-rate abstractions
* used by XRPL's on-ledger decentralized exchange (DEX).
*
* `Quality` is the sortable representation of a currency exchange rate.
* The offer-crossing engine — ranking offers, scaling partial fills, and
* composing multi-hop paths — is expressed entirely in terms of these types.
*
* @see QualityFunction.h for the continuous AMM price-function extension.
*/
#pragma once
#include <xrpl/protocol/AmountConversions.h>
@@ -12,35 +23,50 @@
namespace xrpl {
/** Represents a pair of input and output currencies.
The input currency can be converted to the output
currency by multiplying by the rate, represented by
Quality.
For offers, "in" is always TakerPays and "out" is
always TakerGets.
*/
/** A typed pair of input and output amounts representing one side of a trade.
*
* For offers on the DEX, `in` is always `TakerPays` and `out` is always
* `TakerGets`. The template parameters allow instantiation over
* `STAmount`, `IOUAmount`, `XRPAmount`, and `MPTAmount`.
*
* @tparam In Type of the input (paying) amount.
* @tparam Out Type of the output (receiving) amount.
*/
template <class In, class Out>
struct TAmounts
{
TAmounts() = default;
/** Construct a zero-valued pair. */
TAmounts(beast::Zero, beast::Zero) : in(beast::kZERO), out(beast::kZERO)
{
}
/** Construct from explicit in and out amounts.
*
* @param in The input (TakerPays) amount.
* @param out The output (TakerGets) amount.
*/
TAmounts(In in, Out out) : in(std::move(in)), out(std::move(out))
{
}
/** Returns `true` if either quantity is not positive. */
/** Returns `true` if either quantity is not positive.
*
* Used by the offer-crossing engine to skip exhausted or invalid offers
* without further computation.
*/
[[nodiscard]] bool
empty() const noexcept
{
return in <= beast::kZERO || out <= beast::kZERO;
}
/** Adds `rhs` component-wise to this pair.
*
* @param rhs The amounts to add.
* @return Reference to `*this`.
*/
TAmounts&
operator+=(TAmounts const& rhs)
{
@@ -49,6 +75,11 @@ struct TAmounts
return *this;
}
/** Subtracts `rhs` component-wise from this pair.
*
* @param rhs The amounts to subtract.
* @return Reference to `*this`.
*/
TAmounts&
operator-=(TAmounts const& rhs)
{
@@ -57,12 +88,14 @@ struct TAmounts
return *this;
}
In in{};
Out out{};
In in{}; /**< Input (TakerPays) amount. */
Out out{}; /**< Output (TakerGets) amount. */
};
/** Canonical `TAmounts` alias used by the `STAmount`-based offer-crossing path. */
using Amounts = TAmounts<STAmount, STAmount>;
/** Returns `true` when both sides of two `TAmounts` pairs are equal. */
template <class In, class Out>
bool
operator==(TAmounts<In, Out> const& lhs, TAmounts<In, Out> const& rhs) noexcept
@@ -70,6 +103,7 @@ operator==(TAmounts<In, Out> const& lhs, TAmounts<In, Out> const& rhs) noexcept
return lhs.in == rhs.in && lhs.out == rhs.out;
}
/** Returns `true` when either side of two `TAmounts` pairs differs. */
template <class In, class Out>
bool
operator!=(TAmounts<In, Out> const& lhs, TAmounts<In, Out> const& rhs) noexcept
@@ -79,54 +113,107 @@ operator!=(TAmounts<In, Out> const& lhs, TAmounts<In, Out> const& rhs) noexcept
//------------------------------------------------------------------------------
// XRPL specific constant used for parsing qualities and other things
/** Unity exchange rate (1:1), scaled to XRPL's 9-decimal fixed-point precision.
*
* Appears throughout offer parsing and fee calculations wherever a 1:1
* exchange rate must be expressed as a raw integer.
*/
#define QUALITY_ONE 1'000'000'000
/** Represents the logical ratio of output currency to input currency.
Internally this is stored using a custom floating point representation,
as the inverse of the ratio, so that quality will be descending in
a sequence of actual values that represent qualities.
*/
/** The exchange rate of an offer, stored as an inverted packed floating-point
* integer so that higher-quality offers sort first under plain integer comparison.
*
* A `Quality` encodes the ratio `out / in` (TakerGets / TakerPays): how much
* output the taker receives per unit of input. Higher quality is better for
* the taker (more output per unit of input).
*
* The internal `uint64_t` uses the same bit layout as `STAmount` IOU encoding:
* the top 8 bits hold a biased exponent (stored value = actual exponent + 100)
* and the lower 56 bits hold an unsigned mantissa. Critically, the integer
* value is **inverted** relative to the economic concept — a *higher* quality
* corresponds to a *lower* `uint64_t` — so that ascending integer order in the
* ledger's offer directories corresponds to descending quality, allowing the
* best offers to be processed first.
*
* @note The increment/decrement operators navigate the discrete floating-point
* grid by modifying `value_` by one ULP. The representation may become
* non-canonical after such operations.
*
* @see composedQuality() for two-hop path composition.
* @see QualityFunction.h for the continuous AMM extension of this type.
*/
class Quality
{
public:
// Type of the internal representation. Higher qualities
// have lower unsigned integer representations.
/** Underlying storage type. Higher qualities have lower integer values. */
using value_type = std::uint64_t;
/** Minimum valid tick size (significant decimal digits) for `round()`. */
static int const kMIN_TICK_SIZE = 3;
/** Maximum valid tick size (significant decimal digits) for `round()`. */
static int const kMAX_TICK_SIZE = 16;
private:
// This has the same representation as STAmount, see the comment on the
// STAmount. However, this class does not always use the canonical
// representation. In particular, the increment and decrement operators may
// cause a non-canonical representation.
// Packed 64-bit encoding: bits [63:56] = biased exponent (actual + 100),
// bits [55:0] = mantissa. Identical to the STAmount IOU wire format.
// May be non-canonical after operator++ / operator--.
value_type value_;
public:
Quality() = default;
/** Create a quality from the integer encoding of an STAmount */
/** Construct from a raw packed integer in STAmount encoding.
*
* The top 8 bits are the biased exponent (actual exponent + 100) and
* the bottom 56 bits are the mantissa. Higher integers denote lower
* (worse) quality because the internal ordering is inverted.
*
* @param value Packed 64-bit quality value.
*/
explicit Quality(std::uint64_t value);
/** Create a quality from the ratio of two amounts. */
/** Construct from an `STAmount` in/out pair encoding `out / in`.
*
* Calls `getRate(amount.out, amount.in)` to produce the packed value.
* Neither side should be zero.
*
* @param amount Offer amounts: `in` = TakerPays, `out` = TakerGets.
*/
explicit Quality(Amounts const& amount);
/** Create a quality from the ratio of two amounts. */
/** Construct from a typed in/out pair by converting to `STAmount` first.
*
* @tparam In Input amount type (e.g., `XRPAmount`, `IOUAmount`).
* @tparam Out Output amount type.
* @param amount The typed offer amounts.
*/
template <class In, class Out>
explicit Quality(TAmounts<In, Out> const& amount)
: Quality(Amounts(toSTAmount(amount.in), toSTAmount(amount.out)))
{
}
/** Create a quality from the ratio of two amounts. */
/** Construct from explicit out and in amounts by converting to `STAmount`.
*
* @tparam In Input amount type.
* @tparam Out Output amount type.
* @param out The output (TakerGets) amount.
* @param in The input (TakerPays) amount.
*/
template <class In, class Out>
Quality(Out const& out, In const& in) : Quality(Amounts(toSTAmount(in), toSTAmount(out)))
{
}
/** Advances to the next higher quality level. */
/** Advance to the next higher quality level.
*
* Because the internal encoding is inverted, this decrements the stored
* integer by one ULP. Used during offer-book traversal to step the
* crossing price up by the smallest representable increment.
*
* @pre `value_ > 0`; underflow is asserted.
*/
/** @{ */
Quality&
operator++();
@@ -135,7 +222,13 @@ public:
operator++(int);
/** @} */
/** Advances to the next lower quality level. */
/** Retreat to the next lower quality level.
*
* Because the internal encoding is inverted, this increments the stored
* integer by one ULP.
*
* @pre `value_ < UINT64_MAX`; overflow is asserted.
*/
/** @{ */
Quality&
operator--();
@@ -144,65 +237,184 @@ public:
operator--(int);
/** @} */
/** Returns the quality as STAmount. */
/** Decode the packed quality value into an `STAmount` exchange rate.
*
* The returned amount represents the rate `out / in` in the IOU
* floating-point format. Callers use this when passing the quality
* to `mulRound` / `divRound` for proportional scaling.
*
* @return The exchange rate as an `STAmount`.
*/
[[nodiscard]] STAmount
rate() const
{
return amountFromQuality(value_);
}
/** Returns the quality rounded up to the specified number
of decimal digits.
*/
/** Round the quality's mantissa up to `tickSize` significant decimal digits.
*
* Used for tick-size enforcement: coarsens the price grid so that offers
* differing only in low-order digits are treated as equivalent. Rounding
* is always upward (ceiling), which makes the encoded rate slightly higher
* (worse for the taker) and prevents a rounded quality from being mistakenly
* ranked better than the original.
*
* @param tickSize Number of significant digits to retain. Must be in
* `[kMIN_TICK_SIZE, kMAX_TICK_SIZE]`; enforcement is the caller's
* responsibility.
* @return A new `Quality` with a rounded-up mantissa and unchanged exponent.
*/
[[nodiscard]] Quality
round(int tickSize) const;
/** Returns the scaled amount with in capped.
Math is avoided if the result is exact. The output is clamped
to prevent money creation.
*/
/** Scale an offer's amounts down so that the input does not exceed `limit`.
*
* If `amount.in > limit`, sets `in = limit` and recomputes `out`
* proportionally via `divRound`. The computed output is clamped to
* `amount.out` if arithmetic would produce a larger value, preventing
* money creation due to rounding. Returns `amount` unchanged when
* `amount.in <= limit`.
*
* @param amount Current offer amounts (`in` = TakerPays, `out` = TakerGets).
* @param limit Maximum allowed input amount.
* @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`.
* @note Uses `divRound` (legacy rounding that ignores low-order bits).
* Use `ceilInStrict` when full-precision rounding is required.
*/
[[nodiscard]] Amounts
ceilIn(Amounts const& amount, STAmount const& limit) const;
/** Scale a typed offer's amounts down so that the input does not exceed `limit`.
*
* Converts both sides to `STAmount`, delegates to the `STAmount` overload,
* then converts the result back to the typed amounts.
*
* @tparam In Input amount type.
* @tparam Out Output amount type.
* @param amount Current offer amounts.
* @param limit Maximum allowed input amount.
* @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`.
*/
template <class In, class Out>
[[nodiscard]] TAmounts<In, Out>
ceilIn(TAmounts<In, Out> const& amount, In const& limit) const;
// Some of the underlying rounding functions called by ceil_in() ignored
// low order bits that could influence rounding decisions. This "strict"
// method uses underlying functions that pay attention to all the bits.
/** Scale an offer's amounts down so that the input does not exceed `limit`,
* using full-precision rounding.
*
* Identical to `ceilIn` except it delegates to `divRoundStrict`, which
* considers all low-order bits that `divRound` ignores. Introduced to
* fix subtle rounding bugs where a borderline result could influence
* whether an offer crosses.
*
* @param amount Current offer amounts.
* @param limit Maximum allowed input amount.
* @param roundUp Whether to round the recomputed output up (`true`) or
* down (`false`).
* @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`.
*/
[[nodiscard]] Amounts
ceilInStrict(Amounts const& amount, STAmount const& limit, bool roundUp) const;
/** Scale a typed offer's amounts down so that the input does not exceed `limit`,
* using full-precision rounding.
*
* @tparam In Input amount type.
* @tparam Out Output amount type.
* @param amount Current offer amounts.
* @param limit Maximum allowed input amount.
* @param roundUp Whether to round the recomputed output up or down.
* @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`.
*/
template <class In, class Out>
[[nodiscard]] TAmounts<In, Out>
ceilInStrict(TAmounts<In, Out> const& amount, In const& limit, bool roundUp) const;
/** Returns the scaled amount with out capped.
Math is avoided if the result is exact. The input is clamped
to prevent money creation.
*/
/** Scale an offer's amounts down so that the output does not exceed `limit`.
*
* If `amount.out > limit`, sets `out = limit` and recomputes `in`
* proportionally via `mulRound`. The computed input is clamped to
* `amount.in` if arithmetic would produce a larger value, preventing
* money creation due to rounding. Returns `amount` unchanged when
* `amount.out <= limit`.
*
* @param amount Current offer amounts.
* @param limit Maximum allowed output amount.
* @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`.
* @note Uses `mulRound` (legacy rounding that ignores low-order bits).
* Use `ceilOutStrict` when full-precision rounding is required.
*/
[[nodiscard]] Amounts
ceilOut(Amounts const& amount, STAmount const& limit) const;
/** Scale a typed offer's amounts down so that the output does not exceed `limit`.
*
* Converts both sides to `STAmount`, delegates to the `STAmount` overload,
* then converts the result back to the typed amounts.
*
* @tparam In Input amount type.
* @tparam Out Output amount type.
* @param amount Current offer amounts.
* @param limit Maximum allowed output amount.
* @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`.
*/
template <class In, class Out>
[[nodiscard]] TAmounts<In, Out>
ceilOut(TAmounts<In, Out> const& amount, Out const& limit) const;
// Some of the underlying rounding functions called by ceil_out() ignored
// low order bits that could influence rounding decisions. This "strict"
// method uses underlying functions that pay attention to all the bits.
/** Scale an offer's amounts down so that the output does not exceed `limit`,
* using full-precision rounding.
*
* Identical to `ceilOut` except it delegates to `mulRoundStrict`, which
* considers all low-order bits that `mulRound` ignores.
*
* @param amount Current offer amounts.
* @param limit Maximum allowed output amount.
* @param roundUp Whether to round the recomputed input up (`true`) or
* down (`false`).
* @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`.
*/
[[nodiscard]] Amounts
ceilOutStrict(Amounts const& amount, STAmount const& limit, bool roundUp) const;
/** Scale a typed offer's amounts down so that the output does not exceed `limit`,
* using full-precision rounding.
*
* @tparam In Input amount type.
* @tparam Out Output amount type.
* @param amount Current offer amounts.
* @param limit Maximum allowed output amount.
* @param roundUp Whether to round the recomputed input up or down.
* @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`.
*/
template <class In, class Out>
[[nodiscard]] TAmounts<In, Out>
ceilOutStrict(TAmounts<In, Out> const& amount, Out const& limit, bool roundUp) const;
private:
// The ceil_in and ceil_out methods that deal in TAmount all convert
// their arguments to STAmount and convert the result back to TAmount.
// This helper function takes care of all the conversion operations.
/** Shared implementation for all typed `ceilIn`/`ceilOut` overloads.
*
* Converts `amount` and `limit` to `STAmount`, calls `ceilFunction` (a
* member-function pointer to one of the `STAmount`-based overloads), and
* converts the result back to `TAmounts<In, Out>`. Returns `amount`
* unchanged when `limitCmp <= limit` (i.e., the limit is not binding).
*
* The variadic `Round...` pack forwards an optional `bool roundUp` argument
* to strict variants without requiring separate instantiations.
*
* @tparam In Input amount type.
* @tparam Out Output amount type.
* @tparam Lim Limit amount type (same as `In` or `Out`).
* @tparam FnPtr Pointer to the `STAmount`-based ceil member function.
* @tparam Round Empty or `{bool}` — forwarded as `roundUp`.
* @param amount Current typed offer amounts.
* @param limit The cap to apply.
* @param limitCmp The side of `amount` to compare against `limit`
* (either `amount.in` or `amount.out`).
* @param ceilFunction Member-function pointer to dispatch to.
* @param round Optional rounding direction (strict variants only).
* @return Scaled `TAmounts<In, Out>`.
*/
template <class In, class Out, class Lim, typename FnPtr, std::same_as<bool>... Round>
[[nodiscard]] TAmounts<In, Out>
ceilTAmountsHelper(
@@ -213,46 +425,53 @@ private:
Round... round) const;
public:
/** Returns `true` if lhs is lower quality than `rhs`.
Lower quality means the taker receives a worse deal.
Higher quality is better for the taker.
*/
/** Returns `true` if `lhs` is lower quality (worse for the taker) than `rhs`.
*
* Because the internal encoding is inverted, a lower quality corresponds
* to a *higher* stored integer, so this compares `lhs.value_ > rhs.value_`.
*/
friend bool
operator<(Quality const& lhs, Quality const& rhs) noexcept
{
return lhs.value_ > rhs.value_;
}
/** Returns `true` if `lhs` is higher quality (better for the taker) than `rhs`. */
friend bool
operator>(Quality const& lhs, Quality const& rhs) noexcept
{
return lhs.value_ < rhs.value_;
}
/** Returns `true` if `lhs` is lower or equal quality to `rhs`. */
friend bool
operator<=(Quality const& lhs, Quality const& rhs) noexcept
{
return !(lhs > rhs);
}
/** Returns `true` if `lhs` is higher or equal quality to `rhs`. */
friend bool
operator>=(Quality const& lhs, Quality const& rhs) noexcept
{
return !(lhs < rhs);
}
/** Returns `true` if both qualities encode the same exchange rate. */
friend bool
operator==(Quality const& lhs, Quality const& rhs) noexcept
{
return lhs.value_ == rhs.value_;
}
/** Returns `true` if the two qualities encode different exchange rates. */
friend bool
operator!=(Quality const& lhs, Quality const& rhs) noexcept
{
return !(lhs == rhs);
}
/** Write the raw packed integer value of the quality to an output stream. */
friend std::ostream&
operator<<(std::ostream& os, Quality const& quality)
{
@@ -260,8 +479,18 @@ public:
return os;
}
// return the relative distance (relative error) between two qualities. This
// is used for testing only. relative distance is abs(a-b)/min(a,b)
/** Return the relative error between two quality values: `|a - b| / min(a, b)`.
*
* Extracts the exponent and mantissa from each packed value, scales them
* to a common exponent, and returns the normalized distance. Used only
* in unit tests to verify that two qualities are sufficiently close.
*
* @param q1 First quality; must be non-zero (asserted).
* @param q2 Second quality; must be non-zero (asserted).
* @return `|q1 - q2| / min(q1, q2)` as a `double`.
* @note For testing only. Production code should compare with the
* relational operators.
*/
friend double
relativeDistance(Quality const& q1, Quality const& q2)
{
@@ -284,9 +513,8 @@ public:
double const maxVD =
(expDiff != 0) ? maxVMantissa * pow(10, expDiff) : static_cast<double>(maxVMantissa);
// maxVD and minVD are scaled so they have the same exponents. Dividing
// cancels out the exponents, so we only need to deal with the (scaled)
// mantissas
// maxVD and minVD are scaled so they have the same exponent; dividing
// cancels out the exponents, leaving only the normalized mantissa difference.
return (maxVD - minVD) / minVD;
}
};
@@ -315,7 +543,6 @@ template <class In, class Out>
TAmounts<In, Out>
Quality::ceilIn(TAmounts<In, Out> const& amount, In const& limit) const
{
// Construct a function pointer to the function we want to call.
static constexpr Amounts (Quality::*kCEIL_IN_FN_PTR)(Amounts const&, STAmount const&) const =
&Quality::ceilIn;
@@ -326,7 +553,6 @@ template <class In, class Out>
TAmounts<In, Out>
Quality::ceilInStrict(TAmounts<In, Out> const& amount, In const& limit, bool roundUp) const
{
// Construct a function pointer to the function we want to call.
static constexpr Amounts (Quality::*kCEIL_IN_FN_PTR)(Amounts const&, STAmount const&, bool)
const = &Quality::ceilInStrict;
@@ -337,7 +563,6 @@ template <class In, class Out>
TAmounts<In, Out>
Quality::ceilOut(TAmounts<In, Out> const& amount, Out const& limit) const
{
// Construct a function pointer to the function we want to call.
static constexpr Amounts (Quality::*kCEIL_OUT_FN_PTR)(Amounts const&, STAmount const&) const =
&Quality::ceilOut;
@@ -348,17 +573,26 @@ template <class In, class Out>
TAmounts<In, Out>
Quality::ceilOutStrict(TAmounts<In, Out> const& amount, Out const& limit, bool roundUp) const
{
// Construct a function pointer to the function we want to call.
static constexpr Amounts (Quality::*kCEIL_OUT_FN_PTR)(Amounts const&, STAmount const&, bool)
const = &Quality::ceilOutStrict;
return ceilTAmountsHelper(amount, limit, amount.out, kCEIL_OUT_FN_PTR, roundUp);
}
/** Calculate the quality of a two-hop path given the two hops.
@param lhs The first leg of the path: input to intermediate.
@param rhs The second leg of the path: intermediate to output.
*/
/** Compute the effective end-to-end exchange rate for a two-hop path.
*
* If the first hop converts A→B at rate `lhs` and the second converts B→C
* at rate `rhs`, the composed quality is their product, re-encoded into the
* packed 64-bit format. Used by the pathfinding engine to rank multi-hop
* routes against single-hop offers on a common scale.
*
* @param lhs Quality of the first leg (input → intermediate currency).
* @param rhs Quality of the second leg (intermediate → output currency).
* @return Composed quality representing the overall A→C exchange rate.
* @note Both input rates must be non-zero (asserted at runtime). The
* composed exponent must fit in 8 bits (i.e., actual exponent in
* [-99, 155]); astronomically large or small paths will assert.
*/
Quality
composedQuality(Quality const& lhs, Quality const& rhs);

View File

@@ -6,52 +6,152 @@
namespace xrpl {
/** Average quality of a path as a function of `out`: q(out) = m * out + b,
* where m = -1 / poolGets, b = poolPays / poolGets. If CLOB offer then
* `m` is equal to 0 `b` is equal to the offer's quality. The function
* is derived by substituting `in` in q = out / in with the swap out formula
* for `in`:
* in = [(poolGets * poolPays) / (poolGets - out) - poolPays] / (1 - tfee)
* and combining the function for multiple steps. The function is used
* to limit required output amount when quality limit is provided in one
* path optimization.
/** Average quality of a payment strand expressed as a linear function of output.
*
* Models the relationship `q(out) = m_ * out + b_`, where `q` is the average
* exchange rate (quality) that a strand delivers when it produces `out` units.
* This analytical model lets `StrandFlow::limitOut()` compute — without
* simulation — the maximum output the strand may produce before AMM price
* impact degrades the average quality below a caller-supplied limit.
*
* **Derivation.** For an AMM step with pool balances `poolGets` (input side)
* and `poolPays` (output side) and fee multiplier `cfee = 1 - tfee`, the
* constant-product swap formula gives:
* @code
* in = [(poolGets * poolPays) / (poolGets - out) - poolPays] / cfee
* @endcode
* Substituting into `q = out / in` and linearising yields:
* @code
* m = -cfee / poolGets (always negative for a valid AMM step)
* b = poolPays * cfee / poolGets
* @endcode
*
* **Multi-hop composition.** For strands with sequential steps (e.g. a
* transfer-fee hop preceding an AMM hop), `combine()` chains two quality
* functions analytically. `StrandFlow::limitOut()` calls `combine()` in a
* loop over all steps to accumulate a single QF representing the whole strand.
*
* **Two construction modes** are selected via tag dispatch:
* - `AMMTag` — variable quality; slope and intercept derived from pool balances.
* - `CLOBLikeTag` — constant quality (`m_ = 0`); used for plain CLOB orders and
* for AMM offers in multi-path mode, where each path's AMM allocation is fixed.
*
* @note The linear approximation is exact for *average* quality but not for
* instantaneous (marginal) quality, which is quadratic. Using averages
* keeps composition algebraically simple while still providing a
* conservative, analytically tractable bound.
*
* @see StrandFlow.h `limitOut()` — primary consumer of this class.
*/
class QualityFunction
{
private:
// slope
/** Slope of the qualityoutput line (`-cfee / poolGets` for AMM; 0 for CLOB). */
Number m_;
// intercept
/** Intercept of the qualityoutput line (`poolPays * cfee / poolGets` for AMM;
* `1 / quality.rate()` for CLOB). */
Number b_;
// seated if QF is for CLOB offer.
/** Cached constant quality; seated only when `m_ == 0` (CLOB-like function). */
std::optional<Quality> quality_;
public:
/** Tag type that selects the AMM constructor (variable-quality path step). */
struct AMMTag
{
};
// AMMOffer for multi-path is like CLOB, i.e. the offer size
// changes proportionally to its quality.
/** Tag type that selects the CLOB-like constructor (constant-quality path step).
*
* Used for both plain CLOB orders and AMM offers operating in multi-path
* mode, where the AMM offer size scales proportionally with quality just
* like a CLOB, making the effective quality constant from this sub-path's
* perspective.
*/
struct CLOBLikeTag
{
};
/** Construct a constant-quality (CLOB-like) quality function.
*
* Sets `m_ = 0` and `b_ = 1 / quality.rate()`. `quality_` is seated so
* that `isConst()` returns `true` and `StrandFlow::limitOut()` skips the
* output cap.
*
* @param quality The fixed exchange rate of this path step.
* @throws std::runtime_error if `quality.rate()` is zero, which would
* make the intercept infinite.
*/
QualityFunction(Quality const& quality, CLOBLikeTag);
/** Construct a variable-quality (AMM) quality function from pool balances.
*
* Derives the slope and intercept from the constant-product swap formula:
* @code
* m_ = -cfee / amounts.in
* b_ = amounts.out * cfee / amounts.in
* @endcode
* where `cfee = feeMult(tfee)`. `quality_` is left empty; `isConst()`
* returns `false`.
*
* @tparam TIn Input amount type (e.g. `XRPAmount`, `IOUAmount`).
* @tparam TOut Output amount type.
* @param amounts Current AMM pool balances: `amounts.in` is the input-side
* pool depth, `amounts.out` is the output-side pool depth.
* @param tfee AMM trading fee in the same units as `feeMult()` expects.
* @throws std::runtime_error if either pool balance is zero or negative,
* which would cause division-by-zero in downstream arithmetic.
*/
template <typename TIn, typename TOut>
QualityFunction(TAmounts<TIn, TOut> const& amounts, std::uint32_t tfee, AMMTag);
/** Combines QF with the next step QF
/** Chain this quality function with the next path step's quality function.
*
* Applies linear function composition in reciprocal-rate space:
* @code
* m_ += b_ * qf.m_;
* b_ *= qf.b_;
* @endcode
* If the combined slope becomes nonzero, `quality_` is cleared to reflect
* that the resulting function is no longer constant and `outFromAvgQ()`
* must be used rather than a simple pass/fail quality check.
*
* @param qf Quality function for the next step to compose in.
*/
void
combine(QualityFunction const& qf);
/** Find output to produce the requested
* average quality.
* @param quality requested average quality (quality limit)
/** Solve for the maximum output at which average quality meets the given limit.
*
* Inverts `q(out) = m_ * out + b_` by substituting `q = 1 / quality.rate()`:
* @code
* out = (1 / quality.rate() - b_) / m_
* @endcode
* The rounding mode is set to `Upward` during the calculation so the
* returned bound is conservative: because `m_` is negative, dividing an
* upward-rounded numerator by a negative slope yields a result that rounds
* down, ensuring the engine never requests marginally more output than the
* quality constraint allows.
*
* Returns `std::nullopt` in three cases:
* - `m_ == 0`: the function is constant (CLOB-like); quality either passes
* or fails uniformly, so no output cap is meaningful.
* - `quality.rate() == 0`: guards against division-by-zero when forming
* `1 / rate`.
* - `out <= 0`: the quality limit cannot be achieved at any positive output;
* the strand is effectively dead for this constraint.
*
* @param quality The minimum acceptable average exchange rate (quality limit).
* @return The output amount at which the strand's average quality equals
* `quality`, or `std::nullopt` if the cap is inapplicable or infeasible.
*/
std::optional<Number>
outFromAvgQ(Quality const& quality);
/** Return true if the quality function is constant
/** Return `true` if this quality function is constant (CLOB-like).
*
* A constant function has `m_ == 0`: the average quality is the same
* regardless of output size. `StrandFlow::limitOut()` treats a constant
* function as a signal to skip the output cap and return `remainingOut`
* unchanged.
*/
[[nodiscard]] bool
isConst() const
@@ -59,6 +159,13 @@ public:
return quality_.has_value();
}
/** Return the cached constant quality, if any.
*
* Seated only when `isConst() == true` (i.e., this is a CLOB-like
* function constructed via `CLOBLikeTag`). Returns `std::nullopt` for
* variable-quality (AMM) functions and for any combined function whose
* slope became nonzero after `combine()`.
*/
[[nodiscard]] std::optional<Quality> const&
quality() const
{

View File

@@ -1,13 +1,49 @@
#pragma once
/** @file
* Deprecated compatibility shim for the XRPL RPC error API.
*
* Declares `rpcError()` and `isRpcError()` — legacy entry points in the
* `xrpl` namespace that predate the richer `RPC`-namespaced error
* infrastructure in `ErrorCodes.h`. New code should use `RPC::makeError()`
* and `RPC::containsError()` from `ErrorCodes.h` directly.
*/
#include <xrpl/json/json_value.h>
#include <xrpl/protocol/ErrorCodes.h>
namespace xrpl {
// VFALCO NOTE these are deprecated
/** Return `true` if @p jvResult represents an RPC error response.
*
* Duck-types the JSON value by checking for the presence of the `"error"`
* key — the same structural sentinel that `RPC::containsError()` tests,
* making it the direct modern equivalent.
*
* @param jvResult The JSON value to inspect (taken by value, not
* `const` reference — an inefficiency inherited from the original
* implementation that was never corrected given this function's
* deprecated status).
* @return `true` if @p jvResult is a JSON object containing an `"error"`
* member.
* @deprecated Use `RPC::containsError(json)` from `ErrorCodes.h` instead.
*/
bool
isRpcError(json::Value jvResult);
/** Construct a fresh JSON error object for the given error code.
*
* Delegates to `RPC::injectError()`, which populates a new `Json::Value`
* object with the canonical `error` token, `error_code`, and
* `error_message` fields drawn from the static `ErrorInfo` registry.
* The return-by-value produces a self-contained error object ready for
* direct return from an RPC handler.
*
* @param iError The RPC error code to encode.
* @return A new `Json::Value` object with `error`, `error_code`, and
* `error_message` populated.
* @deprecated Use `RPC::makeError(code)` from `ErrorCodes.h` instead.
*/
json::Value
rpcError(ErrorCodeI iError);

View File

@@ -1,3 +1,13 @@
/** @file
* Defines the `Rate` struct and its arithmetic free functions for applying
* XRPL transfer fees to `STAmount` values.
*
* Transfer rates are billion-scale fractions: `1,000,000,000` is parity
* (no fee). `kPARITY_RATE` is the sentinel for the fee-free common case;
* all six arithmetic functions short-circuit on it without entering the
* `STAmount` multiply/divide path.
*/
#pragma once
#include <xrpl/beast/utility/instrumentation.h>
@@ -10,14 +20,27 @@
namespace xrpl {
/** Represents a transfer rate
Transfer rates are specified as fractions of 1 billion.
For example, a transfer rate of 1% is represented as
1,010,000,000.
*/
/** Protocol-level transfer rate, expressed as a fraction of one billion.
*
* A value of `1,000,000,000` means 1:1 — no fee. A value of
* `1,010,000,000` means the sender must deliver 1.01 units for every 1 unit
* the recipient receives (a 1% fee). This scale matches `QUALITY_ONE` in
* `Quality.h`, tying transfer fees directly to the ledger's price
* representation.
*
* `boost::totally_ordered<Rate>` generates `!=`, `>`, `<=`, and `>=` from
* the manually provided `==` and `<`, keeping the struct concise while
* remaining fully ordered.
*
* @note The default constructor is deleted: a `Rate` with an unspecified
* value is meaningless, and zero would violate the nonzero precondition
* asserted by every arithmetic function in this header. The constructor
* is `explicit` to prevent accidental implicit conversion from the
* large integers that rate values resemble.
*/
struct Rate : private boost::totally_ordered<Rate>
{
/** The raw billion-scale rate value as stored in `sfTransferRate`. */
std::uint32_t value;
Rate() = delete;
@@ -27,18 +50,21 @@ struct Rate : private boost::totally_ordered<Rate>
}
};
/** Returns `true` if both rates have the same billion-scale value. */
inline bool
operator==(Rate const& lhs, Rate const& rhs) noexcept
{
return lhs.value == rhs.value;
}
/** Returns `true` if `lhs` is a strictly smaller rate than `rhs`. */
inline bool
operator<(Rate const& lhs, Rate const& rhs) noexcept
{
return lhs.value < rhs.value;
}
/** Writes the raw billion-scale rate value to `os`. */
inline std::ostream&
operator<<(std::ostream& os, Rate const& rate)
{
@@ -46,32 +72,126 @@ operator<<(std::ostream& os, Rate const& rate)
return os;
}
/** Scale an amount by a transfer rate, preserving its asset.
*
* Computes `amount × (rate / 10^9)`. Returns `amount` unchanged when
* `rate == kPARITY_RATE`, avoiding the `STAmount` arithmetic path for the
* common fee-free case.
*
* @param amount The value to scale.
* @param rate The transfer rate to apply; must be nonzero.
* @return The scaled `STAmount` denominated in the same asset as `amount`.
* @pre `rate.value != 0`; asserted in debug builds.
*/
STAmount
multiply(STAmount const& amount, Rate const& rate);
/** Scale an amount by a transfer rate with controlled rounding, preserving its asset.
*
* Like `multiply()`, but the caller controls rounding direction. Used in
* IOU payment routing where fee calculations stay in a single currency.
*
* @param amount The value to scale.
* @param rate The transfer rate to apply; must be nonzero.
* @param roundUp If `true`, round fractional results toward positive
* infinity; otherwise round toward zero.
* @return The scaled `STAmount` denominated in the same asset as `amount`.
* @pre `rate.value != 0`; asserted in debug builds.
*/
STAmount
multiplyRound(STAmount const& amount, Rate const& rate, bool roundUp);
/** Scale an amount by a transfer rate with controlled rounding, emitting a specified asset.
*
* Overload for offer-crossing and cross-currency paths where the output
* must be denominated in a different asset than the input.
*
* @param amount The value to scale.
* @param rate The transfer rate to apply; must be nonzero.
* @param asset The asset type of the returned `STAmount`.
* @param roundUp If `true`, round fractional results toward positive
* infinity; otherwise round toward zero.
* @return The scaled `STAmount` denominated in `asset`.
* @pre `rate.value != 0`; asserted in debug builds.
*/
STAmount
multiplyRound(STAmount const& amount, Rate const& rate, Asset const& asset, bool roundUp);
/** Scale an amount by the inverse of a transfer rate, preserving its asset.
*
* Computes `amount / (rate / 10^9)` — the inverse of `multiply()`. Used
* when back-calculating the gross send amount needed to deliver a given net
* amount after fees. Returns `amount` unchanged for `kPARITY_RATE`.
*
* @param amount The value to scale.
* @param rate The transfer rate to invert; must be nonzero.
* @return The scaled `STAmount` denominated in the same asset as `amount`.
* @pre `rate.value != 0`; asserted in debug builds.
*/
STAmount
divide(STAmount const& amount, Rate const& rate);
/** Scale an amount by the inverse of a transfer rate with controlled rounding, preserving its asset.
*
* Like `divide()`, but the caller controls rounding direction. Used in
* IOU payment routing for single-currency gross-amount back-calculation.
*
* @param amount The value to scale.
* @param rate The transfer rate to invert; must be nonzero.
* @param roundUp If `true`, round fractional results toward positive
* infinity; otherwise round toward zero.
* @return The scaled `STAmount` denominated in the same asset as `amount`.
* @pre `rate.value != 0`; asserted in debug builds.
*/
STAmount
divideRound(STAmount const& amount, Rate const& rate, bool roundUp);
/** Scale an amount by the inverse of a transfer rate with controlled rounding, emitting a specified asset.
*
* Overload for offer-crossing and cross-currency paths where the output
* must be denominated in a different asset than the input.
*
* @param amount The value to scale.
* @param rate The transfer rate to invert; must be nonzero.
* @param asset The asset type of the returned `STAmount`.
* @param roundUp If `true`, round fractional results toward positive
* infinity; otherwise round toward zero.
* @return The scaled `STAmount` denominated in `asset`.
* @pre `rate.value != 0`; asserted in debug builds.
*/
STAmount
divideRound(STAmount const& amount, Rate const& rate, Asset const& asset, bool roundUp);
namespace nft {
/** Given a transfer fee (in basis points) convert it to a transfer rate. */
/** Convert an NFT transfer fee in basis points to a billion-scale `Rate`.
*
* NFT royalties are stored as a `uint16_t` in basis points (050,000
* representing 0%50%). Because `Rate` uses `10^9` as its unit, the
* conversion multiplies by `10,000`: a maximum fee of `50,000 bp` becomes
* `500,000,000`, safely below `QUALITY_ONE` and within `uint32_t` range.
*
* @param fee NFT transfer fee in basis points (050,000); validated by
* transaction processing before reaching this function.
* @return A `Rate` suitable for passing to `multiply()` or `multiplyRound()`.
* @note Do not call this for ordinary IOU transfer rates — those are already
* billion-scale and should be wrapped in `Rate` directly.
*/
Rate
transferFeeAsRate(std::uint16_t fee);
} // namespace nft
/** A transfer rate signifying a 1:1 exchange */
/** The 1:1 transfer rate — sender pays exactly what the recipient receives.
*
* Equal to `QUALITY_ONE` (`1,000,000,000`). Every arithmetic function in
* this header returns `amount` unchanged when it detects this value, so
* payment paths through accounts with no transfer fee never enter the
* `STAmount` multiply/divide path. `transferRate()` returns this sentinel
* when an account's `sfTransferRate` field is absent.
*
* @see transferRate() in AccountRootHelpers.h
*/
extern Rate const kPARITY_RATE;
} // namespace xrpl

View File

@@ -1,9 +1,31 @@
/** @file
* Defines the `LedgerHash` type alias used throughout the ledger stack to
* identify closed ledgers by their cryptographic digest.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
namespace xrpl {
/** The SHA-512/256 digest that uniquely identifies a closed XRP Ledger.
*
* A ledger hash is computed over the serialized `LedgerHeader` — covering the
* account-state hash, transaction-set hash, sequence number, close time, drop
* totals, and parent ledger hash — and is 32 bytes wide.
*
* The alias over bare `uint256` serves two purposes: it makes interfaces
* self-documenting at call sites (e.g., `CanonicalTXSet(LedgerHash const&)`),
* and it isolates all ledger-hash usage behind a single name so that a
* tagged variant (`base_uint<256, struct LedgerHashTag>`) can be introduced
* later to prevent cross-domain substitution with transaction hashes or
* account IDs without touching every call site.
*
* @note `LedgerHeader` stores its hash fields as bare `uint256` for historical
* reasons; higher-level APIs (`CanonicalTXSet`, `LedgerHistory`,
* `InboundLedgers`, `RCLValidations`) consistently use this alias.
*/
using LedgerHash = uint256;
} // namespace xrpl

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