mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-22 11:47:09 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19c543f817 | ||
|
|
3dc41b2a59 | ||
|
|
dcfe881290 | ||
|
|
56bedcc958 | ||
|
|
c9455093b4 | ||
|
|
b9f2914c0e | ||
|
|
023f55e283 | ||
|
|
d20e8ffb56 | ||
|
|
c63ee8cc5f | ||
|
|
64a30b2a15 | ||
|
|
0025355f43 | ||
|
|
7046616cdb | ||
|
|
a2e7f3c645 | ||
|
|
004752d1c2 | ||
|
|
22e6ad21b4 | ||
|
|
3423ee4665 |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -101,6 +103,7 @@ 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
|
||||
@@ -124,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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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,100 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
# [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.
|
||||
#
|
||||
#
|
||||
#
|
||||
# [lite_mode]
|
||||
#
|
||||
# Run as a lite node: follow the network's validated ledgers without
|
||||
# holding their state. For each validated ledger the server fetches the
|
||||
# header, the whole transaction map (small, and what account subscriptions
|
||||
# and `tx` answer from) and only the state nodes on the paths from the root
|
||||
# to the objects in [lite_watch], and to the objects the transactions in
|
||||
# that ledger created or touched for the watched accounts. Everything else
|
||||
# stays on the network. Sync takes a few request/response rounds, not
|
||||
# hours, and a ledger costs a few kilobytes rather than the full state.
|
||||
#
|
||||
# This is for small devices that need to see whether a transaction
|
||||
# succeeded, read a balance or a trust line, or submit a transaction, and
|
||||
# do not need to serve other nodes. In lite mode the server:
|
||||
#
|
||||
# - takes no part in consensus and never closes a ledger of its own;
|
||||
# - serves no ledger data to peers and advertises no ledger range;
|
||||
# - fetches no history: only ledgers validated after it started;
|
||||
# - implies [node_db] type=rwdb (nothing is written to a node store;
|
||||
# the last ledger_history ledgers are kept in memory), path_search_max
|
||||
# 0, and refuses [validation_seed] / [validator_token] and standalone;
|
||||
# - answers `ledger_index: current` and `closed` with the validated
|
||||
# ledger;
|
||||
# - relays a submitted transaction after checking its form and
|
||||
# signature, and reports terLITE_RELAYED instead of applying it: read
|
||||
# the outcome with `tx` or an account subscription;
|
||||
# - answers a request for any object it does not hold with
|
||||
# liteNotWatched. Add the object with the lite_watch RPC; it is read
|
||||
# from the next validated ledger.
|
||||
#
|
||||
# Legal values are 0 (the default) and 1.
|
||||
#
|
||||
# Suggested companions for a device with little memory:
|
||||
#
|
||||
# [node_size] tiny
|
||||
# [ledger_history] 64
|
||||
# [peers_max] 3
|
||||
#
|
||||
# [lite_watch]
|
||||
#
|
||||
# One entry per line: a base58 account address (its AccountRoot is watched,
|
||||
# and the objects its transactions create or touch are discovered as they
|
||||
# happen) or the 64 hex digit key of any ledger object. The fee settings,
|
||||
# amendments, skip list and negative UNL are always watched.
|
||||
#
|
||||
# Entries can be added and removed while running with the lite_watch and
|
||||
# lite_unwatch admin RPCs, which also take trust lines by their parties;
|
||||
# lite_status lists what is watched and why.
|
||||
#
|
||||
# [lite_discover]
|
||||
#
|
||||
# Whether a lite node grows its watch set from the transactions it sees:
|
||||
# an affected object naming a watched account is watched from then on, and
|
||||
# a transaction that touched anything watched has every object it created
|
||||
# watched (the trust line, escrow, check, offer, URIToken or hook state it
|
||||
# made for the account). Deleted objects are forgotten again.
|
||||
#
|
||||
# Legal values are 1 (the default) and 0, which watches only what is
|
||||
# configured.
|
||||
#
|
||||
#
|
||||
#
|
||||
# [ledger_history]
|
||||
@@ -1416,6 +1650,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
|
||||
|
||||
@@ -151,4 +151,3 @@ D686F2538F410C9D0D856788E98E3579595DAF7B38D38887F81ECAC934B06040 HooksUpdate1
|
||||
4B8466415FAB32FFA89D9DCBE166A42340115771DF611A7160F8D7439C87ECD8 fixNSDelete
|
||||
EDB4EE4C524E16BDD91D9A529332DED08DCAAA51CC6DC897ACFA1A0ED131C5B6 fix240819
|
||||
8063140E9260799D6716756B891CEC3E7006C4E4F277AB84670663A88F94B9C4 fixPageCap
|
||||
88693F108C3CD8A967F3F4253A32DEF5E35F9406ACD2A11B88B11D90865763A9 fix240911
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,6 +401,7 @@ enum class ExitType : uint8_t {
|
||||
const uint16_t max_state_modifications = 256;
|
||||
const uint8_t max_slots = 255;
|
||||
const uint8_t max_nonce = 255;
|
||||
const uint16_t max_rng_draws = 256; // featureRNG, per Hook execution
|
||||
const uint8_t max_emit = 255;
|
||||
const uint8_t max_params = 16;
|
||||
const double fee_base_multiplier = 1.1f;
|
||||
|
||||
@@ -1096,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')
|
||||
|
||||
@@ -298,4 +298,12 @@ HOOK_API_DEFINITION(
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, prepare, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
featureHooksUpdate2)
|
||||
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, dice, (uint32_t sides),
|
||||
featureRNG)
|
||||
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_random, (uint32_t write_ptr, uint32_t write_len),
|
||||
featureRNG)
|
||||
// clang-format on
|
||||
@@ -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
|
||||
@@ -157,7 +157,11 @@ enum error_code_i {
|
||||
// Simulate
|
||||
rpcTX_SIGNED = 97,
|
||||
|
||||
rpcLAST = rpcTX_SIGNED // rpcLAST should always equal the last code.
|
||||
// Lite mode: the object is not among the watched keys, so this node
|
||||
// does not hold it
|
||||
rpcLITE_NOT_WATCHED = 98,
|
||||
|
||||
rpcLAST = rpcLITE_NOT_WATCHED // rpcLAST should always equal the last code.
|
||||
};
|
||||
|
||||
/** Codes returned in the `warnings` array of certain RPC commands.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -197,6 +197,7 @@ enum TEFcodes : TERUnderlyingType {
|
||||
tefINVALID_LEDGER_FIX_TYPE,
|
||||
tefPAST_MANIFEST_SEQ,
|
||||
tefREVOKED_MANIFEST,
|
||||
tefMANIFEST_KEY_IN_USE,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -237,8 +238,11 @@ enum TERcodes : TERUnderlyingType {
|
||||
terQUEUED, // Transaction is being held in TxQ until fee drops
|
||||
terPRE_TICKET, // Ticket is not yet in ledger but might be on its way
|
||||
terNO_AMM, // AMM doesn't exist for the asset pair
|
||||
terNO_HOOK // Transaction requires a non-existent hook definition
|
||||
terNO_HOOK, // Transaction requires a non-existent hook definition
|
||||
// (referenced by sfHookHash)
|
||||
terLITE_RELAYED // Relayed by a [lite_mode] node that holds no state to
|
||||
// apply it against; the result is in the validated
|
||||
// ledger it turns up in, if any
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -34,9 +34,11 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FEATURE(RNG, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(PWALoader, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (20261005, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FEATURE(EscrowDestinationCancel, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (20260929, Supported::yes, VoteBehavior::DefaultNo)
|
||||
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 +90,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 +165,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, ({
|
||||
|
||||
@@ -171,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
|
||||
@@ -238,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
|
||||
@@ -378,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
|
||||
@@ -451,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
|
||||
@@ -589,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
|
||||
@@ -607,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
|
||||
@@ -741,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
|
||||
@@ -748,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
|
||||
@@ -769,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
@@ -113,7 +113,8 @@ constexpr static ErrorInfo unorderedErrorInfos[]{
|
||||
{rpcLEDGER_MISSING, "ledgerMissing", "One or more ledgers in the specified range is missing", 406},
|
||||
{rpcORACLE_MALFORMED, "oracleMalformed", "Oracle request is malformed.", 400},
|
||||
{rpcBAD_CREDENTIALS, "badCredentials", "Credentials do not exist, are not accepted, or have expired.", 400},
|
||||
{rpcTX_SIGNED, "transactionSigned", "Transaction should not be signed.", 400}};
|
||||
{rpcTX_SIGNED, "transactionSigned", "Transaction should not be signed.", 400},
|
||||
{rpcLITE_NOT_WATCHED, "liteNotWatched", "This lite node does not watch that ledger object; add it with lite_watch.", 404}};
|
||||
// clang-format on
|
||||
|
||||
// Sort and validate unorderedErrorInfos at compile time. Should be
|
||||
|
||||
@@ -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(),
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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."),
|
||||
@@ -241,6 +242,7 @@ transResults()
|
||||
MAKE_ERROR(terPRE_TICKET, "Ticket is not yet in ledger."),
|
||||
MAKE_ERROR(terNO_HOOK, "No hook with that hash exists on the ledger."),
|
||||
MAKE_ERROR(terNO_AMM, "AMM doesn't exist for the asset pair."),
|
||||
MAKE_ERROR(terLITE_RELAYED, "Relayed to the network by a lite node; not applied locally."),
|
||||
|
||||
MAKE_ERROR(tesSUCCESS, "The transaction was applied. Only final in a validated ledger."),
|
||||
MAKE_ERROR(tesPARTIAL, "The transaction was applied but should be submitted again until returning tesSUCCESS."),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1271,6 +1271,79 @@ public:
|
||||
BEAST_EXPECT(table->needValidatedLedger(257));
|
||||
}
|
||||
|
||||
void
|
||||
testIgnoredUnsupported()
|
||||
{
|
||||
testcase("ignore_amendments");
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
weeks constexpr w(1);
|
||||
test::jtx::Env env{*this, makeConfig()};
|
||||
|
||||
static std::vector<AmendmentTable::FeatureInfo> const supported =
|
||||
makeDefaultYes(yes_);
|
||||
|
||||
// Ignore every unsupported_ amendment except the first one.
|
||||
BEAST_EXPECT(unsupported_.size() > 1);
|
||||
std::vector<std::string> const ignoredNames(
|
||||
unsupported_.begin() + 1, unsupported_.end());
|
||||
|
||||
auto table = make_AmendmentTable(
|
||||
env.app(),
|
||||
w,
|
||||
supported,
|
||||
emptySection_,
|
||||
emptySection_,
|
||||
journal_,
|
||||
makeSection(ignoredNames));
|
||||
|
||||
for (auto const& a : ignoredNames)
|
||||
BEAST_EXPECT(table->isSupported(amendmentId(a)));
|
||||
BEAST_EXPECT(!table->isSupported(amendmentId(unsupported_[0])));
|
||||
|
||||
majorityAmendments_t const majority;
|
||||
|
||||
// Enabling only ignored amendments must not block.
|
||||
std::set<uint256> enabled;
|
||||
for (auto const& a : ignoredNames)
|
||||
enabled.insert(amendmentId(a));
|
||||
table->doValidatedLedger(1, enabled, majority);
|
||||
BEAST_EXPECT(!table->hasUnsupportedEnabled());
|
||||
BEAST_EXPECT(table->unsupportedAmendments().empty());
|
||||
|
||||
// An ignored amendment must never be voted for.
|
||||
auto const desired = table->getDesired();
|
||||
for (auto const& a : ignoredNames)
|
||||
BEAST_EXPECT(
|
||||
std::find(desired.begin(), desired.end(), amendmentId(a)) ==
|
||||
desired.end());
|
||||
|
||||
// A non-ignored unsupported amendment still blocks.
|
||||
enabled.insert(amendmentId(unsupported_[0]));
|
||||
table->doValidatedLedger(2, enabled, majority);
|
||||
BEAST_EXPECT(table->hasUnsupportedEnabled());
|
||||
|
||||
// Bad entries are rejected.
|
||||
Section bad("ignore_amendments");
|
||||
bad.append("NotARealFeatureName");
|
||||
try
|
||||
{
|
||||
make_AmendmentTable(
|
||||
env.app(),
|
||||
w,
|
||||
supported,
|
||||
emptySection_,
|
||||
emptySection_,
|
||||
journal_,
|
||||
bad);
|
||||
fail("expected bad [ignore_amendments] entry to throw");
|
||||
}
|
||||
catch (std::runtime_error const&)
|
||||
{
|
||||
pass();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testFeature(FeatureBitset const& feat)
|
||||
{
|
||||
@@ -1294,6 +1367,7 @@ public:
|
||||
testBadConfig();
|
||||
testEnableVeto();
|
||||
testHasUnsupported();
|
||||
testIgnoredUnsupported();
|
||||
testFeature(all - fixMajorityCalc);
|
||||
testFeature(all);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -3043,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)
|
||||
{
|
||||
@@ -3741,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)
|
||||
{
|
||||
@@ -4958,6 +5211,7 @@ public:
|
||||
test_fee_base(features);
|
||||
|
||||
test_otxn_field(features);
|
||||
test_otxn_field_emit_failure(features);
|
||||
|
||||
test_ledger_keylet(features);
|
||||
|
||||
@@ -5012,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);
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 <test/shamap/common.h>
|
||||
#include <xrpld/app/ledger/Ledger.h>
|
||||
#include <xrpld/app/ledger/LiteWatch.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class LiteWatch_test : public beast::unit_test::suite
|
||||
{
|
||||
static Config
|
||||
liteConfig(std::vector<std::string> watch)
|
||||
{
|
||||
Config cfg;
|
||||
cfg.LITE_MODE = true;
|
||||
cfg.LITE_WATCH = std::move(watch);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void
|
||||
testConfigLines()
|
||||
{
|
||||
testcase("config lines");
|
||||
|
||||
using namespace jtx;
|
||||
Account const alice{"alice"};
|
||||
|
||||
std::string err;
|
||||
auto const acct = parseLiteWatchLine(" " + alice.human() + " ", err);
|
||||
BEAST_EXPECT(acct && acct->first && *acct->first == alice.id());
|
||||
BEAST_EXPECT(acct && acct->second == keylet::account(alice).key);
|
||||
|
||||
auto const key = keylet::fees().key;
|
||||
auto const raw = parseLiteWatchLine(to_string(key), err);
|
||||
BEAST_EXPECT(raw && !raw->first && raw->second == key);
|
||||
|
||||
BEAST_EXPECT(!parseLiteWatchLine("", err));
|
||||
BEAST_EXPECT(!parseLiteWatchLine("not a key", err));
|
||||
BEAST_EXPECT(!parseLiteWatchLine(to_string(key).substr(1), err));
|
||||
}
|
||||
|
||||
void
|
||||
testWatchSet()
|
||||
{
|
||||
testcase("watch set");
|
||||
|
||||
using namespace jtx;
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
|
||||
Env env{*this};
|
||||
LiteWatch watch{liteConfig({alice.human()}), env.journal};
|
||||
|
||||
BEAST_EXPECT(watch.enabled());
|
||||
BEAST_EXPECT(watch.isWatchedAccount(alice));
|
||||
BEAST_EXPECT(watch.isWatched(keylet::account(alice).key));
|
||||
BEAST_EXPECT(!watch.isWatchedAccount(bob));
|
||||
|
||||
// The keys every ledger needs are there, and cannot be removed.
|
||||
for (auto const& k :
|
||||
{keylet::fees().key,
|
||||
keylet::amendments().key,
|
||||
keylet::skip().key,
|
||||
keylet::negativeUNL().key})
|
||||
{
|
||||
BEAST_EXPECT(watch.isWatched(k));
|
||||
BEAST_EXPECT(!watch.unwatchKey(k));
|
||||
BEAST_EXPECT(watch.isWatched(k));
|
||||
}
|
||||
BEAST_EXPECT(watch.isWatched(keylet::hook(alice).key));
|
||||
BEAST_EXPECT(watch.size() == 6);
|
||||
|
||||
BEAST_EXPECT(watch.watchAccount(bob));
|
||||
BEAST_EXPECT(!watch.watchAccount(bob));
|
||||
BEAST_EXPECT(watch.isWatched(keylet::account(bob).key));
|
||||
BEAST_EXPECT(watch.unwatchAccount(bob));
|
||||
BEAST_EXPECT(!watch.isWatched(keylet::account(bob).key));
|
||||
BEAST_EXPECT(!watch.isWatched(keylet::hook(bob).key));
|
||||
BEAST_EXPECT(!watch.unwatchAccount(bob));
|
||||
|
||||
auto const k = keylet::ownerDir(alice).key;
|
||||
BEAST_EXPECT(watch.watchKey(k, LiteWatch::Tier::discovered, "test"));
|
||||
BEAST_EXPECT(!watch.watchKey(k));
|
||||
// Asking for a discovered key by name promotes it: it now survives
|
||||
// the account it was discovered through being unwatched.
|
||||
BEAST_EXPECT(watch.unwatchAccount(alice));
|
||||
BEAST_EXPECT(watch.isWatched(k));
|
||||
BEAST_EXPECT(watch.unwatchKey(k));
|
||||
BEAST_EXPECT(!watch.isWatched(k));
|
||||
|
||||
auto const json = watch.getJson();
|
||||
BEAST_EXPECT(json[jss::enabled].asBool());
|
||||
BEAST_EXPECT(json["system"].asUInt() == 4);
|
||||
}
|
||||
|
||||
void
|
||||
testDiscover()
|
||||
{
|
||||
testcase("discover");
|
||||
|
||||
using namespace jtx;
|
||||
Account const gw{"gw"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
Env env{*this};
|
||||
env.fund(XRP(10000), gw, alice, bob);
|
||||
env.close();
|
||||
|
||||
LiteWatch watch{liteConfig({alice.human()}), env.journal};
|
||||
auto const line = keylet::line(alice, gw, USD.currency).key;
|
||||
auto const bobLine = keylet::line(bob, gw, USD.currency).key;
|
||||
|
||||
// Nothing in the funding ledger concerns alice's objects beyond her
|
||||
// root, which is configured already.
|
||||
watch.discover(*env.closed());
|
||||
BEAST_EXPECT(!watch.isWatched(keylet::account(bob).key));
|
||||
BEAST_EXPECT(!watch.isWatched(line));
|
||||
|
||||
env.trust(USD(100), alice);
|
||||
env.trust(USD(100), bob);
|
||||
env(pay(gw, alice, USD(50)));
|
||||
env.close();
|
||||
|
||||
auto const added = watch.discover(*env.closed());
|
||||
BEAST_EXPECT(added > 0);
|
||||
|
||||
// alice's new trust line names her as a party; bob's does not.
|
||||
BEAST_EXPECT(watch.isWatched(line));
|
||||
BEAST_EXPECT(!watch.isWatched(bobLine));
|
||||
BEAST_EXPECT(!watch.isWatched(keylet::account(bob).key));
|
||||
// Her owner directory names her as its owner.
|
||||
BEAST_EXPECT(watch.isWatched(keylet::ownerDir(alice).key));
|
||||
|
||||
// Paying the line back to zero and clearing the limit deletes it,
|
||||
// and a discovered object that is deleted is forgotten.
|
||||
env(pay(alice, gw, USD(50)));
|
||||
env(trust(alice, USD(0)));
|
||||
env.close();
|
||||
watch.discover(*env.closed());
|
||||
BEAST_EXPECT(!watch.isWatched(line));
|
||||
BEAST_EXPECT(watch.isWatched(keylet::account(alice).key));
|
||||
|
||||
// With discovery off nothing is learned.
|
||||
auto cfg = liteConfig({alice.human()});
|
||||
cfg.LITE_DISCOVER = false;
|
||||
LiteWatch quiet{cfg, env.journal};
|
||||
env.trust(USD(100), alice);
|
||||
env.close();
|
||||
BEAST_EXPECT(quiet.discover(*env.closed()) == 0);
|
||||
BEAST_EXPECT(!quiet.isWatched(line));
|
||||
}
|
||||
|
||||
void
|
||||
testPartialSync()
|
||||
{
|
||||
testcase("partial sync");
|
||||
|
||||
using namespace jtx;
|
||||
Account const gw{"gw"};
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
Env env{*this};
|
||||
env.fund(XRP(10000), gw, alice, bob);
|
||||
env.trust(USD(100), alice);
|
||||
env(pay(gw, alice, USD(50)));
|
||||
env.close();
|
||||
|
||||
auto const ledger =
|
||||
std::dynamic_pointer_cast<Ledger const>(env.closed());
|
||||
if (!BEAST_EXPECT(ledger))
|
||||
return;
|
||||
auto const& full = ledger->stateMap();
|
||||
|
||||
LiteWatch watch{liteConfig({alice.human()}), env.journal};
|
||||
watch.discover(*ledger);
|
||||
auto const line = keylet::line(alice, gw, USD.currency).key;
|
||||
BEAST_EXPECT(watch.isWatched(line));
|
||||
|
||||
// A whole map needs nothing.
|
||||
BEAST_EXPECT(watch.missingNodes(full, 100).empty());
|
||||
|
||||
// A map holding just the root, in a family of its own so that
|
||||
// nothing resolves from a shared cache or store: what a lite node
|
||||
// holds after the root reply. Feed it what a peer would answer, one
|
||||
// round per level, until every watched key reads.
|
||||
tests::TestNodeFamily family{env.journal};
|
||||
SHAMap partial{SHAMapType::STATE, family};
|
||||
partial.setSynching();
|
||||
{
|
||||
Serializer s;
|
||||
full.serializeRoot(s);
|
||||
BEAST_EXPECT(partial.addRootNode(full.getHash(), s.slice(), nullptr)
|
||||
.isGood());
|
||||
}
|
||||
|
||||
int rounds = 0;
|
||||
std::size_t held = 1; // the root
|
||||
for (; rounds < 32; ++rounds)
|
||||
{
|
||||
auto const missing = watch.missingNodes(partial, 100);
|
||||
if (missing.empty())
|
||||
break;
|
||||
|
||||
held += missing.size();
|
||||
for (auto const& [id, hash] : missing)
|
||||
{
|
||||
std::vector<std::pair<SHAMapNodeID, Blob>> data;
|
||||
full.getNodeFat(id, data, false, 0);
|
||||
if (!BEAST_EXPECT(!data.empty()))
|
||||
return;
|
||||
BEAST_EXPECT(
|
||||
partial.addKnownNode(id, makeSlice(data[0].second), nullptr)
|
||||
.isGood());
|
||||
}
|
||||
}
|
||||
|
||||
// Converged, in no more rounds than the map is deep.
|
||||
BEAST_EXPECT(rounds > 0 && rounds < 32);
|
||||
BEAST_EXPECT(watch.missingNodes(partial, 100).empty());
|
||||
BEAST_EXPECT(partial.getHash() == full.getHash());
|
||||
|
||||
std::optional<std::pair<SHAMapNodeID, uint256>> gap;
|
||||
BEAST_EXPECT(partial.peekItemPartial(line, gap) && !gap);
|
||||
BEAST_EXPECT(
|
||||
partial.peekItemPartial(keylet::account(alice).key, gap) && !gap);
|
||||
BEAST_EXPECT(partial.peekItemPartial(keylet::fees().key, gap) && !gap);
|
||||
|
||||
// bob was never asked for: his path stops at a node not held.
|
||||
BEAST_EXPECT(
|
||||
!partial.peekItemPartial(keylet::account(bob).key, gap) && gap);
|
||||
|
||||
// And the partial map is a fraction of the whole one. (visitNodes()
|
||||
// cannot be asked of the partial map: it dereferences every child.)
|
||||
std::size_t whole = 0;
|
||||
full.visitNodes([&](SHAMapTreeNode&) {
|
||||
++whole;
|
||||
return true;
|
||||
});
|
||||
BEAST_EXPECT(held < whole);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testConfigLines();
|
||||
testWatchSet();
|
||||
testDiscover();
|
||||
testPartialSync();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(LiteWatch, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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");
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -33,9 +33,11 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
@@ -93,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,
|
||||
@@ -1153,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.
|
||||
@@ -1302,6 +1446,8 @@ public:
|
||||
testNullFactoryDropsWrites();
|
||||
testRotate();
|
||||
testPinnedRangeRestoreRequiresPinnedData();
|
||||
testPinnedRangeRestoreEmpty();
|
||||
testPinnedRangeRestoreTrimsMissingTail();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -16192,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -4676,11 +4677,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 +5569,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 +5660,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 +5678,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);
|
||||
@@ -5723,7 +5910,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
|
||||
testGovernHookWithFeats(sa);
|
||||
testRewardHookWithFeats(sa);
|
||||
testRewardHookWithFeats(sa - fix240819);
|
||||
testRewardHookWithFeats(sa - fix240819 - fix240911);
|
||||
testRewardHookWithFeats(sa - fix20261005);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
@@ -1747,6 +1843,7 @@ r.ripple.com:51235
|
||||
testValidatorsFile();
|
||||
testSetup(false);
|
||||
testSetup(true);
|
||||
testSetupStandaloneRwdb();
|
||||
testPort();
|
||||
testZeroPort();
|
||||
testWhitespace();
|
||||
@@ -1758,6 +1855,8 @@ r.ripple.com:51235
|
||||
testDatabasePinnedValidation();
|
||||
testOverlay();
|
||||
testNetworkID();
|
||||
testEarlyManifestSync();
|
||||
testUdpHighway();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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.",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace tests {
|
||||
@@ -131,6 +133,7 @@ public:
|
||||
testCanonicalizeMergesInnerChildren(journal);
|
||||
testCanonicalizeSharesDuplicateContent(journal);
|
||||
testGetLinkedNode(journal);
|
||||
testUpsertAndFlushToSink(journal);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -284,6 +287,82 @@ public:
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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,10 @@ 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
|
||||
@@ -389,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
|
||||
@@ -400,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));
|
||||
@@ -422,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1581,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);
|
||||
|
||||
@@ -1895,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
|
||||
{
|
||||
@@ -1905,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);
|
||||
|
||||
@@ -2149,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);
|
||||
@@ -2172,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);
|
||||
@@ -2828,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)
|
||||
@@ -2842,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}}}}}};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -2866,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -129,6 +133,32 @@ private:
|
||||
void
|
||||
trigger(std::shared_ptr<Peer> const&, TriggerReason);
|
||||
|
||||
/** The [lite_mode] acquire: header, whole transaction map, then only
|
||||
the state nodes on the paths to the watched keys, which the
|
||||
transactions just fetched may have added to. Called by trigger()
|
||||
once the header is held, with mtx_ locked and tmGL prepared. */
|
||||
void
|
||||
liteTrigger(
|
||||
ScopedLockType& sl,
|
||||
std::shared_ptr<Peer> const& peer,
|
||||
TriggerReason reason,
|
||||
protocol::TMGetLedger& tmGL);
|
||||
|
||||
/** The state nodes still needed to read every watched key. */
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
liteMissingStateNodes(std::size_t max) const;
|
||||
|
||||
/** Ask peers for the nodes the early manifest read needs, if any.
|
||||
@return true if a request was sent, and so the caller should return;
|
||||
false if the manifests are read, being read, not wanted, or
|
||||
the nodes are all recently asked for */
|
||||
bool
|
||||
earlyManifestSync(
|
||||
ScopedLockType& sl,
|
||||
std::shared_ptr<Peer> const& peer,
|
||||
TriggerReason reason,
|
||||
protocol::TMGetLedger& tmGL);
|
||||
|
||||
std::vector<neededHash_t>
|
||||
getNeededHashes();
|
||||
|
||||
@@ -175,6 +205,42 @@ 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};
|
||||
|
||||
/** [lite_mode]: the transactions of this ledger have been read for
|
||||
objects the watch set should grow by. Done once, before the state
|
||||
paths are requested, so that an object created in this ledger is
|
||||
fetched from this ledger. */
|
||||
bool mLiteDiscovered{false};
|
||||
|
||||
bool mHaveHeader;
|
||||
bool mHaveState;
|
||||
bool mHaveTransactions;
|
||||
|
||||
@@ -228,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
|
||||
@@ -434,6 +447,11 @@ private:
|
||||
// ledgers are retained in memory (see mRetainedLedgers).
|
||||
bool const nullBackend_;
|
||||
|
||||
// [lite_mode]: ledgers hold only the state paths to the watched keys.
|
||||
// They are never marked full, never backfilled, and the validated
|
||||
// ledger doubles as the closed ledger since no consensus round runs.
|
||||
bool const liteMode_;
|
||||
|
||||
std::uint32_t const ledger_fetch_size_;
|
||||
|
||||
TaggedCache<uint256, Blob> fetch_packs_;
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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_LITEWATCH_H_INCLUDED
|
||||
#define RIPPLE_APP_LEDGER_LITEWATCH_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/beast/utility/Journal.h>
|
||||
#include <xrpl/json/json_value.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/STObject.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class Config;
|
||||
class ReadView;
|
||||
|
||||
/** The set of ledger objects a lite node tracks.
|
||||
|
||||
A node in [lite_mode] does not hold a ledger's state map. For each
|
||||
validated ledger it fetches the header, the whole transaction map (small,
|
||||
and needed to answer `tx` and account subscriptions), and only the state
|
||||
map nodes on the paths from the root to the keys in this set. Everything
|
||||
else about the ledger stays on the network.
|
||||
|
||||
The set has three tiers:
|
||||
|
||||
- System keys every ledger needs to be usable at all: the fee settings,
|
||||
the amendments object, the skip list and the negative UNL. Always
|
||||
present, never removed.
|
||||
- Configured keys from [lite_watch] and the lite_watch RPC: account
|
||||
roots, raw keylets and trust lines. Removed only by lite_unwatch.
|
||||
- Discovered keys: objects that transactions touching a watched account
|
||||
created or modified (a trust line opened to it, an escrow it is the
|
||||
destination of, hook state its hooks wrote). Added by discover() as
|
||||
each ledger's transactions arrive, removed again when the object is
|
||||
deleted.
|
||||
|
||||
All methods are thread safe. The walk over the state map in
|
||||
missingNodes() reads a map that InboundLedger is still adding nodes to,
|
||||
which is what SHAMap::getMissingNodes() already does.
|
||||
*/
|
||||
class LiteWatch
|
||||
{
|
||||
public:
|
||||
enum class Tier { system, configured, discovered };
|
||||
|
||||
struct Entry
|
||||
{
|
||||
uint256 key;
|
||||
Tier tier;
|
||||
/** Why it is watched, for lite_status: "account rXXX", "system
|
||||
fees", "created by <txid>", ... */
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
/** Most keys the set will hold, system keys included. Discovery stops
|
||||
adding at this point rather than let one busy issuer account drag a
|
||||
Raspberry Pi into fetching its whole owner tree. */
|
||||
static constexpr std::size_t maxKeys = 16384;
|
||||
|
||||
LiteWatch(Config const& config, beast::Journal journal);
|
||||
|
||||
/** True when the server runs in [lite_mode]. */
|
||||
bool
|
||||
enabled() const
|
||||
{
|
||||
return enabled_;
|
||||
}
|
||||
|
||||
/** Watch an account: its AccountRoot and Hook object, plus discovery of
|
||||
the objects its transactions touch. Returns false if already watched
|
||||
or full. */
|
||||
bool
|
||||
watchAccount(AccountID const& id, Tier tier = Tier::configured);
|
||||
|
||||
/** Watch one ledger object by key. */
|
||||
bool
|
||||
watchKey(
|
||||
uint256 const& key,
|
||||
Tier tier = Tier::configured,
|
||||
std::string reason = {});
|
||||
|
||||
/** Stop watching a key. System keys cannot be removed. Returns true if
|
||||
something was removed. */
|
||||
bool
|
||||
unwatchKey(uint256 const& key);
|
||||
|
||||
/** Stop watching an account and its root. Objects discovered through it
|
||||
are dropped as well; configured keys are kept. */
|
||||
bool
|
||||
unwatchAccount(AccountID const& id);
|
||||
|
||||
bool
|
||||
isWatched(uint256 const& key) const;
|
||||
|
||||
bool
|
||||
isWatchedAccount(AccountID const& id) const;
|
||||
|
||||
/** Every key currently watched. */
|
||||
std::vector<uint256>
|
||||
keys() const;
|
||||
|
||||
/** Every entry, for lite_status. */
|
||||
std::vector<Entry>
|
||||
entries() const;
|
||||
|
||||
std::size_t
|
||||
size() const;
|
||||
|
||||
/** The nodes a partly held state map still needs to read every watched
|
||||
key. One entry per node, however many keys it blocks; at most `max`.
|
||||
Empty once every watched key can be read (or shown absent).
|
||||
|
||||
@param stateMap State map with at least its root node
|
||||
@param max Most nodes to report
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
missingNodes(SHAMap const& stateMap, std::size_t max) const;
|
||||
|
||||
/** Learn from one validated transaction and its metadata which objects
|
||||
are now of interest.
|
||||
|
||||
- An affected node any of whose account fields name a watched account
|
||||
(Account, Owner, Destination, Issuer, the two limits of a trust
|
||||
line, ...) is watched from now on.
|
||||
- If the transaction was about a watched account (sent by it, ran a
|
||||
hook of it, or wrote an object naming it), every node it created
|
||||
is watched too: a new trust line, escrow, check, offer, URIToken
|
||||
or hook state belonging to the account is almost always among
|
||||
them, and a transaction creates few objects.
|
||||
- A deleted node that was only discovered is forgotten.
|
||||
|
||||
@return number of keys added
|
||||
*/
|
||||
std::size_t
|
||||
discover(STTx const& tx, STObject const& meta, std::uint32_t ledgerSeq);
|
||||
|
||||
/** discover() over every transaction of a ledger whose transaction map
|
||||
is complete. Reading the state map is not required. */
|
||||
std::size_t
|
||||
discover(ReadView const& ledger);
|
||||
|
||||
Json::Value
|
||||
getJson() const;
|
||||
|
||||
private:
|
||||
bool
|
||||
addLocked(uint256 const& key, Tier tier, std::string reason);
|
||||
|
||||
bool const enabled_;
|
||||
bool const discoverEnabled_;
|
||||
beast::Journal j_;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
hash_map<uint256, Entry> keys_;
|
||||
hash_set<AccountID> accounts_;
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -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,19 @@
|
||||
#include <xrpld/app/ledger/InboundLedger.h>
|
||||
#include <xrpld/app/ledger/InboundLedgers.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/LiteWatch.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 +44,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 {
|
||||
@@ -230,10 +245,33 @@ InboundLedger::neededTxHashes(int max, SHAMapSyncFilter* filter) const
|
||||
std::vector<uint256>
|
||||
InboundLedger::neededStateHashes(int max, SHAMapSyncFilter* filter) const
|
||||
{
|
||||
if (app_.config().liteMode())
|
||||
{
|
||||
// Only the paths to the watched keys are wanted, so a walk of the
|
||||
// whole map (which would also mark subtrees full below) is never
|
||||
// the right question to ask of it.
|
||||
std::vector<uint256> ret;
|
||||
if (mLedger->info().accountHash.isNonZero())
|
||||
{
|
||||
if (mLedger->stateMap().getHash().isZero())
|
||||
ret.push_back(mLedger->info().accountHash);
|
||||
else
|
||||
for (auto const& n : liteMissingStateNodes(max))
|
||||
ret.push_back(n.second);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
return neededHashes(
|
||||
mLedger->info().accountHash, mLedger->stateMap(), max, filter);
|
||||
}
|
||||
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
InboundLedger::liteMissingStateNodes(std::size_t max) const
|
||||
{
|
||||
return app_.getLiteWatch().missingNodes(mLedger->stateMap(), max);
|
||||
}
|
||||
|
||||
// See how much of the ledger data is stored locally
|
||||
// Data found in a fetch pack will be stored
|
||||
void
|
||||
@@ -457,6 +495,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:
|
||||
@@ -611,6 +662,12 @@ InboundLedger::trigger(std::shared_ptr<Peer> const& peer, TriggerReason reason)
|
||||
else
|
||||
tmGL.set_querydepth(1);
|
||||
|
||||
if (app_.config().liteMode())
|
||||
{
|
||||
liteTrigger(sl, peer, reason, tmGL);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the state data first because it's the most likely to be useful
|
||||
// if we wind up abandoning this fetch.
|
||||
if (mHaveHeader && !mHaveState && !failed_)
|
||||
@@ -636,6 +693,21 @@ 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 (earlyManifestSync(sl, peer, reason, tmGL))
|
||||
return;
|
||||
|
||||
AccountStateSF filter(
|
||||
mLedger->stateMap().family().db(), app_.getLedgerMaster());
|
||||
|
||||
@@ -764,6 +836,283 @@ InboundLedger::trigger(std::shared_ptr<Peer> const& peer, TriggerReason reason)
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
InboundLedger::earlyManifestSync(
|
||||
ScopedLockType& sl,
|
||||
std::shared_ptr<Peer> const& peer,
|
||||
TriggerReason reason,
|
||||
protocol::TMGetLedger& tmGL)
|
||||
{
|
||||
if (mManifestsSynced || !app_.config().EARLY_MANIFEST_SYNC ||
|
||||
!wantManifests())
|
||||
return false;
|
||||
|
||||
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();
|
||||
|
||||
// Make sure nothing happened while we released the lock
|
||||
if (failed_ || complete_ || mHaveState || mManifestsSynced)
|
||||
return false;
|
||||
|
||||
if (nodes.empty())
|
||||
{
|
||||
syncManifests(std::move(listed), manifestSyncPages);
|
||||
return false;
|
||||
}
|
||||
|
||||
filterNodes(nodes, reason);
|
||||
|
||||
// All recently asked for: fall through to the rest of the map rather
|
||||
// than wait on them.
|
||||
if (nodes.empty())
|
||||
return false;
|
||||
|
||||
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 true;
|
||||
}
|
||||
|
||||
void
|
||||
InboundLedger::liteTrigger(
|
||||
ScopedLockType& sl,
|
||||
std::shared_ptr<Peer> const& peer,
|
||||
TriggerReason reason,
|
||||
protocol::TMGetLedger& tmGL)
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
failed_ || (mHaveHeader && mLedger),
|
||||
"ripple::InboundLedger::liteTrigger : header held");
|
||||
|
||||
// Transactions first, and all of them. They are what the watchers are
|
||||
// waiting to see, and they name the objects this ledger created for the
|
||||
// watched accounts, which must be known before the state paths are
|
||||
// asked for if they are to be read from this ledger.
|
||||
if (!mHaveTransactions && !failed_)
|
||||
{
|
||||
if (!mLedger->txMap().isValid())
|
||||
{
|
||||
failed_ = true;
|
||||
}
|
||||
else if (mLedger->txMap().getHash().isZero())
|
||||
{
|
||||
tmGL.set_itype(protocol::liTX_NODE);
|
||||
*(tmGL.add_nodeids()) = SHAMapNodeID().getRawString();
|
||||
JLOG(journal_.trace()) << "Lite: sending TX root request to "
|
||||
<< (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
TransactionStateSF filter(
|
||||
mLedger->txMap().family().db(), app_.getLedgerMaster());
|
||||
|
||||
auto nodes =
|
||||
mLedger->txMap().getMissingNodes(missingNodesFind, &filter);
|
||||
|
||||
if (nodes.empty())
|
||||
{
|
||||
if (!mLedger->txMap().isValid())
|
||||
failed_ = true;
|
||||
else
|
||||
mHaveTransactions = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
filterNodes(nodes, reason);
|
||||
|
||||
if (!nodes.empty())
|
||||
{
|
||||
tmGL.set_itype(protocol::liTX_NODE);
|
||||
for (auto const& n : nodes)
|
||||
*(tmGL.add_nodeids()) = n.first.getRawString();
|
||||
|
||||
JLOG(journal_.trace())
|
||||
<< "Lite: sending TX node request (" << nodes.size()
|
||||
<< ") to " << (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return;
|
||||
}
|
||||
|
||||
JLOG(journal_.trace()) << "Lite: all TX nodes filtered";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grow the watch set by what this ledger's transactions did to the
|
||||
// watched accounts. Reading the complete transaction map is what
|
||||
// getMissingNodes() just did, so the lock can be released for it.
|
||||
if (mHaveTransactions && !mLiteDiscovered && !failed_)
|
||||
{
|
||||
mLiteDiscovered = true;
|
||||
sl.unlock();
|
||||
auto const added = app_.getLiteWatch().discover(*mLedger);
|
||||
sl.lock();
|
||||
|
||||
if (added != 0)
|
||||
{
|
||||
JLOG(journal_.debug()) << "Lite: ledger " << mLedger->info().seq
|
||||
<< " adds " << added << " watched keys";
|
||||
}
|
||||
}
|
||||
|
||||
if (mHaveTransactions && !mHaveState && !failed_)
|
||||
{
|
||||
if (!mLedger->stateMap().isValid())
|
||||
{
|
||||
failed_ = true;
|
||||
}
|
||||
else if (mLedger->stateMap().getHash().isZero())
|
||||
{
|
||||
tmGL.set_itype(protocol::liAS_NODE);
|
||||
*tmGL.add_nodeids() = SHAMapNodeID().getRawString();
|
||||
JLOG(journal_.trace()) << "Lite: sending AS root request to "
|
||||
<< (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Before the first validated ledger the manifests are what
|
||||
// decide which validations to believe; they are a few paths
|
||||
// more, read the same way.
|
||||
if (earlyManifestSync(sl, peer, reason, tmGL))
|
||||
return;
|
||||
|
||||
// The paths to the watched keys, and nothing else. Unchanged
|
||||
// paths resolve from the tree node cache, which is keyed by
|
||||
// hash, so in steady state this costs the nodes near the root
|
||||
// that every ledger rewrites.
|
||||
sl.unlock();
|
||||
auto nodes = liteMissingStateNodes(missingNodesFind);
|
||||
sl.lock();
|
||||
|
||||
// Make sure nothing happened while we released the lock
|
||||
if (!failed_ && !complete_ && !mHaveState)
|
||||
{
|
||||
if (nodes.empty())
|
||||
{
|
||||
mHaveState = true;
|
||||
complete_ = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
filterNodes(nodes, reason);
|
||||
|
||||
if (!nodes.empty())
|
||||
{
|
||||
tmGL.set_itype(protocol::liAS_NODE);
|
||||
for (auto const& id : nodes)
|
||||
*(tmGL.add_nodeids()) = id.first.getRawString();
|
||||
|
||||
JLOG(journal_.trace())
|
||||
<< "Lite: sending AS path request (" << nodes.size()
|
||||
<< ") to "
|
||||
<< (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return;
|
||||
}
|
||||
|
||||
JLOG(journal_.trace()) << "Lite: all AS nodes filtered";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (mHaveTransactions && mHaveState && !failed_)
|
||||
{
|
||||
// A ledger with no state root, or one tryDB() found whole.
|
||||
complete_ = true;
|
||||
}
|
||||
|
||||
if (complete_ || failed_)
|
||||
{
|
||||
JLOG(journal_.debug()) << "Lite done:" << (complete_ ? " complete" : "")
|
||||
<< (failed_ ? " failed " : " ")
|
||||
<< (mLedger ? mLedger->info().seq : mSeq);
|
||||
sl.unlock();
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -114,6 +114,7 @@ LedgerMaster::LedgerMaster(
|
||||
app_.getSHAMapStore().clampFetchDepth(app_.config().FETCH_DEPTH))
|
||||
, ledger_history_(app_.config().LEDGER_HISTORY)
|
||||
, nullBackend_(app_.config().nullBackend())
|
||||
, liteMode_(app_.config().liteMode())
|
||||
, ledger_fetch_size_(app_.config().getValueFor(SizedItem::ledgerFetch))
|
||||
, fetch_packs_(
|
||||
"FetchPack",
|
||||
@@ -278,6 +279,28 @@ LedgerMaster::setValidLedger(std::shared_ptr<Ledger const> const& l)
|
||||
|
||||
mValidLedger.set(l);
|
||||
mValidLedgerSign = signTime.time_since_epoch().count();
|
||||
|
||||
// A lite node runs no consensus round, so nothing else ever sets a
|
||||
// closed ledger or moves the open ledger on. The validated ledger is the
|
||||
// most recent one it holds: it is the closed ledger, and the open ledger
|
||||
// is an empty view on top of it, so that `current` reads, sequence and
|
||||
// fee autofill see the watched accounts as they now are. Nothing is
|
||||
// applied to it: a transaction submitted to a lite node is relayed, not
|
||||
// applied (NetworkOPsImp::apply).
|
||||
if (liteMode_)
|
||||
{
|
||||
mClosedLedger.set(l);
|
||||
OrderedTxs retries{uint256{}};
|
||||
app_.openLedger().accept(
|
||||
app_,
|
||||
l->rules(),
|
||||
l,
|
||||
OrderedTxs({}),
|
||||
false,
|
||||
retries,
|
||||
tapNONE,
|
||||
"lite");
|
||||
}
|
||||
XRPL_ASSERT(
|
||||
mValidLedgerSeq || !app_.getMaxDisallowedLedger() ||
|
||||
l->info().seq + max_ledger_difference_ >
|
||||
@@ -501,7 +524,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;
|
||||
}
|
||||
@@ -586,6 +611,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)
|
||||
{
|
||||
@@ -905,7 +943,11 @@ LedgerMaster::setFullLedger(
|
||||
"ripple::LedgerMaster::setFullLedger : nonzero ledger state hash");
|
||||
|
||||
ledger->setValidated();
|
||||
ledger->setFull();
|
||||
|
||||
// A lite ledger holds only the watched paths: marking it full would make
|
||||
// the first read of anything else trigger a fresh acquire.
|
||||
if (!liteMode_)
|
||||
ledger->setFull();
|
||||
|
||||
if (isCurrent)
|
||||
mLedgerHistory.insert(ledger, true);
|
||||
@@ -1084,7 +1126,8 @@ LedgerMaster::checkAccept(std::shared_ptr<Ledger const> const& ledger)
|
||||
<< " validations";
|
||||
|
||||
ledger->setValidated();
|
||||
ledger->setFull();
|
||||
if (!liteMode_)
|
||||
ledger->setFull();
|
||||
setValidLedger(ledger);
|
||||
|
||||
JLOG(m_journal.info()) << "checkAccept (" << ledger->info().seq
|
||||
@@ -2257,7 +2300,10 @@ LedgerMaster::doAdvance(std::unique_lock<std::recursive_mutex>& sl)
|
||||
auto const pubLedgers = findNewLedgersToPublish(sl);
|
||||
if (pubLedgers.empty())
|
||||
{
|
||||
if (!standalone_ && !app_.getFeeTrack().isLoadedLocal() &&
|
||||
// A lite node keeps no history: it serves nobody, and every
|
||||
// ledger it fetched would cost the same paths over again.
|
||||
if (!standalone_ && !liteMode_ &&
|
||||
!app_.getFeeTrack().isLoadedLocal() &&
|
||||
(app_.getJobQueue().getJobCount(jtPUBOLDLEDGER) < 10) &&
|
||||
(mValidLedgerSeq == mPubLedgerSeq) &&
|
||||
(getValidatedLedgerAge() < MAX_LEDGER_AGE_ACQUIRE) &&
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/LiteWatch.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpld/ledger/ReadView.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
#include <xrpl/protocol/STArray.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
LiteWatch::LiteWatch(Config const& config, beast::Journal journal)
|
||||
: enabled_(config.LITE_MODE)
|
||||
, discoverEnabled_(config.LITE_DISCOVER)
|
||||
, j_(journal)
|
||||
{
|
||||
if (!enabled_)
|
||||
return;
|
||||
|
||||
// Read by Ledger::setup(), AmendmentTable::doValidatedLedger(),
|
||||
// hashOfSeq() and LedgerMaster::setValidLedger() on every ledger: without
|
||||
// them a ledger cannot even be accepted.
|
||||
addLocked(keylet::fees().key, Tier::system, "system fees");
|
||||
addLocked(keylet::amendments().key, Tier::system, "system amendments");
|
||||
addLocked(keylet::skip().key, Tier::system, "system skip list");
|
||||
addLocked(keylet::negativeUNL().key, Tier::system, "system negative UNL");
|
||||
|
||||
for (auto const& line : config.LITE_WATCH)
|
||||
{
|
||||
std::string err;
|
||||
auto const parsed = parseLiteWatchLine(line, err);
|
||||
if (!parsed)
|
||||
{
|
||||
// Config::loadFromString() has already rejected these.
|
||||
JLOG(j_.warn()) << "[" SECTION_LITE_WATCH "] " << err;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsed->first)
|
||||
{
|
||||
accounts_.insert(*parsed->first);
|
||||
auto const reason = "account " + toBase58(*parsed->first);
|
||||
addLocked(parsed->second, Tier::configured, reason);
|
||||
addLocked(
|
||||
keylet::hook(*parsed->first).key, Tier::configured, reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
addLocked(parsed->second, Tier::configured, "keylet");
|
||||
}
|
||||
}
|
||||
|
||||
JLOG(j_.info()) << "Lite mode: watching " << keys_.size() << " keys ("
|
||||
<< accounts_.size() << " accounts)";
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::addLocked(uint256 const& key, Tier tier, std::string reason)
|
||||
{
|
||||
auto it = keys_.find(key);
|
||||
if (it != keys_.end())
|
||||
{
|
||||
// Promote: a discovered object the operator then asks for by name
|
||||
// must survive the deletion of whatever it was discovered through.
|
||||
if (static_cast<int>(tier) < static_cast<int>(it->second.tier))
|
||||
{
|
||||
it->second.tier = tier;
|
||||
it->second.reason = std::move(reason);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keys_.size() >= maxKeys)
|
||||
{
|
||||
JLOG(j_.warn()) << "Lite watch set is full (" << maxKeys
|
||||
<< " keys); not watching " << key;
|
||||
return false;
|
||||
}
|
||||
|
||||
keys_.emplace(key, Entry{key, tier, std::move(reason)});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::watchAccount(AccountID const& id, Tier tier)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
bool const added = accounts_.insert(id).second;
|
||||
auto const reason = "account " + toBase58(id);
|
||||
|
||||
// The root, and the Hook object that lists the hooks installed on it:
|
||||
// the base fee of anything the account sends depends on those, and a
|
||||
// submit autofilling its fee reads them.
|
||||
bool const root = addLocked(keylet::account(id).key, tier, reason);
|
||||
bool const hook = addLocked(keylet::hook(id).key, tier, reason);
|
||||
return root || hook || added;
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::watchKey(uint256 const& key, Tier tier, std::string reason)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
return addLocked(key, tier, reason.empty() ? "keylet" : std::move(reason));
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::unwatchKey(uint256 const& key)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
auto it = keys_.find(key);
|
||||
if (it == keys_.end() || it->second.tier == Tier::system)
|
||||
return false;
|
||||
keys_.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::unwatchAccount(AccountID const& id)
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
bool removed = accounts_.erase(id) != 0;
|
||||
|
||||
for (auto const& k : {keylet::account(id).key, keylet::hook(id).key})
|
||||
{
|
||||
auto it = keys_.find(k);
|
||||
if (it != keys_.end() && it->second.tier != Tier::system)
|
||||
{
|
||||
keys_.erase(it);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Objects only known through this account go with it. Everything an
|
||||
// operator asked for by key stays.
|
||||
for (auto i = keys_.begin(); i != keys_.end();)
|
||||
{
|
||||
if (i->second.tier == Tier::discovered)
|
||||
{
|
||||
i = keys_.erase(i);
|
||||
removed = true;
|
||||
}
|
||||
else
|
||||
++i;
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::isWatched(uint256 const& key) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
return keys_.contains(key);
|
||||
}
|
||||
|
||||
bool
|
||||
LiteWatch::isWatchedAccount(AccountID const& id) const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
return accounts_.contains(id);
|
||||
}
|
||||
|
||||
std::vector<uint256>
|
||||
LiteWatch::keys() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::vector<uint256> ret;
|
||||
ret.reserve(keys_.size());
|
||||
for (auto const& [key, entry] : keys_)
|
||||
ret.push_back(key);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<LiteWatch::Entry>
|
||||
LiteWatch::entries() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
std::vector<Entry> ret;
|
||||
ret.reserve(keys_.size());
|
||||
for (auto const& [key, entry] : keys_)
|
||||
ret.push_back(entry);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
LiteWatch::size() const
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
return keys_.size();
|
||||
}
|
||||
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
LiteWatch::missingNodes(SHAMap const& stateMap, std::size_t max) const
|
||||
{
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>> missing;
|
||||
|
||||
// Snapshot the keys: the walk reads the map, not the set, and a
|
||||
// discovery running on another ledger must not wait on it.
|
||||
auto const wanted = keys();
|
||||
|
||||
// By hash: several keys blocked by the same node report it once. Near
|
||||
// the root that is every key.
|
||||
hash_set<uint256> reported;
|
||||
|
||||
for (auto const& key : wanted)
|
||||
{
|
||||
if (missing.size() >= max)
|
||||
break;
|
||||
|
||||
std::optional<std::pair<SHAMapNodeID, uint256>> gap;
|
||||
stateMap.peekItemPartial(key, gap);
|
||||
if (gap && reported.insert(gap->second).second)
|
||||
missing.push_back(*gap);
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
LiteWatch::discover(
|
||||
STTx const& tx,
|
||||
STObject const& meta,
|
||||
std::uint32_t ledgerSeq)
|
||||
{
|
||||
if (!discoverEnabled_ || !meta.isFieldPresent(sfAffectedNodes))
|
||||
return 0;
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
|
||||
auto const watchedAccount = [&](AccountID const& id) {
|
||||
return accounts_.contains(id);
|
||||
};
|
||||
|
||||
// Is this transaction about a watched account? Sent by one, run a hook
|
||||
// of one, or wrote an object that names one. Merely modifying a key
|
||||
// already watched is not enough: a trust line set against a watched
|
||||
// issuer touches the issuer's owner directory, and that must not pull
|
||||
// in every stranger's line.
|
||||
bool touches = watchedAccount(tx.getAccountID(sfAccount));
|
||||
|
||||
if (!touches && meta.isFieldPresent(sfHookExecutions))
|
||||
{
|
||||
for (auto const& exec : meta.getFieldArray(sfHookExecutions))
|
||||
{
|
||||
if (exec.isFieldPresent(sfHookAccount) &&
|
||||
watchedAccount(exec.getAccountID(sfHookAccount)))
|
||||
{
|
||||
touches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto const& nodes = meta.getFieldArray(sfAffectedNodes);
|
||||
|
||||
// Pass 1: nodes that name a watched account, and whether anything
|
||||
// watched was touched.
|
||||
std::vector<uint256> wanted;
|
||||
std::vector<uint256> deleted;
|
||||
|
||||
for (auto const& node : nodes)
|
||||
{
|
||||
if (!node.isFieldPresent(sfLedgerIndex))
|
||||
continue;
|
||||
|
||||
auto const index = node.getFieldH256(sfLedgerIndex);
|
||||
bool const isDeleted = node.getFName() == sfDeletedNode;
|
||||
|
||||
if (keys_.contains(index))
|
||||
{
|
||||
if (isDeleted)
|
||||
deleted.push_back(index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDeleted)
|
||||
continue;
|
||||
|
||||
int const fieldsIndex = node.getFieldIndex(
|
||||
node.getFName() == sfCreatedNode ? sfNewFields : sfFinalFields);
|
||||
if (fieldsIndex == -1)
|
||||
continue;
|
||||
|
||||
auto const inner =
|
||||
dynamic_cast<STObject const*>(&node.peekAtIndex(fieldsIndex));
|
||||
if (!inner)
|
||||
continue;
|
||||
|
||||
bool const isLine = node.isFieldPresent(sfLedgerEntryType) &&
|
||||
node.getFieldU16(sfLedgerEntryType) == ltRIPPLE_STATE;
|
||||
|
||||
bool names = false;
|
||||
for (auto const& field : *inner)
|
||||
{
|
||||
if (auto const sa = dynamic_cast<STAccount const*>(&field))
|
||||
{
|
||||
if (!sa->isDefault() && watchedAccount(sa->value()))
|
||||
{
|
||||
names = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (
|
||||
isLine &&
|
||||
(field.getFName() == sfLowLimit ||
|
||||
field.getFName() == sfHighLimit))
|
||||
{
|
||||
// The two parties of a trust line are the issuers of its
|
||||
// limits. Not done for offers: an issuer's every order book
|
||||
// entry is not what "my account" means.
|
||||
if (auto const lim = dynamic_cast<STAmount const*>(&field);
|
||||
lim && watchedAccount(lim->getIssuer()))
|
||||
{
|
||||
names = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (names)
|
||||
{
|
||||
touches = true;
|
||||
wanted.push_back(index);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: a transaction that touched something watched created these.
|
||||
if (touches)
|
||||
{
|
||||
for (auto const& node : nodes)
|
||||
{
|
||||
if (node.getFName() == sfCreatedNode &&
|
||||
node.isFieldPresent(sfLedgerIndex))
|
||||
wanted.push_back(node.getFieldH256(sfLedgerIndex));
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t added = 0;
|
||||
std::string const reason = "tx " + to_string(tx.getTransactionID()) +
|
||||
" ledger " + std::to_string(ledgerSeq);
|
||||
for (auto const& key : wanted)
|
||||
if (addLocked(key, Tier::discovered, reason))
|
||||
++added;
|
||||
|
||||
for (auto const& key : deleted)
|
||||
{
|
||||
auto it = keys_.find(key);
|
||||
if (it != keys_.end() && it->second.tier == Tier::discovered)
|
||||
keys_.erase(it);
|
||||
}
|
||||
|
||||
if (added != 0)
|
||||
{
|
||||
JLOG(j_.debug()) << "Lite mode: " << reason << " adds " << added
|
||||
<< " keys (" << keys_.size() << " watched)";
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
LiteWatch::discover(ReadView const& ledger)
|
||||
{
|
||||
if (!discoverEnabled_)
|
||||
return 0;
|
||||
|
||||
std::size_t added = 0;
|
||||
for (auto const& [tx, meta] : ledger.txs)
|
||||
{
|
||||
if (tx && meta)
|
||||
added += discover(*tx, *meta, ledger.seq());
|
||||
}
|
||||
return added;
|
||||
}
|
||||
|
||||
Json::Value
|
||||
LiteWatch::getJson() const
|
||||
{
|
||||
Json::Value ret{Json::objectValue};
|
||||
ret[jss::enabled] = enabled_;
|
||||
|
||||
std::lock_guard lock(mutex_);
|
||||
ret["discover"] = discoverEnabled_;
|
||||
ret["key_count"] = static_cast<Json::UInt>(keys_.size());
|
||||
ret["max_keys"] = static_cast<Json::UInt>(maxKeys);
|
||||
|
||||
Json::Value accounts{Json::arrayValue};
|
||||
for (auto const& id : accounts_)
|
||||
accounts.append(toBase58(id));
|
||||
ret[jss::accounts] = std::move(accounts);
|
||||
|
||||
std::size_t system = 0, configured = 0, discovered = 0;
|
||||
Json::Value keys{Json::arrayValue};
|
||||
for (auto const& [key, entry] : keys_)
|
||||
{
|
||||
switch (entry.tier)
|
||||
{
|
||||
case Tier::system:
|
||||
++system;
|
||||
break;
|
||||
case Tier::configured:
|
||||
++configured;
|
||||
break;
|
||||
case Tier::discovered:
|
||||
++discovered;
|
||||
break;
|
||||
}
|
||||
Json::Value k{Json::objectValue};
|
||||
k[jss::key] = to_string(key);
|
||||
k["tier"] = entry.tier == Tier::system ? "system"
|
||||
: entry.tier == Tier::configured ? "configured"
|
||||
: "discovered";
|
||||
k["reason"] = entry.reason;
|
||||
keys.append(std::move(k));
|
||||
}
|
||||
ret["system"] = static_cast<Json::UInt>(system);
|
||||
ret["configured"] = static_cast<Json::UInt>(configured);
|
||||
ret["discovered"] = static_cast<Json::UInt>(discovered);
|
||||
ret["keys"] = std::move(keys);
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -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
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/LedgerReplayer.h>
|
||||
#include <xrpld/app/ledger/LedgerToJson.h>
|
||||
#include <xrpld/app/ledger/LiteWatch.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/ledger/OrderBookDB.h>
|
||||
#include <xrpld/app/ledger/PendingSaves.h>
|
||||
@@ -217,6 +218,7 @@ public:
|
||||
RCLValidations mValidations;
|
||||
std::unique_ptr<LoadManager> m_loadManager;
|
||||
std::unique_ptr<TxQ> txQ_;
|
||||
std::unique_ptr<LiteWatch> liteWatch_;
|
||||
ClosureCounter<void, boost::system::error_code const&> waitHandlerCounter_;
|
||||
boost::asio::steady_timer sweepTimer_;
|
||||
boost::asio::steady_timer entropyTimer_;
|
||||
@@ -228,11 +230,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_;
|
||||
|
||||
@@ -461,6 +459,10 @@ public:
|
||||
, txQ_(
|
||||
std::make_unique<TxQ>(setup_TxQ(*config_), logs_->journal("TxQ")))
|
||||
|
||||
, liteWatch_(std::make_unique<LiteWatch>(
|
||||
*config_,
|
||||
logs_->journal("LiteWatch")))
|
||||
|
||||
, sweepTimer_(get_io_context())
|
||||
|
||||
, entropyTimer_(get_io_context())
|
||||
@@ -815,6 +817,15 @@ public:
|
||||
return *txQ_;
|
||||
}
|
||||
|
||||
LiteWatch&
|
||||
getLiteWatch() override
|
||||
{
|
||||
XRPL_ASSERT(
|
||||
liteWatch_,
|
||||
"ripple::ApplicationImp::getLiteWatch : non-null lite watch");
|
||||
return *liteWatch_;
|
||||
}
|
||||
|
||||
RelationalDatabase&
|
||||
getRelationalDatabase() override
|
||||
{
|
||||
@@ -1264,13 +1275,29 @@ ApplicationImp::setup(boost::program_options::variables_map const& cmdline)
|
||||
|
||||
Section const& upVoted = config_->section(SECTION_AMENDMENTS);
|
||||
|
||||
Section const& ignored = config_->section(SECTION_IGNORE_AMENDMENTS);
|
||||
|
||||
// [ignore_amendments] exists to keep long-lived test networks syncable
|
||||
// after an amendment they activated is removed from the codebase. It
|
||||
// must never be usable on a production network.
|
||||
if (!ignored.lines().empty() &&
|
||||
(config_->NETWORK_ID == 0 || config_->NETWORK_ID == 21337))
|
||||
{
|
||||
JLOG(m_journal.fatal()) << "[" SECTION_IGNORE_AMENDMENTS
|
||||
"] is not permitted on "
|
||||
"network_id "
|
||||
<< config_->NETWORK_ID;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_amendmentTable = make_AmendmentTable(
|
||||
*this,
|
||||
config().AMENDMENT_MAJORITY_TIME,
|
||||
supported,
|
||||
upVoted,
|
||||
downVoted,
|
||||
logs_->journal("Amendments"));
|
||||
logs_->journal("Amendments"),
|
||||
ignored);
|
||||
}
|
||||
|
||||
Pathfinder::initPathTable();
|
||||
@@ -1585,10 +1612,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 +1699,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 +1725,7 @@ ApplicationImp::checkSigs(bool check)
|
||||
bool
|
||||
ApplicationImp::isStopping() const
|
||||
{
|
||||
return isTimeToStop.load();
|
||||
return isTimeToStop.test(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
int
|
||||
|
||||
@@ -79,6 +79,7 @@ class Ledger;
|
||||
class LedgerMaster;
|
||||
class LedgerCleaner;
|
||||
class LedgerReplayer;
|
||||
class LiteWatch;
|
||||
class LoadManager;
|
||||
class ManifestCache;
|
||||
class ValidatorKeys;
|
||||
@@ -190,6 +191,8 @@ public:
|
||||
overlay() = 0;
|
||||
virtual TxQ&
|
||||
getTxQ() = 0;
|
||||
virtual LiteWatch&
|
||||
getLiteWatch() = 0;
|
||||
virtual ValidatorList&
|
||||
validators() = 0;
|
||||
virtual ValidatorSite&
|
||||
|
||||
@@ -159,6 +159,7 @@ printHelp(const po::options_description& desc)
|
||||
" gateway_balances [<ledger>] <issuer_account> [ <hotwallet> [ "
|
||||
"<hotwallet> ]]\n"
|
||||
" get_counts\n"
|
||||
" hook_validate <hex>\n"
|
||||
" json <method> <json>\n"
|
||||
" ledger [<id>|current|closed|validated] [full]\n"
|
||||
" ledger_accept\n"
|
||||
|
||||
@@ -211,7 +211,8 @@ make_AmendmentTable(
|
||||
std::vector<AmendmentTable::FeatureInfo> const& supported,
|
||||
Section const& enabled,
|
||||
Section const& vetoed,
|
||||
beast::Journal journal);
|
||||
beast::Journal journal,
|
||||
Section const& ignored = Section{});
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
|
||||
+432
-122
@@ -20,13 +20,20 @@
|
||||
#ifndef RIPPLE_APP_MISC_MANIFEST_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_MANIFEST_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/misc/RotatingBloomFilter.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
@@ -36,6 +43,8 @@
|
||||
namespace ripple {
|
||||
|
||||
class ReadView;
|
||||
class STLedgerEntry;
|
||||
using SLE = STLedgerEntry;
|
||||
|
||||
/*
|
||||
Validator key manifests
|
||||
@@ -64,8 +73,9 @@ class ReadView;
|
||||
master public key, and (b) the most senior of all valid manifests it has
|
||||
seen for that validator, if any. On startup, the [validator_token] config
|
||||
entry (which contains the manifest for this validator) is decoded and
|
||||
added to the manifest cache. Other manifests are added as "gossip"
|
||||
received from rippled peers.
|
||||
added to the manifest cache. Other manifests arrive in published validator
|
||||
lists, from ltMANIFEST ledger objects, and as "gossip" received from
|
||||
rippled peers; see ManifestCache for how each is retained.
|
||||
|
||||
When an ephemeral key is compromised, a new signing key pair is created,
|
||||
along with a new manifest vouching for it (with a higher sequence number),
|
||||
@@ -242,7 +252,17 @@ enum class ManifestDisposition {
|
||||
badEphemeralKey,
|
||||
|
||||
/// Timely, but invalid signature
|
||||
invalid
|
||||
invalid,
|
||||
|
||||
/// Gossip introducing a master key not held, whose ephemeral key has not
|
||||
/// recently signed a validation this node verified, or which names no
|
||||
/// ephemeral key at all. Not admitted and not relayed; if it names an
|
||||
/// ephemeral key it is held pending, unverified, until that key is seen.
|
||||
unseen,
|
||||
|
||||
/// Gossip with nowhere to go: every manifest held from gossip has been
|
||||
/// more active than this one could claim to be.
|
||||
full
|
||||
};
|
||||
|
||||
inline std::string
|
||||
@@ -260,6 +280,58 @@ to_string(ManifestDisposition m)
|
||||
return "badEphemeralKey";
|
||||
case ManifestDisposition::invalid:
|
||||
return "invalid";
|
||||
case ManifestDisposition::unseen:
|
||||
return "unseen";
|
||||
case ManifestDisposition::full:
|
||||
return "full";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/** Where a held manifest came from, in increasing order of precedence.
|
||||
|
||||
The source never makes a manifest valid. Whatever its source, a manifest
|
||||
is accepted only if its signatures verify and its sequence is newer than
|
||||
the one held. The source decides how long the cache keeps it, and which
|
||||
side loses when two held manifests claim the same key.
|
||||
*/
|
||||
enum class ManifestSource : std::uint8_t {
|
||||
/** Relayed by a peer: the legacy transport, open to anyone.
|
||||
|
||||
A master key not already held from gossip is admitted only if its
|
||||
ephemeral key has recently signed a validation this node verified, and
|
||||
only into one of ManifestCache::gossipCapacity slots, which go to the
|
||||
most active.
|
||||
*/
|
||||
gossip = 0,
|
||||
|
||||
/** Read from an ltMANIFEST ledger object.
|
||||
|
||||
The ledger is the store and this is a lookup cache over it, of
|
||||
ManifestCache::ledgerCapacity entries: dropping one costs a SHAMap
|
||||
read the next time it is wanted, and loses nothing. Revocations are
|
||||
expected to live here, since a validator revokes on-ledger.
|
||||
*/
|
||||
ledger = 1,
|
||||
|
||||
/** Carried in a validator list, the config, or the wallet those were
|
||||
saved to. Never evicted.
|
||||
*/
|
||||
list = 2,
|
||||
};
|
||||
|
||||
inline std::string
|
||||
to_string(ManifestSource s)
|
||||
{
|
||||
switch (s)
|
||||
{
|
||||
case ManifestSource::gossip:
|
||||
return "gossip";
|
||||
case ManifestSource::ledger:
|
||||
return "ledger";
|
||||
case ManifestSource::list:
|
||||
return "list";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
@@ -267,65 +339,51 @@ to_string(ManifestDisposition m)
|
||||
|
||||
class DatabaseCon;
|
||||
|
||||
/** Remembers manifests with the highest sequence number. */
|
||||
/** Remembers manifests with the highest sequence number.
|
||||
|
||||
Every master key held is retained in one of three tiers:
|
||||
|
||||
- list: the master key is pinned (trusted), configured, or a validator
|
||||
list or the wallet carried a manifest for it. Never evicted, whatever
|
||||
transport later delivered a newer manifest for it.
|
||||
|
||||
- ledger: otherwise, if the manifest held was read from the ledger. A
|
||||
lookup cache over the ledger, capped at ledgerCapacity; when full the
|
||||
least active entry is dropped, to be read again on demand.
|
||||
|
||||
- gossip: otherwise. Capped at gossipCapacity. A master key enters only
|
||||
if its ephemeral key is in seen_, a rotating bloom filter of ephemeral
|
||||
keys that recently signed a validation this node verified, and only by
|
||||
displacing an entry less active than a single fresh sighting.
|
||||
|
||||
Activity is the number of validations seen from the held ephemeral key,
|
||||
decayed exponentially with a half-life of halfLife. It is what decides
|
||||
eviction in both capped tiers.
|
||||
|
||||
Gossip that fails the bloom filter is held, unverified, in a fixed ring of
|
||||
pendingCapacity entries keyed by ephemeral key. The first validation
|
||||
signed by that key releases it into the cache. Without this a validator's
|
||||
manifest, which normally arrives before its first validation, would be
|
||||
refused everywhere and never offered again.
|
||||
|
||||
A held manifest that conflicts with a new one over a key, and is held in a
|
||||
lower tier than the new one would be, is evicted rather than the new one
|
||||
refused. A manifest is never evicted by one ranked at or below it.
|
||||
|
||||
Ledger access goes through readLedger() alone, so a source other than a
|
||||
full SHAMap can be substituted there.
|
||||
*/
|
||||
class ManifestCache
|
||||
{
|
||||
private:
|
||||
beast::Journal j_;
|
||||
std::shared_mutex mutable mutex_;
|
||||
|
||||
/** Active manifests stored by master public key. */
|
||||
hash_map<PublicKey, Manifest> map_;
|
||||
|
||||
/** Master public keys stored by current ephemeral public key. */
|
||||
hash_map<PublicKey, PublicKey> signingToMasterKeys_;
|
||||
|
||||
/** Master keys always offered to a peer, whatever their recency.
|
||||
|
||||
Set by pin(); in practice the master keys on the configured validator
|
||||
lists, which are the manifests consensus actually depends on.
|
||||
*/
|
||||
hash_set<PublicKey> pinned_;
|
||||
|
||||
/** Recency of use, keyed by master public key.
|
||||
|
||||
The structure of this map is guarded by mutex_ exactly as map_ is:
|
||||
entries are created next to it in applyManifest() and are never
|
||||
removed. The counters themselves are atomic, so recording a hit is a
|
||||
write to an atomic rather than a structural modification and is legal
|
||||
while only a shared lock is held.
|
||||
|
||||
This is deliberately not a second mutex. A second mutex would have to
|
||||
be ordered against mutex_, and that ordering would be an unenforced
|
||||
invariant that any future caller could invert.
|
||||
*/
|
||||
hash_map<PublicKey, std::atomic<std::uint64_t>> mutable lastUsed_;
|
||||
std::atomic<std::uint64_t> mutable tick_{0};
|
||||
|
||||
/** Ephemeral keys already probed against the ledger, and where.
|
||||
|
||||
Bounds the reads driven by incoming validations to one per key per
|
||||
ledger. A key that resolves does not come back here: the mapping then
|
||||
lives in signingToMasterKeys_ and applyLedgerSigningKey() answers from
|
||||
it before reaching this map.
|
||||
|
||||
Unlike lastUsed_ this is written structurally, so it is guarded by
|
||||
mutex_ in exclusive mode.
|
||||
*/
|
||||
hash_map<PublicKey, std::uint32_t> probed_;
|
||||
|
||||
/** Record that a manifest was looked up.
|
||||
|
||||
@pre The caller holds mutex_, shared or exclusive.
|
||||
*/
|
||||
void
|
||||
touch(PublicKey const& masterKey) const;
|
||||
|
||||
std::atomic<std::uint32_t> seq_{0};
|
||||
|
||||
public:
|
||||
/** Ceiling on the unpinned manifests offered to a newly connected peer. */
|
||||
static constexpr std::size_t gossipLimit = 64;
|
||||
/** Manifests held for gossip-tier master keys. */
|
||||
static constexpr std::size_t gossipCapacity = 64;
|
||||
|
||||
/** Manifests held for ledger-tier master keys: a lookup cache. */
|
||||
static constexpr std::size_t ledgerCapacity = 512;
|
||||
|
||||
/** Gossip manifests waiting, unverified, for their ephemeral key. */
|
||||
static constexpr std::size_t pendingCapacity = 64;
|
||||
|
||||
/** Ceiling on remembered ephemeral key probes before they are dropped.
|
||||
|
||||
@@ -334,11 +392,202 @@ public:
|
||||
*/
|
||||
static constexpr std::size_t probeLimit = 4096;
|
||||
|
||||
explicit ManifestCache(
|
||||
beast::Journal j = beast::Journal(beast::Journal::getNullSink()))
|
||||
: j_(j)
|
||||
/** Time over which a master key's validation activity halves. */
|
||||
static constexpr std::chrono::seconds halfLife{300};
|
||||
|
||||
/** Epoch of seen_. A key is remembered for three to four of them. */
|
||||
static constexpr std::chrono::seconds seenInterval{120};
|
||||
|
||||
/** Ephemeral keys recently seen signing a verified validation.
|
||||
|
||||
2 KiB (16 Kibit) per generation, 4 generations: 8 KiB in all. Around
|
||||
0.2% false positives per live generation with a thousand distinct keys
|
||||
in it.
|
||||
*/
|
||||
using SeenFilter = RotatingBloomFilter<std::size_t{1} << 14, 4, 4>;
|
||||
|
||||
private:
|
||||
struct Slot
|
||||
{
|
||||
}
|
||||
Slot(
|
||||
ManifestSource source_,
|
||||
ManifestSource tier_,
|
||||
bool vouched_,
|
||||
double weight_)
|
||||
: source(source_), tier(tier_), vouched(vouched_), weight(weight_)
|
||||
{
|
||||
}
|
||||
|
||||
/// Where the manifest held for this master key came from.
|
||||
ManifestSource source;
|
||||
|
||||
/// What it is retained as; see tierOf().
|
||||
ManifestSource tier;
|
||||
|
||||
/// A validator list, the config or the wallet once carried a manifest
|
||||
/// for this master key. Survives later updates from other sources.
|
||||
bool vouched;
|
||||
|
||||
/** Validations seen from the held ephemeral key, decayed.
|
||||
|
||||
Stored as the sum, over the times t it was seen, of
|
||||
2^((t - weightBase_) / halfLife). Every slot shares weightBase_, so
|
||||
comparing two stored weights compares their decayed values at any
|
||||
common instant, without computing a decay.
|
||||
|
||||
Atomic so that a sighting is recorded under a shared lock.
|
||||
Everything else in a slot changes only under an exclusive one.
|
||||
*/
|
||||
std::atomic<double> weight;
|
||||
};
|
||||
|
||||
struct Pending
|
||||
{
|
||||
/// Arrival order; the lowest is replaced first.
|
||||
std::uint64_t order = 0;
|
||||
std::optional<Manifest> manifest;
|
||||
/// manifest has been checked with Manifest::verify(), and passed.
|
||||
bool verified = false;
|
||||
};
|
||||
|
||||
beast::Journal j_;
|
||||
Stopwatch& clock_;
|
||||
std::shared_mutex mutable mutex_;
|
||||
|
||||
/** Active manifests stored by master public key. */
|
||||
hash_map<PublicKey, Manifest> map_;
|
||||
|
||||
/** Master public keys stored by current ephemeral public key. */
|
||||
hash_map<PublicKey, PublicKey> signingToMasterKeys_;
|
||||
|
||||
/** Retention state, by master public key. In step with map_. */
|
||||
hash_map<PublicKey, Slot> slots_;
|
||||
|
||||
/** Number of slots_ in each tier, indexed by ManifestSource. */
|
||||
std::array<std::size_t, 3> tierSize_{};
|
||||
|
||||
/** Master keys retained as list regardless of source; set by pin(). */
|
||||
hash_set<PublicKey> pinned_;
|
||||
|
||||
/** Master keys of the configured validator token and revocation. */
|
||||
hash_set<PublicKey> configured_;
|
||||
|
||||
std::array<Pending, pendingCapacity> pending_;
|
||||
std::uint64_t pendingOrder_ = 0;
|
||||
std::size_t pendingSize_ = 0;
|
||||
|
||||
/** Ephemeral keys already probed against the ledger, and where.
|
||||
|
||||
Bounds the reads driven by incoming validations to one per key per
|
||||
ledger. Guarded by mutex_ in exclusive mode.
|
||||
*/
|
||||
hash_map<PublicKey, std::uint32_t> probed_;
|
||||
|
||||
/** Reference instant for Slot::weight. Moved forward by rebase(). */
|
||||
Stopwatch::time_point weightBase_;
|
||||
|
||||
SeenFilter seen_{seenInterval};
|
||||
|
||||
std::atomic<std::uint32_t> seq_{0};
|
||||
|
||||
/** Tier a master key is retained in, given where its manifest came from.
|
||||
|
||||
@pre The caller holds mutex_, shared or exclusive.
|
||||
*/
|
||||
ManifestSource
|
||||
tierOf(PublicKey const& master, ManifestSource source, bool vouched) const;
|
||||
|
||||
/** Weight of a single sighting at now. @pre mutex_ held. */
|
||||
double
|
||||
unit(Stopwatch::time_point now) const;
|
||||
|
||||
/** Whether weightBase_ is far enough behind to need moving now.
|
||||
|
||||
@pre mutex_ held.
|
||||
*/
|
||||
bool
|
||||
overdue(Stopwatch::time_point now) const;
|
||||
|
||||
/** Move weightBase_ toward now, rescaling every weight to match.
|
||||
|
||||
@pre mutex_ held exclusively.
|
||||
*/
|
||||
void
|
||||
rebase(Stopwatch::time_point now);
|
||||
|
||||
/** The least active entry in a tier, other than exclude.
|
||||
|
||||
@pre mutex_ held.
|
||||
*/
|
||||
std::optional<std::pair<PublicKey, double>>
|
||||
coldest(ManifestSource tier, PublicKey const* exclude) const;
|
||||
|
||||
/** Whether master could enter tier, competing with weight.
|
||||
|
||||
A capped tier that is full admits by evicting its least active entry.
|
||||
The lookup cache always does; the gossip tier only if that entry is
|
||||
less active than weight.
|
||||
|
||||
@pre mutex_ held.
|
||||
*/
|
||||
bool
|
||||
admissible(ManifestSource tier, PublicKey const& master, double weight)
|
||||
const;
|
||||
|
||||
/** Evict what admissible() said could be evicted.
|
||||
|
||||
@pre mutex_ held exclusively.
|
||||
*/
|
||||
bool
|
||||
makeRoom(ManifestSource tier, PublicKey const& master, double weight);
|
||||
|
||||
/** Forget a master key entirely. @pre mutex_ held exclusively. */
|
||||
void
|
||||
erase(PublicKey const& master);
|
||||
|
||||
/** Move a slot to another tier. @pre mutex_ held exclusively. */
|
||||
void
|
||||
retier(Slot& slot, ManifestSource tier);
|
||||
|
||||
/** Hold gossip until its ephemeral key is seen.
|
||||
|
||||
@param verified m has already been checked with Manifest::verify().
|
||||
Only a verified manifest may displace one already waiting for
|
||||
the same ephemeral key.
|
||||
|
||||
@pre mutex_ held exclusively; m names an ephemeral key.
|
||||
*/
|
||||
void
|
||||
addPending(Manifest m, bool verified);
|
||||
|
||||
/** @pre mutex_ held. */
|
||||
bool
|
||||
hasPending(PublicKey const& signingKey) const;
|
||||
|
||||
/** @pre mutex_ held exclusively. */
|
||||
std::optional<Manifest>
|
||||
takePending(PublicKey const& signingKey);
|
||||
|
||||
/** Adopt a gossip entry the ledger turns out to hold verbatim.
|
||||
|
||||
It is then a ledger lookup cache entry, and frees its gossip slot.
|
||||
|
||||
@pre mutex_ held exclusively.
|
||||
*/
|
||||
void
|
||||
adopt(Manifest const& m);
|
||||
|
||||
/** The ledger object for a manifest, keyed by its master or ephemeral key.
|
||||
|
||||
The one place the cache reads the ledger.
|
||||
*/
|
||||
std::shared_ptr<SLE const>
|
||||
readLedger(ReadView const& view, PublicKey const& key) const;
|
||||
|
||||
public:
|
||||
explicit ManifestCache(
|
||||
beast::Journal j = beast::Journal(beast::Journal::getNullSink()),
|
||||
Stopwatch& clock = stopwatch());
|
||||
|
||||
/** A monotonically increasing number used to detect new manifests. */
|
||||
std::uint32_t
|
||||
@@ -408,24 +657,65 @@ public:
|
||||
bool
|
||||
revoked(PublicKey const& pk) const;
|
||||
|
||||
/** Returns the tier a master key is held in, if it is held.
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::optional<ManifestSource>
|
||||
getTier(PublicKey const& pk) const;
|
||||
|
||||
/** Add manifest to cache.
|
||||
|
||||
@param m Manifest to add
|
||||
|
||||
@param source Where it came from. The default is the trusted path
|
||||
taken by validator lists, the config and the wallet.
|
||||
|
||||
@param view For gossip, a ledger to consult first. A master key whose
|
||||
ledger object is at the same or a later sequence -- above all, a
|
||||
revocation -- has that object ingested instead, and the gossip is
|
||||
stale. Ignored for other sources.
|
||||
|
||||
@return `ManifestDisposition::accepted` if successful, or
|
||||
`stale` or `invalid` otherwise
|
||||
the disposition explaining why admission was refused
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
ManifestDisposition
|
||||
applyManifest(Manifest m);
|
||||
applyManifest(
|
||||
Manifest m,
|
||||
ManifestSource source = ManifestSource::list,
|
||||
ReadView const* view = nullptr);
|
||||
|
||||
/** Set the master keys that are always offered to peers.
|
||||
/** Record a verified, current validation signed by an ephemeral key.
|
||||
|
||||
Replaces any previous set. Bumps sequence() when the set actually
|
||||
changes, so a cached gossip message built from it is rebuilt.
|
||||
Marks the key as seen for gossip admission, adds to the activity of the
|
||||
master key it belongs to, and releases a pending manifest for it.
|
||||
|
||||
@param signingKey Ephemeral key that signed the validation
|
||||
|
||||
@param view Passed on to applyManifest() for a released manifest
|
||||
|
||||
@return the manifest released and accepted, if any, for the caller to
|
||||
relay
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::optional<Manifest>
|
||||
noteValidation(PublicKey const& signingKey, ReadView const* view = nullptr);
|
||||
|
||||
/** Set the master keys retained as list, whatever their source.
|
||||
|
||||
Replaces any previous set. A key that leaves falls back to the tier of
|
||||
its source, which sheds its least active entries if that puts it over
|
||||
capacity. Bumps sequence() when the set actually changes, so a cached
|
||||
gossip message built from it is rebuilt.
|
||||
|
||||
@param keys Master public keys to pin
|
||||
|
||||
@@ -458,8 +748,7 @@ public:
|
||||
manifest found and feeds it through applyManifest(), so an on-chain
|
||||
manifest is subject to exactly the same staleness, revocation and
|
||||
key-reuse rules -- and the same signature check -- as one arriving by
|
||||
peer gossip or in a published list. It is a third source of manifests,
|
||||
not a more trusted one.
|
||||
peer gossip or in a published list.
|
||||
|
||||
Probes a known key set rather than scanning the ledger's transactions:
|
||||
this costs one SHAMap read per key, and picks up manifests published in
|
||||
@@ -477,22 +766,39 @@ public:
|
||||
std::size_t
|
||||
applyLedger(ReadView const& view, hash_set<PublicKey> const& masterKeys);
|
||||
|
||||
/** Refresh from the ledger everything held or pinned.
|
||||
|
||||
Probes every pinned and configured master key, and every master key
|
||||
held in any tier. This is what brings in a revocation, which is
|
||||
published on-ledger by master key and nowhere else, for a master key
|
||||
whose ephemeral key the cache already resolves and so never misses on.
|
||||
|
||||
@return the number of manifests newly accepted
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::size_t
|
||||
applyLedger(ReadView const& view);
|
||||
|
||||
/** Resolve an ephemeral signing key against a manifest published on-ledger.
|
||||
|
||||
applyLedger() probes a known master key set, which cannot help a key
|
||||
this node has no manifest for: the master key is exactly what is
|
||||
missing. SetManifest writes a second copy of every manifest keyed by
|
||||
its ephemeral key, so that case is one read rather than a search.
|
||||
The miss path of the ledger tier. applyLedger() probes a known master
|
||||
key set, which cannot help a key this node has no manifest for: the
|
||||
master key is exactly what is missing. SetManifest writes a second copy
|
||||
of every manifest keyed by its ephemeral key, so that case is one read
|
||||
rather than a search.
|
||||
|
||||
Anything found is fed through applyManifest(), so an on-chain manifest
|
||||
faces the same signature check and the same staleness, revocation and
|
||||
key-reuse rules as one arriving by gossip. The ledger is a transport
|
||||
here, not an authority: the answer is read back out of the cache rather
|
||||
than taken from the ledger object, because applyManifest() may decline
|
||||
it.
|
||||
key-reuse rules as one arriving by gossip. The answer is read back out
|
||||
of the cache rather than taken from the ledger object, because
|
||||
applyManifest() may decline it.
|
||||
|
||||
A key is probed at most once per ledger, and a key that resolves is
|
||||
answered from the cache thereafter without any ledger read.
|
||||
answered from the cache thereafter without any ledger read, until it
|
||||
is evicted.
|
||||
|
||||
@param view Ledger to read from
|
||||
@param signingKey Ephemeral public key to resolve
|
||||
@@ -506,6 +812,37 @@ public:
|
||||
std::optional<PublicKey>
|
||||
applyLedgerSigningKey(ReadView const& view, PublicKey const& signingKey);
|
||||
|
||||
/** Ingest the manifests listed in the ledger's manifest directory.
|
||||
|
||||
Every master key with a manifest on-ledger is listed in
|
||||
keylet::manifestDir(), so unlike applyLedger() this needs no key to
|
||||
start from. Pinned, configured and held master keys are refreshed as
|
||||
applyLedger() would; any other is admitted to the ledger tier only
|
||||
while it has room, so a bulk read fills the lookup cache without
|
||||
churning it.
|
||||
|
||||
The view need not be complete: one in which the directory and the
|
||||
objects it lists can be read will do. That is what lets a node read
|
||||
them during sync, before it holds the rest of the state map. The
|
||||
ledger need not be validated either: every manifest read is verified
|
||||
as from any other source, and the cache only ever moves forward, so
|
||||
the worst an unvalidated ledger can do is leave something out.
|
||||
|
||||
@param view Ledger to read from
|
||||
@param maxPages Most directory pages to read; see
|
||||
forEachLedgerManifest()
|
||||
|
||||
@return the number of manifests newly accepted
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::size_t
|
||||
applyLedgerDirectory(
|
||||
ReadView const& view,
|
||||
std::uint64_t maxPages = std::numeric_limits<std::uint64_t>::max());
|
||||
|
||||
/** Populate manifest cache with manifests in database and config.
|
||||
|
||||
@param dbCon Database connection with dbTable
|
||||
@@ -542,11 +879,15 @@ public:
|
||||
void
|
||||
load(DatabaseCon& dbCon, std::string const& dbTable);
|
||||
|
||||
/** Save cached manifests to database.
|
||||
/** Save list-tier manifests to database.
|
||||
|
||||
Ledger-tier manifests are on the ledger already, revocations included,
|
||||
and gossip-tier ones are not worth carrying across a restart.
|
||||
|
||||
@param dbCon Database connection with `ValidatorManifests` table
|
||||
|
||||
@param isTrusted Function that returns true if manifest is trusted
|
||||
@param isTrusted Function that returns true if manifest is trusted.
|
||||
Called without the cache locked, so it may call back into it.
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
@@ -614,19 +955,16 @@ public:
|
||||
|
||||
/** Invokes the callback for the manifests worth offering a new peer.
|
||||
|
||||
Offering the whole cache means offering everything the node has ever
|
||||
seen, which grows without bound and is mostly of no use to the peer.
|
||||
This offers the pinned set plus up to gossipLimit further manifests,
|
||||
most recently used first. A manifest left out still reaches the peer by
|
||||
ordinary relay if it turns out to be needed.
|
||||
The list and gossip tiers: at most the listed validators plus
|
||||
gossipCapacity. The ledger tier is left out, since any peer that is
|
||||
not amendment blocked can read it from the ledger.
|
||||
|
||||
@note Do not call ManifestCache member functions from within the
|
||||
callback. This can re-lock the mutex from the same thread, which is UB.
|
||||
@note Do not write ManifestCache member variables from
|
||||
within the callback. This can lead to data races.
|
||||
|
||||
@param pf Pre-function called with the maximum number of times f will be
|
||||
called (useful for memory allocations)
|
||||
@param pf Pre-function called with the number of times f will be called
|
||||
|
||||
@param f Function called for each manifest
|
||||
|
||||
@@ -640,40 +978,12 @@ public:
|
||||
{
|
||||
std::shared_lock lock{mutex_};
|
||||
|
||||
// Rank the unpinned entries by recency and keep the head. Pointers
|
||||
// into map_ stay valid: it is node based and the shared lock is held
|
||||
// throughout.
|
||||
std::vector<std::pair<std::uint64_t, PublicKey const*>> ranked;
|
||||
ranked.reserve(map_.size());
|
||||
for (auto const& [key, manifest] : map_)
|
||||
{
|
||||
(void)manifest;
|
||||
if (pinned_.count(key))
|
||||
continue;
|
||||
auto const used = lastUsed_.find(key);
|
||||
ranked.emplace_back(
|
||||
used == lastUsed_.end()
|
||||
? 0
|
||||
: used->second.load(std::memory_order_relaxed),
|
||||
&key);
|
||||
}
|
||||
pf(tierSize_[static_cast<std::size_t>(ManifestSource::list)] +
|
||||
tierSize_[static_cast<std::size_t>(ManifestSource::gossip)]);
|
||||
|
||||
auto const keep = std::min(gossipLimit, ranked.size());
|
||||
std::partial_sort(
|
||||
ranked.begin(),
|
||||
ranked.begin() + keep,
|
||||
ranked.end(),
|
||||
[](auto const& a, auto const& b) { return a.first > b.first; });
|
||||
|
||||
// An upper bound: a pinned key need not have a manifest yet.
|
||||
pf(pinned_.size() + keep);
|
||||
|
||||
for (auto const& key : pinned_)
|
||||
if (auto const iter = map_.find(key); iter != map_.end())
|
||||
f(iter->second);
|
||||
|
||||
for (std::size_t i = 0; i < keep; ++i)
|
||||
f(map_.find(*ranked[i].second)->second);
|
||||
for (auto const& [key, slot] : slots_)
|
||||
if (slot.tier != ManifestSource::ledger)
|
||||
f(map_.find(key)->second);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_MISC_MANIFESTLEDGER_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_MANIFESTLEDGER_H_INCLUDED
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ApplyView;
|
||||
class ReadView;
|
||||
struct Manifest;
|
||||
|
||||
/** Write a manifest to the ledger, replacing any earlier one for its master.
|
||||
|
||||
A manifest is stored twice so that it can be found from either key:
|
||||
|
||||
keylet::manifest(masterKey) -> obj1, sfManifestID -> obj2
|
||||
keylet::manifest(signingKey) -> obj2, sfManifestID -> obj1
|
||||
|
||||
A revocation has no signing key, so it is stored only as obj1, with no
|
||||
sfManifestID. The master key's account points at obj1 with its own
|
||||
sfManifestID.
|
||||
|
||||
obj1 is listed in keylet::manifestDir() and records its page there in
|
||||
sfOwnerNode. The entry is made with the master key's first manifest and is
|
||||
never removed: every later manifest for the master key, a revocation
|
||||
included, is written at the same key, so the entry stays and the page is
|
||||
carried from the old obj1 to the new. Removing and re-adding it instead
|
||||
would move it between pages on every key rotation for no gain. obj2 is not
|
||||
listed; it moves with the ephemeral key, and a reader finds it from obj1.
|
||||
|
||||
Both of the old objects are erased and both new ones written afresh, so the
|
||||
two copies can never drift apart.
|
||||
|
||||
@param view View to write to
|
||||
@param account The master key's account root, which is updated
|
||||
@param manifest The manifest as signed, in its serialized STObject form
|
||||
@param parsed The same manifest, deserialized and checked by the caller
|
||||
@param j Journal for reporting a corrupt ledger
|
||||
|
||||
@return tesSUCCESS; tecDIR_FULL if the directory cannot take another
|
||||
master key; tefBAD_LEDGER if the existing objects are inconsistent
|
||||
*/
|
||||
TER
|
||||
writeManifestObjects(
|
||||
ApplyView& view,
|
||||
std::shared_ptr<SLE> const& account,
|
||||
STObject const& manifest,
|
||||
Manifest const& parsed,
|
||||
beast::Journal j);
|
||||
|
||||
/** Visit every master key's manifest object listed in keylet::manifestDir().
|
||||
|
||||
Each master key is visited once, with its current object: a revocation if
|
||||
it has been revoked. Entries that do not name an ltMANIFEST object are
|
||||
skipped.
|
||||
|
||||
The view need not be complete. It must be able to read the directory and
|
||||
the objects listed in it, which is all this reads; a view over a partially
|
||||
synced state map that holds them will do.
|
||||
|
||||
@param view View to read from
|
||||
@param f Called with each master manifest object
|
||||
@param maxPages Most directory pages to read, the root page included.
|
||||
Pages are only ever appended, so this reads the oldest entries. A
|
||||
partial view need hold no more pages than this.
|
||||
*/
|
||||
void
|
||||
forEachLedgerManifest(
|
||||
ReadView const& view,
|
||||
std::function<void(std::shared_ptr<SLE const> const&)> const& f,
|
||||
std::uint64_t maxPages = dirNodeMaxPages);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/LedgerToJson.h>
|
||||
#include <xrpld/app/ledger/LocalTxs.h>
|
||||
#include <xrpld/app/ledger/ManifestSync.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/ledger/OrderBookDB.h>
|
||||
#include <xrpld/app/ledger/TransactionMaster.h>
|
||||
@@ -319,6 +320,13 @@ private:
|
||||
void
|
||||
publishNewerManifests(ReadView const& ledger);
|
||||
|
||||
/** Read manifests from ledgers peers report, while too few of this node's
|
||||
validators can be resolved to trust any ledger. See
|
||||
[early_manifest_sync].
|
||||
*/
|
||||
void
|
||||
bootstrapManifests();
|
||||
|
||||
public:
|
||||
bool
|
||||
beginConsensus(
|
||||
@@ -667,6 +675,12 @@ private:
|
||||
std::atomic<OperatingMode> mMode;
|
||||
|
||||
std::atomic<bool> needNetworkLedger_{false};
|
||||
|
||||
/** State of bootstrapManifests(), which only the heartbeat calls. */
|
||||
std::mutex manifestBootstrapMutex_;
|
||||
hash_set<uint256> manifestBootstrapTried_;
|
||||
std::chrono::steady_clock::time_point manifestBootstrapNext_{};
|
||||
|
||||
std::atomic<bool> amendmentBlocked_{false};
|
||||
std::atomic<bool> amendmentWarned_{false};
|
||||
std::atomic<bool> unlBlocked_{false};
|
||||
@@ -1013,6 +1027,30 @@ NetworkOPsImp::processHeartbeatTimer()
|
||||
<< ", changing to " << strOperatingMode(newMode, true);
|
||||
}
|
||||
CLOG(clog.ss()) << ". ";
|
||||
|
||||
// A lite node holds no state to close a ledger from, so no
|
||||
// consensus round ever runs. It tracks the network by the validated
|
||||
// ledgers it fetches: in step while the last one is fresh.
|
||||
if (app_.config().liteMode())
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
auto const age = m_ledgerMaster.getValidatedLedgerAge();
|
||||
if (age < 1min)
|
||||
{
|
||||
if (mMode < OperatingMode::TRACKING)
|
||||
setMode(OperatingMode::TRACKING);
|
||||
}
|
||||
else if (mMode == OperatingMode::TRACKING)
|
||||
{
|
||||
setMode(OperatingMode::CONNECTED);
|
||||
}
|
||||
CLOG(clog.ss())
|
||||
<< "lite mode, validated ledger age " << age.count() << "s. ";
|
||||
lock.unlock();
|
||||
bootstrapManifests();
|
||||
setHeartbeatTimer();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mConsensus.timerEntry(app_.timeKeeper().closeTime(), clog.ss());
|
||||
@@ -1027,6 +1065,8 @@ NetworkOPsImp::processHeartbeatTimer()
|
||||
}
|
||||
CLOG(clog.ss()) << ". ";
|
||||
|
||||
bootstrapManifests();
|
||||
|
||||
setHeartbeatTimer();
|
||||
}
|
||||
|
||||
@@ -1246,6 +1286,105 @@ NetworkOPsImp::publishNewerManifests(ReadView const& ledger)
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/** How often a node that cannot resolve enough of its validators looks for
|
||||
ledgers to read their manifests from. */
|
||||
constexpr std::chrono::seconds manifestBootstrapInterval{15};
|
||||
|
||||
/** Most ledgers it starts reading at each look. */
|
||||
constexpr std::size_t manifestBootstrapWidth = 3;
|
||||
|
||||
/** Ledgers remembered as read, past which the record starts over. */
|
||||
constexpr std::size_t manifestBootstrapMemory = 256;
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
NetworkOPsImp::bootstrapManifests()
|
||||
{
|
||||
// Only until a ledger has been validated: from then on the manifests are
|
||||
// kept current from each ledger as it closes, in beginConsensus().
|
||||
if (!app_.config().EARLY_MANIFEST_SYNC || m_ledgerMaster.haveValidated())
|
||||
return;
|
||||
|
||||
std::vector<uint256> picked;
|
||||
{
|
||||
auto const now = std::chrono::steady_clock::now();
|
||||
std::lock_guard lock(manifestBootstrapMutex_);
|
||||
if (now < manifestBootstrapNext_)
|
||||
return;
|
||||
manifestBootstrapNext_ = now + manifestBootstrapInterval;
|
||||
|
||||
// The validators whose manifests matter: the trusted ones once the
|
||||
// trusted set has been worked out, every listed one before that.
|
||||
auto keys = app_.validators().getTrustedMasterKeys();
|
||||
bool const trusted = !keys.empty();
|
||||
if (!trusted)
|
||||
app_.validators().for_each_listed(
|
||||
[&keys](PublicKey const& pk, bool) { keys.insert(pk); });
|
||||
if (keys.empty())
|
||||
return;
|
||||
|
||||
// Enough of them to make a quorum.
|
||||
std::size_t const needed =
|
||||
manifestQuorum(keys.size(), trusted, app_.validators().quorum());
|
||||
|
||||
auto const& manifests = app_.validatorManifests();
|
||||
std::size_t resolved = 0;
|
||||
for (auto const& pk : keys)
|
||||
if (manifests.getManifest(pk))
|
||||
++resolved;
|
||||
|
||||
if (resolved >= needed)
|
||||
return;
|
||||
|
||||
std::vector<uint256> reported;
|
||||
for (auto const& peer : app_.overlay().getActivePeers())
|
||||
reported.push_back(peer->getClosedLedgerHash());
|
||||
|
||||
if (manifestBootstrapTried_.size() > manifestBootstrapMemory)
|
||||
manifestBootstrapTried_.clear();
|
||||
|
||||
picked = pickManifestCandidates(
|
||||
reported, manifestBootstrapTried_, manifestBootstrapWidth);
|
||||
if (picked.empty())
|
||||
return;
|
||||
|
||||
manifestBootstrapTried_.insert(picked.begin(), picked.end());
|
||||
|
||||
JLOG(m_journal.info())
|
||||
<< "Manifests held for " << resolved << " of " << keys.size()
|
||||
<< " validators, " << needed << " needed: reading manifests from "
|
||||
<< picked.size() << " ledger(s) reported by " << reported.size()
|
||||
<< " peer(s)";
|
||||
}
|
||||
|
||||
// Each is acquired as consensus would acquire it. Before a ledger has
|
||||
// been validated, InboundLedger reads the manifests first, by key and from
|
||||
// the directory, and only then fetches the rest. The first pick is
|
||||
// usually the ledger checkLastClosedLedger() is already acquiring. Ledgers
|
||||
// of the same network share nearly all their nodes, so each further one
|
||||
// costs little more than what changed; one that only a peer has fails and
|
||||
// costs nothing more.
|
||||
//
|
||||
// Runs on the heartbeat, ahead of re-arming it, so nothing here may throw.
|
||||
for (auto const& hash : picked)
|
||||
{
|
||||
JLOG(m_journal.debug()) << "Reading manifests from ledger " << hash;
|
||||
try
|
||||
{
|
||||
app_.getInboundLedgers().acquire(
|
||||
hash, 0, InboundLedger::Reason::CONSENSUS);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(m_journal.warn()) << "Could not start reading manifests from "
|
||||
<< "ledger " << hash << ": " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::processTransaction(
|
||||
std::shared_ptr<Transaction>& transaction,
|
||||
@@ -1451,9 +1590,28 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
|
||||
batchLock.unlock();
|
||||
|
||||
// A lite node cannot apply a transaction: doing so reads the sender's
|
||||
// root, the destination's, its hooks, directories and whatever else the
|
||||
// transactor touches, nearly all of which it does not hold. It checks
|
||||
// what can be checked without state (preflight, done by
|
||||
// checkValidity() on the way in) and relays the rest to the network,
|
||||
// reporting terLITE_RELAYED. The outcome is read from the validated
|
||||
// ledger it turns up in, through `tx` or an account subscription.
|
||||
bool const lite = app_.config().liteMode();
|
||||
|
||||
{
|
||||
std::unique_lock masterLock{app_.getMasterMutex(), std::defer_lock};
|
||||
bool changed = false;
|
||||
if (lite)
|
||||
{
|
||||
masterLock.lock();
|
||||
for (TransactionStatus& e : transactions)
|
||||
{
|
||||
e.result = terLITE_RELAYED;
|
||||
e.applied = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::unique_lock ledgerLock{
|
||||
m_ledgerMaster.peekMutex(), std::defer_lock};
|
||||
@@ -1541,6 +1699,16 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
JLOG(m_journal.info()) << "Transaction is obsolete";
|
||||
e.transaction->setStatus(OBSOLETE);
|
||||
}
|
||||
else if (e.result == terLITE_RELAYED)
|
||||
{
|
||||
// Neither held nor kept: a lite node has nothing to retry
|
||||
// it against, and LocalTxs::sweep() would read the sender
|
||||
// on every validated ledger.
|
||||
JLOG(m_journal.debug())
|
||||
<< "Transaction relayed by lite node, not applied";
|
||||
e.transaction->setStatus(HELD);
|
||||
addLocal = false;
|
||||
}
|
||||
else if (e.result == terQUEUED)
|
||||
{
|
||||
JLOG(m_journal.debug())
|
||||
@@ -1588,7 +1756,8 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
if ((e.applied ||
|
||||
((mMode != OperatingMode::FULL) &&
|
||||
(e.failType != FailHard::yes) && e.local) ||
|
||||
(e.result == terQUEUED)) &&
|
||||
(e.result == terQUEUED) ||
|
||||
(e.result == terLITE_RELAYED && e.local)) &&
|
||||
!enforceFailHard)
|
||||
{
|
||||
auto const toSkip =
|
||||
@@ -1610,11 +1779,19 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
tx.set_deferred(e.result == terQUEUED);
|
||||
// FIXME: This should be when we received it
|
||||
app_.overlay().relay(e.transaction->getID(), tx, *toSkip);
|
||||
|
||||
// Also fan it out over the UDP Superhighway, if enabled.
|
||||
// shouldRelay() above ensures we do this at most once.
|
||||
app_.overlay().publishTxXUSH(
|
||||
s.slice(), e.transaction->getID());
|
||||
|
||||
e.transaction->setBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
if (validatedLedgerIndex)
|
||||
// Not for a lite node: this reads the sender's root, which it
|
||||
// holds only if the sender is watched.
|
||||
if (validatedLedgerIndex && !lite)
|
||||
{
|
||||
auto [fee, accountSeq, availableSeq] =
|
||||
app_.getTxQ().getTxRequiredFeeAndSeq(
|
||||
@@ -2081,13 +2258,13 @@ NetworkOPsImp::beginConsensus(
|
||||
app_.validators().setNegativeUNL(prevLedger->negativeUNL());
|
||||
// Pull in any manifests published on-ledger before the trusted set is
|
||||
// recomputed, so a validator that rotated its ephemeral key on-chain is
|
||||
// resolved to the new signing key in this same round. The master keys come
|
||||
// from the published lists, so this needs no bootstrap: only the ephemeral
|
||||
// half of the mapping ever comes from a manifest.
|
||||
// resolved to the new signing key in this same round. Every master key the
|
||||
// cache holds or pins is probed, not only the trusted ones: a revocation is
|
||||
// published on-ledger by master key alone, and a master key whose ephemeral
|
||||
// key the cache already resolves would otherwise never be looked up again.
|
||||
if (prevLedger->rules().enabled(featureOnChainManifests))
|
||||
{
|
||||
app_.validatorManifests().applyLedger(
|
||||
*prevLedger, app_.validators().getTrustedMasterKeys());
|
||||
app_.validatorManifests().applyLedger(*prevLedger);
|
||||
|
||||
// The reverse of applyLedger above. Manifests reach us by peer gossip
|
||||
// and in published validator lists, both of which can arrive before
|
||||
@@ -2103,8 +2280,8 @@ NetworkOPsImp::beginConsensus(
|
||||
app_.overlay(),
|
||||
app_.getHashRouter());
|
||||
|
||||
// Pin the trusted master keys so they are always offered to a new peer and
|
||||
// cannot be crowded out of the gossip set by more recently used manifests.
|
||||
// Pin the trusted master keys: their manifests are retained in the list
|
||||
// tier and offered to every new peer, whichever source delivered them.
|
||||
app_.validatorManifests().pin(app_.validators().getTrustedMasterKeys());
|
||||
|
||||
if (!changes.added.empty() || !changes.removed.empty())
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_MISC_ROTATINGBLOOMFILTER_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_ROTATINGBLOOMFILTER_H_INCLUDED
|
||||
|
||||
#include <xrpl/basics/hardened_hash.h>
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Approximate membership of everything inserted recently, in fixed space.
|
||||
|
||||
Time is cut into epochs of a fixed interval. Each of the Generations
|
||||
filters holds the insertions of one epoch: an insertion goes to the filter
|
||||
for the current epoch, and a query consults every filter whose epoch is
|
||||
one of the last Generations. An element is therefore reported for at least
|
||||
(Generations - 1) and at most Generations intervals after it was last
|
||||
inserted, and then forgotten.
|
||||
|
||||
A filter is cleared lazily, when its slot is first wanted for a newer
|
||||
epoch. Nothing has to tick: an idle filter costs nothing, and a clock that
|
||||
jumps forward simply finds every slot expired.
|
||||
|
||||
Within the window there are no false negatives. False positives occur at
|
||||
the usual bloom rate for one epoch's insertions, once per live generation.
|
||||
The footprint is Generations * Bits / 8 bytes however much is inserted:
|
||||
saturating the filter raises the false positive rate and never grows it.
|
||||
|
||||
Bit positions come from a hash seeded randomly per instance, so nobody
|
||||
without the seed can choose elements that set a particular set of bits.
|
||||
|
||||
@par Thread Safety
|
||||
Safe to call concurrently. The internal mutex is never held while calling
|
||||
out, so this is a leaf in any lock order.
|
||||
*/
|
||||
template <std::size_t Bits, std::size_t Hashes, std::size_t Generations>
|
||||
class RotatingBloomFilter
|
||||
{
|
||||
static_assert(
|
||||
Bits >= 64 && (Bits & (Bits - 1)) == 0,
|
||||
"Bits must be a power of two, at least 64");
|
||||
static_assert(
|
||||
Bits <= (std::size_t{1} << 32),
|
||||
"bit positions are computed in 32 bits");
|
||||
static_assert(Hashes >= 1 && Hashes <= 16);
|
||||
static_assert(
|
||||
Generations >= 2,
|
||||
"a single generation would forget everything at each rotation");
|
||||
|
||||
public:
|
||||
using clock_type = std::chrono::steady_clock;
|
||||
using time_point = clock_type::time_point;
|
||||
using duration = clock_type::duration;
|
||||
|
||||
/** Size of the filter state, in bytes. */
|
||||
static constexpr std::size_t bytes = Generations * Bits / 8;
|
||||
|
||||
explicit RotatingBloomFilter(duration interval)
|
||||
: interval_(interval > duration::zero() ? interval : duration{1})
|
||||
{
|
||||
epochs_.fill(none);
|
||||
}
|
||||
|
||||
RotatingBloomFilter(RotatingBloomFilter const&) = delete;
|
||||
RotatingBloomFilter&
|
||||
operator=(RotatingBloomFilter const&) = delete;
|
||||
|
||||
/** Length of one epoch. */
|
||||
duration
|
||||
interval() const
|
||||
{
|
||||
return interval_;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void
|
||||
insert(T const& item, time_point now)
|
||||
{
|
||||
auto const positions = probe(item);
|
||||
auto const epoch = epochOf(now);
|
||||
auto const slot = slotOf(epoch);
|
||||
|
||||
std::lock_guard lock{mutex_};
|
||||
|
||||
// The slot last held an earlier epoch, now out of the window or about
|
||||
// to be: it is reused for this one. A slot already ahead of us (the
|
||||
// caller read the clock before another thread rotated) keeps its bits,
|
||||
// and ours are added to them: that can only remember the element a
|
||||
// little longer than asked, never forget it early.
|
||||
if (epochs_[slot] < epoch)
|
||||
{
|
||||
bits_[slot].fill(0);
|
||||
epochs_[slot] = epoch;
|
||||
}
|
||||
|
||||
for (auto const p : positions)
|
||||
bits_[slot][p / 64] |= std::uint64_t{1} << (p % 64);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
bool
|
||||
contains(T const& item, time_point now) const
|
||||
{
|
||||
auto const positions = probe(item);
|
||||
auto const oldest =
|
||||
epochOf(now) - static_cast<std::int64_t>(Generations);
|
||||
|
||||
std::lock_guard lock{mutex_};
|
||||
|
||||
for (std::size_t g = 0; g < Generations; ++g)
|
||||
{
|
||||
if (epochs_[g] == none || epochs_[g] <= oldest)
|
||||
continue;
|
||||
|
||||
bool all = true;
|
||||
for (auto const p : positions)
|
||||
{
|
||||
if ((bits_[g][p / 64] & (std::uint64_t{1} << (p % 64))) == 0)
|
||||
{
|
||||
all = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (all)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::size_t words = Bits / 64;
|
||||
static constexpr std::int64_t none =
|
||||
std::numeric_limits<std::int64_t>::min();
|
||||
|
||||
std::int64_t
|
||||
epochOf(time_point now) const
|
||||
{
|
||||
// Floor division, so a clock reading before its own epoch still maps
|
||||
// to consecutive epochs.
|
||||
auto const t = now.time_since_epoch().count();
|
||||
auto const i = interval_.count();
|
||||
return t >= 0 ? t / i : -((-t + i - 1) / i);
|
||||
}
|
||||
|
||||
static std::size_t
|
||||
slotOf(std::int64_t epoch)
|
||||
{
|
||||
auto const g = static_cast<std::int64_t>(Generations);
|
||||
return static_cast<std::size_t>(((epoch % g) + g) % g);
|
||||
}
|
||||
|
||||
/** Bit positions for an element: Kirsch-Mitzenmacher double hashing.
|
||||
|
||||
Hashes positions from a single 64 bit hash, as h1 + i * h2. Forcing h2
|
||||
odd makes it invertible modulo the power of two table size, so the
|
||||
positions for one element are all distinct.
|
||||
*/
|
||||
template <class T>
|
||||
std::array<std::uint32_t, Hashes>
|
||||
probe(T const& item) const
|
||||
{
|
||||
std::uint64_t const h = hash_(item);
|
||||
auto const h1 = static_cast<std::uint32_t>(h);
|
||||
auto const h2 = static_cast<std::uint32_t>(h >> 32) | 1u;
|
||||
constexpr auto mask = static_cast<std::uint32_t>(Bits - 1);
|
||||
|
||||
std::array<std::uint32_t, Hashes> positions;
|
||||
for (std::size_t i = 0; i < Hashes; ++i)
|
||||
positions[i] = (h1 + static_cast<std::uint32_t>(i) * h2) & mask;
|
||||
return positions;
|
||||
}
|
||||
|
||||
hardened_hash<> const hash_;
|
||||
duration const interval_;
|
||||
std::mutex mutable mutex_;
|
||||
std::array<std::int64_t, Generations> epochs_;
|
||||
std::array<std::array<std::uint64_t, words>, Generations> bits_{};
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -381,7 +381,9 @@ void
|
||||
SHAMapStoreImp::loadPinnedRanges()
|
||||
{
|
||||
auto rangesStr = state_db_.getPinnedRanges();
|
||||
if (rangesStr.empty())
|
||||
// "empty" is what to_string() renders for an empty set; accept it in
|
||||
// case it was stored before setPinnedRanges() learned to write "".
|
||||
if (rangesStr.empty() || rangesStr == "empty")
|
||||
return;
|
||||
|
||||
auto const& nscfg = app_.config().section(ConfigSection::nodeDatabase());
|
||||
@@ -411,22 +413,62 @@ SHAMapStoreImp::loadPinnedRanges()
|
||||
static_cast<bool>(app_.getNodeStore().fetchNodeObject(hash, seq));
|
||||
};
|
||||
|
||||
//
|
||||
// catalogue_load records ranges at checkpoints, after handing the pinned
|
||||
// backend every node of the recorded ledgers, but not every backend can
|
||||
// make those writes durable on demand (NuDB commits on its own thread).
|
||||
// A crash can therefore leave the tail of an interval recorded but not
|
||||
// on disk. Each ledger's nodes reach the backend before its header, and
|
||||
// ledgers in sequence, so what survives is a prefix: trim the upper bound
|
||||
// back to the last ledger whose header is present instead of refusing to
|
||||
// start. A missing lower bound still means the configured pinned store
|
||||
// is not the one these ranges were loaded into.
|
||||
constexpr std::uint32_t maxTailTrim = 65536;
|
||||
|
||||
RangeSet<std::uint32_t> verified;
|
||||
for (auto const& interval : persistedRanges)
|
||||
{
|
||||
if (!hasLedgerData(interval.lower()) ||
|
||||
!hasLedgerData(interval.upper()))
|
||||
{
|
||||
Throw<std::runtime_error>(
|
||||
"Persisted pinned interval " +
|
||||
std::to_string(interval.lower()) + "-" +
|
||||
auto const lower = interval.lower();
|
||||
auto upper = interval.upper();
|
||||
|
||||
auto const notPresent = [&]() {
|
||||
return std::runtime_error(
|
||||
"Persisted pinned interval " + std::to_string(lower) + "-" +
|
||||
std::to_string(interval.upper()) +
|
||||
" is not present in the currently configured pinned store");
|
||||
};
|
||||
|
||||
if (!hasLedgerData(lower))
|
||||
Throw<std::runtime_error>(notPresent());
|
||||
|
||||
std::uint32_t trimmed = 0;
|
||||
while (upper > lower && !hasLedgerData(upper))
|
||||
{
|
||||
if (++trimmed > maxTailTrim)
|
||||
Throw<std::runtime_error>(notPresent());
|
||||
--upper;
|
||||
}
|
||||
|
||||
if (trimmed)
|
||||
{
|
||||
JLOG(journal_.warn())
|
||||
<< "Pinned interval " << lower << "-" << interval.upper()
|
||||
<< " was recorded but its last " << trimmed
|
||||
<< " ledger(s) are not in the pinned store (likely an "
|
||||
"interrupted catalogue_load); trimmed to "
|
||||
<< lower << "-" << upper
|
||||
<< ". Re-run catalogue_load to restore them.";
|
||||
}
|
||||
|
||||
verified.insert(range(lower, upper));
|
||||
}
|
||||
|
||||
if (verified != persistedRanges)
|
||||
state_db_.setPinnedRanges(to_string(verified));
|
||||
|
||||
JLOG(journal_.info()) << "Loaded pinned ranges from database: "
|
||||
<< rangesStr;
|
||||
app_.getLedgerMaster().setPinnedLedgersRangeSet(persistedRanges);
|
||||
<< to_string(verified);
|
||||
app_.getLedgerMaster().setPinnedLedgersRangeSet(verified);
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -184,9 +184,11 @@ public:
|
||||
void
|
||||
setPinnedRanges(RangeSet<std::uint32_t> const& ranges) override
|
||||
{
|
||||
// Save pinned ranges to database for persistence
|
||||
std::string rangesStr = to_string(ranges);
|
||||
state_db_.setPinnedRanges(rangesStr);
|
||||
// Save pinned ranges to database for persistence. to_string()
|
||||
// renders an empty set as "empty", which from_string() rejects, so
|
||||
// store "" instead: loadPinnedRanges() treats that as nothing pinned.
|
||||
state_db_.setPinnedRanges(
|
||||
ranges.empty() ? std::string{} : to_string(ranges));
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -302,6 +302,15 @@ struct AmendmentState
|
||||
/** Indicates an amendment that this server has code support for. */
|
||||
bool supported = false;
|
||||
|
||||
/** Indicates an amendment listed in [ignore_amendments]. The server has
|
||||
no code for it, but must not become amendment blocked if it is (or
|
||||
becomes) enabled on the ledger. Such an amendment is reported as
|
||||
`supported` internally so every blocking check (LedgerMaster,
|
||||
NetworkOPs, Change) passes, but is never voted for and is shown as
|
||||
unsupported + ignored over RPC.
|
||||
*/
|
||||
bool ignored = false;
|
||||
|
||||
/** The name of this amendment, possibly empty. */
|
||||
std::string name;
|
||||
|
||||
@@ -476,7 +485,8 @@ public:
|
||||
std::vector<FeatureInfo> const& supported,
|
||||
Section const& enabled,
|
||||
Section const& vetoed,
|
||||
beast::Journal journal);
|
||||
beast::Journal journal,
|
||||
Section const& ignored);
|
||||
|
||||
uint256
|
||||
find(std::string const& name) const override;
|
||||
@@ -543,7 +553,8 @@ AmendmentTableImpl::AmendmentTableImpl(
|
||||
std::vector<FeatureInfo> const& supported,
|
||||
Section const& enabled,
|
||||
Section const& vetoed,
|
||||
beast::Journal journal)
|
||||
beast::Journal journal,
|
||||
Section const& ignored)
|
||||
: lastUpdateSeq_(0)
|
||||
, majorityTime_(majorityTime)
|
||||
, unsupportedEnabled_(false)
|
||||
@@ -586,6 +597,59 @@ AmendmentTableImpl::AmendmentTableImpl(
|
||||
<< " voted by default if not enabled on the ledger.";
|
||||
}
|
||||
|
||||
// Parse ignored amendments from config. Each line is either a registered
|
||||
// feature name (e.g. "HookOnV2") or "<64 hex amendment ID> [name]". These
|
||||
// are deliberately NOT persisted to wallet.db: removing the line from the
|
||||
// config must restore normal amendment-blocking behaviour.
|
||||
for (auto const& line : ignored.lines())
|
||||
{
|
||||
std::string const trimmed = boost::algorithm::trim_copy(line);
|
||||
if (trimmed.empty())
|
||||
continue;
|
||||
|
||||
std::vector<std::string> parts;
|
||||
boost::algorithm::split(
|
||||
parts,
|
||||
trimmed,
|
||||
boost::algorithm::is_space(),
|
||||
boost::algorithm::token_compress_on);
|
||||
|
||||
uint256 id;
|
||||
std::string name;
|
||||
if (id.parseHex(parts[0]))
|
||||
name = parts.size() > 1 ? parts[1] : std::string{};
|
||||
else if (auto const f = getRegisteredFeature(parts[0]))
|
||||
{
|
||||
id = *f;
|
||||
name = parts[0];
|
||||
}
|
||||
else
|
||||
Throw<std::runtime_error>(
|
||||
"Invalid entry '" + line + "' in [" + ignored.name() +
|
||||
"]: expected a registered feature name or amendment ID");
|
||||
|
||||
AmendmentState& s = add(id, lock);
|
||||
if (s.supported)
|
||||
{
|
||||
JLOG(j_.warn()) << "[" << ignored.name() << "] lists amendment "
|
||||
<< id << " (" << s.name
|
||||
<< ") which this server supports; entry has no "
|
||||
"effect.";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!name.empty())
|
||||
s.name = name;
|
||||
s.supported = true;
|
||||
s.ignored = true;
|
||||
s.vote = AmendmentVote::obsolete;
|
||||
|
||||
JLOG(j_.warn()) << "Amendment " << id << " (" << s.name
|
||||
<< ") is unsupported but listed in [" << ignored.name()
|
||||
<< "]: this server will NOT become amendment blocked "
|
||||
"if it is enabled on the ledger.";
|
||||
}
|
||||
|
||||
hash_set<uint256> detect_conflict;
|
||||
// Parse enabled amendments from config
|
||||
for (std::pair<uint256, std::string> const& a : parseSection(enabled))
|
||||
@@ -775,7 +839,12 @@ AmendmentTableImpl::enable(uint256 const& amendment)
|
||||
|
||||
s.enabled = true;
|
||||
|
||||
if (!s.supported)
|
||||
if (s.ignored)
|
||||
{
|
||||
JLOG(j_.warn()) << "Ignored unsupported amendment " << amendment << " ("
|
||||
<< s.name << ") is enabled on the ledger.";
|
||||
}
|
||||
else if (!s.supported)
|
||||
{
|
||||
JLOG(j_.error()) << "Unsupported amendment " << amendment
|
||||
<< " activated.";
|
||||
@@ -1044,7 +1113,9 @@ AmendmentTableImpl::injectJson(
|
||||
if (!fs.name.empty())
|
||||
v[jss::name] = fs.name;
|
||||
|
||||
v[jss::supported] = fs.supported;
|
||||
v[jss::supported] = fs.supported && !fs.ignored;
|
||||
if (fs.ignored)
|
||||
v["ignored"] = true;
|
||||
if (!fs.enabled && isAdmin)
|
||||
{
|
||||
if (fs.vote == AmendmentVote::obsolete)
|
||||
@@ -1113,10 +1184,11 @@ make_AmendmentTable(
|
||||
std::vector<AmendmentTable::FeatureInfo> const& supported,
|
||||
Section const& enabled,
|
||||
Section const& vetoed,
|
||||
beast::Journal journal)
|
||||
beast::Journal journal,
|
||||
Section const& ignored)
|
||||
{
|
||||
return std::make_unique<AmendmentTableImpl>(
|
||||
app, majorityTime, supported, enabled, vetoed, journal);
|
||||
app, majorityTime, supported, enabled, vetoed, journal, ignored);
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user