mirror of
https://github.com/XRPLF/rippled.git
synced 2026-03-10 06:42:24 +00:00
Compare commits
3 Commits
tapanito/i
...
a1q123456/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
058b38a488 | ||
|
|
f638cbae3e | ||
|
|
94737f399a |
@@ -1,98 +0,0 @@
|
||||
# Custom CMake command definitions for gersemi formatting.
|
||||
# These stubs teach gersemi the signatures of project-specific commands
|
||||
# so it can format their invocations correctly.
|
||||
|
||||
function(git_branch branch_val)
|
||||
endfunction()
|
||||
|
||||
function(isolate_headers target A B scope)
|
||||
endfunction()
|
||||
|
||||
function(create_symbolic_link target link)
|
||||
endfunction()
|
||||
|
||||
function(xrpl_add_test name)
|
||||
endfunction()
|
||||
|
||||
macro(exclude_from_default target_)
|
||||
endmacro()
|
||||
|
||||
macro(exclude_if_included target_)
|
||||
endmacro()
|
||||
|
||||
function(target_protobuf_sources target prefix)
|
||||
set(options APPEND_PATH DESCRIPTORS)
|
||||
set(oneValueArgs
|
||||
LANGUAGE
|
||||
OUT_VAR
|
||||
EXPORT_MACRO
|
||||
TARGET
|
||||
PROTOC_OUT_DIR
|
||||
PLUGIN
|
||||
PLUGIN_OPTIONS
|
||||
PROTOC_EXE
|
||||
)
|
||||
set(multiValueArgs
|
||||
PROTOS
|
||||
IMPORT_DIRS
|
||||
GENERATE_EXTENSIONS
|
||||
PROTOC_OPTIONS
|
||||
DEPENDENCIES
|
||||
)
|
||||
cmake_parse_arguments(
|
||||
THIS_FUNCTION_PREFIX
|
||||
"${options}"
|
||||
"${oneValueArgs}"
|
||||
"${multiValueArgs}"
|
||||
${ARGN}
|
||||
)
|
||||
endfunction()
|
||||
|
||||
function(add_module parent name)
|
||||
endfunction()
|
||||
|
||||
function(target_link_modules parent scope)
|
||||
endfunction()
|
||||
|
||||
function(setup_target_for_coverage_gcovr)
|
||||
set(options NONE)
|
||||
set(oneValueArgs BASE_DIRECTORY NAME FORMAT)
|
||||
set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
|
||||
cmake_parse_arguments(
|
||||
THIS_FUNCTION_PREFIX
|
||||
"${options}"
|
||||
"${oneValueArgs}"
|
||||
"${multiValueArgs}"
|
||||
${ARGN}
|
||||
)
|
||||
endfunction()
|
||||
|
||||
function(add_code_coverage_to_target name scope)
|
||||
endfunction()
|
||||
|
||||
function(verbose_find_path variable name)
|
||||
set(options
|
||||
NO_CACHE
|
||||
REQUIRED
|
||||
OPTIONAL
|
||||
NO_DEFAULT_PATH
|
||||
NO_PACKAGE_ROOT_PATH
|
||||
NO_CMAKE_PATH
|
||||
NO_CMAKE_ENVIRONMENT_PATH
|
||||
NO_SYSTEM_ENVIRONMENT_PATH
|
||||
NO_CMAKE_SYSTEM_PATH
|
||||
NO_CMAKE_INSTALL_PREFIX
|
||||
CMAKE_FIND_ROOT_PATH_BOTH
|
||||
ONLY_CMAKE_FIND_ROOT_PATH
|
||||
NO_CMAKE_FIND_ROOT_PATH
|
||||
)
|
||||
set(oneValueArgs REGISTRY_VIEW VALIDATOR DOC)
|
||||
set(multiValueArgs NAMES HINTS PATHS PATH_SUFFIXES)
|
||||
cmake_parse_arguments(
|
||||
THIS_FUNCTION_PREFIX
|
||||
"${options}"
|
||||
"${oneValueArgs}"
|
||||
"${multiValueArgs}"
|
||||
${ARGN}
|
||||
)
|
||||
endfunction()
|
||||
@@ -1 +0,0 @@
|
||||
definitions: [.gersemi]
|
||||
6
.github/scripts/levelization/README.md
vendored
6
.github/scripts/levelization/README.md
vendored
@@ -70,7 +70,7 @@ that `test` code should _never_ be included in `xrpl` or `xrpld` code.)
|
||||
|
||||
## Validation
|
||||
|
||||
The [levelization](generate.sh) script takes no parameters,
|
||||
The [levelization](generate.py) script takes no parameters,
|
||||
reads no environment variables, and can be run from any directory,
|
||||
as long as it is in the expected location in the rippled repo.
|
||||
It can be run at any time from within a checked out repo, and will
|
||||
@@ -104,7 +104,7 @@ It generates many files of [results](results):
|
||||
Github Actions workflow to test that levelization loops haven't
|
||||
changed. Unfortunately, if changes are detected, it can't tell if
|
||||
they are improvements or not, so if you have resolved any issues or
|
||||
done anything else to improve levelization, run `levelization.sh`,
|
||||
done anything else to improve levelization, run `generate.py`,
|
||||
and commit the updated results.
|
||||
|
||||
The `loops.txt` and `ordering.txt` files relate the modules
|
||||
@@ -128,7 +128,7 @@ The committed files hide the detailed values intentionally, to
|
||||
prevent false alarms and merging issues, and because it's easy to
|
||||
get those details locally.
|
||||
|
||||
1. Run `levelization.sh`
|
||||
1. Run `generate.py`
|
||||
2. Grep the modules in `paths.txt`.
|
||||
- For example, if a cycle is found `A ~= B`, simply `grep -w
|
||||
A .github/scripts/levelization/results/paths.txt | grep -w B`
|
||||
|
||||
369
.github/scripts/levelization/generate.py
vendored
Normal file
369
.github/scripts/levelization/generate.py
vendored
Normal file
@@ -0,0 +1,369 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Usage: generate.py
|
||||
This script takes no parameters, and can be run from any directory,
|
||||
as long as it is in the expected.
|
||||
location in the repo.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Set, Optional
|
||||
|
||||
|
||||
# Compile regex patterns once at module level
|
||||
INCLUDE_PATTERN = re.compile(r"^\s*#include.*/.*\.h")
|
||||
INCLUDE_PATH_PATTERN = re.compile(r'[<"]([^>"]+)[>"]')
|
||||
|
||||
|
||||
def dictionary_sort_key(s: str) -> str:
|
||||
"""
|
||||
Create a sort key that mimics 'sort -d' (dictionary order).
|
||||
Dictionary order only considers blanks and alphanumeric characters.
|
||||
This means punctuation like '.' is ignored during sorting.
|
||||
"""
|
||||
# Keep only alphanumeric characters and spaces
|
||||
return "".join(c for c in s if c.isalnum() or c.isspace())
|
||||
|
||||
|
||||
def get_level(file_path: str) -> str:
|
||||
"""
|
||||
Extract the level from a file path (second and third directory components).
|
||||
Equivalent to bash: cut -d/ -f 2,3
|
||||
|
||||
Examples:
|
||||
src/xrpld/app/main.cpp -> xrpld.app
|
||||
src/libxrpl/protocol/STObject.cpp -> libxrpl.protocol
|
||||
include/xrpl/basics/base_uint.h -> xrpl.basics
|
||||
"""
|
||||
parts = file_path.split("/")
|
||||
|
||||
# Get fields 2 and 3 (indices 1 and 2 in 0-based indexing)
|
||||
if len(parts) >= 3:
|
||||
level = f"{parts[1]}/{parts[2]}"
|
||||
elif len(parts) >= 2:
|
||||
level = f"{parts[1]}/toplevel"
|
||||
else:
|
||||
level = file_path
|
||||
|
||||
# If the "level" indicates a file, cut off the filename
|
||||
if "." in level.split("/")[-1]: # Avoid Path object creation
|
||||
# Use the "toplevel" label as a workaround for `sort`
|
||||
# inconsistencies between different utility versions
|
||||
level = level.rsplit("/", 1)[0] + "/toplevel"
|
||||
|
||||
return level.replace("/", ".")
|
||||
|
||||
|
||||
def extract_include_level(include_line: str) -> Optional[str]:
|
||||
"""
|
||||
Extract the include path from an #include directive.
|
||||
Gets the first two directory components from the include path.
|
||||
Equivalent to bash: cut -d/ -f 1,2
|
||||
|
||||
Examples:
|
||||
#include <xrpl/basics/base_uint.h> -> xrpl.basics
|
||||
#include "xrpld/app/main/Application.h" -> xrpld.app
|
||||
"""
|
||||
# Remove everything before the quote or angle bracket
|
||||
match = INCLUDE_PATH_PATTERN.search(include_line)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
include_path = match.group(1)
|
||||
parts = include_path.split("/")
|
||||
|
||||
# Get first two fields (indices 0 and 1)
|
||||
if len(parts) >= 2:
|
||||
include_level = f"{parts[0]}/{parts[1]}"
|
||||
else:
|
||||
include_level = include_path
|
||||
|
||||
# If the "includelevel" indicates a file, cut off the filename
|
||||
if "." in include_level.split("/")[-1]: # Avoid Path object creation
|
||||
include_level = include_level.rsplit("/", 1)[0] + "/toplevel"
|
||||
|
||||
return include_level.replace("/", ".")
|
||||
|
||||
|
||||
def find_repo_root(start_path: Path, depth_limit: int = 10) -> Path:
|
||||
"""
|
||||
Find the repository root by looking for .git directory or src/include folders.
|
||||
Walks up the directory tree from the start path.
|
||||
"""
|
||||
current = start_path.resolve()
|
||||
|
||||
# Walk up the directory tree
|
||||
for _ in range(depth_limit): # Limit search depth to prevent infinite loops
|
||||
# Check if this directory has src or include folders
|
||||
has_src = (current / "src").exists()
|
||||
has_include = (current / "include").exists()
|
||||
|
||||
if has_src or has_include:
|
||||
return current
|
||||
|
||||
# Check if this is a git repository root
|
||||
if (current / ".git").exists():
|
||||
# Check if it has src or include nearby
|
||||
if has_src or has_include:
|
||||
return current
|
||||
|
||||
# Move up one level
|
||||
parent = current.parent
|
||||
if parent == current: # Reached filesystem root
|
||||
break
|
||||
current = parent
|
||||
|
||||
# If we couldn't find it, raise an error
|
||||
raise RuntimeError(
|
||||
"Could not find repository root. "
|
||||
"Expected to find a directory containing 'src' and/or 'include' folders."
|
||||
)
|
||||
|
||||
|
||||
def get_scan_directories(repo_root: Path) -> List[Path]:
|
||||
"""
|
||||
Get the list of directories to scan for include files.
|
||||
Returns paths that actually exist.
|
||||
"""
|
||||
directories = []
|
||||
|
||||
for dir_name in ["include", "src"]:
|
||||
dir_path = repo_root / dir_name
|
||||
if dir_path.exists() and dir_path.is_dir():
|
||||
directories.append(dir_path)
|
||||
|
||||
if not directories:
|
||||
raise RuntimeError(f"No 'src' or 'include' directories found in {repo_root}")
|
||||
|
||||
return directories
|
||||
|
||||
|
||||
def main():
|
||||
# Change to the script's directory
|
||||
script_dir = Path(__file__).parent.resolve()
|
||||
os.chdir(script_dir)
|
||||
|
||||
# If the shell is interactive, clean up any flotsam before analyzing
|
||||
# Match bash behavior: check if PS1 is set (indicates interactive shell)
|
||||
# When running a script, PS1 is not set even if stdin/stdout are TTYs
|
||||
if os.environ.get("PS1"):
|
||||
try:
|
||||
subprocess.run(["git", "clean", "-ix"], check=False, timeout=30)
|
||||
except (subprocess.TimeoutExpired, KeyboardInterrupt):
|
||||
print("Skipping git clean...")
|
||||
except Exception:
|
||||
# If git clean fails for any reason, just continue
|
||||
pass
|
||||
|
||||
# Clean up and create results directory
|
||||
results_dir = script_dir / "results"
|
||||
if results_dir.exists():
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(results_dir)
|
||||
results_dir.mkdir()
|
||||
|
||||
# Find the repository root by searching for src and include directories
|
||||
try:
|
||||
repo_root = find_repo_root(script_dir)
|
||||
scan_dirs = get_scan_directories(repo_root)
|
||||
|
||||
print(f"Found repository root: {repo_root}")
|
||||
print(f"Scanning directories:")
|
||||
for scan_dir in scan_dirs:
|
||||
print(f" - {scan_dir.relative_to(repo_root)}")
|
||||
except RuntimeError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print("\nScanning for raw includes...")
|
||||
# Find all #include directives
|
||||
raw_includes: List[Tuple[str, str]] = []
|
||||
rawincludes_file = results_dir / "rawincludes.txt"
|
||||
|
||||
# Write to file as we go to avoid storing everything in memory
|
||||
with open(rawincludes_file, "w", buffering=8192) as raw_f:
|
||||
for dir_path in scan_dirs:
|
||||
print(f" Scanning {dir_path.relative_to(repo_root)}...")
|
||||
|
||||
for file_path in dir_path.rglob("*"):
|
||||
if not file_path.is_file():
|
||||
continue
|
||||
|
||||
try:
|
||||
rel_path_str = str(file_path.relative_to(repo_root))
|
||||
|
||||
# Read file with larger buffer for better performance
|
||||
with open(
|
||||
file_path,
|
||||
"r",
|
||||
encoding="utf-8",
|
||||
errors="ignore",
|
||||
buffering=8192,
|
||||
) as f:
|
||||
for line in f:
|
||||
# Quick check before regex
|
||||
if "#include" not in line or "boost" in line:
|
||||
continue
|
||||
|
||||
if INCLUDE_PATTERN.match(line):
|
||||
line_stripped = line.strip()
|
||||
entry = f"{rel_path_str}:{line_stripped}\n"
|
||||
print(entry, end="")
|
||||
raw_f.write(entry)
|
||||
raw_includes.append((rel_path_str, line_stripped))
|
||||
except Exception as e:
|
||||
print(f"Error reading {file_path}: {e}", file=sys.stderr)
|
||||
|
||||
# Build levelization paths and count directly (no need to sort first)
|
||||
print("Build levelization paths")
|
||||
path_counts: Dict[Tuple[str, str], int] = defaultdict(int)
|
||||
|
||||
for file_path, include_line in raw_includes:
|
||||
level = get_level(file_path)
|
||||
include_level = extract_include_level(include_line)
|
||||
|
||||
if include_level and level != include_level:
|
||||
path_counts[(level, include_level)] += 1
|
||||
|
||||
# Sort and deduplicate paths (using dictionary order like bash 'sort -d')
|
||||
print("Sort and deduplicate paths")
|
||||
|
||||
paths_file = results_dir / "paths.txt"
|
||||
with open(paths_file, "w") as f:
|
||||
# Sort using dictionary order: only alphanumeric and spaces matter
|
||||
sorted_items = sorted(
|
||||
path_counts.items(),
|
||||
key=lambda x: (dictionary_sort_key(x[0][0]), dictionary_sort_key(x[0][1])),
|
||||
)
|
||||
for (level, include_level), count in sorted_items:
|
||||
line = f"{count:7} {level} {include_level}\n"
|
||||
print(line.rstrip())
|
||||
f.write(line)
|
||||
|
||||
# Split into flat-file database
|
||||
print("Split into flat-file database")
|
||||
includes_dir = results_dir / "includes"
|
||||
included_by_dir = results_dir / "included_by"
|
||||
includes_dir.mkdir()
|
||||
included_by_dir.mkdir()
|
||||
|
||||
# Batch writes by grouping data first to avoid repeated file opens
|
||||
includes_data: Dict[str, List[Tuple[str, int]]] = defaultdict(list)
|
||||
included_by_data: Dict[str, List[Tuple[str, int]]] = defaultdict(list)
|
||||
|
||||
# Process in sorted order to match bash script behavior (dictionary order)
|
||||
sorted_items = sorted(
|
||||
path_counts.items(),
|
||||
key=lambda x: (dictionary_sort_key(x[0][0]), dictionary_sort_key(x[0][1])),
|
||||
)
|
||||
for (level, include_level), count in sorted_items:
|
||||
includes_data[level].append((include_level, count))
|
||||
included_by_data[include_level].append((level, count))
|
||||
|
||||
# Write all includes files in sorted order (dictionary order)
|
||||
for level in sorted(includes_data.keys(), key=dictionary_sort_key):
|
||||
entries = includes_data[level]
|
||||
with open(includes_dir / level, "w") as f:
|
||||
for include_level, count in entries:
|
||||
line = f"{include_level} {count}\n"
|
||||
print(line.rstrip())
|
||||
f.write(line)
|
||||
|
||||
# Write all included_by files in sorted order (dictionary order)
|
||||
for include_level in sorted(included_by_data.keys(), key=dictionary_sort_key):
|
||||
entries = included_by_data[include_level]
|
||||
with open(included_by_dir / include_level, "w") as f:
|
||||
for level, count in entries:
|
||||
line = f"{level} {count}\n"
|
||||
print(line.rstrip())
|
||||
f.write(line)
|
||||
|
||||
# Search for loops
|
||||
print("Search for loops")
|
||||
loops_file = results_dir / "loops.txt"
|
||||
ordering_file = results_dir / "ordering.txt"
|
||||
|
||||
loops_found: Set[Tuple[str, str]] = set()
|
||||
|
||||
# Pre-load all include files into memory to avoid repeated I/O
|
||||
# This is the biggest optimization - we were reading files repeatedly in nested loops
|
||||
# Use list of tuples to preserve file order
|
||||
includes_cache: Dict[str, List[Tuple[str, int]]] = {}
|
||||
includes_lookup: Dict[str, Dict[str, int]] = {} # For fast lookup
|
||||
|
||||
# Note: bash script uses 'for source in *' which uses standard glob sorting,
|
||||
# NOT dictionary order. So we use standard sorted() here, not dictionary_sort_key.
|
||||
for include_file in sorted(includes_dir.iterdir(), key=lambda p: p.name):
|
||||
if not include_file.is_file():
|
||||
continue
|
||||
|
||||
includes_cache[include_file.name] = []
|
||||
includes_lookup[include_file.name] = {}
|
||||
with open(include_file, "r") as f:
|
||||
for line in f:
|
||||
parts = line.strip().split()
|
||||
if len(parts) >= 2:
|
||||
include_name = parts[0]
|
||||
include_count = int(parts[1])
|
||||
includes_cache[include_file.name].append(
|
||||
(include_name, include_count)
|
||||
)
|
||||
includes_lookup[include_file.name][include_name] = include_count
|
||||
|
||||
with open(loops_file, "w", buffering=8192) as loops_f, open(
|
||||
ordering_file, "w", buffering=8192
|
||||
) as ordering_f:
|
||||
|
||||
# Use standard sorting to match bash glob expansion 'for source in *'
|
||||
for source in sorted(includes_cache.keys()):
|
||||
source_includes = includes_cache[source]
|
||||
|
||||
for include, include_freq in source_includes:
|
||||
# Check if include file exists and references source
|
||||
if include not in includes_lookup:
|
||||
continue
|
||||
|
||||
source_freq = includes_lookup[include].get(source)
|
||||
|
||||
if source_freq is not None:
|
||||
# Found a loop
|
||||
loop_key = tuple(sorted([source, include]))
|
||||
if loop_key in loops_found:
|
||||
continue
|
||||
loops_found.add(loop_key)
|
||||
|
||||
loops_f.write(f"Loop: {source} {include}\n")
|
||||
|
||||
# If the counts are close, indicate that the two modules are
|
||||
# on the same level, though they shouldn't be
|
||||
diff = include_freq - source_freq
|
||||
if diff > 3:
|
||||
loops_f.write(f" {source} > {include}\n\n")
|
||||
elif diff < -3:
|
||||
loops_f.write(f" {include} > {source}\n\n")
|
||||
elif source_freq == include_freq:
|
||||
loops_f.write(f" {include} == {source}\n\n")
|
||||
else:
|
||||
loops_f.write(f" {include} ~= {source}\n\n")
|
||||
else:
|
||||
ordering_f.write(f"{source} > {include}\n")
|
||||
|
||||
# Print results
|
||||
print("\nOrdering:")
|
||||
with open(ordering_file, "r") as f:
|
||||
print(f.read(), end="")
|
||||
|
||||
print("\nLoops:")
|
||||
with open(loops_file, "r") as f:
|
||||
print(f.read(), end="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
130
.github/scripts/levelization/generate.sh
vendored
130
.github/scripts/levelization/generate.sh
vendored
@@ -1,130 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Usage: generate.sh
|
||||
# This script takes no parameters, reads no environment variables,
|
||||
# and can be run from any directory, as long as it is in the expected
|
||||
# location in the repo.
|
||||
|
||||
pushd $( dirname $0 )
|
||||
|
||||
if [ -v PS1 ]
|
||||
then
|
||||
# if the shell is interactive, clean up any flotsam before analyzing
|
||||
git clean -ix
|
||||
fi
|
||||
|
||||
# Ensure all sorting is ASCII-order consistently across platforms.
|
||||
export LANG=C
|
||||
|
||||
rm -rfv results
|
||||
mkdir results
|
||||
includes="$( pwd )/results/rawincludes.txt"
|
||||
pushd ../../..
|
||||
echo Raw includes:
|
||||
grep -r '^[ ]*#include.*/.*\.h' include src | \
|
||||
grep -v boost | tee ${includes}
|
||||
popd
|
||||
pushd results
|
||||
|
||||
oldifs=${IFS}
|
||||
IFS=:
|
||||
mkdir includes
|
||||
mkdir included_by
|
||||
echo Build levelization paths
|
||||
exec 3< ${includes} # open rawincludes.txt for input
|
||||
while read -r -u 3 file include
|
||||
do
|
||||
level=$( echo ${file} | cut -d/ -f 2,3 )
|
||||
# If the "level" indicates a file, cut off the filename
|
||||
if [[ "${level##*.}" != "${level}" ]]
|
||||
then
|
||||
# Use the "toplevel" label as a workaround for `sort`
|
||||
# inconsistencies between different utility versions
|
||||
level="$( dirname ${level} )/toplevel"
|
||||
fi
|
||||
level=$( echo ${level} | tr '/' '.' )
|
||||
|
||||
includelevel=$( echo ${include} | sed 's/.*["<]//; s/[">].*//' | \
|
||||
cut -d/ -f 1,2 )
|
||||
if [[ "${includelevel##*.}" != "${includelevel}" ]]
|
||||
then
|
||||
# Use the "toplevel" label as a workaround for `sort`
|
||||
# inconsistencies between different utility versions
|
||||
includelevel="$( dirname ${includelevel} )/toplevel"
|
||||
fi
|
||||
includelevel=$( echo ${includelevel} | tr '/' '.' )
|
||||
|
||||
if [[ "$level" != "$includelevel" ]]
|
||||
then
|
||||
echo $level $includelevel | tee -a paths.txt
|
||||
fi
|
||||
done
|
||||
echo Sort and deduplicate paths
|
||||
sort -ds paths.txt | uniq -c | tee sortedpaths.txt
|
||||
mv sortedpaths.txt paths.txt
|
||||
exec 3>&- #close fd 3
|
||||
IFS=${oldifs}
|
||||
unset oldifs
|
||||
|
||||
echo Split into flat-file database
|
||||
exec 4<paths.txt # open paths.txt for input
|
||||
while read -r -u 4 count level include
|
||||
do
|
||||
echo ${include} ${count} | tee -a includes/${level}
|
||||
echo ${level} ${count} | tee -a included_by/${include}
|
||||
done
|
||||
exec 4>&- #close fd 4
|
||||
|
||||
loops="$( pwd )/loops.txt"
|
||||
ordering="$( pwd )/ordering.txt"
|
||||
pushd includes
|
||||
echo Search for loops
|
||||
# Redirect stdout to a file
|
||||
exec 4>&1
|
||||
exec 1>"${loops}"
|
||||
for source in *
|
||||
do
|
||||
if [[ -f "$source" ]]
|
||||
then
|
||||
exec 5<"${source}" # open for input
|
||||
while read -r -u 5 include includefreq
|
||||
do
|
||||
if [[ -f $include ]]
|
||||
then
|
||||
if grep -q -w $source $include
|
||||
then
|
||||
if grep -q -w "Loop: $include $source" "${loops}"
|
||||
then
|
||||
continue
|
||||
fi
|
||||
sourcefreq=$( grep -w $source $include | cut -d\ -f2 )
|
||||
echo "Loop: $source $include"
|
||||
# If the counts are close, indicate that the two modules are
|
||||
# on the same level, though they shouldn't be
|
||||
if [[ $(( $includefreq - $sourcefreq )) -gt 3 ]]
|
||||
then
|
||||
echo -e " $source > $include\n"
|
||||
elif [[ $(( $sourcefreq - $includefreq )) -gt 3 ]]
|
||||
then
|
||||
echo -e " $include > $source\n"
|
||||
elif [[ $sourcefreq -eq $includefreq ]]
|
||||
then
|
||||
echo -e " $include == $source\n"
|
||||
else
|
||||
echo -e " $include ~= $source\n"
|
||||
fi
|
||||
else
|
||||
echo "$source > $include" >> "${ordering}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
exec 5>&- #close fd 5
|
||||
fi
|
||||
done
|
||||
exec 1>&4 #close fd 1
|
||||
exec 4>&- #close fd 4
|
||||
cat "${ordering}"
|
||||
cat "${loops}"
|
||||
popd
|
||||
popd
|
||||
popd
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Check levelization
|
||||
run: .github/scripts/levelization/generate.sh
|
||||
run: python .github/scripts/levelization/generate.py
|
||||
- name: Check for differences
|
||||
env:
|
||||
MESSAGE: |
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
removed from loops.txt, it's probably an improvement, while if
|
||||
something was added, it's probably a regression.
|
||||
|
||||
Run '.github/scripts/levelization/generate.sh' in your repo, commit
|
||||
Run '.github/scripts/levelization/generate.py' in your repo, commit
|
||||
and push the changes. See .github/scripts/levelization/README.md for
|
||||
more info.
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -75,6 +75,9 @@ DerivedData
|
||||
/.claude
|
||||
/CLAUDE.md
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
|
||||
# Direnv's directory
|
||||
/.direnv
|
||||
|
||||
|
||||
@@ -11,12 +11,7 @@ function(xrpl_add_test name)
|
||||
)
|
||||
add_executable(${target} ${ARGN} ${sources})
|
||||
|
||||
isolate_headers(
|
||||
${target}
|
||||
"${CMAKE_SOURCE_DIR}"
|
||||
"${CMAKE_SOURCE_DIR}/tests/${name}"
|
||||
PRIVATE
|
||||
)
|
||||
isolate_headers(${target} "${CMAKE_SOURCE_DIR}" "${CMAKE_SOURCE_DIR}/tests/${name}" PRIVATE)
|
||||
|
||||
add_test(NAME ${target} COMMAND ${target})
|
||||
endfunction()
|
||||
|
||||
@@ -10,25 +10,16 @@ include(target_protobuf_sources)
|
||||
# so we just build them as a separate library.
|
||||
add_library(xrpl.libpb)
|
||||
set_target_properties(xrpl.libpb PROPERTIES UNITY_BUILD OFF)
|
||||
target_protobuf_sources(
|
||||
xrpl.libpb
|
||||
xrpl/proto
|
||||
LANGUAGE cpp
|
||||
IMPORT_DIRS include/xrpl/proto
|
||||
PROTOS include/xrpl/proto/xrpl.proto
|
||||
target_protobuf_sources(xrpl.libpb xrpl/proto LANGUAGE cpp IMPORT_DIRS include/xrpl/proto
|
||||
PROTOS include/xrpl/proto/xrpl.proto
|
||||
)
|
||||
|
||||
file(GLOB_RECURSE protos "include/xrpl/proto/org/*.proto")
|
||||
target_protobuf_sources(
|
||||
xrpl.libpb
|
||||
xrpl/proto
|
||||
LANGUAGE cpp
|
||||
IMPORT_DIRS include/xrpl/proto
|
||||
PROTOS "${protos}"
|
||||
target_protobuf_sources(xrpl.libpb xrpl/proto LANGUAGE cpp IMPORT_DIRS include/xrpl/proto
|
||||
PROTOS "${protos}"
|
||||
)
|
||||
target_protobuf_sources(
|
||||
xrpl.libpb
|
||||
xrpl/proto
|
||||
xrpl.libpb xrpl/proto
|
||||
LANGUAGE grpc
|
||||
IMPORT_DIRS include/xrpl/proto
|
||||
PROTOS "${protos}"
|
||||
|
||||
@@ -39,15 +39,19 @@ list(
|
||||
)
|
||||
|
||||
setup_target_for_coverage_gcovr(
|
||||
NAME coverage
|
||||
FORMAT ${coverage_format}
|
||||
NAME
|
||||
coverage
|
||||
FORMAT
|
||||
${coverage_format}
|
||||
EXCLUDE
|
||||
"src/test"
|
||||
"src/tests"
|
||||
"include/xrpl/beast/test"
|
||||
"include/xrpl/beast/unit_test"
|
||||
"${CMAKE_BINARY_DIR}/pb-xrpl.libpb"
|
||||
DEPENDENCIES xrpld xrpl.tests
|
||||
"src/test"
|
||||
"src/tests"
|
||||
"include/xrpl/beast/test"
|
||||
"include/xrpl/beast/unit_test"
|
||||
"${CMAKE_BINARY_DIR}/pb-xrpl.libpb"
|
||||
DEPENDENCIES
|
||||
xrpld
|
||||
xrpl.tests
|
||||
)
|
||||
|
||||
add_code_coverage_to_target(opts INTERFACE)
|
||||
|
||||
@@ -42,11 +42,7 @@ function(verbose_find_path variable name)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
verbose_find_path(
|
||||
doxygen_plantuml_jar_path
|
||||
plantuml.jar
|
||||
PATH_SUFFIXES share/plantuml
|
||||
)
|
||||
verbose_find_path(doxygen_plantuml_jar_path plantuml.jar PATH_SUFFIXES share/plantuml)
|
||||
verbose_find_path(doxygen_dot_path dot)
|
||||
|
||||
# https://en.cppreference.com/w/Cppreference:Archives
|
||||
|
||||
@@ -25,16 +25,10 @@ function(add_module parent name)
|
||||
${target}
|
||||
PUBLIC "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
|
||||
)
|
||||
isolate_headers(
|
||||
${target}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include/${parent}/${name}"
|
||||
PUBLIC
|
||||
isolate_headers(${target} "${CMAKE_CURRENT_SOURCE_DIR}/include"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include/${parent}/${name}" PUBLIC
|
||||
)
|
||||
isolate_headers(
|
||||
${target}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/lib${parent}/${name}"
|
||||
PRIVATE
|
||||
isolate_headers(${target} "${CMAKE_CURRENT_SOURCE_DIR}/src"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/lib${parent}/${name}" PRIVATE
|
||||
)
|
||||
endfunction()
|
||||
|
||||
@@ -1,885 +0,0 @@
|
||||
# Proposal: Separating Domain and Transaction Invariants
|
||||
|
||||
## Problem
|
||||
|
||||
XRPL on-chain protocols are implemented as semantically cohesive groups of transactions (see
|
||||
`include/xrpl/tx/transactors/` — vault, dex, lending, etc.). Each protocol also has invariant
|
||||
checks that run after every transaction to catch bugs before they reach the ledger.
|
||||
|
||||
The current invariant system conflates two distinct concerns in a single class:
|
||||
|
||||
1. **Domain invariants** — properties that must hold regardless of which transaction ran.
|
||||
Example: "a vault with zero shares must have zero assets."
|
||||
|
||||
2. **Transaction invariants** — properties specific to a particular transaction type.
|
||||
Example: "VaultDeposit must increase vault balance by the deposit amount."
|
||||
|
||||
This leads to:
|
||||
|
||||
- **Monolithic switch statements**: `ValidVault::finalize` is 800+ lines with a switch
|
||||
dispatching per transaction type. `ValidAMM::finalize` has a similar pattern.
|
||||
- **Scattered rules**: the invariant rules for `VaultDeposit` are split between
|
||||
`VaultDeposit.cpp` (business logic) and `VaultInvariant.cpp` (validation), making it hard
|
||||
to reason about a transaction holistically.
|
||||
- **No isolation**: transaction-specific invariant logic cannot be unit-tested without the
|
||||
full invariant infrastructure.
|
||||
- **Unbounded growth**: every new transaction type adds another case to the switch.
|
||||
|
||||
---
|
||||
|
||||
## Current Architecture
|
||||
|
||||
### Two-Phase Invariant Checking
|
||||
|
||||
After a transaction is applied, `ApplyContext::checkInvariantsHelper` runs all invariant
|
||||
checkers in two phases:
|
||||
|
||||
```
|
||||
Phase 1 — State Collection
|
||||
For each modified ledger entry:
|
||||
call visitEntry() on every checker in the InvariantChecks tuple
|
||||
|
||||
Phase 2 — Validation
|
||||
For each checker:
|
||||
call finalize() → returns true (pass) or false (fail)
|
||||
```
|
||||
|
||||
This is implemented via a `std::tuple` of checker classes, iterated at compile time with
|
||||
`std::index_sequence`. Each checker implements `visitEntry` and `finalize` as duck-typed
|
||||
methods — no base class, no virtual dispatch.
|
||||
|
||||
### Existing Invariant Categories
|
||||
|
||||
The 24 current checkers fall into three informal categories:
|
||||
|
||||
| Category | Description | Examples |
|
||||
| ------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| **Universal** | Always run, no tx-type awareness | `XRPNotCreated`, `TransactionFeeCheck`, `LedgerEntryTypesMatch` |
|
||||
| **Privilege-gated** | Use `hasPrivilege()` to differentiate by tx-type | `AccountRootsNotDeleted`, `ValidNewAccountRoot`, `ValidMPTIssuance` |
|
||||
| **Domain** | Large switch on tx-type mixing domain + tx-specific checks | `ValidVault`, `ValidAMM`, `ValidLoan` |
|
||||
|
||||
The first two categories work well. The third is where the problem lies.
|
||||
|
||||
---
|
||||
|
||||
## Proposed Architecture
|
||||
|
||||
### Goal
|
||||
|
||||
Separate domain invariants from transaction invariants with minimal machinery:
|
||||
|
||||
- **Domain invariants** stay in domain invariant classes (e.g., `ValidVault`). They always
|
||||
run when the domain is touched, regardless of transaction type.
|
||||
- **Transaction invariants** move to static methods on the transactors themselves. They run
|
||||
only for their specific transaction type.
|
||||
- **Opt-in with compile-time enforcement**: transactors that declare they have invariants
|
||||
must implement them, or compilation fails.
|
||||
- **Two-phase process preserved**: state collection in Phase 1, validation in Phase 2.
|
||||
- **ApplyContext unchanged**: the tuple, `visitEntry`, and `finalize` interface stay as-is.
|
||||
|
||||
### Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ InvariantChecks tuple │
|
||||
│ │
|
||||
│ XRPNotCreated │ ... │ ValidVault │ ValidAMM │ ... │
|
||||
└──────────────────────────┬──────────┬──────────────────-┘
|
||||
│ │
|
||||
┌────────────┘ └─────────────┐
|
||||
▼ ▼
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ ValidVault │ │ ValidAMM │
|
||||
│ │ │ │
|
||||
│ visitEntry() │ │ visitEntry() │
|
||||
│ → collect state │ │ → collect state │
|
||||
│ │ │ │
|
||||
│ finalize() │ │ finalize() │
|
||||
│ 1. domain checks │ │ 1. domain checks │
|
||||
│ 2. dispatch tx │ │ 2. dispatch tx │
|
||||
│ invariant │ │ invariant │
|
||||
└─────────┬───────────┘ └─────────┬───────────┘
|
||||
│ │
|
||||
│ if ttVAULT_DEPOSIT │ if ttAMM_DEPOSIT
|
||||
▼ ▼
|
||||
VaultDeposit::checkInvariants() AMMDeposit::checkInvariants()
|
||||
```
|
||||
|
||||
### Design
|
||||
|
||||
There are four components:
|
||||
|
||||
1. **Domain state** — extracted from the current invariant class into its own type
|
||||
2. **Domain invariant class** — owns the state, runs domain-wide checks, delegates tx dispatch
|
||||
3. **Transaction invariants** — static methods on transactors
|
||||
4. **Opt-in trait + dispatch** — connects transactors to domain state with compile-time safety
|
||||
|
||||
Each is described below.
|
||||
|
||||
---
|
||||
|
||||
### 1. Domain State
|
||||
|
||||
Each domain extracts its accumulated state into a standalone class. This class is populated
|
||||
during Phase 1 (`visitEntry`) and read during Phase 2 by both domain checks and transaction
|
||||
checks.
|
||||
|
||||
```cpp
|
||||
// include/xrpl/tx/invariants/VaultInvariantState.h
|
||||
|
||||
class VaultInvariantState
|
||||
{
|
||||
public:
|
||||
struct Vault
|
||||
{
|
||||
uint256 key;
|
||||
Asset asset;
|
||||
AccountID pseudoId;
|
||||
AccountID owner;
|
||||
uint192 shareMPTID;
|
||||
Number assetsTotal;
|
||||
Number assetsAvailable;
|
||||
Number assetsMaximum;
|
||||
Number lossUnrealized;
|
||||
|
||||
static Vault make(SLE const&);
|
||||
};
|
||||
|
||||
struct Shares
|
||||
{
|
||||
MPTIssue share;
|
||||
std::uint64_t sharesTotal;
|
||||
std::uint64_t sharesMaximum;
|
||||
|
||||
static Shares make(SLE const&);
|
||||
};
|
||||
|
||||
void visitEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after);
|
||||
|
||||
// Read-only accessors
|
||||
std::vector<Vault> const& beforeVault() const { return beforeVault_; }
|
||||
std::vector<Vault> const& afterVault() const { return afterVault_; }
|
||||
std::vector<Shares> const& beforeMPTs() const { return beforeMPTs_; }
|
||||
std::vector<Shares> const& afterMPTs() const { return afterMPTs_; }
|
||||
|
||||
std::optional<Number> deltaAssets(Asset const&, AccountID const&) const;
|
||||
|
||||
private:
|
||||
std::vector<Vault> beforeVault_;
|
||||
std::vector<Vault> afterVault_;
|
||||
std::vector<Shares> beforeMPTs_;
|
||||
std::vector<Shares> afterMPTs_;
|
||||
std::unordered_map<uint256, Number> deltas_;
|
||||
};
|
||||
```
|
||||
|
||||
The `visitEntry` implementation is extracted directly from the current `ValidVault::visitEntry`
|
||||
— no logic changes, just moved to a new class.
|
||||
|
||||
### 2. Domain Invariant Class
|
||||
|
||||
Each domain invariant class owns its state, delegates `visitEntry`, and splits `finalize` into
|
||||
two parts: domain checks (always run) and transaction dispatch.
|
||||
|
||||
```cpp
|
||||
// include/xrpl/tx/invariants/VaultInvariant.h
|
||||
|
||||
class ValidVault
|
||||
{
|
||||
VaultInvariantState state_;
|
||||
|
||||
public:
|
||||
void visitEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after)
|
||||
{
|
||||
state_.visitEntry(isDelete, before, after);
|
||||
}
|
||||
|
||||
bool finalize(
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
XRPAmount fee,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j);
|
||||
};
|
||||
```
|
||||
|
||||
```cpp
|
||||
// src/libxrpl/tx/invariants/VaultInvariant.cpp
|
||||
|
||||
bool ValidVault::finalize(
|
||||
STTx const& tx, TER result, XRPAmount fee,
|
||||
ReadView const& view, beast::Journal const& j)
|
||||
{
|
||||
if (!isTesSuccess(result))
|
||||
return true;
|
||||
|
||||
// --- Domain invariants (always run) ---
|
||||
// These are the checks currently before/outside the switch statement:
|
||||
// - vault operation must modify exactly one vault
|
||||
// - vault immutable fields (asset, pseudoId, shareMPTID) unchanged
|
||||
// - vault with zero shares has zero assets
|
||||
// - assets available <= assets total
|
||||
// - MPT issuance consistent with vault shareMPTID
|
||||
if (!checkDomainInvariants(state_, tx, result, view, j))
|
||||
return false;
|
||||
|
||||
// --- Transaction invariants (per-transaction) ---
|
||||
return dispatchTransactionInvariant<VaultInvariantState>(
|
||||
state_, tx, result, view, j);
|
||||
}
|
||||
```
|
||||
|
||||
`checkDomainInvariants` is a private method (or free function in the .cpp) containing the
|
||||
checks that currently live outside the switch statement in `ValidVault::finalize`. These checks
|
||||
run for every transaction that touches a vault.
|
||||
|
||||
### 3. Transaction Invariants
|
||||
|
||||
Each transactor that has transaction-specific invariant logic declares and implements a static
|
||||
`checkInvariants` method:
|
||||
|
||||
```cpp
|
||||
// include/xrpl/tx/transactors/vault/VaultDeposit.h
|
||||
|
||||
class VaultInvariantState; // forward declaration suffices
|
||||
|
||||
class VaultDeposit : public Transactor
|
||||
{
|
||||
public:
|
||||
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
|
||||
|
||||
explicit VaultDeposit(ApplyContext& ctx) : Transactor(ctx) {}
|
||||
|
||||
static NotTEC preflight(PreflightContext const& ctx);
|
||||
static TER preclaim(PreclaimContext const& ctx);
|
||||
TER doApply() override;
|
||||
|
||||
// Transaction invariant: validates post-conditions specific to VaultDeposit
|
||||
static bool checkInvariants(
|
||||
VaultInvariantState const& state,
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j);
|
||||
};
|
||||
```
|
||||
|
||||
```cpp
|
||||
// src/libxrpl/tx/transactors/vault/VaultDeposit.cpp
|
||||
|
||||
#include <xrpl/tx/invariants/VaultInvariantState.h> // full definition needed here
|
||||
|
||||
bool VaultDeposit::checkInvariants(
|
||||
VaultInvariantState const& state,
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
// Logic currently in the ttVAULT_DEPOSIT case of ValidVault::finalize:
|
||||
// - vault balance increased by deposit amount
|
||||
// - depositor balance decreased by same amount
|
||||
// - vault shares outstanding increased
|
||||
// - assets maximum not exceeded
|
||||
}
|
||||
```
|
||||
|
||||
Transactors that have no transaction-specific invariants (e.g., `Payment` for the vault
|
||||
domain) define nothing — the dispatch skips them.
|
||||
|
||||
### 4. Opt-In Trait and Dispatch
|
||||
|
||||
#### The Trait
|
||||
|
||||
A transactor declares participation in a domain by specializing `InvariantDomains<T>`:
|
||||
|
||||
```cpp
|
||||
// include/xrpl/tx/invariants/InvariantDomains.h
|
||||
|
||||
#pragma once
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
// Primary template: no domain invariants by default.
|
||||
template <typename Transactor>
|
||||
struct InvariantDomains
|
||||
{
|
||||
using types = std::tuple<>;
|
||||
};
|
||||
|
||||
// Compile-time tuple membership check.
|
||||
template <typename State, typename Tuple>
|
||||
struct tuple_contains : std::false_type {};
|
||||
|
||||
template <typename State, typename... Ts>
|
||||
struct tuple_contains<State, std::tuple<State, Ts...>> : std::true_type {};
|
||||
|
||||
template <typename State, typename T, typename... Ts>
|
||||
struct tuple_contains<State, std::tuple<T, Ts...>>
|
||||
: tuple_contains<State, std::tuple<Ts...>> {};
|
||||
|
||||
template <typename State, typename Tuple>
|
||||
inline constexpr bool tuple_contains_v = tuple_contains<State, Tuple>::value;
|
||||
|
||||
} // namespace xrpl
|
||||
```
|
||||
|
||||
Transactors opt in by specializing the trait in their header:
|
||||
|
||||
```cpp
|
||||
// At the bottom of VaultDeposit.h, after the class definition
|
||||
|
||||
template <>
|
||||
struct InvariantDomains<VaultDeposit>
|
||||
{
|
||||
using types = std::tuple<VaultInvariantState>;
|
||||
};
|
||||
```
|
||||
|
||||
#### Why Traits Instead of SFINAE
|
||||
|
||||
An alternative is to use `if constexpr (requires { T::checkInvariants(...) })` to detect
|
||||
whether a transactor provides invariant checks. This is dangerous because:
|
||||
|
||||
- If the `checkInvariants` signature changes (e.g., parameter reordering), the `requires`
|
||||
expression quietly evaluates to `false` and the check is **silently skipped**.
|
||||
- If someone deletes `checkInvariants` during refactoring, the same silent skip occurs.
|
||||
- A method name typo (`checkInvariant` vs `checkInvariants`) disables the check silently.
|
||||
|
||||
The traits approach separates **declaration of intent** from **implementation**:
|
||||
|
||||
| Scenario | SFINAE | Traits |
|
||||
| --------------------------- | --------------- | ------------------------- |
|
||||
| Opted in, method correct | Runs | Runs |
|
||||
| Opted in, method missing | **Silent skip** | **Compile error** |
|
||||
| Opted in, wrong signature | **Silent skip** | **Compile error** |
|
||||
| Not opted in, no method | Skips (correct) | Skips (correct) |
|
||||
| Method deleted in refactor | **Silent skip** | **Compile error** |
|
||||
| Opt-in removed deliberately | N/A | Skips (visible in review) |
|
||||
|
||||
The only way to disable a check is to remove the trait specialization — a deliberate,
|
||||
reviewable change.
|
||||
|
||||
#### The Dispatch Function
|
||||
|
||||
The dispatch function is **declared** in a lightweight header and **defined** in a single
|
||||
`.cpp` file. This keeps all 45+ transactor `#include`s out of invariant headers.
|
||||
|
||||
```cpp
|
||||
// include/xrpl/tx/invariants/InvariantDispatch.h — declaration only
|
||||
|
||||
#pragma once
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
class ReadView;
|
||||
|
||||
template <typename State>
|
||||
bool dispatchTransactionInvariant(
|
||||
State const& state,
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j);
|
||||
|
||||
} // namespace xrpl
|
||||
```
|
||||
|
||||
The implementation uses `transactions.macro` to generate a switch over all transaction types.
|
||||
For each type, `tuple_contains_v` checks whether that transactor opted in for the given
|
||||
domain state. If it did, the transactor's `checkInvariants` is called — and if the method
|
||||
is missing or has the wrong signature, **compilation fails**.
|
||||
|
||||
```cpp
|
||||
// src/libxrpl/tx/invariants/InvariantDispatch.cpp
|
||||
|
||||
#define TRANSACTION_INCLUDE 1
|
||||
#include <xrpl/protocol/detail/transactions.macro>
|
||||
|
||||
#include <xrpl/tx/invariants/InvariantDomains.h>
|
||||
#include <xrpl/tx/invariants/VaultInvariantState.h>
|
||||
#include <xrpl/tx/invariants/AMMInvariantState.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
template <typename State>
|
||||
bool dispatchTransactionInvariant(
|
||||
State const& state,
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
switch (tx.getTxnType())
|
||||
{
|
||||
#pragma push_macro("TRANSACTION")
|
||||
#undef TRANSACTION
|
||||
#define TRANSACTION(tag, value, name, ...) \
|
||||
case tag: { \
|
||||
if constexpr (tuple_contains_v<State, \
|
||||
typename InvariantDomains<name>::types>) { \
|
||||
return name::checkInvariants(state, tx, result, view, j); \
|
||||
} \
|
||||
return true; \
|
||||
}
|
||||
#include <xrpl/protocol/detail/transactions.macro>
|
||||
#undef TRANSACTION
|
||||
#pragma pop_macro("TRANSACTION")
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit instantiation — one line per domain
|
||||
template bool dispatchTransactionInvariant<VaultInvariantState>(
|
||||
VaultInvariantState const&, STTx const&, TER,
|
||||
ReadView const&, beast::Journal const&);
|
||||
template bool dispatchTransactionInvariant<AMMInvariantState>(
|
||||
AMMInvariantState const&, STTx const&, TER,
|
||||
ReadView const&, beast::Journal const&);
|
||||
|
||||
} // namespace xrpl
|
||||
```
|
||||
|
||||
Each new domain adds one explicit instantiation line. Forgetting it produces a linker error.
|
||||
|
||||
**Note on RAII number guards:** `Transactor::operator()` establishes `NumberSO` and
|
||||
`CurrentTransactionRulesGuard` before calling `checkInvariants`, so the dispatch function
|
||||
does not need to duplicate them.
|
||||
|
||||
---
|
||||
|
||||
## Execution Flow
|
||||
|
||||
### Phase 1 — State Collection
|
||||
|
||||
```
|
||||
ApplyContext::checkInvariantsHelper()
|
||||
│
|
||||
│ for each modified ledger entry:
|
||||
│
|
||||
├──▶ TransactionFeeCheck::visitEntry() (no-op)
|
||||
├──▶ XRPNotCreated::visitEntry() → accumulates drops_
|
||||
├──▶ ...
|
||||
└──▶ ValidVault::visitEntry()
|
||||
│
|
||||
└──▶ VaultInvariantState::visitEntry()
|
||||
│
|
||||
├─ ltVAULT → push to beforeVault_ / afterVault_
|
||||
├─ ltMPTOKEN_ISSUANCE → push to beforeMPTs_ / afterMPTs_
|
||||
├─ ltACCOUNT_ROOT → record in deltas_
|
||||
└─ (other types ignored)
|
||||
```
|
||||
|
||||
### Phase 2 — Validation
|
||||
|
||||
```
|
||||
ApplyContext::checkInvariantsHelper()
|
||||
│
|
||||
├──▶ TransactionFeeCheck::finalize() → checks fee
|
||||
├──▶ XRPNotCreated::finalize() → checks drops_
|
||||
├──▶ ...
|
||||
└──▶ ValidVault::finalize()
|
||||
│
|
||||
├─ 1. checkDomainInvariants(state_)
|
||||
│ "exactly one vault modified"
|
||||
│ "immutable fields unchanged"
|
||||
│ "vault with zero shares has zero assets"
|
||||
│
|
||||
└─ 2. dispatchTransactionInvariant<VaultInvariantState>(state_, ...)
|
||||
│
|
||||
│ switch(tx.getTxnType())
|
||||
│
|
||||
│ case ttVAULT_DEPOSIT:
|
||||
│ InvariantDomains<VaultDeposit>::types
|
||||
│ contains VaultInvariantState? YES
|
||||
│ → VaultDeposit::checkInvariants(state_, ...)
|
||||
│ "vault balance increased by deposit amount"
|
||||
│ "depositor balance decreased by same amount"
|
||||
│ "shares outstanding increased"
|
||||
│
|
||||
│ case ttPAYMENT:
|
||||
│ InvariantDomains<Payment>::types
|
||||
│ contains VaultInvariantState? NO
|
||||
│ → return true (skip)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Include Dependencies
|
||||
|
||||
The design avoids circular dependencies and minimizes compile-time impact:
|
||||
|
||||
```
|
||||
InvariantDomains.h ← lightweight, only <tuple>
|
||||
InvariantDispatch.h ← lightweight, only forward declarations + STTx/TER
|
||||
VaultInvariantState.h ← protocol types only (Number, Asset, MPTIssue, etc.)
|
||||
|
||||
VaultInvariant.h → VaultInvariantState.h, InvariantDispatch.h
|
||||
VaultDeposit.h → Transactor.h, InvariantDomains.h
|
||||
(forward-declares VaultInvariantState)
|
||||
|
||||
InvariantDispatch.cpp → transactions.macro (TRANSACTION_INCLUDE=1)
|
||||
ALL transactor headers, ALL state headers
|
||||
(heavy includes confined to one .cpp)
|
||||
|
||||
VaultDeposit.cpp → VaultInvariantState.h (full definition)
|
||||
```
|
||||
|
||||
No invariant header includes any transactor header. No transactor header includes any
|
||||
invariant state header (forward declaration suffices). The heavy fan-out from
|
||||
`transactions.macro` is confined to `InvariantDispatch.cpp`.
|
||||
|
||||
---
|
||||
|
||||
## What Changes and What Doesn't
|
||||
|
||||
| Component | Changes? | Details |
|
||||
| -------------------------- | -------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `ApplyContext` | **No** | Tuple iteration, `visitEntry`/`finalize` interface unchanged |
|
||||
| `InvariantChecks` tuple | **No** | `ValidVault`, `ValidAMM` stay in the tuple |
|
||||
| Universal invariants | **No** | `XRPNotCreated`, `TransactionFeeCheck`, etc. untouched |
|
||||
| Privilege-gated invariants | **No** | `AccountRootsNotDeleted`, `ValidMPTIssuance`, etc. untouched |
|
||||
| `ValidVault` interface | **No** | Still has `visitEntry` + `finalize` with same signatures |
|
||||
| `ValidVault` internals | **Yes** | State extracted; finalize split into domain + dispatch |
|
||||
| `ValidAMM` internals | **Yes** | Same treatment as `ValidVault` |
|
||||
| Transactor headers | **Yes** | Add `checkInvariants` declaration + trait specialization |
|
||||
| Transactor `.cpp` files | **Yes** | Add `checkInvariants` implementation |
|
||||
| New files | **Yes** | `InvariantDomains.h`, `InvariantDispatch.h`, `InvariantDispatch.cpp`, per-domain state headers |
|
||||
|
||||
---
|
||||
|
||||
## Migration Path
|
||||
|
||||
| Phase | Change | Files |
|
||||
| ----- | ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||
| 1 | Add `InvariantDomains.h` trait and `InvariantDispatch.h` declaration | 2 new headers |
|
||||
| 2 | Add `InvariantDispatch.cpp` with dispatch template (initially no instantiations) | 1 new `.cpp` |
|
||||
| 3 | Extract `VaultInvariantState` from `ValidVault` | `VaultInvariantState.h`, `VaultInvariantState.cpp` |
|
||||
| 4 | Refactor `ValidVault`: own state, split finalize into domain checks + dispatch | `VaultInvariant.h`, `VaultInvariant.cpp` |
|
||||
| 5 | Migrate one transactor (e.g., `VaultDeposit`): add `checkInvariants` + trait, remove its switch case | `VaultDeposit.h`, `VaultDeposit.cpp`, `VaultInvariant.cpp` |
|
||||
| 6 | Repeat for remaining vault transactors (`VaultCreate`, `VaultSet`, `VaultWithdraw`, `VaultClawback`) | Vault transactor files, `VaultInvariant.cpp` |
|
||||
| 7 | Delete empty switch from `ValidVault::finalize` | `VaultInvariant.cpp` |
|
||||
| 8 | Repeat 3–7 for `ValidAMM` and lending invariants | AMM/loan files, `InvariantDispatch.cpp` |
|
||||
|
||||
Each phase is independently deployable. The switch shrinks one case at a time.
|
||||
|
||||
---
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### A. CRTP Base Class (`DomainInvariantBase<Derived, State>`)
|
||||
|
||||
A CRTP base could provide `visitEntry` delegation and the two-phase `finalize` pattern
|
||||
generically, eliminating ~5 lines of boilerplate per domain.
|
||||
|
||||
**Rejected because:**
|
||||
|
||||
- The codebase has zero CRTP in the invariant system today. Adding it raises the template
|
||||
literacy bar for all contributors.
|
||||
- `static_cast<Derived*>(this)` is a known source of subtle bugs if the CRTP contract is
|
||||
violated.
|
||||
- Each domain's `finalize` has unique structure (feature gates, result filtering, helper
|
||||
methods). A generic base cannot capture this without becoming complex itself.
|
||||
|
||||
### B. Virtual Base Class
|
||||
|
||||
A virtual `DomainInvariant` base class could provide a common interface.
|
||||
|
||||
**Rejected because:**
|
||||
|
||||
- The existing invariant system is entirely duck-typed via `std::tuple`. Introducing virtual
|
||||
dispatch changes the paradigm for only some checkers, creating inconsistency.
|
||||
- Does not solve the dispatch-to-transactor problem — you still need a mechanism to call
|
||||
`VaultDeposit::checkInvariants` from the invariant system.
|
||||
|
||||
### C. SFINAE Detection Instead of Traits
|
||||
|
||||
Use `if constexpr (requires { T::checkInvariants(...) })` to detect transactor methods.
|
||||
|
||||
**Rejected because:**
|
||||
|
||||
- Silent failure on signature mismatch or method removal (see "Why Traits Instead of SFINAE"
|
||||
above). This is the single most dangerous failure mode for a safety-critical system like
|
||||
invariant checking.
|
||||
|
||||
### D. Runtime Registry
|
||||
|
||||
Transactors register `std::function` callbacks keyed by `TxType` at startup.
|
||||
|
||||
**Rejected because:**
|
||||
|
||||
- Static initialization order fiasco across translation units.
|
||||
- Type safety loss (`void*` casts or type-erased state).
|
||||
- A transactor that forgets to register silently passes — same problem as SFINAE.
|
||||
- Runtime overhead on every transaction for no benefit.
|
||||
|
||||
### E. Extend `transactions.macro` with Invariant Domain Field
|
||||
|
||||
See **Proposal B** below — this is presented as a full alternative design rather than a
|
||||
rejected option. It uses a domain bitfield in the macro (analogous to the existing `privileges`
|
||||
field) instead of per-transactor trait specializations.
|
||||
|
||||
---
|
||||
|
||||
## Proposal B: Macro-Based Domain Opt-In
|
||||
|
||||
This is an alternative to the traits-based opt-in described above. Everything else in the
|
||||
architecture (state extraction, domain invariant classes, `checkInvariants` on transactors,
|
||||
the dispatch function, the two-phase process) stays the same. Only **how a transactor declares
|
||||
its domain membership** changes.
|
||||
|
||||
### Motivation
|
||||
|
||||
The `transactions.macro` already serves as the single source of truth for transaction
|
||||
metadata: tag, name, delegation, amendments, privileges, fields. The `privileges` bitfield
|
||||
is already queried by invariant code via `hasPrivilege()`. A domain bitfield follows the same
|
||||
established pattern — it belongs in the macro alongside everything else, rather than scattered
|
||||
across transactor headers as trait specializations.
|
||||
|
||||
### Domain Enum
|
||||
|
||||
```cpp
|
||||
// include/xrpl/tx/invariants/InvariantCheckDomain.h
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
enum Domain : std::uint16_t {
|
||||
noDomain = 0x0000,
|
||||
vaultDomain = 0x0001,
|
||||
ammDomain = 0x0002,
|
||||
loanDomain = 0x0004,
|
||||
};
|
||||
|
||||
constexpr Domain operator|(Domain a, Domain b)
|
||||
{
|
||||
return static_cast<Domain>(
|
||||
static_cast<std::uint16_t>(a) | static_cast<std::uint16_t>(b));
|
||||
}
|
||||
|
||||
constexpr Domain operator&(Domain a, Domain b)
|
||||
{
|
||||
return static_cast<Domain>(
|
||||
static_cast<std::uint16_t>(a) & static_cast<std::uint16_t>(b));
|
||||
}
|
||||
|
||||
} // namespace xrpl
|
||||
```
|
||||
|
||||
### Macro Change
|
||||
|
||||
Add `domain` as the 7th parameter, pushing `fields` to 8th:
|
||||
|
||||
```
|
||||
// Before (7 parameters):
|
||||
TRANSACTION(tag, value, name, delegable, amendments, privileges, fields)
|
||||
|
||||
// After (8 parameters):
|
||||
TRANSACTION(tag, value, name, delegable, amendments, privileges, domain, fields)
|
||||
```
|
||||
|
||||
Example entries:
|
||||
|
||||
```cpp
|
||||
TRANSACTION(ttVAULT_DEPOSIT, 68, VaultDeposit,
|
||||
Delegation::delegable,
|
||||
featureSingleAssetVault,
|
||||
mayAuthorizeMPT | mustModifyVault,
|
||||
vaultDomain,
|
||||
({
|
||||
{sfVaultID, soeREQUIRED},
|
||||
{sfAmount, soeREQUIRED, soeMPTSupported},
|
||||
}))
|
||||
|
||||
TRANSACTION(ttPAYMENT, 0, Payment,
|
||||
Delegation::notDelegable,
|
||||
uint256{},
|
||||
noPriv,
|
||||
noDomain,
|
||||
({
|
||||
{sfDestination, soeREQUIRED},
|
||||
...
|
||||
}))
|
||||
|
||||
// A transaction participating in multiple domains:
|
||||
TRANSACTION(ttLOAN_MANAGE, 81, LoanManage,
|
||||
Delegation::delegable,
|
||||
featureLendingProtocol,
|
||||
mustModifyVault | ...,
|
||||
loanDomain | vaultDomain,
|
||||
({...}))
|
||||
```
|
||||
|
||||
### Impact on Existing Macro Consumers
|
||||
|
||||
Most consumers use `TRANSACTION(tag, value, name, ...)` and are **unaffected**:
|
||||
|
||||
| Consumer | Current definition | Impact |
|
||||
| --------------------- | ------------------------------------------------------------------------- | --------------- |
|
||||
| `TxFormats.h` | `TRANSACTION(tag, value, ...)` | None |
|
||||
| `jss.h` | `TRANSACTION(tag, value, name, ...)` | None |
|
||||
| `applySteps.cpp` | `TRANSACTION(tag, value, name, ...)` | None |
|
||||
| `Permissions.cpp` (1) | `TRANSACTION(tag, value, name, delegable, amendment, ...)` | None |
|
||||
| `Permissions.cpp` (2) | `TRANSACTION(tag, value, name, delegable, ...)` | None |
|
||||
| `InvariantCheck.cpp` | `TRANSACTION(tag, value, name, delegable, amendment, privileges, ...)` | None |
|
||||
| `TxFormats.cpp` | `TRANSACTION(tag, value, name, delegable, amendment, privileges, fields)` | **Must update** |
|
||||
|
||||
Only `TxFormats.cpp` uses all 7 positional parameters and must be updated to accept 8:
|
||||
|
||||
```cpp
|
||||
// TxFormats.cpp — only change needed
|
||||
#define TRANSACTION(tag, value, name, delegable, amendment, privileges, domain, fields) \
|
||||
add(jss::name, tag, UNWRAP fields, getCommonFields());
|
||||
```
|
||||
|
||||
### Domain-to-State Mapping
|
||||
|
||||
A template variable maps each `State` type to its domain flag:
|
||||
|
||||
```cpp
|
||||
// include/xrpl/tx/invariants/InvariantCheckDomain.h
|
||||
|
||||
template <typename State>
|
||||
inline constexpr Domain domainFor = noDomain;
|
||||
```
|
||||
|
||||
Each domain state header specializes it:
|
||||
|
||||
```cpp
|
||||
// include/xrpl/tx/invariants/VaultInvariantState.h (at the bottom)
|
||||
|
||||
template <>
|
||||
inline constexpr Domain domainFor<VaultInvariantState> = vaultDomain;
|
||||
```
|
||||
|
||||
### Dispatch Function
|
||||
|
||||
The dispatch is the same structure as Proposal A, but uses the macro's domain field instead
|
||||
of a traits lookup:
|
||||
|
||||
```cpp
|
||||
// src/libxrpl/tx/invariants/InvariantDispatch.cpp
|
||||
|
||||
#define TRANSACTION_INCLUDE 1
|
||||
#include <xrpl/protocol/detail/transactions.macro>
|
||||
|
||||
#include <xrpl/tx/invariants/InvariantCheckDomain.h>
|
||||
#include <xrpl/tx/invariants/VaultInvariantState.h>
|
||||
#include <xrpl/tx/invariants/AMMInvariantState.h>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
template <typename State>
|
||||
bool dispatchTransactionInvariant(
|
||||
State const& state,
|
||||
STTx const& tx,
|
||||
TER result,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
constexpr auto target = domainFor<State>;
|
||||
|
||||
switch (tx.getTxnType())
|
||||
{
|
||||
#pragma push_macro("TRANSACTION")
|
||||
#undef TRANSACTION
|
||||
#define TRANSACTION(tag, value, name, delegable, amend, priv, domain, fields) \
|
||||
case tag: { \
|
||||
if constexpr ((domain & target) != noDomain) { \
|
||||
return name::checkInvariants(state, tx, result, view, j); \
|
||||
} \
|
||||
return true; \
|
||||
}
|
||||
#include <xrpl/protocol/detail/transactions.macro>
|
||||
#undef TRANSACTION
|
||||
#pragma pop_macro("TRANSACTION")
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit instantiations — one per domain
|
||||
template bool dispatchTransactionInvariant<VaultInvariantState>(
|
||||
VaultInvariantState const&, STTx const&, TER,
|
||||
ReadView const&, beast::Journal const&);
|
||||
template bool dispatchTransactionInvariant<AMMInvariantState>(
|
||||
AMMInvariantState const&, STTx const&, TER,
|
||||
ReadView const&, beast::Journal const&);
|
||||
|
||||
} // namespace xrpl
|
||||
```
|
||||
|
||||
**Compile-time enforcement**: when the macro expands for `ttVAULT_DEPOSIT`, `domain` is
|
||||
literally `vaultDomain`, so `(vaultDomain & vaultDomain) != noDomain` is a true `constexpr`
|
||||
expression. The `if constexpr` branch is compiled, and `VaultDeposit::checkInvariants` **must
|
||||
exist** with the correct signature — otherwise compilation fails.
|
||||
|
||||
For `ttPAYMENT`, `domain` is `noDomain`, so `(noDomain & vaultDomain) != noDomain` is false.
|
||||
The branch is discarded. `Payment::checkInvariants` is never referenced.
|
||||
|
||||
### `hasDomain` Query Function
|
||||
|
||||
Analogous to `hasPrivilege()`, for use in domain invariant `finalize` methods:
|
||||
|
||||
```cpp
|
||||
// include/xrpl/tx/invariants/InvariantCheckDomain.h
|
||||
|
||||
bool hasDomain(STTx const& tx, Domain domain);
|
||||
```
|
||||
|
||||
```cpp
|
||||
// src/libxrpl/tx/invariants/InvariantCheckDomain.cpp
|
||||
|
||||
bool hasDomain(STTx const& tx, Domain domain)
|
||||
{
|
||||
switch (tx.getTxnType())
|
||||
{
|
||||
#pragma push_macro("TRANSACTION")
|
||||
#undef TRANSACTION
|
||||
#define TRANSACTION(tag, value, name, delegable, amend, priv, txDomain, fields) \
|
||||
case tag: \
|
||||
return (txDomain & domain) != noDomain;
|
||||
#include <xrpl/protocol/detail/transactions.macro>
|
||||
#undef TRANSACTION
|
||||
#pragma pop_macro("TRANSACTION")
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This allows domain invariant classes to use `hasDomain(tx, vaultDomain)` in their domain-wide
|
||||
checks, analogous to how existing invariants use `hasPrivilege(tx, mustModifyVault)`.
|
||||
|
||||
### Proposal A vs Proposal B
|
||||
|
||||
| Aspect | A: Traits | B: Macro domain field |
|
||||
| ---------------------------- | ----------------------------------- | ---------------------------------------- |
|
||||
| Opt-in location | Scattered across transactor headers | Centralized in `transactions.macro` |
|
||||
| Single source of truth | No — trait + macro both describe tx | Yes — macro is the one place |
|
||||
| Follows existing patterns | New pattern (traits) | Extends existing pattern (`privileges`) |
|
||||
| New transactor | Add trait in header | Add domain flag in macro |
|
||||
| New domain | Add `domainFor<>` + tuple helper | Add `domainFor<>` + enum value |
|
||||
| Macro migration | None | One-time: update `TxFormats.cpp` |
|
||||
| Compile-time enforcement | Same (`if constexpr` → hard error) | Same (`if constexpr` → hard error) |
|
||||
| Auditability | Grep headers for specializations | Read one macro file |
|
||||
| Runtime query | N/A | `hasDomain()` parallels `hasPrivilege()` |
|
||||
| No transactor header changes | Trait specialization needed | Only `checkInvariants` declaration |
|
||||
|
||||
Proposal B is **recommended** because it follows the established `privileges` pattern, keeps
|
||||
all transaction metadata in one file, and requires fewer new concepts (no traits template, no
|
||||
`tuple_contains` helper).
|
||||
Reference in New Issue
Block a user