mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-21 19:28:10 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
043dcb2a38 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
#
|
||||
|
||||
@@ -146,6 +146,8 @@ D686F2538F410C9D0D856788E98E3579595DAF7B38D38887F81ECAC934B06040 HooksUpdate1
|
||||
3C43D9A973AA4443EF3FC38E42DD306160FBFFDAB901CD8BAA15D09F2597EB87 NonFungibleTokensV1
|
||||
0285B7E5E08E1A8E4C15636F0591D87F73CB6A7B6452A932AD72BBC8E5D1CBE3 fixNFTokenDirV1
|
||||
36799EA497B1369B170805C078AEFE6188345F9B3E324C21E9CA3FF574E3C3D6 fixNFTokenNegOffer
|
||||
4C499D17719BB365B69010A436B64FD1A82AAB199FC1CEB06962EBD01059FB09 fixXahauV1
|
||||
215181D23BF5C173314B5FDB9C872C92DE6CC918483727DE037C0C13E7E6EE9D fixXahauV2
|
||||
0D8BF22FF7570D58598D1EF19EBB6E142AD46E59A223FD3816262FBB69345BEA Remit
|
||||
7CA0426E7F411D39BB014E57CD9E08F61DE1750F0D41FCD428D9FB80BB7596B0 ZeroB2M
|
||||
4B8466415FAB32FFA89D9DCBE166A42340115771DF611A7160F8D7439C87ECD8 fixNSDelete
|
||||
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
|
||||
@@ -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>)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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 \
|
||||
*/ \
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -194,8 +194,6 @@ enum TEFcodes : TERUnderlyingType {
|
||||
tefNONDIR_EMIT,
|
||||
tefIMPORT_BLACKHOLED,
|
||||
tefINVALID_LEDGER_FIX_TYPE,
|
||||
tefPAST_MANIFEST_SEQ,
|
||||
tefREVOKED_MANIFEST,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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."),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -30,6 +30,12 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
TxConsequences
|
||||
Cron::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
return TxConsequences{ctx.tx, TxConsequences::normal};
|
||||
}
|
||||
|
||||
NotTEC
|
||||
Cron::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
TxConsequences
|
||||
CronSet::makeTxConsequences(PreflightContext const& ctx)
|
||||
{
|
||||
return TxConsequences{ctx.tx, TxConsequences::normal};
|
||||
}
|
||||
|
||||
NotTEC
|
||||
CronSet::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in new issue
Block a user