Compare commits

...
Author SHA1 Message Date
Richard Holland 46fc5f0a8f disable onchainmanifests for this release 2026-09-29 05:34:52 +10:00
tequ 4d08a9e603 etxn_fee_base: compute the fee against the applied view (fix20261001) (#842) 2026-09-29 05:02:37 +10:00
Niq Dudfield f155556017 test(jtx): reconnect the JSON-RPC client when the server closed it (#840) 2026-09-29 04:59:03 +10:00
tequ c4570a699f Move isValidUTF8 to xrpl/basics and its unit test to test/basics (#834) 2026-09-28 11:50:07 +10:00
tequ 426ff27e42 Merge fixHookNameValidation into fix20261001 (#837) 2026-09-28 11:48:50 +10:00
tequ e6bdd55803 Merge fixHookExitOutOfBounds into fix20261001 (#836) 2026-09-28 11:48:05 +10:00
tequ 9dd103fad4 Merge fixHookAPISType into fix20261001 (#835) 2026-09-28 11:46:59 +10:00
Richard Holland a7f9c683c4 fix keylet api (#833) 2026-09-26 14:24:00 +10:00
tequ 48a94af13d Add HookName validation to ClaimReward hookCan process (#786)
* Add HookName validation to ClaimReward hookCan process

* fixHookNameValidation Amendment (#776)

* Add fix20261001 Amendment placeholder (#831)

* address review

* use Slice instead Blob for canHook helper
2026-09-26 13:25:19 +10:00
Richard Holland 1ddda79c34 utf8 patch (#832) 2026-09-26 13:11:53 +10:00
tequ 76b7111098 Add fix20261001 Amendment placeholder (#831) 2026-09-26 11:07:18 +10:00
tequ 20f2b7f456 fixHookNameValidation Amendment (#776) 2026-09-26 10:45:24 +10: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
tequ 03d75df295 Reduce MagicEnum usage in server definitions (#775) 2026-09-25 19:47:54 +10:00
tequ c2e253b365 refactor new account seqno (#772)
When creating accounts in GenesisLedger, we previously set the account Sequence to 0. However, since this conflicts with the PseudoAccount requirements, we are changing it to 1.
There will be no impact on networks that are already running, and for future networks, there won't be any impact unless you create accounts via GenesisLedger.
This specifically addresses an issue that comes up during unittests.
2026-09-25 13:10:18 +10:00
tequandRichard Holland ba130f8363 AMMClawback supported::no (#827)
Co-authored-by: Richard Holland <[email protected]>
2026-09-25 12:36:31 +10:00
tequ e87eabd8be HookOnV2_1 Amendment (#766) 2026-09-25 11:17:12 +10:00
Niq Dudfield 2e32b5c6bb feat(server_definitions): distinguish config-forced from ledger-enabled amendments (#765) 2026-09-23 15:33:43 +10:00
tequ 902ed9b492 Use normal consequences factory for standard transactions (#774)
Replace redundant custom consequence factories that returned normal consequences with the built-in Normal factory.
2026-09-21 21:12:15 +10:00
63 changed files with 2274 additions and 985 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)
+1
View File
@@ -38,6 +38,7 @@
#define KEYLET_HOOK_DEFINITION 24
#define KEYLET_HOOK_STATE_DIR 25
#define KEYLET_CRON 26
#define KEYLET_MANIFEST 37
#define COMPARE_EQUAL 1U
#define COMPARE_LESS 2U
+60
View File
@@ -0,0 +1,60 @@
//------------------------------------------------------------------------------
/*
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_BASICS_UTF8_H_INCLUDED
#define RIPPLE_BASICS_UTF8_H_INCLUDED
#include <xrpl/basics/Slice.h>
namespace ripple {
/** Check that a byte sequence is well-formed UTF-8, excluding the two
noncharacters U+FFFE and U+FFFF.
Rejects:
- overlong encodings;
- UTF-16 surrogate halves (U+D800..U+DFFF);
- scalar values above U+10FFFF, and 0xF5..0xFF lead bytes;
- stray continuation bytes;
- multi-byte sequences truncated by the end of the buffer;
- U+FFFE and U+FFFF (EF BF BE, EF BF BF).
U+FFFE and U+FFFF are well-formed, but have no use in a URI or a name
and exist to be misread: U+FFFE is a byte-swapped BOM, so a client that
transcodes to UTF-16/32 and sniffs the byte order can be pushed into
decoding the rest of the string with the wrong endianness, and both are
commonly used as in-band sentinels. They have always been rejected by
URIToken and HookName validation, and this function preserves that.
This is consensus-critical. For every input the previous decoder could
evaluate without reading past the end of the buffer, the result is
identical to it. The only difference is that truncated trailing
sequences, where the previous decoder read out of bounds, are now always
rejected. Any further change to what this accepts needs an amendment.
@param s The bytes to check.
@note Total: every byte read is within s.
*/
bool
isValidUTF8(Slice const& s) noexcept;
} // namespace ripple
#endif
+1
View File
@@ -313,6 +313,7 @@ enum keylet_code : uint32_t {
MPTOKEN = 34,
CREDENTIAL = 35,
PERMISSIONED_DOMAIN = 36,
MANIFEST = 37,
};
}
+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(fix20261001)) \
return OUT_OF_BOUNDS; \
/* fix20261001: 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 \
*/ \
+6 -3
View File
@@ -34,7 +34,9 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FEATURE(OnChainManifests, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (20261001, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(HookOnV2_1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(OnChainManifests, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)
@@ -45,7 +47,7 @@ XRPL_FEATURE(HookAPISerializedType240, Supported::yes, VoteBehavior::DefaultNo
XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(DynamicNFT, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(Credentials, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(AMMClawback, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(AMMClawback, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(MPTokensV1, Supported::no, VoteBehavior::DefaultNo)
// InvariantsV1_1 will be changes to Supported::yes when all the
// invariants expected to be included under it are complete.
@@ -65,7 +67,8 @@ XRPL_FEATURE(XChainBridge, Supported::no, VoteBehavior::DefaultNo
XRPL_FEATURE(AMM, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (ReducedOffersV1, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(HookOnV2, Supported::yes, VoteBehavior::DefaultNo)
// replaced to HookOnV2_1
XRPL_FEATURE(HookOnV2, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (HookAPI20251128, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (CronStacking, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo)
+3 -1
View File
@@ -292,7 +292,9 @@ JSS(duration_us); // out: NetworkOPs
JSS(effective); // out: ValidatorList
// in: UNL
JSS(elapsed_seconds);
JSS(enabled); // out: AmendmentTable
JSS(enabled); // out: AmendmentTable (on-ledger);
// ServerDefinitions (this server)
JSS(ledger_enabled); // out: ServerDefinitions (on-ledger)
JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit
+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();
+101
View File
@@ -0,0 +1,101 @@
//------------------------------------------------------------------------------
/*
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 <xrpl/basics/UTF8.h>
namespace ripple {
bool
isValidUTF8(Slice const& s) noexcept
{
std::size_t const size = s.size();
// Markus Kuhn's utf8_check.c
// (https://www.cl.cam.ac.uk/~mgk25/ucs/utf8_check.c), the decoder
// URIToken has always used, with one change: each branch establishes
// that the whole sequence is present before reading any continuation
// byte, so a truncated tail is invalid rather than an out-of-bounds read.
std::size_t i = 0;
while (i < size)
{
std::uint8_t const c0 = s[i];
if (c0 < 0x80)
{
// 0xxxxxxx
++i;
continue;
}
if ((c0 & 0xE0) == 0xC0)
{
// 110xxxxx 10xxxxxx
if (size - i < 2)
return false;
if ((s[i + 1] & 0xC0) != 0x80)
return false;
if ((c0 & 0xFE) == 0xC0) // overlong
return false;
i += 2;
continue;
}
if ((c0 & 0xF0) == 0xE0)
{
// 1110xxxx 10xxxxxx 10xxxxxx
if (size - i < 3)
return false;
std::uint8_t const c1 = s[i + 1];
if ((c1 & 0xC0) != 0x80 || (s[i + 2] & 0xC0) != 0x80)
return false;
if (c0 == 0xE0 && (c1 & 0xE0) == 0x80) // overlong
return false;
if (c0 == 0xED && (c1 & 0xE0) == 0xA0) // UTF-16 surrogate
return false;
if (c0 == 0xEF && c1 == 0xBF &&
(s[i + 2] & 0xFE) == 0xBE) // U+FFFE or U+FFFF
return false;
i += 3;
continue;
}
if ((c0 & 0xF8) == 0xF0)
{
// 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
if (size - i < 4)
return false;
std::uint8_t const c1 = s[i + 1];
if ((c1 & 0xC0) != 0x80 || (s[i + 2] & 0xC0) != 0x80 ||
(s[i + 3] & 0xC0) != 0x80)
return false;
if (c0 == 0xF0 && (c1 & 0xF0) == 0x80) // overlong
return false;
if (c0 > 0xF4 || (c0 == 0xF4 && c1 > 0x8F)) // > U+10FFFF
return false;
i += 4;
continue;
}
// Stray continuation byte or 0xF8..0xFF.
return false;
}
return true;
}
} // namespace ripple
+5 -1
View File
@@ -150,12 +150,15 @@ private:
env.fund(XRP(20'000), alice, bob, carol, gw1, gw2);
env.fund(XRP(20'000), dan);
env.close();
env.trust(USD1(20'000), alice, bob, carol, dan);
env.trust(USD2(1'000), alice, bob, carol, dan);
env.close();
env(pay(gw1, dan, USD1(10'050)));
env(pay(gw1, bob, USD1(50)));
env(pay(gw2, bob, USD2(50)));
env.close();
AMM ammDan(env, dan, XRP(10'000), USD1(10'050));
@@ -1990,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);
@@ -3641,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,
+14
View File
@@ -563,6 +563,20 @@ public:
env(reward::claim(alice),
reward::issuer(issuer),
ter(tecNO_TARGET));
env.close();
}
// Invalid HookName
{
auto hookObj = hso(jtx::genesis::AcceptHook, overrideFlag);
hookObj[jss::HookOn] = to_string(~UINT256_BIT[ttCLAIM_REWARD]);
hookObj[jss::HookName] = "4142434445";
env(hook(issuer, {{hookObj}}, 0), fee(XRP(1)));
env.close();
env(reward::claim(alice),
reward::issuer(issuer),
ter(tecNO_TARGET));
env.close();
}
// Vaild HookOn
{
+239 -319
View File
@@ -732,6 +732,56 @@ public:
else
BEAST_EXPECT(result.value() == baseFee + memoSize);
}
{
// fix20261001: the minimum fee is computed against the view
// being applied, not the node-local open ledger. Simulate that
// by inserting a hook definition (with a callback fee) into
// applyCtx.view() only -- it is never inserted into the open
// ledger the node otherwise reads.
uint256 const hookHash{1};
STTx const emittedTx = STTx(ttINVOKE, [&](STObject& obj) {
obj[sfAccount] = alice.id();
obj[sfSequence] = 0;
obj[sfSigningPubKey] = Slice{};
obj[sfFirstLedgerSequence] = ov.seq() + 1;
obj[sfLastLedgerSequence] = ov.seq() + 5;
obj[sfFee] = env.closed()->fees().base;
auto& emitDetails = obj.peekFieldObject(sfEmitDetails);
emitDetails[sfEmitGeneration] = 1;
emitDetails[sfEmitBurden] = 1;
emitDetails[sfEmitParentTxnID] = invokeTx.getTransactionID();
emitDetails[sfEmitNonce] = uint256();
emitDetails[sfEmitHookHash] = hookHash;
});
Serializer const emittedTxSerializer = emittedTx.getSerializer();
auto const baseline =
api.etxn_fee_base(emittedTxSerializer.slice());
BEAST_EXPECT(baseline.has_value());
auto hookDef =
std::make_shared<SLE>(keylet::hookDefinition(hookHash));
hookDef->setFieldH256(sfHookHash, hookHash);
hookDef->setFieldH256(sfHookNamespace, uint256());
hookDef->setFieldArray(sfHookParameters, STArray{});
hookDef->setFieldU16(sfHookApiVersion, 0);
hookDef->setFieldVL(sfCreateCode, std::vector<uint8_t>{});
hookDef->setFieldH256(sfHookSetTxnID, invokeTx.getTransactionID());
hookDef->setFieldU64(sfReferenceCount, 1);
hookDef->setFieldAmount(sfFee, XRPAmount{0});
hookDef->setFieldAmount(sfHookCallbackFee, XRPAmount{12345});
applyCtx.view().insert(hookDef);
auto const result = api.etxn_fee_base(emittedTxSerializer.slice());
BEAST_EXPECT(result.has_value());
if (env.closed()->rules().enabled(fix20261001))
BEAST_EXPECT(result.value() == baseline.value() + 12345);
else
BEAST_EXPECT(result.value() == baseline.value());
}
}
void
@@ -3941,310 +3991,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(fix20261001))
{
// 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(fix20261001);
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 +4165,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(fix20261001))
{
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(fix20261001))
{
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 +4252,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(fix20261001))
{
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 +4330,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
@@ -4852,6 +4768,7 @@ public:
test_otxn_generation(features);
test_etxn_details(features);
test_etxn_fee_base(features - fixHookAPI20251128);
test_etxn_fee_base(features - fix20261001);
test_etxn_fee_base(features);
test_etxn_nonce(features);
test_etxn_reserve(features);
@@ -4917,9 +4834,12 @@ public:
test_state_set(features);
test_sto_emplace(features);
test_sto_emplace(features - fix20261001);
// test_sto_erase(features); // tested in test_sto_emplace
test_sto_subarray(features);
test_sto_subarray(features - fix20261001);
test_sto_subfield(features);
test_sto_subfield(features - fix20261001);
test_sto_validate(features);
test_trace(features);
+4 -4
View File
@@ -104,8 +104,8 @@ class LedgerMaster_test : public beast::unit_test::suite
startLegSeq, txnIndex);
BEAST_EXPECT(
*result ==
uint256("0CC11AD1AD89661689F6B6148D82CB6A7101DA4D66EC843670262D"
"0B618F5745"));
uint256("DCAECE52F028B9D0F7CA43CBE15AB4EF7B84A8EF5D455238992E1E"
"DF2BDA1637"));
}
// success (second tx)
{
@@ -114,8 +114,8 @@ class LedgerMaster_test : public beast::unit_test::suite
startLegSeq + 1, txnIndex);
BEAST_EXPECT(
*result ==
uint256("DCAECE52F028B9D0F7CA43CBE15AB4EF7B84A8EF5D455238992E1E"
"DF2BDA1637"));
uint256("722BD5EC9E4AE36E724B6223F5A5887927AADFD0B18A3053AAE57F"
"434F8C9FE8"));
}
}
+5 -5
View File
@@ -1174,11 +1174,11 @@ class MPToken_test : public beast::unit_test::suite
JsonOptions::none)[sfAffectedNodes.fieldName];
// Issuer got 10 in the transfer fees
BEAST_EXPECT(
meta[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
meta[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
[sfOutstandingAmount.fieldName] == "9990");
// Destination account got 9'990
BEAST_EXPECT(
meta[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
meta[3u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
[sfMPTAmount.fieldName] == "9990");
// Source account spent 10'000
BEAST_EXPECT(
@@ -1910,8 +1910,8 @@ class MPToken_test : public beast::unit_test::suite
env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
BEAST_EXPECTS(
txHash ==
"42175D43E3D236A86B0F1B07182A1B2C78AD382372CFA09A48239C395FFFEE"
"30",
"61041108F0DAD50BAFD2F7B1102AC70B283EBDCED194CEBAF04D0184E4A70B"
"02",
txHash);
Json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta];
auto const id = meta[jss::mpt_issuance_id].asString();
@@ -1919,7 +1919,7 @@ class MPToken_test : public beast::unit_test::suite
BEAST_EXPECT(meta.isMember(jss::mpt_issuance_id));
BEAST_EXPECT(id == to_string(mptAlice.issuanceID()));
BEAST_EXPECTS(
id == "00000001AE123A8556F3CF91154711376AFB0F894F832B3D", id);
id == "00000002AE123A8556F3CF91154711376AFB0F894F832B3D", id);
}
void
+10 -10
View File
@@ -8279,16 +8279,16 @@ private:
// validate the emitted txn ids
std::vector<std::string> const txIds = {
"9610F73CDD6590EB6B3C82E5EC55D4B4C80CD7128B98AA556F7EC9DD96AE7056",
"2F4582A29272390C0C25A80D4A3BCE5A14ACE6D86D8D0CB2C57719EB6FA881AE",
"89A301CFEF0DD781AB9032A6A2DCE0937BC0119D2CDD06033B8B2FD80968E519",
"DD8721B59024E168480B4DF8F8E93778601F0BD2E77FC991F3DA1182F5AD8B1E",
"5D735C2EE3CB8289F8E11621FDC9565F9D6D67F3AE59D65332EACE591D67945F",
"F02470E01731C968881AF4CBDEC90BB9E1F7AB0BE1CC22AF15451FB6D191096D",
"8AD65E541DECD49B1693F8C17DFD8A2B906F49C673C4FD2034FF772E2BE50C30",
"9F225229059CCC6257814D03C107884CF588C1C246A89ADFC16E50DF671B834C",
"13C2A54A14BADF3648CED05175E1CCAD713F7E5EA56D9735CF8813CD5551F281",
"87C60F41A96554587CED289F83F52DEE3CF670EEB189B067E6066B9A06056ADF",
"2FBEF981BAC322225D13C274D0118FECC1B93A89EDBC0AB3CBD8B30A1591024E",
"3C0A12F6A322486ABDA4180DC2B424BD252FD2FA6A33553B0B5D1526C020C475",
"AFFD55D757136FC0BE665F16E0D8CFBDC0D37CF85B5D3329BE1DBD3A44BB281C",
"BBE43C01068CE511DBF24F84A928E481451FF448AF2493E1A9BE099628D1F523",
"45970753F7B52448C23D66E5E19DBBC65F5BE6055C04AC3BA9896D7D67891DB1",
"97F4E09B2AEF4D798D378B1390B0624A68F304247B358334167E93217D502A16",
"770D3B786C8E3BEF560D9589B02557B0BDFC1C7FD15C6CF42E86068A6A411EFC",
"239DFA86EE8391418170D7FDF5AA8B8EEE0BBEF5E9F928BC29C23EEF6F1858AD",
"9FAA93AC6340C87B5C722FF0E8D4D3F39B9A0CA5E9799EC4CE316BFD5E379540",
"8672B2D8AA5E9EF86929B8662FE436541B2F739052732020B874BC304601C4DB",
};
Json::Value params;
params[jss::transaction] =
+537 -78
View File
@@ -1355,11 +1355,16 @@ public:
auto const baseFee = drops[jss::base_fee_no_hooks];
BEAST_EXPECT(baseFee == to_string(feeDrops));
auto const openLedgerFee = drops[jss::open_ledger_fee];
BEAST_EXPECT(openLedgerFee == expected);
BEAST_EXPECTS(
openLedgerFee == expected,
"openLedgerFee: " + openLedgerFee.asString() +
", expected: " + expected);
// verify hooks fee
auto const hooksFee = jrr[jss::result][jss::fee_hooks_feeunits];
BEAST_EXPECT(hooksFee == expected);
BEAST_EXPECTS(
hooksFee == expected,
"hooksFee: " + hooksFee.asString() + ", expected: " + expected);
}
void
@@ -1369,7 +1374,8 @@ public:
using namespace jtx;
Env env{*this, features};
bool const hookOnV2 = env.current()->rules().enabled(featureHookOnV2);
bool const hookOnV2 = env.current()->rules().enabled(featureHookOnV2) ||
env.current()->rules().enabled(featureHookOnV2_1);
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
@@ -1532,12 +1538,10 @@ public:
jv.removeMember(jss::HookOn);
jv[jss::HookOnIncoming] =
"0000000000000000000000000000000000000000000000000000000000"
"0000"
"00";
"000000";
jv[jss::HookOnOutgoing] =
"0000000000000000000000000000000000000000000000000000000000"
"0000"
"01";
"000001";
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Execution: Install"),
HSFEE);
@@ -1602,12 +1606,10 @@ public:
jv.removeMember(jss::HookOn);
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfff"
"ff"; // Invoke high
"bfffff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bfff"
"fe"; // Payment high
"bffffe"; // Payment high
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Execution: Install"),
HSFEE);
@@ -1618,8 +1620,7 @@ public:
jv[jss::Flags] = hsfOVERRIDE;
jv[jss::HookOn] =
"0000000000000000000000000000000000000000000000000000000000"
"0000"
"00";
"000000";
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Execution: Install"),
HSFEE);
@@ -1653,18 +1654,285 @@ public:
deleteHook(alice);
}
for (auto const& withFix : {false, true})
{
// test featureHookOnV2_1 preflight
auto f = features - featureHookOnV2 - featureHookOnV2_1;
if (withFix)
f = f | featureHookOnV2_1;
else
f = f | featureHookOnV2;
Env env{*this, f};
env.fund(XRP(10000), alice, bob);
env.close();
TER const expected =
env.current()->rules().enabled(featureHookOnV2_1)
? TER(temMALFORMED)
: TER(tesSUCCESS);
// 0. only one direction HookOn is not allowed
for (auto const& key : {jss::HookOnIncoming, jss::HookOnOutgoing})
{
auto noopJv = Json::Value{};
noopJv[key] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfffff";
env(ripple::test::jtx::hook(alice, {{noopJv}}, 0),
M("Lone direction HookOn NOOP"),
HSFEE,
ter(expected));
env.close();
if (!withFix)
BEAST_EXPECT(!env.le(keylet::hook(alice)));
}
// 1. Create Hook with HookOnIncoming/Outgoing to alice
auto jv = hso(accept_wasm);
jv.removeMember(jss::HookOn);
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfffff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bffffe"; // Payment high
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
env.close();
// 2. Install Hook with HookOn, Incoming/Outgoing to bob
jv = Json::Value{};
jv[jss::HookHash] = accept_hash_str;
jv[jss::Flags] = hsfOVERRIDE;
jv[jss::HookOn] =
"0000000000000000000000000000000000000000000000000000000000"
"000000";
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfffff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bffffe"; // Payment high
env(ripple::test::jtx::hook(bob, {{jv}}, 0), HSFEE, ter(expected));
env.close();
// 3. Update Hook with HookOn, Incoming/Outgoing to alice
jv = Json::Value{};
jv[jss::HookOn] =
"0000000000000000000000000000000000000000000000000000000000"
"000000";
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfffff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bffffe"; // Payment high
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
HSFEE,
ter(expected));
env.close();
}
for (auto const& withFix : {false, true})
{
// test featureHookOnV2_1 install/update
auto f = features - featureHookOnV2 - featureHookOnV2_1;
if (withFix)
f = f | featureHookOnV2_1;
else
f = f | featureHookOnV2;
{
// install:: HookDefinition: HookOn -> Hook:Incoming/Outgoing
// update:: Hook: HookOn -> Hook:Incoming/Outgoing
Env env{*this, f};
env.fund(XRP(10000), alice, bob);
env.close();
// Create Hook with HookOn to alice
auto jv = hso(accept_wasm);
jv.removeMember(jss::HookOn);
jv[jss::HookOn] =
"0000000000000000000000000000000000000000000000000000000000"
"000000";
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
env.close();
// Install Hook with Incoming/Outgoing to bob
jv = Json::Value{};
jv[jss::HookHash] = accept_hash_str;
jv[jss::Flags] = hsfOVERRIDE;
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfffff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bffffe"; // Payment high
env(ripple::test::jtx::hook(bob, {{jv}}, 0), HSFEE);
env.close();
{
auto const hooksObj = env.le(keylet::hook(bob));
BEAST_EXPECT(hooksObj && hooksObj->isFieldPresent(sfHooks));
auto const& hooks = hooksObj->getFieldArray(sfHooks);
BEAST_EXPECT(hooks.size() == 1);
auto const& h = hooks[0];
BEAST_EXPECT(!h.isFieldPresent(sfHookOn));
BEAST_EXPECT(h.isFieldPresent(sfHookOnOutgoing));
BEAST_EXPECT(h.isFieldPresent(sfHookOnIncoming));
}
// Update Hook with HookOn, Incoming/Outgoing to alice
jv = Json::Value{};
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfffff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bffffe"; // Payment high
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
env.close();
{
auto const hooksObj = env.le(keylet::hook(alice));
BEAST_EXPECT(hooksObj && hooksObj->isFieldPresent(sfHooks));
auto const& hooks = hooksObj->getFieldArray(sfHooks);
BEAST_EXPECT(hooks.size() == 1);
auto const& h = hooks[0];
BEAST_EXPECT(!h.isFieldPresent(sfHookOn));
BEAST_EXPECT(h.isFieldPresent(sfHookOnOutgoing));
BEAST_EXPECT(h.isFieldPresent(sfHookOnIncoming));
}
// Re-update Hook with HookOn, Incoming/Outgoing to alice
jv = Json::Value{};
jv[jss::HookOn] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfffff";
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
env.close();
{
auto const hooksObj = env.le(keylet::hook(alice));
BEAST_EXPECT(hooksObj && hooksObj->isFieldPresent(sfHooks));
auto const& hooks = hooksObj->getFieldArray(sfHooks);
BEAST_EXPECT(hooks.size() == 1);
auto const& h = hooks[0];
BEAST_EXPECT(h.isFieldPresent(sfHookOn));
if (withFix)
{
BEAST_EXPECT(!h.isFieldPresent(sfHookOnOutgoing));
BEAST_EXPECT(!h.isFieldPresent(sfHookOnIncoming));
}
else
{
BEAST_EXPECT(h.isFieldPresent(sfHookOnOutgoing));
BEAST_EXPECT(h.isFieldPresent(sfHookOnIncoming));
}
}
}
{
// install:: HookDefinition: Incoming/Outgoing -> Hook:HookOn
// update:: Hook: Incoming/Outgoing -> Hook:HookOn
Env env{*this, f};
env.fund(XRP(10000), alice, bob);
env.close();
// Create Hook with HookOn to alice
auto jv = hso(accept_wasm);
jv.removeMember(jss::HookOn);
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfffff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bffffe"; // Payment high
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
env.close();
// Install Hook with Incoming/Outgoing to bob
jv = Json::Value{};
jv[jss::HookHash] = accept_hash_str;
jv[jss::Flags] = hsfOVERRIDE;
jv[jss::HookOn] =
"0000000000000000000000000000000000000000000000000000000000"
"000000";
env(ripple::test::jtx::hook(bob, {{jv}}, 0), HSFEE);
env.close();
{
auto const hooksObj = env.le(keylet::hook(bob));
BEAST_EXPECT(hooksObj && hooksObj->isFieldPresent(sfHooks));
auto const& hooks = hooksObj->getFieldArray(sfHooks);
BEAST_EXPECT(hooks.size() == 1);
auto const& h = hooks[0];
BEAST_EXPECT(h.isFieldPresent(sfHookOn));
BEAST_EXPECT(!h.isFieldPresent(sfHookOnOutgoing));
BEAST_EXPECT(!h.isFieldPresent(sfHookOnIncoming));
}
// Update Hook with HookOn, Incoming/Outgoing to alice
jv = Json::Value{};
jv[jss::HookOn] =
"0000000000000000000000000000000000000000000000000000000000"
"000000";
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
env.close();
{
auto const hooksObj = env.le(keylet::hook(alice));
BEAST_EXPECT(hooksObj && hooksObj->isFieldPresent(sfHooks));
auto const& hooks = hooksObj->getFieldArray(sfHooks);
BEAST_EXPECT(hooks.size() == 1);
auto const& h = hooks[0];
if (withFix)
BEAST_EXPECT(h.isFieldPresent(sfHookOn));
// Cause different results between gcc and clang due to
// undefined behavior so we don't test it
// else
// BEAST_EXPECT(!h.isFieldPresent(sfHookOn));
BEAST_EXPECT(!h.isFieldPresent(sfHookOnOutgoing));
BEAST_EXPECT(!h.isFieldPresent(sfHookOnIncoming));
}
// Re-update Hook with HookOn, Incoming/Outgoing to alice
jv = Json::Value{};
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bffff0"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bffff1"; // Payment high
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
env.close();
{
auto const hooksObj = env.le(keylet::hook(alice));
BEAST_EXPECT(hooksObj && hooksObj->isFieldPresent(sfHooks));
auto const& hooks = hooksObj->getFieldArray(sfHooks);
BEAST_EXPECT(hooks.size() == 1);
auto const& h = hooks[0];
if (withFix)
BEAST_EXPECT(!h.isFieldPresent(sfHookOn));
// Cause different results between gcc and clang due to
// undefined behavior so we don't test it
// else
// BEAST_EXPECT(h.isFieldPresent(sfHookOn));
BEAST_EXPECT(h.isFieldPresent(sfHookOnOutgoing));
BEAST_EXPECT(h.isFieldPresent(sfHookOnIncoming));
}
}
}
// Fee RPC
{
auto jv = hso(accept_wasm);
jv.removeMember(jss::HookOn);
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfff"
"ff"; // Invoke high
"bfffff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bfff"
"fe"; // Payment high
"bffffe"; // Payment high
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
env.close();
@@ -2000,11 +2268,64 @@ 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 - fix20261001;
if (fix)
f = f | fix20261001;
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 a named hook with a HookName containing a noncharacter
// (U+FFFF followed by 'A'). Well-formed UTF-8, but always rejected.
{
Env env{*this, features};
env.fund(XRP(10000), alice);
auto jv = invoke::invoke(alice);
jv[jss::HookName] = "EFBFBF41";
env(jv,
M("Call named hook with a noncharacter in the hook name"),
HSFEE,
ter(temMALFORMED));
env.close();
}
// Call a named hook with a HookName ending in a truncated sequence.
{
Env env{*this, features};
env.fund(XRP(10000), alice);
auto jv = invoke::invoke(alice);
jv[jss::HookName] = "414243E0";
env(jv,
M("Call named hook with a truncated hook name"),
HSFEE,
ter(temMALFORMED));
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));
@@ -3003,25 +3324,37 @@ public:
testInferHookSetOperation()
{
testcase("Test operation inference");
using namespace jtx;
auto const alice = Account{"alice"};
Env env{*this};
env.fund(XRP(10000), alice);
env(noop(alice));
SetHookCtx shCtx{
.j = env.app().journal("SetHook"),
.tx = *(env.tx()),
.app = env.app(),
.rules = env.current()->rules()};
// hsoNOOP
{
STObject hso{sfHook};
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoNOOP);
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoNOOP);
}
// hsoCREATE
{
STObject hso{sfHook};
hso.setFieldVL(sfCreateCode, {1}); // non-empty create code
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoCREATE);
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoCREATE);
}
// hsoDELETE
{
STObject hso{sfHook};
hso.setFieldVL(sfCreateCode, ripple::Blob{}); // empty create code
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoDELETE);
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoDELETE);
}
// hsoINSTALL
@@ -3029,7 +3362,7 @@ public:
STObject hso{sfHook};
hso.setFieldH256(
sfHookHash, uint256{beast::zero}); // all zeros hook hash
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoINSTALL);
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoINSTALL);
}
// hsoNSDELETE
@@ -3038,14 +3371,14 @@ public:
hso.setFieldH256(
sfHookNamespace, uint256{beast::zero}); // all zeros hook hash
hso.setFieldU32(sfFlags, hsfNSDELETE);
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoNSDELETE);
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoNSDELETE);
}
// hsoUPDATE
{
STObject hso{sfHook};
hso.setFieldH256(sfHookOn, UINT256_BIT[0]);
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoUPDATE);
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoUPDATE);
}
// hsoINVALID
@@ -3054,7 +3387,39 @@ public:
hso.setFieldVL(sfCreateCode, {1}); // non-empty create code
hso.setFieldH256(
sfHookHash, uint256{beast::zero}); // all zeros hook hash
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoINVALID);
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoINVALID);
for (auto fix : {true, false})
{
auto feature = supported_amendments() - featureHookOnV2 -
featureHookOnV2_1;
if (fix)
feature = feature | featureHookOnV2_1;
else
feature = feature | featureHookOnV2;
Env env{*this, feature};
SetHookCtx shCtx{
.j = env.app().journal("SetHook"),
.tx = *env.tx(),
.app = env.app(),
.rules = env.current()->rules()};
STObject hso2{sfHook};
hso2.setFieldH256(
sfHookOnOutgoing,
uint256{beast::zero}); // all zeros hook on outgoing
BEAST_EXPECT(
SetHook::inferOperation(shCtx, hso2) ==
(fix ? hsoINVALID : hsoNOOP));
STObject hso3{sfHook};
hso3.setFieldH256(
sfHookOnIncoming,
uint256{beast::zero}); // all zeros hook on incoming
BEAST_EXPECT(
SetHook::inferOperation(shCtx, hso3) ==
(fix ? hsoINVALID : hsoNOOP));
}
}
}
@@ -3126,6 +3491,79 @@ 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 - fix20261001};
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)
{
@@ -4277,10 +4715,10 @@ public:
uint8_t expected1[49] = {
0xEDU, 0x20U, 0x2EU, 0x00U, 0x00U, 0x00U, 0x01U, 0x3DU, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU, 0xB8U, 0x05U, 0xD6U,
0xC3U, 0x52U, 0xDFU, 0x7AU, 0x27U, 0x76U, 0x6DU, 0xC0U, 0x20U, 0x47U,
0xB7U, 0x64U, 0x22U, 0x5AU, 0xB7U, 0x5DU, 0xF3U, 0xFAU, 0x0DU, 0xE3U,
0xBDU, 0xC6U, 0x40U, 0xBAU, 0xD0U, 0x0AU, 0x66U, 0xEBU, 0x68U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU, 0xBCU, 0x09U, 0x95U,
0xFDU, 0x03U, 0x0FU, 0xF1U, 0x85U, 0xF4U, 0xECU, 0xACU, 0x94U, 0xDBU,
0x02U, 0x19U, 0x26U, 0xFEU, 0x58U, 0xD4U, 0x8DU, 0x48U, 0x21U, 0xCCU,
0x82U, 0x2EU, 0x4AU, 0x6AU, 0xC9U, 0xC8U, 0xC6U, 0x7CU, 0x88U
};
// 0x5CU
// EmitNonce 32bytes
@@ -12055,57 +12493,76 @@ 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 - fix20261001;
if (with20251128)
feats = feats | fixHookAPI20251128;
if (withSType)
feats = feats | fix20261001;
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));
}
}
}
}
@@ -15075,7 +15532,8 @@ public:
testNSDeletePartial(features);
testPageCap(features);
testHookOnV2(features);
testHookOnV2((features | featureHookOnV2) - featureHookOnV2_1);
testHookOnV2((features | featureHookOnV2_1) - featureHookOnV2);
testHookName(features);
testFillCopy(features);
@@ -15083,6 +15541,7 @@ public:
testWasm(features);
test_accept(features);
test_rollback(features);
test_exit_out_of_bounds(features);
testGuards(features);
+66 -8
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)))
@@ -1613,10 +1671,10 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
uint8_t expected1[49] = {
0xEDU, 0x20U, 0x2EU, 0x00U, 0x00U, 0x00U, 0x01U, 0x3DU, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU, 0xB8U, 0x05U, 0xD6U,
0xC3U, 0x52U, 0xDFU, 0x7AU, 0x27U, 0x76U, 0x6DU, 0xC0U, 0x20U, 0x47U,
0xB7U, 0x64U, 0x22U, 0x5AU, 0xB7U, 0x5DU, 0xF3U, 0xFAU, 0x0DU, 0xE3U,
0xBDU, 0xC6U, 0x40U, 0xBAU, 0xD0U, 0x0AU, 0x66U, 0xEBU, 0x68U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU, 0xBCU, 0x09U, 0x95U,
0xFDU, 0x03U, 0x0FU, 0xF1U, 0x85U, 0xF4U, 0xECU, 0xACU, 0x94U, 0xDBU,
0x02U, 0x19U, 0x26U, 0xFEU, 0x58U, 0xD4U, 0x8DU, 0x48U, 0x21U, 0xCCU,
0x82U, 0x2EU, 0x4AU, 0x6AU, 0xC9U, 0xC8U, 0xC6U, 0x7CU, 0x88U
};
// 0x5CU
// EmitNonce 32bytes
@@ -1763,10 +1821,10 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x36U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0xEDU, 0x20U, 0x2EU, 0x00U, 0x00U, 0x00U, 0x01U,
0x3DU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU,
0xB8U, 0x05U, 0xD6U, 0xC3U, 0x52U, 0xDFU, 0x7AU, 0x27U, 0x76U, 0x6DU,
0xC0U, 0x20U, 0x47U, 0xB7U, 0x64U, 0x22U, 0x5AU, 0xB7U, 0x5DU, 0xF3U,
0xFAU, 0x0DU, 0xE3U, 0xBDU, 0xC6U, 0x40U, 0xBAU, 0xD0U, 0x0AU, 0x66U,
0xEBU, 0x68U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x68U, 0x61U, 0x73U,
0xBCU, 0x09U, 0x95U, 0xFDU, 0x03U, 0x0FU, 0xF1U, 0x85U, 0xF4U, 0xECU,
0xACU, 0x94U, 0xDBU, 0x02U, 0x19U, 0x26U, 0xFEU, 0x58U, 0xD4U, 0x8DU,
0x48U, 0x21U, 0xCCU, 0x82U, 0x2EU, 0x4AU, 0x6AU, 0xC9U, 0xC8U, 0xC6U,
0x7CU, 0x88U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x68U, 0x61U, 0x73U,
0x68U, 0x28U, 0x28U, 0x75U, 0x69U, 0x6EU, 0x74U, 0x33U, 0x32U, 0x5FU,
0x74U, 0x29U, 0x65U, 0x78U, 0x70U, 0x65U, 0x63U, 0x74U, 0x65U, 0x64U,
0x5FU, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x68U, 0x61U, 0x73U, 0x68U,
+128 -118
View File
@@ -980,6 +980,7 @@ struct URIToken_test : public beast::unit_test::suite
// setup env
env.fund(XRP(1000), alice, bob, gw);
env.close();
env.trust(USD(100000), alice, bob);
env.close();
env(pay(gw, alice, USD(1000)));
@@ -2310,12 +2311,16 @@ struct URIToken_test : public beast::unit_test::suite
auto const bob = Account("bob");
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
std::string uri = "";
// test utf-8 success
// =========================================================================
// Cases that should succeed:
// valid well-formed UTF-8 that is not a noncharacter
// =========================================================================
{
// case: kosme
uri = "κόσμε";
@@ -2348,133 +2353,83 @@ struct URIToken_test : public beast::unit_test::suite
// case: ipfs metadata url
uri = "https://example.com/ipfs/";
env(uritoken::mint(alice, uri));
}
// =========================================================================
// Cases that should fail:
// genuinely malformed UTF-8 (well-formedness failures)
// =========================================================================
{
// BOUNDRY - START
// ----------------------------------------------------------------
// case: 1 byte (U-00000000)
uri = "\x00";
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
// case: 2 bytes (U-00000080)
uri = "\xC2\x80";
env(uritoken::mint(alice, uri));
// case: 3 bytes (U-00000800)
uri = "\xE0\xA0\x80";
env(uritoken::mint(alice, uri));
// case: 4 bytes (U-00010000)
uri = "\xF0\x90\x80\x80";
env(uritoken::mint(alice, uri));
// case: 5 bytes (U-00200000)
// case: 5 bytes (U-00200000) - beyond valid UTF-8 range
uri = "\xF8\x88\x80\x80\x80";
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
// case: 6 bytes (U-04000000)
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// case: 6 bytes (U-04000000) - beyond valid UTF-8 range
uri = "\xFC\x84\x80\x80\x80\x80";
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// BOUNDRY - END
// ----------------------------------------------------------------
// case: 1 byte (U-0000007F)
uri = "\x7F";
env(uritoken::mint(alice, uri));
// case: 2 bytes (U-000007FF)
uri = "\xDF\xBF";
env(uritoken::mint(alice, uri));
// case: 3 bytes (U-0000FFFF)
uri = "\xEF\xBF\xBF";
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
// case: 4 bytes (U-001FFFFF)
uri = "\xF7\xBF\xBF\xBF";
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
// case: 5 bytes (U-03FFFFFF)
uri = "\xFB\xBF\xBF\xBF\xBF";
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
// case: 6 bytes (U-7FFFFFFF)
uri = "\xFD\xBF\xBF\xBF\xBF\xBF";
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
// case: 3 bytes max (U-0000FFFF) - but this is U+FFFF
// noncharacter, rejected, see below
// // BOUNDRY - OTHER
// case: 4 bytes max (U-001FFFFF) - beyond valid BMP
uri = "\xF7\xBF\xBF\xBF";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// case: 5 bytes max (U-03FFFFFF) - beyond valid UTF-8
uri = "\xFB\xBF\xBF\xBF\xBF";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// case: 6 bytes max (U-7FFFFFFF) - beyond valid UTF-8
uri = "\xFD\xBF\xBF\xBF\xBF\xBF";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// BOUNDRY - OTHER
// ----------------------------------------------------------------
// case: 1 bytes (U-0000D7FF)
// case: 1 bytes (U-0000D7FF) - incomplete 2-byte sequence
uri = "\xD7\xFF";
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
// case: 2 bytes (U-0000E000)
uri = "\xEE\x80\x80";
env(uritoken::mint(alice, uri));
// case: 3 bytes (U-0000FFFD)
uri = "\xEF\xBF\xBD";
env(uritoken::mint(alice, uri));
// // case: 4 bytes (U-0010FFFF)
uri = "\xF4\x8F\xBF\xBF";
env(uritoken::mint(alice, uri));
// // case: 4 bytes (U-00110000)
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// case: 4 bytes (U-00110000) - beyond valid UTF-8 range
uri = "\xF4\x90\x80\x80";
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
env(uritoken::mint(alice, uri), ter(temMALFORMED));
}
// test utf8 malformed
// =========================================================================
// Genuinely malformed UTF-8 sequences - always fail
// =========================================================================
{
// MALFORMED - END
// MALFORMED - START
// ----------------------------------------------------------------
// First continuation byte 0x80:
uri = "\x80";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// Last continuation byte 0xbf
uri = "\xBF";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// 2 continuation bytes
uri = "��";
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
// 3 continuation bytes
uri = "���";
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
// 4 continuation bytes
uri = "����";
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
// 5 continuation bytes
uri = "�����";
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
// 6 continuation bytes
uri = "������";
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
// 7 continuation bytes
uri = "�������";
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
// Sequence of all 64 possible continuation bytes (0x80-0xbf)
uri =
"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E"
"\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D"
"\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC"
"\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB"
"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D"
"\x8E"
"\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C"
"\x9D"
"\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB"
"\xAC"
"\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA"
"\xBB"
"\xBC\xBD\xBE\xBF";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// TODO: REVIEW - THIS IS NOT THE CORRECT 32 byte sequence.
// All 32 first bytes of 2-byte sequences (0xc0-0xdf), each followed
// by a space character
// uri = "\xE0\x80\x80 \xE0\x80\x81 \xE0\x80\x82 \xE0\x80\x83
// \xE0\x80\x84 \xE0\x80\x85 \xE0\x80\x86 \xE0\x80\x87 \xE0\x80\x88
// \xE0\x80\x89 \xE0\x80\x8A \xE0\x80\x8B \xE0\x80\x8C \xE0\x80\x8D
// \xE0\x80\x8E \xE0\x80\x8F \xE0\x80\x90";
// env(uritoken::mint(alice, uri), ter(temMALFORMED));
// All 16 first bytes of 3-byte sequences (0xe0-0xef), each followed
// by a space character
// All 16 first bytes of 3-byte sequences (0xe0-0xef), each
// followed by a space character
uri =
"\xE0\x80\x80 \xE0\x80\x81 \xE0\x80\x82 \xE0\x80\x83 "
"\xE0\x80\x84 \xE0\x80\x85 \xE0\x80\x86 \xE0\x80\x87 "
@@ -2483,30 +2438,25 @@ struct URIToken_test : public beast::unit_test::suite
"\xE0\x80\x90";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// All 8 first bytes of 4-byte sequences (0xf0-0xf7), each followed
// by a space character
// All 8 first bytes of 4-byte sequences (0xf0-0xf7), each
// followed by a space character
uri =
"\xF0\x90\x80\x80 \xF0\x90\x80\x81 \xF0\x90\x80\x82 "
"\xF0\x90\x80\x83 \xF0\x90\x80\x84 \xF0\x90\x80\x85 "
"\xF0\x90\x80\x86 \xF0\x90\x80\x87";
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
env(uritoken::mint(alice, uri));
// All 4 first bytes of 5-byte sequences (0xf8-0xfb), each followed
// by a space character
// All 4 first bytes of 5-byte sequences (0xf8-0xfb), each
// followed by a space character
uri =
"\xF8\x88\x80\x80\x80 \xF8\x88\x80\x80\x81 "
"\xF8\x88\x80\x80\x82 \xF8\x88\x80\x80\x83";
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD FAIL
// All 2 first bytes of 6-byte sequences (0xfc-0xfd), each followed
// by a space character
uri = "\xFC\x84\x80\x80\x80\x80 \xFC\x84\x80\x80\x80\x81";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// Sequences with last continuation byte missing
// Concatenation of incomplete sequences
// All 2 first bytes of 6-byte sequences (0xfc-0xfd), each
// followed by a space character
uri = "\xFC\x84\x80\x80\x80\x80 \xFC\x84\x80\x80\x80\x81";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// Impossible bytes
uri = "\xFE";
@@ -2616,15 +2566,75 @@ struct URIToken_test : public beast::unit_test::suite
uri = "\xED\xAF\xBF\xED\xBF\xBF";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// problematic noncharacters in 16-bit applications
// case: (U+FFFE)
uri = "\xEF\xBF\xBE";
// MALFORMED - END
// ----------------------------------------------------------------
}
// =========================================================================
// Noncharacters U+FFFE and U+FFFF: well-formed UTF-8, but
// deliberately rejected (U+FFFE is a byte-swapped BOM).
// =========================================================================
{
uri = "\xEF\xBF\xBE"; // U+FFFE
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// case: (U+FFFF)
uri = "\xEF\xBF\xBF";
uri = "\xEF\xBF\xBF"; // U+FFFF
env(uritoken::mint(alice, uri), ter(temMALFORMED));
uri = "ipfs://x\xEF\xBF\xBE"; // not only at the start
env(uritoken::mint(alice, uri), ter(temMALFORMED));
}
// =========================================================================
// Valid boundary cases that should always succeed
// =========================================================================
{
// BOUNDRY - START (valid minimums)
// ----------------------------------------------------------------
// case: 1 byte (U-00000000). Must use the string
// constructor, not a C-string literal, to avoid strlen
// truncation to an empty URI.
uri = std::string(1, '\0');
env(uritoken::mint(alice, uri));
// case: 2 bytes (U-00000080)
uri = "\xC2\x80";
env(uritoken::mint(alice, uri));
// case: 3 bytes (U-00000800)
uri = "\xE0\xA0\x80";
env(uritoken::mint(alice, uri));
// case: 4 bytes (U-00010000)
uri = "\xF0\x90\x80\x80";
env(uritoken::mint(alice, uri));
// BOUNDRY - END (valid maximums)
// ----------------------------------------------------------------
// case: 1 byte (U-0000007F)
uri = "\x7F";
env(uritoken::mint(alice, uri));
// case: 2 bytes (U-000007FF)
uri = "\xDF\xBF";
env(uritoken::mint(alice, uri));
// case: 3 bytes (U-0000FFFF) - noncharacter, rejected, see
// above.
// case: 4 bytes (U-001FFFFF) - beyond U+10FFFF, always
// rejected, see above.
// BOUNDRY - OTHER
// ----------------------------------------------------------------
// case: 2 bytes (U-0000E000) - above surrogates
uri = "\xEE\x80\x80";
env(uritoken::mint(alice, uri));
// case: 3 bytes (U-0000FFFD) - replacement character, valid
uri = "\xEF\xBF\xBD";
env(uritoken::mint(alice, uri));
// case: 4 bytes (U-0010FFFF) - max valid code point
uri = "\xF4\x8F\xBF\xBF";
env(uritoken::mint(alice, uri));
}
}
void
testWithFeats(FeatureBitset features)
{
+69
View File
@@ -0,0 +1,69 @@
//------------------------------------------------------------------------------
/*
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 <xrpl/basics/Slice.h>
#include <xrpl/basics/UTF8.h>
#include <xrpl/beast/unit_test.h>
namespace ripple {
struct UTF8_test : public beast::unit_test::suite
{
void
run() override
{
testcase("isValidUTF8");
auto const check = [](std::string const& s) {
return isValidUTF8(makeSlice(s));
};
// Sequences truncated by the end of the buffer are invalid. The
// previous decoder read past the end of the vector here.
BEAST_EXPECT(!check("a\xC2"));
BEAST_EXPECT(!check("a\xE0\xA4"));
BEAST_EXPECT(!check("a\xEF\xBF"));
BEAST_EXPECT(!check("a\xF0\x90\x8D"));
BEAST_EXPECT(!check(std::string("\xC2", 1)));
BEAST_EXPECT(!check(std::string("\xF4\x8F\xBF", 3)));
// The complete sequences are fine.
BEAST_EXPECT(check("a\xC2\xA2"));
BEAST_EXPECT(check("a\xE0\xA4\xB9"));
BEAST_EXPECT(check("a\xF0\x90\x8D\x88"));
BEAST_EXPECT(check("a\xF4\x8F\xBF\xBF"));
BEAST_EXPECT(check(""));
// U+FFFE and U+FFFF are rejected wherever they appear.
BEAST_EXPECT(!check("\xEF\xBF\xBE"));
BEAST_EXPECT(!check("ab\xEF\xBF\xBF"));
BEAST_EXPECT(!check("\xE2\x82\xAC\xEF\xBF\xBEz"));
// Their neighbours are not, and nor are the other noncharacters,
// which the validator has never rejected.
BEAST_EXPECT(check("\xEF\xBF\xBD")); // U+FFFD
BEAST_EXPECT(check("\xEF\xBE\xBF")); // U+FFBF
BEAST_EXPECT(check("\xEF\xBB\xBF")); // U+FEFF (BOM)
BEAST_EXPECT(check("\xF0\x9F\xBF\xBE")); // U+1FFFE
}
};
BEAST_DEFINE_TESTSUITE(UTF8, basics, ripple);
} // namespace ripple
+7
View File
@@ -56,6 +56,13 @@ public:
/// Get RPC 1.0 or RPC 2.0
virtual unsigned
version() const = 0;
/// Times invoke() replaced a connection the server had closed.
virtual unsigned
reconnects() const
{
return 0;
}
};
} // namespace test
+112
View File
@@ -18,6 +18,7 @@
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/JSONRPCClient.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpl/beast/hash/uhash.h>
@@ -27,8 +28,16 @@
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/jss.h>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast/core/flat_buffer.hpp>
#include <boost/beast/http/read.hpp>
#include <boost/beast/http/string_body.hpp>
#include <boost/beast/http/write.hpp>
#include <boost/lexical_cast.hpp>
#include <chrono>
#include <optional>
#include <thread>
#include <utility>
namespace ripple {
@@ -906,6 +915,106 @@ 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", {});
auto const reconnects = env.client().reconnects();
// 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");
// The server kept the connection; the client did not replace it.
BEAST_EXPECT(env.client().reconnects() == reconnects);
}
void
testJSONRPCClientSlowFirstRequest()
{
testcase("JSON-RPC first request survives slow test setup");
using namespace std::chrono_literals;
using namespace jtx;
Env env{*this};
// The client connects when the Env is built, and the server closes a
// loopback connection whose first request takes longer than
// BaseHTTPPeer::timeoutSecondsLocal (3 s) to arrive. Test setup can
// take that long on a loaded machine. Whether or not the server's
// timer has fired by now, the request must succeed;
// testJSONRPCClientReconnects covers the replacement itself.
std::this_thread::sleep_for(4s);
auto const response = env.client().invoke("server_info", {});
BEAST_EXPECT(response[jss::result][jss::status] == "success");
}
void
testJSONRPCClientReconnects()
{
testcase("JSON-RPC client replaces a connection the server closed");
using namespace boost::asio;
namespace http = boost::beast::http;
// A server that closes the client's first connection without
// answering, as BaseHTTPPeer does when the first request is late, and
// answers on the second.
io_context ioc;
ip::tcp::acceptor acceptor(ioc, {ip::make_address("127.0.0.1"), 0});
ip::tcp::socket first(ioc);
ip::tcp::socket second(ioc);
boost::beast::flat_buffer buffer;
http::request<http::string_body> req;
http::response<http::string_body> res{http::status::ok, 11};
res.body() = R"({"result":{"status":"success"}})";
res.prepare_payload();
acceptor.async_accept(first, [&](boost::system::error_code) {
boost::system::error_code ignored;
first.close(ignored);
acceptor.async_accept(second, [&](boost::system::error_code acc) {
if (acc)
return;
http::async_read(
second,
buffer,
req,
[&](boost::system::error_code rd, std::size_t) {
if (!rd)
http::async_write(
second,
res,
[](boost::system::error_code, std::size_t) {});
});
});
});
std::thread server([&] { ioc.run(); });
auto cfg = jtx::envconfig();
(*cfg)["port_rpc"].set("ip", "127.0.0.1");
(*cfg)["port_rpc"].set(
"port", std::to_string(acceptor.local_endpoint().port()));
auto client = makeJSONRPCClient(*cfg);
try
{
auto const response = client->invoke("server_info", {});
BEAST_EXPECT(response[jss::result][jss::status] == "success");
BEAST_EXPECT(client->reconnects() == 1);
}
catch (std::exception const& e)
{
fail(e.what(), __FILE__, __LINE__);
}
ioc.stop();
server.join();
}
void
run() override
{
@@ -933,6 +1042,9 @@ public:
testSignAndSubmit(all);
testFeatures(all);
testExceptionalShutdown();
testJSONRPCClientKeepAlive();
testJSONRPCClientSlowFirstRequest();
testJSONRPCClientReconnects();
}
};
+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
+53 -2
View File
@@ -23,6 +23,7 @@
#include <xrpl/server/Port.h>
#include <boost/asio.hpp>
#include <boost/beast/http/dynamic_body.hpp>
#include <boost/beast/http/error.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/read.hpp>
#include <boost/beast/http/string_body.hpp>
@@ -79,6 +80,30 @@ class JSONRPCClient : public AbstractClient
boost::beast::multi_buffer bin_;
boost::beast::multi_buffer bout_;
unsigned rpc_version_;
unsigned reconnects_ = 0;
// Errors meaning the server closed the keep-alive connection, rather
// than a failed request, so the request can be sent again on a fresh one.
static bool
droppedConnection(boost::system::error_code const& ec)
{
namespace error = boost::asio::error;
return ec == boost::beast::http::error::end_of_stream ||
ec == error::eof || ec == error::connection_reset ||
ec == error::connection_aborted || ec == error::broken_pipe ||
ec == error::not_connected;
}
// Replace the dropped connection, discarding any bytes buffered from it.
void
reconnect()
{
boost::system::error_code ec;
stream_.close(ec);
bin_.clear();
stream_.connect(ep_);
++reconnects_;
}
public:
explicit JSONRPCClient(Config const& cfg, unsigned rpc_version)
@@ -133,10 +158,30 @@ public:
req.body() = to_string(jr);
}
req.prepare_payload();
write(stream_, req);
// The connection is opened at construction and kept alive, but the
// server closes it when the first request takes longer than
// BaseHTTPPeer::timeoutSecondsLocal to arrive (test setup on a loaded
// machine can), or when it idles past timeoutSeconds between requests.
// If the server has closed it, reconnect and send the request once
// more.
response<dynamic_body> res;
read(stream_, bin_, res);
auto writeAndRead = [&] {
write(stream_, req);
read(stream_, bin_, res);
};
try
{
writeAndRead();
}
catch (boost::system::system_error const& e)
{
if (!droppedConnection(e.code()))
throw;
reconnect();
res = {};
writeAndRead();
}
Json::Reader jr;
Json::Value jv;
@@ -153,6 +198,12 @@ public:
{
return rpc_version_;
}
unsigned
reconnects() const override
{
return reconnects_;
}
};
std::unique_ptr<AbstractClient>
+40 -32
View File
@@ -37,20 +37,36 @@ namespace test {
static char const* bobs_account_objects[] = {
R"json({
"Account" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
"BookDirectory" : "B025997A323F5C3E03DDF1334471F5984ABDE31C59D463525D038D7EA4C68000",
"BookNode" : "0",
"Flags" : 65536,
"LedgerEntryType" : "Offer",
"OwnerNode" : "0",
"Sequence" : 4,
"TakerGets" : {
"currency" : "USD",
"issuer" : "r32rQHyesiTtdWFU7UJVtff4nCR5SHCbJW",
"value" : "1"
},
"TakerPays" : "100000000",
"index" : "A984D036A0E562433A8377CA57D1A1E056E58C0D04818F8DFD3A1AA3F217DD82"
"Account" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
"BookDirectory" : "50AD0A9E54D2B381288D535EB724E4275FFBF41580D28A925D038D7EA4C68000",
"BookNode" : "0",
"Flags" : 65536,
"LedgerEntryType" : "Offer",
"OwnerNode" : "0",
"Sequence" : 4,
"TakerGets" : {
"currency" : "USD",
"issuer" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
"value" : "1"
},
"TakerPays" : "100000000",
"index" : "A984D036A0E562433A8377CA57D1A1E056E58C0D04818F8DFD3A1AA3F217DD82"
})json",
R"json({
"Account" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
"BookDirectory" : "B025997A323F5C3E03DDF1334471F5984ABDE31C59D463525D038D7EA4C68000",
"BookNode" : "0",
"Flags" : 65536,
"LedgerEntryType" : "Offer",
"OwnerNode" : "0",
"Sequence" : 5,
"TakerGets" : {
"currency" : "USD",
"issuer" : "r32rQHyesiTtdWFU7UJVtff4nCR5SHCbJW",
"value" : "1"
},
"TakerPays" : "100000000",
"index" : "CAFE32332D752387B01083B60CC63069BA4A969C9730836929F841450F6A718E"
})json",
R"json({
"Balance" : {
@@ -95,22 +111,6 @@ static char const* bobs_account_objects[] = {
},
"LowNode" : "0",
"index" : "D89BC239086183EB9458C396E643795C1134963E6550E682A190A5F021766D43"
})json",
R"json({
"Account" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
"BookDirectory" : "50AD0A9E54D2B381288D535EB724E4275FFBF41580D28A925D038D7EA4C68000",
"BookNode" : "0",
"Flags" : 65536,
"LedgerEntryType" : "Offer",
"OwnerNode" : "0",
"Sequence" : 3,
"TakerGets" : {
"currency" : "USD",
"issuer" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
"value" : "1"
},
"TakerPays" : "100000000",
"index" : "E11029302EE744401427793A4F37BCB18F698D55C96851BEC5ABBD6242CF03D7"
})json"};
class AccountObjects_test : public beast::unit_test::suite
@@ -332,7 +332,7 @@ public:
auto& aobj = resp[jss::result][jss::account_objects][i];
aobj.removeMember("PreviousTxnID");
aobj.removeMember("PreviousTxnLgrSeq");
BEAST_EXPECT(aobj == bobj[i + 1]);
BEAST_EXPECT(aobj == bobj[i + 2]);
}
}
// test stepped one-at-a-time with limit=1, resume from prev marker
@@ -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());
+6 -6
View File
@@ -117,9 +117,9 @@ public:
BEAST_EXPECT(jroOuter[0u][jss::quality] == "100000000");
BEAST_EXPECT(jroOuter[0u][jss::taker_gets][jss::currency] == "USD");
BEAST_EXPECT(
jroOuter[0u][jss::taker_gets][jss::issuer] == bob.human());
BEAST_EXPECT(jroOuter[0u][jss::taker_gets][jss::value] == "1");
BEAST_EXPECT(jroOuter[0u][jss::taker_pays] == "100000000");
jroOuter[0u][jss::taker_gets][jss::issuer] == gw.human());
BEAST_EXPECT(jroOuter[0u][jss::taker_gets][jss::value] == "2");
BEAST_EXPECT(jroOuter[0u][jss::taker_pays] == "200000000");
BEAST_EXPECT(jroOuter[1u][jss::quality] == "5000000");
BEAST_EXPECT(jroOuter[1u][jss::taker_gets][jss::currency] == "USD");
@@ -131,9 +131,9 @@ public:
BEAST_EXPECT(jroOuter[2u][jss::quality] == "100000000");
BEAST_EXPECT(jroOuter[2u][jss::taker_gets][jss::currency] == "USD");
BEAST_EXPECT(
jroOuter[2u][jss::taker_gets][jss::issuer] == gw.human());
BEAST_EXPECT(jroOuter[2u][jss::taker_gets][jss::value] == "2");
BEAST_EXPECT(jroOuter[2u][jss::taker_pays] == "200000000");
jroOuter[2u][jss::taker_gets][jss::issuer] == bob.human());
BEAST_EXPECT(jroOuter[2u][jss::taker_gets][jss::value] == "1");
BEAST_EXPECT(jroOuter[2u][jss::taker_pays] == "100000000");
}
{
+128 -8
View File
@@ -163,7 +163,7 @@ public:
void
testNoParams(FeatureBitset features)
{
testcase("No Params, None Enabled");
testcase("Default Env: config-forced, none on-ledger");
using namespace test::jtx;
Env env{*this};
@@ -178,8 +178,6 @@ public:
{
if (!BEAST_EXPECT(feature.isMember(jss::name)))
return;
// default config - so all should be disabled, and
// supported. Some may be vetoed.
bool expectVeto =
(votes.at(feature[jss::name].asString()) ==
VoteBehavior::DefaultNo);
@@ -188,8 +186,12 @@ public:
VoteBehavior::Obsolete);
BEAST_EXPECTS(
feature.isMember(jss::enabled) &&
!feature[jss::enabled].asBool(),
feature[jss::enabled].asBool(),
feature[jss::name].asString() + " enabled");
BEAST_EXPECTS(
feature.isMember(jss::ledger_enabled) &&
!feature[jss::ledger_enabled].asBool(),
feature[jss::name].asString() + " ledger_enabled");
BEAST_EXPECTS(
feature.isMember(jss::vetoed) &&
feature[jss::vetoed].isBool() == !expectObsolete &&
@@ -208,7 +210,7 @@ public:
void
testSomeEnabled(FeatureBitset features)
{
testcase("No Params, Some Enabled");
testcase("Two config-forced, none on-ledger");
using namespace test::jtx;
Env env{
@@ -228,7 +230,10 @@ public:
(void)id.parseHex(it.key().asString().c_str());
if (!BEAST_EXPECT((*it).isMember(jss::name)))
return;
bool expectEnabled = env.app().getAmendmentTable().isEnabled(id);
bool const expectOnLedger =
env.app().getAmendmentTable().isEnabled(id);
bool const expectForced =
id == featureDepositAuth || id == featureDepositPreauth;
bool expectSupported =
env.app().getAmendmentTable().isSupported(id);
bool expectVeto =
@@ -239,9 +244,14 @@ public:
VoteBehavior::Obsolete);
BEAST_EXPECTS(
(*it).isMember(jss::enabled) &&
(*it)[jss::enabled].asBool() == expectEnabled,
(*it)[jss::enabled].asBool() ==
(expectOnLedger || expectForced),
(*it)[jss::name].asString() + " enabled");
if (expectEnabled)
BEAST_EXPECTS(
(*it).isMember(jss::ledger_enabled) &&
(*it)[jss::ledger_enabled].asBool() == expectOnLedger,
(*it)[jss::name].asString() + " ledger_enabled");
if (expectOnLedger)
BEAST_EXPECTS(
!(*it).isMember(jss::vetoed),
(*it)[jss::name].asString() + " vetoed");
@@ -360,12 +370,122 @@ public:
}
}
void
testConfigForced(FeatureBitset features)
{
testcase("Config-forced features ([features] stanza)");
using namespace test::jtx;
auto const forced = featurePriceOracle;
auto const forcedHex = to_string(forced);
Env env{*this, FeatureBitset(forced)};
auto jrr = env.rpc("server_definitions")[jss::result];
if (!BEAST_EXPECT(jrr.isMember(jss::features)))
return;
bool sawForced = false;
for (auto it = jrr[jss::features].begin();
it != jrr[jss::features].end();
++it)
{
auto const& f = *it;
auto const name = f[jss::name].asString();
if (!BEAST_EXPECTS(
f.isMember(jss::enabled) && f.isMember(jss::ledger_enabled),
name + " enabled/ledger_enabled"))
return;
BEAST_EXPECTS(
!f[jss::ledger_enabled].asBool(), name + " ledger_enabled");
if (it.key().asString() == forcedHex)
{
sawForced = true;
BEAST_EXPECTS(f[jss::enabled].asBool(), name + " enabled");
}
else
{
BEAST_EXPECTS(!f[jss::enabled].asBool(), name + " enabled");
}
}
BEAST_EXPECT(sawForced);
}
void
testOnLedger(FeatureBitset features)
{
testcase("On-ledger plus one config-forced");
using namespace test::jtx;
// Veto XahauGenesis: FRESH would otherwise enable it via pseudo-tx.
auto const forced = featurePriceOracle;
auto const forcedHex = to_string(forced);
Env env{
*this,
envconfig([](std::unique_ptr<Config> cfg) {
cfg->START_UP = Config::FRESH;
cfg->section(SECTION_VETO_AMENDMENTS)
.append(to_string(featureXahauGenesis) + " XahauGenesis");
return cfg;
}),
FeatureBitset(forced)};
auto jrr = env.rpc("server_definitions")[jss::result];
if (!BEAST_EXPECT(jrr.isMember(jss::features)))
return;
bool sawOnLedger = false;
bool sawForced = false;
for (auto it = jrr[jss::features].begin();
it != jrr[jss::features].end();
++it)
{
uint256 id;
(void)id.parseHex(it.key().asString().c_str());
auto const& f = *it;
auto const name = f[jss::name].asString();
if (!BEAST_EXPECTS(
f.isMember(jss::enabled) && f.isMember(jss::ledger_enabled),
name + " enabled/ledger_enabled"))
return;
bool const onLedger = env.app().getAmendmentTable().isEnabled(id);
bool const isForced = it.key().asString() == forcedHex;
BEAST_EXPECTS(
f[jss::ledger_enabled].asBool() == onLedger,
name + " ledger_enabled");
BEAST_EXPECTS(
f[jss::enabled].asBool() == (onLedger || isForced),
name + " enabled");
if (onLedger)
sawOnLedger = true;
if (isForced)
{
sawForced = true;
BEAST_EXPECTS(!onLedger, name + " forced not on-ledger");
}
}
BEAST_EXPECT(sawOnLedger);
BEAST_EXPECT(sawForced);
}
void
testServerFeatures(FeatureBitset features)
{
testNoParams(features);
testSomeEnabled(features);
testWithMajorities(features);
testConfigForced(features);
testOnLedger(features);
}
void
+4 -1
View File
@@ -82,7 +82,10 @@ namespace hook_api {
namespace hook {
bool
canHook(ripple::TxType txType, ripple::uint256 hookOn);
canHook(
STTx const& tx,
ripple::uint256 hookOn,
std::optional<ripple::Slice> hookName);
bool
canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit);
+94 -23
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(fix20261001))
{
// 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(fix20261001))
{
// 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(fix20261001))
{
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(fix20261001) &&
length.value() != field_object->size())
return Unexpected(PARSE_ERROR);
if ((type << 16) + field != field_id)
{
return Unexpected(PARSE_ERROR);
@@ -838,14 +868,22 @@ HookAPI::etxn_fee_base(ripple::Slice const& txBlob) const
std::unique_ptr<STTx const> stpTrans =
std::make_unique<STTx const>(std::ref(sitTrans));
if (!hookCtx.applyCtx.view().rules().enabled(fixHookAPI20251128))
return Transactor::calculateBaseFee(
*(applyCtx.app.openLedger().current()), *stpTrans)
.drops();
// fix20261001: the minimum fee decides whether emit() accepts the
// txn, which is consensus-visible. Compute it against the view being
// applied, not the node-local open ledger (whose hook definitions can
// differ between nodes).
std::shared_ptr<ReadView const> hold;
ReadView const* feeView = &applyCtx.view();
if (!applyCtx.view().rules().enabled(fix20261001))
{
hold = applyCtx.app.openLedger().current();
feeView = hold.get();
}
return invoke_calculateBaseFee(
*(applyCtx.app.openLedger().current()), *stpTrans)
.drops();
if (!hookCtx.applyCtx.view().rules().enabled(fixHookAPI20251128))
return Transactor::calculateBaseFee(*feeView, *stpTrans).drops();
return invoke_calculateBaseFee(*feeView, *stpTrans).drops();
}
catch (std::exception const& e)
{
@@ -3048,6 +3086,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 +3104,9 @@ HookAPI::get_stobject_length(
if (flag & 0x20) // issuer
length += 20;
if (rules.enabled(fix20261001) && 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 +3118,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(fix20261001) && !terminated)
return Unexpected(pe_unexpected_end);
}
else if (type == STI_ISSUE)
{
bool const fix = rules.enabled(fix20261001);
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(fix20261001);
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 +3196,10 @@ HookAPI::get_stobject_length(
length,
payload_start,
payload_length);
if (rules.enabled(fix20261001) && length > end - upto)
return Unexpected(pe_unexpected_end);
return length + (upto - start);
}
+52 -2
View File
@@ -785,7 +785,7 @@ hook::removeHookNamespaceEntry(ripple::SLE& sleAccount, ripple::uint256 ns)
// transactions. If you wish to set a hook that has control over ttHOOK_SET then
// set bit 1U<<22.
bool
hook::canHook(ripple::TxType txType, ripple::uint256 hookOn)
canHookTT(ripple::TxType txType, ripple::uint256 hookOn)
{
// invert ttHOOK_SET bit
hookOn ^= UINT256_BIT[ttHOOK_SET];
@@ -799,7 +799,28 @@ hook::canHook(ripple::TxType txType, ripple::uint256 hookOn)
bool
hook::canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit)
{
return hook::canHook(txType, hookCanEmit);
return canHookTT(txType, hookCanEmit);
}
bool
hook::canHook(
STTx const& tx,
ripple::uint256 hookOn,
std::optional<ripple::Slice> hookName)
{
if (!canHookTT(tx.getTxnType(), hookOn))
return false;
if (!hookName || hookName->empty())
// no hook name specified to hook, so we can always hook
return true;
if (!tx.isFieldPresent(sfHookName))
// hook name specified hook, but no hook name specified in the
// transaction, so we can't hook without the hook name
return false;
return tx[sfHookName] == *hookName;
}
ripple::uint256
@@ -2191,6 +2212,35 @@ DEFINE_HOOK_FUNCTION(
return serialize_keylet(kl, memory, write_ptr, write_len);
}
// keylets that take a validator public key
case keylet_code::MANIFEST: {
if (!applyCtx.view().rules().enabled(featureOnChainManifests))
return INVALID_ARGUMENT;
if (a == 0 || b == 0)
return INVALID_ARGUMENT;
if (c != 0 || d != 0 || e != 0 || f != 0)
return INVALID_ARGUMENT;
uint32_t read_ptr = a, read_len = b;
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
return OUT_OF_BOUNDS;
ripple::Slice const pkSlice{memory + read_ptr, read_len};
// Reject anything that is not a well-formed public key before
// constructing one: the PublicKey ctor throws on bad input.
if (!publicKeyType(pkSlice))
return INVALID_ARGUMENT;
ripple::Keylet kl =
ripple::keylet::manifest(ripple::PublicKey(pkSlice));
return serialize_keylet(kl, memory, write_ptr, write_len);
}
// keylets that take 20 byte account id, and (4 byte uint for 32
// byte hash)
case keylet_code::ORACLE: {
+3 -8
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
@@ -252,12 +252,7 @@ applyCreate(
auto sleAMMRoot = std::make_shared<SLE>(keylet::account(*ammAccount));
sleAMMRoot->setAccountID(sfAccount, *ammAccount);
sleAMMRoot->setFieldAmount(sfBalance, STAmount{});
std::uint32_t const seqno{
ctx_.view().rules().enabled(featureXahauGenesis)
? ctx_.view().info().parentCloseTime.time_since_epoch().count()
: ctx_.view().rules().enabled(featureDeletableAccounts)
? ctx_.view().seq()
: 1};
std::uint32_t const seqno = newAccountSeqNo(ctx_.view());
sleAMMRoot->setFieldU32(sfSequence, seqno);
// Ignore reserves requirement, disable the master key, allow default
// rippling (AMM LPToken can be used in payments and offer crossing but
+1 -2
View File
@@ -554,8 +554,7 @@ Change::activateXahauGenesis()
{
sle = std::make_shared<SLE>(kl);
sle->setAccountID(sfAccount, accid);
std::uint32_t const seqno{
sb.info().parentCloseTime.time_since_epoch().count()};
std::uint32_t const seqno = newAccountSeqNo(sb);
sle->setFieldU32(sfSequence, seqno);
}
+4 -7
View File
@@ -30,12 +30,6 @@
namespace ripple {
TxConsequences
ClaimReward::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
ClaimReward::preflight(PreflightContext const& ctx)
{
@@ -164,7 +158,10 @@ ClaimReward::preclaim(PreclaimContext const& ctx)
auto const& hookOn =
hook::getHookOn(hook, sleDef, sfHookOnIncoming);
if (hook::canHook(ttCLAIM_REWARD, hookOn))
auto const hookName = ctx.view.rules().enabled(fix20261001)
? hook[~sfHookName]
: std::nullopt;
if (hook::canHook(ctx.tx, hookOn, hookName))
{
hasClaimRewardHook = true;
break;
+1 -4
View File
@@ -32,15 +32,12 @@ namespace ripple {
class ClaimReward : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit ClaimReward(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
-6
View File
@@ -30,12 +30,6 @@
namespace ripple {
TxConsequences
Cron::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
Cron::preflight(PreflightContext const& ctx)
{
+1 -4
View File
@@ -30,7 +30,7 @@ namespace ripple {
class Cron : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit Cron(ApplyContext& ctx) : Transactor(ctx)
{
@@ -39,9 +39,6 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
-6
View File
@@ -27,12 +27,6 @@
namespace ripple {
TxConsequences
CronSet::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
CronSet::preflight(PreflightContext const& ctx)
{
+1 -4
View File
@@ -29,7 +29,7 @@ namespace ripple {
class CronSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit CronSet(ApplyContext& ctx) : Transactor(ctx)
{
@@ -38,9 +38,6 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
+1 -2
View File
@@ -243,8 +243,7 @@ GenesisMint::doApply()
if (created)
{
// Create the account.
std::uint32_t const seqno{
view().info().parentCloseTime.time_since_epoch().count()};
std::uint32_t const seqno = newAccountSeqNo(view());
sle = std::make_shared<SLE>(k);
sle->setAccountID(sfAccount, id);
+1 -6
View File
@@ -1307,12 +1307,7 @@ Import::doApply()
if (create)
{
// Create the account.
std::uint32_t const seqno{
view().rules().enabled(featureXahauGenesis)
? view().info().parentCloseTime.time_since_epoch().count()
: view().rules().enabled(featureDeletableAccounts)
? view().seq()
: 1};
std::uint32_t const seqno = newAccountSeqNo(view());
sle = std::make_shared<SLE>(keylet::account(id));
sle->setAccountID(sfAccount, id);
+1 -5
View File
@@ -1041,11 +1041,7 @@ ValidNewAccountRoot::finalize(
tt == ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION) &&
isTesSuccess(result))
{
std::uint32_t const startingSeq{
view.rules().enabled(featureXahauGenesis)
? view.info().parentCloseTime.time_since_epoch().count()
: view.rules().enabled(featureDeletableAccounts) ? view.seq()
: 1};
std::uint32_t const startingSeq = newAccountSeqNo(view);
if (accountSeq_ != startingSeq)
{
-6
View File
@@ -26,12 +26,6 @@
namespace ripple {
TxConsequences
Invoke::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
Invoke::preflight(PreflightContext const& ctx)
{
+1 -4
View File
@@ -30,7 +30,7 @@ namespace ripple {
class Invoke : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit Invoke(ApplyContext& ctx) : Transactor(ctx)
{
@@ -39,9 +39,6 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
+1 -6
View File
@@ -354,12 +354,7 @@ Payment::doApply()
if (!sleDst)
{
std::uint32_t const seqno{
view().rules().enabled(featureXahauGenesis)
? view().info().parentCloseTime.time_since_epoch().count()
: view().rules().enabled(featureDeletableAccounts)
? view().seq()
: 1};
std::uint32_t const seqno = newAccountSeqNo(view());
// Create the account.
sleDst = std::make_shared<SLE>(k);
+3 -7
View File
@@ -18,9 +18,9 @@
//==============================================================================
#include <xrpld/app/tx/detail/Remit.h>
#include <xrpld/app/tx/detail/URIToken.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/UTF8.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
@@ -201,7 +201,7 @@ Remit::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
if (!URIToken::validateUTF8(uri))
if (!isValidUTF8(makeSlice(uri)))
{
JLOG(ctx.j.warn())
<< "Malformed transaction: Invalid UTF8 inside MintURIToken.";
@@ -328,11 +328,7 @@ Remit::doApply()
nativeRemit += accountReserve;
// Create the account.
std::uint32_t const seqno{
sb.rules().enabled(featureXahauGenesis)
? sb.info().parentCloseTime.time_since_epoch().count()
: sb.rules().enabled(featureDeletableAccounts) ? sb.seq()
: 1};
std::uint32_t const seqno = newAccountSeqNo(sb);
sleDstAcc = std::make_shared<SLE>(keylet::account(dstAccID));
sleDstAcc->setAccountID(sfAccount, dstAccID);
+115 -58
View File
@@ -23,9 +23,9 @@
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/tx/detail/URIToken.h>
#include <xrpld/ledger/ApplyView.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/UTF8.h>
#include <xrpl/hook/Enum.h>
#include <xrpl/hook/Guard.h>
#include <xrpl/protocol/Feature.h>
@@ -207,7 +207,7 @@ validateHookParams(SetHookCtx& ctx, STArray const& hookParams)
// infer which operation the user is attempting to execute from the present and
// absent fields
HookSetOperation
SetHook::inferOperation(STObject const& hookSetObj)
SetHook::inferOperation(SetHookCtx& ctx, STObject const& hookSetObj)
{
uint64_t wasmByteCount = hookSetObj.isFieldPresent(sfCreateCode)
? hookSetObj.getFieldVL(sfCreateCode).size()
@@ -216,7 +216,12 @@ SetHook::inferOperation(STObject const& hookSetObj)
bool hasHash = hookSetObj.isFieldPresent(sfHookHash);
bool hasCode = hookSetObj.isFieldPresent(sfCreateCode);
if (hasHash && hasCode) // Both HookHash and CreateCode: invalid
bool invalidHookOn = ctx.rules.enabled(featureHookOnV2_1) &&
hookSetObj.isFieldPresent(sfHookOnOutgoing) !=
hookSetObj.isFieldPresent(sfHookOnIncoming);
if ((hasHash && hasCode) || invalidHookOn) // Both HookHash and CreateCode
// or invalid HookOn: invalid
return hsoINVALID;
else if (hasHash) // Hookhash only: install
return hsoINSTALL;
@@ -244,6 +249,62 @@ SetHook::inferOperation(STObject const& hookSetObj)
: hsoUPDATE;
}
bool
validateHookOn(SetHookCtx& ctx, STObject const& hookSetObj)
{
if (!hookSetObj.isFieldPresent(sfHookOn))
{
if (!ctx.rules.enabled(featureHookOnV2) &&
!ctx.rules.enabled(featureHookOnV2_1))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook must include "
"sfHookOn before featureHookOnV2_1 is enabled.";
return false;
}
if (!hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
!hookSetObj.isFieldPresent(sfHookOnIncoming))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook must include "
"sfHookOnOutgoing and sfHookOnIncoming "
"when creating a new hook without sfHookOn.";
return false;
}
auto const outgoing = hookSetObj.getFieldH256(sfHookOnOutgoing);
auto const incoming = hookSetObj.getFieldH256(sfHookOnIncoming);
if (outgoing == incoming)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook outgoing and "
"incoming hookon must be different.";
return false;
}
return true;
}
else
{
if (hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
hookSetObj.isFieldPresent(sfHookOnIncoming))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")[" << HS_ACC()
<< "]: Malformed transaction: SetHook must no"
"include sfHookOnOutgoing and sfHookOnIncoming "
"when creating a new hook with sfHookOn.";
return false;
}
}
return true;
}
// This is a context-free validation, it does not take into account the current
// state of the ledger returns < valid, instruction count > may throw
// overflow_error
@@ -254,7 +315,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
? hookSetObj.getFieldU32(sfFlags)
: 0;
switch (inferOperation(hookSetObj))
switch (inferOperation(ctx, hookSetObj))
{
case hsoNOOP: {
return true;
@@ -363,6 +424,13 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
// hookon may be present if the user so chooses
// flags may be present if the user so chooses
if (ctx.rules.enabled(featureHookOnV2_1) &&
(hookSetObj.isFieldPresent(sfHookOn) ||
hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
hookSetObj.isFieldPresent(sfHookOnIncoming)) &&
!validateHookOn(ctx, hookSetObj))
return false;
return true;
}
@@ -407,6 +475,13 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
// hookon may be present if the user so chooses
// flags may be present if the user so chooses
if (ctx.rules.enabled(featureHookOnV2_1) &&
(hookSetObj.isFieldPresent(sfHookOn) ||
hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
hookSetObj.isFieldPresent(sfHookOnIncoming)) &&
!validateHookOn(ctx, hookSetObj))
return false;
return true;
}
@@ -456,56 +531,8 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
}
// validate sfHookOn
if (!hookSetObj.isFieldPresent(sfHookOn))
{
if (!ctx.rules.enabled(featureHookOnV2))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook must include "
"sfHookOn before featureHookOnV2 is enabled.";
return false;
}
if (!hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
!hookSetObj.isFieldPresent(sfHookOnIncoming))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook must include "
"sfHookOnOutgoing and sfHookOnIncoming "
"when creating a new hook without sfHookOn.";
return false;
}
auto const outgoing = hookSetObj.getFieldH256(sfHookOnOutgoing);
auto const incoming = hookSetObj.getFieldH256(sfHookOnIncoming);
if (outgoing == incoming)
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook outgoing and "
"incoming hookon must be different.";
return false;
}
}
else
{
if (hookSetObj.isFieldPresent(sfHookOnOutgoing) ||
hookSetObj.isFieldPresent(sfHookOnIncoming))
{
JLOG(ctx.j.trace())
<< "HookSet(" << hook::log::HOOKON_MISSING << ")["
<< HS_ACC()
<< "]: Malformed transaction: SetHook must no"
"include sfHookOnOutgoing and sfHookOnIncoming "
"when creating a new hook with sfHookOn.";
return false;
}
}
if (!validateHookOn(ctx, hookSetObj))
return false;
// validate sfHookCanEmit
// HookCanEmit field is an optional field for backward compatibility
@@ -630,7 +657,7 @@ SetHook::validateHookName(Blob const& name, beast::Journal const& j)
<< "sfHookName must be between 8 and 32 hex characters.";
return false;
}
if (!URIToken::validateUTF8(name))
if (!isValidUTF8(makeSlice(name)))
{
JLOG(j.trace()) << "sfHookName must be a valid UTF-8 string.";
return false;
@@ -1405,7 +1432,7 @@ SetHook::setHook()
HookSetOperation op = hsoNOOP;
if (hookSetObj)
op = inferOperation(hookSetObj->get());
op = inferOperation(ctx, hookSetObj->get());
// these flags are not able to be passed onto the ledger object
int newFlags = 0;
@@ -1627,10 +1654,23 @@ SetHook::setHook()
newHook.setFieldH256(sfHookNamespace, *newNamespace);
}
if (ctx.rules.enabled(featureHookOnV2_1))
{
// sanity check
if (newHookOn && (newHookOnOutgoing || newHookOnIncoming))
return tecINTERNAL; // LCOV_EXCL_LINE
if ((!newHookOnOutgoing && newHookOnIncoming) ||
(newHookOnOutgoing && !newHookOnIncoming))
return tecINTERNAL; // LCOV_EXCL_LINE
}
// set the hookon field if it differs from definition
if (newHookOn)
{
if (*defHookOn == *newHookOn)
if ((!view().rules().enabled(featureHookOnV2_1) ||
defHookOn.has_value()) &&
*defHookOn == *newHookOn)
{
if (newHook.isFieldPresent(sfHookOn))
newHook.makeFieldAbsent(sfHookOn);
@@ -1665,6 +1705,22 @@ SetHook::setHook()
sfHookOnIncoming, *newHookOnIncoming);
}
if (ctx.rules.enabled(featureHookOnV2_1))
{
if (newHookOn)
{
if (newHook.isFieldPresent(sfHookOnIncoming))
newHook.makeFieldAbsent(sfHookOnIncoming);
if (newHook.isFieldPresent(sfHookOnOutgoing))
newHook.makeFieldAbsent(sfHookOnOutgoing);
}
if (newHookOnOutgoing || newHookOnIncoming)
{
if (newHook.isFieldPresent(sfHookOn))
newHook.makeFieldAbsent(sfHookOn);
}
}
// set the hookcanemit field if it differs from definition
if (newHookCanEmit)
{
@@ -1843,7 +1899,8 @@ SetHook::setHook()
newHookDef->setFieldH256(sfHookHash, *createHookHash);
// only HookOn or (HookOnOutgoing and HookOnIncoming)
if (!view().rules().enabled(featureHookOnV2) ||
if ((!view().rules().enabled(featureHookOnV2) &&
!view().rules().enabled(featureHookOnV2_1)) ||
(!newHookOnOutgoing && !newHookOnIncoming))
newHookDef->setFieldH256(sfHookOn, *newHookOn);
else
+1 -1
View File
@@ -86,7 +86,7 @@ public:
calculateBaseFee(ReadView const& view, STTx const& tx);
static HookSetOperation
inferOperation(STObject const& hookSetObj);
inferOperation(SetHookCtx& ctx, STObject const& hookSetObj);
static HookSetValidation
validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj);
-6
View File
@@ -30,12 +30,6 @@
namespace ripple {
TxConsequences
SetRemarks::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
SetRemarks::validateRemarks(STArray const& remarks, beast::Journal const& j)
{
+1 -4
View File
@@ -30,7 +30,7 @@ namespace ripple {
class SetRemarks : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit SetRemarks(ApplyContext& ctx) : Transactor(ctx)
{
@@ -39,9 +39,6 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
+9 -29
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(fix20261001))
return temMALFORMED;
if (!SetHook::validateHookName(name, ctx.j))
return temMALFORMED;
}
@@ -283,19 +287,7 @@ Transactor::calculateHookChainFee(
// LCOV_EXCL_STOP
}
std::optional<Blob> requiredHookName;
if (hookObj.isFieldPresent(sfHookName) &&
hookObj.getFieldVL(sfHookName).size() > 0)
requiredHookName = hookObj.getFieldVL(sfHookName);
if (requiredHookName)
{
// need to specify same hook name in the transaction
if (!tx.isFieldPresent(sfHookName))
continue;
if (*requiredHookName != tx.getFieldVL(sfHookName))
continue;
}
auto const hookName = hookObj[~sfHookName];
uint32_t flags = 0;
if (hookObj.isFieldPresent(sfFlags))
@@ -307,7 +299,7 @@ Transactor::calculateHookChainFee(
uint256 hookOn = hook::getHookOn(
hookObj, hookDef, isOutgoing ? sfHookOnOutgoing : sfHookOnIncoming);
if (hook::canHook(tx.getTxnType(), hookOn) &&
if (hook::canHook(tx, hookOn, hookName) &&
(!collectCallsOnly || (flags & hook::hsfCOLLECT)))
{
XRPAmount const toAdd{hookDef->getFieldAmount(sfFee).xrp().drops()};
@@ -1377,25 +1369,13 @@ Transactor::executeHookChain(
// LCOV_EXCL_STOP
}
std::optional<Blob> requiredHookName;
if (hookObj.isFieldPresent(sfHookName) &&
hookObj.getFieldVL(sfHookName).size() > 0)
requiredHookName = hookObj.getFieldVL(sfHookName);
if (requiredHookName)
{
// need to specify same hook name in the transaction
if (!ctx_.tx.isFieldPresent(sfHookName))
continue;
if (*requiredHookName != ctx_.tx.getFieldVL(sfHookName))
continue;
}
auto const hookName = hookObj[~sfHookName];
// check if the hook can fire
uint256 hookOn = hook::getHookOn(
hookObj, hookDef, isOutgoing ? sfHookOnOutgoing : sfHookOnIncoming);
if (!hook::canHook(ctx_.tx.getTxnType(), hookOn))
if (!hook::canHook(ctx_.tx, hookOn, hookName))
continue; // skip if it can't
uint256 hookCanEmit = hook::getHookCanEmit(hookObj, hookDef);
+2 -1
View File
@@ -20,6 +20,7 @@
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/tx/detail/URIToken.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/UTF8.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/Quality.h>
@@ -98,7 +99,7 @@ URIToken::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
if (!validateUTF8(uri))
if (!isValidUTF8(makeSlice(uri)))
{
JLOG(ctx.j.warn()) << "Malformed transaction. URI must be a "
"valid utf-8 string.";
-50
View File
@@ -30,56 +30,6 @@ namespace ripple {
class URIToken : public Transactor
{
public:
bool inline static validateUTF8(std::vector<uint8_t> const& u)
{
// this code is from
// https://www.cl.cam.ac.uk/~mgk25/ucs/utf8_check.c
uint8_t const* s = (uint8_t const*)u.data();
uint8_t const* end = s + u.size();
while (s < end)
{
if (*s < 0x80)
/* 0xxxxxxx */
s++;
else if ((s[0] & 0xe0) == 0xc0)
{
/* 110XXXXx 10xxxxxx */
if ((s[1] & 0xc0) != 0x80 ||
(s[0] & 0xfe) == 0xc0) /* overlong? */
return false;
else
s += 2;
}
else if ((s[0] & 0xf0) == 0xe0)
{
/* 1110XXXX 10Xxxxxx 10xxxxxx */
if ((s[1] & 0xc0) != 0x80 || (s[2] & 0xc0) != 0x80 ||
(s[0] == 0xe0 && (s[1] & 0xe0) == 0x80) || /* overlong? */
(s[0] == 0xed && (s[1] & 0xe0) == 0xa0) || /* surrogate? */
(s[0] == 0xef && s[1] == 0xbf &&
(s[2] & 0xfe) == 0xbe)) /* U+FFFE or U+FFFF? */
return false;
else
s += 3;
}
else if ((s[0] & 0xf8) == 0xf0)
{
/* 11110XXX 10XXxxxx 10xxxxxx 10xxxxxx */
if ((s[1] & 0xc0) != 0x80 || (s[2] & 0xc0) != 0x80 ||
(s[3] & 0xc0) != 0x80 ||
(s[0] == 0xf0 && (s[1] & 0xf0) == 0x80) || /* overlong? */
(s[0] == 0xf4 && s[1] > 0x8f) ||
s[0] > 0xf4) /* > U+10FFFF? */
return false;
else
s += 4;
}
else
return false;
}
return true;
}
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
explicit URIToken(ApplyContext& ctx) : Transactor(ctx)
+1 -5
View File
@@ -481,11 +481,7 @@ transferHelper(
}
// Create the account.
std::uint32_t const seqno{
psb.rules().enabled(featureXahauGenesis)
? psb.info().parentCloseTime.time_since_epoch().count()
: psb.rules().enabled(featureDeletableAccounts) ? psb.seq()
: 1};
std::uint32_t const seqno = newAccountSeqNo(psb);
sleDst = std::make_shared<SLE>(dstK);
sleDst->setAccountID(sfAccount, dst);
+18
View File
@@ -1266,6 +1266,24 @@ deleteAMMTrustLine(
std::optional<AccountID> const& ammAccountID,
beast::Journal j);
[[nodiscard]] inline std::uint32_t
newAccountSeqNo(ReadView const& view)
{
auto const time = view.info().parentCloseTime.time_since_epoch().count();
return view.rules().enabled(featureXahauGenesis)
// TEQU:
// When creating accounts in GenesisLedger, we previously set
// the account Sequence to 0. However, since this conflicts with
// the PseudoAccount requirements, we are changing it to 1.
// There will be no impact on networks that are already running,
// and for future networks, there won't be any impact unless you
// create accounts via GenesisLedger.
// This specifically addresses an issue that comes up during
// unittests.
? (time == 0 ? 1 : time)
: view.rules().enabled(featureDeletableAccounts) ? view.seq() : 1;
}
} // namespace ripple
#endif
+31 -91
View File
@@ -22,12 +22,10 @@
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/AmendmentTable.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/core/Config.h>
#include <xrpld/rpc/detail/TransactionSign.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/json_writer.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
@@ -35,14 +33,6 @@
#include <magic_enum.hpp>
#include <sstream>
#define MAGIC_ENUM(x, _min, _max) \
template <> \
struct magic_enum::customize::enum_range<x> \
{ \
static constexpr int min = _min; \
static constexpr int max = _max; \
};
#define MAGIC_ENUM_16(x) \
template <> \
struct magic_enum::customize::enum_range<x> \
@@ -58,15 +48,6 @@
static constexpr bool is_flags = true; \
};
MAGIC_ENUM(ripple::SerializedTypeID, -2, 10004);
MAGIC_ENUM(ripple::LedgerEntryType, 0, 255);
MAGIC_ENUM(ripple::TELcodes, -399, 300);
MAGIC_ENUM(ripple::TEMcodes, -299, -200);
MAGIC_ENUM(ripple::TEFcodes, -199, -100);
MAGIC_ENUM(ripple::TERcodes, -99, -1);
MAGIC_ENUM(ripple::TEScodes, 0, 1);
MAGIC_ENUM(ripple::TECcodes, 100, 255);
MAGIC_ENUM_16(ripple::TxType);
MAGIC_ENUM_FLAG(ripple::UniversalFlags);
MAGIC_ENUM_FLAG(ripple::AccountSetFlags);
MAGIC_ENUM_FLAG(ripple::OfferCreateFlags);
@@ -192,24 +173,19 @@ private:
ret[jss::TYPES]["Done"] = -1;
std::map<int32_t, std::string> type_map{{-1, "Done"}};
for (auto const& entry : magic_enum::enum_entries<SerializedTypeID>())
for (auto const& [rawName, typeValue] : sTypeMap)
{
const auto name = entry.second;
std::string type_name =
translate(name.data() + 4 /* remove STI_ */);
int32_t type_value = static_cast<int32_t>(entry.first);
ret[jss::TYPES][type_name] = type_value;
type_map[type_value] = type_name;
std::string typeName =
translate(std::string(rawName).substr(4) /* remove STI_ */);
ret[jss::TYPES][typeName] = typeValue;
type_map[typeValue] = typeName;
}
ret[jss::LEDGER_ENTRY_TYPES] = Json::objectValue;
ret[jss::LEDGER_ENTRY_TYPES][jss::Invalid] = -1;
for (auto const& entry : magic_enum::enum_entries<LedgerEntryType>())
for (auto const& f : LedgerFormats::getInstance())
{
const auto name = entry.second;
std::string type_name = translate(name.data() + 2 /* remove lt_ */);
int32_t type_value = static_cast<int32_t>(entry.first);
ret[jss::LEDGER_ENTRY_TYPES][type_name] = type_value;
ret[jss::LEDGER_ENTRY_TYPES][f.getName()] = f.getType();
}
ret[jss::FIELDS] = Json::arrayValue;
@@ -326,71 +302,16 @@ private:
}
ret[jss::TRANSACTION_RESULTS] = Json::objectValue;
for (auto const& entry : magic_enum::enum_entries<TELcodes>())
for (auto const& [code, terInfo] : transResults())
{
const auto name = entry.second;
ret[jss::TRANSACTION_RESULTS][STR(name)] =
static_cast<int32_t>(entry.first);
ret[jss::TRANSACTION_RESULTS][terInfo.first] = code;
}
for (auto const& entry : magic_enum::enum_entries<TEMcodes>())
{
const auto name = entry.second;
ret[jss::TRANSACTION_RESULTS][STR(name)] =
static_cast<int32_t>(entry.first);
}
for (auto const& entry : magic_enum::enum_entries<TEFcodes>())
{
const auto name = entry.second;
ret[jss::TRANSACTION_RESULTS][STR(name)] =
static_cast<int32_t>(entry.first);
}
for (auto const& entry : magic_enum::enum_entries<TERcodes>())
{
const auto name = entry.second;
ret[jss::TRANSACTION_RESULTS][STR(name)] =
static_cast<int32_t>(entry.first);
}
for (auto const& entry : magic_enum::enum_entries<TEScodes>())
{
const auto name = entry.second;
ret[jss::TRANSACTION_RESULTS][STR(name)] =
static_cast<int32_t>(entry.first);
}
for (auto const& entry : magic_enum::enum_entries<TECcodes>())
{
const auto name = entry.second;
ret[jss::TRANSACTION_RESULTS][STR(name)] =
static_cast<int32_t>(entry.first);
}
auto const translate_tt = [](std::string inp) -> std::string {
if (inp == "Amendment")
return "EnableAmendment";
if (inp == "Fee")
return "SetFee";
if (inp == "PaychanClaim")
return "PaymentChannelClaim";
if (inp == "PaychanCreate")
return "PaymentChannelCreate";
if (inp == "PaychanFund")
return "PaymentChannelFund";
if (inp == "RegularKeySet")
return "SetRegularKey";
if (inp == "HookSet")
return "SetHook";
if (inp == "RemarksSet")
return "SetRemarks";
return inp;
};
ret[jss::TRANSACTION_TYPES] = Json::objectValue;
ret[jss::TRANSACTION_TYPES][jss::Invalid] = -1;
for (auto const& entry : magic_enum::enum_entries<TxType>())
for (auto const& f : TxFormats::getInstance())
{
const auto name = entry.second;
std::string type_name = translate_tt(translate(name.data() + 2));
int32_t type_value = static_cast<int32_t>(entry.first);
ret[jss::TRANSACTION_TYPES][type_name] = type_value;
ret[jss::TRANSACTION_TYPES][f.getName()] = f.getType();
}
// Transaction Flags:
@@ -545,6 +466,25 @@ doServerDefinitions(RPC::JsonContext& context)
features[to_string(h)][jss::majority] =
t.time_since_epoch().count();
// getJson's enabled is on-ledger; [features] also apply here.
for (auto const& name : features.getMemberNames())
{
Json::Value& entry = features[name];
entry[jss::ledger_enabled] = entry[jss::enabled].asBool();
}
for (auto const& h : context.app.config().features)
{
Json::Value& entry = features[to_string(h)];
if (!entry.isMember(jss::name))
{
if (auto const fname = featureToName(h); !fname.empty())
entry[jss::name] = fname;
}
if (!entry.isMember(jss::ledger_enabled))
entry[jss::ledger_enabled] = false;
entry[jss::enabled] = true;
}
lastFeatures = features;
{
const std::string out = Json::FastWriter().write(features);