mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-22 11:47:09 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcd2b0a57d | ||
|
|
1440940636 | ||
|
|
ebaaf2132b | ||
|
|
ed10ffb935 |
No files matched your search
@@ -97,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
|
||||
@@ -303,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
|
||||
|
||||
@@ -12,7 +12,6 @@ libxrpl.server > xrpl.basics
|
||||
libxrpl.server > xrpl.json
|
||||
libxrpl.server > xrpl.protocol
|
||||
libxrpl.server > xrpl.server
|
||||
test.app > test.shamap
|
||||
test.app > test.toplevel
|
||||
test.app > test.unit_test
|
||||
test.app > xrpl.basics
|
||||
@@ -22,7 +21,6 @@ test.app > xrpld.ledger
|
||||
test.app > xrpld.nodestore
|
||||
test.app > xrpld.overlay
|
||||
test.app > xrpld.rpc
|
||||
test.app > xrpld.shamap
|
||||
test.app > xrpl.hook
|
||||
test.app > xrpl.json
|
||||
test.app > xrpl.protocol
|
||||
|
||||
@@ -166,13 +166,10 @@
|
||||
# ws Websockets
|
||||
# wss Secure Websockets
|
||||
# peer Peer Protocol
|
||||
# pwa On-ledger AppLoader documents (featurePWALoader), for a
|
||||
# reverse proxy only. Read "pwa ports" below first.
|
||||
#
|
||||
# Restrictions:
|
||||
#
|
||||
# Only one port may be configured to support the peer protocol.
|
||||
# The pwa protocol cannot be combined with any other protocol.
|
||||
# A port cannot have websocket and non websocket protocols at the
|
||||
# same time. It is possible have both Websockets and Secure Websockets
|
||||
# together in one port.
|
||||
@@ -267,119 +264,6 @@
|
||||
# "secure_gateway" connections, then they will be treated as
|
||||
# "admin" addresses.
|
||||
#
|
||||
# pwa ports
|
||||
#
|
||||
# A port whose protocol is "pwa" serves accounts' on-ledger AppLoader
|
||||
# documents (featurePWALoader) as HTML. This makes the node a web host
|
||||
# for third-party content: anyone can publish a loader to an account
|
||||
# for about a drop per byte, and nothing in protocol takes one down.
|
||||
# Hosting, and a hosting provider's obligations, are yours.
|
||||
#
|
||||
# One host per account, on a zone used for nothing else:
|
||||
#
|
||||
# https://pwa<account-index>.<domain>/<r-address>
|
||||
# https://<domain>/<r-address> 301 to the above
|
||||
# https://<custom host>/ see custom_domains
|
||||
#
|
||||
# The first label of the Host header is "pwa" followed by the decimal
|
||||
# AccountIndex of the account (an account created before AccountIndex
|
||||
# existed has no host); the rest must be one of the port's "domain"
|
||||
# values; the path must be that account's exact r-address. One
|
||||
# trailing slash and a query string are ignored; anything else is
|
||||
# 404. The bare zone never serves a document (every page would share
|
||||
# one origin): it redirects, keeping :port and query.
|
||||
# Never set cookies at or above the zone and never serve your own
|
||||
# site from it. Wildcard DNS and a wildcard certificate cover it.
|
||||
#
|
||||
# Rules enforced at startup and on every request:
|
||||
#
|
||||
# - "pwa" is the port's only protocol; no admin, user/password or
|
||||
# ssl_* keys (terminate TLS at the proxy).
|
||||
# - "domain" is required: the zone(s), comma-separated. The proxy must
|
||||
# forward the Host header unchanged.
|
||||
# - "secure_gateway" is required and may not cover every address.
|
||||
# Every request must come from it carrying X-Forwarded-For, whose
|
||||
# RIGHTMOST entry (the one the proxy wrote) is the client. The
|
||||
# "Forwarded" header is ignored.
|
||||
# - Clients are metered per IPv4 address, per IPv6 /64; a miss costs
|
||||
# what a hit does; over-use is answered 503 until it decays.
|
||||
# - GET and HEAD only; every response closes the connection; only
|
||||
# validated state is served, with an ETag (304 on If-None-Match).
|
||||
# X-NetworkID, X-LedgerSeq, X-LedgerHash and X-PWASeq name the
|
||||
# validated ledger and AppLoaderSequence the document came from, so
|
||||
# a client (a browser extension, say) can vet it against a second
|
||||
# node with the pwa_validate RPC (account, hash of the bytes, host).
|
||||
# - Lookups run on at most two job queue threads, below client RPC;
|
||||
# beyond a bounded backlog they are refused with 503. Put a cache in
|
||||
# front (the ETag makes revalidation cheap).
|
||||
# - The admin RPC "pwa_info" reports requests by outcome and bytes
|
||||
# served; "reset": true zeroes them, "flush_dns": true drops the
|
||||
# custom domain cache (CLI: xahaud pwa_info [reset] [flush_dns]).
|
||||
#
|
||||
# Example (also add port_pwa to the [server] list):
|
||||
#
|
||||
# [port_pwa]
|
||||
# port = 8088
|
||||
# ip = 127.0.0.1
|
||||
# protocol = pwa
|
||||
# secure_gateway = 127.0.0.1
|
||||
# domain = pwapp.example
|
||||
#
|
||||
# with nginx in front, TLS terminated there, Host passed through:
|
||||
#
|
||||
# server {
|
||||
# listen 443 ssl;
|
||||
# server_name pwapp.example ~^pwa[0-9]+\.pwapp\.example$;
|
||||
# ssl_certificate /etc/ssl/wildcard.pwapp.example.pem;
|
||||
# ssl_certificate_key /etc/ssl/wildcard.pwapp.example.key;
|
||||
# location / {
|
||||
# proxy_pass http://127.0.0.1:8088;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Forwarded-For $remote_addr;
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# custom_domains = 0 | 1
|
||||
#
|
||||
# Default 0. With 1, a request whose Host is outside every zone is
|
||||
# served as the account named by DNS, so a domain owner can point
|
||||
# their own name at your proxy:
|
||||
#
|
||||
# app.example.org. CNAME pwa-proxy.your.example.
|
||||
# _xahau-pwa.app.example.org. TXT "rAbC..."
|
||||
#
|
||||
# The record lives on the _xahau-pwa label beneath the host because
|
||||
# the host is usually a CNAME to your proxy. Exactly one address must
|
||||
# be present there (other TXT strings are ignored; two addresses count
|
||||
# as none). The named account must consent: its Domain field must
|
||||
# equal the host or a parent of it (ignoring case), else 503. Lists
|
||||
# apply as at the account's own host; only the root path is a
|
||||
# document. No usable record is 404 (cached 15 minutes), a resolver
|
||||
# failure 503 (cached 60 s), a record is cached for its TTL (clamped
|
||||
# to 60..3600 s). Lookups run on their own job queue threads, at most
|
||||
# four at a time and one per host; a request that misses the cache is
|
||||
# answered 429 with Retry-After and a refreshing page rather than
|
||||
# waiting, so a flood of Host headers cannot tie up the node. The
|
||||
# admin RPC pwa_info with "flush_dns": true drops the cache. Your
|
||||
# proxy must accept, and hold certificates for, the hosts you will
|
||||
# serve (e.g. Caddy on-demand TLS); xahaud only sees the Host header.
|
||||
#
|
||||
# resolver = <IPv4 address>[:port]
|
||||
#
|
||||
# The recursive resolver for custom_domains; default is the system
|
||||
# resolver. xahaud does not validate DNSSEC, so prefer a validating
|
||||
# resolver on the same host.
|
||||
#
|
||||
# Documents are served with "Content-Security-Policy: sandbox
|
||||
# allow-scripts ...": an opaque origin that cannot read anything from
|
||||
# the node but can still send requests anywhere, including the
|
||||
# visitor's own machine, so keep admin ports behind credentials.
|
||||
#
|
||||
# [pwa_allowlist] and [pwa_blocklist], one r-address per line, apply to
|
||||
# every pwa port: if the allowlist is non-empty only those accounts are
|
||||
# served; blocklisted accounts are never served (403), whatever the
|
||||
# allowlist says. An account on both is a startup error.
|
||||
#
|
||||
# ssl_key = <filename>
|
||||
# ssl_cert = <filename>
|
||||
# ssl_chain = <filename>
|
||||
@@ -790,38 +674,6 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
# [early_manifest_sync]
|
||||
#
|
||||
# Controls whether, while acquiring its first ledger, the server fetches and
|
||||
# reads the on-ledger validator manifests ahead of the rest of that ledger's
|
||||
# state. Doing so lets it resolve validators' current signing keys, including
|
||||
# rotations and revocations published on-ledger, within seconds of starting
|
||||
# a sync rather than once the whole state has arrived.
|
||||
#
|
||||
# The manifests read are those of the master keys on the server's own
|
||||
# validator lists, fetched by key, and as many more from the on-ledger
|
||||
# manifest directory as the manifest cache keeps.
|
||||
#
|
||||
# The cost is a dozen or so request/response rounds with peers. With more
|
||||
# than one peer serving the ledger these overlap with fetching the rest of
|
||||
# its state; with only one, they come first.
|
||||
#
|
||||
# It also lets a server that starts with nothing but the master keys of its
|
||||
# validators and a few peers find its way. Until it has validated a ledger,
|
||||
# while it holds manifests for too few of its validators to make a quorum,
|
||||
# it reads them from up to three of the ledgers its peers report, the most
|
||||
# widely reported first, every 15 seconds. Every manifest read is checked
|
||||
# against its master key's signature, so a ledger reported by a peer that
|
||||
# is not on the network can leave something out but cannot add anything.
|
||||
#
|
||||
# Legal values are:
|
||||
# 1 - Fetch the manifests first, and from peers' ledgers if needed
|
||||
# 0 - Fetch the ledger's state in the usual order, and read the manifests
|
||||
# from it once it is complete
|
||||
#
|
||||
# The default is 1. A ledger without on-ledger manifests costs only the few
|
||||
# nodes on the paths to where their directory and objects would be.
|
||||
#
|
||||
#
|
||||
#
|
||||
# [ledger_history]
|
||||
@@ -1564,19 +1416,6 @@
|
||||
# contains breaking changes that require a new API version number.
|
||||
# They are not ready for public consumption.
|
||||
#
|
||||
# [hook_validate_rpc]
|
||||
#
|
||||
# 0 or 1.
|
||||
#
|
||||
# 0: Disable the hook_validate RPC method
|
||||
# 1: Enable the hook_validate RPC method, which runs the Hook guard
|
||||
# checker and WASM VM validation against caller-supplied code using
|
||||
# the current open ledger's rules.
|
||||
#
|
||||
# The default is 0 on networked nodes and 1 in standalone mode (-a).
|
||||
# When enabled the method is available to non-admin (user role) clients,
|
||||
# so consider the CPU cost before enabling it on a public-facing node.
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# 10. Example Settings
|
||||
|
||||
@@ -97,7 +97,6 @@ target_link_libraries (ripple_syslibs
|
||||
crypt32
|
||||
>
|
||||
$<$<NOT:$<BOOL:${MSVC}>>:dl>
|
||||
$<$<NOT:$<BOOL:${MSVC}>>:resolv>
|
||||
$<$<NOT:$<OR:$<BOOL:${MSVC}>,$<BOOL:${APPLE}>>>:rt>)
|
||||
|
||||
if (NOT MSVC)
|
||||
|
||||
@@ -47,6 +47,5 @@
|
||||
#define MEM_OVERLAP -43
|
||||
#define TOO_MANY_STATE_MODIFICATIONS -44
|
||||
#define TOO_MANY_NAMESPACES -45
|
||||
#define TOO_LITTLE_ENTROPY -46
|
||||
#define HOOK_ERROR_CODES
|
||||
#endif //HOOK_ERROR_CODES
|
||||
@@ -339,12 +339,6 @@ prepare(
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len);
|
||||
|
||||
extern int64_t
|
||||
dice(uint32_t sides);
|
||||
|
||||
extern int64_t
|
||||
util_random(uint32_t write_ptr, uint32_t write_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+1
-1
@@ -46,8 +46,8 @@
|
||||
|
||||
#include "error.h"
|
||||
#include "extern.h"
|
||||
#include "macro.h"
|
||||
#include "sfcodes.h"
|
||||
#include "macro.h"
|
||||
#include "tts.h"
|
||||
|
||||
#include "ls_flags.h"
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
#define sfHookApiVersion ((1U << 16U) + 20U)
|
||||
#define sfHookStateScale ((1U << 16U) + 21U)
|
||||
#define sfLedgerFixType ((1U << 16U) + 22U)
|
||||
#define sfEntropyCount ((1U << 16U) + 99U)
|
||||
#define sfEntropyDenominator ((1U << 16U) + 100U)
|
||||
#define sfNetworkID ((2U << 16U) + 1U)
|
||||
#define sfFlags ((2U << 16U) + 2U)
|
||||
#define sfSourceTag ((2U << 16U) + 3U)
|
||||
@@ -82,7 +80,6 @@
|
||||
#define sfRewardTime ((2U << 16U) + 98U)
|
||||
#define sfRewardLgrFirst ((2U << 16U) + 99U)
|
||||
#define sfRewardLgrLast ((2U << 16U) + 100U)
|
||||
#define sfAppLoaderSequence ((2U << 16U) + 101U)
|
||||
#define sfIndexNext ((3U << 16U) + 1U)
|
||||
#define sfIndexPrevious ((3U << 16U) + 2U)
|
||||
#define sfBookNode ((3U << 16U) + 3U)
|
||||
@@ -155,8 +152,6 @@
|
||||
#define sfEscrowID ((5U << 16U) + 35U)
|
||||
#define sfURITokenID ((5U << 16U) + 36U)
|
||||
#define sfDomainID ((5U << 16U) + 37U)
|
||||
#define sfNextRandomDigest ((5U << 16U) + 89U)
|
||||
#define sfRandomData ((5U << 16U) + 90U)
|
||||
#define sfManifestID ((5U << 16U) + 91U)
|
||||
#define sfHookOnOutgoing ((5U << 16U) + 93U)
|
||||
#define sfHookOnIncoming ((5U << 16U) + 94U)
|
||||
@@ -227,7 +222,6 @@
|
||||
#define sfProvider ((7U << 16U) + 30U)
|
||||
#define sfMPTokenMetadata ((7U << 16U) + 31U)
|
||||
#define sfCredentialType ((7U << 16U) + 32U)
|
||||
#define sfAppLoader ((7U << 16U) + 96U)
|
||||
#define sfHookName ((7U << 16U) + 97U)
|
||||
#define sfRemarkValue ((7U << 16U) + 98U)
|
||||
#define sfRemarkName ((7U << 16U) + 99U)
|
||||
@@ -295,7 +289,6 @@
|
||||
#define sfXChainCreateAccountAttestationCollectionElement ((14U << 16U) + 31U)
|
||||
#define sfPriceData ((14U << 16U) + 32U)
|
||||
#define sfCredential ((14U << 16U) + 33U)
|
||||
#define sfRandomDigestEntry ((14U << 16U) + 88U)
|
||||
#define sfManifest ((14U << 16U) + 90U)
|
||||
#define sfAmountEntry ((14U << 16U) + 91U)
|
||||
#define sfMintURIToken ((14U << 16U) + 92U)
|
||||
@@ -328,7 +321,6 @@
|
||||
#define sfAuthorizeCredentials ((15U << 16U) + 26U)
|
||||
#define sfUnauthorizeCredentials ((15U << 16U) + 27U)
|
||||
#define sfAcceptedCredentials ((15U << 16U) + 28U)
|
||||
#define sfRandomDigests ((15U << 16U) + 91U)
|
||||
#define sfAmounts ((15U << 16U) + 92U)
|
||||
#define sfHookEmissions ((15U << 16U) + 93U)
|
||||
#define sfImportVLKeys ((15U << 16U) + 94U)
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
#define ttNFTOKEN_MODIFY 70
|
||||
#define ttPERMISSIONED_DOMAIN_SET 71
|
||||
#define ttPERMISSIONED_DOMAIN_DELETE 72
|
||||
#define ttENTROPY 89
|
||||
#define ttMANIFEST_SET 91
|
||||
#define ttCRON 92
|
||||
#define ttCRON_SET 93
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#define uint256 std::string
|
||||
#define featureHooksUpdate1 "1"
|
||||
#define featureHooksUpdate2 "1"
|
||||
#define featureRNG "1"
|
||||
#define fix20250131 "1"
|
||||
#define fixGuardDepth32 "1"
|
||||
namespace hook_api {
|
||||
@@ -386,9 +385,7 @@ enum class hook_return_code : int64_t {
|
||||
MEM_OVERLAP = -43, // one or more specified buffers are the same memory
|
||||
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 256 modified state
|
||||
// entires in the combined hook chains
|
||||
TOO_MANY_NAMESPACES = -45,
|
||||
TOO_LITTLE_ENTROPY = -46 // featureRNG: no seed for this ledger, or too few
|
||||
// validators contributed to it
|
||||
TOO_MANY_NAMESPACES = -45
|
||||
};
|
||||
|
||||
enum class ExitType : uint8_t {
|
||||
@@ -401,7 +398,6 @@ enum class ExitType : uint8_t {
|
||||
const uint16_t max_state_modifications = 256;
|
||||
const uint8_t max_slots = 255;
|
||||
const uint8_t max_nonce = 255;
|
||||
const uint16_t max_rng_draws = 256; // featureRNG, per Hook execution
|
||||
const uint8_t max_emit = 255;
|
||||
const uint8_t max_params = 16;
|
||||
const double fee_base_multiplier = 1.1f;
|
||||
|
||||
@@ -1096,11 +1096,7 @@ validateGuards(
|
||||
{
|
||||
int name_len = parseLeb128(wasm, i, &i);
|
||||
CHECK_SHORT_HOOK();
|
||||
// Only compare the name if all four bytes are in bounds. A
|
||||
// name running past the end is rejected as a short hook below
|
||||
// either way, so this does not change which hooks are valid.
|
||||
if (name_len == 4 &&
|
||||
static_cast<std::size_t>(i) + 4 <= wasm.size())
|
||||
if (name_len == 4)
|
||||
{
|
||||
if (wasm[i] == 'h' && wasm[i + 1] == 'o' &&
|
||||
wasm[i + 2] == 'o' && wasm[i + 3] == 'k')
|
||||
|
||||
@@ -298,12 +298,4 @@ HOOK_API_DEFINITION(
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, prepare, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
featureHooksUpdate2)
|
||||
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, dice, (uint32_t sides),
|
||||
featureRNG)
|
||||
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_random, (uint32_t write_ptr, uint32_t write_len),
|
||||
featureRNG)
|
||||
// clang-format on
|
||||
@@ -1,66 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_PROTOCOL_APPLOADER_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_APPLOADER_H_INCLUDED
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Validation of an AppLoader document (sfAppLoader), the bootstrap page of
|
||||
a Progressive Web App served from an account.
|
||||
|
||||
Consensus cannot afford a real HTML parser, so this is a shallow sanity
|
||||
gate and nothing more. A document must be non-empty and at most
|
||||
maxAppLoaderLength bytes; well-formed UTF-8; free of C0 control
|
||||
characters other than TAB, LF, FF and CR, and of DEL; begin, after an
|
||||
optional BOM and whitespace, with an HTML doctype or an <html start tag;
|
||||
and contain an <html start tag followed later by a </html> end tag with
|
||||
nothing but whitespace after the last one. Nothing parses comments,
|
||||
scripts or attributes, and content is not policed.
|
||||
|
||||
Consensus critical: a change to what it accepts needs an amendment.
|
||||
*/
|
||||
namespace appLoader {
|
||||
|
||||
enum class Result : std::uint8_t {
|
||||
ok = 0,
|
||||
empty,
|
||||
tooLarge,
|
||||
badUTF8,
|
||||
badControlChar,
|
||||
noDoctype, // does not open with a doctype or <html tag
|
||||
noHtmlElement, // no <html start tag
|
||||
unclosed, // no </html> end tag after it
|
||||
trailingGarbage, // non-whitespace after the last </html>
|
||||
};
|
||||
|
||||
/** Total: every input yields a result. `data` may be null iff size is 0. */
|
||||
Result
|
||||
validate(std::uint8_t const* data, std::size_t size);
|
||||
|
||||
char const*
|
||||
to_string(Result r);
|
||||
|
||||
} // namespace appLoader
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -118,10 +118,6 @@ negativeUNL() noexcept;
|
||||
Keylet const&
|
||||
UNLReport() noexcept;
|
||||
|
||||
/** The (fixed) index of the featureRNG random number generator object. */
|
||||
Keylet const&
|
||||
random() noexcept;
|
||||
|
||||
/** The beginning of an order book */
|
||||
struct book_t
|
||||
{
|
||||
@@ -332,14 +328,6 @@ xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq);
|
||||
Keylet
|
||||
did(AccountID const& account) noexcept;
|
||||
|
||||
/** An AppLoader belonging to an account.
|
||||
|
||||
There is at most one per account, so the key is derived from the owner's
|
||||
AccountID alone.
|
||||
*/
|
||||
Keylet
|
||||
appLoader(AccountID const& account) noexcept;
|
||||
|
||||
Keylet
|
||||
oracle(AccountID const& account, std::uint32_t const& documentID) noexcept;
|
||||
|
||||
@@ -388,16 +376,6 @@ permissionedDomain(uint256 const& domainID) noexcept;
|
||||
Keylet
|
||||
manifest(PublicKey const& pk) noexcept;
|
||||
|
||||
/** The directory listing every master key's ltMANIFEST object.
|
||||
|
||||
One entry per master key, made when its first manifest is published and
|
||||
kept for good: a newer manifest, a revocation included, replaces the object
|
||||
at the same key and keeps its place. The copy at the ephemeral key is not
|
||||
listed; it is named by the master object.
|
||||
*/
|
||||
Keylet const&
|
||||
manifestDir() noexcept;
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
// Everything below is deprecated and should be removed in favor of keylets:
|
||||
@@ -428,7 +406,7 @@ struct keyletDesc
|
||||
|
||||
// This list should include all of the keylet functions that take a single
|
||||
// AccountID parameter.
|
||||
std::array<keyletDesc<AccountID const&>, 7> const directAccountKeylets{
|
||||
std::array<keyletDesc<AccountID const&>, 6> const directAccountKeylets{
|
||||
{{&keylet::account, jss::AccountRoot, false},
|
||||
{&keylet::ownerDir, jss::DirectoryNode, true},
|
||||
{&keylet::signers, jss::SignerList, true},
|
||||
@@ -436,8 +414,7 @@ std::array<keyletDesc<AccountID const&>, 7> const directAccountKeylets{
|
||||
// test it anyway, since the invariant checks for it.
|
||||
{&keylet::nftpage_min, jss::NFTokenPage, true},
|
||||
{&keylet::nftpage_max, jss::NFTokenPage, true},
|
||||
{&keylet::did, jss::DID, true},
|
||||
{&keylet::appLoader, jss::AppLoader, true}}};
|
||||
{&keylet::did, jss::DID, true}}};
|
||||
|
||||
MPTID
|
||||
makeMptID(std::uint32_t sequence, AccountID const& account);
|
||||
|
||||
@@ -96,25 +96,6 @@ std::size_t constexpr maxDIDAttestationLength = 256;
|
||||
/** The maximum length of a domain */
|
||||
std::size_t constexpr maxDomainLength = 256;
|
||||
|
||||
/** The maximum length of an AppLoader document.
|
||||
|
||||
The AppLoader holds the bootstrap document of a Progressive Web App
|
||||
served from an account. It is deliberately capped: the blob lives in a
|
||||
ledger object that carries no owner reserve at all, so the only things
|
||||
standing between it and unbounded ledger growth are this limit and the
|
||||
per-byte transaction fee below.
|
||||
*/
|
||||
std::size_t constexpr maxAppLoaderLength = 4096;
|
||||
|
||||
/** Drops of transaction fee charged per byte of AppLoader document.
|
||||
|
||||
Storage is paid for through the fee rather than through an owner
|
||||
reserve, so that a loader can be published by an account sitting at its
|
||||
reserve floor. The fee is charged on every AccountSet that carries the
|
||||
field, including overwrites.
|
||||
*/
|
||||
std::uint64_t constexpr appLoaderFeeDropsPerByte = 1;
|
||||
|
||||
/** The maximum length of a URI inside a Credential */
|
||||
std::size_t constexpr maxCredentialURILength = 256;
|
||||
|
||||
|
||||
@@ -197,7 +197,6 @@ enum TEFcodes : TERUnderlyingType {
|
||||
tefINVALID_LEDGER_FIX_TYPE,
|
||||
tefPAST_MANIFEST_SEQ,
|
||||
tefREVOKED_MANIFEST,
|
||||
tefMANIFEST_KEY_IN_USE,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -152,26 +152,6 @@ public:
|
||||
return static_cast<bool>(mHookEmissions);
|
||||
}
|
||||
|
||||
/** featureRNG: the RNG state this transaction's first Hook draw saw.
|
||||
Every value the transaction's Hooks drew is derived from it. */
|
||||
void
|
||||
setRandomData(uint256 const& randomData)
|
||||
{
|
||||
mRandomData = randomData;
|
||||
}
|
||||
|
||||
bool
|
||||
hasRandomData() const
|
||||
{
|
||||
return static_cast<bool>(mRandomData);
|
||||
}
|
||||
|
||||
uint256 const&
|
||||
getRandomData() const
|
||||
{
|
||||
return *mRandomData;
|
||||
}
|
||||
|
||||
STAmount
|
||||
getDeliveredAmount() const
|
||||
{
|
||||
@@ -196,7 +176,6 @@ private:
|
||||
std::optional<STAmount> mDelivered;
|
||||
std::optional<STArray> mHookExecutions;
|
||||
std::optional<STArray> mHookEmissions;
|
||||
std::optional<uint256> mRandomData;
|
||||
|
||||
STArray mNodes;
|
||||
};
|
||||
|
||||
@@ -34,8 +34,6 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FEATURE(RNG, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(PWALoader, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (20261005, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FEATURE(EscrowDestinationCancel, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (20260929, Supported::yes, VoteBehavior::DefaultNo)
|
||||
|
||||
@@ -142,22 +142,6 @@ LEDGER_ENTRY(ltIMPORT_VLSEQ, 0x0049, ImportVLSequence, import_vlseq, ({
|
||||
{sfPreviousTxnLgrSeq, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** The bootstrap document of a Progressive Web App (featurePWALoader).
|
||||
|
||||
At most one per account, keyed on the owner alone (keylet::appLoader).
|
||||
It is in no owner directory and takes no reserve: its storage is paid
|
||||
for by a per-byte fee on the AccountSet that writes it, and
|
||||
DeleteAccount erases it explicitly. AppLoaderSequence equals the owner's
|
||||
AccountRoot AppLoaderSequence as of the AccountSet that wrote it.
|
||||
*/
|
||||
LEDGER_ENTRY(ltAPP_LOADER, 0x004c, AppLoader, app_loader, ({
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfAppLoader, soeREQUIRED},
|
||||
{sfAppLoaderSequence, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** The ledger object which tracks the current negative UNL state.
|
||||
|
||||
\note This is a singleton: only one such object exists in the ledger.
|
||||
@@ -196,26 +180,6 @@ LEDGER_ENTRY_DUPLICATE(ltUNL_REPORT, 0x0052, UNLReport, unl_report, ({
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** featureRNG: the consensus random number generator.
|
||||
|
||||
A singleton. sfRandomData is seeded from validator reveals by the
|
||||
ttENTROPY transactions applied at the start of each ledger, and then
|
||||
iterated by every Hook API draw for the rest of that ledger.
|
||||
sfRandomData and sfRandomDigests are never written to metadata, and
|
||||
Hooks cannot load this object.
|
||||
|
||||
\sa keylet::random
|
||||
*/
|
||||
LEDGER_ENTRY(ltRANDOM, 0x526E, Random, random, ({
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfRandomData, soeREQUIRED},
|
||||
{sfEntropyCount, soeREQUIRED},
|
||||
{sfEntropyDenominator, soeREQUIRED},
|
||||
{sfRandomDigests, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** A ledger object which contains a signer list for an account.
|
||||
|
||||
\sa keylet::signers
|
||||
@@ -298,7 +262,6 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
|
||||
{sfHookStateScale, soeOPTIONAL},
|
||||
{sfCron, soeOPTIONAL},
|
||||
{sfAMMID, soeOPTIONAL},
|
||||
{sfAppLoaderSequence, soeOPTIONAL},
|
||||
{sfManifestID, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
@@ -646,7 +609,6 @@ LEDGER_ENTRY(ltMANIFEST, 0x004D, Manifest, manifest_entry, ({
|
||||
{sfMasterSignature, soeREQUIRED},
|
||||
{sfSignature, soeOPTIONAL}, /* absent if the master is revoked */
|
||||
{sfManifestID, soeOPTIONAL}, /* pointer to the identical other copy on the other key */
|
||||
{sfOwnerNode, soeOPTIONAL}, /* master copy only: its page in keylet::manifestDir() */
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
@@ -59,8 +59,6 @@ TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19)
|
||||
TYPED_SFIELD(sfHookApiVersion, UINT16, 20)
|
||||
TYPED_SFIELD(sfHookStateScale, UINT16, 21)
|
||||
TYPED_SFIELD(sfLedgerFixType, UINT16, 22)
|
||||
TYPED_SFIELD(sfEntropyCount, UINT16, 99)
|
||||
TYPED_SFIELD(sfEntropyDenominator, UINT16, 100)
|
||||
|
||||
// 32-bit integers (common)
|
||||
TYPED_SFIELD(sfNetworkID, UINT32, 1)
|
||||
@@ -125,7 +123,6 @@ TYPED_SFIELD(sfImportSequence, UINT32, 97)
|
||||
TYPED_SFIELD(sfRewardTime, UINT32, 98)
|
||||
TYPED_SFIELD(sfRewardLgrFirst, UINT32, 99)
|
||||
TYPED_SFIELD(sfRewardLgrLast, UINT32, 100)
|
||||
TYPED_SFIELD(sfAppLoaderSequence, UINT32, 101)
|
||||
|
||||
// 64-bit integers (common)
|
||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
||||
@@ -213,9 +210,6 @@ TYPED_SFIELD(sfOfferID, UINT256, 34)
|
||||
TYPED_SFIELD(sfEscrowID, UINT256, 35)
|
||||
TYPED_SFIELD(sfURITokenID, UINT256, 36)
|
||||
TYPED_SFIELD(sfDomainID, UINT256, 37)
|
||||
TYPED_SFIELD(sfNextRandomDigest, UINT256, 89)
|
||||
// RNG running state: never written to metadata (see featureRNG)
|
||||
TYPED_SFIELD(sfRandomData, UINT256, 90, SField::sMD_Never)
|
||||
TYPED_SFIELD(sfManifestID, UINT256, 91)
|
||||
TYPED_SFIELD(sfHookOnOutgoing, UINT256, 93)
|
||||
TYPED_SFIELD(sfHookOnIncoming, UINT256, 94)
|
||||
@@ -300,7 +294,6 @@ TYPED_SFIELD(sfAssetClass, VL, 29)
|
||||
TYPED_SFIELD(sfProvider, VL, 30)
|
||||
TYPED_SFIELD(sfMPTokenMetadata, VL, 31)
|
||||
TYPED_SFIELD(sfCredentialType, VL, 32)
|
||||
TYPED_SFIELD(sfAppLoader, VL, 96)
|
||||
TYPED_SFIELD(sfHookName, VL, 97)
|
||||
TYPED_SFIELD(sfRemarkValue, VL, 98)
|
||||
TYPED_SFIELD(sfRemarkName, VL, 99)
|
||||
@@ -390,7 +383,6 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
|
||||
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
|
||||
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
|
||||
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
|
||||
UNTYPED_SFIELD(sfRandomDigestEntry, OBJECT, 88)
|
||||
UNTYPED_SFIELD(sfManifest, OBJECT, 90)
|
||||
UNTYPED_SFIELD(sfAmountEntry, OBJECT, 91)
|
||||
UNTYPED_SFIELD(sfMintURIToken, OBJECT, 92)
|
||||
@@ -431,8 +423,6 @@ UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25)
|
||||
UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26)
|
||||
UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27)
|
||||
UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28)
|
||||
// RNG validator commitments: never written to metadata (see featureRNG)
|
||||
UNTYPED_SFIELD(sfRandomDigests, ARRAY, 91, SField::sMD_Never)
|
||||
UNTYPED_SFIELD(sfAmounts, ARRAY, 92)
|
||||
UNTYPED_SFIELD(sfHookEmissions, ARRAY, 93)
|
||||
UNTYPED_SFIELD(sfImportVLKeys, ARRAY, 94)
|
||||
|
||||
@@ -74,7 +74,6 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, ({
|
||||
{sfTickSize, soeOPTIONAL},
|
||||
{sfNFTokenMinter, soeOPTIONAL},
|
||||
{sfHookStateScale, soeOPTIONAL},
|
||||
{sfAppLoader, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction type cancels an existing escrow. */
|
||||
@@ -501,22 +500,6 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
|
||||
{sfDomainID, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** featureRNG: a validator's entropy contribution for one ledger.
|
||||
|
||||
Each proposing UNLReport validator places exactly one of these in its own
|
||||
initial position when it closes the ledger. It reveals the secret behind
|
||||
the validator's commitment currently stored in the Random object (if any)
|
||||
and commits to the secret it will reveal next time. It never enters the
|
||||
open ledger, so a reveal only becomes public after the revealing
|
||||
validator's transaction set is locked.
|
||||
*/
|
||||
TRANSACTION(ttENTROPY, 89, Entropy, ({
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfPublicKey, soeREQUIRED},
|
||||
{sfNextRandomDigest, soeREQUIRED},
|
||||
{sfRandomData, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/* sfAccount is supplied by TxFormats::commonFields; listing it here as well
|
||||
makes the SOTemplate reject the format at construction. */
|
||||
TRANSACTION(ttMANIFEST_SET, 91, SetManifest, ({
|
||||
|
||||
@@ -238,7 +238,7 @@ JSS(closed); // out: NetworkOPs, LedgerToJson,
|
||||
// handlers/Ledger
|
||||
JSS(closed_ledger); // out: NetworkOPs
|
||||
JSS(cluster); // out: PeerImp
|
||||
JSS(code); // in: HookValidate; out: errors
|
||||
JSS(code); // out: errors
|
||||
JSS(command); // in: RPCHandler
|
||||
JSS(complete); // out: NetworkOPs, InboundLedger
|
||||
JSS(complete_ledgers); // out: NetworkOPs, PeerImp
|
||||
@@ -378,8 +378,6 @@ JSS(index); // in: LedgerEntry,
|
||||
// LedgerEntry, TxHistory, LedgerData
|
||||
JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo
|
||||
JSS(initial_sync_duration_us);
|
||||
JSS(instruction_count_cbak); // out: HookValidate
|
||||
JSS(instruction_count_hook); // out: HookValidate
|
||||
JSS(internal_command); // in: Internal
|
||||
JSS(invalid_API_version); // out: Many, when a request has an invalid
|
||||
// version
|
||||
@@ -453,7 +451,6 @@ JSS(local); // out: resource/Logic.h
|
||||
JSS(local_txs); // out: GetCounts
|
||||
JSS(local_static_keys); // out: ValidatorList
|
||||
JSS(locked_balance); // out: AccountLines
|
||||
JSS(log); // out: HookValidate
|
||||
JSS(low); // out: BookChanges
|
||||
JSS(lock_count); // out: AccountLines
|
||||
JSS(lowest_sequence); // out: AccountInfo
|
||||
@@ -592,6 +589,7 @@ JSS(queue_data); // out: AccountInfo
|
||||
JSS(queued); // out: SubmitTransaction
|
||||
JSS(queued_duration_us);
|
||||
JSS(quote_asset); // in: get_aggregate_price
|
||||
JSS(random); // out: Random
|
||||
JSS(raw_meta); // out: AcceptedLedgerTx
|
||||
JSS(receive_currencies); // out: AccountCurrencies
|
||||
JSS(reference_level); // out: TxQ
|
||||
@@ -609,7 +607,6 @@ JSS(reserve_base_native); // out: NetworkOPs
|
||||
JSS(reserve_inc); // out: NetworkOPs
|
||||
JSS(reserve_inc_xrp); // out: NetworkOPs
|
||||
JSS(reserve_inc_native); // out: NetworkOPs
|
||||
JSS(reset); // in/out: PWAInfo
|
||||
JSS(response); // websocket
|
||||
JSS(result); // RPC
|
||||
JSS(ripple_lines); // out: NetworkOPs
|
||||
@@ -751,7 +748,6 @@ JSS(url_password); // in: Subscribe
|
||||
JSS(url_username); // in: Subscribe
|
||||
JSS(urlgravatar); //
|
||||
JSS(username); // in: Subscribe
|
||||
JSS(valid); // out: HookValidate
|
||||
JSS(validated); // out: NetworkOPs, RPCHelpers, AccountTx*
|
||||
// Tx
|
||||
JSS(validator_list_expires); // out: NetworkOps, ValidatorList
|
||||
@@ -773,7 +769,6 @@ JSS(validator_sites); // out: ValidatorSites
|
||||
JSS(value); // out: STAmount
|
||||
JSS(version); // out: RPCVersion
|
||||
JSS(vetoed); // out: AmendmentTableImpl
|
||||
JSS(vm_error); // out: HookValidate
|
||||
JSS(volume_a); // out: BookChanges
|
||||
JSS(volume_b); // out: BookChanges
|
||||
JSS(vote); // in: Feature
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
#include <xrpl/basics/BasicConfig.h>
|
||||
#include <xrpl/beast/net/IPEndpoint.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/network_v4.hpp>
|
||||
#include <boost/asio/ip/network_v6.hpp>
|
||||
@@ -58,12 +57,6 @@ struct Port
|
||||
std::vector<boost::asio::ip::network_v6> admin_nets_v6;
|
||||
std::vector<boost::asio::ip::network_v4> secure_gateway_nets_v4;
|
||||
std::vector<boost::asio::ip::network_v6> secure_gateway_nets_v6;
|
||||
// "pwa" ports only; see the "pwa ports" section of xahaud-example.cfg.
|
||||
std::set<std::string> pwa_domains; // zones, lower case, no trailing dot
|
||||
std::set<AccountID> pwa_allowlist; // if non-empty, only these are served
|
||||
std::set<AccountID> pwa_blocklist; // never served; wins over allowlist
|
||||
bool pwa_custom_domains = false; // serve hosts named by TXT records
|
||||
std::optional<beast::IP::Endpoint> pwa_resolver; // for those; else system
|
||||
std::string user;
|
||||
std::string password;
|
||||
std::string admin_user;
|
||||
@@ -133,9 +126,6 @@ struct ParsedPort
|
||||
std::vector<boost::asio::ip::network_v6> admin_nets_v6;
|
||||
std::vector<boost::asio::ip::network_v4> secure_gateway_nets_v4;
|
||||
std::vector<boost::asio::ip::network_v6> secure_gateway_nets_v6;
|
||||
std::set<std::string> pwa_domains;
|
||||
bool pwa_custom_domains = false;
|
||||
std::optional<beast::IP::Endpoint> pwa_resolver;
|
||||
};
|
||||
|
||||
void
|
||||
|
||||
@@ -298,7 +298,7 @@ Door<Handler>::Door(
|
||||
port_.protocol.count("peer") > 0)
|
||||
, plain_(
|
||||
port_.protocol.count("http") > 0 || port_.protocol.count("ws") > 0 ||
|
||||
port_.protocol.count("ws2") || port_.protocol.count("pwa"))
|
||||
port_.protocol.count("ws2"))
|
||||
{
|
||||
reOpen();
|
||||
}
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/UTF8.h>
|
||||
#include <xrpl/protocol/AppLoader.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace ripple {
|
||||
namespace appLoader {
|
||||
|
||||
namespace {
|
||||
|
||||
// HTML "ASCII whitespace": TAB, LF, FF, CR, SPACE.
|
||||
inline bool
|
||||
isSpace(std::uint8_t c) noexcept
|
||||
{
|
||||
return c == 0x09 || c == 0x0A || c == 0x0C || c == 0x0D || c == 0x20;
|
||||
}
|
||||
|
||||
inline std::uint8_t
|
||||
lower(std::uint8_t c) noexcept
|
||||
{
|
||||
return (c >= 'A' && c <= 'Z') ? static_cast<std::uint8_t>(c + 32) : c;
|
||||
}
|
||||
|
||||
// Case-insensitive match of lower-case `lit` at `pos`; never reads past
|
||||
// the end.
|
||||
bool
|
||||
matchAt(
|
||||
std::uint8_t const* data,
|
||||
std::size_t size,
|
||||
std::size_t pos,
|
||||
std::string_view lit) noexcept
|
||||
{
|
||||
if (pos > size || size - pos < lit.size())
|
||||
return false;
|
||||
for (std::size_t i = 0; i < lit.size(); ++i)
|
||||
if (lower(data[pos + i]) != static_cast<std::uint8_t>(lit[i]))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
findFrom(
|
||||
std::uint8_t const* data,
|
||||
std::size_t size,
|
||||
std::size_t from,
|
||||
std::string_view lit) noexcept
|
||||
{
|
||||
for (std::size_t i = from; i + lit.size() <= size; ++i)
|
||||
if (matchAt(data, size, i, lit))
|
||||
return i;
|
||||
return size;
|
||||
}
|
||||
|
||||
// So that "<htmlish>" is not mistaken for "<html>".
|
||||
inline bool
|
||||
isTagNameEnd(std::uint8_t c) noexcept
|
||||
{
|
||||
return isSpace(c) || c == '>' || c == '/';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result
|
||||
validate(std::uint8_t const* data, std::size_t size)
|
||||
{
|
||||
if (size == 0 || data == nullptr)
|
||||
return Result::empty;
|
||||
if (size > maxAppLoaderLength)
|
||||
return Result::tooLarge;
|
||||
if (!isValidUTF8(Slice(data, size)))
|
||||
return Result::badUTF8;
|
||||
for (std::size_t i = 0; i < size; ++i)
|
||||
if (!isSpace(data[i]) && (data[i] < 0x20 || data[i] == 0x7F))
|
||||
return Result::badControlChar;
|
||||
|
||||
std::size_t begin = 0;
|
||||
if (size >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF)
|
||||
begin = 3;
|
||||
while (begin < size && isSpace(data[begin]))
|
||||
++begin;
|
||||
if (begin == size)
|
||||
return Result::empty;
|
||||
|
||||
// "<!doctype" followed by whitespace is the only doctype form any
|
||||
// browser treats as standards-mode HTML.
|
||||
bool const doctype = matchAt(data, size, begin, "<!doctype") &&
|
||||
begin + 9 < size && isSpace(data[begin + 9]);
|
||||
bool const html = matchAt(data, size, begin, "<html") && begin + 5 < size &&
|
||||
isTagNameEnd(data[begin + 5]);
|
||||
if (!doctype && !html)
|
||||
return Result::noDoctype;
|
||||
|
||||
std::size_t open = begin;
|
||||
for (;;)
|
||||
{
|
||||
open = findFrom(data, size, open, "<html");
|
||||
if (open == size)
|
||||
return Result::noHtmlElement;
|
||||
if (open + 5 < size && isTagNameEnd(data[open + 5]))
|
||||
break;
|
||||
++open;
|
||||
}
|
||||
|
||||
// The LAST well-formed </html>, so that one quoted inside a script
|
||||
// does not shadow the real one. Linear: each probe resumes one past
|
||||
// the previous match.
|
||||
// closeEnd == 0 means none found: a well-formed </html> always ends
|
||||
// past position 0.
|
||||
std::size_t closeEnd = 0;
|
||||
for (std::size_t probe = open;;)
|
||||
{
|
||||
std::size_t const found = findFrom(data, size, probe, "</html");
|
||||
if (found == size)
|
||||
break;
|
||||
std::size_t j = found + 6;
|
||||
while (j < size && isSpace(data[j]))
|
||||
++j;
|
||||
if (j < size && data[j] == '>')
|
||||
closeEnd = j + 1;
|
||||
probe = found + 1;
|
||||
}
|
||||
if (closeEnd == 0)
|
||||
return Result::unclosed;
|
||||
|
||||
for (std::size_t i = closeEnd; i < size; ++i)
|
||||
if (!isSpace(data[i]))
|
||||
return Result::trailingGarbage;
|
||||
return Result::ok;
|
||||
}
|
||||
|
||||
char const*
|
||||
to_string(Result r)
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
case Result::ok:
|
||||
return "ok";
|
||||
case Result::empty:
|
||||
return "empty document";
|
||||
case Result::tooLarge:
|
||||
return "document exceeds maxAppLoaderLength";
|
||||
case Result::badUTF8:
|
||||
return "malformed UTF-8";
|
||||
case Result::badControlChar:
|
||||
return "forbidden control character";
|
||||
case Result::noDoctype:
|
||||
return "does not open with a doctype or <html> tag";
|
||||
case Result::noHtmlElement:
|
||||
return "missing <html> start tag";
|
||||
case Result::unclosed:
|
||||
return "missing or misplaced </html> end tag";
|
||||
case Result::trailingGarbage:
|
||||
return "content after </html>";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
} // namespace appLoader
|
||||
} // namespace ripple
|
||||
@@ -81,7 +81,6 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
IMPORT_VLSEQ = 'I',
|
||||
UNL_REPORT = 'R',
|
||||
CRON = 'L',
|
||||
APP_LOADER = 'W',
|
||||
AMM = 'A',
|
||||
BRIDGE = LEDGER_NAMESPACE2(0x01, 'H'),
|
||||
XCHAIN_CLAIM_ID = 'Q',
|
||||
@@ -91,10 +90,8 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
MPTOKEN_ISSUANCE = '~',
|
||||
MPTOKEN = 't',
|
||||
MANIFEST = 'M',
|
||||
MANIFEST_DIR = LEDGER_NAMESPACE2(0x01, 'M'),
|
||||
CREDENTIAL = LEDGER_NAMESPACE2(0x01, 'D'),
|
||||
PERMISSIONED_DOMAIN = 'm',
|
||||
RANDOM = 0x526E, // Rn
|
||||
|
||||
// No longer used or supported. Left here to reserve the space
|
||||
// to avoid accidental reuse.
|
||||
@@ -279,13 +276,6 @@ UNLReport() noexcept
|
||||
return ret;
|
||||
}
|
||||
|
||||
Keylet const&
|
||||
random() noexcept
|
||||
{
|
||||
static Keylet const ret{ltRANDOM, indexHash(LedgerNameSpace::RANDOM)};
|
||||
return ret;
|
||||
}
|
||||
|
||||
Keylet
|
||||
book_t::operator()(Book const& b) const
|
||||
{
|
||||
@@ -623,12 +613,6 @@ did(AccountID const& account) noexcept
|
||||
return {ltDID, indexHash(LedgerNameSpace::DID, account)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
appLoader(AccountID const& account) noexcept
|
||||
{
|
||||
return {ltAPP_LOADER, indexHash(LedgerNameSpace::APP_LOADER, account)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
oracle(AccountID const& account, std::uint32_t const& documentID) noexcept
|
||||
{
|
||||
@@ -693,14 +677,6 @@ manifest(PublicKey const& pk) noexcept
|
||||
return {ltMANIFEST, indexHash(LedgerNameSpace::MANIFEST, pk.slice())};
|
||||
}
|
||||
|
||||
Keylet const&
|
||||
manifestDir() noexcept
|
||||
{
|
||||
static Keylet const ret{
|
||||
ltDIR_NODE, indexHash(LedgerNameSpace::MANIFEST_DIR)};
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace ripple
|
||||
@@ -143,14 +143,6 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfAccount, soeOPTIONAL},
|
||||
});
|
||||
|
||||
add(sfRandomDigestEntry.jsonName,
|
||||
sfRandomDigestEntry.getCode(),
|
||||
{
|
||||
{sfPublicKey, soeREQUIRED},
|
||||
{sfNextRandomDigest, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
});
|
||||
|
||||
add(sfAmountEntry.jsonName,
|
||||
sfAmountEntry.getCode(),
|
||||
{
|
||||
|
||||
@@ -684,8 +684,7 @@ isPseudoTx(STObject const& tx)
|
||||
|
||||
auto tt = safe_cast<TxType>(*t);
|
||||
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY ||
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON ||
|
||||
tt == ttENTROPY;
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -153,7 +153,6 @@ transResults()
|
||||
MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE, "The LedgerFixType field has an invalid value."),
|
||||
MAKE_ERROR(tefPAST_MANIFEST_SEQ, "The submitted manifest's sequence is not newer than the current."),
|
||||
MAKE_ERROR(tefREVOKED_MANIFEST, "The submitted manifest is for a revoked master key."),
|
||||
MAKE_ERROR(tefMANIFEST_KEY_IN_USE, "A key the submitted manifest names is already in use by another manifest."),
|
||||
|
||||
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
|
||||
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
|
||||
|
||||
@@ -49,9 +49,6 @@ TxMeta::TxMeta(
|
||||
|
||||
if (obj.isFieldPresent(sfHookEmissions))
|
||||
setHookEmissions(obj.getFieldArray(sfHookEmissions));
|
||||
|
||||
if (obj.isFieldPresent(sfRandomData))
|
||||
setRandomData(obj.getFieldH256(sfRandomData));
|
||||
}
|
||||
|
||||
TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj)
|
||||
@@ -78,9 +75,6 @@ TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj)
|
||||
|
||||
if (obj.isFieldPresent(sfHookEmissions))
|
||||
setHookEmissions(obj.getFieldArray(sfHookEmissions));
|
||||
|
||||
if (obj.isFieldPresent(sfRandomData))
|
||||
setRandomData(obj.getFieldH256(sfRandomData));
|
||||
}
|
||||
|
||||
TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, Blob const& vec)
|
||||
@@ -251,9 +245,6 @@ TxMeta::getAsObject() const
|
||||
if (hasHookEmissions())
|
||||
metaData.setFieldArray(sfHookEmissions, getHookEmissions());
|
||||
|
||||
if (hasRandomData())
|
||||
metaData.setFieldH256(sfRandomData, getRandomData());
|
||||
|
||||
return metaData;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,7 @@
|
||||
#include <xrpl/beast/core/LexicalCast.h>
|
||||
#include <xrpl/beast/rfc2616.h>
|
||||
#include <xrpl/server/Port.h>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/algorithm/string/classification.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/split.hpp>
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
@@ -69,14 +66,6 @@ operator<<(std::ostream& os, Port const& p)
|
||||
}
|
||||
}
|
||||
|
||||
if (!p.pwa_domains.empty())
|
||||
{
|
||||
os << "domains:";
|
||||
for (auto const& d : p.pwa_domains)
|
||||
os << " " << d;
|
||||
os << ", ";
|
||||
}
|
||||
|
||||
if (p.secure_gateway_nets_v4.size() || p.secure_gateway_nets_v6.size())
|
||||
{
|
||||
os << "secure_gateway nets:";
|
||||
@@ -322,64 +311,6 @@ parse_Port(ParsedPort& port, Section const& section, std::ostream& log)
|
||||
port.secure_gateway_nets_v4,
|
||||
port.secure_gateway_nets_v6);
|
||||
|
||||
// pwa ports: the zone(s) documents are served under, normalised like a
|
||||
// Host header: lower case, no trailing dot, LDH labels of 1..63, with
|
||||
// room left for the longest "pwa<AccountIndex>" label in front.
|
||||
if (auto const optResult = section.get("domain"))
|
||||
{
|
||||
std::vector<std::string> domains;
|
||||
boost::algorithm::split(
|
||||
domains, *optResult, boost::algorithm::is_any_of(","));
|
||||
for (auto& domain : domains)
|
||||
{
|
||||
boost::algorithm::trim(domain);
|
||||
boost::algorithm::to_lower(domain);
|
||||
if (!domain.empty() && domain.back() == '.')
|
||||
domain.pop_back();
|
||||
if (domain.empty())
|
||||
continue;
|
||||
bool valid = domain.size() + 24 <= 253;
|
||||
std::vector<std::string> labels;
|
||||
boost::algorithm::split(
|
||||
labels, domain, boost::algorithm::is_any_of("."));
|
||||
for (auto const& l : labels)
|
||||
valid = valid && !l.empty() && l.size() <= 63 &&
|
||||
l.front() != '-' && l.back() != '-' &&
|
||||
l.find_first_not_of(
|
||||
"abcdefghijklmnopqrstuvwxyz0123456789-") ==
|
||||
std::string::npos;
|
||||
if (!valid)
|
||||
{
|
||||
log << "Invalid value '" << domain << "' for key 'domain' in ["
|
||||
<< section.name()
|
||||
<< "]: expected a DNS zone such as pwapp.example";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
port.pwa_domains.insert(domain);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom domains on pwa ports, and the IPv4 recursive resolver to use for
|
||||
// their TXT records (default: the system resolver).
|
||||
if (auto const v = section.get("custom_domains"))
|
||||
if (!beast::lexicalCastChecked(port.pwa_custom_domains, *v))
|
||||
{
|
||||
log << "Invalid value '" << *v << "' for key 'custom_domains' in ["
|
||||
<< section.name() << "]: expected 0 or 1";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
if (auto const v = section.get("resolver"))
|
||||
{
|
||||
auto const ep = beast::IP::Endpoint::from_string_checked(*v);
|
||||
if (!ep || !ep->is_v4() || ep->address().is_unspecified())
|
||||
{
|
||||
log << "Invalid value '" << *v << "' for key 'resolver' in ["
|
||||
<< section.name() << "]: expected an IPv4 address[:port]";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
port.pwa_resolver = ep->port() ? *ep : ep->at_port(53);
|
||||
}
|
||||
|
||||
set(port.user, "user", section);
|
||||
set(port.password, "password", section);
|
||||
set(port.admin_user, "admin_user", section);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -24,7 +24,6 @@
|
||||
#include <xrpld/app/rdb/Wallet.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/protocol/STExchange.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
@@ -976,482 +975,9 @@ public:
|
||||
".example.com"));
|
||||
}
|
||||
|
||||
struct Validator
|
||||
{
|
||||
SecretKey masterSecret;
|
||||
PublicKey master;
|
||||
SecretKey signingSecret;
|
||||
PublicKey signing;
|
||||
};
|
||||
|
||||
static Validator
|
||||
makeValidator()
|
||||
{
|
||||
auto const ms = randomSecretKey();
|
||||
auto const ss = randomSecretKey();
|
||||
return {
|
||||
ms,
|
||||
derivePublicKey(KeyType::ed25519, ms),
|
||||
ss,
|
||||
derivePublicKey(KeyType::secp256k1, ss)};
|
||||
}
|
||||
|
||||
/** The same master key with a fresh ephemeral key. */
|
||||
static Validator
|
||||
rotate(Validator const& v)
|
||||
{
|
||||
auto const ss = randomSecretKey();
|
||||
return {
|
||||
v.masterSecret,
|
||||
v.master,
|
||||
ss,
|
||||
derivePublicKey(KeyType::secp256k1, ss)};
|
||||
}
|
||||
|
||||
/** Another master key claiming the same ephemeral key. */
|
||||
static Validator
|
||||
impostor(Validator const& v)
|
||||
{
|
||||
auto const ms = randomSecretKey();
|
||||
return {
|
||||
ms,
|
||||
derivePublicKey(KeyType::ed25519, ms),
|
||||
v.signingSecret,
|
||||
v.signing};
|
||||
}
|
||||
|
||||
Manifest
|
||||
manifestFor(Validator const& v, int seq, bool invalidSig = false)
|
||||
{
|
||||
return makeManifest(
|
||||
v.masterSecret,
|
||||
KeyType::ed25519,
|
||||
v.signingSecret,
|
||||
KeyType::secp256k1,
|
||||
seq,
|
||||
invalidSig);
|
||||
}
|
||||
|
||||
static std::size_t
|
||||
offered(ManifestCache const& cache)
|
||||
{
|
||||
std::size_t n = 0;
|
||||
cache.for_each_gossip_manifest(
|
||||
[](std::size_t) {}, [&n](Manifest const&) { ++n; });
|
||||
return n;
|
||||
}
|
||||
|
||||
void
|
||||
testGossipAdmission()
|
||||
{
|
||||
testcase("gossip admission");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
// Gossip for a master key not held, before its ephemeral key has
|
||||
// signed anything: held back, not admitted.
|
||||
auto const a = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(a, 1), gossip) == D::unseen);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == a.signing);
|
||||
BEAST_EXPECT(!cache.getTier(a.master));
|
||||
|
||||
// Its first validation releases it, once.
|
||||
auto const seq = cache.sequence();
|
||||
auto const released = cache.noteValidation(a.signing);
|
||||
BEAST_EXPECT(released && released->masterKey == a.master);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == a.master);
|
||||
BEAST_EXPECT(cache.getTier(a.master) == gossip);
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
BEAST_EXPECT(!cache.noteValidation(a.signing));
|
||||
|
||||
// Seen first, then gossiped: admitted at once.
|
||||
auto const b = makeValidator();
|
||||
BEAST_EXPECT(!cache.noteValidation(b.signing));
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(b, 1), gossip) == D::accepted);
|
||||
|
||||
// A master key held from gossip rotates without its new key having
|
||||
// been seen...
|
||||
auto const b2 = rotate(b);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(b2, 2), gossip) == D::accepted);
|
||||
BEAST_EXPECT(cache.getMasterKey(b2.signing) == b.master);
|
||||
BEAST_EXPECT(cache.getMasterKey(b.signing) == b.signing);
|
||||
|
||||
// ...and is revoked from gossip too, since that only takes trust away.
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
makeRevocation(b.masterSecret, KeyType::ed25519), gossip) ==
|
||||
D::accepted);
|
||||
BEAST_EXPECT(cache.revoked(b.master));
|
||||
BEAST_EXPECT(cache.getMasterKey(b2.signing) == b2.signing);
|
||||
BEAST_EXPECT(cache.getTier(b.master) == gossip);
|
||||
|
||||
// A revocation for a master key not held names no ephemeral key that
|
||||
// could have been seen, so gossip cannot introduce one...
|
||||
auto const c = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
makeRevocation(c.masterSecret, KeyType::ed25519), gossip) ==
|
||||
D::unseen);
|
||||
BEAST_EXPECT(!cache.getTier(c.master));
|
||||
|
||||
// ...but a list can.
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(makeRevocation(
|
||||
c.masterSecret, KeyType::ed25519)) == D::accepted);
|
||||
BEAST_EXPECT(cache.getTier(c.master) == ManifestSource::list);
|
||||
BEAST_EXPECT(cache.revoked(c.master));
|
||||
|
||||
// A forgery waits unverified, and is dropped when released.
|
||||
auto const d = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(d, 1, true), gossip) == D::unseen);
|
||||
BEAST_EXPECT(!cache.noteValidation(d.signing));
|
||||
BEAST_EXPECT(!cache.getTier(d.master));
|
||||
|
||||
// A key is remembered for three to four epochs of the filter.
|
||||
auto const e = makeValidator();
|
||||
cache.noteValidation(e.signing);
|
||||
clock.advance(ManifestCache::seenInterval * 3);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(e, 1), gossip) == D::accepted);
|
||||
|
||||
auto const f = makeValidator();
|
||||
cache.noteValidation(f.signing);
|
||||
clock.advance(ManifestCache::seenInterval * 4);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(f, 1), gossip) == D::unseen);
|
||||
}
|
||||
|
||||
void
|
||||
testPendingSubstitution()
|
||||
{
|
||||
testcase("pending ring: forged replacement");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
// Same master and signing key, higher sequence, bad master signature.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 2, true), gossip) ==
|
||||
D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->sequence == 1);
|
||||
BEAST_EXPECT(cache.getMasterKey(v.signing) == v.master);
|
||||
}
|
||||
|
||||
// Different master claiming the same signing key, bad master signature.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
manifestFor(impostor(v), 2, true), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->masterKey == v.master);
|
||||
BEAST_EXPECT(cache.getMasterKey(v.signing) == v.master);
|
||||
}
|
||||
|
||||
// A forgery that arrives first, for the same master key with a
|
||||
// higher sequence, does not keep out the genuine manifest.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 2, true), gossip) ==
|
||||
D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->sequence == 1);
|
||||
BEAST_EXPECT(cache.getMasterKey(v.signing) == v.master);
|
||||
}
|
||||
|
||||
// Nor does one that arrives first for another master key.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
manifestFor(impostor(v), 2, true), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->masterKey == v.master);
|
||||
BEAST_EXPECT(cache.getMasterKey(v.signing) == v.master);
|
||||
}
|
||||
|
||||
// A genuine manifest waiting is not displaced by an older genuine one.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 2), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->sequence == 2);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testGossipCapacity()
|
||||
{
|
||||
testcase("gossip capacity and decay");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
std::vector<Validator> held;
|
||||
for (std::size_t i = 0; i < ManifestCache::gossipCapacity; ++i)
|
||||
{
|
||||
held.push_back(makeValidator());
|
||||
cache.noteValidation(held.back().signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(held.back(), 1), gossip) ==
|
||||
D::accepted);
|
||||
}
|
||||
BEAST_EXPECT(offered(cache) == ManifestCache::gossipCapacity);
|
||||
|
||||
// Every entry is exactly as active as a newcomer: none gives way.
|
||||
auto const newcomer = makeValidator();
|
||||
cache.noteValidation(newcomer.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(newcomer, 1), gossip) == D::full);
|
||||
|
||||
// Four half-lives on, all but the first have gone on validating. The
|
||||
// first has decayed to a sixteenth of a sighting, the rest stand at
|
||||
// seventeen sixteenths, and a newcomer is worth a whole one: it
|
||||
// displaces the first and nothing else.
|
||||
clock.advance(ManifestCache::halfLife * 4);
|
||||
for (std::size_t i = 1; i < held.size(); ++i)
|
||||
cache.noteValidation(held[i].signing);
|
||||
|
||||
auto const seq = cache.sequence();
|
||||
cache.noteValidation(newcomer.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(newcomer, 1), gossip) ==
|
||||
D::accepted);
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
BEAST_EXPECT(!cache.getTier(held[0].master));
|
||||
BEAST_EXPECT(cache.getMasterKey(held[0].signing) == held[0].signing);
|
||||
for (std::size_t i = 1; i < held.size(); ++i)
|
||||
BEAST_EXPECT(cache.getTier(held[i].master) == gossip);
|
||||
BEAST_EXPECT(offered(cache) == ManifestCache::gossipCapacity);
|
||||
|
||||
// The newcomer is now the least active, but no less active than
|
||||
// another newcomer, which therefore finds no room.
|
||||
auto const late = makeValidator();
|
||||
cache.noteValidation(late.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(late, 1), gossip) == D::full);
|
||||
|
||||
// Far enough on that the weights are rebased. Their order survives:
|
||||
// the one entry that stopped validating is the one displaced.
|
||||
clock.advance(ManifestCache::halfLife * 300);
|
||||
for (std::size_t i = 2; i < held.size(); ++i)
|
||||
cache.noteValidation(held[i].signing);
|
||||
cache.noteValidation(newcomer.signing);
|
||||
cache.noteValidation(late.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(late, 1), gossip) == D::accepted);
|
||||
BEAST_EXPECT(!cache.getTier(held[1].master));
|
||||
BEAST_EXPECT(cache.getTier(newcomer.master) == gossip);
|
||||
for (std::size_t i = 2; i < held.size(); ++i)
|
||||
BEAST_EXPECT(cache.getTier(held[i].master) == gossip);
|
||||
}
|
||||
|
||||
void
|
||||
testPinning()
|
||||
{
|
||||
testcase("pinning");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
std::vector<Validator> held;
|
||||
for (std::size_t i = 0; i < ManifestCache::gossipCapacity; ++i)
|
||||
{
|
||||
held.push_back(makeValidator());
|
||||
cache.noteValidation(held.back().signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(held.back(), 1), gossip) ==
|
||||
D::accepted);
|
||||
}
|
||||
|
||||
// Pinned, a gossip entry is retained as list and frees its slot.
|
||||
auto const seq = cache.sequence();
|
||||
cache.pin({held[0].master});
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
BEAST_EXPECT(cache.getTier(held[0].master) == ManifestSource::list);
|
||||
|
||||
auto const newcomer = makeValidator();
|
||||
cache.noteValidation(newcomer.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(newcomer, 1), gossip) ==
|
||||
D::accepted);
|
||||
for (auto const& v : held)
|
||||
BEAST_EXPECT(cache.getTier(v.master));
|
||||
|
||||
// Unpinned, it falls back into a full tier, which sheds its least
|
||||
// active entry: one of those that stopped validating.
|
||||
clock.advance(ManifestCache::halfLife);
|
||||
cache.noteValidation(held[0].signing);
|
||||
cache.noteValidation(newcomer.signing);
|
||||
cache.pin({});
|
||||
BEAST_EXPECT(cache.getTier(held[0].master) == gossip);
|
||||
BEAST_EXPECT(cache.getTier(newcomer.master) == gossip);
|
||||
std::size_t remaining = 0;
|
||||
for (std::size_t i = 1; i < held.size(); ++i)
|
||||
remaining += cache.getTier(held[i].master) ? 1 : 0;
|
||||
BEAST_EXPECT(remaining == held.size() - 2);
|
||||
BEAST_EXPECT(offered(cache) == ManifestCache::gossipCapacity);
|
||||
|
||||
// A master key a list vouched for stays in the list tier however it
|
||||
// is updated later, and is offered alongside gossip.
|
||||
auto const listed = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(listed, 1)) == D::accepted);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(rotate(listed), 2), gossip) ==
|
||||
D::accepted);
|
||||
BEAST_EXPECT(cache.getTier(listed.master) == ManifestSource::list);
|
||||
BEAST_EXPECT(offered(cache) == ManifestCache::gossipCapacity + 1);
|
||||
}
|
||||
|
||||
void
|
||||
testPrecedence()
|
||||
{
|
||||
testcase("tier precedence on key conflicts");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
// Gossip binds an ephemeral key to a master key...
|
||||
auto const a = makeValidator();
|
||||
cache.noteValidation(a.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(a, 1), gossip) == D::accepted);
|
||||
|
||||
// ...and a list binding the same key to another master key wins,
|
||||
// evicting it, where before it would have been refused.
|
||||
auto const b = impostor(a);
|
||||
BEAST_EXPECT(cache.applyManifest(manifestFor(b, 1)) == D::accepted);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == b.master);
|
||||
BEAST_EXPECT(!cache.getTier(a.master));
|
||||
|
||||
// Gossip cannot do the same to the list...
|
||||
auto const c = impostor(a);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(c, 1), gossip) ==
|
||||
D::badEphemeralKey);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == b.master);
|
||||
|
||||
// ...and between equals the first holds, as it always has.
|
||||
auto const d = impostor(a);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(d, 1)) == D::badEphemeralKey);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == b.master);
|
||||
}
|
||||
|
||||
void
|
||||
testPending()
|
||||
{
|
||||
testcase("pending");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
{
|
||||
// A newer manifest for a key already waiting replaces it.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 2), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 3), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->sequence == 3);
|
||||
BEAST_EXPECT(cache.getSequence(v.master) == 3);
|
||||
}
|
||||
|
||||
{
|
||||
// The ring holds pendingCapacity; the first to arrive makes way
|
||||
// for the one after.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
std::vector<Validator> waiting;
|
||||
for (std::size_t i = 0; i <= ManifestCache::pendingCapacity; ++i)
|
||||
{
|
||||
waiting.push_back(makeValidator());
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
manifestFor(waiting.back(), 1), gossip) == D::unseen);
|
||||
}
|
||||
|
||||
BEAST_EXPECT(!cache.noteValidation(waiting.front().signing));
|
||||
BEAST_EXPECT(!cache.getTier(waiting.front().master));
|
||||
|
||||
std::size_t released = 0;
|
||||
for (std::size_t i = 1; i < waiting.size(); ++i)
|
||||
released += cache.noteValidation(waiting[i].signing) ? 1 : 0;
|
||||
BEAST_EXPECT(released == ManifestCache::pendingCapacity);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testGossipAdmission();
|
||||
testGossipCapacity();
|
||||
testPendingSubstitution();
|
||||
testPinning();
|
||||
testPrecedence();
|
||||
testPending();
|
||||
|
||||
ManifestCache cache;
|
||||
{
|
||||
testcase("apply");
|
||||
|
||||
@@ -1,400 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/protocol/AppLoader.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class PWALoader_test : public beast::unit_test::suite
|
||||
{
|
||||
static appLoader::Result
|
||||
check(std::string const& s)
|
||||
{
|
||||
return appLoader::validate(
|
||||
reinterpret_cast<std::uint8_t const*>(s.data()), s.size());
|
||||
}
|
||||
|
||||
static std::string
|
||||
docOfSize(std::size_t len)
|
||||
{
|
||||
std::string const head = "<!DOCTYPE html><html><body>";
|
||||
std::string const tail = "</body></html>";
|
||||
if (len < head.size() + tail.size())
|
||||
return head + tail;
|
||||
return head + std::string(len - head.size() - tail.size(), 'x') + tail;
|
||||
}
|
||||
|
||||
static std::string
|
||||
doc()
|
||||
{
|
||||
return "<!DOCTYPE html><html><body>hi</body></html>";
|
||||
}
|
||||
|
||||
// AccountSet carrying sfAppLoader.
|
||||
static Json::Value
|
||||
set(jtx::Account const& a, std::string const& d)
|
||||
{
|
||||
auto jt = jtx::noop(a);
|
||||
jt[sfAppLoader.fieldName] = strHex(d);
|
||||
return jt;
|
||||
}
|
||||
|
||||
// validate() is consensus critical: these pin its edges, so a refactor
|
||||
// that moves any of them fails here rather than forking the network.
|
||||
void
|
||||
testValidator()
|
||||
{
|
||||
testcase("validator");
|
||||
using R = appLoader::Result;
|
||||
using namespace std::string_literals;
|
||||
|
||||
for (auto const& s :
|
||||
{"<!DOCTYPE html><html></html>"s,
|
||||
"<!doctype html><html></html>"s,
|
||||
"<html></html>"s,
|
||||
"<HTML></HTML>"s,
|
||||
"<html lang=\"en\" class=\"root\"></html>"s,
|
||||
" \n\t<html></html>\n\n "s,
|
||||
"<html></html >"s,
|
||||
"<html></html\n>"s,
|
||||
"\xEF\xBB\xBF<html></html>"s,
|
||||
"\xEF\xBB\xBF<!DOCTYPE html><html></html>"s,
|
||||
"<html><body>\xE6\xBC\xA2\xE5\xAD\x97</body></html>"s,
|
||||
"<html><script>var s=\"</html>\";</script></html>"s,
|
||||
"<html>\t\n\r\f</html>"s,
|
||||
"<html></html> \n"s,
|
||||
"<!DOCTYPE html>\n<!-- c --><html></html>"s,
|
||||
"<html>\xC2\x85</html>"s, // C1 controls are not rejected
|
||||
docOfSize(maxAppLoaderLength)})
|
||||
BEAST_EXPECTS(check(s) == R::ok, s);
|
||||
|
||||
std::vector<std::pair<std::string, R>> const bad{
|
||||
{""s, R::empty},
|
||||
{" \n"s, R::empty},
|
||||
{"\xEF\xBB\xBF"s, R::empty},
|
||||
{docOfSize(maxAppLoaderLength + 1), R::tooLarge},
|
||||
{"<html>\xC0\xAF</html>"s, R::badUTF8},
|
||||
{"<html>\xED\xA0\x80</html>"s, R::badUTF8},
|
||||
{"<html>\xEF\xBF\xBE</html>"s, R::badUTF8},
|
||||
{"<html>\xE6\xBC</html>"s, R::badUTF8},
|
||||
{"<html></html>\xE6"s, R::badUTF8},
|
||||
{"<html>\0</html>"s, R::badControlChar},
|
||||
{"<html>\x1B</html>"s, R::badControlChar},
|
||||
{"<html>\x7F</html>"s, R::badControlChar},
|
||||
{"hello"s, R::noDoctype},
|
||||
{"<div>frag</div>"s, R::noDoctype},
|
||||
{"<htmlish></html>"s, R::noDoctype},
|
||||
{"<!DOCTYPEhtml><html></html>"s, R::noDoctype},
|
||||
{"<!DOCTYPE"s, R::noDoctype},
|
||||
{"<!DOCTYPE html>"s, R::noHtmlElement},
|
||||
{"<!DOCTYPE html><htmlish></html>"s, R::noHtmlElement},
|
||||
{"<html>"s, R::unclosed},
|
||||
{"<html></html"s, R::unclosed},
|
||||
{"<html></htmlx>"s, R::unclosed},
|
||||
{"<html></html>trailing"s, R::trailingGarbage},
|
||||
{"<html></html><p>"s, R::trailingGarbage},
|
||||
{"<html></html>\xC2\xA0"s, R::trailingGarbage}, // NBSP
|
||||
};
|
||||
for (auto const& [s, r] : bad)
|
||||
BEAST_EXPECTS(
|
||||
check(s) == r, s + ": " + appLoader::to_string(check(s)));
|
||||
}
|
||||
|
||||
void
|
||||
testEnabled()
|
||||
{
|
||||
testcase("amendment gate");
|
||||
using namespace jtx;
|
||||
Env env{*this, supported_amendments() - featurePWALoader};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
env(set(alice, doc()), fee(XRP(1)), ter(temDISABLED));
|
||||
env(set(alice, ""), fee(XRP(1)), ter(temDISABLED));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::appLoader(alice.id())));
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAppLoaderSequence));
|
||||
}
|
||||
|
||||
void
|
||||
testSetAndRemove()
|
||||
{
|
||||
testcase("set, overwrite, remove, sequence");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
auto const kl = keylet::appLoader(alice.id());
|
||||
auto seq = [&] {
|
||||
return (*env.le(alice))[~sfAppLoaderSequence].value_or(0);
|
||||
};
|
||||
|
||||
BEAST_EXPECT(!env.le(kl));
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAppLoaderSequence));
|
||||
|
||||
// Create: the object, with the owner's new sequence; nothing inline
|
||||
// on the AccountRoot, no reserve, not in the owner directory.
|
||||
env(set(alice, doc()), fee(XRP(1)));
|
||||
env.close();
|
||||
auto sle = env.le(kl);
|
||||
if (BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT((*sle)[sfAppLoader] == makeSlice(doc()));
|
||||
BEAST_EXPECT((*sle)[sfOwner] == alice.id());
|
||||
BEAST_EXPECT((*sle)[sfAppLoaderSequence] == 1);
|
||||
}
|
||||
BEAST_EXPECT(seq() == 1);
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAppLoader));
|
||||
BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 0);
|
||||
BEAST_EXPECT(
|
||||
env.rpc(
|
||||
"json",
|
||||
"account_objects",
|
||||
R"({"account":")" + alice.human() +
|
||||
R"("})")[jss::result][jss::account_objects]
|
||||
.size() == 0);
|
||||
auto const txid = (*sle)[sfPreviousTxnID];
|
||||
|
||||
// Overwrite: new content, next sequence, rethreaded.
|
||||
env(set(alice, "<html><body>v2</body></html>"), fee(XRP(1)));
|
||||
env.close();
|
||||
sle = env.le(kl);
|
||||
if (BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
(*sle)[sfAppLoader] ==
|
||||
makeSlice(std::string("<html><body>v2</body></html>")));
|
||||
BEAST_EXPECT((*sle)[sfAppLoaderSequence] == 2);
|
||||
BEAST_EXPECT((*sle)[sfPreviousTxnID] != txid);
|
||||
}
|
||||
BEAST_EXPECT(seq() == 2);
|
||||
|
||||
// An AccountSet without the field leaves everything alone.
|
||||
env(noop(alice));
|
||||
env.close();
|
||||
BEAST_EXPECT((*env.le(kl))[sfAppLoaderSequence] == 2 && seq() == 2);
|
||||
|
||||
// Remove with an empty blob: the object goes, the sequence still
|
||||
// advances (so a copy of sequence 2 is known to be stale), and
|
||||
// removing again is a no-op that advances it again.
|
||||
env(set(alice, ""), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(kl) && seq() == 3);
|
||||
env(set(alice, ""), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(kl) && seq() == 4);
|
||||
|
||||
// Republish: the sequence carries on.
|
||||
env(set(alice, doc()), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT((*env.le(kl))[sfAppLoaderSequence] == 5 && seq() == 5);
|
||||
|
||||
// ledger_entry reaches it by account or by index.
|
||||
for (auto const& req :
|
||||
{R"({"app_loader":")" + alice.human() + R"("})",
|
||||
R"({"index":")" + to_string(kl.key) + R"("})"})
|
||||
{
|
||||
auto const r = env.rpc("json", "ledger_entry", req)[jss::result];
|
||||
BEAST_EXPECTS(
|
||||
r[jss::node][sfLedgerEntryType.fieldName] == jss::AppLoader &&
|
||||
r[jss::node][sfAppLoaderSequence.fieldName] == 5,
|
||||
req);
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
env.rpc(
|
||||
"json",
|
||||
"ledger_entry",
|
||||
R"({"app_loader":"nope"})")[jss::result][jss::error] ==
|
||||
"malformedAddress");
|
||||
}
|
||||
|
||||
void
|
||||
testFee()
|
||||
{
|
||||
testcase("fee: base plus one drop per byte");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
auto const base = env.current()->fees().base;
|
||||
auto const kl = keylet::appLoader(alice.id());
|
||||
|
||||
auto const expected = base + XRPAmount{1024};
|
||||
env(set(alice, docOfSize(1024)),
|
||||
fee(expected - XRPAmount{1}),
|
||||
ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(kl));
|
||||
auto const before = env.balance(alice);
|
||||
env(set(alice, docOfSize(1024)), fee(expected));
|
||||
env.close();
|
||||
BEAST_EXPECT(
|
||||
env.le(kl) && before - env.balance(alice) == drops(expected));
|
||||
|
||||
// Overwrites pay again; removal is base fee only.
|
||||
env(set(alice, docOfSize(100)),
|
||||
fee(base + XRPAmount{99}),
|
||||
ter(telINSUF_FEE_P));
|
||||
env(set(alice, docOfSize(100)), fee(base + XRPAmount{100}));
|
||||
env(set(alice, ""), fee(base));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(kl));
|
||||
|
||||
// Publishing takes no reserve: an account at its reserve floor can.
|
||||
Account const poor{"poor"};
|
||||
auto const reserve = env.current()->fees().accountReserve(0);
|
||||
env.fund(drops(reserve + base + XRPAmount{100}), noripple(poor));
|
||||
env.close();
|
||||
env(set(poor, docOfSize(100)), fee(base + XRPAmount{100}));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(keylet::appLoader(poor.id())));
|
||||
BEAST_EXPECT(env.balance(poor) == drops(reserve));
|
||||
env.require(owners(poor, 0));
|
||||
}
|
||||
|
||||
void
|
||||
testMalformed()
|
||||
{
|
||||
testcase("malformed documents are temMALFORMED");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
for (auto const& d :
|
||||
{std::string("hello"),
|
||||
std::string("<html>"),
|
||||
std::string("<html></html>x"),
|
||||
std::string("<html>\0</html>", 14),
|
||||
std::string("<html>\xC0\xAF</html>"),
|
||||
docOfSize(maxAppLoaderLength + 1)})
|
||||
{
|
||||
env(set(alice, d), fee(XRP(1)), ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
BEAST_EXPECT(!env.le(keylet::appLoader(alice.id())));
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAppLoaderSequence));
|
||||
}
|
||||
|
||||
void
|
||||
testAccountDelete()
|
||||
{
|
||||
testcase("account delete erases the loader");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"}, bob{"bob"};
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
env(set(alice, doc()), fee(XRP(1)));
|
||||
env.close();
|
||||
for (int i = 0; i < 256; ++i)
|
||||
env.close();
|
||||
// Not in the owner directory, so it cannot block deletion; erased
|
||||
// explicitly, or AccountRootsDeletedClean would fire on the orphan.
|
||||
env(acctdelete(alice, bob),
|
||||
fee(drops(env.current()->fees().increment)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(alice));
|
||||
BEAST_EXPECT(!env.le(keylet::appLoader(alice.id())));
|
||||
}
|
||||
|
||||
void
|
||||
testValidateRPC()
|
||||
{
|
||||
testcase("pwa_validate");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
auto validate = [&](std::string const& d, std::string const& domain) {
|
||||
Json::Value p(Json::objectValue);
|
||||
p[jss::account] = alice.human();
|
||||
p[jss::hash] = to_string(sha512Half(makeSlice(d)));
|
||||
if (!domain.empty())
|
||||
p[jss::domain] = domain;
|
||||
return env.rpc("json", "pwa_validate", to_string(p))[jss::result];
|
||||
};
|
||||
|
||||
BEAST_EXPECT(validate(doc(), "")["reason"] == "no loader");
|
||||
env(set(alice, doc()), fee(XRP(1)));
|
||||
env.close();
|
||||
auto r = validate(doc(), "");
|
||||
BEAST_EXPECT(r["valid"] == true && r["app_loader_sequence"] == 1);
|
||||
BEAST_EXPECT(r[jss::ledger_index] == env.closed()->seq());
|
||||
BEAST_EXPECT(
|
||||
validate("<html></html>", "")["reason"] == "hash mismatch");
|
||||
|
||||
// domain: the account's Domain, or a host beneath it, ignoring case.
|
||||
BEAST_EXPECT(
|
||||
validate(doc(), "pwa.test")["reason"] ==
|
||||
"domain not endorsed by account");
|
||||
auto jt = noop(alice);
|
||||
jt[sfDomain.fieldName] = strHex(std::string("Pwa.Test"));
|
||||
env(jt);
|
||||
env.close();
|
||||
for (auto const d :
|
||||
{"pwa.test", "PWA.TEST.", "pwaabcd.pwa.test", "a.b.pwa.test"})
|
||||
BEAST_EXPECTS(validate(doc(), d)["valid"] == true, d);
|
||||
for (auto const d : {"other.test", "xpwa.test", "pwa.test.evil"})
|
||||
BEAST_EXPECTS(validate(doc(), d)["valid"] == false, d);
|
||||
|
||||
// Malformed input.
|
||||
BEAST_EXPECT(
|
||||
env.rpc(
|
||||
"json",
|
||||
"pwa_validate",
|
||||
R"({"account":"x","hash":"00"})")[jss::result][jss::error] ==
|
||||
"actMalformed");
|
||||
BEAST_EXPECT(
|
||||
env.rpc(
|
||||
"pwa_validate", alice.human(), "zz")[jss::result][jss::error] ==
|
||||
"invalidParams");
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testValidator();
|
||||
testEnabled();
|
||||
testSetAndRemove();
|
||||
testFee();
|
||||
testMalformed();
|
||||
testAccountDelete();
|
||||
testValidateRPC();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(PWALoader, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,111 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/RotatingBloomFilter.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class RotatingBloomFilter_test : public beast::unit_test::suite
|
||||
{
|
||||
using time_point = std::chrono::steady_clock::time_point;
|
||||
|
||||
void
|
||||
testWindow()
|
||||
{
|
||||
testcase("window");
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
RotatingBloomFilter<1024, 4, 4> filter{10s};
|
||||
time_point const t{};
|
||||
std::string const a{"a"};
|
||||
std::string const b{"b"};
|
||||
|
||||
filter.insert(a, t);
|
||||
BEAST_EXPECT(filter.contains(a, t));
|
||||
BEAST_EXPECT(!filter.contains(b, t));
|
||||
|
||||
// Remembered to the end of the third epoch after its own, and
|
||||
// forgotten at the start of the fourth.
|
||||
BEAST_EXPECT(filter.contains(a, t + 39s));
|
||||
BEAST_EXPECT(!filter.contains(a, t + 40s));
|
||||
|
||||
// Inserting again extends it.
|
||||
filter.insert(a, t + 35s);
|
||||
BEAST_EXPECT(filter.contains(a, t + 69s));
|
||||
BEAST_EXPECT(!filter.contains(a, t + 70s));
|
||||
|
||||
// A clock that jumps finds everything expired.
|
||||
filter.insert(b, t + 70s);
|
||||
BEAST_EXPECT(!filter.contains(b, t + 1h));
|
||||
|
||||
// An insertion that read the clock before another thread rotated its
|
||||
// slot lands in the newer epoch rather than being lost.
|
||||
filter.insert(b, t + 1h + 40s);
|
||||
filter.insert(a, t + 1h);
|
||||
BEAST_EXPECT(filter.contains(a, t + 1h + 40s));
|
||||
}
|
||||
|
||||
void
|
||||
testFalsePositives()
|
||||
{
|
||||
testcase("false positives");
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using Filter = RotatingBloomFilter<std::size_t{1} << 14, 4, 4>;
|
||||
static_assert(Filter::bytes == 8192);
|
||||
|
||||
Filter filter{120s};
|
||||
time_point const t{};
|
||||
|
||||
std::uint64_t const inserted = 1000;
|
||||
for (std::uint64_t i = 0; i < inserted; ++i)
|
||||
filter.insert(i, t);
|
||||
|
||||
bool all = true;
|
||||
for (std::uint64_t i = 0; i < inserted; ++i)
|
||||
all = all && filter.contains(i, t);
|
||||
BEAST_EXPECT(all);
|
||||
|
||||
// About 0.24% in theory for a thousand elements in one generation.
|
||||
std::uint64_t const probes = 100000;
|
||||
std::uint64_t hits = 0;
|
||||
for (std::uint64_t i = inserted; i < inserted + probes; ++i)
|
||||
hits += filter.contains(i, t) ? 1 : 0;
|
||||
BEAST_EXPECT(hits < probes / 100);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testWindow();
|
||||
testFalsePositives();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(RotatingBloomFilter, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -6146,15 +6146,6 @@ private:
|
||||
pass();
|
||||
}
|
||||
|
||||
void
|
||||
testEntropyTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("entropy tsh");
|
||||
|
||||
// pseudo transaction
|
||||
pass();
|
||||
}
|
||||
|
||||
// | otxn | tfBurnable | tsh | mint | burn | buy | sell | cancel
|
||||
// | O | false | O | N/A | S | N/A | S | S
|
||||
// | O | false | I | N/A | N | N/A | W | N/A
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/ManifestLedger.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
#include <xrpld/app/tx/detail/SetManifest.h>
|
||||
@@ -554,99 +553,9 @@ struct SetManifest_test : public beast::unit_test::suite
|
||||
submit(env, makeManifest(master, ephemeral, 1));
|
||||
env.close();
|
||||
|
||||
// Every refusal over a key in use is tef. The transaction is unsigned
|
||||
// and consumes no sequence, so anyone holding it could resubmit it in
|
||||
// every ledger; a tec would charge the master account each time.
|
||||
auto const masterBalance = env.balance(master);
|
||||
auto const otherBalance = env.balance(other);
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(other, ephemeral, 1))) ==
|
||||
"tefMANIFEST_KEY_IN_USE");
|
||||
|
||||
// A newer manifest that keeps the current ephemeral key. Every
|
||||
// manifest cache refuses it, so the ledger must too, or it would stay
|
||||
// ahead of what any node believes.
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(master, ephemeral, 2))) ==
|
||||
"tefMANIFEST_KEY_IN_USE");
|
||||
|
||||
// A master key that is already another account's ephemeral key: the
|
||||
// two kinds of manifest keylet share one namespace.
|
||||
auto const third = Account("third", KeyType::ed25519);
|
||||
auto const thirdEph = Account("thirdEph", KeyType::ed25519);
|
||||
env.fund(XRP(1000), third);
|
||||
env.close();
|
||||
|
||||
auto const thirdBalance = env.balance(third);
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(other, third, 1))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(third, thirdEph, 1))) ==
|
||||
"tefMANIFEST_KEY_IN_USE");
|
||||
env.close();
|
||||
|
||||
// Nothing was charged and nothing was written.
|
||||
BEAST_EXPECT(env.balance(master) == masterBalance);
|
||||
BEAST_EXPECT(env.balance(third) == thirdBalance);
|
||||
BEAST_EXPECT(env.balance(other) < otherBalance);
|
||||
if (auto const sle = env.le(keylet::manifest(master.pk()));
|
||||
BEAST_EXPECT(sle))
|
||||
BEAST_EXPECT(sle->getFieldU32(sfSequence) == 1);
|
||||
if (auto const sle = env.le(keylet::manifest(third.pk()));
|
||||
BEAST_EXPECT(sle))
|
||||
BEAST_EXPECT(sle->getAccountID(sfAccount) == other.id());
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(thirdEph.pk())));
|
||||
BEAST_EXPECT(
|
||||
!env.le(keylet::account(third.id()))->isFieldPresent(sfManifestID));
|
||||
}
|
||||
|
||||
void
|
||||
testAccountDelete(FeatureBitset features)
|
||||
{
|
||||
testcase("account delete");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
|
||||
auto const plain = Account("plain", KeyType::ed25519);
|
||||
auto const dest = Account("dest");
|
||||
env.fund(XRP(1000), master, plain, dest);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(master, ephemeral, 1))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
// Old enough to delete, so the manifest is the only obstacle.
|
||||
incLgrSeqForAccDel(env, master);
|
||||
incLgrSeqForAccDel(env, plain);
|
||||
|
||||
// An on-ledger manifest outlives its account: neither copy is in the
|
||||
// owner directory, and the master copy stays listed in the manifest
|
||||
// directory for good.
|
||||
env(acctdelete(master, dest),
|
||||
fee(drops(env.current()->fees().increment)),
|
||||
ter(tecHAS_OBLIGATIONS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(keylet::account(master.id())));
|
||||
BEAST_EXPECT(env.le(keylet::manifest(master.pk())));
|
||||
BEAST_EXPECT(env.le(keylet::manifest(ephemeral.pk())));
|
||||
|
||||
// An account of the same age without one goes as usual.
|
||||
env(acctdelete(plain, dest),
|
||||
fee(drops(env.current()->fees().increment)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::account(plain.id())));
|
||||
"tecDUPLICATE");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -971,11 +880,6 @@ struct SetManifest_test : public beast::unit_test::suite
|
||||
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed(), all) == 4);
|
||||
|
||||
// Read from the ledger and pinned by nothing, all four are held as
|
||||
// ledger lookup cache entries.
|
||||
for (auto const& m : masters)
|
||||
BEAST_EXPECT(cache.getTier(m.pk()) == ManifestSource::ledger);
|
||||
|
||||
// The raw form is what a republishing node needs: the exact bytes the
|
||||
// master key signed, plus the sequence to compare against the ledger.
|
||||
auto const raw = cache.getRawManifest(masters[0].pk());
|
||||
@@ -991,237 +895,33 @@ struct SetManifest_test : public beast::unit_test::suite
|
||||
auto const seq = cache.sequence();
|
||||
cache.pin({masters[0].pk(), masters[1].pk()});
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
BEAST_EXPECT(cache.getTier(masters[0].pk()) == ManifestSource::list);
|
||||
|
||||
auto const seq2 = cache.sequence();
|
||||
cache.pin({masters[0].pk(), masters[1].pk()});
|
||||
BEAST_EXPECT(cache.sequence() == seq2);
|
||||
|
||||
// Only the pinned two are offered. The other two are on the ledger,
|
||||
// where any peer that is not amendment blocked can read them.
|
||||
// Everything is offered: two pinned plus two under the gossip limit.
|
||||
std::size_t reserved = 0;
|
||||
std::vector<PublicKey> offered;
|
||||
cache.for_each_gossip_manifest(
|
||||
[&](std::size_t n) { reserved = n; },
|
||||
[&](Manifest const& m) { offered.push_back(m.masterKey); });
|
||||
|
||||
BEAST_EXPECT(reserved == 2);
|
||||
BEAST_EXPECT(reserved == 4);
|
||||
BEAST_EXPECT(offered.size() == 4);
|
||||
BEAST_EXPECT(
|
||||
hash_set<PublicKey>(offered.begin(), offered.end()) ==
|
||||
hash_set<PublicKey>({masters[0].pk(), masters[1].pk()}));
|
||||
hash_set<PublicKey>(offered.begin(), offered.end()) == all);
|
||||
|
||||
// Unpinned, a master key falls back to the lookup cache. A pinned key
|
||||
// with no manifest is neither offered nor counted.
|
||||
// A pinned key with no manifest is counted in the reservation but not
|
||||
// offered, since the reservation is only an upper bound.
|
||||
cache.pin({masters[0].pk(), ephemeral.pk()});
|
||||
BEAST_EXPECT(cache.getTier(masters[1].pk()) == ManifestSource::ledger);
|
||||
offered.clear();
|
||||
cache.for_each_gossip_manifest(
|
||||
[&](std::size_t n) { reserved = n; },
|
||||
[&](Manifest const& m) { offered.push_back(m.masterKey); });
|
||||
|
||||
BEAST_EXPECT(reserved == 1);
|
||||
BEAST_EXPECT(offered.size() == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerOutranksGossip(FeatureBitset features)
|
||||
{
|
||||
testcase("ledger outranks gossip");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const eph1 = Account("eph1", KeyType::ed25519);
|
||||
auto const eph2 = Account("eph2", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master);
|
||||
env.close();
|
||||
|
||||
submit(env, makeManifest(master, eph1, 1));
|
||||
env.close();
|
||||
|
||||
auto const fromPeer = [](std::string const& s) {
|
||||
auto mo = deserializeManifest(s);
|
||||
return std::move(*mo);
|
||||
};
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
{
|
||||
// Gossip repeating what the ledger holds is stale, and it is the
|
||||
// ledger's copy that is kept, as a lookup cache entry.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{env.journal, clock};
|
||||
cache.noteValidation(eph1.pk());
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
fromPeer(makeManifest(master, eph1, 1)),
|
||||
gossip,
|
||||
env.closed().get()) == ManifestDisposition::stale);
|
||||
BEAST_EXPECT(cache.getTier(master.pk()) == ManifestSource::ledger);
|
||||
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == master.pk());
|
||||
}
|
||||
|
||||
submit(
|
||||
env,
|
||||
makeManifest(
|
||||
master, eph1, std::numeric_limits<std::uint32_t>::max()));
|
||||
env.close();
|
||||
|
||||
{
|
||||
// Revoked on-ledger, a master key cannot come back by gossip with
|
||||
// a fresh ephemeral key, even one seen validating, and even in a
|
||||
// cache that never held it.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{env.journal, clock};
|
||||
cache.noteValidation(eph2.pk());
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
fromPeer(makeManifest(master, eph2, 5)),
|
||||
gossip,
|
||||
env.closed().get()) == ManifestDisposition::stale);
|
||||
BEAST_EXPECT(cache.revoked(master.pk()));
|
||||
BEAST_EXPECT(cache.getTier(master.pk()) == ManifestSource::ledger);
|
||||
BEAST_EXPECT(cache.getMasterKey(eph2.pk()) == eph2.pk());
|
||||
}
|
||||
|
||||
{
|
||||
// A cache that took the gossip without a ledger to check it
|
||||
// against learns of the revocation at its next refresh, though it
|
||||
// never misses on the ephemeral key.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{env.journal, clock};
|
||||
cache.noteValidation(eph2.pk());
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
fromPeer(makeManifest(master, eph2, 5)), gossip) ==
|
||||
ManifestDisposition::accepted);
|
||||
BEAST_EXPECT(cache.getMasterKey(eph2.pk()) == master.pk());
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed()) == 1);
|
||||
BEAST_EXPECT(cache.revoked(master.pk()));
|
||||
BEAST_EXPECT(cache.getMasterKey(eph2.pk()) == eph2.pk());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDirectory(FeatureBitset features)
|
||||
{
|
||||
testcase("manifest directory");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
auto const revoke = std::numeric_limits<std::uint32_t>::max();
|
||||
|
||||
std::vector<Account> masters;
|
||||
std::vector<Account> ephs;
|
||||
for (int i = 0; i < 40; ++i)
|
||||
{
|
||||
masters.emplace_back("dm" + std::to_string(i), KeyType::ed25519);
|
||||
ephs.emplace_back("de" + std::to_string(i), KeyType::ed25519);
|
||||
env.fund(XRP(1000), masters.back());
|
||||
}
|
||||
env.close();
|
||||
|
||||
for (int i = 0; i < 40; ++i)
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[i], ephs[i], 1))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
auto const listed = [&]() {
|
||||
std::map<PublicKey, std::uint32_t> out;
|
||||
forEachLedgerManifest(
|
||||
*env.closed(), [&](std::shared_ptr<SLE const> const& sle) {
|
||||
out.emplace(
|
||||
PublicKey{makeSlice(sle->getFieldVL(sfPublicKey))},
|
||||
sle->getFieldU32(sfSequence));
|
||||
});
|
||||
return out;
|
||||
};
|
||||
|
||||
auto const pageOf = [&](Account const& a) {
|
||||
auto const sle = env.le(keylet::manifest(a.pk()));
|
||||
return sle && sle->isFieldPresent(sfOwnerNode)
|
||||
? std::optional<std::uint64_t>{sle->getFieldU64(sfOwnerNode)}
|
||||
: std::nullopt;
|
||||
};
|
||||
|
||||
// Each master object listed once, on the page it records; the
|
||||
// ephemeral copies are not listed.
|
||||
{
|
||||
auto const all = listed();
|
||||
BEAST_EXPECT(all.size() == 40);
|
||||
for (auto const& m : masters)
|
||||
BEAST_EXPECT(all.count(m.pk()) && all.at(m.pk()) == 1);
|
||||
for (auto const& e : ephs)
|
||||
{
|
||||
auto const sle = env.le(keylet::manifest(e.pk()));
|
||||
BEAST_EXPECT(sle && !sle->isFieldPresent(sfOwnerNode));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::optional<std::uint64_t>> pages;
|
||||
for (auto const& m : masters)
|
||||
{
|
||||
pages.push_back(pageOf(m));
|
||||
BEAST_EXPECT(pages.back());
|
||||
}
|
||||
|
||||
// Rotated to a new ephemeral key...
|
||||
std::vector<Account> rotated;
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
rotated.emplace_back("dr" + std::to_string(i), KeyType::ed25519);
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[i], rotated.back(), 2))) ==
|
||||
"tesSUCCESS");
|
||||
}
|
||||
env.close();
|
||||
|
||||
// ...revoked...
|
||||
for (int i = 10; i < 15; ++i)
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[i], ephs[i], revoke))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
// ...or replaced unsuccessfully, which changes nothing.
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[0], rotated[0], 2))) ==
|
||||
"tefPAST_MANIFEST_SEQ");
|
||||
env.close();
|
||||
|
||||
// Still each master key once, at its latest, on the same page.
|
||||
auto const all = listed();
|
||||
BEAST_EXPECT(all.size() == 40);
|
||||
for (int i = 0; i < 40; ++i)
|
||||
{
|
||||
auto const expected = i < 10 ? 2u : i < 15 ? revoke : 1u;
|
||||
BEAST_EXPECT(
|
||||
all.count(masters[i].pk()) &&
|
||||
all.at(masters[i].pk()) == expected);
|
||||
BEAST_EXPECT(pageOf(masters[i]) == pages[i]);
|
||||
}
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(ephs[i].pk())));
|
||||
BEAST_EXPECT(env.le(keylet::manifest(rotated[i].pk())));
|
||||
}
|
||||
for (int i = 10; i < 15; ++i)
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(ephs[i].pk())));
|
||||
|
||||
// Read from the directory alone, with no key to start from.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{env.journal, clock};
|
||||
BEAST_EXPECT(cache.applyLedgerDirectory(*env.closed()) == 40);
|
||||
BEAST_EXPECT(cache.getMasterKey(rotated[0].pk()) == masters[0].pk());
|
||||
BEAST_EXPECT(cache.getMasterKey(ephs[0].pk()) == ephs[0].pk());
|
||||
BEAST_EXPECT(cache.getMasterKey(ephs[20].pk()) == masters[20].pk());
|
||||
BEAST_EXPECT(cache.revoked(masters[10].pk()));
|
||||
BEAST_EXPECT(cache.applyLedgerDirectory(*env.closed()) == 0);
|
||||
BEAST_EXPECT(reserved == 5);
|
||||
BEAST_EXPECT(offered.size() == 4);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1261,13 +961,10 @@ public:
|
||||
testRetrieval(sa);
|
||||
testSigningKeyRetrieval(sa);
|
||||
testMalformed(sa);
|
||||
testAccountDelete(sa);
|
||||
testEnvelopeRejections(sa);
|
||||
testCanonicalFee(sa);
|
||||
testCorruptLedger(sa);
|
||||
testGossipSelection(sa);
|
||||
testLedgerOutranksGossip(sa);
|
||||
testDirectory(sa);
|
||||
testDisabled(sa);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5055,15 +5055,7 @@ public:
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
// featureRNG is excluded: makeConfig() runs as a validator whenever
|
||||
// voting settings are given, and a validator puts a ttENTROPY with a
|
||||
// fresh random commitment into every ledger. That makes the
|
||||
// transaction set hash, and so the canonical order in which
|
||||
// different accounts' transactions apply when the ledger closes,
|
||||
// differ from run to run, which breaks tests that assert balances
|
||||
// depending on that order (e.g. unexpected balance change).
|
||||
FeatureBitset const all{
|
||||
supported_amendments() - featureXahauGenesis - featureRNG};
|
||||
FeatureBitset const all{supported_amendments() - featureXahauGenesis};
|
||||
testQueueSeq(all);
|
||||
testQueueTicket(all);
|
||||
testTecResult(all);
|
||||
@@ -5090,15 +5082,7 @@ public:
|
||||
runMetaInfo()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
// featureRNG is excluded: makeConfig() runs as a validator whenever
|
||||
// voting settings are given, and a validator puts a ttENTROPY with a
|
||||
// fresh random commitment into every ledger. That makes the
|
||||
// transaction set hash, and so the canonical order in which
|
||||
// different accounts' transactions apply when the ledger closes,
|
||||
// differ from run to run, which breaks tests that assert balances
|
||||
// depending on that order (e.g. unexpected balance change).
|
||||
FeatureBitset const all{
|
||||
supported_amendments() - featureXahauGenesis - featureRNG};
|
||||
FeatureBitset const all{supported_amendments() - featureXahauGenesis};
|
||||
testAcctInQueueButEmpty(all);
|
||||
testRPC(all);
|
||||
testExpirationReplacement(all);
|
||||
|
||||
@@ -1052,67 +1052,6 @@ public:
|
||||
BEAST_EXPECT(sim.synchronized());
|
||||
}
|
||||
|
||||
void
|
||||
testForcedInclusion()
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("forced inclusion");
|
||||
|
||||
// Transaction n * 1000 + k belongs to peer n - 1. Each one is placed
|
||||
// only in a single peer's open set and never relayed, so without
|
||||
// forced inclusion it has one vote in five and stays out.
|
||||
auto const owner = [](Tx const& tx) -> std::optional<PeerID> {
|
||||
if (tx.id() < 1000)
|
||||
return std::nullopt;
|
||||
return PeerID{tx.id() / 1000 - 1};
|
||||
};
|
||||
|
||||
auto const run = [&](bool withHook) {
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
PeerGroup peers = sim.createGroup(5);
|
||||
peers.trustAndConnect(
|
||||
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
|
||||
|
||||
for (Peer* p : peers)
|
||||
{
|
||||
if (withHook)
|
||||
p->txOwner = owner;
|
||||
|
||||
std::uint32_t const base =
|
||||
(static_cast<std::uint32_t>(p->id) + 1) * 1000;
|
||||
p->openTxs.insert(Tx{base + 1});
|
||||
if (p->id == PeerID{4})
|
||||
{
|
||||
// more of its own than it may force
|
||||
p->openTxs.insert(Tx{base + 2});
|
||||
p->openTxs.insert(Tx{base + 3});
|
||||
// one that names another owner, who does not hold it
|
||||
p->openTxs.insert(Tx{1999});
|
||||
}
|
||||
}
|
||||
|
||||
sim.run(1);
|
||||
BEAST_EXPECT(sim.synchronized());
|
||||
return peers[0]->lastClosedLedger.txs();
|
||||
};
|
||||
|
||||
{
|
||||
auto const txs = run(false);
|
||||
BEAST_EXPECT(txs.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto const txs = run(true);
|
||||
// every owner's own transaction, and only the lowest of the
|
||||
// several peer 4 put in its position
|
||||
TxSetType const expected{
|
||||
Tx{1001}, Tx{2001}, Tx{3001}, Tx{4001}, Tx{5001}};
|
||||
BEAST_EXPECTS(txs == expected, to_string(txs));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -1129,7 +1068,6 @@ public:
|
||||
testHubNetwork();
|
||||
testPreferredByBranch();
|
||||
testPauseForLaggards();
|
||||
testForcedInclusion();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -421,39 +421,6 @@ port_wss_admin
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testEarlyManifestSync()
|
||||
{
|
||||
testcase("early_manifest_sync");
|
||||
|
||||
// On unless turned off.
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("");
|
||||
BEAST_EXPECT(c.EARLY_MANIFEST_SYNC);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, bool>> const values{
|
||||
{"0", false}, {"false", false}, {"1", true}, {"TRUE", true}};
|
||||
for (auto const& [value, expected] : values)
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("[early_manifest_sync]\n" + value + "\n");
|
||||
BEAST_EXPECT(c.EARLY_MANIFEST_SYNC == expected);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("[early_manifest_sync]\nsometimes\n");
|
||||
fail();
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
pass();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testNetworkID()
|
||||
{
|
||||
@@ -1791,7 +1758,6 @@ r.ripple.com:51235
|
||||
testDatabasePinnedValidation();
|
||||
testOverlay();
|
||||
testNetworkID();
|
||||
testEarlyManifestSync();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
#include <boost/container/flat_map.hpp>
|
||||
#include <boost/container/flat_set.hpp>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
@@ -259,11 +257,6 @@ struct Peer
|
||||
// Simulation parameters
|
||||
ConsensusParms consensusParms;
|
||||
|
||||
//! Names the peer a transaction belongs to, for Consensus forced
|
||||
//! inclusion (as featureRNG uses for entropy contributions). Unset, it
|
||||
//! behaves exactly like an adaptor without the forcedTxOwner hook.
|
||||
std::function<std::optional<PeerID>(Tx const&)> txOwner;
|
||||
|
||||
//! The collectors to report events to
|
||||
CollectorRefs& collectors;
|
||||
|
||||
@@ -662,14 +655,6 @@ struct Peer
|
||||
return consensusParms;
|
||||
}
|
||||
|
||||
std::optional<PeerID>
|
||||
forcedTxOwner(Tx const& tx, Ledger const&) const
|
||||
{
|
||||
if (!txOwner)
|
||||
return std::nullopt;
|
||||
return txOwner(tx);
|
||||
}
|
||||
|
||||
// Not interested in tracking consensus mode changes for now
|
||||
void
|
||||
onModeChange(ConsensusMode, ConsensusMode)
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/InnerObjectFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
@@ -1235,90 +1234,6 @@ class Invariants_test : public beast::unit_test::suite
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
void
|
||||
testAppLoader()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
static std::string const good = "<html></html>";
|
||||
|
||||
auto insert = [](ApplyContext& ac,
|
||||
AccountID const& owner,
|
||||
uint256 const& key,
|
||||
std::string const& blob,
|
||||
std::uint32_t seq) {
|
||||
auto sle = std::make_shared<SLE>(Keylet{ltAPP_LOADER, key});
|
||||
sle->setAccountID(sfOwner, owner);
|
||||
sle->setFieldVL(sfAppLoader, Slice(blob.data(), blob.size()));
|
||||
sle->setFieldU32(sfAppLoaderSequence, seq);
|
||||
ac.view().insert(sle);
|
||||
auto root = ac.view().peek(keylet::account(owner));
|
||||
root->setFieldU32(sfAppLoaderSequence, seq);
|
||||
ac.view().update(root);
|
||||
return true;
|
||||
};
|
||||
Preclose publish = [](Account const& A1, Account const&, Env& env) {
|
||||
auto jt = noop(A1);
|
||||
jt[sfAppLoader.fieldName] = strHex(good);
|
||||
env(jt, fee(XRP(1)));
|
||||
return bool(env.le(keylet::appLoader(A1.id())));
|
||||
};
|
||||
auto bad = [&](char const* log,
|
||||
Precheck const& pre,
|
||||
std::uint16_t txType = ttACCOUNT_SET,
|
||||
Preclose const& preclose = {}) {
|
||||
doInvariantCheck(
|
||||
{{log}},
|
||||
pre,
|
||||
XRPAmount{},
|
||||
STTx{TxType(txType), [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
preclose);
|
||||
};
|
||||
|
||||
testcase << "AppLoader at the wrong key";
|
||||
bad("AppLoader is not keyed at its owner's keylet",
|
||||
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
return insert(ac, A1, keylet::appLoader(A2).key, good, 1);
|
||||
});
|
||||
testcase << "AppLoader with invalid blob";
|
||||
bad("AppLoader blob does not validate",
|
||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||
return insert(ac, A1, keylet::appLoader(A1).key, "nope", 1);
|
||||
});
|
||||
testcase << "AppLoader blob over the size limit";
|
||||
bad("AppLoader blob does not validate",
|
||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||
return insert(
|
||||
ac,
|
||||
A1,
|
||||
keylet::appLoader(A1).key,
|
||||
"<html>" + std::string(maxAppLoaderLength, 'x') + "</html>",
|
||||
1);
|
||||
});
|
||||
testcase << "AppLoader sequence differs from its owner's";
|
||||
bad(
|
||||
"AppLoader sequence does not match its owner's",
|
||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::appLoader(A1));
|
||||
sle->setFieldU32(sfAppLoaderSequence, 7);
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
ttACCOUNT_SET,
|
||||
publish);
|
||||
testcase << "AppLoader changed by the wrong transaction type";
|
||||
bad(
|
||||
"AppLoader changed by a transaction other than AccountSet or "
|
||||
"AccountDelete",
|
||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::appLoader(A1));
|
||||
ac.view().erase(sle);
|
||||
return true;
|
||||
},
|
||||
ttPAYMENT,
|
||||
publish);
|
||||
}
|
||||
|
||||
void
|
||||
testLockedBalance()
|
||||
{
|
||||
@@ -1374,7 +1289,6 @@ public:
|
||||
testNFTokenPageInvariants();
|
||||
testPermissionedDomainInvariants();
|
||||
testLockedBalance();
|
||||
testAppLoader();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpl/hook/Enum.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
// A valid hook exporting both hook() and cbak() (emit test hook).
|
||||
static char const validHex[] =
|
||||
"0061736D0100000001250660027F7F017F60037F7F7E017E60017F017E600001"
|
||||
"7E60027F7F017E60047F7F7F7F017E028F010903656E76025F67000003656E76"
|
||||
"06616363657074000103656E760C6574786E5F72657365727665000203656E76"
|
||||
"0A6C65646765725F736571000303656E760C686F6F6B5F6163636F756E740004"
|
||||
"03656E760C6574786E5F64657461696C73000403656E760D6574786E5F666565"
|
||||
"5F62617365000403656E7604656D6974000503656E7608726F6C6C6261636B00"
|
||||
"01030302020205030100020627067F0141908A040B7F0041820A0B7F00418008"
|
||||
"0B7F0041908A040B7F004180080B7F004180080B070F02046362616B00090468"
|
||||
"6F6F6B000A0AA883000299800000410141011080808080001A41004100420010"
|
||||
"81808080000B88830002037F017E23808080800041206B220124808080800041"
|
||||
"0141011080808080001A41011082808080001A4100108380808000A722024105"
|
||||
"6A22034118742003410874418080FC07717220034108764180FE037120034118"
|
||||
"767272360295888080004100200241016A22034118742003410874418080FC07"
|
||||
"717220034108764180FE03712003411876727236028F8880800041C788808000"
|
||||
"41141084808080001A41DB88808000418A011085808080001A41004180888080"
|
||||
"0041E50110868080800022043C00A188808000410020044208883C00A0888080"
|
||||
"00410020044210883C009F88808000410020044218883C009E88808000410020"
|
||||
"044220883C009D88808000410020044228883C009C8880800041002004423088"
|
||||
"3C009B8880800041002004423888A7413F7141C000723A009A88808000024002"
|
||||
"402001412041808880800041E501108780808000427F550D0041E58980800041"
|
||||
"0D42D30010888080800021040C010B41F289808000411042D400108180808000"
|
||||
"21040B200141206A24808080800020040B0B900202004180080BE50112006322"
|
||||
"000000002400000000201A00000000201B000000006840000000000000007321"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0081140000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"000041E5090B1D456D6974206661696C65642E00456D69742073756363656564"
|
||||
"65642E00";
|
||||
|
||||
class HookValidate_test : public beast::unit_test::suite
|
||||
{
|
||||
static Json::Value
|
||||
call(test::jtx::Env& env, std::string const& code)
|
||||
{
|
||||
Json::Value params(Json::objectValue);
|
||||
params[jss::code] = code;
|
||||
return env.rpc("json", "hook_validate", to_string(params))[jss::result];
|
||||
}
|
||||
|
||||
static std::unique_ptr<Config>
|
||||
withRpc(std::unique_ptr<Config> cfg, bool enabled)
|
||||
{
|
||||
cfg->HOOK_VALIDATE_RPC = enabled;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void
|
||||
testDisabled()
|
||||
{
|
||||
testcase("Disabled");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, envconfig(withRpc, false)};
|
||||
auto const r = call(env, validHex);
|
||||
BEAST_EXPECT(r[jss::error] == "notSupported");
|
||||
}
|
||||
|
||||
void
|
||||
testParams()
|
||||
{
|
||||
testcase("Parameter validation");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, envconfig(withRpc, true)};
|
||||
|
||||
{
|
||||
// missing code
|
||||
auto const r = env.rpc("json", "hook_validate", "{}")[jss::result];
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
}
|
||||
{
|
||||
// code not a string
|
||||
auto const r =
|
||||
env.rpc("json", "hook_validate", "{\"code\": 1}")[jss::result];
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
}
|
||||
{
|
||||
// empty, odd-length and non-hex input
|
||||
for (auto const& bad : {"", "0", "0061736Z"})
|
||||
{
|
||||
auto const r = call(env, bad);
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
}
|
||||
}
|
||||
{
|
||||
// one byte over the maximum hook size
|
||||
auto const r =
|
||||
call(env, std::string(2 * (hook::maxHookWasmSize() + 1), '0'));
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testValidate()
|
||||
{
|
||||
testcase("Validate");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, envconfig(withRpc, true)};
|
||||
|
||||
{
|
||||
auto const r = call(env, validHex);
|
||||
BEAST_EXPECT(r[jss::valid] == true);
|
||||
BEAST_EXPECT(r.isMember(jss::instruction_count_hook));
|
||||
BEAST_EXPECT(r.isMember(jss::instruction_count_cbak));
|
||||
BEAST_EXPECT(!r.isMember(jss::vm_error));
|
||||
BEAST_EXPECT(r[jss::log].isArray());
|
||||
}
|
||||
{
|
||||
// the lowercase hex the toolchains emit is accepted too
|
||||
std::string lower = validHex;
|
||||
for (auto& c : lower)
|
||||
c = std::tolower(static_cast<unsigned char>(c));
|
||||
auto const r = call(env, lower);
|
||||
BEAST_EXPECT(r[jss::valid] == true);
|
||||
}
|
||||
{
|
||||
// not webassembly
|
||||
auto const r = call(env, std::string(140, '0'));
|
||||
BEAST_EXPECT(r[jss::valid] == false);
|
||||
BEAST_EXPECT(!r.isMember(jss::instruction_count_hook));
|
||||
BEAST_EXPECT(r[jss::log].isArray() && r[jss::log].size() > 0);
|
||||
}
|
||||
{
|
||||
std::string bad = validHex;
|
||||
bad[2] = 'F'; // corrupt magic header
|
||||
auto const r = call(env, bad);
|
||||
BEAST_EXPECT(r[jss::valid] == false);
|
||||
BEAST_EXPECT(r[jss::log].isArray() && r[jss::log].size() > 0);
|
||||
}
|
||||
{
|
||||
// truncated module
|
||||
std::string const truncated = std::string(validHex).substr(0, 200);
|
||||
auto const r = call(env, truncated);
|
||||
BEAST_EXPECT(r[jss::valid] == false);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testConfigDefault()
|
||||
{
|
||||
testcase("Config default");
|
||||
auto load = [](bool standalone, char const* content) {
|
||||
Config cfg;
|
||||
cfg.setupControl(true, true, standalone);
|
||||
cfg.loadFromString(content);
|
||||
return cfg.HOOK_VALIDATE_RPC;
|
||||
};
|
||||
BEAST_EXPECT(load(true, ""));
|
||||
BEAST_EXPECT(!load(true, "[hook_validate_rpc]\n0\n"));
|
||||
BEAST_EXPECT(!load(false, ""));
|
||||
BEAST_EXPECT(load(false, "[hook_validate_rpc]\n1\n"));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testConfigDefault();
|
||||
testDisabled();
|
||||
testParams();
|
||||
testValidate();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(HookValidate, rpc, ripple);
|
||||
|
||||
} // namespace ripple
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2998,50 +2998,6 @@ static RPCCallTestData const rpcCallTestArray[] = {
|
||||
RPCCallTestData::bad_cast,
|
||||
R"()"},
|
||||
|
||||
// hook_validate
|
||||
// ------------------------------------------------------------------------
|
||||
{"hook_validate: minimal.",
|
||||
__LINE__,
|
||||
{"hook_validate", "0061736D01000000"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "hook_validate",
|
||||
"params" : [
|
||||
{
|
||||
"api_version" : %API_VER%,
|
||||
"code" : "0061736D01000000"
|
||||
}
|
||||
]
|
||||
})"},
|
||||
{"hook_validate: too few arguments.",
|
||||
__LINE__,
|
||||
{"hook_validate"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "hook_validate",
|
||||
"params" : [
|
||||
{
|
||||
"error" : "badSyntax",
|
||||
"error_code" : 1,
|
||||
"error_message" : "Syntax error."
|
||||
}
|
||||
]
|
||||
})"},
|
||||
{"hook_validate: too many arguments.",
|
||||
__LINE__,
|
||||
{"hook_validate", "00", "00"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "hook_validate",
|
||||
"params" : [
|
||||
{
|
||||
"error" : "badSyntax",
|
||||
"error_code" : 1,
|
||||
"error_message" : "Syntax error."
|
||||
}
|
||||
]
|
||||
})"},
|
||||
|
||||
// json
|
||||
// ------------------------------------------------------------------------
|
||||
{"json: minimal.",
|
||||
|
||||
@@ -41,11 +41,8 @@
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/beast/core/LexicalCast.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/beast/utility/rngfill.h>
|
||||
#include <xrpl/crypto/csprng.h>
|
||||
#include <xrpl/protocol/BuildInfo.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -351,10 +348,6 @@ RCLConsensus::Adaptor::onClose(
|
||||
make_shamapitem(tx.first->getTransactionID(), s.slice()));
|
||||
}
|
||||
|
||||
// featureRNG: our entropy contribution, if we add one. It is not tracked
|
||||
// by the censorship detector: it is only valid for this ledger.
|
||||
std::optional<uint256> entropyTxID;
|
||||
|
||||
// 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
|
||||
@@ -396,26 +389,6 @@ RCLConsensus::Adaptor::onClose(
|
||||
app_.getValidations(),
|
||||
initialSet);
|
||||
}
|
||||
|
||||
// featureRNG: our entropy contribution goes into our own initial
|
||||
// position, never the open ledger, so it only becomes public once our
|
||||
// transaction set for this ledger is fixed.
|
||||
if (prevLedger->rules().enabled(featureRNG))
|
||||
{
|
||||
if (auto const entropyTx = makeEntropyTx(prevLedger))
|
||||
{
|
||||
Serializer s;
|
||||
entropyTx->add(s);
|
||||
entropyTxID = entropyTx->getTransactionID();
|
||||
if (!initialSet->addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(*entropyTxID, s.slice())))
|
||||
{
|
||||
entropyTxID.reset();
|
||||
JLOG(j_.warn()) << "RNG: failed to add entropy txn";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now we need an immutable snapshot
|
||||
@@ -427,10 +400,9 @@ RCLConsensus::Adaptor::onClose(
|
||||
RCLCensorshipDetector<TxID, LedgerIndex>::TxIDSeqVec proposed;
|
||||
|
||||
initialSet->visitLeaves(
|
||||
[&proposed, &entropyTxID, seq](
|
||||
boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
if (item->key() != entropyTxID)
|
||||
proposed.emplace_back(item->key(), seq);
|
||||
[&proposed,
|
||||
seq](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
proposed.emplace_back(item->key(), seq);
|
||||
});
|
||||
|
||||
censorshipDetector_.propose(std::move(proposed));
|
||||
@@ -450,99 +422,6 @@ RCLConsensus::Adaptor::onClose(
|
||||
validatorKeys_.nodeID}};
|
||||
}
|
||||
|
||||
std::optional<STTx>
|
||||
RCLConsensus::Adaptor::makeEntropyTx(
|
||||
std::shared_ptr<Ledger const> const& prevLedger)
|
||||
{
|
||||
if (!validatorKeys_.keys)
|
||||
return std::nullopt;
|
||||
|
||||
auto const& masterKey = validatorKeys_.keys->masterPublicKey;
|
||||
|
||||
// The commitment the ledger currently holds for us, if any.
|
||||
std::optional<uint256> commitment;
|
||||
if (auto const sle = prevLedger->read(keylet::random()))
|
||||
{
|
||||
for (auto const& entry : sle->getFieldArray(sfRandomDigests))
|
||||
{
|
||||
if (makeSlice(entry.getFieldVL(sfPublicKey)) == masterKey.slice())
|
||||
{
|
||||
commitment = entry.getFieldH256(sfNextRandomDigest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We only ever hold the secret from our last contribution, which has
|
||||
// never been published. Reveal it iff the ledger holds its commitment.
|
||||
// Otherwise (first contribution, restart, or our last contribution was
|
||||
// not included) we only commit this time, and our previous secret,
|
||||
// whatever became of it, is never used again.
|
||||
std::optional<uint256> reveal;
|
||||
if (commitment && rngSecret_ && rngSecret_->first == *commitment)
|
||||
reveal = rngSecret_->second;
|
||||
|
||||
uint256 secret;
|
||||
beast::rngfill(secret.begin(), secret.size(), crypto_prng());
|
||||
uint256 const nextCommitment = sha512Half(secret);
|
||||
rngSecret_.emplace(nextCommitment, secret);
|
||||
|
||||
auto const seq = prevLedger->info().seq + 1;
|
||||
|
||||
JLOG(j_.debug()) << "RNG: entropy txn for ledger " << seq
|
||||
<< (reveal ? " (reveal and commit)" : " (commit only)");
|
||||
|
||||
return STTx(ttENTROPY, [&](auto& obj) {
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
obj.setFieldVL(sfPublicKey, masterKey.slice());
|
||||
obj.setFieldH256(sfNextRandomDigest, nextCommitment);
|
||||
if (reveal)
|
||||
obj.setFieldH256(sfRandomData, *reveal);
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<NodeID>
|
||||
RCLConsensus::Adaptor::forcedTxOwner(
|
||||
RCLCxTx const& tx,
|
||||
RCLCxLedger const& prevLedger) const
|
||||
{
|
||||
if (!prevLedger.ledger_ || !prevLedger.ledger_->rules().enabled(featureRNG))
|
||||
return std::nullopt;
|
||||
|
||||
try
|
||||
{
|
||||
SerialIter sit(tx.tx_->slice());
|
||||
STTx const stx(sit);
|
||||
|
||||
if (stx.getTxnType() != ttENTROPY ||
|
||||
stx.getFieldU32(sfLedgerSequence) != prevLedger.seq() + 1)
|
||||
return std::nullopt;
|
||||
|
||||
auto const pk = stx.getFieldVL(sfPublicKey);
|
||||
if (!publicKeyType(makeSlice(pk)))
|
||||
return std::nullopt;
|
||||
|
||||
// Only the exact bytes makeEntropyTx would build, so an owner cannot
|
||||
// force extra fields (memos and the like) in with its contribution.
|
||||
STTx const canonical(ttENTROPY, [&](auto& obj) {
|
||||
obj.setFieldU32(sfLedgerSequence, prevLedger.seq() + 1);
|
||||
obj.setFieldVL(sfPublicKey, pk);
|
||||
obj.setFieldH256(
|
||||
sfNextRandomDigest, stx.getFieldH256(sfNextRandomDigest));
|
||||
if (stx.isFieldPresent(sfRandomData))
|
||||
obj.setFieldH256(sfRandomData, stx.getFieldH256(sfRandomData));
|
||||
});
|
||||
if (canonical.getTransactionID() != tx.id())
|
||||
return std::nullopt;
|
||||
|
||||
return calcNodeID(PublicKey(makeSlice(pk)));
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RCLConsensus::Adaptor::onForceAccept(
|
||||
Result const& result,
|
||||
|
||||
@@ -34,12 +34,10 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/RippleLedgerHash.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STValidation.h>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -93,11 +91,6 @@ class RCLConsensus
|
||||
RCLCensorshipDetector<TxID, LedgerIndex> censorshipDetector_;
|
||||
NegativeUNLVote nUnlVote_;
|
||||
|
||||
// featureRNG: the secret behind the commitment in our most recent
|
||||
// entropy contribution, as {commitment, secret}. Only touched by
|
||||
// onClose, which runs under the consensus lock.
|
||||
std::optional<std::pair<uint256, uint256>> rngSecret_;
|
||||
|
||||
public:
|
||||
using Ledger_t = RCLCxLedger;
|
||||
using NodeID_t = NodeID;
|
||||
@@ -189,23 +182,7 @@ class RCLConsensus
|
||||
return parms_;
|
||||
}
|
||||
|
||||
/** featureRNG: if tx is a validator's entropy contribution for the
|
||||
ledger after prevLedger, the NodeID of that validator.
|
||||
|
||||
Consensus votes for such a transaction while it is in its owner's
|
||||
own position during the opening window of the establish phase.
|
||||
*/
|
||||
std::optional<NodeID>
|
||||
forcedTxOwner(RCLCxTx const& tx, RCLCxLedger const& prevLedger) const;
|
||||
|
||||
private:
|
||||
/** featureRNG: build our entropy contribution for the ledger after
|
||||
prevLedger, revealing our previous secret if the ledger holds its
|
||||
commitment, and committing to a fresh one.
|
||||
*/
|
||||
std::optional<STTx>
|
||||
makeEntropyTx(std::shared_ptr<Ledger const> const& prevLedger);
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// The following members implement the generic Consensus requirements
|
||||
// and are marked private to indicate ONLY Consensus<Adaptor> will call
|
||||
|
||||
@@ -23,22 +23,17 @@
|
||||
#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/misc/ValidatorList.h>
|
||||
#include <xrpld/app/rdb/Wallet.h>
|
||||
#include <xrpld/consensus/LedgerTiming.h>
|
||||
#include <xrpld/core/JobQueue.h>
|
||||
#include <xrpld/core/TimeKeeper.h>
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/overlay/predicates.h>
|
||||
#include <xrpld/perflog/PerfLog.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/messages.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
@@ -172,40 +167,6 @@ RCLValidationsAdaptor::acquire(LedgerHash const& hash)
|
||||
return RCLValidatedLedger(std::move(ledger), j_);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/** Announce a manifest the cache accepted other than from peer gossip.
|
||||
|
||||
Not to the peers it was held back from: OverlayImpl::onManifests records
|
||||
each of them against the manifest's hash.
|
||||
*/
|
||||
void
|
||||
relayManifest(Application& app, Manifest const& manifest)
|
||||
{
|
||||
app.getOPs().pubManifest(manifest);
|
||||
|
||||
// As OverlayImpl::onManifests does for gossip it accepts directly.
|
||||
if (app.validators().listed(manifest.masterKey))
|
||||
{
|
||||
auto db = app.getWalletDB().checkoutDb();
|
||||
addValidatorManifest(*db, manifest.serialized);
|
||||
}
|
||||
|
||||
auto const toSkip = app.getHashRouter().shouldRelay(manifest.hash());
|
||||
if (!toSkip)
|
||||
return;
|
||||
|
||||
protocol::TMManifests tm;
|
||||
tm.add_list()->set_stobject(
|
||||
manifest.serialized.data(), manifest.serialized.size());
|
||||
|
||||
app.overlay().foreach(send_if_not(
|
||||
std::make_shared<Message>(tm, protocol::mtMANIFESTS),
|
||||
peer_in_set(*toSkip)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
handleNewValidation(
|
||||
Application& app,
|
||||
@@ -230,29 +191,13 @@ handleNewValidation(
|
||||
// Resolving now also means this validation is classified in the round it
|
||||
// arrived in rather than written off along with every other one until the
|
||||
// trusted set is next recomputed.
|
||||
auto& manifests = app.validatorManifests();
|
||||
|
||||
std::shared_ptr<ReadView const> view;
|
||||
if (manifests.getMasterKey(signingKey) == signingKey)
|
||||
if (app.validatorManifests().getMasterKey(signingKey) == signingKey)
|
||||
{
|
||||
view = app.openLedger().current();
|
||||
if (view && !view->rules().enabled(featureOnChainManifests))
|
||||
view.reset();
|
||||
if (auto const view = app.openLedger().current();
|
||||
view && view->rules().enabled(featureOnChainManifests))
|
||||
app.validatorManifests().applyLedgerSigningKey(*view, signingKey);
|
||||
}
|
||||
|
||||
// Every caller has verified this validation's signature and found it
|
||||
// current, so its ephemeral key is live. That is what admits a manifest
|
||||
// gossiped for the key, and what keeps the manifest it belongs to from
|
||||
// being evicted. A manifest that arrived before the key was ever seen is
|
||||
// released here, and relayed: the peers that held it back are waiting on
|
||||
// the same validation.
|
||||
if (auto const released = manifests.noteValidation(signingKey, view.get()))
|
||||
relayManifest(app, *released);
|
||||
|
||||
// The ledger tier's miss path.
|
||||
if (view && manifests.getMasterKey(signingKey) == signingKey)
|
||||
manifests.applyLedgerSigningKey(*view, signingKey);
|
||||
|
||||
// Ensure validation is marked as trusted if signer currently trusted
|
||||
auto masterKey = app.validators().getTrustedKey(signingKey);
|
||||
|
||||
|
||||
@@ -491,16 +491,6 @@ public:
|
||||
Expected<uint256, HookReturnCode>
|
||||
ledger_nonce() const;
|
||||
|
||||
/// RNG APIs (featureRNG)
|
||||
|
||||
// A uniform integer in [0, sides).
|
||||
Expected<uint64_t, HookReturnCode>
|
||||
dice(uint32_t sides) const;
|
||||
|
||||
// len (1 to 512) random bytes.
|
||||
Expected<Bytes, HookReturnCode>
|
||||
util_random(uint32_t len) const;
|
||||
|
||||
Expected<Keylet, HookReturnCode>
|
||||
ledger_keylet(Keylet const& klLo, Keylet const& klHi) const;
|
||||
|
||||
@@ -640,10 +630,6 @@ private:
|
||||
Rules const& rules,
|
||||
int recursion_depth = 0) // used internally
|
||||
const;
|
||||
|
||||
// featureRNG: advance the ledger's RNG state and return one output block.
|
||||
Expected<uint256, HookReturnCode>
|
||||
rng_next() const;
|
||||
};
|
||||
|
||||
} // namespace hook
|
||||
|
||||
@@ -207,7 +207,6 @@ struct HookContext
|
||||
uint16_t emit_nonce_counter{
|
||||
0}; // incremented whenever nonce is called to ensure unique nonces
|
||||
uint16_t ledger_nonce_counter{0};
|
||||
uint16_t rng_draws{0}; // featureRNG: dice/random calls this execution
|
||||
int64_t expected_etxn_count{-1}; // make this a 64bit int so the uint32
|
||||
// from the hookapi cant overflow it
|
||||
std::map<ripple::uint256, bool> nonce_used{};
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/ledger/TransactionMaster.h>
|
||||
#include <xrpld/app/tx/detail/Import.h>
|
||||
#include <xrpld/ledger/ApplyViewImpl.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace hook {
|
||||
|
||||
@@ -1898,147 +1895,6 @@ HookAPI::ledger_nonce() const
|
||||
return hash;
|
||||
}
|
||||
|
||||
// featureRNG
|
||||
//
|
||||
// The ledger's RNG state lives in the Random object. The ttENTROPY
|
||||
// transactions applied at the start of the ledger seed it from validator
|
||||
// reveals; after that every draw from any Hook replaces it with
|
||||
// H(step, state). So the state iterates between API calls, between Hook
|
||||
// executions and between transactions, including transactions that end in a
|
||||
// tec: ApplyContext::discard keeps the advance, so a rollback cannot preserve
|
||||
// an RNG state for whatever executes next.
|
||||
//
|
||||
// No Hook can read the state, so none can work out what a later draw will
|
||||
// return, even one that ran earlier in the same transaction: slot_set and
|
||||
// ledger_keylet refuse the Random object, slot_set refuses ttENTROPY
|
||||
// transactions, the object is left out of provisional metadata (meta_slot),
|
||||
// and its state and commitment fields are never written to any metadata.
|
||||
//
|
||||
// Each draw returns H(output, state, txid, hook account, hook hash, chain
|
||||
// position, strong, callback, wasm param). Those extra inputs keep outputs
|
||||
// distinct for different executions even if they somehow saw the same state.
|
||||
//
|
||||
// The state the transaction's first draw saw is written to the transaction's
|
||||
// final metadata (sfRandomData), never its provisional metadata.
|
||||
Expected<uint256, HookReturnCode>
|
||||
HookAPI::rng_next() const
|
||||
{
|
||||
static constexpr std::uint32_t rngOutputTag = 0x524E474F; // 'RNGO'
|
||||
static constexpr std::uint32_t rngStepTag = 0x524E4749; // 'RNGI'
|
||||
|
||||
auto& applyCtx = hookCtx.applyCtx;
|
||||
auto& view = applyCtx.view();
|
||||
|
||||
if (!view.rules().enabled(featureRNG))
|
||||
return Unexpected(TOO_LITTLE_ENTROPY);
|
||||
|
||||
if (hookCtx.rng_draws >= hook_api::max_rng_draws)
|
||||
return Unexpected(TOO_MANY_NONCES);
|
||||
|
||||
auto sle = view.peek(keylet::random());
|
||||
if (!sle)
|
||||
return Unexpected(TOO_LITTLE_ENTROPY);
|
||||
|
||||
// A closed ledger may only draw on its own seed. Open ledger application
|
||||
// is a preview, so it may run on the last closed ledger's state; the
|
||||
// final result is decided when the ledger closes.
|
||||
auto const seq = view.seq();
|
||||
auto const seededFor = sle->getFieldU32(sfLedgerSequence);
|
||||
if (seededFor != seq && !(view.open() && seededFor + 1 == seq))
|
||||
return Unexpected(TOO_LITTLE_ENTROPY);
|
||||
|
||||
// At least 80% of the UNLReport must have contributed (at least one
|
||||
// validator if there is no UNLReport). Both fields are 16 bits on the
|
||||
// ledger; they are widened here so the scaled comparison cannot overflow.
|
||||
std::uint32_t const count = sle->getFieldU16(sfEntropyCount);
|
||||
std::uint32_t const denominator = sle->getFieldU16(sfEntropyDenominator);
|
||||
if (count == 0 || count * 5 < denominator * 4)
|
||||
return Unexpected(TOO_LITTLE_ENTROPY);
|
||||
|
||||
uint256 const state = sle->getFieldH256(sfRandomData);
|
||||
|
||||
if (auto* avi = dynamic_cast<ApplyViewImpl*>(&view))
|
||||
avi->setRandomData(state);
|
||||
|
||||
auto const& hr = hookCtx.result;
|
||||
uint256 const out = ripple::sha512Half(
|
||||
rngOutputTag,
|
||||
state,
|
||||
applyCtx.tx.getTransactionID(),
|
||||
hr.account,
|
||||
hr.hookHash,
|
||||
static_cast<std::uint8_t>(hr.hookChainPosition),
|
||||
static_cast<std::uint8_t>(hr.isStrong ? 1 : 0),
|
||||
static_cast<std::uint8_t>(hr.isCallback ? 1 : 0),
|
||||
hr.wasmParam);
|
||||
|
||||
sle->setFieldH256(sfRandomData, ripple::sha512Half(rngStepTag, state));
|
||||
view.update(sle);
|
||||
|
||||
++hookCtx.rng_draws;
|
||||
return out;
|
||||
}
|
||||
|
||||
Expected<uint64_t, HookReturnCode>
|
||||
HookAPI::dice(uint32_t sides) const
|
||||
{
|
||||
if (sides == 0)
|
||||
return Unexpected(INVALID_ARGUMENT);
|
||||
|
||||
auto block = rng_next();
|
||||
if (!block)
|
||||
return Unexpected(block.error());
|
||||
|
||||
// Rejection sampling over big-endian 64-bit words, so every side is
|
||||
// exactly equally likely. A word is rejected with probability below
|
||||
// 2^-32, so the bound on blocks is never reached in practice; it only
|
||||
// keeps the loop finite.
|
||||
std::uint64_t const max = std::numeric_limits<std::uint64_t>::max();
|
||||
std::uint64_t const excess = (max % sides + 1) % sides; // 2^64 mod sides
|
||||
std::uint64_t word = 0;
|
||||
uint256 b = *block;
|
||||
for (int i = 0; i < 16; ++i)
|
||||
{
|
||||
for (std::size_t w = 0; w < 4; ++w)
|
||||
{
|
||||
word = 0;
|
||||
for (std::size_t k = 0; k < 8; ++k)
|
||||
word = (word << 8) | b.data()[w * 8 + k];
|
||||
if (word <= max - excess)
|
||||
return word % sides;
|
||||
}
|
||||
b = ripple::sha512Half(b);
|
||||
}
|
||||
return word % sides;
|
||||
}
|
||||
|
||||
Expected<Bytes, HookReturnCode>
|
||||
HookAPI::util_random(uint32_t len) const
|
||||
{
|
||||
if (len == 0)
|
||||
return Unexpected(TOO_SMALL);
|
||||
|
||||
if (len > 512)
|
||||
return Unexpected(TOO_BIG);
|
||||
|
||||
auto block = rng_next();
|
||||
if (!block)
|
||||
return Unexpected(block.error());
|
||||
|
||||
Bytes out;
|
||||
out.reserve(len);
|
||||
uint256 b = *block;
|
||||
while (true)
|
||||
{
|
||||
auto const n = std::min<std::size_t>(32, len - out.size());
|
||||
out.insert(out.end(), b.data(), b.data() + n);
|
||||
if (out.size() >= len)
|
||||
break;
|
||||
b = ripple::sha512Half(b);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Expected<Keylet, HookReturnCode>
|
||||
HookAPI::ledger_keylet(Keylet const& klLo, Keylet const& klHi) const
|
||||
{
|
||||
@@ -2049,10 +1905,6 @@ HookAPI::ledger_keylet(Keylet const& klLo, Keylet const& klHi) const
|
||||
std::optional<ripple::uint256> found =
|
||||
hookCtx.applyCtx.view().succ(klLo.key, klHi.key.next());
|
||||
|
||||
// featureRNG: the RNG object is invisible to Hooks, even by key.
|
||||
if (found && *found == keylet::random().key)
|
||||
found = hookCtx.applyCtx.view().succ(*found, klHi.key.next());
|
||||
|
||||
if (!found)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
@@ -2297,12 +2149,6 @@ HookAPI::slot_set(Bytes const& data, uint32_t slot_no) const
|
||||
if (kl->key == beast::zero)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
// featureRNG: the RNG state must never be readable by a Hook, or an
|
||||
// earlier Hook could compute what later draws will return. Matched on
|
||||
// the key because a Hook-supplied keylet type can be ltANY.
|
||||
if (kl->key == keylet::random().key)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
auto const sle = hookCtx.applyCtx.view().read(*kl);
|
||||
if (!sle)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
@@ -2326,11 +2172,6 @@ HookAPI::slot_set(Bytes const& data, uint32_t slot_no) const
|
||||
slot_value = p->first->getSTransaction();
|
||||
else
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
// featureRNG: validator reveals are never readable by Hooks.
|
||||
if (*slot_value &&
|
||||
(*slot_value)->getFieldU16(sfTransactionType) == ttENTROPY)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
}
|
||||
else
|
||||
return Unexpected(INVALID_ARGUMENT);
|
||||
|
||||
@@ -558,8 +558,7 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
case ttFEE:
|
||||
case ttUNL_MODIFY:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttUNL_REPORT:
|
||||
case ttENTROPY: {
|
||||
case ttUNL_REPORT: {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -2867,49 +2866,6 @@ DEFINE_HOOK_FUNCTION(
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
// featureRNG: a uniform integer in [0, sides)
|
||||
DEFINE_HOOK_FUNCTION(int64_t, dice, uint32_t sides)
|
||||
{
|
||||
HOOK_SETUP();
|
||||
|
||||
auto const result = api.dice(sides);
|
||||
if (!result)
|
||||
return result.error();
|
||||
|
||||
return result.value();
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
// featureRNG: write_len (1 to 512) random bytes
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
int64_t,
|
||||
util_random,
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len)
|
||||
{
|
||||
HOOK_SETUP();
|
||||
|
||||
if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
auto const result = api.util_random(write_len);
|
||||
if (!result)
|
||||
return result.error();
|
||||
|
||||
auto const& bytes = result.value();
|
||||
|
||||
WRITE_WASM_MEMORY_AND_RETURN(
|
||||
write_ptr,
|
||||
write_len,
|
||||
bytes.data(),
|
||||
bytes.size(),
|
||||
memory,
|
||||
memory_length);
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
int64_t,
|
||||
ledger_keylet,
|
||||
|
||||
@@ -25,11 +25,7 @@
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/overlay/PeerSet.h>
|
||||
#include <xrpl/basics/CountedObject.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
@@ -179,36 +175,6 @@ private:
|
||||
clock_type::time_point mLastAction;
|
||||
|
||||
std::shared_ptr<Ledger> mLedger;
|
||||
|
||||
/** Whether this ledger is to be read for manifests as soon as it can be:
|
||||
early, before the rest of the state map, if [early_manifest_sync]
|
||||
allows, and otherwise once it is complete. */
|
||||
bool
|
||||
wantManifests();
|
||||
|
||||
/** Read the manifests from the ledger, now that they can be.
|
||||
|
||||
Dispatches the read at most once per ledger, however many paths reach
|
||||
here.
|
||||
|
||||
@param listed Master keys to read by key, whether or not the part of
|
||||
the directory read lists them. If unseated, the master keys on the
|
||||
validator lists are collected in the job, so that a caller holding
|
||||
mtx_ need not take the validator list's lock.
|
||||
@param maxPages Most manifest directory pages to read
|
||||
*/
|
||||
void
|
||||
syncManifests(
|
||||
std::optional<hash_set<PublicKey>> listed,
|
||||
std::uint64_t maxPages);
|
||||
|
||||
/** The manifests have been read, or are being, from this ledger.
|
||||
|
||||
Atomic because done() can be reached from trigger() after it has
|
||||
released mtx_.
|
||||
*/
|
||||
std::atomic<bool> mManifestsSynced{false};
|
||||
|
||||
bool mHaveHeader;
|
||||
bool mHaveState;
|
||||
bool mHaveTransactions;
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_LEDGER_MANIFESTSYNC_H_INCLUDED
|
||||
#define RIPPLE_APP_LEDGER_MANIFESTSYNC_H_INCLUDED
|
||||
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpld/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Manifest directory pages read while a ledger is still being acquired.
|
||||
|
||||
Enough entries to fill the manifest cache's ledger tier, which is all a
|
||||
bulk read of the directory can admit. The master keys a node actually
|
||||
depends on are fetched by key instead, wherever they are listed, so a
|
||||
directory grown long by anyone able to fund an account cannot make the
|
||||
early phase of a sync arbitrarily long.
|
||||
*/
|
||||
std::uint64_t constexpr manifestSyncPages = 16;
|
||||
|
||||
/** The nodes a partly synced state map still needs to read its manifests.
|
||||
|
||||
Walks the manifest directory as far as the nodes held allow: the root page;
|
||||
every other page, all at once, since the root names the last of them and
|
||||
pages are only ever added after it; each master object listed; and the
|
||||
ephemeral object each unrevoked one names, which is what a validation is
|
||||
resolved through. Where a key cannot be read, the first node on its path
|
||||
that is not held is reported, once however many keys it blocks.
|
||||
|
||||
Asking peers for the nodes reported and calling again converges in a
|
||||
handful of rounds -- roughly the depth of the map plus one round each for
|
||||
the pages and for the objects they list -- on a set of nodes far smaller
|
||||
than the map. After that every read forEachLedgerManifest() and
|
||||
ManifestCache::applyLedgerDirectory() make succeeds on the partial map.
|
||||
|
||||
A map with no manifest directory converges on the path to where its root
|
||||
would be, and nothing else.
|
||||
|
||||
@param stateMap State map, with at least its root node
|
||||
@param max Most nodes to report
|
||||
@param keys Further keys to read, with the ephemeral object any manifest
|
||||
among them names: in practice keylet::manifest() of each master key on
|
||||
the node's validator lists, which needs no directory to find
|
||||
@param maxPages Most directory pages to read, the root page included
|
||||
|
||||
@return nodes needed, as getMissingNodes() reports them; empty once every
|
||||
manifest can be read
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
missingManifestNodes(
|
||||
SHAMap const& stateMap,
|
||||
std::size_t max,
|
||||
std::vector<uint256> const& keys = {},
|
||||
std::uint64_t maxPages = dirNodeMaxPages);
|
||||
|
||||
/** Ledgers to read manifests from, chosen by how many peers report them.
|
||||
|
||||
A node that has never validated a ledger, and holds no manifest for enough
|
||||
of the validators it trusts, cannot tell which validations to believe. The
|
||||
closed ledgers its peers report are then its only evidence of where the
|
||||
network is, and any recent ledger of the network holds the manifests
|
||||
published on it.
|
||||
|
||||
Choosing wrongly costs little. Every manifest read from a ledger is
|
||||
verified as from any other source and the cache only moves forward, so a
|
||||
ledger a peer made up can at worst leave something out. Reading several
|
||||
candidates, rather than trusting the single most reported, is what keeps
|
||||
one misreporting peer among a few from deciding what is read.
|
||||
|
||||
@param reported The closed ledger each peer last reported, one entry per
|
||||
peer. Zero entries are ignored.
|
||||
@param skip Ledgers already read, or being read
|
||||
@param max Most ledgers to return
|
||||
|
||||
@return distinct ledgers, the most widely reported first. Ties go to the
|
||||
larger hash, as when NetworkOPs picks a ledger by peer count, so the
|
||||
first is usually a ledger being acquired anyway.
|
||||
*/
|
||||
std::vector<uint256>
|
||||
pickManifestCandidates(
|
||||
std::vector<uint256> const& reported,
|
||||
hash_set<uint256> const& skip,
|
||||
std::size_t max);
|
||||
|
||||
/** How many validators need a manifest before a node can trust a ledger.
|
||||
|
||||
@param validators The validators that count: the trusted set once
|
||||
ValidatorList has worked it out, and every listed one before that
|
||||
@param trusted Whether `validators` is the trusted set, and so whether
|
||||
`quorum` has been worked out from it
|
||||
@param quorum ValidatorList::quorum(). Before the trusted set exists this
|
||||
is a placeholder, so the usual 80% of `validators` is assumed instead.
|
||||
|
||||
@return the number needed; never more than `validators`
|
||||
*/
|
||||
std::size_t
|
||||
manifestQuorum(std::size_t validators, bool trusted, std::size_t quorum);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -106,41 +106,6 @@ applyTransactions(
|
||||
bool certainRetry = true;
|
||||
std::size_t count = 0;
|
||||
|
||||
// featureRNG: validator entropy contributions are applied before anything
|
||||
// else, so this ledger's seed is final before any Hook can draw from it.
|
||||
// Pseudo-transactions otherwise sort to an arbitrary position. Each is
|
||||
// attempted exactly once; one that fails is never retried later, where it
|
||||
// could change the seed after Hooks had drawn from it.
|
||||
if (view.rules().enabled(featureRNG))
|
||||
{
|
||||
for (auto it = txns.begin(); it != txns.end();)
|
||||
{
|
||||
if (it->second->getTxnType() != ttENTROPY)
|
||||
{
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto const txid = it->first.getTXID();
|
||||
try
|
||||
{
|
||||
if (applyTransaction(
|
||||
app, view, *it->second, false, tapNONE, j) ==
|
||||
ApplyTransactionResult::Success)
|
||||
++count;
|
||||
else
|
||||
failed.insert(txid);
|
||||
}
|
||||
catch (std::exception const& ex)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "Transaction " << txid << " throws: " << ex.what();
|
||||
failed.insert(txid);
|
||||
}
|
||||
it = txns.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to apply all of the retriable transactions
|
||||
for (int pass = 0; pass < LEDGER_TOTAL_PASSES; ++pass)
|
||||
{
|
||||
|
||||
@@ -21,18 +21,14 @@
|
||||
#include <xrpld/app/ledger/InboundLedger.h>
|
||||
#include <xrpld/app/ledger/InboundLedgers.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/ManifestSync.h>
|
||||
#include <xrpld/app/ledger/TransactionStateSF.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
#include <xrpld/core/JobQueue.h>
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
|
||||
@@ -43,16 +39,6 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Master keys on any of this node's validator lists, trusted or not. */
|
||||
static hash_set<PublicKey>
|
||||
listedMasterKeys(Application& app)
|
||||
{
|
||||
hash_set<PublicKey> keys;
|
||||
app.validators().for_each_listed(
|
||||
[&keys](PublicKey const& pk, bool) { keys.insert(pk); });
|
||||
return keys;
|
||||
}
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
enum {
|
||||
@@ -471,19 +457,6 @@ InboundLedger::done()
|
||||
mLedger->read(keylet::fees()),
|
||||
"ripple::InboundLedger::done : valid ledger fees");
|
||||
mLedger->setImmutable();
|
||||
|
||||
// The state map arrived whole before the early walk in trigger()
|
||||
// finished, as it does when most of it is held locally already, or
|
||||
// when [early_manifest_sync] turns the early walk off. Read the
|
||||
// manifests from the whole map instead, now that it is immutable.
|
||||
// Only as many directory pages as the early walk reads: past those
|
||||
// the ledger tier is full, and the listed master keys are read by
|
||||
// key whatever the directory holds. The keys are collected in the
|
||||
// job: this runs under mtx_, and the validator list has a lock of
|
||||
// its own.
|
||||
if (!mManifestsSynced && wantManifests())
|
||||
syncManifests(std::nullopt, manifestSyncPages);
|
||||
|
||||
switch (mReason)
|
||||
{
|
||||
case Reason::HISTORY:
|
||||
@@ -663,63 +636,6 @@ InboundLedger::trigger(std::shared_ptr<Peer> const& peer, TriggerReason reason)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Partial sync. A node that has not yet validated a ledger cannot
|
||||
// tell which validations to trust until it knows each trusted
|
||||
// validator's current ephemeral key, and on-ledger is where a
|
||||
// rotation or a revocation is published. So the manifests come
|
||||
// first: just the nodes on the paths to the manifest directory and
|
||||
// the objects it lists, which can be read and ingested long before
|
||||
// the rest of the state map arrives.
|
||||
//
|
||||
// The master keys on this node's validator lists are fetched by
|
||||
// key as well as through the directory. They are what a node with
|
||||
// no manifests at all needs, and reading them by key does not
|
||||
// depend on how long the directory has grown.
|
||||
if (!mManifestsSynced && app_.config().EARLY_MANIFEST_SYNC &&
|
||||
wantManifests())
|
||||
{
|
||||
sl.unlock();
|
||||
auto listed = listedMasterKeys(app_);
|
||||
std::vector<uint256> targets;
|
||||
targets.reserve(listed.size());
|
||||
for (auto const& pk : listed)
|
||||
targets.push_back(keylet::manifest(pk).key);
|
||||
auto nodes = missingManifestNodes(
|
||||
mLedger->stateMap(),
|
||||
missingNodesFind,
|
||||
targets,
|
||||
manifestSyncPages);
|
||||
sl.lock();
|
||||
|
||||
if (!failed_ && !complete_ && !mHaveState && !mManifestsSynced)
|
||||
{
|
||||
if (nodes.empty())
|
||||
{
|
||||
syncManifests(std::move(listed), manifestSyncPages);
|
||||
}
|
||||
else
|
||||
{
|
||||
filterNodes(nodes, reason);
|
||||
|
||||
// All recently asked for: fall through to the rest of
|
||||
// the map rather than wait on them.
|
||||
if (!nodes.empty())
|
||||
{
|
||||
tmGL.set_itype(protocol::liAS_NODE);
|
||||
for (auto const& id : nodes)
|
||||
*(tmGL.add_nodeids()) = id.first.getRawString();
|
||||
|
||||
JLOG(journal_.trace())
|
||||
<< "Sending manifest node request ("
|
||||
<< nodes.size() << ") to "
|
||||
<< (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccountStateSF filter(
|
||||
mLedger->stateMap().family().db(), app_.getLedgerMaster());
|
||||
|
||||
@@ -848,73 +764,6 @@ InboundLedger::trigger(std::shared_ptr<Peer> const& peer, TriggerReason reason)
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
InboundLedger::wantManifests()
|
||||
{
|
||||
// Not for history, and not once a ledger has been validated: from then on
|
||||
// the manifests are kept current from each new ledger as it closes.
|
||||
return mReason != Reason::HISTORY &&
|
||||
!app_.getLedgerMaster().haveValidated();
|
||||
}
|
||||
|
||||
void
|
||||
InboundLedger::syncManifests(
|
||||
std::optional<hash_set<PublicKey>> listed,
|
||||
std::uint64_t maxPages)
|
||||
{
|
||||
if (mManifestsSynced.exchange(true))
|
||||
return;
|
||||
|
||||
// Reading verifies a signature per manifest, so it runs as its own job.
|
||||
// The ledger is held by shared_ptr, and reading a map while nodes are
|
||||
// still being added to it is what getMissingNodes() already does.
|
||||
app_.getJobQueue().addJob(
|
||||
jtMANIFEST,
|
||||
"InboundLedger::syncManifests",
|
||||
[ledger = mLedger,
|
||||
listed = std::move(listed),
|
||||
maxPages,
|
||||
&app = app_,
|
||||
j = journal_]() mutable {
|
||||
auto const keys =
|
||||
listed ? std::move(*listed) : listedMasterKeys(app);
|
||||
auto& cache = app.validatorManifests();
|
||||
std::size_t accepted = 0;
|
||||
std::size_t unreadable = 0;
|
||||
|
||||
// By key first, one at a time: on a partial map a key whose path
|
||||
// is not held throws, and must not cost the keys after it. One
|
||||
// can be missing if the lists changed after the nodes were found.
|
||||
for (auto const& pk : keys)
|
||||
{
|
||||
try
|
||||
{
|
||||
accepted += cache.applyLedger(*ledger, {pk});
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
++unreadable;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
accepted += cache.applyLedgerDirectory(*ledger, maxPages);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "Could not read the manifest directory of ledger "
|
||||
<< ledger->info().seq << " during sync: " << e.what();
|
||||
}
|
||||
|
||||
JLOG(j.info()) << "Read manifests from ledger "
|
||||
<< ledger->info().seq << " during sync: " << accepted
|
||||
<< " accepted, " << keys.size() << " listed, "
|
||||
<< unreadable << " not yet readable";
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
InboundLedger::filterNodes(
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>& nodes,
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/ledger/ManifestSync.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
static_assert(
|
||||
manifestSyncPages * dirNodeMaxEntries >= ManifestCache::ledgerCapacity,
|
||||
"the early read must be able to fill the ledger tier");
|
||||
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
missingManifestNodes(
|
||||
SHAMap const& stateMap,
|
||||
std::size_t max,
|
||||
std::vector<uint256> const& keys,
|
||||
std::uint64_t maxPages)
|
||||
{
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>> missing;
|
||||
|
||||
// By hash: several keys blocked by the same node report it once.
|
||||
hash_set<uint256> reported;
|
||||
hash_set<uint256> visited;
|
||||
|
||||
auto const& root = keylet::manifestDir();
|
||||
|
||||
// The manifest directory's own pages: the only directory nodes whose
|
||||
// entries are followed. The ledger walked is not validated, and one a
|
||||
// peer made up could otherwise lead the walk from directory to directory
|
||||
// through as much of the map as it cared to serve.
|
||||
hash_set<uint256> pages{root.key};
|
||||
|
||||
// A stack, so the keys asked for by name go last and are read first: they
|
||||
// are the ones that decide whether this node can trust a validation.
|
||||
std::vector<uint256> pending{root.key};
|
||||
pending.insert(pending.end(), keys.rbegin(), keys.rend());
|
||||
|
||||
maxPages = std::clamp<std::uint64_t>(maxPages, 1, dirNodeMaxPages);
|
||||
|
||||
while (!pending.empty() && missing.size() < max)
|
||||
{
|
||||
auto const key = pending.back();
|
||||
pending.pop_back();
|
||||
|
||||
if (!visited.insert(key).second)
|
||||
continue;
|
||||
|
||||
std::optional<std::pair<SHAMapNodeID, uint256>> gap;
|
||||
auto const item = stateMap.peekItemPartial(key, gap);
|
||||
|
||||
if (gap)
|
||||
{
|
||||
if (reported.insert(gap->second).second)
|
||||
missing.push_back(*gap);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!item)
|
||||
continue;
|
||||
|
||||
std::optional<SLE const> sle;
|
||||
try
|
||||
{
|
||||
sle.emplace(SerialIter{item->slice()}, item->key());
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
// Not this walk's to judge: the node verified against its hash,
|
||||
// so a full read of the ledger would meet the same object.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sle->getType() == ltDIR_NODE)
|
||||
{
|
||||
if (!pages.contains(key))
|
||||
continue;
|
||||
|
||||
for (auto const& k : sle->getFieldV256(sfIndexes))
|
||||
pending.push_back(k);
|
||||
|
||||
// The root names the last page, every page in use has an index no
|
||||
// greater, and the directory only ever grows at its end. Asking
|
||||
// for them all now saves a round trip per page; an index not in
|
||||
// use costs at most the node that shows it absent.
|
||||
if (key == root.key)
|
||||
{
|
||||
auto const last = std::min<std::uint64_t>(
|
||||
sle->getFieldU64(sfIndexPrevious), maxPages - 1);
|
||||
for (std::uint64_t i = 1; i <= last; ++i)
|
||||
{
|
||||
auto const pageKey = keylet::page(root, i).key;
|
||||
pages.insert(pageKey);
|
||||
pending.push_back(pageKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (
|
||||
sle->getType() == ltMANIFEST &&
|
||||
sle->isFieldPresent(sfSigningPubKey) &&
|
||||
sle->getFieldU32(sfSequence) !=
|
||||
std::numeric_limits<std::uint32_t>::max())
|
||||
{
|
||||
auto const signing = sle->getFieldVL(sfSigningPubKey);
|
||||
if (publicKeyType(makeSlice(signing)))
|
||||
pending.push_back(
|
||||
keylet::manifest(PublicKey{makeSlice(signing)}).key);
|
||||
}
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
std::vector<uint256>
|
||||
pickManifestCandidates(
|
||||
std::vector<uint256> const& reported,
|
||||
hash_set<uint256> const& skip,
|
||||
std::size_t max)
|
||||
{
|
||||
hash_map<uint256, std::size_t> counts;
|
||||
for (auto const& hash : reported)
|
||||
if (hash.isNonZero() && !skip.contains(hash))
|
||||
++counts[hash];
|
||||
|
||||
std::vector<std::pair<std::size_t, uint256>> ranked;
|
||||
ranked.reserve(counts.size());
|
||||
for (auto const& [hash, count] : counts)
|
||||
ranked.emplace_back(count, hash);
|
||||
|
||||
auto const keep = std::min(max, ranked.size());
|
||||
std::partial_sort(
|
||||
ranked.begin(),
|
||||
ranked.begin() + keep,
|
||||
ranked.end(),
|
||||
[](auto const& a, auto const& b) { return a > b; });
|
||||
|
||||
std::vector<uint256> picked;
|
||||
picked.reserve(keep);
|
||||
for (std::size_t i = 0; i < keep; ++i)
|
||||
picked.push_back(ranked[i].second);
|
||||
return picked;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
manifestQuorum(std::size_t validators, bool trusted, std::size_t quorum)
|
||||
{
|
||||
if (trusted)
|
||||
return std::min(quorum, validators);
|
||||
|
||||
// ceil(0.8 * validators), in integers.
|
||||
return (validators * 4 + 4) / 5;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -228,7 +228,11 @@ public:
|
||||
|
||||
boost::asio::signal_set m_signals;
|
||||
|
||||
std::atomic_flag isTimeToStop;
|
||||
// Once we get C++20, we could use `std::atomic_flag` for `isTimeToStop`
|
||||
// and eliminate the need for the condition variable and the mutex.
|
||||
std::condition_variable stoppingCondition_;
|
||||
mutable std::mutex stoppingMutex_;
|
||||
std::atomic<bool> isTimeToStop = false;
|
||||
|
||||
std::atomic<bool> checkSigs_;
|
||||
|
||||
@@ -1581,7 +1585,10 @@ ApplicationImp::run()
|
||||
getLoadManager().activateDeadlockDetector();
|
||||
}
|
||||
|
||||
isTimeToStop.wait(false, std::memory_order_relaxed);
|
||||
{
|
||||
std::unique_lock<std::mutex> lk{stoppingMutex_};
|
||||
stoppingCondition_.wait(lk, [this] { return isTimeToStop.load(); });
|
||||
}
|
||||
|
||||
JLOG(m_journal.debug()) << "Application stopping";
|
||||
|
||||
@@ -1668,14 +1675,19 @@ ApplicationImp::run()
|
||||
void
|
||||
ApplicationImp::signalStop(std::string msg)
|
||||
{
|
||||
if (!isTimeToStop.test_and_set(std::memory_order_acquire))
|
||||
if (!isTimeToStop.exchange(true))
|
||||
{
|
||||
if (msg.empty())
|
||||
JLOG(m_journal.warn()) << "Server stopping";
|
||||
else
|
||||
JLOG(m_journal.warn()) << "Server stopping: " << msg;
|
||||
|
||||
isTimeToStop.notify_all();
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1694,7 +1706,7 @@ ApplicationImp::checkSigs(bool check)
|
||||
bool
|
||||
ApplicationImp::isStopping() const
|
||||
{
|
||||
return isTimeToStop.test(std::memory_order_relaxed);
|
||||
return isTimeToStop.load();
|
||||
}
|
||||
|
||||
int
|
||||
|
||||
@@ -159,7 +159,6 @@ printHelp(const po::options_description& desc)
|
||||
" gateway_balances [<ledger>] <issuer_account> [ <hotwallet> [ "
|
||||
"<hotwallet> ]]\n"
|
||||
" get_counts\n"
|
||||
" hook_validate <hex>\n"
|
||||
" json <method> <json>\n"
|
||||
" ledger [<id>|current|closed|validated] [full]\n"
|
||||
" ledger_accept\n"
|
||||
|
||||
+103
-413
@@ -20,20 +20,13 @@
|
||||
#ifndef RIPPLE_APP_MISC_MANIFEST_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_MANIFEST_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/misc/RotatingBloomFilter.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
@@ -43,8 +36,6 @@
|
||||
namespace ripple {
|
||||
|
||||
class ReadView;
|
||||
class STLedgerEntry;
|
||||
using SLE = STLedgerEntry;
|
||||
|
||||
/*
|
||||
Validator key manifests
|
||||
@@ -73,9 +64,8 @@ using SLE = STLedgerEntry;
|
||||
master public key, and (b) the most senior of all valid manifests it has
|
||||
seen for that validator, if any. On startup, the [validator_token] config
|
||||
entry (which contains the manifest for this validator) is decoded and
|
||||
added to the manifest cache. Other manifests arrive in published validator
|
||||
lists, from ltMANIFEST ledger objects, and as "gossip" received from
|
||||
rippled peers; see ManifestCache for how each is retained.
|
||||
added to the manifest cache. Other manifests are added as "gossip"
|
||||
received from rippled peers.
|
||||
|
||||
When an ephemeral key is compromised, a new signing key pair is created,
|
||||
along with a new manifest vouching for it (with a higher sequence number),
|
||||
@@ -252,17 +242,7 @@ enum class ManifestDisposition {
|
||||
badEphemeralKey,
|
||||
|
||||
/// Timely, but invalid signature
|
||||
invalid,
|
||||
|
||||
/// Gossip introducing a master key not held, whose ephemeral key has not
|
||||
/// recently signed a validation this node verified, or which names no
|
||||
/// ephemeral key at all. Not admitted and not relayed; if it names an
|
||||
/// ephemeral key it is held pending, unverified, until that key is seen.
|
||||
unseen,
|
||||
|
||||
/// Gossip with nowhere to go: every manifest held from gossip has been
|
||||
/// more active than this one could claim to be.
|
||||
full
|
||||
invalid
|
||||
};
|
||||
|
||||
inline std::string
|
||||
@@ -280,58 +260,6 @@ to_string(ManifestDisposition m)
|
||||
return "badEphemeralKey";
|
||||
case ManifestDisposition::invalid:
|
||||
return "invalid";
|
||||
case ManifestDisposition::unseen:
|
||||
return "unseen";
|
||||
case ManifestDisposition::full:
|
||||
return "full";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/** Where a held manifest came from, in increasing order of precedence.
|
||||
|
||||
The source never makes a manifest valid. Whatever its source, a manifest
|
||||
is accepted only if its signatures verify and its sequence is newer than
|
||||
the one held. The source decides how long the cache keeps it, and which
|
||||
side loses when two held manifests claim the same key.
|
||||
*/
|
||||
enum class ManifestSource : std::uint8_t {
|
||||
/** Relayed by a peer: the legacy transport, open to anyone.
|
||||
|
||||
A master key not already held from gossip is admitted only if its
|
||||
ephemeral key has recently signed a validation this node verified, and
|
||||
only into one of ManifestCache::gossipCapacity slots, which go to the
|
||||
most active.
|
||||
*/
|
||||
gossip = 0,
|
||||
|
||||
/** Read from an ltMANIFEST ledger object.
|
||||
|
||||
The ledger is the store and this is a lookup cache over it, of
|
||||
ManifestCache::ledgerCapacity entries: dropping one costs a SHAMap
|
||||
read the next time it is wanted, and loses nothing. Revocations are
|
||||
expected to live here, since a validator revokes on-ledger.
|
||||
*/
|
||||
ledger = 1,
|
||||
|
||||
/** Carried in a validator list, the config, or the wallet those were
|
||||
saved to. Never evicted.
|
||||
*/
|
||||
list = 2,
|
||||
};
|
||||
|
||||
inline std::string
|
||||
to_string(ManifestSource s)
|
||||
{
|
||||
switch (s)
|
||||
{
|
||||
case ManifestSource::gossip:
|
||||
return "gossip";
|
||||
case ManifestSource::ledger:
|
||||
return "ledger";
|
||||
case ManifestSource::list:
|
||||
return "list";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
@@ -339,119 +267,11 @@ to_string(ManifestSource s)
|
||||
|
||||
class DatabaseCon;
|
||||
|
||||
/** Remembers manifests with the highest sequence number.
|
||||
|
||||
Every master key held is retained in one of three tiers:
|
||||
|
||||
- list: the master key is pinned (trusted), configured, or a validator
|
||||
list or the wallet carried a manifest for it. Never evicted, whatever
|
||||
transport later delivered a newer manifest for it.
|
||||
|
||||
- ledger: otherwise, if the manifest held was read from the ledger. A
|
||||
lookup cache over the ledger, capped at ledgerCapacity; when full the
|
||||
least active entry is dropped, to be read again on demand.
|
||||
|
||||
- gossip: otherwise. Capped at gossipCapacity. A master key enters only
|
||||
if its ephemeral key is in seen_, a rotating bloom filter of ephemeral
|
||||
keys that recently signed a validation this node verified, and only by
|
||||
displacing an entry less active than a single fresh sighting.
|
||||
|
||||
Activity is the number of validations seen from the held ephemeral key,
|
||||
decayed exponentially with a half-life of halfLife. It is what decides
|
||||
eviction in both capped tiers.
|
||||
|
||||
Gossip that fails the bloom filter is held, unverified, in a fixed ring of
|
||||
pendingCapacity entries keyed by ephemeral key. The first validation
|
||||
signed by that key releases it into the cache. Without this a validator's
|
||||
manifest, which normally arrives before its first validation, would be
|
||||
refused everywhere and never offered again.
|
||||
|
||||
A held manifest that conflicts with a new one over a key, and is held in a
|
||||
lower tier than the new one would be, is evicted rather than the new one
|
||||
refused. A manifest is never evicted by one ranked at or below it.
|
||||
|
||||
Ledger access goes through readLedger() alone, so a source other than a
|
||||
full SHAMap can be substituted there.
|
||||
*/
|
||||
/** Remembers manifests with the highest sequence number. */
|
||||
class ManifestCache
|
||||
{
|
||||
public:
|
||||
/** Manifests held for gossip-tier master keys. */
|
||||
static constexpr std::size_t gossipCapacity = 64;
|
||||
|
||||
/** Manifests held for ledger-tier master keys: a lookup cache. */
|
||||
static constexpr std::size_t ledgerCapacity = 512;
|
||||
|
||||
/** Gossip manifests waiting, unverified, for their ephemeral key. */
|
||||
static constexpr std::size_t pendingCapacity = 64;
|
||||
|
||||
/** Ceiling on remembered ephemeral key probes before they are dropped.
|
||||
|
||||
A cache of negatives, so dropping it costs at most one extra ledger
|
||||
read per key.
|
||||
*/
|
||||
static constexpr std::size_t probeLimit = 4096;
|
||||
|
||||
/** Time over which a master key's validation activity halves. */
|
||||
static constexpr std::chrono::seconds halfLife{300};
|
||||
|
||||
/** Epoch of seen_. A key is remembered for three to four of them. */
|
||||
static constexpr std::chrono::seconds seenInterval{120};
|
||||
|
||||
/** Ephemeral keys recently seen signing a verified validation.
|
||||
|
||||
2 KiB (16 Kibit) per generation, 4 generations: 8 KiB in all. Around
|
||||
0.2% false positives per live generation with a thousand distinct keys
|
||||
in it.
|
||||
*/
|
||||
using SeenFilter = RotatingBloomFilter<std::size_t{1} << 14, 4, 4>;
|
||||
|
||||
private:
|
||||
struct Slot
|
||||
{
|
||||
Slot(
|
||||
ManifestSource source_,
|
||||
ManifestSource tier_,
|
||||
bool vouched_,
|
||||
double weight_)
|
||||
: source(source_), tier(tier_), vouched(vouched_), weight(weight_)
|
||||
{
|
||||
}
|
||||
|
||||
/// Where the manifest held for this master key came from.
|
||||
ManifestSource source;
|
||||
|
||||
/// What it is retained as; see tierOf().
|
||||
ManifestSource tier;
|
||||
|
||||
/// A validator list, the config or the wallet once carried a manifest
|
||||
/// for this master key. Survives later updates from other sources.
|
||||
bool vouched;
|
||||
|
||||
/** Validations seen from the held ephemeral key, decayed.
|
||||
|
||||
Stored as the sum, over the times t it was seen, of
|
||||
2^((t - weightBase_) / halfLife). Every slot shares weightBase_, so
|
||||
comparing two stored weights compares their decayed values at any
|
||||
common instant, without computing a decay.
|
||||
|
||||
Atomic so that a sighting is recorded under a shared lock.
|
||||
Everything else in a slot changes only under an exclusive one.
|
||||
*/
|
||||
std::atomic<double> weight;
|
||||
};
|
||||
|
||||
struct Pending
|
||||
{
|
||||
/// Arrival order; the lowest is replaced first.
|
||||
std::uint64_t order = 0;
|
||||
std::optional<Manifest> manifest;
|
||||
/// manifest has been checked with Manifest::verify(), and passed.
|
||||
bool verified = false;
|
||||
};
|
||||
|
||||
beast::Journal j_;
|
||||
Stopwatch& clock_;
|
||||
std::shared_mutex mutable mutex_;
|
||||
|
||||
/** Active manifests stored by master public key. */
|
||||
@@ -460,134 +280,65 @@ private:
|
||||
/** Master public keys stored by current ephemeral public key. */
|
||||
hash_map<PublicKey, PublicKey> signingToMasterKeys_;
|
||||
|
||||
/** Retention state, by master public key. In step with map_. */
|
||||
hash_map<PublicKey, Slot> slots_;
|
||||
/** Master keys always offered to a peer, whatever their recency.
|
||||
|
||||
/** Number of slots_ in each tier, indexed by ManifestSource. */
|
||||
std::array<std::size_t, 3> tierSize_{};
|
||||
|
||||
/** Master keys retained as list regardless of source; set by pin(). */
|
||||
Set by pin(); in practice the master keys on the configured validator
|
||||
lists, which are the manifests consensus actually depends on.
|
||||
*/
|
||||
hash_set<PublicKey> pinned_;
|
||||
|
||||
/** Master keys of the configured validator token and revocation. */
|
||||
hash_set<PublicKey> configured_;
|
||||
/** Recency of use, keyed by master public key.
|
||||
|
||||
std::array<Pending, pendingCapacity> pending_;
|
||||
std::uint64_t pendingOrder_ = 0;
|
||||
std::size_t pendingSize_ = 0;
|
||||
The structure of this map is guarded by mutex_ exactly as map_ is:
|
||||
entries are created next to it in applyManifest() and are never
|
||||
removed. The counters themselves are atomic, so recording a hit is a
|
||||
write to an atomic rather than a structural modification and is legal
|
||||
while only a shared lock is held.
|
||||
|
||||
This is deliberately not a second mutex. A second mutex would have to
|
||||
be ordered against mutex_, and that ordering would be an unenforced
|
||||
invariant that any future caller could invert.
|
||||
*/
|
||||
hash_map<PublicKey, std::atomic<std::uint64_t>> mutable lastUsed_;
|
||||
std::atomic<std::uint64_t> mutable tick_{0};
|
||||
|
||||
/** Ephemeral keys already probed against the ledger, and where.
|
||||
|
||||
Bounds the reads driven by incoming validations to one per key per
|
||||
ledger. Guarded by mutex_ in exclusive mode.
|
||||
ledger. A key that resolves does not come back here: the mapping then
|
||||
lives in signingToMasterKeys_ and applyLedgerSigningKey() answers from
|
||||
it before reaching this map.
|
||||
|
||||
Unlike lastUsed_ this is written structurally, so it is guarded by
|
||||
mutex_ in exclusive mode.
|
||||
*/
|
||||
hash_map<PublicKey, std::uint32_t> probed_;
|
||||
|
||||
/** Reference instant for Slot::weight. Moved forward by rebase(). */
|
||||
Stopwatch::time_point weightBase_;
|
||||
|
||||
SeenFilter seen_{seenInterval};
|
||||
|
||||
std::atomic<std::uint32_t> seq_{0};
|
||||
|
||||
/** Tier a master key is retained in, given where its manifest came from.
|
||||
/** Record that a manifest was looked up.
|
||||
|
||||
@pre The caller holds mutex_, shared or exclusive.
|
||||
*/
|
||||
ManifestSource
|
||||
tierOf(PublicKey const& master, ManifestSource source, bool vouched) const;
|
||||
|
||||
/** Weight of a single sighting at now. @pre mutex_ held. */
|
||||
double
|
||||
unit(Stopwatch::time_point now) const;
|
||||
|
||||
/** Whether weightBase_ is far enough behind to need moving now.
|
||||
|
||||
@pre mutex_ held.
|
||||
*/
|
||||
bool
|
||||
overdue(Stopwatch::time_point now) const;
|
||||
|
||||
/** Move weightBase_ toward now, rescaling every weight to match.
|
||||
|
||||
@pre mutex_ held exclusively.
|
||||
*/
|
||||
void
|
||||
rebase(Stopwatch::time_point now);
|
||||
touch(PublicKey const& masterKey) const;
|
||||
|
||||
/** The least active entry in a tier, other than exclude.
|
||||
|
||||
@pre mutex_ held.
|
||||
*/
|
||||
std::optional<std::pair<PublicKey, double>>
|
||||
coldest(ManifestSource tier, PublicKey const* exclude) const;
|
||||
|
||||
/** Whether master could enter tier, competing with weight.
|
||||
|
||||
A capped tier that is full admits by evicting its least active entry.
|
||||
The lookup cache always does; the gossip tier only if that entry is
|
||||
less active than weight.
|
||||
|
||||
@pre mutex_ held.
|
||||
*/
|
||||
bool
|
||||
admissible(ManifestSource tier, PublicKey const& master, double weight)
|
||||
const;
|
||||
|
||||
/** Evict what admissible() said could be evicted.
|
||||
|
||||
@pre mutex_ held exclusively.
|
||||
*/
|
||||
bool
|
||||
makeRoom(ManifestSource tier, PublicKey const& master, double weight);
|
||||
|
||||
/** Forget a master key entirely. @pre mutex_ held exclusively. */
|
||||
void
|
||||
erase(PublicKey const& master);
|
||||
|
||||
/** Move a slot to another tier. @pre mutex_ held exclusively. */
|
||||
void
|
||||
retier(Slot& slot, ManifestSource tier);
|
||||
|
||||
/** Hold gossip until its ephemeral key is seen.
|
||||
|
||||
@param verified m has already been checked with Manifest::verify().
|
||||
Only a verified manifest may displace one already waiting for
|
||||
the same ephemeral key.
|
||||
|
||||
@pre mutex_ held exclusively; m names an ephemeral key.
|
||||
*/
|
||||
void
|
||||
addPending(Manifest m, bool verified);
|
||||
|
||||
/** @pre mutex_ held. */
|
||||
bool
|
||||
hasPending(PublicKey const& signingKey) const;
|
||||
|
||||
/** @pre mutex_ held exclusively. */
|
||||
std::optional<Manifest>
|
||||
takePending(PublicKey const& signingKey);
|
||||
|
||||
/** Adopt a gossip entry the ledger turns out to hold verbatim.
|
||||
|
||||
It is then a ledger lookup cache entry, and frees its gossip slot.
|
||||
|
||||
@pre mutex_ held exclusively.
|
||||
*/
|
||||
void
|
||||
adopt(Manifest const& m);
|
||||
|
||||
/** The ledger object for a manifest, keyed by its master or ephemeral key.
|
||||
|
||||
The one place the cache reads the ledger.
|
||||
*/
|
||||
std::shared_ptr<SLE const>
|
||||
readLedger(ReadView const& view, PublicKey const& key) const;
|
||||
std::atomic<std::uint32_t> seq_{0};
|
||||
|
||||
public:
|
||||
/** Ceiling on the unpinned manifests offered to a newly connected peer. */
|
||||
static constexpr std::size_t gossipLimit = 64;
|
||||
|
||||
/** Ceiling on remembered ephemeral key probes before they are dropped.
|
||||
|
||||
A cache of negatives, so dropping it costs at most one extra ledger
|
||||
read per key.
|
||||
*/
|
||||
static constexpr std::size_t probeLimit = 4096;
|
||||
|
||||
explicit ManifestCache(
|
||||
beast::Journal j = beast::Journal(beast::Journal::getNullSink()),
|
||||
Stopwatch& clock = stopwatch());
|
||||
beast::Journal j = beast::Journal(beast::Journal::getNullSink()))
|
||||
: j_(j)
|
||||
{
|
||||
}
|
||||
|
||||
/** A monotonically increasing number used to detect new manifests. */
|
||||
std::uint32_t
|
||||
@@ -657,65 +408,24 @@ public:
|
||||
bool
|
||||
revoked(PublicKey const& pk) const;
|
||||
|
||||
/** Returns the tier a master key is held in, if it is held.
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::optional<ManifestSource>
|
||||
getTier(PublicKey const& pk) const;
|
||||
|
||||
/** Add manifest to cache.
|
||||
|
||||
@param m Manifest to add
|
||||
|
||||
@param source Where it came from. The default is the trusted path
|
||||
taken by validator lists, the config and the wallet.
|
||||
|
||||
@param view For gossip, a ledger to consult first. A master key whose
|
||||
ledger object is at the same or a later sequence -- above all, a
|
||||
revocation -- has that object ingested instead, and the gossip is
|
||||
stale. Ignored for other sources.
|
||||
|
||||
@return `ManifestDisposition::accepted` if successful, or
|
||||
the disposition explaining why admission was refused
|
||||
`stale` or `invalid` otherwise
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
ManifestDisposition
|
||||
applyManifest(
|
||||
Manifest m,
|
||||
ManifestSource source = ManifestSource::list,
|
||||
ReadView const* view = nullptr);
|
||||
applyManifest(Manifest m);
|
||||
|
||||
/** Record a verified, current validation signed by an ephemeral key.
|
||||
/** Set the master keys that are always offered to peers.
|
||||
|
||||
Marks the key as seen for gossip admission, adds to the activity of the
|
||||
master key it belongs to, and releases a pending manifest for it.
|
||||
|
||||
@param signingKey Ephemeral key that signed the validation
|
||||
|
||||
@param view Passed on to applyManifest() for a released manifest
|
||||
|
||||
@return the manifest released and accepted, if any, for the caller to
|
||||
relay
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::optional<Manifest>
|
||||
noteValidation(PublicKey const& signingKey, ReadView const* view = nullptr);
|
||||
|
||||
/** Set the master keys retained as list, whatever their source.
|
||||
|
||||
Replaces any previous set. A key that leaves falls back to the tier of
|
||||
its source, which sheds its least active entries if that puts it over
|
||||
capacity. Bumps sequence() when the set actually changes, so a cached
|
||||
gossip message built from it is rebuilt.
|
||||
Replaces any previous set. Bumps sequence() when the set actually
|
||||
changes, so a cached gossip message built from it is rebuilt.
|
||||
|
||||
@param keys Master public keys to pin
|
||||
|
||||
@@ -748,7 +458,8 @@ public:
|
||||
manifest found and feeds it through applyManifest(), so an on-chain
|
||||
manifest is subject to exactly the same staleness, revocation and
|
||||
key-reuse rules -- and the same signature check -- as one arriving by
|
||||
peer gossip or in a published list.
|
||||
peer gossip or in a published list. It is a third source of manifests,
|
||||
not a more trusted one.
|
||||
|
||||
Probes a known key set rather than scanning the ledger's transactions:
|
||||
this costs one SHAMap read per key, and picks up manifests published in
|
||||
@@ -766,39 +477,22 @@ public:
|
||||
std::size_t
|
||||
applyLedger(ReadView const& view, hash_set<PublicKey> const& masterKeys);
|
||||
|
||||
/** Refresh from the ledger everything held or pinned.
|
||||
|
||||
Probes every pinned and configured master key, and every master key
|
||||
held in any tier. This is what brings in a revocation, which is
|
||||
published on-ledger by master key and nowhere else, for a master key
|
||||
whose ephemeral key the cache already resolves and so never misses on.
|
||||
|
||||
@return the number of manifests newly accepted
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::size_t
|
||||
applyLedger(ReadView const& view);
|
||||
|
||||
/** Resolve an ephemeral signing key against a manifest published on-ledger.
|
||||
|
||||
The miss path of the ledger tier. applyLedger() probes a known master
|
||||
key set, which cannot help a key this node has no manifest for: the
|
||||
master key is exactly what is missing. SetManifest writes a second copy
|
||||
of every manifest keyed by its ephemeral key, so that case is one read
|
||||
rather than a search.
|
||||
applyLedger() probes a known master key set, which cannot help a key
|
||||
this node has no manifest for: the master key is exactly what is
|
||||
missing. SetManifest writes a second copy of every manifest keyed by
|
||||
its ephemeral key, so that case is one read rather than a search.
|
||||
|
||||
Anything found is fed through applyManifest(), so an on-chain manifest
|
||||
faces the same signature check and the same staleness, revocation and
|
||||
key-reuse rules as one arriving by gossip. The answer is read back out
|
||||
of the cache rather than taken from the ledger object, because
|
||||
applyManifest() may decline it.
|
||||
key-reuse rules as one arriving by gossip. The ledger is a transport
|
||||
here, not an authority: the answer is read back out of the cache rather
|
||||
than taken from the ledger object, because applyManifest() may decline
|
||||
it.
|
||||
|
||||
A key is probed at most once per ledger, and a key that resolves is
|
||||
answered from the cache thereafter without any ledger read, until it
|
||||
is evicted.
|
||||
answered from the cache thereafter without any ledger read.
|
||||
|
||||
@param view Ledger to read from
|
||||
@param signingKey Ephemeral public key to resolve
|
||||
@@ -812,37 +506,6 @@ public:
|
||||
std::optional<PublicKey>
|
||||
applyLedgerSigningKey(ReadView const& view, PublicKey const& signingKey);
|
||||
|
||||
/** Ingest the manifests listed in the ledger's manifest directory.
|
||||
|
||||
Every master key with a manifest on-ledger is listed in
|
||||
keylet::manifestDir(), so unlike applyLedger() this needs no key to
|
||||
start from. Pinned, configured and held master keys are refreshed as
|
||||
applyLedger() would; any other is admitted to the ledger tier only
|
||||
while it has room, so a bulk read fills the lookup cache without
|
||||
churning it.
|
||||
|
||||
The view need not be complete: one in which the directory and the
|
||||
objects it lists can be read will do. That is what lets a node read
|
||||
them during sync, before it holds the rest of the state map. The
|
||||
ledger need not be validated either: every manifest read is verified
|
||||
as from any other source, and the cache only ever moves forward, so
|
||||
the worst an unvalidated ledger can do is leave something out.
|
||||
|
||||
@param view Ledger to read from
|
||||
@param maxPages Most directory pages to read; see
|
||||
forEachLedgerManifest()
|
||||
|
||||
@return the number of manifests newly accepted
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::size_t
|
||||
applyLedgerDirectory(
|
||||
ReadView const& view,
|
||||
std::uint64_t maxPages = std::numeric_limits<std::uint64_t>::max());
|
||||
|
||||
/** Populate manifest cache with manifests in database and config.
|
||||
|
||||
@param dbCon Database connection with dbTable
|
||||
@@ -879,15 +542,11 @@ public:
|
||||
void
|
||||
load(DatabaseCon& dbCon, std::string const& dbTable);
|
||||
|
||||
/** Save list-tier manifests to database.
|
||||
|
||||
Ledger-tier manifests are on the ledger already, revocations included,
|
||||
and gossip-tier ones are not worth carrying across a restart.
|
||||
/** Save cached manifests to database.
|
||||
|
||||
@param dbCon Database connection with `ValidatorManifests` table
|
||||
|
||||
@param isTrusted Function that returns true if manifest is trusted.
|
||||
Called without the cache locked, so it may call back into it.
|
||||
@param isTrusted Function that returns true if manifest is trusted
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
@@ -955,16 +614,19 @@ public:
|
||||
|
||||
/** Invokes the callback for the manifests worth offering a new peer.
|
||||
|
||||
The list and gossip tiers: at most the listed validators plus
|
||||
gossipCapacity. The ledger tier is left out, since any peer that is
|
||||
not amendment blocked can read it from the ledger.
|
||||
Offering the whole cache means offering everything the node has ever
|
||||
seen, which grows without bound and is mostly of no use to the peer.
|
||||
This offers the pinned set plus up to gossipLimit further manifests,
|
||||
most recently used first. A manifest left out still reaches the peer by
|
||||
ordinary relay if it turns out to be needed.
|
||||
|
||||
@note Do not call ManifestCache member functions from within the
|
||||
callback. This can re-lock the mutex from the same thread, which is UB.
|
||||
@note Do not write ManifestCache member variables from
|
||||
within the callback. This can lead to data races.
|
||||
|
||||
@param pf Pre-function called with the number of times f will be called
|
||||
@param pf Pre-function called with the maximum number of times f will be
|
||||
called (useful for memory allocations)
|
||||
|
||||
@param f Function called for each manifest
|
||||
|
||||
@@ -978,12 +640,40 @@ public:
|
||||
{
|
||||
std::shared_lock lock{mutex_};
|
||||
|
||||
pf(tierSize_[static_cast<std::size_t>(ManifestSource::list)] +
|
||||
tierSize_[static_cast<std::size_t>(ManifestSource::gossip)]);
|
||||
// Rank the unpinned entries by recency and keep the head. Pointers
|
||||
// into map_ stay valid: it is node based and the shared lock is held
|
||||
// throughout.
|
||||
std::vector<std::pair<std::uint64_t, PublicKey const*>> ranked;
|
||||
ranked.reserve(map_.size());
|
||||
for (auto const& [key, manifest] : map_)
|
||||
{
|
||||
(void)manifest;
|
||||
if (pinned_.count(key))
|
||||
continue;
|
||||
auto const used = lastUsed_.find(key);
|
||||
ranked.emplace_back(
|
||||
used == lastUsed_.end()
|
||||
? 0
|
||||
: used->second.load(std::memory_order_relaxed),
|
||||
&key);
|
||||
}
|
||||
|
||||
for (auto const& [key, slot] : slots_)
|
||||
if (slot.tier != ManifestSource::ledger)
|
||||
f(map_.find(key)->second);
|
||||
auto const keep = std::min(gossipLimit, ranked.size());
|
||||
std::partial_sort(
|
||||
ranked.begin(),
|
||||
ranked.begin() + keep,
|
||||
ranked.end(),
|
||||
[](auto const& a, auto const& b) { return a.first > b.first; });
|
||||
|
||||
// An upper bound: a pinned key need not have a manifest yet.
|
||||
pf(pinned_.size() + keep);
|
||||
|
||||
for (auto const& key : pinned_)
|
||||
if (auto const iter = map_.find(key); iter != map_.end())
|
||||
f(iter->second);
|
||||
|
||||
for (std::size_t i = 0; i < keep; ++i)
|
||||
f(map_.find(*ranked[i].second)->second);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_MISC_MANIFESTLEDGER_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_MANIFESTLEDGER_H_INCLUDED
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ApplyView;
|
||||
class ReadView;
|
||||
struct Manifest;
|
||||
|
||||
/** Write a manifest to the ledger, replacing any earlier one for its master.
|
||||
|
||||
A manifest is stored twice so that it can be found from either key:
|
||||
|
||||
keylet::manifest(masterKey) -> obj1, sfManifestID -> obj2
|
||||
keylet::manifest(signingKey) -> obj2, sfManifestID -> obj1
|
||||
|
||||
A revocation has no signing key, so it is stored only as obj1, with no
|
||||
sfManifestID. The master key's account points at obj1 with its own
|
||||
sfManifestID.
|
||||
|
||||
obj1 is listed in keylet::manifestDir() and records its page there in
|
||||
sfOwnerNode. The entry is made with the master key's first manifest and is
|
||||
never removed: every later manifest for the master key, a revocation
|
||||
included, is written at the same key, so the entry stays and the page is
|
||||
carried from the old obj1 to the new. Removing and re-adding it instead
|
||||
would move it between pages on every key rotation for no gain. obj2 is not
|
||||
listed; it moves with the ephemeral key, and a reader finds it from obj1.
|
||||
|
||||
Both of the old objects are erased and both new ones written afresh, so the
|
||||
two copies can never drift apart.
|
||||
|
||||
@param view View to write to
|
||||
@param account The master key's account root, which is updated
|
||||
@param manifest The manifest as signed, in its serialized STObject form
|
||||
@param parsed The same manifest, deserialized and checked by the caller
|
||||
@param j Journal for reporting a corrupt ledger
|
||||
|
||||
@return tesSUCCESS; tecDIR_FULL if the directory cannot take another
|
||||
master key; tefBAD_LEDGER if the existing objects are inconsistent
|
||||
*/
|
||||
TER
|
||||
writeManifestObjects(
|
||||
ApplyView& view,
|
||||
std::shared_ptr<SLE> const& account,
|
||||
STObject const& manifest,
|
||||
Manifest const& parsed,
|
||||
beast::Journal j);
|
||||
|
||||
/** Visit every master key's manifest object listed in keylet::manifestDir().
|
||||
|
||||
Each master key is visited once, with its current object: a revocation if
|
||||
it has been revoked. Entries that do not name an ltMANIFEST object are
|
||||
skipped.
|
||||
|
||||
The view need not be complete. It must be able to read the directory and
|
||||
the objects listed in it, which is all this reads; a view over a partially
|
||||
synced state map that holds them will do.
|
||||
|
||||
@param view View to read from
|
||||
@param f Called with each master manifest object
|
||||
@param maxPages Most directory pages to read, the root page included.
|
||||
Pages are only ever appended, so this reads the oldest entries. A
|
||||
partial view need hold no more pages than this.
|
||||
*/
|
||||
void
|
||||
forEachLedgerManifest(
|
||||
ReadView const& view,
|
||||
std::function<void(std::shared_ptr<SLE const> const&)> const& f,
|
||||
std::uint64_t maxPages = dirNodeMaxPages);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -25,7 +25,6 @@
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/LedgerToJson.h>
|
||||
#include <xrpld/app/ledger/LocalTxs.h>
|
||||
#include <xrpld/app/ledger/ManifestSync.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/ledger/OrderBookDB.h>
|
||||
#include <xrpld/app/ledger/TransactionMaster.h>
|
||||
@@ -320,13 +319,6 @@ private:
|
||||
void
|
||||
publishNewerManifests(ReadView const& ledger);
|
||||
|
||||
/** Read manifests from ledgers peers report, while too few of this node's
|
||||
validators can be resolved to trust any ledger. See
|
||||
[early_manifest_sync].
|
||||
*/
|
||||
void
|
||||
bootstrapManifests();
|
||||
|
||||
public:
|
||||
bool
|
||||
beginConsensus(
|
||||
@@ -675,12 +667,6 @@ private:
|
||||
std::atomic<OperatingMode> mMode;
|
||||
|
||||
std::atomic<bool> needNetworkLedger_{false};
|
||||
|
||||
/** State of bootstrapManifests(), which only the heartbeat calls. */
|
||||
std::mutex manifestBootstrapMutex_;
|
||||
hash_set<uint256> manifestBootstrapTried_;
|
||||
std::chrono::steady_clock::time_point manifestBootstrapNext_{};
|
||||
|
||||
std::atomic<bool> amendmentBlocked_{false};
|
||||
std::atomic<bool> amendmentWarned_{false};
|
||||
std::atomic<bool> unlBlocked_{false};
|
||||
@@ -1041,8 +1027,6 @@ NetworkOPsImp::processHeartbeatTimer()
|
||||
}
|
||||
CLOG(clog.ss()) << ". ";
|
||||
|
||||
bootstrapManifests();
|
||||
|
||||
setHeartbeatTimer();
|
||||
}
|
||||
|
||||
@@ -1262,105 +1246,6 @@ NetworkOPsImp::publishNewerManifests(ReadView const& ledger)
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/** How often a node that cannot resolve enough of its validators looks for
|
||||
ledgers to read their manifests from. */
|
||||
constexpr std::chrono::seconds manifestBootstrapInterval{15};
|
||||
|
||||
/** Most ledgers it starts reading at each look. */
|
||||
constexpr std::size_t manifestBootstrapWidth = 3;
|
||||
|
||||
/** Ledgers remembered as read, past which the record starts over. */
|
||||
constexpr std::size_t manifestBootstrapMemory = 256;
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
NetworkOPsImp::bootstrapManifests()
|
||||
{
|
||||
// Only until a ledger has been validated: from then on the manifests are
|
||||
// kept current from each ledger as it closes, in beginConsensus().
|
||||
if (!app_.config().EARLY_MANIFEST_SYNC || m_ledgerMaster.haveValidated())
|
||||
return;
|
||||
|
||||
std::vector<uint256> picked;
|
||||
{
|
||||
auto const now = std::chrono::steady_clock::now();
|
||||
std::lock_guard lock(manifestBootstrapMutex_);
|
||||
if (now < manifestBootstrapNext_)
|
||||
return;
|
||||
manifestBootstrapNext_ = now + manifestBootstrapInterval;
|
||||
|
||||
// The validators whose manifests matter: the trusted ones once the
|
||||
// trusted set has been worked out, every listed one before that.
|
||||
auto keys = app_.validators().getTrustedMasterKeys();
|
||||
bool const trusted = !keys.empty();
|
||||
if (!trusted)
|
||||
app_.validators().for_each_listed(
|
||||
[&keys](PublicKey const& pk, bool) { keys.insert(pk); });
|
||||
if (keys.empty())
|
||||
return;
|
||||
|
||||
// Enough of them to make a quorum.
|
||||
std::size_t const needed =
|
||||
manifestQuorum(keys.size(), trusted, app_.validators().quorum());
|
||||
|
||||
auto const& manifests = app_.validatorManifests();
|
||||
std::size_t resolved = 0;
|
||||
for (auto const& pk : keys)
|
||||
if (manifests.getManifest(pk))
|
||||
++resolved;
|
||||
|
||||
if (resolved >= needed)
|
||||
return;
|
||||
|
||||
std::vector<uint256> reported;
|
||||
for (auto const& peer : app_.overlay().getActivePeers())
|
||||
reported.push_back(peer->getClosedLedgerHash());
|
||||
|
||||
if (manifestBootstrapTried_.size() > manifestBootstrapMemory)
|
||||
manifestBootstrapTried_.clear();
|
||||
|
||||
picked = pickManifestCandidates(
|
||||
reported, manifestBootstrapTried_, manifestBootstrapWidth);
|
||||
if (picked.empty())
|
||||
return;
|
||||
|
||||
manifestBootstrapTried_.insert(picked.begin(), picked.end());
|
||||
|
||||
JLOG(m_journal.info())
|
||||
<< "Manifests held for " << resolved << " of " << keys.size()
|
||||
<< " validators, " << needed << " needed: reading manifests from "
|
||||
<< picked.size() << " ledger(s) reported by " << reported.size()
|
||||
<< " peer(s)";
|
||||
}
|
||||
|
||||
// Each is acquired as consensus would acquire it. Before a ledger has
|
||||
// been validated, InboundLedger reads the manifests first, by key and from
|
||||
// the directory, and only then fetches the rest. The first pick is
|
||||
// usually the ledger checkLastClosedLedger() is already acquiring. Ledgers
|
||||
// of the same network share nearly all their nodes, so each further one
|
||||
// costs little more than what changed; one that only a peer has fails and
|
||||
// costs nothing more.
|
||||
//
|
||||
// Runs on the heartbeat, ahead of re-arming it, so nothing here may throw.
|
||||
for (auto const& hash : picked)
|
||||
{
|
||||
JLOG(m_journal.debug()) << "Reading manifests from ledger " << hash;
|
||||
try
|
||||
{
|
||||
app_.getInboundLedgers().acquire(
|
||||
hash, 0, InboundLedger::Reason::CONSENSUS);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(m_journal.warn()) << "Could not start reading manifests from "
|
||||
<< "ledger " << hash << ": " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::processTransaction(
|
||||
std::shared_ptr<Transaction>& transaction,
|
||||
@@ -2196,13 +2081,13 @@ NetworkOPsImp::beginConsensus(
|
||||
app_.validators().setNegativeUNL(prevLedger->negativeUNL());
|
||||
// Pull in any manifests published on-ledger before the trusted set is
|
||||
// recomputed, so a validator that rotated its ephemeral key on-chain is
|
||||
// resolved to the new signing key in this same round. Every master key the
|
||||
// cache holds or pins is probed, not only the trusted ones: a revocation is
|
||||
// published on-ledger by master key alone, and a master key whose ephemeral
|
||||
// key the cache already resolves would otherwise never be looked up again.
|
||||
// resolved to the new signing key in this same round. The master keys come
|
||||
// from the published lists, so this needs no bootstrap: only the ephemeral
|
||||
// half of the mapping ever comes from a manifest.
|
||||
if (prevLedger->rules().enabled(featureOnChainManifests))
|
||||
{
|
||||
app_.validatorManifests().applyLedger(*prevLedger);
|
||||
app_.validatorManifests().applyLedger(
|
||||
*prevLedger, app_.validators().getTrustedMasterKeys());
|
||||
|
||||
// The reverse of applyLedger above. Manifests reach us by peer gossip
|
||||
// and in published validator lists, both of which can arrive before
|
||||
@@ -2218,8 +2103,8 @@ NetworkOPsImp::beginConsensus(
|
||||
app_.overlay(),
|
||||
app_.getHashRouter());
|
||||
|
||||
// Pin the trusted master keys: their manifests are retained in the list
|
||||
// tier and offered to every new peer, whichever source delivered them.
|
||||
// Pin the trusted master keys so they are always offered to a new peer and
|
||||
// cannot be crowded out of the gossip set by more recently used manifests.
|
||||
app_.validatorManifests().pin(app_.validators().getTrustedMasterKeys());
|
||||
|
||||
if (!changes.added.empty() || !changes.removed.empty())
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_MISC_ROTATINGBLOOMFILTER_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_ROTATINGBLOOMFILTER_H_INCLUDED
|
||||
|
||||
#include <xrpl/basics/hardened_hash.h>
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Approximate membership of everything inserted recently, in fixed space.
|
||||
|
||||
Time is cut into epochs of a fixed interval. Each of the Generations
|
||||
filters holds the insertions of one epoch: an insertion goes to the filter
|
||||
for the current epoch, and a query consults every filter whose epoch is
|
||||
one of the last Generations. An element is therefore reported for at least
|
||||
(Generations - 1) and at most Generations intervals after it was last
|
||||
inserted, and then forgotten.
|
||||
|
||||
A filter is cleared lazily, when its slot is first wanted for a newer
|
||||
epoch. Nothing has to tick: an idle filter costs nothing, and a clock that
|
||||
jumps forward simply finds every slot expired.
|
||||
|
||||
Within the window there are no false negatives. False positives occur at
|
||||
the usual bloom rate for one epoch's insertions, once per live generation.
|
||||
The footprint is Generations * Bits / 8 bytes however much is inserted:
|
||||
saturating the filter raises the false positive rate and never grows it.
|
||||
|
||||
Bit positions come from a hash seeded randomly per instance, so nobody
|
||||
without the seed can choose elements that set a particular set of bits.
|
||||
|
||||
@par Thread Safety
|
||||
Safe to call concurrently. The internal mutex is never held while calling
|
||||
out, so this is a leaf in any lock order.
|
||||
*/
|
||||
template <std::size_t Bits, std::size_t Hashes, std::size_t Generations>
|
||||
class RotatingBloomFilter
|
||||
{
|
||||
static_assert(
|
||||
Bits >= 64 && (Bits & (Bits - 1)) == 0,
|
||||
"Bits must be a power of two, at least 64");
|
||||
static_assert(
|
||||
Bits <= (std::size_t{1} << 32),
|
||||
"bit positions are computed in 32 bits");
|
||||
static_assert(Hashes >= 1 && Hashes <= 16);
|
||||
static_assert(
|
||||
Generations >= 2,
|
||||
"a single generation would forget everything at each rotation");
|
||||
|
||||
public:
|
||||
using clock_type = std::chrono::steady_clock;
|
||||
using time_point = clock_type::time_point;
|
||||
using duration = clock_type::duration;
|
||||
|
||||
/** Size of the filter state, in bytes. */
|
||||
static constexpr std::size_t bytes = Generations * Bits / 8;
|
||||
|
||||
explicit RotatingBloomFilter(duration interval)
|
||||
: interval_(interval > duration::zero() ? interval : duration{1})
|
||||
{
|
||||
epochs_.fill(none);
|
||||
}
|
||||
|
||||
RotatingBloomFilter(RotatingBloomFilter const&) = delete;
|
||||
RotatingBloomFilter&
|
||||
operator=(RotatingBloomFilter const&) = delete;
|
||||
|
||||
/** Length of one epoch. */
|
||||
duration
|
||||
interval() const
|
||||
{
|
||||
return interval_;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void
|
||||
insert(T const& item, time_point now)
|
||||
{
|
||||
auto const positions = probe(item);
|
||||
auto const epoch = epochOf(now);
|
||||
auto const slot = slotOf(epoch);
|
||||
|
||||
std::lock_guard lock{mutex_};
|
||||
|
||||
// The slot last held an earlier epoch, now out of the window or about
|
||||
// to be: it is reused for this one. A slot already ahead of us (the
|
||||
// caller read the clock before another thread rotated) keeps its bits,
|
||||
// and ours are added to them: that can only remember the element a
|
||||
// little longer than asked, never forget it early.
|
||||
if (epochs_[slot] < epoch)
|
||||
{
|
||||
bits_[slot].fill(0);
|
||||
epochs_[slot] = epoch;
|
||||
}
|
||||
|
||||
for (auto const p : positions)
|
||||
bits_[slot][p / 64] |= std::uint64_t{1} << (p % 64);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
bool
|
||||
contains(T const& item, time_point now) const
|
||||
{
|
||||
auto const positions = probe(item);
|
||||
auto const oldest =
|
||||
epochOf(now) - static_cast<std::int64_t>(Generations);
|
||||
|
||||
std::lock_guard lock{mutex_};
|
||||
|
||||
for (std::size_t g = 0; g < Generations; ++g)
|
||||
{
|
||||
if (epochs_[g] == none || epochs_[g] <= oldest)
|
||||
continue;
|
||||
|
||||
bool all = true;
|
||||
for (auto const p : positions)
|
||||
{
|
||||
if ((bits_[g][p / 64] & (std::uint64_t{1} << (p % 64))) == 0)
|
||||
{
|
||||
all = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (all)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::size_t words = Bits / 64;
|
||||
static constexpr std::int64_t none =
|
||||
std::numeric_limits<std::int64_t>::min();
|
||||
|
||||
std::int64_t
|
||||
epochOf(time_point now) const
|
||||
{
|
||||
// Floor division, so a clock reading before its own epoch still maps
|
||||
// to consecutive epochs.
|
||||
auto const t = now.time_since_epoch().count();
|
||||
auto const i = interval_.count();
|
||||
return t >= 0 ? t / i : -((-t + i - 1) / i);
|
||||
}
|
||||
|
||||
static std::size_t
|
||||
slotOf(std::int64_t epoch)
|
||||
{
|
||||
auto const g = static_cast<std::int64_t>(Generations);
|
||||
return static_cast<std::size_t>(((epoch % g) + g) % g);
|
||||
}
|
||||
|
||||
/** Bit positions for an element: Kirsch-Mitzenmacher double hashing.
|
||||
|
||||
Hashes positions from a single 64 bit hash, as h1 + i * h2. Forcing h2
|
||||
odd makes it invertible modulo the power of two table size, so the
|
||||
positions for one element are all distinct.
|
||||
*/
|
||||
template <class T>
|
||||
std::array<std::uint32_t, Hashes>
|
||||
probe(T const& item) const
|
||||
{
|
||||
std::uint64_t const h = hash_(item);
|
||||
auto const h1 = static_cast<std::uint32_t>(h);
|
||||
auto const h2 = static_cast<std::uint32_t>(h >> 32) | 1u;
|
||||
constexpr auto mask = static_cast<std::uint32_t>(Bits - 1);
|
||||
|
||||
std::array<std::uint32_t, Hashes> positions;
|
||||
for (std::size_t i = 0; i < Hashes; ++i)
|
||||
positions[i] = (h1 + static_cast<std::uint32_t>(i) * h2) & mask;
|
||||
return positions;
|
||||
}
|
||||
|
||||
hardened_hash<> const hash_;
|
||||
duration const interval_;
|
||||
std::mutex mutable mutex_;
|
||||
std::array<std::int64_t, Generations> epochs_;
|
||||
std::array<std::array<std::uint64_t, words>, Generations> bits_{};
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,181 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/ManifestLedger.h>
|
||||
#include <xrpld/ledger/ApplyView.h>
|
||||
#include <xrpld/ledger/ReadView.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
TER
|
||||
writeManifestObjects(
|
||||
ApplyView& view,
|
||||
std::shared_ptr<SLE> const& account,
|
||||
STObject const& manifest,
|
||||
Manifest const& parsed,
|
||||
beast::Journal j)
|
||||
{
|
||||
AccountID const id = account->getAccountID(sfAccount);
|
||||
Keylet const klMan1 = keylet::manifest(parsed.masterKey);
|
||||
|
||||
// The page obj1 is listed on, carried across a replacement.
|
||||
std::optional<std::uint64_t> page;
|
||||
|
||||
if (account->isFieldPresent(sfManifestID))
|
||||
{
|
||||
uint256 const firstID = account->getFieldH256(sfManifestID);
|
||||
|
||||
// The account is derived from the master key, so it can only ever
|
||||
// have pointed at that key's obj1. Anything else is corruption, and
|
||||
// carrying its page across would corrupt the directory as well.
|
||||
auto const sleMan1 = view.peek(Keylet{ltMANIFEST, firstID});
|
||||
if (firstID != klMan1.key || !sleMan1 ||
|
||||
sleMan1->getAccountID(sfAccount) != id)
|
||||
{
|
||||
JLOG(j.error()) << "SetManifest: Old manifest object missing or "
|
||||
"misowned (ID1) !! "
|
||||
<< strHex(firstID);
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
// Absent when the previous manifest was a revocation.
|
||||
if (sleMan1->isFieldPresent(sfManifestID))
|
||||
{
|
||||
uint256 const secondID = sleMan1->getFieldH256(sfManifestID);
|
||||
auto const sleMan2 = view.peek(Keylet{ltMANIFEST, secondID});
|
||||
if (secondID == firstID || !sleMan2 ||
|
||||
sleMan2->getAccountID(sfAccount) != id)
|
||||
{
|
||||
JLOG(j.error())
|
||||
<< "SetManifest: Old manifest object missing, misowned or "
|
||||
"self-referential (ID2) !! "
|
||||
<< strHex(secondID);
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
view.erase(sleMan2);
|
||||
}
|
||||
|
||||
if (sleMan1->isFieldPresent(sfOwnerNode))
|
||||
page = sleMan1->getFieldU64(sfOwnerNode);
|
||||
|
||||
view.erase(sleMan1);
|
||||
}
|
||||
|
||||
std::optional<Keylet> klMan2;
|
||||
if (!parsed.revoked() && parsed.signingKey)
|
||||
klMan2 = keylet::manifest(*parsed.signingKey);
|
||||
|
||||
// Neither key may still be occupied: preclaim rejects an ephemeral key held
|
||||
// by another account, and the block above cleared this account's own
|
||||
// copies.
|
||||
if (view.exists(klMan1) || (klMan2 && view.exists(*klMan2)))
|
||||
{
|
||||
JLOG(j.error()) << "SetManifest: Manifest keylet already occupied !! "
|
||||
<< strHex(klMan1.key);
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
// A master key's first manifest. Also an obj1 written before it carried a
|
||||
// page, which has no entry to keep.
|
||||
if (!page)
|
||||
{
|
||||
page = view.dirInsert(
|
||||
keylet::manifestDir(), klMan1.key, [](std::shared_ptr<SLE> const&) {
|
||||
});
|
||||
if (!page)
|
||||
return tecDIR_FULL;
|
||||
}
|
||||
|
||||
// Mirror the manifest losslessly, signatures included, so any node can
|
||||
// reconstruct and independently verify it (ManifestCache::applyLedger).
|
||||
// Field *presence* is copied faithfully: sfVersion is soeDEFAULT in the
|
||||
// manifest format, so materialising an absent one would alter the signed
|
||||
// payload and break verification.
|
||||
auto const write = [&](Keylet const& kl,
|
||||
std::optional<uint256> const& other,
|
||||
std::optional<std::uint64_t> const& dirPage) {
|
||||
auto sleMan = std::make_shared<SLE>(kl);
|
||||
sleMan->setAccountID(sfAccount, id);
|
||||
sleMan->setFieldU32(sfSequence, manifest.getFieldU32(sfSequence));
|
||||
sleMan->setFieldVL(sfPublicKey, manifest.getFieldVL(sfPublicKey));
|
||||
sleMan->setFieldVL(
|
||||
sfMasterSignature, manifest.getFieldVL(sfMasterSignature));
|
||||
if (manifest.isFieldPresent(sfVersion))
|
||||
sleMan->setFieldU16(sfVersion, manifest.getFieldU16(sfVersion));
|
||||
if (manifest.isFieldPresent(sfSigningPubKey))
|
||||
sleMan->setFieldVL(
|
||||
sfSigningPubKey, manifest.getFieldVL(sfSigningPubKey));
|
||||
if (manifest.isFieldPresent(sfSignature))
|
||||
sleMan->setFieldVL(sfSignature, manifest.getFieldVL(sfSignature));
|
||||
if (manifest.isFieldPresent(sfDomain))
|
||||
sleMan->setFieldVL(sfDomain, manifest.getFieldVL(sfDomain));
|
||||
if (other)
|
||||
sleMan->setFieldH256(sfManifestID, *other);
|
||||
if (dirPage)
|
||||
sleMan->setFieldU64(sfOwnerNode, *dirPage);
|
||||
view.insert(sleMan);
|
||||
};
|
||||
|
||||
write(
|
||||
klMan1,
|
||||
klMan2 ? std::optional<uint256>{klMan2->key} : std::nullopt,
|
||||
page);
|
||||
if (klMan2)
|
||||
write(*klMan2, klMan1.key, std::nullopt);
|
||||
|
||||
account->setFieldH256(sfManifestID, klMan1.key);
|
||||
view.update(account);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
void
|
||||
forEachLedgerManifest(
|
||||
ReadView const& view,
|
||||
std::function<void(std::shared_ptr<SLE const> const&)> const& f,
|
||||
std::uint64_t maxPages)
|
||||
{
|
||||
auto const& root = keylet::manifestDir();
|
||||
auto page = view.read(root);
|
||||
|
||||
// Also bounds the walk should a corrupt ledger link the pages in a cycle.
|
||||
maxPages = std::min(maxPages, dirNodeMaxPages);
|
||||
for (std::uint64_t pages = 0; page && pages < maxPages; ++pages)
|
||||
{
|
||||
for (auto const& key : page->getFieldV256(sfIndexes))
|
||||
if (auto const sle = view.read(Keylet{ltMANIFEST, key}))
|
||||
f(sle);
|
||||
|
||||
auto const next = page->getFieldU64(sfIndexNext);
|
||||
if (next == 0 || pages + 1 >= maxPages)
|
||||
break;
|
||||
|
||||
page = view.read(keylet::page(root, next));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -106,18 +106,17 @@ TxQ::FeeMetrics::update(
|
||||
TxQ::Setup const& setup)
|
||||
{
|
||||
std::vector<FeeLevel64> feeLevels;
|
||||
for (auto const& tx : view.txs)
|
||||
{
|
||||
// featureRNG: every proposing validator puts a zero-fee ttENTROPY
|
||||
// into every ledger. These are consensus plumbing, not demand for
|
||||
// ledger space, so they must not raise the expected transaction count
|
||||
// or drag down the median fee level used for escalation.
|
||||
if (tx.first->getTxnType() == ttENTROPY)
|
||||
continue;
|
||||
auto const txBegin = view.txs.begin();
|
||||
auto const txEnd = view.txs.end();
|
||||
auto const size = std::distance(txBegin, txEnd);
|
||||
feeLevels.reserve(size);
|
||||
std::for_each(txBegin, txEnd, [&](auto const& tx) {
|
||||
feeLevels.push_back(getFeeLevelPaid(view, *tx.first));
|
||||
}
|
||||
});
|
||||
std::sort(feeLevels.begin(), feeLevels.end());
|
||||
std::size_t const size = feeLevels.size();
|
||||
XRPL_ASSERT(
|
||||
size == feeLevels.size(),
|
||||
"ripple::TxQ::FeeMetrics::update : fee levels size");
|
||||
|
||||
JLOG((timeLeap ? j_.warn() : j_.debug()))
|
||||
<< "Ledger " << view.info().seq << " has " << size << " transactions. "
|
||||
|
||||
@@ -68,13 +68,11 @@ getManifests(
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
* @brief saveManifests Replaces the table's contents with the given
|
||||
* manifests: every revocation, and every other manifest whose master
|
||||
* key is trusted.
|
||||
* @brief saveManifests Saves all given manifests to the database.
|
||||
* @param session Session with the database.
|
||||
* @param dbTable Name of the database table that will store the manifest.
|
||||
* @param isTrusted Callback that returns true if the key is trusted.
|
||||
* @param manifests Manifests to consider.
|
||||
* @param map Maps public keys to manifests.
|
||||
* @param j Journal.
|
||||
*/
|
||||
void
|
||||
@@ -82,7 +80,7 @@ saveManifests(
|
||||
soci::session& session,
|
||||
std::string const& dbTable,
|
||||
std::function<bool(PublicKey const&)> const& isTrusted,
|
||||
std::vector<Manifest> const& manifests,
|
||||
hash_map<PublicKey, Manifest> const& map,
|
||||
beast::Journal j);
|
||||
|
||||
/**
|
||||
|
||||
@@ -94,22 +94,22 @@ saveManifests(
|
||||
soci::session& session,
|
||||
std::string const& dbTable,
|
||||
std::function<bool(PublicKey const&)> const& isTrusted,
|
||||
std::vector<Manifest> const& manifests,
|
||||
hash_map<PublicKey, Manifest> const& map,
|
||||
beast::Journal j)
|
||||
{
|
||||
soci::transaction tr(session);
|
||||
session << "DELETE FROM " << dbTable;
|
||||
for (auto const& m : manifests)
|
||||
for (auto const& v : map)
|
||||
{
|
||||
// Save all revocation manifests,
|
||||
// but only save trusted non-revocation manifests.
|
||||
if (!m.revoked() && !isTrusted(m.masterKey))
|
||||
if (!v.second.revoked() && !isTrusted(v.second.masterKey))
|
||||
{
|
||||
JLOG(j.info()) << "Untrusted manifest in cache not saved to db";
|
||||
continue;
|
||||
}
|
||||
|
||||
saveManifest(session, dbTable, m.serialized);
|
||||
saveManifest(session, dbTable, v.second.serialized);
|
||||
}
|
||||
tr.commit();
|
||||
}
|
||||
|
||||
@@ -50,31 +50,7 @@ ApplyContext::ApplyContext(
|
||||
void
|
||||
ApplyContext::discard()
|
||||
{
|
||||
// featureRNG: Hook draws are never undone. If this transaction drew from
|
||||
// the RNG, its advanced state survives the discard (as does the record of
|
||||
// what its Hooks were given), so a rollback cannot preserve an RNG state
|
||||
// for whatever executes next.
|
||||
std::optional<uint256> const randomData = view_->getRandomData();
|
||||
std::optional<uint256> rngState;
|
||||
if (randomData)
|
||||
{
|
||||
if (auto const sle = view_->read(keylet::random()))
|
||||
rngState = sle->getFieldH256(sfRandomData);
|
||||
}
|
||||
|
||||
view_.emplace(&base_, flags_);
|
||||
|
||||
if (randomData)
|
||||
view_->setRandomData(*randomData);
|
||||
|
||||
if (rngState)
|
||||
{
|
||||
if (auto sle = view_->peek(keylet::random()))
|
||||
{
|
||||
sle->setFieldH256(sfRandomData, *rngState);
|
||||
view_->update(sle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<TxMeta>
|
||||
|
||||
@@ -34,8 +34,6 @@
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
|
||||
namespace ripple {
|
||||
@@ -100,21 +98,6 @@ Change::preflight(PreflightContext const& ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.tx.getTxnType() == ttENTROPY)
|
||||
{
|
||||
if (!ctx.rules.enabled(featureRNG))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Change: RNG is not enabled.";
|
||||
return temDISABLED;
|
||||
}
|
||||
|
||||
if (!publicKeyType(makeSlice(ctx.tx.getFieldVL(sfPublicKey))))
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Change: Entropy has an invalid PublicKey.";
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
@@ -174,29 +157,6 @@ Change::preclaim(PreclaimContext const& ctx)
|
||||
case ttUNL_MODIFY:
|
||||
case ttEMIT_FAILURE:
|
||||
return tesSUCCESS;
|
||||
case ttENTROPY: {
|
||||
if (ctx.tx.getFieldU32(sfLedgerSequence) != ctx.view.seq())
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Change: Entropy for wrong ledger.";
|
||||
return tefFAILURE;
|
||||
}
|
||||
|
||||
// Only validators on the UNLReport may contribute, when there is
|
||||
// one. Without a UNLReport (new networks, standalone, unit tests)
|
||||
// inclusion by consensus is the only gate.
|
||||
auto const unlRep = ctx.view.read(keylet::UNLReport());
|
||||
if (!unlRep || !unlRep->isFieldPresent(sfActiveValidators))
|
||||
return tesSUCCESS;
|
||||
|
||||
auto const pk = ctx.tx.getFieldVL(sfPublicKey);
|
||||
for (auto const& av : unlRep->getFieldArray(sfActiveValidators))
|
||||
if (av.getFieldVL(sfPublicKey) == pk)
|
||||
return tesSUCCESS;
|
||||
|
||||
JLOG(ctx.j.warn()) << "Change: Entropy from a validator that is "
|
||||
"not on the UNLReport.";
|
||||
return tefFAILURE;
|
||||
}
|
||||
case ttUNL_REPORT: {
|
||||
if (!ctx.tx.isFieldPresent(sfImportVLKey) ||
|
||||
ctx.app.config().IMPORT_VL_KEYS.empty())
|
||||
@@ -251,142 +211,12 @@ Change::doApply()
|
||||
return applyEmitFailure();
|
||||
case ttUNL_REPORT:
|
||||
return applyUNLReport();
|
||||
case ttENTROPY:
|
||||
return applyEntropy();
|
||||
default:
|
||||
UNREACHABLE("ripple::Change::doApply : invalid transaction type");
|
||||
return tefFAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
// featureRNG
|
||||
//
|
||||
// The Random object holds one commitment per validator: the hash of the
|
||||
// secret that validator will reveal next. A ttENTROPY reveals the secret for
|
||||
// the stored commitment (if it has one) and replaces the commitment with a new
|
||||
// one. A reveal that matches is mixed into sfRandomData and counted. Anything
|
||||
// else (no commitment yet, a lost secret, a mismatch) just re-registers, which
|
||||
// is no different from the validator having withheld.
|
||||
//
|
||||
// The first ttENTROPY of each ledger restarts sfRandomData. All of them are
|
||||
// applied before any other transaction (see BuildLedger), so by the time Hooks
|
||||
// run, sfRandomData is the seed for this ledger. The Hook API then iterates it
|
||||
// on every draw.
|
||||
TER
|
||||
Change::applyEntropy()
|
||||
{
|
||||
// Commitments not refreshed within this many ledgers are forgotten.
|
||||
static constexpr std::uint32_t maxCommitmentAge = 8;
|
||||
|
||||
// domain separators for the two hashes computed here
|
||||
static constexpr std::uint32_t rngSeedTag = 0x524E4753; // 'RNGS'
|
||||
static constexpr std::uint32_t rngAccumulateTag = 0x524E4741; // 'RNGA'
|
||||
|
||||
auto const seq = view().seq();
|
||||
auto const& tx = ctx_.tx;
|
||||
auto const pk = tx.getFieldVL(sfPublicKey);
|
||||
|
||||
auto sle = view().peek(keylet::random());
|
||||
bool const created = !sle;
|
||||
|
||||
// find this validator's commitment, if it has one
|
||||
std::optional<STObject> commitment;
|
||||
if (!created)
|
||||
{
|
||||
for (auto const& entry : sle->getFieldArray(sfRandomDigests))
|
||||
{
|
||||
if (entry.getFieldVL(sfPublicKey) == pk)
|
||||
{
|
||||
commitment = entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// one contribution per validator per ledger
|
||||
if (commitment && commitment->getFieldU32(sfLedgerSequence) == seq)
|
||||
{
|
||||
JLOG(j_.warn()) << "Change: second Entropy from one validator in "
|
||||
<< "ledger " << seq;
|
||||
return tefFAILURE;
|
||||
}
|
||||
|
||||
if (created)
|
||||
sle = std::make_shared<SLE>(keylet::random());
|
||||
|
||||
// the first ttENTROPY applied in this ledger restarts the accumulator
|
||||
if (created || sle->getFieldU32(sfLedgerSequence) != seq)
|
||||
{
|
||||
std::size_t denominator = 0;
|
||||
if (auto const unlRep = view().read(keylet::UNLReport());
|
||||
unlRep && unlRep->isFieldPresent(sfActiveValidators))
|
||||
denominator = unlRep->getFieldArray(sfActiveValidators).size();
|
||||
|
||||
sle->setFieldU32(sfLedgerSequence, seq);
|
||||
sle->setFieldU16(sfEntropyCount, 0);
|
||||
sle->setFieldU16(
|
||||
sfEntropyDenominator,
|
||||
static_cast<std::uint16_t>(
|
||||
std::min<std::size_t>(denominator, 0xFFFFU)));
|
||||
sle->setFieldH256(
|
||||
sfRandomData,
|
||||
sha512Half(rngSeedTag, seq, view().info().parentHash));
|
||||
}
|
||||
|
||||
// mix in the reveal if it matches the validator's commitment
|
||||
if (commitment && tx.isFieldPresent(sfRandomData))
|
||||
{
|
||||
auto const reveal = tx.getFieldH256(sfRandomData);
|
||||
if (sha512Half(reveal) == commitment->getFieldH256(sfNextRandomDigest))
|
||||
{
|
||||
sle->setFieldH256(
|
||||
sfRandomData,
|
||||
sha512Half(
|
||||
rngAccumulateTag,
|
||||
sle->getFieldH256(sfRandomData),
|
||||
makeSlice(pk),
|
||||
reveal));
|
||||
sle->setFieldU16(
|
||||
sfEntropyCount, sle->getFieldU16(sfEntropyCount) + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
JLOG(j_.warn()) << "Change: Entropy reveal does not match the "
|
||||
<< "validator's commitment, ignoring it.";
|
||||
}
|
||||
}
|
||||
|
||||
// store the new commitment, dropping stale ones
|
||||
STArray digests(sfRandomDigests);
|
||||
for (auto const& entry : sle->getFieldArray(sfRandomDigests))
|
||||
{
|
||||
if (entry.getFieldVL(sfPublicKey) == pk)
|
||||
continue;
|
||||
if (entry.getFieldU32(sfLedgerSequence) + maxCommitmentAge < seq)
|
||||
continue;
|
||||
digests.push_back(entry);
|
||||
}
|
||||
|
||||
STObject entry(sfRandomDigestEntry);
|
||||
entry.setFieldVL(sfPublicKey, pk);
|
||||
entry.setFieldH256(sfNextRandomDigest, tx.getFieldH256(sfNextRandomDigest));
|
||||
entry.setFieldU32(sfLedgerSequence, seq);
|
||||
digests.push_back(std::move(entry));
|
||||
|
||||
digests.sort([](STObject const& a, STObject const& b) {
|
||||
return a.getFieldVL(sfPublicKey) < b.getFieldVL(sfPublicKey);
|
||||
});
|
||||
|
||||
sle->setFieldArray(sfRandomDigests, digests);
|
||||
|
||||
if (created)
|
||||
view().insert(sle);
|
||||
else
|
||||
view().update(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
TER
|
||||
Change::applyUNLReport()
|
||||
{
|
||||
|
||||
@@ -76,9 +76,6 @@ private:
|
||||
|
||||
TER
|
||||
applyUNLReport();
|
||||
|
||||
TER
|
||||
applyEntropy();
|
||||
};
|
||||
|
||||
using EnableAmendment = Change;
|
||||
@@ -86,7 +83,6 @@ using SetFee = Change;
|
||||
using UNLModify = Change;
|
||||
using EmitFailure = Change;
|
||||
using UNLReport = Change;
|
||||
using Entropy = Change;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
@@ -292,15 +292,6 @@ DeleteAccount::preclaim(PreclaimContext const& ctx)
|
||||
if (sleAccount->getFlags() & lsfURITokenIssuer)
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
// An on-ledger manifest outlives its account. Neither copy is in the owner
|
||||
// directory, and the master copy stays listed in keylet::manifestDir() for
|
||||
// good, so deleting the account would orphan both. A re-funded account
|
||||
// could then never publish again: its first SetManifest would find the old
|
||||
// master copy in the way, with no sfManifestID to say it is its own.
|
||||
if (ctx.view.rules().enabled(featureOnChainManifests) &&
|
||||
sleAccount->isFieldPresent(sfManifestID))
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
if (ctx.view.rules().enabled(featureNonFungibleTokensV1))
|
||||
{
|
||||
// If an issuer has any issued NFTs resident in the ledger then it
|
||||
@@ -437,13 +428,6 @@ DeleteAccount::doApply()
|
||||
if (src->isFieldPresent(sfHookNamespaces) || src->isFieldPresent(sfHooks))
|
||||
return tecHAS_OBLIGATIONS;
|
||||
|
||||
// The AppLoader is in no owner directory, so the cleanup walk below never
|
||||
// visits it; erase it here or AccountRootsDeletedClean fires on the
|
||||
// orphan. Not gated on featurePWALoader: without it no such object can
|
||||
// exist, and gating could only ever leave one behind.
|
||||
if (auto sleLoader = view().peek(keylet::appLoader(account_)))
|
||||
view().erase(sleLoader);
|
||||
|
||||
// Delete all of the entries in the account directory.
|
||||
Keylet const ownerDirKeylet{keylet::ownerDir(account_)};
|
||||
auto const ter = cleanupOnAccountDelete(
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
#include <xrpld/ledger/ReadView.h>
|
||||
#include <xrpld/ledger/View.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/protocol/AppLoader.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/FeeUnits.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
@@ -604,7 +603,6 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltCRON:
|
||||
case ltIMPORT_VLSEQ:
|
||||
case ltUNL_REPORT:
|
||||
case ltRANDOM:
|
||||
case ltAMM:
|
||||
case ltBRIDGE:
|
||||
case ltXCHAIN_OWNED_CLAIM_ID:
|
||||
@@ -615,7 +613,6 @@ LedgerEntryTypesMatch::visitEntry(
|
||||
case ltMPTOKEN:
|
||||
case ltCREDENTIAL:
|
||||
case ltPERMISSIONED_DOMAIN:
|
||||
case ltAPP_LOADER:
|
||||
case ltMANIFEST:
|
||||
break;
|
||||
default:
|
||||
@@ -2023,61 +2020,4 @@ ValidLockedBalance::finalize(
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
ValidAppLoader::visitEntry(
|
||||
bool isDelete,
|
||||
std::shared_ptr<SLE const> const& before,
|
||||
std::shared_ptr<SLE const> const& after)
|
||||
{
|
||||
auto const& sle = after ? after : before;
|
||||
if (!sle || sle->getType() != ltAPP_LOADER)
|
||||
return;
|
||||
touched_ = true;
|
||||
if (!isDelete)
|
||||
live_.push_back(after);
|
||||
}
|
||||
|
||||
bool
|
||||
ValidAppLoader::finalize(
|
||||
STTx const& tx,
|
||||
TER const,
|
||||
XRPAmount const,
|
||||
ReadView const& view,
|
||||
beast::Journal const& j)
|
||||
{
|
||||
if (!touched_)
|
||||
return true;
|
||||
|
||||
auto fail = [&](char const* why) {
|
||||
JLOG(j.fatal()) << "Invariant failed: AppLoader " << why;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!view.rules().enabled(featurePWALoader))
|
||||
return fail("changed before featurePWALoader was enabled");
|
||||
if (tx.getTxnType() != ttACCOUNT_SET && tx.getTxnType() != ttACCOUNT_DELETE)
|
||||
return fail(
|
||||
"changed by a transaction other than AccountSet or "
|
||||
"AccountDelete");
|
||||
|
||||
for (auto const& sle : live_)
|
||||
{
|
||||
auto const owner = sle->getAccountID(sfOwner);
|
||||
if (sle->key() != keylet::appLoader(owner).key)
|
||||
return fail("is not keyed at its owner's keylet");
|
||||
auto const blob = sle->getFieldVL(sfAppLoader);
|
||||
if (appLoader::validate(blob.data(), blob.size()) !=
|
||||
appLoader::Result::ok)
|
||||
return fail("blob does not validate");
|
||||
auto const root = view.read(keylet::account(owner));
|
||||
if (!root ||
|
||||
(*root)[~sfAppLoaderSequence] != (*sle)[sfAppLoaderSequence])
|
||||
return fail("sequence does not match its owner's");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -708,37 +708,8 @@ public:
|
||||
beast::Journal const&);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Invariant: AppLoader objects stay consistent with their owners.
|
||||
*
|
||||
* An ltAPP_LOADER is in no owner directory, so nothing else would notice it
|
||||
* drifting. For every transaction that creates, changes or deletes one:
|
||||
* featurePWALoader is enabled and the transaction is an AccountSet or
|
||||
* AccountDelete; a live loader is keyed at keylet::appLoader(sfOwner), its
|
||||
* blob passes appLoader::validate, and its AppLoaderSequence equals its
|
||||
* owner's.
|
||||
*/
|
||||
class ValidAppLoader
|
||||
{
|
||||
bool touched_ = false;
|
||||
std::vector<std::shared_ptr<SLE const>> live_;
|
||||
|
||||
public:
|
||||
void
|
||||
visitEntry(
|
||||
bool,
|
||||
std::shared_ptr<SLE const> const&,
|
||||
std::shared_ptr<SLE const> const&);
|
||||
|
||||
bool
|
||||
finalize(
|
||||
STTx const&,
|
||||
TER const,
|
||||
XRPAmount const,
|
||||
ReadView const&,
|
||||
beast::Journal const&);
|
||||
};
|
||||
|
||||
// additional invariant checks can be declared above and then added to this
|
||||
// tuple
|
||||
using InvariantChecks = std::tuple<
|
||||
TransactionFeeCheck,
|
||||
AccountRootsNotDeleted,
|
||||
@@ -758,8 +729,7 @@ using InvariantChecks = std::tuple<
|
||||
ValidMPTIssuance,
|
||||
ValidPermissionedDomain,
|
||||
ValidAMM,
|
||||
ValidLockedBalance,
|
||||
ValidAppLoader>;
|
||||
ValidLockedBalance>;
|
||||
|
||||
/**
|
||||
* @brief get a tuple of all invariant checks
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/ledger/View.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/protocol/AppLoader.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
@@ -56,25 +55,6 @@ SetAccount::makeTxConsequences(PreflightContext const& ctx)
|
||||
return TxConsequences{ctx.tx, getTxConsequencesCategory(ctx.tx)};
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
SetAccount::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
XRPAmount extraFee{0};
|
||||
|
||||
if (view.rules().enabled(featurePWALoader))
|
||||
{
|
||||
if (auto const loader = tx[~sfAppLoader])
|
||||
{
|
||||
// One drop per byte, on every AccountSet carrying the field;
|
||||
// removal carries an empty blob and so costs nothing extra.
|
||||
extraFee += XRPAmount{static_cast<std::int64_t>(
|
||||
loader->size() * appLoaderFeeDropsPerByte)};
|
||||
}
|
||||
}
|
||||
|
||||
return Transactor::calculateBaseFee(view, tx) + extraFee;
|
||||
}
|
||||
|
||||
NotTEC
|
||||
SetAccount::preflight(PreflightContext const& ctx)
|
||||
{
|
||||
@@ -204,28 +184,6 @@ SetAccount::preflight(PreflightContext const& ctx)
|
||||
return temMALFORMED;
|
||||
}
|
||||
|
||||
// AppLoader
|
||||
if (auto const loader = tx[~sfAppLoader])
|
||||
{
|
||||
if (!ctx.rules.enabled(featurePWALoader))
|
||||
return temDISABLED;
|
||||
|
||||
// An empty blob is the removal sentinel, matching Domain and
|
||||
// MessageKey. It is not validated as a document.
|
||||
if (!loader->empty())
|
||||
{
|
||||
auto const result =
|
||||
appLoader::validate(loader->data(), loader->size());
|
||||
|
||||
if (result != appLoader::Result::ok)
|
||||
{
|
||||
JLOG(j.trace()) << "Malformed transaction: AppLoader: "
|
||||
<< appLoader::to_string(result);
|
||||
return temMALFORMED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HookStateScale
|
||||
if (tx.isFieldPresent(sfHookStateScale))
|
||||
{
|
||||
@@ -607,42 +565,6 @@ SetAccount::doApply()
|
||||
}
|
||||
}
|
||||
|
||||
// AppLoader: its own ltAPP_LOADER object (no directory entry, no
|
||||
// reserve; the per-byte fee from calculateBaseFee pays for it). Every
|
||||
// AccountSet carrying the field, removal included, advances the owner's
|
||||
// AppLoaderSequence, and a written object records the sequence it was
|
||||
// written at, so a reader can tell whether a copy is current.
|
||||
if (view().rules().enabled(featurePWALoader) &&
|
||||
tx.isFieldPresent(sfAppLoader))
|
||||
{
|
||||
Blob const loader = tx.getFieldVL(sfAppLoader);
|
||||
Keylet const klLoader = keylet::appLoader(account_);
|
||||
auto sleLoader = view().peek(klLoader);
|
||||
std::uint32_t const seq = (*sle)[~sfAppLoaderSequence].value_or(0) + 1;
|
||||
(*sle)[sfAppLoaderSequence] = seq;
|
||||
|
||||
if (loader.empty())
|
||||
{
|
||||
if (sleLoader)
|
||||
view().erase(sleLoader);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool const create = !sleLoader;
|
||||
if (create)
|
||||
{
|
||||
sleLoader = std::make_shared<SLE>(klLoader);
|
||||
(*sleLoader)[sfOwner] = account_;
|
||||
}
|
||||
sleLoader->setFieldVL(sfAppLoader, loader);
|
||||
(*sleLoader)[sfAppLoaderSequence] = seq;
|
||||
if (create)
|
||||
view().insert(sleLoader);
|
||||
else
|
||||
view().update(sleLoader);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// TransferRate
|
||||
//
|
||||
|
||||
@@ -41,14 +41,6 @@ public:
|
||||
static TxConsequences
|
||||
makeTxConsequences(PreflightContext const& ctx);
|
||||
|
||||
/** Adds appLoaderFeeDropsPerByte per byte of AppLoader document.
|
||||
|
||||
The AppLoader ledger object carries no owner reserve, so the cost of
|
||||
the storage is recovered here instead.
|
||||
*/
|
||||
static XRPAmount
|
||||
calculateBaseFee(ReadView const& view, STTx const& tx);
|
||||
|
||||
static NotTEC
|
||||
preflight(PreflightContext const& ctx);
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/ManifestLedger.h>
|
||||
#include <xrpld/app/tx/detail/SetManifest.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/ledger/View.h>
|
||||
@@ -251,12 +250,6 @@ SetManifest::preclaim(PreclaimContext const& ctx)
|
||||
// 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.
|
||||
//
|
||||
// The same goes for every other refusal here that turns on ledger state.
|
||||
// The envelope is unsigned and consumes no account sequence, so once
|
||||
// relayed anyone can resubmit it, in every ledger, for as long as the
|
||||
// state it was refused on stays as it is. A tec would charge the master
|
||||
// account its fee each time.
|
||||
if (sle->isFieldPresent(sfManifestID))
|
||||
{
|
||||
// A dangling sfManifestID is a corrupt ledger; doApply reports it.
|
||||
@@ -286,22 +279,6 @@ SetManifest::preclaim(PreclaimContext const& ctx)
|
||||
// account, but keylet::manifest(signingKey) is not: without this, a
|
||||
// manifest naming another validator's key as its ephemeral key would
|
||||
// collide with -- and clobber -- that validator's object.
|
||||
//
|
||||
// The two kinds of keylet share one namespace, so the master key's own
|
||||
// keylet can be held as well: by another account's ephemeral copy, when
|
||||
// this master key is that account's ephemeral key. Left to doApply, that
|
||||
// would be reported as a corrupt ledger.
|
||||
if (auto const sleMaster =
|
||||
ctx.view.read(keylet::manifest(newManifest->masterKey));
|
||||
sleMaster && sleMaster->getAccountID(sfAccount) != id)
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "SetManifest: Master key already claimed as an ephemeral key "
|
||||
"by another account. "
|
||||
<< id;
|
||||
return tefMANIFEST_KEY_IN_USE;
|
||||
}
|
||||
|
||||
if (newManifest->signingKey)
|
||||
{
|
||||
auto const sleEph =
|
||||
@@ -312,22 +289,7 @@ SetManifest::preclaim(PreclaimContext const& ctx)
|
||||
<< "SetManifest: Ephemeral key already claimed by another "
|
||||
"account. "
|
||||
<< id;
|
||||
return tefMANIFEST_KEY_IN_USE;
|
||||
}
|
||||
|
||||
// Held by this account, the object can only be the ephemeral copy of
|
||||
// its current manifest: the master copy is at another key, since a
|
||||
// manifest's two keys must differ. Every manifest cache refuses a
|
||||
// newer manifest that keeps the ephemeral key (badEphemeralKey), so
|
||||
// accepting one here would leave the ledger permanently ahead of what
|
||||
// any node believes. A rotation names a new key; a revocation none.
|
||||
if (sleEph)
|
||||
{
|
||||
JLOG(ctx.j.warn())
|
||||
<< "SetManifest: Ephemeral key already used by this master "
|
||||
"key's current manifest. "
|
||||
<< id;
|
||||
return tefMANIFEST_KEY_IN_USE;
|
||||
return tecDUPLICATE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,9 +313,94 @@ SetManifest::doApply()
|
||||
if (!manifest || calcAccountID(manifest->masterKey) != account_)
|
||||
return tefINTERNAL;
|
||||
|
||||
// The objects, and the manifest directory entry, are maintained in one
|
||||
// place; see writeManifestObjects.
|
||||
return writeManifestObjects(view(), sle, obj, *manifest, j_);
|
||||
// A manifest is stored twice so it can be found from either key:
|
||||
// keylet::manifest(masterKey) -> obj1, sfManifestID -> obj2
|
||||
// keylet::manifest(signingKey) -> obj2, sfManifestID -> obj1
|
||||
// A revoked manifest has no signing key, so it exists only as obj1 with no
|
||||
// sfManifestID. Both copies are erased and rewritten on every update so
|
||||
// they can never drift apart.
|
||||
if (sle->isFieldPresent(sfManifestID))
|
||||
{
|
||||
uint256 const firstID = sle->getFieldH256(sfManifestID);
|
||||
auto const sleMan1 = view().peek(Keylet{ltMANIFEST, firstID});
|
||||
if (!sleMan1 || sleMan1->getAccountID(sfAccount) != account_)
|
||||
{
|
||||
JLOG(j_.error()) << "SetManifest: Old manifest object missing or "
|
||||
"misowned (ID1) !! "
|
||||
<< strHex(firstID);
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
// Absent when the previous manifest was a revocation.
|
||||
if (sleMan1->isFieldPresent(sfManifestID))
|
||||
{
|
||||
uint256 const secondID = sleMan1->getFieldH256(sfManifestID);
|
||||
auto const sleMan2 = view().peek(Keylet{ltMANIFEST, secondID});
|
||||
if (secondID == firstID || !sleMan2 ||
|
||||
sleMan2->getAccountID(sfAccount) != account_)
|
||||
{
|
||||
JLOG(j_.error())
|
||||
<< "SetManifest: Old manifest object missing, misowned or "
|
||||
"self-referential (ID2) !! "
|
||||
<< strHex(secondID);
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
view().erase(sleMan2);
|
||||
}
|
||||
|
||||
view().erase(sleMan1);
|
||||
}
|
||||
|
||||
Keylet const klMan1 = keylet::manifest(manifest->masterKey);
|
||||
std::optional<Keylet> klMan2;
|
||||
if (!manifest->revoked() && manifest->signingKey)
|
||||
klMan2 = keylet::manifest(*manifest->signingKey);
|
||||
|
||||
// Neither key may still be occupied: preclaim rejects an ephemeral key held
|
||||
// by another account, and the block above cleared this account's own
|
||||
// copies.
|
||||
if (view().exists(klMan1) || (klMan2 && view().exists(*klMan2)))
|
||||
{
|
||||
JLOG(j_.error()) << "SetManifest: Manifest keylet already occupied !! "
|
||||
<< strHex(klMan1.key);
|
||||
return tefBAD_LEDGER;
|
||||
}
|
||||
|
||||
// Mirror the manifest losslessly, signatures included, so any node can
|
||||
// reconstruct and independently verify it (ManifestCache::applyLedger).
|
||||
// Field *presence* is copied faithfully: sfVersion is soeDEFAULT in the
|
||||
// manifest format, so materialising an absent one would alter the signed
|
||||
// payload and break verification.
|
||||
auto const write = [&](Keylet const& kl,
|
||||
std::optional<uint256> const& other) {
|
||||
auto sleMan = std::make_shared<SLE>(kl);
|
||||
sleMan->setAccountID(sfAccount, account_);
|
||||
sleMan->setFieldU32(sfSequence, obj.getFieldU32(sfSequence));
|
||||
sleMan->setFieldVL(sfPublicKey, obj.getFieldVL(sfPublicKey));
|
||||
sleMan->setFieldVL(
|
||||
sfMasterSignature, obj.getFieldVL(sfMasterSignature));
|
||||
if (obj.isFieldPresent(sfVersion))
|
||||
sleMan->setFieldU16(sfVersion, obj.getFieldU16(sfVersion));
|
||||
if (obj.isFieldPresent(sfSigningPubKey))
|
||||
sleMan->setFieldVL(
|
||||
sfSigningPubKey, obj.getFieldVL(sfSigningPubKey));
|
||||
if (obj.isFieldPresent(sfSignature))
|
||||
sleMan->setFieldVL(sfSignature, obj.getFieldVL(sfSignature));
|
||||
if (obj.isFieldPresent(sfDomain))
|
||||
sleMan->setFieldVL(sfDomain, obj.getFieldVL(sfDomain));
|
||||
if (other)
|
||||
sleMan->setFieldH256(sfManifestID, *other);
|
||||
view().insert(sleMan);
|
||||
};
|
||||
|
||||
write(klMan1, klMan2 ? std::optional<uint256>{klMan2->key} : std::nullopt);
|
||||
if (klMan2)
|
||||
write(*klMan2, klMan1.key);
|
||||
|
||||
sle->setFieldH256(sfManifestID, klMan1.key);
|
||||
view().update(sle);
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
|
||||
XRPAmount
|
||||
|
||||
@@ -2119,17 +2119,6 @@ Transactor::finishApply(
|
||||
if (ctx_.size() > oversizeMetaDataCap)
|
||||
result = tecOVERSIZE;
|
||||
|
||||
// featureRNG: a transaction whose Hooks drew from the RNG is never
|
||||
// retried on a tec. Under tapRETRY (the first closed-ledger pass, and
|
||||
// open-ledger retries) a tec would otherwise drop the view without going
|
||||
// through reset(), so the RNG state would not be rolled forward: a Hook
|
||||
// that rolled back (tecHOOK_REJECTED) would leave the state it drew from
|
||||
// in place for the next transaction in the pass, and would draw again on
|
||||
// the final pass against a different state. Draws only exist when the
|
||||
// amendment is enabled, so this needs no separate gate.
|
||||
bool const rngDrawn =
|
||||
dynamic_cast<ApplyViewImpl&>(ctx_.view()).getRandomData().has_value();
|
||||
|
||||
if (isTecClaim(result) && (view().flags() & tapFAIL_HARD))
|
||||
{
|
||||
// If the tapFAIL_HARD flag is set, a tec result
|
||||
@@ -2140,8 +2129,7 @@ Transactor::finishApply(
|
||||
else if (
|
||||
(result == tecOVERSIZE) || (result == tecKILLED) ||
|
||||
(result == tecINCOMPLETE) || (result == tecEXPIRED) ||
|
||||
(isTecClaimHardFail(result, view().flags())) ||
|
||||
(rngDrawn && isTecClaim(result)))
|
||||
(isTecClaimHardFail(result, view().flags())))
|
||||
{
|
||||
JLOG(j_.trace()) << "reapplying because of " << transToken(result);
|
||||
|
||||
|
||||
@@ -32,9 +32,7 @@
|
||||
#include <boost/logic/tribool.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <concepts>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
|
||||
@@ -534,50 +532,6 @@ private:
|
||||
void
|
||||
updateOurPositions(std::unique_ptr<std::stringstream> const& clog);
|
||||
|
||||
/** Whether a disputed transaction must be in our position regardless of
|
||||
its vote count.
|
||||
|
||||
Optional adaptor hook: an adaptor that provides
|
||||
`std::optional<NodeID_t> forcedTxOwner(Tx_t const&, Ledger_t const&)`
|
||||
can name a node that a transaction belongs to (featureRNG uses this
|
||||
for each validator's entropy contribution). During the opening
|
||||
window of the establish phase such a transaction is voted yes iff its
|
||||
owner's own position contains it, but at most one per owner per round
|
||||
(see forcedTxs_). Afterwards it is voted on like any other disputed
|
||||
transaction, so an owner publishing late cannot force churn. Adaptors
|
||||
without the hook are unaffected.
|
||||
*/
|
||||
bool
|
||||
forcedInclusion(typename Result::Dispute_t const& dispute) const;
|
||||
|
||||
/** The owner the adaptor names for a disputed transaction while the
|
||||
forced-inclusion window is open. Always unseated for an adaptor
|
||||
without the forcedTxOwner hook.
|
||||
*/
|
||||
std::optional<NodeID_t>
|
||||
forcedOwner(typename Result::Dispute_t const& dispute) const;
|
||||
|
||||
/** Add any forced-inclusion transactions to our position. */
|
||||
void
|
||||
mergeForcedTxs(std::unique_ptr<std::stringstream> const& clog);
|
||||
|
||||
/** The one transaction each owner may force into our position this
|
||||
round, chosen by mergeForcedTxs the first time the owner's position
|
||||
offers one. An owner whose position carries several cannot make us
|
||||
carry them all. Cleared when we close.
|
||||
*/
|
||||
std::map<NodeID_t, typename Tx_t::ID> forcedTxs_;
|
||||
|
||||
/** forcedTxOwner results for this round, keyed by transaction ID.
|
||||
|
||||
The adaptor's answer depends only on the transaction and the previous
|
||||
ledger, both fixed for the round, so it is computed once per disputed
|
||||
transaction rather than on every tick (forcedOwner is reached several
|
||||
times per dispute per tick, and the RCL adaptor deserialises the
|
||||
transaction to answer). Cleared when we close.
|
||||
*/
|
||||
mutable std::map<typename Tx_t::ID, std::optional<NodeID_t>> forcedOwners_;
|
||||
|
||||
bool
|
||||
haveConsensus(std::unique_ptr<std::stringstream> const& clog);
|
||||
|
||||
@@ -1414,8 +1368,6 @@ Consensus<Adaptor>::phaseEstablish(
|
||||
<< "avMIN_CONSENSUS_TIME: " << parms.avMIN_CONSENSUS_TIME.count()
|
||||
<< "ms. ";
|
||||
|
||||
mergeForcedTxs(clog);
|
||||
|
||||
// Give everyone a chance to take an initial position
|
||||
if (result_->roundTime.read() < parms.ledgerMIN_CONSENSUS)
|
||||
{
|
||||
@@ -1466,8 +1418,6 @@ Consensus<Adaptor>::closeLedger(std::unique_ptr<std::stringstream> const& clog)
|
||||
phase_ = ConsensusPhase::establish;
|
||||
JLOG(j_.debug()) << "transitioned to ConsensusPhase::establish";
|
||||
rawCloseTimes_.self = now_;
|
||||
forcedTxs_.clear();
|
||||
forcedOwners_.clear();
|
||||
|
||||
result_.emplace(adaptor_.onClose(previousLedger_, now_, mode_.get()));
|
||||
result_->roundTime.reset(clock_.now());
|
||||
@@ -1563,10 +1513,6 @@ Consensus<Adaptor>::updateOurPositions(
|
||||
std::optional<typename TxSet_t::MutableTxSet> mutableSet;
|
||||
for (auto& [txId, dispute] : result_->disputes)
|
||||
{
|
||||
// Already merged into our position by mergeForcedTxs.
|
||||
if (forcedInclusion(dispute))
|
||||
continue;
|
||||
|
||||
// Because the threshold for inclusion increases,
|
||||
// time can change our position on a dispute
|
||||
if (dispute.updateVote(
|
||||
@@ -1713,128 +1659,6 @@ Consensus<Adaptor>::updateOurPositions(
|
||||
}
|
||||
}
|
||||
|
||||
template <class Adaptor>
|
||||
std::optional<typename Consensus<Adaptor>::NodeID_t>
|
||||
Consensus<Adaptor>::forcedOwner(typename Result::Dispute_t const& dispute) const
|
||||
{
|
||||
if constexpr (requires(Adaptor& a, Tx_t const& tx, Ledger_t const& l) {
|
||||
{
|
||||
a.forcedTxOwner(tx, l)
|
||||
} -> std::convertible_to<std::optional<NodeID_t>>;
|
||||
})
|
||||
{
|
||||
if (!result_)
|
||||
return std::nullopt;
|
||||
|
||||
ConsensusParms const& parms = adaptor_.parms();
|
||||
if (result_->roundTime.read() >=
|
||||
parms.ledgerMIN_CONSENSUS + parms.ledgerGRANULARITY)
|
||||
return std::nullopt;
|
||||
|
||||
auto const& id = dispute.tx().id();
|
||||
auto it = forcedOwners_.find(id);
|
||||
if (it == forcedOwners_.end())
|
||||
it = forcedOwners_
|
||||
.emplace(
|
||||
id,
|
||||
adaptor_.forcedTxOwner(dispute.tx(), previousLedger_))
|
||||
.first;
|
||||
return it->second;
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
template <class Adaptor>
|
||||
bool
|
||||
Consensus<Adaptor>::forcedInclusion(
|
||||
typename Result::Dispute_t const& dispute) const
|
||||
{
|
||||
std::optional<NodeID_t> const owner = forcedOwner(dispute);
|
||||
if (!owner)
|
||||
return false;
|
||||
|
||||
// our own contribution: keep it while the window is open
|
||||
if (*owner == result_->position.nodeID())
|
||||
return result_->txns.exists(dispute.tx().id());
|
||||
|
||||
// otherwise only the one transaction chosen for this owner this round
|
||||
auto const it = forcedTxs_.find(*owner);
|
||||
if (it == forcedTxs_.end() || it->second != dispute.tx().id())
|
||||
return false;
|
||||
|
||||
return dispute.getVote(*owner).value_or(false);
|
||||
}
|
||||
|
||||
template <class Adaptor>
|
||||
void
|
||||
Consensus<Adaptor>::mergeForcedTxs(
|
||||
std::unique_ptr<std::stringstream> const& clog)
|
||||
{
|
||||
XRPL_ASSERT(result_, "ripple::Consensus::mergeForcedTxs : result is set");
|
||||
|
||||
// Choose the one transaction each owner may force this round: the lowest
|
||||
// ID its position offers the first time it offers any. Nodes that see the
|
||||
// same position make the same choice.
|
||||
NodeID_t const& self = result_->position.nodeID();
|
||||
std::map<NodeID_t, typename Tx_t::ID> chosen;
|
||||
for (auto const& [txId, dispute] : result_->disputes)
|
||||
{
|
||||
std::optional<NodeID_t> const owner = forcedOwner(dispute);
|
||||
if (!owner || *owner == self || forcedTxs_.count(*owner) != 0 ||
|
||||
!dispute.getVote(*owner).value_or(false))
|
||||
continue;
|
||||
|
||||
auto const [it, inserted] = chosen.emplace(*owner, txId);
|
||||
if (!inserted && txId < it->second)
|
||||
it->second = txId;
|
||||
}
|
||||
forcedTxs_.merge(chosen);
|
||||
|
||||
std::optional<typename TxSet_t::MutableTxSet> mutableSet;
|
||||
for (auto& [txId, dispute] : result_->disputes)
|
||||
{
|
||||
if (dispute.getOurVote() || !forcedInclusion(dispute))
|
||||
continue;
|
||||
|
||||
dispute.setOurVote(true);
|
||||
if (!mutableSet)
|
||||
mutableSet.emplace(result_->txns);
|
||||
mutableSet->insert(dispute.tx());
|
||||
}
|
||||
|
||||
if (!mutableSet)
|
||||
return;
|
||||
|
||||
TxSet_t newSet{std::move(*mutableSet)};
|
||||
auto const newID = newSet.id();
|
||||
result_->txns = std::move(newSet);
|
||||
|
||||
JLOG(j_.info()) << "Position change: forced inclusion, tx " << newID;
|
||||
CLOG(clog) << "Position change: forced inclusion, tx " << newID << ". ";
|
||||
|
||||
result_->position.changePosition(
|
||||
newID, result_->position.closeTime(), now_);
|
||||
|
||||
if (acquired_.emplace(newID, result_->txns).second)
|
||||
{
|
||||
if (!result_->position.isBowOut())
|
||||
adaptor_.share(result_->txns);
|
||||
|
||||
for (auto const& [nodeId, peerPos] : currPeerPositions_)
|
||||
{
|
||||
if (peerPos.proposal().position() == newID)
|
||||
updateDisputes(nodeId, result_->txns);
|
||||
}
|
||||
}
|
||||
|
||||
if (!result_->position.isBowOut() &&
|
||||
(mode_.get() == ConsensusMode::proposing))
|
||||
adaptor_.propose(result_->position);
|
||||
}
|
||||
|
||||
template <class Adaptor>
|
||||
bool
|
||||
Consensus<Adaptor>::haveConsensus(
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
#include <boost/container/flat_map.hpp>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -114,19 +113,6 @@ public:
|
||||
void
|
||||
unVote(NodeID_t const& peer);
|
||||
|
||||
/** A peer's vote, if we have one.
|
||||
|
||||
@param peer Identifier of peer.
|
||||
*/
|
||||
std::optional<bool>
|
||||
getVote(NodeID_t const& peer) const
|
||||
{
|
||||
auto const it = votes_.find(peer);
|
||||
if (it == votes_.end())
|
||||
return std::nullopt;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
/** Update our vote given progression of consensus.
|
||||
|
||||
Updates our vote on this disputed transaction based on our peers' votes
|
||||
|
||||
@@ -191,10 +191,6 @@ public:
|
||||
int RELAY_UNTRUSTED_VALIDATIONS = 1;
|
||||
int RELAY_UNTRUSTED_PROPOSALS = 0;
|
||||
|
||||
// While acquiring the first ledger, fetch and read the on-ledger manifests
|
||||
// ahead of the rest of its state. See [early_manifest_sync].
|
||||
bool EARLY_MANIFEST_SYNC = true;
|
||||
|
||||
// True to ask peers not to relay current IP.
|
||||
bool PEER_PRIVATE = false;
|
||||
// peers_max is a legacy configuration, which is going to be replaced
|
||||
@@ -313,9 +309,6 @@ public:
|
||||
// Enable the beta API version
|
||||
bool BETA_RPC_API = false;
|
||||
|
||||
// Enable the hook_validate RPC (defaults to true in standalone mode).
|
||||
bool HOOK_VALIDATE_RPC = false;
|
||||
|
||||
// First, attempt to load the latest ledger directly from disk.
|
||||
bool FAST_LOAD = false;
|
||||
// When starting rippled with existing database it do not know it has those
|
||||
|
||||
@@ -49,11 +49,9 @@ struct ConfigSection
|
||||
#define SECTION_COMPRESSION "compression"
|
||||
#define SECTION_DATAGRAM_MONITOR "datagram_monitor"
|
||||
#define SECTION_DEBUG_LOGFILE "debug_logfile"
|
||||
#define SECTION_EARLY_MANIFEST_SYNC "early_manifest_sync"
|
||||
#define SECTION_ELB_SUPPORT "elb_support"
|
||||
#define SECTION_FEE_DEFAULT "fee_default"
|
||||
#define SECTION_FETCH_DEPTH "fetch_depth"
|
||||
#define SECTION_HOOK_VALIDATE_RPC "hook_validate_rpc"
|
||||
#define SECTION_IMPORT_VL_KEYS "import_vl_keys"
|
||||
#define SECTION_INSIGHT "insight"
|
||||
#define SECTION_IO_WORKERS "io_workers"
|
||||
@@ -76,8 +74,6 @@ struct ConfigSection
|
||||
#define SECTION_PEERS_IN_MAX "peers_in_max"
|
||||
#define SECTION_PEERS_OUT_MAX "peers_out_max"
|
||||
#define SECTION_PORT_GRPC "port_grpc"
|
||||
#define SECTION_PWA_ALLOWLIST "pwa_allowlist"
|
||||
#define SECTION_PWA_BLOCKLIST "pwa_blocklist"
|
||||
#define SECTION_PREFETCH_WORKERS "prefetch_workers"
|
||||
#define SECTION_REDUCE_RELAY "reduce_relay"
|
||||
#define SECTION_RELATIONAL_DB "relational_db"
|
||||
|
||||
@@ -47,8 +47,6 @@ enum JobType {
|
||||
jtCLIENT_FEE_CHANGE, // Subscription for fee change by a client
|
||||
jtCLIENT_CONSENSUS, // Subscription for consensus state change by a client
|
||||
jtCLIENT_ACCT_HIST, // Subscription for account history by a client
|
||||
jtCLIENT_PWA, // AppLoader document lookup for a pwa port
|
||||
jtCLIENT_PWADNS, // Custom domain TXT lookup for a pwa port (blocks)
|
||||
jtCLIENT_RPC, // Client RPC request
|
||||
jtCLIENT_WEBSOCKET, // Client websocket request
|
||||
jtRPC, // A websocket command from the client
|
||||
|
||||
@@ -90,10 +90,6 @@ private:
|
||||
add(jtCLIENT_CONSENSUS, "clientConsensus", maxLimit, 2000ms, 5000ms);
|
||||
add(jtCLIENT_ACCT_HIST, "clientAccountHistory", maxLimit, 2000ms, 5000ms);
|
||||
add(jtCLIENT_RPC, "clientRPC", maxLimit, 2000ms, 5000ms);
|
||||
// Web traffic from pwa ports: at most two workers, and no latency
|
||||
// targets, so that it can never move the node's load factor.
|
||||
add(jtCLIENT_PWA, "clientPWA", 2, 0ms, 0ms);
|
||||
add(jtCLIENT_PWADNS, "clientPWADNS", 2, 0ms, 0ms);
|
||||
add(jtCLIENT_WEBSOCKET, "clientWebsocket", maxLimit, 2000ms, 5000ms);
|
||||
add(jtRPC, "RPC", maxLimit, 0ms, 0ms);
|
||||
add(jtUPDATE_PF, "updatePaths", 1, 0ms, 0ms);
|
||||
|
||||
@@ -617,9 +617,6 @@ Config::loadFromString(std::string const& fileContents)
|
||||
"] section");
|
||||
}
|
||||
|
||||
if (getSingleSection(secConfig, SECTION_EARLY_MANIFEST_SYNC, strTemp, j_))
|
||||
EARLY_MANIFEST_SYNC = beast::lexicalCastThrow<bool>(strTemp);
|
||||
|
||||
if (getSingleSection(secConfig, SECTION_RELAY_PROPOSALS, strTemp, j_))
|
||||
{
|
||||
if (boost::iequals(strTemp, "all"))
|
||||
@@ -852,12 +849,6 @@ Config::loadFromString(std::string const& fileContents)
|
||||
if (getSingleSection(secConfig, SECTION_BETA_RPC_API, strTemp, j_))
|
||||
BETA_RPC_API = beast::lexicalCastThrow<bool>(strTemp);
|
||||
|
||||
// hook_validate RPC is opt-in on networked nodes, but on by default in
|
||||
// standalone mode for local hook development.
|
||||
HOOK_VALIDATE_RPC = RUN_STANDALONE;
|
||||
if (getSingleSection(secConfig, SECTION_HOOK_VALIDATE_RPC, strTemp, j_))
|
||||
HOOK_VALIDATE_RPC = beast::lexicalCastThrow<bool>(strTemp);
|
||||
|
||||
// Do not load trusted validator configuration for standalone mode
|
||||
do
|
||||
{
|
||||
|
||||
@@ -128,21 +128,6 @@ public:
|
||||
return hookExecution_.size();
|
||||
}
|
||||
|
||||
/** featureRNG: record the RNG state seen by this transaction's first Hook
|
||||
draw. Later calls are ignored. Written only to the final metadata. */
|
||||
void
|
||||
setRandomData(uint256 const& randomData)
|
||||
{
|
||||
if (!randomData_)
|
||||
randomData_ = randomData;
|
||||
}
|
||||
|
||||
std::optional<uint256> const&
|
||||
getRandomData() const
|
||||
{
|
||||
return randomData_;
|
||||
}
|
||||
|
||||
/** Get the number of modified entries
|
||||
*/
|
||||
std::size_t
|
||||
@@ -163,7 +148,6 @@ private:
|
||||
std::optional<STAmount> deliver_;
|
||||
std::vector<STObject> hookExecution_;
|
||||
std::vector<STObject> hookEmission_;
|
||||
std::optional<uint256> randomData_;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -157,12 +157,6 @@ ApplyStateTable::generateTxMeta(
|
||||
std::uint16_t nodeType = curNode
|
||||
? curNode->getFieldU16(sfLedgerEntryType)
|
||||
: origNode->getFieldU16(sfLedgerEntryType);
|
||||
|
||||
// featureRNG: the RNG object is never in provisional metadata, which
|
||||
// weak Hooks can read through meta_slot.
|
||||
if (isProvisional && nodeType == ltRANDOM)
|
||||
continue;
|
||||
|
||||
meta.setAffectedNode(item.first, *type, nodeType);
|
||||
if (type == &sfDeletedNode)
|
||||
{
|
||||
@@ -308,7 +302,6 @@ ApplyStateTable::apply(
|
||||
std::optional<STAmount> const& deliver,
|
||||
std::vector<STObject> const& hookExecution,
|
||||
std::vector<STObject> const& hookEmission,
|
||||
std::optional<uint256> const& randomData,
|
||||
bool isDryRun,
|
||||
beast::Journal j)
|
||||
{
|
||||
@@ -323,11 +316,6 @@ ApplyStateTable::apply(
|
||||
auto [meta, newMod] =
|
||||
generateTxMeta(to, tx, deliver, hookExecution, hookEmission, j);
|
||||
|
||||
// featureRNG: added only here, never to provisional metadata, so a
|
||||
// weak Hook reading meta_slot cannot learn the RNG state.
|
||||
if (randomData)
|
||||
meta.setRandomData(*randomData);
|
||||
|
||||
if (!isDryRun)
|
||||
{
|
||||
// add any new modified nodes to the modification set
|
||||
|
||||
@@ -97,7 +97,6 @@ public:
|
||||
std::optional<STAmount> const& deliver,
|
||||
std::vector<STObject> const& hookExecution,
|
||||
std::vector<STObject> const& hookEmission,
|
||||
std::optional<uint256> const& randomData,
|
||||
bool isDryRun,
|
||||
beast::Journal j);
|
||||
|
||||
|
||||
@@ -37,15 +37,7 @@ ApplyViewImpl::apply(
|
||||
beast::Journal j)
|
||||
{
|
||||
return items_.apply(
|
||||
to,
|
||||
tx,
|
||||
ter,
|
||||
deliver_,
|
||||
hookExecution_,
|
||||
hookEmission_,
|
||||
randomData_,
|
||||
isDryRun,
|
||||
j);
|
||||
to, tx, ter, deliver_, hookExecution_, hookEmission_, isDryRun, j);
|
||||
}
|
||||
|
||||
TxMeta
|
||||
|
||||
@@ -533,15 +533,6 @@ private:
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// hook_validate <hex>
|
||||
Json::Value
|
||||
parseHookValidate(Json::Value const& jvParams)
|
||||
{
|
||||
Json::Value jvRequest(Json::objectValue);
|
||||
jvRequest[jss::code] = jvParams[0u].asString();
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// sign_for <account> <secret> <json> offline
|
||||
// sign_for <account> <secret> <json>
|
||||
Json::Value
|
||||
@@ -1380,34 +1371,6 @@ private:
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// pwa_info [reset] [flush_dns]
|
||||
Json::Value
|
||||
parsePWAInfo(Json::Value const& jvParams)
|
||||
{
|
||||
Json::Value jvRequest(Json::objectValue);
|
||||
for (Json::UInt i = 0; i < jvParams.size(); ++i)
|
||||
{
|
||||
auto const param = jvParams[i].asString();
|
||||
if ((param != "reset" && param != "flush_dns") ||
|
||||
jvRequest.isMember(param))
|
||||
return rpcError(rpcINVALID_PARAMS);
|
||||
jvRequest[param] = true;
|
||||
}
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// pwa_validate <account> <hash> [<domain>]
|
||||
Json::Value
|
||||
parsePWAValidate(Json::Value const& jvParams)
|
||||
{
|
||||
Json::Value jvRequest(Json::objectValue);
|
||||
jvRequest[jss::account] = jvParams[0u].asString();
|
||||
jvRequest[jss::hash] = jvParams[1u].asString();
|
||||
if (jvParams.size() > 2)
|
||||
jvRequest[jss::domain] = jvParams[2u].asString();
|
||||
return jvRequest;
|
||||
}
|
||||
|
||||
// server_info [counters]
|
||||
Json::Value
|
||||
parseServerInfo(Json::Value const& jvParams)
|
||||
@@ -1480,7 +1443,6 @@ public:
|
||||
{"fetch_info", &RPCParser::parseFetchInfo, 0, 1},
|
||||
{"gateway_balances", &RPCParser::parseGatewayBalances, 1, -1},
|
||||
{"get_counts", &RPCParser::parseGetCounts, 0, 1},
|
||||
{"hook_validate", &RPCParser::parseHookValidate, 1, 1},
|
||||
{"json", &RPCParser::parseJson, 2, 2},
|
||||
{"json2", &RPCParser::parseJson2, 1, 1},
|
||||
{"ledger", &RPCParser::parseLedger, 0, 2},
|
||||
@@ -1497,8 +1459,6 @@ public:
|
||||
{"peers", &RPCParser::parseAsIs, 0, 0},
|
||||
{"ping", &RPCParser::parseAsIs, 0, 0},
|
||||
{"print", &RPCParser::parseAsIs, 0, 1},
|
||||
{"pwa_info", &RPCParser::parsePWAInfo, 0, 2},
|
||||
{"pwa_validate", &RPCParser::parsePWAValidate, 2, 3},
|
||||
// { "profile", &RPCParser::parseProfile, 1, 9
|
||||
// },
|
||||
{"random", &RPCParser::parseAsIs, 0, 0},
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
@@ -36,7 +35,6 @@
|
||||
#include <xrpl/basics/make_SSLContext.h>
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/beast/core/LexicalCast.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/server/SimpleWriter.h>
|
||||
|
||||
@@ -641,12 +639,6 @@ OverlayImpl::onManifests(
|
||||
auto const n = m->list_size();
|
||||
auto const& journal = from->pjournal();
|
||||
|
||||
// Once manifests are published on-ledger, gossip is checked against the
|
||||
// ledger before it is believed: that is where a revocation lives.
|
||||
std::shared_ptr<ReadView const> view = app_.openLedger().current();
|
||||
if (view && !view->rules().enabled(featureOnChainManifests))
|
||||
view.reset();
|
||||
|
||||
protocol::TMManifests relay;
|
||||
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
@@ -657,11 +649,13 @@ OverlayImpl::onManifests(
|
||||
{
|
||||
auto const serialized = mo->serialized;
|
||||
|
||||
auto const result = app_.validatorManifests().applyManifest(
|
||||
std::move(*mo), ManifestSource::gossip, view.get());
|
||||
auto const result =
|
||||
app_.validatorManifests().applyManifest(std::move(*mo));
|
||||
|
||||
if (result == ManifestDisposition::accepted)
|
||||
{
|
||||
relay.add_list()->set_stobject(s);
|
||||
|
||||
// N.B.: this is important; the applyManifest call above moves
|
||||
// the loaded Manifest out of the optional so we need to
|
||||
// reload it here.
|
||||
@@ -671,12 +665,6 @@ OverlayImpl::onManifests(
|
||||
"ripple::OverlayImpl::onManifests : manifest "
|
||||
"deserialization succeeded");
|
||||
|
||||
// An evicted manifest can be accepted again later. It is only
|
||||
// relayed again once its hash has aged out of the router, so
|
||||
// nodes trading the same entry back and forth stay quiet.
|
||||
if (app_.getHashRouter().shouldRelay(mo->hash()))
|
||||
relay.add_list()->set_stobject(s);
|
||||
|
||||
app_.getOPs().pubManifest(*mo);
|
||||
|
||||
if (app_.validators().listed(mo->masterKey))
|
||||
@@ -685,15 +673,6 @@ OverlayImpl::onManifests(
|
||||
addValidatorManifest(*db, serialized);
|
||||
}
|
||||
}
|
||||
else if (result == ManifestDisposition::unseen)
|
||||
{
|
||||
// Held until its ephemeral key is seen validating, and then
|
||||
// relayed from there; see handleNewValidation(). Note who
|
||||
// sent it, so it is not sent back.
|
||||
if (auto const held = deserializeManifest(serialized))
|
||||
app_.getHashRouter().addSuppressionPeer(
|
||||
held->hash(), from->id());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -704,11 +683,8 @@ OverlayImpl::onManifests(
|
||||
}
|
||||
|
||||
if (!relay.list().empty())
|
||||
for_each([m2 = std::make_shared<Message>(relay, protocol::mtMANIFESTS),
|
||||
source = from->id()](std::shared_ptr<PeerImp>&& p) {
|
||||
if (p->id() != source)
|
||||
p->send(m2);
|
||||
});
|
||||
for_each([m2 = std::make_shared<Message>(relay, protocol::mtMANIFESTS)](
|
||||
std::shared_ptr<PeerImp>&& p) { p->send(m2); });
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1217,10 +1193,12 @@ OverlayImpl::getManifestsMessage()
|
||||
{
|
||||
protocol::TMManifests tm;
|
||||
|
||||
// The list and gossip tiers of the cache, not the ledger tier; see
|
||||
// ManifestCache::for_each_gossip_manifest. Rebuilt whenever the cache
|
||||
// sequence changes, which every admission, eviction and change of
|
||||
// tier bumps.
|
||||
// A bounded subset of the cache rather than all of it; see
|
||||
// ManifestCache::for_each_gossip_manifest for what is selected.
|
||||
// This message is only rebuilt when the cache sequence changes, so a
|
||||
// shift in which manifests are the most recently used does not by
|
||||
// itself refresh it. That is acceptable: the pinned manifests are the
|
||||
// ones a peer needs, and they are always included.
|
||||
app_.validatorManifests().for_each_gossip_manifest(
|
||||
[&tm](std::size_t s) { tm.mutable_list()->Reserve(s); },
|
||||
[&tm, &hr = app_.getHashRouter()](Manifest const& manifest) {
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include <xrpld/rpc/detail/UDPInfoSub.h>
|
||||
#include <xrpld/rpc/detail/WSInfoSub.h>
|
||||
#include <xrpl/json/Output.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/server/Server.h>
|
||||
#include <xrpl/server/Session.h>
|
||||
#include <xrpl/server/WSSession.h>
|
||||
@@ -35,9 +34,6 @@
|
||||
#include <boost/beast/ssl/ssl_stream.hpp>
|
||||
#include <boost/utility/string_view.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
@@ -54,42 +50,6 @@ operator<(Port const& lhs, Port const& rhs)
|
||||
class ServerHandler
|
||||
{
|
||||
public:
|
||||
/** How a pwa port answered, for the pwa_info RPC. */
|
||||
enum class PWAOutcome : std::uint8_t {
|
||||
served, // 200
|
||||
notModified, // 304
|
||||
redirected, // 301
|
||||
notFound, // 404: no such loader
|
||||
badTarget, // 404: host and path name no document
|
||||
unknownDomain, // 404: no usable _xahau-pwa record
|
||||
badHost, // 400
|
||||
noForwardedFor, // 400
|
||||
directConnection, // 403: not from a secure_gateway address
|
||||
blocked, // 403: allowlist / blocklist
|
||||
domainMismatch, // 503: account's Domain does not name the host
|
||||
dnsPending, // 429: lookup started or in flight, retry
|
||||
dnsFailed, // 503: resolver failed
|
||||
upgradeRefused, // 403
|
||||
badMethod, // 405
|
||||
throttled, // 503: resource manager
|
||||
busy, // 503: too many in flight
|
||||
unavailable, // 503: no validated ledger or lookup failed
|
||||
count_
|
||||
};
|
||||
|
||||
struct PWAStats
|
||||
{
|
||||
std::array<std::atomic<std::uint64_t>, std::size_t(PWAOutcome::count_)>
|
||||
outcomes{};
|
||||
std::atomic<std::uint64_t> bytesServed{0};
|
||||
|
||||
static char const*
|
||||
name(PWAOutcome o);
|
||||
|
||||
Json::Value
|
||||
getJson(bool reset);
|
||||
};
|
||||
|
||||
struct Setup
|
||||
{
|
||||
explicit Setup() = default;
|
||||
@@ -139,47 +99,6 @@ private:
|
||||
std::condition_variable condition_;
|
||||
bool stopped_{false};
|
||||
std::map<std::reference_wrapper<Port const>, int> count_;
|
||||
PWAStats pwaStats_;
|
||||
|
||||
// PWA lookups queued or running on the job queue. Requests past
|
||||
// maxPWAJobs are refused with 503 rather than queued.
|
||||
std::atomic<std::size_t> pwaJobs_{0};
|
||||
static constexpr std::size_t maxPWAJobs = 64;
|
||||
|
||||
// Custom hosts: _xahau-pwa.<host> TXT -> account, looked up with
|
||||
// libresolv on jtCLIENT_PWADNS (it blocks) and cached: a record for its
|
||||
// TTL clamped to 60..3600 s, no record 15 min, a failure 60 s. At most
|
||||
// maxPWADnsInFlight lookups run at once, one per host, and a request
|
||||
// that misses the cache is answered 429 at once, so no flood of Host
|
||||
// headers can start more than a handful of queries or hold a thread.
|
||||
struct PWADns
|
||||
{
|
||||
enum class Status : std::uint8_t { found, none, failed };
|
||||
Status status = Status::failed;
|
||||
AccountID account;
|
||||
std::chrono::steady_clock::time_point expires;
|
||||
};
|
||||
std::mutex pwaDnsMutex_;
|
||||
std::map<std::string, PWADns> pwaDnsCache_;
|
||||
std::set<std::string> pwaDnsInFlight_;
|
||||
static constexpr std::size_t maxPWADnsInFlight = 4;
|
||||
static constexpr std::size_t maxPWADnsEntries = 10'000;
|
||||
|
||||
void
|
||||
resolvePWADomain(
|
||||
std::string const& host,
|
||||
std::optional<beast::IP::Endpoint> const& resolver);
|
||||
|
||||
// One document lookup, once the request has named an account.
|
||||
struct PWALookup
|
||||
{
|
||||
AccountID account;
|
||||
bool head = false;
|
||||
std::string ifNoneMatch;
|
||||
std::string domain; // custom host it came in on, if any
|
||||
std::uint64_t index = 0; // AccountIndex the host label named
|
||||
std::string redirectTail; // non-empty: bare-zone 301 to pwa<index>
|
||||
};
|
||||
|
||||
// A private type used to restrict access to the ServerHandler constructor.
|
||||
struct ServerHandlerCreator
|
||||
@@ -222,22 +141,6 @@ public:
|
||||
return setup_;
|
||||
}
|
||||
|
||||
PWAStats&
|
||||
pwaStats()
|
||||
{
|
||||
return pwaStats_;
|
||||
}
|
||||
|
||||
/** Drop every cached custom domain answer; returns how many. */
|
||||
std::size_t
|
||||
pwaDnsFlush()
|
||||
{
|
||||
std::lock_guard lock(pwaDnsMutex_);
|
||||
auto const n = pwaDnsCache_.size();
|
||||
pwaDnsCache_.clear();
|
||||
return n;
|
||||
}
|
||||
|
||||
Endpoints const&
|
||||
endpoints() const
|
||||
{
|
||||
@@ -314,28 +217,6 @@ private:
|
||||
std::shared_ptr<Session> const&,
|
||||
std::shared_ptr<JobQueue::Coro> coro);
|
||||
|
||||
/** Entry point for ports whose protocol is "pwa". Runs on the I/O
|
||||
thread: proxy and X-Forwarded-For checks, metering, routing of the
|
||||
Host header and target (document host, custom domain or bare-zone
|
||||
redirect), then
|
||||
queuePWALookup. */
|
||||
void
|
||||
onPWARequest(Session& session);
|
||||
|
||||
/** Apply the allowlist and blocklist, then post the ledger read to the
|
||||
job queue as jtCLIENT_PWA. Callable from any thread. */
|
||||
void
|
||||
queuePWALookup(
|
||||
std::shared_ptr<Session> const& session,
|
||||
PWALookup const& lookup);
|
||||
|
||||
/** Serve the document, or the bare-zone redirect, from the validated
|
||||
ledger. Runs on the job queue. Always closes the connection. */
|
||||
void
|
||||
processPWARequest(
|
||||
std::shared_ptr<Session> const& session,
|
||||
PWALookup const& lookup);
|
||||
|
||||
void
|
||||
processRequest(
|
||||
Port const& port,
|
||||
|
||||
@@ -114,7 +114,6 @@ Handler const handlerArray[]{
|
||||
byRef(&doGetAggregatePrice),
|
||||
Role::USER,
|
||||
NO_CONDITION},
|
||||
{"hook_validate", byRef(&doHookValidate), Role::USER, NO_CONDITION},
|
||||
{"inject", byRef(&doInject), Role::ADMIN, NEEDS_CURRENT_LEDGER},
|
||||
{"ledger_accept",
|
||||
byRef(&doLedgerAccept),
|
||||
@@ -144,8 +143,6 @@ Handler const handlerArray[]{
|
||||
{"path_find", byRef(&doPathFind), Role::USER, NEEDS_CURRENT_LEDGER},
|
||||
{"ping", byRef(&doPing), Role::USER, NO_CONDITION},
|
||||
{"print", byRef(&doPrint), Role::ADMIN, NO_CONDITION},
|
||||
{"pwa_info", byRef(&doPWAInfo), Role::ADMIN, NO_CONDITION},
|
||||
{"pwa_validate", byRef(&doPWAValidate), Role::USER, NO_CONDITION},
|
||||
// { "profile", byRef (&doProfile), Role::USER,
|
||||
// NEEDS_CURRENT_LEDGER },
|
||||
{"random", byRef(&doRandom), Role::USER, NO_CONDITION},
|
||||
|
||||
@@ -19,12 +19,10 @@
|
||||
|
||||
#include <xrpld/rpc/ServerHandler.h>
|
||||
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpld/core/JobQueue.h>
|
||||
#include <xrpld/ledger/ReadView.h>
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/rpc/RPCHandler.h>
|
||||
#include <xrpld/rpc/Role.h>
|
||||
@@ -32,20 +30,15 @@
|
||||
#include <xrpld/rpc/detail/Tuning.h>
|
||||
#include <xrpld/rpc/json_body.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/basics/make_SSLContext.h>
|
||||
#include <xrpl/beast/core/LexicalCast.h>
|
||||
#include <xrpl/beast/net/IPAddressConversion.h>
|
||||
#include <xrpl/beast/rfc2616.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
#include <xrpl/resource/ResourceManager.h>
|
||||
#include <xrpl/server/Server.h>
|
||||
@@ -53,24 +46,11 @@
|
||||
#include <xrpl/server/detail/JSONRPCUtil.h>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/beast/core/string.hpp>
|
||||
#include <boost/beast/http/fields.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <arpa/nameser.h>
|
||||
#include <netinet/in.h>
|
||||
#include <resolv.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
@@ -223,14 +203,6 @@ ServerHandler::onHandoff(
|
||||
p.count("ws") > 0 || p.count("ws2") > 0 || p.count("wss") > 0 ||
|
||||
p.count("wss2") > 0};
|
||||
|
||||
// PWA ports: no websockets, no peers, no status page. Every request,
|
||||
// upgrade requests included, goes to onRequest and from there to
|
||||
// onPWARequest, so that it meets the proxy rules, is metered and counted,
|
||||
// and is answered without the full version string statusRequestResponse
|
||||
// would put in the Server header.
|
||||
if (p.count("pwa") > 0)
|
||||
return {};
|
||||
|
||||
if (websocket::is_upgrade(request))
|
||||
{
|
||||
if (!is_ws)
|
||||
@@ -321,713 +293,9 @@ buffers_to_string(ConstBufferSequence const& bs)
|
||||
return s;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
//
|
||||
// PWA ports (protocol = pwa): serve accounts' on-ledger AppLoader documents
|
||||
// as HTML, one host per account, behind a reverse proxy only. See the "pwa
|
||||
// ports" section of xahaud-example.cfg.
|
||||
//
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
using Outcome = ServerHandler::PWAOutcome;
|
||||
|
||||
// The client a secure_gateway proxy vouches for: the RIGHTMOST entry of the
|
||||
// (last) X-Forwarded-For field, the only one the proxy itself wrote. Not
|
||||
// forwardedFor(), which takes the leftmost entry and trusts a client-supplied
|
||||
// Forwarded header. IPv4-mapped IPv6 is returned as IPv4.
|
||||
std::optional<boost::asio::ip::address>
|
||||
pwaClientAddress(http_request_type const& request)
|
||||
{
|
||||
std::string_view value;
|
||||
bool found = false;
|
||||
for (auto const& field : request)
|
||||
if (boost::beast::iequals(field.name_string(), "X-Forwarded-For"))
|
||||
{
|
||||
value = {field.value().data(), field.value().size()};
|
||||
found = true;
|
||||
}
|
||||
if (!found)
|
||||
return std::nullopt;
|
||||
if (auto const comma = value.rfind(','); comma != std::string_view::npos)
|
||||
value.remove_prefix(comma + 1);
|
||||
auto const isSpace = [](char c) { return c == ' ' || c == '\t'; };
|
||||
while (!value.empty() && isSpace(value.front()))
|
||||
value.remove_prefix(1);
|
||||
while (!value.empty() && isSpace(value.back()))
|
||||
value.remove_suffix(1);
|
||||
if (value.empty())
|
||||
return std::nullopt;
|
||||
|
||||
boost::system::error_code ec;
|
||||
auto addr = boost::asio::ip::make_address(std::string(value), ec);
|
||||
if (ec)
|
||||
return std::nullopt;
|
||||
if (addr.is_v6() && addr.to_v6().is_v4_mapped())
|
||||
addr = boost::asio::ip::make_address_v4(
|
||||
boost::asio::ip::v4_mapped, addr.to_v6());
|
||||
if (addr.is_unspecified())
|
||||
return std::nullopt;
|
||||
return addr;
|
||||
}
|
||||
|
||||
// Bill IPv4 per address and IPv6 per /64: a subscriber usually holds a /64.
|
||||
boost::asio::ip::address
|
||||
pwaBillingAddress(boost::asio::ip::address const& client)
|
||||
{
|
||||
if (!client.is_v6())
|
||||
return client;
|
||||
auto bytes = client.to_v6().to_bytes();
|
||||
std::fill(bytes.begin() + 8, bytes.end(), 0);
|
||||
return boost::asio::ip::address_v6(bytes);
|
||||
}
|
||||
|
||||
// What a request's Host header and target name. Every account has a host of
|
||||
// its own so that loaders never share an origin with each other or with the
|
||||
// operator's site:
|
||||
//
|
||||
// pwa<account-index>.<zone>/<r-address> the label is the AccountIndex
|
||||
// (decimal) of the account the
|
||||
// path names
|
||||
// <zone>/<r-address> 301 to the above, keeping :port
|
||||
// and query, scheme-relative
|
||||
// <custom host>/ with custom_domains: served as
|
||||
// the account its _xahau-pwa TXT
|
||||
// names, if that account's Domain
|
||||
// names the host (or a parent)
|
||||
//
|
||||
// One trailing slash and a query string are ignored. Anything else is 404.
|
||||
struct PWARoute
|
||||
{
|
||||
enum class Kind { account, domain, redirect, badHost, badTarget };
|
||||
Kind kind = Kind::badTarget;
|
||||
AccountID account;
|
||||
std::uint64_t index = 0; // account: what the label said
|
||||
std::string redirectTail; // redirect: Location after "//pwa<index>"
|
||||
std::string domain; // domain: the normalised Host, no port
|
||||
};
|
||||
|
||||
PWARoute
|
||||
parsePWATarget(Port const& port, http_request_type const& request)
|
||||
{
|
||||
using Kind = PWARoute::Kind;
|
||||
PWARoute route;
|
||||
auto badHost = [&] {
|
||||
route.kind = Kind::badHost;
|
||||
return route;
|
||||
};
|
||||
|
||||
// Exactly one Host, no IP literal; an optional :port (digits, since it
|
||||
// is echoed into a redirect) and trailing dot are dropped.
|
||||
auto const range = request.equal_range(boost::beast::http::field::host);
|
||||
if (range.first == request.end() || std::next(range.first) != range.second)
|
||||
return badHost();
|
||||
std::string host(range.first->value().data(), range.first->value().size());
|
||||
if (host.empty() || host.front() == '[')
|
||||
return badHost();
|
||||
std::string hostPort;
|
||||
if (auto const colon = host.find(':'); colon != std::string::npos)
|
||||
{
|
||||
hostPort = host.substr(colon);
|
||||
host.erase(colon);
|
||||
if (hostPort.size() < 2 || hostPort.size() > 6 ||
|
||||
hostPort.find_first_not_of("0123456789", 1) != std::string::npos)
|
||||
return badHost();
|
||||
}
|
||||
boost::algorithm::to_lower(host);
|
||||
if (!host.empty() && host.back() == '.')
|
||||
host.pop_back();
|
||||
if (host.empty() ||
|
||||
host.find_first_not_of("abcdefghijklmnopqrstuvwxyz0123456789-.") !=
|
||||
std::string::npos)
|
||||
return badHost();
|
||||
|
||||
// Path less query (kept for a redirect; beast has refused any target
|
||||
// with control characters) and one trailing slash.
|
||||
auto const target = request.target();
|
||||
if (target.empty() || target.front() != '/')
|
||||
return route;
|
||||
std::string path{target.substr(1)};
|
||||
std::string query;
|
||||
if (auto const cut = path.find_first_of("?#"); cut != std::string::npos)
|
||||
{
|
||||
if (path[cut] == '?')
|
||||
query = path.substr(cut, path.find('#', cut) - cut);
|
||||
path.erase(cut);
|
||||
}
|
||||
if (!path.empty() && path.back() == '/')
|
||||
path.pop_back();
|
||||
if (path.find('/') != std::string::npos)
|
||||
return route;
|
||||
|
||||
// Anything under one of our zones is a wrong URL, never a custom domain.
|
||||
bool const ours = port.pwa_domains.count(host) > 0 ||
|
||||
std::any_of(port.pwa_domains.begin(),
|
||||
port.pwa_domains.end(),
|
||||
[&](auto const& z) {
|
||||
return boost::algorithm::ends_with(host, "." + z);
|
||||
});
|
||||
if (!ours)
|
||||
{
|
||||
if (port.pwa_custom_domains && path.empty())
|
||||
{
|
||||
route.kind = Kind::domain;
|
||||
route.domain = std::move(host);
|
||||
}
|
||||
return route;
|
||||
}
|
||||
|
||||
auto const account = parseBase58<AccountID>(path);
|
||||
if (!account || account->isZero())
|
||||
return route;
|
||||
route.account = *account;
|
||||
|
||||
if (port.pwa_domains.count(host) > 0)
|
||||
{
|
||||
// Scheme-relative: the browser keeps the scheme it used. The index
|
||||
// comes from the ledger later.
|
||||
route.kind = Kind::redirect;
|
||||
route.redirectTail = "." + host + hostPort + "/" + path + query;
|
||||
return route;
|
||||
}
|
||||
|
||||
// pwa<decimal AccountIndex>.<zone>; whether it is the path's account's
|
||||
// index is checked against the ledger later.
|
||||
auto const dot = host.find('.');
|
||||
std::string label;
|
||||
if (dot != std::string::npos && dot > 3 && host.compare(0, 3, "pwa") == 0)
|
||||
label = host.substr(3, dot - 3);
|
||||
if (!label.empty() && port.pwa_domains.count(host.substr(dot + 1)) > 0 &&
|
||||
label.size() <= 20 && (label == "0" || label.front() != '0') &&
|
||||
label.find_first_not_of("0123456789") == std::string::npos &&
|
||||
beast::lexicalCastChecked(route.index, label))
|
||||
route.kind = Kind::account;
|
||||
return route;
|
||||
}
|
||||
|
||||
// Does an account's Domain field name `host`, or a parent of it?
|
||||
bool
|
||||
pwaEndorses(std::shared_ptr<SLE const> const& root, std::string const& host)
|
||||
{
|
||||
if (!root || !root->isFieldPresent(sfDomain))
|
||||
return false;
|
||||
auto const d = root->getFieldVL(sfDomain);
|
||||
auto const endorsed =
|
||||
boost::algorithm::to_lower_copy(std::string(d.begin(), d.end()));
|
||||
return !endorsed.empty() &&
|
||||
(host == endorsed || boost::algorithm::ends_with(host, "." + endorsed));
|
||||
}
|
||||
|
||||
std::string
|
||||
pwaReason(int status)
|
||||
{
|
||||
return std::string(boost::beast::http::obsolete_reason(
|
||||
static_cast<boost::beast::http::status>(status)));
|
||||
}
|
||||
|
||||
// A complete HTTP/1.1 response, then close: one request per connection.
|
||||
// HEAD gets the GET's headers, Content-Length included, and no body.
|
||||
void
|
||||
pwaReply(
|
||||
Session& session,
|
||||
int status,
|
||||
bool head,
|
||||
std::vector<std::string> const& headers = {},
|
||||
std::string const& body = {})
|
||||
{
|
||||
std::string out =
|
||||
"HTTP/1.1 " + std::to_string(status) + " " + pwaReason(status) + "\r\n";
|
||||
out += "Server: " + systemName() +
|
||||
"\r\nConnection: close\r\nX-Content-Type-Options: nosniff\r\n"
|
||||
"Referrer-Policy: no-referrer\r\n";
|
||||
for (auto const& h : headers)
|
||||
out += h + "\r\n";
|
||||
if (status != 304) // a 304 has no body and no Content-Length
|
||||
{
|
||||
if (status != 200 &&
|
||||
std::none_of(headers.begin(), headers.end(), [](auto const& h) {
|
||||
return boost::algorithm::istarts_with(h, "Content-Type:");
|
||||
}))
|
||||
out += "Content-Type: text/plain; charset=utf-8\r\n";
|
||||
if (status != 200 && status != 301)
|
||||
out += "Cache-Control: no-store\r\n";
|
||||
out += "Content-Length: " + std::to_string(body.size()) + "\r\n";
|
||||
}
|
||||
out += "\r\n";
|
||||
if (status != 304 && !head)
|
||||
out += body;
|
||||
session.write(out);
|
||||
session.close(true);
|
||||
}
|
||||
|
||||
void
|
||||
pwaError(
|
||||
Session& session,
|
||||
int status,
|
||||
bool head,
|
||||
std::vector<std::string> const& headers = {})
|
||||
{
|
||||
pwaReply(
|
||||
session, status, head, headers, std::string(pwaReason(status)) + "\n");
|
||||
}
|
||||
|
||||
// Releases a ServerHandler PWA job slot when the last copy of the job
|
||||
// closure dies, whether or not it ever ran.
|
||||
// Non-copyable: a copy would release the slot twice.
|
||||
struct PWAJobSlot
|
||||
{
|
||||
std::atomic<std::size_t>& count;
|
||||
|
||||
explicit PWAJobSlot(std::atomic<std::size_t>& c) : count(c)
|
||||
{
|
||||
}
|
||||
PWAJobSlot(PWAJobSlot const&) = delete;
|
||||
PWAJobSlot&
|
||||
operator=(PWAJobSlot const&) = delete;
|
||||
|
||||
~PWAJobSlot()
|
||||
{
|
||||
count.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
char const*
|
||||
ServerHandler::PWAStats::name(PWAOutcome o)
|
||||
{
|
||||
static constexpr char const* names[] = {
|
||||
"served",
|
||||
"not_modified",
|
||||
"redirected",
|
||||
"not_found",
|
||||
"bad_target",
|
||||
"unknown_domain",
|
||||
"bad_host",
|
||||
"no_forwarded_for",
|
||||
"direct_connection",
|
||||
"blocked",
|
||||
"domain_mismatch",
|
||||
"dns_pending",
|
||||
"dns_failed",
|
||||
"upgrade_refused",
|
||||
"bad_method",
|
||||
"throttled",
|
||||
"busy",
|
||||
"unavailable"};
|
||||
static_assert(std::size(names) == std::size_t(PWAOutcome::count_));
|
||||
return names[std::size_t(o)];
|
||||
}
|
||||
|
||||
Json::Value
|
||||
ServerHandler::PWAStats::getJson(bool reset)
|
||||
{
|
||||
Json::Value ret(Json::objectValue);
|
||||
Json::Value& o = ret["outcomes"] = Json::objectValue;
|
||||
std::uint64_t total = 0;
|
||||
for (std::size_t i = 0; i < outcomes.size(); ++i)
|
||||
{
|
||||
auto const n = reset ? outcomes[i].exchange(0) : outcomes[i].load();
|
||||
total += n;
|
||||
o[name(PWAOutcome(i))] = std::to_string(n);
|
||||
}
|
||||
ret["requests"] = std::to_string(total);
|
||||
ret["bytes_served"] =
|
||||
std::to_string(reset ? bytesServed.exchange(0) : bytesServed.load());
|
||||
return ret;
|
||||
}
|
||||
|
||||
void
|
||||
ServerHandler::onPWARequest(Session& session)
|
||||
{
|
||||
using boost::beast::http::verb;
|
||||
auto const& port = session.port();
|
||||
auto const& request = session.request();
|
||||
auto const remote = session.remoteAddress().at_port(0);
|
||||
bool const head = request.method() == verb::head;
|
||||
auto count = [&](Outcome o) { ++pwaStats_.outcomes[std::size_t(o)]; };
|
||||
|
||||
// Only the configured proxy, and it must say who it forwards for.
|
||||
if (!ipAllowed(
|
||||
remote.address(),
|
||||
port.secure_gateway_nets_v4,
|
||||
port.secure_gateway_nets_v6))
|
||||
{
|
||||
count(Outcome::directConnection);
|
||||
return pwaError(session, 403, head);
|
||||
}
|
||||
auto const forwarded = pwaClientAddress(request);
|
||||
if (!forwarded)
|
||||
{
|
||||
count(Outcome::noForwardedFor);
|
||||
return pwaError(session, 400, head);
|
||||
}
|
||||
|
||||
// Meter the client, never the proxy.
|
||||
auto usage = m_resourceManager.newInboundEndpoint(
|
||||
beast::IPAddressConversion::from_asio(pwaBillingAddress(*forwarded)));
|
||||
if (usage.disconnect(m_journal))
|
||||
{
|
||||
count(Outcome::throttled);
|
||||
return pwaError(session, 503, head);
|
||||
}
|
||||
usage.charge(Resource::feeReferenceRPC);
|
||||
|
||||
if (request.method() != verb::get && !head)
|
||||
{
|
||||
usage.charge(Resource::feeMalformedRPC);
|
||||
count(Outcome::badMethod);
|
||||
return pwaError(session, 405, head, {"Allow: GET, HEAD"});
|
||||
}
|
||||
if (boost::beast::websocket::is_upgrade(request))
|
||||
{
|
||||
usage.charge(Resource::feeMalformedRPC);
|
||||
count(Outcome::upgradeRefused);
|
||||
return pwaError(session, 403, head);
|
||||
}
|
||||
|
||||
auto const route = parsePWATarget(port, request);
|
||||
switch (route.kind)
|
||||
{
|
||||
case PWARoute::Kind::badHost:
|
||||
usage.charge(Resource::feeMalformedRPC);
|
||||
count(Outcome::badHost);
|
||||
return pwaError(session, 400, head);
|
||||
case PWARoute::Kind::badTarget:
|
||||
usage.charge(Resource::feeMalformedRPC);
|
||||
count(Outcome::badTarget);
|
||||
return pwaError(session, 404, head);
|
||||
case PWARoute::Kind::redirect:
|
||||
case PWARoute::Kind::account:
|
||||
case PWARoute::Kind::domain:
|
||||
break;
|
||||
}
|
||||
|
||||
// A miss costs what a hit does, so sweeping is no cheaper than reading.
|
||||
usage.charge(Resource::feeMediumBurdenRPC);
|
||||
|
||||
PWALookup lookup;
|
||||
lookup.account = route.account;
|
||||
lookup.head = head;
|
||||
lookup.domain = route.domain;
|
||||
lookup.index = route.index;
|
||||
lookup.redirectTail = route.redirectTail;
|
||||
if (auto const it = request.find(boost::beast::http::field::if_none_match);
|
||||
it != request.end())
|
||||
lookup.ifNoneMatch =
|
||||
std::string(it->value().data(), it->value().size());
|
||||
|
||||
if (route.kind == PWARoute::Kind::domain)
|
||||
{
|
||||
// The cache answers now. A miss starts one lookup (if fewer than
|
||||
// maxPWADnsInFlight are running and none for this host) and is
|
||||
// answered 429 with a refresh, so no request ever waits on DNS.
|
||||
std::optional<PWADns> dns;
|
||||
bool start = false;
|
||||
{
|
||||
std::lock_guard lock(pwaDnsMutex_);
|
||||
auto const it = pwaDnsCache_.find(route.domain);
|
||||
if (it != pwaDnsCache_.end() &&
|
||||
it->second.expires > std::chrono::steady_clock::now())
|
||||
dns = it->second;
|
||||
else if (
|
||||
pwaDnsInFlight_.size() < maxPWADnsInFlight &&
|
||||
pwaDnsInFlight_.insert(route.domain).second)
|
||||
start = true;
|
||||
}
|
||||
if (!dns)
|
||||
{
|
||||
usage.charge(Resource::feeMediumBurdenRPC);
|
||||
if (start &&
|
||||
!m_jobQueue.addJob(
|
||||
jtCLIENT_PWADNS,
|
||||
"PWA-DNS",
|
||||
[this, host = route.domain, resolver = port.pwa_resolver] {
|
||||
resolvePWADomain(host, resolver);
|
||||
}))
|
||||
{
|
||||
std::lock_guard lock(pwaDnsMutex_);
|
||||
pwaDnsInFlight_.erase(route.domain);
|
||||
}
|
||||
count(Outcome::dnsPending);
|
||||
return pwaReply(
|
||||
session,
|
||||
429,
|
||||
head,
|
||||
{"Retry-After: 2", "Content-Type: text/html; charset=utf-8"},
|
||||
"<!DOCTYPE html><html><head><meta http-equiv=\"refresh\" "
|
||||
"content=\"2\"></head><body>Looking up this host; retrying."
|
||||
"</body></html>\n");
|
||||
}
|
||||
switch (dns->status)
|
||||
{
|
||||
case PWADns::Status::none:
|
||||
count(Outcome::unknownDomain);
|
||||
return pwaError(session, 404, head);
|
||||
case PWADns::Status::failed:
|
||||
count(Outcome::dnsFailed);
|
||||
return pwaError(session, 503, head);
|
||||
case PWADns::Status::found:
|
||||
lookup.account = dns->account;
|
||||
}
|
||||
}
|
||||
queuePWALookup(session.detach(), lookup);
|
||||
}
|
||||
|
||||
void
|
||||
ServerHandler::resolvePWADomain(
|
||||
std::string const& host,
|
||||
std::optional<beast::IP::Endpoint> const& resolver)
|
||||
{
|
||||
using Status = PWADns::Status;
|
||||
PWADns dns;
|
||||
std::uint32_t ttl = 60;
|
||||
struct __res_state res
|
||||
{
|
||||
};
|
||||
if (res_ninit(&res) == 0)
|
||||
{
|
||||
res.retrans = 1; // seconds per attempt
|
||||
res.retry = 2;
|
||||
if (resolver)
|
||||
{
|
||||
res.nscount = 1;
|
||||
res.nsaddr_list[0].sin_family = AF_INET;
|
||||
res.nsaddr_list[0].sin_port = htons(resolver->port());
|
||||
res.nsaddr_list[0].sin_addr.s_addr =
|
||||
htonl(resolver->to_v4().to_uint());
|
||||
}
|
||||
unsigned char buf[4096];
|
||||
int const n = res_nquery(
|
||||
&res,
|
||||
("_xahau-pwa." + host).c_str(),
|
||||
ns_c_in,
|
||||
ns_t_txt,
|
||||
buf,
|
||||
sizeof(buf));
|
||||
ns_msg msg;
|
||||
if (n < 0)
|
||||
{
|
||||
// NXDOMAIN and NOERROR-without-data are both "no record".
|
||||
if (res.res_h_errno == HOST_NOT_FOUND || res.res_h_errno == NO_DATA)
|
||||
dns.status = Status::none;
|
||||
}
|
||||
else if (ns_initparse(buf, n, &msg) == 0)
|
||||
{
|
||||
// Exactly one distinct address across the TXT records; other
|
||||
// strings are ignored, two or more addresses count as none.
|
||||
dns.status = Status::none;
|
||||
bool conflict = false;
|
||||
ns_rr rr;
|
||||
for (int i = 0; i < ns_msg_count(msg, ns_s_an); ++i)
|
||||
{
|
||||
if (ns_parserr(&msg, ns_s_an, i, &rr) != 0 ||
|
||||
ns_rr_type(rr) != ns_t_txt)
|
||||
continue;
|
||||
auto const* p = ns_rr_rdata(rr);
|
||||
auto const* const end = p + ns_rr_rdlen(rr);
|
||||
for (; p < end && p + 1 + *p <= end; p += 1 + *p)
|
||||
{
|
||||
auto const a = parseBase58<AccountID>(
|
||||
boost::algorithm::trim_copy(std::string(
|
||||
reinterpret_cast<char const*>(p + 1), *p)));
|
||||
if (!a || a->isZero())
|
||||
continue;
|
||||
conflict = conflict ||
|
||||
(dns.status == Status::found && dns.account != *a);
|
||||
dns.status = Status::found;
|
||||
dns.account = *a;
|
||||
ttl = std::clamp<std::uint32_t>(ns_rr_ttl(rr), 60, 3600);
|
||||
}
|
||||
}
|
||||
if (conflict)
|
||||
dns.status = Status::none;
|
||||
}
|
||||
res_nclose(&res);
|
||||
}
|
||||
dns.expires = std::chrono::steady_clock::now() +
|
||||
(dns.status == Status::found ? std::chrono::seconds(ttl)
|
||||
: dns.status == Status::none
|
||||
? std::chrono::seconds(std::chrono::minutes(15))
|
||||
: std::chrono::seconds(60));
|
||||
|
||||
std::lock_guard lock(pwaDnsMutex_);
|
||||
pwaDnsInFlight_.erase(host);
|
||||
if (pwaDnsCache_.size() >= maxPWADnsEntries)
|
||||
{
|
||||
auto const now = std::chrono::steady_clock::now();
|
||||
std::erase_if(pwaDnsCache_, [&](auto const& kv) {
|
||||
return kv.second.expires <= now;
|
||||
});
|
||||
if (pwaDnsCache_.size() >= maxPWADnsEntries)
|
||||
pwaDnsCache_.erase(pwaDnsCache_.begin());
|
||||
}
|
||||
pwaDnsCache_[host] = dns;
|
||||
}
|
||||
|
||||
void
|
||||
ServerHandler::queuePWALookup(
|
||||
std::shared_ptr<Session> const& session,
|
||||
PWALookup const& lookup)
|
||||
{
|
||||
auto const& port = session->port();
|
||||
if (port.pwa_blocklist.count(lookup.account) > 0 ||
|
||||
(!port.pwa_allowlist.empty() &&
|
||||
port.pwa_allowlist.count(lookup.account) == 0))
|
||||
{
|
||||
++pwaStats_.outcomes[std::size_t(Outcome::blocked)];
|
||||
return pwaError(*session, 403, lookup.head);
|
||||
}
|
||||
|
||||
// jtCLIENT_PWA limits the workers; this bounds the queue behind them
|
||||
// and so the detached connections it holds.
|
||||
if (pwaJobs_.fetch_add(1, std::memory_order_relaxed) >= maxPWAJobs)
|
||||
{
|
||||
pwaJobs_.fetch_sub(1, std::memory_order_relaxed);
|
||||
++pwaStats_.outcomes[std::size_t(Outcome::busy)];
|
||||
return pwaError(*session, 503, lookup.head);
|
||||
}
|
||||
auto const slot = std::make_shared<PWAJobSlot>(pwaJobs_);
|
||||
if (!m_jobQueue.addJob(
|
||||
jtCLIENT_PWA, "PWA-Client", [this, session, slot, lookup]() {
|
||||
processPWARequest(session, lookup);
|
||||
}))
|
||||
{
|
||||
++pwaStats_.outcomes[std::size_t(Outcome::unavailable)];
|
||||
pwaError(*session, 503, lookup.head);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ServerHandler::processPWARequest(
|
||||
std::shared_ptr<Session> const& session,
|
||||
PWALookup const& lookup)
|
||||
{
|
||||
auto const& account = lookup.account;
|
||||
|
||||
// Decide the whole response before writing any of it: the ledger read
|
||||
// can throw (SHAMapMissingNode), and nothing above Job::doJob catches.
|
||||
Outcome outcome = Outcome::unavailable;
|
||||
int status = 503;
|
||||
std::vector<std::string> headers;
|
||||
std::string body;
|
||||
try
|
||||
{
|
||||
// Validated state only: a closed ledger may still lose consensus.
|
||||
auto const ledger = app_.getLedgerMaster().getValidatedLedger();
|
||||
std::shared_ptr<SLE const> sle, root;
|
||||
if (ledger)
|
||||
{
|
||||
sle = ledger->read(keylet::appLoader(account));
|
||||
root = ledger->read(keylet::account(account));
|
||||
}
|
||||
// The host label must be the account's AccountIndex; an account
|
||||
// without one (created before it existed) has no host.
|
||||
std::optional<std::uint64_t> index;
|
||||
if (root)
|
||||
index = (*root)[~sfAccountIndex];
|
||||
if (!ledger)
|
||||
{
|
||||
}
|
||||
else if (!lookup.redirectTail.empty())
|
||||
{
|
||||
if (index)
|
||||
{
|
||||
outcome = Outcome::redirected;
|
||||
status = 301;
|
||||
headers = {
|
||||
"Location: //pwa" + std::to_string(*index) +
|
||||
lookup.redirectTail,
|
||||
"Cache-Control: public, max-age=86400"};
|
||||
body = "Moved Permanently\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
outcome = Outcome::notFound;
|
||||
status = 404;
|
||||
}
|
||||
}
|
||||
else if (!sle || (lookup.domain.empty() && index != lookup.index))
|
||||
{
|
||||
outcome = Outcome::notFound;
|
||||
status = 404;
|
||||
}
|
||||
else if (!lookup.domain.empty() && !pwaEndorses(root, lookup.domain))
|
||||
{
|
||||
// A custom host serves an account only with its consent.
|
||||
outcome = Outcome::domainMismatch;
|
||||
status = 503;
|
||||
}
|
||||
else if (sle)
|
||||
{
|
||||
// Every change is threaded, so the last transaction that touched
|
||||
// the object is a strong validator for its content.
|
||||
std::string const etag =
|
||||
"\"" + to_string(sle->getFieldH256(sfPreviousTxnID)) + "\"";
|
||||
headers = {
|
||||
"Content-Security-Policy: sandbox allow-scripts allow-forms "
|
||||
"allow-popups",
|
||||
"X-Frame-Options: DENY",
|
||||
"Cross-Origin-Resource-Policy: same-origin",
|
||||
"Cross-Origin-Opener-Policy: same-origin",
|
||||
"Cache-Control: no-cache", // store, but revalidate
|
||||
"ETag: " + etag,
|
||||
// Enough for a client to vet the document against a second
|
||||
// node (pwa_validate, or ledger_entry app_loader at this
|
||||
// ledger).
|
||||
"X-NetworkID: " + std::to_string(app_.config().NETWORK_ID),
|
||||
"X-LedgerSeq: " + std::to_string(ledger->seq()),
|
||||
"X-LedgerHash: " + to_string(ledger->info().hash),
|
||||
"X-PWASeq: " +
|
||||
std::to_string(sle->getFieldU32(sfAppLoaderSequence))};
|
||||
|
||||
// If-None-Match: the tag as we sent it, or *.
|
||||
if (lookup.ifNoneMatch == etag || lookup.ifNoneMatch == "*")
|
||||
{
|
||||
outcome = Outcome::notModified;
|
||||
status = 304;
|
||||
}
|
||||
else
|
||||
{
|
||||
Blob const blob = sle->getFieldVL(sfAppLoader);
|
||||
body.assign(blob.begin(), blob.end());
|
||||
headers.push_back("Content-Type: text/html; charset=utf-8");
|
||||
outcome = Outcome::served;
|
||||
status = 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(app_.journal("PWA").warn())
|
||||
<< "lookup for " << toBase58(account) << " failed: " << e.what();
|
||||
outcome = Outcome::unavailable;
|
||||
status = 503;
|
||||
headers.clear();
|
||||
body.clear();
|
||||
}
|
||||
|
||||
++pwaStats_.outcomes[std::size_t(outcome)];
|
||||
if (status == 200 || status == 304 || status == 301)
|
||||
{
|
||||
if (status == 200 && !lookup.head)
|
||||
pwaStats_.bytesServed += body.size();
|
||||
return pwaReply(*session, status, lookup.head, headers, body);
|
||||
}
|
||||
pwaError(*session, status, lookup.head);
|
||||
}
|
||||
|
||||
void
|
||||
ServerHandler::onRequest(Session& session)
|
||||
{
|
||||
// PWA ports speak nothing else.
|
||||
if (session.port().protocol.count("pwa") > 0)
|
||||
{
|
||||
onPWARequest(session);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure RPC is enabled on the port
|
||||
if (session.port().protocol.count("http") == 0 &&
|
||||
session.port().protocol.count("https") == 0)
|
||||
@@ -2065,9 +1333,6 @@ to_Port(ParsedPort const& parsed, std::ostream& log)
|
||||
p.admin_nets_v6 = parsed.admin_nets_v6;
|
||||
p.secure_gateway_nets_v4 = parsed.secure_gateway_nets_v4;
|
||||
p.secure_gateway_nets_v6 = parsed.secure_gateway_nets_v6;
|
||||
p.pwa_domains = parsed.pwa_domains;
|
||||
p.pwa_custom_domains = parsed.pwa_custom_domains;
|
||||
p.pwa_resolver = parsed.pwa_resolver;
|
||||
|
||||
return p;
|
||||
}
|
||||
@@ -2106,74 +1371,6 @@ parse_Ports(Config const& config, std::ostream& log)
|
||||
result.push_back(to_Port(parsed, log));
|
||||
}
|
||||
|
||||
// PWA ports host third-party content and must only ever be reached
|
||||
// through a reverse proxy. [pwa_allowlist] and [pwa_blocklist], one
|
||||
// r-address per line, apply to all of them.
|
||||
auto accounts = [&](char const* name) {
|
||||
std::set<AccountID> out;
|
||||
for (auto const& line : config.section(name).values())
|
||||
{
|
||||
auto const account = parseBase58<AccountID>(line);
|
||||
if (!account || account->isZero())
|
||||
{
|
||||
log << "Invalid r-address '" << line << "' in [" << name << "]";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
out.insert(*account);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
auto const allowlist = accounts(SECTION_PWA_ALLOWLIST);
|
||||
auto const blocklist = accounts(SECTION_PWA_BLOCKLIST);
|
||||
for (auto const& a : allowlist)
|
||||
if (blocklist.count(a) > 0)
|
||||
{
|
||||
log << "Account " << toBase58(a)
|
||||
<< " is in both [" SECTION_PWA_ALLOWLIST
|
||||
"] and [" SECTION_PWA_BLOCKLIST "]";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
for (auto& p : result)
|
||||
{
|
||||
auto fail = [&](char const* why) {
|
||||
log << "Invalid [" << p.name << "]: " << why;
|
||||
Throw<std::exception>();
|
||||
};
|
||||
if (p.protocol.count("pwa") == 0)
|
||||
{
|
||||
if (!p.pwa_domains.empty() || p.pwa_custom_domains ||
|
||||
p.pwa_resolver)
|
||||
fail(
|
||||
"domain, custom_domains and resolver apply to pwa "
|
||||
"ports only");
|
||||
continue;
|
||||
}
|
||||
if (p.protocol.size() != 1)
|
||||
fail("pwa cannot be combined with any other protocol");
|
||||
if (p.pwa_domains.empty())
|
||||
fail("pwa port requires domain");
|
||||
// A /0 gateway would let every client write its own X-Forwarded-For.
|
||||
auto const everyAddress = [](auto const& nets) {
|
||||
return std::any_of(nets.begin(), nets.end(), [](auto const& n) {
|
||||
return n.prefix_length() == 0;
|
||||
});
|
||||
};
|
||||
if ((p.secure_gateway_nets_v4.empty() &&
|
||||
p.secure_gateway_nets_v6.empty()) ||
|
||||
everyAddress(p.secure_gateway_nets_v4) ||
|
||||
everyAddress(p.secure_gateway_nets_v6))
|
||||
fail(
|
||||
"pwa port requires secure_gateway naming the proxy's own "
|
||||
"addresses");
|
||||
if (!p.admin_nets_v4.empty() || !p.admin_nets_v6.empty() ||
|
||||
!p.admin_user.empty() || !p.admin_password.empty() ||
|
||||
!p.user.empty() || !p.password.empty() || !p.ssl_key.empty() ||
|
||||
!p.ssl_cert.empty() || !p.ssl_chain.empty())
|
||||
fail("pwa port cannot have admin, credentials or TLS");
|
||||
p.pwa_allowlist = allowlist;
|
||||
p.pwa_blocklist = blocklist;
|
||||
}
|
||||
|
||||
if (config.standalone())
|
||||
{
|
||||
auto it = result.begin();
|
||||
|
||||
@@ -75,8 +75,6 @@ doGetCounts(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doGetAggregatePrice(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doHookValidate(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doLedgerAccept(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doLedgerCleaner(RPC::JsonContext&);
|
||||
@@ -115,10 +113,6 @@ doPeers(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doPing(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doPWAInfo(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doPWAValidate(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doPrint(RPC::JsonContext&);
|
||||
Json::Value
|
||||
doRandom(RPC::JsonContext&);
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/hook/applyHook.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/hook/Enum.h>
|
||||
#include <xrpl/hook/Guard.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/Rules.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
Json::Value
|
||||
doHookValidate(RPC::JsonContext& context)
|
||||
{
|
||||
if (!context.app.config().HOOK_VALIDATE_RPC)
|
||||
return rpcError(rpcNOT_SUPPORTED);
|
||||
|
||||
context.loadType = Resource::feeHeavyBurdenRPC;
|
||||
|
||||
if (!context.params.isMember(jss::code))
|
||||
return RPC::missing_field_error(jss::code);
|
||||
|
||||
if (!context.params[jss::code].isString())
|
||||
return RPC::expected_field_error(jss::code, "hex string");
|
||||
|
||||
std::string const hex = context.params[jss::code].asString();
|
||||
|
||||
// strUnHex accepts odd-length input by treating the leading nibble as a
|
||||
// byte, which would let half-byte garbage through as a 1-byte module.
|
||||
if (hex.size() % 2 != 0)
|
||||
return RPC::invalid_field_error(jss::code);
|
||||
|
||||
// Reject oversized input before decoding it.
|
||||
if (hex.size() > 2 * static_cast<std::size_t>(hook::maxHookWasmSize()))
|
||||
return RPC::make_param_error(
|
||||
"code exceeds the maximum hook size of " +
|
||||
std::to_string(hook::maxHookWasmSize()) + " bytes");
|
||||
|
||||
auto const blob = strUnHex(hex);
|
||||
if (!blob || blob->empty())
|
||||
return RPC::invalid_field_error(jss::code);
|
||||
|
||||
// Copy the Rules: current() hands back a temporary snapshot, so binding a
|
||||
// reference to its rules() would dangle once the open ledger moves on.
|
||||
Rules const rules = context.app.openLedger().current()->rules();
|
||||
|
||||
std::ostringstream loggerStream;
|
||||
std::optional<std::reference_wrapper<std::basic_ostream<char>>> logger =
|
||||
loggerStream;
|
||||
std::string exceptionMsg;
|
||||
std::optional<std::pair<uint64_t, uint64_t>> result;
|
||||
try
|
||||
{
|
||||
result = validateGuards(
|
||||
*blob,
|
||||
logger,
|
||||
"",
|
||||
hook_api::getImportWhitelist(rules),
|
||||
hook_api::getGuardRulesVersion(rules));
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
exceptionMsg = e.what();
|
||||
}
|
||||
|
||||
Json::Value jvResult(Json::objectValue);
|
||||
jvResult[jss::valid] = result.has_value();
|
||||
if (result)
|
||||
{
|
||||
jvResult[jss::instruction_count_hook] = std::to_string(result->first);
|
||||
jvResult[jss::instruction_count_cbak] = std::to_string(result->second);
|
||||
|
||||
if (auto const vmError =
|
||||
hook::HookExecutor::validateWasm(blob->data(), blob->size()))
|
||||
{
|
||||
jvResult[jss::valid] = false;
|
||||
jvResult[jss::vm_error] = *vmError;
|
||||
}
|
||||
}
|
||||
|
||||
Json::Value& log = jvResult[jss::log] = Json::arrayValue;
|
||||
std::istringstream lines(loggerStream.str());
|
||||
for (std::string line; std::getline(lines, line);)
|
||||
if (!line.empty())
|
||||
log.append(line);
|
||||
if (!exceptionMsg.empty())
|
||||
log.append(exceptionMsg);
|
||||
|
||||
return jvResult;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -352,24 +352,6 @@ parseRippleState(Json::Value const& jvRippleState, Json::Value& jvResult)
|
||||
return keylet::line(*id1, *id2, uCurrency).key;
|
||||
}
|
||||
|
||||
// featureRNG: the Random object is a singleton, so `"random": true` selects
|
||||
// it.
|
||||
// A string is an explicit index, as for the other types.
|
||||
static std::optional<uint256>
|
||||
parseRandom(Json::Value const& params, Json::Value& jvResult)
|
||||
{
|
||||
if (params.isString())
|
||||
return parseIndex(params, jvResult);
|
||||
|
||||
if (!params.isBool() || !params.asBool())
|
||||
{
|
||||
jvResult[jss::error] = "malformedRequest";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return keylet::random().key;
|
||||
}
|
||||
|
||||
static std::optional<uint256>
|
||||
parseTicket(Json::Value const& params, Json::Value& jvResult)
|
||||
{
|
||||
@@ -651,25 +633,6 @@ parseXChainOwnedCreateAccountClaimID(
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static std::optional<uint256>
|
||||
parseAppLoader(Json::Value const& params, Json::Value& jvResult)
|
||||
{
|
||||
if (!params.isString())
|
||||
{
|
||||
jvResult[jss::error] = "malformedAddress";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto const account = parseBase58<AccountID>(params.asString());
|
||||
if (!account || account->isZero())
|
||||
{
|
||||
jvResult[jss::error] = "malformedAddress";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return keylet::appLoader(*account).key;
|
||||
}
|
||||
|
||||
static std::optional<uint256>
|
||||
parseDID(Json::Value const& params, Json::Value& jvResult)
|
||||
{
|
||||
@@ -1127,7 +1090,6 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
{jss::account_root, parseAccountRoot, ltACCOUNT_ROOT},
|
||||
// TODO: add amendments
|
||||
{jss::amm, parseAMM, ltAMM},
|
||||
{jss::app_loader, parseAppLoader, ltAPP_LOADER},
|
||||
{jss::bridge, parseBridge, ltBRIDGE},
|
||||
{jss::check, parseCheck, ltCHECK},
|
||||
{jss::credential, parseCredential, ltCREDENTIAL},
|
||||
@@ -1153,7 +1115,6 @@ doLedgerEntry(RPC::JsonContext& context)
|
||||
{jss::permissioned_domain,
|
||||
parsePermissionedDomains,
|
||||
ltPERMISSIONED_DOMAIN},
|
||||
{jss::random, parseRandom, ltRANDOM},
|
||||
{jss::ripple_state, parseRippleState, ltRIPPLE_STATE},
|
||||
// This is an alias, since the `ledger_data` filter uses jss::state
|
||||
{jss::state, parseRippleState, ltRIPPLE_STATE},
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpld/rpc/ServerHandler.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** pwa_info: usage of this node's "pwa" ports. Admin only.
|
||||
|
||||
{"reset": true} zeroes the request counters after reading them;
|
||||
{"flush_dns": true} drops every cached custom domain answer. Returns the
|
||||
ports (name, ip, port, domains, custom_domains), "requests" and
|
||||
"outcomes" (64-bit counters as strings) and "bytes_served".
|
||||
*/
|
||||
Json::Value
|
||||
doPWAInfo(RPC::JsonContext& context)
|
||||
{
|
||||
auto const& params = context.params;
|
||||
auto flag = [&](char const* key, bool& out) {
|
||||
if (!params.isMember(key))
|
||||
return true;
|
||||
if (!params[key].isBool())
|
||||
return false;
|
||||
out = params[key].asBool();
|
||||
return true;
|
||||
};
|
||||
bool reset = false, flush = false;
|
||||
if (!flag(jss::reset, reset))
|
||||
return RPC::expected_field_error(jss::reset, "bool");
|
||||
if (!flag("flush_dns", flush))
|
||||
return RPC::expected_field_error("flush_dns", "bool");
|
||||
|
||||
auto& handler = context.app.getServerHandler();
|
||||
Json::Value ret = handler.pwaStats().getJson(reset);
|
||||
if (flush)
|
||||
ret["dns_flushed"] = static_cast<Json::UInt>(handler.pwaDnsFlush());
|
||||
Json::Value& ports = ret[jss::ports] = Json::arrayValue;
|
||||
for (auto const& port : handler.setup().ports)
|
||||
{
|
||||
if (port.protocol.count("pwa") == 0)
|
||||
continue;
|
||||
Json::Value& p = ports.append(Json::objectValue);
|
||||
p[jss::name] = port.name;
|
||||
p[jss::ip] = port.ip.to_string();
|
||||
p[jss::port] = port.port;
|
||||
Json::Value& domains = p["domains"] = Json::arrayValue;
|
||||
for (auto const& d : port.pwa_domains)
|
||||
domains.append(d);
|
||||
p["custom_domains"] = port.pwa_custom_domains;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -1,111 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/rpc/Context.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** pwa_validate: is a served AppLoader document the account's current one?
|
||||
|
||||
{"account": <r-address>, "hash": <hex SHA-512Half of the bytes served>,
|
||||
"domain": <host it was served from, optional>}
|
||||
|
||||
Judged against the validated ledger. "valid" is true when the account's
|
||||
current loader hashes to `hash` and, if `domain` is given, the account's
|
||||
Domain field equals it or is a parent of it (ignoring case) -- i.e. the
|
||||
owner endorses that host. On false, "reason" says why. A client that
|
||||
was served from an older ledger (X-LedgerSeq) than "ledger_index" and
|
||||
gets "hash mismatch" should simply reload.
|
||||
*/
|
||||
Json::Value
|
||||
doPWAValidate(RPC::JsonContext& context)
|
||||
{
|
||||
auto const& params = context.params;
|
||||
for (auto const& field : {jss::account, jss::hash})
|
||||
if (!params.isMember(field))
|
||||
return RPC::missing_field_error(field);
|
||||
else if (!params[field].isString())
|
||||
return RPC::invalid_field_error(field);
|
||||
auto const account =
|
||||
parseBase58<AccountID>(params[jss::account].asString());
|
||||
if (!account || account->isZero())
|
||||
return rpcError(rpcACT_MALFORMED);
|
||||
uint256 hash;
|
||||
if (!hash.parseHex(params[jss::hash].asString()))
|
||||
return RPC::invalid_field_error(jss::hash);
|
||||
std::string domain;
|
||||
if (params.isMember(jss::domain))
|
||||
{
|
||||
if (!params[jss::domain].isString())
|
||||
return RPC::invalid_field_error(jss::domain);
|
||||
domain =
|
||||
boost::algorithm::to_lower_copy(params[jss::domain].asString());
|
||||
if (!domain.empty() && domain.back() == '.')
|
||||
domain.pop_back();
|
||||
}
|
||||
|
||||
auto const ledger = context.ledgerMaster.getValidatedLedger();
|
||||
if (!ledger)
|
||||
return rpcError(rpcNOT_SYNCED);
|
||||
|
||||
Json::Value ret(Json::objectValue);
|
||||
ret[jss::ledger_index] = ledger->seq();
|
||||
auto invalid = [&](char const* reason) {
|
||||
ret["valid"] = false;
|
||||
ret["reason"] = reason;
|
||||
return ret;
|
||||
};
|
||||
|
||||
auto const sle = ledger->read(keylet::appLoader(*account));
|
||||
if (!sle)
|
||||
return invalid("no loader");
|
||||
ret["app_loader_sequence"] = (*sle)[sfAppLoaderSequence];
|
||||
if (sha512Half(makeSlice(sle->getFieldVL(sfAppLoader))) != hash)
|
||||
return invalid("hash mismatch");
|
||||
if (!domain.empty())
|
||||
{
|
||||
auto const root = ledger->read(keylet::account(*account));
|
||||
std::string endorsed;
|
||||
if (root && root->isFieldPresent(sfDomain))
|
||||
{
|
||||
auto const d = root->getFieldVL(sfDomain);
|
||||
endorsed = boost::algorithm::to_lower_copy(
|
||||
std::string(d.begin(), d.end()));
|
||||
}
|
||||
if (endorsed.empty() ||
|
||||
(domain != endorsed &&
|
||||
!boost::algorithm::ends_with(domain, "." + endorsed)))
|
||||
return invalid("domain not endorsed by account");
|
||||
}
|
||||
ret["valid"] = true;
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
Loaded 100 of 102 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user