mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-23 07:00:21 +00:00
Compare commits
1 Commits
dangell7/d
...
dangell7/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88794a1ea9 |
@@ -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,
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/** @file
|
||||
* Declares `ApplyViewBase`, the abstract concrete base class shared by all
|
||||
* buffered mutable ledger views used during transaction application.
|
||||
*
|
||||
* `ApplyViewBase` lives in `xrpl::detail` to signal that it is internal
|
||||
* infrastructure; transaction processing code works with `ApplyView` or
|
||||
* `ApplyViewImpl` references. The three concrete subclasses —
|
||||
* `ApplyViewImpl`, `Sandbox`, and `PaymentSandbox` — are the only types
|
||||
* that need to reach into this layer directly.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/ledger/ApplyView.h>
|
||||
@@ -7,6 +18,24 @@
|
||||
|
||||
namespace xrpl::detail {
|
||||
|
||||
/** Concrete base for buffered mutable ledger views.
|
||||
*
|
||||
* Implements the full `ApplyView` and `RawView` interfaces on top of two
|
||||
* members: a read-only pointer to the base ledger snapshot (`base_`) and
|
||||
* an `ApplyStateTable` change buffer (`items_`). Queries that need
|
||||
* awareness of pending mutations (e.g. `exists`, `read`, `peek`) merge
|
||||
* `items_` with `base_`; purely structural queries (`header`, `fees`,
|
||||
* `rules`) and SLE iterators bypass `items_` and forward directly to
|
||||
* `base_`.
|
||||
*
|
||||
* Not copyable; move-constructible only. Subclasses (`ApplyViewImpl`,
|
||||
* `Sandbox`) supply lifecycle logic such as `apply()`.
|
||||
*
|
||||
* @note The `erase()` and `update()` methods enforce pointer identity: the
|
||||
* caller must pass the exact `shared_ptr` returned by `peek()` on
|
||||
* **this** view instance. Passing an SLE obtained from a different view
|
||||
* results in a `LogicError`.
|
||||
*/
|
||||
class ApplyViewBase : public ApplyView, public RawView
|
||||
{
|
||||
public:
|
||||
@@ -19,85 +48,254 @@ public:
|
||||
|
||||
ApplyViewBase(ApplyViewBase&&) = default;
|
||||
|
||||
/** Construct over an existing read-only ledger snapshot.
|
||||
*
|
||||
* @param base Non-owning pointer to the base ledger state; must
|
||||
* outlive this view. All reads that bypass the change buffer
|
||||
* are forwarded here.
|
||||
* @param flags Per-transaction policy flags (retry mode, dry-run,
|
||||
* unlimited, etc.) that are carried through the apply pass and
|
||||
* exposed via `flags()`.
|
||||
*/
|
||||
ApplyViewBase(ReadView const* base, ApplyFlags flags);
|
||||
|
||||
// ReadView
|
||||
|
||||
/** @return `true` if the underlying view represents an open ledger. */
|
||||
[[nodiscard]] bool
|
||||
open() const override;
|
||||
|
||||
/** @return The ledger header from the base snapshot. */
|
||||
[[nodiscard]] LedgerHeader const&
|
||||
header() const override;
|
||||
|
||||
/** @return The fee schedule from the base snapshot. */
|
||||
[[nodiscard]] Fees const&
|
||||
fees() const override;
|
||||
|
||||
/** @return The amendment rules from the base snapshot. */
|
||||
[[nodiscard]] Rules const&
|
||||
rules() const override;
|
||||
|
||||
/** Test whether a ledger object exists, accounting for pending changes.
|
||||
*
|
||||
* Returns `false` for objects pending erasure, `true` for objects
|
||||
* buffered as inserted or modified, and delegates to `base_` for
|
||||
* keys not yet in the change buffer.
|
||||
*
|
||||
* @param k Keylet identifying the object.
|
||||
* @return `true` if the object will exist after the pending changes.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
exists(Keylet const& k) const override;
|
||||
|
||||
/** Find the next live key after `key`, accounting for pending changes.
|
||||
*
|
||||
* Merges the base key space (skipping keys pending deletion) with the
|
||||
* local change buffer (skipping erased entries) and returns the smaller
|
||||
* candidate key that is less than `last`.
|
||||
*
|
||||
* @param key Exclusive lower bound.
|
||||
* @param last Optional exclusive upper bound.
|
||||
* @return The next live key, or `std::nullopt` if none exists in range.
|
||||
*/
|
||||
[[nodiscard]] std::optional<key_type>
|
||||
succ(key_type const& key, std::optional<key_type> const& last = std::nullopt) const override;
|
||||
|
||||
/** Read a ledger object as an immutable snapshot, accounting for pending
|
||||
* changes.
|
||||
*
|
||||
* Returns `nullptr` for objects pending erasure or when the keylet check
|
||||
* fails; returns the buffered SLE for inserted/modified entries; falls
|
||||
* back to `base_` for unknown keys.
|
||||
*
|
||||
* @param k Keylet identifying the object.
|
||||
* @return A `const`-qualified handle to the SLE, or `nullptr`.
|
||||
*/
|
||||
[[nodiscard]] std::shared_ptr<SLE const>
|
||||
read(Keylet const& k) const override;
|
||||
|
||||
/** @name SLE iterators (base snapshot only)
|
||||
*
|
||||
* These iterators forward directly to `base_` and do **not** reflect
|
||||
* pending insertions or deletions in the change buffer. This is
|
||||
* intentional: the apply phase never needs to iterate its own buffered
|
||||
* writes, and bypassing the buffer keeps SLE traversal consistent with
|
||||
* the base ledger snapshot.
|
||||
*/
|
||||
/** @{ */
|
||||
[[nodiscard]] std::unique_ptr<SlesType::iter_base>
|
||||
slesBegin() const override;
|
||||
|
||||
[[nodiscard]] std::unique_ptr<SlesType::iter_base>
|
||||
slesEnd() const override;
|
||||
|
||||
/** Return an iterator to the first SLE whose key is not less than `key`,
|
||||
* drawn from the base snapshot only.
|
||||
*
|
||||
* @param key The lower-bound key for the search.
|
||||
* @return An iterator into the base SLE map at or after `key`.
|
||||
*/
|
||||
[[nodiscard]] std::unique_ptr<SlesType::iter_base>
|
||||
slesUpperBound(uint256 const& key) const override;
|
||||
/** @} */
|
||||
|
||||
/** @name Transaction-map accessors (forwarded to base snapshot) */
|
||||
/** @{ */
|
||||
[[nodiscard]] std::unique_ptr<TxsType::iter_base>
|
||||
txsBegin() const override;
|
||||
|
||||
[[nodiscard]] std::unique_ptr<TxsType::iter_base>
|
||||
txsEnd() const override;
|
||||
|
||||
/** Test whether a transaction exists in the base snapshot's tx-map.
|
||||
*
|
||||
* @param key The transaction ID to look up.
|
||||
* @return `true` if the transaction is present in the base ledger's
|
||||
* transaction map.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
txExists(key_type const& key) const override;
|
||||
|
||||
/** Read a transaction and its metadata from the base snapshot's tx-map.
|
||||
*
|
||||
* @param key The transaction ID to retrieve.
|
||||
* @return A pair of `(STTx, STObject metadata)` for the transaction,
|
||||
* or `{nullptr, nullptr}` if not found.
|
||||
*/
|
||||
[[nodiscard]] tx_type
|
||||
txRead(key_type const& key) const override;
|
||||
/** @} */
|
||||
|
||||
// ApplyView
|
||||
|
||||
/** Return the flags governing this transaction apply pass.
|
||||
*
|
||||
* @return The `ApplyFlags` bitmask set at construction.
|
||||
*/
|
||||
[[nodiscard]] ApplyFlags
|
||||
flags() const override;
|
||||
|
||||
/** Check out a ledger entry for in-place mutation.
|
||||
*
|
||||
* Loads the entry into the change buffer on first access (tagged
|
||||
* `Cache`). Returns the same `shared_ptr` on subsequent calls.
|
||||
* The returned pointer must later be passed to `update()` or `erase()`
|
||||
* on **this** view instance to record the intended change.
|
||||
*
|
||||
* @param k Keylet identifying the entry.
|
||||
* @return A mutable handle to the buffered SLE, or `nullptr` if the
|
||||
* entry does not exist (including if it is pending erasure).
|
||||
*/
|
||||
std::shared_ptr<SLE>
|
||||
peek(Keylet const& k) override;
|
||||
|
||||
/** Stage a deletion for a checked-out entry.
|
||||
*
|
||||
* Transitions the buffer entry from `Cache` or `Modify` to `Erase`.
|
||||
* If the entry was inserted within this same transaction, it is removed
|
||||
* entirely (net-zero effect on the base).
|
||||
*
|
||||
* @param sle The exact `shared_ptr` previously returned by `peek()`
|
||||
* on this view instance.
|
||||
* @throws std::logic_error If the pointer does not match the buffered
|
||||
* entry or the entry is already erased.
|
||||
*/
|
||||
void
|
||||
erase(std::shared_ptr<SLE> const& sle) override;
|
||||
|
||||
/** Stage a new ledger entry for insertion.
|
||||
*
|
||||
* Records the SLE under `Action::Insert`. If the key was previously
|
||||
* erased within this transaction the action is collapsed to `Modify`.
|
||||
*
|
||||
* @param sle The new entry; its key must not already exist in the view.
|
||||
* @throws std::logic_error If the key already exists with a non-erase
|
||||
* action in the buffer.
|
||||
*/
|
||||
void
|
||||
insert(std::shared_ptr<SLE> const& sle) override;
|
||||
|
||||
/** Promote a checked-out entry to a definitive write.
|
||||
*
|
||||
* Transitions the buffer action from `Cache` to `Modify`; `Insert` and
|
||||
* `Modify` entries are left unchanged (already write-intent).
|
||||
*
|
||||
* @param sle The exact `shared_ptr` previously returned by `peek()`
|
||||
* on this view instance.
|
||||
* @throws std::logic_error If the pointer does not match, the entry is
|
||||
* erased, or the key is unknown.
|
||||
*/
|
||||
void
|
||||
update(std::shared_ptr<SLE> const& sle) override;
|
||||
|
||||
// RawView
|
||||
|
||||
/** Erase a ledger entry without enforcing pointer-identity ownership.
|
||||
*
|
||||
* Bypasses the `peek()`-pointer ownership check enforced by `erase()`.
|
||||
* Used by `RawView` callers (e.g. `Sandbox::apply`) that flush changes
|
||||
* from another view's table and cannot satisfy the same-instance
|
||||
* invariant.
|
||||
*
|
||||
* @param sle An SLE whose key identifies the object to erase.
|
||||
* @throws std::logic_error If the object is already pending erasure.
|
||||
*/
|
||||
void
|
||||
rawErase(std::shared_ptr<SLE> const& sle) override;
|
||||
|
||||
/** Insert a ledger entry via the same validated path as `insert()`.
|
||||
*
|
||||
* Despite being a raw-tier operation, this method calls the same
|
||||
* `items_.insert()` that the high-level `insert()` uses; the
|
||||
* distinction is that callers from the `RawView` flush path are not
|
||||
* required to have obtained the SLE from `peek()`.
|
||||
*
|
||||
* @param sle The new entry to stage for insertion.
|
||||
* @throws std::logic_error If the key already exists with a non-erase
|
||||
* action.
|
||||
*/
|
||||
void
|
||||
rawInsert(std::shared_ptr<SLE> const& sle) override;
|
||||
|
||||
/** Unconditionally overwrite the buffered SLE for an existing key.
|
||||
*
|
||||
* Records the SLE under `Action::Modify`, replacing any `Cache` or
|
||||
* `Insert` entry. Unlike `update()`, does not enforce pointer identity.
|
||||
*
|
||||
* @param sle The SLE to store; its key must exist in this view.
|
||||
* @throws std::logic_error If the key is currently pending erasure.
|
||||
*/
|
||||
void
|
||||
rawReplace(std::shared_ptr<SLE> const& sle) override;
|
||||
|
||||
/** Record XRP drops destroyed by fees within this transaction's scope.
|
||||
*
|
||||
* Accumulates into the change buffer and is forwarded to the parent
|
||||
* view's `rawDestroyXRP()` when the buffer is committed.
|
||||
*
|
||||
* @param feeDrops The amount of XRP to permanently remove from
|
||||
* circulation.
|
||||
*/
|
||||
void
|
||||
rawDestroyXRP(XRPAmount const& feeDrops) override;
|
||||
|
||||
protected:
|
||||
/** Per-transaction policy flags set at construction; exposed via `flags()`. */
|
||||
ApplyFlags flags_;
|
||||
|
||||
/** Non-owning pointer to the base ledger snapshot.
|
||||
*
|
||||
* All reads that do not need awareness of pending changes are forwarded
|
||||
* here. The pointed-to view must outlive this object.
|
||||
*/
|
||||
ReadView const* base_;
|
||||
|
||||
/** Change buffer accumulating per-transaction ledger mutations.
|
||||
*
|
||||
* Maps each touched `uint256` key to an `(Action, SLE)` pair. Flushed
|
||||
* to the parent view atomically on `apply()`; discarded on destruction.
|
||||
*/
|
||||
detail::ApplyStateTable items_;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,27 +11,73 @@
|
||||
|
||||
namespace xrpl::detail {
|
||||
|
||||
// Helper class that buffers raw modifications
|
||||
/** In-memory write buffer that accumulates SLE mutations before flushing them
|
||||
* to a backing `RawView`.
|
||||
*
|
||||
* Every mutable ledger view (`OpenView`, and indirectly `ApplyStateTable`)
|
||||
* embeds a `RawStateTable` as its delta accumulator. The three mutation
|
||||
* methods — `erase`, `insert`, and `replace` — apply a state-machine
|
||||
* collapse so the map stays minimal: insert-then-erase cancels out entirely;
|
||||
* erase-then-insert upgrades to replace; and illegal sequences (double-erase,
|
||||
* double-insert) throw `std::logic_error`. `read`, `exists`, and `succ`
|
||||
* overlay the pending delta transparently onto the supplied base `ReadView`,
|
||||
* so callers always see a coherent merged state. Once a transaction succeeds,
|
||||
* `apply()` flushes the buffer to the target `RawView` in a single pass.
|
||||
*
|
||||
* The `items_` map uses a `boost::container::pmr::monotonic_buffer_resource`
|
||||
* with a 256 KB initial arena for O(1) amortised allocation during the burst
|
||||
* of mutations that constitute a single transaction round. Because the
|
||||
* resource cannot be shared or assigned, copy construction allocates a fresh
|
||||
* resource and deep-copies the map; move construction transfers the
|
||||
* `unique_ptr` directly. Both assignment operators are deleted.
|
||||
*
|
||||
* XRP fee destruction is tracked separately in `dropsDestroyed_` and
|
||||
* replayed as a single `rawDestroyXRP` call during `apply()`.
|
||||
*
|
||||
* @note This class is an internal implementation detail of `OpenView`.
|
||||
* Transaction logic should not interact with it directly; use the
|
||||
* `RawView` interface instead.
|
||||
* @see OpenView, RawView
|
||||
*/
|
||||
class RawStateTable
|
||||
{
|
||||
public:
|
||||
using key_type = ReadView::key_type;
|
||||
// Initial size for the monotonic_buffer_resource used for allocations
|
||||
// The size was chosen from the old `qalloc` code (which this replaces).
|
||||
// It is unclear how the size initially chosen in qalloc.
|
||||
|
||||
/** Initial arena size for the PMR monotonic buffer resource.
|
||||
*
|
||||
* Inherited from the legacy `qalloc` scheme this replaced. The 256 KB
|
||||
* budget covers the typical per-transaction working set without triggering
|
||||
* heap growth for the common case.
|
||||
*/
|
||||
static constexpr size_t kINITIAL_BUFFER_SIZE = kilobytes(256);
|
||||
|
||||
/** Construct an empty table with a fresh 256 KB monotonic arena. */
|
||||
RawStateTable()
|
||||
: monotonic_resource_{std::make_unique<boost::container::pmr::monotonic_buffer_resource>(
|
||||
kINITIAL_BUFFER_SIZE)}
|
||||
, items_{monotonic_resource_.get()} {};
|
||||
|
||||
/** Copy-construct by allocating a fresh monotonic arena and copying items.
|
||||
*
|
||||
* The SLE `shared_ptr` values in `items_` are shared with the source —
|
||||
* not deep-copied — which is safe because SLEs are immutable once
|
||||
* published. `dropsDestroyed_` is copied verbatim.
|
||||
*
|
||||
* @param rhs The source table to copy.
|
||||
*/
|
||||
RawStateTable(RawStateTable const& rhs)
|
||||
: monotonic_resource_{std::make_unique<boost::container::pmr::monotonic_buffer_resource>(
|
||||
kINITIAL_BUFFER_SIZE)}
|
||||
, items_{rhs.items_, monotonic_resource_.get()}
|
||||
, dropsDestroyed_{rhs.dropsDestroyed_} {};
|
||||
|
||||
/** Move-construct by transferring the monotonic resource and items map.
|
||||
*
|
||||
* After the move, the source table is left in a valid but empty state.
|
||||
* The `unique_ptr` transfer preserves the stable address that `items_`'
|
||||
* `polymorphic_allocator` holds.
|
||||
*/
|
||||
RawStateTable(RawStateTable&&) = default;
|
||||
|
||||
RawStateTable&
|
||||
@@ -39,48 +85,166 @@ public:
|
||||
RawStateTable&
|
||||
operator=(RawStateTable const&) = delete;
|
||||
|
||||
/** Flush all buffered mutations to a backing `RawView`.
|
||||
*
|
||||
* First calls `to.rawDestroyXRP(dropsDestroyed_)` to replay accumulated
|
||||
* fee burns, then iterates `items_` and dispatches each pending action
|
||||
* to the corresponding `rawErase`, `rawInsert`, or `rawReplace` method.
|
||||
* The table is not cleared after apply; this object should be discarded
|
||||
* or destroyed once flushed.
|
||||
*
|
||||
* @param to The target `RawView` that receives all buffered mutations.
|
||||
*/
|
||||
void
|
||||
apply(RawView& to) const;
|
||||
|
||||
/** Test whether an SLE exists, overlaying the pending delta onto `base`.
|
||||
*
|
||||
* Checks the pending buffer first: a pending erase returns `false`; a
|
||||
* pending insert or replace returns `true` only if `k.check()` passes
|
||||
* (type-tag validation). Falls through to `base.exists(k)` when the key
|
||||
* has no pending action.
|
||||
*
|
||||
* @param base The underlying read-only ledger state.
|
||||
* @param k The keylet specifying key and expected SLE type.
|
||||
* @return `true` if the entry exists and its type satisfies `k.check()`.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
exists(ReadView const& base, Keylet const& k) const;
|
||||
|
||||
/** Find the smallest key strictly greater than `key` in the merged state.
|
||||
*
|
||||
* Runs two parallel searches: (1) walks `base.succ()` repeatedly,
|
||||
* skipping any base key that has a pending `Action::Erase`; (2) scans
|
||||
* `items_` forward from `key` for the first non-erase entry. Returns
|
||||
* the lower of the two candidates. If `last` is given and the result is
|
||||
* `>= last`, returns `std::nullopt` (half-open range semantics).
|
||||
*
|
||||
* @param base The underlying read-only ledger state.
|
||||
* @param key Exclusive lower bound; the search begins strictly after this.
|
||||
* @param last Optional exclusive upper bound; `std::nullopt` means unbounded.
|
||||
* @return The next existing key, or `std::nullopt` if none is in range.
|
||||
*/
|
||||
[[nodiscard]] std::optional<key_type>
|
||||
succ(ReadView const& base, key_type const& key, std::optional<key_type> const& last) const;
|
||||
|
||||
/** Stage an SLE deletion, applying state-machine transition rules.
|
||||
*
|
||||
* Transitions on the key's existing pending action:
|
||||
* - None → records `Action::Erase`.
|
||||
* - `Insert` → removes the entry entirely (net-zero; base is unaffected).
|
||||
* - `Replace` → downgrades to `Action::Erase`.
|
||||
* - `Erase` → `LogicError` (double-delete).
|
||||
*
|
||||
* @param sle The ledger entry to stage for deletion; key is taken from the SLE.
|
||||
* @throws std::logic_error if the key already has a pending erase.
|
||||
*/
|
||||
void
|
||||
erase(std::shared_ptr<SLE> const& sle);
|
||||
|
||||
/** Stage an SLE creation, applying state-machine transition rules.
|
||||
*
|
||||
* Transitions on the key's existing pending action:
|
||||
* - None → records `Action::Insert`.
|
||||
* - `Erase` → upgrades to `Action::Replace` (delete-then-recreate in
|
||||
* the same transaction batch).
|
||||
* - `Insert` → `LogicError` (duplicate insert).
|
||||
* - `Replace` → `LogicError` (key already present in the delta).
|
||||
*
|
||||
* @param sle The new ledger entry to stage; key is taken from the SLE.
|
||||
* @throws std::logic_error if the key is already pending insert or replace.
|
||||
*/
|
||||
void
|
||||
insert(std::shared_ptr<SLE> const& sle);
|
||||
|
||||
/** Stage an SLE field update, applying state-machine transition rules.
|
||||
*
|
||||
* Transitions on the key's existing pending action:
|
||||
* - None → records `Action::Replace`.
|
||||
* - `Insert` → updates the stored SLE pointer; preserves `Insert`
|
||||
* because from the base's perspective the key is still being created.
|
||||
* - `Replace` → updates the stored SLE pointer.
|
||||
* - `Erase` → `LogicError` (cannot replace a deleted key).
|
||||
*
|
||||
* @param sle The updated ledger entry to stage; key is taken from the SLE.
|
||||
* @throws std::logic_error if the key has a pending erase.
|
||||
*/
|
||||
void
|
||||
replace(std::shared_ptr<SLE> const& sle);
|
||||
|
||||
/** Read an SLE, overlaying the pending delta onto `base`.
|
||||
*
|
||||
* Checks the buffer first: a pending erase returns `nullptr`; a pending
|
||||
* insert or replace returns the buffered SLE if `k.check()` passes
|
||||
* (guards against type mismatches at the same key). Falls through to
|
||||
* `base.read(k)` when the key has no pending action.
|
||||
*
|
||||
* @param base The underlying read-only ledger state.
|
||||
* @param k The keylet specifying key and expected SLE type.
|
||||
* @return The SLE if it exists and the type matches, otherwise `nullptr`.
|
||||
*/
|
||||
[[nodiscard]] std::shared_ptr<SLE const>
|
||||
read(ReadView const& base, Keylet const& k) const;
|
||||
|
||||
/** Accumulate XRP drops to destroy at `apply()` time.
|
||||
*
|
||||
* Drops are not forwarded individually; they accumulate in
|
||||
* `dropsDestroyed_` and are replayed as a single `rawDestroyXRP` call in
|
||||
* `apply()`, keeping fee-burn accounting atomic with the rest of the flush.
|
||||
*
|
||||
* @param fee The quantity of XRP drops to add to the accumulated burn total.
|
||||
*/
|
||||
void
|
||||
destroyXRP(XRPAmount const& fee);
|
||||
|
||||
/** Return a begin iterator for the merged SLE range over `base` and the delta.
|
||||
*
|
||||
* The returned iterator implements the two-pointer merge defined by
|
||||
* `SlesIterImpl`: pending inserts appear in sorted position, pending
|
||||
* erases are hidden, and pending replaces shadow the base entry.
|
||||
*
|
||||
* @param base The underlying read-only ledger state to merge with.
|
||||
* @return A heap-allocated `iter_base` positioned at the first merged SLE.
|
||||
*/
|
||||
[[nodiscard]] std::unique_ptr<ReadView::SlesType::iter_base>
|
||||
slesBegin(ReadView const& base) const;
|
||||
|
||||
/** Return an end sentinel for the merged SLE range over `base` and the delta.
|
||||
*
|
||||
* @param base The underlying read-only ledger state to merge with.
|
||||
* @return A heap-allocated `iter_base` positioned past the last merged SLE.
|
||||
*/
|
||||
[[nodiscard]] std::unique_ptr<ReadView::SlesType::iter_base>
|
||||
slesEnd(ReadView const& base) const;
|
||||
|
||||
/** Return an iterator to the first merged SLE with key strictly greater
|
||||
* than `key`.
|
||||
*
|
||||
* @param base The underlying read-only ledger state to merge with.
|
||||
* @param key Exclusive lower bound for the search.
|
||||
* @return A heap-allocated `iter_base` positioned at the first qualifying SLE.
|
||||
*/
|
||||
[[nodiscard]] std::unique_ptr<ReadView::SlesType::iter_base>
|
||||
slesUpperBound(ReadView const& base, uint256 const& key) const;
|
||||
|
||||
private:
|
||||
/** Pending mutation kind for an entry in `items_`. */
|
||||
enum class Action {
|
||||
Erase,
|
||||
Insert,
|
||||
Replace,
|
||||
Erase, /**< Entry is scheduled for deletion. */
|
||||
Insert, /**< Entry is being created; does not yet exist in the base. */
|
||||
Replace, /**< Entry exists in the base and has been modified. */
|
||||
};
|
||||
|
||||
/** Private iterator class that merges base-view SLEs with the pending
|
||||
* delta; defined in the `.cpp`. */
|
||||
class SlesIterImpl;
|
||||
|
||||
/** Pairs a pending `Action` with the SLE it acts on.
|
||||
*
|
||||
* Stored as the mapped value in `items_`. The SLE pointer is always
|
||||
* non-null; for `Erase` it is the last version written before the
|
||||
* deletion was staged (used by `RawView::rawErase`).
|
||||
*/
|
||||
struct SleAction
|
||||
{
|
||||
Action action;
|
||||
@@ -99,11 +263,17 @@ private:
|
||||
SleAction,
|
||||
std::less<key_type>,
|
||||
boost::container::pmr::polymorphic_allocator<std::pair<key_type const, SleAction>>>;
|
||||
|
||||
// monotonic_resource_ must outlive `items_`. Make a pointer so it may be
|
||||
// easily moved.
|
||||
std::unique_ptr<boost::container::pmr::monotonic_buffer_resource> monotonic_resource_;
|
||||
|
||||
/** Ordered map from ledger key to pending mutation; backed by the
|
||||
* monotonic arena for O(1) amortised node allocation. */
|
||||
items_t items_;
|
||||
|
||||
/** Accumulated XRP drops burned by fees; replayed as one `rawDestroyXRP`
|
||||
* call during `apply()`. */
|
||||
XRPAmount dropsDestroyed_{0};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/** @file
|
||||
* Type-erased forward-iterator infrastructure for `ReadView` traversal.
|
||||
*
|
||||
* Defines `ReadViewFwdIter` (the abstract iterator interface) and
|
||||
* `ReadViewFwdRange` (the STL-compatible range wrapper) that together let
|
||||
* any `ReadView` subclass expose its state and transaction maps through a
|
||||
* single, stable iterator type. Callers interact indirectly via
|
||||
* `ReadView::sles` and `ReadView::txs`; this header is internal plumbing.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
@@ -10,8 +20,18 @@ class ReadView;
|
||||
|
||||
namespace detail {
|
||||
|
||||
// A type-erased ForwardIterator
|
||||
//
|
||||
/** Abstract base defining the four primitive operations of a type-erased forward iterator.
|
||||
*
|
||||
* Each concrete `ReadView` implementation provides a private subclass of
|
||||
* this template and hands heap-allocated instances to `ReadViewFwdRange::Iterator`
|
||||
* via the factory methods `slesBegin()`, `slesEnd()`, `slesUpperBound()`,
|
||||
* `txsBegin()`, and `txsEnd()` on `ReadView`. Callers never interact with
|
||||
* this class directly.
|
||||
*
|
||||
* @tparam ValueType The element type yielded by the iterator —
|
||||
* `std::shared_ptr<SLE const>` for state-map iteration or
|
||||
* `ReadView::tx_type` for transaction-map iteration.
|
||||
*/
|
||||
template <class ValueType>
|
||||
class ReadViewFwdIter
|
||||
{
|
||||
@@ -27,21 +47,57 @@ public:
|
||||
|
||||
virtual ~ReadViewFwdIter() = default;
|
||||
|
||||
/** Returns a heap-allocated deep copy of this iterator.
|
||||
*
|
||||
* Provides value-semantics copy for the owning `unique_ptr` wrapper.
|
||||
* Each concrete subclass must return a new instance of itself in the
|
||||
* same position.
|
||||
*
|
||||
* @return A `unique_ptr` to a fresh copy of this iterator instance.
|
||||
*/
|
||||
[[nodiscard]] virtual std::unique_ptr<ReadViewFwdIter>
|
||||
copy() const = 0;
|
||||
|
||||
/** Returns `true` if this iterator denotes the same position as @p impl.
|
||||
*
|
||||
* Both iterators must be over the same underlying view; mixing iterators
|
||||
* from different views produces undefined behavior.
|
||||
*
|
||||
* @param impl The other iterator to compare against.
|
||||
* @return `true` when both iterators point to the same element (or both
|
||||
* are end sentinels).
|
||||
*/
|
||||
[[nodiscard]] virtual bool
|
||||
equal(ReadViewFwdIter const& impl) const = 0;
|
||||
|
||||
/** Advances this iterator to the next element in the sequence. */
|
||||
virtual void
|
||||
increment() = 0;
|
||||
|
||||
/** Returns the element at the current iterator position.
|
||||
*
|
||||
* @return The current `ValueType` value. The result is cached by the
|
||||
* wrapping `Iterator` so repeated dereferences are inexpensive.
|
||||
* @throw May throw if the underlying view operation fails.
|
||||
*/
|
||||
[[nodiscard]] virtual value_type
|
||||
dereference() const = 0;
|
||||
};
|
||||
|
||||
// A range using type-erased ForwardIterator
|
||||
//
|
||||
/** STL-compatible forward range backed by a type-erased iterator.
|
||||
*
|
||||
* Wraps a `ReadViewFwdIter<ValueType>` behind a regular value-type iterator
|
||||
* so that callers can write range-for loops over any `ReadView` subclass
|
||||
* without knowing the concrete iterator type. Virtual dispatch is hidden
|
||||
* inside the `impl_` pointer; the public `Iterator` API is fully inlined.
|
||||
*
|
||||
* `ReadView::SlesType` and `ReadView::TxsType` inherit from this template;
|
||||
* application code should use those types rather than instantiating
|
||||
* `ReadViewFwdRange` directly.
|
||||
*
|
||||
* @tparam ValueType The element type — must be noexcept-move-constructible
|
||||
* so that `Iterator` move operations are noexcept.
|
||||
*/
|
||||
template <class ValueType>
|
||||
class ReadViewFwdRange
|
||||
{
|
||||
@@ -53,6 +109,18 @@ public:
|
||||
"ReadViewFwdRange move and move assign constructors should be "
|
||||
"noexcept");
|
||||
|
||||
/** STL forward iterator over a `ReadViewFwdRange`.
|
||||
*
|
||||
* Value-type wrapper around a heap-allocated `iter_base`. Copy uses
|
||||
* `iter_base::copy()` for a polymorphic deep clone; move transfers
|
||||
* ownership of the `unique_ptr` without allocation and is `noexcept`.
|
||||
* Dereference results are cached in `cache_` and cleared on advance,
|
||||
* amortizing the cost of repeated `*it` or `it->` calls in tight loops.
|
||||
*
|
||||
* @note Comparing iterators from different views triggers an
|
||||
* `XRPL_ASSERT` in debug builds. The `view_` pointer is carried
|
||||
* solely for this cross-view sanity check.
|
||||
*/
|
||||
class Iterator
|
||||
{
|
||||
public:
|
||||
@@ -66,43 +134,127 @@ public:
|
||||
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
|
||||
/** Constructs a singular (default) iterator.
|
||||
*
|
||||
* A default-constructed iterator is not dereferenceable and must
|
||||
* not be incremented. It compares equal only to other
|
||||
* default-constructed iterators.
|
||||
*/
|
||||
Iterator() = default;
|
||||
|
||||
/** Copy-constructs an independent iterator at the same position.
|
||||
*
|
||||
* Calls `iter_base::copy()` to deep-clone the polymorphic
|
||||
* implementation, producing a new iterator that advances
|
||||
* independently of @p other.
|
||||
*
|
||||
* @param other The iterator to clone.
|
||||
*/
|
||||
Iterator(Iterator const& other);
|
||||
|
||||
/** Move-constructs an iterator, transferring ownership of the impl.
|
||||
*
|
||||
* @param other The iterator to move from; left in a valid but
|
||||
* singular state.
|
||||
*/
|
||||
Iterator(Iterator&& other) noexcept;
|
||||
|
||||
// Used by the implementation
|
||||
/** Constructs an iterator from a raw view pointer and a polymorphic impl.
|
||||
*
|
||||
* Used exclusively by `ReadView`'s factory methods (`slesBegin()`,
|
||||
* `slesEnd()`, etc.). Not intended for direct use by callers.
|
||||
*
|
||||
* @param view The owning view; stored only for cross-view assertion.
|
||||
* @param impl The heap-allocated concrete iterator; ownership is
|
||||
* transferred to this object.
|
||||
*/
|
||||
explicit Iterator(ReadView const* view, std::unique_ptr<iter_base> impl);
|
||||
|
||||
/** Copy-assigns from another iterator at the same position.
|
||||
*
|
||||
* Deep-clones via `iter_base::copy()`.
|
||||
*
|
||||
* @param other The iterator to copy.
|
||||
* @return `*this`.
|
||||
*/
|
||||
Iterator&
|
||||
operator=(Iterator const& other);
|
||||
|
||||
/** Move-assigns from another iterator.
|
||||
*
|
||||
* @param other The iterator to move from; left in a valid but
|
||||
* singular state.
|
||||
* @return `*this`.
|
||||
*/
|
||||
Iterator&
|
||||
operator=(Iterator&& other) noexcept;
|
||||
|
||||
/** Returns `true` if both iterators denote the same position.
|
||||
*
|
||||
* Delegates to `iter_base::equal()`. Two null `impl_` pointers also
|
||||
* compare equal (both are end sentinels / default-constructed).
|
||||
*
|
||||
* @param other The iterator to compare against.
|
||||
* @return `true` when both iterators are at the same element.
|
||||
* @note Asserts in debug builds that both iterators belong to the
|
||||
* same view. Comparing iterators from different views is
|
||||
* undefined behaviour.
|
||||
*/
|
||||
bool
|
||||
operator==(Iterator const& other) const;
|
||||
|
||||
/** Returns `true` if the iterators denote different positions.
|
||||
*
|
||||
* @param other The iterator to compare against.
|
||||
* @return `true` when the iterators are not at the same element.
|
||||
*/
|
||||
bool
|
||||
operator!=(Iterator const& other) const;
|
||||
|
||||
/** Returns a reference to the current element.
|
||||
*
|
||||
* The result is cached after the first call; subsequent calls before
|
||||
* the next `operator++` return the cached value at no extra cost.
|
||||
*
|
||||
* @return A `const` reference to the current `ValueType`.
|
||||
* @throw May throw if the underlying `iter_base::dereference()` call fails.
|
||||
*/
|
||||
// Can throw
|
||||
reference
|
||||
operator*() const;
|
||||
|
||||
/** Returns a pointer to the current element.
|
||||
*
|
||||
* Delegates to `operator*()` so caching and exception behaviour are
|
||||
* identical to that of the dereference operator.
|
||||
*
|
||||
* @return A `const` pointer to the current `ValueType`.
|
||||
* @throw May throw if the underlying `iter_base::dereference()` call fails.
|
||||
*/
|
||||
// Can throw
|
||||
pointer
|
||||
operator->() const;
|
||||
|
||||
/** Advances the iterator and clears the dereference cache.
|
||||
*
|
||||
* @return `*this` after advancing to the next element.
|
||||
*/
|
||||
Iterator&
|
||||
operator++();
|
||||
|
||||
/** Returns a copy of the current iterator, then advances.
|
||||
*
|
||||
* @return An iterator to the element before the advance.
|
||||
*/
|
||||
Iterator
|
||||
operator++(int);
|
||||
|
||||
private:
|
||||
/** Owning view; compared in `operator==` to catch cross-view misuse. */
|
||||
ReadView const* view_ = nullptr;
|
||||
/** Heap-allocated polymorphic iterator; null for the end sentinel. */
|
||||
std::unique_ptr<iter_base> impl_{};
|
||||
/** One-slot dereference cache; cleared on each advance. */
|
||||
std::optional<value_type> mutable cache_;
|
||||
};
|
||||
|
||||
@@ -118,11 +270,19 @@ public:
|
||||
ReadViewFwdRange&
|
||||
operator=(ReadViewFwdRange const&) = default;
|
||||
|
||||
/** Constructs a range bound to @p view.
|
||||
*
|
||||
* The range stores a raw pointer to the view. The view must outlive
|
||||
* the range and any iterators derived from it.
|
||||
*
|
||||
* @param view The `ReadView` whose factory methods supply iterators.
|
||||
*/
|
||||
explicit ReadViewFwdRange(ReadView const& view) : view_(&view)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
/** The view whose factory methods supply concrete `iter_base` instances. */
|
||||
ReadView const* view_;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/** @file
|
||||
* Mathematical and operational backbone of the XRPL Automated Market Maker.
|
||||
*
|
||||
* Provides every computation needed to run a constant-product AMM pool:
|
||||
* LP token minting and burning (XLS-30d Equations 3, 4, 7, 8), spot-price
|
||||
* quality alignment against the central limit order book, swap execution with
|
||||
* rigorous directional rounding, and ledger-state helpers for pool balance
|
||||
* queries and AMM account lifecycle management.
|
||||
*
|
||||
* All arithmetic observes the pool invariant:
|
||||
* @code
|
||||
* sqrt(poolAsset1 × poolAsset2) >= LPTokenBalance
|
||||
* @endcode
|
||||
* Rounding is always directed to keep the pool at least as large as required.
|
||||
* The `fixAMMv1_1` amendment introduced per-step directional rounding for
|
||||
* swaps; `fixAMMv1_3` extended this discipline to LP token and
|
||||
* deposit/withdrawal formulas. Pre-amendment paths are preserved for
|
||||
* historic ledger replay.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Expected.h>
|
||||
@@ -22,6 +41,17 @@ namespace xrpl {
|
||||
|
||||
namespace detail {
|
||||
|
||||
/** Scale @p amount down by 99.99% as a last-resort quality rescue.
|
||||
*
|
||||
* When the rounded offer from `getAMMOfferStartWithTakerGets` or
|
||||
* `getAMMOfferStartWithTakerPays` still falls below the target quality due
|
||||
* to XRP integer-drop discretization, this function shrinks it by 0.01%
|
||||
* (rounding toward zero) so the resulting offer quality meets or exceeds
|
||||
* the target without generating an implausibly small trade.
|
||||
*
|
||||
* @param amount The offer side (takerGets or takerPays) to reduce.
|
||||
* @return The reduced amount, or zero if already at zero.
|
||||
*/
|
||||
Number
|
||||
reduceOffer(auto const& amount)
|
||||
{
|
||||
@@ -34,22 +64,41 @@ reduceOffer(auto const& amount)
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/** Direction tag used throughout deposit/withdrawal and rounding helpers.
|
||||
*
|
||||
* Passed to functions that behave asymmetrically between deposit (LP tokens
|
||||
* rounded down, assets rounded up) and withdrawal (LP tokens rounded up,
|
||||
* assets rounded down) to preserve the pool invariant.
|
||||
*/
|
||||
enum class IsDeposit : bool { No = false, Yes = true };
|
||||
|
||||
/** Calculate LP Tokens given AMM pool reserves.
|
||||
* @param asset1 AMM one side of the pool reserve
|
||||
* @param asset2 AMM another side of the pool reserve
|
||||
* @return LP Tokens as IOU
|
||||
/** Compute the initial LP token supply for a newly seeded AMM pool.
|
||||
*
|
||||
* Uses the geometric mean `sqrt(asset1 × asset2)`, which sets the
|
||||
* pool invariant to equality at creation: `sqrt(asset1 × asset2) == LPTokens`.
|
||||
* Under `fixAMMv1_3` the result is rounded downward so the pool starts
|
||||
* with a slight surplus, preserving the invariant.
|
||||
*
|
||||
* @param asset1 Balance of the first pool asset.
|
||||
* @param asset2 Balance of the second pool asset.
|
||||
* @param lptIssue Asset descriptor identifying the LP token currency/issuer.
|
||||
* @return Initial LP token amount as an IOU `STAmount`.
|
||||
*/
|
||||
STAmount
|
||||
ammLPTokens(STAmount const& asset1, STAmount const& asset2, Asset const& lptIssue);
|
||||
|
||||
/** Calculate LP Tokens given asset's deposit amount.
|
||||
* @param asset1Balance current AMM asset1 balance
|
||||
* @param asset1Deposit requested asset1 deposit amount
|
||||
* @param lptAMMBalance AMM LPT balance
|
||||
* @param tfee trading fee in basis points
|
||||
* @return tokens
|
||||
/** LP tokens minted for a single-asset deposit (XLS-30d Equation 3).
|
||||
*
|
||||
* A single-sided deposit is economically equivalent to a proportional
|
||||
* deposit plus a fee-bearing swap; the fee is embedded via `feeMult` and
|
||||
* `feeMultHalf`. Under `fixAMMv1_3` the final multiplication is rounded
|
||||
* downward so fewer tokens are issued, preserving the pool invariant.
|
||||
*
|
||||
* @param asset1Balance Current pool balance of the asset being deposited.
|
||||
* @param asset1Deposit Amount being deposited.
|
||||
* @param lptAMMBalance Current total LP token supply.
|
||||
* @param tfee Trading fee in basis points (e.g. 1000 = 1%).
|
||||
* @return LP tokens to mint for the depositor.
|
||||
*/
|
||||
STAmount
|
||||
lpTokensOut(
|
||||
@@ -58,12 +107,19 @@ lpTokensOut(
|
||||
STAmount const& lptAMMBalance,
|
||||
std::uint16_t tfee);
|
||||
|
||||
/** Calculate asset deposit given LP Tokens.
|
||||
* @param asset1Balance current AMM asset1 balance
|
||||
* @param lpTokens LP Tokens
|
||||
* @param lptAMMBalance AMM LPT balance
|
||||
* @param tfee trading fee in basis points
|
||||
* @return
|
||||
/** Asset deposit required to receive a given number of LP tokens (XLS-30d Equation 4).
|
||||
*
|
||||
* Inverse of `lpTokensOut`: solves Equation 3 for the deposit amount given a
|
||||
* desired token output. The solution is a quadratic whose positive root is
|
||||
* found via `solveQuadraticEq`. Under `fixAMMv1_3` the result is rounded
|
||||
* upward so the depositor contributes slightly more, preserving the pool
|
||||
* invariant.
|
||||
*
|
||||
* @param asset1Balance Current pool balance of the asset to deposit.
|
||||
* @param lptAMMBalance Current total LP token supply.
|
||||
* @param lpTokens Desired LP token amount.
|
||||
* @param tfee Trading fee in basis points.
|
||||
* @return Asset amount the depositor must contribute.
|
||||
*/
|
||||
STAmount
|
||||
ammAssetIn(
|
||||
@@ -72,13 +128,18 @@ ammAssetIn(
|
||||
STAmount const& lpTokens,
|
||||
std::uint16_t tfee);
|
||||
|
||||
/** Calculate LP Tokens given asset's withdraw amount. Return 0
|
||||
* if can't calculate.
|
||||
* @param asset1Balance current AMM asset1 balance
|
||||
* @param asset1Withdraw requested asset1 withdraw amount
|
||||
* @param lptAMMBalance AMM LPT balance
|
||||
* @param tfee trading fee in basis points
|
||||
* @return tokens out amount
|
||||
/** LP tokens to burn for a single-asset withdrawal (XLS-30d Equation 7).
|
||||
*
|
||||
* Computes how many LP tokens must be redeemed to withdraw a specified asset
|
||||
* amount. Returns zero if the inputs make calculation impossible. Under
|
||||
* `fixAMMv1_3` the final multiplication is rounded upward so more tokens must
|
||||
* be burned, preserving the pool invariant.
|
||||
*
|
||||
* @param asset1Balance Current pool balance of the asset being withdrawn.
|
||||
* @param asset1Withdraw Requested withdrawal amount.
|
||||
* @param lptAMMBalance Current total LP token supply.
|
||||
* @param tfee Trading fee in basis points.
|
||||
* @return LP tokens the withdrawer must burn, or zero if the calculation fails.
|
||||
*/
|
||||
STAmount
|
||||
lpTokensIn(
|
||||
@@ -87,12 +148,18 @@ lpTokensIn(
|
||||
STAmount const& lptAMMBalance,
|
||||
std::uint16_t tfee);
|
||||
|
||||
/** Calculate asset withdrawal by tokens
|
||||
* @param assetBalance balance of the asset being withdrawn
|
||||
* @param lptAMMBalance total AMM Tokens balance
|
||||
* @param lpTokens LP Tokens balance
|
||||
* @param tfee trading fee in basis points
|
||||
* @return calculated asset amount
|
||||
/** Asset returned when burning a given number of LP tokens (XLS-30d Equation 8).
|
||||
*
|
||||
* Inverse of `lpTokensIn`: solves Equation 7 for the withdrawal amount given
|
||||
* the token burn. Under `fixAMMv1_3` the final multiplication is rounded
|
||||
* downward so the withdrawer receives slightly less, preserving the pool
|
||||
* invariant.
|
||||
*
|
||||
* @param assetBalance Current pool balance of the asset to withdraw.
|
||||
* @param lptAMMBalance Current total LP token supply.
|
||||
* @param lpTokens LP tokens being burned.
|
||||
* @param tfee Trading fee in basis points.
|
||||
* @return Asset amount returned to the withdrawer.
|
||||
*/
|
||||
STAmount
|
||||
ammAssetOut(
|
||||
@@ -101,12 +168,19 @@ ammAssetOut(
|
||||
STAmount const& lpTokens,
|
||||
std::uint16_t tfee);
|
||||
|
||||
/** Check if the relative distance between the qualities
|
||||
* is within the requested distance.
|
||||
* @param calcQuality calculated quality
|
||||
* @param reqQuality requested quality
|
||||
* @param dist requested relative distance
|
||||
* @return true if within dist, false otherwise
|
||||
/** Check whether two `Quality` values are within a relative tolerance.
|
||||
*
|
||||
* `Quality` has no subtraction operator, so the comparison is performed via
|
||||
* `Quality::rate()`, which returns the *inverse* of quality (output/input).
|
||||
* The formula `(min.rate - max.rate) / min.rate < dist` is equivalent to
|
||||
* the standard `(max - min) / max < dist` after accounting for the inversion.
|
||||
* Used in `changeSpotPriceQuality` to suppress trace-level errors when the
|
||||
* quality mismatch is within one part in ten million (1e-7).
|
||||
*
|
||||
* @param calcQuality Computed quality.
|
||||
* @param reqQuality Target quality.
|
||||
* @param dist Maximum acceptable relative distance (e.g. `Number(1, -7)`).
|
||||
* @return `true` if the two qualities are within @p dist of each other.
|
||||
*/
|
||||
inline bool
|
||||
withinRelativeDistance(Quality const& calcQuality, Quality const& reqQuality, Number const& dist)
|
||||
@@ -120,12 +194,18 @@ withinRelativeDistance(Quality const& calcQuality, Quality const& reqQuality, Nu
|
||||
return ((min.rate() - max.rate()) / min.rate()) < dist;
|
||||
}
|
||||
|
||||
/** Check if the relative distance between the amounts
|
||||
* is within the requested distance.
|
||||
* @param calc calculated amount
|
||||
* @param req requested amount
|
||||
* @param dist requested relative distance
|
||||
* @return true if within dist, false otherwise
|
||||
/** Check whether two numeric amounts are within a relative tolerance.
|
||||
*
|
||||
* Computes `(max - min) / max` and tests that it is less than @p dist.
|
||||
* Accepted for `STAmount`, `IOUAmount`, `XRPAmount`, `MPTAmount`, and
|
||||
* `Number`. Used alongside the `Quality` overload to emit quality-mismatch
|
||||
* errors only when the discrepancy is truly significant.
|
||||
*
|
||||
* @tparam Amt Amount type; constrained to the five types listed above.
|
||||
* @param calc Computed amount.
|
||||
* @param req Target amount.
|
||||
* @param dist Maximum acceptable relative distance.
|
||||
* @return `true` if the two amounts are within @p dist of each other.
|
||||
*/
|
||||
template <typename Amt>
|
||||
requires(
|
||||
@@ -141,34 +221,49 @@ withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist)
|
||||
return ((max - min) / max) < dist;
|
||||
}
|
||||
|
||||
/** Solve quadratic equation to find takerGets or takerPays. Round
|
||||
* to minimize the amount in order to maximize the quality.
|
||||
/** Smallest positive root of `a·x² + b·x + c = 0`, used to minimize offer size.
|
||||
*
|
||||
* Uses the numerically stable "citardauq" formula (Blinn 2006): when `b > 0`
|
||||
* it computes `2c / (-b - sqrt(d))` instead of the standard
|
||||
* `(-b + sqrt(d)) / 2a`, avoiding catastrophic cancellation when the two
|
||||
* terms in the numerator are nearly equal. Minimizing the root maximizes
|
||||
* offer quality in `getAMMOfferStartWithTakerGets` / `getAMMOfferStartWithTakerPays`.
|
||||
*
|
||||
* @param a Quadratic coefficient.
|
||||
* @param b Linear coefficient.
|
||||
* @param c Constant term.
|
||||
* @return The smallest positive root, or `std::nullopt` if the discriminant
|
||||
* is negative (no real solution) or the root is non-positive.
|
||||
*/
|
||||
std::optional<Number>
|
||||
solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c);
|
||||
|
||||
/** Generate AMM offer starting with takerGets when AMM pool
|
||||
* from the payment perspective is IOU(in)/XRP(out)
|
||||
* Equations:
|
||||
* Spot Price Quality after the offer is consumed:
|
||||
* Qsp = (O - o) / (I + i) -- equation (1)
|
||||
* where O is poolPays, I is poolGets, o is takerGets, i is takerPays
|
||||
* Swap out:
|
||||
* i = (I * o) / (O - o) * f -- equation (2)
|
||||
* where f is (1 - tfee/100000), tfee is in basis points
|
||||
* Effective price targetQuality:
|
||||
* Qep = o / i -- equation (3)
|
||||
* There are two scenarios to consider
|
||||
* A) Qsp = Qep. Substitute i in (1) with (2) and solve for o
|
||||
* and Qsp = targetQuality(Qt):
|
||||
* o**2 + o * (I * Qt * (1 - 1 / f) - 2 * O) + O**2 - Qt * I * O = 0
|
||||
* B) Qep = Qsp. Substitute i in (3) with (2) and solve for o
|
||||
* and Qep = targetQuality(Qt):
|
||||
* o = O - I * Qt / f
|
||||
* Since the scenario is not known a priori, both A and B are solved and
|
||||
* the lowest value of o is takerGets. takerPays is calculated with
|
||||
* swap out eq (2). If o is less or equal to 0 then the offer can't
|
||||
* be generated.
|
||||
/** Generate a synthetic AMM offer whose quality matches @p targetQuality,
|
||||
* starting from takerGets (XRP out, IOU in).
|
||||
*
|
||||
* Used when the pool pays XRP (IOU-in / XRP-out). Starting from the XRP
|
||||
* side ensures that rounding XRP down to integer drops improves rather than
|
||||
* degrades offer quality (post-`fixAMMv1_1` behavior).
|
||||
*
|
||||
* Two binding constraints are solved and the smaller takerGets is chosen:
|
||||
* - Scenario A — post-swap spot price equals @p targetQuality:
|
||||
* `o² + o·(I·Qt·(1 - 1/f) - 2·O) + O² - Qt·I·O = 0`
|
||||
* - Scenario B — effective offer price equals @p targetQuality:
|
||||
* `o = O - I·Qt / f`
|
||||
*
|
||||
* where `O = poolPays`, `I = poolGets`, `f = feeMult(tfee)`.
|
||||
* takerPays is then derived from the swap-out equation. If the resulting
|
||||
* offer quality is still below @p targetQuality after rounding, a 99.99%
|
||||
* rescale via `detail::reduceOffer` is attempted.
|
||||
*
|
||||
* @tparam TIn Asset type flowing into the pool (IOU side).
|
||||
* @tparam TOut Asset type flowing out of the pool (XRP side).
|
||||
* @param pool Current AMM pool balances (`in` = poolGets, `out` = poolPays).
|
||||
* @param targetQuality Desired offer quality (CLOB best quality).
|
||||
* @param tfee Trading fee in basis points.
|
||||
* @return Seated `{takerPays, takerGets}` amounts, or `std::nullopt` if a
|
||||
* valid offer cannot be generated (e.g. target quality unreachable at
|
||||
* current fee).
|
||||
*/
|
||||
template <typename TIn, typename TOut>
|
||||
std::optional<TAmounts<TIn, TOut>>
|
||||
@@ -214,28 +309,30 @@ getAMMOfferStartWithTakerGets(
|
||||
return amounts;
|
||||
}
|
||||
|
||||
/** Generate AMM offer starting with takerPays when AMM pool
|
||||
* from the payment perspective is XRP(in)/IOU(out) or IOU(in)/IOU(out).
|
||||
* Equations:
|
||||
* Spot Price Quality after the offer is consumed:
|
||||
* Qsp = (O - o) / (I + i) -- equation (1)
|
||||
* where O is poolPays, I is poolGets, o is takerGets, i is takerPays
|
||||
* Swap in:
|
||||
* o = (O * i * f) / (I + i * f) -- equation (2)
|
||||
* where f is (1 - tfee/100000), tfee is in basis points
|
||||
* Effective price quality:
|
||||
* Qep = o / i -- equation (3)
|
||||
* There are two scenarios to consider
|
||||
* A) Qsp = Qep. Substitute o in (1) with (2) and solve for i
|
||||
* and Qsp = targetQuality(Qt):
|
||||
* i**2 * f + i * I * (1 + f) + I**2 - I * O / Qt = 0
|
||||
* B) Qep = Qsp. Substitute i in (3) with (2) and solve for i
|
||||
* and Qep = targetQuality(Qt):
|
||||
* i = O / Qt - I / f
|
||||
* Since the scenario is not known a priori, both A and B are solved and
|
||||
* the lowest value of i is takerPays. takerGets is calculated with
|
||||
* swap in eq (2). If i is less or equal to 0 then the offer can't
|
||||
* be generated.
|
||||
/** Generate a synthetic AMM offer whose quality matches @p targetQuality,
|
||||
* starting from takerPays (XRP in, or IOU/IOU).
|
||||
*
|
||||
* Used for XRP-in/IOU-out and IOU/IOU pools. Starting from the XRP
|
||||
* side (takerPays) under `fixAMMv1_1` keeps rounding effects favorable.
|
||||
*
|
||||
* Two binding constraints are solved and the smaller takerPays is chosen:
|
||||
* - Scenario A — post-swap spot price equals @p targetQuality:
|
||||
* `i²·f + i·I·(1+f) + I² - I·O/Qt = 0`
|
||||
* - Scenario B — effective offer price equals @p targetQuality:
|
||||
* `i = O/Qt - I/f`
|
||||
*
|
||||
* where `O = poolPays`, `I = poolGets`, `f = feeMult(tfee)`.
|
||||
* takerGets is then derived from the swap-in equation. If the resulting
|
||||
* offer quality is still below @p targetQuality after rounding, a 99.99%
|
||||
* rescale via `detail::reduceOffer` is attempted.
|
||||
*
|
||||
* @tparam TIn Asset type flowing into the pool.
|
||||
* @tparam TOut Asset type flowing out of the pool.
|
||||
* @param pool Current AMM pool balances (`in` = poolGets, `out` = poolPays).
|
||||
* @param targetQuality Desired offer quality (CLOB best quality).
|
||||
* @param tfee Trading fee in basis points.
|
||||
* @return Seated `{takerPays, takerGets}` amounts, or `std::nullopt` if a
|
||||
* valid offer cannot be generated.
|
||||
*/
|
||||
template <typename TIn, typename TOut>
|
||||
std::optional<TAmounts<TIn, TOut>>
|
||||
@@ -281,21 +378,34 @@ getAMMOfferStartWithTakerPays(
|
||||
return amounts;
|
||||
}
|
||||
|
||||
/** Generate AMM offer so that either updated Spot Price Quality (SPQ)
|
||||
* is equal to LOB quality (in this case AMM offer quality is
|
||||
* better than LOB quality) or AMM offer is equal to LOB quality
|
||||
* (in this case SPQ is better than LOB quality).
|
||||
* Pre-amendment code calculates takerPays first. If takerGets is XRP,
|
||||
* it is rounded down, which results in worse offer quality than
|
||||
* LOB quality, and the offer might fail to generate.
|
||||
* Post-amendment code calculates the XRP offer side first. The result
|
||||
* is rounded down, which makes the offer quality better.
|
||||
* It might not be possible to match either SPQ or AMM offer to LOB
|
||||
* quality. This generally happens at higher fees.
|
||||
* @param pool AMM pool balances
|
||||
* @param quality requested quality
|
||||
* @param tfee trading fee in basis points
|
||||
* @return seated in/out amounts if the quality can be changed
|
||||
/** Generate a synthetic AMM offer that aligns the pool's spot price with a CLOB quality.
|
||||
*
|
||||
* The payment engine calls this when it encounters both AMM pools and order
|
||||
* book offers for the same currency pair. The resulting offer has a quality
|
||||
* such that either the post-swap spot price equals @p quality (AMM offer
|
||||
* quality is better) or the offer's effective price equals @p quality (the
|
||||
* post-swap spot price is better) — whichever produces the smaller offer.
|
||||
*
|
||||
* Amendment behavior:
|
||||
* - Pre-`fixAMMv1_1`: always solves for takerPays first; rounding down XRP
|
||||
* takerGets can push quality below target, causing the offer to be rejected.
|
||||
* - Post-`fixAMMv1_1`: solves for the XRP side first (takerGets when pool pays
|
||||
* XRP, takerPays otherwise) so XRP rounding improves rather than degrades
|
||||
* quality. Falls back to `detail::reduceOffer` if quality is still below
|
||||
* target after rounding.
|
||||
*
|
||||
* A quality mismatch larger than 1e-7 is logged at `j.error()` level; smaller
|
||||
* mismatches are trace-only.
|
||||
*
|
||||
* @tparam TIn Asset type flowing into the pool.
|
||||
* @tparam TOut Asset type flowing out of the pool.
|
||||
* @param pool Current AMM pool balances.
|
||||
* @param quality Target quality (best CLOB offer quality for this pair).
|
||||
* @param tfee Trading fee in basis points.
|
||||
* @param rules Current ledger rules (for amendment checks).
|
||||
* @param j Journal for diagnostic logging.
|
||||
* @return Seated `{takerPays, takerGets}` amounts, or `std::nullopt` if the
|
||||
* quality cannot be achieved (generally at high fees).
|
||||
*/
|
||||
template <typename TIn, typename TOut>
|
||||
std::optional<TAmounts<TIn, TOut>>
|
||||
@@ -398,26 +508,26 @@ changeSpotPriceQuality(
|
||||
return amounts;
|
||||
}
|
||||
|
||||
/** AMM pool invariant - the product (A * B) after swap in/out has to remain
|
||||
* at least the same: (A + in) * (B - out) >= A * B
|
||||
* XRP round-off may result in a smaller product after swap in/out.
|
||||
* To address this:
|
||||
* - if on swapIn the out is XRP then the amount is round-off
|
||||
* downward, making the product slightly larger since out
|
||||
* value is reduced.
|
||||
* - if on swapOut the in is XRP then the amount is round-off
|
||||
* upward, making the product slightly larger since in
|
||||
* value is increased.
|
||||
*/
|
||||
// --- Swap-in / Swap-out ---
|
||||
|
||||
/** Swap assetIn into the pool and swap out a proportional amount
|
||||
* of the other asset. Implements AMM Swap in.
|
||||
* @see [XLS30d:AMM
|
||||
* Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78)
|
||||
* @param pool current AMM pool balances
|
||||
* @param assetIn amount to swap in
|
||||
* @param tfee trading fee in basis points
|
||||
* @return
|
||||
/** Deposit @p assetIn into the pool and receive a proportional amount of the
|
||||
* other asset (AMM Swap in, XLS-30d).
|
||||
*
|
||||
* Formula: `out = pool.out - (pool.in × pool.out) / (pool.in + assetIn × feeMult(tfee))`
|
||||
*
|
||||
* Pool invariant: `(pool.in + assetIn) × (pool.out - out) >= pool.in × pool.out`.
|
||||
* XRP integer rounding can violate this; post-`fixAMMv1_1` each sub-expression
|
||||
* has an explicitly directed rounding mode so the pool retains a tiny surplus.
|
||||
* The output is always rounded downward so the trader receives less, not more.
|
||||
*
|
||||
* @tparam TIn Asset type deposited (poolGets side).
|
||||
* @tparam TOut Asset type received (poolPays side).
|
||||
* @param pool Current AMM pool balances.
|
||||
* @param assetIn Amount being deposited into the pool.
|
||||
* @param tfee Trading fee in basis points.
|
||||
* @return Amount of the output asset the trader receives; zero if the pool
|
||||
* denominator is non-positive.
|
||||
* @see [XLS-30d AMM Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78)
|
||||
*/
|
||||
template <typename TIn, typename TOut>
|
||||
TOut
|
||||
@@ -476,14 +586,23 @@ swapAssetIn(TAmounts<TIn, TOut> const& pool, TIn const& assetIn, std::uint16_t t
|
||||
Number::RoundingMode::Downward);
|
||||
}
|
||||
|
||||
/** Swap assetOut out of the pool and swap in a proportional amount
|
||||
* of the other asset. Implements AMM Swap out.
|
||||
* @see [XLS30d:AMM
|
||||
* Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78)
|
||||
* @param pool current AMM pool balances
|
||||
* @param assetOut amount to swap out
|
||||
* @param tfee trading fee in basis points
|
||||
* @return
|
||||
/** Withdraw @p assetOut from the pool and compute the required input asset (AMM Swap out, XLS-30d).
|
||||
*
|
||||
* Formula: `in = ((pool.in × pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee)`
|
||||
*
|
||||
* The input is always rounded upward so the trader pays at least what the
|
||||
* pool needs to maintain its invariant. Post-`fixAMMv1_1` each intermediate
|
||||
* step is individually directed; if the pool denominator is non-positive (i.e.
|
||||
* @p assetOut >= the entire pool), the maximum representable `TIn` is returned.
|
||||
*
|
||||
* @tparam TIn Asset type deposited (poolGets side).
|
||||
* @tparam TOut Asset type withdrawn (poolPays side).
|
||||
* @param pool Current AMM pool balances.
|
||||
* @param assetOut Amount being withdrawn from the pool.
|
||||
* @param tfee Trading fee in basis points.
|
||||
* @return Amount of the input asset the trader must pay; `toMaxAmount<TIn>`
|
||||
* if the requested output would exhaust the pool.
|
||||
* @see [XLS-30d AMM Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78)
|
||||
*/
|
||||
template <typename TIn, typename TOut>
|
||||
TIn
|
||||
@@ -542,35 +661,46 @@ swapAssetOut(TAmounts<TIn, TOut> const& pool, TOut const& assetOut, std::uint16_
|
||||
Number::RoundingMode::Upward);
|
||||
}
|
||||
|
||||
/** Return square of n.
|
||||
*/
|
||||
/** Return `n²`. */
|
||||
Number
|
||||
square(Number const& n);
|
||||
|
||||
/** Adjust LP tokens to deposit/withdraw.
|
||||
* Amount type keeps 16 digits. Maintaining the LP balance by adding
|
||||
* deposited tokens or subtracting withdrawn LP tokens from LP balance
|
||||
* results in losing precision in LP balance. I.e. the resulting LP balance
|
||||
* is less than the actual sum of LP tokens. To adjust for this, subtract
|
||||
* old tokens balance from the new one for deposit or vice versa for
|
||||
* withdraw to cancel out the precision loss.
|
||||
* @param lptAMMBalance LPT AMM Balance
|
||||
* @param lpTokens LP tokens to deposit or withdraw
|
||||
* @param isDeposit Yes if deposit, No if withdraw
|
||||
/** Adjust LP tokens to account for 16-digit precision loss in the running balance.
|
||||
*
|
||||
* Adding newly-minted tokens to an already-large `lptAMMBalance` can lose
|
||||
* significance in the least-significant digit: the stored balance advances
|
||||
* by less than `lpTokens`. This function round-trips through the 16-digit
|
||||
* representation by computing `(balance + tokens) - balance` (deposit) or
|
||||
* `(tokens - balance) + balance` (withdraw), returning the value that will
|
||||
* actually be committed to the ledger. Result is forced downward to ensure
|
||||
* the adjusted tokens do not exceed the requested tokens.
|
||||
*
|
||||
* @param lptAMMBalance Current total LP token supply stored on the AMM SLE.
|
||||
* @param lpTokens Tokens being minted or burned.
|
||||
* @param isDeposit `IsDeposit::Yes` for deposit, `IsDeposit::No` for withdrawal.
|
||||
* @return Adjusted token amount that exactly matches the representable delta
|
||||
* in the 16-digit balance.
|
||||
*/
|
||||
STAmount
|
||||
adjustLPTokens(STAmount const& lptAMMBalance, STAmount const& lpTokens, IsDeposit isDeposit);
|
||||
|
||||
/** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if
|
||||
* the adjusted LP tokens are less than the provided LP tokens.
|
||||
* @param amountBalance asset1 pool balance
|
||||
* @param amount asset1 to deposit or withdraw
|
||||
* @param amount2 asset2 to deposit or withdraw
|
||||
* @param lptAMMBalance LPT AMM Balance
|
||||
* @param lpTokens LP tokens to deposit or withdraw
|
||||
* @param tfee trading fee in basis points
|
||||
* @param isDeposit Yes if deposit, No if withdraw
|
||||
* @return
|
||||
/** Adjust deposit/withdrawal asset amounts to match the precision-corrected LP token count.
|
||||
*
|
||||
* Calls `adjustLPTokens()` to compute the representable token delta. If the
|
||||
* adjusted count is less than @p lpTokens, the corresponding asset amounts are
|
||||
* scaled down so the ledger does not grant assets that exceed what the LP token
|
||||
* math supports. A no-op when `fixAMMv1_3` is active because `getRoundedLPTokens`
|
||||
* already incorporates the precision adjustment.
|
||||
*
|
||||
* @param amountBalance Current pool balance of the primary asset.
|
||||
* @param amount Primary asset amount to deposit or withdraw.
|
||||
* @param amount2 Secondary asset amount for two-sided operations; `std::nullopt`
|
||||
* for single-asset operations.
|
||||
* @param lptAMMBalance Current total LP token supply.
|
||||
* @param lpTokens Calculated LP tokens before precision adjustment.
|
||||
* @param tfee Trading fee in basis points.
|
||||
* @param isDeposit `IsDeposit::Yes` for deposit, `IsDeposit::No` for withdrawal.
|
||||
* @return Tuple of `(adjustedAmount, adjustedAmount2, adjustedLPTokens)`.
|
||||
*/
|
||||
std::tuple<STAmount, std::optional<STAmount>, STAmount>
|
||||
adjustAmountsByLPTokens(
|
||||
@@ -582,17 +712,46 @@ adjustAmountsByLPTokens(
|
||||
std::uint16_t tfee,
|
||||
IsDeposit isDeposit);
|
||||
|
||||
/** Positive solution for quadratic equation:
|
||||
* x = (-b + sqrt(b**2 + 4*a*c))/(2*a)
|
||||
/** Positive root of `a·x² + b·x + c = 0` using the standard formula.
|
||||
*
|
||||
* Computes `x = (-b + sqrt(b² - 4·a·c)) / (2·a)`. Used by `ammAssetIn`
|
||||
* to invert Equation 4; the discriminant is guaranteed non-negative by the
|
||||
* deposit formula's domain.
|
||||
*
|
||||
* @param a Quadratic coefficient.
|
||||
* @param b Linear coefficient.
|
||||
* @param c Constant term.
|
||||
* @return The positive root.
|
||||
*/
|
||||
Number
|
||||
solveQuadraticEq(Number const& a, Number const& b, Number const& c);
|
||||
|
||||
/** Multiply @p amount by @p frac with an explicitly directed rounding mode.
|
||||
*
|
||||
* Installs @p rm for both the `Number` multiplication and the subsequent
|
||||
* `toSTAmount` conversion so that rounding is applied once at the final step,
|
||||
* not accumulated through intermediates. This is the building block for all
|
||||
* `fixAMMv1_3` directional-rounding paths.
|
||||
*
|
||||
* @param amount Base `STAmount` to scale.
|
||||
* @param frac Scaling factor.
|
||||
* @param rm Rounding mode to apply at the final conversion step.
|
||||
* @return `amount × frac` rounded according to @p rm, expressed in the same
|
||||
* asset as @p amount.
|
||||
*/
|
||||
STAmount
|
||||
multiply(STAmount const& amount, Number const& frac, Number::RoundingMode rm);
|
||||
|
||||
namespace detail {
|
||||
|
||||
/** Select the LP token rounding direction that preserves the pool invariant.
|
||||
*
|
||||
* Deposit: round downward (fewer tokens minted → pool worth more per token).
|
||||
* Withdraw: round upward (more tokens burned → pool retains slightly more).
|
||||
*
|
||||
* @param isDeposit Direction of the operation.
|
||||
* @return `Downward` for deposit, `Upward` for withdrawal.
|
||||
*/
|
||||
inline Number::RoundingMode
|
||||
getLPTokenRounding(IsDeposit isDeposit)
|
||||
{
|
||||
@@ -602,6 +761,14 @@ getLPTokenRounding(IsDeposit isDeposit)
|
||||
: Number::RoundingMode::Upward;
|
||||
}
|
||||
|
||||
/** Select the asset rounding direction that preserves the pool invariant.
|
||||
*
|
||||
* Deposit: round upward (depositor pays slightly more → pool is larger).
|
||||
* Withdraw: round downward (withdrawer receives slightly less → pool retains).
|
||||
*
|
||||
* @param isDeposit Direction of the operation.
|
||||
* @return `Upward` for deposit, `Downward` for withdrawal.
|
||||
*/
|
||||
inline Number::RoundingMode
|
||||
getAssetRounding(IsDeposit isDeposit)
|
||||
{
|
||||
@@ -613,10 +780,19 @@ getAssetRounding(IsDeposit isDeposit)
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/** Round AMM equal deposit/withdrawal amount. Deposit/withdrawal formulas
|
||||
* calculate the amount as a fractional value of the pool balance. The rounding
|
||||
* takes place on the last step of multiplying the balance by the fraction if
|
||||
* AMMv1_3 is enabled.
|
||||
/** Compute a proportional asset amount with amendment-gated directional rounding.
|
||||
*
|
||||
* Used for two-sided (equal) deposit/withdrawal where the asset amount is
|
||||
* `balance × frac`. Under `fixAMMv1_3` the final multiplication is rounded
|
||||
* via `detail::getAssetRounding` (upward on deposit, downward on withdraw).
|
||||
* Without the amendment the result uses the current ambient rounding mode.
|
||||
*
|
||||
* @tparam A Type of @p frac; either `STAmount` or `Number`.
|
||||
* @param rules Current ledger rules.
|
||||
* @param balance Pool balance of the asset.
|
||||
* @param frac Fraction of the pool balance to apply.
|
||||
* @param isDeposit Direction; controls rounding when `fixAMMv1_3` is active.
|
||||
* @return `balance × frac` rounded to preserve the pool invariant.
|
||||
*/
|
||||
template <typename A>
|
||||
STAmount
|
||||
@@ -637,14 +813,20 @@ getRoundedAsset(Rules const& rules, STAmount const& balance, A const& frac, IsDe
|
||||
return multiply(balance, frac, rm);
|
||||
}
|
||||
|
||||
/** Round AMM single deposit/withdrawal amount.
|
||||
* The lambda's are used to delay evaluation until the function
|
||||
* is executed so that the calculation is not done twice. noRoundCb() is
|
||||
* called if AMMv1_3 is disabled. Otherwise, the rounding is set and
|
||||
* the amount is:
|
||||
* isDeposit is Yes - the balance multiplied by productCb()
|
||||
* isDeposit is No - the result of productCb(). The rounding is
|
||||
* the same for all calculations in productCb()
|
||||
/** Compute a single-asset deposit/withdrawal amount with amendment-gated rounding.
|
||||
*
|
||||
* The callback form defers evaluation to avoid computing the formula twice:
|
||||
* - Without `fixAMMv1_3`: calls `noRoundCb()` and converts without directed rounding.
|
||||
* - With `fixAMMv1_3`, deposit: calls `multiply(balance, productCb(), rm)`.
|
||||
* - With `fixAMMv1_3`, withdrawal: installs @p rm globally and calls `productCb()`
|
||||
* so every arithmetic step inside the callback shares the same rounding direction.
|
||||
*
|
||||
* @param rules Current ledger rules.
|
||||
* @param noRoundCb Produces the unrounded result (pre-amendment path).
|
||||
* @param balance Pool balance of the asset.
|
||||
* @param productCb Produces the rounding fraction (post-amendment path).
|
||||
* @param isDeposit Direction; controls which rounding mode is selected.
|
||||
* @return Rounded asset amount preserving the pool invariant.
|
||||
*/
|
||||
STAmount
|
||||
getRoundedAsset(
|
||||
@@ -654,12 +836,18 @@ getRoundedAsset(
|
||||
std::function<Number()> const& productCb,
|
||||
IsDeposit isDeposit);
|
||||
|
||||
/** Round AMM deposit/withdrawal LPToken amount. Deposit/withdrawal formulas
|
||||
* calculate the lptokens as a fractional value of the AMM total lptokens.
|
||||
* The rounding takes place on the last step of multiplying the balance by
|
||||
* the fraction if AMMv1_3 is enabled. The tokens are then
|
||||
* adjusted to factor in the loss in precision (we only keep 16 significant
|
||||
* digits) when adding the lptokens to the balance.
|
||||
/** Compute a proportional LP token amount with amendment-gated rounding and precision adjustment.
|
||||
*
|
||||
* Used for two-sided (equal) deposit/withdrawal. Under `fixAMMv1_3` the
|
||||
* multiplication `balance × frac` is rounded via `detail::getLPTokenRounding`,
|
||||
* then `adjustLPTokens` corrects for the 16-digit precision loss introduced
|
||||
* when adding the result to the running LP token balance.
|
||||
*
|
||||
* @param rules Current ledger rules.
|
||||
* @param balance Current total LP token supply.
|
||||
* @param frac Fraction of the pool's LP supply to mint or burn.
|
||||
* @param isDeposit Direction; controls rounding and sign of the adjustment.
|
||||
* @return LP token amount after rounding and precision correction.
|
||||
*/
|
||||
STAmount
|
||||
getRoundedLPTokens(
|
||||
@@ -668,16 +856,22 @@ getRoundedLPTokens(
|
||||
Number const& frac,
|
||||
IsDeposit isDeposit);
|
||||
|
||||
/** Round AMM single deposit/withdrawal LPToken amount.
|
||||
* The lambda's are used to delay evaluation until the function is executed
|
||||
* so that the calculations are not done twice.
|
||||
* noRoundCb() is called if AMMv1_3 is disabled. Otherwise, the rounding is set
|
||||
* and the lptokens are:
|
||||
* if isDeposit is Yes - the result of productCb(). The rounding is
|
||||
* the same for all calculations in productCb()
|
||||
* if isDeposit is No - the balance multiplied by productCb()
|
||||
* The lptokens are then adjusted to factor in the loss in precision
|
||||
* (we only keep 16 significant digits) when adding the lptokens to the balance.
|
||||
/** Compute a single-asset LP token amount with amendment-gated rounding and precision adjustment.
|
||||
*
|
||||
* The callback form avoids evaluating the formula twice:
|
||||
* - Without `fixAMMv1_3`: calls `noRoundCb()` with no directed rounding.
|
||||
* - With `fixAMMv1_3`, deposit: installs the LP rounding mode globally and
|
||||
* calls `productCb()` (all arithmetic inside shares the direction).
|
||||
* - With `fixAMMv1_3`, withdrawal: calls `multiply(lptAMMBalance, productCb(), rm)`.
|
||||
* In all post-amendment cases, `adjustLPTokens` then corrects for 16-digit
|
||||
* precision loss in the running LP balance.
|
||||
*
|
||||
* @param rules Current ledger rules.
|
||||
* @param noRoundCb Produces the unrounded result (pre-amendment path).
|
||||
* @param lptAMMBalance Current total LP token supply.
|
||||
* @param productCb Produces the rounding fraction (post-amendment path).
|
||||
* @param isDeposit Direction; controls rounding mode selection.
|
||||
* @return LP token amount after rounding and precision correction.
|
||||
*/
|
||||
STAmount
|
||||
getRoundedLPTokens(
|
||||
@@ -687,16 +881,21 @@ getRoundedLPTokens(
|
||||
std::function<Number()> const& productCb,
|
||||
IsDeposit isDeposit);
|
||||
|
||||
/* Next two functions adjust asset in/out amount to factor in the adjusted
|
||||
* lptokens. The lptokens are calculated from the asset in/out. The lptokens are
|
||||
* then adjusted to factor in the loss in precision. The adjusted lptokens might
|
||||
* be less than the initially calculated tokens. Therefore, the asset in/out
|
||||
* must be adjusted. The rounding might result in the adjusted amount being
|
||||
* greater than the original asset in/out amount. If this happens,
|
||||
* then the original amount is reduced by the difference in the adjusted amount
|
||||
* and the original amount. The actual tokens and the actual adjusted amount
|
||||
* are then recalculated. The minimum of the original and the actual
|
||||
* adjusted amount is returned.
|
||||
/** Adjust a single-asset deposit amount to match the precision-corrected LP token count.
|
||||
*
|
||||
* Under `fixAMMv1_3`: computes `ammAssetIn(balance, lptAMMBalance, tokens, tfee)`.
|
||||
* If rounding causes the derived asset amount to exceed @p amount, the deposit is
|
||||
* reduced by the overshoot and both tokens and asset are recomputed, then the minimum
|
||||
* of original and adjusted amounts is returned. Before the amendment, returns the
|
||||
* inputs unchanged.
|
||||
*
|
||||
* @param rules Current ledger rules.
|
||||
* @param balance Pool balance of the asset being deposited.
|
||||
* @param amount Requested deposit amount.
|
||||
* @param lptAMMBalance Current total LP token supply.
|
||||
* @param tokens LP token count before precision adjustment.
|
||||
* @param tfee Trading fee in basis points.
|
||||
* @return `{adjustedTokens, adjustedAmount}` pair.
|
||||
*/
|
||||
std::pair<STAmount, STAmount>
|
||||
adjustAssetInByTokens(
|
||||
@@ -706,6 +905,23 @@ adjustAssetInByTokens(
|
||||
STAmount const& lptAMMBalance,
|
||||
STAmount const& tokens,
|
||||
std::uint16_t tfee);
|
||||
|
||||
/** Adjust a single-asset withdrawal amount to match the precision-corrected LP token count.
|
||||
*
|
||||
* Under `fixAMMv1_3`: computes `ammAssetOut(balance, lptAMMBalance, tokens, tfee)`.
|
||||
* If rounding causes the derived asset amount to exceed @p amount, the withdrawal is
|
||||
* reduced by the overshoot and both tokens and asset are recomputed, then the minimum
|
||||
* of original and adjusted amounts is returned. Before the amendment, returns the
|
||||
* inputs unchanged.
|
||||
*
|
||||
* @param rules Current ledger rules.
|
||||
* @param balance Pool balance of the asset being withdrawn.
|
||||
* @param amount Requested withdrawal amount.
|
||||
* @param lptAMMBalance Current total LP token supply.
|
||||
* @param tokens LP token count before precision adjustment.
|
||||
* @param tfee Trading fee in basis points.
|
||||
* @return `{adjustedTokens, adjustedAmount}` pair.
|
||||
*/
|
||||
std::pair<STAmount, STAmount>
|
||||
adjustAssetOutByTokens(
|
||||
Rules const& rules,
|
||||
@@ -715,8 +931,20 @@ adjustAssetOutByTokens(
|
||||
STAmount const& tokens,
|
||||
std::uint16_t tfee);
|
||||
|
||||
/** Find a fraction of tokens after the tokens are adjusted. The fraction
|
||||
* is used to adjust equal deposit/withdraw amount.
|
||||
/** Recompute the LP token fraction after precision adjustment.
|
||||
*
|
||||
* Under `fixAMMv1_3` the precision-adjusted token count may differ from the
|
||||
* originally requested count, so the fraction `tokens / lptAMMBalance` must
|
||||
* be recomputed from the adjusted value before it is used to scale equal
|
||||
* deposit/withdrawal amounts. Returns @p frac unchanged when `fixAMMv1_3`
|
||||
* is inactive (the precision adjustment has not yet been applied).
|
||||
*
|
||||
* @param rules Current ledger rules.
|
||||
* @param lptAMMBalance Current total LP token supply.
|
||||
* @param tokens Precision-adjusted LP token count.
|
||||
* @param frac Original fraction before adjustment.
|
||||
* @return Adjusted fraction `tokens / lptAMMBalance`, or @p frac if
|
||||
* `fixAMMv1_3` is not active.
|
||||
*/
|
||||
Number
|
||||
adjustFracByTokens(
|
||||
@@ -725,7 +953,19 @@ adjustFracByTokens(
|
||||
STAmount const& tokens,
|
||||
Number const& frac);
|
||||
|
||||
/** Get AMM pool balances.
|
||||
/** Read the AMM's current pool asset balances from the ledger.
|
||||
*
|
||||
* Delegates to `accountHolds` for each asset, respecting freeze and
|
||||
* authorization policy. Does not read the LP token balance.
|
||||
*
|
||||
* @param view Ledger state to query.
|
||||
* @param ammAccountID AccountID of the AMM's pseudo-account.
|
||||
* @param asset1 First pool asset.
|
||||
* @param asset2 Second pool asset.
|
||||
* @param freezeHandling Whether to enforce freeze restrictions.
|
||||
* @param authHandling Whether to enforce authorization restrictions.
|
||||
* @param j Journal for diagnostic logging.
|
||||
* @return `{balance1, balance2}` pair in the same asset order as the inputs.
|
||||
*/
|
||||
std::pair<STAmount, STAmount>
|
||||
ammPoolHolds(
|
||||
@@ -737,9 +977,23 @@ ammPoolHolds(
|
||||
AuthHandling authHandling,
|
||||
beast::Journal const j);
|
||||
|
||||
/** Get AMM pool and LP token balances. If both optIssue are
|
||||
* provided then they are used as the AMM token pair issues.
|
||||
* Otherwise the missing issues are fetched from ammSle.
|
||||
/** Read the AMM's pool balances and total LP token supply from the ledger.
|
||||
*
|
||||
* When both optional assets are provided they are validated against the AMM
|
||||
* SLE's stored pair and used as the query order; providing only one resolves
|
||||
* the counterpart from `ammSle`. If neither is provided, the canonical order
|
||||
* from `ammSle` is used. An invalid asset pair (mismatched with the AMM SLE)
|
||||
* indicates a corrupted AMM object and returns `tecAMM_INVALID_TOKENS`.
|
||||
*
|
||||
* @param view Ledger state to query.
|
||||
* @param ammSle The AMM's `ltAMM` SLE.
|
||||
* @param optAsset1 Optional first asset override.
|
||||
* @param optAsset2 Optional second asset override.
|
||||
* @param freezeHandling Whether to enforce freeze restrictions.
|
||||
* @param authHandling Whether to enforce authorization restrictions.
|
||||
* @param j Journal for diagnostic logging.
|
||||
* @return `{balance1, balance2, lpTokenBalance}` on success, or
|
||||
* `Unexpected(tecAMM_INVALID_TOKENS)` if the asset pair is invalid.
|
||||
*/
|
||||
Expected<std::tuple<STAmount, STAmount, STAmount>, TER>
|
||||
ammHolds(
|
||||
@@ -751,7 +1005,21 @@ ammHolds(
|
||||
AuthHandling authHandling,
|
||||
beast::Journal const j);
|
||||
|
||||
/** Get the balance of LP tokens.
|
||||
/** Read an LP's token balance from its direct trustline with the AMM account.
|
||||
*
|
||||
* Intentionally bypasses `accountHolds` — that function would also check
|
||||
* whether the AMM's underlying pool assets are frozen (under
|
||||
* `fixFrozenLPTokenTransfer`), which is incorrect policy for LP token balance
|
||||
* queries. Only the LP token trustline's own freeze flag is checked.
|
||||
* Trust-line orientation: raw `sfBalance` is negated when `lpAccount > ammAccount`.
|
||||
*
|
||||
* @param view Ledger state to query.
|
||||
* @param asset1 First pool asset (used to derive the LP token currency).
|
||||
* @param asset2 Second pool asset.
|
||||
* @param ammAccount AccountID of the AMM's pseudo-account (LP token issuer).
|
||||
* @param lpAccount AccountID of the liquidity provider.
|
||||
* @param j Journal for diagnostic logging.
|
||||
* @return The LP's token balance, or zero if the trustline is absent or frozen.
|
||||
*/
|
||||
STAmount
|
||||
ammLPHolds(
|
||||
@@ -762,6 +1030,17 @@ ammLPHolds(
|
||||
AccountID const& lpAccount,
|
||||
beast::Journal const j);
|
||||
|
||||
/** Read an LP's token balance using the asset pair stored in @p ammSle.
|
||||
*
|
||||
* Convenience overload; extracts `sfAsset`, `sfAsset2`, and `sfAccount` from
|
||||
* @p ammSle and delegates to the five-parameter `ammLPHolds`.
|
||||
*
|
||||
* @param view Ledger state to query.
|
||||
* @param ammSle The AMM's `ltAMM` SLE.
|
||||
* @param lpAccount AccountID of the liquidity provider.
|
||||
* @param j Journal for diagnostic logging.
|
||||
* @return The LP's token balance, or zero if the trustline is absent or frozen.
|
||||
*/
|
||||
STAmount
|
||||
ammLPHolds(
|
||||
ReadView const& view,
|
||||
@@ -769,25 +1048,72 @@ ammLPHolds(
|
||||
AccountID const& lpAccount,
|
||||
beast::Journal const j);
|
||||
|
||||
/** Get AMM trading fee for the given account. The fee is discounted
|
||||
* if the account is the auction slot owner or one of the slot's authorized
|
||||
* accounts.
|
||||
/** Get the effective AMM trading fee for @p account.
|
||||
*
|
||||
* Returns the auction slot's `sfDiscountedFee` if the slot is unexpired and
|
||||
* @p account is either the slot owner or one of up to four authorized accounts;
|
||||
* otherwise returns the AMM's global `sfTradingFee`. Expiration is compared
|
||||
* against the ledger's `parentCloseTime` (the slot stores
|
||||
* `parentCloseTime + TOTAL_TIME_SLOT_SECS` at creation, i.e. 24 hours).
|
||||
*
|
||||
* @param view Ledger state providing the current close time.
|
||||
* @param ammSle The AMM's `ltAMM` SLE.
|
||||
* @param account The account whose fee rate is needed.
|
||||
* @return Fee rate in basis points (0–1000).
|
||||
*/
|
||||
std::uint16_t
|
||||
getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account);
|
||||
|
||||
/** Returns total amount held by AMM for the given token.
|
||||
/** Read the AMM account's raw pool-asset balance, bypassing balance hooks.
|
||||
*
|
||||
* Unlike `accountHolds`, this function does not invoke `balanceHookIOU` or
|
||||
* `balanceHookMPT`, so the result is unaffected by `PaymentSandbox`
|
||||
* deferred-credit accounting. Used when the AMM needs its own unmodified
|
||||
* balance for math, not for payment routing. Returns zero if the trustline
|
||||
* or MPToken object is absent or frozen.
|
||||
*
|
||||
* @param view Ledger state to query.
|
||||
* @param ammAccountID AccountID of the AMM's pseudo-account.
|
||||
* @param asset The pool asset to query (IOU, XRP, or MPT).
|
||||
* @return The raw balance, or zero if unavailable.
|
||||
*/
|
||||
STAmount
|
||||
ammAccountHolds(ReadView const& view, AccountID const& ammAccountID, Asset const& asset);
|
||||
|
||||
/** Delete trustlines to AMM. If all trustlines are deleted then
|
||||
* AMM object and account are deleted. Otherwise tecINCOMPLETE is returned.
|
||||
/** Remove all ledger objects owned by the AMM and, if successful, delete the AMM itself.
|
||||
*
|
||||
* Deletion is ordered: IOU trustlines first, then MPToken objects, then the
|
||||
* AMM SLE and its `AccountRoot`. Because each ledger transaction has a bounded
|
||||
* work budget, not all trustlines may be removable in one call; in that case
|
||||
* `tecINCOMPLETE` is returned and the caller must submit additional transactions
|
||||
* to finish. The AMM can be re-deposited while deletion is incomplete.
|
||||
*
|
||||
* @param view Sandbox for applying state changes.
|
||||
* @param asset First pool asset (used to locate the AMM keylet).
|
||||
* @param asset2 Second pool asset.
|
||||
* @param j Journal for diagnostic logging.
|
||||
* @return `tesSUCCESS` on full deletion, `tecINCOMPLETE` if trustlines remain,
|
||||
* or `tecINTERNAL` for unexpected ledger inconsistencies.
|
||||
*/
|
||||
TER
|
||||
deleteAMMAccount(Sandbox& view, Asset const& asset, Asset const& asset2, beast::Journal j);
|
||||
|
||||
/** Initialize Auction and Voting slots and set the trading/discounted fee.
|
||||
/** Initialize the vote slot and auction slot on a new or re-created AMM.
|
||||
*
|
||||
* Called on both `AMMCreate` and on `AMMDeposit` when the pool was previously
|
||||
* drained to zero. Sets up:
|
||||
* - One vote entry for @p account with full weight (`kVOTE_WEIGHT_SCALE_FACTOR`).
|
||||
* - An auction slot owned by @p account, expiring in 24 hours, at zero price.
|
||||
* - `sfDiscountedFee` = `tfee / kAUCTION_SLOT_DISCOUNTED_FEE_FRACTION`.
|
||||
* - Absent-field canonicalization: fee fields are removed if their value is zero.
|
||||
* - Under `fixCleanup3_2_0`, stale `sfAuthAccounts` from any previous slot owner
|
||||
* are cleared.
|
||||
*
|
||||
* @param view Apply-view for the current transaction.
|
||||
* @param ammSle The AMM's `ltAMM` SLE (modified in place).
|
||||
* @param account The creator/re-depositor receiving the slot.
|
||||
* @param lptAsset The LP token asset descriptor (used as the `sfPrice` currency).
|
||||
* @param tfee Trading fee in basis points to set.
|
||||
*/
|
||||
void
|
||||
initializeFeeAuctionVote(
|
||||
@@ -797,16 +1123,41 @@ initializeFeeAuctionVote(
|
||||
Asset const& lptAsset,
|
||||
std::uint16_t tfee);
|
||||
|
||||
/** Return true if the Liquidity Provider is the only AMM provider, false
|
||||
* otherwise. Return tecINTERNAL if encountered an unexpected condition,
|
||||
* for instance Liquidity Provider has more than one LPToken trustline.
|
||||
/** Determine whether @p lpAccount is the sole remaining liquidity provider.
|
||||
*
|
||||
* Walks the AMM account's owner directory (up to 10 pages, covering at most
|
||||
* 4 objects) counting LPToken trustlines, pool-asset trustlines, MPToken
|
||||
* objects, and the AMM SLE itself. Any second LPToken trustline belonging to
|
||||
* a different account returns `false` immediately.
|
||||
*
|
||||
* @param view Ledger state to query.
|
||||
* @param ammIssue The LP token issue (currency + AMM account as issuer).
|
||||
* @param lpAccount AccountID of the candidate sole LP.
|
||||
* @return `true` if @p lpAccount is the only LP, `false` if other LPs exist,
|
||||
* or `Unexpected(tecINTERNAL)` for any unexpected directory state
|
||||
* (e.g. more than one LPToken trustline for @p lpAccount).
|
||||
*/
|
||||
Expected<bool, TER>
|
||||
isOnlyLiquidityProvider(ReadView const& view, Issue const& ammIssue, AccountID const& lpAccount);
|
||||
|
||||
/** Due to rounding, the LPTokenBalance of the last LP might
|
||||
* not match the LP's trustline balance. If it's within the tolerance,
|
||||
* update LPTokenBalance to match the LP's trustline balance.
|
||||
/** Reconcile the AMM's `sfLPTokenBalance` with the last LP's trustline balance.
|
||||
*
|
||||
* Accumulated rounding over the life of the pool can cause the AMM's running
|
||||
* `sfLPTokenBalance` to differ slightly from the sole LP's trustline balance.
|
||||
* This function:
|
||||
* 1. Confirms @p account is the only remaining LP via `isOnlyLiquidityProvider`.
|
||||
* 2. If so, verifies the discrepancy is within 0.1% (tolerance `1e-3`).
|
||||
* 3. If within tolerance, updates `sfLPTokenBalance` to @p lpTokens so the
|
||||
* final withdrawal leaves the AMM in a fully consistent state.
|
||||
*
|
||||
* @param sb Sandbox for applying the balance correction.
|
||||
* @param lpTokens The last LP's actual trustline balance.
|
||||
* @param ammSle The AMM's `ltAMM` SLE (updated in place if correction applied).
|
||||
* @param account AccountID of the candidate sole LP.
|
||||
* @return `true` if the balance was reconciled or no adjustment was needed
|
||||
* (other LPs exist), `Unexpected(tecAMM_INVALID_TOKENS)` if the
|
||||
* discrepancy exceeds tolerance, or `Unexpected(tecINTERNAL)` on an
|
||||
* unexpected directory error.
|
||||
*/
|
||||
Expected<bool, TER>
|
||||
verifyAndAdjustLPTokenBalance(
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/** @file
|
||||
* Free functions for querying and mutating `ltACCOUNT_ROOT` ledger entries.
|
||||
*
|
||||
* Provides the canonical helpers for freeze-state queries, spendable XRP
|
||||
* balance, owner-count bookkeeping, transfer fees, destination-tag
|
||||
* enforcement, and the creation and detection of pseudo-accounts (AMM,
|
||||
* Vault, LoanBroker). Almost every transaction processor depends on at
|
||||
* least one function here.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Expected.h>
|
||||
@@ -15,26 +24,60 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Check if the issuer has the global freeze flag set.
|
||||
@param issuer The account to check
|
||||
@return true if the account has global freeze set
|
||||
*/
|
||||
/** Check whether an IOU issuer has the global freeze flag active.
|
||||
*
|
||||
* XRP is never frozen; this function returns `false` immediately for the XRP
|
||||
* account. For any other issuer it reads `lsfGlobalFreeze` from the
|
||||
* account root. Missing accounts are treated as non-frozen.
|
||||
*
|
||||
* @param view The read-only ledger view to query.
|
||||
* @param issuer The account whose freeze state is to be checked.
|
||||
* @return `true` if `issuer` is a non-XRP account with `lsfGlobalFreeze` set;
|
||||
* `false` otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isGlobalFrozen(ReadView const& view, AccountID const& issuer);
|
||||
|
||||
// Calculate liquid XRP balance for an account.
|
||||
// This function may be used to calculate the amount of XRP that
|
||||
// the holder is able to freely spend. It subtracts reserve requirements.
|
||||
//
|
||||
// ownerCountAdj adjusts the owner count in case the caller calculates
|
||||
// before ledger entries are added or removed. Positive to add, negative
|
||||
// to subtract.
|
||||
//
|
||||
// @param ownerCountAdj positive to add to count, negative to reduce count.
|
||||
/** Compute the spendable XRP balance for an account after reserve deduction.
|
||||
*
|
||||
* Queries the account's current balance and owner count through the view's
|
||||
* virtual hook methods (`balanceHookIOU`, `ownerCountHook`) so that
|
||||
* `PaymentSandbox` can overlay uncommitted in-flight changes without any
|
||||
* branching here. The reserve is then subtracted; if the balance is below
|
||||
* the reserve, the function returns zero rather than a negative amount.
|
||||
*
|
||||
* Pseudo-accounts (AMM, Vault, LoanBroker) bypass the reserve calculation
|
||||
* entirely and receive the full balance as spendable XRP, because they
|
||||
* cannot submit transactions and must never be blocked by reserve checks.
|
||||
*
|
||||
* @param view The ledger view to query.
|
||||
* @param id The account whose liquid XRP balance is computed.
|
||||
* @param ownerCountAdj Signed delta applied to `sfOwnerCount` before the
|
||||
* reserve is calculated. Pass a positive value when the caller is about
|
||||
* to add ledger entries; pass a negative value when entries are about to
|
||||
* be removed. This lets callers reason about post-mutation availability
|
||||
* before the state is committed to the view.
|
||||
* @param j Journal for trace-level diagnostics.
|
||||
* @return The spendable XRP amount, clamped to zero from below.
|
||||
*/
|
||||
[[nodiscard]] XRPAmount
|
||||
xrpLiquid(ReadView const& view, AccountID const& id, std::int32_t ownerCountAdj, beast::Journal j);
|
||||
|
||||
/** Adjust the owner count up or down. */
|
||||
/** Increment or decrement `sfOwnerCount` on an account SLE and notify the view.
|
||||
*
|
||||
* Delegates to a file-static helper that clamps the result to
|
||||
* `[0, UINT32_MAX]`, logging at `fatal` severity if either bound would be
|
||||
* exceeded — silent wrapping of the `uint32_t` field would corrupt ledger
|
||||
* state. After clamping, `view.adjustOwnerCountHook()` is called before the
|
||||
* new value is written; `PaymentSandbox` overrides that hook to track the
|
||||
* high-water-mark count, ensuring subsequent `ownerCountHook` reads use the
|
||||
* most conservative value seen during the payment.
|
||||
*
|
||||
* @param view The mutable view on which the SLE update is recorded.
|
||||
* @param sle The account SLE to adjust; a null pointer is silently ignored.
|
||||
* @param amount Signed delta to apply to `sfOwnerCount`; must be non-zero.
|
||||
* @param j Journal for fatal-level diagnostics on overflow or underflow.
|
||||
*/
|
||||
void
|
||||
adjustOwnerCount(
|
||||
ApplyView& view,
|
||||
@@ -42,45 +85,89 @@ adjustOwnerCount(
|
||||
std::int32_t amount,
|
||||
beast::Journal j);
|
||||
|
||||
/** Returns IOU issuer transfer fee as Rate. Rate specifies
|
||||
* the fee as fractions of 1 billion. For example, 1% transfer rate
|
||||
* is represented as 1,010,000,000.
|
||||
* @param issuer The IOU issuer
|
||||
/** Return the IOU transfer fee for an issuer as a `Rate` value.
|
||||
*
|
||||
* `Rate` expresses the fee as a fraction of one billion, so a 1% fee is
|
||||
* represented as 1,010,000,000. If the issuer account does not exist or
|
||||
* has not set `sfTransferRate`, `parityRate` (no fee, i.e., 1,000,000,000)
|
||||
* is returned — callers never need to handle a null case.
|
||||
*
|
||||
* @param view The ledger view to query.
|
||||
* @param issuer The IOU issuer whose transfer fee is requested.
|
||||
* @return The issuer's `Rate`, or `parityRate` if none is configured.
|
||||
*/
|
||||
[[nodiscard]] Rate
|
||||
transferRate(ReadView const& view, AccountID const& issuer);
|
||||
|
||||
/** Generate a pseudo-account address from a pseudo owner key.
|
||||
@param pseudoOwnerKey The key to generate the address from
|
||||
@return The generated account ID
|
||||
*/
|
||||
/** Derive a collision-free pseudo-account `AccountID` from an owner key.
|
||||
*
|
||||
* Iterates up to 256 attempts. Each attempt hashes a counter, the parent
|
||||
* ledger's hash, and `pseudoOwnerKey` through `sha512Half` then
|
||||
* `ripesha_hasher` (RIPEMD-160(SHA-256(...))). The parent-hash component
|
||||
* prevents precomputation of collisions. The first candidate address that
|
||||
* has no existing `AccountRoot` in `view` is returned.
|
||||
*
|
||||
* @param view The ledger view used to check for address collisions.
|
||||
* @param pseudoOwnerKey The 256-bit key identifying the pseudo-account owner
|
||||
* (e.g., the AMM or Vault object ID).
|
||||
* @return A collision-free `AccountID`, or `beast::kZERO` if all 256
|
||||
* attempts collided. `createPseudoAccount` propagates exhaustion as
|
||||
* `tecDUPLICATE`.
|
||||
* @note The 256-attempt cap is consensus-critical and must not be changed
|
||||
* without an amendment, as it determines the pseudo-account address space.
|
||||
*/
|
||||
AccountID
|
||||
pseudoAccountAddress(ReadView const& view, uint256 const& pseudoOwnerKey);
|
||||
|
||||
/** Returns the list of fields that define an ACCOUNT_ROOT as a pseudo-account
|
||||
if set.
|
||||
|
||||
The list is constructed during initialization and is const after that.
|
||||
Pseudo-account designator fields MUST be maintained by including the
|
||||
SField::sMD_PseudoAccount flag in the SField definition.
|
||||
*/
|
||||
/** Return the singleton list of `SField`s that designate a pseudo-account.
|
||||
*
|
||||
* Built once at first call by scanning the `ltACCOUNT_ROOT` `SOTemplate`
|
||||
* from `LedgerFormats` and selecting every field whose `SField::sMD_PseudoAccount`
|
||||
* metadata bit is set. Currently includes `sfAMMID`, `sfVaultID`, and
|
||||
* `sfLoanBrokerID`. The discovery is fully data-driven: adding a new
|
||||
* pseudo-account type requires only tagging its key field with
|
||||
* `SField::sMD_PseudoAccount` in `sfields.macro` — no manual registration
|
||||
* here is needed.
|
||||
*
|
||||
* @return A const reference to the cached vector of pseudo-account fields.
|
||||
* @note Non-active amendments are harmless: the corresponding field will
|
||||
* never be set in practice, so the list remains correct regardless of
|
||||
* which amendments are enabled.
|
||||
*/
|
||||
[[nodiscard]] std::vector<SField const*> const&
|
||||
getPseudoAccountFields();
|
||||
|
||||
/** Returns true if and only if sleAcct is a pseudo-account or specific
|
||||
pseudo-accounts in pseudoFieldFilter.
|
||||
|
||||
Returns false if sleAcct is:
|
||||
- NOT a pseudo-account OR
|
||||
- NOT a ltACCOUNT_ROOT OR
|
||||
- null pointer
|
||||
*/
|
||||
/** Determine whether an SLE is a pseudo-account (optionally of a specific type).
|
||||
*
|
||||
* Returns `true` only when all three conditions hold: `sleAcct` is non-null,
|
||||
* its ledger-entry type is `ltACCOUNT_ROOT`, and at least one pseudo-account
|
||||
* designator field (from `getPseudoAccountFields()`) is present. When
|
||||
* `pseudoFieldFilter` is non-empty, only fields in the filter are considered,
|
||||
* allowing callers to distinguish AMM pseudo-accounts from Vault
|
||||
* pseudo-accounts.
|
||||
*
|
||||
* @param sleAcct The SLE to inspect; may be null.
|
||||
* @param pseudoFieldFilter Optional subset of pseudo-account fields to match
|
||||
* against. An empty set (the default) matches any pseudo-account field.
|
||||
* @return `true` if `sleAcct` is a pseudo-account (of a type in the filter
|
||||
* when one is provided); `false` otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isPseudoAccount(
|
||||
std::shared_ptr<SLE const> sleAcct,
|
||||
std::set<SField const*> const& pseudoFieldFilter = {});
|
||||
|
||||
/** Convenience overload that reads the account from the view. */
|
||||
/** Convenience overload that looks up the account from a `ReadView`.
|
||||
*
|
||||
* Reads the `AccountRoot` for `accountId` via `keylet::account()` and
|
||||
* delegates to the SLE overload.
|
||||
*
|
||||
* @param view The ledger view to query.
|
||||
* @param accountId The account address to look up.
|
||||
* @param pseudoFieldFilter Optional field filter forwarded to the SLE overload.
|
||||
* @return `true` if the account exists and is a pseudo-account matching the
|
||||
* filter; `false` otherwise.
|
||||
*/
|
||||
[[nodiscard]] inline bool
|
||||
isPseudoAccount(
|
||||
ReadView const& view,
|
||||
@@ -90,29 +177,48 @@ isPseudoAccount(
|
||||
return isPseudoAccount(view.read(keylet::account(accountId)), pseudoFieldFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create pseudo-account, storing pseudoOwnerKey into ownerField.
|
||||
/** Create a protocol-owned pseudo-account `AccountRoot` SLE.
|
||||
*
|
||||
* The list of valid ownerField is maintained in AccountRootHelpers.cpp and
|
||||
* the caller to this function must perform necessary amendment check(s)
|
||||
* before using a field. The amendment check is **not** performed in
|
||||
* createPseudoAccount.
|
||||
* Derives a collision-free address via `pseudoAccountAddress()`, constructs
|
||||
* an `AccountRoot` with zero balance, `lsfDisableMaster | lsfDefaultRipple |
|
||||
* lsfDepositAuth`, and stores `pseudoOwnerKey` in `ownerField`. When
|
||||
* `featureSingleAssetVault` or `featureLendingProtocol` is enabled,
|
||||
* `sfSequence` is set to `0`; otherwise it is set to the current ledger
|
||||
* sequence. The zero sequence makes pseudo-accounts visually distinguishable
|
||||
* and provides an extra barrier against accidental transaction submission.
|
||||
*
|
||||
* In debug builds, an `XRPL_ASSERT` fires if `ownerField` does not carry the
|
||||
* `SField::sMD_PseudoAccount` flag, catching misuse at development time.
|
||||
*
|
||||
* @param view The mutable ledger view into which the new SLE is
|
||||
* inserted.
|
||||
* @param pseudoOwnerKey The 256-bit key of the owning object (e.g., the AMM
|
||||
* or Vault ledger entry key); stored in `ownerField` on the new SLE.
|
||||
* @param ownerField The back-link field written on the new SLE; must be
|
||||
* one of the fields returned by `getPseudoAccountFields()`.
|
||||
* @return The newly created SLE on success, or `tecDUPLICATE` if all 256
|
||||
* address derivation attempts collided.
|
||||
* @note Amendment checks are the **caller's** responsibility. This function
|
||||
* is amendment-neutral by design; callers such as `VaultCreate` and
|
||||
* `LoanBrokerSet` must gate on the relevant feature flag before invoking.
|
||||
*/
|
||||
[[nodiscard]] Expected<std::shared_ptr<SLE>, TER>
|
||||
createPseudoAccount(
|
||||
ApplyView& view,
|
||||
uint256 const& pseudoOwnerKey,
|
||||
SField const& ownerField,
|
||||
std::uint32_t additionalFlags = 0);
|
||||
createPseudoAccount(ApplyView& view, uint256 const& pseudoOwnerKey, SField const& ownerField);
|
||||
|
||||
[[nodiscard]] bool
|
||||
isBlackholed(ReadView const& view, std::shared_ptr<SLE const> const& sle);
|
||||
|
||||
/** Checks the destination and tag.
|
||||
|
||||
- Checks that the SLE is not null.
|
||||
- If the SLE requires a destination tag, checks that there is a tag.
|
||||
*/
|
||||
/** Validate a payment destination SLE and its destination-tag requirement.
|
||||
*
|
||||
* Returns `tecNO_DST` if `toSle` is null (the destination account does not
|
||||
* exist), and `tecDST_TAG_NEEDED` if the destination has set
|
||||
* `lsfRequireDestTag` but the transaction supplies no tag. Returns
|
||||
* `tesSUCCESS` otherwise.
|
||||
*
|
||||
* @param toSle The destination account SLE; may be null.
|
||||
* @param hasDestinationTag `true` if the transaction includes a destination
|
||||
* tag field.
|
||||
* @return `tecNO_DST`, `tecDST_TAG_NEEDED`, or `tesSUCCESS`.
|
||||
* @note The ledger enforces the *presence* of a tag but never interprets its
|
||||
* value; semantics (e.g., exchange user IDs) are opaque to the protocol.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkDestinationAndTag(SLE::const_ref toSle, bool hasDestinationTag);
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
/** @file
|
||||
* Central contract for credential and deposit pre-authorization logic.
|
||||
*
|
||||
* Included by every fund-transfer transactor (Payment, EscrowFinish,
|
||||
* PaymentChannelClaim, VaultDeposit) that must honor destination-account
|
||||
* access controls.
|
||||
*
|
||||
* Functions divide along the preclaim / doApply boundary:
|
||||
* - `xrpl::credentials::*` — read-only checks safe to call from preclaim.
|
||||
* - `xrpl::verifyDepositPreauth` / `xrpl::verifyValidDomain` — mutating
|
||||
* counterparts that must be called from doApply when the corresponding
|
||||
* preclaim function succeeds, so that expired credential objects are
|
||||
* physically deleted from the ledger as a side effect.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -13,57 +27,225 @@
|
||||
namespace xrpl {
|
||||
namespace credentials {
|
||||
|
||||
// These function will be used by the code that use DepositPreauth / Credentials
|
||||
// (and any future pre-authorization modes) as part of authorization (all the
|
||||
// transfer funds transactions)
|
||||
|
||||
// Check if credential sfExpiration field has passed ledger's parentCloseTime
|
||||
/** Test whether a credential SLE has passed its expiration time.
|
||||
*
|
||||
* Reads `sfExpiration` from @p sleCredential, defaulting to
|
||||
* `std::numeric_limits<uint32_t>::max()` when the field is absent, so
|
||||
* credentials with no expiration field never expire.
|
||||
*
|
||||
* @param sleCredential The credential SLE to inspect.
|
||||
* @param closed The parent ledger's close time. Must be a
|
||||
* NetClock epoch value — do not pass wall-clock time.
|
||||
* @return `true` if the credential has expired, `false` otherwise.
|
||||
*/
|
||||
bool
|
||||
checkExpired(SLE const& sleCredential, NetClock::time_point const& closed);
|
||||
|
||||
// Actually remove a credentials object from the ledger
|
||||
/** Remove a credential SLE and its entries from both owner directories.
|
||||
*
|
||||
* A credential is indexed in two owner directories — the issuer's and the
|
||||
* subject's. Reserve-count accounting depends on acceptance state:
|
||||
* - Before acceptance (`lsfAccepted` unset): only the issuer holds the
|
||||
* reserve; only the issuer's count is decremented.
|
||||
* - After acceptance with distinct accounts: the subject holds the reserve
|
||||
* and its count is decremented.
|
||||
* - When issuer and subject are the same account, only one directory
|
||||
* removal is performed.
|
||||
*
|
||||
* @note Paths indicating ledger corruption (missing account SLE, failed
|
||||
* `dirRemove`) are marked `LCOV_EXCL` and are unreachable under normal
|
||||
* operation.
|
||||
*
|
||||
* @param view Mutable ledger view through which the SLE is erased.
|
||||
* @param sleCredential The credential SLE to delete; must not be null.
|
||||
* @param j Journal for fatal-level error logging.
|
||||
* @return `tesSUCCESS` on success; `tecNO_ENTRY` if @p sleCredential is
|
||||
* null; `tecINTERNAL` or `tefBAD_LEDGER` on internal directory
|
||||
* inconsistency.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
deleteSLE(ApplyView& view, std::shared_ptr<SLE> const& sleCredential, beast::Journal j);
|
||||
|
||||
// Amendment and parameters checks for sfCredentialIDs field
|
||||
/** Validate the `sfCredentialIDs` field of a transaction at preflight time.
|
||||
*
|
||||
* Enforces non-empty, at most `kMAX_CREDENTIALS_ARRAY_SIZE` entries, and no
|
||||
* duplicate hashes. Returns `tesSUCCESS` immediately when `sfCredentialIDs`
|
||||
* is absent, as credentials are optional for most transaction types.
|
||||
*
|
||||
* @param tx The transaction under preflight validation.
|
||||
* @param j Journal for trace-level malformed-transaction logging.
|
||||
* @return `tesSUCCESS` if the field is absent or valid; `temMALFORMED` if
|
||||
* the array is empty, too large, or contains duplicates.
|
||||
*/
|
||||
NotTEC
|
||||
checkFields(STTx const& tx, beast::Journal j);
|
||||
|
||||
// Accessing the ledger to check if provided credentials are valid. Do not use
|
||||
// in doApply (only in preclaim) since it does not remove expired credentials.
|
||||
// If you call it in preclaim, you also must call verifyDepositPreauth in
|
||||
// doApply
|
||||
/** Verify that all credentials in a transaction exist, are owned by the
|
||||
* sender, and have been accepted — for use in preclaim only.
|
||||
*
|
||||
* Checks each ID in `sfCredentialIDs`: the SLE must exist, its `sfSubject`
|
||||
* must equal @p src, and `lsfAccepted` must be set. Expiration is
|
||||
* deliberately not checked here; expired credentials are deleted in doApply
|
||||
* by `verifyDepositPreauth` or `verifyValidDomain`.
|
||||
*
|
||||
* @note If this returns `tesSUCCESS` in preclaim, the caller must invoke
|
||||
* `verifyDepositPreauth` in doApply to garbage-collect any credentials
|
||||
* that expire before the enclosing transaction applies.
|
||||
*
|
||||
* @param tx The transaction whose `sfCredentialIDs` field is inspected.
|
||||
* @param view Read-only ledger view for SLE lookups.
|
||||
* @param src The account that must own every listed credential.
|
||||
* @param j Journal for trace-level logging.
|
||||
* @return `tesSUCCESS` if `sfCredentialIDs` is absent or all credentials are
|
||||
* valid; `tecBAD_CREDENTIALS` if any credential is missing, belongs to a
|
||||
* different account, or has not been accepted.
|
||||
*/
|
||||
TER
|
||||
valid(STTx const& tx, ReadView const& view, AccountID const& src, beast::Journal j);
|
||||
|
||||
// Check if subject has any credential maching the given domain. If you call it
|
||||
// in preclaim and it returns tecEXPIRED, you should call verifyValidDomain in
|
||||
// doApply. This will ensure that expired credentials are deleted.
|
||||
/** Check whether @p subject holds a live, accepted credential for a
|
||||
* permissioned domain — for use in preclaim only.
|
||||
*
|
||||
* Reads the `PermissionedDomain` SLE, iterates its `sfAcceptedCredentials`
|
||||
* array, and looks up the corresponding credential SLE for @p subject.
|
||||
* A credential qualifies when it exists, has not expired, and carries
|
||||
* `lsfAccepted`.
|
||||
*
|
||||
* Because a `ReadView` is immutable, expired credentials cannot be deleted
|
||||
* here. The function returns `tecEXPIRED` when all matching credentials
|
||||
* are expired — signaling the caller that the condition may resolve in
|
||||
* doApply where `verifyValidDomain` will physically remove them.
|
||||
*
|
||||
* @note If this returns `tecEXPIRED` in preclaim, the caller must invoke
|
||||
* `verifyValidDomain` in doApply so that expired objects are
|
||||
* garbage-collected even if the transaction ultimately fails.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param domainID Key of the `PermissionedDomain` SLE to check against.
|
||||
* @param subject Account that must hold a qualifying credential.
|
||||
* @return `tesSUCCESS` if a live accepted credential exists; `tecEXPIRED`
|
||||
* if only expired credentials were found; `tecNO_AUTH` if no matching
|
||||
* credential exists; `tecOBJECT_NOT_FOUND` if the domain does not exist.
|
||||
*/
|
||||
TER
|
||||
validDomain(ReadView const& view, uint256 domainID, AccountID const& subject);
|
||||
|
||||
// This function is only called when we about to return tecNO_PERMISSION
|
||||
// because all the checks for the DepositPreauth authorization failed.
|
||||
/** Check whether a set of credential IDs matches a credential-set
|
||||
* `DepositPreauth` entry for the destination account.
|
||||
*
|
||||
* Builds a sorted `std::set<std::pair<AccountID, Slice>>` of
|
||||
* `(issuer, credentialType)` pairs from @p credIDs and tests for the
|
||||
* existence of the corresponding `keylet::depositPreauth(dst, sorted)`.
|
||||
* The sorted representation matches the canonical key used at
|
||||
* `DepositPreauth` creation time.
|
||||
*
|
||||
* @note Credential existence is assumed to have been confirmed in preclaim.
|
||||
* A missing SLE here indicates an internal consistency error.
|
||||
* @note `Slice` members in the internal sorted set are non-owning views
|
||||
* into SLE storage. A `lifeExtender` vector keeps the SLEs alive for
|
||||
* the duration of the lookup.
|
||||
*
|
||||
* @param view Read-only ledger view for SLE and keylet lookups.
|
||||
* @param credIDs The `sfCredentialIDs` vector from the transaction.
|
||||
* @param dst The destination account whose `DepositPreauth` is checked.
|
||||
* @return `tesSUCCESS` if a matching `DepositPreauth` object exists;
|
||||
* `tecNO_PERMISSION` if none exists; `tefINTERNAL` if a credential SLE
|
||||
* is unexpectedly missing or a duplicate pair is encountered.
|
||||
*/
|
||||
TER
|
||||
authorizedDepositPreauth(ReadView const& view, STVector256 const& ctx, AccountID const& dst);
|
||||
|
||||
// Sort credentials array, return empty set if there are duplicates
|
||||
/** Build a sorted `(issuer, credentialType)` set from a credentials array.
|
||||
*
|
||||
* Produces the canonical representation used to key `DepositPreauth`
|
||||
* objects. Each element of @p credentials must carry `sfIssuer` and
|
||||
* `sfCredentialType`.
|
||||
*
|
||||
* @param credentials An `STArray` of credential pairs, as stored in a
|
||||
* `DepositPreauth` or `PermissionedDomainSet` transaction.
|
||||
* @return A sorted set of `(AccountID, Slice)` pairs; an empty set if any
|
||||
* duplicate `(issuer, credentialType)` pair is detected.
|
||||
*/
|
||||
std::set<std::pair<AccountID, Slice>>
|
||||
makeSorted(STArray const& credentials);
|
||||
|
||||
// Check credentials array passed to DepositPreauth/PermissionedDomainSet
|
||||
// transactions
|
||||
/** Validate a credential array in `DepositPreauth` or
|
||||
* `PermissionedDomainSet` transactions at preflight time.
|
||||
*
|
||||
* Credentials in these transactions are `(issuer, credentialType)` pairs
|
||||
* rather than object hashes. Enforces: non-empty; at most @p maxSize
|
||||
* entries; valid issuer `AccountID`; `sfCredentialType` length in
|
||||
* `[1, kMAX_CREDENTIAL_TYPE_LENGTH]` bytes; and no logical duplicates
|
||||
* (detected via `sha512Half(issuer, credentialType)`).
|
||||
*
|
||||
* @param credentials The `STArray` of credential pairs to validate.
|
||||
* @param maxSize Maximum permitted array length (caller-supplied per
|
||||
* transaction type).
|
||||
* @param j Journal for trace-level malformed-transaction logging.
|
||||
* @return `tesSUCCESS` if all entries are valid; `temARRAY_EMPTY`,
|
||||
* `temARRAY_TOO_LARGE`, `temINVALID_ACCOUNT_ID`, or `temMALFORMED`
|
||||
* on the first constraint violation found.
|
||||
*/
|
||||
NotTEC
|
||||
checkArray(STArray const& credentials, unsigned maxSize, beast::Journal j);
|
||||
|
||||
} // namespace credentials
|
||||
|
||||
// Check expired credentials and for credentials maching DomainID of the ledger
|
||||
// object
|
||||
/** Enforce domain-credential authorization in doApply, deleting expired
|
||||
* credentials as a side effect.
|
||||
*
|
||||
* The doApply counterpart to `credentials::validDomain`. Collects all
|
||||
* credential SLEs for @p account that match the `sfAcceptedCredentials`
|
||||
* list of the `PermissionedDomain` at @p domainID, calls
|
||||
* `credentials::removeExpired` to physically delete any that have expired,
|
||||
* then re-checks whether at least one live, accepted credential remains.
|
||||
*
|
||||
* The two-pass design (collect → expire → re-validate) ensures expired
|
||||
* objects are garbage-collected even when the surrounding transaction
|
||||
* ultimately fails.
|
||||
*
|
||||
* @param view Mutable ledger view; expired credential SLEs are erased.
|
||||
* @param account Account whose credentials are being verified.
|
||||
* @param domainID Key of the `PermissionedDomain` SLE.
|
||||
* @param j Journal for trace/error logging.
|
||||
* @return `tesSUCCESS` if a live accepted credential for the domain exists;
|
||||
* `tecEXPIRED` if only expired credentials were found; `tecNO_PERMISSION`
|
||||
* if no matching credential exists; `tecOBJECT_NOT_FOUND` if the domain
|
||||
* SLE is missing; or a propagated `TER` error from `removeExpired` under
|
||||
* `fixCleanup3_1_3`.
|
||||
*/
|
||||
TER
|
||||
verifyValidDomain(ApplyView& view, AccountID const& account, uint256 domainID, beast::Journal j);
|
||||
|
||||
// Check expired credentials and for existing DepositPreauth ledger object
|
||||
/** Enforce deposit pre-authorization in doApply, deleting expired credentials
|
||||
* as a side effect.
|
||||
*
|
||||
* Called by Payment, EscrowFinish, and PaymentChannelClaim when the
|
||||
* destination account has `lsfDepositAuth` set. Authorization succeeds
|
||||
* when any of the following hold:
|
||||
* - `src == dst` (self-payments are always allowed).
|
||||
* - `keylet::depositPreauth(dst, src)` exists (account-level pre-auth).
|
||||
* - A credential-set `DepositPreauth` object exists for the credentials
|
||||
* submitted via `sfCredentialIDs` (via `credentials::authorizedDepositPreauth`).
|
||||
*
|
||||
* If `sfCredentialIDs` is present, `credentials::removeExpired` is called
|
||||
* unconditionally before the authorization tests. If any credential was
|
||||
* expired, `tecEXPIRED` is returned immediately without attempting
|
||||
* authorization.
|
||||
*
|
||||
* @param tx The transaction under doApply; may carry `sfCredentialIDs`.
|
||||
* @param view Mutable ledger view; expired credential SLEs may be erased.
|
||||
* @param src The sending account.
|
||||
* @param dst The destination account.
|
||||
* @param sleDst The destination account's SLE, used to test `lsfDepositAuth`.
|
||||
* If null, `lsfDepositAuth` is treated as unset and the function returns
|
||||
* `tesSUCCESS`.
|
||||
* @param j Journal for trace/error logging.
|
||||
* @return `tesSUCCESS` if authorized or `lsfDepositAuth` is not set;
|
||||
* `tecEXPIRED` if submitted credentials have expired;
|
||||
* `tecNO_PERMISSION` if no matching pre-authorization exists; or a
|
||||
* propagated error from `removeExpired` or `authorizedDepositPreauth`.
|
||||
*/
|
||||
TER
|
||||
verifyDepositPreauth(
|
||||
STTx const& tx,
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/** @file
|
||||
* Runtime enforcement helpers for the XRPL delegate account system.
|
||||
*
|
||||
* Transactors call these two functions in sequence during permission
|
||||
* validation: `checkTxPermission` for the broad transaction-type gate,
|
||||
* then `loadGranularPermission` when a more restrictive, field-level
|
||||
* check is needed. The permission schema and encoding convention live
|
||||
* in `xrpl/protocol/Permissions.h`.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/Permissions.h>
|
||||
@@ -7,24 +16,64 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* Check if the delegate account has permission to execute the transaction.
|
||||
* @param delegate The delegate account.
|
||||
* @param tx The transaction that the delegate account intends to execute.
|
||||
* @return tesSUCCESS if the transaction is allowed, terNO_DELEGATE_PERMISSION
|
||||
* if not.
|
||||
/** Determine whether a delegate relationship grants blanket permission for
|
||||
* a transaction type.
|
||||
*
|
||||
* Scans the `sfPermissions` array of the `ltDELEGATE` ledger entry for an
|
||||
* element whose `sfPermissionValue` equals `tx.getTxnType() + 1` — the
|
||||
* transaction-level encoding used on-ledger. Returns `tesSUCCESS` on the
|
||||
* first match, or `terNO_DELEGATE_PERMISSION` if no match is found.
|
||||
*
|
||||
* A null `delegate` pointer is treated as a missing ledger entry and
|
||||
* returns `terNO_DELEGATE_PERMISSION` immediately.
|
||||
*
|
||||
* The result is `NotTEC` (no `tec` fee-claim codes) because the two
|
||||
* meaningful outcomes are `tesSUCCESS` and `terNO_DELEGATE_PERMISSION`.
|
||||
* The `ter` (retry) code is intentional: the `ltDELEGATE` object could be
|
||||
* updated in a subsequent ledger, so an identical transaction may succeed
|
||||
* in the future without modification.
|
||||
*
|
||||
* @param delegate Immutable `ltDELEGATE` SLE obtained via `view.read()`;
|
||||
* may be null, in which case `terNO_DELEGATE_PERMISSION` is returned.
|
||||
* @param tx The transaction whose type is being checked.
|
||||
* @return `tesSUCCESS` if the delegate holds a transaction-level permission
|
||||
* for `tx`'s type; `terNO_DELEGATE_PERMISSION` otherwise.
|
||||
* @note Callers should resolve the SLE via `keylet::delegate(account,
|
||||
* delegate)` and pass it directly. If the SLE is absent from the
|
||||
* ledger, `view.read()` returns null and the guard here handles it.
|
||||
* @see loadGranularPermission — for fine-grained per-flag enforcement when
|
||||
* this function returns `terNO_DELEGATE_PERMISSION`.
|
||||
*/
|
||||
NotTEC
|
||||
checkTxPermission(std::shared_ptr<SLE const> const& delegate, STTx const& tx);
|
||||
|
||||
/**
|
||||
* Load the granular permissions granted to the delegate account for the
|
||||
* specified transaction type
|
||||
* @param delegate The delegate account.
|
||||
* @param type Used to determine which granted granular permissions to load,
|
||||
* based on the transaction type.
|
||||
* @param granularPermissions Granted granular permissions tied to the
|
||||
* transaction type.
|
||||
/** Populate a set with all granular sub-operation permissions the delegate
|
||||
* holds for a given transaction type.
|
||||
*
|
||||
* Walks the `sfPermissions` array of the `ltDELEGATE` ledger entry. For
|
||||
* each element, it casts the `sfPermissionValue` to `GranularPermissionType`
|
||||
* and asks `Permission::getInstance().getGranularTxType()` whether that
|
||||
* granular type belongs to `type`. Matching values are inserted into
|
||||
* `granularPermissions`.
|
||||
*
|
||||
* A null `delegate` pointer is a silent no-op; the output set is left
|
||||
* unchanged.
|
||||
*
|
||||
* The set is caller-owned and passed by reference so transactors can declare
|
||||
* it on the stack, avoiding heap allocation. Callers may also accumulate
|
||||
* results from multiple calls if needed.
|
||||
*
|
||||
* @param delegate Immutable `ltDELEGATE` SLE; may be null (no-op).
|
||||
* @param type The transaction type whose granular permissions should be
|
||||
* collected (e.g., `ttTRUST_SET`, `ttPAYMENT`).
|
||||
* @param granularPermissions Output set populated with every
|
||||
* `GranularPermissionType` the delegate holds that maps to `type`.
|
||||
* @note This function is the second stage of a two-step check. Call
|
||||
* `checkTxPermission` first; only invoke this when that returns
|
||||
* `terNO_DELEGATE_PERMISSION` and the transaction type supports
|
||||
* granular flags. Calling it unconditionally wastes a full scan of
|
||||
* the permissions array on the common case.
|
||||
* @see checkTxPermission — for the broad transaction-type gate.
|
||||
*/
|
||||
void
|
||||
loadGranularPermission(
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/** @file
|
||||
* Traversal utilities for ledger directory nodes (`ltDIR_NODE`).
|
||||
*
|
||||
* A directory is a linked list of pages (`SLE` of type `ltDIR_NODE`),
|
||||
* where each page holds an `sfIndexes` field (`STVector256`) of child
|
||||
* ledger-entry keys and an `sfIndexNext` field that chains to the next
|
||||
* page. Owner directories track every object an account holds; order-
|
||||
* book directories track standing offers at a given quality.
|
||||
*
|
||||
* This header provides:
|
||||
* - A const-aware template core (`detail::internalDirFirst` /
|
||||
* `detail::internalDirNext`) that unifies the read and write traversal
|
||||
* paths at compile time.
|
||||
* - A deprecated step-iterator API (`cdirFirst`, `cdirNext`, `dirFirst`,
|
||||
* `dirNext`) used only where cursor patching during deletion is required.
|
||||
* - Higher-level callback iterators (`forEachItem`, `forEachItemAfter`)
|
||||
* for exhaustive and paginated walks.
|
||||
* - `dirIsEmpty` and `describeOwnerDir` utility helpers.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
@@ -15,6 +34,32 @@ namespace xrpl {
|
||||
|
||||
namespace detail {
|
||||
|
||||
/** Advance a directory cursor to the next entry, crossing page boundaries.
|
||||
*
|
||||
* When the cursor has consumed all entries in the current page, the function
|
||||
* follows `sfIndexNext` to load the next page and tail-calls itself to yield
|
||||
* the first entry of that page in a single logical step. If `sfIndexNext` is
|
||||
* zero the directory is exhausted: `entry` is zeroed and `false` is returned.
|
||||
*
|
||||
* The `if constexpr` branch selects `view.read()` when `N` is `SLE const`
|
||||
* (read-only traversal via `ReadView`) and `view.peek()` when `N` is `SLE`
|
||||
* (mutable traversal via `ApplyView`), keeping both paths in one template.
|
||||
*
|
||||
* @tparam V A view type derived from `ReadView`.
|
||||
* @tparam N Either `SLE` (mutable) or `SLE const` (read-only).
|
||||
* @param view The ledger view to query pages from.
|
||||
* @param root The 256-bit key of the directory's root (anchor) page.
|
||||
* @param page In/out: the current page SLE; updated when a page boundary
|
||||
* is crossed.
|
||||
* @param index In/out: the zero-based cursor within `page->sfIndexes`;
|
||||
* incremented to point past the entry that was just returned.
|
||||
* @param entry Out: the key of the current entry on success; zeroed on
|
||||
* end-of-directory.
|
||||
* @return `true` if an entry was produced; `false` if the directory is
|
||||
* exhausted.
|
||||
* @note An `XRPL_ASSERT` fires in instrumented builds if `index` exceeds
|
||||
* the page's entry count, indicating a corrupted cursor.
|
||||
*/
|
||||
template <
|
||||
class V,
|
||||
class N,
|
||||
@@ -64,6 +109,23 @@ internalDirNext(
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Initialise a directory cursor at the first entry of the root page.
|
||||
*
|
||||
* Loads the root page via `view.read()` (when `N` is `SLE const`) or
|
||||
* `view.peek()` (when `N` is `SLE`), resets the index to zero, then
|
||||
* delegates to `internalDirNext` to yield the first entry.
|
||||
*
|
||||
* @tparam V A view type derived from `ReadView`.
|
||||
* @tparam N Either `SLE` (mutable) or `SLE const` (read-only).
|
||||
* @param view The ledger view to query pages from.
|
||||
* @param root The 256-bit key of the directory's root (anchor) page.
|
||||
* @param page Out: set to the root page SLE on success; unchanged if the
|
||||
* root page is absent.
|
||||
* @param index Out: set to zero before delegating to `internalDirNext`.
|
||||
* @param entry Out: the key of the first entry on success.
|
||||
* @return `true` if the directory has at least one entry; `false` if the
|
||||
* root page is absent or the directory is empty.
|
||||
*/
|
||||
template <
|
||||
class V,
|
||||
class N,
|
||||
@@ -119,6 +181,24 @@ cdirFirst(
|
||||
unsigned int& index,
|
||||
uint256& entry);
|
||||
|
||||
/** Returns the first entry in the directory, advancing the index.
|
||||
*
|
||||
* Mutable overload of `cdirFirst` for use with `ApplyView`. Yields a
|
||||
* `shared_ptr<SLE>` obtained via `view.peek()`, allowing the caller to
|
||||
* modify the page SLE if required.
|
||||
*
|
||||
* @deprecated Prefer the `Dir` range adaptor or `forEachItem` for new
|
||||
* code. Use this overload only when cursor patching during deletion
|
||||
* is required (see `cleanupOnAccountDelete` in `View.cpp`).
|
||||
*
|
||||
* @param view The mutable view against which to operate.
|
||||
* @param root The 256-bit key of the directory's root page.
|
||||
* @param page Out: set to the root page SLE obtained via `peek()`.
|
||||
* @param index Out: set to the cursor position within `page->sfIndexes`.
|
||||
* @param entry Out: the key of the first directory entry.
|
||||
* @return `true` if the directory has at least one entry; `false`
|
||||
* otherwise.
|
||||
*/
|
||||
bool
|
||||
dirFirst(
|
||||
ApplyView& view,
|
||||
@@ -151,6 +231,31 @@ cdirNext(
|
||||
unsigned int& index,
|
||||
uint256& entry);
|
||||
|
||||
/** Advances the mutable directory cursor to the next entry.
|
||||
*
|
||||
* Mutable overload of `cdirNext` for use with `ApplyView`. Page
|
||||
* transitions are handled transparently: when `index` reaches the end
|
||||
* of the current page, `sfIndexNext` is followed and the cursor is reset
|
||||
* to the first entry of the new page.
|
||||
*
|
||||
* @deprecated Prefer the `Dir` range adaptor or `forEachItem` for new
|
||||
* code. The primary use case for this function is cursor patching
|
||||
* during deletion: `cleanupOnAccountDelete` (in `View.cpp`) decrements
|
||||
* `index` after each deletion so the cursor stays aligned as entries
|
||||
* shift — a technique that relies on the cursor being externally
|
||||
* accessible.
|
||||
*
|
||||
* @param view The mutable view against which to operate.
|
||||
* @param root The 256-bit key of the directory's root page.
|
||||
* @param page In/out: the current page SLE; updated on page boundary
|
||||
* crossing.
|
||||
* @param index In/out: the cursor position within `page->sfIndexes`;
|
||||
* incremented past the returned entry.
|
||||
* @param entry Out: the key of the current entry on success; zeroed when
|
||||
* the directory is exhausted.
|
||||
* @return `true` if an entry was produced; `false` if the directory is
|
||||
* exhausted.
|
||||
*/
|
||||
bool
|
||||
dirNext(
|
||||
ApplyView& view,
|
||||
@@ -160,19 +265,61 @@ dirNext(
|
||||
uint256& entry);
|
||||
/** @} */
|
||||
|
||||
/** Iterate all items in the given directory. */
|
||||
/** Exhaustively walk every entry in a directory, invoking a callback for each.
|
||||
*
|
||||
* Iterates all pages of the directory in `sfIndexNext` chain order, calling
|
||||
* `f` with the materialised child SLE for every key in `sfIndexes`. The
|
||||
* child SLE is obtained via `view.read(keylet::child(key))` and may be
|
||||
* `nullptr` if the referenced entry is absent from the view; the callback
|
||||
* must handle that case. Iteration terminates when `sfIndexNext` is zero or
|
||||
* a page SLE is missing; there is no early-exit mechanism.
|
||||
*
|
||||
* @param view The read-only ledger view to query.
|
||||
* @param root Keylet of the directory's root page; must have type
|
||||
* `ltDIR_NODE`.
|
||||
* @param f Callback invoked with each child SLE (possibly `nullptr`).
|
||||
* @note An `XRPL_ASSERT` fires in instrumented builds if `root.type` is
|
||||
* not `ltDIR_NODE`; in release builds the function returns silently.
|
||||
*/
|
||||
void
|
||||
forEachItem(
|
||||
ReadView const& view,
|
||||
Keylet const& root,
|
||||
std::function<void(std::shared_ptr<SLE const> const&)> const& f);
|
||||
|
||||
/** Iterate all items after an item in the given directory.
|
||||
@param after The key of the item to start after
|
||||
@param hint The directory page containing `after`
|
||||
@param limit The maximum number of items to return
|
||||
@return `false` if the iteration failed
|
||||
*/
|
||||
/** Paginated directory walk, delivering items that follow a cursor key.
|
||||
*
|
||||
* Supports cursor-based pagination as used by RPC handlers such as
|
||||
* `account_offers`, `account_lines`, and `account_channels`. When
|
||||
* `after` is non-zero the function first attempts to jump to the `hint`
|
||||
* page (the page the client last saw) to avoid re-scanning all prior
|
||||
* pages; if the hint does not contain `after`, it falls back to a linear
|
||||
* scan from the root. Once the cursor is located, subsequent entries are
|
||||
* delivered to `f` until `limit` is reached or the directory is exhausted.
|
||||
*
|
||||
* The callback `f` returns `bool`: `true` to continue (and decrement the
|
||||
* limit counter), `false` to stop immediately regardless of the remaining
|
||||
* limit. Callers conventionally request `limit + 1` items and infer a
|
||||
* non-empty next page when exactly `limit + 1` items are delivered.
|
||||
*
|
||||
* @param view The read-only ledger view to query.
|
||||
* @param root Keylet of the directory's root page; must have type
|
||||
* `ltDIR_NODE`.
|
||||
* @param after Cursor key: only entries that follow this key in directory
|
||||
* order are delivered. Pass `uint256()` (zero) to start from the
|
||||
* beginning, in which case the function always returns `true`.
|
||||
* @param hint Page number expected to contain `after`; used as a fast-
|
||||
* path optimisation. Ignored when `after` is zero or when the hint
|
||||
* page does not actually contain `after`.
|
||||
* @param limit Maximum number of `true`-returning callback invocations
|
||||
* before the walk stops.
|
||||
* @param f Callback invoked for each qualifying child SLE (possibly
|
||||
* `nullptr` if the key is absent). Return `true` to continue
|
||||
* iteration; `false` to stop early.
|
||||
* @return `true` if `after` was found (or `after` is zero); `false` if
|
||||
* the cursor key was never located, indicating a stale or invalid
|
||||
* marker that callers should surface as a pagination error.
|
||||
*/
|
||||
bool
|
||||
forEachItemAfter(
|
||||
ReadView const& view,
|
||||
@@ -182,7 +329,15 @@ forEachItemAfter(
|
||||
unsigned int limit,
|
||||
std::function<bool(std::shared_ptr<SLE const> const&)> const& f);
|
||||
|
||||
/** Iterate all items in an account's owner directory. */
|
||||
/** Exhaustively walk every entry in an account's owner directory.
|
||||
*
|
||||
* Convenience overload that resolves `id` to `keylet::ownerDir(id)` and
|
||||
* forwards to `forEachItem(view, Keylet, f)`.
|
||||
*
|
||||
* @param view The read-only ledger view to query.
|
||||
* @param id The account whose owner directory should be iterated.
|
||||
* @param f Callback invoked with each child SLE (possibly `nullptr`).
|
||||
*/
|
||||
inline void
|
||||
forEachItem(
|
||||
ReadView const& view,
|
||||
@@ -192,12 +347,22 @@ forEachItem(
|
||||
forEachItem(view, keylet::ownerDir(id), f);
|
||||
}
|
||||
|
||||
/** Iterate all items after an item in an owner directory.
|
||||
@param after The key of the item to start after
|
||||
@param hint The directory page containing `after`
|
||||
@param limit The maximum number of items to return
|
||||
@return `false` if the iteration failed
|
||||
*/
|
||||
/** Paginated walk of an account's owner directory after a cursor key.
|
||||
*
|
||||
* Convenience overload that resolves `id` to `keylet::ownerDir(id)` and
|
||||
* forwards to `forEachItemAfter(view, Keylet, after, hint, limit, f)`.
|
||||
*
|
||||
* @param view The read-only ledger view to query.
|
||||
* @param id The account whose owner directory should be iterated.
|
||||
* @param after Cursor key; pass `uint256()` (zero) to start from the
|
||||
* beginning.
|
||||
* @param hint Page number expected to contain `after`.
|
||||
* @param limit Maximum number of `true`-returning callback invocations.
|
||||
* @param f Callback invoked for each qualifying child SLE. Return `true`
|
||||
* to continue; `false` to stop early.
|
||||
* @return `true` if `after` was found (or is zero); `false` if the cursor
|
||||
* was never located.
|
||||
*/
|
||||
inline bool
|
||||
forEachItemAfter(
|
||||
ReadView const& view,
|
||||
@@ -210,13 +375,36 @@ forEachItemAfter(
|
||||
return forEachItemAfter(view, keylet::ownerDir(id), after, hint, limit, f);
|
||||
}
|
||||
|
||||
/** Returns `true` if the directory is empty
|
||||
@param key The key of the directory
|
||||
*/
|
||||
/** Returns `true` if the directory contains no entries.
|
||||
*
|
||||
* An empty `sfIndexes` array on the root page is necessary but not
|
||||
* sufficient: the root is an anchor page and may have an empty index
|
||||
* while `sfIndexNext` still points to a populated subsequent page. Both
|
||||
* conditions — empty `sfIndexes` *and* `sfIndexNext == 0` — must hold
|
||||
* before declaring the directory empty. A missing root SLE is also
|
||||
* treated as empty.
|
||||
*
|
||||
* @param view The read-only ledger view to query.
|
||||
* @param k Keylet of the directory's root page.
|
||||
* @return `true` if the directory has no entries or does not exist;
|
||||
* `false` otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
dirIsEmpty(ReadView const& view, Keylet const& k);
|
||||
|
||||
/** Returns a function that sets the owner on a directory SLE */
|
||||
/** Returns a callback that stamps a new directory page with its owner account.
|
||||
*
|
||||
* The returned `std::function<void(SLE::ref)>` sets `sfOwner = account` on
|
||||
* the newly allocated `ltDIR_NODE` SLE. It is passed as the `describe`
|
||||
* argument to `ApplyView::dirInsert` throughout the codebase (e.g.,
|
||||
* `RippleStateHelpers.cpp`, `PaymentChannelCreate.cpp`) and is invoked only
|
||||
* when `dirInsert` actually allocates a fresh overflow page, keeping the
|
||||
* owning account ID out of the generic insertion logic.
|
||||
*
|
||||
* @param account The `AccountID` to record as `sfOwner` on each new page.
|
||||
* @return A callable suitable for the `describe` parameter of
|
||||
* `ApplyView::dirInsert`.
|
||||
*/
|
||||
[[nodiscard]] std::function<void(SLE::ref)>
|
||||
describeOwnerDir(AccountID const& account);
|
||||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/** @file
|
||||
* Token-delivery helper for IOU and MPT escrow resolution.
|
||||
*
|
||||
* Implements `escrowUnlockApplyHelper`, the single function responsible for
|
||||
* crediting the appropriate account when an IOU or MPT escrow is finished
|
||||
* (`EscrowFinish`) or cancelled (`EscrowCancel`) under `featureTokenEscrow`.
|
||||
* The function is specialised once for `Issue` (IOU trust-line path) and once
|
||||
* for `MPTIssue` (MPToken path); callers reach the correct specialisation via
|
||||
* `std::visit` on the `Asset` variant, with zero runtime dispatch overhead.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -13,6 +23,33 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Credit an account with tokens held in escrow, applying transfer-fee logic.
|
||||
*
|
||||
* Primary template — no body is provided. Only the `Issue` and `MPTIssue`
|
||||
* full specialisations are defined. Callers should invoke via `std::visit`
|
||||
* on an `Asset` variant so the compiler selects the correct specialisation
|
||||
* at compile time.
|
||||
*
|
||||
* @tparam T Asset type; must satisfy `ValidIssueType` (`Issue` or `MPTIssue`).
|
||||
* @param view Mutable ledger view on which state changes are applied.
|
||||
* @param lockedRate Transfer rate snapshotted at escrow creation time.
|
||||
* Pass `kPARITY_RATE` for cancellations (return to sender, no fee).
|
||||
* @param sleDest SLE for the destination account (`receiver`); used for
|
||||
* owner-count and reserve checks when auto-creating a trust line or
|
||||
* MPToken holding object.
|
||||
* @param xrpBalance Pre-fee XRP balance of the destination account; compared
|
||||
* against the incremental reserve required to create a new holding object.
|
||||
* @param amount Escrowed token amount (face value locked at escrow creation).
|
||||
* @param issuer Token issuer.
|
||||
* @param sender Escrow creator / original token sender.
|
||||
* @param receiver Account that will receive the unlocked tokens.
|
||||
* @param createAsset When `true`, auto-creates a trust line or MPToken object
|
||||
* for `receiver` if one does not already exist. Callers set this only
|
||||
* when the transaction submitter is also the beneficiary, preserving
|
||||
* account sovereignty over directory entries.
|
||||
* @param journal Logging sink.
|
||||
* @return `tesSUCCESS` on success, or a `tec` error code on failure.
|
||||
*/
|
||||
template <ValidIssueType T>
|
||||
TER
|
||||
escrowUnlockApplyHelper(
|
||||
@@ -27,6 +64,40 @@ escrowUnlockApplyHelper(
|
||||
bool createAsset,
|
||||
beast::Journal journal);
|
||||
|
||||
/** IOU trust-line specialisation of `escrowUnlockApplyHelper`.
|
||||
*
|
||||
* Delivers IOU tokens from a finished or cancelled escrow to `receiver`,
|
||||
* optionally creating the trust line and applying the snapshotted transfer
|
||||
* fee.
|
||||
*
|
||||
* **Issuer short-circuits.** `sender == issuer` returns `tecINTERNAL` (an
|
||||
* issuer cannot be an escrow originator for their own obligation).
|
||||
* `receiver == issuer` returns `tesSUCCESS` immediately — delivery to the
|
||||
* issuer is a redemption handled by the calling transactor at the balance
|
||||
* level.
|
||||
*
|
||||
* **Trust line creation.** When `createAsset` is `true` and no trust line
|
||||
* exists, one is created with a zero balance and zero limit via `trustCreate`.
|
||||
* The `sfDefaultRipple` flag is inherited from `sleDest`. Reserve is checked
|
||||
* first; insufficient reserve returns `tecNO_LINE_INSUF_RESERVE`. When
|
||||
* `createAsset` is `false` and no line exists, returns `tecNO_LINE`.
|
||||
*
|
||||
* **Transfer fee.** The effective rate is `min(lockedRate, currentRate)`,
|
||||
* protecting the receiver from a rate increase during the escrow lifetime.
|
||||
* The fee is deducted *from* `amount` (not added on top), so `receiver` gets
|
||||
* `amount - fee`. When neither party is the issuer and the rate differs from
|
||||
* `kPARITY_RATE`, the check against the trust-line limit uses `finalAmt`.
|
||||
*
|
||||
* **Limit check.** When `createAsset` is `false`, the post-transfer balance
|
||||
* is compared to `receiver`'s trust-line limit; `tecLIMIT_EXCEEDED` is
|
||||
* returned if the delivery would exceed it. This check is skipped when
|
||||
* `createAsset` is `true` because a freshly created line has a zero limit
|
||||
* and would always fail it spuriously.
|
||||
*
|
||||
* @note This function is reached via `std::visit` on an `Asset` variant in
|
||||
* `EscrowFinish` and `EscrowCancel`. `EscrowCancel` always passes
|
||||
* `kPARITY_RATE` so no fee is charged on the return-to-sender path.
|
||||
*/
|
||||
template <>
|
||||
inline TER
|
||||
escrowUnlockApplyHelper<Issue>(
|
||||
@@ -41,12 +112,6 @@ escrowUnlockApplyHelper<Issue>(
|
||||
bool createAsset,
|
||||
beast::Journal journal)
|
||||
{
|
||||
if (view.rules().enabled(fixTokenEscrowV1_1))
|
||||
{
|
||||
if (!sleDest || sleDest->getType() != ltACCOUNT_ROOT)
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
Issue const& issue = amount.get<Issue>();
|
||||
Keylet const trustLineKey = keylet::line(receiver, issue);
|
||||
bool const recvLow = issuer > receiver;
|
||||
@@ -76,21 +141,21 @@ escrowUnlockApplyHelper<Issue>(
|
||||
initialBalance.get<Issue>().account = noAccount();
|
||||
|
||||
if (TER const ter = trustCreate(
|
||||
view, // payment sandbox
|
||||
recvLow, // is dest low?
|
||||
issuer, // source
|
||||
receiver, // destination
|
||||
trustLineKey.key, // ledger index
|
||||
sleDest, // Account to add to
|
||||
false, // authorize account
|
||||
(sleDest->getFlags() & lsfDefaultRipple) == 0, //
|
||||
false, // freeze trust line
|
||||
false, // deep freeze trust line
|
||||
initialBalance, // zero initial balance
|
||||
Issue(currency, receiver), // limit of zero
|
||||
0, // quality in
|
||||
0, // quality out
|
||||
journal); // journal
|
||||
view,
|
||||
recvLow,
|
||||
issuer,
|
||||
receiver,
|
||||
trustLineKey.key,
|
||||
sleDest,
|
||||
false,
|
||||
(sleDest->getFlags() & lsfDefaultRipple) == 0,
|
||||
false,
|
||||
false,
|
||||
initialBalance,
|
||||
Issue(currency, receiver),
|
||||
0,
|
||||
0,
|
||||
journal);
|
||||
!isTesSuccess(ter))
|
||||
{
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
@@ -103,57 +168,43 @@ escrowUnlockApplyHelper<Issue>(
|
||||
return tecNO_LINE;
|
||||
|
||||
auto const xferRate = transferRate(view, amount);
|
||||
// update if issuer rate is less than locked rate
|
||||
// Cap to the lower of the snapshotted and current rate to protect the receiver.
|
||||
if (xferRate < lockedRate)
|
||||
lockedRate = xferRate;
|
||||
|
||||
// Transfer Rate only applies when:
|
||||
// 1. Issuer is not involved in the transfer (senderIssuer or
|
||||
// receiverIssuer)
|
||||
// 2. The locked rate is different from the parity rate
|
||||
|
||||
// NOTE: Transfer fee in escrow works a bit differently from a normal
|
||||
// payment. In escrow, the fee is deducted from the locked/sending amount,
|
||||
// whereas in a normal payment, the transfer fee is taken on top of the
|
||||
// sending amount.
|
||||
// Fee is deducted from `amount` (not added on top): finalAmt = amount - fee.
|
||||
// No fee when either party is the issuer, or when lockedRate == kPARITY_RATE.
|
||||
auto finalAmt = amount;
|
||||
if ((!senderIssuer && !receiverIssuer) && lockedRate != kPARITY_RATE)
|
||||
{
|
||||
// compute transfer fee, if any
|
||||
auto const xferFee =
|
||||
amount.value() - divideRound(amount, lockedRate, amount.get<Issue>(), true);
|
||||
// compute balance to transfer
|
||||
finalAmt = amount.value() - xferFee;
|
||||
}
|
||||
|
||||
// validate the line limit if the account submitting txn is not the receiver
|
||||
// of the funds
|
||||
// Limit check skipped when createAsset is true (freshly created line has
|
||||
// zero limit and would always fail spuriously).
|
||||
if (!createAsset)
|
||||
{
|
||||
auto const sleRippleState = view.peek(trustLineKey);
|
||||
if (!sleRippleState)
|
||||
return tecINTERNAL; // LCOV_EXCL_LINE
|
||||
|
||||
// if the issuer is the high, then we use the low limit
|
||||
// otherwise we use the high limit
|
||||
// recvLow true → receiver is low side → use sfLowLimit; else sfHighLimit.
|
||||
STAmount const lineLimit =
|
||||
sleRippleState->getFieldAmount(recvLow ? sfLowLimit : sfHighLimit);
|
||||
|
||||
STAmount lineBalance = sleRippleState->getFieldAmount(sfBalance);
|
||||
|
||||
// flip the sign of the line balance if the issuer is not high
|
||||
if (!recvLow)
|
||||
lineBalance.negate();
|
||||
|
||||
// add the final amount to the line balance
|
||||
lineBalance += finalAmt;
|
||||
|
||||
// if the transfer would exceed the line limit return tecLIMIT_EXCEEDED
|
||||
if (lineLimit < lineBalance)
|
||||
return tecLIMIT_EXCEEDED;
|
||||
}
|
||||
|
||||
// if destination is not the issuer then transfer funds
|
||||
if (!receiverIssuer)
|
||||
{
|
||||
auto const ter = directSendNoFee(view, issuer, receiver, finalAmt, true, journal);
|
||||
@@ -163,6 +214,32 @@ escrowUnlockApplyHelper<Issue>(
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
/** MPT specialisation of `escrowUnlockApplyHelper`.
|
||||
*
|
||||
* Delivers MPT tokens from a finished or cancelled escrow to `receiver`,
|
||||
* optionally creating an MPToken holding object and applying the snapshotted
|
||||
* transfer fee.
|
||||
*
|
||||
* **MPToken creation.** When `createAsset` is `true`, `receiver` is not the
|
||||
* issuer, and no MPToken SLE exists for this issuance, one is created via
|
||||
* `createMPToken` and the owner count is incremented. Insufficient reserve
|
||||
* returns `tecINSUFFICIENT_RESERVE`. If no MPToken exists after the creation
|
||||
* attempt (and `receiver` is not the issuer), returns `tecNO_PERMISSION`.
|
||||
*
|
||||
* **Transfer fee.** Identical to the `Issue` path: effective rate is
|
||||
* `min(lockedRate, currentRate)`, fee is deducted *from* `amount`, and no
|
||||
* fee is applied when either party is the issuer or the rate is parity.
|
||||
*
|
||||
* **`fixTokenEscrowV1` bug fix.** The gross amount passed to `unlockEscrowMPT`
|
||||
* (used to reduce `sfOutstandingAmount`) is `amount` when the amendment is
|
||||
* enabled, and `finalAmt` otherwise. Without the fix, the outstanding supply
|
||||
* is only reduced by the net delivered amount, silently retaining the fee
|
||||
* portion; with the fix, the full face value is removed from circulation and
|
||||
* the fee is burned from the outstanding supply.
|
||||
*
|
||||
* @note `EscrowCancel` passes `kPARITY_RATE` so no fee is charged when
|
||||
* tokens are returned to the original sender.
|
||||
*/
|
||||
template <>
|
||||
inline TER
|
||||
escrowUnlockApplyHelper<MPTIssue>(
|
||||
@@ -177,12 +254,6 @@ escrowUnlockApplyHelper<MPTIssue>(
|
||||
bool createAsset,
|
||||
beast::Journal journal)
|
||||
{
|
||||
if (view.rules().enabled(fixTokenEscrowV1_1))
|
||||
{
|
||||
if (!sleDest || sleDest->getType() != ltACCOUNT_ROOT)
|
||||
return tecINTERNAL;
|
||||
}
|
||||
|
||||
bool const senderIssuer = issuer == sender;
|
||||
bool const receiverIssuer = issuer == receiver;
|
||||
|
||||
@@ -201,7 +272,6 @@ escrowUnlockApplyHelper<MPTIssue>(
|
||||
return ter; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
// update owner count.
|
||||
adjustOwnerCount(view, sleDest, 1, journal);
|
||||
}
|
||||
|
||||
@@ -209,25 +279,16 @@ escrowUnlockApplyHelper<MPTIssue>(
|
||||
return tecNO_PERMISSION;
|
||||
|
||||
auto const xferRate = transferRate(view, amount);
|
||||
// update if issuer rate is less than locked rate
|
||||
// Cap to the lower of the snapshotted and current rate to protect the receiver.
|
||||
if (xferRate < lockedRate)
|
||||
lockedRate = xferRate;
|
||||
|
||||
// Transfer Rate only applies when:
|
||||
// 1. Issuer is not involved in the transfer (senderIssuer or
|
||||
// receiverIssuer)
|
||||
// 2. The locked rate is different from the parity rate
|
||||
|
||||
// NOTE: Transfer fee in escrow works a bit differently from a normal
|
||||
// payment. In escrow, the fee is deducted from the locked/sending amount,
|
||||
// whereas in a normal payment, the transfer fee is taken on top of the
|
||||
// sending amount.
|
||||
// Fee is deducted from `amount` (not added on top): finalAmt = amount - fee.
|
||||
// No fee when either party is the issuer, or when lockedRate == kPARITY_RATE.
|
||||
auto finalAmt = amount;
|
||||
if ((!senderIssuer && !receiverIssuer) && lockedRate != kPARITY_RATE)
|
||||
{
|
||||
// compute transfer fee, if any
|
||||
auto const xferFee = amount.value() - divideRound(amount, lockedRate, amount.asset(), true);
|
||||
// compute balance to transfer
|
||||
finalAmt = amount.value() - xferFee;
|
||||
}
|
||||
return unlockEscrowMPT(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,16 @@
|
||||
/** @file
|
||||
* MPT-specific ledger helper declarations.
|
||||
*
|
||||
* Declares the MPT counterpart to `RippleStateHelpers.h`. The asset-agnostic
|
||||
* `TokenHelpers.h` dispatchers route `MPTIssue`-typed calls here via
|
||||
* `std::visit` on the `Asset` variant. In addition to the functions that
|
||||
* mirror IOU trust-line semantics (freeze, transfer rate, holding lifecycle,
|
||||
* authorization), this header exposes operations with no IOU equivalent:
|
||||
* escrow accounting, DEX permission gating, supply-overflow arithmetic, and
|
||||
* the two-phase authorization protocol specific to MPT.
|
||||
*
|
||||
* @see RippleStateHelpers.h, TokenHelpers.h
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -20,15 +33,65 @@ namespace xrpl {
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Check whether an entire MPT issuance is globally frozen.
|
||||
*
|
||||
* Reads the `MPTokenIssuance` SLE and tests `lsfMPTLocked`. A missing
|
||||
* issuance SLE is treated as unfrozen.
|
||||
*
|
||||
* @param view The ledger state to query.
|
||||
* @param mptIssue The MPT issuance to check.
|
||||
* @return `true` if `lsfMPTLocked` is set on the issuance; `false` otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isGlobalFrozen(ReadView const& view, MPTIssue const& mptIssue);
|
||||
|
||||
/** Check whether a specific account's MPToken holding is individually frozen.
|
||||
*
|
||||
* Reads the per-holder `MPToken` SLE and tests `lsfMPTLocked`. Returns
|
||||
* `false` if no `MPToken` SLE exists for the account (i.e., the account
|
||||
* holds no balance for this issuance).
|
||||
*
|
||||
* @param view The ledger state to query.
|
||||
* @param account The account whose holding is checked.
|
||||
* @param mptIssue The MPT issuance to check against.
|
||||
* @return `true` if the account's `MPToken` carries `lsfMPTLocked`;
|
||||
* `false` otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isIndividualFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);
|
||||
|
||||
/** Check whether an account's access to an MPT issuance is frozen by any tier.
|
||||
*
|
||||
* Applies three checks in order: global issuance lock (`isGlobalFrozen`),
|
||||
* per-account holding lock (`isIndividualFrozen`), and vault pseudo-account
|
||||
* freeze (`isVaultPseudoAccountFrozen`). Short-circuits on the first match.
|
||||
*
|
||||
* @param view The ledger state to query.
|
||||
* @param account The account to check.
|
||||
* @param mptIssue The MPT issuance to check against.
|
||||
* @param depth Recursion depth guard forwarded to `isVaultPseudoAccountFrozen`;
|
||||
* bounds pathological nested-vault configurations (currently unreachable
|
||||
* in practice, but defended against up to `maxAssetCheckDepth`).
|
||||
* @return `true` if any freeze tier applies; `false` otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue, int depth = 0);
|
||||
|
||||
/** Check whether any account in a set is frozen for an MPT issuance.
|
||||
*
|
||||
* Sequences checks across separate passes to minimize cost: the global freeze
|
||||
* is tested once and short-circuits immediately; individual per-account locks
|
||||
* are checked for every account before the more expensive vault
|
||||
* pseudo-account recursion begins.
|
||||
*
|
||||
* @param view The ledger state to query.
|
||||
* @param accounts The set of accounts to check.
|
||||
* @param mptIssue The MPT issuance to check against.
|
||||
* @param depth Recursion depth guard forwarded to `isVaultPseudoAccountFrozen`.
|
||||
* @return `true` if the global freeze is set, or if any account carries an
|
||||
* individual freeze, or if any account is a frozen vault pseudo-account;
|
||||
* `false` otherwise.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isAnyFrozen(
|
||||
ReadView const& view,
|
||||
@@ -42,10 +105,18 @@ isAnyFrozen(
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Returns MPT transfer fee as Rate. Rate specifies
|
||||
* the fee as fractions of 1 billion. For example, 1% transfer rate
|
||||
* is represented as 1,010,000,000.
|
||||
* @param issuanceID MPTokenIssuanceID of MPTTokenIssuance object
|
||||
/** Convert the `sfTransferFee` field of an MPT issuance to the XRPL `Rate` type.
|
||||
*
|
||||
* `sfTransferFee` is a `uint16` in the range 0–50,000 representing 0–50%
|
||||
* (units of 0.001%). The encoding maps to `Rate` via
|
||||
* `1,000,000,000 + (10,000 × fee)`, so a 50,000 field value becomes
|
||||
* `1,500,000,000` (50% surcharge over the gross). When `sfTransferFee` is
|
||||
* absent, `parityRate` (1,000,000,000 — no fee) is returned.
|
||||
*
|
||||
* @param view The ledger state to query.
|
||||
* @param issuanceID The `MPTokenIssuanceID` of the issuance.
|
||||
* @return The transfer rate as a `Rate` value; `parityRate` when no fee is
|
||||
* configured or the issuance SLE is absent.
|
||||
*/
|
||||
[[nodiscard]] Rate
|
||||
transferRate(ReadView const& view, MPTID const& issuanceID);
|
||||
@@ -56,6 +127,18 @@ transferRate(ReadView const& view, MPTID const& issuanceID);
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Read-only pre-check: verify that an independent holding can be created.
|
||||
*
|
||||
* Validates two preconditions before `addEmptyHolding` mutates the ledger:
|
||||
* the `MPTokenIssuance` must exist, and it must carry `lsfMPTCanTransfer`.
|
||||
* Tokens without `lsfMPTCanTransfer` can only move directly between the
|
||||
* issuer and counterparties, making independent holdings meaningless.
|
||||
*
|
||||
* @param view The ledger state to query.
|
||||
* @param mptIssue The MPT issuance the caller wants to hold.
|
||||
* @return `tesSUCCESS`, `tecOBJECT_NOT_FOUND` if the issuance SLE is absent,
|
||||
* or `tecNO_AUTH` if `lsfMPTCanTransfer` is not set.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
canAddHolding(ReadView const& view, MPTIssue const& mptIssue);
|
||||
|
||||
@@ -65,6 +148,33 @@ canAddHolding(ReadView const& view, MPTIssue const& mptIssue);
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Core MPToken SLE lifecycle function — create, delete, or toggle authorization.
|
||||
*
|
||||
* Behavior depends on `holderID`:
|
||||
* - `holderID` absent (`nullopt`): `account` is the holder. Without
|
||||
* `tfMPTUnauthorize`, a new zero-balance `MPToken` SLE is created and
|
||||
* inserted into the owner directory; the XRP reserve is enforced when
|
||||
* `ownerCount >= 2` (same policy as trust lines). With `tfMPTUnauthorize`,
|
||||
* the existing SLE is erased and the owner count decremented.
|
||||
* - `holderID` set: `account` must be the issuance's issuer. The function
|
||||
* toggles `lsfMPTAuthorized` on the holder's existing `MPToken` SLE.
|
||||
*
|
||||
* @param view The mutable ledger state.
|
||||
* @param priorBalance XRP balance before this transaction; used only for the
|
||||
* reserve check when creating a new holding (`holderID` absent and
|
||||
* `tfMPTUnauthorize` not set).
|
||||
* @param mptIssuanceID The issuance being authorized or deauthorized.
|
||||
* @param account Submitting account: the holder (when `holderID` is absent)
|
||||
* or the issuer (when `holderID` is set).
|
||||
* @param journal Logging sink.
|
||||
* @param flags Transaction flags; `tfMPTUnauthorize` selects the
|
||||
* delete/deauthorize path.
|
||||
* @param holderID When set, `account` is the issuer and this is the holder
|
||||
* whose `lsfMPTAuthorized` flag is toggled.
|
||||
* @return `tesSUCCESS`, `tecINSUFFICIENT_RESERVE` if reserves are too low,
|
||||
* `tecDUPLICATE` if the holding already exists, or a `tef` code on
|
||||
* invariant violations.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
authorizeMPToken(
|
||||
ApplyView& view,
|
||||
@@ -75,12 +185,31 @@ authorizeMPToken(
|
||||
std::uint32_t flags = 0,
|
||||
std::optional<AccountID> holderID = std::nullopt);
|
||||
|
||||
/** Check if the account lacks required authorization for MPT.
|
||||
/** Preclaim (read-only) authorization check for an MPT holding.
|
||||
*
|
||||
* requireAuth check is recursive for MPT shares in a vault, descending to
|
||||
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
|
||||
* purely defensive, as we currently do not allow such vaults to be created.
|
||||
* WeakAuth intentionally allows missing MPTokens under MPToken V2.
|
||||
* Issuers are always authorized. When `featureSingleAssetVault` is active,
|
||||
* vault and `LoanBroker` pseudo-accounts are implicitly authorized, and the
|
||||
* check recurses into the vault's underlying asset (bounded by `depth`
|
||||
* vs. `kMAX_ASSET_CHECK_DEPTH`). Domain-based authorization via
|
||||
* `credentials::validDomain` takes precedence over `lsfMPTAuthorized` when
|
||||
* `sfDomainID` is present on the issuance — a passing domain check succeeds
|
||||
* even if no `MPToken` SLE exists.
|
||||
*
|
||||
* `WeakAuth` intentionally permits a missing `MPToken` SLE; used in MPToken
|
||||
* V2 flows where the SLE is created on demand during apply.
|
||||
*
|
||||
* @note The recursion through vault assets is purely defensive; the ledger
|
||||
* does not currently permit nested-vault MPT configurations.
|
||||
* @param view The ledger state to query (read-only; called in preclaim).
|
||||
* @param mptIssue The MPT issuance being accessed.
|
||||
* @param account The account requesting access.
|
||||
* @param authType Controls leniency toward missing `MPToken` SLEs;
|
||||
* `WeakAuth` allows a missing SLE, `StrongAuth`/`Legacy` require it.
|
||||
* @param depth Current recursion depth; guards against theoretical infinite
|
||||
* recursion through nested vault configurations.
|
||||
* @return `tesSUCCESS` if authorized, `tecOBJECT_NOT_FOUND` if the issuance
|
||||
* is absent, `tecNO_AUTH` if authorization fails, or `tecEXPIRED` if
|
||||
* domain credentials have expired.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
requireAuth(
|
||||
@@ -90,11 +219,25 @@ requireAuth(
|
||||
AuthType authType = AuthType::Legacy,
|
||||
int depth = 0);
|
||||
|
||||
/** Enforce account has MPToken to match its authorization.
|
||||
/** Enforce account has MPToken to match its authorization (doApply phase).
|
||||
*
|
||||
* Called from doApply - it will check for expired (and delete if found any)
|
||||
* credentials matching DomainID set in MPTokenIssuance. Must be called if
|
||||
* requireAuth(...MPTIssue...) returned tesSUCCESS or tecEXPIRED in preclaim.
|
||||
* Must be called when `requireAuth` returned `tesSUCCESS` or `tecEXPIRED`
|
||||
* during preclaim. Re-checks authorization and, if a `sfDomainID` is set on
|
||||
* the issuance, runs `verifyValidDomain` (which deletes expired credentials
|
||||
* as a side effect). When domain authorization succeeds but the account has
|
||||
* no `MPToken` SLE, one is created on the fly using `priorBalance` for the
|
||||
* XRP reserve check.
|
||||
*
|
||||
* @note Must not be called for the issuer account.
|
||||
* @param view The mutable ledger state (called in doApply).
|
||||
* @param mptIssuanceID The issuance being accessed.
|
||||
* @param account The holder account; must not be the issuer.
|
||||
* @param priorBalance XRP balance before this transaction; used when lazily
|
||||
* allocating a new `MPToken` SLE for domain-authorized holders.
|
||||
* @param j Logging sink.
|
||||
* @return `tesSUCCESS`, `tecNO_AUTH` if not authorized, `tecEXPIRED` if
|
||||
* credentials have expired, or `tecINSUFFICIENT_RESERVE` if the reserve
|
||||
* check fails during on-demand SLE creation.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
enforceMPTokenAuthorization(
|
||||
@@ -104,9 +247,20 @@ enforceMPTokenAuthorization(
|
||||
XRPAmount const& priorBalance,
|
||||
beast::Journal j);
|
||||
|
||||
/** Check if the destination account is allowed
|
||||
* to receive MPT. Return tecNO_AUTH if it doesn't
|
||||
* and tesSUCCESS otherwise.
|
||||
/** Check whether a transfer between two accounts is permitted by the issuance.
|
||||
*
|
||||
* When `lsfMPTCanTransfer` is absent, third-party transfers are blocked.
|
||||
* Transfers where either `from` or `to` is the issuer are always allowed,
|
||||
* mirroring the IOU trust-line policy that lets issuers send and receive
|
||||
* their own tokens unconditionally.
|
||||
*
|
||||
* @param view The ledger state to query.
|
||||
* @param mptIssue The MPT issuance involved in the transfer.
|
||||
* @param from The sending account.
|
||||
* @param to The receiving account.
|
||||
* @return `tesSUCCESS` if the transfer is permitted, `tecOBJECT_NOT_FOUND`
|
||||
* if the issuance SLE is absent, or `tecNO_AUTH` if `lsfMPTCanTransfer`
|
||||
* is unset and neither endpoint is the issuer.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
canTransfer(
|
||||
@@ -115,8 +269,16 @@ canTransfer(
|
||||
AccountID const& from,
|
||||
AccountID const& to);
|
||||
|
||||
/** Check if Asset can be traded on DEX. return tecNO_PERMISSION
|
||||
* if it doesn't and tesSUCCESS otherwise.
|
||||
/** Check whether an asset may be traded on the DEX.
|
||||
*
|
||||
* Dispatches via `asset.visit`: XRP and IOU assets always succeed; for MPT,
|
||||
* reads the issuance SLE and checks `lsfMPTCanTrade`.
|
||||
*
|
||||
* @param view The ledger state to query.
|
||||
* @param asset The asset to check; non-MPT assets always pass.
|
||||
* @return `tesSUCCESS` if trading is permitted, `tecOBJECT_NOT_FOUND` if
|
||||
* the MPT issuance SLE is absent, or `tecNO_PERMISSION` if
|
||||
* `lsfMPTCanTrade` is not set.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
canTrade(ReadView const& view, Asset const& asset);
|
||||
@@ -127,6 +289,24 @@ canTrade(ReadView const& view, Asset const& asset);
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Create a zero-balance `MPToken` holding for `accountID`.
|
||||
*
|
||||
* Short-circuits to `tesSUCCESS` when the caller is the issuer — issuers
|
||||
* never hold a `MPToken` SLE for their own issuance. For all other accounts,
|
||||
* delegates to `authorizeMPToken`, which enforces the XRP reserve requirement
|
||||
* and inserts the SLE into the owner directory. Returns `tefINTERNAL` if the
|
||||
* issuance SLE is missing or globally locked (invariant violations).
|
||||
*
|
||||
* @param view The mutable ledger state.
|
||||
* @param accountID The account requesting the holding.
|
||||
* @param priorBalance XRP balance before this transaction; forwarded to
|
||||
* `authorizeMPToken` for the reserve check.
|
||||
* @param mptIssue The MPT issuance to hold.
|
||||
* @param journal Logging sink.
|
||||
* @return `tesSUCCESS`, `tecDUPLICATE` if a holding already exists,
|
||||
* `tecINSUFFICIENT_RESERVE` if reserves are too low, or `tefINTERNAL`
|
||||
* on issuance-state invariant violations.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
addEmptyHolding(
|
||||
ApplyView& view,
|
||||
@@ -135,6 +315,23 @@ addEmptyHolding(
|
||||
MPTIssue const& mptIssue,
|
||||
beast::Journal journal);
|
||||
|
||||
/** Delete a zero-balance `MPToken` holding.
|
||||
*
|
||||
* Requires `sfMPTAmount` to be zero and, when `fixCleanup3_1_3` is enabled,
|
||||
* `sfLockedAmount` to be zero as well; returns `tecHAS_OBLIGATIONS` otherwise.
|
||||
* When `accountID` is the issuer and no `MPToken` SLE exists, returns
|
||||
* `tesSUCCESS` immediately — the normal issuer state. Otherwise delegates to
|
||||
* `authorizeMPToken` with `tfMPTUnauthorize` to erase the SLE and decrement
|
||||
* the owner count.
|
||||
*
|
||||
* @param view The mutable ledger state.
|
||||
* @param accountID The account whose holding is being removed.
|
||||
* @param mptIssue The MPT issuance.
|
||||
* @param journal Logging sink.
|
||||
* @return `tesSUCCESS`, `tecOBJECT_NOT_FOUND` if no holding exists (and
|
||||
* caller is not the issuer), or `tecHAS_OBLIGATIONS` if the holding
|
||||
* carries a non-zero balance or locked amount.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
removeEmptyHolding(
|
||||
ApplyView& view,
|
||||
@@ -148,6 +345,22 @@ removeEmptyHolding(
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Move MPT funds from a holder's spendable balance into escrow.
|
||||
*
|
||||
* Decrements `sfMPTAmount` and increments `sfLockedAmount` on the sender's
|
||||
* `MPToken` SLE, then increments `sfLockedAmount` on the `MPTokenIssuance`
|
||||
* SLE. `sfOutstandingAmount` on the issuance is deliberately left unchanged —
|
||||
* escrowed tokens remain outstanding until the escrow completes and the
|
||||
* recipient actually receives them. All arithmetic is guarded by
|
||||
* `canSubtract`/`canAdd`.
|
||||
*
|
||||
* @param view The mutable ledger state.
|
||||
* @param uGrantorID The account placing tokens in escrow; must not be the issuer.
|
||||
* @param saAmount The MPT amount to lock; must be a valid `MPTIssue` amount.
|
||||
* @param j Logging sink.
|
||||
* @return `tesSUCCESS`, or a `tec`/`tef` error if the issuance or `MPToken`
|
||||
* SLE is missing, the sender is the issuer, or an arithmetic guard fires.
|
||||
*/
|
||||
TER
|
||||
lockEscrowMPT(
|
||||
ApplyView& view,
|
||||
@@ -155,6 +368,28 @@ lockEscrowMPT(
|
||||
STAmount const& saAmount,
|
||||
beast::Journal j);
|
||||
|
||||
/** Release MPT funds from escrow and credit the recipient.
|
||||
*
|
||||
* Decrements `sfLockedAmount` on both the sender's `MPToken` SLE and the
|
||||
* `MPTokenIssuance` SLE by `grossAmount`. Then, depending on the receiver:
|
||||
* - Receiver is a third party: `sfMPTAmount` on the receiver's `MPToken` is
|
||||
* incremented by `netAmount`.
|
||||
* - Receiver is the issuer: `sfOutstandingAmount` on the issuance is
|
||||
* decremented by `netAmount` — tokens return to the issuer and retire.
|
||||
* When `fixTokenEscrowV1` is enabled and `grossAmount > netAmount`, the fee
|
||||
* difference is additionally subtracted from `sfOutstandingAmount` because
|
||||
* the fee tokens are effectively burned. All arithmetic is guarded by
|
||||
* `canSubtract`/`canAdd`.
|
||||
*
|
||||
* @param view The mutable ledger state.
|
||||
* @param uGrantorID The escrow grantor; must not be the issuer.
|
||||
* @param uGranteeID The escrow grantee (may be the issuer).
|
||||
* @param netAmount The MPT amount credited to the receiver after fees.
|
||||
* @param grossAmount The MPT amount unlocked from escrow (>= `netAmount`).
|
||||
* @param j Logging sink.
|
||||
* @return `tesSUCCESS`, or a `tec`/`tef` error on missing SLEs or
|
||||
* arithmetic guard failure.
|
||||
*/
|
||||
TER
|
||||
unlockEscrowMPT(
|
||||
ApplyView& view,
|
||||
@@ -164,6 +399,18 @@ unlockEscrowMPT(
|
||||
STAmount const& grossAmount,
|
||||
beast::Journal j);
|
||||
|
||||
/** Low-level primitive: insert a new `MPToken` SLE and link it into the owner directory.
|
||||
*
|
||||
* Inserts the SLE unconditionally without checking for duplicates, enforcing
|
||||
* reserves, or verifying issuance validity. Callers must perform those checks
|
||||
* before invoking this function.
|
||||
*
|
||||
* @param view The mutable ledger state.
|
||||
* @param mptIssuanceID The issuance the token belongs to.
|
||||
* @param account The account that will own the `MPToken`.
|
||||
* @param flags Initial `sfFlags` value for the new SLE.
|
||||
* @return `tesSUCCESS`, or `tecDIR_FULL` if the owner directory is full.
|
||||
*/
|
||||
TER
|
||||
createMPToken(
|
||||
ApplyView& view,
|
||||
@@ -171,6 +418,21 @@ createMPToken(
|
||||
AccountID const& account,
|
||||
std::uint32_t const flags);
|
||||
|
||||
/** Idempotently ensure a `MPToken` holding exists for `holder`.
|
||||
*
|
||||
* Succeeds immediately if `holder` is the issuer or if the `MPToken` SLE
|
||||
* already exists. Otherwise calls `createMPToken` and increments the owner
|
||||
* count. Suitable for apply-phase callers that need to auto-create a holding
|
||||
* without the full reserve and issuance validity checks performed by
|
||||
* `addEmptyHolding`.
|
||||
*
|
||||
* @param view The mutable ledger state.
|
||||
* @param mptIssue The MPT issuance the holder will hold.
|
||||
* @param holder The account to receive the holding.
|
||||
* @param j Logging sink.
|
||||
* @return `tesSUCCESS`, `tecDIR_FULL` if the owner directory is full, or
|
||||
* `tecINTERNAL` if the holder's account SLE is missing.
|
||||
*/
|
||||
TER
|
||||
checkCreateMPT(
|
||||
xrpl::ApplyView& view,
|
||||
@@ -184,25 +446,62 @@ checkCreateMPT(
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// MaximumAmount doesn't exceed 2**63-1
|
||||
/** Return the configured supply cap for an MPT issuance.
|
||||
*
|
||||
* Returns `sfMaximumAmount` when present, or `kMAX_MP_TOKEN_AMOUNT` (2^63−1)
|
||||
* when the field is absent, representing an uncapped issuance. The result is
|
||||
* always non-negative and fits in a `std::int64_t`.
|
||||
*
|
||||
* @param sleIssuance The `MPTokenIssuance` SLE to query.
|
||||
* @return The maximum allowed outstanding amount.
|
||||
*/
|
||||
std::int64_t
|
||||
maxMPTAmount(SLE const& sleIssuance);
|
||||
|
||||
// OutstandingAmount may overflow and available amount might be negative.
|
||||
// But available amount is always <= |MaximumAmount - OutstandingAmount|.
|
||||
/** Compute remaining issuance headroom from a pre-read SLE.
|
||||
*
|
||||
* Returns `maxMPTAmount(sleIssuance) - sfOutstandingAmount`. May transiently
|
||||
* be negative when the payment engine is processing a path step that
|
||||
* temporarily exceeds `MaximumAmount` under `AllowMPTOverflow::Yes`.
|
||||
*
|
||||
* @param sleIssuance The `MPTokenIssuance` SLE to query.
|
||||
* @return Headroom as a signed 64-bit integer; may be negative.
|
||||
*/
|
||||
std::int64_t
|
||||
availableMPTAmount(SLE const& sleIssuance);
|
||||
|
||||
/** Compute remaining issuance headroom by reading the SLE from the view.
|
||||
*
|
||||
* Convenience overload that performs the SLE lookup. Throws
|
||||
* `std::runtime_error` if the issuance SLE is absent — a missing issuance at
|
||||
* this call site indicates a ledger consistency failure rather than a user
|
||||
* error.
|
||||
*
|
||||
* @param view The ledger state to query.
|
||||
* @param mptID The `MPTID` of the issuance.
|
||||
* @return Headroom as a signed 64-bit integer; may be negative.
|
||||
* @throws std::runtime_error if the `MPTokenIssuance` SLE is absent.
|
||||
*/
|
||||
std::int64_t
|
||||
availableMPTAmount(ReadView const& view, MPTID const& mptID);
|
||||
|
||||
/** Checks for two types of OutstandingAmount overflow during a send operation.
|
||||
* 1. **Direct directSendNoFee (Overflow: No):** A true overflow check when
|
||||
* `OutstandingAmount > MaximumAmount`. This threshold is used for direct
|
||||
* directSendNoFee transactions that bypass the payment engine.
|
||||
* 2. **accountSend & Payment Engine (Overflow: Yes):** A temporary overflow
|
||||
* check when `OutstandingAmount > UINT64_MAX`. This higher threshold is used
|
||||
* for `accountSend` and payments processed via the payment engine.
|
||||
/** Check whether crediting `sendAmount` would overflow the outstanding supply.
|
||||
*
|
||||
* Two distinct overflow thresholds are applied based on `allowOverflow`:
|
||||
* 1. **`AllowMPTOverflow::No` (direct send):** Enforces the strict cap
|
||||
* `OutstandingAmount + sendAmount ≤ MaximumAmount`. Used by
|
||||
* `directSendNoFee` transactions that bypass the payment engine.
|
||||
* 2. **`AllowMPTOverflow::Yes` (payment engine):** Raises the effective
|
||||
* ceiling to `UINT64_MAX` to allow transient in-flight values that exceed
|
||||
* `MaximumAmount` during path routing. A matching redemption step in the
|
||||
* same transaction collapses the overshoot before settlement.
|
||||
*
|
||||
* @param sendAmount The proposed additional issuance; must be non-negative.
|
||||
* @param outstandingAmount Current `sfOutstandingAmount` from the issuance SLE.
|
||||
* @param maximumAmount The configured cap (`sfMaximumAmount` or
|
||||
* `kMAX_MP_TOKEN_AMOUNT`).
|
||||
* @param allowOverflow Selects which ceiling to apply.
|
||||
* @return `true` if adding `sendAmount` would exceed the applicable limit.
|
||||
*/
|
||||
bool
|
||||
isMPTOverflow(
|
||||
@@ -211,18 +510,33 @@ isMPTOverflow(
|
||||
std::int64_t maximumAmount,
|
||||
AllowMPTOverflow allowOverflow);
|
||||
|
||||
/**
|
||||
* Determine funds available for an issuer to sell in an issuer owned offer.
|
||||
* Issuing step, which could be either MPTEndPointStep last step or BookStep's
|
||||
* TakerPays may overflow OutstandingAmount. Redeeming step, in BookStep's
|
||||
* TakerGets redeems the offer's owner funds, essentially balancing out
|
||||
* the overflow, unless the offer's owner is the issuer.
|
||||
/** Determine funds available for an issuer to sell in an issuer-owned DEX offer.
|
||||
*
|
||||
* During an issuing step (outbound from the issuer), the issuer's
|
||||
* "available" balance is the remaining issuance headroom (`availableMPTAmount`)
|
||||
* adjusted by `balanceHookSelfIssueMPT` to account for any amount already
|
||||
* sold within the same payment. Without this hook, offer-crossing could
|
||||
* allow the issuer to exceed `sfMaximumAmount` across parallel paths in the
|
||||
* same transaction.
|
||||
*
|
||||
* @param view The ledger state to query.
|
||||
* @param issue The MPT issuance for which to compute issuer funds.
|
||||
* @return The effective amount the issuer can sell; zero if the issuance SLE
|
||||
* is absent.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
issuerFundsToSelfIssue(ReadView const& view, MPTIssue const& issue);
|
||||
|
||||
/** Facilitate tracking of MPT sold by an issuer owning MPT sell offer.
|
||||
* See ApplyView::issuerSelfDebitHookMPT().
|
||||
/** Track MPT sold by an issuer that owns an MPT sell offer.
|
||||
*
|
||||
* Records the cumulative amount sold during the current payment step so that
|
||||
* subsequent calls to `issuerFundsToSelfIssue` return a correctly reduced
|
||||
* available balance. Delegates to `ApplyView::issuerSelfDebitHookMPT` after
|
||||
* computing the current issuance headroom.
|
||||
*
|
||||
* @param view The mutable ledger state.
|
||||
* @param issue The MPT issuance being sold.
|
||||
* @param amount The additional amount sold in this step.
|
||||
*/
|
||||
void
|
||||
issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amount);
|
||||
@@ -233,9 +547,26 @@ issuerSelfDebitHookMPT(ApplyView& view, MPTIssue const& issue, std::uint64_t amo
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/* Return true if a transaction is allowed for the specified MPT/account. The
|
||||
* function checks MPTokenIssuance and MPToken objects flags to determine if the
|
||||
* transaction is allowed.
|
||||
/** Comprehensive MPT transaction permission check for DEX and payment types.
|
||||
*
|
||||
* Verifies in order: the issuer account exists, the `MPTokenIssuance` SLE
|
||||
* exists, the issuance is not globally locked (`lsfMPTLocked`), the
|
||||
* `lsfMPTCanTrade` flag is set, and — for non-issuer accounts — that
|
||||
* `lsfMPTCanTransfer` is set and the account's own `MPToken` is not
|
||||
* individually locked. A missing `MPToken` SLE for a non-issuer is treated
|
||||
* as passing: some transaction types create the `MPToken` on demand and
|
||||
* perform their own missing-token checks.
|
||||
*
|
||||
* @note Must not be called with `txType == ttPAYMENT`; use the payment-engine
|
||||
* path's own checks for payments.
|
||||
* @param v The ledger state to query.
|
||||
* @param tx The transaction type being gated.
|
||||
* @param asset The asset involved; non-MPT assets always succeed.
|
||||
* @param accountID The account initiating the transaction.
|
||||
* @return `tesSUCCESS`, `tecOBJECT_NOT_FOUND` if the issuance is absent,
|
||||
* `tecNO_ISSUER` if the issuer account is gone, `tecLOCKED` if the
|
||||
* issuance or account is frozen, or `tecNO_PERMISSION` if trading or
|
||||
* transfer is not permitted.
|
||||
*/
|
||||
TER
|
||||
checkMPTTxAllowed(ReadView const& v, TxType tx, Asset const& asset, AccountID const& accountID);
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/**
|
||||
* @file NFTokenHelpers.h
|
||||
* @brief Core helpers for NFT paged-directory and offer management.
|
||||
*
|
||||
* Declares all mutable and read-only operations on the NFToken paged-directory
|
||||
* structure and offer queues. Every transaction that touches an NFToken —
|
||||
* minting, burning, transferring, or creating/cancelling offers — calls these
|
||||
* helpers rather than manipulating ledger state directly.
|
||||
*
|
||||
* @note NFTs are packed into doubly-linked `ltNFTOKEN_PAGE` SLEs, each
|
||||
* holding up to `kDIR_MAX_TOKENS_PER_PAGE` (32) tokens sorted by
|
||||
* `compareTokens()`. Tokens sharing the same low-96-bit masked value
|
||||
* (issuer + taxon) are *equivalent* and must be collocated on the same
|
||||
* page. Page key invariant: every token's low 96 bits are strictly less
|
||||
* than the low 96 bits of its enclosing page key.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -13,18 +30,48 @@
|
||||
namespace xrpl::nft {
|
||||
|
||||
/** Delete up to a specified number of offers from the specified token offer
|
||||
* directory. */
|
||||
* directory.
|
||||
*
|
||||
* Iterates the directory page-by-page, deleting offers in reverse index order
|
||||
* within each page. Reverse iteration is required because `sfIndexes` is
|
||||
* vector-backed and forward deletion would corrupt the remaining indices.
|
||||
* Stops as soon as `maxDeletableOffers` offers have been removed.
|
||||
*
|
||||
* @param view The apply view to mutate.
|
||||
* @param directory Keylet of the NFT buy or sell offer directory to drain.
|
||||
* @param maxDeletableOffers Maximum number of offers to remove in this call.
|
||||
* @return The number of offers actually deleted.
|
||||
* @note Returns 0 immediately if `maxDeletableOffers` is 0. Used by
|
||||
* `NFTokenBurn` to drain open offers within the per-transaction
|
||||
* deletion cap (`maxDeletableTokenOfferEntries`).
|
||||
*/
|
||||
std::size_t
|
||||
removeTokenOffersWithLimit(
|
||||
ApplyView& view,
|
||||
Keylet const& directory,
|
||||
std::size_t maxDeletableOffers);
|
||||
|
||||
/** Finds the specified token in the owner's token directory. */
|
||||
/** Finds the specified token in the owner's token directory.
|
||||
*
|
||||
* Read-only traversal: locates the `ltNFTOKEN_PAGE` candidate via `succ()`
|
||||
* and searches the page's `sfNFTokens` array for a matching `sfNFTokenID`.
|
||||
*
|
||||
* @param view The read-only view to query.
|
||||
* @param owner The account whose NFT directory is searched.
|
||||
* @param nftokenID The 256-bit NFT identifier to look up.
|
||||
* @return The matching token `STObject`, or `std::nullopt` if not found.
|
||||
* @see findTokenAndPage for the mutable overload that also returns the page.
|
||||
*/
|
||||
std::optional<STObject>
|
||||
findToken(ReadView const& view, AccountID const& owner, uint256 const& nftokenID);
|
||||
|
||||
/** Finds the token in the owner's token directory. Returns token and page. */
|
||||
/** Token and its containing page, returned by `findTokenAndPage()`.
|
||||
*
|
||||
* Bundles the located token `STObject` with the mutable `shared_ptr<SLE>`
|
||||
* page so callers can modify the token in place without a second ledger
|
||||
* traversal. The page pointer must be used exclusively on the same
|
||||
* `ApplyView` that produced it.
|
||||
*/
|
||||
struct TokenAndPage
|
||||
{
|
||||
STObject token;
|
||||
@@ -35,17 +82,81 @@ struct TokenAndPage
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/** Finds the token in the owner's token directory and returns it with its page.
|
||||
*
|
||||
* Mutable traversal via `ApplyView::peek()`. Returns both the token
|
||||
* `STObject` and the `shared_ptr<SLE>` page so that callers such as
|
||||
* `NFTokenAcceptOffer` can pass the page directly to `removeToken()`,
|
||||
* avoiding a redundant page lookup.
|
||||
*
|
||||
* @param view The apply view to query (mutable; uses `peek()`).
|
||||
* @param owner The account whose NFT directory is searched.
|
||||
* @param nftokenID The 256-bit NFT identifier to look up.
|
||||
* @return A `TokenAndPage` containing the token and its page, or
|
||||
* `std::nullopt` if the token is not found.
|
||||
* @see findToken for the read-only alternative that returns only the token.
|
||||
*/
|
||||
std::optional<TokenAndPage>
|
||||
findTokenAndPage(ApplyView& view, AccountID const& owner, uint256 const& nftokenID);
|
||||
|
||||
/** Insert the token in the owner's token directory. */
|
||||
/** Insert the token in the owner's token directory.
|
||||
*
|
||||
* Locates or creates the appropriate `ltNFTOKEN_PAGE` via `getPageForToken()`.
|
||||
* If the target page is full, it is split to make room; each split increments
|
||||
* the owner's reserve count. Tokens are kept sorted within a page by
|
||||
* `compareTokens()` (low 96-bit key first, full ID as tiebreaker).
|
||||
*
|
||||
* @param view The apply view to mutate.
|
||||
* @param owner The account that will own the token.
|
||||
* @param nft The token `STObject` to insert; must contain `sfNFTokenID`.
|
||||
* @return `tesSUCCESS` on success, or `tecNO_SUITABLE_NFTOKEN_PAGE` if the
|
||||
* target page is entirely filled with equivalent tokens (same low 96-bit
|
||||
* key) and no split is possible.
|
||||
*/
|
||||
TER
|
||||
insertToken(ApplyView& view, AccountID owner, STObject&& nft);
|
||||
|
||||
/** Remove the token from the owner's token directory. */
|
||||
/** Remove the token from the owner's token directory.
|
||||
*
|
||||
* Page-discovery overload: locates the containing `ltNFTOKEN_PAGE` via
|
||||
* `succ()` and then delegates to the two-argument form. Use this when
|
||||
* the caller does not already hold a page reference.
|
||||
*
|
||||
* After erasure, attempts to merge the affected page with its neighbours;
|
||||
* each successful merge credits one reserve. If the page becomes empty it
|
||||
* is unlinked and erased.
|
||||
*
|
||||
* @param view The apply view to mutate.
|
||||
* @param owner The account that currently holds the token.
|
||||
* @param nftokenID The 256-bit NFT identifier to remove.
|
||||
* @return `tesSUCCESS`, or `tecNO_ENTRY` if the page or token cannot be
|
||||
* found.
|
||||
* @see removeToken(ApplyView&, AccountID const&, uint256 const&, shared_ptr<SLE> const&)
|
||||
* for the overload that skips the page lookup.
|
||||
*/
|
||||
TER
|
||||
removeToken(ApplyView& view, AccountID const& owner, uint256 const& nftokenID);
|
||||
|
||||
/** Remove the token from the owner's token directory using a pre-located page.
|
||||
*
|
||||
* Caller-supplied page overload: skips the `succ()`-based page lookup when
|
||||
* the caller already holds the page (e.g., from `findTokenAndPage()`).
|
||||
* The `page` pointer must have been obtained from the same `ApplyView`
|
||||
* instance.
|
||||
*
|
||||
* Under the `fixNFTokenPageLinks` amendment, if the emptied page is the final
|
||||
* anchor page (`nftpage_max`), its contents are replaced with those of the
|
||||
* previous page and the now-empty previous page is erased, preserving the
|
||||
* invariant that the last page always has the stable sentinel key.
|
||||
*
|
||||
* @param view The apply view to mutate.
|
||||
* @param owner The account that currently holds the token.
|
||||
* @param nftokenID The 256-bit NFT identifier to remove.
|
||||
* @param page The mutable SLE page known to contain the token.
|
||||
* @return `tesSUCCESS`, or `tecNO_ENTRY` if the token is not found on the
|
||||
* supplied page.
|
||||
*/
|
||||
TER
|
||||
removeToken(
|
||||
ApplyView& view,
|
||||
@@ -53,28 +164,74 @@ removeToken(
|
||||
uint256 const& nftokenID,
|
||||
std::shared_ptr<SLE> const& page);
|
||||
|
||||
/** Deletes the given token offer.
|
||||
|
||||
An offer is tracked in two separate places:
|
||||
- The token's 'buy' directory, if it's a buy offer; or
|
||||
- The token's 'sell' directory, if it's a sell offer; and
|
||||
- The owner directory of the account that placed the offer.
|
||||
|
||||
The offer also consumes one incremental reserve.
|
||||
/** Deletes the given token offer and removes it from both tracking directories.
|
||||
*
|
||||
* An offer is tracked in two separate places:
|
||||
* - The token's `nft_buys` directory, if it is a buy offer; or
|
||||
* - The token's `nft_sells` directory, if it is a sell offer; and
|
||||
* - The owner's owner directory.
|
||||
*
|
||||
* Both directory entries are removed, the owner's reserve count is
|
||||
* decremented by one, and the offer SLE is erased.
|
||||
*
|
||||
* @param view The apply view to mutate.
|
||||
* @param offer The SLE for the offer to delete; must be of type
|
||||
* `ltNFTOKEN_OFFER`.
|
||||
* @return `true` if the offer was successfully deleted; `false` if the SLE
|
||||
* is not of type `ltNFTOKEN_OFFER` or if a directory removal fails,
|
||||
* acting as a type-safety guard.
|
||||
*/
|
||||
bool
|
||||
deleteTokenOffer(ApplyView& view, std::shared_ptr<SLE> const& offer);
|
||||
|
||||
/** Repairs the links in an NFTokenPage directory.
|
||||
|
||||
Returns true if a repair took place, otherwise false.
|
||||
*/
|
||||
/** Repairs the links in an NFToken page directory.
|
||||
*
|
||||
* Walks the entire `ltNFTOKEN_PAGE` chain for the owner and corrects any
|
||||
* broken `sfNextPageMin` / `sfPreviousPageMin` links. If the final page does
|
||||
* not have the expected `nftpage_max` sentinel key, its contents are migrated
|
||||
* to a newly created SLE with the correct key, the old SLE is erased, and the
|
||||
* chain is relinked. Owner count is unchanged by this operation because the
|
||||
* page count is preserved.
|
||||
*
|
||||
* Intended to be called by the `LedgerStateFix` transaction on accounts with
|
||||
* known directory corruption.
|
||||
*
|
||||
* @param view The apply view to mutate.
|
||||
* @param owner The account whose NFToken page directory is to be repaired.
|
||||
* @return `true` if any correction was applied; `false` if the directory was
|
||||
* already consistent.
|
||||
*/
|
||||
bool
|
||||
repairNFTokenDirectoryLinks(ApplyView& view, AccountID const& owner);
|
||||
|
||||
/** Ordering predicate for NFToken IDs within and across pages.
|
||||
*
|
||||
* Sorts first by the low 96 bits of each ID (the `pageMask` region that
|
||||
* determines page placement), then by the full 256-bit value as a
|
||||
* tiebreaker. This ensures deterministic ordering for tokens that share
|
||||
* the same low 96-bit prefix (equivalent tokens) and must co-reside on
|
||||
* a single page.
|
||||
*
|
||||
* @param a First NFToken ID.
|
||||
* @param b Second NFToken ID.
|
||||
* @return `true` if `a` sorts before `b`.
|
||||
*/
|
||||
bool
|
||||
compareTokens(uint256 const& a, uint256 const& b);
|
||||
|
||||
/** Modify the URI of an existing NFToken in the owner's directory.
|
||||
*
|
||||
* Locates the token's page and updates the `sfURI` field in the token's
|
||||
* `STObject` within the page's `sfNFTokens` array. If `uri` is
|
||||
* `std::nullopt`, the `sfURI` field is removed from the token.
|
||||
*
|
||||
* @param view The apply view to mutate.
|
||||
* @param owner The account that owns the token.
|
||||
* @param nftokenID The 256-bit NFT identifier whose URI is to be changed.
|
||||
* @param uri The new URI value, or `std::nullopt` to clear the URI.
|
||||
* @return `tesSUCCESS` on success, or `tecINTERNAL` if the page or token
|
||||
* cannot be located (indicates ledger inconsistency).
|
||||
*/
|
||||
TER
|
||||
changeTokenURI(
|
||||
ApplyView& view,
|
||||
@@ -82,7 +239,33 @@ changeTokenURI(
|
||||
uint256 const& nftokenID,
|
||||
std::optional<xrpl::Slice> const& uri);
|
||||
|
||||
/** Preflight checks shared by NFTokenCreateOffer and NFTokenMint */
|
||||
/** Preflight checks shared by NFTokenCreateOffer and NFTokenMint.
|
||||
*
|
||||
* Validates offer parameters that require no ledger access: negative or
|
||||
* zero amounts (buy offers must carry a non-zero amount), zero IOU amounts,
|
||||
* zero expiration, and malformed `owner`/`destination` combinations.
|
||||
* A buy offer must supply `owner` (the targeted token holder); a sell offer
|
||||
* must not (the seller is implicit). Neither party may designate itself as
|
||||
* the destination.
|
||||
*
|
||||
* Defaults (`owner = nullopt`, `txFlags = tfSellNFToken`) allow
|
||||
* `NFTokenMint` to reuse this path with minimal adaptation.
|
||||
*
|
||||
* @param acctID Account executing the transaction.
|
||||
* @param amount The offer amount; must be non-negative and, for buy offers,
|
||||
* non-zero and non-zero for IOUs.
|
||||
* @param dest Optional destination account that may exclusively accept the
|
||||
* offer; must not equal `acctID`.
|
||||
* @param expiration Optional offer expiration; must not be zero.
|
||||
* @param nftFlags The flags field of the NFToken being offered.
|
||||
* @param rules Current ledger rule set used for amendment checks.
|
||||
* @param owner For buy offers, the account that currently holds the token;
|
||||
* must be absent for sell offers.
|
||||
* @param txFlags Transaction flags; `tfSellNFToken` distinguishes sell from
|
||||
* buy.
|
||||
* @return `tesSUCCESS` if all static checks pass, or a `temXXX` error code
|
||||
* indicating which parameter is invalid.
|
||||
*/
|
||||
NotTEC
|
||||
tokenOfferCreatePreflight(
|
||||
AccountID const& acctID,
|
||||
@@ -94,7 +277,37 @@ tokenOfferCreatePreflight(
|
||||
std::optional<AccountID> const& owner = std::nullopt,
|
||||
std::uint32_t txFlags = tfSellNFToken);
|
||||
|
||||
/** Preclaim checks shared by NFTokenCreateOffer and NFTokenMint */
|
||||
/** Preclaim checks shared by NFTokenCreateOffer and NFTokenMint.
|
||||
*
|
||||
* Accesses the ledger to validate conditions that cannot be checked
|
||||
* statically:
|
||||
* - For non-XRP offers on tokens without `flagCreateTrustLines`, verifies
|
||||
* that the NFT issuer's trust line for the IOU exists and is not frozen.
|
||||
* Under `featureNFTokenMintOffer`, an issuer selling their own currency is
|
||||
* exempt from this check.
|
||||
* - Enforces `flagTransferable`: if absent and the transacting account is
|
||||
* neither the issuer nor the current `sfNFTokenMinter`, returns
|
||||
* `tefNFTOKEN_IS_NOT_TRANSFERABLE`.
|
||||
* - For buy offers, verifies the account currently has sufficient funds.
|
||||
* - Verifies `dest` and `owner` accounts exist and have not set
|
||||
* `lsfDisallowIncomingNFTokenOffer`.
|
||||
* - Under `fixEnforceNFTokenTrustlineV2`, calls `checkTrustlineAuthorized()`
|
||||
* to reject offers backed by unauthorized trust lines that carry a balance.
|
||||
*
|
||||
* @param view The read-only ledger view.
|
||||
* @param acctID Account executing the transaction.
|
||||
* @param nftIssuer Issuer encoded in the NFToken ID.
|
||||
* @param amount The offer amount.
|
||||
* @param dest Optional restricted destination account.
|
||||
* @param nftFlags The flags field of the NFToken being offered.
|
||||
* @param xferFee Transfer fee encoded in the NFToken ID (basis points).
|
||||
* @param j Journal for diagnostic logging.
|
||||
* @param owner For buy offers, the account that currently holds the token.
|
||||
* @param txFlags Transaction flags; `tfSellNFToken` distinguishes sell from
|
||||
* buy.
|
||||
* @return `tesSUCCESS` if all ledger-state checks pass, or a `tecXXX` /
|
||||
* `tefXXX` error code.
|
||||
*/
|
||||
TER
|
||||
tokenOfferCreatePreclaim(
|
||||
ReadView const& view,
|
||||
@@ -108,7 +321,28 @@ tokenOfferCreatePreclaim(
|
||||
std::optional<AccountID> const& owner = std::nullopt,
|
||||
std::uint32_t txFlags = tfSellNFToken);
|
||||
|
||||
/** doApply implementation shared by NFTokenCreateOffer and NFTokenMint */
|
||||
/** doApply implementation shared by NFTokenCreateOffer and NFTokenMint.
|
||||
*
|
||||
* Reserves XRP for the new `ltNFTOKEN_OFFER` object, inserts the offer into
|
||||
* the account's owner directory and into the token's buy or sell directory
|
||||
* (determined by `tfSellNFToken` in `txFlags`), constructs the SLE with the
|
||||
* supplied fields, and increments the owner count.
|
||||
*
|
||||
* @param view The apply view to mutate.
|
||||
* @param acctID Account executing the transaction.
|
||||
* @param amount The offer amount.
|
||||
* @param dest Optional restricted destination account.
|
||||
* @param expiration Optional expiration time for the offer.
|
||||
* @param seqProxy Sequence or ticket proxy used to derive the offer keylet.
|
||||
* @param nftokenID The 256-bit ID of the NFToken being offered.
|
||||
* @param priorBalance The account's XRP balance before the transaction fee
|
||||
* was deducted; used to verify the reserve requirement.
|
||||
* @param j Journal for diagnostic logging.
|
||||
* @param txFlags Transaction flags; `tfSellNFToken` controls offer direction.
|
||||
* @return `tesSUCCESS` on success, `tecINSUFFICIENT_RESERVE` if the account
|
||||
* cannot cover the new object reserve, or `tecDIR_FULL` if either
|
||||
* directory is at capacity.
|
||||
*/
|
||||
TER
|
||||
tokenOfferCreateApply(
|
||||
ApplyView& view,
|
||||
@@ -122,6 +356,25 @@ tokenOfferCreateApply(
|
||||
beast::Journal j,
|
||||
std::uint32_t txFlags = tfSellNFToken);
|
||||
|
||||
/** Verify that an account is authorized to hold a given IOU trust line.
|
||||
*
|
||||
* Only active under the `fixEnforceNFTokenTrustlineV2` amendment; returns
|
||||
* `tesSUCCESS` unconditionally when the amendment is not enabled.
|
||||
*
|
||||
* When active, checks that if the IOU issuer requires authorization
|
||||
* (`lsfRequireAuth`), the trust line between `id` and the issuer exists and
|
||||
* carries the appropriate `lsfLowAuth` / `lsfHighAuth` flag. The issuer
|
||||
* account is always considered authorized to hold its own issuance.
|
||||
*
|
||||
* @param view The read-only ledger view.
|
||||
* @param id The account whose authorization is being verified.
|
||||
* @param j Journal for diagnostic logging.
|
||||
* @param issue The IOU issue (currency + issuer) to check; must not be XRP.
|
||||
* @return `tesSUCCESS` if authorized, `tecNO_ISSUER` if the issuer account
|
||||
* does not exist, `tecNO_LINE` if the required trust line is absent, or
|
||||
* `tecNO_AUTH` if the trust line exists but is not authorized.
|
||||
* @note Only valid for custom (non-XRP) currencies; asserts otherwise.
|
||||
*/
|
||||
TER
|
||||
checkTrustlineAuthorized(
|
||||
ReadView const& view,
|
||||
@@ -129,6 +382,26 @@ checkTrustlineAuthorized(
|
||||
beast::Journal const j,
|
||||
Issue const& issue);
|
||||
|
||||
/** Verify that an IOU trust line is not deep-frozen for a given account.
|
||||
*
|
||||
* Only active under the `featureDeepFreeze` amendment; returns
|
||||
* `tesSUCCESS` unconditionally when the amendment is not enabled.
|
||||
*
|
||||
* When active, checks whether the trust line between `id` and the IOU issuer
|
||||
* carries either `lsfLowDeepFreeze` or `lsfHighDeepFreeze`. Either side
|
||||
* enacting deep freeze blocks token receipt, regardless of which party set it.
|
||||
* The issuer account is always permitted to accept its own issuance; accounts
|
||||
* with no trust line are treated as not frozen.
|
||||
*
|
||||
* @param view The read-only ledger view.
|
||||
* @param id The account whose deep-freeze status is being checked.
|
||||
* @param j Journal for diagnostic logging.
|
||||
* @param issue The IOU issue (currency + issuer) to check; must not be XRP.
|
||||
* @return `tesSUCCESS` if not deep-frozen or if no trust line exists,
|
||||
* `tecNO_ISSUER` if the issuer account does not exist, or `tecFROZEN`
|
||||
* if the trust line is deep-frozen.
|
||||
* @note Only valid for custom (non-XRP) currencies; asserts otherwise.
|
||||
*/
|
||||
TER
|
||||
checkTrustlineDeepFrozen(
|
||||
ReadView const& view,
|
||||
|
||||
@@ -9,18 +9,38 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Delete an offer.
|
||||
|
||||
Requirements:
|
||||
The offer must exist.
|
||||
The caller must have already checked permissions.
|
||||
|
||||
@param view The ApplyView to modify.
|
||||
@param sle The offer to delete.
|
||||
@param j Journal for logging.
|
||||
|
||||
@return tesSUCCESS on success, otherwise an error code.
|
||||
*/
|
||||
/** Remove an offer and its directory back-references from the ledger.
|
||||
*
|
||||
* Performs the full teardown sequence atomically within the transaction
|
||||
* buffer: removes the offer from the owner's directory, removes it from
|
||||
* the order-book quality directory, decrements the owner's reserve count,
|
||||
* and erases the SLE. For hybrid offers (flagged `lsfHybrid`) that
|
||||
* participate in one or more Permissioned DEX domains, each entry in
|
||||
* `sfAdditionalBooks` is also removed from its domain-specific book
|
||||
* directory before the owner-count adjustment and erasure.
|
||||
*
|
||||
* If `sle` is null the function returns `tesSUCCESS` immediately,
|
||||
* allowing callers to pass the result of a failed `peek()` without
|
||||
* a pre-check (defensive against double-delete within one batch).
|
||||
*
|
||||
* @pre The offer SLE must exist in the ledger and both its
|
||||
* `sfOwnerNode` and `sfBookNode` back-references must be valid.
|
||||
* @pre The caller must have already verified that the submitting
|
||||
* account is authorized to delete this offer; this function
|
||||
* performs no ownership or permission check.
|
||||
*
|
||||
* @param view The `ApplyView` transaction buffer to modify.
|
||||
* @param sle The offer SLE to delete. May be null (treated as no-op).
|
||||
* @param j Journal for diagnostic logging.
|
||||
*
|
||||
* @return `tesSUCCESS` on success, or `tefBAD_LEDGER` if a directory
|
||||
* back-reference is missing (invariant violation; should not occur
|
||||
* in a well-formed ledger).
|
||||
*
|
||||
* @note `[[nodiscard]]` is intentionally absent: `BookTip` and payment
|
||||
* path callers do not always inspect the return value, and enforcing
|
||||
* the attribute would have broken compilation across the engine.
|
||||
*/
|
||||
// [[nodiscard]] // nodiscard commented out so Flow, BookTip and others compile.
|
||||
TER
|
||||
offerDelete(ApplyView& view, std::shared_ptr<SLE> const& sle, beast::Journal j);
|
||||
|
||||
@@ -7,6 +7,35 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Tear down a payment channel and return unspent XRP to its source account.
|
||||
*
|
||||
* Performs four ledger mutations in order:
|
||||
* 1. Removes the channel from the source's owner directory (`sfOwnerNode`).
|
||||
* 2. Conditionally removes the channel from the destination's owner directory
|
||||
* (`sfDestinationNode`) — the field is absent on older channel objects that
|
||||
* predate destination-directory tracking, so its presence is tested before
|
||||
* the removal attempt.
|
||||
* 3. Credits the unspent balance (`sfAmount - sfBalance`) back to the source
|
||||
* account. `sfAmount` is the total XRP escrowed; `sfBalance` is the
|
||||
* cumulative amount already paid to the destination.
|
||||
* 4. Decrements the source's owner count and erases the `ltPAYCHAN` SLE.
|
||||
*
|
||||
* Called by both `PaymentChannelClaim` and `PaymentChannelFund` whenever a
|
||||
* channel must be closed — on expiry (`cancelAfter`/`expiration` elapsed), on
|
||||
* an explicit `tfClose` flag, or when the channel is fully drained.
|
||||
*
|
||||
* @param slep The `ltPAYCHAN` SLE to close; must satisfy
|
||||
* `sfAmount >= sfBalance` (asserted).
|
||||
* @param view The apply view through which all ledger mutations are made.
|
||||
* @param key The ledger key of the channel SLE (used for directory removal).
|
||||
* @param j Journal for fatal-level diagnostic messages on internal errors.
|
||||
* @return `tesSUCCESS` on the normal path; `tefBAD_LEDGER` if an owner
|
||||
* directory removal fails (indicates corrupted ledger state);
|
||||
* `tefINTERNAL` if the source account SLE cannot be found.
|
||||
* @note The `tefBAD_LEDGER` and `tefINTERNAL` branches are annotated
|
||||
* `LCOV_EXCL` — they guard against ledger corruption that cannot occur
|
||||
* during correct operation.
|
||||
*/
|
||||
TER
|
||||
closeChannel(
|
||||
std::shared_ptr<SLE> const& slep,
|
||||
|
||||
@@ -1,13 +1,90 @@
|
||||
/**
|
||||
* @file PermissionedDEXHelpers.h
|
||||
* @brief Domain membership predicates for the Permissioned DEX.
|
||||
*
|
||||
* Declares the two authorization gatekeepers used by `xrpl::permissioned_dex`
|
||||
* to enforce credential-based access control on restricted order books.
|
||||
* Both functions are called from transaction preclaim logic and from live
|
||||
* order-book traversal in `OfferStream`.
|
||||
*/
|
||||
#pragma once
|
||||
#include <xrpl/ledger/View.h>
|
||||
|
||||
namespace xrpl::permissioned_dex {
|
||||
|
||||
// Check if an account is in a permissioned domain
|
||||
/**
|
||||
* @brief Test whether an account currently qualifies as a member of a
|
||||
* permissioned domain.
|
||||
*
|
||||
* Resolves the `PermissionedDomain` ledger object identified by @p domainID
|
||||
* and applies a two-tier membership test:
|
||||
*
|
||||
* 1. **Owner shortcut** — the domain's `sfOwner` is always considered a member,
|
||||
* avoiding a bootstrap problem where the owner couldn't trade in their own
|
||||
* domain.
|
||||
* 2. **Credential scan** — for all other accounts, the function iterates
|
||||
* `sfAcceptedCredentials` and returns `true` as soon as it finds a
|
||||
* credential issued to @p account that (a) carries the `lsfAccepted` flag
|
||||
* and (b) has not expired according to `credentials::checkExpired` evaluated
|
||||
* against the ledger's `parentCloseTime`.
|
||||
*
|
||||
* Expiry is evaluated against `parentCloseTime` (not wall time) so that all
|
||||
* validators reach the same deterministic result regardless of local clock skew.
|
||||
*
|
||||
* @param view The read-only ledger view to query.
|
||||
* @param account The account whose domain membership is being tested.
|
||||
* @param domainID The identifier of the `PermissionedDomain` ledger object.
|
||||
* @return `true` if @p account is the domain owner or holds at least one
|
||||
* accepted, non-expired credential listed in the domain; `false` if the
|
||||
* domain object does not exist, or if no qualifying credential is found.
|
||||
*
|
||||
* @note Called from `OfferCreate` preclaim (rejects with `tecNO_PERMISSION` if
|
||||
* `false`) and twice from `Payment` preclaim — once for the sender, once for
|
||||
* the destination — since a domain payment requires both parties to be
|
||||
* members. Also called internally by `offerInDomain`.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
accountInDomain(ReadView const& view, AccountID const& account, Domain const& domainID);
|
||||
|
||||
// Check if an offer is in the permissioned domain
|
||||
/**
|
||||
* @brief Test whether a specific offer is still legitimately part of a
|
||||
* permissioned domain at the time it is being consumed.
|
||||
*
|
||||
* Called by `OfferStream` during order-book traversal to handle the race
|
||||
* between offer creation and subsequent credential expiry. An offer that was
|
||||
* valid when placed may become invalid if the owner's credentials expire before
|
||||
* the offer is matched. When this function returns `false`, `OfferStream`
|
||||
* removes the offer from the book immediately (`permRmOffer`) instead of
|
||||
* matching it.
|
||||
*
|
||||
* The function performs the following checks in order:
|
||||
* - Offer SLE must exist (defensive; should not occur in a well-formed book).
|
||||
* - Offer must carry `sfDomainID` (defensive; should not occur).
|
||||
* - `sfDomainID` must match @p domainID (defensive; should not occur).
|
||||
* - **Post-`fixCleanup3_1_3`**: a hybrid offer (`lsfHybrid`) must have
|
||||
* `sfAdditionalBooks` present with exactly one entry; a violation is logged
|
||||
* as an error and `false` is returned.
|
||||
* - **Pre-`fixCleanup3_1_3`**: a hybrid offer must have `sfAdditionalBooks`
|
||||
* present (size is not validated).
|
||||
* - Delegates the final membership check to `accountInDomain` for the offer's
|
||||
* owner (`sfAccount`).
|
||||
*
|
||||
* The three defensive checks are marked `LCOV_EXCL_LINE`; they guard against
|
||||
* invariant violations that cannot occur under normal operation but are retained
|
||||
* as safety nets.
|
||||
*
|
||||
* @param view The read-only ledger view to query.
|
||||
* @param offerID The hash identifier of the offer SLE to validate.
|
||||
* @param domainID The permissioned domain the offer is expected to belong to.
|
||||
* @param j Journal used to log an error if a hybrid offer has a missing
|
||||
* or malformed `sfAdditionalBooks` field.
|
||||
* @return `true` if the offer passes all structural checks and its owner is
|
||||
* currently a member of @p domainID; `false` otherwise.
|
||||
*
|
||||
* @note The `fixCleanup3_1_3` amendment tightens hybrid-offer validation from
|
||||
* a presence-only check on `sfAdditionalBooks` to a presence-plus-size-one
|
||||
* check. Both code paths must be preserved for deterministic historic replay.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
offerInDomain(
|
||||
ReadView const& view,
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
/** @file
|
||||
* IOU trustline (RippleState) operations for the XRP Ledger.
|
||||
*
|
||||
* Declares every ledger operation that reads from or writes to a
|
||||
* `RippleState` (trustline) SLE: credit-limit and balance queries,
|
||||
* freeze checks, trustline lifecycle, IOU issuance/redemption,
|
||||
* authorization and rippling enforcement, zero-balance holding
|
||||
* management, and AMM-specific cleanup.
|
||||
*
|
||||
* This file is the IOU-specific leaf of the token helper layer.
|
||||
* Asset-agnostic callers should go through the dispatchers in
|
||||
* `TokenHelpers.h`, which branch on `Issue` vs `MPTIssue` and
|
||||
* delegate here for the IOU path.
|
||||
*
|
||||
* @note The trustline orientation invariant is pervasive here:
|
||||
* `sfLowLimit` always belongs to the account whose `AccountID`
|
||||
* compares less; `sfHighLimit` to the other. Every function
|
||||
* applies this flip internally — callers supply `(account, issuer)`
|
||||
* and receive results in account-centric terms.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -10,27 +30,29 @@
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// RippleState (Trustline) helpers
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
// --- RippleState (Trustline) helpers ---
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Credit functions (from Credit.h)
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
// --- Credit queries ---
|
||||
|
||||
/** Calculate the maximum amount of IOUs that an account can hold
|
||||
@param view the ledger to check against.
|
||||
@param account the account of interest.
|
||||
@param issuer the issuer of the IOU.
|
||||
@param currency the IOU to check.
|
||||
@return The maximum amount that can be held.
|
||||
*/
|
||||
/** Read the maximum IOU balance that @p account has authorised @p issuer to
|
||||
* carry on their behalf.
|
||||
*
|
||||
* Reads `sfLowLimit` or `sfHighLimit` from the trustline depending on
|
||||
* which side `account` occupies (low if `account < issuer`). The issuer
|
||||
* field of the returned amount is rewritten to `account` so the result is
|
||||
* safe to consume without knowing the binary-ordering of the two accounts.
|
||||
* Returns a zero-valued `STAmount` (with the correct issue) if no trustline
|
||||
* exists.
|
||||
*
|
||||
* @param view Read-only ledger view to query.
|
||||
* @param account The account whose credit limit is requested.
|
||||
* @param issuer The IOU issuer.
|
||||
* @param currency The currency of the trustline.
|
||||
* @return The credit limit expressed from @p account's perspective, or zero
|
||||
* if no trustline exists.
|
||||
*/
|
||||
/** @{ */
|
||||
STAmount
|
||||
creditLimit(
|
||||
@@ -39,16 +61,35 @@ creditLimit(
|
||||
AccountID const& issuer,
|
||||
Currency const& currency);
|
||||
|
||||
/** Convenience wrapper returning the credit limit as `IOUAmount`.
|
||||
*
|
||||
* @param v Read-only ledger view to query.
|
||||
* @param acc The account whose credit limit is requested.
|
||||
* @param iss The IOU issuer.
|
||||
* @param cur The currency of the trustline.
|
||||
* @return The credit limit as `IOUAmount`, or zero if no trustline exists.
|
||||
* @see creditLimit
|
||||
*/
|
||||
IOUAmount
|
||||
creditLimit2(ReadView const& v, AccountID const& acc, AccountID const& iss, Currency const& cur);
|
||||
/** @} */
|
||||
|
||||
/** Returns the amount of IOUs issued by issuer that are held by an account
|
||||
@param view the ledger to check against.
|
||||
@param account the account of interest.
|
||||
@param issuer the issuer of the IOU.
|
||||
@param currency the IOU to check.
|
||||
*/
|
||||
/** Read the IOU balance that @p account currently holds.
|
||||
*
|
||||
* `sfBalance` is stored in "low-account-sends-to-high-account" orientation.
|
||||
* When `account` is the high side the stored value is negated before being
|
||||
* returned, so callers always receive a balance expressed as "how much of
|
||||
* this currency does @p account hold", regardless of which slot they occupy
|
||||
* on the trustline. Returns zero (with the correct issue) if no trustline
|
||||
* exists.
|
||||
*
|
||||
* @param view Read-only ledger view to query.
|
||||
* @param account The account whose balance is requested.
|
||||
* @param issuer The IOU issuer.
|
||||
* @param currency The currency of the trustline.
|
||||
* @return The balance expressed from @p account's perspective, or zero if
|
||||
* no trustline exists.
|
||||
*/
|
||||
/** @{ */
|
||||
STAmount
|
||||
creditBalance(
|
||||
@@ -58,12 +99,20 @@ creditBalance(
|
||||
Currency const& currency);
|
||||
/** @} */
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Freeze checking (IOU-specific)
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
// --- Freeze checks (IOU-specific) ---
|
||||
|
||||
/** Check whether @p issuer has individually frozen @p account's trustline.
|
||||
*
|
||||
* Inspects only the issuer's side flag (`lsfLowFreeze`/`lsfHighFreeze`) on
|
||||
* the trustline. Does **not** check the issuer's global freeze flag — use
|
||||
* `isFrozen` for that combined check. Always returns `false` for XRP.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param currency The IOU currency.
|
||||
* @param issuer The IOU issuer.
|
||||
* @return `true` if the issuer has set a line-level freeze on this account.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isIndividualFrozen(
|
||||
ReadView const& view,
|
||||
@@ -71,12 +120,34 @@ isIndividualFrozen(
|
||||
Currency const& currency,
|
||||
AccountID const& issuer);
|
||||
|
||||
/** Convenience overload accepting an `Issue`.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param issue The IOU issue (currency + issuer).
|
||||
* @return `true` if the issuer has set a line-level freeze on this account.
|
||||
* @see isIndividualFrozen(ReadView const&, AccountID const&, Currency const&,
|
||||
* AccountID const&)
|
||||
*/
|
||||
[[nodiscard]] inline bool
|
||||
isIndividualFrozen(ReadView const& view, AccountID const& account, Issue const& issue)
|
||||
{
|
||||
return isIndividualFrozen(view, account, issue.currency, issue.account);
|
||||
}
|
||||
|
||||
/** Check whether @p account is frozen for @p currency issued by @p issuer.
|
||||
*
|
||||
* Returns `true` if either the issuer's `AccountRoot` has `lsfGlobalFreeze`
|
||||
* set, or the issuer has frozen this specific trustline (`lsfLowFreeze` /
|
||||
* `lsfHighFreeze`). Always returns `false` for XRP or when
|
||||
* `account == issuer`. This is the check used by payment paths.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param currency The IOU currency.
|
||||
* @param issuer The IOU issuer.
|
||||
* @return `true` if the account cannot move this IOU due to any freeze.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isFrozen(
|
||||
ReadView const& view,
|
||||
@@ -84,20 +155,52 @@ isFrozen(
|
||||
Currency const& currency,
|
||||
AccountID const& issuer);
|
||||
|
||||
/** Convenience overload accepting an `Issue`.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param issue The IOU issue (currency + issuer).
|
||||
* @return `true` if the account cannot move this IOU due to any freeze.
|
||||
* @see isFrozen(ReadView const&, AccountID const&, Currency const&,
|
||||
* AccountID const&)
|
||||
*/
|
||||
[[nodiscard]] inline bool
|
||||
isFrozen(ReadView const& view, AccountID const& account, Issue const& issue)
|
||||
{
|
||||
return isFrozen(view, account, issue.currency, issue.account);
|
||||
}
|
||||
|
||||
// Overload with depth parameter for uniformity with MPTIssue version.
|
||||
// The depth parameter is ignored for IOUs since they don't have vault recursion.
|
||||
/** Overload accepting a depth parameter for interface uniformity with MPT.
|
||||
*
|
||||
* IOUs do not have vault-level recursion, so the `depth` argument is
|
||||
* unconditionally ignored.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param issue The IOU issue (currency + issuer).
|
||||
* @return `true` if the account cannot move this IOU due to any freeze.
|
||||
*/
|
||||
[[nodiscard]] inline bool
|
||||
isFrozen(ReadView const& view, AccountID const& account, Issue const& issue, int /*depth*/)
|
||||
{
|
||||
return isFrozen(view, account, issue);
|
||||
}
|
||||
|
||||
/** Check whether @p account is deep-frozen for @p currency issued by
|
||||
* @p issuer.
|
||||
*
|
||||
* Deep-freeze (`lsfHighDeepFreeze` / `lsfLowDeepFreeze`) is a stricter
|
||||
* condition than ordinary freeze: it prevents both sending *and* receiving
|
||||
* the currency. Always returns `false` for XRP, and always returns `false`
|
||||
* when `issuer == account` (an issuer cannot deep-freeze their own balance
|
||||
* with themselves).
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param currency The IOU currency.
|
||||
* @param issuer The IOU issuer.
|
||||
* @return `true` if the deep-freeze flag is set on either side of the line.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isDeepFrozen(
|
||||
ReadView const& view,
|
||||
@@ -105,6 +208,18 @@ isDeepFrozen(
|
||||
Currency const& currency,
|
||||
AccountID const& issuer);
|
||||
|
||||
/** Convenience overload accepting an `Issue`, with an optional depth parameter
|
||||
* for interface uniformity with the MPT equivalent.
|
||||
*
|
||||
* The `depth` argument is unconditionally ignored for IOUs.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param issue The IOU issue (currency + issuer).
|
||||
* @return `true` if the deep-freeze flag is set on either side of the line.
|
||||
* @see isDeepFrozen(ReadView const&, AccountID const&, Currency const&,
|
||||
* AccountID const&)
|
||||
*/
|
||||
[[nodiscard]] inline bool
|
||||
isDeepFrozen(
|
||||
ReadView const& view,
|
||||
@@ -115,22 +230,63 @@ isDeepFrozen(
|
||||
return isDeepFrozen(view, account, issue.currency, issue.account);
|
||||
}
|
||||
|
||||
/** Convert a deep-freeze check into a `TER` result.
|
||||
*
|
||||
* Convenience wrapper for transactor preflight code that returns
|
||||
* `tecFROZEN` if the account is deep-frozen and `tesSUCCESS` otherwise.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param issue The IOU issue (currency + issuer).
|
||||
* @return `tecFROZEN` if deep-frozen, `tesSUCCESS` otherwise.
|
||||
*/
|
||||
[[nodiscard]] inline TER
|
||||
checkDeepFrozen(ReadView const& view, AccountID const& account, Issue const& issue)
|
||||
{
|
||||
return isDeepFrozen(view, account, issue) ? (TER)tecFROZEN : (TER)tesSUCCESS;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Trust line operations
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
// --- Trust line lifecycle ---
|
||||
|
||||
/** Create a trust line
|
||||
|
||||
This can set an initial balance.
|
||||
*/
|
||||
/** Create a new `RippleState` (trustline) SLE and insert it into both owner
|
||||
* directories.
|
||||
*
|
||||
* This is the lowest-level entry point for trustline creation. It is called
|
||||
* directly by `TrustSet` transactors and indirectly by `issueIOU` when the
|
||||
* destination has no existing line.
|
||||
*
|
||||
* The function writes all trustline fields — limits, quality in/out, balance,
|
||||
* and flag bits — using side-aware field selectors (`sfLowLimit`/`sfHighLimit`
|
||||
* etc.) derived from `bSrcHigh`. The peer account's `lsfNoRipple` flag is
|
||||
* initialised from the peer's `lsfDefaultRipple` setting (absent means
|
||||
* noRipple is on by default).
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param bSrcHigh `true` if `uSrcAccountID` occupies the "high" slot
|
||||
* (i.e., `uSrcAccountID > uDstAccountID`).
|
||||
* @param uSrcAccountID The account whose limit and flags are being
|
||||
* configured.
|
||||
* @param uDstAccountID The peer account on the other side of the line.
|
||||
* @param uIndex Pre-calculated keylet key for the new SLE.
|
||||
* @param sleAccount The `AccountRoot` SLE for the account being set
|
||||
* (used to adjust owner count); must not be null.
|
||||
* @param bAuth If `true`, set the authorization flag on the source
|
||||
* side of the line.
|
||||
* @param bNoRipple If `true`, set `lsfNoRipple` on the source side.
|
||||
* @param bFreeze If `true`, set the freeze flag on the source side.
|
||||
* @param bDeepFreeze If `true`, set the deep-freeze flag on the source
|
||||
* side.
|
||||
* @param saBalance Initial balance from the source account's
|
||||
* perspective; the issuer field must be `noAccount()`.
|
||||
* @param saLimit Credit limit for the source account; the issuer
|
||||
* field must be `uSrcAccountID`.
|
||||
* @param uQualityIn Quality-in override (0 = default/no override).
|
||||
* @param uQualityOut Quality-out override (0 = default/no override).
|
||||
* @param j Journal for trace/debug logging.
|
||||
* @return `tesSUCCESS` on success, `tecDIR_FULL` if either owner directory
|
||||
* is at capacity, `tecNO_TARGET` if the peer account does not exist,
|
||||
* or `tefINTERNAL` if `sleAccount` is null or has a mismatched ID.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
trustCreate(
|
||||
ApplyView& view,
|
||||
@@ -151,6 +307,21 @@ trustCreate(
|
||||
std::uint32_t uQualityOut,
|
||||
beast::Journal j);
|
||||
|
||||
/** Delete a `RippleState` (trustline) SLE and remove its directory backlinks.
|
||||
*
|
||||
* Removes the SLE from both the low and high owner directories using the
|
||||
* `sfLowNode`/`sfHighNode` deletion hints stored inside the SLE itself,
|
||||
* then erases the SLE from the view.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param sleRippleState The trustline SLE to delete; must be obtained
|
||||
* from `view.peek()`.
|
||||
* @param uLowAccountID The account occupying the low slot.
|
||||
* @param uHighAccountID The account occupying the high slot.
|
||||
* @param j Journal for trace/debug logging.
|
||||
* @return `tesSUCCESS` on success, `tefBAD_LEDGER` if either directory
|
||||
* removal fails (indicating ledger corruption).
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
trustDelete(
|
||||
ApplyView& view,
|
||||
@@ -159,12 +330,30 @@ trustDelete(
|
||||
AccountID const& uHighAccountID,
|
||||
beast::Journal j);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// IOU issuance/redemption
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
// --- IOU issuance/redemption ---
|
||||
|
||||
/** Issue IOUs from @p issue.account to @p account, adjusting the trustline
|
||||
* balance.
|
||||
*
|
||||
* Debits the issuer's side of the trustline and credits the receiver. After
|
||||
* adjusting the balance, calls the internal `updateTrustLine` helper: if the
|
||||
* sender's balance crosses zero and seven specific cleanup conditions are met
|
||||
* (zero limit, no freeze, etc.), the sender's reserve is released and the
|
||||
* line may be deleted via `trustDelete`.
|
||||
*
|
||||
* If no trustline exists for the receiver, one is created via `trustCreate`,
|
||||
* inheriting the receiver's `lsfDefaultRipple` setting for the initial
|
||||
* `lsfNoRipple` state. Always invokes `view.creditHookIOU()` after mutating
|
||||
* the balance.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param account The account receiving the IOUs (must not be the issuer).
|
||||
* @param amount The amount to issue; its `Issue` must match @p issue.
|
||||
* @param issue Identifies the currency and issuer.
|
||||
* @param j Journal for trace/debug logging.
|
||||
* @return `tesSUCCESS` on success, or a `tef`/`tec` code propagated from
|
||||
* `trustCreate` or `trustDelete` if an error occurs.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
issueIOU(
|
||||
ApplyView& view,
|
||||
@@ -173,6 +362,26 @@ issueIOU(
|
||||
Issue const& issue,
|
||||
beast::Journal j);
|
||||
|
||||
/** Redeem IOUs held by @p account back toward the issuer, adjusting the
|
||||
* trustline balance.
|
||||
*
|
||||
* The mirror image of `issueIOU`: credits the issuer and debits the holder.
|
||||
* After adjusting the balance, calls `updateTrustLine` for the same
|
||||
* automatic cleanup logic. Always invokes `view.creditHookIOU()` after
|
||||
* mutating the balance.
|
||||
*
|
||||
* Unlike `issueIOU`, a missing trustline is treated as a fatal internal
|
||||
* error (`tefINTERNAL`) because it is impossible to redeem a balance on a
|
||||
* line that does not exist.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param account The account redeeming IOUs (must not be the issuer).
|
||||
* @param amount The amount to redeem; its `Issue` must match @p issue.
|
||||
* @param issue Identifies the currency and issuer.
|
||||
* @param j Journal for trace/debug logging.
|
||||
* @return `tesSUCCESS` on success, `tefINTERNAL` if no trustline exists,
|
||||
* or a `tef`/`tec` code from `trustDelete` if cleanup triggers an error.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
redeemIOU(
|
||||
ApplyView& view,
|
||||
@@ -181,28 +390,30 @@ redeemIOU(
|
||||
Issue const& issue,
|
||||
beast::Journal j);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Authorization and transfer checks (IOU-specific)
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
// --- Authorization and transfer checks (IOU-specific) ---
|
||||
|
||||
/** Check if the account lacks required authorization.
|
||||
/** Check whether @p account is authorized to hold the IOU described by
|
||||
* @p issue.
|
||||
*
|
||||
* Return tecNO_AUTH or tecNO_LINE if it does
|
||||
* and tesSUCCESS otherwise.
|
||||
* Behaviour depends on @p authType:
|
||||
* - **`StrongAuth`**: Returns `tecNO_LINE` immediately if no trustline
|
||||
* exists. If the issuer has `lsfRequireAuth` and the line exists but is
|
||||
* not authorized, returns `tecNO_AUTH`.
|
||||
* - **`WeakAuth`** / **`Legacy`** (equivalent for IOUs): Returns
|
||||
* `tecNO_AUTH` if `lsfRequireAuth` is set, the line exists, but is not
|
||||
* authorized. Returns `tecNO_LINE` if auth is required and no line
|
||||
* exists. If `lsfRequireAuth` is not set, returns `tesSUCCESS` even when
|
||||
* no line exists — appropriate for payment path-finding where a line may
|
||||
* be created on the fly.
|
||||
*
|
||||
* If StrongAuth then return tecNO_LINE if the RippleState doesn't exist. Return
|
||||
* tecNO_AUTH if lsfRequireAuth is set on the issuer's AccountRoot, and the
|
||||
* RippleState does exist, and the RippleState is not authorized.
|
||||
* Always returns `tesSUCCESS` for XRP or when `account == issue.account`.
|
||||
*
|
||||
* If WeakAuth then return tecNO_AUTH if lsfRequireAuth is set, and the
|
||||
* RippleState exists, and is not authorized. Return tecNO_LINE if
|
||||
* lsfRequireAuth is set and the RippleState doesn't exist. Consequently, if
|
||||
* WeakAuth and lsfRequireAuth is *not* set, this function will return
|
||||
* tesSUCCESS even if RippleState does *not* exist.
|
||||
*
|
||||
* The default "Legacy" auth type is equivalent to WeakAuth.
|
||||
* @param view Read-only ledger view.
|
||||
* @param issue The IOU to check authorization for.
|
||||
* @param account The account to check.
|
||||
* @param authType Authorization strictness; defaults to `AuthType::Legacy`
|
||||
* (equivalent to `WeakAuth` for IOUs).
|
||||
* @return `tesSUCCESS`, `tecNO_AUTH`, or `tecNO_LINE`.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
requireAuth(
|
||||
@@ -211,21 +422,53 @@ requireAuth(
|
||||
AccountID const& account,
|
||||
AuthType authType = AuthType::Legacy);
|
||||
|
||||
/** Check if the destination account is allowed
|
||||
* to receive IOU. Return terNO_RIPPLE if rippling is
|
||||
* disabled on both sides and tesSUCCESS otherwise.
|
||||
/** Check whether an IOU can be transferred between @p from and @p to via the
|
||||
* issuer's trustlines.
|
||||
*
|
||||
* Returns `tesSUCCESS` unconditionally when either endpoint is the issuer,
|
||||
* or when the IOU is native (XRP). For third-party transfers, returns
|
||||
* `terNO_RIPPLE` only when both the `from` and the `to` trustlines have
|
||||
* `lsfNoRipple` set on the issuer's side, blocking rippling through. If a
|
||||
* trustline does not exist for a given account, the issuer's
|
||||
* `lsfDefaultRipple` flag is consulted as a fallback preference.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param issue The IOU (identifies the issuer and currency).
|
||||
* @param from The sending account.
|
||||
* @param to The receiving account.
|
||||
* @return `tesSUCCESS` if the transfer is permitted, `terNO_RIPPLE` if
|
||||
* rippling is disabled on both sides.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
canTransfer(ReadView const& view, Issue const& issue, AccountID const& from, AccountID const& to);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// Empty holding operations (IOU-specific)
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
// --- Empty holding operations (IOU-specific) ---
|
||||
|
||||
/// Any transactors that call addEmptyHolding() in doApply must call
|
||||
/// canAddHolding() in preflight with the same View and Asset
|
||||
/** Create a zero-balance trustline for @p accountID, reserving the destination
|
||||
* slot before any funds arrive.
|
||||
*
|
||||
* Used by transactors (e.g., DEX limit orders) that need to guarantee a
|
||||
* destination line exists before settlement. Checks that @p accountID can
|
||||
* cover the increased owner-count reserve before calling `trustCreate`.
|
||||
*
|
||||
* Returns `tesSUCCESS` immediately for XRP or when `accountID` is the
|
||||
* issuer. Returns `tecDUPLICATE` if the trustline already exists.
|
||||
*
|
||||
* @note Any transactor that calls this function in `doApply` **must** call
|
||||
* `canAddHolding()` (declared in `TokenHelpers.h`) in `preflight` with
|
||||
* the same view and asset to validate the reserve precondition.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param accountID The account that will hold the IOU.
|
||||
* @param priorBalance The account's XRP balance before the current
|
||||
* transaction, used to test reserve sufficiency.
|
||||
* @param issue The IOU to create a holding for.
|
||||
* @param journal Journal for trace/debug logging.
|
||||
* @return `tesSUCCESS` on success; `tecFROZEN` if the issuer is globally
|
||||
* frozen; `tecNO_LINE_INSUF_RESERVE` if the account cannot afford the
|
||||
* reserve; `tecDUPLICATE` if the line already exists; or a `tec`/`tef`
|
||||
* code from `trustCreate`.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
addEmptyHolding(
|
||||
ApplyView& view,
|
||||
@@ -234,6 +477,20 @@ addEmptyHolding(
|
||||
Issue const& issue,
|
||||
beast::Journal journal);
|
||||
|
||||
/** Delete a zero-balance trustline previously created by `addEmptyHolding`.
|
||||
*
|
||||
* Validates that the balance is actually zero before deletion. Adjusts
|
||||
* owner counts for both the low and high sides if their reserve flags are
|
||||
* set, then calls `trustDelete`.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param accountID The account whose holding line should be removed.
|
||||
* @param issue The IOU identifying the trustline to remove.
|
||||
* @param journal Journal for trace/debug logging.
|
||||
* @return `tesSUCCESS` on success; `tecHAS_OBLIGATIONS` if the balance is
|
||||
* non-zero; `tecOBJECT_NOT_FOUND` if no line exists (and the account
|
||||
* is not the issuer); or a `tef`/`tec` code from `trustDelete`.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
removeEmptyHolding(
|
||||
ApplyView& view,
|
||||
@@ -241,9 +498,27 @@ removeEmptyHolding(
|
||||
Issue const& issue,
|
||||
beast::Journal journal);
|
||||
|
||||
/** Delete trustline to AMM. The passed `sle` must be obtained from a prior
|
||||
* call to view.peek(). Fail if neither side of the trustline is AMM or
|
||||
* if ammAccountID is seated and is not one of the trustline's side.
|
||||
/** Delete a trustline owned by an AMM pool account during AMM withdrawal.
|
||||
*
|
||||
* Validates that:
|
||||
* - @p sleState is a non-null `ltRIPPLE_STATE` SLE.
|
||||
* - Exactly one of the two trustline endpoints is an AMM account
|
||||
* (identified by the presence of `sfAMMID` in the `AccountRoot`).
|
||||
* - If @p ammAccountID is provided, it matches one of the endpoints.
|
||||
*
|
||||
* On success, calls `trustDelete` and decrements the owner count of the
|
||||
* non-AMM side.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param sleState The `ltRIPPLE_STATE` SLE to delete; must be obtained
|
||||
* from `view.peek()`.
|
||||
* @param ammAccountID If provided, the expected AMM account ID; the
|
||||
* function returns `terNO_AMM` if neither endpoint matches.
|
||||
* @param j Journal for trace/debug logging.
|
||||
* @return `tesSUCCESS` on success; `tecINTERNAL` if the SLE is null, has
|
||||
* the wrong type, if both sides are AMM, or if the reserve flag is
|
||||
* unexpectedly absent; `terNO_AMM` if neither endpoint is an AMM or
|
||||
* the optional ID does not match; or a `tef` code from `trustDelete`.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
deleteAMMTrustLine(
|
||||
@@ -252,8 +527,19 @@ deleteAMMTrustLine(
|
||||
std::optional<AccountID> const& ammAccountID,
|
||||
beast::Journal j);
|
||||
|
||||
/** Delete AMMs MPToken. The passed `sle` must be obtained from a prior
|
||||
* call to view.peek().
|
||||
/** Delete an AMM account's `MPToken` SLE during AMM withdrawal.
|
||||
*
|
||||
* Removes the `MPToken` SLE from @p ammAccountID's owner directory and
|
||||
* erases it from the view. The caller is responsible for any balance
|
||||
* assertions before invoking this function.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param sleMPT The `MPToken` SLE to delete; must be obtained from
|
||||
* `view.peek()`.
|
||||
* @param ammAccountID The AMM account that owns the `MPToken`.
|
||||
* @param j Journal for trace/debug logging.
|
||||
* @return `tesSUCCESS` on success, `tefBAD_LEDGER` if the directory removal
|
||||
* fails (indicating ledger corruption).
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
deleteAMMMPToken(
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/** @file
|
||||
* Asset-agnostic dispatcher layer for all token operations on the XRP Ledger.
|
||||
*
|
||||
* This header is the unified entry point for token operations that must work
|
||||
* across XRPL's three asset classes: XRP, IOU (trust-line-based), and MPT
|
||||
* (Multi-Party Token). It sits between transaction-processing code that wants
|
||||
* to be asset-agnostic and the two type-specific leaf modules:
|
||||
* `RippleStateHelpers.h` for IOU trust lines and `MPTokenHelpers.h` for
|
||||
* `MPToken`/`MPTokenIssuance` objects.
|
||||
*
|
||||
* Callers pass an `Asset` — a `std::variant<Issue, MPTIssue>` — and the
|
||||
* functions here dispatch via `std::visit` or `Asset::visit` to the correct
|
||||
* lower-level function, returning consistent result types (`STAmount`, `TER`,
|
||||
* `bool`) regardless of asset kind. Adding a new asset type requires only
|
||||
* extending the `Asset` variant and the branches here, not modifying call
|
||||
* sites.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
@@ -20,30 +37,83 @@ namespace xrpl {
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Controls the treatment of frozen account balances */
|
||||
enum class FreezeHandling { IgnoreFreeze, ZeroIfFrozen };
|
||||
|
||||
/** Controls the treatment of unauthorized MPT balances */
|
||||
enum class AuthHandling { IgnoreAuth, ZeroIfUnauthorized };
|
||||
|
||||
/** Controls whether to include the account's full spendable balance */
|
||||
enum class SpendableHandling { SimpleBalance, FullBalance };
|
||||
|
||||
enum class WaiveTransferFee : bool { No = false, Yes };
|
||||
|
||||
/** Controls whether accountSend is allowed to overflow OutstandingAmount **/
|
||||
enum class AllowMPTOverflow : bool { No = false, Yes };
|
||||
|
||||
/* Check if MPToken (for MPT) or trust line (for IOU) exists:
|
||||
* - StrongAuth - before checking if authorization is required
|
||||
* - WeakAuth
|
||||
* for MPT - after checking lsfMPTRequireAuth flag
|
||||
* for IOU - do not check if trust line exists
|
||||
* - Legacy
|
||||
* for MPT - before checking lsfMPTRequireAuth flag i.e. same as StrongAuth
|
||||
* for IOU - do not check if trust line exists i.e. same as WeakAuth
|
||||
/** Controls how a frozen balance is reported by balance-query functions.
|
||||
*
|
||||
* Use `ZeroIfFrozen` in payment paths where a frozen balance must not be
|
||||
* spent. Use `IgnoreFreeze` in cleanup paths that need the real value
|
||||
* regardless of freeze state.
|
||||
*/
|
||||
enum class AuthType { StrongAuth, WeakAuth, Legacy };
|
||||
enum class FreezeHandling {
|
||||
IgnoreFreeze, /**< Return the actual balance even if the holding is frozen. */
|
||||
ZeroIfFrozen /**< Return zero when the holding is frozen (the spendable amount). */
|
||||
};
|
||||
|
||||
/** Controls how an unauthorized MPT balance is reported by balance-query functions.
|
||||
*
|
||||
* Parallel to `FreezeHandling` but for MPT authorization. Use
|
||||
* `ZeroIfUnauthorized` when computing the amount an account may legally spend.
|
||||
*/
|
||||
enum class AuthHandling {
|
||||
IgnoreAuth, /**< Return the actual balance even if the MPToken is unauthorized. */
|
||||
ZeroIfUnauthorized /**< Return zero when the MPToken is not authorized. */
|
||||
};
|
||||
|
||||
/** Controls whether `accountHolds` reports simple or full spendable balance.
|
||||
*
|
||||
* - `SimpleBalance`: the amount the account can spend without going into
|
||||
* debt, i.e. the raw trustline balance (negated to account-centric terms)
|
||||
* for IOU, or the `sfMPTAmount` for MPT.
|
||||
* - `FullBalance`: for IOU, also includes the peer's credit limit so the
|
||||
* account can borrow up to that limit; for the IOU issuer, returns
|
||||
* `STAmount::kMAX_VALUE`; for the MPT issuer, returns
|
||||
* `MaximumAmount - OutstandingAmount`.
|
||||
*/
|
||||
enum class SpendableHandling {
|
||||
SimpleBalance, /**< Balance the account can spend without going into debt. */
|
||||
FullBalance /**< Full spendable balance including borrowable credit or issuance capacity. */
|
||||
};
|
||||
|
||||
/** Controls whether the transfer fee is skipped during a send operation.
|
||||
*
|
||||
* Typed as `enum class : bool` to prevent accidental transposition with
|
||||
* other boolean parameters at call sites.
|
||||
*/
|
||||
enum class WaiveTransferFee : bool {
|
||||
No = false, /**< Apply the normal transfer fee. */
|
||||
Yes /**< Skip the transfer fee entirely. */
|
||||
};
|
||||
|
||||
/** Controls whether `accountSend` permits `OutstandingAmount` to transiently
|
||||
* exceed `MaximumAmount` during MPT payment-engine routing.
|
||||
*
|
||||
* The payment engine issues tokens first (raising `OutstandingAmount`) and
|
||||
* redeems them in the same transaction (lowering it back). `Yes` raises the
|
||||
* overflow ceiling to `UINT64_MAX` for that transient window. Direct sends
|
||||
* use `No` and enforce the strict `MaximumAmount` cap.
|
||||
*/
|
||||
enum class AllowMPTOverflow : bool {
|
||||
No = false, /**< Enforce the strict MaximumAmount cap. */
|
||||
Yes /**< Allow transient overflow up to UINT64_MAX during routing. */
|
||||
};
|
||||
|
||||
/** Encodes the three-way authorization-strictness contract.
|
||||
*
|
||||
* Determines how `requireAuth` behaves when checking whether an account may
|
||||
* hold or interact with a token:
|
||||
* - `StrongAuth` checks that the holding object (trust line or `MPToken`)
|
||||
* exists *before* asking whether authorization is set. Returns `tecNO_LINE`
|
||||
* immediately if no holding exists.
|
||||
* - `WeakAuth` skips the existence check, returning `tesSUCCESS` when
|
||||
* authorization is not required even if no holding exists. Appropriate for
|
||||
* payment path-finding where a line may be created on the fly.
|
||||
* - `Legacy` maps to `StrongAuth` for MPT and `WeakAuth` for IOU, preserving
|
||||
* historical behavior at existing call sites.
|
||||
*/
|
||||
enum class AuthType {
|
||||
StrongAuth, /**< Existence of the holding object is verified first. */
|
||||
WeakAuth, /**< Holding existence is not required when auth is not needed. */
|
||||
Legacy /**< StrongAuth for MPT; WeakAuth for IOU (historical default). */
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
@@ -51,35 +121,126 @@ enum class AuthType { StrongAuth, WeakAuth, Legacy };
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Check whether the issuer of @p asset has activated a global freeze.
|
||||
*
|
||||
* Dispatches to the IOU or MPT leaf based on the runtime type of @p asset.
|
||||
* A global freeze on the issuer's `AccountRoot` blocks all holders
|
||||
* simultaneously.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param asset The asset to test.
|
||||
* @return `true` if the issuer has a global freeze in effect.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isGlobalFrozen(ReadView const& view, Asset const& asset);
|
||||
|
||||
/** Check whether @p account has an individual freeze on @p asset.
|
||||
*
|
||||
* Dispatches to the IOU or MPT leaf based on the runtime type of @p asset.
|
||||
* For IOU, checks the issuer's per-line freeze flag. For MPT, checks the
|
||||
* `lsfMPTLocked` flag on the `MPToken` SLE. Does not check global freeze.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param asset The asset to test.
|
||||
* @return `true` if the issuer has set an individual freeze on this account.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isIndividualFrozen(ReadView const& view, AccountID const& account, Asset const& asset);
|
||||
|
||||
/**
|
||||
* isFrozen check is recursive for MPT shares in a vault, descending to
|
||||
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
|
||||
* purely defensive, as we currently do not allow such vaults to be created.
|
||||
/** Check whether @p account is frozen for @p asset (global or individual).
|
||||
*
|
||||
* Returns `true` if either `isGlobalFrozen` or `isIndividualFrozen` is true
|
||||
* for the given account and asset. Dispatches to the typed IOU or MPT leaf
|
||||
* via `std::visit`.
|
||||
*
|
||||
* The `depth` parameter enables recursive vault checking: if @p asset is an
|
||||
* MPT backed by a vault, the vault's underlying asset is checked up to
|
||||
* `maxAssetCheckDepth` levels deep.
|
||||
*
|
||||
* @note Recursion is purely defensive. The ledger currently does not allow
|
||||
* nested vaults to be created, so `depth > 0` should not occur in
|
||||
* practice.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param asset The asset to test.
|
||||
* @param depth Current recursion depth for vault checking; defaults to 0.
|
||||
* @return `true` if the account cannot move this asset due to any freeze.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isFrozen(ReadView const& view, AccountID const& account, Asset const& asset, int depth = 0);
|
||||
|
||||
/** Convert a freeze check on an IOU to a `TER`.
|
||||
*
|
||||
* Returns `tecFROZEN` if `isFrozen` is true for the given account and issue,
|
||||
* `tesSUCCESS` otherwise.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param issue The IOU to test.
|
||||
* @return `tecFROZEN` if frozen, `tesSUCCESS` otherwise.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkFrozen(ReadView const& view, AccountID const& account, Issue const& issue);
|
||||
|
||||
/** Convert a freeze check on an MPT to a `TER`.
|
||||
*
|
||||
* Returns `tecLOCKED` (not `tecFROZEN`) if `isFrozen` is true for the given
|
||||
* account and MPT issuance, `tesSUCCESS` otherwise. The distinct error code
|
||||
* reflects the separate protocol semantics of MPT locking vs IOU freezing.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param mptIssue The MPT issuance to test.
|
||||
* @return `tecLOCKED` if frozen/locked, `tesSUCCESS` otherwise.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);
|
||||
|
||||
/** Convert a freeze check on any asset to a `TER`.
|
||||
*
|
||||
* Dispatches to `checkFrozen(…, Issue)` or `checkFrozen(…, MPTIssue)` based
|
||||
* on the runtime type of @p asset, returning the type-appropriate error code
|
||||
* (`tecFROZEN` for IOU, `tecLOCKED` for MPT).
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param asset The asset to test.
|
||||
* @return `tecFROZEN` (IOU) or `tecLOCKED` (MPT) if frozen, `tesSUCCESS`
|
||||
* otherwise.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkFrozen(ReadView const& view, AccountID const& account, Asset const& asset);
|
||||
|
||||
/** Check whether any account in @p accounts is frozen for @p issue.
|
||||
*
|
||||
* Iterates the list and returns `true` on the first frozen account. Used to
|
||||
* check both sides (taker and maker) of an offer with a single call.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param accounts The accounts to test, e.g. `{takerID, makerID}`.
|
||||
* @param issue The IOU to test.
|
||||
* @return `true` if any account in the list is frozen for @p issue.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isAnyFrozen(
|
||||
ReadView const& view,
|
||||
std::initializer_list<AccountID> const& accounts,
|
||||
Issue const& issue);
|
||||
|
||||
/** Check whether any account in @p accounts is frozen for @p asset.
|
||||
*
|
||||
* Asset-dispatching overload. Delegates to the IOU or MPT leaf for each
|
||||
* account in the list. The `depth` parameter passes through to `isFrozen`
|
||||
* for vault-backed MPT recursion.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param accounts The accounts to test.
|
||||
* @param asset The asset to test.
|
||||
* @param depth Recursion depth for vault checking; defaults to 0.
|
||||
* @return `true` if any account in the list is frozen for @p asset.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isAnyFrozen(
|
||||
ReadView const& view,
|
||||
@@ -87,6 +248,22 @@ isAnyFrozen(
|
||||
Asset const& asset,
|
||||
int depth = 0);
|
||||
|
||||
/** Check whether @p account is deep-frozen for @p mptIssue.
|
||||
*
|
||||
* For MPT, deep-freeze semantics are identical to regular freeze: a frozen
|
||||
* MPT holder cannot send or receive. This function delegates to
|
||||
* `isFrozen(view, account, mptIssue, depth)`.
|
||||
*
|
||||
* @note For IOU, deep-freeze is a distinct state (`lsfDeepFreeze`) where the
|
||||
* holder cannot send but can still receive. See `isDeepFrozen` in
|
||||
* `RippleStateHelpers.h` for IOU-specific semantics.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param mptIssue The MPT issuance to test.
|
||||
* @param depth Recursion depth for vault checking; defaults to 0.
|
||||
* @return `true` if the account is frozen/locked for this MPT.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isDeepFrozen(
|
||||
ReadView const& view,
|
||||
@@ -94,17 +271,51 @@ isDeepFrozen(
|
||||
MPTIssue const& mptIssue,
|
||||
int depth = 0);
|
||||
|
||||
/**
|
||||
* isFrozen check is recursive for MPT shares in a vault, descending to
|
||||
* assets in the vault, up to maxAssetCheckDepth recursion depth. This is
|
||||
* purely defensive, as we currently do not allow such vaults to be created.
|
||||
/** Check whether @p account is deep-frozen for @p asset.
|
||||
*
|
||||
* Dispatches to the IOU or MPT leaf via `std::visit`. For MPT, deep-freeze
|
||||
* is equivalent to regular freeze. For IOU, checks the `lsfDeepFreeze` flag,
|
||||
* which prevents sending but allows receiving.
|
||||
*
|
||||
* The `depth` parameter enables recursive vault checking up to
|
||||
* `maxAssetCheckDepth` levels.
|
||||
*
|
||||
* @note Recursion is purely defensive — nested vaults cannot currently be
|
||||
* created on the ledger.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param asset The asset to test.
|
||||
* @param depth Recursion depth for vault checking; defaults to 0.
|
||||
* @return `true` if the account is deep-frozen for @p asset.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset, int depth = 0);
|
||||
|
||||
/** Convert a deep-freeze check on an MPT to a `TER`.
|
||||
*
|
||||
* Returns `tecLOCKED` if `isDeepFrozen` is true, `tesSUCCESS` otherwise.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param mptIssue The MPT issuance to test.
|
||||
* @return `tecLOCKED` if deep-frozen, `tesSUCCESS` otherwise.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkDeepFrozen(ReadView const& view, AccountID const& account, MPTIssue const& mptIssue);
|
||||
|
||||
/** Convert a deep-freeze check on any asset to a `TER`.
|
||||
*
|
||||
* Dispatches to `checkDeepFrozen(…, Issue)` (`tecFROZEN`) or
|
||||
* `checkDeepFrozen(…, MPTIssue)` (`tecLOCKED`) based on the runtime type of
|
||||
* @p asset.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account to test.
|
||||
* @param asset The asset to test.
|
||||
* @return `tecFROZEN` (IOU) or `tecLOCKED` (MPT) if deep-frozen,
|
||||
* `tesSUCCESS` otherwise.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& asset);
|
||||
|
||||
@@ -114,19 +325,31 @@ checkDeepFrozen(ReadView const& view, AccountID const& account, Asset const& ass
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Returns the amount an account can spend.
|
||||
//
|
||||
// If shSIMPLE_BALANCE is specified, this is the amount the account can spend
|
||||
// without going into debt.
|
||||
//
|
||||
// If shFULL_BALANCE is specified, this is the amount the account can spend
|
||||
// total. Specifically:
|
||||
// * The account can go into debt if using a trust line, and the other side has
|
||||
// a non-zero limit.
|
||||
// * If the account is the asset issuer the limit is defined by the asset /
|
||||
// issuance.
|
||||
//
|
||||
// <-- saAmount: amount of currency held by account. May be negative.
|
||||
/** Return the amount that @p account can spend of the given currency/issuer.
|
||||
*
|
||||
* This is the canonical implementation. All other `accountHolds` overloads
|
||||
* ultimately delegate here for the IOU path.
|
||||
*
|
||||
* - For XRP: returns `xrpLiquid(view, account, 0, j)` (reserve-adjusted).
|
||||
* - For IOU with `shFULL_BALANCE` when `account == issuer`: returns
|
||||
* `STAmount::kMAX_VALUE` — the issuer has effectively unlimited issuance
|
||||
* capacity.
|
||||
* - For IOU otherwise: reads the trust-line balance from the ledger,
|
||||
* negating it to account-centric terms. If `shFULL_BALANCE` is specified,
|
||||
* also adds the peer's credit limit so the account can draw down that
|
||||
* credit. Returns zero if the line is frozen (when `ZeroIfFrozen`) or does
|
||||
* not exist.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account whose balance is queried.
|
||||
* @param currency The IOU currency.
|
||||
* @param issuer The IOU issuer.
|
||||
* @param zeroIfFrozen Whether to return zero for frozen balances.
|
||||
* @param j Journal for trace logging.
|
||||
* @param includeFullBalance Whether to include borrowable credit or max
|
||||
* issuance capacity; defaults to `SimpleBalance`.
|
||||
* @return The spendable balance, which may be negative (e.g. trust-line debt).
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
accountHolds(
|
||||
ReadView const& view,
|
||||
@@ -137,6 +360,19 @@ accountHolds(
|
||||
beast::Journal j,
|
||||
SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance);
|
||||
|
||||
/** Return the spendable balance of an IOU for @p account.
|
||||
*
|
||||
* Convenience adapter over the `(Currency, AccountID)` overload, extracting
|
||||
* the currency and issuer from @p issue.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account whose balance is queried.
|
||||
* @param issue The IOU (currency + issuer).
|
||||
* @param zeroIfFrozen Whether to return zero for frozen balances.
|
||||
* @param j Journal for trace logging.
|
||||
* @param includeFullBalance Balance mode; defaults to `SimpleBalance`.
|
||||
* @return The spendable balance from @p account's perspective.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
accountHolds(
|
||||
ReadView const& view,
|
||||
@@ -146,6 +382,29 @@ accountHolds(
|
||||
beast::Journal j,
|
||||
SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance);
|
||||
|
||||
/** Return the spendable balance of an MPT for @p account.
|
||||
*
|
||||
* - For the MPT issuer with `shFULL_BALANCE`: returns
|
||||
* `MaximumAmount - OutstandingAmount` (available issuance capacity) via
|
||||
* `availableMPTAmount`.
|
||||
* - For regular holders: reads `sfMPTAmount` from the `MPToken` SLE. Returns
|
||||
* zero if: the `MPToken` SLE does not exist; the token is frozen and
|
||||
* `ZeroIfFrozen` is set; or the token is unauthorized and
|
||||
* `ZeroIfUnauthorized` is set (with `featureSingleAssetVault` gating the
|
||||
* precise auth-check path).
|
||||
* - Under `featureMPTokensV2`, the result passes through
|
||||
* `view.balanceHookMPT` to allow `PaymentSandbox` deferred-credit
|
||||
* interception.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account whose balance is queried.
|
||||
* @param mptIssue The MPT issuance.
|
||||
* @param zeroIfFrozen Whether to zero the balance when frozen/locked.
|
||||
* @param zeroIfUnauthorized Whether to zero the balance when unauthorized.
|
||||
* @param j Journal for trace logging.
|
||||
* @param includeFullBalance Balance mode; defaults to `SimpleBalance`.
|
||||
* @return The spendable MPT balance, or zero per the policy flags above.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
accountHolds(
|
||||
ReadView const& view,
|
||||
@@ -156,6 +415,22 @@ accountHolds(
|
||||
beast::Journal j,
|
||||
SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance);
|
||||
|
||||
/** Return the spendable balance of any asset for @p account.
|
||||
*
|
||||
* Asset-dispatching overload. Delegates to the `Issue` overload (which
|
||||
* ignores `zeroIfUnauthorized`) or the `MPTIssue` overload based on the
|
||||
* runtime type of @p asset.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param account The account whose balance is queried.
|
||||
* @param asset The asset to query.
|
||||
* @param zeroIfFrozen Whether to zero the balance when frozen.
|
||||
* @param zeroIfUnauthorized Whether to zero the balance when unauthorized
|
||||
* (MPT only; ignored for IOU).
|
||||
* @param j Journal for trace logging.
|
||||
* @param includeFullBalance Balance mode; defaults to `SimpleBalance`.
|
||||
* @return The spendable balance per the policy flags.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
accountHolds(
|
||||
ReadView const& view,
|
||||
@@ -166,11 +441,29 @@ accountHolds(
|
||||
beast::Journal j,
|
||||
SpendableHandling includeFullBalance = SpendableHandling::SimpleBalance);
|
||||
|
||||
// Returns the amount an account can spend of the currency type saDefault, or
|
||||
// returns saDefault if this account is the issuer of the currency in
|
||||
// question. Should be used in favor of accountHolds when questioning how much
|
||||
// an account can spend while also allowing currency issuers to spend
|
||||
// unlimited amounts of their own currency (since they can always issue more).
|
||||
/** Return how much of @p saDefault's currency @p id can fund, treating the
|
||||
* issuer as having unlimited supply of their own currency.
|
||||
*
|
||||
* For IOU: if `id == saDefault.getIssuer()`, returns `saDefault` directly —
|
||||
* the issuer can always fund an offer for their own currency up to whatever
|
||||
* amount they specify. Otherwise delegates to `accountHolds` with
|
||||
* `SimpleBalance`.
|
||||
*
|
||||
* This is the correct semantic for offer matching; prefer `accountFunds` over
|
||||
* `accountHolds` when asking "can this account fund this offer?".
|
||||
*
|
||||
* @note `saDefault` must hold an `Issue` (not MPT). Use the `AuthHandling`
|
||||
* overload for asset-agnostic callers.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param id The account to query.
|
||||
* @param saDefault The amount (currency + issuer) to check fundability
|
||||
* for.
|
||||
* @param freezeHandling Whether to zero the balance when frozen.
|
||||
* @param j Journal for trace logging.
|
||||
* @return `saDefault` if @p id is the issuer; otherwise the trust-line
|
||||
* balance, zeroed per @p freezeHandling.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
accountFunds(
|
||||
ReadView const& view,
|
||||
@@ -179,7 +472,22 @@ accountFunds(
|
||||
FreezeHandling freezeHandling,
|
||||
beast::Journal j);
|
||||
|
||||
// Overload with AuthHandling to support IOU and MPT.
|
||||
/** Asset-agnostic overload of `accountFunds` supporting both IOU and MPT.
|
||||
*
|
||||
* For IOU: delegates to the `FreezeHandling`-only overload above.
|
||||
* For MPT: delegates to `accountHolds` with `shFULL_BALANCE`, which
|
||||
* returns the issuer's available issuance capacity or the holder's
|
||||
* `sfMPTAmount`.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param id The account to query.
|
||||
* @param saDefault The amount (currency/asset + issuer) to check.
|
||||
* @param freezeHandling Whether to zero the balance when frozen.
|
||||
* @param authHandling Whether to zero the balance when unauthorized (MPT
|
||||
* only).
|
||||
* @param j Journal for trace logging.
|
||||
* @return The fundable balance per the policy flags.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
accountFunds(
|
||||
ReadView const& view,
|
||||
@@ -189,9 +497,15 @@ accountFunds(
|
||||
AuthHandling authHandling,
|
||||
beast::Journal j);
|
||||
|
||||
/** Returns the transfer fee as Rate based on the type of token
|
||||
* @param view The ledger view
|
||||
* @param amount The amount to transfer
|
||||
/** Return the transfer fee for the asset embedded in @p amount.
|
||||
*
|
||||
* Dispatches on `amount.asset()`: for IOU, reads the issuer's transfer rate
|
||||
* from their `AccountRoot`; for MPT, reads the `sfTransferFee` field from
|
||||
* the `MPTokenIssuance` SLE. Both paths return a `Rate` (parts-per-billion).
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param amount The amount whose asset determines which fee to look up.
|
||||
* @return The transfer fee as a `Rate`, or `parityRate` if no fee is set.
|
||||
*/
|
||||
[[nodiscard]] Rate
|
||||
transferRate(ReadView const& view, STAmount const& amount);
|
||||
@@ -202,9 +516,42 @@ transferRate(ReadView const& view, STAmount const& amount);
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Check whether a new holding object (trust line or MPToken) can be created.
|
||||
*
|
||||
* For IOU: verifies that the issuer's `AccountRoot` has `lsfDefaultRipple`
|
||||
* set; returns `terNO_RIPPLE` if not, `terNO_ACCOUNT` if the issuer does not
|
||||
* exist, `tesSUCCESS` for XRP. For MPT: delegates to the MPT-specific check.
|
||||
*
|
||||
* @note This function is read-only (takes `ReadView`) and is intended to be
|
||||
* called during `preflight`. Any transactor that calls `addEmptyHolding`
|
||||
* in `doApply` must call this function in `preflight` first.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param asset The asset for which a holding would be created.
|
||||
* @return `tesSUCCESS` if a holding can be added; `terNO_RIPPLE`,
|
||||
* `terNO_ACCOUNT`, or an MPT-specific error otherwise.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
canAddHolding(ReadView const& view, Asset const& asset);
|
||||
|
||||
/** Create an empty holding object (trust line or MPToken) for @p accountID.
|
||||
*
|
||||
* Dispatches to `addEmptyHolding(…, Issue)` or `addEmptyHolding(…, MPTIssue)`
|
||||
* based on the runtime type of @p asset. The holding is created with zero
|
||||
* balance and consumes an owner-count reserve slot.
|
||||
*
|
||||
* @note The caller must have invoked `canAddHolding` in `preflight` with the
|
||||
* same view and asset to validate preconditions before calling this.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param accountID The account that will hold the asset.
|
||||
* @param priorBalance The account's XRP balance before this transaction,
|
||||
* used to test reserve sufficiency.
|
||||
* @param asset The asset to create a holding for.
|
||||
* @param journal Journal for trace/debug logging.
|
||||
* @return `tesSUCCESS` on success, or a `tec`/`tef` error from the
|
||||
* type-specific leaf.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
addEmptyHolding(
|
||||
ApplyView& view,
|
||||
@@ -213,6 +560,21 @@ addEmptyHolding(
|
||||
Asset const& asset,
|
||||
beast::Journal journal);
|
||||
|
||||
/** Delete a zero-balance holding object (trust line or MPToken) for @p accountID.
|
||||
*
|
||||
* Dispatches to `removeEmptyHolding(…, Issue)` or
|
||||
* `removeEmptyHolding(…, MPTIssue)` based on the runtime type of @p asset.
|
||||
* The holding must have a zero balance; a non-zero balance returns
|
||||
* `tecHAS_OBLIGATIONS`.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param accountID The account whose holding should be removed.
|
||||
* @param asset The asset identifying the holding to remove.
|
||||
* @param journal Journal for trace/debug logging.
|
||||
* @return `tesSUCCESS` on success; `tecHAS_OBLIGATIONS` if the balance is
|
||||
* non-zero; `tecOBJECT_NOT_FOUND` if no holding exists; or a `tec`/`tef`
|
||||
* error from the type-specific leaf.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
removeEmptyHolding(
|
||||
ApplyView& view,
|
||||
@@ -226,6 +588,25 @@ removeEmptyHolding(
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Check whether @p account is authorized to hold or interact with @p asset.
|
||||
*
|
||||
* Dispatches to `requireAuth(…, Issue, …)` or `requireAuth(…, MPTIssue, …)`
|
||||
* based on the runtime type of @p asset.
|
||||
*
|
||||
* - `StrongAuth`: verifies the holding object exists first; returns
|
||||
* `tecNO_LINE` (IOU) or `tecNO_AUTH` (MPT) if absent.
|
||||
* - `WeakAuth`: skips the existence check; returns success if authorization
|
||||
* is not required even when no holding exists.
|
||||
* - `Legacy`: maps to `StrongAuth` for MPT and `WeakAuth` for IOU to
|
||||
* preserve historical behavior.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param asset The asset to check authorization for.
|
||||
* @param account The account to check.
|
||||
* @param authType Authorization strictness; defaults to `AuthType::Legacy`.
|
||||
* @return `tesSUCCESS`, `tecNO_AUTH`, or `tecNO_LINE` depending on the asset
|
||||
* type and authorization state.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
requireAuth(
|
||||
ReadView const& view,
|
||||
@@ -233,6 +614,20 @@ requireAuth(
|
||||
AccountID const& account,
|
||||
AuthType authType = AuthType::Legacy);
|
||||
|
||||
/** Check whether @p asset can be transferred from @p from to @p to.
|
||||
*
|
||||
* Dispatches to the IOU or MPT leaf. For IOU, checks rippling flags on the
|
||||
* trustlines (returns `terNO_RIPPLE` if both sides block rippling). For MPT,
|
||||
* checks `lsfMPTCanTransfer` on the issuance and the destination's
|
||||
* authorization state.
|
||||
*
|
||||
* @param view Read-only ledger view.
|
||||
* @param asset The asset to transfer.
|
||||
* @param from The sending account.
|
||||
* @param to The receiving account.
|
||||
* @return `tesSUCCESS` if the transfer is permitted, or an asset-specific
|
||||
* error (`terNO_RIPPLE`, `tecNO_AUTH`, etc.) otherwise.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
canTransfer(ReadView const& view, Asset const& asset, AccountID const& from, AccountID const& to);
|
||||
|
||||
@@ -242,14 +637,29 @@ canTransfer(ReadView const& view, Asset const& asset, AccountID const& from, Acc
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Direct send w/o fees:
|
||||
// - Redeeming IOUs and/or sending sender's own IOUs.
|
||||
// - Create trust line of needed.
|
||||
// --> bCheckIssuer : normally require issuer to be involved.
|
||||
// [[nodiscard]] // nodiscard commented out so DirectStep.cpp compiles.
|
||||
|
||||
/** Calls static directSendNoFeeIOU if saAmount represents Issue.
|
||||
* Calls static directSendNoFeeMPT if saAmount represents MPTIssue.
|
||||
/** Send @p saAmount directly without applying transfer fees or limit checks.
|
||||
*
|
||||
* Used for IOU redemption, intra-issuer transfers, and MPT moves where the
|
||||
* issuer is one of the endpoints. Dispatches to `directSendNoFeeIOU` for
|
||||
* IOU and `directSendNoFeeMPT` for MPT.
|
||||
*
|
||||
* For IOU, @p bCheckIssuer controls whether the function asserts that the
|
||||
* issuer is one of the endpoints. For MPT, the issuer check is not performed
|
||||
* (`bCheckIssuer` must be `false` for MPT).
|
||||
*
|
||||
* @note This function is intentionally **not** marked `[[nodiscard]]` for
|
||||
* compatibility with `DirectStep.cpp`, which discards the return value in
|
||||
* certain control paths. All other callers should inspect the result.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param uSenderID The sending account.
|
||||
* @param uReceiverID The receiving account.
|
||||
* @param saAmount The amount to send; its asset determines the dispatch.
|
||||
* @param bCheckIssuer If `true` (IOU only), asserts that the issuer is one
|
||||
* of the endpoints. Must be `false` for MPT.
|
||||
* @param j Journal for trace/debug logging.
|
||||
* @return `tesSUCCESS` on success, or a `tec`/`tef` error from the
|
||||
* type-specific leaf.
|
||||
*/
|
||||
TER
|
||||
directSendNoFee(
|
||||
@@ -260,8 +670,30 @@ directSendNoFee(
|
||||
bool bCheckIssuer,
|
||||
beast::Journal j);
|
||||
|
||||
/** Calls static accountSendIOU if saAmount represents Issue.
|
||||
* Calls static accountSendMPT if saAmount represents MPTIssue.
|
||||
/** Send @p saAmount from @p from to @p to, applying transfer fees when
|
||||
* applicable.
|
||||
*
|
||||
* This is the main asset-transfer entry point for transactors. Dispatches to
|
||||
* `accountSendIOU` or `accountSendMPT` based on the asset type embedded in
|
||||
* @p saAmount. Transfer fees are applied unless `WaiveTransferFee::Yes` is
|
||||
* passed.
|
||||
*
|
||||
* The `allowOverflow` flag is forwarded to the MPT path only and controls
|
||||
* whether `OutstandingAmount` may transiently exceed `MaximumAmount` during
|
||||
* the two-phase issue-then-redeem structure used by the payment engine. Direct
|
||||
* sends should use `AllowMPTOverflow::No`.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param from The sending account.
|
||||
* @param to The receiving account.
|
||||
* @param saAmount The amount to send.
|
||||
* @param j Journal for trace/debug logging.
|
||||
* @param waiveFee Whether to skip the transfer fee; defaults to `No`.
|
||||
* @param allowOverflow Whether MPT OutstandingAmount may transiently exceed
|
||||
* MaximumAmount; defaults to `No`. Use `Yes` only in payment-engine
|
||||
* routing.
|
||||
* @return `tesSUCCESS` on success, or a `tec`/`tef` error from the
|
||||
* type-specific leaf.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
accountSend(
|
||||
@@ -273,12 +705,34 @@ accountSend(
|
||||
WaiveTransferFee waiveFee = WaiveTransferFee::No,
|
||||
AllowMPTOverflow allowOverflow = AllowMPTOverflow::No);
|
||||
|
||||
/** A vector of (receiver, amount) pairs used by `accountSendMulti`. */
|
||||
using MultiplePaymentDestinations = std::vector<std::pair<AccountID, Number>>;
|
||||
/** Like accountSend, except one account is sending multiple payments (with the
|
||||
* same asset!) simultaneously
|
||||
|
||||
/** Send the same @p asset from @p senderID to multiple @p receivers in one
|
||||
* atomic operation.
|
||||
*
|
||||
* Calls static accountSendMultiIOU if saAmount represents Issue.
|
||||
* Calls static accountSendMultiMPT if saAmount represents MPTIssue.
|
||||
* Dispatches to `accountSendMultiIOU` or `accountSendMultiMPT` based on
|
||||
* @p asset. Batching avoids repeated round-trips through the ledger state for
|
||||
* the sender's balance and the issuance's `OutstandingAmount` field.
|
||||
*
|
||||
* For MPT, the `fixCleanup3_1_3` amendment switches the aggregate
|
||||
* `MaximumAmount` check from a per-iteration stale-snapshot check (pre-fix)
|
||||
* to an exact `uint64_t` running-total check (post-fix) to prevent precision
|
||||
* loss at 19-digit magnitudes near `kMAX_MP_TOKEN_AMOUNT`.
|
||||
*
|
||||
* @note `receivers.size()` must be greater than 1 (asserted).
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param senderID The account sending the asset.
|
||||
* @param asset The asset to send (must match the type of all receiver
|
||||
* amounts).
|
||||
* @param receivers List of (AccountID, Number) destination pairs. All amounts
|
||||
* must be non-negative. Sender-equals-receiver entries are silently
|
||||
* skipped.
|
||||
* @param j Journal for trace/debug logging.
|
||||
* @param waiveFee Whether to skip transfer fees; defaults to `No`.
|
||||
* @return `tesSUCCESS` on success, or a `tec`/`tef` error from the
|
||||
* type-specific leaf.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
accountSendMulti(
|
||||
@@ -289,6 +743,23 @@ accountSendMulti(
|
||||
beast::Journal j,
|
||||
WaiveTransferFee waiveFee = WaiveTransferFee::No);
|
||||
|
||||
/** Transfer XRP directly between two accounts without reserve or fee checks.
|
||||
*
|
||||
* XRP has no trust lines, no transfer fees, and no authorization model, so
|
||||
* it bypasses the Asset-dispatch path entirely. Both @p from and @p to must
|
||||
* be non-zero and distinct. Returns `telFAILED_PROCESSING` (open ledger) or
|
||||
* `tecFAILED_PROCESSING` (closed ledger) if the sender's balance is
|
||||
* insufficient.
|
||||
*
|
||||
* @param view Mutable ledger view.
|
||||
* @param from The sending account; must not be `beast::kZERO`.
|
||||
* @param to The receiving account; must not be `beast::kZERO`.
|
||||
* @param amount The XRP amount to transfer; must be native (XRP).
|
||||
* @param j Journal for trace/debug logging.
|
||||
* @return `tesSUCCESS` on success; `telFAILED_PROCESSING` or
|
||||
* `tecFAILED_PROCESSING` if balance is insufficient; `tefINTERNAL` if
|
||||
* either account SLE cannot be found.
|
||||
*/
|
||||
[[nodiscard]] TER
|
||||
transferXRP(
|
||||
ApplyView& view,
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/** @file
|
||||
* Pure arithmetic helpers for the XLS-65d Single-Sided Vault feature.
|
||||
*
|
||||
* Each function converts between the two token types a vault manages:
|
||||
* the underlying *asset* (XRP, IOU, or MPT that depositors contribute) and
|
||||
* vault *shares* (an MPT representing proportional ownership). Because MPT
|
||||
* values are always integers every function makes an explicit rounding
|
||||
* decision — and those decisions differ between the deposit and withdrawal
|
||||
* paths to protect vault solvency.
|
||||
*
|
||||
* These functions are stateless and side-effect-free; all ledger mutations
|
||||
* are the caller's responsibility.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
@@ -8,53 +21,105 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** From the perspective of a vault, return the number of shares to give
|
||||
depositor when they offer a fixed amount of assets. Note, since shares are
|
||||
MPT, this number is integral and always truncated in this calculation.
|
||||
|
||||
@param vault The vault SLE.
|
||||
@param issuance The MPTokenIssuance SLE for the vault's shares.
|
||||
@param assets The amount of assets to convert.
|
||||
|
||||
@return The number of shares, or nullopt on error.
|
||||
*/
|
||||
/** Compute the shares minted when a depositor offers a fixed asset amount.
|
||||
*
|
||||
* Uses `sfAssetsTotal` from `vault` directly, *without* subtracting
|
||||
* `sfLossUnrealized`. Unrealized losses are a risk borne by existing
|
||||
* shareholders, not a discount for new depositors.
|
||||
*
|
||||
* **Bootstrap case**: when `sfAssetsTotal == 0` the result is
|
||||
* `assets × 10^sfScale` (truncated), establishing the initial exchange rate.
|
||||
* The non-bootstrap result is `(sfOutstandingAmount × assets) / sfAssetsTotal`,
|
||||
* always truncated — depositors always receive a whole number of shares, never
|
||||
* more than the assets strictly warrant.
|
||||
*
|
||||
* @note The deposit transactor calls this first, then back-calculates the
|
||||
* true asset cost via `sharesToAssetsDeposit()` to ensure it never
|
||||
* extracts more than the depositor offered.
|
||||
* @throws std::overflow_error if `sfScale` is large enough to overflow
|
||||
* XRPL's `Number` type; callers should catch and return `tecPATH_DRY`.
|
||||
*
|
||||
* @param vault The vault SLE; must contain `sfAsset`, `sfAssetsTotal`,
|
||||
* `sfScale`, and `sfShareMPTID`.
|
||||
* @param issuance The MPTokenIssuance SLE for the vault's share token;
|
||||
* must contain `sfOutstandingAmount`.
|
||||
* @param assets The asset amount to convert; must be non-negative and
|
||||
* must match `vault->at(sfAsset)`.
|
||||
* @return The integral share amount, or `nullopt` if `assets` is negative
|
||||
* or its asset type does not match the vault.
|
||||
*/
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesDeposit(
|
||||
std::shared_ptr<SLE const> const& vault,
|
||||
std::shared_ptr<SLE const> const& issuance,
|
||||
STAmount const& assets);
|
||||
|
||||
/** From the perspective of a vault, return the number of assets to take from
|
||||
depositor when they receive a fixed amount of shares. Note, since shares are
|
||||
MPT, they are always an integral number.
|
||||
|
||||
@param vault The vault SLE.
|
||||
@param issuance The MPTokenIssuance SLE for the vault's shares.
|
||||
@param shares The amount of shares to convert.
|
||||
|
||||
@return The number of assets, or nullopt on error.
|
||||
*/
|
||||
/** Compute the asset cost for a depositor who will receive a fixed share amount.
|
||||
*
|
||||
* This is the inverse of `assetsToSharesDeposit()` and is used in the second
|
||||
* step of the deposit calculation: after truncating the forward direction to
|
||||
* determine how many whole shares are created, the transactor calls this
|
||||
* function to derive the exact asset amount to collect.
|
||||
*
|
||||
* Uses `sfAssetsTotal` directly, without subtracting `sfLossUnrealized`,
|
||||
* matching the deposit-path convention.
|
||||
*
|
||||
* **Bootstrap case**: when `sfAssetsTotal == 0` the result uses `sfScale` to
|
||||
* reverse the bootstrap formula applied by `assetsToSharesDeposit()`.
|
||||
*
|
||||
* @throws std::overflow_error if `sfScale` is large enough to overflow
|
||||
* XRPL's `Number` type; callers should catch and return `tecPATH_DRY`.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param issuance The MPTokenIssuance SLE for the vault's share token.
|
||||
* @param shares The share amount to convert; must be non-negative and must
|
||||
* match `vault->at(sfShareMPTID)`.
|
||||
* @return The asset amount, or `nullopt` if `shares` is negative or its
|
||||
* asset type does not match the vault's share MPT.
|
||||
*/
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
sharesToAssetsDeposit(
|
||||
std::shared_ptr<SLE const> const& vault,
|
||||
std::shared_ptr<SLE const> const& issuance,
|
||||
STAmount const& shares);
|
||||
|
||||
/** Controls whether to truncate shares instead of rounding. */
|
||||
/** Controls whether to truncate (floor) the share result instead of rounding.
|
||||
*
|
||||
* `No` (the default) rounds to nearest, ensuring the vault is never
|
||||
* shortchanged when computing shares to redeem for a fixed asset withdrawal.
|
||||
* `Yes` applies floor truncation, used when the caller explicitly needs
|
||||
* conservative (depositor-favoring) rounding.
|
||||
*/
|
||||
enum class TruncateShares : bool { No = false, Yes = true };
|
||||
|
||||
/** From the perspective of a vault, return the number of shares to demand from
|
||||
the depositor when they ask to withdraw a fixed amount of assets. Since
|
||||
shares are MPT this number is integral, and it will be rounded to nearest
|
||||
unless explicitly requested to be truncated instead.
|
||||
|
||||
@param vault The vault SLE.
|
||||
@param issuance The MPTokenIssuance SLE for the vault's shares.
|
||||
@param assets The amount of assets to convert.
|
||||
@param truncate Whether to truncate instead of rounding.
|
||||
|
||||
@return The number of shares, or nullopt on error.
|
||||
*/
|
||||
/** Compute the shares a withdrawer must redeem to receive a fixed asset amount.
|
||||
*
|
||||
* Unlike the deposit path, this function subtracts `sfLossUnrealized` from
|
||||
* `sfAssetsTotal` before computing the exchange rate. Withdrawers receive fewer
|
||||
* assets per share when the vault has recorded unrealized losses, preventing
|
||||
* early withdrawers from exiting at inflated prices at the expense of remaining
|
||||
* holders.
|
||||
*
|
||||
* The result is rounded to nearest by default (`TruncateShares::No`), ensuring
|
||||
* the vault is not shortchanged. The withdraw transactor then back-calculates
|
||||
* the actual assets delivered via `sharesToAssetsWithdraw()` for a precise
|
||||
* two-step computation.
|
||||
*
|
||||
* If `sfAssetsTotal - sfLossUnrealized == 0` (fully insolvent vault), returns
|
||||
* a zero-valued `STAmount` rather than dividing by zero.
|
||||
*
|
||||
* @throws std::overflow_error if arithmetic overflows XRPL's `Number` type;
|
||||
* callers should catch and return `tecPATH_DRY`.
|
||||
*
|
||||
* @param vault The vault SLE; must contain `sfAsset`, `sfAssetsTotal`,
|
||||
* `sfLossUnrealized`, and `sfShareMPTID`.
|
||||
* @param issuance The MPTokenIssuance SLE for the vault's share token.
|
||||
* @param assets The asset amount to convert; must be non-negative and must
|
||||
* match `vault->at(sfAsset)`.
|
||||
* @param truncate Whether to truncate instead of rounding to nearest.
|
||||
* @return The integral share amount, or `nullopt` if `assets` is negative or
|
||||
* its asset type does not match the vault.
|
||||
*/
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
assetsToSharesWithdraw(
|
||||
std::shared_ptr<SLE const> const& vault,
|
||||
@@ -62,16 +127,25 @@ assetsToSharesWithdraw(
|
||||
STAmount const& assets,
|
||||
TruncateShares truncate = TruncateShares::No);
|
||||
|
||||
/** From the perspective of a vault, return the number of assets to give the
|
||||
depositor when they redeem a fixed amount of shares. Note, since shares are
|
||||
MPT, they are always an integral number.
|
||||
|
||||
@param vault The vault SLE.
|
||||
@param issuance The MPTokenIssuance SLE for the vault's shares.
|
||||
@param shares The amount of shares to convert.
|
||||
|
||||
@return The number of assets, or nullopt on error.
|
||||
*/
|
||||
/** Compute the assets delivered when a withdrawer redeems a fixed share amount.
|
||||
*
|
||||
* Like `assetsToSharesWithdraw()`, this function subtracts `sfLossUnrealized`
|
||||
* from `sfAssetsTotal` before computing the exchange rate, so withdrawers
|
||||
* bear their proportional share of any recorded losses.
|
||||
*
|
||||
* If `sfAssetsTotal - sfLossUnrealized == 0` (fully insolvent vault), returns
|
||||
* a zero-valued `STAmount` rather than dividing by zero.
|
||||
*
|
||||
* @throws std::overflow_error if arithmetic overflows XRPL's `Number` type;
|
||||
* callers should catch and return `tecPATH_DRY`.
|
||||
*
|
||||
* @param vault The vault SLE.
|
||||
* @param issuance The MPTokenIssuance SLE for the vault's share token.
|
||||
* @param shares The share amount to convert; must be non-negative and must
|
||||
* match `vault->at(sfShareMPTID)`.
|
||||
* @return The asset amount, or `nullopt` if `shares` is negative or its
|
||||
* asset type does not match the vault's share MPT.
|
||||
*/
|
||||
[[nodiscard]] std::optional<STAmount>
|
||||
sharesToAssetsWithdraw(
|
||||
std::shared_ptr<SLE const> const& vault,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -2,20 +2,57 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
* @brief Enumeration of ledger shortcuts for specifying which ledger to use.
|
||||
/** Symbolic names for the three canonical XRPL ledger states.
|
||||
*
|
||||
* These shortcuts provide a convenient way to reference commonly used ledgers
|
||||
* without needing to specify their exact hash or sequence number.
|
||||
* The XRPL consensus model maintains three distinct ledger states at any
|
||||
* point in time. Rather than requiring callers to pass magic strings
|
||||
* (`"current"`, `"closed"`, `"validated"`) or ad-hoc integer sentinels,
|
||||
* `LedgerShortcut` gives the type system a precise vocabulary for expressing
|
||||
* ledger-selection intent without a specific sequence number or hash.
|
||||
*
|
||||
* In `RPCLedgerHelpers.cpp`, `lookupLedger` parsing maps the JSON strings
|
||||
* `"current"`, `"closed"`, and `"validated"` onto the corresponding enum
|
||||
* values before dispatching to the appropriate `getLedger` overload. The
|
||||
* `AccountTx` RPC handler performs the same mapping when processing the
|
||||
* `ledger_index` field. The gRPC adapter maps protobuf shortcut constants to
|
||||
* these values as well.
|
||||
*
|
||||
* `LedgerShortcut` also participates as one arm of
|
||||
* `RelationalDatabase::LedgerSpecifier` — a
|
||||
* `std::variant<LedgerRange, LedgerShortcut, LedgerSequence, LedgerHash>` —
|
||||
* allowing symbolic ledger names to flow through the database query layer via
|
||||
* `std::visit` dispatch without special-case handling.
|
||||
*
|
||||
* @note The scoped `enum class` form prevents implicit integer conversions and
|
||||
* namespace pollution, both of which are hazards in a codebase that also
|
||||
* works extensively with raw integer ledger sequence numbers.
|
||||
*/
|
||||
enum class LedgerShortcut {
|
||||
/** The current working ledger (open, not yet closed) */
|
||||
/** The open, in-progress ledger still accumulating new transactions.
|
||||
*
|
||||
* This ledger has not been closed or validated, so its contents may
|
||||
* change. Results derived from it are not final and may be rolled back
|
||||
* during a reorganisation or consensus failure.
|
||||
*/
|
||||
Current,
|
||||
|
||||
/** The most recently closed ledger (may not be validated) */
|
||||
/** The most recently closed ledger; stable in structure but not yet
|
||||
* consensus-validated.
|
||||
*
|
||||
* No new transactions are accepted into this ledger, but the network has
|
||||
* not yet confirmed it as the authoritative chain tip. It is more stable
|
||||
* than `Current` but still not suitable for finality guarantees.
|
||||
*/
|
||||
Closed,
|
||||
|
||||
/** The most recently validated ledger */
|
||||
/** The most recently validated ledger; the fully consensus-confirmed chain
|
||||
* tip.
|
||||
*
|
||||
* This is the only state considered immutable and trustworthy for finality
|
||||
* purposes. An RPC node that cannot provide a fresh validated ledger
|
||||
* (i.e., it is stale) will return an error rather than serve potentially
|
||||
* incorrect data.
|
||||
*/
|
||||
Validated
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/** @file
|
||||
* Defines MPTAmount, the canonical signed-integer amount type for
|
||||
* Multi-Purpose Tokens (MPTs) on the XRP Ledger.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/Number.h>
|
||||
@@ -13,12 +18,40 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Typed signed-integer quantity for Multi-Purpose Tokens (MPTs).
|
||||
*
|
||||
* MPT balances are plain whole-unit integers — no mantissa/exponent pair,
|
||||
* no sub-unit naming — capped at `maxMPTokenAmount` (INT64_MAX) by the
|
||||
* protocol. The class sits alongside `XRPAmount` and `IOUAmount` as one
|
||||
* of the three concrete amount types that satisfy the `StepAmount` concept
|
||||
* used by the payment-path and DEX engines.
|
||||
*
|
||||
* Arithmetic operators are composed via Boost.Operators (CRTP):
|
||||
* - `boost::totally_ordered<MPTAmount>` — synthesizes `!=`, `>`, `>=`,
|
||||
* `<=` from the declared `==` and `<`.
|
||||
* - `boost::additive<MPTAmount>` — synthesizes binary `+`/`-` from
|
||||
* `+=`/`-=`.
|
||||
* - `boost::equality_comparable<MPTAmount, int64_t>` — heterogeneous `!=`
|
||||
* from `operator==(value_type)`.
|
||||
* - `boost::additive<MPTAmount, int64_t>` — heterogeneous `+`/`-` with
|
||||
* raw integers.
|
||||
*
|
||||
* Out-of-line `+=`, `-=`, `operator-()`, `==`, and `<` perform no overflow
|
||||
* detection; callers are responsible for keeping balances in range through
|
||||
* the ledger constraint machinery. The safe multiplication path
|
||||
* (`mulRatio`) uses 128-bit intermediates and throws on overflow.
|
||||
*
|
||||
* @note `value_` is `protected` (not `private`) to allow subclassing
|
||||
* without exposing the raw integer to unrelated code. No subclasses
|
||||
* exist in the current codebase.
|
||||
*/
|
||||
class MPTAmount : private boost::totally_ordered<MPTAmount>,
|
||||
private boost::additive<MPTAmount>,
|
||||
private boost::equality_comparable<MPTAmount, std::int64_t>,
|
||||
private boost::additive<MPTAmount, std::int64_t>
|
||||
{
|
||||
public:
|
||||
/** Underlying integer type; matches `XRPAmount::value_type`. */
|
||||
using value_type = std::int64_t;
|
||||
|
||||
protected:
|
||||
@@ -27,57 +60,149 @@ protected:
|
||||
public:
|
||||
MPTAmount() = default;
|
||||
constexpr MPTAmount(MPTAmount const& other) = default;
|
||||
|
||||
/** Construct a zero amount from the `beast::Zero` sentinel.
|
||||
*
|
||||
* Allows idiomatic zero-initialization via `beast::zero` in generic
|
||||
* code that is templated on amount type.
|
||||
*/
|
||||
constexpr MPTAmount(beast::Zero);
|
||||
constexpr MPTAmount&
|
||||
operator=(MPTAmount const& other) = default;
|
||||
|
||||
// Round to nearest, even on tie.
|
||||
/** Construct from a `Number`, rounding to nearest with ties to even.
|
||||
*
|
||||
* Provides implicit compatibility with XRPL's high-precision arithmetic
|
||||
* type. The rounding mode matches IEEE 754 default (round-half-to-even).
|
||||
*
|
||||
* @param x The `Number` value to convert.
|
||||
*/
|
||||
explicit MPTAmount(Number const& x) : MPTAmount(static_cast<value_type>(x))
|
||||
{
|
||||
}
|
||||
|
||||
/** Construct from a raw `int64_t` value.
|
||||
*
|
||||
* Explicit to prevent accidental implicit conversion from integers.
|
||||
* The caller is responsible for ensuring `value` does not exceed
|
||||
* `maxMPTokenAmount` (INT64_MAX).
|
||||
*
|
||||
* @param value The integer amount in whole MPT units.
|
||||
*/
|
||||
constexpr explicit MPTAmount(value_type value);
|
||||
|
||||
/** Assign the `beast::Zero` sentinel, setting the amount to zero. */
|
||||
constexpr MPTAmount& operator=(beast::Zero);
|
||||
|
||||
/** Add `other` to this amount in place.
|
||||
*
|
||||
* No overflow detection is performed; callers must ensure the result
|
||||
* remains within `int64_t` range.
|
||||
*
|
||||
* @param other The amount to add.
|
||||
* @return Reference to `*this` after addition.
|
||||
*/
|
||||
MPTAmount&
|
||||
operator+=(MPTAmount const& other);
|
||||
|
||||
/** Subtract `other` from this amount in place.
|
||||
*
|
||||
* No overflow detection is performed; callers must ensure the result
|
||||
* remains within `int64_t` range.
|
||||
*
|
||||
* @param other The amount to subtract.
|
||||
* @return Reference to `*this` after subtraction.
|
||||
*/
|
||||
MPTAmount&
|
||||
operator-=(MPTAmount const& other);
|
||||
|
||||
/** Return the arithmetic negation of this amount.
|
||||
*
|
||||
* Used where a credit and a debit are expressed as equal-magnitude
|
||||
* amounts of opposite sign before being applied to the ledger.
|
||||
* Negating `INT64_MIN` is undefined behavior; callers must avoid it.
|
||||
*
|
||||
* @return A new `MPTAmount` equal to `-value_`.
|
||||
*/
|
||||
MPTAmount
|
||||
operator-() const;
|
||||
|
||||
/** Test equality with another `MPTAmount`.
|
||||
*
|
||||
* Together with `operator<`, satisfies `boost::totally_ordered`,
|
||||
* from which `!=`, `>`, `<=`, and `>=` are synthesized.
|
||||
*
|
||||
* @param other The amount to compare against.
|
||||
* @return `true` if both amounts hold the same integer value.
|
||||
*/
|
||||
bool
|
||||
operator==(MPTAmount const& other) const;
|
||||
|
||||
/** Test equality with a raw `int64_t` value.
|
||||
*
|
||||
* Allows expressions like `amt == 0` without constructing a temporary.
|
||||
* `boost::equality_comparable<MPTAmount, int64_t>` synthesizes the
|
||||
* mixed-type `!=` from this overload.
|
||||
*
|
||||
* @param other The raw integer value to compare against.
|
||||
* @return `true` if `value_` equals `other`.
|
||||
*/
|
||||
bool
|
||||
operator==(value_type other) const;
|
||||
|
||||
/** Return `true` if this amount is strictly less than `other`.
|
||||
*
|
||||
* The single total-order primitive from which `boost::totally_ordered`
|
||||
* derives `>`, `<=`, and `>=`. Signed comparison gives correct
|
||||
* semantics for negative balances.
|
||||
*
|
||||
* @param other The amount to compare against.
|
||||
* @return `true` if `value_` is strictly less than `other.value_`.
|
||||
*/
|
||||
bool
|
||||
operator<(MPTAmount const& other) const;
|
||||
|
||||
/** Returns true if the amount is not zero */
|
||||
/** Returns true if the amount is not zero. */
|
||||
explicit constexpr
|
||||
operator bool() const noexcept;
|
||||
|
||||
/** Implicit conversion to `Number` for use in high-precision arithmetic.
|
||||
*
|
||||
* Allows `MPTAmount` to be passed anywhere a `Number` is expected —
|
||||
* arithmetic operations, rounding, and comparisons — without an explicit
|
||||
* cast. The reverse direction (construction from `Number`) is explicit.
|
||||
*/
|
||||
operator Number() const noexcept
|
||||
{
|
||||
return value();
|
||||
}
|
||||
|
||||
/** Return the sign of the amount */
|
||||
/** Return the sign of the amount.
|
||||
*
|
||||
* @return `-1` if negative, `0` if zero, `1` if positive.
|
||||
*/
|
||||
[[nodiscard]] constexpr int
|
||||
signum() const noexcept;
|
||||
|
||||
/** Returns the underlying value. Code SHOULD NOT call this
|
||||
function unless the type has been abstracted away,
|
||||
e.g. in a templated function.
|
||||
*/
|
||||
/** Return the underlying integer value.
|
||||
*
|
||||
* Code SHOULD NOT call this function unless the type has been abstracted
|
||||
* away, e.g. in a templated function. Prefer operating on `MPTAmount`
|
||||
* directly to keep arithmetic in the typed domain.
|
||||
*
|
||||
* @return The raw `int64_t` balance in whole MPT units.
|
||||
*/
|
||||
[[nodiscard]] constexpr value_type
|
||||
value() const;
|
||||
|
||||
/** Return the smallest positive MPT amount (one indivisible unit).
|
||||
*
|
||||
* Provides a uniform factory interface shared with `XRPAmount` and
|
||||
* `IOUAmount` so generic payment-path code can obtain the minimum
|
||||
* step size without knowing the concrete amount type.
|
||||
*
|
||||
* @return `MPTAmount{1}`.
|
||||
*/
|
||||
static MPTAmount
|
||||
minPositiveAmount();
|
||||
};
|
||||
@@ -98,14 +223,12 @@ MPTAmount::operator=(beast::Zero)
|
||||
return *this;
|
||||
}
|
||||
|
||||
/** Returns true if the amount is not zero */
|
||||
constexpr MPTAmount::
|
||||
operator bool() const noexcept
|
||||
{
|
||||
return value_ != 0;
|
||||
}
|
||||
|
||||
/** Return the sign of the amount */
|
||||
constexpr int
|
||||
MPTAmount::signum() const noexcept
|
||||
{
|
||||
@@ -114,17 +237,13 @@ MPTAmount::signum() const noexcept
|
||||
return (value_ != 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/** Returns the underlying value. Code SHOULD NOT call this
|
||||
function unless the type has been abstracted away,
|
||||
e.g. in a templated function.
|
||||
*/
|
||||
constexpr MPTAmount::value_type
|
||||
MPTAmount::value() const
|
||||
{
|
||||
return value_;
|
||||
}
|
||||
|
||||
// Output MPTAmount as just the value.
|
||||
/** Stream an `MPTAmount` as its raw integer value. */
|
||||
template <class Char, class Traits>
|
||||
std::basic_ostream<Char, Traits>&
|
||||
operator<<(std::basic_ostream<Char, Traits>& os, MPTAmount const& q)
|
||||
@@ -132,12 +251,35 @@ operator<<(std::basic_ostream<Char, Traits>& os, MPTAmount const& q)
|
||||
return os << q.value();
|
||||
}
|
||||
|
||||
/** Return the decimal string representation of an `MPTAmount`. */
|
||||
inline std::string
|
||||
to_string(MPTAmount const& amount)
|
||||
{
|
||||
return std::to_string(amount.value());
|
||||
}
|
||||
|
||||
/** Compute `amt * num / den` with configurable rounding direction.
|
||||
*
|
||||
* The intermediate product is computed in 128-bit arithmetic to avoid
|
||||
* overflow when multiplying a 63-bit MPT balance by a 32-bit numerator
|
||||
* (up to 95 bits required). After division, any remainder is resolved
|
||||
* based on the sign of `amt` and `roundUp`:
|
||||
* - Positive amounts round up when `roundUp` is `true`.
|
||||
* - Negative amounts round away from zero (more negative) when `roundUp`
|
||||
* is `false`.
|
||||
*
|
||||
* Used for fee and reserve calculations that apply percentage-style ratios
|
||||
* to MPT amounts.
|
||||
*
|
||||
* @param amt The base amount to scale.
|
||||
* @param num Numerator of the ratio (32-bit unsigned).
|
||||
* @param den Denominator of the ratio (32-bit unsigned, must be > 0).
|
||||
* @param roundUp If `true`, round the result toward positive infinity;
|
||||
* if `false`, round toward negative infinity.
|
||||
* @return The scaled `MPTAmount`.
|
||||
* @throws std::runtime_error If `den` is zero.
|
||||
* @throws std::overflow_error If the result exceeds `INT64_MAX`.
|
||||
*/
|
||||
inline MPTAmount
|
||||
mulRatio(MPTAmount const& amt, std::uint32_t num, std::uint32_t den, bool roundUp)
|
||||
{
|
||||
|
||||
@@ -5,9 +5,23 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/* Adapt MPTID to provide the same interface as Issue. Enables using static
|
||||
* polymorphism by Asset and other classes. MPTID is a 192-bit concatenation
|
||||
* of a 32-bit account sequence and a 160-bit account id.
|
||||
/** Identifies a Multi-Purpose Token issuance, adapting `MPTID` to mirror
|
||||
* the public interface of `Issue`.
|
||||
*
|
||||
* `MPTIssue` wraps a single 192-bit `MPTID` (32-bit big-endian sequence
|
||||
* concatenated with a 160-bit `AccountID`) and exposes the same accessors as
|
||||
* `Issue` — `getIssuer()`, `getText()`, `setJson()`, `native()`, and
|
||||
* `integral()` — allowing `Asset` and any template code constrained by
|
||||
* `ValidIssueType` to treat MPTs and IOUs uniformly.
|
||||
*
|
||||
* Key semantic differences from `Issue`:
|
||||
* - `native()` always returns `false` (MPTs are never the native currency).
|
||||
* - `integral()` always returns `true` (MPT amounts are 64-bit integers,
|
||||
* unlike IOUs which use multi-precision rational arithmetic).
|
||||
* - Equality and ordering compare the full 192-bit `MPTID`, with no
|
||||
* special-case equivalence class for any sentinel value.
|
||||
*
|
||||
* @see Issue, Asset, MPTID
|
||||
*/
|
||||
class MPTIssue
|
||||
{
|
||||
@@ -17,27 +31,80 @@ private:
|
||||
public:
|
||||
MPTIssue() = default;
|
||||
|
||||
/** Constructs an MPTIssue from a pre-formed 192-bit issuance identifier.
|
||||
*
|
||||
* @param issuanceID The packed MPTID (32-bit sequence ‖ 160-bit AccountID).
|
||||
*/
|
||||
MPTIssue(MPTID const& issuanceID);
|
||||
|
||||
/** Constructs an MPTIssue from the raw components of an MPTID.
|
||||
*
|
||||
* Delegates to `xrpl::makeMptID(sequence, account)` to assemble the
|
||||
* packed MPTID, saving callers from invoking that helper explicitly.
|
||||
*
|
||||
* @param sequence The issuer's account sequence number at the time of
|
||||
* issuance.
|
||||
* @param account The AccountID of the issuer.
|
||||
*/
|
||||
MPTIssue(std::uint32_t sequence, AccountID const& account);
|
||||
|
||||
/** Implicit conversion to the underlying `MPTID`.
|
||||
*
|
||||
* Allows an `MPTIssue` to be passed wherever a raw `MPTID` is expected
|
||||
* without an explicit cast.
|
||||
*
|
||||
* @return A reference to the underlying `MPTID`, valid for the lifetime
|
||||
* of this object.
|
||||
*/
|
||||
operator MPTID const&() const
|
||||
{
|
||||
return mptID_;
|
||||
}
|
||||
|
||||
/** Extracts the issuer's `AccountID` from the packed `MPTID`.
|
||||
*
|
||||
* `MPTID` lays out 4 bytes of sequence followed immediately by 20 bytes
|
||||
* of `AccountID`. This method returns a reference into that buffer by
|
||||
* pointer-casting past the leading 4 bytes. A `static_assert` on the
|
||||
* total size of `MPTID` guards against layout changes breaking this
|
||||
* assumption at compile time.
|
||||
*
|
||||
* @return A reference to the `AccountID` embedded in the `MPTID`.
|
||||
* Valid for the lifetime of this `MPTIssue` object.
|
||||
* @note The free function `getMPTIssuer()` achieves the same extraction
|
||||
* via `std::bit_cast` and returns by value, avoiding any
|
||||
* lifetime dependency on the source object.
|
||||
*/
|
||||
[[nodiscard]] AccountID const&
|
||||
getIssuer() const;
|
||||
|
||||
/** Returns the underlying `MPTID`.
|
||||
*
|
||||
* @return A reference to the 192-bit issuance identifier, valid for the
|
||||
* lifetime of this object.
|
||||
*/
|
||||
[[nodiscard]] constexpr MPTID const&
|
||||
getMptID() const
|
||||
{
|
||||
return mptID_;
|
||||
}
|
||||
|
||||
/** Returns the hex string representation of the underlying `MPTID`.
|
||||
*
|
||||
* @return Uppercase hex encoding of the 192-bit issuance identifier.
|
||||
*/
|
||||
[[nodiscard]] std::string
|
||||
getText() const;
|
||||
|
||||
/** Writes the issuance identifier into a JSON object under the key
|
||||
* `mpt_issuance_id`.
|
||||
*
|
||||
* Serializes the `MPTID` as a hex string. Contrasted with
|
||||
* `Issue::setJson()`, which writes separate `currency` and `issuer` keys.
|
||||
*
|
||||
* @param jv Output JSON object to write into; existing keys are not
|
||||
* cleared.
|
||||
*/
|
||||
void
|
||||
setJson(json::Value& jv) const;
|
||||
|
||||
@@ -47,12 +114,30 @@ public:
|
||||
friend constexpr std::weak_ordering
|
||||
operator<=>(MPTIssue const& lhs, MPTIssue const& rhs);
|
||||
|
||||
/** Returns `false`; MPTs are never the native asset (XRP).
|
||||
*
|
||||
* Mirrors `Issue::native()` so that generic code can query XRP-ness
|
||||
* without a type dispatch. `Asset::getAmountType()` relies on this flag
|
||||
* to select `XRPAmount` vs `IOUAmount` vs `MPTAmount` at compile time.
|
||||
*
|
||||
* @return Always `false`.
|
||||
*/
|
||||
static bool
|
||||
native()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Returns `true`; MPT amounts are stored as 64-bit integers.
|
||||
*
|
||||
* Mirrors the naming of `Issue::integral()` so that generic code can
|
||||
* distinguish integer (drop/MPT) amounts from multi-precision IOU
|
||||
* amounts without a type dispatch. Unlike `Issue::integral()`, this is
|
||||
* unconditionally `true` — all MPTs use integer arithmetic regardless of
|
||||
* the token configuration.
|
||||
*
|
||||
* @return Always `true`.
|
||||
*/
|
||||
static bool
|
||||
integral()
|
||||
{
|
||||
@@ -60,19 +145,44 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
/** Returns `true` if two `MPTIssue` instances represent the same issuance.
|
||||
*
|
||||
* Delegates to the full 192-bit comparison of the underlying `MPTID`s.
|
||||
* Both the sequence number and the issuer `AccountID` must match; there is
|
||||
* no partial-equality exemption as there is in `Issue::operator==` for XRP.
|
||||
*
|
||||
* @param lhs Left-hand issuance.
|
||||
* @param rhs Right-hand issuance.
|
||||
* @return `true` iff both `MPTID`s are bitwise equal.
|
||||
*/
|
||||
constexpr bool
|
||||
operator==(MPTIssue const& lhs, MPTIssue const& rhs)
|
||||
{
|
||||
return lhs.mptID_ == rhs.mptID_;
|
||||
}
|
||||
|
||||
/** Provides a strict weak ordering over `MPTIssue` values.
|
||||
*
|
||||
* Delegates to the 192-bit lexicographic comparison of the underlying
|
||||
* `MPTID`s. The ordering is consistent with `operator==`: two issuances are
|
||||
* equivalent iff their full `MPTID`s are identical.
|
||||
*
|
||||
* @param lhs Left-hand issuance.
|
||||
* @param rhs Right-hand issuance.
|
||||
* @return A `std::weak_ordering` value consistent with `operator==`.
|
||||
*/
|
||||
constexpr std::weak_ordering
|
||||
operator<=>(MPTIssue const& lhs, MPTIssue const& rhs)
|
||||
{
|
||||
return lhs.mptID_ <=> rhs.mptID_;
|
||||
}
|
||||
|
||||
/** MPT is a non-native token.
|
||||
/** Returns `false`; an `MPTID` never identifies the native XRP asset.
|
||||
*
|
||||
* Provides the same naming convention as `isXRP(Issue)`, allowing call
|
||||
* sites to test XRP-ness uniformly across both issue types.
|
||||
*
|
||||
* @return Always `false`.
|
||||
*/
|
||||
inline bool
|
||||
isXRP(MPTID const&)
|
||||
@@ -80,6 +190,21 @@ isXRP(MPTID const&)
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Extracts the issuer `AccountID` from a `MPTID` by value.
|
||||
*
|
||||
* Copies the 20 bytes that follow the leading 4-byte sequence field into a
|
||||
* temporary array, then uses `std::bit_cast` to reinterpret them as an
|
||||
* `AccountID`. The `static_assert` on the total size of `MPTID` ensures the
|
||||
* layout assumption holds; if the type ever gains padding the build fails.
|
||||
* `std::bit_cast` is typically optimized to nothing in the final assembly.
|
||||
*
|
||||
* @param mptid The 192-bit issuance identifier to extract from.
|
||||
* @return The `AccountID` embedded in bytes 4–23 of `mptid`.
|
||||
* @note Use `MPTIssue::getIssuer()` when a zero-copy reference into an
|
||||
* existing `MPTIssue` object is sufficient. The rvalue overloads of
|
||||
* this function are deleted to prevent dangling references from
|
||||
* temporaries.
|
||||
*/
|
||||
inline AccountID
|
||||
getMPTIssuer(MPTID const& mptid)
|
||||
{
|
||||
@@ -93,12 +218,21 @@ getMPTIssuer(MPTID const& mptid)
|
||||
return std::bit_cast<AccountID>(bytes);
|
||||
}
|
||||
|
||||
// Disallow temporary
|
||||
// Deleted to prevent a dangling-reference bug: if a temporary MPTID were
|
||||
// accepted, the returned AccountID const& would immediately dangle.
|
||||
AccountID const&
|
||||
getMPTIssuer(MPTID const&&) = delete;
|
||||
AccountID const&
|
||||
getMPTIssuer(MPTID&&) = delete;
|
||||
|
||||
/** Returns the `MPTID` sentinel representing "no MPT".
|
||||
*
|
||||
* Encodes `{ sequence=0, account=noAccount() }` — all-zero bits.
|
||||
* Mirrors `noIssue()` in `Issue.h` for use in contexts where a
|
||||
* missing or invalid MPT must be represented without `std::optional`.
|
||||
*
|
||||
* @return The all-zero 192-bit sentinel `MPTID`.
|
||||
*/
|
||||
inline MPTID
|
||||
noMPT()
|
||||
{
|
||||
@@ -106,6 +240,15 @@ noMPT()
|
||||
return kMPT.getMptID();
|
||||
}
|
||||
|
||||
/** Returns the `MPTID` sentinel representing a structurally invalid MPT.
|
||||
*
|
||||
* Encodes `{ sequence=0, account=xrpAccount() }` — sequence zero with the
|
||||
* XRP account address as issuer, which is a conventionally invalid issuer
|
||||
* for MPTs. `Asset`'s `BadAsset` comparison detects this sentinel by
|
||||
* checking `getIssuer() == xrpAccount()`.
|
||||
*
|
||||
* @return The sentinel `MPTID` whose issuer is `xrpAccount()`.
|
||||
*/
|
||||
inline MPTID
|
||||
badMPT()
|
||||
{
|
||||
@@ -113,6 +256,15 @@ badMPT()
|
||||
return kMPT.getMptID();
|
||||
}
|
||||
|
||||
/** Appends the underlying `MPTID` to a hasher.
|
||||
*
|
||||
* Plugs `MPTIssue` into the Beast hashing framework, enabling use in
|
||||
* Beast-aware hash maps and sets.
|
||||
*
|
||||
* @tparam Hasher A type satisfying the `beast::hash_append` concept.
|
||||
* @param h The hasher to append to.
|
||||
* @param r The issuance whose `MPTID` is appended.
|
||||
*/
|
||||
template <class Hasher>
|
||||
void
|
||||
hash_append(Hasher& h, MPTIssue const& r)
|
||||
@@ -121,15 +273,49 @@ hash_append(Hasher& h, MPTIssue const& r)
|
||||
hash_append(h, r.getMptID());
|
||||
}
|
||||
|
||||
/** Returns the canonical wire-format JSON representation of an MPT issuance.
|
||||
*
|
||||
* Convenience wrapper around `MPTIssue::setJson()`. The returned object
|
||||
* contains a single `mpt_issuance_id` field with the hex-encoded `MPTID`.
|
||||
*
|
||||
* @param mptIssue The issuance to serialize.
|
||||
* @return A JSON object of the form `{"mpt_issuance_id": "<hex>"}`.
|
||||
*/
|
||||
json::Value
|
||||
toJson(MPTIssue const& mptIssue);
|
||||
|
||||
/** Returns the hex string representation of an MPT issuance.
|
||||
*
|
||||
* @param mptIssue The issuance to convert.
|
||||
* @return Uppercase hex encoding of the underlying 192-bit `MPTID`.
|
||||
*/
|
||||
std::string
|
||||
to_string(MPTIssue const& mptIssue);
|
||||
|
||||
/** Parses an MPT issuance from a JSON object.
|
||||
*
|
||||
* Validates in strict order: `v` must be a JSON object; `currency` and
|
||||
* `issuer` keys must be absent (their presence indicates IOU data routed
|
||||
* to the wrong parser); `mpt_issuance_id` must be a string containing a
|
||||
* valid 48-character hex-encoded `MPTID`.
|
||||
*
|
||||
* @param jv The JSON value to parse; must be an object.
|
||||
* @return The parsed `MPTIssue`.
|
||||
* @throws std::runtime_error if `jv` is not a JSON object, or if `currency`
|
||||
* or `issuer` keys are present.
|
||||
* @throws json::Error if `mpt_issuance_id` is absent, not a string, or not
|
||||
* a valid 192-bit hex value.
|
||||
* @see toJson for the inverse operation.
|
||||
*/
|
||||
MPTIssue
|
||||
mptIssueFromJson(json::Value const& jv);
|
||||
|
||||
/** Writes the hex representation of an MPT issuance to a stream.
|
||||
*
|
||||
* @param os The output stream.
|
||||
* @param x The issuance to write.
|
||||
* @return `os`, to allow chaining.
|
||||
*/
|
||||
std::ostream&
|
||||
operator<<(std::ostream& os, MPTIssue const& x);
|
||||
|
||||
@@ -137,6 +323,13 @@ operator<<(std::ostream& os, MPTIssue const& x);
|
||||
|
||||
namespace std {
|
||||
|
||||
/** Specializes `std::hash` for `xrpl::MPTID`, delegating to the type's own
|
||||
* hasher.
|
||||
*
|
||||
* Enables `MPTID` to be used directly as a key in `std::unordered_map`,
|
||||
* `std::unordered_set`, and similar standard containers without wrapping
|
||||
* in `MPTIssue`.
|
||||
*/
|
||||
template <>
|
||||
struct hash<xrpl::MPTID> : xrpl::MPTID::hasher
|
||||
{
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/** @file
|
||||
* Holds one pre-built `Json::Value` per supported API version so that a
|
||||
* single ledger event can be delivered to subscribers speaking different API
|
||||
* versions without re-serializing on every send.
|
||||
*
|
||||
* The public alias `xrpl::MultiApiJson` binds the template to the live
|
||||
* version range `[kAPI_MINIMUM_SUPPORTED_VERSION, kAPI_MAXIMUM_VALID_VERSION]`.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
@@ -14,6 +23,15 @@
|
||||
namespace xrpl {
|
||||
|
||||
namespace detail {
|
||||
|
||||
/** Variable template that is `true` only for lvalue-reference-qualified
|
||||
* `std::integral_constant` specializations (both cv-variants).
|
||||
*
|
||||
* Used as the building block for the `some_integral_constant` concept, which
|
||||
* disambiguates the compile-time and runtime overloads of `VisitorT::operator()`.
|
||||
*
|
||||
* @tparam T The type to test.
|
||||
*/
|
||||
template <typename T>
|
||||
constexpr bool kIS_INTEGRAL_CONSTANT = false;
|
||||
template <typename I, auto A>
|
||||
@@ -21,35 +39,94 @@ constexpr bool kIS_INTEGRAL_CONSTANT<std::integral_constant<I, A>&> = true;
|
||||
template <typename I, auto A>
|
||||
constexpr bool kIS_INTEGRAL_CONSTANT<std::integral_constant<I, A> const&> = true;
|
||||
|
||||
/** Concept satisfied only by `std::integral_constant` specializations (lvalue refs).
|
||||
*
|
||||
* Used in `requires` clauses on `VisitorT::operator()` to prevent the
|
||||
* runtime-`unsigned` overloads from being selected when a compile-time
|
||||
* constant is passed, avoiding otherwise-ambiguous partial ordering.
|
||||
*
|
||||
* @tparam T The type to constrain.
|
||||
*/
|
||||
template <typename T>
|
||||
concept some_integral_constant = detail::kIS_INTEGRAL_CONSTANT<T&>;
|
||||
|
||||
// This class is designed to wrap a collection of _almost_ identical json::Value
|
||||
// objects, indexed by version (i.e. there is some mapping of version to object
|
||||
// index). It is used e.g. when we need to publish JSON data to users supporting
|
||||
// different API versions. We allow manipulation and inspection of all objects
|
||||
// at once with `isMember` and `set`, and also individual inspection and updates
|
||||
// of an object selected by the user by version, using `visitor_t` nested type.
|
||||
/** Holds one `Json::Value` per API version in a fixed-size array, enabling
|
||||
* single-pass event serialization for multi-version subscriber delivery.
|
||||
*
|
||||
* When an XRPL server event (e.g., a validated transaction) must be published
|
||||
* to subscribers that may speak different API versions, re-serializing or
|
||||
* branching inside the send path would add latency proportional to subscriber
|
||||
* count. `MultiApiJson` amortizes version-specific transformations to once per
|
||||
* event: callers construct the object from a common base `Json::Value`, apply
|
||||
* per-version mutations via `visit`, and then each subscriber's delivery path
|
||||
* calls `visit(apiVersion, sender)` to pick the pre-built slot cheaply.
|
||||
*
|
||||
* The array has `MaxVer + 1 - MinVer` elements; version `v` maps to index
|
||||
* `v - MinVer`. `set` and `isMember` operate across all slots; `visit`
|
||||
* operates on a single slot selected by version.
|
||||
*
|
||||
* @note Prefer the `xrpl::MultiApiJson` type alias over instantiating this
|
||||
* template directly. Direct instantiation is intended for tests only; all
|
||||
* production code should use the alias, which is bound to the live version
|
||||
* constants and automatically tracks any future version-range changes.
|
||||
*
|
||||
* @tparam MinVer Minimum (inclusive) supported API version.
|
||||
* @tparam MaxVer Maximum (inclusive) supported API version.
|
||||
*/
|
||||
template <unsigned MinVer, unsigned MaxVer>
|
||||
struct MultiApiJson
|
||||
{
|
||||
static_assert(MinVer <= MaxVer);
|
||||
|
||||
/** Returns `true` if `v` falls within `[MinVer, MaxVer]`.
|
||||
*
|
||||
* Used by `VisitorT` to guard against out-of-range version accesses.
|
||||
* @param v The API version number to test.
|
||||
* @return `true` iff `v` is a valid slot index.
|
||||
*/
|
||||
static constexpr auto
|
||||
valid(unsigned int v) noexcept -> bool
|
||||
{
|
||||
return v >= MinVer && v <= MaxVer;
|
||||
}
|
||||
|
||||
/** Maps an API version number to its zero-based array slot.
|
||||
*
|
||||
* Out-of-range values below `MinVer` clamp to 0 rather than underflowing;
|
||||
* the caller is responsible for checking `valid(v)` before trusting the
|
||||
* result. Values above `MaxVer` are not clamped — `valid()` must be used
|
||||
* to guard against those.
|
||||
*
|
||||
* @param v The API version number to map.
|
||||
* @return The corresponding index into `val`.
|
||||
*/
|
||||
static constexpr auto
|
||||
index(unsigned int v) noexcept -> std::size_t
|
||||
{
|
||||
return (v < MinVer) ? 0 : static_cast<std::size_t>(v - MinVer);
|
||||
}
|
||||
|
||||
/** Number of API version slots stored; equals `MaxVer + 1 - MinVer`. */
|
||||
constexpr static std::size_t kSIZE = MaxVer + 1 - MinVer;
|
||||
|
||||
/** The per-version JSON values, indexed by `index(version)`.
|
||||
*
|
||||
* Public to allow direct slot access in tests and for `VisitorT` (which
|
||||
* is a friend via the `static constexpr` data member). Production callers
|
||||
* should use `set`, `isMember`, and `visit` rather than indexing directly.
|
||||
*/
|
||||
std::array<json::Value, kSIZE> val = {};
|
||||
|
||||
/** Constructs the object, optionally copy-initializing every slot.
|
||||
*
|
||||
* When `init` is the default (null) `Json::Value`, all slots remain
|
||||
* default-initialized (null). When a non-null value is supplied, every
|
||||
* slot is copy-initialized to it. The common pattern in `NetworkOPs.cpp`
|
||||
* is to pass a shared base object and then apply per-version mutations
|
||||
* via `visit`.
|
||||
*
|
||||
* @param init Base value to copy into every slot; omit for null slots.
|
||||
*/
|
||||
explicit MultiApiJson(json::Value const& init = {})
|
||||
{
|
||||
if (init == json::Value{})
|
||||
@@ -58,6 +135,16 @@ struct MultiApiJson
|
||||
v = init;
|
||||
}
|
||||
|
||||
/** Writes a key-value pair into every slot simultaneously.
|
||||
*
|
||||
* Use for fields that are identical across all API versions — the majority
|
||||
* of transaction fields. Cheaper than calling `visit` once per version for
|
||||
* shared data. The `requires` clause restricts `v` to types from which
|
||||
* `Json::Value` can be constructed, preventing silent misuse.
|
||||
*
|
||||
* @param key The JSON object key to set.
|
||||
* @param v The value to assign; must be constructible to `Json::Value`.
|
||||
*/
|
||||
void
|
||||
set(char const* key, auto const& v)
|
||||
requires std::constructible_from<json::Value, decltype(v)>
|
||||
@@ -66,8 +153,26 @@ struct MultiApiJson
|
||||
a[key] = v;
|
||||
}
|
||||
|
||||
enum class IsMemberResult : int { None = 0, Some, All };
|
||||
/** Tri-state result of `isMember`: indicates how many version slots contain a key.
|
||||
*
|
||||
* Scoped to `MultiApiJson` rather than a separate class enum deliberately —
|
||||
* the struct is narrow enough to serve as its own scope for this result.
|
||||
*/
|
||||
enum class IsMemberResult : int {
|
||||
None = 0, /**< No slot contains the key. */
|
||||
Some, /**< At least one but not all slots contain the key. */
|
||||
All /**< Every slot contains the key. */
|
||||
};
|
||||
|
||||
/** Queries how many version slots contain the given JSON key.
|
||||
*
|
||||
* Useful for asserting that version-specific mutations were (or were not)
|
||||
* applied before delivery. `NetworkOPs` uses it in assertions to verify
|
||||
* that certain fields are never set on a freshly-constructed object.
|
||||
*
|
||||
* @param key The JSON object key to look up in each slot.
|
||||
* @return `IsMemberResult::None`, `Some`, or `All`.
|
||||
*/
|
||||
[[nodiscard]] IsMemberResult
|
||||
isMember(char const* key) const
|
||||
{
|
||||
@@ -83,6 +188,33 @@ struct MultiApiJson
|
||||
return count < kSIZE ? IsMemberResult::Some : IsMemberResult::All;
|
||||
}
|
||||
|
||||
/** Stateless callable that routes invocations to the correct version slot.
|
||||
*
|
||||
* Provides four `operator()` overloads split along two axes:
|
||||
*
|
||||
* 1. **Compile-time version** (`std::integral_constant<unsigned, V>`):
|
||||
* the version is checked with `static_assert`; the JSON reference and
|
||||
* optional extra arguments are forwarded to `fn` at compile time.
|
||||
*
|
||||
* 2. **Runtime version** (any type convertible to `unsigned` that is
|
||||
* *not* an `integral_constant`): the version is checked with
|
||||
* `XRPL_ASSERT`; the `some_integral_constant` concept in the `requires`
|
||||
* clause prevents these overloads from being selected when a
|
||||
* compile-time constant is passed, resolving the otherwise-ambiguous
|
||||
* partial ordering.
|
||||
*
|
||||
* Each axis is further split by whether extra arguments are forwarded to
|
||||
* `fn` after the `Json::Value` (and possibly the version value). This
|
||||
* matches the calling convention of `forAllApiVersions`/`forApiVersions`,
|
||||
* which pass each version as an `integral_constant` plus any extra args
|
||||
* bound at the call site.
|
||||
*
|
||||
* `const`-propagation is automatic: the JSON reference passed to `fn`
|
||||
* mirrors the `const`-ness of the `Json&` parameter.
|
||||
*
|
||||
* @note Exposed as `kVISITOR` to allow direct testing; prefer `visit()`
|
||||
* for all production call sites.
|
||||
*/
|
||||
static constexpr struct VisitorT final
|
||||
{
|
||||
// integral_constant version, extra arguments
|
||||
@@ -145,6 +277,19 @@ struct MultiApiJson
|
||||
}
|
||||
} kVISITOR = {};
|
||||
|
||||
/** Returns a closure that dispatches `kVISITOR` for this object (mutable).
|
||||
*
|
||||
* The returned callable captures `this` and forwards all arguments to
|
||||
* `kVISITOR`. This form is composable with `forAllApiVersions` and
|
||||
* `forApiVersions`: those utilities iterate the version range at compile
|
||||
* time, passing each version as an `integral_constant`. The closure
|
||||
* satisfies that calling convention exactly, so
|
||||
* `forAllApiVersions(obj.visit(), lambda)` iterates every version with a
|
||||
* single consistent lambda without any per-version conditional logic.
|
||||
*
|
||||
* @return A lambda `(auto... args) -> auto` that calls
|
||||
* `kVISITOR(*this, args...)`.
|
||||
*/
|
||||
auto
|
||||
visit()
|
||||
{
|
||||
@@ -155,6 +300,16 @@ struct MultiApiJson
|
||||
{ return kVISITOR(*self, std::forward<decltype(args)>(args)...); };
|
||||
}
|
||||
|
||||
/** Returns a closure that dispatches `kVISITOR` for this object (const).
|
||||
*
|
||||
* Identical to the mutable overload but captures `this` as `const`,
|
||||
* propagating const-ness through to the `Json::Value` reference passed to
|
||||
* the callable. Used when the caller only needs to read the pre-built JSON
|
||||
* (e.g., subscriber delivery in `BookListeners::publish`).
|
||||
*
|
||||
* @return A lambda `(auto... args) -> auto` that calls
|
||||
* `kVISITOR(*this, args...)` on the const object.
|
||||
*/
|
||||
[[nodiscard]] auto
|
||||
visit() const
|
||||
{
|
||||
@@ -165,6 +320,20 @@ struct MultiApiJson
|
||||
{ return kVISITOR(*self, std::forward<decltype(args)>(args)...); };
|
||||
}
|
||||
|
||||
/** Directly invokes `kVISITOR` for a single version (mutable).
|
||||
*
|
||||
* Equivalent to `visit()(args...)` but avoids the closure allocation.
|
||||
* Typical usage:
|
||||
* ```cpp
|
||||
* jvObj.visit(RPC::kAPI_VERSION<1>, [](Json::Value& jv) {
|
||||
* jv["ledger_index"] = std::to_string(jv["ledger_index"].asInt());
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param args Version (compile-time or runtime) followed by a callable
|
||||
* and any extra arguments accepted by `kVISITOR`.
|
||||
* @return The return value of the callable.
|
||||
*/
|
||||
template <typename... Args>
|
||||
auto
|
||||
visit(Args... args) -> std::invoke_result_t<VisitorT, MultiApiJson&, Args...>
|
||||
@@ -174,6 +343,20 @@ struct MultiApiJson
|
||||
return kVISITOR(*this, std::forward<decltype(args)>(args)...);
|
||||
}
|
||||
|
||||
/** Directly invokes `kVISITOR` for a single version (const).
|
||||
*
|
||||
* Const counterpart of the mutable `visit(args...)` overload. Used when
|
||||
* the JSON slot must not be mutated — for example in the subscriber
|
||||
* delivery path where each subscriber picks its pre-built slot:
|
||||
* ```cpp
|
||||
* jvObj.visit(subscriber->getApiVersion(),
|
||||
* [&](Json::Value const& jv) { subscriber->send(jv, true); });
|
||||
* ```
|
||||
*
|
||||
* @param args Version (compile-time or runtime) followed by a callable
|
||||
* and any extra arguments accepted by `kVISITOR`.
|
||||
* @return The return value of the callable.
|
||||
*/
|
||||
template <typename... Args>
|
||||
[[nodiscard]] auto
|
||||
visit(Args... args) const -> std::invoke_result_t<VisitorT, MultiApiJson const&, Args...>
|
||||
@@ -186,7 +369,15 @@ struct MultiApiJson
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// Wrapper for Json for all supported API versions.
|
||||
/** Holds one pre-built `Json::Value` per currently supported API version.
|
||||
*
|
||||
* Bound to `[kAPI_MINIMUM_SUPPORTED_VERSION, kAPI_MAXIMUM_VALID_VERSION]`
|
||||
* (currently versions 1–3), so the concrete type stores exactly three
|
||||
* `Json::Value` objects. Changing those constants automatically resizes
|
||||
* every `MultiApiJson` instance in the server.
|
||||
*
|
||||
* @see detail::MultiApiJson for the full behavioral contract.
|
||||
*/
|
||||
using MultiApiJson =
|
||||
detail::MultiApiJson<RPC::kAPI_MINIMUM_SUPPORTED_VERSION, RPC::kAPI_MAXIMUM_VALID_VERSION>;
|
||||
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/** @file
|
||||
* Aggregator entry point for injecting synthetic NFT fields into RPC
|
||||
* transaction responses.
|
||||
*
|
||||
* "Synthetic" fields (`nftoken_ids`, `nftoken_id`, `offer_id`) are derived
|
||||
* at query time from the ledger state changes recorded in `TxMeta`; they are
|
||||
* not stored on-chain. Callers invoke a single function here rather than
|
||||
* calling the individual NFT inserters directly, keeping call sites from
|
||||
* accumulating an ever-growing list of per-type injector calls as new NFT
|
||||
* transaction types are added.
|
||||
*
|
||||
* The underlying extraction helpers (`insertNFTokenID`, `insertNFTokenOfferID`)
|
||||
* live in `NFTokenID.h` and `NFTokenOfferID.h` under the broader `xrpl::`
|
||||
* namespace so that Clio (the XRPL History API server) can call those helpers
|
||||
* directly without the `xrpl::RPC` coupling imposed by this header.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
@@ -8,13 +25,44 @@
|
||||
|
||||
namespace xrpl::RPC {
|
||||
|
||||
/**
|
||||
Adds common synthetic fields to transaction-related JSON responses
|
||||
|
||||
@{
|
||||
/** Enrich a transaction JSON response with NFT-derived synthetic fields.
|
||||
*
|
||||
* Delegates to two independent inserters, in order:
|
||||
*
|
||||
* - `insertNFTokenID` — adds `nftoken_id` (for `NFTokenMint` and
|
||||
* `NFTokenAcceptOffer`) or `nftoken_ids` (for `NFTokenCancelOffer`) by
|
||||
* diffing the NFToken arrays across all affected ledger nodes recorded in
|
||||
* the transaction metadata.
|
||||
* - `insertNFTokenOfferID` — adds `offer_id` for `NFTokenCreateOffer` (and
|
||||
* mints that include an immediate sell offer) by locating the newly created
|
||||
* `NFTokenOffer` node and extracting its `sfLedgerIndex`.
|
||||
*
|
||||
* Both delegates gate themselves on transaction type and `tesSUCCESS`, so
|
||||
* this function is safe to call for any transaction type: non-NFT
|
||||
* transactions produce no output.
|
||||
*
|
||||
* Synthetic fields are written into `response[jss::meta]`. The `meta`
|
||||
* sub-object should already be populated by the caller (e.g., via
|
||||
* `TxMeta::getJson`) before this function is invoked — consistent with the
|
||||
* call-site pattern in `Tx.cpp`, `Simulate.cpp`, `AccountTx.cpp`, and
|
||||
* `NetworkOPs.cpp`, where this call appears alongside `insertDeliveredAmount`
|
||||
* and `insertMPTokenIssuanceID` as part of a fixed metadata-enrichment
|
||||
* sequence.
|
||||
*
|
||||
* @param response Top-level RPC response object. Synthetic fields are
|
||||
* written into its `meta` sub-object, which is created on demand if
|
||||
* absent.
|
||||
* @param transaction The executed transaction. A null pointer is handled
|
||||
* gracefully by the delegates (no-op).
|
||||
* @param transactionMeta Read-only view of the transaction's metadata used to
|
||||
* diff ledger node states and locate newly created objects.
|
||||
*
|
||||
* @see xrpl::insertNFTokenID, xrpl::insertNFTokenOfferID
|
||||
*/
|
||||
void
|
||||
insertNFTSyntheticInJson(json::Value&, std::shared_ptr<STTx const> const&, TxMeta const&);
|
||||
/** @} */
|
||||
insertNFTSyntheticInJson(
|
||||
json::Value& response,
|
||||
std::shared_ptr<STTx const> const& transaction,
|
||||
TxMeta const& transactionMeta);
|
||||
|
||||
} // namespace xrpl::RPC
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/** @file
|
||||
* Helpers that reconstruct NFToken identities from transaction metadata
|
||||
* and inject them into RPC JSON responses as synthetic fields.
|
||||
*
|
||||
* Raw ledger metadata records before/after state of `NFTokenPage` objects
|
||||
* but does not directly annotate which token was created or consumed. The
|
||||
* functions below bridge that gap. They are free (non-static) functions so
|
||||
* that Clio (the XRPL History API server) can link against them directly
|
||||
* and perform the same enrichment without duplicating the logic.
|
||||
*
|
||||
* @see NFTokenOfferID.h for the analogous helpers for `NFTokenOffer` IDs.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
@@ -11,28 +24,100 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
Add a `nftoken_ids` field to the `meta` output parameter.
|
||||
The field is only added to successful NFTokenMint, NFTokenAcceptOffer,
|
||||
and NFTokenCancelOffer transactions.
|
||||
|
||||
Helper functions are not static because they can be used by Clio.
|
||||
@{
|
||||
/** Returns true if this transaction could have produced or consumed an NFToken.
|
||||
*
|
||||
* Acts as a cheap early-exit guard for all downstream extraction logic.
|
||||
* A transaction qualifies only when it is one of the three NFT types
|
||||
* (`ttNFTOKEN_MINT`, `ttNFTOKEN_ACCEPT_OFFER`, `ttNFTOKEN_CANCEL_OFFER`)
|
||||
* and its result code is `tesSUCCESS`. A failed transaction cannot have
|
||||
* mutated any NFToken page, so metadata diffing would be meaningless.
|
||||
*
|
||||
* @param serializedTx The executed transaction; a null pointer yields
|
||||
* false immediately.
|
||||
* @param transactionMeta Metadata from the same transaction, used to
|
||||
* check the result code.
|
||||
* @return True only when `serializedTx` is non-null, its type is one of
|
||||
* the three NFT transaction types, and the result is `tesSUCCESS`.
|
||||
*/
|
||||
bool
|
||||
canHaveNFTokenID(std::shared_ptr<STTx const> const& serializedTx, TxMeta const& transactionMeta);
|
||||
|
||||
/** Recovers the ID of the NFToken added by a mint transaction.
|
||||
*
|
||||
* `ttNFTOKEN_MINT` metadata records the full token arrays of every
|
||||
* affected `NFTokenPage` in `sfPreviousFields` and `sfFinalFields` but
|
||||
* does not tag the newly inserted entry. This function recovers it by
|
||||
* set-difference: it accumulates token IDs from all previous states into
|
||||
* `prevIDs` and all final states into `finalIDs`, then uses
|
||||
* `std::mismatch` to locate the first entry present in `finalIDs` but
|
||||
* absent from `prevIDs`. Because `NFTokenPage` entries are stored in
|
||||
* sorted order by token ID, both vectors are already ordered and
|
||||
* `std::mismatch` finds the insertion point in linear time without
|
||||
* additional sorting.
|
||||
*
|
||||
* @note When a mint causes an existing page to split, the linked-list
|
||||
* rewiring may produce a `sfModifiedNode` for a third page whose
|
||||
* `sfPreviousFields` contain only pointer updates (`NextPageMin` /
|
||||
* `PreviousPageMin`) with no `sfNFTokens` array. Such nodes are
|
||||
* skipped silently; without this guard the size invariant below
|
||||
* would incorrectly fail for legitimate mints.
|
||||
*
|
||||
* @param transactionMeta Metadata from a `ttNFTOKEN_MINT` transaction.
|
||||
* @return The `uint256` ID of the newly minted token, or `std::nullopt`
|
||||
* if `finalIDs.size() != prevIDs.size() + 1` (tokens are minted one
|
||||
* at a time) or if `std::mismatch` unexpectedly reaches the end of
|
||||
* `finalIDs`.
|
||||
*/
|
||||
std::optional<uint256>
|
||||
getNFTokenIDFromPage(TxMeta const& transactionMeta);
|
||||
|
||||
/** Collects the NFToken IDs referenced by deleted `NFTokenOffer` objects.
|
||||
*
|
||||
* Both `ttNFTOKEN_ACCEPT_OFFER` and `ttNFTOKEN_CANCEL_OFFER` delete one
|
||||
* or more `ltNFTOKEN_OFFER` ledger entries. Each deleted offer's
|
||||
* `sfFinalFields` carries the `sfNFTokenID` it was created for, so the
|
||||
* token identity is recoverable without set-difference arithmetic.
|
||||
* Results are sorted and deduplicated because a single cancel transaction
|
||||
* can target multiple offers that reference the same underlying NFT.
|
||||
*
|
||||
* @param transactionMeta Metadata from a `ttNFTOKEN_ACCEPT_OFFER` or
|
||||
* `ttNFTOKEN_CANCEL_OFFER` transaction.
|
||||
* @return Sorted, deduplicated vector of `uint256` NFToken IDs recovered
|
||||
* from all deleted offer nodes; empty if no qualifying deletions are
|
||||
* found.
|
||||
*/
|
||||
std::vector<uint256>
|
||||
getNFTokenIDFromDeletedOffer(TxMeta const& transactionMeta);
|
||||
|
||||
/** Injects synthetic NFToken ID field(s) into an RPC transaction response.
|
||||
*
|
||||
* Calls `canHaveNFTokenID` first; returns immediately without modifying
|
||||
* `response` if the transaction is ineligible or extraction yields nothing.
|
||||
* When eligible, dispatches by transaction type:
|
||||
*
|
||||
* - `ttNFTOKEN_MINT` — writes `jss::nftoken_id` (single string) derived
|
||||
* from `getNFTokenIDFromPage`.
|
||||
* - `ttNFTOKEN_ACCEPT_OFFER` — writes `jss::nftoken_id` (single string,
|
||||
* first element) derived from `getNFTokenIDFromDeletedOffer`.
|
||||
* - `ttNFTOKEN_CANCEL_OFFER` — writes `jss::nftoken_ids` (JSON array of
|
||||
* all deduplicated IDs) derived from `getNFTokenIDFromDeletedOffer`.
|
||||
*
|
||||
* The singular/plural field-name distinction reflects a real semantic
|
||||
* difference: accept and mint affect exactly one NFT, while cancel can
|
||||
* affect many.
|
||||
*
|
||||
* @param response The JSON object to enrich; fields are written
|
||||
* directly into it. The caller is responsible for scoping this to
|
||||
* the `jss::meta` sub-object of the full response.
|
||||
* @param transaction The executed transaction. A null pointer is
|
||||
* handled gracefully via `canHaveNFTokenID`.
|
||||
* @param transactionMeta Read-only metadata used for eligibility checking
|
||||
* and token ID extraction.
|
||||
*/
|
||||
void
|
||||
insertNFTokenID(
|
||||
json::Value& response,
|
||||
std::shared_ptr<STTx const> const& transaction,
|
||||
TxMeta const& transactionMeta);
|
||||
/** @} */
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/** @file
|
||||
* Helpers that recover the ledger index of a newly created `NFTokenOffer`
|
||||
* from transaction metadata and inject it into RPC JSON responses as a
|
||||
* synthetic `offer_id` field.
|
||||
*
|
||||
* The XRPL transaction format records only inputs; the ledger index of a
|
||||
* newly created offer object appears solely in the `CreatedNode` entries of
|
||||
* the transaction metadata. The three functions below encapsulate the scan
|
||||
* once so that every API consumer — rippled RPC handlers and Clio alike —
|
||||
* can obtain the offer ID without walking `AffectedNodes` manually. All
|
||||
* three functions are free (non-static) so that Clio can call them directly
|
||||
* without duplicating the logic.
|
||||
*
|
||||
* @see NFTokenID.h for the analogous helpers that inject `nftoken_id` /
|
||||
* `nftoken_ids` for mint, accept-offer, and cancel-offer operations.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
@@ -10,26 +27,76 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/**
|
||||
Add an `offer_id` field to the `meta` output parameter.
|
||||
The field is only added to successful NFTokenCreateOffer transactions.
|
||||
|
||||
Helper functions are not static because they can be used by Clio.
|
||||
@{
|
||||
/** Determine whether a transaction can have an NFToken offer ID.
|
||||
*
|
||||
* Acts as a cheap pre-filter before the metadata scan in
|
||||
* `getOfferIDFromCreatedOffer`. Three conditions must all hold:
|
||||
*
|
||||
* 1. `serializedTx` is non-null.
|
||||
* 2. The transaction type is `ttNFTOKEN_CREATE_OFFER`, **or** it is
|
||||
* `ttNFTOKEN_MINT` with `sfAmount` present (a mint that simultaneously
|
||||
* creates an immediate-sale offer).
|
||||
* 3. The transaction succeeded (`tesSUCCESS`). A failed transaction never
|
||||
* modifies the ledger, so no offer object can exist in the metadata.
|
||||
*
|
||||
* @param serializedTx The transaction to inspect. A null `shared_ptr`
|
||||
* is handled safely and causes the function to return `false`.
|
||||
* @param transactionMeta Metadata whose result code is checked for success.
|
||||
* @return `true` only when all three conditions are satisfied, indicating
|
||||
* that a subsequent call to `getOfferIDFromCreatedOffer` may yield a
|
||||
* value.
|
||||
*/
|
||||
bool
|
||||
canHaveNFTokenOfferID(
|
||||
std::shared_ptr<STTx const> const& serializedTx,
|
||||
TxMeta const& transactionMeta);
|
||||
|
||||
/** Extract the ledger index of the NFToken offer created by a transaction.
|
||||
*
|
||||
* Scans the `AffectedNodes` array in `transactionMeta` for a `CreatedNode`
|
||||
* whose `sfLedgerEntryType` is `ltNFTOKEN_OFFER`. Modified and deleted
|
||||
* nodes are skipped. The first qualifying node's `sfLedgerIndex` is
|
||||
* returned; because at most one `NFTokenOffer` can be created per
|
||||
* transaction, the loop exits immediately on the first match.
|
||||
*
|
||||
* @param transactionMeta Read-only transaction metadata to scan.
|
||||
* @return The `uint256` ledger index of the newly created offer, or
|
||||
* `std::nullopt` if no `CreatedNode` of type `ltNFTOKEN_OFFER` is
|
||||
* found. Absence is a plausible non-exceptional condition (e.g., when
|
||||
* processing historical or externally sourced transactions with
|
||||
* incomplete metadata), not an error.
|
||||
*
|
||||
* @note Callers that have already performed their own eligibility checks
|
||||
* (e.g., Clio) may call this function directly without first calling
|
||||
* `canHaveNFTokenOfferID`.
|
||||
*/
|
||||
std::optional<uint256>
|
||||
getOfferIDFromCreatedOffer(TxMeta const& transactionMeta);
|
||||
|
||||
/** Inject the NFToken offer ID into a JSON response as `jss::offer_id`.
|
||||
*
|
||||
* Composes `canHaveNFTokenOfferID` and `getOfferIDFromCreatedOffer`:
|
||||
* returns immediately without touching `response` if the transaction is
|
||||
* ineligible or the metadata contains no created offer node. When an offer
|
||||
* ID is successfully extracted, it is written into `response[jss::offer_id]`
|
||||
* as a hex string.
|
||||
*
|
||||
* The primary call site is `xrpl::RPC::insertNFTSyntheticInJson`, which
|
||||
* passes `response[jss::meta]` as the target so that `offer_id` appears
|
||||
* inside the `meta` sub-object alongside the raw node data.
|
||||
*
|
||||
* @param response The JSON object to enrich; `jss::offer_id` is
|
||||
* written directly into it on success. The caller is responsible for
|
||||
* scoping this to `jss::meta` of the full response.
|
||||
* @param transaction The executed transaction. A null pointer is
|
||||
* handled gracefully (no-op) via `canHaveNFTokenOfferID`.
|
||||
* @param transactionMeta Read-only transaction metadata used to locate the
|
||||
* created `NFTokenOffer` node.
|
||||
*/
|
||||
void
|
||||
insertNFTokenOfferID(
|
||||
json::Value& response,
|
||||
std::shared_ptr<STTx const> const& transaction,
|
||||
TxMeta const& transactionMeta);
|
||||
/** @} */
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/** @file
|
||||
* Defines `PathAsset`, the token identifier for a single hop in an XRPL
|
||||
* payment path, and its associated free functions.
|
||||
*
|
||||
* `PathAsset` holds `std::variant<Currency, MPTID>` — just the *which
|
||||
* currency or MPT* component of a path element, without the issuer.
|
||||
* This is narrower than `Asset` (`std::variant<Issue, MPTIssue>`) because
|
||||
* `STPathElement` records the issuer in a separate field.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/Asset.h>
|
||||
@@ -5,7 +14,19 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/* Represent STPathElement's asset, which can be Currency or MPTID.
|
||||
/** Token identifier for a single hop within an XRPL payment path.
|
||||
*
|
||||
* Holds `std::variant<Currency, MPTID>` — the *which currency/MPT* component
|
||||
* of a path element, without the issuer. Issuers are stored separately in
|
||||
* `STPathElement::mIssuerID` because payment-path serialization records them
|
||||
* as independent fields; folding them into `PathAsset` would duplicate data
|
||||
* and complicate encoding.
|
||||
*
|
||||
* This is intentionally narrower than `Asset`, which pairs a currency or MPTID
|
||||
* with its issuer. `PathAsset` carries only the identifier half. Use
|
||||
* `PathAsset(Asset const&)` to project an `Asset` down to a `PathAsset`.
|
||||
*
|
||||
* @see Asset, STPathElement, ValidPathAsset
|
||||
*/
|
||||
class PathAsset
|
||||
{
|
||||
@@ -14,36 +35,83 @@ private:
|
||||
|
||||
public:
|
||||
PathAsset() = default;
|
||||
// Enables comparing Asset and PathAsset
|
||||
|
||||
/** Construct a PathAsset by projecting an Asset, discarding the issuer.
|
||||
*
|
||||
* For an `Issue`-bearing `Asset`, retains the `Currency`. For an
|
||||
* `MPTIssue`-bearing `Asset`, retains the `MPTID`. This enables direct
|
||||
* comparison between the richer `Asset` type and the path-element
|
||||
* representation without manually extracting the identifier.
|
||||
*
|
||||
* @param asset The full asset to project.
|
||||
*/
|
||||
PathAsset(Asset const& asset);
|
||||
|
||||
/** Construct a PathAsset representing an XRP or IOU currency. */
|
||||
PathAsset(Currency const& currency) : easset_(currency)
|
||||
{
|
||||
}
|
||||
|
||||
/** Construct a PathAsset representing an MPT issuance. */
|
||||
PathAsset(MPTID const& mpt) : easset_(mpt)
|
||||
{
|
||||
}
|
||||
|
||||
/** Return whether the active alternative is exactly `T`.
|
||||
*
|
||||
* @tparam T `Currency` or `MPTID` (enforced by `ValidPathAsset`).
|
||||
* @return `true` if the held alternative is `T`, `false` otherwise.
|
||||
*/
|
||||
template <ValidPathAsset T>
|
||||
[[nodiscard]] constexpr bool
|
||||
holds() const;
|
||||
|
||||
/** Return whether this path asset represents native XRP.
|
||||
*
|
||||
* A `Currency` alternative delegates to `xrpl::isXRP(currency)`. An
|
||||
* `MPTID` alternative always returns `false` — MPT can never be native.
|
||||
*
|
||||
* @return `true` if the held currency is the XRP zero-currency sentinel.
|
||||
*/
|
||||
[[nodiscard]] constexpr bool
|
||||
isXRP() const;
|
||||
|
||||
/** Return a const reference to the held value of type `T`.
|
||||
*
|
||||
* @tparam T `Currency` or `MPTID` (enforced by `ValidPathAsset`).
|
||||
* @return A reference to the active alternative.
|
||||
* @throws std::runtime_error if the active alternative is not `T`. Call
|
||||
* `holds<T>()` or dispatch through `visit()` to avoid this.
|
||||
*/
|
||||
template <ValidPathAsset T>
|
||||
T const&
|
||||
get() const;
|
||||
|
||||
/** Return a const reference to the underlying variant.
|
||||
*
|
||||
* Provides direct access to `std::variant<Currency, MPTID>` for callers
|
||||
* that need to pass it to `std::visit` or store it without going through
|
||||
* the member `visit()` wrapper.
|
||||
*
|
||||
* @return The internal variant holding `Currency` or `MPTID`.
|
||||
*/
|
||||
[[nodiscard]] constexpr std::variant<Currency, MPTID> const&
|
||||
value() const;
|
||||
|
||||
// Custom, generic visit implementation
|
||||
/** Visit the active alternative with a set of per-type callables.
|
||||
*
|
||||
* Combines `visitors...` into a single overload set via
|
||||
* `detail::CombineVisitors` and forwards to `std::visit`. Both
|
||||
* alternatives (`Currency` and `MPTID`) must be covered.
|
||||
*
|
||||
* @tparam Visitors Callable types, one per alternative.
|
||||
* @param visitors Callables to dispatch to; typically lambdas.
|
||||
* @return The return value of the selected visitor.
|
||||
*/
|
||||
template <typename... Visitors>
|
||||
constexpr auto
|
||||
visit(Visitors&&... visitors) const -> decltype(auto)
|
||||
{
|
||||
// Simple delegation to the reusable utility, passing the internal
|
||||
// variant data.
|
||||
return detail::visit(easset_, std::forward<Visitors>(visitors)...);
|
||||
}
|
||||
|
||||
@@ -51,9 +119,23 @@ public:
|
||||
operator==(PathAsset const& lhs, PathAsset const& rhs);
|
||||
};
|
||||
|
||||
/** True when `PA` is `Currency`, false when `PA` is `MPTID`.
|
||||
*
|
||||
* Compile-time predicate for `if constexpr` branches in generic code that
|
||||
* must distinguish XRP/IOU paths from MPT paths.
|
||||
*
|
||||
* @tparam PA `Currency` or `MPTID` (enforced by `ValidPathAsset`).
|
||||
*/
|
||||
template <ValidPathAsset PA>
|
||||
constexpr bool kIS_CURRENCY_V = std::is_same_v<PA, Currency>;
|
||||
|
||||
/** True when `PA` is `MPTID`, false when `PA` is `Currency`.
|
||||
*
|
||||
* Compile-time predicate for `if constexpr` branches in generic code that
|
||||
* must distinguish MPT paths from XRP/IOU paths.
|
||||
*
|
||||
* @tparam PA `Currency` or `MPTID` (enforced by `ValidPathAsset`).
|
||||
*/
|
||||
template <ValidPathAsset PA>
|
||||
constexpr bool kIS_MPTID_V = std::is_same_v<PA, MPTID>;
|
||||
|
||||
@@ -94,6 +176,17 @@ PathAsset::isXRP() const
|
||||
[](MPTID const&) { return false; });
|
||||
}
|
||||
|
||||
/** Compare two PathAssets for equality.
|
||||
*
|
||||
* Two `PathAsset` values are equal only when both hold the same alternative
|
||||
* type *and* the contained values are equal. A `Currency` and an `MPTID`
|
||||
* are never equal even if their raw bytes coincide, preventing cross-type
|
||||
* false positives.
|
||||
*
|
||||
* @param lhs Left-hand operand.
|
||||
* @param rhs Right-hand operand.
|
||||
* @return `true` if both hold the same type and equal value, `false` otherwise.
|
||||
*/
|
||||
constexpr bool
|
||||
operator==(PathAsset const& lhs, PathAsset const& rhs)
|
||||
{
|
||||
@@ -112,6 +205,16 @@ operator==(PathAsset const& lhs, PathAsset const& rhs)
|
||||
rhs.value());
|
||||
}
|
||||
|
||||
/** Append a PathAsset's value to a hash state.
|
||||
*
|
||||
* Dispatches to the appropriate `hash_append` overload for the active
|
||||
* alternative (`Currency` or `MPTID`), enabling `PathAsset` to be used as
|
||||
* a key in hash-based containers built on the `beast::uhash` infrastructure.
|
||||
*
|
||||
* @tparam Hasher A type satisfying the `beast::hash_append` Hasher concept.
|
||||
* @param h The hash accumulator to append to.
|
||||
* @param pathAsset The path asset whose value is appended.
|
||||
*/
|
||||
template <typename Hasher>
|
||||
void
|
||||
hash_append(Hasher& h, PathAsset const& pathAsset)
|
||||
@@ -119,15 +222,41 @@ hash_append(Hasher& h, PathAsset const& pathAsset)
|
||||
std::visit([&]<ValidPathAsset T>(T const& e) { hash_append(h, e); }, pathAsset.value());
|
||||
}
|
||||
|
||||
/** Return whether a PathAsset represents native XRP.
|
||||
*
|
||||
* Free-function wrapper for `PathAsset::isXRP()`, provided for symmetry
|
||||
* with the `isXRP()` overloads for `Currency`, `Asset`, and `STAmount`.
|
||||
*
|
||||
* @param asset The path asset to test.
|
||||
* @return `true` if `asset` holds the XRP zero-currency sentinel.
|
||||
*/
|
||||
inline bool
|
||||
isXRP(PathAsset const& asset)
|
||||
{
|
||||
return asset.isXRP();
|
||||
}
|
||||
|
||||
/** Produce a human-readable string identifying a PathAsset.
|
||||
*
|
||||
* Dispatches to `to_string(Currency const&)` or `to_string(MPTID const&)`
|
||||
* depending on the active alternative. For a `Currency` this yields the ISO
|
||||
* 4217 ticker or `"XRP"`; for an `MPTID` it yields the base-58 encoded token
|
||||
* identifier.
|
||||
*
|
||||
* @param asset The path asset to stringify.
|
||||
* @return A descriptive string identifying the currency or MPT issuance.
|
||||
*/
|
||||
std::string
|
||||
to_string(PathAsset const& asset);
|
||||
|
||||
/** Stream-insert a human-readable description of a PathAsset.
|
||||
*
|
||||
* Equivalent to `os << to_string(x)`. Intended for logging and diagnostics.
|
||||
*
|
||||
* @param os The output stream to write to.
|
||||
* @param x The path asset to write.
|
||||
* @return `os`, for chaining.
|
||||
*/
|
||||
std::ostream&
|
||||
operator<<(std::ostream& os, PathAsset const& x);
|
||||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/** @file
|
||||
* Canonical serialization for payment channel claim authorizations.
|
||||
*
|
||||
* Defines the single function that all three call sites — channel
|
||||
* authorization (RPC), channel verification (RPC), and on-ledger
|
||||
* claim validation (transaction engine) — must use to build the
|
||||
* signed payload. Centralizing this here ensures that a signature
|
||||
* produced off-ledger is always accepted on-ledger.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
@@ -7,6 +17,34 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Serialize the signing payload for a payment channel claim authorization.
|
||||
*
|
||||
* Writes exactly three fields into @p msg in a protocol-defined order:
|
||||
* the `HashPrefix::paymentChannelClaim` domain-separation tag (4 bytes),
|
||||
* the 256-bit channel keylet @p key, and the authorized cumulative amount
|
||||
* @p amt as a 64-bit drop count. The resulting byte sequence is what the
|
||||
* channel sender signs and what the recipient or ledger verifies.
|
||||
*
|
||||
* This function is the single source of truth for the signed payload layout.
|
||||
* It is called identically by `channel_authorize` (RPC), `channel_verify`
|
||||
* (RPC), and `PaymentChannelClaim` preflight (transaction engine). Any drift
|
||||
* between those sites would cause off-ledger signatures to fail on-ledger
|
||||
* validation.
|
||||
*
|
||||
* @param msg Serializer to append the payload fields into. The caller is
|
||||
* responsible for constructing the `Serializer` and, after this call,
|
||||
* passing `msg.slice()` to the sign or verify primitive.
|
||||
* @param key The 256-bit keylet of the payment channel ledger object. Binds
|
||||
* the authorization to exactly one channel so it cannot be replayed
|
||||
* against a different channel.
|
||||
* @param amt The authorized cumulative ceiling in drops. The on-ledger claim
|
||||
* validator rejects any claim whose running total exceeds this value.
|
||||
*
|
||||
* @note The `HashPrefix::paymentChannelClaim` tag (`'C','L','M',0x00`) is
|
||||
* protocol-immutable. Changing it would invalidate all existing payment
|
||||
* channel authorizations.
|
||||
* @see HashPrefix::paymentChannelClaim
|
||||
*/
|
||||
inline void
|
||||
serializePayChanAuthorization(Serializer& msg, uint256 const& key, XRPAmount const& amt)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
/** @file
|
||||
* Central definition of XRPL's account-delegation permission system,
|
||||
* used by the `DelegateSet` transaction type.
|
||||
*
|
||||
* Two numeric ranges partition the `sfPermissionValue` field stored
|
||||
* on-ledger:
|
||||
* - **Transaction-level** (≤ `UINT16_MAX`): `TxType + 1`, granting
|
||||
* authority over an entire transaction type.
|
||||
* - **Granular** (> `UINT16_MAX`, minimum 65537): covers a specific
|
||||
* sub-operation within a transaction type (e.g., freezing a trustline
|
||||
* without being able to authorize it).
|
||||
*
|
||||
* The `Permission` singleton is the runtime authority for both ranges.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
@@ -9,12 +23,21 @@
|
||||
#include <unordered_map>
|
||||
|
||||
namespace xrpl {
|
||||
/**
|
||||
* We have both transaction type permissions and granular type permissions.
|
||||
* Since we will reuse the TransactionFormats to parse the Transaction
|
||||
* Permissions, only the GranularPermissionType is defined here. To prevent
|
||||
* conflicts with TxType, the GranularPermissionType is always set to a value
|
||||
* greater than the maximum value of uint16.
|
||||
|
||||
/** Granular sub-operation permission values used by the delegation system.
|
||||
*
|
||||
* Each enumerator targets a specific capability within a parent transaction
|
||||
* type, enabling fine-grained delegation without granting broad transaction-
|
||||
* level authority. For example, `TrustlineFreeze` delegates only the ability
|
||||
* to freeze a trustline via `ttTRUST_SET`, not to authorize or unfreeze.
|
||||
*
|
||||
* All values are greater than `UINT16_MAX` (minimum 65537), which keeps them
|
||||
* numerically disjoint from transaction-level permissions (≤ `UINT16_MAX`).
|
||||
* This invariant is asserted at startup inside the `Permission` constructor.
|
||||
*
|
||||
* Generated from `detail/permissions.macro` via the X-macro pattern. Adding
|
||||
* a new sub-operation requires only a single `PERMISSION(...)` entry in that
|
||||
* file.
|
||||
*/
|
||||
// Macro-generated, complex
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
|
||||
@@ -30,27 +53,67 @@ enum GranularPermissionType : std::uint32_t {
|
||||
#pragma pop_macro("PERMISSION")
|
||||
};
|
||||
|
||||
/** Indicates whether a transaction type may be delegated in bulk via
|
||||
* a transaction-level `DelegateSet` permission.
|
||||
*
|
||||
* The policy for each `TxType` is encoded in `detail/transactions.macro`
|
||||
* as the `delegable` parameter of every `TRANSACTION(...)` entry.
|
||||
* Sensitive types such as `ttACCOUNT_SET` and `ttREGULAR_KEY_SET` are
|
||||
* `NotDelegable`; most operational types are `Delegable`.
|
||||
*
|
||||
* @note Bare enumerators (`xrpl::Delegable` / `xrpl::NotDelegable`) are
|
||||
* required by preprocessor expansions in tests and macro-generated
|
||||
* code; `enum class` would break that usage.
|
||||
*/
|
||||
// Injected bare enumerators (xrpl::delegable / xrpl::notDelegable) are required by preprocessor
|
||||
// tricks in tests and macro-generated code; enum class would break that.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-use-enum-class)
|
||||
enum Delegation { Delegable, NotDelegable };
|
||||
|
||||
/** Central authority for XRPL's account-delegation permission system.
|
||||
*
|
||||
* A Meyer's singleton populated at first call to `getInstance()`. Its
|
||||
* constructor expands `transactions.macro` and `permissions.macro` to build
|
||||
* five immutable lookup maps covering every known transaction type and
|
||||
* granular sub-operation. After construction the maps are never mutated,
|
||||
* so all concurrent read access from transaction-processing threads is safe
|
||||
* without synchronization.
|
||||
*
|
||||
* The two principal call sites are:
|
||||
* - `DelegateSet::preflight()` — calls `isDelegable()` to validate each
|
||||
* `sfPermissionValue` before it is written on-ledger.
|
||||
* - `DelegateUtils` / transactors — call `getGranularTxType()` and related
|
||||
* helpers to enforce granular limits at execution time.
|
||||
*/
|
||||
class Permission
|
||||
{
|
||||
private:
|
||||
Permission();
|
||||
|
||||
/** Maps each `TxType` to the amendment required to use it, or `uint256{}` if none. */
|
||||
std::unordered_map<std::uint16_t, uint256> txFeatureMap_;
|
||||
|
||||
/** Maps each `TxType` to its `Delegable` / `NotDelegable` policy tag. */
|
||||
std::unordered_map<std::uint16_t, Delegation> delegableTx_;
|
||||
|
||||
/** Maps granular permission name strings to their `GranularPermissionType` values. */
|
||||
std::unordered_map<std::string, GranularPermissionType> granularPermissionMap_;
|
||||
|
||||
/** Maps `GranularPermissionType` values to their name strings (inverse of `granularPermissionMap_`). */
|
||||
std::unordered_map<GranularPermissionType, std::string> granularNameMap_;
|
||||
|
||||
/** Maps each `GranularPermissionType` to its parent `TxType`. */
|
||||
std::unordered_map<GranularPermissionType, TxType> granularTxTypeMap_;
|
||||
|
||||
public:
|
||||
/** Returns the process-wide singleton instance.
|
||||
*
|
||||
* Initialized on first call via a function-local `static`; C++11
|
||||
* guarantees thread-safe initialization. The instance is never mutated
|
||||
* after construction.
|
||||
*
|
||||
* @return A `const` reference to the singleton `Permission` object.
|
||||
*/
|
||||
static Permission const&
|
||||
getInstance();
|
||||
|
||||
@@ -58,29 +121,125 @@ public:
|
||||
Permission&
|
||||
operator=(Permission const&) = delete;
|
||||
|
||||
/** Resolves a raw `sfPermissionValue` to its human-readable name.
|
||||
*
|
||||
* Checks the granular permission table first (values > `UINT16_MAX`).
|
||||
* If unrecognized there, decodes the value as a transaction-level
|
||||
* permission (`value - 1` = `TxType`) and delegates to `TxFormats` for
|
||||
* the canonical name. Used by `STUInt32::getText()` and
|
||||
* `STUInt32::getJson()` to render any `sfPermissionValue` as a string
|
||||
* instead of a raw number.
|
||||
*
|
||||
* @param value Raw `sfPermissionValue` from the ledger.
|
||||
* @return The permission name, or `std::nullopt` if `value` is not
|
||||
* recognized as either a granular or transaction-level permission.
|
||||
*/
|
||||
[[nodiscard]] std::optional<std::string>
|
||||
getPermissionName(std::uint32_t const value) const;
|
||||
|
||||
/** Looks up the numeric wire value of a granular permission by name.
|
||||
*
|
||||
* Used when deserializing `sfPermissionValue` from JSON (e.g., during
|
||||
* `DelegateSet` preflight or RPC input parsing) to convert a
|
||||
* human-readable name like `"TrustlineFreeze"` back to its `uint32_t`
|
||||
* representation.
|
||||
*
|
||||
* @param name Case-sensitive granular permission name.
|
||||
* @return The corresponding `uint32_t` wire value, or `std::nullopt` if
|
||||
* `name` is not a known granular permission.
|
||||
*/
|
||||
[[nodiscard]] std::optional<std::uint32_t>
|
||||
getGranularValue(std::string const& name) const;
|
||||
|
||||
/** Looks up the name of a granular permission by its enum value.
|
||||
*
|
||||
* Inverse of `getGranularValue`; used when serializing a granular
|
||||
* permission value to human-readable output.
|
||||
*
|
||||
* @param value A `GranularPermissionType` enum value.
|
||||
* @return The permission name string, or `std::nullopt` if `value` is
|
||||
* not a known granular permission.
|
||||
*/
|
||||
[[nodiscard]] std::optional<std::string>
|
||||
getGranularName(GranularPermissionType const& value) const;
|
||||
|
||||
/** Returns the parent transaction type for a granular permission.
|
||||
*
|
||||
* Multiple granular permissions share the same parent `TxType`; for
|
||||
* example, `TrustlineAuthorize`, `TrustlineFreeze`, and
|
||||
* `TrustlineUnfreeze` all map to `ttTRUST_SET`. Used by `isDelegable()`
|
||||
* and execution-time helpers to locate the relevant transactor context
|
||||
* and required amendment for a granular sub-operation.
|
||||
*
|
||||
* @param gpType A `GranularPermissionType` enum value.
|
||||
* @return The parent `TxType`, or `std::nullopt` if `gpType` is not a
|
||||
* known granular permission.
|
||||
*/
|
||||
[[nodiscard]] std::optional<TxType>
|
||||
getGranularTxType(GranularPermissionType const& gpType) const;
|
||||
|
||||
/** Returns the amendment required to use a transaction type, if any.
|
||||
*
|
||||
* A `uint256{}` stored in `txFeatureMap_` means the transaction type
|
||||
* requires no enabling amendment. In that case `std::nullopt` is
|
||||
* returned, signalling that the type is unconditionally available.
|
||||
*
|
||||
* @param txType A recognized transaction type.
|
||||
* @return A const reference to the required amendment hash wrapped in
|
||||
* `std::optional`, or `std::nullopt` if no amendment is required.
|
||||
* @note Asserts in debug builds that `txType` is present in
|
||||
* `txFeatureMap_`. Passing an unregistered `TxType` is a
|
||||
* programming error (a transaction missing from `transactions.macro`).
|
||||
*/
|
||||
[[nodiscard]] std::optional<std::reference_wrapper<uint256 const>>
|
||||
getTxFeature(TxType txType) const;
|
||||
|
||||
/** Determines whether a permission value may appear in a `DelegateSet`
|
||||
* transaction under the current ledger rules.
|
||||
*
|
||||
* The check differs by permission kind:
|
||||
* - **Granular** (value > `UINT16_MAX`): accepted whenever the value
|
||||
* resolves to a known `GranularPermissionType`; no further gate is
|
||||
* applied because granular permissions are inherently narrow.
|
||||
* - **Transaction-level** (value ≤ `UINT16_MAX`): accepted only when the
|
||||
* decoded `TxType` is recognized, its required amendment is currently
|
||||
* enabled in `rules` (or no amendment is required), and the type is
|
||||
* marked `Delegable` in `transactions.macro`.
|
||||
*
|
||||
* @param permissionValue Raw `sfPermissionValue` to validate.
|
||||
* @param rules Active amendment rules for the current ledger.
|
||||
* @return `true` if the permission may be granted, `false` otherwise.
|
||||
* @note The amendment check prevents a transaction type from being
|
||||
* delegated before the ledger feature that introduces it is live,
|
||||
* even if the macro table already includes it.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isDelegable(std::uint32_t const& permissionValue, Rules const& rules) const;
|
||||
|
||||
// for tx level permission, permission value is equal to tx type plus one
|
||||
/** Converts a `TxType` to its transaction-level permission value.
|
||||
*
|
||||
* Transaction-level permissions are encoded as `TxType + 1`. The `+1`
|
||||
* offset ensures zero is never a valid permission value and keeps the
|
||||
* entire range within `uint16` (transaction-level permissions ≤
|
||||
* `UINT16_MAX`).
|
||||
*
|
||||
* @param type A transaction type.
|
||||
* @return The corresponding `uint32_t` permission value (`TxType + 1`).
|
||||
* @see permissionToTxType
|
||||
*/
|
||||
static uint32_t
|
||||
txToPermissionType(TxType const& type);
|
||||
|
||||
// tx type value is permission value minus one
|
||||
/** Converts a transaction-level permission value back to its `TxType`.
|
||||
*
|
||||
* Inverse of `txToPermissionType`. Callers must verify that `value` is
|
||||
* in the transaction-level range (≤ `UINT16_MAX`) before calling; this
|
||||
* function performs no range check.
|
||||
*
|
||||
* @param value A transaction-level permission value (`TxType + 1`).
|
||||
* @return The decoded `TxType` (`value - 1`).
|
||||
* @see txToPermissionType
|
||||
*/
|
||||
static TxType
|
||||
permissionToTxType(uint32_t const& value);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
/** @file
|
||||
* Canonical source of XRPL protocol constants and boundary predicates.
|
||||
*
|
||||
* Every hard-coded numeric limit that, if changed silently, would cause a
|
||||
* **hard fork** — a ledger-state disagreement between nodes running different
|
||||
* software versions — is defined here. All constants are `constexpr` and
|
||||
* therefore available at compile time with zero runtime overhead.
|
||||
*
|
||||
* @note Changing any value in this file without pairing the change with an
|
||||
* amendment-gated detection mechanism will split the network.
|
||||
*
|
||||
* @ingroup protocol
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
@@ -8,100 +21,182 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Protocol specific constants.
|
||||
|
||||
This information is, implicitly, part of the protocol.
|
||||
|
||||
@note Changing these values without adding code to the
|
||||
server to detect "pre-change" and "post-change"
|
||||
will result in a hard fork.
|
||||
|
||||
@ingroup protocol
|
||||
*/
|
||||
/** Smallest legal byte size of a transaction. */
|
||||
/** Smallest legal serialized size of a transaction, in bytes.
|
||||
*
|
||||
* Transactions below this threshold are trivially malformed and are rejected
|
||||
* before deserialization begins.
|
||||
*/
|
||||
std::size_t constexpr kTX_MIN_SIZE_BYTES = 32;
|
||||
|
||||
/** Largest legal byte size of a transaction. */
|
||||
/** Largest legal serialized size of a transaction, in bytes.
|
||||
*
|
||||
* The 1 MB cap protects node memory and network bandwidth. Transactions
|
||||
* exceeding this limit are rejected on receipt without further processing.
|
||||
*/
|
||||
std::size_t constexpr kTX_MAX_SIZE_BYTES = megabytes(1);
|
||||
|
||||
/** The maximum number of unfunded offers to delete at once */
|
||||
/** Maximum number of unfunded offers that may be removed in a single
|
||||
* transaction pass.
|
||||
*
|
||||
* Unfunded-offer cleanup is opportunistic: stale offers are removed as a
|
||||
* side-effect of offer placement. Capping the count keeps the worst-case
|
||||
* execution time of a single transaction predictable.
|
||||
*
|
||||
* @note The asymmetry with `kEXPIRED_OFFER_REMOVE_LIMIT` (1000 vs 256)
|
||||
* reflects that unfunded-offer removal was designed to handle larger
|
||||
* batches; expired offers are discovered through a different, narrower
|
||||
* path.
|
||||
*/
|
||||
std::size_t constexpr kUNFUNDED_OFFER_REMOVE_LIMIT = 1000;
|
||||
|
||||
/** The maximum number of expired offers to delete at once */
|
||||
/** Maximum number of expired offers that may be removed in a single
|
||||
* transaction pass.
|
||||
*
|
||||
* @see kUNFUNDED_OFFER_REMOVE_LIMIT for the rationale behind the asymmetric
|
||||
* cap.
|
||||
*/
|
||||
std::size_t constexpr kEXPIRED_OFFER_REMOVE_LIMIT = 256;
|
||||
|
||||
/** The maximum number of metadata entries allowed in one transaction */
|
||||
/** Maximum number of metadata entries a single transaction may produce.
|
||||
*
|
||||
* When a transaction would exceed this cap the transactor returns
|
||||
* `tecOVERSIZE`, triggering a controlled teardown that applies the fee
|
||||
* and rolls back ledger mutations rather than allowing unbounded metadata
|
||||
* growth.
|
||||
*/
|
||||
std::size_t constexpr kOVERSIZE_META_DATA_CAP = 5200;
|
||||
|
||||
/** The maximum number of entries per directory page */
|
||||
/** Maximum number of entries per owner-directory or offer-directory page.
|
||||
*
|
||||
* Keeping pages small bounds the work required to traverse a directory:
|
||||
* each page hop visits at most 32 entries.
|
||||
*/
|
||||
std::size_t constexpr kDIR_NODE_MAX_ENTRIES = 32;
|
||||
|
||||
/** The maximum number of pages allowed in a directory
|
||||
|
||||
Made obsolete by fixDirectoryLimit amendment.
|
||||
*/
|
||||
/** Historical maximum number of pages in a single directory.
|
||||
*
|
||||
* This limit was enforced before the `fixDirectoryLimit` amendment.
|
||||
* Post-amendment, directories may grow beyond 262 144 pages; this
|
||||
* constant is retained for pre-amendment replay correctness.
|
||||
*
|
||||
* @note Pre-amendment code returns `tecDIR_FULL` when this limit is
|
||||
* reached. Post-amendment, only unsigned-integer overflow can
|
||||
* produce a null page index.
|
||||
*/
|
||||
std::uint64_t constexpr kDIR_NODE_MAX_PAGES = 262144;
|
||||
|
||||
/** The maximum number of items in an NFT page */
|
||||
/** Maximum number of NFToken entries per NFT directory page. */
|
||||
std::size_t constexpr kDIR_MAX_TOKENS_PER_PAGE = 32;
|
||||
|
||||
/** The maximum number of owner directory entries for account to be deletable */
|
||||
/** Maximum number of owner-directory entries an account may hold and still
|
||||
* be eligible for deletion via `AccountDelete`.
|
||||
*
|
||||
* Accounts with more than 1000 directory entries cannot be deleted; this
|
||||
* protects against unbounded cleanup work within a single transaction.
|
||||
*/
|
||||
std::size_t constexpr kMAX_DELETABLE_DIR_ENTRIES = 1000;
|
||||
|
||||
/** The maximum number of token offers that can be canceled at once */
|
||||
/** Maximum number of NFToken offers that may be cancelled in a single
|
||||
* `NFTokenCancelOffer` transaction.
|
||||
*/
|
||||
std::size_t constexpr kMAX_TOKEN_OFFER_CANCEL_COUNT = 500;
|
||||
|
||||
/** The maximum number of offers in an offer directory for NFT to be burnable */
|
||||
/** Maximum number of NFToken offers that must be cleaned up before an NFT
|
||||
* can be burned.
|
||||
*
|
||||
* An NFT with more than 500 live offers cannot be burned until the excess
|
||||
* offers are cancelled first.
|
||||
*/
|
||||
std::size_t constexpr kMAX_DELETABLE_TOKEN_OFFER_ENTRIES = 500;
|
||||
|
||||
/** The maximum token transfer fee allowed.
|
||||
|
||||
Token transfer fees can range from 0% to 50% and are specified in tenths of
|
||||
a basis point; that is a value of 1000 represents a transfer fee of 1% and
|
||||
a value of 10000 represents a transfer fee of 10%.
|
||||
|
||||
Note that for extremely low transfer fees values, it is possible that the
|
||||
calculated fee will be 0.
|
||||
/** Maximum NFToken transfer fee, expressed in tenths of a basis point.
|
||||
*
|
||||
* Transfer fees range from 0% to 50%. A value of 1 000 represents 1% and
|
||||
* a value of 50 000 represents 50%. For very low fee values the computed
|
||||
* fee amount may round down to zero drops.
|
||||
*/
|
||||
std::uint16_t constexpr kMAX_TRANSFER_FEE = 50000;
|
||||
|
||||
/** There are 10,000 basis points (bips) in 100%.
|
||||
/** Number of basis points (bips) in 100% (unity).
|
||||
*
|
||||
* Basis points represent 0.01%.
|
||||
* One basis point equals 0.01%. To compute the share of a value `X`
|
||||
* corresponding to `B` bips, use `X * B / kBIPS_PER_UNITY`. To convert
|
||||
* a whole-percentage `P` to bips, use `P * kBIPS_PER_UNITY / 100`
|
||||
* (or simply call `percentageToBips(P)`).
|
||||
*
|
||||
* Given a value X, to find the amount for B bps,
|
||||
* use X * B / bipsPerUnity
|
||||
* Example: 10% coverage on 999 XRP (999 000 000 drops) =
|
||||
* `999'000'000 * 1'000 / 10'000` = 99 900 000 drops.
|
||||
*
|
||||
* Example: If a loan broker has 999 XRP of debt, and must maintain 1,000 bps of
|
||||
* that debt as cover (10%), then the minimum cover amount is 999,000,000 drops
|
||||
* * 1000 / bipsPerUnity = 99,900,00 drops or 99.9 XRP.
|
||||
*
|
||||
* Given a percentage P, to find the number of bps that percentage represents,
|
||||
* use P * bipsPerUnity.
|
||||
*
|
||||
* Example: 50% is 0.50 * bipsPerUnity = 5,000 bps.
|
||||
* All ledger fee and rate arithmetic uses integer bips to guarantee
|
||||
* bit-identical results across all validator platforms.
|
||||
*/
|
||||
Bips32 constexpr kBIPS_PER_UNITY(100 * 100);
|
||||
static_assert(kBIPS_PER_UNITY == Bips32{10'000});
|
||||
|
||||
/** Number of tenth-basis-points in 100% (unity).
|
||||
*
|
||||
* One tenth-basis-point equals 0.001%. Use `percentageToTenthBips(P)`
|
||||
* to convert a whole percentage, or `tenthBipsOfValue(value, rate)` to
|
||||
* apply a rate to a value.
|
||||
*/
|
||||
TenthBips32 constexpr kTENTH_BIPS_PER_UNITY(kBIPS_PER_UNITY.value() * 10);
|
||||
static_assert(kTENTH_BIPS_PER_UNITY == TenthBips32(100'000));
|
||||
|
||||
/** Convert a whole-percentage value to a strongly-typed `Bips32`.
|
||||
*
|
||||
* Uses integer division; fractional basis points are truncated.
|
||||
*
|
||||
* @param percentage An integer percentage in [0, 100].
|
||||
* @return The equivalent number of basis points as a `Bips32`.
|
||||
*/
|
||||
constexpr Bips32
|
||||
percentageToBips(std::uint32_t percentage)
|
||||
{
|
||||
return Bips32(percentage * kBIPS_PER_UNITY.value() / 100);
|
||||
}
|
||||
|
||||
/** Convert a whole-percentage value to a strongly-typed `TenthBips32`.
|
||||
*
|
||||
* Uses integer division; fractional tenth-bips are truncated.
|
||||
*
|
||||
* @param percentage An integer percentage in [0, 100].
|
||||
* @return The equivalent number of tenth-basis-points as a `TenthBips32`.
|
||||
*/
|
||||
constexpr TenthBips32
|
||||
percentageToTenthBips(std::uint32_t percentage)
|
||||
{
|
||||
return TenthBips32(percentage * kTENTH_BIPS_PER_UNITY.value() / 100);
|
||||
}
|
||||
|
||||
/** Compute the basis-point share of a value using integer arithmetic.
|
||||
*
|
||||
* Calculates `value * bips / kBIPS_PER_UNITY` without floating point,
|
||||
* guaranteeing deterministic results on all platforms.
|
||||
*
|
||||
* @tparam T Numeric type of the value (must support `*` and `/`).
|
||||
* @tparam TBips Underlying storage type of the `Bips` wrapper.
|
||||
* @param value The base amount to take a share of.
|
||||
* @param bips The rate in basis points.
|
||||
* @return The share of `value` at the given rate, truncated toward zero.
|
||||
*/
|
||||
template <typename T, class TBips>
|
||||
constexpr T
|
||||
bipsOfValue(T value, Bips<TBips> bips)
|
||||
{
|
||||
return value * bips.value() / kBIPS_PER_UNITY.value();
|
||||
}
|
||||
|
||||
/** Compute the tenth-basis-point share of a value using integer arithmetic.
|
||||
*
|
||||
* Calculates `value * bips / kTENTH_BIPS_PER_UNITY` without floating
|
||||
* point, guaranteeing deterministic results on all platforms.
|
||||
*
|
||||
* @tparam T Numeric type of the value (must support `*` and `/`).
|
||||
* @tparam TBips Underlying storage type of the `TenthBips` wrapper.
|
||||
* @param value The base amount to take a share of.
|
||||
* @param bips The rate in tenth-basis-points.
|
||||
* @return The share of `value` at the given rate, truncated toward zero.
|
||||
*/
|
||||
template <typename T, class TBips>
|
||||
constexpr T
|
||||
tenthBipsOfValue(T value, TenthBips<TBips> bips)
|
||||
@@ -109,202 +204,293 @@ tenthBipsOfValue(T value, TenthBips<TBips> bips)
|
||||
return value * bips.value() / kTENTH_BIPS_PER_UNITY.value();
|
||||
}
|
||||
|
||||
/** Rate and limit constants specific to the on-ledger lending protocol. */
|
||||
namespace Lending {
|
||||
/** The maximum management fee rate allowed by a loan broker in 1/10 bips.
|
||||
|
||||
Valid values are between 0 and 10% inclusive.
|
||||
*/
|
||||
/** Maximum management fee a LoanBroker may charge, in tenth-basis-points.
|
||||
*
|
||||
* Valid values are in [0, 10%]. Stored as `TenthBips16` (fits in
|
||||
* `uint16_t`) because 10 000 < 65 535.
|
||||
*/
|
||||
TenthBips16 constexpr kMAX_MANAGEMENT_FEE_RATE(
|
||||
unsafeCast<std::uint16_t>(percentageToTenthBips(10).value()));
|
||||
static_assert(kMAX_MANAGEMENT_FEE_RATE == TenthBips16(std::uint16_t(10'000u)));
|
||||
|
||||
/** The maximum coverage rate required of a loan broker in 1/10 bips.
|
||||
|
||||
Valid values are between 0 and 100% inclusive.
|
||||
*/
|
||||
/** Maximum coverage rate a LoanBroker must maintain, in tenth-basis-points.
|
||||
*
|
||||
* The coverage rate specifies the minimum fraction of outstanding loan
|
||||
* debt that the broker must hold as collateral. Valid values are in
|
||||
* [0, 100%].
|
||||
*/
|
||||
TenthBips32 constexpr kMAX_COVER_RATE = percentageToTenthBips(100);
|
||||
static_assert(kMAX_COVER_RATE == TenthBips32(100'000u));
|
||||
|
||||
/** The maximum overpayment fee on a loan in 1/10 bips.
|
||||
*
|
||||
Valid values are between 0 and 100% inclusive.
|
||||
*/
|
||||
/** Maximum overpayment fee on a loan, in tenth-basis-points.
|
||||
*
|
||||
* Applied when a borrower pays more than the scheduled amount. Valid
|
||||
* values are in [0, 100%].
|
||||
*/
|
||||
TenthBips32 constexpr kMAX_OVERPAYMENT_FEE = percentageToTenthBips(100);
|
||||
static_assert(kMAX_OVERPAYMENT_FEE == TenthBips32(100'000u));
|
||||
|
||||
/** Annualized interest rate of the Loan in 1/10 bips.
|
||||
/** Maximum annualized interest rate on a Loan, in tenth-basis-points.
|
||||
*
|
||||
* Valid values are between 0 and 100% inclusive.
|
||||
* Valid values are in [0, 100%].
|
||||
*/
|
||||
TenthBips32 constexpr kMAX_INTEREST_RATE = percentageToTenthBips(100);
|
||||
static_assert(kMAX_INTEREST_RATE == TenthBips32(100'000u));
|
||||
|
||||
/** The maximum premium added to the interest rate for late payments on a loan
|
||||
* in 1/10 bips.
|
||||
/** Maximum late-payment interest premium on a Loan, in tenth-basis-points.
|
||||
*
|
||||
* Valid values are between 0 and 100% inclusive.
|
||||
* This rate is added to the base interest rate when payments are overdue.
|
||||
* Valid values are in [0, 100%].
|
||||
*/
|
||||
TenthBips32 constexpr kMAX_LATE_INTEREST_RATE = percentageToTenthBips(100);
|
||||
static_assert(kMAX_LATE_INTEREST_RATE == TenthBips32(100'000u));
|
||||
|
||||
/** The maximum close interest rate charged for repaying a loan early in 1/10
|
||||
* bips.
|
||||
/** Maximum early-repayment (close) interest rate on a Loan, in
|
||||
* tenth-basis-points.
|
||||
*
|
||||
* Valid values are between 0 and 100% inclusive.
|
||||
* Charged when a borrower repays a loan ahead of schedule. Valid values
|
||||
* are in [0, 100%].
|
||||
*/
|
||||
TenthBips32 constexpr kMAX_CLOSE_INTEREST_RATE = percentageToTenthBips(100);
|
||||
static_assert(kMAX_CLOSE_INTEREST_RATE == TenthBips32(100'000u));
|
||||
|
||||
/** The maximum overpayment interest rate charged on loan overpayments in 1/10
|
||||
* bips.
|
||||
/** Maximum overpayment interest rate charged on loan overpayments, in
|
||||
* tenth-basis-points.
|
||||
*
|
||||
* Valid values are between 0 and 100% inclusive.
|
||||
* Valid values are in [0, 100%].
|
||||
*/
|
||||
TenthBips32 constexpr kMAX_OVERPAYMENT_INTEREST_RATE = percentageToTenthBips(100);
|
||||
static_assert(kMAX_OVERPAYMENT_INTEREST_RATE == TenthBips32(100'000u));
|
||||
|
||||
/** LoanPay transaction cost will be one base fee per X combined payments
|
||||
/** Number of loan payments per base-fee increment charged by `LoanPay`.
|
||||
*
|
||||
* The number of payments is estimated based on the Amount paid and the Loan's
|
||||
* Fixed Payment size. Overpayments (indicated with the tfLoanOverpayment flag)
|
||||
* count as one more payment.
|
||||
* The fee is estimated from the transaction `Amount` divided by the
|
||||
* loan's fixed payment size. Overpayments (flagged with
|
||||
* `tfLoanOverpayment`) count as one additional payment in the estimate.
|
||||
* One base fee unit is charged for every 5 estimated payments.
|
||||
*
|
||||
* This number was chosen arbitrarily, but should not be changed once released
|
||||
* without an amendment
|
||||
* @note This value was chosen arbitrarily and is amendment-locked once
|
||||
* released: changing it without an amendment would alter the fee
|
||||
* schedule for existing `LoanPay` transactions.
|
||||
* @see kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION
|
||||
*/
|
||||
static constexpr int kLOAN_PAYMENTS_PER_FEE_INCREMENT = 5;
|
||||
|
||||
/** Maximum number of combined payments that a LoanPay transaction will process
|
||||
/** Hard cap on the number of combined payments processed by one `LoanPay`.
|
||||
*
|
||||
* This limit is enforced during the loan payment process, and thus is not
|
||||
* estimated. If the limit is hit, no further payments or overpayments will be
|
||||
* processed, no matter how much of the transaction Amount is left, but the
|
||||
* transaction will succeed with the payments that have been processed up to
|
||||
* that point.
|
||||
* This limit is enforced during execution, not during fee estimation.
|
||||
* When the cap is reached the transaction succeeds with the payments
|
||||
* processed so far; any remaining `Amount` is not applied.
|
||||
*
|
||||
* This limit is independent of loanPaymentsPerFeeIncrement, so a transaction
|
||||
* could potentially be charged for many more payments than actually get
|
||||
* processed. Users should take care not to submit a transaction paying more
|
||||
* than loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment. Because
|
||||
* overpayments are charged as a payment, if submitting
|
||||
* loanMaximumPaymentsPerTransaction * Loan.PeriodicPayment, users should not
|
||||
* set the tfLoanOverpayment flag.
|
||||
* Because the fee is based on the *estimated* payment count (derived from
|
||||
* `Amount / PeriodicPayment`) and the cap is enforced on the *actual*
|
||||
* count, a transaction can be charged for more payments than it processes.
|
||||
* Submitters should not exceed
|
||||
* `kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION * Loan.PeriodicPayment` in
|
||||
* `Amount`, and should omit `tfLoanOverpayment` if paying exactly that
|
||||
* much.
|
||||
*
|
||||
* Even though they're independent, loanMaximumPaymentsPerTransaction should be
|
||||
* a multiple of loanPaymentsPerFeeIncrement.
|
||||
*
|
||||
* This number was chosen arbitrarily, but should not be changed once released
|
||||
* without an amendment
|
||||
* @note `kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION` must remain a multiple
|
||||
* of `kLOAN_PAYMENTS_PER_FEE_INCREMENT`; this invariant is checked
|
||||
* at startup via `static_assert` in LoanPay.cpp. Both values are
|
||||
* amendment-locked once released.
|
||||
*/
|
||||
static constexpr int kLOAN_MAXIMUM_PAYMENTS_PER_TRANSACTION = 100;
|
||||
} // namespace Lending
|
||||
|
||||
/** The maximum length of a URI inside an NFT */
|
||||
/** Maximum byte length of a URI stored in an NFToken. */
|
||||
std::size_t constexpr kMAX_TOKEN_URI_LENGTH = 256;
|
||||
|
||||
/** The maximum length of a Data element inside a DID */
|
||||
/** Maximum byte length of the `Data` field (DID document) in a DID object. */
|
||||
std::size_t constexpr kMAX_DID_DOCUMENT_LENGTH = 256;
|
||||
|
||||
/** The maximum length of a URI inside a DID */
|
||||
/** Maximum byte length of the `URI` field in a DID object. */
|
||||
std::size_t constexpr kMAX_DIDURI_LENGTH = 256;
|
||||
|
||||
/** The maximum length of an Attestation inside a DID */
|
||||
/** Maximum byte length of the `Attestation` field in a DID object. */
|
||||
std::size_t constexpr kMAX_DID_DATA_LENGTH = 256;
|
||||
|
||||
/** The maximum length of a domain */
|
||||
/** Maximum byte length of an account `Domain` field. */
|
||||
std::size_t constexpr kMAX_DOMAIN_LENGTH = 256;
|
||||
|
||||
/** The maximum length of a URI inside a Credential */
|
||||
/** Maximum byte length of the `URI` field in a Credential object. */
|
||||
std::size_t constexpr kMAX_CREDENTIAL_URI_LENGTH = 256;
|
||||
|
||||
/** The maximum length of a CredentialType inside a Credential */
|
||||
/** Maximum byte length of the `CredentialType` field in a Credential object.
|
||||
*
|
||||
* Narrower than the 256-byte default to keep credential-type strings
|
||||
* human-readable and prevent abuse of the type field as an arbitrary blob.
|
||||
*/
|
||||
std::size_t constexpr kMAX_CREDENTIAL_TYPE_LENGTH = 64;
|
||||
|
||||
/** The maximum number of credentials can be passed in array */
|
||||
/** Maximum number of credentials that may appear in a transaction's
|
||||
* `Credentials` array.
|
||||
*/
|
||||
std::size_t constexpr kMAX_CREDENTIALS_ARRAY_SIZE = 8;
|
||||
|
||||
/** The maximum number of credentials can be passed in array for permissioned
|
||||
* domain */
|
||||
/** Maximum number of credentials that a permissioned domain may reference. */
|
||||
std::size_t constexpr kMAX_PERMISSIONED_DOMAIN_CREDENTIALS_ARRAY_SIZE = 10;
|
||||
|
||||
/** The maximum length of MPTokenMetadata */
|
||||
/** Maximum byte length of the `MPTokenMetadata` field on an MPTokenIssuance. */
|
||||
std::size_t constexpr kMAX_MP_TOKEN_METADATA_LENGTH = 1024;
|
||||
|
||||
/** The maximum amount of MPTokenIssuance */
|
||||
/** Maximum quantity representable by an MPToken amount field.
|
||||
*
|
||||
* Equal to `INT64_MAX` (2^63 − 1). The `static_assert` below guarantees
|
||||
* that the XRPL `Number` type can represent every valid MPToken quantity
|
||||
* without overflow.
|
||||
*/
|
||||
std::uint64_t constexpr kMAX_MP_TOKEN_AMOUNT = 0x7FFF'FFFF'FFFF'FFFFull;
|
||||
static_assert(Number::kMAX_REP >= kMAX_MP_TOKEN_AMOUNT);
|
||||
|
||||
/** The maximum length of Data payload */
|
||||
/** Maximum byte length of the `Data` payload field. */
|
||||
std::size_t constexpr kMAX_DATA_PAYLOAD_LENGTH = 256;
|
||||
|
||||
/** Vault withdrawal policies */
|
||||
/** Vault withdrawal policy: first-come, first-served.
|
||||
*
|
||||
* The numeric value 1 is the wire-stable identifier for this strategy;
|
||||
* it must not change once released.
|
||||
*/
|
||||
std::uint8_t constexpr kVAULT_STRATEGY_FIRST_COME_FIRST_SERVE = 1;
|
||||
|
||||
/** Default IOU scale factor for a Vault */
|
||||
/** Default IOU-to-share scale exponent for a Vault.
|
||||
*
|
||||
* When no explicit scale is specified at Vault creation the scale
|
||||
* defaults to 6, meaning one IOU unit maps to 10^6 shares. This
|
||||
* applies only to IOU-backed vaults; native-asset and MPT vaults always
|
||||
* use scale 0.
|
||||
*/
|
||||
std::uint8_t constexpr kVAULT_DEFAULT_IOU_SCALE = 6;
|
||||
/** Maximum scale factor for a Vault. The number is chosen to ensure that
|
||||
1 IOU can be always converted to shares.
|
||||
10^19 > maxMPTokenAmount (2^64-1) > 10^18 */
|
||||
|
||||
/** Maximum IOU-to-share scale exponent for a Vault.
|
||||
*
|
||||
* Chosen so that exactly one IOU unit can always be converted to at
|
||||
* least one share: 10^19 > `kMAX_MP_TOKEN_AMOUNT` (≈ 2^63) > 10^18.
|
||||
* Preflight rejects any `VaultCreate` that specifies a scale above this
|
||||
* value with `temMALFORMED`. Applies only to IOU-backed vaults.
|
||||
*/
|
||||
std::uint8_t constexpr kVAULT_MAXIMUM_IOU_SCALE = 18;
|
||||
|
||||
/** Maximum recursion depth for vault shares being put as an asset inside
|
||||
* another vault; counted from 0 */
|
||||
/** Maximum recursion depth when checking whether a vault's asset is itself
|
||||
* backed by another vault.
|
||||
*
|
||||
* Counted from 0, so a depth of 5 permits at most 6 levels of nesting.
|
||||
* This prevents pathological chains from consuming unbounded stack space
|
||||
* during asset-validation traversal.
|
||||
*/
|
||||
std::uint8_t constexpr kMAX_ASSET_CHECK_DEPTH = 5;
|
||||
|
||||
/** A ledger index. */
|
||||
/** Ledger sequence number type.
|
||||
*
|
||||
* A named alias for `uint32_t` that makes function signatures
|
||||
* self-documenting wherever ledger positions are passed.
|
||||
*/
|
||||
using LedgerIndex = std::uint32_t;
|
||||
|
||||
/** Number of ledgers between consecutive flag-ledger boundaries.
|
||||
*
|
||||
* Every 256 ledgers the network applies accumulated validator votes for
|
||||
* fee adjustments, reserve requirements, amendment activation, and
|
||||
* Negative UNL reliability scoring. Both `isFlagLedger()` and
|
||||
* `isVotingLedger()` test `seq % kFLAG_LEDGER_INTERVAL == 0`; the
|
||||
* semantic distinction between the two predicates is resolved by callers
|
||||
* via a `+1` offset on the sequence number they pass.
|
||||
*
|
||||
* @note This constant is an implicit part of the wire protocol. Changing
|
||||
* it without an amendment-gated migration path will cause a hard fork.
|
||||
*/
|
||||
std::uint32_t constexpr kFLAG_LEDGER_INTERVAL = 256;
|
||||
|
||||
/** Returns true if the given ledgerIndex is a voting ledgerIndex */
|
||||
/** Return `true` if @p seq is a voting ledger.
|
||||
*
|
||||
* Semantically, this asks: "will the ledger built *on top of* `seq`
|
||||
* be a flag ledger?" Callers therefore pass `seq + 1` (the sequence of
|
||||
* the ledger currently being assembled). `RCLConsensus` uses this
|
||||
* predicate to decide whether to inject Negative UNL pseudo-transactions
|
||||
* for the new consensus round.
|
||||
*
|
||||
* The arithmetic is identical to `isFlagLedger`; the two names exist to
|
||||
* make the `+1` offset explicit at each call site without embedding it
|
||||
* inside these functions.
|
||||
*
|
||||
* @param seq The ledger index to test (typically the previous ledger's
|
||||
* sequence plus one).
|
||||
* @return `true` if `seq % kFLAG_LEDGER_INTERVAL == 0`.
|
||||
* @see isFlagLedger
|
||||
*/
|
||||
bool
|
||||
isVotingLedger(LedgerIndex seq);
|
||||
|
||||
/** Returns true if the given ledgerIndex is a flag ledgerIndex */
|
||||
/** Return `true` if @p seq is a flag ledger.
|
||||
*
|
||||
* A flag ledger is any ledger whose sequence number is an exact multiple
|
||||
* of `kFLAG_LEDGER_INTERVAL` (256). It is the ledger in which fee-vote
|
||||
* and amendment pseudo-transactions are applied, and in which Negative
|
||||
* UNL reliability updates take effect. `Change::doApply` and
|
||||
* `FeeVoteImpl` gate their parameter-update logic on this predicate.
|
||||
*
|
||||
* Callers pass the ledger's **own** sequence number to ask "has this
|
||||
* ledger already crossed the boundary?", as opposed to `isVotingLedger`,
|
||||
* which is called with `seq + 1`.
|
||||
*
|
||||
* @param seq The ledger index to test.
|
||||
* @return `true` if `seq % kFLAG_LEDGER_INTERVAL == 0`.
|
||||
* @see isVotingLedger
|
||||
*/
|
||||
bool
|
||||
isFlagLedger(LedgerIndex seq);
|
||||
|
||||
/** A transaction identifier.
|
||||
The value is computed as the hash of the
|
||||
canonicalized, serialized transaction object.
|
||||
*/
|
||||
/** Transaction identifier type.
|
||||
*
|
||||
* A 256-bit hash computed over the canonicalized, serialized transaction
|
||||
* object using `HashPrefix::transactionID` as the domain separator.
|
||||
*/
|
||||
using TxID = uint256;
|
||||
|
||||
/** The maximum number of trustlines to delete as part of AMM account
|
||||
* deletion cleanup.
|
||||
/** Maximum number of AMM trust lines that may be deleted as part of an
|
||||
* AMM account-deletion cleanup pass.
|
||||
*/
|
||||
std::uint16_t constexpr kMAX_DELETABLE_AMM_TRUST_LINES = 512;
|
||||
|
||||
/** The maximum length of a URI inside an Oracle */
|
||||
/** Maximum byte length of the `URI` field in an Oracle object. */
|
||||
std::size_t constexpr kMAX_ORACLE_URI = 256;
|
||||
|
||||
/** The maximum length of a Provider inside an Oracle */
|
||||
/** Maximum byte length of the `Provider` field in an Oracle object. */
|
||||
std::size_t constexpr kMAX_ORACLE_PROVIDER = 256;
|
||||
|
||||
/** The maximum size of a data series array inside an Oracle */
|
||||
/** Maximum number of price data-series entries in an Oracle object. */
|
||||
std::size_t constexpr kMAX_ORACLE_DATA_SERIES = 10;
|
||||
|
||||
/** The maximum length of a SymbolClass inside an Oracle */
|
||||
/** Maximum byte length of the `SymbolClass` field in an Oracle object. */
|
||||
std::size_t constexpr kMAX_ORACLE_SYMBOL_CLASS = 16;
|
||||
|
||||
/** The maximum allowed time difference between lastUpdateTime and the time
|
||||
of the last closed ledger
|
||||
*/
|
||||
/** Maximum allowed age of an Oracle price update, in seconds.
|
||||
*
|
||||
* `OracleSet` rejects updates whose `LastUpdateTime` differs from the
|
||||
* last-closed-ledger close time by more than 300 seconds (5 minutes).
|
||||
*/
|
||||
std::size_t constexpr kMAX_LAST_UPDATE_TIME_DELTA = 300;
|
||||
|
||||
/** The maximum price scaling factor
|
||||
*/
|
||||
/** Maximum price-scaling exponent accepted in an Oracle object. */
|
||||
std::size_t constexpr kMAX_PRICE_SCALE = 20;
|
||||
|
||||
/** The maximum percentage of outliers to trim
|
||||
/** Maximum percentage of outlier data points to trim in Oracle price
|
||||
* aggregation.
|
||||
*/
|
||||
std::size_t constexpr kMAX_TRIM = 25;
|
||||
|
||||
/** The maximum number of delegate permissions an account can grant
|
||||
*/
|
||||
/** Maximum number of granular delegate permissions an account may grant. */
|
||||
std::size_t constexpr kPERMISSION_MAX_SIZE = 10;
|
||||
|
||||
/** The maximum number of transactions that can be in a batch. */
|
||||
/** Maximum number of inner transactions in a single Batch transaction.
|
||||
*
|
||||
* Enforced during preflight; batches exceeding this count are rejected.
|
||||
* The limit directly bounds the worst-case compute cost for batch
|
||||
* signature validation and fee calculation.
|
||||
*/
|
||||
std::size_t constexpr kMAX_BATCH_TX_COUNT = 8;
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -16,35 +16,31 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** A public key.
|
||||
|
||||
Public keys are used in the public-key cryptography
|
||||
system used to verify signatures attached to messages.
|
||||
|
||||
The format of the public key is XRPL specific,
|
||||
information needed to determine the cryptosystem
|
||||
parameters used is stored inside the key.
|
||||
|
||||
As of this writing two systems are supported:
|
||||
|
||||
secp256k1
|
||||
ed25519
|
||||
|
||||
secp256k1 public keys consist of a 33 byte
|
||||
compressed public key, with the lead byte equal
|
||||
to 0x02 or 0x03.
|
||||
|
||||
The ed25519 public keys consist of a 1 byte
|
||||
prefix constant 0xED, followed by 32 bytes of
|
||||
public key data.
|
||||
*/
|
||||
/** Immutable 33-byte value type holding an XRPL public key.
|
||||
*
|
||||
* Supports both secp256k1 and Ed25519 cryptosystems. The lead byte acts as
|
||||
* a self-describing type tag — `0x02`/`0x03` for secp256k1 compressed keys,
|
||||
* `0xED` for Ed25519 keys (an XRPL-specific prefix that pads the native
|
||||
* 32-byte Ed25519 key to the common 33-byte size). This uniform encoding
|
||||
* allows `publicKeyType()` to identify the algorithm in O(1) from the raw
|
||||
* bytes alone, with no external metadata.
|
||||
*
|
||||
* The default constructor is deleted; the only construction path is from a
|
||||
* `Slice`. If the slice does not represent a recognized key format,
|
||||
* construction calls `LogicError` (process termination) rather than
|
||||
* throwing — an invalid key at this point indicates a programming error,
|
||||
* not a recoverable runtime condition. Any live `PublicKey` object is
|
||||
* therefore always well-formed and algorithm-identified.
|
||||
*
|
||||
* The implicit conversion to `Slice` is intentional: it lets `PublicKey`
|
||||
* flow into serialization and hashing APIs without explicit casting.
|
||||
*/
|
||||
class PublicKey
|
||||
{
|
||||
protected:
|
||||
// All the constructed public keys are valid, non-empty and contain 33
|
||||
// bytes of data.
|
||||
/** Uniform storage size in bytes for all supported key types. */
|
||||
static constexpr std::size_t kSIZE = 33;
|
||||
std::uint8_t buf_[kSIZE]{}; // should be large enough
|
||||
std::uint8_t buf_[kSIZE]{};
|
||||
|
||||
public:
|
||||
using const_iterator = std::uint8_t const*;
|
||||
@@ -56,72 +52,89 @@ public:
|
||||
PublicKey&
|
||||
operator=(PublicKey const& other);
|
||||
|
||||
/** Create a public key.
|
||||
|
||||
Preconditions:
|
||||
publicKeyType(slice) != std::nullopt
|
||||
*/
|
||||
/** Construct from a raw byte slice.
|
||||
*
|
||||
* Copies exactly 33 bytes from `slice` after verifying that the bytes
|
||||
* represent a recognized key format (secp256k1 or Ed25519). Calls
|
||||
* `LogicError` — terminating the process — if the slice is undersized
|
||||
* or does not pass `publicKeyType()`.
|
||||
*
|
||||
* @param slice Raw bytes to construct from; must satisfy
|
||||
* `publicKeyType(slice) != std::nullopt`.
|
||||
* @note Use `publicKeyType()` to validate untrusted input before
|
||||
* constructing; `parseBase58<PublicKey>` does this automatically
|
||||
* for Base58-encoded keys.
|
||||
*/
|
||||
explicit PublicKey(Slice const& slice);
|
||||
|
||||
/** Return a pointer to the raw 33-byte key buffer. */
|
||||
[[nodiscard]] std::uint8_t const*
|
||||
data() const noexcept
|
||||
{
|
||||
return buf_;
|
||||
}
|
||||
|
||||
/** Return the fixed size of all `PublicKey` objects (always 33). */
|
||||
static std::size_t
|
||||
size() noexcept
|
||||
{
|
||||
return kSIZE;
|
||||
}
|
||||
|
||||
/** Return an iterator to the first byte of the key buffer. */
|
||||
[[nodiscard]] const_iterator
|
||||
begin() const noexcept
|
||||
{
|
||||
return buf_;
|
||||
}
|
||||
|
||||
/** Return a const iterator to the first byte of the key buffer. */
|
||||
[[nodiscard]] const_iterator
|
||||
cbegin() const noexcept
|
||||
{
|
||||
return buf_;
|
||||
}
|
||||
|
||||
/** Return an iterator past the last byte of the key buffer. */
|
||||
[[nodiscard]] const_iterator
|
||||
end() const noexcept
|
||||
{
|
||||
return buf_ + kSIZE;
|
||||
}
|
||||
|
||||
/** Return a const iterator past the last byte of the key buffer. */
|
||||
[[nodiscard]] const_iterator
|
||||
cend() const noexcept
|
||||
{
|
||||
return buf_ + kSIZE;
|
||||
}
|
||||
|
||||
/** Return a `Slice` view over the 33-byte key buffer. */
|
||||
[[nodiscard]] Slice
|
||||
slice() const noexcept
|
||||
{
|
||||
return {buf_, kSIZE};
|
||||
}
|
||||
|
||||
/** Implicit conversion to `Slice` for use with serialization APIs. */
|
||||
operator Slice() const noexcept
|
||||
{
|
||||
return slice();
|
||||
}
|
||||
};
|
||||
|
||||
/** Print the public key to a stream.
|
||||
*/
|
||||
/** Write the public key as a hex string to a stream. */
|
||||
std::ostream&
|
||||
operator<<(std::ostream& os, PublicKey const& pk);
|
||||
|
||||
/** Return `true` if both keys hold identical 33-byte representations. */
|
||||
inline bool
|
||||
operator==(PublicKey const& lhs, PublicKey const& rhs)
|
||||
{
|
||||
return std::memcmp(lhs.data(), rhs.data(), rhs.size()) == 0;
|
||||
}
|
||||
|
||||
/** Return `true` if `lhs` is lexicographically less than `rhs`. */
|
||||
inline bool
|
||||
operator<(PublicKey const& lhs, PublicKey const& rhs)
|
||||
{
|
||||
@@ -129,6 +142,15 @@ operator<(PublicKey const& lhs, PublicKey const& rhs)
|
||||
lhs.data(), lhs.data() + lhs.size(), rhs.data(), rhs.data() + rhs.size());
|
||||
}
|
||||
|
||||
/** Feed the raw 33-byte key into a hash algorithm.
|
||||
*
|
||||
* Enables `PublicKey` to be used as a key in unordered containers via
|
||||
* `boost::hash` or any other `hash_append`-compatible hasher.
|
||||
*
|
||||
* @tparam Hasher A `hash_append`-compatible hasher type.
|
||||
* @param h The hasher to feed bytes into.
|
||||
* @param pk The key whose bytes are appended.
|
||||
*/
|
||||
template <class Hasher>
|
||||
void
|
||||
hash_append(Hasher& h, PublicKey const& pk)
|
||||
@@ -136,6 +158,13 @@ hash_append(Hasher& h, PublicKey const& pk)
|
||||
h(pk.data(), pk.size());
|
||||
}
|
||||
|
||||
/** Serialization bridge between `STBlob` fields and `PublicKey` values.
|
||||
*
|
||||
* This specialization plugs `PublicKey` into XRPL's typed serialization
|
||||
* framework. It allows `get<PublicKey>` and `set<PublicKey>` on `STBlob`
|
||||
* fields in serialized ledger objects and transactions without any
|
||||
* conversion boilerplate at call sites.
|
||||
*/
|
||||
template <>
|
||||
struct STExchange<STBlob, PublicKey>
|
||||
{
|
||||
@@ -143,12 +172,14 @@ struct STExchange<STBlob, PublicKey>
|
||||
|
||||
using value_type = PublicKey;
|
||||
|
||||
/** Read a `PublicKey` from an `STBlob` field into `t`. */
|
||||
static void
|
||||
get(std::optional<value_type>& t, STBlob const& u)
|
||||
{
|
||||
t.emplace(Slice(u.data(), u.size()));
|
||||
}
|
||||
|
||||
/** Write a `PublicKey` into a new `STBlob` for the given field. */
|
||||
static std::unique_ptr<STBlob>
|
||||
set(SField const& f, PublicKey const& t)
|
||||
{
|
||||
@@ -158,55 +189,86 @@ struct STExchange<STBlob, PublicKey>
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/** Encode a public key as a Base58Check string with a token-type prefix.
|
||||
*
|
||||
* @param type The `TokenType` prefix to use (e.g. `TokenType::NodePublic`
|
||||
* for validator keys, `TokenType::AccountPublic` for signing keys).
|
||||
* @param pk The key to encode.
|
||||
* @return The Base58Check-encoded string.
|
||||
*/
|
||||
inline std::string
|
||||
toBase58(TokenType type, PublicKey const& pk)
|
||||
{
|
||||
return encodeBase58Token(type, pk.data(), pk.size());
|
||||
}
|
||||
|
||||
/** Decode a Base58Check-encoded public key.
|
||||
*
|
||||
* Validates the token-type prefix and that the decoded bytes represent a
|
||||
* recognized key format. Safe to call on untrusted input.
|
||||
*
|
||||
* @param type The expected `TokenType` prefix.
|
||||
* @param s The Base58Check-encoded string to decode.
|
||||
* @return A `PublicKey` on success, or `std::nullopt` if the string is
|
||||
* malformed, uses the wrong token type, or the decoded bytes are not
|
||||
* a valid secp256k1 or Ed25519 key.
|
||||
*/
|
||||
template <>
|
||||
std::optional<PublicKey>
|
||||
parseBase58(TokenType type, std::string const& s);
|
||||
|
||||
enum class ECDSACanonicality { Canonical, FullyCanonical };
|
||||
/** Canonicality level of a DER-encoded secp256k1 ECDSA signature.
|
||||
*
|
||||
* For any signed message, both `(R, S)` and `(R, G-S)` are mathematically
|
||||
* valid ECDSA signatures (where G is the secp256k1 curve order). Accepting
|
||||
* both enables transaction malleability attacks. XRPL prevents this by
|
||||
* requiring *fully canonical* signatures — where `S ≤ G-S` — for new
|
||||
* transactions.
|
||||
*/
|
||||
enum class ECDSACanonicality {
|
||||
/** Both R and S are in `[1, G)` with no redundant zero padding, but
|
||||
* `S > G/2`. Structurally valid; may be accepted in legacy contexts. */
|
||||
Canonical,
|
||||
/** Both R and S are in `[1, G)` and `S ≤ G-S`, making the signature
|
||||
* unique and immune to the malleability flip. Required for new XRPL
|
||||
* transactions. */
|
||||
FullyCanonical
|
||||
};
|
||||
|
||||
/** Determines the canonicality of a signature.
|
||||
|
||||
A canonical signature is in its most reduced form.
|
||||
For example the R and S components do not contain
|
||||
additional leading zeroes. However, even in
|
||||
canonical form, (R,S) and (R,G-S) are both
|
||||
valid signatures for message M.
|
||||
|
||||
Therefore, to prevent malleability attacks we
|
||||
define a fully canonical signature as one where:
|
||||
|
||||
R < G - S
|
||||
|
||||
where G is the curve order.
|
||||
|
||||
This routine returns std::nullopt if the format
|
||||
of the signature is invalid (for example, the
|
||||
points are encoded incorrectly).
|
||||
|
||||
@return std::nullopt if the signature fails
|
||||
validity checks.
|
||||
|
||||
@note Only the format of the signature is checked,
|
||||
no verification cryptography is performed.
|
||||
*/
|
||||
/** Determine the canonicality of a DER-encoded secp256k1 ECDSA signature.
|
||||
*
|
||||
* Validates the DER structure (`0x30 <len> 0x02 <R> 0x02 <S>`), checks
|
||||
* that R and S are properly encoded integers (no negative encoding, no
|
||||
* redundant zero padding), and compares them against the secp256k1 curve
|
||||
* order G. Returns `FullyCanonical` when `S ≤ G-S`, `Canonical` when
|
||||
* `S > G-S` but the signature is otherwise structurally sound.
|
||||
*
|
||||
* @param sig DER-encoded ECDSA signature to examine.
|
||||
* @return `ECDSACanonicality::FullyCanonical` if `S ≤ G-S`,
|
||||
* `ECDSACanonicality::Canonical` if `S > G-S` but structurally valid,
|
||||
* or `std::nullopt` if the encoding is malformed (wrong header bytes,
|
||||
* invalid integer components, R or S outside the curve order, or
|
||||
* trailing bytes present).
|
||||
* @note Only the structure and canonicality of the encoding are checked;
|
||||
* no cryptographic verification is performed.
|
||||
*/
|
||||
std::optional<ECDSACanonicality>
|
||||
ecdsaCanonicality(Slice const& sig);
|
||||
|
||||
/** Returns the type of public key.
|
||||
|
||||
@return std::nullopt If the public key does not
|
||||
represent a known type.
|
||||
*/
|
||||
/** Determine the algorithm encoded in a public key.
|
||||
*
|
||||
* Uses the lead byte as a self-describing type tag: `0xED` → Ed25519;
|
||||
* `0x02`/`0x03` → secp256k1 compressed. Any other lead byte, or a slice
|
||||
* that is not exactly 33 bytes, is unrecognized.
|
||||
*
|
||||
* @return The detected `KeyType`, or `std::nullopt` if the bytes do not
|
||||
* match a known key format.
|
||||
*/
|
||||
/** @{ */
|
||||
[[nodiscard]] std::optional<KeyType>
|
||||
publicKeyType(Slice const& slice);
|
||||
|
||||
/** @copydoc publicKeyType(Slice const&) */
|
||||
[[nodiscard]] inline std::optional<KeyType>
|
||||
publicKeyType(PublicKey const& publicKey)
|
||||
{
|
||||
@@ -214,7 +276,24 @@ publicKeyType(PublicKey const& publicKey)
|
||||
}
|
||||
/** @} */
|
||||
|
||||
/** Verify a secp256k1 signature on the digest of a message. */
|
||||
/** Verify a secp256k1 ECDSA signature against a pre-computed digest.
|
||||
*
|
||||
* Validates DER structure and canonicality before calling libsecp256k1.
|
||||
* When `mustBeFullyCanonical` is `false` and the signature is merely
|
||||
* canonical (S > G/2), the S component is normalized to its low form via
|
||||
* `secp256k1_ecdsa_signature_normalize` before verification — preserving
|
||||
* backward compatibility without accepting truly malformed encodings.
|
||||
*
|
||||
* @param publicKey A secp256k1 public key. Passing an Ed25519 key calls
|
||||
* `LogicError` (programming error).
|
||||
* @param digest The 256-bit digest over which the signature was produced.
|
||||
* @param sig DER-encoded ECDSA signature.
|
||||
* @param mustBeFullyCanonical If `true` (default), reject signatures where
|
||||
* `S > G/2`. If `false`, accept them after S normalization.
|
||||
* @return `true` if the signature is cryptographically valid for the given
|
||||
* key and digest; `false` for any structural, canonicality, or
|
||||
* cryptographic failure.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
verifyDigest(
|
||||
PublicKey const& publicKey,
|
||||
@@ -222,22 +301,65 @@ verifyDigest(
|
||||
Slice const& sig,
|
||||
bool mustBeFullyCanonical = true) noexcept;
|
||||
|
||||
/** Verify a signature on a message.
|
||||
With secp256k1 signatures, the data is first hashed with
|
||||
SHA512-Half, and the resulting digest is signed.
|
||||
*/
|
||||
/** Verify a signature over a raw message for either supported key type.
|
||||
*
|
||||
* Dispatches on the cryptosystem detected from `publicKey`:
|
||||
* - **secp256k1**: hashes `m` with SHA512-Half (256-bit digest) and
|
||||
* delegates to `verifyDigest` with `mustBeFullyCanonical = true`.
|
||||
* - **Ed25519**: checks that the signature scalar S is below the Ed25519
|
||||
* subgroup order, then calls the underlying `ed25519_sign_open` library
|
||||
* after stripping the XRPL-specific `0xED` prefix byte that the library
|
||||
* does not understand.
|
||||
*
|
||||
* @param publicKey The public key to verify against.
|
||||
* @param m The message that was signed (raw bytes, not pre-hashed).
|
||||
* @param sig The signature to verify.
|
||||
* @return `true` if the signature is valid; `false` for any failure
|
||||
* including unrecognized key type, non-canonical signature, or
|
||||
* cryptographic mismatch.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
verify(PublicKey const& publicKey, Slice const& m, Slice const& sig) noexcept;
|
||||
|
||||
/** Calculate the 160-bit node ID from a node public key. */
|
||||
/** Derive the 160-bit node identity from a public key.
|
||||
*
|
||||
* Applies RIPEMD-160(SHA-256(pubkey)) to produce the `NodeID` used in the
|
||||
* peer-to-peer layer for validator routing and consensus tracking.
|
||||
*
|
||||
* @param pk The validator's public key (secp256k1 or Ed25519).
|
||||
* @return The 160-bit `NodeID` identifying the validator on the network.
|
||||
*/
|
||||
NodeID
|
||||
calcNodeID(PublicKey const&);
|
||||
|
||||
/** Derive the 160-bit on-ledger account address from a public key.
|
||||
*
|
||||
* Applies RIPEMD-160(SHA-256(pubkey)) — the same algorithm used in
|
||||
* Bitcoin — to produce the `AccountID` that identifies the account on the
|
||||
* XRP Ledger.
|
||||
*
|
||||
* @param pk The account's public key.
|
||||
* @return The `AccountID` corresponding to `pk`.
|
||||
* @note The implementation lives in `AccountID.cpp` rather than
|
||||
* `PublicKey.cpp` due to header dependency ordering constraints.
|
||||
*/
|
||||
// VFALCO This belongs in AccountID.h but
|
||||
// is here because of header issues
|
||||
AccountID
|
||||
calcAccountID(PublicKey const& pk);
|
||||
|
||||
/** Format a human-readable peer fingerprint for diagnostic logging.
|
||||
*
|
||||
* Produces a string of the form
|
||||
* `"IP Address: <addr>[, Public Key: <NodePublic>][, Id: <id>]"` suitable
|
||||
* for audit and connection-lifecycle log messages.
|
||||
*
|
||||
* @param address The peer's IP endpoint (always included).
|
||||
* @param publicKey The peer's node public key, encoded as `NodePublic`
|
||||
* Base58; omitted if not yet known (e.g., before the handshake).
|
||||
* @param id An optional session identifier string; omitted if absent.
|
||||
* @return A formatted fingerprint string.
|
||||
*/
|
||||
inline std::string
|
||||
getFingerprint(
|
||||
beast::IP::Endpoint const& address,
|
||||
@@ -260,7 +382,22 @@ getFingerprint(
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace json {
|
||||
/** Deserialize a `PublicKey` from a JSON field value.
|
||||
*
|
||||
* Accepts three formats in order:
|
||||
* 1. Lowercase hex string of the raw 33-byte key.
|
||||
* 2. `NodePublic` Base58Check encoding (validator keys).
|
||||
* 3. `AccountPublic` Base58Check encoding (signing keys).
|
||||
*
|
||||
* This covers the variety of formats that appear in RPC requests and
|
||||
* configuration files.
|
||||
*
|
||||
* @param v The JSON object to read from.
|
||||
* @param field The field whose value is decoded.
|
||||
* @return The decoded `PublicKey`.
|
||||
* @throws `JsonTypeMismatchError` if the field value does not match any
|
||||
* recognized format.
|
||||
*/
|
||||
template <>
|
||||
inline xrpl::PublicKey
|
||||
getOrThrow(json::Value const& v, xrpl::SField const& field)
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
/** @file
|
||||
* Defines `Quality` and `TAmounts`, the core exchange-rate abstractions
|
||||
* used by XRPL's on-ledger decentralized exchange (DEX).
|
||||
*
|
||||
* `Quality` is the sortable representation of a currency exchange rate.
|
||||
* The offer-crossing engine — ranking offers, scaling partial fills, and
|
||||
* composing multi-hop paths — is expressed entirely in terms of these types.
|
||||
*
|
||||
* @see QualityFunction.h for the continuous AMM price-function extension.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/protocol/AmountConversions.h>
|
||||
@@ -12,35 +23,50 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Represents a pair of input and output currencies.
|
||||
|
||||
The input currency can be converted to the output
|
||||
currency by multiplying by the rate, represented by
|
||||
Quality.
|
||||
|
||||
For offers, "in" is always TakerPays and "out" is
|
||||
always TakerGets.
|
||||
*/
|
||||
/** A typed pair of input and output amounts representing one side of a trade.
|
||||
*
|
||||
* For offers on the DEX, `in` is always `TakerPays` and `out` is always
|
||||
* `TakerGets`. The template parameters allow instantiation over
|
||||
* `STAmount`, `IOUAmount`, `XRPAmount`, and `MPTAmount`.
|
||||
*
|
||||
* @tparam In Type of the input (paying) amount.
|
||||
* @tparam Out Type of the output (receiving) amount.
|
||||
*/
|
||||
template <class In, class Out>
|
||||
struct TAmounts
|
||||
{
|
||||
TAmounts() = default;
|
||||
|
||||
/** Construct a zero-valued pair. */
|
||||
TAmounts(beast::Zero, beast::Zero) : in(beast::kZERO), out(beast::kZERO)
|
||||
{
|
||||
}
|
||||
|
||||
/** Construct from explicit in and out amounts.
|
||||
*
|
||||
* @param in The input (TakerPays) amount.
|
||||
* @param out The output (TakerGets) amount.
|
||||
*/
|
||||
TAmounts(In in, Out out) : in(std::move(in)), out(std::move(out))
|
||||
{
|
||||
}
|
||||
|
||||
/** Returns `true` if either quantity is not positive. */
|
||||
/** Returns `true` if either quantity is not positive.
|
||||
*
|
||||
* Used by the offer-crossing engine to skip exhausted or invalid offers
|
||||
* without further computation.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
empty() const noexcept
|
||||
{
|
||||
return in <= beast::kZERO || out <= beast::kZERO;
|
||||
}
|
||||
|
||||
/** Adds `rhs` component-wise to this pair.
|
||||
*
|
||||
* @param rhs The amounts to add.
|
||||
* @return Reference to `*this`.
|
||||
*/
|
||||
TAmounts&
|
||||
operator+=(TAmounts const& rhs)
|
||||
{
|
||||
@@ -49,6 +75,11 @@ struct TAmounts
|
||||
return *this;
|
||||
}
|
||||
|
||||
/** Subtracts `rhs` component-wise from this pair.
|
||||
*
|
||||
* @param rhs The amounts to subtract.
|
||||
* @return Reference to `*this`.
|
||||
*/
|
||||
TAmounts&
|
||||
operator-=(TAmounts const& rhs)
|
||||
{
|
||||
@@ -57,12 +88,14 @@ struct TAmounts
|
||||
return *this;
|
||||
}
|
||||
|
||||
In in{};
|
||||
Out out{};
|
||||
In in{}; /**< Input (TakerPays) amount. */
|
||||
Out out{}; /**< Output (TakerGets) amount. */
|
||||
};
|
||||
|
||||
/** Canonical `TAmounts` alias used by the `STAmount`-based offer-crossing path. */
|
||||
using Amounts = TAmounts<STAmount, STAmount>;
|
||||
|
||||
/** Returns `true` when both sides of two `TAmounts` pairs are equal. */
|
||||
template <class In, class Out>
|
||||
bool
|
||||
operator==(TAmounts<In, Out> const& lhs, TAmounts<In, Out> const& rhs) noexcept
|
||||
@@ -70,6 +103,7 @@ operator==(TAmounts<In, Out> const& lhs, TAmounts<In, Out> const& rhs) noexcept
|
||||
return lhs.in == rhs.in && lhs.out == rhs.out;
|
||||
}
|
||||
|
||||
/** Returns `true` when either side of two `TAmounts` pairs differs. */
|
||||
template <class In, class Out>
|
||||
bool
|
||||
operator!=(TAmounts<In, Out> const& lhs, TAmounts<In, Out> const& rhs) noexcept
|
||||
@@ -79,54 +113,107 @@ operator!=(TAmounts<In, Out> const& lhs, TAmounts<In, Out> const& rhs) noexcept
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// XRPL specific constant used for parsing qualities and other things
|
||||
/** Unity exchange rate (1:1), scaled to XRPL's 9-decimal fixed-point precision.
|
||||
*
|
||||
* Appears throughout offer parsing and fee calculations wherever a 1:1
|
||||
* exchange rate must be expressed as a raw integer.
|
||||
*/
|
||||
#define QUALITY_ONE 1'000'000'000
|
||||
|
||||
/** Represents the logical ratio of output currency to input currency.
|
||||
Internally this is stored using a custom floating point representation,
|
||||
as the inverse of the ratio, so that quality will be descending in
|
||||
a sequence of actual values that represent qualities.
|
||||
*/
|
||||
/** The exchange rate of an offer, stored as an inverted packed floating-point
|
||||
* integer so that higher-quality offers sort first under plain integer comparison.
|
||||
*
|
||||
* A `Quality` encodes the ratio `out / in` (TakerGets / TakerPays): how much
|
||||
* output the taker receives per unit of input. Higher quality is better for
|
||||
* the taker (more output per unit of input).
|
||||
*
|
||||
* The internal `uint64_t` uses the same bit layout as `STAmount` IOU encoding:
|
||||
* the top 8 bits hold a biased exponent (stored value = actual exponent + 100)
|
||||
* and the lower 56 bits hold an unsigned mantissa. Critically, the integer
|
||||
* value is **inverted** relative to the economic concept — a *higher* quality
|
||||
* corresponds to a *lower* `uint64_t` — so that ascending integer order in the
|
||||
* ledger's offer directories corresponds to descending quality, allowing the
|
||||
* best offers to be processed first.
|
||||
*
|
||||
* @note The increment/decrement operators navigate the discrete floating-point
|
||||
* grid by modifying `value_` by one ULP. The representation may become
|
||||
* non-canonical after such operations.
|
||||
*
|
||||
* @see composedQuality() for two-hop path composition.
|
||||
* @see QualityFunction.h for the continuous AMM extension of this type.
|
||||
*/
|
||||
class Quality
|
||||
{
|
||||
public:
|
||||
// Type of the internal representation. Higher qualities
|
||||
// have lower unsigned integer representations.
|
||||
/** Underlying storage type. Higher qualities have lower integer values. */
|
||||
using value_type = std::uint64_t;
|
||||
|
||||
/** Minimum valid tick size (significant decimal digits) for `round()`. */
|
||||
static int const kMIN_TICK_SIZE = 3;
|
||||
|
||||
/** Maximum valid tick size (significant decimal digits) for `round()`. */
|
||||
static int const kMAX_TICK_SIZE = 16;
|
||||
|
||||
private:
|
||||
// This has the same representation as STAmount, see the comment on the
|
||||
// STAmount. However, this class does not always use the canonical
|
||||
// representation. In particular, the increment and decrement operators may
|
||||
// cause a non-canonical representation.
|
||||
// Packed 64-bit encoding: bits [63:56] = biased exponent (actual + 100),
|
||||
// bits [55:0] = mantissa. Identical to the STAmount IOU wire format.
|
||||
// May be non-canonical after operator++ / operator--.
|
||||
value_type value_;
|
||||
|
||||
public:
|
||||
Quality() = default;
|
||||
|
||||
/** Create a quality from the integer encoding of an STAmount */
|
||||
/** Construct from a raw packed integer in STAmount encoding.
|
||||
*
|
||||
* The top 8 bits are the biased exponent (actual exponent + 100) and
|
||||
* the bottom 56 bits are the mantissa. Higher integers denote lower
|
||||
* (worse) quality because the internal ordering is inverted.
|
||||
*
|
||||
* @param value Packed 64-bit quality value.
|
||||
*/
|
||||
explicit Quality(std::uint64_t value);
|
||||
|
||||
/** Create a quality from the ratio of two amounts. */
|
||||
/** Construct from an `STAmount` in/out pair encoding `out / in`.
|
||||
*
|
||||
* Calls `getRate(amount.out, amount.in)` to produce the packed value.
|
||||
* Neither side should be zero.
|
||||
*
|
||||
* @param amount Offer amounts: `in` = TakerPays, `out` = TakerGets.
|
||||
*/
|
||||
explicit Quality(Amounts const& amount);
|
||||
|
||||
/** Create a quality from the ratio of two amounts. */
|
||||
/** Construct from a typed in/out pair by converting to `STAmount` first.
|
||||
*
|
||||
* @tparam In Input amount type (e.g., `XRPAmount`, `IOUAmount`).
|
||||
* @tparam Out Output amount type.
|
||||
* @param amount The typed offer amounts.
|
||||
*/
|
||||
template <class In, class Out>
|
||||
explicit Quality(TAmounts<In, Out> const& amount)
|
||||
: Quality(Amounts(toSTAmount(amount.in), toSTAmount(amount.out)))
|
||||
{
|
||||
}
|
||||
|
||||
/** Create a quality from the ratio of two amounts. */
|
||||
/** Construct from explicit out and in amounts by converting to `STAmount`.
|
||||
*
|
||||
* @tparam In Input amount type.
|
||||
* @tparam Out Output amount type.
|
||||
* @param out The output (TakerGets) amount.
|
||||
* @param in The input (TakerPays) amount.
|
||||
*/
|
||||
template <class In, class Out>
|
||||
Quality(Out const& out, In const& in) : Quality(Amounts(toSTAmount(in), toSTAmount(out)))
|
||||
{
|
||||
}
|
||||
|
||||
/** Advances to the next higher quality level. */
|
||||
/** Advance to the next higher quality level.
|
||||
*
|
||||
* Because the internal encoding is inverted, this decrements the stored
|
||||
* integer by one ULP. Used during offer-book traversal to step the
|
||||
* crossing price up by the smallest representable increment.
|
||||
*
|
||||
* @pre `value_ > 0`; underflow is asserted.
|
||||
*/
|
||||
/** @{ */
|
||||
Quality&
|
||||
operator++();
|
||||
@@ -135,7 +222,13 @@ public:
|
||||
operator++(int);
|
||||
/** @} */
|
||||
|
||||
/** Advances to the next lower quality level. */
|
||||
/** Retreat to the next lower quality level.
|
||||
*
|
||||
* Because the internal encoding is inverted, this increments the stored
|
||||
* integer by one ULP.
|
||||
*
|
||||
* @pre `value_ < UINT64_MAX`; overflow is asserted.
|
||||
*/
|
||||
/** @{ */
|
||||
Quality&
|
||||
operator--();
|
||||
@@ -144,65 +237,184 @@ public:
|
||||
operator--(int);
|
||||
/** @} */
|
||||
|
||||
/** Returns the quality as STAmount. */
|
||||
/** Decode the packed quality value into an `STAmount` exchange rate.
|
||||
*
|
||||
* The returned amount represents the rate `out / in` in the IOU
|
||||
* floating-point format. Callers use this when passing the quality
|
||||
* to `mulRound` / `divRound` for proportional scaling.
|
||||
*
|
||||
* @return The exchange rate as an `STAmount`.
|
||||
*/
|
||||
[[nodiscard]] STAmount
|
||||
rate() const
|
||||
{
|
||||
return amountFromQuality(value_);
|
||||
}
|
||||
|
||||
/** Returns the quality rounded up to the specified number
|
||||
of decimal digits.
|
||||
*/
|
||||
/** Round the quality's mantissa up to `tickSize` significant decimal digits.
|
||||
*
|
||||
* Used for tick-size enforcement: coarsens the price grid so that offers
|
||||
* differing only in low-order digits are treated as equivalent. Rounding
|
||||
* is always upward (ceiling), which makes the encoded rate slightly higher
|
||||
* (worse for the taker) and prevents a rounded quality from being mistakenly
|
||||
* ranked better than the original.
|
||||
*
|
||||
* @param tickSize Number of significant digits to retain. Must be in
|
||||
* `[kMIN_TICK_SIZE, kMAX_TICK_SIZE]`; enforcement is the caller's
|
||||
* responsibility.
|
||||
* @return A new `Quality` with a rounded-up mantissa and unchanged exponent.
|
||||
*/
|
||||
[[nodiscard]] Quality
|
||||
round(int tickSize) const;
|
||||
|
||||
/** Returns the scaled amount with in capped.
|
||||
Math is avoided if the result is exact. The output is clamped
|
||||
to prevent money creation.
|
||||
*/
|
||||
/** Scale an offer's amounts down so that the input does not exceed `limit`.
|
||||
*
|
||||
* If `amount.in > limit`, sets `in = limit` and recomputes `out`
|
||||
* proportionally via `divRound`. The computed output is clamped to
|
||||
* `amount.out` if arithmetic would produce a larger value, preventing
|
||||
* money creation due to rounding. Returns `amount` unchanged when
|
||||
* `amount.in <= limit`.
|
||||
*
|
||||
* @param amount Current offer amounts (`in` = TakerPays, `out` = TakerGets).
|
||||
* @param limit Maximum allowed input amount.
|
||||
* @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`.
|
||||
* @note Uses `divRound` (legacy rounding that ignores low-order bits).
|
||||
* Use `ceilInStrict` when full-precision rounding is required.
|
||||
*/
|
||||
[[nodiscard]] Amounts
|
||||
ceilIn(Amounts const& amount, STAmount const& limit) const;
|
||||
|
||||
/** Scale a typed offer's amounts down so that the input does not exceed `limit`.
|
||||
*
|
||||
* Converts both sides to `STAmount`, delegates to the `STAmount` overload,
|
||||
* then converts the result back to the typed amounts.
|
||||
*
|
||||
* @tparam In Input amount type.
|
||||
* @tparam Out Output amount type.
|
||||
* @param amount Current offer amounts.
|
||||
* @param limit Maximum allowed input amount.
|
||||
* @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`.
|
||||
*/
|
||||
template <class In, class Out>
|
||||
[[nodiscard]] TAmounts<In, Out>
|
||||
ceilIn(TAmounts<In, Out> const& amount, In const& limit) const;
|
||||
|
||||
// Some of the underlying rounding functions called by ceil_in() ignored
|
||||
// low order bits that could influence rounding decisions. This "strict"
|
||||
// method uses underlying functions that pay attention to all the bits.
|
||||
/** Scale an offer's amounts down so that the input does not exceed `limit`,
|
||||
* using full-precision rounding.
|
||||
*
|
||||
* Identical to `ceilIn` except it delegates to `divRoundStrict`, which
|
||||
* considers all low-order bits that `divRound` ignores. Introduced to
|
||||
* fix subtle rounding bugs where a borderline result could influence
|
||||
* whether an offer crosses.
|
||||
*
|
||||
* @param amount Current offer amounts.
|
||||
* @param limit Maximum allowed input amount.
|
||||
* @param roundUp Whether to round the recomputed output up (`true`) or
|
||||
* down (`false`).
|
||||
* @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`.
|
||||
*/
|
||||
[[nodiscard]] Amounts
|
||||
ceilInStrict(Amounts const& amount, STAmount const& limit, bool roundUp) const;
|
||||
|
||||
/** Scale a typed offer's amounts down so that the input does not exceed `limit`,
|
||||
* using full-precision rounding.
|
||||
*
|
||||
* @tparam In Input amount type.
|
||||
* @tparam Out Output amount type.
|
||||
* @param amount Current offer amounts.
|
||||
* @param limit Maximum allowed input amount.
|
||||
* @param roundUp Whether to round the recomputed output up or down.
|
||||
* @return Scaled amounts satisfying `in <= limit` and `out <= amount.out`.
|
||||
*/
|
||||
template <class In, class Out>
|
||||
[[nodiscard]] TAmounts<In, Out>
|
||||
ceilInStrict(TAmounts<In, Out> const& amount, In const& limit, bool roundUp) const;
|
||||
|
||||
/** Returns the scaled amount with out capped.
|
||||
Math is avoided if the result is exact. The input is clamped
|
||||
to prevent money creation.
|
||||
*/
|
||||
/** Scale an offer's amounts down so that the output does not exceed `limit`.
|
||||
*
|
||||
* If `amount.out > limit`, sets `out = limit` and recomputes `in`
|
||||
* proportionally via `mulRound`. The computed input is clamped to
|
||||
* `amount.in` if arithmetic would produce a larger value, preventing
|
||||
* money creation due to rounding. Returns `amount` unchanged when
|
||||
* `amount.out <= limit`.
|
||||
*
|
||||
* @param amount Current offer amounts.
|
||||
* @param limit Maximum allowed output amount.
|
||||
* @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`.
|
||||
* @note Uses `mulRound` (legacy rounding that ignores low-order bits).
|
||||
* Use `ceilOutStrict` when full-precision rounding is required.
|
||||
*/
|
||||
[[nodiscard]] Amounts
|
||||
ceilOut(Amounts const& amount, STAmount const& limit) const;
|
||||
|
||||
/** Scale a typed offer's amounts down so that the output does not exceed `limit`.
|
||||
*
|
||||
* Converts both sides to `STAmount`, delegates to the `STAmount` overload,
|
||||
* then converts the result back to the typed amounts.
|
||||
*
|
||||
* @tparam In Input amount type.
|
||||
* @tparam Out Output amount type.
|
||||
* @param amount Current offer amounts.
|
||||
* @param limit Maximum allowed output amount.
|
||||
* @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`.
|
||||
*/
|
||||
template <class In, class Out>
|
||||
[[nodiscard]] TAmounts<In, Out>
|
||||
ceilOut(TAmounts<In, Out> const& amount, Out const& limit) const;
|
||||
|
||||
// Some of the underlying rounding functions called by ceil_out() ignored
|
||||
// low order bits that could influence rounding decisions. This "strict"
|
||||
// method uses underlying functions that pay attention to all the bits.
|
||||
/** Scale an offer's amounts down so that the output does not exceed `limit`,
|
||||
* using full-precision rounding.
|
||||
*
|
||||
* Identical to `ceilOut` except it delegates to `mulRoundStrict`, which
|
||||
* considers all low-order bits that `mulRound` ignores.
|
||||
*
|
||||
* @param amount Current offer amounts.
|
||||
* @param limit Maximum allowed output amount.
|
||||
* @param roundUp Whether to round the recomputed input up (`true`) or
|
||||
* down (`false`).
|
||||
* @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`.
|
||||
*/
|
||||
[[nodiscard]] Amounts
|
||||
ceilOutStrict(Amounts const& amount, STAmount const& limit, bool roundUp) const;
|
||||
|
||||
/** Scale a typed offer's amounts down so that the output does not exceed `limit`,
|
||||
* using full-precision rounding.
|
||||
*
|
||||
* @tparam In Input amount type.
|
||||
* @tparam Out Output amount type.
|
||||
* @param amount Current offer amounts.
|
||||
* @param limit Maximum allowed output amount.
|
||||
* @param roundUp Whether to round the recomputed input up or down.
|
||||
* @return Scaled amounts satisfying `out <= limit` and `in <= amount.in`.
|
||||
*/
|
||||
template <class In, class Out>
|
||||
[[nodiscard]] TAmounts<In, Out>
|
||||
ceilOutStrict(TAmounts<In, Out> const& amount, Out const& limit, bool roundUp) const;
|
||||
|
||||
private:
|
||||
// The ceil_in and ceil_out methods that deal in TAmount all convert
|
||||
// their arguments to STAmount and convert the result back to TAmount.
|
||||
// This helper function takes care of all the conversion operations.
|
||||
/** Shared implementation for all typed `ceilIn`/`ceilOut` overloads.
|
||||
*
|
||||
* Converts `amount` and `limit` to `STAmount`, calls `ceilFunction` (a
|
||||
* member-function pointer to one of the `STAmount`-based overloads), and
|
||||
* converts the result back to `TAmounts<In, Out>`. Returns `amount`
|
||||
* unchanged when `limitCmp <= limit` (i.e., the limit is not binding).
|
||||
*
|
||||
* The variadic `Round...` pack forwards an optional `bool roundUp` argument
|
||||
* to strict variants without requiring separate instantiations.
|
||||
*
|
||||
* @tparam In Input amount type.
|
||||
* @tparam Out Output amount type.
|
||||
* @tparam Lim Limit amount type (same as `In` or `Out`).
|
||||
* @tparam FnPtr Pointer to the `STAmount`-based ceil member function.
|
||||
* @tparam Round Empty or `{bool}` — forwarded as `roundUp`.
|
||||
* @param amount Current typed offer amounts.
|
||||
* @param limit The cap to apply.
|
||||
* @param limitCmp The side of `amount` to compare against `limit`
|
||||
* (either `amount.in` or `amount.out`).
|
||||
* @param ceilFunction Member-function pointer to dispatch to.
|
||||
* @param round Optional rounding direction (strict variants only).
|
||||
* @return Scaled `TAmounts<In, Out>`.
|
||||
*/
|
||||
template <class In, class Out, class Lim, typename FnPtr, std::same_as<bool>... Round>
|
||||
[[nodiscard]] TAmounts<In, Out>
|
||||
ceilTAmountsHelper(
|
||||
@@ -213,46 +425,53 @@ private:
|
||||
Round... round) const;
|
||||
|
||||
public:
|
||||
/** Returns `true` if lhs is lower quality than `rhs`.
|
||||
Lower quality means the taker receives a worse deal.
|
||||
Higher quality is better for the taker.
|
||||
*/
|
||||
/** Returns `true` if `lhs` is lower quality (worse for the taker) than `rhs`.
|
||||
*
|
||||
* Because the internal encoding is inverted, a lower quality corresponds
|
||||
* to a *higher* stored integer, so this compares `lhs.value_ > rhs.value_`.
|
||||
*/
|
||||
friend bool
|
||||
operator<(Quality const& lhs, Quality const& rhs) noexcept
|
||||
{
|
||||
return lhs.value_ > rhs.value_;
|
||||
}
|
||||
|
||||
/** Returns `true` if `lhs` is higher quality (better for the taker) than `rhs`. */
|
||||
friend bool
|
||||
operator>(Quality const& lhs, Quality const& rhs) noexcept
|
||||
{
|
||||
return lhs.value_ < rhs.value_;
|
||||
}
|
||||
|
||||
/** Returns `true` if `lhs` is lower or equal quality to `rhs`. */
|
||||
friend bool
|
||||
operator<=(Quality const& lhs, Quality const& rhs) noexcept
|
||||
{
|
||||
return !(lhs > rhs);
|
||||
}
|
||||
|
||||
/** Returns `true` if `lhs` is higher or equal quality to `rhs`. */
|
||||
friend bool
|
||||
operator>=(Quality const& lhs, Quality const& rhs) noexcept
|
||||
{
|
||||
return !(lhs < rhs);
|
||||
}
|
||||
|
||||
/** Returns `true` if both qualities encode the same exchange rate. */
|
||||
friend bool
|
||||
operator==(Quality const& lhs, Quality const& rhs) noexcept
|
||||
{
|
||||
return lhs.value_ == rhs.value_;
|
||||
}
|
||||
|
||||
/** Returns `true` if the two qualities encode different exchange rates. */
|
||||
friend bool
|
||||
operator!=(Quality const& lhs, Quality const& rhs) noexcept
|
||||
{
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
/** Write the raw packed integer value of the quality to an output stream. */
|
||||
friend std::ostream&
|
||||
operator<<(std::ostream& os, Quality const& quality)
|
||||
{
|
||||
@@ -260,8 +479,18 @@ public:
|
||||
return os;
|
||||
}
|
||||
|
||||
// return the relative distance (relative error) between two qualities. This
|
||||
// is used for testing only. relative distance is abs(a-b)/min(a,b)
|
||||
/** Return the relative error between two quality values: `|a - b| / min(a, b)`.
|
||||
*
|
||||
* Extracts the exponent and mantissa from each packed value, scales them
|
||||
* to a common exponent, and returns the normalized distance. Used only
|
||||
* in unit tests to verify that two qualities are sufficiently close.
|
||||
*
|
||||
* @param q1 First quality; must be non-zero (asserted).
|
||||
* @param q2 Second quality; must be non-zero (asserted).
|
||||
* @return `|q1 - q2| / min(q1, q2)` as a `double`.
|
||||
* @note For testing only. Production code should compare with the
|
||||
* relational operators.
|
||||
*/
|
||||
friend double
|
||||
relativeDistance(Quality const& q1, Quality const& q2)
|
||||
{
|
||||
@@ -284,9 +513,8 @@ public:
|
||||
double const maxVD =
|
||||
(expDiff != 0) ? maxVMantissa * pow(10, expDiff) : static_cast<double>(maxVMantissa);
|
||||
|
||||
// maxVD and minVD are scaled so they have the same exponents. Dividing
|
||||
// cancels out the exponents, so we only need to deal with the (scaled)
|
||||
// mantissas
|
||||
// maxVD and minVD are scaled so they have the same exponent; dividing
|
||||
// cancels out the exponents, leaving only the normalized mantissa difference.
|
||||
return (maxVD - minVD) / minVD;
|
||||
}
|
||||
};
|
||||
@@ -315,7 +543,6 @@ template <class In, class Out>
|
||||
TAmounts<In, Out>
|
||||
Quality::ceilIn(TAmounts<In, Out> const& amount, In const& limit) const
|
||||
{
|
||||
// Construct a function pointer to the function we want to call.
|
||||
static constexpr Amounts (Quality::*kCEIL_IN_FN_PTR)(Amounts const&, STAmount const&) const =
|
||||
&Quality::ceilIn;
|
||||
|
||||
@@ -326,7 +553,6 @@ template <class In, class Out>
|
||||
TAmounts<In, Out>
|
||||
Quality::ceilInStrict(TAmounts<In, Out> const& amount, In const& limit, bool roundUp) const
|
||||
{
|
||||
// Construct a function pointer to the function we want to call.
|
||||
static constexpr Amounts (Quality::*kCEIL_IN_FN_PTR)(Amounts const&, STAmount const&, bool)
|
||||
const = &Quality::ceilInStrict;
|
||||
|
||||
@@ -337,7 +563,6 @@ template <class In, class Out>
|
||||
TAmounts<In, Out>
|
||||
Quality::ceilOut(TAmounts<In, Out> const& amount, Out const& limit) const
|
||||
{
|
||||
// Construct a function pointer to the function we want to call.
|
||||
static constexpr Amounts (Quality::*kCEIL_OUT_FN_PTR)(Amounts const&, STAmount const&) const =
|
||||
&Quality::ceilOut;
|
||||
|
||||
@@ -348,17 +573,26 @@ template <class In, class Out>
|
||||
TAmounts<In, Out>
|
||||
Quality::ceilOutStrict(TAmounts<In, Out> const& amount, Out const& limit, bool roundUp) const
|
||||
{
|
||||
// Construct a function pointer to the function we want to call.
|
||||
static constexpr Amounts (Quality::*kCEIL_OUT_FN_PTR)(Amounts const&, STAmount const&, bool)
|
||||
const = &Quality::ceilOutStrict;
|
||||
|
||||
return ceilTAmountsHelper(amount, limit, amount.out, kCEIL_OUT_FN_PTR, roundUp);
|
||||
}
|
||||
|
||||
/** Calculate the quality of a two-hop path given the two hops.
|
||||
@param lhs The first leg of the path: input to intermediate.
|
||||
@param rhs The second leg of the path: intermediate to output.
|
||||
*/
|
||||
/** Compute the effective end-to-end exchange rate for a two-hop path.
|
||||
*
|
||||
* If the first hop converts A→B at rate `lhs` and the second converts B→C
|
||||
* at rate `rhs`, the composed quality is their product, re-encoded into the
|
||||
* packed 64-bit format. Used by the pathfinding engine to rank multi-hop
|
||||
* routes against single-hop offers on a common scale.
|
||||
*
|
||||
* @param lhs Quality of the first leg (input → intermediate currency).
|
||||
* @param rhs Quality of the second leg (intermediate → output currency).
|
||||
* @return Composed quality representing the overall A→C exchange rate.
|
||||
* @note Both input rates must be non-zero (asserted at runtime). The
|
||||
* composed exponent must fit in 8 bits (i.e., actual exponent in
|
||||
* [-99, 155]); astronomically large or small paths will assert.
|
||||
*/
|
||||
Quality
|
||||
composedQuality(Quality const& lhs, Quality const& rhs);
|
||||
|
||||
|
||||
@@ -6,52 +6,152 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Average quality of a path as a function of `out`: q(out) = m * out + b,
|
||||
* where m = -1 / poolGets, b = poolPays / poolGets. If CLOB offer then
|
||||
* `m` is equal to 0 `b` is equal to the offer's quality. The function
|
||||
* is derived by substituting `in` in q = out / in with the swap out formula
|
||||
* for `in`:
|
||||
* in = [(poolGets * poolPays) / (poolGets - out) - poolPays] / (1 - tfee)
|
||||
* and combining the function for multiple steps. The function is used
|
||||
* to limit required output amount when quality limit is provided in one
|
||||
* path optimization.
|
||||
/** Average quality of a payment strand expressed as a linear function of output.
|
||||
*
|
||||
* Models the relationship `q(out) = m_ * out + b_`, where `q` is the average
|
||||
* exchange rate (quality) that a strand delivers when it produces `out` units.
|
||||
* This analytical model lets `StrandFlow::limitOut()` compute — without
|
||||
* simulation — the maximum output the strand may produce before AMM price
|
||||
* impact degrades the average quality below a caller-supplied limit.
|
||||
*
|
||||
* **Derivation.** For an AMM step with pool balances `poolGets` (input side)
|
||||
* and `poolPays` (output side) and fee multiplier `cfee = 1 - tfee`, the
|
||||
* constant-product swap formula gives:
|
||||
* @code
|
||||
* in = [(poolGets * poolPays) / (poolGets - out) - poolPays] / cfee
|
||||
* @endcode
|
||||
* Substituting into `q = out / in` and linearising yields:
|
||||
* @code
|
||||
* m = -cfee / poolGets (always negative for a valid AMM step)
|
||||
* b = poolPays * cfee / poolGets
|
||||
* @endcode
|
||||
*
|
||||
* **Multi-hop composition.** For strands with sequential steps (e.g. a
|
||||
* transfer-fee hop preceding an AMM hop), `combine()` chains two quality
|
||||
* functions analytically. `StrandFlow::limitOut()` calls `combine()` in a
|
||||
* loop over all steps to accumulate a single QF representing the whole strand.
|
||||
*
|
||||
* **Two construction modes** are selected via tag dispatch:
|
||||
* - `AMMTag` — variable quality; slope and intercept derived from pool balances.
|
||||
* - `CLOBLikeTag` — constant quality (`m_ = 0`); used for plain CLOB orders and
|
||||
* for AMM offers in multi-path mode, where each path's AMM allocation is fixed.
|
||||
*
|
||||
* @note The linear approximation is exact for *average* quality but not for
|
||||
* instantaneous (marginal) quality, which is quadratic. Using averages
|
||||
* keeps composition algebraically simple while still providing a
|
||||
* conservative, analytically tractable bound.
|
||||
*
|
||||
* @see StrandFlow.h `limitOut()` — primary consumer of this class.
|
||||
*/
|
||||
class QualityFunction
|
||||
{
|
||||
private:
|
||||
// slope
|
||||
/** Slope of the quality–output line (`-cfee / poolGets` for AMM; 0 for CLOB). */
|
||||
Number m_;
|
||||
// intercept
|
||||
/** Intercept of the quality–output line (`poolPays * cfee / poolGets` for AMM;
|
||||
* `1 / quality.rate()` for CLOB). */
|
||||
Number b_;
|
||||
// seated if QF is for CLOB offer.
|
||||
/** Cached constant quality; seated only when `m_ == 0` (CLOB-like function). */
|
||||
std::optional<Quality> quality_;
|
||||
|
||||
public:
|
||||
/** Tag type that selects the AMM constructor (variable-quality path step). */
|
||||
struct AMMTag
|
||||
{
|
||||
};
|
||||
// AMMOffer for multi-path is like CLOB, i.e. the offer size
|
||||
// changes proportionally to its quality.
|
||||
/** Tag type that selects the CLOB-like constructor (constant-quality path step).
|
||||
*
|
||||
* Used for both plain CLOB orders and AMM offers operating in multi-path
|
||||
* mode, where the AMM offer size scales proportionally with quality just
|
||||
* like a CLOB, making the effective quality constant from this sub-path's
|
||||
* perspective.
|
||||
*/
|
||||
struct CLOBLikeTag
|
||||
{
|
||||
};
|
||||
|
||||
/** Construct a constant-quality (CLOB-like) quality function.
|
||||
*
|
||||
* Sets `m_ = 0` and `b_ = 1 / quality.rate()`. `quality_` is seated so
|
||||
* that `isConst()` returns `true` and `StrandFlow::limitOut()` skips the
|
||||
* output cap.
|
||||
*
|
||||
* @param quality The fixed exchange rate of this path step.
|
||||
* @throws std::runtime_error if `quality.rate()` is zero, which would
|
||||
* make the intercept infinite.
|
||||
*/
|
||||
QualityFunction(Quality const& quality, CLOBLikeTag);
|
||||
|
||||
/** Construct a variable-quality (AMM) quality function from pool balances.
|
||||
*
|
||||
* Derives the slope and intercept from the constant-product swap formula:
|
||||
* @code
|
||||
* m_ = -cfee / amounts.in
|
||||
* b_ = amounts.out * cfee / amounts.in
|
||||
* @endcode
|
||||
* where `cfee = feeMult(tfee)`. `quality_` is left empty; `isConst()`
|
||||
* returns `false`.
|
||||
*
|
||||
* @tparam TIn Input amount type (e.g. `XRPAmount`, `IOUAmount`).
|
||||
* @tparam TOut Output amount type.
|
||||
* @param amounts Current AMM pool balances: `amounts.in` is the input-side
|
||||
* pool depth, `amounts.out` is the output-side pool depth.
|
||||
* @param tfee AMM trading fee in the same units as `feeMult()` expects.
|
||||
* @throws std::runtime_error if either pool balance is zero or negative,
|
||||
* which would cause division-by-zero in downstream arithmetic.
|
||||
*/
|
||||
template <typename TIn, typename TOut>
|
||||
QualityFunction(TAmounts<TIn, TOut> const& amounts, std::uint32_t tfee, AMMTag);
|
||||
|
||||
/** Combines QF with the next step QF
|
||||
/** Chain this quality function with the next path step's quality function.
|
||||
*
|
||||
* Applies linear function composition in reciprocal-rate space:
|
||||
* @code
|
||||
* m_ += b_ * qf.m_;
|
||||
* b_ *= qf.b_;
|
||||
* @endcode
|
||||
* If the combined slope becomes nonzero, `quality_` is cleared to reflect
|
||||
* that the resulting function is no longer constant and `outFromAvgQ()`
|
||||
* must be used rather than a simple pass/fail quality check.
|
||||
*
|
||||
* @param qf Quality function for the next step to compose in.
|
||||
*/
|
||||
void
|
||||
combine(QualityFunction const& qf);
|
||||
|
||||
/** Find output to produce the requested
|
||||
* average quality.
|
||||
* @param quality requested average quality (quality limit)
|
||||
/** Solve for the maximum output at which average quality meets the given limit.
|
||||
*
|
||||
* Inverts `q(out) = m_ * out + b_` by substituting `q = 1 / quality.rate()`:
|
||||
* @code
|
||||
* out = (1 / quality.rate() - b_) / m_
|
||||
* @endcode
|
||||
* The rounding mode is set to `Upward` during the calculation so the
|
||||
* returned bound is conservative: because `m_` is negative, dividing an
|
||||
* upward-rounded numerator by a negative slope yields a result that rounds
|
||||
* down, ensuring the engine never requests marginally more output than the
|
||||
* quality constraint allows.
|
||||
*
|
||||
* Returns `std::nullopt` in three cases:
|
||||
* - `m_ == 0`: the function is constant (CLOB-like); quality either passes
|
||||
* or fails uniformly, so no output cap is meaningful.
|
||||
* - `quality.rate() == 0`: guards against division-by-zero when forming
|
||||
* `1 / rate`.
|
||||
* - `out <= 0`: the quality limit cannot be achieved at any positive output;
|
||||
* the strand is effectively dead for this constraint.
|
||||
*
|
||||
* @param quality The minimum acceptable average exchange rate (quality limit).
|
||||
* @return The output amount at which the strand's average quality equals
|
||||
* `quality`, or `std::nullopt` if the cap is inapplicable or infeasible.
|
||||
*/
|
||||
std::optional<Number>
|
||||
outFromAvgQ(Quality const& quality);
|
||||
|
||||
/** Return true if the quality function is constant
|
||||
/** Return `true` if this quality function is constant (CLOB-like).
|
||||
*
|
||||
* A constant function has `m_ == 0`: the average quality is the same
|
||||
* regardless of output size. `StrandFlow::limitOut()` treats a constant
|
||||
* function as a signal to skip the output cap and return `remainingOut`
|
||||
* unchanged.
|
||||
*/
|
||||
[[nodiscard]] bool
|
||||
isConst() const
|
||||
@@ -59,6 +159,13 @@ public:
|
||||
return quality_.has_value();
|
||||
}
|
||||
|
||||
/** Return the cached constant quality, if any.
|
||||
*
|
||||
* Seated only when `isConst() == true` (i.e., this is a CLOB-like
|
||||
* function constructed via `CLOBLikeTag`). Returns `std::nullopt` for
|
||||
* variable-quality (AMM) functions and for any combined function whose
|
||||
* slope became nonzero after `combine()`.
|
||||
*/
|
||||
[[nodiscard]] std::optional<Quality> const&
|
||||
quality() const
|
||||
{
|
||||
|
||||
@@ -1,13 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
/** @file
|
||||
* Deprecated compatibility shim for the XRPL RPC error API.
|
||||
*
|
||||
* Declares `rpcError()` and `isRpcError()` — legacy entry points in the
|
||||
* `xrpl` namespace that predate the richer `RPC`-namespaced error
|
||||
* infrastructure in `ErrorCodes.h`. New code should use `RPC::makeError()`
|
||||
* and `RPC::containsError()` from `ErrorCodes.h` directly.
|
||||
*/
|
||||
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// VFALCO NOTE these are deprecated
|
||||
/** Return `true` if @p jvResult represents an RPC error response.
|
||||
*
|
||||
* Duck-types the JSON value by checking for the presence of the `"error"`
|
||||
* key — the same structural sentinel that `RPC::containsError()` tests,
|
||||
* making it the direct modern equivalent.
|
||||
*
|
||||
* @param jvResult The JSON value to inspect (taken by value, not
|
||||
* `const` reference — an inefficiency inherited from the original
|
||||
* implementation that was never corrected given this function's
|
||||
* deprecated status).
|
||||
* @return `true` if @p jvResult is a JSON object containing an `"error"`
|
||||
* member.
|
||||
* @deprecated Use `RPC::containsError(json)` from `ErrorCodes.h` instead.
|
||||
*/
|
||||
bool
|
||||
isRpcError(json::Value jvResult);
|
||||
|
||||
/** Construct a fresh JSON error object for the given error code.
|
||||
*
|
||||
* Delegates to `RPC::injectError()`, which populates a new `Json::Value`
|
||||
* object with the canonical `error` token, `error_code`, and
|
||||
* `error_message` fields drawn from the static `ErrorInfo` registry.
|
||||
* The return-by-value produces a self-contained error object ready for
|
||||
* direct return from an RPC handler.
|
||||
*
|
||||
* @param iError The RPC error code to encode.
|
||||
* @return A new `Json::Value` object with `error`, `error_code`, and
|
||||
* `error_message` populated.
|
||||
* @deprecated Use `RPC::makeError(code)` from `ErrorCodes.h` instead.
|
||||
*/
|
||||
json::Value
|
||||
rpcError(ErrorCodeI iError);
|
||||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/** @file
|
||||
* Defines the `Rate` struct and its arithmetic free functions for applying
|
||||
* XRPL transfer fees to `STAmount` values.
|
||||
*
|
||||
* Transfer rates are billion-scale fractions: `1,000,000,000` is parity
|
||||
* (no fee). `kPARITY_RATE` is the sentinel for the fee-free common case;
|
||||
* all six arithmetic functions short-circuit on it without entering the
|
||||
* `STAmount` multiply/divide path.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
@@ -10,14 +20,27 @@
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** Represents a transfer rate
|
||||
|
||||
Transfer rates are specified as fractions of 1 billion.
|
||||
For example, a transfer rate of 1% is represented as
|
||||
1,010,000,000.
|
||||
*/
|
||||
/** Protocol-level transfer rate, expressed as a fraction of one billion.
|
||||
*
|
||||
* A value of `1,000,000,000` means 1:1 — no fee. A value of
|
||||
* `1,010,000,000` means the sender must deliver 1.01 units for every 1 unit
|
||||
* the recipient receives (a 1% fee). This scale matches `QUALITY_ONE` in
|
||||
* `Quality.h`, tying transfer fees directly to the ledger's price
|
||||
* representation.
|
||||
*
|
||||
* `boost::totally_ordered<Rate>` generates `!=`, `>`, `<=`, and `>=` from
|
||||
* the manually provided `==` and `<`, keeping the struct concise while
|
||||
* remaining fully ordered.
|
||||
*
|
||||
* @note The default constructor is deleted: a `Rate` with an unspecified
|
||||
* value is meaningless, and zero would violate the nonzero precondition
|
||||
* asserted by every arithmetic function in this header. The constructor
|
||||
* is `explicit` to prevent accidental implicit conversion from the
|
||||
* large integers that rate values resemble.
|
||||
*/
|
||||
struct Rate : private boost::totally_ordered<Rate>
|
||||
{
|
||||
/** The raw billion-scale rate value as stored in `sfTransferRate`. */
|
||||
std::uint32_t value;
|
||||
|
||||
Rate() = delete;
|
||||
@@ -27,18 +50,21 @@ struct Rate : private boost::totally_ordered<Rate>
|
||||
}
|
||||
};
|
||||
|
||||
/** Returns `true` if both rates have the same billion-scale value. */
|
||||
inline bool
|
||||
operator==(Rate const& lhs, Rate const& rhs) noexcept
|
||||
{
|
||||
return lhs.value == rhs.value;
|
||||
}
|
||||
|
||||
/** Returns `true` if `lhs` is a strictly smaller rate than `rhs`. */
|
||||
inline bool
|
||||
operator<(Rate const& lhs, Rate const& rhs) noexcept
|
||||
{
|
||||
return lhs.value < rhs.value;
|
||||
}
|
||||
|
||||
/** Writes the raw billion-scale rate value to `os`. */
|
||||
inline std::ostream&
|
||||
operator<<(std::ostream& os, Rate const& rate)
|
||||
{
|
||||
@@ -46,32 +72,126 @@ operator<<(std::ostream& os, Rate const& rate)
|
||||
return os;
|
||||
}
|
||||
|
||||
/** Scale an amount by a transfer rate, preserving its asset.
|
||||
*
|
||||
* Computes `amount × (rate / 10^9)`. Returns `amount` unchanged when
|
||||
* `rate == kPARITY_RATE`, avoiding the `STAmount` arithmetic path for the
|
||||
* common fee-free case.
|
||||
*
|
||||
* @param amount The value to scale.
|
||||
* @param rate The transfer rate to apply; must be nonzero.
|
||||
* @return The scaled `STAmount` denominated in the same asset as `amount`.
|
||||
* @pre `rate.value != 0`; asserted in debug builds.
|
||||
*/
|
||||
STAmount
|
||||
multiply(STAmount const& amount, Rate const& rate);
|
||||
|
||||
/** Scale an amount by a transfer rate with controlled rounding, preserving its asset.
|
||||
*
|
||||
* Like `multiply()`, but the caller controls rounding direction. Used in
|
||||
* IOU payment routing where fee calculations stay in a single currency.
|
||||
*
|
||||
* @param amount The value to scale.
|
||||
* @param rate The transfer rate to apply; must be nonzero.
|
||||
* @param roundUp If `true`, round fractional results toward positive
|
||||
* infinity; otherwise round toward zero.
|
||||
* @return The scaled `STAmount` denominated in the same asset as `amount`.
|
||||
* @pre `rate.value != 0`; asserted in debug builds.
|
||||
*/
|
||||
STAmount
|
||||
multiplyRound(STAmount const& amount, Rate const& rate, bool roundUp);
|
||||
|
||||
/** Scale an amount by a transfer rate with controlled rounding, emitting a specified asset.
|
||||
*
|
||||
* Overload for offer-crossing and cross-currency paths where the output
|
||||
* must be denominated in a different asset than the input.
|
||||
*
|
||||
* @param amount The value to scale.
|
||||
* @param rate The transfer rate to apply; must be nonzero.
|
||||
* @param asset The asset type of the returned `STAmount`.
|
||||
* @param roundUp If `true`, round fractional results toward positive
|
||||
* infinity; otherwise round toward zero.
|
||||
* @return The scaled `STAmount` denominated in `asset`.
|
||||
* @pre `rate.value != 0`; asserted in debug builds.
|
||||
*/
|
||||
STAmount
|
||||
multiplyRound(STAmount const& amount, Rate const& rate, Asset const& asset, bool roundUp);
|
||||
|
||||
/** Scale an amount by the inverse of a transfer rate, preserving its asset.
|
||||
*
|
||||
* Computes `amount / (rate / 10^9)` — the inverse of `multiply()`. Used
|
||||
* when back-calculating the gross send amount needed to deliver a given net
|
||||
* amount after fees. Returns `amount` unchanged for `kPARITY_RATE`.
|
||||
*
|
||||
* @param amount The value to scale.
|
||||
* @param rate The transfer rate to invert; must be nonzero.
|
||||
* @return The scaled `STAmount` denominated in the same asset as `amount`.
|
||||
* @pre `rate.value != 0`; asserted in debug builds.
|
||||
*/
|
||||
STAmount
|
||||
divide(STAmount const& amount, Rate const& rate);
|
||||
|
||||
/** Scale an amount by the inverse of a transfer rate with controlled rounding, preserving its asset.
|
||||
*
|
||||
* Like `divide()`, but the caller controls rounding direction. Used in
|
||||
* IOU payment routing for single-currency gross-amount back-calculation.
|
||||
*
|
||||
* @param amount The value to scale.
|
||||
* @param rate The transfer rate to invert; must be nonzero.
|
||||
* @param roundUp If `true`, round fractional results toward positive
|
||||
* infinity; otherwise round toward zero.
|
||||
* @return The scaled `STAmount` denominated in the same asset as `amount`.
|
||||
* @pre `rate.value != 0`; asserted in debug builds.
|
||||
*/
|
||||
STAmount
|
||||
divideRound(STAmount const& amount, Rate const& rate, bool roundUp);
|
||||
|
||||
/** Scale an amount by the inverse of a transfer rate with controlled rounding, emitting a specified asset.
|
||||
*
|
||||
* Overload for offer-crossing and cross-currency paths where the output
|
||||
* must be denominated in a different asset than the input.
|
||||
*
|
||||
* @param amount The value to scale.
|
||||
* @param rate The transfer rate to invert; must be nonzero.
|
||||
* @param asset The asset type of the returned `STAmount`.
|
||||
* @param roundUp If `true`, round fractional results toward positive
|
||||
* infinity; otherwise round toward zero.
|
||||
* @return The scaled `STAmount` denominated in `asset`.
|
||||
* @pre `rate.value != 0`; asserted in debug builds.
|
||||
*/
|
||||
STAmount
|
||||
divideRound(STAmount const& amount, Rate const& rate, Asset const& asset, bool roundUp);
|
||||
|
||||
namespace nft {
|
||||
/** Given a transfer fee (in basis points) convert it to a transfer rate. */
|
||||
|
||||
/** Convert an NFT transfer fee in basis points to a billion-scale `Rate`.
|
||||
*
|
||||
* NFT royalties are stored as a `uint16_t` in basis points (0–50,000
|
||||
* representing 0%–50%). Because `Rate` uses `10^9` as its unit, the
|
||||
* conversion multiplies by `10,000`: a maximum fee of `50,000 bp` becomes
|
||||
* `500,000,000`, safely below `QUALITY_ONE` and within `uint32_t` range.
|
||||
*
|
||||
* @param fee NFT transfer fee in basis points (0–50,000); validated by
|
||||
* transaction processing before reaching this function.
|
||||
* @return A `Rate` suitable for passing to `multiply()` or `multiplyRound()`.
|
||||
* @note Do not call this for ordinary IOU transfer rates — those are already
|
||||
* billion-scale and should be wrapped in `Rate` directly.
|
||||
*/
|
||||
Rate
|
||||
transferFeeAsRate(std::uint16_t fee);
|
||||
|
||||
} // namespace nft
|
||||
|
||||
/** A transfer rate signifying a 1:1 exchange */
|
||||
/** The 1:1 transfer rate — sender pays exactly what the recipient receives.
|
||||
*
|
||||
* Equal to `QUALITY_ONE` (`1,000,000,000`). Every arithmetic function in
|
||||
* this header returns `amount` unchanged when it detects this value, so
|
||||
* payment paths through accounts with no transfer fee never enter the
|
||||
* `STAmount` multiply/divide path. `transferRate()` returns this sentinel
|
||||
* when an account's `sfTransferRate` field is absent.
|
||||
*
|
||||
* @see transferRate() in AccountRootHelpers.h
|
||||
*/
|
||||
extern Rate const kPARITY_RATE;
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
/** @file
|
||||
* Defines the `LedgerHash` type alias used throughout the ledger stack to
|
||||
* identify closed ledgers by their cryptographic digest.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
/** The SHA-512/256 digest that uniquely identifies a closed XRP Ledger.
|
||||
*
|
||||
* A ledger hash is computed over the serialized `LedgerHeader` — covering the
|
||||
* account-state hash, transaction-set hash, sequence number, close time, drop
|
||||
* totals, and parent ledger hash — and is 32 bytes wide.
|
||||
*
|
||||
* The alias over bare `uint256` serves two purposes: it makes interfaces
|
||||
* self-documenting at call sites (e.g., `CanonicalTXSet(LedgerHash const&)`),
|
||||
* and it isolates all ledger-hash usage behind a single name so that a
|
||||
* tagged variant (`base_uint<256, struct LedgerHashTag>`) can be introduced
|
||||
* later to prevent cross-domain substitution with transaction hashes or
|
||||
* account IDs without touching every call site.
|
||||
*
|
||||
* @note `LedgerHeader` stores its hash fields as bare `uint256` for historical
|
||||
* reasons; higher-level APIs (`CanonicalTXSet`, `LedgerHistory`,
|
||||
* `InboundLedgers`, `RCLValidations`) consistently use this alias.
|
||||
*/
|
||||
using LedgerHash = uint256;
|
||||
|
||||
} // namespace xrpl
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user