Compare commits

..
102 changed files with 473 additions and 10744 deletions

No files matched your search

+92 -1
View File
@@ -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
-2
View File
@@ -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
-161
View File
@@ -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
-1
View File
@@ -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)
-1
View File
@@ -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
-6
View File
@@ -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
View File
@@ -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"
-8
View File
@@ -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)
-1
View File
@@ -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
+1 -5
View File
@@ -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;
+1 -5
View File
@@ -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')
-8
View File
@@ -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
-66
View File
@@ -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
+2 -25
View File
@@ -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);
-19
View File
@@ -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;
-1
View File
@@ -197,7 +197,6 @@ enum TEFcodes : TERUnderlyingType {
tefINVALID_LEDGER_FIX_TYPE,
tefPAST_MANIFEST_SEQ,
tefREVOKED_MANIFEST,
tefMANIFEST_KEY_IN_USE,
};
//------------------------------------------------------------------------------
-21
View File
@@ -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, ({
+2 -7
View File
@@ -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
-10
View File
@@ -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
+1 -1
View File
@@ -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();
}
-180
View File
@@ -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
-24
View File
@@ -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(),
{
+1 -2
View File
@@ -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
-1
View File
@@ -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."),
-9
View File
@@ -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;
}
-69
View File
@@ -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
-474
View File
@@ -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");
-400
View File
@@ -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
-111
View File
@@ -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
-9
View File
@@ -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
+9 -312
View File
@@ -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);
}
};
+2 -18
View File
@@ -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);
-62
View File
@@ -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();
}
};
-34
View File
@@ -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();
}
};
-15
View File
@@ -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)
-86
View File
@@ -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();
}
};
-198
View File
@@ -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
-44
View File
@@ -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.",
+3 -124
View File
@@ -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,
-23
View File
@@ -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
+4 -59
View File
@@ -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);
-14
View File
@@ -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
-1
View File
@@ -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{};
-159
View File
@@ -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);
+1 -45
View File
@@ -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,
-34
View File
@@ -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;
-126
View File
@@ -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
+17 -5
View File
@@ -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
-1
View File
@@ -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
View File
@@ -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);
}
};
-101
View File
@@ -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
+7 -122
View File
@@ -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())
-207
View File
@@ -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
+9 -10
View File
@@ -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. "
+3 -5
View File
@@ -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);
/**
+4 -4
View File
@@ -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();
}
-24
View File
@@ -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>
-170
View File
@@ -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()
{
-4
View File
@@ -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
-16
View File
@@ -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
+3 -33
View File
@@ -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
-78
View File
@@ -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
//
-8
View File
@@ -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);
+89 -42
View File
@@ -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
+1 -13
View File
@@ -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);
-176
View File
@@ -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(
-14
View File
@@ -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
-7
View File
@@ -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
-4
View File
@@ -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"
-2
View File
@@ -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
-4
View File
@@ -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);
-9
View File
@@ -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
{
-16
View File
@@ -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);
+1 -9
View File
@@ -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
-40
View File
@@ -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},
+12 -34
View File
@@ -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) {
-119
View File
@@ -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,
-3
View File
@@ -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},
-803
View File
@@ -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();
-6
View File
@@ -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&);
-117
View File
@@ -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
-39
View File
@@ -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},
-74
View File
@@ -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
-111
View File
@@ -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