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
70 changed files with 5112 additions and 802 deletions

No files matched your search

+93 -151
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
+2
View File
@@ -96,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
@@ -191,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
+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"
@@ -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 -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 {};
+14 -12
View File
@@ -404,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
@@ -415,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;
+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
View File
@@ -69,6 +69,7 @@ enum TELcodes : TERUnderlyingType {
telIMPORT_VL_KEY_NOT_RECOGNISED,
telCAN_NOT_QUEUE_IMPORT,
telENV_RPC_FAILED,
telMANIFEST_FEE_MISMATCH,
};
//------------------------------------------------------------------------------
+1 -1
View File
@@ -38,7 +38,7 @@ XRPL_FIX (20261005, Supported::yes, VoteBehavior::DefaultYe
XRPL_FEATURE(EscrowDestinationCancel, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (20260929, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(HookOnV2_1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(OnChainManifests, Supported::no, VoteBehavior::DefaultNo)
XRPL_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)
+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);
+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);
+208 -20
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)
{
@@ -1828,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 = {
@@ -3281,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
@@ -4315,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");
@@ -4390,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
@@ -4759,6 +4944,7 @@ public:
test_prepare(features);
test_emit(features);
test_emitted_txn_entry(features);
test_etxn_burden(features);
test_etxn_generation(features);
test_otxn_burden(features);
@@ -4787,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);
@@ -4838,6 +5025,7 @@ public:
test_sto_subfield(features);
test_sto_subfield(features - fix20260929);
test_sto_validate(features);
test_sto_validate(features - fix20261005);
test_trace(features);
test_trace_float(features);
+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
+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();
}
+2 -5
View File
@@ -8439,11 +8439,8 @@ private:
using namespace std::literals;
testcase("set manifest TSH");
if (!features[featureOnChainManifests])
{
pass();
return;
}
// OnChainManifests is not yet Supported::yes, so enable it explicitly.
features = features | featureOnChainManifests;
// otxn: master
// tsh: ephemeral
+665 -25
View File
@@ -6482,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"};
@@ -9493,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>
@@ -9572,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
@@ -10957,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)
{
@@ -12667,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"};
@@ -12923,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*/));
}
}
}
@@ -15616,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); //
File diff suppressed because it is too large. Load diff
+194 -41
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
@@ -798,21 +953,19 @@ public:
void
run() override
{
// RH TODO: re-add these when the amendment is ready for supported::yes
/*
using namespace test::jtx;
auto const sa = supported_amendments();
testSubmission(sa);
testUpdate(sa);
testRevocation(sa);
testRetrieval(sa);
testSigningKeyRetrieval(sa);
testMalformed(sa);
testEnvelopeRejections(sa);
testCorruptLedger(sa);
testGossipSelection(sa);
testDisabled(sa);
*/
using namespace test::jtx;
auto const sa = supported_amendments();
testSubmission(sa);
testUpdate(sa);
testRevocation(sa);
testRetrieval(sa);
testSigningKeyRetrieval(sa);
testMalformed(sa);
testEnvelopeRejections(sa);
testCanonicalFee(sa);
testCorruptLedger(sa);
testGossipSelection(sa);
testDisabled(sa);
}
};
@@ -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
+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
+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();
}
};
+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 =
+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
+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();
+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));
}
}
+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();
+43
View File
@@ -20,11 +20,13 @@
#include <test/shamap/common.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpld/shamap/SHAMap.h>
#include <xrpld/shamap/SHAMapInnerNode.h>
#include <xrpld/shamap/SHAMapItem.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/protocol/Serializer.h>
namespace ripple {
namespace tests {
@@ -84,12 +86,53 @@ public:
return true;
}
void
testNullBackendIgnoresFullBelow(beast::Journal const& journal)
{
testcase("null backend does not skip children via FullBelowCache");
TestNodeFamily sourceFamily(journal);
TestNodeFamily destFamily(journal);
destFamily.setNullBackend(true);
SHAMap source(SHAMapType::FREE, sourceFamily);
for (int i = 0; i < 32; ++i)
source.addItem(SHAMapNodeType::tnACCOUNT_STATE, makeRandomAS());
source.setImmutable();
auto const rootHash = source.getHash();
auto const root = source.getLinkedNode(SHAMapNodeID{});
if (!BEAST_EXPECT(root && root->isInner()))
return;
auto* inner = static_cast<SHAMapInnerNode*>(root.get());
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
{
if (!inner->isEmptyBranch(b))
{
destFamily.getFullBelowCache()->insert(
inner->getChildHash(b).as_uint256());
}
}
SHAMap destination(SHAMapType::FREE, destFamily);
destination.setSynching();
Serializer s;
source.serializeRoot(s);
BEAST_EXPECT(
destination.addRootNode(rootHash, s.slice(), nullptr).isGood());
auto const missing = destination.getMissingNodes(2048, nullptr);
BEAST_EXPECT(!missing.empty());
}
void
run() override
{
using namespace beast::severities;
test::SuiteJournal journal("SHAMapSync_test", *this);
testNullBackendIgnoresFullBelow(journal);
TestNodeFamily f(journal), f2(journal);
SHAMap source(SHAMapType::FREE, f);
SHAMap destination(SHAMapType::FREE, f2);
+224
View File
@@ -19,11 +19,17 @@
#include <test/shamap/common.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpld/nodestore/NodeObject.h>
#include <xrpld/shamap/SHAMap.h>
#include <xrpld/shamap/SHAMapInnerNode.h>
#include <xrpld/shamap/SHAMapTreeNode.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Serializer.h>
#include <optional>
namespace ripple {
namespace tests {
@@ -122,6 +128,224 @@ public:
run(true, journal);
run(false, journal);
testCanonicalizeMergesInnerChildren(journal);
testCanonicalizeSharesDuplicateContent(journal);
testGetLinkedNode(journal);
}
void
testCanonicalizeSharesDuplicateContent(beast::Journal const& journal)
{
testcase("canonicalize shares nodes of identical maps");
tests::TestNodeFamily f(journal);
SHAMap a{SHAMapType::FREE, f};
SHAMap b{SHAMapType::FREE, f};
constexpr uint256 h1(
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
constexpr uint256 h2(
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
for (auto* map : {&a, &b})
{
BEAST_EXPECT(map->addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h1, IntToVUC(1))));
BEAST_EXPECT(map->addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h2, IntToVUC(2))));
}
// Do not call getHash() first: it unshares the map and flushDirty
// then has nothing to write.
auto const harvestedBefore = SHAMap::canonicalInnerBranchesHarvested();
a.flushDirty(hotTRANSACTION_NODE);
b.flushDirty(hotTRANSACTION_NODE);
// b's root and leaves were canonicalized onto a's. a's root was
// already fully linked, so there was nothing to harvest, and the
// leaf duplicates take the non-inner early return.
auto const rootA = a.getLinkedNode(SHAMapNodeID{});
auto const rootB = b.getLinkedNode(SHAMapNodeID{});
if (!BEAST_EXPECT(rootA && rootB && rootA->isInner()))
return;
BEAST_EXPECT(rootA.get() == rootB.get());
BEAST_EXPECT(
SHAMap::canonicalInnerBranchesHarvested() == harvestedBefore);
auto* inner = static_cast<SHAMapInnerNode*>(rootA.get());
int shared = 0;
for (int branch = 0; branch < SHAMapInnerNode::branchFactor; ++branch)
{
if (inner->isEmptyBranch(branch))
continue;
auto const id = SHAMapNodeID{}.getChildNodeID(branch);
auto const leafA = a.getLinkedNode(id);
auto const leafB = b.getLinkedNode(id);
BEAST_EXPECT(leafA && leafB && leafA.get() == leafB.get());
++shared;
}
BEAST_EXPECT(shared == 2);
}
void
testCanonicalizeMergesInnerChildren(beast::Journal const& journal)
{
testcase("canonicalize harvests inner children onto cache winner");
tests::TestNodeFamily f(journal);
SHAMap map{SHAMapType::FREE, f};
constexpr uint256 h1(
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
constexpr uint256 h2(
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
constexpr uint256 h3(
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
constexpr uint256 h4(
"c92891fe4ef6cee585fdc6fda2e09eb4d386363158ec3321b8123e5a772c6ca8");
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h1, IntToVUC(1))));
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h2, IntToVUC(2))));
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h3, IntToVUC(3))));
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h4, IntToVUC(4))));
// getHash() unshares the map (cowid=0), which makes flushDirty a
// no-op. Snapshot first so the original stays dirty and still
// canonicalizes onto the cache shell.
auto snap = map.snapShot(false);
auto const rootHash = snap->getHash();
Serializer s;
snap->serializeRoot(s);
auto shell = SHAMapTreeNode::makeFromWire(s.slice());
if (!BEAST_EXPECT(
shell && shell->isInner() && shell->getHash() == rootHash))
return;
auto innerShell = std::static_pointer_cast<SHAMapInnerNode>(shell);
int linkedBefore = 0;
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
{
if (!innerShell->isEmptyBranch(b) && innerShell->getChildPointer(b))
++linkedBefore;
}
BEAST_EXPECT(linkedBefore == 0);
f.getTreeNodeCache()->canonicalize_replace_client(
rootHash.as_uint256(), shell);
auto const harvestedBefore = SHAMap::canonicalInnerBranchesHarvested();
map.flushDirty(hotTRANSACTION_NODE);
auto cached = f.getTreeNodeCache()->fetch(rootHash.as_uint256());
if (!BEAST_EXPECT(cached && cached->isInner()))
return;
auto* inner = static_cast<SHAMapInnerNode*>(cached.get());
int linkedAfter = 0;
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
{
if (!inner->isEmptyBranch(b) && inner->getChildPointer(b))
++linkedAfter;
}
BEAST_EXPECT(linkedAfter > 0);
BEAST_EXPECT(
SHAMap::canonicalInnerBranchesHarvested() > harvestedBefore);
auto const rootNode = map.getLinkedNode(SHAMapNodeID{});
if (!BEAST_EXPECT(rootNode && rootNode->isInner()))
return;
auto* liveInner = static_cast<SHAMapInnerNode*>(rootNode.get());
int liveLinked = 0;
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
{
if (liveInner->isEmptyBranch(b))
{
BEAST_EXPECT(
!map.getLinkedNode(SHAMapNodeID{}.getChildNodeID(b)));
continue;
}
auto const child =
map.getLinkedNode(SHAMapNodeID{}.getChildNodeID(b));
if (liveInner->getChildPointer(b))
{
++liveLinked;
BEAST_EXPECT(
child && child->getHash() == liveInner->getChildHash(b));
}
}
BEAST_EXPECT(liveLinked > 0);
}
void
testGetLinkedNode(beast::Journal const& journal)
{
testcase("getLinkedNode walks only already-linked children");
tests::TestNodeFamily f(journal);
SHAMap map{SHAMapType::FREE, f};
constexpr uint256 h1(
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
constexpr uint256 h2(
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
constexpr uint256 h3(
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
constexpr uint256 h4(
"c92891fe4ef6cee585fdc6fda2e09eb4d386363158ec3321b8123e5a772c6ca8");
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h1, IntToVUC(1))));
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h2, IntToVUC(2))));
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h3, IntToVUC(3))));
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h4, IntToVUC(4))));
auto const root = map.getLinkedNode(SHAMapNodeID{});
if (!BEAST_EXPECT(root && root->isInner()))
return;
auto* inner = static_cast<SHAMapInnerNode*>(root.get());
std::optional<int> leafBranch;
std::optional<int> emptyBranch;
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
{
if (inner->isEmptyBranch(b))
{
if (!emptyBranch)
emptyBranch = b;
continue;
}
auto const child =
map.getLinkedNode(SHAMapNodeID{}.getChildNodeID(b));
BEAST_EXPECT(child);
if (child && !child->isInner() && !leafBranch)
leafBranch = b;
}
if (BEAST_EXPECT(emptyBranch))
{
BEAST_EXPECT(!map.getLinkedNode(
SHAMapNodeID{}.getChildNodeID(*emptyBranch)));
}
if (BEAST_EXPECT(leafBranch))
{
// A leaf has no children. Asking for a grandchild must miss.
auto const leafId = SHAMapNodeID{}.getChildNodeID(*leafBranch);
BEAST_EXPECT(!map.getLinkedNode(leafId.getChildNodeID(0)));
}
}
void
+15
View File
@@ -120,11 +120,26 @@ public:
tnCache_->reset();
}
bool
isNullBackend() const override
{
return nullBackend_;
}
void
setNullBackend(bool v)
{
nullBackend_ = v;
}
beast::manual_clock<std::chrono::steady_clock>
clock()
{
return clock_;
}
private:
bool nullBackend_ = false;
};
} // namespace tests
+11
View File
@@ -348,6 +348,17 @@ RCLConsensus::Adaptor::onClose(
make_shamapitem(tx.first->getTransactionID(), s.slice()));
}
// Keep enough validation history for the next flag ledger's score table
// even when we don't vote this round (not proposing / wrong LCL).
// Otherwise toKeep_ goes stale, validations older than
// validationSET_EXPIRES (10 min, < 256 ledgers) get swept, and we fail
// the next flag's score table too.
if (prevLedger->isVotingLedger())
{
auto const seq = prevLedger->info().seq + 1;
app_.getValidations().setSeqToKeep(seq - 1, seq + FLAG_LEDGER_INTERVAL);
}
// Add pseudo-transactions to the set
if (app_.config().standalone() || (proposing && !wrongLCL))
{
+6 -1
View File
@@ -15,6 +15,7 @@
#include <memory>
#include <optional>
#include <queue>
#include <set>
#include <utility>
#include <vector>
#include <wasmedge/wasmedge.h>
@@ -257,11 +258,15 @@ finalizeHookResult(
bool doEmit);
// write state map to ledger
// if `written` is supplied, every account whose hook state was successfully
// written is inserted into it (own and foreign namespaces alike); deleting an
// entry that does not exist is not a write
ripple::TER
finalizeHookState(
HookStateMap const&,
ripple::ApplyContext&,
ripple::uint256 const&);
ripple::uint256 const&,
std::set<ripple::AccountID>* written = nullptr);
// if the txn being executed was an emitted txn then this removes it from the
// emission directory
+18 -3
View File
@@ -69,7 +69,13 @@ Expected<bool, HookReturnCode>
HookAPI::sto_validate(Bytes const& data) const
{
if (data.size() < 2)
{
// fix20261005: a buffer this short can never hold a valid STObject,
// so report it as invalid (0) as documented, rather than TOO_SMALL
if (hookCtx.applyCtx.view().rules().enabled(fix20261005))
return false;
return Unexpected(TOO_SMALL);
}
unsigned char* start = const_cast<unsigned char*>(data.data());
unsigned char* upto = start;
@@ -1029,15 +1035,21 @@ HookAPI::float_set(int32_t exponent, int64_t mantissa) const
auto normalized = hook_float::normalize_xfl(mantissa, exponent);
// the above function will underflow into a canonical 0
// but this api must report that underflow
// but this api must report that underflow.
// The range check is on the exponent *after* the mantissa is normalized
// to 16 digits. fix20261005: report overflow / underflow as
// EXPONENT_OVERSIZED / EXPONENT_UNDERSIZED; before the fix both were
// reported as INVALID_FLOAT.
bool const fixExpCodes =
hookCtx.applyCtx.view().rules().enabled(fix20261005);
if (!normalized)
{
if (normalized.error() == XFL_OVERFLOW)
return Unexpected(INVALID_FLOAT);
return Unexpected(fixExpCodes ? EXPONENT_OVERSIZED : INVALID_FLOAT);
return Unexpected(normalized.error());
}
if (normalized.value() == 0)
return Unexpected(INVALID_FLOAT);
return Unexpected(fixExpCodes ? EXPONENT_UNDERSIZED : INVALID_FLOAT);
return normalized;
}
@@ -2145,6 +2157,9 @@ HookAPI::slot_set(Bytes const& data, uint32_t slot_no) const
}
else if (data.size() == 32)
{
if (hookCtx.applyCtx.view().rules().enabled(fix20261005))
return Unexpected(INVALID_ARGUMENT);
uint256 hash = uint256::fromVoid(data.data());
ripple::error_code_i ec{ripple::error_code_i::rpcUNKNOWN};
+25 -2
View File
@@ -1368,7 +1368,8 @@ ripple::TER
hook::finalizeHookState(
HookStateMap const& stateMap,
ripple::ApplyContext& applyCtx,
ripple::uint256 const& txnID)
ripple::uint256 const& txnID,
std::set<ripple::AccountID>* written)
{
auto const& j = applyCtx.app.journal("View");
uint16_t changeCount = 0;
@@ -1400,6 +1401,12 @@ hook::finalizeHookState(
// this entry isn't just cached, it was actually modified
auto slice = Slice(blob.data(), blob.size());
// deleting an entry that does not exist leaves the
// ledger unchanged, so it does not count as a write
bool const changesLedger = !written || !blob.empty() ||
applyCtx.view().exists(
ripple::keylet::hookState(acc, key, ns));
TER result = setHookState(applyCtx, acc, ns, key, slice);
if (!isTesSuccess(result))
@@ -1411,6 +1418,9 @@ hook::finalizeHookState(
return result;
}
// ^ should not fail... checks were done before map insert
if (written && changesLedger)
written->insert(acc);
}
}
}
@@ -1536,7 +1546,20 @@ hook::finalizeHookResult(
ptr->add(s);
SerialIter sit(s.slice());
sleEmitted->emplace_back(ripple::STObject(sit, sfEmittedTxn));
ripple::STObject emittedTxn(sit, sfEmittedTxn);
// ltEMITTED_TXN has an SOTemplate, so the new SLE already
// holds a non-present sfEmittedTxn slot. emplace_back()
// appends a second entry behind it, and lookups (which go
// through the template index) keep reporting the field as
// absent until the SLE is serialized and re-read. set()
// replaces the template slot. The serialized entry is the
// same either way, but a hook slotting the entry later in
// the same ledger can observe the difference, so gate it.
if (applyCtx.view().rules().enabled(fix20261005))
sleEmitted->set(std::move(emittedTxn));
else
sleEmitted->emplace_back(std::move(emittedTxn));
auto page = applyCtx.view().dirInsert(
keylet::emittedDir(), emittedId, [&](SLE::ref sle) {
(*sle)[sfFlags] = lsfEmittedDir;
+6
View File
@@ -155,6 +155,12 @@ LedgerHistory::getLedgerByHash(LedgerHash const& hash)
return ret;
}
std::shared_ptr<Ledger const>
LedgerHistory::getCachedLedger(LedgerHash const& hash)
{
return m_ledgers_by_hash.fetch(hash);
}
static void
log_one(
ReadView const& ledger,
+7
View File
@@ -63,6 +63,13 @@ public:
std::shared_ptr<Ledger const>
getLedgerByHash(LedgerHash const& ledgerHash);
/** Return a ledger already in the hash cache.
Does not load from the database or wire the SHAMap.
*/
std::shared_ptr<Ledger const>
getCachedLedger(LedgerHash const& ledgerHash);
/** Get a ledger's hash given its sequence number
@param ledgerIndex The sequence number of the desired ledger
@return The hash of the specified ledger
+33
View File
@@ -37,6 +37,8 @@
#include <xrpl/protocol/RippleLedgerHash.h>
#include <xrpl/protocol/STValidation.h>
#include <xrpl/protocol/messages.h>
#include <deque>
#include <functional>
#include <optional>
#include <mutex>
@@ -186,6 +188,21 @@ public:
std::shared_ptr<Ledger const>
getLedgerByHash(uint256 const& hash);
/** Return a ledger that is already resident in memory.
Looks in the ledger history cache, the closed and validated
ledgers and the null-mode retained window. Unlike getLedgerByHash
and getLedgerBySeq this never loads from SQL or the node store and
never edits the complete-ledger set, so it is safe to call with
values supplied by a peer.
*/
std::shared_ptr<Ledger const>
getResidentLedgerByHash(uint256 const& hash);
/** Sequence-number form of getResidentLedgerByHash. */
std::shared_ptr<Ledger const>
getResidentLedgerBySeq(std::uint32_t seq);
void
setLedgerRangePresent(
std::uint32_t minV,
@@ -323,6 +340,10 @@ private:
void
doAdvance(std::unique_lock<std::recursive_mutex>&);
// Closed, validated, then retained ledgers; never loads.
std::shared_ptr<Ledger const>
findResidentLedger(std::function<bool(LedgerInfo const&)> const& match);
std::vector<std::shared_ptr<Ledger const>>
findNewLedgersToPublish(std::unique_lock<std::recursive_mutex>&);
@@ -354,6 +375,14 @@ private:
// The last ledger we handled fetching history
std::shared_ptr<Ledger const> mHistLedger;
// Sliding window of the last ledger_history published ledgers, pinned
// so their SHAMaps stay reachable via shared_ptr. Only populated when
// [node_db] type=rwdb (null node store), where nothing else can bring
// a dropped tree back. Has its own lock so peer requests that consult
// it do not contend on m_mutex.
std::mutex mutable mRetainedLock;
std::deque<std::shared_ptr<Ledger const>> mRetainedLedgers;
// Fully validated ledger, whether or not we have the ledger resident.
std::pair<uint256, LedgerIndex> mLastValidLedger{uint256(), 0};
@@ -401,6 +430,10 @@ private:
// How much history do we want to keep
std::uint32_t const ledger_history_;
// [node_db] type=rwdb: the node store keeps nothing, so recent
// ledgers are retained in memory (see mRetainedLedgers).
bool const nullBackend_;
std::uint32_t const ledger_fetch_size_;
TaggedCache<uint256, Blob> fetch_packs_;
@@ -113,6 +113,7 @@ LedgerMaster::LedgerMaster(
, fetch_depth_(
app_.getSHAMapStore().clampFetchDepth(app_.config().FETCH_DEPTH))
, ledger_history_(app_.config().LEDGER_HISTORY)
, nullBackend_(app_.config().nullBackend())
, ledger_fetch_size_(app_.config().getValueFor(SizedItem::ledgerFetch))
, fetch_packs_(
"FetchPack",
@@ -937,6 +938,20 @@ LedgerMaster::setFullLedger(
mCompleteLedgers.insert(ledger->info().seq);
}
// Null node store only: pin a sliding window of recently validated
// current ledgers so their SHAMap state trees stay resident via
// shared_ptr. A persistent node store must not do this. It would pin
// ledger_history full trees for nothing, and ledger_history=full
// (uint32 max) would never trim. SHAMapStoreImp rejects both zero and
// "full" ledger_history for type=rwdb.
if (nullBackend_ && isCurrent)
{
std::lock_guard rl(mRetainedLock);
mRetainedLedgers.push_back(ledger);
while (mRetainedLedgers.size() > ledger_history_)
mRetainedLedgers.pop_front();
}
{
std::lock_guard ml(m_mutex);
@@ -1852,6 +1867,12 @@ LedgerMaster::getCloseTimeByHash(
LedgerHash const& ledgerHash,
std::uint32_t index)
{
// Resident ledgers first (getLedgerByHash would load a cache miss).
// Close time is a header field, so fall through to the node-store
// header when nothing is already in memory.
if (auto const ledger = getResidentLedgerByHash(ledgerHash))
return ledger->info().closeTime;
auto nodeObject = app_.getNodeStore().fetchNodeObject(ledgerHash, index);
if (nodeObject && (nodeObject->getData().size() >= 120))
{
@@ -1996,6 +2017,44 @@ LedgerMaster::getLedgerByHash(uint256 const& hash)
return {};
}
std::shared_ptr<Ledger const>
LedgerMaster::findResidentLedger(
std::function<bool(LedgerInfo const&)> const& match)
{
for (auto const& ledger : {mClosedLedger.get(), mValidLedger.get()})
{
if (ledger && match(ledger->info()))
return ledger;
}
std::lock_guard lock(mRetainedLock);
auto const it = std::find_if(
mRetainedLedgers.rbegin(),
mRetainedLedgers.rend(),
[&](auto const& ledger) { return match(ledger->info()); });
return it == mRetainedLedgers.rend() ? nullptr : *it;
}
std::shared_ptr<Ledger const>
LedgerMaster::getResidentLedgerByHash(uint256 const& hash)
{
if (auto ret = mLedgerHistory.getCachedLedger(hash))
return ret;
return findResidentLedger(
[&](LedgerInfo const& info) { return info.hash == hash; });
}
std::shared_ptr<Ledger const>
LedgerMaster::getResidentLedgerBySeq(std::uint32_t seq)
{
// getLedgerHash only reads the in-memory index; a zero hash misses.
if (auto ret =
mLedgerHistory.getCachedLedger(mLedgerHistory.getLedgerHash(seq)))
return ret;
return findResidentLedger(
[&](LedgerInfo const& info) { return info.seq == seq; });
}
void
LedgerMaster::setLedgerRangePresent(
std::uint32_t minV,
+5
View File
@@ -1682,6 +1682,11 @@ ApplicationImp::signalStop(std::string msg)
else
JLOG(m_journal.warn()) << "Server stopping: " << msg;
// run() checks isTimeToStop and blocks on stoppingCondition_ while
// holding stoppingMutex_. Notifying without the lock can land in the
// gap between that check and the wait and be lost, leaving run()
// blocked forever (seen as a hung Env::~AppBundle thread.join()).
std::lock_guard lk{stoppingMutex_};
stoppingCondition_.notify_all();
}
}
+17 -18
View File
@@ -170,26 +170,25 @@ LoadManager::run()
LogicError("Deadlock detected");
}
}
}
bool change;
bool change;
if (app_.getJobQueue().isOverloaded())
{
JLOG(journal_.info()) << "Raising local fee (JQ overload): "
<< app_.getJobQueue().getJson(0);
change = app_.getFeeTrack().raiseLocalFee();
}
else
{
change = app_.getFeeTrack().lowerLocalFee();
}
if (app_.getJobQueue().isOverloaded())
{
JLOG(journal_.info()) << "Raising local fee (JQ overload): "
<< app_.getJobQueue().getJson(0);
change = app_.getFeeTrack().raiseLocalFee();
}
else
{
change = app_.getFeeTrack().lowerLocalFee();
}
if (change)
{
// VFALCO TODO replace this with a Listener / observer and
// subscribe in NetworkOPs or Application.
app_.getOPs().reportFeeChange();
if (change)
{
// VFALCO TODO replace this with a Listener / observer and
// subscribe in NetworkOPs or Application.
app_.getOPs().reportFeeChange();
}
}
}
+63 -17
View File
@@ -58,9 +58,19 @@ NegativeUNLVote::doVoting(
unlNodeIDs.emplace(nid);
}
// Build a reliability score table of validators
if (std::optional<hash_map<NodeID, std::uint32_t>> scoreTable =
buildScoreTable(prevLedger, unlNodeIDs, validations))
// Build a reliability score table of validators. This only fails if
// there isn't enough ledger history; it does NOT apply the local
// reliability gate (that is N-UNL specific, see below).
auto const scoreTable =
buildRawScoreTable(prevLedger, unlNodeIDs, validations);
if (!scoreTable)
return;
auto const seq = prevLedger->info().seq + 1;
// N-UNL voting: only vote to disable/re-enable others if we ourselves
// have been reliable over the last flag ledger interval.
if (localNodeReliableForNUNL(*scoreTable, seq))
{
// build next negUnl
auto negUnlKeys = prevLedger->negativeUNL();
@@ -82,7 +92,6 @@ NegativeUNLVote::doVoting(
}
}
auto const seq = prevLedger->info().seq + 1;
purgeNewValidators(seq);
// Process the table and find all candidates to disable or to re-enable
@@ -109,14 +118,33 @@ NegativeUNLVote::doVoting(
"ripple::NegativeUNLVote::doVoting : found node to enable");
addTx(seq, nidToKeyMap.at(n), ToReEnable, initialSet);
}
}
// do reporting when enabled
if (prevLedger->rules().enabled(featureXahauGenesis) &&
scoreTable->size() > 0)
// UNLReport. Previously this sat behind the N-UNL local reliability gate
// (>= 90% of our own validations on-chain in the last 256 ledgers). Every
// ttUNL_REPORT is its own deterministic txn and is voted on individually
// in consensus, so one proposer's skewed view can't change the outcome;
// but gating the whole batch per-proposer made the reports all-or-nothing:
// whenever fewer than ~half the UNL cleared the 90% bar the entire report
// lost the dispute and the flag ledger carried no UNLReport at all.
// Use the same bar the report itself uses for "active".
if (prevLedger->rules().enabled(featureXahauGenesis) &&
!scoreTable->empty())
{
auto const it = scoreTable->find(myId_);
auto const myScore = it == scoreTable->end() ? 0u : it->second;
if (myScore > (FLAG_LEDGER_INTERVAL >> 1))
{
addReportingTx(seq, *scoreTable, nidToKeyMap, initialSet);
addImportVLTx(seq, initialSet);
}
else
{
JLOG(j_.debug())
<< "R-UNL: ledger " << seq << ". Local node only issued "
<< myScore << " validations in last " << FLAG_LEDGER_INTERVAL
<< " ledgers, not proposing UNLReport.";
}
}
}
@@ -277,7 +305,7 @@ NegativeUNLVote::choose(
}
std::optional<hash_map<NodeID, std::uint32_t>>
NegativeUNLVote::buildScoreTable(
NegativeUNLVote::buildRawScoreTable(
std::shared_ptr<Ledger const> const& prevLedger,
hash_set<NodeID> const& unl,
RCLValidations& validations)
@@ -327,6 +355,14 @@ NegativeUNLVote::buildScoreTable(
}
}
return scoreTable;
}
bool
NegativeUNLVote::localNodeReliableForNUNL(
hash_map<NodeID, std::uint32_t> const& scoreTable,
LedgerIndex seq) const
{
// Return false if the validation message history or local node's
// participation in the history is not good.
auto const myValidationCount = [&]() -> std::uint32_t {
@@ -341,23 +377,33 @@ NegativeUNLVote::buildScoreTable(
<< " validations in last " << FLAG_LEDGER_INTERVAL
<< " ledgers."
<< " The reliability measurement could be wrong.";
return {};
return false;
}
else if (
myValidationCount > negativeUNLMinLocalValsToVote &&
myValidationCount <= FLAG_LEDGER_INTERVAL)
{
return scoreTable;
}
else
if (myValidationCount > FLAG_LEDGER_INTERVAL)
{
// cannot happen because validations.getTrustedForLedger does not
// return multiple validations of the same ledger from a validator.
JLOG(j_.error()) << "N-UNL: ledger " << seq << ". Local node issued "
<< myValidationCount << " validations in last "
<< FLAG_LEDGER_INTERVAL << " ledgers. Too many!";
return {};
return false;
}
// NB: the old code used `>` here, so exactly 230 fell through to the
// "Too many!" branch.
return true;
}
std::optional<hash_map<NodeID, std::uint32_t>>
NegativeUNLVote::buildScoreTable(
std::shared_ptr<Ledger const> const& prevLedger,
hash_set<NodeID> const& unl,
RCLValidations& validations)
{
auto scoreTable = buildRawScoreTable(prevLedger, unl, validations);
if (scoreTable &&
!localNodeReliableForNUNL(*scoreTable, prevLedger->info().seq + 1))
return {};
return scoreTable;
}
NegativeUNLVote::Candidates const
+20
View File
@@ -192,6 +192,26 @@ private:
NodeID
choose(uint256 const& randomPadData, std::vector<NodeID> const& candidates);
/**
* Count trusted full validations per UNL validator over the last
* FLAG_LEDGER_INTERVAL ledgers. Fails only on insufficient ledger
* history; does not apply the local reliability gate.
*/
std::optional<hash_map<NodeID, std::uint32_t>>
buildRawScoreTable(
std::shared_ptr<Ledger const> const& prevLedger,
hash_set<NodeID> const& unl,
RCLValidations& validations);
/**
* True if the local node issued enough validations in the score table
* to be trusted to vote on N-UNL changes.
*/
bool
localNodeReliableForNUNL(
hash_map<NodeID, std::uint32_t> const& scoreTable,
LedgerIndex seq) const;
/**
* Build a reliability measurement score table of validators' validation
* messages in the last flag ledger period.
+68 -14
View File
@@ -704,7 +704,10 @@ private:
std::array<SubMapType, SubTypes::sLastEntry> mStreamMaps;
ServerFeeSummary mLastFeeSummary;
ServerFeeSummary mLastFeeSummary; ///< Guarded by mFeeSummaryMutex_.
std::mutex mFeeSummaryMutex_; ///< Guards mLastFeeSummary only. Kept
///< separate from mSubLock to avoid
///< lock-ordering hazards with masterMutex.
JobQueue& m_job_queue;
@@ -1136,6 +1139,16 @@ NetworkOPsImp::submitTransaction(std::shared_ptr<STTx const> const& iTrans)
try
{
// Not a bad signature: this server may not have reached the fee
// setting the transaction was built for. Drop without marking it bad.
if (auto const [fee, feeReason] = checkManifestIngressFee(app_, *trans);
fee == ManifestIngressFee::Refused)
{
JLOG(m_journal.debug())
<< "Submitted manifest-authorized tx dropped: " << feeReason;
return;
}
auto const [validity, reason] = checkValidity(
app_.getHashRouter(),
*trans,
@@ -1273,6 +1286,19 @@ NetworkOPsImp::processTransaction(
// NOTE eahennis - I think this check is redundant,
// but I'm not 100% sure yet.
// If so, only cost is looking up HashRouter flags.
// Before checkValidity(), which verifies the manifest: a fee miss is local
// and must not mark the transaction bad.
if (auto const [fee, feeReason] =
checkManifestIngressFee(app_, *transaction->getSTransaction());
fee == ManifestIngressFee::Refused)
{
JLOG(m_journal.debug())
<< "Manifest-authorized tx dropped: " << feeReason;
transaction->setStatus(INVALID);
transaction->setResult(telMANIFEST_FEE_MISMATCH);
return;
}
auto const [validity, reason] = checkValidity(
app_.getHashRouter(),
*transaction->getSTransaction(),
@@ -2325,7 +2351,10 @@ NetworkOPsImp::pubServer()
else
jvObj[jss::load_factor] = f.loadFactorServer;
mLastFeeSummary = f;
{
std::lock_guard fsl(mFeeSummaryMutex_);
mLastFeeSummary = f;
}
for (auto i = mStreamMaps[sServer].begin();
i != mStreamMaps[sServer].end();)
@@ -3210,14 +3239,23 @@ NetworkOPsImp::reportFeeChange()
app_.getTxQ().getMetrics(*app_.openLedger().current()),
app_.getFeeTrack()};
// only schedule the job if something has changed
if (f != mLastFeeSummary)
{
m_job_queue.addJob(
jtCLIENT_FEE_CHANGE, "reportFeeChange->pubServer", [this]() {
pubServer();
});
}
// Guard mLastFeeSummary under mFeeSummaryMutex_ to prevent concurrent
// threads from simultaneously passing the check and queuing duplicate
// jtCLIENT_FEE_CHANGE jobs (data race fix).
// Also fixes the no-subscriber case where mLastFeeSummary was
// never updated by pubServer(), causing endless job queuing.
// mFeeSummaryMutex_ is used instead of mSubLock to avoid a
// lock-ordering hazard: reportFeeChange() is called with masterMutex
// held, and pubServer() holds mSubLock across the subscriber fan-out.
if (std::lock_guard sl(mFeeSummaryMutex_); f != mLastFeeSummary)
mLastFeeSummary = f;
else
return;
m_job_queue.addJob(
jtCLIENT_FEE_CHANGE, "reportFeeChange->pubServer", [this]() {
pubServer();
});
}
void
@@ -4267,10 +4305,26 @@ NetworkOPsImp::subServer(
jvResult[jss::pubkey_node] =
toBase58(TokenType::NodePublic, app_.nodeIdentity().first);
std::lock_guard sl(mSubLock);
return mStreamMaps[sServer]
.emplace(isrListener->getSeq(), isrListener)
.second;
bool added;
bool isFirstSubscriber = false;
{
std::lock_guard sl(mSubLock);
added = mStreamMaps[sServer]
.emplace(isrListener->getSeq(), isrListener)
.second;
isFirstSubscriber = added && mStreamMaps[sServer].size() == 1;
}
if (isFirstSubscriber)
{
// First subscriber on an otherwise-quiet node: reset mLastFeeSummary
// so the next reportFeeChange() tick publishes a full serverStatus
// with base_fee and load_factor_* fields. Skip if a PubFee job is
// already queued — it will publish to the new subscriber anyway,
// and resetting here would cause a duplicate notification.
std::lock_guard fsl(mFeeSummaryMutex_);
mLastFeeSummary = {};
}
return added;
}
// <-- bool: true=erased, false=was not there
+69 -5
View File
@@ -134,7 +134,39 @@ SHAMapStoreImp::SHAMapStoreImp(
section.set("filter_bits", "10");
}
get_if_exists(section, "online_delete", deleteInterval_);
// An explicit online_delete=0 disables cleanup. Only a missing
// key is defaulted below. get_if_exists cannot tell those apart.
bool const onlineDeleteConfigured = section.exists("online_delete");
if (onlineDeleteConfigured)
get_if_exists(section, "online_delete", deleteInterval_);
isNullBackend_ = config.nullBackend();
if (isNullBackend_)
{
// Ledgers stay resident through LedgerMaster's retained window,
// which is ledger_history long. Zero keeps nothing alive, and
// "full" (uint32 max) never trims, so memory grows without bound.
if (config.LEDGER_HISTORY == 0 ||
config.LEDGER_HISTORY == std::numeric_limits<std::uint32_t>::max())
{
Throw<std::runtime_error>(
"RWDB null mode requires a finite ledger_history > 0");
}
JLOG(journal_.info())
<< "RWDB null mode: node store is ephemeral, " << "retaining "
<< config.LEDGER_HISTORY << " ledgers in memory";
}
// For RWDB, default online_delete to ledger_history only when the
// key is absent. Clamp to the minimum so an implicit value never
// triggers the "online_delete must be at least …" throw.
if (isNullBackend_ && !onlineDeleteConfigured)
{
auto const minInterval = config.standalone()
? minimumDeletionIntervalSA_
: minimumDeletionInterval_;
deleteInterval_ = std::max(config.LEDGER_HISTORY, minInterval);
}
// Always initialize state database for pinned ranges persistence
state_db_.init(config, dbName_);
@@ -175,7 +207,7 @@ SHAMapStoreImp::SHAMapStoreImp(
std::to_string(config.LEDGER_HISTORY) + ")");
}
if (!config.mem_backend())
if (!isNullBackend_)
dbPaths();
}
}
@@ -200,7 +232,25 @@ SHAMapStoreImp::makeNodeStore(int readThreads)
std::unique_ptr<NodeStore::Database> db;
if (deleteInterval_)
if (isNullBackend_)
{
// Tree-only: use the existing NullFactory (type=none). No
// DatabaseRotatingImp, no rotation thread, no object cache.
// A cached header would look like a full acquire and cannot
// be rebuilt from this backend.
nscfg.set("type", "none");
nscfg.set("cache_size", "0");
nscfg.set("cache_age", "0");
db = NodeStore::Manager::instance().make_Database(
megabytes(
app_.config().getValueFor(SizedItem::burstSize, std::nullopt)),
scheduler_,
readThreads,
nscfg,
app_.logs().journal(nodeStoreName_));
fdRequired_ += db->fdRequired();
}
else if (deleteInterval_)
{
SavedState state = state_db_.getState();
@@ -421,8 +471,10 @@ SHAMapStoreImp::run()
state_db_.setLastRotated(lastRotated);
}
bool const readyToRotate =
validatedSeq >= lastRotated + deleteInterval_ &&
// Widen the sum: an online_delete near uint32 max would wrap a
// 32-bit add and make this true on every ledger.
bool const readyToRotate = std::uint64_t{validatedSeq} >=
std::uint64_t{lastRotated} + deleteInterval_ &&
canDelete_ >= lastRotated - 1 && healthWait() == keepGoing;
// will delete up to (not including) lastRotated
@@ -439,6 +491,18 @@ SHAMapStoreImp::run()
if (healthWait() == stopping)
return;
if (isNullBackend_)
{
// Null node store: nothing to copy or rotate. Resident
// trees are kept alive by LedgerMaster's retained window,
// so only the SQL cleanup above applies.
lastRotated = validatedSeq;
state_db_.setLastRotated(lastRotated);
JLOG(journal_.info())
<< "finished null-mode cleanup " << validatedSeq;
continue;
}
JLOG(journal_.debug()) << "copying ledger " << validatedSeq;
std::uint64_t nodeCount = 0;
+1
View File
@@ -107,6 +107,7 @@ private:
std::uint32_t deleteInterval_ = 0;
bool advisoryDelete_ = false;
bool isNullBackend_ = false;
std::uint32_t deleteBatch_ = 100;
std::chrono::milliseconds backOff_{100};
std::chrono::seconds ageThreshold_{60};
+3 -5
View File
@@ -1658,11 +1658,9 @@ TxQ::accept(Application& app, OpenView& view)
[seq, txnHash, emitDetails](auto& obj) {
obj[sfLedgerSequence] = seq;
obj[sfTransactionHash] = txnHash;
obj.emplace_back(emitDetails);
/*std::unique_ptr<STBase> ed =
std::make_unique<STBase>(emitDetails);
ed->setFName(sfEmitDetails);
obj.set(std::move(ed));*/
// replace the template slot rather than
// appending a duplicate (bytes unchanged)
obj.set(STObject(emitDetails));
});
uint256 txID = efTx.getTransactionID();
+36
View File
@@ -68,6 +68,42 @@ checkValidity(
Config const& config,
ApplyFlags const flags = tapNONE);
/** Ingress fee outcome for a manifest-authorized SetManifest.
@see checkManifestIngressFee
*/
enum class ManifestIngressFee {
/// Not a manifest-authorized SetManifest, or the amendment is not
/// enabled; checkValidity applies as usual.
NotApplicable,
/// The Fee is canonical for the ledger this server would apply it to.
Canonical,
/// A canonically shaped envelope whose Fee cannot be confirmed here. Drop
/// without verifying the manifest. Not a bad signature: the same bytes
/// may be canonical on another node, or here once this node reaches the
/// fee setting they were built for.
Refused
};
/** Checks an unsigned SetManifest's Fee before any manifest signature work.
The canonical Fee follows the voted base fee of the parent of the ledger
a transaction is applied to. Ingress accepts exactly the value for this
server's open ledger, priced from its parent: the same value preclaim
enforces, so nothing that passes here is refused there for its Fee. A
server with no validated ledger, or one that is amendment blocked, has no
trustworthy base and refuses.
Every ingress caller runs this before checkValidity(), which no longer
checks the Fee because the transactor engine calls it without a ledger.
A Refused result must be dropped without marking the transaction bad or
charging for an invalid signature.
@return the outcome and, if Refused, a reason
*/
std::pair<ManifestIngressFee, std::string>
checkManifestIngressFee(Application& app, STTx const& tx);
/** Sets the validity of a given transaction in the cache.
@warning Use with extreme care.
+155 -76
View File
@@ -26,14 +26,126 @@
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Quality.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/XRPAmount.h> // mulRatio
#include <xrpl/protocol/XRPAmount.h>
#include <xrpl/protocol/serialize.h>
#include <xrpl/protocol/st.h>
#include <limits>
namespace ripple {
bool
hasManifestAuthorityMarkers(STTx const& tx) noexcept
{
try
{
return tx.getTxnType() == ttMANIFEST_SET &&
tx.isFieldPresent(sfSigningPubKey) &&
tx.getSigningPubKey().empty() &&
tx.isFieldPresent(sfTxnSignature) && tx.getSignature().empty();
}
catch (std::exception const&)
{
return false;
}
}
/** Build the only permitted outer transaction for `manifest`. */
static STTx
canonicalSetManifest(
STObject const& manifest,
AccountID const& account,
std::optional<std::uint32_t> networkID,
XRPAmount fee)
{
return STTx(ttMANIFEST_SET, [&](STObject& obj) {
obj.setAccountID(sfAccount, account);
obj.setFieldU32(sfSequence, 0);
if (networkID)
obj.setFieldU32(sfNetworkID, *networkID);
obj.setFieldAmount(sfFee, fee);
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setFieldVL(sfTxnSignature, Blob{});
obj.peekFieldObject(sfManifest) = manifest;
});
}
/** The NetworkID a canonical envelope carries for this server's network.
Networks above 1024 require the field and legacy networks forbid it
(preflight0), so the canonical envelope follows the same rule.
*/
static std::optional<std::uint32_t>
canonicalNetworkID(std::uint32_t networkID)
{
if (networkID > 1024)
return networkID;
return std::nullopt;
}
bool
hasCanonicalSetManifestShape(STTx const& tx, std::uint32_t networkID) noexcept
{
try
{
auto const& manifest =
const_cast<STTx&>(tx).getField(sfManifest).downcast<STObject>();
auto const masterKey = manifest.getFieldVL(sfPublicKey);
if (!publicKeyType(makeSlice(masterKey)))
return false;
auto const canonical = canonicalSetManifest(
manifest,
calcAccountID(PublicKey(makeSlice(masterKey))),
canonicalNetworkID(networkID),
tx[sfFee].xrp());
auto const actualBytes = tx.getSerializer();
auto const canonicalBytes = canonical.getSerializer();
return actualBytes.slice() == canonicalBytes.slice();
}
catch (std::exception const&)
{
return false;
}
}
std::optional<XRPAmount>
canonicalSetManifestFee(XRPAmount base, STObject const& manifest)
{
// Every relayer must compute the same Fee, so it depends only on the voted
// reference fee (ledger state, never local load) and the manifest's size.
// Only the multipliers are fixed; a fee vote changes the canonical txid
// once.
constexpr std::int64_t baseUnits = 100;
constexpr std::int64_t unitsPerByte = 10;
constexpr auto maxDrops = std::numeric_limits<std::int64_t>::max();
// Largest size for which baseUnits + bytes * unitsPerByte cannot overflow.
constexpr std::uint64_t maxBytes = (maxDrops - baseUnits) / unitsPerByte;
auto const bytes = manifest.getSerializer().getDataLength();
// A non-positive base would make the transaction free.
if (base <= beast::zero || bytes == 0 || bytes > maxBytes)
return std::nullopt;
auto const units =
baseUnits + static_cast<std::int64_t>(bytes) * unitsPerByte;
// base is positive, so this is an exact test for units * base overflowing.
if (units > maxDrops / base.drops())
return std::nullopt;
XRPAmount const fee{base.drops() * units};
if (!isLegalAmount(fee) || fee <= beast::zero)
return std::nullopt;
return fee;
}
TxConsequences
SetManifest::makeTxConsequences(PreflightContext const& ctx)
{
@@ -58,6 +170,19 @@ SetManifest::preflight(PreflightContext const& ctx)
return temINVALID_FLAG;
}
// The envelope carries no account signature: authority comes solely from
// the manifest's own master and ephemeral signatures, which do not cover
// it. So the whole envelope is one exact transaction per manifest, with
// Account derived from the manifest's master key and NetworkID from this
// server, and only the Fee mirrored here (checkFee() pins it). Checked
// before any signature work, so a relayer cannot vary a field to mint
// fresh txids that each cost manifest signature checks.
if (!hasCanonicalSetManifestShape(tx, ctx.app.config().NETWORK_ID))
{
JLOG(j.warn()) << "SetManifest: non-canonical envelope.";
return temMALFORMED;
}
// rules:
// 1. sfManifest must match the manifest template and be validly signed
// 2. the signingpubkey must match the master r-address
@@ -95,23 +220,6 @@ SetManifest::preflight(PreflightContext const& ctx)
// 3. not already revoked will be checked in preclaim because it depends on
// lgr state
// 4. the envelope carries no account signature: authority comes solely
// from the manifest's own master/ephemeral signatures, which do not cover
// the envelope. Pin every envelope field a relayer could otherwise choose.
// The shape below must match the one checkValidity() recognises, or the
// txn falls through to the ordinary signature path and is rejected there.
// sfFee cannot be bounded here because the computed base fee is not in
// scope until preclaim; checkFee() bounds it instead.
if (!tx.isFieldPresent(sfSigningPubKey) || !tx.getSigningPubKey().empty() ||
!tx.isFieldPresent(sfTxnSignature) || !tx.getSignature().empty() ||
tx.isFieldPresent(sfSigners) || tx.isFieldPresent(sfAccountTxnID) ||
tx.isFieldPresent(sfTicketSequence) || tx.getFieldU32(sfSequence) != 0)
{
JLOG(j.warn())
<< "SetManifest: envelope must be unsigned with Sequence 0.";
return temMALFORMED;
}
return preflight2(ctx);
}
@@ -137,9 +245,9 @@ SetManifest::preclaim(PreclaimContext const& ctx)
return tefINTERNAL; // preflight already parsed this successfully
// Replay protection. A byte-identical resubmission is rejected as
// tefALREADY by checkPriorTxAndLastLedger, but sfFee may vary within the
// band checkFee() allows, so the same manifest can also arrive under a
// different txid. The strictly-increasing sequence test below is what
// tefALREADY by checkPriorTxAndLastLedger, but the canonical Fee follows
// the voted base fee, so the same manifest can arrive under a different
// txid after a fee change. The strictly-increasing sequence test below
// covers that, both within this ledger and in every later one. Either
// result is tef, so a replay is never included and never claims a fee.
if (sle->isFieldPresent(sfManifestID))
@@ -312,33 +420,26 @@ SetManifest::calculateBaseFee(ReadView const& view, STTx const& tx)
return Transactor::calculateBaseFee(view, tx) + manifestFee;
}
/** The most sfFee may be: the same 1.2x headroom Submit applies.
Kept in one place so the value Submit writes and the value preclaim will
accept cannot drift apart.
*/
static XRPAmount
manifestFeeCeiling(XRPAmount baseFee)
{
return mulRatio(baseFee, 12, 10, /*roundUp*/ true);
}
TER
SetManifest::checkFee(PreclaimContext const& ctx, XRPAmount baseFee)
{
// A ceiling is required because the envelope carries no account signature,
// so sfFee is chosen by whoever relays the txn -- and manifests are public:
// they are gossiped over the peer protocol and embedded in published UNLs,
// so the relayer need not be the master key holder. Uncapped, any observer
// of a not-yet-recorded manifest could wrap it with sfFee set to that
// validator's entire balance. The 20% band is headroom against a fee floor
// that has risen since the txn was built, and bounds what an attacker can
// burn to the same 20%.
if (ctx.tx[sfFee].xrp() > manifestFeeCeiling(baseFee))
// The envelope carries no account signature, so a relayer-chosen Fee would
// be both a malleable field and a way to charge the master account. The
// canonical Fee is priced from this view's voted base fee, which is the
// parent ledger's: one exact value per fee setting. A mismatch is local,
// not final: the same bytes are canonical on a server that has reached
// the fee setting they were built for. The check also runs on a closed
// ledger, so a proposer cannot charge the master account an arbitrary Fee
// by skipping it.
STObject const& manifest =
const_cast<STTx&>(ctx.tx).getField(sfManifest).downcast<STObject>();
auto const canonical =
canonicalSetManifestFee(ctx.view.fees().base, manifest);
if (!canonical || ctx.tx[sfFee].xrp() != *canonical)
{
JLOG(ctx.j.trace()) << "SetManifest: fee above ceiling: "
JLOG(ctx.j.trace()) << "SetManifest: non-canonical fee: "
<< to_string(ctx.tx[sfFee].xrp());
return temBAD_FEE;
return telMANIFEST_FEE_MISMATCH;
}
// Floor and balance are the ordinary rules.
@@ -358,41 +459,19 @@ makeSetManifestTx(
if (!man || !man->verify())
return std::nullopt;
auto const encode = [&](XRPAmount fee) {
return serializeHex(STTx(ttMANIFEST_SET, [&](STObject& obj) {
obj.setAccountID(sfAccount, calcAccountID(man->masterKey));
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfNetworkID, networkID);
obj.setFieldAmount(sfFee, fee);
obj.setFieldVL(sfSigningPubKey, std::vector<std::uint8_t>{});
obj.setFieldVL(sfTxnSignature, std::vector<std::uint8_t>{});
SerialIter manifestIter{manifest};
STObject const manifestObject{manifestIter, sfManifest};
// sfManifest is soeREQUIRED, so STObject::set(SOTemplate) has
// already materialised it as a present, empty object. Fill
// that one in: emitting a second is a duplicate field, which
// STObject::set(SerialIter&) rejects on the way back in.
SerialIter mit{manifest};
obj.peekFieldObject(sfManifest).set(mit);
}));
};
// calculateBaseFee() takes a parsed transaction, so encode once with a
// placeholder fee purely to have something to price. The resulting fee
// does not depend on the placeholder: it is derived from the length of
// the manifest object and the ledger's base fee.
auto const priced = strUnHex(encode(XRPAmount{0}));
if (!priced || priced->empty())
auto const fee =
canonicalSetManifestFee(openView.fees().base, manifestObject);
if (!fee)
return std::nullopt;
SerialIter sit{makeSlice(*priced)};
STTx const probe{std::ref(sit)};
// Submit the ceiling exactly. preclaim rejects anything above it, and
// the floor rises with network load, so the ceiling is both always
// acceptable and the value most likely to still clear the floor by the
// time the transaction is applied.
return encode(
manifestFeeCeiling(SetManifest::calculateBaseFee(openView, probe)));
return serializeHex(canonicalSetManifest(
manifestObject,
calcAccountID(man->masterKey),
canonicalNetworkID(networkID),
*fee));
}
catch (std::exception const& e)
{
+38 -13
View File
@@ -25,29 +25,54 @@
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Indexes.h>
#include <optional>
namespace ripple {
/** Return whether empty outer signatures nominate manifest-only authority. */
bool
hasManifestAuthorityMarkers(STTx const& tx) noexcept;
/** Return whether every outer byte has the canonical SetManifest shape.
Account is derived from the manifest's master key and NetworkID from
`networkID`, this server's network. Only the Fee is mirrored from the
candidate; checkFee pins it against the applying view.
*/
bool
hasCanonicalSetManifestShape(STTx const& tx, std::uint32_t networkID) noexcept;
/** Return the one canonical Fee for a SetManifest carrying `manifest`.
The Fee is `base * (100 + 10 * manifestBytes)`, where `base` is the voted
reference fee of the ledger the transaction is checked against, which is
its parent's. Every node reading the same ledger computes the same value,
so one manifest maps to one transaction ID per fee setting; local load
never enters it.
Returns nullopt when there is no canonical Fee: a zero base, or a product
that overflows or is not a legal amount.
*/
std::optional<XRPAmount>
canonicalSetManifestFee(XRPAmount base, STObject const& manifest);
/** Encode the transaction that publishes `manifest` on-ledger.
A manifest transaction carries no account signature, so the protocol pins
the whole envelope: Sequence must be 0, SigningPubKey and TxnSignature must
be empty, and Fee must fall between the computed base fee and a ceiling
above it. SetManifest::preflight and SetManifest::checkFee reject anything
else. Every caller that submits a manifest builds it here so those rules
cannot drift apart from the ones the transactor enforces.
Returns hex rather than an STTx because the manifest is appended to the
encoded transaction verbatim, behind its object marker, instead of being
parsed and re-emitted: the bytes the master key signed survive untouched,
and a future change to the manifest format needs no change here.
A manifest transaction carries no account signature, so the whole
envelope is one exact transaction: Account is the manifest master key's
account, Sequence is 0, SigningPubKey and TxnSignature are empty, NetworkID
is this network's, and Fee is the canonical Fee for `openView`'s base fee.
No other field is allowed. SetManifest::preflight and SetManifest::checkFee
reject anything else, and every caller that submits a manifest builds it
here.
@param manifest Serialized manifest
@param networkID Network the transaction is for
@param openView Ledger the fee is priced against
@param openView Open ledger the Fee is priced against
@param j Journal
@return the hex-encoded transaction, or nullopt if the manifest does not
parse or does not verify
parse or does not verify, or there is no canonical Fee
*/
std::optional<std::string>
makeSetManifestTx(
+63 -6
View File
@@ -1198,6 +1198,46 @@ touchAccount(ApplyView& view, AccountID const& id)
view.update(sle);
}
void
Transactor::touchOnce(AccountID const& id)
{
auto& view = ctx_.view();
// before fix20261005 every request touches, and touched_ stays empty
if (!view.rules().enabled(fix20261005))
{
touchAccount(view, id);
return;
}
if (touched_.insert(id).second)
touchAccount(view, id);
}
void
Transactor::touchHookStateOwners(std::set<AccountID> const& written)
{
if (!view().rules().enabled(fix20261005))
return;
// A hook may write into another account's namespace (state_foreign_set
// under a HookGrant) without that account being a transactional
// stakeholder. Touch it so the transaction threads to its AccountRoot.
for (auto const& acc : written)
touchOnce(acc);
}
void
Transactor::discardView()
{
ctx_.discard();
// The touches touched_ records were discarded with the view. touched_ is
// only populated under fix20261005, so before the fix this is a no-op and
// needs no amendment guard.
touched_.clear();
}
static void
removeUnfundedOffers(
ApplyView& view,
@@ -1284,7 +1324,7 @@ Transactor::reset(XRPAmount fee)
std::vector<STObject> executions;
std::vector<STObject> emissions;
avi.copyHookMetaData(executions, emissions);
ctx_.discard();
discardView();
ApplyViewImpl& avi2 = dynamic_cast<ApplyViewImpl&>(ctx_.view());
avi2.setHookMetaData(std::move(executions), std::move(emissions));
@@ -1595,8 +1635,12 @@ Transactor::doHookCallback(
// write any state changes if cbak resulted in accept()
if (success)
{
std::set<AccountID> written;
hook::finalizeHookState(
stateMap, ctx_, ctx_.tx.getTransactionID());
stateMap, ctx_, ctx_.tx.getTransactionID(), &written);
touchHookStateOwners(written);
}
// write the final result
ripple::TER result =
@@ -1707,7 +1751,10 @@ Transactor::doTSH(
if ((!canRollback && strong) || (canRollback && !strong))
continue;
touchAccount(view, tshAccountID);
// under fix20261005 an account is touched at most once per
// transaction (a foreign state write in the strong pass or a callback
// may already have touched an account that is a weak TSH)
touchOnce(tshAccountID);
if (view.rules().enabled(fixHookAPI20251128))
{
@@ -2017,7 +2064,12 @@ Transactor::operator()()
// write state if all chains executed successfully
if (isTesSuccess(result))
hook::finalizeHookState(stateMap, ctx_, ctx_.tx.getTransactionID());
{
std::set<AccountID> written;
hook::finalizeHookState(
stateMap, ctx_, ctx_.tx.getTransactionID(), &written);
touchHookStateOwners(written);
}
// write hook results
// this happens irrespective of whether final result was a tesSUCCESS
@@ -2071,7 +2123,7 @@ Transactor::finishApply(
{
// If the tapFAIL_HARD flag is set, a tec result
// must not do anything
ctx_.discard();
discardView();
applied = false;
}
else if (
@@ -2407,7 +2459,12 @@ Transactor::finishApply(
doAgainAsWeak(accID, hookHashes, stateMap, weakResults, proMeta);
// write hook results
hook::finalizeHookState(stateMap, ctx_, ctx_.tx.getTransactionID());
{
std::set<AccountID> written;
hook::finalizeHookState(
stateMap, ctx_, ctx_.tx.getTransactionID(), &written);
touchHookStateOwners(written);
}
for (auto& weakResult : weakResults)
hook::finalizeHookResult(weakResult, ctx_, isTesSuccess(result));
+19
View File
@@ -218,6 +218,21 @@ protected:
void
addWeakTSHFromBalanceChanges(detail::ApplyViewBase const& pv);
// Touch an account (featureTouch). Under fix20261005 each account is
// touched at most once per transaction.
void
touchOnce(AccountID const& id);
// fix20261005: touch every account whose hook state finalizeHookState
// wrote, so the transaction threads to the AccountRoot of each
void
touchHookStateOwners(std::set<AccountID> const& written);
// Discard every change made to the view, together with the record of
// which accounts were touched in it.
void
discardView();
// hooks amendment fields, these are unpopulated and unused unless
// featureHooks is enabled
int executedHookCount_ =
@@ -227,6 +242,10 @@ protected:
// end of the transactor, who isn't able to be
// deduced until after apply i.e. pathing
// participants, crossed offers
std::set<AccountID>
touched_; // fix20261005: accounts touched (featureTouch) in the
// current view. Never populated before the fix, emptied
// whenever the view is discarded.
// The post-apply pipeline of operator(): tec handling (reset),
// invariants, balance rewards, weak hooks and commit.
+67 -5
View File
@@ -17,10 +17,15 @@
*/
//==============================================================================
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/main/Application.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/applySteps.h>
#include <xrpld/app/tx/detail/SetManifest.h>
#include <xrpl/basics/Log.h>
#include <xrpl/protocol/Feature.h>
@@ -75,12 +80,17 @@ checkValidity(
}
if (rules.enabled(featureOnChainManifests) &&
tx.getTxnType() == ttMANIFEST_SET &&
tx.isFieldPresent(sfTxnSignature) &&
tx.getFieldVL(sfTxnSignature).empty() &&
tx.isFieldPresent(sfSigningPubKey) &&
tx.getFieldVL(sfSigningPubKey).empty() && tx.isFieldPresent(sfManifest))
hasManifestAuthorityMarkers(tx) && tx.isFieldPresent(sfManifest))
{
// Structural nonsense must not buy manifest signature checks. The Fee
// depends on ledger fee state, which this function does not have;
// ingress callers pin it first with checkManifestIngressFee(), and the
// transactor pins it against the applying view in checkFee().
if (!hasCanonicalSetManifestShape(tx, config.NETWORK_ID))
return {
Validity::SigBad,
"Manifest-authorized envelope is not canonical"};
// perform alternative signature check over manifest
STObject const& manObj = const_cast<ripple::STTx&>(tx)
.getField(sfManifest)
@@ -221,4 +231,56 @@ applyTransaction(
}
}
std::pair<ManifestIngressFee, std::string>
checkManifestIngressFee(Application& app, STTx const& tx)
{
auto const open = app.openLedger().current();
if (!open->rules().enabled(featureOnChainManifests) ||
!hasManifestAuthorityMarkers(tx))
return {ManifestIngressFee::NotApplicable, {}};
// A non-canonical shape can never become valid, so it is not a fee miss:
// leave it to checkValidity(), which rejects it as a bad envelope.
if (!hasCanonicalSetManifestShape(tx, app.config().NETWORK_ID))
return {ManifestIngressFee::NotApplicable, {}};
// An amendment-blocked node's open ledger may sit on history the network
// is not validating, and with no validated ledger there is no agreed base
// at all. Neither is a reason to spend manifest crypto finding out.
if (app.getOPs().isAmendmentBlocked())
return {ManifestIngressFee::Refused, "server is amendment blocked"};
auto const validated = app.getLedgerMaster().getValidatedLedger();
if (!validated)
return {ManifestIngressFee::Refused, "no validated ledger"};
try
{
auto const fee = tx[sfFee].xrp();
auto const& manifest =
const_cast<STTx&>(tx).getField(sfManifest).downcast<STObject>();
// A transaction is priced from the parent of the ledger it is applied
// to. Here that is the open ledger, whose fees() are its parent's:
// exactly what preclaim enforces. A wrapper priced for any other fee
// setting cannot apply on this server, so it is not worth verifying,
// and no remembered setting widens the set of txids that reach
// manifest signature checks.
if (auto const canonical =
canonicalSetManifestFee(open->fees().base, manifest);
canonical && *canonical == fee)
return {ManifestIngressFee::Canonical, {}};
}
catch (std::exception const&)
{
// Malformed Fee or manifest: leave it to checkValidity()'s shape
// check, which rejects it as a bad envelope.
return {ManifestIngressFee::NotApplicable, {}};
}
return {
ManifestIngressFee::Refused,
"Fee is not the canonical SetManifest fee for this server's open "
"ledger"};
}
} // namespace ripple
+15
View File
@@ -375,6 +375,21 @@ public:
return isMem;
}
/** True when [node_db] type=rwdb.
The app node store is the existing NullFactory (type=none).
Nodes stay alive through Ledger and SHAMap pointers.
The result is read from this configuration, so two
applications in one process do not share it.
*/
bool
nullBackend() const
{
auto const& node = section(ConfigSection::nodeDatabase());
return !node.empty() &&
boost::beast::iequals(get(node, "type"), "rwdb");
}
bool
useTxTables() const
{
+11 -19
View File
@@ -118,7 +118,7 @@ sizedItems
{SizedItem::txnDBCache, {{ 4, 12, 24, 64, 128 }}},
{SizedItem::lgrDBCache, {{ 4, 8, 16, 32, 128 }}},
{SizedItem::openFinalLimit, {{ 8, 16, 32, 64, 128 }}},
{SizedItem::burstSize, {{ 4, 8, 16, 32, 64*1024*1024 }}},
{SizedItem::burstSize, {{ 4, 8, 16, 32, 64 }}},
{SizedItem::ramSizeGB, {{ 8, 12, 16, 24, 32 }}},
{SizedItem::accountIdCacheSize, {{ 20047, 50053, 77081, 150061, 300007 }}}
}};
@@ -1060,24 +1060,6 @@ Config::loadFromString(std::string const& fileContents)
"the maximum number of allowed peers (peers_max)");
}
}
if (!RUN_STANDALONE)
{
auto db_section = section(ConfigSection::nodeDatabase());
if (auto type = get(db_section, "type", ""); type == "rwdb")
{
if (auto delete_interval = get(db_section, "online_delete", 0);
delete_interval == 0)
{
Throw<std::runtime_error>(
"RWDB (in-memory backend) requires online_delete to "
"prevent OOM "
"Exception: standalone mode (used by tests) doesn't need "
"online_delete");
}
}
}
// DatabasePinned validation (applies to all modes including standalone)
auto db_section = section(ConfigSection::nodeDatabase());
if (db_section.exists("pinned_type"))
@@ -1089,6 +1071,16 @@ Config::loadFromString(std::string const& fileContents)
"type must be specified when using pinned_type");
}
// type=rwdb is the tree-only null node store. SHAMapStore never
// builds DatabasePinned for it, so pinned (and catalogue) data
// would be accepted and then silently dropped.
if (nullBackend())
{
Throw<std::runtime_error>(
"pinned_type is not supported with [node_db] type=rwdb "
"(the rwdb node store does not persist nodes)");
}
// Ensure pinned_path is specified for filesystem-backed types.
// In-memory backends (rwdb) don't need a path.
auto pinnedTypeForPath = get(db_section, "pinned_type", "");
+20 -16
View File
@@ -3,12 +3,14 @@
#include <xrpld/nodestore/detail/DecodedBlob.h>
#include <xrpld/nodestore/detail/EncodedBlob.h>
#include <xrpld/nodestore/detail/codec.h>
#include <xrpl/basics/ReaderPreferringSharedMutex.h>
#include <xrpl/basics/contract.h>
#include <boost/beast/core/string.hpp>
#include <boost/core/ignore_unused.hpp>
#include <boost/unordered/concurrent_flat_map.hpp>
#include <memory>
#include <mutex>
#include <shared_mutex>
namespace ripple {
namespace NodeStore {
@@ -34,8 +36,7 @@ private:
using DataStore =
std::map<uint256, std::vector<std::uint8_t>>; // Store compressed blob
// data
mutable std::recursive_mutex
mutex_; // Only needed for std::map implementation
mutable ReaderPreferringSharedMutex mutex_;
DataStore table_;
@@ -65,7 +66,7 @@ public:
void
open(bool createIfMissing) override
{
std::lock_guard lock(mutex_);
std::unique_lock lock(mutex_);
if (isOpen_)
Throw<std::runtime_error>("already open");
isOpen_ = true;
@@ -74,26 +75,31 @@ public:
bool
isOpen() override
{
std::shared_lock lock(mutex_);
return isOpen_;
}
void
close() override
{
std::lock_guard lock(mutex_);
table_.clear();
isOpen_ = false;
DataStore old;
{
std::unique_lock lock(mutex_);
isOpen_ = false;
old.swap(table_); // O(1) swap; release lock before destructor runs
}
// 'old' is now destroyed outside the lock — no fetch() can be
// blocked by the (potentially millions-of-entries) map destructor.
}
Status
fetch(void const* key, std::shared_ptr<NodeObject>* pObject) override
{
if (!isOpen_)
return notFound;
uint256 const hash(uint256::fromVoid(key));
std::lock_guard lock(mutex_);
std::shared_lock lock(mutex_);
if (!isOpen_)
return notFound;
auto it = table_.find(hash);
if (it == table_.end())
return notFound;
@@ -128,9 +134,6 @@ public:
void
store(std::shared_ptr<NodeObject> const& object) override
{
if (!isOpen_)
return;
if (!object)
return;
@@ -143,7 +146,9 @@ public:
static_cast<const std::uint8_t*>(result.first),
static_cast<const std::uint8_t*>(result.first) + result.second);
std::lock_guard lock(mutex_);
std::unique_lock lock(mutex_);
if (!isOpen_)
return;
table_[object->getHash()] = std::move(compressed);
}
@@ -162,10 +167,9 @@ public:
void
for_each(std::function<void(std::shared_ptr<NodeObject>)> f) override
{
std::shared_lock lock(mutex_);
if (!isOpen_)
return;
std::lock_guard lock(mutex_);
for (const auto& entry : table_)
{
nudb::detail::buffer bf;
@@ -22,6 +22,8 @@
#include <xrpld/nodestore/detail/DatabaseRotatingImp.h>
#include <xrpl/protocol/HashPrefix.h>
#include <shared_mutex>
namespace ripple {
namespace NodeStore {
@@ -59,10 +61,13 @@ DatabaseRotatingImp::rotate(
// deleted.
std::shared_ptr<NodeStore::Backend> oldArchiveBackend;
{
std::lock_guard lock(mutex_);
std::unique_lock const lock(mutex_);
// Mark the archive backend for deletion
archiveBackend_->setDeletePath();
// Do NOT call setDeletePath() inside this lock. For in-memory
// backends, setDeletePath() calls close() which destructs the entire
// table_ map while the lock is held and starves consensus reads.
// Pinned history is preserved by DatabasePinned, not by copying
// ledgers into the writable backend during rotation.
oldArchiveBackend = std::move(archiveBackend_);
// Complete the rotation
@@ -72,20 +77,23 @@ DatabaseRotatingImp::rotate(
writableBackend_ = std::move(newBackend);
}
// Lock released — clear the old archive now without blocking fetches.
oldArchiveBackend->setDeletePath();
f(newWritableBackendName, newArchiveBackendName);
}
std::string
DatabaseRotatingImp::getName() const
{
std::lock_guard lock(mutex_);
std::shared_lock const lock(mutex_);
return writableBackend_->getName();
}
std::int32_t
DatabaseRotatingImp::getWriteLoad() const
{
std::lock_guard lock(mutex_);
std::shared_lock const lock(mutex_);
return writableBackend_->getWriteLoad();
}
@@ -93,7 +101,7 @@ void
DatabaseRotatingImp::importDatabase(Database& source)
{
auto const backend = [&] {
std::lock_guard lock(mutex_);
std::shared_lock const lock(mutex_);
return writableBackend_;
}();
@@ -104,7 +112,7 @@ bool
DatabaseRotatingImp::storeLedger(std::shared_ptr<Ledger const> const& srcLedger)
{
auto const backend = [&] {
std::lock_guard lock(mutex_);
std::shared_lock const lock(mutex_);
return writableBackend_;
}();
@@ -114,7 +122,7 @@ DatabaseRotatingImp::storeLedger(std::shared_ptr<Ledger const> const& srcLedger)
void
DatabaseRotatingImp::sync()
{
std::lock_guard lock(mutex_);
std::unique_lock const lock(mutex_);
writableBackend_->sync();
}
@@ -128,7 +136,7 @@ DatabaseRotatingImp::store(
auto nObj = NodeObject::createObject(type, std::move(data), hash);
auto const backend = [&] {
std::lock_guard lock(mutex_);
std::shared_lock const lock(mutex_);
return writableBackend_;
}();
@@ -182,7 +190,7 @@ DatabaseRotatingImp::fetchNodeObject(
std::shared_ptr<NodeObject> nodeObject;
auto [writable, archive] = [&] {
std::lock_guard lock(mutex_);
std::shared_lock const lock(mutex_);
return std::make_pair(writableBackend_, archiveBackend_);
}();
@@ -196,12 +204,17 @@ DatabaseRotatingImp::fetchNodeObject(
{
{
// Refresh the writable backend pointer
std::lock_guard lock(mutex_);
std::shared_lock const lock(mutex_);
writable = writableBackend_;
}
// Update writable backend with data from the archive backend
// if (duplicate)
// Promote every archive hit into the writable backend.
// Ordinary reads pass duplicate=false; the rotation copy
// pass passes true. Both must copy. The next rotation
// deletes this archive, and copyNode only walks the state
// tree, so an unpromoted header or transaction node
// disappears while SQL still names it.
(void)duplicate;
writable->store(nodeObject);
}
}
@@ -217,7 +230,7 @@ DatabaseRotatingImp::for_each(
std::function<void(std::shared_ptr<NodeObject>)> f)
{
auto [writable, archive] = [&] {
std::lock_guard lock(mutex_);
std::shared_lock const lock(mutex_);
return std::make_pair(writableBackend_, archiveBackend_);
}();
@@ -22,7 +22,10 @@
#include <xrpld/nodestore/DatabaseRotating.h>
#include <xrpl/basics/ReaderPreferringSharedMutex.h>
#include <mutex>
#include <shared_mutex>
namespace ripple {
namespace NodeStore {
@@ -93,7 +96,7 @@ public:
private:
std::shared_ptr<Backend> writableBackend_;
std::shared_ptr<Backend> archiveBackend_;
mutable std::mutex mutex_;
mutable ReaderPreferringSharedMutex mutex_;
std::shared_ptr<NodeObject>
fetchNodeObject(
+116 -6
View File
@@ -32,11 +32,16 @@
#include <xrpld/overlay/detail/PeerImp.h>
#include <xrpld/overlay/detail/Tuning.h>
#include <xrpld/perflog/PerfLog.h>
#include <xrpld/shamap/Family.h>
#include <xrpld/shamap/SHAMap.h>
#include <xrpld/shamap/SHAMapNodeID.h>
#include <xrpld/shamap/SHAMapTreeNode.h>
#include <xrpl/basics/UptimeClock.h>
#include <xrpl/basics/base64.h>
#include <xrpl/basics/random.h>
#include <xrpl/basics/safe_cast.h>
#include <xrpl/beast/core/LexicalCast.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/digest.h>
#include <boost/algorithm/string/predicate.hpp>
@@ -46,6 +51,7 @@
#include <memory>
#include <mutex>
#include <numeric>
#include <optional>
#include <sstream>
using namespace std::chrono_literals;
@@ -58,10 +64,48 @@ std::chrono::milliseconds constexpr peerHighLatency{300};
/** How often we PING the peer to check for latency and sendq probe */
std::chrono::seconds constexpr peerTimerInterval{60};
} // namespace
// TODO: Remove this exclusion once unit tests are added after the hotfix
// release.
// Null node store (type=rwdb) only: find a requested tree node by walking
// the child links of resident ledgers. Uses resident-only lookups, since
// seq and hash come from the peer and must not trigger SQL / node-store
// loads or edit the complete-ledger set.
std::shared_ptr<SHAMapTreeNode>
fetchLinkedTreeNode(
Application& app,
uint256 const& hash,
std::uint32_t seq,
std::optional<SHAMapNodeID> const& nodeId)
{
if (!nodeId || !app.getNodeFamily().isNullBackend())
return {};
auto findInMap = [&](SHAMap const& map) -> std::shared_ptr<SHAMapTreeNode> {
auto node = map.getLinkedNode(*nodeId);
if (node && node->getHash().as_uint256() == hash)
return node;
return {};
};
auto findInLedger = [&](std::shared_ptr<Ledger const> const& ledger)
-> std::shared_ptr<SHAMapTreeNode> {
if (!ledger)
return {};
if (auto node = findInMap(ledger->stateMap()))
return node;
return findInMap(ledger->txMap());
};
if (seq)
{
if (auto node =
findInLedger(app.getLedgerMaster().getResidentLedgerBySeq(seq)))
return node;
}
if (auto node = findInLedger(app.getLedgerMaster().getClosedLedger()))
return node;
return findInLedger(app.getLedgerMaster().getValidatedLedger());
}
} // namespace
PeerImp::PeerImp(
Application& app,
@@ -2478,13 +2522,65 @@ PeerImp::onMessage(std::shared_ptr<protocol::TMGetObjectByHash> const& m)
// need to inject the NodeStore interfaces.
std::uint32_t seq{obj.has_ledgerseq() ? obj.ledgerseq() : 0};
auto nodeObject{app_.getNodeStore().fetchNodeObject(hash, seq)};
void const* dataPtr = nullptr;
std::size_t dataSize = 0;
Blob treeBlob;
if (nodeObject)
{
dataPtr = nodeObject->getData().data();
dataSize = nodeObject->getData().size();
}
else if (
auto treeNode =
app_.getNodeFamily().getTreeNodeCache()->fetch(hash))
{
Serializer s;
treeNode->serializeWithPrefix(s);
treeBlob = std::move(s.modData());
dataPtr = treeBlob.data();
dataSize = treeBlob.size();
}
else if (
auto treeNode = fetchLinkedTreeNode(
app_,
hash,
seq,
obj.has_nodeid() ? deserializeSHAMapNodeID(obj.nodeid())
: std::nullopt))
{
Serializer s;
treeNode->serializeWithPrefix(s);
treeBlob = std::move(s.modData());
dataPtr = treeBlob.data();
dataSize = treeBlob.size();
}
else if (
packet.type() == protocol::TMGetObjectByHash::otLEDGER &&
app_.getNodeFamily().isNullBackend())
{
// Null node store: serve the header of a resident
// ledger in the same wire format the node store uses.
// Resident-only, so a peer cannot trigger a load.
if (auto ledger =
app_.getLedgerMaster().getResidentLedgerByHash(
hash))
{
Serializer s(sizeof(LedgerInfo) + 4);
s.add32(HashPrefix::ledgerMaster);
addRaw(ledger->info(), s);
treeBlob = std::move(s.modData());
dataPtr = treeBlob.data();
dataSize = treeBlob.size();
}
}
if (dataPtr)
{
protocol::TMIndexedObject& newObj = *reply.add_objects();
newObj.set_hash(hash.begin(), hash.size());
newObj.set_data(
&nodeObject->getData().front(),
nodeObject->getData().size());
newObj.set_data(dataPtr, dataSize);
if (obj.has_nodeid())
newObj.set_index(obj.nodeid());
@@ -2878,6 +2974,20 @@ PeerImp::checkTransaction(
}
}
// A manifest-authorized SetManifest's Fee follows the voted base fee,
// so a miss may be a fee setting this server has not reached yet.
// Drop it before any manifest signature work, but do not mark it bad:
// the same bytes can become canonical here.
if (auto const [fee, feeReason] = checkManifestIngressFee(app_, *stx);
fee == ManifestIngressFee::Refused)
{
JLOG(p_journal_.trace())
<< "Dropping manifest-authorized tx " << stx->getTransactionID()
<< ": " << feeReason;
charge(Resource::feeUselessData, "manifest fee miss");
return;
}
if (checkSignature)
{
// Check the signature before handing off to the job queue.
@@ -24,6 +24,7 @@
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/core/Config.h>
#include <xrpld/rpc/detail/TransactionSign.h>
#include <xrpl/hook/Enum.h>
#include <xrpl/json/json_value.h>
#include <xrpl/json/json_writer.h>
#include <xrpl/protocol/TxFlags.h>
@@ -60,6 +61,14 @@ MAGIC_ENUM_FLAG(ripple::NFTokenCreateOfferFlags);
MAGIC_ENUM_FLAG(ripple::ClaimRewardFlags);
MAGIC_ENUM_16(ripple::AccountFlags);
// hook_return_code spans [-10024, 0] because of INVALID_FLOAT
template <>
struct magic_enum::customize::enum_range<hook_api::hook_return_code>
{
static constexpr int min = -10024;
static constexpr int max = 0;
};
namespace ripple {
class Definitions
@@ -83,6 +92,50 @@ private:
}
}
template <typename EnumType>
void
addEnumToJson(Json::Value& json)
{
json = Json::objectValue;
// No structured binding: STR() is a lambda, and capturing a
// binding needs P1091 (Clang 16+).
for (auto const& entry : magic_enum::enum_entries<EnumType>())
{
auto const name = entry.second;
json[STR(name)] = static_cast<Json::Int>(entry.first);
}
}
// params is the stringified C parameter list, e.g.
// "(uint32_t read_ptr, uint32_t read_len)"
void
addHookApi(
Json::Value& json,
std::string const& returnType,
std::string const& name,
std::string params,
uint256 const& amendment)
{
Json::Value& fn = json[name];
fn["return"] = returnType;
fn["params"] = Json::arrayValue;
boost::trim_if(params, boost::is_any_of("() "));
std::vector<std::string> parts;
if (!params.empty())
boost::split(parts, params, boost::is_any_of(","));
for (auto& part : parts)
{
boost::trim(part);
auto const sp = part.rfind(' ');
Json::Value p = Json::objectValue;
p[jss::type] = part.substr(0, sp);
p[jss::name] = part.substr(sp + 1);
fn["params"].append(p);
}
if (amendment != uint256{})
fn["amendment"] = featureToName(amendment);
}
Json::Value
generate()
{
@@ -385,6 +438,29 @@ private:
ret[jss::native_currency_code] = systemCurrencyCode();
// Hook API: enough to regenerate hook/extern.h, hookapi.h keylet
// constants and error.h without the xahaud source tree.
Json::Value& hook = ret[jss::HOOK];
hook = Json::objectValue;
hook[jss::API] = Json::objectValue;
#pragma push_macro("HOOK_API_DEFINITION")
#undef HOOK_API_DEFINITION
#define HOOK_API_DEFINITION( \
RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, AMENDMENT) \
addHookApi( \
hook[jss::API], \
#RETURN_TYPE, \
#FUNCTION_NAME, \
#PARAMS_TUPLE, \
AMENDMENT);
#include <xrpl/hook/hook_api.macro>
#undef HOOK_API_DEFINITION
#pragma pop_macro("HOOK_API_DEFINITION")
addEnumToJson<hook_api::keylet_code::keylet_code>(
hook[jss::KEYLET_TYPES]);
addEnumToJson<hook_api::hook_return_code>(hook[jss::RETURN_CODES]);
// generate hash
{
const std::string out = Json::FastWriter().write(ret);
+12
View File
@@ -197,6 +197,18 @@ doSubmit(RPC::JsonContext& context)
}
{
// Before any manifest signature work. Not a bad signature, so the
// transaction is not marked bad.
if (auto const [fee, feeReason] =
checkManifestIngressFee(context.app, *stTx);
fee == ManifestIngressFee::Refused)
{
jvResult[jss::error] = "invalidTransaction";
jvResult[jss::error_exception] = "fails local checks: " + feeReason;
return jvResult;
}
if (!context.app.checkSigs())
forceValidity(
context.app.getHashRouter(),
+8
View File
@@ -82,6 +82,14 @@ public:
virtual void
reset() = 0;
/** True when this family's node store does not persist nodes.
type=rwdb uses NullFactory. Tree nodes live only through
in-memory SHAMap pointers. The flag is per family.
*/
virtual bool
isNullBackend() const = 0;
};
} // namespace ripple
+7
View File
@@ -87,8 +87,15 @@ public:
acquire(hash, seq);
}
bool
isNullBackend() const override
{
return nullBackend_;
}
private:
Application& app_;
bool const nullBackend_;
NodeStore::Database& db_;
beast::Journal const j_;
+12
View File
@@ -367,6 +367,18 @@ public:
void
invariants() const;
/** Child pointers copied from a discarded same-hash inner onto the
TreeNodeCache winner during canonicalize.
*/
static std::uint64_t
canonicalInnerBranchesHarvested();
/** Return the node at id if every parent on the path already has a
child pointer. Does not fetch from the node store.
*/
std::shared_ptr<SHAMapTreeNode>
getLinkedNode(SHAMapNodeID const& id) const;
private:
using SharedPtrNodeStack =
std::stack<std::pair<std::shared_ptr<SHAMapTreeNode>, SHAMapNodeID>>;
+1
View File
@@ -27,6 +27,7 @@ namespace ripple {
NodeFamily::NodeFamily(Application& app, CollectorManager& cm)
: app_(app)
, nullBackend_(app.config().nullBackend())
, db_(app.getNodeStore())
, j_(app.journal("NodeFamily"))
, fbCache_(std::make_shared<FullBelowCache>(
+67
View File
@@ -19,14 +19,53 @@
#include <xrpld/shamap/SHAMap.h>
#include <xrpld/shamap/SHAMapAccountStateLeafNode.h>
#include <xrpld/shamap/SHAMapInnerNode.h>
#include <xrpld/shamap/SHAMapNodeID.h>
#include <xrpld/shamap/SHAMapSyncFilter.h>
#include <xrpld/shamap/SHAMapTxLeafNode.h>
#include <xrpld/shamap/SHAMapTxPlusMetaLeafNode.h>
#include <xrpl/basics/contract.h>
#include <atomic>
namespace ripple {
namespace {
std::atomic<std::uint64_t> canonicalInnerBranchesHarvested_{0};
/** Join linked children from a discarded same-hash inner onto the cache winner.
Same-hash inners have identical branch masks and child hashes. Installing a
child the winner lacks can only add linkage; it cannot change content.
*/
void
mergeCanonicalInner(
std::shared_ptr<SHAMapTreeNode> const& canonical,
std::shared_ptr<SHAMapTreeNode> const& incoming)
{
// Same hash implies same node type, so checking one side suffices.
if (!canonical->isInner())
return;
auto* cached = static_cast<SHAMapInnerNode*>(canonical.get());
auto* other = static_cast<SHAMapInnerNode*>(incoming.get());
for (int branch = 0; branch < SHAMapInnerNode::branchFactor; ++branch)
{
if (cached->isEmptyBranch(branch) || cached->getChildPointer(branch))
continue;
auto child = other->getChild(branch);
if (!child)
continue;
cached->canonicalizeChild(branch, std::move(child));
canonicalInnerBranchesHarvested_.fetch_add(
1, std::memory_order_relaxed);
}
}
} // namespace
[[nodiscard]] std::shared_ptr<SHAMapLeafNode>
makeTypedLeaf(
SHAMapNodeType type,
@@ -1279,7 +1318,35 @@ SHAMap::canonicalize(
node->getHash() == hash,
"ripple::SHAMap::canonicalize : node hash do match");
auto incoming = node;
f_.getTreeNodeCache()->canonicalize_replace_client(hash.as_uint256(), node);
if (node != incoming)
mergeCanonicalInner(node, incoming);
}
std::uint64_t
SHAMap::canonicalInnerBranchesHarvested()
{
return canonicalInnerBranchesHarvested_.load(std::memory_order_relaxed);
}
std::shared_ptr<SHAMapTreeNode>
SHAMap::getLinkedNode(SHAMapNodeID const& id) const
{
auto node = root_;
SHAMapNodeID current;
while (node && current.getDepth() < id.getDepth())
{
if (!node->isInner())
return {};
auto* inner = static_cast<SHAMapInnerNode*>(node.get());
auto const branch = selectBranch(current, id.getNodeID());
if (inner->isEmptyBranch(branch))
return {};
node = inner->getChild(branch);
current = current.getChildNodeID(branch);
}
return node;
}
void
+4 -3
View File
@@ -191,7 +191,7 @@ SHAMap::gmn_ProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
fullBelow = false;
}
else if (
!backed_ ||
!backed_ || f_.isNullBackend() ||
!f_.getFullBelowCache()->touch_if_exists(childHash.as_uint256()))
{
bool pending = false;
@@ -248,7 +248,7 @@ SHAMap::gmn_ProcessNodes(MissingNodes& mn, MissingNodes::StackEntry& se)
if (fullBelow)
{ // No partial node encountered below this node
node->setFullBelowGen(mn.generation_);
if (backed_)
if (backed_ && !f_.isNullBackend())
{
f_.getFullBelowCache()->insert(node->getHash().as_uint256());
}
@@ -605,7 +605,8 @@ SHAMap::addKnownNode(
}
auto childHash = inner->getChildHash(branch);
if (f_.getFullBelowCache()->touch_if_exists(childHash.as_uint256()))
if (!f_.isNullBackend() &&
f_.getFullBelowCache()->touch_if_exists(childHash.as_uint256()))
{
return SHAMapAddNode::duplicate();
}