Compare commits

...
Author SHA1 Message Date
tequ 47c37c0a76 fixHookNameValidation Amendment 2026-09-25 22:02:15 +09:00
tequ c6a292ca55 fix AMMCreate::calculateBaseFee to include hook fee (and base fee, multisign fee, ...) (#828) 2026-09-25 20:29:36 +10:00
tequ 1ae3fb3f57 fix: record rollback when accept()/rollback() reason is out of bounds (#803)
When a hook calls accept() or rollback() with a reason string pointer
outside of wasm memory, HOOK_EXIT returned OUT_OF_BOUNDS and let the hook
keep running. If the hook then returned normally, the execution ended
with exitType UNSET and the HookExecution metadata carried no useful
information about what happened.

Behind the new fixHookExitOutOfBounds amendment, HOOK_EXIT now terminates
the hook as a ROLLBACK with exitCode OUT_OF_BOUNDS (mirroring the guard
violation path), so HookResult and HookReturnCode in the transaction
metadata make the failure visible to hook developers.

Adds a SetHook test covering accept() and rollback() with an out of
bounds reason, with and without the amendment.

Fixes #767
2026-09-25 20:26:59 +10:00
Niq Dudfield 1ea6b76853 fix(server): preserve idle JSON-RPC keep-alives (#797) 2026-09-25 20:24:06 +10:00
tequ 8f4569a63c fixHookAPISType Amendment (#778) 2026-09-25 20:20:40 +10:00
Niq Dudfield b775f3c3c2 ci: add -Dcoverage_tool=gcov|llvm option, wire native llvm-cov in matrix (#740)
* ci: add -Dcoverage_tool=gcov|llvm option, wire native llvm-cov in matrix

introduces native llvm source-based coverage as an alternative to the
existing gcov + gcovr pipeline. driven by a new -Dcoverage_tool cache
variable (default 'gcov' for backwards compat; 'llvm' enables
-fprofile-instr-generate / -fcoverage-mapping + llvm-profdata + llvm-cov).

cmake:
- RippledSettings.cmake: add coverage_tool with validation
- RippledInterface.cmake: split coverage compile/link flags by tool
- RippledCov.cmake: dispatch to the new helper when tool=llvm
- CodeCoverageLLVM.cmake (new): setup_target_for_coverage_llvm() driving
  profraw -> profdata -> export with format-aware output (lcov/json/txt/html)

ci:
- new clang-20 llvm-cov coverage matrix row (apt.llvm.org bootstrap for
  clang >= 19 since 24.04 default repos cap at clang-18)
- conditional install of llvm-N vs gcovr based on coverage_tool
- artifact + codecov upload generalised to coverage.lcov | coverage.xml
- coverage cmake-args switched to *_FLAGS_DEBUG so the build action's
  stdlib flag isn't clobbered

temp (revert before merging to dev):
- matrix narrowed to just the llvm-cov row on non-main refs so we can
  iterate without burning runners
- 'coverage-llm' added to push trigger for direct-push CI runs

* ci: drop coverage-llm from push triggers, PR trigger covers it

* ci: actually narrow matrix to just the llvm-cov row

prior guard checked base_ref against ['dev','candidate','release'] which
excluded the very PR we're iterating on. drop the guard - this branch
is for iteration only and must be reverted before merging anyway.

* ci(deps): apply Wno-missing-template-arg-list workaround to Linux clang

grpc 1.50.1 hits -Werror=missing-template-arg-list-after-template-kw on
clang-19+. macOS clang already had this workaround in the conan profile;
mirror it to the Linux branch, gated on compiler==clang.

* ci(temp): disable all workflows except Nix GA on this branch

renames every non-nix workflow to .yml.disabled so they stop firing on
PR pushes while we iterate on the llvm-cov coverage row. MUST be
reverted before merging to dev.

* fix(cov): use absolute binary path in llvm-cov run command

bare 'rippled' wasn't on PATH and the build dir isn't '.', so the
profile-collection step failed with 'No such file or directory'.
splice the resolved absolute path back into Cov_EXECUTABLE before
the run command consumes it.

* ci(deps): replace hardcoded grpc workaround with matrix-driven conan_deps_cxxflags

new dependencies action input `conan_deps_cxxflags` (json list, default
'[]') drives `tools.build:cxxflags` in the conan profile. clearly named
to indicate the flags only apply to conan dependency builds, not the
rippled build itself.

removes the per-os/per-compiler hardcoded workaround (linux clang
conditional + unconditional macOS block) that used to set the same flag
in two places kept-in-sync by hand.

call sites:
- xahau-ga-nix.yml: clang-20 coverage row gets the
  -Wno-missing-template-arg-list-after-template-kw workaround for grpc 1.50.1
- xahau-ga-macos.yml.disabled: same flag plumbed through (preserves
  prior behaviour when re-enabled)

drop the workaround entries when grpc is bumped past the fix.

* fix(cov): tighten coverage_tool=llvm validation + macOS tool discovery

two bugs caught in review:

- RippledSettings.cmake: the clang-only guard for coverage_tool=llvm ran
  before coverage_test could auto-enable coverage. so
  '-Dcoverage_tool=llvm -Dcoverage_test=Foo' on a gcc build slipped past
  the guard and produced an instrumentation/tool mismatch. move the
  guard after the auto-enable block.

- CodeCoverageLLVM.cmake: _find_llvm_cov_tools unconditionally
  preferred xcrun's tools on APPLE, which on a homebrew clang-N build
  would pair the user's clang with xcode's llvm-cov - exactly the
  version mismatch the helper is trying to avoid. gate the xcrun branch
  on CMAKE_CXX_COMPILER_ID == AppleClang.

* chore(cov): drop dead BASE_DIRECTORY arg + revert .disabled file edit

- CodeCoverageLLVM.cmake: BASE_DIRECTORY was parsed but never used
  (no analog to gcovr's -r in the llvm-cov commands we emit). dead arg.
- xahau-ga-macos.yml.disabled: revert the conan_deps_cxxflags addition.
  the file is disabled so the edit was bit-rotting in unreachable code.
  whoever revives the macOS workflow can wire up the field then.

* Revert "chore(cov): drop ... .disabled file edit" (partial)

restore the conan_deps_cxxflags addition to xahau-ga-macos.yml.disabled.
prior commit framed it as dead code, but the .disabled rename is itself
a TEMP measure being reverted before merge - the field is needed for the
macOS workflow to keep building grpc 1.50.1 once it's re-enabled, since
the unconditional workaround was removed from the dependencies action.

* fix(ci): restore conan_deps_cxxflags on macOS workflow

reverts part of f8a30c528d - removing this line dropped the apple-clang
grpc workaround that the dependencies action used to apply
unconditionally before the matrix-driven refactor. the .disabled rename
is itself a TEMP measure being reverted before merge, so the field needs
to be in place when the macOS workflow comes back.

* ci: restore matrix-row comments explaining clang-20 + grpc workaround

* ci: dedupe conan_deps_cxxflags in macOS workflow row

prior turn-of-events created two identical conan_deps_cxxflags entries
in the same matrix row from overlapping restore commits. keep one.

* ci: undisable workflows, drop gcc-13 coverage row, keep clang-20 llvm-cov as sole codecov reporter

- restore the 8 .yml.disabled workflows back to .yml
- drop the gcc-13 gcov coverage matrix row; native clang-20 llvm-cov is
  now the only codecov reporter
- update minimal_matrix indices to match
- remove the TEMP narrowing block that filtered to just the llvm-cov row
- log line says '7 configs (build x6 + clang-20 llvm-cov coverage)' to
  reflect the new shape

* fix(ci): scope conan_deps_cxxflags per Conan package pattern

prior shape (bare json list -> 'tools.build:cxxflags=[...]') leaked into
the consumer/rippled build via the conan-generated toolchain
(CMAKE_CXX_FLAGS_INIT). on macOS, where the build action doesn't
override CMAKE_CXX_FLAGS, this would silently apply the workaround flag
to rippled itself.

reshape the input to a json object keyed by Conan package pattern:
  {"grpc/*":["-Wno-..."]}

action emits package-pattern scoped lines:
  grpc/*:tools.build:cxxflags=["-Wno-..."]

flags only apply while building the matching dependency. consumer
toolchain stays clean. python validator rejects the consumer pattern
('&') and malformed shapes.

* ci: bump apt retries to 5 to handle ppa.launchpadcontent.net flakes

* ci: relax patch coverage for PeerImp
2026-09-25 19:55:54 +10:00
23 changed files with 868 additions and 431 deletions

No files matched your search

+9
View File
@@ -19,6 +19,15 @@ coverage:
default:
target: auto
threshold: 2%
paths:
# PeerImp is historically hard to exercise in the current unit-test
# harness. Keep this list narrow; new testable code should remain
# covered by the default patch gate.
- "!src/xrpld/overlay/detail/PeerImp.cpp"
historically-untested:
target: 0%
paths:
- "src/xrpld/overlay/detail/PeerImp.cpp"
changes: false
github_checks:
@@ -50,6 +50,10 @@ inputs:
options:
- libstdcxx
- libcxx
conan_deps_cxxflags:
description: 'Extra cxxflags applied to Conan dependency package builds only (NOT the rippled build). JSON object keyed by Conan package pattern, e.g. {"grpc/*":["-Wno-foo"]}. Maps to <pattern>:tools.build:cxxflags.'
required: false
default: '{}'
outputs:
cache-hit:
@@ -81,6 +85,8 @@ runs:
- name: Configure Conan
shell: bash
env:
CONAN_DEPS_CXXFLAGS: ${{ inputs.conan_deps_cxxflags }}
run: |
# Create the default profile directory if it doesn't exist
mkdir -p ~/.conan2/profiles
@@ -105,7 +111,14 @@ runs:
os=${{ inputs.os }}
EOF
# Add buildenv and conf sections for Linux (not needed for macOS)
# [buildenv] + [conf] sections.
# Linux pins compiler executables; macOS uses the system toolchain.
# conan_deps_cxxflags (matrix-driven) optionally adds package-pattern
# scoped tools.build:cxxflags for Conan dependency builds only - typically
# grpc workarounds for newer clang's stricter diagnostics. Because these
# are profile-pattern scoped (e.g. grpc/*:...), they do NOT affect the
# consumer/rippled toolchain generated for the main build.
NEED_CONF=0
if [ "${{ inputs.os }}" = "Linux" ] && [ -n "${{ inputs.cc }}" ]; then
cat >> ~/.conan2/profiles/default <<EOF
@@ -116,16 +129,38 @@ runs:
[conf]
tools.build:compiler_executables={"c": "/usr/bin/${{ inputs.cc }}", "cpp": "/usr/bin/${{ inputs.cxx }}"}
EOF
NEED_CONF=1
fi
# Add macOS-specific conf if needed
if [ "${{ inputs.os }}" = "Macos" ]; then
cat >> ~/.conan2/profiles/default <<EOF
if [ -n "${CONAN_DEPS_CXXFLAGS}" ] && [ "${CONAN_DEPS_CXXFLAGS}" != "{}" ]; then
CONAN_DEPS_CXXFLAGS_LINES="$(python3 - <<'PY'
import json
import os
import sys
[conf]
# Workaround for gRPC with newer Apple Clang
tools.build:cxxflags=["-Wno-missing-template-arg-list-after-template-kw"]
EOF
raw = os.environ["CONAN_DEPS_CXXFLAGS"]
data = json.loads(raw)
if not isinstance(data, dict):
sys.exit("conan_deps_cxxflags must be a JSON object like {\"grpc/*\": [\"-Wno-...\"]}")
for pattern, flags in data.items():
if not isinstance(pattern, str) or not pattern:
sys.exit("conan_deps_cxxflags keys must be non-empty Conan package patterns")
if pattern == "&":
sys.exit("conan_deps_cxxflags must target dependency package patterns, not the consumer (&)")
if not isinstance(flags, list) or not all(isinstance(flag, str) for flag in flags):
sys.exit(f"{pattern}: cxxflags must be a JSON string list")
if flags:
print(f"{pattern}:tools.build:cxxflags={json.dumps(flags, separators=(',', ':'))}")
PY
)"
if [ -n "${CONAN_DEPS_CXXFLAGS_LINES}" ] && [ "$NEED_CONF" = "0" ]; then
echo "" >> ~/.conan2/profiles/default
echo "[conf]" >> ~/.conan2/profiles/default
fi
if [ -n "${CONAN_DEPS_CXXFLAGS_LINES}" ]; then
printf '%s\n' "${CONAN_DEPS_CXXFLAGS_LINES}" >> ~/.conan2/profiles/default
fi
fi
# Display profile for verification
+3
View File
@@ -107,6 +107,9 @@ jobs:
compiler: apple-clang
compiler_version: ${{ steps.detect-compiler.outputs.compiler_version }}
stdlib: libcxx
# grpc 1.50.1 trips clang-19+ -Werror=missing-template-arg-list-after-template-kw
# on Apple Clang. Drop when grpc is bumped past the fix.
conan_deps_cxxflags: '{"grpc/*":["-Wno-missing-template-arg-list-after-template-kw"]}'
- name: Build
uses: ./.github/actions/xahau-ga-build
+66 -22
View File
@@ -72,15 +72,26 @@ jobs:
"job_type": "build"
},
{
"compiler_id": "gcc-13-libstdcxx",
"compiler": "gcc",
"cc": "gcc-13",
"cxx": "g++-13",
"gcov": "gcov-13",
"compiler_version": 13,
"stdlib": "default",
# Latest stable Clang for the most accurate source-based
# coverage mapping (newer language features, fewer bugs in
# llvm-cov region inference). Pulled from apt.llvm.org since
# Ubuntu 24.04 default repos cap at clang-18.
"compiler_id": "clang-20-libcxx",
"compiler": "clang",
"cc": "clang-20",
"cxx": "clang++-20",
"compiler_version": 20,
"stdlib": "libcxx",
"configuration": "Debug",
"job_type": "coverage"
"job_type": "coverage",
"coverage_tool": "llvm",
"coverage_format": "lcov",
# grpc 1.50.1 uses `Foo::template Bar(...)` without an
# angle-bracket arg list; clang-19+ promoted that to
# -Werror. Drop when grpc is bumped past the fix.
"conan_deps_cxxflags": {
"grpc/*": ["-Wno-missing-template-arg-list-after-template-kw"]
}
},
{
"compiler_id": "clang-14-libstdcxx-gcc11",
@@ -131,7 +142,7 @@ jobs:
# Minimal matrix for PRs and feature branches
minimal_matrix = [
full_matrix[1], # gcc-13 (middle-ground gcc)
full_matrix[2], # gcc-13 coverage
full_matrix[2], # clang-20 llvm-cov coverage
full_matrix[3] # clang-14 (mature, stable clang)
]
@@ -207,9 +218,9 @@ jobs:
# Select the appropriate matrix
if use_full:
if force_full:
print(f"Using FULL matrix (7 configs) - forced by [ci-nix-full-matrix] tag")
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - forced by [ci-nix-full-matrix] tag")
else:
print(f"Using FULL matrix (7 configs) - targeting main branch")
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - targeting main branch")
matrix = full_matrix
else:
print(f"Using MINIMAL matrix (3 configs) - feature branch/PR")
@@ -257,9 +268,25 @@ jobs:
- name: Install build dependencies
run: |
# Bump apt's default 3 retries; papers over short upstream blips
# like the recurring ppa.launchpadcontent.net outages.
echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries
apt-get update
apt-get install -y software-properties-common
add-apt-repository ppa:ubuntu-toolchain-r/test -y
# apt.llvm.org for Clang versions newer than what Ubuntu 24.04 ships
# (24.04 default repos cap at clang-18). The bootstrap script adds
# the LLVM apt source for the requested version and runs apt-get update.
if [ "${{ matrix.compiler }}" = "clang" ] && [ "${{ matrix.compiler_version }}" -ge 19 ]; then
apt-get install -y wget gnupg lsb-release
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
chmod +x /tmp/llvm.sh
# `all` installs clang + libllvm + lldb + lld + the llvm-N package
# (which provides llvm-profdata-N / llvm-cov-N for coverage runs).
/tmp/llvm.sh ${{ matrix.compiler_version }} all
fi
apt-get update
apt-get install -y git python3 python-is-python3 pipx
pipx ensurepath
@@ -332,10 +359,16 @@ jobs:
pipx install "conan>=2.0,<3"
echo "$HOME/.local/bin" >> $GITHUB_PATH
# Install gcovr for coverage jobs
# Install coverage tooling
if [ "${{ matrix.job_type }}" = "coverage" ]; then
pipx install "gcovr>=7,<9"
apt-get install -y curl lcov
if [ "${{ matrix.coverage_tool }}" = "llvm" ]; then
# Native LLVM source-based coverage: llvm-profdata + llvm-cov.
# The clang-N package doesn't pull these in; the llvm-N package does.
apt-get install -y "llvm-${{ matrix.compiler_version }}"
else
pipx install "gcovr>=7,<9"
fi
fi
- name: Check environment
@@ -348,10 +381,15 @@ jobs:
which ${{ matrix.cxx }} && ${{ matrix.cxx }} --version || echo "${{ matrix.cxx }} not found"
which ccache && ccache --version || echo "ccache not found"
# Check gcovr for coverage jobs
# Check coverage tooling
if [ "${{ matrix.job_type }}" = "coverage" ]; then
which gcov && gcov --version || echo "gcov not found"
which gcovr && gcovr --version || echo "gcovr not found"
if [ "${{ matrix.coverage_tool }}" = "llvm" ]; then
which "llvm-profdata-${{ matrix.compiler_version }}" && "llvm-profdata-${{ matrix.compiler_version }}" --version || echo "llvm-profdata not found"
which "llvm-cov-${{ matrix.compiler_version }}" && "llvm-cov-${{ matrix.compiler_version }}" --version || echo "llvm-cov not found"
else
which gcov && gcov --version || echo "gcov not found"
which gcovr && gcovr --version || echo "gcovr not found"
fi
fi
echo "---- Full Environment ----"
@@ -378,6 +416,7 @@ jobs:
cc: ${{ matrix.cc }}
cxx: ${{ matrix.cxx }}
stdlib: ${{ matrix.stdlib }}
conan_deps_cxxflags: ${{ matrix.conan_deps_cxxflags && toJson(matrix.conan_deps_cxxflags) || '{}' }}
gha_cache_enabled: 'false' # Disable caching for self hosted runner
- name: Build
@@ -410,8 +449,9 @@ jobs:
cache_version: ${{ env.CACHE_VERSION }}
main_branch: ${{ env.MAIN_BRANCH_NAME }}
stdlib: ${{ matrix.stdlib }}
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness
cmake-args: '-Dcoverage=ON -Dcoverage_format=xml -Dcoverage_test_parallelism=$(($(nproc)/2)) -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_CXX_FLAGS="-O0" -DCMAKE_C_FLAGS="-O0"'
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness.
# Use *_FLAGS_DEBUG so the build action's stdlib flag (e.g. -stdlib=libc++) in CMAKE_CXX_FLAGS isn't clobbered.
cmake-args: '-Dcoverage=ON -Dcoverage_tool=${{ matrix.coverage_tool }} -Dcoverage_format=${{ matrix.coverage_format }} -Dcoverage_test_parallelism=$(($(nproc)/2)) -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_CXX_FLAGS_DEBUG="-g -O0" -DCMAKE_C_FLAGS_DEBUG="-g -O0"'
cmake-target: 'coverage'
ccache_max_size: '100G'
@@ -443,22 +483,26 @@ jobs:
- name: Move coverage report
if: matrix.job_type == 'coverage'
shell: bash
env:
COVERAGE_FILE: ${{ matrix.coverage_tool == 'llvm' && 'coverage.lcov' || 'coverage.xml' }}
run: |
mv "${{ env.build_dir }}/coverage.xml" ./
mv "${{ env.build_dir }}/${COVERAGE_FILE}" ./
echo "COVERAGE_FILE=${COVERAGE_FILE}" >> "$GITHUB_ENV"
- name: Archive coverage report
if: matrix.job_type == 'coverage'
uses: actions/upload-artifact@v4
with:
name: coverage.xml
path: coverage.xml
name: ${{ env.COVERAGE_FILE }}-${{ matrix.compiler_id }}
path: ${{ env.COVERAGE_FILE }}
retention-days: 30
- name: Upload coverage report
if: matrix.job_type == 'coverage'
uses: codecov/codecov-action@v5
with:
files: coverage.xml
files: ${{ env.COVERAGE_FILE }}
flags: ${{ matrix.coverage_tool }}
fail_ci_if_error: true
disable_search: true
verbose: true
+156
View File
@@ -0,0 +1,156 @@
#[===================================================================[
Native LLVM source-based code coverage helper.
Drives the -fprofile-instr-generate / -fcoverage-mapping pipeline:
1. Run instrumented binary with LLVM_PROFILE_FILE=...%m-%p.profraw
2. llvm-profdata merge -sparse -> coverage.profdata
3. llvm-cov export/show/report -> final report
Output filename per coverage_format:
lcov -> coverage.lcov
json -> coverage.json
txt | text -> coverage.txt
html | html-details -> <NAME>/index.html
#]===================================================================]
include(CMakeParseArguments)
# Locate llvm-profdata / llvm-cov, preferring versioned variants matching the
# Clang we're building with so we don't accidentally pair clang-20 with
# llvm-cov-14 (profile format mismatch -> hard failure).
function(_find_llvm_cov_tools)
if(LLVM_PROFDATA_PATH AND LLVM_COV_PATH)
return()
endif()
string(REGEX MATCH "^[0-9]+" _major "${CMAKE_CXX_COMPILER_VERSION}")
set(_pd_names llvm-profdata)
set(_cov_names llvm-cov)
if(_major)
list(PREPEND _pd_names "llvm-profdata-${_major}")
list(PREPEND _cov_names "llvm-cov-${_major}")
endif()
# Only delegate to xcrun when the *compiler* is AppleClang. On macOS with
# Homebrew/system clang-N, xcrun would resolve to Xcode's llvm tools which
# could be a different version - exactly the mismatch we want to avoid.
if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
execute_process(COMMAND xcrun -f llvm-profdata
OUTPUT_VARIABLE _pd_xcrun OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET RESULT_VARIABLE _pd_rc)
if(_pd_rc EQUAL 0 AND _pd_xcrun)
set(LLVM_PROFDATA_PATH "${_pd_xcrun}" CACHE FILEPATH "llvm-profdata" FORCE)
endif()
execute_process(COMMAND xcrun -f llvm-cov
OUTPUT_VARIABLE _cov_xcrun OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET RESULT_VARIABLE _cov_rc)
if(_cov_rc EQUAL 0 AND _cov_xcrun)
set(LLVM_COV_PATH "${_cov_xcrun}" CACHE FILEPATH "llvm-cov" FORCE)
endif()
endif()
if(NOT LLVM_PROFDATA_PATH)
find_program(LLVM_PROFDATA_PATH NAMES ${_pd_names})
endif()
if(NOT LLVM_COV_PATH)
find_program(LLVM_COV_PATH NAMES ${_cov_names})
endif()
endfunction()
function(setup_target_for_coverage_llvm)
set(oneValueArgs NAME FORMAT)
set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
cmake_parse_arguments(Cov "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
_find_llvm_cov_tools()
if(NOT LLVM_PROFDATA_PATH)
message(FATAL_ERROR "llvm-profdata not found (needed for coverage_tool=llvm)")
endif()
if(NOT LLVM_COV_PATH)
message(FATAL_ERROR "llvm-cov not found (needed for coverage_tool=llvm)")
endif()
if(NOT Cov_FORMAT)
set(Cov_FORMAT lcov)
endif()
set(_profraw_dir "${PROJECT_BINARY_DIR}/${Cov_NAME}-profraw")
set(_profdata "${PROJECT_BINARY_DIR}/${Cov_NAME}.profdata")
# Resolve binary path: accept either an absolute path or a bare target name
# (resolved against PROJECT_BINARY_DIR). Splice the resolved path back into
# Cov_EXECUTABLE so the run command invokes it via absolute path - bare
# names aren't on PATH and the build dir isn't `.` either.
list(GET Cov_EXECUTABLE 0 _exec_name)
if(IS_ABSOLUTE "${_exec_name}")
set(_binary "${_exec_name}")
else()
set(_binary "${PROJECT_BINARY_DIR}/${_exec_name}")
list(REMOVE_AT Cov_EXECUTABLE 0)
list(PREPEND Cov_EXECUTABLE "${_binary}")
endif()
# llvm-cov takes a single -ignore-filename-regex; OR our excludes together.
set(_ignore_regex "")
foreach(EXC IN LISTS Cov_EXCLUDE)
if(_ignore_regex)
string(APPEND _ignore_regex "|")
endif()
string(APPEND _ignore_regex "${EXC}")
endforeach()
set(_filter "")
if(_ignore_regex)
set(_filter "-ignore-filename-regex='${_ignore_regex}'")
endif()
# Pick llvm-cov subcommand + output file for the requested format. Each
# branch builds a single shell command string that we'll hand to bash -c.
if(Cov_FORMAT STREQUAL "lcov")
set(_output "${PROJECT_BINARY_DIR}/coverage.lcov")
set(_report_sh "${LLVM_COV_PATH} export -instr-profile='${_profdata}' -format=lcov ${_filter} '${_binary}' > '${_output}'")
elseif(Cov_FORMAT STREQUAL "json")
set(_output "${PROJECT_BINARY_DIR}/coverage.json")
set(_report_sh "${LLVM_COV_PATH} export -instr-profile='${_profdata}' -format=text ${_filter} '${_binary}' > '${_output}'")
elseif(Cov_FORMAT STREQUAL "txt" OR Cov_FORMAT STREQUAL "text")
set(_output "${PROJECT_BINARY_DIR}/coverage.txt")
set(_report_sh "${LLVM_COV_PATH} report -instr-profile='${_profdata}' ${_filter} '${_binary}' > '${_output}'")
elseif(Cov_FORMAT STREQUAL "html" OR Cov_FORMAT STREQUAL "html-details")
set(_output "${PROJECT_BINARY_DIR}/${Cov_NAME}/index.html")
set(_report_sh "${LLVM_COV_PATH} show -instr-profile='${_profdata}' -format=html -output-dir='${PROJECT_BINARY_DIR}/${Cov_NAME}' ${_filter} '${_binary}'")
else()
message(FATAL_ERROR "coverage_tool=llvm: unsupported coverage_format '${Cov_FORMAT}' (use lcov|json|txt|html)")
endif()
set(_merge_sh "${LLVM_PROFDATA_PATH} merge -sparse -o '${_profdata}' '${_profraw_dir}'/*.profraw")
if(CODE_COVERAGE_VERBOSE)
message(STATUS "[coverage:llvm] binary: ${_binary}")
message(STATUS "[coverage:llvm] profraw: ${_profraw_dir}")
message(STATUS "[coverage:llvm] profdata: ${_profdata}")
message(STATUS "[coverage:llvm] format: ${Cov_FORMAT}")
message(STATUS "[coverage:llvm] output: ${_output}")
if(_ignore_regex)
message(STATUS "[coverage:llvm] ignore: ${_ignore_regex}")
endif()
message(STATUS "[coverage:llvm] merge: ${_merge_sh}")
message(STATUS "[coverage:llvm] report: ${_report_sh}")
endif()
# %m: hash of the binary, %p: pid. Wipe the dir up front so stale profraw
# files can't leak into a fresh merge.
add_custom_target(${Cov_NAME}
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_profraw_dir}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${_profraw_dir}"
COMMAND ${CMAKE_COMMAND} -E env
"LLVM_PROFILE_FILE=${_profraw_dir}/rippled-%m-%p.profraw"
${Cov_EXECUTABLE} ${Cov_EXECUTABLE_ARGS}
COMMAND bash -c "${_merge_sh}"
COMMAND bash -c "${_report_sh}"
BYPRODUCTS ${_output}
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
DEPENDS ${Cov_DEPENDENCIES}
VERBATIM
COMMENT "Running llvm-cov (${Cov_FORMAT}) -> ${_output}"
)
endfunction()
+15
View File
@@ -11,6 +11,21 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
return()
endif()
if(coverage_tool STREQUAL "llvm")
include(CodeCoverageLLVM)
setup_target_for_coverage_llvm(
NAME coverage
FORMAT ${coverage_format}
EXECUTABLE rippled
EXECUTABLE_ARGS --unittest$<$<BOOL:${coverage_test}>:=${coverage_test}> --unittest-jobs ${coverage_test_parallelism} --quiet --unittest-log
EXCLUDE "src/test" "include/xrpl/beast/test" "include/xrpl/beast/unit_test" "${CMAKE_BINARY_DIR}/pb-xrpl.libpb"
DEPENDENCIES rippled
)
return()
endif()
# coverage_tool == "gcov" (default): existing gcovr-driven pipeline.
include(CodeCoverage)
# The instructions for these commands come from the `CodeCoverage` module,
+6 -4
View File
@@ -28,15 +28,17 @@ target_compile_options (opts
$<$<AND:$<BOOL:${is_gcc}>,$<COMPILE_LANGUAGE:CXX>>:-Wsuggest-override>
$<$<BOOL:${is_gcc}>:-Wno-maybe-uninitialized>
$<$<BOOL:${perf}>:-fno-omit-frame-pointer>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-g --coverage>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},llvm>>:-g -fprofile-instr-generate -fcoverage-mapping>
$<$<BOOL:${profile}>:-pg>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>)
target_link_libraries (opts
INTERFACE
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-g --coverage>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},llvm>>:-g -fprofile-instr-generate -fcoverage-mapping>
$<$<BOOL:${profile}>:-pg>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>)
+13 -1
View File
@@ -29,13 +29,25 @@ if(is_gcc OR is_clang)
"Unit tests parallelism for the purpose of coverage report.")
set(coverage_format "html-details" CACHE STRING
"Output format of the coverage report.")
set(coverage_tool "gcov" CACHE STRING
"Coverage instrumentation tool: 'gcov' (default, gcc/clang via --coverage + gcovr) or 'llvm' (clang only, native source-based coverage via -fprofile-instr-generate).")
set_property(CACHE coverage_tool PROPERTY STRINGS "gcov" "llvm")
if(NOT coverage_tool MATCHES "^(gcov|llvm)$")
message(FATAL_ERROR "coverage_tool must be 'gcov' or 'llvm', got '${coverage_tool}'")
endif()
set(coverage_extra_args "" CACHE STRING
"Additional arguments to pass to gcovr.")
"Additional arguments to pass to gcovr (gcov tool only).")
set(coverage_test "" CACHE STRING
"On gcc & clang, the specific unit test(s) to run for coverage. Default is all tests.")
if(coverage_test AND NOT coverage)
set(coverage ON CACHE BOOL "gcc/clang only" FORCE)
endif()
# Validate after coverage_test may have flipped coverage on, otherwise
# `-Dcoverage_tool=llvm -Dcoverage_test=Foo` on gcc would silently slip
# past the Clang guard and produce a broken instrumentation combo.
if(coverage AND coverage_tool STREQUAL "llvm" AND NOT is_clang)
message(FATAL_ERROR "coverage_tool=llvm requires Clang (got ${CMAKE_CXX_COMPILER_ID})")
endif()
option(wextra "compile with extra gcc/clang warnings enabled" ON)
else()
set(profile OFF CACHE BOOL "gcc/clang only" FORCE)
+7 -1
View File
@@ -244,7 +244,13 @@
<< "Tried to accept/rollback but specified " \
"memory outside of the wasm instance " \
<< "limit when specifying a reason string"; \
return OUT_OF_BOUNDS; \
if (!view.rules().enabled(fixHookExitOutOfBounds)) \
return OUT_OF_BOUNDS; \
/* fixHookExitOutOfBounds: terminate as a rollback so the \
failure is visible in HookExecution metadata */ \
hookCtx.result.exitType = hook_api::ExitType::ROLLBACK; \
hookCtx.result.exitCode = (int64_t)OUT_OF_BOUNDS; \
return RC_ROLLBACK; \
} \
/* assembly script and some other languages use utf16 for strings \
*/ \
@@ -34,6 +34,9 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FIX (HookNameValidation, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (HookExitOutOfBounds, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookAPISType, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(HookOnV2_1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(OnChainManifests, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
+7 -1
View File
@@ -300,7 +300,13 @@ BaseHTTPPeer<Handler, Impl>::do_read(yield_context do_yield)
{
complete_ = false;
error_code ec;
start_timer();
// Do not treat an established keep-alive waiting for its next request as
// an unfinished loopback request.
if (request_count_ == 0)
start_timer();
else
boost::beast::get_lowest_layer(impl().stream_)
.expires_after(std::chrono::seconds(timeoutSeconds));
boost::beast::http::async_read(
impl().stream_, read_buf_, message_, do_yield[ec]);
cancel_timer();
+2 -1
View File
@@ -1993,6 +1993,7 @@ private:
env.fund(XRP(10'000), alice, carol, gw);
env.fund(XRP(10'000), bob);
env.close();
env.trust(USD(1'000), alice, bob, carol);
env.trust(BTC(1'000), alice, bob, carol);
env.trust(EUR(1'000), alice, bob, carol);
@@ -3644,7 +3645,7 @@ private:
USD(10'000),
false,
0,
ammCrtFee(env).drops(),
ammCrtFee(env).drops() + 2 * env.current()->fees().base.drops(),
std::nullopt,
std::nullopt,
ms,
+3 -3
View File
@@ -5004,8 +5004,8 @@ private:
alice,
STAmount{USD, UINT64_C(30'000'0000000003), -10}));
// alice XRP balance is 30,000initial - 50 ammcreate fee -
// 10drops fee
BEAST_EXPECT(accountBalance(env, alice) == "29949999990");
// 10drops ammcreate base fee - 10drops fee
BEAST_EXPECT(accountBalance(env, alice) == "29949999980");
},
std::nullopt,
0,
@@ -5081,7 +5081,7 @@ private:
(xrpBalance + drops(5)).getText());
BEAST_EXPECT(
accountBalance(env, alice) ==
std::to_string(29'950'000'000 - baseFee + 80));
std::to_string(29'950'000'000 - 2 * baseFee + 80));
},
std::nullopt,
0,
+188 -319
View File
@@ -3941,310 +3941,142 @@ public:
auto const _source_object =
*strUnHex("81140000000000000000000000000000000000000000");
std::vector<std::pair<uint32_t, std::string>> data_list = {
{// UINT16 Version
sfVersion.getCode(),
"10100001"},
{// UINT32 Sequence
sfSequence.getCode(),
"2400000001"},
// UINT64
{// Uint64 ExchangeRate
sfExchangeRate.getCode(),
"360000000000000001"},
{// UINT256 EmailHash
sfEmailHash.getCode(),
"4100000000000000000000000000000000"},
{// UINT256 ObjectID
sfObjectID.getCode(),
"5E000000000000000000000000000000000000000000000000000000000000000"
"0"},
{// Amount NativeAmount
sfAmount.getCode(),
"61999999999999999999999999999999999999999999999999999999999999999"
"999999999999999999999999999999999"},
{// Amount IOUAmount
sfAmount.getCode(),
"614999999999999999"},
{// Object AmountEntry
sfAmountEntry.getCode(),
"E05B614000000000000064E1"},
{// Array Memos
sfMemos.getCode(),
"F9EA7D04DEADBEEFE1F1"},
{// UINT8 TickSize
sfTickSize.getCode(),
"00101001"},
{// UINT160 TakerPaysCurrency
sfTakerPaysCurrency.getCode(),
"01110000000000000000000000005553440000000000"},
{// PathSet Paths
sfPaths.getCode(),
"0112300000000000000000000000005553440000000000054F6F784A58F9EFB0A"
"9EB90B83464F9D166461900"},
{// Vector256 Amendments
sfAmendments.getCode(),
"03132042426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF"
"044EE"},
{// Issue ClaimCurrency
sfClaimCurrency.getCode(),
"05180000000000000000000000005553440000000000054F6F784A58F9EFB0A9E"
"B90B83464F9D1664619"},
{// XChainBridge Native-Native
sfXChainBridge.getCode(),
"011914AE123A8556F3CF91154711376AFB0F894F832B3D000000000000"
"000000000000000000000000000014AE123A8556F3CF91154711376AFB"
"0F894F832B3D0000000000000000000000000000000000000000"},
{// Currency BaseAsset
sfBaseAsset.getCode(),
"011A0000000000000000000000005553440000000000"},
};
if (env.closed()->rules().enabled(fixHookAPISType))
{
// encodings the legacy parser measures incorrectly
data_list.insert(
data_list.end(),
{
// MPT is not supported yet
// {// Issue ClaimCurrency MPT
// sfClaimCurrency.getCode(),
// "0518AE123A8556F3CF91154711376AFB0F894F832B3D0000000000"
// "00000000000000000000000000000100000001"},
{// XChainBridge default door + IOU, Native
sfXChainBridge.getCode(),
"0119000000000000000000000000005553440000000000AE123A85"
"56F3CF91154711376AFB0F894F832B3D14AE123A8556F3CF911547"
"11376AFB0F894F832B3D00000000000000000000000000000000000"
"00000"},
});
}
bool const fixEnabled = env.closed()->rules().enabled(fixHookAPISType);
for (auto const& data : data_list)
{
// use UINT16
auto source_object = _source_object;
auto field_object = *strUnHex("10100001");
// Version: 1
auto field_object = *strUnHex(data.second);
auto field_id = data.first;
auto const result = api.sto_emplace(
source_object, field_object, sfVersion.getCode());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(
result.value().size() ==
source_object.size() + field_object.size());
field_object.insert(
field_object.end(), source_object.begin(), source_object.end());
BEAST_EXPECT(result.value() == field_object);
// test invalid insert: trailing byte after the field
auto ft = field_object;
ft.push_back(0);
auto const result3 = api.sto_emplace(source_object, ft, field_id);
// legacy parser ignores trailing bytes in the field
if (fixEnabled)
BEAST_EXPECTS(!result3.has_value(), data.second);
else
BEAST_EXPECTS(result3.has_value(), data.second);
auto const erase_result = api.sto_emplace(
result.value(), std::nullopt, sfVersion.getCode());
BEAST_EXPECT(erase_result.has_value());
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
BEAST_EXPECT(erase_result.value() == _source_object);
// truncated field / source are only exercised after the fix;
// the legacy parser may read past the end of these buffers
if (!fixEnabled)
continue;
auto const fo = Blob{field_object.begin(), field_object.end() - 1};
auto const result1 = api.sto_emplace(source_object, fo, field_id);
BEAST_EXPECTS(!result1.has_value(), data.second);
auto const so =
Blob{source_object.begin(), source_object.end() - 1};
auto const result2 = api.sto_emplace(so, field_object, field_id);
BEAST_EXPECTS(!result2.has_value(), data.second);
}
for (auto const& data : data_list)
{
// use UINT32
auto source_object = _source_object;
auto field_object = *strUnHex("2400000001");
// Sequence: 1
auto const result = api.sto_emplace(
source_object, field_object, sfSequence.getCode());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(
result.value().size() ==
source_object.size() + field_object.size());
field_object.insert(
field_object.end(), source_object.begin(), source_object.end());
BEAST_EXPECT(result.value() == field_object);
auto const erase_result = api.sto_emplace(
result.value(), std::nullopt, sfSequence.getCode());
BEAST_EXPECT(erase_result.has_value());
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
BEAST_EXPECT(erase_result.value() == _source_object);
}
{
// use UINT64
auto source_object = _source_object;
auto field_object = *strUnHex("360000000000000001");
// ExchangeRate: 1
auto const result = api.sto_emplace(
source_object, field_object, sfExchangeRate.getCode());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(
result.value().size() ==
source_object.size() + field_object.size());
field_object.insert(
field_object.end(), source_object.begin(), source_object.end());
BEAST_EXPECT(result.value() == field_object);
auto const erase_result = api.sto_emplace(
result.value(), std::nullopt, sfExchangeRate.getCode());
BEAST_EXPECT(erase_result.has_value());
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
BEAST_EXPECT(erase_result.value() == _source_object);
}
{
// use UINT128
auto source_object = _source_object;
auto field_object = *strUnHex("4100000000000000000000000000000000");
// EmailHash: 1
auto const result = api.sto_emplace(
source_object, field_object, sfEmailHash.getCode());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(
result.value().size() ==
source_object.size() + field_object.size());
field_object.insert(
field_object.end(), source_object.begin(), source_object.end());
BEAST_EXPECT(result.value() == field_object);
auto const erase_result = api.sto_emplace(
result.value(), std::nullopt, sfEmailHash.getCode());
BEAST_EXPECT(erase_result.has_value());
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
BEAST_EXPECT(erase_result.value() == _source_object);
}
{
// use UINT256
auto source_object = _source_object;
auto field_object = *strUnHex(
"5E000000000000000000000000000000000000000000000000000000000000"
"0000");
// ObjectID:
// "0000000000000000000000000000000000000000000000000000000000000000"
auto const result = api.sto_emplace(
source_object, field_object, sfObjectID.getCode());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(
result.value().size() ==
source_object.size() + field_object.size());
field_object.insert(
field_object.end(), source_object.begin(), source_object.end());
BEAST_EXPECT(result.value() == field_object);
auto const erase_result = api.sto_emplace(
result.value(), std::nullopt, sfObjectID.getCode());
BEAST_EXPECT(erase_result.has_value());
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
BEAST_EXPECT(erase_result.value() == _source_object);
}
{
// use AMOUNT
auto source_object = _source_object;
auto nativeamount = *strUnHex(
"61999999999999999999999999999999999999999999999999999999999999"
"999999999999999999999999999999999999");
auto iouamount = *strUnHex("614999999999999999");
for (auto field_object : {nativeamount, iouamount})
{
auto const result = api.sto_emplace(
source_object, field_object, sfAmount.getCode());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(
result.value().size() ==
source_object.size() + field_object.size());
field_object.insert(
field_object.end(),
source_object.begin(),
source_object.end());
BEAST_EXPECT(result.value() == field_object);
auto const erase_result = api.sto_emplace(
result.value(), std::nullopt, sfAmount.getCode());
BEAST_EXPECT(erase_result.has_value());
BEAST_EXPECT(
erase_result.value().size() == _source_object.size());
BEAST_EXPECT(erase_result.value() == _source_object);
}
}
{
// OBJECT
auto source_object = _source_object;
auto field_object = *strUnHex("E05B614000000000000064E1");
// {"AmountEntry": {"Amount": "100"}}
auto const result = api.sto_emplace(
source_object, field_object, sfAmountEntry.getCode());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(
result.value().size() ==
source_object.size() + field_object.size());
source_object.insert(
source_object.end(), field_object.begin(), field_object.end());
BEAST_EXPECT(result.value() == source_object);
auto const erase_result = api.sto_emplace(
result.value(), std::nullopt, sfAmountEntry.getCode());
BEAST_EXPECT(erase_result.has_value());
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
BEAST_EXPECT(erase_result.value() == _source_object);
}
{
// ARRAY
auto source_object = _source_object;
auto field_object = *strUnHex("F9EA7D04DEADBEEFE1F1");
// {"Memos": [{"Memo":{ "MemoData": "DEADBEEF" }}]}
auto field_object = *strUnHex(data.second);
auto field_id = data.first;
auto const result =
api.sto_emplace(source_object, field_object, sfMemos.getCode());
BEAST_EXPECT(result.has_value());
api.sto_emplace(source_object, field_object, field_id);
BEAST_EXPECTS(result.has_value(), data.second);
auto const r = result.value();
BEAST_EXPECT(
result.value().size() ==
source_object.size() + field_object.size());
source_object.insert(
source_object.end(), field_object.begin(), field_object.end());
BEAST_EXPECT(result.value() == source_object);
r.size() == source_object.size() + field_object.size());
field_object.insert(
field_id < sfAccount.getCode() ? field_object.end()
: field_object.begin(),
source_object.begin(),
source_object.end());
BEAST_EXPECT(r == field_object);
auto const erase_result = api.sto_emplace(
result.value(), std::nullopt, sfMemos.getCode());
BEAST_EXPECT(erase_result.has_value());
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
BEAST_EXPECT(erase_result.value() == _source_object);
}
{
// UINT8
auto source_object = _source_object;
auto field_object = *strUnHex("00101001");
// {"TickSize": 1}
auto const result = api.sto_emplace(
source_object, field_object, sfTickSize.getCode());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(
result.value().size() ==
source_object.size() + field_object.size());
source_object.insert(
source_object.end(), field_object.begin(), field_object.end());
BEAST_EXPECT(result.value() == source_object);
auto const erase_result = api.sto_emplace(
result.value(), std::nullopt, sfTickSize.getCode());
BEAST_EXPECT(erase_result.has_value());
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
BEAST_EXPECT(erase_result.value() == _source_object);
}
{
// UINT160
auto source_object = _source_object;
auto field_object =
*strUnHex("01110000000000000000000000005553440000000000");
// {"TakerPaysCurrency": "0000000000000000000000005553440000000000"}
auto const result = api.sto_emplace(
source_object, field_object, sfTakerPaysCurrency.getCode());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(
result.value().size() ==
source_object.size() + field_object.size());
source_object.insert(
source_object.end(), field_object.begin(), field_object.end());
BEAST_EXPECT(result.value() == source_object);
auto const erase_result = api.sto_emplace(
result.value(), std::nullopt, sfTakerPaysCurrency.getCode());
BEAST_EXPECT(erase_result.has_value());
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
BEAST_EXPECT(erase_result.value() == _source_object);
}
{
// PATHSET
auto source_object = _source_object;
auto field_object = *strUnHex(
"0112300000000000000000000000005553440000000000054F6F784A58F9EF"
"B0A9EB90B83464F9D166461900");
// {"Paths": [[{ "currency": "USD", "issuer":
// "rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc" }]]}
auto const result =
api.sto_emplace(source_object, field_object, sfPaths.getCode());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(
result.value().size() ==
source_object.size() + field_object.size());
source_object.insert(
source_object.end(), field_object.begin(), field_object.end());
BEAST_EXPECT(result.value() == source_object);
auto const erase_result = api.sto_emplace(
result.value(), std::nullopt, sfPaths.getCode());
BEAST_EXPECT(erase_result.has_value());
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
BEAST_EXPECT(erase_result.value() == _source_object);
}
{
// VECTOR256
auto source_object = _source_object;
auto field_object = *strUnHex(
"03132042426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97"
"ABF044EE");
// {"Amendments":["42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE"]}
auto const result = api.sto_emplace(
source_object, field_object, sfAmendments.getCode());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(
result.value().size() ==
source_object.size() + field_object.size());
source_object.insert(
source_object.end(), field_object.begin(), field_object.end());
BEAST_EXPECT(result.value() == source_object);
auto const erase_result = api.sto_emplace(
result.value(), std::nullopt, sfAmendments.getCode());
BEAST_EXPECT(erase_result.has_value());
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
BEAST_EXPECT(erase_result.value() == _source_object);
}
{
// UINT96
}
{
// UINT384
}
{
// UINT512
// test erase
auto const erase_result =
api.sto_emplace(result.value(), std::nullopt, field_id);
BEAST_EXPECTS(erase_result.has_value(), data.second);
auto const er = erase_result.value();
BEAST_EXPECT(er.size() == _source_object.size());
BEAST_EXPECT(er == _source_object);
}
}
@@ -4283,6 +4115,38 @@ public:
PARSE_ERROR);
}
{
// Invalid: wrapped array has an invalid end marker.
// should be "F9EA7D02BEEFE1F1" {Memos:[{Memo:{MemoData:"BEEF"}}]}
auto const invalid_end_marker = *strUnHex("F9EA7D02BEEFE100");
auto const result = api.sto_subarray(invalid_end_marker, 0);
if (env.closed()->rules().enabled(fixHookAPISType))
{
BEAST_EXPECT(!result.has_value());
BEAST_EXPECT(result.error() == PARSE_ERROR);
}
else
{
BEAST_EXPECT(result.value() == std::make_pair(1u, 6u));
}
}
{
// Invalid: the first element is valid but a following element is
// truncated.
auto const invalid_tail = *strUnHex("F9EA7D02BEEFE1EA7D02BEEFF1");
auto const result = api.sto_subarray(invalid_tail, 0);
if (env.closed()->rules().enabled(fixHookAPISType))
{
BEAST_EXPECT(!result.has_value());
BEAST_EXPECT(result.error() == PARSE_ERROR);
}
else
{
BEAST_EXPECT(result.value() == std::make_pair(1u, 6u));
}
}
{
// doesn't found
// { Memos: [{Memo: {MemoData: "BEEF"}}] }
@@ -4338,6 +4202,25 @@ public:
PARSE_ERROR);
}
{
// Invalid: the requested field is valid but a later field is
// truncated.
auto const data = *strUnHex(
"240000000181140000000000000000000000000000000000000000");
auto const invalid_tail = Blob{data.begin(), data.end() - 1};
auto const result =
api.sto_subfield(invalid_tail, sfSequence.getCode());
if (env.closed()->rules().enabled(fixHookAPISType))
{
BEAST_EXPECT(!result.has_value());
BEAST_EXPECT(result.error() == PARSE_ERROR);
}
else
{
BEAST_EXPECT(result.value() == std::make_pair(1u, 4u));
}
}
{
// doesn't found
// { Memo: {MemoData: "BEEF"} }
@@ -4397,34 +4280,17 @@ public:
// { AmountEntry: {Amount: "100"} }
auto const amountEntry = *strUnHex("E05B614000000000000064E1");
BEAST_EXPECT(api.sto_validate(memos).value() == true);
BEAST_EXPECT(api.sto_validate(amounts).value() == true);
BEAST_EXPECT(api.sto_validate(memo).value() == true);
BEAST_EXPECT(api.sto_validate(amountEntry).value() == true);
for (auto const& data : {memos, amounts, memo, amountEntry})
{
// Valid data
BEAST_EXPECT(api.sto_validate(data).value() == true);
// Invalid data
BEAST_EXPECT(
api.sto_validate(Bytes{0xFF, 0xFF, 0xFF, 0xFF}).value() == false);
Bytes const i_memos(&memos[0], &memos[memos.size() - 1]);
Bytes const i_amounts(&amounts[0], &amounts[amounts.size() - 1]);
Bytes const i_memo(&memo[0], &memo[memo.size() - 1]);
Bytes const i_amountEntry(
&amountEntry[0], &amountEntry[amountEntry.size() - 1]);
BEAST_EXPECT(api.sto_validate(i_memos).value() == false);
BEAST_EXPECT(api.sto_validate(i_amounts).value() == false);
BEAST_EXPECT(api.sto_validate(i_memo).value() == false);
BEAST_EXPECT(api.sto_validate(i_amountEntry).value() == false);
Bytes const i2_memos(&memos[1], &memos[memos.size()]);
Bytes const i2_amounts(&amounts[1], &amounts[amounts.size()]);
Bytes const i2_memo(&memo[1], &memo[memo.size()]);
Bytes const i2_amountEntry(
&amountEntry[1], &amountEntry[amountEntry.size()]);
BEAST_EXPECT(api.sto_validate(i_memos).value() == false);
BEAST_EXPECT(api.sto_validate(i_amounts).value() == false);
BEAST_EXPECT(api.sto_validate(i_memo).value() == false);
BEAST_EXPECT(api.sto_validate(i_amountEntry).value() == false);
// Invalid data
auto const partial_data = Blob{data.begin(), data.end() - 1};
BEAST_EXPECT(api.sto_validate(partial_data).value() == false);
auto const partial_data2 = Blob{data.begin() + 1, data.end()};
BEAST_EXPECT(api.sto_validate(partial_data2).value() == false);
}
}
void
@@ -4917,9 +4783,12 @@ public:
test_state_set(features);
test_sto_emplace(features);
test_sto_emplace(features - fixHookAPISType);
// test_sto_erase(features); // tested in test_sto_emplace
test_sto_subarray(features);
test_sto_subarray(features - fixHookAPISType);
test_sto_subfield(features);
test_sto_subfield(features - fixHookAPISType);
test_sto_validate(features);
test_trace(features);
+169 -49
View File
@@ -2268,11 +2268,35 @@ public:
BEAST_EXPECT(!env.meta()->isFieldPresent(sfHookEmissions));
}
// Call named hook with the wrong hook name
// Call named hook with the wrong hook name (size == 0)
for (auto const fix : {true, false})
{
auto f = features - fixHookNameValidation;
if (fix)
f = f | fixHookNameValidation;
Env env{*this, f};
env.fund(XRP(10000), alice);
// execute both named and non-named hooks
auto jv = invoke::invoke(alice);
jv[jss::HookName] = "";
auto const expected = fix ? ter(temMALFORMED) : ter(tesSUCCESS);
env(jv,
M("Call named hook with the wrong hook name (size == 0)"),
HSFEE,
ter(expected));
env.close();
}
// Call named hook with the wrong hook name (size > 0)
{
auto jv = invoke::invoke(alice);
jv[jss::HookName] = "41424345";
env(jv, M("Call named hook with the wrong hook name"), HSFEE);
env(jv,
M("Call named hook with the wrong hook name (size > 0)"),
HSFEE);
env.close();
// execute only non-named hook
BEAST_EXPECT(!env.meta()->isFieldPresent(sfHookEmissions));
@@ -3438,6 +3462,81 @@ public:
env.close();
}
void
test_exit_out_of_bounds(FeatureBitset features)
{
testcase("Test accept()/rollback() with out of bounds reason");
using namespace jtx;
// reason string pointer outside of wasm memory, then fall through
TestHook accept_oob_wasm = wasm[R"[test.hook](
#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 hook(uint32_t reserved )
{
_g(1,1);
accept(0xFFFFFF00U, 32, 42);
return 0;
}
)[test.hook]"];
TestHook rollback_oob_wasm = wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
int64_t hook(uint32_t reserved )
{
_g(1,1);
rollback(0xFFFFFF00U, 32, 42);
return 0;
}
)[test.hook]"];
for (bool const withFix : {true, false})
{
for (auto const& hook_wasm : {accept_oob_wasm, rollback_oob_wasm})
{
Env env{
*this,
withFix ? features : features - fixHookExitOutOfBounds};
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
env.fund(XRP(10000), alice);
env.fund(XRP(10000), bob);
env(ripple::test::jtx::hook(alice, {{hso(hook_wasm)}}, 0),
M("Install OOB exit hook"),
HSFEE);
env.close();
env(pay(bob, alice, XRP(1)),
M("Test OOB exit hook"),
fee(XRP(1)),
ter(tecHOOK_REJECTED));
env.close();
auto const meta = env.meta();
BEAST_REQUIRE(meta && meta->isFieldPresent(sfHookExecutions));
auto const& execs = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(execs.size() == 1);
// with the fix: ROLLBACK with OUT_OF_BOUNDS (-1)
// without: UNSET (fixXahauV3) with default -1
BEAST_EXPECT(
execs[0].getFieldU8(sfHookResult) ==
static_cast<uint8_t>(
withFix ? hook_api::ExitType::ROLLBACK
: hook_api::ExitType::UNSET));
BEAST_EXPECT(
execs[0].getFieldU64(sfHookReturnCode) ==
0x8000000000000001ULL);
BEAST_EXPECT(execs[0].getFieldVL(sfHookReturnString).empty());
}
}
}
void
testGuards(FeatureBitset features)
{
@@ -12367,57 +12466,77 @@ public:
)[test.hook]"];
HASH_WASM(hook);
for (auto isfixHookAPI20251128 : {true, false})
for (bool const with20251128 : {true, false})
{
Env env{
*this,
isfixHookAPI20251128 ? features | fixHookAPI20251128
: features - fixHookAPI20251128};
env.fund(XRP(10000), alice, bob);
env.close();
// install the hook on alice
env(ripple::test::jtx::hook(
alice, {{hso(hook_wasm, overrideFlag)}}, 0),
M("set sto_subarray"),
HSFEE);
env.close();
EXPECT_HOOK_FEE(hook, 19);
// invoke the hook
env(pay(bob, alice, XRP(1)),
M("test sto_subarray"),
fee(XRP(1)));
env.close();
auto const meta = env.meta();
BEAST_REQUIRE(meta);
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookExecutions =
meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
auto const hookExecution = hookExecutions[0];
BEAST_REQUIRE(hookExecution.isFieldPresent(sfHookReturnCode));
auto const returnCode =
hookExecution.getFieldU64(sfHookReturnCode);
if (isfixHookAPI20251128)
for (bool const withSType : {true, false})
{
auto feats =
features - fixHookAPI20251128 - fixHookAPISType;
if (with20251128)
feats = feats | fixHookAPI20251128;
if (withSType)
feats = feats | fixHookAPISType;
Env env{*this, feats};
env.fund(XRP(10000), alice, bob);
env.close();
// install the hook on alice
env(ripple::test::jtx::hook(
alice, {{hso(hook_wasm, overrideFlag)}}, 0),
M("set sto_subarray"),
HSFEE);
env.close();
EXPECT_HOOK_FEE(hook, 19);
// invoke the hook
env(pay(bob, alice, XRP(1)),
M("test sto_subarray"),
fee(XRP(1)));
env.close();
auto const meta = env.meta();
BEAST_REQUIRE(meta);
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookExecutions =
meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
auto const hookExecution = hookExecutions[0];
BEAST_REQUIRE(
hookExecution.isFieldPresent(sfHookReturnCode));
auto const returnCode =
hookExecution.getFieldU64(sfHookReturnCode);
auto const doesntExistError = -5;
auto const position = 2;
auto const length = 12;
BEAST_REQUIRE(
returnCode ==
(doesntExistError + ((int64_t)position << 32) +
length));
}
else
{
auto const parseError = -18;
auto const position = 1;
auto const length = 33;
BEAST_REQUIRE(
returnCode ==
(parseError + ((int64_t)position << 32) + length));
if (with20251128)
{
auto const position = 2;
auto const length = 12;
// unwrapped correctly: 1 element at pos 2, len 12
BEAST_REQUIRE(
returnCode ==
(doesntExistError + ((int64_t)position << 32) +
length));
}
else if (withSType)
{
// legacy unwrap leaves 0x5C as a 32 byte field header,
// which is rejected as out of bounds.
// negative codes are stored as sign bit + magnitude
BEAST_REQUIRE(
returnCode ==
(0x8000'0000'0000'0000ULL |
-(parseError + parseError)));
}
else
{
auto const position = 1;
auto const length = 33;
// legacy: 32 byte field overruns the 13 byte buffer
BEAST_REQUIRE(
returnCode ==
(parseError + ((int64_t)position << 32) + length));
}
}
}
}
@@ -15396,6 +15515,7 @@ public:
testWasm(features);
test_accept(features);
test_rollback(features);
test_exit_out_of_bounds(features);
testGuards(features);
+58
View File
@@ -189,6 +189,64 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x3DU, 0x20U, 0x54U, 0x4FU, 0x4FU, 0x5FU, 0x42U, 0x49U, 0x47U, 0x00U,
}},
{R"[test.hook](
#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 hook(uint32_t reserved )
{
_g(1,1);
accept(0xFFFFFF00U, 32, 42);
return 0;
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x13U,
0x03U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x03U, 0x7FU,
0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U,
0x17U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U,
0x00U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U, 0x63U, 0x63U, 0x65U,
0x70U, 0x74U, 0x00U, 0x01U, 0x03U, 0x02U, 0x01U, 0x02U, 0x05U, 0x03U,
0x01U, 0x00U, 0x02U, 0x06U, 0x21U, 0x05U, 0x7FU, 0x01U, 0x41U, 0x80U,
0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU,
0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x88U,
0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x07U, 0x08U,
0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x02U, 0x0AU, 0xA1U,
0x80U, 0x00U, 0x01U, 0x9DU, 0x80U, 0x00U, 0x00U, 0x41U, 0x01U, 0x41U,
0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x41U, 0x80U,
0x7EU, 0x41U, 0x20U, 0x42U, 0x2AU, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U,
0x00U, 0x1AU, 0x42U, 0x00U, 0x0BU,
}},
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
int64_t hook(uint32_t reserved )
{
_g(1,1);
rollback(0xFFFFFF00U, 32, 42);
return 0;
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x13U,
0x03U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x03U, 0x7FU,
0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U,
0x19U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U,
0x00U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU,
0x62U, 0x61U, 0x63U, 0x6BU, 0x00U, 0x01U, 0x03U, 0x02U, 0x01U, 0x02U,
0x05U, 0x03U, 0x01U, 0x00U, 0x02U, 0x06U, 0x21U, 0x05U, 0x7FU, 0x01U,
0x41U, 0x80U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U,
0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U,
0x80U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU,
0x07U, 0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x02U,
0x0AU, 0xA1U, 0x80U, 0x00U, 0x01U, 0x9DU, 0x80U, 0x00U, 0x00U, 0x41U,
0x01U, 0x41U, 0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU,
0x41U, 0x80U, 0x7EU, 0x41U, 0x20U, 0x42U, 0x2AU, 0x10U, 0x81U, 0x80U,
0x80U, 0x80U, 0x00U, 0x1AU, 0x42U, 0x00U, 0x0BU,
}},
{R"[test.hook](
(module
(type (;0;) (func (param i32 i32) (result i64)))
+21
View File
@@ -28,7 +28,9 @@
#include <xrpl/protocol/jss.h>
#include <boost/lexical_cast.hpp>
#include <chrono>
#include <optional>
#include <thread>
#include <utility>
namespace ripple {
@@ -906,6 +908,24 @@ public:
pass();
}
void
testJSONRPCClientKeepAlive()
{
testcase("JSON-RPC keep-alive survives test work");
using namespace std::chrono_literals;
using namespace jtx;
Env env{*this};
env.client().invoke("server_info", {});
// The normal loopback message timeout is three seconds. Established
// Env connections have a separate idle lease because test work can
// legitimately take longer between RPCs.
std::this_thread::sleep_for(4s);
auto const response = env.client().invoke("server_info", {});
BEAST_EXPECT(response[jss::result][jss::status] == "success");
}
void
run() override
{
@@ -933,6 +953,7 @@ public:
testSignAndSubmit(all);
testFeatures(all);
testExceptionalShutdown();
testJSONRPCClientKeepAlive();
}
};
+3 -1
View File
@@ -141,7 +141,9 @@ AMM::create(
if (fee_ != 0)
jv[sfFee] = std::to_string(fee_);
else
jv[jss::Fee] = std::to_string(env_.current()->fees().increment.drops());
jv[jss::Fee] = std::to_string(
env_.current()->fees().increment.drops() +
env_.current()->fees().base.drops());
submit(jv, seq, ter);
if (!ter || env_.ter() == tesSUCCESS)
+1 -1
View File
@@ -180,7 +180,7 @@ AMMTest::reserve(jtx::Env& env, std::uint32_t count) const
XRPAmount
AMMTest::ammCrtFee(jtx::Env& env) const
{
return env.current()->fees().increment;
return env.current()->fees().increment + env.current()->fees().base;
}
jtx::Env
+9 -1
View File
@@ -1135,7 +1135,15 @@ public:
return types == typesOut;
};
// Find AMM objects
AMM amm(env, gw, XRP(1'000), USD(1'000));
// gw has a hook installed, so pay the hook execution fee too.
AMM amm(
env,
gw,
XRP(1'000),
USD(1'000),
false,
0,
env.current()->fees().increment.drops() + 1'000'000);
amm.deposit(alice, USD(1));
// AMM account has 4 objects: AMM object and 3 trustlines
auto const lines = getAccountLines(env, amm.ammAccount());
+79 -16
View File
@@ -101,6 +101,14 @@ HookAPI::sto_subfield(Bytes const& data, uint32_t field_id) const
if (data.size() < 2)
return Unexpected(TOO_SMALL);
if (hookCtx.applyCtx.view().rules().enabled(fixHookAPISType))
{
// validate the data
auto const valid = sto_validate(data);
if (!valid || !valid.value())
return Unexpected(PARSE_ERROR);
}
unsigned char* start = const_cast<unsigned char*>(data.data());
unsigned char* upto = start;
unsigned char* end = start + data.size();
@@ -165,6 +173,18 @@ HookAPI::sto_subarray(Bytes const& data, uint32_t index_id) const
unsigned char* upto = start;
unsigned char* end = start + data.size();
if (hookCtx.applyCtx.view().rules().enabled(fixHookAPISType))
{
// check if the array has valid trailing data
if ((*upto & 0xF0U) == 0xF0U && *(end - 1) != 0xF1U)
return Unexpected(PARSE_ERROR);
// validate the array
auto const valid = sto_validate(data);
if (!valid || !valid.value())
return Unexpected(PARSE_ERROR);
}
// unwrap the array if it is wrapped,
// by removing a byte from the start and end
// why here 0xF0?
@@ -259,6 +279,13 @@ HookAPI::sto_emplace(
return Unexpected(TOO_SMALL);
}
if (hookCtx.applyCtx.view().rules().enabled(fixHookAPISType))
{
auto const source_valid = sto_validate(source_object);
if (!source_valid || !source_valid.value())
return Unexpected(PARSE_ERROR);
}
if (field_object.has_value() &&
hookCtx.applyCtx.view().rules().enabled(fixHookAPI20251128))
{
@@ -279,6 +306,9 @@ HookAPI::sto_emplace(
0);
if (!length)
return Unexpected(PARSE_ERROR);
if (hookCtx.applyCtx.view().rules().enabled(fixHookAPISType) &&
length.value() != field_object->size())
return Unexpected(PARSE_ERROR);
if ((type << 16) + field != field_id)
{
return Unexpected(PARSE_ERROR);
@@ -3048,6 +3078,7 @@ HookAPI::get_stobject_length(
type == STI_PATHSET && rules.enabled(featureHookAPISerializedType240))
{
length = 0;
bool terminated = false;
while (upto + length < end)
{
// iterate Path step
@@ -3065,6 +3096,9 @@ HookAPI::get_stobject_length(
if (flag & 0x20) // issuer
length += 20;
if (rules.enabled(fixHookAPISType) && upto + length >= end)
return Unexpected(pe_unexpected_end);
int next_flag = *(upto + length);
if (next_flag == 0x00 || next_flag == 0xff)
// end of Path step
@@ -3076,43 +3110,68 @@ HookAPI::get_stobject_length(
if (lastflag == 0xff)
continue; // continue byte
else if (lastflag == 0x00)
{
terminated = true;
break; // end byte
}
else
return Unexpected(pe_unexpected_end);
}
if (upto >= end)
return Unexpected(pe_unexpected_end);
if (rules.enabled(fixHookAPISType) && !terminated)
return Unexpected(pe_unexpected_end);
}
else if (type == STI_ISSUE)
{
bool const fix = rules.enabled(fixHookAPISType);
auto zero20 = std::array<char, 20>{0};
// if first 20 byte is all zeros return 20
// else return 40
if (fix && end - upto < 20)
return Unexpected(pe_unexpected_end);
if (memcmp(upto, zero20.data(), 20) == 0)
length = 20;
// MPT is not supported yet
// else if (
// fix && end - upto >= 40 &&
// memcmp(upto + 20, noAccount().data(), 20) == 0)
// length = 44; // MPT: Issuer + noAccount + Sequence
else
length = 40;
}
else if (type == STI_XCHAIN_BRIDGE)
{
bool const fix = rules.enabled(fixHookAPISType);
auto zero20 = std::array<char, 20>{0};
// Lock Chain
length = 1; // Door Account1 prefix length
length += 20; // Door Account1 length
// Door Issue1
if (memcmp(upto + length, zero20.data(), 20) == 0)
length += 20; // only Currency
else
length += 40; // Currency and Issue
length = 0;
for (int i = 0; i < 2; ++i) // Locking Chain, Issuing Chain
{
// Door Account
if (!fix)
length += 21;
else if (end - upto <= length)
return Unexpected(pe_unexpected_end);
else if (upto[length] == 0)
length += 1; // default (empty) account
else if (upto[length] == 20)
length += 21;
else
return Unexpected(pe_unexpected_end);
// Issuing Chain
length += 1; // Door Account2 prefix length
length += 20; // Door Account2 length
// Door Issue2
if (memcmp(upto + length, zero20.data(), 20) == 0)
length += 20; // only Currency
else
length += 40; // Currency and Issue
// Door Issue
if (fix && end - upto < length + 20)
return Unexpected(pe_unexpected_end);
if (memcmp(upto + length, zero20.data(), 20) == 0)
length += 20; // only Currency
// MPT is not supported yet
// else if (
// fix && end - upto >= length + 40 &&
// memcmp(upto + length + 20, noAccount().data(), 20) == 0)
// length += 44; // MPT: Issuer + noAccount + Sequence
else
length += 40; // Currency and Issuer
}
}
if (length > -1)
@@ -3129,6 +3188,10 @@ HookAPI::get_stobject_length(
length,
payload_start,
payload_length);
if (rules.enabled(fixHookAPISType) && length > end - upto)
return Unexpected(pe_unexpected_end);
return length + (upto - start);
}
+2 -2
View File
@@ -81,8 +81,8 @@ AMMCreate::preflight(PreflightContext const& ctx)
XRPAmount
AMMCreate::calculateBaseFee(ReadView const& view, STTx const& tx)
{
// The fee required for AMMCreate is one owner reserve.
return view.fees().increment;
// The least fee required for AMMCreate is one owner reserve.
return Transactor::calculateBaseFee(view, tx) + view.fees().increment;
}
TER
+5 -1
View File
@@ -155,7 +155,11 @@ preflight1(PreflightContext const& ctx)
!ctx.rules.enabled(featureNamedHooks))
return temMALFORMED;
if (!SetHook::validateHookName(ctx.tx.getFieldVL(sfHookName), ctx.j))
auto const& name = ctx.tx.getFieldVL(sfHookName);
if (name.size() == 0 && ctx.rules.enabled(fixHookNameValidation))
return temMALFORMED;
if (!SetHook::validateHookName(name, ctx.j))
return temMALFORMED;
}