Compare commits

..

3 Commits

Author SHA1 Message Date
Nicholas Dudfield
823d41775a Revert "chore: use improved levelization script with threading and argparse"
This reverts commit 5c1d7d9ae9.
2026-03-13 12:33:19 +07:00
Nicholas Dudfield
5c1d7d9ae9 chore: use improved levelization script with threading and argparse 2026-03-13 12:13:39 +07:00
Nicholas Dudfield
70d4d3ba81 chore: replace levelization shell script with python
Backport of XRPLF/rippled#6325. The python version runs ~80x faster.
2026-03-13 12:08:27 +07:00
30 changed files with 385 additions and 5499 deletions

View File

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

3
.gitignore vendored
View File

@@ -53,6 +53,9 @@ Builds/levelization/results/paths.txt
Builds/levelization/results/includes/
Builds/levelization/results/includedby/
# Python
__pycache__
# Ignore tmp directory.
tmp

View File

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

View 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()

View File

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

View File

@@ -72,10 +72,6 @@
#define sfLockCount ((2U << 16U) + 49U)
#define sfFirstNFTokenSequence ((2U << 16U) + 50U)
#define sfOracleDocumentID ((2U << 16U) + 51U)
#define sfHookCallbackGas ((2U << 16U) + 89U)
#define sfHookWeakGas ((2U << 16U) + 90U)
#define sfHookInstructionCost ((2U << 16U) + 91U)
#define sfHookGas ((2U << 16U) + 92U)
#define sfStartTime ((2U << 16U) + 93U)
#define sfRepeatCount ((2U << 16U) + 94U)
#define sfDelaySeconds ((2U << 16U) + 95U)

View File

@@ -266,8 +266,6 @@ enum hook_log_code : uint16_t {
CUSTOM_SECTION_DISALLOWED =
86, // the wasm contained a custom section (id=0)
INTERNAL_ERROR = 87, // an internal error described by the log text
MEMORY_PAGE_LIMIT =
88, // memory import declares min or max exceeding the page limit
// RH NOTE: only HookSet msgs got log codes, possibly all Hook log lines
// should get a code?
};
@@ -393,7 +391,6 @@ enum ExitType : uint8_t {
WASM_ERROR = 1,
ROLLBACK = 2,
ACCEPT = 3,
GAS_INSUFFICIENT = 4,
};
const uint16_t max_state_modifications = 256;
@@ -403,8 +400,6 @@ const uint8_t max_emit = 255;
const uint8_t max_params = 16;
const double fee_base_multiplier = 1.1f;
const uint8_t max_memory_pages = 8;
using APIWhitelist = std::map<std::string, std::vector<uint8_t>>;
// RH NOTE: Find descriptions of api functions in ./impl/applyHook.cpp and

View File

@@ -1,61 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_APP_HOOK_GASVALIDATOR_H_INCLUDED
#define RIPPLE_APP_HOOK_GASVALIDATOR_H_INCLUDED
#include <xrpl/basics/Expected.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Rules.h>
#include <functional>
#include <optional>
#include <ostream>
#include <string>
#include <vector>
// Forward declaration
using GuardLog =
std::optional<std::reference_wrapper<std::basic_ostream<char>>>;
namespace hook {
/**
* @brief Validate WASM host functions for Gas-type hooks
*
* Validates that a WASM binary only imports allowed host functions
* and does not import the _g (guard) function, which is only for
* Guard-type hooks.
*
* @param wasm The WASM binary to validate
* @param guardLog Logging function for validation errors
* @param guardLogAccStr Account string for logging
* @return bool if validation succeeds,
* @return true if contains cbak function
* @return false if otherwise
* @return error message if validation fails
*/
ripple::Expected<bool, std::string>
validateWasmHostFunctionsForGas(
std::vector<uint8_t> const& wasm,
ripple::Rules const& rules,
beast::Journal const& j);
} // namespace hook
#endif // RIPPLE_APP_HOOK_GASVALIDATOR_H_INCLUDED

View File

