mirror of
https://github.com/Xahau/xahaud.git
synced 2026-04-29 15:37:46 +00:00
Compare commits
13 Commits
backport-l
...
hook-helpe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04c2929883 | ||
|
|
cd00ed72d8 | ||
|
|
05a3e04f2d | ||
|
|
66f7294120 | ||
|
|
7f6ac75617 | ||
|
|
4150f0383c | ||
|
|
25123b370a | ||
|
|
63096d5fbc | ||
|
|
2e128acdcf | ||
|
|
043c60b62e | ||
|
|
5dd1198e4f | ||
|
|
5d9071695a | ||
|
|
ec6dc93834 |
4
.github/workflows/levelization.yml
vendored
4
.github/workflows/levelization.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Check levelization
|
||||
run: Builds/levelization/levelization.sh
|
||||
run: python Builds/levelization/levelization.py
|
||||
- name: Check for differences
|
||||
id: assert
|
||||
run: |
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
To fix it, you can do one of two things:
|
||||
1. Download and apply the patch generated as an artifact of this
|
||||
job to your repo, commit, and push.
|
||||
2. Run './Builds/levelization/levelization.sh' in your repo,
|
||||
2. Run 'python Builds/levelization/levelization.py' in your repo,
|
||||
commit, and push.
|
||||
|
||||
See Builds/levelization/README.md for more info.
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -53,6 +53,9 @@ Builds/levelization/results/paths.txt
|
||||
Builds/levelization/results/includes/
|
||||
Builds/levelization/results/includedby/
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
|
||||
# Ignore tmp directory.
|
||||
tmp
|
||||
|
||||
@@ -126,3 +129,6 @@ generated
|
||||
|
||||
# Suggested in-tree build directory
|
||||
/.build/
|
||||
|
||||
guard_checker
|
||||
guard_checker.dSYM
|
||||
|
||||
@@ -50,7 +50,7 @@ that `test` code should *never* be included in `ripple` code.)
|
||||
|
||||
## Validation
|
||||
|
||||
The [levelization.sh](levelization.sh) script takes no parameters,
|
||||
The [levelization.py](levelization.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
|
||||
@@ -84,7 +84,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 `levelization.py`,
|
||||
and commit the updated results.
|
||||
|
||||
The `loops.txt` and `ordering.txt` files relate the modules
|
||||
@@ -108,7 +108,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 `levelization.py`
|
||||
2. Grep the modules in `paths.txt`.
|
||||
* For example, if a cycle is found `A ~= B`, simply `grep -w
|
||||
A Builds/levelization/results/paths.txt | grep -w B`
|
||||
|
||||
283
Builds/levelization/levelization.py
Executable file
283
Builds/levelization/levelization.py
Executable file
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Usage: levelization.py
|
||||
This script takes no parameters, and can be called from any directory in the file system.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
# 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):
|
||||
"""
|
||||
Create a sort key that mimics 'sort -d' (dictionary order).
|
||||
Dictionary order only considers blanks and alphanumeric characters.
|
||||
"""
|
||||
return "".join(c for c in s if c.isalnum() or c.isspace())
|
||||
|
||||
|
||||
def get_level(file_path):
|
||||
"""
|
||||
Extract the level from a file path (second and third directory components).
|
||||
Equivalent to bash: cut -d/ -f 2,3
|
||||
|
||||
Examples:
|
||||
src/ripple/app/main.cpp -> ripple.app
|
||||
src/test/app/Import_test.cpp -> test.app
|
||||
"""
|
||||
parts = file_path.split("/")
|
||||
|
||||
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]:
|
||||
# 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):
|
||||
"""
|
||||
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 <ripple/basics/base_uint.h> -> ripple.basics
|
||||
#include "ripple/app/main/Application.h" -> ripple.app
|
||||
"""
|
||||
match = INCLUDE_PATH_PATTERN.search(include_line)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
include_path = match.group(1)
|
||||
parts = include_path.split("/")
|
||||
|
||||
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]:
|
||||
include_level = include_level.rsplit("/", 1)[0] + "/toplevel"
|
||||
|
||||
return include_level.replace("/", ".")
|
||||
|
||||
|
||||
def find_repository_directories(start_path, depth_limit=10):
|
||||
"""
|
||||
Find the repository root by looking for src or include folders.
|
||||
Walks up the directory tree from the start path.
|
||||
"""
|
||||
current = start_path.resolve()
|
||||
|
||||
for _ in range(depth_limit):
|
||||
src_path = current / "src"
|
||||
include_path = current / "include"
|
||||
has_src = src_path.exists()
|
||||
has_include = include_path.exists()
|
||||
|
||||
if has_src or has_include:
|
||||
dirs = []
|
||||
if has_src:
|
||||
dirs.append(src_path)
|
||||
if has_include:
|
||||
dirs.append(include_path)
|
||||
return current, dirs
|
||||
|
||||
parent = current.parent
|
||||
if parent == current:
|
||||
break
|
||||
current = parent
|
||||
|
||||
raise RuntimeError(
|
||||
"Could not find repository root. "
|
||||
"Expected to find a directory containing 'src' and/or 'include' folders."
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
script_dir = Path(__file__).parent.resolve()
|
||||
os.chdir(script_dir)
|
||||
|
||||
# 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.
|
||||
try:
|
||||
repo_root, scan_dirs = find_repository_directories(script_dir)
|
||||
print(f"Found repository root: {repo_root}")
|
||||
for scan_dir in scan_dirs:
|
||||
print(f" Scanning: {scan_dir.relative_to(repo_root)}")
|
||||
except RuntimeError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Find all #include directives.
|
||||
print("\nScanning for raw includes...")
|
||||
raw_includes = []
|
||||
rawincludes_file = results_dir / "rawincludes.txt"
|
||||
|
||||
with open(rawincludes_file, "w", buffering=8192) as raw_f:
|
||||
for dir_path in scan_dirs:
|
||||
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))
|
||||
with open(
|
||||
file_path, "r", encoding="utf-8", errors="ignore", buffering=8192
|
||||
) as f:
|
||||
for line in f:
|
||||
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.
|
||||
print("Build levelization paths")
|
||||
path_counts = defaultdict(int)
|
||||
|
||||
for file_path, include_line in raw_includes:
|
||||
include_level = extract_include_level(include_line)
|
||||
if not include_level:
|
||||
continue
|
||||
level = get_level(file_path)
|
||||
if level != include_level:
|
||||
path_counts[(level, include_level)] += 1
|
||||
|
||||
# Sort and deduplicate paths.
|
||||
print("Sort and deduplicate paths")
|
||||
sorted_items = sorted(
|
||||
path_counts.items(),
|
||||
key=lambda x: (dictionary_sort_key(x[0][0]), dictionary_sort_key(x[0][1])),
|
||||
)
|
||||
|
||||
paths_file = results_dir / "paths.txt"
|
||||
with open(paths_file, "w") as f:
|
||||
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"
|
||||
includedby_dir = results_dir / "includedby"
|
||||
includes_dir.mkdir()
|
||||
includedby_dir.mkdir()
|
||||
|
||||
includes_data = defaultdict(list)
|
||||
includedby_data = defaultdict(list)
|
||||
|
||||
for (level, include_level), count in sorted_items:
|
||||
includes_data[level].append((include_level, count))
|
||||
includedby_data[include_level].append((level, count))
|
||||
|
||||
for level in sorted(includes_data.keys(), key=dictionary_sort_key):
|
||||
with open(includes_dir / level, "w") as f:
|
||||
for include_level, count in includes_data[level]:
|
||||
line = f"{include_level} {count}\n"
|
||||
print(line.rstrip())
|
||||
f.write(line)
|
||||
|
||||
for include_level in sorted(includedby_data.keys(), key=dictionary_sort_key):
|
||||
with open(includedby_dir / include_level, "w") as f:
|
||||
for level, count in includedby_data[include_level]:
|
||||
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"
|
||||
|
||||
# Pre-load all include files into memory for fast lookup.
|
||||
includes_cache = {}
|
||||
includes_lookup = {}
|
||||
|
||||
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:
|
||||
name, count = parts[0], int(parts[1])
|
||||
includes_cache[include_file.name].append((name, count))
|
||||
includes_lookup[include_file.name][name] = count
|
||||
|
||||
loops_found = set()
|
||||
|
||||
with open(loops_file, "w", buffering=8192) as loops_f, open(
|
||||
ordering_file, "w", buffering=8192
|
||||
) as ordering_f:
|
||||
for source in sorted(includes_cache.keys()):
|
||||
for include, include_freq in includes_cache[source]:
|
||||
if include not in includes_lookup:
|
||||
continue
|
||||
|
||||
source_freq = includes_lookup[include].get(source)
|
||||
|
||||
if source_freq is not None:
|
||||
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")
|
||||
|
||||
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()
|
||||
@@ -1,130 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Usage: levelization.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 includedby
|
||||
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 dedup 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 includedby/${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
|
||||
@@ -12,7 +12,7 @@ The server software that powers Xahau is called `xahaud` and is available in thi
|
||||
|
||||
### Build from Source
|
||||
|
||||
* [Read the build instructions in our documentation](https://xahau.network/infrastructure/building-xahau)
|
||||
* [Read the build instructions in our documentation](https://xahau.network/docs/infrastructure/build-xahaud/)
|
||||
* If you encounter any issues, please [open an issue](https://github.com/xahau/xahaud/issues)
|
||||
|
||||
## Highlights of Xahau
|
||||
|
||||
@@ -68,6 +68,17 @@ target_link_libraries(xrpl.imports.main
|
||||
$<$<BOOL:${voidstar}>:antithesis-sdk-cpp>
|
||||
)
|
||||
|
||||
# date-tz for enhanced logging (always linked, code is #ifdef guarded)
|
||||
if(TARGET date::date-tz)
|
||||
target_link_libraries(xrpl.imports.main INTERFACE date::date-tz)
|
||||
endif()
|
||||
|
||||
# BEAST_ENHANCED_LOGGING: enable for Debug builds OR when explicitly requested
|
||||
# Uses generator expression so it works with multi-config generators (Xcode, VS, Ninja Multi-Config)
|
||||
target_compile_definitions(xrpl.imports.main INTERFACE
|
||||
$<$<OR:$<CONFIG:Debug>,$<BOOL:${BEAST_ENHANCED_LOGGING}>>:BEAST_ENHANCED_LOGGING=1>
|
||||
)
|
||||
|
||||
include(add_module)
|
||||
include(target_link_modules)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <set>
|
||||
#include <stack>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -282,7 +283,8 @@ check_guard(
|
||||
* might have unforeseen consequences, without also rolling back further
|
||||
* changes that are fine.
|
||||
*/
|
||||
uint64_t rulesVersion = 0
|
||||
uint64_t rulesVersion = 0,
|
||||
std::set<int>* out_callees = nullptr
|
||||
|
||||
)
|
||||
{
|
||||
@@ -492,17 +494,27 @@ check_guard(
|
||||
{
|
||||
REQUIRE(1);
|
||||
uint64_t callee_idx = LEB();
|
||||
// disallow calling of user defined functions inside a hook
|
||||
|
||||
// record user-defined function calls if tracking is enabled
|
||||
if (callee_idx > last_import_idx)
|
||||
{
|
||||
GUARDLOG(hook::log::CALL_ILLEGAL)
|
||||
<< "GuardCheck "
|
||||
<< "Hook calls a function outside of the whitelisted "
|
||||
"imports "
|
||||
<< "codesec: " << codesec << " hook byte offset: " << i
|
||||
<< "\n";
|
||||
if (out_callees != nullptr)
|
||||
{
|
||||
// record the callee for call graph analysis
|
||||
out_callees->insert(callee_idx);
|
||||
}
|
||||
else
|
||||
{
|
||||
// if not tracking, maintain original behavior: reject
|
||||
GUARDLOG(hook::log::CALL_ILLEGAL)
|
||||
<< "GuardCheck "
|
||||
<< "Hook calls a function outside of the whitelisted "
|
||||
"imports "
|
||||
<< "codesec: " << codesec << " hook byte offset: " << i
|
||||
<< "\n";
|
||||
|
||||
return {};
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// enforce guard call limit
|
||||
@@ -838,6 +850,42 @@ validateGuards(
|
||||
*/
|
||||
uint64_t rulesVersion = 0x00)
|
||||
{
|
||||
// Structure to track function call graph information
|
||||
struct FunctionInfo
|
||||
{
|
||||
int func_idx;
|
||||
std::set<int> callees; // functions this function calls
|
||||
std::set<int> callers; // functions that call this function
|
||||
bool has_loops; // whether this function contains loops
|
||||
uint64_t local_wce; // local worst-case execution count
|
||||
uint64_t total_wce; // total WCE including callees
|
||||
bool wce_calculated; // whether total_wce has been computed
|
||||
bool in_calculation; // for cycle detection in WCE calculation
|
||||
|
||||
FunctionInfo()
|
||||
: func_idx(-1)
|
||||
, has_loops(false)
|
||||
, local_wce(0)
|
||||
, total_wce(0)
|
||||
, wce_calculated(false)
|
||||
, in_calculation(false)
|
||||
{
|
||||
}
|
||||
|
||||
FunctionInfo(int idx, uint64_t local_wce_val, bool has_loops_val)
|
||||
: func_idx(idx)
|
||||
, has_loops(has_loops_val)
|
||||
, local_wce(local_wce_val)
|
||||
, total_wce(0)
|
||||
, wce_calculated(false)
|
||||
, in_calculation(false)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
// Call graph: maps function index to its information
|
||||
std::map<int, FunctionInfo> call_graph;
|
||||
|
||||
uint64_t byteCount = wasm.size();
|
||||
|
||||
// 63 bytes is the smallest possible valid hook wasm
|
||||
@@ -1170,6 +1218,12 @@ validateGuards(
|
||||
if (DEBUG_GUARD)
|
||||
printf("Function map: func %d -> type %d\n", j, type_idx);
|
||||
func_type_map[j] = type_idx;
|
||||
|
||||
// Step 4: Initialize FunctionInfo for each user-defined
|
||||
// function func_idx starts from last_import_number + 1
|
||||
int actual_func_idx = last_import_number + 1 + j;
|
||||
call_graph[actual_func_idx] = FunctionInfo();
|
||||
call_graph[actual_func_idx].func_idx = actual_func_idx;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1211,9 +1265,6 @@ validateGuards(
|
||||
return {};
|
||||
}
|
||||
|
||||
int64_t maxInstrCountHook = 0;
|
||||
int64_t maxInstrCountCbak = 0;
|
||||
|
||||
// second pass... where we check all the guard function calls follow the
|
||||
// guard rules minimal other validation in this pass because first pass
|
||||
// caught most of it
|
||||
@@ -1247,6 +1298,7 @@ validateGuards(
|
||||
std::optional<
|
||||
std::reference_wrapper<std::vector<uint8_t> const>>
|
||||
first_signature;
|
||||
bool helper_function = false;
|
||||
if (auto const& usage = import_type_map.find(j);
|
||||
usage != import_type_map.end())
|
||||
{
|
||||
@@ -1278,7 +1330,7 @@ validateGuards(
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (j == hook_type_idx)
|
||||
else if (j == hook_type_idx) // hook() or cbak() function type
|
||||
{
|
||||
// pass
|
||||
}
|
||||
@@ -1291,7 +1343,8 @@ validateGuards(
|
||||
<< "Codesec: " << section_type << " "
|
||||
<< "Local: " << j << " "
|
||||
<< "Offset: " << i << "\n";
|
||||
return {};
|
||||
// return {};
|
||||
helper_function = true;
|
||||
}
|
||||
|
||||
int param_count = parseLeb128(wasm, i, &i);
|
||||
@@ -1308,12 +1361,19 @@ validateGuards(
|
||||
return {};
|
||||
}
|
||||
}
|
||||
else if (helper_function)
|
||||
{
|
||||
// pass
|
||||
}
|
||||
else if (param_count != (*first_signature).get().size() - 1)
|
||||
{
|
||||
GUARDLOG(hook::log::FUNC_TYPE_INVALID)
|
||||
<< "Malformed transaction. "
|
||||
<< "Hook API: " << *first_name
|
||||
<< " has the wrong number of parameters.\n";
|
||||
<< " has the wrong number of parameters.\n"
|
||||
<< "param_count: " << param_count << " "
|
||||
<< "first_signature: "
|
||||
<< (*first_signature).get().size() - 1 << "\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1360,6 +1420,10 @@ validateGuards(
|
||||
return {};
|
||||
}
|
||||
}
|
||||
else if (helper_function)
|
||||
{
|
||||
// pass
|
||||
}
|
||||
else if ((*first_signature).get()[k + 1] != param_type)
|
||||
{
|
||||
GUARDLOG(hook::log::FUNC_PARAM_INVALID)
|
||||
@@ -1436,6 +1500,10 @@ validateGuards(
|
||||
return {};
|
||||
}
|
||||
}
|
||||
else if (helper_function)
|
||||
{
|
||||
// pass
|
||||
}
|
||||
else if ((*first_signature).get()[0] != result_type)
|
||||
{
|
||||
GUARDLOG(hook::log::FUNC_RETURN_INVALID)
|
||||
@@ -1487,6 +1555,17 @@ validateGuards(
|
||||
// execution to here means we are up to the actual expr for the
|
||||
// codesec/function
|
||||
|
||||
// Step 5: Calculate actual function index and prepare callees
|
||||
// tracking
|
||||
int actual_func_idx = last_import_number + 1 + j;
|
||||
std::set<int>* out_callees_ptr = nullptr;
|
||||
|
||||
// Only track callees if this function is in the call_graph
|
||||
if (call_graph.find(actual_func_idx) != call_graph.end())
|
||||
{
|
||||
out_callees_ptr = &call_graph[actual_func_idx].callees;
|
||||
}
|
||||
|
||||
auto valid = check_guard(
|
||||
wasm,
|
||||
j,
|
||||
@@ -1496,33 +1575,188 @@ validateGuards(
|
||||
last_import_number,
|
||||
guardLog,
|
||||
guardLogAccStr,
|
||||
rulesVersion);
|
||||
rulesVersion,
|
||||
out_callees_ptr);
|
||||
|
||||
if (!valid)
|
||||
return {};
|
||||
|
||||
if (hook_func_idx && *hook_func_idx == j)
|
||||
maxInstrCountHook = *valid;
|
||||
else if (cbak_func_idx && *cbak_func_idx == j)
|
||||
maxInstrCountCbak = *valid;
|
||||
else
|
||||
// Step 5: Store local WCE and build bidirectional call
|
||||
// relationships
|
||||
if (call_graph.find(actual_func_idx) != call_graph.end())
|
||||
{
|
||||
if (DEBUG_GUARD)
|
||||
printf(
|
||||
"code section: %d not hook_func_idx: %d or "
|
||||
"cbak_func_idx: %d\n",
|
||||
j,
|
||||
*hook_func_idx,
|
||||
(cbak_func_idx ? *cbak_func_idx : -1));
|
||||
// assert(false);
|
||||
call_graph[actual_func_idx].local_wce = *valid;
|
||||
|
||||
// Build bidirectional relationships: for each callee, add
|
||||
// this function as a caller
|
||||
for (int callee_idx : call_graph[actual_func_idx].callees)
|
||||
{
|
||||
if (call_graph.find(callee_idx) != call_graph.end())
|
||||
{
|
||||
call_graph[callee_idx].callers.insert(
|
||||
actual_func_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We will calculate total WCE later after processing all
|
||||
// functions
|
||||
i = code_end;
|
||||
}
|
||||
}
|
||||
i = next_section;
|
||||
}
|
||||
|
||||
// execution to here means guards are installed correctly
|
||||
// Step 6: Cycle detection using DFS
|
||||
// Lambda function for DFS-based cycle detection
|
||||
std::set<int> visited;
|
||||
std::set<int> rec_stack;
|
||||
std::function<bool(int)> detect_cycles_dfs = [&](int func_idx) -> bool {
|
||||
if (rec_stack.find(func_idx) != rec_stack.end())
|
||||
{
|
||||
// Found a cycle: func_idx is already in the recursion stack
|
||||
return true;
|
||||
}
|
||||
|
||||
return std::pair<uint64_t, uint64_t>{maxInstrCountHook, maxInstrCountCbak};
|
||||
if (visited.find(func_idx) != visited.end())
|
||||
{
|
||||
// Already visited and no cycle found from this node
|
||||
return false;
|
||||
}
|
||||
|
||||
visited.insert(func_idx);
|
||||
rec_stack.insert(func_idx);
|
||||
|
||||
// Check all callees
|
||||
if (call_graph.find(func_idx) != call_graph.end())
|
||||
{
|
||||
for (int callee_idx : call_graph[func_idx].callees)
|
||||
{
|
||||
if (detect_cycles_dfs(callee_idx))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rec_stack.erase(func_idx);
|
||||
return false;
|
||||
};
|
||||
|
||||
// Run cycle detection on all user-defined functions
|
||||
for (const auto& [func_idx, func_info] : call_graph)
|
||||
{
|
||||
if (detect_cycles_dfs(func_idx))
|
||||
{
|
||||
GUARDLOG(hook::log::CALL_ILLEGAL)
|
||||
<< "GuardCheck: Recursive function calls detected. "
|
||||
<< "Hooks cannot contain recursive or mutually recursive "
|
||||
"functions.\n";
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Step 7: Calculate total WCE for each function using bottom-up approach
|
||||
// Lambda function for recursive WCE calculation with memoization
|
||||
std::function<uint64_t(int)> calculate_function_wce =
|
||||
[&](int func_idx) -> uint64_t {
|
||||
// Check if function exists in call graph
|
||||
if (call_graph.find(func_idx) == call_graph.end())
|
||||
{
|
||||
// This is an imported function, WCE = 0 (already accounted for)
|
||||
return 0;
|
||||
}
|
||||
|
||||
FunctionInfo& func_info = call_graph[func_idx];
|
||||
|
||||
// If already calculated, return cached result
|
||||
if (func_info.wce_calculated)
|
||||
{
|
||||
return func_info.total_wce;
|
||||
}
|
||||
|
||||
// Detect circular dependency in WCE calculation (should not happen
|
||||
// after cycle detection)
|
||||
if (func_info.in_calculation)
|
||||
{
|
||||
GUARDLOG(hook::log::CALL_ILLEGAL)
|
||||
<< "GuardCheck: Internal error - circular dependency detected "
|
||||
"during WCE calculation.\n";
|
||||
return 0xFFFFFFFFU; // Return large value to trigger overflow error
|
||||
}
|
||||
|
||||
func_info.in_calculation = true;
|
||||
|
||||
// Start with local WCE
|
||||
uint64_t total = func_info.local_wce;
|
||||
|
||||
// Add WCE of all callees
|
||||
for (int callee_idx : func_info.callees)
|
||||
{
|
||||
uint64_t callee_wce = calculate_function_wce(callee_idx);
|
||||
|
||||
// Check for overflow
|
||||
if (total > 0xFFFFU || callee_wce > 0xFFFFU ||
|
||||
(total + callee_wce) > 0xFFFFU)
|
||||
{
|
||||
func_info.in_calculation = false;
|
||||
return 0xFFFFFFFFU; // Signal overflow
|
||||
}
|
||||
|
||||
total += callee_wce;
|
||||
}
|
||||
|
||||
func_info.total_wce = total;
|
||||
func_info.wce_calculated = true;
|
||||
func_info.in_calculation = false;
|
||||
|
||||
return total;
|
||||
};
|
||||
|
||||
// Calculate WCE for hook and cbak functions
|
||||
int64_t hook_wce_actual = 0;
|
||||
int64_t cbak_wce_actual = 0;
|
||||
|
||||
if (hook_func_idx)
|
||||
{
|
||||
int actual_hook_idx = last_import_number + 1 + *hook_func_idx;
|
||||
hook_wce_actual = calculate_function_wce(actual_hook_idx);
|
||||
|
||||
if (hook_wce_actual >= 0xFFFFU)
|
||||
{
|
||||
GUARDLOG(hook::log::INSTRUCTION_EXCESS)
|
||||
<< "GuardCheck: hook() function exceeds maximum instruction "
|
||||
"count (65535). "
|
||||
<< "Total WCE including called functions: " << hook_wce_actual
|
||||
<< "\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
if (DEBUG_GUARD)
|
||||
printf("hook() total WCE: %ld\n", hook_wce_actual);
|
||||
}
|
||||
|
||||
if (cbak_func_idx)
|
||||
{
|
||||
int actual_cbak_idx = last_import_number + 1 + *cbak_func_idx;
|
||||
cbak_wce_actual = calculate_function_wce(actual_cbak_idx);
|
||||
|
||||
if (cbak_wce_actual >= 0xFFFFU)
|
||||
{
|
||||
GUARDLOG(hook::log::INSTRUCTION_EXCESS)
|
||||
<< "GuardCheck: cbak() function exceeds maximum instruction "
|
||||
"count (65535). "
|
||||
<< "Total WCE including called functions: " << cbak_wce_actual
|
||||
<< "\n";
|
||||
return {};
|
||||
}
|
||||
|
||||
if (DEBUG_GUARD)
|
||||
printf("cbak() total WCE: %ld\n", cbak_wce_actual);
|
||||
}
|
||||
|
||||
// execution to here means guards are installed correctly and WCE is within
|
||||
// limits
|
||||
|
||||
return std::pair<uint64_t, uint64_t>{hook_wce_actual, cbak_wce_actual};
|
||||
}
|
||||
|
||||
@@ -2971,6 +2971,809 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testHelperFunctions(FeatureBitset features)
|
||||
{
|
||||
testcase("Test helper functions and recursion detection");
|
||||
using namespace jtx;
|
||||
Env env{*this, features};
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// Test 1: Valid helper function without loops - should pass
|
||||
{
|
||||
/*
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t
|
||||
error_code);
|
||||
extern int64_t hook_pos(void);
|
||||
|
||||
int64_t helper(int64_t n) { return n + hook_pos(); }
|
||||
|
||||
int64_t cbak(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = helper(34);
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
|
||||
int64_t hook(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = helper(5);
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
*/
|
||||
TestHook hook_wasm = wasm[R"[test.hook](
|
||||
(module
|
||||
(type (;0;) (func (param i32) (result i64)))
|
||||
(type (;1;) (func (result i64)))
|
||||
(type (;2;) (func (param i32 i32) (result i32)))
|
||||
(type (;3;) (func (param i32 i32 i64) (result i64)))
|
||||
(type (;4;) (func (param i64) (result i64)))
|
||||
(import "env" "hook_pos" (func (;0;) (type 1)))
|
||||
(import "env" "_g" (func (;1;) (type 2)))
|
||||
(import "env" "accept" (func (;2;) (type 3)))
|
||||
(func (;3;) (type 4) (param i64) (result i64)
|
||||
call 0
|
||||
local.get 0
|
||||
i64.add)
|
||||
(func (;4;) (type 0) (param i32) (result i64)
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call 1
|
||||
drop
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
i64.const 34
|
||||
call 3
|
||||
call 2)
|
||||
(func (;5;) (type 0) (param i32) (result i64)
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call 1
|
||||
drop
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
i64.const 5
|
||||
call 3
|
||||
call 2)
|
||||
(memory (;0;) 2)
|
||||
(export "memory" (memory 0))
|
||||
(export "cbak" (func 4))
|
||||
(export "hook" (func 5)))
|
||||
)[test.hook]"];
|
||||
HASH_WASM(hook);
|
||||
|
||||
env(ripple::test::jtx::hook(
|
||||
alice, {{hso(hook_wasm, overrideFlag)}}, 0),
|
||||
M("Valid helper function without loops"),
|
||||
HSFEE,
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
EXPECT_HOOK_FEE(hook, 14);
|
||||
|
||||
env(pay(bob, alice, XRP(1)), M("Test helper 1"), fee(XRP(1)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Test 2: Helper function with guarded loop - should pass
|
||||
{
|
||||
/*
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t
|
||||
error_code); extern int64_t hook_pos(void);
|
||||
|
||||
int64_t helper(int64_t n) {
|
||||
int64_t sum = 0;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
_g(2, 4);
|
||||
sum += i * n;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
int64_t cbak(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = helper(2);
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
|
||||
int64_t hook(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = helper(3);
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
*/
|
||||
TestHook hook_wasm = wasm[R"[test.hook](
|
||||
(module
|
||||
(type (;0;) (func (param i32) (result i64)))
|
||||
(type (;1;) (func (param i32 i32) (result i32)))
|
||||
(type (;2;) (func (param i32 i32 i64) (result i64)))
|
||||
(type (;3;) (func (param i64) (result i64)))
|
||||
(import "env" "_g" (func (;0;) (type 1)))
|
||||
(import "env" "accept" (func (;1;) (type 2)))
|
||||
(func (;2;) (type 0) (param i32) (result i64)
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call 0
|
||||
drop
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
i64.const 3
|
||||
call 3
|
||||
call 1)
|
||||
(func (;3;) (type 3) (param i64) (result i64)
|
||||
i32.const 2
|
||||
i32.const 4
|
||||
call 0
|
||||
drop
|
||||
i32.const 2
|
||||
i32.const 4
|
||||
call 0
|
||||
drop
|
||||
i32.const 2
|
||||
i32.const 4
|
||||
call 0
|
||||
drop
|
||||
local.get 0
|
||||
i64.const 3
|
||||
i64.mul)
|
||||
(func (;4;) (type 0) (param i32) (result i64)
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call 0
|
||||
drop
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
i64.const 2
|
||||
call 3
|
||||
call 1)
|
||||
(memory (;0;) 2)
|
||||
(export "memory" (memory 0))
|
||||
(export "hook" (func 2))
|
||||
(export "cbak" (func 4)))
|
||||
)[test.hook]"];
|
||||
HASH_WASM(hook);
|
||||
|
||||
env(ripple::test::jtx::hook(
|
||||
alice, {{hso(hook_wasm, overrideFlag)}}, 0),
|
||||
M("Helper function with guarded loop"),
|
||||
HSFEE,
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
EXPECT_HOOK_FEE(hook, 26);
|
||||
|
||||
env(pay(bob, alice, XRP(1)), M("Test helper 2"), fee(XRP(1)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Test 3: Direct recursion - should fail
|
||||
{
|
||||
/*
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t
|
||||
error_code);
|
||||
extern int64_t hook_pos(void);
|
||||
|
||||
int64_t recursive_func(int64_t n) {
|
||||
if (n <= 0)
|
||||
return 0;
|
||||
return n + recursive_func(n - hook_pos());
|
||||
}
|
||||
int64_t cbak(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = recursive_func(5);
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
|
||||
int64_t hook(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = recursive_func(10);
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
|
||||
*/
|
||||
TestHook hook = wasm[R"[test.hook](
|
||||
(module
|
||||
(type (;0;) (func (param i32) (result i64)))
|
||||
(type (;1;) (func (param i32 i32) (result i32)))
|
||||
(type (;2;) (func (param i32 i32 i64) (result i64)))
|
||||
(type (;3;) (func (result i64)))
|
||||
(type (;4;) (func (param i64) (result i64)))
|
||||
(import "env" "_g" (func $g (type 1)))
|
||||
(import "env" "accept" (func $accept (type 2)))
|
||||
(import "env" "hook_pos" (func $hook_pos (type 3)))
|
||||
(func $recursive_func (type 4) (param $n i64) (result i64)
|
||||
(if (result i64)
|
||||
(i64.le_s (local.get $n) (i64.const 0))
|
||||
(then
|
||||
(i64.const 0)
|
||||
)
|
||||
(else
|
||||
(i64.add
|
||||
(local.get $n)
|
||||
(call $recursive_func
|
||||
(i64.sub (local.get $n) (call $hook_pos))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(func (;3;) (type 0) (param i32) (result i64) ;; cbak
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call $g
|
||||
drop
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
i64.const 5
|
||||
call $recursive_func
|
||||
call $accept
|
||||
)
|
||||
(func (;5;) (type 0) (param i32) (result i64) ;; hook
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call $g
|
||||
drop
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
i64.const 10
|
||||
call $recursive_func
|
||||
call $accept
|
||||
)
|
||||
(memory (;0;) 2)
|
||||
(export "memory" (memory 0))
|
||||
(export "cbak" (func 3))
|
||||
(export "hook" (func 5)))
|
||||
)[test.hook]"];
|
||||
|
||||
env(ripple::test::jtx::hook(alice, {{hso(hook)}}, 0),
|
||||
M("Direct recursion should fail"),
|
||||
HSFEE,
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Test 4: Indirect recursion (A -> B -> A) - should fail
|
||||
{
|
||||
/*
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t
|
||||
error_code);
|
||||
|
||||
int64_t func_b(int64_t n);
|
||||
|
||||
int64_t func_a(int64_t n) {
|
||||
if (n <= 0)
|
||||
return 0;
|
||||
return n + func_b(n - 1);
|
||||
}
|
||||
|
||||
int64_t func_b(int64_t n) {
|
||||
if (n <= 0)
|
||||
return 0;
|
||||
return n + func_a(n - 1);
|
||||
}
|
||||
|
||||
int64_t cbak(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = func_a(5);
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
|
||||
int64_t hook(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = func_a(10);
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
|
||||
*/
|
||||
TestHook hook = wasm[R"[test.hook](
|
||||
(module
|
||||
(import "env" "_g" (func $_g (param i32 i32) (result i32)))
|
||||
(import "env" "accept" (func $accept (param i32 i32 i64) (result i64)))
|
||||
(type $func_type (func (param i64) (result i64)))
|
||||
(func $func_b (param $n i64) (result i64)
|
||||
(if (result i64)
|
||||
(i64.le_s (local.get $n) (i64.const 0))
|
||||
(then
|
||||
(i64.const 0)
|
||||
)
|
||||
(else
|
||||
(i64.add
|
||||
(local.get $n)
|
||||
(call $func_a
|
||||
(i64.sub (local.get $n) (i64.const 1))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(func $func_a (param $n i64) (result i64)
|
||||
(if (result i64)
|
||||
(i64.le_s (local.get $n) (i64.const 0))
|
||||
(then
|
||||
(i64.const 0)
|
||||
)
|
||||
(else
|
||||
(i64.add
|
||||
(local.get $n)
|
||||
(call $func_b
|
||||
(i64.sub (local.get $n) (i64.const 1))
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(func $cbak (param $reserved i32) (result i64)
|
||||
(local $result i64)
|
||||
(drop (call $_g (i32.const 1) (i32.const 1)))
|
||||
(local.set $result (call $func_a (i64.const 5)))
|
||||
(call $accept (i32.const 0) (i32.const 0) (local.get $result))
|
||||
)
|
||||
(func $hook (param $reserved i32) (result i64)
|
||||
(local $result i64)
|
||||
(drop (call $_g (i32.const 1) (i32.const 1)))
|
||||
(local.set $result (call $func_a (i64.const 10)))
|
||||
(call $accept (i32.const 0) (i32.const 0) (local.get $result))
|
||||
)
|
||||
(export "cbak" (func $cbak))
|
||||
(export "hook" (func $hook)))
|
||||
)[test.hook]"];
|
||||
|
||||
env(ripple::test::jtx::hook(alice, {{hso(hook)}}, 0),
|
||||
M("Indirect recursion should fail"),
|
||||
HSFEE,
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Test 5: Deep call chain (A -> B -> C -> D) - should pass if WCE is OK
|
||||
{
|
||||
/*
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t
|
||||
error_code);
|
||||
extern int64_t hook_pos(void);
|
||||
|
||||
int64_t helper(int64_t n) { return n + hook_pos(); }
|
||||
|
||||
int64_t cbak(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = helper(34);
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
|
||||
int64_t hook(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = helper(5);
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
*/
|
||||
TestHook hook_wasm = wasm[R"[test.hook](
|
||||
(module
|
||||
(type (;0;) (func (param i32) (result i64)))
|
||||
(type (;1;) (func (result i64)))
|
||||
(type (;2;) (func (param i32 i32) (result i32)))
|
||||
(type (;3;) (func (param i32 i32 i64) (result i64)))
|
||||
(type (;4;) (func (param i64) (result i64)))
|
||||
(import "env" "hook_pos" (func (;0;) (type 1)))
|
||||
(import "env" "_g" (func (;1;) (type 2)))
|
||||
(import "env" "accept" (func (;2;) (type 3)))
|
||||
(func (;3;) (type 4) (param i64) (result i64)
|
||||
call 0
|
||||
local.get 0
|
||||
i64.add)
|
||||
(func (;4;) (type 0) (param i32) (result i64)
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call 1
|
||||
drop
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
i64.const 34
|
||||
call 3
|
||||
call 2)
|
||||
(func (;5;) (type 0) (param i32) (result i64)
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call 1
|
||||
drop
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
i64.const 5
|
||||
call 3
|
||||
call 2)
|
||||
(memory (;0;) 2)
|
||||
(export "memory" (memory 0))
|
||||
(export "cbak" (func 4))
|
||||
(export "hook" (func 5)))
|
||||
)[test.hook]"];
|
||||
HASH_WASM(hook);
|
||||
|
||||
env(ripple::test::jtx::hook(
|
||||
alice, {{hso(hook_wasm, overrideFlag)}}, 0),
|
||||
M("Deep call chain without recursion"),
|
||||
HSFEE,
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
EXPECT_HOOK_FEE(hook, 14);
|
||||
|
||||
env(pay(bob, alice, XRP(1)), M("Test helper 5"), fee(XRP(1)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Test 6: Helper called multiple times - WCE should accumulate
|
||||
{
|
||||
/*
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t
|
||||
error_code);
|
||||
|
||||
int64_t expensive_helper() {
|
||||
int64_t sum = 0;
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
_g(2, 301);
|
||||
sum += i;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
int64_t hook(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = 0;
|
||||
result += expensive_helper();
|
||||
result += expensive_helper();
|
||||
result += expensive_helper();
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
*/
|
||||
|
||||
TestHook hook_wasm = wasm[R"[test.hook](
|
||||
(module
|
||||
(type (;0;) (func (param i32 i32) (result i32)))
|
||||
(type (;1;) (func (param i32 i32 i64) (result i64)))
|
||||
(type (;2;) (func (result i64)))
|
||||
(type (;3;) (func (param i32) (result i64)))
|
||||
(import "env" "_g" (func (;0;) (type 0)))
|
||||
(import "env" "accept" (func (;1;) (type 1)))
|
||||
(func (;2;) (type 2) (result i64)
|
||||
(local i64)
|
||||
i64.const 100
|
||||
local.set 0
|
||||
loop ;; label = @1
|
||||
i32.const 2
|
||||
i32.const 301
|
||||
call 0
|
||||
drop
|
||||
local.get 0
|
||||
i64.const 1
|
||||
i64.sub
|
||||
local.tee 0
|
||||
i64.eqz
|
||||
i32.eqz
|
||||
br_if 0 (;@1;)
|
||||
end
|
||||
i64.const 4950)
|
||||
(func (;3;) (type 3) (param i32) (result i64)
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call 0
|
||||
drop
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
call 2
|
||||
call 2
|
||||
i64.add
|
||||
call 2
|
||||
i64.add
|
||||
call 1)
|
||||
(memory (;0;) 2)
|
||||
(export "memory" (memory 0))
|
||||
(export "hook" (func 3)))
|
||||
)[test.hook]"];
|
||||
HASH_WASM(hook);
|
||||
|
||||
env(ripple::test::jtx::hook(
|
||||
alice, {{hso(hook_wasm, overrideFlag)}}, 0),
|
||||
M("Helper called multiple times"),
|
||||
HSFEE,
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
EXPECT_HOOK_FEE(hook, 2727);
|
||||
|
||||
env(pay(bob, alice, XRP(1)), M("Test helper 6"), fee(XRP(1)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Test 7: WCE overflow through many helpers - should fail
|
||||
{
|
||||
/*
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t
|
||||
error_code);
|
||||
|
||||
int64_t large_helper(int64_t n) {
|
||||
int64_t sum = n;
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
_g(2, 10001);
|
||||
sum += i;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
int64_t cbak(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = 10;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
_g(3, 11);
|
||||
result += large_helper(10);
|
||||
}
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
|
||||
int64_t hook(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = 0;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
_g(3, 11);
|
||||
result += large_helper(0);
|
||||
}
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
|
||||
*/
|
||||
TestHook hook = wasm[R"[test.hook](
|
||||
(module
|
||||
(type (;0;) (func (param i32) (result i64)))
|
||||
(type (;1;) (func (param i32 i32) (result i32)))
|
||||
(type (;2;) (func (param i32 i32 i64) (result i64)))
|
||||
(type (;3;) (func (param i64) (result i64)))
|
||||
(import "env" "_g" (func (;0;) (type 1)))
|
||||
(import "env" "accept" (func (;1;) (type 2)))
|
||||
(func (;2;) (type 0) (param i32) (result i64)
|
||||
(local i64)
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call 0
|
||||
drop
|
||||
i32.const 10
|
||||
local.set 0
|
||||
i64.const 10
|
||||
local.set 1
|
||||
loop ;; label = @1
|
||||
i32.const 3
|
||||
i32.const 11
|
||||
call 0
|
||||
drop
|
||||
i64.const 10
|
||||
call 3
|
||||
local.get 1
|
||||
i64.add
|
||||
local.set 1
|
||||
local.get 0
|
||||
i32.const 1
|
||||
i32.sub
|
||||
local.tee 0
|
||||
br_if 0 (;@1;)
|
||||
end
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
local.get 1
|
||||
call 1)
|
||||
(func (;3;) (type 3) (param i64) (result i64)
|
||||
(local i64)
|
||||
i64.const 10000
|
||||
local.set 1
|
||||
loop ;; label = @1
|
||||
i32.const 2
|
||||
i32.const 10001
|
||||
call 0
|
||||
drop
|
||||
local.get 1
|
||||
i64.const 1
|
||||
i64.sub
|
||||
local.tee 1
|
||||
i64.eqz
|
||||
i32.eqz
|
||||
br_if 0 (;@1;)
|
||||
end
|
||||
local.get 0
|
||||
i64.const 49995000
|
||||
i64.add)
|
||||
(func (;4;) (type 0) (param i32) (result i64)
|
||||
(local i64)
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call 0
|
||||
drop
|
||||
i32.const 10
|
||||
local.set 0
|
||||
loop ;; label = @1
|
||||
i32.const 3
|
||||
i32.const 11
|
||||
call 0
|
||||
drop
|
||||
i64.const 0
|
||||
call 3
|
||||
local.get 1
|
||||
i64.add
|
||||
local.set 1
|
||||
local.get 0
|
||||
i32.const 1
|
||||
i32.sub
|
||||
local.tee 0
|
||||
br_if 0 (;@1;)
|
||||
end
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
local.get 1
|
||||
call 1)
|
||||
(memory (;0;) 2)
|
||||
(export "memory" (memory 0))
|
||||
(export "cbak" (func 2))
|
||||
(export "hook" (func 4)))
|
||||
)[test.hook]"];
|
||||
|
||||
env(ripple::test::jtx::hook(alice, {{hso(hook)}}, 0),
|
||||
M("WCE overflow through helpers"),
|
||||
HSFEE,
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Test 8: guard inside guard
|
||||
{
|
||||
/*
|
||||
#include <stdint.h>
|
||||
extern int32_t _g(uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t
|
||||
error_code);
|
||||
|
||||
int64_t helper(int64_t n) {
|
||||
int64_t sum = n;
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
_g(2, 1000);
|
||||
sum += i;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
int64_t cbak(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = 10;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
_g(3, 11);
|
||||
result += helper(10);
|
||||
}
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
|
||||
int64_t hook(uint32_t reserved) {
|
||||
_g(1, 1);
|
||||
int64_t result = 0;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
_g(3, 11);
|
||||
result += helper(0);
|
||||
}
|
||||
return accept(0, 0, result);
|
||||
}
|
||||
*/
|
||||
|
||||
TestHook hook_wasm = wasm[R"[test.hook](
|
||||
(module
|
||||
(type (;0;) (func (param i32) (result i64)))
|
||||
(type (;1;) (func (param i32 i32) (result i32)))
|
||||
(type (;2;) (func (param i32 i32 i64) (result i64)))
|
||||
(type (;3;) (func (param i64) (result i64)))
|
||||
(import "env" "_g" (func (;0;) (type 1)))
|
||||
(import "env" "accept" (func (;1;) (type 2)))
|
||||
(func (;2;) (type 3) (param i64) (result i64)
|
||||
(local i64)
|
||||
i64.const 100
|
||||
local.set 1
|
||||
loop ;; label = @1
|
||||
i32.const 2
|
||||
i32.const 1000
|
||||
call 0
|
||||
drop
|
||||
local.get 1
|
||||
i64.const 1
|
||||
i64.sub
|
||||
local.tee 1
|
||||
i64.eqz
|
||||
i32.eqz
|
||||
br_if 0 (;@1;)
|
||||
end
|
||||
local.get 0
|
||||
i64.const 4950
|
||||
i64.add)
|
||||
(func (;3;) (type 0) (param i32) (result i64)
|
||||
(local i64)
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call 0
|
||||
drop
|
||||
i32.const 10
|
||||
local.set 0
|
||||
i64.const 10
|
||||
local.set 1
|
||||
loop ;; label = @1
|
||||
i32.const 3
|
||||
i32.const 11
|
||||
call 0
|
||||
drop
|
||||
i64.const 10
|
||||
call 2
|
||||
local.get 1
|
||||
i64.add
|
||||
local.set 1
|
||||
local.get 0
|
||||
i32.const 1
|
||||
i32.sub
|
||||
local.tee 0
|
||||
br_if 0 (;@1;)
|
||||
end
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
local.get 1
|
||||
call 1)
|
||||
(func (;4;) (type 0) (param i32) (result i64)
|
||||
(local i64)
|
||||
i32.const 1
|
||||
i32.const 1
|
||||
call 0
|
||||
drop
|
||||
i32.const 10
|
||||
local.set 0
|
||||
loop ;; label = @1
|
||||
i32.const 3
|
||||
i32.const 11
|
||||
call 0
|
||||
drop
|
||||
i64.const 0
|
||||
call 2
|
||||
local.get 1
|
||||
i64.add
|
||||
local.set 1
|
||||
local.get 0
|
||||
i32.const 1
|
||||
i32.sub
|
||||
local.tee 0
|
||||
br_if 0 (;@1;)
|
||||
end
|
||||
i32.const 0
|
||||
i32.const 0
|
||||
local.get 1
|
||||
call 1)
|
||||
(memory (;0;) 2)
|
||||
(export "memory" (memory 0))
|
||||
(export "cbak" (func 3))
|
||||
(export "hook" (func 4)))
|
||||
)[test.hook]"];
|
||||
HASH_WASM(hook);
|
||||
|
||||
env(ripple::test::jtx::hook(
|
||||
alice, {{hso(hook_wasm, overrideFlag)}}, 0),
|
||||
M("guard inside guard"),
|
||||
HSFEE,
|
||||
ter(tesSUCCESS));
|
||||
EXPECT_HOOK_FEE(hook, 9151);
|
||||
|
||||
env(pay(bob, alice, XRP(1)), M("Test helper 8"), fee(XRP(1)));
|
||||
env.close();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_emit(FeatureBitset features)
|
||||
{
|
||||
@@ -14729,6 +15532,7 @@ public:
|
||||
test_rollback(features);
|
||||
|
||||
testGuards(features);
|
||||
testHelperFunctions(features);
|
||||
|
||||
test_emit(features); //
|
||||
test_prepare(features);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,8 +58,21 @@ cat $INPUT_FILE | tr '\n' '\f' |
|
||||
then
|
||||
echo '#include "api.h"' > "$WASM_DIR/test-$COUNTER-gen.c"
|
||||
tr '\f' '\n' <<< $line >> "$WASM_DIR/test-$COUNTER-gen.c"
|
||||
DECLARED="`tr '\f' '\n' <<< $line | grep -E '(extern|define) ' | grep -Eo '[a-z\-\_]+ *\(' | grep -v 'sizeof' | sed -E 's/[^a-z\-\_]//g' | sort | uniq`"
|
||||
USED="`tr '\f' '\n' <<< $line | grep -vE '(extern|define) ' | grep -Eo '[a-z\-\_]+\(' | grep -v 'sizeof' | sed -E 's/[^a-z\-\_]//g' | grep -vE '^(hook|cbak)' | sort | uniq`"
|
||||
DECLARED="`tr '\f' '\n' <<< $line \
|
||||
| grep -E '(extern|static|define) ' \
|
||||
| grep -Eo '[a-z\-\_]+ *\(' \
|
||||
| grep -v 'sizeof' \
|
||||
| sed -E 's/[^a-z\-\_]//g' \
|
||||
| grep -vE '^__attribute__$' \
|
||||
| sort | uniq`"
|
||||
|
||||
USED="`tr '\f' '\n' <<< $line \
|
||||
| grep -vE '(extern|static|define) ' \
|
||||
| grep -Eo '[a-z\-\_]+\(' \
|
||||
| grep -v 'sizeof' \
|
||||
| sed -E 's/[^a-z\-\_]//g' \
|
||||
| grep -vE '^(__attribute__|hook|cbak)$' \
|
||||
| sort | uniq`"
|
||||
ONCE="`echo $DECLARED $USED | tr ' ' '\n' | sort | uniq -c | grep '1 ' | sed -E 's/^ *1 //g'`"
|
||||
FILTER="`echo $DECLARED | tr ' ' '|' | sed -E 's/\|$//g'`"
|
||||
UNDECL="`echo $ONCE | grep -v -E $FILTER 2>/dev/null || echo ''`"
|
||||
@@ -69,7 +82,7 @@ cat $INPUT_FILE | tr '\n' '\f' |
|
||||
echo "$line"
|
||||
exit 1
|
||||
fi
|
||||
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined <<< "`tr '\f' '\n' <<< $line`" |
|
||||
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined,--export=hook,--export=cbak <<< "`tr '\f' '\n' <<< $line`" |
|
||||
hook-cleaner - - 2>/dev/null |
|
||||
xxd -p -u -c 10 |
|
||||
sed -E 's/../0x&U,/g' | sed -E 's/^/ /g' >> $OUTPUT_FILE
|
||||
|
||||
@@ -65,29 +65,16 @@ hso_delete(void (*f)(Json::Value& jv))
|
||||
Json::Value
|
||||
hso(std::vector<uint8_t> const& wasmBytes, void (*f)(Json::Value& jv))
|
||||
{
|
||||
if (wasmBytes.size() == 0)
|
||||
throw std::runtime_error("empty hook wasm passed to hso()");
|
||||
|
||||
Json::Value jv;
|
||||
jv[jss::CreateCode] = strHex(wasmBytes);
|
||||
{
|
||||
jv[jss::HookOn] =
|
||||
"0000000000000000000000000000000000000000000000000000000000000000";
|
||||
jv[jss::HookNamespace] = to_string(uint256{beast::zero});
|
||||
jv[jss::HookApiVersion] = Json::Value{0};
|
||||
}
|
||||
|
||||
if (f)
|
||||
f(jv);
|
||||
|
||||
return jv;
|
||||
return hso(strHex(wasmBytes), f);
|
||||
}
|
||||
|
||||
Json::Value
|
||||
hso(std::string const& wasmHex, void (*f)(Json::Value& jv))
|
||||
{
|
||||
if (wasmHex.size() == 0)
|
||||
throw std::runtime_error("empty hook wasm passed to hso()");
|
||||
throw std::runtime_error(
|
||||
"empty hook wasm passed to hso(): run "
|
||||
"src/test/app/build_test_hooks.sh to generate the hook wasm");
|
||||
|
||||
Json::Value jv;
|
||||
jv[jss::CreateCode] = wasmHex;
|
||||
|
||||
Reference in New Issue
Block a user