mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-04 09:16:47 +00:00
Compare commits
1 Commits
pratik/ote
...
dangell7/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
159ea3d7dd |
32
include/xrpl/basics/Archive.h.ai.json
Normal file
32
include/xrpl/basics/Archive.h.ai.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 13,
|
||||
"name": "src"
|
||||
},
|
||||
{
|
||||
"lineno": 13,
|
||||
"name": "dst"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "Header file declaring a function to extract a tar archive compressed with lz4 using Boost Filesystem, within the xrpl namespace.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Archive.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"src",
|
||||
"dst"
|
||||
],
|
||||
"lineno": 13,
|
||||
"name": "extractTarLz4"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 4,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
31
include/xrpl/basics/Archive.h.ai.md
Normal file
31
include/xrpl/basics/Archive.h.ai.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# `Archive.h` — Tar/LZ4 Archive Extraction
|
||||
|
||||
This header declares a single utility function within the `xrpl` namespace: `extractTarLz4`. Its purpose is narrowly scoped — providing the XRPL node software with the ability to unpack `.tar.lz4` archives to a target directory at runtime. The most natural use case is ledger database bootstrapping, where a node downloads a pre-built snapshot of the ledger state rather than replaying the entire transaction history from genesis.
|
||||
|
||||
## The Interface
|
||||
|
||||
```cpp
|
||||
void extractTarLz4(
|
||||
boost::filesystem::path const& src,
|
||||
boost::filesystem::path const& dst);
|
||||
```
|
||||
|
||||
Both parameters are `boost::filesystem::path` rather than `std::string` or `std::filesystem::path`. This is consistent with the broader `xrpl/basics` module (see `FileUtilities.h`), which predates C++17's standard filesystem library and relies on Boost.Filesystem throughout. The function throws `std::runtime_error` on any failure — there is no return value to check or error code to inspect.
|
||||
|
||||
## Implementation Design
|
||||
|
||||
The implementation in `Archive.cpp` delegates all archive I/O to **libarchive**, a portable C library (`<archive.h>`, `<archive_entry.h>`). This is a deliberate choice over rolling a custom tar/lz4 parser: libarchive handles format detection, streaming decompression, and sparse file support in a well-tested, security-audited way.
|
||||
|
||||
Resource management for the two libarchive handles — a reader (`ar`) and a disk writer (`aw`) — is handled via `std::unique_ptr` with custom deleters that call `archive_read_free` and `archive_write_free` respectively. This is the only safe pattern here: libarchive resources must be released even when intermediate steps throw, and wrapping them in `unique_ptr` ensures cleanup happens automatically as the stack unwinds.
|
||||
|
||||
The reader is configured explicitly for the tar format and the lz4 filter (rather than using libarchive's auto-detection). This prevents the function from silently accepting other archive formats, keeping the interface contract tight. The file is opened with a 10240-byte block size, which matches the canonical recommendation in libarchive documentation.
|
||||
|
||||
The disk writer is configured with `ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS`, meaning extracted files faithfully preserve timestamps, permissions, access control lists, and BSD file flags from the archive. For a snapshot intended to be a drop-in replacement for a live ledger database directory, this fidelity matters: the consuming software may rely on mtime or permission bits being intact.
|
||||
|
||||
A non-obvious detail is the pathname rewriting on line 65: before writing each entry to disk, the function prepends `dst` to the entry's stored path using Boost.Filesystem's `/` operator. This is what places all extracted content under `dst` rather than at absolute paths embedded in the archive, and it prevents path traversal issues where a maliciously constructed archive might attempt to write files outside the intended directory tree.
|
||||
|
||||
## Error Handling
|
||||
|
||||
All errors are surfaced through `xrpl::Throw<std::runtime_error>`, defined in `contract.h`. Unlike a raw `throw`, `Throw` first calls `LogThrow` to capture a stack trace before the exception propagates. This means extraction failures produce actionable diagnostics in the node's log — important for diagnosing corrupted snapshots or filesystem problems during a bootstrap operation that might otherwise appear as a silent crash.
|
||||
|
||||
The function validates `src` is a regular file (not a directory or symlink) before opening it, providing a clear early error rather than letting libarchive fail with a less informative message.
|
||||
324
include/xrpl/basics/BasicConfig.h.ai.json
Normal file
324
include/xrpl/basics/BasicConfig.h.ai.json
Normal file
@@ -0,0 +1,324 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 15,
|
||||
"name": "name"
|
||||
},
|
||||
{
|
||||
"lineno": 0,
|
||||
"name": "src"
|
||||
},
|
||||
{
|
||||
"lineno": 0,
|
||||
"name": "dst"
|
||||
},
|
||||
{
|
||||
"lineno": 45,
|
||||
"name": "value"
|
||||
},
|
||||
{
|
||||
"lineno": 66,
|
||||
"name": "key"
|
||||
},
|
||||
{
|
||||
"lineno": 72,
|
||||
"name": "lines"
|
||||
},
|
||||
{
|
||||
"lineno": 78,
|
||||
"name": "line"
|
||||
},
|
||||
{
|
||||
"lineno": 94,
|
||||
"name": "other"
|
||||
},
|
||||
{
|
||||
"lineno": 156,
|
||||
"name": "section"
|
||||
},
|
||||
{
|
||||
"lineno": 193,
|
||||
"name": "sectionName"
|
||||
},
|
||||
{
|
||||
"lineno": 211,
|
||||
"name": "ifs"
|
||||
},
|
||||
{
|
||||
"lineno": 220,
|
||||
"name": "target"
|
||||
},
|
||||
{
|
||||
"lineno": 235,
|
||||
"name": "defaultValue"
|
||||
},
|
||||
{
|
||||
"lineno": 272,
|
||||
"name": "v"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"name"
|
||||
],
|
||||
"lineno": 13,
|
||||
"name": "Section"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 140,
|
||||
"name": "BasicConfig"
|
||||
}
|
||||
],
|
||||
"description": "Defines classes and utility functions for handling configuration sections and key/value pairs, including parsing, storing, and retrieving configuration data for the xrpl project.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/BasicConfig.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 23,
|
||||
"name": "Section::name"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 30,
|
||||
"name": "Section::lines"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 37,
|
||||
"name": "Section::values"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"value"
|
||||
],
|
||||
"lineno": 44,
|
||||
"name": "Section::legacy"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 53,
|
||||
"name": "Section::legacy"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"lineno": 65,
|
||||
"name": "Section::set"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"lines"
|
||||
],
|
||||
"lineno": 71,
|
||||
"name": "Section::append"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"line"
|
||||
],
|
||||
"lineno": 77,
|
||||
"name": "Section::append"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"name"
|
||||
],
|
||||
"lineno": 82,
|
||||
"name": "Section::exists"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"name"
|
||||
],
|
||||
"lineno": 85,
|
||||
"name": "Section::get"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"name",
|
||||
"other"
|
||||
],
|
||||
"lineno": 93,
|
||||
"name": "Section::value_or"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 101,
|
||||
"name": "Section::had_trailing_comments"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 110,
|
||||
"name": "Section::empty"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 115,
|
||||
"name": "Section::size"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 120,
|
||||
"name": "Section::begin"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 125,
|
||||
"name": "Section::cbegin"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 130,
|
||||
"name": "Section::end"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 135,
|
||||
"name": "Section::cend"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"name"
|
||||
],
|
||||
"lineno": 151,
|
||||
"name": "BasicConfig::exists"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"name"
|
||||
],
|
||||
"lineno": 155,
|
||||
"name": "BasicConfig::section"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"name"
|
||||
],
|
||||
"lineno": 158,
|
||||
"name": "BasicConfig::section"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"name"
|
||||
],
|
||||
"lineno": 161,
|
||||
"name": "BasicConfig::operator[]"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"name"
|
||||
],
|
||||
"lineno": 165,
|
||||
"name": "BasicConfig::operator[]"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"section",
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"lineno": 171,
|
||||
"name": "BasicConfig::overwrite"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"section"
|
||||
],
|
||||
"lineno": 176,
|
||||
"name": "BasicConfig::deprecatedClearSection"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"section",
|
||||
"value"
|
||||
],
|
||||
"lineno": 183,
|
||||
"name": "BasicConfig::legacy"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"sectionName"
|
||||
],
|
||||
"lineno": 192,
|
||||
"name": "BasicConfig::legacy"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 201,
|
||||
"name": "BasicConfig::had_trailing_comments"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"ifs"
|
||||
],
|
||||
"lineno": 210,
|
||||
"name": "BasicConfig::build"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"target",
|
||||
"name",
|
||||
"section"
|
||||
],
|
||||
"lineno": 219,
|
||||
"name": "set"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"target",
|
||||
"defaultValue",
|
||||
"name",
|
||||
"section"
|
||||
],
|
||||
"lineno": 234,
|
||||
"name": "set"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"section",
|
||||
"name",
|
||||
"defaultValue"
|
||||
],
|
||||
"lineno": 247,
|
||||
"name": "get"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"section",
|
||||
"name",
|
||||
"defaultValue"
|
||||
],
|
||||
"lineno": 260,
|
||||
"name": "get"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"section",
|
||||
"name",
|
||||
"v"
|
||||
],
|
||||
"lineno": 271,
|
||||
"name": "get_if_exists"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"section",
|
||||
"name",
|
||||
"v"
|
||||
],
|
||||
"lineno": 277,
|
||||
"name": "get_if_exists<bool>"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 10,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
include/xrpl/basics/BasicConfig.h.ai.md
Normal file
44
include/xrpl/basics/BasicConfig.h.ai.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# `BasicConfig.h` — INI-Style Configuration Substrate
|
||||
|
||||
`BasicConfig.h` defines the foundational data model for the XRPL node's configuration system. It sits at the bottom of a two-layer design: this file provides the in-memory representation and query interface for section-based configuration data, while the concrete `Config` class (in `src/xrpld/core/Config.h`) inherits from `BasicConfig` and adds filesystem loading, application-specific typed fields, and validator management. The header comment on `Config` explicitly labels that derived class as deprecated, signaling that `BasicConfig`'s style — decentralized, per-module parsing — is the intended long-term direction.
|
||||
|
||||
## Data Model: Two Representations in One `Section`
|
||||
|
||||
The `Section` class maintains three parallel containers for the same underlying config content:
|
||||
|
||||
- `lookup_` — an `unordered_map<string, string>` for `key = value` pairs, used for named lookups
|
||||
- `values_` — a `vector<string>` of non-key-value lines (bare tokens like IP addresses or file paths)
|
||||
- `lines_` — a `vector<string>` containing every non-empty, non-comment line in canonical form
|
||||
|
||||
This triple storage isn't redundancy — it reflects the two distinct ways config sections are used in practice. Sections like `[server]` contain key=value pairs consumed by name; sections like `[validators]` contain bare values (one per line) iterated as a list. The `lines()` accessor preserves insertion order, which matters for list-type sections where positional meaning exists.
|
||||
|
||||
The `append()` method is where parsing happens. It applies a Boost regex matching `^key=value` to each incoming line. Lines that match go into `lookup_` via `set()`; non-matching lines go into `values_`. Both go into `lines_`. The same method also handles inline comment stripping: `#` characters are treated as comment delimiters unless escaped with `\`. The escape character is consumed when found (`val.erase(comment - 1, 1)`), allowing literal `#` characters in values. This detail is tracked via `had_trailing_comments_`, which bubbles up through `BasicConfig::had_trailing_comments()` via `std::any_of` — presumably to emit a deprecation warning to operators about ambiguous config syntax.
|
||||
|
||||
## The "Legacy" Pattern
|
||||
|
||||
Some older config sections hold a single freeform value rather than key-value pairs — for example `[node_db]` in its pre-structured form. The `legacy()` getter/setter pair accommodates this by treating the first entry of `lines_` as the canonical value. Reading a `Section` as legacy on a multi-line section intentionally throws `std::runtime_error` via `Throw<>()`, enforcing that this access path is only valid for single-line sections. This prevents silent misreads where code expecting one value silently gets only the first of many.
|
||||
|
||||
`BasicConfig` also exposes `legacy()` at the aggregate level, forwarding to the named section's `legacy()`. This provides `config.legacy("section_name")` as a convenience for the many legacy callsites in `Config.cpp`.
|
||||
|
||||
## `BasicConfig`: Container and Access Protocol
|
||||
|
||||
`BasicConfig` holds an `unordered_map<string, Section>`, keyed by section name. The critical behavioral difference between the const and non-const `section()` overloads reflects a deliberate design choice:
|
||||
|
||||
- Non-const `section()` calls `map_.emplace(name, name)` — it auto-creates an empty section on first access. This allows callers to unconditionally call `config["new_section"].set(...)` without precondition checks.
|
||||
- Const `section()` returns a reference to a `static Section const none("")` sentinel when the section doesn't exist. This avoids exceptions during read-only configuration queries and makes `operator[]` safe to call on a const `BasicConfig` even for absent sections.
|
||||
|
||||
The `overwrite()` method is specifically for command-line argument injection, layering CLI-provided values over whatever the config file contains. `deprecatedClearSection()` (name signals intent) wipes a section's content by replacing its `Section` object wholesale — used historically to clear sections before reloading.
|
||||
|
||||
The `build()` method is `protected`, not `public`. It consumes an `IniFileSections` (a `unordered_map<string, vector<string>>`), which is the raw pre-parsed form produced by `parseIniFile()` in `Config.cpp`. Subclasses call `build()` after obtaining this intermediate representation, keeping the file I/O and INI parsing out of `BasicConfig` itself.
|
||||
|
||||
## Free Function Query Layer
|
||||
|
||||
The file exports three sets of free functions designed for module-level configuration consumption:
|
||||
|
||||
`set(target, name, section)` reads a named key, casts it via `boost::lexical_cast<T>`, and assigns to `target` only on success — leaving `target` unchanged on missing key or bad cast. The two-argument variant adds an explicit default value applied on failure. Both return `bool` indicating whether the config file actually specified the value, which is important for distinguishing "user set this to the default" from "user didn't set this."
|
||||
|
||||
`get(section, name, defaultValue)` is a value-returning variant; it catches `bad_lexical_cast` and falls back to the default silently. An overload handles `char const*` defaults to avoid awkward template deduction with string literals.
|
||||
|
||||
`get_if_exists<bool>` is explicitly specialized to read boolean config values as integers (`0` or `1`) rather than as the string tokens `"true"` or `"false"`. This matches the XRPL config file convention where booleans are expressed numerically, and avoids `lexical_cast<bool>` which in Boost accepts `"true"` but not `"1"` depending on locale.
|
||||
|
||||
Together these three free functions provide a consistent, exception-safe pattern that modules throughout the codebase use to pull typed values from their respective config sections without having to handle parse failures individually.
|
||||
14
include/xrpl/basics/Blob.h.ai.json
Normal file
14
include/xrpl/basics/Blob.h.ai.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [],
|
||||
"description": "Defines a type alias 'Blob' for storing linear binary data as a vector of unsigned char within the xrpl namespace.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Blob.h",
|
||||
"functions": [],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 4,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
include/xrpl/basics/Blob.h.ai.md
Normal file
17
include/xrpl/basics/Blob.h.ai.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# `include/xrpl/basics/Blob.h`
|
||||
|
||||
`Blob.h` introduces a single named type alias used throughout the XRPL codebase for owning, mutable binary data:
|
||||
|
||||
```cpp
|
||||
using Blob = std::vector<unsigned char>;
|
||||
```
|
||||
|
||||
Its role is to give raw byte sequences a meaningful, searchable name rather than leaving `std::vector<unsigned char>` scattered as an anonymous type across the protocol and serialization layers. `Blob` appears as the internal storage buffer inside `Serializer` (`mData`), as the return type of serialization helpers, and in `StringUtilities` for hex encoding and SQL blob literals.
|
||||
|
||||
`Blob` sits at one corner of the three-type binary data model in `xrpl::basics`:
|
||||
|
||||
- **`Blob`** (`std::vector<unsigned char>`) — mutable, dynamically resizable, owns its memory. The right choice when data is built up incrementally, as in `Serializer`.
|
||||
- **`Buffer`** — fixed-size block allocated with `unique_ptr<uint8_t[]>`, no capacity overhead, suitable when size is known upfront and resizing is not required.
|
||||
- **`Slice`** — a non-owning, read-only `(pointer, length)` view. Cheap to copy and pass; `makeSlice()` factory overloads accept both `Blob` and `Buffer` seamlessly.
|
||||
|
||||
The choice of `unsigned char` rather than `char` is deliberate: it avoids signed/unsigned arithmetic warnings when working with raw binary values and aligns with the `uint8_t` element type used by `Slice` and `Buffer`. Because `Blob` is simply a `std::vector`, callers get the full standard iterator interface, `push_back`, `resize`, and range-insert without any additional wrapper API.
|
||||
42
include/xrpl/basics/Buffer.h.ai.json
Normal file
42
include/xrpl/basics/Buffer.h.ai.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"size",
|
||||
"data",
|
||||
"other",
|
||||
"s"
|
||||
],
|
||||
"lineno": 10,
|
||||
"name": "Buffer"
|
||||
}
|
||||
],
|
||||
"description": "Defines a Buffer class for managing dynamic byte arrays, similar to std::vector<char> but optimized for use as a BufferFactory, including copy/move semantics, assignment from slices, and comparison operators.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Buffer.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"lhs",
|
||||
"rhs"
|
||||
],
|
||||
"lineno": 120,
|
||||
"name": "operator=="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"lhs",
|
||||
"rhs"
|
||||
],
|
||||
"lineno": 130,
|
||||
"name": "operator!="
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 7,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
include/xrpl/basics/Buffer.h.ai.md
Normal file
35
include/xrpl/basics/Buffer.h.ai.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# `include/xrpl/basics/Buffer.h`
|
||||
|
||||
## Role in the System
|
||||
|
||||
`Buffer` is the XRPL codebase's canonical owning byte container. It occupies a distinct position alongside two other byte-handling types: `Slice`, which is a non-owning, immutable view into existing memory, and `Blob` (a typedef for `std::vector<unsigned char>`), which is a general-purpose growable sequence. `Buffer` fills the gap between these: it owns its memory exclusively, is mutable, but makes no provision for incremental growth. When you need to allocate a block of bytes, write into it once, and pass it around by move, `Buffer` is the right tool.
|
||||
|
||||
The class also satisfies an informal `BufferFactory` concept used by compression utilities — a callable that accepts a size and returns a `void*` to writable memory. This dual role as both a container and an allocator-callback is the most distinctive design choice in the file.
|
||||
|
||||
## Ownership and Internal Layout
|
||||
|
||||
The backing store is a `std::unique_ptr<std::uint8_t[]>`, giving the class clear exclusive ownership with automatic deallocation. The invariant enforced throughout is that an empty buffer (`size_ == 0`) always holds a null pointer — never a zero-byte allocation. This is visible in the size constructor: `new std::uint8_t[size]` is called only when `size` is non-zero, and `alloc()` resets to `nullptr` if `n == 0`. The test suite verifies this invariant explicitly via its `sane()` helper, which asserts `data() == nullptr` iff `empty()`. Treating null as the canonical empty state avoids any ambiguity at the call site and makes zero-initialization checks safe without checking both pointer and size.
|
||||
|
||||
## The `alloc()` Pattern — Discard, Don't Resize
|
||||
|
||||
The central API difference from `std::vector` is `alloc(std::size_t n)`, which reallocates the buffer to exactly `n` bytes and discards any existing content. Unlike `vector::resize()`, there is no attempt to preserve data. This is intentional: the primary workload for `Buffer` is receiving output from operations like decompression, where the caller pre-computes the required size and wants a fresh block to write into. Reallocation is skipped entirely if the requested size equals the current size, avoiding a pointless free/alloc cycle when the same `Buffer` is reused across calls of equal output length.
|
||||
|
||||
The `operator()(std::size_t n)` overload simply delegates to `alloc()` and returns a `void*`, satisfying the `BufferFactory` concept expected by `lz4Compress` in `CompressionAlgorithms.h`. That template function calls `bf(outCapacity)` to obtain the destination buffer — passing a `Buffer` object directly fills both roles (allocation and storage) in a single object.
|
||||
|
||||
## Slice Integration
|
||||
|
||||
`Buffer` is tightly coupled to `Slice`. It provides an implicit conversion `operator Slice() const noexcept`, so any `Buffer` can be passed wherever a `Slice` is expected without an explicit cast. The reverse — constructing a `Buffer` from a `Slice` — is marked `explicit`, preventing accidental copies of view-only data.
|
||||
|
||||
The `operator=(Slice)` assignment requires particular attention: before copying, it checks via `XRPL_ASSERT` that the source slice does not overlap with the `Buffer`'s own storage. The danger is that `alloc()` frees the old memory first, and if the incoming `Slice` pointed into that memory, the subsequent `memcpy` would be a use-after-free. The assertion guards against this specific self-overlapping scenario. Note that `operator=(Buffer const&)` uses a different path through `alloc()` + `memcpy`, which naturally handles self-assignment because `alloc()` is a no-op when sizes match — the existing pointer is reused and then `memcpy`-d over itself (which is defined behavior for `memcpy` with identical source and destination).
|
||||
|
||||
## Move Semantics
|
||||
|
||||
Both move constructor and move assignment are `noexcept`, a static guarantee the test suite verifies with `static_assert`. This ensures `Buffer` can be held in standard containers like `std::vector` without triggering copies on reallocation. After a move, the source is left in a valid empty state: `p_` is null (via `unique_ptr` move semantics) and `size_` is explicitly reset to zero.
|
||||
|
||||
## Comparison and Iteration
|
||||
|
||||
Equality comparison is implemented as a free function using `std::memcmp` after a size check. The class exposes only `const_iterator` (raw `uint8_t const*` pointers), meaning range-for loops and standard algorithms can consume the buffer's contents read-only. Mutable iteration is available only through `data()`, keeping the interface honest about the distinction between reading and writing into the buffer.
|
||||
|
||||
## Contrast with `Blob`
|
||||
|
||||
`Blob` (`std::vector<unsigned char>`) is still used extensively in the codebase for cases where the byte sequence grows incrementally, such as serialization output. `Buffer` is preferred when the size is known upfront, ownership transfer by move is the primary operation, or the `BufferFactory` pattern is required — for example, storing the output of an LZ4 decompression call without needing the capacity/size distinction that `vector` maintains internally.
|
||||
38
include/xrpl/basics/ByteUtilities.h.ai.json
Normal file
38
include/xrpl/basics/ByteUtilities.h.ai.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 6,
|
||||
"name": "value"
|
||||
},
|
||||
{
|
||||
"lineno": 13,
|
||||
"name": "value"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "Provides constexpr utility functions to convert values to kilobytes and megabytes within the xrpl namespace.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/ByteUtilities.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"value"
|
||||
],
|
||||
"lineno": 6,
|
||||
"name": "kilobytes"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"value"
|
||||
],
|
||||
"lineno": 13,
|
||||
"name": "megabytes"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 3,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
include/xrpl/basics/ByteUtilities.h.ai.md
Normal file
26
include/xrpl/basics/ByteUtilities.h.ai.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# `ByteUtilities.h` — Compile-Time Byte-Size Helpers
|
||||
|
||||
`ByteUtilities.h` is a minimal, header-only utility in `xrpl/basics` that provides two `constexpr` template functions — `kilobytes()` and `megabytes()` — for expressing byte-count constants in human-readable units at compile time. The file exists purely to eliminate magic numbers from sites that configure buffer sizes, memory limits, and slab allocator parameters throughout the XRPL codebase.
|
||||
|
||||
## The Functions
|
||||
|
||||
`kilobytes(value)` multiplies its argument by 1024. `megabytes(value)` composes that twice — it calls `kilobytes(kilobytes(value))` — which gives the correct factor of 1,048,576 (2²⁰) without any separate literal. Both functions are templated on `T`, so they work with any integral or arithmetic type and return the same type that the arithmetic produces, letting the caller's type context drive the result without an explicit cast. Both are `constexpr` and `noexcept`, meaning the computation happens entirely at compile time and has no runtime overhead whatsoever.
|
||||
|
||||
The `static_assert` lines immediately below the definitions act as inline tests: they verify `kilobytes(2) == 2048` and `megabytes(3) == 3145728` during every compilation, preventing any silent regression if the implementation were ever accidentally changed.
|
||||
|
||||
## Design Rationale
|
||||
|
||||
The template design over a fixed `size_t` signature is deliberate. Call sites like `megabytes(std::size_t(60))` in `SHAMapItem.h` need to produce `std::size_t` results for slab allocator configuration, while other uses such as `megabytes(256)` in `RPCCall.cpp` are happy with `int`-width results for comparison. By letting `auto` return the natural result of the arithmetic, the functions avoid both unwanted narrowing conversions and unwanted widening that could paper over a type mismatch.
|
||||
|
||||
The composition `kilobytes(kilobytes(value))` for megabytes is a small but telling choice: it reuses the already-tested primitive rather than independently writing `value * 1024 * 1024`, keeping the chain of trust short and making the relationship between units self-documenting.
|
||||
|
||||
## Usage Across the Codebase
|
||||
|
||||
The functions appear at exactly the kinds of boundaries where misreading a magnitude would have serious consequences:
|
||||
|
||||
- **Overlay message cap**: `src/xrpld/overlay/Message.h` defines `constexpr std::size_t maximumMessageSize = megabytes(64)`, bounding the maximum peer-to-peer message size to 64 MiB.
|
||||
- **RPC reply limit**: `src/xrpld/rpc/detail/RPCCall.cpp` defines `constexpr auto RPC_REPLY_MAX_BYTES = megabytes(256)` to guard against unbounded JSON responses.
|
||||
- **Ledger and open-view buffers**: `include/xrpl/ledger/OpenView.h` and `include/xrpl/ledger/detail/RawStateTable.h` both set `initialBufferSize = kilobytes(256)` for their serialisation scratch buffers.
|
||||
- **ShaMap slab allocator**: `SHAMapItem.h` uses `megabytes()` to express the per-size-class allocation limits for the slab allocator pools (60 MB, 46 MB, etc.), and `TaggedPointer.ipp` uses `kilobytes(512)` for the slab block granularity.
|
||||
|
||||
The consistent use of these helpers rather than raw literals means that anyone reading any of those files immediately understands the intended scale without mental arithmetic, and the compiler catches any integer overflow that a bare literal might hide at the point of definition.
|
||||
93
include/xrpl/basics/CompressionAlgorithms.h.ai.json
Normal file
93
include/xrpl/basics/CompressionAlgorithms.h.ai.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 18,
|
||||
"name": "in"
|
||||
},
|
||||
{
|
||||
"lineno": 18,
|
||||
"name": "inSize"
|
||||
},
|
||||
{
|
||||
"lineno": 18,
|
||||
"name": "bf"
|
||||
},
|
||||
{
|
||||
"lineno": 41,
|
||||
"name": "in"
|
||||
},
|
||||
{
|
||||
"lineno": 41,
|
||||
"name": "inSizeUnchecked"
|
||||
},
|
||||
{
|
||||
"lineno": 41,
|
||||
"name": "decompressed"
|
||||
},
|
||||
{
|
||||
"lineno": 41,
|
||||
"name": "decompressedSizeUnchecked"
|
||||
},
|
||||
{
|
||||
"lineno": 62,
|
||||
"name": "in"
|
||||
},
|
||||
{
|
||||
"lineno": 62,
|
||||
"name": "inSize"
|
||||
},
|
||||
{
|
||||
"lineno": 62,
|
||||
"name": "decompressed"
|
||||
},
|
||||
{
|
||||
"lineno": 62,
|
||||
"name": "decompressedSize"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "Provides LZ4 block compression and decompression utilities, including template and inline functions for compressing and decompressing data buffers and streams within the xrpl::compression_algorithms namespace.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/CompressionAlgorithms.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"in",
|
||||
"inSize",
|
||||
"bf"
|
||||
],
|
||||
"lineno": 18,
|
||||
"name": "lz4Compress"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"in",
|
||||
"inSizeUnchecked",
|
||||
"decompressed",
|
||||
"decompressedSizeUnchecked"
|
||||
],
|
||||
"lineno": 41,
|
||||
"name": "lz4Decompress"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"in",
|
||||
"inSize",
|
||||
"decompressed",
|
||||
"decompressedSize"
|
||||
],
|
||||
"lineno": 62,
|
||||
"name": "lz4Decompress"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 9,
|
||||
"name": "xrpl"
|
||||
},
|
||||
{
|
||||
"lineno": 11,
|
||||
"name": "compression_algorithms"
|
||||
}
|
||||
]
|
||||
}
|
||||
53
include/xrpl/basics/CompressionAlgorithms.h.ai.md
Normal file
53
include/xrpl/basics/CompressionAlgorithms.h.ai.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# `CompressionAlgorithms.h` — LZ4 Block Compression Primitives
|
||||
|
||||
This header lives in `include/xrpl/basics/` and provides the low-level LZ4 compression and decompression routines used by the XRPL peer overlay network. It sits one abstraction layer below `src/xrpld/overlay/Compression.h`, which adds algorithm-selection logic and error suppression on top of what this file exposes.
|
||||
|
||||
## Architectural Role
|
||||
|
||||
When XRPL nodes exchange P2P messages they can optionally compress the payload before transmission. The overlay layer negotiates compression during the connection handshake and then routes compressed messages through the functions defined here. `CompressionAlgorithms.h` isolates the raw LZ4 calls — the `int`-based C API hazards, buffer management, and stream chunking — from the policy-level decisions that live in `Compression.h`.
|
||||
|
||||
The functions are entirely in the `xrpl::compression_algorithms` namespace. There are no classes, no state, no singletons — just three free functions.
|
||||
|
||||
## `lz4Compress` — Template with BufferFactory
|
||||
|
||||
```cpp
|
||||
template <typename BufferFactory>
|
||||
std::size_t lz4Compress(void const* in, std::size_t inSize, BufferFactory&& bf)
|
||||
```
|
||||
|
||||
The design choice to accept a `BufferFactory` callable rather than returning a `std::vector` is deliberate and important. The caller knows its allocation context: in the overlay code it may be writing into a Protobuf `CodedOutputStream` region or a pooled buffer. The factory receives the worst-case compressed size from `LZ4_compressBound` and returns a raw pointer; the template accepts any callable that satisfies this contract without virtual dispatch overhead.
|
||||
|
||||
The sole pre-condition check guards against input larger than `UINT32_MAX`. LZ4's block API uses `int` internally, so exceeding that limit would silently truncate the size argument. The function throws via `Throw<std::runtime_error>`, which logs a call stack through `contract.h` before throwing — consistent with XRPL's "crash loudly with context" philosophy for invariant violations.
|
||||
|
||||
## `lz4Decompress` — Raw Buffer Overload
|
||||
|
||||
```cpp
|
||||
inline std::size_t lz4Decompress(
|
||||
std::uint8_t const* in, std::size_t inSizeUnchecked,
|
||||
std::uint8_t* decompressed, std::size_t decompressedSizeUnchecked)
|
||||
```
|
||||
|
||||
The `Unchecked` naming in the parameters is the code's way of signalling that the `size_t` → `int` narrowing has not yet been validated. The function immediately casts both sizes to `int` and checks for `<= 0`. This catches two distinct failure modes: a genuinely zero-length buffer, and a `size_t` value large enough that the narrowing wrap produces a non-positive `int`. Separating these checks with distinct error messages makes debugging easier.
|
||||
|
||||
`LZ4_decompress_safe` is used rather than the faster `LZ4_decompress_fast`. The safe variant takes the output buffer capacity as a bound and will not write past it even if the compressed data is malformed — essential when the input arrives from an untrusted peer on the network.
|
||||
|
||||
The function enforces an exact-size postcondition: if `LZ4_decompress_safe` returns anything other than the expected `decompressedSize` it throws. This reflects the fact that, in the overlay protocol, the original message size is transmitted in the message header; any mismatch means either corruption or a peer bug.
|
||||
|
||||
## `lz4Decompress` — Streaming ZeroCopyInputStream Overload
|
||||
|
||||
```cpp
|
||||
template <typename InputStream>
|
||||
std::size_t lz4Decompress(
|
||||
InputStream& in, std::size_t inSize,
|
||||
std::uint8_t* decompressed, std::size_t decompressedSize)
|
||||
```
|
||||
|
||||
This overload works with Protobuf-style `ZeroCopyInputStream` objects that expose data as a series of chunks rather than a single contiguous buffer. The key optimization is the fast path: if the very first chunk returned by `in.Next()` is at least `inSize` bytes long, the function uses that chunk's pointer directly and avoids any allocation. In practice, compressed P2P messages typically arrive in a single TCP read buffer, so this path is taken most of the time.
|
||||
|
||||
When the data spans multiple chunks, the function lazily allocates a `std::vector<std::uint8_t>` of exactly `inSize` bytes (note the `compressed.resize(inSize)` is only reached on the second iteration) and copies chunks into it until the full compressed message is assembled. After reading, any bytes that were consumed from the stream beyond `inSize` are returned via `in.BackUp()`, preserving the stream cursor for the next message in the framing protocol.
|
||||
|
||||
The final validation before delegating to the raw overload checks that the amount actually read matches what was requested. This guards against a stream that ends early — e.g., a truncated TCP connection or a framing bug where the declared size doesn't match the available data.
|
||||
|
||||
## Relationship to `Compression.h`
|
||||
|
||||
The overlay's `Compression.h` wraps these two functions inside `compress()` and `decompress()` functions that add an `Algorithm` enum parameter (currently `Algorithm::LZ4 = 0x90` or `Algorithm::None`). Those wrappers catch all exceptions from the functions here and return `0` on failure, converting the throw-on-error contract into a return-zero-on-error contract. The distinction is intentional: the raw primitives throw so that callers who want structured error handling can use them; the overlay wrapper normalises failures to a `0` return value to simplify the state machine in the peer message processing loop.
|
||||
88
include/xrpl/basics/CountedObject.h.ai.json
Normal file
88
include/xrpl/basics/CountedObject.h.ai.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 23,
|
||||
"name": "name"
|
||||
},
|
||||
{
|
||||
"lineno": 16,
|
||||
"name": "minimumThreshold"
|
||||
},
|
||||
{
|
||||
"lineno": 65,
|
||||
"name": "Object"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 7,
|
||||
"name": "CountedObjects"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"name"
|
||||
],
|
||||
"lineno": 22,
|
||||
"name": "Counter"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 65,
|
||||
"name": "CountedObject"
|
||||
}
|
||||
],
|
||||
"description": "Provides a mechanism to count and report the number of instances of various object types at runtime, using a lock-free linked list and atomic counters. Includes a base class for automatic instance counting.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/CountedObject.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 10,
|
||||
"name": "getInstance"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"minimumThreshold"
|
||||
],
|
||||
"lineno": 16,
|
||||
"name": "getCounts"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 36,
|
||||
"name": "increment"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 41,
|
||||
"name": "decrement"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 46,
|
||||
"name": "getCount"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 51,
|
||||
"name": "getNext"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 56,
|
||||
"name": "getName"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 71,
|
||||
"name": "getCounter"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 6,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
55
include/xrpl/basics/CountedObject.h.ai.md
Normal file
55
include/xrpl/basics/CountedObject.h.ai.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# `include/xrpl/basics/CountedObject.h`
|
||||
|
||||
## Purpose
|
||||
|
||||
This header provides a zero-per-instance-overhead mechanism for counting live objects of any given type throughout the rippled process lifetime. It exists for operational diagnostics: the `get_counts` admin RPC command interrogates `CountedObjects` to report how many instances of each tracked type are currently alive, helping operators identify memory growth, cache saturation, or unexpected object accumulation.
|
||||
|
||||
## Design Pattern — CRTP Instance Counting
|
||||
|
||||
The design uses the Curiously Recurring Template Pattern (CRTP). A class opts into counting by inheriting `CountedObject<Derived>`:
|
||||
|
||||
```cpp
|
||||
class SHAMapItem : public CountedObject<SHAMapItem> { ... };
|
||||
class NodeObject : public CountedObject<NodeObject> { ... };
|
||||
class Job : public CountedObject<Job> { ... };
|
||||
```
|
||||
|
||||
Across the codebase, roughly two dozen types follow this pattern — `STPathElement`, `STPath`, `InfoSub`, `HashRouter::Entry`, `Book`, `CanonicalTXSet`, and many more. Adding the base class is the entire integration cost; no other instrumentation is required.
|
||||
|
||||
The key insight that makes this zero-per-instance overhead is that `CountedObject<T>::getCounter()` returns a **function-local static** `Counter` object — one per template instantiation, not one per live instance. The only per-instance cost is two atomic increments (constructor and destructor) touching a shared counter.
|
||||
|
||||
## Three-Layer Architecture
|
||||
|
||||
**`CountedObject<T>`** (template base class) — the public-facing layer. Its default constructor, copy constructor, and destructor call `getCounter().increment()` / `decrement()` respectively. The copy constructor is explicitly defined to increment because a copy produces a new live object; the assignment operator is `= default` because assigning between two existing objects doesn't change the total number of live instances. There is no explicit move constructor, so moves fall back to the copy constructor, which correctly increments for the new object while the source's destructor later decrements for the old one.
|
||||
|
||||
**`CountedObjects::Counter`** (inner class) — the per-type bookkeeping node. Each `Counter` holds its type name (obtained via `beast::type_name<T>()`, which uses `typeid` plus GCC/Clang ABI demangling for a human-readable string), an `std::atomic<int>` live count, and a raw `Counter*` pointer to the next node in an intrusive singly-linked list.
|
||||
|
||||
**`CountedObjects`** (singleton) — the global registry. It owns the head of the lock-free linked list and a count of registered counter types.
|
||||
|
||||
## Lock-Free Registration
|
||||
|
||||
`Counter` objects self-register when they are first constructed — which happens at first use of any given type, during static initialization of `getCounter()`'s local static. Registration must be thread-safe without a mutex, because many types can be instantiated concurrently at startup:
|
||||
|
||||
```cpp
|
||||
Counter* head = nullptr;
|
||||
do {
|
||||
head = instance.m_head.load();
|
||||
next_ = head;
|
||||
} while (instance.m_head.exchange(this) != head);
|
||||
```
|
||||
|
||||
This is a classic CAS (compare-and-swap) insertion loop: load the current head, set `next_` to it, then atomically exchange the head with `this`. If the head changed between the load and the exchange, retry. Because `Counter` objects are permanent (static lifetime), they are never removed from the list, so traversal during `getCounts()` never encounters a dangling pointer regardless of whether other registrations are happening concurrently.
|
||||
|
||||
## `getCounts()` and the Reporting Path
|
||||
|
||||
`CountedObjects::getCounts(int minimumThreshold)` traverses the linked list and collects `(name, count)` pairs for any type whose live count is at or above the threshold. It pre-reserves the result vector using `m_count.load()` as a hint (the comment in the implementation acknowledges this can be temporarily under-counted under concurrency — it is only an optimization). The results are sorted alphabetically before return.
|
||||
|
||||
The `get_counts` admin RPC handler (`GetCounts.cpp`) calls this with a configurable `min_count` (defaulting to 10) and serializes the results into a JSON object, mixing them with cache statistics, database sizes, write load, and uptime. Object counts appear as top-level keys named by the demangled C++ type.
|
||||
|
||||
## Concurrency Properties
|
||||
|
||||
All per-type counts use `std::atomic<int>` with default sequential consistency, so `increment()` and `decrement()` are safe from any thread. The linked-list head pointer `m_head` is also `std::atomic<Counter*>`. There are no mutexes anywhere in this file. The only non-atomic operation is reading `Counter::next_` during traversal in `getCounts()`, which is safe because `next_` is written exactly once at construction time and never modified thereafter.
|
||||
|
||||
## Why Not Alternatives
|
||||
|
||||
A virtual-function approach (e.g., a pure virtual `typeName()` method) would require each instance to carry a vtable pointer and would not trivially aggregate counts across all instances of the same type without additional infrastructure. A manual registry with `std::map` would need a mutex. The CRTP-plus-static-counter approach achieves type safety, automatic demangled names, lock-free operation, and zero per-instance storage — at the cost of slightly surprising copy/move semantics that operators must understand when subclassing.
|
||||
115
include/xrpl/basics/DecayingSample.h.ai.json
Normal file
115
include/xrpl/basics/DecayingSample.h.ai.json
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 18,
|
||||
"name": "now"
|
||||
},
|
||||
{
|
||||
"lineno": 26,
|
||||
"name": "value"
|
||||
},
|
||||
{
|
||||
"lineno": 26,
|
||||
"name": "now"
|
||||
},
|
||||
{
|
||||
"lineno": 34,
|
||||
"name": "now"
|
||||
},
|
||||
{
|
||||
"lineno": 41,
|
||||
"name": "now"
|
||||
},
|
||||
{
|
||||
"lineno": 61,
|
||||
"name": "now"
|
||||
},
|
||||
{
|
||||
"lineno": 74,
|
||||
"name": "value"
|
||||
},
|
||||
{
|
||||
"lineno": 74,
|
||||
"name": "now"
|
||||
},
|
||||
{
|
||||
"lineno": 79,
|
||||
"name": "now"
|
||||
},
|
||||
{
|
||||
"lineno": 86,
|
||||
"name": "now"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"time_point now"
|
||||
],
|
||||
"lineno": 10,
|
||||
"name": "DecayingSample"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"time_point now"
|
||||
],
|
||||
"lineno": 61,
|
||||
"name": "DecayWindow"
|
||||
}
|
||||
],
|
||||
"description": "Provides two template classes for sampling functions using exponential decay: DecayingSample (with a fixed window) and DecayWindow (with a half-life), useful for tracking decaying averages or statistics over time.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/DecayingSample.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"value",
|
||||
"now"
|
||||
],
|
||||
"lineno": 26,
|
||||
"name": "DecayingSample::add"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"now"
|
||||
],
|
||||
"lineno": 34,
|
||||
"name": "DecayingSample::value"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"now"
|
||||
],
|
||||
"lineno": 41,
|
||||
"name": "DecayingSample::decay"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"value",
|
||||
"now"
|
||||
],
|
||||
"lineno": 74,
|
||||
"name": "DecayWindow::add"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"now"
|
||||
],
|
||||
"lineno": 79,
|
||||
"name": "DecayWindow::value"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"now"
|
||||
],
|
||||
"lineno": 86,
|
||||
"name": "DecayWindow::decay"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 4,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
include/xrpl/basics/DecayingSample.h.ai.md
Normal file
39
include/xrpl/basics/DecayingSample.h.ai.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# `DecayingSample.h` — Exponential Decay Accumulators
|
||||
|
||||
This header provides two small template classes that maintain a running accumulation of values that automatically decay over time. Both are used throughout the XRPL node to answer the question "how much activity has happened recently?" without needing to store timestamped histories — the decay does the windowing implicitly.
|
||||
|
||||
## `DecayingSample<Window, Clock>`
|
||||
|
||||
`DecayingSample` maintains an integer accumulator that decays by approximately `1/Window` of its current value each second, producing a rate estimate normalized over the window length. It drives the resource manager's per-peer charge tracking: `Entry.h` declares `local_balance` as `DecayingSample<decayWindowSeconds, clock_type>` where `decayWindowSeconds = 32` (a power of two, per a comment in `Tuning.h`, so the division can be optimized to a bit-shift by the compiler).
|
||||
|
||||
The core decay step is deliberately integer arithmetic with ceiling division:
|
||||
|
||||
```cpp
|
||||
m_value -= (m_value + Window - 1) / Window;
|
||||
```
|
||||
|
||||
This subtracts at least 1 when `m_value` is positive, so the value cannot stall at a non-zero integer indefinitely — a safety property important for rate limiting. Adding `Window - 1` before dividing implements ceiling division, meaning the decay rounds up rather than down. The practical effect is the balance decays slightly faster than the mathematically ideal `m_value *= (1 - 1/Window)^elapsed`, which is a conservative choice for load balancing: erring toward under-charging rather than over-charging.
|
||||
|
||||
The `decay()` fast-path cuts off long idle periods: if more than `4 * Window` seconds have elapsed since the last update (which would leave the value at less than ~2% of its original magnitude), `m_value` is simply zeroed. This prevents the per-second loop from iterating hundreds of times on a reconnecting peer.
|
||||
|
||||
`add()` ages the accumulator first, then adds the new sample, and returns `m_value / Window` — the normalized balance representing average load per second across the window. `value()` does the same without adding anything. Both methods demand a `time_point now` from the caller rather than reading a clock themselves; this makes the class testable and clock-agnostic.
|
||||
|
||||
## `DecayWindow<HalfLife, Clock>`
|
||||
|
||||
`DecayWindow` takes a different approach: it stores a `double` and applies the mathematically exact exponential half-life formula:
|
||||
|
||||
```cpp
|
||||
value_ *= std::pow(2.0, -elapsed / HalfLife);
|
||||
```
|
||||
|
||||
After exactly `HalfLife` seconds of inactivity, the accumulated value halves. After two half-lives it quarters, and so on. Unlike `DecayingSample`, which loops through whole seconds, `DecayWindow` casts the elapsed duration to `duration<double>`, giving it sub-second precision — appropriate when the caller's clock has higher resolution or when calls are frequent.
|
||||
|
||||
`InboundLedgers.cpp` uses this class as `DecayWindow<30, clock_type> fetchRate_` to measure the rate at which ledgers are being fetched from peers. Each fetch fires `fetchRate_.add(1, now)`. The `fetchRate()` accessor returns `60 * fetchRate_.value(now)`, converting the per-second average to a per-minute rate for reporting.
|
||||
|
||||
The `static_assert(HalfLife > 0)` guards against a zero divisor in `std::pow`, which would produce undefined floating-point behavior.
|
||||
|
||||
## Design Rationale: Two Classes Rather Than One
|
||||
|
||||
The two classes reflect different use cases that have incompatible requirements. `DecayingSample` works with integer `value_type` (derived from the clock's duration representation), which matters for the resource manager where charges are counted in discrete units and the result feeds integer comparison thresholds. Integer arithmetic also avoids floating-point instability in tight loops. `DecayWindow` accepts `double` inputs and uses `std::pow`, accepting the floating-point cost in exchange for smooth decay curves and sub-second accuracy — the right tradeoff when measuring continuous rates rather than discrete charges.
|
||||
|
||||
Neither class is thread-safe on its own; callers are responsible for synchronization. `InboundLedgersImp` wraps `fetchRate_` with `fetchRateMutex_`, and the resource `Entry` is similarly protected by the table's lock.
|
||||
109
include/xrpl/basics/Expected.h.ai.json
Normal file
109
include/xrpl/basics/Expected.h.ai.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 29,
|
||||
"name": "Impl"
|
||||
},
|
||||
{
|
||||
"lineno": 36,
|
||||
"name": "Impl"
|
||||
},
|
||||
{
|
||||
"lineno": 43,
|
||||
"name": "Impl"
|
||||
},
|
||||
{
|
||||
"lineno": 53,
|
||||
"name": "E"
|
||||
},
|
||||
{
|
||||
"lineno": 80,
|
||||
"name": "U"
|
||||
},
|
||||
{
|
||||
"lineno": 87,
|
||||
"name": "U"
|
||||
},
|
||||
{
|
||||
"lineno": 128,
|
||||
"name": "U"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 16,
|
||||
"name": "bad_expected_access"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 27,
|
||||
"name": "throw_policy"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"E const& e",
|
||||
"E&& e"
|
||||
],
|
||||
"lineno": 53,
|
||||
"name": "Unexpected"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"U&& r",
|
||||
"Unexpected<U> e"
|
||||
],
|
||||
"lineno": 77,
|
||||
"name": "Expected"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Expected()",
|
||||
"Unexpected<U> e"
|
||||
],
|
||||
"lineno": 120,
|
||||
"name": "Expected<void, E>"
|
||||
}
|
||||
],
|
||||
"description": "This file provides an approximation of std::expected (proposed for C++23) using boost::outcome_v2::result, including custom error handling and policies for expected/unexpected result types.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Expected.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 18,
|
||||
"name": "bad_expected_access"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Impl&& self"
|
||||
],
|
||||
"lineno": 29,
|
||||
"name": "wide_value_check"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Impl&& self"
|
||||
],
|
||||
"lineno": 36,
|
||||
"name": "wide_error_check"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Impl&& self"
|
||||
],
|
||||
"lineno": 43,
|
||||
"name": "wide_exception_check"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 7,
|
||||
"name": "xrpl"
|
||||
},
|
||||
{
|
||||
"lineno": 25,
|
||||
"name": "detail"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
include/xrpl/basics/Expected.h.ai.md
Normal file
41
include/xrpl/basics/Expected.h.ai.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# `include/xrpl/basics/Expected.h`
|
||||
|
||||
## Role and Motivation
|
||||
|
||||
This header provides `xrpl::Expected<T, E>`, a polyfill for the `std::expected<T, E>` type proposed for C++23 (P0323R10). At the time this code was written, `std::expected` was not yet available, so the implementation delegates all storage and state management to `boost::outcome_v2::result<T, E, Policy>` while exposing an API that closely mirrors the eventual standard — making a future migration straightforward.
|
||||
|
||||
`Expected<T, E>` represents a value that is *either* a success of type `T` or an error of type `E`. Unlike `std::optional`, which only signals absence, `Expected` carries diagnostic information about *why* a result is missing. Unlike exceptions, it forces callers to explicitly inspect the outcome. The `[[nodiscard]]` attribute on both the primary template and the `void` specialization guarantees at compile time that callers cannot silently drop a return value — a critical safety property in a financial ledger where ignored error returns could mean silent transaction corruption.
|
||||
|
||||
## Components
|
||||
|
||||
### `bad_expected_access`
|
||||
|
||||
A thin `std::runtime_error` subclass thrown whenever code tries to read the wrong half of an `Expected` — e.g., calling `value()` on an error-holding instance. It carries no additional data because the error value itself is available via `error()` and the point of failure is immediately clear from the stack trace. By inheriting from `std::runtime_error`, it integrates naturally with XRPL's existing exception hierarchy.
|
||||
|
||||
### `detail::throw_policy`
|
||||
|
||||
Boost.Outcome's policy mechanism controls what happens when the invariants of a `result` are violated. The default boost policy may assert or exhibit undefined behavior depending on build configuration; `throw_policy` replaces that with deterministic exception throwing. All three "wide" check entry points — `wide_value_check`, `wide_error_check`, and `wide_exception_check` — delegate to `Throw<bad_expected_access>()` (from `contract.h`) rather than a bare `throw`, so the violation is also logged before the exception propagates, consistent with XRPL's programming-by-contract philosophy.
|
||||
|
||||
### `Unexpected<E>`
|
||||
|
||||
A wrapper type that acts as an explicit tag for the error path. A function returning `Expected<T, E>` constructs the error branch by returning `Unexpected<E>(err)`, not a bare `E`. This prevents the implicit construction ambiguity that would arise if both `T` and `E` were, for example, `std::string`. The class provides all four value-category overloads of `value()` (lvalue/rvalue × const/non-const) for perfect forwarding into `Expected`'s constructor.
|
||||
|
||||
The deduction guide `Unexpected(E (&)[N]) -> Unexpected<E const*>` makes it ergonomic to pass string literals: `Unexpected("bad input")` deduces to `Unexpected<char const*>` rather than to a fixed-length array type, avoiding obscure template errors.
|
||||
|
||||
### `Expected<T, E>` (primary template)
|
||||
|
||||
Privately inherits from `boost::outcome_v2::result<T, E, detail::throw_policy>`. Private inheritance is intentional — it exposes only the `std::expected`-shaped API and hides the broader Outcome API (which includes channel-specific accessors and other facilities that would pollute the interface). The two constructors use `requires std::convertible_to` constraints so that implicit narrowing is rejected at compile time.
|
||||
|
||||
`operator bool`, `operator*`, and `operator->` map onto `has_value()` and `value()`, matching the pointer-like ergonomics of the standard proposal. Accessing `operator*` or `operator->` on an error-holding `Expected` triggers `throw_policy::wide_value_check`, which throws `bad_expected_access`. Similarly, calling `error()` on a value-holding instance triggers `wide_error_check`.
|
||||
|
||||
### `Expected<void, E>` (partial specialization)
|
||||
|
||||
Functions that either succeed (producing no value) or fail with a diagnostic use this specialization. Its default constructor calls `boost::outcome_v2::success()` to produce a successful instance — matching the proposed `std::expected<void, E>{}` default construction semantics. This is the pattern used in `STTx::checkSign()` and related signature-verification methods, which return `Expected<void, std::string>`: on success the caller simply checks `operator bool`; on failure the error string explains what went wrong.
|
||||
|
||||
## Usage Patterns in the Codebase
|
||||
|
||||
`tokens.h` defines a convenience alias `B58Result<T> = Expected<T, std::error_code>` for Base58Check encoding/decoding operations, where the error is a standard system error code. `base_uint.h` uses `Expected<decltype(data_), ParseResult>` for a `noexcept` hex-parsing path, capturing a per-character parse failure without throwing. `STTx.h` uses `Expected<void, std::string>` for all signature-check entry points — a natural fit because signature validation either passes silently or produces a human-readable error message.
|
||||
|
||||
## Design Trade-offs
|
||||
|
||||
Choosing `boost::outcome` over a hand-rolled type means the storage layout, move semantics, and triviality propagation are handled by a well-tested library, reducing the risk of subtle UB in low-level storage operations. The cost is a dependency on Boost and some mismatch between Outcome's three-state model (value / error / exception pointer) and `std::expected`'s two-state model; the `wide_exception_check` override in `throw_policy` handles the third state consistently by also throwing `bad_expected_access`, even though `Expected` itself never stores an exception pointer in practice. When C++23 `std::expected` becomes universally available, the migration path is clear: the public API is already a subset of the standard interface.
|
||||
58
include/xrpl/basics/FileUtilities.h.ai.json
Normal file
58
include/xrpl/basics/FileUtilities.h.ai.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 9,
|
||||
"name": "ec"
|
||||
},
|
||||
{
|
||||
"lineno": 10,
|
||||
"name": "sourcePath"
|
||||
},
|
||||
{
|
||||
"lineno": 11,
|
||||
"name": "maxSize"
|
||||
},
|
||||
{
|
||||
"lineno": 15,
|
||||
"name": "ec"
|
||||
},
|
||||
{
|
||||
"lineno": 16,
|
||||
"name": "destPath"
|
||||
},
|
||||
{
|
||||
"lineno": 17,
|
||||
"name": "contents"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "Provides utility functions for reading from and writing to files using Boost filesystem, with error handling and optional size limit.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/FileUtilities.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"ec",
|
||||
"sourcePath",
|
||||
"maxSize"
|
||||
],
|
||||
"lineno": 8,
|
||||
"name": "getFileContents"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"ec",
|
||||
"destPath",
|
||||
"contents"
|
||||
],
|
||||
"lineno": 14,
|
||||
"name": "writeFileContents"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 6,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
include/xrpl/basics/FileUtilities.h.ai.md
Normal file
33
include/xrpl/basics/FileUtilities.h.ai.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# `include/xrpl/basics/FileUtilities.h`
|
||||
|
||||
This header declares two thin file I/O utilities — `getFileContents` and `writeFileContents` — that form the XRPL codebase's standard interface for synchronous file access. The design problem they solve is not the I/O itself, but the error-handling contract: the broader rippled codebase avoids exceptions in many subsystems and instead relies on `boost::system::error_code` for structured, non-throwing error propagation. These two functions provide a consistent, exception-free surface for the handful of places in the node that must read or write files.
|
||||
|
||||
## Interface Design
|
||||
|
||||
Both functions follow the same convention: an `error_code&` output parameter is the first argument, populated on failure while the function returns an empty result (or returns nothing for the write case). This is the classic Boost.Asio-style error-out-parameter pattern, chosen over exception-throwing I/O because the callers — configuration loading, validator list file reads, test scaffolding — operate in contexts where an error is a recoverable condition requiring a structured diagnostic path rather than a stack unwind.
|
||||
|
||||
`getFileContents` takes a `boost::filesystem::path` and an optional `std::size_t` upper bound. The `std::optional<std::size_t> maxSize` parameter is the key safety valve: it allows callers to cap memory usage before any bytes are read into a `std::string`. When absent, the file is read in full. When present, the function checks the on-disk file size before opening the stream and returns `file_too_large` immediately if the limit is exceeded. This pre-check is cheap and prevents unbounded allocation when reading untrusted or potentially large files.
|
||||
|
||||
`writeFileContents` takes a `boost::filesystem::path` and the string to write. It opens with `std::ios::out | std::ios::trunc`, guaranteeing the destination file is replaced atomically from the content's perspective — no partial appends. The function does not check or create intermediate directories; the caller is responsible for ensuring the destination path exists.
|
||||
|
||||
## Implementation Notes (from the `.cpp`)
|
||||
|
||||
`getFileContents` calls `boost::filesystem::canonical()` before doing anything else. This resolves symlinks and relative components into an absolute, normalized path, ensuring the subsequent `file_size()` check and stream open operate on the same physical file. Calling `canonical()` with the `ec` overload also intercepts path resolution errors (non-existent file, permission denied) through the same error-code channel rather than a filesystem exception.
|
||||
|
||||
After path resolution, the implementation reads the file via a `std::istreambuf_iterator` range construction directly into a `std::string`. This is idiomatic C++ for slurping a whole file but has a subtle implication: for text-mode streams on some platforms, newline translation may occur. The stream is opened in `std::ios::in` (text mode), consistent with the intended use cases — TOML/JSON configuration and validator list JSON — where the content is human-readable text rather than binary data.
|
||||
|
||||
Error checking is done at three points: path resolution failure, pre-open size check, and post-read `fileStream.bad()`. The `bad()` check (not `fail()`) specifically catches I/O errors during reading, not logical stream state issues, which is the correct guard for a hardware or OS-level read failure mid-stream.
|
||||
|
||||
## Callers in Context
|
||||
|
||||
The three primary call sites reveal the intended use scope:
|
||||
|
||||
- `src/xrpld/core/detail/Config.cpp` uses `getFileContents` twice: once to load the main configuration file and once to load the validators file specified within that config. These are startup-time reads on the main thread, where a missing file is a fatal misconfiguration.
|
||||
- `src/xrpld/app/misc/detail/WorkFile.h` uses `getFileContents` with a hard cap of `megabytes(1)` to read validator list files fetched from the network. The 1 MB cap is a deliberate denial-of-service defense against a maliciously large or corrupted file consuming unbounded memory.
|
||||
- `src/xrpld/app/misc/detail/ValidatorList.cpp` uses `writeFileContents` to persist the current validator list as styled JSON after an update.
|
||||
|
||||
The `maxSize` parameter's real motivation is visible in the `WorkFile` usage: without it, a 4 GB file at a validator list URL would allocate 4 GB of heap before the caller could inspect the error. The pre-check using `file_size()` is a TOCTOU (time-of-check/time-of-use) race in theory, but in practice the files involved are either local config files or freshly downloaded files in a controlled temp location, making the race window negligible.
|
||||
|
||||
## Relationship to the `basics` Module
|
||||
|
||||
Within `include/xrpl/basics/`, this header occupies the narrowest role: it is a leaf utility with no dependencies on other XRPL types. It depends only on Boost.Filesystem and `<optional>`, making it safe to include anywhere in the stack without pulling in heavier XRPL headers. The `ByteUtilities.h` header (which provides `kilobytes()` and `megabytes()`) is the natural companion when callers need to express size limits in readable units, as the test suite and `WorkFile` both demonstrate.
|
||||
115
include/xrpl/basics/IntrusivePointer.h.ai.json
Normal file
115
include/xrpl/basics/IntrusivePointer.h.ai.json
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 61,
|
||||
"name": "T* p"
|
||||
},
|
||||
{
|
||||
"lineno": 61,
|
||||
"name": "TAdoptTag"
|
||||
},
|
||||
{
|
||||
"lineno": 63,
|
||||
"name": "SharedIntrusive const& rhs"
|
||||
},
|
||||
{
|
||||
"lineno": 67,
|
||||
"name": "SharedIntrusive<TT> const& rhs"
|
||||
},
|
||||
{
|
||||
"lineno": 70,
|
||||
"name": "SharedIntrusive&& rhs"
|
||||
},
|
||||
{
|
||||
"lineno": 74,
|
||||
"name": "SharedIntrusive<TT>&& rhs"
|
||||
},
|
||||
{
|
||||
"lineno": 196,
|
||||
"name": "TT"
|
||||
},
|
||||
{
|
||||
"lineno": 196,
|
||||
"name": "Args&&... args"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 11,
|
||||
"name": "StaticCastTagSharedIntrusive"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 19,
|
||||
"name": "DynamicCastTagSharedIntrusive"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 27,
|
||||
"name": "SharedIntrusiveAdoptIncrementStrongTag"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 34,
|
||||
"name": "SharedIntrusiveAdoptNoIncrementTag"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T* p, TAdoptTag",
|
||||
"SharedIntrusive const& rhs",
|
||||
"SharedIntrusive<TT> const& rhs",
|
||||
"SharedIntrusive&& rhs",
|
||||
"SharedIntrusive<TT>&& rhs",
|
||||
"StaticCastTagSharedIntrusive, SharedIntrusive<TT> const& rhs",
|
||||
"StaticCastTagSharedIntrusive, SharedIntrusive<TT>&& rhs",
|
||||
"DynamicCastTagSharedIntrusive, SharedIntrusive<TT> const& rhs",
|
||||
"DynamicCastTagSharedIntrusive, SharedIntrusive<TT>&& rhs"
|
||||
],
|
||||
"lineno": 54,
|
||||
"name": "SharedIntrusive"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"WeakIntrusive const& rhs",
|
||||
"WeakIntrusive&& rhs",
|
||||
"SharedIntrusive<T> const& rhs",
|
||||
"SharedIntrusive<T> const&& rhs"
|
||||
],
|
||||
"lineno": 151,
|
||||
"name": "WeakIntrusive"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SharedWeakUnion const& rhs",
|
||||
"SharedIntrusive<TT> const& rhs",
|
||||
"SharedWeakUnion&& rhs",
|
||||
"SharedIntrusive<TT>&& rhs"
|
||||
],
|
||||
"lineno": 210,
|
||||
"name": "SharedWeakUnion"
|
||||
}
|
||||
],
|
||||
"description": "This file implements shared and weak intrusive pointer classes (SharedIntrusive, WeakIntrusive, SharedWeakUnion) for reference-counted memory management, including utilities for casting and pointer creation, primarily for use in the XRPL codebase.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/IntrusivePointer.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"Args&&... args"
|
||||
],
|
||||
"lineno": 196,
|
||||
"name": "make_SharedIntrusive"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 7,
|
||||
"name": "xrpl"
|
||||
},
|
||||
{
|
||||
"lineno": 222,
|
||||
"name": "intr_ptr"
|
||||
}
|
||||
]
|
||||
}
|
||||
52
include/xrpl/basics/IntrusivePointer.h.ai.md
Normal file
52
include/xrpl/basics/IntrusivePointer.h.ai.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# `include/xrpl/basics/IntrusivePointer.h`
|
||||
|
||||
This header defines XRPL's custom intrusive smart pointer system: `SharedIntrusive<T>`, `WeakIntrusive<T>`, and `SharedWeakUnion<T>`. The system was designed specifically for `SHAMapInnerNode` — the inner nodes of the radix-16 Merkle trie at the heart of ledger state — but is general enough to serve other reference-counted types. The driver for building this rather than using `std::shared_ptr` is a lifecycle feature called the *partial destructor*, combined with a memory-efficient combined strong/weak pointer variant.
|
||||
|
||||
## Why Not `std::shared_ptr`?
|
||||
|
||||
The file's own comment names the key difference clearly. With `std::shared_ptr` created via `make_shared`, the control block (which contains both the strong and weak counts) lives alongside the object in a single allocation. That allocation is not reclaimed until both the strong *and* weak counts hit zero. So if something holds a `std::weak_ptr` to an inner node, the node's full allocation — including its 16 child pointers — stays live even after the last `shared_ptr` drops. For the SHAMap this is expensive: each inner node can hold up to 16 child `SharedIntrusive` pointers. The partial destructor mechanism exists specifically to release those children as soon as the strong count falls to zero, leaving only a shell waiting for the weak count to drain.
|
||||
|
||||
## Reference Count Layout
|
||||
|
||||
The actual counters live in `IntrusiveRefCounts` (`IntrusiveRefCounts.h`), which must be a base class of any type `T` used with these pointers. A single `std::atomic<uint32_t>` field encodes four things:
|
||||
|
||||
- **Bits 0–15**: strong count (up to 65535 owners)
|
||||
- **Bits 16–29**: weak count (14 bits, up to 16383 weak holders)
|
||||
- **Bit 30**: `partialDestroyStarted` flag
|
||||
- **Bit 31**: `partialDestroyFinished` flag
|
||||
|
||||
Packing counts and flags into one atomic integer means `releaseStrongRef()` can atomically decrement the count *and* set the `partialDestroyStarted` flag in a single CAS loop, avoiding a TOCTOU window where two threads could both decide to trigger partial destruction. The two flags are required to safely sequence concurrent partial- and full-destruction: the last weak pointer release spins on `atomic::wait()` if the partial destructor has started but not yet finished, preventing `delete` from racing with `partialDestructor()`.
|
||||
|
||||
## `SharedIntrusive<T>` — The Strong Pointer
|
||||
|
||||
`SharedIntrusive<T>` holds a raw `T* ptr_` whose lifetime is controlled by an intrusive strong count on `*ptr_`. Copy construction calls `ptr_->addStrongRef()`; move construction steals via `unsafeExchange(nullptr)` without touching the count. When the last strong holder releases (`unsafeReleaseAndStore(nullptr)` called from destructor or `reset()`), `releaseStrongRef()` returns one of three `ReleaseStrongRefAction` values:
|
||||
|
||||
- `noop` — other strong holders remain
|
||||
- `destroy` — both counts are zero; `delete prev`
|
||||
- `partialDestroy` — weak holders remain; call `prev->partialDestructor()` then `partialDestructorFinished(&prev)`
|
||||
|
||||
The call to `partialDestructorFinished` is the responsibility of the smart pointer class, not the pointee's `partialDestructor()`. This deliberate separation — noted in comments — forces every new `partialDestructor` implementation to explicitly arrange that call, making it harder to accidentally omit the step that wakes waiting threads.
|
||||
|
||||
The `unsafe*` private methods (`unsafeGetRawPtr`, `unsafeSetRawPtr`, `unsafeExchange`, `unsafeReleaseAndStore`) are named with the "unsafe" prefix not because they are dangerous in isolation, but as an architectural seam: the comment explicitly anticipates a future patch where `ptr_` might become `std::atomic<T*>`, and isolating all direct pointer access through these methods makes such a change localized.
|
||||
|
||||
## Adopt Tags and `make_SharedIntrusive`
|
||||
|
||||
Two tag types — `SharedIntrusiveAdoptIncrementStrongTag` and `SharedIntrusiveAdoptNoIncrementTag` — control whether adopting a raw pointer bumps the strong count. `make_SharedIntrusive<TT>()` allocates a new object with `new TT(...)` and wraps it with `NoIncrement`. This is correct because `IntrusiveRefCounts` initializes its atomic field to `strongDelta` (= 1), meaning the object is born with a strong count of one; incrementing again would be a double-count. The `static_assert` in `make_SharedIntrusive` verifies that the adopting constructor is `noexcept`, since a throw after the raw `new` but before the pointer is wrapped would leak the allocation.
|
||||
|
||||
## Cast Tags
|
||||
|
||||
`StaticCastTagSharedIntrusive` and `DynamicCastTagSharedIntrusive` are dispatch tags for cast-constructors, enabling the `intr_ptr::static_pointer_cast<T>()` and `intr_ptr::dynamic_pointer_cast<T>()` free functions. The move variant of the dynamic-cast constructor handles failure carefully: it uses `unsafeExchange` to steal the pointer from `rhs`, attempts `dynamic_cast`, and if it fails, exchanges the pointer back into `rhs` so ownership is not lost.
|
||||
|
||||
## `WeakIntrusive<T>` — The Weak Pointer
|
||||
|
||||
`WeakIntrusive<T>` mirrors the weak semantics of `std::weak_ptr`. Copy construction calls `ptr_->addWeakRef()`; the destructor calls `unsafeReleaseNoStore()` which invokes `releaseWeakRef()`. The interesting method is `lock()`: it calls `checkoutStrongRefFromWeak()`, a CAS loop that increments the strong count only if it is already non-zero. If the strong count has already hit zero the lock fails and an empty `SharedIntrusive` is returned. Note that copy assignment from a `WeakIntrusive` is deleted — the comment explains this was omitted to simplify the implementation since no current use case required it.
|
||||
|
||||
## `SharedWeakUnion<T>` — The Tagged Pointer
|
||||
|
||||
`SharedWeakUnion<T>` is the most architecturally unusual piece. It stores both the pointer value and a strong/weak discriminator inside a single `uintptr_t` field `tp_` by using pointer tagging: if the low bit is `1`, the pointer represents a weak reference; if it is `0`, a strong reference. This works because `alignof(T) >= 2` is statically asserted, guaranteeing the low bit of any valid `T*` is always zero.
|
||||
|
||||
The practical value is for tagged caches, where a cache slot should hold a strong pointer when the object is actively needed but can downgrade to a weak pointer to allow eviction without cache churn. `convertToStrong()` and `convertToWeak()` perform in-place promotion and demotion: `convertToStrong()` atomically promotes a weak checkout to a strong reference using `checkoutStrongRefFromWeak()` then releases the weak count; `convertToWeak()` uses the atomic `addWeakReleaseStrongRef()` operation to swap one strong count for one weak count in a single CAS loop, handling the `partialDestroy` case that arises if this was the very last strong pointer. The `lock()` method unifies weak and strong paths: if already strong, increment and return; if weak, attempt a checkout.
|
||||
|
||||
## `intr_ptr` Namespace
|
||||
|
||||
The nested `intr_ptr` namespace provides `std::shared_ptr`-style vocabulary aliases — `SharedPtr<T>`, `WeakPtr<T>`, `SharedWeakUnionPtr<T>`, `make_shared<T>()`, `static_pointer_cast<T>()`, `dynamic_pointer_cast<T>()` — used throughout the SHAMap subsystem. `SHAMapInnerNode` stores its 16 children as `intr_ptr::SharedPtr<SHAMapTreeNode>` and exposes `partialDestructor()` to reset them when the last strong holder drops.
|
||||
698
include/xrpl/basics/IntrusivePointer.ipp.ai.json
Normal file
698
include/xrpl/basics/IntrusivePointer.ipp.ai.json
Normal file
@@ -0,0 +1,698 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [],
|
||||
"code_paths": [
|
||||
{
|
||||
"call_chain": [
|
||||
"SharedIntrusive<T>::SharedIntrusive(T* p, TAdoptTag)",
|
||||
"if constexpr (std::is_same_v<TAdoptTag, SharedIntrusiveAdoptIncrementStrongTag>)",
|
||||
"if (p)",
|
||||
"p->addStrongRef()"
|
||||
],
|
||||
"entry_point": "SharedIntrusive<T>::SharedIntrusive(T* p, TAdoptTag)",
|
||||
"purpose": "Constructs a SharedIntrusive from a raw pointer, optionally incrementing the strong ref count if adopting.",
|
||||
"validation_points": [
|
||||
"if (p) // Validates pointer before addStrongRef"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"SharedIntrusive<T>::SharedIntrusive(SharedIntrusive const& rhs)",
|
||||
"rhs.unsafeGetRawPtr()",
|
||||
"if (p)",
|
||||
"p->addStrongRef()"
|
||||
],
|
||||
"entry_point": "SharedIntrusive<T>::SharedIntrusive(SharedIntrusive const& rhs)",
|
||||
"purpose": "Copy constructor, increments strong ref if pointer is not null.",
|
||||
"validation_points": [
|
||||
"if (p) // Validates pointer before addStrongRef"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"SharedIntrusive<T>::operator=(SharedIntrusive const& rhs)",
|
||||
"if (this == &rhs)",
|
||||
"rhs.unsafeGetRawPtr()",
|
||||
"if (p)",
|
||||
"p->addStrongRef()",
|
||||
"unsafeReleaseAndStore(p)"
|
||||
],
|
||||
"entry_point": "SharedIntrusive<T>::operator=(SharedIntrusive const& rhs)",
|
||||
"purpose": "Assignment operator, handles self-assignment, increments ref if needed, releases old pointer.",
|
||||
"validation_points": [
|
||||
"if (this == &rhs) // Self-assignment check",
|
||||
"if (p) // Validates pointer before addStrongRef"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"SharedIntrusive<T>::operator=(SharedIntrusive<TT> const& rhs)",
|
||||
"if constexpr (std::is_same_v<T, TT>)",
|
||||
"if (this == &rhs)",
|
||||
"rhs.unsafeGetRawPtr()",
|
||||
"if (p)",
|
||||
"p->addStrongRef()",
|
||||
"unsafeReleaseAndStore(p)"
|
||||
],
|
||||
"entry_point": "SharedIntrusive<T>::operator=(SharedIntrusive<TT> const& rhs)",
|
||||
"purpose": "Assignment from convertible type, handles self-assignment, increments ref if needed, releases old pointer.",
|
||||
"validation_points": [
|
||||
"if (this == &rhs) // Self-assignment check (if T == TT)",
|
||||
"if (p) // Validates pointer before addStrongRef"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"SharedIntrusive<T>::adopt(T* p)",
|
||||
"if constexpr (std::is_same_v<TAdoptTag, SharedIntrusiveAdoptIncrementStrongTag>)",
|
||||
"if (p)",
|
||||
"p->addStrongRef()",
|
||||
"unsafeReleaseAndStore(p)"
|
||||
],
|
||||
"entry_point": "SharedIntrusive<T>::adopt(T* p)",
|
||||
"purpose": "Adopts a raw pointer, optionally increments ref count, releases old pointer.",
|
||||
"validation_points": [
|
||||
"if (p) // Validates pointer before addStrongRef"
|
||||
]
|
||||
}
|
||||
],
|
||||
"data_flows": [
|
||||
{
|
||||
"field": "ptr_",
|
||||
"flow": [
|
||||
"T* p (input or from rhs)",
|
||||
"if (p) validation",
|
||||
"p->addStrongRef() (if validated)",
|
||||
"ptr_ = p"
|
||||
],
|
||||
"origin": "Constructor argument (T* p) or rhs.unsafeGetRawPtr()",
|
||||
"transformations": [
|
||||
"Pointer is checked for null",
|
||||
"Reference count incremented if not null",
|
||||
"Stored in ptr_"
|
||||
],
|
||||
"validated_at": "if (p) before addStrongRef"
|
||||
},
|
||||
{
|
||||
"field": "rhs (SharedIntrusive const& rhs or SharedIntrusive<TT> const& rhs)",
|
||||
"flow": [
|
||||
"rhs (input)",
|
||||
"if (this == &rhs) validation (self-assignment)",
|
||||
"rhs.unsafeGetRawPtr()",
|
||||
"if (p) validation",
|
||||
"p->addStrongRef()",
|
||||
"unsafeReleaseAndStore(p)"
|
||||
],
|
||||
"origin": "Function argument",
|
||||
"transformations": [
|
||||
"Self-assignment check",
|
||||
"Pointer extracted",
|
||||
"Reference count incremented if not null",
|
||||
"Old pointer released and replaced"
|
||||
],
|
||||
"validated_at": "if (this == &rhs), if (p)"
|
||||
},
|
||||
{
|
||||
"field": "T* p (adopt)",
|
||||
"flow": [
|
||||
"T* p (input)",
|
||||
"if (p) validation",
|
||||
"p->addStrongRef() (if validated)",
|
||||
"unsafeReleaseAndStore(p)"
|
||||
],
|
||||
"origin": "adopt(T* p) argument",
|
||||
"transformations": [
|
||||
"Pointer is checked for null",
|
||||
"Reference count incremented if not null",
|
||||
"Old pointer released and replaced"
|
||||
],
|
||||
"validated_at": "if (p)"
|
||||
}
|
||||
],
|
||||
"description": "Implements the definitions for intrusive smart pointer types (SharedIntrusive, WeakIntrusive, SharedWeakUnion) used for reference-counted memory management in the xrpl namespace.",
|
||||
"false_positive_patterns": [
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"input_validation"
|
||||
],
|
||||
"confidence": 0.9,
|
||||
"detection_keywords": [
|
||||
"template type parameters (via static_assert and if constexpr)",
|
||||
"validation",
|
||||
"missing",
|
||||
"check"
|
||||
],
|
||||
"evidence": "Field template type parameters (via static_assert and if constexpr) validated by C++ type system, manual null checks, static_assert",
|
||||
"issue_pattern": "Missing validation for template type parameters (via static_assert and if constexpr)",
|
||||
"why_false_positive": "C++ type system, manual null checks, static_assert validates template type parameters (via static_assert and if constexpr) automatically"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"input_validation"
|
||||
],
|
||||
"confidence": 0.9,
|
||||
"detection_keywords": [
|
||||
"pointer nullness (via if (p))",
|
||||
"validation",
|
||||
"missing",
|
||||
"check"
|
||||
],
|
||||
"evidence": "Field pointer nullness (via if (p)) validated by C++ type system, manual null checks, static_assert",
|
||||
"issue_pattern": "Missing validation for pointer nullness (via if (p))",
|
||||
"why_false_positive": "C++ type system, manual null checks, static_assert validates pointer nullness (via if (p)) automatically"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"null_empty"
|
||||
],
|
||||
"confidence": 1.0,
|
||||
"detection_keywords": [
|
||||
"p (pointer to T)",
|
||||
"empty",
|
||||
"string",
|
||||
"validation"
|
||||
],
|
||||
"evidence": "if (p) at SharedIntrusive<T>::SharedIntrusive(T* p, TAdoptTag) noexcept",
|
||||
"issue_pattern": "Missing empty string validation for p (pointer to T)",
|
||||
"why_false_positive": "if (p) validates p (pointer to T) for empty strings"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"null_empty"
|
||||
],
|
||||
"confidence": 1.0,
|
||||
"detection_keywords": [
|
||||
"rhs (SharedIntrusive const& rhs)",
|
||||
"empty",
|
||||
"string",
|
||||
"validation"
|
||||
],
|
||||
"evidence": "if (p) at SharedIntrusive<T>::SharedIntrusive(SharedIntrusive const& rhs)",
|
||||
"issue_pattern": "Missing empty string validation for rhs (SharedIntrusive const& rhs)",
|
||||
"why_false_positive": "if (p) validates rhs (SharedIntrusive const& rhs) for empty strings"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"null_empty"
|
||||
],
|
||||
"confidence": 1.0,
|
||||
"detection_keywords": [
|
||||
"rhs (SharedIntrusive<TT> const& rhs)",
|
||||
"empty",
|
||||
"string",
|
||||
"validation"
|
||||
],
|
||||
"evidence": "if (p) at SharedIntrusive<T>::SharedIntrusive(SharedIntrusive<TT> const& rhs)",
|
||||
"issue_pattern": "Missing empty string validation for rhs (SharedIntrusive<TT> const& rhs)",
|
||||
"why_false_positive": "if (p) validates rhs (SharedIntrusive<TT> const& rhs) for empty strings"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"null_empty"
|
||||
],
|
||||
"confidence": 1.0,
|
||||
"detection_keywords": [
|
||||
"this and rhs (self-assignment)",
|
||||
"empty",
|
||||
"string",
|
||||
"validation"
|
||||
],
|
||||
"evidence": "if (this == &rhs) at SharedIntrusive<T>::operator=(SharedIntrusive const& rhs)",
|
||||
"issue_pattern": "Missing empty string validation for this and rhs (self-assignment)",
|
||||
"why_false_positive": "if (this == &rhs) validates this and rhs (self-assignment) for empty strings"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"null_empty"
|
||||
],
|
||||
"confidence": 1.0,
|
||||
"detection_keywords": [
|
||||
"this and rhs (self-assignment)",
|
||||
"empty",
|
||||
"string",
|
||||
"validation"
|
||||
],
|
||||
"evidence": "if (this == &rhs) at SharedIntrusive<T>::operator=(SharedIntrusive<TT> const& rhs)",
|
||||
"issue_pattern": "Missing empty string validation for this and rhs (self-assignment)",
|
||||
"why_false_positive": "if (this == &rhs) validates this and rhs (self-assignment) for empty strings"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"null_empty"
|
||||
],
|
||||
"confidence": 1.0,
|
||||
"detection_keywords": [
|
||||
"this and rhs (self-assignment)",
|
||||
"empty",
|
||||
"string",
|
||||
"validation"
|
||||
],
|
||||
"evidence": "if (this == &rhs) at SharedIntrusive<T>::operator=(SharedIntrusive&& rhs)",
|
||||
"issue_pattern": "Missing empty string validation for this and rhs (self-assignment)",
|
||||
"why_false_positive": "if (this == &rhs) validates this and rhs (self-assignment) for empty strings"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"null_empty"
|
||||
],
|
||||
"confidence": 1.0,
|
||||
"detection_keywords": [
|
||||
"TT (template type parameter)",
|
||||
"empty",
|
||||
"string",
|
||||
"validation"
|
||||
],
|
||||
"evidence": "static_assert(!std::is_same_v<T, TT>, ...) at SharedIntrusive<T>::operator=(SharedIntrusive<TT>&& rhs)",
|
||||
"issue_pattern": "Missing empty string validation for TT (template type parameter)",
|
||||
"why_false_positive": "static_assert(!std::is_same_v<T, TT>, ...) validates TT (template type parameter) for empty strings"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"type_safety"
|
||||
],
|
||||
"confidence": 0.85,
|
||||
"detection_keywords": [
|
||||
"TT (template type parameter)",
|
||||
"type",
|
||||
"validation",
|
||||
"check"
|
||||
],
|
||||
"evidence": "static_assert(!std::is_same_v<T, TT>, ...) at SharedIntrusive<T>::operator=(SharedIntrusive<TT>&& rhs)",
|
||||
"issue_pattern": "Missing type validation for TT (template type parameter)",
|
||||
"why_false_positive": "static_assert(!std::is_same_v<T, TT>, ...) validates TT (template type parameter) type"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"null_empty"
|
||||
],
|
||||
"confidence": 1.0,
|
||||
"detection_keywords": [
|
||||
"TAdoptTag (template type parameter)",
|
||||
"empty",
|
||||
"string",
|
||||
"validation"
|
||||
],
|
||||
"evidence": "if constexpr (std::is_same_v<TAdoptTag, SharedIntrusiveAdoptIncrementStrongTag>) at SharedIntrusive<T>::SharedIntrusive(T* p, TAdoptTag) noexcept and adopt(T* p)",
|
||||
"issue_pattern": "Missing empty string validation for TAdoptTag (template type parameter)",
|
||||
"why_false_positive": "if constexpr (std::is_same_v<TAdoptTag, SharedIntrusiveAdoptIncrementStrongTag>) validates TAdoptTag (template type parameter) for empty strings"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"type_safety"
|
||||
],
|
||||
"confidence": 0.85,
|
||||
"detection_keywords": [
|
||||
"TAdoptTag (template type parameter)",
|
||||
"type",
|
||||
"validation",
|
||||
"check"
|
||||
],
|
||||
"evidence": "if constexpr (std::is_same_v<TAdoptTag, SharedIntrusiveAdoptIncrementStrongTag>) at SharedIntrusive<T>::SharedIntrusive(T* p, TAdoptTag) noexcept and adopt(T* p)",
|
||||
"issue_pattern": "Missing type validation for TAdoptTag (template type parameter)",
|
||||
"why_false_positive": "if constexpr (std::is_same_v<TAdoptTag, SharedIntrusiveAdoptIncrementStrongTag>) validates TAdoptTag (template type parameter) type"
|
||||
}
|
||||
],
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/IntrusivePointer.ipp",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"SharedIntrusive const& rhs"
|
||||
],
|
||||
"lineno": 61,
|
||||
"name": "SharedIntrusive<T>::operator="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SharedIntrusive<TT> const& rhs"
|
||||
],
|
||||
"lineno": 80,
|
||||
"name": "SharedIntrusive<T>::operator="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SharedIntrusive&& rhs"
|
||||
],
|
||||
"lineno": 99,
|
||||
"name": "SharedIntrusive<T>::operator="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SharedIntrusive<TT>&& rhs"
|
||||
],
|
||||
"lineno": 110,
|
||||
"name": "SharedIntrusive<T>::operator="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::nullptr_t"
|
||||
],
|
||||
"lineno": 120,
|
||||
"name": "SharedIntrusive<T>::operator!="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::nullptr_t"
|
||||
],
|
||||
"lineno": 126,
|
||||
"name": "SharedIntrusive<T>::operator=="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T* p"
|
||||
],
|
||||
"lineno": 132,
|
||||
"name": "SharedIntrusive<T>::adopt"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 142,
|
||||
"name": "SharedIntrusive<T>::~SharedIntrusive"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 170,
|
||||
"name": "SharedIntrusive<T>::operator*"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 176,
|
||||
"name": "SharedIntrusive<T>::operator->"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 182,
|
||||
"name": "SharedIntrusive<T>::operator bool"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 188,
|
||||
"name": "SharedIntrusive<T>::reset"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 193,
|
||||
"name": "SharedIntrusive<T>::get"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 198,
|
||||
"name": "SharedIntrusive<T>::use_count"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 206,
|
||||
"name": "SharedIntrusive<T>::unsafeGetRawPtr"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T* p"
|
||||
],
|
||||
"lineno": 211,
|
||||
"name": "SharedIntrusive<T>::unsafeSetRawPtr"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T* p"
|
||||
],
|
||||
"lineno": 216,
|
||||
"name": "SharedIntrusive<T>::unsafeExchange"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T* next"
|
||||
],
|
||||
"lineno": 221,
|
||||
"name": "SharedIntrusive<T>::unsafeReleaseAndStore"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SharedIntrusive<TT> const& rhs"
|
||||
],
|
||||
"lineno": 265,
|
||||
"name": "WeakIntrusive<T>::operator="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T* ptr"
|
||||
],
|
||||
"lineno": 274,
|
||||
"name": "WeakIntrusive<T>::adopt"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 280,
|
||||
"name": "WeakIntrusive<T>::~WeakIntrusive"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 285,
|
||||
"name": "WeakIntrusive<T>::lock"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 294,
|
||||
"name": "WeakIntrusive<T>::expired"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 299,
|
||||
"name": "WeakIntrusive<T>::reset"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 304,
|
||||
"name": "WeakIntrusive<T>::unsafeReleaseNoStore"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SharedWeakUnion const& rhs"
|
||||
],
|
||||
"lineno": 353,
|
||||
"name": "SharedWeakUnion<T>::operator="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SharedIntrusive<TT> const& rhs"
|
||||
],
|
||||
"lineno": 380,
|
||||
"name": "SharedWeakUnion<T>::operator="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SharedIntrusive<TT>&& rhs"
|
||||
],
|
||||
"lineno": 391,
|
||||
"name": "SharedWeakUnion<T>::operator="
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 399,
|
||||
"name": "SharedWeakUnion<T>::~SharedWeakUnion"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 404,
|
||||
"name": "SharedWeakUnion<T>::getStrong"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 415,
|
||||
"name": "SharedWeakUnion<T>::operator bool"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 420,
|
||||
"name": "SharedWeakUnion<T>::reset"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 425,
|
||||
"name": "SharedWeakUnion<T>::get"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 430,
|
||||
"name": "SharedWeakUnion<T>::use_count"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 437,
|
||||
"name": "SharedWeakUnion<T>::expired"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 442,
|
||||
"name": "SharedWeakUnion<T>::lock"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 463,
|
||||
"name": "SharedWeakUnion<T>::isStrong"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 468,
|
||||
"name": "SharedWeakUnion<T>::isWeak"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 473,
|
||||
"name": "SharedWeakUnion<T>::convertToStrong"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 491,
|
||||
"name": "SharedWeakUnion<T>::convertToWeak"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 517,
|
||||
"name": "SharedWeakUnion<T>::unsafeGetRawPtr"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T* p",
|
||||
"RefStrength rs"
|
||||
],
|
||||
"lineno": 522,
|
||||
"name": "SharedWeakUnion<T>::unsafeSetRawPtr"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::nullptr_t"
|
||||
],
|
||||
"lineno": 528,
|
||||
"name": "SharedWeakUnion<T>::unsafeSetRawPtr"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 532,
|
||||
"name": "SharedWeakUnion<T>::unsafeReleaseNoStore"
|
||||
}
|
||||
],
|
||||
"language": "cpp",
|
||||
"language_patterns": {
|
||||
"exception_patterns": [],
|
||||
"namespace_accessors": [],
|
||||
"raii_usage": [],
|
||||
"smart_pointers": [],
|
||||
"template_validation": []
|
||||
},
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 6,
|
||||
"name": "xrpl"
|
||||
}
|
||||
],
|
||||
"test_coverage_notes": "The code is a low-level utility for intrusive reference counting. Typical tests would be in files like IntrusivePointer_test.cpp or SharedIntrusive_test.cpp, likely under the 'test' or 'unittest' directories. Tests should cover: construction from raw pointer (null and non-null), copy/move construction, assignment (including self-assignment), adopt(), and destruction. Gaps may exist in testing edge cases such as self-assignment, null pointer handling, and cross-type assignment. No direct evidence of test files is present in this snippet, so coverage should be verified in the codebase.",
|
||||
"validation_architecture": {
|
||||
"auto_validated_fields": [
|
||||
"template type parameters (via static_assert and if constexpr)",
|
||||
"pointer nullness (via if (p))"
|
||||
],
|
||||
"framework": "C++ type system, manual null checks, static_assert",
|
||||
"validation_layer": "business_logic"
|
||||
},
|
||||
"validations": [
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"error_thrown": "none (conditional logic only)",
|
||||
"field": "p (pointer to T)",
|
||||
"location": "SharedIntrusive<T>::SharedIntrusive(T* p, TAdoptTag) noexcept",
|
||||
"validated_by": "if (p)",
|
||||
"validates": [
|
||||
"Checks if pointer p is not null before calling p->addStrongRef()"
|
||||
],
|
||||
"validation_type": "null check"
|
||||
},
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"error_thrown": "none (conditional logic only)",
|
||||
"field": "rhs (SharedIntrusive const& rhs)",
|
||||
"location": "SharedIntrusive<T>::SharedIntrusive(SharedIntrusive const& rhs)",
|
||||
"validated_by": "if (p)",
|
||||
"validates": [
|
||||
"Checks if pointer p (from rhs.unsafeGetRawPtr()) is not null before calling p->addStrongRef()"
|
||||
],
|
||||
"validation_type": "null check"
|
||||
},
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"error_thrown": "none (conditional logic only)",
|
||||
"field": "rhs (SharedIntrusive<TT> const& rhs)",
|
||||
"location": "SharedIntrusive<T>::SharedIntrusive(SharedIntrusive<TT> const& rhs)",
|
||||
"validated_by": "if (p)",
|
||||
"validates": [
|
||||
"Checks if pointer p (from rhs.unsafeGetRawPtr()) is not null before calling p->addStrongRef()"
|
||||
],
|
||||
"validation_type": "null check"
|
||||
},
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"error_thrown": "none (early return)",
|
||||
"field": "this and rhs (self-assignment)",
|
||||
"location": "SharedIntrusive<T>::operator=(SharedIntrusive const& rhs)",
|
||||
"validated_by": "if (this == &rhs)",
|
||||
"validates": [
|
||||
"Checks for self-assignment to avoid unnecessary operations"
|
||||
],
|
||||
"validation_type": "business_logic"
|
||||
},
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"error_thrown": "none (early return)",
|
||||
"field": "this and rhs (self-assignment)",
|
||||
"location": "SharedIntrusive<T>::operator=(SharedIntrusive<TT> const& rhs)",
|
||||
"validated_by": "if (this == &rhs)",
|
||||
"validates": [
|
||||
"Checks for self-assignment to avoid unnecessary operations (only if T == TT)"
|
||||
],
|
||||
"validation_type": "business_logic"
|
||||
},
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"error_thrown": "none (early return)",
|
||||
"field": "this and rhs (self-assignment)",
|
||||
"location": "SharedIntrusive<T>::operator=(SharedIntrusive&& rhs)",
|
||||
"validated_by": "if (this == &rhs)",
|
||||
"validates": [
|
||||
"Checks for self-assignment to avoid unnecessary operations"
|
||||
],
|
||||
"validation_type": "business_logic"
|
||||
},
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"error_thrown": "static_assert failure (compile-time error)",
|
||||
"field": "TT (template type parameter)",
|
||||
"location": "SharedIntrusive<T>::operator=(SharedIntrusive<TT>&& rhs)",
|
||||
"validated_by": "static_assert(!std::is_same_v<T, TT>, ...)",
|
||||
"validates": [
|
||||
"Ensures that this overload is not instantiated for T == TT"
|
||||
],
|
||||
"validation_type": "type"
|
||||
},
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"error_thrown": "none (compile-time conditional)",
|
||||
"field": "TAdoptTag (template type parameter)",
|
||||
"location": "SharedIntrusive<T>::SharedIntrusive(T* p, TAdoptTag) noexcept and adopt(T* p)",
|
||||
"validated_by": "if constexpr (std::is_same_v<TAdoptTag, SharedIntrusiveAdoptIncrementStrongTag>)",
|
||||
"validates": [
|
||||
"Ensures that addStrongRef() is only called if TAdoptTag is SharedIntrusiveAdoptIncrementStrongTag"
|
||||
],
|
||||
"validation_type": "type"
|
||||
}
|
||||
]
|
||||
}
|
||||
64
include/xrpl/basics/IntrusivePointer.ipp.ai.md
Normal file
64
include/xrpl/basics/IntrusivePointer.ipp.ai.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# `IntrusivePointer.ipp` — Intrusive Smart Pointer Method Definitions
|
||||
|
||||
This file provides the out-of-line template method bodies for three intrusive smart pointer classes declared in `IntrusivePointer.h`: `SharedIntrusive<T>`, `WeakIntrusive<T>`, and `SharedWeakUnion<T>`. As is conventional for C++ template implementations that must be visible to all translation units, the definitions live in a `.ipp` file that `IntrusivePointer.h` includes, rather than in a `.cpp` file.
|
||||
|
||||
## Why Not `std::shared_ptr`?
|
||||
|
||||
The comment on `SharedIntrusive` in the header explains the core motivation: the XRPL codebase needs a smart pointer that can release an object's *data* when the last strong reference drops, while deferring the *memory* release until the last weak reference also drops. `std::shared_ptr` guarantees that the destructor runs at strong-count zero, but an implementation using `make_shared` keeps the entire memory block alive until weak-count zero. More importantly, `std::shared_ptr` provides no hook between "last strong gone" and "last weak gone."
|
||||
|
||||
The intrusive design solves this by embedding reference counts directly in the pointee via `IntrusiveRefCounts` (a base struct the controlled type must inherit). When the strong count reaches zero, the pointer machinery calls a user-defined `partialDestructor()` — which can, for instance, reset `SHAMapInnerNode`'s child-pointer array to free the expensive working data — while the object shell continues to exist as long as any weak pointer holds on. Only when the weak count also reaches zero does `delete` run.
|
||||
|
||||
## `SharedIntrusive<T>` — The Strong Pointer
|
||||
|
||||
Construction and assignment follow a consistent pattern: acquire the new ref before releasing the old. The copy constructor uses a lambda initializer to atomically call `addStrongRef()` before storing the pointer in `ptr_`, ensuring the count is correct even if the lambda result is used to initialize a field directly:
|
||||
|
||||
```cpp
|
||||
ptr_{[&] {
|
||||
auto p = rhs.unsafeGetRawPtr();
|
||||
if (p) p->addStrongRef();
|
||||
return p;
|
||||
}()}
|
||||
```
|
||||
|
||||
Move construction is cheaper: it calls `unsafeExchange(nullptr)` on the source to steal the pointer without touching ref counts. The `static_assert` in the heterogeneous move-assignment operator enforces at compile time that the same-type case is handled by the homogeneous overload, preventing this overload from being instantiated for `T == TT`.
|
||||
|
||||
### The `TAdoptTag` Pattern
|
||||
|
||||
The `adopt(T* p)` method and the raw-pointer constructor both take a `TAdoptTag` template parameter constrained by the `CAdoptTag` concept. Two tags exist: `SharedIntrusiveAdoptIncrementStrongTag` increments the strong count (for absorbing a raw pointer that hasn't had its count bumped yet), and `SharedIntrusiveAdoptNoIncrementTag` adopts the pointer without incrementing. `make_SharedIntrusive` allocates via `new T` — `IntrusiveRefCounts` initializes the strong count to 1 — and then adopts with `SharedIntrusiveAdoptNoIncrementTag` to avoid a double-count. The `noexcept` guarantee on that constructor path is enforced with a `static_assert` inside `make_SharedIntrusive` to prevent memory leaks if construction were to throw after allocation.
|
||||
|
||||
### `unsafeReleaseAndStore` — The Core Destruction Path
|
||||
|
||||
Every operation that replaces the stored pointer funnels through `unsafeReleaseAndStore(T* next)`. It atomically swaps the new pointer in via `std::exchange`, then calls `releaseStrongRef()` on the evicted pointer. The return value is a `ReleaseStrongRefAction` enum with three values:
|
||||
|
||||
- `noop` — other strong pointers remain; do nothing.
|
||||
- `destroy` — no strong or weak pointers remain; call `delete`.
|
||||
- `partialDestroy` — the weak count is non-zero; call `partialDestructor()` then `partialDestructorFinished()`.
|
||||
|
||||
The `partialDestructorFinished` template friend function sets the `partialDestroyFinishedMask` bit atomically on `IntrusiveRefCounts::refCounts`, and if the weak count has already reached zero it calls `notify_one()` to wake any thread that is waiting in `releaseWeakRef()` for the partial destructor to complete before running the full destructor.
|
||||
|
||||
### Cast Constructors
|
||||
|
||||
`SharedIntrusive` supports both `static_cast` and `dynamic_cast` construction from a `SharedIntrusive<TT>`. For the move variant of `dynamic_cast`, there is a subtle correctness invariant: if `dynamic_cast<T*>` returns null (the cast fails), the source pointer is restored via `rhs.unsafeExchange(toSet)` to prevent the controlled object from leaking. A code comment notes that the `unsafeExchange` structure is also kept in anticipation of a future atomic pointer mode.
|
||||
|
||||
## `WeakIntrusive<T>` — The Weak Pointer
|
||||
|
||||
`WeakIntrusive` manages a non-owning reference via `addWeakRef()` and `releaseWeakRef()`. Two deliberate omissions in the interface are worth noting:
|
||||
|
||||
- Copy assignment from another `WeakIntrusive` is `delete`d. The header comment explains this is because there are currently no use cases, and omitting it simplifies implementation. It can be reintroduced if needed.
|
||||
- There is no move constructor from `SharedIntrusive<T>&&`. Moving a strong pointer into a weak pointer would require decrementing the strong count and adding a weak count, making it *more* expensive than copying the raw pointer and adding a weak ref. The deleted overload prevents this surprising hidden cost.
|
||||
|
||||
`lock()` calls `checkoutStrongRefFromWeak()` on the raw pointer, which uses a CAS loop to atomically increment the strong count only if it is currently non-zero. On success, the new `SharedIntrusive` is constructed with `SharedIntrusiveAdoptNoIncrementTag` — the checkout already performed the increment, so a second increment must not occur.
|
||||
|
||||
## `SharedWeakUnion<T>` — The Tagged-Pointer Union
|
||||
|
||||
`SharedWeakUnion` packs both a strong and weak reference into the space of a single pointer word. It stores the pointer as a `std::uintptr_t` called `tp_`, uses the low bit as a tag (1 = weak, 0 = strong), and recovers the raw pointer by masking with `ptrMask = ~1`. A `static_assert` on `alignof(T) >= 2` enforces that the actual pointer will never set the low bit, keeping the encoding sound.
|
||||
|
||||
`unsafeGetRawPtr()` applies the mask; `unsafeSetRawPtr(T*, RefStrength)` stores the pointer and conditionally ORs in the tag bit. `isStrong()` / `isWeak()` read the tag bit directly.
|
||||
|
||||
`convertToStrong()` and `convertToWeak()` allow in-place reference strength switching. `convertToWeak()` uses `addWeakReleaseStrongRef()` — an atomic operation on `IntrusiveRefCounts` that adds a weak delta and subtracts a strong delta in one CAS loop — to avoid a window where the strong count is zero but the weak count hasn't been incremented yet. If the result is `partialDestroy`, `convertToWeak` handles the two-phase partial destruction, including the `partialDestructorFinished` call that clears the pointer variable.
|
||||
|
||||
`get()` returns the raw pointer only if the union holds a strong reference; calling `get()` on a weak-tagged union returns null. `lock()` unifies both cases: if already strong, it bumps the strong count and returns; if weak, it attempts `checkoutStrongRefFromWeak()` and adopts without increment on success.
|
||||
|
||||
## Naming Convention for Primitives
|
||||
|
||||
All methods prefixed with `unsafe` are private and skip reference counting entirely. They manipulate the raw pointer field directly. This naming convention serves two purposes: it makes the separation between raw pointer mechanics and safe counted semantics immediately visible during code review, and the header comments note that these wrappers exist in anticipation of a future patch to support atomic pointer storage (which would require replacing `std::exchange` with `std::atomic::exchange` inside these one-line helpers).
|
||||
118
include/xrpl/basics/IntrusiveRefCounts.h.ai.json
Normal file
118
include/xrpl/basics/IntrusiveRefCounts.h.ai.json
Normal file
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 272,
|
||||
"name": "v"
|
||||
},
|
||||
{
|
||||
"lineno": 282,
|
||||
"name": "s"
|
||||
},
|
||||
{
|
||||
"lineno": 282,
|
||||
"name": "w"
|
||||
},
|
||||
{
|
||||
"lineno": 299,
|
||||
"name": "o"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 38,
|
||||
"name": "IntrusiveRefCounts"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"FieldType v",
|
||||
"CountType s, CountType w"
|
||||
],
|
||||
"lineno": 101,
|
||||
"name": "IntrusiveRefCounts::RefCountPair"
|
||||
}
|
||||
],
|
||||
"description": "Implements atomic reference counting for intrusive smart pointers, including strong and weak reference management, partial destruction, and thread-safe operations for use in the XRPL codebase.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/IntrusiveRefCounts.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 120,
|
||||
"name": "IntrusiveRefCounts::addStrongRef"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 125,
|
||||
"name": "IntrusiveRefCounts::addWeakRef"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 130,
|
||||
"name": "IntrusiveRefCounts::releaseStrongRef"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 170,
|
||||
"name": "IntrusiveRefCounts::addWeakReleaseStrongRef"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 210,
|
||||
"name": "IntrusiveRefCounts::releaseWeakRef"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 235,
|
||||
"name": "IntrusiveRefCounts::checkoutStrongRefFromWeak"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 248,
|
||||
"name": "IntrusiveRefCounts::expired"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 253,
|
||||
"name": "IntrusiveRefCounts::use_count"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 258,
|
||||
"name": "IntrusiveRefCounts::~IntrusiveRefCounts"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"IntrusiveRefCounts::FieldType v"
|
||||
],
|
||||
"lineno": 271,
|
||||
"name": "IntrusiveRefCounts::RefCountPair::RefCountPair"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"IntrusiveRefCounts::CountType s",
|
||||
"IntrusiveRefCounts::CountType w"
|
||||
],
|
||||
"lineno": 281,
|
||||
"name": "IntrusiveRefCounts::RefCountPair::RefCountPair"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 289,
|
||||
"name": "IntrusiveRefCounts::RefCountPair::combinedValue"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T** o"
|
||||
],
|
||||
"lineno": 299,
|
||||
"name": "partialDestructorFinished"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 6,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
56
include/xrpl/basics/IntrusiveRefCounts.h.ai.md
Normal file
56
include/xrpl/basics/IntrusiveRefCounts.h.ai.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# `IntrusiveRefCounts.h` — Atomic Reference Counting for Intrusive Smart Pointers
|
||||
|
||||
## Role in the System
|
||||
|
||||
`IntrusiveRefCounts` is the reference-counting backbone for XRPL's custom intrusive smart pointer family: `SharedIntrusive<T>`, `WeakIntrusive<T>`, and `SharedWeakUnion<T>`, all defined in `IntrusivePointer.h`. A class participates in this system simply by inheriting from `IntrusiveRefCounts`; the notable consumer is `SHAMapInnerNode`, whose billions-of-ops-per-second traversal patterns make every byte and every allocation matter.
|
||||
|
||||
The design answers a specific criticism of `std::shared_ptr`: with `make_shared`, the control block and the object are co-allocated, which is efficient, but the memory cannot be reclaimed until the weak count also hits zero. XRPL's intrusive design embeds the counts directly in the object, saves the separate control-block allocation, and — crucially — introduces a *partial destruction* protocol that lets the object shed its expensive payload (e.g., child node pointers) the moment the strong count reaches zero, even while weak pointers keep the shell alive.
|
||||
|
||||
## The Packed Atomic Field
|
||||
|
||||
All state lives in a single `mutable std::atomic<uint32_t> refCounts`. The 32 bits are divided as follows:
|
||||
|
||||
| Bits | Field |
|
||||
|------|-------|
|
||||
| 0–15 | Strong count (16 bits) |
|
||||
| 16–29 | Weak count (14 bits) |
|
||||
| 30 | `partialDestroyStartedBit` |
|
||||
| 31 | `partialDestroyFinishedBit` |
|
||||
|
||||
Packing everything into one atomic word means that decrementing a count *and* setting a flag can be done as a single compare-and-swap, eliminating any window between the two operations that a second atomic would expose. The helper struct `RefCountPair` wraps the unpack/repack logic: its constructor extracts each field by masking and shifting, and `combinedValue()` reassembles them.
|
||||
|
||||
## Destruction Lifecycle and the Partial Destructor
|
||||
|
||||
When `releaseStrongRef()` finds that the strong count is dropping to exactly one (i.e., to zero), it branches on whether any weak references exist:
|
||||
|
||||
- **No weak refs**: returns `ReleaseStrongRefAction::destroy`. The caller (in `IntrusivePointer.ipp`) runs `delete ptr`, calling the full destructor.
|
||||
- **Weak refs present**: atomically sets `partialDestroyStartedBit` *in the same CAS that decrements the count*, then returns `partialDestroy`. The caller invokes `ptr->partialDestructor()`, then calls the free function `partialDestructorFinished(&ptr)`.
|
||||
|
||||
This CAS loop in `releaseStrongRef()` almost always executes once; looping is necessary only if another thread modifies `refCounts` between the `load` and `compare_exchange_weak`.
|
||||
|
||||
`partialDestructorFinished()` is a `friend` function template declared in the class. It atomically sets `partialDestroyFinishedBit` via `fetch_or`, then — if the weak count is already zero at that point — calls `refCounts.notify_one()` to wake any thread blocked in `releaseWeakRef()`. The intentional `T**` (double-pointer) signature is a deliberate API ergonomic signal: after the call, `*o` is set to `nullptr` to discourage use-after-free, because another thread may immediately delete the object upon seeing the finished bit.
|
||||
|
||||
## Why Two Bits for Partial Destroy
|
||||
|
||||
There is a genuine race that a single bit cannot handle. Consider:
|
||||
|
||||
1. Thread A: last strong pointer releases → strong count goes to zero, weak count is 1, `partialDestroyStarted` is about to be set (but has not been set yet).
|
||||
2. Thread B: last weak pointer releases → sees `strong == 0`, `weak == 1 → 0` → would naively call the full destructor, racing with Thread A's partial destructor.
|
||||
|
||||
The `partialDestroyStarted` bit, set atomically in the same CAS that decrements the strong count, prevents Thread B from proceeding until the partial destructor's state is stable. The `partialDestroyFinished` bit then lets `releaseWeakRef()` know it is safe to destroy. When neither bit is set, `releaseWeakRef()` calls `refCounts.wait(…)` — a futex-style block on the atomic value — and rechecks upon wake-up.
|
||||
|
||||
## Key Operations
|
||||
|
||||
**`addStrongRef()` / `addWeakRef()`** use `fetch_add` with `acq_rel` ordering — the common fast path that requires no CAS loop.
|
||||
|
||||
**`addWeakReleaseStrongRef()`** is an atomic composite operation needed when `SharedWeakUnion` converts itself from a strong to a weak reference. Doing these as two separate operations would create a transient moment where the object has neither kind of reference holding it, which could trigger premature destruction. The implementation computes `weakDelta - strongDelta` and applies it as one delta in a CAS loop, while still setting `partialDestroyStartedBit` if appropriate.
|
||||
|
||||
**`checkoutStrongRefFromWeak()`** implements `lock()` semantics: it atomically increments the strong count, but only if the strong count is currently nonzero. If it reaches zero between load and CAS, the loop exits with `false`, signalling that the object is already being destroyed.
|
||||
|
||||
## Design Notes and Tradeoffs
|
||||
|
||||
The `addStrongRef()` is `noexcept` by requirement: `make_SharedIntrusive` calls it immediately after `new T(...)`, and if it could throw, the freshly allocated object would leak. The `static_assert` in `make_SharedIntrusive` enforces this.
|
||||
|
||||
The `uint16_t` strong count cap of 65 535 and 14-bit weak count cap of 16 383 are annotated with a `TODO`: if audit reveals these are insufficient, both types would need to widen to `uint32_t`, moving the entire atomic to `uint64_t`. The `checkStrongMaxValue` and `checkWeakMaxValue` constants leave a 32-unit margin below the hard cap, and debug-mode assertions in `RefCountPair`'s constructors fire before the actual overflow, providing early warning.
|
||||
|
||||
The `partialDestructorFinished()` free function is deliberately *not* called at the end of the `partialDestructor()` virtual method itself. This means any class that inherits `IntrusiveRefCounts` and implements its own `partialDestructor()` must explicitly call `partialDestructorFinished()` when done. The comment explains this was chosen to make the protocol visible at the call site rather than hiding it inside the smart-pointer machinery, reducing the chance that new subclasses silently skip the required notification.
|
||||
14
include/xrpl/basics/KeyCache.h.ai.json
Normal file
14
include/xrpl/basics/KeyCache.h.ai.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [],
|
||||
"description": "Defines a type alias KeyCache as a TaggedCache specialized for uint256 keys and int values within the xrpl namespace.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/KeyCache.h",
|
||||
"functions": [],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 5,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
include/xrpl/basics/KeyCache.h.ai.md
Normal file
13
include/xrpl/basics/KeyCache.h.ai.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# `KeyCache.h` — Key-Only Cache Type Alias
|
||||
|
||||
`KeyCache.h` provides a single-line type alias that expresses a common, narrow caching pattern throughout the XRPL codebase: track the *presence* of `uint256` keys over time, without attaching any value to them.
|
||||
|
||||
```cpp
|
||||
using KeyCache = TaggedCache<uint256, int, true>;
|
||||
```
|
||||
|
||||
The three template arguments to `TaggedCache` are: the key type (`uint256`, the 256-bit hash used pervasively in XRPL), a value type placeholder (`int`), and the `IsKeyCache` boolean flag set to `true`. That third argument is the critical one. Inside `TaggedCache`, `IsKeyCache = true` activates a compile-time branch via `std::conditional` that substitutes `KeyOnlyEntry` — a struct holding nothing but a `last_access` timestamp — in place of the full `ValueEntry` that carries a `shared_ptr` to an actual object. The `int` value type is therefore never stored or accessed; it exists only to satisfy the template parameter list.
|
||||
|
||||
This design lets callers answer a single yes/no question efficiently: *"Have I seen this hash recently enough that I don't need to re-check it?"* The primary consumer is `FullBelowCache` in `include/xrpl/shamap/FullBelowCache.h`, which uses a `KeyCache` to remember which SHAMap tree nodes have all their descendants resident in the database. When a node is marked "full below," the ledger acquisition machinery can skip redundant subtree traversals, a meaningful performance win during sync.
|
||||
|
||||
Because `KeyCache` is built directly on `TaggedCache`, it inherits the full sweep-based expiry mechanism, thread-safe access under `std::recursive_mutex`, and the `touch_if_exists` / `insert` interface — with `insert` enabled only in the key-only overload path when `IsKeyCache` is `true`.
|
||||
76
include/xrpl/basics/LocalValue.h.ai.json
Normal file
76
include/xrpl/basics/LocalValue.h.ai.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 34,
|
||||
"name": "lvs"
|
||||
},
|
||||
{
|
||||
"lineno": 53,
|
||||
"name": "Args... args"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 9,
|
||||
"name": "LocalValues"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 15,
|
||||
"name": "BasicValue"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T",
|
||||
"t"
|
||||
],
|
||||
"lineno": 21,
|
||||
"name": "Value"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Args... args"
|
||||
],
|
||||
"lineno": 52,
|
||||
"name": "LocalValue"
|
||||
}
|
||||
],
|
||||
"description": "Implements a thread- and coroutine-local storage utility for storing values specific to the calling thread or coroutine, using boost::thread_specific_ptr and custom value wrappers.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/LocalValue.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"lvs"
|
||||
],
|
||||
"lineno": 34,
|
||||
"name": "cleanup"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 41,
|
||||
"name": "getLocalValues"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 67,
|
||||
"name": "operator*"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 61,
|
||||
"name": "operator->"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 6,
|
||||
"name": "xrpl"
|
||||
},
|
||||
{
|
||||
"lineno": 8,
|
||||
"name": "detail"
|
||||
}
|
||||
]
|
||||
}
|
||||
64
include/xrpl/basics/LocalValue.h.ai.md
Normal file
64
include/xrpl/basics/LocalValue.h.ai.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# `LocalValue.h` — Coroutine-Aware Local Storage
|
||||
|
||||
## Role and Motivation
|
||||
|
||||
Standard `thread_local` storage is insufficient in a system that runs cooperative coroutines on a thread pool. XRPL's `JobQueue::Coro` coroutines (backed by `boost::coroutines2`) are created on one thread, suspended with `yield()`, and resumed — potentially on a different thread. If two coroutines share a worker thread, a naive `thread_local` variable would expose the state left behind by a previously suspended coroutine, producing subtle, hard-to-reproduce bugs.
|
||||
|
||||
`LocalValue<T>` solves this by providing **coroutine-aware local storage**: each coroutine (or plain thread, as a fallback) gets its own independent copy of a `T`, keyed to its execution context rather than its OS thread.
|
||||
|
||||
## Architecture
|
||||
|
||||
The design has two interlocking layers: a per-coroutine dictionary (`LocalValues`) managed via a thread-local pointer, and a typed wrapper (`LocalValue<T>`) that uses its own address as a dictionary key.
|
||||
|
||||
### `detail::LocalValues`
|
||||
|
||||
`LocalValues` is a runtime dictionary that maps `void const*` keys to heap-allocated `BasicValue` instances. The keys are the addresses of `LocalValue<T>` objects; the values hold a type-erased `Value<T>` (a concrete subclass of `BasicValue`) accessed through a virtual `get()` that returns `void*`. This type-erasure pattern lets a single heterogeneous map hold values of arbitrary types without any registry or type-ID scheme — the `LocalValue<T>` template handles all casts.
|
||||
|
||||
The `onCoro` flag distinguishes two ownership modes. When `onCoro == true`, the `LocalValues` is embedded directly in a `Coro` object (`detail::LocalValues lvs_`) and its lifetime is tied to the coroutine's. When `onCoro == false`, the `LocalValues` was heap-allocated for a plain thread worker, and the `cleanup()` deleter passed to `boost::thread_specific_ptr` will `delete` it on thread exit.
|
||||
|
||||
### Thread-Local Pointer Swap in `Coro::resume()`
|
||||
|
||||
The critical mechanism lives in `Coro.ipp`. Every time a coroutine is resumed, `Coro::resume()` performs a pointer swap:
|
||||
|
||||
```cpp
|
||||
auto saved = detail::getLocalValues().release();
|
||||
detail::getLocalValues().reset(&lvs_);
|
||||
// ... run coroutine body ...
|
||||
detail::getLocalValues().release();
|
||||
detail::getLocalValues().reset(saved);
|
||||
```
|
||||
|
||||
This installs the coroutine's private `lvs_` as the active `LocalValues` for the duration of the coroutine's time slice, then restores the previous state. Any `LocalValue<T>` dereference during that time slice will therefore see the coroutine's private dictionary. Because `lvs_` is owned by the `Coro` object and the `thread_specific_ptr` merely borrows it (it will not delete it thanks to `cleanup()`'s `onCoro` guard), there is no double-free risk.
|
||||
|
||||
### `LocalValue<T>` — The Public Interface
|
||||
|
||||
`LocalValue<T>` is a global or static object that holds a single "prototype" value `t_` set at construction time. The prototype is never mutated; it only serves as the initializer for per-context copies.
|
||||
|
||||
`operator*()` implements the lookup:
|
||||
|
||||
1. Call `detail::getLocalValues().get()` to retrieve (or lazily create) the `LocalValues` for the current context.
|
||||
2. If no `LocalValues` exists yet, allocate one with `onCoro = false` (plain thread path) and register it with the `thread_specific_ptr`.
|
||||
3. Search for `this` in `lvs->values`. On a hit, cast the `void*` back to `T&` and return it.
|
||||
4. On a miss, emplace a new `Value<T>` copy-initialized from `t_`, then return a reference to that new copy.
|
||||
|
||||
`operator->()` is a trivial forwarding wrapper to `operator*()`.
|
||||
|
||||
## Concrete Usage
|
||||
|
||||
In `IOUAmount.cpp`, `LocalValue<bool>` governs whether IOU arithmetic uses the newer `STNumber` code path or the legacy one. Wrapping this flag in a `LocalValue` rather than `thread_local` ensures that two coroutines executing concurrently on the same thread pool can independently select their arithmetic mode without one overwriting the other's flag:
|
||||
|
||||
```cpp
|
||||
static LocalValue<bool> r{true};
|
||||
```
|
||||
|
||||
The coroutine test in `Coroutine_test.cpp` verifies the isolation property directly: four coroutines each set `*lv = id` (their own integer ID), interleave via yields, and confirm that no coroutine ever sees another's value. A plain-thread job running on the same pool also sees an independent copy.
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
**Address-keyed map instead of a registry.** Using `this` (the `LocalValue<T>*`) as the map key avoids any global registry or static ID counter. Each `LocalValue<T>` is naturally unique by address, and the approach requires no synchronization beyond what the `thread_specific_ptr` already provides.
|
||||
|
||||
**Lazy allocation.** The per-context copy is created on first dereference rather than at construction. This keeps coroutine startup cheap when only some `LocalValue` instances are actually accessed.
|
||||
|
||||
**`void*` type erasure with `unique_ptr<BasicValue>`.** A single `unordered_map` can hold values of unrelated types (`bool`, `int`, custom structs) because ownership and destruction are managed through the virtual destructor of `BasicValue`. The `LocalValue<T>` template retains type knowledge and performs the cast safely — the key equality guarantees that the `void*` behind a given key will always be a `T*`.
|
||||
|
||||
**`onCoro` ownership flag.** The asymmetry between coroutine-owned and thread-owned `LocalValues` is handled by a single boolean rather than two separate code paths or a custom deleter per instance. The `boost::thread_specific_ptr` deleter is fixed at static-initialization time, so the flag is the only extensible hook available.
|
||||
62
include/xrpl/basics/Log.h.ai.json
Normal file
62
include/xrpl/basics/Log.h.ai.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 36,
|
||||
"name": "partition"
|
||||
},
|
||||
{
|
||||
"lineno": 36,
|
||||
"name": "thresh"
|
||||
},
|
||||
{
|
||||
"lineno": 36,
|
||||
"name": "logs"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"level"
|
||||
],
|
||||
"lineno": 27,
|
||||
"name": "Logs"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"partition",
|
||||
"thresh",
|
||||
"logs"
|
||||
],
|
||||
"lineno": 33,
|
||||
"name": "Logs::Sink"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 61,
|
||||
"name": "Logs::File"
|
||||
}
|
||||
],
|
||||
"description": "This file defines logging utilities for the XRPL project, including log severity levels, a Logs manager class for handling log partitions and files, and macros/utilities for debug logging.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Log.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"sink"
|
||||
],
|
||||
"lineno": 168,
|
||||
"name": "setDebugLogSink"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 175,
|
||||
"name": "debugLog"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 18,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
57
include/xrpl/basics/Log.h.ai.md
Normal file
57
include/xrpl/basics/Log.h.ai.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# `include/xrpl/basics/Log.h` — Logging Infrastructure
|
||||
|
||||
## Role in the System
|
||||
|
||||
`Log.h` is the entry point for all structured logging in the XRPL C++ server. It defines the central `Logs` class, which acts as a factory and registry for named logging partitions, manages the output file, and controls severity thresholds. Every component in the server — consensus, ledger management, networking, transaction processing — acquires a `beast::Journal` handle from `Logs`, then uses that handle to write messages at the appropriate severity level without coupling to the output destination.
|
||||
|
||||
The header sits at the boundary between xrpl's own types and the `beast` logging primitives it builds on. `beast::Journal` (from `xrpl/beast/utility/Journal.h`) is the lightweight, copyable handle that individual subsystems carry; `Logs` is the stateful root that backs all those handles.
|
||||
|
||||
## The `beast::Journal` Layer
|
||||
|
||||
Before understanding `Logs`, it helps to understand what it produces. A `beast::Journal` is a non-owning, copyable pointer to a `beast::Journal::Sink`. The `Sink` is an abstract base providing two key operations: `write()` (filtered by threshold) and `writeAlways()` (bypasses the threshold check). The `Journal` itself offers named severity accessors — `trace()`, `debug()`, `info()`, `warn()`, `error()`, `fatal()` — each returning a `Journal::Stream` that implements `operator bool()` returning whether the level is currently active.
|
||||
|
||||
This architecture makes cheap caller-side checks possible: every `Stream` can be tested for activity before any string formatting happens.
|
||||
|
||||
## The `JLOG` Macro and Lazy Evaluation
|
||||
|
||||
The `JLOG` macro is the primary idiom for logging throughout the codebase:
|
||||
|
||||
```cpp
|
||||
JLOG(j.warn()) << "Computed value: " << expensiveFunction();
|
||||
```
|
||||
|
||||
Expanding to an `if (!x) {} else x` pattern, this ensures that if the `warn()` stream is below threshold, the `<<` chain is never evaluated. This is architecturally significant: without it, even a disabled `trace()` call would still serialize all arguments into a string, burning CPU in a hot path. The macro rather than an inline function avoids any overhead whatsoever at the disabled level — the compiler simply skips the branch. `CLOG` is a similar guard for optional pointer-style stream objects (used when the stream may be null).
|
||||
|
||||
## The `Logs` Class
|
||||
|
||||
`Logs` is constructed once per process with an initial severity threshold and acts as the single routing point for all log output. It maintains a `std::map<std::string, unique_ptr<Sink>>` keyed by partition name (subsystem label), with a `boost::beast::iless` comparator for case-insensitive lookup. This means code can request `"Ledger"` or `"ledger"` and get the same sink.
|
||||
|
||||
### Partition Sinks
|
||||
|
||||
The inner `Logs::Sink` class extends `beast::Journal::Sink`, carrying a partition name and a back-reference to its parent `Logs`. When `write()` is called on a sink, it delegates immediately to `Logs::write()`, which holds the shared `mutex_`, formats the message, writes to the log file, and — unless `silent_` is set — writes to `std::cerr`. The indirection through `Logs::write()` is what makes it possible to serialize all partition output through one lock and one file handle.
|
||||
|
||||
Callers acquire sinks via `Logs::get()` or `Logs::journal()`. The `get()` implementation uses `emplace()` on the map: if the partition already exists the existing sink is returned, otherwise a new one is created via the virtual `makeSink()` factory method. Because `makeSink()` is virtual, tests can subclass `Logs` to inject mock sinks without touching the rest of the system.
|
||||
|
||||
Changing the global threshold via `Logs::threshold(Severity)` updates `thresh_` and then iterates all existing sinks to propagate the change. This means a run-time config reload can silence verbose partitions or turn on trace output without restarting the server.
|
||||
|
||||
### File Output and Log Rotation
|
||||
|
||||
The nested `Logs::File` class wraps a `std::ofstream` opened in append mode. The design is intentionally minimal: `open()`, `close()`, `write()`, `writeln()`. The critical method is `closeAndReopen()`, which is the POSIX log rotation idiom: external tools like `logrotate(8)` rename the live log file and then send a signal to the server; the server responds by calling `Logs::rotate()`, which closes its file handle and reopens the path, creating a fresh file at the original location. The `File` class stores `m_path` specifically to enable this reopen without any external coordination.
|
||||
|
||||
### Message Formatting and Security Scrubbing
|
||||
|
||||
`Logs::format()` — a private static method called unconditionally before any write — prepends a timestamp and renders the severity as a three-letter tag (`TRC`, `DBG`, `NFO`, `WRN`, `ERR`, `FTL`), followed by the partition name. It then truncates messages exceeding 12 KB to prevent runaway log lines.
|
||||
|
||||
Critically, `format()` also performs **sensitive data redaction**. After composing the full output string, it scans for JSON keys `"seed"`, `"seed_hex"`, `"secret"`, `"master_key"`, `"master_seed"`, `"master_seed_hex"`, and `"passphrase"`, and replaces the associated quoted values with asterisks. This is a defensive measure to prevent operator errors from leaking wallet secrets into log files — even if a developer mistakenly logs a raw JSON RPC request containing signing keys.
|
||||
|
||||
## Deprecated `LogSeverity` Enum
|
||||
|
||||
The `LogSeverity` enum (`lsTRACE`, `lsDEBUG`, etc.) is a legacy mapping that predates the `beast::severities::Severity` enum. It is marked deprecated but kept for backwards compatibility, with `fromSeverity()` and `toSeverity()` providing the translation. The `fromString()` method accepts several aliases (`"warn"`, `"warning"`, `"warnings"`) using case-insensitive comparison, which serves the config file parser.
|
||||
|
||||
## Debug Logging
|
||||
|
||||
`setDebugLogSink()` and `debugLog()` provide a secondary, process-global logging channel backed by a thread-safe `DebugSink` Meyers singleton (defined in `Log.cpp`). This channel defaults to a null sink and is primarily for test code that wants to capture log output without constructing a full `Logs` instance. The documentation explicitly warns that output from `debugLog()` may never be seen — it is unsuitable for any information that matters operationally.
|
||||
|
||||
## Thread Safety
|
||||
|
||||
All mutable state in `Logs` — the sink map, the file handle, and the threshold — is protected by a single `mutable mutex_`. `beast::Journal` and `Stream` objects are copyable and safe to use across threads because they only dereference a `Sink*` for reads. The `DebugSink` in `Log.cpp` has its own separate mutex for swapping the debug sink atomically while concurrent `debugLog()` calls may be in flight.
|
||||
32
include/xrpl/basics/MallocTrim.h.ai.json
Normal file
32
include/xrpl/basics/MallocTrim.h.ai.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 54,
|
||||
"name": "tag"
|
||||
},
|
||||
{
|
||||
"lineno": 54,
|
||||
"name": "journal"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "Provides a facility to attempt to return freed memory to the operating system by invoking malloc_trim on Linux/glibc, along with a report structure for before/after memory metrics.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/MallocTrim.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"tag",
|
||||
"journal"
|
||||
],
|
||||
"lineno": 54,
|
||||
"name": "mallocTrim"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 6,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
45
include/xrpl/basics/MallocTrim.h.ai.md
Normal file
45
include/xrpl/basics/MallocTrim.h.ai.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# `include/xrpl/basics/MallocTrim.h`
|
||||
|
||||
## Role in the System
|
||||
|
||||
This header is a targeted memory-pressure relief valve for the XRPL node process. On long-running servers, glibc's ptmalloc allocator can accumulate free pages in its internal arenas without returning them to the OS, causing the process's Resident Set Size (RSS) to drift upward over time even when application-level memory usage has genuinely declined. `MallocTrim.h` exposes a controlled wrapper around `::malloc_trim(0)` that reclaims that slack, paired with a diagnostic report so callers can observe what the operation actually accomplished.
|
||||
|
||||
## `MallocTrimReport`
|
||||
|
||||
The `MallocTrimReport` struct is the observability surface for a single trim invocation. All fields default to sentinel values (`-1` or `false`) that signal "not populated" rather than zero, which is important because a real RSS reading or page-fault count of zero is valid and meaningful. The fields capture:
|
||||
|
||||
- **`supported`** — whether the current platform supports trimming at all. This lets callers distinguish "trim ran and did nothing" from "trim was never attempted."
|
||||
- **`trimResult`** — the raw return value from `::malloc_trim`: `1` if the allocator actually released pages, `0` if there was nothing to release.
|
||||
- **`rssBeforeKB` / `rssAfterKB`** — process RSS read from `/proc/self/statm` bracketing the trim call, in kilobytes.
|
||||
- **`durationUs`** — wall-clock duration of the `::malloc_trim` call in microseconds, useful for detecting trim latency spikes.
|
||||
- **`minfltDelta` / `majfltDelta`** — thread-level minor and major page fault deltas (via `RUSAGE_THREAD`) across the trim, making it possible to detect cases where trimming caused unexpected kernel re-faulting of pages.
|
||||
|
||||
The inline `deltaKB()` method returns the signed RSS change (negative means memory was freed, positive means RSS grew). It guards against uninitialized sentinel values by returning `0` if either RSS reading is negative.
|
||||
|
||||
## `mallocTrim()` Function
|
||||
|
||||
The function signature is:
|
||||
|
||||
```cpp
|
||||
MallocTrimReport mallocTrim(std::string_view tag, beast::Journal journal);
|
||||
```
|
||||
|
||||
The `tag` is a caller-supplied identifier that appears in log output, allowing multiple call sites to be distinguished in diagnostics. The actual implementation in `MallocTrim.cpp` is compiled conditionally: the entire trim path is gated on `#if defined(__GLIBC__) && BOOST_OS_LINUX`. On any other platform, the function is a no-op that returns a default-constructed report with `supported = false`.
|
||||
|
||||
When active, the implementation makes a deliberate performance tradeoff gated on the journal's debug severity. If debug logging is disabled, only `::malloc_trim(0)` is called and the report stays mostly at sentinel values — no `/proc` reads, no rusage calls, minimal overhead. If debug logging is enabled, the function reads `/proc/self/statm` before and after, calls `getrusage(RUSAGE_THREAD, ...)` before and after, and measures wall time. This layered instrumentation is only paid when someone is actively debugging memory behavior, which makes the function cheap enough for production call sites.
|
||||
|
||||
The trim padding constant is hardcoded to `TRIM_PAD = 0`. A comment in the implementation documents that this choice came from 12-hour empirical testing on Mainnet across four padding values (0, 256 KB, 1 MB, 16 MB): zero delivered the best balance of RSS reduction and trim-latency stability without adding a tuning surface. This is a non-obvious design choice that prevents the parameter from becoming a configuration footgun.
|
||||
|
||||
## Allocator Compatibility and Scope Limits
|
||||
|
||||
The header's comment block is worth reading carefully: `malloc_trim` only affects glibc's own `sbrk`-based arenas. Large allocations backed by `mmap` are returned to the OS when freed, regardless of trimming. More critically, if jemalloc or tcmalloc is linked or `LD_PRELOAD`ed, calling `::malloc_trim` from glibc's symbol is harmless but does not touch those allocators' arenas. The function has no way to detect this at runtime, so the `supported` flag is set based on compiler/OS detection only, not on whether the active allocator will actually respond.
|
||||
|
||||
## Call Site
|
||||
|
||||
The function is invoked in `Application.cpp`'s `doSweep()` method, immediately after cache sweeps and ledger cleanup:
|
||||
|
||||
```cpp
|
||||
mallocTrim("doSweep", m_journal);
|
||||
```
|
||||
|
||||
This is the canonical usage pattern: call at a known point after significant bulk-free operations, rather than on a timer. The header comment recommends rate-limiting calls to avoid churn, which `doSweep`'s existing sweep timer naturally provides.
|
||||
32
include/xrpl/basics/MathUtilities.h.ai.json
Normal file
32
include/xrpl/basics/MathUtilities.h.ai.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 16,
|
||||
"name": "count"
|
||||
},
|
||||
{
|
||||
"lineno": 16,
|
||||
"name": "total"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "Provides a constexpr function to calculate the percentage of one number divided by another, rounded up and capped between 0 and 100, with static_assert unit tests.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/MathUtilities.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"count",
|
||||
"total"
|
||||
],
|
||||
"lineno": 16,
|
||||
"name": "calculatePercent"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 6,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
include/xrpl/basics/MathUtilities.h.ai.md
Normal file
23
include/xrpl/basics/MathUtilities.h.ai.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# `MathUtilities.h` — Integer Percentage Utility
|
||||
|
||||
This header lives in the `xrpl::basics` utility layer and provides a single function, `calculatePercent`, for computing integer percentages with ceiling rounding and range clamping. It exists because this specific combination of behaviors — ceiling division, overflow-safe clamping, and compile-time verifiability — recurs in the XRPL codebase wherever a count-over-total ratio needs to be expressed as a human-readable integer percentage without floating point.
|
||||
|
||||
## `calculatePercent`
|
||||
|
||||
```cpp
|
||||
constexpr std::size_t calculatePercent(std::size_t count, std::size_t total)
|
||||
```
|
||||
|
||||
The function answers: "what percent of `total` is `count`, rounded up to the nearest whole percent, and never exceeding 100?" The ceiling rounding is intentional: a fraction like 1/99 (≈1.01%) rounds up to 2, signalling that *some* nonzero fraction is present. This is the conservative, safety-oriented direction for thresholding use cases — if you're checking whether at least N% of validators have seen something, you want to err on the side of reporting a higher fraction rather than losing signal in truncation.
|
||||
|
||||
The arithmetic `((std::min(count, total) * 100) + total - 1) / total` encodes three distinct behaviors in one expression. The `std::min(count, total)` clamp prevents the numerator from exceeding `total * 100`, which both enforces the 100% cap and guards against the case where `count > total` (e.g., a validator set that temporarily grows). The `+ total - 1` addend is the standard ceiling-division trick: it causes any non-zero remainder in the integer division to round up rather than truncate. The whole expression stays in unsigned integer arithmetic with no floating point, making it safe across all platforms and constexpr-eligible.
|
||||
|
||||
The `assert(total != 0)` guard catches division-by-zero in debug builds. The comment explains why `XRPL_ASSERT` is not used here: the function is `constexpr`, and `XRPL_ASSERT` is not constexpr-compatible, so the plain `assert` is the appropriate defence at runtime while still permitting compile-time evaluation.
|
||||
|
||||
## Real-World Use
|
||||
|
||||
The primary call site is in `LedgerMaster.cpp`, which uses `calculatePercent` to decide whether to warn operators about a potential software upgrade need. Two separate thresholds are evaluated: whether at least 90% of the UNL (Unique Node List) validators have sent validation messages, and whether at least 60% of xrpld-running validators are on a higher version. Both checks use `calculatePercent` with named constants (`reportingPercent`, `cutoffPercent`) rather than raw literals, and the thresholding via `>=` naturally benefits from ceiling rounding — a value just above a threshold boundary will not be falsely excluded by truncation.
|
||||
|
||||
## Compile-Time Tests
|
||||
|
||||
The `static_assert` block at namespace scope doubles as both documentation and zero-cost test coverage. Fourteen cases are checked at compile time, covering the zero numerator, full 100% match, over-100% input, boundary rounding cases (1/99, 1/64, near-100% fractions), and large-scale inputs up to 100 million. If the implementation is ever changed and any of these properties breaks, the build fails immediately — there is no need for a separate test binary or runtime fixture. This pattern is well-suited to a pure arithmetic utility that has no external dependencies.
|
||||
60
include/xrpl/basics/Mutex.hpp.ai.json
Normal file
60
include/xrpl/basics/Mutex.hpp.ai.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 11,
|
||||
"name": "ProtectedDataType"
|
||||
},
|
||||
{
|
||||
"lineno": 11,
|
||||
"name": "MutexType"
|
||||
},
|
||||
{
|
||||
"lineno": 15,
|
||||
"name": "LockType"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"MutexType& mutex, ProtectedDataType& data"
|
||||
],
|
||||
"lineno": 18,
|
||||
"name": "Lock"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"ProtectedDataType data"
|
||||
],
|
||||
"lineno": 56,
|
||||
"name": "Mutex"
|
||||
}
|
||||
],
|
||||
"description": "Provides a thread-safe container (Mutex) for protecting data with a mutex, inspired by Rust's Mutex, and a Lock class for accessing the protected data safely.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Mutex.hpp",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"Args&&... args"
|
||||
],
|
||||
"lineno": 66,
|
||||
"name": "Mutex::make"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 81,
|
||||
"name": "Mutex::lock (const)"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 92,
|
||||
"name": "Mutex::lock"
|
||||
}
|
||||
],
|
||||
"language": "cpp header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 8,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
51
include/xrpl/basics/Mutex.hpp.ai.md
Normal file
51
include/xrpl/basics/Mutex.hpp.ai.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# `include/xrpl/basics/Mutex.hpp`
|
||||
|
||||
## Purpose and Motivation
|
||||
|
||||
This header introduces a Rust-inspired `Mutex<T>` idiom to the XRPL C++ codebase. The core problem it solves is a well-known C++ anti-pattern: separating a mutex from the data it protects, which lets callers accidentally read or write the data without holding the lock. The standard library provides `std::mutex` and RAII guards, but the data itself remains a separate, freely-accessible member. Nothing in the type system stops code from doing `data_ = x;` without ever calling `lock()`.
|
||||
|
||||
The Rust model solves this by making the lock guard the only path to the data. `Mutex.hpp` reproduces that guarantee in C++: the protected value lives inside `Mutex<T>`, is entirely private, and the only way to reach it is through a `Lock` object returned by `Mutex::lock()`. A `Lock` holds both the RAII lock and a reference to the data, so when the `Lock` goes out of scope the mutex is released and the reference becomes unreachable simultaneously.
|
||||
|
||||
The file is credited to the Clio project (`https://github.com/XRPLF/clio`) and lives under the `xrpl` namespace in `include/xrpl/basics/`.
|
||||
|
||||
## `Lock<ProtectedDataType, LockType, MutexType>`
|
||||
|
||||
`Lock` is a thin RAII wrapper that bundles a lock instance (`LockType<MutexType> lock_`) with a reference to the protected data (`ProtectedDataType& data_`). It exposes `operator*`, `get()`, and `operator->` — all in both const and non-const variants — so users interact with the guarded value naturally.
|
||||
|
||||
The constructor `Lock(MutexType& mutex, ProtectedDataType& data)` is `private`, with `friend class Mutex<std::remove_const_t<ProtectedDataType>, MutexType>` as the sole access point. The `std::remove_const_t` strip is intentional: when the `Mutex` is const it instantiates `Lock<ProtectedDataType const, ...>`, whose friend declaration must still point back to `Mutex<ProtectedDataType>` (non-const), not the non-existent `Mutex<ProtectedDataType const>`. This single line of `friend` is what makes the safety guarantee airtight — no code path outside `Mutex::lock()` can construct a `Lock`.
|
||||
|
||||
The conversion operators `operator LockType<MutexType>&()` and `operator LockType<MutexType> const&()` allow a `Lock` to decay to a reference to the underlying lock object. This matters in practice when `std::unique_lock` is chosen as the `LockType` and the lock needs to be passed to `std::condition_variable::wait()`, which accepts a `std::unique_lock<std::mutex>&`. The conversion keeps the idiom composable with the standard library's synchronisation utilities.
|
||||
|
||||
## `Mutex<ProtectedDataType, MutexType>`
|
||||
|
||||
`Mutex` stores a `mutable MutexType mutex_` and a `ProtectedDataType data_{}`. The `mutable` qualifier is deliberate: acquiring a lock is logically a non-mutating operation on the container, so it must be callable on a `const Mutex` instance. Without `mutable` the const `lock()` overload could not take `mutex_` by non-const reference as required by any standard lock type.
|
||||
|
||||
Two `lock()` overloads handle const-correctness propagation:
|
||||
|
||||
```cpp
|
||||
// const Mutex → Lock<ProtectedDataType const, ...> (read-only access)
|
||||
Lock<ProtectedDataType const, LockType, MutexType> lock() const;
|
||||
|
||||
// non-const Mutex → Lock<ProtectedDataType, ...> (read-write access)
|
||||
Lock<ProtectedDataType, LockType, MutexType> lock();
|
||||
```
|
||||
|
||||
Calling `lock()` on a `const Mutex` yields a `Lock` whose `operator*` and `operator->` return `const` references. The compiler enforces this — there is no cast or escape hatch. The test suite verifies it with `static_assert(std::is_const_v<...>)`.
|
||||
|
||||
Both overloads are templated on `LockType`, which defaults to `std::lock_guard`. Callers can substitute `std::unique_lock` for deferred or timed locking, or combine a `std::shared_mutex` as the `MutexType` with `std::shared_lock` as the `LockType` to implement reader-writer semantics with the same ergonomic API. The test in `src/tests/libxrpl/basics/Mutex.cpp` demonstrates exactly this: multiple shared readers coexist while an exclusive writer uses `std::unique_lock`.
|
||||
|
||||
The `make()` static factory provides perfect-forwarding in-place construction of the protected object:
|
||||
|
||||
```cpp
|
||||
auto m = Mutex<MyStruct>::make(arg1, arg2); // forwards to MyStruct(arg1, arg2)
|
||||
```
|
||||
|
||||
This avoids a move-construct-then-copy sequence when constructing from arguments and also supports move-only types like `std::unique_ptr<T>`.
|
||||
|
||||
## Design Trade-offs
|
||||
|
||||
The main cost of this pattern is that the `Lock` object must remain alive for the entire scope of any access. Code that needs to unlock mid-scope, or pass just the protected value to a function, requires `std::unique_lock` as the `LockType` so the conversion operator can expose the lock for manual `unlock()`/`lock()` calls. The default `std::lock_guard` is intentionally non-flexible — it signals "lock for this scope, nothing else" and prevents premature unlocking.
|
||||
|
||||
There is no `try_lock()` surface exposed through `Mutex` itself. A caller wanting try-lock semantics must drop to `std::unique_lock` (for `try_lock()`) or interact with the raw mutex type outside this wrapper, which is a deliberate friction point rather than an oversight. The design prioritises the common, safe path — RAII lock for the lifetime of a `Lock` object — over less common, riskier patterns.
|
||||
|
||||
The file is the sole resident of the `basics/` header directory that was found, suggesting it was added as a focused utility pulled from Clio rather than part of a larger local module. Its test coverage in `Mutex.cpp` is comprehensive: default construction, value construction, `make()` with forwarding, const and non-const access, custom lock types, `std::shared_mutex` reader-writer patterns, and move-only protected types.
|
||||
363
include/xrpl/basics/Number.h.ai.json
Normal file
363
include/xrpl/basics/Number.h.ai.json
Normal file
@@ -0,0 +1,363 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 10,
|
||||
"name": "amount"
|
||||
},
|
||||
{
|
||||
"lineno": 13,
|
||||
"name": "value"
|
||||
},
|
||||
{
|
||||
"lineno": 31,
|
||||
"name": "scale_"
|
||||
},
|
||||
{
|
||||
"lineno": 119,
|
||||
"name": "mantissa"
|
||||
},
|
||||
{
|
||||
"lineno": 119,
|
||||
"name": "exponent"
|
||||
},
|
||||
{
|
||||
"lineno": 117,
|
||||
"name": "negative"
|
||||
},
|
||||
{
|
||||
"lineno": 373,
|
||||
"name": "mode"
|
||||
},
|
||||
{
|
||||
"lineno": 407,
|
||||
"name": "scale"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"mantissa_scale scale_"
|
||||
],
|
||||
"lineno": 28,
|
||||
"name": "MantissaRange"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 81,
|
||||
"name": "Number"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 120,
|
||||
"name": "Number::unchecked"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 127,
|
||||
"name": "Number::normalized"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number::rounding_mode mode"
|
||||
],
|
||||
"lineno": 370,
|
||||
"name": "saveNumberRoundMode"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number::rounding_mode mode"
|
||||
],
|
||||
"lineno": 386,
|
||||
"name": "NumberRoundModeGuard"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"MantissaRange::mantissa_scale scale"
|
||||
],
|
||||
"lineno": 404,
|
||||
"name": "NumberMantissaScaleGuard"
|
||||
}
|
||||
],
|
||||
"description": "Defines the xrpl::Number class, a floating point type for representing a wide range of asset values with precise mantissa and exponent control, including normalization, rounding, and mantissa range switching. Also provides related utilities, guards, and helper functions.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Number.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"Number const& amount"
|
||||
],
|
||||
"lineno": 10,
|
||||
"name": "to_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T value"
|
||||
],
|
||||
"lineno": 13,
|
||||
"name": "logTen"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T value"
|
||||
],
|
||||
"lineno": 24,
|
||||
"name": "isPowerOfTen"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number const& x",
|
||||
"Number const& y"
|
||||
],
|
||||
"lineno": 154,
|
||||
"name": "operator=="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number const& x",
|
||||
"Number const& y"
|
||||
],
|
||||
"lineno": 160,
|
||||
"name": "operator!="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number const& x",
|
||||
"Number const& y"
|
||||
],
|
||||
"lineno": 165,
|
||||
"name": "operator<"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 186,
|
||||
"name": "signum"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 191,
|
||||
"name": "truncate"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number const& x",
|
||||
"Number const& y"
|
||||
],
|
||||
"lineno": 194,
|
||||
"name": "operator>"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number const& x",
|
||||
"Number const& y"
|
||||
],
|
||||
"lineno": 198,
|
||||
"name": "operator<="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number const& x",
|
||||
"Number const& y"
|
||||
],
|
||||
"lineno": 202,
|
||||
"name": "operator>="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::ostream& os",
|
||||
"Number const& x"
|
||||
],
|
||||
"lineno": 206,
|
||||
"name": "operator<<"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number const& amount"
|
||||
],
|
||||
"lineno": 211,
|
||||
"name": "to_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number f",
|
||||
"unsigned d"
|
||||
],
|
||||
"lineno": 213,
|
||||
"name": "root"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number f"
|
||||
],
|
||||
"lineno": 215,
|
||||
"name": "root2"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 220,
|
||||
"name": "getround"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"rounding_mode mode"
|
||||
],
|
||||
"lineno": 222,
|
||||
"name": "setround"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 229,
|
||||
"name": "getMantissaScale"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"MantissaRange::mantissa_scale scale"
|
||||
],
|
||||
"lineno": 234,
|
||||
"name": "setMantissaScale"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 238,
|
||||
"name": "minMantissa"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 243,
|
||||
"name": "maxMantissa"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 248,
|
||||
"name": "mantissaLog"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 254,
|
||||
"name": "oneSmall"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 256,
|
||||
"name": "oneLarge"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 260,
|
||||
"name": "one"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T minMantissa",
|
||||
"T maxMantissa"
|
||||
],
|
||||
"lineno": 264,
|
||||
"name": "normalizeToRange"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 282,
|
||||
"name": "normalize"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"bool& negative",
|
||||
"T& mantissa",
|
||||
"int& exponent",
|
||||
"internalrep const& minMantissa",
|
||||
"internalrep const& maxMantissa"
|
||||
],
|
||||
"lineno": 287,
|
||||
"name": "normalize"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"bool& negative",
|
||||
"T& mantissa_",
|
||||
"int& exponent_",
|
||||
"MantissaRange::rep const& minMantissa",
|
||||
"MantissaRange::rep const& maxMantissa"
|
||||
],
|
||||
"lineno": 295,
|
||||
"name": "doNormalize"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 299,
|
||||
"name": "isnormal"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"int exponentDelta"
|
||||
],
|
||||
"lineno": 304,
|
||||
"name": "shiftExponent"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"rep mantissa"
|
||||
],
|
||||
"lineno": 309,
|
||||
"name": "externalToInternal"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number x"
|
||||
],
|
||||
"lineno": 324,
|
||||
"name": "abs"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number const& f",
|
||||
"unsigned n"
|
||||
],
|
||||
"lineno": 334,
|
||||
"name": "power"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number f",
|
||||
"unsigned d"
|
||||
],
|
||||
"lineno": 338,
|
||||
"name": "root"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number f"
|
||||
],
|
||||
"lineno": 341,
|
||||
"name": "root2"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number const& f",
|
||||
"unsigned n",
|
||||
"unsigned d"
|
||||
],
|
||||
"lineno": 344,
|
||||
"name": "power"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Number const& x",
|
||||
"Number const& limit"
|
||||
],
|
||||
"lineno": 348,
|
||||
"name": "squelch"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"MantissaRange::mantissa_scale const& scale"
|
||||
],
|
||||
"lineno": 355,
|
||||
"name": "to_string"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 8,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
54
include/xrpl/basics/Number.h.ai.md
Normal file
54
include/xrpl/basics/Number.h.ai.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# `include/xrpl/basics/Number.h` — Decimal Floating-Point Arithmetic for XRPL
|
||||
|
||||
`Number` is the XRPL ledger's custom decimal floating-point type. It exists because IEEE 754 `double` is unsuitable for consensus-critical financial arithmetic: binary floating point introduces rounding artifacts that can cause two nodes to compute slightly different results for the same transaction, breaking the ledger's agreement requirement. All arithmetic needed during transaction processing — AMM liquidity calculations, lending protocol interest, IOU amounts, XRP drops, MPT balances — routes through `Number`.
|
||||
|
||||
## Internal Representation
|
||||
|
||||
A `Number` stores three fields: a `bool negative_` sign flag, a `uint64_t mantissa_` (called `internalrep` in the code), and an `int exponent_`. The mantissa is kept *normalized*, meaning it sits in a range `[min, max]` where `min` is a power of ten and `max = min * 10 - 1`. The exponent is bounded to `[minExponent, maxExponent]` = `[-32768, 32768]`. Zero is the special case where mantissa equals zero and `exponent_` is `std::numeric_limits<int>::lowest()`.
|
||||
|
||||
This representation was inherited from `STAmount`, which encodes IOU values the same way. The key insight is that financial values in the ledger are already expressed in decimal — storing them in a decimal floating-point type avoids any binary-to-decimal conversion loss.
|
||||
|
||||
## The Two Mantissa Scales — and Why They Both Exist
|
||||
|
||||
The file introduces `MantissaRange`, a small value-type holding the `min`, `max`, and `log` for one of two permitted scales:
|
||||
|
||||
- **Small scale** (`10^15` to `10^16 - 1`): the original `STAmount` normalization range. It gives 15–16 significant decimal digits, which is sufficient for IOU values but cannot exactly represent large integers like XRP drops or MPT balances up to `2^63 - 1 ≈ 9.2 × 10^18`.
|
||||
- **Large scale** (`10^18` to `10^19 - 1`): introduced for `SingleAssetVault` and `LendingProtocol`. It gives 18–19 significant decimal digits, covering the full positive `int64_t` range and the integer values needed for XRP and MPT precisely.
|
||||
|
||||
The active scale is controlled by the thread-local `range_` member, a `std::reference_wrapper<MantissaRange const>` that points to one of two `constexpr static` instances (`smallRange` or `largeRange`). Using a reference wrapper rather than copying the range is deliberate: it prevents accidentally mutating the authoritative constants and makes the scale switch cheap — just a pointer swap.
|
||||
|
||||
Scale switching is amendment-gated. In `applySteps.cpp`, the `with_txn_type` function checks `featureSingleAssetVault` and `featureLendingProtocol` and, if either is enabled, installs a `NumberMantissaScaleGuard` that sets the scale to `large` for the duration of the transaction and restores the previous value on exit. This means pre-amendment transactions continue to use the small scale, preserving backward-compatible arithmetic results even in mixed-amendment ledgers.
|
||||
|
||||
## External vs. Internal Interface
|
||||
|
||||
The internal mantissa is an unsigned `uint64_t` and can hold the large-scale maximum of `9,999,999,999,999,999,999` — which exceeds `INT64_MAX = 9,223,372,036,854,775,807`. However, the external interface (`mantissa()` and `exponent()`) must return a signed `int64_t`, so `mantissa()` silently divides by 10 and `exponent()` increments by 1 whenever the internal mantissa is in this "overflow zone." The pair is guaranteed consistent. This is the mechanism that allows the internal representation to precisely track the full large-scale range while still presenting a canonical 63-bit external view.
|
||||
|
||||
`Number` cannot represent `-2^63` exactly, but that value is not a valid ledger amount: XRP drops are non-negative, and MPT maximum is `2^63 - 1`.
|
||||
|
||||
The conversion from signed external `rep` to internal unsigned `internalrep` is handled by `externalToInternal()`. It cannot simply negate `INT64_MIN` because that is undefined behavior in C++; the method falls through to a 128-bit intermediate cast for that edge case.
|
||||
|
||||
## Guard Digits and Rounding
|
||||
|
||||
The `Guard` inner class is the arithmetic workhorse. It maintains a 64-bit BCD-like register of up to 16 guard decimal digits (each stored in 4 bits using a shift-and-push register) plus a sticky `xbit_` that records whether any non-zero digit was ever pushed beyond the 16-digit window. This is classic guard-digit arithmetic, ensuring that intermediate precision lost during alignment or multiplication is available for correct final rounding.
|
||||
|
||||
Rounding mode is also thread-local (`mode_`). The four modes — `to_nearest` (banker's rounding / round-half-to-even), `towards_zero`, `downward`, `upward` — map directly to IEEE 754 semantics. The default is `to_nearest`. `NumberRoundModeGuard` sets a new mode on construction and restores the old one on destruction, enabling scoped rounding control without global mutation.
|
||||
|
||||
## Arithmetic Implementation
|
||||
|
||||
Addition aligns the two operands to the same exponent by dividing the smaller one by successive powers of 10, pushing each lost digit into a `Guard`. Subtraction is implemented as `*this += -y`. Multiplication and division use `uint128_t` intermediates (GCC/Clang `__uint128_t`; Boost multiprecision on MSVC) to avoid 64-bit overflow before normalization. The `divu10()` function is a bespoke 128-bit divide-by-10 using the Hacker's Delight bit-trick, avoiding the cost of a full 128/64 hardware division.
|
||||
|
||||
`power(f, n)` uses repeated squaring (O(log n) multiplications). `root(f, d)` applies Newton–Raphson iteration until convergence, and `root2` is a specialized square-root shortcut. `power(f, n, d)` combines both for rational exponents.
|
||||
|
||||
## Constructor Design
|
||||
|
||||
The `unchecked{}` tag constructor bypasses normalization entirely. It exists for two legitimate purposes: constructing the compile-time constants `numZero`, `oneSml`, `oneLrg`, and the range sentinels; and at type-conversion boundaries where the caller guarantees the values are already normalized. The `normalized{}` tag constructor forces normalization from an unsigned internal representation and is reserved for unit tests. Regular constructors (taking a signed `int64_t` mantissa) always normalize via `externalToInternal()` + `normalize()`.
|
||||
|
||||
The `implicit` conversion direction is intentional: any integral type or `STAmount` converts *to* `Number` implicitly, while extraction back to `int64_t` is `explicit`. This makes `Number` the natural accumulator type for mixed-mode expressions like `MPTAmount + Number` without risk of silent truncation on the way out.
|
||||
|
||||
## Utility Helpers
|
||||
|
||||
`squelch(x, limit)` returns zero when `abs(x) < limit` and `x` otherwise. This handles the common financial pattern of zeroing out sub-precision residuals that arise from rounding chains.
|
||||
|
||||
`normalizeToRange<T>(minMantissa, maxMantissa)` is a public template that reprojects the `Number` into a caller-supplied integer range, returning a `(mantissa, exponent)` pair. This is used by `STAmount` conversion code to map back from `Number` to specific asset representations without needing `Number` to know about each asset type.
|
||||
|
||||
The `logTen()` and `isPowerOfTen()` constexpr templates at namespace scope verify the `MantissaRange` boundary invariants at compile time via `static_assert`, ensuring that the chosen scale boundaries are always exact powers of ten — a precondition for correct normalization arithmetic.
|
||||
91
include/xrpl/basics/RangeSet.h.ai.json
Normal file
91
include/xrpl/basics/RangeSet.h.ai.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 22,
|
||||
"name": "low"
|
||||
},
|
||||
{
|
||||
"lineno": 22,
|
||||
"name": "high"
|
||||
},
|
||||
{
|
||||
"lineno": 39,
|
||||
"name": "ci"
|
||||
},
|
||||
{
|
||||
"lineno": 54,
|
||||
"name": "rs"
|
||||
},
|
||||
{
|
||||
"lineno": 73,
|
||||
"name": "rs"
|
||||
},
|
||||
{
|
||||
"lineno": 73,
|
||||
"name": "s"
|
||||
},
|
||||
{
|
||||
"lineno": 120,
|
||||
"name": "rs"
|
||||
},
|
||||
{
|
||||
"lineno": 120,
|
||||
"name": "t"
|
||||
},
|
||||
{
|
||||
"lineno": 120,
|
||||
"name": "minVal"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "Provides utilities for working with closed intervals and sets of intervals (ranges) over a domain T, including conversion to/from styled strings and finding missing values in a range set. Uses Boost ICL for interval management.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/RangeSet.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"low",
|
||||
"high"
|
||||
],
|
||||
"lineno": 22,
|
||||
"name": "range"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"ci"
|
||||
],
|
||||
"lineno": 39,
|
||||
"name": "to_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"rs"
|
||||
],
|
||||
"lineno": 54,
|
||||
"name": "to_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"rs",
|
||||
"s"
|
||||
],
|
||||
"lineno": 73,
|
||||
"name": "from_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"rs",
|
||||
"t",
|
||||
"minVal"
|
||||
],
|
||||
"lineno": 120,
|
||||
"name": "prevMissing"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 9,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
include/xrpl/basics/RangeSet.h.ai.md
Normal file
39
include/xrpl/basics/RangeSet.h.ai.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# `include/xrpl/basics/RangeSet.h`
|
||||
|
||||
## Purpose and Context
|
||||
|
||||
This header provides the XRPL ledger's primary abstraction for representing sparse sets of sequence numbers: specifically, which ledger indexes a node has fully acquired and validated. The core data structure, `RangeSet<T>`, is used by `LedgerMaster` to maintain `mCompleteLedgers` — a live record of which historical ledgers are locally available — and supports serialization for network peer advertising and database persistence.
|
||||
|
||||
The design is deliberately thin: both `ClosedInterval<T>` and `RangeSet<T>` are pure type aliases over Boost ICL (`boost::icl::closed_interval` and `boost::icl::interval_set`). All the algebraic machinery — automatic coalescing of adjacent intervals, set difference, containment queries — is delegated directly to Boost ICL, and the header simply layers XRPL-specific string serialization and one domain-relevant query (`prevMissing`) on top.
|
||||
|
||||
## Types and Construction
|
||||
|
||||
`ClosedInterval<T>` represents a single contiguous range `[low, high]` where both endpoints are included. The `range(low, high)` helper exists purely to avoid repeating the template argument when constructing intervals inline — compare `range(10u, 15u)` vs. `ClosedInterval<std::uint32_t>(10u, 15u)`.
|
||||
|
||||
`RangeSet<T>` is an ordered, normalized collection of disjoint `ClosedInterval<T>` objects. The key property Boost ICL provides automatically is coalescing: inserting ledger 6 into `{1-5, 7-10}` yields `{1-10}` with no extra code. This invariant — the set always contains the minimum number of disjoint intervals — is what makes the format both compact in memory and straightforward to serialize.
|
||||
|
||||
## Serialization
|
||||
|
||||
The `to_string`/`from_string` pair implements a human-readable canonical format: `"1-2,4-6,9"` where a single-element interval is written as a bare number and a range uses a dash. `to_string` for an empty set returns `"empty"` rather than an empty string, providing a safe diagnostic representation.
|
||||
|
||||
`from_string` is more carefully designed. It bears the `[[nodiscard]]` attribute, forcing callers to check success. On any parse failure — an unrecognized token, a lexical cast that fails, a dash-split producing more than two parts — the function immediately clears the output set and returns `false`. This all-or-nothing contract means the output `RangeSet` is never left in a partial or corrupt state, which matters because a partial ledger set could cause `LedgerMaster` to believe it has acquired ledgers it hasn't.
|
||||
|
||||
Parsing uses `beast::lexicalCastChecked` for safe numeric conversion rather than `std::stoi` or `atoi`, which would silently truncate or throw on bad input. The loop eagerly clears `intervals` between tokens, avoiding stale data from a previous successful parse contaminating the next one.
|
||||
|
||||
## `prevMissing`: Gap-Driven Acquisition
|
||||
|
||||
The most algorithmically interesting function is `prevMissing`. Given a `RangeSet`, a target value `t`, and a lower bound `minVal`, it returns the largest value strictly less than `t` that is **not** in the set — that is, the largest gap below the query point.
|
||||
|
||||
This is the engine behind `LedgerMaster`'s historical ledger acquisition loop. When the node is filling in its ledger history, it repeatedly calls `prevMissing(mCompleteLedgers, maxVal)` to find the next sequence it still needs to fetch. Scanning backward from the most recent known ledger and prioritizing the largest missing sequence number is an efficient greedy strategy: it minimizes the number of passes needed to converge on a contiguous range.
|
||||
|
||||
The implementation is elegant: rather than iterating candidate values, it constructs the interval `[minVal, t-1]`, subtracts the existing set from it (Boost ICL set-difference), and returns the last element of the complement. This computes the answer in terms of interval arithmetic rather than element-by-element search, making it efficient even when the set contains thousands of intervals.
|
||||
|
||||
The two early-exit conditions — empty set (everything is missing, so answer is `t-1`) and `t == minVal` (no valid predecessor exists) — are handled by returning `std::nullopt`, which the caller must check before dereferencing.
|
||||
|
||||
## Concurrency Notes
|
||||
|
||||
`RangeSet` itself has no internal synchronization. In `LedgerMaster`, every access to `mCompleteLedgers` is guarded by a separate `mCompleteLock` mutex — a deliberate externalized-locking design that keeps the data structure lightweight and composable while still protecting concurrent reads and writes during ledger validation, gap fill, and peer advertisement.
|
||||
|
||||
## Relationship to Adjacent Code
|
||||
|
||||
`RelationalDatabase.h` includes this header, indicating that ledger range information flows through the database layer as well — sequences of complete ledger ranges are stored and queried as part of persistent state. The string format serves double duty as both a wire representation for peer protocol messages and a storage format for the database, making `to_string`/`from_string` round-trip fidelity essential.
|
||||
69
include/xrpl/basics/Resolver.h.ai.json
Normal file
69
include/xrpl/basics/Resolver.h.ai.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 27,
|
||||
"name": "names"
|
||||
},
|
||||
{
|
||||
"lineno": 27,
|
||||
"name": "handler"
|
||||
},
|
||||
{
|
||||
"lineno": 32,
|
||||
"name": "names"
|
||||
},
|
||||
{
|
||||
"lineno": 32,
|
||||
"name": "handler"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 7,
|
||||
"name": "Resolver"
|
||||
}
|
||||
],
|
||||
"description": "Defines the xrpl::Resolver abstract class for asynchronous and synchronous DNS resolution, including start/stop and resolve methods.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Resolver.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 15,
|
||||
"name": "stop_async"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 18,
|
||||
"name": "stop"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 21,
|
||||
"name": "start"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"names",
|
||||
"handler"
|
||||
],
|
||||
"lineno": 27,
|
||||
"name": "resolve"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"names",
|
||||
"handler"
|
||||
],
|
||||
"lineno": 32,
|
||||
"name": "resolve"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 5,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
include/xrpl/basics/Resolver.h.ai.md
Normal file
33
include/xrpl/basics/Resolver.h.ai.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# `include/xrpl/basics/Resolver.h`
|
||||
|
||||
## Role and Purpose
|
||||
|
||||
`Resolver.h` defines the `xrpl::Resolver` abstract interface — the single extension point through which the XRPL node resolves DNS hostnames at runtime. Its job is narrow: given a batch of `host:port` strings, asynchronously resolve each one into a list of `beast::IP::Endpoint` values and deliver the results to a caller-supplied callback. The interface abstracts away the underlying I/O mechanism so that production code uses a real Boost.Asio resolver while tests can substitute a mock without touching callers.
|
||||
|
||||
## Interface Design
|
||||
|
||||
The class is intentionally minimal. It declares four pure-virtual methods: `start()`, `stop()`, `stop_async()`, and `resolve()`. These map directly to the lifecycle and operation of a background I/O service. The destructor is pure virtual with a non-inline out-of-line definition (provided in `ResolverAsio.cpp`), which is the standard C++ idiom for giving an abstract base class a vtable anchor without a concrete body in the header.
|
||||
|
||||
The `HandlerType` alias, `std::function<void(std::string, std::vector<beast::IP::Endpoint>)>`, captures the callback contract. Each resolved name triggers one invocation, receiving both the original hostname string and the resulting address list. This pairing matters: callers often need to correlate results back to the name they submitted (e.g., to log which peer seed produced which IP), and re-passing the name through the completion avoids callers having to maintain their own lookup tables.
|
||||
|
||||
## The Template/Virtual Overload Pair
|
||||
|
||||
The `resolve()` method appears twice: once as a `virtual` function taking `HandlerType const&`, and once as a non-virtual `template<class Handler>` that wraps any callable into `HandlerType` before forwarding to the virtual overload. This is the non-virtual interface (NVI) pattern applied to templates. The motivation is that if the template version were virtual, every instantiation would need a vtable entry — impractical for a polymorphic class. Instead, the template normalises the input type, and the virtual function carries the actual polymorphic dispatch. Callers get the convenience of passing lambdas directly without boilerplate `std::function` construction.
|
||||
|
||||
## Lifecycle Contract
|
||||
|
||||
The three lifecycle methods reflect a service that runs on a shared Boost.Asio `io_context`. `start()` registers the resolver's reference in the pending I/O counter; `stop_async()` posts a cancellation request to the resolver's strand and returns immediately; `stop()` combines `stop_async()` with a blocking wait on a condition variable until all in-flight handlers drain. This two-phase shutdown pattern lets callers choose between fire-and-forget teardown (during orderly application shutdown where the io_context will be drained anyway) and synchronous teardown (when you need a hard guarantee that the resolver is idle before proceeding).
|
||||
|
||||
## Concrete Implementation: `ResolverAsioImpl`
|
||||
|
||||
The only production implementation is `ResolverAsioImpl`, which lives entirely inside `ResolverAsio.cpp` and is exposed only through the `ResolverAsio::New()` factory. This internal linkage is deliberate: the implementation is never constructed directly; ownership flows through `std::unique_ptr<ResolverAsio>`.
|
||||
|
||||
`ResolverAsioImpl` inherits from both `ResolverAsio` (which extends `Resolver`) and `AsyncObject<ResolverAsioImpl>`, a CRTP mixin that reference-counts outstanding completion handlers via an atomic integer. When the count drops to zero, `asyncHandlersComplete()` fires and notifies the condition variable that `stop()` is waiting on. The `CompletionCounter` RAII type is bound into every async handler so the count is maintained correctly even under cancellation paths.
|
||||
|
||||
Work items are queued as `Work` structs in a `std::deque<Work>`. A subtle optimisation: names within a `Work` item are stored in reverse order (via `std::reverse_copy`), so `do_work()` pops from the back of the vector in O(1) rather than the front. The strand ensures that the queue is only ever accessed from the `io_context` thread, making no additional locking necessary on the deque itself.
|
||||
|
||||
The `parseName()` helper handles two cases: if the string parses as a fully-qualified `beast::IP::Endpoint` (a raw IP address with port), it extracts the components directly without a DNS lookup. Otherwise it falls back to splitting on whitespace and `:` delimiters. This means callers can freely mix hostnames, plain IP addresses, and `host port` strings in the same batch.
|
||||
|
||||
## Usage in Context
|
||||
|
||||
`Application` holds a `std::unique_ptr<ResolverAsio>` created at startup and passed to `OverlayImpl`. The overlay calls `resolve()` twice during startup: once for the hardcoded bootstrap IPs (including well-known XRPL Commons hub addresses) and once for the `[ips_fixed]` entries from the node's config file. Both calls use lambdas that convert the resulting `beast::IP::Endpoint` addresses into the PeerFinder's known-peers list. No caller ever interacts with the concrete `ResolverAsioImpl` type — all access flows through the `Resolver` interface, keeping the overlay's dependency on DNS mechanics entirely behind the abstraction.
|
||||
34
include/xrpl/basics/ResolverAsio.h.ai.json
Normal file
34
include/xrpl/basics/ResolverAsio.h.ai.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 7,
|
||||
"name": "ResolverAsio"
|
||||
}
|
||||
],
|
||||
"description": "Defines the ResolverAsio class, an implementation of the Resolver interface using Boost.Asio for asynchronous network resolution in the xrpl namespace.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/ResolverAsio.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 9,
|
||||
"name": "ResolverAsio"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"boost::asio::io_context&",
|
||||
"beast::Journal"
|
||||
],
|
||||
"lineno": 12,
|
||||
"name": "New"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 5,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
25
include/xrpl/basics/ResolverAsio.h.ai.md
Normal file
25
include/xrpl/basics/ResolverAsio.h.ai.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# `include/xrpl/basics/ResolverAsio.h`
|
||||
|
||||
## Role in the System
|
||||
|
||||
`ResolverAsio.h` is the public header for the Boost.Asio-backed implementation of the abstract `Resolver` interface. Its purpose is narrow: it introduces one concrete subclass, `ResolverAsio`, and exposes a single static factory method. The header's brevity is intentional — it acts as an opaque handle into a non-trivial implementation that the caller never sees directly.
|
||||
|
||||
## The Abstraction Layer
|
||||
|
||||
`ResolverAsio` extends `Resolver`, the abstract base class defined in `Resolver.h`. That base declares the entire observable contract: `start()`, `stop()`, `stop_async()`, and a templated `resolve()` that accepts a list of hostname strings and a completion handler of type `std::function<void(std::string, std::vector<beast::IP::Endpoint>)>`. By defining `ResolverAsio` as a pure intermediate class (its constructor is `= default` and defaulted) with only a static `New()` factory, the header ensures that client code depends solely on the `Resolver` interface. The concrete work happens exclusively inside `ResolverAsioImpl`, which is defined entirely within `ResolverAsio.cpp` and is therefore invisible to any translation unit that includes this header.
|
||||
|
||||
This two-level separation — abstract `Resolver` base, thin `ResolverAsio` header, hidden `ResolverAsioImpl` body — is a classic pImpl-adjacent pattern. The difference from a true pImpl is that the indirection goes through virtual dispatch rather than a pointer-to-impl member. The effect is the same: implementation details, including the Asio strand, the internal work queue, and the `AsyncObject` mixin, are completely insulated from the public API surface.
|
||||
|
||||
## The Factory and Ownership Model
|
||||
|
||||
`ResolverAsio::New(boost::asio::io_context&, beast::Journal)` returns a `std::unique_ptr<ResolverAsio>`. The calling site in `Application.cpp` stores the result as a member and later accesses it through the `Resolver*` interface. Ownership is unambiguous: whoever holds the `unique_ptr` owns the object and is responsible for calling `stop()` before destruction. The destructor assertions in `ResolverAsioImpl` enforce this — destroying the object with pending I/O or without stopping first triggers `XRPL_ASSERT` failures.
|
||||
|
||||
The `io_context` reference is non-owning and must outlive the resolver. This is a well-understood contract in Asio programming: the context drives all I/O and must not be destroyed before the objects that post work to it.
|
||||
|
||||
## Why a Static Factory Over a Public Constructor?
|
||||
|
||||
Making the constructor `explicit ... = default` while routing all real construction through `New()` ensures that the concrete `ResolverAsioImpl` type — with all its Asio internals — never needs to be named by consumers. This also gives the factory freedom to perform any pre-construction initialization and return the object as the abstract base pointer, guaranteeing that the caller can only interact through the `Resolver` interface without a cast.
|
||||
|
||||
## Relationship to `beast::Journal`
|
||||
|
||||
The `beast::Journal` parameter to `New()` is threaded directly into the implementation for structured logging of resolution queuing, stop events, and parse failures. It is stored by value rather than by reference, which is the standard practice for `Journal` — it is a lightweight handle that is cheap to copy.
|
||||
144
include/xrpl/basics/SHAMapHash.h.ai.json
Normal file
144
include/xrpl/basics/SHAMapHash.h.ai.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 14,
|
||||
"name": "hash"
|
||||
},
|
||||
{
|
||||
"lineno": 47,
|
||||
"name": "os"
|
||||
},
|
||||
{
|
||||
"lineno": 37,
|
||||
"name": "x"
|
||||
},
|
||||
{
|
||||
"lineno": 37,
|
||||
"name": "y"
|
||||
},
|
||||
{
|
||||
"lineno": 57,
|
||||
"name": "h"
|
||||
},
|
||||
{
|
||||
"lineno": 68,
|
||||
"name": "key"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"uint256 const& hash"
|
||||
],
|
||||
"lineno": 10,
|
||||
"name": "SHAMapHash"
|
||||
}
|
||||
],
|
||||
"description": "Defines the SHAMapHash class, which represents the hash of a node or the entire SHAMap in XRPL, providing utility methods and operators for hash manipulation and comparison.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/SHAMapHash.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 13,
|
||||
"name": "SHAMapHash"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"uint256 const& hash"
|
||||
],
|
||||
"lineno": 14,
|
||||
"name": "SHAMapHash"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 18,
|
||||
"name": "as_uint256"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 21,
|
||||
"name": "as_uint256"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 24,
|
||||
"name": "isZero"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 27,
|
||||
"name": "isNonZero"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 30,
|
||||
"name": "signum"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 33,
|
||||
"name": "zero"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SHAMapHash const& x",
|
||||
"SHAMapHash const& y"
|
||||
],
|
||||
"lineno": 37,
|
||||
"name": "operator=="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SHAMapHash const& x",
|
||||
"SHAMapHash const& y"
|
||||
],
|
||||
"lineno": 42,
|
||||
"name": "operator<"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::ostream& os",
|
||||
"SHAMapHash const& x"
|
||||
],
|
||||
"lineno": 47,
|
||||
"name": "operator<<"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SHAMapHash const& x"
|
||||
],
|
||||
"lineno": 52,
|
||||
"name": "to_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"H& h",
|
||||
"SHAMapHash const& x"
|
||||
],
|
||||
"lineno": 57,
|
||||
"name": "hash_append"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SHAMapHash const& x",
|
||||
"SHAMapHash const& y"
|
||||
],
|
||||
"lineno": 63,
|
||||
"name": "operator!="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SHAMapHash const& key"
|
||||
],
|
||||
"lineno": 68,
|
||||
"name": "extract"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 7,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
include/xrpl/basics/SHAMapHash.h.ai.md
Normal file
40
include/xrpl/basics/SHAMapHash.h.ai.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# `SHAMapHash.h` — Strongly-Typed Node Hash for SHAMap
|
||||
|
||||
## Role in the System
|
||||
|
||||
`SHAMapHash` is a thin but intentional strong-typedef wrapper around `uint256` that represents the cryptographic hash of either a single node in the SHAMap radix tree or the hash of the entire map (which is, by definition, the hash of its root node). The XRP Ledger uses `uint256` for many semantically distinct purposes — transaction IDs, account state keys, ledger hashes, and node hashes — so wrapping the SHAMap-specific case in a distinct type lets the compiler enforce that a raw object identifier is never accidentally passed to an API that expects a node hash, or vice versa.
|
||||
|
||||
## Class Design
|
||||
|
||||
The class holds a single private member `hash_` of type `uint256` and provides precisely two construction paths: the default constructor (producing a zero hash), and an `explicit` single-argument constructor from `uint256 const&`. The `explicit` keyword is load-bearing here: it prevents implicit conversions from arbitrary `uint256` values into a `SHAMapHash`, which is the primary value of the wrapper type. Downstream code that wants to create a `SHAMapHash` must state that intention clearly.
|
||||
|
||||
Access to the raw `uint256` value is provided through `as_uint256()`, offered in both const and mutable overloads. The mutable overload exists because certain serialization paths need to compute the hash in place. State-query helpers — `isZero()`, `isNonZero()`, `signum()`, and `zero()` — delegate directly to the corresponding `uint256` methods rather than reimplementing them, keeping the wrapper thin.
|
||||
|
||||
The comparison operators `operator==` and `operator<`, along with `operator<<`, `to_string()`, and the `hash_append()` template, round out the set of operations needed to use `SHAMapHash` in containers, ordered structures, and diagnostic output. These are defined as hidden-friend functions inside the class body, which means they participate in argument-dependent lookup only when one of their arguments is a `SHAMapHash`, preventing accidental overload resolution with other types. `operator!=` is defined as a non-member inline outside the class, delegating to `operator==`.
|
||||
|
||||
## The `extract()` Specialization
|
||||
|
||||
The most architecturally significant piece of this header is the explicit specialization of the `extract()` function template at the bottom of the file:
|
||||
|
||||
```cpp
|
||||
template <>
|
||||
inline std::size_t
|
||||
extract(SHAMapHash const& key)
|
||||
{
|
||||
return *reinterpret_cast<std::size_t const*>(key.as_uint256().data());
|
||||
}
|
||||
```
|
||||
|
||||
This specialization integrates `SHAMapHash` with `partitioned_unordered_map` — a concurrent-access-friendly container that splits its buckets across multiple independent maps, one per hardware thread. The `partitioned_unordered_map::partitioner()` method calls `extract(key) % partitions_` to decide which sub-map owns a given entry. For `SHAMapHash`, the partition selector is simply the first `sizeof(std::size_t)` bytes of the underlying 256-bit hash, reinterpreted as a native integer. Because SHA-512 half-digests (which generate SHAMap node hashes) have uniform bit distribution, this naive prefix extraction yields an even spread across partitions without any additional hashing. This is why the header includes `partitioned_unordered_map.h` at all — not for the map itself, but to place the `extract` specialization in the same translation unit that sees the primary template declaration.
|
||||
|
||||
It is worth noting that the analogous `extract<uint256>` specialization in `base_uint.h` uses `std::memcpy` to avoid potential undefined behavior from unaligned pointer access, whereas this specialization uses a direct `reinterpret_cast`. Both produce the same result on common architectures, but the `memcpy` form is more strictly correct under the C++ aliasing rules.
|
||||
|
||||
## Relationship to the SHAMap Subsystem
|
||||
|
||||
`SHAMapTreeNode` declares a protected `SHAMapHash hash_` member that holds the cached content hash for each tree node. Derived types — `SHAMapInnerNode`, `SHAMapLeafNode`, and their concrete variants — inherit this field and set it during construction or when their content is modified. `SHAMapInnerNode` additionally stores an array of 16 child `SHAMapHash` values inside its `TaggedPointer hashesAndChildren_` structure, enabling hash-based validity checks on child branches without requiring the child node to be loaded into memory.
|
||||
|
||||
At the `SHAMap` level, `SHAMapHash` appears as the key type for cache lookups (`cacheLookup`, `canonicalize`), for fetching missing nodes from the database or peer-provided data (`fetchNodeNT`, `fetchNodeFromDB`), and as the return type of `getHash()` which exposes the map's current root hash. The `Delta` structure tracks nodes that were modified between two map versions, and the missing-node set inside `SHAMap::DeltaFinder` is typed as `std::set<SHAMapHash>`, again relying on `operator<` for ordering. `SHAMapMissingNode` stores a `SHAMapHash` to report which hash was absent when a synchronization fetch failed, making it useful for targeted peer requests during ledger sync.
|
||||
|
||||
## Summary
|
||||
|
||||
`SHAMapHash` is a compact but effective type-safety boundary: it costs nothing at runtime — no vtable, no additional storage, no indirection — while preventing the kind of silent `uint256` category confusion that would otherwise be possible in a codebase that uses the same underlying 32-byte type for many distinct protocol-level concepts. Its `extract()` specialization is a deliberate hook into the partitioned hash map infrastructure, enabling lock-striped concurrent node caching without requiring any changes to the container itself.
|
||||
136
include/xrpl/basics/SharedWeakCachePointer.h.ai.json
Normal file
136
include/xrpl/basics/SharedWeakCachePointer.h.ai.json
Normal file
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"SharedWeakCachePointer()",
|
||||
"SharedWeakCachePointer(SharedWeakCachePointer const& rhs)",
|
||||
"SharedWeakCachePointer(std::shared_ptr<TT> const& rhs)",
|
||||
"SharedWeakCachePointer(SharedWeakCachePointer&& rhs)",
|
||||
"SharedWeakCachePointer(std::shared_ptr<TT>&& rhs)"
|
||||
],
|
||||
"lineno": 14,
|
||||
"name": "SharedWeakCachePointer"
|
||||
}
|
||||
],
|
||||
"description": "Defines the SharedWeakCachePointer template class, a wrapper around std::variant<std::shared_ptr, std::weak_ptr> for efficient storage and management of intrusive pointers in caches.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/SharedWeakCachePointer.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"SharedWeakCachePointer const& rhs"
|
||||
],
|
||||
"lineno": 18,
|
||||
"name": "SharedWeakCachePointer"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::shared_ptr<TT> const& rhs"
|
||||
],
|
||||
"lineno": 22,
|
||||
"name": "SharedWeakCachePointer"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SharedWeakCachePointer&& rhs"
|
||||
],
|
||||
"lineno": 25,
|
||||
"name": "SharedWeakCachePointer"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::shared_ptr<TT>&& rhs"
|
||||
],
|
||||
"lineno": 28,
|
||||
"name": "SharedWeakCachePointer"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SharedWeakCachePointer const& rhs"
|
||||
],
|
||||
"lineno": 31,
|
||||
"name": "operator="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::shared_ptr<TT> const& rhs"
|
||||
],
|
||||
"lineno": 35,
|
||||
"name": "operator="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::shared_ptr<TT>&& rhs"
|
||||
],
|
||||
"lineno": 39,
|
||||
"name": "operator="
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 42,
|
||||
"name": "~SharedWeakCachePointer"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 48,
|
||||
"name": "getStrong"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 54,
|
||||
"name": "operator bool"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 61,
|
||||
"name": "reset"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 68,
|
||||
"name": "get"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 74,
|
||||
"name": "use_count"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 79,
|
||||
"name": "expired"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 85,
|
||||
"name": "lock"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 90,
|
||||
"name": "isStrong"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 94,
|
||||
"name": "isWeak"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 99,
|
||||
"name": "convertToStrong"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 107,
|
||||
"name": "convertToWeak"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 6,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
43
include/xrpl/basics/SharedWeakCachePointer.h.ai.md
Normal file
43
include/xrpl/basics/SharedWeakCachePointer.h.ai.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# `SharedWeakCachePointer<T>` — Single-Slot Strong/Weak Pointer for Tagged Caches
|
||||
|
||||
## Purpose and Motivation
|
||||
|
||||
`SharedWeakCachePointer<T>` solves a memory layout problem specific to the XRPL ledger's `TaggedCache` subsystem. A tagged cache must track every live object it has ever handed out, even after evicting that object from its hot tier. The naïve representation would store both a `std::shared_ptr<T>` (the strong reference that keeps the object alive while it is cached) and a `std::weak_ptr<T>` (the tracking reference after eviction) side-by-side in each cache entry. That wastes an entire control-block pointer per entry: both smart-pointer types are the same size internally, and only one is ever meaningful at a time.
|
||||
|
||||
This class resolves that by wrapping `std::variant<std::shared_ptr<T>, std::weak_ptr<T>>` in a single object. At any moment the cache entry holds *either* a strong pointer *or* a weak pointer — never both, never neither (after construction). The variant's discriminator bit is all that's needed to distinguish the two states.
|
||||
|
||||
## State Model
|
||||
|
||||
The class has two logical states and explicit transitions between them:
|
||||
|
||||
**Strong state** — the variant holds a `std::shared_ptr<T>`. The cache entry is "hot": it keeps the referenced object alive independently of any caller. `isStrong()` returns `true`, and `getStrong()` returns the `shared_ptr` by const reference without any atomic increment.
|
||||
|
||||
**Weak state** — the variant holds a `std::weak_ptr<T>`. The cache entry is "tracking": the object stays alive only as long as some caller holds its own `shared_ptr`. `isWeak()` returns `true`. Whether the tracked object still exists must be determined by calling `lock()` or `expired()`.
|
||||
|
||||
`convertToStrong()` atomically attempts to promote a weak entry to strong by calling `weak_ptr::lock()`. If the referent has already been destroyed it returns `false` and the entry remains weak/expired. `convertToWeak()` demotes a strong entry by constructing a `weak_ptr` from the held `shared_ptr` and replacing the variant alternative. Both transitions are in-place — no allocation or deallocation, just variant reassignment.
|
||||
|
||||
## Relationship to `TaggedCache`
|
||||
|
||||
`TaggedCache` is the direct consumer of this class. Its inner `ValueEntry` stores a `shared_weak_combo_pointer_type` (defaulted to `SharedWeakCachePointer<T>`) alongside a `last_access` timestamp. `ValueEntry` delegates the strong/weak distinction entirely to the wrapper:
|
||||
|
||||
```cpp
|
||||
bool isCached() const { return ptr && ptr.isStrong(); }
|
||||
bool isWeak() const { if (!ptr) return true; return ptr.isWeak(); }
|
||||
bool isExpired() const { return ptr.expired(); }
|
||||
```
|
||||
|
||||
During `sweep()`, the cache iterates its entries and calls `convertToWeak()` on entries whose `last_access` has aged out. Entries that are already weak and whose `expired()` is `true` are removed from the map entirely. This two-phase lifecycle — strong while hot, weak while tracked — is the reason the pointer needs to change state in place rather than being replaced by a different type.
|
||||
|
||||
## Accessor Semantics
|
||||
|
||||
`lock()` is the general-purpose accessor. It works regardless of current state: it returns the held `shared_ptr` directly if the variant is strong, or calls `weak_ptr::lock()` if it is weak. `getStrong()` is a cheaper alternative when the caller already knows or expects the strong state; it returns the `shared_ptr` by const reference (avoiding a reference-count increment) and returns a static empty `shared_ptr` if the variant is currently weak.
|
||||
|
||||
`operator bool()` deserves a note: it returns `true` when the variant's active alternative is `std::shared_ptr<T>`, even if that `shared_ptr` is null (e.g., after `reset()`). It does *not* dereference the pointer. This is why `TaggedCache::ValueEntry::isCached()` combines both `ptr &&` (variant is in shared-state) and `ptr.isStrong()` (the shared pointer is non-null). `isStrong()` explicitly checks `p->get() != nullptr` and is therefore the correct predicate when null-ness matters.
|
||||
|
||||
## Template Constraints and Covariance
|
||||
|
||||
All constructors and assignment operators that accept a `std::shared_ptr<TT>` carry the constraint `requires std::convertible_to<TT*, T*>`. This permits initialising a `SharedWeakCachePointer<Base>` from a `shared_ptr<Derived>` while preventing accidental narrowing conversions. Move overloads are provided alongside copy overloads to avoid unnecessary reference-count increments when a temporary `shared_ptr` is being transferred into the cache entry.
|
||||
|
||||
## Implementation Split
|
||||
|
||||
The class declaration lives in `SharedWeakCachePointer.h`, with all method bodies in `SharedWeakCachePointer.ipp`. `TaggedCache.h` includes the `.ipp` directly, which is the standard XRPL pattern for template implementations that are only needed by a single well-known consumer. This keeps the header lean and makes the dependency relationship explicit: if you include `TaggedCache.h` you get the implementation; including only the header gives you the interface for forward-declaration purposes.
|
||||
395
include/xrpl/basics/SharedWeakCachePointer.ipp.ai.json
Normal file
395
include/xrpl/basics/SharedWeakCachePointer.ipp.ai.json
Normal file
@@ -0,0 +1,395 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [],
|
||||
"code_paths": [
|
||||
{
|
||||
"call_chain": [
|
||||
"SharedWeakCachePointer<T>::SharedWeakCachePointer(std::shared_ptr<TT> const& rhs)"
|
||||
],
|
||||
"entry_point": "SharedWeakCachePointer<T>::SharedWeakCachePointer(std::shared_ptr<TT> const& rhs)",
|
||||
"purpose": "Constructs a SharedWeakCachePointer from a std::shared_ptr<TT> if TT* is convertible to T*.",
|
||||
"validation_points": [
|
||||
"C++20 concept: requires std::convertible_to<TT*, T*> (compile-time validation of pointer type compatibility)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"SharedWeakCachePointer<T>::operator=(std::shared_ptr<TT> const& rhs)"
|
||||
],
|
||||
"entry_point": "SharedWeakCachePointer<T>::operator=(std::shared_ptr<TT> const& rhs)",
|
||||
"purpose": "Assigns a std::shared_ptr<TT> to the SharedWeakCachePointer if TT* is convertible to T*.",
|
||||
"validation_points": [
|
||||
"C++20 concept: requires std::convertible_to<TT*, T*> (compile-time validation of pointer type compatibility)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"SharedWeakCachePointer<T>::SharedWeakCachePointer(std::shared_ptr<TT>&& rhs)"
|
||||
],
|
||||
"entry_point": "SharedWeakCachePointer<T>::SharedWeakCachePointer(std::shared_ptr<TT>&& rhs)",
|
||||
"purpose": "Move-constructs a SharedWeakCachePointer from a std::shared_ptr<TT> if TT* is convertible to T*.",
|
||||
"validation_points": [
|
||||
"C++20 concept: requires std::convertible_to<TT*, T*> (compile-time validation of pointer type compatibility)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"SharedWeakCachePointer<T>::operator=(std::shared_ptr<TT>&& rhs)"
|
||||
],
|
||||
"entry_point": "SharedWeakCachePointer<T>::operator=(std::shared_ptr<TT>&& rhs)",
|
||||
"purpose": "Move-assigns a std::shared_ptr<TT> to the SharedWeakCachePointer if TT* is convertible to T*.",
|
||||
"validation_points": [
|
||||
"C++20 concept: requires std::convertible_to<TT*, T*> (compile-time validation of pointer type compatibility)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"SharedWeakCachePointer<T>::convertToStrong()",
|
||||
"SharedWeakCachePointer<T>::isStrong()",
|
||||
"std::get_if<std::shared_ptr<T>>(&combo_)"
|
||||
],
|
||||
"entry_point": "SharedWeakCachePointer<T>::convertToStrong()",
|
||||
"purpose": "Converts the internal state to a strong pointer if possible.",
|
||||
"validation_points": [
|
||||
"Checks if combo_ holds a std::weak_ptr<T> and if it can be locked to a std::shared_ptr<T>."
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"SharedWeakCachePointer<T>::convertToWeak()",
|
||||
"SharedWeakCachePointer<T>::isWeak()",
|
||||
"SharedWeakCachePointer<T>::isStrong()",
|
||||
"std::get_if<std::shared_ptr<T>>(&combo_)"
|
||||
],
|
||||
"entry_point": "SharedWeakCachePointer<T>::convertToWeak()",
|
||||
"purpose": "Converts the internal state to a weak pointer if possible.",
|
||||
"validation_points": [
|
||||
"Checks if combo_ holds a std::shared_ptr<T> and converts it to std::weak_ptr<T>."
|
||||
]
|
||||
}
|
||||
],
|
||||
"data_flows": [
|
||||
{
|
||||
"field": "combo_",
|
||||
"flow": [
|
||||
"Constructor (combo_ = rhs or combo_ = std::move(rhs))",
|
||||
"Assignment operator (combo_ = rhs or combo_ = std::move(rhs))",
|
||||
"Accessed by getStrong(), lock(), isStrong(), isWeak(), expired(), use_count(), get(), convertToStrong(), convertToWeak(), reset()"
|
||||
],
|
||||
"origin": "Initialized in constructors (from std::shared_ptr<TT> or std::weak_ptr<T>)",
|
||||
"transformations": [
|
||||
"Set to std::shared_ptr<T> or std::weak_ptr<T> depending on input",
|
||||
"Moved or copied as needed",
|
||||
"Converted between strong/weak via convertToStrong/convertToWeak"
|
||||
],
|
||||
"validated_at": "At construction/assignment: requires std::convertible_to<TT*, T*> (compile-time); at runtime: type checked via std::get_if"
|
||||
},
|
||||
{
|
||||
"field": "std::shared_ptr<TT> rhs (constructor/assignment input)",
|
||||
"flow": [
|
||||
"Passed to constructor or assignment operator",
|
||||
"combo_ = rhs or combo_ = std::move(rhs)"
|
||||
],
|
||||
"origin": "User code or upstream function passes in a shared_ptr",
|
||||
"transformations": [
|
||||
"Type-checked at compile time for pointer compatibility",
|
||||
"Stored in combo_ as std::shared_ptr<T>"
|
||||
],
|
||||
"validated_at": "Compile-time: requires std::convertible_to<TT*, T*>"
|
||||
},
|
||||
{
|
||||
"field": "std::weak_ptr<T> (internal state)",
|
||||
"flow": [
|
||||
"convertToWeak() checks if combo_ holds std::shared_ptr<T>",
|
||||
"If so, combo_ = std::weak_ptr<T>(*p)"
|
||||
],
|
||||
"origin": "Created in convertToWeak() from std::shared_ptr<T>",
|
||||
"transformations": [
|
||||
"Conversion from strong to weak pointer"
|
||||
],
|
||||
"validated_at": "Runtime: only if combo_ holds std::shared_ptr<T>"
|
||||
}
|
||||
],
|
||||
"description": "Implements the methods for the SharedWeakCachePointer template class, which wraps a std::variant of std::shared_ptr and std::weak_ptr to efficiently manage strong and weak references for caching purposes.",
|
||||
"false_positive_patterns": [
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"input_validation"
|
||||
],
|
||||
"confidence": 0.9,
|
||||
"detection_keywords": [
|
||||
"TT* convertible to T* (template parameter type)",
|
||||
"validation",
|
||||
"missing",
|
||||
"check"
|
||||
],
|
||||
"evidence": "Field TT* convertible to T* (template parameter type) validated by C++20 concepts (std::convertible_to)",
|
||||
"issue_pattern": "Missing validation for TT* convertible to T* (template parameter type)",
|
||||
"why_false_positive": "C++20 concepts (std::convertible_to) validates TT* convertible to T* (template parameter type) automatically"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"null_empty"
|
||||
],
|
||||
"confidence": 1.0,
|
||||
"detection_keywords": [
|
||||
"TT* convertible to T* (template parameter type)",
|
||||
"empty",
|
||||
"string",
|
||||
"validation"
|
||||
],
|
||||
"evidence": "C++20 concepts (std::convertible_to<TT*, T*>) at template constructors and assignment operators (e.g., SharedWeakCachePointer(std::shared_ptr<TT> const& rhs))",
|
||||
"issue_pattern": "Missing empty string validation for TT* convertible to T* (template parameter type)",
|
||||
"why_false_positive": "C++20 concepts (std::convertible_to<TT*, T*>) validates TT* convertible to T* (template parameter type) for empty strings"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"validation",
|
||||
"type_safety"
|
||||
],
|
||||
"confidence": 0.85,
|
||||
"detection_keywords": [
|
||||
"TT* convertible to T* (template parameter type)",
|
||||
"type",
|
||||
"validation",
|
||||
"check"
|
||||
],
|
||||
"evidence": "C++20 concepts (std::convertible_to<TT*, T*>) at template constructors and assignment operators (e.g., SharedWeakCachePointer(std::shared_ptr<TT> const& rhs))",
|
||||
"issue_pattern": "Missing type validation for TT* convertible to T* (template parameter type)",
|
||||
"why_false_positive": "C++20 concepts (std::convertible_to<TT*, T*>) validates TT* convertible to T* (template parameter type) type"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"null_check",
|
||||
"memory_safety"
|
||||
],
|
||||
"confidence": 0.9,
|
||||
"detection_keywords": [
|
||||
"null",
|
||||
"nullptr",
|
||||
"check",
|
||||
"std::shared_ptr"
|
||||
],
|
||||
"evidence": "Code uses std::shared_ptr",
|
||||
"issue_pattern": "Missing null check for std::shared_ptr",
|
||||
"why_false_positive": "RAII smart pointers guarantee initialization"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"memory_safety",
|
||||
"resource_leak"
|
||||
],
|
||||
"confidence": 0.85,
|
||||
"detection_keywords": [
|
||||
"memory",
|
||||
"leak",
|
||||
"delete"
|
||||
],
|
||||
"evidence": "Code uses std::shared_ptr",
|
||||
"issue_pattern": "Memory leak - missing delete",
|
||||
"why_false_positive": "Smart pointer handles cleanup automatically"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"null_check",
|
||||
"memory_safety"
|
||||
],
|
||||
"confidence": 0.9,
|
||||
"detection_keywords": [
|
||||
"null",
|
||||
"nullptr",
|
||||
"check",
|
||||
"std::weak_ptr"
|
||||
],
|
||||
"evidence": "Code uses std::weak_ptr",
|
||||
"issue_pattern": "Missing null check for std::weak_ptr",
|
||||
"why_false_positive": "RAII smart pointers guarantee initialization"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"memory_safety",
|
||||
"resource_leak"
|
||||
],
|
||||
"confidence": 0.85,
|
||||
"detection_keywords": [
|
||||
"memory",
|
||||
"leak",
|
||||
"delete"
|
||||
],
|
||||
"evidence": "Code uses std::weak_ptr",
|
||||
"issue_pattern": "Memory leak - missing delete",
|
||||
"why_false_positive": "Smart pointer handles cleanup automatically"
|
||||
}
|
||||
],
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/SharedWeakCachePointer.ipp",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"SharedWeakCachePointer const& rhs"
|
||||
],
|
||||
"lineno": 5,
|
||||
"name": "SharedWeakCachePointer"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::shared_ptr<TT> const& rhs"
|
||||
],
|
||||
"lineno": 9,
|
||||
"name": "SharedWeakCachePointer"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SharedWeakCachePointer&& rhs"
|
||||
],
|
||||
"lineno": 14,
|
||||
"name": "SharedWeakCachePointer"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::shared_ptr<TT>&& rhs"
|
||||
],
|
||||
"lineno": 18,
|
||||
"name": "SharedWeakCachePointer"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"SharedWeakCachePointer const& rhs"
|
||||
],
|
||||
"lineno": 24,
|
||||
"name": "operator="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::shared_ptr<TT> const& rhs"
|
||||
],
|
||||
"lineno": 29,
|
||||
"name": "operator="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::shared_ptr<TT>&& rhs"
|
||||
],
|
||||
"lineno": 36,
|
||||
"name": "operator="
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 43,
|
||||
"name": "~SharedWeakCachePointer"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 48,
|
||||
"name": "getStrong"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 58,
|
||||
"name": "operator bool"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 63,
|
||||
"name": "reset"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 68,
|
||||
"name": "get"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 73,
|
||||
"name": "use_count"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 81,
|
||||
"name": "expired"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 89,
|
||||
"name": "lock"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 99,
|
||||
"name": "isStrong"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 106,
|
||||
"name": "isWeak"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 111,
|
||||
"name": "convertToStrong"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 126,
|
||||
"name": "convertToWeak"
|
||||
}
|
||||
],
|
||||
"language": "cpp",
|
||||
"language_patterns": {
|
||||
"exception_patterns": [],
|
||||
"namespace_accessors": [],
|
||||
"raii_usage": [
|
||||
{
|
||||
"audit_implication": "No manual delete needed, no null checks after construction",
|
||||
"false_positive_risk": "Missing null check, memory leak",
|
||||
"pointer_type": "std::shared_ptr",
|
||||
"type": "smart_pointer"
|
||||
},
|
||||
{
|
||||
"audit_implication": "No manual delete needed, no null checks after construction",
|
||||
"false_positive_risk": "Missing null check, memory leak",
|
||||
"pointer_type": "std::weak_ptr",
|
||||
"type": "smart_pointer"
|
||||
}
|
||||
],
|
||||
"smart_pointers": [
|
||||
{
|
||||
"audit_note": "Reference counted, auto cleanup when last ref dropped",
|
||||
"cleanup_needed": false,
|
||||
"false_positive_risk": "Missing null check or manual delete",
|
||||
"null_check_needed": false,
|
||||
"ownership": "shared",
|
||||
"type": "shared_ptr"
|
||||
}
|
||||
],
|
||||
"template_validation": []
|
||||
},
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 4,
|
||||
"name": "xrpl"
|
||||
}
|
||||
],
|
||||
"test_coverage_notes": "There is no direct evidence of test files in this code snippet. Typically, tests for SharedWeakCachePointer would be found in files like 'SharedWeakCachePointer_test.cpp' or similar in the test/unit directory. The main validation (std::convertible_to) is a compile-time check and would be tested by attempting to instantiate SharedWeakCachePointer with various pointer types. Runtime behaviors (convertToStrong, convertToWeak, lock, expired, etc.) should be covered by unit tests that check state transitions and pointer validity. Gaps: If tests do not attempt invalid type conversions, the compile-time validation may not be explicitly tested. Also, edge cases (e.g., null pointers, expired weak pointers) should be tested to ensure correct runtime behavior.",
|
||||
"validation_architecture": {
|
||||
"auto_validated_fields": [
|
||||
"TT* convertible to T* (template parameter type)"
|
||||
],
|
||||
"framework": "C++20 concepts (std::convertible_to)",
|
||||
"validation_layer": "compile-time (template instantiation/type checking)"
|
||||
},
|
||||
"validations": [
|
||||
{
|
||||
"confidence": 1.0,
|
||||
"error_thrown": "Compilation error (static_assertion failure, concept not satisfied)",
|
||||
"field": "TT* convertible to T* (template parameter type)",
|
||||
"location": "template constructors and assignment operators (e.g., SharedWeakCachePointer(std::shared_ptr<TT> const& rhs))",
|
||||
"validated_by": "C++20 concepts (std::convertible_to<TT*, T*>)",
|
||||
"validates": [
|
||||
"Ensures TT* can be converted to T* before allowing construction or assignment"
|
||||
],
|
||||
"validation_type": "type"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
include/xrpl/basics/SharedWeakCachePointer.ipp.ai.md
Normal file
39
include/xrpl/basics/SharedWeakCachePointer.ipp.ai.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# `SharedWeakCachePointer.ipp` — Template Method Implementations
|
||||
|
||||
## Role in the System
|
||||
|
||||
This file provides the out-of-line method implementations for `SharedWeakCachePointer<T>`, declared in `SharedWeakCachePointer.h`. Because the class is a template, the definitions live in a `.ipp` file rather than a `.cpp` file; `TaggedCache.h` `#include`s this `.ipp` directly to ensure the definitions are available at the point of instantiation.
|
||||
|
||||
`SharedWeakCachePointer<T>` is the core pointer abstraction that makes `TaggedCache`'s two-tier lifecycle work. Each `ValueEntry` in the cache map holds one of these, and the variant state — strong or weak — encodes which tier an object occupies: strongly-referenced objects are "hot" and kept alive by the cache itself; weakly-referenced objects are "warm" and tracked but not kept alive, surviving only as long as external code holds a `shared_ptr` to them.
|
||||
|
||||
## Internal Representation
|
||||
|
||||
The private `combo_` member is a `std::variant<std::shared_ptr<T>, std::weak_ptr<T>>`. The design comment in the header is precise: this uses less memory than holding both pointers simultaneously. Because the variant can only hold one alternative at a time, storage is the size of the larger alternative plus discriminant overhead — a meaningful saving in a map that may contain millions of ledger-object entries.
|
||||
|
||||
The variant discriminant is interrogated throughout via `std::get_if`, which returns a pointer to the held alternative (or `nullptr` if the wrong alternative is active), avoiding exceptions and enabling the no-throw invariants.
|
||||
|
||||
## Lifecycle Management: `convertToStrong` and `convertToWeak`
|
||||
|
||||
These two methods are the reason the class exists. `convertToWeak()` upgrades a hot cache entry to a warm one by replacing the `shared_ptr` alternative with a `std::weak_ptr<T>` constructed from it, releasing the cache's own ownership stake. If the object still has external owners, `weak_ptr::lock()` will continue to succeed; once all external owners drop their handles the object is collected and subsequent `lock()` calls return null.
|
||||
|
||||
`convertToStrong()` does the reverse during a cache-hit path: it calls `weak_ptr::lock()` and, if successful, replaces the variant with the resulting `shared_ptr`, reasserting the cache's ownership. Both methods are idempotent — calling them when already in the target state is a harmless no-op returning `true`.
|
||||
|
||||
`TaggedCache::sweepHelper()` depends on this pair to implement the sweep lifecycle without erasing entries from the map: entries that haven't been accessed recently are demoted to weak; entries whose weak pointer has also expired are removed entirely.
|
||||
|
||||
## Accessor Semantics
|
||||
|
||||
**`getStrong()` vs `lock()`** serve different call sites. `getStrong()` returns a `const&` to the held `shared_ptr` — if the variant is in the weak state it returns a reference to a static empty `shared_ptr`. This avoids incrementing the reference count and is appropriate when the caller already knows the pointer is strong (e.g., during a cache insertion). `lock()` unconditionally produces a new owning `shared_ptr` by either copying the strong alternative or calling `weak_ptr::lock()`, and is the safe choice when the strength is uncertain.
|
||||
|
||||
**`operator bool()`** returns `true` only if the variant holds a `shared_ptr` alternative and that pointer is non-null. A slot in weak state always returns `false`, which is deliberately asymmetric: code that checks `if (entry.ptr)` treats weak entries the same as empty entries, simplifying `TaggedCache::ValueEntry::isCached()` and `isWeak()`.
|
||||
|
||||
**`expired()`** has a slightly subtle implementation: for the `weak_ptr` alternative it delegates to `weak_ptr::expired()`; for the `shared_ptr` alternative it returns `false` (a live strong pointer is never expired). A null strong pointer — i.e., a `shared_ptr{}` stored after `reset()` — falls through to the final `return !std::get_if<std::shared_ptr<T>>(&combo_)`, which evaluates to `true` because `get_if` returns a non-null pointer to the held (but null-content) `shared_ptr`. This edge case correctly marks a reset slot as expired.
|
||||
|
||||
**`reset()`** explicitly stores a default-constructed `shared_ptr<T>{}` into the variant rather than relying on any implicit state. This ensures the variant is in the `shared_ptr` alternative (the "null strong" state), preventing future calls from hitting the `weak_ptr` branch unexpectedly.
|
||||
|
||||
## Type Safety via C++20 Concepts
|
||||
|
||||
Every constructor and assignment operator that accepts a `shared_ptr<TT>` is gated by `requires std::convertible_to<TT*, T*>`. This enforces covariance at compile time: you can construct a `SharedWeakCachePointer<Base>` from a `shared_ptr<Derived>` only if `Derived*` is implicitly convertible to `Base*`. No runtime check or `dynamic_cast` is involved. This mirrors the implicit conversions already present in `std::shared_ptr`'s own converting constructors, maintaining a familiar interface contract.
|
||||
|
||||
## Concurrency Considerations
|
||||
|
||||
`SharedWeakCachePointer` itself carries no internal lock. Concurrent mutation is the responsibility of the surrounding `TaggedCache`, which guards all access to `ValueEntry::ptr` through its `m_mutex`. The lack of internal synchronization is intentional: a per-pointer lock would add overhead to every cache access for a case where the surrounding container already serializes operations.
|
||||
110
include/xrpl/basics/SlabAllocator.h.ai.json
Normal file
110
include/xrpl/basics/SlabAllocator.h.ai.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 17,
|
||||
"name": "Type"
|
||||
},
|
||||
{
|
||||
"lineno": 209,
|
||||
"name": "Type"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"extra",
|
||||
"alloc",
|
||||
"align"
|
||||
],
|
||||
"lineno": 18,
|
||||
"name": "SlabAllocator"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"next",
|
||||
"data",
|
||||
"size",
|
||||
"item"
|
||||
],
|
||||
"lineno": 24,
|
||||
"name": "SlabBlock"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"cfg"
|
||||
],
|
||||
"lineno": 210,
|
||||
"name": "SlabAllocatorSet"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"extra_",
|
||||
"alloc_",
|
||||
"align_"
|
||||
],
|
||||
"lineno": 222,
|
||||
"name": "SlabConfig"
|
||||
}
|
||||
],
|
||||
"description": "Implements a slab allocator and a set of slab allocators for efficient memory management of fixed-size objects, with support for alignment and extra bytes, intended for use in the XRPL codebase.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/SlabAllocator.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"p"
|
||||
],
|
||||
"lineno": 54,
|
||||
"name": "own"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 59,
|
||||
"name": "allocate"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"ptr"
|
||||
],
|
||||
"lineno": 77,
|
||||
"name": "deallocate"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 120,
|
||||
"name": "size"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 129,
|
||||
"name": "allocate"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"ptr"
|
||||
],
|
||||
"lineno": 186,
|
||||
"name": "deallocate"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"extra"
|
||||
],
|
||||
"lineno": 246,
|
||||
"name": "allocate"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"ptr"
|
||||
],
|
||||
"lineno": 266,
|
||||
"name": "deallocate"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 15,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
84
include/xrpl/basics/SlabAllocator.h.ai.md
Normal file
84
include/xrpl/basics/SlabAllocator.h.ai.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# `SlabAllocator.h` — Fixed-Size Slab Memory Allocator
|
||||
|
||||
## Why This File Exists
|
||||
|
||||
Standard `malloc`/`new` carries costs that matter at XRPL's throughput: lock contention inside the system allocator, per-object bookkeeping overhead, and heap fragmentation that degrades TLB efficiency over time. `SlabAllocator.h` replaces this with a classic slab strategy: carve a large pre-aligned region into uniform slots, maintain a per-block free list, and serve allocations in O(1) without touching the system heap. The primary consumer is `SHAMapItem` — a high-churn node type that pairs a fixed header with a variable-length data payload living immediately after it in memory.
|
||||
|
||||
## `SlabAllocator<Type>`: Structure and Internals
|
||||
|
||||
### `SlabBlock` — Self-Hosting Memory Region
|
||||
|
||||
The central internal structure is `SlabBlock`. Each slab is a single `boost::alignment::aligned_alloc` call; the `SlabBlock` header is placement-new'd at the very start of that buffer, and the item pool occupies the rest. This self-hosting layout means only one system allocation is needed per slab regardless of how many objects it holds.
|
||||
|
||||
The constructor walks the entire pool and links every item-sized slot into a singly-linked free list. The link pointer is written with `std::memcpy` rather than a direct pointer cast:
|
||||
|
||||
```cpp
|
||||
std::memcpy(data, &l_, sizeof(std::uint8_t*));
|
||||
```
|
||||
|
||||
This idiom appears three times in the file. It is not premature caution — writing through a `uint8_t*` to store a `uint8_t**` value violates strict aliasing rules, which is undefined behavior that optimizers can miscompile in subtle ways. `memcpy` is the standard-blessed type-pun that compilers reliably lower to a plain store.
|
||||
|
||||
Each `SlabBlock` carries its own `std::mutex` protecting only its own free list (`l_`). Per-block rather than per-allocator locking is deliberate: under concurrent load, multiple threads can allocate from different slabs simultaneously without contending. The `own()` method is a pointer range check against `[p_, p_ + size_)` — O(1) and branch-predictor-friendly, relying on the pool's contiguity.
|
||||
|
||||
### Lock-Free Slab Growth
|
||||
|
||||
The set of active `SlabBlock` instances forms a lock-free singly-linked list through `std::atomic<SlabBlock*> slabs_`. When every existing slab is exhausted, `allocate()` allocates a fresh buffer at a **2 MiB boundary** — not an aesthetic choice, but a deliberate alignment to enable Linux transparent huge pages. For allocations ≥ 4 MiB, a `madvise(buf, size, MADV_HUGEPAGE)` hint is issued on Linux, potentially reducing TLB pressure significantly under memory-intensive workloads.
|
||||
|
||||
The new slab is linked with a `compare_exchange_weak` CAS loop:
|
||||
|
||||
```cpp
|
||||
while (!slabs_.compare_exchange_weak(
|
||||
slab->next_, slab, std::memory_order_release, std::memory_order_relaxed))
|
||||
;
|
||||
```
|
||||
|
||||
This is the standard lock-free list prepend. A subtle consequence: two threads could concurrently decide that all existing slabs are exhausted, both allocate new slabs, and both successfully link them. The result is a wasted slab's worth of memory in that rare race. The design explicitly accepts this for the sake of eliminating a global growth lock — correct, since slab growth events are infrequent and slab memory is not small.
|
||||
|
||||
### The Intentional Destructor Leak
|
||||
|
||||
The destructor body is empty with a `FIXME` comment explaining that releasing slab memory at shutdown is unsafe: there is no mechanism to guarantee that objects constructed inside the slab have been destroyed first. XRPL's shutdown model does not provide this guarantee, so the destructor deliberately leaks all slab memory. A controlled leak is far safer than a use-after-free.
|
||||
|
||||
### Constructor Parameters and Disabled Allocators
|
||||
|
||||
`SlabAllocator(extra, alloc, align)` accepts:
|
||||
- `extra`: additional bytes beyond `sizeof(Type)` per slot (for trailing payload)
|
||||
- `alloc`: slab size in bytes; **0 means the allocator is permanently disabled** and will always return `nullptr` — an explicit design affordance for environments needing minimal memory usage
|
||||
- `align`: override for per-slot alignment; defaults to `alignof(Type)`
|
||||
|
||||
`itemSize_` is computed as `align_up(sizeof(Type) + extra, itemAlignment_)`, ensuring every slot satisfies the type's alignment requirements including any trailing data.
|
||||
|
||||
Two `static_assert`s enforce hard constraints: `sizeof(Type) >= sizeof(uint8_t*)` (the free-list pointer must fit inside the slot it inhabits), and `alignof(Type)` must be 4 or 8.
|
||||
|
||||
## `SlabAllocatorSet<Type>`: Tiered Dispatch
|
||||
|
||||
`SlabAllocatorSet` groups up to 64 `SlabAllocator<Type>` instances in a `boost::container::static_vector` — a fixed-capacity, stack-allocated container that avoids a heap allocation for the allocator array itself. Allocators are sorted by item size at construction and validated for uniqueness; duplicate sizes throw `std::runtime_error` at startup, appropriate since this is a static configuration error.
|
||||
|
||||
`allocate(extra)` performs a linear scan for the smallest slot that can fit `sizeof(Type) + extra`, short-circuiting through the `maxSize_` fast path:
|
||||
|
||||
```cpp
|
||||
if (auto const size = sizeof(Type) + extra; size <= maxSize_)
|
||||
```
|
||||
|
||||
If no configured tier can satisfy the request, `nullptr` is returned immediately without scanning. This lets callers implement a transparent fallback to `operator new[]` without coupling the allocator to any specific failure policy.
|
||||
|
||||
`deallocate(ptr)` iterates across allocators and relies on each `SlabAllocator::deallocate()` returning `bool` to indicate ownership. The return value propagates to the caller so it can distinguish "pointer owned by the slab" from "pointer was allocated some other way."
|
||||
|
||||
`SlabConfig` is a nested public value type exposing the `(extra, alloc, align)` triple, with `SlabAllocatorSet` declared as a `friend` to access its private fields. This keeps the construction API declarative without polluting the public interface with setters.
|
||||
|
||||
## How This Is Used: `SHAMapItem`
|
||||
|
||||
The concrete deployment is in `SHAMapItem.h`, where an `inline` global `SlabAllocatorSet<SHAMapItem>` named `slabber` is configured with seven size tiers:
|
||||
|
||||
```cpp
|
||||
inline SlabAllocatorSet<SHAMapItem> slabber({
|
||||
{ 128, megabytes(60) },
|
||||
{ 192, megabytes(46) },
|
||||
{ 272, megabytes(60) },
|
||||
{ 384, megabytes(56) },
|
||||
{ 564, megabytes(40) },
|
||||
{ 772, megabytes(46) },
|
||||
{ 1052, megabytes(60) },
|
||||
});
|
||||
```
|
||||
|
||||
The sizes and slab capacities are manually tuned to match the expected distribution of ledger object sizes and to minimize intra-slab slack. `make_shamapitem()` calls `slabber.allocate(data.size())`, falls back to `new std::uint8_t[sizeof(SHAMapItem) + data.size()]` if the slab can't satisfy the request, then placement-new's the `SHAMapItem` into the raw memory. The matching `intrusive_ptr_release()` calls `slabber.deallocate()`, falling back to `delete[]` if the pointer wasn't slab-owned. This opt-in, fallback-capable design means `SlabAllocator` never needs to handle arbitrarily sized allocations and the caller never hard-depends on the slab being able to serve the request.
|
||||
86
include/xrpl/basics/Slice.h.ai.json
Normal file
86
include/xrpl/basics/Slice.h.ai.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"void",
|
||||
"Slice const&",
|
||||
"void const*, std::size_t"
|
||||
],
|
||||
"lineno": 16,
|
||||
"name": "Slice"
|
||||
}
|
||||
],
|
||||
"description": "Defines the xrpl::Slice class, an immutable, non-owning, lightweight view over a linear range of bytes, along with related utility functions and operators for handling byte slices.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/Slice.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"Hasher& h",
|
||||
"Slice const& v"
|
||||
],
|
||||
"lineno": 98,
|
||||
"name": "hash_append"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Slice const& lhs",
|
||||
"Slice const& rhs"
|
||||
],
|
||||
"lineno": 104,
|
||||
"name": "operator=="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Slice const& lhs",
|
||||
"Slice const& rhs"
|
||||
],
|
||||
"lineno": 112,
|
||||
"name": "operator!="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Slice const& lhs",
|
||||
"Slice const& rhs"
|
||||
],
|
||||
"lineno": 117,
|
||||
"name": "operator<"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Stream& s",
|
||||
"Slice const& v"
|
||||
],
|
||||
"lineno": 124,
|
||||
"name": "operator<<"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::array<T, N> const& a"
|
||||
],
|
||||
"lineno": 131,
|
||||
"name": "makeSlice"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::vector<T, Alloc> const& v"
|
||||
],
|
||||
"lineno": 137,
|
||||
"name": "makeSlice"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::basic_string<char, Traits, Alloc> const& s"
|
||||
],
|
||||
"lineno": 143,
|
||||
"name": "makeSlice"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 12,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
include/xrpl/basics/Slice.h.ai.md
Normal file
33
include/xrpl/basics/Slice.h.ai.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# `xrpl/basics/Slice.h` — Immutable Byte-Range View
|
||||
|
||||
`Slice` is the foundational non-owning byte-view type for the XRPL codebase. It fills the same conceptual role as `std::string_view` or `std::span<const std::uint8_t>` but predates both, and is tightly integrated with XRPL's binary protocol handling infrastructure.
|
||||
|
||||
## Core Design
|
||||
|
||||
The class holds exactly two fields: a `const uint8_t*` pointer and a `size_t` length. This makes it trivially copyable — two words on any 64-bit platform — and safe to pass by value everywhere, which is the intended usage. A default-constructed `Slice` is always valid, representing an empty range. There is no null-vs-empty distinction: an empty `Slice` is simply one with `size_ == 0`, and a null pointer is only possible when size is also zero.
|
||||
|
||||
The class is explicitly `const`-only. There is no non-const `data()` accessor and no mutable iterator. This is deliberate: when a subsystem needs to mutate bytes, it uses `Buffer` (the companion owning type in `Buffer.h`). `Slice` is the read-only transport token.
|
||||
|
||||
## Advancing and Trimming
|
||||
|
||||
Two families of mutators allow consuming a byte stream in-place. `operator+=` / `operator+` advance the start of the slice and throw `std::domain_error` (via the `Throw<>` contract helper from `contract.h`) if `n > size_`. This makes them appropriate for protocol parsing loops where overrun is an error condition. In contrast, `remove_prefix` and `remove_suffix` perform no bounds checking, mirroring the intentionally unchecked semantics of `std::string_view::remove_prefix`. Callers are responsible for validating bounds before calling these. The distinction is important: the `+=` operator is for defensive parsing code, while `remove_prefix`/`remove_suffix` are for tight paths where invariants are already established.
|
||||
|
||||
`substr()` follows `std::string_view::substr` semantics exactly: it throws `std::out_of_range` if `pos > size()`, but clamps `count` to avoid running past the end. This means `substr(pos)` reliably returns the tail of a slice without requiring the caller to compute the remaining length.
|
||||
|
||||
## Indexing and Hashing
|
||||
|
||||
`operator[]` is guarded only by `XRPL_ASSERT`, which is a debug-mode check via the Beast instrumentation layer. This trades safety in release builds for performance in hot paths, consistent with how the rest of the protocol code handles per-byte access. Callers are expected to validate bounds externally.
|
||||
|
||||
The `hash_append` template function integrates `Slice` with XRPL's open hashing protocol. Any hasher that satisfies the `Hasher` concept (takes a pointer and byte-count) can hash a `Slice` directly. This is how `base_uint` instances, serialized ledger objects, and other byte sequences are hashed for use in unordered containers and the SHAMap.
|
||||
|
||||
## Stream Output and Comparisons
|
||||
|
||||
`operator<<` renders a `Slice` as its uppercase hex representation via `strHex`, which in turn uses `boost::algorithm::hex`. This hex rendering appears throughout XRPL logging and diagnostic output. The equality operator uses `std::memcmp` (fast, no locale concerns) and the less-than operator uses `std::lexicographical_compare`, giving `Slice` a total order suitable for use in sorted containers.
|
||||
|
||||
## The `makeSlice` Factory Functions
|
||||
|
||||
The three `makeSlice` overloads provide safe, implicit-free construction from common standard containers. The array and vector overloads are constrained via `std::enable_if` to only accept `T = char` or `T = unsigned char`, preventing the accidental construction of a `Slice` from a `std::vector<int>` or similar. The `std::basic_string<char>` overload has no such constraint since `char` is always the element type. These factories centralize the `reinterpret_cast` that is otherwise unavoidable when going from `char*` to `uint8_t*`, keeping that unsafe operation in one place.
|
||||
|
||||
## Relationship to `Buffer`
|
||||
|
||||
`Buffer` is the owning counterpart: it manages a heap-allocated `unique_ptr<uint8_t[]>` and provides an implicit conversion `operator Slice()`. The explicit constructor `Buffer(Slice)` deep-copies from a slice. The assignment `Buffer& operator=(Slice)` includes an XRPL_ASSERT guard to detect the case where the source slice is a subset of the buffer being overwritten — a subtle aliasing bug that would otherwise silently corrupt data. Together, `Slice` (non-owning, cheap, immutable) and `Buffer` (owning, allocated, mutable) form the complete binary data vocabulary used throughout the XRPL protocol layer, including `Serializer`, `STBlob`, `SHAMapItem`, cryptographic key types, and the conditions/fulfillments subsystem.
|
||||
125
include/xrpl/basics/StringUtilities.h.ai.json
Normal file
125
include/xrpl/basics/StringUtilities.h.ai.json
Normal file
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 18,
|
||||
"name": "blob"
|
||||
},
|
||||
{
|
||||
"lineno": 22,
|
||||
"name": "strSize"
|
||||
},
|
||||
{
|
||||
"lineno": 22,
|
||||
"name": "begin"
|
||||
},
|
||||
{
|
||||
"lineno": 22,
|
||||
"name": "end"
|
||||
},
|
||||
{
|
||||
"lineno": 61,
|
||||
"name": "strSrc"
|
||||
},
|
||||
{
|
||||
"lineno": 66,
|
||||
"name": "strSrc"
|
||||
},
|
||||
{
|
||||
"lineno": 81,
|
||||
"name": "pUrl"
|
||||
},
|
||||
{
|
||||
"lineno": 81,
|
||||
"name": "strUrl"
|
||||
},
|
||||
{
|
||||
"lineno": 83,
|
||||
"name": "str"
|
||||
},
|
||||
{
|
||||
"lineno": 85,
|
||||
"name": "s"
|
||||
},
|
||||
{
|
||||
"lineno": 93,
|
||||
"name": "domain"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 70,
|
||||
"name": "parsedURL"
|
||||
}
|
||||
],
|
||||
"description": "Provides utility functions for handling binary data, hexadecimal encoding/decoding, URL parsing, string trimming, and TOML domain validation in the xrpl namespace.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/StringUtilities.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"blob"
|
||||
],
|
||||
"lineno": 18,
|
||||
"name": "sqlBlobLiteral"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"strSize",
|
||||
"begin",
|
||||
"end"
|
||||
],
|
||||
"lineno": 22,
|
||||
"name": "strUnHex"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"strSrc"
|
||||
],
|
||||
"lineno": 61,
|
||||
"name": "strUnHex"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"strSrc"
|
||||
],
|
||||
"lineno": 66,
|
||||
"name": "strViewUnHex"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"pUrl",
|
||||
"strUrl"
|
||||
],
|
||||
"lineno": 81,
|
||||
"name": "parseUrl"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"str"
|
||||
],
|
||||
"lineno": 83,
|
||||
"name": "trim_whitespace"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"s"
|
||||
],
|
||||
"lineno": 85,
|
||||
"name": "to_uint64"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"domain"
|
||||
],
|
||||
"lineno": 93,
|
||||
"name": "isProperlyFormedTomlDomain"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 11,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
54
include/xrpl/basics/StringUtilities.h.ai.md
Normal file
54
include/xrpl/basics/StringUtilities.h.ai.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# `include/xrpl/basics/StringUtilities.h`
|
||||
|
||||
## Role and Purpose
|
||||
|
||||
This header is the central string manipulation toolkit for the `xrpl` namespace, gathering five loosely-related but frequently needed operations: SQLite blob escaping, hex decoding, URL parsing, whitespace trimming, integer parsing, and TOML domain validation. These utilities are used throughout the node's database layer, RPC subsystem, configuration parser, and peer-handshake code, making this one of the more broadly depended-upon headers in the `basics` module.
|
||||
|
||||
The header deliberately keeps its own template logic (the `strUnHex` family) inline while deferring everything regex-heavy or Boost-heavy to the `.cpp` implementation, keeping compile times manageable for translation units that only need hex conversion.
|
||||
|
||||
---
|
||||
|
||||
## Key Components
|
||||
|
||||
### Hex Decoding — `strUnHex`
|
||||
|
||||
The primary workhorse is the templated `strUnHex(strSize, begin, end)`. Rather than calling a library function, it builds a `static constexpr` 256-entry lookup table at compile time. The table maps every `unsigned char` value to its nibble value (0–15) or -1 for invalid characters, supporting both upper- and lower-case hex. Returning -1 for invalid bytes (rather than throwing) allows a cheap validity check without exception overhead.
|
||||
|
||||
The design handles **odd-length hex strings** explicitly: if `strSize` is odd, the first character is decoded alone as a high nibble, making `"A"` decode to `\x0A` rather than failing. This matters in practice; the test suite verifies `"D0A"` decodes to the two-byte sequence `"\r\n"`.
|
||||
|
||||
Two thin wrappers, `strUnHex(std::string const&)` and `strViewUnHex(std::string_view)`, exist solely to spare callers from passing the string size and iterators by hand. Both return `std::optional<Blob>`, using `std::nullopt` to signal a malformed or invalid input — consistent with the xrpl codebase's preference for value-returning error signaling over exceptions in hot paths.
|
||||
|
||||
The companion `strHex.h` (included here) provides the inverse direction via `boost::algorithm::hex`, giving callers both encode and decode in a single include. `Blob` — a `std::vector<unsigned char>` — is the shared currency between them.
|
||||
|
||||
### SQLite Blob Literals — `sqlBlobLiteral`
|
||||
|
||||
`sqlBlobLiteral(Blob const&)` produces SQLite's `X'HEXDATA'` literal syntax, used when constructing raw SQL queries that embed binary ledger objects. It is called in `AcceptedLedgerTx::getEscMeta()` (which encodes raw transaction metadata for the ledger SQLite store) and in `STTx` serialization. The function pre-reserves `size * 2 + 3` characters to avoid reallocations, then uses `boost::algorithm::hex` for the hex encoding, sandwiched between the `X'` prefix and `'` suffix. The existence of this function keeps SQL-escaping concerns out of the objects that own the binary data.
|
||||
|
||||
### URL Parsing — `parsedURL` and `parseUrl`
|
||||
|
||||
`parsedURL` is a plain-data aggregate holding scheme, username, password, domain, an optional `uint16_t` port, and path. The equality operator omits `username` and `password`, which matters for connection deduplication: two endpoints with the same scheme, domain, port, and path are considered the same regardless of credentials.
|
||||
|
||||
`parseUrl(parsedURL&, std::string const&)` drives a static `boost::regex` against the RFC 3986 authority-form URI pattern. Several non-obvious decisions are worth noting:
|
||||
|
||||
- **IPv6 bracket stripping**: After the regex extracts the host segment, the result is passed through `beast::IP::Endpoint::from_string_checked` to strip the surrounding brackets from IPv6 addresses (e.g., `[::1]` becomes `::1`). Doing this via the IP endpoint parser means the bracket removal is validated rather than naïve substring manipulation.
|
||||
- **Port overflow rejection**: Ports larger than 65535 cause `beast::lexicalCast` to return 0, and the function treats port 0 as a parse failure and returns `false`. This prevents silent misrouting to port 0.
|
||||
- **Scheme normalization**: The scheme is converted to lowercase unconditionally, so callers can do case-insensitive scheme comparison without extra work.
|
||||
- **Exception safety**: The regex match is wrapped in a bare `catch(...)` that returns `false`. This guards against `boost::regex` throwing on pathological input, which can happen with certain degenerate strings.
|
||||
|
||||
`parseUrl` is called in `RPCSub` (WebSocket subscription URLs) and `ValidatorSite` (validator list fetch URLs), making robustness to malformed user input essential.
|
||||
|
||||
### TOML Domain Validation — `isProperlyFormedTomlDomain`
|
||||
|
||||
`isProperlyFormedTomlDomain(std::string_view)` validates that a string looks like a plausible internet domain for the purpose of fetching TOML-based validator metadata. The header comment explicitly warns this function is **not** a strict domain validity check — it rejects obviously bad inputs but may also reject some valid internationalized domain names (IDNs). The regex in the `.cpp` enforces label-level rules (no leading/trailing hyphens, alphanumeric plus hyphen, 1–63 characters per label) and requires at least one dot with a 2–63 character alphabetic TLD. Length is gated first (4–128 characters) before regex evaluation to avoid unnecessary overhead. This function is used in `Config.cpp` and `Handshake.cpp` to validate `[validator_token]` domain fields before attempting TOML file fetches.
|
||||
|
||||
### Miscellaneous Helpers
|
||||
|
||||
`trim_whitespace(std::string)` takes its argument by value and delegates to `boost::trim` in place, returning the result. The by-value parameter communicates intent: the caller's string is not modified, but no extra copy is needed when passing a temporary.
|
||||
|
||||
`to_uint64(std::string const&)` wraps `beast::lexicalCastChecked` to return an `std::optional<uint64_t>`, converting the library's boolean-plus-out-parameter convention into a modern value-returning form.
|
||||
|
||||
---
|
||||
|
||||
## Design Notes
|
||||
|
||||
The mix of inline template functions and opaque declarations is deliberate: `strUnHex` is generic over iterator types and cannot live in a `.cpp`, while `parseUrl` and `isProperlyFormedTomlDomain` carry static `boost::regex` objects that are expensive to initialize and must be compiled once. The header's `#include` of `strHex.h` and `Blob.h` closes the encode/decode loop for callers who need both directions of hex conversion, and the `boost/format.hpp` include (present in the header but not actively used by any declared function) suggests this header accumulated dependencies over time rather than being designed from scratch.
|
||||
323
include/xrpl/basics/TaggedCache.h.ai.json
Normal file
323
include/xrpl/basics/TaggedCache.h.ai.json
Normal file
@@ -0,0 +1,323 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 32,
|
||||
"name": "name"
|
||||
},
|
||||
{
|
||||
"lineno": 32,
|
||||
"name": "size"
|
||||
},
|
||||
{
|
||||
"lineno": 32,
|
||||
"name": "expiration"
|
||||
},
|
||||
{
|
||||
"lineno": 32,
|
||||
"name": "clock"
|
||||
},
|
||||
{
|
||||
"lineno": 32,
|
||||
"name": "journal"
|
||||
},
|
||||
{
|
||||
"lineno": 32,
|
||||
"name": "collector"
|
||||
},
|
||||
{
|
||||
"lineno": 64,
|
||||
"name": "key"
|
||||
},
|
||||
{
|
||||
"lineno": 86,
|
||||
"name": "data"
|
||||
},
|
||||
{
|
||||
"lineno": 86,
|
||||
"name": "replaceCallback"
|
||||
},
|
||||
{
|
||||
"lineno": 106,
|
||||
"name": "value"
|
||||
},
|
||||
{
|
||||
"lineno": 132,
|
||||
"name": "digest"
|
||||
},
|
||||
{
|
||||
"lineno": 132,
|
||||
"name": "h"
|
||||
},
|
||||
{
|
||||
"lineno": 139,
|
||||
"name": "l"
|
||||
},
|
||||
{
|
||||
"lineno": 144,
|
||||
"name": "prefix"
|
||||
},
|
||||
{
|
||||
"lineno": 144,
|
||||
"name": "handler"
|
||||
},
|
||||
{
|
||||
"lineno": 163,
|
||||
"name": "last_access_"
|
||||
},
|
||||
{
|
||||
"lineno": 175,
|
||||
"name": "ptr_"
|
||||
},
|
||||
{
|
||||
"lineno": 181,
|
||||
"name": "when_expire"
|
||||
},
|
||||
{
|
||||
"lineno": 181,
|
||||
"name": "now"
|
||||
},
|
||||
{
|
||||
"lineno": 181,
|
||||
"name": "partition"
|
||||
},
|
||||
{
|
||||
"lineno": 181,
|
||||
"name": "stuffToSweep"
|
||||
},
|
||||
{
|
||||
"lineno": 181,
|
||||
"name": "allRemovals"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"name",
|
||||
"size",
|
||||
"expiration",
|
||||
"clock",
|
||||
"journal",
|
||||
"collector"
|
||||
],
|
||||
"lineno": 18,
|
||||
"name": "TaggedCache"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"prefix",
|
||||
"handler",
|
||||
"collector"
|
||||
],
|
||||
"lineno": 144,
|
||||
"name": "Stats"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"last_access_"
|
||||
],
|
||||
"lineno": 163,
|
||||
"name": "KeyOnlyEntry"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"last_access_",
|
||||
"ptr_"
|
||||
],
|
||||
"lineno": 175,
|
||||
"name": "ValueEntry"
|
||||
}
|
||||
],
|
||||
"description": "Implements a thread-safe tagged cache/map combination for storing and managing objects with strong and weak references, supporting cache expiration, concurrency, and metrics collection.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/TaggedCache.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"name",
|
||||
"size",
|
||||
"expiration",
|
||||
"clock",
|
||||
"journal",
|
||||
"collector"
|
||||
],
|
||||
"lineno": 32,
|
||||
"name": "TaggedCache"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 41,
|
||||
"name": "clock"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 44,
|
||||
"name": "size"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 47,
|
||||
"name": "getCacheSize"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 50,
|
||||
"name": "getTrackSize"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 53,
|
||||
"name": "getHitRate"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 56,
|
||||
"name": "clear"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 59,
|
||||
"name": "reset"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key"
|
||||
],
|
||||
"lineno": 64,
|
||||
"name": "touch_if_exists"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 71,
|
||||
"name": "sweep"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"valid"
|
||||
],
|
||||
"lineno": 73,
|
||||
"name": "del"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"data",
|
||||
"replaceCallback"
|
||||
],
|
||||
"lineno": 86,
|
||||
"name": "canonicalize"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"data"
|
||||
],
|
||||
"lineno": 95,
|
||||
"name": "canonicalize_replace_cache"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"data"
|
||||
],
|
||||
"lineno": 97,
|
||||
"name": "canonicalize_replace_client"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key"
|
||||
],
|
||||
"lineno": 99,
|
||||
"name": "fetch"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"lineno": 106,
|
||||
"name": "insert"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key"
|
||||
],
|
||||
"lineno": 110,
|
||||
"name": "insert"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"data"
|
||||
],
|
||||
"lineno": 117,
|
||||
"name": "retrieve"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 120,
|
||||
"name": "peekMutex"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 123,
|
||||
"name": "getKeys"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 127,
|
||||
"name": "rate"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"digest",
|
||||
"h"
|
||||
],
|
||||
"lineno": 132,
|
||||
"name": "fetch"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"l"
|
||||
],
|
||||
"lineno": 139,
|
||||
"name": "initialFetch"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 141,
|
||||
"name": "collect_metrics"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"when_expire",
|
||||
"now",
|
||||
"partition",
|
||||
"stuffToSweep",
|
||||
"allRemovals",
|
||||
""
|
||||
],
|
||||
"lineno": 181,
|
||||
"name": "sweepHelper"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"when_expire",
|
||||
"now",
|
||||
"partition",
|
||||
"stuffToSweep",
|
||||
"allRemovals",
|
||||
""
|
||||
],
|
||||
"lineno": 190,
|
||||
"name": "sweepHelper"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 11,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
69
include/xrpl/basics/TaggedCache.h.ai.md
Normal file
69
include/xrpl/basics/TaggedCache.h.ai.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# `include/xrpl/basics/TaggedCache.h`
|
||||
|
||||
## Role and Purpose
|
||||
|
||||
`TaggedCache` is the central in-memory caching primitive for the XRPL node. It appears in every subsystem that needs to keep parsed or computed data close to the CPU: ledger entries (`CachedSLEs`), SHAMap tree nodes (`TreeNodeCache`), accepted ledger objects, transaction history, and more. The design solves a problem that a plain LRU cache cannot: in a highly concurrent system where multiple threads may independently load the same keyed object from storage, you want all of them to converge on *one* canonical in-memory copy. An ordinary cache gives you a place to look things up; `TaggedCache` additionally enforces object identity.
|
||||
|
||||
The file defines only the class template declaration and its nested types. All method bodies live in the companion `TaggedCache.ipp`, which is included by consumers that need the full implementation.
|
||||
|
||||
## The Dual-Region Model
|
||||
|
||||
Every entry lives in one of two logical regions within the same `m_cache` hash map:
|
||||
|
||||
- **Strong region** ("cached"): The `ValueEntry` holds the `SharedWeakUnionPointerType` as a strong reference. This is what `m_cache_count` tracks. So long as an entry is here, the object stays alive regardless of whether any external code holds a reference to it.
|
||||
- **Weak region** ("tracked"): After the entry is swept or explicitly demoted via `del()`, the pointer is converted to a weak reference. The entry remains in `m_cache` and allows later callers who fetch the key to re-promote it back to the strong region if the object is still alive — i.e., if external `shared_ptr` holders still exist.
|
||||
|
||||
This two-region model means the total map size (`getTrackSize()`) is always ≥ the cache count (`getCacheSize()`). The gap is objects that are evicted from the hot cache but still alive elsewhere in the system.
|
||||
|
||||
## Template Parameters and Pointer Abstraction
|
||||
|
||||
The class is parameterized over two distinct pointer type arguments:
|
||||
|
||||
- `SharedWeakUnionPointerType` (defaults to `SharedWeakCachePointer<T>`): stored inside each `ValueEntry`. Must support `isStrong()`, `isWeak()`, `isExpired()`, `lock()`, `getStrong()`, `convertToStrong()`, and `convertToWeak()`.
|
||||
- `SharedPointerType` (defaults to `std::shared_ptr<T>`): returned to callers.
|
||||
|
||||
This abstraction allows two distinct implementations to plug in:
|
||||
|
||||
1. **`SharedWeakCachePointer<T>`** (the default): a `std::variant<std::shared_ptr<T>, std::weak_ptr<T>>`, saving the cost of storing both at once. Used with ordinary heap objects.
|
||||
2. **`SharedWeakUnion<T>`** (from `IntrusivePointer.h`): a single tagged raw pointer whose low bit encodes whether it is a strong or weak intrusive reference. Used by `TreeNodeCache` for `SHAMapTreeNode`, where the intrusive reference counting avoids the separate control-block allocation of `std::make_shared`.
|
||||
|
||||
The `IsKeyCache` boolean parameter enables a third mode: a pure key-existence cache that stores no value at all — only a `KeyOnlyEntry` carrying a `last_access` timestamp. `KeyCache` (`TaggedCache<uint256, int, true>`) uses this to track, for example, which full-below ranges the SHAMap has validated, without storing any associated data.
|
||||
|
||||
## Canonicalization
|
||||
|
||||
The `canonicalize(key, data, replaceCallback)` method is the most architecturally important operation. It is called when a piece of code has already loaded or constructed an object and wants to register it with the cache — but in a world where another thread may have beaten it there.
|
||||
|
||||
The logic:
|
||||
|
||||
1. If the key is absent, insert the new object as a strong entry and return `false` (the caller was first).
|
||||
2. If the key is present and cached (strong), invoke `replaceCallback` to decide which copy wins. If the callback returns `true`, the cache entry is replaced with the caller's data; otherwise the caller's `data` parameter is updated to point at the existing canonical copy.
|
||||
3. If the key is present but only weakly tracked, attempt to promote. If the object is still alive, again apply `replaceCallback`; if dead, adopt the caller's data.
|
||||
|
||||
The two convenience wrappers bake in the replacement policy: `canonicalize_replace_cache` always prefers the caller's new data (useful when the cache may hold a stale version), while `canonicalize_replace_client` always prefers the existing cached copy (the usual object-identity guarantee). The callback receives `entry.ptr.getStrong()` only when `R` is not a no-argument callable, avoiding the cost of materializing a strong pointer for intrusive types when it isn't needed.
|
||||
|
||||
## Sweep and Eviction
|
||||
|
||||
`sweep()` is called periodically (typically from a timer thread). It computes a `when_expire` cutoff based on the configured `m_target_age` and the current cache pressure relative to `m_target_size`. When the cache is over capacity, the effective age window shrinks proportionally, clamped to a minimum of one second, so that a rapidly growing cache doesn't evict everything instantly.
|
||||
|
||||
The `m_cache` is a `hardened_partitioned_hash_map`, which shards the data across multiple independent `std::unordered_map` partitions. `sweep()` spawns one worker thread per partition (`sweepHelper`) so that the per-partition linear scan proceeds in parallel. All threads are joined before the main lock is released. Swept entries whose strong pointers are about to be released are moved into a `SweptPointersVector` per partition; these vectors outlive the lock scope and are destroyed after the lock is dropped, so potentially expensive object destructors don't run under the lock.
|
||||
|
||||
The sweep has two outcomes for a strong entry whose age exceeds `when_expire`:
|
||||
|
||||
- If `use_count() == 1` (cache is the sole owner): move the strong pointer out to be destroyed and erase the map entry entirely.
|
||||
- If `use_count() > 1` (someone else holds a reference): demote to weak. The entry survives in the map as a tracker.
|
||||
|
||||
Expired weak entries (where the external owner has also released) are erased unconditionally.
|
||||
|
||||
## Concurrency and the Recursive Mutex
|
||||
|
||||
All public operations acquire `m_mutex`, which is a `std::recursive_mutex` by default. The recursive mutex is necessary because `del()` and `canonicalize()` may both be called from code paths that are already holding the lock via `peekMutex()`. The `peekMutex()` accessor is a deliberate escape hatch: callers like `ConsensusTransSetSF` need to hold the cache lock while issuing a batch of lookups to make the multi-step operation atomic. The class comment warns that callers must not modify cached objects unless they hold a lock over all cache operations, enforcing an implicit immutability contract on stored values.
|
||||
|
||||
`sweep()` passes a `std::lock_guard<std::recursive_mutex> const&` token to each `sweepHelper` overload as a proof-of-lock parameter — not to capture it, but to statically enforce at the call site that the lock is held when per-partition threads are spawned. Because `m_mutex` is recursive, the sweeper threads themselves don't attempt to re-acquire it; they work only on the partition they are handed.
|
||||
|
||||
## Metric Integration
|
||||
|
||||
The inner `Stats` struct integrates with the `beast::insight` metrics framework. Construction registers a hook callback (`collect_metrics`) that fires when the collector polls for data. The hook publishes two gauges: the current cache size and the hit-rate percentage. Hits and misses are accumulated as `uint64_t` counters (`m_hits`, `m_misses`) under the cache lock and converted to a rate on demand, so there is no atomic contention on the hot path.
|
||||
|
||||
## Relationship to Consumers
|
||||
|
||||
`CachedSLEs` is the simplest instantiation — `TaggedCache<uint256, SLE const>` with all defaults — used by `CachedView` to memoize ledger state entries looked up during transaction processing. `TreeNodeCache` substitutes the intrusive pointer pair for both the union pointer and the shared pointer type, avoiding control-block allocations for the high-frequency SHAMap node working set. `KeyCache` flips `IsKeyCache=true` to track membership of `uint256` keys with no associated value, used by `FullBelowCache` to short-circuit redundant SHAMap full-below validation.
|
||||
415
include/xrpl/basics/TaggedCache.ipp.ai.json
Normal file
415
include/xrpl/basics/TaggedCache.ipp.ai.json
Normal file
@@ -0,0 +1,415 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [],
|
||||
"code_paths": [
|
||||
{
|
||||
"call_chain": [
|
||||
"TaggedCache::TaggedCache"
|
||||
],
|
||||
"entry_point": "TaggedCache::TaggedCache",
|
||||
"purpose": "Constructs a TaggedCache instance, initializing internal fields and metrics.",
|
||||
"validation_points": [
|
||||
"No explicit validation in constructor; assumes parameters are valid."
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"TaggedCache::size"
|
||||
],
|
||||
"entry_point": "TaggedCache::size",
|
||||
"purpose": "Returns the number of elements in the cache.",
|
||||
"validation_points": [
|
||||
"Locks mutex to ensure thread safety, but no data validation."
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"TaggedCache::getCacheSize"
|
||||
],
|
||||
"entry_point": "TaggedCache::getCacheSize",
|
||||
"purpose": "Returns the cache count (number of cached items).",
|
||||
"validation_points": [
|
||||
"Locks mutex for thread safety, no data validation."
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"TaggedCache::getTrackSize"
|
||||
],
|
||||
"entry_point": "TaggedCache::getTrackSize",
|
||||
"purpose": "Returns the size of the cache container.",
|
||||
"validation_points": [
|
||||
"Locks mutex for thread safety, no data validation."
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"TaggedCache::getHitRate"
|
||||
],
|
||||
"entry_point": "TaggedCache::getHitRate",
|
||||
"purpose": "Calculates and returns the cache hit rate as a percentage.",
|
||||
"validation_points": [
|
||||
"Locks mutex for thread safety; uses std::max to avoid division by zero."
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"TaggedCache::clear"
|
||||
],
|
||||
"entry_point": "TaggedCache::clear",
|
||||
"purpose": "Clears the cache and resets the cache count.",
|
||||
"validation_points": [
|
||||
"Locks mutex for thread safety, no data validation."
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"TaggedCache::reset"
|
||||
],
|
||||
"entry_point": "TaggedCache::reset",
|
||||
"purpose": "Clears the cache and resets cache count, hits, and misses.",
|
||||
"validation_points": [
|
||||
"Locks mutex for thread safety, no data validation."
|
||||
]
|
||||
},
|
||||
{
|
||||
"call_chain": [
|
||||
"TaggedCache::touch_if_exists"
|
||||
],
|
||||
"entry_point": "TaggedCache::touch_if_exists",
|
||||
"purpose": "Checks if a key exists in the cache and updates its usage if so.",
|
||||
"validation_points": [
|
||||
"Locks mutex for thread safety; key is checked for existence but not validated for format or value."
|
||||
]
|
||||
}
|
||||
],
|
||||
"data_flows": [
|
||||
{
|
||||
"field": "m_cache",
|
||||
"flow": [
|
||||
"constructor",
|
||||
"used in size(), getTrackSize(), clear(), reset(), touch_if_exists()"
|
||||
],
|
||||
"origin": "Initialized in TaggedCache constructor.",
|
||||
"transformations": [
|
||||
"Cleared in clear() and reset(), queried in size() and getTrackSize(), checked for key existence in touch_if_exists()"
|
||||
],
|
||||
"validated_at": "No explicit validation; only thread safety via mutex."
|
||||
},
|
||||
{
|
||||
"field": "m_cache_count",
|
||||
"flow": [
|
||||
"constructor",
|
||||
"used in getCacheSize(), clear(), reset()"
|
||||
],
|
||||
"origin": "Initialized in constructor, updated in clear(), reset(), possibly elsewhere in full implementation.",
|
||||
"transformations": [
|
||||
"Reset to 0 in clear() and reset(), returned in getCacheSize()"
|
||||
],
|
||||
"validated_at": "No explicit validation."
|
||||
},
|
||||
{
|
||||
"field": "m_hits, m_misses",
|
||||
"flow": [
|
||||
"constructor",
|
||||
"used in getHitRate(), reset()"
|
||||
],
|
||||
"origin": "Initialized in constructor, updated in cache accessors (not shown in this snippet).",
|
||||
"transformations": [
|
||||
"Reset to 0 in reset(), used in hit rate calculation in getHitRate()"
|
||||
],
|
||||
"validated_at": "Division by zero avoided in getHitRate() via std::max."
|
||||
},
|
||||
{
|
||||
"field": "key (template parameter)",
|
||||
"flow": [
|
||||
"input to touch_if_exists()",
|
||||
"checked for existence in m_cache"
|
||||
],
|
||||
"origin": "Passed as argument to touch_if_exists() and likely other cache accessors (not shown).",
|
||||
"transformations": [
|
||||
"Used as lookup key; no transformation."
|
||||
],
|
||||
"validated_at": "No explicit validation of key format or value; only existence in cache is checked."
|
||||
}
|
||||
],
|
||||
"description": "This file implements the methods for the xrpl::TaggedCache template class, which provides a thread-safe, partitioned cache with strong/weak reference semantics, expiration, and metrics collection. It supports both key-value and key-only caching, with customizable pointer types and concurrency primitives.",
|
||||
"false_positive_patterns": [
|
||||
{
|
||||
"applies_to": [
|
||||
"null_check",
|
||||
"memory_safety"
|
||||
],
|
||||
"confidence": 0.9,
|
||||
"detection_keywords": [
|
||||
"null",
|
||||
"nullptr",
|
||||
"check",
|
||||
"std::shared_ptr"
|
||||
],
|
||||
"evidence": "Code uses std::shared_ptr",
|
||||
"issue_pattern": "Missing null check for std::shared_ptr",
|
||||
"why_false_positive": "RAII smart pointers guarantee initialization"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"memory_safety",
|
||||
"resource_leak"
|
||||
],
|
||||
"confidence": 0.85,
|
||||
"detection_keywords": [
|
||||
"memory",
|
||||
"leak",
|
||||
"delete"
|
||||
],
|
||||
"evidence": "Code uses std::shared_ptr",
|
||||
"issue_pattern": "Memory leak - missing delete",
|
||||
"why_false_positive": "Smart pointer handles cleanup automatically"
|
||||
},
|
||||
{
|
||||
"applies_to": [
|
||||
"resource_management",
|
||||
"cleanup"
|
||||
],
|
||||
"confidence": 0.88,
|
||||
"detection_keywords": [
|
||||
"mutex lock",
|
||||
"cleanup",
|
||||
"release",
|
||||
"close"
|
||||
],
|
||||
"evidence": "Code uses std::lock_guard",
|
||||
"issue_pattern": "Missing mutex lock cleanup",
|
||||
"why_false_positive": "std::lock_guard provides automatic mutex lock cleanup"
|
||||
}
|
||||
],
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/TaggedCache.ipp",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"name",
|
||||
"size",
|
||||
"expiration",
|
||||
"clock",
|
||||
"journal",
|
||||
"collector"
|
||||
],
|
||||
"lineno": 15,
|
||||
"name": "TaggedCache::TaggedCache"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 38,
|
||||
"name": "TaggedCache::clock"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 51,
|
||||
"name": "TaggedCache::size"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 64,
|
||||
"name": "TaggedCache::getCacheSize"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 77,
|
||||
"name": "TaggedCache::getTrackSize"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 90,
|
||||
"name": "TaggedCache::getHitRate"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 104,
|
||||
"name": "TaggedCache::clear"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 115,
|
||||
"name": "TaggedCache::reset"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key"
|
||||
],
|
||||
"lineno": 127,
|
||||
"name": "TaggedCache::touch_if_exists"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 146,
|
||||
"name": "TaggedCache::sweep"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"valid"
|
||||
],
|
||||
"lineno": 210,
|
||||
"name": "TaggedCache::del"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"data",
|
||||
"replaceCallback"
|
||||
],
|
||||
"lineno": 241,
|
||||
"name": "TaggedCache::canonicalize"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"data"
|
||||
],
|
||||
"lineno": 292,
|
||||
"name": "TaggedCache::canonicalize_replace_cache"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"data"
|
||||
],
|
||||
"lineno": 299,
|
||||
"name": "TaggedCache::canonicalize_replace_client"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key"
|
||||
],
|
||||
"lineno": 306,
|
||||
"name": "TaggedCache::fetch"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"value"
|
||||
],
|
||||
"lineno": 319,
|
||||
"name": "TaggedCache::insert"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key"
|
||||
],
|
||||
"lineno": 338,
|
||||
"name": "TaggedCache::insert"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"data"
|
||||
],
|
||||
"lineno": 353,
|
||||
"name": "TaggedCache::retrieve"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 366,
|
||||
"name": "TaggedCache::peekMutex"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 374,
|
||||
"name": "TaggedCache::getKeys"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 389,
|
||||
"name": "TaggedCache::rate"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"digest",
|
||||
"h"
|
||||
],
|
||||
"lineno": 402,
|
||||
"name": "TaggedCache::fetch"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key",
|
||||
"l"
|
||||
],
|
||||
"lineno": 423,
|
||||
"name": "TaggedCache::initialFetch"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 445,
|
||||
"name": "TaggedCache::collect_metrics"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"when_expire",
|
||||
"now",
|
||||
"partition",
|
||||
"stuffToSweep",
|
||||
"allRemovals",
|
||||
"lock"
|
||||
],
|
||||
"lineno": 463,
|
||||
"name": "TaggedCache::sweepHelper"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"when_expire",
|
||||
"now",
|
||||
"partition",
|
||||
"stuffToSweep",
|
||||
"allRemovals",
|
||||
"lock"
|
||||
],
|
||||
"lineno": 507,
|
||||
"name": "TaggedCache::sweepHelper"
|
||||
}
|
||||
],
|
||||
"language": "cpp",
|
||||
"language_patterns": {
|
||||
"exception_patterns": [],
|
||||
"namespace_accessors": [],
|
||||
"raii_usage": [
|
||||
{
|
||||
"audit_implication": "No manual delete needed, no null checks after construction",
|
||||
"false_positive_risk": "Missing null check, memory leak",
|
||||
"pointer_type": "std::shared_ptr",
|
||||
"type": "smart_pointer"
|
||||
},
|
||||
{
|
||||
"audit_implication": "Automatic mutex lock cleanup",
|
||||
"false_positive_risk": "Missing mutex lock cleanup",
|
||||
"resource": "mutex lock",
|
||||
"type": "raii_wrapper",
|
||||
"wrapper_type": "std::lock_guard"
|
||||
}
|
||||
],
|
||||
"smart_pointers": [
|
||||
{
|
||||
"audit_note": "Reference counted, auto cleanup when last ref dropped",
|
||||
"cleanup_needed": false,
|
||||
"false_positive_risk": "Missing null check or manual delete",
|
||||
"null_check_needed": false,
|
||||
"ownership": "shared",
|
||||
"type": "shared_ptr"
|
||||
}
|
||||
],
|
||||
"template_validation": []
|
||||
},
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 4,
|
||||
"name": "xrpl"
|
||||
}
|
||||
],
|
||||
"test_coverage_notes": "This file is a template implementation and does not contain direct validation logic or input sanitization; it relies on thread safety via mutexes and basic checks (e.g., avoiding division by zero in getHitRate). Validation of input parameters (such as cache size, expiration, or key validity) is not present in this code. Test coverage would likely be found in unit tests for TaggedCache in the rippled codebase, possibly in files like 'TaggedCache_test.cpp' or similar. However, based on this snippet, there is no evidence of explicit validation being tested, and edge cases such as invalid constructor parameters or malformed keys are not handled or tested here.",
|
||||
"validation_architecture": {
|
||||
"auto_validated_fields": [],
|
||||
"framework": "None detected",
|
||||
"validation_layer": "N/A"
|
||||
},
|
||||
"validations": []
|
||||
}
|
||||
60
include/xrpl/basics/TaggedCache.ipp.ai.md
Normal file
60
include/xrpl/basics/TaggedCache.ipp.ai.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# `TaggedCache.ipp` — Template Implementation of the XRPL Cache/Tracker
|
||||
|
||||
## Role in the System
|
||||
|
||||
`TaggedCache.ipp` contains the out-of-line template method bodies for `xrpl::TaggedCache`, a general-purpose concurrent cache that underpins most of rippled's in-memory object reuse: ledger state entries (`SLE`s), transactions, account roots, trust lines, and other ledger objects that are expensive to decode or fetch from disk. The `.ipp` pattern separates implementation from declaration while keeping both in header-includable form — the `.h` defines the class interface and the `.ipp` is included at the bottom (or by consuming translation units) to instantiate methods.
|
||||
|
||||
The class solves a problem specific to ledger node processing: many concurrent paths may independently fetch or create an object identified by the same hash key. Rather than permitting duplicates, the cache enforces a single *canonical* instance per key, replacing or redirecting callers as needed. It also acts as a weak-reference *tracker* — once an object is evicted from the active cache, the map entry survives as a weak pointer, so any code that retained a reference before eviction can still benefit from deduplication.
|
||||
|
||||
## Template Parameters and the Dual Mode
|
||||
|
||||
`TaggedCache` is parameterized over eight template arguments. The most architecturally significant is `bool IsKeyCache`. When `false` (the default), each entry stores a `ValueEntry` — a timestamp plus a `SharedWeakUnionPointerType` that can hold either a strong or a weak reference to `T`. When `true`, each entry stores a `KeyOnlyEntry` — just a timestamp, with no associated value. Key-only mode is used for negative-existence caches (e.g., "have we seen this transaction hash before?") where the value is irrelevant and memory footprint matters.
|
||||
|
||||
This duality is resolved at compile time via `std::conditional<IsKeyCache, KeyOnlyEntry, ValueEntry>::type Entry`, allowing a single class template to cover both use cases without virtual dispatch or runtime branching in fast paths.
|
||||
|
||||
## The Strong/Weak Pointer Abstraction
|
||||
|
||||
`ValueEntry` wraps a `SharedWeakUnionPointerType`, which defaults to `SharedWeakCachePointer<T>`. This type holds either a `std::shared_ptr<T>` (strong reference) or a `std::weak_ptr<T>` inside a `std::variant`, providing `isStrong()`, `isWeak()`, `convertToWeak()`, `convertToStrong()`, `getStrong()`, and `lock()`. An alternative intrusive implementation (`SharedWeakUnion<T>`) stores the strong/weak tag in the low bit of the pointer itself, avoiding the variant overhead when objects participate in intrusive reference counting.
|
||||
|
||||
The key insight is that `m_cache_count` tracks only strong-reference entries. The total `m_cache.size()` (returned by `getTrackSize()`) counts both strong and weak entries. This is why the class distinguishes `getCacheSize()` from `size()` / `getTrackSize()` — the former answers "how many objects is the cache keeping alive?" while the latter answers "how many objects is the cache aware of?"
|
||||
|
||||
## `sweep()` — Adaptive Expiry and Parallel Eviction
|
||||
|
||||
`sweep()` is the periodic eviction function. Its first decision is the expiry cutoff. If the cache is at or below `m_target_size`, entries older than `m_target_age` are expired. If the cache is oversize, the cutoff age is *proportionally shortened*: `target_age * target_size / current_size`, clamped to a minimum of one second. This creates a feedback loop: the more overloaded the cache is, the more aggressively it evicts.
|
||||
|
||||
`sweep()` then spawns one `std::thread` per partition of the underlying `hardened_partitioned_hash_map`, calling `sweepHelper`. Each thread iterates its partition independently. Because each partition is a distinct data structure (not a subset of a shared one), there is no intra-sweep contention. All threads are joined before the outer `std::lock_guard` exits — the sweep is fully synchronous from the caller's perspective, but parallelised internally.
|
||||
|
||||
The two `sweepHelper` overloads handle the value and key-only cases:
|
||||
|
||||
- **Value cache sweep**: Three cases per entry — (1) weak and expired: move pointer into `stuffToSweep` and erase the map entry; (2) strong and expired but with external holders (`use_count() > 1`): demote to weak, leave in map; (3) strong and expired with no external holders: move into `stuffToSweep` and erase.
|
||||
- **Key-only cache sweep**: Simpler — entries past the cutoff are erased. An extra guard clamps `last_access` to `now` if it is somehow in the future, preventing entries from appearing permanently recent.
|
||||
|
||||
The `stuffToSweep` vector pattern is deliberate: moved-out smart pointers are destroyed *after* the mutex is released, so potentially expensive object destructors never run under the lock. The vector is sized per-partition to avoid reallocations.
|
||||
|
||||
## `canonicalize()` — Deduplication Under Concurrency
|
||||
|
||||
`canonicalize()` is the cache's most subtle operation. Its contract: given a key and a caller's shared pointer, if the cache already has a live entry for that key, one of them must win and the other must be redirected to point at the canonical instance. The `replaceCallback` parameter decides who wins.
|
||||
|
||||
The callback has two supported signatures via `if constexpr`:
|
||||
- `bool()` — a zero-argument predicate. The common variants `canonicalize_replace_cache` (always returns `true`, cache wins) and `canonicalize_replace_client` (always returns `false`, client wins) use this form.
|
||||
- `bool(SharedPointerType)` — a unary predicate receiving the existing strong pointer, for policies that inspect content.
|
||||
|
||||
The zero-argument form exists as a performance optimisation: obtaining a strong pointer from a `SharedWeakUnion` requires an atomic operation (`checkoutStrongRefFromWeak`), which is unnecessary for the simple "always replace" and "never replace" cases.
|
||||
|
||||
The entry may be in one of three states: not present, present with strong reference, or present with weak reference. In the weak case, `canonicalize()` attempts to lock the weak pointer. If it succeeds, the object is still alive in memory (held by some other caller), and it is promoted back to a strong reference in the cache. If it fails (the object was destroyed), the new data is inserted as a fresh strong entry.
|
||||
|
||||
## `fetch()` and `initialFetch()`
|
||||
|
||||
`initialFetch()` is the shared internal lookup path: it handles the three states of a value entry (absent, strong, weak) and updates `m_cache_count` on weak→strong promotion. It does *not* increment `m_misses` — that is left to the callers so they can control miss accounting differently.
|
||||
|
||||
The two-argument `fetch(key, handler)` overload implements a double-checked locking pattern. It checks the cache under lock, releases the lock, calls the handler to load from an external source (e.g., the database), then re-acquires the lock to insert the result. This avoids holding the cache mutex during I/O while still protecting the map from concurrent modifications. A second check on re-entry (via `emplace`) ensures that if another thread beat this one to insert while the lock was dropped, both insertions are handled gracefully.
|
||||
|
||||
## `del()` — Conditional Erasure
|
||||
|
||||
`del(key, valid)` has a nuanced `valid` flag. When `valid=true`, the strong reference is released (decrementing `m_cache_count`) and the entry is converted to a weak pointer, but the key stays in the map so that any existing external holders still benefit from tracking. When `valid=false`, or when the existing entry has already expired, the key is erased entirely. This models the difference between "this object is no longer cached" and "this key is invalid and must not be returned."
|
||||
|
||||
## Metrics and Observability
|
||||
|
||||
`m_stats` wires the cache into the `beast::insight` telemetry system via a hook that calls `collect_metrics()` periodically. This exports `size` (strong-reference count) and `hit_rate` as gauges. There is a subtle inconsistency: `touch_if_exists()` increments `m_stats.hits` / `m_stats.misses` (the collector-facing counters), while `fetch()` increments `m_hits` / `m_misses` (the raw counters returned by `getHitRate()` and `rate()`). The two accounting streams serve different audiences — the collector feeds external monitoring, while `getHitRate()` / `rate()` are queried programmatically within the process.
|
||||
|
||||
`peekMutex()` exposes the internal `recursive_mutex` directly. Its use is intentional: callers that need to perform a composite operation atomically (e.g., fetch followed by conditional re-insert) must hold the same lock the cache uses. The `recursive_mutex` permits this without deadlocking when the same thread re-enters via a cache method while already holding the lock.
|
||||
71
include/xrpl/basics/ToString.h.ai.json
Normal file
71
include/xrpl/basics/ToString.h.ai.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 15,
|
||||
"name": "t"
|
||||
},
|
||||
{
|
||||
"lineno": 21,
|
||||
"name": "b"
|
||||
},
|
||||
{
|
||||
"lineno": 26,
|
||||
"name": "c"
|
||||
},
|
||||
{
|
||||
"lineno": 31,
|
||||
"name": "s"
|
||||
},
|
||||
{
|
||||
"lineno": 36,
|
||||
"name": "s"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "Provides generalized to_string functions in the xrpl namespace to handle various types (arithmetic, bool, char, std::string, const char*), extending std::to_string.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/ToString.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"t"
|
||||
],
|
||||
"lineno": 15,
|
||||
"name": "to_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"b"
|
||||
],
|
||||
"lineno": 21,
|
||||
"name": "to_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"c"
|
||||
],
|
||||
"lineno": 26,
|
||||
"name": "to_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"s"
|
||||
],
|
||||
"lineno": 31,
|
||||
"name": "to_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"s"
|
||||
],
|
||||
"lineno": 36,
|
||||
"name": "to_string"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 6,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
38
include/xrpl/basics/ToString.h.ai.md
Normal file
38
include/xrpl/basics/ToString.h.ai.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# `include/xrpl/basics/ToString.h`
|
||||
|
||||
## Purpose
|
||||
|
||||
`ToString.h` provides a small family of `xrpl::to_string()` overloads that patch over two well-known deficiencies in the standard `std::to_string()` and make string conversion uniform across all primitive types in XRPL code.
|
||||
|
||||
## Why This File Exists
|
||||
|
||||
`std::to_string` has two inconvenient behaviors that appear repeatedly in ledger code:
|
||||
|
||||
- `std::to_string(true)` returns `"1"`, not `"true"`. Log messages, JSON serialization, and error strings throughout the rippled codebase need human-readable booleans.
|
||||
- `std::to_string('A')` returns `"65"` — the integer value of the character — because `char` is an arithmetic type and the standard function treats it numerically.
|
||||
|
||||
Beyond these fixes, there is no single standard function that accepts `std::string` or `const char*` as identity cases. Generic template code that wants to uniformly convert any value to a string would have to branch on type. `xrpl::to_string` eliminates that branching.
|
||||
|
||||
## Design
|
||||
|
||||
The header defines five overloads under the `xrpl` namespace:
|
||||
|
||||
**Arithmetic template** (line 14–19): A SFINAE-constrained function template gated on `std::is_arithmetic<T>` that forwards to `std::to_string(t)`. This handles `int`, `long`, `float`, `double`, and the rest of the arithmetic family without any additional boilerplate. The `enable_if` guard prevents this template from competing with the non-template overloads below.
|
||||
|
||||
**`bool` overload** (line 21–25): Returns `"true"` or `"false"`. Even though `bool` satisfies `std::is_arithmetic`, C++ overload resolution prefers a non-template exact-match over a template instantiation. This overload therefore silently wins whenever a `bool` is passed, suppressing the `"0"`/`"1"` behavior.
|
||||
|
||||
**`char` overload** (line 27–31): Returns a one-character `std::string`. The same overload-resolution rule applies: `char` is arithmetic, but the non-template overload is preferred, preventing the integer-value conversion.
|
||||
|
||||
**`std::string` overload** (line 33–37): Identity conversion — returns the string by value. This is what makes generic code like `template<class T> std::string format(T v) { return xrpl::to_string(v); }` work for string arguments without special-casing them.
|
||||
|
||||
**`const char*` overload** (line 39–43): Constructs a `std::string` from a C-string literal, completing the string-identity family.
|
||||
|
||||
## Usage in the Codebase
|
||||
|
||||
The most visible consumer is `src/libxrpl/json/Writer.cpp`, which calls `xrpl::to_string(f)` when serializing `float` and `double` values to JSON output. The header is also `#include`d by `include/xrpl/json/Writer.h` and the XRPL transaction path debug-info header, and it appears indirectly wherever logging or JSON serialization needs to convert primitives to strings in a uniform way.
|
||||
|
||||
The comment in the header (`"It's also possible to provide implementation of to_string for a class which needs a string implementation"`) signals the intended extension point: XRPL domain types such as `Number` define their own `xrpl::to_string` overload in their own translation unit, and ADL (argument-dependent lookup) finds it automatically. `Number.cpp` demonstrates this with a full `xrpl::to_string(Number const&)` implementation that handles XRP and IOU formatting.
|
||||
|
||||
## Key Tradeoff
|
||||
|
||||
The header deliberately keeps the arithmetic template *enabled* for `bool` and `char` types — it is the non-template overloads that win in practice, not any exclusion in the template constraint. An alternative design would exclude `bool` and `char` from the `enable_if` (`!std::is_same<T,bool>` etc.), which would make the intent more explicit at the cost of a more complex constraint expression. The current approach is shorter and relies on well-defined overload-resolution rules, which is idiomatic modern C++.
|
||||
14
include/xrpl/basics/UnorderedContainers.h.ai.json
Normal file
14
include/xrpl/basics/UnorderedContainers.h.ai.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [],
|
||||
"description": "Defines type aliases for hash-based containers (maps and sets) with both standard and hardened (cryptographically secure) hash functions, within the xrpl namespace.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/UnorderedContainers.h",
|
||||
"functions": [],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 19,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
include/xrpl/basics/UnorderedContainers.h.ai.md
Normal file
33
include/xrpl/basics/UnorderedContainers.h.ai.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# `include/xrpl/basics/UnorderedContainers.h`
|
||||
|
||||
This header is the canonical entry point for hash-based container types throughout the XRPL codebase. It defines two distinct families of type aliases — `hash_*` and `hardened_hash_*` — that differ in a single, security-critical dimension: whether the underlying hash function is seeded at runtime to resist adversarial key collisions.
|
||||
|
||||
## The Two-Family Design
|
||||
|
||||
The file's own comment states the rule plainly: use `hash_*` containers for keys that do not need a cryptographically secure hashing algorithm; use `hardened_hash_*` when they do. This is not just a style preference — it is a security boundary. In a network node like rippled, many data structures are keyed by values that arrive from untrusted peers or from the open ledger, making them candidates for **HashDoS attacks** (where an adversary crafts many keys with identical hashes to degrade a hash map to O(n) lookup). The two families exist precisely to make that threat explicit at the type level.
|
||||
|
||||
### `hash_*` containers
|
||||
|
||||
`hash_map`, `hash_multimap`, `hash_set`, and `hash_multiset` all default to `beast::uhash<>` as their hash function. `beast::uhash` is a thin wrapper: it constructs a fresh `beast::xxhasher` (seeded-free, no runtime state) and calls `hash_append` on the key. `beast::xxhasher` itself wraps the XXH3 64-bit algorithm from the `xxhash` library — an extremely fast, non-cryptographic hash that works through a 64-byte internal buffer to avoid the streaming API overhead for small keys. There is no per-container randomization, so these containers are **fast but unsuitable for keys derived from external input**.
|
||||
|
||||
### `hardened_hash_*` containers
|
||||
|
||||
`hardened_hash_map`, `hardened_hash_multimap`, `hardened_hash_set`, `hardened_hash_multiset`, and `hardened_partitioned_hash_map` all default to `hardened_hash<strong_hash>`, where `strong_hash` is an alias for `beast::xxhasher`.
|
||||
|
||||
The protection comes from `hardened_hash`'s constructor. Each instance generates a random `seed_pair` (two independent `uint64_t` values) using a process-wide singleton: a `std::mt19937_64` generator seeded from `std::random_device`, protected by a `std::mutex`. This happens once per `hardened_hash` instance, not once per hash call. The seed is then passed into `beast::xxhasher` as its seed parameter on every invocation. Because each container instance has its own random seed that an attacker cannot know in advance, collisions crafted against one process are useless against another — and collisions crafted against one container cannot be reused against another container of the same type in the same process.
|
||||
|
||||
The comment in `hardened_hash.h` explicitly warns against Murmur or CityHash as the `HashAlgorithm` template parameter, citing the SipHash vulnerability research (https://131002.net/siphash/#at). XXH3 with a secret seed is the chosen alternative — fast enough that it doesn't compromise performance for lookup-heavy paths, while offering the unpredictability the hardened family requires.
|
||||
|
||||
## The Partitioned Variant
|
||||
|
||||
`hardened_partitioned_hash_map` adds a third property beyond security: **sharded concurrency**. It wraps `partitioned_unordered_map<Key, Value, hardened_hash<strong_hash>, ...>`, which internally holds a `std::vector` of independent `std::unordered_map` sub-maps. On construction (defaulting to `std::thread::hardware_concurrency()` partitions), each key is routed to a specific sub-map by `extract(key) % partitions_`. The `extract` function is specialized for `std::string` (hashing via `beast::uhash`) and for integral keys (using them directly as shard indices).
|
||||
|
||||
The sharding design assumes callers take per-partition locks externally — the class itself has no synchronization. The pattern is used where the key space is large and concurrent access from multiple threads is expected, allowing thread A to read partition 3 while thread B writes to partition 7 without contention. The default of hardware concurrency as partition count reflects a deliberate choice to match OS-level parallelism without over-provisioning.
|
||||
|
||||
## Relationships
|
||||
|
||||
- **`hardened_hash.h`** defines `hardened_hash` and the `make_seed_pair()` RNG machinery. The mutex-protected singleton ensures that two `hardened_hash` objects constructed concurrently from different threads each get independent seeds without races.
|
||||
- **`partitioned_unordered_map.h`** defines the sharded map template. Its forward-iterator walks all partitions sequentially, so range-for over a `hardened_partitioned_hash_map` works but is not order-stable across calls.
|
||||
- **`beast/hash/uhash.h`** and **`beast/hash/xxhasher.h`** provide the underlying hash plumbing. The `hash_append` protocol means any type that implements the `hash_append` free function in its own namespace is automatically supported by all containers in this file.
|
||||
|
||||
In practice, `hash_map` and `hash_set` appear in performance-sensitive internal structures keyed by types the node controls (e.g., pathfinding caches, RPC result sets), while the `hardened_*` variants appear where ledger-derived or peer-supplied data forms the key.
|
||||
36
include/xrpl/basics/UptimeClock.h.ai.json
Normal file
36
include/xrpl/basics/UptimeClock.h.ai.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 13,
|
||||
"name": "UptimeClock"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 31,
|
||||
"name": "update_thread"
|
||||
}
|
||||
],
|
||||
"description": "Defines the UptimeClock class, which tracks program uptime with seconds precision and provides a cached, performant way to query elapsed time since program start.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/UptimeClock.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 22,
|
||||
"name": "now"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 38,
|
||||
"name": "start_clock"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 7,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
include/xrpl/basics/UptimeClock.h.ai.md
Normal file
40
include/xrpl/basics/UptimeClock.h.ai.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# `UptimeClock` — Seconds-Precision Process Uptime Clock
|
||||
|
||||
## Role and Motivation
|
||||
|
||||
`UptimeClock` is a C++ named clock type (satisfying the `<chrono>` `TrivialClock` concept) that tracks how many seconds have elapsed since the `xrpld` process first called `now()`. Its defining characteristic is that it never queries the OS for the current time on each call — instead, a single background thread wakes up once per second to increment an `std::atomic<int>` counter, and every call to `now()` simply reads that cached integer. This makes it extremely cheap to call from hot paths anywhere in the server, at the cost of one-second granularity and a possible ~1 s error at the start of the process.
|
||||
|
||||
The motivation is straightforward: many subsystems — load monitoring, overlay slot management, peer metrics, the uptime display in `get_counts` — need to compare elapsed time but do not need sub-second precision. Using `std::chrono::system_clock::now()` for each comparison would involve a syscall; using `UptimeClock::now()` costs a single atomic load.
|
||||
|
||||
## Type Design
|
||||
|
||||
`UptimeClock` is designed as a drop-in clock type for the `<chrono>` framework:
|
||||
|
||||
- `rep` is plain `int`, meaning the uptime counter fits in a 32-bit signed integer. This gives a practical maximum of ~68 years — more than sufficient for a network daemon.
|
||||
- `period` is `std::ratio<1>`, meaning the tick unit is one second.
|
||||
- `time_point` is `std::chrono::time_point<UptimeClock>`, carrying the seconds-since-start value.
|
||||
- `is_steady` mirrors `std::chrono::system_clock::is_steady`. This is a minor implementation detail — the clock is not truly steady in the C++ sense (it is not guaranteed to never go backwards), but the value follows from its backing clock.
|
||||
|
||||
Because `UptimeClock` satisfies the named clock requirements, it can be used as a template argument wherever the standard library or library code expects a clock, such as `reduce_relay::Slots<UptimeClock>` in `OverlayImpl.h` and `UptimeClock::time_point mLastUpdate` in `LoadMonitor`.
|
||||
|
||||
## The Background Thread Mechanism
|
||||
|
||||
The clock uses a lazy-initialization pattern: the first call to `now()` constructs a `static update_thread` via `start_clock()`. The function-local `static` ensures this happens exactly once, and C++11 guarantees that static-local initialization is thread-safe, so no explicit lock is needed.
|
||||
|
||||
The `update_thread` inner class is a thin RAII wrapper around `std::thread`. It inherits from `std::thread` privately, exposes `std::thread::thread` constructors via a `using` declaration, and overrides the destructor to perform a clean shutdown: it sets the shared `stop_` atomic to `true` and calls `join()`, waiting up to 1 second for the thread to notice the flag and exit. The comment in the implementation is honest about this: the join may take up to 1 s but happens only once at shutdown, so the latency is acceptable.
|
||||
|
||||
Inside the thread itself, the loop uses `std::this_thread::sleep_until` rather than `sleep_for`. This avoids drift: the next wake time is computed as `next += 1s` before sleeping, so accumulated scheduler jitter does not cause the counter to fall progressively behind wall time.
|
||||
|
||||
Both `now_` and `stop_` are `std::atomic` — `now_` because it is read by multiple calling threads while being written by the update thread, and `stop_` because it must be visible across thread boundaries without a data race.
|
||||
|
||||
## Epoch and Precision Caveats
|
||||
|
||||
The `now()` function initializes the update thread on first call, not at process startup. The implementation comment acknowledges this: the epoch is strictly "time since first use" rather than "time since xrpld start". However, the first call to `now()` happens very early in initialization (e.g., when `LoadMonitor` is constructed), so the discrepancy is a small fraction of a second and does not matter for any current consumer.
|
||||
|
||||
Separately, because the counter increments after each 1-second sleep, the value returned by `now()` starts at 0 and reaches 1 only after the first full second has elapsed. Consumers should treat the value as a lower bound on elapsed seconds, accurate to ±1 s.
|
||||
|
||||
## Usage in the Codebase
|
||||
|
||||
The primary consumers fall into two categories. First, display/reporting: `GetCounts.cpp` calls `UptimeClock::now()` to compute a human-readable uptime string ("3 days 4 hours 20 minutes"), decomposing the `time_point` via repeated `time_since_epoch() / unitVal` operations. Second, time-keyed data structures: `LoadMonitor` stores a `UptimeClock::time_point mLastUpdate` to rate-limit logging, and `OverlayImpl` instantiates `reduce_relay::Slots<UptimeClock>` to manage per-peer transmission windows that expire after a fixed number of seconds.
|
||||
|
||||
In all these cases the one-second resolution is exactly what is needed, and the atomic-load cost of `now()` is negligible compared to the work being gated on it.
|
||||
39
include/xrpl/basics/algorithm.h.ai.json
Normal file
39
include/xrpl/basics/algorithm.h.ai.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [],
|
||||
"description": "This file provides generic algorithms for set intersection and conditional removal on ordered ranges, including a generalized set intersection function and a function to remove elements based on intersection or predicate matching.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/algorithm.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"first1",
|
||||
"last1",
|
||||
"first2",
|
||||
"last2",
|
||||
"action",
|
||||
"comp"
|
||||
],
|
||||
"lineno": 13,
|
||||
"name": "generalized_set_intersection"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"first1",
|
||||
"last1",
|
||||
"first2",
|
||||
"last2",
|
||||
"pred",
|
||||
"comp"
|
||||
],
|
||||
"lineno": 49,
|
||||
"name": "remove_if_intersect_or_match"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 5,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
25
include/xrpl/basics/algorithm.h.ai.md
Normal file
25
include/xrpl/basics/algorithm.h.ai.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# `include/xrpl/basics/algorithm.h`
|
||||
|
||||
This header provides two generic, header-only algorithms for working with sorted ranges. Both are evolutionary descendants of standard library algorithms — `std::set_intersection` and `std::remove_if` — extended to handle cases that the standard versions cannot express cleanly. The file has no dependencies beyond `<utility>` and serves as a low-level utility for the broader `xrpl::basics` module.
|
||||
|
||||
## `generalized_set_intersection`
|
||||
|
||||
The standard `std::set_intersection` copies matched elements into an output iterator. That is sufficient when you only need one element from each matched pair and want no side effects beyond writing to output. `generalized_set_intersection` replaces the output iterator with an `Action` functor that receives *both* matched elements — `action(*first1, *first2)` — giving the caller full read/write access to the matched pair.
|
||||
|
||||
The traversal is the same two-pointer walk used by `std::set_intersection`: advance the iterator pointing to the smaller element, and when both are equal (equivalence under `comp`), fire the action and advance both. The implementation uses the strict-weak-ordering identity `a == b ⟺ !comp(a,b) && !comp(b,a)` to detect equality without requiring a separate equality operator.
|
||||
|
||||
The sole user in the codebase is `RCLCensorshipDetector::propose()`, which calls this function to carry sequence numbers forward. The detector tracks how many consecutive consensus rounds each transaction has failed to be included, and the sequence counter must survive across rounds. When the node re-proposes a transaction it already tracked, `generalized_set_intersection` finds the match and copies the old `seq` field into the new `TxIDSeq` entry via the action lambda — `[](auto& x, auto const& y) { x.seq = y.seq; }`. A standard `set_intersection` can't do this because the output range would only hold one element type, not a reference to both.
|
||||
|
||||
## `remove_if_intersect_or_match`
|
||||
|
||||
This algorithm fuses `std::remove_if` with set intersection into a single O(n + m) pass. It removes elements from the first range `[first1, last1)` if either condition holds: the element appears in the sorted second range `[first2, last2)`, or a predicate `pred` returns true for it. The caller must subsequently call `erase` on the returned end iterator to actually shrink the container, following the standard erase-remove idiom.
|
||||
|
||||
The internal state is maintained as three contiguous, non-overlapping regions in the original first range: `[original-first1, first1)` holds preserved elements compacted to the front; `[first1, i)` holds removed elements awaiting overwrite; `[i, last1)` holds untested elements. Each iteration either compacts `*i` into the preserved prefix (if it should be kept) or leaves a gap. Movement uses `std::move` rather than copy, which is important for types with expensive or move-only semantics.
|
||||
|
||||
The fusion with intersection logic works because both ranges are sorted: when `*i >= *first2`, the only way `*i` could equal some element in the second range is if that element is at or near `first2`. The algorithm checks `!comp(*first2, *i)` to detect equality, then advances `i` (marking it removed) before stepping `first2`. Crucially, once `first2` has moved past `*i`, no earlier element in the second range can match future elements of the first range — a guarantee that depends entirely on sorted order.
|
||||
|
||||
`RCLCensorshipDetector::check()` uses this to clean up its tracker in one pass after a consensus round. The second range is the set of accepted transaction IDs; the predicate fires `pred(x.txid, x.seq)` to let the caller detect potential censorship (e.g., if a transaction has been blocked for too many rounds). The comparator passed is `std::less<void>` — C++14's heterogeneous comparator — which dispatches to whichever `operator<` overload best matches the operand types. Because `RCLCensorshipDetector` defines heterogeneous `operator<` between `TxIDSeq` and raw `TxID`, the intersection check compares across the two different element types of the two ranges without constructing temporary objects.
|
||||
|
||||
## Design context
|
||||
|
||||
Both algorithms enforce sorted-range preconditions but do not assert or check them. Violations silently produce incorrect results, consistent with the standard library's approach for range algorithms. The decision to keep these in a separate `algorithm.h` rather than inline inside `RCLCensorshipDetector` reflects the conventional separation of algorithmic mechanics from domain logic — the censorship detector is free to express its `propose` and `check` operations at the level of intent, delegating the traversal bookkeeping here.
|
||||
54
include/xrpl/basics/base64.h.ai.json
Normal file
54
include/xrpl/basics/base64.h.ai.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 44,
|
||||
"name": "data"
|
||||
},
|
||||
{
|
||||
"lineno": 44,
|
||||
"name": "len"
|
||||
},
|
||||
{
|
||||
"lineno": 46,
|
||||
"name": "s"
|
||||
},
|
||||
{
|
||||
"lineno": 52,
|
||||
"name": "data"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "Provides base64 encoding and decoding utilities for binary and string data, including functions to encode raw bytes or strings to base64 and decode base64 strings.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/base64.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"std::uint8_t const* data",
|
||||
"std::size_t len"
|
||||
],
|
||||
"lineno": 44,
|
||||
"name": "base64_encode"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::string const& s"
|
||||
],
|
||||
"lineno": 46,
|
||||
"name": "base64_encode"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::string_view data"
|
||||
],
|
||||
"lineno": 52,
|
||||
"name": "base64_decode"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 42,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
include/xrpl/basics/base64.h.ai.md
Normal file
39
include/xrpl/basics/base64.h.ai.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# `include/xrpl/basics/base64.h`
|
||||
|
||||
This header exposes the XRPL ledger's standard-library-free Base64 codec. It is part of the `xrpl/basics` utility layer — a collection of low-level primitives that the rest of the stack depends on but that carry no ledger-specific semantics themselves.
|
||||
|
||||
## API Surface
|
||||
|
||||
The header declares three free functions in the `xrpl` namespace:
|
||||
|
||||
```cpp
|
||||
std::string base64_encode(std::uint8_t const* data, std::size_t len);
|
||||
std::string base64_encode(std::string const& s); // inline convenience overload
|
||||
std::string base64_decode(std::string_view data);
|
||||
```
|
||||
|
||||
The primary `base64_encode` overload takes a raw byte pointer and a length, reflecting the reality that the callers — cryptographic subsystems, peer handshaking, manifest deserialisation — deal in raw binary buffers rather than `std::string`. The second overload is a thin inline forwarder that `reinterpret_cast`s a `std::string`'s data pointer, colocating the type-pun at the definition site so it doesn't leak into every call site.
|
||||
|
||||
`base64_decode` takes `std::string_view`, the right choice for a read-only text consumer: callers can pass a `std::string`, a string literal, or a substring view without forcing a copy.
|
||||
|
||||
## Implementation Details (from `src/libxrpl/basics/base64.cpp`)
|
||||
|
||||
The implementation is adapted from René Nyffenegger's public-domain codec (2004–2008) and lives inside an anonymous `base64` sub-namespace within `xrpl`, keeping its helper tables and low-level routines invisible to the public API.
|
||||
|
||||
The encode path pre-allocates the output string to `encoded_size(n) = 4 * ((n + 2) / 3)` bytes, writes directly into the string buffer, then calls `resize` a second time with the actual byte count returned by the inner `encode()` function. This double-resize idiom avoids an extra heap allocation while ensuring the string's `size()` is accurate after the fact.
|
||||
|
||||
The decode path does the same trick against `decoded_size(n) = ((n / 4) * 3) + 2`. The slight over-allocation (`+2`) is intentional: it accommodates the worst-case padding without needing a branch, and `decoded_size` is only used for reservation. The inner `decode()` function returns a `std::pair<size_t, size_t>` — bytes written and input characters consumed — so the public `base64_decode` trims the string to the first element of the pair.
|
||||
|
||||
The inverse-table approach in `decode()` is a classic O(1) lookup: a 256-element `signed char` table maps every possible byte value to its 6-bit value or `-1` for invalid characters. When a `-1` is encountered, decoding stops immediately and returns whatever has been written so far. The test suite intentionally exercises this: `base64_decode("not_base64!!")` and `base64_decode("not")` produce identical output because both stop at the first character (`n`) that maps to a valid value and then halt on subsequent invalid ones.
|
||||
|
||||
This silent-truncation behavior is a deliberate pragmatic choice rather than an error-throwing design. The callers — manifest deserialisation, session-signature verification, validator token loading — handle malformed input at a higher level by checking the output length or passing the result through a cryptographic verifier that will reject garbage data.
|
||||
|
||||
## Usage in the Codebase
|
||||
|
||||
The two primary consumer categories are:
|
||||
|
||||
**Peer handshaking** (`src/xrpld/overlay/detail/Handshake.cpp`): When two rippled nodes establish an encrypted overlay connection, the initiating side base64-encodes the raw ECDSA session signature bytes into the `Session-Signature` HTTP header, and the receiving side decodes it back to bytes before passing them to `verifyDigest`. Base64 here is transport hygiene — HTTP headers are text, signatures are binary.
|
||||
|
||||
**Validator infrastructure** (`src/libxrpl/server/Manifest.cpp`, `ValidatorKeys.cpp`, `ValidatorList.cpp`): Validator manifests and revocations are serialised as binary blobs and then base64-encoded for embedding in configuration files and JSON RPC responses. The config parser strips whitespace from multi-line base64 blobs, concatenates them, and passes the result directly to `base64_decode`. The `RpcCall.cpp` and `ServerHandler.cpp` paths do the same for over-the-wire RPC payloads.
|
||||
|
||||
The codec is intentionally RFC 4648 standard (alphabet `A–Z a–z 0–9 + /`, `=` padding) rather than the URL-safe variant, matching the expectations of existing tooling and the external validator configuration format.
|
||||
152
include/xrpl/basics/base_uint.h.ai.json
Normal file
152
include/xrpl/basics/base_uint.h.ai.json
Normal file
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 15,
|
||||
"name": "is_contiguous_container"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 21,
|
||||
"name": "is_contiguous_container<Container, std::void_t<...>>"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 28,
|
||||
"name": "is_contiguous_container<Slice>"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"void const* data, VoidHelper",
|
||||
"std::uint64_t b",
|
||||
"std::string_view sv",
|
||||
"Container const& c"
|
||||
],
|
||||
"lineno": 34,
|
||||
"name": "base_uint"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 97,
|
||||
"name": "base_uint::VoidHelper"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 464,
|
||||
"name": "is_uniquely_represented<xrpl::base_uint<Bits, Tag>>"
|
||||
}
|
||||
],
|
||||
"description": "Defines the xrpl::base_uint template class for fixed-width big-endian unsigned integers (128, 160, 192, 256 bits) used in XRP Ledger, along with related operators, utilities, and type aliases.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/base_uint.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"Hasher& h",
|
||||
"base_uint const& a"
|
||||
],
|
||||
"lineno": 232,
|
||||
"name": "hash_append"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"base_uint<Bits, Tag> const& lhs",
|
||||
"base_uint<Bits, Tag> const& rhs"
|
||||
],
|
||||
"lineno": 370,
|
||||
"name": "operator<=>"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"base_uint<Bits, Tag> const& lhs",
|
||||
"base_uint<Bits, Tag> const& rhs"
|
||||
],
|
||||
"lineno": 384,
|
||||
"name": "operator=="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"base_uint<Bits, Tag> const& a",
|
||||
"std::uint64_t b"
|
||||
],
|
||||
"lineno": 392,
|
||||
"name": "operator=="
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"base_uint<Bits, Tag> const& a",
|
||||
"base_uint<Bits, Tag> const& b"
|
||||
],
|
||||
"lineno": 399,
|
||||
"name": "operator^"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"base_uint<Bits, Tag> const& a",
|
||||
"base_uint<Bits, Tag> const& b"
|
||||
],
|
||||
"lineno": 405,
|
||||
"name": "operator&"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"base_uint<Bits, Tag> const& a",
|
||||
"base_uint<Bits, Tag> const& b"
|
||||
],
|
||||
"lineno": 411,
|
||||
"name": "operator|"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"base_uint<Bits, Tag> const& a",
|
||||
"base_uint<Bits, Tag> const& b"
|
||||
],
|
||||
"lineno": 417,
|
||||
"name": "operator+"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"base_uint<Bits, Tag> const& a"
|
||||
],
|
||||
"lineno": 423,
|
||||
"name": "to_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"base_uint<Bits, Tag> const& a"
|
||||
],
|
||||
"lineno": 429,
|
||||
"name": "to_short_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::ostream& out",
|
||||
"base_uint<Bits, Tag> const& u"
|
||||
],
|
||||
"lineno": 435,
|
||||
"name": "operator<<"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"uint256 const& key"
|
||||
],
|
||||
"lineno": 441,
|
||||
"name": "extract"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 12,
|
||||
"name": "xrpl"
|
||||
},
|
||||
{
|
||||
"lineno": 14,
|
||||
"name": "detail"
|
||||
},
|
||||
{
|
||||
"lineno": 461,
|
||||
"name": "beast"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
include/xrpl/basics/base_uint.h.ai.md
Normal file
41
include/xrpl/basics/base_uint.h.ai.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# `base_uint.h` — Fixed-Width Big-Endian Integers for the XRP Ledger Protocol
|
||||
|
||||
## Role in the System
|
||||
|
||||
`base_uint<Bits, Tag>` is the foundational integer type for every fixed-width hash or identifier used in the XRP Ledger. Transaction hashes, ledger hashes, account IDs, currency codes, directory indices, node IDs, and MPT issuance IDs all live as instantiations of this template. The four concrete aliases `uint128`, `uint160`, `uint192`, and `uint256` are defined here; protocol-layer aliases like `AccountID`, `Currency`, `NodeID`, and `Directory` are defined in `UintTypes.h` and `AccountID.h` using the tagged form.
|
||||
|
||||
The file originated in the Bitcoin codebase (copyright notice from 2009–2011), was adapted for Ripple/XRP, and has accumulated XRPL-specific concerns such as hardened hashing, `Slice` interoperability, and tag-based type safety.
|
||||
|
||||
## Big-Endian Storage Is a Protocol Invariant
|
||||
|
||||
The most important design constraint is stated in the class comment: **the internal representation is big-endian and is part of the binary wire protocol**. This isn't just a performance choice; changing it would corrupt serialized ledger data.
|
||||
|
||||
Internally, the value is stored as `std::array<std::uint32_t, Bits/32>`, and the comment acknowledges that the array is "really big-endian in byte order" while using 32-bit words for speed. Every arithmetic operation that traverses the array must respect this: `operator++`, `operator--`, and `operator+=` all call `boost::endian::big_to_native` before doing native arithmetic and `boost::endian::native_to_big` when writing back. This per-operation conversion is the price paid for using `uint32_t` words rather than raw `uint8_t` bytes.
|
||||
|
||||
## Tag Parameter for Protocol-Level Type Safety
|
||||
|
||||
The second template parameter `Tag` exists solely to make `base_uint<160, AccountIDTag>` and `base_uint<160, CurrencyTag>` incompatible types even though they have identical representations. Without `Tag`, an `AccountID` and a `Currency` would be the same C++ type, and the compiler couldn't catch accidental mixing. Tags are empty structs with nothing in them — their sole purpose is to name otherwise-identical instantiations as distinct. This is the phantom-type pattern, and it's used extensively across the protocol layer.
|
||||
|
||||
## Hex Parsing: A Baked-In Byteswap
|
||||
|
||||
The private `parseFromStringView()` method contains a non-obvious bit-manipulation trick. For each 32-bit word it consumes eight hex characters and places them using the shift sequence `{4, 0, 12, 8, 20, 16, 28, 24}`. This interleaving directly constructs the `uint32_t` value so that when its bytes are read from memory, they appear in the same big-endian order as the hex string — without a separate `native_to_big` call at the end. The first hex character (most significant nibble of the big-endian representation) is placed at bits 4–7, which on a little-endian platform lands in the lowest-address byte. The effect is that the in-memory byte sequence equals the hex string's byte sequence, satisfying the big-endian invariant efficiently.
|
||||
|
||||
The parser accepts the special input `"0"` as a shorthand for the all-zero value, regardless of the expected width. Any other string must be exactly `2 * bytes` characters long; otherwise `ParseResult::badLength` is returned. This two-outcome design (via `Expected<decltype(data_), ParseResult>`) feeds both the `[[nodiscard]] bool parseHex()` path (which returns false on error) and the throwing `explicit constexpr base_uint(std::string_view)` constructor. The comment notes this constructor is intended for compile-time use and suggests it become `consteval` in C++23.
|
||||
|
||||
## Comparison: Why Byte-by-Byte Works
|
||||
|
||||
The spaceship operator `<=>` compares the two values using `std::mismatch` across the raw byte iterators. A comment explicitly explains why this is correct: because the data is stored in big-endian byte order, a byte-by-byte lexicographic comparison of the raw bytes produces the same result as a numeric comparison of the integers. A FIXME note records that `std::lexicographical_compare_three_way` would be preferable but was unavailable on macOS at the time of writing.
|
||||
|
||||
## Hashing: Seeded and Raw
|
||||
|
||||
`base_uint` exposes two hashing interfaces. The `using hasher = hardened_hash<>` member makes the hash seeded per-container construction using a random 128-bit seed (via `hardened_hash.h`). This resists hash-flooding attacks on `unordered_map`s keyed by protocol values. The `hash_append` friend function feeds raw memory directly to the hash algorithm without any endian conversion, which is correct because the bytes are already in a canonical form. The `beast::is_uniquely_represented` specialization at the bottom of the file asserts that there are no padding bytes, permitting hash algorithms to hash the whole object as a contiguous byte sequence.
|
||||
|
||||
The `extract()` specialization for `uint256` is wired into `partitioned_unordered_map`, which uses the extracted value to choose a shard. It reads the first `sizeof(std::size_t)` bytes via `memcpy` to avoid undefined behavior from potentially unaligned access, and the comment notes this will compile to an equivalent direct load on most platforms.
|
||||
|
||||
## Container Interface and `fromVoid`
|
||||
|
||||
`base_uint` exposes a byte-level STL container interface (`begin()`, `end()`, `data()`, `size()`) with `value_type = unsigned char`. This lets it be treated as a byte range by serialization code, stream operators, and `Slice`-based APIs. The templated constructor and assignment operator accept any `is_contiguous_container` (including `Slice`) and `memcpy` the bytes in, with an assertion that sizes match exactly. The `fromVoid(void const*)` factory and its checked variant `fromVoidChecked` provide a controlled path from raw pointers, with the internal `VoidHelper` tag struct ensuring the ambiguity-prone `base_uint(0)` call routes to the `uint64_t` constructor rather than the raw-pointer one.
|
||||
|
||||
## Deprecated Convenience Members
|
||||
|
||||
`isZero()`, `isNonZero()`, and `zero()` are marked deprecated; the preferred idiom is comparison against `beast::zero` and assignment from `beast::zero`. The zero-value constructor `base_uint(beast::Zero)` and the assignment `operator=(beast::Zero)` both zero-fill the internal array, integrating with the `beast::Zero` sentinel type used across the codebase.
|
||||
71
include/xrpl/basics/chrono.h.ai.json
Normal file
71
include/xrpl/basics/chrono.h.ai.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 38,
|
||||
"name": "tp"
|
||||
},
|
||||
{
|
||||
"lineno": 44,
|
||||
"name": "tp"
|
||||
},
|
||||
{
|
||||
"lineno": 54,
|
||||
"name": "tp"
|
||||
},
|
||||
{
|
||||
"lineno": 60,
|
||||
"name": "tp"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 23,
|
||||
"name": "NetClock"
|
||||
}
|
||||
],
|
||||
"description": "Defines network and stopwatch clocks for the XRPL project, including time formatting utilities and epoch offset calculations.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/chrono.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"tp"
|
||||
],
|
||||
"lineno": 38,
|
||||
"name": "to_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"tp"
|
||||
],
|
||||
"lineno": 44,
|
||||
"name": "to_string"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"tp"
|
||||
],
|
||||
"lineno": 54,
|
||||
"name": "to_string_iso"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"tp"
|
||||
],
|
||||
"lineno": 60,
|
||||
"name": "to_string_iso"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 74,
|
||||
"name": "stopwatch"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 11,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
37
include/xrpl/basics/chrono.h.ai.md
Normal file
37
include/xrpl/basics/chrono.h.ai.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# `include/xrpl/basics/chrono.h`
|
||||
|
||||
This header is the central time-abstraction layer for the XRPL codebase. It defines three conceptually distinct things: the network's own clock type (`NetClock`) with its unusual epoch, a pair of stopwatch types for measuring elapsed time in production and test contexts, and formatting utilities that bridge between `NetClock` timestamps and human-readable strings. Nearly every subsystem that cares about ledger close times, validation timestamps, or consensus durations imports this file.
|
||||
|
||||
## The `NetClock` and Its Epoch
|
||||
|
||||
`NetClock` is a C++ *Clock* named type — a class with the required nested `rep`, `period`, `duration`, and `time_point` typedefs — that serves as the tag for all network-relative timestamps on the XRP Ledger. Its `rep` is `std::uint32_t` and its `period` is `std::ratio<1>`, meaning each tick is one second and values are unsigned 32-bit integers. This gives roughly 136 years of range from the epoch, expiring around the year 2136.
|
||||
|
||||
The epoch itself is January 1, 2000 00:00:00 UTC, not the Unix epoch. The offset `epoch_offset` (946684800 seconds) is computed at compile time using the `date::sys_days` facility and verified with a `static_assert`. According to a comment in `TimeKeeper.h`, this epoch was chosen arbitrarily by Arthur Britto and David Schwartz during early development and "no rationale has been provided for this curious and annoying, but otherwise unimportant, choice." The compile-time assertion exists precisely because this magic constant appears throughout serialized protocol data and on-ledger structures — any accidental change would be a silent protocol-breaking bug.
|
||||
|
||||
`is_steady = false` is the correct declaration because `NetClock` tracks wall time, which can be stepped or skewed by NTP. Downstream code cannot assume monotonicity, and this flag ensures standard library machinery treats it accordingly.
|
||||
|
||||
## Epoch Conversion and String Formatting
|
||||
|
||||
Because `NetClock::time_point` values are seconds since 2000, converting them to display strings requires shifting by `epoch_offset` back to the Unix epoch so that the `date` library can format them correctly. Both overloads of `to_string` and `to_string_iso` handle this conversion:
|
||||
|
||||
```cpp
|
||||
return to_string(system_clock::time_point{tp.time_since_epoch() + epoch_offset});
|
||||
```
|
||||
|
||||
The `to_string` variant produces a human-friendly `"YYYY-Mon-DD HH:MM:SS UTC"` format; `to_string_iso` produces ISO 8601 `"YYYY-MM-DDTHH:MM:SSZ"`. Template overloads accepting `date::sys_time<Duration>` allow the same functions to be called with standard UTC time points, while the `NetClock::time_point` overloads handle the epoch shift before delegating. A `static_assert` in `to_string_iso` guards that `NetClock::duration::period` is still `std::ratio<1>`, preventing silent precision loss if the clock's resolution were ever changed.
|
||||
|
||||
## Stopwatch Types for Elapsed-Time Measurement
|
||||
|
||||
`Stopwatch` (`beast::abstract_clock<std::chrono::steady_clock>`) and `TestStopwatch` (`beast::manual_clock<std::chrono::steady_clock>`) form a dependency-injection pair. Production components accept a `Stopwatch&` reference; unit tests supply a `TestStopwatch`, which exposes `set()`, `advance()`, and `operator++()` to move time forward deterministically without sleeping.
|
||||
|
||||
The design follows the pattern documented in `abstract_clock.h`: making `now()` a virtual instance method rather than a static function so the clock can be injected as a dependency. This is why production code never calls `std::chrono::steady_clock::now()` directly — it always goes through the abstraction, enabling full time-control in tests.
|
||||
|
||||
The `stopwatch()` free function returns a global singleton backed by `beast::basic_seconds_clock`. That class uses a background thread to sample `std::chrono::steady_clock` at most once per second and caches the result. Callers that need the current time repeatedly in a tight loop therefore pay only a single atomic load rather than a syscall per iteration, at the cost of up to one second of staleness — acceptable for the consensus and network-overlay subsystems that are the primary consumers.
|
||||
|
||||
## Convenience Duration Aliases
|
||||
|
||||
`days` and `weeks` are `std::chrono::duration` specialisations that fill a gap in C++14/17's `<chrono>` (these became standard in C++20 as `std::chrono::days` and `std::chrono::weeks`). They use `std::ratio_multiply` to derive their periods from `std::chrono::hours::period`, keeping them interoperable with the rest of `<chrono>` arithmetic. They appear in ledger aging, amendment timeouts, and fee-escalation calculations across the codebase.
|
||||
|
||||
## Relationship to `TimeKeeper`
|
||||
|
||||
`TimeKeeper` in `src/xrpld/core/TimeKeeper.h` is the sole concrete implementation of `beast::abstract_clock<NetClock>`. It uses `epoch_offset` from this header to convert `std::chrono::system_clock::now()` into a `NetClock::time_point`, and maintains an atomic `closeOffset_` that nudges the reported close time toward the network-wide consensus view. The split between the clock *type* definition here and the running clock *implementation* in `TimeKeeper` is intentional: it lets protocol-level code reference `NetClock::time_point` without depending on the application-layer time-synchronization logic.
|
||||
42
include/xrpl/basics/comparators.h.ai.json
Normal file
42
include/xrpl/basics/comparators.h.ai.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 18,
|
||||
"name": "less"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 30,
|
||||
"name": "equal_to"
|
||||
}
|
||||
],
|
||||
"description": "Provides MSVC-specific wrappers for std::less and std::equal_to to strip [[nodiscard]] from operator() for compatibility with boost::bimap, and otherwise aliases them directly.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/comparators.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"T const& left",
|
||||
"T const& right"
|
||||
],
|
||||
"lineno": 22,
|
||||
"name": "xrpl::less::operator()"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"T const& left",
|
||||
"T const& right"
|
||||
],
|
||||
"lineno": 34,
|
||||
"name": "xrpl::equal_to::operator()"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 5,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
include/xrpl/basics/comparators.h.ai.md
Normal file
21
include/xrpl/basics/comparators.h.ai.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# `comparators.h` — MSVC Compatibility Shims for `boost::bimap`
|
||||
|
||||
This file exists to paper over a specific compiler/library incompatibility: MSVC 2019 (16.9.0+) added `[[nodiscard]]` to the `operator()` of `std::less` and `std::equal_to`, which collides with how `boost::bimap` validates comparators.
|
||||
|
||||
### The Problem
|
||||
|
||||
`boost::bimap` checks that a provided comparator satisfies the `BinaryFunction` concept by invoking `operator()` and discarding the return value. When MSVC decorated those operators with `[[nodiscard]]`, that validation step began emitting warnings-treated-as-errors (or outright compilation failures), since the entire point of the check is to call the function and throw the result away. The two behaviors are fundamentally at odds: Boost deliberately ignores the return value, and MSVC now insists you don't.
|
||||
|
||||
### The Solution
|
||||
|
||||
Under `_MSC_VER`, `xrpl::less<T>` and `xrpl::equal_to<T>` are thin wrapper structs whose `operator()` delegates to the standard counterparts but is itself *not* marked `[[nodiscard]]`. The `result_type = bool` member alias satisfies the older Boost `BinaryFunction` concept checks, which sometimes inspect this typedef directly. On any non-MSVC compiler, the types are simple `using` aliases to `std::less<T>` and `std::equal_to<T>`, meaning there is zero overhead or behavioral difference on GCC, Clang, or other targets.
|
||||
|
||||
### Usage in the Codebase
|
||||
|
||||
Both known consumers — `Bootcache.h` in the PeerFinder subsystem and `ledgers.h` in the consensus simulation framework — include this header specifically because they use `boost::bimap` with ordered collection types. By substituting `xrpl::less` where `std::less` would ordinarily appear, those files compile cleanly across MSVC and non-MSVC toolchains without any conditional compilation at the call site. The fix is fully transparent to the caller.
|
||||
|
||||
### Design Notes
|
||||
|
||||
The `#else` branch uses `using` aliases rather than repeating the struct definitions — the right call, since it keeps non-MSVC builds on the real standard types with all their optimizations and specializations (including the transparent `void` specializations). The MSVC wrapper structs handle the default `T = void` template argument but do not attempt to re-implement the transparent comparator `void` specialization; this is acceptable because `boost::bimap` collection types always require explicitly typed comparators anyway.
|
||||
|
||||
This is a narrow, surgical fix. The file makes no attempt to be a general comparator utility — it solves exactly one problem (stripped `[[nodiscard]]`) in exactly the context where it appears (ordered `boost::bimap` collections), and nothing more.
|
||||
50
include/xrpl/basics/contract.h.ai.json
Normal file
50
include/xrpl/basics/contract.h.ai.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 17,
|
||||
"name": "title"
|
||||
},
|
||||
{
|
||||
"lineno": 58,
|
||||
"name": "how"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "Provides programming by contract utilities for exception handling, logging, and invariant checking in the xrpl namespace.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/contract.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"title"
|
||||
],
|
||||
"lineno": 17,
|
||||
"name": "LogThrow"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 27,
|
||||
"name": "Rethrow"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"args"
|
||||
],
|
||||
"lineno": 41,
|
||||
"name": "Throw"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"how"
|
||||
],
|
||||
"lineno": 58,
|
||||
"name": "LogicError"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 11,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
56
include/xrpl/basics/contract.h.ai.md
Normal file
56
include/xrpl/basics/contract.h.ai.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# `include/xrpl/basics/contract.h` — Programming by Contract Utilities
|
||||
|
||||
This header implements the XRPL ledger's Programming by Contract (DbC) discipline — a small but critical set of primitives for handling precondition failures, invariant violations, and structured exception throwing throughout the codebase. Rather than leaving each subsystem to throw exceptions ad-hoc, `contract.h` centralises the pattern so that every exception path produces a log entry, and every logic error terminates predictably.
|
||||
|
||||
## The Two Failure Modes
|
||||
|
||||
The file cleanly distinguishes two fundamentally different failure categories:
|
||||
|
||||
**Recoverable runtime errors** are handled by `Throw<E>` and `Rethrow`. These represent conditions the caller is expected to handle — malformed data, failed I/O, invalid configurations. The exception propagates up the stack normally, and callers can `catch` and recover.
|
||||
|
||||
**Unrecoverable logic errors** are handled by `LogicError`. These represent violated invariants — bugs in the program itself, not unexpected input. `LogicError` is declared `noexcept` and ends with `std::abort()`, signalling that there is no safe recovery path.
|
||||
|
||||
## `Throw<E>` — Logged Exception Throwing
|
||||
|
||||
```cpp
|
||||
template <class E, class... Args>
|
||||
[[noreturn]] XRPL_NO_SANITIZE_ADDRESS inline void
|
||||
Throw(Args&&... args)
|
||||
```
|
||||
|
||||
`Throw<E>` is the standard mechanism for raising exceptions across the XRPL codebase. It constructs the exception object, logs a warning that includes the exception's demangled type name (via `beast::type_name<E>()`) and its `what()` message, and then throws by move. The `static_assert` enforces that `E` derives from `std::exception`, preventing careless use of non-standard exception types that would bypass catch-all handlers expecting `std::exception&`.
|
||||
|
||||
The logging before the throw is the key design choice here. Because exceptions can be silently swallowed by broad `catch(...)` handlers or can propagate across subsystem boundaries, having an unconditional warning log at the throw site creates a reliable audit trail even if no handler ever logs the caught exception. In practice, callers across `nodestore`, `json`, and `net` subsystems use this pattern:
|
||||
|
||||
```cpp
|
||||
Throw<std::runtime_error>("lz4_decompress: integer overflow (input)");
|
||||
xrpl::Throw<Json::error>(message);
|
||||
```
|
||||
|
||||
## `Rethrow` — Logged Rethrow
|
||||
|
||||
`Rethrow` wraps the bare `throw;` statement, prepending a warning log entry. The comment in the source is honest about why this exists: `throw;` inside a catch block re-raises the active exception, but this is transparent to logging infrastructure. `Rethrow` makes the re-throw visible in log output, which is valuable when tracking exception propagation chains in production.
|
||||
|
||||
## `LogThrow` — The Logging Primitive
|
||||
|
||||
`LogThrow(title)` is the shared sink called by both `Throw<E>` and `Rethrow`. The implementation (in `contract.cpp`) routes to `JLOG(debugLog().warn())`, feeding into the structured ledger logging framework. It takes a human-readable title string — in `Throw<E>`, this is assembled as `"Throwing exception of type <typename>: <what()>"`.
|
||||
|
||||
## `LogicError` — Unrecoverable Invariant Violations
|
||||
|
||||
```cpp
|
||||
[[noreturn]] void LogicError(std::string const& how) noexcept;
|
||||
```
|
||||
|
||||
`LogicError` is a terminate path. Its implementation logs at `fatal` severity, writes directly to `std::cerr` as a belt-and-suspenders safeguard, fires the `UNREACHABLE` instrumentation macro (which is `assert(false)` in debug builds and a no-op in release/fuzzing mode via the Antithesis SDK integration), and then calls `std::abort()`. The entire body is wrapped in `// LCOV_EXCL_START/STOP` because correctly-operating code should never reach it — coverage tools would flag it as untested dead code without the exclusion.
|
||||
|
||||
The `noexcept` marker is meaningful: a function signalling a logic error must not throw, because it's called from sites that are already in an undefined or corrupted state. `noexcept` also helps the compiler understand this is a hard termination point.
|
||||
|
||||
The comment in `contract.cpp` explains a deliberate naming convention: `UNREACHABLE("LogicError", {{"message", s}})` is the only callsite that passes a dynamic message parameter to the instrumentation macro, because `LogicError` is a convergence point for many different unrelated execution paths — unlike the named per-feature `XRPL_ASSERT` calls elsewhere in the codebase.
|
||||
|
||||
## ASAN Suppression
|
||||
|
||||
Both `Throw<E>` and `Rethrow` are annotated with `XRPL_NO_SANITIZE_ADDRESS`, defined in `sanitizers.h` as `__attribute__((no_sanitize("address", "hwaddress")))` on GCC/Clang. Address Sanitizer tracks memory state through normal control flow but poorly handles the non-local jumps caused by C++ exceptions — the unwinding path can trigger false positive stack-use-after-scope or heap-use-after-free reports. The annotation suppresses instrumentation for these specific functions without disabling ASAN globally, keeping sanitizer coverage intact everywhere exceptions are not thrown.
|
||||
|
||||
## Design Rationale
|
||||
|
||||
The primitive set is deliberately minimal. There is no `Precondition()` or `Postcondition()` macro — callers simply call `Throw<E>` at the point of detected violation, with a descriptive message. This keeps the abstraction thin while providing the essential guarantees: every exception is logged before it leaves its origin, type safety is statically enforced, and logic errors terminate loudly rather than silently corrupting state.
|
||||
42
include/xrpl/basics/hardened_hash.h.ai.json
Normal file
42
include/xrpl/basics/hardened_hash.h.ai.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 56,
|
||||
"name": "HashAlgorithm"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 18,
|
||||
"name": "state_t"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"HashAlgorithm = beast::xxhasher"
|
||||
],
|
||||
"lineno": 56,
|
||||
"name": "hardened_hash"
|
||||
}
|
||||
],
|
||||
"description": "Provides a hardened hash functor for use in hash-based containers, using a seeded hash algorithm to resist adversarial inputs. Includes utilities for generating random seeds and a template class for hashing with customizable algorithms.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/hardened_hash.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 13,
|
||||
"name": "make_seed_pair"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 8,
|
||||
"name": "xrpl"
|
||||
},
|
||||
{
|
||||
"lineno": 10,
|
||||
"name": "detail"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
include/xrpl/basics/hardened_hash.h.ai.md
Normal file
33
include/xrpl/basics/hardened_hash.h.ai.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# `hardened_hash.h` — Per-Instance Seeded Hash Functor
|
||||
|
||||
This header addresses a concrete security problem for any server that accepts external input and stores it in hash-based containers: **hash-flooding attacks**. An adversary who knows the hash algorithm can craft keys that all map to the same bucket, degrading `O(1)` container operations to `O(n)`. For XRPL — which receives transactions, peer messages, and ledger objects from untrusted sources — storing such data in standard `std::unordered_*` containers without mitigation would expose the node to denial-of-service.
|
||||
|
||||
`hardened_hash<HashAlgorithm>` solves this by generating a fresh pair of 64-bit random seeds at construction time and injecting them into the hash algorithm. An attacker who cannot predict the seeds cannot craft collisions.
|
||||
|
||||
## Seed Generation: `make_seed_pair()`
|
||||
|
||||
The private `detail::make_seed_pair()` function holds a function-local static `state_t` that owns a `std::random_device` (OS entropy), a `std::mt19937_64` PRNG seeded from it at startup, and a uniform distribution over `uint64_t`. A `std::mutex` guards the shared mutable state so that concurrent construction of `hardened_hash` instances from multiple threads is safe. The C++11 guarantee of thread-safe static initialization handles the one-time construction of `state_t` itself; the mutex handles every subsequent call to draw two random seeds.
|
||||
|
||||
The `bool = true` non-type template parameter on `make_seed_pair` is a deliberate extensibility hook — it allows test code to explicitly specialize the function template and inject deterministic seeds, without changing the default behavior in production.
|
||||
|
||||
## `hardened_hash<HashAlgorithm>` Class
|
||||
|
||||
The class stores a single `detail::seed_pair` (two `uint64_t` values) generated at construction. Its `operator()` builds a fresh `HashAlgorithm` seeded with those values, then dispatches through `beast::hash_append` to feed the object being hashed into it. The result is extracted via the hasher's `explicit operator result_type()` conversion.
|
||||
|
||||
Because seeds are stored per-instance rather than globally, each container gets its own independent randomization. A hash-set and a hash-map holding the same key type will produce different bucket distributions for the same input — this reduces the blast radius if any single seed is ever somehow leaked or guessed.
|
||||
|
||||
## The `hash_append` Protocol
|
||||
|
||||
Types must be made hashable by providing a free function `hash_append(Hasher&, T const&)` discoverable via ADL. The function is expected to forward-append each constituent field of `T` into the hasher, recursively. This is the composable, algorithm-agnostic hashing design from the N3333/P0029 proposal family. It cleanly decouples the hash algorithm from the type being hashed: the same `T` can be hashed with `xxhasher`, a BLAKE variant, or any future algorithm without modifying `T`.
|
||||
|
||||
## Default Algorithm: `beast::xxhasher`
|
||||
|
||||
The default `HashAlgorithm` is `beast::xxhasher`, a wrapper around XXH3-64 with seed support. The implementation maintains a 64-byte internal buffer to avoid the streaming API for small inputs, only spilling to an `XXH3_state_t` when data exceeds that buffer. The two-seed constructor `xxhasher(Seed seed, Seed)` accepts both values but only uses the first as the XXH3 seed — the second parameter is effectively ignored, leaving room for a future extension without breaking the two-seed interface that `hardened_hash` provides.
|
||||
|
||||
The header comment explicitly prohibits using Murmur or CityHash as the `HashAlgorithm`, citing the SipHash paper (https://131002.net/siphash/#at). Both of those algorithms are known to be trivially attackable via differential cryptanalysis once an attacker can observe output. XXH3 with a secret seed provides practical resistance to this class of attack for non-cryptographic use cases.
|
||||
|
||||
## Integration via `UnorderedContainers.h`
|
||||
|
||||
`UnorderedContainers.h` consumes `hardened_hash` to define the `hardened_hash_map`, `hardened_hash_set`, `hardened_hash_multimap`, `hardened_hash_multiset`, and `hardened_partitioned_hash_map` type aliases — all defaulting to `hardened_hash<beast::xxhasher>` (`strong_hash`). That file explicitly documents the split: use plain `hash_*` aliases (backed by `beast::uhash`) for internal data unreachable by adversaries; use `hardened_hash_*` aliases anywhere external data lands. This makes the security intent visible at the call site rather than buried in template arguments.
|
||||
|
||||
Callers like `AccountID.cpp`, `HashRouter`, `CachedView`, and `AssetCache` all use the hardened variants for data structures keyed on ledger objects or peer-supplied identifiers — the exact scenarios where hash-flooding is a realistic threat.
|
||||
91
include/xrpl/basics/join.h.ai.json
Normal file
91
include/xrpl/basics/join.h.ai.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 7,
|
||||
"name": "s"
|
||||
},
|
||||
{
|
||||
"lineno": 7,
|
||||
"name": "iter"
|
||||
},
|
||||
{
|
||||
"lineno": 7,
|
||||
"name": "end"
|
||||
},
|
||||
{
|
||||
"lineno": 7,
|
||||
"name": "delimiter"
|
||||
},
|
||||
{
|
||||
"lineno": 23,
|
||||
"name": "c"
|
||||
},
|
||||
{
|
||||
"lineno": 23,
|
||||
"name": "delim"
|
||||
},
|
||||
{
|
||||
"lineno": 43,
|
||||
"name": "c"
|
||||
},
|
||||
{
|
||||
"lineno": 43,
|
||||
"name": "delim"
|
||||
},
|
||||
{
|
||||
"lineno": 62,
|
||||
"name": "c"
|
||||
},
|
||||
{
|
||||
"lineno": 62,
|
||||
"name": "delim"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"c",
|
||||
"delim"
|
||||
],
|
||||
"lineno": 18,
|
||||
"name": "CollectionAndDelimiter"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"c",
|
||||
"delim"
|
||||
],
|
||||
"lineno": 38,
|
||||
"name": "CollectionAndDelimiter<Collection[N]>"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"c",
|
||||
"delim"
|
||||
],
|
||||
"lineno": 57,
|
||||
"name": "CollectionAndDelimiter<char[N]>"
|
||||
}
|
||||
],
|
||||
"description": "Provides utilities for joining collections into a stream with a delimiter, including template classes for handling various collection types and specializations for arrays and C-strings.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/join.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"s",
|
||||
"iter",
|
||||
"end",
|
||||
"delimiter"
|
||||
],
|
||||
"lineno": 7,
|
||||
"name": "join"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 4,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
37
include/xrpl/basics/join.h.ai.md
Normal file
37
include/xrpl/basics/join.h.ai.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# `join.h` — Stream-Based Collection Joining with Delimiter
|
||||
|
||||
This header provides a small but general-purpose utility for rendering the elements of any iterable collection into a stream with a separator between each element — the XRPL equivalent of Python's `str.join()` or Boost's `algorithm::join`, but targeted at streams rather than string construction.
|
||||
|
||||
## Core Algorithm
|
||||
|
||||
The free function `join(Stream& s, Iter iter, Iter end, std::string const& delimiter)` is the foundation. It handles the classic "join without trailing delimiter" pattern: the first element is written directly, and every subsequent element is prefixed by the delimiter. An empty range short-circuits immediately. Returning the stream reference allows the call to compose naturally within chained `<<` expressions.
|
||||
|
||||
The design choice to accept iterators rather than a collection directly keeps this function maximally generic — it works with any input-iterator pair, whether from a standard container, a raw pointer range, or a custom sequence.
|
||||
|
||||
## The `CollectionAndDelimiter` Wrapper
|
||||
|
||||
The raw iterator-based `join()` is not convenient at a call site like a logging statement. The `CollectionAndDelimiter<Collection>` class solves this by bundling a collection reference and a delimiter string together into a single value that can be inserted into any stream with `operator<<`. This is the primary API that callers actually use.
|
||||
|
||||
The real-world use in `Pathfinder.cpp` illustrates the motivation perfectly:
|
||||
|
||||
```cpp
|
||||
JLOG(j_.debug()) << "addPathsForType " << CollectionAndDelimiter(pathType, ", ");
|
||||
```
|
||||
|
||||
Without this wrapper, the developer would need to manually loop or build an intermediate string just to log a path type list. The wrapper defers the work until the stream actually needs the data, which fits naturally into XRPL's conditional-logging idiom — if the debug level is disabled, the stream never evaluates the insertion at all.
|
||||
|
||||
The delimiter is stored by value inside the wrapper (`std::string const delimiter`), while the collection is stored by `const&`. This means the wrapper is a lightweight view: it borrows the collection but owns a copy of the delimiter string. Callers should ensure the collection outlives the wrapper, which is naturally satisfied when both are in the same expression or scope.
|
||||
|
||||
## Specializations for Arrays and C-Strings
|
||||
|
||||
Two partial specializations of `CollectionAndDelimiter` handle cases where template argument deduction would otherwise produce unworkable types.
|
||||
|
||||
The `CollectionAndDelimiter<Collection[N]>` specialization handles C-style arrays of non-character types (e.g., `char letters[4]` or `std::string words[5]`). Since raw arrays decay to pointers in most template contexts, this specialization captures the element type and size as separate template parameters and constructs the iterator range as `collection` to `collection + N`. The collection is stored as a plain pointer rather than a reference-to-array to avoid array reference decay issues.
|
||||
|
||||
The `CollectionAndDelimiter<char[N]>` specialization is the most defensive of the three. A `char` array might be a C-style string with a null terminator occupying the last position of the array — iterating through it character-by-character and including `'\0'` in the output would be wrong. The `operator<<` implementation therefore checks whether the last element of the array is the null terminator and, if so, backs the end iterator up by one before delegating to `join()`. This correctly handles the common case of a string literal like `"string"` (which has `N=7` but only 6 printable characters) as well as the degenerate case of `""` (which produces no output at all).
|
||||
|
||||
Note that when a `std::string` is passed, it matches the primary template since `std::string` is a proper range with `begin()`/`end()` iterators — it iterates character by character, so `CollectionAndDelimiter(std::string{"hello"}, "-")` produces `"h-e-l-l-o"`. This is intentional and consistent behavior across all sequence types.
|
||||
|
||||
## Relationship to the Broader Codebase
|
||||
|
||||
Within the `xrpl/basics/` module, this file sits alongside other small utility headers (`strHex.h`, `toString.h`, etc.) that fill gaps in the standard library for common formatting tasks. The pattern of composing stream-insertable value objects is consistent with how other XRPL logging helpers are structured. The only current production caller is `Pathfinder::addPathsForType()`, but the utility is generic enough to serve any component that needs to log or serialize a collection in a readable form.
|
||||
53
include/xrpl/basics/make_SSLContext.h.ai.json
Normal file
53
include/xrpl/basics/make_SSLContext.h.ai.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 10,
|
||||
"name": "cipherList"
|
||||
},
|
||||
{
|
||||
"lineno": 15,
|
||||
"name": "keyFile"
|
||||
},
|
||||
{
|
||||
"lineno": 16,
|
||||
"name": "certFile"
|
||||
},
|
||||
{
|
||||
"lineno": 17,
|
||||
"name": "chainFile"
|
||||
},
|
||||
{
|
||||
"lineno": 18,
|
||||
"name": "cipherList"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "This file declares functions for creating SSL contexts (both self-signed and authenticated) for use with Boost.Asio in the xrpl namespace.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/make_SSLContext.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"cipherList"
|
||||
],
|
||||
"lineno": 10,
|
||||
"name": "make_SSLContext"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"keyFile",
|
||||
"certFile",
|
||||
"chainFile",
|
||||
"cipherList"
|
||||
],
|
||||
"lineno": 14,
|
||||
"name": "make_SSLContextAuthed"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 6,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
37
include/xrpl/basics/make_SSLContext.h.ai.md
Normal file
37
include/xrpl/basics/make_SSLContext.h.ai.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# `include/xrpl/basics/make_SSLContext.h`
|
||||
|
||||
This header is the public interface for creating TLS/SSL contexts used across the XRPL node's two distinct network-facing subsystems: the peer-to-peer overlay network and the HTTP/WebSocket RPC server. It declares exactly two factory functions, keeping all OpenSSL implementation details confined to the corresponding `.cpp` translation unit.
|
||||
|
||||
## The Two Security Modes
|
||||
|
||||
The header reflects a deliberate architectural split in how XRPL secures its connections:
|
||||
|
||||
`make_SSLContext(cipherList)` creates a context for **anonymous TLS**, used between validator/relay nodes in the overlay network. Peer identity in the overlay is not established by TLS certificates — it is established by XRPL's own cryptographic node identity scheme. TLS here serves purely as a transport encryption layer, so a self-signed ephemeral certificate is sufficient. The returned context is configured with `verify_none`, meaning neither side validates the other's certificate.
|
||||
|
||||
`make_SSLContextAuthed(keyFile, certFile, chainFile, cipherList)` creates a context for **certificate-authenticated TLS**, used by the RPC/HTTP server when an operator supplies their own key and certificate files. This is appropriate for external-facing endpoints where clients (wallets, applications, monitoring tools) need server identity assurance. The `chainFile` parameter supports intermediate certificate chains, allowing operators to use CA-issued certificates without embedding the full chain in the certificate file itself.
|
||||
|
||||
## Shared Security Baseline
|
||||
|
||||
Both functions delegate to an internal `get_context()` helper that enforces a shared hardened baseline regardless of authentication mode:
|
||||
|
||||
- SSLv2, SSLv3, TLSv1.0, and TLSv1.1 are all disabled — only TLS 1.2 and above are accepted.
|
||||
- TLS compression is disabled (mitigates CRIME-class attacks).
|
||||
- TLS renegotiation is disabled via `SSL_OP_NO_RENEGOTIATION`, which guards against CVE-2021-3499 on older OpenSSL versions.
|
||||
- The default cipher list `"TLSv1.2:!CBC:!DSS:!PSK:!eNULL:!aNULL"` excludes block-cipher modes (CBC), DSS-based suites, pre-shared key suites, and any suites lacking encryption or authentication.
|
||||
- Pre-generated 2048-bit Diffie-Hellman parameters are embedded directly in the binary (generated via `openssl dhparam 2048`). Hardcoding these avoids the startup latency of runtime generation and is safe because DH parameters are not secret — only the ephemeral DH keypairs need to remain secret, and those are handled by `single_dh_use`.
|
||||
|
||||
## Self-Signed Certificate Design
|
||||
|
||||
The anonymous context's certificate generation reveals several non-obvious defensive choices. The RSA key and X.509 certificate are created once as function-local statics, meaning they are shared across every call to `make_SSLContext` within a process lifetime. This is intentional: the certificate is ephemeral by design (it is regenerated on each server restart), so there is no value in creating multiple distinct certificates per connection.
|
||||
|
||||
The certificate validity start time is backdated by 25 hours. This prevents a network observer from inferring the server's precise startup time from the certificate's `notBefore` field — a subtle privacy consideration that reduces side-channel information leakage. The certificate is set valid for two years from creation, and carries a 128-bit randomly generated serial number to avoid collisions in logs or caches.
|
||||
|
||||
X.509v3 extensions are set to mark the certificate as a non-CA leaf (`CA:FALSE`), restrict key usage to `digitalSignature`, and allow the certificate for both `serverAuth` and `clientAuth` extended key usage — the latter because overlay connections are mutually encrypted (not strictly client/server asymmetric).
|
||||
|
||||
## Error Handling Philosophy
|
||||
|
||||
All failures in context construction call `LogicError()`, which terminates the process. This is appropriate because SSL context creation is a startup-time prerequisite: if the TLS layer cannot be initialized (e.g., invalid cipher list, missing key file, mismatched key and certificate), the node cannot operate safely and there is no meaningful recovery path. The authenticated path additionally calls `SSL_CTX_check_private_key` to verify that the loaded private key actually corresponds to the loaded certificate before returning, catching misconfigured deployments at startup rather than at connection time.
|
||||
|
||||
## Callers
|
||||
|
||||
In `OverlayImpl.cpp`, `make_SSLContext("")` (empty cipher list, falling back to the default) is called unconditionally during overlay setup — peer connections always use anonymous TLS. In `ServerHandler.cpp`, the choice between the two functions is made at port configuration time: if any of `ssl_key`, `ssl_cert`, or `ssl_chain` are populated, `make_SSLContextAuthed` is called; otherwise, `make_SSLContext` is used. Operators can override the cipher list on a per-port basis via the `ssl_ciphers` config directive, which is passed through as the `cipherList` argument.
|
||||
37
include/xrpl/basics/mulDiv.h.ai.json
Normal file
37
include/xrpl/basics/mulDiv.h.ai.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 17,
|
||||
"name": "value"
|
||||
},
|
||||
{
|
||||
"lineno": 17,
|
||||
"name": "mul"
|
||||
},
|
||||
{
|
||||
"lineno": 17,
|
||||
"name": "div"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "Provides a utility function to perform multiplication and division in a single step with overflow checking, returning an optional result.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/mulDiv.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"value",
|
||||
"mul",
|
||||
"div"
|
||||
],
|
||||
"lineno": 17,
|
||||
"name": "mulDiv"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 6,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
include/xrpl/basics/mulDiv.h.ai.md
Normal file
26
include/xrpl/basics/mulDiv.h.ai.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# `include/xrpl/basics/mulDiv.h`
|
||||
|
||||
This header declares a single arithmetic utility, `mulDiv()`, that computes `value * mul / div` on unsigned 64-bit integers without intermediate overflow and without sacrificing precision. It also exposes `muldiv_max`, a `constexpr` alias for `std::numeric_limits<std::uint64_t>::max()`, which callers use as a natural overflow sentinel.
|
||||
|
||||
## The problem it solves
|
||||
|
||||
A direct evaluation of `value * mul / div` on `uint64_t` silently overflows whenever the product `value * mul` exceeds 2⁶⁴−1, which is common in ledger fee calculations where both operands can be near the full 64-bit range. Splitting the operation into two steps makes this worse, not better — intermediate truncation in integer division also discards precision. `mulDiv` fixes both issues at once.
|
||||
|
||||
## Implementation strategy
|
||||
|
||||
The implementation (`src/libxrpl/basics/mulDiv.cpp`) uses `boost::multiprecision::uint128_t` as the scratch type. The 64-bit inputs are multiplied into the 128-bit accumulator via Boost's `multiply()`, then divided in place. Only at the very end does the code check whether the 128-bit result fits back into a `uint64_t`; if it exceeds `muldiv_max` the function returns `std::nullopt`. This approach is exact — no floating-point rounding, no two-step integer approximation.
|
||||
|
||||
## Return convention and caller responsibility
|
||||
|
||||
The function is declared noexcept-by-convention (the header comment explicitly says "Throws: None") and returns `std::optional<std::uint64_t>`. This shifts the overflow decision to the caller, which is important because different callers have different policies:
|
||||
|
||||
- `LoadFeeTrack.cpp` treats overflow as a logic error and propagates an `std::overflow_error` via `Throw<>`.
|
||||
- `TxQ.cpp` uses `value_or(xrpl::muldiv_max)` throughout — clamping to the maximum integer is acceptable for fee-level arithmetic where "astronomical fee" is a safe ceiling.
|
||||
|
||||
The `muldiv_max` constant is exported from this header precisely to support the `value_or` pattern consistently across call sites.
|
||||
|
||||
## Usage in practice
|
||||
|
||||
Every call site in the ledger involves proportional fee scaling: converting raw fees to fee levels, applying percentage adjustments, escalating fees based on queue depth, or scaling a base fee by a load factor. In all cases the multiplication would overflow `uint64_t` for large inputs, but the mathematical result still fits once divided — exactly the scenario `mulDiv` is built for.
|
||||
|
||||
The test suite (`src/tests/libxrpl/basics/mulDiv.cpp`) confirms correct results for values near `UINT64_MAX`, verifies commutativity of `value` and `mul`, checks zero-operand edge cases, and asserts that `std::nullopt` is returned when the division cannot bring the 128-bit product back under the 64-bit ceiling.
|
||||
60
include/xrpl/basics/partitioned_unordered_map.h.ai.json
Normal file
60
include/xrpl/basics/partitioned_unordered_map.h.ai.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 11,
|
||||
"name": "key"
|
||||
},
|
||||
{
|
||||
"lineno": 16,
|
||||
"name": "key"
|
||||
},
|
||||
{
|
||||
"lineno": 137,
|
||||
"name": "partitions"
|
||||
}
|
||||
],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"std::optional<std::size_t> partitions = std::nullopt"
|
||||
],
|
||||
"lineno": 23,
|
||||
"name": "partitioned_unordered_map"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 41,
|
||||
"name": "iterator"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 87,
|
||||
"name": "const_iterator"
|
||||
}
|
||||
],
|
||||
"description": "This file defines a thread-partitioned unordered map container template (partitioned_unordered_map) for concurrent or partitioned access, along with supporting iterator types and utility functions for key extraction and partitioning.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/partitioned_unordered_map.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"key"
|
||||
],
|
||||
"lineno": 11,
|
||||
"name": "extract"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"key"
|
||||
],
|
||||
"lineno": 16,
|
||||
"name": "extract"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 10,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
56
include/xrpl/basics/partitioned_unordered_map.h.ai.md
Normal file
56
include/xrpl/basics/partitioned_unordered_map.h.ai.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# `partitioned_unordered_map.h`
|
||||
|
||||
## Role and Purpose
|
||||
|
||||
This header introduces `partitioned_unordered_map`, a sharded hash map whose primary purpose is to reduce lock contention in multi-threaded workloads. Rather than wrapping a single `std::unordered_map` with one coarse-grained lock, the container holds a `std::vector` of independent `std::unordered_map` instances ("partitions"). Each key deterministically belongs to exactly one partition, so callers who know the key can lock only that partition — leaving every other partition free for concurrent access.
|
||||
|
||||
The container itself provides **no synchronization primitives**. That is an intentional design choice: the container exposes its raw `partition_map_type` (the vector of maps) through `map()`, and callers take responsibility for locking individual entries in that vector. The split between "partition selection" and "mutual exclusion" allows each consumer to use whatever locking mechanism suits it — `std::recursive_mutex`, `packed_spinlock`, or anything else.
|
||||
|
||||
## Partition Selection and the `extract()` Hook
|
||||
|
||||
Keys are mapped to partitions by `extract(key) % partitions_`. The free-function template `extract()` is a customization point separate from the map's `Hash` template parameter. The general template simply casts the key to `std::size_t`, suiting integer-like types. Two specializations in this header and two in sibling headers override it for richer types:
|
||||
|
||||
- `std::string` — hashes via `beast::uhash<>{}`.
|
||||
- `uint256` — reads the first `sizeof(std::size_t)` bytes of the 256-bit value via `memcpy` (avoiding UB from unaligned access).
|
||||
- `SHAMapHash` — same byte-extraction trick applied to its inner `uint256`.
|
||||
|
||||
Separating the partition key from the hash function matters because the hash function may be cryptographically randomized (as in `hardened_hash`) while the partition index must be **stable across calls** to ensure a key always lands in the same shard. The `extract()` convention achieves that stability without coupling partitioning logic to the `Hash` template.
|
||||
|
||||
## Constructor and Partition Count
|
||||
|
||||
```cpp
|
||||
partitioned_unordered_map(std::optional<std::size_t> partitions = std::nullopt)
|
||||
```
|
||||
|
||||
When `partitions` is omitted or zero, the container defaults to `std::thread::hardware_concurrency()`, aligning the shard count with the number of logical CPU cores. This is a pragmatic default: one thread per core can own one shard, eliminating contention under ideally scheduled workloads. An `XRPL_ASSERT` guards against the edge case where `hardware_concurrency()` returns zero.
|
||||
|
||||
## Iterator Design
|
||||
|
||||
The nested `iterator` and `const_iterator` structs navigate a two-level structure using two sub-iterators:
|
||||
|
||||
- `ait_` — a `partition_map_type::iterator` that points to the current inner `unordered_map`.
|
||||
- `mit_` — a `map_type::iterator` that points to the current element within that inner map.
|
||||
|
||||
The `inc()` helper advances `mit_`; when it exhausts a partition, it steps `ait_` forward until it finds a non-empty partition or reaches the end of the vector. `begin()` applies the same logic to skip leading empty partitions. `end()` points `ait_` past the vector's end and `mit_` to the last partition's `end()`.
|
||||
|
||||
Two points are worth noting. First, equality comparison checks all three of `map_`, `ait_`, and `mit_`, so iterators from different `partitioned_unordered_map` instances correctly compare unequal. Second, `const_iterator` holds a non-const `map_type::iterator` internally (matching the non-const `partition_map_type*`); `const`-correctness at the element level is enforced by returning `const_reference` from `operator*()`.
|
||||
|
||||
## Operations
|
||||
|
||||
`find()` computes the partition index from the key, then delegates to the underlying `unordered_map::find()` within that shard. A miss returns `end()`. Both `emplace()` overloads — piecewise-construction and key-value forwarding — follow the same pattern: select the partition from the key, emplace into it, and wrap the returned iterator.
|
||||
|
||||
`erase()` removes an element and then advances the iterator through any trailing empty partitions, so the returned iterator is valid for continued forward traversal.
|
||||
|
||||
`size()` is O(N) — it accumulates counts across all partitions. This is a deliberate tradeoff; maintaining an atomic counter would add write contention on every insert and erase, undermining the sharding benefit.
|
||||
|
||||
`operator[]` provides straightforward subscript access, routing through `map_[partitioner(key)]`.
|
||||
|
||||
## Usage in `TaggedCache`
|
||||
|
||||
The only consumer in the codebase is `TaggedCache`, which instantiates the container via the `hardened_partitioned_hash_map` alias defined in `UnorderedContainers.h`:
|
||||
|
||||
```cpp
|
||||
using hardened_partitioned_hash_map = partitioned_unordered_map<Key, Value, hardened_hash<xxhasher>, ...>;
|
||||
```
|
||||
|
||||
`TaggedCache` exposes a `sweepHelper()` that receives an individual `partition_map_type` entry (one `unordered_map`) along with a held `std::lock_guard`, and processes that shard independently. The sweep can therefore spawn one thread per partition, working in parallel while each thread holds only its own per-partition lock — exactly the concurrency pattern the container's design enables.
|
||||
92
include/xrpl/basics/random.h.ai.json
Normal file
92
include/xrpl/basics/random.h.ai.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [],
|
||||
"description": "Provides deterministic, thread-local, non-cryptographically secure pseudo-random number generation utilities, including functions for generating random integers, bytes, and booleans, using a default PRNG engine.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/random.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 32,
|
||||
"name": "default_prng"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"engine",
|
||||
"min",
|
||||
"max"
|
||||
],
|
||||
"lineno": 65,
|
||||
"name": "rand_int"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"min",
|
||||
"max"
|
||||
],
|
||||
"lineno": 74,
|
||||
"name": "rand_int"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"engine",
|
||||
"max"
|
||||
],
|
||||
"lineno": 79,
|
||||
"name": "rand_int"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"max"
|
||||
],
|
||||
"lineno": 84,
|
||||
"name": "rand_int"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"engine"
|
||||
],
|
||||
"lineno": 89,
|
||||
"name": "rand_int"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 94,
|
||||
"name": "rand_int"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"engine"
|
||||
],
|
||||
"lineno": 104,
|
||||
"name": "rand_byte"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 113,
|
||||
"name": "rand_byte"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"engine"
|
||||
],
|
||||
"lineno": 121,
|
||||
"name": "rand_bool"
|
||||
},
|
||||
{
|
||||
"args": [],
|
||||
"lineno": 126,
|
||||
"name": "rand_bool"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 11,
|
||||
"name": "xrpl"
|
||||
},
|
||||
{
|
||||
"lineno": 19,
|
||||
"name": "detail"
|
||||
}
|
||||
]
|
||||
}
|
||||
34
include/xrpl/basics/random.h.ai.md
Normal file
34
include/xrpl/basics/random.h.ai.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# `include/xrpl/basics/random.h`
|
||||
|
||||
This header provides the XRPL ledger's general-purpose pseudo-random number generation layer. It deliberately occupies a narrow scope: fast, deterministic, thread-safe non-cryptographic randomness for simulation, jitter, test data, and protocol logic that does not require unpredictability guarantees. It is explicitly excluded from any use in key generation, IVs, or security-sensitive contexts.
|
||||
|
||||
## The Engine: `beast::xor_shift_engine`
|
||||
|
||||
The underlying generator is `beast::xor_shift_engine`, an xorshift128+ implementation with a `uint64_t` result type. The algorithm maintains two 64-bit state words, advances them with XOR-shift operations per the Vigna reference implementation, and mixes the seed through MurmurHash3 finalizer constants (`0xff51afd7ed558ccd` and `0xc4ceb9fe1a85ec53`) to ensure the state is well-distributed even from low-entropy seeds. The engine satisfies C++11's `UniformRandomBitGenerator` concept — it provides `min()`, `max()`, and `operator()` — making it compatible with `std::uniform_int_distribution` and all standard distribution types.
|
||||
|
||||
Two `static_assert`s at namespace scope guard the engine contract: the result type must be unsigned integral, and its maximum must be at least as large as `uint64_t::max`. These are guarded with `#ifndef __INTELLISENSE__` to suppress false-positive diagnostic noise in IDEs, which sometimes fail to evaluate constant expressions across template instantiation boundaries.
|
||||
|
||||
## `default_prng()`: A Two-Level Seeding Hierarchy
|
||||
|
||||
The design challenge for thread-local PRNGs is giving each thread a distinct, high-quality seed without paying the cost of `std::random_device` on every thread startup (which can be slow or even blocking on some systems). `default_prng()` solves this with a two-level hierarchy:
|
||||
|
||||
1. A single `static beast::xor_shift_engine seeder` is initialized once from `std::random_device`, which provides true entropy at program startup. A `static std::mutex` serializes all accesses to this seeder.
|
||||
2. Each thread gets a `thread_local beast::xor_shift_engine engine` that is seeded lazily on first access by drawing one value from `seeder` under the mutex lock. After initialization, the thread-local engine runs entirely independently with zero contention.
|
||||
|
||||
This approach ensures that threads never share RNG state, avoiding the need for per-call locking while still guaranteeing statistically independent sequences across threads. The `std::uniform_int_distribution<uint64_t>` with lower bound `1` is used when seeding to respect the engine's constraint that seed zero is invalid (which would throw `std::domain_error` in `xor_shift_engine::seed()`).
|
||||
|
||||
## `rand_int`: Overload Family
|
||||
|
||||
The `rand_int` family provides six overloads covering every combination of: with/without explicit engine, with/without min, with/without max. All are constrained with `std::enable_if_t<std::is_integral<Integral>::value>` to prevent misuse with floating-point or enum types. The engine-taking variants additionally require `detail::is_engine<Engine>::value`, which is a type alias for `std::is_invocable_r<Result, Engine>` — a minimal duck-typing check that the type can be called with no arguments and returns its `result_type`. All overloads delegate to `std::uniform_int_distribution`, and the comment in the implementation acknowledges that constructing the distribution object should be negligible cost, with a note to optimize if profiling reveals otherwise.
|
||||
|
||||
The `XRPL_ASSERT` on the two-argument form checks `max > min` (strict), which is slightly tighter than `uniform_int_distribution`'s requirement of `min <= max`. Equal bounds are not supported through this API, which is a defensible choice since calling `rand_int(5, 5)` is almost certainly a bug.
|
||||
|
||||
## `rand_byte` and `rand_bool`
|
||||
|
||||
`rand_byte` is constrained to exactly `unsigned char` or `uint8_t` (not any integral type) via a conjunction in `std::enable_if_t`. The internal implementation routes through `rand_int<Engine, std::uint32_t>` rather than `rand_int<Engine, Byte>` — this sidesteps potential implementation-defined behavior in `std::uniform_int_distribution<uint8_t>` on platforms where the standard library is not required to handle byte-wide integer distributions.
|
||||
|
||||
`rand_bool` is the simplest primitive: `rand_int(engine, 1) == 1`, producing a fair coin flip. The symmetric phrasing (rather than `rand_int(engine, 1) != 0`) is intentional for clarity.
|
||||
|
||||
## Usage Context
|
||||
|
||||
The header is included across networking subsystems (`peerfinder`, `overlay`), consensus code, HTTP/WebSocket work queues, and extensively throughout the test suite. In production code the role is typically non-security jitter — for example, randomizing peer selection order or staggering reconnect timers — while tests use the engine-taking overloads with deterministic seeds to produce reproducible random data. The engine-taking overloads exist precisely to support this dual use: callers that need reproducibility pass their own seeded engine; callers that just want "some randomness" omit it and get the thread-local default.
|
||||
9
include/xrpl/basics/rocksdb.h.ai.json
Normal file
9
include/xrpl/basics/rocksdb.h.ai.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [],
|
||||
"description": "This header file conditionally includes various RocksDB headers if XRPL_ROCKSDB_AVAILABLE is defined, providing access to RocksDB database functionality.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/rocksdb.h",
|
||||
"functions": [],
|
||||
"language": "c header",
|
||||
"namespaces": []
|
||||
}
|
||||
34
include/xrpl/basics/rocksdb.h.ai.md
Normal file
34
include/xrpl/basics/rocksdb.h.ai.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# `include/xrpl/basics/rocksdb.h`
|
||||
|
||||
## Role and Purpose
|
||||
|
||||
This header is a thin aggregation shim that centralizes all RocksDB public API includes behind a single compile-time feature flag. Rather than scattering `#if XRPL_ROCKSDB_AVAILABLE` guards and individual `<rocksdb/…>` includes across every translation unit that touches the database layer, the codebase funnels all such inclusions through this one file. Any source file that needs RocksDB types simply includes `<xrpl/basics/rocksdb.h>` and never needs to mention the feature flag itself.
|
||||
|
||||
## The `XRPL_ROCKSDB_AVAILABLE` Guard
|
||||
|
||||
The macro `XRPL_ROCKSDB_AVAILABLE` is set to `1` via a CMake target property when the RocksDB library is found during the build. The relevant line in `CMakeLists.txt`:
|
||||
|
||||
```
|
||||
PROPERTIES INTERFACE_COMPILE_DEFINITIONS XRPL_ROCKSDB_AVAILABLE=1
|
||||
```
|
||||
|
||||
This means the macro is propagated as an interface definition on the CMake target, so any consumer that links against it automatically sees the flag without manual `-D` flags. When RocksDB is absent — for example, in minimal builds or unsupported platforms — the entire block compiles away to nothing, and the rest of the `#if XRPL_ROCKSDB_AVAILABLE` guards in implementation files like `RocksDBFactory.cpp` suppress the RocksDB-specific code paths as well.
|
||||
|
||||
## Headers Aggregated
|
||||
|
||||
When the flag is set, the file pulls in the full surface of RocksDB headers needed by the XRPL node-store backend:
|
||||
|
||||
- Core database API (`db.h`, `options.h`, `status.h`, `slice.h`, `iterator.h`)
|
||||
- Write pipeline (`write_batch.h`, `transaction_log.h`)
|
||||
- Tuning and customization points (`cache.h`, `filter_policy.h`, `memtablerep.h`, `merge_operator.h`, `compaction_filter.h`, `slice_transform.h`, `comparator.h`)
|
||||
- Diagnostics and introspection (`statistics.h`, `perf_context.h`, `table_properties.h`)
|
||||
- Block and table format control (`table.h`, `flush_block_policy.h`, `universal_compaction.h`)
|
||||
- Environment abstraction (`env.h`, `convenience.h`, `types.h`)
|
||||
|
||||
The breadth of this list matches what `RocksDBFactory.cpp` actually uses: the factory constructs `rocksdb::Options`, employs the `rocksdb::EnvWrapper` subclass `RocksDBEnv` for custom thread naming, and uses `rocksdb::WriteBatch` for batched writes through the `BatchWriter` layer.
|
||||
|
||||
## Design Rationale
|
||||
|
||||
Keeping all RocksDB includes behind a single header prevents the `#if XRPL_ROCKSDB_AVAILABLE` boilerplate from leaking into every file that indirectly depends on RocksDB types. It also makes it straightforward to swap the exact set of required headers in one place should the RocksDB API evolve. The commented-out line `// #include <rocksdb2/port/port_posix.h>` is a remnant of an earlier investigation into an alternative RocksDB namespace or fork (`rocksdb2`) and signals that portability considerations were weighed at some point.
|
||||
|
||||
This pattern is consistent with how XRPL handles other optional system dependencies: the feature availability check is resolved once at the CMake level and then expressed as a simple macro, keeping the C++ headers themselves free of build-system details.
|
||||
84
include/xrpl/basics/safe_cast.h.ai.json
Normal file
84
include/xrpl/basics/safe_cast.h.ai.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"lineno": 11,
|
||||
"name": "Src"
|
||||
},
|
||||
{
|
||||
"lineno": 11,
|
||||
"name": "Dest"
|
||||
},
|
||||
{
|
||||
"lineno": 17,
|
||||
"name": "s"
|
||||
}
|
||||
],
|
||||
"classes": [],
|
||||
"description": "Provides safe and unsafe casting utilities (safe_cast, unsafe_cast, safe_downcast) with compile-time checks for type safety, especially for integral and enum types, as well as pointer/reference downcasting with runtime assertions in debug mode.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/safe_cast.h",
|
||||
"functions": [
|
||||
{
|
||||
"args": [
|
||||
"Src s"
|
||||
],
|
||||
"lineno": 17,
|
||||
"name": "safe_cast"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Src s"
|
||||
],
|
||||
"lineno": 32,
|
||||
"name": "safe_cast"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Src s"
|
||||
],
|
||||
"lineno": 38,
|
||||
"name": "safe_cast"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Src s"
|
||||
],
|
||||
"lineno": 46,
|
||||
"name": "unsafe_cast"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Src s"
|
||||
],
|
||||
"lineno": 56,
|
||||
"name": "unsafe_cast"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Src s"
|
||||
],
|
||||
"lineno": 62,
|
||||
"name": "unsafe_cast"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Src* s"
|
||||
],
|
||||
"lineno": 69,
|
||||
"name": "safe_downcast"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"Src& s"
|
||||
],
|
||||
"lineno": 81,
|
||||
"name": "safe_downcast"
|
||||
}
|
||||
],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 7,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
include/xrpl/basics/safe_cast.h.ai.md
Normal file
44
include/xrpl/basics/safe_cast.h.ai.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# `include/xrpl/basics/safe_cast.h`
|
||||
|
||||
This header provides a small family of casting utilities that replace raw `static_cast` at the call site with one that encodes intent and enforces correctness at compile time. Its motivating use case is enum-to-integer and integer-to-enum conversions, where `static_cast` is technically required but silently accepts lossy or sign-mismatching casts.
|
||||
|
||||
## `SafeToCast` Concept
|
||||
|
||||
The `SafeToCast<Src, Dest>` concept is the foundational correctness predicate. It evaluates to true only when both types are integral, neither cast direction loses sign range (a signed source cannot be cast to unsigned of equal size), and the destination is at least as wide as the source — with an extra byte of headroom required when signedness differs, since a signed destination must accommodate values that were representable in the unsigned source beyond the signed destination's positive range. This encoded arithmetic captures exactly what an implicit promotion would guarantee, made explicit and inspectable by the compiler.
|
||||
|
||||
## `safe_cast`: Verified Lossless Conversion
|
||||
|
||||
`safe_cast<Dest>(src)` comes in three overloads covering integral→integral, integral→enum, and enum→integral conversions.
|
||||
|
||||
The integral→integral overload enforces the `SafeToCast` rules via two `static_assert` statements and then delegates to `static_cast`. Because both assertions fire at compile time, zero-overhead is guaranteed — the optimizer sees a plain `static_cast` after template instantiation. The function is `constexpr noexcept`, so it composes freely in constant expressions and `noexcept` propagation chains.
|
||||
|
||||
The enum overloads decompose the cast into a two-step path through the enum's underlying integer type: `enum→underlying_type→Dest` or `Src→underlying_type→enum`. This pattern forces the sign and width checks to operate on actual integer types rather than the nominal enum type, which has undefined underlying width without an explicit base, and keeps the enum-related casts from bypassing the size checks.
|
||||
|
||||
## `unsafe_cast`: Explicit Acknowledgement of Lossy Casts
|
||||
|
||||
`unsafe_cast<Dest>(src)` inverts the `SafeToCast` predicate. Its `static_assert(!SafeToCast<Src, Dest>, ...)` will reject any call where the cast has actually become safe — meaning if underlying types are later changed to be compatible, the call site will break at compile time with a message suggesting promotion to `safe_cast`. This turns `unsafe_cast` into self-enforcing documentation: it records both the current necessity and the future obligation to revisit the decision.
|
||||
|
||||
A real example from `STAmount.cpp` illustrates the intent:
|
||||
|
||||
```cpp
|
||||
mValue = unsafe_cast<std::uint64_t>(-amount.drops());
|
||||
```
|
||||
|
||||
Here a signed drop count is negated and stored in an unsigned field; the caller knows the invariant holds, but the types make no such promise, so `unsafe_cast` marks the acknowledgement in code rather than a comment that can drift out of sync.
|
||||
|
||||
## `safe_downcast`: Polymorphic Hierarchy Navigation
|
||||
|
||||
`safe_downcast<Dest>(src)` handles pointer and lvalue-reference downcasts within a polymorphic class hierarchy. Its two-mode design is characteristic of performance-sensitive C++:
|
||||
|
||||
- **Release builds** (`NDEBUG` defined): compiles to a `static_cast` with a `// NOLINT` suppressing the Clang-Tidy warning about unchecked pointer downcasts.
|
||||
- **Debug builds**: uses `dynamic_cast` for the pointer overload and checks the result against `nullptr` via `XRPL_ASSERT`; uses a `dynamic_cast` to a pointer in the reference overload just to validate the downcast, then falls through to `static_cast` for the actual conversion (since `dynamic_cast` to a reference throws on failure, and the assertion messaging is preferred here).
|
||||
|
||||
This avoids the runtime overhead of `dynamic_cast` in production while catching incorrect casts during development and CI. The `XRPL_ASSERT` macro (aliased to `ALWAYS_OR_UNREACHABLE` from `instrumentation.h`) marks the check as an invariant that must hold, and during fuzzing the execution continues past a failure rather than aborting.
|
||||
|
||||
## Integration with `Units.h`
|
||||
|
||||
`Units.h` extends the same `safe_cast`/`unsafe_cast` names into the `xrpl` namespace for strong-typed unit wrappers (`XRPAmount`, `IOUAmount`, etc.). Those overloads accept `IntegralValue` wrapper types and unwrap them to their underlying integer, delegate to the integral overloads here, and rewrap the result. This pattern lets code cast between unit types using the same idiom as raw integer casts, maintaining compile-time correctness guarantees across the abstraction boundary.
|
||||
|
||||
## Design Rationale
|
||||
|
||||
Raw `static_cast` between integers and enums is legal C++ but invisible to reviewers — a narrowing cast looks identical to a widening one. By separating casts into `safe_cast` (always correct) and `unsafe_cast` (annotated exception), this header makes every numeric conversion a policy decision visible in the source. The compile-time inversion in `unsafe_cast` additionally prevents the codebase from accumulating dead safety exceptions as the code evolves.
|
||||
9
include/xrpl/basics/sanitizers.h.ai.json
Normal file
9
include/xrpl/basics/sanitizers.h.ai.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [],
|
||||
"description": "Defines a macro to disable AddressSanitizer (ASan) and Hardware AddressSanitizer (HwASan) instrumentation for specific functions, mainly to avoid false positives in certain control flow scenarios.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/sanitizers.h",
|
||||
"functions": [],
|
||||
"language": "c header",
|
||||
"namespaces": []
|
||||
}
|
||||
15
include/xrpl/basics/sanitizers.h.ai.md
Normal file
15
include/xrpl/basics/sanitizers.h.ai.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# `include/xrpl/basics/sanitizers.h`
|
||||
|
||||
## Purpose
|
||||
|
||||
This header provides a single compiler-attribute macro, `XRPL_NO_SANITIZE_ADDRESS`, that suppresses AddressSanitizer (ASan) and Hardware AddressSanitizer (HwASan) instrumentation on individual functions. It exists because both sanitizers instrument every memory access via shadow memory, and certain legitimate C++ control-flow patterns — notably `throw`, `catch`, and coroutine stack switches — can cause ASan to report false positives.
|
||||
|
||||
## The Macro
|
||||
|
||||
On GCC and Clang, `XRPL_NO_SANITIZE_ADDRESS` expands to `__attribute__((no_sanitize("address", "hwaddress")))`, placed on a function declaration to tell the compiler to skip shadow-memory instrumentation for that function only. On MSVC and other non-GCC/Clang compilers the macro expands to nothing, so annotated code compiles cleanly everywhere without `#ifdef` noise at each call site.
|
||||
|
||||
## Design Rationale
|
||||
|
||||
The suppression is deliberately **surgical rather than global**. Running a sanitizer build and then disabling ASan process-wide would defeat the entire point of the instrumentation. By confining the annotation to the specific functions that trigger false positives, the rest of the codebase remains fully checked.
|
||||
|
||||
The primary consumer is `contract.h`, which applies the macro to `Throw<E>()` and `Rethrow()` — the two functions that perform the actual `throw` statement in XRPL's Programming-by-Contract layer. Both functions are `[[noreturn]]` and transfer control non-linearly, which is exactly the pattern ASan struggles with. Keeping the macro in its own header rather than inside `contract.h` preserves the option to annotate other throw-sites or coroutine switch-points across the codebase without creating a circular dependency on the broader contract machinery.
|
||||
46
include/xrpl/basics/scope.h.ai.json
Normal file
46
include/xrpl/basics/scope.h.ai.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"args": [],
|
||||
"classes": [
|
||||
{
|
||||
"args": [
|
||||
"EFP&& f",
|
||||
"std::enable_if_t<!std::is_same_v<std::remove_cv_t<EFP>, scope_exit> && std::is_constructible_v<EF, EFP>>* = 0"
|
||||
],
|
||||
"lineno": 22,
|
||||
"name": "scope_exit"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"EFP&& f",
|
||||
"std::enable_if_t<!std::is_same_v<std::remove_cv_t<EFP>, scope_fail> && std::is_constructible_v<EF, EFP>>* = 0"
|
||||
],
|
||||
"lineno": 61,
|
||||
"name": "scope_fail"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"EFP&& f",
|
||||
"std::enable_if_t<!std::is_same_v<std::remove_cv_t<EFP>, scope_success> && std::is_constructible_v<EF, EFP>>* = 0"
|
||||
],
|
||||
"lineno": 100,
|
||||
"name": "scope_success"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
"std::unique_lock<Mutex>& lock"
|
||||
],
|
||||
"lineno": 139,
|
||||
"name": "scope_unlock"
|
||||
}
|
||||
],
|
||||
"description": "This file provides RAII (Resource Acquisition Is Initialization) scope guard helpers for C++ such as scope_exit, scope_fail, scope_success, and scope_unlock, which execute user-provided functions on scope exit, failure, or success, and manage mutex unlocking/locking automatically.",
|
||||
"file_path": "workflow/XRPLF-rippled-develop/source/include/xrpl/basics/scope.h",
|
||||
"functions": [],
|
||||
"language": "c header",
|
||||
"namespaces": [
|
||||
{
|
||||
"lineno": 9,
|
||||
"name": "xrpl"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
include/xrpl/basics/scope.h.ai.md
Normal file
33
include/xrpl/basics/scope.h.ai.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# `include/xrpl/basics/scope.h`
|
||||
|
||||
This header provides four RAII scope-guard utilities for the `xrpl` namespace: `scope_exit`, `scope_fail`, `scope_success`, and `scope_unlock`. The first three follow the design specified in the C++ Library Fundamentals TS v3 (N4873, §[scopeguard]), giving XRPL the same ergonomics that the standard committee intended for a future `<scope>` header but without waiting for compiler support. `scope_unlock` is an independent addition that solves a recurring pattern in the ledger's concurrency model.
|
||||
|
||||
## The Three Scope Guard Templates
|
||||
|
||||
All three share a common structure: they wrap a callable `EF`, an `execute_on_destruction_` flag, and (for `scope_fail` and `scope_success`) a snapshot of `std::uncaught_exceptions()` taken at construction time. The difference lies entirely in the condition evaluated in each destructor.
|
||||
|
||||
**`scope_exit`** is the unconditional guard. Its destructor calls the stored functor whenever `execute_on_destruction_` is `true`, regardless of whether the scope is leaving normally or due to an exception. This is the go-to tool for "always clean up this resource" situations — a direct replacement for wrapping teardown code in a dedicated RAII type. The `CheckCash` transactor uses it precisely this way: after temporarily tweaking a trust-line limit during a payment, a `scope_exit` guarantees the original value is restored before the function returns, even if an error path is taken mid-function.
|
||||
|
||||
**`scope_fail`** fires only when the scope is unwinding due to an exception. It compares `std::uncaught_exceptions()` at destruction against the value snapshotted at construction; if the count has grown, an exception is in flight. This is the "rollback on failure" idiom: register compensating actions at the top of an operation, and they will run automatically if anything throws before completion.
|
||||
|
||||
**`scope_success`** is the mirror of `scope_fail`. It fires only when `std::uncaught_exceptions()` has *not* grown since construction — the scope exited cleanly. Because its exit function only runs on the happy path, the destructor is conditionally `noexcept(noexcept(exit_function_()))`, propagating the callable's exception specification correctly. Unlike `scope_exit` and `scope_fail`, the constructor of `scope_success` does not force `noexcept` construction through `static_assert`, since the implementation already handles non-noexcept construction via its `noexcept(...)` specifier on the constructor itself.
|
||||
|
||||
### The Deviation from the TS Specification
|
||||
|
||||
The spec's constructors for `scope_exit` and `scope_fail` contain a `try/catch` block to handle the case where the functor's construction (not invocation) throws. In practice, almost all callers pass a lambda literal, making those constructors trivially noexcept. Several compilers flagged the try/catch as superfluous in these cases. The implementation resolves this by marking the constructors `noexcept` unconditionally and substituting a `static_assert` on `std::is_nothrow_constructible_v<EF, ...>`. The effect is identical — throwing constructors are rejected — but via a compile-time diagnostic rather than a runtime branch.
|
||||
|
||||
### Move Semantics and `release()`
|
||||
|
||||
All three scope guards are move-constructible but not move-assignable. The move constructor forwards the functor and copies the `execute_on_destruction_` flag, then calls `rhs.release()` to disarm the source. This makes ownership transfer unambiguous: exactly one live instance holds responsibility for running the functor. Calling `release()` directly sets `execute_on_destruction_ = false`, cancelling the deferred action — useful when a resource has been successfully handed off and cleanup is no longer needed.
|
||||
|
||||
CTAD guides (`scope_exit(EF) -> scope_exit<EF>`, etc.) allow clean brace-initialization without spelling out the template parameter.
|
||||
|
||||
## `scope_unlock`
|
||||
|
||||
This template solves a specific problem in XRPL's mutex-heavy ledger management code: a function holds a `std::unique_lock` for most of its work but must temporarily release it for a blocking call or an outward-facing callback, then re-acquire it before continuing. Without a RAII wrapper, every early-return path must remember to re-lock before leaving.
|
||||
|
||||
`scope_unlock` inverts the conventional lock-guard contract. Construction immediately calls `plock->unlock()` (after asserting via `XRPL_ASSERT` that the lock is owned), and the destructor calls `plock->lock()`. The mutex is held for exactly the surrounding scope minus the inner block where `scope_unlock` lives. `LedgerMaster` uses this repeatedly — temporarily releasing its recursive mutex while publishing ledgers or fetching history — and `InboundLedgers` uses it to release a lock before calling `acquire()`.
|
||||
|
||||
Unlike the three scope guards above, `scope_unlock` is intentionally immovable: both the copy constructor and copy-assignment operator are deleted, and there is no move constructor. The semantics of transferring "who will re-lock this mutex" are too error-prone to support safely, so the type is tied to the scope in which it is created.
|
||||
|
||||
The `XRPL_ASSERT` at construction time (`plock->owns_lock()`) acts as a defensive invariant check: calling `scope_unlock` on an already-unlocked `unique_lock` would double-unlock the mutex, a precondition violation that the assertion catches in debug builds.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user