Compare commits

..

9 Commits

Author SHA1 Message Date
Gregory Tsipenyuk
845b7ef7b5 Fix clang-format/tidy 2026-05-14 13:20:12 -04:00
Gregory Tsipenyuk
54f8450baf Update comments 2026-05-14 10:42:24 -04:00
Gregory Tsipenyuk
61f51e5445 Address reviewer's feedback. 2026-05-14 10:26:14 -04:00
Gregory Tsipenyuk
1b8776ed36 Merge branch 'develop' into gregtatcam/mpt/fix-stissue-serialization 2026-05-13 19:28:34 -04:00
Gregory Tsipenyuk
17ee7e784c Add Submit tests to verify transactions with V1/V2 STIssue serialization. 2026-05-13 10:00:54 -04:00
Gregory Tsipenyuk
a87cff1c1b Fix clang-format 2026-05-11 10:25:01 -04:00
Gregory Tsipenyuk
c33526f88d Address reviewer's feedback. 2026-05-11 10:23:05 -04:00
Gregory Tsipenyuk
e59cb667e6 Update src/libxrpl/protocol/STIssue.cpp
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
2026-05-11 09:54:11 -04:00
Gregory Tsipenyuk
4b2d7871fb fix: Fix correct MPT sequence byte order in STIssue serialization 2026-05-07 18:21:01 -04:00
3046 changed files with 7740 additions and 340130 deletions

View File

@@ -171,7 +171,7 @@ CheckOptions:
readability-identifier-naming.EnumCase: CamelCase
readability-identifier-naming.EnumConstantCase: CamelCase
readability-identifier-naming.ScopedEnumConstantCase: CamelCase
readability-identifier-naming.GlobalConstantCase: CamelCase
readability-identifier-naming.GlobalConstantCase: UPPER_CASE
readability-identifier-naming.GlobalConstantPrefix: "k"
readability-identifier-naming.GlobalVariableCase: CamelCase
readability-identifier-naming.GlobalVariablePrefix: "g"
@@ -179,12 +179,14 @@ CheckOptions:
readability-identifier-naming.ConstexprMethodCase: camelBack
readability-identifier-naming.ClassMethodCase: camelBack
readability-identifier-naming.ClassMemberCase: camelBack
readability-identifier-naming.ClassConstantCase: CamelCase
readability-identifier-naming.ClassConstantCase: UPPER_CASE
readability-identifier-naming.ClassConstantPrefix: "k"
readability-identifier-naming.StaticConstantCase: CamelCase
readability-identifier-naming.StaticConstantCase: UPPER_CASE
readability-identifier-naming.StaticConstantPrefix: "k"
readability-identifier-naming.StaticVariableCase: camelBack
readability-identifier-naming.ConstexprVariableCase: camelBack
readability-identifier-naming.StaticVariableCase: UPPER_CASE
readability-identifier-naming.StaticVariablePrefix: "k"
readability-identifier-naming.ConstexprVariableCase: UPPER_CASE
readability-identifier-naming.ConstexprVariablePrefix: "k"
readability-identifier-naming.LocalConstantCase: camelBack
readability-identifier-naming.LocalVariableCase: camelBack
readability-identifier-naming.TemplateParameterCase: CamelCase

View File

@@ -62,7 +62,7 @@ ${SED_COMMAND} -i 's@ripple/@xrpld/@g' src/test/core/Config_test.cpp
${SED_COMMAND} -i 's/Rippled/File/g' src/test/core/Config_test.cpp
# Restore the old config file name in the code that maintains support for now.
${SED_COMMAND} -i 's/kConfigLegacyName = "xrpld.cfg"/kConfigLegacyName = "rippled.cfg"/g' src/xrpld/core/detail/Config.cpp
${SED_COMMAND} -i 's/kCONFIG_LEGACY_NAME = "xrpld.cfg"/kCONFIG_LEGACY_NAME = "rippled.cfg"/g' src/xrpld/core/detail/Config.cpp
# Restore an URL.
${SED_COMMAND} -i 's/connect-your-xrpld-to-the-xrp-test-net.html/connect-your-rippled-to-the-xrp-test-net.html/g' cfg/xrpld-example.cfg

View File

@@ -90,7 +90,7 @@ ${SED_COMMAND} -i 's/www.ripple.com/www.xrpl.org/g' src/test/protocol/Seed_test.
# Restore specific changes.
${SED_COMMAND} -i 's@b5efcc/src/xrpld@b5efcc/src/ripple@' include/xrpl/protocol/README.md
${SED_COMMAND} -i 's/dbPrefix_ = "xrpldb"/dbPrefix_ = "rippledb"/' src/xrpld/app/misc/SHAMapStoreImp.h # cspell: disable-line
${SED_COMMAND} -i 's/kConfigLegacyName = "xrpld.cfg"/kConfigLegacyName = "rippled.cfg"/' src/xrpld/core/detail/Config.cpp
${SED_COMMAND} -i 's/kCONFIG_LEGACY_NAME = "xrpld.cfg"/kCONFIG_LEGACY_NAME = "rippled.cfg"/' src/xrpld/core/detail/Config.cpp
popd
echo "Renaming complete."

View File

