This commit is contained in:
Denis Angell
2026-05-13 23:01:44 +02:00
parent f3535b1158
commit d8febb71bd
293 changed files with 38699 additions and 6633 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

@@ -7,6 +7,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,30 +37,82 @@ 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;
@@ -51,7 +121,10 @@ public:
[[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;
@@ -63,41 +136,137 @@ public:
[[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

@@ -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

@@ -9,6 +9,27 @@
namespace xrpl {
/** Abstract collaborator bundle for SHAMap I/O, caching, and recovery.
*
* Every `SHAMap` holds a `Family&` reference and routes all storage and
* caching decisions through it. The interface bundles four external
* collaborators — a persistent `NodeStore::Database`, two in-memory caches,
* and a logging journal — into a single coherent dependency with a shared
* lifetime, ensuring they are always consistent with each other.
*
* The abstraction decouples the pure Merkle-radix-tree logic from
* application infrastructure, allowing different implementations for
* production (`NodeFamily`) and unit tests (lighter-weight in-memory
* variants).
*
* @note `Family` instances are non-copyable and non-movable by design.
* `SHAMap` stores a `Family&` reference (not a pointer), and multiple
* maps may share the same `Family`. Allowing move would dangle those
* references. Stable addresses are guaranteed for the object's full
* lifetime.
*
* @see NodeFamily
*/
class Family
{
public:
@@ -24,42 +45,93 @@ public:
explicit Family() = default;
virtual ~Family() = default;
/** Return the persistent node-store database backing this family. */
virtual NodeStore::Database&
db() = 0;
/** Return the persistent node-store database backing this family. */
[[nodiscard]] virtual NodeStore::Database const&
db() const = 0;
/** Return the journal used for diagnostic logging within this family. */
virtual beast::Journal const&
journal() = 0;
/** Return a pointer to the Family Full Below Cache */
/** Return the FullBelowCache for this family.
*
* An entry in this cache means every descendant of the keyed tree node
* is already stored locally. During traversal or sync, a cache hit on a
* node's hash allows the entire subtree beneath it to be skipped.
*
* The cache is generation-stamped: `clear()` increments the generation,
* invalidating all in-memory per-node markers without a per-entry purge.
* `reset()` additionally resets the generation back to 1 for full
* rebuild scenarios.
*/
virtual std::shared_ptr<FullBelowCache>
getFullBelowCache() = 0;
/** Return a pointer to the Family Tree Node Cache */
/** Return the TreeNodeCache for this family.
*
* Holds deserialized `SHAMapTreeNode` objects keyed by hash. Nodes
* read from the `NodeStore::Database` are placed here after
* deserialization; subsequent lookups by the same hash retrieve the
* already-decoded object, avoiding redundant disk reads.
*
* Uses `SharedWeakUnionPtr` internally so that nodes can be evicted
* from the cache while live `SHAMap` trees that hold strong pointers
* to them continue to operate normally.
*/
virtual std::shared_ptr<TreeNodeCache>
getTreeNodeCache() = 0;
/** Expire stale entries from both caches.
*
* Called periodically by the application's maintenance loop to
* prevent unbounded memory growth. This is a routine background
* operation and does not invalidate the cache generation.
*/
virtual void
sweep() = 0;
/** Acquire ledger that has a missing node by ledger sequence
/** Trigger peer acquisition of a ledger identified by sequence number.
*
* @param refNum Sequence of ledger to acquire.
* @param nodeHash Hash of missing node to report in throw.
* Called when a tree traversal reaches a node hash that is absent from
* both the cache and the local database, signalling an incomplete
* ledger. Implementations should forward to the inbound-ledger
* acquisition pipeline.
*
* @note Implementations typically maintain a high-water `maxSeq_`
* under a mutex to suppress redundant acquisition requests when
* many concurrent SHAMap operations discover the same missing node.
*
* @param refNum Sequence number of the ledger that owns the missing node.
* @param nodeHash Hash of the missing node, used for diagnostic logging.
*/
virtual void
missingNodeAcquireBySeq(std::uint32_t refNum, uint256 const& nodeHash) = 0;
/** Acquire ledger that has a missing node by ledger hash
/** Trigger peer acquisition of a ledger identified by its hash.
*
* @param refHash Hash of ledger to acquire.
* @param refNum Ledger sequence with missing node.
* Alternative to `missingNodeAcquireBySeq` for callers — typically
* sync flows — that have the ledger hash but not the sequence number
* as the primary identifier.
*
* @param refHash Hash of the ledger that owns the missing node.
* @param refNum Sequence number of the same ledger, for logging and
* deduplication.
*/
virtual void
missingNodeAcquireByHash(uint256 const& refHash, std::uint32_t refNum) = 0;
/** Tear down all cache state and reset the FullBelowCache generation.
*
* Used when the family's data is being rebuilt from scratch (e.g.,
* after a database wipe or during ledger-replaying scenarios). More
* destructive than `sweep()`: all cached data is discarded and the
* FullBelowCache generation is reset to 1, invalidating every
* in-memory per-node marker that references a prior generation.
*/
virtual void
reset() = 0;
};

View File

@@ -1,3 +1,13 @@
/** @file
* Subtree-completeness cache for SHAMap synchronization.
*
* During ledger acquisition, traversing a SHAMap to discover absent nodes
* is expensive: every inner node may require 16 child lookups, each of which
* can hit the database. `BasicFullBelowCache` short-circuits this walk by
* recording the hashes of inner nodes whose entire subtree is confirmed
* present in local storage.
*/
#pragma once
#include <xrpl/basics/KeyCache.h>
@@ -13,27 +23,61 @@ namespace xrpl {
namespace detail {
/** Remembers which tree keys have all descendants resident.
This optimizes the process of acquiring a complete tree.
*/
/** Remembers which SHAMap inner-node hashes have all descendants resident.
*
* Once a subtree rooted at an inner node is confirmed complete, the node's
* hash is inserted here. Future sync traversals call `touchIfExists` before
* descending; a hit skips the entire subtree without further DB lookups.
*
* A two-layer scheme avoids redundant work at different granularities:
* - **This cache** (inter-pass, cross-SHAMap): entries survive across
* multiple `getMissingNodes` calls and are shared among all SHAMaps in
* the same `Family`.
* - **Per-node `fullBelowGen_`** (intra-pass): `SHAMapInnerNode` stores the
* generation at which it was marked complete; `isFullBelow(gen)` returns
* true without a cache lookup when the generation still matches.
*
* Entries expire automatically after the configured duration (default two
* minutes), handling the case where a previously-complete subtree is later
* invalidated by database eviction.
*
* All public methods are thread-safe; thread-safety is inherited from the
* underlying `KeyCache` (`TaggedCache<uint256, int, true>`). Only `backed_`
* SHAMaps (those integrated with a `NodeStore`) consult this cache; in-memory
* maps bypass it.
*
* @see SHAMapInnerNode::isFullBelow
* @see SHAMapInnerNode::setFullBelowGen
* @see Family::getFullBelowCache
*/
class BasicFullBelowCache
{
private:
using CacheType = KeyCache;
public:
/** Target size passed to the constructor to request an unbounded cache. */
static constexpr auto kDEFAULT_CACHE_TARGET_SIZE = 0;
using key_type = uint256;
using clock_type = typename CacheType::clock_type;
/** Construct the cache.
@param name A label for diagnostics and stats reporting.
@param collector The collector to use for reporting stats.
@param targetSize The cache target size.
@param targetExpirationSeconds The expiration time for items.
*/
*
* The generation counter is initialised to 1. All `SHAMapInnerNode`
* instances start with `fullBelowGen_ = 0`, so `isFullBelow(1)` returns
* false for every node until explicitly marked.
*
* @param name A label used in diagnostics and stats reporting.
* @param clock The clock used for entry expiration.
* @param j Journal for internal cache logging.
* @param collector Collector for stats export; defaults to a no-op
* collector when not provided.
* @param targetSize Maximum number of entries to retain; 0 means
* unbounded (see `kDEFAULT_CACHE_TARGET_SIZE`).
* @param expiration Duration after which an entry is eligible for
* eviction by `sweep()`; defaults to two minutes.
*/
BasicFullBelowCache(
std::string const& name,
clock_type& clock,
@@ -52,58 +96,102 @@ public:
return cache_.clock();
}
/** Return the number of elements in the cache.
Thread safety:
Safe to call from any thread.
*/
/** Return the number of entries currently held in the cache.
*
* @note Safe to call from any thread.
*/
std::size_t
size() const
{
return cache_.size();
}
/** Remove expired cache items.
Thread safety:
Safe to call from any thread.
*/
/** Evict entries whose age exceeds the configured expiration duration.
*
* Called periodically by `NodeFamily::sweep()` on the same housekeeping
* cycle as the tree-node cache sweep.
*
* @note Safe to call from any thread.
*/
void
sweep()
{
cache_.sweep();
}
/** Refresh the last access time of an item, if it exists.
Thread safety:
Safe to call from any thread.
@param key The key to refresh.
@return `true` If the key exists.
*/
/** Test whether a hash is cached and, if so, reset its expiration timer.
*
* This is the hot path in `SHAMap::getMissingNodes` and
* `SHAMap::addKnownNode`: before descending into a child subtree the
* caller checks the cache; a `true` return means the entire subtree is
* locally complete and can be skipped, returning
* `SHAMapAddNode::duplicate()`.
*
* @param key Hash of the inner node whose subtree completeness is queried.
* @return `true` if the hash is present in the cache (subtree complete);
* `false` if absent or expired.
* @note Safe to call from any thread.
*/
bool
touchIfExists(key_type const& key)
{
return cache_.touchIfExists(key);
}
/** Insert a key into the cache.
If the key already exists, the last access time will still
be refreshed.
Thread safety:
Safe to call from any thread.
@param key The key to insert.
*/
/** Record that the subtree rooted at `key` is fully present locally.
*
* Called after a complete depth-first traversal of a subtree confirms no
* missing nodes. Subsequent calls to `touchIfExists` with the same hash
* will return `true` until the entry expires or `clear()`/`reset()` is
* called.
*
* If the key is already present its expiration timer is refreshed.
*
* @param key Hash of the inner node whose subtree has been verified
* complete.
* @note Safe to call from any thread.
*/
void
insert(key_type const& key)
{
cache_.insert(key);
}
/** generation determines whether cached entry is valid */
/** Return the current generation counter.
*
* The generation is threaded through a `getMissingNodes` traversal and
* compared against `SHAMapInnerNode::fullBelowGen_` via
* `isFullBelow(generation)`. A match means the node was marked complete
* during the current or a still-valid prior pass and its subtree can be
* skipped without a cache lookup.
*
* The generation is incremented by `clear()` and reset to 1 by `reset()`,
* both of which globally invalidate all in-memory per-node markers at
* zero cost — no tree walk is needed to clear them.
*
* @return The current generation value.
* @note Safe to call from any thread; `gen_` is `std::atomic`.
*/
std::uint32_t
getGeneration(void) const
{
return gen_;
}
/** Purge all cache entries and invalidate all in-memory per-node markers.
*
* Increments the generation counter so that every `SHAMapInnerNode`
* whose `fullBelowGen_` was set to the old generation will return
* `false` from `isFullBelow` on the next traversal, forcing a fresh
* descent. No tree walk is required; the stale markers are invalidated
* implicitly by the generation mismatch.
*
* Called by `NodeFamily::reset()` when the family is torn down and
* rebuilt (e.g., after missing-node recovery or between ledger replays).
*
* @note Safe to call from any thread.
* @see reset()
*/
void
clear()
{
@@ -111,6 +199,21 @@ public:
++gen_;
}
/** Purge all cache entries and reset the generation counter to 1.
*
* Semantically equivalent to `clear()` but sets `m_gen = 1` instead of
* incrementing it. Used at initial construction (via the member
* initialiser) and on full application restart, where returning to a
* canonical baseline generation is preferable to retaining a growing
* counter.
*
* Any `SHAMapInnerNode` carrying `fullBelowGen_ > 1` will not match the
* reset-to-1 state, correctly marking every in-memory marker stale.
* Those nodes are expected to be recreated fresh after a hard reset.
*
* @note Safe to call from any thread.
* @see clear()
*/
void
reset()
{
@@ -125,6 +228,12 @@ private:
} // namespace detail
/** Thread-safe cache recording which SHAMap subtrees are fully present locally.
*
* Public alias for `detail::BasicFullBelowCache`. Used throughout the codebase
* via `Family::getFullBelowCache()` to short-circuit expensive tree traversals
* during ledger synchronization.
*/
using FullBelowCache = detail::BasicFullBelowCache;
} // namespace xrpl

View File

@@ -1,3 +1,17 @@
/** @file
* Defines `SHAMap`, the authenticated radix-Merkle tree that underlies every
* XRP Ledger snapshot.
*
* Every ledger carries two `SHAMap` instances: a transaction tree mapping
* transaction IDs to serialized transaction data, and a state tree mapping
* account-state object keys to their serialized values. The root hash of
* each tree is what validators sign — two nodes hold the same ledger if and
* only if their root hashes match.
*
* This header also defines `SHAMap::ConstIterator`, the forward iterator over
* leaf nodes in key order, and the `SHAMapState` enum that governs which
* operations are legal on a given map instance.
*/
#pragma once
#include <xrpl/basics/IntrusivePointer.h>
@@ -23,7 +37,13 @@ namespace xrpl {
class SHAMapNodeID;
class SHAMapSyncFilter;
/** Describes the current state of a given SHAMap */
/** Lifecycle state of a SHAMap instance.
*
* Determines which mutation operations are legal. The state may advance
* forward but never backward (except `Synching` → `Modifying` via
* `clearSynching()`). Attempting to mutate a map in `Immutable` or
* `Invalid` state is guarded by asserts.
*/
enum class SHAMapState {
/** The map is in flux and objects can be added and removed.
@@ -50,28 +70,32 @@ enum class SHAMapState {
Invalid = 3,
};
/** A SHAMap is both a radix tree with a fan-out of 16 and a Merkle tree.
A radix tree is a tree with two properties:
1. The key for a node is represented by the node's position in the tree
(the "prefix property").
2. A node with only one child is merged with that child
(the "merge property")
These properties result in a significantly smaller memory footprint for
a radix tree.
A fan-out of 16 means that each node in the tree has at most 16
children. See https://en.wikipedia.org/wiki/Radix_tree
A Merkle tree is a tree where each non-leaf node is labelled with the hash
of the combined labels of its children nodes.
A key property of a Merkle tree is that testing for node inclusion is
O(log(N)) where N is the number of nodes in the tree.
See https://en.wikipedia.org/wiki/Merkle_tree
/** Authenticated 16-way radix Merkle trie over 256-bit keys.
*
* As a radix tree (fan-out 16), the key for a leaf is encoded entirely by
* its position in the tree (prefix property), and any inner node with only
* one descendant is collapsed upward (merge property). Keys are consumed
* 4 bits (one nibble) per level, giving a fixed leaf depth of 64.
*
* As a Merkle tree, every inner node's hash is derived from the combined
* hashes of all 16 child slots (zero-hash for empty slots). This allows
* O(log N) membership proofs and makes the root hash a cryptographic
* commitment to the entire ledger state.
*
* **Copy-on-write (CoW)**: snapshots share physical nodes. Each
* `SHAMapTreeNode` carries a `cowid_`; nodes owned exclusively by this map
* can be mutated in place. Shared nodes are cloned on first write via
* `unshareNode()`. `snapShot(isMutable=false)` on an immutable map is O(1)
* with zero node copies.
*
* **Backing**: when `backed_ == true` (the default) the map integrates with
* the `Family`-provided `NodeStore` for persistent storage and cache lookups.
* Call `setUnbacked()` for transient in-memory trees (e.g., during
* transaction processing) that must not touch the database.
*
* @note Not copyable or movable. Use `snapShot()` to create a CoW
* derivative.
* @see SHAMapState, SHAMapItem, SHAMapInnerNode, SHAMapLeafNode, Family
*/
class SHAMap
{
@@ -92,15 +116,23 @@ private:
mutable bool full_ = false; // Map is believed complete in database
public:
/** Number of children each non-leaf node has (the 'radix tree' part of the
* map) */
/** Number of children each inner node may have (the radix fan-out). */
static constexpr unsigned int kBRANCH_FACTOR = SHAMapInnerNode::kBRANCH_FACTOR;
/** The depth of the hash map: data is only present in the leaves */
/** Tree depth at which leaves reside; keys are 256 bits consumed 4 bits
* per level, so leaves are always at depth 64.
*/
static constexpr unsigned int kLEAF_DEPTH = 64;
/** A (before, after) pair of `SHAMapItem` pointers representing one changed
* entry in a delta computation. A null `first` means the item was added;
* a null `second` means it was deleted; both non-null means it was
* modified.
*/
using DeltaItem =
std::pair<boost::intrusive_ptr<SHAMapItem const>, boost::intrusive_ptr<SHAMapItem const>>;
/** Map from item key to its before/after state, produced by `compare()`. */
using Delta = std::map<uint256, DeltaItem>;
SHAMap() = delete;
@@ -108,22 +140,54 @@ public:
SHAMap&
operator=(SHAMap const&) = delete;
// Take a snapshot of the given map:
/** Construct a CoW snapshot of `other`.
*
* No tree nodes are copied. The new map shares `root_` with `other` and
* takes ownership of subsequent mutations through CoW cloning. If either
* map is mutable, `unshare()` is called immediately so that later writes
* on one map cannot corrupt the other. An immutable snapshot of an
* immutable map is O(1) with zero copies.
*
* Prefer `snapShot()` over calling this constructor directly.
*
* @param other The source map to snapshot.
* @param isMutable If `true`, the new map is in `Modifying` state;
* otherwise `Immutable`.
*/
SHAMap(SHAMap const& other, bool isMutable);
// build new map
/** Construct a new, empty map in `Modifying` state.
*
* @param t Whether this map holds transactions (`SHAMapType::Transaction`)
* or account state (`SHAMapType::State`).
* @param f The `Family` providing the NodeStore, caches, and journal.
*/
SHAMap(SHAMapType t, Family& f);
/** Construct a map in `Synching` state for a ledger whose root hash is
* known but whose nodes must still be fetched from peers.
*
* The `hash` parameter is not stored internally; it serves as a
* documentation signal that callers should subsequently call
* `fetchRoot(hash, filter)` to install the root node.
*
* @param t Whether this map holds transactions or account state.
* @param hash The expected root hash (not stored; used to select this
* overload over the two-argument form).
* @param f The `Family` providing the NodeStore, caches, and journal.
*/
SHAMap(SHAMapType t, uint256 const& hash, Family& f);
~SHAMap() = default;
/** Return the `Family` that backs this map's storage and caches. */
Family const&
family() const
{
return f_;
}
/** Return the `Family` that backs this map's storage and caches. */
Family&
family()
{
@@ -132,124 +196,257 @@ public:
//--------------------------------------------------------------------------
/** Iterator to a SHAMap's leaves
This is always a const iterator.
Meets the requirements of ForwardRange.
*/
/** Forward iterator over leaf nodes in ascending key order.
*
* Always const — see `ConstIterator` for details. Satisfies
* `std::forward_iterator`.
*/
class ConstIterator;
/** Return an iterator to the first leaf in key order, or `end()` if empty. */
ConstIterator
begin() const;
/** Return the past-the-end sentinel iterator. */
ConstIterator
end() const;
//--------------------------------------------------------------------------
// Returns a new map that's a snapshot of this one.
// Handles copy on write for mutable snapshots.
/** Return a heap-allocated CoW snapshot of this map.
*
* Delegates to the copy constructor. If `isMutable` is false and this
* map is also immutable, no nodes are copied (O(1)). If either side is
* mutable, `unshare()` is called to prevent cross-map corruption on
* subsequent writes.
*
* @param isMutable If `true`, the snapshot enters `Modifying` state and
* may be mutated independently.
* @return A `shared_ptr` to the new snapshot.
*/
std::shared_ptr<SHAMap>
snapShot(bool isMutable) const;
/* Mark this SHAMap as "should be full", indicating
that the local server wants all the corresponding nodes
in durable storage.
*/
/** Mark this map as expected to be locally complete.
*
* Sets `full_ = true`, indicating the server wants all referenced nodes
* in durable storage. A subsequent cache miss clears the flag and
* triggers missing-node acquisition via the `Family`.
*/
void
setFull();
/** Associate this map with a specific ledger sequence number.
*
* The sequence number is passed to `NodeStore` fetch calls and to the
* missing-node acquisition callback so that the inbound-ledger pipeline
* can identify which ledger needs repair.
*
* @param lseq The ledger sequence number.
*/
void
setLedgerSeq(std::uint32_t lseq);
/** Fetch and install the root node for a map in `Synching` state.
*
* Attempts to locate the node identified by `hash` via the tiered fetch
* path (cache → NodeStore → `filter`). On success, `root_` is replaced
* and the map is ready for further node ingestion via `addKnownNode()`.
*
* @param hash Expected hash of the root node.
* @param filter Optional sync filter consulted if the node is not in the
* cache or NodeStore; may be `nullptr`.
* @return `true` if the root was successfully installed.
*/
bool
fetchRoot(SHAMapHash const& hash, SHAMapSyncFilter* filter);
// normal hash access functions
/** Does the tree have an item with the given ID? */
/** Return `true` if the map contains an item whose key equals `id`. */
bool
hasItem(uint256 const& id) const;
/** Remove the item with key `id` from the map.
*
* If the deletion leaves a parent inner node with only one child, that
* node is collapsed upward (merge property). The map must be in
* `Modifying` state.
*
* @param id Key of the item to remove.
* @return `true` if the item was found and removed; `false` if not found.
*/
bool
delItem(uint256 const& id);
/** Insert a new item into the map, taking shared ownership of `item`.
*
* Forwards to `addGiveItem()`. The map must be in `Modifying` state and
* must not already contain an item with the same key.
*
* @param type Leaf type determining the hash prefix and wire format.
* @param item The item to insert.
* @return `true` if the item was inserted; `false` if a duplicate key
* already exists.
*/
bool
addItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const> item);
/** Compute and return the root hash of this map.
*
* If any node hashes are dirty the tree is re-hashed bottom-up before
* returning. This involves a `const_cast` internally (logical const,
* physical mutate) — acknowledged design compromise documented in
* `SHAMap.cpp`.
*
* @return The `SHAMapHash` of the root node.
*/
SHAMapHash
getHash() const;
// save a copy if you have a temporary anyway
/** Replace an existing item in the map, transferring ownership of `item`.
*
* Locates the leaf whose key matches `item->key()` and replaces its
* payload. Calls `dirtyUp()` only when `setItem()` reports the hash
* changed, avoiding spurious rehashing for no-op updates. The map must
* be in `Modifying` state.
*
* @param type Leaf type (must match the type the item was inserted with).
* @param item Replacement payload; its `key()` must match an existing
* entry.
* @return `true` if the item was found and updated; `false` if not found.
*/
bool
updateGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const> item);
/** Insert a new item into the map, transferring ownership of `item`.
*
* If the target leaf position is empty, a new leaf is created there. If
* it collides with an existing leaf whose key differs, inner nodes are
* created at deeper levels until the two keys' nibbles diverge (respecting
* the merge property). The map must be in `Modifying` state.
*
* @param type Leaf type determining the hash prefix and wire format.
* @param item The item to insert; its `key()` must not already exist.
* @return `true` if the item was inserted; `false` if a duplicate key
* already exists.
*/
bool
addGiveItem(SHAMapNodeType type, boost::intrusive_ptr<SHAMapItem const> item);
// Save a copy if you need to extend the life
// of the SHAMapItem beyond this SHAMap
/** Return a reference to the intrusive pointer for the item with key `id`.
*
* The reference is valid only while the map is not mutated. To keep the
* item alive beyond the map's next mutation, copy the intrusive pointer.
*
* @param id Key to look up.
* @return Reference to the stored `intrusive_ptr`, or to a null pointer
* if `id` is not found.
*/
boost::intrusive_ptr<SHAMapItem const> const&
peekItem(uint256 const& id) const;
/** Return a reference to the intrusive pointer for `id`, also supplying
* the leaf's hash.
*
* @param id Key to look up.
* @param hash Receives the hash of the leaf node on success; unchanged
* on miss.
* @return Reference to the stored `intrusive_ptr`, or to a null pointer
* if `id` is not found.
*/
boost::intrusive_ptr<SHAMapItem const> const&
peekItem(uint256 const& id, SHAMapHash& hash) const;
// traverse functions
/** Find the first item after the given item.
@param id the identifier of the item.
@note The item does not need to exist.
/** Return an iterator to the first leaf whose key is strictly greater than
* `id`.
*
* `id` does not need to exist in the map.
*
* @param id Lower-bound key (exclusive).
* @return Iterator to the first leaf with key > `id`, or `end()`.
*/
ConstIterator
upperBound(uint256 const& id) const;
/** Find the object with the greatest object id smaller than the input id.
@param id the identifier of the item.
@note The item does not need to exist.
/** Return an iterator to the first leaf whose key is greater than or equal
* to `id`.
*
* `id` does not need to exist in the map.
*
* @param id Lower-bound key (inclusive).
* @return Iterator to the first leaf with key >= `id`, or `end()`.
*/
ConstIterator
lowerBound(uint256 const& id) const;
/** Visit every node in this SHAMap
@param function called with every node visited.
If function returns false, visitNodes exits.
*/
/** Invoke `function` on every node (inner and leaf) in the tree.
*
* Uses an explicit stack to avoid recursion on 64-level trees.
* Traversal order is unspecified but consistent within a single call.
*
* @param function Callable invoked with each `SHAMapTreeNode&`. Return
* `false` to stop traversal early.
*/
void
visitNodes(std::function<bool(SHAMapTreeNode&)> const& function) const;
/** Visit every node in this SHAMap that
is not present in the specified SHAMap
@param function called with every node visited.
If function returns false, visitDifferences exits.
*/
/** Invoke `function` on every node present in this map but absent from
* `have`.
*
* Short-circuits at hash equality, so it is O(d) in the number of
* differing nodes rather than O(n) in total nodes.
*
* @param have The map whose nodes are considered "already present";
* may be `nullptr` (treats every node in `this` as absent from `have`).
* @param function Callable invoked with each differing `SHAMapTreeNode
* const&`. Return `false` to stop traversal early.
*/
void
visitDifferences(SHAMap const* have, std::function<bool(SHAMapTreeNode const&)> const&) const;
/** Visit every leaf node in this SHAMap
@param function called with every non inner node visited.
*/
/** Invoke `function` on the payload of every leaf node in the tree.
*
* Delegates to `visitNodes`, filtering out inner nodes.
*
* @param function Callable invoked with each leaf's
* `boost::intrusive_ptr<SHAMapItem const>`.
*/
void
visitLeaves(std::function<void(boost::intrusive_ptr<SHAMapItem const> const&)> const&) const;
// comparison/sync functions
/** Check for nodes in the SHAMap not available
Traverse the SHAMap efficiently, maximizing I/O
concurrency, to discover nodes referenced in the
SHAMap but not available locally.
@param maxNodes The maximum number of found nodes to return
@param filter The filter to use when retrieving nodes
@param return The nodes known to be missing
*/
/** Traverse the tree to discover nodes that are referenced but not
* available locally, maximizing I/O concurrency via async fetches.
*
* Uses the `MissingNodes` DFS engine with bounded async concurrency
* (`maxDefer = 512`). Subtrees confirmed complete via the
* `FullBelowCache` are skipped. Random start nibbles per inner node
* spread concurrent callers across different parts of the tree.
*
* @param maxNodes Stop after collecting this many missing-node entries.
* @param filter Optional sync filter consulted for each missing hash;
* may be `nullptr`.
* @return Vector of `(SHAMapNodeID, hash)` pairs for nodes that could not
* be located.
*/
std::vector<std::pair<SHAMapNodeID, uint256>>
getMissingNodes(int maxNodes, SHAMapSyncFilter* filter);
/** Serialize a node and a bounded-depth subtree for peer delivery.
*
* Bundles the target node with adjacent inner nodes up to `depth` levels
* deep. The depth budget is decremented only when an inner node has more
* than one non-empty child; single-child chains (compressed radix paths)
* are traversed for free. This amortizes per-message round-trip cost
* during tree synchronization.
*
* @param wanted The `SHAMapNodeID` of the node the peer requested.
* @param data Output vector; each appended entry is a
* `(SHAMapNodeID, wire-serialized blob)` pair.
* @param fatLeaves If `true`, leaf nodes adjacent to traversed inner nodes
* are also included; otherwise only inner nodes are emitted.
* @param depth Maximum number of additional levels to bundle.
* @return `true` if the requested node was found and at least one entry
* was appended; `false` if the node could not be located.
*/
bool
getNodeFat(
SHAMapNodeID const& wanted,
@@ -257,71 +454,220 @@ public:
bool fatLeaves,
std::uint32_t depth) const;
/**
* Get the proof path of the key. The proof path is every node on the path
* from leaf to root. Sibling hashes are stored in the parent nodes.
* @param key key of the leaf
* @return the proof path if found
/** Collect the Merkle proof path for `key`.
*
* Walks from the root toward `key`, collecting the serialized form of
* every node on the path (leaf first, root last). Sibling hashes are
* encoded inside each parent's serialization, enabling the recipient to
* reconstruct and verify the root hash without the full tree.
*
* @param key 256-bit key of the target leaf.
* @return A vector of serialized node blobs from leaf to root if `key`
* is present in the map; `std::nullopt` if not found.
*/
std::optional<std::vector<Blob>>
getProofPath(uint256 const& key) const;
/**
* Verify the proof path
* @param rootHash root hash of the map
* @param key key of the leaf
* @param path the proof path
* @return true if verified successfully
/** Verify a Merkle proof path without a live tree.
*
* Recomputes the root hash from `path` (leaf-to-root order) using the
* nibbles of `key` to select the correct branch at each level, and
* compares the result against `rootHash`. Bounded to 65 levels; network
* input is wrapped in try/catch to handle malformed blobs.
*
* @param rootHash Expected root hash of the tree.
* @param key 256-bit key that the path was generated for. The
* verifier uses this key's nibbles to navigate — a wrong key at any
* level produces a false negative.
* @param path Serialized nodes from leaf to root, as returned by
* `getProofPath()`.
* @return `true` if the recomputed root matches `rootHash`.
*/
static bool
verifyProofPath(uint256 const& rootHash, uint256 const& key, std::vector<Blob> const& path);
/** Serializes the root in a format appropriate for sending over the wire */
/** Serialize the root node into `s` in the wire format used for peer
* messages.
*
* @param s Serializer to append the encoded root node to.
*/
void
serializeRoot(Serializer& s) const;
/** Install the root node received from a peer during synchronization.
*
* Deserializes `rootNode`, verifies its hash matches `hash`, and
* installs it as `root_` if the map is in `Synching` state and the
* current root is empty. Notifies `filter` on success so it can persist
* or cache the data.
*
* @param hash Expected hash of the root node.
* @param rootNode Wire-serialized root node received from a peer.
* @param filter Optional sync filter for notification; may be `nullptr`.
* @return A `SHAMapAddNode` tri-state: useful (new root installed),
* duplicate (root already present), or invalid (hash mismatch or
* deserialization failure).
*/
SHAMapAddNode
addRootNode(SHAMapHash const& hash, Slice const& rootNode, SHAMapSyncFilter* filter);
/** Install a known interior or leaf node received from a peer.
*
* Performs two integrity checks before installing: (1) the deserialized
* node's hash must match the hash recorded in its parent branch; (2) for
* leaves at `kLEAF_DEPTH`, the reconstructed `SHAMapNodeID` from the
* leaf's own key must match `nodeID`, closing a theoretical
* hash-collision-at-wrong-position attack. A mismatch on either check
* transitions the map to `Invalid` rather than crashing.
*
* Skips descent into subtrees already confirmed complete via the
* `FullBelowCache`.
*
* @param nodeID Tree address of the node being provided.
* @param rawNode Wire-serialized node data.
* @param filter Optional sync filter; may be `nullptr`.
* @return `SHAMapAddNode` tri-state: useful, duplicate, or invalid.
* @note Returning `invalid` is normal for stale or mismatched peer data;
* callers must handle it gracefully without crashing.
*/
SHAMapAddNode
addKnownNode(SHAMapNodeID const& nodeID, Slice const& rawNode, SHAMapSyncFilter* filter);
// status functions
/** Freeze the map, forbidding all future writes.
*
* Advances the state from `Modifying` or `Synching` to `Immutable`.
* Asserts that the current state is not `Invalid` — only coherent maps
* may be frozen. After this call, any attempt to mutate the map will
* trigger an assertion failure.
*/
void
setImmutable();
/** Return `true` if the map is in `Synching` state. */
bool
isSynching() const;
/** Advance the map to `Synching` state to begin peer synchronization. */
void
setSynching();
/** Revert the map from `Synching` back to `Modifying` state.
*
* Used when synchronization is abandoned before completion so the map
* can be used for local modifications again.
*/
void
clearSynching();
/** Return `true` if the map is in any state other than `Invalid`. */
bool
isValid() const;
// caution: otherMap must be accessed only by this function
// return value: true=successfully completed, false=too different
/** Compute the symmetric difference between this map and `otherMap`.
*
* Walks both trees concurrently, short-circuiting at subtree roots whose
* hashes match (O(d) in the number of differences, not O(n) in total
* items). Results are recorded in `differences` as `DeltaItem` pairs.
*
* @param otherMap The map to compare against. Must not be accessed
* concurrently by any other caller for the duration of this call.
* @param differences Receives one entry per key that differs between the
* two maps. Entries are appended; the caller is responsible for
* starting with an empty map if that is desired.
* @param maxCount Maximum number of differences to record before
* returning early. Pass `INT_MAX` for unlimited.
* @return `true` if the comparison completed without hitting `maxCount`;
* `false` if truncated.
*/
bool
compare(SHAMap const& otherMap, Delta& differences, int maxCount) const;
/** Convert any modified nodes to shared. */
/** Set `cowid_` to zero on every node, making them all shareable.
*
* After this call the map's nodes may be safely shared across CoW
* snapshots without cloning. Does not write to the NodeStore.
*
* @return The number of nodes processed.
*/
int
unshare();
/** Flush modified nodes to the nodestore and convert them to shared. */
/** Serialize and persist dirty nodes, then mark them as shared.
*
* Performs a post-order DFS with an explicit stack to avoid stack
* overflow on 64-deep trees. Per node: clones if CoW-shared, recomputes
* hash, clears `cowid_`, and writes to `Family::db()` if `backed_` is
* `true`.
*
* @param t The `NodeObjectType` tag used when writing to the NodeStore.
* @return The number of nodes written.
*/
int
flushDirty(NodeObjectType t);
/** Single-threaded completeness check; records missing nodes up to a limit.
*
* Traverses the tree, attempting to fetch each referenced node via
* `descendNoStore` (returns null instead of throwing on a miss). Any
* unreachable node is appended to `missingNodes`.
*
* @param missingNodes Output vector; missing nodes are appended.
* @param maxMissing Stop after recording this many missing nodes.
*/
void
walkMap(std::vector<SHAMapMissingNode>& missingNodes, int maxMissing) const;
/** Parallel completeness check using one thread per top-level branch.
*
* Partitions at depth 1 — up to 16 `std::thread`s, one per non-empty,
* non-leaf top-level child. Each thread has its own node stack;
* `missingNodes` and an internal exceptions vector are mutex-protected.
*
* @param missingNodes Output vector; missing nodes are appended (all
* threads share it under a mutex).
* @param maxMissing Stop after recording this many missing nodes.
* @return `true` if no worker thread threw an exception; `false`
* otherwise. A `false` return does NOT mean there are no missing
* nodes — always inspect `missingNodes` regardless of the return
* value.
* @note Worker threads catch `SHAMapMissingNode` to avoid `std::terminate`;
* uncaught exceptions are stored internally and reflected only in
* the return value. Callers must inspect both the return value and
* `missingNodes` for a complete picture.
*/
bool
walkMapParallel(std::vector<SHAMapMissingNode>& missingNodes, int maxMissing) const;
bool
deepCompare(SHAMap& other) const; // Intended for debug/test only
/** Item-by-item equality check for debug and test use only.
*
* Slower than hash comparison; intended for validating test fixtures.
*
* @param other The map to compare against.
* @return `true` if both maps contain identical items.
*/
bool
deepCompare(SHAMap& other) const;
/** Detach this map from the NodeStore, making all I/O in-memory only.
*
* Sets `backed_ = false`. Subsequent fetches consult only the in-process
* `TreeNodeCache`; all writes are suppressed. Use for transient trees
* (e.g., during transaction processing) that must not touch the database.
*/
void
setUnbacked();
/** Log every node in the tree to the journal for debugging. */
void
dump(bool withHashes = false) const;
/** Assert that all structural invariants hold across the entire tree.
*
* Checks that every node satisfies its own `invariants()`, that inner
* nodes' `isBranch_` bitmask is consistent with their children, and that
* no leaf appears above `kLEAF_DEPTH`. Intended for test and debug
* builds; a violation triggers an assertion failure.
*/
void
invariants() const;
@@ -459,8 +805,13 @@ private:
int
walkSubTree(bool doWrite, NodeObjectType t);
// Structure to track information about call to
// getMissingNodes while it's in progress
/** Mutable traversal state for a single call to `getMissingNodes()`.
*
* Bundles the DFS stack, async I/O bookkeeping, and result accumulation
* for one invocation of the missing-node discovery algorithm. Non-
* copyable; created on the stack inside `getMissingNodes()` and passed by
* reference to the two helper functions.
*/
struct MissingNodes
{
MissingNodes() = delete;
@@ -468,21 +819,31 @@ private:
MissingNodes&
operator=(MissingNodes const&) = delete;
// basic parameters
/** Maximum number of missing nodes to collect before returning. */
int max;
/** Optional sync filter consulted for each missing hash. */
SHAMapSyncFilter* filter;
/** Maximum number of async reads to keep in flight simultaneously. */
int const maxDefer;
/** `FullBelowCache` generation at traversal start; subtrees whose
* `fullBelowGen_` matches this value are skipped as already complete.
*/
std::uint32_t generation;
// nodes we have discovered to be missing
/** Accumulated (SHAMapNodeID, hash) pairs for nodes confirmed absent. */
std::vector<std::pair<SHAMapNodeID, uint256>> missingNodes;
/** Hashes of missing nodes, for deduplication. */
std::set<SHAMapHash> missingHashes;
// nodes we are in the process of traversing
/** One entry per inner node actively being traversed. */
using StackEntry = std::tuple<
SHAMapInnerNode*, // pointer to the node
SHAMapNodeID, // the node's ID
int, // while child we check first
int, // which child we check first
int, // which child we check next
bool>; // whether we've found any missing children yet
@@ -493,20 +854,28 @@ private:
// such as std::vector, can't be used here.
std::stack<StackEntry, std::deque<StackEntry>> stack;
// nodes we may have acquired from deferred reads
/** One completed async read awaiting processing. */
using DeferredNode = std::tuple<
SHAMapInnerNode*, // parent node
SHAMapNodeID, // parent node ID
int, // branch
intr_ptr::SharedPtr<SHAMapTreeNode>>; // node
intr_ptr::SharedPtr<SHAMapTreeNode>>; // fetched node
/** Count of async reads currently in flight. */
int deferred;
/** Guards `finishedReads` and `deferred` for async callbacks. */
std::mutex deferLock;
/** Signalled by async callbacks when `finishedReads` is non-empty. */
std::condition_variable deferCondVar;
/** Completed async reads waiting to be installed into the tree. */
std::vector<DeferredNode> finishedReads;
// nodes we need to resume after we get their children from deferred
// reads
/** Inner nodes that must be revisited once their deferred children
* have been installed.
*/
std::map<SHAMapInnerNode*, SHAMapNodeID> resumes;
MissingNodes(int max, SHAMapSyncFilter* filter, int maxDefer, std::uint32_t generation)
@@ -579,6 +948,21 @@ SHAMap::setUnbacked()
//------------------------------------------------------------------------------
/** Forward iterator over `SHAMap` leaf nodes in ascending key order.
*
* Carries its own `SharedPtrNodeStack` — a path from the root to the current
* position — so that `operator++` can resume descent without rescanning from
* the root. Always const: the Merkle invariant requires that any write
* re-hashes the entire path to the root, so a non-const iterator would either
* silently break the invariant or force expensive re-hashing on every
* dereference.
*
* Satisfies `std::forward_iterator`. Default-constructing is not permitted;
* obtain iterators from `SHAMap::begin()` and `SHAMap::end()`.
*
* @note Comparing iterators from different `SHAMap` instances is undefined
* and will trigger an assertion in debug builds.
*/
class SHAMap::ConstIterator
{
public:
@@ -602,13 +986,23 @@ public:
~ConstIterator() = default;
/** Dereference the iterator, returning the current `SHAMapItem`. */
reference
operator*() const;
/** Dereference the iterator, returning a pointer to the current item. */
pointer
operator->() const;
/** Advance to the next leaf in key order and return a reference to this
* iterator.
*/
ConstIterator&
operator++();
/** Advance to the next leaf in key order and return the prior iterator
* value.
*/
ConstIterator
operator++(int);
@@ -676,6 +1070,10 @@ SHAMap::ConstIterator::operator++(int)
return tmp;
}
/** Return `true` if `x` and `y` refer to the same leaf (or both are end).
*
* @note Asserts in debug builds that both iterators belong to the same map.
*/
inline bool
operator==(SHAMap::ConstIterator const& x, SHAMap::ConstIterator const& y)
{
@@ -686,6 +1084,7 @@ operator==(SHAMap::ConstIterator const& x, SHAMap::ConstIterator const& y)
return x.item_ == y.item_;
}
/** Return `true` if `x` and `y` do not refer to the same leaf. */
inline bool
operator!=(SHAMap::ConstIterator const& x, SHAMap::ConstIterator const& y)
{

View File

@@ -8,17 +8,57 @@
namespace xrpl {
/** A leaf node for a state object. */
/** SHAMap leaf node holding a serialized ledger state object.
*
* Represents account roots, offers, trust lines, and other ledger objects
* in the SHAMap state trie. One of three concrete leaf types alongside
* `SHAMapTxLeafNode` and `SHAMapTxPlusMetaLeafNode`; each encodes
* type-specific hashing and serialization rules statically via virtual
* dispatch, eliminating runtime branching in the hot path.
*
* Unlike transaction leaves, the Merkle hash commits to both the payload
* and the item key (see `updateHash()`). This is required because state
* object keys (e.g. account address, offer index) are externally assigned
* and may not appear verbatim in the serialized blob — omitting the key
* would allow two distinct objects with identical payloads to collide.
*
* All mutable state lives in the base classes. This class is a stateless
* policy layer: it supplies only the hash formula, clone factory, type
* tag, and wire format for account-state leaves.
*
* @see SHAMapLeafNode
* @see SHAMapTxLeafNode
* @see SHAMapTxPlusMetaLeafNode
*/
class SHAMapAccountStateLeafNode final : public SHAMapLeafNode,
public CountedObject<SHAMapAccountStateLeafNode>
{
public:
/** Construct a new account-state leaf and compute its hash.
*
* Use this constructor when creating a brand-new node from a freshly
* produced item. `updateHash()` is called immediately so the node is
* valid for insertion into the trie.
*
* @param item The ledger state object payload; must be non-null.
* @param cowid Copy-on-write owner ID of the creating SHAMap instance.
*/
SHAMapAccountStateLeafNode(boost::intrusive_ptr<SHAMapItem const> item, std::uint32_t cowid)
: SHAMapLeafNode(std::move(item), cowid)
{
updateHash();
}
/** Construct an account-state leaf with a pre-computed hash.
*
* Used by `clone()` when the underlying item has not changed: forwarding
* the existing hash avoids a redundant SHA-512 computation.
*
* @param item The ledger state object payload; must be non-null.
* @param cowid Copy-on-write owner ID for the new node.
* @param hash Known hash of `item`; must be consistent with the item's
* current content or the Merkle tree will be corrupted.
*/
SHAMapAccountStateLeafNode(
boost::intrusive_ptr<SHAMapItem const> item,
std::uint32_t cowid,
@@ -27,24 +67,60 @@ public:
{
}
/** Produce an exclusively owned copy of this node for copy-on-write mutation.
*
* The new node shares the same item and hash as the original — no
* recomputation occurs. The caller supplies the new `cowid` so the clone
* is immediately owned by the mutating SHAMap.
*
* @param cowid Copy-on-write owner ID for the cloned node.
* @return A freshly allocated `SHAMapAccountStateLeafNode` with the same
* item and hash, owned exclusively by `cowid`.
*/
intr_ptr::SharedPtr<SHAMapTreeNode>
clone(std::uint32_t cowid) const final
{
return intr_ptr::makeShared<SHAMapAccountStateLeafNode>(item_, cowid, hash_);
}
/** Return the node type tag for account-state leaves.
*
* @return `SHAMapNodeType::TnAccountState`
*/
SHAMapNodeType
getType() const final
{
return SHAMapNodeType::TnAccountState;
}
/** Recompute and store this node's Merkle hash.
*
* Hashes the `HashPrefix::LeafNode` domain separator (`'M'`,`'L'`,`'N'`),
* the raw item payload, and the item key together via `sha512Half`. The key
* is included because state object keys are externally assigned identifiers
* that bind the object to its trie position; without the key, two objects
* with identical serialized data would produce the same hash regardless of
* their position in the ledger.
*
* @note The hash formula differs from `SHAMapTxLeafNode::updateHash()`,
* which omits the key because a transaction's ID is already derived
* from `sha512Half(prefix, blob)`.
*/
void
updateHash() final
{
hash_ = SHAMapHash{sha512Half(HashPrefix::LeafNode, item_->slice(), item_->key())};
}
/** Serialize this node for peer-to-peer sync (wire format).
*
* Writes the raw item payload, then the item key as a fixed-width bit
* string, then the single-byte wire-type tag `kWIRE_TYPE_ACCOUNT_STATE`
* (`1`). The tag at the end allows `SHAMapTreeNode::makeFromWire()` to
* reconstruct the correct concrete leaf type on the receiving peer.
*
* @param s Serializer to append to.
*/
void
serializeForWire(Serializer& s) const final
{
@@ -53,6 +129,15 @@ public:
s.add8(kWIRE_TYPE_ACCOUNT_STATE);
}
/** Serialize this node in the canonical hashing format.
*
* Writes the 4-byte `HashPrefix::LeafNode` domain separator, the raw item
* payload, and the item key. This matches the input fed to `sha512Half` in
* `updateHash()` and is used for Merkle proof verification where the hash
* prefix already encodes the node type (no wire-type tag is appended).
*
* @param s Serializer to append to.
*/
void
serializeWithPrefix(Serializer& s) const final
{

View File

@@ -4,7 +4,25 @@
namespace xrpl {
// results of adding nodes
/** Accumulates the outcome of adding one or more nodes to a SHAMap during sync.
*
* During ledger synchronization, raw trie nodes received from peers are
* classified as useful (new and hash-verified), duplicate (already present),
* or invalid (corrupt, hash-mismatched, or structurally wrong). This class
* collects those three counts so callers can assess whether a peer's
* contribution was helpful, harmless, or harmful.
*
* Instances are typically constructed via the static factory methods
* (`useful()`, `duplicate()`, `invalid()`) as single-count return values
* from `SHAMap::addRootNode()` and `SHAMap::addKnownNode()`. They are then
* aggregated with `operator+=` across a batch of nodes in higher-level
* acquisition code such as `InboundLedger::receiveNode()`.
*
* @note Duplicates count on the positive side of `isGood()` because
* receiving a node you already have is benign. Only `mBad` accumulates
* evidence of peer misbehavior.
* @see SHAMap::addRootNode, SHAMap::addKnownNode
*/
class SHAMapAddNode
{
private:
@@ -13,33 +31,114 @@ private:
int duplicate_;
public:
/** Construct a zero-count accumulator. */
SHAMapAddNode();
/** Record one invalid node (corrupt, hash-mismatched, or structurally wrong). */
void
incInvalid();
/** Record one useful node (new, hash-verified, and written to the map). */
void
incUseful();
/** Record one duplicate node (valid but already present in the map). */
void
incDuplicate();
/** Reset all counters to zero. */
void
reset();
/** Return the count of useful (good) nodes recorded.
*
* @return The number of new, hash-verified nodes added.
*/
[[nodiscard]] int
getGood() const;
/** Return whether the exchange was net non-harmful.
*
* Returns `true` when `(good + duplicate) > bad`. Duplicates count
* positively because they are benign; only bad nodes are evidence of
* misbehavior. Used by `TransactionAcquire::takeNodes()` and
* `InboundLedger` to decide whether to continue processing a peer's
* contribution.
*
* @return `true` if the peer's contribution was not net-harmful.
*/
[[nodiscard]] bool
isGood() const;
/** Return whether any invalid nodes were recorded.
*
* @return `true` if at least one node was classified as invalid.
*/
[[nodiscard]] bool
isInvalid() const;
/** Return whether actual forward progress was made.
*
* Answers "did we receive at least one new node we didn't already have?"
* Strictly stronger than `isGood()`: duplicates do not satisfy this
* predicate. Used by `InboundLedger` to advance the `progress_` flag
* and decide whether sync moved forward.
*
* @return `true` if at least one useful (new) node was added.
*/
[[nodiscard]] bool
isUseful() const;
/** Format the counters as a human-readable string for journal output.
*
* Emits only the non-zero counters, e.g., `"good:3 dupe:1"` or
* `"bad:2"`. Returns `"no nodes processed"` when all three counts are
* zero.
*
* @return A compact diagnostic string suitable for debug-level logging.
*/
[[nodiscard]] std::string
get() const;
/** Combine another accumulator into this one by summing all three counters.
*
* Used to aggregate results across a batch of nodes, e.g., in
* `InboundLedger::receiveNode()` where `san += map.addKnownNode(...)` is
* called in a loop.
*
* @param n The accumulator to add.
* @return A reference to this accumulator.
*/
SHAMapAddNode&
operator+=(SHAMapAddNode const& n);
/** Construct a single-duplicate-count instance.
*
* Returned by `addRootNode` / `addKnownNode` when the node was valid
* but already present in the map.
*
* @return An instance with `duplicate = 1`, `good = 0`, `bad = 0`.
*/
static SHAMapAddNode
duplicate();
/** Construct a single-useful-count instance.
*
* Returned by `addRootNode` / `addKnownNode` when the node was new
* and successfully verified and inserted.
*
* @return An instance with `good = 1`, `bad = 0`, `duplicate = 0`.
*/
static SHAMapAddNode
useful();
/** Construct a single-invalid-count instance.
*
* Returned by `addRootNode` / `addKnownNode` when the node failed hash
* verification, had a structural mismatch, or arrived on an empty branch.
*
* @return An instance with `bad = 1`, `good = 0`, `duplicate = 0`.
*/
static SHAMapAddNode
invalid();

View File

@@ -11,71 +11,130 @@
namespace xrpl {
/** Branching (non-leaf) node of the SHAMap authenticated Merkle radix tree.
*
* Each inner node fans out into exactly `kBRANCH_FACTOR` (16) children, one
* per hexadecimal nibble of a 256-bit key. Together with `SHAMapLeafNode`,
* inner nodes form a trie of depth at most 64 levels.
*
* Child hashes and child pointers are stored together in a single sparse
* allocation via `TaggedPointer hashesAndChildren_`. When fewer than all 16
* branches are occupied the arrays are kept compact (packed in branch-index
* order), reducing per-node memory to roughly 25% of a dense layout for a
* typical production tree.
*
* The `lock_` field is a 16-bit atomic bitlock — one bit per child slot —
* allowing concurrent access to *different* children of the same node without
* global serialization.
*
* @note Callers must `clone()` a shared node (one with `cowid() == 0`) before
* mutating it. `setChild()` and `shareChild()` both assert `cowid_ != 0`.
* @see SHAMapTreeNode for copy-on-write ownership semantics.
* @see TaggedPointer for the sparse storage implementation.
*/
class SHAMapInnerNode final : public SHAMapTreeNode, public CountedObject<SHAMapInnerNode>
{
public:
/** Each inner node has 16 children (the 'radix tree' part of the map) */
/** Number of children per inner node — one per hex nibble of the key. */
static constexpr unsigned int kBRANCH_FACTOR = 16;
private:
/** Opaque type that contains the `hashes` array (array of type
`SHAMapHash`) and the `children` array (array of type
`intr_ptr::SharedPtr<SHAMapInnerNode>`).
/** Co-located sparse arrays: `SHAMapHash[N]` followed by
* `SharedPtr<SHAMapTreeNode>[N]`, where N is determined by the 2-bit tag
* stored in the pointer's low bits (capacity tiers: 2, 4, 8, 16).
*
* `isBranch_` is the authoritative occupancy bitset; in sparse mode the
* arrays hold only non-empty children packed in ascending branch-index
* order.
*/
TaggedPointer hashesAndChildren_;
/** Generation counter used by the full-below optimization.
*
* When equal to the current sync generation, every node in the subtree
* below this node is known to be present locally and does not need to be
* fetched from peers.
*/
std::uint32_t fullBelowGen_ = 0;
/** Bitmask of occupied branches; bit `i` is set iff branch `i` is
* non-empty. This is the single source of truth for occupancy; the
* `hashesAndChildren_` arrays are sized accordingly.
*/
std::uint16_t isBranch_ = 0;
/** A bitlock for the children of this node, with one bit per child */
/** Per-child bit spinlock, one bit per physical array slot.
*
* Allows concurrent reads of *different* children without a global lock.
* `getChild()`, `getChildPointer()`, and `canonicalizeChild()` all
* acquire only the single bit corresponding to the target child's array
* index. `setChild()` and `shareChild()` skip locking because they
* require CoW ownership (asserted via `cowid_`).
*/
mutable std::atomic<std::uint16_t> lock_ = 0;
/** Convert arrays stored in `hashesAndChildren_` so they can store the
requested number of children.
@param toAllocate allocate space for at least this number of children
(must be <= branchFactor)
@note the arrays may allocate more than the requested value in
`toAllocate`. This is due to the implementation of TagPointer, which
only supports allocating arrays of 4 different sizes.
/** Resize the co-located hash and child-pointer arrays to accommodate
* at least `toAllocate` children.
*
* Existing children are preserved; the caller is responsible for
* updating `isBranch_` before and after as needed. Because
* `TaggedPointer` only supports four capacity tiers (2, 4, 8, 16), the
* actual allocation may be larger than `toAllocate`.
*
* @param toAllocate minimum required capacity; must be ≤ `kBRANCH_FACTOR`.
*/
void
resizeChildArrays(std::uint8_t toAllocate);
/** Get the child's index inside the `hashes` or `children` array (stored in
`hashesAndChildren_`.
These arrays may or may not be sparse). The optional will be empty is an
empty branch is requested and the arrays are sparse.
@param i index of the requested child
/** Translate a logical branch number to a physical array index.
*
* In sparse mode, only occupied branches are stored, so the physical index
* is `popcount(isBranch_ & ((1 << i) - 1))`. In dense mode (all 16 slots
* allocated) branch number equals array index.
*
* @param i Logical branch number (015).
* @return The physical array index, or `std::nullopt` if the branch is
* empty and the arrays are in sparse mode.
*/
std::optional<int>
getChildIndex(int i) const;
/** Call the `f` callback for all 16 (branchFactor) branches - even if
the branch is empty.
@param f a one parameter callback function. The parameter is the
child's hash.
*/
/** Invoke `f` for every one of the 16 branches, passing each branch's
* `SHAMapHash` — zero-hash for empty branches.
*
* Used by `updateHash()` and `serializeForWire()` to iterate the full
* logical child-hash array without caring about sparse vs. dense layout.
*
* @tparam F Callable with signature `void(SHAMapHash const&)`.
* @param f Callback invoked once per branch in branch-index order.
*/
template <class F>
void
iterChildren(F&& f) const;
/** Call the `f` callback for all non-empty branches.
@param f a two parameter callback function. The first parameter is
the branch number, the second parameter is the index into the array.
For dense formats these are the same, for sparse they may be
different.
*/
/** Invoke `f` for every non-empty branch, passing both the logical branch
* number and the physical array index.
*
* For a dense (16-element) layout the two values are identical. For a
* sparse layout the array index is the packed position, which differs
* from the branch number when lower-numbered branches are absent.
*
* @tparam F Callable with signature `void(int branchNum, int arrayIdx)`.
* @param f Callback invoked once per occupied branch.
*/
template <class F>
void
iterNonEmptyChildIndexes(F&& f) const;
public:
/** Construct an inner node owned by the given SHAMap copy-on-write epoch.
*
* @param cowid Copy-on-write identifier of the owning SHAMap; pass 0 for
* a shareable (read-only) node.
* @param numAllocatedChildren Initial array capacity; defaults to 2
* (smallest sparse tier). The actual allocation may be rounded up to
* the next supported tier.
*/
explicit SHAMapInnerNode(std::uint32_t cowid, std::uint8_t numAllocatedChildren = 2);
SHAMapInnerNode(SHAMapInnerNode const&) = delete;
@@ -83,87 +142,277 @@ public:
operator=(SHAMapInnerNode const&) = delete;
~SHAMapInnerNode() override;
// Needed to support intrusive weak pointers
/** Release all child `SharedPtr`s before the node's memory is reclaimed.
*
* Called by the intrusive reference-count infrastructure when the strong
* count reaches zero but weak references may still exist. Explicitly
* resets every occupied child slot so that downstream reference counts are
* decremented promptly, breaking potential reference cycles even while
* the storage itself outlives its strong references.
*/
void
partialDestructor() override;
/** Produce an independent copy of this node assigned to a new CoW epoch.
*
* Allocates a new `SHAMapInnerNode` with `cowid`, copies all hashes and
* child pointers, and right-sizes the sparse arrays to actual occupancy.
* Hashes are copied without locking (they are immutable on shared nodes);
* child pointers are copied under `lock_` to prevent races with
* concurrent `canonicalizeChild()` calls.
*
* @param cowid Copy-on-write identifier for the new node's owning map.
* @return A fully independent `SharedPtr` to the cloned node.
*/
intr_ptr::SharedPtr<SHAMapTreeNode>
clone(std::uint32_t cowid) const override;
/** @return `SHAMapNodeType::TnInner`. */
SHAMapNodeType
getType() const override
{
return SHAMapNodeType::TnInner;
}
/** @return `false` — inner nodes are never leaves. */
bool
isLeaf() const override
{
return false;
}
/** @return `true` — this is always an inner node. */
bool
isInner() const override
{
return true;
}
/** @return `true` if no branches are populated (`isBranch_ == 0`). */
bool
isEmpty() const;
/** Check whether a specific branch is unoccupied.
*
* @param m Branch index (015).
* @return `true` if branch `m` has no child.
*/
bool
isEmptyBranch(int m) const;
/** @return The number of populated branches (popcount of `isBranch_`). */
int
getBranchCount() const;
/** Return the Merkle hash committed to branch `m`.
*
* @param m Branch index (015).
* @return The child's `SHAMapHash`, or the zero hash if the branch is
* empty.
*/
SHAMapHash const&
getChildHash(int m) const;
/** Replace the child at branch `m`, resizing the sparse arrays as needed.
*
* Passing a null `child` removes the branch; passing a non-null pointer
* installs it. Zeroes the corresponding hash entry so that the next
* `updateHash()` recomputes from the pointer. Zeroes `hash_` to mark
* this node dirty.
*
* @param m Branch index (015).
* @param child New child node, or null to clear the branch.
* @note Asserts `cowid_ != 0` — the node must be CoW-owned before
* mutation. Asserts `child.get() != this` to prevent self-loops.
*/
void
setChild(int m, intr_ptr::SharedPtr<SHAMapTreeNode> child);
/** Install a child pointer into an already-occupied branch without
* resizing arrays or dirtying the hash.
*
* Used during tree construction when the branch is known to exist (e.g.,
* after `setChild` allocated the slot). Unlike `setChild`, this does not
* zero `hash_` and does not resize the arrays.
*
* @param m Branch index (015); must already be non-empty.
* @param child Non-null child pointer to install.
* @note Asserts `cowid_ != 0`, `child != null`, and that branch `m` is
* already occupied.
*/
void
shareChild(int m, intr_ptr::SharedPtr<SHAMapTreeNode> const& child);
/** Return a raw (non-owning) pointer to the child at branch `m`.
*
* Acquires the per-child bit spinlock for `m`'s physical array index,
* then reads the pointer. The returned pointer is only valid while the
* caller holds the tree in a state that prevents the node from being
* released.
*
* @param branch Branch index (015); must be non-empty.
* @return Raw pointer to the child node (never null for a non-empty branch
* that has been loaded from storage).
*/
SHAMapTreeNode*
getChildPointer(int branch);
/** Return a ref-counted pointer to the child at branch `m`.
*
* Acquires the per-child bit spinlock for the physical array index before
* copying the `SharedPtr`, ensuring the reference count is incremented
* atomically with respect to concurrent `canonicalizeChild()` calls.
*
* @param branch Branch index (015); must be non-empty.
* @return `SharedPtr` to the child node.
*/
intr_ptr::SharedPtr<SHAMapTreeNode>
getChild(int branch);
/** Deduplicate a concurrently loaded child node.
*
* When multiple threads fetch the same child from backing storage
* simultaneously, this method serializes installation under the per-child
* bit spinlock. The first caller installs `node` and gets it back; any
* subsequent caller discards its freshly-deserialized copy and receives
* the incumbent pointer instead ("winner keeps it"). The supplied node's
* hash must match the stored branch hash.
*
* @param branch Branch index (015); must be non-empty.
* @param node Freshly-loaded node whose hash equals `getChildHash(branch)`.
* @return The canonical (winning) pointer for this child slot — either
* `node` itself (if this caller won) or the pre-existing pointer.
* @note Asserts that `node->getHash() == getChildHash(branch)`.
*/
intr_ptr::SharedPtr<SHAMapTreeNode>
canonicalizeChild(int branch, intr_ptr::SharedPtr<SHAMapTreeNode> node);
// sync functions
/** Check whether the entire subtree below this node is locally complete.
*
* Returns `true` when `fullBelowGen_` equals `generation`, meaning a
* previous sync pass confirmed all descendant nodes are present in local
* storage. Because generations are monotonically increasing, a stale
* marker automatically becomes invalid on the next sync cycle.
*
* @param generation Current sync generation from `FullBelowCache`.
* @return `true` if the subtree is known to be complete for `generation`.
*/
bool
isFullBelow(std::uint32_t generation) const;
/** Mark the entire subtree below this node as locally complete.
*
* Records `gen` in `fullBelowGen_`. Subsequent calls to
* `isFullBelow(gen)` will return `true`, allowing traversal to skip this
* subtree when scanning for missing nodes.
*
* @param gen Current sync generation to record.
*/
void
setFullBelowGen(std::uint32_t gen);
/** Recompute `hash_` as SHA-512/2 of `HashPrefix::InnerNode` followed by
* all 16 child hashes (zero-hashes for empty branches).
*
* Reads hashes from the local `hashesAndChildren_` arrays; does NOT pull
* hashes from in-memory child objects. Call `updateHashDeep()` instead
* when child nodes were set via pointer without updating the hash arrays.
*/
void
updateHash() override;
/** Recalculate the hash of all children and this node. */
/** Sync child hashes from in-memory child objects then recompute this
* node's hash.
*
* For every occupied branch that has a non-null child pointer, copies
* `child->getHash()` into the local hash array, then delegates to
* `updateHash()`. Needed after batch mutations where child nodes were
* installed via pointer but the corresponding hash slots were not updated.
*/
void
updateHashDeep();
/** Serialize this node for peer-to-peer wire transmission.
*
* Chooses format based on occupancy:
* - Fewer than 12 populated branches: *compressed inner* format — each
* non-empty branch is emitted as 32 bytes of hash followed by 1 byte of
* branch index (33 bytes per child), terminated by
* `kWIRE_TYPE_COMPRESSED_INNER`.
* - 12 or more branches: *full inner* format — all 16 hashes in order
* (512 bytes), terminated by `kWIRE_TYPE_INNER`.
*
* @param s Serializer to append to.
* @note Asserts that the node is non-empty before serializing.
*/
void
serializeForWire(Serializer&) const override;
/** Serialize this node in canonical hash-input form.
*
* Prepends `HashPrefix::InnerNode` (4 bytes) then emits all 16 child
* hashes in order (512 bytes), regardless of actual occupancy. This is
* the form consumed by `updateHash()` and verified by Merkle proof
* checks.
*
* @param s Serializer to append to.
* @note Asserts that the node is non-empty before serializing.
*/
void
serializeWithPrefix(Serializer&) const override;
/** Build a human-readable description for debugging.
*
* Appends each non-empty branch number and its hash to the base-class
* string from `SHAMapTreeNode::getString`.
*
* @param id The tree address of this node, used by the base-class portion.
* @return Multiline string with one `bN = <hash>` line per occupied branch.
*/
std::string
getString(SHAMapNodeID const&) const override;
/** Verify structural invariants in debug builds.
*
* Checks that every occupied branch has a non-zero hash, that `isBranch_`
* and the hash array agree on occupancy, and (unless this is the root)
* that `hash_` is non-zero and at least one branch is occupied.
*
* @param isRoot Pass `true` when checking the tree root; relaxes the
* non-zero-hash and minimum-count assertions that do not apply to an
* empty root.
*/
void
invariants(bool isRoot = false) const override;
/** Deserialize an inner node from the *full inner* wire format.
*
* Expects exactly `kBRANCH_FACTOR * 32` bytes: 16 consecutive 256-bit
* hashes. After parsing, right-sizes the sparse arrays to actual
* occupancy via `resizeChildArrays()`.
*
* @param data Raw bytes in full-inner format.
* @param hash Expected Merkle hash of this node.
* @param hashValid If `true`, assign `hash` directly; if `false`,
* recompute via `updateHash()`.
* @return A `SharedPtr<SHAMapTreeNode>` to the new inner node.
* @throws std::runtime_error if `data.size()` is not exactly 512 bytes.
*/
static intr_ptr::SharedPtr<SHAMapTreeNode>
makeFullInner(Slice data, SHAMapHash const& hash, bool hashValid);
/** Deserialize an inner node from the *compressed inner* wire format.
*
* Expects a sequence of 33-byte chunks: 32 bytes of hash followed by
* 1 byte of branch index. After parsing, right-sizes the sparse arrays
* and recomputes the hash via `updateHash()`.
*
* @param data Raw bytes in compressed-inner format; size must be a
* non-zero multiple of 33 and at most `33 * kBRANCH_FACTOR`.
* @return A `SharedPtr<SHAMapTreeNode>` to the new inner node.
* @throws std::runtime_error if the size is invalid or a branch index
* is ≥ `kBRANCH_FACTOR`.
*/
static intr_ptr::SharedPtr<SHAMapTreeNode>
makeCompressedInner(Slice data);
};

View File

@@ -1,3 +1,10 @@
/** @file
* Defines `SHAMapItem`, the immutable, slab-allocated payload leaf of the
* XRP Ledger's SHAMap Merkle-Patricia trie, together with its slab allocator
* pool (`detail::gSlabber`), `boost::intrusive_ptr` lifetime hooks, and the
* `makeShamapitem` factory functions.
*/
#pragma once
#include <xrpl/basics/ByteUtilities.h>
@@ -11,19 +18,36 @@
namespace xrpl {
// an item stored in a SHAMap
/** Immutable, slab-allocated payload item stored at the leaves of a SHAMap.
*
* Each `SHAMapItem` pairs a 256-bit trie key (`tag_`) with an opaque
* variable-length byte payload. The payload is stored via the struct-hack:
* it occupies the memory immediately after the fixed-size struct fields in
* the same allocation, avoiding a second heap allocation and keeping the
* header and payload in one contiguous block.
*
* Objects are always managed through `boost::intrusive_ptr<SHAMapItem const>`
* using an embedded atomic reference count. Once constructed the key and
* payload are immutable; the SHAMap copy-on-write protocol creates a new
* item rather than mutating an existing one, allowing the same item to be
* shared across CoW snapshots safely.
*
* The only valid construction path is `makeShamapitem()`. The default,
* copy, and move constructors are all deleted. `CountedObject<SHAMapItem>`
* maintains a global live-object counter for diagnostics.
*
* @note Payload size is limited to 16 MiB (asserted in `makeShamapitem`).
* The struct must have `alignof` of 4 or 8 (enforced by `static_assert`)
* to satisfy the slab allocator's alignment contract.
*/
class SHAMapItem : public CountedObject<SHAMapItem>
{
// These are used to support boost::intrusive_ptr reference counting
// These functions are used internally by boost::intrusive_ptr to handle
// lifetime management.
friend void
intrusive_ptr_add_ref(SHAMapItem const* x);
friend void
intrusive_ptr_release(SHAMapItem const* x);
// This is the interface for creating new instances of this class.
friend boost::intrusive_ptr<SHAMapItem>
makeShamapitem(uint256 const& tag, Slice data);
@@ -35,13 +59,17 @@ private:
// is safe.
std::uint32_t const size_;
// This is the reference count used to support boost::intrusive_ptr
// Embedded reference count for boost::intrusive_ptr. Initialised to 1 so
// makeShamapitem can pass `false` to suppress the automatic increment that
// would otherwise bring the count to 2.
mutable std::atomic<std::uint32_t> refcount_ = 1;
// Because of the unusual way in which SHAMapItem objects are constructed
// the only way to properly create one is to first allocate enough memory
// so we limit this constructor to codepaths that do this right and limit
// arbitrary construction.
/** Placement-new constructor; must only be called by `makeShamapitem`.
*
* Copies `data` into the memory immediately following the struct fields.
* The caller is responsible for pre-allocating `sizeof(SHAMapItem) +
* data.size()` bytes before invoking this via placement new.
*/
SHAMapItem(uint256 const& tag, Slice data)
: tag_(tag), size_(static_cast<std::uint32_t>(data.size()))
{
@@ -62,24 +90,33 @@ public:
SHAMapItem&
operator=(SHAMapItem&&) = delete;
/** Return the 256-bit trie key that identifies this item in the SHAMap. */
uint256 const&
key() const
{
return tag_;
}
/** Return the size of the payload in bytes. */
std::size_t
size() const
{
return size_;
}
/** Return a pointer to the first byte of the payload.
*
* The payload is stored immediately after the struct in the same
* allocation (struct-hack layout). The pointer is valid for the
* lifetime of this object.
*/
void const*
data() const
{
return reinterpret_cast<std::uint8_t const*>(this) + sizeof(*this);
}
/** Return the payload as a `Slice` (pointer + length view). */
Slice
slice() const
{
@@ -90,9 +127,17 @@ public:
namespace detail {
// clang-format off
// The slab cutoffs and the number of megabytes per allocation are customized
// based on the number of objects of each size we expect to need at any point
// in time and with an eye to minimize the number of slack bytes in a block.
/** Slab allocator pool backing all `SHAMapItem` allocations.
*
* Seven size tiers cover payloads of up to 1052 extra bytes (added to
* `sizeof(SHAMapItem)`). Tier cutoffs and pool sizes are tuned to the
* empirical distribution of ledger-object sizes and minimise intra-block
* padding. Each backing block is allocated at a 2 MiB boundary to allow
* Linux transparent huge-page (THP) support to engage automatically.
*
* Payloads exceeding the largest tier are served by a plain
* `new uint8_t[]` fallback in `makeShamapitem`.
*/
inline SlabAllocatorSet<SHAMapItem> gSlabber({
{ 128, megabytes(std::size_t(60)) },
{ 192, megabytes(std::size_t(46)) },
@@ -106,15 +151,39 @@ inline SlabAllocatorSet<SHAMapItem> gSlabber({
} // namespace detail
/** Increment the reference count of a `SHAMapItem`.
*
* Called automatically by `boost::intrusive_ptr` when a new owning pointer
* is created. Guards against resurrection: if the count is already zero when
* the increment is attempted — indicating another thread concurrently released
* the last reference — `logicError` is called rather than silently
* resurrecting a dead object.
*
* @param x The item whose reference count should be incremented.
*/
inline void
intrusive_ptr_add_ref(SHAMapItem const* x)
{
// This can only happen if someone releases the last reference to the
// item while we were trying to increment the refcount.
if (x->refcount_++ == 0)
logicError("SHAMapItem: the reference count is 0!");
}
/** Decrement the reference count of a `SHAMapItem`, destroying it when zero.
*
* Called automatically by `boost::intrusive_ptr` when an owning pointer is
* destroyed or reset. On reaching zero, performs a two-phase teardown
* required by the struct-hack layout:
*
* 1. Explicitly calls `std::destroy_at` to run the `SHAMapItem` destructor
* (needed because `CountedObject`'s destructor decrements a global
* diagnostic counter and is not trivial). The `if constexpr` guard
* eliminates this call if the destructor chain ever becomes trivial.
* 2. Returns the raw memory to `detail::gSlabber`. If the pointer was not
* slab-managed (allocated via the `new uint8_t[]` fallback), `deallocate`
* returns `false` and the memory is freed with `delete[]`.
*
* @param x The item whose reference count should be decremented.
*/
inline void
intrusive_ptr_release(SHAMapItem const* x)
{
@@ -122,7 +191,7 @@ intrusive_ptr_release(SHAMapItem const* x)
{
auto p = reinterpret_cast<std::uint8_t const*>(x);
// The SHAMapItem constructor isn't trivial (because the destructor
// The SHAMapItem destructor isn't trivial (because the destructor
// for CountedObject isn't) so we can't avoid calling it here, but
// plan for a future where we might not need to.
if constexpr (!std::is_trivially_destructible_v<SHAMapItem>)
@@ -135,6 +204,21 @@ intrusive_ptr_release(SHAMapItem const* x)
}
}
/** Allocate and construct a new `SHAMapItem`.
*
* The sole factory for `SHAMapItem` objects. Requests a slot from
* `detail::gSlabber` sized for `sizeof(SHAMapItem) + data.size()`, falling
* back to `new uint8_t[]` when no slab tier fits. The item is constructed
* in-place via placement new; the returned `intrusive_ptr` is initialised
* with `false` for the second argument to suppress the automatic reference
* increment — the constructor already sets `refcount_` to 1.
*
* @param tag The 256-bit key that identifies this item in the SHAMap trie.
* @param data The payload bytes to store; copied into the allocation.
* @return An owning `intrusive_ptr` to the newly created item with a
* reference count of 1.
* @note Asserts that `data.size() <= 16 MiB`.
*/
inline boost::intrusive_ptr<SHAMapItem>
makeShamapitem(uint256 const& tag, Slice data)
{
@@ -159,6 +243,15 @@ makeShamapitem(uint256 const& tag, Slice data)
static_assert(alignof(SHAMapItem) != 40);
static_assert(alignof(SHAMapItem) == 8 || alignof(SHAMapItem) == 4);
/** Produce a freshly allocated copy of an existing `SHAMapItem`.
*
* Equivalent to `makeShamapitem(other.key(), other.slice())`. Provides the
* copy semantics that the class itself refuses to expose, yielding an
* independently owned item with the same key and payload.
*
* @param other The item to copy.
* @return An owning `intrusive_ptr` to the newly allocated copy.
*/
inline boost::intrusive_ptr<SHAMapItem>
makeShamapitem(SHAMapItem const& other)
{

View File

@@ -7,13 +7,65 @@
namespace xrpl {
/** Abstract base class for all SHAMap leaf nodes.
*
* Provides shared item storage, copy-on-write mutation, and identity queries
* for the three concrete leaf types: `SHAMapAccountStateLeafNode`,
* `SHAMapTxLeafNode`, and `SHAMapTxPlusMetaLeafNode`. Each concrete subclass
* supplies its own `updateHash()`, `serializeForWire()`, and
* `serializeWithPrefix()` implementations reflecting the distinct hash
* formulas and wire formats of account state, bare transactions, and
* transaction-plus-metadata respectively.
*
* Copy construction and copy assignment are deleted; duplication is always
* explicit via the virtual `clone(cowid)` method, which produces an owned
* copy ready for mutation under copy-on-write semantics.
*
* @note `item_` is `protected` rather than `private` so that concrete
* subclasses can access it directly in their inline `updateHash()` and
* `serialize*()` implementations, avoiding virtual-dispatch overhead in
* the hot hash-recomputation path.
*
* @see SHAMapAccountStateLeafNode
* @see SHAMapTxLeafNode
* @see SHAMapTxPlusMetaLeafNode
*/
class SHAMapLeafNode : public SHAMapTreeNode
{
protected:
/** The keyed payload carried by this leaf.
*
* Deliberately `const`-qualified through the pointer: the item itself is
* immutable once created and may be shared safely across CoW snapshots.
* `setItem()` replaces this pointer; it never mutates the referent.
*/
boost::intrusive_ptr<SHAMapItem const> item_;
/** Construct a leaf and defer hash computation to the concrete subclass.
*
* Each concrete subclass calls `updateHash()` immediately after invoking
* this constructor so the node is valid for insertion into the trie.
* Asserts `item->size() >= 12`; any well-formed XRPL serialized object is
* at least 12 bytes — shorter data indicates corruption or a caller error.
*
* @param item Non-null payload for the new leaf.
* @param cowid Copy-on-write owner ID of the creating `SHAMap` instance.
*/
SHAMapLeafNode(boost::intrusive_ptr<SHAMapItem const> item, std::uint32_t cowid);
/** Construct a leaf with a pre-computed hash.
*
* Used by `clone()` and deserialization paths where the hash is already
* known, avoiding a redundant SHA-512 computation. The caller must ensure
* `hash` is consistent with `item`'s current content or the Merkle tree
* will be silently corrupted.
* Asserts `item->size() >= 12`.
*
* @param item Non-null payload for the new leaf.
* @param cowid Copy-on-write owner ID for the new node.
* @param hash Pre-computed hash of the leaf; passed straight through to
* `SHAMapTreeNode` without recomputation.
*/
SHAMapLeafNode(
boost::intrusive_ptr<SHAMapItem const> item,
std::uint32_t cowid,
@@ -24,34 +76,68 @@ public:
SHAMapLeafNode&
operator=(SHAMapLeafNode const&) = delete;
/** Returns `true`; sealed here so all concrete leaf subclasses inherit it. */
bool
isLeaf() const final
{
return true;
}
/** Returns `false`; sealed here so all concrete leaf subclasses inherit it. */
bool
isInner() const final
{
return false;
}
/** Assert the node's fundamental invariants.
*
* Checks that `hash_` is non-zero and `item_` is non-null. The `isRoot`
* argument is meaningful only for inner nodes and is silently ignored here.
*
* @param isRoot Ignored for leaf nodes.
*/
void
invariants(bool isRoot = false) const final;
public:
/** Return a non-owning reference to the stored item pointer.
*
* Gives callers access to the item's key and payload without transferring
* ownership or bumping the reference count. The "peek" convention signals
* zero-cost, non-owning access throughout the XRPL codebase.
*
* @return A `const` reference to the `intrusive_ptr` holding the leaf's
* item. The reference is valid for the lifetime of this node.
*/
boost::intrusive_ptr<SHAMapItem const> const&
peekItem() const;
/** Set the item that this node points to and update the node's hash.
@param i the new item
@return false if the change was, effectively, a noop (that is, if the
hash was unchanged); true otherwise.
/** Replace the stored item and recompute this node's hash.
*
* Asserts that `cowid_` is non-zero — the node must be exclusively owned
* by a `SHAMap` instance (i.e. previously unshared via `clone()`) before
* it may be mutated. After swapping the item, `updateHash()` is called;
* `dirtyUp()` in the caller is only necessary when this returns `true`.
*
* @param i The replacement item; must share the same key as the current
* item (cross-key swaps corrupt the trie position).
* @return `true` if the hash changed (the effective content of the leaf
* was modified); `false` if the new item produces the same hash as the
* old one, indicating a no-op update.
*/
bool
setItem(boost::intrusive_ptr<SHAMapItem const> i);
/** Format this leaf's identity and payload summary as a human-readable string.
*
* Appends the node type tag (txn, txn+md, or as), the item key, the hash,
* and the item size to the base class string produced by
* `SHAMapTreeNode::getString()`. Intended for debugging and logging only.
*
* @param id The `SHAMapNodeID` identifying this node's trie position.
* @return A multi-line string describing the node.
*/
std::string
getString(SHAMapNodeID const&) const final;
};

View File

@@ -1,3 +1,8 @@
/** @file
* Defines the `SHAMapType` enum and the `SHAMapMissingNode` exception,
* forming the error-signalling contract for the SHAMap subsystem.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
@@ -10,12 +15,32 @@
namespace xrpl {
/** Classifies the role of a SHAMap within the ledger.
*
* Every closed ledger contains two SHAMaps: a `TRANSACTION` tree holding the
* set of transactions included in that ledger and a `STATE` tree holding the
* full account-state database. `FREE` trees are ephemeral and not tied to a
* finalized ledger (e.g., consensus transaction sets).
*
* @note The underlying integer values appear in wire-protocol serialization
* for sync packets and must not be changed.
*/
enum class SHAMapType {
TRANSACTION = 1, // A tree of transactions
STATE = 2, // A tree of state nodes
FREE = 3, // A tree not part of a ledger
TRANSACTION = 1, /**< A tree of transactions included in a ledger. */
STATE = 2, /**< A tree of account-state objects at a ledger's close. */
FREE = 3, /**< An ephemeral tree not associated with a finalized ledger. */
};
/** Convert a `SHAMapType` to a human-readable label for logging.
*
* Returns `"Transaction Tree"`, `"State Tree"`, or `"Free Tree"` for the
* three defined enumerators. For any out-of-range value the numeric
* underlying integer is returned as a string rather than producing
* undefined behavior.
*
* @param t The tree type to convert.
* @return A string label suitable for log messages.
*/
inline std::string
to_string(SHAMapType t)
{
@@ -32,14 +57,53 @@ to_string(SHAMapType t)
}
}
/** Exception thrown when SHAMap traversal encounters a locally absent node.
*
* This is a routine operational condition in a distributed ledger node:
* partially-synced or historical ledgers may have gaps in local storage.
* The exception propagates upward through the traversal call stack so that
* each subsystem can apply its own recovery policy:
*
* - `LedgerCleaner` and `LedgerMaster` catch it at `warn` level and schedule
* a peer fetch via `getInboundLedgers().acquire()`.
* - `RCLConsensus` treats it as fatal during consensus processing, logging at
* `error` level and re-throwing.
* - `Ledger.cpp` catches it silently and treats the incomplete tree as an
* invalid ledger.
*
* The `what()` message is built eagerly at construction time since it is the
* primary data surface available to catch sites, all of which log it directly.
*
* @see SHAMapType
*/
class SHAMapMissingNode : public std::runtime_error
{
public:
/** Construct with the hash of the absent node.
*
* Used when the tree descends toward a child whose hash is known (stored
* in the parent inner node) but whose backing data is not in local storage.
* Produces the message `"Missing Node: <tree type>: hash <hex>"`.
*
* @param t The type of tree in which the node was not found.
* @param hash The SHA-512Half digest identifying the absent node.
*/
SHAMapMissingNode(SHAMapType t, SHAMapHash const& hash)
: std::runtime_error("Missing Node: " + to_string(t) + ": hash " + to_string(hash))
{
}
/** Construct with the item key that could not be located.
*
* Used when the failure is expressed at the level of a leaf identifier
* (e.g., an account ID or transaction ID) rather than a structural node
* hash — typically when a key-based lookup descends far enough to
* determine the leaf should exist but cannot find it.
* Produces the message `"Missing Node: <tree type>: id <hex>"`.
*
* @param t The type of tree in which the item was not found.
* @param id The 256-bit key of the item that could not be located.
*/
SHAMapMissingNode(SHAMapType t, uint256 const& id)
: std::runtime_error("Missing Node: " + to_string(t) + ": id " + to_string(id))
{

View File

@@ -1,3 +1,12 @@
/** @file
* Defines `SHAMapNodeID`, the (depth, masked-prefix) address type used to
* locate nodes within a SHAMap 16-way radix-Merkle trie.
*
* Every traversal, sync, and proof operation in the SHAMap subsystem
* carries a `SHAMapNodeID` alongside node pointers so the tree position
* is always unambiguous — independent of node content.
*/
#pragma once
#include <xrpl/basics/CountedObject.h>
@@ -9,7 +18,21 @@
namespace xrpl {
/** Identifies a node inside a SHAMap */
/** Encodes the position of a node within a SHAMap as a (depth, prefix) pair.
*
* A SHAMap has 65 levels (depth 0 = root, depth 64 = leaf level). Each
* level consumes one nibble (4 bits) of the 256-bit key, giving a branch
* factor of 16. `SHAMapNodeID` captures "which node" rather than "which
* data": two nodes with the same (depth, masked prefix) are at the same
* tree position regardless of their content or hash.
*
* @invariant `id_` carries non-zero bits only in its top `depth_` nibbles;
* all lower nibbles are zero. The constructor asserts this property.
* Use `createID()` when constructing from an unmasked leaf key.
*
* Live instance counts are tracked via `CountedObject` for diagnostic
* purposes and can be queried through `CountedObjects::getInstance()`.
*/
class SHAMapNodeID : public CountedObject<SHAMapNodeID>
{
private:
@@ -19,6 +42,17 @@ private:
public:
SHAMapNodeID() = default;
SHAMapNodeID(SHAMapNodeID const& other) = default;
/** Construct a node ID at the given depth with a pre-masked prefix.
*
* The caller is responsible for supplying a correctly masked `hash`:
* only the top `depth` nibbles may be non-zero. Violating this
* invariant triggers an assertion. Prefer `createID()` when
* constructing from an unmasked leaf key.
*
* @param depth Tree level, 0 (root) through `SHAMap::kLEAF_DEPTH` (64).
* @param hash 256-bit path prefix, masked to `depth` nibbles.
*/
SHAMapNodeID(unsigned int depth, uint256 const& hash);
SHAMapNodeID&
@@ -30,7 +64,14 @@ public:
return depth_ == 0;
}
// Get the wire format (256-bit nodeID, 1-byte depth)
/** Serialize this node ID to its 33-byte wire representation.
*
* Produces a 33-byte string: the 32-byte big-endian `id_` prefix
* followed by a single byte containing `depth_`. Used when
* exchanging node positions with peers during tree synchronization.
*
* @return A 33-byte string containing the wire-format node ID.
*/
[[nodiscard]] std::string
getRawString() const;
@@ -46,28 +87,53 @@ public:
return id_;
}
/** Return the node ID of the child at branch `m`.
*
* Increments `depth_` by one and sets the nibble at the new depth
* to `m`, producing the canonical address of that child in the tree.
* This is the step primitive used by every traversal loop in SHAMap.
*
* @param m Branch index, 015.
* @return The child's `SHAMapNodeID` at `depth_ + 1`.
* @throws `std::logic_error` if called on a leaf-depth node
* (`depth_ >= SHAMap::kLEAF_DEPTH`), because a depth-65
* node would violate the tree's structural invariant.
*/
[[nodiscard]] SHAMapNodeID
getChildNodeID(unsigned int m) const;
/**
* Create a SHAMapNodeID of a node with the depth of the node and
* the key of a leaf
/** Derive the ancestor node ID at `depth` from a full leaf key.
*
* @param depth the depth of the node
* @param key the key of a leaf
* @return SHAMapNodeID of the node
* Applies the depth mask to `key`, discarding the lower nibbles,
* and constructs the resulting `SHAMapNodeID`. This is the correct
* factory to use when you have a leaf key and need the address of an
* intermediate ancestor — for example, during sync validation where
* `addKnownNode` reconstructs the expected position of a received
* leaf and compares it against the claimed `SHAMapNodeID`.
*
* @param depth Tree level of the desired ancestor, 064.
* @param key Full 256-bit leaf key; lower nibbles are masked away.
* @return The `SHAMapNodeID` at `depth` on the path to `key`.
*/
static SHAMapNodeID
createID(int depth, uint256 const& key);
// FIXME-C++20: use spaceship and operator synthesis
/** Comparison operators */
/** Compare node IDs lexicographically by (depth, prefix).
*
* Shallower nodes (smaller `depth_`) sort before deeper ones; among
* equal depths, ordering follows the 256-bit prefix value. This
* ordering is used when storing node IDs in `std::map` or `std::set`
* containers that track traversal or sync frontiers.
*/
bool
operator<(SHAMapNodeID const& n) const
{
return std::tie(depth_, id_) < std::tie(n.depth_, n.id_);
}
/** @{ */
bool
operator>(SHAMapNodeID const& n) const
{
@@ -97,8 +163,18 @@ public:
{
return !(*this == n);
}
/** @} */
};
/** Format a node ID as a human-readable string for logging.
*
* Returns `"NodeID(root)"` for the root node (depth 0) or
* `"NodeID(<depth>,<hex_id>)"` for all other nodes. This format
* appears in journal messages throughout the SHAMap traversal code.
*
* @param node The node ID to format.
* @return A human-readable string representation.
*/
inline std::string
to_string(SHAMapNodeID const& node)
{
@@ -108,32 +184,51 @@ to_string(SHAMapNodeID const& node)
return "NodeID(" + std::to_string(node.getDepth()) + "," + to_string(node.getNodeID()) + ")";
}
/** Write a node ID to an output stream using `to_string()` format. */
inline std::ostream&
operator<<(std::ostream& out, SHAMapNodeID const& node)
{
return out << to_string(node);
}
/** Return an object representing a serialized SHAMap Node ID
/** Deserialize a `SHAMapNodeID` from a 33-byte wire buffer.
*
* @param s A string of bytes
* @param data a non-null pointer to a buffer of @param size bytes.
* @param size the size, in bytes, of the buffer pointed to by @param data.
* @return A seated optional if the buffer contained a serialized SHAMap
* node ID and an unseated optional otherwise.
* Validates that `size` is exactly 33, that the depth byte (at offset 32)
* does not exceed `SHAMap::kLEAF_DEPTH` (64), and that the 32-byte prefix
* satisfies the depth-mask invariant. Any violation yields an empty
* optional rather than an exception, making this safe to call on
* untrusted peer data.
*
* @param data Pointer to the raw byte buffer.
* @param size Length of the buffer in bytes; must equal 33 for success.
* @return The decoded `SHAMapNodeID`, or an empty optional on any
* validation failure.
*/
/** @{ */
[[nodiscard]] std::optional<SHAMapNodeID>
deserializeSHAMapNodeID(void const* data, std::size_t size);
/** @cond */
[[nodiscard]] inline std::optional<SHAMapNodeID>
deserializeSHAMapNodeID(std::string const& s)
{
return deserializeSHAMapNodeID(s.data(), s.size());
}
/** @endcond */
/** @} */
/** Returns the branch that would contain the given hash */
/** Return the branch index (015) to follow from `id` toward `hash`.
*
* Extracts the nibble of `hash` at depth `id.getDepth()`, which is the
* child branch number to descend into at that level. This is the core
* traversal primitive: every lookup, insert, delete, and sync walk in
* SHAMap calls `selectBranch` to advance one level down the trie.
*
* @param id Position of the current node; its depth determines which
* nibble of `hash` to read.
* @param hash The 256-bit key being navigated toward.
* @return Branch index in [0, 15].
*/
[[nodiscard]] unsigned int
selectBranch(SHAMapNodeID const& id, uint256 const& hash);

View File

@@ -4,9 +4,34 @@
#include <optional>
/** Callback for filtering SHAMap during sync. */
namespace xrpl {
/** Abstract callback interface connecting SHAMap sync traversal to node
* persistence and transient caching infrastructure.
*
* The SHAMap engine is decoupled from knowledge about where nodes come from
* or where they should go. `SHAMapSyncFilter` provides exactly two hooks that
* let the application layer supply that knowledge:
*
* - `getNode()` — pull: called when a node is needed but absent from the
* in-memory cache and backing database. The filter may satisfy the request
* from a transient source such as a peer fetch pack or consensus cache.
*
* - `gotNode()` — notify: called after a node has been successfully obtained
* and deserialized, regardless of source. The filter uses this to persist
* nodes received from the network.
*
* The two-method split keeps filter implementations small: they operate purely
* on flat `Blob` data keyed by `SHAMapHash`; all deserialization and tree
* structure are handled by the SHAMap engine.
*
* Non-copyable because implementations hold non-owning references to
* databases and caches with independent lifetimes.
*
* @see AccountStateSF
* @see TransactionStateSF
* @see ConsensusTransSetSF
*/
class SHAMapSyncFilter
{
public:
@@ -16,7 +41,30 @@ public:
SHAMapSyncFilter&
operator=(SHAMapSyncFilter const&) = delete;
// Note that the nodeData is overwritten by this call
/** Notify the filter that a node has been successfully obtained and
* integrated into the map.
*
* Called by `addRootNode` and `addKnownNode` (with `fromFilter = false`)
* after a peer-supplied node passes hash and position validation, and by
* the internal `checkFilter` helper (with `fromFilter = true`) after a
* node supplied by this filter's own `getNode()` has been deserialized.
*
* Implementations use `fromFilter` to avoid redundant writes: when
* `fromFilter` is `true` the data originated here and is already known
* locally, so re-storing it is unnecessary. When `fromFilter` is `false`
* the node arrived from the network and should be persisted durably.
*
* @param fromFilter `true` if the data was originally returned by this
* filter's `getNode()` call; `false` if it arrived from a peer.
* @param nodeHash Hash of the node that was received.
* @param ledgerSeq Sequence number of the ledger this node belongs to,
* allowing the filter to associate stored nodes with a specific ledger
* for expiry or prioritization decisions.
* @param nodeData Serialized node bytes. Passed by rvalue reference;
* the implementation may move or destroy the buffer — callers must
* not rely on its contents after this call returns.
* @param type Wire type of the received node.
*/
virtual void
gotNode(
bool fromFilter,
@@ -25,6 +73,21 @@ public:
Blob&& nodeData,
SHAMapNodeType type) const = 0;
/** Attempt to retrieve a node from a transient source.
*
* Called by the SHAMap engine when a node identified by `nodeHash` is
* not present in the in-memory cache or backing NodeStore. The filter
* may satisfy the request from an ephemeral store such as a peer fetch
* pack or consensus transaction cache, avoiding a network round-trip.
*
* If data is returned, the engine will immediately call
* `gotNode(true, nodeHash, ...)` to notify the filter that the node was
* consumed.
*
* @param nodeHash Hash of the node the engine is looking for.
* @return The raw serialized node bytes if available, or `std::nullopt`
* if the filter cannot supply the node.
*/
[[nodiscard]] virtual std::optional<Blob>
getNode(SHAMapHash const& nodeHash) const = 0;
};

View File

@@ -1,3 +1,12 @@
/** @file
* Base class for all nodes in the SHAMap authenticated Merkle radix tree,
* plus the wire-protocol type tags and the in-memory node-type enumeration.
*
* Every node stored in a `SHAMap` — branching inner node or data-carrying
* leaf — derives from `SHAMapTreeNode`. This file is therefore the single
* authoritative location for the tree's on-disk and on-wire node format.
*/
#pragma once
#include <xrpl/basics/IntrusivePointer.h>
@@ -12,44 +21,111 @@
namespace xrpl {
// These are wire-protocol identifiers used during serialization to encode the
// type of a node. They should not be arbitrarily be changed.
/** Wire-protocol type tag for a bare transaction node (no metadata).
*
* Appended as the final byte of a serialized node payload during peer-to-peer
* sync. Part of the XRPL wire protocol — do not change.
*/
static constexpr unsigned char const kWIRE_TYPE_TRANSACTION = 0;
/** Wire-protocol type tag for an account-state (ledger object) node.
*
* Part of the XRPL wire protocol — do not change.
*/
static constexpr unsigned char const kWIRE_TYPE_ACCOUNT_STATE = 1;
/** Wire-protocol type tag for a full inner node (all 16 hashes emitted).
*
* Part of the XRPL wire protocol — do not change.
*/
static constexpr unsigned char const kWIRE_TYPE_INNER = 2;
/** Wire-protocol type tag for a compressed inner node (sparse hash encoding).
*
* Part of the XRPL wire protocol — do not change.
*/
static constexpr unsigned char const kWIRE_TYPE_COMPRESSED_INNER = 3;
/** Wire-protocol type tag for a transaction node that includes metadata.
*
* Part of the XRPL wire protocol — do not change.
*/
static constexpr unsigned char const kWIRE_TYPE_TRANSACTION_WITH_META = 4;
/** In-memory classification of a SHAMap tree node.
*
* Used for runtime dispatch and to select the correct hash prefix and wire
* format. Note that these values are distinct from the `kWIRE_TYPE_*`
* constants: the wire-type byte is appended to the serialized payload,
* while `SHAMapNodeType` lives only in memory.
*/
enum class SHAMapNodeType {
TnInner = 1,
TnTransactionNm = 2, // transaction, no metadata
TnTransactionMd = 3, // transaction, with metadata
TnAccountState = 4
TnTransactionNm = 2, /**< Transaction leaf without metadata. */
TnTransactionMd = 3, /**< Transaction leaf with metadata. */
TnAccountState = 4 /**< Account-state (ledger object) leaf. */
};
/** Polymorphic base for all nodes in the SHAMap authenticated Merkle radix tree.
*
* Concrete subtypes are `SHAMapInnerNode` (the 16-way branching node) and the
* three leaf types: `SHAMapTxLeafNode`, `SHAMapTxPlusMetaLeafNode`, and
* `SHAMapAccountStateLeafNode`. All share the copy-on-write ownership model
* encoded in `cowid_` and the two serialization contracts defined here.
*
* Nodes can be shared across multiple `SHAMap` instances simultaneously when
* `cowid_ == 0`. A map that needs to mutate a shared node must first call
* `clone()` to produce a private copy, then mutate only the clone.
*
* Copy and move are deleted: duplication must always go through `clone()`.
*
* @note This class inherits from `IntrusiveRefCounts`, which packs 16-bit
* strong count, 14-bit weak count, and two lifecycle-state bits into a
* single 32-bit atomic. Per-node overhead is intentionally minimal because
* a full ledger tree can contain millions of nodes.
* @see SHAMapInnerNode for the branching node implementation.
* @see SHAMapLeafNode for the abstract leaf base.
*/
class SHAMapTreeNode : public IntrusiveRefCounts
{
protected:
/** Cached Merkle hash of this node.
*
* Zero when the node is dirty (after a mutation, before `updateHash()` is
* called). `serializeWithPrefix` feeds this value into the parent's hash
* computation.
*/
SHAMapHash hash_;
/** Determines the owning SHAMap, if any. Used for copy-on-write semantics.
If this value is 0, the node is not dirty and does not need to be
flushed. It is eligible for sharing and may be included multiple
SHAMap instances.
/** Identifies which `SHAMap` instance exclusively owns this node.
*
* A zero value means the node is clean and eligible for sharing across
* multiple `SHAMap` instances simultaneously. A non-zero value is the
* `cowid` of the one map that owns and may mutate this node.
*
* `unshare()` resets this to zero, making the node shareable again (called
* during flush, after which the node is effectively immutable).
*/
std::uint32_t cowid_;
/** Construct a node
@param cowid The identifier of a SHAMap. For more, see #cowid_
@param hash The hash associated with this node, if any.
*/
/** @{ */
/** Construct a node with the given CoW owner and an unset hash.
*
* @param cowid Copy-on-write identifier of the owning `SHAMap`; pass 0
* for a shareable (read-only) node.
*/
explicit SHAMapTreeNode(std::uint32_t cowid) noexcept : cowid_(cowid)
{
}
/** Construct a node with the given CoW owner and a pre-computed hash.
*
* Used when deserializing from storage where the hash is already known,
* avoiding a redundant `updateHash()` call.
*
* @param cowid Copy-on-write identifier of the owning `SHAMap`.
* @param hash Pre-validated Merkle hash for this node.
*/
explicit SHAMapTreeNode(std::uint32_t cowid, SHAMapHash const& hash) noexcept
: hash_(hash), cowid_(cowid)
{
@@ -63,32 +139,28 @@ public:
SHAMapTreeNode&
operator=(SHAMapTreeNode const&) = delete;
// Needed to support weak intrusive pointers
/** Release expensive sub-resources while weak references still exist.
*
* Called by the `IntrusiveRefCounts` infrastructure when the strong
* reference count reaches zero but at least one weak reference remains.
* The default implementation is a no-op; `SHAMapInnerNode` overrides it
* to release all 16 child `SharedPtr`s promptly, avoiding a cascade of
* memory retention through the child tree while weak pointers keep this
* node's storage alive.
*
* @note Callers must invoke `partialDestructorFinished()` after this
* returns, per the `IntrusiveRefCounts` contract.
*/
virtual void
partialDestructor() {};
/** \defgroup SHAMap Copy-on-Write Support
By nature, a node may appear in multiple SHAMap instances. Rather
than actually duplicating these nodes, SHAMap opts to be memory
efficient and uses copy-on-write semantics for nodes.
Only nodes that are not modified and don't need to be flushed back
can be shared. Once a node needs to be changed, it must first be
copied and the copy must marked as not shareable.
Note that just because a node may not be *owned* by a given SHAMap
instance does not mean that the node is NOT a part of any SHAMap. It
only means that the node is not owned exclusively by any one SHAMap.
For more on copy-on-write, check out:
https://en.wikipedia.org/wiki/Copy-on-write
*/
/** @{ */
/** Returns the SHAMap that owns this node.
@return the ID of the SHAMap that owns this node, or 0 if the
node is not owned by any SHAMap and is a candidate for sharing.
/** Return the copy-on-write identifier of the owning `SHAMap`.
*
* A return value of 0 means the node is unowned and eligible for sharing
* across multiple `SHAMap` instances. A non-zero value identifies the one
* map that may mutate this node.
*
* @return The owning map's CoW ID, or 0 if the node is shareable.
*/
std::uint32_t
cowid() const
@@ -96,10 +168,14 @@ public:
return cowid_;
}
/** If this node is shared with another map, mark it as no longer shared.
Only nodes that are not modified and do not need to be flushed back
should be marked as unshared.
/** Mark this node as shareable by clearing its CoW ownership.
*
* Sets `cowid_` to 0, making the node eligible for inclusion in multiple
* `SHAMap` snapshots simultaneously. Called during flush, after which the
* node is written to backing storage and must not be further mutated.
*
* @note Only call this on nodes that are clean (do not need to be flushed)
* or have already been persisted.
*/
void
unshare()
@@ -107,61 +183,180 @@ public:
cowid_ = 0;
}
/** Make a copy of this node, setting the owner. */
/** Produce a deep copy of this node assigned to a new CoW epoch.
*
* The returned node carries `cowid` as its owner, allowing the caller's
* `SHAMap` to mutate it independently of any other maps that still hold
* references to the original. The original node is left intact.
*
* @param cowid Copy-on-write identifier of the map that will own the clone.
* @return A `SharedPtr` to the new, independently-owned copy.
*/
virtual intr_ptr::SharedPtr<SHAMapTreeNode>
clone(std::uint32_t cowid) const = 0;
/** @} */
/** Recalculate the hash of this node. */
/** Recompute `hash_` from the node's current contents.
*
* Each concrete subclass feeds the appropriate `HashPrefix` constant and
* payload into SHA-512/2. Must be called after any mutation before the
* node's hash is read by its parent.
*/
virtual void
updateHash() = 0;
/** Return the hash of this node. */
/** Return the cached Merkle hash of this node.
*
* The hash is zero if the node is dirty (mutated since the last
* `updateHash()` call).
*
* @return The current `SHAMapHash` for this node.
*/
SHAMapHash const&
getHash() const
{
return hash_;
}
/** Determines the type of node. */
/** Return the in-memory node type used for runtime dispatch.
*
* @return One of `TnInner`, `TnTransactionNm`, `TnTransactionMd`, or
* `TnAccountState`.
*/
virtual SHAMapNodeType
getType() const = 0;
/** Determines if this is a leaf node. */
/** Return whether this node is a leaf (data-carrying) node.
*
* @return `true` for all three concrete leaf types; `false` for inner
* nodes.
*/
virtual bool
isLeaf() const = 0;
/** Determines if this is an inner node. */
/** Return whether this node is an inner (branching) node.
*
* @return `true` for `SHAMapInnerNode`; `false` for all leaf types.
*/
virtual bool
isInner() const = 0;
/** Serialize the node in a format appropriate for sending over the wire */
/** Serialize this node for peer-to-peer wire transmission.
*
* Appends the node payload followed by a single `kWIRE_TYPE_*` type byte
* at the end of the buffer. The receiver reads the last byte to determine
* the node type before parsing the preceding payload.
*
* @param s Serializer to append to.
* @see serializeWithPrefix for the hashing/database format.
*/
virtual void
serializeForWire(Serializer&) const = 0;
serializeForWire(Serializer& s) const = 0;
/** Serialize the node in a format appropriate for hashing */
/** Serialize this node in canonical hash-input form.
*
* Prepends a 4-byte `HashPrefix` constant before the node data, allowing
* the node type to be identified from the first four bytes. This format is
* used for Merkle hash computation and for database storage.
*
* @param s Serializer to append to.
* @see serializeForWire for the wire-transmission format.
*/
virtual void
serializeWithPrefix(Serializer&) const = 0;
serializeWithPrefix(Serializer& s) const = 0;
/** Return a human-readable description of this node for debugging.
*
* @param id The tree address of this node.
* @return A string representation including the node's position.
*/
virtual std::string
getString(SHAMapNodeID const&) const;
getString(SHAMapNodeID const& id) const;
/** Verify structural invariants in debug builds.
*
* Each concrete subclass asserts its own preconditions (non-zero hash,
* consistent child counts, etc.). The `isRoot` parameter relaxes
* constraints that do not apply at the tree root — e.g., a root inner
* node with a single child is valid for a one-item tree.
*
* @param isRoot Pass `true` when checking the tree root; pass `false`
* (the default) for all other nodes.
*/
virtual void
invariants(bool isRoot = false) const = 0;
/** Deserialize a node from the hash-prefixed database/storage format.
*
* Reads the leading 4-byte `HashPrefix` to identify the node type, strips
* it, then dispatches to the appropriate private factory. The supplied
* `hash` is passed directly to the concrete node constructor (skipping
* `updateHash()`), so the caller is asserting that `hash` is correct.
*
* @param rawNode Serialized node bytes including the leading `HashPrefix`.
* @param hash Pre-validated Merkle hash for this node.
* @return A `SharedPtr<SHAMapTreeNode>` to the newly-constructed node.
* @throws std::runtime_error if `rawNode` is fewer than 4 bytes or its
* prefix does not correspond to a known node type.
*/
static intr_ptr::SharedPtr<SHAMapTreeNode>
makeFromPrefix(Slice rawNode, SHAMapHash const& hash);
/** Deserialize a node from the wire-transmission format.
*
* Reads the trailing type byte to identify the node type, strips it, then
* dispatches to the appropriate factory. Because the hash is not supplied,
* leaf nodes call `updateHash()` internally to compute it from the payload.
*
* @param rawNode Serialized node bytes including the trailing `kWIRE_TYPE_*`
* byte.
* @return A `SharedPtr<SHAMapTreeNode>` to the newly-constructed node, or
* an empty pointer if `rawNode` is empty.
* @throws std::runtime_error if the trailing type byte is unrecognized.
*/
static intr_ptr::SharedPtr<SHAMapTreeNode>
makeFromWire(Slice rawNode);
private:
/** Construct a `SHAMapTxLeafNode` from raw transaction bytes.
*
* The item key is computed as `sha512Half(HashPrefix::TransactionId, data)`.
* If `hashValid` is true, `hash` is assigned directly to the new node;
* otherwise `updateHash()` is deferred to the concrete constructor.
*
* @param data Raw transaction payload.
* @param hash Pre-validated node hash (used only when `hashValid`).
* @param hashValid Whether `hash` may be trusted without recomputation.
* @return A `SharedPtr<SHAMapTreeNode>` to the new leaf node.
*/
static intr_ptr::SharedPtr<SHAMapTreeNode>
makeTransaction(Slice data, SHAMapHash const& hash, bool hashValid);
/** Construct a `SHAMapAccountStateLeafNode` from raw account-state bytes.
*
* The item key is read from the trailing 32 bytes of `data`, then chopped.
* Throws if `data` is too short or the extracted key is zero.
*
* @param data Raw account-state payload with 32-byte key appended.
* @param hash Pre-validated node hash (used only when `hashValid`).
* @param hashValid Whether `hash` may be trusted without recomputation.
* @return A `SharedPtr<SHAMapTreeNode>` to the new leaf node.
* @throws std::runtime_error if `data` is shorter than 32 bytes or the
* extracted key is zero.
*/
static intr_ptr::SharedPtr<SHAMapTreeNode>
makeAccountState(Slice data, SHAMapHash const& hash, bool hashValid);
/** Construct a `SHAMapTxPlusMetaLeafNode` from raw transaction+metadata bytes.
*
* The item key is read from the trailing 32 bytes of `data`, then chopped.
* Throws if `data` is too short.
*
* @param data Raw tx+metadata payload with 32-byte key appended.
* @param hash Pre-validated node hash (used only when `hashValid`).
* @param hashValid Whether `hash` may be trusted without recomputation.
* @return A `SharedPtr<SHAMapTreeNode>` to the new leaf node.
* @throws std::runtime_error if `data` is shorter than 32 bytes.
*/
static intr_ptr::SharedPtr<SHAMapTreeNode>
makeTransactionWithMeta(Slice data, SHAMapHash const& hash, bool hashValid);
};

View File

@@ -8,16 +8,58 @@
namespace xrpl {
/** A leaf node for a transaction. No metadata is included. */
/** SHAMap leaf node for a bare transaction without metadata.
*
* Used when building the transaction tree of an open or proposed ledger,
* before execution metadata is available. One of three concrete leaf types
* alongside `SHAMapTxPlusMetaLeafNode` (transaction + metadata) and
* `SHAMapAccountStateLeafNode` (ledger state); each encodes type-specific
* hashing and serialization rules statically via virtual dispatch,
* eliminating runtime branching in the hot path.
*
* Critically, the Merkle hash does **not** include the item key. A bare
* transaction's ID is itself derived from `sha512Half(prefix, blob)`, so
* the key is redundant — the hash fully characterises the content without
* it. This contrasts with `SHAMapTxPlusMetaLeafNode` and
* `SHAMapAccountStateLeafNode`, which must include the key because their
* keys are externally assigned identifiers that do not appear in the blob.
*
* All mutable state lives in the base classes. This class is a stateless
* policy layer: it supplies only the hash formula, clone factory, type
* tag, and wire format for no-metadata transaction leaves.
*
* @see SHAMapLeafNode
* @see SHAMapTxPlusMetaLeafNode
* @see SHAMapAccountStateLeafNode
*/
class SHAMapTxLeafNode final : public SHAMapLeafNode, public CountedObject<SHAMapTxLeafNode>
{
public:
/** Construct a new bare-transaction leaf and compute its hash.
*
* Use this constructor when creating a brand-new node from a freshly
* produced item. `updateHash()` is called immediately so the node is
* valid for insertion into the trie.
*
* @param item The raw transaction payload; must be non-null.
* @param cowid Copy-on-write owner ID of the creating SHAMap instance.
*/
SHAMapTxLeafNode(boost::intrusive_ptr<SHAMapItem const> item, std::uint32_t cowid)
: SHAMapLeafNode(std::move(item), cowid)
{
updateHash();
}
/** Construct a bare-transaction leaf with a pre-computed hash.
*
* Used by `clone()` when the underlying item has not changed: forwarding
* the existing hash avoids a redundant SHA-512 computation.
*
* @param item The raw transaction payload; must be non-null.
* @param cowid Copy-on-write owner ID for the new node.
* @param hash Known hash of `item`; must be consistent with the item's
* current content or the Merkle tree will be corrupted.
*/
SHAMapTxLeafNode(
boost::intrusive_ptr<SHAMapItem const> item,
std::uint32_t cowid,
@@ -26,24 +68,61 @@ public:
{
}
/** Produce an exclusively owned copy of this node for copy-on-write mutation.
*
* The new node shares the same item and hash as the original — no
* recomputation occurs. The caller supplies the new `cowid` so the clone
* is immediately owned by the mutating SHAMap.
*
* @param cowid Copy-on-write owner ID for the cloned node.
* @return A freshly allocated `SHAMapTxLeafNode` with the same item and
* hash, owned exclusively by `cowid`.
*/
intr_ptr::SharedPtr<SHAMapTreeNode>
clone(std::uint32_t cowid) const final
{
return intr_ptr::makeShared<SHAMapTxLeafNode>(item_, cowid, hash_);
}
/** Return the node type tag for bare-transaction leaves.
*
* @return `SHAMapNodeType::TnTransactionNm`
*/
SHAMapNodeType
getType() const final
{
return SHAMapNodeType::TnTransactionNm;
}
/** Recompute and store this node's Merkle hash.
*
* Hashes the `HashPrefix::TransactionId` domain separator (`'T'`,`'X'`,`'N'`)
* followed by the raw transaction bytes via `sha512Half`. The item key is
* deliberately omitted: for a bare transaction, the transaction ID is
* itself `sha512Half(prefix, blob)`, so the key carries no information
* beyond what the payload already provides.
*
* @note The hash formula differs from `SHAMapTxPlusMetaLeafNode::updateHash()`
* and `SHAMapAccountStateLeafNode::updateHash()`, which both include
* `item_->key()` because their keys are externally assigned and not
* derivable from the payload alone.
*/
void
updateHash() final
{
hash_ = SHAMapHash{sha512Half(HashPrefix::TransactionId, item_->slice())};
}
/** Serialize this node for peer-to-peer sync (wire format).
*
* Writes the raw transaction bytes followed by the single-byte wire-type
* tag `kWIRE_TYPE_TRANSACTION` (`0`). The item key is not written because
* it is fully determined by the payload. The trailing tag allows
* `SHAMapTreeNode::makeFromWire()` to reconstruct the correct concrete
* leaf type on the receiving peer.
*
* @param s Serializer to append to.
*/
void
serializeForWire(Serializer& s) const final
{
@@ -51,6 +130,16 @@ public:
s.add8(kWIRE_TYPE_TRANSACTION);
}
/** Serialize this node in the canonical hashing format.
*
* Writes the 4-byte `HashPrefix::TransactionId` domain separator followed
* by the raw transaction bytes. This matches the input fed to `sha512Half`
* in `updateHash()` and is used for Merkle proof verification where the
* hash prefix already encodes the node type (no wire-type tag is appended).
* The item key is omitted for the same reason as in `updateHash()`.
*
* @param s Serializer to append to.
*/
void
serializeWithPrefix(Serializer& s) const final
{

View File

@@ -8,17 +8,64 @@
namespace xrpl {
/** A leaf node for a transaction and its associated metadata. */
/** SHAMap leaf node for a transaction paired with its execution metadata.
*
* Represents the canonical form of a transaction entry in a validated
* (closed) ledger's transaction map. Each node stores the transaction blob
* and the `TxMeta` blob describing exactly what the transaction did to the
* ledger state. Open-ledger transaction maps use the metadata-free sibling
* `SHAMapTxLeafNode` instead; the two types are structurally incompatible
* because their hash formulas differ, making Merkle roots from the two
* contexts intrinsically distinct.
*
* Sits at the bottom of the inheritance chain:
* `SHAMapTreeNode` → `SHAMapLeafNode` → `SHAMapTxPlusMetaLeafNode`.
* This class is a stateless policy layer: all mutable state (`hash_`,
* `cowid_`, `item_`) lives in the base classes. The only behavior provided
* here is the type-specific hash formula, wire format, clone factory, and
* type tag.
*
* Also inherits `CountedObject<SHAMapTxPlusMetaLeafNode>`, which wires the
* type into the global object telemetry system for diagnosing live-instance
* counts under memory pressure.
*
* @see SHAMapLeafNode
* @see SHAMapTxLeafNode
* @see SHAMapAccountStateLeafNode
*/
class SHAMapTxPlusMetaLeafNode final : public SHAMapLeafNode,
public CountedObject<SHAMapTxPlusMetaLeafNode>
{
public:
/** Construct a new transaction-plus-metadata leaf and compute its hash.
*
* Use this constructor when creating a node from a freshly produced item.
* `updateHash()` is called immediately so the node is valid for insertion
* into the trie.
*
* @param item The serialized transaction-plus-metadata payload; must be
* non-null and at least 12 bytes.
* @param cowid Copy-on-write owner ID of the creating `SHAMap` instance.
*/
SHAMapTxPlusMetaLeafNode(boost::intrusive_ptr<SHAMapItem const> item, std::uint32_t cowid)
: SHAMapLeafNode(std::move(item), cowid)
{
updateHash();
}
/** Construct a transaction-plus-metadata leaf with a pre-computed hash.
*
* Used by `clone()` and deserialization paths where the hash is already
* known, avoiding a redundant SHA-512 computation. The caller must ensure
* `hash` is consistent with `item`'s content or the Merkle tree will be
* silently corrupted.
*
* @param item The serialized transaction-plus-metadata payload; must be
* non-null and at least 12 bytes.
* @param cowid Copy-on-write owner ID for the new node.
* @param hash Pre-computed hash of the leaf; passed straight through to
* `SHAMapLeafNode` without recomputation.
*/
SHAMapTxPlusMetaLeafNode(
boost::intrusive_ptr<SHAMapItem const> item,
std::uint32_t cowid,
@@ -27,24 +74,70 @@ public:
{
}
/** Produce an exclusively owned copy of this node for copy-on-write mutation.
*
* Shares the existing `item_` pointer (no deep copy) and forwards the
* current `hash_` to avoid recomputation. The caller supplies the new
* `cowid` so the clone is immediately owned by the mutating `SHAMap`.
*
* @param cowid Copy-on-write owner ID for the cloned node.
* @return A freshly allocated `SHAMapTxPlusMetaLeafNode` with the same
* item and hash, exclusively owned by `cowid`.
*/
intr_ptr::SharedPtr<SHAMapTreeNode>
clone(std::uint32_t cowid) const override
{
return intr_ptr::makeShared<SHAMapTxPlusMetaLeafNode>(item_, cowid, hash_);
}
/** Return the node type tag for transaction-plus-metadata leaves.
*
* @return `SHAMapNodeType::TnTransactionMd`
*/
SHAMapNodeType
getType() const override
{
return SHAMapNodeType::TnTransactionMd;
}
/** Recompute and store this node's Merkle hash.
*
* Hashes the `HashPrefix::TxNode` domain separator (`'S'`,`'N'`,`'D'`)
* followed by the raw payload slice and then the 32-byte item key, via
* `sha512Half`. Including the key is essential: unlike a bare transaction
* (whose ID is derived from the payload), a tx+meta node's key is an
* externally assigned identifier not present in the blob — omitting it
* would allow two distinct objects with identical payloads to collide.
*
* The `HashPrefix::TxNode` separator is distinct from the
* `HashPrefix::TransactionId` used by `SHAMapTxLeafNode` and
* `HashPrefix::LeafNode` used by `SHAMapAccountStateLeafNode`, ensuring
* cross-context hash collisions are structurally impossible.
*
* @note Marked `final` to signal that the hashing algorithm for this
* node type is fixed. `serializeWithPrefix()` must stay in sync with
* this formula; any drift silently corrupts hash verification.
*/
void
updateHash() final
{
hash_ = SHAMapHash{sha512Half(HashPrefix::TxNode, item_->slice(), item_->key())};
}
/** Serialize this node for peer-to-peer sync (wire format).
*
* Writes the raw payload slice, then the 32-byte item key via
* `addBitString`, then the single-byte wire-type tag
* `kWIRE_TYPE_TRANSACTION_WITH_META` (`4`). The trailing tag allows
* `SHAMapTreeNode::makeFromWire()` to reconstruct the correct concrete
* leaf type on the receiving peer.
*
* The key must be included on the wire because it does not appear in the
* payload — contrast with `SHAMapTxLeafNode::serializeForWire()`, which
* omits the key entirely and uses wire-type `0`.
*
* @param s Serializer to append to.
*/
void
serializeForWire(Serializer& s) const final
{
@@ -53,6 +146,17 @@ public:
s.add8(kWIRE_TYPE_TRANSACTION_WITH_META);
}
/** Serialize this node in the canonical hashing format.
*
* Writes the 4-byte `HashPrefix::TxNode` domain separator followed by
* the raw payload slice and the 32-byte item key. This matches exactly
* the input fed to `sha512Half` in `updateHash()` and is used during
* Merkle proof verification to reconstruct a hash from raw data without
* instantiating a full node object. No wire-type tag is appended because
* the hash prefix already encodes the node type.
*
* @param s Serializer to append to.
*/
void
serializeWithPrefix(Serializer& s) const final
{

View File

@@ -1,3 +1,14 @@
/** @file
* Defines `TreeNodeCache`, the in-memory hot cache of deserialized
* `SHAMapTreeNode` objects used by every live SHAMap.
*
* This file is intentionally minimal: it binds together `TaggedCache`'s
* two-level strong/weak eviction policy with intrusive pointer machinery
* that allows early memory reclamation and single-word strong/weak duality.
* The resulting alias is the canonical type name used throughout the
* SHAMap and `Family` interfaces.
*/
#pragma once
#include <xrpl/basics/IntrusivePointer.h>
@@ -6,10 +17,53 @@
namespace xrpl {
/** In-memory cache of deserialized `SHAMapTreeNode` objects, keyed by hash.
*
* Every ledger's account-state and transaction SHAMap shares a single
* `TreeNodeCache` (via `Family::getTreeNodeCache()`). Nodes fetched from
* persistent storage are placed here after deserialization; subsequent
* lookups by the same `uint256` hash return the already-decoded object,
* avoiding redundant disk reads and ensuring that identical on-disk nodes
* are represented by a single in-memory object — essential for SHAMap's
* copy-on-write scheme, where unmodified nodes are shared freely across
* ledger generations.
*
* The alias uses `intr_ptr::SharedWeakUnionPtr` and `intr_ptr::SharedPtr`
* instead of the `TaggedCache` defaults (`SharedWeakCachePointer` /
* `std::shared_ptr`) for two reasons:
*
* - **Earlier memory reclamation.** With `std::make_shared`, the control
* block and object are co-allocated, so the memory block cannot be freed
* until all weak references (held by the cache) expire. The intrusive
* model stores reference counts inside the `SHAMapTreeNode` itself, and
* `SHAMapInnerNode::partialDestructor()` releases all 16 child pointers
* the moment the strong count hits zero, even while the cache still holds
* a weak reference to the parent.
*
* - **Single-word strong/weak duality.** `SharedWeakUnionPtr<T>` stores
* either a strong or a weak intrusive reference in one pointer-sized word,
* using the low-order bit as a tag (alignment guarantees the bit is always
* zero in a real pointer). When the cache sweeper demotes a hot entry to a
* tracking-only entry, it calls `convertToWeak()` in-place — flipping one
* bit — rather than replacing a `shared_ptr`/`weak_ptr` pair.
*
* `IsKeyCache = false` selects `TaggedCache`'s value-cache mode, where the
* map stores the actual `SHAMapTreeNode` objects (not just keys). Nodes
* remain strongly referenced while hot; they degrade to weak references as
* they age, and are removed entirely when both the cache entry expires and no
* external strong pointer holds the object live.
*
* @note Nodes retrieved from the cache have `cowid_ == 0` by invariant —
* they are shared and must not be mutated. Any map that needs to modify
* such a node must call `clone()` first to obtain a private copy.
* @see Family::getTreeNodeCache()
* @see SHAMapTreeNode::partialDestructor()
*/
using TreeNodeCache = TaggedCache<
uint256,
SHAMapTreeNode,
/*IsKeyCache*/ false,
false,
intr_ptr::SharedWeakUnionPtr<SHAMapTreeNode>,
intr_ptr::SharedPtr<SHAMapTreeNode>>;
} // namespace xrpl

View File

@@ -1,3 +1,12 @@
/** @file
* Sparse child-array manager for SHAMap inner nodes.
*
* Defines `TaggedPointer`, which packs a size-class tag into the two low bits
* of a heap pointer to manage four pool-backed capacity tiers (2, 4, 6, 16
* slots). Also defines `popcnt16`, the popcount primitive used to translate
* branch numbers to sparse array indices.
*/
#pragma once
#include <xrpl/basics/IntrusivePointer.h>
@@ -9,111 +18,147 @@
namespace xrpl {
/** TaggedPointer is a combination of a pointer and a mask stored in the
lowest two bits.
Since pointers do not have arbitrary alignment, the lowest bits in the
pointer are guaranteed to be zero. TaggedPointer stores information in these
low bits. When dereferencing the pointer, these low "tag" bits are set to
zero. When accessing the tag bits, the high "pointer" bits are set to zero.
The "pointer" part points to the equivalent to an array of
`SHAMapHash` followed immediately by an array of
`shared_ptr<SHAMapTreeNode>`. The sizes of these arrays are
determined by the tag. The tag is an index into an array (`boundaries`,
defined in the cpp file) that specifies the size. Both arrays are the
same size. Note that the sizes may be smaller than the full 16 elements
needed to explicitly store all the children. In this case, the arrays
only store the non-empty children. The non-empty children are stored in
index order. For example, if only children `2` and `14` are non-empty, a
two-element array would store child `2` in array index 0 and child `14`
in array index 1. There are functions to convert between a child's tree
index and the child's index in a sparse array.
The motivation for this class is saving RAM. A large percentage of inner
nodes only store a small number of children. Memory can be saved by
storing the inner node's children in sparse arrays. Measurements show
that on average a typical SHAMap's inner nodes can be stored using only
25% of the original space.
*/
/** Sparse co-located array pair for `SHAMapInnerNode` children.
*
* Owns a single heap allocation that holds a `SHAMapHash[]` immediately
* followed by a `SharedPtr<SHAMapTreeNode>[]`, both of the same length N.
* N is one of four capacity tiers (2, 4, 6, or 16) chosen by rounding the
* requested child count up to the nearest boundary. The tier index (03) is
* stored in the two low bits of the allocation pointer, which are always zero
* due to `SHAMapHash`'s minimum alignment of 4.
*
* In the **dense** tier (N == 16, tag == 3) the logical branch number is also
* the array index. In **sparse** tiers only occupied children are stored,
* packed in ascending branch-index order; callers translate via
* `getChildIndex()` (a single `popcnt16` on the occupancy bitset). The
* occupancy bitset itself (`isBranch_`) lives in `SHAMapInnerNode`, not here.
*
* Each tier is backed by its own `boost::singleton_pool` (512 KB blocks),
* keeping allocation O(1) and avoiding general-purpose heap fragmentation for
* these hot, fixed-size objects. On a typical production ledger this layout
* reduces inner-node memory to roughly 25% of a fully-dense allocation.
*
* `TaggedPointer` is move-only. A moved-from instance has `tp_ == 0`, which
* `destroyHashesAndChildren()` treats as a no-op sentinel.
*
* @note The `boundaries` array in `TaggedPointer.ipp` must have exactly 4
* entries; a `static_assert` enforces this because the tag field is only
* 2 bits wide.
* @see SHAMapInnerNode for the owning class and its `isBranch_` occupancy
* bitset.
* @see popcnt16 for the branch-number-to-array-index primitive.
*/
class TaggedPointer
{
private:
static_assert(
alignof(SHAMapHash) >= 4,
"Bad alignment: Tag pointer requires low two bits to be zero.");
/** Upper bits are the pointer, lowest two bits are the tag
A moved-from object will have a tp_ of zero.
*/
/** Combined pointer and 2-bit size-class tag.
*
* High bits (`& kPTR_MASK`) are the raw allocation address; low 2 bits
* (`& kTAG_MASK`) index into the `boundaries` array to give the array
* capacity. Set to 0 in a moved-from instance.
*/
std::uintptr_t tp_ = 0;
/** bit-and with this mask to get the tag bits (lowest two bits) */
/** Mask to extract the 2-bit size-class tag from `tp_`. */
static constexpr std::uintptr_t kTAG_MASK = 3;
/** bit-and with this mask to get the pointer bits (mask out the tag) */
/** Mask to extract the raw pointer from `tp_` (clears the tag bits). */
static constexpr std::uintptr_t kPTR_MASK = ~kTAG_MASK;
/** Deallocate memory and run destructors */
/** Run element destructors on all allocated slots and return memory to the
* pool. A no-op when `tp_ == 0` (moved-from or default state).
*/
void
destroyHashesAndChildren();
/** Tag type used to select the raw-allocate constructor overload.
*
* An empty struct whose sole purpose is to make the intent of the
* private constructor explicit at each call site: the caller is taking
* responsibility for running placement-new on every allocated slot before
* any destructor can fire.
*/
struct RawAllocateTag
{
};
/** This constructor allocates space for the hashes and children, but
does not run constructors.
@param RawAllocateTag used to select overload only
@param numChildren allocate space for at least this number of children
(must be <= branchFactor)
@note Since the hashes/children destructors are always run in the
TaggedPointer destructor, this means those constructors _must_ be run
after this constructor is run. This constructor is private and only used
in places where the hashes/children constructor are subsequently run.
*/
/** Allocate pool memory for at least `numChildren` slots without running
* element constructors.
*
* The actual capacity is rounded up to the nearest tier boundary. Because
* the destructor unconditionally calls `destroyHashesAndChildren()`, which
* runs destructors on all allocated slots, every call site **must**
* follow this constructor with placement-new loops covering all allocated
* slots before any code path that could throw or early-return.
*
* @param RawAllocateTag Overload selector; pass `RawAllocateTag{}`.
* @param numChildren Minimum number of slots to allocate; must be ≤
* `SHAMapInnerNode::kBRANCH_FACTOR`.
*/
explicit TaggedPointer(RawAllocateTag, std::uint8_t numChildren);
public:
TaggedPointer() = delete;
/** Allocate and default-construct arrays for at least `numChildren` slots.
*
* Rounds `numChildren` up to the nearest tier boundary, allocates from
* the corresponding pool, and runs default constructors on every
* `SHAMapHash` and `SharedPtr<SHAMapTreeNode>` slot.
*
* @param numChildren Minimum capacity; must be ≤
* `SHAMapInnerNode::kBRANCH_FACTOR`.
*/
explicit TaggedPointer(std::uint8_t numChildren);
/** Constructor is used change the number of allocated children.
Existing children from `other` are copied (toAllocate must be >= the
number of children). The motivation for making this a constructor is it
saves unneeded copying and zeroing out of hashes if this were
implemented directly in the SHAMapInnerNode class.
@param other children and hashes are moved from this param
@param isBranch bitset of non-empty children in `other`
@param toAllocate allocate space for at least this number of children
(must be <= branchFactor)
*/
/** Resize the allocation, preserving existing children from `other`.
*
* If `toAllocate` maps to the same tier as `other`'s current capacity
* the allocation is reused in-place; otherwise a new pool block is
* allocated and all non-empty children (identified by `isBranch`) are
* moved into it. Remaining slots are default-constructed.
*
* Implemented as a constructor (rather than a member function) to avoid
* unnecessary copies and zero-fills that would occur if the resize were
* performed inside `SHAMapInnerNode` directly.
*
* @param other Source `TaggedPointer`; left in a valid moved-from state.
* @param isBranch Occupancy bitset for `other`: bit `i` set means branch
* `i` is non-empty.
* @param toAllocate Minimum capacity for the result; must be ≥
* `popcnt16(isBranch)` and ≤ `SHAMapInnerNode::kBRANCH_FACTOR`.
*/
explicit TaggedPointer(TaggedPointer&& other, std::uint16_t isBranch, std::uint8_t toAllocate);
/** Given `other` with the specified children in `srcBranches`, create a
new TaggedPointer with the allocated number of children and the
children specified in `dstBranches`.
@param other children and hashes are moved from this param
@param srcBranches bitset of non-empty children in `other`
@param dstBranches bitset of children to copy from `other` (or space to
leave in a sparse array - see note below)
@param toAllocate allocate space for at least this number of children
(must be <= branchFactor)
@note a child may be absent in srcBranches but present in dstBranches
(if dst has a sparse representation, space for the new child will be
left in the sparse array). Typically, srcBranches and dstBranches will
differ by at most one bit. The function works correctly if they differ
by more, but there are likely more efficient algorithms to consider if
this becomes a common use-case.
*/
/** Resize the allocation and simultaneously apply a branch-set delta.
*
* Constructs a new `TaggedPointer` whose logical children are the
* intersection of `other`'s children and `dstBranches`, with empty slots
* prepared for any branch in `dstBranches` that was absent in
* `srcBranches`. When the old and new tier are identical the operation is
* performed in-place (shift left to remove, shift right to insert);
* otherwise a fresh pool block is allocated and elements are copied across
* with placement-new.
*
* Used by `SHAMapInnerNode::resizeChildArrays()` when an add or remove
* operation changes the occupied count across a tier boundary.
*
* @param other Source `TaggedPointer`; left in a valid moved-from state.
* @param srcBranches Occupancy bitset of non-empty children in `other`.
* @param dstBranches Occupancy bitset for the result. A bit may be set in
* `dstBranches` but absent in `srcBranches` — in a sparse result an
* empty slot is reserved at the correct sorted position for the new
* child. `srcBranches` and `dstBranches` typically differ by one bit.
* @param toAllocate Minimum capacity for the result; must be ≥
* `popcnt16(dstBranches)` and ≤ `SHAMapInnerNode::kBRANCH_FACTOR`.
* @note If the two bitsets differ by more than one bit the function
* remains correct but may not be the most efficient approach for that
* use-case.
*/
explicit TaggedPointer(
TaggedPointer&& other,
std::uint16_t srcBranches,
@@ -122,80 +167,137 @@ public:
TaggedPointer(TaggedPointer const&) = delete;
/** Move constructor. Transfers ownership; leaves `other` in a moved-from
* state (`tp_ == 0`).
*/
TaggedPointer(TaggedPointer&&);
/** Move-assignment operator. Destroys the current allocation, then
* transfers ownership from `other`, leaving `other` moved-from.
*/
TaggedPointer&
operator=(TaggedPointer&&);
/** Destroy all allocated slots and return memory to the pool. */
~TaggedPointer();
/** Decode the tagged pointer into its tag and pointer */
/** Separate the stored pointer from its 2-bit size-class tag.
*
* @return A pair of `{tag, rawPtr}` where `tag` is the 2-bit tier index
* into `boundaries` and `rawPtr` is the untagged allocation address.
*/
[[nodiscard]] std::pair<std::uint8_t, void*>
decode() const;
/** Get the number of elements allocated for each array */
/** Number of slots allocated in each array (hashes and children).
*
* This is `boundaries[tag]` — one of 2, 4, 6, or 16.
*
* @return The array capacity for the current size-class tier.
*/
[[nodiscard]] std::uint8_t
capacity() const;
/** Check if the arrays have a dense format.
@note The dense format is when there is an array element for all 16
(branchFactor) possible children.
*/
/** Return true when the allocation holds all 16 (`kBRANCH_FACTOR`) slots.
*
* In the dense layout branch number equals array index directly, so no
* popcount translation is needed.
*
* @return `true` if the tag equals the last (dense) tier index.
*/
[[nodiscard]] bool
isDense() const;
/** Get the number of elements in each array and a pointer to the start
of each array.
*/
/** Return the array capacity and raw pointers to both co-located arrays.
*
* The `SHAMapHash` array begins at the allocation base; the
* `SharedPtr<SHAMapTreeNode>` array begins immediately after (at
* `hashes + numAllocated`). Both arrays have `numAllocated` elements.
*
* @return A tuple of `{numAllocated, hashes, children}`.
*/
[[nodiscard]] std::tuple<std::uint8_t, SHAMapHash*, intr_ptr::SharedPtr<SHAMapTreeNode>*>
getHashesAndChildren() const;
/** Get the `hashes` array */
/** Return a pointer to the start of the `SHAMapHash` array.
*
* Equivalent to `std::get<1>(getHashesAndChildren())` but slightly
* cheaper when the children pointer is not needed.
*
* @return Pointer to the first `SHAMapHash` element.
*/
[[nodiscard]] SHAMapHash*
getHashes() const;
/** Get the `children` array */
/** Return a pointer to the start of the `SharedPtr<SHAMapTreeNode>` array.
*
* @return Pointer to the first child smart-pointer element.
*/
[[nodiscard]] intr_ptr::SharedPtr<SHAMapTreeNode>*
getChildren() const;
/** Call the `f` callback for all 16 (branchFactor) branches - even if
the branch is empty.
@param isBranch bitset of non-empty children
@param f a one parameter callback function. The parameter is the
child's hash.
/** Invoke `f` for all 16 branches, supplying each branch's hash.
*
* Empty branches in a sparse layout receive a zero-valued `SHAMapHash`.
* Iterates all `kBRANCH_FACTOR` branches in ascending branch-index order,
* making this suitable for `updateHash()` which must feed all 16 hashes
* to the SHA-512 half-hash regardless of occupancy.
*
* @tparam F Callable with signature `void(SHAMapHash const&)`.
* @param isBranch Occupancy bitset; bit `i` set means branch `i` is
* non-empty and its hash is stored in the array.
* @param f Callback invoked once per branch in branch-index order.
*/
template <class F>
void
iterChildren(std::uint16_t isBranch, F&& f) const;
/** Call the `f` callback for all non-empty branches.
@param isBranch bitset of non-empty children
@param f a two parameter callback function. The first parameter is
the branch number, the second parameter is the index into the array.
For dense formats these are the same, for sparse they may be
different.
/** Invoke `f` for every non-empty branch with both its branch number and
* its physical array index.
*
* For a dense layout the two values are identical. For a sparse layout
* the array index is the packed position (`popcnt16` of lower set bits in
* `isBranch`), which differs from the branch number whenever any
* lower-numbered branch is absent. Callers that need to index into the
* hashes or children arrays (e.g., mutation helpers) must use the
* array index, not the branch number.
*
* @tparam F Callable with signature `void(int branchNum, int arrayIdx)`.
* @param isBranch Occupancy bitset of non-empty children.
* @param f Callback invoked once per occupied branch in ascending order.
*/
template <class F>
void
iterNonEmptyChildIndexes(std::uint16_t isBranch, F&& f) const;
/** Get the child's index inside the `hashes` or `children` array (which
may or may not be sparse). The optional will be empty if an empty
branch is requested and the children are sparse.
@param isBranch bitset of non-empty children
@param i index of the requested child
/** Translate a logical branch number to a physical array index.
*
* In the dense layout returns `i` directly. In a sparse layout, counts
* the number of occupied branches below `i` via `popcnt16`, which gives
* the packed array position of branch `i`.
*
* @param isBranch Occupancy bitset of non-empty children.
* @param i Logical branch number (015) to look up.
* @return The array index, or `std::nullopt` if the arrays are sparse and
* branch `i` is not occupied (empty branches have no array slot in
* sparse mode).
*/
[[nodiscard]] std::optional<int>
getChildIndex(std::uint16_t isBranch, int i) const;
};
/** Count the number of set bits in a 16-bit value.
*
* Used by `TaggedPointer::getChildIndex()` to translate a logical branch
* number to a sparse array index (number of occupied branches below position
* `i`), and by `SHAMapInnerNode::getBranchCount()`. Both are on hot traversal
* paths, so the implementation dispatches to the fastest available intrinsic:
* `std::popcount` (C++20), `__builtin_popcount` (GCC/Clang), or a
* compile-time-generated 256-entry lookup table as a portable fallback.
*
* @param a 16-bit value whose set bits are to be counted.
* @return Number of bits set in `a`, in the range [0, 16].
*/
[[nodiscard]] inline int
popcnt16(std::uint16_t a)
{

View File

@@ -10,10 +10,47 @@
namespace xrpl {
/** State information when applying a tx. */
/** Central context object for the transaction-application pipeline.
*
* `ApplyContext` is created at the boundary between the *preclaim* phase
* (read-only authorization and fee validation) and the *apply* phase (actual
* ledger state mutation). It lives for the duration of apply and is passed
* by reference to every `Transactor` implementation, giving each handler a
* uniform handle to the sandboxed mutable view, the validated transaction,
* and the fee information.
*
* The sandboxed view is stored as `std::optional<ApplyViewImpl>` layered on
* top of `base_`. Rollback is implemented by discarding and re-emplacing the
* optional rather than walking an undo log — see `discard()`. Mutations are
* not committed to `base_` until `apply()` is called explicitly.
*
* @note The const-qualified public members (`tx`, `preclaimResult`,
* `baseFee`, `journal`) are immutable for the lifetime of an apply
* cycle. All mutable state is confined to the private `view_`,
* `flags_`, and `parentBatchId_` members.
*/
class ApplyContext
{
public:
/** Construct for a batch-inner transaction.
*
* Use this constructor when the transaction executes inside a `ttBATCH`
* envelope. The `parentBatchId` is forwarded to `ApplyViewImpl::apply()`
* so the generated `TxMeta` records the parent-child relationship.
* Asserts that `parentBatchId` is set if and only if `TapBatch` is
* active in `flags`.
*
* @param registry Service registry providing shared engine services.
* @param base The underlying open ledger view that accumulates
* committed state. Never modified until `apply()` is called.
* @param parentBatchId The `uint256` ID of the enclosing batch
* transaction. Must be non-null when `TapBatch` is set in `flags`.
* @param tx The fully-validated transaction to apply.
* @param preclaimResult The `TER` code produced by the preclaim phase.
* @param baseFee The fee determined before applying, in drops.
* @param flags Apply-phase control flags (e.g., `TapDryRun`).
* @param journal Logging sink; defaults to the null sink.
*/
explicit ApplyContext(
ServiceRegistry& registry,
OpenView& base,
@@ -24,6 +61,21 @@ public:
ApplyFlags flags,
beast::Journal journal = beast::Journal{beast::Journal::getNullSink()});
/** Construct for a standalone (non-batch) transaction.
*
* Delegates to the full constructor with `std::nullopt` for
* `parentBatchId`. Asserts that `TapBatch` is not set in `flags` —
* batch-inner transactions must use the constructor that accepts a
* `parentBatchId`.
*
* @param registry Service registry providing shared engine services.
* @param base The underlying open ledger view.
* @param tx The fully-validated transaction to apply.
* @param preclaimResult The `TER` code produced by the preclaim phase.
* @param baseFee The fee determined before applying, in drops.
* @param flags Apply-phase control flags. Must not include `TapBatch`.
* @param journal Logging sink; defaults to the null sink.
*/
explicit ApplyContext(
ServiceRegistry& registry,
OpenView& base,
@@ -37,24 +89,58 @@ public:
XRPL_ASSERT((flags & TapBatch) == 0, "Batch apply flag should not be set");
}
/** Service registry providing shared engine services. */
std::reference_wrapper<ServiceRegistry> registry;
/** The transaction being applied. Immutable for the apply lifecycle. */
STTx const& tx;
/** The `TER` result produced by the preclaim phase. Immutable. */
TER const preclaimResult;
/** The fee charged for this transaction, in drops. Immutable. */
XRPAmount const baseFee;
/** Logging sink. Immutable. */
beast::Journal const journal;
/** Access the sandboxed mutable ledger view.
*
* Returns the `ApplyViewImpl` layered on top of `base_`. All
* transactor mutations go through this view; none reach `base_`
* until `apply()` is called.
*
* @return A mutable reference to the sandboxed apply view.
*/
ApplyView&
view()
{
return *view_; // NOLINT(bugprone-unchecked-optional-access) view_ emplaced in constructor
}
/** Access the sandboxed ledger view (read-only overload).
*
* @return A const reference to the sandboxed apply view.
*/
[[nodiscard]] ApplyView const&
view() const
{
return *view_; // NOLINT(bugprone-unchecked-optional-access) view_ emplaced in constructor
}
/** Access the sandboxed view as a low-level `RawView`.
*
* Bypasses the higher-level constraint enforcement in `ApplyView` to
* allow direct ledger-entry writes. Use only where `ApplyView`'s guards
* are legitimately too restrictive for the operation at hand.
*
* @return A mutable reference to the underlying `RawView` interface of
* the sandboxed view.
*
* @note Prefer `view()` wherever possible. This accessor exists as an
* escape hatch for engine internals that must write ledger entries
* without the higher-level guards.
*/
// VFALCO Unfortunately this is necessary
RawView&
rawView()
@@ -62,13 +148,20 @@ public:
return *view_; // NOLINT(bugprone-unchecked-optional-access) view_ emplaced in constructor
}
/** Return the apply-phase control flags for this transaction. */
[[nodiscard]] ApplyFlags const&
flags() const
{
return flags_;
}
/** Sets the DeliveredAmount field in the metadata */
/** Record the delivered amount in the transaction metadata.
*
* Sets the `sfDeliveredAmount` field written into `TxMeta` when
* `apply()` is called. Must be called before `apply()` to take effect.
*
* @param amount The amount actually delivered by this transaction.
*/
void
deliver(STAmount const& amount)
{
@@ -76,18 +169,52 @@ public:
view_->deliver(amount);
}
/** Discard changes and start fresh. */
/** Discard all sandboxed mutations and reset to a clean view.
*
* Destroys the current `ApplyViewImpl` in-place and constructs a fresh
* one on top of `base_`. The base view is never touched, so all
* accumulated ledger changes evaporate without an undo log. Called by
* `Transactor::reset()` to implement `tec*` rollback and by the
* `tapFAIL_HARD` path to suppress even fee deduction.
*/
void
discard();
/** Apply the transaction result to the base. */
/** Commit sandboxed mutations to the base view and produce metadata.
*
* Delegates to `ApplyViewImpl::apply()`, which writes all accumulated
* state changes into `base_` and generates `TxMeta`. After this call
* the `ApplyViewImpl` is consumed and must not be used again.
*
* @param ter The final `TER` result code for the transaction.
* @return The generated `TxMeta` if the transaction is committed to the
* ledger, or `std::nullopt` when `TapDryRun` is active.
*/
std::optional<TxMeta> apply(TER);
/** Get the number of unapplied changes. */
/** Return the number of pending (uncommitted) ledger-entry changes.
*
* @return Count of SLE modifications accumulated in the sandboxed view
* since the last `discard()` or construction.
*/
std::size_t
size();
/** Visit unapplied changes. */
/** Iterate over pending ledger-entry changes in the sandboxed view.
*
* Calls `func` once for each modified, inserted, or deleted SLE
* tracked by the sandbox. Used by invariant checkers (via
* `checkInvariants`) and by `tecOVERSIZE` / `tecKILLED` cleanup
* handlers to identify objects that need post-failure removal.
*
* @param func Callback invoked per entry. Parameters:
* - `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 SLE state after this transaction (`nullptr`
* for deletions).
*/
void
visit(
std::function<void(
@@ -96,6 +223,13 @@ public:
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after)> const& func);
/** Burn the given XRP fee from the ledger supply.
*
* Forwards to `RawView::rawDestroyXRP()` on the sandboxed view.
* Called by `Transactor::payFee()` as part of fee deduction.
*
* @param fee Amount of XRP, in drops, to remove from the total supply.
*/
void
destroyXRP(XRPAmount const& fee)
{
@@ -103,16 +237,40 @@ public:
view_->rawDestroyXRP(fee);
}
/** Applies all invariant checkers one by one.
@param result the result generated by processing this transaction.
@param fee the fee charged for this transaction
@return the result code that should be returned for this transaction.
/** Run all registered invariant checkers and return the final result.
*
* Iterates the compile-time `InvariantChecks` tuple. For each checker,
* calls `visitEntry()` on every pending SLE change (via `visit()`),
* then calls `finalize()`. Results are collected into an array before
* being tested with `std::all_of` — never short-circuited — so that
* every failing invariant emits its own fatal log message.
*
* If any checker fails or throws, delegates to `failInvariantCheck()`
* to determine whether to return `tecINVARIANT_FAILED` (first failure,
* fee still charged) or `tefINVARIANT_FAILED` (repeat failure, tx not
* included in ledger at all).
*
* @param result The `TER` produced by `doApply()`; must be
* `tesSUCCESS` or a `tec*` claim code.
* @param fee The fee charged for this transaction, in drops.
* @return The original `result` if all invariants pass; otherwise
* `tecINVARIANT_FAILED` or `tefINVARIANT_FAILED`.
*/
TER
checkInvariants(TER const result, XRPAmount const fee);
private:
/** Determine the escalated failure code for a broken invariant.
*
* Returns `tefINVARIANT_FAILED` if `result` is already
* `tecINVARIANT_FAILED` or `tefINVARIANT_FAILED` (the fee-only retry
* path also broke invariants — nothing safe to commit). Returns
* `tecINVARIANT_FAILED` on the first failure so the transaction is
* still included in the ledger with a fee charge.
*
* @param result The current TER before escalation.
* @return The escalated invariant-failure TER.
*/
static TER
failInvariantCheck(TER const result);

View File

@@ -14,24 +14,43 @@ namespace xrpl {
// Forward declarations
class STObject;
// Support for SignerEntries that is needed by a few Transactors.
//
// SignerEntries is represented as a std::vector<SignerEntries::SignerEntry>.
// There is no direct constructor for SignerEntries.
//
// o A std::vector<SignerEntries::SignerEntry> is a SignerEntries.
// o More commonly, SignerEntries are extracted from an STObject by
// calling SignerEntries::deserialize().
/** Non-constructible utility scope for the multi-signature co-signer roster.
*
* A signer list is represented as a `std::vector<SignerEntries::SignerEntry>`.
* Entries are produced exclusively via `SignerEntries::deserialize()`, which
* extracts them from an `STObject` (either a transaction or a live ledger
* entry). This class cannot be instantiated; it exists only to co-locate
* the `SignerEntry` type and the `deserialize()` factory under one name.
*
* @see SignerEntry
* @see deserialize
*/
class SignerEntries
{
public:
explicit SignerEntries() = delete;
/** A single co-signer record extracted from an `sfSignerEntries` array.
*
* Holds the co-signer's account ID, their vote weight toward the quorum,
* and an optional destination tag (`sfWalletLocator`) that supports
* phantom accounts — signers that may not yet have an on-ledger account
* root.
*
* @note Comparison operators are intentionally defined on `account` alone.
* Sorting and duplicate detection in `SignerListSet` and
* `Transactor::checkMultiSign()` both rely on account-only ordering:
* `std::sort()` uses `operator<=>` to produce the sorted vector that
* enables the O(n) linear merge in `checkMultiSign()`, and
* `std::adjacent_find()` uses `operator==` to detect duplicate
* account IDs (a `temBAD_SIGNER` condition). Including `weight` or
* `tag` in either operator would silently break both checks.
*/
struct SignerEntry
{
AccountID account;
std::uint16_t weight;
std::optional<uint256> tag;
AccountID account; /**< The co-signer's account ID. */
std::uint16_t weight; /**< Vote weight contributed toward the quorum. */
std::optional<uint256> tag; /**< Optional `sfWalletLocator` destination tag. */
SignerEntry(
AccountID const& inAccount,
@@ -41,13 +60,18 @@ public:
{
}
// For sorting to look for duplicate accounts
/** Three-way comparison by `account` only, enabling `std::sort` and
* the O(n) merge in `Transactor::checkMultiSign()`.
*/
friend auto
operator<=>(SignerEntry const& lhs, SignerEntry const& rhs)
{
return lhs.account <=> rhs.account;
}
/** Equality test by `account` only, enabling duplicate detection via
* `std::adjacent_find` after sorting.
*/
friend bool
operator==(SignerEntry const& lhs, SignerEntry const& rhs)
{
@@ -55,11 +79,33 @@ public:
}
};
// Deserialize a SignerEntries array from the network or from the ledger.
//
// obj Contains a SignerEntries field that is an STArray.
// journal For reporting error conditions.
// annotation Source of SignerEntries, like "ledger" or "transaction".
/** Extract and lightly validate the `sfSignerEntries` array from an STObject.
*
* Works against both an `STTx` (during preflight/preclaim) and an `SLE`
* (during `checkMultiSign()` against the on-ledger signer list). Each
* array element must carry the `sfSignerEntry` field name; the function
* extracts `sfAccount`, `sfSignerWeight`, and optionally `sfWalletLocator`
* per entry. No business-logic validation (quorum reachability, duplicate
* detection, self-reference) is performed here — that belongs to
* `SignerListSet::validateQuorumAndSignerEntries()`.
*
* The returned vector is pre-reserved to `STTx::kMAX_MULTI_SIGNERS` to
* avoid reallocation during iteration. Callers typically sort it
* immediately after return to enable O(n) duplicate detection and the
* linear merge in `checkMultiSign()`.
*
* @param obj Any `STObject` that carries an `sfSignerEntries` field —
* a transaction being preflight-checked or a ledger entry being read
* during apply.
* @param journal Journal used to emit `trace`-level diagnostics when a
* malformed entry is encountered.
* @param annotation Short label — typically `"transaction"` or `"ledger"`
* — prepended to journal messages to identify the data source.
* @return On success, a vector of `SignerEntry` values in the order they
* appear in the `sfSignerEntries` array. On failure, a `NotTEC` error
* code (typically `temMALFORMED`) that callers should propagate
* immediately without dereferencing the value.
*/
static Expected<std::vector<SignerEntry>, NotTEC>
deserialize(STObject const& obj, beast::Journal journal, std::string_view annotation);
};

View File

@@ -1,3 +1,27 @@
/**
* @file Transactor.h
*
* Base class and context structures for the XRPL transaction-processing
* pipeline.
*
* Every transaction type (Payment, OfferCreate, AMM, NFT, etc.) inherits
* from `Transactor` and participates in a strict three-phase pipeline:
*
* - **preflight** — stateless, no ledger access; validates format, flags, and
* signature syntax via `invokePreflight<T>()`.
* - **preclaim** — read-only `ReadView`; checks sequence, fee balance, and
* signature validity against ledger state.
* - **doApply** — mutable `ApplyView`; applies state changes; only reached
* when preclaim returns `tesSUCCESS`.
*
* Compile-time polymorphism is achieved through name hiding, not virtual
* dispatch: derived classes define static methods (`preflight`,
* `checkExtraFeatures`, `getFlagsMask`, `preflightSigValidated`) that are
* resolved by the `invokePreflight<T>` template at compile time.
*
* @see PreflightContext, PreclaimContext, ApplyContext
*/
#pragma once
#include <xrpl/beast/utility/Journal.h>
@@ -11,17 +35,43 @@
namespace xrpl {
/** State information when preflighting a tx. */
/** Immutable context passed to all preflight checks.
*
* Carries everything a stateless preflight validation step needs: the raw
* transaction, the active ledger rules, apply flags, and — for batch inner
* transactions — the hash of the enclosing batch. No ledger view is
* included because preflight must not access ledger state.
*
* Two constructors enforce the batch/non-batch invariant at construction
* time: the batch constructor asserts `TapBatch` is set and records the
* `parentBatchId`; the non-batch constructor asserts `TapBatch` is clear
* and leaves `parentBatchId` empty.
*/
struct PreflightContext
{
public:
/** Service registry providing network ID, hash router, and load fees. */
std::reference_wrapper<ServiceRegistry> registry;
/** The transaction being validated. */
STTx const& tx;
/** Active ledger rules (amendments) at the time of validation. */
Rules const rules;
/** Apply flags controlling validation behavior (e.g., `TapDryRun`, `TapBatch`). */
ApplyFlags flags;
/** Hash of the enclosing batch transaction, present only for batch inner transactions. */
std::optional<uint256 const> parentBatchId;
/** Journal for diagnostic logging. */
beast::Journal const j;
/** Construct a context for a batch inner transaction.
*
* @param registry Service registry.
* @param tx The inner transaction.
* @param parentBatchId Hash of the outer batch transaction.
* @param rules Active ledger rules.
* @param flags Apply flags; `TapBatch` must be set.
* @param j Journal for logging.
*/
PreflightContext(
ServiceRegistry& registry,
STTx const& tx,
@@ -39,6 +89,14 @@ public:
XRPL_ASSERT((flags & TapBatch) == TapBatch, "Batch apply flag should be set");
}
/** Construct a context for an ordinary (non-batch) transaction.
*
* @param registry Service registry.
* @param tx The transaction.
* @param rules Active ledger rules.
* @param flags Apply flags; `TapBatch` must NOT be set.
* @param j Journal for logging.
*/
PreflightContext(
ServiceRegistry& registry,
STTx const& tx,
@@ -54,18 +112,48 @@ public:
operator=(PreflightContext const&) = delete;
};
/** State information when determining if a tx is likely to claim a fee. */
/** Immutable context passed to all preclaim checks.
*
* Extends `PreflightContext` with a read-only `ReadView` so that preclaim
* can verify account existence, sequence validity, fee sufficiency, and
* signature correctness against the current ledger state. The result of
* the earlier preflight phase is carried forward in `preflightResult` so
* that preclaim helpers can short-circuit when preflight already failed.
*
* The same batch/non-batch constructor duality as `PreflightContext`
* applies: `parentBatchId` presence must match the `TapBatch` flag,
* enforced by assertion in the unified constructor.
*/
struct PreclaimContext
{
public:
/** Service registry providing network ID, hash router, and load fees. */
std::reference_wrapper<ServiceRegistry> registry;
/** Read-only view of the ledger against which preclaim checks are evaluated. */
ReadView const& view;
/** The `NotTEC` code returned by the earlier preflight phase. */
TER preflightResult;
/** Apply flags (e.g., `TapDryRun`, `TapBatch`, `TapUnlimited`). */
ApplyFlags flags;
/** The transaction being evaluated. */
STTx const& tx;
/** Hash of the enclosing batch transaction; set iff `TapBatch` is active. */
std::optional<uint256 const> const parentBatchId;
/** Journal for diagnostic logging. */
beast::Journal const j;
/** Construct for a batch inner transaction (or ordinary tx with explicit batch ID).
*
* Asserts that `parentBatchId.has_value() == ((flags & TapBatch) == TapBatch)`.
*
* @param registry Service registry.
* @param view Read-only ledger view.
* @param preflightResult Result from the preflight phase.
* @param tx The transaction.
* @param flags Apply flags.
* @param parentBatchId Hash of the outer batch, or `std::nullopt`.
* @param j Journal for logging.
*/
PreclaimContext(
ServiceRegistry& registry,
ReadView const& view,
@@ -87,6 +175,15 @@ public:
"Parent Batch ID should be set if batch apply flag is set");
}
/** Construct for an ordinary (non-batch) transaction.
*
* @param registry Service registry.
* @param view Read-only ledger view.
* @param preflightResult Result from the preflight phase.
* @param tx The transaction.
* @param flags Apply flags; `TapBatch` must NOT be set.
* @param j Journal for logging.
*/
PreclaimContext(
ServiceRegistry& registry,
ReadView const& view,
@@ -108,15 +205,48 @@ struct PreflightResult;
// Needed for preflight specialization
class Change;
/** Base class for all XRPL transaction processors.
*
* Implements the three-phase transaction pipeline: preflight (stateless
* validation), preclaim (read-only ledger checks), and doApply (mutable
* ledger application). Every concrete transaction type (Payment,
* OfferCreate, AMMCreate, etc.) inherits from this class.
*
* Polymorphism in the preflight phase is achieved through compile-time
* name hiding rather than virtual dispatch. Derived classes define
* static methods — `preflight`, `preclaim`, `getFlagsMask`,
* `checkExtraFeatures`, `preflightSigValidated` — that are resolved by
* `invokePreflight<T>()` at the call site. See the comment block on
* `invokePreflight` for the rules on what derived classes must and must
* not define.
*
* The single virtual entry point for state mutation is `doApply()`.
* `operator()()` is the top-level dispatch called by the apply loop;
* it drives all three phases, handles fee claiming on failure, runs
* invariant checks, and manages `tapDRY_RUN` simulation semantics.
*
* @note Instances are not copyable. One transactor object is created
* per transaction application.
*/
class Transactor
{
protected:
/** Apply context holding the sandboxed ledger view and transaction. */
ApplyContext& ctx_;
/** Wrapped journal sink that prepends the transaction ID to each log line. */
beast::WrappedSink sink_;
/** Journal backed by `sink_`; use this for all logging inside transactors. */
beast::Journal const j_;
/** The account that submitted the transaction (`sfAccount`). */
AccountID const account_;
XRPAmount preFeeBalance_{}; // Balance before fees.
/** Account balance captured immediately before fee deduction in `apply()`.
*
* Reserve checks in `doApply` must use this value rather than the
* post-fee balance to allow accounts to dip into their reserve to pay
* the fee without violating the reserve requirement for new objects.
*/
XRPAmount preFeeBalance_{};
public:
virtual ~Transactor() = default;
@@ -124,18 +254,56 @@ public:
Transactor&
operator=(Transactor const&) = delete;
/** Controls how `TxConsequences` are produced for the transaction queue.
*
* - `Normal` — standard fee/sequence consequences (most transactors).
* - `Blocker` — signals that applying this transaction may prevent
* subsequent queued transactions from the same account from
* claiming fees (e.g., `SetRegularKey`, `AccountDelete`).
* - `Custom` — the transactor implements `makeTxConsequences()` for
* type-specific cost modeling (e.g., `Payment`, `OfferCreate`).
*
* Each derived class must declare:
* @code
* static constexpr ConsequencesFactoryType ConsequencesFactory{...};
* @endcode
* The correct factory is selected at compile time in `applySteps.cpp`
* via C++20 `requires` constraints.
*/
enum class ConsequencesFactoryType { Normal, Blocker, Custom };
/** Process the transaction. */
/** Execute the full transaction pipeline for this transactor instance.
*
* Called by the apply loop after preclaim succeeds. Runs:
* 1. RAII numeric-rule guards (`NumberSO`, `CurrentTransactionRulesGuard`).
* 2. Debug-mode serialization round-trip check.
* 3. Optional debug trap (`trapTransaction`).
* 4. `apply()` if preclaim returned `tesSUCCESS`; otherwise returns the
* preclaim error directly.
* 5. `tecOVERSIZE` roll-back: if metadata grew too large, discards all
* mutations, re-deducts fee only, and removes unfunded offers found
* during the failed apply.
* 6. `tapFAIL_HARD`: on a `tec*` result, discards everything including
* the fee.
* 7. Invariant checks via `checkInvariants`; a failing invariant triggers
* a second reset and fee-only commit.
* 8. Forces `applied = false` when `tapDRY_RUN` is set.
*
* @return `{result, applied, metadata}`. `applied` is false when the
* transaction produces no ledger changes (dry-run, `tef*`, `tem*`,
* or invariant escalation to `tefINVARIANT_FAILED`).
*/
ApplyResult
operator()();
/** Return the mutable apply view for this transaction. */
ApplyView&
view()
{
return ctx_.view();
}
/** Return the read-only apply view for this transaction. */
[[nodiscard]] ApplyView const&
view() const
{
@@ -156,77 +324,182 @@ public:
[[nodiscard]] TER
checkInvariants(TER result, XRPAmount fee);
/////////////////////////////////////////////////////
/*
These static functions are called from invoke_preclaim<Tx>
using name hiding to accomplish compile-time polymorphism,
so derived classes can override for different or extra
functionality. Use with care, as these are not really
virtual and so don't have the compiler-time protection that
comes with it.
*/
// ---- Preclaim-phase static helpers (overridable via name hiding) --------
//
// These static functions are called from the preclaim dispatch in
// applySteps.cpp using name hiding to accomplish compile-time
// polymorphism. Derived classes can shadow them to add or replace
// validation logic. They are NOT virtual; the compiler provides no
// protection against incorrect overrides.
/** Verify the transaction's sequence number or ticket against the ledger.
*
* Returns `terNO_ACCOUNT` if the source account does not exist,
* `terPRE_SEQ` / `tefPAST_SEQ` for sequence-number mismatches, and
* `terPRE_TICKET` / `tefNO_TICKET` for ticket-based transactions.
*
* @param view Read-only ledger view.
* @param tx The transaction.
* @param j Journal for trace logging.
* @return `tesSUCCESS` if the sequence/ticket is consumable.
*/
static NotTEC
checkSeqProxy(ReadView const& view, STTx const& tx, beast::Journal j);
/** Verify `sfAccountTxnID`, `sfLastLedgerSequence`, and duplicate detection.
*
* Returns `tefWRONG_PRIOR` if `sfAccountTxnID` does not match the
* account's last transaction hash, `tefMAX_LEDGER` if the current ledger
* sequence exceeds `sfLastLedgerSequence`, and `tefALREADY` if the
* transaction is already in the ledger.
*
* @param ctx Preclaim context.
* @return `tesSUCCESS` or a `tef*` / `ter*` error.
*/
static NotTEC
checkPriorTxAndLastLedger(PreclaimContext const& ctx);
/** Verify that the fee attached to the transaction is sufficient.
*
* For open-ledger transactions, the fee must meet the load-scaled
* minimum returned by `minimumFee()`. Also checks that the fee payer's
* account exists and has sufficient balance.
*
* @param ctx Preclaim context.
* @param baseFee Unscaled base fee computed by `calculateBaseFee()`.
* @return `tesSUCCESS`, `telINSUF_FEE_P`, `tecINSUFF_FEE`,
* `terINSUF_FEE_B`, or `terNO_ACCOUNT`.
*/
static TER
checkFee(PreclaimContext const& ctx, XRPAmount baseFee);
/** Verify the cryptographic signature for an ordinary transaction.
*
* Dispatches to `checkMultiSign()` when `sfSigners` is present, or
* `checkSingleSign()` otherwise. Skips the check for batch inner
* transactions (authorized by the outer batch) and dry-run simulations
* without a signing key. Rejects pseudo-account signers when
* `featureLendingProtocol` is active.
*
* @param ctx Preclaim context.
* @return `tesSUCCESS` or a `tef*` error code.
*/
static NotTEC
checkSign(PreclaimContext const& ctx);
/** Verify the `sfBatchSigners` array for an outer batch transaction.
*
* Iterates the batch signers, dispatching to `checkMultiSign()` or
* `checkSingleSign()` as appropriate. Allows a signer for an
* account that does not yet exist in the ledger, provided the signing
* key matches the account's master key (used for fund-on-creation inner
* transactions).
*
* @param ctx Preclaim context for the outer batch transaction.
* @return `tesSUCCESS` or a `tef*` error code.
*/
static NotTEC
checkBatchSign(PreclaimContext const& ctx);
// Returns the fee in fee units, not scaled for load.
/** Compute the base transaction fee in drops, unscaled for load.
*
* Base fee = ledger's configured base fee + one extra base fee per
* multisignature in `sfSigners`. Does not account for server load;
* use `minimumFee()` for the load-adjusted value.
*
* @param view Read-only ledger view (supplies `fees().base`).
* @param tx The transaction.
* @return Fee in drops (XRPAmount).
*/
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
/* Do NOT define an invokePreflight function in a derived class.
Instead, define:
// Optional if the transaction is gated on an amendment that
// isn't specified in transactions.macro
static bool
checkExtraFeatures(PreflightContext const& ctx);
// Optional if the transaction uses any flags other than tfUniversal
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
// Required, even if it just returns tesSUCCESS.
static NotTEC
preflight(PreflightContext const& ctx);
// Optional, rarely needed, if the transaction does any expensive
// checks after the signature is verified.
static NotTEC preflightSigValidated(PreflightContext const& ctx);
* Do not try to call preflight1 or preflight2 directly.
* Do not check whether relevant amendments are enabled in preflight.
Instead, define checkExtraFeatures.
* Do not check flags in preflight. Instead, define getFlagsMask.
*/
/** Compile-time preflight dispatch for transaction type `T`.
*
* The canonical entry point for preflight validation. Executes the
* following steps in order, returning on the first non-`tesSUCCESS`:
*
* 1. Amendment gate: returns `temDISABLED` if the transaction's
* required amendment is not active.
* 2. `T::checkExtraFeatures(ctx)` — additional amendment gates defined
* by the derived class (return `temDISABLED` on failure).
* 3. `preflight1(ctx, T::getFlagsMask(ctx))` — validates account field,
* fee field, signing key format, network ID, flags, and
* ticket/AccountTxnID exclusivity.
* 4. `T::preflight(ctx)` — transaction-specific field validation.
* 5. `preflight2(ctx)` — cryptographic signature check via hash-router
* cache. Skipped for batch inner transactions.
* 6. `T::preflightSigValidated(ctx)` — optional post-signature checks
* (e.g., expensive crypto conditions).
*
* @note Do NOT define `invokePreflight` in a derived class. Instead,
* define any combination of the static methods above. Do NOT call
* `preflight1` or `preflight2` directly; they are called in the
* correct order by this template. Do NOT gate on amendments in
* `preflight`; use `checkExtraFeatures` for that. Do NOT validate
* flags in `preflight`; define `getFlagsMask` instead.
*
* @note The explicit specialization `invokePreflight<Change>` is
* defined in `Change.cpp` and uses entirely different logic because
* `Change` is a pseudo-transaction with no real sender.
*
* @tparam T The concrete transactor type.
* @param ctx Preflight context.
* @return `tesSUCCESS` or a `tem*` / `tel*` error.
*/
template <class T>
static NotTEC
invokePreflight(PreflightContext const& ctx);
/** Base-class preclaim hook; most transactors do not need to override this.
*
* The sequence/fee/sign checks are called directly by the preclaim
* dispatch in `applySteps.cpp` before this method. Override only to
* add extra read-only ledger checks that cannot be expressed as field
* validation in `preflight`.
*
* @param ctx Preclaim context.
* @return `tesSUCCESS` (base implementation).
*/
static TER
preclaim(PreclaimContext const& ctx)
{
// Most transactors do nothing
// after checkSeq/Fee/Sign.
return tesSUCCESS;
}
/** Verify delegate permissions if `sfDelegate` is present.
*
* If the transaction carries an `sfDelegate` field, reads the
* `DelegateObject` at `keylet::delegate(account, delegate)` and
* verifies that its permission set covers this transaction type.
* Returns `terNO_DELEGATE_PERMISSION` if the object is missing or the
* permission is not granted.
*
* Called as a static method during preclaim so the ledger check
* happens before any mutation.
*
* @param view Read-only ledger view.
* @param tx The transaction (may contain `sfDelegate`).
* @return `tesSUCCESS` or `terNO_DELEGATE_PERMISSION`.
*/
static NotTEC
checkPermission(ReadView const& view, STTx const& tx);
/////////////////////////////////////////////////////
// -------------------------------------------------------------------------
// Interface used by AccountDelete
/** Remove a single Ticket SLE and adjust the owner's ticket count and reserve.
*
* Used by `AccountDelete` (via a static interface) and by
* `consumeSeqProxy` when a ticket-based transaction is applied.
* Removes the ticket from the owner directory, decrements `sfTicketCount`
* on the account root, adjusts the owner reserve count, and erases the
* ticket SLE.
*
* @param view Mutable ledger view.
* @param account Owner of the ticket.
* @param ticketIndex Ledger index of the Ticket SLE.
* @param j Journal for fatal-error logging.
* @return `tesSUCCESS` or `tefBAD_LEDGER` if the ledger is corrupt.
*/
static TER
ticketDelete(
ApplyView& view,
@@ -235,14 +508,50 @@ public:
beast::Journal j);
protected:
/** Run the sequence/fee/state-mutation steps for a validated transaction.
*
* Called by `operator()()` when preclaim returned `tesSUCCESS`.
* Snapshots `preFeeBalance_`, advances the sequence (or consumes the
* ticket), deducts the fee, updates `sfAccountTxnID`, then calls
* `doApply()`.
*
* @return The TER returned by `doApply()`, or a `tef*` code if the
* sequence/fee bookkeeping fails (indicates ledger corruption).
*/
TER
apply();
/** Construct a transactor bound to the given apply context.
*
* Initialises `account_` from `ctx.tx[sfAccount]` and sets up the
* transaction-ID-prefixed journal sink.
*/
explicit Transactor(ApplyContext& ctx);
/** Perform any pre-apply computation that should not repeat per-ledger.
*
* Called at the start of `apply()` before `consumeSeqProxy` and
* `payFee`. The base implementation asserts that `account_` is
* non-zero. Derived classes may cache expensive lookups here.
*/
virtual void
preCompute();
/** Apply the transaction's state changes to the mutable ledger view.
*
* The sole virtual method in the pipeline. Only called when all
* preflight and preclaim checks have passed and the fee/sequence have
* been consumed.
*
* Implementations must return `tesSUCCESS` for a full commit.
* Returning a `tec*` code causes `operator()()` to roll back all
* mutations via `reset()` and re-apply the fee only. The tec rollback
* is automatic — there is no need to order mutations defensively or
* undo partial changes before returning `tec*`.
*
* @return `tesSUCCESS` or a `tec*` error. Must not return `tem*`,
* `tef*`, or `ter*` codes (those belong in preflight/preclaim).
*/
virtual TER
doApply() = 0;
@@ -292,22 +601,56 @@ protected:
ReadView const& view,
beast::Journal const& j) = 0;
/** Compute the minimum fee required to process a transaction
with a given baseFee based on the current server load.
@param registry The service registry.
@param baseFee The base fee of a candidate transaction
@see xrpl::calculateBaseFee
@param fees Fee settings from the current ledger
@param flags Transaction processing fees
/** Compute the load-scaled minimum fee required to relay this transaction.
*
* Scales `baseFee` using the node's current `LoadFeeTrack`. The
* `TapUnlimited` flag suppresses load scaling (used for locally-submitted
* or admin transactions).
*
* @param registry Service registry (provides `getFeeTrack()`).
* @param baseFee Unscaled base fee from `calculateBaseFee()`.
* @param fees Fee schedule from the current ledger.
* @param flags Apply flags; `TapUnlimited` disables load scaling.
* @return Minimum fee in drops that the network will accept.
*/
static XRPAmount
minimumFee(ServiceRegistry& registry, XRPAmount baseFee, Fees const& fees, ApplyFlags flags);
// Returns the fee in fee units, not scaled for load.
/** Return the owner-reserve increment as a fee, in drops.
*
* Used by transactions that create a ledger object and wish to charge
* one full reserve increment as the transaction fee (e.g.,
* `AccountDelete`, `AMMCreate`, `LoanBrokerSet`).
* Asserts that the reserve increment is at least 100× the base fee,
* ensuring the anti-spam reserve is meaningful.
*
* @param view Read-only ledger view (supplies `fees().increment`).
* @param tx The transaction (unused; present for uniformity).
* @return `fees().increment` in drops.
*/
static XRPAmount
calculateOwnerReserveFee(ReadView const& view, STTx const& tx);
/** Low-level signature check used by both the preclaim and batch paths.
*
* Selects between `checkMultiSign` and `checkSingleSign` based on
* transaction contents. Handles the special cases for batch inner
* transactions (no signature required), dry-run simulation (no key or
* signers is valid), and pseudo-account rejection under
* `featureLendingProtocol`.
*
* The public `checkSign(PreclaimContext const&)` overload is a thin
* wrapper around this one.
*
* @param view Read-only ledger view.
* @param flags Apply flags.
* @param parentBatchId Set for batch inner transactions; suppresses sig check.
* @param idAccount The account whose key must authorize the transaction.
* @param sigObject The STObject containing `sfSigningPubKey` /
* `sfSigners` (usually `ctx.tx`).
* @param j Journal for trace logging.
* @return `tesSUCCESS` or a `tef*` error.
*/
static NotTEC
checkSign(
ReadView const& view,
@@ -317,25 +660,85 @@ protected:
STObject const& sigObject,
beast::Journal const j);
// Base class always returns true
/** Amendment gate hook — override to gate the transaction on amendments.
*
* Called by `invokePreflight<T>` before `preflight1`. The base
* implementation always returns `true` (no extra gating). Derived
* classes that depend on amendments not listed in `transactions.macro`
* should override this method; return `false` to produce `temDISABLED`.
*
* @param ctx Preflight context.
* @return `true` if the transaction is permitted; `false` to disable it.
*/
static bool
checkExtraFeatures(PreflightContext const& ctx);
// Base class always returns tfUniversalMask
/** Flag-mask hook — override to declare valid flags for this transaction.
*
* The returned mask is passed to `preflight0` to reject unknown flag bits.
* The base implementation returns `tfUniversalMask`. Derived classes
* should override this to OR in their transaction-specific flag bits.
*
* @param ctx Preflight context.
* @return Bitmask of all valid flag bits for this transaction type.
*/
static std::uint32_t
getFlagsMask(PreflightContext const& ctx);
// Base class always returns tesSUCCESS
/** Post-signature preflight hook — override for expensive post-sig checks.
*
* Called by `invokePreflight<T>` after `preflight2` (signature
* verification). The base implementation returns `tesSUCCESS`.
* Derived classes that need to perform expensive checks that can only
* run after the signature is verified (e.g., crypto-condition validation
* in `EscrowFinish`) should override this.
*
* @param ctx Preflight context.
* @return `tesSUCCESS` or a `tem*` error.
*/
static NotTEC
preflightSigValidated(PreflightContext const& ctx);
/** Validate an optional blob field's length.
*
* Returns `false` if the slice is present but empty or exceeds
* `maxLength`; returns `true` if absent or within bounds.
*
* @param slice Optional blob (e.g., from `tx[~sfURI]`).
* @param maxLength Maximum permitted byte length.
* @return `true` if the length is valid.
*/
static bool
validDataLength(std::optional<Slice> const& slice, std::size_t maxLength);
/** Validate that an optional numeric field is within `[min, max]`.
*
* An absent optional (`std::nullopt`) is treated as valid — only
* present values are range-checked. This reflects the convention that
* optional fields are legal to omit.
*
* @tparam T Numeric type (must support `<=` comparison).
* @param value Optional field value.
* @param max Inclusive upper bound.
* @param min Inclusive lower bound (default-constructed, usually 0).
* @return `true` if absent or within `[min, max]`.
*/
template <class T>
static bool
validNumericRange(std::optional<T> value, T max, T min = T{});
/** Validate an optional strong-unit numeric field within `[min, max]`.
*
* Overload for `unit::ValueUnit<Unit, T>` bounds to maintain type
* safety across unit systems. Delegates to the plain-value overload.
*
* @tparam T Underlying numeric type.
* @tparam Unit Unit tag.
* @param value Optional field value (raw numeric).
* @param max Inclusive upper bound (unit-typed).
* @param min Inclusive lower bound (unit-typed, default zero).
* @return `true` if absent or within `[min, max]`.
*/
template <class T, class Unit>
static bool
validNumericRange(
@@ -343,12 +746,30 @@ protected:
unit::ValueUnit<Unit, T> max,
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});
/// Minimum will usually be zero.
/** Validate that an optional numeric field is at least `min`.
*
* An absent optional is treated as valid.
*
* @tparam T Numeric type.
* @param value Optional field value.
* @param min Inclusive lower bound (default-constructed, usually 0).
* @return `true` if absent or `>= min`.
*/
template <class T>
static bool
validNumericMinimum(std::optional<T> value, T min = T{});
/// Minimum will usually be zero.
/** Validate an optional strong-unit numeric field against a minimum.
*
* Overload for `unit::ValueUnit<Unit, T>` bounds. Delegates to the
* plain-value overload.
*
* @tparam T Underlying numeric type.
* @tparam Unit Unit tag.
* @param value Optional field value.
* @param min Inclusive lower bound (unit-typed, default zero).
* @return `true` if absent or `>= min`.
*/
template <class T, class Unit>
static bool
validNumericMinimum(
@@ -356,13 +777,56 @@ protected:
unit::ValueUnit<Unit, T> min = unit::ValueUnit<Unit, T>{});
private:
/** Roll back all doApply mutations and re-apply fee deduction only.
*
* Calls `ctx_.discard()` to discard all ledger changes, then
* re-deducts the fee from the fee payer's balance (clamped to the
* available balance), and re-consumes the sequence/ticket. Used for
* fee-claiming `tec*` results and after invariant failures.
*
* @param fee Requested fee in drops; clamped to available balance.
* @return `{tesSUCCESS, actualFee}` on success, or
* `{tefINTERNAL, 0}` if the account SLE is missing (ledger
* corruption).
*/
std::pair<TER, XRPAmount>
reset(XRPAmount fee);
/** Advance `sfSequence` or consume the Ticket for this transaction.
*
* For sequence-based transactions, increments `sfSequence` by one.
* For ticket-based transactions, delegates to `ticketDelete`.
*
* @param sleAccount Mutable SLE for the submitting account.
* @return `tesSUCCESS` or `tefBAD_LEDGER` if the ticket is missing.
*/
TER
consumeSeqProxy(SLE::pointer const& sleAccount);
/** Deduct the transaction fee from the fee payer's balance.
*
* Reads `sfFee` from the transaction and subtracts it from the fee
* payer's `sfBalance`. The caller is responsible for calling
* `view().update(sle)` to commit the change.
*
* @return `tesSUCCESS` or `tefINTERNAL` if the payer account is absent.
*/
TER
payFee();
/** Verify a single-signature transaction against the account root.
*
* Checks, in priority order: regular key → enabled master key →
* disabled master key (`tefMASTER_DISABLED`) → unknown key
* (`tefBAD_AUTH`).
*
* @param view Read-only ledger view.
* @param idSigner AccountID derived from the signing public key.
* @param idAccount AccountID from `sfAccount` (the authorizing account).
* @param sleAccount AccountRoot SLE for `idAccount`.
* @param j Journal for trace logging.
* @return `tesSUCCESS`, `tefMASTER_DISABLED`, or `tefBAD_AUTH`.
*/
static NotTEC
checkSingleSign(
ReadView const& view,
@@ -370,6 +834,24 @@ private:
AccountID const& idAccount,
std::shared_ptr<SLE const> sleAccount,
beast::Journal const j);
/** Verify a multi-signature against the account's SignerList.
*
* Performs an O(n) linear merge of the sorted `sfSigners` array from
* the transaction against the sorted `SignerEntry` list from the
* account's signer list SLE. Every signer in the transaction must
* appear in the account's signer list and pass key verification.
* Returns `tefBAD_QUORUM` if the accumulated weight is below
* `sfSignerQuorum`.
*
* @param view Read-only ledger view.
* @param flags Apply flags (used for dry-run simulation handling).
* @param id The account whose signer list governs authorization.
* @param sigObject The STObject containing `sfSigners`.
* @param j Journal for trace logging.
* @return `tesSUCCESS`, `tefNOT_MULTI_SIGNING`, `tefBAD_SIGNATURE`,
* `tefMASTER_DISABLED`, or `tefBAD_QUORUM`.
*/
static NotTEC
checkMultiSign(
ReadView const& view,
@@ -378,23 +860,51 @@ private:
STObject const& sigObject,
beast::Journal const j);
/** Named breakpoint for replaying specific transactions under a debugger.
*
* Does nothing except log at debug level. Set a breakpoint here to
* pause execution when a specific transaction (identified by its hash
* in the service registry's trap configuration) is being applied.
*/
void trapTransaction(uint256) const;
/** Performs early sanity checks on the account and fee fields.
(And passes flagMask to preflight0)
Do not try to call preflight1 from preflight() in derived classes. See
the description of invokePreflight for details.
*/
/** Early sanity checks on the account field, fee field, and flags.
*
* Called as step 3 of `invokePreflight<T>` (after
* `checkExtraFeatures`, before `T::preflight`). Validates:
* - `sfDelegate` presence (requires `featurePermissionDelegationV1_1`)
* - `preflight0` (network ID, txid, flags via `flagMask`)
* - `sfAccount` is non-zero
* - `sfFee` is native XRP and non-negative
* - signing key format
* - ticket / AccountTxnID mutual exclusivity
* - `tfInnerBatchTxn` requires `featureBatch`
*
* @note Do not call this from `preflight()` in derived classes. It is
* invoked automatically by `invokePreflight<T>`.
*
* @param ctx Preflight context.
* @param flagMask Bitmask of valid flags from `T::getFlagsMask()`.
* @return `tesSUCCESS` or a `tem*` / `tel*` error.
*/
static NotTEC
preflight1(PreflightContext const& ctx, std::uint32_t flagMask);
/** Checks whether the signature appears valid
Do not try to call preflight2 from preflight() in derived classes. See
the description of invokePreflight for details.
*/
/** Validate the cryptographic signature via the hash-router cache.
*
* Called as step 5 of `invokePreflight<T>` (after `T::preflight`,
* before `T::preflightSigValidated`). Skips the check entirely for
* batch inner transactions (`tfInnerBatchTxn` + `featureBatch`) since
* they are authorized by the outer batch's signature. For simulation
* (`TapDryRun`), validates key/signer consistency but skips
* cryptographic verification.
*
* @note Do not call this from `preflight()` in derived classes. It is
* invoked automatically by `invokePreflight<T>`.
*
* @param ctx Preflight context.
* @return `tesSUCCESS` or `temINVALID`.
*/
static NotTEC
preflight2(PreflightContext const& ctx);
@@ -420,28 +930,65 @@ Transactor::checkExtraFeatures(PreflightContext const& ctx)
return true;
}
/** Performs early sanity checks on the txid and flags */
/** Early sanity checks on the transaction ID, network ID, and flag bits.
*
* The very first check in the preflight pipeline, called from `preflight1`.
* Validates:
* - Pseudo-transactions may not carry `tfInnerBatchTxn`.
* - `sfNetworkID` presence/absence rules: legacy networks (ID ≤ 1024) must
* not include `sfNetworkID`; newer networks must include it and it must
* match the local node.
* - Transaction ID must not be all-zeros.
* - No flag bits outside `flagMask` may be set.
*
* @param ctx Preflight context.
* @param flagMask Bitmask of valid flags for this transaction type.
* @return `tesSUCCESS` or a `tel*` / `tem*` error.
*/
NotTEC
preflight0(PreflightContext const& ctx, std::uint32_t flagMask);
namespace detail {
/** Checks the validity of the transactor signing key.
/** Validate the format of the signing public key in a transaction or signer.
*
* Normally called from preflight1 with ctx.tx.
* Returns `temBAD_SIGNATURE` if the `sfSigningPubKey` field is non-empty
* but not a recognized key type (secp256k1 or Ed25519). An empty key is
* valid (indicates multi-signing or batch inner transaction).
*
* Called from `preflight1` with the transaction object.
*
* @param sigObject The STObject containing `sfSigningPubKey`.
* @param j Journal for debug logging.
* @return `tesSUCCESS` or `temBAD_SIGNATURE`.
*/
NotTEC
preflightCheckSigningKey(STObject const& sigObject, beast::Journal j);
/** Checks the special signing key state needed for simulation
/** Validate signing-key state for dry-run simulation transactions.
*
* Normally called from preflight2 with ctx.tx.
* Called from `preflight2` when `TapDryRun` is set. A simulation
* transaction is valid if it has neither a signature nor a multi-signer
* list, or if it uses multi-signers with empty individual signatures.
* Returns `std::nullopt` when `TapDryRun` is not set (the caller should
* proceed to normal signature verification).
*
* @param flags Apply flags; must have `TapDryRun` set to take effect.
* @param sigObject The transaction's STObject.
* @param j Journal for debug logging.
* @return `tesSUCCESS` or `temINVALID` if the simulation keys are
* inconsistent; `std::nullopt` if not in simulation mode.
*/
std::optional<NotTEC>
preflightCheckSimulateKeys(ApplyFlags flags, STObject const& sigObject, beast::Journal j);
} // namespace detail
// Defined in Change.cpp
/** Explicit preflight specialization for `Change` pseudo-transactions.
*
* `Change` is a validator-generated pseudo-transaction with no real sender;
* its preflight logic is entirely different from normal transactions.
* Defined in `Change.cpp`.
*/
template <>
NotTEC
Transactor::invokePreflight<Change>(PreflightContext const& ctx);

View File

@@ -13,87 +13,100 @@ class HashRouter;
class ServiceRegistry;
/** Describes the pre-processing validity of a transaction.
@see checkValidity, forceValidity
*/
*
* The three levels form a strict hierarchy: `SigBad < SigGoodOnly < Valid`.
* Local checks are only worth performing when the signature is good, so
* `SigGoodOnly` implies the signature passed but local checks failed.
* This hierarchy maps directly to P2P relay semantics: `SigBad` transactions
* are not forwarded; `SigGoodOnly` transactions are relayed but not applied;
* `Valid` transactions are both relayed and applied.
*
* @see checkValidity, forceValidity
*/
enum class Validity {
/// Signature is bad. Didn't do local checks.
/// Signature is invalid. Local checks were not attempted.
SigBad,
/// Signature is good, but local checks fail.
/// Signature is valid, but local checks failed.
SigGoodOnly,
/// Signature and local checks are good / passed.
/// Signature is valid and local checks passed.
Valid
};
/** Checks transaction signature and local checks.
@return A `Validity` enum representing how valid the
`STTx` is and, if not `Valid`, a reason string.
@note Results are cached internally, so tests will not be
repeated over repeated calls, unless cache expires.
@return `std::pair`, where `.first` is the status, and
`.second` is the reason if appropriate.
@see Validity
*/
/** Check a transaction's cryptographic signature and local well-formedness.
*
* Results are cached in the `HashRouter` using four private flag bits
* (`PRIVATE1``PRIVATE4`). Subsequent calls for the same transaction ID
* return immediately from the cache rather than re-verifying the signature.
* The cache inherits its TTL from the `HashRouter`'s aged map.
*
* Batch inner transactions (flagged `tfInnerBatchTxn`) follow a separate
* code path: they must have no signature fields, and after `fixBatchInnerSigs`
* activates they are permanently treated as never-valid to prevent erroneous
* `SF_SIGGOOD` cache entries on unsigned objects.
*
* @param router The hash router used to cache validity flags.
* @param tx The transaction to check.
* @param rules The current ledger rules (used for amendment-gated logic).
* @return A pair whose `.first` is the `Validity` status and whose `.second`
* is a human-readable reason string when the transaction is not `Valid`
* (empty on success).
*
* @see Validity, forceValidity
*/
std::pair<Validity, std::string>
checkValidity(HashRouter& router, STTx const& tx, Rules const& rules);
/** Sets the validity of a given transaction in the cache.
@warning Use with extreme care.
@note Can only raise the validity to a more valid state,
and can not override anything cached bad.
@see checkValidity, Validity
*/
/** Assert a specific validity level for a transaction in the hash-router cache.
*
* Uses a deliberate `[[fallthrough]]` switch to enforce monotonicity: setting
* `Valid` also sets the `SigGoodOnly` flag, because local checks cannot pass
* without a valid signature. This can only raise the cached state — it never
* marks a transaction as `SigBad`, so calling with `SigBad` is a no-op.
*
* The primary use case is for locally-constructed transactions that were never
* signed by a remote peer; the transaction queue can mark them pre-verified to
* avoid redundant signature checks on re-application.
*
* @param router The hash router that holds the cached flags.
* @param txid The transaction ID whose cached validity to update.
* @param validity The minimum validity level to assert. Passing `SigBad`
* has no effect.
*
* @warning Calling this bypasses real cryptographic verification. Only use
* when you have an out-of-band guarantee that the transaction is valid
* (e.g., a transaction you constructed locally and submitted yourself).
*
* @see checkValidity, Validity
*/
void
forceValidity(HashRouter& router, uint256 const& txid, Validity validity);
/** Apply a transaction to an `OpenView`.
This function is the canonical way to apply a transaction
to a ledger. It rolls the validation and application
steps into one function. To do the steps manually, the
correct calling order is:
@code{.cpp}
preflight -> preclaim -> doApply
@endcode
The result of one function must be passed to the next.
The `preflight` result can be safely cached and reused
asynchronously, but `preclaim` and `doApply` must be called
in the same thread and with the same view.
@note Does not throw.
For open ledgers, the `Transactor` will catch exceptions
and return `tefEXCEPTION`. For closed ledgers, the
`Transactor` will attempt to only charge a fee,
and return `tecFAILED_PROCESSING`.
If the `Transactor` gets an exception while trying
to charge the fee, it will be caught and
turned into `tefEXCEPTION`.
For network health, a `Transactor` makes its
best effort to at least charge a fee if the
ledger is closed.
@param app The current running `Application`.
@param view The open ledger that the transaction
will attempt to be applied to.
@param tx The transaction to be checked.
@param flags `ApplyFlags` describing processing options.
@param journal A journal.
@see preflight, preclaim, doApply
@return A pair with the `TER` and a `bool` indicating
whether or not the transaction was applied.
*/
/** Apply a transaction to an `OpenView`, running all three pipeline stages.
*
* Convenience wrapper that composes `preflight → preclaim → doApply` into a
* single call. The `preflight` result can be safely cached and reused across
* threads, but `preclaim` and `doApply` must run on the same thread and with
* the same view.
*
* This function does not throw. Exceptions inside a `Transactor` are caught
* and converted to `tefEXCEPTION`. For closed ledgers, if full application
* fails the `Transactor` will attempt a best-effort fee deduction and return
* `tecFAILED_PROCESSING`; if even the fee-deduction path throws, that
* exception is also caught and returned as `tefEXCEPTION`. This best-effort
* fee guarantee prevents fee-free spam vectors during consensus.
*
* @param registry The service registry providing transactor implementations.
* @param view The open ledger to which the transaction will be applied.
* @param tx The transaction to apply.
* @param flags `ApplyFlags` controlling processing options (e.g., `tapRETRY`,
* `tapDRY_RUN`).
* @param journal Logging sink.
* @return An `ApplyResult` whose `.ter` is the transaction result code and
* whose `.applied` is `true` if the transaction's mutations were committed
* to `view`.
*
* @see preflight, preclaim, doApply, applyTransaction
*/
ApplyResult
apply(
ServiceRegistry& registry,
@@ -102,26 +115,58 @@ apply(
ApplyFlags flags,
beast::Journal journal);
/** Enum class for return value from `applyTransaction`
@see applyTransaction
*/
/** Outcome classification returned by `applyTransaction`.
*
* Wraps the raw `TER` code from `apply()` into the three-way decision the
* transaction queue needs: commit, evict, or hold for a retry pass.
*
* @see applyTransaction
*/
enum class ApplyTransactionResult {
/// Applied to this ledger
/// Transaction was applied and its mutations committed to the ledger view.
Success,
/// Should not be retried in this ledger
/// Terminal failure — do not retry in this ledger.
/// Covers `tef*` (internal failures), `tem*` (malformed), and `tel*`
/// (local-node rejections).
Fail,
/// Should be retried in this ledger
/// Soft failure — the transaction may succeed in a later pass or ledger.
/// Covers all other non-applied results (e.g., `ter*`, `tec*` with
/// `tapRETRY`).
Retry
};
/** Transaction application helper
Provides more detailed logging and decodes the
correct behavior based on the `TER` type
@see ApplyTransactionResult
*/
/** Apply a transaction and classify the outcome for the transaction queue.
*
* Calls `apply()` and maps its `TER` result to `ApplyTransactionResult`:
* - `tefFailure`, `temMalformed`, `telLocal` → `Fail` (evict, no retry)
* - Any other non-applied result → `Retry` (hold for later)
* - Applied result → `Success`
*
* When `retryAssured` is `true`, `tapRETRY` is added to `flags` before
* calling `apply()`. With `tapRETRY` set, `tec` results are treated as soft
* failures rather than hard fee-claims; this affects `preclaim`'s
* `likelyToClaimFee` signal and determines whether the transaction is safe
* to relay without first applying it to the open ledger.
*
* For `ttBATCH` transactions that succeed, inner transactions are applied in
* a nested `OpenView` sandbox. Inner-transaction changes are committed to the
* main view only if the batch as a whole succeeds under its execution policy
* (`tfAllOrNothing`, `tfUntilFailure`, `tfOnlyOne`, `tfIndependent`).
*
* Exceptions from `apply()` are caught and returned as `Fail`.
*
* @param registry The service registry providing transactor implementations.
* @param view The open ledger to which the transaction will be applied.
* @param tx The transaction to apply.
* @param retryAssured If `true`, adds `tapRETRY` to `flags` so that `tec`
* results are treated as retryable soft failures rather than fee claims.
* @param flags Base `ApplyFlags` controlling processing options.
* @param journal Logging sink.
* @return An `ApplyTransactionResult` indicating success, terminal failure,
* or retryable failure.
*
* @see apply, ApplyTransactionResult
*/
ApplyTransactionResult
applyTransaction(
ServiceRegistry& registry,

View File

@@ -1,3 +1,14 @@
/** @file
* Public interface for the XRPL transaction application pipeline.
*
* Defines the structured three-stage sequence `preflight → preclaim → doApply`
* that every transaction traverses before being committed to an open ledger.
* The stages are exposed as separate functions so the Transaction Queue (TxQ)
* can cache `PreflightResult` across ledger boundaries and defer
* `preclaim`/`doApply` until an application slot is available.
*
* @see apply.h for a single-call wrapper that composes all three stages.
*/
#pragma once
#include <xrpl/beast/utility/Journal.h>
@@ -9,6 +20,14 @@ class ServiceRegistry;
class STTx;
class TxQ;
/** Outcome of a complete transaction application attempt.
*
* Returned by `doApply()` and by the single-call `apply()` wrapper.
* `applied` is true only when the transaction was committed to the ledger
* (either `tesSUCCESS` or a fee-claiming `tec*` without `tapRETRY`).
* `metadata` is populated when `applied` is true and the caller requested
* metadata generation.
*/
struct ApplyResult
{
TER ter;
@@ -21,8 +40,25 @@ struct ApplyResult
}
};
/** Return true if the transaction can claim a fee (tec),
and the `ApplyFlags` do not allow soft failures.
/** Return true when a `tec` result will definitely charge a fee.
*
* A `tec` transaction normally charges its fee and is included in the ledger
* with all `doApply` mutations rolled back. However, when `tapRETRY` is set
* the TxQ is treating the transaction as a soft failure that may succeed after
* other queued transactions settle — in that mode the fee must not yet be
* charged because the transaction is not actually being applied.
*
* This predicate is the authoritative definition of "this transaction will
* cost the submitter money right now." It is evaluated in the
* `PreclaimResult` constructor to populate `likelyToClaimFee`, and again
* in `Transactor::operator()()` to decide whether to call `reset()` (which
* discards all `doApply` mutations and re-applies the fee only).
*
* @param ter The `TER` result from preclaim or doApply.
* @param flags The `ApplyFlags` governing this application attempt.
* @return True when `ter` is a `tec*` code and `tapRETRY` is absent,
* meaning the fee will be charged and the transaction included in the
* ledger.
*/
inline bool
isTecClaimHardFail(TER ter, ApplyFlags flags)
@@ -30,100 +66,169 @@ isTecClaimHardFail(TER ter, ApplyFlags flags)
return isTecClaim(ter) && ((flags & TapRetry) == 0u);
}
/** Class describing the consequences to the account
of applying a transaction if the transaction consumes
the maximum XRP allowed.
*/
/** Worst-case XRP cost and queue-ordering impact of a transaction.
*
* The Transaction Queue (TxQ) evaluates this object — produced during
* `preflight` — to decide whether queued follow-on transactions from the
* same account remain viable *before* the transaction is applied to a
* ledger. It answers: how much XRP can this transaction consume in the
* worst case, and does it invalidate subsequent queue entries?
*
* The five constructors are deliberately distinct variants rather than a
* single struct with defaulted fields, so that each variant enforces its own
* invariant: the `NotTEC` constructor zeros everything to ensure no cost
* estimate leaks from a rejected transaction; the `Category` constructor
* flips `isBlocker_`; and the `XRPAmount`/`uint32_t` constructors extend the
* normal case for transactions with non-standard spending or sequence use.
*
* @note `potentialSpend_` does not include the fee; it represents additional
* XRP that may leave the account (e.g., the `sfSendMax` of a Payment).
*/
class TxConsequences
{
public:
/// Describes how the transaction affects subsequent
/// transactions
/** Categorises the impact a transaction has on subsequent queue entries. */
enum class Category {
/// Moves currency around, creates offers, etc.
/** Standard transaction: moves currency, creates offers, etc.
* Subsequent transactions from the same account may still queue. */
Normal = 0,
/// Affects the ability of subsequent transactions
/// to claim a fee. Eg. `SetRegularKey`
/** Key-management operation whose execution may invalidate the
* signatures on transactions already in the queue.
* Examples: `SetRegularKey` (key removal), `AccountDelete`,
* `SignerListSet`. TxQ enforces that a blocker cannot coexist
* with other queued transactions from the same account. */
Blocker
};
private:
/// Describes how the transaction affects subsequent
/// transactions
bool isBlocker_;
/// Transaction fee
XRPAmount fee_;
/// Does NOT include the fee.
/// Additional XRP the transaction may spend beyond the fee.
XRPAmount potentialSpend_;
/// SeqProxy of transaction.
SeqProxy seqProx_;
/// Number of sequences consumed.
std::uint32_t sequencesConsumed_;
public:
// Constructor if preflight returns a value other than tesSUCCESS.
// Asserts if tesSUCCESS is passed.
/** Construct a zeroed-out consequences for a failed preflight.
*
* All cost fields are set to zero so that no XRP estimate leaks from
* a rejected transaction into queue accounting.
*
* @param pfResult A non-success `NotTEC` code from preflight.
* Asserts if `tesSUCCESS` is passed.
*/
explicit TxConsequences(NotTEC pfResult);
/// Constructor if the STTx has no notable consequences for the TxQ.
/** Construct consequences for a transaction with no special queue impact.
*
* Fee and sequence are read from the transaction. `potentialSpend_` is
* zero (fee only) and `sequencesConsumed_` is 1 for a sequence-based
* transaction or 0 for a ticket.
*
* @param tx The transaction to summarise.
*/
explicit TxConsequences(STTx const& tx);
/// Constructor for a blocker.
/** Construct consequences for a blocker transaction.
*
* Sets `isBlocker_` when `category == Category::Blocker`, preventing
* the TxQ from accepting additional sequence-based transactions from the
* same account while this transaction is queued.
*
* @param tx The transaction to summarise.
* @param category `Category::Blocker` to mark as a blocker;
* `Category::Normal` behaves identically to the single-argument
* constructor.
*/
TxConsequences(STTx const& tx, Category category);
/// Constructor for an STTx that may consume more XRP than the fee.
/** Construct consequences for a transaction that may spend XRP beyond its fee.
*
* Used by `ConsequencesFactoryType::Custom` transactors such as
* `Payment` (via `sfSendMax`) and `OfferCreate` (via XRP `TakerGets`).
*
* @param tx The transaction to summarise.
* @param potentialSpend The maximum additional XRP (above the fee)
* the transaction might consume.
*/
TxConsequences(STTx const& tx, XRPAmount potentialSpend);
/// Constructor for an STTx that consumes more than the usual sequences.
/** Construct consequences for a transaction that burns multiple sequences.
*
* Used by `TicketCreate`, which reserves a range of future sequence slots
* in one transaction. `followingSeq()` accounts for the full range.
*
* @param tx The transaction to summarise.
* @param sequencesConsumed The total number of sequence slots consumed,
* including the transaction's own sequence number.
*/
TxConsequences(STTx const& tx, std::uint32_t sequencesConsumed);
/// Copy constructor
TxConsequences(TxConsequences const&) = default;
/// Copy assignment operator
TxConsequences&
operator=(TxConsequences const&) = default;
/// Move constructor
TxConsequences(TxConsequences&&) = default;
/// Move assignment operator
TxConsequences&
operator=(TxConsequences&&) = default;
/// Fee
/** Transaction fee in drops. */
[[nodiscard]] XRPAmount
fee() const
{
return fee_;
}
/// Potential Spend
/** Maximum XRP spend beyond the fee, in drops.
*
* Zero for most transactions. Non-zero for `Payment` and `OfferCreate`
* when the transaction carries an XRP spending cap (`sfSendMax` /
* XRP `TakerGets`).
*/
[[nodiscard]] XRPAmount const&
potentialSpend() const
{
return potentialSpend_;
}
/// SeqProxy
/** Sequence or ticket proxy identifying this transaction's queue slot. */
[[nodiscard]] SeqProxy
seqProxy() const
{
return seqProx_;
}
/// Sequences consumed
/** Number of sequence slots consumed by this transaction.
*
* Normally 1 for sequence-based transactions and 0 for ticket-based
* ones. `TicketCreate` returns the number of tickets it creates.
*/
[[nodiscard]] std::uint32_t
sequencesConsumed() const
{
return sequencesConsumed_;
}
/// Returns true if the transaction is a blocker.
/** Return true if this transaction may invalidate subsequent queue entries.
*
* Blockers are key-management operations (`SetRegularKey`, `AccountDelete`,
* `SignerListSet`) whose execution can change the account's signing
* authority, rendering the cached signatures of queued followers invalid.
*/
[[nodiscard]] bool
isBlocker() const
{
return isBlocker_;
}
// Return the SeqProxy that would follow this.
/** Return the first `SeqProxy` not consumed by this transaction.
*
* The TxQ uses this to find gaps in the queued sequence range for an
* account. For sequence-based transactions the result is `seqProxy + 1`;
* for tickets it is unchanged (tickets do not occupy sequence order); for
* `TicketCreate` it advances by `sequencesConsumed`.
*
* @return The `SeqProxy` that should immediately follow this transaction.
*/
[[nodiscard]] SeqProxy
followingSeq() const
{
@@ -133,32 +238,52 @@ public:
}
};
/** Describes the results of the `preflight` check
@note All members are const to make it more difficult
to "fake" a result without calling `preflight`.
@see preflight, preclaim, doApply, apply
*/
/** Immutable token produced by the `preflight` stage of the pipeline.
*
* Captures every input and output of ledger-agnostic transaction validation
* in a single, copy-constructible object. All fields are `const` to prevent
* callers from fabricating a result without going through `preflight`.
*
* The TxQ caches this object (`MaybeTx::pfResult`) and passes it to
* `preclaim` when an application slot opens. Because ledger rules can change
* between the two calls, `preclaim` compares `result.rules` against the
* current view and automatically re-runs `preflight` when they differ.
*
* @note Copy-assignment is deleted; copy-construction is allowed so the TxQ
* can store results in `std::optional`.
* @see preflight, preclaim, doApply, apply
*/
struct PreflightResult
{
public:
/// From the input - the transaction
/** The transaction that was checked. */
STTx const& tx;
/// From the input - the batch identifier, if part of a batch
/** Batch group identifier, present when this is an inner batch transaction. */
std::optional<uint256 const> const parentBatchId;
/// From the input - the rules
/** Amendment rules in effect at the time `preflight` ran.
* `preclaim` compares this against the current ledger's rules to detect
* stale results that must be re-validated. */
Rules const rules;
/// Consequences of the transaction
/** Worst-case XRP cost and queue-ordering impact, valid even when
* `ter != tesSUCCESS`. */
TxConsequences const consequences;
/// From the input - the flags
/** Processing flags supplied to `preflight`. */
ApplyFlags const flags;
/// From the input - the journal
/** Journal for diagnostics. */
beast::Journal const j;
/// Intermediate transaction result
/** Validation result. `NotTEC` — only `tem*`, `tel*`, or `tesSUCCESS`;
* `tec*` codes never appear at the preflight stage. */
NotTEC const ter;
/// Constructor
/** Construct from a preflight context and its computed result.
*
* @tparam Context A preflight context type exposing `tx`, `parentBatchId`,
* `rules`, `flags`, and `j` members.
* @param ctx The context object used during the preflight call.
* @param result A pair of `(NotTEC, TxConsequences)` returned by the
* transactor-specific preflight implementation.
*/
template <class Context>
PreflightResult(Context const& ctx, std::pair<NotTEC, TxConsequences> const& result)
: tx(ctx.tx)
@@ -172,39 +297,59 @@ public:
}
PreflightResult(PreflightResult const&) = default;
/// Deleted copy assignment operator
/** Deleted to prevent mutation of a cached preflight result. */
PreflightResult&
operator=(PreflightResult const&) = delete;
};
/** Describes the results of the `preclaim` check
@note All members are const to make it more difficult
to "fake" a result without calling `preclaim`.
@see preflight, preclaim, doApply, apply
*/
/** Immutable token produced by the `preclaim` stage of the pipeline.
*
* Captures every input and output of ledger-dependent transaction validation.
* All fields are `const` to prevent callers from fabricating a result without
* going through `preclaim`. The object is not cached by the TxQ; a fresh
* `PreclaimResult` is produced each time a transaction is about to be applied.
*
* The `likelyToClaimFee` flag is the primary gate for `doApply`: if false,
* the transaction will not be applied and no fee is charged.
*
* @note Copy-assignment is deleted; copy-construction is allowed so callers
* can store the result in a local before passing it to `doApply`.
* @see preflight, preclaim, doApply, apply
*/
struct PreclaimResult
{
public:
/// From the input - the ledger view
/** The ledger view against which the transaction was checked.
* `doApply` verifies that its view sequence matches this one; a mismatch
* (ledger advanced between preclaim and apply) returns `tefEXCEPTION`. */
ReadView const& view;
/// From the input - the transaction
/** The transaction that was checked. */
STTx const& tx;
/// From the input - the batch identifier, if part of a batch
/** Batch group identifier, present when this is an inner batch transaction. */
std::optional<uint256 const> const parentBatchId;
/// From the input - the flags
/** Processing flags supplied to `preclaim`. */
ApplyFlags const flags;
/// From the input - the journal
/** Journal for diagnostics. */
beast::Journal const j;
/// Intermediate transaction result
/** Validation result. Full `TER` range: `tes*`, `tec*`, `ter*`,
* `tef*`, or `tem*`. */
TER const ter;
/// Success flag - whether the transaction is likely to
/// claim a fee
/** True when the transaction will charge a fee and should be applied.
*
* Computed as `isTesSuccess(ter) || isTecClaimHardFail(ter, flags)`.
* When false, `doApply` returns immediately without mutating the ledger. */
bool const likelyToClaimFee{};
/// Constructor
/** Construct from a preclaim context and its computed `TER`.
*
* @tparam Context A preclaim context type exposing `view`, `tx`,
* `parentBatchId`, `flags`, and `j` members.
* @param ctx The context object used during the preclaim call.
* @param ter The `TER` produced by the transactor-specific preclaim
* implementation.
*/
template <class Context>
PreclaimResult(Context const& ctx, TER ter)
: view(ctx.view)
@@ -218,27 +363,36 @@ public:
}
PreclaimResult(PreclaimResult const&) = default;
/// Deleted copy assignment operator
/** Deleted to prevent mutation of a preclaim result. */
PreclaimResult&
operator=(PreclaimResult const&) = delete;
};
/** Gate a transaction based on static information.
The transaction is checked against all possible
validity constraints that do not require a ledger.
@param app The current running `Application`.
@param rules The `Rules` in effect at the time of the check.
@param tx The transaction to be checked.
@param flags `ApplyFlags` describing processing options.
@param j A journal.
@see PreflightResult, preclaim, doApply, apply
@return A `PreflightResult` object containing, among
other things, the `TER` code.
*/
/** Perform ledger-agnostic validation of a transaction (stage 1 of 3).
*
* Validates the transaction against all constraints that can be checked
* without ledger state: field presence and format, fee field sanity, signing
* key structure, flag validity, and any static rules imposed by the active
* amendments. This is the cheapest stage and can be parallelised across
* transactions.
*
* The resulting `PreflightResult` — including its `TxConsequences` — may be
* cached by the TxQ across ledger boundaries. If the ledger's amendment
* rules change before `preclaim` runs, `preclaim` will re-execute `preflight`
* automatically with the updated rules before proceeding.
*
* @param registry The service registry providing transactor implementations.
* @param rules Amendment rules in effect at the time of the check.
* @param tx The transaction to validate.
* @param flags `ApplyFlags` describing processing options (e.g. `tapRETRY`,
* `tapDRY_RUN`).
* @param j Journal for diagnostics.
* @return A `PreflightResult` whose `ter` is `tesSUCCESS` if the transaction
* is well-formed, or a `tem*`/`tel*` code if it is not. The
* `consequences` field is always populated regardless of `ter`.
*
* @see PreflightResult, preclaim, doApply, apply
*/
/** @{ */
PreflightResult
preflight(
@@ -248,6 +402,24 @@ preflight(
ApplyFlags flags,
beast::Journal j);
/** Perform ledger-agnostic validation of an inner batch transaction (stage 1 of 3).
*
* Identical to the standard overload but associates the result with a
* `parentBatchId`, which is stored in `PreflightResult::parentBatchId` and
* threaded through to `preclaim` and `doApply`. Used when an inner
* transaction belonging to a `Batch` group is validated independently.
*
* @param registry The service registry providing transactor implementations.
* @param rules Amendment rules in effect at the time of the check.
* @param parentBatchId The transaction ID of the enclosing `Batch` transaction.
* @param tx The inner transaction to validate.
* @param flags `ApplyFlags` describing processing options.
* @param j Journal for diagnostics.
* @return A `PreflightResult` for the inner transaction, carrying the batch
* association.
*
* @see PreflightResult, preclaim, doApply, apply
*/
PreflightResult
preflight(
ServiceRegistry& registry,
@@ -258,86 +430,99 @@ preflight(
beast::Journal j);
/** @} */
/** Gate a transaction based on static ledger information.
The transaction is checked against all possible
validity constraints that DO require a ledger.
If preclaim succeeds, then the transaction is very
likely to claim a fee. This will determine if the
transaction is safe to relay without being applied
to the open ledger.
"Succeeds" in this case is defined as returning a
`tes` or `tec`, since both lead to claiming a fee.
@pre The transaction has been checked
and validated using `preflight`
@param preflightResult The result of a previous
call to `preflight` for the transaction.
@param app The current running `Application`.
@param view The open ledger that the transaction
will attempt to be applied to.
@see PreclaimResult, preflight, doApply, apply
@return A `PreclaimResult` object containing, among
other things the `TER` code and the base fee value for
this transaction.
*/
/** Perform ledger-dependent validation of a transaction (stage 2 of 3).
*
* Checks all constraints that require read-only access to the current ledger
* state: account existence, sequence/ticket validity, fee sufficiency, and
* cryptographic signature verification. If `preflightResult.ter` is not
* `tesSUCCESS` this function is a no-op (the pipeline short-circuits).
*
* **Rules-change handling**: if the amendment rules embedded in
* `preflightResult` differ from those in `view` (because the ledger advanced
* since `preflight` ran), `preclaim` automatically re-executes `preflight`
* with the updated rules before proceeding. Callers do not need to detect or
* handle this case.
*
* **Security invariant**: every check up to and including signature
* verification must return a `NotTEC` code (never `tec*`). A `tec` before
* the signature check would charge a fee without authentication.
*
* A `tesSUCCESS` or fee-claiming `tec*` result (without `tapRETRY`) sets
* `PreclaimResult::likelyToClaimFee`, indicating the transaction is safe to
* relay to peers even before it is applied.
*
* @pre `preflightResult` was produced by a successful call to `preflight`.
* @param preflightResult The cached result of a prior `preflight` call.
* @param registry The service registry providing transactor implementations.
* @param view The open ledger the transaction will be applied to.
* @return A `PreclaimResult` with the full `TER` and a
* `likelyToClaimFee` boolean that gates `doApply`.
*
* @see PreclaimResult, preflight, doApply, apply
*/
PreclaimResult
preclaim(PreflightResult const& preflightResult, ServiceRegistry& registry, OpenView const& view);
/** Compute only the expected base fee for a transaction.
Base fees are transaction specific, so any calculation
needing them must get the base fee for each transaction.
No validation is done or implied by this function.
Caller is responsible for handling any exceptions.
Since none should be thrown, that will usually
mean terminating.
@param view The current open ledger.
@param tx The transaction to be checked.
@return The base fee.
*/
/** Return the minimum fee floor for this specific transaction type.
*
* Dispatches through the `with_txn_type` X-macro to the transaction-type's
* static `calculateBaseFee` override, so transactors that impose non-standard
* fees (e.g., multi-signers add one base fee per signer) return the correct
* floor. The TxQ calls this to compute each transaction's fee level —
* the ratio of its actual fee to the floor — for prioritisation.
*
* No validation of the transaction is performed.
*
* @param view The current open ledger (provides the network base fee).
* @param tx The transaction whose fee floor is required.
* @return The minimum acceptable fee in drops.
* @note Callers are responsible for handling any exceptions; in practice
* none should be thrown and an unhandled exception should terminate.
*/
XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
/** Return the minimum fee that an "ordinary" transaction would pay.
When computing the FeeLevel for a transaction the TxQ sometimes needs
the know what an "ordinary" or reference transaction would be required
to pay.
@param view The current open ledger.
@param tx The transaction so the correct multisigner count is used.
@return The base fee in XRPAmount.
*/
/** Return the fee floor for a generic "reference" transaction.
*
* Unlike `calculateBaseFee`, this function bypasses transactor-specific
* dispatch and calls `Transactor::calculateBaseFee` directly, returning what
* a plain transaction would pay. The TxQ uses this as the denominator when
* computing fee levels, ensuring a non-zero reference even when a particular
* transaction's own base fee is zero.
*
* @param view The current open ledger.
* @param tx The transaction (used only for multisigner count, not tx type).
* @return The reference base fee in drops.
*/
XRPAmount
calculateDefaultBaseFee(ReadView const& view, STTx const& tx);
/** Apply a prechecked transaction to an OpenView.
@pre The transaction has been checked
and validated using `preflight` and `preclaim`
@param preclaimResult The result of a previous
call to `preclaim` for the transaction.
@param registry The service registry.
@param view The open ledger that the transaction
will attempt to be applied to.
@see preflight, preclaim, apply
@return A pair with the `TER` and a `bool` indicating
whether or not the transaction was applied.
*/
/** Apply a pre-validated transaction to an open ledger (stage 3 of 3).
*
* Only runs if `preclaimResult.likelyToClaimFee` is true; otherwise returns
* the preclaim `TER` immediately with `applied = false`.
*
* Constructs an `ApplyContext` over `view`, instantiates the concrete
* transactor, and invokes `Transactor::operator()()`. All ledger mutations
* are staged in the context and are not committed to `view` until
* `ctx_.apply(result)` is called inside the transactor — meaning an early
* return, exception, or `tec*` result leaves the view in its original state
* (except for the fee and sequence deduction on `tec*` hard-fail paths).
*
* As a defensive check, `doApply` compares the view's ledger sequence against
* the one seen during `preclaim`; if they differ (the ledger advanced in the
* interim), it returns `tefEXCEPTION` without mutating anything.
*
* @pre `preclaimResult` was produced by a successful call to `preclaim` on
* the same transaction and the same open ledger.
* @param preclaimResult The result of a prior `preclaim` call.
* @param registry The service registry providing transactor implementations.
* @param view The mutable open ledger to apply the transaction to.
* @return An `ApplyResult` with the final `TER`, an `applied` flag indicating
* whether the transaction was committed, and optional `TxMeta`.
*
* @see preflight, preclaim, apply
*/
ApplyResult
doApply(PreclaimResult const& preclaimResult, ServiceRegistry& registry, OpenView& view);

View File

@@ -1,3 +1,7 @@
/** @file
* Declares the ValidAMM post-transaction invariant checker for AMM ledger state.
*/
#pragma once
#include <xrpl/beast/utility/Journal.h>
@@ -10,39 +14,231 @@
namespace xrpl {
/** Post-transaction invariant checker that validates AMM ledger state consistency.
*
* Part of the `InvariantChecks` tuple run after every transaction. Detects
* corrupt or impossible AMM state that correct code should never produce. Two
* categories of corruption are guarded against:
*
* 1. **Constant-product violation** — after any deposit or withdrawal the
* geometric mean of pool reserves must satisfy
* `sqrt(amount × amount2) ≥ lptAMMBalance`.
* 2. **Structural invariants** — create must set balances exactly equal to
* `sqrt(amount × amount2)`; bid must burn LP tokens without touching the
* pool; vote must change nothing at all; delete must remove the AMM object;
* DEX operations (Payment, OfferCreate, CheckCash) must not write to the
* AMM object.
*
* Enforcement is gated on the `fixAMMv1_3` amendment. Before the amendment
* activates, violations are logged but the checker still returns `true`,
* preserving backward compatibility. After activation, any violation returns
* `false` and triggers the invariant-failure escalation in `ApplyContext`.
*
* @note Each instance is constructed fresh per transaction by the invariant
* framework; there is no shared mutable state between transactions.
*
* @see InvariantChecks
* @see InvariantChecker_PROTOTYPE
*/
class ValidAMM
{
/** AMM pseudo-account ID, populated when an `ltAMM` object is modified. */
std::optional<AccountID> ammAccount_;
/** LP token supply recorded from the `ltAMM` object after the transaction. */
std::optional<STAmount> lptAMMBalanceAfter_;
/** LP token supply recorded from the `ltAMM` object before the transaction. */
std::optional<STAmount> lptAMMBalanceBefore_;
/** True if any AMM pool entry (`ltRIPPLE_STATE` with `lsfAMMNode`, or
* `ltACCOUNT_ROOT` with `sfAMMID`) was modified by the transaction. */
bool ammPoolChanged_{false};
public:
/** Controls whether an all-zeros pool state is accepted as valid.
*
* `No` requires all three balances (amount, amount2, LP supply) to be
* strictly positive — used for deposits where a zero pool is never
* legitimate. `Yes` additionally permits the simultaneous all-zeros case
* that occurs when the final withdrawal drains the pool completely.
*/
enum class ZeroAllowed : bool { No = false, Yes = true };
ValidAMM() = default;
/** Record AMM-relevant changes for a single modified ledger entry.
*
* Called by the invariant framework once per modified SLE before
* `finalize` is called. Deletions are ignored entirely. For surviving
* entries the method captures:
* - The AMM pseudo-account ID and post-transaction LP token balance when
* an `ltAMM` object is modified.
* - The pre-transaction LP token balance from the before-snapshot of an
* `ltAMM` object.
* - The `ammPoolChanged_` flag when an AMM pool entry is touched.
*
* @param isDelete True if the entry is being deleted; the call is a no-op
* in that case.
* @param before SLE snapshot from before the transaction, or nullptr for
* newly created entries.
* @param after SLE snapshot from after the transaction, or nullptr for
* deleted entries.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
/** Render a verdict on AMM state consistency after all entries are visited.
*
* Short-circuits (returns `true`) for any failure result that is neither
* `tesSUCCESS` nor `tecINCOMPLETE`. The `tecINCOMPLETE` carve-out exists
* because `AMMDelete` may return that code on a partial deletion pass and
* still requires validation.
*
* Dispatches to a per-transaction-type helper:
* - `ttAMM_CREATE` → `finalizeCreate` (exact equality check)
* - `ttAMM_DEPOSIT` → `finalizeDeposit` (geometric-mean ≥ LP supply,
* zero pool disallowed)
* - `ttAMM_WITHDRAW`, `ttAMM_CLAWBACK` → `finalizeWithdraw`
* (geometric-mean ≥ LP supply, all-zeros terminal state allowed)
* - `ttAMM_BID` → `finalizeBid` (pool unchanged, LP supply decreased)
* - `ttAMM_VOTE` → `finalizeVote` (pool and LP supply both unchanged)
* - `ttAMM_DELETE` → `finalizeDelete` (AMM object absent on success)
* - `ttCHECK_CASH`, `ttOFFER_CREATE`, `ttPAYMENT` → `finalizeDEX`
* (AMM object must not have been written)
*
* Whether a detected violation returns `false` or is merely logged depends
* on whether the `fixAMMv1_3` amendment is active in `view`.
*
* @param tx The transaction that was applied.
* @param result The TER result of the transaction.
* @param fee The XRP fee charged (unused by this checker).
* @param view Read-only ledger view used to re-read pool balances.
* @param j Journal for error logging.
* @return `true` if the invariant holds (or is unenforced); `false` if a
* violation is detected and `fixAMMv1_3` is active.
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
private:
/** Verify that a bid left the pool unchanged and burned LP tokens.
*
* @param enforce Whether to return `false` on violation (`fixAMMv1_3`).
* @param j Journal for error logging.
* @return `false` if the pool changed or LP supply did not strictly
* decrease to a positive value and `enforce` is true.
*/
[[nodiscard]] bool
finalizeBid(bool enforce, beast::Journal const&) const;
/** Verify that a vote left both the pool and the LP token supply unchanged.
*
* @param enforce Whether to return `false` on violation (`fixAMMv1_3`).
* @param j Journal for error logging.
* @return `false` if either the LP token balance or the pool changed and
* `enforce` is true.
*/
[[nodiscard]] bool
finalizeVote(bool enforce, beast::Journal const&) const;
/** Verify that an AMM create produced a valid initial pool state.
*
* Checks that the AMM object was actually created, that all three
* balances are strictly positive, and that the LP token supply equals
* `sqrt(amount × amount2)` exactly (using `ammLPTokens`).
*
* @param tx The `ttAMM_CREATE` transaction.
* @param view Ledger view used to read actual pool balances via
* `ammPoolHolds`.
* @param enforce Whether to return `false` on violation (`fixAMMv1_3`).
* @param j Journal for error logging.
* @return `false` if the AMM object is absent, any balance is non-positive,
* or `sqrt(amount × amount2) ≠ lptAMMBalance` and `enforce` is true.
*/
[[nodiscard]] bool
finalizeCreate(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const;
/** Verify that an AMM delete removed the AMM object.
*
* On `tesSUCCESS` the AMM object must be absent. On `tecINCOMPLETE`
* (partial deletion pass) the object must similarly not have been written.
*
* @param enforce Whether to return `false` on violation (`fixAMMv1_3`).
* @param res The TER result of the delete transaction.
* @param j Journal for error logging.
* @return `false` if `ammAccount_` is set (object was modified rather than
* deleted) and `enforce` is true.
*/
[[nodiscard]] bool
finalizeDelete(bool enforce, TER res, beast::Journal const&) const;
/** Verify the constant-product invariant after a deposit.
*
* Delegates to `generalInvariant` with `ZeroAllowed::No`; a zero pool
* is never legitimate after a deposit.
*
* @param tx The `ttAMM_DEPOSIT` transaction (provides `sfAsset`/`sfAsset2`).
* @param view Ledger view used to read actual pool balances.
* @param enforce Whether to return `false` on violation (`fixAMMv1_3`).
* @param j Journal for error logging.
* @return `false` if the AMM object was deleted or the constant-product
* invariant is violated and `enforce` is true.
*/
[[nodiscard]] bool
finalizeDeposit(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const;
/** Verify the constant-product invariant after a withdrawal or clawback.
*
* If `ammAccount_` is absent, the final withdrawal deleted the AMM — this
* is legitimate and the method returns `true` immediately. Otherwise
* delegates to `generalInvariant` with `ZeroAllowed::Yes`, which permits
* the simultaneous all-zeros terminal state.
*
* @param tx The `ttAMM_WITHDRAW` or `ttAMM_CLAWBACK` transaction.
* @param view Ledger view used to read actual pool balances.
* @param enforce Whether to return `false` on violation (`fixAMMv1_3`).
* @param j Journal for error logging.
* @return `false` if the constant-product invariant is violated and
* `enforce` is true; `true` if the AMM was legitimately deleted.
*/
// Includes clawback
[[nodiscard]] bool
finalizeWithdraw(STTx const&, ReadView const&, bool enforce, beast::Journal const&) const;
/** Verify that a DEX operation (Payment, OfferCreate, CheckCash) did not
* write to the AMM object.
*
* DEX transactions may route liquidity through AMM pools but must never
* mutate the `ltAMM` ledger object itself.
*
* @param enforce Whether to return `false` on violation (`fixAMMv1_3`).
* @param j Journal for error logging.
* @return `false` if `ammAccount_` is set (the AMM object was written)
* and `enforce` is true.
*/
[[nodiscard]] bool
finalizeDEX(bool enforce, beast::Journal const&) const;
/** Check the constant-product invariant for deposit and withdrawal paths.
*
* Re-reads actual pool balances from `view` via `ammPoolHolds`, then
* verifies:
* - All balances satisfy the `zeroAllowed` constraint (strictly positive,
* or simultaneously all-zeros when `ZeroAllowed::Yes`).
* - `sqrt(amount × amount2) ≥ lptAMMBalanceAfter_` (strong check), or
* within a relative tolerance of `1e-11` (weak fallback to absorb
* fixed-point rounding).
*
* Logging is unconditional on failure: the error line always includes the
* transaction hash, individual pool amounts, geometric mean, LP token
* balance, and relative deviation. The `enforce` flag is NOT consulted
* here — callers decide whether to propagate `false`.
*
* @param tx Transaction providing `sfAsset`/`sfAsset2` for pool lookup.
* @param view Ledger view used to read pool balances.
* @param zeroAllowed Whether an all-zeros pool is a valid terminal state.
* @param j Journal for error logging.
* @return `false` if either the balance constraint or the geometric-mean
* bound is violated; `true` otherwise.
*/
[[nodiscard]] bool
generalInvariant(STTx const&, ReadView const&, ZeroAllowed zeroAllowed, beast::Journal const&)
const;

View File

@@ -13,56 +13,208 @@
namespace xrpl {
/**
* @brief Invariant: frozen trust line balance change is not allowed.
* @brief Invariant checker that prevents token transfers across frozen trust lines.
*
* We iterate all affected trust lines and ensure that they don't have
* unexpected change of balance if they're frozen.
* Registered in the `InvariantChecks` tuple and executed after every transaction,
* including failed ones. Implements the two-phase `InvariantChecker_PROTOTYPE`
* contract: `visitEntry()` collects trust line balance changes, and `finalize()`
* evaluates freeze rules across the full set of collected changes.
*
* A single-pass approach is insufficient because a trust line's freeze state
* alone does not determine whether a transfer is forbidden — the check must be
* performed end-to-end, comparing both sides of the transfer across potentially
* different freeze states. The two-phase design makes this possible.
*
* Enforcement is gated on `featureDeepFreeze`: before the amendment activates,
* violations are logged at `fatal` severity and fire `XRPL_ASSERT` in debug
* builds, but do not invalidate the transaction in release builds. This
* provides early warning without a consensus break, and the single `enforce`
* variable is the only change needed if a fix amendment is introduced.
*
* @note `AMMClawback` transactions hold the `OverrideFreeze` privilege and may
* move funds across individually frozen or deep-frozen trust lines, but not
* when the issuer has set a global freeze.
*/
class TransfersNotFrozen
{
/**
* @brief A single trust line's participation in a balance change.
*
* `balanceChangeSign` is +1 if the balance increased (receiving) or -1 if
* it decreased (sending), from the current issuer's perspective.
*/
struct BalanceChange
{
std::shared_ptr<SLE const> const line;
int const balanceChangeSign;
};
/**
* @brief All balance changes for a single issuer's token, split by direction.
*
* When both `senders` and `receivers` are non-empty the transfer is
* holder-to-holder and freeze rules apply. If either is empty the tokens
* are moving to or from the issuer directly, which is always permitted.
*/
struct IssuerChanges
{
std::vector<BalanceChange> senders;
std::vector<BalanceChange> receivers;
};
/** Balance changes keyed by `Issue` (currency + issuer account). */
using ByIssuer = std::map<Issue, IssuerChanges>;
ByIssuer balanceChanges_;
/**
* @brief Cache of `ltACCOUNT_ROOT` SLEs observed during `visitEntry()`.
*
* `findIssuer()` checks this cache before falling back to `view.read()`,
* avoiding a redundant ledger lookup in the common case where the issuer
* account was already touched by the transaction.
*/
std::map<AccountID, std::shared_ptr<SLE const> const> possibleIssuers_;
public:
/**
* @brief Collect balance changes from a single modified ledger entry.
*
* Skips non-trust-line entries, but caches any `ltACCOUNT_ROOT` entries
* in `possibleIssuers_` for later use by `findIssuer()`. For trust lines,
* records the net balance change under both sides' issuer keys so that
* `finalize()` sees issuer-relative directionality regardless of which
* side of the trust line an account sits on.
*
* @param isDelete True if the entry is being deleted; causes the final
* balance to be treated as zero so that trust-line deletion cannot
* transfer frozen funds to a third party.
* @param before SLE state before the transaction; null for newly-created
* trust lines (balance treated as zero in that case).
* @param after SLE state after the transaction; never null even on delete.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
/**
* @brief Validate that no collected balance change violates freeze rules.
*
* Iterates over every issuer in `balanceChanges_` and delegates to
* `validateIssuerChanges()`. Only holder-to-holder transfers (both
* `senders` and `receivers` non-empty) are evaluated for freeze
* violations; issuance and redemption are unconditionally allowed.
*
* Enforcement is controlled by `featureDeepFreeze`: when disabled,
* violations are logged and asserted in debug builds but do not cause the
* method to return `false`.
*
* @param tx The transaction being applied (used for privilege checks and
* log correlation).
* @param ter The transaction result (unused; invariant runs regardless).
* @param fee The fee charged (unused by this checker).
* @param view The post-transaction ledger view, used to look up issuers
* not cached in `possibleIssuers_`.
* @param j Journal for diagnostic logging.
* @return `true` if no frozen-fund movement is detected (or if
* `featureDeepFreeze` is disabled and a violation is found).
* `false` if a violation is found and the amendment is active.
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
private:
/**
* @brief Return true if the entry should be processed for freeze checks.
*
* Caches `ltACCOUNT_ROOT` entries in `possibleIssuers_` as a side effect.
* Returns `true` only for `ltRIPPLE_STATE` (trust line) entries where the
* type has not changed between `before` and `after`.
*
* @param before Pre-transaction SLE; null for newly-created entries.
* @param after Post-transaction SLE; must not be null.
* @return `true` if the entry is a trust line eligible for balance-change
* recording.
*/
bool
isValidEntry(std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/**
* @brief Compute the net balance change for a trust line.
*
* When `before` is null (trust line created mid-transaction, e.g., by a
* payment crossing offers), the pre-existing balance is treated as zero.
* When `isDelete` is true, the final balance is treated as zero so that
* deletion cannot bypass frozen-transfer restrictions.
*
* @param before Pre-transaction SLE; null if the trust line was just created.
* @param after Post-transaction SLE.
* @param isDelete True when the entry is being deleted.
* @return Signed `STAmount` representing `balanceAfter - balanceBefore`.
*/
static STAmount
calculateBalanceChange(
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after,
bool isDelete);
/**
* @brief Insert a single `BalanceChange` into `balanceChanges_` under the given issue.
*
* Routes the change to `IssuerChanges::senders` when `balanceChangeSign < 0`,
* or `IssuerChanges::receivers` when positive.
*
* @param issue The currency+issuer key for the change.
* @param change The trust line and direction of the change.
*/
void
recordBalance(Issue const& issue, BalanceChange change);
/**
* @brief Record a trust line balance change from both sides' issuer perspectives.
*
* Because XRPL trust line balances are stored from the low account's
* perspective, the same physical balance change must be inserted twice —
* once for the high-limit account's issuer (using the raw sign) and once
* for the low-limit account's issuer (with the sign inverted) — so that
* `validateIssuerChanges()` sees consistent directionality from each
* issuer's point of view.
*
* @param after Post-transaction trust line SLE.
* @param balanceChange Net balance change; must be non-zero.
*/
void
recordBalanceChanges(std::shared_ptr<SLE const> const& after, STAmount const& balanceChange);
/**
* @brief Look up an issuer's `AccountRoot` SLE, using the local cache first.
*
* Checks `possibleIssuers_` (populated during `visitEntry()`) before
* falling back to `view.read()`, so that issuers already modified by the
* transaction do not require an additional ledger lookup.
*
* @param issuerID The account ID to look up.
* @param view The post-transaction read-only ledger view.
* @return The issuer's SLE, or nullptr if not found.
*/
std::shared_ptr<SLE const>
findIssuer(AccountID const& issuerID, ReadView const& view);
/**
* @brief Validate all balance changes for one issuer's token.
*
* Unconditionally allows issuance (no senders) and redemption (no
* receivers). For holder-to-holder transfers, checks every sender and
* receiver trust line against the issuer's global freeze flag and the
* per-line freeze/deep-freeze flags via `validateFrozenState()`.
*
* @param issuer The issuer's `AccountRoot` SLE; must not be null.
* @param changes All senders and receivers for this issuer's token.
* @param tx The transaction being applied.
* @param j Journal for diagnostic logging.
* @param enforce When `false`, violations are logged but do not cause the
* method to return `false` (pre-`featureDeepFreeze` mode).
* @return `true` if all changes are permitted; `false` on a freeze violation
* when `enforce` is `true`.
*/
static bool
validateIssuerChanges(
std::shared_ptr<SLE const> const& issuer,
@@ -71,6 +223,28 @@ private:
beast::Journal const& j,
bool enforce);
/**
* @brief Check whether a single trust line balance change violates freeze rules.
*
* Evaluates three layered freeze conditions in order:
* 1. **Global freeze** (`lsfGlobalFreeze` on the issuer): freezes all trust
* lines with that issuer; no override is possible.
* 2. **Deep freeze** (`lsfLowDeepFreeze`/`lsfHighDeepFreeze`): blocks all
* transfers regardless of direction; overrideable by `AMMClawback`.
* 3. **Standard freeze** (`lsfLowFreeze`/`lsfHighFreeze`): only blocks
* outgoing transfers (`balanceChangeSign < 0`); overrideable by `AMMClawback`.
*
* @param change The trust line and direction of the balance change.
* @param high `true` if the issuer is the high-limit account on the trust
* line (determines which freeze flag bits to examine).
* @param tx The transaction being applied (for privilege and log checks).
* @param j Journal for diagnostic logging.
* @param enforce When `false`, violations log and assert but return `true`
* (pre-`featureDeepFreeze` behavior).
* @param globalFreeze `true` if the issuer's `lsfGlobalFreeze` flag is set.
* @return `true` if the transfer is permitted; `false` if it violates a
* freeze rule and `enforce` is `true`.
*/
static bool
validateFrozenState(
BalanceChange const& change,

View File

@@ -1,3 +1,30 @@
/** @file
* Central registry for the XRPL transaction invariant-checking system.
*
* After every transaction (whether it succeeded or failed), the invariant
* framework scans all modified ledger entries and verifies that the result
* is internally consistent. If any check fails, the transaction is rolled
* back to a fee-only charge (`tecINVARIANT_FAILED`), or excluded from the
* ledger entirely (`tefINVARIANT_FAILED`) if even that minimal commit
* breaks an invariant.
*
* This file declares the core checker classes and aggregates every checker
* — including those from sibling headers such as `FreezeInvariant.h`,
* `NFTInvariant.h`, `AMMInvariant.h`, and `VaultInvariant.h` — into the
* `InvariantChecks` tuple. That tuple is the single source of truth for
* which invariants exist. Dispatch in `ApplyContext::checkInvariantsHelper`
* iterates the tuple at compile time via `std::index_sequence`; no virtual
* calls are involved.
*
* To add a new invariant: declare its class (here or in a new sibling
* header), then append it to `InvariantChecks`. No other registration is
* needed.
*
* @see InvariantChecker_PROTOTYPE for the duck-typed interface every
* checker must satisfy.
* @see InvariantCheckPrivilege.h for the `Privilege` bitmask and
* `hasPrivilege()` used by several checkers.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
@@ -100,71 +127,121 @@ public:
};
#endif
/**
* @brief Invariant: We should never charge a transaction a negative fee or a
* fee that is larger than what the transaction itself specifies.
/** Invariant: the fee charged must be non-negative, less than the total XRP
* supply, and no greater than the fee the transaction authorized.
*
* We can, in some circumstances, charge less.
* Undercharging is permitted (e.g., when an account's balance is clamped by
* `reset()`), but overcharging or a negative fee is always a bug.
* `visitEntry` is a no-op; all logic is in `finalize`.
*/
class TransactionFeeCheck
{
public:
/** No-op: fee validation needs no per-entry state. */
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
/** Verify the fee is in `[0, sfFee]` and strictly below `INITIAL_XRP`.
*
* @param fee The fee actually deducted from the sending account.
* @return `true` if the fee is valid; `false` with a `fatal` log entry
* on any violation.
*/
static bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
finalize(STTx const&, TER const, XRPAmount const fee, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: A transaction must not create XRP and should only destroy
* the XRP fee.
/** Invariant: a transaction must not create XRP; it may only destroy XRP
* equal to the fee charged.
*
* We iterate through all account roots, payment channels and escrow entries
* that were modified and calculate the net change in XRP caused by the
* transactions.
* Accumulates the net drop-level change across account roots, payment
* channels, and escrows. Payment channel net is `sfAmount - sfBalance`
* (unclaimed funds). Escrow and pay-channel deletions skip the `after`
* side because those entries' amount fields are not adjusted at deletion
* time — only the pre-deletion value is subtracted.
*/
class XRPNotCreated
{
std::int64_t drops_ = 0;
public:
/** Accumulate XRP delta for account roots, payment channels, and escrows.
*
* @param isDelete `true` when the entry is being deleted; suppresses the
* `after` contribution for pay-channel and escrow entries.
* @param before Entry state before the transaction; null for new entries.
* @param after Entry state after the transaction; null for deleted entries.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
/** Verify net XRP change equals exactly `-fee`.
*
* @return `true` if the net drop delta equals the negative of the fee;
* `false` with a `fatal` log entry if XRP was created or the net
* change does not match the fee.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/**
* @brief Invariant: we cannot remove an account ledger entry
/** Invariant: account roots may only be deleted by transactions that hold
* the correct deletion privilege.
*
* We iterate all account roots that were modified, and ensure that any that
* were present before the transaction was applied continue to be present
* afterwards unless they were explicitly deleted by a successful
* AccountDelete transaction.
* Transactions with `MustDeleteAcct` (e.g., `AccountDelete`, `AMMDelete`)
* must delete exactly one account root on success. Transactions with
* `MayDeleteAcct` (e.g., `AMMWithdraw`, `AMMClawback`) may delete at most
* one. All other transactions must delete zero.
*
* @note Privilege semantics are defined in `InvariantCheckPrivilege.h` and
* encoded per-transaction-type in `transactions.macro`.
*/
class AccountRootsNotDeleted
{
std::uint32_t accountsDeleted_ = 0;
public:
/** Count deleted account roots.
*
* @param isDelete `true` when the entry is being deleted.
* @param before Entry state before modification; null for new entries.
* @param after Unused by this checker.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
/** Verify deletion count matches the transaction's privilege.
*
* @return `true` if the deletion count is consistent with the privilege
* held by the transaction; `false` with a `fatal` log entry otherwise.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/**
* @brief Invariant: a deleted account must not have any objects left
/** Invariant: a deleted account must leave no orphaned ledger objects behind.
*
* We iterate all deleted account roots, and ensure that there are no
* objects left that are directly accessible with that account's ID.
* For every deleted account root, verifies: the post-deletion balance is
* zero, the owner count is zero, and no directly-keyed objects (trust
* lines, escrows, offers, NFT pages, pay channels, etc.) remain in the
* ledger. For pseudo-accounts (AMM, Vault, etc.), the linked protocol
* object must also be absent.
*
* There should only be one deleted account, but that's checked by
* AccountRootsNotDeleted. This invariant will handle multiple deleted account
* roots without a problem.
* The `before` snapshot is used to locate linked objects even when an
* ID field is cleared during deletion; `after` is used only for
* post-deletion balance and owner-count assertions.
*
* @note This checker is amendment-gated: violations are always logged at
* `fatal` level and trigger a debug-build `XRPL_ASSERT`, but
* `finalize` only returns `false` (blocking the transaction) when
* `featureInvariantsV1_1`, `featureSingleAssetVault`, or
* `featureLendingProtocol` is enabled.
*/
class AccountRootsDeletedClean
{
@@ -176,35 +253,63 @@ class AccountRootsDeletedClean
std::vector<std::pair<std::shared_ptr<SLE const>, std::shared_ptr<SLE const>>> accountsDeleted_;
public:
/** Record each deleted account root's before/after snapshots.
*
* @param isDelete `true` when the entry is being deleted.
* @param before Entry state before deletion; used to locate linked objects.
* @param after Entry state after deletion; used to check balance/owner count.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
/** Scan the ledger for objects orphaned by each deleted account.
*
* @return `true` if all deleted accounts are clean; `false` with `fatal`
* log entries for each violation when the gating amendment is active.
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariant: An account XRP balance must be in XRP and take a value
* between 0 and INITIAL_XRP drops, inclusive.
/** Invariant: every account root's XRP balance must be a native amount in
* `[0, INITIAL_XRP]` (inclusive) both before and after the transaction.
*
* We iterate all account roots modified by the transaction and ensure that
* their XRP balances are reasonable.
* The `bad_` flag is sticky — once set by any visited entry it remains set
* regardless of subsequent entries.
*/
class XRPBalanceChecks
{
bool bad_ = false;
public:
/** Set `bad_` if any visited account root has an out-of-range balance.
*
* @param before Entry state before modification; null for new entries.
* @param after Entry state after modification; null for deleted entries.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** Report failure if any balance violation was detected.
*
* @return `true` if all visited balances were valid; `false` with a
* `fatal` log entry if any balance was out of range.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/**
* @brief Invariant: corresponding modified ledger entries should match in type
* and added entries should be a valid type.
/** Invariant: modified entries must not change `LedgerEntryType`, and newly
* created entries must be a type recognized by `ledger_entries.macro`.
*
* Catches two distinct corruption scenarios: a modification that mutates
* the type field of an existing object (`typeMismatch_`), and creation of
* an object with an unregistered type tag (`invalidTypeAdded_`). Both
* flags are checked independently in `finalize` so each failure produces
* its own diagnostic.
*/
class LedgerEntryTypesMatch
{
@@ -212,89 +317,144 @@ class LedgerEntryTypesMatch
bool invalidTypeAdded_ = false;
public:
/** Detect type changes on modified entries and unrecognized types on new ones.
*
* @param before Entry state before modification; null for new entries.
* @param after Entry state after modification; null for deleted entries.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** Report any type mismatch or unknown type that was detected.
*
* @return `true` if no type anomaly was seen; `false` with separate
* `fatal` log entries for each distinct violation kind.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/**
* @brief Invariant: Trust lines using XRP are not allowed.
/** Invariant: no trust line (`ltRIPPLE_STATE`) may reference XRP as its asset.
*
* We iterate all the trust lines created by this transaction and ensure
* that they are against a valid issuer.
* XRP is natively held in account roots; a trust line denominated in XRP
* has no valid semantics and indicates a bug. Both `sfLowLimit` and
* `sfHighLimit` are checked so the asset comparison does not rely solely
* on the `native()` predicate.
*/
class NoXRPTrustLines
{
bool xrpTrustLine_ = false;
public:
/** Set the violation flag if any trust line's asset is XRP.
*
* Only inspects the `after` snapshot; pre-existing invalid state
* cannot be introduced by this transaction.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const& after);
/** @return `true` if no XRP trust lines were seen; `false` with a
* `fatal` log entry otherwise.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/**
* @brief Invariant: Trust lines with deep freeze flag are not allowed if normal
* freeze flag is not set.
/** Invariant: `lsfLowDeepFreeze`/`lsfHighDeepFreeze` may only be set when
* the corresponding `lsfLowFreeze`/`lsfHighFreeze` flag is also set.
*
* We iterate all the trust lines created by this transaction and ensure
* that they don't have deep freeze flag set without normal freeze flag set.
* Deep freeze is a stricter form of freeze; it is undefined behaviour to
* deep-freeze a trust line that is not already frozen.
*/
class NoDeepFreezeTrustLinesWithoutFreeze
{
bool deepFreezeWithoutFreeze_ = false;
public:
/** Set the violation flag if any trust line has deep freeze without freeze.
*
* Only inspects the `after` snapshot; checks both low and high sides
* of the trust line independently.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const& after);
/** @return `true` if no deep-freeze-without-freeze condition was seen;
* `false` with a `fatal` log entry otherwise.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/**
* @brief Invariant: offers should be for non-negative amounts and must not
* be XRP to XRP.
/** Invariant: all offer entries (`ltOFFER`) must have non-negative amounts
* and must not exchange XRP for XRP.
*
* Examine all offers modified by the transaction and ensure that there are
* no offers which contain negative amounts or which exchange XRP for XRP.
* Both the `before` and `after` snapshots of each offer are inspected, so
* a modification that corrupts an existing offer is caught alongside a
* newly created bad offer.
*/
class NoBadOffers
{
bool bad_ = false;
public:
/** Set the violation flag if any offer has negative amounts or is XRP↔XRP.
*
* @param before Entry state before modification; null for new entries.
* @param after Entry state after modification; null for deleted entries.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** @return `true` if no bad offers were seen; `false` with a `fatal`
* log entry otherwise.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/**
* @brief Invariant: an escrow entry must take a value between 0 and
* INITIAL_XRP drops exclusive.
/** Invariant: escrow and MPToken amounts must be strictly positive and
* within protocol bounds.
*
* Despite its name this checker is not limited to escrows. It validates:
* - `ltESCROW` amounts: XRP in `(0, INITIAL_XRP)`, IOU > 0 with a valid
* currency, MPT in `(0, MAX_MP_TOKEN_AMOUNT]`.
* - `ltMPTOKEN_ISSUANCE` `sfOutstandingAmount` and optional `sfLockedAmount`:
* both in `[0, MAX_MP_TOKEN_AMOUNT]`, with `lockedAmount ≤ outstandingAmount`.
* - `ltMPTOKEN` `sfMPTAmount` and optional `sfLockedAmount`:
* both in `[0, MAX_MP_TOKEN_AMOUNT]`.
*/
class NoZeroEscrow
{
bool bad_ = false;
public:
/** Set the violation flag if any escrow or MPToken entry has an invalid amount.
*
* @param before Entry state before modification; null for new entries.
* @param after Entry state after modification; null for deleted entries.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** @return `true` if all amounts were in range; `false` with a `fatal`
* log entry otherwise.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/**
* @brief Invariant: a new account root must be the consequence of a payment,
* must have the right starting sequence, and the payment
* may not create more than one new account root.
/** Invariant: at most one account root may be created per transaction, it
* must originate from a transaction with the correct creation privilege,
* and it must start with the ledger-mandated sequence number and flags.
*
* Normal accounts start with `sfSequence == view.seq()` (current ledger
* sequence). Pseudo-accounts (AMM, Vault, LoanBroker) start with
* `sfSequence == 0` and must have exactly `lsfDisableMaster |
* lsfDefaultRipple | lsfDepositAuth` set. Creating a pseudo-account
* requires the `CreatePseudoAcct` privilege; creating a normal account
* requires `CreateAcct`.
*/
class ValidNewAccountRoot
{
@@ -304,20 +464,31 @@ class ValidNewAccountRoot
std::uint32_t flags_ = 0;
public:
/** Record creation details for newly added account roots.
*
* @param before Null for newly created entries (creation only).
* @param after The new account root's state.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** Verify creation count, privilege, starting sequence, and flags.
*
* @return `true` if the account was created validly; `false` with a
* `fatal` log entry for each distinct violation.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/**
* @brief Invariant: Token holder's trustline balance cannot be negative after
* Clawback.
/** Invariant: a `ttCLAWBACK` transaction must not modify more than one
* trust line or MPToken entry, and must leave the holder's balance
* non-negative.
*
* We iterate all the trust lines affected by this transaction and ensure
* that no more than one trustline is modified, and also holder's balance is
* non-negative.
* On success: at most one trust line and at most one MPToken modified;
* the holder's resulting balance ≥ 0. On failure: neither trust lines
* nor MPTokens may have been modified at all. For all other transaction
* types `finalize` is a no-op.
*/
class ValidClawback
{
@@ -325,39 +496,82 @@ class ValidClawback
std::uint32_t mptokensChanged_ = 0;
public:
/** Count touched trust lines and MPToken entries.
*
* @param before Entry state before modification; counts only when non-null.
* @param after Unused by this checker.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** Validate clawback constraints if the transaction is `ttCLAWBACK`.
*
* @return `true` for non-clawback transactions unconditionally; for
* clawback, `true` only if modification counts and holder balance
* pass all checks; `false` with a `fatal` log entry otherwise.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/**
* @brief Invariants: Pseudo-accounts have valid and consistent properties
/** Invariant: pseudo-accounts must maintain their structural invariants
* after every modification.
*
* Pseudo-accounts have certain properties, and some of those properties are
* unique to pseudo-accounts. Check that all pseudo-accounts are following the
* rules, and that only pseudo-accounts look like pseudo-accounts.
* Any account root with a pseudo-account discriminator field (e.g.,
* `sfAMMID`, `sfVaultID`) or with `sfSequence == 0` is treated as a
* pseudo-account and checked for:
* 1. Exactly one pseudo-account discriminator field present.
* 2. `sfSequence` unchanged from before.
* 3. Flags `lsfDisableMaster | lsfDefaultRipple | lsfDepositAuth` all set.
* 4. `sfRegularKey` absent.
*
* All violations are collected into `errors_` and logged individually.
* Deletion events are ignored (covered by `AccountRootsDeletedClean`).
*
* @note Enforcement is gated on `featureSingleAssetVault`: violations log
* and fire a debug `XRPL_ASSERT` unconditionally but only return
* `false` when the amendment is active.
*/
class ValidPseudoAccounts
{
std::vector<std::string> errors_;
public:
/** Accumulate errors for any pseudo-account that violates its invariants.
*
* @param isDelete Deletion events are skipped entirely.
* @param before Entry state before modification; used for sequence comparison.
* @param after Current account root state to validate.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
/** Log all accumulated errors and fail if the gating amendment is active.
*
* @return `true` if no errors were accumulated or the amendment is not
* yet active; `false` with `fatal` log entries for each violation
* when `featureSingleAssetVault` is enabled.
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
/**
* @brief Invariants: Some fields are unmodifiable
/** Invariant: certain fields on existing ledger entries must never be
* altered once the entry is created.
*
* Check that any fields specified as unmodifiable are not modified when the
* object is modified. Creation and deletion are ignored.
* Creation and deletion are ignored (those events have their own checkers).
* For `ltLOAN_BROKER` and `ltLOAN` entries, a broad set of origination
* fields (`sfInterestRate`, `sfBorrower`, `sfStartDate`, etc.) are
* immutable. For all other entry types, `sfLedgerEntryType` and
* `sfLedgerIndex` are universally immutable.
*
* @note Enforcement is gated on `featureLendingProtocol`: violations log
* and fire a debug `XRPL_ASSERT` unconditionally but only return
* `false` when that amendment is active — even for the universally
* immutable fields, because that is when this checker was introduced.
*/
class NoModifiedUnmodifiableFields
{
@@ -365,15 +579,43 @@ class NoModifiedUnmodifiableFields
std::set<std::pair<SLE::const_pointer, SLE::const_pointer>> changedEntries_;
public:
/** Record every modification (non-delete) for inspection in `finalize`.
*
* @param isDelete Deletion and creation events are skipped.
* @param before Entry state before modification.
* @param after Entry state after modification.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(
bool isDelete,
std::shared_ptr<SLE const> const& before,
std::shared_ptr<SLE const> const& after);
/** Scan all recorded modifications for immutable-field changes.
*
* @return `true` if no immutable field was altered; `false` with a
* `fatal` log entry when a violation is found and
* `featureLendingProtocol` is active.
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};
// additional invariant checks can be declared above and then added to this
// tuple
/** Complete set of invariant checkers run after every transaction.
*
* `ApplyContext::checkInvariantsHelper` unpacks this tuple at compile time
* via `std::index_sequence`, calling `visitEntry` on each element for
* every modified ledger entry and then `finalize` on each element once.
* Results from `finalize` are collected into a `std::array` rather than
* short-circuited with `&&`, so every failing checker produces its own
* `fatal`-level log entry before the combined verdict is returned.
*
* To add a new invariant: declare its class (here or in a sibling header),
* include that header above, and append the class to this list.
*
* @see InvariantChecker_PROTOTYPE for the interface each element must satisfy.
* @see ApplyContext::checkInvariants for the dispatch entry point.
*/
using InvariantChecks = std::tuple<
TransactionFeeCheck,
AccountRootsNotDeleted,
@@ -401,13 +643,15 @@ using InvariantChecks = std::tuple<
ValidVault,
ValidMPTPayment>;
/**
* @brief get a tuple of all invariant checks
/** Construct a default-initialized `InvariantChecks` tuple.
*
* @return std::tuple of instances that implement the required invariant check
* methods
* Called by `ApplyContext::checkInvariantsHelper` at the start of each
* invariant-checking pass. Each checker accumulates state via `visitEntry`
* across all modified ledger entries before `finalize` renders its verdict.
*
* @see xrpl::InvariantChecker_PROTOTYPE
* @return A value-initialized tuple containing one instance of every
* registered invariant checker, ready for a new pass.
* @see InvariantChecker_PROTOTYPE
*/
inline InvariantChecks
getInvariantChecks()

View File

@@ -1,3 +1,20 @@
/** @file
* Per-transaction-type privilege bitmasks used by the invariant checker.
*
* Each transaction type published in `transactions.macro` carries a
* `Privilege` bitmask that declares what ledger mutations it is permitted to
* perform. Invariant checkers call `hasPrivilege()` in their `finalize()`
* methods to enforce that no forbidden mutation occurred.
*
* @note The `assert(enforce)` pattern: several invariant checker files
* contain `XRPL_ASSERT(enforce, ...)` where `enforce` is `true` only
* when the relevant amendment is active. This is a deliberate two-layer
* defence strategy. Invariants should never fire in production; the
* assert is aimed at developers — it fires in debug/unit-test builds
* when code violates an invariant without the protecting amendment being
* enabled, catching bugs as early as possible while remaining invisible
* to validators in release builds.
*/
#pragma once
#include <xrpl/protocol/STTx.h>
@@ -6,49 +23,114 @@
namespace xrpl {
/*
assert(enforce)
There are several asserts (or XRPL_ASSERTs) in invariant check files that check
a variable named `enforce` when an invariant fails. At first glance, those
asserts may look incorrect, but they are not.
Those asserts take advantage of two facts:
1. `asserts` are not (normally) executed in release builds.
2. Invariants should *never* fail, except in tests that specifically modify
the open ledger to break them.
This makes `assert(enforce)` sort of a second-layer of invariant enforcement
aimed at _developers_. It's designed to fire if a developer writes code that
violates an invariant, and runs it in unit tests or a develop build that _does
not have the relevant amendments enabled_. It's intentionally a pain in the neck
so that bad code gets caught and fixed as early as possible.
*/
// Bitwise flags, 86 files, used in macros files
/** Bitmask of operations that a transaction type is permitted to perform.
*
* Each enumerator represents one class of ledger mutation. The `must*`
* variants mean the transaction is *required* to perform that mutation on
* success; the `may*` variants mean it is *permitted* but not required.
* Invariant checkers use this distinction to differentiate structural
* violations (wrong count) from policy violations (wrong type).
*
* Privilege sets for each transaction type are declared in
* `transactions.macro` and composed with `operator|`. Unknown or
* deprecated transaction types carry `NoPriv`.
*
* @see hasPrivilege()
* @see transactions.macro
*/
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
enum Privilege {
NoPriv = 0x0000, // The transaction can not do any of the enumerated operations
CreateAcct = 0x0001, // The transaction can create a new ACCOUNT_ROOT object.
CreatePseudoAcct = 0x0002, // The transaction can create a pseudo account,
// which implies createAcct
MustDeleteAcct = 0x0004, // The transaction must delete an ACCOUNT_ROOT object
MayDeleteAcct = 0x0008, // The transaction may delete an ACCOUNT_ROOT
// object, but does not have to
OverrideFreeze = 0x0010, // The transaction can override some freeze rules
ChangeNftCounts = 0x0020, // The transaction can mint or burn an NFT
CreateMptIssuance = 0x0040, // The transaction can create a new MPT issuance
DestroyMptIssuance = 0x0080, // The transaction can destroy an MPT issuance
MustAuthorizeMpt = 0x0100, // The transaction MUST create or delete an MPT
// object (except by issuer)
MayAuthorizeMpt = 0x0200, // The transaction MAY create or delete an MPT
// object (except by issuer)
MayDeleteMpt = 0x0400, // The transaction MAY delete an MPT object. May not create.
MustModifyVault = 0x0800, // The transaction must modify, delete or create, a vault
MayModifyVault = 0x1000, // The transaction MAY modify, delete or create, a vault
MayCreateMpt = 0x2000, // The transaction MAY create an MPT object, except for issuer.
/** The transaction may not perform any of the enumerated operations. */
NoPriv = 0x0000,
/** The transaction may create a new `ACCOUNT_ROOT` object. */
CreateAcct = 0x0001,
/** The transaction may create a pseudo-account `ACCOUNT_ROOT`.
*
* Implies `CreateAcct`; checkers that permit `CreatePseudoAcct` also
* accept `CreateAcct`, but the converse is not true — a transaction
* with only `CreateAcct` must not create a pseudo-account.
*/
CreatePseudoAcct = 0x0002,
/** The transaction must delete exactly one `ACCOUNT_ROOT` on success.
*
* Example: `ttACCOUNT_DELETE`, `ttAMM_DELETE`.
*/
MustDeleteAcct = 0x0004,
/** The transaction may delete one `ACCOUNT_ROOT`, but is not required to.
*
* Example: `ttAMM_WITHDRAW`, `ttAMM_CLAWBACK` (when LP-token supply
* reaches zero).
*/
MayDeleteAcct = 0x0008,
/** The transaction may bypass certain freeze rules.
*
* Example: `ttAMM_CLAWBACK` (clawback against AMM trust lines is
* permitted even under global freeze).
*/
OverrideFreeze = 0x0010,
/** The transaction may mint or burn an NFT, changing `sfMintedNFTokens`
* or `sfBurnedNFTokens` on an account root.
*/
ChangeNftCounts = 0x0020,
/** The transaction may create a new MPT issuance object. */
CreateMptIssuance = 0x0040,
/** The transaction may destroy an existing MPT issuance object. */
DestroyMptIssuance = 0x0080,
/** The transaction must create or delete an MPT holder object
* (non-issuer path).
*
* Example: `ttMPTOKEN_AUTHORIZE` when the holder explicitly opts in or
* out.
*/
MustAuthorizeMpt = 0x0100,
/** The transaction may create or delete an MPT holder object
* (non-issuer path), but is not required to.
*
* Example: `ttAMM_WITHDRAW`, `ttAMM_CLAWBACK`.
*/
MayAuthorizeMpt = 0x0200,
/** The transaction may delete an MPT holder object but may not create one.
*
* Example: `ttMPTOKEN_ISSUANCE_DESTROY`.
*/
MayDeleteMpt = 0x0400,
/** The transaction must modify, create, or delete a vault object. */
MustModifyVault = 0x0800,
/** The transaction may modify, create, or delete a vault object,
* but is not required to.
*/
MayModifyVault = 0x1000,
/** The transaction may create an MPT holder object (non-issuer path).
*
* Example: `ttPAYMENT`, `ttCHECK_CASH`.
*/
MayCreateMpt = 0x2000,
};
/** Compose two `Privilege` bitmasks with bitwise-OR.
*
* Used in `transactions.macro` to declare combined privilege sets such as
* `CreateAcct | MayCreateMpt`. `safeCast` guards against accidental
* out-of-range integer conversions.
*
* @param lhs Left-hand privilege set.
* @param rhs Right-hand privilege set.
* @return A `Privilege` value whose set bits are the union of `lhs` and `rhs`.
*/
constexpr Privilege
operator|(Privilege lhs, Privilege rhs)
{
@@ -57,6 +139,24 @@ operator|(Privilege lhs, Privilege rhs)
safeCast<std::underlying_type_t<Privilege>>(rhs));
}
/** Query whether a transaction type holds a given privilege.
*
* Implemented in `InvariantCheck.cpp` via an X-macro expansion of
* `transactions.macro`: each transaction type's `privileges` bitmask is
* AND-ed against `priv` and returned as a `bool`. Deprecated or unknown
* transaction types return `false` (no privileges).
*
* Called exclusively from invariant checker `finalize()` methods to
* determine whether a ledger mutation that was observed is permitted for
* the transaction type being applied. Failed transactions carry no
* privileges regardless of type — checkers must guard on `isTesSuccess`
* separately when the privilege implies a required mutation.
*
* @param tx The transaction being applied.
* @param priv The privilege bit (or OR-composed set of bits) to test.
* @return `true` if `tx`'s transaction type holds all bits in `priv`.
* @see Privilege
*/
bool
hasPrivilege(STTx const& tx, Privilege priv);

View File

@@ -11,45 +11,134 @@
namespace xrpl {
/**
* @brief Invariants: Loan brokers are internally consistent
/** Invariant checker that verifies every touched `LoanBroker` ledger object is
* internally consistent after a transaction.
*
* 1. If `LoanBroker.OwnerCount = 0` the `DirectoryNode` will have at most one
* node (the root), which will only hold entries for `RippleState` or
* `MPToken` objects.
* Implements the `InvariantChecker_PROTOTYPE` two-method interface and is
* registered unconditionally in the `InvariantChecks` tuple. It enforces:
*
* 1. **Zero-OwnerCount directory structure** — when `sfOwnerCount == 0` the
* broker's owner directory may have at most one root page containing at
* most one entry, and that entry must be an `ltRIPPLE_STATE` or
* `ltMPTOKEN` object.
* 2. **Sequence monotonicity** — `sfLoanSequence` must not decrease between
* the before and after snapshots.
* 3. **Non-negative financials** — `sfDebtTotal` and `sfCoverAvailable` must
* each be ≥ 0.
* 4. **Vault linkage** — `sfVaultID` must reference an existing `ltVAULT`.
* 5. **Coverbalance lower bound** — `sfCoverAvailable` must be ≥ the
* pseudo-account's actual asset balance (via `accountHolds`).
* 6. **Coverbalance upper bound** (when `fixSecurity3_1_3` is active) —
* `sfCoverAvailable` must also be ≤ the pseudo-account balance, except
* during `ttLOAN_BROKER_DELETE` where `sfCoverAvailable` is intentionally
* left un-zeroed at deletion.
*
* Brokers are discovered through three channels during `visitEntry`: direct
* `ltLOAN_BROKER` touches, `ltACCOUNT_ROOT` pseudo-account touches carrying
* `sfLoanBrokerID`, and deferred resolution of modified `ltRIPPLE_STATE` /
* `ltMPTOKEN` entries whose owning accounts may be broker pseudo-accounts.
* The map-keyed design deduplicates brokers reached through multiple channels.
*
* @note No amendment gate is needed: `LoanBroker` objects can only exist when
* the Lending Protocol amendment is active, so an empty `brokers_` map
* on pre-amendment ledgers is the correct fast-path.
*/
class ValidLoanBroker
{
// Not all of these elements will necessarily be populated. Remaining items
// will be looked up as needed.
/** Snapshot pair for one broker implicated by a transaction.
*
* `brokerAfter` is the primary snapshot used for absolute-value checks.
* `brokerBefore` is used only for monotonicity checks that require a
* before/after comparison; it may be `nullptr` for newly-created brokers.
* Either pointer may be `nullptr` if the broker was discovered indirectly
* (e.g., through a modified trust line); in that case `finalize()` reads
* the current SLE from the view.
*/
struct BrokerInfo
{
SLE::const_pointer brokerBefore = nullptr;
// After is used for most of the checks, except
// those that check changed values.
SLE::const_pointer brokerAfter = nullptr;
};
// Collect all the LoanBrokers found directly or indirectly through
// pseudo-accounts. Key is the brokerID / index. It will be used to find the
// LoanBroker object if brokerBefore and brokerAfter are nullptr
/** Brokers implicated by this transaction, keyed by ledger index (broker
* ID). Populated directly from `ltLOAN_BROKER` or `ltACCOUNT_ROOT`
* (`sfLoanBrokerID`) touches in `visitEntry`, and extended in `finalize`
* via trust-line and MPToken endpoint resolution. `std::map::emplace`
* prevents double-entry when the same broker is reached through multiple
* channels.
*/
std::map<uint256, BrokerInfo> brokers_;
// Collect all the modified trust lines. Their high and low accounts will be
// loaded to look for LoanBroker pseudo-accounts.
/** Modified trust lines collected during `visitEntry`. In `finalize`,
* both the high and low account of each line are resolved; if either
* carries `sfLoanBrokerID` the broker is added to `brokers_`.
*/
std::vector<SLE::const_pointer> lines_;
// Collect all the modified MPTokens. Their accounts will be loaded to look
// for LoanBroker pseudo-accounts.
/** Modified MPToken entries collected during `visitEntry`. In `finalize`,
* the owning account of each token is resolved; if it carries
* `sfLoanBrokerID` the broker is added to `brokers_`.
*/
std::vector<SLE::const_pointer> mpts_;
/** Validate the owner directory of a broker whose `sfOwnerCount` is zero.
*
* Checks that `dir` is a single-page directory (no chained pages via
* `sfIndexNext` / `sfIndexPrevious`) containing at most one entry, and
* that the entry — if present — resolves to an `ltRIPPLE_STATE` or
* `ltMPTOKEN` object. Logs a fatal message for each violation found.
*
* @param view Read-only view used to resolve the referenced object.
* @param dir The owner-directory root SLE to inspect.
* @param j Journal for fatal-level diagnostic logging.
* @return `true` if the directory satisfies the zero-owner-count
* structural constraint; `false` otherwise.
*/
static bool
goodZeroDirectory(ReadView const& view, SLE::const_ref dir, beast::Journal const& j);
public:
/** Accumulate ledger entries modified by the transaction for later
* validation.
*
* Records `ltLOAN_BROKER` SLEs directly into `brokers_` with their
* before/after snapshots. Records `ltACCOUNT_ROOT` entries that carry
* `sfLoanBrokerID` (broker pseudo-accounts) as broker stubs so that
* `finalize()` will look them up even if the `ltLOAN_BROKER` SLE was not
* itself modified. Collects `ltRIPPLE_STATE` and `ltMPTOKEN` entries
* into `lines_` and `mpts_` for deferred broker-discovery in `finalize`.
*
* @param isDelete `true` when the entry is being removed from the ledger.
* @param before SLE snapshot before the transaction; `nullptr` if the
* entry was created by this transaction.
* @param after SLE snapshot after the transaction; `nullptr` if the
* entry was deleted by this transaction.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool isDelete, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** Validate all `LoanBroker` objects implicated by the transaction.
*
* First extends `brokers_` by resolving the high/low accounts of every
* collected trust line and the owner account of every collected MPToken,
* adding any broker pseudo-accounts discovered there. Then iterates over
* `brokers_` and for each broker asserts the six invariants described in
* the class documentation.
*
* @param tx The transaction being applied (used to detect
* `ttLOAN_BROKER_DELETE`, which exempts the cover upper-bound check).
* @param ter Result code returned by `doApply` (unused by this checker).
* @param fee Fee charged by the transaction (unused by this checker).
* @param view Read-only post-transaction ledger view used to resolve
* broker SLEs not captured directly during `visitEntry`, vault
* objects, and pseudo-account balances.
* @param j Journal for fatal-level diagnostic logging on failure.
* @return `true` if every implicated broker satisfies all invariants;
* `false` if any invariant is violated (transaction will be rolled
* back and escalated to `tecINVARIANT_FAILED`).
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
finalize(STTx const& tx, TER const ter, XRPAmount const fee, ReadView const& view, beast::Journal const& j);
};
} // namespace xrpl

View File

@@ -9,24 +9,85 @@
namespace xrpl {
/**
* @brief Invariants: Loans are internally consistent
/** Invariant checker that verifies every touched `ltLOAN` ledger entry is
* internally consistent after a transaction.
*
* 1. If `Loan.PaymentRemaining = 0` then `Loan.PrincipalOutstanding = 0`
* Implements the two-method invariant-checker interface and is registered
* unconditionally in the `InvariantChecks` tuple. If the Lending Protocol
* amendment is not active, no `ltLOAN` objects can exist, so `visitEntry`
* never collects anything and `finalize` returns `true` immediately — no
* explicit amendment gate is required.
*
* The following invariants are enforced on every collected loan:
* 1. **Payment-completion consistency (zero direction)** — if
* `sfPaymentRemaining == 0` then `sfTotalValueOutstanding`,
* `sfPrincipalOutstanding`, and `sfManagementFeeOutstanding` must all
* be zero (loan is fully settled).
* 2. **Payment-completion consistency (non-zero direction)** — if
* `sfPaymentRemaining != 0` then at least one of the outstanding amounts
* must also be non-zero (a zeroed balance cannot coexist with a non-zero
* payment count).
* 3. **Overpayment flag immutability** — `lsfLoanOverpayment` must not
* change during a transaction.
* 4. **Non-negative financial fields** — `sfLoanServiceFee`,
* `sfLatePaymentFee`, `sfClosePaymentFee`, `sfPrincipalOutstanding`,
* `sfTotalValueOutstanding`, and `sfManagementFeeOutstanding` must each
* be ≥ 0.
* 5. **Strictly positive periodic payment** — `sfPeriodicPayment` must be
* > 0; a zero or negative value would produce undefined amortization.
*/
class ValidLoan
{
// Pair is <before, after>. After is used for most of the checks, except
// those that check changed values.
/** Collected loan entries touched by the transaction.
*
* Each pair holds the (before, after) SLE snapshots. `after` is used
* for all absolute-value checks; `before` is used only for the
* overpayment-flag change detection and may be `nullptr` for newly
* created loans.
*/
std::vector<std::pair<SLE::const_pointer, SLE::const_pointer>> loans_;
public:
/** Accumulate `ltLOAN` entries modified by the transaction for later
* validation.
*
* Records an entry only when `after` is non-null and its type is
* `ltLOAN`; deletions (where `after` is null) are intentionally
* ignored because a removed loan has no post-transaction state to
* validate.
*
* @param isDelete `true` when the entry is being removed from the
* ledger (unused — filtering is based on `after` nullness).
* @param before SLE snapshot before the transaction; `nullptr` if
* the entry was created by this transaction.
* @param after SLE snapshot after the transaction; `nullptr` if
* the entry was deleted by this transaction.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool isDelete, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** Validate all `ltLOAN` objects collected during `visitEntry`.
*
* Iterates over the collected (before, after) pairs and enforces the
* five invariants described in the class documentation. Returns `false`
* and emits a `fatal`-level journal message on the first violation
* detected within each loan entry.
*
* The `tx`, `fee`, and `view` parameters are unused; loan consistency
* can be verified entirely from the collected SLE snapshots.
*
* @param tx The transaction being applied (unused by this checker).
* @param ter Result code returned by `doApply` (unused by this checker).
* @param fee Fee charged by the transaction (unused by this checker).
* @param view Read-only post-transaction ledger view (unused by this
* checker).
* @param j Journal for fatal-level diagnostic logging on failure.
* @return `true` if every collected loan satisfies all invariants;
* `false` if any invariant is violated (triggers rollback and
* escalation to `tecINVARIANT_FAILED`).
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
finalize(STTx const& tx, TER const ter, XRPAmount const fee, ReadView const& view, beast::Journal const& j);
};
} // namespace xrpl

View File

@@ -1,3 +1,23 @@
/** @file
* Invariant checker declarations for Multi-Purpose Token (MPT) ledger
* consistency.
*
* Declares `ValidMPTIssuance` and `ValidMPTPayment`, which together guard
* the MPT subsystem against state corruption after every transaction
* application. Both classes follow the two-phase invariant contract:
* `visitEntry()` accumulates per-SLE data, and `finalize()` renders a
* pass/fail verdict once all entries have been visited. They are registered
* in the `InvariantChecks` tuple in `InvariantCheck.h` and run
* unconditionally — including on failed transactions — as the last line of
* defence against unexpected ledger mutations.
*
* `ValidMPTIssuance` addresses *object lifecycle* (did the correct set of
* `ltMPTOKEN_ISSUANCE` and `ltMPTOKEN` entries appear or disappear?).
* `ValidMPTPayment` addresses *numeric conservation* (does each issuance's
* `OutstandingAmount` remain equal to the sum of all holder balances?).
* The two concerns are kept in separate classes so each remains small and
* straightforward to audit.
*/
#pragma once
#include <xrpl/beast/utility/Journal.h>
@@ -9,6 +29,38 @@
namespace xrpl {
/** Invariant checker that enforces structural integrity of MPT lifecycle
* objects.
*
* During `visitEntry()` the checker counts `ltMPTOKEN_ISSUANCE` and
* `ltMPTOKEN` entries created and deleted, and flags the pathological case
* where a new `ltMPTOKEN` was auto-created for the issuance's own issuer
* account (always a protocol violation).
*
* During `finalize()` the accumulated counts are validated against the
* transaction's privilege bitmask (see `InvariantCheckPrivilege.h`):
* - `createMPTIssuance` → exactly one `ltMPTOKEN_ISSUANCE` created, none
* deleted.
* - `destroyMPTIssuance` → exactly one deleted, none created.
* - `mustAuthorizeMPT` (holder submission) → exactly one `ltMPTOKEN`
* created or deleted.
* - `mayAuthorizeMPT` (e.g. `ttAMM_WITHDRAW`, `ttAMM_CLAWBACK`) → at most
* one created, at most two deleted (covers both sides of an AMM pool
* dissolution).
* - `mayCreateMPT` → auto-creation only: up to two for `ttAMM_CREATE`, up
* to one for `ttCHECK_CASH`; no deletions.
* - `mayDeleteMPT` → deletions only, no creations.
* - No privilege → all counters must be zero on a successful result.
*
* The issuer-MPToken check uses the `assert(enforce)` soft-rollout pattern:
* the fatal log fires unconditionally, but the hard `false` return is gated
* on `featureSingleAssetVault` or `featureLendingProtocol` being active,
* preserving backward compatibility while surfacing problems to operators
* and debug builds.
*
* @see ValidMPTPayment
* @see InvariantCheckPrivilege.h
*/
class ValidMPTIssuance
{
std::uint32_t mptIssuancesCreated_ = 0;
@@ -21,24 +73,89 @@ class ValidMPTIssuance
bool mptCreatedByIssuer_ = false;
public:
/** Accumulate MPT object counts from a single modified ledger entry.
*
* Updates the four creation/deletion counters for `ltMPTOKEN_ISSUANCE`
* and `ltMPTOKEN` entries. Sets `mptCreatedByIssuer_` when a newly
* created `ltMPTOKEN` belongs to the issuance's own issuer account —
* a condition that is always a protocol violation.
*
* @param isDelete `true` when the entry is being removed from the ledger.
* @param before Snapshot of the SLE before the transaction; null if the
* entry did not exist prior to this transaction.
* @param after Snapshot of the SLE after the transaction; null when
* the entry has been erased.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool isDelete, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** Verify that the transaction's MPT object lifecycle changes were
* authorized by its privilege mask.
*
* Dispatches on the transaction's privilege flags rather than its type,
* keeping the logic independent of the growing set of MPT-capable
* transaction types. With `featureMPTokensV2` enabled, `tecINCOMPLETE`
* results are also subject to full invariant checking because partial
* progress is still valid ledger state.
*
* @param tx The transaction that was applied.
* @param result The `TER` returned by `doApply()` or the post-reset
* result.
* @param fee The fee deducted (unused by this checker).
* @param view Read-only view of the post-apply ledger, used to query
* active amendment rules.
* @param j Journal for fatal-level diagnostics on violation.
* @return `true` if all MPT lifecycle constraints are satisfied.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
};
/** Verify:
* - OutstandingAmount <= MaximumAmount for any MPT
* - OutstandingAmount after = OutstandingAmount before +
* sum (MPT after - MPT before) - this is total MPT credit/debit
/** Invariant checker that enforces conservation of MPT outstanding amounts.
*
* After any successful transaction, each `ltMPTOKEN_ISSUANCE` entry's
* `OutstandingAmount` must equal the sum of all holder `ltMPTOKEN` balances
* (`sfMPTAmount + sfLockedAmount`) for that issuance. The conservation
* equation is:
*
* ```
* OutstandingAmount[After] ==
* OutstandingAmount[Before] + Σ(MPTAmount[After] MPTAmount[Before])
* ```
*
* where locked amounts are included because they remain part of the
* outstanding supply.
*
* Overflow is treated as a first-class failure: any individual amount that
* exceeds `kMAX_MP_TOKEN_AMOUNT`, or any arithmetic wrap-around in the
* accumulated delta, sets `overflow_` and causes `finalize()` to fail
* immediately. Enforcement is amendment-gated on `featureMPTokensV2`.
*
* @note Unlike `ValidMPTIssuance`, `finalize()` is non-`const` because the
* `data_` accumulator may be mutated lazily; `finalize()` is the single
* point at which all data is final.
*
* @see ValidMPTIssuance
*/
class ValidMPTPayment
{
/** Index into `MPTData::outstanding` distinguishing the pre- and
* post-transaction `OutstandingAmount` snapshots.
*/
enum class Order { Before = 0, After = 1 };
/** Per-issuance accounting data accumulated across all visited SLEs. */
struct MPTData
{
/** `OutstandingAmount` before (`[0]`) and after (`[1]`) the
* transaction.
*/
std::array<std::int64_t, 2> outstanding{};
/** Net delta across all holder `ltMPTOKEN` entries:
* Σ(`sfMPTAmount` + `sfLockedAmount`)_after
* Σ(`sfMPTAmount` + `sfLockedAmount`)_before.
*/
// sum (MPT after - MPT before)
std::int64_t mptAmount{0};
};
@@ -49,9 +166,48 @@ class ValidMPTPayment
hash_map<uint192, MPTData> data_;
public:
/** Accumulate outstanding-amount snapshots and holder-balance deltas for
* a single modified ledger entry.
*
* For `ltMPTOKEN_ISSUANCE` entries, records the before/after
* `sfOutstandingAmount` and checks that the post-apply value does not
* exceed `sfMaximumAmount`. For `ltMPTOKEN` entries, adds or subtracts
* `sfMPTAmount + sfLockedAmount` from the running `mptAmount` delta.
* Sets `overflow_` and returns early if any individual value exceeds
* `kMAX_MP_TOKEN_AMOUNT` or if the addition would wrap a 64-bit integer.
*
* @note The `isDelete` parameter is unused because deletion is already
* signalled by `after == nullptr`; the presence of `before` is
* sufficient to handle the before-state contribution.
*
* @param before Snapshot of the SLE before the transaction; null for
* newly inserted entries.
* @param after Snapshot of the SLE after the transaction; null for
* deleted entries.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** Verify the `OutstandingAmount` conservation invariant for all touched
* MPT issuances.
*
* Iterates over every MPT ID recorded during `visitEntry()` and checks
* that `OutstandingAmount[After] == OutstandingAmount[Before] + mptAmount`.
* Fails immediately if `overflow_` was set during accumulation, or if the
* final delta arithmetic would overflow. Only `tesSUCCESS` results
* trigger the check; the invariant passes silently for failed transactions.
* Hard-fails (returns `false`) only when `featureMPTokensV2` is active;
* before activation, violations are logged at fatal severity but return
* `true`.
*
* @param tx The transaction that was applied (unused by this checker).
* @param result The `TER` result; only `tesSUCCESS` triggers the check.
* @param fee The fee deducted (unused by this checker).
* @param view Read-only view used to query active amendment rules.
* @param j Journal for fatal-level diagnostics on violation.
* @return `true` if outstanding-amount conservation holds for all touched
* MPT issuances.
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};

View File

@@ -10,61 +10,156 @@
namespace xrpl {
/**
* @brief Invariant: Validates several invariants for NFToken pages.
/** Invariant: every touched `ltNFTOKEN_PAGE` entry must satisfy the
* structural rules of the NFToken directory after each transaction.
*
* The following checks are made:
* - The page is correctly associated with the owner.
* - The page is correctly ordered between the next and previous links.
* - The page contains at least one and no more than 32 NFTokens.
* - The NFTokens on this page do not belong on a lower or higher page.
* - The NFTokens are correctly sorted on the page.
* - Each URI, if present, is not empty.
* NFToken pages use a composite 256-bit key: the high 160 bits are the
* owning account and the low 96 bits are the page's *high limit* — the
* exclusive upper bound on which token IDs (by their own low 96 bits)
* belong on that page. Six structural properties are checked on every
* snapshot (before and after) of each visited `ltNFTOKEN_PAGE`:
*
* 1. **Link ownership and ordering**: `sfPreviousPageMin`/`sfNextPageMin`
* high 160 bits must match the current page's account; low 96 bits
* must be strictly ordered (`prev < current < next`).
* 2. **Size**: 1`dirMaxTokensPerPage` (32) tokens, unless the page is
* being deleted (empty is permitted only then).
* 3. **Membership**: each token's page-bits must fall in `[loLimit, hiLimit)`.
* 4. **Sort**: tokens within the page must be strictly ascending under
* `nft::compareTokens()`.
* 5. **URI**: an `sfURI` field, if present, must be non-empty.
*
* Two additional cross-snapshot checks are gated on `fixNFTokenPageLinks`
* to avoid penalising pre-amendment history:
* - Deleting the final page (all 96 page-bits set) while
* `sfPreviousPageMin` is still present would orphan the directory.
* - A non-final page silently losing `sfNextPageMin` breaks forward
* traversal of the directory.
*
* Failures are recorded in boolean flags; `finalize` reports them.
*
* @see InvariantChecker_PROTOTYPE for the two-phase interface contract.
* @see NFTokenCountTracking for the paired mint/burn counter checker.
*/
class ValidNFTokenPage
{
/** True if any token's page-bits fall outside `[loLimit, hiLimit)`. */
bool badEntry_ = false;
/** True if any page-link crosses accounts or violates strict ordering. */
bool badLink_ = false;
/** True if tokens within any page are not strictly ascending. */
bool badSort_ = false;
/** True if any token carries an empty `sfURI` field. */
bool badURI_ = false;
/** True if any non-deleted page has zero tokens or more than 32 tokens. */
bool invalidSize_ = false;
/** True if the final page (all 96 page-bits == 1) was deleted while
* `sfPreviousPageMin` was still present. Gated on `fixNFTokenPageLinks`. */
bool deletedFinalPage_ = false;
/** True if a non-final page lost `sfNextPageMin` without being deleted.
* Gated on `fixNFTokenPageLinks`. */
bool deletedLink_ = false;
public:
/** Inspect one touched `ltNFTOKEN_PAGE` entry for structural integrity.
*
* Non-NFT-page SLEs are silently ignored. For each snapshot that is
* present the inner check validates links, size, membership, sort, and
* URI. The two amendment-gated transition checks (`deletedFinalPage_`,
* `deletedLink_`) are applied across the `before`→`after` pair.
*
* @param isDelete True if the SLE is being removed from the ledger;
* allows an empty token array on the `before` snapshot.
* @param before Snapshot of the entry before the transaction, or nullptr
* if the entry is being created.
* @param after Snapshot of the entry after the transaction, or nullptr if
* the entry is being deleted.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool isDelete, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** Render a pass/fail verdict for all accumulated NFT page checks.
*
* Reports the first set flag and returns `false` (triggering
* `tecINVARIANT_FAILED`). The `deletedFinalPage_` and `deletedLink_`
* checks are enforced only when `fixNFTokenPageLinks` is active, so
* pre-amendment historical replay is unaffected.
*
* @param tx Unused.
* @param result Unused.
* @param fee Unused.
* @param view The post-transaction ledger view; used to query whether
* `fixNFTokenPageLinks` is active.
* @param j Journal for fatal-level diagnostics on failure.
* @return True if all NFT page invariants pass; false on any violation.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
finalize(STTx const& tx, TER const result, XRPAmount const fee, ReadView const& view, beast::Journal const& j) const;
};
/**
* @brief Invariant: Validates counts of NFTokens after all transaction types.
/** Invariant: the global `sfMintedNFTokens` and `sfBurnedNFTokens` totals
* across all touched account roots must change only as the transaction type
* and result code require.
*
* The following checks are made:
* - The number of minted or burned NFTokens can only be changed by
* NFTokenMint or NFTokenBurn transactions.
* - A successful NFTokenMint must increase the number of NFTokens.
* - A failed NFTokenMint must not change the number of minted NFTokens.
* - An NFTokenMint transaction cannot change the number of burned NFTokens.
* - A successful NFTokenBurn must increase the number of burned NFTokens.
* - A failed NFTokenBurn must not change the number of burned NFTokens.
* - An NFTokenBurn transaction cannot change the number of minted NFTokens.
* `visitEntry` accumulates pre- and post-transaction values of both fields
* across every `ltACCOUNT_ROOT` touched by the transaction. `finalize`
* then branches on the `ChangeNftCounts` privilege:
*
* - **Without privilege** (all transactions except `ttNFTOKEN_MINT`/`BURN`):
* both totals must be completely unchanged.
* - **`ttNFTOKEN_MINT`** success: minted total must strictly increase; burned
* total must be unchanged. On failure: both totals must be unchanged.
* - **`ttNFTOKEN_BURN`** success: burned total must strictly increase; minted
* total must be unchanged. On failure: both totals must be unchanged.
*
* Tracking global sums rather than per-account deltas is intentional: a
* legitimate mint or burn touches exactly one account's counters, so
* global-sum equality is both necessary and sufficient. Strict inequality
* (`>=` rather than `==`) on success additionally catches counter
* wrap-around and incorrect field rewrites.
*
* @note Failed transactions carry no privileges, so the no-privilege path
* catches counter mutations during a failed mint/burn — a critical
* guard against exploit code that corrupts counters on failure.
* @see InvariantChecker_PROTOTYPE for the two-phase interface contract.
* @see ValidNFTokenPage for the paired page-structure checker.
*/
class NFTokenCountTracking
{
/** Sum of `sfMintedNFTokens` across all touched account roots, before the transaction. */
std::uint32_t beforeMintedTotal_ = 0;
/** Sum of `sfBurnedNFTokens` across all touched account roots, before the transaction. */
std::uint32_t beforeBurnedTotal_ = 0;
/** Sum of `sfMintedNFTokens` across all touched account roots, after the transaction. */
std::uint32_t afterMintedTotal_ = 0;
/** Sum of `sfBurnedNFTokens` across all touched account roots, after the transaction. */
std::uint32_t afterBurnedTotal_ = 0;
public:
/** Accumulate `sfMintedNFTokens` and `sfBurnedNFTokens` totals.
*
* Non-account-root SLEs are silently skipped. Absent fields are treated
* as zero via `.value_or(0)`.
*
* @param isDelete Unused.
* @param before Pre-transaction snapshot, or nullptr for new entries.
* @param after Post-transaction snapshot, or nullptr for deleted entries.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool isDelete, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** Validate NFT mint/burn counter invariants for the completed transaction.
*
* @param tx The transaction; used for type and privilege checks.
* @param result The transaction result code; distinguishes success from
* failure for the mint/burn symmetry rules.
* @param fee Unused.
* @param view Unused.
* @param j Journal for fatal-level diagnostics on failure.
* @return True if all count invariants pass; false on any violation.
*/
[[nodiscard]] bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&) const;
finalize(STTx const& tx, TER const result, XRPAmount const fee, ReadView const& view, beast::Journal const& j) const;
};
} // namespace xrpl

View File

@@ -1,3 +1,8 @@
/** @file
* Declares `ValidPermissionedDEX`, the invariant checker that enforces
* domain isolation for the Permissioned DEX feature.
*/
#pragma once
#include <xrpl/basics/base_uint.h>
@@ -8,6 +13,26 @@
namespace xrpl {
/** Invariant checker that enforces the isolation contract of the Permissioned DEX.
*
* Verifies that every successful `ttPAYMENT` or `ttOFFER_CREATE` transaction
* that carries an `sfDomainID` operates exclusively within its declared
* domain: it must not touch order-book directories or offers belonging to a
* different domain, and it must not interact with regular (non-domain) offers.
* Additionally ensures that any hybrid offer produced by an `OfferCreate` is
* structurally well-formed.
*
* The definition of "well-formed" for hybrid offers is amendment-gated:
* before `fixSecurity3_1_3` activates, the checker rejects hybrids with a
* missing `sfDomainID`, missing `sfAdditionalBooks`, or an
* `sfAdditionalBooks` array larger than 1; after the amendment activates,
* the size must be exactly 1 (an empty array is also rejected).
*
* Follows the two-phase `visitEntry` / `finalize` protocol required of all
* invariant checkers registered in `InvariantChecks`.
*
* @see ValidPermissionedDomain
*/
class ValidPermissionedDEX
{
bool regularOffers_ = false;
@@ -16,9 +41,48 @@ class ValidPermissionedDEX
hash_set<uint256> domains_;
public:
/** Accumulate domain and offer state from a single modified ledger entry.
*
* Called once per modified entry before `finalize`. Inspects only the
* post-transaction (`after`) snapshot; `before` is unused. For each
* `ltDIR_NODE` or `ltOFFER` entry, records any `sfDomainID` encountered
* into `domains_`. Sets `regularOffers_` if an offer lacks `sfDomainID`.
* Sets `badHybrids_` / `badHybridsOld_` if a hybrid offer (`lsfHybrid`)
* is structurally malformed (missing domain, missing `sfAdditionalBooks`,
* or wrong array size).
*
* @param isDelete True if the entry is being deleted (ignored).
* @param before Pre-transaction SLE snapshot (unused by this checker).
* @param after Post-transaction SLE snapshot; may be null for deletions.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
/** Evaluate all accumulated state and return whether the invariant holds.
*
* Only active for successful (`isTesSuccess`) `ttPAYMENT` and
* `ttOFFER_CREATE` transactions; all others pass unconditionally.
* Transactions that carry no `sfDomainID` also pass unconditionally —
* unpermissioned transactions are not constrained by this checker.
*
* Checks (in order):
* - For `ttOFFER_CREATE`: no hybrid offer is structurally malformed.
* Which malformation predicate applies depends on whether
* `fixSecurity3_1_3` is active in the current rule set (pre-amendment:
* `badHybridsOld_`; post-amendment: `badHybrids_`).
* - The `ltPERMISSIONED_DOMAIN` referenced by `sfDomainID` exists in
* the ledger — guards against a domain deleted within the same batch.
* - Every domain ID recorded in `domains_` matches the transaction's own
* domain; any foreign domain indicates cross-domain contamination.
* - No regular (non-domain) offers were touched.
*
* @param tx The transaction being applied.
* @param result The TER code returned by `doApply`.
* @param fee The fee charged (unused by this checker).
* @param view Read-only ledger view used to verify domain existence.
* @param j Journal for fatal-level diagnostic logging on failure.
* @return True if the invariant holds; false if a violation was detected.
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};

View File

@@ -1,3 +1,8 @@
/** @file
* Declares `ValidPermissionedDomain`, the invariant checker that enforces the
* structural integrity of `ltPERMISSIONED_DOMAIN` ledger entries.
*/
#pragma once
#include <xrpl/beast/utility/Journal.h>
@@ -9,31 +14,130 @@
namespace xrpl {
/**
* @brief Invariants: Permissioned Domains must have some rules and
* AcceptedCredentials must have length between 1 and 10 inclusive.
/** Invariant checker that enforces the structural integrity of
* `ltPERMISSIONED_DOMAIN` ledger entries.
*
* Since only permissions constitute rules, an empty credentials list
* means that there are no rules and the invariant is violated.
* Every `ltPERMISSIONED_DOMAIN` entry must leave its `sfAcceptedCredentials`
* array in a valid state: non-empty, within the
* `kMAX_PERMISSIONED_DOMAIN_CREDENTIALS_ARRAY_SIZE` (10) cap, free of
* duplicate `(sfIssuer, sfCredentialType)` pairs, and in canonical
* lexicographic sort order as defined by `credentials::makeSorted`.
*
* Credentials must be sorted and no duplicates allowed
* These guarantees are foundational: downstream consumers such as
* `credentials::validDomain` iterate `sfAcceptedCredentials` without
* re-validating its structure on every access.
*
* Follows the two-phase `visitEntry` / `finalize` protocol required of all
* invariant checkers registered in `InvariantChecks`. `visitEntry` snapshots
* credential-array facts into the `sleStatus_` vector; `finalize` interprets
* those facts in the context of the completed transaction.
*
* The strictness of `finalize` is gated on the `fixPermissionedDomainInvariant`
* amendment. Without the amendment, only a successful `ttPERMISSIONED_DOMAIN_SET`
* is checked. With the amendment, the invariant also asserts that failed
* transactions leave all domain entries untouched, that no transaction affects
* more than one domain entry, that `ttPERMISSIONED_DOMAIN_DELETE` deletes
* exactly one entry, and that no unauthorized transaction type touches a
* domain entry at all.
*
* @see ValidPermissionedDEX
* @see InvariantChecker_PROTOTYPE for the duck-typed interface.
*/
class ValidPermissionedDomain
{
/** Snapshot of credential-array facts for a single `ltPERMISSIONED_DOMAIN`
* entry, recorded by `visitEntry` and consumed by `finalize`.
*
* All fields are derived from the post-transaction (`after`) SLE state.
* `isSorted` and `isUnique` are pre-computed in `visitEntry` so
* `finalize` can evaluate them without re-reading the SLE through
* `ReadView`. The sort check short-circuits on the first out-of-order
* pair (O(n)); `isUnique` relies on `credentials::makeSorted` returning
* an empty set when any duplicate pair is present.
*/
struct SleStatus
{
/** Raw count of entries in `sfAcceptedCredentials`.
* Zero means no rules exist (always invalid); greater than
* `kMAX_PERMISSIONED_DOMAIN_CREDENTIALS_ARRAY_SIZE` means the array
* is oversized.
*/
std::size_t credentialsSize{0};
/** True when the array's iteration order matches the canonical order
* produced by `credentials::makeSorted`. Only meaningful when
* `isUnique` is also true; duplicates make the sorted comparison
* meaningless.
*/
bool isSorted = false;
/** True when `credentials::makeSorted` returned a non-empty result,
* indicating no duplicate `(sfIssuer, sfCredentialType)` pairs exist.
*/
bool isUnique = false;
/** Propagated from the `isDel` argument of `visitEntry`; allows
* `finalize` to distinguish a creation/modification from a deletion
* without re-querying the view.
*/
bool isDelete = false;
};
/** Accumulated observations, one per `ltPERMISSIONED_DOMAIN` entry
* touched by the current transaction.
*/
std::vector<SleStatus> sleStatus_;
public:
/** Record credential-array facts for a single modified `ltPERMISSIONED_DOMAIN`
* entry.
*
* Non-domain entries are ignored immediately. Only the post-transaction
* `after` state is examined; the pre-transaction state is irrelevant to
* whether the resulting ledger is structurally valid. Deleted entries
* (where `after` is null) are recorded as deletions with zero credential
* count — `finalize` uses `isDelete` to distinguish them.
*
* @param isDel True when the entry is being deleted by this transaction.
* @param before The ledger entry state before the transaction; may be null
* for newly created entries. Unused by this checker.
* @param after The ledger entry state after the transaction; null for
* deleted entries, in which case this call is a no-op.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
/** Apply the invariant policy using the entry facts collected by `visitEntry`.
*
* Behavior depends on whether `fixPermissionedDomainInvariant` is active:
*
* **Without the amendment (legacy path):** only validates after a
* successful `ttPERMISSIONED_DOMAIN_SET` that touched at least one domain
* entry. All other transaction types and all failed transactions pass
* unconditionally.
*
* **With the amendment (strict path):**
* - A failed transaction must not have mutated any domain entry
* (`sleStatus_` must be empty).
* - At most one domain entry may be affected per transaction.
* - `ttPERMISSIONED_DOMAIN_SET`: must have modified (not deleted) exactly
* one entry whose `sfAcceptedCredentials` array satisfies all four
* constraints (non-empty, within cap, unique, sorted).
* - `ttPERMISSIONED_DOMAIN_DELETE`: must have deleted exactly one entry
* and must not have modified it.
* - Any other transaction type: must not have affected any domain entry.
*
* All failures are logged at `fatal` severity before returning `false`,
* which causes `ApplyContext` to roll back the transaction.
*
* @param tx The transaction being finalized.
* @param result The `TER` code produced by `doApply`.
* @param fee The fee charged (unused by this checker).
* @param view Post-transaction read view; used to query whether
* `fixPermissionedDomainInvariant` is active.
* @param j Journal for `fatal`-level diagnostic logging on failure.
* @return `true` if the invariant holds; `false` to veto the transaction.
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
};

View File

@@ -1,3 +1,12 @@
/** @file
* Post-transaction invariant checker for the SingleAssetVault feature.
*
* Declares `ValidVault`, the dedicated guardian for vault ledger objects
* (`ltVAULT`, the share `ltMPTOKEN_ISSUANCE`, depositor `ltMPTOKEN` entries,
* vault pseudo-account `ltACCOUNT_ROOT`, and associated trust lines). It is
* registered as position 24 in the `InvariantChecks` tuple and runs after
* every transaction before ledger state is committed.
*/
#pragma once
#include <xrpl/basics/Number.h>
@@ -14,29 +23,38 @@
namespace xrpl {
/*
* @brief Invariants: Vault object and MPTokenIssuance for vault shares
/** Post-transaction consistency guard for Single Asset Vaults.
*
* - vault deleted and vault created is empty
* - vault created must be linked to pseudo-account for shares and assets
* - vault must have MPTokenIssuance for shares
* - vault without shares outstanding must have no shares
* - loss unrealized does not exceed the difference between assets total and
* assets available
* - assets available do not exceed assets total
* - vault deposit increases assets and share issuance, and adds to:
* total assets, assets available, shares outstanding
* - vault withdrawal and clawback reduce assets and share issuance, and
* subtracts from: total assets, assets available, shares outstanding
* - vault set must not alter the vault assets or shares balance
* - no vault transaction can change loss unrealized (it's updated by loan
* transactions)
* Implements the two-phase invariant checker interface required by
* `InvariantChecks`. `visitEntry` accumulates per-entry balance deltas and
* snapshots vault/MPT state; `finalize` evaluates per-transaction invariants
* against those snapshots.
*
* All meaningful work is skipped via an early-exit when no vault entry was
* touched, making this checker essentially free for the vast majority of
* transactions. `Number` (high-precision rational) is used throughout for
* asset amounts because IOU assets require fractional precision beyond what
* 64-bit integers can represent losslessly.
*
* Enforcement is gated on `featureSingleAssetVault`: violations are always
* logged at `fatal` level and fire a debug-build `XRPL_ASSERT`, but
* `finalize` only returns `false` (blocking the transaction) once the
* amendment is live on the network.
*
* @see InvariantChecker_PROTOTYPE for the duck-typed interface contract.
* @see InvariantChecks in `InvariantCheck.h` for checker registration.
*/
class ValidVault
{
Number static constexpr kZERO{};
/** Immutable snapshot of a single `ltVAULT` ledger entry.
*
* Captures every field that invariant checks compare across before/after
* states. Constructed exclusively via `Vault::make`; not an aggregate so
* that factory validation (the `XRPL_ASSERT` on entry type) is always
* enforced.
*/
struct Vault final
{
uint256 key = beast::kZERO;
@@ -49,25 +67,72 @@ class ValidVault
Number assetsMaximum = 0;
Number lossUnrealized = 0;
/** Construct a `Vault` snapshot from a live `ltVAULT` ledger entry.
*
* @param from A ledger entry whose type must be `ltVAULT`.
* @return A fully-populated `Vault` value object.
*/
Vault static make(SLE const&);
};
/** Immutable snapshot of a single `ltMPTOKEN_ISSUANCE` ledger entry.
*
* Captures identity (`MPTIssue`), current outstanding amount, and
* effective maximum so `finalize` can check share-ceiling constraints.
* Because a modified `ltMPTOKEN_ISSUANCE` may belong to the vault or to
* an entirely unrelated MPT issuance, these are recorded lazily in
* `visitEntry` and resolved against `shareMPTID` in `finalize`.
*/
struct Shares final
{
MPTIssue share;
std::uint64_t sharesTotal = 0;
std::uint64_t sharesMaximum = 0;
/** Construct a `Shares` snapshot from a live `ltMPTOKEN_ISSUANCE` entry.
*
* When `sfMaximumAmount` is absent the effective maximum defaults to
* `kMAX_MP_TOKEN_AMOUNT`.
*
* @param from A ledger entry whose type must be `ltMPTOKEN_ISSUANCE`.
* @return A fully-populated `Shares` value object.
*/
Shares static make(SLE const&);
};
public:
/** Signed balance delta for a single ledger entry, with precision metadata.
*
* Pairs a `Number` delta with an optional scale (exponent) so that
* comparisons in `finalize` can round all operands to a common precision
* before testing equality. The scale is `std::nullopt` until the entry
* type is identified in `visitEntry`; a present scale of `0` means the
* value is an integer (XRP drops or MPT integer amount).
*
* Sign convention (set in `visitEntry`): positive delta means assets or
* shares *flowed into* the associated account from the vault's perspective.
* For `ltMPTOKEN_ISSUANCE` the sign is `+1` (outstanding amount increases
* as shares are minted). For account roots, trust lines, and `ltMPTOKEN`
* the sign is `-1` (a balance decrease means assets left the holder).
*/
struct DeltaInfo final
{
Number delta = kNUM_ZERO;
std::optional<int> scale;
// Compute the delta between two Numbers, taking the coarsest scale
/** Construct a `DeltaInfo` representing the change from `before` to `after`.
*
* Sets `scale` to the coarser (larger) of the two values' asset-specific
* scales so that subsequent rounding via `roundToAsset` uses a precision
* no finer than either operand.
*
* @param before Pre-transaction numeric value.
* @param after Post-transaction numeric value.
* @param asset Asset type used to determine the appropriate scale for
* each value via `xrpl::scale()`.
* @return `DeltaInfo` where `delta = after - before` and `scale` is the
* maximum of the two operand scales.
*/
[[nodiscard]] static DeltaInfo
makeDelta(Number const& before, Number const& after, Asset const& asset);
};
@@ -80,13 +145,72 @@ private:
std::unordered_map<uint256, DeltaInfo> deltas_;
public:
/** Phase-1 entry visitor: accumulate per-key balance deltas and snapshots.
*
* Called once per modified ledger entry during transaction processing.
* For `ltVAULT` entries, pushes a `Vault` snapshot into `beforeVault_`
* and/or `afterVault_`. For `ltMPTOKEN_ISSUANCE` entries, pushes a
* `Shares` snapshot (vault vs. unrelated issuance is resolved later in
* `finalize`). For balance-carrying entries (`ltMPTOKEN_ISSUANCE`,
* `ltMPTOKEN`, `ltACCOUNT_ROOT`, `ltRIPPLE_STATE`), accumulates a signed
* `DeltaInfo` into `deltas_` keyed by ledger-entry key.
*
* A delta entry is recorded even when the net balance change is zero
* (e.g., a fee exactly offsets an incoming transfer) to avoid treating a
* coincidental zero as "no activity".
*
* @param isDelete `true` when the entry is being deleted; the `after`
* snapshot is not pushed for deleted entries.
* @param before Pre-transaction SLE, or `nullptr` for newly created entries.
* @param after Post-transaction SLE; must be non-null.
*/
void
visitEntry(bool, std::shared_ptr<SLE const> const&, std::shared_ptr<SLE const> const&);
visitEntry(bool isDelete, std::shared_ptr<SLE const> const& before, std::shared_ptr<SLE const> const& after);
/** Phase-2 invariant evaluation: verify vault consistency after transaction.
*
* Called once after all `visitEntry` calls complete. Returns `true` if
* all relevant invariants pass (or the transaction did not touch any vault
* objects), `false` if a violation is detected and `featureSingleAssetVault`
* is active.
*
* Short-circuits immediately on non-`tesSUCCESS` results. Per-transaction
* invariants checked include:
* - At most one vault is created or modified per transaction.
* - Immutable fields (`sfAsset`, `sfAccount`, `sfShareMPTID`) never change.
* - `assetsAvailable` ≤ `assetsTotal` ≥ 0; `assetsMaximum` ≥ 0.
* - `lossUnrealized` ≤ `assetsTotal assetsAvailable`.
* - `lossUnrealized` changes only for `ttLOAN_MANAGE` / `ttLOAN_PAY`.
* - Asset and share conservation per transaction type (deposit, withdraw,
* clawback, set, create, delete).
* - Deletion co-deletes the share issuance and requires zero assets/shares.
* - Creation produces an empty vault whose pseudo-account back-links via
* `sfVaultID`.
* - Only transactions with `MustModifyVault` or `MayModifyVault` privilege
* may touch vault state at all.
*
* @param tx The applied transaction.
* @param ret Final TER result of the transaction.
* @param fee Transaction fee in drops (compensated in XRP-vault delta checks).
* @param view Current ledger view for read-only fallback lookups.
* @param j Journal for `fatal`-level invariant-failure diagnostics.
* @return `true` if invariants pass or are not applicable; `false` on violation.
*/
bool
finalize(STTx const&, TER const, XRPAmount const, ReadView const&, beast::Journal const&);
finalize(STTx const& tx, TER const ret, XRPAmount const fee, ReadView const& view, beast::Journal const& j);
// Compute the coarsest scale required to represent all numbers
/** Return the coarsest (largest) scale across a set of `DeltaInfo` values.
*
* Invariant comparisons mix values computed at different precisions
* (e.g., XRP drops vs. IOU mantissa-exponent pairs). Rounding all operands
* to the coarsest scale prevents spurious equality failures caused by
* sub-precision differences.
*
* @param numbers Collection of `DeltaInfo` values, each carrying an
* optional scale. An absent scale indicates the value has not yet been
* assigned a precision context (should not occur in well-formed inputs).
* @return The maximum scale value found, or `0` if `numbers` is empty.
*/
[[nodiscard]] static std::int32_t
computeCoarsestScale(std::vector<DeltaInfo> const& numbers);
};

View File

@@ -1,3 +1,15 @@
/** @file
* Declares `AMMLiquidity`, the adapter that exposes an on-ledger Automated
* Market Maker pool as a sequence of synthetic offers to the XRPL payment
* engine's `BookStep` traversal layer.
*
* `AMMLiquidity` produces `AMMOffer<TIn,TOut>` objects — virtual offers sized
* from live pool state — so that `BookStep` can consume AMM liquidity
* identically to CLOB (Central Limit Order Book) offers during path execution.
* Two sizing strategies are used depending on whether the payment traverses
* a single path or multiple paths.
*/
#pragma once
#include <xrpl/basics/Log.h>
@@ -13,35 +25,77 @@ namespace xrpl {
template <StepAmount TIn, StepAmount TOut>
class AMMOffer;
/** AMMLiquidity class provides AMM offers to BookStep class.
* The offers are generated in two ways. If there are multiple
* paths specified to the payment transaction then the offers
* are generated based on the Fibonacci sequence with
* a limited number of payment engine iterations consuming AMM offers.
* These offers behave the same way as CLOB offers in that if
* there is a limiting step, then the offers are adjusted
* based on their quality.
* If there is only one path specified in the payment transaction
* then the offers are generated based on the competing CLOB offer
* quality. In this case the offer's size is set in such a way
* that the new AMM's pool spot price quality is equal to the CLOB's
* offer quality.
/** Adapts an on-ledger AMM pool to the payment engine's offer-based interface.
*
* `BookStep` iterates over discrete offers sorted by quality. `AMMLiquidity`
* bridges the continuous-liquidity AMM model into that interface by generating
* synthetic `AMMOffer<TIn,TOut>` objects on demand from live pool state.
*
* Two sizing strategies are selected at construction time via `AMMContext`:
* - **Multi-path** (`ammContext_.multiPath()` true): `generateFibSeqOffer()`
* emits exponentially growing offers keyed to the iteration count, so each
* path strand gets a modest initial slice and larger slices only after
* prior iterations have established price quality.
* - **Single-path**: `changeSpotPriceQuality()` computes the exact swap that
* moves the pool's spot price to the competing CLOB offer's quality level,
* maximising value extraction in a single pass.
*
* @tparam TIn Amount type for the pool's input asset (`IOUAmount`,
* `XRPAmount`, or `MPTAmount`).
* @tparam TOut Amount type for the pool's output asset (`IOUAmount`,
* `XRPAmount`, or `MPTAmount`).
*
* @note Not copyable. `AMMLiquidity` holds a mutable reference to `AMMContext`
* (shared state) and an immutable snapshot of pool balances captured at
* construction; a copy would alias that state without representing a
* coherent point in time. `BookStep` stores instances via
* `std::optional::emplace()` to avoid copies.
* @note Explicitly instantiated for all eight valid `(TIn, TOut)` pairs in
* `AMMLiquidity.cpp`; do not add implicit instantiations elsewhere.
*/
template <typename TIn, typename TOut>
class AMMLiquidity
{
private:
/** Base fraction of `initialBalances_.in` used for the first Fibonacci
* offer (iteration 0). Equals 5/20000 = 0.025% of the initial input
* balance, keeping the opening offer small relative to the pool.
*/
inline static Number const kINITIAL_FIB_SEQ_PCT = Number(5) / 20000;
AMMContext& ammContext_;
AccountID const ammAccountID_;
std::uint32_t const tradingFee_;
Asset const assetIn_;
Asset const assetOut_;
// Initial AMM pool balances
/** Pool balances captured at construction time.
*
* Used as the scaling base for Fibonacci offer sizes in
* `generateFibSeqOffer()`. Keeping these fixed across iterations ensures
* offer sizes are deterministic given the same starting state; using live
* balances would create feedback loops where earlier iterations change
* the sizes of later ones unpredictably.
*/
TAmounts<TIn, TOut> const initialBalances_;
beast::Journal const j_;
public:
/** Construct an `AMMLiquidity` for the given AMM account.
*
* Immediately fetches pool balances from `view` and stores them in
* `initialBalances_` for use as a Fibonacci scaling base.
*
* @param view Read-only ledger view used to fetch initial
* pool balances.
* @param ammAccountID On-ledger account ID of the AMM pool.
* @param tradingFee AMM trading fee in basis points (01000).
* @param in Input-side asset of the pool.
* @param out Output-side asset of the pool.
* @param ammContext Shared context tracking iteration count and
* multi-path state; must outlive this object.
* @param j Journal for diagnostic logging.
* @throws std::runtime_error if either pool balance is negative, which
* indicates ledger corruption.
*/
AMMLiquidity(
ReadView const& view,
AccountID const& ammAccountID,
@@ -55,10 +109,33 @@ public:
AMMLiquidity&
operator=(AMMLiquidity const&) = delete;
/** Generate AMM offer. Returns nullopt if clobQuality is provided
* and it is better than AMM offer quality. Otherwise returns AMM offer.
* If clobQuality is provided then AMM offer size is set based on the
* quality.
/** Generate a synthetic AMM offer for the current payment engine iteration.
*
* Returns `std::nullopt` without generating an offer when:
* - `AMMContext::maxItersReached()` is true (30-iteration cap exhausted),
* - Either pool balance is zero (frozen account),
* - The pool's current spot-price quality is less than or within 1e-7 of
* `clobQuality` (AMM cannot profitably compete), or
* - The chosen sizing strategy produces an offer of zero size or overflow.
*
* Strategy selection:
* - **Multi-path**: delegates to `generateFibSeqOffer()`, then discards
* the result if its quality is below `clobQuality`.
* - **Single-path, no CLOB**: delegates to `maxOffer()`; `BookStep` will
* trim the offer to the actual delivery limit.
* - **Single-path, with CLOB**: uses `changeSpotPriceQuality()` to size
* the offer so that full consumption moves the spot price to exactly
* `clobQuality`. Falls back to `maxOffer()` under `fixAMMv1_2` if
* `changeSpotPriceQuality()` returns nothing and `maxOffer()` beats
* `clobQuality`. On `std::overflow_error` (pre-`fixAMMOverflowOffer`)
* falls back to `maxOffer()` rather than propagating.
*
* @param view Current read-only ledger view; used to fetch live
* pool balances and check active amendments.
* @param clobQuality Quality of the best competing CLOB offer, or
* `std::nullopt` if no CLOB offer is available on this strand.
* @return A synthetic `AMMOffer` priced from live pool state, or
* `std::nullopt` if the AMM has no profitable offer to contribute.
*/
[[nodiscard]] std::optional<AMMOffer<TIn, TOut>>
getOffer(ReadView const& view, std::optional<Quality> const& clobQuality) const;
@@ -100,29 +177,51 @@ public:
}
private:
/** Fetches current AMM balances.
/** Fetch live pool balances from the ledger.
*
* @param view Read-only ledger view.
* @return Balances as a `TAmounts<TIn, TOut>` pair.
* @throws std::runtime_error if either balance is negative, indicating
* ledger corruption (the AMM invariant checker guarantees non-negative
* balances under normal operation).
*/
[[nodiscard]] TAmounts<TIn, TOut>
fetchBalances(ReadView const& view) const;
/** Generate AMM offers with the offer size based on Fibonacci sequence.
* The sequence corresponds to the payment engine iterations with AMM
* liquidity. Iterations that don't consume AMM offers don't count.
* The number of iterations with AMM offers is limited.
* If the generated offer exceeds the pool balance then the function
* throws overflow exception.
/** Compute the offer size for one multi-path engine iteration using the
* Fibonacci sequence.
*
* The base offer (`curIters == 0`) is `kINITIAL_FIB_SEQ_PCT × initialBalances_.in`
* (0.025% of the input balance at construction). For subsequent iterations
* the output amount is scaled by `kFIB[curIters - 1]`, a hard-coded
* 30-entry Fibonacci table matching `AMMContext::kMAX_ITERATIONS`.
* Scaling against `initialBalances_` (not live balances) keeps offer
* sizes deterministic across iterations.
*
* @param balances Current live pool balances, used to derive the input
* amount via `swapAssetOut()` and to guard against overflow.
* @return Synthetic `TAmounts` representing the offer's `{in, out}` pair.
* @throws std::overflow_error if the computed output equals or exceeds
* `balances.out`; the caller (`getOffer`) catches this and falls back
* to `std::nullopt` or `maxOffer()` depending on the active amendments.
*/
[[nodiscard]] TAmounts<TIn, TOut>
generateFibSeqOffer(TAmounts<TIn, TOut> const& balances) const;
/** Generate max offer.
* If `fixAMMOverflowOffer` is active, the offer is generated as:
* takerGets = 99% * balances.out takerPays = swapOut(takerGets).
* Return nullopt if takerGets is 0 or takerGets == balances.out.
/** Construct the largest safe synthetic offer against the pool.
*
* If `fixAMMOverflowOffer` is not active, the offer is generated as:
* takerPays = max input amount;
* takerGets = swapIn(takerPays).
* Behaviour depends on the `fixAMMOverflowOffer` amendment:
* - **Active**: `takerGets = 99% × balances.out` (rounded down);
* `takerPays = swapAssetOut(takerGets)`. Returns `std::nullopt` if the
* 99% cap rounds to zero or equals `balances.out` (degenerate pool).
* - **Inactive** (legacy): `takerPays = maxAmount<TIn>()` (protocol
* ceiling); `takerGets = swapAssetIn(takerPays)`. This path could
* overflow on large pools — the bug that motivated the amendment.
*
* @param balances Current live pool balances.
* @param rules Active amendment rules, used to gate `fixAMMOverflowOffer`.
* @return The maximum-size `AMMOffer`, or `std::nullopt` if the pool is
* too small to produce a valid offer under the fixed path.
*/
[[nodiscard]] std::optional<AMMOffer<TIn, TOut>>
maxOffer(TAmounts<TIn, TOut> const& balances, Rules const& rules) const;

View File

@@ -1,3 +1,8 @@
/** @file
* Implements `extractTarLz4`, the decompression primitive used when
* bootstrapping an XRPL node from a pre-built ledger database snapshot.
*/
#include <xrpl/basics/Archive.h>
#include <xrpl/basics/contract.h>
@@ -14,6 +19,37 @@
namespace xrpl {
/** Decompress and extract a `.tar.lz4` archive into a destination directory.
*
* Uses the libarchive two-handle idiom: a read handle (`ar`) for
* decompression and TAR parsing, and a disk-write handle (`aw`) for
* filesystem output. Both handles are managed by RAII `unique_ptr` with
* custom deleters so they are released on any exit path, including throws.
*
* The reader is narrowed explicitly to TAR + LZ4 rather than using
* auto-detect, so passing a different archive format fails early with a
* clear error. The writer restores timestamps, permissions, ACLs, and file
* flags (`ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL
* | ARCHIVE_EXTRACT_FFLAGS`) and resolves user/group names via the local
* system's user database.
*
* Every stored entry pathname is prefixed with `dst` before writing, so all
* output lands under the caller-supplied destination regardless of the paths
* recorded in the archive.
*
* @param src Path to the `.tar.lz4` source file; must be a regular file.
* @param dst Destination directory under which all entries are extracted.
* @throws std::runtime_error if `src` is not a regular file, if any
* libarchive call fails, or if a filesystem write error occurs. On
* error mid-extraction the destination is left in a partial state;
* callers that require atomicity must clean up themselves.
* @note The pathname rewrite prepends `dst` but does not strip `..`
* components from stored entry names. Archives from untrusted sources
* could use paths such as `../../etc/cron.d/evil` to escape `dst`.
* This is acceptable for the trusted first-party snapshots this
* function was designed for, but callers should be aware of the
* limitation before using it against untrusted input.
*/
void
extractTarLz4(boost::filesystem::path const& src, boost::filesystem::path const& dst)
{
@@ -31,7 +67,7 @@ extractTarLz4(boost::filesystem::path const& src, boost::filesystem::path const&
if (archive_read_support_filter_lz4(ar.get()) < ARCHIVE_OK)
Throw<std::runtime_error>(archive_error_string(ar.get()));
// Examples suggest this block size
// 10 240-byte block size matches libarchive's own example code.
if (archive_read_open_filename(ar.get(), src.string().c_str(), 10240) < ARCHIVE_OK)
{
Throw<std::runtime_error>(archive_error_string(ar.get()));

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