@@ -80,7 +80,7 @@ namespace detail {
// Feature.cpp. Because it's only used to reserve storage, and determine how
// large to make the FeatureBitset, it MAY be larger. It MUST NOT be less than
// the actual number of amendments. A LogicError on startup will verify this.
static constexpr std::size_t numFeatures = 114;
static constexpr std::size_t numFeatures = 113;
/** Amendments that this server supports and the default voting behavior.
Whether they are enabled depends on the Rules defined in the validated

View File

@@ -362,8 +362,6 @@ enum TECcodes : TERUnderlyingType {
tecARRAY_TOO_LARGE = 197,
tecLOCKED = 198,
tecBAD_CREDENTIALS = 199,
tecHOOK_INSUFFICIENT_GAS = 200,
tecHOOK_INVALID = 201,
tecLAST_POSSIBLE_ENTRY = 255,
};

View File

@@ -31,7 +31,6 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(HookGas, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(HookAPISerializedType240, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(DynamicNFT, Supported::no, VoteBehavior::DefaultNo)

View File

@@ -103,12 +103,10 @@ LEDGER_ENTRY(ltHOOK_DEFINITION, 'D', HookDefinition, hook_definition, ({
{sfCreateCode, soeREQUIRED},
{sfHookSetTxnID, soeREQUIRED},
{sfReferenceCount, soeREQUIRED},
{sfFee, soeOPTIONAL},
{sfFee, soeREQUIRED},
{sfHookCallbackFee, soeOPTIONAL},
{sfPreviousTxnID, soeOPTIONAL},
{sfPreviousTxnLgrSeq, soeOPTIONAL},
{sfHookCallbackGas, soeOPTIONAL},
{sfHookWeakGas, soeOPTIONAL},
}))
/** A ledger object containing a hook-emitted transaction from a previous hook execution.

View File

@@ -115,10 +115,6 @@ TYPED_SFIELD(sfLockCount, UINT32, 49)
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
TYPED_SFIELD(sfHookCallbackGas, UINT32, 89)
TYPED_SFIELD(sfHookWeakGas, UINT32, 90)
TYPED_SFIELD(sfHookInstructionCost, UINT32, 91)
TYPED_SFIELD(sfHookGas, UINT32, 92)
TYPED_SFIELD(sfStartTime, UINT32, 93)
TYPED_SFIELD(sfRepeatCount, UINT32, 94)
TYPED_SFIELD(sfDelaySeconds, UINT32, 95)

View File

@@ -87,8 +87,6 @@ JSS(HookParameterName); // field
JSS(HookParameterValue); // field
JSS(HookParameter); // field
JSS(HookGrant); // field
JSS(HookCallbackGas); // field
JSS(HookWeakGas); // field
JSS(isSerialized); // out: RPC server_definitions
// matches definitions.json format
JSS(isSigningField); // out: RPC server_definitions

View File

@@ -78,10 +78,7 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookExecutionIndex, soeREQUIRED},
{sfHookStateChangeCount, soeREQUIRED},
{sfHookEmitCount, soeREQUIRED},
{sfFlags, soeOPTIONAL},
{sfHookInstructionCost, soeOPTIONAL},
{sfHookWeakGas, soeOPTIONAL},
{sfHookCallbackGas, soeOPTIONAL}});
{sfFlags, soeOPTIONAL}});
add(sfHookEmission.jsonName,
sfHookEmission.getCode(),
@@ -101,7 +98,7 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookCanEmit, soeOPTIONAL},
{sfHookApiVersion, soeREQUIRED},
{sfFlags, soeREQUIRED},
{sfFee, soeOPTIONAL}});
{sfFee, soeREQUIRED}});
add(sfHook.jsonName,
sfHook.getCode(),
@@ -115,8 +112,6 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookOnOutgoing, soeOPTIONAL},
{sfHookCanEmit, soeOPTIONAL},
{sfHookApiVersion, soeOPTIONAL},
{sfHookCallbackGas, soeOPTIONAL},
{sfHookWeakGas, soeOPTIONAL},
{sfFlags, soeOPTIONAL}});
add(sfHookGrant.jsonName,

View File

@@ -124,8 +124,6 @@ transResults()
MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."),
MAKE_ERROR(tecLOCKED, "Fund is locked."),
MAKE_ERROR(tecBAD_CREDENTIALS, "Bad credentials."),
MAKE_ERROR(tecHOOK_INSUFFICIENT_GAS, "Insufficient hook gas to complete the transaction."),
MAKE_ERROR(tecHOOK_INVALID, "Invalid hook."),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),

View File

@@ -48,7 +48,6 @@ TxFormats::TxFormats()
{sfFirstLedgerSequence, soeOPTIONAL},
{sfNetworkID, soeOPTIONAL},
{sfHookParameters, soeOPTIONAL},
{sfHookGas, soeOPTIONAL},
};
#pragma push_macro("UNWRAP")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -41,28 +41,17 @@ echo '
namespace ripple {
namespace test {
std::map<std::string, std::vector<uint8_t>> wasm = {' > $OUTPUT_FILE
# Counter file for sharing between subshells
COUNTER_FILE=$(mktemp)
echo "0" > $COUNTER_FILE
trap "rm -f $COUNTER_FILE" EXIT
# Process both [test.hook] and [test.hook.gas] blocks
process_block() {
local tag_pattern="$1" # regex pattern: "hook" or "hook\.gas"
local tag_output="$2" # output string: "hook" or "hook.gas"
local skip_cleaner="$3" # "0" for no skip, "1" for skip
cat $INPUT_FILE | tr '\n' '\f' |
grep -Po "R\"\[test\.${tag_pattern}\](.*?)\[test\.${tag_pattern}\]\"" |
sed -E "s/R\"\[test\.${tag_pattern}\]\(//g" |
sed -E "s/\)\[test\.${tag_pattern}\]\"[\f \t]*/\/*end*\//g" |
COUNTER="0"
cat $INPUT_FILE | tr '\n' '\f' |
grep -Po 'R"\[test\.hook\](.*?)\[test\.hook\]"' |
sed -E 's/R"\[test\.hook\]\(//g' |
sed -E 's/\)\[test\.hook\]"[\f \t]*/\/*end*\//g' |
while read -r line
do
COUNTER=$(cat $COUNTER_FILE)
echo "/* ==== WASM: $COUNTER ==== */" >> $OUTPUT_FILE
echo -n "{ R\"[test.${tag_output}](" >> $OUTPUT_FILE
echo -n '{ R"[test.hook](' >> $OUTPUT_FILE
cat <<< "$line" | sed -E 's/.{7}$//g' | tr -d '\n' | tr '\f' '\n' >> $OUTPUT_FILE
echo ")[test.${tag_output}]\"," >> $OUTPUT_FILE
echo ')[test.hook]",' >> $OUTPUT_FILE
echo "{" >> $OUTPUT_FILE
WAT=`grep -Eo '\(module' <<< $line | wc -l`
if [ "$WAT" -eq "0" ]
@@ -80,19 +69,10 @@ process_block() {
echo "$line"
exit 1
fi
if [ "$skip_cleaner" -eq "1" ]
then
# Skip hook-cleaner for [test.hook.gas]
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined -Wno-int-conversion -Wno-pointer-sign -Wno-return-type <<< "`tr '\f' '\n' <<< $line`" |
xxd -p -u -c 10 |
sed -E 's/../0x&U,/g' | sed -E 's/^/ /g' >> $OUTPUT_FILE
else
# Run hook-cleaner for [test.hook]
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined -Wno-int-conversion -Wno-pointer-sign -Wno-return-type <<< "`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
fi
wasmcc -x c /dev/stdin -o /dev/stdout -O2 -Wl,--allow-undefined <<< "`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
else
wat2wasm - -o /dev/stdout <<< "`tr '\f' '\n' <<< $(sed -E 's/.{7}$//g' <<< $line)`" |
xxd -p -u -c 10 |
@@ -105,15 +85,8 @@ process_block() {
fi
echo '}},' >> $OUTPUT_FILE
echo >> $OUTPUT_FILE
echo $((COUNTER + 1)) > $COUNTER_FILE
COUNTER=`echo $COUNTER + 1 | bc`
done
}
# Process [test.hook] blocks (with hook-cleaner)
process_block "hook" "hook" "0"
# Process [test.hook.gas] blocks (without hook-cleaner)
process_block "hook\.gas" "hook.gas" "1"
echo '};
}
}

View File

@@ -44,9 +44,6 @@ hso(std::vector<uint8_t> const& wasmBytes, void (*f)(Json::Value& jv) = 0);
Json::Value
hso(std::string const& wasmHex, void (*f)(Json::Value& jv) = 0);
Json::Value
hso(uint256 const& hookHash, void (*f)(Json::Value& jv) = 0);
Json::Value
hso_delete(void (*f)(Json::Value& jv) = 0);

View File

@@ -1,80 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TEST_JTX_HOOKGAS_H_INCLUDED
#define RIPPLE_TEST_JTX_HOOKGAS_H_INCLUDED
#include <test/jtx/Env.h>
#include <test/jtx/tags.h>
#include <xrpl/basics/contract.h>
namespace ripple {
namespace test {
namespace jtx {
/** Set the HookGas on a JTx. */
class hookgas
{
private:
std::uint32_t gas_;
public:
hookgas(std::uint32_t gas) : gas_{gas}
{
}
void
operator()(Env&, JTx& jt) const;
};
/** Set the HookCallbackGas on a JTx. */
class cbakgas
{
private:
std::uint32_t gas_;
public:
cbakgas(std::uint32_t gas) : gas_{gas}
{
}
void
operator()(Env&, JTx& jt) const;
};
/** Set the HookWeakGas on a JTx. */
class weakgas
{
private:
std::uint32_t gas_;
public:
weakgas(std::uint32_t gas) : gas_{gas}
{
}
void
operator()(Env&, JTx& jt) const;
};
} // namespace jtx
} // namespace test
} // namespace ripple
#endif

View File

@@ -104,16 +104,6 @@ hso(std::string const& wasmHex, void (*f)(Json::Value& jv))
return jv;
}
Json::Value
hso(uint256 const& hookHash, void (*f)(Json::Value& jv))
{
Json::Value jv;
jv[jss::HookHash] = to_string(hookHash);
if (f)
f(jv);
return jv;
}
// Helper function to create HookContext with external stateMap
hook::HookContext
makeStubHookContext(

View File

@@ -1,47 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx/hookgas.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
namespace test {
namespace jtx {
void
hookgas::operator()(Env&, JTx& jt) const
{
jt[sfHookGas.jsonName] = gas_;
}
void
cbakgas::operator()(Env&, JTx& jt) const
{
jt[sfHookCallbackGas.jsonName] = gas_;
}
void
weakgas::operator()(Env&, JTx& jt) const
{
jt[sfHookWeakGas.jsonName] = gas_;
}
} // namespace jtx
} // namespace test
} // namespace ripple

View File

