Compare commits

..
Author SHA1 Message Date
Richard Holland 096a2e455c ordering 2026-10-17 10:21:48 +11:00
Richard Holland f70eaba373 testnet genesis fix 2026-10-17 10:01:46 +11:00
Richard Holland b9f2914c0e catalogue speed and memory fixes (#883) 2026-10-14 14:13:37 +10:00
tequ 023f55e283 fix: keep ledger_history in standalone mode for rwdb null node store (#893) 2026-10-14 12:30:51 +10:00
tequandRichard Holland d20e8ffb56 fix(hook): crash on the transaction after removing a HookName (#890)
* fix(hook): Read sfHookName via isFieldPresent instead of operator[]

SetHook removes a hook's name with makeFieldAbsent on an untemplated
sfHooks entry, which leaves an STI_NOTPRESENT placeholder for
sfHookName. STObject::at(~sfHookName) then fails the dynamic_cast,
dereferences the null template and asserts (null deref in release).

Every other optional field of an sfHooks entry is already read with
isFieldPresent/getField*, which treats the placeholder as absent.
Align the three sfHookName readers with that convention:

- Transactor::calculateHookChainFee
- Transactor::executeHookChain
- ClaimReward::preclaim (fix20260929 path)

hook::canHook now takes std::optional<Blob> so callers can pass the
copied value without a dangling Slice.

Add regression tests that remove a hook name with a HookName-only
SetHook update and then submit the next transaction (Invoke and
ClaimReward). Both crashed before this change.

* claude fixes

---------

Co-authored-by: Richard Holland <[email protected]>
2026-10-13 10:57:14 +10:00
Richard Holland c63ee8cc5f fix mkdir in release builder (#889) 2026-10-12 13:19:11 +10:00
Richard Holland 64a30b2a15 Various fixes (#888) 2026-10-12 12:48:34 +10:00
Richard Holland 0025355f43 UDP Superhighway (#877) 2026-10-12 10:54:31 +10:00
Richard Holland 7046616cdb FeatureRNG (#869) 2026-10-12 10:49:46 +10:00
Richard Holland a2e7f3c645 PWABootLoader (#793) 2026-10-12 08:22:29 +10:00
Richard Holland 004752d1c2 On Chain Manifests Directory, Partial Sync and Gossip Bloom Filter (#861) 2026-10-12 08:08:29 +10:00
tequ 22e6ad21b4 Add hook_validate RPC to run the hook guard checker (#882) 2026-10-11 23:08:29 +10:00
tequ 3423ee4665 fix: Stop embedded tests from hanging on ARM by using atomic_flag (#6248) (#887) 2026-10-11 22:39:17 +10:00
Richard Holland a2a1e09c2e fix for clang-16 builds (#885) 2026-10-11 16:22:56 +10:00
tequ 505c28768a Expose Hook API signatures, keylet types and return codes in server_definitions (#884) 2026-10-11 15:26:25 +10:00
onledger.net d7f320bd17 fix(hook): sto_validate returns 0 for buffers under 2 bytes (#881) 2026-10-11 12:33:47 +10:00
onledger.net 22a94c4429 fix(base58): Read characters as unsigned char in b58_to_b256_be (#868) 2026-10-08 23:08:47 +10:00
Alphonse N. Mousse 42a33a224d Fix handling of emitted transactions: (#731) 2026-10-08 18:30:35 +10:00
Fomo ee33d825fc fix: NULL RWDB and mutex locks (#728) 2026-10-08 16:39:29 +10:00
onledger.net 9ef66a65b8 fix: ReportFeeChange data race, no-subscriber queuing, and LoadManager loop (#785) 2026-10-07 18:26:53 +10:00
Richard Holland 98fdaa4afa UNLReport Fix (#872) 2026-10-07 17:12:19 +10:00
tequ d9cc66afe9 ci: always run fixed 3-config matrix in nix workflow (#873) 2026-10-07 14:36:37 +10:00
183 changed files with 20817 additions and 1199 deletions

No files matched your search

+1 -1
View File
@@ -26,7 +26,7 @@ Loop: xrpld.app xrpld.nodestore
xrpld.app > xrpld.nodestore
Loop: xrpld.app xrpld.overlay
xrpld.overlay ~= xrpld.app
xrpld.overlay == xrpld.app
Loop: xrpld.app xrpld.peerfinder
xrpld.app > xrpld.peerfinder
+6
View File
@@ -12,6 +12,7 @@ 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
@@ -21,6 +22,7 @@ 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
@@ -96,10 +98,12 @@ test.overlay > test.toplevel
test.overlay > test.unit_test
test.overlay > xrpl.basics
test.overlay > xrpld.app
test.overlay > xrpld.core
test.overlay > xrpld.overlay
test.overlay > xrpld.peerfinder
test.overlay > xrpld.shamap
test.overlay > xrpl.protocol
test.overlay > xrpl.resource
test.peerfinder > test.beast
test.peerfinder > test.unit_test
test.peerfinder > xrpl.basics
@@ -123,6 +127,7 @@ test.rpc > xrpl.basics
test.rpc > xrpld.app
test.rpc > xrpld.core
test.rpc > xrpld.net
test.rpc > xrpld.nodestore
test.rpc > xrpld.overlay
test.rpc > xrpld.rpc
test.rpc > xrpld.shamap
@@ -191,6 +196,7 @@ xrpld.overlay > xrpl.basics
xrpld.overlay > xrpld.core
xrpld.overlay > xrpld.peerfinder
xrpld.overlay > xrpld.perflog
xrpld.overlay > xrpld.shamap
xrpld.overlay > xrpl.json
xrpld.overlay > xrpl.protocol
xrpld.overlay > xrpl.resource
+1 -1
View File
@@ -97,7 +97,7 @@ else
# GH Action, runner
if [[ "$(git rev-parse --abbrev-ref HEAD)" == "release" ]]; then
echo "building on the release branch... placing it in builds/candidate"
mkdir /data/builds/candidate
mkdir -p /data/builds/candidate
cp /io/release-build/xahaud /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
cp /io/release-build/release.info /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
else
+185
View File
@@ -166,10 +166,13 @@
# 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.
@@ -264,6 +267,119 @@
# "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>
@@ -448,6 +564,30 @@
#
#
#
# [udp_highway]
#
# 0 or 1.
#
# 0: Disable the Xahau UDP Superhighway (XUSH). [default]
# 1: Enable the Xahau UDP Superhighway (XUSH).
#
# The UDP Superhighway is a best-effort transport that runs alongside the
# peer protocol. When it is enabled the server also listens for UDP
# datagrams on its peer port number, and every transaction it relays to its
# peers is also sent, over UDP, to a small random set of other servers that
# have the highway enabled. It never replaces the peer protocol: anything
# sent over the highway is also relayed normally.
#
# To receive highway traffic, inbound UDP must be allowed to the peer port
# (in addition to inbound TCP).
#
# A server that keeps its address private (peer_private = 1, or any server
# configured as a validator) only exchanges highway traffic with the peers
# listed in [ips_fixed], and never advertises highway endpoints. Fixed peers
# are never advertised to other servers.
#
#
#
# [peers_max]
#
# The largest number of desired peer connections (incoming or outgoing).
@@ -674,6 +814,38 @@
#
#
#
# [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]
@@ -1416,6 +1588,19 @@
# 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
@@ -151,4 +151,3 @@ D686F2538F410C9D0D856788E98E3579595DAF7B38D38887F81ECAC934B06040 HooksUpdate1
4B8466415FAB32FFA89D9DCBE166A42340115771DF611A7160F8D7439C87ECD8 fixNSDelete
EDB4EE4C524E16BDD91D9A529332DED08DCAAA51CC6DC897ACFA1A0ED131C5B6 fix240819
8063140E9260799D6716756B891CEC3E7006C4E4F277AB84670663A88F94B9C4 fixPageCap
88693F108C3CD8A967F3F4253A32DEF5E35F9406ACD2A11B88B11D90865763A9 fix240911
+1
View File
@@ -97,6 +97,7 @@ 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,5 +47,6 @@
#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,6 +339,12 @@ 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
+23 -23
View File
@@ -22,29 +22,29 @@ APPLY_HOOK="$SCRIPT_DIR/../include/xrpl/hook/hook_api.macro"
return s;
}
{
line = $0;
# Skip block comments
if (line ~ /\/\*/) {
next;
}
# Look for comment lines that start with // and contain function signature
if (line ~ /^[[:space:]]*\/\/[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(/) {
# Remove leading // and trim
sub(/^[[:space:]]*\/\/[[:space:]]*/, "", line);
line = trim(line);
# Check if function name is "_g" to add attribute
if (line ~ /[[:space:]]+_g[[:space:]]*\(/) {
# Insert __attribute__((noduplicate)) before _g
sub(/[[:space:]]+_g/, " __attribute__((noduplicate)) _g", line);
}
# printf("\n");
printf("extern %s\n\n", line);
# Each entry is HOOK_API_DEFINITION(RET, NAME, (PARAMS), AMENDMENT),
# possibly spread over several lines. Collect until the parentheses
# balance, then split on the first two commas; the parameter tuple
# is the first parenthesised group.
function emit(s, i, ret, name, p, q, params) {
sub(/^[[:space:]]*HOOK_API_DEFINITION[[:space:]]*\(/, "", s);
gsub(/[[:space:]]+/, " ", s);
i = index(s, ","); ret = trim(substr(s, 1, i - 1)); s = substr(s, i + 1);
i = index(s, ","); name = trim(substr(s, 1, i - 1)); s = substr(s, i + 1);
p = index(s, "("); q = index(s, ")");
params = trim(substr(s, p + 1, q - p - 1));
if (name == "_g")
name = "__attribute__((noduplicate)) _g";
printf("extern %s %s(%s);\n\n", ret, name, params);
}
/^[[:space:]]*HOOK_API_DEFINITION[[:space:]]*\(/ { buf = ""; collecting = 1; }
collecting {
buf = buf " " $0;
if (gsub(/\(/, "(", buf) == gsub(/\)/, ")", buf)) {
collecting = 0;
emit(buf);
}
}
' "$APPLY_HOOK"
+1 -1
View File
@@ -46,8 +46,8 @@
#include "error.h"
#include "extern.h"
#include "sfcodes.h"
#include "macro.h"
#include "sfcodes.h"
#include "tts.h"
#include "ls_flags.h"
+8
View File
@@ -22,6 +22,8 @@
#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)
@@ -80,6 +82,7 @@
#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)
@@ -152,6 +155,8 @@
#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)
@@ -222,6 +227,7 @@
#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)
@@ -289,6 +295,7 @@
#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)
@@ -321,6 +328,7 @@
#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,6 +61,7 @@
#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
@@ -0,0 +1,105 @@
#pragma once
#include <shared_mutex>
// On Linux (glibc), std::shared_mutex wraps pthread_rwlock_t initialised
// with PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP. This means a
// pending exclusive lock() blocks new shared (reader) acquisitions,
// causing reader starvation when writers contend frequently.
//
// On macOS / ARM (libc++), std::shared_mutex is already reader-preferring,
// so the same code behaves differently across platforms.
//
// This header provides ReaderPreferringSharedMutex:
// - On Linux it wraps pthread_rwlock_t initialised with
// PTHREAD_RWLOCK_PREFER_READER_NP, matching macOS semantics.
// - On all other platforms it is a type alias for std::shared_mutex.
//
// The interface is identical to std::shared_mutex, so it works with
// std::shared_lock and std::unique_lock.
#if defined(__linux__)
#include <cerrno>
#include <pthread.h>
#include <system_error>
namespace ripple {
class ReaderPreferringSharedMutex
{
pthread_rwlock_t rwlock_;
public:
ReaderPreferringSharedMutex()
{
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_READER_NP);
int rc = pthread_rwlock_init(&rwlock_, &attr);
pthread_rwlockattr_destroy(&attr);
if (rc != 0)
throw std::system_error(
rc, std::system_category(), "pthread_rwlock_init");
}
~ReaderPreferringSharedMutex()
{
pthread_rwlock_destroy(&rwlock_);
}
ReaderPreferringSharedMutex(ReaderPreferringSharedMutex const&) = delete;
ReaderPreferringSharedMutex&
operator=(ReaderPreferringSharedMutex const&) = delete;
// Exclusive (writer) locking
void
lock()
{
pthread_rwlock_wrlock(&rwlock_);
}
bool
try_lock()
{
return pthread_rwlock_trywrlock(&rwlock_) == 0;
}
void
unlock()
{
pthread_rwlock_unlock(&rwlock_);
}
// Shared (reader) locking
void
lock_shared()
{
pthread_rwlock_rdlock(&rwlock_);
}
bool
try_lock_shared()
{
return pthread_rwlock_tryrdlock(&rwlock_) == 0;
}
void
unlock_shared()
{
pthread_rwlock_unlock(&rwlock_);
}
};
} // namespace ripple
#else // !__linux__
namespace ripple {
// macOS, Windows, etc. — std::shared_mutex is already reader-preferring.
using ReaderPreferringSharedMutex = std::shared_mutex;
} // namespace ripple
#endif
+3 -3
View File
@@ -77,7 +77,7 @@ strUnHex(std::size_t strSize, Iterator begin, Iterator end)
if (strSize & 1)
{
int c = unxtab[*iter++];
int c = unxtab[static_cast<unsigned char>(*iter++)];
if (c < 0)
return {};
@@ -87,12 +87,12 @@ strUnHex(std::size_t strSize, Iterator begin, Iterator end)
while (iter != end)
{
int cHigh = unxtab[*iter++];
int cHigh = unxtab[static_cast<unsigned char>(*iter++)];
if (cHigh < 0)
return {};
int cLow = unxtab[*iter++];
int cLow = unxtab[static_cast<unsigned char>(*iter++)];
if (cLow < 0)
return {};
+19 -13
View File
@@ -15,6 +15,7 @@
#define uint256 std::string
#define featureHooksUpdate1 "1"
#define featureHooksUpdate2 "1"
#define featureRNG "1"
#define fix20250131 "1"
#define fixGuardDepth32 "1"
namespace hook_api {
@@ -385,7 +386,9 @@ 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_MANY_NAMESPACES = -45,
TOO_LITTLE_ENTROPY = -46 // featureRNG: no seed for this ledger, or too few
// validators contributed to it
};
enum class ExitType : uint8_t {
@@ -398,12 +401,25 @@ 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;
using APIWhitelist = std::map<std::string, std::vector<uint8_t>>;
namespace detail {
template <class T>
constexpr uint8_t wasmTypeCode = sizeof(T) == 8 ? 0x7EU : 0x7FU;
template <class R, class... Args>
std::vector<uint8_t>
wasmSignature(R (*)(Args...))
{
return {wasmTypeCode<R>, wasmTypeCode<Args>...};
}
} // namespace detail
// RH NOTE: Find descriptions of api functions in ./impl/applyHook.cpp and
// hookapi.h (include for hooks) this is a map of the api name to its return
// code (vec[0] and its parameters vec[>0]) as wasm type codes
@@ -415,25 +431,15 @@ getImportWhitelist(Rules const& rules)
#pragma push_macro("HOOK_API_DEFINITION")
#undef HOOK_API_DEFINITION
#define int64_t 0x7EU
#define int32_t 0x7FU
#define uint32_t 0x7FU
#define HOOK_WRAP_PARAMS(...) __VA_ARGS__
#define HOOK_API_DEFINITION( \
RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, AMENDMENT) \
if (AMENDMENT == uint256{} || rules.enabled(AMENDMENT)) \
whitelist[#FUNCTION_NAME] = { \
RETURN_TYPE, HOOK_WRAP_PARAMS PARAMS_TUPLE};
whitelist[#FUNCTION_NAME] = \
detail::wasmSignature((RETURN_TYPE(*) PARAMS_TUPLE) nullptr);
#include "hook_api.macro"
#undef HOOK_API_DEFINITION
#undef HOOK_WRAP_PARAMS
#undef int64_t
#undef int32_t
#undef uint32_t
#pragma pop_macro("HOOK_API_DEFINITION")
return whitelist;
+5 -1
View File
@@ -1096,7 +1096,11 @@ validateGuards(
{
int name_len = parseLeb128(wasm, i, &i);
CHECK_SHORT_HOOK();
if (name_len == 4)
// 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 (wasm[i] == 'h' && wasm[i + 1] == 'o' &&
wasm[i + 2] == 'o' && wasm[i + 3] == 'k')
+74 -139
View File
@@ -1,374 +1,309 @@
// int32_t _g(uint32_t guard_id, uint32_t maxiter);
// clang-format off
HOOK_API_DEFINITION(
int32_t, _g, (uint32_t, uint32_t),
int32_t, _g, (uint32_t guard_id, uint32_t maxiter),
uint256{})
// int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
HOOK_API_DEFINITION(
int64_t, accept, (uint32_t, uint32_t, int64_t),
int64_t, accept, (uint32_t read_ptr, uint32_t read_len, int64_t error_code),
uint256{})
// int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
HOOK_API_DEFINITION(
int64_t, rollback, (uint32_t, uint32_t, int64_t),
int64_t, rollback, (uint32_t read_ptr, uint32_t read_len, int64_t error_code),
uint256{})
// int64_t util_raddr(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, util_raddr, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, util_raddr, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t util_accid(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, util_accid, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, util_accid, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t util_verify(uint32_t dread_ptr, uint32_t dread_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t kread_ptr, uint32_t kread_len);
HOOK_API_DEFINITION(
int64_t, util_verify, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, util_verify, (uint32_t dread_ptr, uint32_t dread_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t kread_ptr, uint32_t kread_len),
uint256{})
// int64_t util_sha512h(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, util_sha512h, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, util_sha512h, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t util_keylet(uint32_t write_ptr, uint32_t write_len, uint32_t keylet_type, uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f);
HOOK_API_DEFINITION(
int64_t, util_keylet, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, util_keylet, (uint32_t write_ptr, uint32_t write_len, uint32_t keylet_type, uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f),
uint256{})
// int64_t sto_validate(uint32_t tread_ptr, uint32_t tread_len);
HOOK_API_DEFINITION(
int64_t, sto_validate, (uint32_t, uint32_t),
int64_t, sto_validate, (uint32_t tread_ptr, uint32_t tread_len),
uint256{})
// int64_t sto_subfield(uint32_t read_ptr, uint32_t read_len, uint32_t field_id);
HOOK_API_DEFINITION(
int64_t, sto_subfield, (uint32_t, uint32_t, uint32_t),
int64_t, sto_subfield, (uint32_t read_ptr, uint32_t read_len, uint32_t field_id),
uint256{})
// int64_t sto_subarray(uint32_t read_ptr, uint32_t read_len, uint32_t array_id);
HOOK_API_DEFINITION(
int64_t, sto_subarray, (uint32_t, uint32_t, uint32_t),
int64_t, sto_subarray, (uint32_t read_ptr, uint32_t read_len, uint32_t array_id),
uint256{})
// int64_t sto_emplace(uint32_t write_ptr, uint32_t write_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t fread_ptr, uint32_t fread_len, uint32_t field_id);
HOOK_API_DEFINITION(
int64_t, sto_emplace, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, sto_emplace, (uint32_t write_ptr, uint32_t write_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t fread_ptr, uint32_t fread_len, uint32_t field_id),
uint256{})
// int64_t sto_erase(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t field_id);
HOOK_API_DEFINITION(
int64_t, sto_erase, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, sto_erase, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t field_id),
uint256{})
// int64_t etxn_burden();
HOOK_API_DEFINITION(
int64_t, etxn_burden, (),
uint256{})
// int64_t etxn_details(uint32_t write_ptr, uint32_t write_len);
HOOK_API_DEFINITION(
int64_t, etxn_details, (uint32_t, uint32_t),
int64_t, etxn_details, (uint32_t write_ptr, uint32_t write_len),
uint256{})
// int64_t etxn_fee_base(uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, etxn_fee_base, (uint32_t, uint32_t),
int64_t, etxn_fee_base, (uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t etxn_reserve(uint32_t count);
HOOK_API_DEFINITION(
int64_t, etxn_reserve, (uint32_t),
int64_t, etxn_reserve, (uint32_t count),
uint256{})
// int64_t etxn_generation();
HOOK_API_DEFINITION(
int64_t, etxn_generation, (),
uint256{})
// int64_t etxn_nonce(uint32_t write_ptr, uint32_t write_len);
HOOK_API_DEFINITION(
int64_t, etxn_nonce, (uint32_t, uint32_t),
int64_t, etxn_nonce, (uint32_t write_ptr, uint32_t write_len),
uint256{})
// int64_t emit(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, emit, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, emit, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t float_set(int32_t exponent, int64_t mantissa);
HOOK_API_DEFINITION(
int64_t, float_set, (int32_t, int64_t),
int64_t, float_set, (int32_t exponent, int64_t mantissa),
uint256{})
// int64_t float_multiply(int64_t float1, int64_t float2);
HOOK_API_DEFINITION(
int64_t, float_multiply, (int64_t, int64_t),
int64_t, float_multiply, (int64_t float1, int64_t float2),
uint256{})
// int64_t float_mulratio(int64_t float1, uint32_t round_up, uint32_t numerator, uint32_t denominator);
HOOK_API_DEFINITION(
int64_t, float_mulratio, (int64_t, uint32_t, uint32_t, uint32_t),
int64_t, float_mulratio, (int64_t float1, uint32_t round_up, uint32_t numerator, uint32_t denominator),
uint256{})
// int64_t float_negate(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_negate, (int64_t),
int64_t, float_negate, (int64_t float1),
uint256{})
// int64_t float_compare(int64_t float1, int64_t float2, uint32_t mode);
HOOK_API_DEFINITION(
int64_t, float_compare, (int64_t, int64_t, uint32_t),
int64_t, float_compare, (int64_t float1, int64_t float2, uint32_t mode),
uint256{})
// int64_t float_sum(int64_t float1, int64_t float2);
HOOK_API_DEFINITION(
int64_t, float_sum, (int64_t, int64_t),
int64_t, float_sum, (int64_t float1, int64_t float2),
uint256{})
// int64_t float_sto(uint32_t write_ptr, uint32_t write_len, uint32_t cread_ptr, uint32_t cread_len, uint32_t iread_ptr, uint32_t iread_len, int64_t float1, uint32_t field_code);
HOOK_API_DEFINITION(
int64_t, float_sto, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, int64_t, uint32_t),
int64_t, float_sto, (uint32_t write_ptr, uint32_t write_len, uint32_t cread_ptr, uint32_t cread_len, uint32_t iread_ptr, uint32_t iread_len, int64_t float1, uint32_t field_code),
uint256{})
// int64_t float_sto_set(uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, float_sto_set, (uint32_t, uint32_t),
int64_t, float_sto_set, (uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t float_invert(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_invert, (int64_t),
int64_t, float_invert, (int64_t float1),
uint256{})
// int64_t float_divide(int64_t float1, int64_t float2);
HOOK_API_DEFINITION(
int64_t, float_divide, (int64_t, int64_t),
int64_t, float_divide, (int64_t float1, int64_t float2),
uint256{})
// int64_t float_one();
HOOK_API_DEFINITION(
int64_t, float_one, (),
uint256{})
// int64_t float_mantissa(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_mantissa, (int64_t),
int64_t, float_mantissa, (int64_t float1),
uint256{})
// int64_t float_sign(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_sign, (int64_t),
int64_t, float_sign, (int64_t float1),
uint256{})
// int64_t float_int(int64_t float1, uint32_t decimal_places, uint32_t abs);
HOOK_API_DEFINITION(
int64_t, float_int, (int64_t, uint32_t, uint32_t),
int64_t, float_int, (int64_t float1, uint32_t decimal_places, uint32_t abs),
uint256{})
// int64_t float_log(int64_t float1);
HOOK_API_DEFINITION(
int64_t, float_log, (int64_t),
int64_t, float_log, (int64_t float1),
uint256{})
// int64_t float_root(int64_t float1, uint32_t n);
HOOK_API_DEFINITION(
int64_t, float_root, (int64_t, uint32_t),
int64_t, float_root, (int64_t float1, uint32_t n),
uint256{})
// int64_t fee_base();
HOOK_API_DEFINITION(
int64_t, fee_base, (),
uint256{})
// int64_t ledger_seq();
HOOK_API_DEFINITION(
int64_t, ledger_seq, (),
uint256{})
// int64_t ledger_last_time();
HOOK_API_DEFINITION(
int64_t, ledger_last_time, (),
uint256{})
// int64_t ledger_last_hash(uint32_t write_ptr, uint32_t write_len);
HOOK_API_DEFINITION(
int64_t, ledger_last_hash, (uint32_t, uint32_t),
int64_t, ledger_last_hash, (uint32_t write_ptr, uint32_t write_len),
uint256{})
// int64_t ledger_nonce(uint32_t write_ptr, uint32_t write_len);
HOOK_API_DEFINITION(
int64_t, ledger_nonce, (uint32_t, uint32_t),
int64_t, ledger_nonce, (uint32_t write_ptr, uint32_t write_len),
uint256{})
// int64_t ledger_keylet(uint32_t write_ptr, uint32_t write_len, uint32_t lread_ptr, uint32_t lread_len, uint32_t hread_ptr, uint32_t hread_len);
HOOK_API_DEFINITION(
int64_t, ledger_keylet, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, ledger_keylet, (uint32_t write_ptr, uint32_t write_len, uint32_t lread_ptr, uint32_t lread_len, uint32_t hread_ptr, uint32_t hread_len),
uint256{})
// int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
HOOK_API_DEFINITION(
int64_t, hook_account, (uint32_t, uint32_t),
int64_t, hook_account, (uint32_t write_ptr, uint32_t write_len),
uint256{})
// int64_t hook_hash(uint32_t write_ptr, uint32_t write_len, int32_t hook_no);
HOOK_API_DEFINITION(
int64_t, hook_hash, (uint32_t, uint32_t, int32_t),
int64_t, hook_hash, (uint32_t write_ptr, uint32_t write_len, int32_t hook_no),
uint256{})
// int64_t hook_param_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t hread_ptr, uint32_t hread_len);
HOOK_API_DEFINITION(
int64_t, hook_param_set, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, hook_param_set, (uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t hread_ptr, uint32_t hread_len),
uint256{})
// int64_t hook_param(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, hook_param, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, hook_param, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t hook_again();
HOOK_API_DEFINITION(
int64_t, hook_again, (),
uint256{})
// int64_t hook_skip(uint32_t read_ptr, uint32_t read_len, uint32_t flags);
HOOK_API_DEFINITION(
int64_t, hook_skip, (uint32_t, uint32_t, uint32_t),
int64_t, hook_skip, (uint32_t read_ptr, uint32_t read_len, uint32_t flags),
uint256{})
// int64_t hook_pos();
HOOK_API_DEFINITION(
int64_t, hook_pos, (),
uint256{})
// int64_t slot(uint32_t write_ptr, uint32_t write_len, uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot, (uint32_t, uint32_t, uint32_t),
int64_t, slot, (uint32_t write_ptr, uint32_t write_len, uint32_t slot),
uint256{})
// int64_t slot_clear(uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot_clear, (uint32_t),
int64_t, slot_clear, (uint32_t slot),
uint256{})
// int64_t slot_count(uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot_count, (uint32_t),
int64_t, slot_count, (uint32_t slot),
uint256{})
// int64_t slot_set(uint32_t read_ptr, uint32_t read_len, uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot_set, (uint32_t, uint32_t, uint32_t),
int64_t, slot_set, (uint32_t read_ptr, uint32_t read_len, uint32_t slot),
uint256{})
// int64_t slot_size(uint32_t slot);
HOOK_API_DEFINITION(
int64_t, slot_size, (uint32_t),
int64_t, slot_size, (uint32_t slot),
uint256{})
// int64_t slot_subarray(uint32_t parent_slot, uint32_t array_id, uint32_t new_slot);
HOOK_API_DEFINITION(
int64_t, slot_subarray, (uint32_t, uint32_t, uint32_t),
int64_t, slot_subarray, (uint32_t parent_slot, uint32_t array_id, uint32_t new_slot),
uint256{})
// int64_t slot_subfield(uint32_t parent_slot, uint32_t field_id, uint32_t new_slot);
HOOK_API_DEFINITION(
int64_t, slot_subfield, (uint32_t, uint32_t, uint32_t),
int64_t, slot_subfield, (uint32_t parent_slot, uint32_t field_id, uint32_t new_slot),
uint256{})
// int64_t slot_type(uint32_t slot_no, uint32_t flags);
HOOK_API_DEFINITION(
int64_t, slot_type, (uint32_t, uint32_t),
int64_t, slot_type, (uint32_t slot_no, uint32_t flags),
uint256{})
// int64_t slot_float(uint32_t slot_no);
HOOK_API_DEFINITION(
int64_t, slot_float, (uint32_t),
int64_t, slot_float, (uint32_t slot_no),
uint256{})
// int64_t state_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len);
HOOK_API_DEFINITION(
int64_t, state_set, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, state_set, (uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len),
uint256{})
// int64_t state_foreign_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len);
HOOK_API_DEFINITION(
int64_t, state_foreign_set, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, state_foreign_set, (uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len),
uint256{})
// int64_t state(uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len);
HOOK_API_DEFINITION(
int64_t, state, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, state, (uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len),
uint256{})
// int64_t state_foreign(uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len);
HOOK_API_DEFINITION(
int64_t, state_foreign, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, state_foreign, (uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len),
uint256{})
// int64_t trace(uint32_t mread_ptr, uint32_t mread_len, uint32_t dread_ptr, uint32_t dread_len, uint32_t as_hex);
HOOK_API_DEFINITION(
int64_t, trace, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, trace, (uint32_t mread_ptr, uint32_t mread_len, uint32_t dread_ptr, uint32_t dread_len, uint32_t as_hex),
uint256{})
// int64_t trace_num(uint32_t read_ptr, uint32_t read_len, int64_t number);
HOOK_API_DEFINITION(
int64_t, trace_num, (uint32_t, uint32_t, int64_t),
int64_t, trace_num, (uint32_t read_ptr, uint32_t read_len, int64_t number),
uint256{})
// int64_t trace_float(uint32_t read_ptr, uint32_t read_len, int64_t float1);
HOOK_API_DEFINITION(
int64_t, trace_float, (uint32_t, uint32_t, int64_t),
int64_t, trace_float, (uint32_t read_ptr, uint32_t read_len, int64_t float1),
uint256{})
// int64_t otxn_burden();
HOOK_API_DEFINITION(
int64_t, otxn_burden, (),
uint256{})
// int64_t otxn_field(uint32_t write_ptr, uint32_t write_len, uint32_t field_id);
HOOK_API_DEFINITION(
int64_t, otxn_field, (uint32_t, uint32_t, uint32_t),
int64_t, otxn_field, (uint32_t write_ptr, uint32_t write_len, uint32_t field_id),
uint256{})
// int64_t otxn_generation();
HOOK_API_DEFINITION(
int64_t, otxn_generation, (),
uint256{})
// int64_t otxn_id(uint32_t write_ptr, uint32_t write_len, uint32_t flags);
HOOK_API_DEFINITION(
int64_t, otxn_id, (uint32_t, uint32_t, uint32_t),
int64_t, otxn_id, (uint32_t write_ptr, uint32_t write_len, uint32_t flags),
uint256{})
// int64_t otxn_type();
HOOK_API_DEFINITION(
int64_t, otxn_type, (),
uint256{})
// int64_t otxn_slot(uint32_t slot_no);
HOOK_API_DEFINITION(
int64_t, otxn_slot, (uint32_t),
int64_t, otxn_slot, (uint32_t slot_no),
uint256{})
// int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, otxn_param, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, otxn_param, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
uint256{})
// int64_t meta_slot(uint32_t slot_no);
HOOK_API_DEFINITION(
int64_t, meta_slot, (uint32_t),
int64_t, meta_slot, (uint32_t slot_no),
uint256{})
// int64_t xpop_slot(uint32_t slot_no_tx, uint32_t slot_no_meta);
HOOK_API_DEFINITION(
int64_t, xpop_slot, (uint32_t, uint32_t),
int64_t, xpop_slot, (uint32_t slot_no_tx, uint32_t slot_no_meta),
featureHooksUpdate1)
// int64_t prepare(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
HOOK_API_DEFINITION(
int64_t, prepare, (uint32_t, uint32_t, uint32_t, uint32_t),
int64_t, prepare, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
featureHooksUpdate2)
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
@@ -0,0 +1,66 @@
//------------------------------------------------------------------------------
/*
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
+25 -2
View File
@@ -118,6 +118,10 @@ 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
{
@@ -328,6 +332,14 @@ 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;
@@ -376,6 +388,16 @@ 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:
@@ -406,7 +428,7 @@ struct keyletDesc
// This list should include all of the keylet functions that take a single
// AccountID parameter.
std::array<keyletDesc<AccountID const&>, 6> const directAccountKeylets{
std::array<keyletDesc<AccountID const&>, 7> const directAccountKeylets{
{{&keylet::account, jss::AccountRoot, false},
{&keylet::ownerDir, jss::DirectoryNode, true},
{&keylet::signers, jss::SignerList, true},
@@ -414,7 +436,8 @@ std::array<keyletDesc<AccountID const&>, 6> 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::did, jss::DID, true},
{&keylet::appLoader, jss::AppLoader, true}}};
MPTID
makeMptID(std::uint32_t sequence, AccountID const& account);
+19
View File
@@ -96,6 +96,25 @@ 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,6 +197,7 @@ enum TEFcodes : TERUnderlyingType {
tefINVALID_LEDGER_FIX_TYPE,
tefPAST_MANIFEST_SEQ,
tefREVOKED_MANIFEST,
tefMANIFEST_KEY_IN_USE,
};
//------------------------------------------------------------------------------
+21
View File
@@ -152,6 +152,26 @@ 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
{
@@ -176,6 +196,7 @@ private:
std::optional<STAmount> mDelivered;
std::optional<STArray> mHookExecutions;
std::optional<STArray> mHookEmissions;
std::optional<uint256> mRandomData;
STArray mNodes;
};
+8 -2
View File
@@ -34,9 +34,15 @@
// If you add an amendment here, then do not forget to increment `numFeatures`
// in include/xrpl/protocol/Feature.h.
// Xahau testnet (NetworkID 21338) only. Registered as unsupported here; the
// server promotes it to Supported::yes / DefaultYes at startup if and only
// if it is configured with network_id 21338 (see Application.cpp).
XRPL_FIX (XahauTestnetGenesis, Supported::no, VoteBehavior::DefaultNo)
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)
XRPL_FIX (20260929, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(HookOnV2_1, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(OnChainManifests, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
@@ -88,7 +94,6 @@ XRPL_FIX (ReduceImport, Supported::yes, VoteBehavior::DefaultYe
XRPL_FEATURE(Touch, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(Remarks, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FIX (FloatDivide, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (240911, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (PageCap, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (240819, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (NSDelete, Supported::yes, VoteBehavior::DefaultNo)
@@ -164,6 +169,7 @@ XRPL_FEATURE(CryptoConditionsSuite, Supported::yes, VoteBehavior::Obsolete)
// pre-amendment code has been removed and the identifiers are deprecated.
// All known amendments and amendments that may appear in a validated
// ledger must be registered either here or above with the "active" amendments
XRPL_RETIRE(fix240911)
XRPL_RETIRE(fixXahauV2)
XRPL_RETIRE(fixXahauV1)
XRPL_RETIRE(MultiSign)
@@ -142,6 +142,22 @@ 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.
@@ -180,6 +196,26 @@ 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
@@ -262,6 +298,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
{sfHookStateScale, soeOPTIONAL},
{sfCron, soeOPTIONAL},
{sfAMMID, soeOPTIONAL},
{sfAppLoaderSequence, soeOPTIONAL},
{sfManifestID, soeOPTIONAL},
}))
@@ -609,6 +646,7 @@ 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,6 +59,8 @@ 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)
@@ -123,6 +125,7 @@ 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)
@@ -210,6 +213,9 @@ 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)
@@ -294,6 +300,7 @@ 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)
@@ -383,6 +390,7 @@ 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)
@@ -423,6 +431,8 @@ 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,6 +74,7 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, ({
{sfTickSize, soeOPTIONAL},
{sfNFTokenMinter, soeOPTIONAL},
{sfHookStateScale, soeOPTIONAL},
{sfAppLoader, soeOPTIONAL},
}))
/** This transaction type cancels an existing escrow. */
@@ -500,6 +501,22 @@ 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, ({
+13 -2
View File
@@ -47,6 +47,7 @@ JSS(AL_hit_rate); // out: GetCounts
JSS(AcceptedCredentials); // out: AccountObjects
JSS(Account); // in: TransactionSign; field.
JSS(AMMID); // field
JSS(API); // out: RPC server_definitions (HOOK)
JSS(Amount); // in: TransactionSign; field.
JSS(Authorize); // field
JSS(Amount2); // in/out: AMM IOU/XRP pool, deposit, withdraw amount
@@ -68,6 +69,9 @@ JSS(Destination); // in: TransactionSign; field.
JSS(EPrice); // in: AMM Deposit option
JSS(Fee); // in/out: TransactionSign; field.
JSS(FIELDS); // out: RPC server_definitions
JSS(HOOK); // out: RPC server_definitions
JSS(KEYLET_TYPES); // out: RPC server_definitions (HOOK)
JSS(RETURN_CODES); // out: RPC server_definitions (HOOK)
JSS(Flags); // in/out: TransactionSign; field.
JSS(GenesisMints);
JSS(GovernanceMarks);
@@ -167,6 +171,7 @@ JSS(amendment_blocked); // out: NetworkOPs
JSS(amm_account); // in: amm_info
JSS(amount); // out: AccountChannels, amm_info
JSS(amount2); // out: amm_info
JSS(anchored_through); // out: CatalogueLoad
JSS(api_version); // in: many, out: Version
JSS(api_version_low); // out: Version
JSS(applied); // out: SubmitTransaction
@@ -234,7 +239,7 @@ JSS(closed); // out: NetworkOPs, LedgerToJson,
// handlers/Ledger
JSS(closed_ledger); // out: NetworkOPs
JSS(cluster); // out: PeerImp
JSS(code); // out: errors
JSS(code); // in: HookValidate; out: errors
JSS(command); // in: RPCHandler
JSS(complete); // out: NetworkOPs, InboundLedger
JSS(complete_ledgers); // out: NetworkOPs, PeerImp
@@ -374,6 +379,8 @@ 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
@@ -447,6 +454,7 @@ 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
@@ -585,7 +593,6 @@ 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
@@ -603,6 +610,7 @@ 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
@@ -737,6 +745,7 @@ JSS(TRANSACTION_FLAGS_INDICES); // out: RPC server_definitions
JSS(type_hex); // out: STPathSet
JSS(unl); // out: UnlList
JSS(unlimited); // out: Connection.h
JSS(unpinned); // out: CatalogueUnpin
JSS(uptime); // out: GetCounts
JSS(uri); // out: ValidatorSites
JSS(url); // in/out: Subscribe, Unsubscribe
@@ -744,6 +753,7 @@ 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
@@ -765,6 +775,7 @@ 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
+20
View File
@@ -22,6 +22,7 @@
#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>
@@ -57,6 +58,12 @@ 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;
@@ -93,8 +100,18 @@ struct Port
return protocol.count("udp") > 0;
}
bool
has_peer() const
{
return protocol.count("peer") > 0;
}
// Maximum UDP packet size (default 64KB)
std::size_t udp_packet_size = 65536;
// If this is a peer port, also listen for UDP Superhighway (XUSH)
// datagrams on the same port number.
bool udp_highway = false;
};
std::ostream&
@@ -126,6 +143,9 @@ 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("ws2") || port_.protocol.count("pwa"))
{
reOpen();
}
+10
View File
@@ -208,6 +208,16 @@ ServerImpl<Handler>::ports(std::vector<Port> const& ports)
sp->run();
}
// UDP Superhighway (XUSH) datagrams arrive on the peer port
// number. TCP and UDP port numbers are independent, so this
// needs no socket sharing with the TCP door.
if (internalPort.has_peer() && internalPort.udp_highway)
{
if (auto sp = ios_.emplace<UDPDoor<Handler>>(
handler_, io_context_, internalPort, j_))
sp->run();
}
}
}
return eps;
+10 -5
View File
@@ -78,10 +78,9 @@ public:
error_code ec;
// Create UDP endpoint from port configuration
auto const addr = port_.ip.to_v4();
boost::asio::ip::udp::endpoint udp_endpoint(addr, port_.port);
boost::asio::ip::udp::endpoint udp_endpoint(port_.ip, port_.port);
socket_.open(boost::asio::ip::udp::v4(), ec);
socket_.open(udp_endpoint.protocol(), ec);
if (ec)
{
JLOG(j_.error()) << "UDP socket open failed: " << ec.message();
@@ -94,17 +93,22 @@ public:
{
JLOG(j_.error())
<< "UDP set reuse_address failed: " << ec.message();
close();
return;
}
socket_.bind(udp_endpoint, ec);
if (ec)
{
JLOG(j_.error()) << "UDP socket bind failed: " << ec.message();
// Close the socket so that run() does not start receiving on an
// unbound socket, which would silently bind an ephemeral port.
JLOG(j_.error()) << "UDP socket bind to " << udp_endpoint
<< " failed: " << ec.message();
close();
return;
}
JLOG(j_.info()) << "UDP-RPC listening on " << udp_endpoint;
JLOG(j_.info()) << "UDP listening on " << udp_endpoint;
}
endpoint_type
@@ -169,6 +173,7 @@ private:
handler_.onUDPMessage(
std::string(recv_buffer_.data(), bytes_transferred),
tcp_endpoint,
port_,
[this, tcp_endpoint](std::string const& response) {
do_send(response, tcp_endpoint);
});
+180
View File
@@ -0,0 +1,180 @@
//------------------------------------------------------------------------------
/*
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,6 +81,7 @@ 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',
@@ -90,8 +91,10 @@ 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.
@@ -276,6 +279,13 @@ 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
{
@@ -613,6 +623,12 @@ 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
{
@@ -677,6 +693,14 @@ 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,6 +143,14 @@ InnerObjectFormats::InnerObjectFormats()
{sfAccount, soeOPTIONAL},
});
add(sfRandomDigestEntry.jsonName,
sfRandomDigestEntry.getCode(),
{
{sfPublicKey, soeREQUIRED},
{sfNextRandomDigest, soeREQUIRED},
{sfLedgerSequence, soeREQUIRED},
});
add(sfAmountEntry.jsonName,
sfAmountEntry.getCode(),
{
+2 -1
View File
@@ -684,7 +684,8 @@ 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 == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON ||
tt == ttENTROPY;
}
} // namespace ripple
+1
View File
@@ -153,6 +153,7 @@ 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,6 +49,9 @@ 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)
@@ -75,6 +78,9 @@ 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)
@@ -245,6 +251,9 @@ TxMeta::getAsObject() const
if (hasHookEmissions())
metaData.setFieldArray(sfHookEmissions, getHookEmissions());
if (hasRandomData())
metaData.setFieldH256(sfRandomData, getRandomData());
return metaData;
}
+8 -2
View File
@@ -545,7 +545,7 @@ b58_to_b256_be(std::string_view input, std::span<std::uint8_t> out)
XRPL_ASSERT(
num_b_58_10_coeffs <= b_58_10_coeff.size(),
"ripple::b58_fast::detail::b58_to_b256_be : maximum coeff");
for (auto c : input.substr(0, partial_coeff_len))
for (unsigned char const c : input.substr(0, partial_coeff_len))
{
auto cur_val = ::ripple::alphabetReverse[c];
if (cur_val < 0)
@@ -559,7 +559,7 @@ b58_to_b256_be(std::string_view input, std::span<std::uint8_t> out)
{
for (int j = 0; j < num_full_coeffs; ++j)
{
auto c = input[partial_coeff_len + j * 10 + i];
unsigned char const c = input[partial_coeff_len + j * 10 + i];
auto cur_val = ::ripple::alphabetReverse[c];
if (cur_val < 0)
{
@@ -580,6 +580,12 @@ b58_to_b256_be(std::string_view input, std::span<std::uint8_t> out)
{
std::uint64_t const c = b_58_10_coeff[i];
// Each digit is < 58, so at most 4 words are in use here and the
// span below always fits `result`. Guard the invariant anyway rather
// than letting a violation index past the end of the stack array.
if (cur_result_size >= result.size())
return Unexpected(TokenCodecErrc::inputTooLarge);
{
auto code = ripple::b58_fast::detail::inplace_bigint_mul(
std::span(&result[0], cur_result_size + 1), B_58_10);
+69
View File
@@ -21,7 +21,10 @@
#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>
@@ -66,6 +69,14 @@ 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:";
@@ -311,6 +322,64 @@ 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);
+19
View File
@@ -578,6 +578,25 @@ public:
ter(tecNO_TARGET));
env.close();
}
// HookName removed with a HookName-only update
// The sfHooks entry keeps an STI_NOTPRESENT placeholder for
// sfHookName, which preclaim must treat as "no name".
{
Json::Value jv;
jv[jss::Account] = issuer.human();
jv[jss::TransactionType] = jss::SetHook;
jv[jss::Flags] = 0;
jv[jss::Hooks] = Json::Value{Json::arrayValue};
jv[jss::Hooks][0U][jss::Hook][jss::HookName] = "";
env(jv, fee(XRP(1)));
env.close();
env(reward::claim(alice),
reward::issuer(issuer),
fee(XRP(1)),
ter(tesSUCCESS));
env.close();
}
// Vaild HookOn
{
auto hookObj = hso(jtx::genesis::AcceptHook, overrideFlag);
+443 -6
View File
@@ -590,6 +590,134 @@ public:
}
}
void
test_emitted_txn_entry(FeatureBitset features)
{
testcase("Test emitted txn ledger entry");
using namespace jtx;
using namespace hook;
using namespace hook_api;
auto const alice = Account{"alice"};
for (bool const withFix : {true, false})
{
Env env{*this, withFix ? features : features - fix20261005};
// emissions are only written into non-open views, so apply on
// top of the last closed ledger the way consensus does
auto const closed = env.closed();
OpenView ov{&*closed};
BEAST_EXPECT(!ov.open());
STTx const invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
ApplyContext applyCtx{
env.app(),
ov,
invokeTx,
tesSUCCESS,
env.current()->fees().base,
tapNONE,
env.journal};
STTx const emitTx = STTx(ttINVOKE, [&](STObject& obj) {
obj[sfAccount] = alice.id();
obj[sfSequence] = 0;
obj[sfSigningPubKey] = Slice{};
obj[sfFirstLedgerSequence] = closed->seq() + 1;
obj[sfLastLedgerSequence] = closed->seq() + 5;
obj[sfFee] = closed->fees().base;
auto& emitDetails = obj.peekFieldObject(sfEmitDetails);
emitDetails[sfEmitGeneration] = 1;
emitDetails[sfEmitBurden] = 1;
emitDetails[sfEmitParentTxnID] = invokeTx.getTransactionID();
emitDetails[sfEmitNonce] = uint256();
emitDetails[sfEmitHookHash] = uint256();
});
std::string reason;
auto const emitted = std::make_shared<ripple::Transaction>(
std::make_shared<STTx const>(emitTx), reason, env.app());
std::queue<std::shared_ptr<ripple::Transaction>> emittedTxn;
emittedTxn.push(emitted);
auto hookCtx = makeStubHookContext(
applyCtx,
alice.id(),
alice.id(),
{.result = {.emittedTxn = emittedTxn}});
BEAST_EXPECT(
hook::finalizeHookResult(hookCtx.result, applyCtx, true) ==
tesSUCCESS);
auto const kl = keylet::emittedTxn(emitted->getID());
auto const sle = applyCtx.view().peek(kl);
if (!BEAST_EXPECT(sle))
continue;
Serializer expected;
emitTx.add(expected);
// in memory: with the fix sfEmittedTxn occupies its template
// slot exactly once, without it a duplicate is appended behind
// the (non-present) template slot and the field reads absent
auto const entries =
std::count_if(sle->begin(), sle->end(), [](STBase const& f) {
return f.getFName() == sfEmittedTxn;
});
BEAST_EXPECT(entries == (withFix ? 1 : 2));
BEAST_EXPECT(sle->isFieldPresent(sfEmittedTxn) == withFix);
// serialized: identical either way, re-reading the entry always
// yields exactly the emitted transaction
{
Serializer s;
sle->add(s);
SerialIter sit(s.slice());
STLedgerEntry const reread(sit, kl.key);
BEAST_EXPECT(reread.isFieldPresent(sfEmittedTxn));
Serializer txn;
reread.peekAtField(sfEmittedTxn).add(txn);
BEAST_EXPECT(txn.peekData() == expected.peekData());
}
// a hook running later in the same ledger can tell them apart:
// slot_subfield only finds sfEmittedTxn with the fix
auto readerCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = readerCtx.api();
Bytes klBytes{
static_cast<uint8_t>((kl.type >> 8) & 0xFFU),
static_cast<uint8_t>(kl.type & 0xFFU)};
klBytes.insert(klBytes.end(), kl.key.begin(), kl.key.end());
auto const slotNo = api.slot_set(klBytes, 0);
if (!BEAST_EXPECT(slotNo.has_value()))
continue;
auto const sub =
api.slot_subfield(slotNo.value(), sfEmittedTxn.getCode(), 0);
if (!withFix)
{
BEAST_EXPECT(!sub.has_value() && sub.error() == DOESNT_EXIST);
continue;
}
if (!BEAST_EXPECT(sub.has_value()))
continue;
auto const field = api.slot(sub.value());
if (!BEAST_EXPECT(field.has_value()))
continue;
Serializer s;
field.value()->add(s);
BEAST_EXPECT(s.peekData() == expected.peekData());
}
}
void
test_etxn_details(FeatureBitset features)
{
@@ -1828,11 +1956,31 @@ public:
BEAST_EXPECT(api.float_set(-50, 0).value() == 0);
BEAST_EXPECT(api.float_set(0, 0).value() == 0);
// an exponent lower than -96 should produce an invalid float error
BEAST_EXPECT(api.float_set(-97, 1).error() == INVALID_FLOAT);
// fix20261005: exponent out of range is reported as
// EXPONENT_UNDERSIZED / EXPONENT_OVERSIZED; before the fix both
// cases were reported as INVALID_FLOAT
bool const fixExpCodes = env.current()->rules().enabled(fix20261005);
auto const undersized =
fixExpCodes ? EXPONENT_UNDERSIZED : INVALID_FLOAT;
auto const oversized = fixExpCodes ? EXPONENT_OVERSIZED : INVALID_FLOAT;
// an exponent larger than +96 should produce an invalid float error
BEAST_EXPECT(api.float_set(+97, 1).error() == INVALID_FLOAT);
// an exponent lower than -96 should produce an underflow error
BEAST_EXPECT(api.float_set(-97, 1).error() == undersized);
// an exponent larger than +96 should produce an overflow error
BEAST_EXPECT(api.float_set(+97, 1).error() == oversized);
// the -96..80 range applies to the exponent after the mantissa has
// been normalized to 16 digits (1 -> 1e15, exponent - 15)
auto const oneE15 = [](int32_t exp) {
return hook::hook_float::make_float(1000000000000000ULL, exp, false)
.value();
};
BEAST_EXPECT(api.float_set(81, 1).value() == oneE15(66));
BEAST_EXPECT(api.float_set(95, 1).value() == oneE15(80));
BEAST_EXPECT(api.float_set(96, 1).error() == oversized);
BEAST_EXPECT(api.float_set(-81, 1).value() == oneE15(-96));
BEAST_EXPECT(api.float_set(-82, 1).error() == undersized);
// clang-format off
std::vector<std::tuple<int32_t, int64_t, uint64_t>> tests = {
@@ -2895,6 +3043,91 @@ public:
BEAST_EXPECT(acct->value() == alice.id());
}
void
test_otxn_field_emit_failure(FeatureBitset features)
{
testcase("Test otxn_field on emit failure callback");
using namespace jtx;
using namespace hook_api;
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
// the cleanup pseudo carries sfLedgerSequence / sfTransactionHash,
// the original emitted txn carries sfDestination
STTx const pseudo = STTx(ttEMIT_FAILURE, [&](STObject& obj) {
obj.setFieldU32(sfLedgerSequence, 1);
obj.setFieldH256(sfTransactionHash, uint256{5});
});
STTx const emitted = STTx(ttPAYMENT, [&](STObject& obj) {
obj[sfAccount] = alice.id();
obj[sfDestination] = bob.id();
});
for (bool const withFix : {false, true})
{
Env env{
*this,
withFix ? features | fix20261005 : features - fix20261005};
OpenView ov{*env.current()};
ApplyContext applyCtx = createApplyContext(env, ov, pseudo);
StubHookContext stubCtx{.emitFailure = emitted};
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
auto& api = hookCtx.api();
// present on both: read from the emitted txn either way
{
auto const result = api.otxn_field(sfAccount.getCode());
BEAST_EXPECT(result.has_value());
auto const* acct =
dynamic_cast<STAccount const*>(result.value());
BEAST_EXPECT(acct && acct->value() == alice.id());
}
// only on the emitted txn: before the fix presence was checked
// on the pseudo, so the field was reported missing
{
auto const result = api.otxn_field(sfDestination.getCode());
if (withFix)
{
BEAST_EXPECT(result.has_value());
auto const* dest =
dynamic_cast<STAccount const*>(result.value());
BEAST_EXPECT(dest && dest->value() == bob.id());
}
else
{
BEAST_EXPECT(
!result.has_value() && result.error() == DOESNT_EXIST);
}
}
// only on the pseudo: before the fix this throws out of
// getField on the emitted txn
if (withFix)
{
auto const result = api.otxn_field(sfLedgerSequence.getCode());
BEAST_EXPECT(
!result.has_value() && result.error() == DOESNT_EXIST);
}
else
{
bool threw = false;
try
{
(void)api.otxn_field(sfLedgerSequence.getCode());
}
catch (std::exception const&)
{
threw = true;
}
BEAST_EXPECT(threw);
}
}
}
void
test_otxn_id(FeatureBitset features)
{
@@ -3593,6 +3826,174 @@ public:
}
}
void
test_state_foreign_read_accounting(FeatureBitset features)
{
testcase("Test state_foreign read does not consume write resources");
using namespace jtx;
using namespace hook_api;
using namespace hook;
bool const hasFix = features[fix20261005];
bool const hasExtended = features[featureExtendedHookState];
Env env{*this, features};
auto const fees = env.current()->fees();
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
auto const carol = Account{"carol"};
env.fund(XRP(10000), alice);
// exactly one state reserve slot available
env.fund(fees.accountReserve(0) + fees.increment, bob);
// no state reserve slot available
env.fund(fees.accountReserve(0), carol);
env.close();
uint256 const readKey{1};
uint256 const writeKey{2};
uint256 const ns{3};
Bytes const existingData{0xAA, 0xBB};
Bytes writeData{0x01};
OpenView ov{*env.current()};
// Place an existing state entry (and its namespace directory) on
// ledger for each account, so state_foreign's cache-miss path reads it.
auto insertState = [&](AccountID const& acc) {
auto const dirKeylet = keylet::hookStateDir(acc, ns);
auto dir = std::make_shared<SLE>(dirKeylet);
dir->setFieldH256(sfRootIndex, dirKeylet.key);
dir->setFieldV256(
sfIndexes,
STVector256{std::vector<uint256>{
keylet::hookState(acc, readKey, ns).key}});
ov.rawInsert(dir);
auto hs =
std::make_shared<SLE>(keylet::hookState(acc, readKey, ns));
hs->setFieldVL(sfHookStateData, existingData);
hs->setFieldH256(sfHookStateKey, readKey);
hs->setFieldU64(sfOwnerNode, 0);
ov.rawInsert(hs);
};
insertState(alice.id());
insertState(bob.id());
insertState(carol.id());
STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
ApplyContext applyCtx = createApplyContext(env, ov, invokeTx);
auto availableSlots = [&](AccountID const& acc) -> int64_t {
auto const sle = ov.read(keylet::account(acc));
int64_t avail = sle->getFieldAmount(sfBalance).xrp().drops() -
fees.accountReserve(sle->getFieldU32(sfOwnerCount)).drops();
return avail / fees.increment.drops();
};
{
// A read must not consume the shared modification limit.
HookStateMap stateMap;
auto hookCtx = makeStubHookContext(
applyCtx, alice.id(), alice.id(), {}, stateMap);
auto& api = hookCtx.api();
stateMap.modified_entry_count = max_state_modifications - 1;
auto const read = api.state_foreign(readKey, ns, alice.id());
BEAST_EXPECT(read.has_value() && read.value() == existingData);
int64_t const slots = availableSlots(alice.id());
BEAST_EXPECT(slots > 1);
BEAST_EXPECT(
stateMap.modified_entry_count ==
max_state_modifications - (hasFix ? 1 : 0));
BEAST_EXPECT(
std::get<0>(stateMap[alice.id()]) == slots - (hasFix ? 0 : 1));
auto const write =
api.state_foreign_set(writeKey, ns, alice.id(), writeData);
if (hasFix)
{
BEAST_EXPECT(write.has_value());
BEAST_EXPECT(
stateMap.modified_entry_count == max_state_modifications);
}
else
{
BEAST_EXPECT(!write.has_value());
BEAST_EXPECT(write.error() == TOO_MANY_STATE_MODIFICATIONS);
}
}
{
// A read must not consume the reserve budget.
HookStateMap stateMap;
auto hookCtx =
makeStubHookContext(applyCtx, bob.id(), bob.id(), {}, stateMap);
auto& api = hookCtx.api();
BEAST_EXPECT(availableSlots(bob.id()) == 1);
auto const read = api.state_foreign(readKey, ns, bob.id());
BEAST_EXPECT(read.has_value() && read.value() == existingData);
BEAST_EXPECT(std::get<0>(stateMap[bob.id()]) == (hasFix ? 1 : 0));
auto const write =
api.state_foreign_set(writeKey, ns, bob.id(), writeData);
if (hasFix)
{
BEAST_EXPECT(write.has_value());
BEAST_EXPECT(std::get<0>(stateMap[bob.id()]) == 0);
}
else
{
BEAST_EXPECT(!write.has_value());
BEAST_EXPECT(write.error() == RESERVE_INSUFFICIENT);
}
// Modifying the entry that was read needs no new reserve and
// is still charged as a modification.
auto const before = stateMap.modified_entry_count;
Bytes overwrite{0x02};
auto const update =
api.state_foreign_set(readKey, ns, bob.id(), overwrite);
BEAST_EXPECT(update.has_value());
BEAST_EXPECT(stateMap.modified_entry_count == before + 1);
}
{
// A read on an account with no spare reserve must succeed;
// previously ExtendedHookState's sanity check rejected it.
HookStateMap stateMap;
auto hookCtx = makeStubHookContext(
applyCtx, carol.id(), carol.id(), {}, stateMap);
auto& api = hookCtx.api();
BEAST_EXPECT(availableSlots(carol.id()) == 0);
auto const read = api.state_foreign(readKey, ns, carol.id());
if (hasExtended && !hasFix)
{
BEAST_EXPECT(!read.has_value());
BEAST_EXPECT(read.error() == INTERNAL_ERROR);
}
else
{
BEAST_EXPECT(read.has_value() && read.value() == existingData);
BEAST_EXPECT(stateMap.modified_entry_count == (hasFix ? 0 : 1));
}
// Writing a new key still fails on reserve either way.
auto const write =
api.state_foreign_set(writeKey, ns, carol.id(), writeData);
BEAST_EXPECT(!write.has_value());
if (!write.has_value())
BEAST_EXPECT(write.error() == RESERVE_INSUFFICIENT);
}
}
void
test_state_foreign_set_max(FeatureBitset features)
{
@@ -4324,8 +4725,20 @@ public:
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
BEAST_EXPECT(api.sto_validate(Bytes{}).error() == TOO_SMALL);
BEAST_EXPECT(api.sto_validate(Bytes{0x00}).error() == TOO_SMALL);
// a buffer shorter than two bytes can't be a valid STObject:
// fix20261005 reports it as invalid rather than as TOO_SMALL
if (env.closed()->rules().enabled(fix20261005))
{
BEAST_EXPECT(api.sto_validate(Bytes{}).value() == false);
BEAST_EXPECT(api.sto_validate(Bytes{0x00}).value() == false);
BEAST_EXPECT(api.sto_validate(Bytes{0xE1}).value() == false);
}
else
{
BEAST_EXPECT(api.sto_validate(Bytes{}).error() == TOO_SMALL);
BEAST_EXPECT(api.sto_validate(Bytes{0x00}).error() == TOO_SMALL);
BEAST_EXPECT(api.sto_validate(Bytes{0xE1}).error() == TOO_SMALL);
}
// { Memo: {MemoData: "BEEF"} }
auto const memos = *strUnHex("EA7D02BEEFE1");
@@ -4399,6 +4812,22 @@ public:
BEAST_EXPECT(accid.has_value());
auto aliceid = alice.id();
BEAST_EXPECT(accid.value() == Bytes(aliceid.begin(), aliceid.end()));
// Bytes outside the base58 alphabet, including >= 0x80, must be
// rejected wherever they appear (signed-char lookup regression)
{
std::string const addr = alice.human();
for (std::size_t pos = 0; pos < addr.size(); ++pos)
{
for (int const b : {0x00, 0x7f, 0x80, 0xc2, 0xff})
{
std::string bad = addr;
bad[pos] = static_cast<char>(b);
auto const r = api.util_accid(bad);
BEAST_EXPECT(!r && r.error() == INVALID_ARGUMENT);
}
}
}
}
void
@@ -4768,6 +5197,7 @@ public:
test_prepare(features);
test_emit(features);
test_emitted_txn_entry(features);
test_etxn_burden(features);
test_etxn_generation(features);
test_otxn_burden(features);
@@ -4781,6 +5211,7 @@ public:
test_fee_base(features);
test_otxn_field(features);
test_otxn_field_emit_failure(features);
test_ledger_keylet(features);
@@ -4796,6 +5227,7 @@ public:
test_float_one(features);
test_float_root(features);
test_float_set(features);
test_float_set(features - fix20261005);
test_float_sign(features);
test_float_sto(features);
test_float_sto_set(features);
@@ -4834,6 +5266,10 @@ public:
test_state(features);
test_state_foreign(features);
test_state_foreign_read_accounting(features);
test_state_foreign_read_accounting(features - fix20261005);
test_state_foreign_read_accounting(
features - fix20261005 - featureExtendedHookState);
test_state_foreign_set(features - fixHookMap);
test_state_foreign_set(features);
test_state_foreign_set_max(features);
@@ -4847,6 +5283,7 @@ public:
test_sto_subfield(features);
test_sto_subfield(features - fix20260929);
test_sto_validate(features);
test_sto_validate(features - fix20261005);
test_trace(features);
test_trace_float(features);
+203
View File
@@ -0,0 +1,203 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2025 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx/Env.h>
#include <test/jtx/envconfig.h>
#include <xrpld/app/main/LoadManager.h>
#include <xrpld/app/misc/LoadFeeTrack.h>
#include <xrpl/beast/unit_test.h>
#include <cstdint>
namespace ripple {
/**
* Unit tests for LoadFeeTrack fee adjustment mechanics.
*
* These tests document the expected per-second behaviour of
* raiseLocalFee() and lowerLocalFee() as called by LoadManager::run().
*
* Background: prior to the fix in this PR, the fee adjustment block in
* LoadManager::run() was placed *outside* the while loop, meaning
* raiseLocalFee()/lowerLocalFee() fired only once on shutdown rather
* than every second during normal operation. localTxnLoadFee_ (and thus
* load_factor_local in server_info) was therefore never adjusted by
* local job queue load during normal operation.
*/
class LoadManager_test : public beast::unit_test::suite
{
public:
// The normal/minimum fee factor (kLftNormalFee in LoadFeeTrack)
static constexpr std::uint32_t kNormalFee = 256;
void
testRaiseHysteresis()
{
testcase("raiseLocalFee requires two consecutive calls");
LoadFeeTrack track;
// First call: raiseCount_ goes from 0 to 1, returns false (hysteresis)
BEAST_EXPECT(!track.raiseLocalFee());
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
// Second call: raiseCount_ reaches 2, fee is raised
BEAST_EXPECT(track.raiseLocalFee());
BEAST_EXPECT(track.getLocalFee() > kNormalFee);
}
void
testLowerDecaysToBaseline()
{
testcase("lowerLocalFee decays elevated fee back to baseline");
LoadFeeTrack track;
// Raise the fee above baseline (requires two calls due to hysteresis)
track.raiseLocalFee();
track.raiseLocalFee();
auto const elevated = track.getLocalFee();
BEAST_EXPECT(elevated > kNormalFee);
// lowerLocalFee() should decay it each call
// Each call reduces by 1/kLftFeeDecFraction (1/4), so a few
// iterations should bring it back to kNormalFee
bool changed = false;
for (int i = 0; i < 100; ++i)
{
changed |= track.lowerLocalFee();
if (track.getLocalFee() == kNormalFee)
break;
}
BEAST_EXPECT(changed);
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
}
void
testLowerAtBaselineIsNoop()
{
testcase("lowerLocalFee at baseline returns false");
LoadFeeTrack track;
// Already at baseline — should return false (no change)
BEAST_EXPECT(!track.lowerLocalFee());
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
}
void
testRaiseResetsOnLower()
{
testcase("lowerLocalFee resets raiseCount");
LoadFeeTrack track;
// One raise call (hysteresis: count=1, no change yet)
BEAST_EXPECT(!track.raiseLocalFee());
// Lower resets raiseCount_ to 0
track.lowerLocalFee();
// Next raise call starts from 0 again — still needs two calls
BEAST_EXPECT(!track.raiseLocalFee());
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
}
void
testIsLoadedLocal()
{
testcase("isLoadedLocal reflects fee state correctly");
LoadFeeTrack track;
// At baseline, not loaded
BEAST_EXPECT(!track.isLoadedLocal());
// After one raise (hysteresis, fee unchanged) — raiseCount_ != 0
track.raiseLocalFee();
BEAST_EXPECT(track.isLoadedLocal());
// After lower, raiseCount_ reset and fee back at baseline
track.lowerLocalFee();
BEAST_EXPECT(!track.isLoadedLocal());
}
void
testLoadManagerLoop()
{
testcase("LoadManager loop raises and decays load_factor_local");
using namespace test::jtx;
Env env(*this, envconfig());
// Stop the real LoadManager thread so we can drive the fee track
// manually without races. On stock code the fee adjustment block
// is outside the while loop and never fires during normal operation,
// so getFeeTrack().getLocalFee() stays at kNormalFee (256) regardless.
env.app().getLoadManager().stop();
auto& feeTrack = env.app().getFeeTrack();
// Confirm baseline
BEAST_EXPECT(feeTrack.getLocalFee() == kNormalFee);
BEAST_EXPECT(!feeTrack.isLoadedLocal());
// raiseLocalFee() requires two consecutive calls (hysteresis guard).
// This is the exact sequence LoadManager::run() executes each tick
// when the job queue is overloaded.
BEAST_EXPECT(!feeTrack.raiseLocalFee()); // tick 1: count=1, no change
BEAST_EXPECT(feeTrack.raiseLocalFee()); // tick 2: count=2, fee raised
BEAST_EXPECT(feeTrack.getLocalFee() > kNormalFee);
BEAST_EXPECT(feeTrack.isLoadedLocal());
// Now simulate the LoadManager loop calling lowerLocalFee() each tick
// until the fee decays back to baseline. On stock code this block
// never runs during normal operation so the fee would stay elevated.
bool decayed = false;
for (int i = 0; i < 100; ++i)
{
feeTrack.lowerLocalFee();
if (feeTrack.getLocalFee() == kNormalFee)
{
decayed = true;
break;
}
}
BEAST_EXPECT(decayed);
BEAST_EXPECT(feeTrack.getLocalFee() == kNormalFee);
BEAST_EXPECT(!feeTrack.isLoadedLocal());
}
void
run() override
{
testRaiseHysteresis();
testLowerDecaysToBaseline();
testLowerAtBaselineIsNoop();
testRaiseResetsOnLower();
testIsLoadedLocal();
testLoadManagerLoop();
}
};
BEAST_DEFINE_TESTSUITE(LoadManager, app, ripple);
} // namespace ripple
File diff suppressed because it is too large. Load diff
+474
View File
@@ -24,6 +24,7 @@
#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>
@@ -975,9 +976,482 @@ 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
@@ -0,0 +1,400 @@
//------------------------------------------------------------------------------
/*
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
@@ -0,0 +1,111 @@
//------------------------------------------------------------------------------
/*
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
+596 -4
View File
@@ -26,11 +26,19 @@
#include <xrpld/app/misc/detail/OnlineDeleteRanges.h>
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
#include <xrpld/core/ConfigSections.h>
#include <xrpld/nodestore/NodeObject.h>
#include <xrpld/nodestore/detail/DatabaseRotatingImp.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <boost/filesystem.hpp>
#include <chrono>
#include <exception>
#include <limits>
#include <string>
#include <thread>
namespace ripple {
namespace test {
@@ -47,6 +55,19 @@ class SHAMapStore_test : public beast::unit_test::suite
return cfg;
}
// Same as onlineDelete, but on a node store that persists nodes, so
// SHAMapStoreImp builds DatabaseRotatingImp and really rotates.
// envconfig defaults to type=rwdb, which is the null node store.
static auto
onlineDeleteStoring(std::unique_ptr<Config> cfg, std::string const& path)
{
cfg = onlineDelete(std::move(cfg));
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("type", "memory");
section.set("path", path);
return cfg;
}
static auto
advisoryDelete(std::unique_ptr<Config> cfg)
{
@@ -74,6 +95,27 @@ class SHAMapStore_test : public beast::unit_test::suite
return cfg;
}
// Online delete with DatabasePinned over in-memory backends. Unlike
// pinnedPersistent, the hot store keeps nodes (type=rwdb is the null
// node store), so ledgers closed in the test can be fetched. There is
// no database_path, so every SHAMapStoreImp opens its own private
// state.db: a second instance built by a test does not see (or trip
// over) the rotation state of the Env's own.
static auto
pinnedInMemory(
std::unique_ptr<Config> cfg,
std::string const& nodePath,
std::string const& pinnedPath)
{
cfg = onlineDelete(std::move(cfg));
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("type", "memory");
section.set("path", nodePath);
section.set("pinned_type", "memory");
section.set("pinned_path", pinnedPath);
return cfg;
}
bool
goodLedger(
jtx::Env& env,
@@ -308,13 +350,23 @@ public:
}
void
testAutomatic()
testAutomatic(bool storing)
{
testcase("automatic online_delete");
testcase(
storing ? "automatic online_delete (rotating node store)"
: "automatic online_delete (null node store)");
using namespace jtx;
using namespace std::chrono_literals;
Env env(*this, envconfig(onlineDelete));
beast::temp_dir nodeDir;
Env env(
*this,
storing ? envconfig(onlineDeleteStoring, nodeDir.path())
: envconfig(onlineDelete));
BEAST_EXPECT(
storing ==
(dynamic_cast<NodeStore::DatabaseRotating*>(
&env.app().getNodeStore()) != nullptr));
auto& store = env.app().getSHAMapStore();
auto ledgerSeq = waitForReady(env);
@@ -572,6 +624,412 @@ public:
return backend;
}
std::unique_ptr<NodeStore::Backend>
makeMemoryBackend(
jtx::Env& env,
NodeStoreScheduler& scheduler,
std::string const& name)
{
Section section;
section.set("type", "memory");
section.set("path", name);
auto backend = NodeStore::Manager::instance().make_Backend(
section,
megabytes(env.app().config().getValueFor(
SizedItem::burstSize, std::nullopt)),
scheduler,
env.app().logs().journal("NodeStoreTest"));
backend->open();
return backend;
}
// Ordinary fetch (duplicate == false) must copy an archived object
// into the writable backend. The following rotation deletes the
// archive. Without that copy, a transaction node read between
// rotations disappears while callers can still name it.
void
testArchiveReadSurvivesRotation()
{
testcase("archive read survives rotation");
using namespace jtx;
Env env(*this, envconfig(onlineDelete));
NodeStoreScheduler scheduler(env.app().getJobQueue());
Section nscfg;
nscfg.set("type", "memory");
nscfg.set("path", "rotating");
auto dbr = std::make_unique<NodeStore::DatabaseRotatingImp>(
env.app(),
scheduler,
1,
makeMemoryBackend(env, scheduler, "writable"),
makeMemoryBackend(env, scheduler, "archive"),
nscfg,
env.app().logs().journal("NodeStoreTest"));
Blob const expected(32, 0xab);
auto const hash = sha512Half(makeSlice(expected));
{
Blob data = expected;
dbr->store(hotTRANSACTION_NODE, std::move(data), hash, 1);
}
auto const matches = [&](std::shared_ptr<NodeObject> const& object) {
return object && object->getType() == hotTRANSACTION_NODE &&
object->getData() == expected;
};
// Public fetch defaults duplicate to false. The private override
// is the rotation copy pass.
NodeStore::Database& db = *dbr;
BEAST_EXPECT(matches(db.fetchNodeObject(hash)));
auto const nop = [](std::string const&, std::string const&) {};
// Writable becomes the archive. The object now lives only there.
dbr->rotate(makeMemoryBackend(env, scheduler, "writable-2"), nop);
BEAST_EXPECT(matches(db.fetchNodeObject(hash)));
dbr->rotate(makeMemoryBackend(env, scheduler, "writable-3"), nop);
BEAST_EXPECT(matches(db.fetchNodeObject(hash)));
}
// getWriteLoad, sync, storeLedger, importDatabase and for_each changed
// lock type. In null mode no Env builds a DatabaseRotatingImp, so
// drive them directly.
void
testRotatingAccessors()
{
testcase("rotating database accessors");
using namespace jtx;
// Source ledger must come from a node store that holds its nodes.
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("type", "memory");
section.set("path", "rotating-accessors-app");
return cfg;
}));
env.fund(XRP(10000), Account{"alice"});
env.close();
NodeStoreScheduler scheduler(env.app().getJobQueue());
Section nscfg;
nscfg.set("type", "memory");
nscfg.set("path", "rotating-accessors");
auto makeDb = [&](std::string const& prefix) {
return std::make_unique<NodeStore::DatabaseRotatingImp>(
env.app(),
scheduler,
1,
makeMemoryBackend(env, scheduler, prefix + "-writable"),
makeMemoryBackend(env, scheduler, prefix + "-archive"),
nscfg,
env.app().logs().journal("NodeStoreTest"));
};
auto src = makeDb("rotating-accessors-src");
BEAST_EXPECT(src->getName() == "rotating-accessors-src-writable");
BEAST_EXPECT(src->getWriteLoad() >= 0);
src->sync();
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
if (!BEAST_EXPECT(ledger))
return;
BEAST_EXPECT(src->storeLedger(ledger));
NodeStore::Database& srcDb = *src;
auto const stateRoot = ledger->stateMap().getHash().as_uint256();
BEAST_EXPECT(srcDb.fetchNodeObject(ledger->info().hash));
BEAST_EXPECT(srcDb.fetchNodeObject(stateRoot));
// importDatabase walks the source with for_each.
auto dst = makeDb("rotating-accessors-dst");
dst->importDatabase(*src);
NodeStore::Database& dstDb = *dst;
BEAST_EXPECT(dstDb.fetchNodeObject(ledger->info().hash));
BEAST_EXPECT(dstDb.fetchNodeObject(stateRoot));
}
static auto
nullBackend(std::unique_ptr<Config> cfg)
{
cfg->LEDGER_HISTORY = 8;
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("type", "rwdb");
section.set("path", "main");
return cfg;
}
static auto
nullBackendFullHistory(std::unique_ptr<Config> cfg)
{
cfg = nullBackend(std::move(cfg));
cfg->LEDGER_HISTORY = std::numeric_limits<std::uint32_t>::max();
return cfg;
}
static auto
nullBackendDeleteOff(std::unique_ptr<Config> cfg)
{
cfg = nullBackend(std::move(cfg));
cfg->LEDGER_HISTORY = 256;
cfg->section(ConfigSection::nodeDatabase()).set("online_delete", "0");
return cfg;
}
// Payments keep applying after more closes than ledger_history,
// with no node-store record of the state tree. Close time comes
// from the resident ledger, not a node-store header walk.
void
testNullModeLedgerProgression()
{
testcase("null mode ledger progression");
using namespace jtx;
Env env(*this, envconfig(nullBackend));
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
auto const bobBefore = env.balance(bob);
constexpr int payments = 12;
for (int i = 0; i < payments; ++i)
{
env(pay(alice, bob, XRP(1)));
env.close();
}
BEAST_EXPECT(env.balance(bob) == bobBefore + XRP(payments));
BEAST_EXPECT(env.seq(alice) > 1);
auto const info = env.rpc(
"json",
"account_info",
std::string{"{\"account\": \""} + alice.human() + "\"}");
BEAST_EXPECT(
info[jss::result].isMember(jss::account_data) &&
info[jss::result][jss::account_data][jss::Account] ==
alice.human());
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
if (!BEAST_EXPECT(ledger))
return;
BEAST_EXPECT(ledger->read(keylet::account(alice.id())));
auto const closeTime = env.app().getLedgerMaster().getCloseTimeByHash(
ledger->info().hash, ledger->info().seq);
BEAST_EXPECT(closeTime && *closeTime == ledger->info().closeTime);
auto const validated = env.rpc("ledger", "validated");
BEAST_EXPECT(goodLedger(
env, validated, std::to_string(ledger->info().seq), true));
}
// ledger_history=full would make the retained-ledger window grow
// without bound, since nothing else can drop a null-mode tree.
void
testNullModeRejectsFullHistory()
{
testcase("RWDB null mode rejects ledger_history=full");
using namespace jtx;
try
{
Env env(*this, envconfig(nullBackendFullHistory));
fail("Env should throw when ledger_history is full");
}
catch (std::exception const& e)
{
BEAST_EXPECT(
std::string(e.what()).find("ledger_history") !=
std::string::npos);
}
}
// online_delete=0 is an explicit disable. It must not be treated
// as a missing key and replaced with ledger_history.
void
testExplicitOnlineDeleteZero()
{
testcase("explicit online_delete zero stays off");
using namespace jtx;
Env env(*this, envconfig(nullBackendDeleteOff));
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
auto const balance = env.balance(alice);
env.close();
BEAST_EXPECT(env.app().getSHAMapStore().getLastRotated() == 0);
BEAST_EXPECT(env.balance(alice) == balance);
}
void
testNullModeRequiresHistory()
{
testcase("RWDB null mode requires ledger_history > 0");
using namespace jtx;
try
{
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
cfg->LEDGER_HISTORY = 0;
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("type", "rwdb");
section.set("path", "main");
return cfg;
}));
fail("Env should throw when ledger_history is 0");
}
catch (std::exception const& e)
{
BEAST_EXPECT(
std::string(e.what()).find("ledger_history") !=
std::string::npos);
}
}
void
testRetainedLedgerCloseTime()
{
testcase("retained ledger close time survives later closes");
using namespace jtx;
Env env(*this, envconfig(nullBackend));
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
auto const first = env.app().getLedgerMaster().getClosedLedger();
if (!BEAST_EXPECT(first))
return;
auto const firstHash = first->info().hash;
auto const firstSeq = first->info().seq;
auto const firstClose = first->info().closeTime;
for (int i = 0; i < 12; ++i)
{
env(pay(alice, env.master, XRP(1)));
env.close();
}
auto const closeTime =
env.app().getLedgerMaster().getCloseTimeByHash(firstHash, firstSeq);
BEAST_EXPECT(closeTime && *closeTime == firstClose);
BEAST_EXPECT(env.balance(alice) < XRP(10000));
}
void
testNullBackendIsPerConfig()
{
testcase("null backend flag is per configuration");
Config rwdbCfg;
Config upperCfg;
Config nudbCfg;
Config emptyCfg;
rwdbCfg.section(ConfigSection::nodeDatabase()).set("type", "rwdb");
upperCfg.section(ConfigSection::nodeDatabase()).set("type", "RWDB");
nudbCfg.section(ConfigSection::nodeDatabase()).set("type", "NuDB");
// Evaluated per Config object, not latched process-wide.
BEAST_EXPECT(rwdbCfg.nullBackend());
BEAST_EXPECT(upperCfg.nullBackend());
BEAST_EXPECT(!nudbCfg.nullBackend());
BEAST_EXPECT(!emptyCfg.nullBackend());
BEAST_EXPECT(rwdbCfg.nullBackend());
}
// Resident lookups are used with peer-supplied values. They must find
// in-memory ledgers but never load or edit the complete-ledger set.
void
testResidentLedgerLookup()
{
testcase("resident ledger lookup does not load or clear");
using namespace jtx;
Env env(*this, envconfig(nullBackend));
Account const alice{"alice"};
env.fund(XRP(10000), alice);
env.close();
for (int i = 0; i < 4; ++i)
env.close();
auto& lm = env.app().getLedgerMaster();
auto const closed = lm.getClosedLedger();
if (!BEAST_EXPECT(closed))
return;
auto const complete = lm.getCompleteLedgers();
auto const bySeq = lm.getResidentLedgerBySeq(closed->info().seq);
BEAST_EXPECT(bySeq && bySeq->info().hash == closed->info().hash);
auto const byHash = lm.getResidentLedgerByHash(closed->info().hash);
BEAST_EXPECT(byHash && byHash->info().seq == closed->info().seq);
// A recent ledger still inside the retained window.
auto const prior = lm.getResidentLedgerBySeq(closed->info().seq - 2);
BEAST_EXPECT(prior && prior->info().seq == closed->info().seq - 2);
BEAST_EXPECT(!lm.getResidentLedgerByHash(uint256{12345}));
BEAST_EXPECT(!lm.getResidentLedgerBySeq(closed->info().seq + 1000));
BEAST_EXPECT(lm.getCompleteLedgers() == complete);
// Drop every ledger from the history cache. Null mode must still
// reach recent ledgers through the closed/validated holders and
// the retained window, without loading anything.
env.app().getJobQueue().rendezvous();
auto const older = lm.getResidentLedgerBySeq(closed->info().seq - 3);
if (!BEAST_EXPECT(older))
return;
lm.clearLedgerCachePrior(closed->info().seq + 1);
auto const closedAgain = lm.getResidentLedgerBySeq(closed->info().seq);
BEAST_EXPECT(
closedAgain && closedAgain->info().hash == closed->info().hash);
auto const closedByHash =
lm.getResidentLedgerByHash(closed->info().hash);
BEAST_EXPECT(
closedByHash && closedByHash->info().seq == closed->info().seq);
auto const retainedBySeq = lm.getResidentLedgerBySeq(older->info().seq);
BEAST_EXPECT(
retainedBySeq && retainedBySeq->info().hash == older->info().hash);
auto const retainedByHash =
lm.getResidentLedgerByHash(older->info().hash);
BEAST_EXPECT(
retainedByHash && retainedByHash->info().seq == older->info().seq);
BEAST_EXPECT(!lm.getResidentLedgerByHash(uint256{12345}));
BEAST_EXPECT(lm.getCompleteLedgers() == complete);
}
void
testNullFactoryDropsWrites()
{
testcase("type=rwdb uses NullFactory and drops node-store writes");
using namespace jtx;
Env env(*this, envconfig(nullBackend));
auto& db = env.app().getNodeStore();
uint256 const hash{2};
Blob data(32, 7);
db.store(hotACCOUNT_NODE, std::move(data), hash, 1);
BEAST_EXPECT(!db.fetchNodeObject(hash, 1));
}
void
testRotate()
{
@@ -718,6 +1176,127 @@ public:
}
}
// catalogue_unpin of everything (or a rolled-back first load) leaves the
// pinned set empty. That must persist as "no pins", not as a string that
// fails to parse on the next start.
void
testPinnedRangeRestoreEmpty()
{
testcase("pinned range restore of an empty set");
using namespace jtx;
beast::temp_dir tempDir;
auto const tempPath = boost::filesystem::path(tempDir.path());
auto const nodePath = (tempPath / "node").string();
auto const pinnedPath = (tempPath / "pinned").string();
Env env(*this, envconfig(pinnedInMemory, nodePath, pinnedPath));
NodeStoreScheduler scheduler(env.app().getJobQueue());
auto const journal = env.app().journal("SHAMapStoreTest");
try
{
SHAMapStoreImp restoreCheck(env.app(), scheduler, journal);
RangeSet<std::uint32_t> ranges;
ranges.insert(range(100u, 200u));
restoreCheck.setPinnedRanges(ranges);
restoreCheck.setPinnedRanges(RangeSet<std::uint32_t>{});
restoreCheck.start();
restoreCheck.stop();
pass();
}
catch (std::exception const& e)
{
fail(
std::string("restart with no pinned ranges threw: ") +
e.what());
}
BEAST_EXPECT(
env.app().getLedgerMaster().getPinnedLedgersRangeSet().empty());
}
// A crash between a pinned-range checkpoint and the backend committing
// the last ledgers leaves the recorded interval's tail missing. Startup
// trims it back to the last ledger present instead of refusing to start.
void
testPinnedRangeRestoreTrimsMissingTail()
{
testcase("pinned range restore trims a missing tail");
using namespace jtx;
beast::temp_dir tempDir;
auto const tempPath = boost::filesystem::path(tempDir.path());
auto const nodePath = (tempPath / "node").string();
auto const pinnedPath = (tempPath / "pinned").string();
Env env(*this, envconfig(pinnedInMemory, nodePath, pinnedPath));
for (int i = 0; i < 4; ++i)
env.close();
auto& rdb = env.app().getRelationalDatabase();
auto const last = env.app().getLedgerMaster().getValidLedgerIndex();
auto const first = last - 2;
// Ledger saves may complete asynchronously; wait for the SQL rows.
for (int i = 0; i < 100 && rdb.getHashByIndex(last).isZero(); ++i)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
if (!BEAST_EXPECT(
rdb.getHashByIndex(first).isNonZero() &&
rdb.getHashByIndex(last).isNonZero()))
return;
NodeStoreScheduler scheduler(env.app().getJobQueue());
auto const journal = env.app().journal("SHAMapStoreTest");
try
{
SHAMapStoreImp restoreCheck(env.app(), scheduler, journal);
// Recorded through last + 10, but only [first, last] exist.
RangeSet<std::uint32_t> recorded;
recorded.insert(range(first, last + 10));
restoreCheck.setPinnedRanges(recorded);
restoreCheck.start();
restoreCheck.stop();
}
catch (std::exception const& e)
{
fail(std::string("restart with a missing tail threw: ") + e.what());
return;
}
RangeSet<std::uint32_t> expected;
expected.insert(range(first, last));
BEAST_EXPECT(
env.app().getLedgerMaster().getPinnedLedgersRangeSet() == expected);
// A missing tail longer than the trim limit (65536 ledgers) is not
// an interrupted load: it means the wrong pinned store, so startup
// still refuses.
try
{
SHAMapStoreImp restoreCheck(env.app(), scheduler, journal);
RangeSet<std::uint32_t> recorded;
recorded.insert(range(first, last + 70000));
restoreCheck.setPinnedRanges(recorded);
restoreCheck.start();
restoreCheck.stop();
fail("Expected startup failure for an over-long missing tail");
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
std::string(e.what()).find("Persisted pinned interval") !=
std::string::npos);
}
}
// Helper: compare two RangeSet<uint32_t> for set-equality. The
// test cares about which sequences are in the result, not the
// particular interval representation.
@@ -852,10 +1431,23 @@ public:
{
testComputeOnlineDeleteTargets();
testClear();
testAutomatic();
testAutomatic(false);
testAutomatic(true);
testRotatingAccessors();
testCanDelete();
testArchiveReadSurvivesRotation();
testNullModeLedgerProgression();
testNullModeRejectsFullHistory();
testExplicitOnlineDeleteZero();
testNullModeRequiresHistory();
testRetainedLedgerCloseTime();
testNullBackendIsPerConfig();
testResidentLedgerLookup();
testNullFactoryDropsWrites();
testRotate();
testPinnedRangeRestoreRequiresPinnedData();
testPinnedRangeRestoreEmpty();
testPinnedRangeRestoreTrimsMissingTail();
}
};
+9
View File
@@ -6146,6 +6146,15 @@ 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
+371 -2
View File
@@ -2134,6 +2134,118 @@ public:
}
}
{
/// Rename and remove the HookName with HookName-only updates
Env env{*this, features};
env.fund(XRP(10000), alice);
env.close();
// Create with hook name
auto jvh = hso(accept_wasm);
jvh[jss::HookName] = "41424344";
env(ripple::test::jtx::hook(alice, {{jvh}}, 0),
M("Create named hook"),
HSFEE);
env.close();
auto const hookNameOnlyUpdate = [&](std::string const& name) {
Json::Value jv;
jv[jss::Account] = alice.human();
jv[jss::TransactionType] = jss::SetHook;
jv[jss::Flags] = 0;
jv[jss::Hooks] = Json::Value{Json::arrayValue};
Json::Value iv;
iv[jss::HookName] = name;
jv[jss::Hooks][0U][jss::Hook] = iv;
return jv;
};
// Rename: update carrying only a non-empty sfHookName
env(hookNameOnlyUpdate("4142434445"),
M("Rename hook with a HookName-only update"),
HSFEE);
env.close();
{
auto const hooks = env.le(keylet::hook(alice));
BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks));
auto const& hooksArray = hooks->getFieldArray(sfHooks);
BEAST_EXPECT(hooksArray.size() == 1);
BEAST_EXPECT(hooksArray[0].isFieldPresent(sfHookName));
BEAST_EXPECT(
strHex(hooksArray[0].getFieldVL(sfHookName)) ==
"4142434445");
}
// Remove: update carrying only an empty sfHookName
env(hookNameOnlyUpdate(""),
M("Remove hook name with a HookName-only update"),
HSFEE);
// In the same open ledger as the removal: the open view hands
// the next transaction the very sfHooks entry the SetHook just
// modified, so the fee calculation and the hook chain must
// tolerate the removed name. This is the path that crashed the
// node at submit, and crashed every validator replaying the
// pair while building the ledger.
{
auto jv = invoke::invoke(alice);
auto const expectedFee =
calculateBaseFee(*env.current(), *env.jt(jv).stx);
BEAST_EXPECT(expectedFee == drops(19));
env(jv,
M("Invoke in the same open ledger as the removal"),
HSFEE);
}
env.close();
{
auto const hookExecutions =
env.meta()->getFieldArray(sfHookExecutions);
BEAST_EXPECT(hookExecutions.size() == 1);
BEAST_EXPECT(
hookExecutions[0].getFieldH256(sfHookHash) == accept_hash);
}
{
auto const hooks = env.le(keylet::hook(alice));
BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks));
auto const& hooksArray = hooks->getFieldArray(sfHooks);
BEAST_EXPECT(hooksArray.size() == 1);
BEAST_EXPECT(!hooksArray[0].isFieldPresent(sfHookName));
}
// The now non-named hook must still be charged for and
// executed by transactions in later ledgers, where the entry
// is re-read from its serialized form.
{
auto jv = invoke::invoke(alice);
auto const expectedFee =
calculateBaseFee(*env.current(), *env.jt(jv).stx);
BEAST_EXPECT(expectedFee == drops(19));
env(jv, M("Invoke after removing the hook name"), HSFEE);
env.close();
auto const hookExecutions =
env.meta()->getFieldArray(sfHookExecutions);
BEAST_EXPECT(hookExecutions.size() == 1);
BEAST_EXPECT(
hookExecutions[0].getFieldH256(sfHookHash) == accept_hash);
}
// Removing the name of a hook that has none must be a no-op,
// not a thrown FieldErr (-> tefEXCEPTION): makeFieldAbsent on
// the untemplated entry throws when the field is missing.
env(hookNameOnlyUpdate(""),
M("Remove hook name again (no-op)"),
HSFEE);
env.close();
{
auto const hooks = env.le(keylet::hook(alice));
BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks));
auto const& hooksArray = hooks->getFieldArray(sfHooks);
BEAST_EXPECT(hooksArray.size() == 1);
BEAST_EXPECT(!hooksArray[0].isFieldPresent(sfHookName));
}
}
// Install named hook
auto jvh = hso(accept_wasm);
jvh[jss::HookName] = "41424344";
@@ -4675,6 +4787,218 @@ public:
}
}
void
test_emit_failure_callback(FeatureBitset features)
{
testcase("Test emit failure callback");
using namespace jtx;
// An emitted txn that is never applied expires into a ttEMIT_FAILURE
// cleanup pseudo, which removes its ltEMITTED_TXN entry and then
// runs the emitting Hook's cbak. Before fix20261005 the callback
// re-read that (already erased) entry and dereferenced null, taking
// the process down, so only the fixed behaviour can be run here.
Env env{*this, features | fix20261005};
auto const alice = Account{"alice"};
env.fund(XRP(10000), alice);
env.close();
TestHook hook_wasm = wasm[R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t emit (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
extern int64_t hook_account (uint32_t write_ptr, uint32_t write_len);
extern int64_t etxn_reserve (uint32_t);
extern int64_t etxn_fee_base (uint32_t read_ptr, uint32_t read_len);
extern int64_t etxn_details (uint32_t write_ptr, uint32_t write_len);
extern int64_t ledger_seq (void);
extern int64_t otxn_id (uint32_t, uint32_t, uint32_t);
extern int64_t otxn_type (void);
extern int64_t otxn_field (uint32_t write_ptr, uint32_t write_len, uint32_t field_id);
extern int64_t state_set (uint32_t, uint32_t, uint32_t, uint32_t);
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__);
#define DOESNT_EXIST (-5)
#define ttINVOKE 99
#define sfAccount ((8U << 16U) + 1U)
#define sfLedgerSequence ((2U << 16U) + 6U)
uint8_t key[32] = { [31] = 1U };
// an Invoke from the hook account, emitted with a callback
uint8_t txn[229] =
{
/* size, upto, field name */
/* 3, 0, tt = Invoke */ 0x12U, 0x00U, 0x63U,
/* 5, 3, flags */ 0x22U, 0x00U, 0x00U, 0x00U, 0x00U,
/* 5, 8, sequence */ 0x24U, 0x00U, 0x00U, 0x00U, 0x00U,
/* 6, 13, firstledgersequence */ 0x20U, 0x1AU, 0x00U, 0x00U, 0x00U, 0x00U,
/* 6, 19, lastledgersequence */ 0x20U, 0x1BU, 0x00U, 0x00U, 0x00U, 0x00U,
/* 9, 25, fee */ 0x68U, 0x40U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
/* 35, 34, signingpubkey */ 0x73U, 0x21U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* 22, 69, account */ 0x81U, 0x14U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* 138, 91, emit details */
/* 0, 229, */
};
#define FLS_OUT (txn + 15U)
#define LLS_OUT (txn + 21U)
#define FEE_OUT (txn + 26U)
#define ACCOUNT_OUT (txn + 71U)
#define EMIT_OUT (txn + 91U)
#define SET_UINT32(ptr, value)\
{\
uint32_t v = (value);\
(ptr)[0] = v >> 24U; (ptr)[1] = v >> 16U; (ptr)[2] = v >> 8U; (ptr)[3] = v;\
}
#define SET_NATIVE_AMOUNT(ptr, amount)\
{\
uint64_t a = (amount);\
(ptr)[0] = 0x40U + ((a >> 56U) & 0x3FU);\
(ptr)[1] = a >> 48U; (ptr)[2] = a >> 40U; (ptr)[3] = a >> 32U;\
(ptr)[4] = a >> 24U; (ptr)[5] = a >> 16U; (ptr)[6] = a >> 8U; (ptr)[7] = a;\
}
#define ACC_EQ(a, b)\
(*(uint64_t*)(a) == *(uint64_t*)(b) &&\
*(uint64_t*)((a) + 8U) == *(uint64_t*)((b) + 8U) &&\
*(uint32_t*)((a) + 16U) == *(uint32_t*)((b) + 16U))
int64_t cbak(uint32_t r)
{
_g(1,1);
// 1 = the emitted txn failed (ttEMIT_FAILURE)
ASSERT(r == 1);
// the otxn is the original emitted txn, not the pseudo
ASSERT(otxn_type() == ttINVOKE);
uint8_t me[20];
ASSERT(hook_account(SBUF(me)) == 20);
uint8_t acc[20];
ASSERT(otxn_field(SBUF(acc), sfAccount) == 20);
ASSERT(ACC_EQ(acc, me));
// carried by the pseudo but not by the emitted txn
ASSERT(otxn_field(0, 0, sfLedgerSequence) == DOESNT_EXIST);
// record the id of the emitted txn that failed
uint8_t id[32];
ASSERT(otxn_id(SBUF(id), 0) == 32);
ASSERT(state_set(SBUF(id), SBUF(key)) == 32);
return accept(0,0,0);
}
int64_t hook(uint32_t r)
{
_g(1,1);
ASSERT(etxn_reserve(1) == 1);
// FirstLedgerSequence is the current ledger, so by the time
// the emission directory is processed for the next ledger it
// has already passed: the txn is never applied and expires
// once LastLedgerSequence is behind the ledger
uint32_t seq = (uint32_t)ledger_seq();
SET_UINT32(FLS_OUT, seq);
SET_UINT32(LLS_OUT, seq + 1);
ASSERT(hook_account(ACCOUNT_OUT, 20) == 20);
ASSERT(etxn_details(EMIT_OUT, 138U) == 138);
int64_t fee = etxn_fee_base(SBUF(txn));
ASSERT(fee > 0);
SET_NATIVE_AMOUNT(FEE_OUT, fee);
uint8_t emithash[32];
ASSERT(emit(SBUF(emithash), SBUF(txn)) == 32);
return accept(0,0,0);
}
)[test.hook]"];
HASH_WASM(hook);
env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0),
M("set emit failure callback"),
HSFEE);
env.close();
env(invoke::invoke(alice),
M("emit failure callback: emit"),
fee(XRP(1)),
ter(tesSUCCESS));
// the emitted txn, from the ltEMITTED_TXN this created
std::optional<uint256> emittedID;
{
auto const meta = env.meta(); // closes the ledger
BEAST_REQUIRE(meta);
for (auto const& node : meta->getFieldArray(sfAffectedNodes))
{
if (node.getFName() != sfCreatedNode ||
node.getFieldU16(sfLedgerEntryType) != ltEMITTED_TXN)
continue;
auto const& nf = const_cast<ripple::STObject&>(node)
.getField(sfNewFields)
.downcast<STObject>();
auto const& et = const_cast<ripple::STObject&>(nf)
.getField(sfEmittedTxn)
.downcast<STObject>();
Serializer s;
et.add(s);
SerialIter sit(s.slice());
emittedID = STTx(sit).getTransactionID();
}
}
BEAST_REQUIRE(emittedID);
auto const emittedKeylet = keylet::emittedTxn(*emittedID);
BEAST_REQUIRE(env.le(emittedKeylet));
// close until the cleanup pseudo has been applied
std::shared_ptr<STObject const> failureMeta;
for (int i = 0; i < 5 && !failureMeta; ++i)
{
env.close();
for (auto const& [tx, meta] : env.closed()->txs)
{
// the emitted txn itself never makes it into a ledger
BEAST_EXPECT(tx->getTransactionID() != *emittedID);
if (tx->getTxnType() == ttEMIT_FAILURE &&
tx->getFieldH256(sfTransactionHash) == *emittedID)
failureMeta = meta;
}
}
BEAST_REQUIRE(failureMeta);
// the cleanup removed the entry and its directory slot
BEAST_EXPECT(!env.le(emittedKeylet));
BEAST_EXPECT(!env.le(keylet::emittedDir()));
// the callback ran to accept() with the original emitted txn
BEAST_REQUIRE(failureMeta->isFieldPresent(sfHookExecutions));
auto const hookExecutions =
failureMeta->getFieldArray(sfHookExecutions);
BEAST_REQUIRE(hookExecutions.size() == 1);
BEAST_EXPECT(hookExecutions[0].getFieldH256(sfHookHash) == hook_hash);
BEAST_EXPECT(
hookExecutions[0].getAccountID(sfHookAccount) == alice.id());
BEAST_EXPECT(hookExecutions[0].getFieldU64(sfHookReturnCode) == 0);
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
uint256 key;
key.data()[31] = 1;
auto const state =
env.le(keylet::hookState(alice.id(), key, uint256{beast::zero}));
BEAST_REQUIRE(state);
BEAST_EXPECT(
state->getFieldVL(sfHookStateData) ==
Blob(emittedID->begin(), emittedID->end()));
}
void
test_etxn_details(FeatureBitset features)
{
@@ -6482,7 +6806,10 @@ public:
{
testcase("Test float_set");
using namespace jtx;
Env env{*this, features};
// this hook asserts the pre-fix20261005 INVALID_FLOAT result for an
// out of range exponent; the amended EXPONENT_UNDERSIZED/OVERSIZED
// behaviour is covered in HookAPI_test
Env env{*this, features - fix20261005};
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
@@ -13262,7 +13589,9 @@ public:
testcase("Test sto_validate");
using namespace jtx;
Env env{*this, features};
// this hook asserts the pre-fix20261005 TOO_SMALL result for a short
// buffer; the amended behaviour is covered by the param hook below
Env env{*this, features - fix20261005};
auto const bob = Account{"bob"};
auto const alice = Account{"alice"};
@@ -13518,6 +13847,45 @@ public:
testSTI(
"011A0000000000000000000000005553440000000000", hasEnabled);
}
// a buffer shorter than two bytes can't be a valid STObject:
// fix20261005 returns 0 (invalid) instead of TOO_SMALL
for (bool const withFix : {true, false})
{
Env env{
*this,
withFix ? features | fix20261005 : features - fix20261005};
env.fund(XRP(10000), alice, bob);
env.close();
// install the hook on alice
env(ripple::test::jtx::hook(
alice, {{hso(hook, overrideFlag)}}, 0),
M("set sto_validate"),
HSFEE);
env.close();
auto payJv = pay(bob, alice, XRP(1));
Json::Value params{Json::arrayValue};
auto& param = params[0U][jss::HookParameter];
param[jss::HookParameterName] = strHex(std::string("V"));
param[jss::HookParameterValue] = "E1";
payJv[jss::HookParameters] = params;
env(payJv, M("test sto_validate short buffer"), fee(XRP(1)));
env.close();
auto const result =
env.meta()->getFieldArray(sfHookExecutions)[0].getFieldU64(
sfHookReturnCode);
// negative exit codes are recorded with the MSB set
BEAST_EXPECT(
result ==
(withFix ? 0ULL
: 0x8000000000000000ULL + 4ULL /*TOO_SMALL*/));
}
}
}
@@ -16148,6 +16516,7 @@ public:
testGuards(features);
test_emit(features); //
test_emit_failure_callback(features);
test_prepare(features);
// test_etxn_burden(features); // tested above
// test_etxn_generation(features); // tested above
+304
View File
@@ -35007,6 +35007,310 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
0x65U, 0x64U, 0x2EU, 0x00U,
}},
{R"[test.hook](
#include <stdint.h>
extern int32_t _g (uint32_t id, uint32_t maxiter);
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t emit (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
extern int64_t hook_account (uint32_t write_ptr, uint32_t write_len);
extern int64_t etxn_reserve (uint32_t);
extern int64_t etxn_fee_base (uint32_t read_ptr, uint32_t read_len);
extern int64_t etxn_details (uint32_t write_ptr, uint32_t write_len);
extern int64_t ledger_seq (void);
extern int64_t otxn_id (uint32_t, uint32_t, uint32_t);
extern int64_t otxn_type (void);
extern int64_t otxn_field (uint32_t write_ptr, uint32_t write_len, uint32_t field_id);
extern int64_t state_set (uint32_t, uint32_t, uint32_t, uint32_t);
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x)\
if (!(x))\
rollback((uint32_t)#x, sizeof(#x), __LINE__);
#define DOESNT_EXIST (-5)
#define ttINVOKE 99
#define sfAccount ((8U << 16U) + 1U)
#define sfLedgerSequence ((2U << 16U) + 6U)
uint8_t key[32] = { [31] = 1U };
// an Invoke from the hook account, emitted with a callback
uint8_t txn[229] =
{
/* size, upto, field name */
/* 3, 0, tt = Invoke */ 0x12U, 0x00U, 0x63U,
/* 5, 3, flags */ 0x22U, 0x00U, 0x00U, 0x00U, 0x00U,
/* 5, 8, sequence */ 0x24U, 0x00U, 0x00U, 0x00U, 0x00U,
/* 6, 13, firstledgersequence */ 0x20U, 0x1AU, 0x00U, 0x00U, 0x00U, 0x00U,
/* 6, 19, lastledgersequence */ 0x20U, 0x1BU, 0x00U, 0x00U, 0x00U, 0x00U,
/* 9, 25, fee */ 0x68U, 0x40U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
/* 35, 34, signingpubkey */ 0x73U, 0x21U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* 22, 69, account */ 0x81U, 0x14U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
/* 138, 91, emit details */
/* 0, 229, */
};
#define FLS_OUT (txn + 15U)
#define LLS_OUT (txn + 21U)
#define FEE_OUT (txn + 26U)
#define ACCOUNT_OUT (txn + 71U)
#define EMIT_OUT (txn + 91U)
#define SET_UINT32(ptr, value)\
{\
uint32_t v = (value);\
(ptr)[0] = v >> 24U; (ptr)[1] = v >> 16U; (ptr)[2] = v >> 8U; (ptr)[3] = v;\
}
#define SET_NATIVE_AMOUNT(ptr, amount)\
{\
uint64_t a = (amount);\
(ptr)[0] = 0x40U + ((a >> 56U) & 0x3FU);\
(ptr)[1] = a >> 48U; (ptr)[2] = a >> 40U; (ptr)[3] = a >> 32U;\
(ptr)[4] = a >> 24U; (ptr)[5] = a >> 16U; (ptr)[6] = a >> 8U; (ptr)[7] = a;\
}
#define ACC_EQ(a, b)\
(*(uint64_t*)(a) == *(uint64_t*)(b) &&\
*(uint64_t*)((a) + 8U) == *(uint64_t*)((b) + 8U) &&\
*(uint32_t*)((a) + 16U) == *(uint32_t*)((b) + 16U))
int64_t cbak(uint32_t r)
{
_g(1,1);
// 1 = the emitted txn failed (ttEMIT_FAILURE)
ASSERT(r == 1);
// the otxn is the original emitted txn, not the pseudo
ASSERT(otxn_type() == ttINVOKE);
uint8_t me[20];
ASSERT(hook_account(SBUF(me)) == 20);
uint8_t acc[20];
ASSERT(otxn_field(SBUF(acc), sfAccount) == 20);
ASSERT(ACC_EQ(acc, me));
// carried by the pseudo but not by the emitted txn
ASSERT(otxn_field(0, 0, sfLedgerSequence) == DOESNT_EXIST);
// record the id of the emitted txn that failed
uint8_t id[32];
ASSERT(otxn_id(SBUF(id), 0) == 32);
ASSERT(state_set(SBUF(id), SBUF(key)) == 32);
return accept(0,0,0);
}
int64_t hook(uint32_t r)
{
_g(1,1);
ASSERT(etxn_reserve(1) == 1);
// FirstLedgerSequence is the current ledger, so by the time
// the emission directory is processed for the next ledger it
// has already passed: the txn is never applied and expires
// once LastLedgerSequence is behind the ledger
uint32_t seq = (uint32_t)ledger_seq();
SET_UINT32(FLS_OUT, seq);
SET_UINT32(LLS_OUT, seq + 1);
ASSERT(hook_account(ACCOUNT_OUT, 20) == 20);
ASSERT(etxn_details(EMIT_OUT, 138U) == 138);
int64_t fee = etxn_fee_base(SBUF(txn));
ASSERT(fee > 0);
SET_NATIVE_AMOUNT(FEE_OUT, fee);
uint8_t emithash[32];
ASSERT(emit(SBUF(emithash), SBUF(txn)) == 32);
return accept(0,0,0);
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x2CU,
0x07U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x03U, 0x7FU,
0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x00U, 0x01U, 0x7EU, 0x60U, 0x02U,
0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7FU, 0x01U,
0x7EU, 0x60U, 0x04U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U,
0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U, 0xCEU, 0x01U, 0x0DU, 0x03U, 0x65U,
0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU,
0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U, 0x61U, 0x63U, 0x6BU,
0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x09U, 0x6FU, 0x74U, 0x78U,
0x6EU, 0x5FU, 0x74U, 0x79U, 0x70U, 0x65U, 0x00U, 0x02U, 0x03U, 0x65U,
0x6EU, 0x76U, 0x0CU, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x61U, 0x63U,
0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x00U, 0x03U, 0x03U, 0x65U, 0x6EU,
0x76U, 0x0AU, 0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x66U, 0x69U, 0x65U,
0x6CU, 0x64U, 0x00U, 0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x07U, 0x6FU,
0x74U, 0x78U, 0x6EU, 0x5FU, 0x69U, 0x64U, 0x00U, 0x04U, 0x03U, 0x65U,
0x6EU, 0x76U, 0x09U, 0x73U, 0x74U, 0x61U, 0x74U, 0x65U, 0x5FU, 0x73U,
0x65U, 0x74U, 0x00U, 0x05U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U,
0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU,
0x76U, 0x0CU, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x72U, 0x65U, 0x73U,
0x65U, 0x72U, 0x76U, 0x65U, 0x00U, 0x06U, 0x03U, 0x65U, 0x6EU, 0x76U,
0x0AU, 0x6CU, 0x65U, 0x64U, 0x67U, 0x65U, 0x72U, 0x5FU, 0x73U, 0x65U,
0x71U, 0x00U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x65U, 0x74U,
0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, 0x61U, 0x69U, 0x6CU, 0x73U,
0x00U, 0x03U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0DU, 0x65U, 0x74U, 0x78U,
0x6EU, 0x5FU, 0x66U, 0x65U, 0x65U, 0x5FU, 0x62U, 0x61U, 0x73U, 0x65U,
0x00U, 0x03U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x04U, 0x65U, 0x6DU, 0x69U,
0x74U, 0x00U, 0x05U, 0x03U, 0x03U, 0x02U, 0x06U, 0x06U, 0x05U, 0x03U,
0x01U, 0x00U, 0x02U, 0x06U, 0x08U, 0x01U, 0x7FU, 0x01U, 0x41U, 0x90U,
0x8DU, 0x04U, 0x0BU, 0x07U, 0x0FU, 0x02U, 0x04U, 0x63U, 0x62U, 0x61U,
0x6BU, 0x00U, 0x0DU, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x0EU,
0x0AU, 0x85U, 0x87U, 0x00U, 0x02U, 0x88U, 0x83U, 0x00U, 0x02U, 0x01U,
0x7FU, 0x01U, 0x7EU, 0x23U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U,
0xE0U, 0x00U, 0x6BU, 0x22U, 0x01U, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U,
0x00U, 0x41U, 0x01U, 0x41U, 0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U,
0x00U, 0x1AU, 0x02U, 0x40U, 0x20U, 0x00U, 0x41U, 0x01U, 0x46U, 0x0DU,
0x00U, 0x41U, 0xD5U, 0x89U, 0x80U, 0x80U, 0x00U, 0x41U, 0x07U, 0x42U,
0xC5U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU,
0x02U, 0x40U, 0x10U, 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0xE3U,
0x00U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xB3U, 0x88U, 0x80U, 0x80U, 0x00U,
0x41U, 0x18U, 0x42U, 0xC8U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U,
0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0xC0U, 0x00U,
0x6AU, 0x41U, 0x14U, 0x10U, 0x83U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U,
0x14U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xBCU, 0x8AU, 0x80U, 0x80U, 0x00U,
0x41U, 0x1DU, 0x42U, 0xCBU, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U,
0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0x20U, 0x6AU,
0x41U, 0x14U, 0x41U, 0x81U, 0x80U, 0x20U, 0x10U, 0x84U, 0x80U, 0x80U,
0x80U, 0x00U, 0x42U, 0x14U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xF1U, 0x89U,
0x80U, 0x80U, 0x00U, 0x41U, 0x27U, 0x42U, 0xCDU, 0x00U, 0x10U, 0x81U,
0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x02U, 0x40U,
0x20U, 0x01U, 0x29U, 0x03U, 0x20U, 0x20U, 0x01U, 0x29U, 0x03U, 0x40U,
0x52U, 0x0DU, 0x00U, 0x20U, 0x01U, 0x29U, 0x03U, 0x28U, 0x20U, 0x01U,
0x29U, 0x03U, 0x48U, 0x52U, 0x0DU, 0x00U, 0x20U, 0x01U, 0x28U, 0x02U,
0x30U, 0x20U, 0x01U, 0x28U, 0x02U, 0x50U, 0x46U, 0x0DU, 0x01U, 0x0BU,
0x41U, 0xE1U, 0x8AU, 0x80U, 0x80U, 0x00U, 0x41U, 0x10U, 0x42U, 0xCEU,
0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U,
0x40U, 0x41U, 0x00U, 0x41U, 0x00U, 0x41U, 0x86U, 0x80U, 0x08U, 0x10U,
0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x7BU, 0x51U, 0x0DU, 0x00U,
0x41U, 0x80U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x33U, 0x42U, 0xD1U,
0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U,
0x40U, 0x20U, 0x01U, 0x41U, 0x20U, 0x41U, 0x00U, 0x10U, 0x85U, 0x80U,
0x80U, 0x80U, 0x00U, 0x42U, 0x20U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xEFU,
0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x1BU, 0x42U, 0xD5U, 0x00U, 0x10U,
0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x20U,
0x01U, 0x41U, 0x20U, 0x41U, 0x80U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U,
0x20U, 0x10U, 0x86U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x20U, 0x51U,
0x0DU, 0x00U, 0x41U, 0x8AU, 0x89U, 0x80U, 0x80U, 0x00U, 0x41U, 0x25U,
0x42U, 0xD6U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU,
0x0BU, 0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x87U, 0x80U,
0x80U, 0x80U, 0x00U, 0x21U, 0x02U, 0x20U, 0x01U, 0x41U, 0xE0U, 0x00U,
0x6AU, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x20U, 0x02U, 0x0BU,
0xF6U, 0x83U, 0x00U, 0x03U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x01U, 0x7FU,
0x23U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0x20U, 0x6BU, 0x22U,
0x01U, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0x01U, 0x41U,
0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x02U, 0x40U,
0x41U, 0x01U, 0x10U, 0x88U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x01U,
0x51U, 0x0DU, 0x00U, 0x41U, 0xDCU, 0x89U, 0x80U, 0x80U, 0x00U, 0x41U,
0x15U, 0x42U, 0xDFU, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U,
0x1AU, 0x0BU, 0x41U, 0x00U, 0x10U, 0x89U, 0x80U, 0x80U, 0x80U, 0x00U,
0x22U, 0x02U, 0x3CU, 0x00U, 0xB2U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U,
0x00U, 0x20U, 0x02U, 0x42U, 0x08U, 0x88U, 0x3CU, 0x00U, 0xB1U, 0x8BU,
0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U, 0x42U, 0x10U, 0x88U,
0x3CU, 0x00U, 0xB0U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U,
0x02U, 0x42U, 0x18U, 0x88U, 0x3CU, 0x00U, 0xAFU, 0x8BU, 0x80U, 0x80U,
0x00U, 0x41U, 0x00U, 0x20U, 0x02U, 0xA7U, 0x41U, 0x01U, 0x6AU, 0x22U,
0x03U, 0x3AU, 0x00U, 0xB8U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U,
0x20U, 0x03U, 0x41U, 0x08U, 0x76U, 0x3AU, 0x00U, 0xB7U, 0x8BU, 0x80U,
0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x03U, 0x41U, 0x10U, 0x76U, 0x3AU,
0x00U, 0xB6U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x03U,
0x41U, 0x18U, 0x76U, 0x3AU, 0x00U, 0xB5U, 0x8BU, 0x80U, 0x80U, 0x00U,
0x02U, 0x40U, 0x41U, 0xE7U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x14U,
0x10U, 0x83U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x14U, 0x51U, 0x0DU,
0x00U, 0x41U, 0x98U, 0x8AU, 0x80U, 0x80U, 0x00U, 0x41U, 0x24U, 0x42U,
0xE8U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU,
0x02U, 0x40U, 0x41U, 0xFBU, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x8AU,
0x01U, 0x10U, 0x8AU, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x8AU, 0x01U,
0x51U, 0x0DU, 0x00U, 0x41U, 0xCBU, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U,
0x24U, 0x42U, 0xE9U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U,
0x1AU, 0x0BU, 0x02U, 0x40U, 0x41U, 0xA0U, 0x8BU, 0x80U, 0x80U, 0x00U,
0x41U, 0xE5U, 0x01U, 0x10U, 0x8BU, 0x80U, 0x80U, 0x80U, 0x00U, 0x22U,
0x02U, 0x42U, 0x00U, 0x55U, 0x0DU, 0x00U, 0x41U, 0xD9U, 0x8AU, 0x80U,
0x80U, 0x00U, 0x41U, 0x08U, 0x42U, 0xEBU, 0x00U, 0x10U, 0x81U, 0x80U,
0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x41U, 0x00U, 0x20U, 0x02U, 0x3CU,
0x00U, 0xC1U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U,
0x42U, 0x08U, 0x88U, 0x3CU, 0x00U, 0xC0U, 0x8BU, 0x80U, 0x80U, 0x00U,
0x41U, 0x00U, 0x20U, 0x02U, 0x42U, 0x10U, 0x88U, 0x3CU, 0x00U, 0xBFU,
0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U, 0x42U, 0x18U,
0x88U, 0x3CU, 0x00U, 0xBEU, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U,
0x20U, 0x02U, 0x42U, 0x20U, 0x88U, 0x3CU, 0x00U, 0xBDU, 0x8BU, 0x80U,
0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U, 0x42U, 0x28U, 0x88U, 0x3CU,
0x00U, 0xBCU, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U,
0x42U, 0x30U, 0x88U, 0x3CU, 0x00U, 0xBBU, 0x8BU, 0x80U, 0x80U, 0x00U,
0x41U, 0x00U, 0x20U, 0x02U, 0x42U, 0x38U, 0x88U, 0xA7U, 0x41U, 0x3FU,
0x71U, 0x41U, 0xC0U, 0x00U, 0x72U, 0x3AU, 0x00U, 0xBAU, 0x8BU, 0x80U,
0x80U, 0x00U, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0x20U, 0x41U, 0xA0U,
0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0xE5U, 0x01U, 0x10U, 0x8CU, 0x80U,
0x80U, 0x80U, 0x00U, 0x42U, 0x20U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xAFU,
0x89U, 0x80U, 0x80U, 0x00U, 0x41U, 0x26U, 0x42U, 0xEFU, 0x00U, 0x10U,
0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x41U, 0x00U, 0x41U,
0x00U, 0x42U, 0x00U, 0x10U, 0x87U, 0x80U, 0x80U, 0x80U, 0x00U, 0x21U,
0x02U, 0x20U, 0x01U, 0x41U, 0x20U, 0x6AU, 0x24U, 0x80U, 0x80U, 0x80U,
0x80U, 0x00U, 0x20U, 0x02U, 0x0BU, 0x0BU, 0x85U, 0x05U, 0x02U, 0x00U,
0x41U, 0x80U, 0x08U, 0x0BU, 0xF1U, 0x02U, 0x6FU, 0x74U, 0x78U, 0x6EU,
0x5FU, 0x66U, 0x69U, 0x65U, 0x6CU, 0x64U, 0x28U, 0x30U, 0x2CU, 0x20U,
0x30U, 0x2CU, 0x20U, 0x73U, 0x66U, 0x4CU, 0x65U, 0x64U, 0x67U, 0x65U,
0x72U, 0x53U, 0x65U, 0x71U, 0x75U, 0x65U, 0x6EU, 0x63U, 0x65U, 0x29U,
0x20U, 0x3DU, 0x3DU, 0x20U, 0x44U, 0x4FU, 0x45U, 0x53U, 0x4EU, 0x54U,
0x5FU, 0x45U, 0x58U, 0x49U, 0x53U, 0x54U, 0x00U, 0x6FU, 0x74U, 0x78U,
0x6EU, 0x5FU, 0x74U, 0x79U, 0x70U, 0x65U, 0x28U, 0x29U, 0x20U, 0x3DU,
0x3DU, 0x20U, 0x74U, 0x74U, 0x49U, 0x4EU, 0x56U, 0x4FU, 0x4BU, 0x45U,
0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, 0x61U,
0x69U, 0x6CU, 0x73U, 0x28U, 0x45U, 0x4DU, 0x49U, 0x54U, 0x5FU, 0x4FU,
0x55U, 0x54U, 0x2CU, 0x20U, 0x31U, 0x33U, 0x38U, 0x55U, 0x29U, 0x20U,
0x3DU, 0x3DU, 0x20U, 0x31U, 0x33U, 0x38U, 0x00U, 0x6FU, 0x74U, 0x78U,
0x6EU, 0x5FU, 0x69U, 0x64U, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U,
0x69U, 0x64U, 0x29U, 0x2CU, 0x20U, 0x30U, 0x29U, 0x20U, 0x3DU, 0x3DU,
0x20U, 0x33U, 0x32U, 0x00U, 0x73U, 0x74U, 0x61U, 0x74U, 0x65U, 0x5FU,
0x73U, 0x65U, 0x74U, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U, 0x69U,
0x64U, 0x29U, 0x2CU, 0x20U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U, 0x6BU,
0x65U, 0x79U, 0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U,
0x00U, 0x65U, 0x6DU, 0x69U, 0x74U, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U,
0x28U, 0x65U, 0x6DU, 0x69U, 0x74U, 0x68U, 0x61U, 0x73U, 0x68U, 0x29U,
0x2CU, 0x20U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U, 0x74U, 0x78U, 0x6EU,
0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U, 0x72U,
0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U, 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU,
0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x28U, 0x31U,
0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U, 0x00U, 0x6FU, 0x74U, 0x78U,
0x6EU, 0x5FU, 0x66U, 0x69U, 0x65U, 0x6CU, 0x64U, 0x28U, 0x53U, 0x42U,
0x55U, 0x46U, 0x28U, 0x61U, 0x63U, 0x63U, 0x29U, 0x2CU, 0x20U, 0x73U,
0x66U, 0x41U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x29U, 0x20U,
0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U, 0x00U, 0x68U, 0x6FU, 0x6FU, 0x6BU,
0x5FU, 0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x28U, 0x41U,
0x43U, 0x43U, 0x4FU, 0x55U, 0x4EU, 0x54U, 0x5FU, 0x4FU, 0x55U, 0x54U,
0x2CU, 0x20U, 0x32U, 0x30U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U,
0x30U, 0x00U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x61U, 0x63U, 0x63U,
0x6FU, 0x75U, 0x6EU, 0x74U, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U,
0x6DU, 0x65U, 0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U,
0x00U, 0x66U, 0x65U, 0x65U, 0x20U, 0x3EU, 0x20U, 0x30U, 0x00U, 0x41U,
0x43U, 0x43U, 0x5FU, 0x45U, 0x51U, 0x28U, 0x61U, 0x63U, 0x63U, 0x2CU,
0x20U, 0x6DU, 0x65U, 0x29U, 0x00U, 0x00U, 0x41U, 0x80U, 0x0BU, 0x0BU,
0x85U, 0x02U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x01U, 0x12U, 0x00U, 0x63U, 0x22U, 0x00U, 0x00U,
0x00U, 0x00U, 0x24U, 0x00U, 0x00U, 0x00U, 0x00U, 0x20U, 0x1AU, 0x00U,
0x00U, 0x00U, 0x00U, 0x20U, 0x1BU, 0x00U, 0x00U, 0x00U, 0x00U, 0x68U,
0x40U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x73U, 0x21U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x81U, 0x14U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
0x00U, 0x00U, 0x00U,
}},
};
}
} // namespace ripple
+312 -9
View File
@@ -22,6 +22,7 @@
#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>
@@ -553,9 +554,99 @@ 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))) ==
"tecDUPLICATE");
"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())));
}
void
@@ -880,6 +971,11 @@ 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());
@@ -895,33 +991,237 @@ 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);
// Everything is offered: two pinned plus two under the gossip limit.
// Only the pinned two are offered. The other two are on the ledger,
// where any peer that is not amendment blocked can read them.
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 == 4);
BEAST_EXPECT(offered.size() == 4);
BEAST_EXPECT(reserved == 2);
BEAST_EXPECT(
hash_set<PublicKey>(offered.begin(), offered.end()) == all);
hash_set<PublicKey>(offered.begin(), offered.end()) ==
hash_set<PublicKey>({masters[0].pk(), masters[1].pk()}));
// A pinned key with no manifest is counted in the reservation but not
// offered, since the reservation is only an upper bound.
// Unpinned, a master key falls back to the lookup cache. A pinned key
// with no manifest is neither offered nor counted.
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 == 5);
BEAST_EXPECT(offered.size() == 4);
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);
}
void
@@ -961,10 +1261,13 @@ public:
testRetrieval(sa);
testSigningKeyRetrieval(sa);
testMalformed(sa);
testAccountDelete(sa);
testEnvelopeRejections(sa);
testCanonicalFee(sa);
testCorruptLedger(sa);
testGossipSelection(sa);
testLedgerOutranksGossip(sa);
testDirectory(sa);
testDisabled(sa);
}
};
+18 -2
View File
@@ -5055,7 +5055,15 @@ public:
run() override
{
using namespace test::jtx;
FeatureBitset const all{supported_amendments() - featureXahauGenesis};
// 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};
testQueueSeq(all);
testQueueTicket(all);
testTecResult(all);
@@ -5082,7 +5090,15 @@ public:
runMetaInfo()
{
using namespace test::jtx;
FeatureBitset const all{supported_amendments() - featureXahauGenesis};
// 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};
testAcctInQueueButEmpty(all);
testRPC(all);
testExpirationReplacement(all);
+425 -6
View File
@@ -18,6 +18,7 @@
#include <test/jtx.h>
#include <test/jtx/envconfig.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/misc/AmendmentTable.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/XahauGenesis.h>
@@ -28,6 +29,7 @@
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <limits>
#include <string>
#include <vector>
@@ -4676,11 +4678,12 @@ struct XahauGenesis_test : public beast::unit_test::suite
// validate account fields
STAmount const postUser = preUser + netReward;
bool const has240819 = env.current()->rules().enabled(fix240819);
bool const has20261005 = env.current()->rules().enabled(fix20261005);
BEAST_EXPECT(expectAccountFields(
env,
user,
preLedger,
has240819 ? preLedger : preLedger + 1,
(has240819 && !has20261005) ? preLedger : preLedger + 1,
has240819 ? (preUser - feesXRP) : postUser,
preTime));
}
@@ -5567,8 +5570,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
env.close();
// calculate rewards
bool const hasFix = env.current()->rules().enabled(fix240819) &&
env.current()->rules().enabled(fix240911);
bool const hasFix = env.current()->rules().enabled(fix240819);
STAmount const netReward =
rewardUserAmount(*acctSle, preLedger, rateDrops);
BEAST_EXPECT(netReward == (hasFix ? XRP(3.329999) : XRP(3.329999)));
@@ -5659,8 +5661,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
env.close();
// calculate rewards
bool const hasFix = env.current()->rules().enabled(fix240819) &&
env.current()->rules().enabled(fix240911);
bool const hasFix = env.current()->rules().enabled(fix240819);
STAmount const netReward =
rewardUserAmount(*acctSle, preLedger, rateDrops);
BEAST_EXPECT(netReward == (hasFix ? XRP(3.479999) : XRP(6.663333)));
@@ -5678,9 +5679,196 @@ struct XahauGenesis_test : public beast::unit_test::suite
postUser == (hasFix ? XRP(2002.479999) : XRP(2005.663333)));
}
void
test20261005(FeatureBitset features)
{
using namespace jtx;
using namespace std::chrono_literals;
testcase("test fix20261005");
bool const has240819 = features[fix240819];
bool const has20261005 = features[fix20261005];
bool const nofix = !(has240819 && has20261005);
Env env{
*this, makeNetworkConfig(21337), features - featureXahauGenesis};
STAmount const feesXRP = XRP(1);
Account const user = Account("user");
env.fund(XRP(200), user);
env.close();
// setup governance
Account const alice = Account("alice");
Account const bob = Account("bob");
Account const carol = Account("carol");
Account const david = Account("david");
Account const edward = Account("edward");
env.fund(XRP(10000), alice, bob, carol, david, edward);
env.close();
std::vector<AccountID> initial_members_ids{
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
setupGov(env, initial_members_ids);
{
std::vector<uint8_t> vote_data{
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
updateTopic(
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
}
BEAST_EXPECT(hasUNLReport(env) == false);
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
env.close();
env(noop(user),
fee(env.balance(user) - drops(500'000)),
ter(tesSUCCESS));
env.close();
BEAST_EXPECT(env.balance(user) == drops(500'000));
BEAST_EXPECT(env.balance(user) < XRP(1));
std::uint32_t const drainMarker =
env.le(keylet::account(user))->getFieldU32(sfRewardLgrLast);
for (int i = 0; i < 20; ++i)
env.close(10s);
BEAST_EXPECT(env.balance(user) < XRP(1));
env(pay(alice, user, XRP(1000)));
env.close();
std::uint32_t const fundMarker =
env.le(keylet::account(user))->getFieldU32(sfRewardLgrLast);
bool const markerFrozen = has240819 && !has20261005;
if (markerFrozen)
BEAST_EXPECT(fundMarker == drainMarker);
else
BEAST_EXPECT(fundMarker > drainMarker);
STAmount const preUser = env.balance(user);
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
env.close();
env.close();
STAmount const postUser = env.balance(user);
if (nofix)
BEAST_EXPECT(postUser > preUser + XRP(1));
else
BEAST_EXPECT(postUser < preUser + XRP(1));
}
void
testRewardAccumulatorOverflow(FeatureBitset features)
{
using namespace jtx;
testcase("reward accumulator overflow");
bool const has20261005 = features[fix20261005];
Env env{
*this, makeNetworkConfig(21337), features - featureXahauGenesis};
STAmount const feesXRP = XRP(1);
Account const user = Account("user");
env.fund(XRP(10000), user);
env.close();
// setup governance
Account const alice = Account("alice");
Account const bob = Account("bob");
Account const carol = Account("carol");
Account const david = Account("david");
Account const edward = Account("edward");
env.fund(XRP(10000), alice, bob, carol, david, edward);
env.close();
std::vector<AccountID> initial_members_ids{
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
setupGov(env, initial_members_ids);
{
std::vector<uint8_t> vote_data{
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
updateTopic(
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
}
// opt in so the account carries the reward fields
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
env.close();
env.close();
auto const preSle = env.le(keylet::account(user));
BEAST_REQUIRE(preSle);
BEAST_REQUIRE(preSle->isFieldPresent(sfRewardLgrLast));
BEAST_REQUIRE(preSle->isFieldPresent(sfRewardAccumulator));
std::uint32_t const preMarker = preSle->getFieldU32(sfRewardLgrLast);
BEAST_EXPECT(preMarker < env.current()->seq());
constexpr std::uint64_t maxAccum =
std::numeric_limits<std::uint64_t>::max();
// Push the accumulator to the brink in the open ledger so that any
// non-zero bal * lgrElapsed wraps around in finishApply.
env.app().openLedger().modify(
[&](OpenView& view, beast::Journal) -> bool {
auto const sle = view.read(keylet::account(user));
if (!sle)
return false;
auto replacement = std::make_shared<SLE>(*sle);
replacement->setFieldU64(sfRewardAccumulator, maxAccum);
view.rawReplace(replacement);
return true;
});
BEAST_EXPECT(
env.le(keylet::account(user))->getFieldU64(sfRewardAccumulator) ==
maxAccum);
// Any balance change on user runs the reward block in finishApply.
env(pay(alice, user, XRP(1)), ter(tesSUCCESS));
// Check the open ledger before close() rebuilds it without the
// injected accumulator.
auto const postSle = env.le(keylet::account(user));
BEAST_REQUIRE(postSle);
// the overflowing contribution is never added
BEAST_EXPECT(postSle->getFieldU64(sfRewardAccumulator) == maxAccum);
// with the fix the marker advances so the span can't be reused;
// without it the marker is left behind
if (has20261005)
BEAST_EXPECT(
postSle->getFieldU32(sfRewardLgrLast) == env.current()->seq());
else
BEAST_EXPECT(postSle->getFieldU32(sfRewardLgrLast) == preMarker);
env.close();
}
void
testRewardHookWithFeats(FeatureBitset features)
{
test20261005(features);
testRewardAccumulatorOverflow(features);
testLastCloseTime(features);
testInvalidRate0(features);
testInvalidRateGreater1(features);
@@ -5704,6 +5892,236 @@ struct XahauGenesis_test : public beast::unit_test::suite
testNoClaimLate(features);
}
// inject an enabling ttAMENDMENT pseudo for `amendment` and close
void
injectAmendment(jtx::Env& env, uint256 const& amendment)
{
uint32_t const seq =
env.app().getLedgerMaster().getClosedLedger()->info().seq + 1;
env.app().openLedger().modify(
[&](OpenView& view, beast::Journal j) -> bool {
STTx tx(ttAMENDMENT, [&](auto& obj) {
obj.setAccountID(sfAccount, AccountID());
obj.setFieldH256(sfAmendment, amendment);
obj.setFieldU32(sfLedgerSequence, seq);
});
uint256 txID = tx.getTransactionID();
auto s = std::make_shared<ripple::Serializer>();
tx.add(*s);
env.app().getHashRouter().setFlags(txID, SF_PRIVATE2);
view.rawTxInsert(txID, std::move(s), nullptr);
return true;
});
env.close();
}
// check the genesis account is in the shape fixXahauTestnetGenesis
// leaves it in: blackholed, governance + reward hooks, Evernode-only seat,
// no hook state
void
checkTestnetGenesis(jtx::Env& env, AccountID const& evernode)
{
auto const acc = env.le(keylet::account(genesisAccID));
BEAST_REQUIRE(!!acc);
BEAST_EXPECT(acc->getAccountID(sfRegularKey) == noAccount());
BEAST_EXPECT(acc->getFieldU32(sfFlags) == lsfDisableMaster);
BEAST_EXPECT(!acc->isFieldPresent(sfHookStateCount));
BEAST_EXPECT(!acc->isFieldPresent(sfHookNamespaces));
// governance: 1 + IRR, IRD, IS\0, IMC; reward: 1
BEAST_EXPECT(acc->getFieldU32(sfOwnerCount) == 6);
BEAST_EXPECT(!env.le(keylet::signers(genesisAccID)));
BEAST_EXPECT(!env.le(keylet::hookStateDir(genesisAccID, beast::zero)));
auto const hooksLE = env.le(keylet::hook(genesisAccID));
BEAST_REQUIRE(!!hooksLE);
auto const& hooks = hooksLE->getFieldArray(sfHooks);
BEAST_REQUIRE(hooks.size() == 2);
BEAST_EXPECT(hooks[0].getFieldH256(sfHookHash) == governHookHash);
BEAST_EXPECT(hooks[1].getFieldH256(sfHookHash) == rewardHookHash);
std::vector<std::pair<std::vector<uint8_t>, std::vector<uint8_t>>>
expected{XahauGenesis::GovernanceParameters};
expected.emplace_back(
std::vector<uint8_t>{'I', 'S', 0},
std::vector<uint8_t>(evernode.data(), evernode.data() + 20));
expected.emplace_back(
std::vector<uint8_t>{'I', 'M', 'C'}, std::vector<uint8_t>{1});
auto const& params = hooks[0].getFieldArray(sfHookParameters);
BEAST_REQUIRE(params.size() == expected.size());
for (std::size_t i = 0; i < expected.size(); ++i)
{
BEAST_EXPECT(
params[i].getFieldVL(sfHookParameterName) == expected[i].first);
BEAST_EXPECT(
params[i].getFieldVL(sfHookParameterValue) ==
expected[i].second);
}
BEAST_EXPECT(hooks[1].getFieldArray(sfHookParameters).empty());
for (auto const& h : {governHookHash, rewardHookHash})
{
auto const def = env.le(keylet::hookDefinition(h));
BEAST_EXPECT(def && def->getFieldU64(sfReferenceCount) == 1);
}
}
void
testXahauTestnetGenesisFix(FeatureBitset features)
{
using namespace jtx;
testcase("fixXahauTestnetGenesis");
auto const evernodePK = parseBase58<PublicKey>(
TokenType::NodePublic,
"nHUetpkzTgf8c6A1DTGemRrhL4nMjsiJrtcsmmg75ufzV5EDMbWU");
BEAST_REQUIRE(!!evernodePK);
AccountID const evernode = calcAccountID(*evernodePK);
auto stateAt = [&](Env& env, std::vector<uint8_t> const& k) {
uint8_t key[32]{};
std::copy(k.begin(), k.end(), key + 32 - k.size());
return env.le(keylet::hookState(
genesisAccID, uint256::fromVoid(key), beast::zero));
};
std::vector<uint8_t> const mcKey{'M', 'C'};
// only supported on 21338
{
Env env{*this, makeNetworkConfig(21337), features};
BEAST_EXPECT(!env.app().getAmendmentTable().isSupported(
fixXahauTestnetGenesis));
}
{
Env env{*this, makeNetworkConfig(21338), features};
BEAST_EXPECT(env.app().getAmendmentTable().isSupported(
fixXahauTestnetGenesis));
}
// wrongly seated governance with stale hook state gets reset
{
Env env{
*this,
makeNetworkConfig(21338),
features - featureXahauGenesis};
auto const alice = Account("alice");
auto const bob = Account("bob");
auto const invoker = Account("invoker");
auto const user = Account("user");
env.fund(XRP(10000), alice, bob, invoker, user);
env.close();
XahauGenesis::TestNonGovernanceDistribution.clear();
XahauGenesis::TestL1Membership.clear();
XahauGenesis::TestL2Membership.clear();
XahauGenesis::TestL1Membership.emplace_back(
alice.human(), XRPAmount(10000000000));
XahauGenesis::TestL1Membership.emplace_back(
bob.human(), XRPAmount(10000000000));
activate(__LINE__, env, true, false, true, {}, 2 + 3 + 2);
XahauGenesis::TestL1Membership.clear();
Json::Value invoke;
invoke[jss::TransactionType] = "Invoke";
invoke[jss::Account] = invoker.human();
invoke[jss::Destination] = env.master.human();
env(invoke, fee(XRP(10)));
env.close();
{
auto const mc = stateAt(env, mcKey);
BEAST_REQUIRE(!!mc);
BEAST_EXPECT(
mc->getFieldVL(sfHookStateData) == std::vector<uint8_t>{2});
}
injectAmendment(env, fixXahauTestnetGenesis);
BEAST_EXPECT(
env.current()->rules().enabled(fixXahauTestnetGenesis));
checkTestnetGenesis(env, evernode);
BEAST_EXPECT(!stateAt(env, mcKey));
// governance re-initialises from the new parameters
env(invoke, fee(XRP(10)));
env.close();
{
auto const mc = stateAt(env, mcKey);
BEAST_REQUIRE(!!mc);
BEAST_EXPECT(
mc->getFieldVL(sfHookStateData) == std::vector<uint8_t>{1});
auto const seat = stateAt(
env,
std::vector<uint8_t>(
evernode.data(), evernode.data() + 20));
BEAST_EXPECT(
seat &&
seat->getFieldVL(sfHookStateData) ==
std::vector<uint8_t>{0});
BEAST_EXPECT(!stateAt(
env,
std::vector<uint8_t>(
alice.id().data(), alice.id().data() + 20)));
}
// alice no longer holds a seat
env(vote(
__LINE__,
alice,
env.master,
'R',
'R',
{1, 2, 3, 4, 5, 6, 7, 8}),
fee(XRP(1)),
ter(tecHOOK_REJECTED));
env.close();
// reward claim reaches the reward hook
env(claimReward(user, env.master), fee(XRP(1)), ter(tesSUCCESS));
env.close();
// enabling it again is refused and nothing is reset
auto const ownerCount = env.le(keylet::account(genesisAccID))
->getFieldU32(sfOwnerCount);
injectAmendment(env, fixXahauTestnetGenesis);
BEAST_EXPECT(stateAt(env, mcKey));
BEAST_EXPECT(
env.le(keylet::account(genesisAccID))
->getFieldU32(sfOwnerCount) == ownerCount);
}
// never-configured genesis with a regular key, signer list and a
// foreign hook gets configured and blackholed
{
Env env{*this, makeNetworkConfig(21338), features};
auto const alice = Account("alice");
auto const user = Account("user");
env.fund(XRP(10000), alice, user);
env.memoize(env.master);
env(genesis::setAcceptHook(env.master), fee(XRP(10)));
env(signers(env.master, 1, {{alice, 1}}));
env(regkey(env.master, alice));
env.close();
BEAST_EXPECT(env.le(keylet::hookDefinition(acceptHookHash)));
injectAmendment(env, fixXahauTestnetGenesis);
checkTestnetGenesis(env, evernode);
BEAST_EXPECT(!env.le(keylet::hookDefinition(acceptHookHash)));
env(claimReward(user, env.master), fee(XRP(1)), ter(tesSUCCESS));
env.close();
}
// never touches state off 21338 even if enabled
{
Env env{*this, makeNetworkConfig(21337), features};
injectAmendment(env, fixXahauTestnetGenesis);
BEAST_EXPECT(!env.le(keylet::hook(genesisAccID)));
}
}
void
testGovernHookWithFeats(FeatureBitset features)
{
@@ -5713,6 +6131,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
testWithRegularKey(features);
testGovernanceL1(features);
testGovernanceL2(features);
testXahauTestnetGenesisFix(features);
}
void
@@ -5723,7 +6142,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
testGovernHookWithFeats(sa);
testRewardHookWithFeats(sa);
testRewardHookWithFeats(sa - fix240819);
testRewardHookWithFeats(sa - fix240819 - fix240911);
testRewardHookWithFeats(sa - fix20261005);
}
};
@@ -0,0 +1,79 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpl/basics/ReaderPreferringSharedMutex.h>
#include <xrpl/beast/unit_test.h>
#include <mutex>
#include <shared_mutex>
namespace ripple {
namespace test {
class ReaderPreferringSharedMutex_test : public beast::unit_test::suite
{
void
testExclusiveAndShared()
{
testcase("exclusive and shared locking");
ReaderPreferringSharedMutex mutex;
BEAST_EXPECT(mutex.try_lock());
BEAST_EXPECT(!mutex.try_lock());
BEAST_EXPECT(!mutex.try_lock_shared());
mutex.unlock();
mutex.lock_shared();
BEAST_EXPECT(mutex.try_lock_shared());
BEAST_EXPECT(!mutex.try_lock());
mutex.unlock_shared();
mutex.unlock_shared();
{
std::shared_lock shared{mutex};
BEAST_EXPECT(!mutex.try_lock());
BEAST_EXPECT(mutex.try_lock_shared());
mutex.unlock_shared();
}
{
std::unique_lock exclusive{mutex};
BEAST_EXPECT(!mutex.try_lock());
BEAST_EXPECT(!mutex.try_lock_shared());
}
mutex.lock();
BEAST_EXPECT(!mutex.try_lock_shared());
mutex.unlock();
BEAST_EXPECT(mutex.try_lock_shared());
mutex.unlock_shared();
}
void
run() override
{
testExclusiveAndShared();
}
};
BEAST_DEFINE_TESTSUITE(ReaderPreferringSharedMutex, ripple_basics, ripple);
} // namespace test
} // namespace ripple
+11
View File
@@ -59,6 +59,17 @@ public:
testUnHexFailure("123X");
testUnHexFailure("V");
testUnHexFailure("XAH");
// Bytes >= 0x80 (negative when char is signed) must be rejected, not
// used to index the lookup table out of bounds
for (int b = 0x80; b <= 0xff; ++b)
{
std::string const c(1, static_cast<char>(b));
testUnHexFailure(c);
testUnHexFailure("0" + c);
testUnHexFailure(c + "0");
testUnHexFailure("00" + c + "00");
}
}
void
+91
View File
@@ -462,11 +462,102 @@ class base58_test : public beast::unit_test::suite
}
}
void
testDecodeInvalidChars()
{
testcase("decode_invalid_chars");
// Every byte outside the base58 alphabet, including bytes >= 0x80
// (negative when char is signed), must be rejected at any position.
// The fast decoder reads the leading `size % 10` characters and the
// remaining characters in two separate loops; varying the length and
// the position of the bad byte exercises both.
constexpr std::string_view alphabet =
"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz";
std::array<bool, 256> inAlphabet{};
for (unsigned char const c : alphabet)
inAlphabet[c] = true;
auto const invalidCode =
make_error_code(TokenCodecErrc::invalidEncodingChar);
// 52 is the longest input the fast decoder accepts
for (std::size_t len = 1; len <= 52; ++len)
{
std::string base;
for (std::size_t i = 0; i < len; ++i)
base.push_back(alphabet[(i * 7 + len) % alphabet.size()]);
for (std::size_t pos = 0; pos < len; ++pos)
{
for (int b = 0; b < 256; ++b)
{
if (inAlphabet[b])
continue;
std::string s = base;
s[pos] = static_cast<char>(b);
std::array<std::uint8_t, 64> outBuf;
auto const r = b58_fast::detail::b58_to_b256_be(s, outBuf);
BEAST_EXPECT(!r && r.error() == invalidCode);
BEAST_EXPECT(b58_ref::detail::decodeBase58(s).empty());
}
}
}
// Same through the token API, using a valid AccountID as the base
constexpr TokenType tokType = TokenType::AccountID;
std::string const addr = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
BEAST_EXPECT(!decodeBase58Token(addr, tokType).empty());
for (std::size_t pos = 0; pos < addr.size(); ++pos)
{
for (int b = 0; b < 256; ++b)
{
if (inAlphabet[b])
continue;
std::string s = addr;
s[pos] = static_cast<char>(b);
std::array<std::uint8_t, 64> outBuf;
auto const r = b58_fast::decodeBase58Token(tokType, s, outBuf);
BEAST_EXPECT(!r && r.error() == invalidCode);
BEAST_EXPECT(b58_ref::decodeBase58Token(s, tokType).empty());
BEAST_EXPECT(decodeBase58Token(s, tokType).empty());
}
}
}
void
testDecodeMaxValue()
{
testcase("decode_max_value");
// The largest value of every accepted length ('z' is digit 57) must
// decode identically in both implementations. At 52 chars this is the
// widest intermediate the fast decoder's bigint ever has to hold.
for (std::size_t len = 1; len <= 52; ++len)
{
std::string const s(len, 'z');
std::array<std::uint8_t, 64> outBuf;
auto const r = b58_fast::detail::b58_to_b256_be(s, outBuf);
std::string const ref = b58_ref::detail::decodeBase58(s);
if (!BEAST_EXPECT(r))
continue;
BEAST_EXPECT(
std::string(r.value().begin(), r.value().end()) == ref);
}
}
void
run() override
{
testMultiprecision();
testFastMatchesRef();
testDecodeInvalidChars();
testDecodeMaxValue();
}
};
+62
View File
@@ -1052,6 +1052,67 @@ 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
{
@@ -1068,6 +1129,7 @@ public:
testHubNetwork();
testPreferredByBranch();
testPauseForLaggards();
testForcedInclusion();
}
};
+91
View File
@@ -1203,6 +1203,96 @@ class UNLReportVoteNewValidator_test : public beast::unit_test::suite
}
};
/*
* Regression test: UNLReport proposals must not be gated on the N-UNL local
* reliability threshold.
*
* The local node (validator 0) validates exactly `myVals` of the 256 scored
* ledgers, validator 1 validates none (so it is an N-UNL disable candidate and
* absent from the report), every other validator validates all of them.
*
* == myVals <= 128: not active ourselves -> no UNLReport, no UNLModify
* == 129 .. 229: UNLReport proposed, not reliable enough to vote on N-UNL
* == >= 230: UNLReport and one UNLModify (230 used to fall into the
* "Too many!" branch and fail)
*/
class UNLReportVoteLocalReliability_test : public beast::unit_test::suite
{
void
testDoVoting()
{
testcase("Do Voting with local reliability below N-UNL threshold");
struct Case
{
std::uint32_t myVals;
bool expectReport;
bool expectModify;
};
std::array<Case, 6> const cases = {{
{128, false, false},
{129, true, false},
{229, true, false},
{230, true, true},
{231, true, true},
{256, true, true},
}};
for (bool const withVLImport : {true, false})
{
for (auto const& c : cases)
{
std::uint32_t const numNodes = 20;
URNetworkHistory history = {
*this, {numNodes, 0, false, false, withVLImport, {}}};
BEAST_EXPECT(history.goodHistory);
if (!history.goodHistory)
continue;
// scored ledgers are [lastSeq - 256, lastSeq - 1]
auto const lastSeq = history.lastLedger()->seq();
history.walkHistoryAndAddValidations(
[&](std::shared_ptr<Ledger const> const& l,
std::size_t idx) -> bool {
if (idx == 1)
return false;
if (idx != 0)
return true;
return l->seq() + 256 < lastSeq + c.myVals;
});
NegativeUNLVote vote(
history.UNLNodeIDs[0],
history.env.journal,
history.env.app());
auto txSet = std::make_shared<SHAMap>(
SHAMapType::TRANSACTION, history.env.app().getNodeFamily());
vote.doVoting(
history.lastLedger(),
history.UNLKeySet,
history.validations,
txSet);
std::size_t const expectReport =
c.expectReport ? numNodes - 1 + (withVLImport ? 1 : 0) : 0;
std::size_t const expectModify = c.expectModify ? 1 : 0;
std::string const ctx = "myVals=" + std::to_string(c.myVals) +
" withVLImport=" + std::to_string(withVLImport);
BEAST_EXPECTS(countUNLRTx(txSet) == expectReport, ctx);
BEAST_EXPECTS(unl::countTx(txSet) == expectModify, ctx);
}
}
}
void
run() override
{
testDoVoting();
}
};
BEAST_DEFINE_TESTSUITE(UNLReport, ledger, ripple);
BEAST_DEFINE_TESTSUITE(UNLReportNoAmendment, ledger, ripple);
BEAST_DEFINE_TESTSUITE(UNLReportFork, consensus, ripple);
@@ -1215,6 +1305,7 @@ BEAST_DEFINE_TESTSUITE_PRIO(
ripple,
1);
BEAST_DEFINE_TESTSUITE(UNLReportVoteNewValidator, consensus, ripple);
BEAST_DEFINE_TESTSUITE(UNLReportVoteLocalReliability, consensus, ripple);
///////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////
+152 -29
View File
@@ -421,6 +421,72 @@ 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
testUdpHighway()
{
testcase("udp_highway");
// The UDP Superhighway is opt-in: off unless turned on.
{
Config c;
c.loadFromString("");
BEAST_EXPECT(!c.UDP_HIGHWAY);
}
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("[udp_highway]\n" + value + "\n");
BEAST_EXPECT(c.UDP_HIGHWAY == expected);
}
try
{
Config c;
c.loadFromString("[udp_highway]\nsometimes\n");
fail();
}
catch (std::exception const&)
{
pass();
}
}
void
testNetworkID()
{
@@ -1056,6 +1122,36 @@ trustthesevalidators.gov
}
}
// Standalone forces ledger_history to zero, but [node_db] type=rwdb
// (the null node store) sizes its retained-ledger window from it and
// SHAMapStoreImp throws on zero. cfg/rippled.cfg ships exactly this
// configuration, and jtx never calls Config::setup, so pin it here.
void
testSetupStandaloneRwdb()
{
testcase("setup standalone rwdb keeps ledger_history");
for (std::string const type : {"rwdb", "NuDB"})
{
detail::RippledCfgGuard const cfg(
*this,
"testSetupStandaloneRwdb_" + type,
"",
"",
true,
"[node_db]\ntype=" + type + "\npath=main\n");
Config config;
config.setup(
cfg.configFile(),
/*bQuiet*/ true,
/* bSilent */ true,
/* bStandalone */ true);
BEAST_EXPECT(config.standalone());
BEAST_EXPECT(config.nullBackend() == (type == "rwdb"));
BEAST_EXPECT(config.LEDGER_HISTORY == (type == "rwdb" ? 256 : 0));
}
}
void
testPort()
{
@@ -1102,23 +1198,24 @@ trustthesevalidators.gov
Config cfg;
/* NOTE: this string includes some explicit
* space chars in order to verify proper trimming */
std::string toLoad(R"(
std::string toLoad(
R"(
[port_rpc])"
"\x20"
R"(
"\x20"
R"(
# comment
# indented comment
)"
"\x20\x20"
R"(
"\x20\x20"
R"(
[ips])"
"\x20"
R"(
"\x20"
R"(
r.ripple.com 51235
[ips_fixed])"
"\x20\x20"
R"(
"\x20\x20"
R"(
# COMMENT
s1.ripple.com 51235
s2.ripple.com 51235
@@ -1144,23 +1241,24 @@ r.ripple.com 51235
Config cfg;
/* NOTE: this string includes some explicit
* space chars in order to verify proper trimming */
std::string toLoad(R"(
std::string toLoad(
R"(
[port_rpc])"
"\x20"
R"(
"\x20"
R"(
# comment
# indented comment
)"
"\x20\x20"
R"(
"\x20\x20"
R"(
[ips])"
"\x20"
R"(
"\x20"
R"(
r.ripple.com:51235
[ips_fixed])"
"\x20\x20"
R"(
"\x20\x20"
R"(
# COMMENT
s1.ripple.com:51235
s2.ripple.com 51235
@@ -1447,8 +1545,8 @@ r.ripple.com:51235
}
}
// Test 2: RWDB without online_delete NOT in standalone mode (should
// throw)
// Test 2: RWDB without online_delete NOT in standalone mode
// (now allowed — SHAMapStoreImp defaults it to ledger_history)
{
Config c;
std::string toLoad =
@@ -1459,16 +1557,12 @@ r.ripple.com:51235
try
{
c.loadFromString(toLoad);
fail("Expected exception for RWDB without online_delete");
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
std::string(e.what()).find(
"RWDB (in-memory backend) requires online_delete") !=
std::string::npos);
pass();
}
catch (std::runtime_error const&)
{
fail("Should not throw for RWDB without online_delete");
}
}
// Test 3: RWDB with online_delete NOT in standalone mode (should
@@ -1613,7 +1707,33 @@ r.ripple.com:51235
}
}
// Test 5: pinned_type without online_delete (should throw)
// Test 5: type=rwdb with pinned_type (should throw, even in
// standalone). The rwdb node store is tree-only, so pinned data
// would be accepted and then dropped.
for (bool const standalone : {false, true})
{
Config c;
std::string toLoad =
"[node_db]\n"
"type=rwdb\n"
"online_delete=256\n"
"pinned_type=NuDB\n"
"pinned_path=pinned\n";
c.setupControl(true, true, standalone);
try
{
c.loadFromString(toLoad);
fail("Expected exception for pinned_type with type=rwdb");
}
catch (std::runtime_error const& e)
{
BEAST_EXPECT(
std::string(e.what()).find("type=rwdb") !=
std::string::npos);
}
}
// Test 6: pinned_type without online_delete (should throw)
{
Config c;
std::string toLoad =
@@ -1723,6 +1843,7 @@ r.ripple.com:51235
testValidatorsFile();
testSetup(false);
testSetup(true);
testSetupStandaloneRwdb();
testPort();
testZeroPort();
testWhitespace();
@@ -1734,6 +1855,8 @@ r.ripple.com:51235
testDatabasePinnedValidation();
testOverlay();
testNetworkID();
testEarlyManifestSync();
testUdpHighway();
}
};
+15
View File
@@ -33,6 +33,8 @@
#include <boost/container/flat_map.hpp>
#include <boost/container/flat_set.hpp>
#include <algorithm>
#include <functional>
#include <optional>
namespace ripple {
namespace test {
@@ -257,6 +259,11 @@ 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;
@@ -655,6 +662,14 @@ 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,6 +25,7 @@
#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>
@@ -1234,6 +1235,90 @@ 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()
{
@@ -1289,6 +1374,7 @@ public:
testNFTokenPageInvariants();
testPermissionedDomainInvariants();
testLockedBalance();
testAppLoader();
}
};
+37
View File
@@ -66,6 +66,14 @@ public:
// Write the batch
storeBatch(*backend, batch);
// Durability on demand: RocksDB drains its write queue and
// syncs its WAL; in-memory backends have nothing to wait for;
// NuDB commits on its own thread and cannot promise it.
backend->store(batch.front());
bool const durable = backend->syncDurable();
BEAST_EXPECT(durable == (type != "nudb"));
backend->sync();
{
// Read it back in
Batch copy;
@@ -82,6 +90,35 @@ public:
}
}
if (type == "rwdb")
{
std::unique_ptr<Backend> backend = Manager::instance().make_Backend(
params, megabytes(4), scheduler, journal);
backend->open();
try
{
backend->open();
fail("rwdb open twice should throw");
}
catch (std::exception const&)
{
pass();
}
storeBatch(*backend, batch);
backend->close();
BEAST_EXPECT(!backend->isOpen());
std::shared_ptr<NodeObject> fetched;
BEAST_EXPECT(
backend->fetch(batch.front()->getHash().data(), &fetched) ==
notFound);
backend->store(batch.front());
int visited = 0;
backend->for_each([&visited](auto const&) { ++visited; });
BEAST_EXPECT(visited == 0);
}
// rwdb backend does not keep table/data after close
if (type != "rwdb")
{
+11 -5
View File
@@ -25,6 +25,7 @@
#include <xrpld/nodestore/detail/DatabasePinnedImp.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/protocol/digest.h>
namespace ripple {
@@ -33,13 +34,16 @@ namespace test {
class DatabasePinned_test : public beast::unit_test::suite
{
// Build an Env whose nodestore is wired up as DatabasePinnedImp.
// pinned_type=rwdb is allowed in standalone mode (the catalogue
// tests do the same).
// The rotating store must persist (type=memory). type=rwdb is
// NullFactory and never builds DatabasePinnedImp. pinned_type=rwdb
// is allowed in standalone mode.
static std::unique_ptr<Config>
pinnedEnvconfig()
pinnedEnvconfig(std::string const& nodePath)
{
auto cfg = jtx::envconfig();
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
nodeDb.set("type", "memory");
nodeDb.set("path", nodePath);
nodeDb.set("pinned_type", "rwdb");
nodeDb.set("online_delete", "256");
return cfg;
@@ -84,7 +88,8 @@ class DatabasePinned_test : public beast::unit_test::suite
testcase("store routing by NodeObjectType");
using namespace jtx;
Env env{*this, pinnedEnvconfig()};
beast::temp_dir td;
Env env{*this, pinnedEnvconfig(td.path())};
auto* db = getPinnedDb(env);
if (!db)
return;
@@ -136,7 +141,8 @@ class DatabasePinned_test : public beast::unit_test::suite
testcase("store/fetch round-trip across both backends");
using namespace jtx;
Env env{*this, pinnedEnvconfig()};
beast::temp_dir td;
Env env{*this, pinnedEnvconfig(td.path())};
auto& nodeStore = env.app().getNodeStore();
// Store one object of each type with distinct content so the
+305
View File
@@ -0,0 +1,305 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <xrpld/core/ConfigSections.h>
#include <xrpld/overlay/Compression.h>
#include <xrpld/overlay/Message.h>
#include <xrpld/overlay/detail/OverlayImpl.h>
#include <xrpld/overlay/detail/PeerImp.h>
#include <xrpld/shamap/SHAMapInnerNode.h>
#include <xrpld/shamap/SHAMapNodeID.h>
#include <xrpl/basics/make_SSLContext.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/messages.h>
#include <optional>
namespace ripple {
namespace test {
class GetObjectByHash_test : public beast::unit_test::suite
{
using socket_type = boost::asio::ip::tcp::socket;
using middle_type = boost::beast::tcp_stream;
using stream_type = boost::beast::ssl_stream<middle_type>;
class PeerTest : public PeerImp
{
public:
std::vector<std::shared_ptr<Message>> sent;
PeerTest(
Application& app,
std::shared_ptr<PeerFinder::Slot> const& slot,
http_request_type&& request,
PublicKey const& publicKey,
ProtocolVersion protocol,
Resource::Consumer consumer,
std::unique_ptr<GetObjectByHash_test::stream_type>&& stream_ptr,
OverlayImpl& overlay)
: PeerImp(
app,
1,
slot,
std::move(request),
publicKey,
protocol,
consumer,
std::move(stream_ptr),
overlay)
{
}
void
run() override
{
}
void
send(std::shared_ptr<Message> const& m) override
{
sent.push_back(m);
}
};
static auto
nullBackend(std::unique_ptr<Config> cfg)
{
cfg->LEDGER_HISTORY = 8;
auto& section = cfg->section(ConfigSection::nodeDatabase());
section.set("type", "rwdb");
section.set("path", "main");
return cfg;
}
std::shared_ptr<PeerTest>
makePeer(jtx::Env& env)
{
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
http_request_type request;
auto stream_ptr = std::make_unique<stream_type>(
socket_type(env.app().getIOContext()), *context_);
beast::IP::Endpoint const local(
boost::asio::ip::make_address("172.16.0.1"));
beast::IP::Endpoint const remote(
boost::asio::ip::make_address("172.16.0.2"));
PublicKey const key(std::get<0>(randomKeyPair(KeyType::ed25519)));
auto consumer = overlay.resourceManager().newInboundEndpoint(remote);
auto slot = overlay.peerFinder().new_inbound_slot(local, remote);
auto peer = std::make_shared<PeerTest>(
env.app(),
slot,
std::move(request),
key,
ProtocolVersion{1, 7},
consumer,
std::move(stream_ptr),
overlay);
overlay.add_active(peer);
return peer;
}
std::optional<protocol::TMGetObjectByHash>
parseReply(std::shared_ptr<Message> const& message)
{
if (!message)
return {};
auto const& buf = message->getBuffer(compression::Compressed::Off);
if (buf.size() <= compression::headerBytes)
return {};
protocol::TMGetObjectByHash reply;
if (!reply.ParseFromArray(
buf.data() + compression::headerBytes,
static_cast<int>(buf.size() - compression::headerBytes)))
return {};
return reply;
}
std::shared_ptr<boost::asio::ssl::context> context_ = make_SSLContext("");
public:
void
testServesLinkedAndCachedNodes()
{
testcase(
"GetObjectByHash serves cache, linked tree, and ledger header");
using namespace jtx;
Env env(*this, envconfig(nullBackend));
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env(pay(alice, bob, XRP(1)));
env.close();
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
if (!BEAST_EXPECT(ledger))
return;
auto const stateHash = ledger->stateMap().getHash().as_uint256();
auto const txHash = ledger->txMap().getHash().as_uint256();
auto const headerHash = ledger->info().hash;
auto const seq = ledger->info().seq;
auto const rootId = SHAMapNodeID{}.getRawString();
auto peer = makePeer(env);
auto sendQuery = [&](auto&& fill) {
peer->sent.clear();
auto packet = std::make_shared<protocol::TMGetObjectByHash>();
packet->set_query(true);
fill(*packet);
// Production dispatch resets fee_ in onMessageBegin before
// onMessage. Calling onMessage alone leaves a moderate burden
// charge in place and Debug XRPL_ASSERT fires on a later
// malformed request with a smaller fee.
auto const size = packet->ByteSizeLong();
peer->onMessageBegin(
protocol::mtGET_OBJECTS, packet, size, size, false);
peer->onMessage(packet);
peer->onMessageEnd(protocol::mtGET_OBJECTS, packet);
};
// TreeNodeCache hit: hash only, no nodeid.
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
auto& obj = *packet.add_objects();
obj.set_hash(stateHash.begin(), stateHash.size());
obj.set_ledgerseq(seq);
});
if (auto const cached =
env.app().getNodeFamily().getTreeNodeCache()->fetch(stateHash))
{
if (!BEAST_EXPECT(peer->sent.size() == 1))
return;
auto const reply = parseReply(peer->sent.front());
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
return;
BEAST_EXPECT(uint256{reply->objects(0).hash()} == stateHash);
}
// After dropping the interned cache, the same request needs a nodeid
// so fetchLinkedTreeNode can walk the retained SHAMap.
env.app().getNodeFamily().getTreeNodeCache()->reset();
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
auto& obj = *packet.add_objects();
obj.set_hash(stateHash.begin(), stateHash.size());
obj.set_ledgerseq(seq);
});
{
if (!BEAST_EXPECT(peer->sent.size() == 1))
return;
auto const reply = parseReply(peer->sent.front());
if (!BEAST_EXPECT(reply))
return;
BEAST_EXPECT(reply->objects_size() == 0);
}
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
auto& obj = *packet.add_objects();
obj.set_hash(stateHash.begin(), stateHash.size());
obj.set_nodeid(rootId);
obj.set_ledgerseq(seq);
});
{
if (!BEAST_EXPECT(peer->sent.size() == 1))
return;
auto const reply = parseReply(peer->sent.front());
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
return;
BEAST_EXPECT(uint256{reply->objects(0).hash()} == stateHash);
BEAST_EXPECT(reply->objects(0).index() == rootId);
BEAST_EXPECT(reply->objects(0).ledgerseq() == seq);
}
// Transaction map root, found after the state-map miss.
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otTRANSACTION_NODE);
auto& obj = *packet.add_objects();
obj.set_hash(txHash.begin(), txHash.size());
obj.set_nodeid(rootId);
obj.set_ledgerseq(seq);
});
{
if (!BEAST_EXPECT(peer->sent.size() == 1))
return;
auto const reply = parseReply(peer->sent.front());
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
return;
BEAST_EXPECT(uint256{reply->objects(0).hash()} == txHash);
}
// Linked walk finds the node, hash does not match, object omitted.
uint256 const wrong{2};
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
auto& obj = *packet.add_objects();
obj.set_hash(wrong.begin(), wrong.size());
obj.set_nodeid(rootId);
obj.set_ledgerseq(seq);
});
{
if (!BEAST_EXPECT(peer->sent.size() == 1))
return;
auto const reply = parseReply(peer->sent.front());
if (!BEAST_EXPECT(reply))
return;
BEAST_EXPECT(reply->objects_size() == 0);
}
// Ledger header fallback when the node store and tree caches miss.
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otLEDGER);
auto& obj = *packet.add_objects();
obj.set_hash(headerHash.begin(), headerHash.size());
obj.set_ledgerseq(seq);
});
{
if (!BEAST_EXPECT(peer->sent.size() == 1))
return;
auto const reply = parseReply(peer->sent.front());
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
return;
BEAST_EXPECT(uint256{reply->objects(0).hash()} == headerHash);
BEAST_EXPECT(!reply->objects(0).data().empty());
}
// Malformed ledger hash is rejected before a reply is sent.
sendQuery([&](protocol::TMGetObjectByHash& packet) {
packet.set_type(protocol::TMGetObjectByHash::otLEDGER);
packet.set_ledgerhash("short");
auto& obj = *packet.add_objects();
obj.set_hash(headerHash.begin(), headerHash.size());
});
BEAST_EXPECT(peer->sent.empty());
}
void
run() override
{
testServesLinkedAndCachedNodes();
}
};
BEAST_DEFINE_TESTSUITE(GetObjectByHash, overlay, ripple);
} // namespace test
} // namespace ripple
+542
View File
@@ -0,0 +1,542 @@
//------------------------------------------------------------------------------
/*
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/app/misc/HashRouter.h>
#include <xrpld/core/JobQueue.h>
#include <xrpld/overlay/detail/OverlayImpl.h>
#include <xrpld/overlay/detail/XUSH.h>
#include <xrpl/beast/net/IPAddressConversion.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/resource/ResourceManager.h>
#include <xrpl/resource/detail/Tuning.h>
#include <boost/asio/ip/udp.hpp>
#include <algorithm>
#include <array>
#include <chrono>
#include <cstring>
#include <optional>
#include <set>
#include <string>
#include <thread>
#include <vector>
namespace ripple {
namespace test {
// Tests the UDP Superhighway (XUSH) handling in OverlayImpl: inbound datagram
// processing, outbound publishing and the XUSHPEER advertisement timer.
class OverlayXUSH_test : public beast::unit_test::suite
{
using udp = boost::asio::ip::udp;
static std::unique_ptr<Config>
highway(std::unique_ptr<Config> cfg)
{
cfg->UDP_HIGHWAY = true;
return cfg;
}
static beast::IP::Endpoint
ep(std::string const& s)
{
return beast::IP::Endpoint::from_string(s);
}
static std::set<beast::IP::Endpoint>
asSet(std::vector<beast::IP::Endpoint> const& v)
{
return {v.begin(), v.end()};
}
static Slice
slice(Buffer const& b)
{
return Slice(b.data(), b.size());
}
static std::string
str(Buffer const& b)
{
return std::string(reinterpret_cast<char const*>(b.data()), b.size());
}
static Buffer
serialize(STTx const& stx)
{
Serializer s;
stx.add(s);
return Buffer(s.data(), s.size());
}
// A pseudo-transaction, which servers never submit
static STTx
pseudoTx()
{
return STTx(ttAMENDMENT, [](auto& obj) {
obj.setAccountID(sfAccount, AccountID());
obj.setFieldH256(sfAmendment, uint256(2));
obj.setFieldU32(sfLedgerSequence, 5);
});
}
// A local UDP socket that highway datagrams can be sent to
struct Sink
{
boost::asio::io_context io;
udp::socket socket;
explicit Sink(boost::asio::ip::address const& addr)
: socket(io, udp::endpoint(addr, 0))
{
socket.non_blocking(true);
}
beast::IP::Endpoint
endpoint() const
{
auto const local = socket.local_endpoint();
return beast::IP::Endpoint(local.address(), local.port());
}
void
sendTo(Buffer const& datagram, std::uint16_t port)
{
socket.send_to(
boost::asio::buffer(datagram.data(), datagram.size()),
udp::endpoint(socket.local_endpoint().address(), port));
}
std::optional<std::string>
receive(std::chrono::milliseconds timeout = std::chrono::seconds(5))
{
auto const deadline = std::chrono::steady_clock::now() + timeout;
std::array<char, 4096> buf;
while (std::chrono::steady_clock::now() < deadline)
{
udp::endpoint from;
boost::system::error_code ec;
auto const n =
socket.receive_from(boost::asio::buffer(buf), from, 0, ec);
if (!ec)
return std::string(buf.data(), n);
if (ec != boost::asio::error::would_block &&
ec != boost::asio::error::try_again)
return std::nullopt;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
return std::nullopt;
}
// Receives datagrams until one of the given type arrives
std::optional<std::string>
receive(xush::MessageType type)
{
while (auto d = receive())
{
if (xush::classify(makeSlice(*d)) == type)
return d;
}
return std::nullopt;
}
};
struct Fixture
{
jtx::Env env;
OverlayImpl& overlay;
PeerFinder::Manager& peerFinder;
Resource::Manager& resources;
Fixture(beast::unit_test::suite& suite, bool enabled)
: env(suite, enabled ? jtx::envconfig(highway) : jtx::envconfig())
, overlay(dynamic_cast<OverlayImpl&>(env.app().overlay()))
, peerFinder(overlay.peerFinder())
, resources(env.app().getResourceManager())
{
}
// Delivers a datagram as if it had been received from `remote`
void
deliver(Buffer const& datagram, beast::IP::Endpoint const& remote)
{
overlay.processXUSH(
str(datagram), beast::IP::to_asio_endpoint(remote));
}
void
deliver(
std::vector<Buffer> const& datagrams,
beast::IP::Endpoint const& remote)
{
for (auto const& d : datagrams)
deliver(d, remote);
}
// Tracks the resource balance charged to `remote`
Resource::Consumer
probe(beast::IP::Endpoint const& remote)
{
return resources.newInboundEndpoint(remote);
}
std::set<beast::IP::Endpoint>
targets()
{
return asSet(peerFinder.highway_targets(1000));
}
// Whether `probe` was charged at least `fee` since `before`. Balances
// are normalized by the decay window, so a charge of `fee` raises the
// balance by at least fee / decayWindowSeconds.
static bool
charged(Resource::Consumer& probe, int before, int fee)
{
return probe.balance() - before >=
fee / Resource::decayWindowSeconds;
}
bool
isBad(uint256 const& txid)
{
env.app().getJobQueue().rendezvous();
return env.app().getHashRouter().getFlags(txid) & SF_BAD;
}
};
void
testDisabled()
{
testcase("disabled");
using namespace jtx;
Fixture f(*this, false);
auto const remote = ep("203.0.113.1:51235");
// Nothing is learned or sent while the highway is disabled
f.deliver(xush::encodePeers({ep("8.8.8.8:51235")}), remote);
BEAST_EXPECT(f.targets().empty());
Sink sink(boost::asio::ip::make_address("127.0.0.1"));
f.peerFinder.addFixedPeer("sink", {sink.endpoint()});
BEAST_EXPECT(f.targets().empty());
Account const alice("alice");
f.env.fund(XRP(10000), alice);
f.env.close();
Buffer const blob(64);
f.overlay.publishTxXUSH(slice(blob), uint256(1));
BEAST_EXPECT(!sink.receive(std::chrono::milliseconds(200)));
}
void
testPeers()
{
testcase("XUSHPEER");
Fixture f(*this, true);
auto const remote = ep("203.0.113.2:51235");
auto probe = f.probe(remote);
BEAST_EXPECT(f.targets().empty());
f.deliver(
xush::encodePeers({ep("8.8.8.8:51235"), ep("1.1.1.1:51235")}),
remote);
BEAST_EXPECT(
f.targets() == asSet({ep("8.8.8.8:51235"), ep("1.1.1.1:51235")}));
auto const balance = probe.balance();
// Unknown datagram types are ignored and cost almost nothing
f.deliver(Buffer("XUSHPING", 8), remote);
BEAST_EXPECT(!Fixture::charged(probe, balance, 100));
// A malformed advertisement is charged for
Buffer malformed(xush::tagSize + 2);
std::memcpy(malformed.data(), "XUSHPEER\x05\x00", malformed.size());
f.deliver(malformed, remote);
BEAST_EXPECT(Fixture::charged(probe, balance, 200));
BEAST_EXPECT(f.targets().size() == 2);
}
void
testTransaction()
{
testcase("XUSHTXNF transaction");
using namespace jtx;
Fixture f(*this, true);
auto& env = f.env;
auto const remote = ep("203.0.113.3:51235");
Account const alice("alice");
Account const bob("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto const jt = env.jt(pay(alice, bob, XRP(100)));
auto const txid = jt.stx->getTransactionID();
// Fragments arrive out of order and are reassembled
auto datagrams = xush::encodeTxn(slice(serialize(*jt.stx)), txid, 40);
BEAST_EXPECT(datagrams.size() > 2);
std::reverse(datagrams.begin(), datagrams.end());
f.deliver(datagrams, remote);
env.app().getJobQueue().rendezvous();
BEAST_EXPECT(!f.isBad(txid));
env.close();
BEAST_EXPECT(env.balance(bob) == XRP(10100));
// Delivering it again is harmless
f.deliver(datagrams, remote);
env.app().getJobQueue().rendezvous();
env.close();
BEAST_EXPECT(env.balance(bob) == XRP(10100));
}
void
testBadTransactions()
{
testcase("XUSHTXNF bad transactions");
using namespace jtx;
Fixture f(*this, true);
auto& env = f.env;
auto const remote = ep("203.0.113.4:51235");
auto probe = f.probe(remote);
Account const alice("alice");
Account const bob("bob");
env.fund(XRP(10000), alice, bob);
env.close();
auto const good = env.jt(pay(alice, bob, XRP(100)));
auto const blob = serialize(*good.stx);
auto const txid = good.stx->getTransactionID();
// The ID in the header must match the transaction
auto balance = probe.balance();
f.deliver(xush::encodeTxn(slice(blob), uint256(1)), remote);
BEAST_EXPECT(Fixture::charged(probe, balance, 400));
BEAST_EXPECT(!f.isBad(txid));
// Garbage that doesn't parse as a transaction
balance = probe.balance();
f.deliver(
xush::encodeTxn(makeSlice(std::string("garbage")), uint256(2)),
remote);
BEAST_EXPECT(Fixture::charged(probe, balance, 400));
// Fragments of one transaction that disagree with each other
balance = probe.balance();
f.deliver(xush::encodeTxn(slice(blob), uint256(3), 40).front(), remote);
f.deliver(xush::encodeTxn(slice(blob), uint256(3), 60).front(), remote);
BEAST_EXPECT(Fixture::charged(probe, balance, 400));
// A malformed fragment
balance = probe.balance();
f.deliver(Buffer("XUSHTXNF", 8), remote);
BEAST_EXPECT(Fixture::charged(probe, balance, 200));
// Pseudo-transactions are never accepted
balance = probe.balance();
auto const pseudo = pseudoTx();
f.deliver(
xush::encodeTxn(
slice(serialize(pseudo)), pseudo.getTransactionID()),
remote);
BEAST_EXPECT(Fixture::charged(probe, balance, 2000));
// An expired transaction is remembered as bad
auto const expired =
env.jt(pay(alice, bob, XRP(100)), last_ledger_seq(1));
auto const expiredId = expired.stx->getTransactionID();
balance = probe.balance();
f.deliver(
xush::encodeTxn(slice(serialize(*expired.stx)), expiredId), remote);
BEAST_EXPECT(f.isBad(expiredId));
BEAST_EXPECT(Fixture::charged(probe, balance, 150));
// A badly signed transaction is remembered as bad
auto corrupted = good.stx->getJson(JsonOptions::none);
corrupted[jss::TxnSignature] = std::string(128, '0');
STTx const badSig(parse(corrupted));
auto const badSigId = badSig.getTransactionID();
balance = probe.balance();
f.deliver(xush::encodeTxn(slice(serialize(badSig)), badSigId), remote);
BEAST_EXPECT(f.isBad(badSigId));
BEAST_EXPECT(Fixture::charged(probe, balance, 2000));
// A known bad transaction is charged for again
balance = probe.balance();
f.deliver(xush::encodeTxn(slice(serialize(badSig)), badSigId), remote);
BEAST_EXPECT(Fixture::charged(probe, balance, 150));
env.close();
BEAST_EXPECT(env.balance(bob) == XRP(10000));
}
void
testDisconnect()
{
testcase("resource limit");
Fixture f(*this, true);
auto const remote = ep("203.0.113.5:51235");
auto probe = f.probe(remote);
// A sender that keeps misbehaving is ignored
Buffer const blob(64);
for (int i = 0; i < 10000 && probe.balance() < Resource::dropThreshold;
++i)
f.deliver(xush::encodeTxn(slice(blob), uint256(1)), remote);
BEAST_EXPECT(probe.balance() >= Resource::dropThreshold);
f.deliver(xush::encodePeers({ep("8.8.8.8:51235")}), remote);
BEAST_EXPECT(f.targets().empty());
// Other senders are not affected
f.deliver(
xush::encodePeers({ep("8.8.8.8:51235")}), ep("203.0.113.6:51235"));
BEAST_EXPECT(f.targets() == asSet({ep("8.8.8.8:51235")}));
}
void
testPublish()
{
testcase("publish");
using namespace jtx;
Fixture f(*this, true);
auto& env = f.env;
Account const alice("alice");
Account const bob("bob");
env.fund(XRP(10000), alice, bob);
env.close();
Sink sink4(boost::asio::ip::make_address("127.0.0.1"));
f.peerFinder.addFixedPeer("sink4", {sink4.endpoint()});
std::optional<Sink> sink6;
try
{
sink6.emplace(boost::asio::ip::make_address("::1"));
f.peerFinder.addFixedPeer("sink6", {sink6->endpoint()});
}
catch (std::exception const&)
{
log << "IPv6 loopback unavailable, skipping" << std::endl;
}
// A transaction relayed by NetworkOPs is published on the highway
env(pay(alice, bob, XRP(100)));
auto const stx = env.tx();
auto const blob = serialize(*stx);
auto check = [&](Sink& sink) {
auto const d = sink.receive(xush::MessageType::txn);
if (!BEAST_EXPECT(d))
return;
auto const fragment = xush::decodeTxnFragment(makeSlice(*d));
if (!BEAST_EXPECT(fragment))
return;
BEAST_EXPECT(fragment->txid == stx->getTransactionID());
BEAST_EXPECT(fragment->count == 1);
BEAST_EXPECT(fragment->payload == slice(blob));
};
check(sink4);
if (sink6)
check(*sink6);
// Large transactions are fragmented
Buffer big(5000);
for (std::size_t i = 0; i < big.size(); ++i)
big.data()[i] = static_cast<std::uint8_t>(i);
f.overlay.publishTxXUSH(slice(big), uint256(7));
xush::Reassembler reassembler;
std::optional<Buffer> rebuilt;
while (!rebuilt)
{
auto const d = sink4.receive(xush::MessageType::txn);
if (!BEAST_EXPECT(d))
break;
auto const fragment = xush::decodeTxnFragment(makeSlice(*d));
if (!BEAST_EXPECT(fragment && fragment->txid == uint256(7)))
break;
auto result = reassembler.add(
sink4.endpoint(),
*fragment,
xush::Reassembler::clock_type::now());
if (result.status == xush::Reassembler::Status::complete)
rebuilt = std::move(result.txn);
}
BEAST_EXPECT(rebuilt && *rebuilt == big);
// Transactions too large for the highway are not sent
Buffer const huge(xush::maxTxnSize + 1);
f.overlay.publishTxXUSH(slice(huge), uint256(8));
f.overlay.publishTxXUSH(slice(big), uint256(9));
auto const d = sink4.receive(xush::MessageType::txn);
if (BEAST_EXPECT(d))
{
auto const fragment = xush::decodeTxnFragment(makeSlice(*d));
BEAST_EXPECT(fragment && fragment->txid == uint256(9));
}
}
void
testSendPeers()
{
testcase("advertise");
Fixture f(*this, true);
Sink sink(boost::asio::ip::make_address("127.0.0.1"));
f.peerFinder.addFixedPeer("sink", {sink.endpoint()});
// Learned endpoints are advertised to targets once per second, but
// fixed peers are never advertised.
f.peerFinder.add_highway_peers({ep("8.8.8.8:51235")});
auto const d = sink.receive(xush::MessageType::peers);
if (!BEAST_EXPECT(d))
return;
auto const endpoints = xush::decodePeers(makeSlice(*d));
BEAST_EXPECT(
endpoints && asSet(*endpoints) == asSet({ep("8.8.8.8:51235")}));
}
public:
void
run() override
{
testDisabled();
testPeers();
testTransaction();
testBadTransactions();
testDisconnect();
testPublish();
testSendPeers();
}
};
BEAST_DEFINE_TESTSUITE(OverlayXUSH, overlay, ripple);
} // namespace test
} // namespace ripple
+313
View File
@@ -0,0 +1,313 @@
//------------------------------------------------------------------------------
/*
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/overlay/Message.h>
#include <xrpld/overlay/detail/ProtocolMessage.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/messages.h>
#include <boost/asio/buffer.hpp>
#include <boost/beast/core/multi_buffer.hpp>
#include <boost/system/error_code.hpp>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace ripple {
namespace test {
/** Receive-side framing tests for invokeProtocolMessage.
PeerImp::onReadMessage hands its entire read buffer to
invokeProtocolMessage, which must decode exactly one frame and report
exactly that frame's size as consumed. A single read routinely contains
several frames, so the payload parser must never look past the payload
size declared in the frame header.
*/
class ProtocolMessage_test : public beast::unit_test::suite
{
using Compressed = compression::Compressed;
/** Minimal stand-in for the PeerImp handler interface. */
struct Handler
{
bool compression = false;
std::vector<std::shared_ptr<::google::protobuf::Message>> received;
std::vector<std::uint16_t> types;
bool
compressionEnabled() const
{
return compression;
}
void
onMessageBegin(
std::uint16_t,
std::shared_ptr<::google::protobuf::Message> const&,
std::size_t,
std::size_t,
bool)
{
}
template <class T>
void
onMessage(std::shared_ptr<T> const& m)
{
received.push_back(m);
}
void
onMessageEnd(
std::uint16_t type,
std::shared_ptr<::google::protobuf::Message> const&)
{
types.push_back(type);
}
void
onMessageUnknown(std::uint16_t type)
{
types.push_back(type);
}
};
/** Loads wire bytes into a multi_buffer as one or two committed
segments, split at `split` (no split if split is 0 or past the end).
*/
static void
load(
boost::beast::multi_buffer& rb,
std::vector<std::uint8_t> const& wire,
std::size_t split = 0)
{
auto commit = [&rb](std::uint8_t const* p, std::size_t n) {
if (n != 0)
rb.commit(boost::asio::buffer_copy(
rb.prepare(n), boost::asio::buffer(p, n)));
};
if (split == 0 || split >= wire.size())
{
commit(wire.data(), wire.size());
return;
}
commit(wire.data(), split);
commit(wire.data() + split, wire.size() - split);
}
/** Mirrors the consume loop in PeerImp::onReadMessage. */
static boost::system::error_code
drain(boost::beast::multi_buffer& rb, Handler& handler)
{
std::size_t hint = 0;
while (rb.size() > 0)
{
auto const [consumed, ec] =
invokeProtocolMessage(rb.data(), handler, hint);
if (ec)
return ec;
if (consumed == 0)
break;
rb.consume(consumed);
}
return {};
}
static std::vector<std::uint8_t>
frame(
::google::protobuf::Message const& m,
protocol::MessageType type,
Compressed compressed = Compressed::Off)
{
Message msg(m, type);
return msg.getBuffer(compressed);
}
static protocol::TMPing
makePing(protocol::TMPing::pingType type, std::uint32_t seq)
{
protocol::TMPing ping;
ping.set_type(type);
ping.set_seq(seq);
return ping;
}
template <class T>
std::shared_ptr<T>
receivedAs(Handler const& h, std::size_t i)
{
if (!BEAST_EXPECT(i < h.received.size()))
return {};
auto p = std::dynamic_pointer_cast<T>(h.received[i]);
BEAST_EXPECT(p);
return p;
}
void
testCoalescedUncompressed()
{
testcase("Coalesced uncompressed frames");
auto const first = makePing(protocol::TMPing::ptPING, 7);
auto second = makePing(protocol::TMPing::ptPONG, 99);
second.set_nettime(5);
auto wire = frame(first, protocol::mtPING);
auto const firstSize = wire.size();
auto const tail = frame(second, protocol::mtPING);
wire.insert(wire.end(), tail.begin(), tail.end());
// Both frames delivered by a single read, with every possible
// segmentation of the read buffer (0 means one contiguous segment).
for (std::size_t split = 0; split < wire.size(); ++split)
{
boost::beast::multi_buffer rb;
load(rb, wire, split);
Handler h;
auto const ec = drain(rb, h);
BEAST_EXPECTS(
!ec, "split " + std::to_string(split) + ": " + ec.message());
BEAST_EXPECT(rb.size() == 0);
if (!BEAST_EXPECT(h.received.size() == 2))
continue;
auto const a = receivedAs<protocol::TMPing>(h, 0);
auto const b = receivedAs<protocol::TMPing>(h, 1);
if (!a || !b)
continue;
BEAST_EXPECT(a->type() == protocol::TMPing::ptPING);
BEAST_EXPECT(a->seq() == 7);
BEAST_EXPECT(!a->has_nettime());
BEAST_EXPECT(b->type() == protocol::TMPing::ptPONG);
BEAST_EXPECT(b->seq() == 99);
BEAST_EXPECT(b->nettime() == 5);
}
// The first frame on its own, followed by a partial second frame,
// is delivered alone and the remainder is left for the next read.
{
boost::beast::multi_buffer rb;
std::vector<std::uint8_t> partial(
wire.begin(), wire.begin() + firstSize + 3);
load(rb, partial);
Handler h;
BEAST_EXPECT(!drain(rb, h));
BEAST_EXPECT(h.received.size() == 1);
BEAST_EXPECT(rb.size() == 3);
}
}
void
testUncompressedThenCompressed()
{
testcase("Uncompressed frame followed by compressed frame");
auto const ping = makePing(protocol::TMPing::ptPING, 1234);
protocol::TMTransaction tx;
tx.set_rawtransaction(std::string(4096, 'x'));
tx.set_status(protocol::tsNEW);
auto wire = frame(ping, protocol::mtPING);
auto const txFrame = frame(tx, protocol::mtTRANSACTION, Compressed::On);
// The second frame must really be compressed for this test to mean
// anything: its header is what the first parse used to run into.
if (!BEAST_EXPECT(!txFrame.empty() && (txFrame[0] & 0x80)))
return;
wire.insert(wire.end(), txFrame.begin(), txFrame.end());
for (std::size_t split = 0; split < wire.size(); split += 7)
{
boost::beast::multi_buffer rb;
load(rb, wire, split);
Handler h;
h.compression = true;
auto const ec = drain(rb, h);
BEAST_EXPECTS(
!ec, "split " + std::to_string(split) + ": " + ec.message());
BEAST_EXPECT(rb.size() == 0);
if (!BEAST_EXPECT(h.received.size() == 2))
continue;
auto const p = receivedAs<protocol::TMPing>(h, 0);
auto const t = receivedAs<protocol::TMTransaction>(h, 1);
if (!p || !t)
continue;
BEAST_EXPECT(p->type() == protocol::TMPing::ptPING);
BEAST_EXPECT(p->seq() == 1234);
BEAST_EXPECT(t->rawtransaction() == tx.rawtransaction());
BEAST_EXPECT(t->status() == protocol::tsNEW);
}
}
void
testDeclaredSizeIsAuthoritative()
{
testcase("Declared payload size bounds the parse");
auto const ping = makePing(protocol::TMPing::ptPING, 7);
auto wire = frame(ping, protocol::mtPING);
// Shrink the declared payload size by one byte without removing the
// byte from the stream. The declared payload alone is a truncated
// TMPing, so the frame must be rejected rather than completed from
// bytes that lie beyond it.
std::uint32_t declared = (std::uint32_t(wire[0]) << 24) |
(std::uint32_t(wire[1]) << 16) | (std::uint32_t(wire[2]) << 8) |
std::uint32_t(wire[3]);
if (!BEAST_EXPECT(declared > 0))
return;
--declared;
wire[0] = static_cast<std::uint8_t>(declared >> 24);
wire[1] = static_cast<std::uint8_t>(declared >> 16);
wire[2] = static_cast<std::uint8_t>(declared >> 8);
wire[3] = static_cast<std::uint8_t>(declared);
boost::beast::multi_buffer rb;
load(rb, wire);
Handler h;
auto const ec = drain(rb, h);
BEAST_EXPECT(ec == boost::system::errc::bad_message);
BEAST_EXPECT(h.received.empty());
}
public:
void
run() override
{
testCoalescedUncompressed();
testUncompressedThenCompressed();
testDeclaredSizeIsAuthoritative();
}
};
BEAST_DEFINE_TESTSUITE(ProtocolMessage, overlay, ripple);
} // namespace test
} // namespace ripple
+466
View File
@@ -0,0 +1,466 @@
//------------------------------------------------------------------------------
/*
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/overlay/detail/XUSH.h>
#include <xrpl/beast/unit_test.h>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <set>
#include <string>
#include <vector>
namespace ripple {
namespace test {
class XUSH_test : public beast::unit_test::suite
{
using Status = xush::Reassembler::Status;
static beast::IP::Endpoint
ep(std::string const& s)
{
return beast::IP::Endpoint::from_string(s);
}
static Slice
slice(Buffer const& b)
{
return Slice(b.data(), b.size());
}
static Buffer
makeTxn(std::size_t size)
{
Buffer b(size);
for (std::size_t i = 0; i < size; ++i)
b.data()[i] = static_cast<std::uint8_t>(i * 31 + 7);
return b;
}
static void
putU32(std::uint8_t* p, std::uint32_t v)
{
p[0] = static_cast<std::uint8_t>(v >> 24);
p[1] = static_cast<std::uint8_t>(v >> 16);
p[2] = static_cast<std::uint8_t>(v >> 8);
p[3] = static_cast<std::uint8_t>(v);
}
// A XUSHTXNF datagram with an arbitrary (possibly invalid) header
static Buffer
rawFragment(
uint256 const& txid,
std::uint32_t totalSize,
std::uint32_t count,
std::uint32_t index,
std::size_t payloadSize)
{
Buffer b(xush::txnHeaderSize + payloadSize);
std::memset(b.data(), 0xAB, b.size());
std::memcpy(b.data(), "XUSHTXNF", xush::tagSize);
std::memcpy(b.data() + xush::tagSize, txid.data(), txid.size());
putU32(b.data() + 40, totalSize);
putU32(b.data() + 44, count);
putU32(b.data() + 48, index);
return b;
}
// A XUSHPEER datagram with arbitrary (possibly invalid) counts
static Buffer
rawPeers(std::uint8_t n4, std::uint8_t n6, std::size_t bodySize)
{
Buffer b(xush::tagSize + 2 + bodySize);
std::memset(b.data(), 0, b.size());
std::memcpy(b.data(), "XUSHPEER", xush::tagSize);
b.data()[8] = n4;
b.data()[9] = n6;
return b;
}
void
testClassify()
{
testcase("classify");
using namespace xush;
auto type = [](std::string const& s) { return classify(makeSlice(s)); };
BEAST_EXPECT(type("XUSHPEER") == MessageType::peers);
BEAST_EXPECT(type("XUSHPEER and more") == MessageType::peers);
BEAST_EXPECT(type("XUSHTXNF") == MessageType::txn);
BEAST_EXPECT(type("XUSHPING") == MessageType::unknown);
BEAST_EXPECT(type("XUSHPEE") == MessageType::unknown);
BEAST_EXPECT(type("") == MessageType::unknown);
BEAST_EXPECT(type("{\"command\":\"ping\"}") == MessageType::unknown);
}
void
testPeers()
{
testcase("XUSHPEER");
using namespace xush;
std::vector<beast::IP::Endpoint> const endpoints{
ep("1.2.3.4:51235"), ep("[2001:db8::1]:21337"), ep("5.6.7.8:1")};
auto const datagram = encodePeers(endpoints);
BEAST_EXPECT(datagram.size() == tagSize + 2 + 2 * 8 + 20);
BEAST_EXPECT(classify(slice(datagram)) == MessageType::peers);
if (auto const decoded = decodePeers(slice(datagram));
BEAST_EXPECT(decoded))
{
BEAST_EXPECT(
std::set<beast::IP::Endpoint>(
decoded->begin(), decoded->end()) ==
std::set<beast::IP::Endpoint>(
endpoints.begin(), endpoints.end()));
}
// An empty advertisement is valid
if (auto const decoded = decodePeers(slice(encodePeers({})));
BEAST_EXPECT(decoded))
BEAST_EXPECT(decoded->empty());
// At most maxAdvertisedPeers are advertised, and the result fits in
// a single datagram even if they are all IPv6
{
std::vector<beast::IP::Endpoint> many;
for (std::uint16_t i = 1; i <= maxAdvertisedPeers + 10; ++i)
many.emplace_back(
boost::asio::ip::make_address("2001:db8::1"), i);
auto const big = encodePeers(many);
BEAST_EXPECT(big.size() <= maxDatagramSize);
auto const decoded = decodePeers(slice(big));
BEAST_EXPECT(decoded && decoded->size() == maxAdvertisedPeers);
}
// Truncated or padded
BEAST_EXPECT(!decodePeers(Slice(datagram.data(), datagram.size() - 1)));
{
Buffer padded(datagram.size() + 1);
std::memcpy(padded.data(), datagram.data(), datagram.size());
BEAST_EXPECT(!decodePeers(slice(padded)));
}
BEAST_EXPECT(!decodePeers(Slice(datagram.data(), tagSize + 1)));
// Too many endpoints, even with a consistent size
BEAST_EXPECT(!decodePeers(slice(rawPeers(
maxAdvertisedPeers + 1, 0, (maxAdvertisedPeers + 1) * 8))));
BEAST_EXPECT(decodePeers(
slice(rawPeers(maxAdvertisedPeers, 0, maxAdvertisedPeers * 8))));
// A port that doesn't fit in 16 bits
{
auto bad = rawPeers(1, 0, 8);
putU32(bad.data() + tagSize + 2 + 4, 0x10000);
BEAST_EXPECT(!decodePeers(slice(bad)));
putU32(bad.data() + tagSize + 2 + 4, 0xFFFF);
BEAST_EXPECT(decodePeers(slice(bad)));
}
// Wrong tag
BEAST_EXPECT(!decodePeers(slice(rawFragment(uint256{1}, 1, 1, 0, 1))));
}
void
testTxnEncoding()
{
testcase("XUSHTXNF encoding");
using namespace xush;
uint256 const txid{42};
// A typical transaction fits in a single datagram
{
auto const txn = makeTxn(200);
auto const datagrams = encodeTxn(slice(txn), txid);
BEAST_EXPECT(datagrams.size() == 1);
BEAST_EXPECT(datagrams[0].size() == txnHeaderSize + 200);
auto const fragment = decodeTxnFragment(slice(datagrams[0]));
if (BEAST_EXPECT(fragment))
{
BEAST_EXPECT(fragment->txid == txid);
BEAST_EXPECT(fragment->totalSize == 200);
BEAST_EXPECT(fragment->count == 1);
BEAST_EXPECT(fragment->index == 0);
BEAST_EXPECT(fragment->payload == slice(txn));
}
}
// Larger transactions are split, and no datagram is too large
for (auto const size :
{maxFragmentPayload,
maxFragmentPayload + 1,
3 * maxFragmentPayload,
3 * maxFragmentPayload + 17})
{
auto const txn = makeTxn(size);
auto const datagrams = encodeTxn(slice(txn), txid);
auto const count =
(size + maxFragmentPayload - 1) / maxFragmentPayload;
BEAST_EXPECT(datagrams.size() == count);
std::size_t total = 0;
for (std::size_t i = 0; i < datagrams.size(); ++i)
{
BEAST_EXPECT(datagrams[i].size() <= maxDatagramSize);
auto const fragment = decodeTxnFragment(slice(datagrams[i]));
if (!BEAST_EXPECT(fragment))
continue;
BEAST_EXPECT(fragment->index == i);
BEAST_EXPECT(fragment->count == count);
BEAST_EXPECT(fragment->totalSize == size);
BEAST_EXPECT(
fragment->payload ==
Slice(
txn.data() + i * maxFragmentPayload,
fragment->payload.size()));
total += fragment->payload.size();
}
BEAST_EXPECT(total == size);
}
// Limits
BEAST_EXPECT(encodeTxn(Slice{}, txid).empty());
BEAST_EXPECT(encodeTxn(slice(Buffer(maxTxnSize + 1)), txid).empty());
{
auto const datagrams = encodeTxn(slice(Buffer(maxTxnSize)), txid);
BEAST_EXPECT(!datagrams.empty());
BEAST_EXPECT(datagrams.size() <= maxFragments);
}
}
void
testTxnDecoding()
{
testcase("XUSHTXNF decoding");
using namespace xush;
uint256 const id{7};
auto decode = [](Buffer const& b) {
return decodeTxnFragment(slice(b)).has_value();
};
BEAST_EXPECT(decode(rawFragment(id, 10, 2, 1, 5)));
BEAST_EXPECT(decode(rawFragment(id, 10, 1, 0, 10)));
BEAST_EXPECT(decode(rawFragment(id, maxTxnSize, maxFragments, 0, 1)));
// No payload
BEAST_EXPECT(!decode(rawFragment(id, 10, 1, 0, 0)));
// Bad total size
BEAST_EXPECT(!decode(rawFragment(id, 0, 1, 0, 1)));
BEAST_EXPECT(!decode(rawFragment(id, maxTxnSize + 1, 2, 0, 1)));
// Bad fragment count
BEAST_EXPECT(!decode(rawFragment(id, 10, 0, 0, 1)));
BEAST_EXPECT(!decode(rawFragment(id, 10, 11, 0, 1)));
BEAST_EXPECT(
!decode(rawFragment(id, maxTxnSize, maxFragments + 1, 0, 1)));
// Bad index
BEAST_EXPECT(!decode(rawFragment(id, 10, 2, 2, 1)));
// Payload larger than the transaction
BEAST_EXPECT(!decode(rawFragment(id, 10, 2, 0, 11)));
// Wrong tag
BEAST_EXPECT(!decodeTxnFragment(slice(encodePeers({}))));
}
void
testReassembly()
{
testcase("reassembly");
using namespace xush;
using namespace std::chrono_literals;
auto const now = Reassembler::clock_type::now();
auto const alice = ep("1.2.3.4:5000");
auto const bob = ep("5.6.7.8:5000");
auto const carol = ep("9.9.9.9:5000");
uint256 const id1{1};
uint256 const id2{2};
auto const txn1 = makeTxn(2 * maxFragmentPayload + 100);
auto const txn2 = makeTxn(2 * maxFragmentPayload + 200);
auto const d1 = encodeTxn(slice(txn1), id1);
auto const d2 = encodeTxn(slice(txn2), id2);
BEAST_EXPECT(d1.size() == 3 && d2.size() == 3);
auto f1 = [&](std::size_t i) {
return *decodeTxnFragment(slice(d1[i]));
};
auto f2 = [&](std::size_t i) {
return *decodeTxnFragment(slice(d2[i]));
};
// Out of order, with a repeated fragment
{
Reassembler r;
BEAST_EXPECT(r.add(alice, f1(2), now).status == Status::pending);
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
BEAST_EXPECT(r.pending() == 1);
auto const result = r.add(alice, f1(1), now);
BEAST_EXPECT(result.status == Status::complete);
BEAST_EXPECT(result.txn == txn1);
BEAST_EXPECT(r.pending() == 0);
BEAST_EXPECT(r.bytes() == 0);
}
// A transaction in a single datagram needs no state
{
Reassembler r;
auto const small = makeTxn(100);
auto const d = encodeTxn(slice(small), id2);
auto const result =
r.add(alice, *decodeTxnFragment(slice(d[0])), now);
BEAST_EXPECT(result.status == Status::complete);
BEAST_EXPECT(result.txn == small);
BEAST_EXPECT(r.pending() == 0);
// ... but must be consistent
auto bad = *decodeTxnFragment(slice(d[0]));
bad.totalSize += 1;
BEAST_EXPECT(r.add(alice, bad, now).status == Status::invalid);
}
// Senders are kept apart
{
Reassembler r;
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
BEAST_EXPECT(r.add(alice, f1(1), now).status == Status::pending);
BEAST_EXPECT(r.add(bob, f1(2), now).status == Status::pending);
BEAST_EXPECT(r.pending() == 2);
BEAST_EXPECT(r.add(alice, f1(2), now).status == Status::complete);
BEAST_EXPECT(r.pending() == 1);
}
// A header that disagrees with earlier fragments
{
Reassembler r;
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
auto bad = f1(1);
bad.count += 1;
BEAST_EXPECT(r.add(alice, bad, now).status == Status::invalid);
BEAST_EXPECT(r.pending() == 0);
BEAST_EXPECT(r.bytes() == 0);
}
// More bytes than the advertised total size
{
Reassembler r;
auto first = f1(0);
first.count = 2;
first.totalSize = maxFragmentPayload + 10;
auto second = f1(1);
second.count = 2;
second.totalSize = first.totalSize;
BEAST_EXPECT(r.add(alice, first, now).status == Status::pending);
BEAST_EXPECT(r.add(alice, second, now).status == Status::invalid);
BEAST_EXPECT(r.pending() == 0);
}
// Every fragment present but fewer bytes than the total size
{
Reassembler r;
auto first = f1(0);
first.count = 2;
first.totalSize = 2 * maxFragmentPayload + 5;
auto second = f1(1);
second.count = 2;
second.totalSize = first.totalSize;
BEAST_EXPECT(r.add(alice, first, now).status == Status::pending);
BEAST_EXPECT(r.add(alice, second, now).status == Status::invalid);
BEAST_EXPECT(r.pending() == 0);
}
// Partial transactions time out
{
Reassembler r(
Reassembler::defaultMaxPending,
Reassembler::defaultMaxBytes,
30s);
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
BEAST_EXPECT(r.add(alice, f1(1), now).status == Status::pending);
// Not yet
BEAST_EXPECT(
r.add(bob, f2(0), now + 30s).status == Status::pending);
BEAST_EXPECT(r.pending() == 2);
// Now: the first transaction has to start over
BEAST_EXPECT(
r.add(bob, f2(1), now + 31s).status == Status::pending);
BEAST_EXPECT(r.pending() == 1);
BEAST_EXPECT(
r.add(alice, f1(2), now + 31s).status == Status::pending);
BEAST_EXPECT(r.pending() == 2);
}
// The number of partial transactions is limited
{
Reassembler r(2, Reassembler::defaultMaxBytes, 30s);
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
BEAST_EXPECT(r.add(bob, f1(0), now).status == Status::pending);
BEAST_EXPECT(r.add(carol, f1(0), now).status == Status::overloaded);
BEAST_EXPECT(r.pending() == 2);
// Fragments of transactions already being rebuilt are accepted
BEAST_EXPECT(r.add(alice, f1(1), now).status == Status::pending);
BEAST_EXPECT(r.add(alice, f1(2), now).status == Status::complete);
BEAST_EXPECT(r.add(carol, f1(0), now).status == Status::pending);
}
// The number of bytes buffered is limited
{
Reassembler r(
Reassembler::defaultMaxPending,
maxFragmentPayload + Reassembler::fragmentOverhead,
30s);
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
BEAST_EXPECT(r.add(alice, f1(1), now).status == Status::overloaded);
BEAST_EXPECT(r.add(bob, f1(0), now).status == Status::overloaded);
BEAST_EXPECT(r.pending() == 1);
BEAST_EXPECT(
r.bytes() ==
maxFragmentPayload + Reassembler::fragmentOverhead);
}
}
public:
void
run() override
{
testClassify();
testPeers();
testTxnEncoding();
testTxnDecoding();
testReassembly();
}
};
BEAST_DEFINE_TESTSUITE(XUSH, overlay, ripple);
} // namespace test
} // namespace ripple
+276
View File
@@ -0,0 +1,276 @@
//------------------------------------------------------------------------------
/*
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/unit_test/SuiteJournal.h>
#include <xrpld/peerfinder/PeerfinderManager.h>
#include <xrpld/peerfinder/detail/Tuning.h>
#include <xrpld/peerfinder/make_Manager.h>
#include <xrpl/basics/BasicConfig.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/beast/insight/NullCollector.h>
#include <xrpl/beast/net/IPAddressConversion.h>
#include <xrpl/beast/unit_test.h>
#include <boost/asio/io_context.hpp>
#include <memory>
#include <set>
#include <string>
#include <vector>
namespace ripple {
namespace PeerFinder {
// Tests the UDP Superhighway (XUSH) endpoint set kept by PeerFinder::Manager
class HighwayPeers_test : public beast::unit_test::suite
{
test::SuiteJournal journal_{"HighwayPeers_test", *this};
struct Fixture
{
boost::asio::io_context io;
TestStopwatch clock;
BasicConfig basicConfig;
std::unique_ptr<Manager> manager;
Fixture(beast::Journal journal, bool enabled, bool peerPrivate)
: manager(make_Manager(
io,
clock,
journal,
basicConfig,
beast::insight::NullCollector::New(),
false))
{
Config config;
config.udpHighway = enabled;
config.peerPrivate = peerPrivate;
manager->setConfig(config);
}
~Fixture()
{
manager->stop();
}
};
static beast::IP::Endpoint
ep(std::string const& s)
{
return beast::IP::Endpoint::from_string(s);
}
// A distinct public IPv4 endpoint
static beast::IP::Endpoint
publicEp(std::uint32_t i)
{
return beast::IP::Endpoint(
boost::asio::ip::address_v4(0x2C000000 + i), 51235);
}
static std::set<beast::IP::Endpoint>
asSet(std::vector<beast::IP::Endpoint> const& v)
{
return {v.begin(), v.end()};
}
void
testDisabled()
{
testcase("disabled");
Fixture f(journal_, false, false);
f.manager->addFixedPeer("fixed", {ep("10.0.0.1:51235")});
f.manager->add_highway_peers({ep("8.8.8.8:51235")});
f.manager->onRedirects(
beast::IP::to_asio_endpoint(ep("8.8.4.4:51235")),
{beast::IP::to_asio_endpoint(ep("1.1.1.1:51235"))});
BEAST_EXPECT(f.manager->highway_targets(100).empty());
BEAST_EXPECT(f.manager->highway_adverts(100).empty());
}
void
testLearning()
{
testcase("learning");
Fixture f(journal_, true, false);
// Only valid public endpoints are learned from other servers
f.manager->add_highway_peers(
{ep("8.8.8.8:51235"),
ep("10.0.0.1:51235"),
ep("127.0.0.1:51235"),
ep("9.9.9.9:0"),
ep("0.0.0.0:51235")});
BEAST_EXPECT(
asSet(f.manager->highway_targets(100)) ==
std::set<beast::IP::Endpoint>{ep("8.8.8.8:51235")});
// Fixed peers are trusted, even with a private address, but they
// are never advertised
f.manager->addFixedPeer("fixed", {ep("10.0.0.2:51235")});
BEAST_EXPECT(
asSet(f.manager->highway_targets(100)) ==
(std::set<beast::IP::Endpoint>{
ep("8.8.8.8:51235"), ep("10.0.0.2:51235")}));
BEAST_EXPECT(
asSet(f.manager->highway_adverts(100)) ==
std::set<beast::IP::Endpoint>{ep("8.8.8.8:51235")});
// A learned endpoint that is later configured as fixed stops being
// advertised
f.manager->addFixedPeer("fixed2", {ep("8.8.8.8:51235")});
BEAST_EXPECT(f.manager->highway_adverts(100).empty());
BEAST_EXPECT(f.manager->highway_targets(100).size() == 2);
// Redirects are learned too
f.manager->onRedirects(
beast::IP::to_asio_endpoint(ep("8.8.4.4:51235")),
{beast::IP::to_asio_endpoint(ep("1.1.1.1:51235"))});
BEAST_EXPECT(f.manager->highway_targets(100).size() == 3);
}
void
testSampling()
{
testcase("sampling");
Fixture f(journal_, true, false);
std::vector<beast::IP::Endpoint> endpoints;
for (std::uint32_t i = 0; i < 100; ++i)
endpoints.push_back(publicEp(i));
f.manager->add_highway_peers(endpoints);
for (int i = 0; i < 10; ++i)
{
auto const targets = f.manager->highway_targets(16);
BEAST_EXPECT(targets.size() == 16);
BEAST_EXPECT(asSet(targets).size() == 16);
}
BEAST_EXPECT(
asSet(f.manager->highway_targets(1000)) == asSet(endpoints));
BEAST_EXPECT(f.manager->highway_targets(0).empty());
}
void
testPrivate()
{
testcase("private");
Fixture f(journal_, true, true);
f.manager->addFixedPeer("fixed", {ep("10.0.0.2:51235")});
f.manager->add_highway_peers({ep("8.8.8.8:51235")});
// A private server only talks to its fixed peers, and advertises
// nothing
BEAST_EXPECT(
asSet(f.manager->highway_targets(100)) ==
std::set<beast::IP::Endpoint>{ep("10.0.0.2:51235")});
BEAST_EXPECT(f.manager->highway_adverts(100).empty());
}
void
testExpiry()
{
testcase("expiry");
using namespace std::chrono_literals;
Fixture f(journal_, true, false);
auto const ttl = Tuning::highwayPeerSecondsToLive;
auto const advertised = ep("8.8.8.8:51235");
auto const redirected = ep("1.1.1.1:51235");
auto const fixed = ep("10.0.0.2:51235");
f.manager->addFixedPeer("fixed", {fixed});
f.manager->add_highway_peers({advertised});
f.manager->add_highway_peers({redirected});
// Advertisements don't extend the life of an entry, but
// authenticated sources (here, a redirect) do
f.clock.advance(ttl / 2);
f.manager->add_highway_peers({advertised, redirected});
f.manager->onRedirects(
beast::IP::to_asio_endpoint(ep("8.8.4.4:51235")),
{beast::IP::to_asio_endpoint(redirected)});
f.clock.advance(ttl / 2);
f.manager->once_per_second();
BEAST_EXPECT(f.manager->highway_targets(100).size() == 3);
f.clock.advance(1s);
f.manager->once_per_second();
BEAST_EXPECT(
asSet(f.manager->highway_targets(100)) ==
(std::set<beast::IP::Endpoint>{redirected, fixed}));
f.clock.advance(ttl / 2);
f.manager->once_per_second();
BEAST_EXPECT(
asSet(f.manager->highway_targets(100)) ==
std::set<beast::IP::Endpoint>{fixed});
// An expired entry can be learned again
f.manager->add_highway_peers({advertised});
BEAST_EXPECT(f.manager->highway_targets(100).size() == 2);
}
void
testLimit()
{
testcase("limit");
Fixture f(journal_, true, false);
auto const max = Tuning::highwayPeersMax;
std::vector<beast::IP::Endpoint> endpoints;
for (std::uint32_t i = 0; i < max + 100; ++i)
endpoints.push_back(publicEp(i));
f.manager->add_highway_peers(endpoints);
BEAST_EXPECT(f.manager->highway_targets(max + 100).size() == max);
// Fixed peers don't count towards the limit
f.manager->addFixedPeer("fixed", {ep("10.0.0.2:51235")});
BEAST_EXPECT(f.manager->highway_targets(max + 100).size() == max + 1);
// Disabling the highway forgets everything
Config config;
config.udpHighway = false;
f.manager->setConfig(config);
BEAST_EXPECT(f.manager->highway_targets(max + 100).empty());
}
public:
void
run() override
{
testDisabled();
testLearning();
testSampling();
testPrivate();
testExpiry();
testLimit();
}
};
BEAST_DEFINE_TESTSUITE(HighwayPeers, peerfinder, ripple);
} // namespace PeerFinder
} // namespace ripple
+640 -1
View File
@@ -18,16 +18,22 @@
//==============================================================================
#include <test/jtx.h>
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/core/ConfigSections.h>
#include <xrpld/nodestore/detail/DatabasePinnedImp.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/beast/utility/temp_dir.h>
#include <xrpl/protocol/jss.h>
#include <boost/filesystem.hpp>
#include <chrono>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <grpc/impl/codegen/compression_types.h>
#include <iterator>
#include <optional>
#include <thread>
namespace ripple {
@@ -102,14 +108,28 @@ struct CatalogueEnvOpts
};
// envconfig with pinned_type set so catalogue_load passes the config guard.
// Uses rwdb for in-process speed (allowed in standalone).
//
// envconfig() selects [node_db] type=rwdb, which is the tree-only null node
// store: SHAMapStore never builds DatabasePinned on it, so catalogue_load
// would refuse to run. Use the in-memory "memory" store for the hot side
// instead. It is shared by path within the process, so each config gets its
// own path (SHAMapStore creates the directory; nothing is written to it).
// The pinned side uses rwdb, an in-memory persistent backend.
inline std::unique_ptr<Config>
catalogueEnvconfig(CatalogueEnvOpts const& opts = {})
{
auto cfg = test::jtx::envconfig();
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
if (opts.withPinnedType)
{
nodeDb.set("type", "memory");
nodeDb.set(
"path",
(boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path("catalogue-test-%%%%-%%%%-%%%%"))
.string());
nodeDb.set("pinned_type", "rwdb");
}
nodeDb.set("online_delete", opts.onlineDelete);
return cfg;
}
@@ -1067,6 +1087,621 @@ class Catalogue_test : public beast::unit_test::suite
}
public:
// ---- Editing uncompressed catalogues -----------------------------------
//
// Ledger record: seq, hash, txHash, accountHash, parentHash, drops,
// closeFlags, closeTimeResolution, closeTime, parentCloseTime; then the
// state map and the tx map, each a run of leaf records ending in
// tnTERMINAL. A leaf record is type (1), key (32), and, unless it is a
// tnREMOVE, size (4) and data.
static constexpr std::size_t catLedgerHeaderSize =
4 + 32 * 4 + 8 + sizeof(int) + 4 + 8 + 8;
struct CatLedgerRecord
{
std::size_t header; // first byte of the ledger header
std::size_t state; // first byte of the state map
std::size_t tx; // first byte of the tx map
std::size_t end; // one past the tx map's tnTERMINAL
};
// Position just past the map that starts at `pos`.
static std::size_t
skipCatalogueMap(std::vector<std::uint8_t> const& bytes, std::size_t pos)
{
while (pos < bytes.size())
{
auto const type = bytes[pos++];
if (type == 255) // tnTERMINAL
return pos;
pos += 32;
if (type == 254) // tnREMOVE
continue;
if (pos + 4 > bytes.size())
break;
std::uint32_t size = 0;
std::memcpy(&size, &bytes[pos], 4);
pos += 4 + size;
}
return bytes.size();
}
// Position of the size field of the first leaf (not a removal) in the
// map that starts at `pos`, if the map has one.
static std::optional<std::size_t>
firstLeafSizeField(std::vector<std::uint8_t> const& bytes, std::size_t pos)
{
while (pos < bytes.size())
{
auto const type = bytes[pos];
if (type == 255)
return std::nullopt;
if (type != 254)
return pos + 1 + 32;
pos += 1 + 32;
}
return std::nullopt;
}
static std::vector<CatLedgerRecord>
indexCatalogue(std::vector<std::uint8_t> const& bytes)
{
std::vector<CatLedgerRecord> records;
std::size_t pos = sizeof(TestCATLHeader);
while (pos + catLedgerHeaderSize <= bytes.size())
{
CatLedgerRecord r;
r.header = pos;
r.state = pos + catLedgerHeaderSize;
r.tx = skipCatalogueMap(bytes, r.state);
r.end = skipCatalogueMap(bytes, r.tx);
records.push_back(r);
pos = r.end;
}
return records;
}
static std::vector<std::uint8_t>
readCatalogue(std::string const& path)
{
std::ifstream in(path, std::ios::binary);
return {
std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>()};
}
static void
writeCatalogue(std::string const& path, std::vector<std::uint8_t> const& b)
{
std::ofstream out(path, std::ios::binary | std::ios::trunc);
out.write(reinterpret_cast<char const*>(b.data()), b.size());
}
static Json::Value
loadCatalogue(test::jtx::Env& env, std::string const& path, bool ignoreHash)
{
Json::Value params{Json::objectValue};
params[jss::input_file] = path;
if (ignoreHash)
params[jss::ignore_hash] = true;
return env.client().invoke("catalogue_load", params)[jss::result];
}
static bool
errorContains(Json::Value const& result, std::string const& what)
{
return result[jss::status] == "error" &&
result[jss::error_message].asString().find(what) !=
std::string::npos;
}
void
testCatalogueLoadFailures(FeatureBitset features)
{
testcase("catalogue_load: failures part way through a file");
using namespace test::jtx;
using beast::severities::kDisabled;
boost::filesystem::path tempDir =
boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path();
boost::filesystem::create_directories(tempDir);
auto const goodPath = (tempDir / "good.catl").string();
// An uncompressed catalogue of ledgers 3-6, so records can be edited.
{
Env env{*this, catalogueEnvconfig(), features, nullptr, kDisabled};
prepareLedgerData(env, 3);
Json::Value params{Json::objectValue};
params[jss::min_ledger] = 3;
params[jss::max_ledger] = 6;
params[jss::output_file] = goodPath;
params[jss::compression_level] = 0;
auto const result =
env.client().invoke("catalogue_create", params)[jss::result];
BEAST_EXPECT(result[jss::status] == jss::success);
}
auto const good = readCatalogue(goodPath);
auto const records = indexCatalogue(good);
if (!BEAST_EXPECT(
records.size() == 4 && records.back().end == good.size()))
return;
// Write `bytes` to a new file, load it into a fresh server, and hand
// the result and the resulting pinned set to `check`.
int fileNo = 0;
auto loadEdited = [&](std::vector<std::uint8_t> const& bytes,
bool ignoreHash,
auto&& check) {
auto const path =
(tempDir / ("edited" + std::to_string(++fileNo) + ".catl"))
.string();
writeCatalogue(path, bytes);
Env env{*this, catalogueEnvconfig(), features, nullptr, kDisabled};
auto const result = loadCatalogue(env, path, ignoreHash);
check(
result, env.app().getLedgerMaster().getPinnedLedgersRangeSet());
};
auto const hashField = offsetof(TestCATLHeader, hash);
auto const maxLedgerField = offsetof(TestCATLHeader, max_ledger);
// Offsets within a ledger header record.
constexpr std::size_t accountHashAt = 4 + 32 * 2;
constexpr std::size_t dropsAt = 4 + 32 * 4;
// The stored checksum is wrong but every ledger is intact: all four
// load, then the whole-file check fails and the run is rolled back.
{
auto bytes = good;
bytes[hashField] ^= 0xff;
loadEdited(
bytes, false, [&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(
result, "catalogue hash verification failed"));
BEAST_EXPECT(pinned.empty());
});
}
// A ledger part way through is corrupt. The load stops there, the
// rest of the file is still hashed, the checksum fails, and the
// ledgers loaded before it are rolled back too.
{
auto bytes = good;
bytes[records[2].header + dropsAt] ^= 0x01;
loadEdited(
bytes, false, [&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(
result, "catalogue hash verification failed"));
BEAST_EXPECT(pinned.empty());
});
}
// A checksum failure only rolls back what this run added: ledgers
// that were already pinned stay pinned.
{
auto bytes = good;
bytes[hashField] ^= 0xff;
auto const badPath = (tempDir / "badsum.catl").string();
writeCatalogue(badPath, bytes);
Env env{*this, catalogueEnvconfig(), features, nullptr, kDisabled};
BEAST_EXPECT(
loadCatalogue(env, goodPath, false)[jss::status] ==
jss::success);
BEAST_EXPECT(errorContains(
loadCatalogue(env, badPath, false),
"catalogue hash verification failed"));
auto& lm = env.app().getLedgerMaster();
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-6");
// The pinned store: an empty batch is a no-op, an in-memory
// backend is durable on demand, and a fetch made for
// online_delete's copy pass (duplicate == true) finds nodes
// that exist only in the pinned store.
auto* const pinnedDb = dynamic_cast<NodeStore::DatabasePinnedImp*>(
&env.app().getNodeStore());
if (BEAST_EXPECT(pinnedDb))
{
pinnedDb->storePinnedBatch({});
BEAST_EXPECT(pinnedDb->syncPinned());
auto const stateRoot =
uint256::fromVoid(&good[records[0].header + accountHashAt]);
BEAST_EXPECT(env.app().getNodeStore().fetchNodeObject(
stateRoot, 0, NodeStore::FetchType::synchronous, true));
}
}
// Out of sequence: ledger 4's record claims to be ledger 9.
{
auto bytes = good;
std::uint32_t const nine = 9;
std::memcpy(&bytes[records[1].header], &nine, 4);
loadEdited(
bytes, true, [&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(result, "out of sequence"));
BEAST_EXPECT(boost::icl::contains(pinned, 3u));
BEAST_EXPECT(!boost::icl::contains(pinned, 4u));
});
}
// The header promises a ledger 7 the file does not contain. Ledgers
// 3-6 load (hash checks are off) before the file runs out.
{
auto bytes = good;
std::uint32_t const seven = 7;
std::memcpy(&bytes[maxLedgerField], &seven, 4);
loadEdited(
bytes, true, [&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(
result, "Unexpected end of catalogue file"));
BEAST_EXPECT(to_string(pinned) == "3-6");
});
}
// An absurd leaf size in each kind of map is rejected while parsing.
auto withHugeLeaf = [&](std::size_t sizeField) {
auto bytes = good;
std::uint32_t const huge = 0xFFFFFFFF;
std::memcpy(&bytes[sizeField], &huge, 4);
return bytes;
};
// ... in the base ledger's state map.
if (auto const field = firstLeafSizeField(good, records[0].state);
BEAST_EXPECT(field))
{
loadEdited(
withHugeLeaf(*field),
true,
[&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(
result, "Failed to load base ledger state"));
BEAST_EXPECT(pinned.empty());
});
}
// ... in a later ledger's state delta.
{
std::optional<std::size_t> index;
std::optional<std::size_t> field;
for (std::size_t i = 1; i < records.size() && !field; ++i)
if ((field = firstLeafSizeField(good, records[i].state)))
index = i;
if (BEAST_EXPECT(field))
{
std::uint32_t const seq = 3 + *index;
loadEdited(
withHugeLeaf(*field),
true,
[&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(
result, "Failed to apply ledger delta"));
BEAST_EXPECT(boost::icl::contains(pinned, seq - 1));
BEAST_EXPECT(!boost::icl::contains(pinned, seq));
});
}
}
// ... in a ledger's transaction map.
{
std::optional<std::size_t> index;
std::optional<std::size_t> field;
for (std::size_t i = 0; i < records.size() && !field; ++i)
if ((field = firstLeafSizeField(good, records[i].tx)))
index = i;
if (BEAST_EXPECT(field))
{
std::uint32_t const seq = 3 + *index;
loadEdited(
withHugeLeaf(*field),
true,
[&](auto const& result, auto const& pinned) {
BEAST_EXPECT(errorContains(
result, "Failed to apply ledger delta"));
BEAST_EXPECT(!boost::icl::contains(pinned, seq));
});
}
}
boost::filesystem::remove_all(tempDir);
}
void
testCataloguePinnedStoreRequired(FeatureBitset features)
{
testcase("catalogue_load / catalogue_unpin: pinned store required");
using namespace test::jtx;
using beast::severities::kDisabled;
// pinned_type is set, but [node_db] type=rwdb is the null node
// store, on which SHAMapStore never builds the pinned store.
{
auto cfg = test::jtx::envconfig();
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
nodeDb.set("pinned_type", "rwdb");
nodeDb.set("online_delete", "256");
Env env{*this, std::move(cfg), features, nullptr, kDisabled};
Json::Value params{Json::objectValue};
params[jss::input_file] = "/nonexistent.catl";
auto const result =
env.client().invoke("catalogue_load", params)[jss::result];
BEAST_EXPECT(
errorContains(result, "requires the pinned node store"));
}
// catalogue_unpin needs pinned_type configured at all.
{
Env env{
*this,
catalogueEnvconfig({.withPinnedType = false}),
features,
nullptr,
kDisabled};
Json::Value params{Json::objectValue};
params[jss::ledger_index_min] = 1;
params[jss::ledger_index_max] = 2;
auto const result =
env.client().invoke("catalogue_unpin", params)[jss::result];
BEAST_EXPECT(errorContains(result, "pinned_type"));
}
}
void
testCatalogueChainLinkage(FeatureBitset features)
{
testcase("catalogue_load: Ledgers must chain");
using namespace test::jtx;
Env env{
*this,
catalogueEnvconfig(),
features,
nullptr,
beast::severities::kDisabled,
};
prepareLedgerData(env, 3);
boost::filesystem::path tempDir =
boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path();
boost::filesystem::create_directories(tempDir);
auto cataloguePath = (tempDir / "test.catl").string();
// Uncompressed, so ledger records can be located and edited.
{
Json::Value params{Json::objectValue};
params[jss::min_ledger] = 3;
params[jss::max_ledger] = 6;
params[jss::output_file] = cataloguePath;
params[jss::compression_level] = 0;
auto const result =
env.client().invoke("catalogue_create", params)[jss::result];
BEAST_EXPECT(result[jss::status] == jss::success);
}
std::vector<std::uint8_t> bytes;
{
std::ifstream in(cataloguePath, std::ios::binary);
bytes.assign(
std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>());
}
// Ledger record: seq, hash, txHash, accountHash, parentHash, drops,
// closeFlags, closeTimeResolution, closeTime, parentCloseTime; then
// the state map and tx map as leaf records ending in tnTERMINAL.
constexpr std::size_t ledgerHeaderSize =
4 + 32 * 4 + 8 + sizeof(int) + 4 + 8 + 8;
auto skipMap = [&](std::size_t pos) -> std::size_t {
while (pos < bytes.size())
{
auto const type = bytes[pos++];
if (type == 255) // tnTERMINAL
return pos;
pos += 32;
if (type == 254) // tnREMOVE
continue;
if (pos + 4 > bytes.size())
break;
std::uint32_t size = 0;
std::memcpy(&size, &bytes[pos], 4);
pos += 4 + size;
}
return bytes.size();
};
// Skip the base ledger (3) to reach ledger 4.
std::size_t pos = sizeof(TestCATLHeader) + ledgerHeaderSize;
pos = skipMap(skipMap(pos));
if (!BEAST_EXPECT(pos + ledgerHeaderSize <= bytes.size()))
return;
LedgerInfo info;
std::size_t p = pos;
auto take = [&](void* dst, std::size_t n) {
std::memcpy(dst, &bytes[p], n);
p += n;
};
std::uint64_t drops = 0, closeTime = 0, parentCloseTime = 0;
std::uint32_t resolution = 0;
take(&info.seq, 4);
std::size_t const hashOffset = p;
take(info.hash.data(), 32);
take(info.txHash.data(), 32);
take(info.accountHash.data(), 32);
std::size_t const parentOffset = p;
take(info.parentHash.data(), 32);
take(&drops, 8);
take(&info.closeFlags, sizeof(info.closeFlags));
take(&resolution, 4);
take(&closeTime, 8);
take(&parentCloseTime, 8);
info.drops = drops;
info.closeTime = NetClock::time_point{NetClock::duration{closeTime}};
info.parentCloseTime =
NetClock::time_point{NetClock::duration{parentCloseTime}};
info.closeTimeResolution = NetClock::duration{resolution};
BEAST_EXPECT(info.seq == 4);
// Sanity: the record parses to a ledger whose hash checks out.
BEAST_EXPECT(calculateLedgerHash(info) == info.hash);
// Re-parent ledger 4 and fix up its hash, so the file stays
// internally consistent ledger by ledger; only the chain check can
// catch it.
info.parentHash = ~info.parentHash;
info.hash = calculateLedgerHash(info);
std::memcpy(&bytes[parentOffset], info.parentHash.data(), 32);
std::memcpy(&bytes[hashOffset], info.hash.data(), 32);
{
std::ofstream out(
cataloguePath, std::ios::binary | std::ios::trunc);
out.write(
reinterpret_cast<char const*>(bytes.data()), bytes.size());
}
Env loadEnv{
*this,
catalogueEnvconfig(),
features,
nullptr,
beast::severities::kDisabled,
};
Json::Value params{Json::objectValue};
params[jss::input_file] = cataloguePath;
params[jss::ignore_hash] = true; // the file was edited
auto const result =
loadEnv.client().invoke("catalogue_load", params)[jss::result];
BEAST_EXPECT(result[jss::status] == "error");
BEAST_EXPECT(
result[jss::error_message].asString().find(
"does not chain to its predecessor") != std::string::npos);
// The base ledger verified and stays pinned; nothing after it does.
auto const pinned =
loadEnv.app().getLedgerMaster().getPinnedLedgersRangeSet();
BEAST_EXPECT(boost::icl::contains(pinned, 3u));
BEAST_EXPECT(!boost::icl::contains(pinned, 4u));
boost::filesystem::remove_all(tempDir);
}
void
testCatalogueUnpin(FeatureBitset features)
{
testcase("catalogue_unpin");
using namespace test::jtx;
Env env{
*this,
catalogueEnvconfig(),
features,
nullptr,
beast::severities::kDisabled,
};
prepareLedgerData(env, 5);
boost::filesystem::path tempDir =
boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path();
boost::filesystem::create_directories(tempDir);
auto cataloguePath = (tempDir / "test.catl").string();
{
Json::Value params{Json::objectValue};
params[jss::min_ledger] = 3;
params[jss::max_ledger] = 8;
params[jss::output_file] = cataloguePath;
auto const result =
env.client().invoke("catalogue_create", params)[jss::result];
BEAST_EXPECT(result[jss::status] == jss::success);
}
{
Json::Value params{Json::objectValue};
params[jss::input_file] = cataloguePath;
auto const result =
env.client().invoke("catalogue_load", params)[jss::result];
BEAST_EXPECT(result[jss::status] == jss::success);
}
auto& lm = env.app().getLedgerMaster();
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-8");
auto unpin = [&](std::optional<Json::Value> min,
std::optional<Json::Value> max) {
Json::Value params{Json::objectValue};
if (min)
params[jss::ledger_index_min] = *min;
if (max)
params[jss::ledger_index_max] = *max;
return env.client().invoke("catalogue_unpin", params)[jss::result];
};
// Bad parameters
BEAST_EXPECT(unpin({}, {})[jss::status] == "error");
BEAST_EXPECT(unpin(5, {})[jss::status] == "error");
BEAST_EXPECT(unpin(6, 5)[jss::status] == "error");
BEAST_EXPECT(unpin(-1, 5)[jss::status] == "error");
BEAST_EXPECT(unpin("x", 5)[jss::status] == "error");
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-8");
// Cut a hole
{
auto const result = unpin(5, 6);
BEAST_EXPECT(result[jss::status] == jss::success);
BEAST_EXPECT(result[jss::unpinned] == "5-6");
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "3-4,7-8");
BEAST_EXPECT(lm.isPinned(4) && lm.isPinned(7));
BEAST_EXPECT(!lm.isPinned(5) && !lm.isPinned(6));
}
// A range only partly pinned unpins just the pinned part
{
auto const result = unpin(1, 4);
BEAST_EXPECT(result[jss::status] == jss::success);
BEAST_EXPECT(result[jss::unpinned] == "3-4");
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "7-8");
}
// Nothing pinned in range: success, nothing unpinned. (Unsigned
// JSON values are accepted as well as non-negative signed ones.)
{
auto const result = unpin(Json::UInt(5), Json::UInt(6));
BEAST_EXPECT(result[jss::status] == jss::success);
BEAST_EXPECT(result[jss::unpinned] == "empty");
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "7-8");
}
// Unpinning the rest leaves nothing pinned. (state.db then records
// an empty set, which must not stop the next start; see
// SHAMapStore_test.)
{
auto const result = unpin(1, 100);
BEAST_EXPECT(result[jss::status] == jss::success);
BEAST_EXPECT(result[jss::unpinned] == "7-8");
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "empty");
BEAST_EXPECT(lm.getPinnedLedgersRangeSet().empty());
}
// Re-loading the file pins it again
{
Json::Value params{Json::objectValue};
params[jss::input_file] = cataloguePath;
auto const result =
env.client().invoke("catalogue_load", params)[jss::result];
BEAST_EXPECT(result[jss::status] == jss::success);
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-8");
}
boost::filesystem::remove_all(tempDir);
}
void
run() override
{
@@ -1081,6 +1716,10 @@ public:
testCatalogueFileSize(all);
testCatalogueCompression(all);
testCatalogueStatus(all);
testCatalogueChainLinkage(all);
testCatalogueUnpin(all);
testCatalogueLoadFailures(all);
testCataloguePinnedStoreRequired(all);
}
};
+198
View File
@@ -0,0 +1,198 @@
//------------------------------------------------------------------------------
/*
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
+94
View File
@@ -1934,6 +1934,56 @@ static RPCCallTestData const rpcCallTestArray[] = {
RPCCallTestData::bad_cast,
R"()"},
// catalogue_unpin
// ------------------------------------------------------------
{"catalogue_unpin: minimal.",
__LINE__,
{"catalogue_unpin", "5000001", "6000000"},
RPCCallTestData::no_exception,
R"({
"method" : "catalogue_unpin",
"params" : [
{
"api_version" : %API_VER%,
"ledger_index_max" : 6000000,
"ledger_index_min" : 5000001
}
]
})"},
{"catalogue_unpin: too few arguments.",
__LINE__,
{"catalogue_unpin", "5000001"},
RPCCallTestData::no_exception,
R"({
"method" : "catalogue_unpin",
"params" : [
{
"error" : "badSyntax",
"error_code" : 1,
"error_message" : "Syntax error."
}
]
})"},
{"catalogue_unpin: too many arguments.",
__LINE__,
{"catalogue_unpin", "1", "2", "3"},
RPCCallTestData::no_exception,
R"({
"method" : "catalogue_unpin",
"params" : [
{
"error" : "badSyntax",
"error_code" : 1,
"error_message" : "Syntax error."
}
]
})"},
{"catalogue_unpin: non-integer bound.",
__LINE__,
{"catalogue_unpin", "first", "6000000"},
RPCCallTestData::bad_cast,
R"()"},
// channel_authorize
// -----------------------------------------------------------
{"channel_authorize: minimal.",
@@ -2998,6 +3048,50 @@ 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.",
+31
View File
@@ -103,6 +103,35 @@ public:
BEAST_EXPECT(fieldNames.insert(name).second);
}
}
// hook api, keylet types and return codes
{
auto const& hook = result[jss::result][jss::HOOK];
BEAST_EXPECT(hook.isMember(jss::API));
BEAST_EXPECT(hook.isMember(jss::KEYLET_TYPES));
BEAST_EXPECT(hook.isMember(jss::RETURN_CODES));
auto const& accept = hook[jss::API]["accept"];
BEAST_EXPECT(accept["return"] == "int64_t");
BEAST_EXPECT(accept["params"].size() == 3);
BEAST_EXPECT(accept["params"][0u][jss::type] == "uint32_t");
BEAST_EXPECT(accept["params"][0u][jss::name] == "read_ptr");
BEAST_EXPECT(accept["params"][2u][jss::type] == "int64_t");
BEAST_EXPECT(accept["params"][2u][jss::name] == "error_code");
BEAST_EXPECT(!accept.isMember("amendment"));
auto const& burden = hook[jss::API]["etxn_burden"];
BEAST_EXPECT(burden["params"].size() == 0);
auto const& xpop = hook[jss::API]["xpop_slot"];
BEAST_EXPECT(xpop["amendment"] == "HooksUpdate1");
BEAST_EXPECT(hook[jss::KEYLET_TYPES]["HOOK"] == 1);
BEAST_EXPECT(hook[jss::KEYLET_TYPES]["MANIFEST"] == 37);
BEAST_EXPECT(hook[jss::RETURN_CODES]["SUCCESS"] == 0);
BEAST_EXPECT(hook[jss::RETURN_CODES]["INVALID_FLOAT"] == -10024);
BEAST_EXPECT(hook[jss::RETURN_CODES]["TOO_MANY_NAMESPACES"] == -45);
}
}
void
@@ -132,6 +161,7 @@ public:
BEAST_EXPECT(
!result[jss::result].isMember(jss::TRANSACTION_FLAGS_INDICES));
BEAST_EXPECT(!result[jss::result].isMember(jss::TYPES));
BEAST_EXPECT(!result[jss::result].isMember(jss::HOOK));
BEAST_EXPECT(result[jss::result].isMember(jss::hash));
}
@@ -156,6 +186,7 @@ public:
BEAST_EXPECT(
result[jss::result].isMember(jss::TRANSACTION_FLAGS_INDICES));
BEAST_EXPECT(result[jss::result].isMember(jss::TYPES));
BEAST_EXPECT(result[jss::result].isMember(jss::HOOK));
BEAST_EXPECT(result[jss::result].isMember(jss::hash));
}
}
+13 -5
View File
@@ -847,6 +847,14 @@ public:
Account alice("alice");
Account bob("bob");
Account carol("carol");
// Account history loads ledgers by seq. type=rwdb is tree-only
// and cannot reconstruct those maps from the node store.
auto storing = [] {
auto cfg = envconfig();
cfg->section(ConfigSection::nodeDatabase()).set("type", "memory");
return cfg;
};
Account david("david");
///////////////////////////////////////////////////////////////////
@@ -1015,7 +1023,7 @@ public:
*
* also test subscribe to the account before it is created
*/
Env env(*this);
Env env(*this, storing());
auto wscTxHistory = makeWSClient(env.app().config());
Json::Value request;
request[jss::account_history_tx_stream] = Json::objectValue;
@@ -1061,7 +1069,7 @@ public:
* subscribe genesis account tx history without txns
* subscribe to bob's account after it is created
*/
Env env(*this);
Env env(*this, storing());
auto wscTxHistory = makeWSClient(env.app().config());
Json::Value request;
request[jss::account_history_tx_stream] = Json::objectValue;
@@ -1140,7 +1148,7 @@ public:
* subscribe account and subscribe account tx history
* and compare txns streamed
*/
Env env(*this);
Env env(*this, storing());
auto wscAccount = makeWSClient(env.app().config());
auto wscTxHistory = makeWSClient(env.app().config());
@@ -1211,7 +1219,7 @@ public:
* alice issues USD to carol
* mix USD and XRP payments
*/
Env env(*this);
Env env(*this, storing());
auto const USD_a = alice["USD"];
std::array<Account, 2> accounts = {alice, carol};
@@ -1250,7 +1258,7 @@ public:
/*
* long transaction history
*/
Env env(*this);
Env env(*this, storing());
std::array<Account, 2> accounts = {alice, carol};
env.fund(XRP(444444), accounts);
env.close();
+95
View File
@@ -36,6 +36,8 @@
#include <boost/utility/in_place_factory.hpp>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <thread>
@@ -158,6 +160,7 @@ public:
onUDPMessage(
std::string const& message,
boost::asio::ip::tcp::endpoint const& remoteEndpoint,
Port const& port,
std::function<void(std::string const&)> sendResponse)
{
}
@@ -371,6 +374,7 @@ public:
onUDPMessage(
std::string const& message,
boost::asio::ip::tcp::endpoint const& remoteEndpoint,
Port const& port,
std::function<void(std::string const&)> sendResponse)
{
}
@@ -404,6 +408,96 @@ public:
pass();
}
// Records the datagrams delivered to onUDPMessage
struct UDPHandler : TestHandler
{
std::mutex mutex;
std::condition_variable cv;
std::vector<std::pair<std::string, bool>> received;
void
onUDPMessage(
std::string const& message,
boost::asio::ip::tcp::endpoint const& remoteEndpoint,
Port const& port,
std::function<void(std::string const&)> sendResponse)
{
std::lock_guard lock(mutex);
received.emplace_back(message, port.has_peer());
cv.notify_all();
}
bool
wait(std::size_t count, std::chrono::milliseconds timeout)
{
std::unique_lock lock(mutex);
return cv.wait_for(
lock, timeout, [&] { return received.size() >= count; });
}
};
// A peer port with udp_highway set also accepts UDP datagrams on the
// same port number; one without it does not.
void
udpHighwayTests()
{
testcase("UDP Superhighway door");
using namespace std::chrono_literals;
using udp = boost::asio::ip::udp;
TestSink sink{*this};
TestThread thread;
sink.threshold(beast::severities::Severity::kAll);
beast::Journal journal{sink};
auto const addr = boost::asio::ip::make_address(getEnvLocalhostAddr());
auto run = [&](bool highway) {
UDPHandler handler;
auto s = make_Server(handler, thread.get_io_context(), journal);
std::vector<Port> ports(1);
ports.back().name = "peer";
ports.back().ip = addr;
ports.back().port = 0;
ports.back().protocol.insert("peer");
ports.back().udp_highway = highway;
auto const eps = s->ports(ports);
if (!BEAST_EXPECT(eps.size() == 1))
return;
// The TCP door picked the port number; the UDP door shares it.
auto const port = eps.begin()->second.port();
BEAST_EXPECT(port != 0);
boost::asio::io_context io;
udp::socket client(io, udp::endpoint(addr, 0));
std::string const message = "XUSHTEST";
client.send_to(
boost::asio::buffer(message), udp::endpoint(addr, port));
if (highway)
{
if (BEAST_EXPECT(handler.wait(1, 5s)))
{
std::lock_guard lock(handler.mutex);
BEAST_EXPECT(handler.received.front().first == message);
BEAST_EXPECT(handler.received.front().second);
}
}
else
{
BEAST_EXPECT(!handler.wait(1, 200ms));
}
s = nullptr;
};
run(true);
run(false);
}
void
testBadConfig()
{
@@ -547,6 +641,7 @@ public:
{
basicTests();
stressTest();
udpHighwayTests();
testBadConfig();
}
};
+43
View File
@@ -20,11 +20,13 @@
#include <test/shamap/common.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpld/shamap/SHAMap.h>
#include <xrpld/shamap/SHAMapInnerNode.h>
#include <xrpld/shamap/SHAMapItem.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/random.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/beast/xor_shift_engine.h>
#include <xrpl/protocol/Serializer.h>
namespace ripple {
namespace tests {
@@ -84,12 +86,53 @@ public:
return true;
}
void
testNullBackendIgnoresFullBelow(beast::Journal const& journal)
{
testcase("null backend does not skip children via FullBelowCache");
TestNodeFamily sourceFamily(journal);
TestNodeFamily destFamily(journal);
destFamily.setNullBackend(true);
SHAMap source(SHAMapType::FREE, sourceFamily);
for (int i = 0; i < 32; ++i)
source.addItem(SHAMapNodeType::tnACCOUNT_STATE, makeRandomAS());
source.setImmutable();
auto const rootHash = source.getHash();
auto const root = source.getLinkedNode(SHAMapNodeID{});
if (!BEAST_EXPECT(root && root->isInner()))
return;
auto* inner = static_cast<SHAMapInnerNode*>(root.get());
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
{
if (!inner->isEmptyBranch(b))
{
destFamily.getFullBelowCache()->insert(
inner->getChildHash(b).as_uint256());
}
}
SHAMap destination(SHAMapType::FREE, destFamily);
destination.setSynching();
Serializer s;
source.serializeRoot(s);
BEAST_EXPECT(
destination.addRootNode(rootHash, s.slice(), nullptr).isGood());
auto const missing = destination.getMissingNodes(2048, nullptr);
BEAST_EXPECT(!missing.empty());
}
void
run() override
{
using namespace beast::severities;
test::SuiteJournal journal("SHAMapSync_test", *this);
testNullBackendIgnoresFullBelow(journal);
TestNodeFamily f(journal), f2(journal);
SHAMap source(SHAMapType::FREE, f);
SHAMap destination(SHAMapType::FREE, f2);
+303
View File
@@ -19,11 +19,19 @@
#include <test/shamap/common.h>
#include <test/unit_test/SuiteJournal.h>
#include <xrpld/nodestore/NodeObject.h>
#include <xrpld/shamap/SHAMap.h>
#include <xrpld/shamap/SHAMapInnerNode.h>
#include <xrpld/shamap/SHAMapTreeNode.h>
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Buffer.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/Serializer.h>
#include <algorithm>
#include <optional>
#include <vector>
namespace ripple {
namespace tests {
@@ -122,6 +130,301 @@ public:
run(true, journal);
run(false, journal);
testCanonicalizeMergesInnerChildren(journal);
testCanonicalizeSharesDuplicateContent(journal);
testGetLinkedNode(journal);
testUpsertAndFlushToSink(journal);
}
void
testCanonicalizeSharesDuplicateContent(beast::Journal const& journal)
{
testcase("canonicalize shares nodes of identical maps");
tests::TestNodeFamily f(journal);
SHAMap a{SHAMapType::FREE, f};
SHAMap b{SHAMapType::FREE, f};
constexpr uint256 h1(
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
constexpr uint256 h2(
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
for (auto* map : {&a, &b})
{
BEAST_EXPECT(map->addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h1, IntToVUC(1))));
BEAST_EXPECT(map->addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h2, IntToVUC(2))));
}
// Do not call getHash() first: it unshares the map and flushDirty
// then has nothing to write.
auto const harvestedBefore = SHAMap::canonicalInnerBranchesHarvested();
a.flushDirty(hotTRANSACTION_NODE);
b.flushDirty(hotTRANSACTION_NODE);
// b's root and leaves were canonicalized onto a's. a's root was
// already fully linked, so there was nothing to harvest, and the
// leaf duplicates take the non-inner early return.
auto const rootA = a.getLinkedNode(SHAMapNodeID{});
auto const rootB = b.getLinkedNode(SHAMapNodeID{});
if (!BEAST_EXPECT(rootA && rootB && rootA->isInner()))
return;
BEAST_EXPECT(rootA.get() == rootB.get());
BEAST_EXPECT(
SHAMap::canonicalInnerBranchesHarvested() == harvestedBefore);
auto* inner = static_cast<SHAMapInnerNode*>(rootA.get());
int shared = 0;
for (int branch = 0; branch < SHAMapInnerNode::branchFactor; ++branch)
{
if (inner->isEmptyBranch(branch))
continue;
auto const id = SHAMapNodeID{}.getChildNodeID(branch);
auto const leafA = a.getLinkedNode(id);
auto const leafB = b.getLinkedNode(id);
BEAST_EXPECT(leafA && leafB && leafA.get() == leafB.get());
++shared;
}
BEAST_EXPECT(shared == 2);
}
void
testCanonicalizeMergesInnerChildren(beast::Journal const& journal)
{
testcase("canonicalize harvests inner children onto cache winner");
tests::TestNodeFamily f(journal);
SHAMap map{SHAMapType::FREE, f};
constexpr uint256 h1(
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
constexpr uint256 h2(
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
constexpr uint256 h3(
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
constexpr uint256 h4(
"c92891fe4ef6cee585fdc6fda2e09eb4d386363158ec3321b8123e5a772c6ca8");
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h1, IntToVUC(1))));
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h2, IntToVUC(2))));
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h3, IntToVUC(3))));
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h4, IntToVUC(4))));
// getHash() unshares the map (cowid=0), which makes flushDirty a
// no-op. Snapshot first so the original stays dirty and still
// canonicalizes onto the cache shell.
auto snap = map.snapShot(false);
auto const rootHash = snap->getHash();
Serializer s;
snap->serializeRoot(s);
auto shell = SHAMapTreeNode::makeFromWire(s.slice());
if (!BEAST_EXPECT(
shell && shell->isInner() && shell->getHash() == rootHash))
return;
auto innerShell = std::static_pointer_cast<SHAMapInnerNode>(shell);
int linkedBefore = 0;
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
{
if (!innerShell->isEmptyBranch(b) && innerShell->getChildPointer(b))
++linkedBefore;
}
BEAST_EXPECT(linkedBefore == 0);
f.getTreeNodeCache()->canonicalize_replace_client(
rootHash.as_uint256(), shell);
auto const harvestedBefore = SHAMap::canonicalInnerBranchesHarvested();
map.flushDirty(hotTRANSACTION_NODE);
auto cached = f.getTreeNodeCache()->fetch(rootHash.as_uint256());
if (!BEAST_EXPECT(cached && cached->isInner()))
return;
auto* inner = static_cast<SHAMapInnerNode*>(cached.get());
int linkedAfter = 0;
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
{
if (!inner->isEmptyBranch(b) && inner->getChildPointer(b))
++linkedAfter;
}
BEAST_EXPECT(linkedAfter > 0);
BEAST_EXPECT(
SHAMap::canonicalInnerBranchesHarvested() > harvestedBefore);
auto const rootNode = map.getLinkedNode(SHAMapNodeID{});
if (!BEAST_EXPECT(rootNode && rootNode->isInner()))
return;
auto* liveInner = static_cast<SHAMapInnerNode*>(rootNode.get());
int liveLinked = 0;
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
{
if (liveInner->isEmptyBranch(b))
{
BEAST_EXPECT(
!map.getLinkedNode(SHAMapNodeID{}.getChildNodeID(b)));
continue;
}
auto const child =
map.getLinkedNode(SHAMapNodeID{}.getChildNodeID(b));
if (liveInner->getChildPointer(b))
{
++liveLinked;
BEAST_EXPECT(
child && child->getHash() == liveInner->getChildHash(b));
}
}
BEAST_EXPECT(liveLinked > 0);
}
void
testUpsertAndFlushToSink(beast::Journal const& journal)
{
testcase("upsertGiveItem and flushDirty to a sink");
tests::TestNodeFamily f(journal);
SHAMap a{SHAMapType::FREE, f}; // built with upsertGiveItem
SHAMap b{SHAMapType::FREE, f}; // reference: addItem/updateGiveItem
// Distinct first nibbles: a root with three leaf children.
constexpr uint256 h1(
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
constexpr uint256 h2(
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
constexpr uint256 h3(
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
auto const nm = SHAMapNodeType::tnTRANSACTION_NM;
// Absent keys are added.
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h1, IntToVUC(1))));
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h2, IntToVUC(2))));
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h3, IntToVUC(3))));
// A present key has its data replaced.
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h2, IntToVUC(20))));
// A change of leaf type is refused and leaves the item untouched.
BEAST_EXPECT(!a.upsertGiveItem(
SHAMapNodeType::tnTRANSACTION_MD,
make_shamapitem(h1, IntToVUC(10))));
BEAST_EXPECT(b.addItem(nm, make_shamapitem(h1, IntToVUC(1))));
BEAST_EXPECT(b.addItem(nm, make_shamapitem(h2, IntToVUC(2))));
BEAST_EXPECT(b.addItem(nm, make_shamapitem(h3, IntToVUC(3))));
BEAST_EXPECT(b.updateGiveItem(nm, make_shamapitem(h2, IntToVUC(20))));
{
auto const v20 = IntToVUC(20);
auto const& item = a.peekItem(h2);
BEAST_EXPECT(
item && item->slice() == Slice(v20.data(), v20.size()));
auto const v1 = IntToVUC(1);
auto const& item1 = a.peekItem(h1);
BEAST_EXPECT(
item1 && item1->slice() == Slice(v1.data(), v1.size()));
}
// Flush to a sink. Do not call getHash() on `a` first: it unshares
// the map and the flush would then have nothing to write.
auto const cacheBefore = f.getTreeNodeCache()->size();
std::vector<uint256> written;
bool sinkArgsOk = true;
SHAMap::FlushSink const sink =
[&](NodeObjectType t, Blob&& data, uint256 const& hash) {
sinkArgsOk =
sinkArgsOk && t == hotTRANSACTION_NODE && !data.empty();
written.push_back(hash);
};
a.flushDirty(hotTRANSACTION_NODE, sink);
BEAST_EXPECT(sinkArgsOk);
BEAST_EXPECT(written.size() == 4); // root + three leaves
// Unlike flushDirty(t), nothing was canonicalized into the cache.
BEAST_EXPECT(f.getTreeNodeCache()->size() == cacheBefore);
// The upserted map matches the reference, and its root was written.
BEAST_EXPECT(a.getHash() == b.getHash());
BEAST_EXPECT(
std::find(
written.begin(), written.end(), a.getHash().as_uint256()) !=
written.end());
// Everything is now shared: a second flush writes nothing.
written.clear();
a.flushDirty(hotTRANSACTION_NODE, sink);
BEAST_EXPECT(written.empty());
}
void
testGetLinkedNode(beast::Journal const& journal)
{
testcase("getLinkedNode walks only already-linked children");
tests::TestNodeFamily f(journal);
SHAMap map{SHAMapType::FREE, f};
constexpr uint256 h1(
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
constexpr uint256 h2(
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
constexpr uint256 h3(
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
constexpr uint256 h4(
"c92891fe4ef6cee585fdc6fda2e09eb4d386363158ec3321b8123e5a772c6ca8");
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h1, IntToVUC(1))));
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h2, IntToVUC(2))));
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h3, IntToVUC(3))));
BEAST_EXPECT(map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(h4, IntToVUC(4))));
auto const root = map.getLinkedNode(SHAMapNodeID{});
if (!BEAST_EXPECT(root && root->isInner()))
return;
auto* inner = static_cast<SHAMapInnerNode*>(root.get());
std::optional<int> leafBranch;
std::optional<int> emptyBranch;
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
{
if (inner->isEmptyBranch(b))
{
if (!emptyBranch)
emptyBranch = b;
continue;
}
auto const child =
map.getLinkedNode(SHAMapNodeID{}.getChildNodeID(b));
BEAST_EXPECT(child);
if (child && !child->isInner() && !leafBranch)
leafBranch = b;
}
if (BEAST_EXPECT(emptyBranch))
{
BEAST_EXPECT(!map.getLinkedNode(
SHAMapNodeID{}.getChildNodeID(*emptyBranch)));
}
if (BEAST_EXPECT(leafBranch))
{
// A leaf has no children. Asking for a grandchild must miss.
auto const leafId = SHAMapNodeID{}.getChildNodeID(*leafBranch);
BEAST_EXPECT(!map.getLinkedNode(leafId.getChildNodeID(0)));
}
}
void
+15
View File
@@ -120,11 +120,26 @@ public:
tnCache_->reset();
}
bool
isNullBackend() const override
{
return nullBackend_;
}
void
setNullBackend(bool v)
{
nullBackend_ = v;
}
beast::manual_clock<std::chrono::steady_clock>
clock()
{
return clock_;
}
private:
bool nullBackend_ = false;
};
} // namespace tests
+135 -3
View File
@@ -41,8 +41,11 @@
#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>
@@ -348,6 +351,21 @@ 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
// validationSET_EXPIRES (10 min, < 256 ledgers) get swept, and we fail
// the next flag's score table too.
if (prevLedger->isVotingLedger())
{
auto const seq = prevLedger->info().seq + 1;
app_.getValidations().setSeqToKeep(seq - 1, seq + FLAG_LEDGER_INTERVAL);
}
// Add pseudo-transactions to the set
if (app_.config().standalone() || (proposing && !wrongLCL))
{
@@ -378,6 +396,26 @@ 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
@@ -389,9 +427,10 @@ RCLConsensus::Adaptor::onClose(
RCLCensorshipDetector<TxID, LedgerIndex>::TxIDSeqVec proposed;
initialSet->visitLeaves(
[&proposed,
seq](boost::intrusive_ptr<SHAMapItem const> const& item) {
proposed.emplace_back(item->key(), seq);
[&proposed, &entropyTxID, seq](
boost::intrusive_ptr<SHAMapItem const> const& item) {
if (item->key() != entropyTxID)
proposed.emplace_back(item->key(), seq);
});
censorshipDetector_.propose(std::move(proposed));
@@ -411,6 +450,99 @@ 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,10 +34,12 @@
#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>
@@ -91,6 +93,11 @@ 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;
@@ -182,7 +189,23 @@ 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
+59 -4
View File
@@ -23,17 +23,22 @@
#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>
@@ -167,6 +172,40 @@ 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,
@@ -191,13 +230,29 @@ 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.
if (app.validatorManifests().getMasterKey(signingKey) == signingKey)
auto& manifests = app.validatorManifests();
std::shared_ptr<ReadView const> view;
if (manifests.getMasterKey(signingKey) == signingKey)
{
if (auto const view = app.openLedger().current();
view && view->rules().enabled(featureOnChainManifests))
app.validatorManifests().applyLedgerSigningKey(*view, signingKey);
view = app.openLedger().current();
if (view && !view->rules().enabled(featureOnChainManifests))
view.reset();
}
// 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,6 +491,16 @@ 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;
@@ -630,6 +640,10 @@ 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
+7 -2
View File
@@ -86,7 +86,7 @@ bool
canHook(
STTx const& tx,
ripple::uint256 hookOn,
std::optional<ripple::Slice> hookName);
std::optional<ripple::Blob> hookName);
bool
canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit);
@@ -129,7 +129,11 @@ apply(
uint32_t wasmParam,
uint8_t hookChainPosition,
// result of apply() if this is weak exec
std::shared_ptr<STObject const> const& provisionalMeta);
std::shared_ptr<STObject const> const& provisionalMeta,
// fix20261005: the original emitted transaction for a ttEMIT_FAILURE
// callback, captured before the cleanup pseudo removed its
// ltEMITTED_TXN entry. Ignored before the fix.
std::optional<ripple::STObject> const& emitFailureTxn = std::nullopt);
struct HookContext;
@@ -207,6 +211,7 @@ 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{};
+206 -10
View File
@@ -4,7 +4,10 @@
#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 {
@@ -69,7 +72,13 @@ Expected<bool, HookReturnCode>
HookAPI::sto_validate(Bytes const& data) const
{
if (data.size() < 2)
{
// fix20261005: a buffer this short can never hold a valid STObject,
// so report it as invalid (0) as documented, rather than TOO_SMALL
if (hookCtx.applyCtx.view().rules().enabled(fix20261005))
return false;
return Unexpected(TOO_SMALL);
}
unsigned char* start = const_cast<unsigned char*>(data.data());
unsigned char* upto = start;
@@ -1029,15 +1038,21 @@ HookAPI::float_set(int32_t exponent, int64_t mantissa) const
auto normalized = hook_float::normalize_xfl(mantissa, exponent);
// the above function will underflow into a canonical 0
// but this api must report that underflow
// but this api must report that underflow.
// The range check is on the exponent *after* the mantissa is normalized
// to 16 digits. fix20261005: report overflow / underflow as
// EXPONENT_OVERSIZED / EXPONENT_UNDERSIZED; before the fix both were
// reported as INVALID_FLOAT.
bool const fixExpCodes =
hookCtx.applyCtx.view().rules().enabled(fix20261005);
if (!normalized)
{
if (normalized.error() == XFL_OVERFLOW)
return Unexpected(INVALID_FLOAT);
return Unexpected(fixExpCodes ? EXPONENT_OVERSIZED : INVALID_FLOAT);
return Unexpected(normalized.error());
}
if (normalized.value() == 0)
return Unexpected(INVALID_FLOAT);
return Unexpected(fixExpCodes ? EXPONENT_UNDERSIZED : INVALID_FLOAT);
return normalized;
}
@@ -1569,6 +1584,18 @@ HookAPI::otxn_field(uint32_t field_id) const
if (fieldType == sfInvalid)
return Unexpected(INVALID_FIELD);
// fix20261005: on an emit failure callback the field is read from the
// original emitted transaction, so check presence there rather than on
// the ttEMIT_FAILURE pseudo (which carries sfLedgerSequence and
// sfTransactionHash the emitted txn lacks, and lacks most of its fields).
if (hookCtx.emitFailure &&
hookCtx.applyCtx.view().rules().enabled(fix20261005))
{
if (!hookCtx.emitFailure->isFieldPresent(fieldType))
return Unexpected(DOESNT_EXIST);
return &hookCtx.emitFailure->getField(fieldType);
}
if (!hookCtx.applyCtx.tx.isFieldPresent(fieldType))
return Unexpected(DOESNT_EXIST);
@@ -1883,6 +1910,147 @@ 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
{
@@ -1893,6 +2061,10 @@ 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);
@@ -2137,6 +2309,12 @@ 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);
@@ -2160,6 +2338,11 @@ 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);
@@ -2816,11 +2999,19 @@ HookAPI::set_state_cache(
if (availableForReserves < hookStateScale && modified)
return Unexpected(RESERVE_INSUFFICIENT);
// Prior to fix20261005 the first-account branch charged write
// resources (a state modification, a reserve slot and, if needed, a
// namespace slot) even when the entry was only being read into the
// cache (modified == false). Every other branch only charges for
// writes. With the fix, reads into the cache are free; a later write
// of the same key is charged by the existing-key branch below.
bool const chargeWrite = modified || !view.rules().enabled(fix20261005);
int64_t namespaceCount = accSLE->isFieldPresent(sfHookNamespaces)
? accSLE->getFieldV256(sfHookNamespaces).size()
: 0;
if (createNamespace)
if (createNamespace && chargeWrite)
{
// overflow should never ever happen but check anyway
if (namespaceCount + 1 < namespaceCount)
@@ -2830,15 +3021,20 @@ HookAPI::set_state_cache(
return Unexpected(TOO_MANY_NAMESPACES);
}
stateMap.modified_entry_count++;
if (chargeWrite)
{
stateMap.modified_entry_count++;
// sanity check
if (view.rules().enabled(featureExtendedHookState) &&
availableForReserves < hookStateScale)
return Unexpected(INTERNAL_ERROR);
// sanity check
if (view.rules().enabled(featureExtendedHookState) &&
availableForReserves < hookStateScale)
return Unexpected(INTERNAL_ERROR);
availableForReserves -= hookStateScale;
}
stateMap[acc] = {
availableForReserves - hookStateScale,
availableForReserves,
namespaceCount,
hookStateScale,
{{ns, {{key, {modified, data}}}}}};
+84 -11
View File
@@ -558,7 +558,8 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
case ttFEE:
case ttUNL_MODIFY:
case ttEMIT_FAILURE:
case ttUNL_REPORT: {
case ttUNL_REPORT:
case ttENTROPY: {
break;
}
default: {
@@ -813,7 +814,7 @@ bool
hook::canHook(
STTx const& tx,
ripple::uint256 hookOn,
std::optional<ripple::Slice> hookName)
std::optional<ripple::Blob> hookName)
{
if (!canHookTT(tx.getTxnType(), hookOn))
return false;
@@ -827,7 +828,7 @@ hook::canHook(
// transaction, so we can't hook without the hook name
return false;
return tx[sfHookName] == *hookName;
return tx.getFieldVL(sfHookName) == *hookName;
}
ripple::uint256
@@ -1041,8 +1042,29 @@ hook::apply(
bool isStrong,
uint32_t wasmParam,
uint8_t hookChainPosition,
std::shared_ptr<STObject const> const& provisionalMeta)
std::shared_ptr<STObject const> const& provisionalMeta,
std::optional<ripple::STObject> const& emitFailureTxn)
{
std::optional<ripple::STObject> emitFailure;
if (isCallback && (wasmParam & 1))
{
if (applyCtx.view().rules().enabled(fix20261005))
{
// fix20261005: by the time the callback runs the ttEMIT_FAILURE
// pseudo has already erased the ltEMITTED_TXN entry from this
// view, so it can't be read back here. The caller captures the
// original emitted transaction before base application.
emitFailure = emitFailureTxn;
}
else
{
// pre-fix: reads the entry the pseudo has already erased
emitFailure = (*(applyCtx.view().peek(keylet::emittedTxn(
applyCtx.tx.getFieldH256(sfTransactionHash)))))
.downcast<STObject>();
}
}
HookContext hookCtx = {
.applyCtx = applyCtx,
// we will return this context object (RVO / move constructed)
@@ -1074,12 +1096,7 @@ hook::apply(
.foreignStateSetDisabled = false,
.provisionalMeta = provisionalMeta,
.foreignStateGrantCache = {}},
.emitFailure = isCallback && wasmParam & 1
? std::optional<ripple::STObject>(
(*(applyCtx.view().peek(keylet::emittedTxn(
applyCtx.tx.getFieldH256(sfTransactionHash)))))
.downcast<STObject>())
: std::optional<ripple::STObject>(),
.emitFailure = std::move(emitFailure),
.module = nullptr,
.api_ = nullptr};
@@ -1546,7 +1563,20 @@ hook::finalizeHookResult(
ptr->add(s);
SerialIter sit(s.slice());
sleEmitted->emplace_back(ripple::STObject(sit, sfEmittedTxn));
ripple::STObject emittedTxn(sit, sfEmittedTxn);
// ltEMITTED_TXN has an SOTemplate, so the new SLE already
// holds a non-present sfEmittedTxn slot. emplace_back()
// appends a second entry behind it, and lookups (which go
// through the template index) keep reporting the field as
// absent until the SLE is serialized and re-read. set()
// replaces the template slot. The serialized entry is the
// same either way, but a hook slotting the entry later in
// the same ledger can observe the difference, so gate it.
if (applyCtx.view().rules().enabled(fix20261005))
sleEmitted->set(std::move(emittedTxn));
else
sleEmitted->emplace_back(std::move(emittedTxn));
auto page = applyCtx.view().dirInsert(
keylet::emittedDir(), emittedId, [&](SLE::ref sle) {
(*sle)[sfFlags] = lsfEmittedDir;
@@ -2853,6 +2883,49 @@ 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,
+3 -2
View File
@@ -25,7 +25,8 @@ namespace ripple {
AcceptedLedger::AcceptedLedger(
std::shared_ptr<ReadView const> const& ledger,
Application& app)
Application& app,
bool withJson)
: mLedger(ledger)
{
transactions_.reserve(256);
@@ -33,7 +34,7 @@ AcceptedLedger::AcceptedLedger(
auto insertAll = [&](auto const& txns) {
for (auto const& item : txns)
transactions_.emplace_back(std::make_unique<AcceptedLedgerTx>(
ledger, item.first, item.second));
ledger, item.first, item.second, withJson));
};
transactions_.reserve(256);
+7 -1
View File
@@ -44,9 +44,15 @@ namespace ripple {
class AcceptedLedger : public CountedObject<AcceptedLedger>
{
public:
/** @param withJson build each transaction's publishable JSON. Pass
false only for ledgers that will never be published from this
object (e.g. pinned catalogue history being saved to SQL);
getJson() on such transactions returns a null value.
*/
AcceptedLedger(
std::shared_ptr<ReadView const> const& ledger,
Application& app);
Application& app,
bool withJson = true);
std::shared_ptr<ReadView const> const&
getLedger() const
+7 -1
View File
@@ -28,7 +28,8 @@ namespace ripple {
AcceptedLedgerTx::AcceptedLedgerTx(
std::shared_ptr<ReadView const> const& ledger,
std::shared_ptr<STTx const> const& txn,
std::shared_ptr<STObject const> const& met)
std::shared_ptr<STObject const> const& met,
bool withJson)
: mTxn(txn)
, mMeta(txn->getTransactionID(), ledger->seq(), *met)
, mAffected(mMeta.getAffectedAccounts())
@@ -41,6 +42,11 @@ AcceptedLedgerTx::AcceptedLedgerTx(
met->add(s);
mRawMeta = std::move(s.modData());
// The JSON (full tx + meta + hex raw meta per transaction) is only
// needed for publishing. Bulk SQL saves of pinned history skip it.
if (!withJson)
return;
mJson = Json::objectValue;
mJson[jss::transaction] = mTxn->getJson(JsonOptions::none);
+3 -1
View File
@@ -46,7 +46,8 @@ public:
AcceptedLedgerTx(
std::shared_ptr<ReadView const> const& ledger,
std::shared_ptr<STTx const> const&,
std::shared_ptr<STObject const> const&);
std::shared_ptr<STObject const> const&,
bool withJson = true);
std::shared_ptr<STTx const> const&
getTxn() const
@@ -88,6 +89,7 @@ public:
std::string
getEscMeta() const;
/** Null if constructed with withJson == false. */
Json::Value const&
getJson() const
{
+34
View File
@@ -25,7 +25,11 @@
#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>
@@ -175,6 +179,36 @@ 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;
+6
View File
@@ -155,6 +155,12 @@ LedgerHistory::getLedgerByHash(LedgerHash const& hash)
return ret;
}
std::shared_ptr<Ledger const>
LedgerHistory::getCachedLedger(LedgerHash const& hash)
{
return m_ledgers_by_hash.fetch(hash);
}
static void
log_one(
ReadView const& ledger,
+7
View File
@@ -63,6 +63,13 @@ public:
std::shared_ptr<Ledger const>
getLedgerByHash(LedgerHash const& ledgerHash);
/** Return a ledger already in the hash cache.
Does not load from the database or wire the SHAMap.
*/
std::shared_ptr<Ledger const>
getCachedLedger(LedgerHash const& ledgerHash);
/** Get a ledger's hash given its sequence number
@param ledgerIndex The sequence number of the desired ledger
@return The hash of the specified ledger
+46
View File
@@ -37,6 +37,8 @@
#include <xrpl/protocol/RippleLedgerHash.h>
#include <xrpl/protocol/STValidation.h>
#include <xrpl/protocol/messages.h>
#include <deque>
#include <functional>
#include <optional>
#include <mutex>
@@ -186,6 +188,21 @@ public:
std::shared_ptr<Ledger const>
getLedgerByHash(uint256 const& hash);
/** Return a ledger that is already resident in memory.
Looks in the ledger history cache, the closed and validated
ledgers and the null-mode retained window. Unlike getLedgerByHash
and getLedgerBySeq this never loads from SQL or the node store and
never edits the complete-ledger set, so it is safe to call with
values supplied by a peer.
*/
std::shared_ptr<Ledger const>
getResidentLedgerByHash(uint256 const& hash);
/** Sequence-number form of getResidentLedgerByHash. */
std::shared_ptr<Ledger const>
getResidentLedgerBySeq(std::uint32_t seq);
void
setLedgerRangePresent(
std::uint32_t minV,
@@ -211,6 +228,19 @@ public:
isPinned(std::uint32_t seq);
void
unpinLedger(std::uint32_t seq);
/** Remove `ranges` from the pinned set.
@param dropFromComplete of the ledgers actually unpinned, those to
also remove from complete_ledgers now (used to roll back a
load). Otherwise unpinned ledgers stay complete until online
delete releases them.
@return the ledgers that were pinned and no longer are.
*/
RangeSet<std::uint32_t>
unpinLedgers(
RangeSet<std::uint32_t> const& ranges,
RangeSet<std::uint32_t> const& dropFromComplete = {});
bool
isValidated(ReadView const& ledger);
bool
@@ -323,6 +353,10 @@ private:
void
doAdvance(std::unique_lock<std::recursive_mutex>&);
// Closed, validated, then retained ledgers; never loads.
std::shared_ptr<Ledger const>
findResidentLedger(std::function<bool(LedgerInfo const&)> const& match);
std::vector<std::shared_ptr<Ledger const>>
findNewLedgersToPublish(std::unique_lock<std::recursive_mutex>&);
@@ -354,6 +388,14 @@ private:
// The last ledger we handled fetching history
std::shared_ptr<Ledger const> mHistLedger;
// Sliding window of the last ledger_history published ledgers, pinned
// so their SHAMaps stay reachable via shared_ptr. Only populated when
// [node_db] type=rwdb (null node store), where nothing else can bring
// a dropped tree back. Has its own lock so peer requests that consult
// it do not contend on m_mutex.
std::mutex mutable mRetainedLock;
std::deque<std::shared_ptr<Ledger const>> mRetainedLedgers;
// Fully validated ledger, whether or not we have the ledger resident.
std::pair<uint256, LedgerIndex> mLastValidLedger{uint256(), 0};
@@ -401,6 +443,10 @@ private:
// How much history do we want to keep
std::uint32_t const ledger_history_;
// [node_db] type=rwdb: the node store keeps nothing, so recent
// ledgers are retained in memory (see mRetainedLedgers).
bool const nullBackend_;
std::uint32_t const ledger_fetch_size_;
TaggedCache<uint256, Blob> fetch_packs_;
+126
View File
@@ -0,0 +1,126 @@
//------------------------------------------------------------------------------
/*
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,6 +106,41 @@ 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,14 +21,18 @@
#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>
@@ -39,6 +43,16 @@
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 {
@@ -457,6 +471,19 @@ 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:
@@ -636,6 +663,63 @@ 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());
@@ -764,6 +848,73 @@ 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,
+75 -1
View File
@@ -113,6 +113,7 @@ LedgerMaster::LedgerMaster(
, fetch_depth_(
app_.getSHAMapStore().clampFetchDepth(app_.config().FETCH_DEPTH))
, ledger_history_(app_.config().LEDGER_HISTORY)
, nullBackend_(app_.config().nullBackend())
, ledger_fetch_size_(app_.config().getValueFor(SizedItem::ledgerFetch))
, fetch_packs_(
"FetchPack",
@@ -500,7 +501,9 @@ LedgerMaster::storeLedger(std::shared_ptr<Ledger const> ledger, bool pin)
std::lock_guard sl(mPinnedLock);
mPinnedLedgers.insert(range(seq, seq));
}
JLOG(m_journal.info()) << "Pinned ledger : " << seq;
// catalogue_load pins every ledger it imports; at info this would
// emit one line per ledger for the whole file.
JLOG(m_journal.debug()) << "Pinned ledger : " << seq;
}
return true;
}
@@ -585,6 +588,19 @@ LedgerMaster::unpinLedger(std::uint32_t seq)
mPinnedLedgers.erase(range(seq, seq));
}
RangeSet<std::uint32_t>
LedgerMaster::unpinLedgers(
RangeSet<std::uint32_t> const& ranges,
RangeSet<std::uint32_t> const& dropFromComplete)
{
std::scoped_lock lock(mCompleteLock, mPinnedLock);
RangeSet<std::uint32_t> removed = mPinnedLedgers & ranges;
mPinnedLedgers -= removed;
if (!dropFromComplete.empty())
mCompleteLedgers -= (removed & dropFromComplete);
return removed;
}
bool
LedgerMaster::isValidated(ReadView const& ledger)
{
@@ -937,6 +953,20 @@ LedgerMaster::setFullLedger(
mCompleteLedgers.insert(ledger->info().seq);
}
// Null node store only: pin a sliding window of recently validated
// current ledgers so their SHAMap state trees stay resident via
// shared_ptr. A persistent node store must not do this. It would pin
// ledger_history full trees for nothing, and ledger_history=full
// (uint32 max) would never trim. SHAMapStoreImp rejects both zero and
// "full" ledger_history for type=rwdb.
if (nullBackend_ && isCurrent)
{
std::lock_guard rl(mRetainedLock);
mRetainedLedgers.push_back(ledger);
while (mRetainedLedgers.size() > ledger_history_)
mRetainedLedgers.pop_front();
}
{
std::lock_guard ml(m_mutex);
@@ -1852,6 +1882,12 @@ LedgerMaster::getCloseTimeByHash(
LedgerHash const& ledgerHash,
std::uint32_t index)
{
// Resident ledgers first (getLedgerByHash would load a cache miss).
// Close time is a header field, so fall through to the node-store
// header when nothing is already in memory.
if (auto const ledger = getResidentLedgerByHash(ledgerHash))
return ledger->info().closeTime;
auto nodeObject = app_.getNodeStore().fetchNodeObject(ledgerHash, index);
if (nodeObject && (nodeObject->getData().size() >= 120))
{
@@ -1996,6 +2032,44 @@ LedgerMaster::getLedgerByHash(uint256 const& hash)
return {};
}
std::shared_ptr<Ledger const>
LedgerMaster::findResidentLedger(
std::function<bool(LedgerInfo const&)> const& match)
{
for (auto const& ledger : {mClosedLedger.get(), mValidLedger.get()})
{
if (ledger && match(ledger->info()))
return ledger;
}
std::lock_guard lock(mRetainedLock);
auto const it = std::find_if(
mRetainedLedgers.rbegin(),
mRetainedLedgers.rend(),
[&](auto const& ledger) { return match(ledger->info()); });
return it == mRetainedLedgers.rend() ? nullptr : *it;
}
std::shared_ptr<Ledger const>
LedgerMaster::getResidentLedgerByHash(uint256 const& hash)
{
if (auto ret = mLedgerHistory.getCachedLedger(hash))
return ret;
return findResidentLedger(
[&](LedgerInfo const& info) { return info.hash == hash; });
}
std::shared_ptr<Ledger const>
LedgerMaster::getResidentLedgerBySeq(std::uint32_t seq)
{
// getLedgerHash only reads the in-memory index; a zero hash misses.
if (auto ret =
mLedgerHistory.getCachedLedger(mLedgerHistory.getLedgerHash(seq)))
return ret;
return findResidentLedger(
[&](LedgerInfo const& info) { return info.seq == seq; });
}
void
LedgerMaster::setLedgerRangePresent(
std::uint32_t minV,
@@ -0,0 +1,179 @@
//------------------------------------------------------------------------------
/*
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 -14
View File
@@ -228,11 +228,7 @@ public:
boost::asio::signal_set m_signals;
// 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_flag isTimeToStop;
std::atomic<bool> checkSigs_;
@@ -1246,10 +1242,10 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
// Configure the amendments the server supports
{
auto const supported = []() {
auto const supported = [this]() {
auto const& amendments = detail::supportedAmendments();
std::vector<AmendmentTable::FeatureInfo> supported;
supported.reserve(amendments.size());
supported.reserve(amendments.size() + 1);
for (auto const& [a, vote] : amendments)
{
auto const f = ripple::getRegisteredFeature(a);
@@ -1258,6 +1254,16 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
if (f)
supported.emplace_back(a, *f, vote);
}
// fixXahauTestnetGenesis is registered Supported::no. It is only
// supported (and voted for) by servers configured for the Xahau
// testnet. Everywhere else it stays unsupported.
if (config_->NETWORK_ID == 21338)
supported.emplace_back(
"fixXahauTestnetGenesis",
fixXahauTestnetGenesis,
VoteBehavior::DefaultYes);
return supported;
}();
Section const& downVoted = config_->section(SECTION_VETO_AMENDMENTS);
@@ -1585,10 +1591,7 @@ ApplicationImp::run()
getLoadManager().activateDeadlockDetector();
}
{
std::unique_lock<std::mutex> lk{stoppingMutex_};
stoppingCondition_.wait(lk, [this] { return isTimeToStop.load(); });
}
isTimeToStop.wait(false, std::memory_order_relaxed);
JLOG(m_journal.debug()) << "Application stopping";
@@ -1675,14 +1678,14 @@ ApplicationImp::run()
void
ApplicationImp::signalStop(std::string msg)
{
if (!isTimeToStop.exchange(true))
if (!isTimeToStop.test_and_set(std::memory_order_acquire))
{
if (msg.empty())
JLOG(m_journal.warn()) << "Server stopping";
else
JLOG(m_journal.warn()) << "Server stopping: " << msg;
stoppingCondition_.notify_all();
isTimeToStop.notify_all();
}
}
@@ -1701,7 +1704,7 @@ ApplicationImp::checkSigs(bool check)
bool
ApplicationImp::isStopping() const
{
return isTimeToStop.load();
return isTimeToStop.test(std::memory_order_relaxed);
}
int
Loaded 100 of 183 files, more files were not shown because too many files have changed in this diff. Show more