Compare commits

..
Author SHA1 Message Date
tequ dcd2b0a57d fix: take stoppingMutex_ in ApplicationImp::signalStop so the stop notify cannot be lost 2026-10-11 20:50:20 +09:00
tequ 1440940636 ci: diagnostic: HookAPI stress loop before full run [DO NOT MERGE] 2026-10-11 20:07:11 +09:00
tequ ebaaf2132b ci: diagnostic: ptrace cap + gdb stack dumps, shorter deadline [DO NOT MERGE] 2026-10-11 19:40:51 +09:00
tequ ed10ffb935 ci: diagnostic run of coverage tests outside ninja with sampling and stack dumps [DO NOT MERGE] 2026-10-11 19:03:41 +09:00
Richard Holland a2a1e09c2e fix for clang-16 builds (#885) 2026-10-11 16:22:56 +10:00
tequ 505c28768a Expose Hook API signatures, keylet types and return codes in server_definitions (#884) 2026-10-11 15:26:25 +10:00
onledger.net d7f320bd17 fix(hook): sto_validate returns 0 for buffers under 2 bytes (#881) 2026-10-11 12:33:47 +10:00
onledger.net 22a94c4429 fix(base58): Read characters as unsigned char in b58_to_b256_be (#868) 2026-10-08 23:08:47 +10:00
Alphonse N. Mousse 42a33a224d Fix handling of emitted transactions: (#731) 2026-10-08 18:30:35 +10:00
Fomo ee33d825fc fix: NULL RWDB and mutex locks (#728) 2026-10-08 16:39:29 +10:00
onledger.net 9ef66a65b8 fix: ReportFeeChange data race, no-subscriber queuing, and LoadManager loop (#785) 2026-10-07 18:26:53 +10:00
Richard Holland 98fdaa4afa UNLReport Fix (#872) 2026-10-07 17:12:19 +10:00
tequ d9cc66afe9 ci: always run fixed 3-config matrix in nix workflow (#873) 2026-10-07 14:36:37 +10:00
Richard Holland b260b445e9 Foreign Hook State Touch (#859) 2026-10-06 11:05:49 +10:00
Richard HollandandNiq Dudfield 4e0e108654 On Chain Manifest Canonical Envelope (port from 854) (#862)
Co-authored-by: Niq Dudfield <[email protected]>
2026-10-05 17:05:21 +10:00
tequ 919d817edc Restrict slot_set to keylets (#863) 2026-10-05 15:56:18 +10:00
Richard Holland 0373045bff re-enable ocm (#864) 2026-10-05 15:53:59 +10:00
tequ 2baa23d14b Fix/string plus int warning (#857) 2026-10-05 15:03:40 +10:00
tequ de9d5e53f1 test: cover BuildInfo::getVersionString (#856)
Add a unit test for getVersionString() / getFullVersionString() so the
generated "<year>.<month>.<day>-CustomBuild" version string is checked
for strict semver round-trip and sane date ranges before touching the
__DATE__ parsing code.
2026-10-05 14:39:12 +10:00
tequ 80c70ec03d Add fix20261005 Amendment placeholder (#860) 2026-10-05 14:08:57 +10:00
Richard Holland 2566bcf6fa EscrowDestinationCancel (#858) 2026-10-04 10:51:15 +10:00
tequ 7f37aeac39 Fix compiler warnings in gcc CI builds and Boost 1.91 deprecation (#855) 2026-10-02 14:41:57 +10:00
Niq DudfieldandAlex Kremer 21fab66a17 build: bump boost to 1.91.0 and fix beast/asio timeout bugs (#812)
Cherry-picking XRPLF/rippled 1506e65558

Co-authored-by: Alex Kremer <[email protected]>
2026-10-01 15:32:18 +10:00
Richard HollandandNicholas Dudfield 0f3258d235 import exploit fixes (#9)
Co-authored-by: Nicholas Dudfield <[email protected]>
2026-09-29 18:07:37 +10:00
tequ 3fbeb130f8 fix20261001 to fix20260929 (#14) 2026-09-29 17:58:39 +10:00
58efea062c Offer create exploit fix (#10)
Co-authored-by: tequ <[email protected]>
Co-authored-by: Nicholas Dudfield <[email protected]>
2026-09-29 17:57:50 +10:00
Niq Dudfield 43d90b8549 ci: run the codecov upload and docker release builder only from xahau/xahaud (#13)
* ci: upload coverage to codecov only from xahau/xahaud

* ci: run the docker release builder only from xahau/xahaud
2026-09-29 15:23:04 +10:00
Niq Dudfield a3bd76fdc9 fix(test): mark skipped SetHookTSH manifest case (#12) 2026-09-29 12:22:47 +10:00
Richard Holland 4de217a2c6 mint exploit fix (#8) 2026-09-29 08:26:29 +10:00
Richard Holland 5e49dc8474 Disable ocm tests (#847) clang
clang
2026-09-29 07:22:32 +10:00
Richard Holland 77e309fff5 disable ocm tests (#846) 2026-09-29 07:15:33 +10:00
Niq Dudfield 79319aaf96 fix(ci): skip docker cleanup when the run has no checkout path (#841) 2026-09-29 05:40:39 +10:00
tequ a5163eeb50 Extract Transactor::finishApply from Transactor::operator() (#843)
Pure move of the post-apply pipeline (tec handling, invariants,
balance rewards, weak hooks, commit) into a helper so it can be
invoked more than once. No behaviour change.
2026-09-29 05:39:57 +10:00
Richard Holland 3bc435864f disable onchainmanifests for this release (#845) 2026-09-29 05:35:30 +10:00
tequ 4d08a9e603 etxn_fee_base: compute the fee against the applied view (fix20261001) (#842) 2026-09-29 05:02:37 +10:00
Niq Dudfield f155556017 test(jtx): reconnect the JSON-RPC client when the server closed it (#840) 2026-09-29 04:59:03 +10:00
tequ c4570a699f Move isValidUTF8 to xrpl/basics and its unit test to test/basics (#834) 2026-09-28 11:50:07 +10:00
tequ 426ff27e42 Merge fixHookNameValidation into fix20261001 (#837) 2026-09-28 11:48:50 +10:00
tequ e6bdd55803 Merge fixHookExitOutOfBounds into fix20261001 (#836) 2026-09-28 11:48:05 +10:00
tequ 9dd103fad4 Merge fixHookAPISType into fix20261001 (#835) 2026-09-28 11:46:59 +10:00
Richard Holland a7f9c683c4 fix keylet api (#833) 2026-09-26 14:24:00 +10:00
tequ 48a94af13d Add HookName validation to ClaimReward hookCan process (#786)
* Add HookName validation to ClaimReward hookCan process

* fixHookNameValidation Amendment (#776)

* Add fix20261001 Amendment placeholder (#831)

* address review

* use Slice instead Blob for canHook helper
2026-09-26 13:25:19 +10:00
Richard Holland 1ddda79c34 utf8 patch (#832) 2026-09-26 13:11:53 +10:00
tequ 76b7111098 Add fix20261001 Amendment placeholder (#831) 2026-09-26 11:07:18 +10:00
191 changed files with 9579 additions and 1919 deletions

No files matched your search

+10 -1
View File
@@ -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}"
+94 -152
View File
@@ -8,12 +8,6 @@ on:
types: [opened, synchronize, reopened, labeled, unlabeled]
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
inputs:
full_matrix:
description: "Force full matrix (6 configs)"
required: false
default: "false"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -30,37 +24,14 @@ jobs:
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: escape double quotes
id: escape
shell: bash
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
ESCAPED_PR_TITLE="${PR_TITLE//\"/\\\"}"
echo "title=${ESCAPED_PR_TITLE}" >> "$GITHUB_OUTPUT"
- name: Generate build matrix
id: set-matrix
shell: python
env:
GH_TOKEN: ${{ github.token }}
run: |
import json
import os
import urllib.request
# Full matrix with all 6 compiler configurations
# Each configuration includes all parameters needed by the build job
full_matrix = [
{
"compiler_id": "gcc-11-libstdcxx",
"compiler": "gcc",
"cc": "gcc-11",
"cxx": "g++-11",
"compiler_version": 11,
"stdlib": "default",
"configuration": "Debug",
"job_type": "build"
},
matrix = [
{
"compiler_id": "gcc-13-libstdcxx",
"compiler": "gcc",
@@ -93,17 +64,6 @@ jobs:
"grpc/*": ["-Wno-missing-template-arg-list-after-template-kw"]
}
},
{
"compiler_id": "clang-14-libstdcxx-gcc11",
"compiler": "clang",
"cc": "clang-14",
"cxx": "clang++-14",
"compiler_version": 14,
"stdlib": "libstdcxx",
"clang_gcc_toolchain": 11,
"configuration": "Debug",
"job_type": "build"
},
{
"compiler_id": "clang-16-libstdcxx-gcc13",
"compiler": "clang",
@@ -114,117 +74,8 @@ jobs:
"clang_gcc_toolchain": 13,
"configuration": "Debug",
"job_type": "build"
},
{
"compiler_id": "clang-17-libcxx",
"compiler": "clang",
"cc": "clang-17",
"cxx": "clang++-17",
"compiler_version": 17,
"stdlib": "libcxx",
"configuration": "Debug",
"job_type": "build"
},
{
# Clang 18 - testing if it's faster than Clang 17 with libc++
# Requires patching Conan v1 settings.yml to add version 18
"compiler_id": "clang-18-libcxx",
"compiler": "clang",
"cc": "clang-18",
"cxx": "clang++-18",
"compiler_version": 18,
"stdlib": "libcxx",
"configuration": "Debug",
"job_type": "build"
}
]
# Minimal matrix for PRs and feature branches
minimal_matrix = [
full_matrix[1], # gcc-13 (middle-ground gcc)
full_matrix[2], # clang-20 llvm-cov coverage
full_matrix[3] # clang-14 (mature, stable clang)
]
# Determine which matrix to use based on the target branch
ref = "${{ github.ref }}"
base_ref = "${{ github.base_ref }}" # For PRs, this is the target branch
event_name = "${{ github.event_name }}"
pr_title = """${{ steps.escape.outputs.title }}"""
pr_labels = """${{ join(github.event.pull_request.labels.*.name, ',') }}"""
pr_head_sha = "${{ github.event.pull_request.head.sha }}"
# Get commit message - for PRs, fetch via API since head_commit.message is empty
if event_name == "pull_request" and pr_head_sha:
try:
url = f"https://api.github.com/repos/${{ github.repository }}/commits/{pr_head_sha}"
req = urllib.request.Request(url, headers={
"Accept": "application/vnd.github.v3+json",
"Authorization": f"Bearer {os.environ.get('GH_TOKEN', '')}"
})
with urllib.request.urlopen(req) as response:
data = json.load(response)
commit_message = data["commit"]["message"]
except Exception as e:
print(f"Failed to fetch commit message: {e}")
commit_message = ""
else:
commit_message = """${{ github.event.head_commit.message }}"""
# Debug logging
print(f"Event: {event_name}")
print(f"Ref: {ref}")
print(f"Base ref: {base_ref}")
print(f"PR head SHA: {pr_head_sha}")
print(f"PR title: {pr_title}")
print(f"PR labels: {pr_labels}")
print(f"Commit message: {commit_message}")
# Manual trigger input to force full matrix.
manual_full = "${{ github.event.inputs.full_matrix || 'false' }}" == "true"
# Label/manual overrides, while preserving existing title/commit behavior.
force_full = (
manual_full
or "[ci-nix-full-matrix]" in commit_message
or "[ci-nix-full-matrix]" in pr_title
or ("ci-full-build" in pr_labels and "ci-nix-full-matrix" in pr_labels)
)
force_min = (
"ci-full-build" in pr_labels
)
print(f"Force full matrix: {force_full}")
print(f"Force min matrix: {force_min}")
# Check if this is targeting a main branch
# For PRs: check base_ref (target branch)
# For pushes: check ref (current branch)
main_branches = ["refs/heads/dev", "refs/heads/release", "refs/heads/candidate"]
if force_full:
# Override: always use full matrix if forced by manual input or label.
use_full = True
elif force_min:
# Override: always use minimal matrix if ci-full-build label is present.
use_full = False
elif event_name == "pull_request":
# For PRs, base_ref is just the branch name (e.g., "dev", not "refs/heads/dev")
# Check if the PR targets release or candidate (more critical branches)
use_full = base_ref in ["release", "candidate"]
else:
# For pushes, ref is the full reference (e.g., "refs/heads/dev")
use_full = ref in main_branches
# Select the appropriate matrix
if use_full:
if force_full:
print(f"Using FULL matrix (7 configs (build x6 + clang-20 llvm-cov coverage)) - forced by [ci-nix-full-matrix] tag")
else:
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")
matrix = minimal_matrix
# Add runs_on based on job_type
for entry in matrix:
@@ -246,6 +97,8 @@ jobs:
contents: read
container:
image: ubuntu:24.04
# [DO NOT MERGE] diagnostic: allow gdb/eu-stack to attach to hung test children
options: --cap-add=SYS_PTRACE
volumes:
- /home/runner/.conan-cache:/.conan-cache
- /home/runner/.ccache-cache:/github/home/.ccache-cache
@@ -452,9 +305,98 @@ jobs:
# 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'
cmake-target: 'rippled'
ccache_max_size: '100G'
# [DO NOT MERGE] Diagnostic: run the coverage test pass directly (not via
# the ninja `coverage` target, which buffers all output until exit) so
# every line gets a live timestamp, sample host/process state every 60s,
# and dump thread stacks of every rippled process at 12/25 min and on a
# 90 min deadline instead of sitting for the 6h job timeout.
- name: Run tests (coverage, diagnostic)
if: matrix.job_type == 'coverage'
shell: bash
run: |
apt-get install -y procps elfutils gdb > /dev/null
cd ${{ env.build_dir }}
rm -rf coverage-profraw && mkdir coverage-profraw
JOBS=$(($(nproc)/2))
echo "nproc=$(nproc) jobs=$JOBS loadavg=$(cat /proc/loadavg)"
free -m; df -h . /dev/shm /tmp; ulimit -a; cat /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io 2>/dev/null || true
cat > /tmp/flag.py <<'EOF'
import gdb
for t in gdb.selected_inferior().threads():
t.switch()
f = gdb.newest_frame()
while f:
if f.name() and 'ApplicationImp::run' in f.name():
f.select()
print("thread", t.num, "in ApplicationImp::run, isTimeToStop =", gdb.parse_and_eval("this->isTimeToStop"))
break
f = f.older()
EOF
dump_stacks() {
echo "===== STACK DUMP ($1) $(date -u +%T)"
for p in $(pgrep -x rippled); do
echo "--- pid $p state=$(awk '/^State/{print $2}' /proc/$p/status) wchan=$(cat /proc/$p/wchan 2>/dev/null) cmd=$(tr '\0' ' ' < /proc/$p/cmdline | cut -c1-100)"
timeout 120 eu-stack -p $p 2>&1 | head -300 || true
st=$(awk '/^State/{print $2}' /proc/$p/status 2>/dev/null)
if [ "$st" = "S" ] && [ "$p" != "$RP" ]; then
timeout 900 gdb -p $p -batch -ex 'set pagination off' -ex 'source /tmp/flag.py' -ex 'thread apply all bt 30' 2>&1 \
| grep -vE '^\[New LWP|^\[Thread debugging|^Using host|^warning:|^Reading symbols|^Download' | head -800 || true
fi
done
}
# Stress the suite whose Env lifetimes are shortest: the hang sits in
# Env::~AppBundle (thread.join on ApplicationImp::run) and shows up
# within a few iterations when it is present.
for i in 1 2 3 4 5 6; do
st=$SECONDS
if LLVM_PROFILE_FILE=/tmp/stress-%p.profraw timeout -s KILL 240 ./rippled --unittest=ripple.app.HookAPI --unittest-jobs 1 > /tmp/stress_$i.log 2>&1; then
echo "stress iter $i ok $((SECONDS-st))s: $(grep -oE '[0-9]+ cases, [0-9]+ tests total, [0-9]+ failures' /tmp/stress_$i.log)"
else
echo "stress iter $i FAILED/HUNG rc=$? after $((SECONDS-st))s; last lines:"; tail -3 /tmp/stress_$i.log
fi
done
rm -f /tmp/stress-*.profraw
( while true; do
echo "===== sample $(date -u +%T) loadavg=$(cut -d' ' -f1-3 /proc/loadavg) psi_cpu=$(grep -o 'avg60=[0-9.]*' /proc/pressure/cpu 2>/dev/null | head -1) psi_mem=$(grep -o 'avg60=[0-9.]*' /proc/pressure/memory 2>/dev/null | head -1) psi_io=$(grep -o 'avg60=[0-9.]*' /proc/pressure/io 2>/dev/null | head -1)"
free -m | sed -n 2,3p
ps -eo pid,ppid,stat,etimes,pcpu,rss,wchan:28,args --sort=-pcpu | grep -E 'PID|rippled' | grep -v grep | head -14 | cut -c1-150
sleep 60
done ) &
MON=$!
LLVM_PROFILE_FILE="$PWD/coverage-profraw/rippled-%m-%p.profraw" \
stdbuf -oL -eL ./rippled --unittest --unittest-jobs $JOBS --unittest-log &
RP=$!
START=$SECONDS; D20=0; D40=0; rc=0
while kill -0 $RP 2>/dev/null; do
el=$((SECONDS-START))
if [ $D20 = 0 ] && [ $el -ge 720 ]; then D20=1; dump_stacks "12min"; fi
if [ $D40 = 0 ] && [ $el -ge 1500 ]; then D40=1; dump_stacks "25min"; fi
if [ $el -ge 3600 ]; then
dump_stacks "60min deadline"
echo "===== DEADLINE: killing rippled processes"
pkill -KILL -x rippled || true
break
fi
sleep 10
done
wait $RP || rc=$?
kill $MON 2>/dev/null || true
echo "rippled unittest exit code: $rc after $((SECONDS-START))s"
ls -la coverage-profraw | head -20
llvm-profdata-${{ matrix.compiler_version }} merge -sparse -o coverage.profdata coverage-profraw/*.profraw
llvm-cov-${{ matrix.compiler_version }} export -instr-profile=coverage.profdata -format=lcov \
-ignore-filename-regex="src/test|include/xrpl/beast/test|include/xrpl/beast/unit_test|$PWD/pb-xrpl.libpb" \
./rippled > coverage.lcov
exit $rc
- name: Set artifact name
if: matrix.job_type == 'build'
id: set-artifact-name
@@ -498,7 +440,7 @@ jobs:
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: ${{ env.COVERAGE_FILE }}
+3
View File
@@ -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
@@ -95,6 +96,7 @@ test.overlay > test.toplevel
test.overlay > test.unit_test
test.overlay > xrpl.basics
test.overlay > xrpld.app
test.overlay > xrpld.core
test.overlay > xrpld.overlay
test.overlay > xrpld.peerfinder
test.overlay > xrpld.shamap
@@ -190,6 +192,7 @@ xrpld.overlay > xrpl.basics
xrpld.overlay > xrpld.core
xrpld.overlay > xrpld.peerfinder
xrpld.overlay > xrpld.perflog
xrpld.overlay > xrpld.shamap
xrpld.overlay > xrpl.json
xrpld.overlay > xrpl.protocol
xrpld.overlay > xrpl.resource
+1 -1
View File
@@ -124,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
+1 -1
View File
@@ -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
View File
@@ -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")
+23 -23
View File
@@ -22,29 +22,29 @@ APPLY_HOOK="$SCRIPT_DIR/../include/xrpl/hook/hook_api.macro"
return s;
}
{
line = $0;
# Skip block comments
if (line ~ /\/\*/) {
next;
}
# Look for comment lines that start with // and contain function signature
if (line ~ /^[[:space:]]*\/\/[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(/) {
# Remove leading // and trim
sub(/^[[:space:]]*\/\/[[:space:]]*/, "", line);
line = trim(line);
# Check if function name is "_g" to add attribute
if (line ~ /[[:space:]]+_g[[:space:]]*\(/) {
# Insert __attribute__((noduplicate)) before _g
sub(/[[:space:]]+_g/, " __attribute__((noduplicate)) _g", line);
}
# printf("\n");
printf("extern %s\n\n", line);
# Each entry is HOOK_API_DEFINITION(RET, NAME, (PARAMS), AMENDMENT),
# possibly spread over several lines. Collect until the parentheses
# balance, then split on the first two commas; the parameter tuple
# is the first parenthesised group.
function emit(s, i, ret, name, p, q, params) {
sub(/^[[:space:]]*HOOK_API_DEFINITION[[:space:]]*\(/, "", s);
gsub(/[[:space:]]+/, " ", s);
i = index(s, ","); ret = trim(substr(s, 1, i - 1)); s = substr(s, i + 1);
i = index(s, ","); name = trim(substr(s, 1, i - 1)); s = substr(s, i + 1);
p = index(s, "("); q = index(s, ")");
params = trim(substr(s, p + 1, q - p - 1));
if (name == "_g")
name = "__attribute__((noduplicate)) _g";
printf("extern %s %s(%s);\n\n", ret, name, params);
}
/^[[:space:]]*HOOK_API_DEFINITION[[:space:]]*\(/ { buf = ""; collecting = 1; }
collecting {
buf = buf " " $0;
if (gsub(/\(/, "(", buf) == gsub(/\)/, ")", buf)) {
collecting = 0;
emit(buf);
}
}
' "$APPLY_HOOK"
+1
View File
@@ -38,6 +38,7 @@
#define KEYLET_HOOK_DEFINITION 24
#define KEYLET_HOOK_STATE_DIR 25
#define KEYLET_CRON 26
#define KEYLET_MANIFEST 37
#define COMPARE_EQUAL 1U
#define COMPARE_LESS 2U
+57
View File
@@ -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
@@ -0,0 +1,105 @@
#pragma once
#include <shared_mutex>
// On Linux (glibc), std::shared_mutex wraps pthread_rwlock_t initialised
// with PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP. This means a
// pending exclusive lock() blocks new shared (reader) acquisitions,
// causing reader starvation when writers contend frequently.
//
// On macOS / ARM (libc++), std::shared_mutex is already reader-preferring,
// so the same code behaves differently across platforms.
//
// This header provides ReaderPreferringSharedMutex:
// - On Linux it wraps pthread_rwlock_t initialised with
// PTHREAD_RWLOCK_PREFER_READER_NP, matching macOS semantics.
// - On all other platforms it is a type alias for std::shared_mutex.
//
// The interface is identical to std::shared_mutex, so it works with
// std::shared_lock and std::unique_lock.
#if defined(__linux__)
#include <cerrno>
#include <pthread.h>
#include <system_error>
namespace ripple {
class ReaderPreferringSharedMutex
{
pthread_rwlock_t rwlock_;
public:
ReaderPreferringSharedMutex()
{
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_READER_NP);
int rc = pthread_rwlock_init(&rwlock_, &attr);
pthread_rwlockattr_destroy(&attr);
if (rc != 0)
throw std::system_error(
rc, std::system_category(), "pthread_rwlock_init");
}
~ReaderPreferringSharedMutex()
{
pthread_rwlock_destroy(&rwlock_);
}
ReaderPreferringSharedMutex(ReaderPreferringSharedMutex const&) = delete;
ReaderPreferringSharedMutex&
operator=(ReaderPreferringSharedMutex const&) = delete;
// Exclusive (writer) locking
void
lock()
{
pthread_rwlock_wrlock(&rwlock_);
}
bool
try_lock()
{
return pthread_rwlock_trywrlock(&rwlock_) == 0;
}
void
unlock()
{
pthread_rwlock_unlock(&rwlock_);
}
// Shared (reader) locking
void
lock_shared()
{
pthread_rwlock_rdlock(&rwlock_);
}
bool
try_lock_shared()
{
return pthread_rwlock_tryrdlock(&rwlock_) == 0;
}
void
unlock_shared()
{
pthread_rwlock_unlock(&rwlock_);
}
};
} // namespace ripple
#else // !__linux__
namespace ripple {
// macOS, Windows, etc. — std::shared_mutex is already reader-preferring.
using ReaderPreferringSharedMutex = std::shared_mutex;
} // namespace ripple
#endif
+3 -2
View File
@@ -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
+3 -3
View File
@@ -77,7 +77,7 @@ strUnHex(std::size_t strSize, Iterator begin, Iterator end)
if (strSize & 1)
{
int c = unxtab[*iter++];
int c = unxtab[static_cast<unsigned char>(*iter++)];
if (c < 0)
return {};
@@ -87,12 +87,12 @@ strUnHex(std::size_t strSize, Iterator begin, Iterator end)
while (iter != end)
{
int cHigh = unxtab[*iter++];
int cHigh = unxtab[static_cast<unsigned char>(*iter++)];
if (cHigh < 0)
return {};
int cLow = unxtab[*iter++];
int cLow = unxtab[static_cast<unsigned char>(*iter++)];
if (cLow < 0)
return {};
+60
View File
@@ -0,0 +1,60 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_BASICS_UTF8_H_INCLUDED
#define RIPPLE_BASICS_UTF8_H_INCLUDED
#include <xrpl/basics/Slice.h>
namespace ripple {
/** Check that a byte sequence is well-formed UTF-8, excluding the two
noncharacters U+FFFE and U+FFFF.
Rejects:
- overlong encodings;
- UTF-16 surrogate halves (U+D800..U+DFFF);
- scalar values above U+10FFFF, and 0xF5..0xFF lead bytes;
- stray continuation bytes;
- multi-byte sequences truncated by the end of the buffer;
- U+FFFE and U+FFFF (EF BF BE, EF BF BF).
U+FFFE and U+FFFF are well-formed, but have no use in a URI or a name
and exist to be misread: U+FFFE is a byte-swapped BOM, so a client that
transcodes to UTF-16/32 and sniffs the byte order can be pushed into
decoding the rest of the string with the wrong endianness, and both are
commonly used as in-band sentinels. They have always been rejected by
URIToken and HookName validation, and this function preserves that.
This is consensus-critical. For every input the previous decoder could
evaluate without reading past the end of the buffer, the result is
identical to it. The only difference is that truncated trailing
sequences, where the previous decoder read out of bounds, are now always
rejected. Any further change to what this accepts needs an amendment.
@param s The bytes to check.
@note Total: every byte read is within s.
*/
bool
isValidUTF8(Slice const& s) noexcept;
} // namespace ripple
#endif
+23 -16
View File
@@ -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));
}
};
+19 -8
View File
@@ -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...);
}
+15 -12
View File
@@ -313,6 +313,7 @@ enum keylet_code : uint32_t {
MPTOKEN = 34,
CREDENTIAL = 35,
PERMISSIONED_DOMAIN = 36,
MANIFEST = 37,
};
}
@@ -403,6 +404,18 @@ const double fee_base_multiplier = 1.1f;
using APIWhitelist = std::map<std::string, std::vector<uint8_t>>;
namespace detail {
template <class T>
constexpr uint8_t wasmTypeCode = sizeof(T) == 8 ? 0x7EU : 0x7FU;
template <class R, class... Args>
std::vector<uint8_t>
wasmSignature(R (*)(Args...))
{
return {wasmTypeCode<R>, wasmTypeCode<Args>...};
}
} // namespace detail
// RH NOTE: Find descriptions of api functions in ./impl/applyHook.cpp and
// hookapi.h (include for hooks) this is a map of the api name to its return
// code (vec[0] and its parameters vec[>0]) as wasm type codes
@@ -414,25 +427,15 @@ getImportWhitelist(Rules const& rules)
#pragma push_macro("HOOK_API_DEFINITION")
#undef HOOK_API_DEFINITION
#define int64_t 0x7EU
#define int32_t 0x7FU
#define uint32_t 0x7FU
#define HOOK_WRAP_PARAMS(...) __VA_ARGS__
#define HOOK_API_DEFINITION( \
RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, AMENDMENT) \
if (AMENDMENT == uint256{} || rules.enabled(AMENDMENT)) \
whitelist[#FUNCTION_NAME] = { \
RETURN_TYPE, HOOK_WRAP_PARAMS PARAMS_TUPLE};
whitelist[#FUNCTION_NAME] = \
detail::wasmSignature((RETURN_TYPE(*) PARAMS_TUPLE) nullptr);
#include "hook_api.macro"
#undef HOOK_API_DEFINITION
#undef HOOK_WRAP_PARAMS
#undef int64_t
#undef int32_t
#undef uint32_t
#pragma pop_macro("HOOK_API_DEFINITION")
return whitelist;
+1 -1
View File
@@ -87,7 +87,7 @@ parseSignedLeb128(
}
*end_offset = i;
if (shift < 64 && (b & 0x40U))
val |= (~0 << shift);
val |= (~int64_t{0} << shift);
return val;
}
+2 -2
View File
@@ -244,9 +244,9 @@
<< "Tried to accept/rollback but specified " \
"memory outside of the wasm instance " \
<< "limit when specifying a reason string"; \
if (!view.rules().enabled(fixHookExitOutOfBounds)) \
if (!view.rules().enabled(fix20260929)) \
return OUT_OF_BOUNDS; \
/* fixHookExitOutOfBounds: terminate as a rollback so the \
/* 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; \
+66 -139
View File
@@ -1,374 +1,301 @@
// int32_t _g(uint32_t guard_id, uint32_t maxiter);
// clang-format off
HOOK_API_DEFINITION(
int32_t, _g, (uint32_t, uint32_t),
int32_t, _g, (uint32_t guard_id, uint32_t maxiter),
uint256{})
// int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
HOOK_API_DEFINITION(
int64_t, accept, (uint32_t, uint32_t, int64_t),
int64_t, accept, (uint32_t read_ptr, uint32_t read_len, int64_t error_code),
uint256{})
// int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
HOOK_API_DEFINITION(
int64_t, rollback, (uint32_t, uint32_t, int64_t),
int64_t, rollback, (uint32_t read_ptr, uint32_t read_len, int64_t error_code),
uint256{})
// int64_t util_raddr(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, util_raddr, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, util_raddr, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t util_accid(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, util_accid, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, util_accid, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t util_verify(uint32_t dread_ptr, uint32_t dread_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t kread_ptr, uint32_t kread_len);
HOOK_API_DEFINITION(
int64_t, util_verify, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, util_verify, (uint32_t dread_ptr, uint32_t dread_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t kread_ptr, uint32_t kread_len),
uint256{})
// int64_t util_sha512h(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, util_sha512h, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, util_sha512h, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t util_keylet(uint32_t write_ptr, uint32_t write_len, uint32_t keylet_type, uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f);
HOOK_API_DEFINITION(
int64_t, util_keylet, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, util_keylet, (uint32_t write_ptr, uint32_t write_len, uint32_t keylet_type, uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f),
uint256{})
// int64_t sto_validate(uint32_t tread_ptr, uint32_t tread_len);
HOOK_API_DEFINITION(
int64_t, sto_validate, (uint32_t, uint32_t),
int64_t, sto_validate, (uint32_t tread_ptr, uint32_t tread_len),
uint256{})
// int64_t sto_subfield(uint32_t read_ptr, uint32_t read_len, uint32_t field_id);
HOOK_API_DEFINITION(
int64_t, sto_subfield, (uint32_t, uint32_t, uint32_t),
int64_t, sto_subfield, (uint32_t read_ptr, uint32_t read_len, uint32_t field_id),
uint256{})
// int64_t sto_subarray(uint32_t read_ptr, uint32_t read_len, uint32_t array_id);
HOOK_API_DEFINITION(
int64_t, sto_subarray, (uint32_t, uint32_t, uint32_t),
int64_t, sto_subarray, (uint32_t read_ptr, uint32_t read_len, uint32_t array_id),
uint256{})
// int64_t sto_emplace(uint32_t write_ptr, uint32_t write_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t fread_ptr, uint32_t fread_len, uint32_t field_id);
HOOK_API_DEFINITION(
int64_t, sto_emplace, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, sto_emplace, (uint32_t write_ptr, uint32_t write_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t fread_ptr, uint32_t fread_len, uint32_t field_id),
uint256{})
// int64_t sto_erase(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t field_id);
HOOK_API_DEFINITION(
int64_t, sto_erase, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, sto_erase, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t field_id),
uint256{})
// int64_t etxn_burden();
HOOK_API_DEFINITION(
int64_t, etxn_burden, (),
uint256{})
// int64_t etxn_details(uint32_t write_ptr, uint32_t write_len);
HOOK_API_DEFINITION(
int64_t, etxn_details, (uint32_t, uint32_t),
int64_t, etxn_details, (uint32_t write_ptr, uint32_t write_len),
uint256{})
// int64_t etxn_fee_base(uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, etxn_fee_base, (uint32_t, uint32_t),
int64_t, etxn_fee_base, (uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t etxn_reserve(uint32_t count);
HOOK_API_DEFINITION(
int64_t, etxn_reserve, (uint32_t),
int64_t, etxn_reserve, (uint32_t count),
uint256{})
// int64_t etxn_generation();
HOOK_API_DEFINITION(
int64_t, etxn_generation, (),
uint256{})
// int64_t etxn_nonce(uint32_t write_ptr, uint32_t write_len);
HOOK_API_DEFINITION(
int64_t, etxn_nonce, (uint32_t, uint32_t),
int64_t, etxn_nonce, (uint32_t write_ptr, uint32_t write_len),
uint256{})
// int64_t emit(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, emit, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, emit, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t float_set(int32_t exponent, int64_t mantissa);
HOOK_API_DEFINITION(
int64_t, float_set, (int32_t, int64_t),
int64_t, float_set, (int32_t exponent, int64_t mantissa),
uint256{})
// int64_t float_multiply(int64_t float1, int64_t float2);
HOOK_API_DEFINITION(
int64_t, float_multiply, (int64_t, int64_t),
int64_t, float_multiply, (int64_t float1, int64_t float2),
uint256{})
// int64_t float_mulratio(int64_t float1, uint32_t round_up, uint32_t numerator, uint32_t denominator);
HOOK_API_DEFINITION(
int64_t, float_mulratio, (int64_t, uint32_t, uint32_t, uint32_t),
int64_t, float_mulratio, (int64_t float1, uint32_t round_up, uint32_t numerator, uint32_t denominator),
uint256{})
// int64_t float_negate(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_negate, (int64_t),
int64_t, float_negate, (int64_t float1),
uint256{})
// int64_t float_compare(int64_t float1, int64_t float2, uint32_t mode);
HOOK_API_DEFINITION(
int64_t, float_compare, (int64_t, int64_t, uint32_t),
int64_t, float_compare, (int64_t float1, int64_t float2, uint32_t mode),
uint256{})
// int64_t float_sum(int64_t float1, int64_t float2);
HOOK_API_DEFINITION(
int64_t, float_sum, (int64_t, int64_t),
int64_t, float_sum, (int64_t float1, int64_t float2),
uint256{})
// int64_t float_sto(uint32_t write_ptr, uint32_t write_len, uint32_t cread_ptr, uint32_t cread_len, uint32_t iread_ptr, uint32_t iread_len, int64_t float1, uint32_t field_code);
HOOK_API_DEFINITION(
int64_t, float_sto, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, int64_t, uint32_t),
int64_t, float_sto, (uint32_t write_ptr, uint32_t write_len, uint32_t cread_ptr, uint32_t cread_len, uint32_t iread_ptr, uint32_t iread_len, int64_t float1, uint32_t field_code),
uint256{})
// int64_t float_sto_set(uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, float_sto_set, (uint32_t, uint32_t),
int64_t, float_sto_set, (uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t float_invert(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_invert, (int64_t),
int64_t, float_invert, (int64_t float1),
uint256{})
// int64_t float_divide(int64_t float1, int64_t float2);
HOOK_API_DEFINITION(
int64_t, float_divide, (int64_t, int64_t),
int64_t, float_divide, (int64_t float1, int64_t float2),
uint256{})
// int64_t float_one();
HOOK_API_DEFINITION(
int64_t, float_one, (),
uint256{})
// int64_t float_mantissa(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_mantissa, (int64_t),
int64_t, float_mantissa, (int64_t float1),
uint256{})
// int64_t float_sign(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_sign, (int64_t),
int64_t, float_sign, (int64_t float1),
uint256{})
// int64_t float_int(int64_t float1, uint32_t decimal_places, uint32_t abs);
HOOK_API_DEFINITION(
int64_t, float_int, (int64_t, uint32_t, uint32_t),
int64_t, float_int, (int64_t float1, uint32_t decimal_places, uint32_t abs),
uint256{})
// int64_t float_log(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_log, (int64_t),
int64_t, float_log, (int64_t float1),
uint256{})
// int64_t float_root(int64_t float1, uint32_t n);
HOOK_API_DEFINITION(
int64_t, float_root, (int64_t, uint32_t),
int64_t, float_root, (int64_t float1, uint32_t n),
uint256{})
// int64_t fee_base();
HOOK_API_DEFINITION(
int64_t, fee_base, (),
uint256{})
// int64_t ledger_seq();
HOOK_API_DEFINITION(
int64_t, ledger_seq, (),
uint256{})
// int64_t ledger_last_time();
HOOK_API_DEFINITION(
int64_t, ledger_last_time, (),
uint256{})
// int64_t ledger_last_hash(uint32_t write_ptr, uint32_t write_len);
HOOK_API_DEFINITION(
int64_t, ledger_last_hash, (uint32_t, uint32_t),
int64_t, ledger_last_hash, (uint32_t write_ptr, uint32_t write_len),
uint256{})
// int64_t ledger_nonce(uint32_t write_ptr, uint32_t write_len);
HOOK_API_DEFINITION(
int64_t, ledger_nonce, (uint32_t, uint32_t),
int64_t, ledger_nonce, (uint32_t write_ptr, uint32_t write_len),
uint256{})
// int64_t ledger_keylet(uint32_t write_ptr, uint32_t write_len, uint32_t lread_ptr, uint32_t lread_len, uint32_t hread_ptr, uint32_t hread_len);
HOOK_API_DEFINITION(
int64_t, ledger_keylet, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, ledger_keylet, (uint32_t write_ptr, uint32_t write_len, uint32_t lread_ptr, uint32_t lread_len, uint32_t hread_ptr, uint32_t hread_len),
uint256{})
// int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
HOOK_API_DEFINITION(
int64_t, hook_account, (uint32_t, uint32_t),
int64_t, hook_account, (uint32_t write_ptr, uint32_t write_len),
uint256{})
// int64_t hook_hash(uint32_t write_ptr, uint32_t write_len, int32_t hook_no);
HOOK_API_DEFINITION(
int64_t, hook_hash, (uint32_t, uint32_t, int32_t),
int64_t, hook_hash, (uint32_t write_ptr, uint32_t write_len, int32_t hook_no),
uint256{})
// int64_t hook_param_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t hread_ptr, uint32_t hread_len);
HOOK_API_DEFINITION(
int64_t, hook_param_set, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, hook_param_set, (uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t hread_ptr, uint32_t hread_len),
uint256{})
// int64_t hook_param(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, hook_param, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, hook_param, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t hook_again();
HOOK_API_DEFINITION(
int64_t, hook_again, (),
uint256{})
// int64_t hook_skip(uint32_t read_ptr, uint32_t read_len, uint32_t flags);
HOOK_API_DEFINITION(
int64_t, hook_skip, (uint32_t, uint32_t, uint32_t),
int64_t, hook_skip, (uint32_t read_ptr, uint32_t read_len, uint32_t flags),
uint256{})
// int64_t hook_pos();
HOOK_API_DEFINITION(
int64_t, hook_pos, (),
uint256{})
// int64_t slot(uint32_t write_ptr, uint32_t write_len, uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot, (uint32_t, uint32_t, uint32_t),
int64_t, slot, (uint32_t write_ptr, uint32_t write_len, uint32_t slot),
uint256{})
// int64_t slot_clear(uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot_clear, (uint32_t),
int64_t, slot_clear, (uint32_t slot),
uint256{})
// int64_t slot_count(uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot_count, (uint32_t),
int64_t, slot_count, (uint32_t slot),
uint256{})
// int64_t slot_set(uint32_t read_ptr, uint32_t read_len, uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot_set, (uint32_t, uint32_t, uint32_t),
int64_t, slot_set, (uint32_t read_ptr, uint32_t read_len, uint32_t slot),
uint256{})
// int64_t slot_size(uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot_size, (uint32_t),
int64_t, slot_size, (uint32_t slot),
uint256{})
// int64_t slot_subarray(uint32_t parent_slot, uint32_t array_id, uint32_t new_slot);
HOOK_API_DEFINITION(
int64_t, slot_subarray, (uint32_t, uint32_t, uint32_t),
int64_t, slot_subarray, (uint32_t parent_slot, uint32_t array_id, uint32_t new_slot),
uint256{})
// int64_t slot_subfield(uint32_t parent_slot, uint32_t field_id, uint32_t new_slot);
HOOK_API_DEFINITION(
int64_t, slot_subfield, (uint32_t, uint32_t, uint32_t),
int64_t, slot_subfield, (uint32_t parent_slot, uint32_t field_id, uint32_t new_slot),
uint256{})
// int64_t slot_type(uint32_t slot_no, uint32_t flags);
HOOK_API_DEFINITION(
int64_t, slot_type, (uint32_t, uint32_t),
int64_t, slot_type, (uint32_t slot_no, uint32_t flags),
uint256{})
// int64_t slot_float(uint32_t slot_no);
HOOK_API_DEFINITION(
int64_t, slot_float, (uint32_t),
int64_t, slot_float, (uint32_t slot_no),
uint256{})
// int64_t state_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len);
HOOK_API_DEFINITION(
int64_t, state_set, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, state_set, (uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len),
uint256{})
// int64_t state_foreign_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len);
HOOK_API_DEFINITION(
int64_t, state_foreign_set, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, state_foreign_set, (uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len),
uint256{})
// int64_t state(uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len);
HOOK_API_DEFINITION(
int64_t, state, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, state, (uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len),
uint256{})
// int64_t state_foreign(uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len);
HOOK_API_DEFINITION(
int64_t, state_foreign, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, state_foreign, (uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len),
uint256{})
// int64_t trace(uint32_t mread_ptr, uint32_t mread_len, uint32_t dread_ptr, uint32_t dread_len, uint32_t as_hex);
HOOK_API_DEFINITION(
int64_t, trace, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, trace, (uint32_t mread_ptr, uint32_t mread_len, uint32_t dread_ptr, uint32_t dread_len, uint32_t as_hex),
uint256{})
// int64_t trace_num(uint32_t read_ptr, uint32_t read_len, int64_t number);
HOOK_API_DEFINITION(
int64_t, trace_num, (uint32_t, uint32_t, int64_t),
int64_t, trace_num, (uint32_t read_ptr, uint32_t read_len, int64_t number),
uint256{})
// int64_t trace_float(uint32_t read_ptr, uint32_t read_len, int64_t float1);
HOOK_API_DEFINITION(
int64_t, trace_float, (uint32_t, uint32_t, int64_t),
int64_t, trace_float, (uint32_t read_ptr, uint32_t read_len, int64_t float1),
uint256{})
// int64_t otxn_burden();
HOOK_API_DEFINITION(
int64_t, otxn_burden, (),
uint256{})
// int64_t otxn_field(uint32_t write_ptr, uint32_t write_len, uint32_t field_id);
HOOK_API_DEFINITION(
int64_t, otxn_field, (uint32_t, uint32_t, uint32_t),
int64_t, otxn_field, (uint32_t write_ptr, uint32_t write_len, uint32_t field_id),
uint256{})
// int64_t otxn_generation();
HOOK_API_DEFINITION(
int64_t, otxn_generation, (),
uint256{})
// int64_t otxn_id(uint32_t write_ptr, uint32_t write_len, uint32_t flags);
HOOK_API_DEFINITION(
int64_t, otxn_id, (uint32_t, uint32_t, uint32_t),
int64_t, otxn_id, (uint32_t write_ptr, uint32_t write_len, uint32_t flags),
uint256{})
// int64_t otxn_type();
HOOK_API_DEFINITION(
int64_t, otxn_type, (),
uint256{})
// int64_t otxn_slot(uint32_t slot_no);
HOOK_API_DEFINITION(
int64_t, otxn_slot, (uint32_t),
int64_t, otxn_slot, (uint32_t slot_no),
uint256{})
// int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, otxn_param, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, otxn_param, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t meta_slot(uint32_t slot_no);
HOOK_API_DEFINITION(
int64_t, meta_slot, (uint32_t),
int64_t, meta_slot, (uint32_t slot_no),
uint256{})
// int64_t xpop_slot(uint32_t slot_no_tx, uint32_t slot_no_meta);
HOOK_API_DEFINITION(
int64_t, xpop_slot, (uint32_t, uint32_t),
int64_t, xpop_slot, (uint32_t slot_no_tx, uint32_t slot_no_meta),
featureHooksUpdate1)
// int64_t prepare(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, prepare, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, prepare, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
featureHooksUpdate2)
// clang-format on
+1 -1
View File
@@ -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);
}
+49 -4
View File
@@ -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
+1
View File
@@ -69,6 +69,7 @@ enum TELcodes : TERUnderlyingType {
telIMPORT_VL_KEY_NOT_RECOGNISED,
telCAN_NOT_QUEUE_IMPORT,
telENV_RPC_FAILED,
telMANIFEST_FEE_MISMATCH,
};
//------------------------------------------------------------------------------
+4 -5
View File
@@ -34,12 +34,11 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
XRPL_FIX (20261001, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (HookNameValidation, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (HookExitOutOfBounds, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookAPISType, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (20261005, Supported::yes, VoteBehavior::DefaultYes)
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::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(OnChainManifests, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (GuardDepth32, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(NamedHooks, Supported::yes, VoteBehavior::DefaultNo)
@@ -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
+4
View File
@@ -47,6 +47,7 @@ JSS(AL_hit_rate); // out: GetCounts
JSS(AcceptedCredentials); // out: AccountObjects
JSS(Account); // in: TransactionSign; field.
JSS(AMMID); // field
JSS(API); // out: RPC server_definitions (HOOK)
JSS(Amount); // in: TransactionSign; field.
JSS(Authorize); // field
JSS(Amount2); // in/out: AMM IOU/XRP pool, deposit, withdraw amount
@@ -68,6 +69,9 @@ JSS(Destination); // in: TransactionSign; field.
JSS(EPrice); // in: AMM Deposit option
JSS(Fee); // in/out: TransactionSign; field.
JSS(FIELDS); // out: RPC server_definitions
JSS(HOOK); // out: RPC server_definitions
JSS(KEYLET_TYPES); // out: RPC server_definitions (HOOK)
JSS(RETURN_CODES); // out: RPC server_definitions (HOOK)
JSS(Flags); // in/out: TransactionSign; field.
JSS(GenesisMints);
JSS(GovernanceMarks);
+4 -3
View File
@@ -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
+2 -4
View File
@@ -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>
+11 -9
View File
@@ -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)
{
@@ -361,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,
@@ -380,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,
@@ -411,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,
@@ -453,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
@@ -495,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
+2 -2
View File
@@ -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))
{
}
+20 -7
View File
@@ -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>
+10 -9
View File
@@ -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);
}
+2 -2
View File
@@ -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,
+3 -3
View File
@@ -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,
+17 -13
View File
@@ -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);
+111
View File
@@ -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
+1 -1
View File
@@ -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.
*/
+63 -30
View File
@@ -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);
}
//-----------------------------------------------------------------------------
+101
View File
@@ -0,0 +1,101 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpl/basics/UTF8.h>
namespace ripple {
bool
isValidUTF8(Slice const& s) noexcept
{
std::size_t const size = s.size();
// Markus Kuhn's utf8_check.c
// (https://www.cl.cam.ac.uk/~mgk25/ucs/utf8_check.c), the decoder
// URIToken has always used, with one change: each branch establishes
// that the whole sequence is present before reading any continuation
// byte, so a truncated tail is invalid rather than an out-of-bounds read.
std::size_t i = 0;
while (i < size)
{
std::uint8_t const c0 = s[i];
if (c0 < 0x80)
{
// 0xxxxxxx
++i;
continue;
}
if ((c0 & 0xE0) == 0xC0)
{
// 110xxxxx 10xxxxxx
if (size - i < 2)
return false;
if ((s[i + 1] & 0xC0) != 0x80)
return false;
if ((c0 & 0xFE) == 0xC0) // overlong
return false;
i += 2;
continue;
}
if ((c0 & 0xF0) == 0xE0)
{
// 1110xxxx 10xxxxxx 10xxxxxx
if (size - i < 3)
return false;
std::uint8_t const c1 = s[i + 1];
if ((c1 & 0xC0) != 0x80 || (s[i + 2] & 0xC0) != 0x80)
return false;
if (c0 == 0xE0 && (c1 & 0xE0) == 0x80) // overlong
return false;
if (c0 == 0xED && (c1 & 0xE0) == 0xA0) // UTF-16 surrogate
return false;
if (c0 == 0xEF && c1 == 0xBF &&
(s[i + 2] & 0xFE) == 0xBE) // U+FFFE or U+FFFF
return false;
i += 3;
continue;
}
if ((c0 & 0xF8) == 0xF0)
{
// 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
if (size - i < 4)
return false;
std::uint8_t const c1 = s[i + 1];
if ((c1 & 0xC0) != 0x80 || (s[i + 2] & 0xC0) != 0x80 ||
(s[i + 3] & 0xC0) != 0x80)
return false;
if (c0 == 0xF0 && (c1 & 0xF0) == 0x80) // overlong
return false;
if (c0 > 0xF4 || (c0 == 0xF4 && c1 > 0x8F)) // > U+10FFFF
return false;
i += 4;
continue;
}
// Stray continuation byte or 0xF8..0xFF.
return false;
}
return true;
}
} // namespace ripple
+76 -38
View File
@@ -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
+4 -4
View File
@@ -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
+5 -1
View File
@@ -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
+13 -1
View File
@@ -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);
+2 -2
View File
@@ -56,9 +56,9 @@ getVersionString()
(versionString == std::string("0.") + std::string("0.0") ||
versionString == std::string("0.0.0+DEBUG")))
{
std::string y = std::string(__DATE__ + 7);
std::string y = std::string(&__DATE__[7]);
std::string d = std::string(
__DATE__ + 4 + (__DATE__[4] == ' ' ? 1 : 0),
&__DATE__[4 + (__DATE__[4] == ' ' ? 1 : 0)],
__DATE__[4] == ' ' ? 1 : 2);
std::string m;
switch (__DATE__[0])
+1
View File
@@ -174,6 +174,7 @@ transResults()
MAKE_ERROR(telIMPORT_VL_KEY_NOT_RECOGNISED, "Import vl key was not recognized."),
MAKE_ERROR(telCAN_NOT_QUEUE_IMPORT, "Import transaction was not able to be directly applied and cannot be queued."),
MAKE_ERROR(telENV_RPC_FAILED, "Unit test RPC failure."),
MAKE_ERROR(telMANIFEST_FEE_MISMATCH, "Unsigned SetManifest fee is not, or cannot be confirmed as, canonical for this server's ledger fee settings."),
MAKE_ERROR(temMALFORMED, "Malformed transaction."),
MAKE_ERROR(temBAD_AMM_TOKENS, "Malformed: Invalid LPTokens."),
+8 -2
View File
@@ -545,7 +545,7 @@ b58_to_b256_be(std::string_view input, std::span<std::uint8_t> out)
XRPL_ASSERT(
num_b_58_10_coeffs <= b_58_10_coeff.size(),
"ripple::b58_fast::detail::b58_to_b256_be : maximum coeff");
for (auto c : input.substr(0, partial_coeff_len))
for (unsigned char const c : input.substr(0, partial_coeff_len))
{
auto cur_val = ::ripple::alphabetReverse[c];
if (cur_val < 0)
@@ -559,7 +559,7 @@ b58_to_b256_be(std::string_view input, std::span<std::uint8_t> out)
{
for (int j = 0; j < num_full_coeffs; ++j)
{
auto c = input[partial_coeff_len + j * 10 + i];
unsigned char const c = input[partial_coeff_len + j * 10 + i];
auto cur_val = ::ripple::alphabetReverse[c];
if (cur_val < 0)
{
@@ -580,6 +580,12 @@ b58_to_b256_be(std::string_view input, std::span<std::uint8_t> out)
{
std::uint64_t const c = b_58_10_coeff[i];
// Each digit is < 58, so at most 4 words are in use here and the
// span below always fits `result`. Guard the invariant anyway rather
// than letting a violation index past the end of the stack array.
if (cur_result_size >= result.size())
return Unexpected(TokenCodecErrc::inputTooLarge);
{
auto code = ripple::b58_fast::detail::inplace_bigint_mul(
std::span(&result[0], cur_result_size + 1), B_58_10);
+4 -1
View File
@@ -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&)
{
+14
View File
@@ -563,6 +563,20 @@ public:
env(reward::claim(alice),
reward::issuer(issuer),
ter(tecNO_TARGET));
env.close();
}
// Invalid HookName
{
auto hookObj = hso(jtx::genesis::AcceptHook, overrideFlag);
hookObj[jss::HookOn] = to_string(~UINT256_BIT[ttCLAIM_REWARD]);
hookObj[jss::HookName] = "4142434445";
env(hook(issuer, {{hookObj}}, 0), fee(XRP(1)));
env.close();
env(reward::claim(alice),
reward::issuer(issuer),
ter(tecNO_TARGET));
env.close();
}
// Vaild HookOn
{
+5 -4
View File
@@ -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;
+16 -4
View File
@@ -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)
+362 -4
View File
@@ -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);
}
};
+280 -44
View File
@@ -590,6 +590,134 @@ public:
}
}
void
test_emitted_txn_entry(FeatureBitset features)
{
testcase("Test emitted txn ledger entry");
using namespace jtx;
using namespace hook;
using namespace hook_api;
auto const alice = Account{"alice"};
for (bool const withFix : {true, false})
{
Env env{*this, withFix ? features : features - fix20261005};
// emissions are only written into non-open views, so apply on
// top of the last closed ledger the way consensus does
auto const closed = env.closed();
OpenView ov{&*closed};
BEAST_EXPECT(!ov.open());
STTx const invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
ApplyContext applyCtx{
env.app(),
ov,
invokeTx,
tesSUCCESS,
env.current()->fees().base,
tapNONE,
env.journal};
STTx const emitTx = STTx(ttINVOKE, [&](STObject& obj) {
obj[sfAccount] = alice.id();
obj[sfSequence] = 0;
obj[sfSigningPubKey] = Slice{};
obj[sfFirstLedgerSequence] = closed->seq() + 1;
obj[sfLastLedgerSequence] = closed->seq() + 5;
obj[sfFee] = closed->fees().base;
auto& emitDetails = obj.peekFieldObject(sfEmitDetails);
emitDetails[sfEmitGeneration] = 1;
emitDetails[sfEmitBurden] = 1;
emitDetails[sfEmitParentTxnID] = invokeTx.getTransactionID();
emitDetails[sfEmitNonce] = uint256();
emitDetails[sfEmitHookHash] = uint256();
});
std::string reason;
auto const emitted = std::make_shared<ripple::Transaction>(
std::make_shared<STTx const>(emitTx), reason, env.app());
std::queue<std::shared_ptr<ripple::Transaction>> emittedTxn;
emittedTxn.push(emitted);
auto hookCtx = makeStubHookContext(
applyCtx,
alice.id(),
alice.id(),
{.result = {.emittedTxn = emittedTxn}});
BEAST_EXPECT(
hook::finalizeHookResult(hookCtx.result, applyCtx, true) ==
tesSUCCESS);
auto const kl = keylet::emittedTxn(emitted->getID());
auto const sle = applyCtx.view().peek(kl);
if (!BEAST_EXPECT(sle))
continue;
Serializer expected;
emitTx.add(expected);
// in memory: with the fix sfEmittedTxn occupies its template
// slot exactly once, without it a duplicate is appended behind
// the (non-present) template slot and the field reads absent
auto const entries =
std::count_if(sle->begin(), sle->end(), [](STBase const& f) {
return f.getFName() == sfEmittedTxn;
});
BEAST_EXPECT(entries == (withFix ? 1 : 2));
BEAST_EXPECT(sle->isFieldPresent(sfEmittedTxn) == withFix);
// serialized: identical either way, re-reading the entry always
// yields exactly the emitted transaction
{
Serializer s;
sle->add(s);
SerialIter sit(s.slice());
STLedgerEntry const reread(sit, kl.key);
BEAST_EXPECT(reread.isFieldPresent(sfEmittedTxn));
Serializer txn;
reread.peekAtField(sfEmittedTxn).add(txn);
BEAST_EXPECT(txn.peekData() == expected.peekData());
}
// a hook running later in the same ledger can tell them apart:
// slot_subfield only finds sfEmittedTxn with the fix
auto readerCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = readerCtx.api();
Bytes klBytes{
static_cast<uint8_t>((kl.type >> 8) & 0xFFU),
static_cast<uint8_t>(kl.type & 0xFFU)};
klBytes.insert(klBytes.end(), kl.key.begin(), kl.key.end());
auto const slotNo = api.slot_set(klBytes, 0);
if (!BEAST_EXPECT(slotNo.has_value()))
continue;
auto const sub =
api.slot_subfield(slotNo.value(), sfEmittedTxn.getCode(), 0);
if (!withFix)
{
BEAST_EXPECT(!sub.has_value() && sub.error() == DOESNT_EXIST);
continue;
}
if (!BEAST_EXPECT(sub.has_value()))
continue;
auto const field = api.slot(sub.value());
if (!BEAST_EXPECT(field.has_value()))
continue;
Serializer s;
field.value()->add(s);
BEAST_EXPECT(s.peekData() == expected.peekData());
}
}
void
test_etxn_details(FeatureBitset features)
{
@@ -732,6 +860,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
@@ -1778,11 +1956,31 @@ public:
BEAST_EXPECT(api.float_set(-50, 0).value() == 0);
BEAST_EXPECT(api.float_set(0, 0).value() == 0);
// an exponent lower than -96 should produce an invalid float error
BEAST_EXPECT(api.float_set(-97, 1).error() == INVALID_FLOAT);
// fix20261005: exponent out of range is reported as
// EXPONENT_UNDERSIZED / EXPONENT_OVERSIZED; before the fix both
// cases were reported as INVALID_FLOAT
bool const fixExpCodes = env.current()->rules().enabled(fix20261005);
auto const undersized =
fixExpCodes ? EXPONENT_UNDERSIZED : INVALID_FLOAT;
auto const oversized = fixExpCodes ? EXPONENT_OVERSIZED : INVALID_FLOAT;
// an exponent larger than +96 should produce an invalid float error
BEAST_EXPECT(api.float_set(+97, 1).error() == INVALID_FLOAT);
// an exponent lower than -96 should produce an underflow error
BEAST_EXPECT(api.float_set(-97, 1).error() == undersized);
// an exponent larger than +96 should produce an overflow error
BEAST_EXPECT(api.float_set(+97, 1).error() == oversized);
// the -96..80 range applies to the exponent after the mantissa has
// been normalized to 16 digits (1 -> 1e15, exponent - 15)
auto const oneE15 = [](int32_t exp) {
return hook::hook_float::make_float(1000000000000000ULL, exp, false)
.value();
};
BEAST_EXPECT(api.float_set(81, 1).value() == oneE15(66));
BEAST_EXPECT(api.float_set(95, 1).value() == oneE15(80));
BEAST_EXPECT(api.float_set(96, 1).error() == oversized);
BEAST_EXPECT(api.float_set(-81, 1).value() == oneE15(-96));
BEAST_EXPECT(api.float_set(-82, 1).error() == undersized);
// clang-format off
std::vector<std::tuple<int32_t, int64_t, uint64_t>> tests = {
@@ -2172,22 +2370,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
@@ -3234,21 +3429,30 @@ public:
using namespace hook_api;
auto const alice = Account{"alice"};
Env env{*this, features};
STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
OpenView ov{*env.current()};
ApplyContext applyCtx = createApplyContext(env, ov, invokeTx);
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
for (bool const withFix : {true, false})
{
Env env{*this, withFix ? features : features - fix20261005};
STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
OpenView ov{*env.current()};
ApplyContext applyCtx = createApplyContext(env, ov, invokeTx);
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
// Invalid argument (wrong size)
BEAST_EXPECT(
api.slot_set(Bytes{1, 2, 3}, 0).error() == INVALID_ARGUMENT);
// Invalid argument (slot_no beyond max)
BEAST_EXPECT(
api.slot_set(Bytes(32, 0), hook_api::max_slots + 1).error() ==
INVALID_ARGUMENT);
// Invalid argument (wrong size)
BEAST_EXPECT(
api.slot_set(Bytes{1, 2, 3}, 0).error() == INVALID_ARGUMENT);
// Invalid argument (slot_no beyond max)
BEAST_EXPECT(
api.slot_set(Bytes(32, 0), hook_api::max_slots + 1).error() ==
INVALID_ARGUMENT);
// 32-byte txn hash: rejected with fix20261005, looked up (and
// not found here) without it
BEAST_EXPECT(
api.slot_set(Bytes(32, 0), 1).error() ==
(withFix ? INVALID_ARGUMENT : DOESNT_EXIST));
}
}
void
@@ -4000,7 +4204,7 @@ public:
sfBaseAsset.getCode(),
"011A0000000000000000000000005553440000000000"},
};
if (env.closed()->rules().enabled(fixHookAPISType))
if (env.closed()->rules().enabled(fix20260929))
{
// encodings the legacy parser measures incorrectly
data_list.insert(
@@ -4019,7 +4223,7 @@ public:
"00000"},
});
}
bool const fixEnabled = env.closed()->rules().enabled(fixHookAPISType);
bool const fixEnabled = env.closed()->rules().enabled(fix20260929);
for (auto const& data : data_list)
{
auto source_object = _source_object;
@@ -4120,7 +4324,7 @@ public:
// should be "F9EA7D02BEEFE1F1" {Memos:[{Memo:{MemoData:"BEEF"}}]}
auto const invalid_end_marker = *strUnHex("F9EA7D02BEEFE100");
auto const result = api.sto_subarray(invalid_end_marker, 0);
if (env.closed()->rules().enabled(fixHookAPISType))
if (env.closed()->rules().enabled(fix20260929))
{
BEAST_EXPECT(!result.has_value());
BEAST_EXPECT(result.error() == PARSE_ERROR);
@@ -4136,7 +4340,7 @@ public:
// truncated.
auto const invalid_tail = *strUnHex("F9EA7D02BEEFE1EA7D02BEEFF1");
auto const result = api.sto_subarray(invalid_tail, 0);
if (env.closed()->rules().enabled(fixHookAPISType))
if (env.closed()->rules().enabled(fix20260929))
{
BEAST_EXPECT(!result.has_value());
BEAST_EXPECT(result.error() == PARSE_ERROR);
@@ -4210,7 +4414,7 @@ public:
auto const invalid_tail = Blob{data.begin(), data.end() - 1};
auto const result =
api.sto_subfield(invalid_tail, sfSequence.getCode());
if (env.closed()->rules().enabled(fixHookAPISType))
if (env.closed()->rules().enabled(fix20260929))
{
BEAST_EXPECT(!result.has_value());
BEAST_EXPECT(result.error() == PARSE_ERROR);
@@ -4268,8 +4472,20 @@ public:
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
BEAST_EXPECT(api.sto_validate(Bytes{}).error() == TOO_SMALL);
BEAST_EXPECT(api.sto_validate(Bytes{0x00}).error() == TOO_SMALL);
// a buffer shorter than two bytes can't be a valid STObject:
// fix20261005 reports it as invalid rather than as TOO_SMALL
if (env.closed()->rules().enabled(fix20261005))
{
BEAST_EXPECT(api.sto_validate(Bytes{}).value() == false);
BEAST_EXPECT(api.sto_validate(Bytes{0x00}).value() == false);
BEAST_EXPECT(api.sto_validate(Bytes{0xE1}).value() == false);
}
else
{
BEAST_EXPECT(api.sto_validate(Bytes{}).error() == TOO_SMALL);
BEAST_EXPECT(api.sto_validate(Bytes{0x00}).error() == TOO_SMALL);
BEAST_EXPECT(api.sto_validate(Bytes{0xE1}).error() == TOO_SMALL);
}
// { Memo: {MemoData: "BEEF"} }
auto const memos = *strUnHex("EA7D02BEEFE1");
@@ -4343,6 +4559,22 @@ public:
BEAST_EXPECT(accid.has_value());
auto aliceid = alice.id();
BEAST_EXPECT(accid.value() == Bytes(aliceid.begin(), aliceid.end()));
// Bytes outside the base58 alphabet, including >= 0x80, must be
// rejected wherever they appear (signed-char lookup regression)
{
std::string const addr = alice.human();
for (std::size_t pos = 0; pos < addr.size(); ++pos)
{
for (int const b : {0x00, 0x7f, 0x80, 0xc2, 0xff})
{
std::string bad = addr;
bad[pos] = static_cast<char>(b);
auto const r = api.util_accid(bad);
BEAST_EXPECT(!r && r.error() == INVALID_ARGUMENT);
}
}
}
}
void
@@ -4712,12 +4944,14 @@ public:
test_prepare(features);
test_emit(features);
test_emitted_txn_entry(features);
test_etxn_burden(features);
test_etxn_generation(features);
test_otxn_burden(features);
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);
@@ -4739,6 +4973,7 @@ public:
test_float_one(features);
test_float_root(features);
test_float_set(features);
test_float_set(features - fix20261005);
test_float_sign(features);
test_float_sto(features);
test_float_sto_set(features);
@@ -4783,13 +5018,14 @@ public:
test_state_set(features);
test_sto_emplace(features);
test_sto_emplace(features - fixHookAPISType);
test_sto_emplace(features - fix20260929);
// test_sto_erase(features); // tested in test_sto_emplace
test_sto_subarray(features);
test_sto_subarray(features - fixHookAPISType);
test_sto_subarray(features - fix20260929);
test_sto_subfield(features);
test_sto_subfield(features - fixHookAPISType);
test_sto_subfield(features - fix20260929);
test_sto_validate(features);
test_sto_validate(features - fix20261005);
test_trace(features);
test_trace_float(features);
+389 -4
View File
@@ -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);
}
};
+1 -1
View File
@@ -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(
+203
View File
@@ -0,0 +1,203 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx/Env.h>
#include <test/jtx/envconfig.h>
#include <xrpld/app/main/LoadManager.h>
#include <xrpld/app/misc/LoadFeeTrack.h>
#include <xrpl/beast/unit_test.h>
#include <cstdint>
namespace ripple {
/**
* Unit tests for LoadFeeTrack fee adjustment mechanics.
*
* These tests document the expected per-second behaviour of
* raiseLocalFee() and lowerLocalFee() as called by LoadManager::run().
*
* Background: prior to the fix in this PR, the fee adjustment block in
* LoadManager::run() was placed *outside* the while loop, meaning
* raiseLocalFee()/lowerLocalFee() fired only once on shutdown rather
* than every second during normal operation. localTxnLoadFee_ (and thus
* load_factor_local in server_info) was therefore never adjusted by
* local job queue load during normal operation.
*/
class LoadManager_test : public beast::unit_test::suite
{
public:
// The normal/minimum fee factor (kLftNormalFee in LoadFeeTrack)
static constexpr std::uint32_t kNormalFee = 256;
void
testRaiseHysteresis()
{
testcase("raiseLocalFee requires two consecutive calls");
LoadFeeTrack track;
// First call: raiseCount_ goes from 0 to 1, returns false (hysteresis)
BEAST_EXPECT(!track.raiseLocalFee());
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
// Second call: raiseCount_ reaches 2, fee is raised
BEAST_EXPECT(track.raiseLocalFee());
BEAST_EXPECT(track.getLocalFee() > kNormalFee);
}
void
testLowerDecaysToBaseline()
{
testcase("lowerLocalFee decays elevated fee back to baseline");
LoadFeeTrack track;
// Raise the fee above baseline (requires two calls due to hysteresis)
track.raiseLocalFee();
track.raiseLocalFee();
auto const elevated = track.getLocalFee();
BEAST_EXPECT(elevated > kNormalFee);
// lowerLocalFee() should decay it each call
// Each call reduces by 1/kLftFeeDecFraction (1/4), so a few
// iterations should bring it back to kNormalFee
bool changed = false;
for (int i = 0; i < 100; ++i)
{
changed |= track.lowerLocalFee();
if (track.getLocalFee() == kNormalFee)
break;
}
BEAST_EXPECT(changed);
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
}
void
testLowerAtBaselineIsNoop()
{
testcase("lowerLocalFee at baseline returns false");
LoadFeeTrack track;
// Already at baseline — should return false (no change)
BEAST_EXPECT(!track.lowerLocalFee());
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
}
void
testRaiseResetsOnLower()
{
testcase("lowerLocalFee resets raiseCount");
LoadFeeTrack track;
// One raise call (hysteresis: count=1, no change yet)
BEAST_EXPECT(!track.raiseLocalFee());
// Lower resets raiseCount_ to 0
track.lowerLocalFee();
// Next raise call starts from 0 again — still needs two calls
BEAST_EXPECT(!track.raiseLocalFee());
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
}
void
testIsLoadedLocal()
{
testcase("isLoadedLocal reflects fee state correctly");
LoadFeeTrack track;
// At baseline, not loaded
BEAST_EXPECT(!track.isLoadedLocal());
// After one raise (hysteresis, fee unchanged) — raiseCount_ != 0
track.raiseLocalFee();
BEAST_EXPECT(track.isLoadedLocal());
// After lower, raiseCount_ reset and fee back at baseline
track.lowerLocalFee();
BEAST_EXPECT(!track.isLoadedLocal());
}
void
testLoadManagerLoop()
{
testcase("LoadManager loop raises and decays load_factor_local");
using namespace test::jtx;
Env env(*this, envconfig());
// Stop the real LoadManager thread so we can drive the fee track
// manually without races. On stock code the fee adjustment block
// is outside the while loop and never fires during normal operation,
// so getFeeTrack().getLocalFee() stays at kNormalFee (256) regardless.
env.app().getLoadManager().stop();
auto& feeTrack = env.app().getFeeTrack();
// Confirm baseline
BEAST_EXPECT(feeTrack.getLocalFee() == kNormalFee);
BEAST_EXPECT(!feeTrack.isLoadedLocal());
// raiseLocalFee() requires two consecutive calls (hysteresis guard).
// This is the exact sequence LoadManager::run() executes each tick
// when the job queue is overloaded.
BEAST_EXPECT(!feeTrack.raiseLocalFee()); // tick 1: count=1, no change
BEAST_EXPECT(feeTrack.raiseLocalFee()); // tick 2: count=2, fee raised
BEAST_EXPECT(feeTrack.getLocalFee() > kNormalFee);
BEAST_EXPECT(feeTrack.isLoadedLocal());
// Now simulate the LoadManager loop calling lowerLocalFee() each tick
// until the fee decays back to baseline. On stock code this block
// never runs during normal operation so the fee would stay elevated.
bool decayed = false;
for (int i = 0; i < 100; ++i)
{
feeTrack.lowerLocalFee();
if (feeTrack.getLocalFee() == kNormalFee)
{
decayed = true;
break;
}
}
BEAST_EXPECT(decayed);
BEAST_EXPECT(feeTrack.getLocalFee() == kNormalFee);
BEAST_EXPECT(!feeTrack.isLoadedLocal());
}
void
run() override
{
testRaiseHysteresis();
testLowerDecaysToBaseline();
testLowerAtBaselineIsNoop();
testRaiseResetsOnLower();
testIsLoadedLocal();
testLoadManagerLoop();
}
};
BEAST_DEFINE_TESTSUITE(LoadManager, app, ripple);
} // namespace ripple
+96
View File
@@ -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
+450 -4
View File
@@ -26,11 +26,17 @@
#include <xrpld/app/misc/detail/OnlineDeleteRanges.h>
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
#include <xrpld/core/ConfigSections.h>
#include <xrpld/nodestore/NodeObject.h>
#include <xrpld/nodestore/detail/DatabaseRotatingImp.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <boost/filesystem.hpp>
#include <exception>
#include <limits>
#include <string>
namespace ripple {
namespace test {
@@ -47,6 +53,19 @@ class SHAMapStore_test : public beast::unit_test::suite
return cfg;
}
// Same as onlineDelete, but on a node store that persists nodes, so
// SHAMapStoreImp builds DatabaseRotatingImp and really rotates.
// envconfig defaults to type=rwdb, which is the null node store.
static auto
onlineDeleteStoring(std::unique_ptr<Config> cfg, std::string const& path)
{
cfg = onlineDelete(std::move(cfg));
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("type", "memory");
section.set("path", path);
return cfg;
}
static auto
advisoryDelete(std::unique_ptr<Config> cfg)
{
@@ -308,13 +327,23 @@ public:
}
void
testAutomatic()
testAutomatic(bool storing)
{
testcase("automatic online_delete");
testcase(
storing ? "automatic online_delete (rotating node store)"
: "automatic online_delete (null node store)");
using namespace jtx;
using namespace std::chrono_literals;
Env env(*this, envconfig(onlineDelete));
beast::temp_dir nodeDir;
Env env(
*this,
storing ? envconfig(onlineDeleteStoring, nodeDir.path())
: envconfig(onlineDelete));
BEAST_EXPECT(
storing ==
(dynamic_cast<NodeStore::DatabaseRotating*>(
&env.app().getNodeStore()) != nullptr));
auto& store = env.app().getSHAMapStore();
auto ledgerSeq = waitForReady(env);
@@ -572,6 +601,412 @@ public:
return backend;
}
std::unique_ptr<NodeStore::Backend>
makeMemoryBackend(
jtx::Env& env,
NodeStoreScheduler& scheduler,
std::string const& name)
{
Section section;
section.set("type", "memory");
section.set("path", name);
auto backend = NodeStore::Manager::instance().make_Backend(
section,
megabytes(env.app().config().getValueFor(
SizedItem::burstSize, std::nullopt)),
scheduler,
env.app().logs().journal("NodeStoreTest"));
backend->open();
return backend;
}
// Ordinary fetch (duplicate == false) must copy an archived object
// into the writable backend. The following rotation deletes the
// archive. Without that copy, a transaction node read between
// rotations disappears while callers can still name it.
void
testArchiveReadSurvivesRotation()
{
testcase("archive read survives rotation");
using namespace jtx;
Env env(*this, envconfig(onlineDelete));
NodeStoreScheduler scheduler(env.app().getJobQueue());
Section nscfg;
nscfg.set("type", "memory");
nscfg.set("path", "rotating");
auto dbr = std::make_unique<NodeStore::DatabaseRotatingImp>(
env.app(),
scheduler,
1,
makeMemoryBackend(env, scheduler, "writable"),
makeMemoryBackend(env, scheduler, "archive"),
nscfg,
env.app().logs().journal("NodeStoreTest"));
Blob const expected(32, 0xab);
auto const hash = sha512Half(makeSlice(expected));
{
Blob data = expected;
dbr->store(hotTRANSACTION_NODE, std::move(data), hash, 1);
}
auto const matches = [&](std::shared_ptr<NodeObject> const& object) {
return object && object->getType() == hotTRANSACTION_NODE &&
object->getData() == expected;
};
// Public fetch defaults duplicate to false. The private override
// is the rotation copy pass.
NodeStore::Database& db = *dbr;
BEAST_EXPECT(matches(db.fetchNodeObject(hash)));
auto const nop = [](std::string const&, std::string const&) {};
// Writable becomes the archive. The object now lives only there.
dbr->rotate(makeMemoryBackend(env, scheduler, "writable-2"), nop);
BEAST_EXPECT(matches(db.fetchNodeObject(hash)));
dbr->rotate(makeMemoryBackend(env, scheduler, "writable-3"), nop);
BEAST_EXPECT(matches(db.fetchNodeObject(hash)));
}
// getWriteLoad, sync, storeLedger, importDatabase and for_each changed
// lock type. In null mode no Env builds a DatabaseRotatingImp, so
// drive them directly.
void
testRotatingAccessors()
{
testcase("rotating database accessors");
using namespace jtx;
// Source ledger must come from a node store that holds its nodes.
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("type", "memory");
section.set("path", "rotating-accessors-app");
return cfg;
}));
env.fund(XRP(10000), Account{"alice"});
env.close();
NodeStoreScheduler scheduler(env.app().getJobQueue());
Section nscfg;
nscfg.set("type", "memory");
nscfg.set("path", "rotating-accessors");
auto makeDb = [&](std::string const& prefix) {
return std::make_unique<NodeStore::DatabaseRotatingImp>(
env.app(),
scheduler,
1,
makeMemoryBackend(env, scheduler, prefix + "-writable"),
makeMemoryBackend(env, scheduler, prefix + "-archive"),
nscfg,
env.app().logs().journal("NodeStoreTest"));
};
auto src = makeDb("rotating-accessors-src");
BEAST_EXPECT(src->getName() == "rotating-accessors-src-writable");
BEAST_EXPECT(src->getWriteLoad() >= 0);
src->sync();
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
if (!BEAST_EXPECT(ledger))
return;
BEAST_EXPECT(src->storeLedger(ledger));
NodeStore::Database& srcDb = *src;
auto const stateRoot = ledger->stateMap().getHash().as_uint256();
BEAST_EXPECT(srcDb.fetchNodeObject(ledger->info().hash));
BEAST_EXPECT(srcDb.fetchNodeObject(stateRoot));
// importDatabase walks the source with for_each.
auto dst = makeDb("rotating-accessors-dst");
dst->importDatabase(*src);
NodeStore::Database& dstDb = *dst;
BEAST_EXPECT(dstDb.fetchNodeObject(ledger->info().hash));
BEAST_EXPECT(dstDb.fetchNodeObject(stateRoot));
}
static auto
nullBackend(std::unique_ptr<Config> cfg)
{
cfg->LEDGER_HISTORY = 8;
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("type", "rwdb");
section.set("path", "main");
return cfg;
}
static auto
nullBackendFullHistory(std::unique_ptr<Config> cfg)
{
cfg = nullBackend(std::move(cfg));
cfg->LEDGER_HISTORY = std::numeric_limits<std::uint32_t>::max();
return cfg;
}
static auto
nullBackendDeleteOff(std::unique_ptr<Config> cfg)
{
cfg = nullBackend(std::move(cfg));
cfg->LEDGER_HISTORY = 256;
cfg->section(ConfigSection::nodeDatabase()).set("online_delete", "0");
return cfg;
}
// Payments keep applying after more closes than ledger_history,
// with no node-store record of the state tree. Close time comes
// from the resident ledger, not a node-store header walk.
void
testNullModeLedgerProgression()
{
testcase("null mode ledger progression");
using namespace jtx;
Env env(*this, envconfig(nullBackend));
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
auto const bobBefore = env.balance(bob);
constexpr int payments = 12;
for (int i = 0; i < payments; ++i)
{
env(pay(alice, bob, XRP(1)));
env.close();
}
BEAST_EXPECT(env.balance(bob) == bobBefore + XRP(payments));
BEAST_EXPECT(env.seq(alice) > 1);
auto const info = env.rpc(
"json",
"account_info",
std::string{"{\"account\": \""} + alice.human() + "\"}");
BEAST_EXPECT(
info[jss::result].isMember(jss::account_data) &&
info[jss::result][jss::account_data][jss::Account] ==
alice.human());
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
if (!BEAST_EXPECT(ledger))
return;
BEAST_EXPECT(ledger->read(keylet::account(alice.id())));
auto const closeTime = env.app().getLedgerMaster().getCloseTimeByHash(
ledger->info().hash, ledger->info().seq);
BEAST_EXPECT(closeTime && *closeTime == ledger->info().closeTime);
auto const validated = env.rpc("ledger", "validated");
BEAST_EXPECT(goodLedger(
env, validated, std::to_string(ledger->info().seq), true));
}
// ledger_history=full would make the retained-ledger window grow
// without bound, since nothing else can drop a null-mode tree.
void
testNullModeRejectsFullHistory()
{
testcase("RWDB null mode rejects ledger_history=full");
using namespace jtx;
try
{
Env env(*this, envconfig(nullBackendFullHistory));
fail("Env should throw when ledger_history is full");
}
catch (std::exception const& e)
{
BEAST_EXPECT(
std::string(e.what()).find("ledger_history") !=
std::string::npos);
}
}
// online_delete=0 is an explicit disable. It must not be treated
// as a missing key and replaced with ledger_history.
void
testExplicitOnlineDeleteZero()
{
testcase("explicit online_delete zero stays off");
using namespace jtx;
Env env(*this, envconfig(nullBackendDeleteOff));
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
auto const balance = env.balance(alice);
env.close();
BEAST_EXPECT(env.app().getSHAMapStore().getLastRotated() == 0);
BEAST_EXPECT(env.balance(alice) == balance);
}
void
testNullModeRequiresHistory()
{
testcase("RWDB null mode requires ledger_history > 0");
using namespace jtx;
try
{
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->LEDGER_HISTORY = 0;
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("type", "rwdb");
section.set("path", "main");
return cfg;
}));
fail("Env should throw when ledger_history is 0");
}
catch (std::exception const& e)
{
BEAST_EXPECT(
std::string(e.what()).find("ledger_history") !=
std::string::npos);
}
}
void
testRetainedLedgerCloseTime()
{
testcase("retained ledger close time survives later closes");
using namespace jtx;
Env env(*this, envconfig(nullBackend));
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
auto const first = env.app().getLedgerMaster().getClosedLedger();
if (!BEAST_EXPECT(first))
return;
auto const firstHash = first->info().hash;
auto const firstSeq = first->info().seq;
auto const firstClose = first->info().closeTime;
for (int i = 0; i < 12; ++i)
{
env(pay(alice, env.master, XRP(1)));
env.close();
}
auto const closeTime =
env.app().getLedgerMaster().getCloseTimeByHash(firstHash, firstSeq);
BEAST_EXPECT(closeTime && *closeTime == firstClose);
BEAST_EXPECT(env.balance(alice) < XRP(10000));
}
void
testNullBackendIsPerConfig()
{
testcase("null backend flag is per configuration");
Config rwdbCfg;
Config upperCfg;
Config nudbCfg;
Config emptyCfg;
rwdbCfg.section(ConfigSection::nodeDatabase()).set("type", "rwdb");
upperCfg.section(ConfigSection::nodeDatabase()).set("type", "RWDB");
nudbCfg.section(ConfigSection::nodeDatabase()).set("type", "NuDB");
// Evaluated per Config object, not latched process-wide.
BEAST_EXPECT(rwdbCfg.nullBackend());
BEAST_EXPECT(upperCfg.nullBackend());
BEAST_EXPECT(!nudbCfg.nullBackend());
BEAST_EXPECT(!emptyCfg.nullBackend());
BEAST_EXPECT(rwdbCfg.nullBackend());
}
// Resident lookups are used with peer-supplied values. They must find
// in-memory ledgers but never load or edit the complete-ledger set.
void
testResidentLedgerLookup()
{
testcase("resident ledger lookup does not load or clear");
using namespace jtx;
Env env(*this, envconfig(nullBackend));
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
for (int i = 0; i < 4; ++i)
env.close();
auto& lm = env.app().getLedgerMaster();
auto const closed = lm.getClosedLedger();
if (!BEAST_EXPECT(closed))
return;
auto const complete = lm.getCompleteLedgers();
auto const bySeq = lm.getResidentLedgerBySeq(closed->info().seq);
BEAST_EXPECT(bySeq && bySeq->info().hash == closed->info().hash);
auto const byHash = lm.getResidentLedgerByHash(closed->info().hash);
BEAST_EXPECT(byHash && byHash->info().seq == closed->info().seq);
// A recent ledger still inside the retained window.
auto const prior = lm.getResidentLedgerBySeq(closed->info().seq - 2);
BEAST_EXPECT(prior && prior->info().seq == closed->info().seq - 2);
BEAST_EXPECT(!lm.getResidentLedgerByHash(uint256{12345}));
BEAST_EXPECT(!lm.getResidentLedgerBySeq(closed->info().seq + 1000));
BEAST_EXPECT(lm.getCompleteLedgers() == complete);
// Drop every ledger from the history cache. Null mode must still
// reach recent ledgers through the closed/validated holders and
// the retained window, without loading anything.
env.app().getJobQueue().rendezvous();
auto const older = lm.getResidentLedgerBySeq(closed->info().seq - 3);
if (!BEAST_EXPECT(older))
return;
lm.clearLedgerCachePrior(closed->info().seq + 1);
auto const closedAgain = lm.getResidentLedgerBySeq(closed->info().seq);
BEAST_EXPECT(
closedAgain && closedAgain->info().hash == closed->info().hash);
auto const closedByHash =
lm.getResidentLedgerByHash(closed->info().hash);
BEAST_EXPECT(
closedByHash && closedByHash->info().seq == closed->info().seq);
auto const retainedBySeq = lm.getResidentLedgerBySeq(older->info().seq);
BEAST_EXPECT(
retainedBySeq && retainedBySeq->info().hash == older->info().hash);
auto const retainedByHash =
lm.getResidentLedgerByHash(older->info().hash);
BEAST_EXPECT(
retainedByHash && retainedByHash->info().seq == older->info().seq);
BEAST_EXPECT(!lm.getResidentLedgerByHash(uint256{12345}));
BEAST_EXPECT(lm.getCompleteLedgers() == complete);
}
void
testNullFactoryDropsWrites()
{
testcase("type=rwdb uses NullFactory and drops node-store writes");
using namespace jtx;
Env env(*this, envconfig(nullBackend));
auto& db = env.app().getNodeStore();
uint256 const hash{2};
Blob data(32, 7);
db.store(hotACCOUNT_NODE, std::move(data), hash, 1);
BEAST_EXPECT(!db.fetchNodeObject(hash, 1));
}
void
testRotate()
{
@@ -852,8 +1287,19 @@ public:
{
testComputeOnlineDeleteTargets();
testClear();
testAutomatic();
testAutomatic(false);
testAutomatic(true);
testRotatingAccessors();
testCanDelete();
testArchiveReadSurvivesRotation();
testNullModeLedgerProgression();
testNullModeRejectsFullHistory();
testExplicitOnlineDeleteZero();
testNullModeRequiresHistory();
testRetainedLedgerCloseTime();
testNullBackendIsPerConfig();
testResidentLedgerLookup();
testNullFactoryDropsWrites();
testRotate();
testPinnedRangeRestoreRequiresPinnedData();
}
+32 -11
View File
@@ -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 =
@@ -8421,8 +8439,8 @@ private:
using namespace std::literals;
testcase("set manifest TSH");
if (!features[featureOnChainManifests])
return;
// OnChainManifests is not yet Supported::yes, so enable it explicitly.
features = features | featureOnChainManifests;
// otxn: master
// tsh: ephemeral
@@ -8514,8 +8532,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 +8564,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
+715 -39
View File
@@ -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 \
{ \
@@ -2271,9 +2278,9 @@ public:
// Call named hook with the wrong hook name (size == 0)
for (auto const fix : {true, false})
{
auto f = features - fixHookNameValidation;
auto f = features - fix20260929;
if (fix)
f = f | fixHookNameValidation;
f = f | fix20260929;
Env env{*this, f};
env.fund(XRP(10000), alice);
@@ -2290,6 +2297,35 @@ public:
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);
@@ -3497,9 +3533,7 @@ public:
{
for (auto const& hook_wasm : {accept_oob_wasm, rollback_oob_wasm})
{
Env env{
*this,
withFix ? features : features - fixHookExitOutOfBounds};
Env env{*this, withFix ? features : features - fix20260929};
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
@@ -6448,7 +6482,10 @@ public:
{
testcase("Test float_set");
using namespace jtx;
Env env{*this, features};
// this hook asserts the pre-fix20261005 INVALID_FLOAT result for an
// out of range exponent; the amended EXPONENT_UNDERSIZED/OVERSIZED
// behaviour is covered in HookAPI_test
Env env{*this, features - fix20261005};
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
@@ -9459,12 +9496,9 @@ public:
{
testcase("Test slot_set");
using namespace jtx;
Env env{*this, features};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice);
env.fund(XRP(10000), bob);
TestHook hook_wasm = wasm[R"[test.hook](
#include <stdint.h>
@@ -9538,32 +9572,49 @@ public:
for (uint32_t i = 2; GUARD(257), i < 256; ++i)
ASSERT(s == slot_size(i));
// slot a txn
// slot a txn by hash. With fix20261005 a 32 byte argument is
// rejected outright, without it the otxn is slotted.
// The result is reported as the accept code.
uint8_t txn[32];
ASSERT(otxn_id(SBUF(txn), 0) == 32);
ASSERT(slot_set(SBUF(txn), 1) == 1);
int64_t r = slot_set(SBUF(txn), 1);
uint32_t s2 = slot_size(1);
// ensure it's not the same object that was there before
ASSERT(s != s2 && s2 > 0);
// ensure it's not the same object that was there before
if (r == 1)
ASSERT(s != slot_size(1) && slot_size(1) > 0);
// done!
accept(0,0,0);
accept(0,0,r);
}
)[test.hook]"];
HASH_WASM(hook);
// install the hook on alice
env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0),
M("set slot_set"),
HSFEE);
env.close();
EXPECT_HOOK_FEE(hook, 11653);
for (bool const withFix : {true, false})
{
Env env{*this, withFix ? features : features - fix20261005};
env.fund(XRP(10000), alice, bob);
// invoke the hook
env(pay(bob, alice, XRP(1)), M("test slot_set"), fee(XRP(1)));
// install the hook on alice
env(ripple::test::jtx::hook(alice, {{hso(hook_wasm)}}, 0),
M("set slot_set"),
HSFEE);
env.close();
EXPECT_HOOK_FEE(hook, 11645);
// invoke the hook
env(pay(bob, alice, XRP(1)), M("test slot_set"), fee(XRP(1)));
env.close();
auto const meta = env.meta();
BEAST_REQUIRE(meta && meta->isFieldPresent(sfHookExecutions));
auto const& execs = meta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(execs.size() == 1);
BEAST_EXPECT(
execs[0].getFieldU64(sfHookReturnCode) ==
(withFix ? 0x8000000000000007ULL /* INVALID_ARGUMENT */
: 1ULL));
}
}
void
@@ -10923,6 +10974,587 @@ public:
}
}
void
test_state_foreign_set_touch(FeatureBitset features)
{
testcase("Test hook state writes touch the state owner");
using namespace jtx;
if (!features[featureTouch])
return;
for (bool const withFix : {false, true})
{
Env env{
*this,
withFix ? features | fix20261005 : features - fix20261005};
auto const alice = Account{"alice"}; // grantor
auto const bob = Account{"bob"}; // strong grantee
auto const cho = Account{"cho"}; // invoker
auto const dan = Account{"dan"}; // weak grantee
auto const eve = Account{"eve"}; // callback grantee
env.fund(XRP(10000), alice, bob, cho, dan, eve);
// Same hooks as test_state_foreign_set: bob's hook writes the
// current txn id under key 1, namespace 0, of the account named
// in the InvoiceID; alice's hook deletes that key whenever alice
// sends a txn.
TestHook grantee_wasm = wasm[R"[test.hook](
#include <stdint.h>
#define sfInvoiceID ((5U << 16U) + 17U)
extern int32_t _g (uint32_t id, uint32_t maxiter);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t state_foreign (
uint32_t, uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t
);
extern int64_t state_foreign_set (
uint32_t, uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t
);
extern int64_t otxn_id(uint32_t, uint32_t, uint32_t);
extern int64_t otxn_field(uint32_t, uint32_t, uint32_t);
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__);
#define TOO_BIG (-3)
#define TOO_SMALL (-4)
#define OUT_OF_BOUNDS (-1)
#define DOESNT_EXIST (-5)
#define INVALID_ARGUMENT (-7)
#define SBUF(x) (uint32_t)(x), sizeof(x)
int64_t hook(uint32_t reserved )
{
_g(1,1);
// bounds tests
int64_t y;
ASSERT((y=state_foreign_set(1111111, 32, 1, 32, 1, 32, 1, 20)) == OUT_OF_BOUNDS);
ASSERT((y=state_foreign_set(1, 1111111, 1, 32, 1, 32, 1, 20)) == OUT_OF_BOUNDS);
ASSERT((y=state_foreign_set(1, 32, 1111111, 32, 1, 32, 1, 20)) == OUT_OF_BOUNDS);
ASSERT((y=state_foreign_set(1, 32, 1, 1111111, 1, 32, 1, 20)) == TOO_BIG);
ASSERT((y=state_foreign_set(1, 32, 1, 32, 1111111, 32, 1, 20)) == OUT_OF_BOUNDS);
ASSERT((y=state_foreign_set(1, 32, 1, 32, 1, 1111111, 1, 20)) == INVALID_ARGUMENT);
ASSERT((y=state_foreign_set(1, 32, 1, 32, 1, 32, 1111111, 20)) == OUT_OF_BOUNDS);
ASSERT((y=state_foreign_set(1, 32, 1, 32, 1, 32, 1, 1111111)) == INVALID_ARGUMENT);
// get this transaction id
uint8_t txn[32];
ASSERT(otxn_id(SBUF(txn), 0) == 32);
// get the invoice id, which contains the grantor account
uint8_t grantor[32];
ASSERT(otxn_field(SBUF(grantor), sfInvoiceID) == 32);
// set the current txn id on the grantor's state under key 1, namespace 0
uint8_t one[32]; one[31] = 1U;
uint8_t zero[32];
ASSERT(state_foreign_set(SBUF(txn), SBUF(one), SBUF(zero), grantor + 12, 20) == 32);
return accept(0,0,0);
}
)[test.hook]"];
HASH_WASM(grantee);
TestHook grantor_wasm = wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
#define GUARD(maxiter) _g((1ULL << 31U) + __LINE__, (maxiter)+1)
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t otxn_field (uint32_t, uint32_t, uint32_t);
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
extern int64_t state_foreign_set (
uint32_t, uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t
);
#define DOESNT_EXIST (-5)
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__);
#define BUFFER_EQUAL_20(buf1, buf2)\
(\
*(((uint64_t*)(buf1)) + 0) == *(((uint64_t*)(buf2)) + 0) &&\
*(((uint64_t*)(buf1)) + 1) == *(((uint64_t*)(buf2)) + 1) &&\
*(((uint32_t*)(buf1)) + 4) == *(((uint32_t*)(buf2)) + 4))
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define sfAccount ((8U << 16U) + 1U)
int64_t hook(uint32_t reserved )
{
_g(1,1);
uint8_t otxnacc[20];
ASSERT(otxn_field(SBUF(otxnacc), sfAccount) == 20);
uint8_t hookacc[20];
ASSERT(hook_account(SBUF(hookacc)) == 20);
if (BUFFER_EQUAL_20(otxnacc, hookacc))
{
// outgoin txn, delete the state
uint8_t one[32]; one[31] = 1U;
uint8_t zero[32];
// we can use state_foreign_set to do the deletion, a concise way to test this functionality too
int64_t y = state_foreign_set(0,0, SBUF(one), SBUF(zero), SBUF(hookacc));
ASSERT(y == 0 || y == DOESNT_EXIST);
}
return accept(0,0,0);
}
)[test.hook]"];
HASH_WASM(grantor);
// dan's hook writes nothing when executed strongly, it only asks
// to be executed again weakly. Then it writes the txn id under key
// 2, namespace 0, of the account named in the InvoiceID or, if the
// txn carries the parameter D, deletes key 4 there instead.
TestHook weak_grantee_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);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t hook_again (void);
extern int64_t otxn_id (uint32_t, uint32_t, uint32_t);
extern int64_t otxn_field (uint32_t, uint32_t, uint32_t);
extern int64_t otxn_param (uint32_t, uint32_t, uint32_t, uint32_t);
extern int64_t state_foreign_set (
uint32_t, uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t
);
#define sfInvoiceID ((5U << 16U) + 17U)
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__);
uint8_t ns[32];
uint8_t write_key[32] = { [31] = 2U };
uint8_t delete_key[32] = { [31] = 4U };
int64_t hook(uint32_t r)
{
_g(1,1);
// strong execution: write nothing, ask to be executed again weakly
if (r == 0)
{
ASSERT(hook_again() == 1);
return accept(0,0,0);
}
// the grantor is the account in the low 20 bytes of the InvoiceID
uint8_t grantor[32];
ASSERT(otxn_field(SBUF(grantor), sfInvoiceID) == 32);
// with otxn parameter D, delete a key the grantor does not have
uint8_t d[1];
if (otxn_param(SBUF(d), (uint32_t)"D", 1) == 1)
{
ASSERT(state_foreign_set(0, 0, SBUF(delete_key), SBUF(ns), (uint32_t)(grantor + 12), 20) == 0);
return accept(0,0,0);
}
// otherwise write the otxn id under key 2
uint8_t txn[32];
ASSERT(otxn_id(SBUF(txn), 0) == 32);
ASSERT(state_foreign_set(SBUF(txn), SBUF(write_key), SBUF(ns), (uint32_t)(grantor + 12), 20) == 32);
return accept(0,0,0);
}
)[test.hook]"];
HASH_WASM(weak_grantee);
// eve's hook emits an Invoke. Its callback, which runs when that
// Invoke is applied, writes the Invoke's id under key 3, namespace
// 0, of the account in the hook parameter G.
TestHook cbak_grantee_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);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t emit (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
extern int64_t hook_account (uint32_t write_ptr, uint32_t write_len);
extern int64_t hook_param (uint32_t, uint32_t, uint32_t, uint32_t);
extern int64_t etxn_reserve (uint32_t);
extern int64_t etxn_fee_base (uint32_t read_ptr, uint32_t read_len);
extern int64_t etxn_details (uint32_t write_ptr, uint32_t write_len);
extern int64_t ledger_seq (void);
extern int64_t otxn_id (uint32_t, uint32_t, uint32_t);
extern int64_t state_foreign_set (
uint32_t, uint32_t, uint32_t, uint32_t,
uint32_t, uint32_t, uint32_t, uint32_t
);
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__);
uint8_t ns[32];
uint8_t write_key[32] = { [31] = 3U };
// an Invoke from the hook account, emitted so its callback runs
uint8_t txn[229] =
{
/* size, upto, field name */
/* 3, 0, tt = Invoke */ 0x12U, 0x00U, 0x63U,
/* 5, 3, flags */ 0x22U, 0x00U, 0x00U, 0x00U, 0x00U,
/* 5, 8, sequence */ 0x24U, 0x00U, 0x00U, 0x00U, 0x00U,
/* 6, 13, firstledgersequence */ 0x20U, 0x1AU, 0x00U, 0x00U, 0x00U, 0x00U,
/* 6, 19, lastledgersequence */ 0x20U, 0x1BU, 0x00U, 0x00U, 0x00U, 0x00U,
/* 9, 25, fee */ 0x68U, 0x40U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
/* 35, 34, signingpubkey */ 0x73U, 0x21U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* 22, 69, account */ 0x81U, 0x14U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* 138, 91, emit details */
/* 0, 229, */
};
#define FLS_OUT (txn + 15U)
#define LLS_OUT (txn + 21U)
#define FEE_OUT (txn + 26U)
#define ACCOUNT_OUT (txn + 71U)
#define EMIT_OUT (txn + 91U)
#define SET_UINT32(ptr, value)\
{\
uint32_t v = (value);\
(ptr)[0] = v >> 24U; (ptr)[1] = v >> 16U; (ptr)[2] = v >> 8U; (ptr)[3] = v;\
}
#define SET_NATIVE_AMOUNT(ptr, amount)\
{\
uint64_t a = (amount);\
(ptr)[0] = 0x40U + ((a >> 56U) & 0x3FU);\
(ptr)[1] = a >> 48U; (ptr)[2] = a >> 40U; (ptr)[3] = a >> 32U;\
(ptr)[4] = a >> 24U; (ptr)[5] = a >> 16U; (ptr)[6] = a >> 8U; (ptr)[7] = a;\
}
int64_t cbak(uint32_t r)
{
_g(1,1);
// the grantor is the hook parameter G
uint8_t grantor[20];
ASSERT(hook_param(SBUF(grantor), (uint32_t)"G", 1) == 20);
// in a callback the otxn is the emitted txn: write its id under key 3
uint8_t id[32];
ASSERT(otxn_id(SBUF(id), 0) == 32);
ASSERT(state_foreign_set(SBUF(id), SBUF(write_key), SBUF(ns), SBUF(grantor)) == 32);
return accept(0,0,0);
}
int64_t hook(uint32_t r)
{
_g(1,1);
ASSERT(etxn_reserve(1) == 1);
uint32_t fls = (uint32_t)ledger_seq() + 1;
SET_UINT32(FLS_OUT, fls);
SET_UINT32(LLS_OUT, fls + 4);
ASSERT(hook_account(ACCOUNT_OUT, 20) == 20);
ASSERT(etxn_details(EMIT_OUT, 138U) == 138);
int64_t fee = etxn_fee_base(SBUF(txn));
ASSERT(fee > 0);
SET_NATIVE_AMOUNT(FEE_OUT, fee);
uint8_t emithash[32];
ASSERT(emit(SBUF(emithash), SBUF(txn)) == 32);
return accept(0,0,0);
}
)[test.hook]"];
HASH_WASM(cbak_grantee);
// alice grants each grantee hook the right to write her state
{
Json::Value grants{Json::arrayValue};
auto const grant = [&](std::string const& hash,
Account const& grantee) {
Json::Value g;
g[jss::HookGrant][jss::HookHash] = hash;
g[jss::HookGrant][jss::Authorize] = grantee.human();
grants.append(g);
};
grant(grantee_hash_str, bob);
grant(weak_grantee_hash_str, dan);
grant(cbak_grantee_hash_str, eve);
Json::Value json = ripple::test::jtx::hook(
alice, {{hso(grantor_wasm, overrideFlag)}}, 0);
json[jss::Hooks][0U][jss::Hook][jss::HookGrants] = grants;
env(json, M("touch: set grantor"), HSFEE);
}
env(ripple::test::jtx::hook(
bob, {{hso(grantee_wasm, overrideFlag)}}, 0),
M("touch: set grantee"),
HSFEE);
env(ripple::test::jtx::hook(
dan, {{hso(weak_grantee_wasm, overrideFlag)}}, 0),
M("touch: set weak grantee"),
HSFEE);
{
Json::Value params{Json::arrayValue};
params[0U][jss::HookParameter][jss::HookParameterName] =
strHex(std::string("G"));
params[0U][jss::HookParameter][jss::HookParameterValue] =
strHex(alice.id());
Json::Value json = ripple::test::jtx::hook(
eve, {{hso(cbak_grantee_wasm, overrideFlag)}}, 0);
json[jss::Hooks][0U][jss::Hook][jss::HookParameters] = params;
env(json, M("touch: set callback grantee"), HSFEE);
}
env.close();
uint256 const ns{beast::zero};
auto const aliceState = [&](std::uint8_t n) {
uint256 key;
key.data()[31] = n;
return env.le(keylet::hookState(alice.id(), key, ns));
};
auto const stateIs = [&](std::uint8_t n, uint256 const& txid) {
auto const sle = aliceState(n);
return sle &&
sle->getFieldVL(sfHookStateData) ==
Blob(txid.begin(), txid.end());
};
auto const touchCount = [&]() -> std::uint64_t {
auto const sle = env.le(alice);
return sle->isFieldPresent(sfTouchCount)
? sle->getFieldU64(sfTouchCount)
: 0;
};
auto const prevTxnID = [&]() {
return env.le(alice)->getFieldH256(sfPreviousTxnID);
};
auto const aliceKey = keylet::account(alice.id()).key;
auto const inMeta =
[&](std::shared_ptr<STObject const> const& meta) -> bool {
if (!meta)
return false;
for (auto const& node : meta->getFieldArray(sfAffectedNodes))
if (node.getFieldH256(sfLedgerIndex) == aliceKey)
return true;
return false;
};
// env.meta() closes the ledger, so call it before reading state
auto const aliceInMeta = [&]() -> bool {
return inMeta(env.meta());
};
std::string const invid = std::string(24, '0') + strHex(alice.id());
auto const payTo = [&](Account const& from,
Account const& to,
PrettyAmount const& amount) {
Json::Value json = pay(from, to, amount);
json[jss::InvoiceID] = invid;
return json;
};
// 1. strong pass, first write creates key 1: HookStateCount
// changes, so alice's root is modified either way; only the
// fix also touches it
{
auto const before = touchCount();
env(payTo(cho, bob, XRP(1)),
fee(XRP(1)),
M("touch: create"),
ter(tesSUCCESS));
auto const txid = env.tx()->getTransactionID();
BEAST_EXPECT(aliceInMeta());
BEAST_EXPECT(touchCount() == before + (withFix ? 1 : 0));
BEAST_EXPECT(stateIs(1, txid));
}
// 2. overwrite of the existing key: alice is not a stakeholder,
// so without the fix her root is untouched and unthreaded
{
auto const before = touchCount();
auto const prev = prevTxnID();
env(payTo(cho, bob, XRP(1)),
fee(XRP(1)),
M("touch: overwrite"),
ter(tesSUCCESS));
auto const txid = env.tx()->getTransactionID();
BEAST_EXPECT(aliceInMeta() == withFix);
BEAST_EXPECT(touchCount() == before + (withFix ? 1 : 0));
BEAST_EXPECT(prevTxnID() == (withFix ? txid : prev));
BEAST_EXPECT(stateIs(1, txid));
}
// 3. alice is also a strong stakeholder (payment destination) and
// bob's outgoing hook writes her state: touched exactly once
{
auto const before = touchCount();
env(payTo(bob, alice, XRP(1)),
fee(XRP(1)),
M("touch: tsh"),
ter(tesSUCCESS));
BEAST_EXPECT(aliceInMeta());
BEAST_EXPECT(touchCount() == before + 1);
}
// 4. a rejected invocation writes nothing and touches nothing
{
auto const before = touchCount();
Json::Value json = pay(cho, bob, XRP(1));
json[jss::InvoiceID] =
"0000000000000000000000000000000000000000000000000000000000"
"000001";
env(json,
fee(XRP(1)),
M("touch: rejected"),
ter(tecHOOK_REJECTED));
BEAST_EXPECT(!aliceInMeta());
BEAST_EXPECT(touchCount() == before);
}
// the otxn account is a weak stakeholder under fixHookAPI20251128
bool const senderTouched =
env.current()->rules().enabled(fixHookAPI20251128);
// 5. alice sends to bob: bob's strong hook overwrites her key 1
// before the weak pass touches her as the sender. Under the fix
// that write has already touched her, so the weak pass must
// not touch her again.
{
auto const before = touchCount();
env(payTo(alice, bob, XRP(1)),
fee(XRP(1)),
M("touch: foreign write, then weak tsh"),
ter(tesSUCCESS));
auto const txid = env.tx()->getTransactionID();
BEAST_EXPECT(aliceInMeta());
BEAST_EXPECT(
touchCount() ==
before + ((withFix || senderTouched) ? 1 : 0));
BEAST_EXPECT(stateIs(1, txid));
}
// 6. as 5, but the payment fails after the strong pass. The reset
// discards the strong pass' state write and touch, so the weak
// pass must touch alice afresh.
{
auto const before = touchCount();
auto const sle = aliceState(1);
BEAST_REQUIRE(sle);
auto const value = sle->getFieldVL(sfHookStateData);
env(payTo(alice, bob, XRP(100000)),
fee(XRP(1)),
M("touch: reset"),
ter(tecUNFUNDED_PAYMENT));
BEAST_EXPECT(aliceInMeta());
BEAST_EXPECT(touchCount() == before + (senderTouched ? 1 : 0));
auto const after = aliceState(1);
BEAST_EXPECT(
after && after->getFieldVL(sfHookStateData) == value);
}
// every execution of the last txn's hooks accepted
auto const allAccepted =
[&](std::shared_ptr<STObject const> const& meta,
std::size_t count) -> bool {
if (!meta || !meta->isFieldPresent(sfHookExecutions))
return false;
auto const& executions = meta->getFieldArray(sfHookExecutions);
if (executions.size() != count)
return false;
for (auto const& execution : executions)
if (execution.getFieldU8(sfHookResult) !=
static_cast<std::uint8_t>(hook_api::ExitType::ACCEPT))
return false;
return true;
};
// 7. weak pass: dan's hook writes key 2 only when it is executed
// again as weak, after the payment has applied. The first
// write creates the key and so modifies alice's root either
// way; the second only overwrites it.
for (bool const create : {true, false})
{
auto const before = touchCount();
auto const prev = prevTxnID();
env(payTo(cho, dan, XRP(1)),
fee(XRP(1)),
M(create ? "touch: weak create" : "touch: weak overwrite"),
ter(tesSUCCESS));
auto const txid = env.tx()->getTransactionID();
auto const meta = env.meta();
BEAST_EXPECT(allAccepted(meta, 2));
BEAST_EXPECT(inMeta(meta) == (create || withFix));
BEAST_EXPECT(touchCount() == before + (withFix ? 1 : 0));
BEAST_EXPECT(
prevTxnID() == ((create || withFix) ? txid : prev));
BEAST_EXPECT(stateIs(2, txid));
}
// 8. dan's weak execution deletes key 4, which alice never had.
// Nothing of hers changes, so nothing touches her.
{
auto const before = touchCount();
auto const prev = prevTxnID();
Json::Value params{Json::arrayValue};
params[0U][jss::HookParameter][jss::HookParameterName] =
strHex(std::string("D"));
params[0U][jss::HookParameter][jss::HookParameterValue] = "01";
Json::Value json = payTo(cho, dan, XRP(1));
json[jss::HookParameters] = params;
env(json,
fee(XRP(1)),
M("touch: weak delete of a missing key"),
ter(tesSUCCESS));
auto const meta = env.meta();
BEAST_EXPECT(allAccepted(meta, 2));
BEAST_EXPECT(!inMeta(meta));
BEAST_EXPECT(touchCount() == before);
BEAST_EXPECT(prevTxnID() == prev);
BEAST_EXPECT(!aliceState(4));
}
// 9. callback: eve's hook emits an Invoke, whose callback writes
// key 3 when the Invoke is applied in the following ledger.
// Again the first write creates the key, the second overwrites.
for (bool const create : {true, false})
{
auto const before = touchCount();
auto const prev = prevTxnID();
env(invoke::invoke(eve),
fee(XRP(1)),
M(create ? "touch: callback create"
: "touch: callback overwrite"),
ter(tesSUCCESS));
env.close();
auto const meta = env.meta();
BEAST_REQUIRE(meta && meta->isFieldPresent(sfHookEmissions));
auto const etxn =
meta->getFieldArray(sfHookEmissions)[0u].getFieldH256(
sfEmittedTxnID);
// the emitted txn, and with it the callback, applies here
env.close();
auto const emitted = env.closed()->txRead(etxn);
BEAST_REQUIRE(emitted.first && emitted.second);
BEAST_EXPECT(allAccepted(emitted.second, 1));
BEAST_EXPECT(inMeta(emitted.second) == (create || withFix));
BEAST_EXPECT(touchCount() == before + (withFix ? 1 : 0));
BEAST_EXPECT(
prevTxnID() == ((create || withFix) ? etxn : prev));
BEAST_EXPECT(stateIs(3, etxn));
}
}
}
void
test_state_set(FeatureBitset features)
{
@@ -12470,12 +13102,11 @@ public:
{
for (bool const withSType : {true, false})
{
auto feats =
features - fixHookAPI20251128 - fixHookAPISType;
auto feats = features - fixHookAPI20251128 - fix20260929;
if (with20251128)
feats = feats | fixHookAPI20251128;
if (withSType)
feats = feats | fixHookAPISType;
feats = feats | fix20260929;
Env env{*this, feats};
env.fund(XRP(10000), alice, bob);
@@ -12634,7 +13265,9 @@ public:
testcase("Test sto_validate");
using namespace jtx;
Env env{*this, features};
// this hook asserts the pre-fix20261005 TOO_SMALL result for a short
// buffer; the amended behaviour is covered by the param hook below
Env env{*this, features - fix20261005};
auto const bob = Account{"bob"};
auto const alice = Account{"alice"};
@@ -12890,6 +13523,45 @@ public:
testSTI(
"011A0000000000000000000000005553440000000000", hasEnabled);
}
// a buffer shorter than two bytes can't be a valid STObject:
// fix20261005 returns 0 (invalid) instead of TOO_SMALL
for (bool const withFix : {true, false})
{
Env env{
*this,
withFix ? features | fix20261005 : features - fix20261005};
env.fund(XRP(10000), alice, bob);
env.close();
// install the hook on alice
env(ripple::test::jtx::hook(
alice, {{hso(hook, overrideFlag)}}, 0),
M("set sto_validate"),
HSFEE);
env.close();
auto payJv = pay(bob, alice, XRP(1));
Json::Value params{Json::arrayValue};
auto& param = params[0U][jss::HookParameter];
param[jss::HookParameterName] = strHex(std::string("V"));
param[jss::HookParameterValue] = "E1";
payJv[jss::HookParameters] = params;
env(payJv, M("test sto_validate short buffer"), fee(XRP(1)));
env.close();
auto const result =
env.meta()->getFieldArray(sfHookExecutions)[0].getFieldU64(
sfHookReturnCode);
// negative exit codes are recorded with the MSB set
BEAST_EXPECT(
result ==
(withFix ? 0ULL
: 0x8000000000000000ULL + 4ULL /*TOO_SMALL*/));
}
}
}
@@ -15583,11 +16255,12 @@ public:
test_slot_subfield(features); //
test_slot_type(features); //
test_state(features); //
test_state_foreign(features); //
test_state_foreign_set(features); //
test_state_foreign_set_max(features); //
test_state_set(features); //
test_state(features); //
test_state_foreign(features); //
test_state_foreign_set(features); //
test_state_foreign_set_max(features); //
test_state_foreign_set_touch(features); //
test_state_set(features); //
test_sto_emplace(features); //
test_sto_erase(features); //
@@ -15635,7 +16308,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](
@@ -15649,7 +16325,7 @@ private:
}
)[test.hook]"];
HASH_WASM(accept);
HASH_WASM_MEMBER(accept);
TestHook rollback_wasm = // WASM: 1
wasm[
@@ -15665,7 +16341,7 @@ private:
}
)[test.hook]"];
HASH_WASM(rollback);
HASH_WASM_MEMBER(rollback);
TestHook noguard_wasm = // WASM: 2
wasm[
@@ -15754,7 +16430,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
@@ -15770,7 +16446,7 @@ private:
}
)[test.hook]"];
HASH_WASM(accept2);
HASH_WASM_MEMBER(accept2);
// This hook is used to test Callback
TestHook emit_invoke_wasm = // WASM: 7
@@ -15862,7 +16538,7 @@ private:
}
)[test.hook]"];
HASH_WASM(emit_invoke);
HASH_WASM_MEMBER(emit_invoke);
};
#define SETHOOK_TEST(i, last) \
File diff suppressed because it is too large. Load diff
+182 -26
View File
@@ -20,7 +20,9 @@
#include <test/jtx.h>
#include <test/jtx/network.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/misc/Manifest.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/SetManifest.h>
#include <xrpld/core/Config.h>
@@ -54,10 +56,10 @@ struct SetManifest_test : public beast::unit_test::suite
// A manifest transaction is unsigned, so the network id is mandatory and
// the network must be one that requires it (id > 1024).
static std::unique_ptr<Config>
makeConfig()
makeConfig(std::string fee = "10")
{
return jtx::network::makeNetworkConfig(
21337, "10", "1000000", "200000");
21337, std::move(fee), "1000000", "200000");
}
/** Builds a manifest signed by `master` nominating `ephemeral`.
@@ -111,6 +113,15 @@ struct SetManifest_test : public beast::unit_test::suite
return env.rpc("json", "submit", to_string(params))[jss::result];
}
/** Submits an already formed SetManifest transaction. */
static Json::Value
submit(jtx::Env& env, std::shared_ptr<STTx const> const& tx)
{
Serializer s;
tx->add(s);
return env.rpc("submit", strHex(s.slice()))[jss::result];
}
static std::string
engineResult(Json::Value const& result)
{
@@ -166,13 +177,13 @@ struct SetManifest_test : public beast::unit_test::suite
});
};
// The same two-pass pricing makeSetManifestTx() does: encode once with
// a placeholder purely to have something to measure, then encode at the
// ceiling checkFee() will accept.
auto const base = SetManifest::calculateBaseFee(
*env.current(), *build(XRPAmount{0}, false));
return build(mulRatio(base, 12, 10, /*roundUp*/ true), true);
// The canonical Fee makeSetManifestTx() uses for the open ledger.
SerialIter mit{makeSlice(manifest)};
STObject const manifestObject{mit, sfManifest};
return build(
canonicalSetManifestFee(env.current()->fees().base, manifestObject)
.value_or(XRPAmount{0}),
true);
}
/** A mutable copy of a ledger object, suitable for the RawView interface.
@@ -597,7 +608,9 @@ struct SetManifest_test : public beast::unit_test::suite
BEAST_EXPECT(
applyDirect(env, envelope(env, good, other.id())) == temMALFORMED);
// Every envelope field a relayer could otherwise choose is pinned.
// Every envelope field a relayer could otherwise choose is pinned: the
// envelope is one exact transaction, with Account and NetworkID derived
// rather than mirrored.
for (auto const& [name, tweak] : std::vector<
std::pair<char const*, std::function<void(STObject&)>>>{
{"Sequence",
@@ -608,33 +621,175 @@ struct SetManifest_test : public beast::unit_test::suite
}},
{"TicketSequence",
[](STObject& obj) { obj.setFieldU32(sfTicketSequence, 1); }},
{"SigningPubKey", [&](STObject& obj) {
{"SigningPubKey",
[&](STObject& obj) {
obj.setFieldVL(sfSigningPubKey, master.pk().slice());
}}})
}},
{"Memos",
[](STObject& obj) {
obj.setFieldArray(sfMemos, STArray(sfMemos, 1));
STObject memo{sfMemo};
memo.setFieldVL(sfMemoData, Blob{0x01});
obj.peekFieldArray(sfMemos).emplace_back(std::move(memo));
}},
{"SourceTag",
[](STObject& obj) { obj.setFieldU32(sfSourceTag, 1); }},
{"LastLedgerSequence",
[](STObject& obj) {
obj.setFieldU32(sfLastLedgerSequence, 1'000'000);
}},
{"NetworkID",
[](STObject& obj) { obj.setFieldU32(sfNetworkID, 21338); }}})
{
auto const variant = envelope(env, good, master.id(), tweak);
// The generic network check runs first for a foreign NetworkID.
auto const expected = std::string(name) == "NetworkID"
? TER{telWRONG_NETWORK}
: TER{temMALFORMED};
BEAST_EXPECTS(applyDirect(env, variant) == expected, name);
// Ingress rejects the variant as a bad envelope before any
// manifest signature work, and never as a soft fee miss.
auto const [validity, reason] = checkValidity(
env.app().getHashRouter(),
*variant,
env.current()->rules(),
env.app().config());
BEAST_EXPECTS(validity == Validity::SigBad, name);
BEAST_EXPECTS(
applyDirect(env, envelope(env, good, master.id(), tweak)) ==
temMALFORMED,
checkManifestIngressFee(env.app(), *variant).first ==
ManifestIngressFee::NotApplicable,
name);
}
// sfFee is the one envelope field preflight cannot bound, because the
// base fee is not in scope until preclaim. checkFee() caps it instead.
// The Fee is pinned to the one canonical value for the applying view's
// base fee. A different value is a local miss, not a malformed
// transaction, and ingress drops it before manifest crypto.
auto const priced = envelope(env, good, master.id());
auto const ceiling = priced->getFieldAmount(sfFee).xrp();
auto const canonicalFee = priced->getFieldAmount(sfFee).xrp();
BEAST_EXPECT(
checkManifestIngressFee(env.app(), *priced).first ==
ManifestIngressFee::Canonical);
for (auto const fee :
{canonicalFee + XRPAmount{1}, canonicalFee - XRPAmount{1}})
{
auto const variant =
envelope(env, good, master.id(), [&](STObject& obj) {
obj.setFieldAmount(sfFee, fee);
});
BEAST_EXPECT(
checkManifestIngressFee(env.app(), *variant).first ==
ManifestIngressFee::Refused);
BEAST_EXPECT(applyDirect(env, variant) == telMANIFEST_FEE_MISMATCH);
}
// At the canonical value exactly, which is what Submit sends.
BEAST_EXPECT(applyDirect(env, priced) == tesSUCCESS);
}
void
testCanonicalFee(FeatureBitset features)
{
testcase("canonical fee");
using namespace jtx;
auto const master = Account("master", KeyType::ed25519);
auto const eph1 = Account("eph1", KeyType::ed25519);
auto const units = [](STObject const& manifestObject) {
return 100 +
10 *
static_cast<std::int64_t>(
manifestObject.getSerializer().getDataLength());
};
auto const manifest = makeManifest(master, eph1, 1);
// The builder prices against the voted base fee of the open ledger.
auto const build = [&](Env& env) -> std::shared_ptr<STTx const> {
auto const hex = makeSetManifestTx(
makeSlice(manifest),
env.app().config().NETWORK_ID,
*env.current(),
env.app().journal("SetManifest_test"));
if (!BEAST_EXPECT(hex))
return nullptr;
auto const bytes = strUnHex(*hex);
if (!BEAST_EXPECT(bytes))
return nullptr;
SerialIter txIter{makeSlice(*bytes)};
return std::make_shared<STTx const>(std::ref(txIter));
};
auto const manifestOf = [](STTx const& tx) -> STObject const& {
return const_cast<STTx&>(tx)
.getField(sfManifest)
.downcast<STObject>();
};
std::shared_ptr<STTx const> ordinaryTx;
{
Env ordinary{*this, makeConfig("10"), features};
ordinaryTx = build(ordinary);
if (!ordinaryTx)
return;
BEAST_EXPECT(
(*ordinaryTx)[sfFee].xrp().drops() ==
10 * units(manifestOf(*ordinaryTx)));
// A zero base would make the transaction free, so there is no
// canonical Fee for it at all.
BEAST_EXPECT(!canonicalSetManifestFee(
XRPAmount{0}, manifestOf(*ordinaryTx)));
}
// A different fee setting maps the same manifest to exactly one
// different txid.
Env expensive{*this, makeConfig("100000"), features};
auto const expensiveTx = build(expensive);
if (!expensiveTx)
return;
BEAST_EXPECT(
(*expensiveTx)[sfFee].xrp().drops() ==
100'000 * units(manifestOf(*expensiveTx)));
BEAST_EXPECT(
expensiveTx->getTransactionID() != ordinaryTx->getTransactionID());
expensive.fund(XRP(10000), master);
expensive.close();
// A wrapper priced for another fee setting is a local miss, not a bad
// signature: ingress drops it before manifest crypto without marking
// it bad, and preclaim refuses it with a tel result.
BEAST_EXPECT(
checkManifestIngressFee(expensive.app(), *ordinaryTx).first ==
ManifestIngressFee::Refused);
BEAST_EXPECT(
submit(expensive, ordinaryTx)[jss::error] == "invalidTransaction");
BEAST_EXPECT(
(expensive.app().getHashRouter().getFlags(
ordinaryTx->getTransactionID()) &
SF_BAD) == 0);
BEAST_EXPECT(
applyDirect(expensive, ordinaryTx) == telMANIFEST_FEE_MISMATCH);
BEAST_EXPECT(
applyDirect(
env, envelope(env, good, master.id(), [&](STObject& obj) {
obj.setFieldAmount(sfFee, ceiling + XRPAmount{1});
})) == temBAD_FEE);
checkManifestIngressFee(expensive.app(), *expensiveTx).first ==
ManifestIngressFee::Canonical);
BEAST_EXPECT(engineResult(submit(expensive, manifest)) == "tesSUCCESS");
expensive.close();
BEAST_EXPECT(expensive.le(keylet::manifest(master.pk())));
// At the ceiling exactly, which is what Submit sends.
// An amendment-blocked server has no base fee it can trust, so ingress
// refuses even the canonical transaction rather than verify it.
Env blocked{*this, makeConfig("10"), features};
auto const blockedTx = build(blocked);
if (!blockedTx)
return;
BEAST_EXPECT(
applyDirect(
env, envelope(env, good, master.id(), [&](STObject& obj) {
obj.setFieldAmount(sfFee, ceiling);
})) == tesSUCCESS);
checkManifestIngressFee(blocked.app(), *blockedTx).first ==
ManifestIngressFee::Canonical);
blocked.app().getOPs().setAmendmentBlocked();
BEAST_EXPECT(
checkManifestIngressFee(blocked.app(), *blockedTx).first ==
ManifestIngressFee::Refused);
}
void
@@ -807,6 +962,7 @@ public:
testSigningKeyRetrieval(sa);
testMalformed(sa);
testEnvelopeRejections(sa);
testCanonicalFee(sa);
testCorruptLedger(sa);
testGossipSelection(sa);
testDisabled(sa);
+1 -1
View File
@@ -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);
+127 -118
View File
@@ -2311,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 = "κόσμε";
@@ -2349,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 "
@@ -2484,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";
@@ -2617,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)
{
+1 -1
View File
@@ -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}},
+4 -4
View File
@@ -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,79 @@
//------------------------------------------------------------------------------
/*
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/basics/ReaderPreferringSharedMutex.h>
#include <xrpl/beast/unit_test.h>
#include <mutex>
#include <shared_mutex>
namespace ripple {
namespace test {
class ReaderPreferringSharedMutex_test : public beast::unit_test::suite
{
void
testExclusiveAndShared()
{
testcase("exclusive and shared locking");
ReaderPreferringSharedMutex mutex;
BEAST_EXPECT(mutex.try_lock());
BEAST_EXPECT(!mutex.try_lock());
BEAST_EXPECT(!mutex.try_lock_shared());
mutex.unlock();
mutex.lock_shared();
BEAST_EXPECT(mutex.try_lock_shared());
BEAST_EXPECT(!mutex.try_lock());
mutex.unlock_shared();
mutex.unlock_shared();
{
std::shared_lock shared{mutex};
BEAST_EXPECT(!mutex.try_lock());
BEAST_EXPECT(mutex.try_lock_shared());
mutex.unlock_shared();
}
{
std::unique_lock exclusive{mutex};
BEAST_EXPECT(!mutex.try_lock());
BEAST_EXPECT(!mutex.try_lock_shared());
}
mutex.lock();
BEAST_EXPECT(!mutex.try_lock_shared());
mutex.unlock();
BEAST_EXPECT(mutex.try_lock_shared());
mutex.unlock_shared();
}
void
run() override
{
testExclusiveAndShared();
}
};
BEAST_DEFINE_TESTSUITE(ReaderPreferringSharedMutex, ripple_basics, ripple);
} // namespace test
} // namespace ripple
+246
View File
@@ -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
+11
View File
@@ -59,6 +59,17 @@ public:
testUnHexFailure("123X");
testUnHexFailure("V");
testUnHexFailure("XAH");
// Bytes >= 0x80 (negative when char is signed) must be rejected, not
// used to index the lookup table out of bounds
for (int b = 0x80; b <= 0xff; ++b)
{
std::string const c(1, static_cast<char>(b));
testUnHexFailure(c);
testUnHexFailure("0" + c);
testUnHexFailure(c + "0");
testUnHexFailure("00" + c + "00");
}
}
void
+69
View File
@@ -0,0 +1,69 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpl/basics/Slice.h>
#include <xrpl/basics/UTF8.h>
#include <xrpl/beast/unit_test.h>
namespace ripple {
struct UTF8_test : public beast::unit_test::suite
{
void
run() override
{
testcase("isValidUTF8");
auto const check = [](std::string const& s) {
return isValidUTF8(makeSlice(s));
};
// Sequences truncated by the end of the buffer are invalid. The
// previous decoder read past the end of the vector here.
BEAST_EXPECT(!check("a\xC2"));
BEAST_EXPECT(!check("a\xE0\xA4"));
BEAST_EXPECT(!check("a\xEF\xBF"));
BEAST_EXPECT(!check("a\xF0\x90\x8D"));
BEAST_EXPECT(!check(std::string("\xC2", 1)));
BEAST_EXPECT(!check(std::string("\xF4\x8F\xBF", 3)));
// The complete sequences are fine.
BEAST_EXPECT(check("a\xC2\xA2"));
BEAST_EXPECT(check("a\xE0\xA4\xB9"));
BEAST_EXPECT(check("a\xF0\x90\x8D\x88"));
BEAST_EXPECT(check("a\xF4\x8F\xBF\xBF"));
BEAST_EXPECT(check(""));
// U+FFFE and U+FFFF are rejected wherever they appear.
BEAST_EXPECT(!check("\xEF\xBF\xBE"));
BEAST_EXPECT(!check("ab\xEF\xBF\xBF"));
BEAST_EXPECT(!check("\xE2\x82\xAC\xEF\xBF\xBEz"));
// Their neighbours are not, and nor are the other noncharacters,
// which the validator has never rejected.
BEAST_EXPECT(check("\xEF\xBF\xBD")); // U+FFFD
BEAST_EXPECT(check("\xEF\xBE\xBF")); // U+FFBF
BEAST_EXPECT(check("\xEF\xBB\xBF")); // U+FEFF (BOM)
BEAST_EXPECT(check("\xF0\x9F\xBF\xBE")); // U+1FFFE
}
};
BEAST_DEFINE_TESTSUITE(UTF8, basics, ripple);
} // namespace ripple
+91
View File
@@ -462,11 +462,102 @@ class base58_test : public beast::unit_test::suite
}
}
void
testDecodeInvalidChars()
{
testcase("decode_invalid_chars");
// Every byte outside the base58 alphabet, including bytes >= 0x80
// (negative when char is signed), must be rejected at any position.
// The fast decoder reads the leading `size % 10` characters and the
// remaining characters in two separate loops; varying the length and
// the position of the bad byte exercises both.
constexpr std::string_view alphabet =
"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz";
std::array<bool, 256> inAlphabet{};
for (unsigned char const c : alphabet)
inAlphabet[c] = true;
auto const invalidCode =
make_error_code(TokenCodecErrc::invalidEncodingChar);
// 52 is the longest input the fast decoder accepts
for (std::size_t len = 1; len <= 52; ++len)
{
std::string base;
for (std::size_t i = 0; i < len; ++i)
base.push_back(alphabet[(i * 7 + len) % alphabet.size()]);
for (std::size_t pos = 0; pos < len; ++pos)
{
for (int b = 0; b < 256; ++b)
{
if (inAlphabet[b])
continue;
std::string s = base;
s[pos] = static_cast<char>(b);
std::array<std::uint8_t, 64> outBuf;
auto const r = b58_fast::detail::b58_to_b256_be(s, outBuf);
BEAST_EXPECT(!r && r.error() == invalidCode);
BEAST_EXPECT(b58_ref::detail::decodeBase58(s).empty());
}
}
}
// Same through the token API, using a valid AccountID as the base
constexpr TokenType tokType = TokenType::AccountID;
std::string const addr = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
BEAST_EXPECT(!decodeBase58Token(addr, tokType).empty());
for (std::size_t pos = 0; pos < addr.size(); ++pos)
{
for (int b = 0; b < 256; ++b)
{
if (inAlphabet[b])
continue;
std::string s = addr;
s[pos] = static_cast<char>(b);
std::array<std::uint8_t, 64> outBuf;
auto const r = b58_fast::decodeBase58Token(tokType, s, outBuf);
BEAST_EXPECT(!r && r.error() == invalidCode);
BEAST_EXPECT(b58_ref::decodeBase58Token(s, tokType).empty());
BEAST_EXPECT(decodeBase58Token(s, tokType).empty());
}
}
}
void
testDecodeMaxValue()
{
testcase("decode_max_value");
// The largest value of every accepted length ('z' is digit 57) must
// decode identically in both implementations. At 52 chars this is the
// widest intermediate the fast decoder's bigint ever has to hold.
for (std::size_t len = 1; len <= 52; ++len)
{
std::string const s(len, 'z');
std::array<std::uint8_t, 64> outBuf;
auto const r = b58_fast::detail::b58_to_b256_be(s, outBuf);
std::string const ref = b58_ref::detail::decodeBase58(s);
if (!BEAST_EXPECT(r))
continue;
BEAST_EXPECT(
std::string(r.value().begin(), r.value().end()) == ref);
}
}
void
run() override
{
testMultiprecision();
testFastMatchesRef();
testDecodeInvalidChars();
testDecodeMaxValue();
}
};
+21 -14
View File
@@ -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));
+202
View File
@@ -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
+13 -10
View File
@@ -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(); });
+91
View File
@@ -1203,6 +1203,96 @@ class UNLReportVoteNewValidator_test : public beast::unit_test::suite
}
};
/*
* Regression test: UNLReport proposals must not be gated on the N-UNL local
* reliability threshold.
*
* The local node (validator 0) validates exactly `myVals` of the 256 scored
* ledgers, validator 1 validates none (so it is an N-UNL disable candidate and
* absent from the report), every other validator validates all of them.
*
* == myVals <= 128: not active ourselves -> no UNLReport, no UNLModify
* == 129 .. 229: UNLReport proposed, not reliable enough to vote on N-UNL
* == >= 230: UNLReport and one UNLModify (230 used to fall into the
* "Too many!" branch and fail)
*/
class UNLReportVoteLocalReliability_test : public beast::unit_test::suite
{
void
testDoVoting()
{
testcase("Do Voting with local reliability below N-UNL threshold");
struct Case
{
std::uint32_t myVals;
bool expectReport;
bool expectModify;
};
std::array<Case, 6> const cases = {{
{128, false, false},
{129, true, false},
{229, true, false},
{230, true, true},
{231, true, true},
{256, true, true},
}};
for (bool const withVLImport : {true, false})
{
for (auto const& c : cases)
{
std::uint32_t const numNodes = 20;
URNetworkHistory history = {
*this, {numNodes, 0, false, false, withVLImport, {}}};
BEAST_EXPECT(history.goodHistory);
if (!history.goodHistory)
continue;
// scored ledgers are [lastSeq - 256, lastSeq - 1]
auto const lastSeq = history.lastLedger()->seq();
history.walkHistoryAndAddValidations(
[&](std::shared_ptr<Ledger const> const& l,
std::size_t idx) -> bool {
if (idx == 1)
return false;
if (idx != 0)
return true;
return l->seq() + 256 < lastSeq + c.myVals;
});
NegativeUNLVote vote(
history.UNLNodeIDs[0],
history.env.journal,
history.env.app());
auto txSet = std::make_shared<SHAMap>(
SHAMapType::TRANSACTION, history.env.app().getNodeFamily());
vote.doVoting(
history.lastLedger(),
history.UNLKeySet,
history.validations,
txSet);
std::size_t const expectReport =
c.expectReport ? numNodes - 1 + (withVLImport ? 1 : 0) : 0;
std::size_t const expectModify = c.expectModify ? 1 : 0;
std::string const ctx = "myVals=" + std::to_string(c.myVals) +
" withVLImport=" + std::to_string(withVLImport);
BEAST_EXPECTS(countUNLRTx(txSet) == expectReport, ctx);
BEAST_EXPECTS(unl::countTx(txSet) == expectModify, ctx);
}
}
}
void
run() override
{
testDoVoting();
}
};
BEAST_DEFINE_TESTSUITE(UNLReport, ledger, ripple);
BEAST_DEFINE_TESTSUITE(UNLReportNoAmendment, ledger, ripple);
BEAST_DEFINE_TESTSUITE(UNLReportFork, consensus, ripple);
@@ -1215,6 +1305,7 @@ BEAST_DEFINE_TESTSUITE_PRIO(
ripple,
1);
BEAST_DEFINE_TESTSUITE(UNLReportVoteNewValidator, consensus, ripple);
BEAST_DEFINE_TESTSUITE(UNLReportVoteLocalReliability, consensus, ripple);
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
+53 -29
View File
@@ -1102,23 +1102,24 @@ trustthesevalidators.gov
Config cfg;
/* NOTE: this string includes some explicit
* space chars in order to verify proper trimming */
std::string toLoad(R"(
std::string toLoad(
R"(
[port_rpc])"
"\x20"
R"(
"\x20"
R"(
# comment
# indented comment
)"
"\x20\x20"
R"(
"\x20\x20"
R"(
[ips])"
"\x20"
R"(
"\x20"
R"(
r.ripple.com 51235
[ips_fixed])"
"\x20\x20"
R"(
"\x20\x20"
R"(
# COMMENT
s1.ripple.com 51235
s2.ripple.com 51235
@@ -1144,23 +1145,24 @@ r.ripple.com 51235
Config cfg;
/* NOTE: this string includes some explicit
* space chars in order to verify proper trimming */
std::string toLoad(R"(
std::string toLoad(
R"(
[port_rpc])"
"\x20"
R"(
"\x20"
R"(
# comment
# indented comment
)"
"\x20\x20"
R"(
"\x20\x20"
R"(
[ips])"
"\x20"
R"(
"\x20"
R"(
r.ripple.com:51235
[ips_fixed])"
"\x20\x20"
R"(
"\x20\x20"
R"(
# COMMENT
s1.ripple.com:51235
s2.ripple.com 51235
@@ -1447,8 +1449,8 @@ r.ripple.com:51235
}
}
// Test 2: RWDB without online_delete NOT in standalone mode (should
// throw)
// Test 2: RWDB without online_delete NOT in standalone mode
// (now allowed — SHAMapStoreImp defaults it to ledger_history)
{
Config c;
std::string toLoad =
@@ -1459,16 +1461,12 @@ r.ripple.com:51235
try
{
c.loadFromString(toLoad);
fail("Expected exception for RWDB without online_delete");
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
std::string(e.what()).find(
"RWDB (in-memory backend) requires online_delete") !=
std::string::npos);
pass();
}
catch (std::runtime_error const&)
{
fail("Should not throw for RWDB without online_delete");
}
}
// Test 3: RWDB with online_delete NOT in standalone mode (should
@@ -1613,7 +1611,33 @@ r.ripple.com:51235
}
}
// Test 5: pinned_type without online_delete (should throw)
// Test 5: type=rwdb with pinned_type (should throw, even in
// standalone). The rwdb node store is tree-only, so pinned data
// would be accepted and then dropped.
for (bool const standalone : {false, true})
{
Config c;
std::string toLoad =
"[node_db]\n"
"type=rwdb\n"
"online_delete=256\n"
"pinned_type=NuDB\n"
"pinned_path=pinned\n";
c.setupControl(true, true, standalone);
try
{
c.loadFromString(toLoad);
fail("Expected exception for pinned_type with type=rwdb");
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
std::string(e.what()).find("type=rwdb") !=
std::string::npos);
}
}
// Test 6: pinned_type without online_delete (should throw)
{
Config c;
std::string toLoad =
+7
View File
@@ -56,6 +56,13 @@ public:
/// Get RPC 1.0 or RPC 2.0
virtual unsigned
version() const = 0;
/// Times invoke() replaced a connection the server had closed.
virtual unsigned
reconnects() const
{
return 0;
}
};
} // namespace test
+91
View File
@@ -18,6 +18,7 @@
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/JSONRPCClient.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/app/misc/TxQ.h>
#include <xrpl/beast/hash/uhash.h>
@@ -27,6 +28,12 @@
#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>
@@ -917,6 +924,7 @@ public:
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
@@ -924,6 +932,87 @@ public:
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
@@ -954,6 +1043,8 @@ public:
testFeatures(all);
testExceptionalShutdown();
testJSONRPCClientKeepAlive();
testJSONRPCClientSlowFirstRequest();
testJSONRPCClientReconnects();
}
};
+2 -2
View File
@@ -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()}](
+19
View File
@@ -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;
+54 -3
View File
@@ -23,6 +23,7 @@
#include <xrpl/server/Port.h>
#include <boost/asio.hpp>
#include <boost/beast/http/dynamic_body.hpp>
#include <boost/beast/http/error.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/read.hpp>
#include <boost/beast/http/string_body.hpp>
@@ -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>
+39 -17
View File
@@ -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
+9 -2
View File
@@ -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
+182 -27
View File
@@ -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();
+9 -5
View File
@@ -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); });
}
+29
View File
@@ -82,6 +82,35 @@ public:
}
}
if (type == "rwdb")
{
std::unique_ptr<Backend> backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
backend->open();
try
{
backend->open();
fail("rwdb open twice should throw");
}
catch (std::exception const&)
{
pass();
}
storeBatch(*backend, batch);
backend->close();
BEAST_EXPECT(!backend->isOpen());
std::shared_ptr<NodeObject> fetched;
BEAST_EXPECT(
backend->fetch(batch.front()->getHash().data(), &fetched) ==
notFound);
backend->store(batch.front());
int visited = 0;
backend->for_each([&visited](auto const&) { ++visited; });
BEAST_EXPECT(visited == 0);
}
// rwdb backend does not keep table/data after close
if (type != "rwdb")
{
+11 -5
View File
@@ -25,6 +25,7 @@
#include <xrpld/nodestore/detail/DatabasePinnedImp.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/protocol/digest.h>
namespace ripple {
@@ -33,13 +34,16 @@ namespace test {
class DatabasePinned_test : public beast::unit_test::suite
{
// Build an Env whose nodestore is wired up as DatabasePinnedImp.
// pinned_type=rwdb is allowed in standalone mode (the catalogue
// tests do the same).
// The rotating store must persist (type=memory). type=rwdb is
// NullFactory and never builds DatabasePinnedImp. pinned_type=rwdb
// is allowed in standalone mode.
static std::unique_ptr<Config>
pinnedEnvconfig()
pinnedEnvconfig(std::string const& nodePath)
{
auto cfg = jtx::envconfig();
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
nodeDb.set("type", "memory");
nodeDb.set("path", nodePath);
nodeDb.set("pinned_type", "rwdb");
nodeDb.set("online_delete", "256");
return cfg;
@@ -84,7 +88,8 @@ class DatabasePinned_test : public beast::unit_test::suite
testcase("store routing by NodeObjectType");
using namespace jtx;
Env env{*this, pinnedEnvconfig()};
beast::temp_dir td;
Env env{*this, pinnedEnvconfig(td.path())};
auto* db = getPinnedDb(env);
if (!db)
return;
@@ -136,7 +141,8 @@ class DatabasePinned_test : public beast::unit_test::suite
testcase("store/fetch round-trip across both backends");
using namespace jtx;
Env env{*this, pinnedEnvconfig()};
beast::temp_dir td;
Env env{*this, pinnedEnvconfig(td.path())};
auto& nodeStore = env.app().getNodeStore();
// Store one object of each type with distinct content so the
+305
View File
@@ -0,0 +1,305 @@
//------------------------------------------------------------------------------
/*
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/jtx.h>
#include <xrpld/core/ConfigSections.h>
#include <xrpld/overlay/Compression.h>
#include <xrpld/overlay/Message.h>
#include <xrpld/overlay/detail/OverlayImpl.h>
#include <xrpld/overlay/detail/PeerImp.h>
#include <xrpld/shamap/SHAMapInnerNode.h>
#include <xrpld/shamap/SHAMapNodeID.h>
#include <xrpl/basics/make_SSLContext.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/messages.h>
#include <optional>
namespace ripple {
namespace test {
class GetObjectByHash_test : public beast::unit_test::suite
{
using socket_type = boost::asio::ip::tcp::socket;
using middle_type = boost::beast::tcp_stream;
using stream_type = boost::beast::ssl_stream<middle_type>;
class PeerTest : public PeerImp
{
public:
std::vector<std::shared_ptr<Message>> sent;
PeerTest(
Application& app,
std::shared_ptr<PeerFinder::Slot> const& slot,
http_request_type&& request,
PublicKey const& publicKey,
ProtocolVersion protocol,
Resource::Consumer consumer,
std::unique_ptr<GetObjectByHash_test::stream_type>&& stream_ptr,
OverlayImpl& overlay)
: PeerImp(
app,
1,
slot,
std::move(request),
publicKey,
protocol,
consumer,
std::move(stream_ptr),
overlay)
{
}
void
run() override
{
}
void
send(std::shared_ptr<Message> const& m) override
{
sent.push_back(m);
}
};
static auto
nullBackend(std::unique_ptr<Config> cfg)
{
cfg->LEDGER_HISTORY = 8;
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("type", "rwdb");
section.set("path", "main");
return cfg;
}
std::shared_ptr<PeerTest>
makePeer(jtx::Env& env)
{
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
http_request_type request;
auto stream_ptr = std::make_unique<stream_type>(
socket_type(env.app().getIOContext()), *context_);
beast::IP::Endpoint const local(
boost::asio::ip::make_address("172.16.0.1"));
beast::IP::Endpoint const remote(
boost::asio::ip::make_address("172.16.0.2"));
PublicKey const key(std::get<0>(randomKeyPair(KeyType::ed25519)));
auto consumer = overlay.resourceManager().newInboundEndpoint(remote);
auto slot = overlay.peerFinder().new_inbound_slot(local, remote);
auto peer = std::make_shared<PeerTest>(
env.app(),
slot,
std::move(request),
key,
ProtocolVersion{1, 7},
consumer,
std::move(stream_ptr),
overlay);
overlay.add_active(peer);
return peer;
}
std::optional<protocol::TMGetObjectByHash>
parseReply(std::shared_ptr<Message> const& message)
{
if (!message)
return {};
auto const& buf = message->getBuffer(compression::Compressed::Off);
if (buf.size() <= compression::headerBytes)
return {};
protocol::TMGetObjectByHash reply;
if (!reply.ParseFromArray(
buf.data() + compression::headerBytes,
static_cast<int>(buf.size() - compression::headerBytes)))
return {};
return reply;
}
std::shared_ptr<boost::asio::ssl::context> context_ = make_SSLContext("");
public:
void
testServesLinkedAndCachedNodes()
{
testcase(
"GetObjectByHash serves cache, linked tree, and ledger header");
using namespace jtx;
Env env(*this, envconfig(nullBackend));
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env(pay(alice, bob, XRP(1)));
env.close();
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
if (!BEAST_EXPECT(ledger))
return;
auto const stateHash = ledger->stateMap().getHash().as_uint256();
auto const txHash = ledger->txMap().getHash().as_uint256();
auto const headerHash = ledger->info().hash;
auto const seq = ledger->info().seq;
auto const rootId = SHAMapNodeID{}.getRawString();
auto peer = makePeer(env);
auto sendQuery = [&](auto&& fill) {
peer->sent.clear();
auto packet = std::make_shared<protocol::TMGetObjectByHash>();
packet->set_query(true);
fill(*packet);
// Production dispatch resets fee_ in onMessageBegin before
// onMessage. Calling onMessage alone leaves a moderate burden
// charge in place and Debug XRPL_ASSERT fires on a later
// malformed request with a smaller fee.
auto const size = packet->ByteSizeLong();
peer->onMessageBegin(
protocol::mtGET_OBJECTS, packet, size, size, false);
peer->onMessage(packet);
peer->onMessageEnd(protocol::mtGET_OBJECTS, packet);
};
// TreeNodeCache hit: hash only, no nodeid.
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
auto& obj = *packet.add_objects();
obj.set_hash(stateHash.begin(), stateHash.size());
obj.set_ledgerseq(seq);
});
if (auto const cached =
env.app().getNodeFamily().getTreeNodeCache()->fetch(stateHash))
{
if (!BEAST_EXPECT(peer->sent.size() == 1))
return;
auto const reply = parseReply(peer->sent.front());
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
return;
BEAST_EXPECT(uint256{reply->objects(0).hash()} == stateHash);
}
// After dropping the interned cache, the same request needs a nodeid
// so fetchLinkedTreeNode can walk the retained SHAMap.
env.app().getNodeFamily().getTreeNodeCache()->reset();
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
auto& obj = *packet.add_objects();
obj.set_hash(stateHash.begin(), stateHash.size());
obj.set_ledgerseq(seq);
});
{
if (!BEAST_EXPECT(peer->sent.size() == 1))
return;
auto const reply = parseReply(peer->sent.front());
if (!BEAST_EXPECT(reply))
return;
BEAST_EXPECT(reply->objects_size() == 0);
}
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
auto& obj = *packet.add_objects();
obj.set_hash(stateHash.begin(), stateHash.size());
obj.set_nodeid(rootId);
obj.set_ledgerseq(seq);
});
{
if (!BEAST_EXPECT(peer->sent.size() == 1))
return;
auto const reply = parseReply(peer->sent.front());
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
return;
BEAST_EXPECT(uint256{reply->objects(0).hash()} == stateHash);
BEAST_EXPECT(reply->objects(0).index() == rootId);
BEAST_EXPECT(reply->objects(0).ledgerseq() == seq);
}
// Transaction map root, found after the state-map miss.
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otTRANSACTION_NODE);
auto& obj = *packet.add_objects();
obj.set_hash(txHash.begin(), txHash.size());
obj.set_nodeid(rootId);
obj.set_ledgerseq(seq);
});
{
if (!BEAST_EXPECT(peer->sent.size() == 1))
return;
auto const reply = parseReply(peer->sent.front());
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
return;
BEAST_EXPECT(uint256{reply->objects(0).hash()} == txHash);
}
// Linked walk finds the node, hash does not match, object omitted.
uint256 const wrong{2};
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
auto& obj = *packet.add_objects();
obj.set_hash(wrong.begin(), wrong.size());
obj.set_nodeid(rootId);
obj.set_ledgerseq(seq);
});
{
if (!BEAST_EXPECT(peer->sent.size() == 1))
return;
auto const reply = parseReply(peer->sent.front());
if (!BEAST_EXPECT(reply))
return;
BEAST_EXPECT(reply->objects_size() == 0);
}
// Ledger header fallback when the node store and tree caches miss.
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otLEDGER);
auto& obj = *packet.add_objects();
obj.set_hash(headerHash.begin(), headerHash.size());
obj.set_ledgerseq(seq);
});
{
if (!BEAST_EXPECT(peer->sent.size() == 1))
return;
auto const reply = parseReply(peer->sent.front());
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
return;
BEAST_EXPECT(uint256{reply->objects(0).hash()} == headerHash);
BEAST_EXPECT(!reply->objects(0).data().empty());
}
// Malformed ledger hash is rejected before a reply is sent.
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otLEDGER);
packet.set_ledgerhash("short");
auto& obj = *packet.add_objects();
obj.set_hash(headerHash.begin(), headerHash.size());
});
BEAST_EXPECT(peer->sent.empty());
}
void
run() override
{
testServesLinkedAndCachedNodes();
}
};
BEAST_DEFINE_TESTSUITE(GetObjectByHash, overlay, ripple);
} // namespace test
} // namespace ripple
+415
View File
@@ -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
+1 -1
View File
@@ -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(
+1 -1
View File
@@ -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(
+16 -10
View File
@@ -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(
+4 -4
View File
@@ -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);
+40
View File
@@ -17,6 +17,7 @@
*/
//==============================================================================
#include <xrpl/beast/core/SemanticVersion.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/BuildInfo.h>
@@ -25,6 +26,44 @@ namespace ripple {
class BuildInfo_test : public beast::unit_test::suite
{
public:
void
testGetVersionString()
{
testcase("GetVersionString");
auto const& s = BuildInfo::getVersionString();
BEAST_EXPECT(!s.empty());
// Same reference on every call (static cache).
BEAST_EXPECT(&BuildInfo::getVersionString() == &s);
// Must be strict semver and round-trip unchanged (no leading
// zeros, no stray whitespace).
beast::SemanticVersion v;
BEAST_EXPECT(v.parse(s));
BEAST_EXPECT(v.print() == s);
BEAST_EXPECT(BuildInfo::getFullVersionString() == "xahaud-" + s);
if (s.find("-CustomBuild") == std::string::npos)
return;
// Version generated from __DATE__ ("Mmm dd yyyy"):
// <year>.<month>.<day>-CustomBuild[+DEBUG]
BEAST_EXPECT(v.majorVersion >= 2023 && v.majorVersion <= 9999);
BEAST_EXPECT(v.minorVersion >= 1 && v.minorVersion <= 12);
BEAST_EXPECT(v.patchVersion >= 1 && v.patchVersion <= 31);
BEAST_EXPECT(
v.preReleaseIdentifiers ==
beast::SemanticVersion::identifier_list{"CustomBuild"});
#ifdef DEBUG
BEAST_EXPECT(
v.metaData == beast::SemanticVersion::identifier_list{"DEBUG"});
#else
BEAST_EXPECT(v.metaData.empty());
#endif
}
void
testEncodeSoftwareVersion()
{
@@ -119,6 +158,7 @@ public:
void
run() override
{
testGetVersionString();
testEncodeSoftwareVersion();
testIsRippledVersion();
testIsNewerVersion();
+3 -12
View File
@@ -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
+31
View File
@@ -103,6 +103,35 @@ public:
BEAST_EXPECT(fieldNames.insert(name).second);
}
}
// hook api, keylet types and return codes
{
auto const& hook = result[jss::result][jss::HOOK];
BEAST_EXPECT(hook.isMember(jss::API));
BEAST_EXPECT(hook.isMember(jss::KEYLET_TYPES));
BEAST_EXPECT(hook.isMember(jss::RETURN_CODES));
auto const& accept = hook[jss::API]["accept"];
BEAST_EXPECT(accept["return"] == "int64_t");
BEAST_EXPECT(accept["params"].size() == 3);
BEAST_EXPECT(accept["params"][0u][jss::type] == "uint32_t");
BEAST_EXPECT(accept["params"][0u][jss::name] == "read_ptr");
BEAST_EXPECT(accept["params"][2u][jss::type] == "int64_t");
BEAST_EXPECT(accept["params"][2u][jss::name] == "error_code");
BEAST_EXPECT(!accept.isMember("amendment"));
auto const& burden = hook[jss::API]["etxn_burden"];
BEAST_EXPECT(burden["params"].size() == 0);
auto const& xpop = hook[jss::API]["xpop_slot"];
BEAST_EXPECT(xpop["amendment"] == "HooksUpdate1");
BEAST_EXPECT(hook[jss::KEYLET_TYPES]["HOOK"] == 1);
BEAST_EXPECT(hook[jss::KEYLET_TYPES]["MANIFEST"] == 37);
BEAST_EXPECT(hook[jss::RETURN_CODES]["SUCCESS"] == 0);
BEAST_EXPECT(hook[jss::RETURN_CODES]["INVALID_FLOAT"] == -10024);
BEAST_EXPECT(hook[jss::RETURN_CODES]["TOO_MANY_NAMESPACES"] == -45);
}
}
void
@@ -132,6 +161,7 @@ public:
BEAST_EXPECT(
!result[jss::result].isMember(jss::TRANSACTION_FLAGS_INDICES));
BEAST_EXPECT(!result[jss::result].isMember(jss::TYPES));
BEAST_EXPECT(!result[jss::result].isMember(jss::HOOK));
BEAST_EXPECT(result[jss::result].isMember(jss::hash));
}
@@ -156,6 +186,7 @@ public:
BEAST_EXPECT(
result[jss::result].isMember(jss::TRANSACTION_FLAGS_INDICES));
BEAST_EXPECT(result[jss::result].isMember(jss::TYPES));
BEAST_EXPECT(result[jss::result].isMember(jss::HOOK));
BEAST_EXPECT(result[jss::result].isMember(jss::hash));
}
}
+4 -23
View File
@@ -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();
+13 -5
View File
@@ -847,6 +847,14 @@ public:
Account alice("alice");
Account bob("bob");
Account carol("carol");
// Account history loads ledgers by seq. type=rwdb is tree-only
// and cannot reconstruct those maps from the node store.
auto storing = [] {
auto cfg = envconfig();
cfg->section(ConfigSection::nodeDatabase()).set("type", "memory");
return cfg;
};
Account david("david");
///////////////////////////////////////////////////////////////////
@@ -1015,7 +1023,7 @@ public:
*
* also test subscribe to the account before it is created
*/
Env env(*this);
Env env(*this, storing());
auto wscTxHistory = makeWSClient(env.app().config());
Json::Value request;
request[jss::account_history_tx_stream] = Json::objectValue;
@@ -1061,7 +1069,7 @@ public:
* subscribe genesis account tx history without txns
* subscribe to bob's account after it is created
*/
Env env(*this);
Env env(*this, storing());
auto wscTxHistory = makeWSClient(env.app().config());
Json::Value request;
request[jss::account_history_tx_stream] = Json::objectValue;
@@ -1140,7 +1148,7 @@ public:
* subscribe account and subscribe account tx history
* and compare txns streamed
*/
Env env(*this);
Env env(*this, storing());
auto wscAccount = makeWSClient(env.app().config());
auto wscTxHistory = makeWSClient(env.app().config());
@@ -1211,7 +1219,7 @@ public:
* alice issues USD to carol
* mix USD and XRP payments
*/
Env env(*this);
Env env(*this, storing());
auto const USD_a = alice["USD"];
std::array<Account, 2> accounts = {alice, carol};
@@ -1250,7 +1258,7 @@ public:
/*
* long transaction history
*/
Env env(*this);
Env env(*this, storing());
std::array<Account, 2> accounts = {alice, carol};
env.fund(XRP(444444), accounts);
env.close();
+2 -2
View File
@@ -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}},
+93
View File
@@ -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
+10 -13
View File
@@ -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;
+16 -13
View File
@@ -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);
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff. Show more