mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 00:36:48 +00:00
part 1
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 0–570 have 1–3 characters; words 571–2047 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 1–4 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 64–65), 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 0–570)
|
||||
* are words of 1–3 characters while the remaining 1477 (indices
|
||||
* 571–2047) 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 0–570 contain words of 1–3 characters; indices 571–2047
|
||||
* contain words of exactly 4 characters. This structural split is
|
||||
* relied upon by `wsrch()` to restrict binary-search ranges.
|
||||
*/
|
||||
static char const* dictionary[];
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 Merkle–radix 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.
|
||||
/** Merkle–radix 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.
|
||||
/** Merkle–radix 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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 0–99 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 (0–99).
|
||||
*/
|
||||
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
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 0–7: 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();
|
||||
|
||||
|
||||
@@ -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 0–7: 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
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 1–3.
|
||||
* @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 1–3 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);
|
||||
|
||||
@@ -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 (0–126) 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 0–6 (range 0–126); 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)
|
||||
|
||||
@@ -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 (0–19) 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)
|
||||
|
||||
@@ -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 25–34 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 <>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 1–3). 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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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^16−1]` (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^16−1]` 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^16−1]`.
|
||||
* 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_;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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 (2–120).
|
||||
* 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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 (0–15).
|
||||
* @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 (0–15).
|
||||
* @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 (0–15).
|
||||
* @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 (0–15).
|
||||
* @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 (0–15); 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 (0–15); 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 (0–15); 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 (0–15); 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);
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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, 0–15.
|
||||
* @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, 0–64.
|
||||
* @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 (0–15) 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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (0–3) 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 (0–15) 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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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. **Cover–balance lower bound** — `sfCoverAvailable` must be ≥ the
|
||||
* pseudo-account's actual asset balance (via `accountHolds`).
|
||||
* 6. **Cover–balance 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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&);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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&);
|
||||
};
|
||||
|
||||
@@ -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&);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 (0–1000).
|
||||
* @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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user