@@ -32,32 +32,7 @@ We will further set additional CMake arguments as follows:
"""
def build_config_name(os_entry: dict[str, str], platform: str, build_type: str) -> str:
parts = [os_entry["distro_name"]]
for key in ("distro_version", "compiler_name", "compiler_version"):
if value := os_entry[key]:
parts.append(value)
parts.append("arm64" if "arm64" in platform else "amd64")
parts.append(build_type.lower())
return "-".join(parts)
def generate_packaging_matrix(config: Config) -> list[dict]:
"""Emit one entry per os entry with `package: true`. Architecture is
hardcoded to linux/amd64 here (and the runner is hardcoded at the
workflow level) until arm64 packaging is ready.
"""
return [
{
"artifact_name": f"xrpld-{build_config_name(os, 'linux/amd64', 'Release')}",
"os": os,
}
for os in config.os
if os.get("package", False)
]
def generate_strategy_matrix(all: bool, config: Config) -> list[dict]:
def generate_strategy_matrix(all: bool, config: Config) -> list:
configurations = []
for architecture, os, build_type, cmake_args in itertools.product(
config.architecture, config.os, config.build_type, config.cmake_args
@@ -126,15 +101,14 @@ def generate_strategy_matrix(all: bool, config: Config) -> list[dict]:
continue
# RHEL:
# - 9 using GCC 12: Debug and Release on linux/amd64
# (Release is required for RPM packaging).
# - 9 using GCC 12: Debug on linux/amd64.
# - 10 using Clang: Release on linux/amd64.
if os["distro_name"] == "rhel":
skip = True
if os["distro_version"] == "9":
if (
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-12"
and build_type in ["Debug", "Release"]
and build_type == "Debug"
and architecture["platform"] == "linux/amd64"
):
skip = False
@@ -149,8 +123,7 @@ def generate_strategy_matrix(all: bool, config: Config) -> list[dict]:
continue
# Ubuntu:
# - Jammy using GCC 12: Debug on linux/arm64, Release on
# linux/amd64 (Release is required for DEB packaging).
# - Jammy using GCC 12: Debug on linux/arm64.
# - Noble using GCC 14: Release on linux/amd64.
# - Noble using Clang 18: Debug on linux/amd64.
# - Noble using Clang 19: Release on linux/arm64.
@@ -163,12 +136,6 @@ def generate_strategy_matrix(all: bool, config: Config) -> list[dict]:
and architecture["platform"] == "linux/arm64"
):
skip = False
if (
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-12"
and build_type == "Release"
and architecture["platform"] == "linux/amd64"
):
skip = False
elif os["distro_version"] == "noble":
if (
f"{os['compiler_name']}-{os['compiler_version']}" == "gcc-14"
@@ -251,7 +218,17 @@ def generate_strategy_matrix(all: bool, config: Config) -> list[dict]:
# Generate a unique name for the configuration, e.g. macos-arm64-debug
# or debian-bookworm-gcc-12-amd64-release.
config_name = build_config_name(os, architecture["platform"], build_type)
config_name = os["distro_name"]
if (n := os["distro_version"]) != "":
config_name += f"-{n}"
if (n := os["compiler_name"]) != "":
config_name += f"-{n}"
if (n := os["compiler_version"]) != "":
config_name += f"-{n}"
config_name += (
f"-{architecture['platform'][architecture['platform'].find('/')+1:]}"
)
config_name += f"-{build_type.lower()}"
if "-Dcoverage=ON" in cmake_args:
config_name += "-coverage"
if "-Dunity=ON" in cmake_args:
@@ -355,19 +332,10 @@ if __name__ == "__main__":
required=False,
type=Path,
)
parser.add_argument(
"-p",
"--packaging",
help="Emit the packaging matrix (derived from the 'package' field on os entries) instead of the build/test matrix.",
action="store_true",
)
args = parser.parse_args()
matrix = []
if args.packaging:
config_path = args.config if args.config else THIS_DIR / "linux.json"
matrix += generate_packaging_matrix(read_config(config_path))
elif args.config is None or args.config == "":
if args.config is None or args.config == "":
matrix += generate_strategy_matrix(
args.all, read_config(THIS_DIR / "linux.json")
)

View File

@@ -127,8 +127,7 @@
"distro_version": "9",
"compiler_name": "gcc",
"compiler_version": "12",
"image_sha": "4c086b9",
"package": true
"image_sha": "4c086b9"
},
{
"distro_name": "rhel",
@@ -170,8 +169,7 @@
"distro_version": "jammy",
"compiler_name": "gcc",
"compiler_version": "12",
"image_sha": "4c086b9",
"package": true
"image_sha": "4c086b9"
},
{
"distro_name": "ubuntu",

View File

@@ -11,4 +11,4 @@ on:
jobs:
check_title:
if: ${{ github.event.pull_request.draft != true }}
uses: XRPLF/actions/.github/workflows/check-pr-title.yml@291206777251b4d493641b5afbdf7c23009d2988
uses: XRPLF/actions/.github/workflows/check-pr-title.yml@a5d8dd35be543365e90a11358447130c8763871d

View File

@@ -64,13 +64,11 @@ jobs:
.github/workflows/reusable-build-test-config.yml
.github/workflows/reusable-build-test.yml
.github/workflows/reusable-clang-tidy.yml
.github/workflows/reusable-package.yml
.github/workflows/reusable-strategy-matrix.yml
.github/workflows/reusable-test.yml
.github/workflows/reusable-upload-recipe.yml
.clang-tidy
.codecov.yml
cfg/**
cmake/**
conan/**
external/**
@@ -80,10 +78,6 @@ jobs:
CMakeLists.txt
conanfile.py
conan.lock
LICENSE.md
package/**
README.md
- name: Check whether to run
# This step determines whether the rest of the workflow should
# run. The rest of the workflow will run if this job runs AND at
@@ -140,11 +134,6 @@ jobs:
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
package:
needs: [should-run, build-test]
if: ${{ needs.should-run.outputs.go == 'true' }}
uses: ./.github/workflows/reusable-package.yml
upload-recipe:
needs:
- should-run
@@ -179,7 +168,6 @@ jobs:
- check-rename
- clang-tidy
- build-test
- package
- upload-recipe
- notify-clio
runs-on: ubuntu-latest

View File

@@ -1,5 +1,5 @@
# This workflow uploads the libxrpl recipe to the Conan remote and builds
# release packages when a versioned tag is pushed.
# This workflow uploads the libxrpl recipe to the Conan remote when a versioned
# tag is pushed.
name: Tag
on:
@@ -22,22 +22,3 @@ jobs:
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
build-test:
if: ${{ github.repository == 'XRPLF/rippled' }}
uses: ./.github/workflows/reusable-build-test.yml
strategy:
fail-fast: true
matrix:
os: [linux]
with:
ccache_enabled: false
os: ${{ matrix.os }}
strategy_matrix: minimal
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
package:
if: ${{ github.repository == 'XRPLF/rippled' }}
needs: build-test
uses: ./.github/workflows/reusable-package.yml

View File

@@ -21,13 +21,11 @@ on:
- ".github/workflows/reusable-build-test-config.yml"
- ".github/workflows/reusable-build-test.yml"
- ".github/workflows/reusable-clang-tidy.yml"
- ".github/workflows/reusable-package.yml"
- ".github/workflows/reusable-strategy-matrix.yml"
- ".github/workflows/reusable-test.yml"
- ".github/workflows/reusable-upload-recipe.yml"
- ".clang-tidy"
- ".codecov.yml"
- "cfg/**"
- "cmake/**"
- "conan/**"
- "external/**"
@@ -37,9 +35,6 @@ on:
- "CMakeLists.txt"
- "conanfile.py"
- "conan.lock"
- "LICENSE.md"
- "package/**"
- "README.md"
# Run at 06:32 UTC on every day of the week from Monday through Friday. This
# will force all dependencies to be rebuilt, which is useful to verify that
@@ -100,7 +95,3 @@ jobs:
secrets:
remote_username: ${{ secrets.CONAN_REMOTE_USERNAME }}
remote_password: ${{ secrets.CONAN_REMOTE_PASSWORD }}
package:
needs: build-test
uses: ./.github/workflows/reusable-package.yml

View File

@@ -176,7 +176,7 @@ jobs:
- name: Create issue
if: ${{ steps.run_clang_tidy.outcome != 'success' && inputs.create_issue_on_failure }}
uses: XRPLF/actions/create-issue@36d450d12d301e8410c1b7936e5de70c291cbe36
uses: XRPLF/actions/create-issue@fbcc16eb7f20dc3199eaf1aed0d3523a5ba9008c
with:
title: "Clang-tidy check failed"
body_file: ${{ env.ISSUE_FILE }}

View File

@@ -1,99 +0,0 @@
# Build Linux packages (DEB and RPM) from pre-built binary artifacts.
# Discovers which configurations to package from linux.json (os entries
# with "package": true) and fans out one job per entry. Today only
# linux/amd64 is emitted; the architecture is hardcoded both here
# (runner) and in generate.py.
name: Package
on:
workflow_call:
inputs:
pkg_release:
description: "Package release number. Increment when repackaging the same executable."
required: false
type: string
default: "1"
defaults:
run:
shell: bash
env:
BUILD_DIR: build
jobs:
generate-matrix:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.generate.outputs.matrix }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.13
- name: Generate packaging matrix
id: generate
working-directory: .github/scripts/strategy-matrix
run: |
./generate.py --packaging --config=linux.json >> "${GITHUB_OUTPUT}"
generate-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
sparse-checkout: |
.github/actions/generate-version
src/libxrpl/protocol/BuildInfo.cpp
- name: Generate version
id: version
uses: ./.github/actions/generate-version
package:
needs: [generate-matrix, generate-version]
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
name: "${{ matrix.artifact_name }}"
permissions:
contents: read
runs-on: ["self-hosted", "Linux", "X64", "heavy"]
container: ${{ format('ghcr.io/xrplf/ci/{0}-{1}:{2}-{3}-sha-{4}', matrix.os.distro_name, matrix.os.distro_version, matrix.os.compiler_name, matrix.os.compiler_version, matrix.os.image_sha) }}
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download pre-built binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ matrix.artifact_name }}
path: ${{ env.BUILD_DIR }}
- name: Make binary executable
run: chmod +x "${BUILD_DIR}/xrpld"
- name: Build package
env:
PKG_VERSION: ${{ needs.generate-version.outputs.version }}
PKG_RELEASE: ${{ inputs.pkg_release }}
run: ./package/build_pkg.sh
- name: Upload package artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: ${{ github.event.repository.visibility == 'public' }}
with:
name: ${{ matrix.artifact_name }}-pkg-${{ needs.generate-version.outputs.version }}
path: |
${{ env.BUILD_DIR }}/debbuild/*.deb
${{ env.BUILD_DIR }}/debbuild/*.ddeb
${{ env.BUILD_DIR }}/rpmbuild/RPMS/**/*.rpm
if-no-files-found: error

View File

@@ -134,7 +134,6 @@ endif()
include(XrplCore)
include(XrplProtocolAutogen)
include(XrplInstall)
include(XrplPackaging)
include(XrplValidatorKeys)
if(tests)

View File

@@ -28,7 +28,7 @@
# https://vl.ripple.com
# https://unl.xrplf.org
# http://127.0.0.1:8000
# file:///etc/xrpld/vl.txt
# file:///etc/opt/xrpld/vl.txt
#
# [validator_list_keys]
#

View File

@@ -527,17 +527,6 @@
#
# The current default (which is subject to change) is 300 seconds.
#
# verify_endpoints = <0 | 1>
#
# If set to 0, the server will skip validation of endpoint
# addresses received in TMEndpoints peer protocol messages,
# allowing addresses that are not publicly routable or have a
# port of 0. The default is 1 (verification enabled).
#
# WARNING: Disabling this option is a security risk and should
# only be used for local testing and debugging. Do not disable
# on mainnet.
#
#
# [transaction_queue] EXPERIMENTAL
#

View File

@@ -1,44 +0,0 @@
#[===================================================================[
Linux packaging support: 'package' target.
The packaging script (package/build_pkg.sh) installs to FHS-standard
paths (/usr/bin, /etc/xrpld, etc.) regardless of CMAKE_INSTALL_PREFIX,
so no prefix guard is needed here.
#]===================================================================]
if(NOT is_linux)
message(STATUS "Packaging not supported on non-Linux hosts")
return()
endif()
if(NOT DEFINED pkg_release)
set(pkg_release 1)
endif()
find_program(RPMBUILD_EXECUTABLE rpmbuild)
find_program(DPKG_BUILDPACKAGE_EXECUTABLE dpkg-buildpackage)
if(NOT (RPMBUILD_EXECUTABLE OR DPKG_BUILDPACKAGE_EXECUTABLE))
message(
STATUS
"Neither rpmbuild nor dpkg-buildpackage found; 'package' target not available"
)
return()
endif()
set(package_env
SRC_DIR=${CMAKE_SOURCE_DIR}
BUILD_DIR=${CMAKE_BINARY_DIR}
PKG_VERSION=${xrpld_version}
PKG_RELEASE=${pkg_release}
)
add_custom_target(
package
COMMAND
${CMAKE_COMMAND} -E env ${package_env}
${CMAKE_SOURCE_DIR}/package/build_pkg.sh
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
DEPENDS xrpld
COMMENT "Building Linux package (deb/rpm inferred from host tooling)"
VERBATIM
)

View File

@@ -99,15 +99,12 @@ words:
- desync
- desynced
- determ
- disablerepo
- distro
- doxyfile
- dxrpl
- enabled
- enablerepo
- endmacro
- exceptioned
- EXPECT_STREQ
- Falco
- fcontext
- finalizers
@@ -165,7 +162,6 @@ words:
- Merkle
- Metafuncton
- misprediction
- missingok
- mptbalance
- MPTDEX
- mptflags
@@ -197,9 +193,7 @@ words:
- NOLINT
- NOLINTNEXTLINE
- nonxrp
- noreplace
- noripple
- notifempty
- nudb
- nullptr
- nunl
@@ -219,7 +213,6 @@ words:
- preauthorize
- preauthorizes
- preclaim
- preun
- protobuf
- protos
- ptrs
@@ -254,14 +247,12 @@ words:
- sfields
- shamap
- shamapitem
- shlibs
- sidechain
- SIGGOOD
- sle
- sles
- soci
- socidb
- SRPMS
- sslws
- statsd
- STATSDCOLLECTOR
@@ -289,8 +280,8 @@ words:
- txn
- txns
- txs
- ubsan
- UBSAN
- ubsan
- umant
- unacquired
- unambiguity
@@ -327,6 +318,7 @@ words:
- xbridge
- xchain
- ximinez
- EXPECT_STREQ
- XMACRO
- xrpkuwait
- xrpl
@@ -334,4 +326,3 @@ words:
- xrplf
- xxhash
- xxhasher
- CGNAT

View File

@@ -1,32 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,31 +0,0 @@
# `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.

View File

@@ -1,324 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,44 +0,0 @@
# `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.

View File

@@ -1,14 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,17 +0,0 @@
# `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.

View File

@@ -1,42 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,35 +0,0 @@
# `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.

View File

@@ -1,38 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,26 +0,0 @@
# `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.

View File

@@ -1,93 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,53 +0,0 @@
# `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.

View File

@@ -1,88 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,55 +0,0 @@
# `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.

View File

@@ -1,115 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,39 +0,0 @@
# `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.

View File

@@ -1,109 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,41 +0,0 @@
# `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.

View File

@@ -1,58 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,33 +0,0 @@
# `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.

View File

@@ -406,8 +406,8 @@ private:
// pointer. The low bit must be masked to zero when converting back to a
// pointer. If the low bit is '1', this is a weak pointer.
std::uintptr_t tp_{0};
static constexpr std::uintptr_t kTagMask = 1;
static constexpr std::uintptr_t kPtrMask = ~kTagMask;
static constexpr std::uintptr_t kTAG_MASK = 1;
static constexpr std::uintptr_t kPTR_MASK = ~kTAG_MASK;
private:
/** Return the raw pointer held by this object.

View File

@@ -1,115 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,52 +0,0 @@
# `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 015**: strong count (up to 65535 owners)
- **Bits 1629**: 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.

View File

@@ -567,14 +567,14 @@ template <class T>
bool
SharedWeakUnion<T>::isStrong() const
{
return (tp_ & kTagMask) == 0u;
return (tp_ & kTAG_MASK) == 0u;
}
template <class T>
bool
SharedWeakUnion<T>::isWeak() const
{
return (tp_ & kTagMask) != 0u;
return (tp_ & kTAG_MASK) != 0u;
}
template <class T>
@@ -641,7 +641,7 @@ template <class T>
T*
SharedWeakUnion<T>::unsafeGetRawPtr() const
{
return reinterpret_cast<T*>(tp_ & kPtrMask);
return reinterpret_cast<T*>(tp_ & kPTR_MASK);
}
template <class T>
@@ -650,7 +650,7 @@ SharedWeakUnion<T>::unsafeSetRawPtr(T* p, RefStrength rs)
{
tp_ = reinterpret_cast<std::uintptr_t>(p);
if (tp_ && rs == RefStrength::Weak)
tp_ |= kTagMask;
tp_ |= kTAG_MASK;
}
template <class T>

View File

@@ -1,698 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,64 +0,0 @@
# `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).

View File

@@ -98,11 +98,11 @@ private:
// enough for strong pointers and 14 bit counts are enough for weak
// pointers. Use type aliases to make it easy to switch types.
using CountType = std::uint16_t;
static constexpr size_t kStrongCountNumBits = sizeof(CountType) * 8;
static constexpr size_t kWeakCountNumBits = kStrongCountNumBits - 2;
static constexpr size_t kSTRONG_COUNT_NUM_BITS = sizeof(CountType) * 8;
static constexpr size_t kWEAK_COUNT_NUM_BITS = kSTRONG_COUNT_NUM_BITS - 2;
using FieldType = std::uint32_t;
static constexpr size_t kFieldTypeBits = sizeof(FieldType) * 8;
static constexpr FieldType kOne = 1;
static constexpr size_t kFIELD_TYPE_BITS = sizeof(FieldType) * 8;
static constexpr FieldType kONE = 1;
/** `refCounts` consists of four fields that are treated atomically:
@@ -137,21 +137,21 @@ private:
*/
mutable std::atomic<FieldType> refCounts_{kStrongDelta};
mutable std::atomic<FieldType> refCounts_{kSTRONG_DELTA};
/** Amount to change the strong count when adding or releasing a reference
Note: The strong count is stored in the low `StrongCountNumBits` bits
of refCounts
*/
static constexpr FieldType kStrongDelta = 1;
static constexpr FieldType kSTRONG_DELTA = 1;
/** Amount to change the weak count when adding or releasing a reference
Note: The weak count is stored in the high `WeakCountNumBits` bits of
refCounts
*/
static constexpr FieldType kWeakDelta = (kOne << kStrongCountNumBits);
static constexpr FieldType kWEAK_DELTA = (kONE << kSTRONG_COUNT_NUM_BITS);
/** Flag that is set when the partialDestroy function has started running
(or is about to start running).
@@ -159,33 +159,34 @@ private:
See description of the `refCounts` field for a fuller description of
this field.
*/
static constexpr FieldType kPartialDestroyStartedMask = (kOne << (kFieldTypeBits - 1));
static constexpr FieldType kPARTIAL_DESTROY_STARTED_MASK = (kONE << (kFIELD_TYPE_BITS - 1));
/** Flag that is set when the partialDestroy function has finished running
See description of the `refCounts` field for a fuller description of
this field.
*/
static constexpr FieldType kPartialDestroyFinishedMask = (kOne << (kFieldTypeBits - 2));
static constexpr FieldType kPARTIAL_DESTROY_FINISHED_MASK = (kONE << (kFIELD_TYPE_BITS - 2));
/** Mask that will zero out all the `count` bits and leave the tag bits
unchanged.
*/
static constexpr FieldType kTagMask = kPartialDestroyStartedMask | kPartialDestroyFinishedMask;
static constexpr FieldType kTAG_MASK =
kPARTIAL_DESTROY_STARTED_MASK | kPARTIAL_DESTROY_FINISHED_MASK;
/** Mask that will zero out the `tag` bits and leave the count bits
unchanged.
*/
static constexpr FieldType kValueMask = ~kTagMask;
static constexpr FieldType kVALUE_MASK = ~kTAG_MASK;
/** Mask that will zero out everything except the strong count.
*/
static constexpr FieldType kStrongMask = ((kOne << kStrongCountNumBits) - 1) & kValueMask;
static constexpr FieldType kSTRONG_MASK = ((kONE << kSTRONG_COUNT_NUM_BITS) - 1) & kVALUE_MASK;
/** Mask that will zero out everything except the weak count.
*/
static constexpr FieldType kWeakMask =
(((kOne << kWeakCountNumBits) - 1) << kStrongCountNumBits) & kValueMask;
static constexpr FieldType kWEAK_MASK =
(((kONE << kWEAK_COUNT_NUM_BITS) - 1) << kSTRONG_COUNT_NUM_BITS) & kVALUE_MASK;
/** Unpack the count and tag fields from the packed atomic integer form. */
struct RefCountPair
@@ -210,29 +211,29 @@ private:
[[nodiscard]] FieldType
combinedValue() const noexcept;
static constexpr CountType kMaxStrongValue =
static_cast<CountType>((kOne << kStrongCountNumBits) - 1);
static constexpr CountType kMaxWeakValue =
static_cast<CountType>((kOne << kWeakCountNumBits) - 1);
static constexpr CountType kMAX_STRONG_VALUE =
static_cast<CountType>((kONE << kSTRONG_COUNT_NUM_BITS) - 1);
static constexpr CountType kMAX_WEAK_VALUE =
static_cast<CountType>((kONE << kWEAK_COUNT_NUM_BITS) - 1);
/** Put an extra margin to detect when running up against limits.
This is only used in debug code, and is useful if we reduce the
number of bits in the strong and weak counts (to 16 and 14 bits).
*/
static constexpr CountType kCheckStrongMaxValue = kMaxStrongValue - 32;
static constexpr CountType kCheckWeakMaxValue = kMaxWeakValue - 32;
static constexpr CountType kCHECK_STRONG_MAX_VALUE = kMAX_STRONG_VALUE - 32;
static constexpr CountType kCHECK_WEAK_MAX_VALUE = kMAX_WEAK_VALUE - 32;
};
};
inline void
IntrusiveRefCounts::addStrongRef() const noexcept
{
refCounts_.fetch_add(kStrongDelta, std::memory_order_acq_rel);
refCounts_.fetch_add(kSTRONG_DELTA, std::memory_order_acq_rel);
}
inline void
IntrusiveRefCounts::addWeakRef() const noexcept
{
refCounts_.fetch_add(kWeakDelta, std::memory_order_acq_rel);
refCounts_.fetch_add(kWEAK_DELTA, std::memory_order_acq_rel);
}
inline ReleaseStrongRefAction
@@ -251,10 +252,10 @@ IntrusiveRefCounts::releaseStrongRef() const
{
RefCountPair const prevVal{prevIntVal};
XRPL_ASSERT(
(prevVal.strong >= kStrongDelta),
(prevVal.strong >= kSTRONG_DELTA),
"xrpl::IntrusiveRefCounts::releaseStrongRef : previous ref "
"higher than new");
auto nextIntVal = prevIntVal - kStrongDelta;
auto nextIntVal = prevIntVal - kSTRONG_DELTA;
ReleaseStrongRefAction action = NoOp;
if (prevVal.strong == 1)
{
@@ -264,7 +265,7 @@ IntrusiveRefCounts::releaseStrongRef() const
}
else
{
nextIntVal |= kPartialDestroyStartedMask;
nextIntVal |= kPARTIAL_DESTROY_STARTED_MASK;
action = PartialDestroy;
}
}
@@ -275,7 +276,7 @@ IntrusiveRefCounts::releaseStrongRef() const
// count to zero can start a partial destroy, and that can't happen
// twice.
XRPL_ASSERT(
(action == NoOp) || !(prevIntVal & kPartialDestroyStartedMask),
(action == NoOp) || !(prevIntVal & kPARTIAL_DESTROY_STARTED_MASK),
"xrpl::IntrusiveRefCounts::releaseStrongRef : not in partial "
"destroy");
return action;
@@ -288,8 +289,8 @@ IntrusiveRefCounts::addWeakReleaseStrongRef() const
{
using enum ReleaseStrongRefAction;
static_assert(kWeakDelta > kStrongDelta);
static constexpr auto kDelta = kWeakDelta - kStrongDelta;
static_assert(kWEAK_DELTA > kSTRONG_DELTA);
auto constexpr kDELTA = kWEAK_DELTA - kSTRONG_DELTA;
auto prevIntVal = refCounts_.load(std::memory_order_acquire);
// This loop will almost always run once. The loop is needed to atomically
// change the counts and flags (the count could be atomically changed, but
@@ -311,7 +312,7 @@ IntrusiveRefCounts::addWeakReleaseStrongRef() const
"xrpl::IntrusiveRefCounts::addWeakReleaseStrongRef : not in "
"partial destroy");
auto nextIntVal = prevIntVal + kDelta;
auto nextIntVal = prevIntVal + kDELTA;
ReleaseStrongRefAction action = NoOp;
if (prevVal.strong == 1)
{
@@ -321,14 +322,14 @@ IntrusiveRefCounts::addWeakReleaseStrongRef() const
}
else
{
nextIntVal |= kPartialDestroyStartedMask;
nextIntVal |= kPARTIAL_DESTROY_STARTED_MASK;
action = PartialDestroy;
}
}
if (refCounts_.compare_exchange_weak(prevIntVal, nextIntVal, std::memory_order_acq_rel))
{
XRPL_ASSERT(
(!(prevIntVal & kPartialDestroyStartedMask)),
(!(prevIntVal & kPARTIAL_DESTROY_STARTED_MASK)),
"xrpl::IntrusiveRefCounts::addWeakReleaseStrongRef : not "
"started partial destroy");
return action;
@@ -339,7 +340,7 @@ IntrusiveRefCounts::addWeakReleaseStrongRef() const
inline ReleaseWeakRefAction
IntrusiveRefCounts::releaseWeakRef() const
{
auto prevIntVal = refCounts_.fetch_sub(kWeakDelta, std::memory_order_acq_rel);
auto prevIntVal = refCounts_.fetch_sub(kWEAK_DELTA, std::memory_order_acq_rel);
RefCountPair prev = prevIntVal;
if (prev.weak == 1 && prev.strong == 0)
{
@@ -356,7 +357,7 @@ IntrusiveRefCounts::releaseWeakRef() const
{
// partial destroy MUST finish before running a full destroy (when
// using weak pointers)
refCounts_.wait(prevIntVal - kWeakDelta, std::memory_order_acquire);
refCounts_.wait(prevIntVal - kWEAK_DELTA, std::memory_order_acquire);
}
return ReleaseWeakRefAction::Destroy;
}
@@ -375,7 +376,7 @@ IntrusiveRefCounts::checkoutStrongRefFromWeak() const noexcept
if (prev.strong == 0u)
return false;
desiredValue = curValue + kStrongDelta;
desiredValue = curValue + kSTRONG_DELTA;
}
return true;
}
@@ -399,22 +400,23 @@ inline IntrusiveRefCounts::~IntrusiveRefCounts() noexcept
#ifndef NDEBUG
auto v = refCounts_.load(std::memory_order_acquire);
XRPL_ASSERT(
(!(v & kValueMask)), "xrpl::IntrusiveRefCounts::~IntrusiveRefCounts : count must be zero");
auto t = v & kTagMask;
XRPL_ASSERT((!t || t == kTagMask), "xrpl::IntrusiveRefCounts::~IntrusiveRefCounts : valid tag");
(!(v & kVALUE_MASK)), "xrpl::IntrusiveRefCounts::~IntrusiveRefCounts : count must be zero");
auto t = v & kTAG_MASK;
XRPL_ASSERT(
(!t || t == kTAG_MASK), "xrpl::IntrusiveRefCounts::~IntrusiveRefCounts : valid tag");
#endif
}
//------------------------------------------------------------------------------
inline IntrusiveRefCounts::RefCountPair::RefCountPair(IntrusiveRefCounts::FieldType v) noexcept
: strong{static_cast<CountType>(v & kStrongMask)}
, weak{static_cast<CountType>((v & kWeakMask) >> kStrongCountNumBits)}
, partialDestroyStartedBit{v & kPartialDestroyStartedMask}
, partialDestroyFinishedBit{v & kPartialDestroyFinishedMask}
: strong{static_cast<CountType>(v & kSTRONG_MASK)}
, weak{static_cast<CountType>((v & kWEAK_MASK) >> kSTRONG_COUNT_NUM_BITS)}
, partialDestroyStartedBit{v & kPARTIAL_DESTROY_STARTED_MASK}
, partialDestroyFinishedBit{v & kPARTIAL_DESTROY_FINISHED_MASK}
{
XRPL_ASSERT(
(strong < kCheckStrongMaxValue && weak < kCheckWeakMaxValue),
(strong < kCHECK_STRONG_MAX_VALUE && weak < kCHECK_WEAK_MAX_VALUE),
"xrpl::IntrusiveRefCounts::RefCountPair(FieldType) : inputs inside "
"range");
}
@@ -425,7 +427,7 @@ inline IntrusiveRefCounts::RefCountPair::RefCountPair(
: strong{s}, weak{w}
{
XRPL_ASSERT(
(strong < kCheckStrongMaxValue && weak < kCheckWeakMaxValue),
(strong < kCHECK_STRONG_MAX_VALUE && weak < kCHECK_WEAK_MAX_VALUE),
"xrpl::IntrusiveRefCounts::RefCountPair(CountType, CountType) : "
"inputs inside range");
}
@@ -434,11 +436,11 @@ inline IntrusiveRefCounts::FieldType
IntrusiveRefCounts::RefCountPair::combinedValue() const noexcept
{
XRPL_ASSERT(
(strong < kCheckStrongMaxValue && weak < kCheckWeakMaxValue),
(strong < kCHECK_STRONG_MAX_VALUE && weak < kCHECK_WEAK_MAX_VALUE),
"xrpl::IntrusiveRefCounts::RefCountPair::combinedValue : inputs "
"inside range");
return (static_cast<IntrusiveRefCounts::FieldType>(weak)
<< IntrusiveRefCounts::kStrongCountNumBits) |
<< IntrusiveRefCounts::kSTRONG_COUNT_NUM_BITS) |
static_cast<IntrusiveRefCounts::FieldType>(strong) | partialDestroyStartedBit |
partialDestroyFinishedBit;
}
@@ -449,7 +451,7 @@ partialDestructorFinished(T** o)
{
T& self = **o;
IntrusiveRefCounts::RefCountPair const p =
self.refCounts_.fetch_or(IntrusiveRefCounts::kPartialDestroyFinishedMask);
self.refCounts_.fetch_or(IntrusiveRefCounts::kPARTIAL_DESTROY_FINISHED_MASK);
XRPL_ASSERT(
(!p.partialDestroyFinishedBit && p.partialDestroyStartedBit && !p.strong),
"xrpl::partialDestructorFinished : not a weak ref");

View File

@@ -1,118 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,56 +0,0 @@
# `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 |
|------|-------|
| 015 | Strong count (16 bits) |
| 1629 | 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.

View File

@@ -1,14 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,13 +0,0 @@
# `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`.

View File

@@ -55,8 +55,8 @@ template <class = void>
boost::thread_specific_ptr<detail::LocalValues>&
getLocalValues()
{
static boost::thread_specific_ptr<detail::LocalValues> kTsp(&detail::LocalValues::cleanup);
return kTsp;
static boost::thread_specific_ptr<detail::LocalValues> kTSP(&detail::LocalValues::cleanup);
return kTSP;
}
} // namespace detail

View File

@@ -1,76 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,64 +0,0 @@
# `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.

View File

@@ -191,7 +191,7 @@ public:
private:
// Maximum line length for log messages.
// If the message exceeds this length it will be truncated with ellipses.
static constexpr auto kMaximumMessageCharacters = 12 * 1024;
static constexpr auto kMAXIMUM_MESSAGE_CHARACTERS = 12 * 1024;
static void
format(

View File

@@ -1,62 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,57 +0,0 @@
# `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.

View File

@@ -1,32 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,45 +0,0 @@
# `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.

View File

@@ -1,32 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,23 +0,0 @@
# `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.

View File

@@ -1,60 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,51 +0,0 @@
# `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.

View File

@@ -214,12 +214,12 @@ class Number
public:
// The range for the exponent when normalized
static constexpr int kMinExponent = -32768;
static constexpr int kMaxExponent = 32768;
constexpr static int kMIN_EXPONENT = -32768;
constexpr static int kMAX_EXPONENT = 32768;
static constexpr internalrep kMaxRep = std::numeric_limits<rep>::max();
static_assert(kMaxRep == 9'223'372'036'854'775'807);
static_assert(-kMaxRep == std::numeric_limits<rep>::min() + 1);
constexpr static internalrep kMAX_REP = std::numeric_limits<rep>::max();
static_assert(kMAX_REP == 9'223'372'036'854'775'807);
static_assert(-kMAX_REP == std::numeric_limits<rep>::min() + 1);
// May need to make unchecked private
struct Unchecked
@@ -409,26 +409,26 @@ public:
static internalrep
minMantissa()
{
return kRange.get().min;
return kRANGE.get().min;
}
static internalrep
maxMantissa()
{
return kRange.get().max;
return kRANGE.get().max;
}
static int
mantissaLog()
{
return kRange.get().log;
return kRANGE.get().log;
}
/// oneSmall is needed because the ranges are private
static constexpr Number
constexpr static Number
oneSmall();
/// oneLarge is needed because the ranges are private
static constexpr Number
constexpr static Number
oneLarge();
// And one is needed because it needs to choose between oneSmall and
@@ -445,25 +445,25 @@ private:
static thread_local RoundingMode mode;
// The available ranges for mantissa
static constexpr MantissaRange kSmallRange{MantissaRange::MantissaScale::Small};
static_assert(isPowerOfTen(kSmallRange.min));
static_assert(kSmallRange.min == 1'000'000'000'000'000LL);
static_assert(kSmallRange.max == 9'999'999'999'999'999LL);
static_assert(kSmallRange.log == 15);
static_assert(kSmallRange.min < kMaxRep);
static_assert(kSmallRange.max < kMaxRep);
static constexpr MantissaRange kLargeRange{MantissaRange::MantissaScale::Large};
static_assert(isPowerOfTen(kLargeRange.min));
static_assert(kLargeRange.min == 1'000'000'000'000'000'000ULL);
static_assert(kLargeRange.max == internalrep(9'999'999'999'999'999'999ULL));
static_assert(kLargeRange.log == 18);
static_assert(kLargeRange.min < kMaxRep);
static_assert(kLargeRange.max > kMaxRep);
constexpr static MantissaRange kSMALL_RANGE{MantissaRange::MantissaScale::Small};
static_assert(isPowerOfTen(kSMALL_RANGE.min));
static_assert(kSMALL_RANGE.min == 1'000'000'000'000'000LL);
static_assert(kSMALL_RANGE.max == 9'999'999'999'999'999LL);
static_assert(kSMALL_RANGE.log == 15);
static_assert(kSMALL_RANGE.min < kMAX_REP);
static_assert(kSMALL_RANGE.max < kMAX_REP);
constexpr static MantissaRange kLARGE_RANGE{MantissaRange::MantissaScale::Large};
static_assert(isPowerOfTen(kLARGE_RANGE.min));
static_assert(kLARGE_RANGE.min == 1'000'000'000'000'000'000ULL);
static_assert(kLARGE_RANGE.max == internalrep(9'999'999'999'999'999'999ULL));
static_assert(kLARGE_RANGE.log == 18);
static_assert(kLARGE_RANGE.min < kMAX_REP);
static_assert(kLARGE_RANGE.max > kMAX_REP);
// The range for the mantissa when normalized.
// Use reference_wrapper to avoid making copies, and prevent accidentally
// changing the values inside the range.
static thread_local std::reference_wrapper<MantissaRange const> kRange;
static thread_local std::reference_wrapper<MantissaRange const> kRANGE;
void
normalize();
@@ -471,7 +471,7 @@ private:
/** Normalize Number components to an arbitrary range.
*
* min/maxMantissa are parameters because this function is used by both
* normalize(), which reads from kRange, and by normalizeToRange,
* normalize(), which reads from kRANGE, and by normalizeToRange,
* which is public and can accept an arbitrary range from the caller.
*/
template <class T>
@@ -521,7 +521,7 @@ constexpr Number::Number(internalrep mantissa, int exponent, Unchecked) noexcept
{
}
static constexpr Number kNumZero{};
constexpr static Number kNUM_ZERO{};
inline Number::Number(bool negative, internalrep mantissa, int exponent, Normalized)
: Number(negative, mantissa, exponent, Unchecked{})
@@ -552,10 +552,10 @@ constexpr Number::rep
Number::mantissa() const noexcept
{
auto m = mantissa_;
if (m > kMaxRep)
if (m > kMAX_REP)
{
XRPL_ASSERT_PARTS(
!isnormal() || (m % 10 == 0 && m / 10 <= kMaxRep),
!isnormal() || (m % 10 == 0 && m / 10 <= kMAX_REP),
"xrpl::Number::mantissa",
"large normalized mantissa has no remainder");
m /= 10;
@@ -573,10 +573,10 @@ constexpr int
Number::exponent() const noexcept
{
auto e = exponent_;
if (mantissa_ > kMaxRep)
if (mantissa_ > kMAX_REP)
{
XRPL_ASSERT_PARTS(
!isnormal() || (mantissa_ % 10 == 0 && mantissa_ / 10 <= kMaxRep),
!isnormal() || (mantissa_ % 10 == 0 && mantissa_ / 10 <= kMAX_REP),
"xrpl::Number::exponent",
"large normalized mantissa has no remainder");
++e;
@@ -671,29 +671,29 @@ operator/(Number const& x, Number const& y)
inline Number
Number::min() noexcept
{
return Number{false, kRange.get().min, kMinExponent, Unchecked{}};
return Number{false, kRANGE.get().min, kMIN_EXPONENT, Unchecked{}};
}
inline Number
Number::max() noexcept
{
return Number{false, std::min(kRange.get().max, kMaxRep), kMaxExponent, Unchecked{}};
return Number{false, std::min(kRANGE.get().max, kMAX_REP), kMAX_EXPONENT, Unchecked{}};
}
inline Number
Number::lowest() noexcept
{
return Number{true, std::min(kRange.get().max, kMaxRep), kMaxExponent, Unchecked{}};
return Number{true, std::min(kRANGE.get().max, kMAX_REP), kMAX_EXPONENT, Unchecked{}};
}
inline bool
Number::isnormal() const noexcept
{
MantissaRange const& range = kRange;
MantissaRange const& range = kRANGE;
auto const absM = mantissa_;
return *this == Number{} ||
(range.min <= absM && absM <= range.max && (absM <= kMaxRep || absM % 10 == 0) &&
kMinExponent <= exponent_ && exponent_ <= kMaxExponent);
(range.min <= absM && absM <= range.max && (absM <= kMAX_REP || absM % 10 == 0) &&
kMIN_EXPONENT <= exponent_ && exponent_ <= kMAX_EXPONENT);
}
template <Integral64 T>

View File

@@ -1,363 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,54 +0,0 @@
# `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 1516 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 1819 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 NewtonRaphson 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.

View File

@@ -1,91 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,39 +0,0 @@
# `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.

View File

@@ -1,69 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,33 +0,0 @@
# `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.

View File

@@ -1,34 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,25 +0,0 @@
# `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.

View File

@@ -1,144 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,40 +0,0 @@
# `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.

View File

@@ -1,136 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,43 +0,0 @@
# `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.

View File

@@ -57,10 +57,10 @@ template <class T>
std::shared_ptr<T> const&
SharedWeakCachePointer<T>::getStrong() const
{
static std::shared_ptr<T> const kEmpty;
static std::shared_ptr<T> const kEMPTY;
if (auto p = std::get_if<std::shared_ptr<T>>(&combo_))
return *p;
return kEmpty;
return kEMPTY;
}
template <class T>

View File

@@ -1,395 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,39 +0,0 @@
# `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.

View File

@@ -1,110 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,84 +0,0 @@
# `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.

View File

@@ -1,86 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,33 +0,0 @@
# `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.

View File

@@ -34,7 +34,7 @@ template <typename T>
concept SomeChar = std::same_as<std::remove_cvref_t<T>, int8_t> ||
std::same_as<std::remove_cvref_t<T>, char> || std::same_as<std::remove_cvref_t<T>, uint8_t>;
inline constexpr std::array<std::optional<int>, 256> const kDigitLookupTable = []() {
inline constexpr std::array<std::optional<int>, 256> const kDIGIT_LOOKUP_TABLE = []() {
std::array<std::optional<int>, 256> t{};
for (int i = 0; i < 10; ++i)
@@ -52,7 +52,7 @@ inline constexpr std::array<std::optional<int>, 256> const kDigitLookupTable = [
inline std::optional<int>
hexCharToInt(SomeChar auto hexChar)
{
return kDigitLookupTable[static_cast<uint8_t>(hexChar)];
return kDIGIT_LOOKUP_TABLE[static_cast<uint8_t>(hexChar)];
}
} // namespace detail

View File

@@ -1,125 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,54 +0,0 @@
# `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 (015) 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, 163 characters per label) and requires at least one dot with a 263 character alphabetic TLD. Length is gated first (4128 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.

View File

@@ -1,323 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,69 +0,0 @@
# `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.

View File

@@ -1,415 +0,0 @@
{
"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": []
}

View File

@@ -1,60 +0,0 @@
# `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.

View File

@@ -1,71 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,38 +0,0 @@
# `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 1419): 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 2125): 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 2731): 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 3337): 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 3943): 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++.

View File

@@ -1,14 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,33 +0,0 @@
# `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.

View File

@@ -30,8 +30,8 @@ public:
now(); // seconds since xrpld program start
private:
static std::atomic<rep> kNow;
static std::atomic<bool> kStop;
static std::atomic<rep> kNOW;
static std::atomic<bool> kSTOP;
struct UpdateThread : private std::thread
{

View File

@@ -1,36 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,40 +0,0 @@
# `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.

View File

@@ -1,39 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,25 +0,0 @@
# `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.

View File

@@ -1,54 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,39 +0,0 @@
# `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 (20042008) 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 `AZ az 09 + /`, `=` padding) rather than the URL-safe variant, matching the expectations of existing tooling and the external validator configuration format.

View File

@@ -73,12 +73,12 @@ class BaseUInt
static_assert(Bits >= 64, "The length of a base_uint in bits must be at least 64.");
static constexpr std::size_t kWidth = Bits / 32;
static constexpr std::size_t kWIDTH = Bits / 32;
// This is really big-endian in byte order.
// We sometimes use std::uint32_t for speed.
std::array<std::uint32_t, kWidth> data_;
std::array<std::uint32_t, kWIDTH> data_;
public:
//--------------------------------------------------------------------------
@@ -86,8 +86,8 @@ public:
// STL Container Interface
//
static constexpr std::size_t kBytes = Bits / 8;
static_assert(sizeof(data_) == kBytes, "");
static std::size_t constexpr kBYTES = Bits / 8;
static_assert(sizeof(data_) == kBYTES, "");
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
@@ -121,7 +121,7 @@ public:
iterator
end()
{
return data() + kBytes;
return data() + kBYTES;
}
[[nodiscard]] const_iterator
begin() const
@@ -131,7 +131,7 @@ public:
[[nodiscard]] const_iterator
end() const
{
return data() + kBytes;
return data() + kBYTES;
}
[[nodiscard]] const_iterator
cbegin() const
@@ -141,7 +141,7 @@ public:
[[nodiscard]] const_iterator
cend() const
{
return data() + kBytes;
return data() + kBYTES;
}
/** Value hashing function.
@@ -167,7 +167,7 @@ private:
explicit BaseUInt(void const* data, VoidHelper)
{
memcpy(data_.data(), data, kBytes);
memcpy(data_.data(), data, kBYTES);
}
// Helper function to initialize a base_uint from a std::string_view.
@@ -336,7 +336,7 @@ public:
[[nodiscard]] constexpr int
signum() const
{
for (int i = 0; i < kWidth; i++)
for (int i = 0; i < kWIDTH; i++)
{
if (data_[i] != 0)
return 1;
@@ -348,7 +348,7 @@ public:
bool
operator!() const
{
return *this == beast::kZero;
return *this == beast::kZERO;
}
constexpr BaseUInt
@@ -356,7 +356,7 @@ public:
{
BaseUInt ret;
for (int i = 0; i < kWidth; i++)
for (int i = 0; i < kWIDTH; i++)
ret.data_[i] = ~data_[i];
return ret;
@@ -365,7 +365,7 @@ public:
BaseUInt&
operator=(std::uint64_t uHost)
{
*this = beast::kZero;
*this = beast::kZERO;
// NOLINTBEGIN(cppcoreguidelines-pro-type-member-init)
union
{
@@ -375,15 +375,15 @@ public:
// NOLINTEND(cppcoreguidelines-pro-type-member-init)
// Put in least significant bits.
ul = boost::endian::native_to_big(uHost);
data_[kWidth - 2] = u[0];
data_[kWidth - 1] = u[1];
data_[kWIDTH - 2] = u[0];
data_[kWIDTH - 1] = u[1];
return *this;
}
BaseUInt&
operator^=(BaseUInt const& b)
{
for (int i = 0; i < kWidth; i++)
for (int i = 0; i < kWIDTH; i++)
data_[i] ^= b.data_[i];
return *this;
@@ -392,7 +392,7 @@ public:
BaseUInt&
operator&=(BaseUInt const& b)
{
for (int i = 0; i < kWidth; i++)
for (int i = 0; i < kWIDTH; i++)
data_[i] &= b.data_[i];
return *this;
@@ -401,7 +401,7 @@ public:
BaseUInt&
operator|=(BaseUInt const& b)
{
for (int i = 0; i < kWidth; i++)
for (int i = 0; i < kWIDTH; i++)
data_[i] |= b.data_[i];
return *this;
@@ -411,7 +411,7 @@ public:
operator++()
{
// prefix operator
for (int i = kWidth - 1; i >= 0; --i)
for (int i = kWIDTH - 1; i >= 0; --i)
{
data_[i] = boost::endian::native_to_big(boost::endian::big_to_native(data_[i]) + 1);
if (data_[i] != 0)
@@ -434,7 +434,7 @@ public:
BaseUInt&
operator--()
{
for (int i = kWidth - 1; i >= 0; --i)
for (int i = kWIDTH - 1; i >= 0; --i)
{
auto prev = data_[i];
data_[i] = boost::endian::native_to_big(boost::endian::big_to_native(data_[i]) - 1);
@@ -475,7 +475,7 @@ public:
{
std::uint64_t carry = 0;
for (int i = kWidth - 1; i >= 0; i--)
for (int i = kWIDTH - 1; i >= 0; i--)
{
std::uint64_t const n = carry + boost::endian::big_to_native(data_[i]) +
boost::endian::big_to_native(b.data_[i]);
@@ -526,10 +526,10 @@ public:
return parseHex(std::string_view{str});
}
static constexpr std::size_t
constexpr static std::size_t
size()
{
return kBytes;
return kBYTES;
}
BaseUInt<Bits, Tag>&
@@ -543,17 +543,17 @@ public:
[[nodiscard]] bool
isZero() const
{
return *this == beast::kZero;
return *this == beast::kZERO;
}
[[nodiscard]] bool
isNonZero() const
{
return *this != beast::kZero;
return *this != beast::kZERO;
}
void
zero()
{
*this = beast::kZero;
*this = beast::kZERO;
}
};
@@ -639,7 +639,7 @@ template <std::size_t Bits, class Tag>
inline std::string
toShortString(BaseUInt<Bits, Tag> const& a)
{
static_assert(BaseUInt<Bits, Tag>::kBytes > 4, "For 4 bytes or less, use a native type");
static_assert(BaseUInt<Bits, Tag>::kBYTES > 4, "For 4 bytes or less, use a native type");
return strHex(a.cbegin(), a.cbegin() + 4) + "...";
}

View File

@@ -1,152 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,41 +0,0 @@
# `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 20092011), 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 47, 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.

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