Compare commits

..
Author SHA1 Message Date
tequ 043dcb2a38 URITokenBroker Amendment 2026-08-24 17:34:08 +09:00
115 changed files with 3417 additions and 7979 deletions

No files matched your search

-9
View File
@@ -19,15 +19,6 @@ 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,10 +50,6 @@ 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:
@@ -85,8 +81,6 @@ 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
@@ -111,14 +105,7 @@ runs:
os=${{ inputs.os }}
EOF
# [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
# Add buildenv and conf sections for Linux (not needed for macOS)
if [ "${{ inputs.os }}" = "Linux" ] && [ -n "${{ inputs.cc }}" ]; then
cat >> ~/.conan2/profiles/default <<EOF
@@ -129,38 +116,16 @@ runs:
[conf]
tools.build:compiler_executables={"c": "/usr/bin/${{ inputs.cc }}", "cpp": "/usr/bin/${{ inputs.cxx }}"}
EOF
NEED_CONF=1
fi
if [ -n "${CONAN_DEPS_CXXFLAGS}" ] && [ "${CONAN_DEPS_CXXFLAGS}" != "{}" ]; then
CONAN_DEPS_CXXFLAGS_LINES="$(python3 - <<'PY'
import json
import os
import sys
# Add macOS-specific conf if needed
if [ "${{ inputs.os }}" = "Macos" ]; then
cat >> ~/.conan2/profiles/default <<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
[conf]
# Workaround for gRPC with newer Apple Clang
tools.build:cxxflags=["-Wno-missing-template-arg-list-after-template-kw"]
EOF
fi
# Display profile for verification
-3
View File
@@ -107,9 +107,6 @@ 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
+22 -66
View File
@@ -72,26 +72,15 @@ jobs:
"job_type": "build"
},
{
# 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",
"compiler_id": "gcc-13-libstdcxx",
"compiler": "gcc",
"cc": "gcc-13",
"cxx": "g++-13",
"gcov": "gcov-13",
"compiler_version": 13,
"stdlib": "default",
"configuration": "Debug",
"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"]
}
"job_type": "coverage"
},
{
"compiler_id": "clang-14-libstdcxx-gcc11",
@@ -142,7 +131,7 @@ jobs:
# Minimal matrix for PRs and feature branches
minimal_matrix = [
full_matrix[1], # gcc-13 (middle-ground gcc)
full_matrix[2], # clang-20 llvm-cov coverage
full_matrix[2], # gcc-13 coverage
full_matrix[3] # clang-14 (mature, stable clang)
]
@@ -218,9 +207,9 @@ jobs:
# Select the appropriate matrix
if use_full:
if force_full:
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - forced by [ci-nix-full-matrix] tag")
print(f"Using FULL matrix (7 configs) - forced by [ci-nix-full-matrix] tag")
else:
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - targeting main branch")
print(f"Using FULL matrix (7 configs) - targeting main branch")
matrix = full_matrix
else:
print(f"Using MINIMAL matrix (3 configs) - feature branch/PR")
@@ -268,25 +257,9 @@ 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
@@ -359,16 +332,10 @@ jobs:
pipx install "conan>=2.0,<3"
echo "$HOME/.local/bin" >> $GITHUB_PATH
# Install coverage tooling
# Install gcovr for coverage jobs
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
@@ -381,15 +348,10 @@ jobs:
which ${{ matrix.cxx }} && ${{ matrix.cxx }} --version || echo "${{ matrix.cxx }} not found"
which ccache && ccache --version || echo "ccache not found"
# Check coverage tooling
# Check gcovr for coverage jobs
if [ "${{ matrix.job_type }}" = "coverage" ]; then
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
which gcov && gcov --version || echo "gcov not found"
which gcovr && gcovr --version || echo "gcovr not found"
fi
echo "---- Full Environment ----"
@@ -416,7 +378,6 @@ 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
@@ -449,9 +410,8 @@ 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.
# 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"'
# 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"'
cmake-target: 'coverage'
ccache_max_size: '100G'
@@ -483,26 +443,22 @@ 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_FILE}" ./
echo "COVERAGE_FILE=${COVERAGE_FILE}" >> "$GITHUB_ENV"
mv "${{ env.build_dir }}/coverage.xml" ./
- name: Archive coverage report
if: matrix.job_type == 'coverage'
uses: actions/upload-artifact@v4
with:
name: ${{ env.COVERAGE_FILE }}-${{ matrix.compiler_id }}
path: ${{ env.COVERAGE_FILE }}
name: coverage.xml
path: coverage.xml
retention-days: 30
- name: Upload coverage report
if: matrix.job_type == 'coverage'
uses: codecov/codecov-action@v5
with:
files: ${{ env.COVERAGE_FILE }}
flags: ${{ matrix.coverage_tool }}
files: coverage.xml
fail_ci_if_error: true
disable_search: true
verbose: true
-6
View File
@@ -77,11 +77,6 @@ test.ledger > xrpld.app
test.ledger > xrpld.core
test.ledger > xrpld.ledger
test.ledger > xrpl.protocol
test.net > test.toplevel
test.net > xrpl.basics
test.net > xrpld.core
test.net > xrpld.net
test.net > xrpl.json
test.nodestore > test.jtx
test.nodestore > test.toplevel
test.nodestore > test.unit_test
@@ -205,7 +200,6 @@ xrpld.rpc > xrpld.core
xrpld.rpc > xrpld.ledger
xrpld.rpc > xrpld.nodestore
xrpld.rpc > xrpld.shamap
xrpld.rpc > xrpl.hook
xrpld.rpc > xrpl.json
xrpld.rpc > xrpl.protocol
xrpld.rpc > xrpl.resource
+2 -2
View File
@@ -494,7 +494,7 @@
#
# Configure the maximum number of transactions to have in the job queue
#
# Must be a number between 100 and 1000, defaults to 1000
# Must be a number between 100 and 1000, defaults to 250
#
#
# [overlay]
@@ -593,7 +593,7 @@
# reaches or exceeds this number. After that the limit may still
# change, but will stay above the target. If consensus is not
# healthy, the limit will be clamped to this value or lower.
# Default: 1000.
# Default: 50.
#
# maximum_txn_in_ledger = <number>
#
+2
View File
@@ -146,6 +146,8 @@ D686F2538F410C9D0D856788E98E3579595DAF7B38D38887F81ECAC934B06040 HooksUpdate1
3C43D9A973AA4443EF3FC38E42DD306160FBFFDAB901CD8BAA15D09F2597EB87 NonFungibleTokensV1
0285B7E5E08E1A8E4C15636F0591D87F73CB6A7B6452A932AD72BBC8E5D1CBE3 fixNFTokenDirV1
36799EA497B1369B170805C078AEFE6188345F9B3E324C21E9CA3FF574E3C3D6 fixNFTokenNegOffer
4C499D17719BB365B69010A436B64FD1A82AAB199FC1CEB06962EBD01059FB09 fixXahauV1
215181D23BF5C173314B5FDB9C872C92DE6CC918483727DE037C0C13E7E6EE9D fixXahauV2
0D8BF22FF7570D58598D1EF19EBB6E142AD46E59A223FD3816262FBB69345BEA Remit
7CA0426E7F411D39BB014E57CD9E08F61DE1750F0D41FCD428D9FB80BB7596B0 ZeroB2M
4B8466415FAB32FFA89D9DCBE166A42340115771DF611A7160F8D7439C87ECD8 fixNSDelete
-156
View File
@@ -1,156 +0,0 @@
#[===================================================================[
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,21 +11,6 @@ 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,
+4 -6
View File
@@ -28,17 +28,15 @@ 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}>,$<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>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-g --coverage>
$<$<BOOL:${profile}>:-pg>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>)
target_link_libraries (opts
INTERFACE
$<$<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>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-g --coverage -fprofile-abs-path>
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-g --coverage>
$<$<BOOL:${profile}>:-pg>
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>)
+1 -13
View File
@@ -29,25 +29,13 @@ 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 (gcov tool only).")
"Additional arguments to pass to gcovr.")
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,7 +38,6 @@
#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
+1 -2
View File
@@ -152,7 +152,6 @@
#define sfEscrowID ((5U << 16U) + 35U)
#define sfURITokenID ((5U << 16U) + 36U)
#define sfDomainID ((5U << 16U) + 37U)
#define sfManifestID ((5U << 16U) + 91U)
#define sfHookOnOutgoing ((5U << 16U) + 93U)
#define sfHookOnIncoming ((5U << 16U) + 94U)
#define sfCron ((5U << 16U) + 95U)
@@ -243,6 +242,7 @@
#define sfLockingChainDoor ((8U << 16U) + 22U)
#define sfIssuingChainDoor ((8U << 16U) + 23U)
#define sfSubject ((8U << 16U) + 24U)
#define sfBrokerAccount ((8U << 16U) + 98U)
#define sfInform ((8U << 16U) + 99U)
#define sfIndexes ((19U << 16U) + 1U)
#define sfHashes ((19U << 16U) + 2U)
@@ -289,7 +289,6 @@
#define sfXChainCreateAccountAttestationCollectionElement ((14U << 16U) + 31U)
#define sfPriceData ((14U << 16U) + 32U)
#define sfCredential ((14U << 16U) + 33U)
#define sfManifest ((14U << 16U) + 90U)
#define sfAmountEntry ((14U << 16U) + 91U)
#define sfMintURIToken ((14U << 16U) + 92U)
#define sfHookEmission ((14U << 16U) + 93U)
-1
View File
@@ -61,7 +61,6 @@
#define ttNFTOKEN_MODIFY 70
#define ttPERMISSIONED_DOMAIN_SET 71
#define ttPERMISSIONED_DOMAIN_DELETE 72
#define ttMANIFEST_SET 91
#define ttCRON 92
#define ttCRON_SET 93
#define ttREMARKS_SET 94
-60
View File
@@ -1,60 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_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 -2
View File
@@ -313,7 +313,6 @@ enum keylet_code : uint32_t {
MPTOKEN = 34,
CREDENTIAL = 35,
PERMISSIONED_DOMAIN = 36,
MANIFEST = 37,
};
}
@@ -383,7 +382,7 @@ enum class hook_return_code : int64_t {
INVALID_KEY = -41, // user supplied key was not valid
NOT_A_STRING = -42, // nul terminator missing from a string argument
MEM_OVERLAP = -43, // one or more specified buffers are the same memory
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 256 modified state
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 5000 modified state
// entires in the combined hook chains
TOO_MANY_NAMESPACES = -45
};
+1 -7
View File
@@ -244,13 +244,7 @@
<< "Tried to accept/rollback but specified " \
"memory outside of the wasm instance " \
<< "limit when specifying a reason string"; \
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; \
return OUT_OF_BOUNDS; \
} \
/* assembly script and some other languages use utf16 for strings \
*/ \
-4
View File
@@ -372,10 +372,6 @@ permissionedDomain(AccountID const& account, std::uint32_t seq) noexcept;
Keylet
permissionedDomain(uint256 const& domainID) noexcept;
Keylet
manifest(PublicKey const& pk) noexcept;
} // namespace keylet
// Everything below is deprecated and should be removed in favor of keylets:
-2
View File
@@ -194,8 +194,6 @@ enum TEFcodes : TERUnderlyingType {
tefNONDIR_EMIT,
tefIMPORT_BLACKHOLED,
tefINVALID_LEDGER_FIX_TYPE,
tefPAST_MANIFEST_SEQ,
tefREVOKED_MANIFEST,
};
//------------------------------------------------------------------------------
+5 -8
View File
@@ -34,9 +34,7 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FIX (20261001, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(HookOnV2_1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(OnChainManifests, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(URITokenBroker, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)
@@ -47,7 +45,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::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(AMMClawback, Supported::yes, 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.
@@ -67,8 +65,7 @@ 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)
// replaced to HookOnV2_1
XRPL_FEATURE(HookOnV2, Supported::no, VoteBehavior::DefaultNo)
XRPL_FEATURE(HookOnV2, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookAPI20251128, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (CronStacking, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo)
@@ -92,6 +89,8 @@ XRPL_FIX (240819, Supported::yes, VoteBehavior::DefaultYe
XRPL_FIX (NSDelete, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(ZeroB2M, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Remit, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (XahauV2, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (XahauV1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(HooksUpdate1, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(XahauGenesis, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(Import, Supported::yes, VoteBehavior::DefaultYes)
@@ -162,8 +161,6 @@ XRPL_FEATURE(CryptoConditionsSuite, Supported::yes, VoteBehavior::Obsolete)
// pre-amendment code has been removed and the identifiers are deprecated.
// All known amendments and amendments that may appear in a validated
// ledger must be registered either here or above with the "active" amendments
XRPL_RETIRE(fixXahauV2)
XRPL_RETIRE(fixXahauV1)
XRPL_RETIRE(MultiSign)
XRPL_RETIRE(TrustSetAuth)
XRPL_RETIRE(FeeEscalation)
@@ -262,7 +262,6 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
{sfHookStateScale, soeOPTIONAL},
{sfCron, soeOPTIONAL},
{sfAMMID, soeOPTIONAL},
{sfManifestID, soeOPTIONAL},
}))
/** A ledger object which contains a list of object identifiers.
@@ -593,26 +592,6 @@ LEDGER_ENTRY(ltDID, 0x008D, DID, did, ({
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
LEDGER_ENTRY(ltMANIFEST, 0x004D, Manifest, manifest_entry, ({
{sfAccount, soeREQUIRED},
{sfPublicKey, soeREQUIRED},
{sfSigningPubKey, soeOPTIONAL}, /* may be absent if the master is revoked */
{sfSequence, soeREQUIRED},
/* sfVersion defaults to 0 and is omitted from the signed payload when
absent. Storing it unconditionally would change the bytes and break
signature verification when the manifest is reconstructed. */
{sfVersion, soeOPTIONAL},
{sfDomain, soeOPTIONAL},
/* The signatures are mirrored so the object is a lossless copy of the
manifest and can be independently verified, and re-served to peers,
by any node reading it (see ManifestCache::applyLedger). */
{sfMasterSignature, soeREQUIRED},
{sfSignature, soeOPTIONAL}, /* absent if the master is revoked */
{sfManifestID, soeOPTIONAL}, /* pointer to the identical other copy on the other key */
{sfPreviousTxnID, soeREQUIRED},
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE
+1 -2
View File
@@ -210,7 +210,6 @@ TYPED_SFIELD(sfOfferID, UINT256, 34)
TYPED_SFIELD(sfEscrowID, UINT256, 35)
TYPED_SFIELD(sfURITokenID, UINT256, 36)
TYPED_SFIELD(sfDomainID, UINT256, 37)
TYPED_SFIELD(sfManifestID, UINT256, 91)
TYPED_SFIELD(sfHookOnOutgoing, UINT256, 93)
TYPED_SFIELD(sfHookOnIncoming, UINT256, 94)
TYPED_SFIELD(sfCron, UINT256, 95)
@@ -320,6 +319,7 @@ TYPED_SFIELD(sfAttestationRewardAccount, ACCOUNT, 21)
TYPED_SFIELD(sfLockingChainDoor, ACCOUNT, 22)
TYPED_SFIELD(sfIssuingChainDoor, ACCOUNT, 23)
TYPED_SFIELD(sfSubject, ACCOUNT, 24)
TYPED_SFIELD(sfBrokerAccount, ACCOUNT, 98)
TYPED_SFIELD(sfInform, ACCOUNT, 99)
// vector of 256-bit
@@ -383,7 +383,6 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
UNTYPED_SFIELD(sfManifest, OBJECT, 90)
UNTYPED_SFIELD(sfAmountEntry, OBJECT, 91)
UNTYPED_SFIELD(sfMintURIToken, OBJECT, 92)
UNTYPED_SFIELD(sfHookEmission, OBJECT, 93)
@@ -314,6 +314,7 @@ TRANSACTION(ttURITOKEN_BURN, 46, URITokenBurn, ({
TRANSACTION(ttURITOKEN_BUY, 47, URITokenBuy, ({
{sfURITokenID, soeREQUIRED},
{sfAmount, soeREQUIRED},
{sfBrokerAccount, soeOPTIONAL},
}))
TRANSACTION(ttURITOKEN_CREATE_SELL_OFFER, 48, URITokenCreateSellOffer, ({
@@ -500,12 +501,6 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
{sfDomainID, soeREQUIRED},
}))
/* sfAccount is supplied by TxFormats::commonFields; listing it here as well
makes the SOTemplate reject the format at construction. */
TRANSACTION(ttMANIFEST_SET, 91, SetManifest, ({
{sfManifest, soeREQUIRED},
}))
/* A pseudo-txn alarm signal for invoking a hook, emitted by validators after alarm set conditions are met */
TRANSACTION(ttCRON, 92, Cron, ({
{sfOwner, soeREQUIRED},
+1 -4
View File
@@ -292,9 +292,7 @@ JSS(duration_us); // out: NetworkOPs
JSS(effective); // out: ValidatorList
// in: UNL
JSS(elapsed_seconds);
JSS(enabled); // out: AmendmentTable (on-ledger);
// ServerDefinitions (this server)
JSS(ledger_enabled); // out: ServerDefinitions (on-ledger)
JSS(enabled); // out: AmendmentTable
JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit
JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit
@@ -352,7 +350,6 @@ JSS(hash_mismatches); // out: catalogue
JSS(have_header); // out: InboundLedger
JSS(have_state); // out: InboundLedger
JSS(have_transactions); // out: InboundLedger
JSS(hooks); // in/out: AccountInfo
JSS(high); // out: BookChanges
JSS(highest_sequence); // out: AccountInfo
JSS(highest_ticket); // out: AccountInfo
+1 -7
View File
@@ -300,13 +300,7 @@ BaseHTTPPeer<Handler, Impl>::do_read(yield_context do_yield)
{
complete_ = false;
error_code ec;
// 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));
start_timer();
boost::beast::http::async_read(
impl().stream_, read_buf_, message_, do_yield[ec]);
cancel_timer();
-101
View File
@@ -1,101 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <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
-7
View File
@@ -89,7 +89,6 @@ enum class LedgerNameSpace : std::uint16_t {
ORACLE = LEDGER_NAMESPACE2(0x01, 'R'),
MPTOKEN_ISSUANCE = '~',
MPTOKEN = 't',
MANIFEST = 'M',
CREDENTIAL = LEDGER_NAMESPACE2(0x01, 'D'),
PERMISSIONED_DOMAIN = 'm',
@@ -671,12 +670,6 @@ permissionedDomain(uint256 const& domainID) noexcept
return {ltPERMISSIONED_DOMAIN, domainID};
}
Keylet
manifest(PublicKey const& pk) noexcept
{
return {ltMANIFEST, indexHash(LedgerNameSpace::MANIFEST, pk.slice())};
}
} // namespace keylet
} // namespace ripple
-2
View File
@@ -151,8 +151,6 @@ transResults()
MAKE_ERROR(tefNONDIR_EMIT, "An emitted txn was injected into the ledger without a corresponding directory entry."),
MAKE_ERROR(tefIMPORT_BLACKHOLED, "Cannot import keying because target account is blackholed."),
MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE, "The LedgerFixType field has an invalid value."),
MAKE_ERROR(tefPAST_MANIFEST_SEQ, "The submitted manifest's sequence is not newer than the current."),
MAKE_ERROR(tefREVOKED_MANIFEST, "The submitted manifest is for a revoked master key."),
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
+1 -5
View File
@@ -150,15 +150,12 @@ 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));
@@ -1993,7 +1990,6 @@ 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);
@@ -3645,7 +3641,7 @@ private:
USD(10'000),
false,
0,
ammCrtFee(env).drops() + 2 * env.current()->fees().base.drops(),
ammCrtFee(env).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 ammcreate base fee - 10drops fee
BEAST_EXPECT(accountBalance(env, alice) == "29949999980");
// 10drops fee
BEAST_EXPECT(accountBalance(env, alice) == "29949999990");
},
std::nullopt,
0,
@@ -5081,7 +5081,7 @@ private:
(xrpBalance + drops(5)).getText());
BEAST_EXPECT(
accountBalance(env, alice) ==
std::to_string(29'950'000'000 - 2 * baseFee + 80));
std::to_string(29'950'000'000 - baseFee + 80));
},
std::nullopt,
0,
+83 -28
View File
@@ -88,7 +88,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = fset(account, asfTshCollect);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -110,7 +112,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = acctdelete(account, bene);
// verify hooks fee
testRPCCall(env, tx, "200000");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "200000" : "200000";
testRPCCall(env, tx, feeResult);
}
static uint256
@@ -138,7 +142,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = check::cancel(account, checkId);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -161,7 +167,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = check::cash(dest, checkId, XRP(100));
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -183,7 +191,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = check::create(account, dest, XRP(100));
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -204,7 +214,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = reward::claim(account);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -226,7 +238,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = deposit::auth(account, authed);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -248,7 +262,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = cancel(account, account, seq1);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -270,7 +286,8 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = escrow(account, dest, XRP(10));
// verify hooks fee
std::string const feeResult = "16";
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
@@ -293,7 +310,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = finish(account, account, seq1);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -315,7 +334,9 @@ class BaseFee_test : public beast::unit_test::suite
account, import::loadXpop(ImportTCAccountSet::w_seed));
// verify hooks fee
testRPCCall(env, tx, "106");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "106" : "100";
testRPCCall(env, tx, feeResult);
}
void
@@ -336,7 +357,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = invoke::invoke(account);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "16";
testRPCCall(env, tx, feeResult);
}
void
@@ -358,7 +381,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = offer_cancel(account, offerSeq);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -381,7 +406,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = offer(account, USD(1000), XRP(1000));
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -403,7 +430,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = pay(account, dest, XRP(1));
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
static uint256
@@ -439,7 +468,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = paychan::claim(account, chan, reqBal, authAmt);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -463,7 +494,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = paychan::create(account, dest, XRP(10), settleDelay, pk);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -486,7 +519,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = paychan::fund(account, chan, XRP(1));
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -517,7 +552,9 @@ class BaseFee_test : public beast::unit_test::suite
hookParams[jss::HookParameter][jss::HookParameterValue] = "DEADBEEF";
// verify hooks fee
testRPCCall(env, tx, "73022");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "73022" : "73016";
testRPCCall(env, tx, feeResult);
}
void
@@ -539,7 +576,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = regkey(account, dest);
// verify hooks fee
testRPCCall(env, tx, "0");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "0" : "0";
testRPCCall(env, tx, feeResult);
}
void
@@ -562,7 +601,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = signers(account, 2, {{signer1, 1}, {signer2, 1}});
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -583,7 +624,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = ticket::create(account, 2);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -606,7 +649,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = trust(account, USD(1000));
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -630,7 +675,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = uritoken::burn(issuer, hexid);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -655,7 +702,9 @@ class BaseFee_test : public beast::unit_test::suite
tx[jss::Amount] = "1000000";
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -679,7 +728,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = uritoken::cancel(issuer, hexid);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -706,7 +757,8 @@ class BaseFee_test : public beast::unit_test::suite
tx[jss::Amount] = "1000000";
// verify hooks fee
std::string const feeResult = "16";
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
@@ -729,7 +781,9 @@ class BaseFee_test : public beast::unit_test::suite
auto tx = uritoken::mint(account, uri);
// verify hooks fee
testRPCCall(env, tx, "16");
std::string const feeResult =
env.current()->rules().enabled(fixXahauV1) ? "16" : "10";
testRPCCall(env, tx, feeResult);
}
void
@@ -773,6 +827,7 @@ public:
using namespace test::jtx;
auto const sa = supported_amendments();
testWithFeats(sa);
testWithFeats(sa - fixXahauV1);
}
};
-14
View File
@@ -563,20 +563,6 @@ 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
{
+187 -131
View File
@@ -4422,160 +4422,216 @@ struct Escrow_test : public beast::unit_test::suite
auto const gw = Account{"gateway"};
auto const USD = gw["USD"];
Env env{*this, features};
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(USD(1000000), alice);
env.trust(USD(1000000), bob);
env.close();
env(pay(gw, alice, USD(10000)));
env(pay(gw, bob, USD(10000)));
env.close();
// EscrowCancel - EscrowID
for (bool const withXahauV1 : {true, false})
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
auto const amend = withXahauV1 ? features : features - fixXahauV1;
Env env{*this, amend};
env.fund(XRP(10000), alice, bob, gw);
env.close();
env.trust(USD(1000000), alice);
env.trust(USD(1000000), bob);
env.close();
env(pay(gw, alice, USD(10000)));
env(pay(gw, bob, USD(10000)));
env.close();
// withXahauV1 - no OfferSequence
env(cancel(bob, alice),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
// EscrowCancel - EscrowID
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(!escrowLE);
}
if (withXahauV1)
{
// withXahauV1 - no OfferSequence
env(cancel(bob, alice),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
}
else
{
// !withXahauV1 - OfferSequence == 0
env(cancel(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
}
// EscrowCancel - no EscrowID or OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(!escrowLE);
}
env(cancel(bob, alice), fee(1500), ter(temMALFORMED));
env.close();
// EscrowCancel - no EscrowID or OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
env(cancel(bob, alice), fee(1500), ter(temMALFORMED));
env.close();
// EscrowCancel - EscrowID & OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
auto const seq = env.seq(alice);
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
env(cancel(bob, alice, seq),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
// EscrowCancel - EscrowID & OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
auto const seq = env.seq(alice);
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
env(cancel(bob, alice, seq),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
// EscrowCancel - EscrowID & OfferSequence 0
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
// withXahauV1 - OfferSequence 0 == temMALFORMED
env(cancel(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
// EscrowCancel - EscrowID & OfferSequence 0
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
cancel_time(env.now() + 2s),
fee(1500));
env.close();
// EscrowFinish - EscrowID
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close(5s);
if (withXahauV1)
{
// withXahauV1 - OfferSequence 0 == temMALFORMED
env(cancel(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
else
{
// withXahauV1 - OfferSequence 0 == tesSUCCESS
env(cancel(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(!escrowLE);
}
}
// withXahauV1 - no OfferSequence
env(finish(bob, alice),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
// EscrowFinish - EscrowID
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close(5s);
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(!escrowLE);
}
if (withXahauV1)
{
// withXahauV1 - no OfferSequence
env(finish(bob, alice),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
}
else
{
// !withXahauV1 - OfferSequence == 0
env(finish(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
}
// EscrowFinish - no EscrowID or OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(!escrowLE);
}
env(finish(bob, alice), fee(1500), ter(temMALFORMED));
env.close();
// EscrowFinish - no EscrowID or OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
env(finish(bob, alice), fee(1500), ter(temMALFORMED));
env.close();
// EscrowFinish- EscrowID & OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
auto const seq = env.seq(alice);
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close(5s);
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
env(finish(bob, alice, seq),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
// EscrowFinish- EscrowID & OfferSequence
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
auto const seq = env.seq(alice);
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close(5s);
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
env(finish(bob, alice, seq),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
// EscrowFinish- EscrowID & OfferSequence 0
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close(5s);
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
// withXahauV1 - OfferSequence 0 == temMALFORMED
env(finish(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
// EscrowFinish- EscrowID & OfferSequence 0
{
uint256 const escrowId{getEscrowIndex(alice, env.seq(alice))};
env(escrow(alice, bob, USD(1000)),
finish_time(env.now() + 1s),
fee(1500));
env.close(5s);
if (withXahauV1)
{
// withXahauV1 - OfferSequence 0 == temMALFORMED
env(finish(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(temMALFORMED));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(escrowLE);
}
else
{
// !withXahauV1 - OfferSequence 0 == tesSUCCESS
env(finish(bob, alice, 0),
escrow_id(escrowId),
fee(1500),
ter(tesSUCCESS));
env.close();
auto const escrowLE = env.le(keylet::unchecked(escrowId));
BEAST_EXPECT(!escrowLE);
}
}
}
}
+32 -14
View File
@@ -336,7 +336,10 @@ struct GenesisMint_test : public beast::unit_test::suite
env.close();
// validate emitted txn
validateEmittedTxn(env, "tesSUCCESS", __LINE__);
auto const txResult = env.current()->rules().enabled(fixXahauV1)
? "tesSUCCESS"
: "tecINTERNAL";
validateEmittedTxn(env, txResult, __LINE__);
}
// missing an amount
@@ -566,25 +569,39 @@ struct GenesisMint_test : public beast::unit_test::suite
validateEmittedTxn(env, "tesSUCCESS", __LINE__);
}
auto const amtResult = env.current()->rules().enabled(fixXahauV1)
? 30000000ULL
: 10000000ULL;
// try to include the same destination twice
env(invoke::invoke(
invoker,
env.master,
genesis::makeBlob({
{greg.id(), XRP(10).value(), std::nullopt, std::nullopt},
{greg.id(), XRP(10).value(), std::nullopt, std::nullopt},
})),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
env.close();
{
auto const txResult = env.current()->rules().enabled(fixXahauV1)
? ter(tesSUCCESS)
: ter(tecHOOK_REJECTED);
env(invoke::invoke(
invoker,
env.master,
genesis::makeBlob({
{greg.id(),
XRP(10).value(),
std::nullopt,
std::nullopt},
{greg.id(),
XRP(10).value(),
std::nullopt,
std::nullopt},
})),
fee(XRP(1)),
txResult);
env.close();
env.close();
}
// check
{
auto const le = env.le(keylet::account(greg.id()));
BEAST_EXPECT(
!!le &&
le->getFieldAmount(sfBalance).xrp().drops() == 30000000ULL);
le->getFieldAmount(sfBalance).xrp().drops() == amtResult);
}
// trip the supply cap invariant
@@ -609,7 +626,7 @@ struct GenesisMint_test : public beast::unit_test::suite
auto const le = env.le(keylet::account(greg.id()));
BEAST_EXPECT(
!!le &&
le->getFieldAmount(sfBalance).xrp().drops() == 30000000ULL);
le->getFieldAmount(sfBalance).xrp().drops() == amtResult);
auto const postCoins = env.current()->info().drops;
auto const txnFee =
@@ -681,6 +698,7 @@ public:
using namespace test::jtx;
auto const sa = supported_amendments();
testWithFeats(sa);
testWithFeats(sa - fixXahauV1);
testWithFeats(sa - fixHookAPI20251128);
}
};
+319 -239
View File
@@ -732,56 +732,6 @@ 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
@@ -3991,142 +3941,310 @@ 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(data.second);
auto field_id = data.first;
auto field_object = *strUnHex("10100001");
// Version: 1
// 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 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);
// 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);
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);
}
for (auto const& data : data_list)
{
// use UINT32
auto source_object = _source_object;
auto field_object = *strUnHex(data.second);
auto field_id = data.first;
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 const result =
api.sto_emplace(source_object, field_object, field_id);
BEAST_EXPECTS(result.has_value(), data.second);
auto const r = result.value();
api.sto_emplace(source_object, field_object, sfMemos.getCode());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(
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);
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);
// 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);
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
}
}
@@ -4165,38 +4283,6 @@ 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"}}] }
@@ -4252,25 +4338,6 @@ 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"} }
@@ -4330,17 +4397,34 @@ public:
// { AmountEntry: {Amount: "100"} }
auto const amountEntry = *strUnHex("E05B614000000000000064E1");
for (auto const& data : {memos, amounts, memo, amountEntry})
{
// Valid data
BEAST_EXPECT(api.sto_validate(data).value() == true);
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);
// 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);
}
// 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);
}
void
@@ -4768,7 +4852,6 @@ 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);
@@ -4834,12 +4917,9 @@ 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);
+22 -8
View File
@@ -1663,9 +1663,11 @@ class Import_test : public beast::unit_test::suite
// temMALFORMED - Import: xpop did not contain an 80% quorum for the txn
// it purports to prove.
for (bool const withXahauV1 : {true, false})
{
auto const amend = withXahauV1 ? features : features - fixXahauV1;
test::jtx::Env env{
*this, network::makeNetworkVLConfig(21337, keys), features};
*this, network::makeNetworkVLConfig(21337, keys), amend};
auto const alice = Account("alice");
env.fund(XRP(1000), alice);
@@ -1677,10 +1679,16 @@ class Import_test : public beast::unit_test::suite
"";
valData["n9MTTavZe6EPqqyQ27pJbNWpfHw8ZNspVgznrwx5HWm7cdTjKQie"] =
"";
valData["n94J5LCRu9bBWrJiwRWujvuVECVPWbXFcQ2VN38qLD378F5pDSDM"] =
"";
if (withXahauV1)
{
valData
["n94J5LCRu9bBWrJiwRWujvuVECVPWbXFcQ2VN38qLD378F5pDSDM"] =
"";
}
tmpXpop[jss::validation][jss::data] = valData;
env(import::import(alice, tmpXpop), ter(temMALFORMED));
auto const txResult =
withXahauV1 ? ter(temMALFORMED) : ter(temMALFORMED);
env(import::import(alice, tmpXpop), txResult);
}
test::jtx::Env env{*this, network::makeNetworkVLConfig(21337, keys)};
@@ -5230,6 +5238,7 @@ class Import_test : public beast::unit_test::suite
*this, network::makeNetworkVLConfig(21337, keys)};
auto const feeDrops = env.current()->fees().base;
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
// confirm total coins header
auto const initCoins = env.current()->info().drops;
@@ -5279,17 +5288,18 @@ class Import_test : public beast::unit_test::suite
auto const preAlice = env.balance(alice);
auto const preCoins = env.current()->info().drops;
auto const result = fixV2 ? ter(tesSUCCESS) : ter(tecHOOK_REJECTED);
env(import::import(
alice, import::loadXpop(ImportTCAccountSet::w_seed)),
import::issuer(issuer),
fee(1'000'000),
ter(tesSUCCESS));
result);
env.close();
// fixXahauV2
bool const zeroBurn =
env.current()->rules().enabled(featureZeroB2M);
auto const mintXAH = XRP(zeroBurn ? 0 : 1000);
auto const mintXAH = fixV2 ? XRP(zeroBurn ? 0 : 1000) : XRP(0);
// confirm fee was burned mint / no mint
auto const postAlice = env.balance(alice);
BEAST_EXPECT(postAlice == preAlice - XRP(1) + mintXAH);
@@ -5299,14 +5309,17 @@ class Import_test : public beast::unit_test::suite
BEAST_EXPECT(postCoins == preCoins - XRP(1) + mintXAH);
// resubmit import without issuer
auto const result2 =
fixV2 ? ter(tefPAST_IMPORT_SEQ) : ter(tesSUCCESS);
env(import::import(
alice, import::loadXpop(ImportTCAccountSet::w_seed)),
fee(feeDrops * 10),
ter(tefPAST_IMPORT_SEQ));
result2);
env.close();
// total burn
auto const totalBurn = XRP(0);
auto const totalBurn =
fixV2 ? XRP(0) : XRP(zeroBurn ? 0 : 1000) - (feeDrops * 10);
// confirm fee was minted / not minted
auto const postAlice2 = env.balance(alice);
@@ -6263,6 +6276,7 @@ public:
{
using namespace test::jtx;
FeatureBitset const all{supported_amendments()};
testWithFeats(all - fixXahauV2);
testWithFeats(all - featureZeroB2M);
testWithFeats(all);
}
+4 -4
View File
@@ -104,8 +104,8 @@ class LedgerMaster_test : public beast::unit_test::suite
startLegSeq, txnIndex);
BEAST_EXPECT(
*result ==
uint256("DCAECE52F028B9D0F7CA43CBE15AB4EF7B84A8EF5D455238992E1E"
"DF2BDA1637"));
uint256("0CC11AD1AD89661689F6B6148D82CB6A7101DA4D66EC843670262D"
"0B618F5745"));
}
// success (second tx)
{
@@ -114,8 +114,8 @@ class LedgerMaster_test : public beast::unit_test::suite
startLegSeq + 1, txnIndex);
BEAST_EXPECT(
*result ==
uint256("722BD5EC9E4AE36E724B6223F5A5887927AADFD0B18A3053AAE57F"
"434F8C9FE8"));
uint256("DCAECE52F028B9D0F7CA43CBE15AB4EF7B84A8EF5D455238992E1E"
"DF2BDA1637"));
}
}
+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[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
meta[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
[sfOutstandingAmount.fieldName] == "9990");
// Destination account got 9'990
BEAST_EXPECT(
meta[3u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
meta[0u][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 ==
"61041108F0DAD50BAFD2F7B1102AC70B283EBDCED194CEBAF04D0184E4A70B"
"02",
"42175D43E3D236A86B0F1B07182A1B2C78AD382372CFA09A48239C395FFFEE"
"30",
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 == "00000002AE123A8556F3CF91154711376AFB0F894F832B3D", id);
id == "00000001AE123A8556F3CF91154711376AFB0F894F832B3D", id);
}
void
+42 -12
View File
@@ -5105,8 +5105,10 @@ public:
using namespace jtx;
// OfferCreate
for (bool const withXahauV1 : {true, false})
{
Env env{*this, features};
auto const amend = withXahauV1 ? features : features - fixXahauV1;
Env env{*this, amend};
auto const gw = Account{"gateway"};
auto const alice = Account{"alice"};
auto const USD = gw["USD"];
@@ -5190,8 +5192,10 @@ public:
}
// OfferCancel
for (bool const withXahauV1 : {true, false})
{
Env env{*this, features};
auto const amend = withXahauV1 ? features : features - fixXahauV1;
Env env{*this, amend};
auto const gw = Account{"gateway"};
auto const alice = Account{"alice"};
auto const USD = gw["USD"];
@@ -5223,10 +5227,24 @@ public:
env(offer(alice, XRP(50), USD(50)));
env.close();
env(offer_cancel(alice), offer_id(offerId), ter(tesSUCCESS));
env.close();
auto const offerLE = env.le(keylet::unchecked(offerId));
BEAST_EXPECT(!offerLE);
if (withXahauV1)
{
env(offer_cancel(alice),
offer_id(offerId),
ter(tesSUCCESS));
env.close();
auto const offerLE = env.le(keylet::unchecked(offerId));
BEAST_EXPECT(!offerLE);
}
else
{
env(offer_cancel(alice),
offer_id(offerId),
ter(temBAD_SEQUENCE));
env.close();
auto const offerLE = env.le(keylet::unchecked(offerId));
BEAST_EXPECT(offerLE);
}
}
// no offer id or offer sequence
@@ -5248,12 +5266,24 @@ public:
env(offer(alice, XRP(50), USD(50)));
env.close();
env(offer_cancel(alice, offerSeqId),
offer_id(offerId),
ter(temBAD_SEQUENCE));
env.close();
auto const offerLE = env.le(keylet::unchecked(offerId));
BEAST_EXPECT(offerLE);
if (withXahauV1)
{
env(offer_cancel(alice, offerSeqId),
offer_id(offerId),
ter(temBAD_SEQUENCE));
env.close();
auto const offerLE = env.le(keylet::unchecked(offerId));
BEAST_EXPECT(offerLE);
}
else
{
env(offer_cancel(alice, offerSeqId),
offer_id(offerId),
ter(tesSUCCESS));
env.close();
auto const offerLE = env.le(keylet::unchecked(offerId));
BEAST_EXPECT(!offerLE);
}
}
// both offer id and offer sequence 0
+309 -219
View File
@@ -30,10 +30,7 @@
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/hook/Enum.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/PayChan.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/jss.h>
#include <vector>
@@ -740,7 +737,15 @@ private:
{
auto const executions = meta[sfHookExecutions.jsonName];
auto const execution = executions[0u][sfHookExecution.jsonName];
BEAST_EXPECT(execution[sfFlags.jsonName] == expected);
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
if (fixV2)
{
BEAST_EXPECT(execution[sfFlags.jsonName] == expected);
}
else
{
BEAST_REQUIRE(!execution[sfFlags.jsonName]);
}
}
void
@@ -2261,10 +2266,16 @@ private:
setTSHHook(env, account, testStrong);
// cancel escrow
env(cancel(account, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
Json::Value tx;
if (!env.current()->rules().enabled(fixXahauV1))
{
tx = cancel(account, account, 0);
}
else
{
tx = cancel(account, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -2309,10 +2320,16 @@ private:
setTSHHook(env, dest, testStrong);
// cancel escrow
env(cancel(account, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
Json::Value tx;
if (!env.current()->rules().enabled(fixXahauV1))
{
tx = cancel(account, account, 0);
}
else
{
tx = cancel(account, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -2361,10 +2378,16 @@ private:
setTSHHook(env, dest, testStrong);
// cancel escrow
env(cancel(dest, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
Json::Value tx;
if (!env.current()->rules().enabled(fixXahauV1))
{
tx = cancel(dest, account, 0);
}
else
{
tx = cancel(dest, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -2406,14 +2429,23 @@ private:
setTSHHook(env, account, testStrong);
// cancel escrow
env(cancel(dest, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
Json::Value tx;
if (!fixV1)
{
tx = cancel(dest, account, 0);
}
else
{
tx = cancel(dest, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
auto const expected = testStrong ? tshSTRONG : tshSTRONG;
auto const expected =
(fixV1 ? (testStrong ? tshSTRONG : tshSTRONG)
: (testStrong ? tshNONE : tshNONE));
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -2462,10 +2494,16 @@ private:
setTSHHook(env, gw, testStrong);
// cancel escrow
env(cancel(account, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
Json::Value tx;
if (!env.current()->rules().enabled(fixXahauV1))
{
tx = cancel(account, account, 0);
}
else
{
tx = cancel(account, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -2881,10 +2919,16 @@ private:
setTSHHook(env, account, testStrong);
// finish escrow
env(finish(account, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
Json::Value tx;
if (!env.current()->rules().enabled(fixXahauV1))
{
tx = finish(account, account, 0);
}
else
{
tx = finish(account, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -2923,14 +2967,23 @@ private:
setTSHHook(env, dest, testStrong);
// finish escrow
env(finish(account, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
Json::Value tx;
if (!fixV1)
{
tx = finish(account, account, 0);
}
else
{
tx = finish(account, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
auto const expected = testStrong ? tshSTRONG : tshSTRONG;
auto const expected =
(fixV1 ? (testStrong ? tshSTRONG : tshSTRONG)
: (testStrong ? tshNONE : tshNONE));
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -2966,10 +3019,16 @@ private:
setTSHHook(env, dest, testStrong);
// finish escrow
env(finish(dest, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
Json::Value tx;
if (!env.current()->rules().enabled(fixXahauV1))
{
tx = finish(dest, account, 0);
}
else
{
tx = finish(dest, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -3008,14 +3067,23 @@ private:
setTSHHook(env, account, testStrong);
// finish escrow
env(finish(dest, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
Json::Value tx;
if (!fixV1)
{
tx = finish(dest, account, 0);
}
else
{
tx = finish(dest, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
auto const expected = testStrong ? tshSTRONG : tshSTRONG;
auto const expected =
(fixV1 ? (testStrong ? tshSTRONG : tshSTRONG)
: (testStrong ? tshNONE : tshNONE));
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -3061,10 +3129,17 @@ private:
setTSHHook(env, gw, testStrong);
// finish escrow
env(finish(dest, account),
escrow_id(escrowId),
fee(XRP(1)),
ter(tesSUCCESS));
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
Json::Value tx;
if (!fixV1)
{
tx = finish(dest, account, 0);
}
else
{
tx = finish(dest, account);
}
env(tx, escrow_id(escrowId), fee(XRP(1)), ter(tesSUCCESS));
env.close();
// verify tsh hook triggered
@@ -3370,7 +3445,10 @@ private:
env.close();
// verify tsh hook triggered
auto const expected = testStrong ? tshNONE : tshWEAK;
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
auto const expected =
(fixV2 ? (testStrong ? tshNONE : tshWEAK)
: (testStrong ? tshSTRONG : tshSTRONG));
testTSHStrongWeak(env, expected, __LINE__);
}
}
@@ -6229,7 +6307,10 @@ private:
env.close();
// verify tsh hook triggered
auto const expected = testStrong ? tshSTRONG : tshSTRONG;
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
auto const expected =
(fixV2 ? (testStrong ? tshSTRONG : tshSTRONG)
: (testStrong ? tshNONE : tshNONE));
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -6310,7 +6391,10 @@ private:
env.close();
// verify tsh hook triggered
auto const expected = testStrong ? tshSTRONG : tshSTRONG;
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
auto const expected =
(fixV2 ? (testStrong ? tshSTRONG : tshSTRONG)
: (testStrong ? tshNONE : tshNONE));
testTSHStrongWeak(env, expected, __LINE__);
}
}
@@ -6416,12 +6500,15 @@ private:
env.close();
// verify tsh hook triggered
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
bool const withIOUIssuerWeakTSH =
env.current()->rules().enabled(featureIOUIssuerWeakTSH);
auto const expected = testStrong
? tshNONE
: (withIOUIssuerWeakTSH ? tshWEAK : tshNONE);
auto const expected =
(fixV1
? (testStrong ? tshNONE
: (withIOUIssuerWeakTSH ? tshWEAK : tshNONE))
: (testStrong ? tshSTRONG : tshSTRONG));
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -6520,11 +6607,14 @@ private:
env.close();
// verify tsh hook triggered
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
bool const withIOUIssuerWeakTSH =
env.current()->rules().enabled(featureIOUIssuerWeakTSH);
auto const expected = testStrong
? tshNONE
: (withIOUIssuerWeakTSH ? tshWEAK : tshNONE);
auto const expected =
(fixV1
? (testStrong ? tshNONE
: (withIOUIssuerWeakTSH ? tshWEAK : tshNONE))
: (testStrong ? tshSTRONG : tshSTRONG));
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -6574,12 +6664,15 @@ private:
env.close();
// verify tsh hook triggered
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
bool const withIOUIssuerWeakTSH =
env.current()->rules().enabled(featureIOUIssuerWeakTSH);
auto const expected = testStrong
? tshNONE
: (withIOUIssuerWeakTSH ? tshWEAK : tshNONE);
auto const expected =
(fixV1
? (testStrong ? tshNONE
: (withIOUIssuerWeakTSH ? tshWEAK : tshNONE))
: (testStrong ? tshSTRONG : tshSTRONG));
testTSHStrongWeak(env, expected, __LINE__);
}
@@ -6940,6 +7033,130 @@ private:
testTSHStrongWeak(env, tshNONE, __LINE__);
}
}
// A broker is an explicitly discovered weak TSH. This is required for
// XAH sales because native balance changes are not found by the generic
// balance-based weak-TSH discovery.
if (features[featureURITokenBroker] && features[fixXahauV1])
{
for (bool const collectEnabled : {false, true})
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(
21337, "10", "1000000", "200000"),
features};
auto const issuer = Account("alice");
auto const owner = Account("bob");
auto const buyer = Account("carol");
auto const broker = Account("dave");
env.fund(XRP(1000), issuer, owner, buyer, broker);
env.close();
std::string const uri =
collectEnabled ? "broker-collect" : "broker-no-collect";
auto const tid = uritoken::tokenid(issuer, uri);
std::string const hexid{strHex(tid)};
env(uritoken::mint(issuer, uri),
uritoken::dest(owner),
uritoken::amt(XRP(1)),
ter(tesSUCCESS));
env.close();
env(uritoken::buy(owner, hexid),
uritoken::amt(XRP(1)),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
env(uritoken::sell(owner, hexid),
uritoken::dest(buyer),
uritoken::amt(XRP(1)),
ter(tesSUCCESS));
env.close();
if (collectEnabled)
addWeakTSH(env, broker);
setTSHHook(env, broker, false);
auto buy = uritoken::buy(buyer, hexid);
buy[sfAmount.jsonName] =
XRP(2).value().getJson(JsonOptions::none);
buy[sfBrokerAccount.jsonName] = broker.human();
env(buy, fee(XRP(1)), ter(tesSUCCESS));
env.close();
testTSHStrongWeak(
env, collectEnabled ? tshWEAK : tshNONE, __LINE__);
}
}
// If the token issuer is also the broker, TSH strengths are ORed and
// the account executes exactly once: weak for a non-burnable token and
// strong for a burnable token.
if (features[featureURITokenBroker] && features[fixXahauV1])
{
for (bool const burnable : {false, true})
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(
21337, "10", "1000000", "200000"),
features};
auto const issuer = Account("alice");
auto const owner = Account("bob");
auto const buyer = Account("carol");
env.fund(XRP(1000), issuer, owner, buyer);
env.close();
std::string const uri =
burnable ? "broker-issuer-burnable" : "broker-issuer";
auto const tid = uritoken::tokenid(issuer, uri);
std::string const hexid{strHex(tid)};
env(uritoken::mint(issuer, uri),
uritoken::dest(owner),
uritoken::amt(XRP(1)),
txflags(burnable ? tfBurnable : 0),
ter(tesSUCCESS));
env.close();
env(uritoken::buy(owner, hexid),
uritoken::amt(XRP(1)),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
env(uritoken::sell(owner, hexid),
uritoken::dest(buyer),
uritoken::amt(XRP(1)),
ter(tesSUCCESS));
env.close();
if (!burnable)
addWeakTSH(env, issuer);
setTSHHook(env, issuer, burnable);
auto buy = uritoken::buy(buyer, hexid);
buy[sfAmount.jsonName] =
XRP(2).value().getJson(JsonOptions::none);
buy[sfBrokerAccount.jsonName] = issuer.human();
env(buy, fee(XRP(1)), ter(tesSUCCESS));
env.close();
testTSHStrongWeak(
env, burnable ? tshSTRONG : tshWEAK, __LINE__);
Json::Value params;
params[jss::transaction] =
env.tx()->getJson(JsonOptions::none)[jss::hash];
auto const result = env.rpc("json", "tx", to_string(params));
auto const executions =
result[jss::result][jss::meta][sfHookExecutions.jsonName];
BEAST_EXPECT(executions.size() == 1);
}
}
}
void
@@ -7639,6 +7856,7 @@ private:
env.close();
auto const preDest = env.balance(dest);
bool const withFix = env.current()->rules().enabled(fixXahauV2);
env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
auto const tx =
@@ -7646,15 +7864,24 @@ private:
auto result =
ripple::apply(env.app(), view, *tx, tapNONE, env.journal);
BEAST_EXPECT(result.ter == tefNONDIR_EMIT);
BEAST_EXPECT(!result.applied);
bool const applyResult = withFix ? false : true;
if (withFix)
{
BEAST_EXPECT(result.ter == tefNONDIR_EMIT);
}
else
{
BEAST_EXPECT(result.ter == tesSUCCESS);
}
BEAST_EXPECT(result.applied == applyResult);
return result.applied;
});
env.close();
auto const postDest = env.balance(dest);
BEAST_EXPECT(postDest == preDest);
auto const postValue = withFix ? XRP(0) : XRP(1);
BEAST_EXPECT(postDest == preDest + postValue);
for (size_t i = 0; i < 4; i++)
{
@@ -7665,7 +7892,8 @@ private:
}
auto const postDest1 = env.balance(dest);
BEAST_EXPECT(postDest1 == postDest);
auto const postValue1 = withFix ? XRP(0) : XRP(2);
BEAST_EXPECT(postDest1 == postDest + postValue1);
}
void
@@ -8279,16 +8507,16 @@ private:
// validate the emitted txn ids
std::vector<std::string> const txIds = {
"2FBEF981BAC322225D13C274D0118FECC1B93A89EDBC0AB3CBD8B30A1591024E",
"3C0A12F6A322486ABDA4180DC2B424BD252FD2FA6A33553B0B5D1526C020C475",
"AFFD55D757136FC0BE665F16E0D8CFBDC0D37CF85B5D3329BE1DBD3A44BB281C",
"BBE43C01068CE511DBF24F84A928E481451FF448AF2493E1A9BE099628D1F523",
"45970753F7B52448C23D66E5E19DBBC65F5BE6055C04AC3BA9896D7D67891DB1",
"97F4E09B2AEF4D798D378B1390B0624A68F304247B358334167E93217D502A16",
"770D3B786C8E3BEF560D9589B02557B0BDFC1C7FD15C6CF42E86068A6A411EFC",
"239DFA86EE8391418170D7FDF5AA8B8EEE0BBEF5E9F928BC29C23EEF6F1858AD",
"9FAA93AC6340C87B5C722FF0E8D4D3F39B9A0CA5E9799EC4CE316BFD5E379540",
"8672B2D8AA5E9EF86929B8662FE436541B2F739052732020B874BC304601C4DB",
"9610F73CDD6590EB6B3C82E5EC55D4B4C80CD7128B98AA556F7EC9DD96AE7056",
"2F4582A29272390C0C25A80D4A3BCE5A14ACE6D86D8D0CB2C57719EB6FA881AE",
"89A301CFEF0DD781AB9032A6A2DCE0937BC0119D2CDD06033B8B2FD80968E519",
"DD8721B59024E168480B4DF8F8E93778601F0BD2E77FC991F3DA1182F5AD8B1E",
"5D735C2EE3CB8289F8E11621FDC9565F9D6D67F3AE59D65332EACE591D67945F",
"F02470E01731C968881AF4CBDEC90BB9E1F7AB0BE1CC22AF15451FB6D191096D",
"8AD65E541DECD49B1693F8C17DFD8A2B906F49C673C4FD2034FF772E2BE50C30",
"9F225229059CCC6257814D03C107884CF588C1C246A89ADFC16E50DF671B834C",
"13C2A54A14BADF3648CED05175E1CCAD713F7E5EA56D9735CF8813CD5551F281",
"87C60F41A96554587CED289F83F52DEE3CF670EEB189B067E6066B9A06056ADF",
};
Json::Value params;
params[jss::transaction] =
@@ -8341,150 +8569,6 @@ private:
}
}
// Builds a manifest signed by `master`, nominating `ephemeral` as the
// signing key. A sequence of UINT32_MAX makes it a revocation, which by
// definition carries no signing key.
static std::string
makeManifestString(
jtx::Account const& master,
jtx::Account const& ephemeral,
std::uint32_t seq)
{
STObject st(sfGeneric);
st[sfSequence] = seq;
st[sfPublicKey] = master.pk();
if (seq != std::numeric_limits<std::uint32_t>::max())
{
st[sfSigningPubKey] = ephemeral.pk();
sign(
st,
HashPrefix::manifest,
*publicKeyType(ephemeral.pk()),
ephemeral.sk());
}
sign(
st,
HashPrefix::manifest,
*publicKeyType(master.pk()),
master.sk(),
sfMasterSignature);
Serializer s;
st.add(s);
return std::string(static_cast<char const*>(s.data()), s.size());
}
// A manifest transaction carries no account signature, so it cannot be
// submitted through env() the way a signed transaction can. Returns the
// resulting transaction id so the caller can inspect its metadata.
uint256
submitManifest(jtx::Env& env, std::string const& manifest)
{
Json::Value params;
params[jss::manifest] = strHex(manifest);
auto const jrr = env.rpc("json", "submit", to_string(params));
auto const& result = jrr[jss::result];
if (!BEAST_EXPECT(
result[jss::engine_result].asString() == "tesSUCCESS"))
{
log << "submitManifest: " << to_string(jrr) << std::endl;
return beast::zero;
}
// An error response carries no tx_json, and strUnHex("") yields an
// engaged but empty Blob, so fromVoid() would memcpy from nullptr.
auto const blob = strUnHex(result[jss::tx_json][jss::hash].asString());
auto const hash =
blob ? uint256::fromVoidChecked(*blob) : std::optional<uint256>{};
if (!BEAST_EXPECT(hash.has_value()))
{
log << "submitManifest: " << to_string(jrr) << std::endl;
return beast::zero;
}
return *hash;
}
// SetManifest
// | otxn | tsh | manifest |
// | M | M | N/A |
// | M | E | W | ephemeral key's logical account
void
testSetManifestTSH(FeatureBitset features)
{
using namespace test::jtx;
using namespace std::literals;
testcase("set manifest TSH");
if (!features[featureOnChainManifests])
return;
// otxn: master
// tsh: ephemeral
// w/s: weak
//
// The ephemeral account is only named by the manifest, so it may
// observe the transaction but not rollback it. It therefore fires only
// when it has asked to collect.
for (bool const testStrong : {true, false})
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
features};
auto const master = Account("master", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master, ephemeral);
env.close();
if (!testStrong)
addWeakTSH(env, ephemeral);
setTSHHook(env, ephemeral, testStrong);
auto const txHash =
submitManifest(env, makeManifestString(master, ephemeral, 1));
env.close();
// A strong hook on a weak stake holder is never reached.
auto const expected = testStrong ? tshNONE : tshWEAK;
testTSHStrongWeak(env, txHash, expected, __LINE__);
}
// A revocation names no signing key, so there is no ephemeral stake
// holder to notify at all.
{
test::jtx::Env env{
*this,
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
features};
auto const master = Account("master", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master, ephemeral);
env.close();
addWeakTSH(env, ephemeral);
setTSHHook(env, ephemeral, false);
auto const txHash = submitManifest(
env,
makeManifestString(
master,
ephemeral,
std::numeric_limits<std::uint32_t>::max()));
env.close();
testTSHStrongWeak(env, txHash, tshNONE, __LINE__);
}
}
void
testTSH(FeatureBitset features)
{
@@ -8514,8 +8598,10 @@ public:
using namespace test::jtx;
static FeatureBitset const all{supported_amendments()};
static std::array<FeatureBitset, 2> const feats{
static std::array<FeatureBitset, 4> const feats{
all,
all - fixXahauV1 - fixXahauV2 - featureIOUIssuerWeakTSH,
all - fixXahauV2 - featureIOUIssuerWeakTSH,
all - featureIOUIssuerWeakTSH,
};
@@ -8545,10 +8631,14 @@ public:
} \
};
SETHOOKTSH_TEST(1, true)
SETHOOKTSH_TEST(1, false)
SETHOOKTSH_TEST(2, false)
SETHOOKTSH_TEST(3, true)
BEAST_DEFINE_TESTSUITE_PRIO(SetHookTSH0, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHookTSH1, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHookTSH2, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHookTSH3, app, ripple, 2);
} // namespace test
} // namespace ripple
+127 -556
View File
@@ -294,7 +294,9 @@ public:
iv[jss::CreateCode] = "";
jv[jss::Hooks][0U][jss::Hook] = iv;
env(jv, HSFEE, ter(tesSUCCESS));
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
auto const txResult = fixV1 ? ter(tesSUCCESS) : ter(tefBAD_LEDGER);
env(jv, HSFEE, txResult);
env.close();
}
@@ -1355,16 +1357,11 @@ 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_EXPECTS(
openLedgerFee == expected,
"openLedgerFee: " + openLedgerFee.asString() +
", expected: " + expected);
BEAST_EXPECT(openLedgerFee == expected);
// verify hooks fee
auto const hooksFee = jrr[jss::result][jss::fee_hooks_feeunits];
BEAST_EXPECTS(
hooksFee == expected,
"hooksFee: " + hooksFee.asString() + ", expected: " + expected);
BEAST_EXPECT(hooksFee == expected);
}
void
@@ -1374,8 +1371,7 @@ public:
using namespace jtx;
Env env{*this, features};
bool const hookOnV2 = env.current()->rules().enabled(featureHookOnV2) ||
env.current()->rules().enabled(featureHookOnV2_1);
bool const hookOnV2 = env.current()->rules().enabled(featureHookOnV2);
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
@@ -1538,10 +1534,12 @@ public:
jv.removeMember(jss::HookOn);
jv[jss::HookOnIncoming] =
"0000000000000000000000000000000000000000000000000000000000"
"000000";
"0000"
"00";
jv[jss::HookOnOutgoing] =
"0000000000000000000000000000000000000000000000000000000000"
"000001";
"0000"
"01";
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Execution: Install"),
HSFEE);
@@ -1606,10 +1604,12 @@ public:
jv.removeMember(jss::HookOn);
jv[jss::HookOnIncoming] =
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
"bfffff"; // Invoke high
"bfff"
"ff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bffffe"; // Payment high
"bfff"
"fe"; // Payment high
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Execution: Install"),
HSFEE);
@@ -1620,7 +1620,8 @@ public:
jv[jss::Flags] = hsfOVERRIDE;
jv[jss::HookOn] =
"0000000000000000000000000000000000000000000000000000000000"
"000000";
"0000"
"00";
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
M("Execution: Install"),
HSFEE);
@@ -1654,285 +1655,18 @@ 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"
"bfffff"; // Invoke high
"bfff"
"ff"; // Invoke high
jv[jss::HookOnOutgoing] =
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
"bffffe"; // Payment high
"bfff"
"fe"; // Payment high
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
env.close();
@@ -2268,64 +2002,11 @@ public:
BEAST_EXPECT(!env.meta()->isFieldPresent(sfHookEmissions));
}
// 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)
// Call named hook with the wrong hook name
{
auto jv = invoke::invoke(alice);
jv[jss::HookName] = "41424345";
env(jv,
M("Call named hook with the wrong hook name (size > 0)"),
HSFEE);
env(jv, M("Call named hook with the wrong hook name"), HSFEE);
env.close();
// execute only non-named hook
BEAST_EXPECT(!env.meta()->isFieldPresent(sfHookEmissions));
@@ -3324,37 +3005,25 @@ 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(shCtx, hso) == hsoNOOP);
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoNOOP);
}
// hsoCREATE
{
STObject hso{sfHook};
hso.setFieldVL(sfCreateCode, {1}); // non-empty create code
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoCREATE);
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoCREATE);
}
// hsoDELETE
{
STObject hso{sfHook};
hso.setFieldVL(sfCreateCode, ripple::Blob{}); // empty create code
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoDELETE);
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoDELETE);
}
// hsoINSTALL
@@ -3362,7 +3031,7 @@ public:
STObject hso{sfHook};
hso.setFieldH256(
sfHookHash, uint256{beast::zero}); // all zeros hook hash
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoINSTALL);
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoINSTALL);
}
// hsoNSDELETE
@@ -3371,14 +3040,14 @@ public:
hso.setFieldH256(
sfHookNamespace, uint256{beast::zero}); // all zeros hook hash
hso.setFieldU32(sfFlags, hsfNSDELETE);
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoNSDELETE);
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoNSDELETE);
}
// hsoUPDATE
{
STObject hso{sfHook};
hso.setFieldH256(sfHookOn, UINT256_BIT[0]);
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoUPDATE);
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoUPDATE);
}
// hsoINVALID
@@ -3387,39 +3056,7 @@ public:
hso.setFieldVL(sfCreateCode, {1}); // non-empty create code
hso.setFieldH256(
sfHookHash, uint256{beast::zero}); // all zeros hook hash
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));
}
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoINVALID);
}
}
@@ -3491,79 +3128,6 @@ 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)
{
@@ -4139,6 +3703,8 @@ public:
env(invoke, M("test emit"), fee(XRP(1)));
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
std::optional<uint256> emithash;
{
auto meta = env.meta(); // meta can close
@@ -4148,7 +3714,9 @@ public:
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookEmissions = meta->getFieldArray(sfHookEmissions);
BEAST_EXPECT(hookEmissions[0u].isFieldPresent(sfEmitNonce));
BEAST_EXPECT(
hookEmissions[0u].isFieldPresent(sfEmitNonce) == fixV2 ? true
: false);
BEAST_EXPECT(
hookEmissions[0u].getAccountID(sfHookAccount) == alice.id());
@@ -4243,7 +3811,8 @@ public:
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
BEAST_EXPECT(
hookExecutions[0].getFieldU16(sfHookEmitCount) == 2);
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
if (fixV2)
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
}
env.close();
burden_expected *= 2U;
@@ -4267,7 +3836,8 @@ public:
BEAST_EXPECT(
hookExecutions[0].getFieldU64(sfHookReturnCode) ==
283); // emission failure on first emit
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
if (fixV2)
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
}
BEAST_EXPECT(txcount == 256);
}
@@ -4524,6 +4094,8 @@ public:
env(invoke, M("test emit"), fee(XRP(1)));
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
std::optional<uint256> emithash;
{
auto meta = env.meta(); // meta can close
@@ -4533,7 +4105,9 @@ public:
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
auto const hookEmissions = meta->getFieldArray(sfHookEmissions);
BEAST_EXPECT(hookEmissions[0u].isFieldPresent(sfEmitNonce));
BEAST_EXPECT(
hookEmissions[0u].isFieldPresent(sfEmitNonce) == fixV2 ? true
: false);
BEAST_EXPECT(
hookEmissions[0u].getAccountID(sfHookAccount) == alice.id());
@@ -4628,7 +4202,8 @@ public:
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
BEAST_EXPECT(
hookExecutions[0].getFieldU16(sfHookEmitCount) == 2);
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
if (fixV2)
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
}
env.close();
burden_expected *= 2U;
@@ -4652,7 +4227,8 @@ public:
BEAST_EXPECT(
hookExecutions[0].getFieldU64(sfHookReturnCode) ==
172); // emission failure on first emit
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
if (fixV2)
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 2);
}
BEAST_EXPECT(txcount == 256);
}
@@ -4715,10 +4291,10 @@ public:
uint8_t expected1[49] = {
0xEDU, 0x20U, 0x2EU, 0x00U, 0x00U, 0x00U, 0x01U, 0x3DU, 0x00U, 0x00U,
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
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,
};
// 0x5CU
// EmitNonce 32bytes
@@ -7389,8 +6965,12 @@ public:
BEAST_REQUIRE(hookExecutions.size() == 2);
// get the data in the return code of the execution
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 5);
BEAST_EXPECT(hookExecutions[1].getFieldU32(sfFlags) == 0);
bool const fixV2 = env.current()->rules().enabled(fixXahauV2);
if (fixV2)
{
BEAST_EXPECT(hookExecutions[0].getFieldU32(sfFlags) == 5);
BEAST_EXPECT(hookExecutions[1].getFieldU32(sfFlags) == 0);
}
BEAST_EXPECT(hookExecutions[0].getFieldU64(sfHookReturnCode) == 0);
BEAST_EXPECT(hookExecutions[1].getFieldU64(sfHookReturnCode) == 1);
@@ -10463,13 +10043,19 @@ public:
fee(XRP(1)));
}
// fixXahauV1
bool const fixV1 = env.current()->rules().enabled(fixXahauV1);
auto const txResult = fixV1 ? ter(tecHOOK_REJECTED) : ter(tesSUCCESS);
env(pay(bob, alice, XRP(1)),
M("test state_foreign_set_max"),
fee(XRP(1)),
ter(tecHOOK_REJECTED));
ter(txResult));
env.close();
// verify hook result
// TOO_MANY_NAMESPACES / -45
std::string const hookResult = fixV1 ? "800000000000002d" : "9";
Json::Value params;
params[jss::transaction] =
env.tx()->getJson(JsonOptions::none)[jss::hash];
@@ -10477,9 +10063,7 @@ public:
auto const meta = jrr[jss::result][jss::meta];
auto const executions = meta[sfHookExecutions.jsonName];
auto const execution = executions[0u][sfHookExecution.jsonName];
// TOO_MANY_NAMESPACES / -45
BEAST_EXPECT(
execution[sfHookReturnCode.jsonName] == "800000000000002d");
BEAST_EXPECT(execution[sfHookReturnCode.jsonName] == hookResult);
}
void
@@ -12493,76 +12077,57 @@ public:
)[test.hook]"];
HASH_WASM(hook);
for (bool const with20251128 : {true, false})
for (auto isfixHookAPI20251128 : {true, false})
{
for (bool const withSType : {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)
{
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;
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));
}
auto const position = 1;
auto const length = 33;
BEAST_REQUIRE(
returnCode ==
(parseError + ((int64_t)position << 32) + length));
}
}
}
@@ -15532,8 +15097,7 @@ public:
testNSDeletePartial(features);
testPageCap(features);
testHookOnV2((features | featureHookOnV2) - featureHookOnV2_1);
testHookOnV2((features | featureHookOnV2_1) - featureHookOnV2);
testHookOnV2(features);
testHookName(features);
testFillCopy(features);
@@ -15541,7 +15105,6 @@ public:
testWasm(features);
test_accept(features);
test_rollback(features);
test_exit_out_of_bounds(features);
testGuards(features);
@@ -15639,12 +15202,16 @@ public:
using namespace test::jtx;
static FeatureBitset const all{supported_amendments()};
static std::array<FeatureBitset, 6> const feats{
static std::array<FeatureBitset, 8> const feats{
all,
all - fixNSDelete,
all - fixNSDelete - fixPageCap,
all - fixNSDelete - fixPageCap - featureHookCanEmit,
all - fixNSDelete - fixPageCap - featureExtendedHookState,
all - fixXahauV2,
all - fixXahauV1 - fixXahauV2,
all - fixXahauV1 - fixXahauV2 - fixNSDelete,
all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap,
all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap -
featureHookCanEmit,
all - fixXahauV1 - fixXahauV2 - fixNSDelete - fixPageCap -
featureExtendedHookState,
all - featureNamedHooks,
};
@@ -15905,7 +15472,9 @@ SETHOOK_TEST(1, false)
SETHOOK_TEST(2, false)
SETHOOK_TEST(3, false)
SETHOOK_TEST(4, false)
SETHOOK_TEST(5, true)
SETHOOK_TEST(5, false)
SETHOOK_TEST(6, false)
SETHOOK_TEST(7, true)
BEAST_DEFINE_TESTSUITE_PRIO(SetHook0, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook1, app, ripple, 2);
@@ -15913,6 +15482,8 @@ BEAST_DEFINE_TESTSUITE_PRIO(SetHook2, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook3, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook4, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook5, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook6, app, ripple, 2);
BEAST_DEFINE_TESTSUITE_PRIO(SetHook7, app, ripple, 2);
} // namespace test
} // namespace ripple
#undef M
+109 -66
View File
@@ -9,6 +9,7 @@
namespace ripple {
namespace test {
std::map<std::string, std::vector<uint8_t>> wasm = {
/* ==== WASM: 0 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -189,64 +190,7 @@ 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,
}},
/* ==== WASM: 1 ==== */
{R"[test.hook](
(module
(type (;0;) (func (param i32 i32) (result i64)))
@@ -296,6 +240,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x0BU,
}},
/* ==== WASM: 2 ==== */
{R"[test.hook](
(module
(type (;0;) (func (param i32 i32) (result i64)))
@@ -349,6 +294,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x14U, 0x10U, 0x00U, 0x1AU, 0x0CU, 0x00U, 0x0BU, 0x0BU,
}},
/* ==== WASM: 3 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -415,6 +361,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x80U, 0x80U, 0x00U, 0x20U, 0x02U, 0x0BU,
}},
/* ==== WASM: 4 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -520,6 +467,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x6AU, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x20U, 0x03U, 0x0BU,
}},
/* ==== WASM: 5 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -594,6 +542,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x6AU, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x20U, 0x05U, 0x0BU,
}},
/* ==== WASM: 6 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
@@ -1210,6 +1159,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x78U, 0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 7 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
@@ -1638,6 +1588,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 8 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -1671,10 +1622,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, 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
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,
};
// 0x5CU
// EmitNonce 32bytes
@@ -1821,10 +1772,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,
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,
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,
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,
@@ -1843,6 +1794,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x3DU, 0x20U, 0x30U, 0x78U, 0x45U, 0x31U, 0x00U,
}},
/* ==== WASM: 9 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -1964,6 +1916,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x58U, 0x4EU, 0x00U,
}},
/* ==== WASM: 10 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -2096,6 +2049,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x4FU, 0x4EU, 0x43U, 0x45U, 0x53U, 0x00U,
}},
/* ==== WASM: 11 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -2175,6 +2129,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x54U, 0x00U,
}},
/* ==== WASM: 12 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -2219,6 +2174,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x30U, 0x00U,
}},
/* ==== WASM: 13 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -2537,6 +2493,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x80U, 0x80U, 0x80U, 0x00U, 0x0BU,
}},
/* ==== WASM: 14 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -3322,6 +3279,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x37U, 0x36U, 0x33U, 0x4CU, 0x4CU, 0x20U, 0x29U, 0x00U,
}},
/* ==== WASM: 15 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -3711,6 +3669,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x80U, 0x80U, 0x80U, 0x00U, 0x0BU,
}},
/* ==== WASM: 16 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -3891,6 +3850,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x38U, 0x4CU, 0x4CU, 0x29U, 0x00U,
}},
/* ==== WASM: 17 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -4079,6 +4039,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x29U, 0x00U,
}},
/* ==== WASM: 18 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -4317,6 +4278,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U, 0x42U, 0x00U, 0x10U, 0x85U, 0x80U, 0x80U, 0x80U, 0x00U, 0x0BU,
}},
/* ==== WASM: 19 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -5018,6 +4980,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x38U, 0x35U, 0x35U, 0x32U, 0x55U, 0x29U, 0x00U,
}},
/* ==== WASM: 20 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -6362,6 +6325,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x29U, 0x00U,
}},
/* ==== WASM: 21 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -6463,6 +6427,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x0BU,
}},
/* ==== WASM: 22 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -6505,6 +6470,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U, 0x0BU,
}},
/* ==== WASM: 23 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -6652,6 +6618,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x34U, 0x34U, 0x4CU, 0x4CU, 0x2CU, 0x20U, 0x33U, 0x29U, 0x00U,
}},
/* ==== WASM: 24 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -6979,6 +6946,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x38U, 0x34U, 0x39U, 0x30U, 0x4CU, 0x4CU, 0x00U,
}},
/* ==== WASM: 25 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -7183,6 +7151,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x10U, 0x85U, 0x80U, 0x80U, 0x80U, 0x00U, 0x0BU,
}},
/* ==== WASM: 26 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -7866,6 +7835,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x3DU, 0x3DU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 27 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -8160,6 +8130,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x32U, 0x34U, 0x31U, 0x36U, 0x55U, 0x4CU, 0x4CU, 0x00U,
}},
/* ==== WASM: 28 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -8872,6 +8843,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x31U, 0x33U, 0x33U, 0x38U, 0x20U, 0x29U, 0x00U,
}},
/* ==== WASM: 29 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -8956,6 +8928,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x30U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U, 0x00U,
}},
/* ==== WASM: 30 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9017,6 +8990,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x80U, 0x80U, 0x00U, 0x0BU,
}},
/* ==== WASM: 31 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9093,6 +9067,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 32 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9169,6 +9144,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 33 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9425,6 +9401,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U, 0x00U,
}},
/* ==== WASM: 34 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9582,6 +9559,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x04U, 0x00U, 0x00U,
}},
/* ==== WASM: 35 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9934,6 +9912,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U, 0x2AU, 0x04U, 0x00U, 0x00U, 0x31U, 0x04U, 0x00U, 0x00U,
}},
/* ==== WASM: 36 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -9966,6 +9945,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x20U, 0x01U, 0x0BU,
}},
/* ==== WASM: 37 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10192,6 +10172,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 38 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10222,6 +10203,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x80U, 0x80U, 0x00U, 0x1AU, 0x20U, 0x01U, 0x0BU,
}},
/* ==== WASM: 39 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10532,6 +10514,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 40 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10608,6 +10591,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 41 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10641,6 +10625,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x80U, 0x80U, 0x00U, 0x1AU, 0x20U, 0x01U, 0x0BU,
}},
/* ==== WASM: 42 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10727,6 +10712,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x32U, 0x00U,
}},
/* ==== WASM: 43 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10760,6 +10746,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x0BU,
}},
/* ==== WASM: 44 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -10899,6 +10886,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x54U, 0x5FU, 0x4DU, 0x45U, 0x54U, 0x00U,
}},
/* ==== WASM: 45 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -11088,6 +11076,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x31U, 0x34U, 0x00U,
}},
/* ==== WASM: 46 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -11241,6 +11230,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 47 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -11401,6 +11391,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 48 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -11557,6 +11548,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x53U, 0x4CU, 0x4FU, 0x54U, 0x53U, 0x00U,
}},
/* ==== WASM: 49 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -11641,6 +11633,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x74U, 0x79U, 0x70U, 0x65U, 0x28U, 0x29U, 0x00U,
}},
/* ==== WASM: 50 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -11897,6 +11890,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U, 0x00U,
}},
/* ==== WASM: 51 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -12134,6 +12128,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x3EU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 52 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -12229,6 +12224,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x53U, 0x54U, 0x00U,
}},
/* ==== WASM: 53 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -12338,6 +12334,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x31U, 0x00U,
}},
/* ==== WASM: 54 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -12467,6 +12464,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x30U, 0x30U, 0x30U, 0x4CU, 0x4CU, 0x00U,
}},
/* ==== WASM: 55 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -12740,6 +12738,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x30U, 0x00U,
}},
/* ==== WASM: 56 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -12876,6 +12875,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x2CU, 0x20U, 0x73U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U, 0x00U,
}},
/* ==== WASM: 57 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -13169,6 +13169,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x5FU, 0x53U, 0x4CU, 0x4FU, 0x54U, 0x53U, 0x00U,
}},
/* ==== WASM: 58 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -13402,6 +13403,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x5FU, 0x53U, 0x4CU, 0x4FU, 0x54U, 0x53U, 0x00U,
}},
/* ==== WASM: 59 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -13697,6 +13699,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x2CU, 0x20U, 0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 60 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -13900,6 +13903,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x6EU, 0x74U, 0x32U, 0x22U, 0x20U, 0x2BU, 0x20U, 0x69U, 0x29U, 0x00U,
}},
/* ==== WASM: 61 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -14016,6 +14020,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x69U, 0x29U, 0x00U,
}},
/* ==== WASM: 62 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -14124,6 +14129,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x6EU, 0x74U, 0x65U, 0x6EU, 0x74U, 0x32U, 0x22U, 0x29U, 0x00U,
}},
/* ==== WASM: 63 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -14396,6 +14402,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 64 ==== */
{R"[test.hook](
#include <stdint.h>
#define sfInvoiceID ((5U << 16U) + 17U)
@@ -14614,6 +14621,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x30U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 65 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -14725,6 +14733,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x58U, 0x49U, 0x53U, 0x54U, 0x00U,
}},
/* ==== WASM: 66 ==== */
{R"[test.hook](
#include <stdint.h>
#define sfInvoiceID ((5U << 16U) + 17U)
@@ -14860,6 +14869,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x3DU, 0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 67 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -14995,6 +15005,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x49U, 0x47U, 0x00U,
}},
/* ==== WASM: 68 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15133,6 +15144,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x66U, 0x28U, 0x64U, 0x61U, 0x74U, 0x61U, 0x32U, 0x29U, 0x00U,
}},
/* ==== WASM: 69 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15244,6 +15256,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 70 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15337,6 +15350,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x61U, 0x64U, 0x5BU, 0x69U, 0x5DU, 0x00U,
}},
/* ==== WASM: 71 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15469,6 +15483,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x64U, 0x5BU, 0x69U, 0x5DU, 0x00U,
}},
/* ==== WASM: 72 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15556,6 +15571,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x61U, 0x74U, 0x61U, 0x29U, 0x00U,
}},
/* ==== WASM: 73 ==== */
{R"[test.hook](
#include <stdint.h>
#define sfInvoiceID ((5U << 16U) + 17U)
@@ -15666,6 +15682,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x33U, 0x32U, 0x00U,
}},
/* ==== WASM: 74 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15876,6 +15893,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 75 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -15983,6 +16001,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x22U, 0x32U, 0x22U, 0x2CU, 0x20U, 0x31U, 0x29U, 0x00U,
}},
/* ==== WASM: 76 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -16594,6 +16613,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 77 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -16780,6 +16800,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x63U, 0x65U, 0x29U, 0x20U, 0x3EU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 78 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17129,6 +17150,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 79 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17264,6 +17286,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U,
}},
/* ==== WASM: 80 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17322,6 +17345,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x64U, 0xE1U, 0xF1U,
}},
/* ==== WASM: 81 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17494,6 +17518,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x54U, 0x5FU, 0x45U, 0x58U, 0x49U, 0x53U, 0x54U, 0x00U,
}},
/* ==== WASM: 82 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17641,6 +17666,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x30U, 0x00U, 0x22U, 0x00U, 0x00U, 0x00U, 0x00U,
}},
/* ==== WASM: 83 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17794,6 +17820,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x0FU, 0x0BU, 0x02U, 0x56U, 0x00U,
}},
/* ==== WASM: 84 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17890,6 +17917,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x3DU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 85 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -17948,6 +17976,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x4FU, 0x46U, 0x5FU, 0x42U, 0x4FU, 0x55U, 0x4EU, 0x44U, 0x53U, 0x00U,
}},
/* ==== WASM: 86 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -18006,6 +18035,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x4EU, 0x44U, 0x53U, 0x00U,
}},
/* ==== WASM: 87 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -19834,6 +19864,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x53U, 0x4DU, 0x41U, 0x4CU, 0x4CU, 0x00U,
}},
/* ==== WASM: 88 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -21452,6 +21483,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x29U, 0x29U, 0x00U,
}},
/* ==== WASM: 89 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -24384,6 +24416,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x4FU, 0x4FU, 0x5FU, 0x53U, 0x4DU, 0x41U, 0x4CU, 0x4CU, 0x00U,
}},
/* ==== WASM: 90 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -26348,6 +26381,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x54U, 0x4FU, 0x4FU, 0x5FU, 0x53U, 0x4DU, 0x41U, 0x4CU, 0x4CU, 0x00U,
}},
/* ==== WASM: 91 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -26632,6 +26666,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x30U, 0x00U,
}},
/* ==== WASM: 92 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g(uint32_t, uint32_t);
@@ -27218,6 +27253,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x4EU, 0x5FU, 0x46U, 0x41U, 0x49U, 0x4CU, 0x55U, 0x52U, 0x45U, 0x00U,
}},
/* ==== WASM: 93 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -27246,6 +27282,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x0BU,
}},
/* ==== WASM: 94 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -27277,6 +27314,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x20U, 0x52U, 0x65U, 0x6AU, 0x65U, 0x63U, 0x74U, 0x65U, 0x64U, 0x00U,
}},
/* ==== WASM: 95 ==== */
{R"[test.hook](
(module
(type (;0;) (func (param i32 i32 i64) (result i64)))
@@ -27303,6 +27341,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x00U, 0x0BU,
}},
/* ==== WASM: 96 ==== */
{R"[test.hook](
(module
(type (;0;) (func (param i32 i32) (result i32)))
@@ -27355,6 +27394,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x00U, 0x1AU, 0x0BU,
}},
/* ==== WASM: 97 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -33997,6 +34037,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x39U, 0x30U, 0x31U, 0x32U, 0x33U, 0x00U,
}},
/* ==== WASM: 98 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -34042,6 +34083,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x0BU, 0x06U, 0x76U, 0x61U, 0x6CU, 0x75U, 0x65U, 0x00U,
}},
/* ==== WASM: 99 ==== */
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
@@ -34070,6 +34112,7 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x0BU,
}},
/* ==== WASM: 100 ==== */
{R"[test.hook](
#include <stdint.h>
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
-819
View File
@@ -1,819 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/network.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/SetManifest.h>
#include <xrpld/core/Config.h>
#include <xrpld/ledger/OpenView.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/json/to_string.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/protocol/st.h>
#include <functional>
#include <limits>
#include <utility>
#include <vector>
namespace ripple {
namespace test {
/** Tests the OnChainManifests amendment: the SetManifest transactor, the
submit RPC path that builds its transaction, and reading manifests back out
of the ledger into the manifest cache.
*/
struct SetManifest_test : public beast::unit_test::suite
{
// A manifest transaction is unsigned, so the network id is mandatory and
// the network must be one that requires it (id > 1024).
static std::unique_ptr<Config>
makeConfig()
{
return jtx::network::makeNetworkConfig(
21337, "10", "1000000", "200000");
}
/** Builds a manifest signed by `master` nominating `ephemeral`.
A sequence of UINT32_MAX makes it a revocation, which by definition
names no signing key and carries no ephemeral signature.
*/
static std::string
makeManifest(
jtx::Account const& master,
jtx::Account const& ephemeral,
std::uint32_t seq)
{
STObject st(sfGeneric);
st[sfSequence] = seq;
st[sfPublicKey] = master.pk();
if (seq != std::numeric_limits<std::uint32_t>::max())
{
st[sfSigningPubKey] = ephemeral.pk();
sign(
st,
HashPrefix::manifest,
*publicKeyType(ephemeral.pk()),
ephemeral.sk());
}
sign(
st,
HashPrefix::manifest,
*publicKeyType(master.pk()),
master.sk(),
sfMasterSignature);
Serializer s;
st.add(s);
return std::string(static_cast<char const*>(s.data()), s.size());
}
/** Submits a manifest through the submit RPC.
The transaction is unsigned -- authority comes from the manifest's own
master and ephemeral signatures -- so it cannot be submitted through
env() the way a signed transaction can.
*/
static Json::Value
submit(jtx::Env& env, std::string const& manifest)
{
Json::Value params;
params[jss::manifest] = strHex(manifest);
return env.rpc("json", "submit", to_string(params))[jss::result];
}
static std::string
engineResult(Json::Value const& result)
{
return result[jss::engine_result].asString();
}
/** A well formed STObject that is not a well formed manifest.
The manifest format requires sfPublicKey and sfMasterSignature, so this
parses as an object and then fails applyTemplate.
*/
static std::string
makeUnparseableManifest()
{
STObject st(sfGeneric);
st[sfSequence] = 1;
Serializer s;
st.add(s);
return std::string(static_cast<char const*>(s.data()), s.size());
}
/** The envelope Submit would build for `manifest`, optionally tweaked.
makeSetManifestTx() rejects any manifest the transactor would reject,
and checkValidity() rejects any envelope whose signatures do not check
out, so several of the transactor's own rejections cannot be provoked
through the submit RPC at all. Building the envelope here and applying
it straight to the open view reaches them.
*/
static std::shared_ptr<STTx const>
envelope(
jtx::Env& env,
std::string const& manifest,
AccountID const& account,
std::function<void(STObject&)> const& tweak = {})
{
auto const build = [&](XRPAmount fee, bool tweaked) {
return std::make_shared<STTx const>(
ttMANIFEST_SET, [&](STObject& obj) {
obj.setAccountID(sfAccount, account);
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfNetworkID, env.app().config().NETWORK_ID);
obj.setFieldAmount(sfFee, fee);
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setFieldVL(sfTxnSignature, Blob{});
SerialIter mit{makeSlice(manifest)};
obj.peekFieldObject(sfManifest).set(mit);
if (tweaked && tweak)
tweak(obj);
});
};
// The same two-pass pricing makeSetManifestTx() does: encode once with
// a placeholder purely to have something to measure, then encode at the
// ceiling checkFee() will accept.
auto const base = SetManifest::calculateBaseFee(
*env.current(), *build(XRPAmount{0}, false));
return build(mulRatio(base, 12, 10, /*roundUp*/ true), true);
}
/** A mutable copy of a ledger object, suitable for the RawView interface.
Round-tripped through the wire format rather than copy constructed.
applyTemplate() rejects an object carrying a materialised soeDEFAULT
field that holds its default value, and that is exactly what reading one
out of a view hands back: an AccountRoot arrives with sfMintedNFTokens
present and zero. Serializing omits those fields and deserializing
re-materialises them.
*/
static std::shared_ptr<SLE>
rawCopy(std::shared_ptr<SLE const> const& sle)
{
Serializer s;
sle->add(s);
SerialIter sit{s.slice()};
return std::make_shared<SLE>(sit, sle->key());
}
/** Removes a ledger object from a view, if present.
OpenView exposes only the RawView interface, so the entry has to be
copied off the read side before it can be handed back to rawErase.
*/
static void
rawErase(OpenView& view, Keylet const& kl)
{
if (auto const sle = view.read(kl))
view.rawErase(rawCopy(sle));
}
/** Applies `tx` to a throwaway copy of the open ledger and reports the TER.
`prepare` runs against the same view first, so a test can corrupt state
that no ordinary transaction could produce. Nothing is retained.
*/
TER
applyDirect(
jtx::Env& env,
std::shared_ptr<STTx const> const& tx,
std::function<void(OpenView&)> const& prepare = {})
{
TER ret = tesSUCCESS;
env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
if (prepare)
prepare(view);
ret = ripple::apply(env.app(), view, *tx, tapNONE, j).ter;
return false; // discard, corrupt or otherwise
});
return ret;
}
void
testSubmission(FeatureBitset features)
{
testcase("submission");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
BEAST_EXPECT(
engineResult(submit(env, makeManifest(master, ephemeral, 1))) ==
"tesSUCCESS");
env.close();
// A manifest is written twice so it can be found from either key, and
// each copy points at the other.
auto const byMaster = env.le(keylet::manifest(master.pk()));
auto const byEphemeral = env.le(keylet::manifest(ephemeral.pk()));
if (!BEAST_EXPECT(byMaster))
return;
if (!BEAST_EXPECT(byEphemeral))
return;
BEAST_EXPECT(byMaster->getAccountID(sfAccount) == master.id());
BEAST_EXPECT(byEphemeral->getAccountID(sfAccount) == master.id());
BEAST_EXPECT(byMaster->getFieldU32(sfSequence) == 1);
BEAST_EXPECT(
byMaster->getFieldH256(sfManifestID) ==
keylet::manifest(ephemeral.pk()).key);
BEAST_EXPECT(
byEphemeral->getFieldH256(sfManifestID) ==
keylet::manifest(master.pk()).key);
// Both signatures are mirrored so the object can be verified, and
// re-served, by any node that reads it.
BEAST_EXPECT(byMaster->isFieldPresent(sfMasterSignature));
BEAST_EXPECT(byMaster->isFieldPresent(sfSignature));
// The account root points at the master key's copy.
auto const sleAcct = env.le(master);
if (!BEAST_EXPECT(sleAcct))
return;
BEAST_EXPECT(
sleAcct->getFieldH256(sfManifestID) ==
keylet::manifest(master.pk()).key);
// The account sequence must be untouched: the transaction is unsigned
// and pinned to sequence 0, so consuming a sequence would let a third
// party burn the validator's sequence numbers -- and writing seq + 1
// would reset the account to 1.
BEAST_EXPECT(sleAcct->getFieldU32(sfSequence) == env.seq(master));
}
void
testUpdate(FeatureBitset features)
{
testcase("update and stale rejection");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const eph1 = Account("eph1", KeyType::ed25519);
auto const eph2 = Account("eph2", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
submit(env, makeManifest(master, eph1, 1));
env.close();
// Rotating the ephemeral key erases both old copies and writes two
// new ones, so the two can never drift apart.
BEAST_EXPECT(
engineResult(submit(env, makeManifest(master, eph2, 2))) ==
"tesSUCCESS");
env.close();
BEAST_EXPECT(!env.le(keylet::manifest(eph1.pk())));
if (!BEAST_EXPECT(env.le(keylet::manifest(eph2.pk()))))
return;
BEAST_EXPECT(
env.le(keylet::manifest(master.pk()))->getFieldU32(sfSequence) ==
2);
// Replaying the manifest we just applied, and anything older, is
// rejected on sequence. This is what prevents replay: the transaction
// is unsigned, so nothing else would.
BEAST_EXPECT(
engineResult(submit(env, makeManifest(master, eph2, 2))) ==
"tefPAST_MANIFEST_SEQ");
BEAST_EXPECT(
engineResult(submit(env, makeManifest(master, eph1, 1))) ==
"tefPAST_MANIFEST_SEQ");
}
void
testRevocation(FeatureBitset features)
{
testcase("revocation");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
submit(env, makeManifest(master, ephemeral, 1));
env.close();
BEAST_EXPECT(
engineResult(submit(
env,
makeManifest(
master,
ephemeral,
std::numeric_limits<std::uint32_t>::max()))) ==
"tesSUCCESS");
env.close();
// A revocation has no signing key, so only the master key's copy
// exists and it points at nothing.
auto const byMaster = env.le(keylet::manifest(master.pk()));
if (!BEAST_EXPECT(byMaster))
return;
BEAST_EXPECT(!env.le(keylet::manifest(ephemeral.pk())));
BEAST_EXPECT(!byMaster->isFieldPresent(sfManifestID));
BEAST_EXPECT(!byMaster->isFieldPresent(sfSigningPubKey));
BEAST_EXPECT(
byMaster->getFieldU32(sfSequence) ==
std::numeric_limits<std::uint32_t>::max());
// Nothing supersedes a revocation.
BEAST_EXPECT(
engineResult(submit(env, makeManifest(master, ephemeral, 2))) ==
"tefREVOKED_MANIFEST");
}
void
testRetrieval(FeatureBitset features)
{
testcase("retrieval into the manifest cache");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const eph1 = Account("eph1", KeyType::ed25519);
auto const eph2 = Account("eph2", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
submit(env, makeManifest(master, eph1, 1));
env.close();
auto& cache = env.app().validatorManifests();
// Nothing has fed the cache yet, so an unknown key maps to itself.
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == eph1.pk());
// Reading the ledger resolves both directions of the mapping. The
// manifest is reconstructed from the ledger object and verified, so a
// lossy round trip would fail here rather than be accepted.
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 1);
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == master.pk());
BEAST_EXPECT(cache.getSigningKey(master.pk()) == eph1.pk());
BEAST_EXPECT(cache.getSequence(master.pk()) == 1);
// Applying the same ledger again is a no-op: the cache is already at
// that sequence.
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 0);
// A rotation on-ledger is picked up, and the superseded ephemeral key
// stops resolving.
submit(env, makeManifest(master, eph2, 2));
env.close();
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 1);
BEAST_EXPECT(cache.getSigningKey(master.pk()) == eph2.pk());
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == eph1.pk());
// A key we never ask about is never read.
BEAST_EXPECT(cache.applyLedger(*env.closed(), {eph2.pk()}) == 0);
// A revocation reaches the cache too, and is the one thing that must
// never be lost: forgetting it would mean trusting the old key again.
submit(
env,
makeManifest(
master, eph2, std::numeric_limits<std::uint32_t>::max()));
env.close();
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 1);
BEAST_EXPECT(cache.revoked(master.pk()));
}
void
testSigningKeyRetrieval(FeatureBitset features)
{
testcase("retrieval by ephemeral key");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const eph1 = Account("eph1", KeyType::ed25519);
auto const eph2 = Account("eph2", KeyType::ed25519);
auto const stranger = Account("stranger", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
submit(env, makeManifest(master, eph1, 1));
env.close();
auto& cache = env.app().validatorManifests();
// The situation applyLedger() cannot serve: a validation arrives
// signed by eph1 and the node holds no manifest naming it, so the
// master key to probe for is exactly what is missing.
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == eph1.pk());
BEAST_EXPECT(
cache.applyLedgerSigningKey(*env.closed(), eph1.pk()) ==
master.pk());
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == master.pk());
BEAST_EXPECT(cache.getSigningKey(master.pk()) == eph1.pk());
BEAST_EXPECT(cache.getSequence(master.pk()) == 1);
// A key with no manifest on-ledger resolves to nothing and leaves the
// cache untouched.
BEAST_EXPECT(
!cache.applyLedgerSigningKey(*env.closed(), stranger.pk()));
BEAST_EXPECT(cache.getMasterKey(stranger.pk()) == stranger.pk());
// A rotation is recovered from the new ephemeral key alone, and the
// superseded key stops resolving because its object is gone.
submit(env, makeManifest(master, eph2, 2));
env.close();
BEAST_EXPECT(
cache.applyLedgerSigningKey(*env.closed(), eph2.pk()) ==
master.pk());
BEAST_EXPECT(cache.getSigningKey(master.pk()) == eph2.pk());
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == eph1.pk());
BEAST_EXPECT(!env.le(keylet::manifest(eph1.pk())));
BEAST_EXPECT(!cache.applyLedgerSigningKey(*env.closed(), eph1.pk()));
// Asking again is answered from the cache, ahead of the per-ledger
// probe bookkeeping.
BEAST_EXPECT(
cache.applyLedgerSigningKey(*env.closed(), eph2.pk()) ==
master.pk());
// A master key is not a signing key. The object at its keylet is a
// perfectly good manifest and is ingested, but it binds eph2, not the
// master key, so nothing is reported for the key asked about.
BEAST_EXPECT(!cache.applyLedgerSigningKey(*env.closed(), master.pk()));
// A revoked master publishes no ephemeral object at all, so this
// direction goes quiet. The revocation still reaches the cache by
// master key, which is what applyLedger() is for.
submit(
env,
makeManifest(
master, eph2, std::numeric_limits<std::uint32_t>::max()));
env.close();
BEAST_EXPECT(!env.le(keylet::manifest(eph2.pk())));
BEAST_EXPECT(cache.applyLedger(*env.closed(), {master.pk()}) == 1);
BEAST_EXPECT(cache.revoked(master.pk()));
}
void
testMalformed(FeatureBitset features)
{
testcase("malformed submissions");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const other = Account("other", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master, other);
env.close();
// Not a manifest at all.
BEAST_EXPECT(
submit(env, "not a manifest")[jss::error].asString() ==
"invalidManifest");
// A manifest whose ephemeral signature does not check out.
{
auto blob = makeManifest(master, ephemeral, 1);
blob[blob.size() - 1] ^= 0xFF;
BEAST_EXPECT(
submit(env, blob)[jss::error].asString() == "invalidManifest");
}
// The master key's account must exist: it pays the fee and holds the
// pointer to the manifest.
auto const unfunded = Account("unfunded", KeyType::ed25519);
BEAST_EXPECT(
engineResult(submit(env, makeManifest(unfunded, ephemeral, 1))) ==
"terNO_ACCOUNT");
// An ephemeral key already claimed by a different account would
// collide with -- and clobber -- that account's manifest object.
submit(env, makeManifest(master, ephemeral, 1));
env.close();
BEAST_EXPECT(
engineResult(submit(env, makeManifest(other, ephemeral, 1))) ==
"tecDUPLICATE");
}
void
testEnvelopeRejections(FeatureBitset features)
{
testcase("envelope rejections");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const other = Account("other", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master, other);
env.close();
auto const good = makeManifest(master, ephemeral, 1);
// Sanity check: the unmodified envelope is the one Submit builds, so
// every rejection below is attributable to the tweak and nothing else.
BEAST_EXPECT(
applyDirect(env, envelope(env, good, master.id())) == tesSUCCESS);
// Any bit but tfFullyCanonicalSig (0x80000000), which is tfUniversal
// and so permitted on every transaction.
BEAST_EXPECT(
applyDirect(
env, envelope(env, good, master.id(), [](STObject& obj) {
obj.setFieldU32(sfFlags, 0x00000001);
})) == temINVALID_FLAG);
// sfManifest parses as an object but not as a manifest.
BEAST_EXPECT(
applyDirect(
env, envelope(env, makeUnparseableManifest(), master.id())) ==
temMALFORMED);
// A manifest whose signatures do not check out. checkValidity() would
// stop this before preflight, so only a direct apply reaches the
// transactor's own verify() call.
{
auto bad = good;
bad[bad.size() - 1] ^= 0xFF;
BEAST_EXPECT(
applyDirect(env, envelope(env, bad, master.id())) ==
temMALFORMED);
}
// The envelope's account must be the manifest's master key.
BEAST_EXPECT(
applyDirect(env, envelope(env, good, other.id())) == temMALFORMED);
// Every envelope field a relayer could otherwise choose is pinned.
for (auto const& [name, tweak] : std::vector<
std::pair<char const*, std::function<void(STObject&)>>>{
{"Sequence",
[](STObject& obj) { obj.setFieldU32(sfSequence, 1); }},
{"AccountTxnID",
[](STObject& obj) {
obj.setFieldH256(sfAccountTxnID, uint256{1});
}},
{"TicketSequence",
[](STObject& obj) { obj.setFieldU32(sfTicketSequence, 1); }},
{"SigningPubKey", [&](STObject& obj) {
obj.setFieldVL(sfSigningPubKey, master.pk().slice());
}}})
{
BEAST_EXPECTS(
applyDirect(env, envelope(env, good, master.id(), tweak)) ==
temMALFORMED,
name);
}
// sfFee is the one envelope field preflight cannot bound, because the
// base fee is not in scope until preclaim. checkFee() caps it instead.
auto const priced = envelope(env, good, master.id());
auto const ceiling = priced->getFieldAmount(sfFee).xrp();
BEAST_EXPECT(
applyDirect(
env, envelope(env, good, master.id(), [&](STObject& obj) {
obj.setFieldAmount(sfFee, ceiling + XRPAmount{1});
})) == temBAD_FEE);
// At the ceiling exactly, which is what Submit sends.
BEAST_EXPECT(
applyDirect(
env, envelope(env, good, master.id(), [&](STObject& obj) {
obj.setFieldAmount(sfFee, ceiling);
})) == tesSUCCESS);
}
void
testCorruptLedger(FeatureBitset features)
{
testcase("corrupt manifest state");
using namespace jtx;
Env env{*this, makeConfig(), features};
auto const master = Account("master", KeyType::ed25519);
auto const eph1 = Account("eph1", KeyType::ed25519);
auto const eph2 = Account("eph2", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
BEAST_EXPECT(
engineResult(submit(env, makeManifest(master, eph1, 1))) ==
"tesSUCCESS");
env.close();
auto const update =
envelope(env, makeManifest(master, eph2, 2), master.id());
// sfManifestID on the account root points at nothing.
BEAST_EXPECT(applyDirect(env, update, [&](OpenView& view) {
rawErase(view, keylet::manifest(master.pk()));
}) == tefBAD_LEDGER);
// The master copy survives but the ephemeral copy it points at is
// gone.
BEAST_EXPECT(applyDirect(env, update, [&](OpenView& view) {
rawErase(view, keylet::manifest(eph1.pk()));
}) == tefBAD_LEDGER);
// The reverse: the account root has forgotten its manifest, so the
// erase pass is skipped and the keylet is found occupied.
BEAST_EXPECT(applyDirect(env, update, [&](OpenView& view) {
auto replacement =
rawCopy(view.read(keylet::account(master.id())));
replacement->makeFieldAbsent(sfManifestID);
view.rawReplace(replacement);
}) == tefBAD_LEDGER);
// None of the above was retained, so the ledger is still intact and an
// ordinary update still works.
BEAST_EXPECT(applyDirect(env, update) == tesSUCCESS);
}
void
testGossipSelection(FeatureBitset features)
{
testcase("gossip selection");
using namespace jtx;
Env env{*this, makeConfig(), features};
std::vector<Account> masters;
for (int i = 0; i < 4; ++i)
masters.emplace_back("gm" + std::to_string(i), KeyType::ed25519);
auto const ephemeral = Account("gossipeph", KeyType::ed25519);
for (auto const& m : masters)
env.fund(XRP(1000), m);
env.close();
// Each master needs its own ephemeral key: preclaim rejects a key
// already claimed by another account.
std::vector<Account> ephs;
for (int i = 0; i < 4; ++i)
ephs.emplace_back("ge" + std::to_string(i), KeyType::ed25519);
for (int i = 0; i < 4; ++i)
{
BEAST_EXPECT(
engineResult(
submit(env, makeManifest(masters[i], ephs[i], 1))) ==
"tesSUCCESS");
env.close();
}
ManifestCache cache;
hash_set<PublicKey> all;
for (auto const& m : masters)
all.insert(m.pk());
BEAST_EXPECT(cache.applyLedger(*env.closed(), all) == 4);
// The raw form is what a republishing node needs: the exact bytes the
// master key signed, plus the sequence to compare against the ledger.
auto const raw = cache.getRawManifest(masters[0].pk());
if (BEAST_EXPECT(raw))
{
BEAST_EXPECT(raw->first == 1);
BEAST_EXPECT(!raw->second.empty());
}
BEAST_EXPECT(!cache.getRawManifest(ephemeral.pk()));
// Pinning bumps the sequence so a cached gossip message is rebuilt,
// and pinning the same set again does not.
auto const seq = cache.sequence();
cache.pin({masters[0].pk(), masters[1].pk()});
BEAST_EXPECT(cache.sequence() > seq);
auto const seq2 = cache.sequence();
cache.pin({masters[0].pk(), masters[1].pk()});
BEAST_EXPECT(cache.sequence() == seq2);
// Everything is offered: two pinned plus two under the gossip limit.
std::size_t reserved = 0;
std::vector<PublicKey> offered;
cache.for_each_gossip_manifest(
[&](std::size_t n) { reserved = n; },
[&](Manifest const& m) { offered.push_back(m.masterKey); });
BEAST_EXPECT(reserved == 4);
BEAST_EXPECT(offered.size() == 4);
BEAST_EXPECT(
hash_set<PublicKey>(offered.begin(), offered.end()) == all);
// A pinned key with no manifest is counted in the reservation but not
// offered, since the reservation is only an upper bound.
cache.pin({masters[0].pk(), ephemeral.pk()});
offered.clear();
cache.for_each_gossip_manifest(
[&](std::size_t n) { reserved = n; },
[&](Manifest const& m) { offered.push_back(m.masterKey); });
BEAST_EXPECT(reserved == 5);
BEAST_EXPECT(offered.size() == 4);
}
void
testDisabled(FeatureBitset features)
{
testcase("amendment gate");
using namespace jtx;
Env env{*this, makeConfig(), features - featureOnChainManifests};
auto const master = Account("master", KeyType::ed25519);
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
env.fund(XRP(1000), master);
env.close();
// The submitted transaction carries no account signature, so with the
// amendment off it would otherwise be rejected as unsigned and never
// reach preflight's temDISABLED. The RPC therefore refuses it up
// front, and says why.
auto const result = submit(env, makeManifest(master, ephemeral, 1));
BEAST_EXPECT(result[jss::error].asString() == "notEnabled");
BEAST_EXPECT(!result.isMember(jss::engine_result));
env.close();
BEAST_EXPECT(!env.le(keylet::manifest(master.pk())));
}
public:
void
run() override
{
using namespace test::jtx;
auto const sa = supported_amendments();
testSubmission(sa);
testUpdate(sa);
testRevocation(sa);
testRetrieval(sa);
testSigningKeyRetrieval(sa);
testMalformed(sa);
testEnvelopeRejections(sa);
testCorruptLedger(sa);
testGossipSelection(sa);
testDisabled(sa);
}
};
BEAST_DEFINE_TESTSUITE(SetManifest, app, ripple);
} // namespace test
} // namespace ripple
+465
View File
@@ -0,0 +1,465 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/AMM.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple {
namespace test {
struct URITokenBroker_test : public beast::unit_test::suite
{
static Json::Value
brokeredBuy(
jtx::Account const& buyer,
std::string const& tokenID,
STAmount const& amount,
jtx::Account const& broker)
{
auto tx = jtx::uritoken::buy(buyer, tokenID);
tx[sfAmount.jsonName] = amount.getJson(JsonOptions::none);
tx[sfBrokerAccount.jsonName] = broker.human();
return tx;
}
static std::string
mintAndSell(
jtx::Env& env,
jtx::Account const& seller,
std::string const& uri,
STAmount const& amount,
std::optional<jtx::Account> const& destination = std::nullopt)
{
auto const id = strHex(jtx::uritoken::tokenid(seller, uri));
env(jtx::uritoken::mint(seller, uri));
if (destination)
env(jtx::uritoken::sell(seller, id),
jtx::uritoken::amt(amount),
jtx::uritoken::dest(*destination));
else
env(jtx::uritoken::sell(seller, id), jtx::uritoken::amt(amount));
env.close();
return id;
}
static std::shared_ptr<SLE const>
token(jtx::Env const& env, uint256 const& id)
{
return env.le(Keylet{ltURI_TOKEN, id});
}
static std::shared_ptr<SLE const>
token(jtx::Env const& env, std::string const& id)
{
uint256 tokenID;
if (!tokenID.parseHex(id))
return nullptr;
return token(env, tokenID);
}
void
testAmendment(FeatureBitset const& features)
{
testcase("amendment");
using namespace jtx;
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
// A broker-less buy remains valid when the broker amendment is off.
{
Env env{*this, features - featureURITokenBroker};
env.fund(XRP(1000), seller, buyer, broker);
auto const id = mintAndSell(env, seller, "no-broker", XRP(10));
env(uritoken::buy(buyer, id), uritoken::amt(XRP(10)));
}
// BrokerAccount requires both URITokenBroker and fixXahauV1.
for (auto const disabled : {featureURITokenBroker, fixXahauV1})
{
Env env{*this, features - disabled};
env.fund(XRP(1000), seller, buyer, broker);
auto const id =
mintAndSell(env, seller, to_string(disabled), XRP(10));
env(brokeredBuy(buyer, id, XRP(11), broker), ter(temDISABLED));
}
}
void
testMalformed(FeatureBitset const& features)
{
testcase("preflight");
using namespace jtx;
Env env{*this, features};
Account const seller{"seller"};
Account const buyer{"buyer"};
env.fund(XRP(1000), seller, buyer);
auto const id = mintAndSell(env, seller, "preflight", XRP(10));
env(brokeredBuy(buyer, id, XRP(11), buyer), ter(temREDUNDANT));
auto zero = uritoken::buy(buyer, id);
zero[sfAmount.jsonName] = XRP(11).value().getJson(JsonOptions::none);
zero[sfBrokerAccount.jsonName] = "rrrrrrrrrrrrrrrrrrrrrhoLvTp";
env(zero, ter(temMALFORMED));
// BrokerAccount is only part of the URITokenBuy template.
auto mint = uritoken::mint(seller, "broker-on-mint");
mint[sfBrokerAccount.jsonName] = buyer.human();
env(mint, ter(temMALFORMED));
// BrokerAccount belongs only to URITokenBuy's serialization template.
auto payment = pay(buyer, seller, XRP(1));
payment[sfBrokerAccount.jsonName] = seller.human();
env(payment, ter(temMALFORMED));
}
void
testPreclaim(FeatureBitset const& features)
{
testcase("preclaim");
using namespace jtx;
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
Account const missing{"missing"};
// Missing broker account.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer);
env.memoize(missing);
auto const id = mintAndSell(env, seller, "missing", XRP(10));
env(brokeredBuy(buyer, id, XRP(11), missing), ter(tecNO_TARGET));
}
// The seller cannot also receive the broker spread.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer);
auto const id = mintAndSell(env, seller, "seller", XRP(10));
env(brokeredBuy(buyer, id, XRP(11), seller), ter(tecNO_PERMISSION));
}
// A brokered sale requires both a positive price and positive spread.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker);
auto const zeroID = mintAndSell(env, seller, "zero", XRP(0), buyer);
env(brokeredBuy(buyer, zeroID, XRP(1), broker),
ter(tecNO_PERMISSION));
// The legacy destination-restricted zero-price buy is unchanged.
env(uritoken::buy(buyer, zeroID), uritoken::amt(XRP(0)));
auto const id = mintAndSell(env, seller, "spread", XRP(10));
env(brokeredBuy(buyer, id, XRP(9), broker),
ter(tecINSUFFICIENT_PAYMENT));
env(brokeredBuy(buyer, id, XRP(10), broker), ter(tecNO_PERMISSION));
}
// An IOU broker must already have a trust line.
{
Env env{*this, features};
Account const gw{"gateway"};
auto const USD = gw["USD"];
env.fund(XRP(1000), seller, buyer, broker, gw);
env.trust(USD(1000), seller, buyer);
env.close();
env(pay(gw, buyer, USD(100)));
auto const id = mintAndSell(env, seller, "no-line", USD(10));
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecNO_LINE));
}
// AMM pseudo-accounts cannot be named as brokers.
{
Env env{*this, features | featureAMM};
Account const gw{"gateway"};
auto const USD = gw["USD"];
env.fund(XRP(30'000), seller, buyer, gw);
env.trust(USD(30'000), seller, buyer);
env.close();
env(pay(gw, seller, USD(20'000)));
AMM amm{env, seller, XRP(10'000), USD(10'000)};
auto const id = mintAndSell(env, seller, "amm", XRP(10));
auto tx = uritoken::buy(buyer, id);
tx[sfAmount.jsonName] = XRP(11).value().getJson(JsonOptions::none);
tx[sfBrokerAccount.jsonName] = toBase58(amm.ammAccount());
env(tx, ter(tecNO_PERMISSION));
}
}
void
testXAH(FeatureBitset const& features)
{
testcase("XAH settlement");
using namespace jtx;
Env env{*this, features};
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
env.fund(XRP(1000), seller, buyer, broker);
env(fset(seller, asfDepositAuth));
env(fset(broker, asfDepositAuth));
env.close();
auto const id = mintAndSell(env, seller, "xah", XRP(100));
auto const sellerBefore = env.balance(seller);
auto const buyerBefore = env.balance(buyer);
auto const brokerBefore = env.balance(broker);
auto const buyerOwners = env.ownerCount(buyer);
auto const brokerOwners = env.ownerCount(broker);
auto const fee = env.current()->fees().base;
env(brokeredBuy(buyer, id, XRP(110), broker));
env.close();
BEAST_EXPECT(env.balance(seller) == sellerBefore + XRP(100));
BEAST_EXPECT(env.balance(broker) == brokerBefore + XRP(10));
BEAST_EXPECT(env.balance(buyer) == buyerBefore - XRP(110) - fee);
BEAST_EXPECT(env.ownerCount(buyer) == buyerOwners + 1);
BEAST_EXPECT(env.ownerCount(broker) == brokerOwners);
auto const sle = token(env, id);
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT((*sle)[sfOwner] == buyer.id());
BEAST_EXPECT(!sle->isFieldPresent(sfAmount));
BEAST_EXPECT(!sle->isFieldPresent(sfDestination));
}
void
testXAHReserve(FeatureBitset const& features)
{
testcase("XAH reserve boundary");
using namespace jtx;
Env env{*this, features};
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
env.fund(XRP(1000), seller, broker);
env.fund(XRP(300), buyer);
env.close();
auto const id = mintAndSell(env, seller, "reserve", XRP(1));
auto const sellerBefore = env.balance(seller);
auto const brokerBefore = env.balance(broker);
// The buyer needs the next owner reserve plus the full signed amount,
// not merely the seller's one-XAH listing amount.
env(brokeredBuy(buyer, id, XRP(51), broker),
ter(tecINSUFFICIENT_FUNDS));
BEAST_EXPECT(env.balance(seller) == sellerBefore);
BEAST_EXPECT(env.balance(broker) == brokerBefore);
auto const sle = token(env, id);
BEAST_EXPECT(sle && (*sle)[sfOwner] == seller.id());
}
void
testIOU(FeatureBitset const& features)
{
testcase("IOU settlement");
using namespace jtx;
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
Account const gw{"gateway"};
auto const USD = gw["USD"];
// Both recipients bear the transfer rate; buyer debit remains exact.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker, gw);
env(rate(gw, 1.25));
env.trust(USD(1000), seller, buyer, broker);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env(trust(gw, broker["USD"](1000), tfClearNoRipple));
env.close();
env(pay(gw, buyer, USD(1000)));
auto const id = mintAndSell(env, seller, "rate", USD(100));
auto const buyerBefore = env.balance(buyer, USD.issue());
auto const brokerOwners = env.ownerCount(broker);
env(brokeredBuy(buyer, id, USD(110), broker));
env.close();
BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(80));
BEAST_EXPECT(env.balance(broker, USD.issue()) == USD(8));
BEAST_EXPECT(
env.balance(buyer, USD.issue()) == buyerBefore - USD(110));
BEAST_EXPECT(env.ownerCount(broker) == brokerOwners);
}
// The issuer may be the broker and needs no trust line.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, gw);
env(rate(gw, 1.25));
env.trust(USD(1000), seller, buyer);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env.close();
env(pay(gw, buyer, USD(1000)));
auto const id = mintAndSell(env, seller, "issuer", USD(100));
auto const buyerBefore = env.balance(buyer, USD.issue());
env(brokeredBuy(buyer, id, USD(110), gw));
env.close();
BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(80));
BEAST_EXPECT(
env.balance(buyer, USD.issue()) == buyerBefore - USD(110));
}
// Existing line limits are not consulted and no owner object is added.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker, gw);
env.trust(USD(1000), seller, buyer);
env.trust(USD(1), broker);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env(trust(gw, broker["USD"](1), tfClearNoRipple));
env.close();
env(pay(gw, buyer, USD(100)));
env(pay(gw, broker, USD(1)));
auto const id = mintAndSell(env, seller, "limit", USD(10));
auto const brokerOwners = env.ownerCount(broker);
env(brokeredBuy(buyer, id, USD(20), broker));
env.close();
BEAST_EXPECT(env.balance(broker, USD.issue()) == USD(11));
BEAST_EXPECT(env.ownerCount(broker) == brokerOwners);
}
}
void
testIOUPermissions(FeatureBitset const& features)
{
testcase("IOU permissions");
using namespace jtx;
Account const seller{"seller"};
Account const buyer{"buyer"};
Account const broker{"broker"};
Account const gw{"gateway"};
auto const USD = gw["USD"];
// A frozen broker line rejects the broker leg atomically.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker, gw);
env.trust(USD(1000), seller, buyer, broker);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env(pay(gw, buyer, USD(100)));
env(trust(gw, broker["USD"](1000), tfSetFreeze | tfClearNoRipple));
env.close();
auto const id = mintAndSell(env, seller, "freeze", USD(10));
auto const buyerBefore = env.balance(buyer, USD.issue());
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecFROZEN));
BEAST_EXPECT(env.balance(seller, USD.issue()) == USD(0));
BEAST_EXPECT(env.balance(broker, USD.issue()) == USD(0));
BEAST_EXPECT(env.balance(buyer, USD.issue()) == buyerBefore);
}
// Under RequireAuth, an existing but unauthorized broker line fails.
{
Env env{*this, features};
auto const sellerUSD = seller["USD"];
auto const buyerUSD = buyer["USD"];
env.fund(XRP(1000), seller, buyer, broker, gw);
env(fset(gw, asfRequireAuth));
env.close();
env.trust(USD(1000), seller, buyer, broker);
env(trust(gw, sellerUSD(1000)),
txflags(tfSetfAuth | tfClearNoRipple));
env(trust(gw, buyerUSD(1000)),
txflags(tfSetfAuth | tfClearNoRipple));
env(trust(gw, broker["USD"](1000)), txflags(tfClearNoRipple));
env.close();
env(pay(gw, buyer, USD(100)));
auto const id = mintAndSell(env, seller, "auth", USD(10));
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecNO_AUTH));
}
// NoRipple on the broker leg rejects the whole transaction; the
// already-computed seller leg must also be rolled back.
{
Env env{*this, features};
env.fund(XRP(1000), seller, buyer, broker, gw);
env.trust(USD(1000), seller, buyer, broker);
env.close();
env(trust(gw, seller["USD"](1000), tfClearNoRipple));
env(trust(gw, buyer["USD"](1000), tfClearNoRipple));
env(pay(gw, buyer, USD(100)));
env(trust(gw, broker["USD"](1000), tfSetNoRipple));
env.close();
auto const id = mintAndSell(env, seller, "no-ripple", USD(10));
auto const sellerBefore = env.balance(seller, USD.issue());
auto const buyerBefore = env.balance(buyer, USD.issue());
auto const brokerBefore = env.balance(broker, USD.issue());
env(brokeredBuy(buyer, id, USD(11), broker), ter(tecPATH_DRY));
BEAST_EXPECT(env.balance(seller, USD.issue()) == sellerBefore);
BEAST_EXPECT(env.balance(buyer, USD.issue()) == buyerBefore);
BEAST_EXPECT(env.balance(broker, USD.issue()) == brokerBefore);
auto const sle = token(env, id);
BEAST_EXPECT(sle && (*sle)[sfOwner] == seller.id());
}
}
public:
void
run() override
{
auto const features = jtx::supported_amendments();
testAmendment(features);
testMalformed(features);
testPreclaim(features);
testXAH(features);
testXAHReserve(features);
testIOU(features);
testIOUPermissions(features);
}
};
BEAST_DEFINE_TESTSUITE(URITokenBroker, app, ripple);
} // namespace test
} // namespace ripple
+152 -138
View File
@@ -191,6 +191,7 @@ struct URIToken_test : public beast::unit_test::suite
using namespace jtx;
using namespace std::literals::chrono_literals;
// fixXahauV1
{
Env env{*this, features};
auto const alice = Account("alice");
@@ -203,9 +204,11 @@ struct URIToken_test : public beast::unit_test::suite
std::string const hexid{strHex(tid)};
// temMALFORMED - cannot include sfDestination without sfAmount
env(uritoken::mint(alice, uri),
uritoken::dest(bob),
ter(temMALFORMED));
bool const withFixXahauV1 =
env.current()->rules().enabled(fixXahauV1);
auto const txResult =
withFixXahauV1 ? ter(temMALFORMED) : ter(tefINTERNAL);
env(uritoken::mint(alice, uri), uritoken::dest(bob), txResult);
env.close();
}
@@ -514,10 +517,12 @@ struct URIToken_test : public beast::unit_test::suite
env.close();
// tecINSUFFICIENT_FUNDS - insufficient xrp - fees
// fixXahauV1 - fix checking wrong account for insufficient xrp
env(pay(env.master, alice, XRP(10000)));
env(uritoken::buy(bob, hexid),
uritoken::amt(XRP(10000)),
ter(tecINSUFFICIENT_FUNDS));
auto const txResult = env.current()->rules().enabled(fixXahauV1)
? ter(tecINSUFFICIENT_FUNDS)
: ter(tecINTERNAL);
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(10000)), txResult);
env.close();
// clear sell and reset new sell
@@ -574,9 +579,11 @@ struct URIToken_test : public beast::unit_test::suite
env.close();
// tecINSUFFICIENT_FUNDS - insufficient xrp - fees
env(uritoken::buy(bob, hexid),
uritoken::amt(XRP(1000)),
ter(tecINSUFFICIENT_FUNDS));
// fixXahauV1 - fix checking wrong account for insufficient xrp
auto const txResult1 = env.current()->rules().enabled(fixXahauV1)
? ter(tecINSUFFICIENT_FUNDS)
: ter(tecINTERNAL);
env(uritoken::buy(bob, hexid), uritoken::amt(XRP(1000)), txResult1);
env.close();
// clear sell and set usd sell
@@ -616,10 +623,12 @@ struct URIToken_test : public beast::unit_test::suite
env.close();
// tecNO_LINE_INSUF_RESERVE - insufficient xrp to create line
auto const txResult = env.current()->rules().enabled(fixXahauV1)
? ter(tecINSUF_RESERVE_SELLER)
: ter(tecNO_LINE_INSUF_RESERVE);
env(noop(echo), fee(XRP(50)), ter(tesSUCCESS));
env(uritoken::buy(dave, hexid),
uritoken::amt(USD(1)),
ter(tecINSUF_RESERVE_SELLER));
env(uritoken::buy(dave, hexid), uritoken::amt(USD(1)), txResult);
env.close();
}
@@ -980,7 +989,6 @@ 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)));
@@ -2113,7 +2121,14 @@ struct URIToken_test : public beast::unit_test::suite
env(uritoken::buy(bob, id), uritoken::amt(delta));
env.close();
auto const postAlice = env.balance(alice, USD.issue());
BEAST_EXPECT(to_string(postAlice.value()) == tc.divide);
if (!env.current()->rules().enabled(fixXahauV1))
{
BEAST_EXPECT(to_string(postAlice.value()) == tc.multiply);
}
else
{
BEAST_EXPECT(to_string(postAlice.value()) == tc.divide);
}
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob - delta);
}
@@ -2152,7 +2167,14 @@ struct URIToken_test : public beast::unit_test::suite
env.close();
auto const postAlice = env.balance(alice, USD.issue());
BEAST_EXPECT(postAlice.value() == preAlice);
if (!env.current()->rules().enabled(fixXahauV1))
{
BEAST_EXPECT(postAlice.value() == preAlice);
}
else
{
BEAST_EXPECT(postAlice.value() == preAlice);
}
BEAST_EXPECT(env.balance(bob, USD.issue()) == preBob - USD(0));
}
@@ -2311,16 +2333,12 @@ 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 = "";
// =========================================================================
// Cases that should succeed:
// valid well-formed UTF-8 that is not a noncharacter
// =========================================================================
// test utf-8 success
{
// case: kosme
uri = "κόσμε";
@@ -2353,83 +2371,133 @@ 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: 5 bytes (U-00200000) - beyond valid UTF-8 range
// 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)
uri = "\xF8\x88\x80\x80\x80";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// case: 6 bytes (U-04000000) - beyond valid UTF-8 range
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
// case: 6 bytes (U-04000000)
uri = "\xFC\x84\x80\x80\x80\x80";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
// BOUNDRY - END
// ----------------------------------------------------------------
// case: 3 bytes max (U-0000FFFF) - but this is U+FFFF
// noncharacter, rejected, see below
// case: 4 bytes max (U-001FFFFF) - beyond valid BMP
// 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));
// case: 5 bytes max (U-03FFFFFF) - beyond valid UTF-8
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));
// case: 6 bytes max (U-7FFFFFFF) - beyond valid UTF-8
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));
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
// BOUNDRY - OTHER
// // BOUNDRY - OTHER
// ----------------------------------------------------------------
// case: 1 bytes (U-0000D7FF) - incomplete 2-byte sequence
// case: 1 bytes (U-0000D7FF)
uri = "\xD7\xFF";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// case: 4 bytes (U-00110000) - beyond valid UTF-8 range
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)
uri = "\xF4\x90\x80\x80";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
env(uritoken::mint(alice, uri),
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
}
// =========================================================================
// Genuinely malformed UTF-8 sequences - always fail
// =========================================================================
// test utf8 malformed
{
// MALFORMED - START
// MALFORMED - END
// ----------------------------------------------------------------
// 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));
// All 16 first bytes of 3-byte sequences (0xe0-0xef), each
// followed by a space character
// 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
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 "
@@ -2438,26 +2506,31 @@ 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));
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
// 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));
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
// 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
// Impossible bytes
uri = "\xFE";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
@@ -2566,75 +2639,15 @@ struct URIToken_test : public beast::unit_test::suite
uri = "\xED\xAF\xBF\xED\xBF\xBF";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
// 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
// problematic noncharacters in 16-bit applications
// case: (U+FFFE)
uri = "\xEF\xBF\xBE";
env(uritoken::mint(alice, uri), ter(temMALFORMED));
uri = "\xEF\xBF\xBF"; // U+FFFF
// case: (U+FFFF)
uri = "\xEF\xBF\xBF";
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)
{
@@ -2669,6 +2682,7 @@ public:
using namespace test::jtx;
auto const sa = supported_amendments();
testWithFeats(sa);
testWithFeats(sa - fixXahauV1);
}
};
+18 -13
View File
@@ -4354,11 +4354,11 @@ struct XahauGenesis_test : public beast::unit_test::suite
using namespace std::chrono_literals;
testcase("test claim reward valid for L1 with unl report");
for (bool const withXahauV1 : {true, false})
{
Env env{
*this,
makeNetworkConfig(21337),
features - featureXahauGenesis};
FeatureBitset _features = features - featureXahauGenesis;
auto const amend = withXahauV1 ? _features : _features - fixXahauV1;
Env env{*this, makeNetworkConfig(21337), amend};
double const rateDrops = 0.00333333333 * 1'000'000;
STAmount const feesXRP = XRP(1);
@@ -4453,7 +4453,9 @@ struct XahauGenesis_test : public beast::unit_test::suite
accountKeyAndSle(*env.current(), alice);
// claim reward
env(claimReward(alice, env.master), fee(feesXRP), ter(tesSUCCESS));
auto const txResult =
withXahauV1 ? ter(tesSUCCESS) : ter(tecHOOK_REJECTED);
env(claimReward(alice, env.master), fee(feesXRP), txResult);
env.close();
// trigger emitted txn
@@ -4462,7 +4464,8 @@ struct XahauGenesis_test : public beast::unit_test::suite
// calculate rewards
STAmount const netReward =
rewardUserAmount(*acctSle, preLedger, rateDrops);
STAmount const l1Reward = rewardL1Amount(netReward, 20);
STAmount const l1Reward =
withXahauV1 ? rewardL1Amount(netReward, 20) : STAmount(0);
// validate govern rewards
BEAST_EXPECT(env.balance(bob) == preBob + l1Reward);
@@ -4472,14 +4475,16 @@ struct XahauGenesis_test : public beast::unit_test::suite
// validate account fields
STAmount const postAlice = preAlice + netReward + l1Reward;
bool const boolResult = withXahauV1 ? true : false;
bool const has240819 = env.current()->rules().enabled(fix240819);
BEAST_EXPECT(expectAccountFields(
env,
alice,
preLedger,
preLedger + 1,
has240819 ? (preAlice - feesXRP) : postAlice,
preTime));
BEAST_EXPECT(
expectAccountFields(
env,
alice,
preLedger,
preLedger + 1,
has240819 ? (preAlice - feesXRP) : postAlice,
preTime) == boolResult);
}
}
+1
View File
@@ -48,6 +48,7 @@ cat $INPUT_FILE | tr '\n' '\f' |
sed -E 's/\)\[test\.hook\]"[\f \t]*/\/*end*\//g' |
while read -r line
do
echo "/* ==== WASM: $COUNTER ==== */" >> $OUTPUT_FILE
echo -n '{ R"[test.hook](' >> $OUTPUT_FILE
cat <<< "$line" | sed -E 's/.{7}$//g' | tr -d '\n' | tr '\f' '\n' >> $OUTPUT_FILE
echo ')[test.hook]",' >> $OUTPUT_FILE
-69
View File
@@ -1,69 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <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,13 +56,6 @@ 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,7 +18,6 @@
//==============================================================================
#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>
@@ -28,16 +27,8 @@
#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 {
@@ -915,106 +906,6 @@ 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
{
@@ -1042,9 +933,6 @@ public:
testSignAndSubmit(all);
testFeatures(all);
testExceptionalShutdown();
testJSONRPCClientKeepAlive();
testJSONRPCClientSlowFirstRequest();
testJSONRPCClientReconnects();
}
};
+1 -3
View File
@@ -141,9 +141,7 @@ AMM::create(
if (fee_ != 0)
jv[sfFee] = std::to_string(fee_);
else
jv[jss::Fee] = std::to_string(
env_.current()->fees().increment.drops() +
env_.current()->fees().base.drops());
jv[jss::Fee] = std::to_string(env_.current()->fees().increment.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 + env.current()->fees().base;
return env.current()->fees().increment;
}
jtx::Env
+2 -53
View File
@@ -23,7 +23,6 @@
#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>
@@ -80,30 +79,6 @@ 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)
@@ -158,30 +133,10 @@ 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;
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();
}
read(stream_, bin_, res);
Json::Reader jr;
Json::Value jv;
@@ -198,12 +153,6 @@ public:
{
return rpc_version_;
}
unsigned
reconnects() const override
{
return reconnects_;
}
};
std::unique_ptr<AbstractClient>
File diff suppressed because it is too large. Load diff
-351
View File
@@ -1,351 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <xrpld/core/Job.h>
#include <xrpld/core/JobQueue.h>
#include <xrpld/net/RPCSub.h>
#include <xrpl/json/json_value.h>
#include <boost/asio.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <atomic>
#include <chrono>
#include <memory>
#include <string>
#include <thread>
namespace ripple {
namespace test {
// Minimal HTTP endpoint that counts received webhook POSTs and replies
// with a configurable status. Responses are EOF-delimited (no
// Content-Length) and the socket is closed right after writing — the
// exact shape that triggered the original handleData EOF-completion
// leak. So these tests exercise RPCSub flow control AND the HTTPClient
// EOF fix end to end: if either regressed, delivery would stall and the
// expected count would never be reached within the timeout.
class MockWebhookEndpoint
{
boost::asio::io_service ios_;
std::unique_ptr<boost::asio::io_service::work> work_;
boost::asio::ip::tcp::acceptor acceptor_;
std::thread thread_;
unsigned short port_;
std::atomic<int> received_{0};
std::atomic<int> status_{200};
std::atomic<int> delayMs_{0};
public:
MockWebhookEndpoint()
: work_(std::make_unique<boost::asio::io_service::work>(ios_))
, acceptor_(
ios_,
boost::asio::ip::tcp::endpoint(
boost::asio::ip::address::from_string("127.0.0.1"),
0))
{
port_ = acceptor_.local_endpoint().port();
accept();
thread_ = std::thread([this] { ios_.run(); });
}
~MockWebhookEndpoint()
{
work_.reset();
boost::system::error_code ec;
acceptor_.close(ec);
ios_.stop();
if (thread_.joinable())
thread_.join();
}
unsigned short
port() const
{
return port_;
}
int
received() const
{
return received_;
}
void
setStatus(int s)
{
status_ = s;
}
// Delay each reply so delivery is deterministically slower than the
// microsecond-fast enqueue loop — keeps the deque full for the
// queue-cap drop test regardless of scheduling.
void
setResponseDelay(int ms)
{
delayMs_ = ms;
}
private:
void
accept()
{
auto sock = std::make_shared<boost::asio::ip::tcp::socket>(ios_);
acceptor_.async_accept(*sock, [this, sock](auto ec) {
if (ec)
return;
handle(sock);
accept();
});
}
void
handle(std::shared_ptr<boost::asio::ip::tcp::socket> sock)
{
auto buf = std::make_shared<boost::asio::streambuf>();
boost::asio::async_read_until(
*sock, *buf, "\r\n\r\n", [this, sock, buf](auto ec, std::size_t) {
if (ec)
return;
++received_;
auto const delay = delayMs_.load();
if (delay > 0)
{
auto timer =
std::make_shared<boost::asio::steady_timer>(ios_);
timer->expires_from_now(std::chrono::milliseconds(delay));
timer->async_wait(
[this, sock, timer](auto) { reply(sock); });
}
else
{
reply(sock);
}
});
}
void
reply(std::shared_ptr<boost::asio::ip::tcp::socket> sock)
{
// EOF-delimited reply: no Content-Length, close after writing.
// This is the realistic failing-webhook shape.
auto resp = std::make_shared<std::string>(
"HTTP/1.0 " + std::to_string(status_.load()) +
" Reply\r\n\r\n{\"result\":{}}");
boost::asio::async_write(
*sock, boost::asio::buffer(*resp), [sock, resp](auto, std::size_t) {
boost::system::error_code ig;
sock->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ig);
sock->close(ig);
});
}
};
//------------------------------------------------------------------------------
class RPCSub_test : public beast::unit_test::suite
{
// Generous ceiling: the instrumented Debug (coverage) build is much
// slower than Release, so timeouts are sized for that, not Release.
template <class Cond>
bool
waitFor(Cond cond, std::chrono::seconds timeout = std::chrono::seconds{30})
{
auto const deadline = std::chrono::steady_clock::now() + timeout;
while (!cond() && std::chrono::steady_clock::now() < deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
return cond();
}
std::shared_ptr<RPCSub>
makeSub(
jtx::Env& env,
MockWebhookEndpoint& ep,
std::size_t maxQueueSize = 16384)
{
return make_RPCSub(
env.app().getOPs(),
env.app().getJobQueue(),
"http://127.0.0.1:" + std::to_string(ep.port()) + "/",
"",
"",
env.app().logs(),
maxQueueSize);
}
// True once no RPCSub sending job is queued or running. sendThread
// captures a raw `this`, so the RPCSub must not be destroyed while a
// job is still in flight — wait on this before letting the sub die.
bool
sendingIdle(jtx::Env& env)
{
return env.app().getJobQueue().getJobCountTotal(jtCLIENT_SUBSCRIBE) ==
0;
}
// Wait for all events to reach the endpoint AND the sending job to
// finish, so the sub can be torn down without racing sendThread.
void
drainAndSettle(jtx::Env& env, MockWebhookEndpoint& ep, int expected)
{
bool const delivered =
waitFor([&] { return ep.received() >= expected; });
bool const idle = waitFor([&] { return sendingIdle(env); });
log << " drainAndSettle: received=" << ep.received() << "/" << expected
<< " idle=" << idle << std::endl;
BEAST_EXPECT(delivered);
BEAST_EXPECT(idle);
}
void
send(std::shared_ptr<RPCSub> const& sub, int n)
{
Json::Value ev(Json::objectValue);
ev["n"] = n;
sub->send(ev, false);
}
void
testDelivery()
{
testcase("Webhook events are delivered");
using namespace jtx;
Env env{*this};
MockWebhookEndpoint ep;
static constexpr int N = 10;
{
auto sub = makeSub(env, ep);
for (int i = 0; i < N; ++i)
send(sub, i);
drainAndSettle(env, ep, N);
}
BEAST_EXPECT(ep.received() == N);
}
void
testErrorsDoNotStall()
{
testcase("Delivery continues when endpoint returns HTTP 500");
// The original bug (xrpld #6341): an endpoint returning errors
// without Content-Length never completed, stalling delivery to
// ALL subscribers. Here every response is a 500 with no
// Content-Length (EOF-delimited) — all N must still arrive.
using namespace jtx;
Env env{*this};
MockWebhookEndpoint ep;
ep.setStatus(500);
static constexpr int N = 10;
{
auto sub = makeSub(env, ep);
for (int i = 0; i < N; ++i)
send(sub, i);
drainAndSettle(env, ep, N);
}
BEAST_EXPECT(ep.received() == N);
}
void
testRestartAfterDrain()
{
testcase("Sending restarts after the queue drains");
// After a batch drains, sendThread clears mSending and returns.
// A later send() must start a fresh sending job; if mSending were
// left set (the #6341 failure mode) the second burst would never
// be delivered.
using namespace jtx;
Env env{*this};
MockWebhookEndpoint ep;
{
auto sub = makeSub(env, ep);
// First burst, then wait for the sending job to fully drain
// and exit (mSending cleared) — deterministically, not via a
// sleep.
for (int i = 0; i < 5; ++i)
send(sub, i);
drainAndSettle(env, ep, 5);
// Second burst must start a fresh sending job.
for (int i = 5; i < 10; ++i)
send(sub, i);
drainAndSettle(env, ep, 10);
}
BEAST_EXPECT(ep.received() == 10);
}
void
testQueueCapDrops()
{
testcase("Events past the queue cap are dropped");
// With a tiny cap, pushing far more events than delivery can keep
// up with forces send() down the drop path: enqueue is microsecond
// -fast while each (delayed) HTTP delivery is a full round-trip, so
// the deque sits at the cap and excess events are dropped. The
// delay makes "delivery slower than enqueue" hold regardless of
// scheduling, so this isn't timing-dependent. We just need some
// delivered (cap works) and some dropped (drop path exercised).
using namespace jtx;
Env env{*this};
MockWebhookEndpoint ep;
ep.setResponseDelay(50);
static constexpr int pushed = 50;
{
auto sub = makeSub(env, ep, /*maxQueueSize*/ 2);
for (int i = 0; i < pushed; ++i)
send(sub, i);
BEAST_EXPECT(waitFor([&] { return sendingIdle(env); }));
}
log << " queue cap: received " << ep.received() << "/" << pushed
<< std::endl;
BEAST_EXPECT(ep.received() > 0);
BEAST_EXPECT(ep.received() < pushed);
}
public:
void
run() override
{
testDelivery();
testErrorsDoNotStall();
testRestartAfterDrain();
testQueueCapDrops();
}
};
BEAST_DEFINE_TESTSUITE(RPCSub, net, ripple);
} // namespace test
} // namespace ripple
-250
View File
@@ -24,8 +24,6 @@
#include <test/jtx/WSClient.h>
#include <test/rpc/GRPCTestClientBase.h>
#include <xrpld/rpc/GRPCHandlers.h>
#include <xrpl/hook/Misc.h>
#include <xrpl/protocol/InnerObjectFormats.h>
#include <xrpl/resource/Charge.h>
#include <xrpl/resource/Fees.h>
@@ -123,252 +121,6 @@ public:
}
}
void
testHooksRequest()
{
testcase("Hooks request");
using namespace jtx;
Env env(*this);
Account const alice{"alice"};
env.fund(XRP(1000), alice);
auto request = [&](Json::Value const& hooks, unsigned apiVersion) {
Json::Value params;
params[jss::account] = alice.human();
params[jss::hooks] = hooks;
if (apiVersion == 1)
return env.rpc("json", "account_info", to_string(params));
params[jss::api_version] = 2;
return env.rpc("json", "account_info", to_string(params));
};
for (Json::Value const& invalid :
{Json::Value(1),
Json::Value(1.1),
Json::Value("true"),
Json::Value(Json::nullValue),
Json::Value(Json::objectValue),
Json::Value(Json::arrayValue)})
{
for (unsigned const apiVersion : {1u, 2u})
{
auto const info = request(invalid, apiVersion);
BEAST_EXPECT(info[jss::result][jss::error] == "invalidParams");
BEAST_EXPECT(
info[jss::result][jss::error_message] ==
"Invalid field 'hooks'.");
}
}
auto const withoutHooks = env.rpc(
"json",
"account_info",
R"({"account": ")" + alice.human() + R"("})");
BEAST_EXPECT(!withoutHooks[jss::result].isMember(jss::hooks));
auto const explicitlyFalse = env.rpc(
"json",
"account_info",
R"({"account": ")" + alice.human() + R"(","hooks":false})");
BEAST_EXPECT(!explicitlyFalse[jss::result].isMember(jss::hooks));
auto const explicitlyTrue = env.rpc(
"json",
"account_info",
std::string{"{ "} + "\"account\": \"" + alice.human() +
"\", \"hooks\": true }");
BEAST_EXPECT(explicitlyTrue[jss::result][jss::hooks].isArray());
BEAST_EXPECT(explicitlyTrue[jss::result][jss::hooks].size() == 0);
}
void
testHooksEffectiveValues()
{
testcase("Effective hook values");
auto const* hookTemplate =
InnerObjectFormats::getInstance().findSOTemplateBySField(sfHook);
if (!BEAST_EXPECTS(hookTemplate != nullptr, "sfHook template"))
return;
// If this count changes, add or remove tests for the corresponding Hook
// fields.
BEAST_EXPECT(hookTemplate->size() == 12);
using namespace jtx;
Env env(*this);
Account const alice{"alice"};
env.fund(XRP(1000), alice);
uint256 const definitionNamespace{1};
uint256 const accountNamespace{2};
auto const makeParameter = [](std::string const& name,
std::string const& value) {
Json::Value parameter;
parameter[jss::HookParameter][jss::HookParameterName] = name;
parameter[jss::HookParameter][jss::HookParameterValue] = value;
return parameter;
};
auto create = hso(genesis::AcceptHook);
create[jss::Flags] = hsfCOLLECT;
create[jss::HookName] = strHex(std::string{"ACCT"});
create[jss::HookNamespace] = to_string(definitionNamespace);
create[jss::HookParameters] = Json::arrayValue;
create[jss::HookParameters].append(makeParameter("AA", "BB"));
create[jss::HookParameters].append(makeParameter("CC", "DD"));
env(hook(alice, {{create, Json::Value{}}}, 0),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
auto const hookSLE = env.le(keylet::hook(alice.id()));
BEAST_EXPECT(hookSLE && hookSLE->isFieldPresent(sfHooks));
if (!hookSLE || !hookSLE->isFieldPresent(sfHooks))
return;
auto const& installed = hookSLE->getFieldArray(sfHooks)[0];
BEAST_EXPECT(installed.isFieldPresent(sfHookHash));
if (!installed.isFieldPresent(sfHookHash))
return;
auto const hash = installed.getFieldH256(sfHookHash);
auto const hookDefinition = env.le(keylet::hookDefinition(hash));
BEAST_EXPECT(hookDefinition);
if (hookDefinition)
{
BEAST_EXPECT(!hookDefinition->isFieldPresent(sfHookName));
BEAST_EXPECT(!hookDefinition->isFieldPresent(sfHookGrants));
BEAST_EXPECT(hookDefinition->isFieldPresent(sfHookApiVersion));
BEAST_EXPECT(hookDefinition->getFieldU16(sfHookApiVersion) == 0);
}
auto accountInfo = env.rpc(
"json",
"account_info",
R"({"account": ")" + alice.human() + R"(","hooks":true})");
auto const& hookJson =
accountInfo[jss::result][jss::hooks][0U][sfHook.jsonName];
BEAST_EXPECT(hookJson[sfHookHash.jsonName] == to_string(hash));
BEAST_EXPECT(!hookJson.isMember(sfCreateCode.jsonName));
BEAST_EXPECT(
hookJson[sfHookNamespace.jsonName] ==
to_string(definitionNamespace));
BEAST_EXPECT(
hookJson[sfHookName.jsonName].isString() &&
hookJson[sfHookName.jsonName] == strHex(std::string{"ACCT"}));
BEAST_EXPECT(
hookJson[sfHookApiVersion.jsonName].isInt() &&
hookJson[sfHookApiVersion.jsonName].asUInt() == 0);
BEAST_EXPECT(hookJson[sfHookOn.jsonName] == to_string(uint256{0}));
BEAST_EXPECT(!hookJson.isMember(sfHookOnIncoming.jsonName));
BEAST_EXPECT(!hookJson.isMember(sfHookOnOutgoing.jsonName));
BEAST_EXPECT(
hookJson[sfHookCanEmit.jsonName] ==
to_string(UINT256_BIT[ttHOOK_SET]));
BEAST_EXPECT(hookJson[sfFlags.jsonName] == hsfCOLLECT);
BEAST_EXPECT(hookJson[sfHookParameters.jsonName].size() == 2);
auto const& hooksJson = accountInfo[jss::result][jss::hooks];
BEAST_EXPECT(hooksJson.isArray());
BEAST_EXPECT(hooksJson.size() == 2);
if (!hooksJson.isArray() || hooksJson.size() < 2)
return;
BEAST_EXPECT(hooksJson[1U].isObject());
BEAST_EXPECT(hooksJson[1U].isMember(sfHook.jsonName));
if (!hooksJson[1U].isObject() ||
!hooksJson[1U].isMember(sfHook.jsonName))
return;
auto const& blankHook = hooksJson[1U][sfHook.jsonName];
BEAST_EXPECT(blankHook.isObject());
BEAST_EXPECT(blankHook.size() == 0);
Json::Value update;
update[jss::HookHash] = to_string(hash);
update[jss::Flags] = hsfOVERRIDE;
update[jss::HookName] = strHex(std::string{"ACCT"});
update[jss::HookGrants] = Json::arrayValue;
update[jss::HookGrants][0U][jss::HookGrant][jss::HookHash] =
to_string(hash);
update[jss::HookNamespace] = to_string(accountNamespace);
update[jss::HookOnIncoming] = to_string(uint256{5});
update[jss::HookOnOutgoing] = to_string(uint256{6});
update[jss::HookCanEmit] = to_string(uint256{4});
update[jss::HookParameters] = Json::arrayValue;
update[jss::HookParameters].append(makeParameter("AA", ""));
update[jss::HookParameters].append(makeParameter("EE", "FF"));
env(hook(alice, {{update}}, 0), fee(XRP(1)), ter(tesSUCCESS));
env.close();
accountInfo = env.rpc(
"json",
"account_info",
R"({"account": ")" + alice.human() + R"(","hooks":true})");
auto const& overridden =
accountInfo[jss::result][jss::hooks][0U][sfHook.jsonName];
BEAST_EXPECT(
overridden[sfHookNamespace.jsonName] ==
to_string(accountNamespace));
BEAST_EXPECT(
overridden[sfHookName.jsonName].isString() &&
overridden[sfHookName.jsonName] == strHex(std::string{"ACCT"}));
BEAST_EXPECT(
overridden[sfHookApiVersion.jsonName].isInt() &&
overridden[sfHookApiVersion.jsonName].asUInt() == 0);
BEAST_EXPECT(overridden[sfHookGrants.jsonName].isArray());
BEAST_EXPECT(overridden[sfHookGrants.jsonName].size() == 1);
if (overridden[sfHookGrants.jsonName].isArray() &&
overridden[sfHookGrants.jsonName].size() == 1)
{
auto const& grant = overridden[sfHookGrants.jsonName][0U];
BEAST_EXPECT(grant.isObject());
BEAST_EXPECT(grant.size() == 1);
BEAST_EXPECT(grant.isMember(sfHookGrant.jsonName));
if (grant.isObject() && grant.size() == 1 &&
grant.isMember(sfHookGrant.jsonName))
{
auto const& grantObject = grant[sfHookGrant.jsonName];
BEAST_EXPECT(grantObject.isObject());
BEAST_EXPECT(grantObject.size() == 1);
BEAST_EXPECT(grantObject.isMember(sfHookHash.jsonName));
if (grantObject.isObject() && grantObject.size() == 1 &&
grantObject.isMember(sfHookHash.jsonName))
BEAST_EXPECT(
grantObject[sfHookHash.jsonName] == to_string(hash));
}
}
BEAST_EXPECT(overridden[sfFlags.jsonName] == 0);
BEAST_EXPECT(!overridden.isMember(sfHookOn.jsonName));
BEAST_EXPECT(
overridden[sfHookOnIncoming.jsonName] == to_string(uint256{5}));
BEAST_EXPECT(
overridden[sfHookOnOutgoing.jsonName] == to_string(uint256{6}));
BEAST_EXPECT(
overridden[sfHookCanEmit.jsonName] == to_string(uint256{4}));
BEAST_EXPECT(overridden[sfHookParameters.jsonName].size() == 3);
if (overridden[sfHookParameters.jsonName].size() != 3)
return;
auto expectParameter = [&](std::string const& name,
std::string const& value) {
bool found = false;
for (auto const& parameter : overridden[sfHookParameters.jsonName])
{
if (!parameter.isObject() ||
!parameter.isMember(sfHookParameter.jsonName))
continue;
auto const& object = parameter[sfHookParameter.jsonName];
if (!object.isObject() ||
object[sfHookParameterName.jsonName] != name)
continue;
found = true;
BEAST_EXPECT(object[sfHookParameterValue.jsonName] == value);
break;
}
BEAST_EXPECT(found);
};
expectParameter("AA", "");
expectParameter("CC", "DD");
expectParameter("EE", "FF");
}
// Test the "signer_lists" argument in account_info.
void
testSignerLists()
@@ -934,8 +686,6 @@ public:
run() override
{
testErrors();
testHooksRequest();
testHooksEffectiveValues();
testSignerLists();
testSignerListsApiVersion2();
testSignerListsV2();
+32 -40
View File
@@ -37,36 +37,20 @@ namespace test {
static char const* bobs_account_objects[] = {
R"json({
"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"
"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"
})json",
R"json({
"Balance" : {
@@ -111,6 +95,22 @@ 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 + 2]);
BEAST_EXPECT(aobj == bobj[i + 1]);
}
}
// test stepped one-at-a-time with limit=1, resume from prev marker
@@ -1135,15 +1135,7 @@ public:
return types == typesOut;
};
// Find AMM objects
// 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 amm(env, gw, XRP(1'000), USD(1'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] == gw.human());
BEAST_EXPECT(jroOuter[0u][jss::taker_gets][jss::value] == "2");
BEAST_EXPECT(jroOuter[0u][jss::taker_pays] == "200000000");
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");
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] == bob.human());
BEAST_EXPECT(jroOuter[2u][jss::taker_gets][jss::value] == "1");
BEAST_EXPECT(jroOuter[2u][jss::taker_pays] == "100000000");
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");
}
{
-543
View File
@@ -19,548 +19,15 @@
#include <test/jtx.h>
#include <test/jtx/WSClient.h>
#include <test/jtx/envconfig.h>
#include <xrpld/app/consensus/RCLValidations.h>
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#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/core/ConfigSections.h>
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/protocol/BuildInfo.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <chrono>
#include <cstring>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
namespace ripple {
class AmendmentBlocked_test : public beast::unit_test::suite
{
// An amendment id this binary will never support.
static uint256
unsupportedAmendmentId()
{
std::string const in = "AmendmentBlocked_test.unsupported";
sha256_hasher h;
using beast::hash_append;
hash_append(h, in);
auto const d = static_cast<sha256_hasher::result_type>(h);
uint256 result;
std::memcpy(result.data(), d.data(), d.size());
return result;
}
// The majority period only sets how far out activation is expected, so
// shorten it from two weeks. That keeps the ledger close-time jumps in
// these tests down to minutes, and makes the relationship to the
// five-minute shutdown lead time in LedgerMaster obvious.
static std::unique_ptr<Config>
shortMajorityConfig()
{
using namespace std::chrono_literals;
auto cfg = test::jtx::envconfig();
cfg->AMENDMENT_MAJORITY_TIME = 15min;
return cfg;
}
// Give the amendment table a majority, as of now, for an amendment we do
// not support; activation is then expected one majority period out.
// Bypasses the ReadView overload (and therefore needValidatedLedger) on
// purpose: because lastUpdateSeq_ is set to the current sequence, later
// closes within the same 256-ledger block will not recompute -- and so
// will not clear -- what we inject here.
void
injectUnsupportedMajority(test::jtx::Env& env)
{
auto const seq = env.closed()->info().seq;
majorityAmendments_t majority;
majority[unsupportedAmendmentId()] = env.now();
env.app().getAmendmentTable().doValidatedLedger(seq, {}, majority);
auto const first =
env.app().getAmendmentTable().firstUnsupportedExpected();
BEAST_EXPECT(
first &&
*first == env.now() + env.app().config().AMENDMENT_MAJORITY_TIME);
}
void
testReceiptFileHelpers()
{
testcase("amendment blocked receipt helpers");
auto const id = unsupportedAmendmentId();
{
beast::temp_dir td;
Config cfg;
cfg.CONFIG_DIR = td.path();
auto const path = amendmentBlockedFilePath(cfg);
BEAST_EXPECT(path.filename() == "README_AMENDMENT_BLOCKED");
BEAST_EXPECT(
path.parent_path() == boost::filesystem::path{td.path()});
BEAST_EXPECT(!boost::filesystem::exists(path));
// Nothing to remove yet, and that is not an error.
boost::system::error_code ec;
BEAST_EXPECT(!removeAmendmentBlockedFile(cfg, ec));
BEAST_EXPECT(!ec);
BEAST_EXPECT(!writeAmendmentBlockedFile(
cfg, {to_string(id) + " (already active)"}));
BEAST_EXPECT(boost::filesystem::exists(path));
auto const contents = getFileContents(ec, path);
BEAST_EXPECT(!ec);
BEAST_EXPECT(
contents.find("XAHAUD STOPPED: UPGRADE REQUIRED") == 0);
// When it stopped, what was running, and what it choked on.
BEAST_EXPECT(contents.find("Stopped at:") != std::string::npos);
BEAST_EXPECT(
contents.find(BuildInfo::getVersionString()) !=
std::string::npos);
BEAST_EXPECT(contents.find(to_string(id)) != std::string::npos);
BEAST_EXPECT(contents.find("already active") != std::string::npos);
BEAST_EXPECT(contents.find("Upgrade xahaud") != std::string::npos);
// The receipt is self-clearing, so it must not tell the operator
// to delete anything.
BEAST_EXPECT(
contents.find("removed automatically") != std::string::npos);
// The timestamp is rendered, not a placeholder: to_string_iso
// gives YYYY-MM-DDTHH:MM:SSZ.
auto const stampAt = contents.find("Stopped at:");
if (BEAST_EXPECT(stampAt != std::string::npos))
{
auto const eol = contents.find('\n', stampAt);
auto const line = contents.substr(stampAt, eol - stampAt);
BEAST_EXPECT(line.find("20") != std::string::npos);
BEAST_EXPECT(line.find('T') != std::string::npos);
BEAST_EXPECT(line.find('Z') != std::string::npos);
}
// Now it can be removed, and removal is reported.
ec.clear();
BEAST_EXPECT(removeAmendmentBlockedFile(cfg, ec));
BEAST_EXPECT(!ec);
BEAST_EXPECT(!boost::filesystem::exists(path));
// With no amendments to name the receipt still says something
// useful.
BEAST_EXPECT(!writeAmendmentBlockedFile(cfg, {}));
ec.clear();
auto const bare = getFileContents(ec, path);
BEAST_EXPECT(!ec);
BEAST_EXPECT(
bare.find("not supported by this build") != std::string::npos);
}
// A directory we cannot write to must surface an error rather than
// throw. The shutdown continues either way; only the receipt is lost,
// which is what happens on installs where CONFIG_DIR is read-only for
// the account xahaud runs as.
{
Config cfg;
cfg.CONFIG_DIR =
boost::filesystem::path{"/"} / "no" / "such" / "directory";
BEAST_EXPECT(!!writeAmendmentBlockedFile(cfg, {}));
boost::system::error_code ec;
BEAST_EXPECT(!removeAmendmentBlockedFile(cfg, ec));
}
}
void
testStandaloneDoesNotStop()
{
testcase("standalone does not stop or leave a receipt");
using namespace test::jtx;
beast::temp_dir td;
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
cfg->CONFIG_DIR = td.path();
return cfg;
})};
BEAST_EXPECT(env.app().config().standalone());
auto const path = amendmentBlockedFilePath(env.app().config());
env.app().getOPs().setAmendmentBlocked();
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
BEAST_EXPECT(!env.app().getOPs().isAmendmentWarned());
BEAST_EXPECT(!env.app().isStopping());
BEAST_EXPECT(!boost::filesystem::exists(path));
}
void
testShutdownOnBlock()
{
testcase("amendment blocked stops the server");
using namespace test::jtx;
// A non-standalone Env is needed to exercise shutdown rather than
// only setting the blocked flag. Consequences when editing:
// setup() arms the state timer, run() arms the deadlock detector, and
// signalStop() below releases run() to tear the application down
// concurrently with the rest of this function. Do all the assertions
// straight away and let the Env go out of scope promptly.
beast::temp_dir td;
Env env{*this, envconfig([&](std::unique_ptr<Config> config) {
config->NODE_SIZE = 0;
config->setupControl(true, true, false);
// setupControl picks a production node size for
// non-standalone; put it back to "tiny" for the test.
config->NODE_SIZE = 0;
config->CONFIG_DIR = td.path();
config->legacy("database_path", td.path());
return config;
})};
BEAST_EXPECT(!env.app().config().standalone());
auto const path = amendmentBlockedFilePath(env.app().config());
BEAST_EXPECT(path.filename() == "README_AMENDMENT_BLOCKED");
BEAST_EXPECT(!boost::filesystem::exists(path));
BEAST_EXPECT(!env.app().isStopping());
// Make the table report an unsupported amendment so the receipt has
// something concrete to name.
auto const id = unsupportedAmendmentId();
env.app().getAmendmentTable().enable(id);
BEAST_EXPECT(env.app().getAmendmentTable().hasUnsupportedEnabled());
env.app().getOPs().setAmendmentBlocked();
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
BEAST_EXPECT(env.app().isStopping());
BEAST_EXPECT(boost::filesystem::exists(path));
boost::system::error_code readError;
auto const contents = getFileContents(readError, path);
BEAST_EXPECT(!readError);
BEAST_EXPECT(
contents.find("XAHAUD STOPPED: UPGRADE REQUIRED") !=
std::string::npos);
BEAST_EXPECT(contents.find("Upgrade xahaud") != std::string::npos);
BEAST_EXPECT(contents.find(to_string(id)) != std::string::npos);
BEAST_EXPECT(contents.find("already active") != std::string::npos);
// Repeat calls are a no-op: Change::applyAmendment reaches this from
// the transaction apply path without an isBlocked() guard, so it must
// not rewrite the receipt or re-log once per ledger.
BEAST_EXPECT(boost::filesystem::remove(path));
env.app().getOPs().setAmendmentBlocked();
BEAST_EXPECT(!boost::filesystem::exists(path));
}
void
checkJump(bool unsupported)
{
using namespace test::jtx;
beast::temp_dir td;
Env env{*this, envconfig([&](std::unique_ptr<Config> cfg) {
cfg->setupControl(true, true, false);
cfg->NODE_SIZE = 0;
cfg->CONFIG_DIR = td.path();
cfg->legacy("database_path", td.path());
return cfg;
})};
auto& app = env.app();
auto& ops = app.getOPs();
auto& master = app.getLedgerMaster();
std::lock_guard lock(app.getMasterMutex());
auto const before = master.getClosedLedger();
// A direct child is not preferred over our LCL: consensus may be
// about to build it. Two ledgers ahead forces the JUMP path.
auto parent =
std::make_shared<Ledger>(*before, app.timeKeeper().closeTime());
parent->updateSkipList();
parent->setImmutable();
auto candidate =
std::make_shared<Ledger>(*parent, app.timeKeeper().closeTime());
candidate->updateSkipList();
if (unsupported)
{
auto const key = keylet::amendments();
auto const existing = candidate->read(key);
auto sle = existing ? std::make_shared<SLE>(*existing)
: std::make_shared<SLE>(key);
STVector256 amendments;
if (sle->isFieldPresent(sfAmendments))
amendments = sle->getFieldV256(sfAmendments);
amendments.push_back(unsupportedAmendmentId());
sle->setFieldV256(sfAmendments, amendments);
if (existing)
candidate->rawReplace(sle);
else
candidate->rawInsert(sle);
}
// A serialized uint32 with an unknown field number. Insert raw bytes
// so the real transaction parser, reached through TxQ, must throw.
BEAST_EXPECT(SField::getField(STI_UINT32, 255).isInvalid());
auto tx = std::make_shared<Serializer>();
tx->add8(0x20);
tx->add8(255);
while (tx->getDataLength() < txMinSizeBytes)
tx->add8(0);
auto meta = std::make_shared<Serializer>();
meta->add8(0xE1);
auto const txID = sha512Half(tx->slice());
candidate->rawTxInsert(txID, tx, meta);
candidate->setImmutable();
bool unknownField = false;
try
{
candidate->txRead(txID);
}
catch (std::runtime_error const& e)
{
unknownField = std::string(e.what()).starts_with("Unknown field");
}
if (!BEAST_EXPECT(unknownField))
return;
master.storeLedger(candidate);
auto const keys = randomKeyPair(KeyType::secp256k1);
auto const nodeID = calcNodeID(keys.first);
auto validation = std::make_shared<STValidation>(
app.timeKeeper().closeTime(),
keys.first,
keys.second,
nodeID,
[&](STValidation& v) {
v.setFieldH256(sfLedgerHash, candidate->info().hash);
v.setFieldU32(sfLedgerSequence, candidate->seq());
});
// Add directly so ledger acceptance does not discover the amendment
// before JUMP has a chance to inspect the candidate itself.
BEAST_EXPECT(
app.getValidations().add(nodeID, RCLValidation{validation}) ==
ValStatus::current);
if (!BEAST_EXPECT(
app.getValidations().getPreferredLCL(
RCLValidatedLedger{before, env.journal},
master.getValidLedgerIndex(),
{}) == candidate->info().hash))
return;
BEAST_EXPECT(!app.getAmendmentTable().hasUnsupportedEnabled());
BEAST_EXPECT(!app.getAmendmentTable().firstUnsupportedExpected());
BEAST_EXPECT(!ops.isAmendmentBlocked());
BEAST_EXPECT(!app.isStopping());
auto const receipt = amendmentBlockedFilePath(app.config());
BEAST_EXPECT(!boost::filesystem::exists(receipt));
bool threw = false;
try
{
ops.endConsensus({});
}
catch (std::runtime_error const& e)
{
threw = true;
BEAST_EXPECT(std::string(e.what()).starts_with("Unknown field"));
}
BEAST_EXPECT(threw == !unsupported);
BEAST_EXPECT(ops.isAmendmentBlocked() == unsupported);
BEAST_EXPECT(app.isStopping() == unsupported);
BEAST_EXPECT(boost::filesystem::exists(receipt) == unsupported);
BEAST_EXPECT(
master.getClosedLedger()->info().hash == before->info().hash);
}
void
testJumpStopsForUnsupportedAmendment()
{
testcase("JUMP to an unsupported ledger stops the server");
checkJump(/*unsupported=*/true);
}
void
testJumpRethrowsParsingError()
{
testcase("JUMP parsing errors without unsupported amendments escape");
checkJump(/*unsupported=*/false);
}
void
testWarnsOutsideShutdownWindow()
{
testcase("unsupported majority warns while activation is distant");
using namespace test::jtx;
Env env{*this, shortMajorityConfig()};
BEAST_EXPECT(!env.app().getOPs().isBlocked());
BEAST_EXPECT(env.close());
injectUnsupportedMajority(env);
BEAST_EXPECT(env.close());
// A majority period out: warn, keep running.
BEAST_EXPECT(env.app().getOPs().isAmendmentWarned());
BEAST_EXPECT(!env.app().getOPs().isAmendmentBlocked());
// The table can name the amendment, and reports it as pending rather
// than active. This is what ends up in the receipt.
auto const unsupported =
env.app().getAmendmentTable().unsupportedAmendments();
BEAST_EXPECT(unsupported.size() == 1);
if (unsupported.size() == 1)
{
BEAST_EXPECT(unsupported[0].id == unsupportedAmendmentId());
BEAST_EXPECT(
unsupported[0].expected ==
env.app().getAmendmentTable().firstUnsupportedExpected());
}
// Because firstUnsupportedExpected() is set, the warning carries the
// expected activation date.
auto const si = env.rpc("server_info")[jss::result];
BEAST_EXPECT(si.isMember(jss::info));
auto const& warnings = si[jss::info][jss::warnings];
BEAST_EXPECT(warnings.isArray() && warnings.size() == 1);
if (warnings.isArray() && warnings.size() == 1)
{
BEAST_EXPECT(
warnings[0u][jss::id].asInt() == warnRPC_UNSUPPORTED_MAJORITY);
auto const& details = warnings[0u][jss::details];
BEAST_EXPECT(details.isMember(jss::expected_date));
BEAST_EXPECT(details.isMember(jss::expected_date_UTC));
}
}
void
testUnsupportedAmendmentReporting()
{
testcase("unsupported amendments are reported for the receipt");
using namespace test::jtx;
Env env{*this, shortMajorityConfig()};
auto& table = env.app().getAmendmentTable();
BEAST_EXPECT(table.unsupportedAmendments().empty());
// Majority, not yet active -> reported with an expected time.
BEAST_EXPECT(env.close());
injectUnsupportedMajority(env);
auto pending = table.unsupportedAmendments();
BEAST_EXPECT(pending.size() == 1);
if (pending.size() == 1)
BEAST_EXPECT(pending[0].expected.has_value());
// Majority lost -> nothing to report. The list is recomputed from the
// ledger each time, so it must not accumulate.
auto const seq = env.closed()->info().seq;
table.doValidatedLedger(seq, {}, {});
BEAST_EXPECT(table.unsupportedAmendments().empty());
BEAST_EXPECT(!table.firstUnsupportedExpected());
// Active -> reported with no expected time.
BEAST_EXPECT(table.enable(unsupportedAmendmentId()));
BEAST_EXPECT(table.hasUnsupportedEnabled());
auto const active = table.unsupportedAmendments();
BEAST_EXPECT(active.size() == 1);
if (active.size() == 1)
{
BEAST_EXPECT(active[0].id == unsupportedAmendmentId());
BEAST_EXPECT(!active[0].expected);
}
}
void
testWarningVisibleWithoutAdmin()
{
testcase("unsupported majority warning is not admin only");
using namespace test::jtx;
// No closes here: ledger_accept is Role::ADMIN, server_info is
// Role::USER, which is the whole point of the test.
Env env{*this, envconfig(no_admin)};
env.app().getOPs().setAmendmentWarned();
BEAST_EXPECT(env.app().getOPs().isAmendmentWarned());
auto const si = env.rpc("server_info")[jss::result];
BEAST_EXPECT(si.isMember(jss::info));
auto const& warnings = si[jss::info][jss::warnings];
BEAST_EXPECT(warnings.isArray() && warnings.size() == 1);
if (warnings.isArray() && warnings.size() == 1)
{
BEAST_EXPECT(
warnings[0u][jss::id].asInt() == warnRPC_UNSUPPORTED_MAJORITY);
}
}
void
testShutsDownInsideShutdownWindow()
{
testcase("unsupported majority stops the server before activation");
using namespace test::jtx;
Env env{*this, shortMajorityConfig()};
BEAST_EXPECT(!env.app().getOPs().isBlocked());
BEAST_EXPECT(env.close());
injectUnsupportedMajority(env);
// First close only warns: activation is still a majority period away.
BEAST_EXPECT(env.close());
BEAST_EXPECT(env.app().getOPs().isAmendmentWarned());
BEAST_EXPECT(!env.app().getOPs().isAmendmentBlocked());
auto const expected =
*env.app().getAmendmentTable().firstUnsupportedExpected();
// Close two minutes short of the expected activation time. This is not
// a flag ledger and the warning has already been issued, so the check
// has to run on every validated ledger to see this at all.
BEAST_EXPECT(env.close(expected - NetClock::duration{120}));
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
BEAST_EXPECT(!env.app().getOPs().isAmendmentWarned());
// Standalone, so the flag is set but the server keeps running.
BEAST_EXPECT(!env.app().isStopping());
}
void
testShutsDownWhenActivationOverdue()
{
testcase("unsupported majority stops the server when overdue");
using namespace test::jtx;
Env env{*this, shortMajorityConfig()};
BEAST_EXPECT(!env.app().getOPs().isBlocked());
BEAST_EXPECT(env.close());
injectUnsupportedMajority(env);
auto const expected =
*env.app().getAmendmentTable().firstUnsupportedExpected();
// firstUnsupportedExpected() is a lower bound: the amendment actually
// activates at the first flag ledger at or after it, so the server can
// legitimately still be running an hour past it. That is the most
// dangerous state, not the safest, and must stop the server.
BEAST_EXPECT(env.close(expected + NetClock::duration{3600}));
BEAST_EXPECT(env.app().getOPs().isAmendmentBlocked());
BEAST_EXPECT(!env.app().getOPs().isAmendmentWarned());
}
void
testBlockedMethods()
{
@@ -783,16 +250,6 @@ public:
void
run() override
{
testReceiptFileHelpers();
testStandaloneDoesNotStop();
testShutdownOnBlock();
testJumpRethrowsParsingError();
testJumpStopsForUnsupportedAmendment();
testWarnsOutsideShutdownWindow();
testUnsupportedAmendmentReporting();
testWarningVisibleWithoutAdmin();
testShutsDownInsideShutdownWindow();
testShutsDownWhenActivationOverdue();
testBlockedMethods();
}
};
+8 -128
View File
@@ -163,7 +163,7 @@ public:
void
testNoParams(FeatureBitset features)
{
testcase("Default Env: config-forced, none on-ledger");
testcase("No Params, None Enabled");
using namespace test::jtx;
Env env{*this};
@@ -178,6 +178,8 @@ 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);
@@ -186,12 +188,8 @@ 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 &&
@@ -210,7 +208,7 @@ public:
void
testSomeEnabled(FeatureBitset features)
{
testcase("Two config-forced, none on-ledger");
testcase("No Params, Some Enabled");
using namespace test::jtx;
Env env{
@@ -230,10 +228,7 @@ public:
(void)id.parseHex(it.key().asString().c_str());
if (!BEAST_EXPECT((*it).isMember(jss::name)))
return;
bool const expectOnLedger =
env.app().getAmendmentTable().isEnabled(id);
bool const expectForced =
id == featureDepositAuth || id == featureDepositPreauth;
bool expectEnabled = env.app().getAmendmentTable().isEnabled(id);
bool expectSupported =
env.app().getAmendmentTable().isSupported(id);
bool expectVeto =
@@ -244,14 +239,9 @@ public:
VoteBehavior::Obsolete);
BEAST_EXPECTS(
(*it).isMember(jss::enabled) &&
(*it)[jss::enabled].asBool() ==
(expectOnLedger || expectForced),
(*it)[jss::enabled].asBool() == expectEnabled,
(*it)[jss::name].asString() + " enabled");
BEAST_EXPECTS(
(*it).isMember(jss::ledger_enabled) &&
(*it)[jss::ledger_enabled].asBool() == expectOnLedger,
(*it)[jss::name].asString() + " ledger_enabled");
if (expectOnLedger)
if (expectEnabled)
BEAST_EXPECTS(
!(*it).isMember(jss::vetoed),
(*it)[jss::name].asString() + " vetoed");
@@ -370,122 +360,12 @@ 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
@@ -21,9 +21,7 @@
#include <xrpld/app/ledger/InboundLedger.h>
#include <xrpld/app/ledger/InboundLedgers.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/misc/ValidatorList.h>
#include <xrpld/consensus/LedgerTiming.h>
@@ -33,7 +31,6 @@
#include <xrpl/basics/Log.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/protocol/Feature.h>
#include <memory>
#include <mutex>
#include <thread>
@@ -179,25 +176,6 @@ handleNewValidation(
auto const& hash = val->getLedgerHash();
auto const seq = val->getFieldU32(sfLedgerSequence);
// A validator that rotated its ephemeral key while this node was not
// listening signs with a key no held manifest mentions, which is
// indistinguishable from a validator this node has never heard of. Every
// manifest published on-chain is written at its ephemeral keylet as well
// as its master one, so that binding is recoverable in a single read.
//
// Done here rather than left to the next consensus round, because
// ManifestCache::applyLedger() runs there against the master keys already
// trusted and so cannot resolve a key whose master is what is missing.
// Resolving now also means this validation is classified in the round it
// arrived in rather than written off along with every other one until the
// trusted set is next recomputed.
if (app.validatorManifests().getMasterKey(signingKey) == signingKey)
{
if (auto const view = app.openLedger().current();
view && view->rules().enabled(featureOnChainManifests))
app.validatorManifests().applyLedgerSigningKey(*view, signingKey);
}
// Ensure validation is marked as trusted if signer currently trusted
auto masterKey = app.validators().getTrustedKey(signingKey);
+1 -4
View File
@@ -82,10 +82,7 @@ namespace hook_api {
namespace hook {
bool
canHook(
STTx const& tx,
ripple::uint256 hookOn,
std::optional<ripple::Slice> hookName);
canHook(ripple::TxType txType, ripple::uint256 hookOn);
bool
canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit);
+24 -94
View File
@@ -101,14 +101,6 @@ 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();
@@ -173,18 +165,6 @@ 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?
@@ -279,13 +259,6 @@ 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))
{
@@ -306,9 +279,6 @@ 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);
@@ -868,22 +838,14 @@ HookAPI::etxn_fee_base(ripple::Slice const& txBlob) const
std::unique_ptr<STTx const> stpTrans =
std::make_unique<STTx const>(std::ref(sitTrans));
// 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();
}
if (!hookCtx.applyCtx.view().rules().enabled(fixHookAPI20251128))
return Transactor::calculateBaseFee(*feeView, *stpTrans).drops();
return Transactor::calculateBaseFee(
*(applyCtx.app.openLedger().current()), *stpTrans)
.drops();
return invoke_calculateBaseFee(*feeView, *stpTrans).drops();
return invoke_calculateBaseFee(
*(applyCtx.app.openLedger().current()), *stpTrans)
.drops();
}
catch (std::exception const& e)
{
@@ -2781,7 +2743,8 @@ HookAPI::set_state_cache(
if (modified && stateMap.modified_entry_count >= max_state_modifications)
return Unexpected(TOO_MANY_STATE_MODIFICATIONS);
bool const createNamespace = !view.exists(keylet::hookStateDir(acc, ns));
bool const createNamespace = view.rules().enabled(fixXahauV1) &&
!view.exists(keylet::hookStateDir(acc, ns));
if (stateMap.find(acc) == stateMap.end())
{
@@ -3086,7 +3049,6 @@ HookAPI::get_stobject_length(
type == STI_PATHSET && rules.enabled(featureHookAPISerializedType240))
{
length = 0;
bool terminated = false;
while (upto + length < end)
{
// iterate Path step
@@ -3104,9 +3066,6 @@ 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
@@ -3118,68 +3077,43 @@ 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};
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);
// 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
// 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
}
// 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
}
if (length > -1)
@@ -3196,10 +3130,6 @@ 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);
}
+110 -118
View File
@@ -9,9 +9,7 @@
#include <xrpld/app/tx/detail/NFTokenUtils.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/st.h>
#include <xrpl/protocol/tokens.h>
@@ -70,6 +68,9 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
return rv.read(keylet::nftoffer(*id));
};
bool const fixV1 = rv.rules().enabled(fixXahauV1);
bool const fixV2 = rv.rules().enabled(fixXahauV2);
switch (tt)
{
case ttCRON: {
@@ -118,7 +119,7 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
case ttIMPORT: {
if (tx.isFieldPresent(sfIssuer))
ADD_TSH(tx.getAccountID(sfIssuer), tshWEAK);
ADD_TSH(tx.getAccountID(sfIssuer), fixV2 ? tshWEAK : tshSTRONG);
break;
}
@@ -143,13 +144,34 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
break;
// pass, already a TSH
// the owner burns their token, and the issuer is a weak TSH
if (*otxnAcc == owner && rv.exists(keylet::account(issuer)))
ADD_TSH(issuer, tshWEAK);
// the issuer burns the owner's token, and the owner is a weak
// TSH
else if (rv.exists(keylet::account(owner)))
ADD_TSH(owner, tshWEAK);
// new logic
if (fixV1)
{
// the owner burns their token, and the issuer is a weak TSH
if (*otxnAcc == owner && rv.exists(keylet::account(issuer)))
ADD_TSH(issuer, tshWEAK);
// the issuer burns the owner's token, and the owner is a weak
// TSH
else if (rv.exists(keylet::account(owner)))
ADD_TSH(owner, tshWEAK);
break;
}
// old logic
{
if (*otxnAcc == owner)
{
// the owner burns their token, and the issuer is a weak TSH
ADD_TSH(issuer, tshSTRONG);
}
else
{
// the issuer burns the owner's token, and the owner is a
// weak TSH
ADD_TSH(owner, tshSTRONG);
}
}
break;
}
@@ -178,17 +200,24 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
issuer,
(ut->getFlags() & lsfBurnable) ? tshSTRONG : tshWEAK);
if (rv.rules().enabled(featureURITokenBroker) &&
tx.isFieldPresent(sfBrokerAccount))
ADD_TSH(tx.getAccountID(sfBrokerAccount), tshWEAK);
break;
}
case ttURITOKEN_MINT: {
// destination is a strong tsh
if (tx.isFieldPresent(sfDestination))
if (fixV2 && tx.isFieldPresent(sfDestination))
ADD_TSH(tx.getAccountID(sfDestination), tshSTRONG);
break;
}
case ttURITOKEN_CANCEL_SELL_OFFER: {
if (!fixV2)
break;
Keylet const id{ltURI_TOKEN, tx.getFieldH256(sfURITokenID)};
if (!rv.exists(id))
return {};
@@ -359,38 +388,61 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
case ttESCROW_CANCEL:
case ttESCROW_FINISH: {
if (!tx.isFieldPresent(sfOwner))
return {};
// new logic
if (fixV1)
{
if (!tx.isFieldPresent(sfOwner))
return {};
AccountID const owner = tx.getAccountID(sfOwner);
AccountID const owner = tx.getAccountID(sfOwner);
bool const hasSeq = tx.isFieldPresent(sfOfferSequence);
bool const hasID = tx.isFieldPresent(sfEscrowID);
if (!hasSeq && !hasID)
return {};
bool const hasSeq = tx.isFieldPresent(sfOfferSequence);
bool const hasID = tx.isFieldPresent(sfEscrowID);
if (!hasSeq && !hasID)
return {};
Keylet kl = hasSeq
? keylet::escrow(owner, tx.getFieldU32(sfOfferSequence))
: Keylet(ltESCROW, tx.getFieldH256(sfEscrowID));
Keylet kl = hasSeq
? keylet::escrow(owner, tx.getFieldU32(sfOfferSequence))
: Keylet(ltESCROW, tx.getFieldH256(sfEscrowID));
auto escrow = rv.read(kl);
auto escrow = rv.read(kl);
if (!escrow || escrow->getFieldU16(sfLedgerEntryType) != ltESCROW)
return {};
if (!escrow ||
escrow->getFieldU16(sfLedgerEntryType) != ltESCROW)
return {};
// this should always be the same as owner, but defensively...
AccountID const src = escrow->getAccountID(sfAccount);
AccountID const dst = escrow->getAccountID(sfDestination);
// this should always be the same as owner, but defensively...
AccountID const src = escrow->getAccountID(sfAccount);
AccountID const dst = escrow->getAccountID(sfDestination);
// the source account is a strong transacitonal stakeholder for
// fin and can
ADD_TSH(src, tshSTRONG);
// the source account is a strong transacitonal stakeholder for
// fin and can
ADD_TSH(src, tshSTRONG);
// the dest acc is a strong tsh for fin and weak for can
if (src != dst)
ADD_TSH(dst, tt == ttESCROW_FINISH ? tshSTRONG : tshWEAK);
// the dest acc is a strong tsh for fin and weak for can
if (src != dst)
ADD_TSH(dst, tt == ttESCROW_FINISH ? tshSTRONG : tshWEAK);
break;
break;
}
// old logic
{
if (!tx.isFieldPresent(sfOwner) ||
!tx.isFieldPresent(sfOfferSequence))
return {};
auto escrow = rv.read(keylet::escrow(
tx.getAccountID(sfOwner), tx.getFieldU32(sfOfferSequence)));
if (!escrow)
return {};
ADD_TSH(escrow->getAccountID(sfAccount), tshSTRONG);
ADD_TSH(
escrow->getAccountID(sfDestination),
tt == ttESCROW_FINISH ? tshSTRONG : tshWEAK);
break;
}
}
case ttPAYCHAN_FUND:
@@ -465,26 +517,6 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
case ttCRON_SET: {
break;
}
case ttMANIFEST_SET: {
// The ephemeral key's logical account, meaning the r-address its
// public key hashes to, is a weak stake holder: the manifest names
// that key but nothing is done to the account, so it may observe
// but not rollback. Usually no such account exists, in which case
// nothing executes.
if (!tx.isFieldPresent(sfManifest))
break;
STObject const& man =
const_cast<STTx&>(tx).getField(sfManifest).downcast<STObject>();
if (!man.isFieldPresent(sfSigningPubKey))
break;
auto const spk = man.getFieldVL(sfSigningPubKey);
if (publicKeyType(makeSlice(spk)))
ADD_TSH(calcAccountID(PublicKey(makeSlice(spk))), tshWEAK);
break;
}
case ttAMM_CREATE:
case ttAMM_DEPOSIT:
case ttAMM_WITHDRAW:
@@ -785,7 +817,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
canHookTT(ripple::TxType txType, ripple::uint256 hookOn)
hook::canHook(ripple::TxType txType, ripple::uint256 hookOn)
{
// invert ttHOOK_SET bit
hookOn ^= UINT256_BIT[ttHOOK_SET];
@@ -799,28 +831,7 @@ canHookTT(ripple::TxType txType, ripple::uint256 hookOn)
bool
hook::canEmit(ripple::TxType txType, ripple::uint256 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;
return hook::canHook(txType, hookCanEmit);
}
ripple::uint256
@@ -1337,9 +1348,13 @@ DEFINE_HOOK_FUNCTION(
auto const key = make_state_key(
std::string_view{(const char*)(memory + kread_ptr), (size_t)kread_len});
if (!view.exists(hookCtx.result.accountKeylet))
// should return hook_api::hook_return_code
return static_cast<hook_api::hook_return_code>(tefINTERNAL);
if (view.rules().enabled(fixXahauV1))
{
auto const sleAccount = view.peek(hookCtx.result.accountKeylet);
if (!sleAccount)
// should return hook_api::hook_return_code
return static_cast<hook_api::hook_return_code>(tefINTERNAL);
}
if (!key)
return INTERNAL_ERROR;
@@ -1549,6 +1564,7 @@ hook::finalizeHookResult(
}
}
bool const fixV2 = applyCtx.view().rules().enabled(fixXahauV2);
// add a metadata entry for this hook execution result
{
STObject meta{sfHookExecution};
@@ -1577,14 +1593,18 @@ hook::finalizeHookResult(
meta.setFieldU16(sfHookStateChangeCount, hookResult.changedStateCount);
meta.setFieldH256(sfHookHash, hookResult.hookHash);
uint32_t flags = 0;
if (hookResult.isStrong)
flags |= hefSTRONG;
if (hookResult.isCallback)
flags |= hefCALLBACK;
if (hookResult.executeAgainAsWeak)
flags |= hefDOAAW;
meta.setFieldU32(sfFlags, flags);
// add informational flags in fix2
if (fixV2)
{
uint32_t flags = 0;
if (hookResult.isStrong)
flags |= hefSTRONG;
if (hookResult.isCallback)
flags |= hefCALLBACK;
if (hookResult.executeAgainAsWeak)
flags |= hefDOAAW;
meta.setFieldU32(sfFlags, flags);
}
avi.addHookExecutionMetaData(std::move(meta));
}
@@ -1598,7 +1618,8 @@ hook::finalizeHookResult(
meta.setFieldH256(sfHookHash, hookResult.hookHash);
meta.setAccountID(sfHookAccount, hookResult.account);
meta.setFieldH256(sfEmittedTxnID, etxnid);
meta.setFieldH256(sfEmitNonce, enonce);
if (fixV2)
meta.setFieldH256(sfEmitNonce, enonce);
avi.addHookEmissionMetaData(std::move(meta));
}
}
@@ -2212,35 +2233,6 @@ 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 -37
View File
@@ -289,49 +289,14 @@ LedgerMaster::setValidLedger(std::shared_ptr<Ledger const> const& l)
app_.getSHAMapStore().onLedgerClosed(getValidatedLedger());
mLedgerHistory.validatedLedger(l, consensusHash);
app_.getAmendmentTable().doValidatedLedger(l);
using namespace std::chrono_literals;
// How far ahead of the expected activation time to stop the server.
// firstUnsupportedExpected() is only a lower bound on activation: the
// amendment goes live at the first flag ledger at or after that time, so
// stopping early is harmless while stopping late risks being handed a
// ledger we cannot deserialize.
constexpr auto amendmentShutdownLeadTime = 5min;
if (!app_.getOPs().isBlocked())
{
auto const firstUnsupported =
app_.getAmendmentTable().firstUnsupportedExpected();
if (app_.getAmendmentTable().hasUnsupportedEnabled())
{
JLOG(m_journal.error()) << "One or more unsupported amendments "
"activated: server blocked.";
app_.getOPs().setAmendmentBlocked();
}
else if (
firstUnsupported &&
app_.timeKeeper().closeTime() + amendmentShutdownLeadTime >=
*firstUnsupported)
{
// Activation is imminent, or the expected time has already passed
// and we are only waiting on the next flag ledger. Shut down now,
// while we can still deserialize the ledgers we are handed.
//
// This is deliberately checked on every validated ledger rather
// than only on flag ledgers: the lead time above is much shorter
// than the flag ledger interval, so a flag-ledger-only check would
// usually skip straight over the window.
//
// The comparison must not be written as (*first - now), because
// NetClock::rep is unsigned and wraps once the expected time is in
// the past -- which is the most dangerous case, not the safest.
JLOG(m_journal.error())
<< "Unsupported amendment expected to activate at "
<< to_string(*firstUnsupported) << ". Shutting down.";
app_.getOPs().setAmendmentBlocked();
}
else if (!app_.getOPs().isAmendmentWarned() || l->isFlagLedger())
{
// Amendments can lose majority, so re-check periodically (every
@@ -343,11 +308,12 @@ LedgerMaster::setValidLedger(std::shared_ptr<Ledger const> const& l)
// this message may be logged more than once per session, because
// the node will otherwise function normally, and this gives
// operators an opportunity to see and resolve the warning.
if (firstUnsupported)
if (auto const first =
app_.getAmendmentTable().firstUnsupportedExpected())
{
JLOG(m_journal.error()) << "One or more unsupported amendments "
"reached majority. Upgrade before "
<< to_string(*firstUnsupported)
<< to_string(*first)
<< " to prevent your server from "
"becoming amendment blocked.";
app_.getOPs().setAmendmentWarned();
-60
View File
@@ -59,7 +59,6 @@
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpld/shamap/NodeFamily.h>
#include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/FileUtilities.h>
#include <xrpl/basics/ResolverAsio.h>
#include <xrpl/basics/random.h>
#include <xrpl/basics/safe_cast.h>
@@ -2373,65 +2372,6 @@ Application::Application() : beast::PropertyStream::Source("app")
//------------------------------------------------------------------------------
boost::filesystem::path
amendmentBlockedFilePath(Config const& config)
{
return config.CONFIG_DIR / "README_AMENDMENT_BLOCKED";
}
boost::system::error_code
writeAmendmentBlockedFile(
Config const& config,
std::vector<std::string> const& amendments)
{
using namespace std::chrono;
std::ostringstream ss;
ss << "XAHAUD STOPPED: UPGRADE REQUIRED\n"
<< "\n"
<< "Stopped at: "
<< to_string_iso(time_point_cast<seconds>(system_clock::now())) << "\n"
<< "This build: " << BuildInfo::getVersionString() << "\n"
<< "\n";
if (amendments.empty())
{
ss << "One or more network amendments are not supported by this "
"build.\n";
}
else
{
ss << "Amendments this build does not support:\n";
for (auto const& amendment : amendments)
ss << " " << amendment << "\n";
}
ss << "\n"
<< "The network has moved to rules this build does not implement, so "
"the\n"
<< "server stopped rather than keep serving ledgers it cannot read.\n"
<< "\n"
<< "To get back in sync:\n"
<< "1. Upgrade xahaud to a version that supports the amendments "
"above.\n"
<< "2. Start xahaud again. This file is removed automatically on "
"start.\n"
<< "\n"
<< "Starting this build again without upgrading will stop the server "
"again\n"
<< "and rewrite this file. Nothing needs to be deleted by hand.\n";
boost::system::error_code ec;
writeFileContents(ec, amendmentBlockedFilePath(config), ss.str());
return ec;
}
bool
removeAmendmentBlockedFile(Config const& config, boost::system::error_code& ec)
{
return boost::filesystem::remove(amendmentBlockedFilePath(config), ec);
}
std::unique_ptr<Application>
make_Application(
std::unique_ptr<Config> config,
-26
View File
@@ -27,14 +27,9 @@
#include <xrpl/beast/utility/PropertyStream.h>
#include <xrpl/protocol/Protocol.h>
#include <boost/asio.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/program_options.hpp>
#include <boost/system/error_code.hpp>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <vector>
namespace ripple {
@@ -284,27 +279,6 @@ make_Application(
std::unique_ptr<Logs> logs,
std::unique_ptr<TimeKeeper> timeKeeper);
/** Location of the receipt left behind when the server stops because it does
not support a network amendment. */
boost::filesystem::path
amendmentBlockedFilePath(Config const& config);
/** Write the amendment-blocked receipt: a record for the operator of when the
server stopped and which amendments it could not support. `amendments`
holds one already-rendered line per unsupported amendment, and may be
empty. Best effort -- any error is returned rather than thrown, and the
caller is expected to continue shutting down either way. */
boost::system::error_code
writeAmendmentBlockedFile(
Config const& config,
std::vector<std::string> const& amendments);
/** Remove any amendment-blocked receipt left behind by a previous run.
Returns true if a receipt was present and has been removed; sets `ec` if
removal was attempted and failed. */
bool
removeAmendmentBlockedFile(Config const& config, boost::system::error_code& ec);
} // namespace ripple
#endif
-25
View File
@@ -809,31 +809,6 @@ run(int argc, char** argv)
// No arguments. Run server.
if (!vm.count("parameters"))
{
// Clear any receipt left by a previous amendment-blocked shutdown. It
// records why the server stopped; it is not a lock. If this build
// still does not support the amendment it will stop again and write a
// fresh one, so there is nothing for the operator to delete by hand
// and no way for a stale receipt to keep a working build down.
// Standalone does not write receipts, so it does not clear them
// either, leaving the file readable for diagnosis.
if (!config->standalone())
{
boost::system::error_code ec;
auto const blockedFile = amendmentBlockedFilePath(*config);
if (removeAmendmentBlockedFile(*config, ec))
{
JLOG(logs->journal("Application").warn())
<< "Removed amendment-blocked receipt " << blockedFile
<< " left by a previous run.";
}
else if (ec)
{
JLOG(logs->journal("Application").warn())
<< "Could not remove amendment-blocked receipt "
<< blockedFile << ": " << ec.message();
}
}
// TODO: this comment can be removed in a future release -
// say 1.7 or higher
if (config->had_trailing_comments())
-18
View File
@@ -27,7 +27,6 @@
#include <xrpl/protocol/STValidation.h>
#include <optional>
#include <vector>
namespace ripple {
@@ -51,17 +50,6 @@ public:
VoteBehavior const vote;
};
/** An amendment seen on the network that this server has no code
support for. */
struct UnsupportedAmendment
{
uint256 id;
/** The time the amendment is expected to activate. Unset if it is
already active. */
std::optional<NetClock::time_point> expected;
};
virtual ~AmendmentTable() = default;
virtual uint256
@@ -92,12 +80,6 @@ public:
virtual std::optional<NetClock::time_point>
firstUnsupportedExpected() const = 0;
/** Amendments this server does not support that are already enabled, or
that have reached majority and are expected to activate. Ordered by
amendment id. Intended for operator-facing diagnostics. */
virtual std::vector<UnsupportedAmendment>
unsupportedAmendments() const = 0;
virtual Json::Value
getJson(bool isAdmin) const = 0;
-215
View File
@@ -25,18 +25,12 @@
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
#include <algorithm>
#include <atomic>
#include <mutex>
#include <optional>
#include <shared_mutex>
#include <string>
#include <vector>
namespace ripple {
class ReadView;
/*
Validator key manifests
-----------------------
@@ -178,16 +172,6 @@ deserializeManifest(
return deserializeManifest(makeSlice(s), journal);
}
inline std::optional<Manifest>
deserializeManifest(
STObject const& st,
beast::Journal journal = beast::Journal(beast::Journal::getNullSink()))
{
Serializer s;
st.add(s);
return deserializeManifest(makeSlice(s.peekData()), journal);
}
template <
class T,
class = std::enable_if_t<
@@ -280,60 +264,9 @@ private:
/** Master public keys stored by current ephemeral public key. */
hash_map<PublicKey, PublicKey> signingToMasterKeys_;
/** Master keys always offered to a peer, whatever their recency.
Set by pin(); in practice the master keys on the configured validator
lists, which are the manifests consensus actually depends on.
*/
hash_set<PublicKey> pinned_;
/** Recency of use, keyed by master public key.
The structure of this map is guarded by mutex_ exactly as map_ is:
entries are created next to it in applyManifest() and are never
removed. The counters themselves are atomic, so recording a hit is a
write to an atomic rather than a structural modification and is legal
while only a shared lock is held.
This is deliberately not a second mutex. A second mutex would have to
be ordered against mutex_, and that ordering would be an unenforced
invariant that any future caller could invert.
*/
hash_map<PublicKey, std::atomic<std::uint64_t>> mutable lastUsed_;
std::atomic<std::uint64_t> mutable tick_{0};
/** Ephemeral keys already probed against the ledger, and where.
Bounds the reads driven by incoming validations to one per key per
ledger. A key that resolves does not come back here: the mapping then
lives in signingToMasterKeys_ and applyLedgerSigningKey() answers from
it before reaching this map.
Unlike lastUsed_ this is written structurally, so it is guarded by
mutex_ in exclusive mode.
*/
hash_map<PublicKey, std::uint32_t> probed_;
/** Record that a manifest was looked up.
@pre The caller holds mutex_, shared or exclusive.
*/
void
touch(PublicKey const& masterKey) const;
std::atomic<std::uint32_t> seq_{0};
public:
/** Ceiling on the unpinned manifests offered to a newly connected peer. */
static constexpr std::size_t gossipLimit = 64;
/** Ceiling on remembered ephemeral key probes before they are dropped.
A cache of negatives, so dropping it costs at most one extra ledger
read per key.
*/
static constexpr std::size_t probeLimit = 4096;
explicit ManifestCache(
beast::Journal j = beast::Journal(beast::Journal::getNullSink()))
: j_(j)
@@ -422,90 +355,6 @@ public:
ManifestDisposition
applyManifest(Manifest m);
/** Set the master keys that are always offered to peers.
Replaces any previous set. Bumps sequence() when the set actually
changes, so a cached gossip message built from it is rebuilt.
@param keys Master public keys to pin
@par Thread Safety
May be called concurrently
*/
void
pin(hash_set<PublicKey> keys);
/** Returns the sequence and serialized form of a held manifest.
Unlike getManifest() and getSequence(), a revoked master key is
reported rather than skipped. Those two answer "what should I trust",
for which a revocation is correctly nothing; a caller republishing what
it holds needs the revocation most of all.
@param pk Master public key
@par Thread Safety
May be called concurrently
*/
std::optional<std::pair<std::uint32_t, std::string>>
getRawManifest(PublicKey const& pk) const;
/** Ingest manifests published on-ledger.
Reads keylet::manifest() for each supplied master key, reconstructs any
manifest found and feeds it through applyManifest(), so an on-chain
manifest is subject to exactly the same staleness, revocation and
key-reuse rules -- and the same signature check -- as one arriving by
peer gossip or in a published list. It is a third source of manifests,
not a more trusted one.
Probes a known key set rather than scanning the ledger's transactions:
this costs one SHAMap read per key, and picks up manifests published in
ledgers this node never saw.
@param view Ledger to read from
@param masterKeys Master public keys to probe for
@return the number of manifests newly accepted
@par Thread Safety
May be called concurrently
*/
std::size_t
applyLedger(ReadView const& view, hash_set<PublicKey> const& masterKeys);
/** Resolve an ephemeral signing key against a manifest published on-ledger.
applyLedger() probes a known master key set, which cannot help a key
this node has no manifest for: the master key is exactly what is
missing. SetManifest writes a second copy of every manifest keyed by
its ephemeral key, so that case is one read rather than a search.
Anything found is fed through applyManifest(), so an on-chain manifest
faces the same signature check and the same staleness, revocation and
key-reuse rules as one arriving by gossip. The ledger is a transport
here, not an authority: the answer is read back out of the cache rather
than taken from the ledger object, because applyManifest() may decline
it.
A key is probed at most once per ledger, and a key that resolves is
answered from the cache thereafter without any ledger read.
@param view Ledger to read from
@param signingKey Ephemeral public key to resolve
@return the master key now associated with signingKey, if any
@par Thread Safety
May be called concurrently
*/
std::optional<PublicKey>
applyLedgerSigningKey(ReadView const& view, PublicKey const& signingKey);
/** Populate manifest cache with manifests in database and config.
@param dbCon Database connection with dbTable
@@ -611,70 +460,6 @@ public:
f(manifest);
}
}
/** Invokes the callback for the manifests worth offering a new peer.
Offering the whole cache means offering everything the node has ever
seen, which grows without bound and is mostly of no use to the peer.
This offers the pinned set plus up to gossipLimit further manifests,
most recently used first. A manifest left out still reaches the peer by
ordinary relay if it turns out to be needed.
@note Do not call ManifestCache member functions from within the
callback. This can re-lock the mutex from the same thread, which is UB.
@note Do not write ManifestCache member variables from
within the callback. This can lead to data races.
@param pf Pre-function called with the maximum number of times f will be
called (useful for memory allocations)
@param f Function called for each manifest
@par Thread Safety
May be called concurrently
*/
template <class PreFun, class EachFun>
void
for_each_gossip_manifest(PreFun&& pf, EachFun&& f) const
{
std::shared_lock lock{mutex_};
// Rank the unpinned entries by recency and keep the head. Pointers
// into map_ stay valid: it is node based and the shared lock is held
// throughout.
std::vector<std::pair<std::uint64_t, PublicKey const*>> ranked;
ranked.reserve(map_.size());
for (auto const& [key, manifest] : map_)
{
(void)manifest;
if (pinned_.count(key))
continue;
auto const used = lastUsed_.find(key);
ranked.emplace_back(
used == lastUsed_.end()
? 0
: used->second.load(std::memory_order_relaxed),
&key);
}
auto const keep = std::min(gossipLimit, ranked.size());
std::partial_sort(
ranked.begin(),
ranked.begin() + keep,
ranked.end(),
[](auto const& a, auto const& b) { return a.first > b.first; });
// An upper bound: a pinned key need not have a manifest yet.
pf(pinned_.size() + keep);
for (auto const& key : pinned_)
if (auto const iter = map_.find(key); iter != map_.end())
f(iter->second);
for (std::size_t i = 0; i < keep; ++i)
f(map_.find(*ranked[i].second)->second);
}
};
} // namespace ripple
+4 -200
View File
@@ -28,13 +28,11 @@
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/ledger/OrderBookDB.h>
#include <xrpld/app/ledger/TransactionMaster.h>
#include <xrpld/app/main/Application.h>
#include <xrpld/app/main/LoadManager.h>
#include <xrpld/app/misc/AmendmentTable.h>
#include <xrpld/app/misc/DeliverMax.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/misc/LoadFeeTrack.h>
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/misc/StateAccounting.h>
#include <xrpld/app/misc/Transaction.h>
@@ -44,7 +42,6 @@
#include <xrpld/app/misc/detail/AccountTxPaging.h>
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/SetManifest.h> // makeSetManifestTx
#include <xrpld/consensus/Consensus.h>
#include <xrpld/consensus/ConsensusParms.h>
#include <xrpld/overlay/Cluster.h>
@@ -57,7 +54,6 @@
#include <xrpld/rpc/MPTokenIssuanceID.h>
#include <xrpld/rpc/ServerHandler.h>
#include <xrpld/rpc/detail/UDPInfoSub.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/UptimeClock.h>
#include <xrpl/basics/mulDiv.h>
#include <xrpl/basics/safe_cast.h>
@@ -72,7 +68,6 @@
#include <xrpl/protocol/MultiApiJson.h>
#include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/resource/Fees.h>
#include <xrpl/resource/ResourceManager.h>
@@ -316,8 +311,6 @@ private:
switchLastClosedLedger(std::shared_ptr<Ledger const> const& newLCL);
bool
checkLastClosedLedger(const Overlay::PeerSequence&, uint256& networkClosed);
void
publishNewerManifests(ReadView const& ledger);
public:
bool
@@ -1163,72 +1156,6 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
});
}
void
NetworkOPsImp::publishNewerManifests(ReadView const& ledger)
{
for (auto const& pk : app_.validators().getTrustedMasterKeys())
{
// Only for validators that have opted in by publishing on-ledger
// already. Submitting spends the master key account's balance, so an
// account that has never used the feature is left alone.
auto const sleMan = ledger.read(keylet::manifest(pk));
if (!sleMan)
continue;
auto const held = app_.validatorManifests().getRawManifest(pk);
if (!held || held->first <= sleMan->getFieldU32(sfSequence))
continue;
auto const hex = makeSetManifestTx(
makeSlice(held->second),
app_.config().NETWORK_ID,
*app_.openLedger().current(),
app_.journal("Manifest"));
auto const blob = hex ? strUnHex(*hex) : std::nullopt;
if (!blob || blob->empty())
continue;
std::shared_ptr<STTx const> stTx;
std::string reason;
std::shared_ptr<Transaction> tx;
try
{
SerialIter sit{makeSlice(*blob)};
stTx = std::make_shared<STTx const>(std::ref(sit));
tx = std::make_shared<Transaction>(stTx, reason, app_);
}
catch (std::exception const& e)
{
JLOG(m_journal.warn())
<< "publishNewerManifests: " << e.what() << " " << reason;
continue;
}
if (tx->getStatus() != NEW)
continue;
// The master key account pays. Skip rather than submit something that
// can only fail: this runs every ledger, so an unfunded validator
// would otherwise be retried forever.
auto const sleAcct = ledger.read(keylet::account(calcAccountID(pk)));
if (!sleAcct ||
sleAcct->getFieldAmount(sfBalance).xrp() < (*stTx)[sfFee].xrp())
continue;
JLOG(m_journal.info())
<< "publishNewerManifests: publishing manifest seq " << held->first
<< " for " << toBase58(TokenType::NodePublic, pk);
// Submitted from the job queue because this runs on the consensus
// thread, which must not block on transaction processing.
m_job_queue.addJob(jtTRANSACTION, "publishManifest", [this, tx]() {
auto t = tx;
processTransaction(t, false, false, FailHard::no);
});
}
}
void
NetworkOPsImp::processTransaction(
std::shared_ptr<Transaction>& transaction,
@@ -1702,68 +1629,11 @@ NetworkOPsImp::isAmendmentBlocked()
return amendmentBlocked_;
}
// Render the unsupported amendments for the operator-facing receipt. This is
// the only identification available: an amendment this build does not support
// has no name here, so the id is what the operator matches against the
// release notes.
static std::vector<std::string>
describeUnsupportedAmendments(AmendmentTable const& table)
{
std::vector<std::string> lines;
for (auto const& amendment : table.unsupportedAmendments())
{
std::ostringstream ss;
ss << to_string(amendment.id);
if (amendment.expected)
ss << " (expected to activate "
<< to_string_iso(*amendment.expected) << ")";
else
ss << " (already active)";
lines.push_back(ss.str());
}
return lines;
}
void
NetworkOPsImp::setAmendmentBlocked()
{
// Idempotent: this is reached from Change::applyAmendment (i.e. from the
// transaction application path, which is not guarded by isBlocked()) as
// well as from LedgerMaster::setValidLedger. Writing the receipt and
// logging once per process is enough, and it keeps the synchronous file
// write out of any subsequent ledger apply.
if (amendmentBlocked_.exchange(true))
return;
amendmentBlocked_ = true;
setMode(OperatingMode::CONNECTED);
if (!app_.config().standalone())
{
auto const blockedFile = amendmentBlockedFilePath(app_.config());
if (auto const ec = writeAmendmentBlockedFile(
app_.config(),
describeUnsupportedAmendments(app_.getAmendmentTable())))
{
JLOG(m_journal.fatal())
<< "Could not write amendment-blocked receipt " << blockedFile
<< ": " << ec.message();
}
else
{
JLOG(m_journal.fatal())
<< "Amendment-blocked receipt written to " << blockedFile;
}
JLOG(m_journal.fatal())
<< "This version of xahaud does not support a network amendment. "
"The amendment will activate soon or is already active. "
"The server will stop. Upgrade xahaud before you restart the "
"server.";
app_.signalStop(
"Unsupported network amendment. Upgrade xahaud before you restart "
"the server. Details: " +
blockedFile.string());
}
}
inline bool
@@ -1909,27 +1779,6 @@ NetworkOPsImp::checkLastClosedLedger(
return true;
}
// True if `view` enables an amendment this binary does not implement. Reads
// only the amendments ledger entry, so it is safe to call once transaction
// deserialization is already known to be failing.
static bool
ledgerHasUnsupportedAmendments(
AmendmentTable const& table,
ReadView const& view)
{
auto const sle = view.read(keylet::amendments());
if (!sle || !sle->isFieldPresent(sfAmendments))
return false;
for (auto const& amendment : sle->getFieldV256(sfAmendments))
{
if (!table.isSupported(amendment))
return true;
}
return false;
}
void
NetworkOPsImp::switchLastClosedLedger(
std::shared_ptr<Ledger const> const& newLCL)
@@ -1940,32 +1789,8 @@ NetworkOPsImp::switchLastClosedLedger(
clearNeedNetworkLedger();
// Update fee computations. May throw if the ledger contains
// transactions with fields unknown to this binary (e.g. after an
// unsupported amendment activates). Catch to allow graceful shutdown.
try
{
app_.getTxQ().processClosedLedger(app_, *newLCL, true);
}
catch (std::runtime_error const& e)
{
// Do not decide this on amendmentBlocked_ alone. A JUMP can happen
// before any validated ledger has been processed -- e.g. immediately
// after a restart, which is precisely the case that crashed -- so the
// flag may still be clear here. Ask the ledger itself instead.
// Anything else is a real bug and must propagate.
if (!amendmentBlocked_ &&
!ledgerHasUnsupportedAmendments(app_.getAmendmentTable(), *newLCL))
throw;
JLOG(m_journal.error()) << "Failed to process closed ledger "
<< newLCL->info().seq << ": " << e.what();
// No-op if we are already blocked; otherwise this starts the
// shutdown that should have been started before activation.
setAmendmentBlocked();
return;
}
// Update fee computations.
app_.getTxQ().processClosedLedger(app_, *newLCL, true);
// Caller must own master lock
{
@@ -2049,23 +1874,6 @@ NetworkOPsImp::beginConsensus(
if (prevLedger->rules().enabled(featureNegativeUNL))
app_.validators().setNegativeUNL(prevLedger->negativeUNL());
// Pull in any manifests published on-ledger before the trusted set is
// recomputed, so a validator that rotated its ephemeral key on-chain is
// resolved to the new signing key in this same round. The master keys come
// from the published lists, so this needs no bootstrap: only the ephemeral
// half of the mapping ever comes from a manifest.
if (prevLedger->rules().enabled(featureOnChainManifests))
{
app_.validatorManifests().applyLedger(
*prevLedger, app_.validators().getTrustedMasterKeys());
// The reverse of applyLedger above. Manifests reach us by peer gossip
// and in published validator lists, both of which can arrive before
// the validator gets around to publishing on-chain, so the cache may
// hold something newer than the ledger does.
publishNewerManifests(*prevLedger);
}
TrustChanges const changes = app_.validators().updateTrusted(
app_.getValidations().getCurrentNodeIDs(),
closingInfo.parentCloseTime,
@@ -2073,10 +1881,6 @@ NetworkOPsImp::beginConsensus(
app_.overlay(),
app_.getHashRouter());
// Pin the trusted master keys so they are always offered to a new peer and
// cannot be crowded out of the gossip set by more recently used manifests.
app_.validatorManifests().pin(app_.validators().getTrustedMasterKeys());
if (!changes.added.empty() || !changes.removed.empty())
{
app_.getValidations().trustChanged(changes.added, changes.removed);
@@ -2645,7 +2449,7 @@ NetworkOPsImp::getServerInfo(bool human, bool admin, bool counters)
"may be incorrectly configured or some [validator_list_sites] "
"may be unreachable.";
}
if (isAmendmentWarned())
if (admin && isAmendmentWarned())
{
Json::Value& w = warnings.append(Json::objectValue);
w[jss::id] = warnRPC_UNSUPPORTED_MAJORITY;
@@ -432,11 +432,6 @@ private:
// will be enabled.
std::optional<NetClock::time_point> firstUnsupportedExpected_;
// Unsupported amendments that have reached majority, and the time each
// is expected to activate. Recomputed alongside
// firstUnsupportedExpected_, so it clears when majority is lost.
std::vector<std::pair<uint256, NetClock::time_point>> unsupportedMajority_;
beast::Journal const j_;
// Database which persists veto/unveto vote
@@ -500,9 +495,6 @@ public:
std::optional<NetClock::time_point>
firstUnsupportedExpected() const override;
std::vector<UnsupportedAmendment>
unsupportedAmendments() const override;
Json::Value
getJson(bool isAdmin) const override;
Json::Value
@@ -815,36 +807,6 @@ AmendmentTableImpl::firstUnsupportedExpected() const
return firstUnsupportedExpected_;
}
std::vector<AmendmentTable::UnsupportedAmendment>
AmendmentTableImpl::unsupportedAmendments() const
{
std::lock_guard lock(mutex_);
std::vector<UnsupportedAmendment> result;
// Already active.
for (auto const& [id, state] : amendmentMap_)
{
if (state.enabled && !state.supported)
result.push_back({id, std::nullopt});
}
// Reached majority, not yet active.
for (auto const& entry : unsupportedMajority_)
{
if (std::none_of(result.begin(), result.end(), [&entry](auto const& u) {
return u.id == entry.first;
}))
result.push_back({entry.first, entry.second});
}
std::sort(result.begin(), result.end(), [](auto const& a, auto const& b) {
return a.id < b.id;
});
return result;
}
std::vector<uint256>
AmendmentTableImpl::doValidation(std::set<uint256> const& enabled) const
{
@@ -1005,7 +967,6 @@ AmendmentTableImpl::doValidatedLedger(
// if it's currently set. If it's not set when the loop is done, then any
// prior unknown amendments have lost majority.
firstUnsupportedExpected_.reset();
unsupportedMajority_.clear();
for (auto const& [hash, time] : majority)
{
AmendmentState& s = add(hash, lock);
@@ -1017,7 +978,6 @@ AmendmentTableImpl::doValidatedLedger(
{
JLOG(j_.info()) << "Unsupported amendment " << hash
<< " reached majority at " << to_string(time);
unsupportedMajority_.emplace_back(hash, time + majorityTime_);
if (!firstUnsupportedExpected_ || firstUnsupportedExpected_ > time)
firstUnsupportedExpected_ = time;
}
+1 -173
View File
@@ -20,12 +20,10 @@
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/rdb/Wallet.h>
#include <xrpld/core/DatabaseCon.h>
#include <xrpld/ledger/ReadView.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Sign.h>
@@ -56,6 +54,7 @@ deserializeManifest(Slice s, beast::Journal journal)
{
if (s.empty())
return std::nullopt;
static SOTemplate const manifestFormat{
// A manifest must include:
// - the master public key
@@ -302,10 +301,7 @@ ManifestCache::getSigningKey(PublicKey const& pk) const
auto const iter = map_.find(pk);
if (iter != map_.end() && !iter->second.revoked())
{
touch(pk);
return iter->second.signingKey;
}
return pk;
}
@@ -317,10 +313,7 @@ ManifestCache::getMasterKey(PublicKey const& pk) const
if (auto const iter = signingToMasterKeys_.find(pk);
iter != signingToMasterKeys_.end())
{
touch(iter->second);
return iter->second;
}
return pk;
}
@@ -356,10 +349,7 @@ ManifestCache::getManifest(PublicKey const& pk) const
auto const iter = map_.find(pk);
if (iter != map_.end() && !iter->second.revoked())
{
touch(pk);
return iter->second.serialized;
}
return std::nullopt;
}
@@ -371,169 +361,11 @@ ManifestCache::revoked(PublicKey const& pk) const
auto const iter = map_.find(pk);
if (iter != map_.end())
{
touch(pk);
return iter->second.revoked();
}
return false;
}
std::optional<std::pair<std::uint32_t, std::string>>
ManifestCache::getRawManifest(PublicKey const& pk) const
{
std::shared_lock lock{mutex_};
if (auto const iter = map_.find(pk); iter != map_.end())
{
touch(pk);
return std::make_pair(iter->second.sequence, iter->second.serialized);
}
return std::nullopt;
}
void
ManifestCache::touch(PublicKey const& masterKey) const
{
// find() rather than operator[]: inserting here would be a structural
// modification, and callers hold mutex_ only in shared mode. The entry is
// created in applyManifest() alongside the manifest itself, so a lookup
// that hit map_ always finds one here too.
if (auto const iter = lastUsed_.find(masterKey); iter != lastUsed_.end())
iter->second.store(++tick_, std::memory_order_relaxed);
}
void
ManifestCache::pin(hash_set<PublicKey> keys)
{
std::lock_guard lock{mutex_};
if (keys == pinned_)
return;
pinned_ = std::move(keys);
// The pinned set is part of what a gossip message contains, so a change to
// it has to invalidate any message cached against this sequence.
++seq_;
}
namespace {
/** Rebuild the manifest a ltMANIFEST object was written from.
The object is a lossless mirror written by SetManifest::doApply, so this
round-trip is byte-identical to the blob the master key signed and verify()
succeeds, or the manifest is discarded. Presence matters: sfVersion is
soeDEFAULT in the manifest format and must not be materialised.
*/
std::optional<Manifest>
manifestFromSLE(SLE const& sle, beast::Journal j)
{
STObject st{sfGeneric};
st.setFieldU32(sfSequence, sle.getFieldU32(sfSequence));
st.setFieldVL(sfPublicKey, sle.getFieldVL(sfPublicKey));
st.setFieldVL(sfMasterSignature, sle.getFieldVL(sfMasterSignature));
for (auto const& sf :
{std::cref(sfSigningPubKey),
std::cref(sfSignature),
std::cref(sfDomain)})
if (sle.isFieldPresent(sf.get()))
st.setFieldVL(sf.get(), sle.getFieldVL(sf.get()));
if (sle.isFieldPresent(sfVersion))
st.setFieldU16(sfVersion, sle.getFieldU16(sfVersion));
return deserializeManifest(st, j);
}
} // namespace
std::size_t
ManifestCache::applyLedger(
ReadView const& view,
hash_set<PublicKey> const& masterKeys)
{
std::size_t accepted = 0;
for (auto const& pk : masterKeys)
{
auto const sle = view.read(keylet::manifest(pk));
if (!sle)
continue;
// Cheap reject before rebuilding: applyManifest() would call this
// stale anyway, and the signature check is the expensive part.
if (auto const seq = getSequence(pk);
seq && *seq >= sle->getFieldU32(sfSequence))
continue;
if (auto mo = manifestFromSLE(*sle, j_); mo &&
applyManifest(std::move(*mo)) == ManifestDisposition::accepted)
++accepted;
}
return accepted;
}
std::optional<PublicKey>
ManifestCache::applyLedgerSigningKey(
ReadView const& view,
PublicKey const& signingKey)
{
auto const held = [this, &signingKey]() -> std::optional<PublicKey> {
std::shared_lock lock{mutex_};
if (auto const iter = signingToMasterKeys_.find(signingKey);
iter != signingToMasterKeys_.end())
{
touch(iter->second);
return iter->second;
}
return std::nullopt;
};
// A manifest for this key may have arrived by gossip or in a published
// list while the caller was deciding to ask.
if (auto const known = held())
return known;
{
std::lock_guard lock{mutex_};
if (probed_.size() >= probeLimit)
probed_.clear();
// One read per key per ledger. Reaching here already cost the sender a
// signature and this node a verification, so the read is not the
// cheapest thing an unknown key can ask for; the cap is to stop
// repeating it, not to stop anyone.
auto const seq = view.info().seq;
auto const [iter, inserted] = probed_.try_emplace(signingKey, seq);
if (!inserted && iter->second == seq)
return std::nullopt;
iter->second = seq;
}
auto const sle = view.read(keylet::manifest(signingKey));
if (!sle)
return std::nullopt;
// Every manifest is written at both its master and its ephemeral keylet,
// so an object here is either the manifest naming signingKey as its
// ephemeral key -- the case worth having -- or the manifest of a master
// key that is what was asked about. Ingesting either is correct, and
// applyManifest() verifies both signatures, so nothing found here can
// assert a binding its key holder did not sign for.
if (auto mo = manifestFromSLE(*sle, j_))
applyManifest(std::move(*mo));
// Only a signing key resolves: a master key is its own master.
return held();
}
ManifestDisposition
ManifestCache::applyManifest(Manifest m)
{
@@ -667,10 +499,6 @@ ManifestCache::applyManifest(Manifest m)
if (!revoked)
signingToMasterKeys_.emplace(*m.signingKey, m.masterKey);
// Kept in step with map_ so touch() never has to insert; see
// lastUsed_.
lastUsed_.try_emplace(m.masterKey, 0);
auto masterKey = m.masterKey;
map_.emplace(std::move(masterKey), std::move(m));
+2 -12
View File
@@ -1953,23 +1953,13 @@ TxQ::tryDirectApply(
const bool isFirstImport = !sleAccount &&
view.rules().enabled(featureImport) && tx->getTxnType() == ttIMPORT;
// A manifest txn is pinned to sfSequence 0 (Transactor::checkSeqProxy), so
// it can never match the account sequence. Direct-apply it like a first
// Import: letting it fall through to the queue would reject it outright on
// sequence rather than hold it. Manifests are therefore exempt from fee
// escalation, since requiredFeeLevel is not consulted for them.
const bool isManifest = view.rules().enabled(featureOnChainManifests) &&
tx->getTxnType() == ttMANIFEST_SET;
const bool bypassQueue = isFirstImport || isManifest;
// Don't attempt to direct apply if the account is not in the ledger.
if (!sleAccount && !isFirstImport)
return {};
std::optional<SeqProxy> txSeqProx;
if (!bypassQueue)
if (!isFirstImport)
{
SeqProxy const acctSeqProx =
SeqProxy::sequence((*sleAccount)[sfSequence]);
@@ -1982,7 +1972,7 @@ TxQ::tryDirectApply(
}
FeeLevel64 const requiredFeeLevel =
bypassQueue ? FeeLevel64{0} : [this, &view, flags]() {
isFirstImport ? FeeLevel64{0} : [this, &view, flags]() {
std::lock_guard lock(mutex_);
return getRequiredFeeLevel(
view, flags, feeMetrics_.getSnapshot(), lock);
+3 -5
View File
@@ -2033,11 +2033,9 @@ ValidatorList::updateTrusted(
trustChanges.added.insert(calcNodeID(val.first));
}
// Rebuilt on every call rather than only when membership changed: a
// manifest that rotates a validator's ephemeral key changes no membership
// at all, so a membership-gated rebuild would leave trustedSigningKeys_
// stale and the rotated validator's validations would stop counting. This
// is a handful of map lookups over the UNL.
// If there were any changes, we need to update the ephemeral signing
// keys:
if (!trustChanges.added.empty() || !trustChanges.removed.empty())
{
trustedSigningKeys_.clear();
+8 -3
View File
@@ -81,8 +81,8 @@ AMMCreate::preflight(PreflightContext const& ctx)
XRPAmount
AMMCreate::calculateBaseFee(ReadView const& view, STTx const& tx)
{
// The least fee required for AMMCreate is one owner reserve.
return Transactor::calculateBaseFee(view, tx) + view.fees().increment;
// The fee required for AMMCreate is one owner reserve.
return view.fees().increment;
}
TER
@@ -252,7 +252,12 @@ applyCreate(
auto sleAMMRoot = std::make_shared<SLE>(keylet::account(*ammAccount));
sleAMMRoot->setAccountID(sfAccount, *ammAccount);
sleAMMRoot->setFieldAmount(sfBalance, STAmount{});
std::uint32_t const seqno = newAccountSeqNo(ctx_.view());
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};
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
+21 -8
View File
@@ -39,14 +39,26 @@ CancelOffer::preflight(PreflightContext const& ctx)
return temINVALID_FLAG;
}
if ((!ctx.tx.isFieldPresent(sfOfferSequence) &&
!ctx.tx.isFieldPresent(sfOfferID)) ||
(ctx.tx.isFieldPresent(sfOfferSequence) &&
ctx.tx.isFieldPresent(sfOfferID)))
if (ctx.rules.enabled(fixXahauV1))
{
JLOG(ctx.j.trace())
<< "CancelOffer::preflight: invalid sequence or offer id";
return temBAD_SEQUENCE;
if ((!ctx.tx.isFieldPresent(sfOfferSequence) &&
!ctx.tx.isFieldPresent(sfOfferID)) ||
(ctx.tx.isFieldPresent(sfOfferSequence) &&
ctx.tx.isFieldPresent(sfOfferID)))
{
JLOG(ctx.j.trace())
<< "CancelOffer::preflight: invalid sequence or offer id";
return temBAD_SEQUENCE;
}
}
else
{
if (!ctx.tx.isFieldPresent(sfOfferSequence) ||
ctx.tx[sfOfferSequence] == 0)
{
JLOG(ctx.j.trace()) << "CancelOffer::preflight: missing sequence";
return temBAD_SEQUENCE;
}
}
return preflight2(ctx);
@@ -100,7 +112,8 @@ CancelOffer::doApply()
else
JLOG(j_.debug()) << "Trying to cancel offer #" << *offerSequence;
if (sleOffer->getFieldU16(sfLedgerEntryType) != ltOFFER)
bool const fixV1 = view().rules().enabled(fixXahauV1);
if (fixV1 && sleOffer->getFieldU16(sfLedgerEntryType) != ltOFFER)
{
JLOG(j_.debug()) << "OfferCancel specified non-offer ledger object";
return tecINTERNAL;
+2 -1
View File
@@ -554,7 +554,8 @@ Change::activateXahauGenesis()
{
sle = std::make_shared<SLE>(kl);
sle->setAccountID(sfAccount, accid);
std::uint32_t const seqno = newAccountSeqNo(sb);
std::uint32_t const seqno{
sb.info().parentCloseTime.time_since_epoch().count()};
sle->setFieldU32(sfSequence, seqno);
}
+7 -4
View File
@@ -30,6 +30,12 @@
namespace ripple {
TxConsequences
ClaimReward::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
ClaimReward::preflight(PreflightContext const& ctx)
{
@@ -158,10 +164,7 @@ ClaimReward::preclaim(PreclaimContext const& ctx)
auto const& hookOn =
hook::getHookOn(hook, sleDef, sfHookOnIncoming);
auto const hookName = ctx.view.rules().enabled(fix20261001)
? hook[~sfHookName]
: std::nullopt;
if (hook::canHook(ctx.tx, hookOn, hookName))
if (hook::canHook(ttCLAIM_REWARD, hookOn))
{
hasClaimRewardHook = true;
break;
+4 -1
View File
@@ -32,12 +32,15 @@ namespace ripple {
class ClaimReward : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit ClaimReward(ApplyContext& ctx) : Transactor(ctx)
{
}
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
+6
View File
@@ -30,6 +30,12 @@
namespace ripple {
TxConsequences
Cron::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
Cron::preflight(PreflightContext const& ctx)
{
+4 -1
View File
@@ -30,7 +30,7 @@ namespace ripple {
class Cron : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit Cron(ApplyContext& ctx) : Transactor(ctx)
{
@@ -39,6 +39,9 @@ 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,6 +27,12 @@
namespace ripple {
TxConsequences
CronSet::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
CronSet::preflight(PreflightContext const& ctx)
{
+4 -1
View File
@@ -29,7 +29,7 @@ namespace ripple {
class CronSet : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit CronSet(ApplyContext& ctx) : Transactor(ctx)
{
@@ -38,6 +38,9 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
+40 -12
View File
@@ -454,11 +454,23 @@ EscrowFinish::preflight(PreflightContext const& ctx)
// sfOfferSequence was changed to optional, so ensure the behaviour is the
// same until amendment passes
if ((!ctx.tx.isFieldPresent(sfEscrowID) &&
!ctx.tx.isFieldPresent(sfOfferSequence)) ||
(ctx.tx.isFieldPresent(sfEscrowID) &&
ctx.tx.isFieldPresent(sfOfferSequence)))
return temMALFORMED;
if (!ctx.rules.enabled(fixXahauV1))
{
if (!ctx.tx.isFieldPresent(sfOfferSequence))
return temMALFORMED;
if (ctx.tx.isFieldPresent(sfEscrowID) &&
ctx.tx.getFieldU32(sfOfferSequence) != 0)
return temMALFORMED;
}
else
{
if ((!ctx.tx.isFieldPresent(sfEscrowID) &&
!ctx.tx.isFieldPresent(sfOfferSequence)) ||
(ctx.tx.isFieldPresent(sfEscrowID) &&
ctx.tx.isFieldPresent(sfOfferSequence)))
return temMALFORMED;
}
return tesSUCCESS;
}
@@ -500,6 +512,8 @@ EscrowFinish::doApply()
std::optional<uint256> escrowID = ctx_.tx[~sfEscrowID];
std::optional<std::uint32_t> offerSequence = ctx_.tx[~sfOfferSequence];
bool const fixV1 = view().rules().enabled(fixXahauV1);
Keylet k = escrowID ? Keylet(ltESCROW, *escrowID)
: keylet::escrow(ctx_.tx[sfOwner], *offerSequence);
@@ -507,7 +521,7 @@ EscrowFinish::doApply()
if (!slep)
return tecNO_TARGET;
if (slep->getFieldU16(sfLedgerEntryType) != ltESCROW)
if (fixV1 && slep->getFieldU16(sfLedgerEntryType) != ltESCROW)
return tecINTERNAL;
AccountID const account = (*slep)[sfAccount];
@@ -726,11 +740,23 @@ EscrowCancel::preflight(PreflightContext const& ctx)
// sfOfferSequence was changed to optional, so ensure the behaviour is the
// same until amendment passes
if ((!ctx.tx.isFieldPresent(sfEscrowID) &&
!ctx.tx.isFieldPresent(sfOfferSequence)) ||
(ctx.tx.isFieldPresent(sfEscrowID) &&
ctx.tx.isFieldPresent(sfOfferSequence)))
return temMALFORMED;
if (!ctx.rules.enabled(fixXahauV1))
{
if (!ctx.tx.isFieldPresent(sfOfferSequence))
return temMALFORMED;
if (ctx.tx.isFieldPresent(sfEscrowID) &&
ctx.tx.getFieldU32(sfOfferSequence) != 0)
return temMALFORMED;
}
else
{
if ((!ctx.tx.isFieldPresent(sfEscrowID) &&
!ctx.tx.isFieldPresent(sfOfferSequence)) ||
(ctx.tx.isFieldPresent(sfEscrowID) &&
ctx.tx.isFieldPresent(sfOfferSequence)))
return temMALFORMED;
}
return preflight2(ctx);
}
@@ -746,6 +772,8 @@ EscrowCancel::doApply()
std::optional<uint256> escrowID = ctx_.tx[~sfEscrowID];
std::optional<std::uint32_t> offerSequence = ctx_.tx[~sfOfferSequence];
bool const fixV1 = view().rules().enabled(fixXahauV1);
Keylet k = escrowID ? Keylet(ltESCROW, *escrowID)
: keylet::escrow(ctx_.tx[sfOwner], *offerSequence);
@@ -753,7 +781,7 @@ EscrowCancel::doApply()
if (!slep)
return tecNO_TARGET;
if (slep->getFieldU16(sfLedgerEntryType) != ltESCROW)
if (fixV1 && slep->getFieldU16(sfLedgerEntryType) != ltESCROW)
return tecINTERNAL;
if (ctx_.view().rules().enabled(fix1571))
+107 -2
View File
@@ -75,6 +75,8 @@ GenesisMint::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
bool const allowDuplicates = ctx.rules.enabled(fixXahauV1);
std::unordered_set<AccountID> alreadySeen;
for (auto const& dest : dests)
{
@@ -137,6 +139,17 @@ GenesisMint::preflight(PreflightContext const& ctx)
"disallowed account zero or one.";
return temMALFORMED;
}
if (allowDuplicates)
continue;
if (alreadySeen.find(accid) != alreadySeen.end())
{
JLOG(ctx.j.warn()) << "GenesisMint: duplicate in destinations.";
return temMALFORMED;
}
alreadySeen.emplace(accid);
}
return preflight2(ctx);
@@ -164,8 +177,99 @@ GenesisMint::doApply()
{
auto const& dests = ctx_.tx.getFieldArray(sfGenesisMints);
if (!view().rules().enabled(fixXahauV1))
{
STAmount dropsAdded{0};
for (auto const& dest : dests)
{
auto const amt = dest[~sfAmount];
if (amt && !isXRP(*amt))
{
JLOG(ctx_.journal.warn()) << "GenesisMint: Non-xrp amount.";
return tecINTERNAL;
}
auto const flags = dest[~sfGovernanceFlags];
auto const marks = dest[~sfGovernanceMarks];
auto const id = dest.getAccountID(sfDestination);
auto const k = keylet::account(id);
auto sle = view().peek(k);
bool const created = !sle;
if (created)
{
// Create the account.
std::uint32_t const seqno{
view().info().parentCloseTime.time_since_epoch().count()};
sle = std::make_shared<SLE>(k);
sle->setAccountID(sfAccount, id);
sle->setFieldU32(sfSequence, seqno);
if (amt)
{
sle->setFieldAmount(sfBalance, *amt);
dropsAdded += *amt;
}
else // give them 2 XRP if the account didn't exist, same as
// ttIMPORT
{
XRPAmount const initialXrp =
Import::computeStartingBonus(ctx_.view());
sle->setFieldAmount(sfBalance, initialXrp);
dropsAdded += initialXrp;
}
}
else if (amt)
{
// Credit the account
STAmount startBal = sle->getFieldAmount(sfBalance);
STAmount finalBal = startBal + *amt;
if (finalBal <= startBal)
{
JLOG(ctx_.journal.warn())
<< "GenesisMint: cannot credit " << dest
<< " due to balance overflow";
return tecINTERNAL;
}
sle->setFieldAmount(sfBalance, finalBal);
dropsAdded += *amt;
}
// set flags and marks as applicable
if (flags)
sle->setFieldH256(sfGovernanceFlags, *flags);
if (marks)
sle->setFieldH256(sfGovernanceMarks, *marks);
if (created)
view().insert(sle);
else
view().update(sle);
}
// update ledger header
if (dropsAdded < beast::zero ||
dropsAdded.xrp() + view().info().drops < view().info().drops)
{
JLOG(ctx_.journal.warn()) << "GenesisMint: dropsAdded overflowed\n";
return tecINTERNAL;
}
if (dropsAdded > beast::zero)
ctx_.rawView().rawDestroyXRP(-dropsAdded.xrp());
return tesSUCCESS;
}
// RH NOTE:
// duplicate accounts are allowed
// As of fixXahauV1, duplicate accounts are allowed
// so we first do a summation loop then an actioning loop
// if amendment is not active then there's exactly one entry per
@@ -243,7 +347,8 @@ GenesisMint::doApply()
if (created)
{
// Create the account.
std::uint32_t const seqno = newAccountSeqNo(view());
std::uint32_t const seqno{
view().info().parentCloseTime.time_since_epoch().count()};
sle = std::make_shared<SLE>(k);
sle->setAccountID(sfAccount, id);
+17 -4
View File
@@ -821,9 +821,17 @@ Import::preflight(PreflightContext const& ctx)
<< " validation count: " << validationCount;
// check if the validation count is adequate
auto hasInsufficientQuorum = [](uint64_t quorum, uint64_t validationCount) {
return quorum > validationCount;
};
auto hasInsufficientQuorum =
[&ctx](uint64_t quorum, uint64_t validationCount) {
if (ctx.rules.enabled(fixXahauV1))
{
return quorum > validationCount;
}
else
{
return quorum >= validationCount;
}
};
if (hasInsufficientQuorum(quorum, validationCount))
{
JLOG(ctx.j.warn()) << "Import: xpop did not contain an 80% quorum for "
@@ -1307,7 +1315,12 @@ Import::doApply()
if (create)
{
// Create the account.
std::uint32_t const seqno = newAccountSeqNo(view());
std::uint32_t const seqno{
view().rules().enabled(featureXahauGenesis)
? view().info().parentCloseTime.time_since_epoch().count()
: view().rules().enabled(featureDeletableAccounts)
? view().seq()
: 1};
sle = std::make_shared<SLE>(keylet::account(id));
sle->setAccountID(sfAccount, id);
+5 -2
View File
@@ -612,7 +612,6 @@ LedgerEntryTypesMatch::visitEntry(
case ltMPTOKEN:
case ltCREDENTIAL:
case ltPERMISSIONED_DOMAIN:
case ltMANIFEST:
break;
default:
invalidTypeAdded_ = true;
@@ -1041,7 +1040,11 @@ ValidNewAccountRoot::finalize(
tt == ttXCHAIN_ADD_ACCOUNT_CREATE_ATTESTATION) &&
isTesSuccess(result))
{
std::uint32_t const startingSeq = newAccountSeqNo(view);
std::uint32_t const startingSeq{
view.rules().enabled(featureXahauGenesis)
? view.info().parentCloseTime.time_since_epoch().count()
: view.rules().enabled(featureDeletableAccounts) ? view.seq()
: 1};
if (accountSeq_ != startingSeq)
{
+27
View File
@@ -26,6 +26,12 @@
namespace ripple {
TxConsequences
Invoke::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
Invoke::preflight(PreflightContext const& ctx)
{
@@ -86,6 +92,27 @@ Invoke::calculateBaseFee(ReadView const& view, STTx const& tx)
extraFee +=
XRPAmount{static_cast<XRPAmount>(tx.getFieldVL(sfBlob).size())};
// old code (prior to fixXahauV1)
if (!view.rules().enabled(fixXahauV1))
{
if (tx.isFieldPresent(sfHookParameters))
{
uint64_t paramBytes = 0;
auto const& params = tx.getFieldArray(sfHookParameters);
for (auto const& param : params)
{
paramBytes +=
(param.isFieldPresent(sfHookParameterName)
? param.getFieldVL(sfHookParameterName).size()
: 0) +
(param.isFieldPresent(sfHookParameterValue)
? param.getFieldVL(sfHookParameterValue).size()
: 0);
}
extraFee += XRPAmount{static_cast<XRPAmount>(paramBytes)};
}
}
return Transactor::calculateBaseFee(view, tx) + extraFee;
}
+4 -1
View File
@@ -30,7 +30,7 @@ namespace ripple {
class Invoke : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit Invoke(ApplyContext& ctx) : Transactor(ctx)
{
@@ -39,6 +39,9 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
+6 -1
View File
@@ -354,7 +354,12 @@ Payment::doApply()
if (!sleDst)
{
std::uint32_t const seqno = newAccountSeqNo(view());
std::uint32_t const seqno{
view().rules().enabled(featureXahauGenesis)
? view().info().parentCloseTime.time_since_epoch().count()
: view().rules().enabled(featureDeletableAccounts)
? view().seq()
: 1};
// Create the account.
sleDst = std::make_shared<SLE>(k);
+7 -3
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 (!isValidUTF8(makeSlice(uri)))
if (!URIToken::validateUTF8(uri))
{
JLOG(ctx.j.warn())
<< "Malformed transaction: Invalid UTF8 inside MintURIToken.";
@@ -328,7 +328,11 @@ Remit::doApply()
nativeRemit += accountReserve;
// Create the account.
std::uint32_t const seqno = newAccountSeqNo(sb);
std::uint32_t const seqno{
sb.rules().enabled(featureXahauGenesis)
? sb.info().parentCloseTime.time_since_epoch().count()
: sb.rules().enabled(featureDeletableAccounts) ? sb.seq()
: 1};
sleDstAcc = std::make_shared<SLE>(keylet::account(dstAccID));
sleDstAcc->setAccountID(sfAccount, dstAccID);
+62 -116
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(SetHookCtx& ctx, STObject const& hookSetObj)
SetHook::inferOperation(STObject const& hookSetObj)
{
uint64_t wasmByteCount = hookSetObj.isFieldPresent(sfCreateCode)
? hookSetObj.getFieldVL(sfCreateCode).size()
@@ -216,12 +216,7 @@ SetHook::inferOperation(SetHookCtx& ctx, STObject const& hookSetObj)
bool hasHash = hookSetObj.isFieldPresent(sfHookHash);
bool hasCode = hookSetObj.isFieldPresent(sfCreateCode);
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
if (hasHash && hasCode) // Both HookHash and CreateCode: invalid
return hsoINVALID;
else if (hasHash) // Hookhash only: install
return hsoINSTALL;
@@ -249,62 +244,6 @@ SetHook::inferOperation(SetHookCtx& ctx, 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
@@ -315,7 +254,7 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
? hookSetObj.getFieldU32(sfFlags)
: 0;
switch (inferOperation(ctx, hookSetObj))
switch (inferOperation(hookSetObj))
{
case hsoNOOP: {
return true;
@@ -424,13 +363,6 @@ 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;
}
@@ -475,13 +407,6 @@ 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;
}
@@ -531,8 +456,56 @@ SetHook::validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj)
}
// validate sfHookOn
if (!validateHookOn(ctx, hookSetObj))
return false;
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;
}
}
// validate sfHookCanEmit
// HookCanEmit field is an optional field for backward compatibility
@@ -657,7 +630,7 @@ SetHook::validateHookName(Blob const& name, beast::Journal const& j)
<< "sfHookName must be between 8 and 32 hex characters.";
return false;
}
if (!isValidUTF8(makeSlice(name)))
if (!URIToken::validateUTF8(name))
{
JLOG(j.trace()) << "sfHookName must be a valid UTF-8 string.";
return false;
@@ -1432,7 +1405,7 @@ SetHook::setHook()
HookSetOperation op = hsoNOOP;
if (hookSetObj)
op = inferOperation(ctx, hookSetObj->get());
op = inferOperation(hookSetObj->get());
// these flags are not able to be passed onto the ledger object
int newFlags = 0;
@@ -1654,23 +1627,10 @@ 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 ((!view().rules().enabled(featureHookOnV2_1) ||
defHookOn.has_value()) &&
*defHookOn == *newHookOn)
if (*defHookOn == *newHookOn)
{
if (newHook.isFieldPresent(sfHookOn))
newHook.makeFieldAbsent(sfHookOn);
@@ -1705,22 +1665,6 @@ 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)
{
@@ -1899,8 +1843,7 @@ SetHook::setHook()
newHookDef->setFieldH256(sfHookHash, *createHookHash);
// only HookOn or (HookOnOutgoing and HookOnIncoming)
if ((!view().rules().enabled(featureHookOnV2) &&
!view().rules().enabled(featureHookOnV2_1)) ||
if (!view().rules().enabled(featureHookOnV2) ||
(!newHookOnOutgoing && !newHookOnIncoming))
newHookDef->setFieldH256(sfHookOn, *newHookOn);
else
@@ -2230,7 +2173,10 @@ SetHook::setHook()
else if (oldHookSLE && !newHooksEmpty)
{
// UPDATE ltHOOK
(*newHookSLE)[sfOwnerNode] = (*oldHookSLE)[sfOwnerNode];
if (view().rules().enabled(fixXahauV1))
{
(*newHookSLE)[sfOwnerNode] = (*oldHookSLE)[sfOwnerNode];
}
view().erase(oldHookSLE);
view().insert(newHookSLE);
}
+1 -1
View File
@@ -86,7 +86,7 @@ public:
calculateBaseFee(ReadView const& view, STTx const& tx);
static HookSetOperation
inferOperation(SetHookCtx& ctx, STObject const& hookSetObj);
inferOperation(STObject const& hookSetObj);
static HookSetValidation
validateHookSetEntry(SetHookCtx& ctx, STObject const& hookSetObj);
-404
View File
@@ -1,404 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/tx/detail/SetManifest.h>
#include <xrpld/core/Config.h>
#include <xrpld/ledger/View.h>
#include <xrpl/basics/Log.h>
#include <xrpl/basics/StringUtilities.h> // strUnHex
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h> // mulRatio
#include <xrpl/protocol/serialize.h>
#include <xrpl/protocol/st.h>
namespace ripple {
TxConsequences
SetManifest::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
SetManifest::preflight(PreflightContext const& ctx)
{
if (!ctx.rules.enabled(featureOnChainManifests))
return temDISABLED;
if (auto const ret = preflight1(ctx); !isTesSuccess(ret))
return ret;
auto& tx = ctx.tx;
auto& j = ctx.j;
if (tx.getFlags() & tfUniversalMask)
{
JLOG(j.warn()) << "SetManifest: Invalid flags set.";
return temINVALID_FLAG;
}
// rules:
// 1. sfManifest must match the manifest template and be validly signed
// 2. the signingpubkey must match the master r-address
// 3. manifest must not be already revoked
STObject const& obj =
const_cast<ripple::STTx&>(tx).getField(sfManifest).downcast<STObject>();
// 1. sfManifest must match the manifest template and be validly signed
auto manifest = deserializeManifest(obj, j);
if (!manifest.has_value())
{
JLOG(j.warn())
<< "SetManifest: invalid manifest passed (parseManifest failed).";
return temMALFORMED;
}
if (!manifest->verify())
{
JLOG(j.warn())
<< "SetManifest: invalid manifest passed (manifest.verify failed).";
return temMALFORMED;
}
// 2. master must match r-address
auto wantID = calcAccountID(manifest->masterKey);
if (ctx.tx.getAccountID(sfAccount) != wantID)
{
JLOG(j.warn())
<< "SetManifest: master key must match sfAccount (r-address).";
return temMALFORMED;
}
// 3. not already revoked will be checked in preclaim because it depends on
// lgr state
// 4. the envelope carries no account signature: authority comes solely
// from the manifest's own master/ephemeral signatures, which do not cover
// the envelope. Pin every envelope field a relayer could otherwise choose.
// The shape below must match the one checkValidity() recognises, or the
// txn falls through to the ordinary signature path and is rejected there.
// sfFee cannot be bounded here because the computed base fee is not in
// scope until preclaim; checkFee() bounds it instead.
if (!tx.isFieldPresent(sfSigningPubKey) || !tx.getSigningPubKey().empty() ||
!tx.isFieldPresent(sfTxnSignature) || !tx.getSignature().empty() ||
tx.isFieldPresent(sfSigners) || tx.isFieldPresent(sfAccountTxnID) ||
tx.isFieldPresent(sfTicketSequence) || tx.getFieldU32(sfSequence) != 0)
{
JLOG(j.warn())
<< "SetManifest: envelope must be unsigned with Sequence 0.";
return temMALFORMED;
}
return preflight2(ctx);
}
TER
SetManifest::preclaim(PreclaimContext const& ctx)
{
if (!ctx.view.rules().enabled(featureOnChainManifests))
return temDISABLED;
auto const id = ctx.tx[sfAccount];
// The account must exist: it pays the fee and anchors sfManifestID.
auto const sle = ctx.view.read(keylet::account(id));
if (!sle)
return terNO_ACCOUNT;
STObject const& newObj = const_cast<ripple::STTx&>(ctx.tx)
.getField(sfManifest)
.downcast<STObject>();
auto const newManifest = deserializeManifest(newObj, ctx.j);
if (!newManifest)
return tefINTERNAL; // preflight already parsed this successfully
// Replay protection. A byte-identical resubmission is rejected as
// tefALREADY by checkPriorTxAndLastLedger, but sfFee may vary within the
// band checkFee() allows, so the same manifest can also arrive under a
// different txid. The strictly-increasing sequence test below is what
// covers that, both within this ledger and in every later one. Either
// result is tef, so a replay is never included and never claims a fee.
if (sle->isFieldPresent(sfManifestID))
{
// A dangling sfManifestID is a corrupt ledger; doApply reports it.
if (auto const sleOld = ctx.view.read(
Keylet{ltMANIFEST, sle->getFieldH256(sfManifestID)}))
{
if (sleOld->getFieldU32(sfSequence) ==
std::numeric_limits<std::uint32_t>::max())
{
JLOG(ctx.j.warn()) << "SetManifest: New manifest submitted for "
"revoked master. "
<< id;
return tefREVOKED_MANIFEST;
}
if (newManifest->sequence <= sleOld->getFieldU32(sfSequence))
{
JLOG(ctx.j.warn())
<< "SetManifest: Manifest sequence already passed. " << id;
return tefPAST_MANIFEST_SEQ;
}
}
}
// On-chain equivalent of the badMasterKey/badEphemeralKey sanity checks in
// ManifestCache::applyManifest. keylet::manifest(masterKey) is 1:1 with the
// account, but keylet::manifest(signingKey) is not: without this, a
// manifest naming another validator's key as its ephemeral key would
// collide with -- and clobber -- that validator's object.
if (newManifest->signingKey)
{
auto const sleEph =
ctx.view.read(keylet::manifest(*newManifest->signingKey));
if (sleEph && sleEph->getAccountID(sfAccount) != id)
{
JLOG(ctx.j.warn())
<< "SetManifest: Ephemeral key already claimed by another "
"account. "
<< id;
return tecDUPLICATE;
}
}
return tesSUCCESS;
}
TER
SetManifest::doApply()
{
auto sle = view().peek(keylet::account(account_));
if (!sle)
return tefINTERNAL;
STObject const& obj = const_cast<ripple::STTx&>(ctx_.tx)
.getField(sfManifest)
.downcast<STObject>();
auto const manifest = deserializeManifest(obj, j_);
// Both of these were established in preflight.
if (!manifest || calcAccountID(manifest->masterKey) != account_)
return tefINTERNAL;
// A manifest is stored twice so it can be found from either key:
// keylet::manifest(masterKey) -> obj1, sfManifestID -> obj2
// keylet::manifest(signingKey) -> obj2, sfManifestID -> obj1
// A revoked manifest has no signing key, so it exists only as obj1 with no
// sfManifestID. Both copies are erased and rewritten on every update so
// they can never drift apart.
if (sle->isFieldPresent(sfManifestID))
{
uint256 const firstID = sle->getFieldH256(sfManifestID);
auto const sleMan1 = view().peek(Keylet{ltMANIFEST, firstID});
if (!sleMan1 || sleMan1->getAccountID(sfAccount) != account_)
{
JLOG(j_.error()) << "SetManifest: Old manifest object missing or "
"misowned (ID1) !! "
<< strHex(firstID);
return tefBAD_LEDGER;
}
// Absent when the previous manifest was a revocation.
if (sleMan1->isFieldPresent(sfManifestID))
{
uint256 const secondID = sleMan1->getFieldH256(sfManifestID);
auto const sleMan2 = view().peek(Keylet{ltMANIFEST, secondID});
if (secondID == firstID || !sleMan2 ||
sleMan2->getAccountID(sfAccount) != account_)
{
JLOG(j_.error())
<< "SetManifest: Old manifest object missing, misowned or "
"self-referential (ID2) !! "
<< strHex(secondID);
return tefBAD_LEDGER;
}
view().erase(sleMan2);
}
view().erase(sleMan1);
}
Keylet const klMan1 = keylet::manifest(manifest->masterKey);
std::optional<Keylet> klMan2;
if (!manifest->revoked() && manifest->signingKey)
klMan2 = keylet::manifest(*manifest->signingKey);
// Neither key may still be occupied: preclaim rejects an ephemeral key held
// by another account, and the block above cleared this account's own
// copies.
if (view().exists(klMan1) || (klMan2 && view().exists(*klMan2)))
{
JLOG(j_.error()) << "SetManifest: Manifest keylet already occupied !! "
<< strHex(klMan1.key);
return tefBAD_LEDGER;
}
// Mirror the manifest losslessly, signatures included, so any node can
// reconstruct and independently verify it (ManifestCache::applyLedger).
// Field *presence* is copied faithfully: sfVersion is soeDEFAULT in the
// manifest format, so materialising an absent one would alter the signed
// payload and break verification.
auto const write = [&](Keylet const& kl,
std::optional<uint256> const& other) {
auto sleMan = std::make_shared<SLE>(kl);
sleMan->setAccountID(sfAccount, account_);
sleMan->setFieldU32(sfSequence, obj.getFieldU32(sfSequence));
sleMan->setFieldVL(sfPublicKey, obj.getFieldVL(sfPublicKey));
sleMan->setFieldVL(
sfMasterSignature, obj.getFieldVL(sfMasterSignature));
if (obj.isFieldPresent(sfVersion))
sleMan->setFieldU16(sfVersion, obj.getFieldU16(sfVersion));
if (obj.isFieldPresent(sfSigningPubKey))
sleMan->setFieldVL(
sfSigningPubKey, obj.getFieldVL(sfSigningPubKey));
if (obj.isFieldPresent(sfSignature))
sleMan->setFieldVL(sfSignature, obj.getFieldVL(sfSignature));
if (obj.isFieldPresent(sfDomain))
sleMan->setFieldVL(sfDomain, obj.getFieldVL(sfDomain));
if (other)
sleMan->setFieldH256(sfManifestID, *other);
view().insert(sleMan);
};
write(klMan1, klMan2 ? std::optional<uint256>{klMan2->key} : std::nullopt);
if (klMan2)
write(*klMan2, klMan1.key);
sle->setFieldH256(sfManifestID, klMan1.key);
view().update(sle);
return tesSUCCESS;
}
XRPAmount
SetManifest::calculateBaseFee(ReadView const& view, STTx const& tx)
{
XRPAmount manifestFee{0};
if (tx.isFieldPresent(sfManifest))
{
STObject const& obj = const_cast<ripple::STTx&>(tx)
.getField(sfManifest)
.downcast<STObject>();
// one drop per byte
manifestFee = XRPAmount{obj.getSerializer().getDataLength()};
}
return Transactor::calculateBaseFee(view, tx) + manifestFee;
}
/** The most sfFee may be: the same 1.2x headroom Submit applies.
Kept in one place so the value Submit writes and the value preclaim will
accept cannot drift apart.
*/
static XRPAmount
manifestFeeCeiling(XRPAmount baseFee)
{
return mulRatio(baseFee, 12, 10, /*roundUp*/ true);
}
TER
SetManifest::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
{
// A ceiling is required because the envelope carries no account signature,
// so sfFee is chosen by whoever relays the txn -- and manifests are public:
// they are gossiped over the peer protocol and embedded in published UNLs,
// so the relayer need not be the master key holder. Uncapped, any observer
// of a not-yet-recorded manifest could wrap it with sfFee set to that
// validator's entire balance. The 20% band is headroom against a fee floor
// that has risen since the txn was built, and bounds what an attacker can
// burn to the same 20%.
if (ctx.tx[sfFee].xrp() > manifestFeeCeiling(baseFee))
{
JLOG(ctx.j.trace()) << "SetManifest: fee above ceiling: "
<< to_string(ctx.tx[sfFee].xrp());
return temBAD_FEE;
}
// Floor and balance are the ordinary rules.
return Transactor::checkFee(ctx, baseFee);
}
std::optional<std::string>
makeSetManifestTx(
Slice const& manifest,
std::uint32_t networkID,
ReadView const& openView,
beast::Journal j)
{
try
{
auto const man = deserializeManifest(manifest, j);
if (!man || !man->verify())
return std::nullopt;
auto const encode = [&](XRPAmount fee) {
return serializeHex(STTx(ttMANIFEST_SET, [&](STObject& obj) {
obj.setAccountID(sfAccount, calcAccountID(man->masterKey));
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfNetworkID, networkID);
obj.setFieldAmount(sfFee, fee);
obj.setFieldVL(sfSigningPubKey, std::vector<std::uint8_t>{});
obj.setFieldVL(sfTxnSignature, std::vector<std::uint8_t>{});
// sfManifest is soeREQUIRED, so STObject::set(SOTemplate) has
// already materialised it as a present, empty object. Fill
// that one in: emitting a second is a duplicate field, which
// STObject::set(SerialIter&) rejects on the way back in.
SerialIter mit{manifest};
obj.peekFieldObject(sfManifest).set(mit);
}));
};
// calculateBaseFee() takes a parsed transaction, so encode once with a
// placeholder fee purely to have something to price. The resulting fee
// does not depend on the placeholder: it is derived from the length of
// the manifest object and the ledger's base fee.
auto const priced = strUnHex(encode(XRPAmount{0}));
if (!priced || priced->empty())
return std::nullopt;
SerialIter sit{makeSlice(*priced)};
STTx const probe{std::ref(sit)};
// Submit the ceiling exactly. preclaim rejects anything above it, and
// the floor rises with network load, so the ceiling is both always
// acceptable and the value most likely to still clear the floor by the
// time the transaction is applied.
return encode(
manifestFeeCeiling(SetManifest::calculateBaseFee(openView, probe)));
}
catch (std::exception const& e)
{
JLOG(j.warn()) << "makeSetManifestTx: " << e.what();
return std::nullopt;
}
}
} // namespace ripple
-90
View File
@@ -1,90 +0,0 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2024 XRPL-Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_TX_SETMANIFEST_H_INCLUDED
#define RIPPLE_TX_SETMANIFEST_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpld/core/Config.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Indexes.h>
namespace ripple {
/** Encode the transaction that publishes `manifest` on-ledger.
A manifest transaction carries no account signature, so the protocol pins
the whole envelope: Sequence must be 0, SigningPubKey and TxnSignature must
be empty, and Fee must fall between the computed base fee and a ceiling
above it. SetManifest::preflight and SetManifest::checkFee reject anything
else. Every caller that submits a manifest builds it here so those rules
cannot drift apart from the ones the transactor enforces.
Returns hex rather than an STTx because the manifest is appended to the
encoded transaction verbatim, behind its object marker, instead of being
parsed and re-emitted: the bytes the master key signed survive untouched,
and a future change to the manifest format needs no change here.
@param manifest Serialized manifest
@param networkID Network the transaction is for
@param openView Ledger the fee is priced against
@param j Journal
@return the hex-encoded transaction, or nullopt if the manifest does not
parse or does not verify
*/
std::optional<std::string>
makeSetManifestTx(
Slice const& manifest,
std::uint32_t networkID,
ReadView const& openView,
beast::Journal j);
class SetManifest : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit SetManifest(ApplyContext& ctx) : Transactor(ctx)
{
}
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
// Hides Transactor::checkFee; applySteps dispatches this as T::checkFee.
static TER
checkFee(PreclaimContext const& ctx, XRPAmount baseFee);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
static TER
preclaim(PreclaimContext const&);
TER
doApply() override;
};
} // namespace ripple
#endif
+6
View File
@@ -30,6 +30,12 @@
namespace ripple {
TxConsequences
SetRemarks::makeTxConsequences(PreflightContext const& ctx)
{
return TxConsequences{ctx.tx, TxConsequences::normal};
}
NotTEC
SetRemarks::validateRemarks(STArray const& remarks, beast::Journal const& j)
{
+4 -1
View File
@@ -30,7 +30,7 @@ namespace ripple {
class SetRemarks : public Transactor
{
public:
static constexpr ConsequencesFactoryType ConsequencesFactory{Normal};
static constexpr ConsequencesFactoryType ConsequencesFactory{Custom};
explicit SetRemarks(ApplyContext& ctx) : Transactor(ctx)
{
@@ -39,6 +39,9 @@ public:
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TxConsequences
makeTxConsequences(PreflightContext const& ctx);
static NotTEC
preflight(PreflightContext const& ctx);
+32 -35
View File
@@ -155,11 +155,7 @@ preflight1(PreflightContext const& ctx)
!ctx.rules.enabled(featureNamedHooks))
return temMALFORMED;
auto const& name = ctx.tx.getFieldVL(sfHookName);
if (name.size() == 0 && ctx.rules.enabled(fix20261001))
return temMALFORMED;
if (!SetHook::validateHookName(name, ctx.j))
if (!SetHook::validateHookName(ctx.tx.getFieldVL(sfHookName), ctx.j))
return temMALFORMED;
}
@@ -287,7 +283,19 @@ Transactor::calculateHookChainFee(
// LCOV_EXCL_STOP
}
auto const hookName = hookObj[~sfHookName];
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;
}
uint32_t flags = 0;
if (hookObj.isFieldPresent(sfFlags))
@@ -299,7 +307,7 @@ Transactor::calculateHookChainFee(
uint256 hookOn = hook::getHookOn(
hookObj, hookDef, isOutgoing ? sfHookOnOutgoing : sfHookOnIncoming);
if (hook::canHook(tx, hookOn, hookName) &&
if (hook::canHook(tx.getTxnType(), hookOn) &&
(!collectCallsOnly || (flags & hook::hsfCOLLECT)))
{
XRPAmount const toAdd{hookDef->getFieldAmount(sfFee).xrp().drops()};
@@ -400,7 +408,7 @@ Transactor::calculateBaseFee(ReadView const& view, STTx const& tx)
XRPAmount accumulator = baseFee;
if (view.rules().enabled(featureHooks) &&
tx.isFieldPresent(sfHookParameters))
view.rules().enabled(fixXahauV1) && tx.isFieldPresent(sfHookParameters))
{
uint64_t paramBytes = 0;
auto const& params = tx.getFieldArray(sfHookParameters);
@@ -596,13 +604,6 @@ Transactor::checkSeqProxy(
return terNO_ACCOUNT;
}
// A manifest txn is derived deterministically from the manifest alone, so
// it cannot depend on account state: preflight pins sfSequence to 0 and the
// account sequence is neither checked here nor consumed below.
if (view.rules().enabled(featureOnChainManifests) &&
tx.getTxnType() == ttMANIFEST_SET)
return tesSUCCESS;
SeqProxy const a_seq = SeqProxy::sequence((*sle)[sfSequence]);
// pass all emitted tx provided their seq is 0
@@ -704,7 +705,8 @@ Transactor::checkPriorTxAndLastLedger(PreclaimContext const& ctx)
if (ctx.view.txExists(ctx.tx.getTransactionID()))
return tefALREADY;
if (hook::isEmittedTxn(ctx.tx) && ctx.view.rules().enabled(featureHooks))
if (hook::isEmittedTxn(ctx.tx) && ctx.view.rules().enabled(featureHooks) &&
ctx.view.rules().enabled(fixXahauV2))
{
// check if the emitted txn exists on ledger and is in the emission
// directory if not that's a re-apply so discard
@@ -753,17 +755,6 @@ Transactor::consumeSeqProxy(SLE::pointer const& sleAccount)
if (ctx_.isEmittedTxn())
return tesSUCCESS;
// Manifest txns get the same treatment: pinned to sfSequence 0 and not
// signed by the account, so they neither consume nor reset its sequence.
// Doing so would be actively harmful -- the write below is
// seqProx.value() + 1, which for a seq-0 txn sets the account sequence to
// 1 and makes every previously used sequence replayable. Handling it here
// rather than in apply() also covers reset(), which re-consumes on the
// tec / failed-invariant path.
if (view().rules().enabled(featureOnChainManifests) &&
ctx_.tx.getTxnType() == ttMANIFEST_SET)
return tesSUCCESS;
SeqProxy const seqProx = ctx_.tx.getSeqProxy();
if (seqProx.isSeq())
{
@@ -907,12 +898,6 @@ Transactor::checkSign(PreclaimContext const& ctx)
ctx.tx.getTxnType() == ttIMPORT)
return tesSUCCESS;
// pass ttMANIFEST_SETs, their signatures are checked in preflight against
// the manifest's internal key logic
if (ctx.view.rules().enabled(featureOnChainManifests) &&
ctx.tx.getTxnType() == ttMANIFEST_SET)
return tesSUCCESS;
if (ctx.flags & tapDRY_RUN)
{
// This code must be different for `simulate`
@@ -1369,13 +1354,25 @@ Transactor::executeHookChain(
// LCOV_EXCL_STOP
}
auto const hookName = hookObj[~sfHookName];
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;
}
// check if the hook can fire
uint256 hookOn = hook::getHookOn(
hookObj, hookDef, isOutgoing ? sfHookOnOutgoing : sfHookOnIncoming);
if (!hook::canHook(ctx_.tx, hookOn, hookName))
if (!hook::canHook(ctx_.tx.getTxnType(), hookOn))
continue; // skip if it can't
uint256 hookCanEmit = hook::getHookCanEmit(hookObj, hookDef);
+673 -154
View File
@@ -20,7 +20,6 @@
#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>
@@ -30,6 +29,33 @@
namespace ripple {
TER
checkBroker(
ReadView const& view,
AccountID const& broker,
AccountID const& owner,
STAmount const& saleAmount,
STAmount const& purchaseAmount)
{
auto const sleBroker = view.read(keylet::account(broker));
if (!sleBroker)
return tecNO_TARGET;
if (sleBroker->isFieldPresent(sfAMMID))
return tecNO_PERMISSION;
if (broker == owner || saleAmount == beast::zero ||
purchaseAmount == saleAmount)
return tecNO_PERMISSION;
if (!purchaseAmount.native() && broker != purchaseAmount.getIssuer() &&
!view.exists(keylet::line(
broker, purchaseAmount.getIssuer(), purchaseAmount.getCurrency())))
return tecNO_LINE;
return tesSUCCESS;
}
NotTEC
URIToken::preflight(PreflightContext const& ctx)
{
@@ -79,11 +105,16 @@ URIToken::preflight(PreflightContext const& ctx)
}
}
// return temMALFORMED if sfDestination field is present
// fix amendment to return temMALFORMED if sfDestination field is present
// and sfAmount field is not present
if (ctx.tx.isFieldPresent(sfDestination) &&
!ctx.tx.isFieldPresent(sfAmount))
return temMALFORMED;
if (ctx.rules.enabled(fixXahauV1))
{
if (ctx.tx.isFieldPresent(sfDestination) &&
!ctx.tx.isFieldPresent(sfAmount))
{
return temMALFORMED;
}
}
// the validation for the URI field is also the same regardless of the txn
// type
@@ -99,7 +130,7 @@ URIToken::preflight(PreflightContext const& ctx)
return temMALFORMED;
}
if (!isValidUTF8(makeSlice(uri)))
if (!validateUTF8(uri))
{
JLOG(ctx.j.warn()) << "Malformed transaction. URI must be a "
"valid utf-8 string.";
@@ -134,12 +165,31 @@ URIToken::preflight(PreflightContext const& ctx)
ctx.tx.getAccountID(sfAccount) == ctx.tx.getAccountID(sfDestination))
return temREDUNDANT;
if (ctx.tx.isFieldPresent(sfBrokerAccount))
{
if (tt != ttURITOKEN_BUY)
return temMALFORMED;
if (!ctx.rules.enabled(featureURITokenBroker) ||
!ctx.rules.enabled(fixXahauV1))
return temDISABLED;
auto const broker = ctx.tx.getAccountID(sfBrokerAccount);
if (broker == beast::zero)
return temMALFORMED;
if (broker == ctx.tx.getAccountID(sfAccount))
return temREDUNDANT;
}
return preflight2(ctx);
}
TER
URIToken::preclaim(PreclaimContext const& ctx)
{
bool const fixV1 = ctx.view.rules().enabled(fixXahauV1);
std::shared_ptr<SLE const> sleU;
uint32_t leFlags;
std::optional<AccountID> issuer;
@@ -229,42 +279,89 @@ URIToken::preclaim(PreclaimContext const& ctx)
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
if (purchaseAmount.native() && saleAmount->native())
if (ctx.tx.isFieldPresent(sfBrokerAccount))
{
// native transfer
if (!ctx.view.rules().enabled(featureURITokenBroker) || !fixV1)
return temDISABLED;
STAmount needed{ctx.view.fees().accountReserve(
sle->getFieldU32(sfOwnerCount) + 1)};
STAmount const fee = ctx.tx.getFieldAmount(sfFee).xrp();
if (needed + fee < needed)
return tecINTERNAL;
needed += fee;
if (needed + purchaseAmount < needed)
return tecINTERNAL;
needed += purchaseAmount;
if (needed > sle->getFieldAmount(sfBalance))
return tecINSUFFICIENT_FUNDS;
if (TER const result = checkBroker(
ctx.view,
ctx.tx.getAccountID(sfBrokerAccount),
*owner,
*saleAmount,
purchaseAmount);
!isTesSuccess(result))
return result;
}
else if (purchaseAmount.native() || saleAmount->native())
if (fixV1)
{
// should not be able to happen
return tecINTERNAL;
if (purchaseAmount.native() && saleAmount->native())
{
// native transfer
STAmount needed{ctx.view.fees().accountReserve(
sle->getFieldU32(sfOwnerCount) + 1)};
STAmount const fee = ctx.tx.getFieldAmount(sfFee).xrp();
if (needed + fee < needed)
return tecINTERNAL;
needed += fee;
if (needed + purchaseAmount < needed)
return tecINTERNAL;
needed += purchaseAmount;
if (needed > sle->getFieldAmount(sfBalance))
return tecINSUFFICIENT_FUNDS;
}
else if (purchaseAmount.native() || saleAmount->native())
{
// should not be able to happen
return tecINTERNAL;
}
else
{
// iou transfer
STAmount availableFunds{accountFunds(
ctx.view,
acc,
purchaseAmount,
fhZERO_IF_FROZEN,
ctx.j)};
if (purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
}
}
else
{
// iou transfer
// old logic
STAmount availableFunds{accountFunds(
ctx.view, acc, purchaseAmount, fhZERO_IF_FROZEN, ctx.j)};
if (purchaseAmount.native() && saleAmount->native())
{
// if it's an xrp sale/purchase then no trustline needed
if (purchaseAmount >
(sleOwner->getFieldAmount(sfBalance) - ctx.tx[sfFee]))
return tecINSUFFICIENT_FUNDS;
}
else
{
// iou
STAmount availableFunds{accountFunds(
ctx.view,
acc,
purchaseAmount,
fhZERO_IF_FROZEN,
ctx.j)};
if (purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
if (purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
}
}
return tesSUCCESS;
}
@@ -305,6 +402,8 @@ URIToken::doApply()
Sandbox sb(&ctx_.view());
bool const fixV1 = sb.rules().enabled(fixXahauV1);
auto const sle = sb.peek(keylet::account(account_));
if (!sle)
return tefINTERNAL;
@@ -443,134 +542,552 @@ URIToken::doApply()
if (purchaseAmount.issue() != saleAmount->issue())
return temBAD_CURRENCY;
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
std::optional<AccountID> const broker = ctx_.tx[~sfBrokerAccount];
if (broker &&
(!sb.rules().enabled(featureURITokenBroker) || !fixV1))
return temDISABLED;
// if it's an xrp sale/purchase then no trustline needed
if (purchaseAmount.native())
if (fixV1)
{
STAmount needed{sb.fees().accountReserve(
sle->getFieldU32(sfOwnerCount) + 1)};
// this is the reworked version of the buy routine
STAmount const fee = ctx_.tx.getFieldAmount(sfFee).xrp();
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
if (needed + fee < needed)
return tecINTERNAL;
needed += fee;
if (needed + purchaseAmount < needed)
return tecINTERNAL;
needed += purchaseAmount;
if (needed > mPriorBalance)
return tecINSUFFICIENT_FUNDS;
}
else
{
// IOU sale
if (TER result = trustTransferAllowed(
sb, {account_, *owner}, purchaseAmount.issue(), j);
!isTesSuccess(result))
if (broker)
{
JLOG(j.trace())
<< "URIToken::doApply trustTransferAllowed result="
<< result;
return result;
if (TER const result = checkBroker(
sb, *broker, *owner, *saleAmount, purchaseAmount);
!isTesSuccess(result))
return result;
}
if (STAmount availableFunds{accountFunds(
// if it's an xrp sale/purchase then no trustline needed
if (purchaseAmount.native())
{
STAmount needed{sb.fees().accountReserve(
sle->getFieldU32(sfOwnerCount) + 1)};
STAmount const fee = ctx_.tx.getFieldAmount(sfFee).xrp();
if (needed + fee < needed)
return tecINTERNAL;
needed += fee;
if (needed + purchaseAmount < needed)
return tecINTERNAL;
needed += purchaseAmount;
if (needed > mPriorBalance)
return tecINSUFFICIENT_FUNDS;
}
else
{
// IOU sale
if (TER result = trustTransferAllowed(
sb, {account_, *owner}, purchaseAmount.issue(), j);
!isTesSuccess(result))
{
JLOG(j.trace())
<< "URIToken::doApply trustTransferAllowed result="
<< result;
return result;
}
if (broker)
{
if (TER result = trustTransferAllowed(
sb,
{account_, *broker},
purchaseAmount.issue(),
j);
!isTesSuccess(result))
{
JLOG(j.trace()) << "URIToken::doApply broker "
"trustTransferAllowed result="
<< result;
return result;
}
}
if (STAmount availableFunds{accountFunds(
sb, account_, purchaseAmount, fhZERO_IF_FROZEN, j)};
purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
}
// execute the funds transfer, we'll check reserves last
if (TER result = accountSend(
sb,
account_,
*owner,
broker ? *saleAmount : purchaseAmount,
j,
WaiveTransferFee::No,
false);
!isTesSuccess(result))
return result;
if (broker)
{
STAmount const spread = purchaseAmount - *saleAmount;
if (spread <= beast::zero)
return tecINTERNAL; // LCOV_EXCL_LINE
if (TER result = accountSend(
sb,
account_,
*broker,
spread,
j,
WaiveTransferFee::No,
false);
!isTesSuccess(result))
return result;
}
// add token to new owner dir
auto const newPage = sb.dirInsert(
keylet::ownerDir(account_),
*kl,
describeOwnerDir(account_));
JLOG(j_.trace()) << "Adding URIToken to owner directory "
<< to_string(kl->key) << ": "
<< (newPage ? "success" : "failure");
if (!newPage)
return tecDIR_FULL;
// remove from current owner directory
if (!sb.dirRemove(
keylet::ownerDir(*owner),
sleU->getFieldU64(sfOwnerNode),
kl->key,
true))
{
JLOG(j.fatal())
<< "Could not remove URIToken from owner directory";
return tefBAD_LEDGER;
}
// adjust owner counts
adjustOwnerCount(sb, sleOwner, -1, j);
adjustOwnerCount(sb, sle, 1, j);
// clean the offer off the object
sleU->makeFieldAbsent(sfAmount);
if (sleU->isFieldPresent(sfDestination))
sleU->makeFieldAbsent(sfDestination);
// set the new owner of the object
sleU->setAccountID(sfOwner, account_);
// tell the ledger where to find it
sleU->setFieldU64(sfOwnerNode, *newPage);
// check each side has sufficient balance remaining to cover the
// updated ownercounts
auto hasSufficientReserve =
[&](std::shared_ptr<SLE> const& sle) -> bool {
std::uint32_t const uOwnerCount =
sle->getFieldU32(sfOwnerCount);
return sle->getFieldAmount(sfBalance) >=
sb.fees().accountReserve(uOwnerCount);
};
if (!hasSufficientReserve(sle))
{
JLOG(j.trace()) << "URIToken: buyer " << account_
<< " has insufficient reserve to buy";
return tecINSUFFICIENT_RESERVE;
}
// This should only happen if the owner burned their reserves
// below the needed amount via another transactor. If this
// happens they should top up their account before selling!
if (!hasSufficientReserve(sleOwner))
{
JLOG(j.warn())
<< "URIToken: seller " << *owner
<< " has insufficient reserve to allow purchase!";
return tecINSUF_RESERVE_SELLER;
}
sb.update(sle);
sb.update(sleU);
sb.update(sleOwner);
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
// old logic
{
STAmount const purchaseAmount =
ctx_.tx.getFieldAmount(sfAmount);
bool const sellerLow = purchaseAmount.getIssuer() > *owner;
bool const buyerLow = purchaseAmount.getIssuer() > account_;
bool sellerIssuer = purchaseAmount.getIssuer() == *owner;
bool buyerIssuer = purchaseAmount.getIssuer() == account_;
// check if the seller has listed it at all
if (!saleAmount)
return tecNO_PERMISSION;
// check if the seller has listed it for sale to a specific
// account
if (dest && *dest != account_)
return tecNO_PERMISSION;
if (purchaseAmount.issue() != saleAmount->issue())
return temBAD_CURRENCY;
std::optional<STAmount> initBuyerBal;
std::optional<STAmount> initSellerBal;
std::optional<STAmount> finBuyerBal;
std::optional<STAmount> finSellerBal;
std::optional<STAmount> dstAmt;
std::optional<Keylet> tlSeller;
std::shared_ptr<SLE> sleDstLine;
std::shared_ptr<SLE> sleSrcLine;
// if it's an xrp sale/purchase then no trustline needed
if (purchaseAmount.native())
{
if (purchaseAmount < saleAmount)
return tecINSUFFICIENT_PAYMENT;
if (purchaseAmount >
((*sleOwner)[sfBalance] - ctx_.tx[sfFee]))
return tecINSUFFICIENT_FUNDS;
dstAmt = purchaseAmount;
initSellerBal = (*sleOwner)[sfBalance];
initBuyerBal = (*sle)[sfBalance];
finSellerBal = *initSellerBal + purchaseAmount;
finBuyerBal = *initBuyerBal - purchaseAmount;
}
else
{
// IOU sale
STAmount availableFunds{accountFunds(
sb, account_, purchaseAmount, fhZERO_IF_FROZEN, j)};
purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
// check for any possible bars to a buy transaction
// between these accounts for this asset
if (buyerIssuer)
{
// pass: issuer does not create own trustline
}
else
{
TER result = trustTransferAllowed(
sb, {account_, *owner}, purchaseAmount.issue(), j);
JLOG(j.trace())
<< "URIToken::doApply trustTransferAllowed result="
<< result;
if (!isTesSuccess(result))
return result;
}
if (purchaseAmount > availableFunds)
return tecINSUFFICIENT_FUNDS;
// check if the seller has a line
tlSeller = keylet::line(
*owner,
purchaseAmount.getIssuer(),
purchaseAmount.getCurrency());
Keylet tlBuyer = keylet::line(
account_,
purchaseAmount.getIssuer(),
purchaseAmount.getCurrency());
sleDstLine = sb.peek(*tlSeller);
sleSrcLine = sb.peek(tlBuyer);
if (sellerIssuer)
{
// pass: issuer does not create own trustline
}
else if (!sleDstLine)
{
// they do not, so we can create one if they have
// sufficient reserve
if (std::uint32_t const ownerCount = {sleOwner->at(
sfOwnerCount)};
(*sleOwner)[sfBalance] <
sb.fees().accountReserve(ownerCount + 1))
{
JLOG(j_.trace())
<< "Trust line does not exist. "
"Insufficent reserve to create line.";
return tecNO_LINE_INSUF_RESERVE;
}
}
if (buyerIssuer)
{
// pass: issuer does not adjust own trustline
initBuyerBal = purchaseAmount.zeroed();
finBuyerBal = purchaseAmount.zeroed();
}
else
{
// remove from buyer
initBuyerBal = buyerLow ? ((*sleSrcLine)[sfBalance])
: -((*sleSrcLine)[sfBalance]);
finBuyerBal = *initBuyerBal - purchaseAmount;
}
dstAmt = purchaseAmount;
static Rate const parityRate(QUALITY_ONE);
auto xferRate = transferRate(sb, saleAmount->getIssuer());
if (!sellerIssuer && !buyerIssuer && xferRate != parityRate)
{
dstAmt = multiplyRound(
purchaseAmount,
xferRate,
purchaseAmount.issue(),
true);
}
initSellerBal = !sleDstLine ? purchaseAmount.zeroed()
: sellerLow ? ((*sleDstLine)[sfBalance])
: -((*sleDstLine)[sfBalance]);
finSellerBal = *initSellerBal + *dstAmt;
}
// sanity check balance mutations (xrp or iou, both are checked
// the same way now)
if (*finSellerBal < *initSellerBal)
{
JLOG(j.warn())
<< "URIToken txid=" << ctx_.tx.getTransactionID() << " "
<< "finSellerBal < initSellerBal";
return tecINTERNAL;
}
if (*finBuyerBal > *initBuyerBal)
{
JLOG(j.warn())
<< "URIToken txid=" << ctx_.tx.getTransactionID() << " "
<< "finBuyerBal > initBuyerBal";
return tecINTERNAL;
}
if (*finBuyerBal < beast::zero)
{
JLOG(j.warn())
<< "URIToken txid=" << ctx_.tx.getTransactionID() << " "
<< "finBuyerBal < 0";
return tecINTERNAL;
}
if (*finSellerBal < beast::zero)
{
JLOG(j.warn())
<< "URIToken txid=" << ctx_.tx.getTransactionID() << " "
<< "finSellerBal < 0";
return tecINTERNAL;
}
// to this point no ledger changes have been made
// make them in a sensible order such that failure doesn't
// require cleanup
// add to new owner's directory first, this can fail if they
// have too many objects
auto const newPage = sb.dirInsert(
keylet::ownerDir(account_),
*kl,
describeOwnerDir(account_));
JLOG(j_.trace()) << "Adding URIToken to owner directory "
<< to_string(kl->key) << ": "
<< (newPage ? "success" : "failure");
if (!newPage)
{
// nothing has happened at all and there is nothing to clean
// up we can just leave with DIR_FULL
return tecDIR_FULL;
}
// Next create destination trustline where applicable. This
// could fail for a variety of reasons. If it does fail we need
// to remove the dir entry we just added to the buyer before we
// leave.
bool lineCreated = false;
if (!isXRP(purchaseAmount) && !sleDstLine && !sellerIssuer)
{
// clang-format off
if (TER const ter = trustCreate(
sb, // payment sandbox
sellerLow, // is dest low?
purchaseAmount.getIssuer(), // source
*owner, // destination
tlSeller->key, // ledger index
sleOwner, // Account to add to
false, // authorize account
(sleOwner->getFlags() & lsfDefaultRipple) == 0,
false, // freeze trust line
false, // deepfreeze trust line
*dstAmt, // initial balance zero
Issue(
purchaseAmount.getCurrency(),
*owner), // limit of zero
0, // quality in
0, // quality out
j); // journal
!isTesSuccess(ter))
{
// remove the newly inserted directory entry before we leave
//
if (!sb.dirRemove(keylet::ownerDir(account_), *newPage, kl->key, true))
{
JLOG(j.fatal())
<< "Could not remove URIToken from owner directory";
return tefBAD_LEDGER;
}
// leave
return ter;
}
// clang-format on
// add their trustline to their ownercount
lineCreated = true;
}
// execution to here means we added the URIToken to the buyer's
// directory and we definitely have a way to send the funds to
// the seller.
// remove from current owner directory
if (!sb.dirRemove(
keylet::ownerDir(*owner),
sleU->getFieldU64(sfOwnerNode),
kl->key,
true))
{
JLOG(j.fatal())
<< "Could not remove URIToken from owner directory";
// remove the newly inserted directory entry before we leave
if (!sb.dirRemove(
keylet::ownerDir(account_),
*newPage,
kl->key,
true))
{
JLOG(j.fatal()) << "Could not remove URIToken from "
"owner directory (2)";
}
// clean up any trustline we might have made
if (lineCreated)
{
auto line = sb.peek(*tlSeller);
if (line)
sb.erase(line);
}
return tefBAD_LEDGER;
}
// above is all the things that could fail. we now have swapped
// the ownership as far as the ownerdirs are concerned, and we
// have a place to pay to and from.
// if a trustline was created then the ownercount stays the same
// on the seller +1 TL -1 URIToken
if (!lineCreated && !isXRP(purchaseAmount))
adjustOwnerCount(sb, sleOwner, -1, j);
// the buyer gets a new object
adjustOwnerCount(sb, sle, 1, j);
// clean the offer off the object
sleU->makeFieldAbsent(sfAmount);
if (sleU->isFieldPresent(sfDestination))
sleU->makeFieldAbsent(sfDestination);
// set the new owner of the object
sleU->setAccountID(sfOwner, account_);
// tell the ledger where to find it
sleU->setFieldU64(sfOwnerNode, *newPage);
// update the buyer's balance
if (isXRP(purchaseAmount))
{
// the sale is for xrp, so set the balance
sle->setFieldAmount(sfBalance, *finBuyerBal);
}
else if (sleSrcLine)
{
// update the buyer's line to reflect the reduction of the
// purchase price
sleSrcLine->setFieldAmount(
sfBalance, buyerLow ? *finBuyerBal : -(*finBuyerBal));
}
else if (buyerIssuer)
{
// pass: buyer is issuer, no update required.
}
else
return tecINTERNAL;
// update the seller's balance
if (isXRP(purchaseAmount))
{
// the sale is for xrp, so set the balance
sleOwner->setFieldAmount(sfBalance, *finSellerBal);
}
else if (sleDstLine)
{
// the line already existed on the seller side so update it
sleDstLine->setFieldAmount(
sfBalance,
sellerLow ? *finSellerBal : -(*finSellerBal));
}
else if (lineCreated)
{
// pass, the TL already has this balance set on it at
// creation
}
else if (sellerIssuer)
{
// pass: seller is issuer, no update required.
}
else
return tecINTERNAL;
if (sleSrcLine)
sb.update(sleSrcLine);
if (sleDstLine)
sb.update(sleDstLine);
sb.update(sle);
sb.update(sleU);
sb.update(sleOwner);
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
// execute the funds transfer, we'll check reserves last
if (TER result = accountSend(
sb,
account_,
*owner,
purchaseAmount,
j,
WaiveTransferFee::No,
false);
!isTesSuccess(result))
return result;
// add token to new owner dir
auto const newPage = sb.dirInsert(
keylet::ownerDir(account_), *kl, describeOwnerDir(account_));
JLOG(j_.trace())
<< "Adding URIToken to owner directory " << to_string(kl->key)
<< ": " << (newPage ? "success" : "failure");
if (!newPage)
return tecDIR_FULL;
// remove from current owner directory
if (!sb.dirRemove(
keylet::ownerDir(*owner),
sleU->getFieldU64(sfOwnerNode),
kl->key,
true))
{
JLOG(j.fatal())
<< "Could not remove URIToken from owner directory";
return tefBAD_LEDGER;
}
// adjust owner counts
adjustOwnerCount(sb, sleOwner, -1, j);
adjustOwnerCount(sb, sle, 1, j);
// clean the offer off the object
sleU->makeFieldAbsent(sfAmount);
if (sleU->isFieldPresent(sfDestination))
sleU->makeFieldAbsent(sfDestination);
// set the new owner of the object
sleU->setAccountID(sfOwner, account_);
// tell the ledger where to find it
sleU->setFieldU64(sfOwnerNode, *newPage);
// check each side has sufficient balance remaining to cover the
// updated ownercounts
auto hasSufficientReserve =
[&](std::shared_ptr<SLE> const& sle) -> bool {
std::uint32_t const uOwnerCount =
sle->getFieldU32(sfOwnerCount);
return sle->getFieldAmount(sfBalance) >=
sb.fees().accountReserve(uOwnerCount);
};
if (!hasSufficientReserve(sle))
{
JLOG(j.trace()) << "URIToken: buyer " << account_
<< " has insufficient reserve to buy";
return tecINSUFFICIENT_RESERVE;
}
// This should only happen if the owner burned their reserves
// below the needed amount via another transactor. If this
// happens they should top up their account before selling!
if (!hasSufficientReserve(sleOwner))
{
JLOG(j.warn())
<< "URIToken: seller " << *owner
<< " has insufficient reserve to allow purchase!";
return tecINSUF_RESERVE_SELLER;
}
sb.update(sle);
sb.update(sleU);
sb.update(sleOwner);
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
case ttURITOKEN_BURN: {
@@ -600,8 +1117,10 @@ URIToken::doApply()
sb.erase(sleU);
adjustOwnerCount(sb, sleOwner, -1, j);
sb.update(sleOwner);
auto& sleAcc = fixV1 ? sleOwner : sle;
adjustOwnerCount(sb, sleAcc, -1, j);
sb.update(sleAcc);
sb.apply(ctx_.rawView());
return tesSUCCESS;
}
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more