Compare commits

..

54 Commits

Author SHA1 Message Date
Gregory Tsipenyuk
e8de5e5d51 Fix flawed pathfinding constraints will completely block DEX trades for MPT holders 2026-06-03 11:41:19 -04:00
Gregory Tsipenyuk
a0952a1133 Fix Pathfinder constructor silently drops srcAmount 2026-06-01 18:18:32 -04:00
Gregory Tsipenyuk
66b54c1199 Fix arithmetic helpers throw std::overflow_error on extreme MPT/IOU amounts 2026-05-29 15:56:46 -04:00
Gregory Tsipenyuk
949deb5dc8 Merge branch 'develop' into gregtatcam/mpt/audit-attackathon-fixes-1 2026-05-28 16:01:43 -04:00
Bart
2f3558c610 ci: Run PR title and description checks on staging and release branches (#7331)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-05-28 14:57:29 +00:00
Ayaz Salikhov
f9551ac5ca style: Run shfmt on workflows, actions and markdown bash code (#7333) 2026-05-27 19:24:18 +00:00
Bart
1acc42313c release: Bump version to 3.2.0-rc2 (#7348) 2026-05-27 15:11:38 -04:00
Bart
396d772a15 refactor: Enable support for fixCleanup3_2_0 amendment (#7347) 2026-05-27 19:10:33 +00:00
Ayaz Salikhov
1438bf1c67 release: Bump version to 3.2.0-rc1 (#7335) 2026-05-27 13:20:57 -04:00
Gregory Tsipenyuk
b2a0a2b31a Fix missing type tags in cross-type book hashing aliases in getBookBase() 2026-05-27 12:47:37 -04:00
Gregory Tsipenyuk
77d5ceaf05 Fix clang-tidy 2026-05-27 12:24:53 -04:00
Ed Hennis
7da643d864 fix: Fix a rounding error at the Number::maxRep cusp (#7051)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Vito Tumas <5780819+Tapanito@users.noreply.github.com>
2026-05-27 15:19:20 +00:00
Gregory Tsipenyuk
c42ec53a84 Fix book_offers rpc reports inflated mpt liquidity 2026-05-27 10:11:22 -04:00
Ayaz Salikhov
1162371def ci: Only push docker images in XRPLF/rippled (#7330) 2026-05-26 20:03:04 +00:00
dependabot[bot]
2a0feca46b ci: [DEPENDABOT] bump docker/setup-buildx-action from 4.0.0 to 4.1.0 (#7322)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 19:36:32 +00:00
dependabot[bot]
108a4c8217 ci: [DEPENDABOT] bump codecov/codecov-action from 6.0.0 to 6.0.1 (#7321)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 19:36:21 +00:00
dependabot[bot]
4584b01bde ci: [DEPENDABOT] bump docker/build-push-action from 7.1.0 to 7.2.0 (#7320)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 19:36:13 +00:00
dependabot[bot]
7c59786565 ci: [DEPENDABOT] bump docker/metadata-action from 6.0.0 to 6.1.0 (#7319)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 19:36:00 +00:00
dependabot[bot]
9623e67b76 ci: [DEPENDABOT] bump docker/login-action from 4.1.0 to 4.2.0 (#7318)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 19:35:52 +00:00
Andrzej Budzanowski
85af406a0f fix: Update clang-tidy to include src/tests directory header check (#7307) 2026-05-26 19:35:32 +00:00
Ayaz Salikhov
ac33fb32a7 chore: Pin Python packages for codegen using uv (#7329) 2026-05-26 18:35:38 +00:00
Ayaz Salikhov
23d0812827 style: Use shfmt instead of bashate (#7326) 2026-05-26 18:28:23 +00:00
Vito Tumas
49567e7283 fix: Fix edge-case where vault-depositor may get stuck (#7139) 2026-05-26 18:18:40 +00:00
Vito Tumas
633ef4706f fix: Fix VaultInvariant and VaultDeposit precision bugs at IOU scale boundaries (#7272)
Co-authored-by: Bart <bthomee@users.noreply.github.com>
2026-05-26 16:32:44 +00:00
Ayaz Salikhov
49cb3f45a4 ci: Add clang to nix images (#7308)
Co-authored-by: semgrep-companion-app[bot] <218312740+semgrep-companion-app[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-26 15:45:33 +00:00
Vito Tumas
22a21b175e fix: Include management-fee delta in doOverpayment assertion (#7039) 2026-05-26 14:01:52 +00:00
Pratik Mankawde
e9d885bd9b fix: Fix clang-tidy pre-commit hook to locate compile_commands.json from repo root (#7325)
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
2026-05-26 13:50:18 +00:00
Jingchen
a911f9089e fix: Use consistent scale for debtTotal (#7093)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-24 20:44:29 +00:00
Peter Chen
e34c2667d7 fix: Skip deleted book directories and non-root modifications in ValidBookDirectory invariant (#7312) 2026-05-24 20:37:16 +00:00
Valentin Balaschenko
30de556224 fix: Address review feedback on FD/handle guarding (#5823 follow-up) (#7310) 2026-05-23 14:48:48 +00:00
Gregory Tsipenyuk
dcd2ff0b5f fix: Fix non-canonical MPT amount (#7117)
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-23 06:40:26 +00:00
Bart
dfb9b8ed9a release: Bump version to 3.2.0-b7 (#7316)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-05-22 19:32:12 +00:00
Jingchen
179e73594a fix: Check if the MPT first loss cover can be sent to the broker before deleting the broker (#7125)
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
2026-05-22 11:58:48 +00:00
Michael Legleux
15dd653e4b fix: Fix RPM prerelease ordering and start xrpld on DEB install (#7313) 2026-05-22 11:30:45 +00:00
Michael Legleux
a37afe13ff ci: Re-enable full nproc for Linux (#7315) 2026-05-22 11:30:37 +00:00
Gregory Tsipenyuk
3547a9335f fix: Add assorted MPT/DEX fixes (#7040)
Co-authored-by: xrplf-ai-reviewer[bot] <266832837+xrplf-ai-reviewer[bot]@users.noreply.github.com>
Co-authored-by: Shawn Xie <35279399+shawnxie999@users.noreply.github.com>
2026-05-21 18:29:53 +00:00
Bart
1a98182e23 refactor: Remove dead fetchBatch code (#7309)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-05-21 17:52:41 +00:00
Bart
79308705c5 release: Bump version to 3.2.0-b6 (#7311)
Co-authored-by: Bart <11445373+bthomee@users.noreply.github.com>
2026-05-21 17:50:59 +00:00
Vito Tumas
e24de65f42 chore: Revert graceful peer disconnection and follow-up fix (#7296) 2026-05-21 16:13:41 +00:00
Vito Tumas
7fdaa0a5ef fix: Fix IOU precision issues in LoanBrokerCover transactions (#7274) 2026-05-21 14:51:58 +00:00
Vito Tumas
795dc5e364 fix: Avoid principal-zeroing in non-final loan payments at coarse scale (#7050)
Co-authored-by: Ed Hennis <ed@ripple.com>
2026-05-21 14:46:26 +00:00
Pratik Mankawde
f6fd5ddb0a fix: Add null check (#7305)
Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-21 13:24:04 +00:00
Rithvik Reddygari
afcf6fbcdc docs: Add --parallel flag to cmake build commands in BUILD.md (#7302) 2026-05-21 06:33:19 +00:00
Shawn Xie
28cc20c816 fix: Fix wrong hybrid offer orderbook placement and update LedgerStateFix to amend ExchangeRate meta (#7087)
Co-authored-by: Peter Chen <ychen@ripple.com>
2026-05-21 06:19:04 +00:00
Alex Kremer
a830ab10ef style: More clang-tidy identifier renaming (#7290) 2026-05-20 21:31:15 +00:00
Shawn Xie
8c0080020f fix: Update pDEX invariant firing under a valid offer deletion (#7118)
Co-authored-by: Peter Chen <ychen@ripple.com>
2026-05-20 21:10:04 +00:00
yinyiqian1
9cb0740673 fix: Fix multisign and signfor to check for delegate (#7064) 2026-05-20 20:24:09 +00:00
Mayukha Vadari
242ce3e9e4 refactor: Fix sfGeneric and sfInvalid field names (#7300) 2026-05-20 19:47:59 +00:00
box4wangjing
a5d238e7d4 docs: Fix some comments to improve readability (#7122)
Signed-off-by: box4wangjing <box4wangjing@outlook.com>
Co-authored-by: Mayukha Vadari <mvadari@ripple.com>
2026-05-20 19:46:45 +00:00
Vito Tumas
9cb049276d feat: Propagate underlying MPT flags to vault shares (#7077)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Denis Angell <dangell@transia.co>
Co-authored-by: Fomo <508629+shortthefomo@users.noreply.github.com>
Co-authored-by: Bart <bthomee@users.noreply.github.com>
Co-authored-by: Ayaz Salikhov <mathbunnyru@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 19:44:09 +00:00
Vito Tumas
93ac1aa7aa fix: Disable unnecessary sanity-check in VaultDeposit (#7288) 2026-05-19 16:38:50 +00:00
dependabot[bot]
d9a3af8207 ci: [DEPENDABOT] bump actions/upload-artifact from 7.0.0 to 7.0.1 (#7286)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 16:35:38 +00:00
Ayaz Salikhov
8d1083e5ea ci: Only run reusable package in public repos (#7293) 2026-05-19 13:15:11 +00:00
Fomo
1e45d363c5 fix: Set default peering port to 2459 (#6848)
Co-authored-by: Bart <bthomee@users.noreply.github.com>
2026-05-19 06:05:47 +00:00
2686 changed files with 13217 additions and 336394 deletions

View File

@@ -191,11 +191,14 @@ CheckOptions:
readability-identifier-naming.ParameterCase: camelBack
readability-identifier-naming.FunctionCase: camelBack
readability-identifier-naming.MemberCase: camelBack
readability-identifier-naming.PrivateMemberCase: camelBack
readability-identifier-naming.PrivateMemberSuffix: _
readability-identifier-naming.ProtectedMemberCase: camelBack
readability-identifier-naming.ProtectedMemberSuffix: _
readability-identifier-naming.PublicMemberCase: camelBack
readability-identifier-naming.PublicMemberSuffix: ""
readability-identifier-naming.GlobalFunctionIgnoredRegexp: "^(to_string|hash_append|tuple_hash)$"
HeaderFilterRegex: '^.*/(test|xrpl|xrpld)/.*\.(h|hpp|ipp)$'
HeaderFilterRegex: '^.*/(tests?|xrpl|xrpld)/.*\.(h|hpp|ipp)$'
ExcludeHeaderFilterRegex: '^.*/protocol_autogen/.*\.(h|hpp)$'
WarningsAsErrors: "*"

View File

@@ -37,12 +37,12 @@ runs:
run: |
echo 'Installing dependencies.'
conan install \
--profile ci \
--build="${BUILD_OPTION}" \
--options:host='&:tests=True' \
--options:host='&:xrpld=True' \
--settings:all build_type="${BUILD_TYPE}" \
--conf:all tools.build:jobs=${BUILD_NPROC} \
--conf:all tools.build:verbosity="${LOG_VERBOSITY}" \
--conf:all tools.compilation:verbosity="${LOG_VERBOSITY}" \
.
--profile ci \
--build="${BUILD_OPTION}" \
--options:host='&:tests=True' \
--options:host='&:xrpld=True' \
--settings:all build_type="${BUILD_TYPE}" \
--conf:all tools.build:jobs=${BUILD_NPROC} \
--conf:all tools.build:verbosity="${LOG_VERBOSITY}" \
--conf:all tools.compilation:verbosity="${LOG_VERBOSITY}" \
.

View File

@@ -15,7 +15,7 @@ runs:
shell: bash
env:
VERSION: ${{ github.ref_name }}
run: echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
run: echo "VERSION=${VERSION}" >>"${GITHUB_ENV}"
# When a tag is not pushed, then the version (e.g. 1.2.3-b0) is extracted
# from the BuildInfo.cpp file and the shortened commit hash appended to it.
@@ -28,17 +28,17 @@ runs:
echo 'Extracting version from BuildInfo.cpp.'
VERSION="$(cat src/libxrpl/protocol/BuildInfo.cpp | grep "versionString =" | awk -F '"' '{print $2}')"
if [[ -z "${VERSION}" ]]; then
echo 'Unable to extract version from BuildInfo.cpp.'
exit 1
echo 'Unable to extract version from BuildInfo.cpp.'
exit 1
fi
echo 'Appending shortened commit hash to version.'
SHA='${{ github.sha }}'
VERSION="${VERSION}+${SHA:0:7}"
echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
echo "VERSION=${VERSION}" >>"${GITHUB_ENV}"
- name: Output version
id: version
shell: bash
run: echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
run: echo "version=${VERSION}" >>"${GITHUB_OUTPUT}"

403
.github/scripts/format-inline-bash.py vendored Executable file
View File

@@ -0,0 +1,403 @@
#!/usr/bin/env python3
"""
Format embedded shell snippets using the shfmt hook configured in
.pre-commit-config.yaml.
Two shapes are recognised:
* YAML workflow/action files: literal block-scalar runs (`run: |`) and
single-line runs (`run: some command`). A single-line run is upgraded to
a `run: |` block scalar if shfmt's output spans multiple lines.
* Markdown files: ``` ```bash ``` fenced code blocks.
Any block that shfmt cannot parse is skipped with a warning on stderr, so
the file is left untouched and surrounding blocks still get formatted.
For each occurrence the body is dedented, written to a temp .sh file,
formatted via `pre-commit run shfmt --files <temp>` (falling back to
`prek`), then re-indented and written back in place.
When invoked without arguments, every .yml/.yaml under .github/ plus every
.md file in the repo is scanned. When invoked with file arguments (the
pre-commit case), only those files are processed.
"""
from __future__ import annotations
import re
import shutil
import subprocess
import sys
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Union
REPO = Path(__file__).resolve().parents[2]
_HOOK_RUNNER = next((cmd for cmd in ("pre-commit", "prek") if shutil.which(cmd)), None)
if _HOOK_RUNNER is None:
sys.exit("error: neither `pre-commit` nor `prek` found on PATH")
RUN_BLOCK_RE = re.compile(r"^(?P<prefix>[ \t]*(?:- )?)run:[ \t]*\|[+-]?[ \t]*$")
RUN_INLINE_RE = re.compile(
r"^(?P<prefix>[ \t]*(?:- )?)run:[ \t]+" r"(?P<value>(?!\|[+-]?[ \t]*$)\S.*?)[ \t]*$"
)
MD_BASH_OPEN_RE = re.compile(r"^(?P<indent>[ ]{0,3})`{3}bash[ \t]*$")
MD_FENCE_CLOSE_RE = re.compile(r"^[ ]{0,3}`{3,}[ \t]*$")
@dataclass(frozen=True)
class BlockRun:
"""A `run: |` block scalar; `body_start:body_end` slices into `lines`."""
body_start: int
body_end: int
body_indent: int
@dataclass(frozen=True)
class InlineRun:
"""A single-line `run: value` at `line_idx`."""
line_idx: int
prefix: str
value: str
@dataclass(frozen=True)
class MdBashBlock:
"""A markdown ``` ```bash ``` fenced code block.
`body_start:body_end` slices into the file's lines; `open_line_idx`
points at the opening fence line.
"""
open_line_idx: int
body_start: int
body_end: int
body_indent: int
RunItem = Union[BlockRun, InlineRun]
def _scan_block_body(
lines: list[str], body_start: int, run_col: int
) -> tuple[int | None, int]:
"""Locate the body of a `run: |` block scalar starting at `body_start`.
Returns `(body_indent, scan_end)`. `scan_end` is the line index where the
outer scanner should resume. `body_indent` is `None` when no body is
present (the scalar is empty, or the next non-blank line has indent
`<= run_col`).
"""
body_indent: int | None = None
scan_end = len(lines)
for idx in range(body_start, len(lines)):
line = lines[idx]
if line.strip() == "":
continue
indent = len(line) - len(line.lstrip(" "))
if body_indent is None:
if indent > run_col:
body_indent = indent
else:
scan_end = idx
break
elif indent < body_indent:
scan_end = idx
break
if body_indent is not None:
while scan_end > body_start and lines[scan_end - 1].strip() == "":
scan_end -= 1
if scan_end <= body_start:
body_indent = None
return body_indent, scan_end
def find_run_blocks(lines: list[str]) -> list[RunItem]:
"""Return run items in document order."""
items: list[RunItem] = []
line_idx = 0
while line_idx < len(lines):
line = lines[line_idx]
if block_match := RUN_BLOCK_RE.match(line):
run_col = len(block_match.group("prefix"))
body_start = line_idx + 1
body_indent, scan_end = _scan_block_body(lines, body_start, run_col)
if body_indent is not None:
items.append(
BlockRun(
body_start=body_start,
body_end=scan_end,
body_indent=body_indent,
)
)
line_idx = scan_end
continue
if inline_match := RUN_INLINE_RE.match(line):
items.append(
InlineRun(
line_idx=line_idx,
prefix=inline_match.group("prefix"),
value=inline_match.group("value"),
)
)
line_idx += 1
return items
def find_md_bash_blocks(lines: list[str]) -> list[MdBashBlock]:
"""Return ``` ```bash ``` fenced code blocks in document order."""
blocks: list[MdBashBlock] = []
line_idx = 0
while line_idx < len(lines):
open_match = MD_BASH_OPEN_RE.match(lines[line_idx])
if not open_match:
line_idx += 1
continue
body_start = line_idx + 1
close_idx = next(
(
j
for j in range(body_start, len(lines))
if MD_FENCE_CLOSE_RE.match(lines[j])
),
None,
)
if close_idx is None:
line_idx = body_start
continue
body = lines[body_start:close_idx]
non_blank = [b for b in body if b.strip()]
body_indent = (
min(len(b) - len(b.lstrip(" ")) for b in non_blank)
if non_blank
else len(open_match.group("indent"))
)
blocks.append(
MdBashBlock(
open_line_idx=line_idx,
body_start=body_start,
body_end=close_idx,
body_indent=body_indent,
)
)
line_idx = close_idx + 1
return blocks
def dedent(lines: list[str], n: int) -> list[str]:
pad = " " * n
return [
(
""
if line.strip() == ""
else (line[n:] if line.startswith(pad) else line.lstrip(" "))
)
for line in lines
]
def reindent(lines: list[str], n: int) -> list[str]:
pad = " " * n
return [pad + line if line else "" for line in lines]
_SHFMT_ERR_RE = re.compile(r"\.sh:\d+:\d+:\s")
_GHA_EXPR_RE = re.compile(r"\$\{\{.*?\}\}", re.DOTALL)
_GHA_PLACEHOLDER_RE = re.compile(r"__GHA_EXPR_(\d+)__")
def _encode_gha_exprs(text: str) -> tuple[str, list[str]]:
"""Replace `${{ ... }}` expressions with bash-safe placeholder identifiers."""
exprs: list[str] = []
def repl(match: re.Match[str]) -> str:
exprs.append(match.group(0))
return f"__GHA_EXPR_{len(exprs) - 1}__"
return _GHA_EXPR_RE.sub(repl, text), exprs
def _decode_gha_exprs(text: str, exprs: list[str]) -> str:
"""Restore `${{ ... }}` expressions from placeholder identifiers."""
return _GHA_PLACEHOLDER_RE.sub(lambda m: exprs[int(m.group(1))], text)
def shfmt_via_hook(tmp_path: Path) -> tuple[bool, str]:
# `${{ ... }}` is not valid shell, so swap it for a placeholder identifier
# that shfmt can parse, then restore it after formatting.
encoded, exprs = _encode_gha_exprs(tmp_path.read_text())
if exprs:
tmp_path.write_text(encoded)
res = subprocess.run(
[_HOOK_RUNNER, "run", "shfmt", "--files", str(tmp_path)],
cwd=REPO,
capture_output=True,
text=True,
)
output = res.stdout + res.stderr
# shfmt emits parse errors as "<path>:<line>:<col>: <message>".
parse_err = bool(_SHFMT_ERR_RE.search(output))
# A non-zero exit that is neither a parse error nor pre-commit's "I had
# to modify files" signal means the hook itself failed to run (missing
# binary, install failure, bad config, ...). Surface that loudly rather
# than silently treating it as a no-op.
if (
res.returncode != 0
and not parse_err
and "files were modified by this hook" not in output
):
sys.exit(
f"error: `{_HOOK_RUNNER} run shfmt` failed with exit {res.returncode}:\n{output}"
)
if exprs and not parse_err:
tmp_path.write_text(_decode_gha_exprs(tmp_path.read_text(), exprs))
return not parse_err, output
def _skip(path: Path, where: int, kind: str, output: str) -> None:
print(
f" shfmt could not parse {kind} at {path}:{where + 1} — skipped",
file=sys.stderr,
)
print(f" {output.strip()}", file=sys.stderr)
def process_yaml_file(path: Path, tmp_path: Path) -> int:
text = path.read_text()
had_nl = text.endswith("\n")
lines = text.split("\n")
if had_nl:
lines = lines[:-1]
items = find_run_blocks(lines)
if not items:
return 0
changed = 0
# Process in reverse so earlier indices remain valid as we splice.
for item in reversed(items):
if isinstance(item, BlockRun):
body = lines[item.body_start : item.body_end]
tmp_path.write_text("\n".join(dedent(body, item.body_indent)) + "\n")
ok, output = shfmt_via_hook(tmp_path)
if not ok:
_skip(path, item.body_start, "block", output)
continue
formatted = tmp_path.read_text().rstrip("\n")
new_body = reindent(formatted.split("\n"), item.body_indent)
if new_body != body:
lines[item.body_start : item.body_end] = new_body
changed += 1
else:
tmp_path.write_text(item.value + "\n")
ok, output = shfmt_via_hook(tmp_path)
if not ok:
_skip(path, item.line_idx, "inline run", output)
continue
formatted = tmp_path.read_text().rstrip("\n")
if formatted == item.value:
continue
formatted_lines = formatted.split("\n")
if len(formatted_lines) == 1:
lines[item.line_idx] = f"{item.prefix}run: {formatted}"
else:
body_indent = len(item.prefix) + 2
lines[item.line_idx : item.line_idx + 1] = [
f"{item.prefix}run: |",
*reindent(formatted_lines, body_indent),
]
changed += 1
new_text = "\n".join(lines) + ("\n" if had_nl else "")
if new_text != text:
path.write_text(new_text)
return changed
def process_md_file(path: Path, tmp_path: Path) -> int:
text = path.read_text()
had_nl = text.endswith("\n")
lines = text.split("\n")
if had_nl:
lines = lines[:-1]
blocks = find_md_bash_blocks(lines)
if not blocks:
return 0
changed = 0
for block in reversed(blocks):
body = lines[block.body_start : block.body_end]
tmp_path.write_text("\n".join(dedent(body, block.body_indent)) + "\n")
ok, output = shfmt_via_hook(tmp_path)
if not ok:
_skip(path, block.open_line_idx, "```bash block", output)
continue
formatted = tmp_path.read_text().rstrip("\n")
formatted_lines = formatted.split("\n") if formatted else []
new_body = reindent(formatted_lines, block.body_indent)
if new_body != body:
lines[block.body_start : block.body_end] = new_body
changed += 1
new_text = "\n".join(lines) + ("\n" if had_nl else "")
if new_text != text:
path.write_text(new_text)
return changed
def process_file(path: Path, tmp_path: Path) -> int:
if path.suffix in (".yml", ".yaml"):
return process_yaml_file(path, tmp_path)
if path.suffix == ".md":
return process_md_file(path, tmp_path)
return 0
def gather_files(argv: list[str]) -> list[Path]:
"""Return YAML workflow/action files and markdown files that we should
process — either the paths in `argv` or, when `argv` is empty, every
such file in the repo (skipping `external/`)."""
if argv:
candidates: list[Path] = [
(REPO / a).resolve() if not Path(a).is_absolute() else Path(a) for a in argv
]
else:
gh = REPO / ".github"
candidates = [
*gh.rglob("*.yml"),
*gh.rglob("*.yaml"),
*(
p
for p in REPO.rglob("*.md")
if "external" not in p.relative_to(REPO).parts
),
]
return sorted(
p
for p in candidates
if p.exists()
and (
(p.suffix in (".yml", ".yaml") and ".github" in p.parts)
or p.suffix == ".md"
)
)
def main(argv: list[str]) -> int:
files = gather_files(argv)
if not files:
return 0
with tempfile.TemporaryDirectory(prefix="format-inline-bash-") as tmpdir:
tmp_path = Path(tmpdir) / "shfmt.sh"
total = 0
for f in files:
n = process_file(f, tmp_path)
if n:
print(f"{f.relative_to(REPO)}: reformatted {n} block(s)")
total += n
return 1 if total else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

0
.github/scripts/levelization/generate.py vendored Normal file → Executable file
View File

View File

@@ -6,7 +6,7 @@ set -e
# On MacOS, ensure that GNU sed is installed and available as `gsed`.
SED_COMMAND=sed
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi

View File

@@ -8,12 +8,12 @@ set -e
SED_COMMAND=sed
HEAD_COMMAND=head
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi
SED_COMMAND=gsed
if ! command -v ghead &> /dev/null; then
if ! command -v ghead &>/dev/null; then
echo "Error: ghead is not installed. Please install it using 'brew install coreutils'."
exit 1
fi
@@ -74,10 +74,10 @@ if grep -q '"xrpld"' cmake/XrplCore.cmake; then
# The script has been rerun, so just restore the name of the binary.
${SED_COMMAND} -i 's/"xrpld"/"rippled"/' cmake/XrplCore.cmake
elif ! grep -q '"rippled"' cmake/XrplCore.cmake; then
${HEAD_COMMAND} -n -1 cmake/XrplCore.cmake > cmake.tmp
echo ' # For the time being, we will keep the name of the binary as it was.' >> cmake.tmp
echo ' set_target_properties(xrpld PROPERTIES OUTPUT_NAME "rippled")' >> cmake.tmp
tail -1 cmake/XrplCore.cmake >> cmake.tmp
${HEAD_COMMAND} -n -1 cmake/XrplCore.cmake >cmake.tmp
echo ' # For the time being, we will keep the name of the binary as it was.' >>cmake.tmp
echo ' set_target_properties(xrpld PROPERTIES OUTPUT_NAME "rippled")' >>cmake.tmp
tail -1 cmake/XrplCore.cmake >>cmake.tmp
mv cmake.tmp cmake/XrplCore.cmake
fi

View File

@@ -6,7 +6,7 @@ set -e
# On MacOS, ensure that GNU sed is installed and available as `gsed`.
SED_COMMAND=sed
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi

View File

@@ -6,7 +6,7 @@ set -e
# On MacOS, ensure that GNU sed is installed and available as `gsed`.
SED_COMMAND=sed
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi
@@ -62,37 +62,37 @@ done
# restoring the verbiage that is already present in LICENSE.md. Ensure that if
# the script is run multiple times, duplicate notices are not added.
if ! grep -q 'Raw Material Software' include/xrpl/beast/core/CurrentThreadName.h; then
echo -e "// Portions of this file are from JUCE (http://www.juce.com).\n// Copyright (c) 2013 - Raw Material Software Ltd.\n// Please visit http://www.juce.com\n\n$(cat include/xrpl/beast/core/CurrentThreadName.h)" > include/xrpl/beast/core/CurrentThreadName.h
echo -e "// Portions of this file are from JUCE (http://www.juce.com).\n// Copyright (c) 2013 - Raw Material Software Ltd.\n// Please visit http://www.juce.com\n\n$(cat include/xrpl/beast/core/CurrentThreadName.h)" >include/xrpl/beast/core/CurrentThreadName.h
fi
if ! grep -q 'Dev Null' src/test/app/NetworkID_test.cpp; then
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/app/NetworkID_test.cpp)" > src/test/app/NetworkID_test.cpp
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/app/NetworkID_test.cpp)" >src/test/app/NetworkID_test.cpp
fi
if ! grep -q 'Dev Null' src/test/app/tx/apply_test.cpp; then
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/app/tx/apply_test.cpp)" > src/test/app/tx/apply_test.cpp
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/app/tx/apply_test.cpp)" >src/test/app/tx/apply_test.cpp
fi
if ! grep -q 'Dev Null' src/test/rpc/ManifestRPC_test.cpp; then
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/rpc/ManifestRPC_test.cpp)" > src/test/rpc/ManifestRPC_test.cpp
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/rpc/ManifestRPC_test.cpp)" >src/test/rpc/ManifestRPC_test.cpp
fi
if ! grep -q 'Dev Null' src/test/rpc/ValidatorInfo_test.cpp; then
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/rpc/ValidatorInfo_test.cpp)" > src/test/rpc/ValidatorInfo_test.cpp
echo -e "// Copyright (c) 2020 Dev Null Productions\n\n$(cat src/test/rpc/ValidatorInfo_test.cpp)" >src/test/rpc/ValidatorInfo_test.cpp
fi
if ! grep -q 'Dev Null' src/xrpld/rpc/handlers/server_info/Manifest.cpp; then
echo -e "// Copyright (c) 2019 Dev Null Productions\n\n$(cat src/xrpld/rpc/handlers/server_info/Manifest.cpp)" > src/xrpld/rpc/handlers/server_info/Manifest.cpp
echo -e "// Copyright (c) 2019 Dev Null Productions\n\n$(cat src/xrpld/rpc/handlers/server_info/Manifest.cpp)" >src/xrpld/rpc/handlers/server_info/Manifest.cpp
fi
if ! grep -q 'Dev Null' src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp; then
echo -e "// Copyright (c) 2019 Dev Null Productions\n\n$(cat src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp)" > src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp
echo -e "// Copyright (c) 2019 Dev Null Productions\n\n$(cat src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp)" >src/xrpld/rpc/handlers/admin/status/ValidatorInfo.cpp
fi
if ! grep -q 'Bougalis' include/xrpl/basics/SlabAllocator.h; then
echo -e "// Copyright (c) 2022, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/SlabAllocator.h)" > include/xrpl/basics/SlabAllocator.h # cspell: ignore Nikolaos Bougalis nikb
echo -e "// Copyright (c) 2022, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/SlabAllocator.h)" >include/xrpl/basics/SlabAllocator.h # cspell: ignore Nikolaos Bougalis nikb
fi
if ! grep -q 'Bougalis' include/xrpl/basics/spinlock.h; then
echo -e "// Copyright (c) 2022, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/spinlock.h)" > include/xrpl/basics/spinlock.h # cspell: ignore Nikolaos Bougalis nikb
echo -e "// Copyright (c) 2022, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/spinlock.h)" >include/xrpl/basics/spinlock.h # cspell: ignore Nikolaos Bougalis nikb
fi
if ! grep -q 'Bougalis' include/xrpl/basics/tagged_integer.h; then
echo -e "// Copyright (c) 2014, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/tagged_integer.h)" > include/xrpl/basics/tagged_integer.h # cspell: ignore Nikolaos Bougalis nikb
echo -e "// Copyright (c) 2014, Nikolaos D. Bougalis <nikb@bougalis.net>\n\n$(cat include/xrpl/basics/tagged_integer.h)" >include/xrpl/basics/tagged_integer.h # cspell: ignore Nikolaos Bougalis nikb
fi
if ! grep -q 'Ritchford' include/xrpl/beast/utility/Zero.h; then
echo -e "// Copyright (c) 2014, Tom Ritchford <tom@swirly.com>\n\n$(cat include/xrpl/beast/utility/Zero.h)" > include/xrpl/beast/utility/Zero.h # cspell: ignore Ritchford
echo -e "// Copyright (c) 2014, Tom Ritchford <tom@swirly.com>\n\n$(cat include/xrpl/beast/utility/Zero.h)" >include/xrpl/beast/utility/Zero.h # cspell: ignore Ritchford
fi
# Restore newlines and tabs in string literals in the affected file.

View File

@@ -6,7 +6,7 @@ set -e
# On MacOS, ensure that GNU sed is installed and available as `gsed`.
SED_COMMAND=sed
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi

View File

@@ -6,7 +6,7 @@ set -e
# On MacOS, ensure that GNU sed is installed and available as `gsed`.
SED_COMMAND=sed
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi

View File

@@ -6,7 +6,7 @@ set -e
# On MacOS, ensure that GNU sed is installed and available as `gsed`.
SED_COMMAND=sed
if [[ "${OSTYPE}" == 'darwin'* ]]; then
if ! command -v gsed &> /dev/null; then
if ! command -v gsed &>/dev/null; then
echo "Error: gsed is not installed. Please install it using 'brew install gnu-sed'."
exit 1
fi

View File

@@ -6,14 +6,16 @@ on:
- develop
paths:
- ".github/workflows/build-nix-image.yml"
- "docker/nix.Dockerfile"
- ".github/workflows/reusable-build-docker-image.yml"
- "docker/**"
- "flake.nix"
- "flake.lock"
- "nix/**"
pull_request:
paths:
- ".github/workflows/build-nix-image.yml"
- "docker/nix.Dockerfile"
- ".github/workflows/reusable-build-docker-image.yml"
- "docker/**"
- "flake.nix"
- "flake.lock"
- "nix/**"
@@ -27,75 +29,81 @@ defaults:
run:
shell: bash
env:
UBUNTU_VERSION: "20.04"
RHEL_VERSION: "9"
DEBIAN_VERSION: "bookworm"
jobs:
build:
name: Build and push Nix image (${{ matrix.distro }})
name: Build ${{ matrix.distro.name }} (${{ matrix.target.platform }})
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
# The base images are the oldest supported version of each distro
# that we want to build images for.
distro:
- name: nixos
base_image: nixos/nix:latest
- name: ubuntu
base_image: ubuntu:20.04
- name: rhel
base_image: registry.access.redhat.com/ubi9/ubi:latest
- name: debian
base_image: debian:bookworm
target:
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
uses: ./.github/workflows/reusable-build-docker-image.yml
with:
image_name: ghcr.io/xrplf/xrpld/nix-${{ matrix.distro.name }}
dockerfile: docker/nix.Dockerfile
base_image: ${{ matrix.distro.base_image }}
platform: ${{ matrix.target.platform }}
runner: ${{ matrix.target.runner }}
push: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }}
merge:
name: Merge ${{ matrix.distro }} manifest
needs: build
if: ${{ github.repository == 'XRPLF/rippled' && github.event_name == 'push' }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- distro: nixos
- distro: ubuntu
- distro: rhel
- distro: debian
distro: [nixos, ubuntu, rhel, debian]
env:
IMAGE_NAME: ghcr.io/xrplf/xrpld/nix-${{ matrix.distro }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Determine base image
id: vars
run: |
case "${{ matrix.distro }}" in
nixos)
echo "base_image=nixos/nix:latest" >> $GITHUB_OUTPUT
;;
ubuntu)
echo "base_image=ubuntu:${UBUNTU_VERSION}" >> $GITHUB_OUTPUT
;;
rhel)
echo "base_image=registry.access.redhat.com/ubi${RHEL_VERSION}/ubi:latest" >> $GITHUB_OUTPUT
;;
debian)
echo "base_image=debian:${DEBIAN_VERSION}" >> $GITHUB_OUTPUT
;;
esac
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Docker metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=sha-,format=short
type=raw,value=latest
- name: Login to GitHub Container Registry
if: github.event_name == 'push'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ghcr.io/xrplf/ci/nix-${{ matrix.distro }}
tags: |
type=sha,prefix=sha-,format=short
type=raw,value=latest
- name: Create multi-arch manifests
run: |
for tag in $(jq -cr '.tags[]' <<<"$DOCKER_METADATA_OUTPUT_JSON"); do
docker buildx imagetools create -t "$tag" "${tag}-amd64" "${tag}-arm64"
done
- name: Build and push
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: docker/nix.Dockerfile
platforms: linux/amd64
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: BASE_IMAGE=${{ steps.vars.outputs.base_image }}
- name: Inspect image
run: |
docker buildx imagetools inspect "${IMAGE_NAME}:${{ steps.meta.outputs.version }}"

View File

@@ -5,8 +5,17 @@ on:
types:
- checks_requested
pull_request:
types: [opened, edited, reopened, synchronize, ready_for_review]
branches: [develop]
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
branches:
- develop
- "release-*"
- "release/*"
- "staging/*"
jobs:
check_description:
@@ -20,11 +29,11 @@ jobs:
env:
PR_BODY: ${{ github.event.pull_request.body }}
if: ${{ github.event_name == 'pull_request' }}
run: printenv PR_BODY > pr_body.md
run: printenv PR_BODY >pr_body.md
- name: Check PR description differs from template
if: ${{ github.event_name == 'pull_request' }}
run: >
python .github/scripts/check-pr-description.py
--template-file .github/pull_request_template.md
--pr-body-file pr_body.md
run: |
python .github/scripts/check-pr-description.py \
--template-file .github/pull_request_template.md \
--pr-body-file pr_body.md

View File

@@ -5,10 +5,19 @@ on:
types:
- checks_requested
pull_request:
types: [opened, edited, reopened, synchronize, ready_for_review]
branches: [develop]
types:
- opened
- edited
- reopened
- synchronize
- ready_for_review
branches:
- develop
- "release-*"
- "release/*"
- "staging/*"
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@cba1f0891650baf1a9c88624dc2d72573be2eb81

View File

@@ -98,7 +98,7 @@ jobs:
READY: ${{ contains(github.event.pull_request.labels.*.name, 'Ready to merge') }}
MERGE: ${{ github.event_name == 'merge_group' }}
run: |
echo "go=${{ (env.DRAFT != 'true' && env.READY == 'true') || env.FILES == 'true' || env.MERGE == 'true' }}" >> "${GITHUB_OUTPUT}"
echo "go=${{ (env.DRAFT != 'true' && env.READY == 'true') || env.FILES == 'true' || env.MERGE == 'true' }}" >>"${GITHUB_OUTPUT}"
cat "${GITHUB_OUTPUT}"
outputs:
go: ${{ steps.go.outputs.go == 'true' }}
@@ -168,9 +168,9 @@ jobs:
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
gh api --method POST -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \
/repos/xrplf/clio/dispatches -f "event_type=check_libxrpl" \
-F "client_payload[ref]=${{ needs.upload-recipe.outputs.recipe_ref }}" \
-F "client_payload[pr_url]=${PR_URL}"
/repos/xrplf/clio/dispatches -f "event_type=check_libxrpl" \
-F "client_payload[ref]=${{ needs.upload-recipe.outputs.recipe_ref }}" \
-F "client_payload[pr_url]=${PR_URL}"
passed:
if: failure() || cancelled()

View File

@@ -14,7 +14,7 @@ on:
jobs:
# Call the workflow in the XRPLF/actions repo that runs the pre-commit hooks.
run-hooks:
uses: XRPLF/actions/.github/workflows/pre-commit.yml@5e942d61bf32f7557a7c159cfac4712a687b3e3a
uses: XRPLF/actions/.github/workflows/pre-commit.yml@cba1f0891650baf1a9c88624dc2d72573be2eb81
with:
runs_on: ubuntu-latest
container: '{ "image": "ghcr.io/xrplf/ci/tools-rippled-pre-commit:sha-41ec7c1" }'

View File

@@ -0,0 +1,89 @@
# Build a single-platform Docker image. On push, the image is pushed to
# GHCR with arch-suffixed tags (e.g. `:latest-amd64`, `:sha-abc-amd64`)
# so the calling workflow can stitch per-arch builds into a multi-arch
# manifest without needing to pass digests around.
name: Reusable build Docker image (single platform)
on:
workflow_call:
inputs:
image_name:
description: "Full image name without tag (e.g. 'ghcr.io/xrplf/xrpld/nix-ubuntu')"
required: true
type: string
dockerfile:
description: "Path to the Dockerfile, relative to the repository root"
required: true
type: string
base_image:
description: "Value passed to the Dockerfile as the BASE_IMAGE build arg"
required: true
type: string
platform:
description: "Docker platform string, e.g. linux/amd64"
required: true
type: string
runner:
description: "GitHub Actions runner label to build on"
required: true
type: string
push:
description: "Whether to push the image to GHCR"
required: true
type: boolean
defaults:
run:
shell: bash
jobs:
build:
name: Build (${{ inputs.platform }})
runs-on: ${{ inputs.runner }}
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Determine arch
id: vars
env:
PLATFORM: ${{ inputs.platform }}
run: |
echo "arch=${PLATFORM##*/}" >>$GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Login to GitHub Container Registry
if: inputs.push
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: ${{ inputs.image_name }}
tags: |
type=sha,prefix=sha-,format=short
type=raw,value=latest
flavor: |
suffix=-${{ steps.vars.outputs.arch }},onlatest=true
- name: Build and push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: ${{ inputs.dockerfile }}
platforms: ${{ inputs.platform }}
push: ${{ inputs.push }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: BASE_IMAGE=${{ inputs.base_image }}

View File

@@ -113,7 +113,7 @@ jobs:
- name: Set ccache log file
if: ${{ inputs.ccache_enabled && runner.debug == '1' }}
run: echo "CCACHE_LOGFILE=${{ runner.temp }}/ccache.log" >> "${GITHUB_ENV}"
run: echo "CCACHE_LOGFILE=${{ runner.temp }}/ccache.log" >>"${GITHUB_ENV}"
- name: Print build environment
uses: XRPLF/actions/print-build-env@59dec886e4afb05a1724443af08baccbc045b574
@@ -146,11 +146,11 @@ jobs:
CMAKE_ARGS: ${{ inputs.cmake_args }}
run: |
cmake \
-G '${{ runner.os == 'Windows' && 'Visual Studio 17 2022' || 'Ninja' }}' \
-DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \
-DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \
${CMAKE_ARGS} \
..
-G '${{ runner.os == 'Windows' && 'Visual Studio 17 2022' || 'Ninja' }}' \
-DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \
-DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \
${CMAKE_ARGS} \
..
- name: Check protocol autogen files are up-to-date
working-directory: ${{ env.BUILD_DIR }}
@@ -172,32 +172,32 @@ jobs:
cmake --build . --target code_gen
DIFF=$(git -C .. status --porcelain -- include/xrpl/protocol_autogen src/tests/libxrpl/protocol_autogen)
if [ -n "${DIFF}" ]; then
echo "::error::Generated protocol files are out of date"
git -C .. diff -- include/xrpl/protocol_autogen src/tests/libxrpl/protocol_autogen
echo "${MESSAGE}"
exit 1
echo "::error::Generated protocol files are out of date"
git -C .. diff -- include/xrpl/protocol_autogen src/tests/libxrpl/protocol_autogen
echo "${MESSAGE}"
exit 1
fi
- name: Build the binary
working-directory: ${{ env.BUILD_DIR }}
env:
BUILD_NPROC: ${{ runner.os == 'Linux' && '16' || steps.nproc.outputs.nproc }}
BUILD_NPROC: ${{ steps.nproc.outputs.nproc }}
BUILD_TYPE: ${{ inputs.build_type }}
CMAKE_TARGET: ${{ inputs.cmake_target }}
run: |
cmake \
--build . \
--config "${BUILD_TYPE}" \
--parallel "${BUILD_NPROC}" \
--target "${CMAKE_TARGET}"
--build . \
--config "${BUILD_TYPE}" \
--parallel "${BUILD_NPROC}" \
--target "${CMAKE_TARGET}"
- name: Show ccache statistics
if: ${{ inputs.ccache_enabled }}
run: |
ccache --show-stats -vv
if [ '${{ runner.debug }}' = '1' ]; then
cat "${CCACHE_LOGFILE}"
curl ${CCACHE_REMOTE_STORAGE%|*}/status || true
cat "${CCACHE_LOGFILE}"
curl ${CCACHE_REMOTE_STORAGE%|*}/status || true
fi
- name: Upload the binary (Linux)
@@ -214,7 +214,7 @@ jobs:
working-directory: ${{ env.BUILD_DIR }}
run: |
set -o pipefail
./xrpld --definitions | python3 -m json.tool > server_definitions.json
./xrpld --definitions | python3 -m json.tool >server_definitions.json
- name: Upload server definitions
if: ${{ github.event.repository.visibility == 'public' && inputs.config_name == 'debian-bookworm-gcc-13-amd64-release' }}
@@ -231,10 +231,10 @@ jobs:
run: |
ldd ./xrpld
if [ "$(ldd ./xrpld | grep -E '(libstdc\+\+|libgcc)' | wc -l)" -eq 0 ]; then
echo 'The binary is statically linked.'
echo 'The binary is statically linked.'
else
echo 'The binary is dynamically linked.'
exit 1
echo 'The binary is dynamically linked.'
exit 1
fi
- name: Verify presence of instrumentation (Linux)
@@ -250,12 +250,12 @@ jobs:
run: |
ASAN_OPTS="include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-asan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/asan.supp"
if [[ "${CONFIG_NAME}" == *gcc* ]]; then
ASAN_OPTS="${ASAN_OPTS}:alloc_dealloc_mismatch=0"
ASAN_OPTS="${ASAN_OPTS}:alloc_dealloc_mismatch=0"
fi
echo "ASAN_OPTIONS=${ASAN_OPTS}" >> ${GITHUB_ENV}
echo "TSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-tsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/tsan.supp" >> ${GITHUB_ENV}
echo "UBSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-ubsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/ubsan.supp" >> ${GITHUB_ENV}
echo "LSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-lsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/lsan.supp" >> ${GITHUB_ENV}
echo "ASAN_OPTIONS=${ASAN_OPTS}" >>${GITHUB_ENV}
echo "TSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-tsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/tsan.supp" >>${GITHUB_ENV}
echo "UBSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-ubsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/ubsan.supp" >>${GITHUB_ENV}
echo "LSAN_OPTIONS=include=${GITHUB_WORKSPACE}/sanitizers/suppressions/runtime-lsan-options.txt:suppressions=${GITHUB_WORKSPACE}/sanitizers/suppressions/lsan.supp" >>${GITHUB_ENV}
- name: Run the separate tests
if: ${{ !inputs.build_only }}
@@ -266,9 +266,9 @@ jobs:
PARALLELISM: ${{ runner.os == 'Windows' && '1' || steps.nproc.outputs.nproc }}
run: |
ctest \
--output-on-failure \
-C "${BUILD_TYPE}" \
-j "${PARALLELISM}"
--output-on-failure \
-C "${BUILD_TYPE}" \
-j "${PARALLELISM}"
- name: Run the embedded tests
if: ${{ !inputs.build_only }}
@@ -278,7 +278,7 @@ jobs:
run: |
set -o pipefail
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness
[ "$COVERAGE_ENABLED" = "true" ] && BUILD_NPROC=$(( BUILD_NPROC - 2 ))
[ "$COVERAGE_ENABLED" = "true" ] && BUILD_NPROC=$((BUILD_NPROC - 2))
./xrpld --unittest --unittest-jobs "${BUILD_NPROC}" 2>&1 | tee unittest.log
- name: Show test failure summary
@@ -287,19 +287,19 @@ jobs:
WORKING_DIR: ${{ runner.os == 'Windows' && format('{0}\{1}', env.BUILD_DIR, inputs.build_type) || env.BUILD_DIR }}
run: |
if [ ! -d "${WORKING_DIR}" ]; then
echo "Working directory '${WORKING_DIR}' does not exist."
exit 0
echo "Working directory '${WORKING_DIR}' does not exist."
exit 0
fi
cd "${WORKING_DIR}"
if [ ! -f unittest.log ]; then
echo "unittest.log not found; embedded tests may not have run."
exit 0
echo "unittest.log not found; embedded tests may not have run."
exit 0
fi
if ! grep -E "failed" unittest.log; then
echo "Log present but no failure lines found in unittest.log."
echo "Log present but no failure lines found in unittest.log."
fi
- name: Debug failure (Linux)
if: ${{ failure() && runner.os == 'Linux' && !inputs.build_only }}
@@ -317,14 +317,14 @@ jobs:
BUILD_TYPE: ${{ inputs.build_type }}
run: |
cmake \
--build . \
--config "${BUILD_TYPE}" \
--parallel "${BUILD_NPROC}" \
--target coverage
--build . \
--config "${BUILD_TYPE}" \
--parallel "${BUILD_NPROC}" \
--target coverage
- name: Upload coverage report
if: ${{ github.repository == 'XRPLF/rippled' && !inputs.build_only && env.COVERAGE_ENABLED == 'true' }}
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
disable_search: true
disable_telem: true

View File

@@ -38,9 +38,9 @@ jobs:
run: |
DIFF=$(git status --porcelain)
if [ -n "${DIFF}" ]; then
# Print the differences to give the contributor a hint about what to
# expect when running levelization on their own machine.
git diff
echo "${MESSAGE}"
exit 1
# Print the differences to give the contributor a hint about what to
# expect when running levelization on their own machine.
git diff
echo "${MESSAGE}"
exit 1
fi

View File

@@ -48,9 +48,9 @@ jobs:
run: |
DIFF=$(git status --porcelain)
if [ -n "${DIFF}" ]; then
# Print the differences to give the contributor a hint about what to
# expect when running the renaming scripts on their own machine.
git diff
echo "${MESSAGE}"
exit 1
# Print the differences to give the contributor a hint about what to
# expect when running the renaming scripts on their own machine.
git diff
echo "${MESSAGE}"
exit 1
fi

View File

@@ -70,13 +70,13 @@ jobs:
working-directory: ${{ env.BUILD_DIR }}
run: |
cmake \
-G 'Ninja' \
-DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \
-DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \
-Dtests=ON \
-Dwerr=ON \
-Dxrpld=ON \
..
-G 'Ninja' \
-DCMAKE_TOOLCHAIN_FILE:FILEPATH=build/generators/conan_toolchain.cmake \
-DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \
-Dtests=ON \
-Dwerr=ON \
-Dxrpld=ON \
..
# clang-tidy needs headers generated from proto files
- name: Build libxrpl.libpb
@@ -133,7 +133,7 @@ jobs:
- name: Write issue header
if: ${{ steps.run_clang_tidy.outcome != 'success' }}
run: |
cat > "${ISSUE_FILE}" <<EOF
cat >"${ISSUE_FILE}" <<EOF
## Clang-tidy Check Failed
### Clang-tidy Output:
@@ -144,30 +144,30 @@ jobs:
if: ${{ steps.run_clang_tidy.outcome != 'success' }}
run: |
if [ -f "${OUTPUT_FILE}" ]; then
# Extract lines containing 'error:', 'warning:', or 'note:'
grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" > filtered-output.txt || true
# Extract lines containing 'error:', 'warning:', or 'note:'
grep -E '(error:|warning:|note:)' "${OUTPUT_FILE}" >filtered-output.txt || true
# If filtered output is empty, use original (might be a different error format)
if [ ! -s filtered-output.txt ]; then
cp "${OUTPUT_FILE}" filtered-output.txt
fi
# If filtered output is empty, use original (might be a different error format)
if [ ! -s filtered-output.txt ]; then
cp "${OUTPUT_FILE}" filtered-output.txt
fi
# Truncate if too large
head -c 60000 filtered-output.txt >> "${ISSUE_FILE}"
if [ "$(wc -c < filtered-output.txt)" -gt 60000 ]; then
echo "" >> "${ISSUE_FILE}"
echo "... (output truncated, see artifacts for full output)" >> "${ISSUE_FILE}"
fi
# Truncate if too large
head -c 60000 filtered-output.txt >>"${ISSUE_FILE}"
if [ "$(wc -c <filtered-output.txt)" -gt 60000 ]; then
echo "" >>"${ISSUE_FILE}"
echo "... (output truncated, see artifacts for full output)" >>"${ISSUE_FILE}"
fi
rm filtered-output.txt
rm filtered-output.txt
else
echo "No output file found" >> "${ISSUE_FILE}"
echo "No output file found" >>"${ISSUE_FILE}"
fi
- name: Append issue footer
if: ${{ steps.run_clang_tidy.outcome != 'success' }}
run: |
cat >> "${ISSUE_FILE}" <<EOF
cat >>"${ISSUE_FILE}" <<EOF
\`\`\`
---
@@ -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@2b8bc36af85b88bca0dd7bfac2e2dc05f94ad712
with:
title: "Clang-tidy check failed"
body_file: ${{ env.ISSUE_FILE }}

View File

@@ -39,7 +39,7 @@ jobs:
id: generate
working-directory: .github/scripts/strategy-matrix
run: |
./generate.py --packaging --config=linux.json >> "${GITHUB_OUTPUT}"
./generate.py --packaging --config=linux.json >>"${GITHUB_OUTPUT}"
generate-version:
runs-on: ubuntu-latest
@@ -58,6 +58,7 @@ jobs:
package:
needs: [generate-matrix, generate-version]
if: ${{ github.event.repository.visibility == 'public' }}
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
@@ -88,8 +89,7 @@ jobs:
run: ./package/build_pkg.sh
- name: Upload package artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: ${{ github.event.repository.visibility == 'public' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.artifact_name }}-pkg-${{ needs.generate-version.outputs.version }}
path: |

View File

@@ -42,4 +42,4 @@ jobs:
env:
GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}.json', inputs.os) || '' }}
GENERATE_OPTION: ${{ inputs.strategy_matrix == 'all' && '--all' || '' }}
run: ./generate.py ${GENERATE_OPTION} ${GENERATE_CONFIG} >> "${GITHUB_OUTPUT}"
run: ./generate.py ${GENERATE_OPTION} ${GENERATE_CONFIG} >>"${GITHUB_OUTPUT}"

View File

@@ -37,37 +37,50 @@ repos:
exclude: ^include/xrpl/protocol_autogen/(transactions|ledger_entries)/
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: cd481d7b0bfb5c7b3090c21846317f9a8262e891 # frozen: v22.1.0
rev: dd18dad857d6133e90bbe478f4f2f22ec0030269 # frozen: v22.1.5
hooks:
- id: clang-format
args: [--style=file]
"types_or": [c++, c, proto]
exclude: ^include/xrpl/protocol_autogen/(transactions|ledger_entries)/
- repo: https://github.com/BlankSpruce/gersemi
rev: 0.26.0
- repo: https://github.com/BlankSpruce/gersemi-pre-commit
rev: faadd6a9d852369ca94f4d15b2404c967ba8cb01 # frozen: 0.27.6
hooks:
- id: gersemi
- repo: https://github.com/rbubley/mirrors-prettier
rev: c2bc67fe8f8f549cc489e00ba8b45aa18ee713b1 # frozen: v3.8.1
rev: 515f543f5718ebfd6ce22e16708bb32c68ff96e1 # frozen: v3.8.3
hooks:
- id: prettier
args: [--end-of-line=auto]
- repo: https://github.com/psf/black-pre-commit-mirror
rev: ea488cebbfd88a5f50b8bd95d5c829d0bb76feb8 # frozen: 26.1.0
rev: 4160603246a6b365d4a2af661c6d71b0a0f50478 # frozen: 26.5.1
hooks:
- id: black
- repo: https://github.com/openstack/bashate
rev: 5798d24d571676fc407e81df574c1ef57b520f23 # frozen: 2.1.1
- repo: https://github.com/scop/pre-commit-shfmt
rev: 05c1426671b9237fb5e1444dd63aa5731bec0dfb # frozen: v3.13.1-1
hooks:
- id: bashate
args: ["--ignore=E006"]
- id: shfmt
args: [--write, --indent=4, --case-indent=true]
- repo: local
hooks:
- id: format-inline-bash-workflows
name: "format `run:` blocks in workflows/actions"
entry: ./.github/scripts/format-inline-bash.py
language: python
files: ^\.github/(workflows|actions)/.*\.ya?ml$
- id: format-inline-bash-markdown
name: "format ```bash blocks in markdown"
entry: ./.github/scripts/format-inline-bash.py
language: python
files: \.md$
- repo: https://github.com/streetsidesoftware/cspell-cli
rev: a42085ade523f591dca134379a595e7859986445 # frozen: v9.7.0
rev: 4643f154907327ee0a2c7038f0296e0dd77d9776 # frozen: v10.0.0
hooks:
- id: cspell # Spell check changed files
exclude: |

View File

@@ -151,8 +151,8 @@ git init
git remote add origin git@github.com:XRPLF/conan-center-index.git
git sparse-checkout init
for recipe in "${recipes[@]}"; do
echo "Checking out recipe '${recipe}'..."
git sparse-checkout add recipes/${recipe}
echo "Checking out recipe '${recipe}'..."
git sparse-checkout add recipes/${recipe}
done
git fetch origin master
git checkout master
@@ -180,7 +180,7 @@ the new recipe will be automatically pulled from the official Conan Center.
If you see an error similar to the following after running `conan profile show`:
```bash
```text
ERROR: Invalid setting '17' is not a valid 'settings.compiler.version' value.
Possible values are ['5.0', '5.1', '6.0', '6.1', '7.0', '7.3', '8.0', '8.1',
'9.0', '9.1', '10.0', '11.0', '12.0', '13', '13.0', '13.1', '14', '14.0', '15',
@@ -427,16 +427,19 @@ install ccache --version 4.11.3 --allow-downgrade`.
Single-config generators:
```
cmake --build .
cmake --build . --parallel N
```
Multi-config generators:
```
cmake --build . --config Release
cmake --build . --config Debug
cmake --build . --config Release --parallel N
cmake --build . --config Debug --parallel N
```
Replace the `--parallel` parameter N with the desired number of parallel jobs. A common starting point is half of the number of available CPU
cores.
5. Test xrpld.
Single-config generators:

View File

@@ -1,8 +1,8 @@
#!/bin/bash
if [[ $# -ne 1 || "$1" == "--help" || "$1" == "-h" ]]; then
name=$( basename $0 )
cat <<- USAGE
name=$(basename $0)
cat <<-USAGE
Usage: $name <username>
Where <username> is the Github username of the upstream repo. e.g. XRPLF
@@ -14,7 +14,7 @@ fi
shift
user="$1"
# Get the origin URL. Expect it be an SSH-style URL
origin=$( git remote get-url origin )
origin=$(git remote get-url origin)
if [[ "${origin}" == "" ]]; then
echo Invalid origin remote >&2
exit 1
@@ -22,11 +22,11 @@ fi
# echo "Origin: ${origin}"
# Parse the origin
ifs_orig="${IFS}"
IFS=':' read remote originpath <<< "${origin}"
IFS=':' read remote originpath <<<"${origin}"
# echo "Remote: ${remote}, Originpath: ${originpath}"
IFS='@' read sshuser server <<< "${remote}"
IFS='@' read sshuser server <<<"${remote}"
# echo "SSHUser: ${sshuser}, Server: ${server}"
IFS='/' read originuser repo <<< "${originpath}"
IFS='/' read originuser repo <<<"${originpath}"
# echo "Originuser: ${originuser}, Repo: ${repo}"
if [[ "${sshuser}" == "" || "${server}" == "" || "${originuser}" == "" || "${repo}" == "" ]]; then
echo "Can't parse origin URL: ${origin}" >&2
@@ -35,9 +35,9 @@ fi
upstream="https://${server}/${user}/${repo}"
upstreampush="${remote}:${user}/${repo}"
upstreamgroup="upstream upstream-push"
current=$( git remote get-url upstream 2>/dev/null )
currentpush=$( git remote get-url upstream-push 2>/dev/null )
currentgroup=$( git config remotes.upstreams )
current=$(git remote get-url upstream 2>/dev/null)
currentpush=$(git remote get-url upstream-push 2>/dev/null)
currentgroup=$(git config remotes.upstreams)
if [[ "${current}" == "${upstream}" ]]; then
echo "Upstream already set up correctly. Skip"
elif [[ -n "${current}" && "${current}" != "${upstream}" && "${current}" != "${upstreampush}" ]]; then
@@ -45,9 +45,9 @@ elif [[ -n "${current}" && "${current}" != "${upstream}" && "${current}" != "${u
else
if [[ "${current}" == "${upstreampush}" ]]; then
echo "Upstream set to dangerous push URL. Update."
_run git remote rename upstream upstream-push || \
_run git remote remove upstream
currentpush=$( git remote get-url upstream-push 2>/dev/null )
_run git remote rename upstream upstream-push ||
_run git remote remove upstream
currentpush=$(git remote get-url upstream-push 2>/dev/null)
fi
_run git remote add upstream "${upstream}"
fi

View File

@@ -1,8 +1,8 @@
#!/bin/bash
if [[ $# -lt 3 || "$1" == "--help" || "$1" = "-h" ]]; then
name=$( basename $0 )
cat <<- USAGE
name=$(basename $0)
cat <<-USAGE
Usage: $name workbranch base/branch user/branch [user/branch [...]]
* workbranch will be created locally from base/branch
@@ -16,7 +16,7 @@ fi
work="$1"
shift
branches=( $( echo "${@}" | sed "s/:/\//" ) )
branches=($(echo "${@}" | sed "s/:/\//"))
base="${branches[0]}"
unset branches[0]
@@ -24,10 +24,10 @@ set -e
users=()
for b in "${branches[@]}"; do
users+=( $( echo $b | cut -d/ -f1 ) )
users+=($(echo $b | cut -d/ -f1))
done
users=( $( printf '%s\n' "${users[@]}" | sort -u ) )
users=($(printf '%s\n' "${users[@]}" | sort -u))
git fetch --multiple upstreams "${users[@]}"
git checkout -B "$work" --no-track "$base"
@@ -40,7 +40,7 @@ done
# Make sure the commits look right
git log --show-signature "$base..HEAD"
parts=( $( echo $base | sed "s/\// /" ) )
parts=($(echo $base | sed "s/\// /"))
repo="${parts[0]}"
b="${parts[1]}"
push=$repo
@@ -50,7 +50,7 @@ fi
if [[ "$repo" == "upstream" ]]; then
repo="upstreams"
fi
cat << PUSH
cat <<PUSH
-------------------------------------------------------------------
This script will not push. Verify everything is correct, then push

View File

@@ -1,8 +1,8 @@
#!/bin/bash
if [[ $# -ne 3 || "$1" == "--help" || "$1" = "-h" ]]; then
name=$( basename $0 )
cat <<- USAGE
name=$(basename $0)
cat <<-USAGE
Usage: $name workbranch base/branch version
* workbranch will be created locally from base/branch. If it exists,
@@ -16,7 +16,7 @@ fi
work="$1"
shift
base=$( echo "$1" | sed "s/:/\//" )
base=$(echo "$1" | sed "s/:/\//")
shift
version=$1
@@ -28,16 +28,16 @@ git fetch upstreams
git checkout -B "${work}" --no-track "${base}"
push=$( git rev-parse --abbrev-ref --symbolic-full-name '@{push}' \
2>/dev/null ) || true
push=$(git rev-parse --abbrev-ref --symbolic-full-name '@{push}' \
2>/dev/null) || true
if [[ "${push}" != "" ]]; then
echo "Warning: ${push} may already exist."
fi
build=$( find -name BuildInfo.cpp )
sed 's/\(^.*versionString =\).*$/\1 "'${version}'"/' ${build} > version.cpp && \
diff "${build}" version.cpp && exit 1 || \
mv -vi version.cpp ${build}
build=$(find -name BuildInfo.cpp)
sed 's/\(^.*versionString =\).*$/\1 "'${version}'"/' ${build} >version.cpp &&
diff "${build}" version.cpp && exit 1 ||
mv -vi version.cpp ${build}
git diff
@@ -47,7 +47,7 @@ git commit -S -m "Set version to ${version}"
git log --oneline --first-parent ${base}^..
cat << PUSH
cat <<PUSH
-------------------------------------------------------------------
This script will not push. Verify everything is correct, then push

View File

@@ -168,7 +168,13 @@ def main():
if not os.environ.get("TIDY"):
return 0
repo_root = Path(__file__).parent.parent
repo_root = Path(
subprocess.check_output(
["git", "rev-parse", "--show-toplevel"],
cwd=Path(__file__).parent,
text=True,
).strip()
)
files = staged_files(repo_root)
if not files:
return 0

View File

@@ -1466,10 +1466,7 @@ admin = 127.0.0.1
protocol = http
[port_peer]
# Many servers still use the legacy port of 51235, so for backward-compatibility
# we maintain that port number here. However, for new servers we recommend
# changing this to the default port of 2459.
port = 51235
port = 2459
ip = 0.0.0.0
# alternatively, to accept connections on IPv4 + IPv6, use:
#ip = ::

View File

@@ -0,0 +1,13 @@
# Python dependencies for XRP Ledger code generation scripts
#
# These packages are required to run the code generation scripts that
# parse macro files and generate C++ wrapper classes.
# C preprocessor for Python - used to preprocess macro files
pcpp>=1.30
# Parser combinator library - used to parse the macro DSL
pyparsing>=3.0.0
# Template engine - used to generate C++ code from templates
Mako>=1.2.2

View File

@@ -1,13 +1,105 @@
# Python dependencies for XRP Ledger code generation scripts
#
# These packages are required to run the code generation scripts that
# parse macro files and generate C++ wrapper classes.
# C preprocessor for Python - used to preprocess macro files
pcpp>=1.30
# Parser combinator library - used to parse the macro DSL
pyparsing>=3.0.0
# Template engine - used to generate C++ code from templates
Mako>=1.2.2
# This file was autogenerated by uv via the following command:
# uv pip compile requirements.in --generate-hashes --output-file requirements.txt
mako==1.3.12 \
--hash=sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9 \
--hash=sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a
# via -r requirements.in
markupsafe==3.0.3 \
--hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \
--hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \
--hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \
--hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \
--hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \
--hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \
--hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \
--hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \
--hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \
--hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \
--hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \
--hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \
--hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \
--hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \
--hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \
--hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \
--hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \
--hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \
--hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \
--hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \
--hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \
--hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \
--hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \
--hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \
--hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \
--hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \
--hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \
--hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \
--hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \
--hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \
--hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \
--hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \
--hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \
--hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \
--hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \
--hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \
--hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \
--hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \
--hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \
--hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \
--hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \
--hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \
--hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \
--hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \
--hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \
--hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \
--hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \
--hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \
--hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \
--hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \
--hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \
--hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \
--hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \
--hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \
--hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \
--hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \
--hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \
--hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \
--hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \
--hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \
--hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \
--hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \
--hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \
--hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \
--hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \
--hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \
--hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \
--hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \
--hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \
--hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \
--hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \
--hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \
--hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \
--hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \
--hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \
--hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \
--hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \
--hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \
--hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \
--hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \
--hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \
--hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \
--hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \
--hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \
--hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \
--hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \
--hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \
--hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \
--hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50
# via mako
pcpp==1.30 \
--hash=sha256:05fe08292b6da57f385001c891a87f40d6aa7f46787b03e8ba326d20a3297c6e \
--hash=sha256:5af9fbce55f136d7931ae915fae03c34030a3b36c496e72d9636cedc8e2543a1
# via -r requirements.in
pyparsing==3.3.2 \
--hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
--hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
# via -r requirements.in

View File

@@ -93,6 +93,7 @@ words:
- daria
- dcmake
- dearmor
- dedented
- deleteme
- demultiplexer
- deserializaton
@@ -199,11 +200,13 @@ words:
- nonxrp
- noreplace
- noripple
- nostdinc
- notifempty
- nudb
- nullptr
- nunl
- Nyffenegger
- onlatest
- ostr
- pargs
- partitioner
@@ -254,6 +257,7 @@ words:
- sfields
- shamap
- shamapitem
- shfmt
- shlibs
- sidechain
- SIGGOOD
@@ -298,6 +302,7 @@ words:
- unauthorizing
- unergonomic
- unfetched
- unfindable
- unflatten
- unfund
- unimpair

48
docker/check-sanitizers.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/bin/bash
# Sanity-check that the sanitizer runtimes shipped with g++/clang++ work
# end-to-end against the system loader: compile each example with both
# compilers, run it, and confirm the expected diagnostic is emitted.
set -eo pipefail
cpp_files_dir="${1:?usage: $0 <cpp_files_dir>}"
case "$(uname -m)" in
x86_64) loader=/lib64/ld-linux-x86-64.so.2 ;;
aarch64) loader=/lib/ld-linux-aarch64.so.1 ;;
*)
echo "Unsupported arch: $(uname -m)" >&2
exit 1
;;
esac
declare -A sanitize=(
[asan]="-fsanitize=address"
[tsan]="-fsanitize=thread"
[ubsan]="-fsanitize=undefined"
)
declare -A expect=(
[asan]="heap-use-after-free"
[tsan]="data race"
[ubsan]="signed integer overflow"
)
for compiler in g++ clang++; do
for name in asan tsan ubsan; do
bin="/tmp/${name}-${compiler}"
echo "=== Build ${name} with ${compiler} ==="
"$compiler" -std=c++20 -O1 -g ${sanitize[$name]} \
-Wl,--dynamic-linker=$loader \
"${cpp_files_dir}/${name}.cpp" -o "$bin"
echo "=== Run ${name}-${compiler} ==="
output=$("$bin" 2>&1) || true
echo "$output"
echo "$output" | grep -q "${expect[$name]}" ||
{
echo "expected '${expect[$name]}' from $bin"
exit 1
}
rm -f "$bin"
done
done

28
docker/cpp_files/asan.cpp Normal file
View File

@@ -0,0 +1,28 @@
#include <atomic>
#include <cstddef>
#include <iostream>
#if defined(__clang__) || defined(__GNUC__)
__attribute__((noinline))
#elif defined(_MSC_VER)
__declspec(noinline)
#endif
int
read_after_free(volatile int* array, std::size_t index)
{
std::atomic_signal_fence(std::memory_order_seq_cst);
int value = array[index];
std::atomic_signal_fence(std::memory_order_seq_cst);
return value;
}
int
main()
{
int* array = new int[5]{10, 20, 30, 40, 50};
delete[] array;
std::cout << "Value at index 2: " << read_after_free(array, 2) << std::endl;
return 0;
}

26
docker/cpp_files/tsan.cpp Normal file
View File

@@ -0,0 +1,26 @@
#include <iostream>
#include <thread>
static int kCounter = 0;
void
increment()
{
for (int i = 0; i < 100'000; ++i)
{
++kCounter;
}
}
int
main()
{
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter value: " << kCounter << std::endl;
return 0;
}

View File

@@ -0,0 +1,13 @@
#include <iostream>
#include <limits>
int
main()
{
int maxInt = std::numeric_limits<int>::max();
int volatile one = 1;
std::cout << "Current max: " << maxInt << std::endl;
int overflowed = maxInt + one;
std::cout << "Overflowed result: " << overflowed << std::endl;
return 0;
}

View File

@@ -45,8 +45,30 @@ COPY --from=builder /tmp/build/result /nix/ci-env
ENV PATH="/nix/ci-env/bin:$PATH"
# Externally-built dynamically-linked ELF binaries hard-code the loader path
# (e.g. /lib64/ld-linux-x86-64.so.2) in their PT_INTERP header. Copy the
# loader from the Nix store to that path when the base image doesn't already
# provide one (i.e. on nixos/nix).
RUN <<EOF
case "$(uname -m)" in
x86_64) target=/lib64/ld-linux-x86-64.so.2 ;;
aarch64) target=/lib/ld-linux-aarch64.so.1 ;;
*) echo "Unsupported arch: $(uname -m)" >&2; exit 1 ;;
esac
if [ ! -e "$target" ]; then
# Use the loader from the same glibc that gcc links libc against, so
# ld-linux and libc/libpthread share GLIBC_PRIVATE symbols at runtime.
src="$(dirname "$(gcc -print-file-name=libc.so.6)")/$(basename "$target")"
[ -e "$src" ] || { echo "ld-linux not found at $src" >&2; exit 1; }
mkdir -p "$(dirname "$target")"
cp "$src" "$target"
fi
EOF
RUN <<EOF
ccache --version
clang --version
clang++ --version
clang-format --version
cmake --version
conan --version
@@ -64,3 +86,10 @@ python3 --version
run-clang-tidy --help
vim --version
EOF
# Sanity-check that the sanitizer runtimes shipped with g++/clang++ work
# end-to-end against the system loader.
COPY docker/cpp_files/ /tmp/cpp_files/
COPY docker/check-sanitizers.sh /tmp/check-sanitizers.sh
RUN grep -qi ubuntu /etc/os-release 2>/dev/null && /tmp/check-sanitizers.sh /tmp/cpp_files || true

4
flake.lock generated
View File

@@ -15,7 +15,7 @@
"type": "indirect"
}
},
"nixpkgs-glibc231": {
"nixpkgs-custom-glibc": {
"flake": false,
"locked": {
"lastModified": 1593520194,
@@ -35,7 +35,7 @@
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"nixpkgs-glibc231": "nixpkgs-glibc231"
"nixpkgs-custom-glibc": "nixpkgs-custom-glibc"
}
}
},

View File

@@ -6,16 +6,16 @@
# version — matches the system libc on Ubuntu 20.04 LTS. Imported
# manually (flake = false) because this revision predates nixpkgs'
# own flake.nix.
nixpkgs-glibc231 = {
nixpkgs-custom-glibc = {
url = "github:NixOS/nixpkgs/9cd98386a38891d1074fc18036b842dc4416f562";
flake = false;
};
};
outputs =
{ nixpkgs, nixpkgs-glibc231, ... }:
{ nixpkgs, nixpkgs-custom-glibc, ... }:
let
forEachSystem = import ./nix/utils.nix { inherit nixpkgs nixpkgs-glibc231; };
forEachSystem = import ./nix/utils.nix { inherit nixpkgs nixpkgs-custom-glibc; };
in
{
devShells = forEachSystem (import ./nix/devshell.nix);

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

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

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

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

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

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

@@ -7,7 +7,9 @@
#include <limits>
#include <optional>
#include <ostream>
#include <set>
#include <string>
#include <unordered_map>
namespace xrpl {
@@ -44,11 +46,11 @@ isPowerOfTen(T value)
* * min is a power of 10, and
* * max = min * 10 - 1.
*
* The mantissa_scale enum indicates whether the range is "small" or "large".
* This intentionally restricts the number of MantissaRanges that can be
* instantiated to two: one for each scale.
* The MantissaScale enum indicates properties of the range: size, and some behavioral
* options. This intentionally restricts the number of unique MantissaRanges that can
* be instantiated: one for each scale.
*
* The "small" scale is based on the behavior of STAmount for IOUs. It has a min
* The "Small" scale is based on the behavior of STAmount for IOUs. It has a min
* value of 10^15, and a max value of 10^16-1. This was sufficient for
* uses before Lending Protocol was implemented, mostly related to AMM.
*
@@ -59,29 +61,54 @@ isPowerOfTen(T value)
* STNumber field type, and for internal calculations. That necessitated the
* "large" scale.
*
* The "large" scale is intended to represent all values that can be represented
* The "Large" scales are intended to represent all values that can be represented
* by an STAmount - IOUs, XRP, and MPTs. It has a min value of 10^18, and a max
* value of 10^19-1.
* value of 10^19-1. "LargeLegacy" is like "Large", but preserves
* a rounding error when a computation results in a mantissa of
* Number::kMaxRep that needs to be rounded up, but rounds down
* instead. It will maintain consistent behavior until the fixCleanup3_2_0
* amendment is enabled.
*
* Note that if the mentioned amendments are eventually retired, this class
* should be left in place, but the "small" scale option should be removed. This
* should be left in place, but the "Small" scale option should be removed. This
* will allow for future expansion beyond 64-bits if it is ever needed.
*/
struct MantissaRange
struct MantissaRange final
{
using rep = std::uint64_t;
enum class MantissaScale { Small, Large };
enum class MantissaScale {
Small,
// LargeLegacy can be removed when fixCleanup3_2_0 is retired
LargeLegacy,
Large,
};
// This entire enum can be removed when fixCleanup3_2_0 is retired
enum class CuspRoundingFix : bool {
Disabled = false,
Enabled = true,
};
explicit constexpr MantissaRange(MantissaScale scale)
: min(getMin(scale)), log(logTen(min).value_or(-1)), scale(scale)
: min(getMin(scale))
, cuspRoundingFixEnabled(isCuspFixEnabled(scale))
, log(logTen(min).value_or(-1))
, scale(scale)
{
}
rep min;
rep max{(min * 10) - 1};
CuspRoundingFix cuspRoundingFixEnabled;
int log;
MantissaScale scale;
static MantissaRange const&
getMantissaRange(MantissaScale scale);
static std::set<MantissaScale> const&
getAllScales();
private:
static constexpr rep
getMin(MantissaScale scale)
@@ -90,15 +117,35 @@ private:
{
case MantissaScale::Small:
return 1'000'000'000'000'000ULL;
case MantissaScale::LargeLegacy:
case MantissaScale::Large:
return 1'000'000'000'000'000'000ULL;
default:
// Since this can never be called outside a non-constexpr
// context, this throw assures that the build fails if an
// If called in a constexpr context, this throw assures that the build fails if an
// invalid scale is used.
throw std::runtime_error("Unknown mantissa scale");
throw std::runtime_error("Unknown mantissa scale"); // LCOV_EXCL_LINE
}
}
static constexpr CuspRoundingFix
isCuspFixEnabled(MantissaScale scale)
{
switch (scale)
{
case MantissaScale::Small:
case MantissaScale::LargeLegacy:
return CuspRoundingFix::Disabled;
case MantissaScale::Large:
return CuspRoundingFix::Enabled;
default:
// If called in a constexpr context, this throw assures that the build fails if an
// invalid scale is used.
throw std::runtime_error("Unknown mantissa scale"); // LCOV_EXCL_LINE
}
}
static std::unordered_map<MantissaScale, MantissaRange> const&
getRanges();
};
// Like std::integral, but only 64-bit integral types.
@@ -203,7 +250,7 @@ concept Integral64 = std::is_same_v<T, std::int64_t> || std::is_same_v<T, std::u
* amendments are enabled to determine which result to expect.
*
*/
class Number
class Number final
{
using rep = std::int64_t;
using internalrep = MantissaRange::rep;
@@ -424,49 +471,29 @@ public:
return kRange.get().log;
}
/// oneSmall is needed because the ranges are private
static constexpr Number
oneSmall();
/// oneLarge is needed because the ranges are private
static constexpr Number
oneLarge();
// And one is needed because it needs to choose between oneSmall and
// oneLarge based on the current range
static Number
one();
template <Integral64 T>
template <
auto MinMantissa,
auto MaxMantissa,
Integral64 T = std::decay_t<decltype(MinMantissa)>,
Integral64 TMax = std::decay_t<decltype(MaxMantissa)>>
[[nodiscard]]
std::pair<T, int>
normalizeToRange(T minMantissa, T maxMantissa) const;
normalizeToRange() const;
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);
// 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;
void
normalize();
normalize(MantissaRange const& range);
/** Normalize Number components to an arbitrary range.
*
@@ -481,7 +508,8 @@ private:
T& mantissa,
int& exponent,
internalrep const& minMantissa,
internalrep const& maxMantissa);
internalrep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled);
template <class T>
friend void
@@ -490,7 +518,8 @@ private:
T& mantissa,
int& exponent,
MantissaRange::rep const& minMantissa,
MantissaRange::rep const& maxMantissa);
MantissaRange::rep const& maxMantissa,
MantissaRange::CuspRoundingFix cuspRoundingFixEnabled);
[[nodiscard]] bool
isnormal() const noexcept;
@@ -526,7 +555,7 @@ static constexpr Number kNumZero{};
inline Number::Number(bool negative, internalrep mantissa, int exponent, Normalized)
: Number(negative, mantissa, exponent, Unchecked{})
{
normalize();
normalize(kRange);
}
inline Number::Number(internalrep mantissa, int exponent, Normalized)
@@ -696,10 +725,19 @@ Number::isnormal() const noexcept
kMinExponent <= exponent_ && exponent_ <= kMaxExponent);
}
template <Integral64 T>
template <auto MinMantissa, auto MaxMantissa, Integral64 T, Integral64 TMax>
std::pair<T, int>
Number::normalizeToRange(T minMantissa, T maxMantissa) const
Number::normalizeToRange() const
{
static_assert(std::is_same_v<T, std::uint64_t> || std::is_same_v<T, std::int64_t>);
static_assert(std::is_same_v<T, TMax>);
auto constexpr kMIN = static_cast<T>(MinMantissa);
auto constexpr kMAX = static_cast<T>(MaxMantissa);
static_assert(kMIN > 0);
static_assert(kMIN % 10 == 0);
static_assert(kMAX % 10 == 9);
static_assert((kMAX + 1) / 10 == kMIN);
bool negative = negative_;
internalrep mantissa = mantissa_;
int exponent = exponent_;
@@ -711,7 +749,10 @@ Number::normalizeToRange(T minMantissa, T maxMantissa) const
"xrpl::Number::normalizeToRange",
"Number is non-negative for unsigned range.");
}
Number::normalize(negative, mantissa, exponent, minMantissa, maxMantissa);
// Don't need to worry about the cuspRounding fix because rounding up will never take the
// mantissa over maxMantissa with a ones digit value other than 0. 0 can safely be truncated.
Number::normalize(
negative, mantissa, exponent, kMIN, kMAX, MantissaRange::CuspRoundingFix::Disabled);
auto const sign = negative ? -1 : 1;
return std::make_pair(static_cast<T>(sign * mantissa), exponent);
@@ -763,6 +804,8 @@ to_string(MantissaRange::MantissaScale const& scale)
{
case MantissaRange::MantissaScale::Small:
return "small";
case MantissaRange::MantissaScale::LargeLegacy:
return "largeLegacy";
case MantissaRange::MantissaScale::Large:
return "large";
default:

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

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

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

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