@@ -124,9 +124,7 @@ apply(
uint32_t wasmParam,
uint8_t hookChainPosition,
// result of apply() if this is weak exec
std::shared_ptr<STObject const> const& provisionalMeta,
uint16_t hookApiVersion,
uint32_t hookGas);
std::shared_ptr<STObject const> const& provisionalMeta);
struct HookContext;
@@ -164,7 +162,6 @@ struct HookResult
std::string exitReason{""};
int64_t exitCode{-1};
uint64_t instructionCount{0};
uint64_t instructionCost{0};
bool hasCallback = false; // true iff this hook wasm has a cbak function
bool isCallback =
false; // true iff this hook execution is a callback in action
@@ -177,8 +174,6 @@ struct HookResult
false; // hook_again allows strong pre-apply to nominate
// additional weak post-apply execution
std::shared_ptr<STObject const> provisionalMeta;
uint16_t hookApiVersion = 0; // 0 = Guard-type, 1 = Gas-type
uint32_t hookGas; // Gas limit for Gas-type hooks
};
class HookExecutor;
@@ -330,18 +325,12 @@ public:
WasmEdge_ConfigureContext* conf = NULL;
WasmEdge_VMContext* ctx = NULL;
WasmEdgeVM(uint16_t hookApiVersion)
WasmEdgeVM()
{
conf = WasmEdge_ConfigureCreate();
if (!conf)
return;
WasmEdge_ConfigureStatisticsSetInstructionCounting(conf, true);
if (hookApiVersion == 1)
{
WasmEdge_ConfigureStatisticsSetCostMeasuring(conf, true);
WasmEdge_ConfigureSetMaxMemoryPage(
conf, hook_api::max_memory_pages);
}
ctx = WasmEdge_VMCreate(conf, NULL);
}
@@ -376,9 +365,9 @@ public:
* Validate that a web assembly blob can be loaded by wasmedge
*/
static std::optional<std::string>
validateWasm(const void* wasm, size_t len, uint16_t hookApiVersion)
validateWasm(const void* wasm, size_t len)
{
WasmEdgeVM vm{hookApiVersion};
WasmEdgeVM vm;
if (!vm.sane())
return "Could not create WASMEDGE instance";
@@ -423,7 +412,7 @@ public:
WasmEdge_LogOff();
WasmEdgeVM vm{hookCtx.result.hookApiVersion};
WasmEdgeVM vm;
if (!vm.sane())
{
@@ -444,22 +433,6 @@ public:
return;
}
// Set Gas limit for Gas-type hooks (HookApiVersion == 1)
if (hookCtx.result.hookApiVersion == 1)
{
auto* statsCtx = WasmEdge_VMGetStatisticsContext(vm.ctx);
if (statsCtx)
{
// Convert HookGas to cost limit count (1 Gas = 1 cost)
uint32_t gasLimit = hookCtx.result.hookGas;
WasmEdge_StatisticsSetCostLimit(statsCtx, gasLimit);
JLOG(j.trace())
<< "HookInfo[" << HC_ACC() << "]: Set Gas limit to "
<< gasLimit << " cost limit for Gas-type Hook";
}
}
WasmEdge_Value params[1] = {WasmEdge_ValueGenI32((int64_t)wasmParam)};
WasmEdge_Value returns[1];
@@ -473,31 +446,16 @@ public:
returns,
1);
if (auto err = getWasmError("WASM VM error", res); err)
{
JLOG(j.warn()) << "HookError[" << HC_ACC() << "]: " << *err;
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
return;
}
auto* statsCtx = WasmEdge_VMGetStatisticsContext(vm.ctx);
hookCtx.result.instructionCount =
WasmEdge_StatisticsGetInstrCount(statsCtx);
hookCtx.result.instructionCost =
WasmEdge_StatisticsGetTotalCost(statsCtx);
if (auto err = getWasmError("WASM VM error", res); err)
{
JLOG(j.trace()) << "HookError[" << HC_ACC() << "]: " << *err;
// Check if error is due to Gas limit exceeded for Gas-type hooks
if (hookCtx.result.hookApiVersion == 1 &&
err->find("cost limit exceeded") != std::string::npos)
{
JLOG(j.trace()) << "HookError[" << HC_ACC()
<< "]: Gas limit exceeded. Limit was "
<< hookCtx.result.hookGas;
hookCtx.result.exitType = hook_api::ExitType::GAS_INSUFFICIENT;
}
else
{
hookCtx.result.exitType = hook_api::ExitType::WASM_ERROR;
}
return;
}
// RH NOTE: stack unwind will clean up WasmEdgeVM
}

View File

