mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-22 19:57:09 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcd2b0a57d | ||
|
|
1440940636 | ||
|
|
ebaaf2132b | ||
|
|
ed10ffb935 | ||
|
|
a2a1e09c2e | ||
|
|
505c28768a | ||
|
|
d7f320bd17 | ||
|
|
22a94c4429 | ||
|
|
42a33a224d | ||
|
|
ee33d825fc | ||
|
|
9ef66a65b8 | ||
|
|
98fdaa4afa | ||
|
|
d9cc66afe9 | ||
|
|
b260b445e9 | ||
|
|
4e0e108654 | ||
|
|
919d817edc | ||
|
|
0373045bff | ||
|
|
2baa23d14b | ||
|
|
de9d5e53f1 | ||
|
|
80c70ec03d |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -69,6 +69,7 @@ enum TELcodes : TERUnderlyingType {
|
||||
telIMPORT_VL_KEY_NOT_RECOGNISED,
|
||||
telCAN_NOT_QUEUE_IMPORT,
|
||||
telENV_RPC_FAILED,
|
||||
telMANIFEST_FEE_MISMATCH,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -34,10 +34,11 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FIX (20261005, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FEATURE(EscrowDestinationCancel, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (20260929, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(HookOnV2_1, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(OnChainManifests, Supported::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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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."),
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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); //
|
||||
|
||||
+862
-111
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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")
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
-3
@@ -244,9 +244,6 @@ public:
|
||||
// Enable the experimental Ledger Replay functionality
|
||||
bool LEDGER_REPLAY = false;
|
||||
|
||||
// Maximum inbound light peers ([light_peers]); 0 disables light mode
|
||||
std::size_t LIGHT_PEERS_MAX = 0;
|
||||
|
||||
// Work queue limits
|
||||
int MAX_TRANSACTIONS = 1000;
|
||||
static constexpr int MAX_JOB_QUEUE_TX = 1000;
|
||||
@@ -378,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
|
||||
{
|
||||
|
||||
@@ -59,7 +59,6 @@ struct ConfigSection
|
||||
#define SECTION_IPS_FIXED "ips_fixed"
|
||||
#define SECTION_LEDGER_HISTORY "ledger_history"
|
||||
#define SECTION_LEDGER_REPLAY "ledger_replay"
|
||||
#define SECTION_LIGHT_PEERS "light_peers"
|
||||
#define SECTION_MAX_TRANSACTIONS "max_transactions"
|
||||
#define SECTION_NETWORK_ID "network_id"
|
||||
#define SECTION_NETWORK_QUORUM "network_quorum"
|
||||
|
||||
@@ -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 }}}
|
||||
}};
|
||||
@@ -732,9 +732,6 @@ Config::loadFromString(std::string const& fileContents)
|
||||
if (getSingleSection(secConfig, SECTION_LEDGER_REPLAY, strTemp, j_))
|
||||
LEDGER_REPLAY = beast::lexicalCastThrow<bool>(strTemp);
|
||||
|
||||
if (getSingleSection(secConfig, SECTION_LIGHT_PEERS, strTemp, j_))
|
||||
LIGHT_PEERS_MAX = beast::lexicalCastThrow<std::size_t>(strTemp);
|
||||
|
||||
if (exists(SECTION_REDUCE_RELAY))
|
||||
{
|
||||
auto sec = section(SECTION_REDUCE_RELAY);
|
||||
@@ -1063,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"))
|
||||
@@ -1092,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", "");
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -93,13 +93,6 @@ public:
|
||||
return category_;
|
||||
}
|
||||
|
||||
/** Get the protocol message type */
|
||||
int
|
||||
getType() const
|
||||
{
|
||||
return type_;
|
||||
}
|
||||
|
||||
/** Get the validator's key */
|
||||
std::optional<PublicKey> const&
|
||||
getValidatorKey() const
|
||||
@@ -111,7 +104,6 @@ private:
|
||||
std::vector<uint8_t> buffer_;
|
||||
std::vector<uint8_t> bufferCompressed_;
|
||||
std::size_t category_;
|
||||
int type_;
|
||||
std::once_flag once_flag_;
|
||||
std::optional<PublicKey> validatorKey_;
|
||||
|
||||
|
||||
@@ -133,14 +133,6 @@ public:
|
||||
|
||||
virtual bool
|
||||
txReduceRelayEnabled() const = 0;
|
||||
|
||||
/** A light peer (X-Protocol-Ctl: light=1): never used to acquire data,
|
||||
never sent proposals or transactions. */
|
||||
virtual bool
|
||||
isLight() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -93,12 +93,9 @@ makeFeaturesResponseHeader(
|
||||
bool comprEnabled,
|
||||
bool ledgerReplayEnabled,
|
||||
bool txReduceRelayEnabled,
|
||||
bool vpReduceRelayEnabled,
|
||||
bool lightEnabled)
|
||||
bool vpReduceRelayEnabled)
|
||||
{
|
||||
std::stringstream str;
|
||||
if (lightEnabled && featureEnabled(headers, FEATURE_LIGHT))
|
||||
str << FEATURE_LIGHT << "=1" << DELIM_FEATURE;
|
||||
if (comprEnabled && isFeatureValue(headers, FEATURE_COMPR, "lz4"))
|
||||
str << FEATURE_COMPR << "=lz4" << DELIM_FEATURE;
|
||||
if (ledgerReplayEnabled && featureEnabled(headers, FEATURE_LEDGER_REPLAY))
|
||||
@@ -417,8 +414,7 @@ makeResponse(
|
||||
app.config().COMPRESSION,
|
||||
app.config().LEDGER_REPLAY,
|
||||
app.config().TX_REDUCE_RELAY_ENABLE,
|
||||
app.config().VP_REDUCE_RELAY_ENABLE,
|
||||
app.config().LIGHT_PEERS_MAX > 0));
|
||||
app.config().VP_REDUCE_RELAY_ENABLE));
|
||||
|
||||
buildHandshake(resp, sharedValue, networkID, public_ip, remote_ip, app);
|
||||
|
||||
|
||||
@@ -146,9 +146,6 @@ static constexpr char FEATURE_VPRR[] = "vprr";
|
||||
static constexpr char FEATURE_TXRR[] = "txrr";
|
||||
// ledger replay
|
||||
static constexpr char FEATURE_LEDGER_REPLAY[] = "ledgerreplay";
|
||||
// light peer: receives validations, manifests, validator lists, ledger data,
|
||||
// status and endpoints only; may only ping, request ledger data and submit
|
||||
static constexpr char FEATURE_LIGHT[] = "light";
|
||||
static constexpr char DELIM_FEATURE[] = ";";
|
||||
static constexpr char DELIM_VALUE[] = ",";
|
||||
|
||||
@@ -255,8 +252,7 @@ makeFeaturesResponseHeader(
|
||||
bool comprEnabled,
|
||||
bool ledgerReplayEnabled,
|
||||
bool txReduceRelayEnabled,
|
||||
bool vpReduceRelayEnabled,
|
||||
bool lightEnabled = false);
|
||||
bool vpReduceRelayEnabled);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ Message::Message(
|
||||
int type,
|
||||
std::optional<PublicKey> const& validator)
|
||||
: category_(TrafficCount::categorize(message, type, false))
|
||||
, type_(type)
|
||||
, validatorKey_(validator)
|
||||
{
|
||||
using namespace ripple::compression;
|
||||
|
||||
@@ -241,17 +241,6 @@ OverlayImpl::onHandoff(
|
||||
return handoff;
|
||||
}
|
||||
|
||||
if (featureEnabled(request, FEATURE_LIGHT) &&
|
||||
lightPeerCount() >= app_.config().LIGHT_PEERS_MAX)
|
||||
{
|
||||
m_peerFinder->on_closed(slot);
|
||||
handoff.moved = false;
|
||||
handoff.response = makeErrorResponse(
|
||||
slot, request, remote_endpoint.address(), "Light peer slots full");
|
||||
handoff.keep_alive = false;
|
||||
return handoff;
|
||||
}
|
||||
|
||||
auto const sharedValue = makeSharedValue(*stream_ptr, journal);
|
||||
if (!sharedValue)
|
||||
{
|
||||
@@ -1071,17 +1060,6 @@ OverlayImpl::getActivePeers() const
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
OverlayImpl::lightPeerCount() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::size_t n = 0;
|
||||
for (auto const& [id, w] : ids_)
|
||||
if (auto const p = w.lock(); p && p->isLight())
|
||||
++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
Overlay::PeerSequence
|
||||
OverlayImpl::getActivePeers(
|
||||
std::set<Peer::id_t> const& toSkip,
|
||||
@@ -1102,11 +1080,6 @@ OverlayImpl::getActivePeers(
|
||||
{
|
||||
if (p = w.lock(); p != nullptr)
|
||||
{
|
||||
if (p->isLight())
|
||||
{
|
||||
--active;
|
||||
continue;
|
||||
}
|
||||
bool const reduceRelayEnabled = p->txReduceRelayEnabled();
|
||||
// tx reduced relay feature disabled
|
||||
if (!reduceRelayEnabled)
|
||||
|
||||
@@ -197,10 +197,6 @@ public:
|
||||
PeerSequence
|
||||
getActivePeers() const override;
|
||||
|
||||
/** Number of connected light peers. */
|
||||
std::size_t
|
||||
lightPeerCount() const;
|
||||
|
||||
/** Get active peers excluding peers in toSkip.
|
||||
@param toSkip peers to skip
|
||||
@param active a number of active peers
|
||||
|
||||
@@ -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,52 +64,47 @@ 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.
|
||||
|
||||
namespace {
|
||||
|
||||
// What a light peer may be sent: enough to follow validated ledgers and
|
||||
// learn other hubs. Never proposals, transactions or tx-set traffic.
|
||||
bool
|
||||
lightMayReceive(int type)
|
||||
// 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)
|
||||
{
|
||||
using namespace protocol;
|
||||
switch (type)
|
||||
{
|
||||
case mtPING:
|
||||
case mtMANIFESTS:
|
||||
case mtVALIDATION:
|
||||
case mtVALIDATORLIST:
|
||||
case mtVALIDATORLISTCOLLECTION:
|
||||
case mtLEDGER_DATA:
|
||||
case mtSTATUS_CHANGE:
|
||||
case mtENDPOINTS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!nodeId || !app.getNodeFamily().isNullBackend())
|
||||
return {};
|
||||
|
||||
// What a light peer may send us.
|
||||
bool
|
||||
lightMaySend(int type)
|
||||
{
|
||||
using namespace protocol;
|
||||
switch (type)
|
||||
{
|
||||
case mtPING:
|
||||
case mtGET_LEDGER:
|
||||
case mtTRANSACTION:
|
||||
case mtLEDGER_DATA:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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(
|
||||
@@ -163,10 +164,6 @@ PeerImp::PeerImp(
|
||||
headers_,
|
||||
FEATURE_LEDGER_REPLAY,
|
||||
app_.config().LEDGER_REPLAY))
|
||||
, light_(peerFeatureEnabled(
|
||||
headers_,
|
||||
FEATURE_LIGHT,
|
||||
app_.config().LIGHT_PEERS_MAX > 0))
|
||||
, ledgerReplayMsgHandler_(app, app.getLedgerReplayer())
|
||||
{
|
||||
JLOG(journal_.info()) << "compression enabled "
|
||||
@@ -294,9 +291,6 @@ PeerImp::send(std::shared_ptr<Message> const& m)
|
||||
if (detaching_)
|
||||
return;
|
||||
|
||||
if (light_ && !lightMayReceive(m->getType()))
|
||||
return;
|
||||
|
||||
auto validator = m->getValidatorKey();
|
||||
if (validator && !squelch_.expireSquelch(*validator))
|
||||
return;
|
||||
@@ -437,9 +431,6 @@ PeerImp::json()
|
||||
if (inbound_)
|
||||
ret[jss::inbound] = true;
|
||||
|
||||
if (light_)
|
||||
ret["light"] = true;
|
||||
|
||||
if (cluster())
|
||||
{
|
||||
ret[jss::cluster] = true;
|
||||
@@ -1081,10 +1072,6 @@ PeerImp::onMessageBegin(
|
||||
auto const name = protocolMessageName(type);
|
||||
load_event_ = app_.getJobQueue().makeLoadEvent(jtPEER, name);
|
||||
fee_ = {Resource::feeTrivialPeer, name};
|
||||
dropCurrent_ = light_ && !lightMaySend(type);
|
||||
if (dropCurrent_)
|
||||
fee_ = {
|
||||
Resource::feeMalformedRequest, "light peer: disallowed message"};
|
||||
auto const category = TrafficCount::categorize(*m, type, true);
|
||||
overlay_.reportTraffic(category, true, static_cast<int>(size));
|
||||
using namespace protocol;
|
||||
@@ -2535,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());
|
||||
@@ -2935,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.
|
||||
@@ -3309,9 +3362,8 @@ PeerImp::getTxSet(std::shared_ptr<protocol::TMGetLedger> const& m) const
|
||||
void
|
||||
PeerImp::processLedgerRequest(std::shared_ptr<protocol::TMGetLedger> const& m)
|
||||
{
|
||||
// Do not resource charge a peer responding to a relay. Light peers never
|
||||
// relay, so a cookie from one buys nothing.
|
||||
if (!m->has_requestcookie() || light_)
|
||||
// Do not resource charge a peer responding to a relay
|
||||
if (!m->has_requestcookie())
|
||||
charge(
|
||||
Resource::feeModerateBurdenPeer, "received a get ledger request");
|
||||
|
||||
|
||||
@@ -194,10 +194,6 @@ private:
|
||||
// on the peer.
|
||||
bool vpReduceRelayEnabled_ = false;
|
||||
bool ledgerReplayEnabled_ = false;
|
||||
// true if this inbound peer negotiated light mode (FEATURE_LIGHT)
|
||||
bool light_ = false;
|
||||
// set in onMessageBegin when a light peer sent a disallowed message
|
||||
bool dropCurrent_ = false;
|
||||
LedgerReplayMsgHandler ledgerReplayMsgHandler_;
|
||||
|
||||
friend class OverlayImpl;
|
||||
@@ -446,19 +442,6 @@ public:
|
||||
return txReduceRelayEnabled_;
|
||||
}
|
||||
|
||||
bool
|
||||
isLight() const override
|
||||
{
|
||||
return light_;
|
||||
}
|
||||
|
||||
/** Consulted by invokeProtocolMessage after onMessageBegin. */
|
||||
bool
|
||||
dropMessage() const
|
||||
{
|
||||
return dropCurrent_;
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
close();
|
||||
|
||||
@@ -74,8 +74,6 @@ PeerSetImpl::addPeers(
|
||||
pairs.reserve(overlay.size());
|
||||
|
||||
overlay.foreach([&](auto const& peer) {
|
||||
if (peer->isLight())
|
||||
return;
|
||||
auto const score = peer->getScore(hasItem(peer));
|
||||
pairs.emplace_back(score, std::move(peer));
|
||||
});
|
||||
|
||||
@@ -311,14 +311,6 @@ invoke(MessageHeader const& header, Buffers const& buffers, Handler& handler)
|
||||
header.payload_wire_size,
|
||||
header.uncompressed_size,
|
||||
header.algorithm != Algorithm::None);
|
||||
if constexpr (requires { handler.dropMessage(); })
|
||||
{
|
||||
if (handler.dropMessage())
|
||||
{
|
||||
handler.onMessageEnd(header.message_type, m);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
handler.onMessage(m);
|
||||
handler.onMessageEnd(header.message_type, m);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user