mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-21 19:28:10 +00:00
Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
574593ea3d | ||
|
|
2566bcf6fa | ||
|
|
7f37aeac39 | ||
|
|
21fab66a17 | ||
|
|
0f3258d235 | ||
|
|
3fbeb130f8 | ||
|
|
58efea062c | ||
|
|
43d90b8549 | ||
|
|
a3bd76fdc9 | ||
|
|
4de217a2c6 | ||
|
|
5e49dc8474 | ||
|
|
77e309fff5 | ||
|
|
79319aaf96 | ||
|
|
a5163eeb50 | ||
|
|
3bc435864f | ||
|
|
4d08a9e603 | ||
|
|
f155556017 | ||
|
|
c4570a699f | ||
|
|
426ff27e42 | ||
|
|
e6bdd55803 | ||
|
|
9dd103fad4 | ||
|
|
a7f9c683c4 | ||
|
|
48a94af13d | ||
|
|
1ddda79c34 | ||
|
|
76b7111098 | ||
|
|
20f2b7f456 | ||
|
|
c6a292ca55 | ||
|
|
1ae3fb3f57 | ||
|
|
1ea6b76853 | ||
|
|
8f4569a63c | ||
|
|
b775f3c3c2 | ||
|
|
03d75df295 | ||
|
|
c2e253b365 | ||
|
|
ba130f8363 | ||
|
|
e87eabd8be | ||
|
|
2e32b5c6bb | ||
|
|
902ed9b492 |
No files matched your search
@@ -19,6 +19,15 @@ coverage:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 2%
|
||||
paths:
|
||||
# PeerImp is historically hard to exercise in the current unit-test
|
||||
# harness. Keep this list narrow; new testable code should remain
|
||||
# covered by the default patch gate.
|
||||
- "!src/xrpld/overlay/detail/PeerImp.cpp"
|
||||
historically-untested:
|
||||
target: 0%
|
||||
paths:
|
||||
- "src/xrpld/overlay/detail/PeerImp.cpp"
|
||||
changes: false
|
||||
|
||||
github_checks:
|
||||
|
||||
@@ -50,6 +50,10 @@ inputs:
|
||||
options:
|
||||
- libstdcxx
|
||||
- libcxx
|
||||
conan_deps_cxxflags:
|
||||
description: 'Extra cxxflags applied to Conan dependency package builds only (NOT the rippled build). JSON object keyed by Conan package pattern, e.g. {"grpc/*":["-Wno-foo"]}. Maps to <pattern>:tools.build:cxxflags.'
|
||||
required: false
|
||||
default: '{}'
|
||||
|
||||
outputs:
|
||||
cache-hit:
|
||||
@@ -81,6 +85,8 @@ runs:
|
||||
|
||||
- name: Configure Conan
|
||||
shell: bash
|
||||
env:
|
||||
CONAN_DEPS_CXXFLAGS: ${{ inputs.conan_deps_cxxflags }}
|
||||
run: |
|
||||
# Create the default profile directory if it doesn't exist
|
||||
mkdir -p ~/.conan2/profiles
|
||||
@@ -105,7 +111,14 @@ runs:
|
||||
os=${{ inputs.os }}
|
||||
EOF
|
||||
|
||||
# Add buildenv and conf sections for Linux (not needed for macOS)
|
||||
# [buildenv] + [conf] sections.
|
||||
# Linux pins compiler executables; macOS uses the system toolchain.
|
||||
# conan_deps_cxxflags (matrix-driven) optionally adds package-pattern
|
||||
# scoped tools.build:cxxflags for Conan dependency builds only - typically
|
||||
# grpc workarounds for newer clang's stricter diagnostics. Because these
|
||||
# are profile-pattern scoped (e.g. grpc/*:...), they do NOT affect the
|
||||
# consumer/rippled toolchain generated for the main build.
|
||||
NEED_CONF=0
|
||||
if [ "${{ inputs.os }}" = "Linux" ] && [ -n "${{ inputs.cc }}" ]; then
|
||||
cat >> ~/.conan2/profiles/default <<EOF
|
||||
|
||||
@@ -116,16 +129,38 @@ runs:
|
||||
[conf]
|
||||
tools.build:compiler_executables={"c": "/usr/bin/${{ inputs.cc }}", "cpp": "/usr/bin/${{ inputs.cxx }}"}
|
||||
EOF
|
||||
NEED_CONF=1
|
||||
fi
|
||||
|
||||
# Add macOS-specific conf if needed
|
||||
if [ "${{ inputs.os }}" = "Macos" ]; then
|
||||
cat >> ~/.conan2/profiles/default <<EOF
|
||||
if [ -n "${CONAN_DEPS_CXXFLAGS}" ] && [ "${CONAN_DEPS_CXXFLAGS}" != "{}" ]; then
|
||||
CONAN_DEPS_CXXFLAGS_LINES="$(python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
[conf]
|
||||
# Workaround for gRPC with newer Apple Clang
|
||||
tools.build:cxxflags=["-Wno-missing-template-arg-list-after-template-kw"]
|
||||
EOF
|
||||
raw = os.environ["CONAN_DEPS_CXXFLAGS"]
|
||||
data = json.loads(raw)
|
||||
if not isinstance(data, dict):
|
||||
sys.exit("conan_deps_cxxflags must be a JSON object like {\"grpc/*\": [\"-Wno-...\"]}")
|
||||
|
||||
for pattern, flags in data.items():
|
||||
if not isinstance(pattern, str) or not pattern:
|
||||
sys.exit("conan_deps_cxxflags keys must be non-empty Conan package patterns")
|
||||
if pattern == "&":
|
||||
sys.exit("conan_deps_cxxflags must target dependency package patterns, not the consumer (&)")
|
||||
if not isinstance(flags, list) or not all(isinstance(flag, str) for flag in flags):
|
||||
sys.exit(f"{pattern}: cxxflags must be a JSON string list")
|
||||
if flags:
|
||||
print(f"{pattern}:tools.build:cxxflags={json.dumps(flags, separators=(',', ':'))}")
|
||||
PY
|
||||
)"
|
||||
if [ -n "${CONAN_DEPS_CXXFLAGS_LINES}" ] && [ "$NEED_CONF" = "0" ]; then
|
||||
echo "" >> ~/.conan2/profiles/default
|
||||
echo "[conf]" >> ~/.conan2/profiles/default
|
||||
fi
|
||||
if [ -n "${CONAN_DEPS_CXXFLAGS_LINES}" ]; then
|
||||
printf '%s\n' "${CONAN_DEPS_CXXFLAGS_LINES}" >> ~/.conan2/profiles/default
|
||||
fi
|
||||
fi
|
||||
|
||||
# Display profile for verification
|
||||
|
||||
@@ -15,6 +15,9 @@ env:
|
||||
|
||||
jobs:
|
||||
checkout:
|
||||
# build publishes to build.xahau.tech, so this workflow only runs from
|
||||
# Xahau/xahaud. build and tests are skipped along with this job.
|
||||
if: github.repository == 'Xahau/xahaud'
|
||||
runs-on: [self-hosted, vanity]
|
||||
outputs:
|
||||
checkout_path: ${{ steps.vars.outputs.checkout_path }}
|
||||
@@ -86,10 +89,16 @@ jobs:
|
||||
cleanup:
|
||||
runs-on: [self-hosted, xahaud-build]
|
||||
needs: [tests, checkout]
|
||||
if: always()
|
||||
if: always() && github.repository == 'Xahau/xahaud'
|
||||
steps:
|
||||
- name: Cleanup workspace
|
||||
run: |
|
||||
CHECKOUT_PATH="${{ needs.checkout.outputs.checkout_path }}"
|
||||
# A run cancelled before checkout has no path; an empty one would
|
||||
# make the rm below delete every run's checkout on the runner.
|
||||
if [[ -z "${CHECKOUT_PATH}" ]]; then
|
||||
echo "No checkout path (cancelled before checkout); nothing to clean"
|
||||
exit 0
|
||||
fi
|
||||
echo "Cleaning workspace for ${CHECKOUT_PATH}"
|
||||
rm -rf "${{ github.workspace }}/${CHECKOUT_PATH}"
|
||||
@@ -107,6 +107,9 @@ jobs:
|
||||
compiler: apple-clang
|
||||
compiler_version: ${{ steps.detect-compiler.outputs.compiler_version }}
|
||||
stdlib: libcxx
|
||||
# grpc 1.50.1 trips clang-19+ -Werror=missing-template-arg-list-after-template-kw
|
||||
# on Apple Clang. Drop when grpc is bumped past the fix.
|
||||
conan_deps_cxxflags: '{"grpc/*":["-Wno-missing-template-arg-list-after-template-kw"]}'
|
||||
|
||||
- name: Build
|
||||
uses: ./.github/actions/xahau-ga-build
|
||||
|
||||
@@ -72,15 +72,26 @@ jobs:
|
||||
"job_type": "build"
|
||||
},
|
||||
{
|
||||
"compiler_id": "gcc-13-libstdcxx",
|
||||
"compiler": "gcc",
|
||||
"cc": "gcc-13",
|
||||
"cxx": "g++-13",
|
||||
"gcov": "gcov-13",
|
||||
"compiler_version": 13,
|
||||
"stdlib": "default",
|
||||
# Latest stable Clang for the most accurate source-based
|
||||
# coverage mapping (newer language features, fewer bugs in
|
||||
# llvm-cov region inference). Pulled from apt.llvm.org since
|
||||
# Ubuntu 24.04 default repos cap at clang-18.
|
||||
"compiler_id": "clang-20-libcxx",
|
||||
"compiler": "clang",
|
||||
"cc": "clang-20",
|
||||
"cxx": "clang++-20",
|
||||
"compiler_version": 20,
|
||||
"stdlib": "libcxx",
|
||||
"configuration": "Debug",
|
||||
"job_type": "coverage"
|
||||
"job_type": "coverage",
|
||||
"coverage_tool": "llvm",
|
||||
"coverage_format": "lcov",
|
||||
# grpc 1.50.1 uses `Foo::template Bar(...)` without an
|
||||
# angle-bracket arg list; clang-19+ promoted that to
|
||||
# -Werror. Drop when grpc is bumped past the fix.
|
||||
"conan_deps_cxxflags": {
|
||||
"grpc/*": ["-Wno-missing-template-arg-list-after-template-kw"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"compiler_id": "clang-14-libstdcxx-gcc11",
|
||||
@@ -131,7 +142,7 @@ jobs:
|
||||
# Minimal matrix for PRs and feature branches
|
||||
minimal_matrix = [
|
||||
full_matrix[1], # gcc-13 (middle-ground gcc)
|
||||
full_matrix[2], # gcc-13 coverage
|
||||
full_matrix[2], # clang-20 llvm-cov coverage
|
||||
full_matrix[3] # clang-14 (mature, stable clang)
|
||||
]
|
||||
|
||||
@@ -207,9 +218,9 @@ jobs:
|
||||
# Select the appropriate matrix
|
||||
if use_full:
|
||||
if force_full:
|
||||
print(f"Using FULL matrix (7 configs) - forced by [ci-nix-full-matrix] tag")
|
||||
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - forced by [ci-nix-full-matrix] tag")
|
||||
else:
|
||||
print(f"Using FULL matrix (7 configs) - targeting main branch")
|
||||
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - targeting main branch")
|
||||
matrix = full_matrix
|
||||
else:
|
||||
print(f"Using MINIMAL matrix (3 configs) - feature branch/PR")
|
||||
@@ -257,9 +268,25 @@ jobs:
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
# Bump apt's default 3 retries; papers over short upstream blips
|
||||
# like the recurring ppa.launchpadcontent.net outages.
|
||||
echo 'Acquire::Retries "5";' > /etc/apt/apt.conf.d/80-retries
|
||||
apt-get update
|
||||
apt-get install -y software-properties-common
|
||||
add-apt-repository ppa:ubuntu-toolchain-r/test -y
|
||||
|
||||
# apt.llvm.org for Clang versions newer than what Ubuntu 24.04 ships
|
||||
# (24.04 default repos cap at clang-18). The bootstrap script adds
|
||||
# the LLVM apt source for the requested version and runs apt-get update.
|
||||
if [ "${{ matrix.compiler }}" = "clang" ] && [ "${{ matrix.compiler_version }}" -ge 19 ]; then
|
||||
apt-get install -y wget gnupg lsb-release
|
||||
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
|
||||
chmod +x /tmp/llvm.sh
|
||||
# `all` installs clang + libllvm + lldb + lld + the llvm-N package
|
||||
# (which provides llvm-profdata-N / llvm-cov-N for coverage runs).
|
||||
/tmp/llvm.sh ${{ matrix.compiler_version }} all
|
||||
fi
|
||||
|
||||
apt-get update
|
||||
apt-get install -y git python3 python-is-python3 pipx
|
||||
pipx ensurepath
|
||||
@@ -332,10 +359,16 @@ jobs:
|
||||
pipx install "conan>=2.0,<3"
|
||||
echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
# Install gcovr for coverage jobs
|
||||
# Install coverage tooling
|
||||
if [ "${{ matrix.job_type }}" = "coverage" ]; then
|
||||
pipx install "gcovr>=7,<9"
|
||||
apt-get install -y curl lcov
|
||||
if [ "${{ matrix.coverage_tool }}" = "llvm" ]; then
|
||||
# Native LLVM source-based coverage: llvm-profdata + llvm-cov.
|
||||
# The clang-N package doesn't pull these in; the llvm-N package does.
|
||||
apt-get install -y "llvm-${{ matrix.compiler_version }}"
|
||||
else
|
||||
pipx install "gcovr>=7,<9"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Check environment
|
||||
@@ -348,10 +381,15 @@ jobs:
|
||||
which ${{ matrix.cxx }} && ${{ matrix.cxx }} --version || echo "${{ matrix.cxx }} not found"
|
||||
which ccache && ccache --version || echo "ccache not found"
|
||||
|
||||
# Check gcovr for coverage jobs
|
||||
# Check coverage tooling
|
||||
if [ "${{ matrix.job_type }}" = "coverage" ]; then
|
||||
which gcov && gcov --version || echo "gcov not found"
|
||||
which gcovr && gcovr --version || echo "gcovr not found"
|
||||
if [ "${{ matrix.coverage_tool }}" = "llvm" ]; then
|
||||
which "llvm-profdata-${{ matrix.compiler_version }}" && "llvm-profdata-${{ matrix.compiler_version }}" --version || echo "llvm-profdata not found"
|
||||
which "llvm-cov-${{ matrix.compiler_version }}" && "llvm-cov-${{ matrix.compiler_version }}" --version || echo "llvm-cov not found"
|
||||
else
|
||||
which gcov && gcov --version || echo "gcov not found"
|
||||
which gcovr && gcovr --version || echo "gcovr not found"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "---- Full Environment ----"
|
||||
@@ -378,6 +416,7 @@ jobs:
|
||||
cc: ${{ matrix.cc }}
|
||||
cxx: ${{ matrix.cxx }}
|
||||
stdlib: ${{ matrix.stdlib }}
|
||||
conan_deps_cxxflags: ${{ matrix.conan_deps_cxxflags && toJson(matrix.conan_deps_cxxflags) || '{}' }}
|
||||
gha_cache_enabled: 'false' # Disable caching for self hosted runner
|
||||
|
||||
- name: Build
|
||||
@@ -410,8 +449,9 @@ jobs:
|
||||
cache_version: ${{ env.CACHE_VERSION }}
|
||||
main_branch: ${{ env.MAIN_BRANCH_NAME }}
|
||||
stdlib: ${{ matrix.stdlib }}
|
||||
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness
|
||||
cmake-args: '-Dcoverage=ON -Dcoverage_format=xml -Dcoverage_test_parallelism=$(($(nproc)/2)) -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_CXX_FLAGS="-O0" -DCMAKE_C_FLAGS="-O0"'
|
||||
# Coverage builds are slower due to instrumentation; use fewer parallel jobs to avoid flakiness.
|
||||
# Use *_FLAGS_DEBUG so the build action's stdlib flag (e.g. -stdlib=libc++) in CMAKE_CXX_FLAGS isn't clobbered.
|
||||
cmake-args: '-Dcoverage=ON -Dcoverage_tool=${{ matrix.coverage_tool }} -Dcoverage_format=${{ matrix.coverage_format }} -Dcoverage_test_parallelism=$(($(nproc)/2)) -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_CXX_FLAGS_DEBUG="-g -O0" -DCMAKE_C_FLAGS_DEBUG="-g -O0"'
|
||||
cmake-target: 'coverage'
|
||||
ccache_max_size: '100G'
|
||||
|
||||
@@ -443,22 +483,26 @@ jobs:
|
||||
- name: Move coverage report
|
||||
if: matrix.job_type == 'coverage'
|
||||
shell: bash
|
||||
env:
|
||||
COVERAGE_FILE: ${{ matrix.coverage_tool == 'llvm' && 'coverage.lcov' || 'coverage.xml' }}
|
||||
run: |
|
||||
mv "${{ env.build_dir }}/coverage.xml" ./
|
||||
mv "${{ env.build_dir }}/${COVERAGE_FILE}" ./
|
||||
echo "COVERAGE_FILE=${COVERAGE_FILE}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Archive coverage report
|
||||
if: matrix.job_type == 'coverage'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage.xml
|
||||
path: coverage.xml
|
||||
name: ${{ env.COVERAGE_FILE }}-${{ matrix.compiler_id }}
|
||||
path: ${{ env.COVERAGE_FILE }}
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload coverage report
|
||||
if: matrix.job_type == 'coverage'
|
||||
if: matrix.job_type == 'coverage' && github.repository == 'Xahau/xahaud'
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: coverage.xml
|
||||
files: ${{ env.COVERAGE_FILE }}
|
||||
flags: ${{ matrix.coverage_tool }}
|
||||
fail_ci_if_error: true
|
||||
disable_search: true
|
||||
verbose: true
|
||||
|
||||
@@ -32,6 +32,7 @@ test.basics > xrpld.perflog
|
||||
test.basics > xrpld.rpc
|
||||
test.basics > xrpl.json
|
||||
test.basics > xrpl.protocol
|
||||
test.beast > test.unit_test
|
||||
test.beast > xrpl.basics
|
||||
test.conditions > xrpl.basics
|
||||
test.conditions > xrpld.conditions
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#[===================================================================[
|
||||
Native LLVM source-based code coverage helper.
|
||||
|
||||
Drives the -fprofile-instr-generate / -fcoverage-mapping pipeline:
|
||||
1. Run instrumented binary with LLVM_PROFILE_FILE=...%m-%p.profraw
|
||||
2. llvm-profdata merge -sparse -> coverage.profdata
|
||||
3. llvm-cov export/show/report -> final report
|
||||
|
||||
Output filename per coverage_format:
|
||||
lcov -> coverage.lcov
|
||||
json -> coverage.json
|
||||
txt | text -> coverage.txt
|
||||
html | html-details -> <NAME>/index.html
|
||||
#]===================================================================]
|
||||
|
||||
include(CMakeParseArguments)
|
||||
|
||||
# Locate llvm-profdata / llvm-cov, preferring versioned variants matching the
|
||||
# Clang we're building with so we don't accidentally pair clang-20 with
|
||||
# llvm-cov-14 (profile format mismatch -> hard failure).
|
||||
function(_find_llvm_cov_tools)
|
||||
if(LLVM_PROFDATA_PATH AND LLVM_COV_PATH)
|
||||
return()
|
||||
endif()
|
||||
|
||||
string(REGEX MATCH "^[0-9]+" _major "${CMAKE_CXX_COMPILER_VERSION}")
|
||||
|
||||
set(_pd_names llvm-profdata)
|
||||
set(_cov_names llvm-cov)
|
||||
if(_major)
|
||||
list(PREPEND _pd_names "llvm-profdata-${_major}")
|
||||
list(PREPEND _cov_names "llvm-cov-${_major}")
|
||||
endif()
|
||||
|
||||
# Only delegate to xcrun when the *compiler* is AppleClang. On macOS with
|
||||
# Homebrew/system clang-N, xcrun would resolve to Xcode's llvm tools which
|
||||
# could be a different version - exactly the mismatch we want to avoid.
|
||||
if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
|
||||
execute_process(COMMAND xcrun -f llvm-profdata
|
||||
OUTPUT_VARIABLE _pd_xcrun OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET RESULT_VARIABLE _pd_rc)
|
||||
if(_pd_rc EQUAL 0 AND _pd_xcrun)
|
||||
set(LLVM_PROFDATA_PATH "${_pd_xcrun}" CACHE FILEPATH "llvm-profdata" FORCE)
|
||||
endif()
|
||||
execute_process(COMMAND xcrun -f llvm-cov
|
||||
OUTPUT_VARIABLE _cov_xcrun OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET RESULT_VARIABLE _cov_rc)
|
||||
if(_cov_rc EQUAL 0 AND _cov_xcrun)
|
||||
set(LLVM_COV_PATH "${_cov_xcrun}" CACHE FILEPATH "llvm-cov" FORCE)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(NOT LLVM_PROFDATA_PATH)
|
||||
find_program(LLVM_PROFDATA_PATH NAMES ${_pd_names})
|
||||
endif()
|
||||
if(NOT LLVM_COV_PATH)
|
||||
find_program(LLVM_COV_PATH NAMES ${_cov_names})
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(setup_target_for_coverage_llvm)
|
||||
set(oneValueArgs NAME FORMAT)
|
||||
set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
|
||||
cmake_parse_arguments(Cov "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
|
||||
|
||||
_find_llvm_cov_tools()
|
||||
if(NOT LLVM_PROFDATA_PATH)
|
||||
message(FATAL_ERROR "llvm-profdata not found (needed for coverage_tool=llvm)")
|
||||
endif()
|
||||
if(NOT LLVM_COV_PATH)
|
||||
message(FATAL_ERROR "llvm-cov not found (needed for coverage_tool=llvm)")
|
||||
endif()
|
||||
|
||||
if(NOT Cov_FORMAT)
|
||||
set(Cov_FORMAT lcov)
|
||||
endif()
|
||||
|
||||
set(_profraw_dir "${PROJECT_BINARY_DIR}/${Cov_NAME}-profraw")
|
||||
set(_profdata "${PROJECT_BINARY_DIR}/${Cov_NAME}.profdata")
|
||||
|
||||
# Resolve binary path: accept either an absolute path or a bare target name
|
||||
# (resolved against PROJECT_BINARY_DIR). Splice the resolved path back into
|
||||
# Cov_EXECUTABLE so the run command invokes it via absolute path - bare
|
||||
# names aren't on PATH and the build dir isn't `.` either.
|
||||
list(GET Cov_EXECUTABLE 0 _exec_name)
|
||||
if(IS_ABSOLUTE "${_exec_name}")
|
||||
set(_binary "${_exec_name}")
|
||||
else()
|
||||
set(_binary "${PROJECT_BINARY_DIR}/${_exec_name}")
|
||||
list(REMOVE_AT Cov_EXECUTABLE 0)
|
||||
list(PREPEND Cov_EXECUTABLE "${_binary}")
|
||||
endif()
|
||||
|
||||
# llvm-cov takes a single -ignore-filename-regex; OR our excludes together.
|
||||
set(_ignore_regex "")
|
||||
foreach(EXC IN LISTS Cov_EXCLUDE)
|
||||
if(_ignore_regex)
|
||||
string(APPEND _ignore_regex "|")
|
||||
endif()
|
||||
string(APPEND _ignore_regex "${EXC}")
|
||||
endforeach()
|
||||
set(_filter "")
|
||||
if(_ignore_regex)
|
||||
set(_filter "-ignore-filename-regex='${_ignore_regex}'")
|
||||
endif()
|
||||
|
||||
# Pick llvm-cov subcommand + output file for the requested format. Each
|
||||
# branch builds a single shell command string that we'll hand to bash -c.
|
||||
if(Cov_FORMAT STREQUAL "lcov")
|
||||
set(_output "${PROJECT_BINARY_DIR}/coverage.lcov")
|
||||
set(_report_sh "${LLVM_COV_PATH} export -instr-profile='${_profdata}' -format=lcov ${_filter} '${_binary}' > '${_output}'")
|
||||
elseif(Cov_FORMAT STREQUAL "json")
|
||||
set(_output "${PROJECT_BINARY_DIR}/coverage.json")
|
||||
set(_report_sh "${LLVM_COV_PATH} export -instr-profile='${_profdata}' -format=text ${_filter} '${_binary}' > '${_output}'")
|
||||
elseif(Cov_FORMAT STREQUAL "txt" OR Cov_FORMAT STREQUAL "text")
|
||||
set(_output "${PROJECT_BINARY_DIR}/coverage.txt")
|
||||
set(_report_sh "${LLVM_COV_PATH} report -instr-profile='${_profdata}' ${_filter} '${_binary}' > '${_output}'")
|
||||
elseif(Cov_FORMAT STREQUAL "html" OR Cov_FORMAT STREQUAL "html-details")
|
||||
set(_output "${PROJECT_BINARY_DIR}/${Cov_NAME}/index.html")
|
||||
set(_report_sh "${LLVM_COV_PATH} show -instr-profile='${_profdata}' -format=html -output-dir='${PROJECT_BINARY_DIR}/${Cov_NAME}' ${_filter} '${_binary}'")
|
||||
else()
|
||||
message(FATAL_ERROR "coverage_tool=llvm: unsupported coverage_format '${Cov_FORMAT}' (use lcov|json|txt|html)")
|
||||
endif()
|
||||
|
||||
set(_merge_sh "${LLVM_PROFDATA_PATH} merge -sparse -o '${_profdata}' '${_profraw_dir}'/*.profraw")
|
||||
|
||||
if(CODE_COVERAGE_VERBOSE)
|
||||
message(STATUS "[coverage:llvm] binary: ${_binary}")
|
||||
message(STATUS "[coverage:llvm] profraw: ${_profraw_dir}")
|
||||
message(STATUS "[coverage:llvm] profdata: ${_profdata}")
|
||||
message(STATUS "[coverage:llvm] format: ${Cov_FORMAT}")
|
||||
message(STATUS "[coverage:llvm] output: ${_output}")
|
||||
if(_ignore_regex)
|
||||
message(STATUS "[coverage:llvm] ignore: ${_ignore_regex}")
|
||||
endif()
|
||||
message(STATUS "[coverage:llvm] merge: ${_merge_sh}")
|
||||
message(STATUS "[coverage:llvm] report: ${_report_sh}")
|
||||
endif()
|
||||
|
||||
# %m: hash of the binary, %p: pid. Wipe the dir up front so stale profraw
|
||||
# files can't leak into a fresh merge.
|
||||
add_custom_target(${Cov_NAME}
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${_profraw_dir}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${_profraw_dir}"
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
"LLVM_PROFILE_FILE=${_profraw_dir}/rippled-%m-%p.profraw"
|
||||
${Cov_EXECUTABLE} ${Cov_EXECUTABLE_ARGS}
|
||||
COMMAND bash -c "${_merge_sh}"
|
||||
COMMAND bash -c "${_report_sh}"
|
||||
BYPRODUCTS ${_output}
|
||||
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
|
||||
DEPENDS ${Cov_DEPENDENCIES}
|
||||
VERBATIM
|
||||
COMMENT "Running llvm-cov (${Cov_FORMAT}) -> ${_output}"
|
||||
)
|
||||
endfunction()
|
||||
@@ -11,6 +11,21 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(coverage_tool STREQUAL "llvm")
|
||||
include(CodeCoverageLLVM)
|
||||
|
||||
setup_target_for_coverage_llvm(
|
||||
NAME coverage
|
||||
FORMAT ${coverage_format}
|
||||
EXECUTABLE rippled
|
||||
EXECUTABLE_ARGS --unittest$<$<BOOL:${coverage_test}>:=${coverage_test}> --unittest-jobs ${coverage_test_parallelism} --quiet --unittest-log
|
||||
EXCLUDE "src/test" "include/xrpl/beast/test" "include/xrpl/beast/unit_test" "${CMAKE_BINARY_DIR}/pb-xrpl.libpb"
|
||||
DEPENDENCIES rippled
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
# coverage_tool == "gcov" (default): existing gcovr-driven pipeline.
|
||||
include(CodeCoverage)
|
||||
|
||||
# The instructions for these commands come from the `CodeCoverage` module,
|
||||
|
||||
@@ -28,15 +28,17 @@ target_compile_options (opts
|
||||
$<$<AND:$<BOOL:${is_gcc}>,$<COMPILE_LANGUAGE:CXX>>:-Wsuggest-override>
|
||||
$<$<BOOL:${is_gcc}>:-Wno-maybe-uninitialized>
|
||||
$<$<BOOL:${perf}>:-fno-omit-frame-pointer>
|
||||
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-g --coverage -fprofile-abs-path>
|
||||
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-g --coverage>
|
||||
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage -fprofile-abs-path>
|
||||
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage>
|
||||
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},llvm>>:-g -fprofile-instr-generate -fcoverage-mapping>
|
||||
$<$<BOOL:${profile}>:-pg>
|
||||
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>)
|
||||
|
||||
target_link_libraries (opts
|
||||
INTERFACE
|
||||
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>>:-g --coverage -fprofile-abs-path>
|
||||
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>>:-g --coverage>
|
||||
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage -fprofile-abs-path>
|
||||
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},gcov>>:-g --coverage>
|
||||
$<$<AND:$<BOOL:${is_clang}>,$<BOOL:${coverage}>,$<STREQUAL:${coverage_tool},llvm>>:-g -fprofile-instr-generate -fcoverage-mapping>
|
||||
$<$<BOOL:${profile}>:-pg>
|
||||
$<$<AND:$<BOOL:${is_gcc}>,$<BOOL:${profile}>>:-p>)
|
||||
|
||||
|
||||
@@ -29,13 +29,25 @@ if(is_gcc OR is_clang)
|
||||
"Unit tests parallelism for the purpose of coverage report.")
|
||||
set(coverage_format "html-details" CACHE STRING
|
||||
"Output format of the coverage report.")
|
||||
set(coverage_tool "gcov" CACHE STRING
|
||||
"Coverage instrumentation tool: 'gcov' (default, gcc/clang via --coverage + gcovr) or 'llvm' (clang only, native source-based coverage via -fprofile-instr-generate).")
|
||||
set_property(CACHE coverage_tool PROPERTY STRINGS "gcov" "llvm")
|
||||
if(NOT coverage_tool MATCHES "^(gcov|llvm)$")
|
||||
message(FATAL_ERROR "coverage_tool must be 'gcov' or 'llvm', got '${coverage_tool}'")
|
||||
endif()
|
||||
set(coverage_extra_args "" CACHE STRING
|
||||
"Additional arguments to pass to gcovr.")
|
||||
"Additional arguments to pass to gcovr (gcov tool only).")
|
||||
set(coverage_test "" CACHE STRING
|
||||
"On gcc & clang, the specific unit test(s) to run for coverage. Default is all tests.")
|
||||
if(coverage_test AND NOT coverage)
|
||||
set(coverage ON CACHE BOOL "gcc/clang only" FORCE)
|
||||
endif()
|
||||
# Validate after coverage_test may have flipped coverage on, otherwise
|
||||
# `-Dcoverage_tool=llvm -Dcoverage_test=Foo` on gcc would silently slip
|
||||
# past the Clang guard and produce a broken instrumentation combo.
|
||||
if(coverage AND coverage_tool STREQUAL "llvm" AND NOT is_clang)
|
||||
message(FATAL_ERROR "coverage_tool=llvm requires Clang (got ${CMAKE_CXX_COMPILER_ID})")
|
||||
endif()
|
||||
option(wextra "compile with extra gcc/clang warnings enabled" ON)
|
||||
else()
|
||||
set(profile OFF CACHE BOOL "gcc/clang only" FORCE)
|
||||
@@ -112,7 +124,7 @@ option(beast_no_unit_test_inline
|
||||
"Prevents unit test definitions from being inserted into global table"
|
||||
OFF)
|
||||
option(single_io_service_thread
|
||||
"Restricts the number of threads calling io_service::run to one. \
|
||||
"Restricts the number of threads calling io_context::run to one. \
|
||||
This can be useful when debugging."
|
||||
OFF)
|
||||
option(boost_show_deprecated
|
||||
|
||||
@@ -32,9 +32,9 @@ target_link_libraries(ripple_boost
|
||||
Boost::date_time
|
||||
Boost::filesystem
|
||||
Boost::json
|
||||
Boost::process
|
||||
Boost::program_options
|
||||
Boost::regex
|
||||
Boost::system
|
||||
Boost::iostreams
|
||||
Boost::thread)
|
||||
if(Boost_COMPILER)
|
||||
|
||||
+129
-127
@@ -2,158 +2,160 @@ from conan import ConanFile
|
||||
from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout
|
||||
import re
|
||||
|
||||
class Xrpl(ConanFile):
|
||||
name = 'xrpl'
|
||||
|
||||
license = 'ISC'
|
||||
author = 'John Freeman <[email protected]>'
|
||||
url = 'https://github.com/xrplf/rippled'
|
||||
description = 'The XRP Ledger'
|
||||
settings = 'os', 'compiler', 'build_type', 'arch'
|
||||
class Xrpl(ConanFile):
|
||||
name = "xrpl"
|
||||
|
||||
license = "ISC"
|
||||
author = "John Freeman <[email protected]>"
|
||||
url = "https://github.com/xrplf/rippled"
|
||||
description = "The XRP Ledger"
|
||||
settings = "os", "compiler", "build_type", "arch"
|
||||
options = {
|
||||
'assertions': [True, False],
|
||||
'coverage': [True, False],
|
||||
'fPIC': [True, False],
|
||||
'jemalloc': [True, False],
|
||||
'rocksdb': [True, False],
|
||||
'shared': [True, False],
|
||||
'static': [True, False],
|
||||
'tests': [True, False],
|
||||
'unity': [True, False],
|
||||
'xrpld': [True, False],
|
||||
'with_wasmedge': [True, False],
|
||||
'tool_requires_b2': [True, False],
|
||||
"assertions": [True, False],
|
||||
"coverage": [True, False],
|
||||
"fPIC": [True, False],
|
||||
"jemalloc": [True, False],
|
||||
"rocksdb": [True, False],
|
||||
"shared": [True, False],
|
||||
"static": [True, False],
|
||||
"tests": [True, False],
|
||||
"unity": [True, False],
|
||||
"xrpld": [True, False],
|
||||
"with_wasmedge": [True, False],
|
||||
"tool_requires_b2": [True, False],
|
||||
}
|
||||
|
||||
requires = [
|
||||
'date/3.0.3',
|
||||
'grpc/1.50.1',
|
||||
'libarchive/3.7.6',
|
||||
'magic_enum/0.9.5',
|
||||
'nudb/2.0.8',
|
||||
'openssl/3.6.0',
|
||||
'soci/4.0.3@xahaud/stable',
|
||||
'xxhash/0.8.2',
|
||||
'zlib/1.3.1',
|
||||
"date/3.0.3",
|
||||
"grpc/1.50.1",
|
||||
"libarchive/3.7.6",
|
||||
"magic_enum/0.9.5",
|
||||
"nudb/2.0.9",
|
||||
"openssl/3.6.0",
|
||||
"soci/4.0.3@xahaud/stable",
|
||||
"xxhash/0.8.2",
|
||||
"zlib/1.3.1",
|
||||
]
|
||||
|
||||
tool_requires = [
|
||||
'protobuf/3.21.12',
|
||||
"protobuf/3.21.12",
|
||||
]
|
||||
|
||||
default_options = {
|
||||
'assertions': False,
|
||||
'coverage': False,
|
||||
'fPIC': True,
|
||||
'jemalloc': False,
|
||||
'rocksdb': True,
|
||||
'shared': False,
|
||||
'static': True,
|
||||
'tests': False,
|
||||
'unity': False,
|
||||
'xrpld': False,
|
||||
'with_wasmedge': True,
|
||||
'tool_requires_b2': False,
|
||||
|
||||
'date/*:header_only': False,
|
||||
'grpc/*:shared': False,
|
||||
'grpc/*:secure': True,
|
||||
'libarchive/*:shared': False,
|
||||
'libarchive/*:with_acl': False,
|
||||
'libarchive/*:with_bzip2': False,
|
||||
'libarchive/*:with_cng': False,
|
||||
'libarchive/*:with_expat': False,
|
||||
'libarchive/*:with_iconv': False,
|
||||
'libarchive/*:with_libxml2': False,
|
||||
'libarchive/*:with_lz4': True,
|
||||
'libarchive/*:with_lzma': False,
|
||||
'libarchive/*:with_lzo': False,
|
||||
'libarchive/*:with_nettle': False,
|
||||
'libarchive/*:with_openssl': False,
|
||||
'libarchive/*:with_pcreposix': False,
|
||||
'libarchive/*:with_xattr': False,
|
||||
'libarchive/*:with_zlib': False,
|
||||
'lz4/*:shared': False,
|
||||
'openssl/*:shared': False,
|
||||
'protobuf/*:shared': False,
|
||||
'protobuf/*:with_zlib': True,
|
||||
'rocksdb/*:enable_sse': False,
|
||||
'rocksdb/*:lite': False,
|
||||
'rocksdb/*:shared': False,
|
||||
'rocksdb/*:use_rtti': True,
|
||||
'rocksdb/*:with_jemalloc': False,
|
||||
'rocksdb/*:with_lz4': True,
|
||||
'rocksdb/*:with_snappy': True,
|
||||
'snappy/*:shared': False,
|
||||
'soci/*:shared': False,
|
||||
'soci/*:with_sqlite3': True,
|
||||
'soci/*:with_boost': True,
|
||||
'xxhash/*:shared': False,
|
||||
"assertions": False,
|
||||
"coverage": False,
|
||||
"fPIC": True,
|
||||
"jemalloc": False,
|
||||
"rocksdb": True,
|
||||
"shared": False,
|
||||
"static": True,
|
||||
"tests": False,
|
||||
"unity": False,
|
||||
"xrpld": False,
|
||||
"with_wasmedge": True,
|
||||
"tool_requires_b2": False,
|
||||
"boost/*:without_cobalt": True,
|
||||
"date/*:header_only": False,
|
||||
"grpc/*:shared": False,
|
||||
"grpc/*:secure": True,
|
||||
"libarchive/*:shared": False,
|
||||
"libarchive/*:with_acl": False,
|
||||
"libarchive/*:with_bzip2": False,
|
||||
"libarchive/*:with_cng": False,
|
||||
"libarchive/*:with_expat": False,
|
||||
"libarchive/*:with_iconv": False,
|
||||
"libarchive/*:with_libxml2": False,
|
||||
"libarchive/*:with_lz4": True,
|
||||
"libarchive/*:with_lzma": False,
|
||||
"libarchive/*:with_lzo": False,
|
||||
"libarchive/*:with_nettle": False,
|
||||
"libarchive/*:with_openssl": False,
|
||||
"libarchive/*:with_pcreposix": False,
|
||||
"libarchive/*:with_xattr": False,
|
||||
"libarchive/*:with_zlib": False,
|
||||
"lz4/*:shared": False,
|
||||
"openssl/*:shared": False,
|
||||
"protobuf/*:shared": False,
|
||||
"protobuf/*:with_zlib": True,
|
||||
"rocksdb/*:enable_sse": False,
|
||||
"rocksdb/*:lite": False,
|
||||
"rocksdb/*:shared": False,
|
||||
"rocksdb/*:use_rtti": True,
|
||||
"rocksdb/*:with_jemalloc": False,
|
||||
"rocksdb/*:with_lz4": True,
|
||||
"rocksdb/*:with_snappy": True,
|
||||
"snappy/*:shared": False,
|
||||
"soci/*:shared": False,
|
||||
"soci/*:with_sqlite3": True,
|
||||
"soci/*:with_boost": True,
|
||||
"xxhash/*:shared": False,
|
||||
}
|
||||
|
||||
def set_version(self):
|
||||
path = f'{self.recipe_folder}/src/libxrpl/protocol/BuildInfo.cpp'
|
||||
regex = r'versionString\s?=\s?\"(.*)\"'
|
||||
with open(path, 'r') as file:
|
||||
path = f"{self.recipe_folder}/src/libxrpl/protocol/BuildInfo.cpp"
|
||||
regex = r"versionString\s?=\s?\"(.*)\""
|
||||
with open(path, "r") as file:
|
||||
matches = (re.search(regex, line) for line in file)
|
||||
match = next(m for m in matches if m)
|
||||
self.version = match.group(1)
|
||||
|
||||
def build_requirements(self):
|
||||
# These provide build tools (protoc, grpc plugins) that run during build
|
||||
self.tool_requires('grpc/1.50.1')
|
||||
self.tool_requires("grpc/1.50.1")
|
||||
# Explicitly require b2 (e.g. for building from source for glibc compatibility)
|
||||
if self.options.tool_requires_b2:
|
||||
self.tool_requires('b2/5.3.2')
|
||||
self.tool_requires("b2/5.3.2")
|
||||
|
||||
def configure(self):
|
||||
if self.settings.compiler == 'apple-clang':
|
||||
self.options['boost/*'].visibility = 'global'
|
||||
if self.settings.compiler == "apple-clang":
|
||||
self.options["boost/*"].visibility = "global"
|
||||
|
||||
def requirements(self):
|
||||
# Force sqlite3 version to avoid conflicts with soci
|
||||
self.requires('sqlite3/3.47.0', override=True)
|
||||
self.requires("sqlite3/3.47.0", override=True)
|
||||
# Force our custom snappy build for all dependencies
|
||||
self.requires('snappy/1.1.10@xahaud/stable', override=True)
|
||||
self.requires("snappy/1.1.10@xahaud/stable", override=True)
|
||||
# Force boost version for all dependencies to avoid conflicts
|
||||
self.requires('boost/1.86.0', override=True)
|
||||
self.requires('lz4/1.10.0', force=True)
|
||||
self.requires("boost/1.91.0", override=True)
|
||||
self.requires("lz4/1.10.0", force=True)
|
||||
|
||||
if self.options.with_wasmedge:
|
||||
self.requires('wasmedge/0.11.2@xahaud/stable')
|
||||
self.requires("wasmedge/0.11.2@xahaud/stable")
|
||||
if self.options.jemalloc:
|
||||
self.requires('jemalloc/5.3.0')
|
||||
self.requires("jemalloc/5.3.0")
|
||||
if self.options.rocksdb:
|
||||
self.requires('rocksdb/6.29.5')
|
||||
self.requires("rocksdb/6.29.5")
|
||||
|
||||
exports_sources = (
|
||||
'CMakeLists.txt',
|
||||
'bin/getRippledInfo',
|
||||
'cfg/*',
|
||||
'cmake/*',
|
||||
'external/*',
|
||||
'include/*',
|
||||
'src/*',
|
||||
"CMakeLists.txt",
|
||||
"bin/getRippledInfo",
|
||||
"cfg/*",
|
||||
"cmake/*",
|
||||
"external/*",
|
||||
"include/*",
|
||||
"src/*",
|
||||
)
|
||||
|
||||
def layout(self):
|
||||
cmake_layout(self)
|
||||
# Fix this setting to follow the default introduced in Conan 1.48
|
||||
# to align with our build instructions.
|
||||
self.folders.generators = 'build/generators'
|
||||
self.folders.generators = "build/generators"
|
||||
|
||||
generators = "CMakeDeps"
|
||||
|
||||
generators = 'CMakeDeps'
|
||||
def generate(self):
|
||||
tc = CMakeToolchain(self)
|
||||
tc.variables['tests'] = self.options.tests
|
||||
tc.variables['assert'] = self.options.assertions
|
||||
tc.variables['coverage'] = self.options.coverage
|
||||
tc.variables['jemalloc'] = self.options.jemalloc
|
||||
tc.variables['rocksdb'] = self.options.rocksdb
|
||||
tc.variables['BUILD_SHARED_LIBS'] = self.options.shared
|
||||
tc.variables['static'] = self.options.static
|
||||
tc.variables['unity'] = self.options.unity
|
||||
tc.variables['xrpld'] = self.options.xrpld
|
||||
tc.variables["tests"] = self.options.tests
|
||||
tc.variables["assert"] = self.options.assertions
|
||||
tc.variables["coverage"] = self.options.coverage
|
||||
tc.variables["jemalloc"] = self.options.jemalloc
|
||||
tc.variables["rocksdb"] = self.options.rocksdb
|
||||
tc.variables["BUILD_SHARED_LIBS"] = self.options.shared
|
||||
tc.variables["static"] = self.options.static
|
||||
tc.variables["unity"] = self.options.unity
|
||||
tc.variables["xrpld"] = self.options.xrpld
|
||||
tc.generate()
|
||||
|
||||
def build(self):
|
||||
@@ -168,29 +170,29 @@ class Xrpl(ConanFile):
|
||||
cmake.install()
|
||||
|
||||
def package_info(self):
|
||||
libxrpl = self.cpp_info.components['libxrpl']
|
||||
libxrpl = self.cpp_info.components["libxrpl"]
|
||||
libxrpl.libs = [
|
||||
'xrpl',
|
||||
'xrpl.libpb',
|
||||
'ed25519',
|
||||
'secp256k1',
|
||||
"xrpl",
|
||||
"xrpl.libpb",
|
||||
"ed25519",
|
||||
"secp256k1",
|
||||
]
|
||||
# TODO: Fix the protobufs to include each other relative to
|
||||
# `include/`, not `include/ripple/proto/`.
|
||||
libxrpl.includedirs = ['include', 'include/ripple/proto']
|
||||
libxrpl.includedirs = ["include", "include/ripple/proto"]
|
||||
libxrpl.requires = [
|
||||
'boost::boost',
|
||||
'date::date',
|
||||
'grpc::grpc++',
|
||||
'libarchive::libarchive',
|
||||
'lz4::lz4',
|
||||
'nudb::nudb',
|
||||
'openssl::crypto',
|
||||
'protobuf::libprotobuf',
|
||||
'soci::soci',
|
||||
'sqlite3::sqlite',
|
||||
'xxhash::xxhash',
|
||||
'zlib::zlib',
|
||||
"boost::boost",
|
||||
"date::date",
|
||||
"grpc::grpc++",
|
||||
"libarchive::libarchive",
|
||||
"lz4::lz4",
|
||||
"nudb::nudb",
|
||||
"openssl::crypto",
|
||||
"protobuf::libprotobuf",
|
||||
"soci::soci",
|
||||
"sqlite3::sqlite",
|
||||
"xxhash::xxhash",
|
||||
"zlib::zlib",
|
||||
]
|
||||
if self.options.rocksdb:
|
||||
libxrpl.requires.append('rocksdb::librocksdb')
|
||||
libxrpl.requires.append("rocksdb::librocksdb")
|
||||
@@ -38,6 +38,7 @@
|
||||
#define KEYLET_HOOK_DEFINITION 24
|
||||
#define KEYLET_HOOK_STATE_DIR 25
|
||||
#define KEYLET_CRON 26
|
||||
#define KEYLET_MANIFEST 37
|
||||
|
||||
#define COMPARE_EQUAL 1U
|
||||
#define COMPARE_LESS 2U
|
||||
|
||||
@@ -23,9 +23,66 @@
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Add two signed 64-bit integers, returning std::nullopt when the exact
|
||||
* mathematical sum is not representable in std::int64_t.
|
||||
*/
|
||||
[[nodiscard]] constexpr std::optional<std::int64_t>
|
||||
checkedAdd(std::int64_t a, std::int64_t b) noexcept
|
||||
{
|
||||
using L = std::numeric_limits<std::int64_t>;
|
||||
|
||||
if ((b > 0 && a > L::max() - b) || (b < 0 && a < L::min() - b))
|
||||
return std::nullopt;
|
||||
|
||||
return a + b;
|
||||
}
|
||||
|
||||
/** Subtract two signed 64-bit integers, returning std::nullopt when the exact
|
||||
* mathematical difference is not representable in std::int64_t.
|
||||
*/
|
||||
[[nodiscard]] constexpr std::optional<std::int64_t>
|
||||
checkedSub(std::int64_t a, std::int64_t b) noexcept
|
||||
{
|
||||
using L = std::numeric_limits<std::int64_t>;
|
||||
|
||||
if ((b > 0 && a < L::min() + b) || (b < 0 && a > L::max() + b))
|
||||
return std::nullopt;
|
||||
|
||||
return a - b;
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
using i64_limits = std::numeric_limits<std::int64_t>;
|
||||
|
||||
static_assert(checkedAdd(0, 0) == 0);
|
||||
static_assert(checkedAdd(1, -1) == 0);
|
||||
static_assert(checkedAdd(-5, 2) == -3);
|
||||
static_assert(!checkedAdd(i64_limits::max(), 1).has_value());
|
||||
static_assert(!checkedAdd(i64_limits::min(), -1).has_value());
|
||||
static_assert(checkedAdd(i64_limits::max() - 1, 1) == i64_limits::max());
|
||||
static_assert(checkedAdd(i64_limits::min(), i64_limits::max()) == -1);
|
||||
static_assert(checkedAdd(i64_limits::max(), i64_limits::min()) == -1);
|
||||
static_assert(!checkedAdd(i64_limits::max(), i64_limits::max()).has_value());
|
||||
static_assert(!checkedAdd(i64_limits::min(), i64_limits::min()).has_value());
|
||||
|
||||
static_assert(checkedSub(0, 0) == 0);
|
||||
static_assert(checkedSub(1, 1) == 0);
|
||||
static_assert(checkedSub(-5, 2) == -7);
|
||||
static_assert(checkedSub(-5, -2) == -3);
|
||||
static_assert(!checkedSub(i64_limits::min(), 1).has_value());
|
||||
static_assert(!checkedSub(i64_limits::max(), -1).has_value());
|
||||
static_assert(checkedSub(i64_limits::min() + 1, 1) == i64_limits::min());
|
||||
static_assert(checkedSub(-1, i64_limits::max()) == i64_limits::min());
|
||||
static_assert(!checkedSub(i64_limits::max(), i64_limits::min()).has_value());
|
||||
static_assert(!checkedSub(i64_limits::min(), i64_limits::max()).has_value());
|
||||
} // namespace detail
|
||||
|
||||
/** Calculate one number divided by another number in percentage.
|
||||
* The result is rounded up to the next integer, and capped in the range [0,100]
|
||||
* E.g. calculatePercent(1, 100) = 1 because 1/100 = 0.010000
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
|
||||
#include <xrpl/basics/Resolver.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <boost/asio/io_service.hpp>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -32,7 +33,7 @@ public:
|
||||
explicit ResolverAsio() = default;
|
||||
|
||||
static std::unique_ptr<ResolverAsio>
|
||||
New(boost::asio::io_service&, beast::Journal);
|
||||
New(boost::asio::io_context&, beast::Journal);
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_BASICS_UTF8_H_INCLUDED
|
||||
#define RIPPLE_BASICS_UTF8_H_INCLUDED
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Check that a byte sequence is well-formed UTF-8, excluding the two
|
||||
noncharacters U+FFFE and U+FFFF.
|
||||
|
||||
Rejects:
|
||||
- overlong encodings;
|
||||
- UTF-16 surrogate halves (U+D800..U+DFFF);
|
||||
- scalar values above U+10FFFF, and 0xF5..0xFF lead bytes;
|
||||
- stray continuation bytes;
|
||||
- multi-byte sequences truncated by the end of the buffer;
|
||||
- U+FFFE and U+FFFF (EF BF BE, EF BF BF).
|
||||
|
||||
U+FFFE and U+FFFF are well-formed, but have no use in a URI or a name
|
||||
and exist to be misread: U+FFFE is a byte-swapped BOM, so a client that
|
||||
transcodes to UTF-16/32 and sniffs the byte order can be pushed into
|
||||
decoding the rest of the string with the wrong endianness, and both are
|
||||
commonly used as in-band sentinels. They have always been rejected by
|
||||
URIToken and HookName validation, and this function preserves that.
|
||||
|
||||
This is consensus-critical. For every input the previous decoder could
|
||||
evaluate without reading past the end of the buffer, the result is
|
||||
identical to it. The only difference is that truncated trailing
|
||||
sequences, where the previous decoder read out of bounds, are now always
|
||||
rejected. Any further change to what this accepts needs an amendment.
|
||||
|
||||
@param s The bytes to check.
|
||||
|
||||
@note Total: every byte read is within s.
|
||||
*/
|
||||
bool
|
||||
isValidUTF8(Slice const& s) noexcept;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -22,7 +22,8 @@
|
||||
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <boost/asio/basic_waitable_timer.hpp>
|
||||
#include <boost/asio/io_service.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
@@ -31,7 +32,7 @@
|
||||
|
||||
namespace beast {
|
||||
|
||||
/** Measures handler latency on an io_service queue. */
|
||||
/** Measures handler latency on an io_context queue. */
|
||||
template <class Clock>
|
||||
class io_latency_probe
|
||||
{
|
||||
@@ -43,12 +44,12 @@ private:
|
||||
std::condition_variable_any m_cond;
|
||||
std::size_t m_count;
|
||||
duration const m_period;
|
||||
boost::asio::io_service& m_ios;
|
||||
boost::asio::io_context& m_ios;
|
||||
boost::asio::basic_waitable_timer<std::chrono::steady_clock> m_timer;
|
||||
bool m_cancel;
|
||||
|
||||
public:
|
||||
io_latency_probe(duration const& period, boost::asio::io_service& ios)
|
||||
io_latency_probe(duration const& period, boost::asio::io_context& ios)
|
||||
: m_count(1)
|
||||
, m_period(period)
|
||||
, m_ios(ios)
|
||||
@@ -63,16 +64,16 @@ public:
|
||||
cancel(lock, true);
|
||||
}
|
||||
|
||||
/** Return the io_service associated with the latency probe. */
|
||||
/** Return the io_context associated with the latency probe. */
|
||||
/** @{ */
|
||||
boost::asio::io_service&
|
||||
get_io_service()
|
||||
boost::asio::io_context&
|
||||
get_io_context()
|
||||
{
|
||||
return m_ios;
|
||||
}
|
||||
|
||||
boost::asio::io_service const&
|
||||
get_io_service() const
|
||||
boost::asio::io_context const&
|
||||
get_io_context() const
|
||||
{
|
||||
return m_ios;
|
||||
}
|
||||
@@ -108,8 +109,10 @@ public:
|
||||
std::lock_guard lock(m_mutex);
|
||||
if (m_cancel)
|
||||
throw std::logic_error("io_latency_probe is canceled");
|
||||
m_ios.post(sample_op<Handler>(
|
||||
std::forward<Handler>(handler), Clock::now(), false, this));
|
||||
boost::asio::post(
|
||||
m_ios,
|
||||
sample_op<Handler>(
|
||||
std::forward<Handler>(handler), Clock::now(), false, this));
|
||||
}
|
||||
|
||||
/** Initiate continuous i/o latency sampling.
|
||||
@@ -123,8 +126,10 @@ public:
|
||||
std::lock_guard lock(m_mutex);
|
||||
if (m_cancel)
|
||||
throw std::logic_error("io_latency_probe is canceled");
|
||||
m_ios.post(sample_op<Handler>(
|
||||
std::forward<Handler>(handler), Clock::now(), true, this));
|
||||
boost::asio::post(
|
||||
m_ios,
|
||||
sample_op<Handler>(
|
||||
std::forward<Handler>(handler), Clock::now(), true, this));
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -235,12 +240,13 @@ private:
|
||||
// The latency is too high to maintain the desired
|
||||
// period so don't bother with a timer.
|
||||
//
|
||||
m_probe->m_ios.post(
|
||||
boost::asio::post(
|
||||
m_probe->m_ios,
|
||||
sample_op<Handler>(m_handler, now, m_repeat, m_probe));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_probe->m_timer.expires_from_now(when - now);
|
||||
m_probe->m_timer.expires_after(when - now);
|
||||
m_probe->m_timer.async_wait(
|
||||
sample_op<Handler>(m_handler, now, m_repeat, m_probe));
|
||||
}
|
||||
@@ -253,7 +259,8 @@ private:
|
||||
if (!m_probe)
|
||||
return;
|
||||
typename Clock::time_point const now(Clock::now());
|
||||
m_probe->m_ios.post(
|
||||
boost::asio::post(
|
||||
m_probe->m_ios,
|
||||
sample_op<Handler>(m_handler, now, m_repeat, m_probe));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
#ifndef BEAST_TEST_YIELD_TO_HPP
|
||||
#define BEAST_TEST_YIELD_TO_HPP
|
||||
|
||||
#include <boost/asio/io_service.hpp>
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/optional.hpp>
|
||||
#include <boost/thread/csbl/memory/allocator_arg.hpp>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
@@ -29,10 +32,12 @@ namespace test {
|
||||
class enable_yield_to
|
||||
{
|
||||
protected:
|
||||
boost::asio::io_service ios_;
|
||||
boost::asio::io_context ios_;
|
||||
|
||||
private:
|
||||
boost::optional<boost::asio::io_service::work> work_;
|
||||
boost::optional<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>
|
||||
work_;
|
||||
std::vector<std::thread> threads_;
|
||||
std::mutex m_;
|
||||
std::condition_variable cv_;
|
||||
@@ -42,7 +47,8 @@ public:
|
||||
/// The type of yield context passed to functions.
|
||||
using yield_context = boost::asio::yield_context;
|
||||
|
||||
explicit enable_yield_to(std::size_t concurrency = 1) : work_(ios_)
|
||||
explicit enable_yield_to(std::size_t concurrency = 1)
|
||||
: work_(boost::asio::make_work_guard(ios_))
|
||||
{
|
||||
threads_.reserve(concurrency);
|
||||
while (concurrency--)
|
||||
@@ -56,9 +62,9 @@ public:
|
||||
t.join();
|
||||
}
|
||||
|
||||
/// Return the `io_service` associated with the object
|
||||
boost::asio::io_service&
|
||||
get_io_service()
|
||||
/// Return the `io_context` associated with the object
|
||||
boost::asio::io_context&
|
||||
get_io_context()
|
||||
{
|
||||
return ios_;
|
||||
}
|
||||
@@ -111,13 +117,18 @@ enable_yield_to::spawn(F0&& f, FN&&... fn)
|
||||
{
|
||||
boost::asio::spawn(
|
||||
ios_,
|
||||
boost::allocator_arg,
|
||||
boost::context::fixedsize_stack(2 * 1024 * 1024),
|
||||
[&](yield_context yield) {
|
||||
f(yield);
|
||||
std::lock_guard lock{m_};
|
||||
if (--running_ == 0)
|
||||
cv_.notify_all();
|
||||
},
|
||||
boost::coroutines::attributes(2 * 1024 * 1024));
|
||||
[](std::exception_ptr e) {
|
||||
if (e)
|
||||
std::rethrow_exception(e);
|
||||
});
|
||||
spawn(fn...);
|
||||
}
|
||||
|
||||
|
||||
@@ -313,6 +313,7 @@ enum keylet_code : uint32_t {
|
||||
MPTOKEN = 34,
|
||||
CREDENTIAL = 35,
|
||||
PERMISSIONED_DOMAIN = 36,
|
||||
MANIFEST = 37,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ parseSignedLeb128(
|
||||
}
|
||||
*end_offset = i;
|
||||
if (shift < 64 && (b & 0x40U))
|
||||
val |= (~0 << shift);
|
||||
val |= (~int64_t{0} << shift);
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -244,7 +244,13 @@
|
||||
<< "Tried to accept/rollback but specified " \
|
||||
"memory outside of the wasm instance " \
|
||||
<< "limit when specifying a reason string"; \
|
||||
return OUT_OF_BOUNDS; \
|
||||
if (!view.rules().enabled(fix20260929)) \
|
||||
return OUT_OF_BOUNDS; \
|
||||
/* fix20260929: terminate as a rollback so the \
|
||||
failure is visible in HookExecution metadata */ \
|
||||
hookCtx.result.exitType = hook_api::ExitType::ROLLBACK; \
|
||||
hookCtx.result.exitCode = (int64_t)OUT_OF_BOUNDS; \
|
||||
return RC_ROLLBACK; \
|
||||
} \
|
||||
/* assembly script and some other languages use utf16 for strings \
|
||||
*/ \
|
||||
|
||||
@@ -215,7 +215,7 @@ Reader::parse(Value& root, BufferSequence const& bs)
|
||||
std::string s;
|
||||
s.reserve(buffer_size(bs));
|
||||
for (auto const& b : bs)
|
||||
s.append(buffer_cast<char const*>(b), buffer_size(b));
|
||||
s.append(static_cast<char const*>(b.data()), buffer_size(b));
|
||||
return parse(s, root);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <charconv>
|
||||
|
||||
@@ -118,8 +119,9 @@ syntaxCheckProof(
|
||||
{
|
||||
if (!proof["hash"].isString() ||
|
||||
proof["hash"].asString().size() != 64 ||
|
||||
!proof["key"].isString() ||
|
||||
!isHex(proof["hash"].asString()) || !proof["key"].isString() ||
|
||||
proof["key"].asString().size() != 64 ||
|
||||
!isHex(proof["key"].asString()) ||
|
||||
!proof["children"].isObject())
|
||||
{
|
||||
JLOG(j.warn()) << "XPOP.transaction.proof tree node has wrong "
|
||||
@@ -132,7 +134,11 @@ syntaxCheckProof(
|
||||
|
||||
for (const auto& branch : proof.getMemberNames())
|
||||
{
|
||||
if (branch.size() != 1 || !isHex(branch))
|
||||
// Upper case only: the txroot computation only looks up "0"-"F",
|
||||
// so any other key would be silently ignored.
|
||||
if (branch.size() != 1 ||
|
||||
branch.find_first_not_of("0123456789ABCDEF") !=
|
||||
std::string::npos)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "XPOP.transaction.proof child node was not 0-F "
|
||||
@@ -143,9 +149,9 @@ syntaxCheckProof(
|
||||
const auto& node = proof[branch];
|
||||
if (!node.isObject() || !node["hash"].isString() ||
|
||||
node["hash"].asString().size() != 64 ||
|
||||
!node["key"].isString() ||
|
||||
!isHex(node["hash"].asString()) || !node["key"].isString() ||
|
||||
node["key"].asString().size() != 64 ||
|
||||
!node["children"].isObject())
|
||||
!isHex(node["key"].asString()) || !node["children"].isObject())
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "XPOP.transaction.proof tree node has wrong format";
|
||||
@@ -465,6 +471,45 @@ getVLInfo(Json::Value const& xpop, beast::Journal const& j)
|
||||
return {{sequence, m->masterKey}};
|
||||
}
|
||||
|
||||
// <effective, expiration> of the unl blob, effective defaulting to the epoch
|
||||
inline std::optional<std::pair<NetClock::time_point, NetClock::time_point>>
|
||||
getVLWindow(Json::Value const& xpop, beast::Journal const& j)
|
||||
{
|
||||
auto const data =
|
||||
base64_decode(xpop[jss::validation][jss::unl][jss::blob].asString());
|
||||
Json::Reader r;
|
||||
Json::Value parsed;
|
||||
if (!r.parse(data, parsed) || !parsed.isObject())
|
||||
{
|
||||
JLOG(j.warn()) << "Import: unl blob was not a json object (after "
|
||||
"base64 decoding)";
|
||||
return {};
|
||||
}
|
||||
|
||||
// const, so that operator[] does not insert missing members
|
||||
Json::Value const& list = parsed;
|
||||
auto const time = [&list](Json::StaticString const& field)
|
||||
-> std::optional<NetClock::time_point> {
|
||||
auto const& v = list[field];
|
||||
if (!v.isInt() || v.asInt() < 0)
|
||||
return {};
|
||||
return NetClock::time_point{NetClock::duration{v.asUInt()}};
|
||||
};
|
||||
|
||||
auto const validUntil = time(jss::expiration);
|
||||
auto const validFrom = list.isMember(jss::effective)
|
||||
? time(jss::effective)
|
||||
: std::optional<NetClock::time_point>{NetClock::time_point{}};
|
||||
if (!validUntil || !validFrom)
|
||||
{
|
||||
JLOG(j.warn()) << "Import: unl blob json (after base64 decoding) "
|
||||
"had missing or invalid effective/expiration";
|
||||
return {};
|
||||
}
|
||||
|
||||
return {{*validFrom, *validUntil}};
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -34,7 +34,10 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FEATURE(OnChainManifests, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(EscrowDestinationCancel, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (20260929, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(HookOnV2_1, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(OnChainManifests, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)
|
||||
@@ -45,7 +48,7 @@ XRPL_FEATURE(HookAPISerializedType240, Supported::yes, VoteBehavior::DefaultNo
|
||||
XRPL_FEATURE(PermissionedDomains, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(DynamicNFT, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Credentials, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(AMMClawback, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(AMMClawback, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(MPTokensV1, Supported::no, VoteBehavior::DefaultNo)
|
||||
// InvariantsV1_1 will be changes to Supported::yes when all the
|
||||
// invariants expected to be included under it are complete.
|
||||
@@ -65,7 +68,8 @@ XRPL_FEATURE(XChainBridge, Supported::no, VoteBehavior::DefaultNo
|
||||
XRPL_FEATURE(AMM, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (ReducedOffersV1, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(HookOnV2, Supported::yes, VoteBehavior::DefaultNo)
|
||||
// replaced to HookOnV2_1
|
||||
XRPL_FEATURE(HookOnV2, Supported::no, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (HookAPI20251128, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (CronStacking, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FEATURE(ExtendedHookState, Supported::yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -22,14 +22,14 @@
|
||||
#endif
|
||||
|
||||
#ifndef LEDGER_ENTRY_DUPLICATE
|
||||
// The EXPAND macro is needed for Windows
|
||||
// The LEDGER_ENTRY_EXPAND macro is needed for Windows
|
||||
// https://stackoverflow.com/questions/5134523/msvc-doesnt-expand-va-args-correctly
|
||||
#define EXPAND(x) x
|
||||
#define LEDGER_ENTRY_EXPAND(x) x
|
||||
|
||||
// The `LEDGER_ENTRY_DUPLICATE macro is needed to avoid JSS conflicts
|
||||
// Since some transactions and ledger entries have the same name (like `DepositPreauth`)
|
||||
// The compiler won't accept two instances of `JSS(DepositPreauth)`
|
||||
#define LEDGER_ENTRY_DUPLICATE(...) EXPAND(LEDGER_ENTRY(__VA_ARGS__))
|
||||
#define LEDGER_ENTRY_DUPLICATE(...) LEDGER_ENTRY_EXPAND(LEDGER_ENTRY(__VA_ARGS__))
|
||||
#endif
|
||||
|
||||
/**
|
||||
@@ -613,6 +613,6 @@ LEDGER_ENTRY(ltMANIFEST, 0x004D, Manifest, manifest_entry, ({
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
#undef EXPAND
|
||||
#undef LEDGER_ENTRY_EXPAND
|
||||
#undef LEDGER_ENTRY_DUPLICATE
|
||||
|
||||
@@ -292,7 +292,9 @@ JSS(duration_us); // out: NetworkOPs
|
||||
JSS(effective); // out: ValidatorList
|
||||
// in: UNL
|
||||
JSS(elapsed_seconds);
|
||||
JSS(enabled); // out: AmendmentTable
|
||||
JSS(enabled); // out: AmendmentTable (on-ledger);
|
||||
// ServerDefinitions (this server)
|
||||
JSS(ledger_enabled); // out: ServerDefinitions (on-ledger)
|
||||
JSS(engine_result); // out: NetworkOPs, TransactionSign, Submit
|
||||
JSS(engine_result_code); // out: NetworkOPs, TransactionSign, Submit
|
||||
JSS(engine_result_message); // out: NetworkOPs, TransactionSign, Submit
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
#include <xrpl/beast/utility/PropertyStream.h>
|
||||
#include <xrpl/server/Port.h>
|
||||
#include <xrpl/server/detail/ServerImpl.h>
|
||||
#include <boost/asio/io_service.hpp>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -33,10 +34,10 @@ template <class Handler>
|
||||
std::unique_ptr<Server>
|
||||
make_Server(
|
||||
Handler& handler,
|
||||
boost::asio::io_service& io_service,
|
||||
boost::asio::io_context& io_context,
|
||||
beast::Journal journal)
|
||||
{
|
||||
return std::make_unique<ServerImpl<Handler>>(handler, io_service, journal);
|
||||
return std::make_unique<ServerImpl<Handler>>(handler, io_context, journal);
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -86,9 +86,7 @@ public:
|
||||
++iter)
|
||||
{
|
||||
typename BufferSequence::value_type const& buffer(*iter);
|
||||
write(
|
||||
boost::asio::buffer_cast<void const*>(buffer),
|
||||
boost::asio::buffer_size(buffer));
|
||||
write(buffer.data(), boost::asio::buffer_size(buffer));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +100,7 @@ public:
|
||||
|
||||
/** Detach the session.
|
||||
This holds the session open so that the response can be sent
|
||||
asynchronously. Calls to io_service::run made by the server
|
||||
asynchronously. Calls to io_context::run made by the server
|
||||
will not return until all detached sessions are closed.
|
||||
*/
|
||||
virtual std::shared_ptr<Session>
|
||||
|
||||
@@ -24,10 +24,12 @@
|
||||
#include <xrpl/beast/net/IPAddressConversion.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/server/Session.h>
|
||||
#include <xrpl/server/detail/Spawn.h>
|
||||
#include <xrpl/server/detail/io_list.h>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/ssl/stream.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/asio/streambuf.hpp>
|
||||
#include <boost/beast/core/stream_traits.hpp>
|
||||
#include <boost/beast/http/dynamic_body.hpp>
|
||||
@@ -214,8 +216,8 @@ BaseHTTPPeer<Handler, Impl>::BaseHTTPPeer(
|
||||
ConstBufferSequence const& buffers)
|
||||
: port_(port)
|
||||
, handler_(handler)
|
||||
, work_(executor)
|
||||
, strand_(executor)
|
||||
, work_(boost::asio::make_work_guard(executor))
|
||||
, strand_(boost::asio::make_strand(executor))
|
||||
, remote_address_(remote_address)
|
||||
, journal_(journal)
|
||||
{
|
||||
@@ -300,7 +302,13 @@ BaseHTTPPeer<Handler, Impl>::do_read(yield_context do_yield)
|
||||
{
|
||||
complete_ = false;
|
||||
error_code ec;
|
||||
start_timer();
|
||||
// Do not treat an established keep-alive waiting for its next request as
|
||||
// an unfinished loopback request.
|
||||
if (request_count_ == 0)
|
||||
start_timer();
|
||||
else
|
||||
boost::beast::get_lowest_layer(impl().stream_)
|
||||
.expires_after(std::chrono::seconds(timeoutSeconds));
|
||||
boost::beast::http::async_read(
|
||||
impl().stream_, read_buf_, message_, do_yield[ec]);
|
||||
cancel_timer();
|
||||
@@ -355,7 +363,7 @@ BaseHTTPPeer<Handler, Impl>::on_write(
|
||||
return;
|
||||
if (graceful_)
|
||||
return do_close();
|
||||
boost::asio::spawn(
|
||||
util::spawn(
|
||||
strand_,
|
||||
std::bind(
|
||||
&BaseHTTPPeer<Handler, Impl>::do_read,
|
||||
@@ -374,7 +382,7 @@ BaseHTTPPeer<Handler, Impl>::do_writer(
|
||||
{
|
||||
auto const p = impl().shared_from_this();
|
||||
resume = std::function<void(void)>([this, p, writer, keep_alive]() {
|
||||
boost::asio::spawn(
|
||||
util::spawn(
|
||||
strand_,
|
||||
std::bind(
|
||||
&BaseHTTPPeer<Handler, Impl>::do_writer,
|
||||
@@ -405,7 +413,7 @@ BaseHTTPPeer<Handler, Impl>::do_writer(
|
||||
if (!keep_alive)
|
||||
return do_close();
|
||||
|
||||
boost::asio::spawn(
|
||||
util::spawn(
|
||||
strand_,
|
||||
std::bind(
|
||||
&BaseHTTPPeer<Handler, Impl>::do_read,
|
||||
@@ -447,14 +455,14 @@ BaseHTTPPeer<Handler, Impl>::write(
|
||||
std::shared_ptr<Writer> const& writer,
|
||||
bool keep_alive)
|
||||
{
|
||||
boost::asio::spawn(bind_executor(
|
||||
util::spawn(
|
||||
strand_,
|
||||
std::bind(
|
||||
&BaseHTTPPeer<Handler, Impl>::do_writer,
|
||||
impl().shared_from_this(),
|
||||
writer,
|
||||
keep_alive,
|
||||
std::placeholders::_1)));
|
||||
std::placeholders::_1));
|
||||
}
|
||||
|
||||
// DEPRECATED
|
||||
@@ -489,12 +497,12 @@ BaseHTTPPeer<Handler, Impl>::complete()
|
||||
}
|
||||
|
||||
// keep-alive
|
||||
boost::asio::spawn(bind_executor(
|
||||
util::spawn(
|
||||
strand_,
|
||||
std::bind(
|
||||
&BaseHTTPPeer<Handler, Impl>::do_read,
|
||||
impl().shared_from_this(),
|
||||
std::placeholders::_1)));
|
||||
std::placeholders::_1));
|
||||
}
|
||||
|
||||
// DEPRECATED
|
||||
|
||||
@@ -89,8 +89,8 @@ BasePeer<Handler, Impl>::BasePeer(
|
||||
return "##" + std::to_string(++id) + " ";
|
||||
}())
|
||||
, j_(sink_)
|
||||
, work_(executor)
|
||||
, strand_(executor)
|
||||
, work_(boost::asio::make_work_guard(executor))
|
||||
, strand_(boost::asio::make_strand(executor))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <xrpl/server/detail/BasePeer.h>
|
||||
#include <xrpl/server/detail/LowestLayer.h>
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/beast/core/multi_buffer.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
@@ -420,11 +421,17 @@ BaseWSPeer<Handler, Impl>::start_timer()
|
||||
// Max seconds without completing a message
|
||||
static constexpr std::chrono::seconds timeout{30};
|
||||
static constexpr std::chrono::seconds timeoutLocal{3};
|
||||
error_code ec;
|
||||
timer_.expires_from_now(
|
||||
remote_endpoint().address().is_loopback() ? timeoutLocal : timeout, ec);
|
||||
if (ec)
|
||||
return fail(ec, "start_timer");
|
||||
|
||||
try
|
||||
{
|
||||
timer_.expires_after(
|
||||
remote_endpoint().address().is_loopback() ? timeoutLocal : timeout);
|
||||
}
|
||||
catch (boost::system::system_error const& e)
|
||||
{
|
||||
return fail(e.code(), "start_timer");
|
||||
}
|
||||
|
||||
timer_.async_wait(bind_executor(
|
||||
strand_,
|
||||
std::bind(
|
||||
@@ -438,8 +445,14 @@ template <class Handler, class Impl>
|
||||
void
|
||||
BaseWSPeer<Handler, Impl>::cancel_timer()
|
||||
{
|
||||
error_code ec;
|
||||
timer_.cancel(ec);
|
||||
try
|
||||
{
|
||||
timer_.cancel();
|
||||
}
|
||||
catch (boost::system::system_error const&)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
template <class Handler, class Impl>
|
||||
|
||||
@@ -69,7 +69,7 @@ private:
|
||||
stream_type stream_;
|
||||
socket_type& socket_;
|
||||
endpoint_type remote_address_;
|
||||
boost::asio::io_context::strand strand_;
|
||||
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
|
||||
beast::Journal const j_;
|
||||
|
||||
public:
|
||||
@@ -95,7 +95,7 @@ private:
|
||||
Handler& handler_;
|
||||
boost::asio::io_context& ioc_;
|
||||
acceptor_type acceptor_;
|
||||
boost::asio::io_context::strand strand_;
|
||||
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
|
||||
bool ssl_;
|
||||
bool plain_;
|
||||
|
||||
@@ -155,7 +155,7 @@ Door<Handler>::Detector::Detector(
|
||||
, stream_(std::move(stream))
|
||||
, socket_(stream_.socket())
|
||||
, remote_address_(remote_address)
|
||||
, strand_(ioc_)
|
||||
, strand_(boost::asio::make_strand(ioc_))
|
||||
, j_(j)
|
||||
{
|
||||
}
|
||||
@@ -164,7 +164,7 @@ template <class Handler>
|
||||
void
|
||||
Door<Handler>::Detector::run()
|
||||
{
|
||||
boost::asio::spawn(
|
||||
util::spawn(
|
||||
strand_,
|
||||
std::bind(
|
||||
&Detector::do_detect,
|
||||
@@ -269,7 +269,7 @@ Door<Handler>::reOpen()
|
||||
Throw<std::exception>();
|
||||
}
|
||||
|
||||
acceptor_.listen(boost::asio::socket_base::max_connections, ec);
|
||||
acceptor_.listen(boost::asio::socket_base::max_listen_connections, ec);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(j_.error()) << "Listen on port '" << port_.name
|
||||
@@ -291,7 +291,7 @@ Door<Handler>::Door(
|
||||
, handler_(handler)
|
||||
, ioc_(io_context)
|
||||
, acceptor_(io_context)
|
||||
, strand_(io_context)
|
||||
, strand_(boost::asio::make_strand(io_context))
|
||||
, ssl_(
|
||||
port_.protocol.count("https") > 0 ||
|
||||
port_.protocol.count("wss") > 0 || port_.protocol.count("wss2") > 0 ||
|
||||
@@ -307,7 +307,7 @@ template <class Handler>
|
||||
void
|
||||
Door<Handler>::run()
|
||||
{
|
||||
boost::asio::spawn(
|
||||
util::spawn(
|
||||
strand_,
|
||||
std::bind(
|
||||
&Door<Handler>::do_accept,
|
||||
@@ -320,7 +320,8 @@ void
|
||||
Door<Handler>::close()
|
||||
{
|
||||
if (!strand_.running_in_this_thread())
|
||||
return strand_.post(
|
||||
return boost::asio::post(
|
||||
strand_,
|
||||
std::bind(&Door<Handler>::close, this->shared_from_this()));
|
||||
error_code ec;
|
||||
acceptor_.close(ec);
|
||||
@@ -400,7 +401,7 @@ Door<Handler>::do_accept(boost::asio::yield_context do_yield)
|
||||
{
|
||||
create(
|
||||
ssl_,
|
||||
boost::asio::null_buffers{},
|
||||
boost::asio::const_buffer{},
|
||||
std::move(stream),
|
||||
remote_address);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ PlainHTTPPeer<Handler>::run()
|
||||
{
|
||||
if (!this->handler_.onAccept(this->session(), this->remote_address_))
|
||||
{
|
||||
boost::asio::spawn(
|
||||
util::spawn(
|
||||
this->strand_,
|
||||
std::bind(&PlainHTTPPeer::do_close, this->shared_from_this()));
|
||||
return;
|
||||
@@ -112,7 +112,7 @@ PlainHTTPPeer<Handler>::run()
|
||||
if (!socket_.is_open())
|
||||
return;
|
||||
|
||||
boost::asio::spawn(
|
||||
util::spawn(
|
||||
this->strand_,
|
||||
std::bind(
|
||||
&PlainHTTPPeer::do_read,
|
||||
|
||||
@@ -113,14 +113,14 @@ SSLHTTPPeer<Handler>::run()
|
||||
{
|
||||
if (!this->handler_.onAccept(this->session(), this->remote_address_))
|
||||
{
|
||||
boost::asio::spawn(
|
||||
util::spawn(
|
||||
this->strand_,
|
||||
std::bind(&SSLHTTPPeer::do_close, this->shared_from_this()));
|
||||
return;
|
||||
}
|
||||
if (!socket_.is_open())
|
||||
return;
|
||||
boost::asio::spawn(
|
||||
util::spawn(
|
||||
this->strand_,
|
||||
std::bind(
|
||||
&SSLHTTPPeer::do_handshake,
|
||||
@@ -162,7 +162,7 @@ SSLHTTPPeer<Handler>::do_handshake(yield_context do_yield)
|
||||
this->port().protocol.count("https") > 0;
|
||||
if (http)
|
||||
{
|
||||
boost::asio::spawn(
|
||||
util::spawn(
|
||||
this->strand_,
|
||||
std::bind(
|
||||
&SSLHTTPPeer::do_read,
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
#include <xrpl/server/detail/io_list.h>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
@@ -86,9 +88,11 @@ private:
|
||||
|
||||
Handler& handler_;
|
||||
beast::Journal const j_;
|
||||
boost::asio::io_service& io_service_;
|
||||
boost::asio::io_service::strand strand_;
|
||||
std::optional<boost::asio::io_service::work> work_;
|
||||
boost::asio::io_context& io_context_;
|
||||
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
|
||||
std::optional<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>
|
||||
work_;
|
||||
|
||||
std::mutex m_;
|
||||
std::vector<Port> ports_;
|
||||
@@ -101,7 +105,7 @@ private:
|
||||
public:
|
||||
ServerImpl(
|
||||
Handler& handler,
|
||||
boost::asio::io_service& io_service,
|
||||
boost::asio::io_context& io_context,
|
||||
beast::Journal journal);
|
||||
|
||||
~ServerImpl();
|
||||
@@ -124,10 +128,10 @@ public:
|
||||
return ios_;
|
||||
}
|
||||
|
||||
boost::asio::io_service&
|
||||
get_io_service()
|
||||
boost::asio::io_context&
|
||||
get_io_context()
|
||||
{
|
||||
return io_service_;
|
||||
return io_context_;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -141,13 +145,13 @@ private:
|
||||
template <class Handler>
|
||||
ServerImpl<Handler>::ServerImpl(
|
||||
Handler& handler,
|
||||
boost::asio::io_service& io_service,
|
||||
boost::asio::io_context& io_context,
|
||||
beast::Journal journal)
|
||||
: handler_(handler)
|
||||
, j_(journal)
|
||||
, io_service_(io_service)
|
||||
, strand_(io_service_)
|
||||
, work_(io_service_)
|
||||
, io_context_(io_context)
|
||||
, strand_(boost::asio::make_strand(io_context_))
|
||||
, work_(std::in_place, boost::asio::make_work_guard(io_context_))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -179,7 +183,7 @@ ServerImpl<Handler>::ports(std::vector<Port> const& ports)
|
||||
{
|
||||
// UDP-RPC door
|
||||
if (auto sp = ios_.emplace<UDPDoor<Handler>>(
|
||||
handler_, io_service_, internalPort, j_))
|
||||
handler_, io_context_, internalPort, j_))
|
||||
{
|
||||
auto ep = sp->get_endpoint();
|
||||
if (!internalPort.port)
|
||||
@@ -193,7 +197,7 @@ ServerImpl<Handler>::ports(std::vector<Port> const& ports)
|
||||
{
|
||||
// Standard TCP door
|
||||
if (auto sp = ios_.emplace<Door<Handler>>(
|
||||
handler_, io_service_, internalPort, j_))
|
||||
handler_, io_context_, internalPort, j_))
|
||||
{
|
||||
list_.push_back(sp);
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_SERVER_SPAWN_H_INCLUDED
|
||||
#define RIPPLE_SERVER_SPAWN_H_INCLUDED
|
||||
|
||||
#include <xrpl/basics/Log.h>
|
||||
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
|
||||
#include <concepts>
|
||||
#include <type_traits>
|
||||
|
||||
namespace ripple::util {
|
||||
namespace impl {
|
||||
|
||||
template <typename T>
|
||||
concept IsStrand = std::same_as<
|
||||
std::decay_t<T>,
|
||||
boost::asio::strand<typename std::decay_t<T>::inner_executor_type>>;
|
||||
|
||||
/**
|
||||
* @brief A completion handler that restores `boost::asio::spawn`'s behaviour
|
||||
* from Boost 1.83
|
||||
*
|
||||
* This is intended to be passed as the third argument to `boost::asio::spawn`
|
||||
* so that exceptions are not ignored but propagated to `io_context.run()` call
|
||||
* site.
|
||||
*
|
||||
* @param ePtr The exception that was caught on the coroutine
|
||||
*/
|
||||
inline constexpr auto kPROPAGATE_EXCEPTIONS = [](std::exception_ptr ePtr) {
|
||||
if (ePtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::rethrow_exception(ePtr);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(debugLog().warn()) << "Spawn exception: " << e.what();
|
||||
throw;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
JLOG(debugLog().warn()) << "Spawn exception: Unknown";
|
||||
throw;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace impl
|
||||
|
||||
/**
|
||||
* @brief Spawns a coroutine using `boost::asio::spawn`
|
||||
*
|
||||
* @note This uses kPROPAGATE_EXCEPTIONS to force asio to propagate exceptions
|
||||
* through `io_context`
|
||||
* @note Since implicit strand was removed from boost::asio::spawn this helper
|
||||
* function adds the strand back
|
||||
*
|
||||
* @tparam Ctx The type of the context/strand
|
||||
* @tparam F The type of the function to execute
|
||||
* @param ctx The execution context
|
||||
* @param func The function to execute. Must return `void`
|
||||
*/
|
||||
template <typename Ctx, typename F>
|
||||
requires std::is_invocable_r_v<void, F, boost::asio::yield_context>
|
||||
void
|
||||
spawn(Ctx&& ctx, F&& func)
|
||||
{
|
||||
if constexpr (impl::IsStrand<Ctx>)
|
||||
{
|
||||
boost::asio::spawn(
|
||||
std::forward<Ctx>(ctx),
|
||||
std::forward<F>(func),
|
||||
impl::kPROPAGATE_EXCEPTIONS);
|
||||
}
|
||||
else
|
||||
{
|
||||
// make_strand accepts an executor or an execution context directly.
|
||||
// Going through get_associated_executor here resolved a plain
|
||||
// executor to asio's inline executor, which cannot be stranded and
|
||||
// did not compile; nothing in the tree had instantiated this branch.
|
||||
boost::asio::spawn(
|
||||
boost::asio::make_strand(std::forward<Ctx>(ctx)),
|
||||
std::forward<F>(func),
|
||||
impl::kPROPAGATE_EXCEPTIONS);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ripple::util
|
||||
|
||||
#endif
|
||||
@@ -166,7 +166,7 @@ public:
|
||||
May be called concurrently.
|
||||
|
||||
Preconditions:
|
||||
No call to io_service::run on any io_service
|
||||
No call to io_context::run on any io_context
|
||||
used by work objects associated with this io_list
|
||||
exists in the caller's call stack.
|
||||
*/
|
||||
|
||||
@@ -22,7 +22,14 @@
|
||||
#include <xrpl/beast/net/IPAddressConversion.h>
|
||||
#include <xrpl/beast/net/IPEndpoint.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include <boost/asio/bind_executor.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
@@ -110,8 +117,8 @@ public:
|
||||
|
||||
beast::Journal m_journal;
|
||||
|
||||
boost::asio::io_service& m_io_service;
|
||||
boost::asio::io_service::strand m_strand;
|
||||
boost::asio::io_context& m_io_context;
|
||||
boost::asio::strand<boost::asio::io_context::executor_type> m_strand;
|
||||
boost::asio::ip::tcp::resolver m_resolver;
|
||||
|
||||
std::condition_variable m_cv;
|
||||
@@ -141,12 +148,12 @@ public:
|
||||
std::deque<Work> m_work;
|
||||
|
||||
ResolverAsioImpl(
|
||||
boost::asio::io_service& io_service,
|
||||
boost::asio::io_context& io_context,
|
||||
beast::Journal journal)
|
||||
: m_journal(journal)
|
||||
, m_io_service(io_service)
|
||||
, m_strand(io_service)
|
||||
, m_resolver(io_service)
|
||||
, m_io_context(io_context)
|
||||
, m_strand(boost::asio::make_strand(io_context))
|
||||
, m_resolver(io_context)
|
||||
, m_asyncHandlersCompleted(true)
|
||||
, m_stop_called(false)
|
||||
, m_stopped(true)
|
||||
@@ -202,8 +209,14 @@ public:
|
||||
{
|
||||
if (m_stop_called.exchange(true) == false)
|
||||
{
|
||||
m_io_service.dispatch(m_strand.wrap(std::bind(
|
||||
&ResolverAsioImpl::do_stop, this, CompletionCounter(this))));
|
||||
boost::asio::dispatch(
|
||||
m_io_context,
|
||||
boost::asio::bind_executor(
|
||||
m_strand,
|
||||
std::bind(
|
||||
&ResolverAsioImpl::do_stop,
|
||||
this,
|
||||
CompletionCounter(this))));
|
||||
|
||||
JLOG(m_journal.debug()) << "Queued a stop request";
|
||||
}
|
||||
@@ -234,12 +247,16 @@ public:
|
||||
|
||||
// TODO NIKB use rvalue references to construct and move
|
||||
// reducing cost.
|
||||
m_io_service.dispatch(m_strand.wrap(std::bind(
|
||||
&ResolverAsioImpl::do_resolve,
|
||||
this,
|
||||
names,
|
||||
handler,
|
||||
CompletionCounter(this))));
|
||||
boost::asio::dispatch(
|
||||
m_io_context,
|
||||
boost::asio::bind_executor(
|
||||
m_strand,
|
||||
std::bind(
|
||||
&ResolverAsioImpl::do_resolve,
|
||||
this,
|
||||
names,
|
||||
handler,
|
||||
CompletionCounter(this))));
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------------
|
||||
@@ -265,19 +282,20 @@ public:
|
||||
std::string name,
|
||||
boost::system::error_code const& ec,
|
||||
HandlerType handler,
|
||||
boost::asio::ip::tcp::resolver::iterator iter,
|
||||
boost::asio::ip::tcp::resolver::results_type results,
|
||||
CompletionCounter)
|
||||
{
|
||||
if (ec == boost::asio::error::operation_aborted)
|
||||
return;
|
||||
|
||||
std::vector<beast::IP::Endpoint> addresses;
|
||||
auto iter = results.begin();
|
||||
|
||||
// If we get an error message back, we don't return any
|
||||
// results that we may have gotten.
|
||||
if (!ec)
|
||||
{
|
||||
while (iter != boost::asio::ip::tcp::resolver::iterator())
|
||||
while (iter != results.end())
|
||||
{
|
||||
addresses.push_back(
|
||||
beast::IPAddressConversion::from_asio(*iter));
|
||||
@@ -287,8 +305,14 @@ public:
|
||||
|
||||
handler(name, addresses);
|
||||
|
||||
m_io_service.post(m_strand.wrap(std::bind(
|
||||
&ResolverAsioImpl::do_work, this, CompletionCounter(this))));
|
||||
boost::asio::post(
|
||||
m_io_context,
|
||||
boost::asio::bind_executor(
|
||||
m_strand,
|
||||
std::bind(
|
||||
&ResolverAsioImpl::do_work,
|
||||
this,
|
||||
CompletionCounter(this))));
|
||||
}
|
||||
|
||||
HostAndPort
|
||||
@@ -369,16 +393,21 @@ public:
|
||||
{
|
||||
JLOG(m_journal.error()) << "Unable to parse '" << name << "'";
|
||||
|
||||
m_io_service.post(m_strand.wrap(std::bind(
|
||||
&ResolverAsioImpl::do_work, this, CompletionCounter(this))));
|
||||
boost::asio::post(
|
||||
m_io_context,
|
||||
boost::asio::bind_executor(
|
||||
m_strand,
|
||||
std::bind(
|
||||
&ResolverAsioImpl::do_work,
|
||||
this,
|
||||
CompletionCounter(this))));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
boost::asio::ip::tcp::resolver::query query(host, port);
|
||||
|
||||
m_resolver.async_resolve(
|
||||
query,
|
||||
host,
|
||||
port,
|
||||
std::bind(
|
||||
&ResolverAsioImpl::do_finish,
|
||||
this,
|
||||
@@ -409,10 +438,14 @@ public:
|
||||
|
||||
if (m_work.size() > 0)
|
||||
{
|
||||
m_io_service.post(m_strand.wrap(std::bind(
|
||||
&ResolverAsioImpl::do_work,
|
||||
this,
|
||||
CompletionCounter(this))));
|
||||
boost::asio::post(
|
||||
m_io_context,
|
||||
boost::asio::bind_executor(
|
||||
m_strand,
|
||||
std::bind(
|
||||
&ResolverAsioImpl::do_work,
|
||||
this,
|
||||
CompletionCounter(this))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,9 +454,9 @@ public:
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
std::unique_ptr<ResolverAsio>
|
||||
ResolverAsio::New(boost::asio::io_service& io_service, beast::Journal journal)
|
||||
ResolverAsio::New(boost::asio::io_context& io_context, beast::Journal journal)
|
||||
{
|
||||
return std::make_unique<ResolverAsioImpl>(io_service, journal);
|
||||
return std::make_unique<ResolverAsioImpl>(io_context, journal);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpl/basics/UTF8.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
bool
|
||||
isValidUTF8(Slice const& s) noexcept
|
||||
{
|
||||
std::size_t const size = s.size();
|
||||
|
||||
// Markus Kuhn's utf8_check.c
|
||||
// (https://www.cl.cam.ac.uk/~mgk25/ucs/utf8_check.c), the decoder
|
||||
// URIToken has always used, with one change: each branch establishes
|
||||
// that the whole sequence is present before reading any continuation
|
||||
// byte, so a truncated tail is invalid rather than an out-of-bounds read.
|
||||
std::size_t i = 0;
|
||||
while (i < size)
|
||||
{
|
||||
std::uint8_t const c0 = s[i];
|
||||
|
||||
if (c0 < 0x80)
|
||||
{
|
||||
// 0xxxxxxx
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((c0 & 0xE0) == 0xC0)
|
||||
{
|
||||
// 110xxxxx 10xxxxxx
|
||||
if (size - i < 2)
|
||||
return false;
|
||||
if ((s[i + 1] & 0xC0) != 0x80)
|
||||
return false;
|
||||
if ((c0 & 0xFE) == 0xC0) // overlong
|
||||
return false;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((c0 & 0xF0) == 0xE0)
|
||||
{
|
||||
// 1110xxxx 10xxxxxx 10xxxxxx
|
||||
if (size - i < 3)
|
||||
return false;
|
||||
std::uint8_t const c1 = s[i + 1];
|
||||
if ((c1 & 0xC0) != 0x80 || (s[i + 2] & 0xC0) != 0x80)
|
||||
return false;
|
||||
if (c0 == 0xE0 && (c1 & 0xE0) == 0x80) // overlong
|
||||
return false;
|
||||
if (c0 == 0xED && (c1 & 0xE0) == 0xA0) // UTF-16 surrogate
|
||||
return false;
|
||||
if (c0 == 0xEF && c1 == 0xBF &&
|
||||
(s[i + 2] & 0xFE) == 0xBE) // U+FFFE or U+FFFF
|
||||
return false;
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((c0 & 0xF8) == 0xF0)
|
||||
{
|
||||
// 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
|
||||
if (size - i < 4)
|
||||
return false;
|
||||
std::uint8_t const c1 = s[i + 1];
|
||||
if ((c1 & 0xC0) != 0x80 || (s[i + 2] & 0xC0) != 0x80 ||
|
||||
(s[i + 3] & 0xC0) != 0x80)
|
||||
return false;
|
||||
if (c0 == 0xF0 && (c1 & 0xF0) == 0x80) // overlong
|
||||
return false;
|
||||
if (c0 > 0xF4 || (c0 == 0xF4 && c1 > 0x8F)) // > U+10FFFF
|
||||
return false;
|
||||
i += 4;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stray continuation byte or 0xF8..0xFF.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -26,10 +26,24 @@
|
||||
#include <xrpl/beast/insight/StatsDCollector.h>
|
||||
#include <xrpl/beast/net/IPAddressConversion.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
|
||||
#include <boost/asio/basic_waitable_timer.hpp>
|
||||
#include <boost/asio/bind_executor.hpp>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/udp.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
@@ -223,9 +237,11 @@ private:
|
||||
Journal m_journal;
|
||||
IP::Endpoint m_address;
|
||||
std::string m_prefix;
|
||||
boost::asio::io_service m_io_service;
|
||||
std::optional<boost::asio::io_service::work> m_work;
|
||||
boost::asio::io_service::strand m_strand;
|
||||
boost::asio::io_context m_io_context;
|
||||
std::optional<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>
|
||||
m_work;
|
||||
boost::asio::strand<boost::asio::io_context::executor_type> m_strand;
|
||||
boost::asio::basic_waitable_timer<std::chrono::steady_clock> m_timer;
|
||||
boost::asio::ip::udp::socket m_socket;
|
||||
std::deque<std::string> m_data;
|
||||
@@ -249,18 +265,24 @@ public:
|
||||
: m_journal(journal)
|
||||
, m_address(address)
|
||||
, m_prefix(prefix)
|
||||
, m_work(std::ref(m_io_service))
|
||||
, m_strand(m_io_service)
|
||||
, m_timer(m_io_service)
|
||||
, m_socket(m_io_service)
|
||||
, m_work(boost::asio::make_work_guard(m_io_context))
|
||||
, m_strand(boost::asio::make_strand(m_io_context))
|
||||
, m_timer(m_io_context)
|
||||
, m_socket(m_io_context)
|
||||
, m_thread(&StatsDCollectorImp::run, this)
|
||||
{
|
||||
}
|
||||
|
||||
~StatsDCollectorImp() override
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
m_timer.cancel(ec);
|
||||
try
|
||||
{
|
||||
m_timer.cancel();
|
||||
}
|
||||
catch (boost::system::system_error const&)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
m_work.reset();
|
||||
m_thread.join();
|
||||
@@ -319,10 +341,10 @@ public:
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
boost::asio::io_service&
|
||||
get_io_service()
|
||||
boost::asio::io_context&
|
||||
get_io_context()
|
||||
{
|
||||
return m_io_service;
|
||||
return m_io_context;
|
||||
}
|
||||
|
||||
std::string const&
|
||||
@@ -340,8 +362,14 @@ public:
|
||||
void
|
||||
post_buffer(std::string&& buffer)
|
||||
{
|
||||
m_io_service.dispatch(m_strand.wrap(std::bind(
|
||||
&StatsDCollectorImp::do_post_buffer, this, std::move(buffer))));
|
||||
boost::asio::dispatch(
|
||||
m_io_context,
|
||||
boost::asio::bind_executor(
|
||||
m_strand,
|
||||
std::bind(
|
||||
&StatsDCollectorImp::do_post_buffer,
|
||||
this,
|
||||
std::move(buffer))));
|
||||
}
|
||||
|
||||
// The keepAlive parameter makes sure the buffers sent to
|
||||
@@ -371,7 +399,7 @@ public:
|
||||
for (auto const& buffer : buffers)
|
||||
{
|
||||
std::string const s(
|
||||
boost::asio::buffer_cast<char const*>(buffer),
|
||||
static_cast<char const*>(buffer.data()),
|
||||
boost::asio::buffer_size(buffer));
|
||||
std::cerr << s;
|
||||
}
|
||||
@@ -441,7 +469,7 @@ public:
|
||||
set_timer()
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
m_timer.expires_from_now(1s);
|
||||
m_timer.expires_after(1s);
|
||||
m_timer.async_wait(std::bind(
|
||||
&StatsDCollectorImp::on_timer, this, std::placeholders::_1));
|
||||
}
|
||||
@@ -483,13 +511,13 @@ public:
|
||||
|
||||
set_timer();
|
||||
|
||||
m_io_service.run();
|
||||
m_io_context.run();
|
||||
|
||||
m_socket.shutdown(boost::asio::ip::udp::socket::shutdown_send, ec);
|
||||
|
||||
m_socket.close();
|
||||
|
||||
m_io_service.poll();
|
||||
m_io_context.poll();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -532,10 +560,12 @@ StatsDCounterImpl::~StatsDCounterImpl()
|
||||
void
|
||||
StatsDCounterImpl::increment(CounterImpl::value_type amount)
|
||||
{
|
||||
m_impl->get_io_service().dispatch(std::bind(
|
||||
&StatsDCounterImpl::do_increment,
|
||||
std::static_pointer_cast<StatsDCounterImpl>(shared_from_this()),
|
||||
amount));
|
||||
boost::asio::dispatch(
|
||||
m_impl->get_io_context(),
|
||||
std::bind(
|
||||
&StatsDCounterImpl::do_increment,
|
||||
std::static_pointer_cast<StatsDCounterImpl>(shared_from_this()),
|
||||
amount));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -577,10 +607,12 @@ StatsDEventImpl::StatsDEventImpl(
|
||||
void
|
||||
StatsDEventImpl::notify(EventImpl::value_type const& value)
|
||||
{
|
||||
m_impl->get_io_service().dispatch(std::bind(
|
||||
&StatsDEventImpl::do_notify,
|
||||
std::static_pointer_cast<StatsDEventImpl>(shared_from_this()),
|
||||
value));
|
||||
boost::asio::dispatch(
|
||||
m_impl->get_io_context(),
|
||||
std::bind(
|
||||
&StatsDEventImpl::do_notify,
|
||||
std::static_pointer_cast<StatsDEventImpl>(shared_from_this()),
|
||||
value));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -610,19 +642,23 @@ StatsDGaugeImpl::~StatsDGaugeImpl()
|
||||
void
|
||||
StatsDGaugeImpl::set(GaugeImpl::value_type value)
|
||||
{
|
||||
m_impl->get_io_service().dispatch(std::bind(
|
||||
&StatsDGaugeImpl::do_set,
|
||||
std::static_pointer_cast<StatsDGaugeImpl>(shared_from_this()),
|
||||
value));
|
||||
boost::asio::dispatch(
|
||||
m_impl->get_io_context(),
|
||||
std::bind(
|
||||
&StatsDGaugeImpl::do_set,
|
||||
std::static_pointer_cast<StatsDGaugeImpl>(shared_from_this()),
|
||||
value));
|
||||
}
|
||||
|
||||
void
|
||||
StatsDGaugeImpl::increment(GaugeImpl::difference_type amount)
|
||||
{
|
||||
m_impl->get_io_service().dispatch(std::bind(
|
||||
&StatsDGaugeImpl::do_increment,
|
||||
std::static_pointer_cast<StatsDGaugeImpl>(shared_from_this()),
|
||||
amount));
|
||||
boost::asio::dispatch(
|
||||
m_impl->get_io_context(),
|
||||
std::bind(
|
||||
&StatsDGaugeImpl::do_increment,
|
||||
std::static_pointer_cast<StatsDGaugeImpl>(shared_from_this()),
|
||||
amount));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -698,10 +734,12 @@ StatsDMeterImpl::~StatsDMeterImpl()
|
||||
void
|
||||
StatsDMeterImpl::increment(MeterImpl::value_type amount)
|
||||
{
|
||||
m_impl->get_io_service().dispatch(std::bind(
|
||||
&StatsDMeterImpl::do_increment,
|
||||
std::static_pointer_cast<StatsDMeterImpl>(shared_from_this()),
|
||||
amount));
|
||||
boost::asio::dispatch(
|
||||
m_impl->get_io_context(),
|
||||
std::bind(
|
||||
&StatsDMeterImpl::do_increment,
|
||||
std::static_pointer_cast<StatsDMeterImpl>(shared_from_this()),
|
||||
amount));
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -28,11 +28,11 @@ namespace IP {
|
||||
bool
|
||||
is_private(AddressV4 const& addr)
|
||||
{
|
||||
return ((addr.to_ulong() & 0xff000000) ==
|
||||
return ((addr.to_uint() & 0xff000000) ==
|
||||
0x0a000000) || // Prefix /8, 10. #.#.#
|
||||
((addr.to_ulong() & 0xfff00000) ==
|
||||
((addr.to_uint() & 0xfff00000) ==
|
||||
0xac100000) || // Prefix /12 172. 16.#.# - 172.31.#.#
|
||||
((addr.to_ulong() & 0xffff0000) ==
|
||||
((addr.to_uint() & 0xffff0000) ==
|
||||
0xc0a80000) || // Prefix /16 192.168.#.#
|
||||
addr.is_loopback();
|
||||
}
|
||||
@@ -47,7 +47,7 @@ char
|
||||
get_class(AddressV4 const& addr)
|
||||
{
|
||||
static char const* table = "AAAABBCD";
|
||||
return table[(addr.to_ulong() & 0xE0000000) >> 29];
|
||||
return table[(addr.to_uint() & 0xE0000000) >> 29];
|
||||
}
|
||||
|
||||
} // namespace IP
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include <xrpl/beast/net/IPAddressV4.h>
|
||||
#include <xrpl/beast/net/IPAddressV6.h>
|
||||
|
||||
#include <boost/asio/ip/address_v4.hpp>
|
||||
|
||||
namespace beast {
|
||||
namespace IP {
|
||||
|
||||
@@ -28,7 +30,9 @@ is_private(AddressV6 const& addr)
|
||||
{
|
||||
return (
|
||||
(addr.to_bytes()[0] & 0xfd) || // TODO fc00::/8 too ?
|
||||
(addr.is_v4_mapped() && is_private(addr.to_v4())));
|
||||
(addr.is_v4_mapped() &&
|
||||
is_private(boost::asio::ip::make_address_v4(
|
||||
boost::asio::ip::v4_mapped, addr))));
|
||||
}
|
||||
|
||||
bool
|
||||
|
||||
@@ -18,7 +18,19 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpl/beast/net/IPEndpoint.h>
|
||||
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/address_v4.hpp>
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <cctype>
|
||||
#include <ios>
|
||||
#include <istream>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace beast {
|
||||
namespace IP {
|
||||
@@ -157,7 +169,7 @@ operator>>(std::istream& is, Endpoint& endpoint)
|
||||
}
|
||||
|
||||
boost::system::error_code ec;
|
||||
auto addr = Address::from_string(addrStr, ec);
|
||||
auto addr = boost::asio::ip::make_address(addrStr, ec);
|
||||
if (ec)
|
||||
{
|
||||
is.setstate(std::ios_base::failbit);
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -205,7 +208,7 @@ parse_Port(ParsedPort& port, Section const& section, std::ostream& log)
|
||||
{
|
||||
try
|
||||
{
|
||||
port.ip = boost::asio::ip::address::from_string(*optResult);
|
||||
port.ip = boost::asio::ip::make_address(*optResult);
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
|
||||
@@ -150,12 +150,15 @@ private:
|
||||
|
||||
env.fund(XRP(20'000), alice, bob, carol, gw1, gw2);
|
||||
env.fund(XRP(20'000), dan);
|
||||
env.close();
|
||||
env.trust(USD1(20'000), alice, bob, carol, dan);
|
||||
env.trust(USD2(1'000), alice, bob, carol, dan);
|
||||
env.close();
|
||||
|
||||
env(pay(gw1, dan, USD1(10'050)));
|
||||
env(pay(gw1, bob, USD1(50)));
|
||||
env(pay(gw2, bob, USD2(50)));
|
||||
env.close();
|
||||
|
||||
AMM ammDan(env, dan, XRP(10'000), USD1(10'050));
|
||||
|
||||
@@ -1990,6 +1993,7 @@ private:
|
||||
|
||||
env.fund(XRP(10'000), alice, carol, gw);
|
||||
env.fund(XRP(10'000), bob);
|
||||
env.close();
|
||||
env.trust(USD(1'000), alice, bob, carol);
|
||||
env.trust(BTC(1'000), alice, bob, carol);
|
||||
env.trust(EUR(1'000), alice, bob, carol);
|
||||
@@ -3641,7 +3645,7 @@ private:
|
||||
USD(10'000),
|
||||
false,
|
||||
0,
|
||||
ammCrtFee(env).drops(),
|
||||
ammCrtFee(env).drops() + 2 * env.current()->fees().base.drops(),
|
||||
std::nullopt,
|
||||
std::nullopt,
|
||||
ms,
|
||||
|
||||
@@ -5004,8 +5004,8 @@ private:
|
||||
alice,
|
||||
STAmount{USD, UINT64_C(30'000'0000000003), -10}));
|
||||
// alice XRP balance is 30,000initial - 50 ammcreate fee -
|
||||
// 10drops fee
|
||||
BEAST_EXPECT(accountBalance(env, alice) == "29949999990");
|
||||
// 10drops ammcreate base fee - 10drops fee
|
||||
BEAST_EXPECT(accountBalance(env, alice) == "29949999980");
|
||||
},
|
||||
std::nullopt,
|
||||
0,
|
||||
@@ -5081,7 +5081,7 @@ private:
|
||||
(xrpBalance + drops(5)).getText());
|
||||
BEAST_EXPECT(
|
||||
accountBalance(env, alice) ==
|
||||
std::to_string(29'950'000'000 - baseFee + 80));
|
||||
std::to_string(29'950'000'000 - 2 * baseFee + 80));
|
||||
},
|
||||
std::nullopt,
|
||||
0,
|
||||
|
||||
@@ -563,6 +563,20 @@ public:
|
||||
env(reward::claim(alice),
|
||||
reward::issuer(issuer),
|
||||
ter(tecNO_TARGET));
|
||||
env.close();
|
||||
}
|
||||
// Invalid HookName
|
||||
{
|
||||
auto hookObj = hso(jtx::genesis::AcceptHook, overrideFlag);
|
||||
hookObj[jss::HookOn] = to_string(~UINT256_BIT[ttCLAIM_REWARD]);
|
||||
hookObj[jss::HookName] = "4142434445";
|
||||
env(hook(issuer, {{hookObj}}, 0), fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
env(reward::claim(alice),
|
||||
reward::issuer(issuer),
|
||||
ter(tecNO_TARGET));
|
||||
env.close();
|
||||
}
|
||||
// Vaild HookOn
|
||||
{
|
||||
|
||||
@@ -61,7 +61,7 @@ public:
|
||||
pUrl_.domain,
|
||||
pUrl_.path,
|
||||
port_,
|
||||
env_.app().getIOService(),
|
||||
env_.app().getIOContext(),
|
||||
env_.journal,
|
||||
env_.app().config(),
|
||||
lastEndpoint,
|
||||
@@ -78,10 +78,11 @@ public:
|
||||
isMultipleEndpoints()
|
||||
{
|
||||
using boost::asio::ip::tcp;
|
||||
tcp::resolver resolver(env_.app().getIOService());
|
||||
tcp::resolver resolver(env_.app().getIOContext());
|
||||
std::string port = pUrl_.port ? std::to_string(*pUrl_.port) : "443";
|
||||
tcp::resolver::iterator it = resolver.resolve(pUrl_.domain, port);
|
||||
tcp::resolver::iterator end;
|
||||
auto results = resolver.resolve(pUrl_.domain, port);
|
||||
auto it = results.begin();
|
||||
auto end = results.end();
|
||||
int n = 0;
|
||||
for (; it != end; ++it)
|
||||
++n;
|
||||
|
||||
@@ -301,8 +301,14 @@ struct DepositAuth_test : public beast::unit_test::suite
|
||||
Env env(*this, features);
|
||||
|
||||
env.fund(XRP(10000), gw1, alice, bob);
|
||||
env(trust(gw1, alice["USD"](10), noRipplePrev ? tfSetNoRipple : 0));
|
||||
env(trust(gw1, bob["USD"](10), noRippleNext ? tfSetNoRipple : 0));
|
||||
env(trust(
|
||||
gw1,
|
||||
alice["USD"](10),
|
||||
noRipplePrev ? std::uint32_t(tfSetNoRipple) : 0));
|
||||
env(trust(
|
||||
gw1,
|
||||
bob["USD"](10),
|
||||
noRippleNext ? std::uint32_t(tfSetNoRipple) : 0));
|
||||
env.trust(USD1(10), alice, bob);
|
||||
|
||||
env(pay(gw1, alice, USD1(10)));
|
||||
@@ -324,8 +330,14 @@ struct DepositAuth_test : public beast::unit_test::suite
|
||||
Env env(*this, features);
|
||||
|
||||
env.fund(XRP(10000), gw1, gw2, alice);
|
||||
env(trust(alice, USD1(10), noRipplePrev ? tfSetNoRipple : 0));
|
||||
env(trust(alice, USD2(10), noRippleNext ? tfSetNoRipple : 0));
|
||||
env(trust(
|
||||
alice,
|
||||
USD1(10),
|
||||
noRipplePrev ? std::uint32_t(tfSetNoRipple) : 0));
|
||||
env(trust(
|
||||
alice,
|
||||
USD2(10),
|
||||
noRippleNext ? std::uint32_t(tfSetNoRipple) : 0));
|
||||
env(pay(gw2, alice, USD2(10)));
|
||||
|
||||
if (withDepositAuth)
|
||||
|
||||
@@ -4722,6 +4722,359 @@ struct Escrow_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDestinationCancel(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace std::chrono;
|
||||
|
||||
bool const enabled = features[featureEscrowDestinationCancel];
|
||||
TER const destResult =
|
||||
enabled ? TER{tesSUCCESS} : TER{tecNO_PERMISSION};
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const bob = Account("bob");
|
||||
auto const carol = Account("carol");
|
||||
auto const gw = Account{"gateway"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
auto const ownerDirEmpty = [](Env& env, Account const& acct) {
|
||||
ripple::Dir dir(*env.current(), keylet::ownerDir(acct.id()));
|
||||
return std::distance(dir.begin(), dir.end()) == 0;
|
||||
};
|
||||
|
||||
{
|
||||
testcase("Destination Cancel: before CancelAfter");
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(5000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow(alice, bob, XRP(1000)),
|
||||
finish_time(env.now() + 100s),
|
||||
cancel_time(env.now() + 200s));
|
||||
env.close();
|
||||
|
||||
// Neither the owner nor a third party may cancel early.
|
||||
env(cancel(alice, alice, seq), ter(tecNO_PERMISSION));
|
||||
env(cancel(carol, alice, seq), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
STAmount const preAlice = env.balance(alice);
|
||||
env(cancel(bob, alice, seq), ter(destResult));
|
||||
env.close();
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq)));
|
||||
// Funds go back to the owner, not the destination.
|
||||
env.require(balance(alice, preAlice + XRP(1000)));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 0);
|
||||
BEAST_EXPECT(ownerDirEmpty(env, alice));
|
||||
BEAST_EXPECT(ownerDirEmpty(env, bob));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq)));
|
||||
// Rejected outright: nothing moves.
|
||||
env.require(balance(alice, preAlice));
|
||||
BEAST_EXPECT(env.ownerCount(alice) == 1);
|
||||
BEAST_EXPECT(!ownerDirEmpty(env, bob));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Destination Cancel: after FinishAfter");
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(5000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// Finishable, and with no CancelAfter, otherwise never cancellable.
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 1s));
|
||||
env.close(5s);
|
||||
|
||||
env(cancel(alice, alice, seq), ter(tecNO_PERMISSION));
|
||||
env(cancel(bob, alice, seq), ter(destResult));
|
||||
env.close();
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq)));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq)));
|
||||
env(finish(bob, alice, seq));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq)));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Destination Cancel: conditional");
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(5000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// The destination does not need the fulfillment to decline.
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow(alice, bob, XRP(1000)),
|
||||
condition(cb1),
|
||||
cancel_time(env.now() + 200s));
|
||||
env.close();
|
||||
|
||||
env(cancel(bob, alice, seq), ter(destResult));
|
||||
env.close();
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq)));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq)));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Destination Cancel: self-escrow stays locked");
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(5000), alice);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow(alice, alice, XRP(1000)),
|
||||
finish_time(env.now() + 100s),
|
||||
cancel_time(env.now() + 200s));
|
||||
env.close();
|
||||
|
||||
env(cancel(alice, alice, seq), ter(tecNO_PERMISSION));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq)));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Destination Cancel: EscrowID");
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(5000), alice, bob);
|
||||
env.close();
|
||||
|
||||
uint256 const escrowId{keylet::escrow(alice, env.seq(alice)).key};
|
||||
env(escrow(alice, bob, XRP(1000)),
|
||||
finish_time(env.now() + 100s),
|
||||
cancel_time(env.now() + 200s));
|
||||
env.close();
|
||||
|
||||
env(cancel(bob, alice), escrow_id(escrowId), ter(destResult));
|
||||
env.close();
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(keylet::unchecked(escrowId)));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(env.le(keylet::unchecked(escrowId)));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Destination Cancel: owner hook cannot block");
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(5000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow(alice, bob, XRP(1000)),
|
||||
finish_time(env.now() + 1s),
|
||||
cancel_time(env.now() + 2s));
|
||||
env.close();
|
||||
env.close();
|
||||
|
||||
// Past CancelAfter, so only the owner's hook stands in the way.
|
||||
env(jtx::hook(alice, {{hso(jtx::genesis::RollbackHook)}}, 0),
|
||||
fee(XRP(2)));
|
||||
env.close();
|
||||
|
||||
// The owner stays a strong tsh when a third party cancels.
|
||||
env(cancel(carol, alice, seq), fee(XRP(1)), ter(tecHOOK_REJECTED));
|
||||
env.close();
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
env(cancel(bob, alice, seq), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq)));
|
||||
}
|
||||
else
|
||||
{
|
||||
env(cancel(bob, alice, seq),
|
||||
fee(XRP(1)),
|
||||
ter(tecHOOK_REJECTED));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq)));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Destination Cancel: IOU");
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(5000), alice, bob, gw);
|
||||
env.close();
|
||||
env.trust(USD(10000), alice, bob);
|
||||
env.close();
|
||||
env(pay(gw, alice, USD(5000)));
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow(alice, bob, USD(1000)),
|
||||
finish_time(env.now() + 100s),
|
||||
cancel_time(env.now() + 200s));
|
||||
env.close();
|
||||
BEAST_EXPECT(-lockedAmount(env, alice, gw, USD) == USD(1000));
|
||||
|
||||
env(cancel(bob, alice, seq), ter(destResult));
|
||||
env.close();
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq)));
|
||||
BEAST_EXPECT(-lockedAmount(env, alice, gw, USD) == USD(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq)));
|
||||
BEAST_EXPECT(-lockedAmount(env, alice, gw, USD) == USD(1000));
|
||||
}
|
||||
env.require(balance(alice, USD(5000)));
|
||||
env.require(balance(bob, USD(0)));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Destination Cancel: issuer-owned IOU");
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(5000), alice, gw);
|
||||
env.close();
|
||||
env.trust(USD(10000), alice);
|
||||
env.close();
|
||||
|
||||
// An issuer escrowing its own IOU locks nothing, so the cancel
|
||||
// only has to unwind the escrow itself.
|
||||
auto const seq = env.seq(gw);
|
||||
env(escrow(gw, alice, USD(1000)),
|
||||
finish_time(env.now() + 100s),
|
||||
cancel_time(env.now() + 200s));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.ownerCount(gw) == 1);
|
||||
|
||||
env(cancel(gw, gw, seq), ter(tecNO_PERMISSION));
|
||||
env(cancel(alice, gw, seq), ter(destResult));
|
||||
env.close();
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(gw.id(), seq)));
|
||||
BEAST_EXPECT(env.ownerCount(gw) == 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(env.le(keylet::escrow(gw.id(), seq)));
|
||||
BEAST_EXPECT(env.ownerCount(gw) == 1);
|
||||
}
|
||||
env.require(balance(alice, USD(0)));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Destination Cancel: owner hook chain not charged");
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(5000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
auto const seq = env.seq(alice);
|
||||
env(escrow(alice, bob, XRP(1000)),
|
||||
finish_time(env.now() + 1s),
|
||||
cancel_time(env.now() + 2s));
|
||||
env.close();
|
||||
env.close();
|
||||
|
||||
auto const feeFor = [&](Account const& acct) {
|
||||
return calculateBaseFee(
|
||||
*env.current(), *env.jt(cancel(acct, alice, seq)).stx);
|
||||
};
|
||||
|
||||
XRPAmount const bobBefore = feeFor(bob);
|
||||
XRPAmount const carolBefore = feeFor(carol);
|
||||
|
||||
env(jtx::hook(alice, {{hso(jtx::genesis::RollbackHook)}}, 0),
|
||||
fee(XRP(2)));
|
||||
env.close();
|
||||
|
||||
// A third party still pays for the owner's (strong) hook chain.
|
||||
BEAST_EXPECT(feeFor(carol) > carolBefore);
|
||||
|
||||
// The destination does too, but only without the amendment.
|
||||
if (enabled)
|
||||
{
|
||||
BEAST_EXPECT(feeFor(bob) == bobBefore);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(feeFor(bob) > bobBefore);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// fix1571 is still a live (DefaultYes) amendment on Xahau, so the
|
||||
// legacy CancelAfter check in EscrowCancel remains reachable.
|
||||
testcase("Destination Cancel: without fix1571");
|
||||
Env env{*this, features - fix1571};
|
||||
env.fund(XRP(5000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
// No CancelAfter.
|
||||
auto const seq1 = env.seq(alice);
|
||||
env(escrow(alice, bob, XRP(1000)), finish_time(env.now() + 100s));
|
||||
|
||||
// CancelAfter in the future.
|
||||
auto const seq2 = env.seq(alice);
|
||||
env(escrow(alice, bob, XRP(1000)),
|
||||
finish_time(env.now() + 100s),
|
||||
cancel_time(env.now() + 200s));
|
||||
|
||||
// CancelAfter about to pass.
|
||||
auto const seq3 = env.seq(alice);
|
||||
env(escrow(alice, bob, XRP(1000)),
|
||||
finish_time(env.now() + 1s),
|
||||
cancel_time(env.now() + 2s));
|
||||
env.close();
|
||||
|
||||
env(cancel(alice, alice, seq1), ter(tecNO_PERMISSION));
|
||||
env(cancel(carol, alice, seq2), ter(tecNO_PERMISSION));
|
||||
env(cancel(bob, alice, seq1), ter(destResult));
|
||||
env(cancel(bob, alice, seq2), ter(destResult));
|
||||
env.close();
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq1)));
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq2)));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq1)));
|
||||
BEAST_EXPECT(env.le(keylet::escrow(alice.id(), seq2)));
|
||||
}
|
||||
|
||||
// Past CancelAfter anyone may cancel, with or without the
|
||||
// amendment.
|
||||
env(cancel(carol, alice, seq3));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::escrow(alice.id(), seq3)));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
@@ -4770,12 +5123,17 @@ public:
|
||||
{
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{supported_amendments() | featureCredentials};
|
||||
testWithFeats(all - featurePaychanAndEscrowForTokens);
|
||||
testWithFeats(all);
|
||||
testIOUWithFeats(all - featureClawback);
|
||||
testIOUWithFeats(all);
|
||||
// The pre-existing suites assert that the destination cannot cancel
|
||||
// early, so they run without featureEscrowDestinationCancel.
|
||||
FeatureBitset const legacy{all - featureEscrowDestinationCancel};
|
||||
testWithFeats(legacy - featurePaychanAndEscrowForTokens);
|
||||
testWithFeats(legacy);
|
||||
testIOUWithFeats(legacy - featureClawback);
|
||||
testIOUWithFeats(legacy);
|
||||
testEscrowID(all);
|
||||
testCredentials(all);
|
||||
testDestinationCancel(legacy);
|
||||
testDestinationCancel(all);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+252
-335
@@ -732,6 +732,56 @@ public:
|
||||
else
|
||||
BEAST_EXPECT(result.value() == baseFee + memoSize);
|
||||
}
|
||||
{
|
||||
// fix20260929: 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(fix20260929))
|
||||
BEAST_EXPECT(result.value() == baseline.value() + 12345);
|
||||
else
|
||||
BEAST_EXPECT(result.value() == baseline.value());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -2172,22 +2222,19 @@ public:
|
||||
BEAST_EXPECT(api.hook_param(Bytes(33, 1)).error() == TOO_BIG);
|
||||
}
|
||||
|
||||
// {
|
||||
// // Hook params map
|
||||
// Bytes const name{'k'};
|
||||
// Bytes const value{1, 2, 3};
|
||||
// StubHookContext stubCtx{
|
||||
// .result = {
|
||||
// .hookParams = {{{name, value}}},
|
||||
// }};
|
||||
// auto hookCtx =
|
||||
// makeStubHookContext(applyCtx, alice.id(), alice.id(),
|
||||
// stubCtx);
|
||||
// hook::HookAPI api(hookCtx);
|
||||
// auto const result = api.hook_param(name);
|
||||
// BEAST_EXPECT(result.has_value());
|
||||
// BEAST_EXPECT(result.value() == value);
|
||||
// }
|
||||
{
|
||||
// Hook params map
|
||||
Bytes const name{'k'};
|
||||
Bytes const value{1, 2, 3};
|
||||
StubHookContext stubCtx{};
|
||||
stubCtx.result.hookParams = std::map<Bytes, Bytes>{{name, value}};
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
|
||||
auto& api = hookCtx.api();
|
||||
auto const result = api.hook_param(name);
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(result.value() == value);
|
||||
}
|
||||
|
||||
{
|
||||
// Override deletion wins
|
||||
@@ -3941,310 +3988,142 @@ public:
|
||||
|
||||
auto const _source_object =
|
||||
*strUnHex("81140000000000000000000000000000000000000000");
|
||||
|
||||
std::vector<std::pair<uint32_t, std::string>> data_list = {
|
||||
{// UINT16 Version
|
||||
sfVersion.getCode(),
|
||||
"10100001"},
|
||||
{// UINT32 Sequence
|
||||
sfSequence.getCode(),
|
||||
"2400000001"},
|
||||
// UINT64
|
||||
{// Uint64 ExchangeRate
|
||||
sfExchangeRate.getCode(),
|
||||
"360000000000000001"},
|
||||
{// UINT256 EmailHash
|
||||
sfEmailHash.getCode(),
|
||||
"4100000000000000000000000000000000"},
|
||||
{// UINT256 ObjectID
|
||||
sfObjectID.getCode(),
|
||||
"5E000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0"},
|
||||
{// Amount NativeAmount
|
||||
sfAmount.getCode(),
|
||||
"61999999999999999999999999999999999999999999999999999999999999999"
|
||||
"999999999999999999999999999999999"},
|
||||
{// Amount IOUAmount
|
||||
sfAmount.getCode(),
|
||||
"614999999999999999"},
|
||||
{// Object AmountEntry
|
||||
sfAmountEntry.getCode(),
|
||||
"E05B614000000000000064E1"},
|
||||
{// Array Memos
|
||||
sfMemos.getCode(),
|
||||
"F9EA7D04DEADBEEFE1F1"},
|
||||
{// UINT8 TickSize
|
||||
sfTickSize.getCode(),
|
||||
"00101001"},
|
||||
{// UINT160 TakerPaysCurrency
|
||||
sfTakerPaysCurrency.getCode(),
|
||||
"01110000000000000000000000005553440000000000"},
|
||||
{// PathSet Paths
|
||||
sfPaths.getCode(),
|
||||
"0112300000000000000000000000005553440000000000054F6F784A58F9EFB0A"
|
||||
"9EB90B83464F9D166461900"},
|
||||
{// Vector256 Amendments
|
||||
sfAmendments.getCode(),
|
||||
"03132042426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF"
|
||||
"044EE"},
|
||||
{// Issue ClaimCurrency
|
||||
sfClaimCurrency.getCode(),
|
||||
"05180000000000000000000000005553440000000000054F6F784A58F9EFB0A9E"
|
||||
"B90B83464F9D1664619"},
|
||||
{// XChainBridge Native-Native
|
||||
sfXChainBridge.getCode(),
|
||||
"011914AE123A8556F3CF91154711376AFB0F894F832B3D000000000000"
|
||||
"000000000000000000000000000014AE123A8556F3CF91154711376AFB"
|
||||
"0F894F832B3D0000000000000000000000000000000000000000"},
|
||||
{// Currency BaseAsset
|
||||
sfBaseAsset.getCode(),
|
||||
"011A0000000000000000000000005553440000000000"},
|
||||
};
|
||||
if (env.closed()->rules().enabled(fix20260929))
|
||||
{
|
||||
// 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(fix20260929);
|
||||
for (auto const& data : data_list)
|
||||
{
|
||||
// use UINT16
|
||||
auto source_object = _source_object;
|
||||
auto field_object = *strUnHex("10100001");
|
||||
// Version: 1
|
||||
auto field_object = *strUnHex(data.second);
|
||||
auto field_id = data.first;
|
||||
|
||||
auto const result = api.sto_emplace(
|
||||
source_object, field_object, sfVersion.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(
|
||||
result.value().size() ==
|
||||
source_object.size() + field_object.size());
|
||||
field_object.insert(
|
||||
field_object.end(), source_object.begin(), source_object.end());
|
||||
BEAST_EXPECT(result.value() == field_object);
|
||||
// test invalid insert: trailing byte after the field
|
||||
auto ft = field_object;
|
||||
ft.push_back(0);
|
||||
auto const result3 = api.sto_emplace(source_object, ft, field_id);
|
||||
// legacy parser ignores trailing bytes in the field
|
||||
if (fixEnabled)
|
||||
BEAST_EXPECTS(!result3.has_value(), data.second);
|
||||
else
|
||||
BEAST_EXPECTS(result3.has_value(), data.second);
|
||||
|
||||
auto const erase_result = api.sto_emplace(
|
||||
result.value(), std::nullopt, sfVersion.getCode());
|
||||
BEAST_EXPECT(erase_result.has_value());
|
||||
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
|
||||
BEAST_EXPECT(erase_result.value() == _source_object);
|
||||
// truncated field / source are only exercised after the fix;
|
||||
// the legacy parser may read past the end of these buffers
|
||||
if (!fixEnabled)
|
||||
continue;
|
||||
|
||||
auto const fo = Blob{field_object.begin(), field_object.end() - 1};
|
||||
auto const result1 = api.sto_emplace(source_object, fo, field_id);
|
||||
BEAST_EXPECTS(!result1.has_value(), data.second);
|
||||
|
||||
auto const so =
|
||||
Blob{source_object.begin(), source_object.end() - 1};
|
||||
auto const result2 = api.sto_emplace(so, field_object, field_id);
|
||||
BEAST_EXPECTS(!result2.has_value(), data.second);
|
||||
}
|
||||
|
||||
for (auto const& data : data_list)
|
||||
{
|
||||
// use UINT32
|
||||
auto source_object = _source_object;
|
||||
auto field_object = *strUnHex("2400000001");
|
||||
// Sequence: 1
|
||||
|
||||
auto const result = api.sto_emplace(
|
||||
source_object, field_object, sfSequence.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(
|
||||
result.value().size() ==
|
||||
source_object.size() + field_object.size());
|
||||
field_object.insert(
|
||||
field_object.end(), source_object.begin(), source_object.end());
|
||||
BEAST_EXPECT(result.value() == field_object);
|
||||
|
||||
auto const erase_result = api.sto_emplace(
|
||||
result.value(), std::nullopt, sfSequence.getCode());
|
||||
BEAST_EXPECT(erase_result.has_value());
|
||||
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
|
||||
BEAST_EXPECT(erase_result.value() == _source_object);
|
||||
}
|
||||
|
||||
{
|
||||
// use UINT64
|
||||
auto source_object = _source_object;
|
||||
auto field_object = *strUnHex("360000000000000001");
|
||||
// ExchangeRate: 1
|
||||
|
||||
auto const result = api.sto_emplace(
|
||||
source_object, field_object, sfExchangeRate.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(
|
||||
result.value().size() ==
|
||||
source_object.size() + field_object.size());
|
||||
field_object.insert(
|
||||
field_object.end(), source_object.begin(), source_object.end());
|
||||
BEAST_EXPECT(result.value() == field_object);
|
||||
|
||||
auto const erase_result = api.sto_emplace(
|
||||
result.value(), std::nullopt, sfExchangeRate.getCode());
|
||||
BEAST_EXPECT(erase_result.has_value());
|
||||
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
|
||||
BEAST_EXPECT(erase_result.value() == _source_object);
|
||||
}
|
||||
|
||||
{
|
||||
// use UINT128
|
||||
auto source_object = _source_object;
|
||||
auto field_object = *strUnHex("4100000000000000000000000000000000");
|
||||
// EmailHash: 1
|
||||
|
||||
auto const result = api.sto_emplace(
|
||||
source_object, field_object, sfEmailHash.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(
|
||||
result.value().size() ==
|
||||
source_object.size() + field_object.size());
|
||||
field_object.insert(
|
||||
field_object.end(), source_object.begin(), source_object.end());
|
||||
BEAST_EXPECT(result.value() == field_object);
|
||||
|
||||
auto const erase_result = api.sto_emplace(
|
||||
result.value(), std::nullopt, sfEmailHash.getCode());
|
||||
BEAST_EXPECT(erase_result.has_value());
|
||||
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
|
||||
BEAST_EXPECT(erase_result.value() == _source_object);
|
||||
}
|
||||
|
||||
{
|
||||
// use UINT256
|
||||
auto source_object = _source_object;
|
||||
auto field_object = *strUnHex(
|
||||
"5E000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000");
|
||||
// ObjectID:
|
||||
// "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
auto const result = api.sto_emplace(
|
||||
source_object, field_object, sfObjectID.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(
|
||||
result.value().size() ==
|
||||
source_object.size() + field_object.size());
|
||||
field_object.insert(
|
||||
field_object.end(), source_object.begin(), source_object.end());
|
||||
BEAST_EXPECT(result.value() == field_object);
|
||||
|
||||
auto const erase_result = api.sto_emplace(
|
||||
result.value(), std::nullopt, sfObjectID.getCode());
|
||||
BEAST_EXPECT(erase_result.has_value());
|
||||
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
|
||||
BEAST_EXPECT(erase_result.value() == _source_object);
|
||||
}
|
||||
|
||||
{
|
||||
// use AMOUNT
|
||||
auto source_object = _source_object;
|
||||
|
||||
auto nativeamount = *strUnHex(
|
||||
"61999999999999999999999999999999999999999999999999999999999999"
|
||||
"999999999999999999999999999999999999");
|
||||
auto iouamount = *strUnHex("614999999999999999");
|
||||
|
||||
for (auto field_object : {nativeamount, iouamount})
|
||||
{
|
||||
auto const result = api.sto_emplace(
|
||||
source_object, field_object, sfAmount.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(
|
||||
result.value().size() ==
|
||||
source_object.size() + field_object.size());
|
||||
field_object.insert(
|
||||
field_object.end(),
|
||||
source_object.begin(),
|
||||
source_object.end());
|
||||
BEAST_EXPECT(result.value() == field_object);
|
||||
|
||||
auto const erase_result = api.sto_emplace(
|
||||
result.value(), std::nullopt, sfAmount.getCode());
|
||||
BEAST_EXPECT(erase_result.has_value());
|
||||
BEAST_EXPECT(
|
||||
erase_result.value().size() == _source_object.size());
|
||||
BEAST_EXPECT(erase_result.value() == _source_object);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// OBJECT
|
||||
auto source_object = _source_object;
|
||||
auto field_object = *strUnHex("E05B614000000000000064E1");
|
||||
// {"AmountEntry": {"Amount": "100"}}
|
||||
|
||||
auto const result = api.sto_emplace(
|
||||
source_object, field_object, sfAmountEntry.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(
|
||||
result.value().size() ==
|
||||
source_object.size() + field_object.size());
|
||||
source_object.insert(
|
||||
source_object.end(), field_object.begin(), field_object.end());
|
||||
BEAST_EXPECT(result.value() == source_object);
|
||||
|
||||
auto const erase_result = api.sto_emplace(
|
||||
result.value(), std::nullopt, sfAmountEntry.getCode());
|
||||
BEAST_EXPECT(erase_result.has_value());
|
||||
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
|
||||
BEAST_EXPECT(erase_result.value() == _source_object);
|
||||
}
|
||||
|
||||
{
|
||||
// ARRAY
|
||||
auto source_object = _source_object;
|
||||
auto field_object = *strUnHex("F9EA7D04DEADBEEFE1F1");
|
||||
// {"Memos": [{"Memo":{ "MemoData": "DEADBEEF" }}]}
|
||||
auto field_object = *strUnHex(data.second);
|
||||
auto field_id = data.first;
|
||||
|
||||
auto const result =
|
||||
api.sto_emplace(source_object, field_object, sfMemos.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
api.sto_emplace(source_object, field_object, field_id);
|
||||
BEAST_EXPECTS(result.has_value(), data.second);
|
||||
auto const r = result.value();
|
||||
BEAST_EXPECT(
|
||||
result.value().size() ==
|
||||
source_object.size() + field_object.size());
|
||||
source_object.insert(
|
||||
source_object.end(), field_object.begin(), field_object.end());
|
||||
BEAST_EXPECT(result.value() == source_object);
|
||||
r.size() == source_object.size() + field_object.size());
|
||||
field_object.insert(
|
||||
field_id < sfAccount.getCode() ? field_object.end()
|
||||
: field_object.begin(),
|
||||
source_object.begin(),
|
||||
source_object.end());
|
||||
BEAST_EXPECT(r == field_object);
|
||||
|
||||
auto const erase_result = api.sto_emplace(
|
||||
result.value(), std::nullopt, sfMemos.getCode());
|
||||
BEAST_EXPECT(erase_result.has_value());
|
||||
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
|
||||
BEAST_EXPECT(erase_result.value() == _source_object);
|
||||
}
|
||||
|
||||
{
|
||||
// UINT8
|
||||
auto source_object = _source_object;
|
||||
auto field_object = *strUnHex("00101001");
|
||||
// {"TickSize": 1}
|
||||
|
||||
auto const result = api.sto_emplace(
|
||||
source_object, field_object, sfTickSize.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(
|
||||
result.value().size() ==
|
||||
source_object.size() + field_object.size());
|
||||
source_object.insert(
|
||||
source_object.end(), field_object.begin(), field_object.end());
|
||||
BEAST_EXPECT(result.value() == source_object);
|
||||
|
||||
auto const erase_result = api.sto_emplace(
|
||||
result.value(), std::nullopt, sfTickSize.getCode());
|
||||
BEAST_EXPECT(erase_result.has_value());
|
||||
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
|
||||
BEAST_EXPECT(erase_result.value() == _source_object);
|
||||
}
|
||||
|
||||
{
|
||||
// UINT160
|
||||
auto source_object = _source_object;
|
||||
auto field_object =
|
||||
*strUnHex("01110000000000000000000000005553440000000000");
|
||||
// {"TakerPaysCurrency": "0000000000000000000000005553440000000000"}
|
||||
|
||||
auto const result = api.sto_emplace(
|
||||
source_object, field_object, sfTakerPaysCurrency.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(
|
||||
result.value().size() ==
|
||||
source_object.size() + field_object.size());
|
||||
source_object.insert(
|
||||
source_object.end(), field_object.begin(), field_object.end());
|
||||
BEAST_EXPECT(result.value() == source_object);
|
||||
|
||||
auto const erase_result = api.sto_emplace(
|
||||
result.value(), std::nullopt, sfTakerPaysCurrency.getCode());
|
||||
BEAST_EXPECT(erase_result.has_value());
|
||||
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
|
||||
BEAST_EXPECT(erase_result.value() == _source_object);
|
||||
}
|
||||
|
||||
{
|
||||
// PATHSET
|
||||
auto source_object = _source_object;
|
||||
auto field_object = *strUnHex(
|
||||
"0112300000000000000000000000005553440000000000054F6F784A58F9EF"
|
||||
"B0A9EB90B83464F9D166461900");
|
||||
// {"Paths": [[{ "currency": "USD", "issuer":
|
||||
// "rVnYNK9yuxBz4uP8zC8LEFokM2nqH3poc" }]]}
|
||||
|
||||
auto const result =
|
||||
api.sto_emplace(source_object, field_object, sfPaths.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(
|
||||
result.value().size() ==
|
||||
source_object.size() + field_object.size());
|
||||
source_object.insert(
|
||||
source_object.end(), field_object.begin(), field_object.end());
|
||||
BEAST_EXPECT(result.value() == source_object);
|
||||
|
||||
auto const erase_result = api.sto_emplace(
|
||||
result.value(), std::nullopt, sfPaths.getCode());
|
||||
BEAST_EXPECT(erase_result.has_value());
|
||||
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
|
||||
BEAST_EXPECT(erase_result.value() == _source_object);
|
||||
}
|
||||
|
||||
{
|
||||
// VECTOR256
|
||||
auto source_object = _source_object;
|
||||
auto field_object = *strUnHex(
|
||||
"03132042426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97"
|
||||
"ABF044EE");
|
||||
// {"Amendments":["42426C4D4F1009EE67080A9B7965B44656D7714D104A72F9B4369F97ABF044EE"]}
|
||||
|
||||
auto const result = api.sto_emplace(
|
||||
source_object, field_object, sfAmendments.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
BEAST_EXPECT(
|
||||
result.value().size() ==
|
||||
source_object.size() + field_object.size());
|
||||
source_object.insert(
|
||||
source_object.end(), field_object.begin(), field_object.end());
|
||||
BEAST_EXPECT(result.value() == source_object);
|
||||
|
||||
auto const erase_result = api.sto_emplace(
|
||||
result.value(), std::nullopt, sfAmendments.getCode());
|
||||
BEAST_EXPECT(erase_result.has_value());
|
||||
BEAST_EXPECT(erase_result.value().size() == _source_object.size());
|
||||
BEAST_EXPECT(erase_result.value() == _source_object);
|
||||
}
|
||||
|
||||
{
|
||||
// UINT96
|
||||
}
|
||||
|
||||
{
|
||||
// UINT384
|
||||
}
|
||||
|
||||
{
|
||||
// UINT512
|
||||
// test erase
|
||||
auto const erase_result =
|
||||
api.sto_emplace(result.value(), std::nullopt, field_id);
|
||||
BEAST_EXPECTS(erase_result.has_value(), data.second);
|
||||
auto const er = erase_result.value();
|
||||
BEAST_EXPECT(er.size() == _source_object.size());
|
||||
BEAST_EXPECT(er == _source_object);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4283,6 +4162,38 @@ public:
|
||||
PARSE_ERROR);
|
||||
}
|
||||
|
||||
{
|
||||
// Invalid: wrapped array has an invalid end marker.
|
||||
// should be "F9EA7D02BEEFE1F1" {Memos:[{Memo:{MemoData:"BEEF"}}]}
|
||||
auto const invalid_end_marker = *strUnHex("F9EA7D02BEEFE100");
|
||||
auto const result = api.sto_subarray(invalid_end_marker, 0);
|
||||
if (env.closed()->rules().enabled(fix20260929))
|
||||
{
|
||||
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(fix20260929))
|
||||
{
|
||||
BEAST_EXPECT(!result.has_value());
|
||||
BEAST_EXPECT(result.error() == PARSE_ERROR);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(result.value() == std::make_pair(1u, 6u));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// doesn't found
|
||||
// { Memos: [{Memo: {MemoData: "BEEF"}}] }
|
||||
@@ -4338,6 +4249,25 @@ public:
|
||||
PARSE_ERROR);
|
||||
}
|
||||
|
||||
{
|
||||
// Invalid: the requested field is valid but a later field is
|
||||
// truncated.
|
||||
auto const data = *strUnHex(
|
||||
"240000000181140000000000000000000000000000000000000000");
|
||||
auto const invalid_tail = Blob{data.begin(), data.end() - 1};
|
||||
auto const result =
|
||||
api.sto_subfield(invalid_tail, sfSequence.getCode());
|
||||
if (env.closed()->rules().enabled(fix20260929))
|
||||
{
|
||||
BEAST_EXPECT(!result.has_value());
|
||||
BEAST_EXPECT(result.error() == PARSE_ERROR);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(result.value() == std::make_pair(1u, 4u));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// doesn't found
|
||||
// { Memo: {MemoData: "BEEF"} }
|
||||
@@ -4397,34 +4327,17 @@ public:
|
||||
// { AmountEntry: {Amount: "100"} }
|
||||
auto const amountEntry = *strUnHex("E05B614000000000000064E1");
|
||||
|
||||
BEAST_EXPECT(api.sto_validate(memos).value() == true);
|
||||
BEAST_EXPECT(api.sto_validate(amounts).value() == true);
|
||||
BEAST_EXPECT(api.sto_validate(memo).value() == true);
|
||||
BEAST_EXPECT(api.sto_validate(amountEntry).value() == true);
|
||||
for (auto const& data : {memos, amounts, memo, amountEntry})
|
||||
{
|
||||
// Valid data
|
||||
BEAST_EXPECT(api.sto_validate(data).value() == true);
|
||||
|
||||
// Invalid data
|
||||
BEAST_EXPECT(
|
||||
api.sto_validate(Bytes{0xFF, 0xFF, 0xFF, 0xFF}).value() == false);
|
||||
|
||||
Bytes const i_memos(&memos[0], &memos[memos.size() - 1]);
|
||||
Bytes const i_amounts(&amounts[0], &amounts[amounts.size() - 1]);
|
||||
Bytes const i_memo(&memo[0], &memo[memo.size() - 1]);
|
||||
Bytes const i_amountEntry(
|
||||
&amountEntry[0], &amountEntry[amountEntry.size() - 1]);
|
||||
BEAST_EXPECT(api.sto_validate(i_memos).value() == false);
|
||||
BEAST_EXPECT(api.sto_validate(i_amounts).value() == false);
|
||||
BEAST_EXPECT(api.sto_validate(i_memo).value() == false);
|
||||
BEAST_EXPECT(api.sto_validate(i_amountEntry).value() == false);
|
||||
|
||||
Bytes const i2_memos(&memos[1], &memos[memos.size()]);
|
||||
Bytes const i2_amounts(&amounts[1], &amounts[amounts.size()]);
|
||||
Bytes const i2_memo(&memo[1], &memo[memo.size()]);
|
||||
Bytes const i2_amountEntry(
|
||||
&amountEntry[1], &amountEntry[amountEntry.size()]);
|
||||
BEAST_EXPECT(api.sto_validate(i_memos).value() == false);
|
||||
BEAST_EXPECT(api.sto_validate(i_amounts).value() == false);
|
||||
BEAST_EXPECT(api.sto_validate(i_memo).value() == false);
|
||||
BEAST_EXPECT(api.sto_validate(i_amountEntry).value() == false);
|
||||
// Invalid data
|
||||
auto const partial_data = Blob{data.begin(), data.end() - 1};
|
||||
BEAST_EXPECT(api.sto_validate(partial_data).value() == false);
|
||||
auto const partial_data2 = Blob{data.begin() + 1, data.end()};
|
||||
BEAST_EXPECT(api.sto_validate(partial_data2).value() == false);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -4852,6 +4765,7 @@ public:
|
||||
test_otxn_generation(features);
|
||||
test_etxn_details(features);
|
||||
test_etxn_fee_base(features - fixHookAPI20251128);
|
||||
test_etxn_fee_base(features - fix20260929);
|
||||
test_etxn_fee_base(features);
|
||||
test_etxn_nonce(features);
|
||||
test_etxn_reserve(features);
|
||||
@@ -4917,9 +4831,12 @@ public:
|
||||
test_state_set(features);
|
||||
|
||||
test_sto_emplace(features);
|
||||
test_sto_emplace(features - fix20260929);
|
||||
// test_sto_erase(features); // tested in test_sto_emplace
|
||||
test_sto_subarray(features);
|
||||
test_sto_subarray(features - fix20260929);
|
||||
test_sto_subfield(features);
|
||||
test_sto_subfield(features - fix20260929);
|
||||
test_sto_validate(features);
|
||||
|
||||
test_trace(features);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <test/app/Import_json.h>
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/AMM.h>
|
||||
#include <test/jtx/CaptureLogs.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/misc/AmendmentTable.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
@@ -29,7 +30,9 @@
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/json_writer.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/Import.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#define BEAST_REQUIRE(x) \
|
||||
@@ -194,6 +197,35 @@ class Import_test : public beast::unit_test::suite
|
||||
env(import::import(alice, tmpXpop), ter(temMALFORMED));
|
||||
}
|
||||
|
||||
auto const upperHash =
|
||||
"E000CC0736630D66DAB573A2642E1BD0646DFB13A871B2CCFA6E4226F477D88C";
|
||||
auto const makeList = [](std::string const& hash) {
|
||||
Json::Value proof{Json::arrayValue};
|
||||
for (int i = 0; i < 16; ++i)
|
||||
proof.append(hash);
|
||||
return proof;
|
||||
};
|
||||
|
||||
// XPOP.transaction.proof list should be exactly 16 entries
|
||||
{
|
||||
Json::Value proof = makeList(upperHash);
|
||||
proof.append(upperHash);
|
||||
BEAST_EXPECT(syntaxCheckProof(proof, env.journal) == false);
|
||||
}
|
||||
// XPOP.transaction.proof list entry missing or wrong format
|
||||
// (should be hex string with 64 characters)
|
||||
{
|
||||
Json::Value proof = makeList(upperHash);
|
||||
proof[Json::Value::ArrayIndex{0}] = "wrong format";
|
||||
BEAST_EXPECT(syntaxCheckProof(proof, env.journal) == false);
|
||||
}
|
||||
// XPOP.transaction.proof list entry has wrong format
|
||||
{
|
||||
Json::Value proof = makeList(upperHash);
|
||||
proof[Json::Value::ArrayIndex{0}] = 1234;
|
||||
BEAST_EXPECT(syntaxCheckProof(proof, env.journal) == false);
|
||||
}
|
||||
|
||||
// // XPOP.transaction.proof list should be exactly 16 entries
|
||||
// {
|
||||
// Json::Value tmpXpop =
|
||||
@@ -334,6 +366,58 @@ class Import_test : public beast::unit_test::suite
|
||||
tmpProof[jss::children]["3"];
|
||||
BEAST_EXPECT(syntaxCheckProof(tmpProof, env.journal) == false);
|
||||
}
|
||||
// XPOP.transaction.proof tree node has wrong format
|
||||
// invalid hash (must be hex)
|
||||
{
|
||||
Json::Value tmpProof = proof;
|
||||
tmpProof[jss::children]["3"][jss::hash] =
|
||||
"ZZ126BA0486ADAE575BBC5335E42E236275A452037CA9A876D1A8CDACA1AE5"
|
||||
"42";
|
||||
BEAST_EXPECT(syntaxCheckProof(tmpProof, env.journal) == false);
|
||||
}
|
||||
// XPOP.transaction.proof tree node has wrong format
|
||||
// invalid key (must be hex)
|
||||
{
|
||||
Json::Value tmpProof = proof;
|
||||
tmpProof[jss::children]["3"][jss::key] =
|
||||
"ZZD6F6AB7D0A827DD1502B7B9571626A2B601DBE5BC786EF8ADD00E0CB7FCE"
|
||||
"B3";
|
||||
BEAST_EXPECT(syntaxCheckProof(tmpProof, env.journal) == false);
|
||||
}
|
||||
// XPOP.transaction.proof tree node has wrong format (root)
|
||||
// invalid root hash (must be hex)
|
||||
{
|
||||
Json::Value tmpProof = proof;
|
||||
tmpProof[jss::hash] =
|
||||
"ZZ9C8D073DDB5AB07FD2CD4F14467A8F3BC8FFBA16A0032D12D823D8511C12"
|
||||
"F4";
|
||||
BEAST_EXPECT(syntaxCheckProof(tmpProof, env.journal) == false);
|
||||
}
|
||||
// XPOP.transaction.proof child node was not 0-F hex nibble
|
||||
// lower case nibble
|
||||
{
|
||||
Json::Value tmpProof = proof;
|
||||
tmpProof[jss::children]["a"] = tmpProof[jss::children]["3"];
|
||||
BEAST_EXPECT(syntaxCheckProof(tmpProof, env.journal) == false);
|
||||
}
|
||||
// success: hash and key values may be upper or lower case hex. Only
|
||||
// child branch keys are constrained to upper case 0-F.
|
||||
{
|
||||
Json::Value tmpProof = proof;
|
||||
tmpProof[jss::hash] =
|
||||
"409c8d073ddb5ab07fd2cd4f14467a8f3bc8ffba16a0032d12d823d8511c12"
|
||||
"f4";
|
||||
tmpProof[jss::key] =
|
||||
"00000000000000000000000000000000000000000000000000000000000000"
|
||||
"00";
|
||||
tmpProof[jss::children]["3"][jss::hash] =
|
||||
"aa126ba0486adae575bbc5335e42e236275a452037ca9a876d1a8cdaca1ae5"
|
||||
"42";
|
||||
tmpProof[jss::children]["3"][jss::key] =
|
||||
"39d6f6ab7d0a827dd1502b7b9571626a2b601dbe5bc786ef8add00e0cb7fce"
|
||||
"b3";
|
||||
BEAST_EXPECT(syntaxCheckProof(tmpProof, env.journal) == true);
|
||||
}
|
||||
// success
|
||||
{
|
||||
BEAST_EXPECT(syntaxCheckProof(proof, env.journal) == true);
|
||||
@@ -1484,6 +1568,117 @@ class Import_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the XPOP with its blob/meta pair optionally tampered with
|
||||
// (tamperMeta bumps the metadata's TransactionIndex, so the pair no longer
|
||||
// hashes to any leaf in the proof) and, if plant is set, that pair's leaf
|
||||
// hash written over the declared hash of the proof's nibble 0 child. In
|
||||
// the w_seed fixture that child is an inner node, whose declared hash the
|
||||
// txroot computation ignores.
|
||||
static Json::Value
|
||||
spoofInnerNode(Json::Value xpop, bool tamperMeta, bool plant)
|
||||
{
|
||||
auto& txn = xpop[jss::transaction];
|
||||
auto const rawTx = *strUnHex(txn[jss::blob].asString());
|
||||
auto rawMeta = *strUnHex(txn[jss::meta].asString());
|
||||
|
||||
if (tamperMeta)
|
||||
{
|
||||
STObject meta(
|
||||
SerialIter(rawMeta.data(), rawMeta.size()), sfMetadata);
|
||||
meta.setFieldU32(
|
||||
sfTransactionIndex, meta.getFieldU32(sfTransactionIndex) + 1);
|
||||
Serializer ms;
|
||||
meta.add(ms);
|
||||
rawMeta = ms.peekData();
|
||||
txn[jss::meta] = strHex(rawMeta);
|
||||
}
|
||||
|
||||
if (plant)
|
||||
{
|
||||
// leaf hash exactly as Import::preflight computes it
|
||||
STTx const stx(SerialIter(rawTx.data(), rawTx.size()));
|
||||
Serializer s(rawTx.size() + rawMeta.size() + 40);
|
||||
s.addVL(rawTx);
|
||||
s.addVL(rawMeta);
|
||||
s.addBitString(stx.getTransactionID());
|
||||
txn[jss::proof][jss::children]["0"][jss::hash] =
|
||||
strHex(sha512Half(HashPrefix::txNode, s.slice()));
|
||||
}
|
||||
|
||||
return xpop;
|
||||
}
|
||||
|
||||
void
|
||||
testProofInnerNodeSpoof(FeatureBitset features)
|
||||
{
|
||||
testcase("import proof inner node spoof");
|
||||
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
|
||||
// the spoof relies on nibble 0 being an inner node in this fixture
|
||||
auto const fixture = import::loadXpop(ImportTCAccountSet::w_seed);
|
||||
auto const& nibble0 =
|
||||
fixture[jss::transaction][jss::proof][jss::children]["0"];
|
||||
BEAST_EXPECT(nibble0[jss::children].size() != 0);
|
||||
|
||||
std::string const notContained =
|
||||
"xpop proof did not contain the specified txn hash";
|
||||
|
||||
// import the (possibly spoofed) w_seed XPOP into a fresh account and
|
||||
// return the captured log
|
||||
auto const runImport =
|
||||
[&](bool tamperMeta, bool plant, TER expected) -> std::string {
|
||||
std::string logs;
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkVLConfig(21337, keys),
|
||||
features,
|
||||
std::make_unique<CaptureLogs>(&logs),
|
||||
beast::severities::kWarning};
|
||||
|
||||
// burn 10'000 xrp
|
||||
auto const master = Account("masterpassphrase");
|
||||
env(noop(master), fee(10'000'000'000), ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
auto const alice = Account("alice");
|
||||
env.memoize(alice);
|
||||
|
||||
Json::Value tx = import::import(
|
||||
alice, spoofInnerNode(fixture, tamperMeta, plant));
|
||||
tx[jss::Sequence] = 0;
|
||||
tx[jss::Fee] = 0;
|
||||
env(tx, alice, ter(expected));
|
||||
env.close();
|
||||
|
||||
auto const [acct, acctSle] =
|
||||
accountKeyAndSle(*env.current(), alice);
|
||||
BEAST_EXPECT((acctSle != nullptr) == isTesSuccess(expected));
|
||||
}
|
||||
return logs;
|
||||
};
|
||||
|
||||
// control: overwriting the inner node's declared hash alone leaves the
|
||||
// computed txroot unchanged, so the genuine pair still imports
|
||||
BEAST_EXPECT(
|
||||
runImport(false, true, tesSUCCESS).find(notContained) ==
|
||||
std::string::npos);
|
||||
|
||||
// a tampered blob/meta pair is not in the proof
|
||||
BEAST_EXPECT(
|
||||
runImport(true, false, temMALFORMED).find(notContained) !=
|
||||
std::string::npos);
|
||||
|
||||
// spoof: the tampered pair's hash planted on the inner node. The
|
||||
// txroot still verifies (see control), so the containment check is
|
||||
// the only thing that can reject it
|
||||
BEAST_EXPECT(
|
||||
runImport(true, true, temMALFORMED).find(notContained) !=
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
void
|
||||
testGetVLInfo(FeatureBitset features)
|
||||
{
|
||||
@@ -1700,6 +1895,12 @@ class Import_test : public beast::unit_test::suite
|
||||
env.fund(XRP(1000), alice, bob, carol, dave);
|
||||
env.close();
|
||||
|
||||
auto const importWithUnlBlob = [&](std::string const& json) {
|
||||
Json::Value tmpXpop = import::loadXpop(ImportTCAccountSet::w_seed);
|
||||
tmpXpop[jss::validation][jss::unl][jss::blob] = base64_encode(json);
|
||||
env(import::import(alice, tmpXpop), ter(temMALFORMED));
|
||||
};
|
||||
|
||||
// temMALFORMED
|
||||
// Issuer cannot be the source account.
|
||||
{
|
||||
@@ -1713,7 +1914,7 @@ class Import_test : public beast::unit_test::suite
|
||||
{
|
||||
Json::Value tx = import::import(
|
||||
alice, import::loadXpop(ImportTCAccountSet::w_seed));
|
||||
STAmount const& fee = XRP(0);
|
||||
STAmount const fee = XRP(0);
|
||||
tx[jss::Fee] = fee.getJson(JsonOptions::none);
|
||||
env(tx, ter(telINSUF_FEE_P));
|
||||
}
|
||||
@@ -1732,7 +1933,7 @@ class Import_test : public beast::unit_test::suite
|
||||
{
|
||||
Json::Value tx = import::import(
|
||||
alice, import::loadXpop(ImportTCAccountSet::w_seed));
|
||||
STAmount const& amount = XRP(-1);
|
||||
STAmount const amount = XRP(-1);
|
||||
tx[jss::Amount] = amount.getJson(JsonOptions::none);
|
||||
env(tx, ter(temMALFORMED));
|
||||
}
|
||||
@@ -2076,6 +2277,12 @@ class Import_test : public beast::unit_test::suite
|
||||
"WFo3VFFFPSJ9XX0=";
|
||||
env(import::import(alice, tmpXpop), ter(temMALFORMED));
|
||||
}
|
||||
// temMALFORMED - Import: unl blob json (after base64 decoding) wrong
|
||||
// required field (sequence) and/or types: negative int
|
||||
{
|
||||
importWithUnlBlob(
|
||||
R"({"sequence":-1,"expiration":741398400,"validators":[]})");
|
||||
}
|
||||
// temMALFORMED - Import: unl blob json (after base64 decoding)
|
||||
// lacked required field (expiration) and/or types
|
||||
{
|
||||
@@ -2127,6 +2334,12 @@ class Import_test : public beast::unit_test::suite
|
||||
"WFo3VFFFPSJ9XX0=";
|
||||
env(import::import(alice, tmpXpop), ter(temMALFORMED));
|
||||
}
|
||||
// temMALFORMED - Import: unl blob json (after base64 decoding) wrong
|
||||
// required field (expiration) and/or types: negative int
|
||||
{
|
||||
importWithUnlBlob(
|
||||
R"({"sequence":2,"expiration":-1,"validators":[]})");
|
||||
}
|
||||
// temMALFORMED - Import: unl blob json (after base64 decoding)
|
||||
// lacked required field (effective) and/or types
|
||||
{
|
||||
@@ -2153,6 +2366,13 @@ class Import_test : public beast::unit_test::suite
|
||||
"WGJuS1dqVmhxTmFHbTU3N0s2SjY5N1haN1RRRT0ifV19";
|
||||
env(import::import(alice, tmpXpop), ter(temMALFORMED));
|
||||
}
|
||||
// temMALFORMED - Import: unl blob json (after base64 decoding) wrong
|
||||
// required field (effective) and/or types: negative int
|
||||
{
|
||||
importWithUnlBlob(
|
||||
R"({"sequence":2,"effective":-1,"expiration":741398400,)"
|
||||
R"("validators":[]})");
|
||||
}
|
||||
// temMALFORMED - Import: unl blob json (after base64 decoding)
|
||||
// lacked required field (validators) and/or types
|
||||
{
|
||||
@@ -2201,7 +2421,10 @@ class Import_test : public beast::unit_test::suite
|
||||
env(import::import(alice, tmpXpop), ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// temMALFORMED - Import: unl blob expired
|
||||
// temMALFORMED - unl blob expired. The window is now checked in
|
||||
// preclaim (see testVLWindowClock). This blob's validator list
|
||||
// does not match the xpop's validations, so preflight rejects it
|
||||
// first.
|
||||
{
|
||||
Json::Value tmpXpop = import::loadXpop(ImportTCAccountSet::w_seed);
|
||||
tmpXpop[jss::validation][jss::unl][jss::blob] =
|
||||
@@ -2231,7 +2454,10 @@ class Import_test : public beast::unit_test::suite
|
||||
env(import::import(alice, tmpXpop), ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// temMALFORMED - Import: unl blob not yet valid
|
||||
// temMALFORMED - unl blob not yet valid. The window is now checked in
|
||||
// preclaim (see testVLWindowClock). This blob's validator list
|
||||
// does not match the xpop's validations, so preflight rejects it
|
||||
// first.
|
||||
{
|
||||
Json::Value tmpXpop = import::loadXpop(ImportTCAccountSet::w_seed);
|
||||
tmpXpop[jss::validation][jss::unl][jss::blob] =
|
||||
@@ -6257,6 +6483,162 @@ class Import_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testGetVLWindow(FeatureBitset features)
|
||||
{
|
||||
testcase("import utils - getVLWindow");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
beast::Journal const j{beast::Journal::getNullSink()};
|
||||
|
||||
auto const at = [](std::uint32_t s) {
|
||||
return NetClock::time_point{NetClock::duration{s}};
|
||||
};
|
||||
auto const withBlob = [](std::string const& json) {
|
||||
Json::Value xpop;
|
||||
xpop[jss::validation][jss::unl][jss::blob] = base64_encode(json);
|
||||
return xpop;
|
||||
};
|
||||
|
||||
// effective absent: valid from the epoch
|
||||
{
|
||||
auto const window =
|
||||
getVLWindow(import::loadXpop(ImportTCAccountSet::w_seed), j);
|
||||
BEAST_REQUIRE(window);
|
||||
BEAST_EXPECT(window->first == at(0));
|
||||
BEAST_EXPECT(window->second == at(757349624));
|
||||
}
|
||||
|
||||
// effective present
|
||||
{
|
||||
auto const window = getVLWindow(
|
||||
withBlob(R"({"sequence":1,"effective":3600,)"
|
||||
R"("expiration":86400,"validators":[]})"),
|
||||
j);
|
||||
BEAST_REQUIRE(window);
|
||||
BEAST_EXPECT(window->first == at(3600));
|
||||
BEAST_EXPECT(window->second == at(86400));
|
||||
}
|
||||
|
||||
// not json, not an object, or missing / ill-typed fields
|
||||
for (std::string const bad :
|
||||
{"badJson",
|
||||
"[1,2,3]",
|
||||
R"({"sequence":1})",
|
||||
R"({"expiration":"86400"})",
|
||||
R"({"expiration":-1})",
|
||||
R"({"effective":"0","expiration":86400})",
|
||||
R"({"effective":-1,"expiration":86400})"})
|
||||
BEAST_EXPECT(!getVLWindow(withBlob(bad), j));
|
||||
}
|
||||
|
||||
void
|
||||
testVLWindowClock(FeatureBitset features)
|
||||
{
|
||||
testcase("import vl window clock");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
auto const at = [](std::uint32_t s) {
|
||||
return NetClock::time_point{NetClock::duration{s}};
|
||||
};
|
||||
|
||||
// expiration of the w_seed unl blob
|
||||
std::uint32_t const expiry = 757349624;
|
||||
auto const before = at(expiry - 1000);
|
||||
auto const after = at(expiry + 1000);
|
||||
auto const closeParentAt = [&](Env& env,
|
||||
NetClock::time_point closeTime) {
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
auto const resolution = env.closed()->info().closeTimeResolution;
|
||||
env.close(closeTime - resolution + 1s);
|
||||
};
|
||||
|
||||
auto const alice = Account("alice");
|
||||
auto const importTx = [&alice]() {
|
||||
Json::Value tx = import::import(
|
||||
alice, import::loadXpop(ImportTCAccountSet::w_seed));
|
||||
tx[jss::Sequence] = 0;
|
||||
tx[jss::Fee] = 0;
|
||||
return tx;
|
||||
};
|
||||
// Keep this test focused on the VL clock, not max-supply accounting.
|
||||
// w_seed creates an account and, without ZeroB2M, credits its 1000 XRP
|
||||
// inner fee as valid legacy burn-to-mint.
|
||||
auto const burnMintHeadroom = [&](Env& env) {
|
||||
auto const initCoins = env.current()->info().drops;
|
||||
auto headroom = Import::computeStartingBonus(*env.current());
|
||||
if (!env.current()->rules().enabled(featureZeroB2M))
|
||||
headroom += XRP(1000).value().xrp();
|
||||
auto const master = Account("masterpassphrase");
|
||||
env(noop(master),
|
||||
fee(static_cast<std::uint64_t>(headroom.drops())),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.current()->info().drops == initCoins - headroom);
|
||||
};
|
||||
|
||||
// parent ledger inside the window, node clock past it
|
||||
{
|
||||
Env env{*this, network::makeNetworkVLConfig(21337, keys), features};
|
||||
burnMintHeadroom(env);
|
||||
env.memoize(alice);
|
||||
env.close(before);
|
||||
BEAST_EXPECT(env.current()->parentCloseTime() < at(expiry));
|
||||
|
||||
env.timeKeeper().set(after);
|
||||
env(importTx(), alice, ter(tesSUCCESS));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(alice) != nullptr);
|
||||
}
|
||||
|
||||
// parent ledger past the window, node clock inside it
|
||||
{
|
||||
Env env{*this, network::makeNetworkVLConfig(21337, keys), features};
|
||||
burnMintHeadroom(env);
|
||||
env.memoize(alice);
|
||||
env.close(after);
|
||||
BEAST_EXPECT(env.current()->parentCloseTime() >= at(expiry));
|
||||
|
||||
env.timeKeeper().set(before);
|
||||
env(importTx(), alice, ter(temMALFORMED));
|
||||
BEAST_EXPECT(env.le(alice) == nullptr);
|
||||
}
|
||||
|
||||
// Env::close uses close-time resolution, so the requested expiration
|
||||
// time lands on the closest representable parent close before
|
||||
// expiration for this blob.
|
||||
{
|
||||
Env env{*this, network::makeNetworkVLConfig(21337, keys), features};
|
||||
burnMintHeadroom(env);
|
||||
env.memoize(alice);
|
||||
closeParentAt(env, at(expiry));
|
||||
BEAST_EXPECT(env.current()->parentCloseTime() < at(expiry));
|
||||
|
||||
env(importTx(), alice, ter(tesSUCCESS));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(alice) != nullptr);
|
||||
}
|
||||
|
||||
// accepted into the open ledger, which then closes after the window.
|
||||
// Env::close() sets the node clock to the close time before the
|
||||
// ledger is built, so this also covers the rebuild
|
||||
{
|
||||
Env env{*this, network::makeNetworkVLConfig(21337, keys), features};
|
||||
burnMintHeadroom(env);
|
||||
env.memoize(alice);
|
||||
env.close(before);
|
||||
env(importTx(), alice, ter(tesSUCCESS));
|
||||
BEAST_EXPECT(env.le(alice) != nullptr);
|
||||
|
||||
env.close(after);
|
||||
BEAST_EXPECT(env.closed()->info().parentCloseTime < at(expiry));
|
||||
BEAST_EXPECT(env.le(alice) != nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -6278,6 +6660,7 @@ public:
|
||||
testSyntaxCheckProofArray(features);
|
||||
testSyntaxCheckProofObject(features);
|
||||
testSyntaxCheckXPOP(features);
|
||||
testProofInnerNodeSpoof(features);
|
||||
testGetVLInfo(features);
|
||||
testEnabled(features);
|
||||
testInvalidPreflight(features);
|
||||
@@ -6297,6 +6680,8 @@ public:
|
||||
testMinMax(features);
|
||||
testHalving(features - featureOwnerPaysFee);
|
||||
testBlackhole(features);
|
||||
testGetVLWindow(features);
|
||||
testVLWindowClock(features);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -104,8 +104,8 @@ class LedgerMaster_test : public beast::unit_test::suite
|
||||
startLegSeq, txnIndex);
|
||||
BEAST_EXPECT(
|
||||
*result ==
|
||||
uint256("0CC11AD1AD89661689F6B6148D82CB6A7101DA4D66EC843670262D"
|
||||
"0B618F5745"));
|
||||
uint256("DCAECE52F028B9D0F7CA43CBE15AB4EF7B84A8EF5D455238992E1E"
|
||||
"DF2BDA1637"));
|
||||
}
|
||||
// success (second tx)
|
||||
{
|
||||
@@ -114,8 +114,8 @@ class LedgerMaster_test : public beast::unit_test::suite
|
||||
startLegSeq + 1, txnIndex);
|
||||
BEAST_EXPECT(
|
||||
*result ==
|
||||
uint256("DCAECE52F028B9D0F7CA43CBE15AB4EF7B84A8EF5D455238992E1E"
|
||||
"DF2BDA1637"));
|
||||
uint256("722BD5EC9E4AE36E724B6223F5A5887927AADFD0B18A3053AAE57F"
|
||||
"434F8C9FE8"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1111,7 +1111,7 @@ struct LedgerReplayer_test : public beast::unit_test::suite
|
||||
return false;
|
||||
|
||||
beast::IP::Address addr =
|
||||
boost::asio::ip::address::from_string("172.1.1.100");
|
||||
boost::asio::ip::make_address("172.1.1.100");
|
||||
jtx::Env serverEnv(*this);
|
||||
serverEnv.app().config().LEDGER_REPLAY = server;
|
||||
auto http_resp = ripple::makeResponse(
|
||||
|
||||
@@ -1174,11 +1174,11 @@ class MPToken_test : public beast::unit_test::suite
|
||||
JsonOptions::none)[sfAffectedNodes.fieldName];
|
||||
// Issuer got 10 in the transfer fees
|
||||
BEAST_EXPECT(
|
||||
meta[2u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
|
||||
meta[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
|
||||
[sfOutstandingAmount.fieldName] == "9990");
|
||||
// Destination account got 9'990
|
||||
BEAST_EXPECT(
|
||||
meta[0u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
|
||||
meta[3u][sfModifiedNode.fieldName][sfFinalFields.fieldName]
|
||||
[sfMPTAmount.fieldName] == "9990");
|
||||
// Source account spent 10'000
|
||||
BEAST_EXPECT(
|
||||
@@ -1910,8 +1910,8 @@ class MPToken_test : public beast::unit_test::suite
|
||||
env.tx()->getJson(JsonOptions::none)[jss::hash].asString()};
|
||||
BEAST_EXPECTS(
|
||||
txHash ==
|
||||
"42175D43E3D236A86B0F1B07182A1B2C78AD382372CFA09A48239C395FFFEE"
|
||||
"30",
|
||||
"61041108F0DAD50BAFD2F7B1102AC70B283EBDCED194CEBAF04D0184E4A70B"
|
||||
"02",
|
||||
txHash);
|
||||
Json::Value const meta = env.rpc("tx", txHash)[jss::result][jss::meta];
|
||||
auto const id = meta[jss::mpt_issuance_id].asString();
|
||||
@@ -1919,7 +1919,7 @@ class MPToken_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(meta.isMember(jss::mpt_issuance_id));
|
||||
BEAST_EXPECT(id == to_string(mptAlice.issuanceID()));
|
||||
BEAST_EXPECTS(
|
||||
id == "00000001AE123A8556F3CF91154711376AFB0F894F832B3D", id);
|
||||
id == "00000002AE123A8556F3CF91154711376AFB0F894F832B3D", id);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -5104,6 +5104,36 @@ public:
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
// OfferID is only valid when Hooks are enabled.
|
||||
{
|
||||
Env env{*this, features - featureHooks};
|
||||
auto const gw = Account{"gateway"};
|
||||
auto const alice = Account{"alice"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
env.fund(XRP(10000), gw, alice);
|
||||
env.close();
|
||||
|
||||
env(trust(alice, USD(1000)));
|
||||
env.close();
|
||||
|
||||
env(pay(gw, alice, USD(200)));
|
||||
env.close();
|
||||
|
||||
uint256 const offerId{getOfferIndex(alice, env.seq(alice))};
|
||||
env(offer(alice, XRP(50), USD(50)));
|
||||
env.close();
|
||||
|
||||
uint256 const newOfferId{getOfferIndex(alice, env.seq(alice))};
|
||||
env(offer(alice, XRP(50), USD(50)),
|
||||
offer_id(offerId),
|
||||
ter(temDISABLED));
|
||||
env(offer_cancel(alice), offer_id(offerId), ter(temDISABLED));
|
||||
|
||||
BEAST_EXPECT(env.le(keylet::unchecked(offerId)));
|
||||
BEAST_EXPECT(!env.le(keylet::unchecked(newOfferId)));
|
||||
}
|
||||
|
||||
// OfferCreate
|
||||
{
|
||||
Env env{*this, features};
|
||||
@@ -5481,6 +5511,71 @@ public:
|
||||
env.balance(taker, XRP) == takerXRPBalance);
|
||||
}
|
||||
|
||||
void
|
||||
testFixCancelOfferExploit(FeatureBitset features)
|
||||
{
|
||||
testcase("fixCancelOfferExploit");
|
||||
using namespace jtx;
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const charlie("charlie");
|
||||
auto const USD = alice["USD"];
|
||||
auto const bobUSD = bob["USD"];
|
||||
|
||||
Env env(*this, features);
|
||||
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// OfferCancel: not ltOffer, exist account(alice)
|
||||
uint256 const nonOfferId{keylet::account(alice.id()).key};
|
||||
env(offer_cancel(bob), offer_id(nonOfferId), ter(tecNO_PERMISSION));
|
||||
BEAST_EXPECT(env.le(keylet::unchecked(nonOfferId)));
|
||||
|
||||
// OfferCreate: not ltOffer
|
||||
env(offer(bob, XRP(100), bobUSD(100)),
|
||||
offer_id(nonOfferId),
|
||||
ter(tecNO_PERMISSION));
|
||||
BEAST_EXPECT(env.le(keylet::unchecked(nonOfferId)));
|
||||
|
||||
// OfferCancel: not ltOffer, not exist account(charlie)
|
||||
uint256 const nonExistId{keylet::account(charlie.id()).key};
|
||||
env(offer_cancel(bob), offer_id(nonExistId), ter(tesSUCCESS));
|
||||
BEAST_EXPECT(!env.le(keylet::unchecked(nonExistId)));
|
||||
|
||||
// OfferCreate: not ltOffer, not exist account(charlie)
|
||||
env(offer(bob, XRP(100), bobUSD(100)),
|
||||
offer_id(nonExistId),
|
||||
ter(tesSUCCESS));
|
||||
BEAST_EXPECT(!env.le(keylet::unchecked(nonExistId)));
|
||||
|
||||
// create alice offer
|
||||
uint256 const aliceOfferId{getOfferIndex(alice, env.seq(alice))};
|
||||
env(offer(alice, XRP(100), USD(100)));
|
||||
env.close();
|
||||
|
||||
// OfferCancel: bob cannot cancel alice's offer
|
||||
env(offer_cancel(bob), offer_id(aliceOfferId), ter(tecNO_PERMISSION));
|
||||
BEAST_EXPECT(env.le(keylet::unchecked(aliceOfferId)));
|
||||
|
||||
// OfferCreates: bob cannot cancel alice's offer
|
||||
env(offer(bob, XRP(100), bobUSD(100)),
|
||||
offer_id(aliceOfferId),
|
||||
ter(tecNO_PERMISSION));
|
||||
BEAST_EXPECT(env.le(keylet::unchecked(aliceOfferId)));
|
||||
|
||||
// An expired OfferCreate still cannot cancel another account's Offer.
|
||||
uint256 const expiredBobOfferId{getOfferIndex(bob, env.seq(bob))};
|
||||
env(offer(bob, XRP(100), bobUSD(100)),
|
||||
offer_id(aliceOfferId),
|
||||
json(sfExpiration.fieldName, lastClose(env)),
|
||||
ter(tecNO_PERMISSION));
|
||||
BEAST_EXPECT(env.le(keylet::unchecked(aliceOfferId)));
|
||||
BEAST_EXPECT(!env.le(keylet::unchecked(expiredBobOfferId)));
|
||||
|
||||
env(offer_cancel(alice), offer_id(aliceOfferId), ter(tesSUCCESS));
|
||||
}
|
||||
|
||||
void
|
||||
testAll(FeatureBitset features)
|
||||
{
|
||||
@@ -5544,6 +5639,7 @@ public:
|
||||
testRmSmallIncreasedQOffersIOU(features);
|
||||
testOfferID(features);
|
||||
testFillOrKill(features);
|
||||
testFixCancelOfferExploit(features);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -1962,7 +1962,8 @@ private:
|
||||
// | A | A | S | S | S | S | S
|
||||
// | A | D | N | N | S | S | S
|
||||
// | D | D | S | S | N/A | S | S
|
||||
// | D | A | S | S | N/A | S | S
|
||||
// | D | A | W* | W* | N/A | S | S
|
||||
// * S without featureEscrowDestinationCancel
|
||||
|
||||
static uint256
|
||||
getEscrowIndex(AccountID const& account, std::uint32_t uSequence)
|
||||
@@ -2117,7 +2118,7 @@ private:
|
||||
|
||||
// otxn: dest
|
||||
// tsh account
|
||||
// w/s: strong
|
||||
// w/s: weak (strong without featureEscrowDestinationCancel)
|
||||
for (bool const testStrong : {true, false})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
@@ -2125,6 +2126,11 @@ private:
|
||||
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
bool const withIOUIssuerWeakTSH =
|
||||
env.current()->rules().enabled(featureIOUIssuerWeakTSH);
|
||||
bool const withDestCancel =
|
||||
env.current()->rules().enabled(featureEscrowDestinationCancel);
|
||||
|
||||
auto const account = Account("alice");
|
||||
auto const dest = Account("bob");
|
||||
env.fund(XRP(1000), account, dest);
|
||||
@@ -2154,7 +2160,10 @@ private:
|
||||
env.close();
|
||||
|
||||
// verify tsh hook triggered
|
||||
testTSHStrongWeak(env, tshSTRONG, __LINE__);
|
||||
auto const expected = !withDestCancel ? tshSTRONG
|
||||
: !testStrong && withIOUIssuerWeakTSH ? tshWEAK
|
||||
: tshNONE;
|
||||
testTSHStrongWeak(env, expected, __LINE__);
|
||||
}
|
||||
|
||||
// otxn: account
|
||||
@@ -2373,7 +2382,7 @@ private:
|
||||
|
||||
// otxn: dest
|
||||
// tsh account
|
||||
// w/s: strong
|
||||
// w/s: weak (strong without featureEscrowDestinationCancel)
|
||||
for (bool const testStrong : {true, false})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
@@ -2381,6 +2390,11 @@ private:
|
||||
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
bool const withIOUIssuerWeakTSH =
|
||||
env.current()->rules().enabled(featureIOUIssuerWeakTSH);
|
||||
bool const withDestCancel =
|
||||
env.current()->rules().enabled(featureEscrowDestinationCancel);
|
||||
|
||||
auto const account = Account("alice");
|
||||
auto const dest = Account("bob");
|
||||
env.fund(XRP(1000), account, dest);
|
||||
@@ -2413,7 +2427,9 @@ private:
|
||||
env.close();
|
||||
|
||||
// verify tsh hook triggered
|
||||
auto const expected = testStrong ? tshSTRONG : tshSTRONG;
|
||||
auto const expected = !withDestCancel ? tshSTRONG
|
||||
: !testStrong && withIOUIssuerWeakTSH ? tshWEAK
|
||||
: tshNONE;
|
||||
testTSHStrongWeak(env, expected, __LINE__);
|
||||
}
|
||||
|
||||
@@ -4421,9 +4437,11 @@ private:
|
||||
env,
|
||||
issuer,
|
||||
0,
|
||||
mintFlags | (strongIssuerTSH ? tfStrongTSH : 0));
|
||||
mintFlags | (strongIssuerTSH ? std::uint32_t(tfStrongTSH) : 0));
|
||||
env(token::mint(issuer),
|
||||
txflags(mintFlags | (strongIssuerTSH ? tfStrongTSH : 0)));
|
||||
txflags(
|
||||
mintFlags |
|
||||
(strongIssuerTSH ? std::uint32_t(tfStrongTSH) : 0)));
|
||||
env.close();
|
||||
|
||||
auto const offerIndex =
|
||||
@@ -8279,16 +8297,16 @@ private:
|
||||
|
||||
// validate the emitted txn ids
|
||||
std::vector<std::string> const txIds = {
|
||||
"9610F73CDD6590EB6B3C82E5EC55D4B4C80CD7128B98AA556F7EC9DD96AE7056",
|
||||
"2F4582A29272390C0C25A80D4A3BCE5A14ACE6D86D8D0CB2C57719EB6FA881AE",
|
||||
"89A301CFEF0DD781AB9032A6A2DCE0937BC0119D2CDD06033B8B2FD80968E519",
|
||||
"DD8721B59024E168480B4DF8F8E93778601F0BD2E77FC991F3DA1182F5AD8B1E",
|
||||
"5D735C2EE3CB8289F8E11621FDC9565F9D6D67F3AE59D65332EACE591D67945F",
|
||||
"F02470E01731C968881AF4CBDEC90BB9E1F7AB0BE1CC22AF15451FB6D191096D",
|
||||
"8AD65E541DECD49B1693F8C17DFD8A2B906F49C673C4FD2034FF772E2BE50C30",
|
||||
"9F225229059CCC6257814D03C107884CF588C1C246A89ADFC16E50DF671B834C",
|
||||
"13C2A54A14BADF3648CED05175E1CCAD713F7E5EA56D9735CF8813CD5551F281",
|
||||
"87C60F41A96554587CED289F83F52DEE3CF670EEB189B067E6066B9A06056ADF",
|
||||
"2FBEF981BAC322225D13C274D0118FECC1B93A89EDBC0AB3CBD8B30A1591024E",
|
||||
"3C0A12F6A322486ABDA4180DC2B424BD252FD2FA6A33553B0B5D1526C020C475",
|
||||
"AFFD55D757136FC0BE665F16E0D8CFBDC0D37CF85B5D3329BE1DBD3A44BB281C",
|
||||
"BBE43C01068CE511DBF24F84A928E481451FF448AF2493E1A9BE099628D1F523",
|
||||
"45970753F7B52448C23D66E5E19DBBC65F5BE6055C04AC3BA9896D7D67891DB1",
|
||||
"97F4E09B2AEF4D798D378B1390B0624A68F304247B358334167E93217D502A16",
|
||||
"770D3B786C8E3BEF560D9589B02557B0BDFC1C7FD15C6CF42E86068A6A411EFC",
|
||||
"239DFA86EE8391418170D7FDF5AA8B8EEE0BBEF5E9F928BC29C23EEF6F1858AD",
|
||||
"9FAA93AC6340C87B5C722FF0E8D4D3F39B9A0CA5E9799EC4CE316BFD5E379540",
|
||||
"8672B2D8AA5E9EF86929B8662FE436541B2F739052732020B874BC304601C4DB",
|
||||
};
|
||||
Json::Value params;
|
||||
params[jss::transaction] =
|
||||
@@ -8422,7 +8440,10 @@ private:
|
||||
testcase("set manifest TSH");
|
||||
|
||||
if (!features[featureOnChainManifests])
|
||||
{
|
||||
pass();
|
||||
return;
|
||||
}
|
||||
|
||||
// otxn: master
|
||||
// tsh: ephemeral
|
||||
@@ -8514,8 +8535,9 @@ public:
|
||||
using namespace test::jtx;
|
||||
static FeatureBitset const all{supported_amendments()};
|
||||
|
||||
static std::array<FeatureBitset, 2> const feats{
|
||||
static std::array<FeatureBitset, 3> const feats{
|
||||
all,
|
||||
all - featureEscrowDestinationCancel,
|
||||
all - featureIOUIssuerWeakTSH,
|
||||
};
|
||||
|
||||
@@ -8545,10 +8567,12 @@ public:
|
||||
} \
|
||||
};
|
||||
|
||||
SETHOOKTSH_TEST(1, true)
|
||||
SETHOOKTSH_TEST(1, false)
|
||||
SETHOOKTSH_TEST(2, 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);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
+553
-84
@@ -75,6 +75,13 @@ using JSSMap =
|
||||
[[maybe_unused]] std::string const x##_hash_str = to_string(x##_hash); \
|
||||
[[maybe_unused]] Keylet const x##_keylet = keylet::hookDefinition(x##_hash);
|
||||
|
||||
// Class-scope variant: gcc 11 ignores [[maybe_unused]] on data members
|
||||
#define HASH_WASM_MEMBER(x) \
|
||||
uint256 const x##_hash = \
|
||||
ripple::sha512Half_s(ripple::Slice(x##_wasm.data(), x##_wasm.size())); \
|
||||
std::string const x##_hash_str = to_string(x##_hash); \
|
||||
Keylet const x##_keylet = keylet::hookDefinition(x##_hash);
|
||||
|
||||
#define EXPECT_HOOK_FEE(x, fee) \
|
||||
do \
|
||||
{ \
|
||||
@@ -1355,11 +1362,16 @@ public:
|
||||
auto const baseFee = drops[jss::base_fee_no_hooks];
|
||||
BEAST_EXPECT(baseFee == to_string(feeDrops));
|
||||
auto const openLedgerFee = drops[jss::open_ledger_fee];
|
||||
BEAST_EXPECT(openLedgerFee == expected);
|
||||
BEAST_EXPECTS(
|
||||
openLedgerFee == expected,
|
||||
"openLedgerFee: " + openLedgerFee.asString() +
|
||||
", expected: " + expected);
|
||||
|
||||
// verify hooks fee
|
||||
auto const hooksFee = jrr[jss::result][jss::fee_hooks_feeunits];
|
||||
BEAST_EXPECT(hooksFee == expected);
|
||||
BEAST_EXPECTS(
|
||||
hooksFee == expected,
|
||||
"hooksFee: " + hooksFee.asString() + ", expected: " + expected);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1369,7 +1381,8 @@ public:
|
||||
using namespace jtx;
|
||||
Env env{*this, features};
|
||||
|
||||
bool const hookOnV2 = env.current()->rules().enabled(featureHookOnV2);
|
||||
bool const hookOnV2 = env.current()->rules().enabled(featureHookOnV2) ||
|
||||
env.current()->rules().enabled(featureHookOnV2_1);
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
@@ -1532,12 +1545,10 @@ public:
|
||||
jv.removeMember(jss::HookOn);
|
||||
jv[jss::HookOnIncoming] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"
|
||||
"00";
|
||||
"000000";
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"
|
||||
"01";
|
||||
"000001";
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Execution: Install"),
|
||||
HSFEE);
|
||||
@@ -1602,12 +1613,10 @@ public:
|
||||
jv.removeMember(jss::HookOn);
|
||||
jv[jss::HookOnIncoming] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bfff"
|
||||
"ff"; // Invoke high
|
||||
"bfffff"; // Invoke high
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
"bfff"
|
||||
"fe"; // Payment high
|
||||
"bffffe"; // Payment high
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Execution: Install"),
|
||||
HSFEE);
|
||||
@@ -1618,8 +1627,7 @@ public:
|
||||
jv[jss::Flags] = hsfOVERRIDE;
|
||||
jv[jss::HookOn] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"0000"
|
||||
"00";
|
||||
"000000";
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
M("Execution: Install"),
|
||||
HSFEE);
|
||||
@@ -1653,18 +1661,285 @@ public:
|
||||
deleteHook(alice);
|
||||
}
|
||||
|
||||
for (auto const& withFix : {false, true})
|
||||
{
|
||||
// test featureHookOnV2_1 preflight
|
||||
auto f = features - featureHookOnV2 - featureHookOnV2_1;
|
||||
if (withFix)
|
||||
f = f | featureHookOnV2_1;
|
||||
else
|
||||
f = f | featureHookOnV2;
|
||||
Env env{*this, f};
|
||||
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
TER const expected =
|
||||
env.current()->rules().enabled(featureHookOnV2_1)
|
||||
? TER(temMALFORMED)
|
||||
: TER(tesSUCCESS);
|
||||
|
||||
// 0. only one direction HookOn is not allowed
|
||||
for (auto const& key : {jss::HookOnIncoming, jss::HookOnOutgoing})
|
||||
{
|
||||
auto noopJv = Json::Value{};
|
||||
noopJv[key] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bfffff";
|
||||
|
||||
env(ripple::test::jtx::hook(alice, {{noopJv}}, 0),
|
||||
M("Lone direction HookOn NOOP"),
|
||||
HSFEE,
|
||||
ter(expected));
|
||||
env.close();
|
||||
|
||||
if (!withFix)
|
||||
BEAST_EXPECT(!env.le(keylet::hook(alice)));
|
||||
}
|
||||
|
||||
// 1. Create Hook with HookOnIncoming/Outgoing to alice
|
||||
auto jv = hso(accept_wasm);
|
||||
jv.removeMember(jss::HookOn);
|
||||
jv[jss::HookOnIncoming] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bfffff"; // Invoke high
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
"bffffe"; // Payment high
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
|
||||
env.close();
|
||||
|
||||
// 2. Install Hook with HookOn, Incoming/Outgoing to bob
|
||||
jv = Json::Value{};
|
||||
jv[jss::HookHash] = accept_hash_str;
|
||||
jv[jss::Flags] = hsfOVERRIDE;
|
||||
jv[jss::HookOn] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000000";
|
||||
jv[jss::HookOnIncoming] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bfffff"; // Invoke high
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
"bffffe"; // Payment high
|
||||
env(ripple::test::jtx::hook(bob, {{jv}}, 0), HSFEE, ter(expected));
|
||||
env.close();
|
||||
|
||||
// 3. Update Hook with HookOn, Incoming/Outgoing to alice
|
||||
jv = Json::Value{};
|
||||
jv[jss::HookOn] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000000";
|
||||
jv[jss::HookOnIncoming] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bfffff"; // Invoke high
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
"bffffe"; // Payment high
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0),
|
||||
HSFEE,
|
||||
ter(expected));
|
||||
env.close();
|
||||
}
|
||||
|
||||
for (auto const& withFix : {false, true})
|
||||
{
|
||||
// test featureHookOnV2_1 install/update
|
||||
auto f = features - featureHookOnV2 - featureHookOnV2_1;
|
||||
if (withFix)
|
||||
f = f | featureHookOnV2_1;
|
||||
else
|
||||
f = f | featureHookOnV2;
|
||||
|
||||
{
|
||||
// install:: HookDefinition: HookOn -> Hook:Incoming/Outgoing
|
||||
// update:: Hook: HookOn -> Hook:Incoming/Outgoing
|
||||
Env env{*this, f};
|
||||
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// Create Hook with HookOn to alice
|
||||
auto jv = hso(accept_wasm);
|
||||
jv.removeMember(jss::HookOn);
|
||||
jv[jss::HookOn] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000000";
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
|
||||
env.close();
|
||||
|
||||
// Install Hook with Incoming/Outgoing to bob
|
||||
jv = Json::Value{};
|
||||
jv[jss::HookHash] = accept_hash_str;
|
||||
jv[jss::Flags] = hsfOVERRIDE;
|
||||
jv[jss::HookOnIncoming] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bfffff"; // Invoke high
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
"bffffe"; // Payment high
|
||||
env(ripple::test::jtx::hook(bob, {{jv}}, 0), HSFEE);
|
||||
env.close();
|
||||
{
|
||||
auto const hooksObj = env.le(keylet::hook(bob));
|
||||
BEAST_EXPECT(hooksObj && hooksObj->isFieldPresent(sfHooks));
|
||||
auto const& hooks = hooksObj->getFieldArray(sfHooks);
|
||||
BEAST_EXPECT(hooks.size() == 1);
|
||||
auto const& h = hooks[0];
|
||||
BEAST_EXPECT(!h.isFieldPresent(sfHookOn));
|
||||
BEAST_EXPECT(h.isFieldPresent(sfHookOnOutgoing));
|
||||
BEAST_EXPECT(h.isFieldPresent(sfHookOnIncoming));
|
||||
}
|
||||
|
||||
// Update Hook with HookOn, Incoming/Outgoing to alice
|
||||
jv = Json::Value{};
|
||||
jv[jss::HookOnIncoming] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bfffff"; // Invoke high
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
"bffffe"; // Payment high
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
|
||||
env.close();
|
||||
{
|
||||
auto const hooksObj = env.le(keylet::hook(alice));
|
||||
BEAST_EXPECT(hooksObj && hooksObj->isFieldPresent(sfHooks));
|
||||
auto const& hooks = hooksObj->getFieldArray(sfHooks);
|
||||
BEAST_EXPECT(hooks.size() == 1);
|
||||
auto const& h = hooks[0];
|
||||
BEAST_EXPECT(!h.isFieldPresent(sfHookOn));
|
||||
BEAST_EXPECT(h.isFieldPresent(sfHookOnOutgoing));
|
||||
BEAST_EXPECT(h.isFieldPresent(sfHookOnIncoming));
|
||||
}
|
||||
|
||||
// Re-update Hook with HookOn, Incoming/Outgoing to alice
|
||||
jv = Json::Value{};
|
||||
jv[jss::HookOn] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bfffff";
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
|
||||
env.close();
|
||||
{
|
||||
auto const hooksObj = env.le(keylet::hook(alice));
|
||||
BEAST_EXPECT(hooksObj && hooksObj->isFieldPresent(sfHooks));
|
||||
auto const& hooks = hooksObj->getFieldArray(sfHooks);
|
||||
BEAST_EXPECT(hooks.size() == 1);
|
||||
auto const& h = hooks[0];
|
||||
BEAST_EXPECT(h.isFieldPresent(sfHookOn));
|
||||
if (withFix)
|
||||
{
|
||||
BEAST_EXPECT(!h.isFieldPresent(sfHookOnOutgoing));
|
||||
BEAST_EXPECT(!h.isFieldPresent(sfHookOnIncoming));
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(h.isFieldPresent(sfHookOnOutgoing));
|
||||
BEAST_EXPECT(h.isFieldPresent(sfHookOnIncoming));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// install:: HookDefinition: Incoming/Outgoing -> Hook:HookOn
|
||||
// update:: Hook: Incoming/Outgoing -> Hook:HookOn
|
||||
Env env{*this, f};
|
||||
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// Create Hook with HookOn to alice
|
||||
auto jv = hso(accept_wasm);
|
||||
jv.removeMember(jss::HookOn);
|
||||
jv[jss::HookOnIncoming] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bfffff"; // Invoke high
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
"bffffe"; // Payment high
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
|
||||
env.close();
|
||||
|
||||
// Install Hook with Incoming/Outgoing to bob
|
||||
jv = Json::Value{};
|
||||
jv[jss::HookHash] = accept_hash_str;
|
||||
jv[jss::Flags] = hsfOVERRIDE;
|
||||
jv[jss::HookOn] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000000";
|
||||
env(ripple::test::jtx::hook(bob, {{jv}}, 0), HSFEE);
|
||||
env.close();
|
||||
{
|
||||
auto const hooksObj = env.le(keylet::hook(bob));
|
||||
BEAST_EXPECT(hooksObj && hooksObj->isFieldPresent(sfHooks));
|
||||
auto const& hooks = hooksObj->getFieldArray(sfHooks);
|
||||
BEAST_EXPECT(hooks.size() == 1);
|
||||
auto const& h = hooks[0];
|
||||
BEAST_EXPECT(h.isFieldPresent(sfHookOn));
|
||||
BEAST_EXPECT(!h.isFieldPresent(sfHookOnOutgoing));
|
||||
BEAST_EXPECT(!h.isFieldPresent(sfHookOnIncoming));
|
||||
}
|
||||
|
||||
// Update Hook with HookOn, Incoming/Outgoing to alice
|
||||
jv = Json::Value{};
|
||||
jv[jss::HookOn] =
|
||||
"0000000000000000000000000000000000000000000000000000000000"
|
||||
"000000";
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
|
||||
env.close();
|
||||
{
|
||||
auto const hooksObj = env.le(keylet::hook(alice));
|
||||
BEAST_EXPECT(hooksObj && hooksObj->isFieldPresent(sfHooks));
|
||||
auto const& hooks = hooksObj->getFieldArray(sfHooks);
|
||||
BEAST_EXPECT(hooks.size() == 1);
|
||||
auto const& h = hooks[0];
|
||||
if (withFix)
|
||||
BEAST_EXPECT(h.isFieldPresent(sfHookOn));
|
||||
// Cause different results between gcc and clang due to
|
||||
// undefined behavior so we don't test it
|
||||
// else
|
||||
// BEAST_EXPECT(!h.isFieldPresent(sfHookOn));
|
||||
BEAST_EXPECT(!h.isFieldPresent(sfHookOnOutgoing));
|
||||
BEAST_EXPECT(!h.isFieldPresent(sfHookOnIncoming));
|
||||
}
|
||||
|
||||
// Re-update Hook with HookOn, Incoming/Outgoing to alice
|
||||
jv = Json::Value{};
|
||||
jv[jss::HookOnIncoming] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bffff0"; // Invoke high
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
"bffff1"; // Payment high
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
|
||||
env.close();
|
||||
{
|
||||
auto const hooksObj = env.le(keylet::hook(alice));
|
||||
BEAST_EXPECT(hooksObj && hooksObj->isFieldPresent(sfHooks));
|
||||
auto const& hooks = hooksObj->getFieldArray(sfHooks);
|
||||
BEAST_EXPECT(hooks.size() == 1);
|
||||
auto const& h = hooks[0];
|
||||
if (withFix)
|
||||
BEAST_EXPECT(!h.isFieldPresent(sfHookOn));
|
||||
// Cause different results between gcc and clang due to
|
||||
// undefined behavior so we don't test it
|
||||
// else
|
||||
// BEAST_EXPECT(h.isFieldPresent(sfHookOn));
|
||||
BEAST_EXPECT(h.isFieldPresent(sfHookOnOutgoing));
|
||||
BEAST_EXPECT(h.isFieldPresent(sfHookOnIncoming));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fee RPC
|
||||
{
|
||||
auto jv = hso(accept_wasm);
|
||||
jv.removeMember(jss::HookOn);
|
||||
jv[jss::HookOnIncoming] =
|
||||
"fffffffffffffffffffffffffffffffffffffff7ffffffffffffffffff"
|
||||
"bfff"
|
||||
"ff"; // Invoke high
|
||||
"bfffff"; // Invoke high
|
||||
jv[jss::HookOnOutgoing] =
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
"bfff"
|
||||
"fe"; // Payment high
|
||||
"bffffe"; // Payment high
|
||||
env(ripple::test::jtx::hook(alice, {{jv}}, 0), HSFEE);
|
||||
env.close();
|
||||
|
||||
@@ -2000,11 +2275,64 @@ public:
|
||||
BEAST_EXPECT(!env.meta()->isFieldPresent(sfHookEmissions));
|
||||
}
|
||||
|
||||
// Call named hook with the wrong hook name
|
||||
// Call named hook with the wrong hook name (size == 0)
|
||||
for (auto const fix : {true, false})
|
||||
{
|
||||
auto f = features - fix20260929;
|
||||
if (fix)
|
||||
f = f | fix20260929;
|
||||
Env env{*this, f};
|
||||
|
||||
env.fund(XRP(10000), alice);
|
||||
// execute both named and non-named hooks
|
||||
|
||||
auto jv = invoke::invoke(alice);
|
||||
jv[jss::HookName] = "";
|
||||
|
||||
auto const expected = fix ? ter(temMALFORMED) : ter(tesSUCCESS);
|
||||
env(jv,
|
||||
M("Call named hook with the wrong hook name (size == 0)"),
|
||||
HSFEE,
|
||||
ter(expected));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Call a named hook with a HookName containing a noncharacter
|
||||
// (U+FFFF followed by 'A'). Well-formed UTF-8, but always rejected.
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10000), alice);
|
||||
|
||||
auto jv = invoke::invoke(alice);
|
||||
jv[jss::HookName] = "EFBFBF41";
|
||||
env(jv,
|
||||
M("Call named hook with a noncharacter in the hook name"),
|
||||
HSFEE,
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Call a named hook with a HookName ending in a truncated sequence.
|
||||
{
|
||||
Env env{*this, features};
|
||||
env.fund(XRP(10000), alice);
|
||||
|
||||
auto jv = invoke::invoke(alice);
|
||||
jv[jss::HookName] = "414243E0";
|
||||
env(jv,
|
||||
M("Call named hook with a truncated hook name"),
|
||||
HSFEE,
|
||||
ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
|
||||
// Call named hook with the wrong hook name (size > 0)
|
||||
{
|
||||
auto jv = invoke::invoke(alice);
|
||||
jv[jss::HookName] = "41424345";
|
||||
env(jv, M("Call named hook with the wrong hook name"), HSFEE);
|
||||
env(jv,
|
||||
M("Call named hook with the wrong hook name (size > 0)"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
// execute only non-named hook
|
||||
BEAST_EXPECT(!env.meta()->isFieldPresent(sfHookEmissions));
|
||||
@@ -3003,25 +3331,37 @@ public:
|
||||
testInferHookSetOperation()
|
||||
{
|
||||
testcase("Test operation inference");
|
||||
using namespace jtx;
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
Env env{*this};
|
||||
env.fund(XRP(10000), alice);
|
||||
env(noop(alice));
|
||||
|
||||
SetHookCtx shCtx{
|
||||
.j = env.app().journal("SetHook"),
|
||||
.tx = *(env.tx()),
|
||||
.app = env.app(),
|
||||
.rules = env.current()->rules()};
|
||||
|
||||
// hsoNOOP
|
||||
{
|
||||
STObject hso{sfHook};
|
||||
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoNOOP);
|
||||
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoNOOP);
|
||||
}
|
||||
|
||||
// hsoCREATE
|
||||
{
|
||||
STObject hso{sfHook};
|
||||
hso.setFieldVL(sfCreateCode, {1}); // non-empty create code
|
||||
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoCREATE);
|
||||
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoCREATE);
|
||||
}
|
||||
|
||||
// hsoDELETE
|
||||
{
|
||||
STObject hso{sfHook};
|
||||
hso.setFieldVL(sfCreateCode, ripple::Blob{}); // empty create code
|
||||
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoDELETE);
|
||||
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoDELETE);
|
||||
}
|
||||
|
||||
// hsoINSTALL
|
||||
@@ -3029,7 +3369,7 @@ public:
|
||||
STObject hso{sfHook};
|
||||
hso.setFieldH256(
|
||||
sfHookHash, uint256{beast::zero}); // all zeros hook hash
|
||||
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoINSTALL);
|
||||
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoINSTALL);
|
||||
}
|
||||
|
||||
// hsoNSDELETE
|
||||
@@ -3038,14 +3378,14 @@ public:
|
||||
hso.setFieldH256(
|
||||
sfHookNamespace, uint256{beast::zero}); // all zeros hook hash
|
||||
hso.setFieldU32(sfFlags, hsfNSDELETE);
|
||||
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoNSDELETE);
|
||||
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoNSDELETE);
|
||||
}
|
||||
|
||||
// hsoUPDATE
|
||||
{
|
||||
STObject hso{sfHook};
|
||||
hso.setFieldH256(sfHookOn, UINT256_BIT[0]);
|
||||
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoUPDATE);
|
||||
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoUPDATE);
|
||||
}
|
||||
|
||||
// hsoINVALID
|
||||
@@ -3054,7 +3394,39 @@ public:
|
||||
hso.setFieldVL(sfCreateCode, {1}); // non-empty create code
|
||||
hso.setFieldH256(
|
||||
sfHookHash, uint256{beast::zero}); // all zeros hook hash
|
||||
BEAST_EXPECT(SetHook::inferOperation(hso) == hsoINVALID);
|
||||
BEAST_EXPECT(SetHook::inferOperation(shCtx, hso) == hsoINVALID);
|
||||
|
||||
for (auto fix : {true, false})
|
||||
{
|
||||
auto feature = supported_amendments() - featureHookOnV2 -
|
||||
featureHookOnV2_1;
|
||||
if (fix)
|
||||
feature = feature | featureHookOnV2_1;
|
||||
else
|
||||
feature = feature | featureHookOnV2;
|
||||
Env env{*this, feature};
|
||||
SetHookCtx shCtx{
|
||||
.j = env.app().journal("SetHook"),
|
||||
.tx = *env.tx(),
|
||||
.app = env.app(),
|
||||
.rules = env.current()->rules()};
|
||||
|
||||
STObject hso2{sfHook};
|
||||
hso2.setFieldH256(
|
||||
sfHookOnOutgoing,
|
||||
uint256{beast::zero}); // all zeros hook on outgoing
|
||||
BEAST_EXPECT(
|
||||
SetHook::inferOperation(shCtx, hso2) ==
|
||||
(fix ? hsoINVALID : hsoNOOP));
|
||||
|
||||
STObject hso3{sfHook};
|
||||
hso3.setFieldH256(
|
||||
sfHookOnIncoming,
|
||||
uint256{beast::zero}); // all zeros hook on incoming
|
||||
BEAST_EXPECT(
|
||||
SetHook::inferOperation(shCtx, hso3) ==
|
||||
(fix ? hsoINVALID : hsoNOOP));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3126,6 +3498,79 @@ public:
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
test_exit_out_of_bounds(FeatureBitset features)
|
||||
{
|
||||
testcase("Test accept()/rollback() with out of bounds reason");
|
||||
using namespace jtx;
|
||||
|
||||
// reason string pointer outside of wasm memory, then fall through
|
||||
TestHook accept_oob_wasm = wasm[R"[test.hook](
|
||||
#include <stdint.h>
|
||||
extern int32_t _g (uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
int64_t hook(uint32_t reserved )
|
||||
{
|
||||
_g(1,1);
|
||||
accept(0xFFFFFF00U, 32, 42);
|
||||
return 0;
|
||||
}
|
||||
)[test.hook]"];
|
||||
|
||||
TestHook rollback_oob_wasm = wasm[R"[test.hook](
|
||||
#include <stdint.h>
|
||||
extern int32_t _g (uint32_t id, uint32_t maxiter);
|
||||
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
int64_t hook(uint32_t reserved )
|
||||
{
|
||||
_g(1,1);
|
||||
rollback(0xFFFFFF00U, 32, 42);
|
||||
return 0;
|
||||
}
|
||||
)[test.hook]"];
|
||||
|
||||
for (bool const withFix : {true, false})
|
||||
{
|
||||
for (auto const& hook_wasm : {accept_oob_wasm, rollback_oob_wasm})
|
||||
{
|
||||
Env env{*this, withFix ? features : features - fix20260929};
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.fund(XRP(10000), bob);
|
||||
|
||||
env(ripple::test::jtx::hook(alice, {{hso(hook_wasm)}}, 0),
|
||||
M("Install OOB exit hook"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
|
||||
env(pay(bob, alice, XRP(1)),
|
||||
M("Test OOB exit hook"),
|
||||
fee(XRP(1)),
|
||||
ter(tecHOOK_REJECTED));
|
||||
env.close();
|
||||
|
||||
auto const meta = env.meta();
|
||||
BEAST_REQUIRE(meta && meta->isFieldPresent(sfHookExecutions));
|
||||
auto const& execs = meta->getFieldArray(sfHookExecutions);
|
||||
BEAST_REQUIRE(execs.size() == 1);
|
||||
|
||||
// with the fix: ROLLBACK with OUT_OF_BOUNDS (-1)
|
||||
// without: UNSET (fixXahauV3) with default -1
|
||||
BEAST_EXPECT(
|
||||
execs[0].getFieldU8(sfHookResult) ==
|
||||
static_cast<uint8_t>(
|
||||
withFix ? hook_api::ExitType::ROLLBACK
|
||||
: hook_api::ExitType::UNSET));
|
||||
BEAST_EXPECT(
|
||||
execs[0].getFieldU64(sfHookReturnCode) ==
|
||||
0x8000000000000001ULL);
|
||||
BEAST_EXPECT(execs[0].getFieldVL(sfHookReturnString).empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testGuards(FeatureBitset features)
|
||||
{
|
||||
@@ -4277,10 +4722,10 @@ public:
|
||||
|
||||
uint8_t expected1[49] = {
|
||||
0xEDU, 0x20U, 0x2EU, 0x00U, 0x00U, 0x00U, 0x01U, 0x3DU, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU, 0xB8U, 0x05U, 0xD6U,
|
||||
0xC3U, 0x52U, 0xDFU, 0x7AU, 0x27U, 0x76U, 0x6DU, 0xC0U, 0x20U, 0x47U,
|
||||
0xB7U, 0x64U, 0x22U, 0x5AU, 0xB7U, 0x5DU, 0xF3U, 0xFAU, 0x0DU, 0xE3U,
|
||||
0xBDU, 0xC6U, 0x40U, 0xBAU, 0xD0U, 0x0AU, 0x66U, 0xEBU, 0x68U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU, 0xBCU, 0x09U, 0x95U,
|
||||
0xFDU, 0x03U, 0x0FU, 0xF1U, 0x85U, 0xF4U, 0xECU, 0xACU, 0x94U, 0xDBU,
|
||||
0x02U, 0x19U, 0x26U, 0xFEU, 0x58U, 0xD4U, 0x8DU, 0x48U, 0x21U, 0xCCU,
|
||||
0x82U, 0x2EU, 0x4AU, 0x6AU, 0xC9U, 0xC8U, 0xC6U, 0x7CU, 0x88U
|
||||
};
|
||||
// 0x5CU
|
||||
// EmitNonce 32bytes
|
||||
@@ -12055,57 +12500,76 @@ public:
|
||||
)[test.hook]"];
|
||||
HASH_WASM(hook);
|
||||
|
||||
for (auto isfixHookAPI20251128 : {true, false})
|
||||
for (bool const with20251128 : {true, false})
|
||||
{
|
||||
Env env{
|
||||
*this,
|
||||
isfixHookAPI20251128 ? features | fixHookAPI20251128
|
||||
: features - fixHookAPI20251128};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// install the hook on alice
|
||||
env(ripple::test::jtx::hook(
|
||||
alice, {{hso(hook_wasm, overrideFlag)}}, 0),
|
||||
M("set sto_subarray"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
EXPECT_HOOK_FEE(hook, 19);
|
||||
|
||||
// invoke the hook
|
||||
env(pay(bob, alice, XRP(1)),
|
||||
M("test sto_subarray"),
|
||||
fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto const meta = env.meta();
|
||||
BEAST_REQUIRE(meta);
|
||||
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
|
||||
auto const hookExecutions =
|
||||
meta->getFieldArray(sfHookExecutions);
|
||||
BEAST_REQUIRE(hookExecutions.size() == 1);
|
||||
auto const hookExecution = hookExecutions[0];
|
||||
BEAST_REQUIRE(hookExecution.isFieldPresent(sfHookReturnCode));
|
||||
auto const returnCode =
|
||||
hookExecution.getFieldU64(sfHookReturnCode);
|
||||
if (isfixHookAPI20251128)
|
||||
for (bool const withSType : {true, false})
|
||||
{
|
||||
auto feats = features - fixHookAPI20251128 - fix20260929;
|
||||
if (with20251128)
|
||||
feats = feats | fixHookAPI20251128;
|
||||
if (withSType)
|
||||
feats = feats | fix20260929;
|
||||
|
||||
Env env{*this, feats};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// install the hook on alice
|
||||
env(ripple::test::jtx::hook(
|
||||
alice, {{hso(hook_wasm, overrideFlag)}}, 0),
|
||||
M("set sto_subarray"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
EXPECT_HOOK_FEE(hook, 19);
|
||||
|
||||
// invoke the hook
|
||||
env(pay(bob, alice, XRP(1)),
|
||||
M("test sto_subarray"),
|
||||
fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto const meta = env.meta();
|
||||
BEAST_REQUIRE(meta);
|
||||
BEAST_REQUIRE(meta->isFieldPresent(sfHookExecutions));
|
||||
auto const hookExecutions =
|
||||
meta->getFieldArray(sfHookExecutions);
|
||||
BEAST_REQUIRE(hookExecutions.size() == 1);
|
||||
auto const hookExecution = hookExecutions[0];
|
||||
BEAST_REQUIRE(
|
||||
hookExecution.isFieldPresent(sfHookReturnCode));
|
||||
auto const returnCode =
|
||||
hookExecution.getFieldU64(sfHookReturnCode);
|
||||
auto const doesntExistError = -5;
|
||||
auto const position = 2;
|
||||
auto const length = 12;
|
||||
BEAST_REQUIRE(
|
||||
returnCode ==
|
||||
(doesntExistError + ((int64_t)position << 32) +
|
||||
length));
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const parseError = -18;
|
||||
auto const position = 1;
|
||||
auto const length = 33;
|
||||
BEAST_REQUIRE(
|
||||
returnCode ==
|
||||
(parseError + ((int64_t)position << 32) + length));
|
||||
if (with20251128)
|
||||
{
|
||||
auto const position = 2;
|
||||
auto const length = 12;
|
||||
// unwrapped correctly: 1 element at pos 2, len 12
|
||||
BEAST_REQUIRE(
|
||||
returnCode ==
|
||||
(doesntExistError + ((int64_t)position << 32) +
|
||||
length));
|
||||
}
|
||||
else if (withSType)
|
||||
{
|
||||
// legacy unwrap leaves 0x5C as a 32 byte field header,
|
||||
// which is rejected as out of bounds.
|
||||
// negative codes are stored as sign bit + magnitude
|
||||
BEAST_REQUIRE(
|
||||
returnCode ==
|
||||
(0x8000'0000'0000'0000ULL |
|
||||
-(parseError + parseError)));
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const position = 1;
|
||||
auto const length = 33;
|
||||
// legacy: 32 byte field overruns the 13 byte buffer
|
||||
BEAST_REQUIRE(
|
||||
returnCode ==
|
||||
(parseError + ((int64_t)position << 32) + length));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15075,7 +15539,8 @@ public:
|
||||
testNSDeletePartial(features);
|
||||
testPageCap(features);
|
||||
|
||||
testHookOnV2(features);
|
||||
testHookOnV2((features | featureHookOnV2) - featureHookOnV2_1);
|
||||
testHookOnV2((features | featureHookOnV2_1) - featureHookOnV2);
|
||||
testHookName(features);
|
||||
|
||||
testFillCopy(features);
|
||||
@@ -15083,6 +15548,7 @@ public:
|
||||
testWasm(features);
|
||||
test_accept(features);
|
||||
test_rollback(features);
|
||||
test_exit_out_of_bounds(features);
|
||||
|
||||
testGuards(features);
|
||||
|
||||
@@ -15202,7 +15668,10 @@ public:
|
||||
run(0);
|
||||
}
|
||||
|
||||
private:
|
||||
// protected rather than private: some of the HASH_WASM_MEMBER names below
|
||||
// are unused, and clang -Wunused-private-field only fires for private
|
||||
// members.
|
||||
protected:
|
||||
TestHook accept_wasm = // WASM: 0
|
||||
wasm[
|
||||
R"[test.hook](
|
||||
@@ -15216,7 +15685,7 @@ private:
|
||||
}
|
||||
)[test.hook]"];
|
||||
|
||||
HASH_WASM(accept);
|
||||
HASH_WASM_MEMBER(accept);
|
||||
|
||||
TestHook rollback_wasm = // WASM: 1
|
||||
wasm[
|
||||
@@ -15232,7 +15701,7 @@ private:
|
||||
}
|
||||
)[test.hook]"];
|
||||
|
||||
HASH_WASM(rollback);
|
||||
HASH_WASM_MEMBER(rollback);
|
||||
|
||||
TestHook noguard_wasm = // WASM: 2
|
||||
wasm[
|
||||
@@ -15321,7 +15790,7 @@ private:
|
||||
}
|
||||
)[test.hook]"];
|
||||
|
||||
HASH_WASM(makestate);
|
||||
HASH_WASM_MEMBER(makestate);
|
||||
|
||||
// this is just used as a second small hook with a unique hash
|
||||
TestHook accept2_wasm = // WASM: 6
|
||||
@@ -15337,7 +15806,7 @@ private:
|
||||
}
|
||||
)[test.hook]"];
|
||||
|
||||
HASH_WASM(accept2);
|
||||
HASH_WASM_MEMBER(accept2);
|
||||
|
||||
// This hook is used to test Callback
|
||||
TestHook emit_invoke_wasm = // WASM: 7
|
||||
@@ -15429,7 +15898,7 @@ private:
|
||||
}
|
||||
)[test.hook]"];
|
||||
|
||||
HASH_WASM(emit_invoke);
|
||||
HASH_WASM_MEMBER(emit_invoke);
|
||||
};
|
||||
|
||||
#define SETHOOK_TEST(i, last) \
|
||||
|
||||
@@ -189,6 +189,64 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
|
||||
0x3DU, 0x20U, 0x54U, 0x4FU, 0x4FU, 0x5FU, 0x42U, 0x49U, 0x47U, 0x00U,
|
||||
}},
|
||||
|
||||
{R"[test.hook](
|
||||
#include <stdint.h>
|
||||
extern int32_t _g (uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
int64_t hook(uint32_t reserved )
|
||||
{
|
||||
_g(1,1);
|
||||
accept(0xFFFFFF00U, 32, 42);
|
||||
return 0;
|
||||
}
|
||||
)[test.hook]",
|
||||
{
|
||||
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x13U,
|
||||
0x03U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x03U, 0x7FU,
|
||||
0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U,
|
||||
0x17U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U,
|
||||
0x00U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U, 0x63U, 0x63U, 0x65U,
|
||||
0x70U, 0x74U, 0x00U, 0x01U, 0x03U, 0x02U, 0x01U, 0x02U, 0x05U, 0x03U,
|
||||
0x01U, 0x00U, 0x02U, 0x06U, 0x21U, 0x05U, 0x7FU, 0x01U, 0x41U, 0x80U,
|
||||
0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU,
|
||||
0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x88U,
|
||||
0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x07U, 0x08U,
|
||||
0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x02U, 0x0AU, 0xA1U,
|
||||
0x80U, 0x00U, 0x01U, 0x9DU, 0x80U, 0x00U, 0x00U, 0x41U, 0x01U, 0x41U,
|
||||
0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x41U, 0x80U,
|
||||
0x7EU, 0x41U, 0x20U, 0x42U, 0x2AU, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U,
|
||||
0x00U, 0x1AU, 0x42U, 0x00U, 0x0BU,
|
||||
}},
|
||||
|
||||
{R"[test.hook](
|
||||
#include <stdint.h>
|
||||
extern int32_t _g (uint32_t id, uint32_t maxiter);
|
||||
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
int64_t hook(uint32_t reserved )
|
||||
{
|
||||
_g(1,1);
|
||||
rollback(0xFFFFFF00U, 32, 42);
|
||||
return 0;
|
||||
}
|
||||
)[test.hook]",
|
||||
{
|
||||
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x13U,
|
||||
0x03U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x03U, 0x7FU,
|
||||
0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U,
|
||||
0x19U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U,
|
||||
0x00U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU,
|
||||
0x62U, 0x61U, 0x63U, 0x6BU, 0x00U, 0x01U, 0x03U, 0x02U, 0x01U, 0x02U,
|
||||
0x05U, 0x03U, 0x01U, 0x00U, 0x02U, 0x06U, 0x21U, 0x05U, 0x7FU, 0x01U,
|
||||
0x41U, 0x80U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U,
|
||||
0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U,
|
||||
0x80U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU,
|
||||
0x07U, 0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x02U,
|
||||
0x0AU, 0xA1U, 0x80U, 0x00U, 0x01U, 0x9DU, 0x80U, 0x00U, 0x00U, 0x41U,
|
||||
0x01U, 0x41U, 0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU,
|
||||
0x41U, 0x80U, 0x7EU, 0x41U, 0x20U, 0x42U, 0x2AU, 0x10U, 0x81U, 0x80U,
|
||||
0x80U, 0x80U, 0x00U, 0x1AU, 0x42U, 0x00U, 0x0BU,
|
||||
}},
|
||||
|
||||
{R"[test.hook](
|
||||
(module
|
||||
(type (;0;) (func (param i32 i32) (result i64)))
|
||||
@@ -1613,10 +1671,10 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
|
||||
|
||||
uint8_t expected1[49] = {
|
||||
0xEDU, 0x20U, 0x2EU, 0x00U, 0x00U, 0x00U, 0x01U, 0x3DU, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU, 0xB8U, 0x05U, 0xD6U,
|
||||
0xC3U, 0x52U, 0xDFU, 0x7AU, 0x27U, 0x76U, 0x6DU, 0xC0U, 0x20U, 0x47U,
|
||||
0xB7U, 0x64U, 0x22U, 0x5AU, 0xB7U, 0x5DU, 0xF3U, 0xFAU, 0x0DU, 0xE3U,
|
||||
0xBDU, 0xC6U, 0x40U, 0xBAU, 0xD0U, 0x0AU, 0x66U, 0xEBU, 0x68U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU, 0xBCU, 0x09U, 0x95U,
|
||||
0xFDU, 0x03U, 0x0FU, 0xF1U, 0x85U, 0xF4U, 0xECU, 0xACU, 0x94U, 0xDBU,
|
||||
0x02U, 0x19U, 0x26U, 0xFEU, 0x58U, 0xD4U, 0x8DU, 0x48U, 0x21U, 0xCCU,
|
||||
0x82U, 0x2EU, 0x4AU, 0x6AU, 0xC9U, 0xC8U, 0xC6U, 0x7CU, 0x88U
|
||||
};
|
||||
// 0x5CU
|
||||
// EmitNonce 32bytes
|
||||
@@ -1763,10 +1821,10 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
|
||||
0x36U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0xEDU, 0x20U, 0x2EU, 0x00U, 0x00U, 0x00U, 0x01U,
|
||||
0x3DU, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U, 0x5BU,
|
||||
0xB8U, 0x05U, 0xD6U, 0xC3U, 0x52U, 0xDFU, 0x7AU, 0x27U, 0x76U, 0x6DU,
|
||||
0xC0U, 0x20U, 0x47U, 0xB7U, 0x64U, 0x22U, 0x5AU, 0xB7U, 0x5DU, 0xF3U,
|
||||
0xFAU, 0x0DU, 0xE3U, 0xBDU, 0xC6U, 0x40U, 0xBAU, 0xD0U, 0x0AU, 0x66U,
|
||||
0xEBU, 0x68U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x68U, 0x61U, 0x73U,
|
||||
0xBCU, 0x09U, 0x95U, 0xFDU, 0x03U, 0x0FU, 0xF1U, 0x85U, 0xF4U, 0xECU,
|
||||
0xACU, 0x94U, 0xDBU, 0x02U, 0x19U, 0x26U, 0xFEU, 0x58U, 0xD4U, 0x8DU,
|
||||
0x48U, 0x21U, 0xCCU, 0x82U, 0x2EU, 0x4AU, 0x6AU, 0xC9U, 0xC8U, 0xC6U,
|
||||
0x7CU, 0x88U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x68U, 0x61U, 0x73U,
|
||||
0x68U, 0x28U, 0x28U, 0x75U, 0x69U, 0x6EU, 0x74U, 0x33U, 0x32U, 0x5FU,
|
||||
0x74U, 0x29U, 0x65U, 0x78U, 0x70U, 0x65U, 0x63U, 0x74U, 0x65U, 0x64U,
|
||||
0x5FU, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x68U, 0x61U, 0x73U, 0x68U,
|
||||
|
||||
@@ -798,18 +798,21 @@ 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);
|
||||
// RH TODO: re-add these when the amendment is ready for supported::yes
|
||||
/*
|
||||
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);
|
||||
*/
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ private:
|
||||
taker_offer,
|
||||
taker_quality,
|
||||
parse_amount(funds, issue_in),
|
||||
sell ? tfSell : 0,
|
||||
sell ? std::uint32_t(tfSell) : 0,
|
||||
rate_in,
|
||||
rate_out);
|
||||
|
||||
|
||||
+128
-118
@@ -980,6 +980,7 @@ struct URIToken_test : public beast::unit_test::suite
|
||||
|
||||
// setup env
|
||||
env.fund(XRP(1000), alice, bob, gw);
|
||||
env.close();
|
||||
env.trust(USD(100000), alice, bob);
|
||||
env.close();
|
||||
env(pay(gw, alice, USD(1000)));
|
||||
@@ -2310,12 +2311,16 @@ struct URIToken_test : public beast::unit_test::suite
|
||||
auto const bob = Account("bob");
|
||||
|
||||
Env env{*this, features};
|
||||
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
std::string uri = "";
|
||||
|
||||
// test utf-8 success
|
||||
// =========================================================================
|
||||
// Cases that should succeed:
|
||||
// valid well-formed UTF-8 that is not a noncharacter
|
||||
// =========================================================================
|
||||
{
|
||||
// case: kosme
|
||||
uri = "κόσμε";
|
||||
@@ -2348,133 +2353,83 @@ struct URIToken_test : public beast::unit_test::suite
|
||||
// case: ipfs metadata url
|
||||
uri = "https://example.com/ipfs/";
|
||||
env(uritoken::mint(alice, uri));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Cases that should fail:
|
||||
// genuinely malformed UTF-8 (well-formedness failures)
|
||||
// =========================================================================
|
||||
{
|
||||
// BOUNDRY - START
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// case: 1 byte (U-00000000)
|
||||
uri = "\x00";
|
||||
env(uritoken::mint(alice, uri),
|
||||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||||
// case: 2 bytes (U-00000080)
|
||||
uri = "\xC2\x80";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 3 bytes (U-00000800)
|
||||
uri = "\xE0\xA0\x80";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 4 bytes (U-00010000)
|
||||
uri = "\xF0\x90\x80\x80";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 5 bytes (U-00200000)
|
||||
// case: 5 bytes (U-00200000) - beyond valid UTF-8 range
|
||||
uri = "\xF8\x88\x80\x80\x80";
|
||||
env(uritoken::mint(alice, uri),
|
||||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||||
// case: 6 bytes (U-04000000)
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// case: 6 bytes (U-04000000) - beyond valid UTF-8 range
|
||||
uri = "\xFC\x84\x80\x80\x80\x80";
|
||||
env(uritoken::mint(alice, uri),
|
||||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// BOUNDRY - END
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// case: 1 byte (U-0000007F)
|
||||
uri = "\x7F";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 2 bytes (U-000007FF)
|
||||
uri = "\xDF\xBF";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 3 bytes (U-0000FFFF)
|
||||
uri = "\xEF\xBF\xBF";
|
||||
env(uritoken::mint(alice, uri),
|
||||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||||
// case: 4 bytes (U-001FFFFF)
|
||||
uri = "\xF7\xBF\xBF\xBF";
|
||||
env(uritoken::mint(alice, uri),
|
||||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||||
// case: 5 bytes (U-03FFFFFF)
|
||||
uri = "\xFB\xBF\xBF\xBF\xBF";
|
||||
env(uritoken::mint(alice, uri),
|
||||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||||
// case: 6 bytes (U-7FFFFFFF)
|
||||
uri = "\xFD\xBF\xBF\xBF\xBF\xBF";
|
||||
env(uritoken::mint(alice, uri),
|
||||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||||
// case: 3 bytes max (U-0000FFFF) - but this is U+FFFF
|
||||
// noncharacter, rejected, see below
|
||||
|
||||
// // BOUNDRY - OTHER
|
||||
// case: 4 bytes max (U-001FFFFF) - beyond valid BMP
|
||||
uri = "\xF7\xBF\xBF\xBF";
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// case: 5 bytes max (U-03FFFFFF) - beyond valid UTF-8
|
||||
uri = "\xFB\xBF\xBF\xBF\xBF";
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// case: 6 bytes max (U-7FFFFFFF) - beyond valid UTF-8
|
||||
uri = "\xFD\xBF\xBF\xBF\xBF\xBF";
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// BOUNDRY - OTHER
|
||||
// ----------------------------------------------------------------
|
||||
// case: 1 bytes (U-0000D7FF)
|
||||
// case: 1 bytes (U-0000D7FF) - incomplete 2-byte sequence
|
||||
uri = "\xD7\xFF";
|
||||
env(uritoken::mint(alice, uri),
|
||||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||||
// case: 2 bytes (U-0000E000)
|
||||
uri = "\xEE\x80\x80";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 3 bytes (U-0000FFFD)
|
||||
uri = "\xEF\xBF\xBD";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// // case: 4 bytes (U-0010FFFF)
|
||||
uri = "\xF4\x8F\xBF\xBF";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// // case: 4 bytes (U-00110000)
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// case: 4 bytes (U-00110000) - beyond valid UTF-8 range
|
||||
uri = "\xF4\x90\x80\x80";
|
||||
env(uritoken::mint(alice, uri),
|
||||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD NOT FAIL
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
}
|
||||
// test utf8 malformed
|
||||
|
||||
// =========================================================================
|
||||
// Genuinely malformed UTF-8 sequences - always fail
|
||||
// =========================================================================
|
||||
{
|
||||
// MALFORMED - END
|
||||
// MALFORMED - START
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// First continuation byte 0x80:
|
||||
uri = "\x80";
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// Last continuation byte 0xbf
|
||||
uri = "\xBF";
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// 2 continuation bytes
|
||||
uri = "��";
|
||||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||||
|
||||
// 3 continuation bytes
|
||||
uri = "���";
|
||||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||||
|
||||
// 4 continuation bytes
|
||||
uri = "����";
|
||||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||||
|
||||
// 5 continuation bytes
|
||||
uri = "�����";
|
||||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||||
|
||||
// 6 continuation bytes
|
||||
uri = "������";
|
||||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||||
|
||||
// 7 continuation bytes
|
||||
uri = "�������";
|
||||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||||
|
||||
// Sequence of all 64 possible continuation bytes (0x80-0xbf)
|
||||
uri =
|
||||
"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E"
|
||||
"\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D"
|
||||
"\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC"
|
||||
"\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB"
|
||||
"\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D"
|
||||
"\x8E"
|
||||
"\x8F\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C"
|
||||
"\x9D"
|
||||
"\x9E\x9F\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB"
|
||||
"\xAC"
|
||||
"\xAD\xAE\xAF\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA"
|
||||
"\xBB"
|
||||
"\xBC\xBD\xBE\xBF";
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// TODO: REVIEW - THIS IS NOT THE CORRECT 32 byte sequence.
|
||||
// All 32 first bytes of 2-byte sequences (0xc0-0xdf), each followed
|
||||
// by a space character
|
||||
// uri = "\xE0\x80\x80 \xE0\x80\x81 \xE0\x80\x82 \xE0\x80\x83
|
||||
// \xE0\x80\x84 \xE0\x80\x85 \xE0\x80\x86 \xE0\x80\x87 \xE0\x80\x88
|
||||
// \xE0\x80\x89 \xE0\x80\x8A \xE0\x80\x8B \xE0\x80\x8C \xE0\x80\x8D
|
||||
// \xE0\x80\x8E \xE0\x80\x8F \xE0\x80\x90";
|
||||
// env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// All 16 first bytes of 3-byte sequences (0xe0-0xef), each followed
|
||||
// by a space character
|
||||
// All 16 first bytes of 3-byte sequences (0xe0-0xef), each
|
||||
// followed by a space character
|
||||
uri =
|
||||
"\xE0\x80\x80 \xE0\x80\x81 \xE0\x80\x82 \xE0\x80\x83 "
|
||||
"\xE0\x80\x84 \xE0\x80\x85 \xE0\x80\x86 \xE0\x80\x87 "
|
||||
@@ -2483,30 +2438,25 @@ struct URIToken_test : public beast::unit_test::suite
|
||||
"\xE0\x80\x90";
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// All 8 first bytes of 4-byte sequences (0xf0-0xf7), each followed
|
||||
// by a space character
|
||||
// All 8 first bytes of 4-byte sequences (0xf0-0xf7), each
|
||||
// followed by a space character
|
||||
uri =
|
||||
"\xF0\x90\x80\x80 \xF0\x90\x80\x81 \xF0\x90\x80\x82 "
|
||||
"\xF0\x90\x80\x83 \xF0\x90\x80\x84 \xF0\x90\x80\x85 "
|
||||
"\xF0\x90\x80\x86 \xF0\x90\x80\x87";
|
||||
env(uritoken::mint(alice, uri)); // TODO: REVIEW - SHOULD FAIL
|
||||
env(uritoken::mint(alice, uri));
|
||||
|
||||
// All 4 first bytes of 5-byte sequences (0xf8-0xfb), each followed
|
||||
// by a space character
|
||||
// All 4 first bytes of 5-byte sequences (0xf8-0xfb), each
|
||||
// followed by a space character
|
||||
uri =
|
||||
"\xF8\x88\x80\x80\x80 \xF8\x88\x80\x80\x81 "
|
||||
"\xF8\x88\x80\x80\x82 \xF8\x88\x80\x80\x83";
|
||||
env(uritoken::mint(alice, uri),
|
||||
ter(temMALFORMED)); // TODO: REVIEW - SHOULD FAIL
|
||||
|
||||
// All 2 first bytes of 6-byte sequences (0xfc-0xfd), each followed
|
||||
// by a space character
|
||||
uri = "\xFC\x84\x80\x80\x80\x80 \xFC\x84\x80\x80\x80\x81";
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// Sequences with last continuation byte missing
|
||||
|
||||
// Concatenation of incomplete sequences
|
||||
// All 2 first bytes of 6-byte sequences (0xfc-0xfd), each
|
||||
// followed by a space character
|
||||
uri = "\xFC\x84\x80\x80\x80\x80 \xFC\x84\x80\x80\x80\x81";
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// Impossible bytes
|
||||
uri = "\xFE";
|
||||
@@ -2616,15 +2566,75 @@ struct URIToken_test : public beast::unit_test::suite
|
||||
uri = "\xED\xAF\xBF\xED\xBF\xBF";
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
// problematic noncharacters in 16-bit applications
|
||||
// case: (U+FFFE)
|
||||
uri = "\xEF\xBF\xBE";
|
||||
// MALFORMED - END
|
||||
// ----------------------------------------------------------------
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Noncharacters U+FFFE and U+FFFF: well-formed UTF-8, but
|
||||
// deliberately rejected (U+FFFE is a byte-swapped BOM).
|
||||
// =========================================================================
|
||||
{
|
||||
uri = "\xEF\xBF\xBE"; // U+FFFE
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
// case: (U+FFFF)
|
||||
uri = "\xEF\xBF\xBF";
|
||||
|
||||
uri = "\xEF\xBF\xBF"; // U+FFFF
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
|
||||
uri = "ipfs://x\xEF\xBF\xBE"; // not only at the start
|
||||
env(uritoken::mint(alice, uri), ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Valid boundary cases that should always succeed
|
||||
// =========================================================================
|
||||
{
|
||||
// BOUNDRY - START (valid minimums)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// case: 1 byte (U-00000000). Must use the string
|
||||
// constructor, not a C-string literal, to avoid strlen
|
||||
// truncation to an empty URI.
|
||||
uri = std::string(1, '\0');
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 2 bytes (U-00000080)
|
||||
uri = "\xC2\x80";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 3 bytes (U-00000800)
|
||||
uri = "\xE0\xA0\x80";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 4 bytes (U-00010000)
|
||||
uri = "\xF0\x90\x80\x80";
|
||||
env(uritoken::mint(alice, uri));
|
||||
|
||||
// BOUNDRY - END (valid maximums)
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
// case: 1 byte (U-0000007F)
|
||||
uri = "\x7F";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 2 bytes (U-000007FF)
|
||||
uri = "\xDF\xBF";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 3 bytes (U-0000FFFF) - noncharacter, rejected, see
|
||||
// above.
|
||||
// case: 4 bytes (U-001FFFFF) - beyond U+10FFFF, always
|
||||
// rejected, see above.
|
||||
|
||||
// BOUNDRY - OTHER
|
||||
// ----------------------------------------------------------------
|
||||
// case: 2 bytes (U-0000E000) - above surrogates
|
||||
uri = "\xEE\x80\x80";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 3 bytes (U-0000FFFD) - replacement character, valid
|
||||
uri = "\xEF\xBF\xBD";
|
||||
env(uritoken::mint(alice, uri));
|
||||
// case: 4 bytes (U-0010FFFF) - max valid code point
|
||||
uri = "\xF4\x8F\xBF\xBF";
|
||||
env(uritoken::mint(alice, uri));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testWithFeats(FeatureBitset features)
|
||||
{
|
||||
|
||||
@@ -206,7 +206,7 @@ private:
|
||||
NetClock::time_point const expires2 =
|
||||
effective2 + cfg.expiresFromNow;
|
||||
item.server = make_TrustedPublisherServer(
|
||||
env.app().getIOService(),
|
||||
env.app().getIOContext(),
|
||||
item.list,
|
||||
expires,
|
||||
{{effective2, expires2}},
|
||||
|
||||
@@ -327,8 +327,8 @@ struct XahauGenesis_test : public beast::unit_test::suite
|
||||
const char first = rn.c_str()[0];
|
||||
BEAST_EXPECT(
|
||||
(first == 'r' && !!parseBase58<AccountID>(rn)) ||
|
||||
first == 'n' &&
|
||||
!!parseBase58<PublicKey>(TokenType::NodePublic, rn));
|
||||
(first == 'n' &&
|
||||
!!parseBase58<PublicKey>(TokenType::NodePublic, rn)));
|
||||
|
||||
if (first == 'r')
|
||||
{
|
||||
@@ -363,8 +363,8 @@ struct XahauGenesis_test : public beast::unit_test::suite
|
||||
const char first = rn.c_str()[0];
|
||||
BEAST_EXPECT(
|
||||
(first == 'r' && !!parseBase58<AccountID>(rn)) ||
|
||||
first == 'n' &&
|
||||
!!parseBase58<PublicKey>(TokenType::NodePublic, rn));
|
||||
(first == 'n' &&
|
||||
!!parseBase58<PublicKey>(TokenType::NodePublic, rn)));
|
||||
|
||||
if (first == 'r')
|
||||
{
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 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/unit_test/SuiteJournal.h>
|
||||
|
||||
#include <xrpl/basics/ResolverAsio.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
// ResolverAsio was ported from io_service / strand.wrap / resolver iterators
|
||||
// to io_context / bind_executor / results_type for Boost 1.87+. This covers
|
||||
// the ported dispatch, resolve, results iteration, work chaining and stop
|
||||
// paths against a live io_context on a worker thread.
|
||||
class ResolverAsio_test : public beast::unit_test::suite
|
||||
{
|
||||
struct Loop
|
||||
{
|
||||
boost::asio::io_context ioc;
|
||||
std::optional<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>
|
||||
work{boost::asio::make_work_guard(ioc)};
|
||||
std::thread thread{[this] { ioc.run(); }};
|
||||
|
||||
~Loop()
|
||||
{
|
||||
work.reset();
|
||||
ioc.stop();
|
||||
thread.join();
|
||||
}
|
||||
};
|
||||
|
||||
using Result = std::pair<std::string, std::vector<beast::IP::Endpoint>>;
|
||||
|
||||
struct Results
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::vector<Result> items;
|
||||
|
||||
Resolver::HandlerType
|
||||
handler()
|
||||
{
|
||||
return [this](
|
||||
std::string name,
|
||||
std::vector<beast::IP::Endpoint> endpoints) {
|
||||
std::lock_guard lock(mutex);
|
||||
items.emplace_back(std::move(name), std::move(endpoints));
|
||||
cv.notify_all();
|
||||
};
|
||||
}
|
||||
|
||||
bool
|
||||
waitFor(std::size_t n, std::chrono::milliseconds timeout)
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
return cv.wait_for(
|
||||
lock, timeout, [&] { return items.size() >= n; });
|
||||
}
|
||||
};
|
||||
|
||||
void
|
||||
testNumericHost()
|
||||
{
|
||||
testcase("a numeric host:port resolves to itself");
|
||||
using namespace std::chrono_literals;
|
||||
Loop loop;
|
||||
SuiteJournal journal("ResolverAsio_test", *this);
|
||||
auto resolver = ResolverAsio::New(loop.ioc, journal);
|
||||
resolver->start();
|
||||
|
||||
Results results;
|
||||
resolver->resolve({"127.0.0.1:2222"}, results.handler());
|
||||
BEAST_EXPECT(results.waitFor(1, 5000ms));
|
||||
{
|
||||
std::lock_guard lock(results.mutex);
|
||||
if (BEAST_EXPECT(results.items.size() == 1))
|
||||
{
|
||||
auto const& [name, endpoints] = results.items.front();
|
||||
BEAST_EXPECT(name == "127.0.0.1:2222");
|
||||
if (BEAST_EXPECT(endpoints.size() == 1))
|
||||
{
|
||||
BEAST_EXPECT(endpoints.front().port() == 2222);
|
||||
BEAST_EXPECT(
|
||||
endpoints.front().address().to_string() == "127.0.0.1");
|
||||
}
|
||||
}
|
||||
}
|
||||
resolver->stop();
|
||||
}
|
||||
|
||||
void
|
||||
testBatchSkipsUnparseable()
|
||||
{
|
||||
testcase("a batch resolves in order; unparseable names are skipped");
|
||||
using namespace std::chrono_literals;
|
||||
Loop loop;
|
||||
SuiteJournal journal("ResolverAsio_test", *this);
|
||||
auto resolver = ResolverAsio::New(loop.ioc, journal);
|
||||
resolver->start();
|
||||
|
||||
Results results;
|
||||
// the whitespace-only name has no host: the resolver logs and moves
|
||||
// on without invoking the handler for it
|
||||
resolver->resolve(
|
||||
{"127.0.0.1:1", " ", "127.0.0.1:2", "127.0.0.1:3"},
|
||||
results.handler());
|
||||
BEAST_EXPECT(results.waitFor(3, 5000ms));
|
||||
{
|
||||
std::lock_guard lock(results.mutex);
|
||||
if (BEAST_EXPECT(results.items.size() == 3))
|
||||
{
|
||||
BEAST_EXPECT(results.items[0].first == "127.0.0.1:1");
|
||||
BEAST_EXPECT(results.items[1].first == "127.0.0.1:2");
|
||||
BEAST_EXPECT(results.items[2].first == "127.0.0.1:3");
|
||||
for (auto const& [name, endpoints] : results.items)
|
||||
BEAST_EXPECT(endpoints.size() == 1);
|
||||
}
|
||||
}
|
||||
resolver->stop();
|
||||
}
|
||||
|
||||
void
|
||||
testUnresolvableName()
|
||||
{
|
||||
testcase("an unresolvable name reports no endpoints");
|
||||
using namespace std::chrono_literals;
|
||||
Loop loop;
|
||||
SuiteJournal journal("ResolverAsio_test", *this);
|
||||
auto resolver = ResolverAsio::New(loop.ioc, journal);
|
||||
resolver->start();
|
||||
|
||||
Results results;
|
||||
// .invalid is reserved to never resolve (RFC 2606)
|
||||
resolver->resolve({"nonexistent.invalid:80"}, results.handler());
|
||||
// a DNS error surfaces as a callback with an empty list; a slow or
|
||||
// absent resolver makes this take a few seconds, not forever
|
||||
BEAST_EXPECT(results.waitFor(1, 20000ms));
|
||||
{
|
||||
std::lock_guard lock(results.mutex);
|
||||
if (BEAST_EXPECT(results.items.size() == 1))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
results.items.front().first == "nonexistent.invalid:80");
|
||||
BEAST_EXPECT(results.items.front().second.empty());
|
||||
}
|
||||
}
|
||||
resolver->stop();
|
||||
}
|
||||
|
||||
void
|
||||
testStopWithPendingWork()
|
||||
{
|
||||
testcase("stop with work pending returns and drops the queue");
|
||||
using namespace std::chrono_literals;
|
||||
Loop loop;
|
||||
SuiteJournal journal("ResolverAsio_test", *this);
|
||||
auto resolver = ResolverAsio::New(loop.ioc, journal);
|
||||
resolver->start();
|
||||
|
||||
Results results;
|
||||
std::vector<std::string> names;
|
||||
for (int i = 1; i <= 200; ++i)
|
||||
names.push_back("127.0.0.1:" + std::to_string(i));
|
||||
resolver->resolve(names, results.handler());
|
||||
// a synchronous stop waits for the in-flight handler and clears the
|
||||
// rest; it must return promptly and the destructor's invariants
|
||||
// (no pending work, stopped) must then hold
|
||||
auto const start = std::chrono::steady_clock::now();
|
||||
resolver->stop();
|
||||
BEAST_EXPECT(
|
||||
std::chrono::steady_clock::now() - start < std::chrono::seconds(5));
|
||||
std::size_t delivered;
|
||||
{
|
||||
std::lock_guard lock(results.mutex);
|
||||
delivered = results.items.size();
|
||||
}
|
||||
BEAST_EXPECT(delivered < names.size());
|
||||
resolver.reset();
|
||||
pass();
|
||||
}
|
||||
|
||||
void
|
||||
testStopAsync()
|
||||
{
|
||||
testcase("stop_async followed by stop is idempotent");
|
||||
using namespace std::chrono_literals;
|
||||
Loop loop;
|
||||
SuiteJournal journal("ResolverAsio_test", *this);
|
||||
auto resolver = ResolverAsio::New(loop.ioc, journal);
|
||||
resolver->start();
|
||||
|
||||
Results results;
|
||||
resolver->resolve({"127.0.0.1:9"}, results.handler());
|
||||
BEAST_EXPECT(results.waitFor(1, 5000ms));
|
||||
resolver->stop_async();
|
||||
resolver->stop();
|
||||
resolver.reset();
|
||||
pass();
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testNumericHost();
|
||||
testBatchSkipsUnparseable();
|
||||
testUnresolvableName();
|
||||
testStopWithPendingWork();
|
||||
testStopAsync();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ResolverAsio, basics, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -0,0 +1,69 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/UTF8.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
struct UTF8_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testcase("isValidUTF8");
|
||||
|
||||
auto const check = [](std::string const& s) {
|
||||
return isValidUTF8(makeSlice(s));
|
||||
};
|
||||
|
||||
// Sequences truncated by the end of the buffer are invalid. The
|
||||
// previous decoder read past the end of the vector here.
|
||||
BEAST_EXPECT(!check("a\xC2"));
|
||||
BEAST_EXPECT(!check("a\xE0\xA4"));
|
||||
BEAST_EXPECT(!check("a\xEF\xBF"));
|
||||
BEAST_EXPECT(!check("a\xF0\x90\x8D"));
|
||||
BEAST_EXPECT(!check(std::string("\xC2", 1)));
|
||||
BEAST_EXPECT(!check(std::string("\xF4\x8F\xBF", 3)));
|
||||
|
||||
// The complete sequences are fine.
|
||||
BEAST_EXPECT(check("a\xC2\xA2"));
|
||||
BEAST_EXPECT(check("a\xE0\xA4\xB9"));
|
||||
BEAST_EXPECT(check("a\xF0\x90\x8D\x88"));
|
||||
BEAST_EXPECT(check("a\xF4\x8F\xBF\xBF"));
|
||||
BEAST_EXPECT(check(""));
|
||||
|
||||
// U+FFFE and U+FFFF are rejected wherever they appear.
|
||||
BEAST_EXPECT(!check("\xEF\xBF\xBE"));
|
||||
BEAST_EXPECT(!check("ab\xEF\xBF\xBF"));
|
||||
BEAST_EXPECT(!check("\xE2\x82\xAC\xEF\xBF\xBEz"));
|
||||
|
||||
// Their neighbours are not, and nor are the other noncharacters,
|
||||
// which the validator has never rejected.
|
||||
BEAST_EXPECT(check("\xEF\xBF\xBD")); // U+FFFD
|
||||
BEAST_EXPECT(check("\xEF\xBE\xBF")); // U+FFBF
|
||||
BEAST_EXPECT(check("\xEF\xBB\xBF")); // U+FEFF (BOM)
|
||||
BEAST_EXPECT(check("\xF0\x9F\xBF\xBE")); // U+1FFFE
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(UTF8, basics, ripple);
|
||||
|
||||
} // namespace ripple
|
||||
@@ -44,13 +44,13 @@ public:
|
||||
std::string const& normal = "")
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
Address const result{Address::from_string(s, ec)};
|
||||
Address const result{boost::asio::ip::make_address(s, ec)};
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return;
|
||||
if (!BEAST_EXPECTS(result.is_v4(), s + " not v4"))
|
||||
return;
|
||||
if (!BEAST_EXPECTS(
|
||||
result.to_v4().to_ulong() == value, s + " value mismatch"))
|
||||
result.to_v4().to_uint() == value, s + " value mismatch"))
|
||||
return;
|
||||
BEAST_EXPECTS(
|
||||
result.to_string() == (normal.empty() ? s : normal),
|
||||
@@ -61,7 +61,7 @@ public:
|
||||
failParseAddr(std::string const& s)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
auto a = Address::from_string(s, ec);
|
||||
auto a = boost::asio::ip::make_address(s, ec);
|
||||
BEAST_EXPECTS(ec, s + " parses as " + a.to_string());
|
||||
}
|
||||
|
||||
@@ -70,24 +70,24 @@ public:
|
||||
{
|
||||
testcase("AddressV4");
|
||||
|
||||
BEAST_EXPECT(AddressV4{}.to_ulong() == 0);
|
||||
BEAST_EXPECT(AddressV4{}.to_uint() == 0);
|
||||
BEAST_EXPECT(is_unspecified(AddressV4{}));
|
||||
BEAST_EXPECT(AddressV4{0x01020304}.to_ulong() == 0x01020304);
|
||||
BEAST_EXPECT(AddressV4{0x01020304}.to_uint() == 0x01020304);
|
||||
|
||||
{
|
||||
AddressV4::bytes_type d = {{1, 2, 3, 4}};
|
||||
BEAST_EXPECT(AddressV4{d}.to_ulong() == 0x01020304);
|
||||
BEAST_EXPECT(AddressV4{d}.to_uint() == 0x01020304);
|
||||
|
||||
unexpected(is_unspecified(AddressV4{d}));
|
||||
}
|
||||
|
||||
AddressV4 const v1{1};
|
||||
BEAST_EXPECT(AddressV4{v1}.to_ulong() == 1);
|
||||
BEAST_EXPECT(AddressV4{v1}.to_uint() == 1);
|
||||
|
||||
{
|
||||
AddressV4 v;
|
||||
v = v1;
|
||||
BEAST_EXPECT(v.to_ulong() == v1.to_ulong());
|
||||
BEAST_EXPECT(v.to_uint() == v1.to_uint());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -98,7 +98,7 @@ public:
|
||||
d[2] = 3;
|
||||
d[3] = 4;
|
||||
v = AddressV4{d};
|
||||
BEAST_EXPECT(v.to_ulong() == 0x01020304);
|
||||
BEAST_EXPECT(v.to_uint() == 0x01020304);
|
||||
}
|
||||
|
||||
BEAST_EXPECT(AddressV4(0x01020304).to_string() == "1.2.3.4");
|
||||
@@ -160,7 +160,7 @@ public:
|
||||
testcase("Address");
|
||||
|
||||
boost::system::error_code ec;
|
||||
Address result{Address::from_string("1.2.3.4", ec)};
|
||||
Address result{boost::asio::ip::make_address("1.2.3.4", ec)};
|
||||
AddressV4::bytes_type d = {{1, 2, 3, 4}};
|
||||
BEAST_EXPECT(!ec);
|
||||
BEAST_EXPECT(result.is_v4() && result.to_v4() == AddressV4{d});
|
||||
@@ -262,7 +262,10 @@ public:
|
||||
BEAST_EXPECT(is_loopback(ep));
|
||||
BEAST_EXPECT(to_string(ep) == "127.0.0.1:80");
|
||||
// same address as v4 mapped in ipv6
|
||||
ep = Endpoint(AddressV6::v4_mapped(AddressV4{d}), 80);
|
||||
ep = Endpoint(
|
||||
boost::asio::ip::make_address_v6(
|
||||
boost::asio::ip::v4_mapped, AddressV4{d}),
|
||||
80);
|
||||
BEAST_EXPECT(!is_unspecified(ep));
|
||||
BEAST_EXPECT(!is_public(ep));
|
||||
BEAST_EXPECT(is_private(ep));
|
||||
@@ -280,8 +283,11 @@ public:
|
||||
BEAST_EXPECT(!is_loopback(ep));
|
||||
BEAST_EXPECT(to_string(ep) == "10.0.0.1");
|
||||
// same address as v4 mapped in ipv6
|
||||
ep = Endpoint(AddressV6::v4_mapped(AddressV4{d}));
|
||||
BEAST_EXPECT(get_class(ep.to_v6().to_v4()) == 'A');
|
||||
ep = Endpoint(boost::asio::ip::make_address_v6(
|
||||
boost::asio::ip::v4_mapped, AddressV4{d}));
|
||||
BEAST_EXPECT(
|
||||
get_class(boost::asio::ip::make_address_v4(
|
||||
boost::asio::ip::v4_mapped, ep.to_v6())) == 'A');
|
||||
BEAST_EXPECT(!is_unspecified(ep));
|
||||
BEAST_EXPECT(!is_public(ep));
|
||||
BEAST_EXPECT(is_private(ep));
|
||||
@@ -298,7 +304,8 @@ public:
|
||||
BEAST_EXPECT(!is_loopback(ep));
|
||||
BEAST_EXPECT(to_string(ep) == "166.78.151.147");
|
||||
// same address as v4 mapped in ipv6
|
||||
ep = Endpoint(AddressV6::v4_mapped(AddressV4{d}));
|
||||
ep = Endpoint(boost::asio::ip::make_address_v6(
|
||||
boost::asio::ip::v4_mapped, AddressV4{d}));
|
||||
BEAST_EXPECT(!is_unspecified(ep));
|
||||
BEAST_EXPECT(is_public(ep));
|
||||
BEAST_EXPECT(!is_private(ep));
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 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/unit_test/SuiteJournal.h>
|
||||
|
||||
#include <xrpl/beast/insight/StatsDCollector.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/udp.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
// The StatsD collector was ported from io_service / strand.wrap /
|
||||
// expires_from_now to io_context / bind_executor / expires_after for Boost
|
||||
// 1.87+. Nothing exercised it before; this drives every path of the port:
|
||||
// metric posts (dispatch onto the strand), the 1 s flush tick, the UDP send,
|
||||
// and the timer-cancelling teardown.
|
||||
class StatsDCollector_test : public beast::unit_test::suite
|
||||
{
|
||||
// A StatsD "server": a loopback UDP socket polled by its own thread.
|
||||
struct UdpSink
|
||||
{
|
||||
boost::asio::io_context ioc;
|
||||
boost::asio::ip::udp::socket socket;
|
||||
std::mutex mutex;
|
||||
std::string received;
|
||||
std::atomic<bool> stopping{false};
|
||||
std::thread thread;
|
||||
|
||||
UdpSink()
|
||||
: socket(
|
||||
ioc,
|
||||
boost::asio::ip::udp::endpoint{
|
||||
boost::asio::ip::make_address("127.0.0.1"),
|
||||
0})
|
||||
{
|
||||
socket.non_blocking(true);
|
||||
thread = std::thread([this] { poll(); });
|
||||
}
|
||||
|
||||
~UdpSink()
|
||||
{
|
||||
stopping = true;
|
||||
thread.join();
|
||||
}
|
||||
|
||||
beast::IP::Endpoint
|
||||
endpoint() const
|
||||
{
|
||||
auto const local = socket.local_endpoint();
|
||||
// IP::Address is boost::asio::ip::address itself
|
||||
return beast::IP::Endpoint{local.address(), local.port()};
|
||||
}
|
||||
|
||||
void
|
||||
poll()
|
||||
{
|
||||
char buf[2048];
|
||||
while (!stopping)
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
auto const n = socket.receive(boost::asio::buffer(buf), 0, ec);
|
||||
if (!ec && n > 0)
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
received.append(buf, n);
|
||||
received.push_back('\n');
|
||||
}
|
||||
else
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <class Cond>
|
||||
bool
|
||||
waitFor(Cond cond, std::chrono::milliseconds timeout)
|
||||
{
|
||||
auto const deadline = std::chrono::steady_clock::now() + timeout;
|
||||
while (std::chrono::steady_clock::now() < deadline)
|
||||
{
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (cond(received))
|
||||
return true;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
std::lock_guard lock(mutex);
|
||||
return cond(received);
|
||||
}
|
||||
};
|
||||
|
||||
static bool
|
||||
has(std::string const& all, std::string const& s)
|
||||
{
|
||||
return all.find(s) != std::string::npos;
|
||||
}
|
||||
|
||||
void
|
||||
testMetricsReachServer()
|
||||
{
|
||||
testcase("counters, gauges, meters, events and hooks reach the server");
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
UdpSink sink;
|
||||
SuiteJournal journal("StatsDCollector_test", *this);
|
||||
std::atomic<int> hookCalls{0};
|
||||
{
|
||||
auto collector = beast::insight::StatsDCollector::New(
|
||||
sink.endpoint(), "prefix", journal);
|
||||
|
||||
auto hook = collector->make_hook([&] { ++hookCalls; });
|
||||
auto counter = collector->make_counter("hits");
|
||||
auto gauge = collector->make_gauge("depth");
|
||||
auto meter = collector->make_meter("rate");
|
||||
auto event = collector->make_event("latency");
|
||||
|
||||
counter.increment(5);
|
||||
gauge.set(42);
|
||||
meter.increment(3);
|
||||
event.notify(7ms);
|
||||
|
||||
// counters/gauges/meters flush on the collector's 1 s tick and
|
||||
// the batch leaves in one or more datagrams; events post at once
|
||||
// but travel with the same batch
|
||||
bool const arrived = sink.waitFor(
|
||||
[](std::string const& all) {
|
||||
return has(all, "prefix.hits:5|c") &&
|
||||
has(all, "prefix.depth:42|g") &&
|
||||
has(all, "prefix.rate:3|") &&
|
||||
has(all, "prefix.latency:7|ms");
|
||||
},
|
||||
5000ms);
|
||||
BEAST_EXPECT(arrived);
|
||||
if (!arrived)
|
||||
{
|
||||
std::lock_guard lock(sink.mutex);
|
||||
log << "received: " << sink.received << std::endl;
|
||||
}
|
||||
}
|
||||
// leaving the scope destroys the collector: it cancels its flush
|
||||
// timer, drops its work guard and joins its io thread
|
||||
BEAST_EXPECT(hookCalls >= 1);
|
||||
}
|
||||
|
||||
void
|
||||
testTeardownWithoutTraffic()
|
||||
{
|
||||
testcase("a collector with no metrics tears down promptly");
|
||||
UdpSink sink;
|
||||
SuiteJournal journal("StatsDCollector_test", *this);
|
||||
auto const start = std::chrono::steady_clock::now();
|
||||
{
|
||||
auto collector = beast::insight::StatsDCollector::New(
|
||||
sink.endpoint(), "idle", journal);
|
||||
(void)collector;
|
||||
}
|
||||
auto const elapsed = std::chrono::steady_clock::now() - start;
|
||||
// the destructor must not wait for the next 1 s tick
|
||||
BEAST_EXPECT(elapsed < std::chrono::seconds(3));
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testMetricsReachServer();
|
||||
testTeardownWithoutTraffic();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(StatsDCollector, insight, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -23,7 +23,8 @@
|
||||
|
||||
#include <boost/asio/basic_waitable_timer.hpp>
|
||||
#include <boost/asio/deadline_timer.hpp>
|
||||
#include <boost/asio/io_service.hpp>
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -62,8 +63,10 @@ class io_latency_probe_test : public beast::unit_test::suite,
|
||||
measure_asio_timers(duration interval = 100ms, size_t num_samples = 50)
|
||||
{
|
||||
using namespace std::chrono;
|
||||
boost::asio::io_service ios;
|
||||
std::optional<boost::asio::io_service::work> work{ios};
|
||||
boost::asio::io_context ios;
|
||||
std::optional<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>
|
||||
work{boost::asio::make_work_guard(ios)};
|
||||
std::thread worker{[&] { ios.run(); }};
|
||||
boost::asio::basic_waitable_timer<Clock> timer{ios};
|
||||
elapsed_times_.reserve(num_samples);
|
||||
@@ -137,7 +140,7 @@ class io_latency_probe_test : public beast::unit_test::suite,
|
||||
|
||||
test_sampler(
|
||||
std::chrono::milliseconds interval,
|
||||
boost::asio::io_service& ios)
|
||||
boost::asio::io_context& ios)
|
||||
: probe_(interval, ios)
|
||||
{
|
||||
}
|
||||
@@ -166,9 +169,9 @@ class io_latency_probe_test : public beast::unit_test::suite,
|
||||
{
|
||||
testcase << "sample one";
|
||||
boost::system::error_code ec;
|
||||
test_sampler io_probe{100ms, get_io_service()};
|
||||
test_sampler io_probe{100ms, get_io_context()};
|
||||
io_probe.start_one();
|
||||
MyTimer timer{get_io_service(), 1s};
|
||||
MyTimer timer{get_io_context(), 1s};
|
||||
timer.async_wait(yield[ec]);
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return;
|
||||
@@ -200,9 +203,9 @@ class io_latency_probe_test : public beast::unit_test::suite,
|
||||
duration_cast<milliseconds>(probe_duration).count()) /
|
||||
static_cast<size_t>(tt.getMean<milliseconds>());
|
||||
#endif
|
||||
test_sampler io_probe{interval, get_io_service()};
|
||||
test_sampler io_probe{interval, get_io_context()};
|
||||
io_probe.start();
|
||||
MyTimer timer{get_io_service(), probe_duration};
|
||||
MyTimer timer{get_io_context(), probe_duration};
|
||||
timer.async_wait(yield[ec]);
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return;
|
||||
@@ -214,7 +217,7 @@ class io_latency_probe_test : public beast::unit_test::suite,
|
||||
io_probe.probe_.cancel_async();
|
||||
// wait again in order to flush the remaining
|
||||
// probes from the work queue
|
||||
timer.expires_from_now(1s);
|
||||
timer.expires_after(1s);
|
||||
timer.async_wait(yield[ec]);
|
||||
}
|
||||
|
||||
@@ -222,7 +225,7 @@ class io_latency_probe_test : public beast::unit_test::suite,
|
||||
testCanceled(boost::asio::yield_context& yield)
|
||||
{
|
||||
testcase << "canceled";
|
||||
test_sampler io_probe{100ms, get_io_service()};
|
||||
test_sampler io_probe{100ms, get_io_context()};
|
||||
io_probe.probe_.cancel_async();
|
||||
except<std::logic_error>([&io_probe]() { io_probe.start_one(); });
|
||||
except<std::logic_error>([&io_probe]() { io_probe.start(); });
|
||||
|
||||
@@ -56,6 +56,13 @@ public:
|
||||
/// Get RPC 1.0 or RPC 2.0
|
||||
virtual unsigned
|
||||
version() const = 0;
|
||||
|
||||
/// Times invoke() replaced a connection the server had closed.
|
||||
virtual unsigned
|
||||
reconnects() const
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace test
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <test/jtx/JSONRPCClient.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/app/misc/TxQ.h>
|
||||
#include <xrpl/beast/hash/uhash.h>
|
||||
@@ -27,8 +28,16 @@
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/http/read.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/beast/http/write.hpp>
|
||||
#include <boost/lexical_cast.hpp>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
namespace ripple {
|
||||
@@ -906,6 +915,106 @@ public:
|
||||
pass();
|
||||
}
|
||||
|
||||
void
|
||||
testJSONRPCClientKeepAlive()
|
||||
{
|
||||
testcase("JSON-RPC keep-alive survives test work");
|
||||
using namespace std::chrono_literals;
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this};
|
||||
env.client().invoke("server_info", {});
|
||||
auto const reconnects = env.client().reconnects();
|
||||
|
||||
// The normal loopback message timeout is three seconds. Established
|
||||
// Env connections have a separate idle lease because test work can
|
||||
// legitimately take longer between RPCs.
|
||||
std::this_thread::sleep_for(4s);
|
||||
auto const response = env.client().invoke("server_info", {});
|
||||
BEAST_EXPECT(response[jss::result][jss::status] == "success");
|
||||
// The server kept the connection; the client did not replace it.
|
||||
BEAST_EXPECT(env.client().reconnects() == reconnects);
|
||||
}
|
||||
|
||||
void
|
||||
testJSONRPCClientSlowFirstRequest()
|
||||
{
|
||||
testcase("JSON-RPC first request survives slow test setup");
|
||||
using namespace std::chrono_literals;
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this};
|
||||
|
||||
// The client connects when the Env is built, and the server closes a
|
||||
// loopback connection whose first request takes longer than
|
||||
// BaseHTTPPeer::timeoutSecondsLocal (3 s) to arrive. Test setup can
|
||||
// take that long on a loaded machine. Whether or not the server's
|
||||
// timer has fired by now, the request must succeed;
|
||||
// testJSONRPCClientReconnects covers the replacement itself.
|
||||
std::this_thread::sleep_for(4s);
|
||||
auto const response = env.client().invoke("server_info", {});
|
||||
BEAST_EXPECT(response[jss::result][jss::status] == "success");
|
||||
}
|
||||
|
||||
void
|
||||
testJSONRPCClientReconnects()
|
||||
{
|
||||
testcase("JSON-RPC client replaces a connection the server closed");
|
||||
using namespace boost::asio;
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
// A server that closes the client's first connection without
|
||||
// answering, as BaseHTTPPeer does when the first request is late, and
|
||||
// answers on the second.
|
||||
io_context ioc;
|
||||
ip::tcp::acceptor acceptor(ioc, {ip::make_address("127.0.0.1"), 0});
|
||||
ip::tcp::socket first(ioc);
|
||||
ip::tcp::socket second(ioc);
|
||||
boost::beast::flat_buffer buffer;
|
||||
http::request<http::string_body> req;
|
||||
http::response<http::string_body> res{http::status::ok, 11};
|
||||
res.body() = R"({"result":{"status":"success"}})";
|
||||
res.prepare_payload();
|
||||
acceptor.async_accept(first, [&](boost::system::error_code) {
|
||||
boost::system::error_code ignored;
|
||||
first.close(ignored);
|
||||
acceptor.async_accept(second, [&](boost::system::error_code acc) {
|
||||
if (acc)
|
||||
return;
|
||||
http::async_read(
|
||||
second,
|
||||
buffer,
|
||||
req,
|
||||
[&](boost::system::error_code rd, std::size_t) {
|
||||
if (!rd)
|
||||
http::async_write(
|
||||
second,
|
||||
res,
|
||||
[](boost::system::error_code, std::size_t) {});
|
||||
});
|
||||
});
|
||||
});
|
||||
std::thread server([&] { ioc.run(); });
|
||||
|
||||
auto cfg = jtx::envconfig();
|
||||
(*cfg)["port_rpc"].set("ip", "127.0.0.1");
|
||||
(*cfg)["port_rpc"].set(
|
||||
"port", std::to_string(acceptor.local_endpoint().port()));
|
||||
auto client = makeJSONRPCClient(*cfg);
|
||||
try
|
||||
{
|
||||
auto const response = client->invoke("server_info", {});
|
||||
BEAST_EXPECT(response[jss::result][jss::status] == "success");
|
||||
BEAST_EXPECT(client->reconnects() == 1);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
fail(e.what(), __FILE__, __LINE__);
|
||||
}
|
||||
ioc.stop();
|
||||
server.join();
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -933,6 +1042,9 @@ public:
|
||||
testSignAndSubmit(all);
|
||||
testFeatures(all);
|
||||
testExceptionalShutdown();
|
||||
testJSONRPCClientKeepAlive();
|
||||
testJSONRPCClientSlowFirstRequest();
|
||||
testJSONRPCClientReconnects();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ public:
|
||||
bool immediateStart = true,
|
||||
int sequence = 1)
|
||||
: sock_{ioc}
|
||||
, ep_{beast::IP::Address::from_string(
|
||||
, ep_{boost::asio::ip::make_address(
|
||||
ripple::test::getEnvLocalhostAddr()),
|
||||
// 0 means let OS pick the port based on what's available
|
||||
0}
|
||||
@@ -281,7 +281,7 @@ public:
|
||||
acceptor_.set_option(
|
||||
boost::asio::ip::tcp::acceptor::reuse_address(true), ec);
|
||||
acceptor_.bind(ep_);
|
||||
acceptor_.listen(boost::asio::socket_base::max_connections);
|
||||
acceptor_.listen(boost::asio::socket_base::max_listen_connections);
|
||||
acceptor_.async_accept(
|
||||
sock_,
|
||||
[wp = std::weak_ptr<TrustedPublisherServer>{shared_from_this()}](
|
||||
|
||||
@@ -143,6 +143,25 @@ static const std::vector<uint8_t> AcceptHook = {
|
||||
0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU,
|
||||
0x20U, 0x01U, 0x0BU};
|
||||
|
||||
// rollback_wasm from SetHook_test.cpp: rolls back every transaction.
|
||||
static const std::vector<uint8_t> RollbackHook = {
|
||||
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, 0x90U, 0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U,
|
||||
0x41U, 0x8EU, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU,
|
||||
0x00U, 0x41U, 0x90U, 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,
|
||||
0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x0EU, 0x42U, 0x00U, 0x10U, 0x81U, 0x80U,
|
||||
0x80U, 0x80U, 0x00U, 0x0BU, 0x0BU, 0x15U, 0x01U, 0x00U, 0x41U, 0x80U, 0x08U,
|
||||
0x0BU, 0x0EU, 0x48U, 0x6FU, 0x6FU, 0x6BU, 0x20U, 0x52U, 0x65U, 0x6AU, 0x65U,
|
||||
0x63U, 0x74U, 0x65U, 0x64U, 0x00U};
|
||||
|
||||
struct GenMint
|
||||
{
|
||||
std::optional<std::string> dest;
|
||||
|
||||
@@ -141,7 +141,9 @@ AMM::create(
|
||||
if (fee_ != 0)
|
||||
jv[sfFee] = std::to_string(fee_);
|
||||
else
|
||||
jv[jss::Fee] = std::to_string(env_.current()->fees().increment.drops());
|
||||
jv[jss::Fee] = std::to_string(
|
||||
env_.current()->fees().increment.drops() +
|
||||
env_.current()->fees().base.drops());
|
||||
submit(jv, seq, ter);
|
||||
|
||||
if (!ter || env_.ter() == tesSUCCESS)
|
||||
|
||||
@@ -180,7 +180,7 @@ AMMTest::reserve(jtx::Env& env, std::uint32_t count) const
|
||||
XRPAmount
|
||||
AMMTest::ammCrtFee(jtx::Env& env) const
|
||||
{
|
||||
return env.current()->fees().increment;
|
||||
return env.current()->fees().increment + env.current()->fees().base;
|
||||
}
|
||||
|
||||
jtx::Env
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <xrpl/server/Port.h>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/beast/http/dynamic_body.hpp>
|
||||
#include <boost/beast/http/error.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/read.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
@@ -74,11 +75,35 @@ class JSONRPCClient : public AbstractClient
|
||||
}
|
||||
|
||||
boost::asio::ip::tcp::endpoint ep_;
|
||||
boost::asio::io_service ios_;
|
||||
boost::asio::io_context ios_;
|
||||
boost::asio::ip::tcp::socket stream_;
|
||||
boost::beast::multi_buffer bin_;
|
||||
boost::beast::multi_buffer bout_;
|
||||
unsigned rpc_version_;
|
||||
unsigned reconnects_ = 0;
|
||||
|
||||
// Errors meaning the server closed the keep-alive connection, rather
|
||||
// than a failed request, so the request can be sent again on a fresh one.
|
||||
static bool
|
||||
droppedConnection(boost::system::error_code const& ec)
|
||||
{
|
||||
namespace error = boost::asio::error;
|
||||
return ec == boost::beast::http::error::end_of_stream ||
|
||||
ec == error::eof || ec == error::connection_reset ||
|
||||
ec == error::connection_aborted || ec == error::broken_pipe ||
|
||||
ec == error::not_connected;
|
||||
}
|
||||
|
||||
// Replace the dropped connection, discarding any bytes buffered from it.
|
||||
void
|
||||
reconnect()
|
||||
{
|
||||
boost::system::error_code ec;
|
||||
stream_.close(ec);
|
||||
bin_.clear();
|
||||
stream_.connect(ep_);
|
||||
++reconnects_;
|
||||
}
|
||||
|
||||
public:
|
||||
explicit JSONRPCClient(Config const& cfg, unsigned rpc_version)
|
||||
@@ -133,10 +158,30 @@ public:
|
||||
req.body() = to_string(jr);
|
||||
}
|
||||
req.prepare_payload();
|
||||
write(stream_, req);
|
||||
|
||||
// The connection is opened at construction and kept alive, but the
|
||||
// server closes it when the first request takes longer than
|
||||
// BaseHTTPPeer::timeoutSecondsLocal to arrive (test setup on a loaded
|
||||
// machine can), or when it idles past timeoutSeconds between requests.
|
||||
// If the server has closed it, reconnect and send the request once
|
||||
// more.
|
||||
response<dynamic_body> res;
|
||||
read(stream_, bin_, res);
|
||||
auto writeAndRead = [&] {
|
||||
write(stream_, req);
|
||||
read(stream_, bin_, res);
|
||||
};
|
||||
try
|
||||
{
|
||||
writeAndRead();
|
||||
}
|
||||
catch (boost::system::system_error const& e)
|
||||
{
|
||||
if (!droppedConnection(e.code()))
|
||||
throw;
|
||||
reconnect();
|
||||
res = {};
|
||||
writeAndRead();
|
||||
}
|
||||
|
||||
Json::Reader jr;
|
||||
Json::Value jv;
|
||||
@@ -153,6 +198,12 @@ public:
|
||||
{
|
||||
return rpc_version_;
|
||||
}
|
||||
|
||||
unsigned
|
||||
reconnects() const override
|
||||
{
|
||||
return reconnects_;
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<AbstractClient>
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/server/Port.h>
|
||||
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/beast/core/multi_buffer.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
|
||||
@@ -91,9 +95,11 @@ class WSClientImpl : public WSClient
|
||||
return s;
|
||||
}
|
||||
|
||||
boost::asio::io_service ios_;
|
||||
std::optional<boost::asio::io_service::work> work_;
|
||||
boost::asio::io_service::strand strand_;
|
||||
boost::asio::io_context ios_;
|
||||
std::optional<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>
|
||||
work_;
|
||||
boost::asio::strand<boost::asio::io_context::executor_type> strand_;
|
||||
std::thread thread_;
|
||||
boost::asio::ip::tcp::socket stream_;
|
||||
boost::beast::websocket::stream<boost::asio::ip::tcp::socket&> ws_;
|
||||
@@ -116,14 +122,24 @@ class WSClientImpl : public WSClient
|
||||
void
|
||||
cleanup()
|
||||
{
|
||||
ios_.post(strand_.wrap([this] {
|
||||
if (!peerClosed_)
|
||||
{
|
||||
ws_.async_close({}, strand_.wrap([&](error_code ec) {
|
||||
stream_.cancel(ec);
|
||||
}));
|
||||
}
|
||||
}));
|
||||
boost::asio::post(
|
||||
ios_, boost::asio::bind_executor(strand_, [this] {
|
||||
if (!peerClosed_)
|
||||
{
|
||||
ws_.async_close(
|
||||
{},
|
||||
boost::asio::bind_executor(strand_, [&](error_code) {
|
||||
try
|
||||
{
|
||||
stream_.cancel();
|
||||
}
|
||||
catch (boost::system::system_error const&)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
work_ = std::nullopt;
|
||||
thread_.join();
|
||||
}
|
||||
@@ -134,8 +150,8 @@ public:
|
||||
bool v2,
|
||||
unsigned rpc_version,
|
||||
std::unordered_map<std::string, std::string> const& headers = {})
|
||||
: work_(ios_)
|
||||
, strand_(ios_)
|
||||
: work_(std::in_place, boost::asio::make_work_guard(ios_))
|
||||
, strand_(boost::asio::make_strand(ios_))
|
||||
, thread_([&] { ios_.run(); })
|
||||
, stream_(ios_)
|
||||
, ws_(stream_)
|
||||
@@ -155,8 +171,12 @@ public:
|
||||
"/");
|
||||
ws_.async_read(
|
||||
rb_,
|
||||
strand_.wrap(std::bind(
|
||||
&WSClientImpl::on_read_msg, this, std::placeholders::_1)));
|
||||
boost::asio::bind_executor(
|
||||
strand_,
|
||||
std::bind(
|
||||
&WSClientImpl::on_read_msg,
|
||||
this,
|
||||
std::placeholders::_1)));
|
||||
}
|
||||
catch (std::exception&)
|
||||
{
|
||||
@@ -286,8 +306,10 @@ private:
|
||||
}
|
||||
ws_.async_read(
|
||||
rb_,
|
||||
strand_.wrap(std::bind(
|
||||
&WSClientImpl::on_read_msg, this, std::placeholders::_1)));
|
||||
boost::asio::bind_executor(
|
||||
strand_,
|
||||
std::bind(
|
||||
&WSClientImpl::on_read_msg, this, std::placeholders::_1)));
|
||||
}
|
||||
|
||||
// Called when the read op terminates
|
||||
|
||||
@@ -101,7 +101,12 @@ makeStubHookContext(
|
||||
hook::HookStateMap& stateMap)
|
||||
{
|
||||
auto& result = stubHookContext.result;
|
||||
auto hookParams = result.hookParams.value_or(
|
||||
// HookResult::hookParams is a const& that must outlive the returned
|
||||
// context, so a function-local copy would dangle. Same thread_local
|
||||
// workaround as stateMap below: each call resets it.
|
||||
thread_local std::map<std::vector<uint8_t>, std::vector<uint8_t>>
|
||||
hookParams;
|
||||
hookParams = result.hookParams.value_or(
|
||||
std::map<std::vector<uint8_t>, std::vector<uint8_t>>{});
|
||||
return hook::HookContext{
|
||||
.applyCtx = applyCtx,
|
||||
@@ -144,9 +149,11 @@ makeStubHookContext(
|
||||
.foreignStateSetDisabled = result.foreignStateSetDisabled,
|
||||
.executeAgainAsWeak = result.executeAgainAsWeak,
|
||||
.provisionalMeta = result.provisionalMeta,
|
||||
.foreignStateGrantCache = {},
|
||||
},
|
||||
.emitFailure = stubHookContext.emitFailure,
|
||||
.module = nullptr};
|
||||
.module = nullptr,
|
||||
.api_ = nullptr};
|
||||
}
|
||||
|
||||
// Original function - WARNING: stateMap reference may become dangling
|
||||
|
||||
@@ -21,13 +21,16 @@
|
||||
#include <xrpld/net/HTTPClient.h>
|
||||
#include <xrpld/net/RPCCall.h>
|
||||
#include <xrpl/basics/ByteUtilities.h>
|
||||
#include <xrpl/basics/make_SSLContext.h>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -41,8 +44,10 @@ namespace test {
|
||||
// Accepts connections and sends configurable HTTP responses.
|
||||
class MockHTTPServer
|
||||
{
|
||||
boost::asio::io_service ios_;
|
||||
std::unique_ptr<boost::asio::io_service::work> work_;
|
||||
boost::asio::io_context ios_;
|
||||
std::unique_ptr<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>
|
||||
work_;
|
||||
boost::asio::ip::tcp::acceptor acceptor_;
|
||||
std::thread thread_;
|
||||
std::atomic<bool> running_{true};
|
||||
@@ -73,11 +78,13 @@ class MockHTTPServer
|
||||
|
||||
public:
|
||||
MockHTTPServer()
|
||||
: work_(std::make_unique<boost::asio::io_service::work>(ios_))
|
||||
: work_(std::make_unique<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>(
|
||||
boost::asio::make_work_guard(ios_)))
|
||||
, acceptor_(
|
||||
ios_,
|
||||
boost::asio::ip::tcp::endpoint(
|
||||
boost::asio::ip::address::from_string("127.0.0.1"),
|
||||
boost::asio::ip::make_address("127.0.0.1"),
|
||||
0))
|
||||
{
|
||||
port_ = acceptor_.local_endpoint().port();
|
||||
@@ -244,7 +251,7 @@ private:
|
||||
{
|
||||
auto timer =
|
||||
std::make_shared<boost::asio::steady_timer>(ios_);
|
||||
timer->expires_from_now(std::chrono::milliseconds(delay));
|
||||
timer->expires_after(std::chrono::milliseconds(delay));
|
||||
timer->async_wait(
|
||||
[this, sock, timer](auto) { sendHTTPResponse(sock); });
|
||||
}
|
||||
@@ -323,6 +330,72 @@ private:
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Hold one TLS connection open, either before the handshake or after reading
|
||||
// the request. In particular, never answer the client's TLS close_notify.
|
||||
class SilentHTTPSServer
|
||||
{
|
||||
boost::asio::io_context ios_;
|
||||
std::shared_ptr<boost::asio::ssl::context> context_ = make_SSLContext("");
|
||||
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> socket_{
|
||||
ios_,
|
||||
*context_};
|
||||
boost::asio::ip::tcp::acceptor acceptor_{
|
||||
ios_,
|
||||
{boost::asio::ip::make_address("127.0.0.1"), 0}};
|
||||
boost::asio::streambuf request_;
|
||||
std::atomic<bool> accepted_{false};
|
||||
std::atomic<bool> receivedRequest_{false};
|
||||
std::thread thread_;
|
||||
|
||||
public:
|
||||
explicit SilentHTTPSServer(bool stallHandshake)
|
||||
{
|
||||
acceptor_.async_accept(
|
||||
socket_.next_layer(), [this, stallHandshake](auto ec) {
|
||||
if (ec)
|
||||
return;
|
||||
accepted_ = true;
|
||||
if (stallHandshake)
|
||||
return;
|
||||
socket_.async_handshake(
|
||||
boost::asio::ssl::stream_base::server, [this](auto ec) {
|
||||
if (ec)
|
||||
return;
|
||||
boost::asio::async_read_until(
|
||||
socket_,
|
||||
request_,
|
||||
"\r\n\r\n",
|
||||
[this](auto ec, auto) { receivedRequest_ = !ec; });
|
||||
});
|
||||
});
|
||||
thread_ = std::thread([this] { ios_.run(); });
|
||||
}
|
||||
|
||||
~SilentHTTPSServer()
|
||||
{
|
||||
ios_.stop();
|
||||
thread_.join();
|
||||
}
|
||||
|
||||
unsigned short
|
||||
port() const
|
||||
{
|
||||
return acceptor_.local_endpoint().port();
|
||||
}
|
||||
|
||||
bool
|
||||
accepted() const
|
||||
{
|
||||
return accepted_;
|
||||
}
|
||||
|
||||
bool
|
||||
receivedRequest() const
|
||||
{
|
||||
return receivedRequest_;
|
||||
}
|
||||
};
|
||||
|
||||
class HTTPClient_test : public beast::unit_test::suite
|
||||
{
|
||||
// Poll until cond() holds or the timeout elapses. The mock server now
|
||||
@@ -352,7 +425,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
// Helper: fire an HTTP request and track completion via atomic counter.
|
||||
void
|
||||
fireRequest(
|
||||
boost::asio::io_service& ios,
|
||||
boost::asio::io_context& ios,
|
||||
std::string const& host,
|
||||
unsigned short port,
|
||||
std::atomic<int>& completed,
|
||||
@@ -404,7 +477,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
fireRequest(ios, "127.0.0.1", server.port(), completed, j);
|
||||
ios.run();
|
||||
}
|
||||
@@ -431,7 +504,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
fireRequest(ios, "127.0.0.1", server.port(), completed, j);
|
||||
ios.run();
|
||||
}
|
||||
@@ -449,11 +522,11 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
Env env{*this};
|
||||
|
||||
// Bind a port, then close it — guarantees nothing is listening.
|
||||
boost::asio::io_service tmp;
|
||||
boost::asio::io_context tmp;
|
||||
boost::asio::ip::tcp::acceptor acc(
|
||||
tmp,
|
||||
boost::asio::ip::tcp::endpoint(
|
||||
boost::asio::ip::address::from_string("127.0.0.1"), 0));
|
||||
boost::asio::ip::make_address("127.0.0.1"), 0));
|
||||
auto port = acc.local_endpoint().port();
|
||||
acc.close();
|
||||
|
||||
@@ -461,7 +534,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
fireRequest(ios, "127.0.0.1", port, completed, j);
|
||||
ios.run();
|
||||
}
|
||||
@@ -488,7 +561,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
// Short timeout to keep the test fast.
|
||||
fireRequest(
|
||||
ios,
|
||||
@@ -504,6 +577,82 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(completed == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testFallbackAfterConnectionRefused()
|
||||
{
|
||||
testcase("Try the next host after connection refused");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
MockHTTPServer server;
|
||||
server.setResponseBody("fallback");
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
boost::asio::io_context ios;
|
||||
// The mock server listens only on 127.0.0.1, the second entry in the
|
||||
// fallback list; nothing listens on its port at ::1, so the first
|
||||
// connect fails (refused, or unavailable where IPv6 is off). Not
|
||||
// another 127/8 address: macOS configures only 127.0.0.1.
|
||||
int completions = 0;
|
||||
HTTPClient::get(
|
||||
false,
|
||||
ios,
|
||||
std::deque<std::string>{"::1", "127.0.0.1"},
|
||||
server.port(),
|
||||
"/",
|
||||
megabytes(1),
|
||||
std::chrono::seconds{1},
|
||||
[&](auto const& ec, int status, std::string const& body) {
|
||||
++completions;
|
||||
BEAST_EXPECT(!ec);
|
||||
BEAST_EXPECT(status == 200);
|
||||
BEAST_EXPECT(body == "fallback");
|
||||
return false;
|
||||
},
|
||||
j);
|
||||
ios.run_for(std::chrono::seconds{4});
|
||||
BEAST_EXPECT(ios.stopped());
|
||||
BEAST_EXPECT(completions == 1);
|
||||
BEAST_EXPECT(server.totalAcceptedCount() == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testTLSDeadline()
|
||||
{
|
||||
for (bool stallHandshake : {false, true})
|
||||
{
|
||||
testcase(
|
||||
stallHandshake ? "Deadline during TLS handshake"
|
||||
: "Deadline while TLS peer never responds");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
SilentHTTPSServer server(stallHandshake);
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
boost::asio::io_context ios;
|
||||
int completions = 0;
|
||||
HTTPClient::get(
|
||||
true,
|
||||
ios,
|
||||
"127.0.0.1",
|
||||
server.port(),
|
||||
"/",
|
||||
megabytes(1),
|
||||
std::chrono::seconds{1},
|
||||
[&](auto const& ec, int, std::string const&) {
|
||||
++completions;
|
||||
BEAST_EXPECT(ec == boost::asio::error::timed_out);
|
||||
return false;
|
||||
},
|
||||
j);
|
||||
// Bound a regression: graceful SSL shutdown can otherwise wait
|
||||
// forever for this server, preventing io_context::run returning.
|
||||
ios.run_for(std::chrono::seconds{4});
|
||||
BEAST_EXPECT(server.accepted());
|
||||
if (!stallHandshake)
|
||||
BEAST_EXPECT(server.receivedRequest());
|
||||
BEAST_EXPECT(completions == 1);
|
||||
BEAST_EXPECT(ios.stopped());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testReadErrorDuringBody()
|
||||
{
|
||||
@@ -525,7 +674,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
fireRequest(
|
||||
ios,
|
||||
"127.0.0.1",
|
||||
@@ -557,7 +706,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
fireRequest(ios, "127.0.0.1", server.port(), completed, j);
|
||||
ios.run();
|
||||
}
|
||||
@@ -596,7 +745,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
fireRequest(
|
||||
ios,
|
||||
"127.0.0.1",
|
||||
@@ -641,7 +790,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
for (int i = 0; i < N; ++i)
|
||||
{
|
||||
fireRequest(ios, "127.0.0.1", server.port(), completed, j);
|
||||
@@ -679,7 +828,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
for (int i = 0; i < N; ++i)
|
||||
{
|
||||
fireRequest(ios, "127.0.0.1", server.port(), completed, j);
|
||||
@@ -724,7 +873,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
fireRequest(
|
||||
ios,
|
||||
"127.0.0.1",
|
||||
@@ -772,7 +921,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
HTTPClient::request(
|
||||
false,
|
||||
ios,
|
||||
@@ -823,7 +972,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
HTTPClient::request(
|
||||
false,
|
||||
ios,
|
||||
@@ -876,7 +1025,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
|
||||
try
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
Json::Value params(Json::arrayValue);
|
||||
RPCCall::fromNetwork(
|
||||
ios,
|
||||
@@ -925,7 +1074,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
|
||||
try
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
Json::Value params(Json::arrayValue);
|
||||
RPCCall::fromNetwork(
|
||||
ios,
|
||||
@@ -979,8 +1128,10 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
// Persistent io_service — stays alive the whole test.
|
||||
boost::asio::io_service ios;
|
||||
auto work = std::make_unique<boost::asio::io_service::work>(ios);
|
||||
boost::asio::io_context ios;
|
||||
auto work = std::make_unique<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>(
|
||||
boost::asio::make_work_guard(ios));
|
||||
std::thread runner([&ios] { ios.run(); });
|
||||
|
||||
// Fire request on the persistent io_service.
|
||||
@@ -1048,8 +1199,10 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
std::atomic<int> completed{0};
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
boost::asio::io_service ios;
|
||||
auto work = std::make_unique<boost::asio::io_service::work>(ios);
|
||||
boost::asio::io_context ios;
|
||||
auto work = std::make_unique<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>(
|
||||
boost::asio::make_work_guard(ios));
|
||||
std::thread runner([&ios] { ios.run(); });
|
||||
|
||||
for (int i = 0; i < N; ++i)
|
||||
@@ -1124,7 +1277,7 @@ class HTTPClient_test : public beast::unit_test::suite
|
||||
auto j = env.app().journal("HTTPClient");
|
||||
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
HTTPClient::get(
|
||||
false, // no SSL
|
||||
ios,
|
||||
@@ -1165,6 +1318,8 @@ public:
|
||||
testCleanupAfter500();
|
||||
testCleanupAfterConnectionRefused();
|
||||
testCleanupAfterTimeout();
|
||||
testFallbackAfterConnectionRefused();
|
||||
testTLSDeadline();
|
||||
testReadErrorDuringBody();
|
||||
testCleanupAfterServerCloseBeforeResponse();
|
||||
testEOFCompletionCallsCallback();
|
||||
|
||||
@@ -44,8 +44,10 @@ namespace test {
|
||||
// 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::io_context ios_;
|
||||
std::unique_ptr<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>
|
||||
work_;
|
||||
boost::asio::ip::tcp::acceptor acceptor_;
|
||||
std::thread thread_;
|
||||
unsigned short port_;
|
||||
@@ -56,11 +58,13 @@ class MockWebhookEndpoint
|
||||
|
||||
public:
|
||||
MockWebhookEndpoint()
|
||||
: work_(std::make_unique<boost::asio::io_service::work>(ios_))
|
||||
: work_(std::make_unique<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>(
|
||||
boost::asio::make_work_guard(ios_)))
|
||||
, acceptor_(
|
||||
ios_,
|
||||
boost::asio::ip::tcp::endpoint(
|
||||
boost::asio::ip::address::from_string("127.0.0.1"),
|
||||
boost::asio::ip::make_address("127.0.0.1"),
|
||||
0))
|
||||
{
|
||||
port_ = acceptor_.local_endpoint().port();
|
||||
@@ -134,7 +138,7 @@ private:
|
||||
{
|
||||
auto timer =
|
||||
std::make_shared<boost::asio::steady_timer>(ios_);
|
||||
timer->expires_from_now(std::chrono::milliseconds(delay));
|
||||
timer->expires_after(std::chrono::milliseconds(delay));
|
||||
timer->async_wait(
|
||||
[this, sock, timer](auto) { reply(sock); });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
#include <test/jtx.h>
|
||||
|
||||
#include <xrpld/overlay/detail/ConnectAttempt.h>
|
||||
#include <xrpld/overlay/detail/OverlayImpl.h>
|
||||
#include <xrpld/overlay/detail/PeerImp.h>
|
||||
#include <xrpl/basics/make_SSLContext.h>
|
||||
#include <xrpl/beast/net/IPAddressConversion.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class OverlayLifecycle_test : public beast::unit_test::suite
|
||||
{
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
using error_code = boost::system::error_code;
|
||||
using stream_type = boost::beast::ssl_stream<boost::beast::tcp_stream>;
|
||||
|
||||
std::shared_ptr<boost::asio::ssl::context> context_ = make_SSLContext("");
|
||||
|
||||
enum class Behavior {
|
||||
abortHandshake,
|
||||
stallHandshake,
|
||||
reject,
|
||||
stallResponse
|
||||
};
|
||||
|
||||
// A single local endpoint on the same explicitly pumped context as the
|
||||
// real ConnectAttempt. No server thread or external network is needed.
|
||||
struct Listener
|
||||
{
|
||||
Behavior behavior;
|
||||
tcp::acceptor acceptor;
|
||||
boost::asio::ssl::stream<tcp::socket> stream;
|
||||
boost::beast::flat_buffer buffer;
|
||||
boost::beast::http::request<boost::beast::http::empty_body> request;
|
||||
boost::beast::http::response<boost::beast::http::string_body> response;
|
||||
std::array<char, 4096> drainBuffer;
|
||||
bool accepted = false;
|
||||
bool handshakeBytes = false;
|
||||
bool handshaken = false;
|
||||
bool requested = false;
|
||||
bool closedByClient = false;
|
||||
|
||||
Listener(
|
||||
boost::asio::io_context& ioc,
|
||||
boost::asio::ssl::context& context,
|
||||
Behavior mode)
|
||||
: behavior(mode)
|
||||
, acceptor(ioc, {boost::asio::ip::make_address("127.0.0.1"), 0})
|
||||
, stream(ioc, context)
|
||||
{
|
||||
acceptor.async_accept(stream.next_layer(), [this](error_code ec) {
|
||||
if (ec)
|
||||
return;
|
||||
accepted = true;
|
||||
if (behavior == Behavior::abortHandshake)
|
||||
{
|
||||
stream.next_layer().close(ec);
|
||||
return;
|
||||
}
|
||||
if (behavior == Behavior::stallHandshake)
|
||||
return drain();
|
||||
stream.async_handshake(
|
||||
boost::asio::ssl::stream_base::server,
|
||||
[this](error_code ec) {
|
||||
if (ec)
|
||||
return;
|
||||
handshaken = true;
|
||||
boost::beast::http::async_read(
|
||||
stream,
|
||||
buffer,
|
||||
request,
|
||||
[this](error_code ec, std::size_t) {
|
||||
if (ec)
|
||||
return;
|
||||
requested = true;
|
||||
if (behavior == Behavior::stallResponse)
|
||||
return drain();
|
||||
response.version(11);
|
||||
response.result(boost::beast::http::status::
|
||||
service_unavailable);
|
||||
response.body() = R"({"peer-ips":[]})";
|
||||
response.prepare_payload();
|
||||
boost::beast::http::async_write(
|
||||
stream,
|
||||
response,
|
||||
[this](error_code ec, std::size_t) {
|
||||
if (!ec)
|
||||
drain();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
tcp::endpoint
|
||||
endpoint() const
|
||||
{
|
||||
return acceptor.local_endpoint();
|
||||
}
|
||||
|
||||
void
|
||||
drain()
|
||||
{
|
||||
// Consume transport bytes without sending any TLS or HTTP reply.
|
||||
// EOF/reset proves that the attempt itself closed the connection.
|
||||
stream.next_layer().async_read_some(
|
||||
boost::asio::buffer(drainBuffer),
|
||||
[this](error_code ec, std::size_t n) {
|
||||
if (ec)
|
||||
{
|
||||
closedByClient = true;
|
||||
return;
|
||||
}
|
||||
handshakeBytes = handshakeBytes || n != 0;
|
||||
drain();
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
stop()
|
||||
{
|
||||
error_code ec;
|
||||
acceptor.close(ec);
|
||||
stream.next_layer().close(ec);
|
||||
}
|
||||
};
|
||||
|
||||
template <class Predicate>
|
||||
bool
|
||||
runUntil(boost::asio::io_context& ioc, Predicate const& ready)
|
||||
{
|
||||
ioc.restart();
|
||||
auto const limit =
|
||||
std::chrono::steady_clock::now() + std::chrono::seconds{5};
|
||||
while (!ready() && !ioc.stopped() &&
|
||||
std::chrono::steady_clock::now() < limit)
|
||||
ioc.run_one_until(limit);
|
||||
return ready();
|
||||
}
|
||||
|
||||
std::shared_ptr<ConnectAttempt>
|
||||
makeAttempt(
|
||||
jtx::Env& env,
|
||||
boost::asio::io_context& ioc,
|
||||
tcp::endpoint endpoint)
|
||||
{
|
||||
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
|
||||
auto const remote = beast::IPAddressConversion::from_asio(endpoint);
|
||||
auto slot = overlay.peerFinder().new_outbound_slot(remote);
|
||||
if (!BEAST_EXPECT(slot != nullptr))
|
||||
return {};
|
||||
return std::make_shared<ConnectAttempt>(
|
||||
env.app(),
|
||||
ioc,
|
||||
endpoint,
|
||||
overlay.resourceManager().newOutboundEndpoint(remote),
|
||||
context_,
|
||||
100000,
|
||||
slot,
|
||||
env.app().journal("Peer"),
|
||||
overlay);
|
||||
}
|
||||
|
||||
static void
|
||||
stopAttempt(std::weak_ptr<ConnectAttempt> const& weak)
|
||||
{
|
||||
if (auto attempt = weak.lock())
|
||||
attempt->stop();
|
||||
}
|
||||
|
||||
void
|
||||
testRefused()
|
||||
{
|
||||
testcase("refused connection releases its attempt");
|
||||
jtx::Env env{*this};
|
||||
boost::asio::io_context ioc;
|
||||
// Hold a port with a connected socket; no close/rebind race. A bound
|
||||
// but unlistened port is refused only on Linux: BSD stacks (macOS)
|
||||
// drop the SYN and the connect hangs.
|
||||
tcp::acceptor listener(
|
||||
ioc, {boost::asio::ip::make_address("127.0.0.1"), 0});
|
||||
tcp::socket held(ioc);
|
||||
held.connect(listener.local_endpoint());
|
||||
auto attempt = makeAttempt(env, ioc, held.local_endpoint());
|
||||
if (!attempt)
|
||||
return;
|
||||
std::weak_ptr<ConnectAttempt> weak = attempt;
|
||||
attempt->run();
|
||||
attempt.reset();
|
||||
ioc.run_for(std::chrono::seconds{5});
|
||||
BEAST_EXPECT(weak.expired());
|
||||
BEAST_EXPECT(ioc.stopped());
|
||||
stopAttempt(weak);
|
||||
ioc.restart();
|
||||
ioc.run_for(std::chrono::seconds{1});
|
||||
}
|
||||
|
||||
void
|
||||
testRejected(Behavior behavior)
|
||||
{
|
||||
testcase(
|
||||
behavior == Behavior::reject ? "TLS peer refuses protocol upgrade"
|
||||
: "peer closes during TLS handshake");
|
||||
jtx::Env env{*this};
|
||||
boost::asio::io_context ioc;
|
||||
Listener server(ioc, *context_, behavior);
|
||||
auto attempt = makeAttempt(env, ioc, server.endpoint());
|
||||
if (!attempt)
|
||||
return;
|
||||
std::weak_ptr<ConnectAttempt> weak = attempt;
|
||||
attempt->run();
|
||||
attempt.reset();
|
||||
ioc.run_for(std::chrono::seconds{5});
|
||||
BEAST_EXPECT(server.accepted);
|
||||
if (behavior == Behavior::reject)
|
||||
{
|
||||
BEAST_EXPECT(server.handshaken);
|
||||
BEAST_EXPECT(server.requested);
|
||||
BEAST_EXPECT(server.request["Connect-As"] == "Peer");
|
||||
BEAST_EXPECT(server.closedByClient);
|
||||
}
|
||||
BEAST_EXPECT(weak.expired());
|
||||
BEAST_EXPECT(ioc.stopped());
|
||||
stopAttempt(weak);
|
||||
server.stop();
|
||||
ioc.restart();
|
||||
ioc.run_for(std::chrono::seconds{1});
|
||||
}
|
||||
|
||||
void
|
||||
testStop()
|
||||
{
|
||||
testcase("stop cancels a pending TLS handshake and timer");
|
||||
jtx::Env env{*this};
|
||||
boost::asio::io_context ioc;
|
||||
Listener server(ioc, *context_, Behavior::stallHandshake);
|
||||
auto attempt = makeAttempt(env, ioc, server.endpoint());
|
||||
if (!attempt)
|
||||
return;
|
||||
std::weak_ptr<ConnectAttempt> weak = attempt;
|
||||
attempt->run();
|
||||
BEAST_EXPECT(runUntil(ioc, [&] { return server.handshakeBytes; }));
|
||||
// Exercise the off-strand post and repeated stop as well as cancel.
|
||||
attempt->stop();
|
||||
attempt->stop();
|
||||
attempt.reset();
|
||||
ioc.restart();
|
||||
ioc.run_for(std::chrono::seconds{5});
|
||||
BEAST_EXPECT(server.closedByClient);
|
||||
BEAST_EXPECT(weak.expired());
|
||||
BEAST_EXPECT(ioc.stopped());
|
||||
server.stop();
|
||||
}
|
||||
|
||||
void
|
||||
testDeadlines()
|
||||
{
|
||||
testcase("stalled handshake and HTTP response expire");
|
||||
jtx::Env env{*this};
|
||||
boost::asio::io_context ioc;
|
||||
Listener handshake(ioc, *context_, Behavior::stallHandshake);
|
||||
Listener response(ioc, *context_, Behavior::stallResponse);
|
||||
auto first = makeAttempt(env, ioc, handshake.endpoint());
|
||||
auto second = makeAttempt(env, ioc, response.endpoint());
|
||||
if (!first || !second)
|
||||
return;
|
||||
std::weak_ptr<ConnectAttempt> firstWeak = first;
|
||||
std::weak_ptr<ConnectAttempt> secondWeak = second;
|
||||
first->run();
|
||||
second->run();
|
||||
first.reset();
|
||||
second.reset();
|
||||
// Exercise the real 15-second timers concurrently. The upper bound
|
||||
// keeps a missing wait from hanging the suite indefinitely.
|
||||
ioc.run_for(std::chrono::seconds{20});
|
||||
BEAST_EXPECT(handshake.handshakeBytes);
|
||||
BEAST_EXPECT(response.handshaken);
|
||||
BEAST_EXPECT(response.requested);
|
||||
BEAST_EXPECT(handshake.closedByClient);
|
||||
BEAST_EXPECT(response.closedByClient);
|
||||
BEAST_EXPECT(firstWeak.expired());
|
||||
BEAST_EXPECT(secondWeak.expired());
|
||||
BEAST_EXPECT(ioc.stopped());
|
||||
stopAttempt(firstWeak);
|
||||
stopAttempt(secondWeak);
|
||||
handshake.stop();
|
||||
response.stop();
|
||||
ioc.restart();
|
||||
ioc.run_for(std::chrono::seconds{1});
|
||||
}
|
||||
|
||||
void
|
||||
testPeer(bool graceful)
|
||||
{
|
||||
testcase(
|
||||
graceful ? "peer handles TLS close and cancels its timer"
|
||||
: "peer stop closes once and releases pending work");
|
||||
jtx::Env env{*this};
|
||||
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
|
||||
boost::asio::io_context ioc;
|
||||
tcp::acceptor acceptor(
|
||||
ioc, {boost::asio::ip::make_address("127.0.0.1"), 0});
|
||||
boost::asio::ssl::stream<tcp::socket> remote(ioc, *context_);
|
||||
auto stream = std::make_unique<stream_type>(ioc, *context_);
|
||||
stream->next_layer().socket().connect(acceptor.local_endpoint());
|
||||
acceptor.accept(remote.next_layer());
|
||||
auto const endpoint =
|
||||
beast::IPAddressConversion::from_asio(acceptor.local_endpoint());
|
||||
auto const local = beast::IPAddressConversion::from_asio(
|
||||
stream->next_layer().socket().local_endpoint());
|
||||
int handshakes = 0;
|
||||
remote.async_handshake(
|
||||
boost::asio::ssl::stream_base::server, [&](error_code ec) {
|
||||
BEAST_EXPECT(!ec);
|
||||
if (!ec)
|
||||
++handshakes;
|
||||
});
|
||||
stream->async_handshake(
|
||||
boost::asio::ssl::stream_base::client, [&](error_code ec) {
|
||||
BEAST_EXPECT(!ec);
|
||||
if (!ec)
|
||||
++handshakes;
|
||||
});
|
||||
ioc.run_for(std::chrono::seconds{5});
|
||||
if (!BEAST_EXPECT(handshakes == 2))
|
||||
return;
|
||||
|
||||
auto slot = overlay.peerFinder().new_outbound_slot(endpoint);
|
||||
if (!BEAST_EXPECT(slot != nullptr))
|
||||
return;
|
||||
if (!BEAST_EXPECT(overlay.peerFinder().onConnected(slot, local)))
|
||||
{
|
||||
overlay.peerFinder().on_closed(slot);
|
||||
return;
|
||||
}
|
||||
PublicKey key(std::get<0>(randomKeyPair(KeyType::ed25519)));
|
||||
// Reserve the fixture peer independently of the standalone app's
|
||||
// automatic outbound-peer target.
|
||||
if (!BEAST_EXPECT(
|
||||
overlay.peerFinder().activate(slot, key, true) ==
|
||||
PeerFinder::Result::success))
|
||||
{
|
||||
overlay.peerFinder().on_closed(slot);
|
||||
return;
|
||||
}
|
||||
auto const disconnects = overlay.getPeerDisconnect();
|
||||
auto peer = std::make_shared<PeerImp>(
|
||||
env.app(),
|
||||
std::move(stream),
|
||||
boost::asio::const_buffer{},
|
||||
std::move(slot),
|
||||
http_response_type{},
|
||||
overlay.resourceManager().newOutboundEndpoint(endpoint),
|
||||
key,
|
||||
ProtocolVersion{1, 7},
|
||||
100000,
|
||||
overlay);
|
||||
std::weak_ptr<PeerImp> weak = peer;
|
||||
overlay.add_active(peer);
|
||||
int shutdowns = 0;
|
||||
if (graceful)
|
||||
{
|
||||
remote.async_shutdown([&](error_code) { ++shutdowns; });
|
||||
}
|
||||
else
|
||||
{
|
||||
peer->stop();
|
||||
peer->stop();
|
||||
}
|
||||
peer.reset();
|
||||
ioc.restart();
|
||||
ioc.run_for(std::chrono::seconds{5});
|
||||
if (graceful)
|
||||
BEAST_EXPECT(shutdowns == 1);
|
||||
BEAST_EXPECT(weak.expired());
|
||||
BEAST_EXPECT(overlay.size() == 0);
|
||||
BEAST_EXPECT(overlay.getPeerDisconnect() == disconnects + 1);
|
||||
BEAST_EXPECT(ioc.stopped());
|
||||
if (auto remaining = weak.lock())
|
||||
remaining->stop();
|
||||
error_code ec;
|
||||
remote.next_layer().close(ec);
|
||||
ioc.restart();
|
||||
ioc.run_for(std::chrono::seconds{1});
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testRefused();
|
||||
testRejected(Behavior::abortHandshake);
|
||||
testRejected(Behavior::reject);
|
||||
testStop();
|
||||
testDeadlines();
|
||||
testPeer(false);
|
||||
testPeer(true);
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(OverlayLifecycle, overlay, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -485,7 +485,7 @@ public:
|
||||
};
|
||||
auto handshake = [&](int outboundEnable, int inboundEnable) {
|
||||
beast::IP::Address addr =
|
||||
boost::asio::ip::address::from_string("172.1.1.100");
|
||||
boost::asio::ip::make_address("172.1.1.100");
|
||||
|
||||
auto env = getEnv(outboundEnable);
|
||||
auto request = ripple::makeRequest(
|
||||
|
||||
@@ -1510,7 +1510,7 @@ vp_squelched=1
|
||||
};
|
||||
auto handshake = [&](int outboundEnable, int inboundEnable) {
|
||||
beast::IP::Address addr =
|
||||
boost::asio::ip::address::from_string("172.1.1.100");
|
||||
boost::asio::ip::make_address("172.1.1.100");
|
||||
|
||||
setEnv(outboundEnable);
|
||||
auto request = ripple::makeRequest(
|
||||
|
||||
@@ -22,8 +22,13 @@
|
||||
#include <xrpl/beast/core/CurrentThreadName.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/bind_executor.hpp>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/read_until.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/asio/streambuf.hpp>
|
||||
#include <boost/utility/in_place_factory.hpp>
|
||||
|
||||
#include <condition_variable>
|
||||
@@ -50,7 +55,7 @@ class short_read_test : public beast::unit_test::suite
|
||||
{
|
||||
private:
|
||||
using io_context_type = boost::asio::io_context;
|
||||
using strand_type = boost::asio::io_context::strand;
|
||||
using strand_type = boost::asio::strand<io_context_type::executor_type>;
|
||||
using timer_type =
|
||||
boost::asio::basic_waitable_timer<std::chrono::steady_clock>;
|
||||
using acceptor_type = boost::asio::ip::tcp::acceptor;
|
||||
@@ -61,7 +66,8 @@ private:
|
||||
using address_type = boost::asio::ip::address;
|
||||
|
||||
io_context_type io_context_;
|
||||
std::optional<boost::asio::executor_work_guard<boost::asio::executor>>
|
||||
boost::optional<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>
|
||||
work_;
|
||||
std::thread thread_;
|
||||
std::shared_ptr<boost::asio::ssl::context> context_;
|
||||
@@ -73,7 +79,7 @@ private:
|
||||
using boost::asio::buffer;
|
||||
using boost::asio::buffer_copy;
|
||||
using boost::asio::buffer_size;
|
||||
boost::asio::const_buffers_1 buf(s.data(), s.size());
|
||||
boost::asio::const_buffer buf(s.data(), s.size());
|
||||
sb.commit(buffer_copy(sb.prepare(buffer_size(buf)), buf));
|
||||
}
|
||||
|
||||
@@ -186,11 +192,11 @@ private:
|
||||
, acceptor_(
|
||||
test_.io_context_,
|
||||
endpoint_type(
|
||||
beast::IP::Address::from_string(
|
||||
boost::asio::ip::make_address(
|
||||
test::getEnvLocalhostAddr()),
|
||||
0))
|
||||
, socket_(test_.io_context_)
|
||||
, strand_(test_.io_context_)
|
||||
, strand_(boost::asio::make_strand(test_.io_context_))
|
||||
{
|
||||
acceptor_.listen();
|
||||
server_.endpoint_ = acceptor_.local_endpoint();
|
||||
@@ -266,7 +272,7 @@ private:
|
||||
, test_(server_.test_)
|
||||
, socket_(std::move(socket))
|
||||
, stream_(socket_, *test_.context_)
|
||||
, strand_(test_.io_context_)
|
||||
, strand_(boost::asio::make_strand(test_.io_context_))
|
||||
, timer_(test_.io_context_)
|
||||
{
|
||||
}
|
||||
@@ -288,7 +294,7 @@ private:
|
||||
void
|
||||
run()
|
||||
{
|
||||
timer_.expires_from_now(std::chrono::seconds(3));
|
||||
timer_.expires_after(std::chrono::seconds(3));
|
||||
timer_.async_wait(bind_executor(
|
||||
strand_,
|
||||
std::bind(
|
||||
@@ -451,7 +457,7 @@ private:
|
||||
, test_(client_.test_)
|
||||
, socket_(test_.io_context_)
|
||||
, stream_(socket_, *test_.context_)
|
||||
, strand_(test_.io_context_)
|
||||
, strand_(boost::asio::make_strand(test_.io_context_))
|
||||
, timer_(test_.io_context_)
|
||||
, ep_(ep)
|
||||
{
|
||||
@@ -474,7 +480,7 @@ private:
|
||||
void
|
||||
run(endpoint_type const& ep)
|
||||
{
|
||||
timer_.expires_from_now(std::chrono::seconds(3));
|
||||
timer_.expires_after(std::chrono::seconds(3));
|
||||
timer_.async_wait(bind_executor(
|
||||
strand_,
|
||||
std::bind(
|
||||
|
||||
@@ -171,13 +171,13 @@ private:
|
||||
makeFeaturesRequestHeader(false, false, true, false))
|
||||
: (void)nDisabled--;
|
||||
auto stream_ptr = std::make_unique<stream_type>(
|
||||
socket_type(std::forward<boost::asio::io_service&>(
|
||||
env.app().getIOService())),
|
||||
socket_type(std::forward<boost::asio::io_context&>(
|
||||
env.app().getIOContext())),
|
||||
*context_);
|
||||
beast::IP::Endpoint local(
|
||||
beast::IP::Address::from_string("172.1.1." + std::to_string(lid_)));
|
||||
boost::asio::ip::make_address("172.1.1." + std::to_string(lid_)));
|
||||
beast::IP::Endpoint remote(
|
||||
beast::IP::Address::from_string("172.1.1." + std::to_string(rid_)));
|
||||
boost::asio::ip::make_address("172.1.1." + std::to_string(rid_)));
|
||||
PublicKey key(std::get<0>(randomKeyPair(KeyType::ed25519)));
|
||||
auto consumer = overlay.resourceManager().newInboundEndpoint(remote);
|
||||
auto slot = overlay.peerFinder().new_inbound_slot(local, remote);
|
||||
|
||||
@@ -690,14 +690,8 @@ public:
|
||||
BEAST_EXPECT(ledgerKB == 0); // < 1024 bytes rounds to 0 KB
|
||||
BEAST_EXPECT(txKB == 0); // < 1024 bytes rounds to 0 KB
|
||||
}
|
||||
else
|
||||
{
|
||||
// SQLite reports cache/engine memory which has overhead even when
|
||||
// empty Just verify the functions return reasonable values
|
||||
BEAST_EXPECT(allKB >= 0);
|
||||
BEAST_EXPECT(ledgerKB >= 0);
|
||||
BEAST_EXPECT(txKB >= 0);
|
||||
}
|
||||
// SQLite reports cache/engine memory which has overhead even when
|
||||
// empty, so no initial-size assertion is possible there.
|
||||
|
||||
// Create some data and verify size increases
|
||||
Account alice("alice");
|
||||
@@ -713,10 +707,7 @@ public:
|
||||
// RWDB reports actual data memory
|
||||
// After adding data, should see some increase
|
||||
BEAST_EXPECT(newAllKB >= 1); // Should have at least 1KB total
|
||||
BEAST_EXPECT(
|
||||
newTxKB >= 0); // Transactions added (might still be < 1KB)
|
||||
BEAST_EXPECT(
|
||||
newLedgerKB >= 0); // Ledger data (might still be < 1KB)
|
||||
// newTxKB / newLedgerKB might still be < 1KB (unsigned, no check)
|
||||
|
||||
// Key relationships
|
||||
BEAST_EXPECT(newAllKB >= newLedgerKB + newTxKB); // Total >= parts
|
||||
|
||||
@@ -37,20 +37,36 @@ namespace test {
|
||||
|
||||
static char const* bobs_account_objects[] = {
|
||||
R"json({
|
||||
"Account" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
|
||||
"BookDirectory" : "B025997A323F5C3E03DDF1334471F5984ABDE31C59D463525D038D7EA4C68000",
|
||||
"BookNode" : "0",
|
||||
"Flags" : 65536,
|
||||
"LedgerEntryType" : "Offer",
|
||||
"OwnerNode" : "0",
|
||||
"Sequence" : 4,
|
||||
"TakerGets" : {
|
||||
"currency" : "USD",
|
||||
"issuer" : "r32rQHyesiTtdWFU7UJVtff4nCR5SHCbJW",
|
||||
"value" : "1"
|
||||
},
|
||||
"TakerPays" : "100000000",
|
||||
"index" : "A984D036A0E562433A8377CA57D1A1E056E58C0D04818F8DFD3A1AA3F217DD82"
|
||||
"Account" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
|
||||
"BookDirectory" : "50AD0A9E54D2B381288D535EB724E4275FFBF41580D28A925D038D7EA4C68000",
|
||||
"BookNode" : "0",
|
||||
"Flags" : 65536,
|
||||
"LedgerEntryType" : "Offer",
|
||||
"OwnerNode" : "0",
|
||||
"Sequence" : 4,
|
||||
"TakerGets" : {
|
||||
"currency" : "USD",
|
||||
"issuer" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
|
||||
"value" : "1"
|
||||
},
|
||||
"TakerPays" : "100000000",
|
||||
"index" : "A984D036A0E562433A8377CA57D1A1E056E58C0D04818F8DFD3A1AA3F217DD82"
|
||||
})json",
|
||||
R"json({
|
||||
"Account" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
|
||||
"BookDirectory" : "B025997A323F5C3E03DDF1334471F5984ABDE31C59D463525D038D7EA4C68000",
|
||||
"BookNode" : "0",
|
||||
"Flags" : 65536,
|
||||
"LedgerEntryType" : "Offer",
|
||||
"OwnerNode" : "0",
|
||||
"Sequence" : 5,
|
||||
"TakerGets" : {
|
||||
"currency" : "USD",
|
||||
"issuer" : "r32rQHyesiTtdWFU7UJVtff4nCR5SHCbJW",
|
||||
"value" : "1"
|
||||
},
|
||||
"TakerPays" : "100000000",
|
||||
"index" : "CAFE32332D752387B01083B60CC63069BA4A969C9730836929F841450F6A718E"
|
||||
})json",
|
||||
R"json({
|
||||
"Balance" : {
|
||||
@@ -95,22 +111,6 @@ static char const* bobs_account_objects[] = {
|
||||
},
|
||||
"LowNode" : "0",
|
||||
"index" : "D89BC239086183EB9458C396E643795C1134963E6550E682A190A5F021766D43"
|
||||
})json",
|
||||
R"json({
|
||||
"Account" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
|
||||
"BookDirectory" : "50AD0A9E54D2B381288D535EB724E4275FFBF41580D28A925D038D7EA4C68000",
|
||||
"BookNode" : "0",
|
||||
"Flags" : 65536,
|
||||
"LedgerEntryType" : "Offer",
|
||||
"OwnerNode" : "0",
|
||||
"Sequence" : 3,
|
||||
"TakerGets" : {
|
||||
"currency" : "USD",
|
||||
"issuer" : "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK",
|
||||
"value" : "1"
|
||||
},
|
||||
"TakerPays" : "100000000",
|
||||
"index" : "E11029302EE744401427793A4F37BCB18F698D55C96851BEC5ABBD6242CF03D7"
|
||||
})json"};
|
||||
|
||||
class AccountObjects_test : public beast::unit_test::suite
|
||||
@@ -332,7 +332,7 @@ public:
|
||||
auto& aobj = resp[jss::result][jss::account_objects][i];
|
||||
aobj.removeMember("PreviousTxnID");
|
||||
aobj.removeMember("PreviousTxnLgrSeq");
|
||||
BEAST_EXPECT(aobj == bobj[i + 1]);
|
||||
BEAST_EXPECT(aobj == bobj[i + 2]);
|
||||
}
|
||||
}
|
||||
// test stepped one-at-a-time with limit=1, resume from prev marker
|
||||
@@ -1135,7 +1135,15 @@ public:
|
||||
return types == typesOut;
|
||||
};
|
||||
// Find AMM objects
|
||||
AMM amm(env, gw, XRP(1'000), USD(1'000));
|
||||
// gw has a hook installed, so pay the hook execution fee too.
|
||||
AMM amm(
|
||||
env,
|
||||
gw,
|
||||
XRP(1'000),
|
||||
USD(1'000),
|
||||
false,
|
||||
0,
|
||||
env.current()->fees().increment.drops() + 1'000'000);
|
||||
amm.deposit(alice, USD(1));
|
||||
// AMM account has 4 objects: AMM object and 3 trustlines
|
||||
auto const lines = getAccountLines(env, amm.ammAccount());
|
||||
|
||||
@@ -117,9 +117,9 @@ public:
|
||||
BEAST_EXPECT(jroOuter[0u][jss::quality] == "100000000");
|
||||
BEAST_EXPECT(jroOuter[0u][jss::taker_gets][jss::currency] == "USD");
|
||||
BEAST_EXPECT(
|
||||
jroOuter[0u][jss::taker_gets][jss::issuer] == bob.human());
|
||||
BEAST_EXPECT(jroOuter[0u][jss::taker_gets][jss::value] == "1");
|
||||
BEAST_EXPECT(jroOuter[0u][jss::taker_pays] == "100000000");
|
||||
jroOuter[0u][jss::taker_gets][jss::issuer] == gw.human());
|
||||
BEAST_EXPECT(jroOuter[0u][jss::taker_gets][jss::value] == "2");
|
||||
BEAST_EXPECT(jroOuter[0u][jss::taker_pays] == "200000000");
|
||||
|
||||
BEAST_EXPECT(jroOuter[1u][jss::quality] == "5000000");
|
||||
BEAST_EXPECT(jroOuter[1u][jss::taker_gets][jss::currency] == "USD");
|
||||
@@ -131,9 +131,9 @@ public:
|
||||
BEAST_EXPECT(jroOuter[2u][jss::quality] == "100000000");
|
||||
BEAST_EXPECT(jroOuter[2u][jss::taker_gets][jss::currency] == "USD");
|
||||
BEAST_EXPECT(
|
||||
jroOuter[2u][jss::taker_gets][jss::issuer] == gw.human());
|
||||
BEAST_EXPECT(jroOuter[2u][jss::taker_gets][jss::value] == "2");
|
||||
BEAST_EXPECT(jroOuter[2u][jss::taker_pays] == "200000000");
|
||||
jroOuter[2u][jss::taker_gets][jss::issuer] == bob.human());
|
||||
BEAST_EXPECT(jroOuter[2u][jss::taker_gets][jss::value] == "1");
|
||||
BEAST_EXPECT(jroOuter[2u][jss::taker_pays] == "100000000");
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -163,7 +163,7 @@ public:
|
||||
void
|
||||
testNoParams(FeatureBitset features)
|
||||
{
|
||||
testcase("No Params, None Enabled");
|
||||
testcase("Default Env: config-forced, none on-ledger");
|
||||
|
||||
using namespace test::jtx;
|
||||
Env env{*this};
|
||||
@@ -178,8 +178,6 @@ public:
|
||||
{
|
||||
if (!BEAST_EXPECT(feature.isMember(jss::name)))
|
||||
return;
|
||||
// default config - so all should be disabled, and
|
||||
// supported. Some may be vetoed.
|
||||
bool expectVeto =
|
||||
(votes.at(feature[jss::name].asString()) ==
|
||||
VoteBehavior::DefaultNo);
|
||||
@@ -188,8 +186,12 @@ public:
|
||||
VoteBehavior::Obsolete);
|
||||
BEAST_EXPECTS(
|
||||
feature.isMember(jss::enabled) &&
|
||||
!feature[jss::enabled].asBool(),
|
||||
feature[jss::enabled].asBool(),
|
||||
feature[jss::name].asString() + " enabled");
|
||||
BEAST_EXPECTS(
|
||||
feature.isMember(jss::ledger_enabled) &&
|
||||
!feature[jss::ledger_enabled].asBool(),
|
||||
feature[jss::name].asString() + " ledger_enabled");
|
||||
BEAST_EXPECTS(
|
||||
feature.isMember(jss::vetoed) &&
|
||||
feature[jss::vetoed].isBool() == !expectObsolete &&
|
||||
@@ -208,7 +210,7 @@ public:
|
||||
void
|
||||
testSomeEnabled(FeatureBitset features)
|
||||
{
|
||||
testcase("No Params, Some Enabled");
|
||||
testcase("Two config-forced, none on-ledger");
|
||||
|
||||
using namespace test::jtx;
|
||||
Env env{
|
||||
@@ -228,7 +230,10 @@ public:
|
||||
(void)id.parseHex(it.key().asString().c_str());
|
||||
if (!BEAST_EXPECT((*it).isMember(jss::name)))
|
||||
return;
|
||||
bool expectEnabled = env.app().getAmendmentTable().isEnabled(id);
|
||||
bool const expectOnLedger =
|
||||
env.app().getAmendmentTable().isEnabled(id);
|
||||
bool const expectForced =
|
||||
id == featureDepositAuth || id == featureDepositPreauth;
|
||||
bool expectSupported =
|
||||
env.app().getAmendmentTable().isSupported(id);
|
||||
bool expectVeto =
|
||||
@@ -239,9 +244,14 @@ public:
|
||||
VoteBehavior::Obsolete);
|
||||
BEAST_EXPECTS(
|
||||
(*it).isMember(jss::enabled) &&
|
||||
(*it)[jss::enabled].asBool() == expectEnabled,
|
||||
(*it)[jss::enabled].asBool() ==
|
||||
(expectOnLedger || expectForced),
|
||||
(*it)[jss::name].asString() + " enabled");
|
||||
if (expectEnabled)
|
||||
BEAST_EXPECTS(
|
||||
(*it).isMember(jss::ledger_enabled) &&
|
||||
(*it)[jss::ledger_enabled].asBool() == expectOnLedger,
|
||||
(*it)[jss::name].asString() + " ledger_enabled");
|
||||
if (expectOnLedger)
|
||||
BEAST_EXPECTS(
|
||||
!(*it).isMember(jss::vetoed),
|
||||
(*it)[jss::name].asString() + " vetoed");
|
||||
@@ -360,12 +370,122 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testConfigForced(FeatureBitset features)
|
||||
{
|
||||
testcase("Config-forced features ([features] stanza)");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
auto const forced = featurePriceOracle;
|
||||
auto const forcedHex = to_string(forced);
|
||||
|
||||
Env env{*this, FeatureBitset(forced)};
|
||||
|
||||
auto jrr = env.rpc("server_definitions")[jss::result];
|
||||
if (!BEAST_EXPECT(jrr.isMember(jss::features)))
|
||||
return;
|
||||
|
||||
bool sawForced = false;
|
||||
for (auto it = jrr[jss::features].begin();
|
||||
it != jrr[jss::features].end();
|
||||
++it)
|
||||
{
|
||||
auto const& f = *it;
|
||||
auto const name = f[jss::name].asString();
|
||||
|
||||
if (!BEAST_EXPECTS(
|
||||
f.isMember(jss::enabled) && f.isMember(jss::ledger_enabled),
|
||||
name + " enabled/ledger_enabled"))
|
||||
return;
|
||||
|
||||
BEAST_EXPECTS(
|
||||
!f[jss::ledger_enabled].asBool(), name + " ledger_enabled");
|
||||
|
||||
if (it.key().asString() == forcedHex)
|
||||
{
|
||||
sawForced = true;
|
||||
BEAST_EXPECTS(f[jss::enabled].asBool(), name + " enabled");
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECTS(!f[jss::enabled].asBool(), name + " enabled");
|
||||
}
|
||||
}
|
||||
BEAST_EXPECT(sawForced);
|
||||
}
|
||||
|
||||
void
|
||||
testOnLedger(FeatureBitset features)
|
||||
{
|
||||
testcase("On-ledger plus one config-forced");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
// Veto XahauGenesis: FRESH would otherwise enable it via pseudo-tx.
|
||||
auto const forced = featurePriceOracle;
|
||||
auto const forcedHex = to_string(forced);
|
||||
|
||||
Env env{
|
||||
*this,
|
||||
envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->START_UP = Config::FRESH;
|
||||
cfg->section(SECTION_VETO_AMENDMENTS)
|
||||
.append(to_string(featureXahauGenesis) + " XahauGenesis");
|
||||
return cfg;
|
||||
}),
|
||||
FeatureBitset(forced)};
|
||||
|
||||
auto jrr = env.rpc("server_definitions")[jss::result];
|
||||
if (!BEAST_EXPECT(jrr.isMember(jss::features)))
|
||||
return;
|
||||
|
||||
bool sawOnLedger = false;
|
||||
bool sawForced = false;
|
||||
for (auto it = jrr[jss::features].begin();
|
||||
it != jrr[jss::features].end();
|
||||
++it)
|
||||
{
|
||||
uint256 id;
|
||||
(void)id.parseHex(it.key().asString().c_str());
|
||||
auto const& f = *it;
|
||||
auto const name = f[jss::name].asString();
|
||||
|
||||
if (!BEAST_EXPECTS(
|
||||
f.isMember(jss::enabled) && f.isMember(jss::ledger_enabled),
|
||||
name + " enabled/ledger_enabled"))
|
||||
return;
|
||||
|
||||
bool const onLedger = env.app().getAmendmentTable().isEnabled(id);
|
||||
bool const isForced = it.key().asString() == forcedHex;
|
||||
|
||||
BEAST_EXPECTS(
|
||||
f[jss::ledger_enabled].asBool() == onLedger,
|
||||
name + " ledger_enabled");
|
||||
BEAST_EXPECTS(
|
||||
f[jss::enabled].asBool() == (onLedger || isForced),
|
||||
name + " enabled");
|
||||
|
||||
if (onLedger)
|
||||
sawOnLedger = true;
|
||||
if (isForced)
|
||||
{
|
||||
sawForced = true;
|
||||
BEAST_EXPECTS(!onLedger, name + " forced not on-ledger");
|
||||
}
|
||||
}
|
||||
BEAST_EXPECT(sawOnLedger);
|
||||
BEAST_EXPECT(sawForced);
|
||||
}
|
||||
|
||||
void
|
||||
testServerFeatures(FeatureBitset features)
|
||||
{
|
||||
testNoParams(features);
|
||||
testSomeEnabled(features);
|
||||
testWithMajorities(features);
|
||||
testConfigForced(features);
|
||||
testOnLedger(features);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -1136,29 +1136,10 @@ class Simulate_test : public beast::unit_test::suite
|
||||
|
||||
{
|
||||
testcase("simulate Hook rollback");
|
||||
const std::vector<uint8_t> rollbackHook = {
|
||||
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, 0x90U,
|
||||
0x88U, 0x04U, 0x0BU, 0x7FU, 0x00U, 0x41U, 0x8EU, 0x08U, 0x0BU,
|
||||
0x7FU, 0x00U, 0x41U, 0x80U, 0x08U, 0x0BU, 0x7FU, 0x00U, 0x41U,
|
||||
0x90U, 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, 0x88U, 0x80U, 0x80U,
|
||||
0x00U, 0x41U, 0x0EU, 0x42U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U,
|
||||
0x80U, 0x00U, 0x0BU, 0x0BU, 0x15U, 0x01U, 0x00U, 0x41U, 0x80U,
|
||||
0x08U, 0x0BU, 0x0EU, 0x48U, 0x6FU, 0x6FU, 0x6BU, 0x20U, 0x52U,
|
||||
0x65U, 0x6AU, 0x65U, 0x63U, 0x74U, 0x65U, 0x64U, 0x00U,
|
||||
};
|
||||
|
||||
env(hook(alice, {{hso(rollbackHook, overrideFlag)}}, 0),
|
||||
env(hook(
|
||||
alice,
|
||||
{{hso(jtx::genesis::RollbackHook, overrideFlag)}},
|
||||
0),
|
||||
fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
|
||||
@@ -187,14 +187,14 @@ public:
|
||||
for (auto const& val : validators)
|
||||
expectedKeys.insert(toStr(val.masterPublic));
|
||||
|
||||
// Manage single-thread io_service for server.
|
||||
// Manage single-thread io_context for server.
|
||||
BasicApp worker{1};
|
||||
using namespace std::chrono_literals;
|
||||
NetClock::time_point const validUntil{3600s};
|
||||
NetClock::time_point const validFrom2{validUntil - 60s};
|
||||
NetClock::time_point const validUntil2{validFrom2 + 3600s};
|
||||
auto server = make_TrustedPublisherServer(
|
||||
worker.get_io_service(),
|
||||
worker.get_io_context(),
|
||||
validators,
|
||||
validUntil,
|
||||
{{validFrom2, validUntil2}},
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <boost/asio/bind_executor.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/post.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/beast/core/flat_buffer.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class BeastTimeout_test : public beast::unit_test::suite
|
||||
{
|
||||
// Regression for boostorg/beast#2925, fixed by #2926. Boost 1.86 can
|
||||
// move the operation before reading its executor in the expired path.
|
||||
template <class Initiate>
|
||||
void
|
||||
testExpired(std::string const& name, Initiate const& initiate)
|
||||
{
|
||||
namespace net = boost::asio;
|
||||
for (bool onStrand : {false, true})
|
||||
{
|
||||
testcase(name + (onStrand ? " on strand" : " outside strand"));
|
||||
net::io_context ioc;
|
||||
// Match the server's type-erased TS executor and strand.
|
||||
boost::beast::basic_stream<net::ip::tcp, net::executor> stream(ioc);
|
||||
net::strand<net::executor> strand(ioc.get_executor());
|
||||
// Exercise the manual timeout branch without a timer/sleep race.
|
||||
stream.expires_after(-std::chrono::seconds(1));
|
||||
int completions = 0;
|
||||
bool initiating = true;
|
||||
auto handler = net::bind_executor(
|
||||
strand, [&](boost::system::error_code ec, std::size_t n) {
|
||||
++completions;
|
||||
BEAST_EXPECT(!initiating);
|
||||
BEAST_EXPECT(strand.running_in_this_thread());
|
||||
BEAST_EXPECT(ec == boost::beast::error::timeout);
|
||||
BEAST_EXPECT(n == 0);
|
||||
});
|
||||
auto start = [&] {
|
||||
// The expired path runs before socket I/O, so no connection
|
||||
// is needed. Stock Boost 1.86 throws bad_executor with GCC.
|
||||
initiate(stream, handler);
|
||||
initiating = false;
|
||||
};
|
||||
if (onStrand)
|
||||
net::post(strand, start);
|
||||
else
|
||||
start();
|
||||
ioc.run();
|
||||
BEAST_EXPECT(completions == 1);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
namespace net = boost::asio;
|
||||
namespace http = boost::beast::http;
|
||||
|
||||
char byte = 0;
|
||||
testExpired("stream read", [&](auto& stream, auto handler) {
|
||||
stream.async_read_some(net::buffer(&byte, 1), handler);
|
||||
});
|
||||
testExpired("stream write", [&](auto& stream, auto handler) {
|
||||
stream.async_write_some(net::buffer(&byte, 1), handler);
|
||||
});
|
||||
|
||||
// Exercise completion through HTTP's composed operations too. The
|
||||
// reproducer in boostorg/beast#2941 writes HTTP after stream expiry.
|
||||
boost::beast::flat_buffer buffer;
|
||||
http::request<http::string_body> incoming;
|
||||
testExpired("HTTP read", [&](auto& stream, auto handler) {
|
||||
http::async_read(stream, buffer, incoming, handler);
|
||||
});
|
||||
http::request<http::string_body> outgoing{http::verb::post, "/", 11};
|
||||
outgoing.body() = "test";
|
||||
outgoing.prepare_payload();
|
||||
testExpired("HTTP write", [&](auto& stream, auto handler) {
|
||||
http::async_write(stream, outgoing, handler);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(BeastTimeout, server, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/beast/core/multi_buffer.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
@@ -161,12 +162,11 @@ class ServerStatus_test : public beast::unit_test::suite,
|
||||
{
|
||||
using namespace boost::asio;
|
||||
using namespace boost::beast::http;
|
||||
io_service& ios = get_io_service();
|
||||
io_context& ios = get_io_context();
|
||||
ip::tcp::resolver r{ios};
|
||||
boost::beast::multi_buffer sb;
|
||||
|
||||
auto it = r.async_resolve(
|
||||
ip::tcp::resolver::query{host, std::to_string(port)}, yield[ec]);
|
||||
auto it = r.async_resolve(host, std::to_string(port), yield[ec]);
|
||||
if (ec)
|
||||
return;
|
||||
|
||||
@@ -472,12 +472,11 @@ class ServerStatus_test : public beast::unit_test::suite,
|
||||
auto req_string = boost::lexical_cast<std::string>(req);
|
||||
req_string.erase(req_string.find_last_of("13"), std::string::npos);
|
||||
|
||||
io_service& ios = get_io_service();
|
||||
io_context& ios = get_io_context();
|
||||
ip::tcp::resolver r{ios};
|
||||
boost::beast::multi_buffer sb;
|
||||
|
||||
auto it = r.async_resolve(
|
||||
ip::tcp::resolver::query{*ip, std::to_string(*port)}, yield[ec]);
|
||||
auto it = r.async_resolve(*ip, std::to_string(*port), yield[ec]);
|
||||
if (!BEAST_EXPECTS(!ec, ec.message()))
|
||||
return;
|
||||
|
||||
@@ -606,14 +605,13 @@ class ServerStatus_test : public beast::unit_test::suite,
|
||||
env.app().config()["port_rpc"].get<std::string>("ip").value();
|
||||
|
||||
boost::system::error_code ec;
|
||||
io_service& ios = get_io_service();
|
||||
io_context& ios = get_io_context();
|
||||
ip::tcp::resolver r{ios};
|
||||
|
||||
Json::Value jr;
|
||||
jr[jss::method] = "server_info";
|
||||
|
||||
auto it = r.async_resolve(
|
||||
ip::tcp::resolver::query{ip, std::to_string(port)}, yield[ec]);
|
||||
auto it = r.async_resolve(ip, std::to_string(port), yield[ec]);
|
||||
BEAST_EXPECT(!ec);
|
||||
|
||||
std::vector<std::pair<ip::tcp::socket, boost::beast::multi_buffer>>
|
||||
@@ -677,7 +675,7 @@ class ServerStatus_test : public beast::unit_test::suite,
|
||||
std::string(resp["Upgrade"]) == "websocket");
|
||||
BEAST_EXPECT(
|
||||
resp.find("Connection") != resp.end() &&
|
||||
std::string(resp["Connection"]) == "Upgrade");
|
||||
boost::iequals(std::string(resp["Connection"]), "upgrade"));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -724,11 +722,10 @@ class ServerStatus_test : public beast::unit_test::suite,
|
||||
env.app().config()["port_ws"].get<std::string>("ip").value();
|
||||
boost::system::error_code ec;
|
||||
|
||||
io_service& ios = get_io_service();
|
||||
io_context& ios = get_io_context();
|
||||
ip::tcp::resolver r{ios};
|
||||
|
||||
auto it = r.async_resolve(
|
||||
ip::tcp::resolver::query{ip, std::to_string(port)}, yield[ec]);
|
||||
auto it = r.async_resolve(ip, std::to_string(port), yield[ec]);
|
||||
if (!BEAST_EXPECT(!ec))
|
||||
return;
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <test/unit_test/SuiteJournal.h>
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/executor_work_guard.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/ssl/ssl_stream.hpp>
|
||||
#include <boost/utility/in_place_factory.hpp>
|
||||
@@ -51,14 +52,16 @@ public:
|
||||
class TestThread
|
||||
{
|
||||
private:
|
||||
boost::asio::io_service io_service_;
|
||||
std::optional<boost::asio::io_service::work> work_;
|
||||
boost::asio::io_context io_context_;
|
||||
std::optional<boost::asio::executor_work_guard<
|
||||
boost::asio::io_context::executor_type>>
|
||||
work_;
|
||||
std::thread thread_;
|
||||
|
||||
public:
|
||||
TestThread()
|
||||
: work_(std::in_place, std::ref(io_service_))
|
||||
, thread_([&]() { this->io_service_.run(); })
|
||||
: work_(std::in_place, boost::asio::make_work_guard(io_context_))
|
||||
, thread_([&]() { this->io_context_.run(); })
|
||||
{
|
||||
}
|
||||
|
||||
@@ -68,10 +71,10 @@ public:
|
||||
thread_.join();
|
||||
}
|
||||
|
||||
boost::asio::io_service&
|
||||
get_io_service()
|
||||
boost::asio::io_context&
|
||||
get_io_context()
|
||||
{
|
||||
return io_service_;
|
||||
return io_context_;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -241,7 +244,7 @@ public:
|
||||
void
|
||||
test_request(boost::asio::ip::tcp::endpoint const& ep)
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
using socket = boost::asio::ip::tcp::socket;
|
||||
socket s(ios);
|
||||
|
||||
@@ -267,7 +270,7 @@ public:
|
||||
void
|
||||
test_keepalive(boost::asio::ip::tcp::endpoint const& ep)
|
||||
{
|
||||
boost::asio::io_service ios;
|
||||
boost::asio::io_context ios;
|
||||
using socket = boost::asio::ip::tcp::socket;
|
||||
socket s(ios);
|
||||
|
||||
@@ -307,10 +310,10 @@ public:
|
||||
sink.threshold(beast::severities::Severity::kAll);
|
||||
beast::Journal journal{sink};
|
||||
TestHandler handler;
|
||||
auto s = make_Server(handler, thread.get_io_service(), journal);
|
||||
auto s = make_Server(handler, thread.get_io_context(), journal);
|
||||
std::vector<Port> serverPort(1);
|
||||
serverPort.back().ip =
|
||||
beast::IP::Address::from_string(getEnvLocalhostAddr()),
|
||||
boost::asio::ip::make_address(getEnvLocalhostAddr()),
|
||||
serverPort.back().port = 0;
|
||||
serverPort.back().protocol.insert("http");
|
||||
auto eps = s->ports(serverPort);
|
||||
@@ -390,10 +393,10 @@ public:
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
TestThread thread;
|
||||
auto s = make_Server(h, thread.get_io_service(), journal);
|
||||
auto s = make_Server(h, thread.get_io_context(), journal);
|
||||
std::vector<Port> serverPort(1);
|
||||
serverPort.back().ip =
|
||||
beast::IP::Address::from_string(getEnvLocalhostAddr()),
|
||||
boost::asio::ip::make_address(getEnvLocalhostAddr()),
|
||||
serverPort.back().port = 0;
|
||||
serverPort.back().protocol.insert("http");
|
||||
s->ports(serverPort);
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 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 <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/server/detail/Spawn.h>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/spawn.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
// util::spawn wraps boost::asio::spawn (Boost 1.87+ removed the legacy
|
||||
// overloads and the implicit strand): it runs the coroutine on a strand and
|
||||
// propagates exceptions to io_context::run() instead of swallowing them.
|
||||
class Spawn_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testRunsOnGivenStrand()
|
||||
{
|
||||
testcase("a coroutine spawned on a strand runs and can suspend");
|
||||
boost::asio::io_context ioc;
|
||||
auto strand = boost::asio::make_strand(ioc);
|
||||
bool ran = false;
|
||||
bool suspended = false;
|
||||
util::spawn(strand, [&](boost::asio::yield_context yield) {
|
||||
suspended = strand.running_in_this_thread();
|
||||
boost::asio::steady_timer timer(ioc);
|
||||
timer.expires_after(std::chrono::milliseconds(1));
|
||||
timer.async_wait(yield);
|
||||
ran = strand.running_in_this_thread();
|
||||
});
|
||||
ioc.run();
|
||||
BEAST_EXPECT(suspended);
|
||||
BEAST_EXPECT(ran);
|
||||
}
|
||||
|
||||
void
|
||||
testWrapsPlainExecutorInStrand()
|
||||
{
|
||||
testcase("a plain executor or context is wrapped in a strand");
|
||||
{
|
||||
boost::asio::io_context ioc;
|
||||
bool ran = false;
|
||||
util::spawn(ioc.get_executor(), [&](boost::asio::yield_context) {
|
||||
ran = true;
|
||||
});
|
||||
ioc.run();
|
||||
BEAST_EXPECT(ran);
|
||||
}
|
||||
{
|
||||
boost::asio::io_context ioc;
|
||||
bool ran = false;
|
||||
util::spawn(ioc, [&](boost::asio::yield_context) { ran = true; });
|
||||
ioc.run();
|
||||
BEAST_EXPECT(ran);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPropagatesStdException()
|
||||
{
|
||||
testcase("a std::exception escapes io_context::run()");
|
||||
boost::asio::io_context ioc;
|
||||
util::spawn(
|
||||
boost::asio::make_strand(ioc), [](boost::asio::yield_context) {
|
||||
throw std::runtime_error("boom");
|
||||
});
|
||||
bool caught = false;
|
||||
try
|
||||
{
|
||||
ioc.run();
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
caught = std::string(e.what()) == "boom";
|
||||
}
|
||||
BEAST_EXPECT(caught);
|
||||
}
|
||||
|
||||
void
|
||||
testPropagatesUnknownException()
|
||||
{
|
||||
testcase("a non-std exception escapes io_context::run()");
|
||||
boost::asio::io_context ioc;
|
||||
util::spawn(
|
||||
boost::asio::make_strand(ioc),
|
||||
[](boost::asio::yield_context) { throw 42; });
|
||||
bool caught = false;
|
||||
try
|
||||
{
|
||||
ioc.run();
|
||||
}
|
||||
catch (int v)
|
||||
{
|
||||
caught = v == 42;
|
||||
}
|
||||
BEAST_EXPECT(caught);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testRunsOnGivenStrand();
|
||||
testWrapsPlainExecutorInStrand();
|
||||
testPropagatesStdException();
|
||||
testPropagatesUnknownException();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Spawn, server, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -82,7 +82,10 @@ namespace hook_api {
|
||||
namespace hook {
|
||||
|
||||
bool
|
||||
canHook(ripple::TxType txType, ripple::uint256 hookOn);
|
||||
canHook(
|
||||
STTx const& tx,
|
||||
ripple::uint256 hookOn,
|
||||
std::optional<ripple::Slice> hookName);
|
||||
|
||||
bool
|
||||
canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit);
|
||||
|
||||
@@ -101,6 +101,14 @@ HookAPI::sto_subfield(Bytes const& data, uint32_t field_id) const
|
||||
if (data.size() < 2)
|
||||
return Unexpected(TOO_SMALL);
|
||||
|
||||
if (hookCtx.applyCtx.view().rules().enabled(fix20260929))
|
||||
{
|
||||
// validate the data
|
||||
auto const valid = sto_validate(data);
|
||||
if (!valid || !valid.value())
|
||||
return Unexpected(PARSE_ERROR);
|
||||
}
|
||||
|
||||
unsigned char* start = const_cast<unsigned char*>(data.data());
|
||||
unsigned char* upto = start;
|
||||
unsigned char* end = start + data.size();
|
||||
@@ -165,6 +173,18 @@ HookAPI::sto_subarray(Bytes const& data, uint32_t index_id) const
|
||||
unsigned char* upto = start;
|
||||
unsigned char* end = start + data.size();
|
||||
|
||||
if (hookCtx.applyCtx.view().rules().enabled(fix20260929))
|
||||
{
|
||||
// check if the array has valid trailing data
|
||||
if ((*upto & 0xF0U) == 0xF0U && *(end - 1) != 0xF1U)
|
||||
return Unexpected(PARSE_ERROR);
|
||||
|
||||
// validate the array
|
||||
auto const valid = sto_validate(data);
|
||||
if (!valid || !valid.value())
|
||||
return Unexpected(PARSE_ERROR);
|
||||
}
|
||||
|
||||
// unwrap the array if it is wrapped,
|
||||
// by removing a byte from the start and end
|
||||
// why here 0xF0?
|
||||
@@ -259,6 +279,13 @@ HookAPI::sto_emplace(
|
||||
return Unexpected(TOO_SMALL);
|
||||
}
|
||||
|
||||
if (hookCtx.applyCtx.view().rules().enabled(fix20260929))
|
||||
{
|
||||
auto const source_valid = sto_validate(source_object);
|
||||
if (!source_valid || !source_valid.value())
|
||||
return Unexpected(PARSE_ERROR);
|
||||
}
|
||||
|
||||
if (field_object.has_value() &&
|
||||
hookCtx.applyCtx.view().rules().enabled(fixHookAPI20251128))
|
||||
{
|
||||
@@ -279,6 +306,9 @@ HookAPI::sto_emplace(
|
||||
0);
|
||||
if (!length)
|
||||
return Unexpected(PARSE_ERROR);
|
||||
if (hookCtx.applyCtx.view().rules().enabled(fix20260929) &&
|
||||
length.value() != field_object->size())
|
||||
return Unexpected(PARSE_ERROR);
|
||||
if ((type << 16) + field != field_id)
|
||||
{
|
||||
return Unexpected(PARSE_ERROR);
|
||||
@@ -838,14 +868,22 @@ HookAPI::etxn_fee_base(ripple::Slice const& txBlob) const
|
||||
std::unique_ptr<STTx const> stpTrans =
|
||||
std::make_unique<STTx const>(std::ref(sitTrans));
|
||||
|
||||
if (!hookCtx.applyCtx.view().rules().enabled(fixHookAPI20251128))
|
||||
return Transactor::calculateBaseFee(
|
||||
*(applyCtx.app.openLedger().current()), *stpTrans)
|
||||
.drops();
|
||||
// fix20260929: 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(fix20260929))
|
||||
{
|
||||
hold = applyCtx.app.openLedger().current();
|
||||
feeView = hold.get();
|
||||
}
|
||||
|
||||
return invoke_calculateBaseFee(
|
||||
*(applyCtx.app.openLedger().current()), *stpTrans)
|
||||
.drops();
|
||||
if (!hookCtx.applyCtx.view().rules().enabled(fixHookAPI20251128))
|
||||
return Transactor::calculateBaseFee(*feeView, *stpTrans).drops();
|
||||
|
||||
return invoke_calculateBaseFee(*feeView, *stpTrans).drops();
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
@@ -3048,6 +3086,7 @@ HookAPI::get_stobject_length(
|
||||
type == STI_PATHSET && rules.enabled(featureHookAPISerializedType240))
|
||||
{
|
||||
length = 0;
|
||||
bool terminated = false;
|
||||
while (upto + length < end)
|
||||
{
|
||||
// iterate Path step
|
||||
@@ -3065,6 +3104,9 @@ HookAPI::get_stobject_length(
|
||||
if (flag & 0x20) // issuer
|
||||
length += 20;
|
||||
|
||||
if (rules.enabled(fix20260929) && upto + length >= end)
|
||||
return Unexpected(pe_unexpected_end);
|
||||
|
||||
int next_flag = *(upto + length);
|
||||
if (next_flag == 0x00 || next_flag == 0xff)
|
||||
// end of Path step
|
||||
@@ -3076,43 +3118,68 @@ HookAPI::get_stobject_length(
|
||||
if (lastflag == 0xff)
|
||||
continue; // continue byte
|
||||
else if (lastflag == 0x00)
|
||||
{
|
||||
terminated = true;
|
||||
break; // end byte
|
||||
}
|
||||
else
|
||||
return Unexpected(pe_unexpected_end);
|
||||
}
|
||||
if (upto >= end)
|
||||
return Unexpected(pe_unexpected_end);
|
||||
if (rules.enabled(fix20260929) && !terminated)
|
||||
return Unexpected(pe_unexpected_end);
|
||||
}
|
||||
else if (type == STI_ISSUE)
|
||||
{
|
||||
bool const fix = rules.enabled(fix20260929);
|
||||
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(fix20260929);
|
||||
auto zero20 = std::array<char, 20>{0};
|
||||
// Lock Chain
|
||||
length = 1; // Door Account1 prefix length
|
||||
length += 20; // Door Account1 length
|
||||
// Door Issue1
|
||||
if (memcmp(upto + length, zero20.data(), 20) == 0)
|
||||
length += 20; // only Currency
|
||||
else
|
||||
length += 40; // Currency and Issue
|
||||
length = 0;
|
||||
for (int i = 0; i < 2; ++i) // Locking Chain, Issuing Chain
|
||||
{
|
||||
// Door Account
|
||||
if (!fix)
|
||||
length += 21;
|
||||
else if (end - upto <= length)
|
||||
return Unexpected(pe_unexpected_end);
|
||||
else if (upto[length] == 0)
|
||||
length += 1; // default (empty) account
|
||||
else if (upto[length] == 20)
|
||||
length += 21;
|
||||
else
|
||||
return Unexpected(pe_unexpected_end);
|
||||
|
||||
// Issuing Chain
|
||||
length += 1; // Door Account2 prefix length
|
||||
length += 20; // Door Account2 length
|
||||
// Door Issue2
|
||||
if (memcmp(upto + length, zero20.data(), 20) == 0)
|
||||
length += 20; // only Currency
|
||||
else
|
||||
length += 40; // Currency and Issue
|
||||
// Door Issue
|
||||
if (fix && end - upto < length + 20)
|
||||
return Unexpected(pe_unexpected_end);
|
||||
if (memcmp(upto + length, zero20.data(), 20) == 0)
|
||||
length += 20; // only Currency
|
||||
// MPT is not supported yet
|
||||
// else if (
|
||||
// fix && end - upto >= length + 40 &&
|
||||
// memcmp(upto + length + 20, noAccount().data(), 20) == 0)
|
||||
// length += 44; // MPT: Issuer + noAccount + Sequence
|
||||
else
|
||||
length += 40; // Currency and Issuer
|
||||
}
|
||||
}
|
||||
|
||||
if (length > -1)
|
||||
@@ -3129,6 +3196,10 @@ HookAPI::get_stobject_length(
|
||||
length,
|
||||
payload_start,
|
||||
payload_length);
|
||||
|
||||
if (rules.enabled(fix20260929) && length > end - upto)
|
||||
return Unexpected(pe_unexpected_end);
|
||||
|
||||
return length + (upto - start);
|
||||
}
|
||||
|
||||
|
||||
@@ -382,9 +382,16 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
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);
|
||||
// featureEscrowDestinationCancel: an account can always cancel an
|
||||
// escrow destined for it, so when the destination cancels, the
|
||||
// source is only a weak tsh and its hooks cannot roll back.
|
||||
bool const destinationCancel = tt == ttESCROW_CANCEL &&
|
||||
rv.rules().enabled(featureEscrowDestinationCancel) &&
|
||||
*otxnAcc == dst && src != dst;
|
||||
|
||||
// otherwise the source account is a strong transactional
|
||||
// stakeholder for fin and can
|
||||
ADD_TSH(src, destinationCancel ? tshWEAK : tshSTRONG);
|
||||
|
||||
// the dest acc is a strong tsh for fin and weak for can
|
||||
if (src != dst)
|
||||
@@ -785,7 +792,7 @@ hook::removeHookNamespaceEntry(ripple::SLE& sleAccount, ripple::uint256 ns)
|
||||
// transactions. If you wish to set a hook that has control over ttHOOK_SET then
|
||||
// set bit 1U<<22.
|
||||
bool
|
||||
hook::canHook(ripple::TxType txType, ripple::uint256 hookOn)
|
||||
canHookTT(ripple::TxType txType, ripple::uint256 hookOn)
|
||||
{
|
||||
// invert ttHOOK_SET bit
|
||||
hookOn ^= UINT256_BIT[ttHOOK_SET];
|
||||
@@ -799,7 +806,28 @@ hook::canHook(ripple::TxType txType, ripple::uint256 hookOn)
|
||||
bool
|
||||
hook::canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit)
|
||||
{
|
||||
return hook::canHook(txType, hookCanEmit);
|
||||
return canHookTT(txType, hookCanEmit);
|
||||
}
|
||||
|
||||
bool
|
||||
hook::canHook(
|
||||
STTx const& tx,
|
||||
ripple::uint256 hookOn,
|
||||
std::optional<ripple::Slice> hookName)
|
||||
{
|
||||
if (!canHookTT(tx.getTxnType(), hookOn))
|
||||
return false;
|
||||
|
||||
if (!hookName || hookName->empty())
|
||||
// no hook name specified to hook, so we can always hook
|
||||
return true;
|
||||
|
||||
if (!tx.isFieldPresent(sfHookName))
|
||||
// hook name specified hook, but no hook name specified in the
|
||||
// transaction, so we can't hook without the hook name
|
||||
return false;
|
||||
|
||||
return tx[sfHookName] == *hookName;
|
||||
}
|
||||
|
||||
ripple::uint256
|
||||
@@ -1044,13 +1072,16 @@ hook::apply(
|
||||
.wasmParam = wasmParam,
|
||||
.hookChainPosition = hookChainPosition,
|
||||
.foreignStateSetDisabled = false,
|
||||
.provisionalMeta = provisionalMeta},
|
||||
.provisionalMeta = provisionalMeta,
|
||||
.foreignStateGrantCache = {}},
|
||||
.emitFailure = isCallback && wasmParam & 1
|
||||
? std::optional<ripple::STObject>(
|
||||
(*(applyCtx.view().peek(keylet::emittedTxn(
|
||||
applyCtx.tx.getFieldH256(sfTransactionHash)))))
|
||||
.downcast<STObject>())
|
||||
: std::optional<ripple::STObject>()};
|
||||
: std::optional<ripple::STObject>(),
|
||||
.module = nullptr,
|
||||
.api_ = nullptr};
|
||||
|
||||
auto const& j = applyCtx.app.journal("View");
|
||||
|
||||
@@ -2191,6 +2222,35 @@ DEFINE_HOOK_FUNCTION(
|
||||
return serialize_keylet(kl, memory, write_ptr, write_len);
|
||||
}
|
||||
|
||||
// keylets that take a validator public key
|
||||
case keylet_code::MANIFEST: {
|
||||
if (!applyCtx.view().rules().enabled(featureOnChainManifests))
|
||||
return INVALID_ARGUMENT;
|
||||
|
||||
if (a == 0 || b == 0)
|
||||
return INVALID_ARGUMENT;
|
||||
|
||||
if (c != 0 || d != 0 || e != 0 || f != 0)
|
||||
return INVALID_ARGUMENT;
|
||||
|
||||
uint32_t read_ptr = a, read_len = b;
|
||||
|
||||
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
ripple::Slice const pkSlice{memory + read_ptr, read_len};
|
||||
|
||||
// Reject anything that is not a well-formed public key before
|
||||
// constructing one: the PublicKey ctor throws on bad input.
|
||||
if (!publicKeyType(pkSlice))
|
||||
return INVALID_ARGUMENT;
|
||||
|
||||
ripple::Keylet kl =
|
||||
ripple::keylet::manifest(ripple::PublicKey(pkSlice));
|
||||
|
||||
return serialize_keylet(kl, memory, write_ptr, write_len);
|
||||
}
|
||||
|
||||
// keylets that take 20 byte account id, and (4 byte uint for 32
|
||||
// byte hash)
|
||||
case keylet_code::ORACLE: {
|
||||
|
||||
@@ -41,7 +41,7 @@ TimeoutCounter::TimeoutCounter(
|
||||
, progress_(false)
|
||||
, timerInterval_(interval)
|
||||
, queueJobParameter_(std::move(jobParameter))
|
||||
, timer_(app_.getIOService())
|
||||
, timer_(app_.getIOContext())
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
(timerInterval_ > 10ms) && (timerInterval_ < 30s),
|
||||
|
||||
@@ -118,7 +118,7 @@ protected:
|
||||
return complete_ || failed_;
|
||||
}
|
||||
|
||||
// Used in this class for access to boost::asio::io_service and
|
||||
// Used in this class for access to boost::asio::io_context and
|
||||
// ripple::Overlay. Used in subtypes for the kitchen sink.
|
||||
Application& app_;
|
||||
beast::Journal journal_;
|
||||
|
||||
Loaded 100 of 179 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user