@@ -1,438 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpl/basics/Expected.h>
#include <xrpl/basics/Log.h>
#include <xrpl/hook/GasValidator.h>
#include <xrpl/hook/Guard.h>
#include <xrpl/hook/Macro.h>
#include <xrpl/protocol/Feature.h>
#include <wasmedge/wasmedge.h>
namespace hook {
Expected<bool, std::string>
validateExportSection(
WasmEdge_ASTModuleContext* astModule,
beast::Journal const& j)
{
// Get export count
uint32_t exportCount = WasmEdge_ASTModuleListExportsLength(astModule);
if (exportCount == 0)
{
return Unexpected("WASM must export at least hook API functions");
}
// Get exports
const WasmEdge_ExportTypeContext* exports[256];
uint32_t actualExportCount = std::min(exportCount, 256u);
actualExportCount =
WasmEdge_ASTModuleListExports(astModule, exports, actualExportCount);
// Track if we found required hook() function
bool foundHook = false;
bool foundCbak = false;
// Check each export
for (uint32_t i = 0; i < actualExportCount; i++)
{
WasmEdge_ExternalType const type =
WasmEdge_ExportTypeGetExternalType(exports[i]);
// Validate memory exports for page limits
if (type == WasmEdge_ExternalType_Memory)
{
const WasmEdge_MemoryTypeContext* memType =
WasmEdge_ExportTypeGetMemoryType(astModule, exports[i]);
if (!memType)
{
JLOG(j.trace()) << "HookSet(" << hook::log::MEMORY_PAGE_LIMIT
<< "): Exported memory has no type definition";
return Unexpected("Exported memory has no type definition");
}
WasmEdge_Limit limit = WasmEdge_MemoryTypeGetLimit(memType);
constexpr uint32_t kMaxMemoryPages = hook_api::max_memory_pages;
if (limit.Min > kMaxMemoryPages)
{
JLOG(j.trace())
<< "HookSet(" << hook::log::MEMORY_PAGE_LIMIT
<< "): Gas-type hook exported memory minimum "
"pages ("
<< limit.Min << ") exceeds limit of " << kMaxMemoryPages;
return Unexpected(
"Gas-type hook exported memory minimum pages "
"exceed limit of 8");
}
if (limit.HasMax && limit.Max > kMaxMemoryPages)
{
JLOG(j.trace())
<< "HookSet(" << hook::log::MEMORY_PAGE_LIMIT
<< "): Gas-type hook exported memory maximum "
"pages ("
<< limit.Max << ") exceeds limit of " << kMaxMemoryPages;
return Unexpected(
"Gas-type hook exported memory maximum pages "
"exceed limit of 8");
}
continue;
}
// Only check function exports
if (type != WasmEdge_ExternalType_Function)
continue;
WasmEdge_String const name =
WasmEdge_ExportTypeGetExternalName(exports[i]);
std::string nameStr(name.Buf, name.Length);
if (nameStr.starts_with("__"))
{
// skip runtime support functions
continue;
}
// Only allow hook() and cbak() exports
if (nameStr != "hook" && nameStr != "cbak")
{
JLOG(j.trace()) << "HookSet(" << hook::log::EXPORT_MISSING
<< "): Unauthorized export function '" << nameStr
<< "'. Only 'hook' and 'cbak' are allowed";
return Unexpected(
"Unauthorized export function '" + nameStr +
"'. Only 'hook' and 'cbak' are allowed");
}
if (nameStr == "hook")
foundHook = true;
if (nameStr == "cbak")
foundCbak = true;
// Get function type to validate signature
WasmEdge_FunctionTypeContext const* functionType =
WasmEdge_ExportTypeGetFunctionType(astModule, exports[i]);
// Validate parameter count (must be exactly 1)
uint32_t paramCount =
WasmEdge_FunctionTypeGetParametersLength(functionType);
if (paramCount != 1)
{
JLOG(j.trace())
<< "HookSet("
<< (nameStr == "hook" ? hook::log::EXPORT_HOOK_FUNC
: hook::log::EXPORT_CBAK_FUNC)
<< "): Function '" << nameStr
<< "' must have exactly 1 parameter, found " << paramCount;
return Unexpected(
"Function '" + nameStr +
"' must have exactly 1 parameter of type uint32_t");
}
// Validate parameter type (must be i32 / uint32_t)
WasmEdge_ValType parameters[1];
WasmEdge_FunctionTypeGetParameters(functionType, parameters, 1);
if (parameters[0] != WasmEdge_ValType_I32)
{
JLOG(j.trace()) << "HookSet("
<< (nameStr == "hook" ? hook::log::EXPORT_HOOK_FUNC
: hook::log::EXPORT_CBAK_FUNC)
<< "): Function '" << nameStr
<< "' parameter must be uint32_t (i32), found type "
<< parameters[0];
return Unexpected(
"Function '" + nameStr + "' parameter must be uint32_t (i32)");
}
// Validate return type (must be i64 / uint64_t)
uint32_t returnCount =
WasmEdge_FunctionTypeGetReturnsLength(functionType);
if (returnCount != 1)
{
JLOG(j.trace())
<< "HookSet("
<< (nameStr == "hook" ? hook::log::EXPORT_HOOK_FUNC
: hook::log::EXPORT_CBAK_FUNC)
<< "): Function '" << nameStr
<< "' must return exactly 1 value, found " << returnCount;
return Unexpected(
"Function '" + nameStr +
"' must return exactly 1 value of type uint64_t");
}
WasmEdge_ValType returns[1];
WasmEdge_FunctionTypeGetReturns(functionType, returns, 1);
if (returns[0] != WasmEdge_ValType_I64)
{
JLOG(j.trace())
<< "HookSet("
<< (nameStr == "hook" ? hook::log::EXPORT_HOOK_FUNC
: hook::log::EXPORT_CBAK_FUNC)
<< "): Function '" << nameStr
<< "' return type must be uint64_t (i64), found type "
<< returns[0];
return Unexpected(
"Function '" + nameStr +
"' return type must be uint64_t (i64)");
}
}
// Ensure hook() function was exported (required)
if (!foundHook)
{
JLOG(j.trace()) << "HookSet(" << hook::log::EXPORT_MISSING
<< "): Required function 'hook' not found in exports";
return Unexpected("Required function 'hook' not found in exports");
}
return foundCbak;
}
Expected<void, std::string>
validateImportSection(
WasmEdge_ASTModuleContext* astModule,
Rules const& rules,
beast::Journal const& j)
{
// Get import count
uint32_t importCount = WasmEdge_ASTModuleListImportsLength(astModule);
if (importCount == 0)
{
JLOG(j.trace()) << "HookSet(" << hook::log::IMPORTS_MISSING
<< "): WASM must import at least hook API functions";
return Unexpected("WASM must import at least hook API functions");
}
// Get imports (max 256)
const WasmEdge_ImportTypeContext* imports[256];
uint32_t actualImportCount = std::min(importCount, 256u);
actualImportCount =
WasmEdge_ASTModuleListImports(astModule, imports, actualImportCount);
std::optional<std::string> error;
// Check each import
for (uint32_t i = 0; i < actualImportCount; i++)
{
WasmEdge_String moduleName =
WasmEdge_ImportTypeGetModuleName(imports[i]);
WasmEdge_String externalName =
WasmEdge_ImportTypeGetExternalName(imports[i]);
WasmEdge_ExternalType extType =
WasmEdge_ImportTypeGetExternalType(imports[i]);
// Only check function imports
if (extType != WasmEdge_ExternalType_Function)
continue;
// Convert WasmEdge_String to std::string for comparison
std::string modName(moduleName.Buf, moduleName.Length);
std::string extName(externalName.Buf, externalName.Length);
// Check module name is "env"
if (modName != "env")
{
JLOG(j.trace())
<< "HookSet(" << hook::log::IMPORT_MODULE_ENV
<< "): Import module must be 'env', found: " << modName;
return Unexpected("Import module must be 'env', found: " + modName);
}
// Check for forbidden _g function (guard function)
if (extName == "_g")
{
JLOG(j.trace())
<< "HookSet(" << hook::log::IMPORT_ILLEGAL
<< "): Gas-type hooks cannot import _g (guard) function";
return Unexpected(
"Gas-type hooks cannot import _g (guard) function");
}
// Determine which whitelist contains the function and get expected
// signature
std::vector<uint8_t> const* expectedSig = nullptr;
auto importWhitelist = hook_api::getImportWhitelist(rules);
auto baseIt = importWhitelist.find(extName);
if (baseIt != importWhitelist.end())
expectedSig = &baseIt->second;
// Function not in any whitelist
if (!expectedSig)
{
JLOG(j.trace()) << "HookSet(" << hook::log::IMPORT_ILLEGAL
<< "): Import not in whitelist: " << extName;
return Unexpected("Import not in whitelist: " + extName);
}
// Get function type for signature validation
WasmEdge_FunctionTypeContext const* functionType =
WasmEdge_ImportTypeGetFunctionType(astModule, imports[i]);
if (!functionType)
{
JLOG(j.trace()) << "HookSet(" << hook::log::FUNC_TYPELESS
<< "): Import function '" << extName
<< "' has no function type definition";
return Unexpected(
"Import function '" + extName +
"' has no function type definition");
}
// Validate return type
// expectedSig[0] is the return type
uint32_t returnCount =
WasmEdge_FunctionTypeGetReturnsLength(functionType);
if (returnCount != 1)
{
JLOG(j.trace())
<< "HookSet(" << hook::log::FUNC_RETURN_COUNT
<< "): Import function '" << extName
<< "' must return exactly 1 value, found " << returnCount;
return Unexpected(
"Import function '" + extName +
"' must return exactly 1 value");
}
WasmEdge_ValType actualReturnType;
WasmEdge_FunctionTypeGetReturns(functionType, &actualReturnType, 1);
if (actualReturnType != (*expectedSig)[0])
{
JLOG(j.trace()) << "HookSet(" << hook::log::FUNC_RETURN_INVALID
<< "): Import function '" << extName
<< "' has incorrect return type. Expected "
<< static_cast<int>((*expectedSig)[0]) << ", found "
<< static_cast<int>(actualReturnType);
return Unexpected(
"Import function '" + extName + "' has incorrect return type");
}
// Validate parameter count and types
// expectedSig[1..N] are the parameter types
uint32_t expectedParamCount =
expectedSig->size() > 0 ? expectedSig->size() - 1 : 0;
uint32_t actualParamCount =
WasmEdge_FunctionTypeGetParametersLength(functionType);
if (actualParamCount != expectedParamCount)
{
JLOG(j.trace()) << "HookSet(" << hook::log::FUNC_PARAM_INVALID
<< "): Import function '" << extName << "' has "
<< actualParamCount << " parameters, expected "
<< expectedParamCount;
return Unexpected(
"Import function '" + extName +
"' has incorrect parameter count");
}
// Validate each parameter type
if (actualParamCount > 0)
{
std::vector<WasmEdge_ValType> actualParams(actualParamCount);
WasmEdge_FunctionTypeGetParameters(
functionType, actualParams.data(), actualParamCount);
for (uint32_t p = 0; p < actualParamCount; p++)
{
uint8_t expectedParamType = (*expectedSig)[1 + p];
if (actualParams[p] != expectedParamType)
{
JLOG(j.trace())
<< "HookSet(" << hook::log::FUNC_PARAM_INVALID
<< "): Import function '" << extName << "' parameter "
<< p << " has incorrect type. Expected "
<< static_cast<int>(expectedParamType) << ", found "
<< static_cast<int>(actualParams[p]);
return Unexpected(
"Import function '" + extName +
"' has incorrect parameter types");
}
}
}
}
return {};
}
Expected<bool, std::string>
validateWasmHostFunctionsForGas(
std::vector<uint8_t> const& wasm,
Rules const& rules,
beast::Journal const& j)
{
// Create WasmEdge Loader
WasmEdge_LoaderContext* loader = WasmEdge_LoaderCreate(NULL);
if (!loader)
{
return Unexpected("Failed to create WasmEdge Loader");
}
// Parse WASM binary
WasmEdge_ASTModuleContext* astModule = NULL;
WasmEdge_Result res = WasmEdge_LoaderParseFromBuffer(
loader, &astModule, wasm.data(), wasm.size());
if (!WasmEdge_ResultOK(res))
{
WasmEdge_LoaderDelete(loader);
const char* msg = WasmEdge_ResultGetMessage(res);
return Unexpected(
std::string("Failed to parse WASM: ") +
(msg ? msg : "unknown error"));
}
bool foundCbak = false;
//
// check export section
//
auto resultExport = validateExportSection(astModule, j);
if (!resultExport)
{
WasmEdge_ASTModuleDelete(astModule);
WasmEdge_LoaderDelete(loader);
return Unexpected(resultExport.error());
}
foundCbak = resultExport.value();
//
// check import section
//
if (auto result = validateImportSection(astModule, rules, j); !result)
{
WasmEdge_ASTModuleDelete(astModule);
WasmEdge_LoaderDelete(loader);
return Unexpected(result.error());
}
// Cleanup
WasmEdge_ASTModuleDelete(astModule);
WasmEdge_LoaderDelete(loader);
return foundCbak;
}
} // namespace hook

View File

@@ -1197,9 +1197,7 @@ hook::apply(
bool isStrong,
uint32_t wasmParam,
uint8_t hookChainPosition,
std::shared_ptr<STObject const> const& provisionalMeta,
uint16_t hookApiVersion,
uint32_t hookGas)
std::shared_ptr<STObject const> const& provisionalMeta)
{
HookContext hookCtx = {
.applyCtx = applyCtx,
@@ -1230,9 +1228,7 @@ hook::apply(
.wasmParam = wasmParam,
.hookChainPosition = hookChainPosition,
.foreignStateSetDisabled = false,
.provisionalMeta = provisionalMeta,
.hookApiVersion = hookApiVersion,
.hookGas = hookGas},
.provisionalMeta = provisionalMeta},
.emitFailure = isCallback && wasmParam & 1
? std::optional<ripple::STObject>(
(*(applyCtx.view().peek(keylet::emittedTxn(
@@ -1247,14 +1243,10 @@ hook::apply(
executor.executeWasm(
wasm.data(), (size_t)wasm.size(), isCallback, wasmParam, j);
auto const& exitType = hookCtx.result.exitType;
auto const& exitTypeStr = exitType == ExitType::ROLLBACK ? "ROLLBACK"
: exitType == ExitType::ACCEPT ? "ACCEPT"
: exitType == ExitType::GAS_INSUFFICIENT ? "GAS_INSUFFICIENT"
: exitType == ExitType::WASM_ERROR ? "WASM_ERROR"
: "UNSET";
JLOG(j.trace()) << "HookInfo[" << HC_ACC() << "]: " << exitTypeStr
JLOG(j.trace()) << "HookInfo[" << HC_ACC() << "]: "
<< (hookCtx.result.exitType == hook_api::ExitType::ROLLBACK
? "ROLLBACK"
: "ACCEPT")
<< " RS: '" << hookCtx.result.exitReason.c_str()
<< "' RC: " << hookCtx.result.exitCode;
@@ -1743,9 +1735,6 @@ hook::finalizeHookResult(
ripple::Slice{
hookResult.exitReason.data(), hookResult.exitReason.size()});
meta.setFieldU64(sfHookInstructionCount, hookResult.instructionCount);
if (hookResult.hookApiVersion == 1)
meta.setFieldU32(sfHookInstructionCost, hookResult.instructionCost);
meta.setFieldU16(
sfHookEmitCount,
emission_txnid.size()); // this will never wrap, hard limit

View File

@@ -639,7 +639,7 @@ Change::activateXahauGenesis()
std::optional<std::string> result2 =
hook::HookExecutor::validateWasm(
wasmBytes.data(), (size_t)wasmBytes.size(), 0);
wasmBytes.data(), (size_t)wasmBytes.size());
if (result2)
{

View File

@@ -26,7 +26,6 @@
#include <xrpld/ledger/ApplyView.h>
#include <xrpl/basics/Log.h>
#include <xrpl/hook/Enum.h>
#include <xrpl/hook/GasValidator.h>
#include <xrpl/hook/Guard.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
@@ -231,9 +230,7 @@ SetHook::inferOperation(STObject const& hookSetObj)
hookSetObj.isFieldPresent(sfHookOnIncoming))) &&
!hookSetObj.isFieldPresent(sfHookCanEmit) &&
!hookSetObj.isFieldPresent(sfHookApiVersion) &&
!hookSetObj.isFieldPresent(sfFlags) &&
!hookSetObj.isFieldPresent(sfHookCallbackGas) &&
!hookSetObj.isFieldPresent(sfHookWeakGas))
!hookSetObj.isFieldPresent(sfFlags))
return hsoNOOP;
uint32_t flags = hookSetObj.isFieldPresent(sfFlags)
@@ -270,8 +267,6 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
hookSetObj.isFieldPresent(sfHookOnIncoming) ||
hookSetObj.isFieldPresent(sfHookCanEmit) ||
hookSetObj.isFieldPresent(sfHookApiVersion) ||
hookSetObj.isFieldPresent(sfHookCallbackGas) ||
hookSetObj.isFieldPresent(sfHookWeakGas) ||
!hookSetObj.isFieldPresent(sfFlags) ||
!hookSetObj.isFieldPresent(sfHookNamespace))
{
@@ -305,8 +300,6 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
hookSetObj.isFieldPresent(sfHookCanEmit) ||
hookSetObj.isFieldPresent(sfHookApiVersion) ||
hookSetObj.isFieldPresent(sfHookNamespace) ||
hookSetObj.isFieldPresent(sfHookCallbackGas) ||
hookSetObj.isFieldPresent(sfHookWeakGas) ||
!hookSetObj.isFieldPresent(sfFlags))
{
JLOG(ctx.j.trace())
@@ -409,8 +402,6 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
// namespace may be valid, if the user so chooses
// hookon may be present if the user so chooses
// flags may be present if the user so chooses
// hookweakgas may be present if the user so chooses
// hookcallbackgas may be present if the user so chooses
return true;
}
@@ -450,8 +441,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
}
auto version = hookSetObj.getFieldU16(sfHookApiVersion);
if (!ctx.rules.enabled(featureHookGas) && version != 0)
if (version != 0)
{
// we currently only accept api version 0
JLOG(ctx.j.trace())
@@ -461,92 +451,6 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
return false;
}
// allow only version=0 and version=1
if (version != 0 && version != 1)
{
JLOG(ctx.j.trace())
<< "HookSet(" << ::hook::log::API_INVALID << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook "
"sfHook->sfHookApiVersion invalid. (Must be 0 or 1).";
return false;
}
// validate sfHookCallbackGas
auto hasHookCallbackGas = false;
if (hookSetObj.isFieldPresent(sfHookCallbackGas))
{
if (version != 1)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook "
"sfHookCallbackGas is "
"not allowed in version "
<< version << ".";
return false;
}
if (hookSetObj.getFieldU32(sfHookCallbackGas) == 0)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook "
"sfHookCallbackGas must be greater than 0.";
return false;
}
hasHookCallbackGas = true;
}
// validate sfHookWeakGas
if (hookSetObj.isFieldPresent(sfHookWeakGas))
{
if (version != 1)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook sfHookWeakGas is "
"not allowed in version "
<< version << ".";
return false;
}
if (hookSetObj.getFieldU32(sfHookWeakGas) == 0)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook sfHookWeakGas "
"must be greater than 0.";
return false;
}
if (!(flags & hsfCOLLECT))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook with "
"sfHookWeakGas must be used with hsfCOLLECT "
"flag.";
return false;
}
}
else if (version == 1)
{
if (flags & hsfCOLLECT)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook with "
"sfHookWeakGas must be used with hsfCOLLECT "
"flag.";
return false;
}
}
// validate sfHookOn
if (!hookSetObj.isFieldPresent(sfHookOn))
{
@@ -612,7 +516,6 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
return {};
Blob hook = hookSetObj.getFieldVL(sfCreateCode);
auto version = hookSetObj.getFieldU16(sfHookApiVersion);
// RH NOTE: validateGuards has a generic non-rippled specific
// interface so it can be used in other projects (i.e. tooling).
@@ -630,97 +533,46 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
hsacc = ss.str();
}
uint64_t maxInstrCountHook = 0;
uint64_t maxInstrCountCbak = 0;
auto result = validateGuards(
hook, // wasm to verify
logger,
hsacc,
hook_api::getImportWhitelist(ctx.rules),
hook_api::getGuardRulesVersion(ctx.rules));
if (version == 0) // Guard type
if (ctx.j.trace())
{
auto result = validateGuards(
hook, // wasm to verify
logger,
hsacc,
hook_api::getImportWhitelist(ctx.rules),
hook_api::getGuardRulesVersion(ctx.rules));
// clunky but to get the stream to accept the output
// correctly we will split on new line and feed each line
// one by one into the trace stream beast::Journal should be
// updated to inherit from basic_ostream<char> then this
// wouldn't be necessary.
if (ctx.j.trace())
// is this a needless copy or does the compiler do copy
// elision here?
std::string s = loggerStream.str();
char* data = s.data();
size_t len = s.size();
char* last = data;
size_t i = 0;
for (; i < len; ++i)
{
// clunky but to get the stream to accept the output
// correctly we will split on new line and feed each
// line one by one into the trace stream beast::Journal
// should be updated to inherit from basic_ostream<char>
// then this wouldn't be necessary.
// is this a needless copy or does the compiler do copy
// elision here?
std::string s = loggerStream.str();
char* data = s.data();
size_t len = s.size();
char* last = data;
size_t i = 0;
for (; i < len; ++i)
if (data[i] == '\n')
{
if (data[i] == '\n')
{
data[i] = '\0';
ctx.j.trace() << last;
last = data + i;
}
}
if (last < data + i)
data[i] = '\0';
ctx.j.trace() << last;
last = data + i;
}
}
if (!result)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::WASM_BAD_MAGIC << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook "
"sfCreateCode failed validation.";
return false;
}
std::tie(maxInstrCountHook, maxInstrCountCbak) = *result;
if (last < data + i)
ctx.j.trace() << last;
}
else if (version == 1) // Gas type
{
// validate with GasValidator
auto validationResult =
hook::validateWasmHostFunctionsForGas(
hook, ctx.rules, ctx.j);
if (!validationResult)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::IMPORT_ILLEGAL << ")["
<< HS_ACC()
<< "]: Malformed transaction: Gas-type Hook "
"validation failed: "
<< validationResult.error();
return false;
}
auto const hasCbak = validationResult.value();
if ((!hasCbak && hasHookCallbackGas) ||
(hasCbak && !hasHookCallbackGas))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD
<< ")[" << HS_ACC()
<< "]: Malformed transaction: Gas-type Hook must "
"contain either sfHookCallbackGas if it "
"contains cbak function";
return false;
}
// Gas type: maxInstrCount is not pre-calculated (use Gas
// limit at runtime)
maxInstrCountHook = 0;
maxInstrCountCbak = 0;
}
if (!result)
return false;
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::WASM_SMOKE_TEST << ")["
@@ -730,7 +582,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
std::optional<std::string> result2 =
hook::HookExecutor::validateWasm(
hook.data(), (size_t)hook.size(), version);
hook.data(), (size_t)hook.size());
if (result2)
{
@@ -742,7 +594,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
return false;
}
return std::make_pair(maxInstrCountHook, maxInstrCountCbak);
return *result;
}
}
@@ -931,11 +783,6 @@ SetHook::preflight(PreflightContext const& ctx)
hookSetObj.isFieldPresent(sfHookCanEmit))
return temDISABLED;
if (!ctx.rules.enabled(featureHookGas) &&
(hookSetObj.isFieldPresent(sfHookCallbackGas) ||
hookSetObj.isFieldPresent(sfHookWeakGas)))
return temDISABLED;
for (auto const& hookSetElement : hookSetObj)
{
auto const& name = hookSetElement.getFName();
@@ -945,8 +792,7 @@ SetHook::preflight(PreflightContext const& ctx)
name != sfHookOn && name != sfHookOnOutgoing &&
name != sfHookOnIncoming && name != sfHookGrants &&
name != sfHookApiVersion && name != sfFlags &&
name != sfHookCanEmit && name != sfHookCallbackGas &&
name != sfHookWeakGas)
name != sfHookCanEmit)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOK_INVALID_FIELD << ")["
@@ -1409,46 +1255,6 @@ struct KeyletComparator
}
};
TER
validateGasHook(
STObject const& hook,
std::shared_ptr<STLedgerEntry> const& defSLE)
{
auto const version = defSLE->getFieldU16(sfHookApiVersion);
if (version == 1)
{
// Gas Hook
if (!defSLE->isFieldPresent(sfHookCallbackGas) &&
hook.isFieldPresent(sfHookCallbackGas))
return tecHOOK_INVALID;
auto const flags = hook.getFlags();
auto const hasCollectFlag = flags & hsfCOLLECT;
if (hasCollectFlag)
{
// ltHook or ltHookDefinition must have sfHookWeakGas
if (!hook.isFieldPresent(sfHookWeakGas) &&
!defSLE->isFieldPresent(sfHookWeakGas))
return tecHOOK_INVALID;
}
else
{
// ltHook must not have sfHookWeakGas
if (hook.isFieldPresent(sfHookWeakGas))
return tecHOOK_INVALID;
}
}
else
{
// Guard Hook
if (hook.isFieldPresent(sfHookCallbackGas) ||
hook.isFieldPresent(sfHookWeakGas))
return tecHOOK_INVALID;
}
return tesSUCCESS;
}
TER
SetHook::setHook()
{
@@ -1542,14 +1348,6 @@ SetHook::setHook()
std::optional<uint256> newHookCanEmit;
std::optional<uint256> defHookCanEmit;
std::optional<uint32_t> oldHookWeakGas;
std::optional<uint32_t> newHookWeakGas;
std::optional<uint32_t> defHookWeakGas;
std::optional<uint32_t> oldHookCallbackGas;
std::optional<uint32_t> newHookCallbackGas;
std::optional<uint32_t> defHookCallbackGas;
// when hsoCREATE is invoked it populates this variable in case the hook
// definition already exists and the operation falls through into a
// hsoINSTALL operation instead
@@ -1623,23 +1421,6 @@ SetHook::setHook()
oldHookCanEmit = oldHook->get().getFieldH256(sfHookCanEmit);
else if (defHookCanEmit)
oldHookCanEmit = *defHookCanEmit;
if (oldDefSLE && oldDefSLE->isFieldPresent(sfHookWeakGas))
defHookWeakGas = oldDefSLE->getFieldU32(sfHookWeakGas);
if (oldHook && oldHook->get().isFieldPresent(sfHookWeakGas))
oldHookWeakGas = oldHook->get().getFieldU32(sfHookWeakGas);
else if (defHookWeakGas)
oldHookWeakGas = *defHookWeakGas;
if (oldDefSLE && oldDefSLE->isFieldPresent(sfHookCallbackGas))
defHookCallbackGas = oldDefSLE->getFieldU32(sfHookCallbackGas);
if (oldHook && oldHook->get().isFieldPresent(sfHookCallbackGas))
oldHookCallbackGas =
oldHook->get().getFieldU32(sfHookCallbackGas);
else if (defHookCallbackGas)
oldHookCallbackGas = *defHookCallbackGas;
}
// in preparation for three way merge populate fields if they are
@@ -1672,13 +1453,6 @@ SetHook::setHook()
newNamespace = hookSetObj->get().getFieldH256(sfHookNamespace);
newDirKeylet = keylet::hookStateDir(account_, *newNamespace);
}
if (hookSetObj->get().isFieldPresent(sfHookCallbackGas))
newHookCallbackGas =
hookSetObj->get().getFieldU32(sfHookCallbackGas);
if (hookSetObj->get().isFieldPresent(sfHookWeakGas))
newHookWeakGas = hookSetObj->get().getFieldU32(sfHookWeakGas);
}
// users may destroy a namespace in any operation except NOOP and
@@ -1864,40 +1638,6 @@ SetHook::setHook()
newHook.setFieldH256(sfHookCanEmit, *newHookCanEmit);
}
auto const defVersion =
oldDefSLE->getFieldU16(sfHookApiVersion);
if (defVersion == 0 && (newHookCallbackGas || newHookWeakGas))
return tecHOOK_INVALID;
if (!defHookCallbackGas.has_value() && newHookCallbackGas)
return tecHOOK_INVALID;
if (newHookWeakGas)
{
if (defHookWeakGas.has_value() &&
*defHookWeakGas == *newHookWeakGas)
{
if (newHook.isFieldPresent(sfHookWeakGas))
newHook.makeFieldAbsent(sfHookWeakGas);
}
else
newHook.setFieldU32(sfHookWeakGas, *newHookWeakGas);
}
if (newHookCallbackGas)
{
if (defHookCallbackGas.has_value() &&
*defHookCallbackGas == *newHookCallbackGas)
{
if (newHook.isFieldPresent(sfHookCallbackGas))
newHook.makeFieldAbsent(sfHookCallbackGas);
}
else
newHook.setFieldU32(
sfHookCallbackGas, *newHookCallbackGas);
}
// parameters
if (hookSetObj->get().isFieldPresent(sfHookParameters) &&
hookSetObj->get().getFieldArray(sfHookParameters).empty())
@@ -1942,10 +1682,6 @@ SetHook::setHook()
if (flags)
newHook.setFieldU32(sfFlags, *flags);
TER result = validateGasHook(newHook, oldDefSLE);
if (!isTesSuccess(result))
return result;
newHooks.push_back(std::move(newHook));
continue;
}
@@ -2080,24 +1816,10 @@ SetHook::setHook()
newHookDef->setFieldH256(
sfHookSetTxnID, ctx.tx.getTransactionID());
newHookDef->setFieldU64(sfReferenceCount, 1);
// Set HookCallbackGas if present in hookSetObj
if (hookSetObj->get().isFieldPresent(sfHookCallbackGas))
newHookDef->setFieldU32(
sfHookCallbackGas,
hookSetObj->get().getFieldU32(sfHookCallbackGas));
// Set HookWeakGas if present in hookSetObj
if (hookSetObj->get().isFieldPresent(sfHookWeakGas))
newHookDef->setFieldU32(
sfHookWeakGas,
hookSetObj->get().getFieldU32(sfHookWeakGas));
if (hookSetObj->get().getFieldU16(sfHookApiVersion) != 1)
newHookDef->setFieldAmount(
sfFee,
XRPAmount{
hook::computeExecutionFee(maxInstrCountHook)});
newHookDef->setFieldAmount(
sfFee,
XRPAmount{
hook::computeExecutionFee(maxInstrCountHook)});
if (maxInstrCountCbak > 0)
newHookDef->setFieldAmount(
sfHookCallbackFee,
@@ -2119,38 +1841,6 @@ SetHook::setHook()
slesToInsert.emplace(keylet, newHookDef);
newHook.setFieldH256(sfHookHash, *createHookHash);
// Set HookCallbackGas in Hook object only if different from
// Definition
if (hookSetObj->get().isFieldPresent(sfHookCallbackGas))
{
uint32_t objGas =
hookSetObj->get().getFieldU32(sfHookCallbackGas);
if (!newHookDef->isFieldPresent(sfHookCallbackGas) ||
newHookDef->getFieldU32(sfHookCallbackGas) !=
objGas)
{
newHook.setFieldU32(sfHookCallbackGas, objGas);
}
}
// Set HookWeakGas in Hook object only if different from
// Definition
if (hookSetObj->get().isFieldPresent(sfHookWeakGas))
{
uint32_t objGas =
hookSetObj->get().getFieldU32(sfHookWeakGas);
if (!newHookDef->isFieldPresent(sfHookWeakGas) ||
newHookDef->getFieldU32(sfHookWeakGas) != objGas)
{
newHook.setFieldU32(sfHookWeakGas, objGas);
}
}
TER result = validateGasHook(newHook, newHookDef);
if (!isTesSuccess(result))
return result;
newHooks.push_back(std::move(newHook));
continue;
}
@@ -2260,28 +1950,6 @@ SetHook::setHook()
*defHookCanEmit == *newHookCanEmit))
newHook.setFieldH256(sfHookCanEmit, *newHookCanEmit);
auto const defVersion =
newDefSLE->getFieldU16(sfHookApiVersion);
if (defVersion == 0 && (newHookCallbackGas || newHookWeakGas))
return tecHOOK_INVALID;
if (!defHookCallbackGas.has_value() && newHookCallbackGas)
return tecHOOK_INVALID;
if (newHookCallbackGas &&
!(defHookCallbackGas.has_value() &&
*defHookCallbackGas == *newHookCallbackGas))
newHook.setFieldU32(sfHookCallbackGas, *newHookCallbackGas);
if ((!flags || !(*flags & hsfCOLLECT)) && newHookWeakGas)
return tecHOOK_INVALID;
if (newHookWeakGas &&
!(defHookWeakGas.has_value() &&
*defHookWeakGas == *newHookWeakGas))
newHook.setFieldU32(sfHookWeakGas, *newHookWeakGas);
// parameters
TER result = updateHookParameters(
ctx,
@@ -2305,10 +1973,6 @@ SetHook::setHook()
if (flags)
newHook.setFieldU32(sfFlags, newFlags);
result = validateGasHook(newHook, newDefSLE);
if (!isTesSuccess(result))
return result;
newHooks.push_back(std::move(newHook));
slesToUpdate.emplace(*newDefKeylet, newDefSLE);
@@ -2326,8 +1990,6 @@ SetHook::setHook()
}
}
JLOG(ctx.j.warn()) << "HookSet: setHook after for loops";
int reserveDelta = 0;
{
// compute owner counts before modifying anything on ledger
@@ -2337,8 +1999,7 @@ SetHook::setHook()
// sfParameters: 1 reserve PER entry
// sfGrants are: 1 reserve PER entry
// sfHookHash, sfHookNamespace, sfHookOn, sfHookOnOutgoing,
// sfHookOnIncoming, sfHookCanEmit, sfHookApiVersion, sfHookCallbackGas,
// sfHookWeakGas, sfFlags: free
// sfHookOnIncoming, sfHookCanEmit sfHookApiVersion, sfFlags: free
// sfHookDefinition is not reserved because it is an unowned object,
// rather the uploader is billed via fee according to the following:
@@ -2485,7 +2146,6 @@ SetHook::setHook()
view().update(accountSLE);
}
JLOG(ctx.j.warn()) << "HookSet: setHook end";
return nsDeleteResult;
} // namespace ripple

View File

@@ -101,11 +101,6 @@ preflight1(PreflightContext const& ctx)
return temMALFORMED;
}
if (ctx.tx.isFieldPresent(sfHookGas) && !ctx.rules.enabled(featureHookGas))
{
return temMALFORMED;
}
auto const ret = preflight0(ctx);
if (!isTesSuccess(ret))
return ret;
@@ -239,12 +234,6 @@ Transactor::Transactor(ApplyContext& ctx)
{
}
XRPAmount
calculateHookGas(uint32_t gas)
{
return XRPAmount{gas};
}
// RH NOTE: this only computes one chain at a time, so if there is a receiving
// side to a txn then it must seperately be computed by a second call here
XRPAmount
@@ -260,7 +249,6 @@ Transactor::calculateHookChainFee(
return XRPAmount{0};
XRPAmount fee{0};
uint32_t gasTypeHookCount = 0; // Gas type hook counter
auto const& hooks = hookSLE->getFieldArray(sfHooks);
@@ -295,56 +283,18 @@ Transactor::calculateHookChainFee(
if (hook::canHook(tx.getTxnType(), hookOn) &&
(!collectCallsOnly || (flags & hook::hsfCOLLECT)))
{
// get HookApiVersion
uint16_t apiVersion = hookDef->getFieldU16(sfHookApiVersion);
XRPAmount const toAdd{hookDef->getFieldAmount(sfFee).xrp().drops()};
if (apiVersion == 0) // Guard type
{
// existing logic: read HookDefinition's sfFee
XRPAmount const toAdd{
hookDef->getFieldAmount(sfFee).xrp().drops()};
// this overflow should never happen, if somehow it does
// fee is set to the largest possible valid xrp value to force
// fail the transaction
if (fee + toAdd < fee)
fee = XRPAmount{INITIAL_XRP.drops()};
else
fee += toAdd;
}
else if (apiVersion == 1) // Gas type
{
if (!collectCallsOnly)
{
// Gas type: only count
gasTypeHookCount++;
}
else
{
auto const weakFee = hookObj.isFieldPresent(sfHookWeakGas)
? hookObj.getFieldU32(sfHookWeakGas)
: hookDef->getFieldU32(sfHookWeakGas);
XRPAmount const toAdd = calculateHookGas(weakFee);
if (fee + toAdd < fee)
fee = XRPAmount{INITIAL_XRP.drops()};
else
fee += toAdd;
}
}
// this overflow should never happen, if somehow it does
// fee is set to the largest possible valid xrp value to force
// fail the transaction
if (fee + toAdd < fee)
fee = XRPAmount{INITIAL_XRP.drops()};
else
fee += toAdd;
}
}
// Additional cost for Gas type: baseFee * 100 /Hook = 10*100 drops/Hook
if (gasTypeHookCount > 0)
{
auto const baseGasFee = view.fees().base * 100;
XRPAmount const gasTypeFee{gasTypeHookCount * baseGasFee};
if (fee + gasTypeFee < fee)
fee = XRPAmount{INITIAL_XRP.drops()}; // overflow
else
fee += gasTypeFee;
}
return fee;
}
@@ -407,51 +357,6 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
hookExecutionFee += toAdd;
}
if (const auto hookSLE =
view.read(keylet::hook(tx.getAccountID(sfAccount))))
{
const auto& hooks = hookSLE->getFieldArray(sfHooks);
for (auto const& hookObj : hooks)
{
if (hookObj.isFieldPresent(sfHookHash))
{
if (hookObj.getFieldH256(sfHookHash) !=
callbackHookHash)
continue;
uint32_t callbackGas = 0;
// Priority 1: Check HookObject
if (hookObj.isFieldPresent(sfHookCallbackGas))
{
callbackGas =
hookObj.getFieldU32(sfHookCallbackGas);
}
// Priority 2: Check HookDefinition
else if (
hookDef &&
hookDef->isFieldPresent(sfHookCallbackGas))
{
callbackGas =
hookDef->getFieldU32(sfHookCallbackGas);
}
// Priority 3: Default to 0 (implicit)
if (callbackGas > 0)
{
XRPAmount const toAdd =
calculateHookGas(callbackGas);
if (hookExecutionFee + toAdd < hookExecutionFee)
hookExecutionFee =
XRPAmount{INITIAL_XRP.drops()};
else
hookExecutionFee += toAdd;
}
break;
}
}
}
XRPL_ASSERT(
emitDetails.isFieldPresent(sfEmitBurden),
"Transactor::calculateBaseFee : emit burden not present");
@@ -471,10 +376,6 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
if (canRollback)
hookExecutionFee += calculateHookChainFee(
view, tx, keylet::hook(tshAcc), false);
if (view.rules().enabled(featureHookGas) &&
tx.isFieldPresent(sfHookGas))
hookExecutionFee += calculateHookGas(tx.getFieldU32(sfHookGas));
}
XRPAmount accumulator = baseFee;
@@ -1395,15 +1296,6 @@ Transactor::executeHookChain(
std::map<uint256, std::map<std::vector<uint8_t>, std::vector<uint8_t>>>
hookParamOverrides{};
// Initialize Gas pool for Gas-type hooks
uint32_t gasPool = 0;
if (ctx_.tx.isFieldPresent(sfHookGas))
{
gasPool = ctx_.tx.getFieldU32(sfHookGas);
JLOG(j_.trace()) << "HookChain: Initialized Gas pool with " << gasPool
<< " instructions";
}
auto const& hooks = hookSLE->getFieldArray(sfHooks);
uint8_t hook_no = 0;
@@ -1468,30 +1360,7 @@ Transactor::executeHookChain(
return tecINTERNAL;
}
bool hasCallback = hookDef->isFieldPresent(sfHookCallbackFee) ||
hookDef->isFieldPresent(sfHookCallbackGas);
// Extract HookApiVersion for Gas-type hooks
uint16_t hookApiVersion = hookDef->isFieldPresent(sfHookApiVersion)
? hookDef->getFieldU16(sfHookApiVersion)
: 0;
// Prepare Gas limit for this hook execution
uint32_t hookGas = 0;
if (hookApiVersion == 1)
{
if (!strong) // WeakTSH execution
{
hookGas = hookObj.isFieldPresent(sfHookWeakGas)
? hookObj.getFieldU32(sfHookWeakGas)
: hookDef->getFieldU32(sfHookWeakGas);
}
else // Strong execution
{
// Pass remaining Gas pool to this hook
hookGas = gasPool;
}
}
bool hasCallback = hookDef->isFieldPresent(sfHookCallbackFee);
try
{
@@ -1511,35 +1380,12 @@ Transactor::executeHookChain(
strong,
(strong ? 0 : 1UL), // 0 = strong, 1 = weak
hook_no - 1,
provisionalMeta,
hookApiVersion,
hookGas));
provisionalMeta));
executedHookCount_++;
hook::HookResult& hookResult = results.back();
// Track Gas consumption for Gas-type hooks
if (hookApiVersion == 1)
{
uint64_t consumed = hookResult.instructionCost;
JLOG(j_.trace()) << "HookChain: Hook consumed " << consumed
<< " instructions. Pool before: " << gasPool;
if (consumed >= gasPool)
{
JLOG(j_.trace()) << "HookError: Gas pool exhausted. "
<< "Hook tried to consume " << consumed
<< " but only " << gasPool << " remained.";
return tecHOOK_INSUFFICIENT_GAS;
}
gasPool -= consumed;
JLOG(j_.trace()) << "HookChain: Pool after: " << gasPool;
}
if (hookResult.exitType != hook_api::ExitType::ACCEPT)
{
if (results.back().exitType == hook_api::ExitType::WASM_ERROR)
@@ -1619,8 +1465,7 @@ Transactor::doHookCallback(
return;
}
if (!hookDef->isFieldPresent(sfHookCallbackFee) &&
!hookDef->isFieldPresent(sfHookCallbackGas))
if (!hookDef->isFieldPresent(sfHookCallbackFee))
{
JLOG(j_.trace()) << "HookInfo[" << callbackAccountID
<< "]: Callback specified by emitted txn "
@@ -1677,24 +1522,6 @@ Transactor::doHookCallback(
{
hook::HookStateMap stateMap;
// Extract HookApiVersion for callback
uint16_t hookApiVersion = hookDef->getFieldU16(sfHookApiVersion);
// Get callback gas with fallback priority:
// 1. HookObject's HookCallbackGas
// 2. HookDefinition's HookCallbackGas
// 3. Transaction's HookGas (for backward compatibility)
uint32_t hookGas = 0;
if (hookObj.isFieldPresent(sfHookCallbackGas))
{
hookGas = hookObj.getFieldU32(sfHookCallbackGas);
}
else if (hookDef->isFieldPresent(sfHookCallbackGas))
{
hookGas = hookDef->getFieldU32(sfHookCallbackGas);
}
hook::HookResult callbackResult = hook::apply(
hookDef->getFieldH256(sfHookSetTxnID),
callbackHookHash,
@@ -1714,9 +1541,7 @@ Transactor::doHookCallback(
? 1UL
: 0UL,
hook_no - 1,
provisionalMeta,
hookApiVersion,
hookGas);
provisionalMeta);
executedHookCount_++;
@@ -2005,19 +1830,6 @@ Transactor::doAgainAsWeak(
return;
}
// Extract HookApiVersion for aaw execution
uint16_t hookApiVersion = hookDef->getFieldU16(sfHookApiVersion);
// Extract HookGas for Gas-type hooks
uint32_t hookGasWeak = 0;
if (hookApiVersion == 1)
{
if (hookObj.isFieldPresent(sfHookWeakGas))
hookGasWeak = hookObj.getFieldU32(sfHookWeakGas);
else if (hookDef->isFieldPresent(sfHookWeakGas))
hookGasWeak = hookDef->getFieldU32(sfHookWeakGas);
}
try
{
hook::HookResult aawResult = hook::apply(
@@ -2031,15 +1843,12 @@ Transactor::doAgainAsWeak(
stateMap,
ctx_,
hookAccountID,
hookDef->isFieldPresent(sfHookCallbackFee) ||
hookDef->isFieldPresent(sfHookCallbackGas),
hookDef->isFieldPresent(sfHookCallbackFee),
false,
false,
2UL, // param 2 = aaw
hook_no - 1,
provisionalMeta,
hookApiVersion,
hookGasWeak);
provisionalMeta);
executedHookCount_++;