mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-22 11:47:09 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
197633aec7 |
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,7 +12,6 @@ libxrpl.server > xrpl.basics
|
||||
libxrpl.server > xrpl.json
|
||||
libxrpl.server > xrpl.protocol
|
||||
libxrpl.server > xrpl.server
|
||||
test.app > test.shamap
|
||||
test.app > test.toplevel
|
||||
test.app > test.unit_test
|
||||
test.app > xrpl.basics
|
||||
@@ -22,7 +21,6 @@ test.app > xrpld.ledger
|
||||
test.app > xrpld.nodestore
|
||||
test.app > xrpld.overlay
|
||||
test.app > xrpld.rpc
|
||||
test.app > xrpld.shamap
|
||||
test.app > xrpl.hook
|
||||
test.app > xrpl.json
|
||||
test.app > xrpl.protocol
|
||||
@@ -98,12 +96,10 @@ test.overlay > test.toplevel
|
||||
test.overlay > test.unit_test
|
||||
test.overlay > xrpl.basics
|
||||
test.overlay > xrpld.app
|
||||
test.overlay > xrpld.core
|
||||
test.overlay > xrpld.overlay
|
||||
test.overlay > xrpld.peerfinder
|
||||
test.overlay > xrpld.shamap
|
||||
test.overlay > xrpl.protocol
|
||||
test.overlay > xrpl.resource
|
||||
test.peerfinder > test.beast
|
||||
test.peerfinder > test.unit_test
|
||||
test.peerfinder > xrpl.basics
|
||||
@@ -195,7 +191,6 @@ xrpld.overlay > xrpl.basics
|
||||
xrpld.overlay > xrpld.core
|
||||
xrpld.overlay > xrpld.peerfinder
|
||||
xrpld.overlay > xrpld.perflog
|
||||
xrpld.overlay > xrpld.shamap
|
||||
xrpld.overlay > xrpl.json
|
||||
xrpld.overlay > xrpl.protocol
|
||||
xrpld.overlay > xrpl.resource
|
||||
|
||||
+1
-1
@@ -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 -p /data/builds/candidate
|
||||
mkdir /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,13 +166,10 @@
|
||||
# ws Websockets
|
||||
# wss Secure Websockets
|
||||
# peer Peer Protocol
|
||||
# pwa On-ledger AppLoader documents (featurePWALoader), for a
|
||||
# reverse proxy only. Read "pwa ports" below first.
|
||||
#
|
||||
# Restrictions:
|
||||
#
|
||||
# Only one port may be configured to support the peer protocol.
|
||||
# The pwa protocol cannot be combined with any other protocol.
|
||||
# A port cannot have websocket and non websocket protocols at the
|
||||
# same time. It is possible have both Websockets and Secure Websockets
|
||||
# together in one port.
|
||||
@@ -267,119 +264,6 @@
|
||||
# "secure_gateway" connections, then they will be treated as
|
||||
# "admin" addresses.
|
||||
#
|
||||
# pwa ports
|
||||
#
|
||||
# A port whose protocol is "pwa" serves accounts' on-ledger AppLoader
|
||||
# documents (featurePWALoader) as HTML. This makes the node a web host
|
||||
# for third-party content: anyone can publish a loader to an account
|
||||
# for about a drop per byte, and nothing in protocol takes one down.
|
||||
# Hosting, and a hosting provider's obligations, are yours.
|
||||
#
|
||||
# One host per account, on a zone used for nothing else:
|
||||
#
|
||||
# https://pwa<account-index>.<domain>/<r-address>
|
||||
# https://<domain>/<r-address> 301 to the above
|
||||
# https://<custom host>/ see custom_domains
|
||||
#
|
||||
# The first label of the Host header is "pwa" followed by the decimal
|
||||
# AccountIndex of the account (an account created before AccountIndex
|
||||
# existed has no host); the rest must be one of the port's "domain"
|
||||
# values; the path must be that account's exact r-address. One
|
||||
# trailing slash and a query string are ignored; anything else is
|
||||
# 404. The bare zone never serves a document (every page would share
|
||||
# one origin): it redirects, keeping :port and query.
|
||||
# Never set cookies at or above the zone and never serve your own
|
||||
# site from it. Wildcard DNS and a wildcard certificate cover it.
|
||||
#
|
||||
# Rules enforced at startup and on every request:
|
||||
#
|
||||
# - "pwa" is the port's only protocol; no admin, user/password or
|
||||
# ssl_* keys (terminate TLS at the proxy).
|
||||
# - "domain" is required: the zone(s), comma-separated. The proxy must
|
||||
# forward the Host header unchanged.
|
||||
# - "secure_gateway" is required and may not cover every address.
|
||||
# Every request must come from it carrying X-Forwarded-For, whose
|
||||
# RIGHTMOST entry (the one the proxy wrote) is the client. The
|
||||
# "Forwarded" header is ignored.
|
||||
# - Clients are metered per IPv4 address, per IPv6 /64; a miss costs
|
||||
# what a hit does; over-use is answered 503 until it decays.
|
||||
# - GET and HEAD only; every response closes the connection; only
|
||||
# validated state is served, with an ETag (304 on If-None-Match).
|
||||
# X-NetworkID, X-LedgerSeq, X-LedgerHash and X-PWASeq name the
|
||||
# validated ledger and AppLoaderSequence the document came from, so
|
||||
# a client (a browser extension, say) can vet it against a second
|
||||
# node with the pwa_validate RPC (account, hash of the bytes, host).
|
||||
# - Lookups run on at most two job queue threads, below client RPC;
|
||||
# beyond a bounded backlog they are refused with 503. Put a cache in
|
||||
# front (the ETag makes revalidation cheap).
|
||||
# - The admin RPC "pwa_info" reports requests by outcome and bytes
|
||||
# served; "reset": true zeroes them, "flush_dns": true drops the
|
||||
# custom domain cache (CLI: xahaud pwa_info [reset] [flush_dns]).
|
||||
#
|
||||
# Example (also add port_pwa to the [server] list):
|
||||
#
|
||||
# [port_pwa]
|
||||
# port = 8088
|
||||
# ip = 127.0.0.1
|
||||
# protocol = pwa
|
||||
# secure_gateway = 127.0.0.1
|
||||
# domain = pwapp.example
|
||||
#
|
||||
# with nginx in front, TLS terminated there, Host passed through:
|
||||
#
|
||||
# server {
|
||||
# listen 443 ssl;
|
||||
# server_name pwapp.example ~^pwa[0-9]+\.pwapp\.example$;
|
||||
# ssl_certificate /etc/ssl/wildcard.pwapp.example.pem;
|
||||
# ssl_certificate_key /etc/ssl/wildcard.pwapp.example.key;
|
||||
# location / {
|
||||
# proxy_pass http://127.0.0.1:8088;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Forwarded-For $remote_addr;
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# custom_domains = 0 | 1
|
||||
#
|
||||
# Default 0. With 1, a request whose Host is outside every zone is
|
||||
# served as the account named by DNS, so a domain owner can point
|
||||
# their own name at your proxy:
|
||||
#
|
||||
# app.example.org. CNAME pwa-proxy.your.example.
|
||||
# _xahau-pwa.app.example.org. TXT "rAbC..."
|
||||
#
|
||||
# The record lives on the _xahau-pwa label beneath the host because
|
||||
# the host is usually a CNAME to your proxy. Exactly one address must
|
||||
# be present there (other TXT strings are ignored; two addresses count
|
||||
# as none). The named account must consent: its Domain field must
|
||||
# equal the host or a parent of it (ignoring case), else 503. Lists
|
||||
# apply as at the account's own host; only the root path is a
|
||||
# document. No usable record is 404 (cached 15 minutes), a resolver
|
||||
# failure 503 (cached 60 s), a record is cached for its TTL (clamped
|
||||
# to 60..3600 s). Lookups run on their own job queue threads, at most
|
||||
# four at a time and one per host; a request that misses the cache is
|
||||
# answered 429 with Retry-After and a refreshing page rather than
|
||||
# waiting, so a flood of Host headers cannot tie up the node. The
|
||||
# admin RPC pwa_info with "flush_dns": true drops the cache. Your
|
||||
# proxy must accept, and hold certificates for, the hosts you will
|
||||
# serve (e.g. Caddy on-demand TLS); xahaud only sees the Host header.
|
||||
#
|
||||
# resolver = <IPv4 address>[:port]
|
||||
#
|
||||
# The recursive resolver for custom_domains; default is the system
|
||||
# resolver. xahaud does not validate DNSSEC, so prefer a validating
|
||||
# resolver on the same host.
|
||||
#
|
||||
# Documents are served with "Content-Security-Policy: sandbox
|
||||
# allow-scripts ...": an opaque origin that cannot read anything from
|
||||
# the node but can still send requests anywhere, including the
|
||||
# visitor's own machine, so keep admin ports behind credentials.
|
||||
#
|
||||
# [pwa_allowlist] and [pwa_blocklist], one r-address per line, apply to
|
||||
# every pwa port: if the allowlist is non-empty only those accounts are
|
||||
# served; blocklisted accounts are never served (403), whatever the
|
||||
# allowlist says. An account on both is a startup error.
|
||||
#
|
||||
# ssl_key = <filename>
|
||||
# ssl_cert = <filename>
|
||||
# ssl_chain = <filename>
|
||||
@@ -564,30 +448,6 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
# [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).
|
||||
@@ -814,38 +674,6 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
# [early_manifest_sync]
|
||||
#
|
||||
# Controls whether, while acquiring its first ledger, the server fetches and
|
||||
# reads the on-ledger validator manifests ahead of the rest of that ledger's
|
||||
# state. Doing so lets it resolve validators' current signing keys, including
|
||||
# rotations and revocations published on-ledger, within seconds of starting
|
||||
# a sync rather than once the whole state has arrived.
|
||||
#
|
||||
# The manifests read are those of the master keys on the server's own
|
||||
# validator lists, fetched by key, and as many more from the on-ledger
|
||||
# manifest directory as the manifest cache keeps.
|
||||
#
|
||||
# The cost is a dozen or so request/response rounds with peers. With more
|
||||
# than one peer serving the ledger these overlap with fetching the rest of
|
||||
# its state; with only one, they come first.
|
||||
#
|
||||
# It also lets a server that starts with nothing but the master keys of its
|
||||
# validators and a few peers find its way. Until it has validated a ledger,
|
||||
# while it holds manifests for too few of its validators to make a quorum,
|
||||
# it reads them from up to three of the ledgers its peers report, the most
|
||||
# widely reported first, every 15 seconds. Every manifest read is checked
|
||||
# against its master key's signature, so a ledger reported by a peer that
|
||||
# is not on the network can leave something out but cannot add anything.
|
||||
#
|
||||
# Legal values are:
|
||||
# 1 - Fetch the manifests first, and from peers' ledgers if needed
|
||||
# 0 - Fetch the ledger's state in the usual order, and read the manifests
|
||||
# from it once it is complete
|
||||
#
|
||||
# The default is 1. A ledger without on-ledger manifests costs only the few
|
||||
# nodes on the paths to where their directory and objects would be.
|
||||
#
|
||||
#
|
||||
#
|
||||
# [ledger_history]
|
||||
@@ -899,38 +727,6 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
# [export_key_file]
|
||||
#
|
||||
# Where a validator keeps its export keys (featureExport), the keys it uses
|
||||
# to multisign transactions hooks export to other networks. Defaults to
|
||||
# export_keys.txt in [database_path]. The validator generates and rotates
|
||||
# the keys itself, keeping each for as long as its account lists it on
|
||||
# ledger, so this file is as sensitive as the validator token and should be
|
||||
# backed up. Servers that take turns validating with the same token must
|
||||
# share it, or each will replace the other's current export key.
|
||||
#
|
||||
# Example:
|
||||
# /var/lib/xahaud/export_keys.txt
|
||||
#
|
||||
#
|
||||
#
|
||||
# [xrpl_relay]
|
||||
#
|
||||
# Carries exports (featureExport) to the XRPL and their results back: one
|
||||
# XRPL peer per "host [port]" line (port defaults to 51235). Exports whose
|
||||
# UNL signatures meet the exporter's quorum are sent to XRPL, and XRPL
|
||||
# transactions returning an export are imported here as XPOPs. The XRPL UNL
|
||||
# comes from validator lists signed by an [import_vl_keys] or voted import
|
||||
# key, relayed by the peer or fetched from the optional vl_url. Set
|
||||
# network_id when the XRPL network is not mainnet.
|
||||
#
|
||||
# Example:
|
||||
# s1.ripple.com 51235
|
||||
# s2.ripple.com
|
||||
# vl_url=https://vl.ripple.com
|
||||
#
|
||||
#
|
||||
#
|
||||
# [validator_key_revocation]
|
||||
#
|
||||
# If a validator's secret key has been compromised, a revocation must be
|
||||
@@ -1620,19 +1416,6 @@
|
||||
# contains breaking changes that require a new API version number.
|
||||
# They are not ready for public consumption.
|
||||
#
|
||||
# [hook_validate_rpc]
|
||||
#
|
||||
# 0 or 1.
|
||||
#
|
||||
# 0: Disable the hook_validate RPC method
|
||||
# 1: Enable the hook_validate RPC method, which runs the Hook guard
|
||||
# checker and WASM VM validation against caller-supplied code using
|
||||
# the current open ledger's rules.
|
||||
#
|
||||
# The default is 0 on networked nodes and 1 in standalone mode (-a).
|
||||
# When enabled the method is available to non-admin (user role) clients,
|
||||
# so consider the CPU cost before enabling it on a public-facing node.
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# 10. Example Settings
|
||||
|
||||
@@ -151,3 +151,4 @@ D686F2538F410C9D0D856788E98E3579595DAF7B38D38887F81ECAC934B06040 HooksUpdate1
|
||||
4B8466415FAB32FFA89D9DCBE166A42340115771DF611A7160F8D7439C87ECD8 fixNSDelete
|
||||
EDB4EE4C524E16BDD91D9A529332DED08DCAAA51CC6DC897ACFA1A0ED131C5B6 fix240819
|
||||
8063140E9260799D6716756B891CEC3E7006C4E4F277AB84670663A88F94B9C4 fixPageCap
|
||||
88693F108C3CD8A967F3F4253A32DEF5E35F9406ACD2A11B88B11D90865763A9 fix240911
|
||||
@@ -97,7 +97,6 @@ target_link_libraries (ripple_syslibs
|
||||
crypt32
|
||||
>
|
||||
$<$<NOT:$<BOOL:${MSVC}>>:dl>
|
||||
$<$<NOT:$<BOOL:${MSVC}>>:resolv>
|
||||
$<$<NOT:$<OR:$<BOOL:${MSVC}>,$<BOOL:${APPLE}>>>:rt>)
|
||||
|
||||
if (NOT MSVC)
|
||||
|
||||
@@ -47,6 +47,5 @@
|
||||
#define MEM_OVERLAP -43
|
||||
#define TOO_MANY_STATE_MODIFICATIONS -44
|
||||
#define TOO_MANY_NAMESPACES -45
|
||||
#define TOO_LITTLE_ENTROPY -46
|
||||
#define HOOK_ERROR_CODES
|
||||
#endif //HOOK_ERROR_CODES
|
||||
@@ -339,19 +339,6 @@ prepare(
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len);
|
||||
|
||||
extern int64_t
|
||||
dice(uint32_t sides);
|
||||
|
||||
extern int64_t
|
||||
util_random(uint32_t write_ptr, uint32_t write_len);
|
||||
|
||||
extern int64_t
|
||||
xport(
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len,
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+23
-23
@@ -22,29 +22,29 @@ APPLY_HOOK="$SCRIPT_DIR/../include/xrpl/hook/hook_api.macro"
|
||||
return s;
|
||||
}
|
||||
|
||||
# Each entry is HOOK_API_DEFINITION(RET, NAME, (PARAMS), AMENDMENT),
|
||||
# possibly spread over several lines. Collect until the parentheses
|
||||
# balance, then split on the first two commas; the parameter tuple
|
||||
# is the first parenthesised group.
|
||||
function emit(s, i, ret, name, p, q, params) {
|
||||
sub(/^[[:space:]]*HOOK_API_DEFINITION[[:space:]]*\(/, "", s);
|
||||
gsub(/[[:space:]]+/, " ", s);
|
||||
i = index(s, ","); ret = trim(substr(s, 1, i - 1)); s = substr(s, i + 1);
|
||||
i = index(s, ","); name = trim(substr(s, 1, i - 1)); s = substr(s, i + 1);
|
||||
p = index(s, "("); q = index(s, ")");
|
||||
params = trim(substr(s, p + 1, q - p - 1));
|
||||
if (name == "_g")
|
||||
name = "__attribute__((noduplicate)) _g";
|
||||
printf("extern %s %s(%s);\n\n", ret, name, params);
|
||||
}
|
||||
|
||||
/^[[:space:]]*HOOK_API_DEFINITION[[:space:]]*\(/ { buf = ""; collecting = 1; }
|
||||
|
||||
collecting {
|
||||
buf = buf " " $0;
|
||||
if (gsub(/\(/, "(", buf) == gsub(/\)/, ")", buf)) {
|
||||
collecting = 0;
|
||||
emit(buf);
|
||||
{
|
||||
line = $0;
|
||||
|
||||
# Skip block comments
|
||||
if (line ~ /\/\*/) {
|
||||
next;
|
||||
}
|
||||
|
||||
# Look for comment lines that start with // and contain function signature
|
||||
if (line ~ /^[[:space:]]*\/\/[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\(/) {
|
||||
# Remove leading // and trim
|
||||
sub(/^[[:space:]]*\/\/[[:space:]]*/, "", line);
|
||||
line = trim(line);
|
||||
|
||||
# Check if function name is "_g" to add attribute
|
||||
if (line ~ /[[:space:]]+_g[[:space:]]*\(/) {
|
||||
# Insert __attribute__((noduplicate)) before _g
|
||||
sub(/[[:space:]]+_g/, " __attribute__((noduplicate)) _g", line);
|
||||
}
|
||||
|
||||
# printf("\n");
|
||||
|
||||
printf("extern %s\n\n", line);
|
||||
}
|
||||
}
|
||||
' "$APPLY_HOOK"
|
||||
|
||||
+1
-1
@@ -46,8 +46,8 @@
|
||||
|
||||
#include "error.h"
|
||||
#include "extern.h"
|
||||
#include "macro.h"
|
||||
#include "sfcodes.h"
|
||||
#include "macro.h"
|
||||
#include "tts.h"
|
||||
|
||||
#include "ls_flags.h"
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
#define sfHookApiVersion ((1U << 16U) + 20U)
|
||||
#define sfHookStateScale ((1U << 16U) + 21U)
|
||||
#define sfLedgerFixType ((1U << 16U) + 22U)
|
||||
#define sfEntropyCount ((1U << 16U) + 99U)
|
||||
#define sfEntropyDenominator ((1U << 16U) + 100U)
|
||||
#define sfNetworkID ((2U << 16U) + 1U)
|
||||
#define sfFlags ((2U << 16U) + 2U)
|
||||
#define sfSourceTag ((2U << 16U) + 3U)
|
||||
@@ -74,7 +72,6 @@
|
||||
#define sfLockCount ((2U << 16U) + 49U)
|
||||
#define sfFirstNFTokenSequence ((2U << 16U) + 50U)
|
||||
#define sfOracleDocumentID ((2U << 16U) + 51U)
|
||||
#define sfExportKeysSeq ((2U << 16U) + 92U)
|
||||
#define sfStartTime ((2U << 16U) + 93U)
|
||||
#define sfRepeatCount ((2U << 16U) + 94U)
|
||||
#define sfDelaySeconds ((2U << 16U) + 95U)
|
||||
@@ -83,7 +80,6 @@
|
||||
#define sfRewardTime ((2U << 16U) + 98U)
|
||||
#define sfRewardLgrFirst ((2U << 16U) + 99U)
|
||||
#define sfRewardLgrLast ((2U << 16U) + 100U)
|
||||
#define sfAppLoaderSequence ((2U << 16U) + 101U)
|
||||
#define sfIndexNext ((3U << 16U) + 1U)
|
||||
#define sfIndexPrevious ((3U << 16U) + 2U)
|
||||
#define sfBookNode ((3U << 16U) + 3U)
|
||||
@@ -156,8 +152,6 @@
|
||||
#define sfEscrowID ((5U << 16U) + 35U)
|
||||
#define sfURITokenID ((5U << 16U) + 36U)
|
||||
#define sfDomainID ((5U << 16U) + 37U)
|
||||
#define sfNextRandomDigest ((5U << 16U) + 89U)
|
||||
#define sfRandomData ((5U << 16U) + 90U)
|
||||
#define sfManifestID ((5U << 16U) + 91U)
|
||||
#define sfHookOnOutgoing ((5U << 16U) + 93U)
|
||||
#define sfHookOnIncoming ((5U << 16U) + 94U)
|
||||
@@ -228,9 +222,6 @@
|
||||
#define sfProvider ((7U << 16U) + 30U)
|
||||
#define sfMPTokenMetadata ((7U << 16U) + 31U)
|
||||
#define sfCredentialType ((7U << 16U) + 32U)
|
||||
#define sfExportKey ((7U << 16U) + 94U)
|
||||
#define sfExportKeyProof ((7U << 16U) + 95U)
|
||||
#define sfAppLoader ((7U << 16U) + 96U)
|
||||
#define sfHookName ((7U << 16U) + 97U)
|
||||
#define sfRemarkValue ((7U << 16U) + 98U)
|
||||
#define sfRemarkName ((7U << 16U) + 99U)
|
||||
@@ -298,11 +289,6 @@
|
||||
#define sfXChainCreateAccountAttestationCollectionElement ((14U << 16U) + 31U)
|
||||
#define sfPriceData ((14U << 16U) + 32U)
|
||||
#define sfCredential ((14U << 16U) + 33U)
|
||||
#define sfExportKeyEntry ((14U << 16U) + 85U)
|
||||
#define sfExportSignerList ((14U << 16U) + 86U)
|
||||
#define sfExportKeyReport ((14U << 16U) + 87U)
|
||||
#define sfRandomDigestEntry ((14U << 16U) + 88U)
|
||||
#define sfExportedTxn ((14U << 16U) + 89U)
|
||||
#define sfManifest ((14U << 16U) + 90U)
|
||||
#define sfAmountEntry ((14U << 16U) + 91U)
|
||||
#define sfMintURIToken ((14U << 16U) + 92U)
|
||||
@@ -335,8 +321,6 @@
|
||||
#define sfAuthorizeCredentials ((15U << 16U) + 26U)
|
||||
#define sfUnauthorizeCredentials ((15U << 16U) + 27U)
|
||||
#define sfAcceptedCredentials ((15U << 16U) + 28U)
|
||||
#define sfExportKeys ((15U << 16U) + 90U)
|
||||
#define sfRandomDigests ((15U << 16U) + 91U)
|
||||
#define sfAmounts ((15U << 16U) + 92U)
|
||||
#define sfHookEmissions ((15U << 16U) + 93U)
|
||||
#define sfImportVLKeys ((15U << 16U) + 94U)
|
||||
|
||||
@@ -61,10 +61,6 @@
|
||||
#define ttNFTOKEN_MODIFY 70
|
||||
#define ttPERMISSIONED_DOMAIN_SET 71
|
||||
#define ttPERMISSIONED_DOMAIN_DELETE 72
|
||||
#define ttEXPORT 88
|
||||
#define ttEXPORT_SIGN 87
|
||||
#define ttEXPORT_FINAL 90
|
||||
#define ttENTROPY 89
|
||||
#define ttMANIFEST_SET 91
|
||||
#define ttCRON 92
|
||||
#define ttCRON_SET 93
|
||||
|
||||
@@ -106,8 +106,6 @@ enum ClaimRewardFlags : uint32_t {
|
||||
|
||||
enum CronSetFlags : uint32_t {
|
||||
tfCronUnset = 0x00000001,
|
||||
// featureExport: fire after each export key rotation (see Export.h)
|
||||
tfCronExportRotation = 0x00000002,
|
||||
};
|
||||
|
||||
enum AMMClawbackFlags : uint32_t {
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <shared_mutex>
|
||||
|
||||
// On Linux (glibc), std::shared_mutex wraps pthread_rwlock_t initialised
|
||||
// with PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP. This means a
|
||||
// pending exclusive lock() blocks new shared (reader) acquisitions,
|
||||
// causing reader starvation when writers contend frequently.
|
||||
//
|
||||
// On macOS / ARM (libc++), std::shared_mutex is already reader-preferring,
|
||||
// so the same code behaves differently across platforms.
|
||||
//
|
||||
// This header provides ReaderPreferringSharedMutex:
|
||||
// - On Linux it wraps pthread_rwlock_t initialised with
|
||||
// PTHREAD_RWLOCK_PREFER_READER_NP, matching macOS semantics.
|
||||
// - On all other platforms it is a type alias for std::shared_mutex.
|
||||
//
|
||||
// The interface is identical to std::shared_mutex, so it works with
|
||||
// std::shared_lock and std::unique_lock.
|
||||
|
||||
#if defined(__linux__)
|
||||
|
||||
#include <cerrno>
|
||||
#include <pthread.h>
|
||||
#include <system_error>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ReaderPreferringSharedMutex
|
||||
{
|
||||
pthread_rwlock_t rwlock_;
|
||||
|
||||
public:
|
||||
ReaderPreferringSharedMutex()
|
||||
{
|
||||
pthread_rwlockattr_t attr;
|
||||
pthread_rwlockattr_init(&attr);
|
||||
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_READER_NP);
|
||||
int rc = pthread_rwlock_init(&rwlock_, &attr);
|
||||
pthread_rwlockattr_destroy(&attr);
|
||||
if (rc != 0)
|
||||
throw std::system_error(
|
||||
rc, std::system_category(), "pthread_rwlock_init");
|
||||
}
|
||||
|
||||
~ReaderPreferringSharedMutex()
|
||||
{
|
||||
pthread_rwlock_destroy(&rwlock_);
|
||||
}
|
||||
|
||||
ReaderPreferringSharedMutex(ReaderPreferringSharedMutex const&) = delete;
|
||||
ReaderPreferringSharedMutex&
|
||||
operator=(ReaderPreferringSharedMutex const&) = delete;
|
||||
|
||||
// Exclusive (writer) locking
|
||||
void
|
||||
lock()
|
||||
{
|
||||
pthread_rwlock_wrlock(&rwlock_);
|
||||
}
|
||||
|
||||
bool
|
||||
try_lock()
|
||||
{
|
||||
return pthread_rwlock_trywrlock(&rwlock_) == 0;
|
||||
}
|
||||
|
||||
void
|
||||
unlock()
|
||||
{
|
||||
pthread_rwlock_unlock(&rwlock_);
|
||||
}
|
||||
|
||||
// Shared (reader) locking
|
||||
void
|
||||
lock_shared()
|
||||
{
|
||||
pthread_rwlock_rdlock(&rwlock_);
|
||||
}
|
||||
|
||||
bool
|
||||
try_lock_shared()
|
||||
{
|
||||
return pthread_rwlock_tryrdlock(&rwlock_) == 0;
|
||||
}
|
||||
|
||||
void
|
||||
unlock_shared()
|
||||
{
|
||||
pthread_rwlock_unlock(&rwlock_);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#else // !__linux__
|
||||
|
||||
namespace ripple {
|
||||
|
||||
// macOS, Windows, etc. — std::shared_mutex is already reader-preferring.
|
||||
using ReaderPreferringSharedMutex = std::shared_mutex;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -77,7 +77,7 @@ strUnHex(std::size_t strSize, Iterator begin, Iterator end)
|
||||
|
||||
if (strSize & 1)
|
||||
{
|
||||
int c = unxtab[static_cast<unsigned char>(*iter++)];
|
||||
int c = unxtab[*iter++];
|
||||
|
||||
if (c < 0)
|
||||
return {};
|
||||
@@ -87,12 +87,12 @@ strUnHex(std::size_t strSize, Iterator begin, Iterator end)
|
||||
|
||||
while (iter != end)
|
||||
{
|
||||
int cHigh = unxtab[static_cast<unsigned char>(*iter++)];
|
||||
int cHigh = unxtab[*iter++];
|
||||
|
||||
if (cHigh < 0)
|
||||
return {};
|
||||
|
||||
int cLow = unxtab[static_cast<unsigned char>(*iter++)];
|
||||
int cLow = unxtab[*iter++];
|
||||
|
||||
if (cLow < 0)
|
||||
return {};
|
||||
|
||||
+13
-20
@@ -15,10 +15,8 @@
|
||||
#define uint256 std::string
|
||||
#define featureHooksUpdate1 "1"
|
||||
#define featureHooksUpdate2 "1"
|
||||
#define featureRNG "1"
|
||||
#define fix20250131 "1"
|
||||
#define fixGuardDepth32 "1"
|
||||
#define featureExport "1"
|
||||
namespace hook_api {
|
||||
struct Rules
|
||||
{
|
||||
@@ -387,9 +385,7 @@ enum class hook_return_code : int64_t {
|
||||
MEM_OVERLAP = -43, // one or more specified buffers are the same memory
|
||||
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 256 modified state
|
||||
// entires in the combined hook chains
|
||||
TOO_MANY_NAMESPACES = -45,
|
||||
TOO_LITTLE_ENTROPY = -46 // featureRNG: no seed for this ledger, or too few
|
||||
// validators contributed to it
|
||||
TOO_MANY_NAMESPACES = -45
|
||||
};
|
||||
|
||||
enum class ExitType : uint8_t {
|
||||
@@ -402,25 +398,12 @@ enum class ExitType : uint8_t {
|
||||
const uint16_t max_state_modifications = 256;
|
||||
const uint8_t max_slots = 255;
|
||||
const uint8_t max_nonce = 255;
|
||||
const uint16_t max_rng_draws = 256; // featureRNG, per Hook execution
|
||||
const uint8_t max_emit = 255;
|
||||
const uint8_t max_params = 16;
|
||||
const double fee_base_multiplier = 1.1f;
|
||||
|
||||
using APIWhitelist = std::map<std::string, std::vector<uint8_t>>;
|
||||
|
||||
namespace detail {
|
||||
template <class T>
|
||||
constexpr uint8_t wasmTypeCode = sizeof(T) == 8 ? 0x7EU : 0x7FU;
|
||||
|
||||
template <class R, class... Args>
|
||||
std::vector<uint8_t>
|
||||
wasmSignature(R (*)(Args...))
|
||||
{
|
||||
return {wasmTypeCode<R>, wasmTypeCode<Args>...};
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
// RH NOTE: Find descriptions of api functions in ./impl/applyHook.cpp and
|
||||
// hookapi.h (include for hooks) this is a map of the api name to its return
|
||||
// code (vec[0] and its parameters vec[>0]) as wasm type codes
|
||||
@@ -432,15 +415,25 @@ getImportWhitelist(Rules const& rules)
|
||||
#pragma push_macro("HOOK_API_DEFINITION")
|
||||
#undef HOOK_API_DEFINITION
|
||||
|
||||
#define int64_t 0x7EU
|
||||
#define int32_t 0x7FU
|
||||
#define uint32_t 0x7FU
|
||||
|
||||
#define HOOK_WRAP_PARAMS(...) __VA_ARGS__
|
||||
|
||||
#define HOOK_API_DEFINITION( \
|
||||
RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, AMENDMENT) \
|
||||
if (AMENDMENT == uint256{} || rules.enabled(AMENDMENT)) \
|
||||
whitelist[#FUNCTION_NAME] = \
|
||||
detail::wasmSignature((RETURN_TYPE(*) PARAMS_TUPLE) nullptr);
|
||||
whitelist[#FUNCTION_NAME] = { \
|
||||
RETURN_TYPE, HOOK_WRAP_PARAMS PARAMS_TUPLE};
|
||||
|
||||
#include "hook_api.macro"
|
||||
|
||||
#undef HOOK_API_DEFINITION
|
||||
#undef HOOK_WRAP_PARAMS
|
||||
#undef int64_t
|
||||
#undef int32_t
|
||||
#undef uint32_t
|
||||
#pragma pop_macro("HOOK_API_DEFINITION")
|
||||
|
||||
return whitelist;
|
||||
|
||||
@@ -1096,11 +1096,7 @@ validateGuards(
|
||||
{
|
||||
int name_len = parseLeb128(wasm, i, &i);
|
||||
CHECK_SHORT_HOOK();
|
||||
// Only compare the name if all four bytes are in bounds. A
|
||||
// name running past the end is rejected as a short hook below
|
||||
// either way, so this does not change which hooks are valid.
|
||||
if (name_len == 4 &&
|
||||
static_cast<std::size_t>(i) + 4 <= wasm.size())
|
||||
if (name_len == 4)
|
||||
{
|
||||
if (wasm[i] == 'h' && wasm[i + 1] == 'o' &&
|
||||
wasm[i + 2] == 'o' && wasm[i + 3] == 'k')
|
||||
|
||||
@@ -1,313 +1,374 @@
|
||||
// clang-format off
|
||||
// int32_t _g(uint32_t guard_id, uint32_t maxiter);
|
||||
HOOK_API_DEFINITION(
|
||||
int32_t, _g, (uint32_t guard_id, uint32_t maxiter),
|
||||
int32_t, _g, (uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t accept(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, accept, (uint32_t read_ptr, uint32_t read_len, int64_t error_code),
|
||||
int64_t, accept, (uint32_t, uint32_t, int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t rollback(uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, rollback, (uint32_t read_ptr, uint32_t read_len, int64_t error_code),
|
||||
int64_t, rollback, (uint32_t, uint32_t, int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t util_raddr(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_raddr, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
int64_t, util_raddr, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t util_accid(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_accid, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
int64_t, util_accid, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t util_verify(uint32_t dread_ptr, uint32_t dread_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t kread_ptr, uint32_t kread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_verify, (uint32_t dread_ptr, uint32_t dread_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t kread_ptr, uint32_t kread_len),
|
||||
int64_t, util_verify, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t util_sha512h(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_sha512h, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
int64_t, util_sha512h, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t util_keylet(uint32_t write_ptr, uint32_t write_len, uint32_t keylet_type, uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_keylet, (uint32_t write_ptr, uint32_t write_len, uint32_t keylet_type, uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f),
|
||||
int64_t, util_keylet, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t sto_validate(uint32_t tread_ptr, uint32_t tread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, sto_validate, (uint32_t tread_ptr, uint32_t tread_len),
|
||||
int64_t, sto_validate, (uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t sto_subfield(uint32_t read_ptr, uint32_t read_len, uint32_t field_id);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, sto_subfield, (uint32_t read_ptr, uint32_t read_len, uint32_t field_id),
|
||||
int64_t, sto_subfield, (uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t sto_subarray(uint32_t read_ptr, uint32_t read_len, uint32_t array_id);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, sto_subarray, (uint32_t read_ptr, uint32_t read_len, uint32_t array_id),
|
||||
int64_t, sto_subarray, (uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t sto_emplace(uint32_t write_ptr, uint32_t write_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t fread_ptr, uint32_t fread_len, uint32_t field_id);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, sto_emplace, (uint32_t write_ptr, uint32_t write_len, uint32_t sread_ptr, uint32_t sread_len, uint32_t fread_ptr, uint32_t fread_len, uint32_t field_id),
|
||||
int64_t, sto_emplace, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t sto_erase(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t field_id);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, sto_erase, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t field_id),
|
||||
int64_t, sto_erase, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t etxn_burden();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, etxn_burden, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t etxn_details(uint32_t write_ptr, uint32_t write_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, etxn_details, (uint32_t write_ptr, uint32_t write_len),
|
||||
int64_t, etxn_details, (uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t etxn_fee_base(uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, etxn_fee_base, (uint32_t read_ptr, uint32_t read_len),
|
||||
int64_t, etxn_fee_base, (uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t etxn_reserve(uint32_t count);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, etxn_reserve, (uint32_t count),
|
||||
int64_t, etxn_reserve, (uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t etxn_generation();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, etxn_generation, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t etxn_nonce(uint32_t write_ptr, uint32_t write_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, etxn_nonce, (uint32_t write_ptr, uint32_t write_len),
|
||||
int64_t, etxn_nonce, (uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t emit(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, emit, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
int64_t, emit, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_set(int32_t exponent, int64_t mantissa);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_set, (int32_t exponent, int64_t mantissa),
|
||||
int64_t, float_set, (int32_t, int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_multiply(int64_t float1, int64_t float2);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_multiply, (int64_t float1, int64_t float2),
|
||||
int64_t, float_multiply, (int64_t, int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_mulratio(int64_t float1, uint32_t round_up, uint32_t numerator, uint32_t denominator);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_mulratio, (int64_t float1, uint32_t round_up, uint32_t numerator, uint32_t denominator),
|
||||
int64_t, float_mulratio, (int64_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_negate(int64_t float1);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_negate, (int64_t float1),
|
||||
int64_t, float_negate, (int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_compare(int64_t float1, int64_t float2, uint32_t mode);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_compare, (int64_t float1, int64_t float2, uint32_t mode),
|
||||
int64_t, float_compare, (int64_t, int64_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_sum(int64_t float1, int64_t float2);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_sum, (int64_t float1, int64_t float2),
|
||||
int64_t, float_sum, (int64_t, int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_sto(uint32_t write_ptr, uint32_t write_len, uint32_t cread_ptr, uint32_t cread_len, uint32_t iread_ptr, uint32_t iread_len, int64_t float1, uint32_t field_code);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_sto, (uint32_t write_ptr, uint32_t write_len, uint32_t cread_ptr, uint32_t cread_len, uint32_t iread_ptr, uint32_t iread_len, int64_t float1, uint32_t field_code),
|
||||
int64_t, float_sto, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, int64_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_sto_set(uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_sto_set, (uint32_t read_ptr, uint32_t read_len),
|
||||
int64_t, float_sto_set, (uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_invert(int64_t float1);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_invert, (int64_t float1),
|
||||
int64_t, float_invert, (int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_divide(int64_t float1, int64_t float2);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_divide, (int64_t float1, int64_t float2),
|
||||
int64_t, float_divide, (int64_t, int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_one();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_one, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_mantissa(int64_t float1);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_mantissa, (int64_t float1),
|
||||
int64_t, float_mantissa, (int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_sign(int64_t float1);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_sign, (int64_t float1),
|
||||
int64_t, float_sign, (int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_int(int64_t float1, uint32_t decimal_places, uint32_t abs);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_int, (int64_t float1, uint32_t decimal_places, uint32_t abs),
|
||||
int64_t, float_int, (int64_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_log(int64_t float1);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_log, (int64_t float1),
|
||||
int64_t, float_log, (int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t float_root(int64_t float1, uint32_t n);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, float_root, (int64_t float1, uint32_t n),
|
||||
int64_t, float_root, (int64_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t fee_base();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, fee_base, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t ledger_seq();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, ledger_seq, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t ledger_last_time();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, ledger_last_time, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t ledger_last_hash(uint32_t write_ptr, uint32_t write_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, ledger_last_hash, (uint32_t write_ptr, uint32_t write_len),
|
||||
int64_t, ledger_last_hash, (uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t ledger_nonce(uint32_t write_ptr, uint32_t write_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, ledger_nonce, (uint32_t write_ptr, uint32_t write_len),
|
||||
int64_t, ledger_nonce, (uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t ledger_keylet(uint32_t write_ptr, uint32_t write_len, uint32_t lread_ptr, uint32_t lread_len, uint32_t hread_ptr, uint32_t hread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, ledger_keylet, (uint32_t write_ptr, uint32_t write_len, uint32_t lread_ptr, uint32_t lread_len, uint32_t hread_ptr, uint32_t hread_len),
|
||||
int64_t, ledger_keylet, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_account, (uint32_t write_ptr, uint32_t write_len),
|
||||
int64_t, hook_account, (uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_hash(uint32_t write_ptr, uint32_t write_len, int32_t hook_no);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_hash, (uint32_t write_ptr, uint32_t write_len, int32_t hook_no),
|
||||
int64_t, hook_hash, (uint32_t, uint32_t, int32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_param_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t hread_ptr, uint32_t hread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_param_set, (uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t hread_ptr, uint32_t hread_len),
|
||||
int64_t, hook_param_set, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_param(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_param, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
int64_t, hook_param, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_again();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_again, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_skip(uint32_t read_ptr, uint32_t read_len, uint32_t flags);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_skip, (uint32_t read_ptr, uint32_t read_len, uint32_t flags),
|
||||
int64_t, hook_skip, (uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t hook_pos();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, hook_pos, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot(uint32_t write_ptr, uint32_t write_len, uint32_t slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot, (uint32_t write_ptr, uint32_t write_len, uint32_t slot),
|
||||
int64_t, slot, (uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_clear(uint32_t slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_clear, (uint32_t slot),
|
||||
int64_t, slot_clear, (uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_count(uint32_t slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_count, (uint32_t slot),
|
||||
int64_t, slot_count, (uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_set(uint32_t read_ptr, uint32_t read_len, uint32_t slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_set, (uint32_t read_ptr, uint32_t read_len, uint32_t slot),
|
||||
int64_t, slot_set, (uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_size(uint32_t slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_size, (uint32_t slot),
|
||||
int64_t, slot_size, (uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_subarray(uint32_t parent_slot, uint32_t array_id, uint32_t new_slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_subarray, (uint32_t parent_slot, uint32_t array_id, uint32_t new_slot),
|
||||
int64_t, slot_subarray, (uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_subfield(uint32_t parent_slot, uint32_t field_id, uint32_t new_slot);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_subfield, (uint32_t parent_slot, uint32_t field_id, uint32_t new_slot),
|
||||
int64_t, slot_subfield, (uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_type(uint32_t slot_no, uint32_t flags);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_type, (uint32_t slot_no, uint32_t flags),
|
||||
int64_t, slot_type, (uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t slot_float(uint32_t slot_no);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, slot_float, (uint32_t slot_no),
|
||||
int64_t, slot_float, (uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t state_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, state_set, (uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len),
|
||||
int64_t, state_set, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t state_foreign_set(uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, state_foreign_set, (uint32_t read_ptr, uint32_t read_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len),
|
||||
int64_t, state_foreign_set, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t state(uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, state, (uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len),
|
||||
int64_t, state, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t state_foreign(uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, state_foreign, (uint32_t write_ptr, uint32_t write_len, uint32_t kread_ptr, uint32_t kread_len, uint32_t nread_ptr, uint32_t nread_len, uint32_t aread_ptr, uint32_t aread_len),
|
||||
int64_t, state_foreign, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t trace(uint32_t mread_ptr, uint32_t mread_len, uint32_t dread_ptr, uint32_t dread_len, uint32_t as_hex);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, trace, (uint32_t mread_ptr, uint32_t mread_len, uint32_t dread_ptr, uint32_t dread_len, uint32_t as_hex),
|
||||
int64_t, trace, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t trace_num(uint32_t read_ptr, uint32_t read_len, int64_t number);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, trace_num, (uint32_t read_ptr, uint32_t read_len, int64_t number),
|
||||
int64_t, trace_num, (uint32_t, uint32_t, int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t trace_float(uint32_t read_ptr, uint32_t read_len, int64_t float1);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, trace_float, (uint32_t read_ptr, uint32_t read_len, int64_t float1),
|
||||
int64_t, trace_float, (uint32_t, uint32_t, int64_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_burden();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_burden, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_field(uint32_t write_ptr, uint32_t write_len, uint32_t field_id);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_field, (uint32_t write_ptr, uint32_t write_len, uint32_t field_id),
|
||||
int64_t, otxn_field, (uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_generation();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_generation, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_id(uint32_t write_ptr, uint32_t write_len, uint32_t flags);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_id, (uint32_t write_ptr, uint32_t write_len, uint32_t flags),
|
||||
int64_t, otxn_id, (uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_type();
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_type, (),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_slot(uint32_t slot_no);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_slot, (uint32_t slot_no),
|
||||
int64_t, otxn_slot, (uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, otxn_param, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
int64_t, otxn_param, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t meta_slot(uint32_t slot_no);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, meta_slot, (uint32_t slot_no),
|
||||
int64_t, meta_slot, (uint32_t),
|
||||
uint256{})
|
||||
|
||||
// int64_t xpop_slot(uint32_t slot_no_tx, uint32_t slot_no_meta);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, xpop_slot, (uint32_t slot_no_tx, uint32_t slot_no_meta),
|
||||
int64_t, xpop_slot, (uint32_t, uint32_t),
|
||||
featureHooksUpdate1)
|
||||
|
||||
// int64_t prepare(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, prepare, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
int64_t, prepare, (uint32_t, uint32_t, uint32_t, uint32_t),
|
||||
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)
|
||||
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, xport, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
featureExport)
|
||||
// clang-format on
|
||||
@@ -1,66 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_PROTOCOL_APPLOADER_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_APPLOADER_H_INCLUDED
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Validation of an AppLoader document (sfAppLoader), the bootstrap page of
|
||||
a Progressive Web App served from an account.
|
||||
|
||||
Consensus cannot afford a real HTML parser, so this is a shallow sanity
|
||||
gate and nothing more. A document must be non-empty and at most
|
||||
maxAppLoaderLength bytes; well-formed UTF-8; free of C0 control
|
||||
characters other than TAB, LF, FF and CR, and of DEL; begin, after an
|
||||
optional BOM and whitespace, with an HTML doctype or an <html start tag;
|
||||
and contain an <html start tag followed later by a </html> end tag with
|
||||
nothing but whitespace after the last one. Nothing parses comments,
|
||||
scripts or attributes, and content is not policed.
|
||||
|
||||
Consensus critical: a change to what it accepts needs an amendment.
|
||||
*/
|
||||
namespace appLoader {
|
||||
|
||||
enum class Result : std::uint8_t {
|
||||
ok = 0,
|
||||
empty,
|
||||
tooLarge,
|
||||
badUTF8,
|
||||
badControlChar,
|
||||
noDoctype, // does not open with a doctype or <html tag
|
||||
noHtmlElement, // no <html start tag
|
||||
unclosed, // no </html> end tag after it
|
||||
trailingGarbage, // non-whitespace after the last </html>
|
||||
};
|
||||
|
||||
/** Total: every input yields a result. `data` may be null iff size is 0. */
|
||||
Result
|
||||
validate(std::uint8_t const* data, std::size_t size);
|
||||
|
||||
char const*
|
||||
to_string(Result r);
|
||||
|
||||
} // namespace appLoader
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -96,9 +96,6 @@ enum class HashPrefix : std::uint32_t {
|
||||
|
||||
/** Credentials signature */
|
||||
credential = detail::make_hash_prefix('C', 'R', 'D'),
|
||||
|
||||
/** Proof that a validator holds the export key it nominates */
|
||||
exportKeyProof = detail::make_hash_prefix('E', 'K', 'P'),
|
||||
};
|
||||
|
||||
template <class Hasher>
|
||||
|
||||
@@ -118,10 +118,6 @@ negativeUNL() noexcept;
|
||||
Keylet const&
|
||||
UNLReport() noexcept;
|
||||
|
||||
/** The (fixed) index of the featureRNG random number generator object. */
|
||||
Keylet const&
|
||||
random() noexcept;
|
||||
|
||||
/** The beginning of an order book */
|
||||
struct book_t
|
||||
{
|
||||
@@ -332,14 +328,6 @@ xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq);
|
||||
Keylet
|
||||
did(AccountID const& account) noexcept;
|
||||
|
||||
/** An AppLoader belonging to an account.
|
||||
|
||||
There is at most one per account, so the key is derived from the owner's
|
||||
AccountID alone.
|
||||
*/
|
||||
Keylet
|
||||
appLoader(AccountID const& account) noexcept;
|
||||
|
||||
Keylet
|
||||
oracle(AccountID const& account, std::uint32_t const& documentID) noexcept;
|
||||
|
||||
@@ -388,24 +376,6 @@ permissionedDomain(uint256 const& domainID) noexcept;
|
||||
Keylet
|
||||
manifest(PublicKey const& pk) noexcept;
|
||||
|
||||
/** The directory listing every master key's ltMANIFEST object.
|
||||
|
||||
One entry per master key, made when its first manifest is published and
|
||||
kept for good: a newer manifest, a revocation included, replaces the object
|
||||
at the same key and keeps its place. The copy at the ephemeral key is not
|
||||
listed; it is named by the master object.
|
||||
*/
|
||||
Keylet const&
|
||||
manifestDir() noexcept;
|
||||
|
||||
/** A pending export, ordered by creation ledger so TxQ can walk them. */
|
||||
Keylet
|
||||
exportedTxn(std::uint32_t seq, uint256 const& txid) noexcept;
|
||||
|
||||
/** The shadow ticket for an export using `ticketSeq` on the other network. */
|
||||
Keylet
|
||||
shadowTicket(AccountID const& account, std::uint32_t ticketSeq) noexcept;
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
// Everything below is deprecated and should be removed in favor of keylets:
|
||||
@@ -436,7 +406,7 @@ struct keyletDesc
|
||||
|
||||
// This list should include all of the keylet functions that take a single
|
||||
// AccountID parameter.
|
||||
std::array<keyletDesc<AccountID const&>, 7> const directAccountKeylets{
|
||||
std::array<keyletDesc<AccountID const&>, 6> const directAccountKeylets{
|
||||
{{&keylet::account, jss::AccountRoot, false},
|
||||
{&keylet::ownerDir, jss::DirectoryNode, true},
|
||||
{&keylet::signers, jss::SignerList, true},
|
||||
@@ -444,8 +414,7 @@ std::array<keyletDesc<AccountID const&>, 7> const directAccountKeylets{
|
||||
// test it anyway, since the invariant checks for it.
|
||||
{&keylet::nftpage_min, jss::NFTokenPage, true},
|
||||
{&keylet::nftpage_max, jss::NFTokenPage, true},
|
||||
{&keylet::did, jss::DID, true},
|
||||
{&keylet::appLoader, jss::AppLoader, true}}};
|
||||
{&keylet::did, jss::DID, true}}};
|
||||
|
||||
MPTID
|
||||
makeMptID(std::uint32_t sequence, AccountID const& account);
|
||||
|
||||
@@ -96,25 +96,6 @@ std::size_t constexpr maxDIDAttestationLength = 256;
|
||||
/** The maximum length of a domain */
|
||||
std::size_t constexpr maxDomainLength = 256;
|
||||
|
||||
/** The maximum length of an AppLoader document.
|
||||
|
||||
The AppLoader holds the bootstrap document of a Progressive Web App
|
||||
served from an account. It is deliberately capped: the blob lives in a
|
||||
ledger object that carries no owner reserve at all, so the only things
|
||||
standing between it and unbounded ledger growth are this limit and the
|
||||
per-byte transaction fee below.
|
||||
*/
|
||||
std::size_t constexpr maxAppLoaderLength = 4096;
|
||||
|
||||
/** Drops of transaction fee charged per byte of AppLoader document.
|
||||
|
||||
Storage is paid for through the fee rather than through an owner
|
||||
reserve, so that a loader can be published by an account sitting at its
|
||||
reserve floor. The fee is charged on every AccountSet that carries the
|
||||
field, including overwrites.
|
||||
*/
|
||||
std::uint64_t constexpr appLoaderFeeDropsPerByte = 1;
|
||||
|
||||
/** The maximum length of a URI inside a Credential */
|
||||
std::size_t constexpr maxCredentialURILength = 256;
|
||||
|
||||
|
||||
@@ -197,7 +197,6 @@ enum TEFcodes : TERUnderlyingType {
|
||||
tefINVALID_LEDGER_FIX_TYPE,
|
||||
tefPAST_MANIFEST_SEQ,
|
||||
tefREVOKED_MANIFEST,
|
||||
tefMANIFEST_KEY_IN_USE,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -242,11 +242,8 @@ constexpr std::uint32_t const tfClawbackMask = ~tfUniversal;
|
||||
// CronSet Flags:
|
||||
enum CronSetFlags : uint32_t {
|
||||
tfCronUnset = 0x00000001,
|
||||
// featureExport: fire after each export key rotation (see Export.h)
|
||||
tfCronExportRotation = 0x00000002,
|
||||
};
|
||||
constexpr std::uint32_t const tfCronSetMask =
|
||||
~(tfUniversal | tfCronUnset | tfCronExportRotation);
|
||||
constexpr std::uint32_t const tfCronSetMask = ~(tfUniversal | tfCronUnset);
|
||||
|
||||
// AMM Flags:
|
||||
constexpr std::uint32_t tfLPToken = 0x00010000;
|
||||
|
||||
@@ -152,26 +152,6 @@ public:
|
||||
return static_cast<bool>(mHookEmissions);
|
||||
}
|
||||
|
||||
/** featureRNG: the RNG state this transaction's first Hook draw saw.
|
||||
Every value the transaction's Hooks drew is derived from it. */
|
||||
void
|
||||
setRandomData(uint256 const& randomData)
|
||||
{
|
||||
mRandomData = randomData;
|
||||
}
|
||||
|
||||
bool
|
||||
hasRandomData() const
|
||||
{
|
||||
return static_cast<bool>(mRandomData);
|
||||
}
|
||||
|
||||
uint256 const&
|
||||
getRandomData() const
|
||||
{
|
||||
return *mRandomData;
|
||||
}
|
||||
|
||||
STAmount
|
||||
getDeliveredAmount() const
|
||||
{
|
||||
@@ -196,7 +176,6 @@ private:
|
||||
std::optional<STAmount> mDelivered;
|
||||
std::optional<STArray> mHookExecutions;
|
||||
std::optional<STArray> mHookEmissions;
|
||||
std::optional<uint256> mRandomData;
|
||||
|
||||
STArray mNodes;
|
||||
};
|
||||
|
||||
@@ -34,12 +34,9 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FEATURE(Export, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(RNG, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(PWALoader, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (20261005, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FEATURE(EscrowDestinationCancel, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (20260929, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (20260929, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(HookOnV2_1, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(OnChainManifests, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
|
||||
@@ -91,6 +88,7 @@ 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)
|
||||
@@ -166,7 +164,6 @@ 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)
|
||||
|
||||
@@ -109,44 +109,6 @@ LEDGER_ENTRY(ltHOOK_DEFINITION, 'D', HookDefinition, hook_definition, ({
|
||||
{sfPreviousTxnLgrSeq, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** A pending export: the transaction being multisigned for another network,
|
||||
gathering UNL signatures in its sfSigners until ttEXPORT_FINAL deletes it,
|
||||
with the exporter's signer list there when it was created. sfOwner is the
|
||||
exporting account, so the signatures and the result are attributed to it
|
||||
in metadata. sfEmitHookHash is the hook to call back if it closes below
|
||||
quorum.
|
||||
|
||||
\sa keylet::exportedTxn
|
||||
*/
|
||||
LEDGER_ENTRY(ltEXPORTED_TXN, 'X', ExportedTxn, exported_txn, ({
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfExportedTxn, soeREQUIRED},
|
||||
{sfTransactionHash, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfSignerEntries, soeREQUIRED},
|
||||
{sfSignerQuorum, soeREQUIRED},
|
||||
{sfEmitHookHash, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** A shadow ticket: lets one XPOP of a ticketed export that is bound back to
|
||||
this network (OperationLimit == NETWORK_ID) be imported, which consumes it.
|
||||
Only ttEXPORT creates these. They are not hook state because a hook can
|
||||
write any namespace of its own account's state, so it could forge one and
|
||||
import the same XPOP again and again. sfEmitHookHash is the hook whose
|
||||
cbak the Import calls.
|
||||
|
||||
\sa keylet::shadowTicket
|
||||
*/
|
||||
LEDGER_ENTRY(ltSHADOW_TICKET, 'Y', ShadowTicket, shadow_ticket, ({
|
||||
{sfAccount, soeREQUIRED},
|
||||
{sfTicketSequence, soeREQUIRED},
|
||||
{sfTransactionHash, soeREQUIRED},
|
||||
{sfEmitHookHash, soeOPTIONAL},
|
||||
{sfOwnerNode, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** A ledger object containing a hook-emitted transaction from a previous hook execution.
|
||||
|
||||
\sa keylet::emitted
|
||||
@@ -180,22 +142,6 @@ LEDGER_ENTRY(ltIMPORT_VLSEQ, 0x0049, ImportVLSequence, import_vlseq, ({
|
||||
{sfPreviousTxnLgrSeq, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** The bootstrap document of a Progressive Web App (featurePWALoader).
|
||||
|
||||
At most one per account, keyed on the owner alone (keylet::appLoader).
|
||||
It is in no owner directory and takes no reserve: its storage is paid
|
||||
for by a per-byte fee on the AccountSet that writes it, and
|
||||
DeleteAccount erases it explicitly. AppLoaderSequence equals the owner's
|
||||
AccountRoot AppLoaderSequence as of the AccountSet that wrote it.
|
||||
*/
|
||||
LEDGER_ENTRY(ltAPP_LOADER, 0x004c, AppLoader, app_loader, ({
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfAppLoader, soeREQUIRED},
|
||||
{sfAppLoaderSequence, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** The ledger object which tracks the current negative UNL state.
|
||||
|
||||
\note This is a singleton: only one such object exists in the ledger.
|
||||
@@ -230,32 +176,6 @@ LEDGER_ENTRY(ltNFTOKEN_PAGE, 0x0050, NFTokenPage, nft_page, ({
|
||||
LEDGER_ENTRY_DUPLICATE(ltUNL_REPORT, 0x0052, UNLReport, unl_report, ({
|
||||
{sfImportVLKeys, soeOPTIONAL},
|
||||
{sfActiveValidators, soeOPTIONAL},
|
||||
/* featureExport: the ledger of the last ActiveValidator/ImportVLKey
|
||||
report, which decides when the arrays above are reset. Export key
|
||||
reports also modify this object, so PreviousTxnLgrSeq no longer can. */
|
||||
{sfLedgerSequence, soeOPTIONAL},
|
||||
/* featureExport: bumped whenever a validator's export key changes. */
|
||||
{sfExportKeysSeq, soeOPTIONAL},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{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},
|
||||
}))
|
||||
@@ -342,13 +262,7 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
|
||||
{sfHookStateScale, soeOPTIONAL},
|
||||
{sfCron, soeOPTIONAL},
|
||||
{sfAMMID, soeOPTIONAL},
|
||||
{sfAppLoaderSequence, soeOPTIONAL},
|
||||
{sfManifestID, soeOPTIONAL},
|
||||
/* featureExport: a validator's export keys, newest first */
|
||||
{sfExportKeys, soeOPTIONAL},
|
||||
/* featureExport: the signer list this account holds on the network it
|
||||
exports to, as last proven by an Import of its SignerListSet */
|
||||
{sfExportSignerList, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** A ledger object which contains a list of object identifiers.
|
||||
@@ -695,7 +609,6 @@ LEDGER_ENTRY(ltMANIFEST, 0x004D, Manifest, manifest_entry, ({
|
||||
{sfMasterSignature, soeREQUIRED},
|
||||
{sfSignature, soeOPTIONAL}, /* absent if the master is revoked */
|
||||
{sfManifestID, soeOPTIONAL}, /* pointer to the identical other copy on the other key */
|
||||
{sfOwnerNode, soeOPTIONAL}, /* master copy only: its page in keylet::manifestDir() */
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
@@ -59,8 +59,6 @@ TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19)
|
||||
TYPED_SFIELD(sfHookApiVersion, UINT16, 20)
|
||||
TYPED_SFIELD(sfHookStateScale, UINT16, 21)
|
||||
TYPED_SFIELD(sfLedgerFixType, UINT16, 22)
|
||||
TYPED_SFIELD(sfEntropyCount, UINT16, 99)
|
||||
TYPED_SFIELD(sfEntropyDenominator, UINT16, 100)
|
||||
|
||||
// 32-bit integers (common)
|
||||
TYPED_SFIELD(sfNetworkID, UINT32, 1)
|
||||
@@ -117,7 +115,6 @@ TYPED_SFIELD(sfLockCount, UINT32, 49)
|
||||
TYPED_SFIELD(sfFirstNFTokenSequence, UINT32, 50)
|
||||
TYPED_SFIELD(sfOracleDocumentID, UINT32, 51)
|
||||
|
||||
TYPED_SFIELD(sfExportKeysSeq, UINT32, 92)
|
||||
TYPED_SFIELD(sfStartTime, UINT32, 93)
|
||||
TYPED_SFIELD(sfRepeatCount, UINT32, 94)
|
||||
TYPED_SFIELD(sfDelaySeconds, UINT32, 95)
|
||||
@@ -126,7 +123,6 @@ TYPED_SFIELD(sfImportSequence, UINT32, 97)
|
||||
TYPED_SFIELD(sfRewardTime, UINT32, 98)
|
||||
TYPED_SFIELD(sfRewardLgrFirst, UINT32, 99)
|
||||
TYPED_SFIELD(sfRewardLgrLast, UINT32, 100)
|
||||
TYPED_SFIELD(sfAppLoaderSequence, UINT32, 101)
|
||||
|
||||
// 64-bit integers (common)
|
||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
||||
@@ -214,9 +210,6 @@ TYPED_SFIELD(sfOfferID, UINT256, 34)
|
||||
TYPED_SFIELD(sfEscrowID, UINT256, 35)
|
||||
TYPED_SFIELD(sfURITokenID, UINT256, 36)
|
||||
TYPED_SFIELD(sfDomainID, UINT256, 37)
|
||||
TYPED_SFIELD(sfNextRandomDigest, UINT256, 89)
|
||||
// RNG running state: never written to metadata (see featureRNG)
|
||||
TYPED_SFIELD(sfRandomData, UINT256, 90, SField::sMD_Never)
|
||||
TYPED_SFIELD(sfManifestID, UINT256, 91)
|
||||
TYPED_SFIELD(sfHookOnOutgoing, UINT256, 93)
|
||||
TYPED_SFIELD(sfHookOnIncoming, UINT256, 94)
|
||||
@@ -301,9 +294,6 @@ TYPED_SFIELD(sfAssetClass, VL, 29)
|
||||
TYPED_SFIELD(sfProvider, VL, 30)
|
||||
TYPED_SFIELD(sfMPTokenMetadata, VL, 31)
|
||||
TYPED_SFIELD(sfCredentialType, VL, 32)
|
||||
TYPED_SFIELD(sfExportKey, VL, 94)
|
||||
TYPED_SFIELD(sfExportKeyProof, VL, 95)
|
||||
TYPED_SFIELD(sfAppLoader, VL, 96)
|
||||
TYPED_SFIELD(sfHookName, VL, 97)
|
||||
TYPED_SFIELD(sfRemarkValue, VL, 98)
|
||||
TYPED_SFIELD(sfRemarkName, VL, 99)
|
||||
@@ -393,11 +383,6 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
|
||||
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
|
||||
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
|
||||
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
|
||||
UNTYPED_SFIELD(sfExportKeyEntry, OBJECT, 85)
|
||||
UNTYPED_SFIELD(sfExportSignerList, OBJECT, 86)
|
||||
UNTYPED_SFIELD(sfExportKeyReport, OBJECT, 87)
|
||||
UNTYPED_SFIELD(sfRandomDigestEntry, OBJECT, 88)
|
||||
UNTYPED_SFIELD(sfExportedTxn, OBJECT, 89, SField::sMD_Create|SField::sMD_DeleteFinal)
|
||||
UNTYPED_SFIELD(sfManifest, OBJECT, 90)
|
||||
UNTYPED_SFIELD(sfAmountEntry, OBJECT, 91)
|
||||
UNTYPED_SFIELD(sfMintURIToken, OBJECT, 92)
|
||||
@@ -438,9 +423,6 @@ UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25)
|
||||
UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26)
|
||||
UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27)
|
||||
UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28)
|
||||
UNTYPED_SFIELD(sfExportKeys, ARRAY, 90)
|
||||
// RNG validator commitments: never written to metadata (see featureRNG)
|
||||
UNTYPED_SFIELD(sfRandomDigests, ARRAY, 91, SField::sMD_Never)
|
||||
UNTYPED_SFIELD(sfAmounts, ARRAY, 92)
|
||||
UNTYPED_SFIELD(sfHookEmissions, ARRAY, 93)
|
||||
UNTYPED_SFIELD(sfImportVLKeys, ARRAY, 94)
|
||||
|
||||
@@ -74,7 +74,6 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, ({
|
||||
{sfTickSize, soeOPTIONAL},
|
||||
{sfNFTokenMinter, soeOPTIONAL},
|
||||
{sfHookStateScale, soeOPTIONAL},
|
||||
{sfAppLoader, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction type cancels an existing escrow. */
|
||||
@@ -501,44 +500,6 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
|
||||
{sfDomainID, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** Emitted by a hook (see xport()): have the export keys in the account's
|
||||
sfExportSignerList multisign sfExportedTxn, a transaction for another
|
||||
network from the hook's account. See Export.h. */
|
||||
TRANSACTION(ttEXPORT, 88, Export, ({
|
||||
{sfExportedTxn, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** A UNL validator's multisignature over a pending export. Deliberately not a
|
||||
pseudo-txn: disputed pseudo-txns are discarded, these are retried. */
|
||||
TRANSACTION(ttEXPORT_SIGN, 87, ExportSign, ({
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfTransactionHash, soeREQUIRED},
|
||||
{sfSigner, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** A pseudo-txn closing an export's signing window. */
|
||||
TRANSACTION(ttEXPORT_FINAL, 90, ExportFinal, ({
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfTransactionHash, 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, ({
|
||||
@@ -650,5 +611,4 @@ TRANSACTION(ttUNL_REPORT, 104, UNLReport, ({
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfActiveValidator, soeOPTIONAL},
|
||||
{sfImportVLKey, soeOPTIONAL},
|
||||
{sfExportKeyReport, soeOPTIONAL},
|
||||
}))
|
||||
@@ -47,7 +47,6 @@ JSS(AL_hit_rate); // out: GetCounts
|
||||
JSS(AcceptedCredentials); // out: AccountObjects
|
||||
JSS(Account); // in: TransactionSign; field.
|
||||
JSS(AMMID); // field
|
||||
JSS(API); // out: RPC server_definitions (HOOK)
|
||||
JSS(Amount); // in: TransactionSign; field.
|
||||
JSS(Authorize); // field
|
||||
JSS(Amount2); // in/out: AMM IOU/XRP pool, deposit, withdraw amount
|
||||
@@ -69,9 +68,6 @@ JSS(Destination); // in: TransactionSign; field.
|
||||
JSS(EPrice); // in: AMM Deposit option
|
||||
JSS(Fee); // in/out: TransactionSign; field.
|
||||
JSS(FIELDS); // out: RPC server_definitions
|
||||
JSS(HOOK); // out: RPC server_definitions
|
||||
JSS(KEYLET_TYPES); // out: RPC server_definitions (HOOK)
|
||||
JSS(RETURN_CODES); // out: RPC server_definitions (HOOK)
|
||||
JSS(Flags); // in/out: TransactionSign; field.
|
||||
JSS(GenesisMints);
|
||||
JSS(GovernanceMarks);
|
||||
@@ -238,7 +234,7 @@ JSS(closed); // out: NetworkOPs, LedgerToJson,
|
||||
// handlers/Ledger
|
||||
JSS(closed_ledger); // out: NetworkOPs
|
||||
JSS(cluster); // out: PeerImp
|
||||
JSS(code); // in: HookValidate; out: errors
|
||||
JSS(code); // out: errors
|
||||
JSS(command); // in: RPCHandler
|
||||
JSS(complete); // out: NetworkOPs, InboundLedger
|
||||
JSS(complete_ledgers); // out: NetworkOPs, PeerImp
|
||||
@@ -378,8 +374,6 @@ JSS(index); // in: LedgerEntry,
|
||||
// LedgerEntry, TxHistory, LedgerData
|
||||
JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo
|
||||
JSS(initial_sync_duration_us);
|
||||
JSS(instruction_count_cbak); // out: HookValidate
|
||||
JSS(instruction_count_hook); // out: HookValidate
|
||||
JSS(internal_command); // in: Internal
|
||||
JSS(invalid_API_version); // out: Many, when a request has an invalid
|
||||
// version
|
||||
@@ -453,7 +447,6 @@ JSS(local); // out: resource/Logic.h
|
||||
JSS(local_txs); // out: GetCounts
|
||||
JSS(local_static_keys); // out: ValidatorList
|
||||
JSS(locked_balance); // out: AccountLines
|
||||
JSS(log); // out: HookValidate
|
||||
JSS(low); // out: BookChanges
|
||||
JSS(lock_count); // out: AccountLines
|
||||
JSS(lowest_sequence); // out: AccountInfo
|
||||
@@ -592,6 +585,7 @@ JSS(queue_data); // out: AccountInfo
|
||||
JSS(queued); // out: SubmitTransaction
|
||||
JSS(queued_duration_us);
|
||||
JSS(quote_asset); // in: get_aggregate_price
|
||||
JSS(random); // out: Random
|
||||
JSS(raw_meta); // out: AcceptedLedgerTx
|
||||
JSS(receive_currencies); // out: AccountCurrencies
|
||||
JSS(reference_level); // out: TxQ
|
||||
@@ -609,7 +603,6 @@ JSS(reserve_base_native); // out: NetworkOPs
|
||||
JSS(reserve_inc); // out: NetworkOPs
|
||||
JSS(reserve_inc_xrp); // out: NetworkOPs
|
||||
JSS(reserve_inc_native); // out: NetworkOPs
|
||||
JSS(reset); // in/out: PWAInfo
|
||||
JSS(response); // websocket
|
||||
JSS(result); // RPC
|
||||
JSS(ripple_lines); // out: NetworkOPs
|
||||
@@ -751,7 +744,6 @@ JSS(url_password); // in: Subscribe
|
||||
JSS(url_username); // in: Subscribe
|
||||
JSS(urlgravatar); //
|
||||
JSS(username); // in: Subscribe
|
||||
JSS(valid); // out: HookValidate
|
||||
JSS(validated); // out: NetworkOPs, RPCHelpers, AccountTx*
|
||||
// Tx
|
||||
JSS(validator_list_expires); // out: NetworkOps, ValidatorList
|
||||
@@ -773,7 +765,6 @@ JSS(validator_sites); // out: ValidatorSites
|
||||
JSS(value); // out: STAmount
|
||||
JSS(version); // out: RPCVersion
|
||||
JSS(vetoed); // out: AmendmentTableImpl
|
||||
JSS(vm_error); // out: HookValidate
|
||||
JSS(volume_a); // out: BookChanges
|
||||
JSS(volume_b); // out: BookChanges
|
||||
JSS(vote); // in: Feature
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
|
||||
#include <xrpl/basics/BasicConfig.h>
|
||||
#include <xrpl/beast/net/IPEndpoint.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/network_v4.hpp>
|
||||
#include <boost/asio/ip/network_v6.hpp>
|
||||
@@ -58,12 +57,6 @@ struct Port
|
||||
std::vector<boost::asio::ip::network_v6> admin_nets_v6;
|
||||
std::vector<boost::asio::ip::network_v4> secure_gateway_nets_v4;
|
||||
std::vector<boost::asio::ip::network_v6> secure_gateway_nets_v6;
|
||||
// "pwa" ports only; see the "pwa ports" section of xahaud-example.cfg.
|
||||
std::set<std::string> pwa_domains; // zones, lower case, no trailing dot
|
||||
std::set<AccountID> pwa_allowlist; // if non-empty, only these are served
|
||||
std::set<AccountID> pwa_blocklist; // never served; wins over allowlist
|
||||
bool pwa_custom_domains = false; // serve hosts named by TXT records
|
||||
std::optional<beast::IP::Endpoint> pwa_resolver; // for those; else system
|
||||
std::string user;
|
||||
std::string password;
|
||||
std::string admin_user;
|
||||
@@ -100,18 +93,8 @@ 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&
|
||||
@@ -143,9 +126,6 @@ struct ParsedPort
|
||||
std::vector<boost::asio::ip::network_v6> admin_nets_v6;
|
||||
std::vector<boost::asio::ip::network_v4> secure_gateway_nets_v4;
|
||||
std::vector<boost::asio::ip::network_v6> secure_gateway_nets_v6;
|
||||
std::set<std::string> pwa_domains;
|
||||
bool pwa_custom_domains = false;
|
||||
std::optional<beast::IP::Endpoint> pwa_resolver;
|
||||
};
|
||||
|
||||
void
|
||||
|
||||
@@ -298,7 +298,7 @@ Door<Handler>::Door(
|
||||
port_.protocol.count("peer") > 0)
|
||||
, plain_(
|
||||
port_.protocol.count("http") > 0 || port_.protocol.count("ws") > 0 ||
|
||||
port_.protocol.count("ws2") || port_.protocol.count("pwa"))
|
||||
port_.protocol.count("ws2"))
|
||||
{
|
||||
reOpen();
|
||||
}
|
||||
|
||||
@@ -208,16 +208,6 @@ 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,9 +78,10 @@ public:
|
||||
error_code ec;
|
||||
|
||||
// Create UDP endpoint from port configuration
|
||||
boost::asio::ip::udp::endpoint udp_endpoint(port_.ip, port_.port);
|
||||
auto const addr = port_.ip.to_v4();
|
||||
boost::asio::ip::udp::endpoint udp_endpoint(addr, port_.port);
|
||||
|
||||
socket_.open(udp_endpoint.protocol(), ec);
|
||||
socket_.open(boost::asio::ip::udp::v4(), ec);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(j_.error()) << "UDP socket open failed: " << ec.message();
|
||||
@@ -93,22 +94,17 @@ public:
|
||||
{
|
||||
JLOG(j_.error())
|
||||
<< "UDP set reuse_address failed: " << ec.message();
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
socket_.bind(udp_endpoint, ec);
|
||||
if (ec)
|
||||
{
|
||||
// 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();
|
||||
JLOG(j_.error()) << "UDP socket bind failed: " << ec.message();
|
||||
return;
|
||||
}
|
||||
|
||||
JLOG(j_.info()) << "UDP listening on " << udp_endpoint;
|
||||
JLOG(j_.info()) << "UDP-RPC listening on " << udp_endpoint;
|
||||
}
|
||||
|
||||
endpoint_type
|
||||
@@ -173,7 +169,6 @@ 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);
|
||||
});
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/UTF8.h>
|
||||
#include <xrpl/protocol/AppLoader.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace ripple {
|
||||
namespace appLoader {
|
||||
|
||||
namespace {
|
||||
|
||||
// HTML "ASCII whitespace": TAB, LF, FF, CR, SPACE.
|
||||
inline bool
|
||||
isSpace(std::uint8_t c) noexcept
|
||||
{
|
||||
return c == 0x09 || c == 0x0A || c == 0x0C || c == 0x0D || c == 0x20;
|
||||
}
|
||||
|
||||
inline std::uint8_t
|
||||
lower(std::uint8_t c) noexcept
|
||||
{
|
||||
return (c >= 'A' && c <= 'Z') ? static_cast<std::uint8_t>(c + 32) : c;
|
||||
}
|
||||
|
||||
// Case-insensitive match of lower-case `lit` at `pos`; never reads past
|
||||
// the end.
|
||||
bool
|
||||
matchAt(
|
||||
std::uint8_t const* data,
|
||||
std::size_t size,
|
||||
std::size_t pos,
|
||||
std::string_view lit) noexcept
|
||||
{
|
||||
if (pos > size || size - pos < lit.size())
|
||||
return false;
|
||||
for (std::size_t i = 0; i < lit.size(); ++i)
|
||||
if (lower(data[pos + i]) != static_cast<std::uint8_t>(lit[i]))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
findFrom(
|
||||
std::uint8_t const* data,
|
||||
std::size_t size,
|
||||
std::size_t from,
|
||||
std::string_view lit) noexcept
|
||||
{
|
||||
for (std::size_t i = from; i + lit.size() <= size; ++i)
|
||||
if (matchAt(data, size, i, lit))
|
||||
return i;
|
||||
return size;
|
||||
}
|
||||
|
||||
// So that "<htmlish>" is not mistaken for "<html>".
|
||||
inline bool
|
||||
isTagNameEnd(std::uint8_t c) noexcept
|
||||
{
|
||||
return isSpace(c) || c == '>' || c == '/';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result
|
||||
validate(std::uint8_t const* data, std::size_t size)
|
||||
{
|
||||
if (size == 0 || data == nullptr)
|
||||
return Result::empty;
|
||||
if (size > maxAppLoaderLength)
|
||||
return Result::tooLarge;
|
||||
if (!isValidUTF8(Slice(data, size)))
|
||||
return Result::badUTF8;
|
||||
for (std::size_t i = 0; i < size; ++i)
|
||||
if (!isSpace(data[i]) && (data[i] < 0x20 || data[i] == 0x7F))
|
||||
return Result::badControlChar;
|
||||
|
||||
std::size_t begin = 0;
|
||||
if (size >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF)
|
||||
begin = 3;
|
||||
while (begin < size && isSpace(data[begin]))
|
||||
++begin;
|
||||
if (begin == size)
|
||||
return Result::empty;
|
||||
|
||||
// "<!doctype" followed by whitespace is the only doctype form any
|
||||
// browser treats as standards-mode HTML.
|
||||
bool const doctype = matchAt(data, size, begin, "<!doctype") &&
|
||||
begin + 9 < size && isSpace(data[begin + 9]);
|
||||
bool const html = matchAt(data, size, begin, "<html") && begin + 5 < size &&
|
||||
isTagNameEnd(data[begin + 5]);
|
||||
if (!doctype && !html)
|
||||
return Result::noDoctype;
|
||||
|
||||
std::size_t open = begin;
|
||||
for (;;)
|
||||
{
|
||||
open = findFrom(data, size, open, "<html");
|
||||
if (open == size)
|
||||
return Result::noHtmlElement;
|
||||
if (open + 5 < size && isTagNameEnd(data[open + 5]))
|
||||
break;
|
||||
++open;
|
||||
}
|
||||
|
||||
// The LAST well-formed </html>, so that one quoted inside a script
|
||||
// does not shadow the real one. Linear: each probe resumes one past
|
||||
// the previous match.
|
||||
// closeEnd == 0 means none found: a well-formed </html> always ends
|
||||
// past position 0.
|
||||
std::size_t closeEnd = 0;
|
||||
for (std::size_t probe = open;;)
|
||||
{
|
||||
std::size_t const found = findFrom(data, size, probe, "</html");
|
||||
if (found == size)
|
||||
break;
|
||||
std::size_t j = found + 6;
|
||||
while (j < size && isSpace(data[j]))
|
||||
++j;
|
||||
if (j < size && data[j] == '>')
|
||||
closeEnd = j + 1;
|
||||
probe = found + 1;
|
||||
}
|
||||
if (closeEnd == 0)
|
||||
return Result::unclosed;
|
||||
|
||||
for (std::size_t i = closeEnd; i < size; ++i)
|
||||
if (!isSpace(data[i]))
|
||||
return Result::trailingGarbage;
|
||||
return Result::ok;
|
||||
}
|
||||
|
||||
char const*
|
||||
to_string(Result r)
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
case Result::ok:
|
||||
return "ok";
|
||||
case Result::empty:
|
||||
return "empty document";
|
||||
case Result::tooLarge:
|
||||
return "document exceeds maxAppLoaderLength";
|
||||
case Result::badUTF8:
|
||||
return "malformed UTF-8";
|
||||
case Result::badControlChar:
|
||||
return "forbidden control character";
|
||||
case Result::noDoctype:
|
||||
return "does not open with a doctype or <html> tag";
|
||||
case Result::noHtmlElement:
|
||||
return "missing <html> start tag";
|
||||
case Result::unclosed:
|
||||
return "missing or misplaced </html> end tag";
|
||||
case Result::trailingGarbage:
|
||||
return "content after </html>";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
} // namespace appLoader
|
||||
} // namespace ripple
|
||||
@@ -81,7 +81,6 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
IMPORT_VLSEQ = 'I',
|
||||
UNL_REPORT = 'R',
|
||||
CRON = 'L',
|
||||
APP_LOADER = 'W',
|
||||
AMM = 'A',
|
||||
BRIDGE = LEDGER_NAMESPACE2(0x01, 'H'),
|
||||
XCHAIN_CLAIM_ID = 'Q',
|
||||
@@ -91,12 +90,8 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
MPTOKEN_ISSUANCE = '~',
|
||||
MPTOKEN = 't',
|
||||
MANIFEST = 'M',
|
||||
MANIFEST_DIR = LEDGER_NAMESPACE2(0x01, 'M'),
|
||||
CREDENTIAL = LEDGER_NAMESPACE2(0x01, 'D'),
|
||||
PERMISSIONED_DOMAIN = 'm',
|
||||
RANDOM = 0x526E, // Rn
|
||||
EXPORTED_TXN = 'X',
|
||||
SHADOW_TICKET = 'Y',
|
||||
|
||||
// No longer used or supported. Left here to reserve the space
|
||||
// to avoid accidental reuse.
|
||||
@@ -281,13 +276,6 @@ UNLReport() noexcept
|
||||
return ret;
|
||||
}
|
||||
|
||||
Keylet const&
|
||||
random() noexcept
|
||||
{
|
||||
static Keylet const ret{ltRANDOM, indexHash(LedgerNameSpace::RANDOM)};
|
||||
return ret;
|
||||
}
|
||||
|
||||
Keylet
|
||||
book_t::operator()(Book const& b) const
|
||||
{
|
||||
@@ -625,12 +613,6 @@ did(AccountID const& account) noexcept
|
||||
return {ltDID, indexHash(LedgerNameSpace::DID, account)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
appLoader(AccountID const& account) noexcept
|
||||
{
|
||||
return {ltAPP_LOADER, indexHash(LedgerNameSpace::APP_LOADER, account)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
oracle(AccountID const& account, std::uint32_t const& documentID) noexcept
|
||||
{
|
||||
@@ -695,35 +677,6 @@ manifest(PublicKey const& pk) noexcept
|
||||
return {ltMANIFEST, indexHash(LedgerNameSpace::MANIFEST, pk.slice())};
|
||||
}
|
||||
|
||||
Keylet const&
|
||||
manifestDir() noexcept
|
||||
{
|
||||
static Keylet const ret{
|
||||
ltDIR_NODE, indexHash(LedgerNameSpace::MANIFEST_DIR)};
|
||||
return ret;
|
||||
}
|
||||
|
||||
Keylet
|
||||
exportedTxn(std::uint32_t seq, uint256 const& txid) noexcept
|
||||
{
|
||||
// like cron: 8 bytes of namespace, 4 bytes BE ledger seq, 20 of the txid
|
||||
static uint256 const ns = indexHash(LedgerNameSpace::EXPORTED_TXN);
|
||||
uint256 k;
|
||||
std::memcpy(k.data(), ns.data(), 8);
|
||||
for (int i = 0; i < 4; ++i)
|
||||
k.data()[8 + i] = static_cast<std::uint8_t>(seq >> (24 - 8 * i));
|
||||
std::memcpy(k.data() + 12, txid.data(), 20);
|
||||
return {ltEXPORTED_TXN, k};
|
||||
}
|
||||
|
||||
Keylet
|
||||
shadowTicket(AccountID const& account, std::uint32_t ticketSeq) noexcept
|
||||
{
|
||||
return {
|
||||
ltSHADOW_TICKET,
|
||||
indexHash(LedgerNameSpace::SHADOW_TICKET, account, ticketSeq)};
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace ripple
|
||||
@@ -143,42 +143,6 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfAccount, soeOPTIONAL},
|
||||
});
|
||||
|
||||
add(sfRandomDigestEntry.jsonName,
|
||||
sfRandomDigestEntry.getCode(),
|
||||
{
|
||||
{sfPublicKey, soeREQUIRED},
|
||||
{sfNextRandomDigest, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
});
|
||||
|
||||
// featureExport: one validator's export key, as reported by the UNL
|
||||
add(sfExportKeyReport.jsonName,
|
||||
sfExportKeyReport.getCode(),
|
||||
{
|
||||
{sfPublicKey, soeREQUIRED},
|
||||
{sfExportKey, soeREQUIRED},
|
||||
{sfExportKeyProof, soeREQUIRED},
|
||||
});
|
||||
|
||||
// featureExport: an account's signer list on the network it exports to,
|
||||
// and the (ledger, index) there of the SignerListSet that set it
|
||||
add(sfExportSignerList.jsonName,
|
||||
sfExportSignerList.getCode(),
|
||||
{
|
||||
{sfSignerQuorum, soeREQUIRED},
|
||||
{sfSignerEntries, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfTransactionIndex, soeREQUIRED},
|
||||
});
|
||||
|
||||
// featureExport: an entry in a validator account's sfExportKeys
|
||||
add(sfExportKeyEntry.jsonName,
|
||||
sfExportKeyEntry.getCode(),
|
||||
{
|
||||
{sfExportKey, soeREQUIRED},
|
||||
{sfCloseTime, soeREQUIRED},
|
||||
});
|
||||
|
||||
add(sfAmountEntry.jsonName,
|
||||
sfAmountEntry.getCode(),
|
||||
{
|
||||
|
||||
@@ -684,8 +684,7 @@ isPseudoTx(STObject const& tx)
|
||||
|
||||
auto tt = safe_cast<TxType>(*t);
|
||||
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY ||
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON ||
|
||||
tt == ttENTROPY || tt == ttEXPORT_FINAL;
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -66,9 +66,6 @@ STValidation::validationFormat()
|
||||
{sfBaseFeeDrops, soeOPTIONAL},
|
||||
{sfReserveBaseDrops, soeOPTIONAL},
|
||||
{sfReserveIncrementDrops, soeOPTIONAL},
|
||||
// featureExport: sent on flag ledger validations only
|
||||
{sfExportKey, soeOPTIONAL},
|
||||
{sfExportKeyProof, soeOPTIONAL},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
|
||||
@@ -153,7 +153,6 @@ transResults()
|
||||
MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE, "The LedgerFixType field has an invalid value."),
|
||||
MAKE_ERROR(tefPAST_MANIFEST_SEQ, "The submitted manifest's sequence is not newer than the current."),
|
||||
MAKE_ERROR(tefREVOKED_MANIFEST, "The submitted manifest is for a revoked master key."),
|
||||
MAKE_ERROR(tefMANIFEST_KEY_IN_USE, "A key the submitted manifest names is already in use by another manifest."),
|
||||
|
||||
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
|
||||
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
|
||||
|
||||
@@ -49,9 +49,6 @@ TxMeta::TxMeta(
|
||||
|
||||
if (obj.isFieldPresent(sfHookEmissions))
|
||||
setHookEmissions(obj.getFieldArray(sfHookEmissions));
|
||||
|
||||
if (obj.isFieldPresent(sfRandomData))
|
||||
setRandomData(obj.getFieldH256(sfRandomData));
|
||||
}
|
||||
|
||||
TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj)
|
||||
@@ -78,9 +75,6 @@ TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj)
|
||||
|
||||
if (obj.isFieldPresent(sfHookEmissions))
|
||||
setHookEmissions(obj.getFieldArray(sfHookEmissions));
|
||||
|
||||
if (obj.isFieldPresent(sfRandomData))
|
||||
setRandomData(obj.getFieldH256(sfRandomData));
|
||||
}
|
||||
|
||||
TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, Blob const& vec)
|
||||
@@ -251,9 +245,6 @@ TxMeta::getAsObject() const
|
||||
if (hasHookEmissions())
|
||||
metaData.setFieldArray(sfHookEmissions, getHookEmissions());
|
||||
|
||||
if (hasRandomData())
|
||||
metaData.setFieldH256(sfRandomData, getRandomData());
|
||||
|
||||
return metaData;
|
||||
}
|
||||
|
||||
|
||||
@@ -545,7 +545,7 @@ b58_to_b256_be(std::string_view input, std::span<std::uint8_t> out)
|
||||
XRPL_ASSERT(
|
||||
num_b_58_10_coeffs <= b_58_10_coeff.size(),
|
||||
"ripple::b58_fast::detail::b58_to_b256_be : maximum coeff");
|
||||
for (unsigned char const c : input.substr(0, partial_coeff_len))
|
||||
for (auto c : input.substr(0, partial_coeff_len))
|
||||
{
|
||||
auto cur_val = ::ripple::alphabetReverse[c];
|
||||
if (cur_val < 0)
|
||||
@@ -559,7 +559,7 @@ b58_to_b256_be(std::string_view input, std::span<std::uint8_t> out)
|
||||
{
|
||||
for (int j = 0; j < num_full_coeffs; ++j)
|
||||
{
|
||||
unsigned char const c = input[partial_coeff_len + j * 10 + i];
|
||||
auto c = input[partial_coeff_len + j * 10 + i];
|
||||
auto cur_val = ::ripple::alphabetReverse[c];
|
||||
if (cur_val < 0)
|
||||
{
|
||||
@@ -580,12 +580,6 @@ b58_to_b256_be(std::string_view input, std::span<std::uint8_t> out)
|
||||
{
|
||||
std::uint64_t const c = b_58_10_coeff[i];
|
||||
|
||||
// Each digit is < 58, so at most 4 words are in use here and the
|
||||
// span below always fits `result`. Guard the invariant anyway rather
|
||||
// than letting a violation index past the end of the stack array.
|
||||
if (cur_result_size >= result.size())
|
||||
return Unexpected(TokenCodecErrc::inputTooLarge);
|
||||
|
||||
{
|
||||
auto code = ripple::b58_fast::detail::inplace_bigint_mul(
|
||||
std::span(&result[0], cur_result_size + 1), B_58_10);
|
||||
|
||||
@@ -21,10 +21,7 @@
|
||||
#include <xrpl/beast/core/LexicalCast.h>
|
||||
#include <xrpl/beast/rfc2616.h>
|
||||
#include <xrpl/server/Port.h>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/algorithm/string/classification.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/split.hpp>
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
@@ -69,14 +66,6 @@ operator<<(std::ostream& os, Port const& p)
|
||||
}
|
||||
}
|
||||
|
||||
if (!p.pwa_domains.empty())
|
||||
{
|
||||
os << "domains:";
|
||||
for (auto const& d : p.pwa_domains)
|
||||
os << " " << d;
|
||||
os << ", ";
|
||||
}
|
||||
|
||||
if (p.secure_gateway_nets_v4.size() || p.secure_gateway_nets_v6.size())
|
||||
{
|
||||
os << "secure_gateway nets:";
|
||||
@@ -322,64 +311,6 @@ parse_Port(ParsedPort& port, Section const& section, std::ostream& log)
|
||||
port.secure_gateway_nets_v4,
|
||||
port.secure_gateway_nets_v6);
|
||||
|
||||
// pwa ports: the zone(s) documents are served under, normalised like a
|
||||
// Host header: lower case, no trailing dot, LDH labels of 1..63, with
|
||||
// room left for the longest "pwa<AccountIndex>" label in front.
|
||||
if (auto const optResult = section.get("domain"))
|
||||
{
|
||||
std::vector<std::string> domains;
|
||||
boost::algorithm::split(
|
||||
domains, *optResult, boost::algorithm::is_any_of(","));
|
||||
for (auto& domain : domains)
|
||||
{
|
||||
boost::algorithm::trim(domain);
|
||||
boost::algorithm::to_lower(domain);
|
||||
if (!domain.empty() && domain.back() == '.')
|
||||
domain.pop_back();
|
||||
if (domain.empty())
|
||||
continue;
|
||||
bool valid = domain.size() + 24 <= 253;
|
||||
std::vector<std::string> labels;
|
||||
boost::algorithm::split(
|
||||
labels, domain, boost::algorithm::is_any_of("."));
|
||||
for (auto const& l : labels)
|
||||
valid = valid && !l.empty() && l.size() <= 63 &&
|
||||
l.front() != '-' && l.back() != '-' &&
|
||||
l.find_first_not_of(
|
||||
"abcdefghijklmnopqrstuvwxyz0123456789-") ==
|
||||
std::string::npos;
|
||||
if (!valid)
|
||||
{
|
||||
log << "Invalid value '" << domain << "' for key 'domain' in ["
|
||||
<< section.name()
|
||||
<< "]: expected a DNS zone such as pwapp.example";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
port.pwa_domains.insert(domain);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom domains on pwa ports, and the IPv4 recursive resolver to use for
|
||||
// their TXT records (default: the system resolver).
|
||||
if (auto const v = section.get("custom_domains"))
|
||||
if (!beast::lexicalCastChecked(port.pwa_custom_domains, *v))
|
||||
{
|
||||
log << "Invalid value '" << *v << "' for key 'custom_domains' in ["
|
||||
<< section.name() << "]: expected 0 or 1";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
if (auto const v = section.get("resolver"))
|
||||
{
|
||||
auto const ep = beast::IP::Endpoint::from_string_checked(*v);
|
||||
if (!ep || !ep->is_v4() || ep->address().is_unspecified())
|
||||
{
|
||||
log << "Invalid value '" << *v << "' for key 'resolver' in ["
|
||||
<< section.name() << "]: expected an IPv4 address[:port]";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
port.pwa_resolver = ep->port() ? *ep : ep->at_port(53);
|
||||
}
|
||||
|
||||
set(port.user, "user", section);
|
||||
set(port.password, "password", section);
|
||||
set(port.admin_user, "admin_user", section);
|
||||
|
||||
@@ -578,25 +578,6 @@ 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);
|
||||
|
||||
@@ -1,562 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/app/tx/applySteps.h>
|
||||
#include <xrpld/app/tx/detail/Export.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/json/json_reader.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/STValidation.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
/** Export keys: their nomination by UNL report, rotation on the account,
|
||||
crons following the rotation and the export_setup RPC (see
|
||||
Export.h). */
|
||||
class ExportKeys_test : public beast::unit_test::suite
|
||||
{
|
||||
using KeyPair = std::pair<PublicKey, SecretKey>;
|
||||
|
||||
static Blob
|
||||
blob(PublicKey const& pk)
|
||||
{
|
||||
return Blob(pk.begin(), pk.end());
|
||||
}
|
||||
|
||||
static std::uint32_t
|
||||
closeTime(jtx::Env& env)
|
||||
{
|
||||
return env.closed()->info().closeTime.time_since_epoch().count();
|
||||
}
|
||||
|
||||
static void
|
||||
closeAt(jtx::Env& env, std::uint32_t t)
|
||||
{
|
||||
env.close(NetClock::time_point{std::chrono::seconds{t}});
|
||||
}
|
||||
|
||||
// the ttUNL_REPORT the UNL makes of `master`'s nomination of `ek`
|
||||
static STTx
|
||||
exportKeyReport(
|
||||
LedgerIndex seq,
|
||||
PublicKey const& master,
|
||||
KeyPair const& ek,
|
||||
std::uint32_t networkID,
|
||||
std::optional<PublicKey> provenFor = std::nullopt)
|
||||
{
|
||||
auto const proof = sign(
|
||||
ek.first,
|
||||
ek.second,
|
||||
exportKeyProofData(provenFor.value_or(master), ek.first, networkID)
|
||||
.slice());
|
||||
return STTx(ttUNL_REPORT, [&](auto& obj) {
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
auto& r = obj.peekFieldObject(sfExportKeyReport);
|
||||
r.setFieldVL(sfPublicKey, master);
|
||||
r.setFieldVL(sfExportKey, ek.first);
|
||||
r.setFieldVL(sfExportKeyProof, proof);
|
||||
});
|
||||
}
|
||||
|
||||
static STTx
|
||||
activeValidatorReport(LedgerIndex seq, PublicKey const& master)
|
||||
{
|
||||
return STTx(ttUNL_REPORT, [&](auto& obj) {
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
obj.peekFieldObject(sfActiveValidator)
|
||||
.setFieldVL(sfPublicKey, master);
|
||||
});
|
||||
}
|
||||
|
||||
// put pseudo-txns in the open ledger, as consensus would, and close it
|
||||
static void
|
||||
inject(jtx::Env& env, std::vector<STTx> const& txs)
|
||||
{
|
||||
env.app().openLedger().modify([&](OpenView& view, beast::Journal) {
|
||||
for (auto const& tx : txs)
|
||||
{
|
||||
auto const id = tx.getTransactionID();
|
||||
auto s = std::make_shared<Serializer>();
|
||||
tx.add(*s);
|
||||
env.app().getHashRouter().setFlags(id, SF_PRIVATE2);
|
||||
view.rawTxInsert(id, std::move(s), nullptr);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
env.close();
|
||||
}
|
||||
|
||||
static void
|
||||
nominate(jtx::Env& env, PublicKey const& master, KeyPair const& ek)
|
||||
{
|
||||
inject(
|
||||
env,
|
||||
{exportKeyReport(
|
||||
env.current()->seq(),
|
||||
master,
|
||||
ek,
|
||||
env.app().config().NETWORK_ID)});
|
||||
}
|
||||
|
||||
static std::vector<Blob>
|
||||
keysOf(jtx::Env& env, jtx::Account const& acc)
|
||||
{
|
||||
std::vector<Blob> out;
|
||||
if (auto const sle = env.le(acc);
|
||||
sle && sle->isFieldPresent(sfExportKeys))
|
||||
for (auto const& e : sle->getFieldArray(sfExportKeys))
|
||||
out.push_back(e.getFieldVL(sfExportKey));
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::uint32_t
|
||||
keysSeq(jtx::Env& env)
|
||||
{
|
||||
auto const unl = env.le(keylet::UNLReport());
|
||||
return unl ? (*unl)[~sfExportKeysSeq].value_or(0) : 0;
|
||||
}
|
||||
|
||||
void
|
||||
testValidation()
|
||||
{
|
||||
testcase("validation carries a nomination");
|
||||
|
||||
auto const vk = randomKeyPair(KeyType::secp256k1);
|
||||
auto const ek = randomKeyPair(KeyType::ed25519);
|
||||
auto const v = std::make_shared<STValidation>(
|
||||
NetClock::time_point{std::chrono::seconds{1000}},
|
||||
vk.first,
|
||||
vk.second,
|
||||
calcNodeID(vk.first),
|
||||
[&](STValidation& v) {
|
||||
v.setFieldH256(sfLedgerHash, uint256{7});
|
||||
v.setFieldU32(sfLedgerSequence, 512);
|
||||
v.setFlag(vfFullValidation);
|
||||
v.setFieldVL(sfExportKey, ek.first.slice());
|
||||
v.setFieldVL(sfExportKeyProof, Blob{1, 2, 3});
|
||||
});
|
||||
|
||||
Serializer s;
|
||||
v->add(s);
|
||||
SerialIter sit(s.slice());
|
||||
STValidation const back(
|
||||
sit, [](PublicKey const& pk) { return calcNodeID(pk); }, true);
|
||||
BEAST_EXPECT(back.isValid());
|
||||
BEAST_EXPECT(back.getFieldVL(sfExportKey) == blob(ek.first));
|
||||
BEAST_EXPECT(back.getFieldVL(sfExportKeyProof) == (Blob{1, 2, 3}));
|
||||
}
|
||||
|
||||
void
|
||||
testPreflight()
|
||||
{
|
||||
testcase("export key report preflight");
|
||||
using namespace jtx;
|
||||
|
||||
{
|
||||
Env env{*this, supported_amendments() | featureExport};
|
||||
auto const nid = env.app().config().NETWORK_ID;
|
||||
auto const rules = env.current()->rules();
|
||||
Account const val{"val"};
|
||||
auto const ek = randomKeyPair(KeyType::ed25519);
|
||||
auto const seq = env.current()->seq();
|
||||
auto const pf = [&](STTx const& tx) {
|
||||
return preflight(env.app(), rules, tx, tapNONE, env.journal)
|
||||
.ter;
|
||||
};
|
||||
|
||||
BEAST_EXPECT(
|
||||
pf(exportKeyReport(seq, val.pk(), ek, nid)) == tesSUCCESS);
|
||||
|
||||
// proof for another master, or another network
|
||||
BEAST_EXPECT(
|
||||
pf(exportKeyReport(seq, val.pk(), ek, nid, ek.first)) ==
|
||||
temBAD_SIGNATURE);
|
||||
BEAST_EXPECT(
|
||||
pf(exportKeyReport(seq, val.pk(), ek, nid + 1)) ==
|
||||
temBAD_SIGNATURE);
|
||||
|
||||
// the master key itself is not an export key
|
||||
auto const mk = randomKeyPair(KeyType::ed25519);
|
||||
BEAST_EXPECT(
|
||||
pf(exportKeyReport(seq, mk.first, mk, nid)) == temMALFORMED);
|
||||
|
||||
// export key reports travel alone
|
||||
STObject obj = exportKeyReport(seq, val.pk(), ek, nid);
|
||||
obj.peekFieldObject(sfActiveValidator)
|
||||
.setFieldVL(sfPublicKey, val.pk());
|
||||
Serializer s;
|
||||
obj.add(s);
|
||||
SerialIter sit(s.slice());
|
||||
BEAST_EXPECT(pf(STTx(sit)) == temMALFORMED);
|
||||
}
|
||||
|
||||
{
|
||||
Env env{*this, supported_amendments() - featureExport};
|
||||
Account const val{"val"};
|
||||
BEAST_EXPECT(
|
||||
preflight(
|
||||
env.app(),
|
||||
env.current()->rules(),
|
||||
exportKeyReport(
|
||||
env.current()->seq(),
|
||||
val.pk(),
|
||||
randomKeyPair(KeyType::ed25519),
|
||||
env.app().config().NETWORK_ID),
|
||||
tapNONE,
|
||||
env.journal)
|
||||
.ter == temDISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRotation()
|
||||
{
|
||||
testcase("export key rotation");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, supported_amendments() | featureExport};
|
||||
Account const val{"val"};
|
||||
Account const unfunded{"unfunded"};
|
||||
env.fund(XRP(10000), val);
|
||||
env.close();
|
||||
|
||||
auto const P = Export::keyRotationPeriod;
|
||||
auto const epochOf = [&](std::uint32_t t) { return t / P; };
|
||||
auto const headTime = [&]() {
|
||||
return env.le(val)->getFieldArray(sfExportKeys)[0].getFieldU32(
|
||||
sfCloseTime);
|
||||
};
|
||||
|
||||
// start just inside an epoch
|
||||
std::uint32_t const b0 = (closeTime(env) / P + 1) * P;
|
||||
closeAt(env, b0 + 100);
|
||||
|
||||
// first nomination recorded, stamped with the close time
|
||||
auto const k1 = randomKeyPair(KeyType::ed25519);
|
||||
nominate(env, val.pk(), k1);
|
||||
BEAST_EXPECT(keysOf(env, val) == std::vector<Blob>{blob(k1.first)});
|
||||
BEAST_EXPECT(epochOf(headTime()) == epochOf(b0));
|
||||
BEAST_EXPECT(keysSeq(env) == 1);
|
||||
|
||||
// reported again every flag ledger: no change
|
||||
nominate(env, val.pk(), k1);
|
||||
BEAST_EXPECT(keysOf(env, val) == std::vector<Blob>{blob(k1.first)});
|
||||
BEAST_EXPECT(keysSeq(env) == 1);
|
||||
|
||||
// a new key in the same epoch replaces the head
|
||||
auto const k2 = randomKeyPair(KeyType::ed25519);
|
||||
nominate(env, val.pk(), k2);
|
||||
BEAST_EXPECT(keysOf(env, val) == std::vector<Blob>{blob(k2.first)});
|
||||
BEAST_EXPECT(keysSeq(env) == 2);
|
||||
|
||||
// a new key in a later epoch is pushed
|
||||
closeAt(env, b0 + P + 100);
|
||||
auto const k3 = randomKeyPair(KeyType::ed25519);
|
||||
nominate(env, val.pk(), k3);
|
||||
BEAST_EXPECT(
|
||||
keysOf(env, val) ==
|
||||
(std::vector<Blob>{blob(k3.first), blob(k2.first)}));
|
||||
BEAST_EXPECT(keysSeq(env) == 3);
|
||||
|
||||
// an older key never comes back
|
||||
nominate(env, val.pk(), k2);
|
||||
BEAST_EXPECT(keysOf(env, val).front() == blob(k3.first));
|
||||
BEAST_EXPECT(keysSeq(env) == 3);
|
||||
|
||||
// at most maxExportKeys, newest first
|
||||
std::vector<KeyPair> later;
|
||||
for (std::uint32_t i = 0; i < 20; ++i)
|
||||
{
|
||||
closeAt(env, b0 + (2 + i) * P + 100);
|
||||
later.push_back(randomKeyPair(KeyType::ed25519));
|
||||
nominate(env, val.pk(), later.back());
|
||||
}
|
||||
auto const keys = keysOf(env, val);
|
||||
BEAST_EXPECT(keys.size() == Export::maxExportKeys);
|
||||
BEAST_EXPECT(keys.front() == blob(later.back().first));
|
||||
BEAST_EXPECT(keys.back() == blob(later[later.size() - 16].first));
|
||||
BEAST_EXPECT(keysSeq(env) == 23);
|
||||
|
||||
// the account must exist: nothing is created for it
|
||||
nominate(env, unfunded.pk(), randomKeyPair(KeyType::ed25519));
|
||||
BEAST_EXPECT(!env.le(unfunded));
|
||||
BEAST_EXPECT(keysSeq(env) == 23);
|
||||
}
|
||||
|
||||
void
|
||||
testReportReset()
|
||||
{
|
||||
testcase("export key reports do not suppress the UNLReport reset");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, supported_amendments() | featureExport};
|
||||
Account const a{"a"}, b{"b"};
|
||||
env.fund(XRP(10000), a, b);
|
||||
env.close();
|
||||
|
||||
auto const avs = [&]() {
|
||||
std::vector<PublicKey> out;
|
||||
for (auto const& v :
|
||||
env.le(keylet::UNLReport())->getFieldArray(sfActiveValidators))
|
||||
out.emplace_back(v[sfPublicKey]);
|
||||
return out;
|
||||
};
|
||||
|
||||
inject(
|
||||
env,
|
||||
{activeValidatorReport(env.current()->seq(), a.pk()),
|
||||
activeValidatorReport(env.current()->seq(), b.pk())});
|
||||
BEAST_EXPECT(avs().size() == 2);
|
||||
|
||||
// a later round with only `a` active, its export key report applied
|
||||
// in the same ledger: the array must still be rebuilt, not appended
|
||||
auto const seq = env.current()->seq();
|
||||
inject(
|
||||
env,
|
||||
{exportKeyReport(
|
||||
seq,
|
||||
a.pk(),
|
||||
randomKeyPair(KeyType::ed25519),
|
||||
env.app().config().NETWORK_ID),
|
||||
activeValidatorReport(seq, a.pk())});
|
||||
BEAST_EXPECT(avs() == std::vector<PublicKey>{a.pk()});
|
||||
BEAST_EXPECT(keysSeq(env) == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testRotationCron()
|
||||
{
|
||||
testcase("cron after each export key rotation");
|
||||
using namespace jtx;
|
||||
|
||||
auto const P = Export::keyRotationPeriod;
|
||||
Account const alice{"alice"};
|
||||
|
||||
{
|
||||
Env env{
|
||||
*this, supported_amendments() | featureExport | featureCron};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
// the ledger picks the times
|
||||
env(cron::set(alice),
|
||||
txflags(tfCronExportRotation),
|
||||
cron::startTime(0),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
env(cron::set(alice),
|
||||
txflags(tfCronExportRotation),
|
||||
cron::delay(P),
|
||||
cron::repeat(2),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
env(cron::set(alice),
|
||||
txflags(tfCronExportRotation | tfCronUnset),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
env(cron::set(alice),
|
||||
txflags(tfCronExportRotation),
|
||||
cron::repeat(257),
|
||||
fee(XRP(1)),
|
||||
ter(temMALFORMED));
|
||||
|
||||
auto const now = static_cast<std::uint32_t>(
|
||||
env.current()->parentCloseTime().time_since_epoch().count());
|
||||
env(cron::set(alice),
|
||||
txflags(tfCronExportRotation),
|
||||
cron::repeat(4),
|
||||
fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto const cronOf = [&]() {
|
||||
return env.le(
|
||||
keylet::child(env.le(alice)->getFieldH256(sfCron)));
|
||||
};
|
||||
auto const c = cronOf();
|
||||
BEAST_EXPECT(c);
|
||||
if (!c)
|
||||
return;
|
||||
|
||||
// just after the next rotation has settled, then every rotation
|
||||
auto const start = c->getFieldU32(sfStartTime);
|
||||
BEAST_EXPECT(start == Export::rotationCronTime(now, alice.id()));
|
||||
BEAST_EXPECT(start > now);
|
||||
BEAST_EXPECT(
|
||||
start % P >= Export::rotationSettle &&
|
||||
start % P < Export::rotationSettle + Export::rotationSpread);
|
||||
BEAST_EXPECT(c->getFieldU32(sfDelaySeconds) == P);
|
||||
BEAST_EXPECT(c->getFieldU32(sfRepeatCount) == 4);
|
||||
|
||||
// fires once that time has passed, and rearms a rotation later
|
||||
closeAt(env, start + 100);
|
||||
env.close();
|
||||
bool fired = false;
|
||||
for (auto const& [tx, meta] : env.closed()->txs)
|
||||
if (tx->getTxnType() == ttCRON && (*tx)[sfOwner] == alice.id())
|
||||
fired = true;
|
||||
BEAST_EXPECT(fired);
|
||||
|
||||
auto const next = cronOf();
|
||||
BEAST_EXPECT(next);
|
||||
if (next)
|
||||
{
|
||||
BEAST_EXPECT(next->getFieldU32(sfStartTime) == start + P);
|
||||
BEAST_EXPECT(next->getFieldU32(sfRepeatCount) == 3);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Env env{
|
||||
*this, (supported_amendments() | featureCron) - featureExport};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
env(cron::set(alice),
|
||||
txflags(tfCronExportRotation),
|
||||
fee(XRP(1)),
|
||||
ter(temINVALID_FLAG));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSignerListRPC()
|
||||
{
|
||||
testcase("export_setup");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, supported_amendments() | featureExport};
|
||||
Account const v1{"v1"}, v2{"v2"}, v3{"v3"};
|
||||
env.fund(XRP(10000), v1, v2, v3);
|
||||
env.close();
|
||||
|
||||
auto const P = Export::keyRotationPeriod;
|
||||
std::uint32_t const b0 = (closeTime(env) / P + 1) * P;
|
||||
closeAt(env, b0 + 100);
|
||||
|
||||
// v1 and v2 active and nominating, v3 active without a key
|
||||
auto const seq = env.current()->seq();
|
||||
auto const nid = env.app().config().NETWORK_ID;
|
||||
auto const k1 = randomKeyPair(KeyType::ed25519);
|
||||
auto const k2a = randomKeyPair(KeyType::ed25519);
|
||||
inject(
|
||||
env,
|
||||
{activeValidatorReport(seq, v1.pk()),
|
||||
activeValidatorReport(seq, v2.pk()),
|
||||
activeValidatorReport(seq, v3.pk()),
|
||||
exportKeyReport(seq, v1.pk(), k1, nid),
|
||||
exportKeyReport(seq, v2.pk(), k2a, nid)});
|
||||
|
||||
// v2 rotates an epoch later
|
||||
closeAt(env, b0 + P + 100);
|
||||
auto const k2b = randomKeyPair(KeyType::ed25519);
|
||||
nominate(env, v2.pk(), k2b);
|
||||
|
||||
auto const accountsOf = [](Json::Value const& r) {
|
||||
std::set<std::string> out;
|
||||
for (auto const& e : r[jss::tx_json][sfSignerEntries.jsonName])
|
||||
out.insert(
|
||||
e[sfSignerEntry.jsonName][sfAccount.jsonName].asString());
|
||||
return out;
|
||||
};
|
||||
auto const acc = [](KeyPair const& kp) {
|
||||
return toBase58(calcAccountID(kp.first));
|
||||
};
|
||||
|
||||
{
|
||||
auto const r = env.rpc("json", "export_setup", "{}")[jss::result];
|
||||
BEAST_EXPECT(
|
||||
r[jss::tx_json][jss::TransactionType] == "SignerListSet");
|
||||
BEAST_EXPECT(
|
||||
accountsOf(r) == (std::set<std::string>{acc(k1), acc(k2b)}));
|
||||
BEAST_EXPECT(r[jss::tx_json][sfSignerQuorum.jsonName] == 2);
|
||||
BEAST_EXPECT(r[jss::tx_json][sfOperationLimit.jsonName] == nid);
|
||||
BEAST_EXPECT(r["export_keys_seq"] == 3);
|
||||
|
||||
// a Xaman link to the same transaction
|
||||
std::string const prefix = "https://xaman.app/detect/";
|
||||
auto const url = r["xaman_url"].asString();
|
||||
BEAST_EXPECT(url.starts_with(prefix));
|
||||
auto const json = strUnHex(url.substr(prefix.size()));
|
||||
Json::Value tx;
|
||||
BEAST_EXPECT(
|
||||
json &&
|
||||
Json::Reader().parse(
|
||||
std::string(json->begin(), json->end()), tx) &&
|
||||
tx == r[jss::tx_json]);
|
||||
}
|
||||
|
||||
{
|
||||
// as it was in the first epoch
|
||||
Json::Value p;
|
||||
p["as_of"] = b0 + 1000;
|
||||
p["quorum"] = 1;
|
||||
p[jss::account] = v1.human();
|
||||
auto const r =
|
||||
env.rpc("json", "export_setup", to_string(p))[jss::result];
|
||||
BEAST_EXPECT(
|
||||
accountsOf(r) == (std::set<std::string>{acc(k1), acc(k2a)}));
|
||||
BEAST_EXPECT(r[jss::tx_json][sfSignerQuorum.jsonName] == 1);
|
||||
BEAST_EXPECT(r[jss::tx_json][jss::Account] == v1.human());
|
||||
}
|
||||
|
||||
{
|
||||
// before any key, and a bad quorum
|
||||
Json::Value p;
|
||||
p["as_of"] = b0 - 1;
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "export_setup", to_string(p))[jss::result]
|
||||
.isMember(jss::error));
|
||||
p["as_of"] = b0 + 1000;
|
||||
p["quorum"] = 3;
|
||||
BEAST_EXPECT(
|
||||
env.rpc("json", "export_setup", to_string(p))[jss::result]
|
||||
.isMember(jss::error));
|
||||
}
|
||||
|
||||
{
|
||||
// the command line form
|
||||
auto const r =
|
||||
env.rpc("export_setup", v1.human(), "1")[jss::result];
|
||||
BEAST_EXPECT(r[jss::tx_json][jss::Account] == v1.human());
|
||||
BEAST_EXPECT(r[jss::tx_json][sfSignerQuorum.jsonName] == 1);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testValidation();
|
||||
testPreflight();
|
||||
testRotation();
|
||||
testReportReset();
|
||||
testRotationCron();
|
||||
testSignerListRPC();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ExportKeys, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -1,627 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
#include <xrpld/app/tx/applySteps.h>
|
||||
#include <xrpld/app/tx/detail/ApplyContext.h>
|
||||
#include <xrpld/app/tx/detail/Export.h>
|
||||
#include <xrpld/ledger/OpenView.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
/** The export transactors (see Export.h): ttEXPORT's checks, its use of the
|
||||
account's signer list and shadow tickets, and validators signing and
|
||||
finalizing pending exports.
|
||||
|
||||
ttEXPORT is hook-emitted, so its preflight runs as emit() runs it and its
|
||||
doApply runs directly; a full hook round trip and the Import of a ticketed
|
||||
XPOP are left to the hook and XPOP suites. */
|
||||
class Export_test : public beast::unit_test::suite
|
||||
{
|
||||
using KeyPair = std::pair<PublicKey, SecretKey>;
|
||||
|
||||
// a payment from `from` on the other network, ready to be multisigned
|
||||
static STObject
|
||||
payment(
|
||||
AccountID const& from,
|
||||
std::uint32_t drops,
|
||||
std::optional<std::uint32_t> ticket = std::nullopt,
|
||||
std::optional<std::uint32_t> boundTo = std::nullopt)
|
||||
{
|
||||
STObject t(sfExportedTxn);
|
||||
t.setFieldU16(sfTransactionType, ttPAYMENT);
|
||||
t.setAccountID(sfAccount, from);
|
||||
t.setAccountID(sfDestination, noAccount());
|
||||
t.setFieldAmount(sfAmount, XRPAmount{drops});
|
||||
t.setFieldAmount(sfFee, XRPAmount{1000});
|
||||
t.setFieldU32(sfSequence, ticket ? 0 : 5);
|
||||
t.setFieldVL(sfSigningPubKey, Slice{});
|
||||
if (ticket)
|
||||
t.setFieldU32(sfTicketSequence, *ticket);
|
||||
if (boundTo)
|
||||
t.setFieldU32(sfOperationLimit, *boundTo);
|
||||
return t;
|
||||
}
|
||||
|
||||
static uint256
|
||||
txid(STObject const& inner)
|
||||
{
|
||||
Serializer s;
|
||||
inner.add(s);
|
||||
return STTx{SerialIter{s.slice()}}.getTransactionID();
|
||||
}
|
||||
|
||||
static STArray
|
||||
signerEntries(std::vector<AccountID> const& accounts)
|
||||
{
|
||||
STArray a(sfSignerEntries);
|
||||
for (auto const& acc : accounts)
|
||||
{
|
||||
STObject e(sfSignerEntry);
|
||||
e.setAccountID(sfAccount, acc);
|
||||
e.setFieldU16(sfSignerWeight, 1);
|
||||
a.push_back(std::move(e));
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
// `acc`'s signer list on the other network, as Import records it
|
||||
static void
|
||||
setSignerList(
|
||||
OpenView& view,
|
||||
AccountID const& acc,
|
||||
STArray const& entries,
|
||||
std::uint32_t quorum = 1)
|
||||
{
|
||||
auto sle = std::make_shared<SLE>(*view.read(keylet::account(acc)));
|
||||
auto l = STObject::makeInnerObject(sfExportSignerList);
|
||||
l[sfSignerQuorum] = quorum;
|
||||
l.setFieldArray(sfSignerEntries, entries);
|
||||
l[sfLedgerSequence] = 1;
|
||||
l[sfTransactionIndex] = 0;
|
||||
sle->peekFieldObject(sfExportSignerList) = std::move(l);
|
||||
view.rawReplace(sle);
|
||||
}
|
||||
|
||||
// ttEXPORT's doApply, as the hook's emission would reach it
|
||||
static TER
|
||||
applyExport(jtx::Env& env, OpenView& view, STTx const& tx)
|
||||
{
|
||||
ApplyContext ctx(
|
||||
env.app(),
|
||||
view,
|
||||
tx,
|
||||
tesSUCCESS,
|
||||
XRPAmount{0},
|
||||
tapNONE,
|
||||
env.journal);
|
||||
Export ex(ctx);
|
||||
auto const ter = ex.doApply();
|
||||
if (isTesSuccess(ter))
|
||||
ctx.apply(ter);
|
||||
return ter;
|
||||
}
|
||||
|
||||
// ttEXPORT as a hook would emit it
|
||||
static STTx
|
||||
exportTx(
|
||||
jtx::Env& env,
|
||||
AccountID const& from,
|
||||
STObject const& inner,
|
||||
std::uint32_t flags = 0,
|
||||
bool emitted = true)
|
||||
{
|
||||
auto const nid = env.app().config().NETWORK_ID;
|
||||
auto const seq = env.current()->seq();
|
||||
return STTx(ttEXPORT, [&](STObject& o) {
|
||||
o.setAccountID(sfAccount, from);
|
||||
o.setFieldAmount(sfFee, XRPAmount{100000});
|
||||
o.setFieldU32(sfSequence, 0);
|
||||
o.setFieldVL(sfSigningPubKey, Slice{});
|
||||
o.setFieldU32(sfFirstLedgerSequence, seq);
|
||||
o.setFieldU32(sfLastLedgerSequence, seq + 5);
|
||||
if (flags)
|
||||
o.setFieldU32(sfFlags, flags);
|
||||
if (nid > 1024)
|
||||
o.setFieldU32(sfNetworkID, nid);
|
||||
o.set(std::make_unique<STObject>(inner));
|
||||
if (emitted)
|
||||
{
|
||||
auto d = std::make_unique<STObject>(sfEmitDetails);
|
||||
d->setFieldU32(sfEmitGeneration, 1);
|
||||
d->setFieldU64(sfEmitBurden, 1);
|
||||
d->setFieldH256(sfEmitParentTxnID, uint256{1});
|
||||
d->setFieldH256(sfEmitNonce, uint256{2});
|
||||
d->setFieldH256(sfEmitHookHash, uint256{3});
|
||||
d->setAccountID(sfEmitCallback, from);
|
||||
o.set(std::move(d));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static STTx
|
||||
signTx(
|
||||
std::uint32_t nid,
|
||||
std::uint32_t created,
|
||||
uint256 const& id,
|
||||
KeyPair const& kp,
|
||||
Slice const& data)
|
||||
{
|
||||
auto const acc = calcAccountID(kp.first);
|
||||
return STTx(ttEXPORT_SIGN, [&](STObject& o) {
|
||||
o[sfAccount] = AccountID();
|
||||
if (nid > 1024)
|
||||
o[sfNetworkID] = nid;
|
||||
o[sfLedgerSequence] = created;
|
||||
o[sfTransactionHash] = id;
|
||||
auto& s = o.peekFieldObject(sfSigner);
|
||||
s[sfAccount] = acc;
|
||||
s[sfSigningPubKey] = kp.first.slice();
|
||||
s[sfTxnSignature] = sign(kp.first, kp.second, data);
|
||||
});
|
||||
}
|
||||
|
||||
// a pending export, as ttEXPORT would have left it in ledger `created`
|
||||
static Keylet
|
||||
pending(
|
||||
OpenView& view,
|
||||
AccountID const& owner,
|
||||
STObject const& inner,
|
||||
std::uint32_t created,
|
||||
STArray const& entries)
|
||||
{
|
||||
auto const k = keylet::exportedTxn(created, txid(inner));
|
||||
auto const sle = std::make_shared<SLE>(k);
|
||||
sle->setAccountID(sfOwner, owner);
|
||||
sle->set(std::make_unique<STObject>(inner));
|
||||
sle->setFieldH256(sfTransactionHash, txid(inner));
|
||||
sle->setFieldU32(sfLedgerSequence, created);
|
||||
sle->setFieldArray(sfSignerEntries, entries);
|
||||
sle->setFieldU32(sfSignerQuorum, 1);
|
||||
view.rawInsert(sle);
|
||||
return k;
|
||||
}
|
||||
|
||||
static std::vector<AccountID>
|
||||
signersOf(ReadView const& view, Keylet const& k)
|
||||
{
|
||||
std::vector<AccountID> out;
|
||||
auto const sle = view.read(k);
|
||||
if (!sle)
|
||||
return out;
|
||||
auto const& inner =
|
||||
sle->peekAtField(sfExportedTxn).downcast<STObject>();
|
||||
if (inner.isFieldPresent(sfSigners))
|
||||
for (auto const& s : inner.getFieldArray(sfSigners))
|
||||
out.push_back(s[sfAccount]);
|
||||
return out;
|
||||
}
|
||||
|
||||
// the metadata node an export transaction left on ltEXPORTED_TXN
|
||||
static std::optional<STObject>
|
||||
exportNode(OpenView const& view, TxType type, SField const& nodeType)
|
||||
{
|
||||
for (auto const& [tx, meta] : view.txs)
|
||||
{
|
||||
if (tx->getTxnType() != type || !meta)
|
||||
continue;
|
||||
for (auto const& n : meta->getFieldArray(sfAffectedNodes))
|
||||
if (n.getFName() == nodeType &&
|
||||
n.getFieldU16(sfLedgerEntryType) == ltEXPORTED_TXN)
|
||||
return n;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void
|
||||
testPreflight()
|
||||
{
|
||||
testcase("export preflight");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, supported_amendments() | featureExport};
|
||||
Account const alice{"alice"};
|
||||
auto const nid = env.app().config().NETWORK_ID;
|
||||
auto const a = calcAccountID(randomKeyPair(KeyType::ed25519).first);
|
||||
auto const b = calcAccountID(randomKeyPair(KeyType::ed25519).first);
|
||||
|
||||
auto const pf = [&](STTx const& tx) {
|
||||
return preflight(
|
||||
env.app(),
|
||||
env.current()->rules(),
|
||||
tx,
|
||||
tapPREFLIGHT_EMIT,
|
||||
env.journal)
|
||||
.ter;
|
||||
};
|
||||
|
||||
auto const good = payment(alice.id(), 10);
|
||||
BEAST_EXPECT(pf(exportTx(env, alice.id(), good)) == tesSUCCESS);
|
||||
|
||||
// emitted only
|
||||
BEAST_EXPECT(
|
||||
isTemMalformed(pf(exportTx(env, alice.id(), good, 0, false))));
|
||||
|
||||
BEAST_EXPECT(
|
||||
pf(exportTx(env, alice.id(), good, 0x00010000)) == temINVALID_FLAG);
|
||||
|
||||
// the inner transaction: from the hook account, unsigned, elsewhere
|
||||
BEAST_EXPECT(
|
||||
pf(exportTx(env, alice.id(), payment(a, 10))) == temMALFORMED);
|
||||
{
|
||||
auto signed_ = good;
|
||||
signed_.setFieldVL(sfTxnSignature, Blob{1, 2, 3});
|
||||
BEAST_EXPECT(
|
||||
pf(exportTx(env, alice.id(), signed_)) == temMALFORMED);
|
||||
}
|
||||
{
|
||||
auto here = good;
|
||||
here.setFieldU32(sfNetworkID, nid);
|
||||
BEAST_EXPECT(pf(exportTx(env, alice.id(), here)) == temMALFORMED);
|
||||
}
|
||||
|
||||
// a SignerListSet must come back, or the list here would go stale
|
||||
{
|
||||
STObject sls(sfExportedTxn);
|
||||
sls.setFieldU16(sfTransactionType, ttSIGNER_LIST_SET);
|
||||
sls.setAccountID(sfAccount, alice.id());
|
||||
sls.setFieldAmount(sfFee, XRPAmount{1000});
|
||||
sls.setFieldU32(sfSequence, 0);
|
||||
sls.setFieldU32(sfSignerQuorum, 0);
|
||||
sls.setFieldVL(sfSigningPubKey, Slice{});
|
||||
BEAST_EXPECT(pf(exportTx(env, alice.id(), sls)) == temMALFORMED);
|
||||
sls.setFieldU32(sfTicketSequence, 3);
|
||||
BEAST_EXPECT(pf(exportTx(env, alice.id(), sls)) == temMALFORMED);
|
||||
sls.setFieldU32(sfOperationLimit, nid);
|
||||
BEAST_EXPECT(pf(exportTx(env, alice.id(), sls)) == tesSUCCESS);
|
||||
}
|
||||
|
||||
// one base fee per signer in the account's list
|
||||
{
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
OpenView view(open_ledger, env.closed()->rules(), env.closed());
|
||||
auto const fee = [&](std::vector<AccountID> const& signers) {
|
||||
setSignerList(view, alice.id(), signerEntries(signers));
|
||||
return Export::calculateBaseFee(
|
||||
view, exportTx(env, alice.id(), good));
|
||||
};
|
||||
BEAST_EXPECT(fee({a, b}) - fee({a}) == view.fees().base);
|
||||
|
||||
// one bound back pays for the free Import that returns it
|
||||
setSignerList(view, alice.id(), signerEntries({a}));
|
||||
BEAST_EXPECT(
|
||||
Export::calculateBaseFee(
|
||||
view,
|
||||
exportTx(env, alice.id(), payment(alice.id(), 1, 7, nid))) -
|
||||
fee({a}) ==
|
||||
view.fees().base * 10);
|
||||
}
|
||||
|
||||
{
|
||||
Env off{*this, supported_amendments() - featureExport};
|
||||
BEAST_EXPECT(
|
||||
preflight(
|
||||
off.app(),
|
||||
off.current()->rules(),
|
||||
exportTx(off, alice.id(), good),
|
||||
tapPREFLIGHT_EMIT,
|
||||
off.journal)
|
||||
.ter == temDISABLED);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testShadowTicket()
|
||||
{
|
||||
testcase("shadow tickets");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, supported_amendments() | featureExport};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
auto const nid = env.app().config().NETWORK_ID;
|
||||
OpenView view(open_ledger, env.closed()->rules(), env.closed());
|
||||
setSignerList(
|
||||
view,
|
||||
alice.id(),
|
||||
signerEntries(
|
||||
{calcAccountID(randomKeyPair(KeyType::ed25519).first)}));
|
||||
|
||||
auto const owners = [&]() {
|
||||
return view.read(keylet::account(alice.id()))
|
||||
->getFieldU32(sfOwnerCount);
|
||||
};
|
||||
auto const apply = [&](STObject const& inner) {
|
||||
return applyExport(env, view, exportTx(env, alice.id(), inner));
|
||||
};
|
||||
auto const ticket = [&](std::uint32_t t) {
|
||||
return view.read(keylet::shadowTicket(alice.id(), t));
|
||||
};
|
||||
|
||||
auto const before = owners();
|
||||
|
||||
// not ticketed, or ticketed but not bound back here: no ticket
|
||||
BEAST_EXPECT(apply(payment(alice.id(), 1)) == tesSUCCESS);
|
||||
BEAST_EXPECT(apply(payment(alice.id(), 2, 7)) == tesSUCCESS);
|
||||
BEAST_EXPECT(apply(payment(alice.id(), 3, 7, nid + 1)) == tesSUCCESS);
|
||||
BEAST_EXPECT(!ticket(7));
|
||||
BEAST_EXPECT(owners() == before);
|
||||
|
||||
// ticketed and bound back: a ticket, held against the reserve
|
||||
auto const first = payment(alice.id(), 4, 7, nid);
|
||||
BEAST_EXPECT(apply(first) == tesSUCCESS);
|
||||
BEAST_EXPECT(ticket(7));
|
||||
BEAST_EXPECT(
|
||||
ticket(7) && (*ticket(7))[sfTransactionHash] == txid(first));
|
||||
BEAST_EXPECT(owners() == before + 1);
|
||||
|
||||
// both name the hook its Import (or a close below quorum) calls back
|
||||
BEAST_EXPECT(ticket(7) && (*ticket(7))[~sfEmitHookHash] == uint256{3});
|
||||
|
||||
// the pending export is attributed to the exporter
|
||||
auto const k = keylet::exportedTxn(view.seq(), txid(first));
|
||||
BEAST_EXPECT(view.read(k) && (*view.read(k))[sfOwner] == alice.id());
|
||||
BEAST_EXPECT(
|
||||
view.read(k) && (*view.read(k))[~sfEmitHookHash] == uint256{3});
|
||||
|
||||
// an export not bound back calls back now, so names no hook
|
||||
auto const k2 =
|
||||
keylet::exportedTxn(view.seq(), txid(payment(alice.id(), 1)));
|
||||
BEAST_EXPECT(
|
||||
view.read(k2) && !view.read(k2)->isFieldPresent(sfEmitHookHash));
|
||||
|
||||
// the same export again in this ledger
|
||||
BEAST_EXPECT(apply(first) == tecDUPLICATE);
|
||||
|
||||
// exporting again with the ticket repoints it
|
||||
auto const second = payment(alice.id(), 5, 7, nid);
|
||||
BEAST_EXPECT(apply(second) == tesSUCCESS);
|
||||
BEAST_EXPECT(
|
||||
ticket(7) && (*ticket(7))[sfTransactionHash] == txid(second));
|
||||
BEAST_EXPECT(owners() == before + 1);
|
||||
}
|
||||
|
||||
void
|
||||
testSignerList()
|
||||
{
|
||||
testcase("export signer list");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, supported_amendments() | featureExport};
|
||||
Account const alice{"alice"}, bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const nid = env.app().config().NETWORK_ID;
|
||||
auto const a = calcAccountID(randomKeyPair(KeyType::ed25519).first);
|
||||
OpenView view(open_ledger, env.closed()->rules(), env.closed());
|
||||
setSignerList(view, alice.id(), signerEntries({a}), 1);
|
||||
|
||||
// only an account whose signer list is known can export
|
||||
auto const preclaim = [&](AccountID const& from) {
|
||||
return Export::preclaim(PreclaimContext(
|
||||
env.app(),
|
||||
view,
|
||||
tesSUCCESS,
|
||||
exportTx(env, from, payment(from, 1)),
|
||||
tapNONE));
|
||||
};
|
||||
BEAST_EXPECT(preclaim(alice.id()) == tesSUCCESS);
|
||||
BEAST_EXPECT(preclaim(bob.id()) == tecNO_TARGET);
|
||||
|
||||
// the pending export snapshots it
|
||||
auto const pay = payment(alice.id(), 1);
|
||||
BEAST_EXPECT(
|
||||
applyExport(env, view, exportTx(env, alice.id(), pay)) ==
|
||||
tesSUCCESS);
|
||||
if (auto const p =
|
||||
view.read(keylet::exportedTxn(view.seq(), txid(pay)));
|
||||
BEAST_EXPECT(p))
|
||||
{
|
||||
BEAST_EXPECT(p->getFieldArray(sfSignerEntries).size() == 1);
|
||||
BEAST_EXPECT((*p)[sfSignerQuorum] == 1);
|
||||
}
|
||||
|
||||
// one UNL validator with an export key on its account
|
||||
auto const master = randomKeyPair(KeyType::secp256k1);
|
||||
auto const key = randomKeyPair(KeyType::ed25519);
|
||||
{
|
||||
auto unl = std::make_shared<SLE>(keylet::UNLReport());
|
||||
STArray avs(sfActiveValidators);
|
||||
avs.push_back(STObject::makeInnerObject(sfActiveValidator));
|
||||
avs.back()[sfPublicKey] = master.first.slice();
|
||||
unl->setFieldArray(sfActiveValidators, avs);
|
||||
if (view.exists(keylet::UNLReport()))
|
||||
view.rawReplace(unl);
|
||||
else
|
||||
view.rawInsert(unl);
|
||||
|
||||
auto const id = calcAccountID(master.first);
|
||||
auto acc = std::make_shared<SLE>(keylet::account(id));
|
||||
acc->setAccountID(sfAccount, id);
|
||||
acc->setFieldAmount(sfBalance, XRPAmount{100'000'000});
|
||||
acc->setFieldU32(sfSequence, 1);
|
||||
STArray keys(sfExportKeys);
|
||||
keys.push_back(STObject::makeInnerObject(sfExportKeyEntry));
|
||||
keys.back().setFieldVL(sfExportKey, key.first.slice());
|
||||
keys.back().setFieldU32(sfCloseTime, 0);
|
||||
acc->setFieldArray(sfExportKeys, keys);
|
||||
view.rawInsert(acc);
|
||||
}
|
||||
|
||||
// a SignerListSet without entries moves onto the current export keys
|
||||
STObject sls(sfExportedTxn);
|
||||
sls.setFieldU16(sfTransactionType, ttSIGNER_LIST_SET);
|
||||
sls.setAccountID(sfAccount, alice.id());
|
||||
sls.setFieldAmount(sfFee, XRPAmount{1000});
|
||||
sls.setFieldU32(sfSequence, 0);
|
||||
sls.setFieldU32(sfTicketSequence, 3);
|
||||
sls.setFieldU32(sfOperationLimit, nid);
|
||||
sls.setFieldU32(sfSignerQuorum, 0);
|
||||
sls.setFieldVL(sfSigningPubKey, Slice{});
|
||||
BEAST_EXPECT(
|
||||
applyExport(env, view, exportTx(env, alice.id(), sls)) ==
|
||||
tesSUCCESS);
|
||||
|
||||
auto const st = view.read(keylet::shadowTicket(alice.id(), 3));
|
||||
auto const p = st ? view.read(keylet::exportedTxn(
|
||||
view.seq(), (*st)[sfTransactionHash]))
|
||||
: nullptr;
|
||||
BEAST_EXPECT(p);
|
||||
if (!p)
|
||||
return;
|
||||
auto const& inner = p->peekAtField(sfExportedTxn).downcast<STObject>();
|
||||
BEAST_EXPECT(inner[sfSignerQuorum] == 1);
|
||||
BEAST_EXPECT(
|
||||
inner.getFieldArray(sfSignerEntries).size() == 1 &&
|
||||
inner.getFieldArray(sfSignerEntries)[0][sfAccount] ==
|
||||
calcAccountID(key.first));
|
||||
// signed by the list the account holds now, not the new one
|
||||
BEAST_EXPECT(
|
||||
p->getFieldArray(sfSignerEntries).size() == 1 &&
|
||||
p->getFieldArray(sfSignerEntries)[0][sfAccount] == a);
|
||||
}
|
||||
|
||||
void
|
||||
testSignAndFinal()
|
||||
{
|
||||
testcase("export sign and final");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{
|
||||
*this,
|
||||
envconfig(validator, ""),
|
||||
supported_amendments() | featureExport};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
for (int i = 0; i < 6; ++i)
|
||||
env.close();
|
||||
|
||||
auto const nid = env.app().config().NETWORK_ID;
|
||||
|
||||
// this validator's export key, and another listed one held here
|
||||
auto const nominee = env.app().getExportKeys().nominate(*env.closed());
|
||||
BEAST_EXPECT(nominee);
|
||||
if (!nominee)
|
||||
return;
|
||||
auto const ours = calcAccountID(nominee->first);
|
||||
auto const other = randomKeyPair(KeyType::ed25519);
|
||||
auto const stranger = randomKeyPair(KeyType::ed25519);
|
||||
auto const entries = signerEntries({ours, calcAccountID(other.first)});
|
||||
|
||||
OpenView view(open_ledger, env.closed()->rules(), env.closed());
|
||||
auto const created = env.closed()->seq();
|
||||
auto const inner = payment(alice.id(), 10);
|
||||
auto const id = txid(inner);
|
||||
auto const k = pending(view, alice.id(), inner, created, entries);
|
||||
|
||||
auto const apply = [&](STTx const& tx) {
|
||||
return ripple::apply(env.app(), view, tx, tapNONE, env.journal).ter;
|
||||
};
|
||||
auto const data = [&](KeyPair const& kp) {
|
||||
return buildMultiSigningData(inner, calcAccountID(kp.first));
|
||||
};
|
||||
|
||||
// the validator signs once
|
||||
BEAST_EXPECT(Export::accept(env.app(), view, env.journal));
|
||||
BEAST_EXPECT(signersOf(view, k) == std::vector<AccountID>{ours});
|
||||
BEAST_EXPECT(!Export::accept(env.app(), view, env.journal));
|
||||
|
||||
// and its signature is attributed to the exporter
|
||||
auto const signNode = exportNode(view, ttEXPORT_SIGN, sfModifiedNode);
|
||||
BEAST_EXPECT(
|
||||
signNode &&
|
||||
signNode->peekAtField(sfFinalFields)
|
||||
.downcast<STObject>()
|
||||
.getAccountID(sfOwner) == alice.id());
|
||||
|
||||
// only listed accounts, with good signatures, once each
|
||||
BEAST_EXPECT(
|
||||
apply(signTx(nid, created, id, stranger, data(stranger).slice())) ==
|
||||
tefBAD_AUTH);
|
||||
BEAST_EXPECT(
|
||||
apply(signTx(nid, created, id, other, data(stranger).slice())) ==
|
||||
tefBAD_SIGNATURE);
|
||||
BEAST_EXPECT(
|
||||
apply(signTx(nid, created, id, other, data(other).slice())) ==
|
||||
tesSUCCESS);
|
||||
BEAST_EXPECT(
|
||||
apply(signTx(nid, created, id, other, data(other).slice())) ==
|
||||
tefALREADY);
|
||||
|
||||
// kept sorted, as the other network requires
|
||||
auto sorted = std::vector<AccountID>{ours, calcAccountID(other.first)};
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
BEAST_EXPECT(signersOf(view, k) == sorted);
|
||||
|
||||
// `window` ledgers on, every node finalizes it
|
||||
auto const old = payment(alice.id(), 20);
|
||||
auto const createdOld = view.seq() - Export::window;
|
||||
auto const kOld = pending(view, alice.id(), old, createdOld, entries);
|
||||
|
||||
auto const finalTx = [&](AccountID const& owner) {
|
||||
return STTx(ttEXPORT_FINAL, [&](STObject& o) {
|
||||
o[sfAccount] = AccountID();
|
||||
o[sfOwner] = owner;
|
||||
o[sfLedgerSequence] = createdOld;
|
||||
o[sfTransactionHash] = txid(old);
|
||||
});
|
||||
};
|
||||
BEAST_EXPECT(apply(finalTx(ours)) == tefFAILURE);
|
||||
BEAST_EXPECT(view.exists(kOld));
|
||||
|
||||
BEAST_EXPECT(Export::accept(env.app(), view, env.journal));
|
||||
BEAST_EXPECT(!view.exists(kOld));
|
||||
BEAST_EXPECT(view.exists(k));
|
||||
|
||||
// the result is in the DeletedNode, attributed to the exporter
|
||||
auto const finalNode = exportNode(view, ttEXPORT_FINAL, sfDeletedNode);
|
||||
BEAST_EXPECT(finalNode);
|
||||
if (finalNode)
|
||||
{
|
||||
auto const& ff =
|
||||
finalNode->peekAtField(sfFinalFields).downcast<STObject>();
|
||||
BEAST_EXPECT(ff.getAccountID(sfOwner) == alice.id());
|
||||
BEAST_EXPECT(ff.isFieldPresent(sfExportedTxn));
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testPreflight();
|
||||
testShadowTicket();
|
||||
testSignerList();
|
||||
testSignAndFinal();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(Export, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -590,134 +590,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_emitted_txn_entry(FeatureBitset features)
|
||||
{
|
||||
testcase("Test emitted txn ledger entry");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace hook;
|
||||
using namespace hook_api;
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
|
||||
for (bool const withFix : {true, false})
|
||||
{
|
||||
Env env{*this, withFix ? features : features - fix20261005};
|
||||
|
||||
// emissions are only written into non-open views, so apply on
|
||||
// top of the last closed ledger the way consensus does
|
||||
auto const closed = env.closed();
|
||||
OpenView ov{&*closed};
|
||||
BEAST_EXPECT(!ov.open());
|
||||
|
||||
STTx const invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
|
||||
ApplyContext applyCtx{
|
||||
env.app(),
|
||||
ov,
|
||||
invokeTx,
|
||||
tesSUCCESS,
|
||||
env.current()->fees().base,
|
||||
tapNONE,
|
||||
env.journal};
|
||||
|
||||
STTx const emitTx = STTx(ttINVOKE, [&](STObject& obj) {
|
||||
obj[sfAccount] = alice.id();
|
||||
obj[sfSequence] = 0;
|
||||
obj[sfSigningPubKey] = Slice{};
|
||||
obj[sfFirstLedgerSequence] = closed->seq() + 1;
|
||||
obj[sfLastLedgerSequence] = closed->seq() + 5;
|
||||
obj[sfFee] = closed->fees().base;
|
||||
|
||||
auto& emitDetails = obj.peekFieldObject(sfEmitDetails);
|
||||
emitDetails[sfEmitGeneration] = 1;
|
||||
emitDetails[sfEmitBurden] = 1;
|
||||
emitDetails[sfEmitParentTxnID] = invokeTx.getTransactionID();
|
||||
emitDetails[sfEmitNonce] = uint256();
|
||||
emitDetails[sfEmitHookHash] = uint256();
|
||||
});
|
||||
|
||||
std::string reason;
|
||||
auto const emitted = std::make_shared<ripple::Transaction>(
|
||||
std::make_shared<STTx const>(emitTx), reason, env.app());
|
||||
std::queue<std::shared_ptr<ripple::Transaction>> emittedTxn;
|
||||
emittedTxn.push(emitted);
|
||||
|
||||
auto hookCtx = makeStubHookContext(
|
||||
applyCtx,
|
||||
alice.id(),
|
||||
alice.id(),
|
||||
{.result = {.emittedTxn = emittedTxn}});
|
||||
|
||||
BEAST_EXPECT(
|
||||
hook::finalizeHookResult(hookCtx.result, applyCtx, true) ==
|
||||
tesSUCCESS);
|
||||
|
||||
auto const kl = keylet::emittedTxn(emitted->getID());
|
||||
auto const sle = applyCtx.view().peek(kl);
|
||||
if (!BEAST_EXPECT(sle))
|
||||
continue;
|
||||
|
||||
Serializer expected;
|
||||
emitTx.add(expected);
|
||||
|
||||
// in memory: with the fix sfEmittedTxn occupies its template
|
||||
// slot exactly once, without it a duplicate is appended behind
|
||||
// the (non-present) template slot and the field reads absent
|
||||
auto const entries =
|
||||
std::count_if(sle->begin(), sle->end(), [](STBase const& f) {
|
||||
return f.getFName() == sfEmittedTxn;
|
||||
});
|
||||
BEAST_EXPECT(entries == (withFix ? 1 : 2));
|
||||
BEAST_EXPECT(sle->isFieldPresent(sfEmittedTxn) == withFix);
|
||||
|
||||
// serialized: identical either way, re-reading the entry always
|
||||
// yields exactly the emitted transaction
|
||||
{
|
||||
Serializer s;
|
||||
sle->add(s);
|
||||
SerialIter sit(s.slice());
|
||||
STLedgerEntry const reread(sit, kl.key);
|
||||
BEAST_EXPECT(reread.isFieldPresent(sfEmittedTxn));
|
||||
Serializer txn;
|
||||
reread.peekAtField(sfEmittedTxn).add(txn);
|
||||
BEAST_EXPECT(txn.peekData() == expected.peekData());
|
||||
}
|
||||
|
||||
// a hook running later in the same ledger can tell them apart:
|
||||
// slot_subfield only finds sfEmittedTxn with the fix
|
||||
auto readerCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = readerCtx.api();
|
||||
|
||||
Bytes klBytes{
|
||||
static_cast<uint8_t>((kl.type >> 8) & 0xFFU),
|
||||
static_cast<uint8_t>(kl.type & 0xFFU)};
|
||||
klBytes.insert(klBytes.end(), kl.key.begin(), kl.key.end());
|
||||
|
||||
auto const slotNo = api.slot_set(klBytes, 0);
|
||||
if (!BEAST_EXPECT(slotNo.has_value()))
|
||||
continue;
|
||||
|
||||
auto const sub =
|
||||
api.slot_subfield(slotNo.value(), sfEmittedTxn.getCode(), 0);
|
||||
if (!withFix)
|
||||
{
|
||||
BEAST_EXPECT(!sub.has_value() && sub.error() == DOESNT_EXIST);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!BEAST_EXPECT(sub.has_value()))
|
||||
continue;
|
||||
auto const field = api.slot(sub.value());
|
||||
if (!BEAST_EXPECT(field.has_value()))
|
||||
continue;
|
||||
Serializer s;
|
||||
field.value()->add(s);
|
||||
BEAST_EXPECT(s.peekData() == expected.peekData());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_etxn_details(FeatureBitset features)
|
||||
{
|
||||
@@ -1956,31 +1828,11 @@ public:
|
||||
BEAST_EXPECT(api.float_set(-50, 0).value() == 0);
|
||||
BEAST_EXPECT(api.float_set(0, 0).value() == 0);
|
||||
|
||||
// fix20261005: exponent out of range is reported as
|
||||
// EXPONENT_UNDERSIZED / EXPONENT_OVERSIZED; before the fix both
|
||||
// cases were reported as INVALID_FLOAT
|
||||
bool const fixExpCodes = env.current()->rules().enabled(fix20261005);
|
||||
auto const undersized =
|
||||
fixExpCodes ? EXPONENT_UNDERSIZED : INVALID_FLOAT;
|
||||
auto const oversized = fixExpCodes ? EXPONENT_OVERSIZED : INVALID_FLOAT;
|
||||
// an exponent lower than -96 should produce an invalid float error
|
||||
BEAST_EXPECT(api.float_set(-97, 1).error() == INVALID_FLOAT);
|
||||
|
||||
// an exponent lower than -96 should produce an underflow error
|
||||
BEAST_EXPECT(api.float_set(-97, 1).error() == undersized);
|
||||
|
||||
// an exponent larger than +96 should produce an overflow error
|
||||
BEAST_EXPECT(api.float_set(+97, 1).error() == oversized);
|
||||
|
||||
// the -96..80 range applies to the exponent after the mantissa has
|
||||
// been normalized to 16 digits (1 -> 1e15, exponent - 15)
|
||||
auto const oneE15 = [](int32_t exp) {
|
||||
return hook::hook_float::make_float(1000000000000000ULL, exp, false)
|
||||
.value();
|
||||
};
|
||||
BEAST_EXPECT(api.float_set(81, 1).value() == oneE15(66));
|
||||
BEAST_EXPECT(api.float_set(95, 1).value() == oneE15(80));
|
||||
BEAST_EXPECT(api.float_set(96, 1).error() == oversized);
|
||||
BEAST_EXPECT(api.float_set(-81, 1).value() == oneE15(-96));
|
||||
BEAST_EXPECT(api.float_set(-82, 1).error() == undersized);
|
||||
// an exponent larger than +96 should produce an invalid float error
|
||||
BEAST_EXPECT(api.float_set(+97, 1).error() == INVALID_FLOAT);
|
||||
|
||||
// clang-format off
|
||||
std::vector<std::tuple<int32_t, int64_t, uint64_t>> tests = {
|
||||
@@ -3043,91 +2895,6 @@ 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)
|
||||
{
|
||||
@@ -3826,174 +3593,6 @@ 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)
|
||||
{
|
||||
@@ -4725,20 +4324,8 @@ public:
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
|
||||
auto& api = hookCtx.api();
|
||||
|
||||
// a buffer shorter than two bytes can't be a valid STObject:
|
||||
// fix20261005 reports it as invalid rather than as TOO_SMALL
|
||||
if (env.closed()->rules().enabled(fix20261005))
|
||||
{
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{}).value() == false);
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{0x00}).value() == false);
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{0xE1}).value() == false);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{}).error() == TOO_SMALL);
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{0x00}).error() == TOO_SMALL);
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{0xE1}).error() == TOO_SMALL);
|
||||
}
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{}).error() == TOO_SMALL);
|
||||
BEAST_EXPECT(api.sto_validate(Bytes{0x00}).error() == TOO_SMALL);
|
||||
|
||||
// { Memo: {MemoData: "BEEF"} }
|
||||
auto const memos = *strUnHex("EA7D02BEEFE1");
|
||||
@@ -4812,22 +4399,6 @@ public:
|
||||
BEAST_EXPECT(accid.has_value());
|
||||
auto aliceid = alice.id();
|
||||
BEAST_EXPECT(accid.value() == Bytes(aliceid.begin(), aliceid.end()));
|
||||
|
||||
// Bytes outside the base58 alphabet, including >= 0x80, must be
|
||||
// rejected wherever they appear (signed-char lookup regression)
|
||||
{
|
||||
std::string const addr = alice.human();
|
||||
for (std::size_t pos = 0; pos < addr.size(); ++pos)
|
||||
{
|
||||
for (int const b : {0x00, 0x7f, 0x80, 0xc2, 0xff})
|
||||
{
|
||||
std::string bad = addr;
|
||||
bad[pos] = static_cast<char>(b);
|
||||
auto const r = api.util_accid(bad);
|
||||
BEAST_EXPECT(!r && r.error() == INVALID_ARGUMENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -5197,7 +4768,6 @@ public:
|
||||
|
||||
test_prepare(features);
|
||||
test_emit(features);
|
||||
test_emitted_txn_entry(features);
|
||||
test_etxn_burden(features);
|
||||
test_etxn_generation(features);
|
||||
test_otxn_burden(features);
|
||||
@@ -5211,7 +4781,6 @@ public:
|
||||
test_fee_base(features);
|
||||
|
||||
test_otxn_field(features);
|
||||
test_otxn_field_emit_failure(features);
|
||||
|
||||
test_ledger_keylet(features);
|
||||
|
||||
@@ -5227,7 +4796,6 @@ public:
|
||||
test_float_one(features);
|
||||
test_float_root(features);
|
||||
test_float_set(features);
|
||||
test_float_set(features - fix20261005);
|
||||
test_float_sign(features);
|
||||
test_float_sto(features);
|
||||
test_float_sto_set(features);
|
||||
@@ -5266,10 +4834,6 @@ 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);
|
||||
@@ -5283,7 +4847,6 @@ public:
|
||||
test_sto_subfield(features);
|
||||
test_sto_subfield(features - fix20260929);
|
||||
test_sto_validate(features);
|
||||
test_sto_validate(features - fix20261005);
|
||||
|
||||
test_trace(features);
|
||||
test_trace_float(features);
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx/Env.h>
|
||||
#include <test/jtx/envconfig.h>
|
||||
#include <xrpld/app/main/LoadManager.h>
|
||||
#include <xrpld/app/misc/LoadFeeTrack.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/**
|
||||
* Unit tests for LoadFeeTrack fee adjustment mechanics.
|
||||
*
|
||||
* These tests document the expected per-second behaviour of
|
||||
* raiseLocalFee() and lowerLocalFee() as called by LoadManager::run().
|
||||
*
|
||||
* Background: prior to the fix in this PR, the fee adjustment block in
|
||||
* LoadManager::run() was placed *outside* the while loop, meaning
|
||||
* raiseLocalFee()/lowerLocalFee() fired only once on shutdown rather
|
||||
* than every second during normal operation. localTxnLoadFee_ (and thus
|
||||
* load_factor_local in server_info) was therefore never adjusted by
|
||||
* local job queue load during normal operation.
|
||||
*/
|
||||
class LoadManager_test : public beast::unit_test::suite
|
||||
{
|
||||
public:
|
||||
// The normal/minimum fee factor (kLftNormalFee in LoadFeeTrack)
|
||||
static constexpr std::uint32_t kNormalFee = 256;
|
||||
|
||||
void
|
||||
testRaiseHysteresis()
|
||||
{
|
||||
testcase("raiseLocalFee requires two consecutive calls");
|
||||
|
||||
LoadFeeTrack track;
|
||||
|
||||
// First call: raiseCount_ goes from 0 to 1, returns false (hysteresis)
|
||||
BEAST_EXPECT(!track.raiseLocalFee());
|
||||
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
|
||||
|
||||
// Second call: raiseCount_ reaches 2, fee is raised
|
||||
BEAST_EXPECT(track.raiseLocalFee());
|
||||
BEAST_EXPECT(track.getLocalFee() > kNormalFee);
|
||||
}
|
||||
|
||||
void
|
||||
testLowerDecaysToBaseline()
|
||||
{
|
||||
testcase("lowerLocalFee decays elevated fee back to baseline");
|
||||
|
||||
LoadFeeTrack track;
|
||||
|
||||
// Raise the fee above baseline (requires two calls due to hysteresis)
|
||||
track.raiseLocalFee();
|
||||
track.raiseLocalFee();
|
||||
auto const elevated = track.getLocalFee();
|
||||
BEAST_EXPECT(elevated > kNormalFee);
|
||||
|
||||
// lowerLocalFee() should decay it each call
|
||||
// Each call reduces by 1/kLftFeeDecFraction (1/4), so a few
|
||||
// iterations should bring it back to kNormalFee
|
||||
bool changed = false;
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
changed |= track.lowerLocalFee();
|
||||
if (track.getLocalFee() == kNormalFee)
|
||||
break;
|
||||
}
|
||||
|
||||
BEAST_EXPECT(changed);
|
||||
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
|
||||
}
|
||||
|
||||
void
|
||||
testLowerAtBaselineIsNoop()
|
||||
{
|
||||
testcase("lowerLocalFee at baseline returns false");
|
||||
|
||||
LoadFeeTrack track;
|
||||
|
||||
// Already at baseline — should return false (no change)
|
||||
BEAST_EXPECT(!track.lowerLocalFee());
|
||||
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
|
||||
}
|
||||
|
||||
void
|
||||
testRaiseResetsOnLower()
|
||||
{
|
||||
testcase("lowerLocalFee resets raiseCount");
|
||||
|
||||
LoadFeeTrack track;
|
||||
|
||||
// One raise call (hysteresis: count=1, no change yet)
|
||||
BEAST_EXPECT(!track.raiseLocalFee());
|
||||
|
||||
// Lower resets raiseCount_ to 0
|
||||
track.lowerLocalFee();
|
||||
|
||||
// Next raise call starts from 0 again — still needs two calls
|
||||
BEAST_EXPECT(!track.raiseLocalFee());
|
||||
BEAST_EXPECT(track.getLocalFee() == kNormalFee);
|
||||
}
|
||||
|
||||
void
|
||||
testIsLoadedLocal()
|
||||
{
|
||||
testcase("isLoadedLocal reflects fee state correctly");
|
||||
|
||||
LoadFeeTrack track;
|
||||
|
||||
// At baseline, not loaded
|
||||
BEAST_EXPECT(!track.isLoadedLocal());
|
||||
|
||||
// After one raise (hysteresis, fee unchanged) — raiseCount_ != 0
|
||||
track.raiseLocalFee();
|
||||
BEAST_EXPECT(track.isLoadedLocal());
|
||||
|
||||
// After lower, raiseCount_ reset and fee back at baseline
|
||||
track.lowerLocalFee();
|
||||
BEAST_EXPECT(!track.isLoadedLocal());
|
||||
}
|
||||
|
||||
void
|
||||
testLoadManagerLoop()
|
||||
{
|
||||
testcase("LoadManager loop raises and decays load_factor_local");
|
||||
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env(*this, envconfig());
|
||||
|
||||
// Stop the real LoadManager thread so we can drive the fee track
|
||||
// manually without races. On stock code the fee adjustment block
|
||||
// is outside the while loop and never fires during normal operation,
|
||||
// so getFeeTrack().getLocalFee() stays at kNormalFee (256) regardless.
|
||||
env.app().getLoadManager().stop();
|
||||
|
||||
auto& feeTrack = env.app().getFeeTrack();
|
||||
|
||||
// Confirm baseline
|
||||
BEAST_EXPECT(feeTrack.getLocalFee() == kNormalFee);
|
||||
BEAST_EXPECT(!feeTrack.isLoadedLocal());
|
||||
|
||||
// raiseLocalFee() requires two consecutive calls (hysteresis guard).
|
||||
// This is the exact sequence LoadManager::run() executes each tick
|
||||
// when the job queue is overloaded.
|
||||
BEAST_EXPECT(!feeTrack.raiseLocalFee()); // tick 1: count=1, no change
|
||||
BEAST_EXPECT(feeTrack.raiseLocalFee()); // tick 2: count=2, fee raised
|
||||
BEAST_EXPECT(feeTrack.getLocalFee() > kNormalFee);
|
||||
BEAST_EXPECT(feeTrack.isLoadedLocal());
|
||||
|
||||
// Now simulate the LoadManager loop calling lowerLocalFee() each tick
|
||||
// until the fee decays back to baseline. On stock code this block
|
||||
// never runs during normal operation so the fee would stay elevated.
|
||||
bool decayed = false;
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
feeTrack.lowerLocalFee();
|
||||
if (feeTrack.getLocalFee() == kNormalFee)
|
||||
{
|
||||
decayed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
BEAST_EXPECT(decayed);
|
||||
BEAST_EXPECT(feeTrack.getLocalFee() == kNormalFee);
|
||||
BEAST_EXPECT(!feeTrack.isLoadedLocal());
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testRaiseHysteresis();
|
||||
testLowerDecaysToBaseline();
|
||||
testLowerAtBaselineIsNoop();
|
||||
testRaiseResetsOnLower();
|
||||
testIsLoadedLocal();
|
||||
testLoadManagerLoop();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(LoadManager, app, ripple);
|
||||
|
||||
} // namespace ripple
|
||||
File diff suppressed because it is too large.
Load diff
@@ -24,7 +24,6 @@
|
||||
#include <xrpld/app/rdb/Wallet.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/protocol/STExchange.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
@@ -976,482 +975,9 @@ public:
|
||||
".example.com"));
|
||||
}
|
||||
|
||||
struct Validator
|
||||
{
|
||||
SecretKey masterSecret;
|
||||
PublicKey master;
|
||||
SecretKey signingSecret;
|
||||
PublicKey signing;
|
||||
};
|
||||
|
||||
static Validator
|
||||
makeValidator()
|
||||
{
|
||||
auto const ms = randomSecretKey();
|
||||
auto const ss = randomSecretKey();
|
||||
return {
|
||||
ms,
|
||||
derivePublicKey(KeyType::ed25519, ms),
|
||||
ss,
|
||||
derivePublicKey(KeyType::secp256k1, ss)};
|
||||
}
|
||||
|
||||
/** The same master key with a fresh ephemeral key. */
|
||||
static Validator
|
||||
rotate(Validator const& v)
|
||||
{
|
||||
auto const ss = randomSecretKey();
|
||||
return {
|
||||
v.masterSecret,
|
||||
v.master,
|
||||
ss,
|
||||
derivePublicKey(KeyType::secp256k1, ss)};
|
||||
}
|
||||
|
||||
/** Another master key claiming the same ephemeral key. */
|
||||
static Validator
|
||||
impostor(Validator const& v)
|
||||
{
|
||||
auto const ms = randomSecretKey();
|
||||
return {
|
||||
ms,
|
||||
derivePublicKey(KeyType::ed25519, ms),
|
||||
v.signingSecret,
|
||||
v.signing};
|
||||
}
|
||||
|
||||
Manifest
|
||||
manifestFor(Validator const& v, int seq, bool invalidSig = false)
|
||||
{
|
||||
return makeManifest(
|
||||
v.masterSecret,
|
||||
KeyType::ed25519,
|
||||
v.signingSecret,
|
||||
KeyType::secp256k1,
|
||||
seq,
|
||||
invalidSig);
|
||||
}
|
||||
|
||||
static std::size_t
|
||||
offered(ManifestCache const& cache)
|
||||
{
|
||||
std::size_t n = 0;
|
||||
cache.for_each_gossip_manifest(
|
||||
[](std::size_t) {}, [&n](Manifest const&) { ++n; });
|
||||
return n;
|
||||
}
|
||||
|
||||
void
|
||||
testGossipAdmission()
|
||||
{
|
||||
testcase("gossip admission");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
// Gossip for a master key not held, before its ephemeral key has
|
||||
// signed anything: held back, not admitted.
|
||||
auto const a = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(a, 1), gossip) == D::unseen);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == a.signing);
|
||||
BEAST_EXPECT(!cache.getTier(a.master));
|
||||
|
||||
// Its first validation releases it, once.
|
||||
auto const seq = cache.sequence();
|
||||
auto const released = cache.noteValidation(a.signing);
|
||||
BEAST_EXPECT(released && released->masterKey == a.master);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == a.master);
|
||||
BEAST_EXPECT(cache.getTier(a.master) == gossip);
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
BEAST_EXPECT(!cache.noteValidation(a.signing));
|
||||
|
||||
// Seen first, then gossiped: admitted at once.
|
||||
auto const b = makeValidator();
|
||||
BEAST_EXPECT(!cache.noteValidation(b.signing));
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(b, 1), gossip) == D::accepted);
|
||||
|
||||
// A master key held from gossip rotates without its new key having
|
||||
// been seen...
|
||||
auto const b2 = rotate(b);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(b2, 2), gossip) == D::accepted);
|
||||
BEAST_EXPECT(cache.getMasterKey(b2.signing) == b.master);
|
||||
BEAST_EXPECT(cache.getMasterKey(b.signing) == b.signing);
|
||||
|
||||
// ...and is revoked from gossip too, since that only takes trust away.
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
makeRevocation(b.masterSecret, KeyType::ed25519), gossip) ==
|
||||
D::accepted);
|
||||
BEAST_EXPECT(cache.revoked(b.master));
|
||||
BEAST_EXPECT(cache.getMasterKey(b2.signing) == b2.signing);
|
||||
BEAST_EXPECT(cache.getTier(b.master) == gossip);
|
||||
|
||||
// A revocation for a master key not held names no ephemeral key that
|
||||
// could have been seen, so gossip cannot introduce one...
|
||||
auto const c = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
makeRevocation(c.masterSecret, KeyType::ed25519), gossip) ==
|
||||
D::unseen);
|
||||
BEAST_EXPECT(!cache.getTier(c.master));
|
||||
|
||||
// ...but a list can.
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(makeRevocation(
|
||||
c.masterSecret, KeyType::ed25519)) == D::accepted);
|
||||
BEAST_EXPECT(cache.getTier(c.master) == ManifestSource::list);
|
||||
BEAST_EXPECT(cache.revoked(c.master));
|
||||
|
||||
// A forgery waits unverified, and is dropped when released.
|
||||
auto const d = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(d, 1, true), gossip) == D::unseen);
|
||||
BEAST_EXPECT(!cache.noteValidation(d.signing));
|
||||
BEAST_EXPECT(!cache.getTier(d.master));
|
||||
|
||||
// A key is remembered for three to four epochs of the filter.
|
||||
auto const e = makeValidator();
|
||||
cache.noteValidation(e.signing);
|
||||
clock.advance(ManifestCache::seenInterval * 3);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(e, 1), gossip) == D::accepted);
|
||||
|
||||
auto const f = makeValidator();
|
||||
cache.noteValidation(f.signing);
|
||||
clock.advance(ManifestCache::seenInterval * 4);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(f, 1), gossip) == D::unseen);
|
||||
}
|
||||
|
||||
void
|
||||
testPendingSubstitution()
|
||||
{
|
||||
testcase("pending ring: forged replacement");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
// Same master and signing key, higher sequence, bad master signature.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 2, true), gossip) ==
|
||||
D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->sequence == 1);
|
||||
BEAST_EXPECT(cache.getMasterKey(v.signing) == v.master);
|
||||
}
|
||||
|
||||
// Different master claiming the same signing key, bad master signature.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
manifestFor(impostor(v), 2, true), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->masterKey == v.master);
|
||||
BEAST_EXPECT(cache.getMasterKey(v.signing) == v.master);
|
||||
}
|
||||
|
||||
// A forgery that arrives first, for the same master key with a
|
||||
// higher sequence, does not keep out the genuine manifest.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 2, true), gossip) ==
|
||||
D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->sequence == 1);
|
||||
BEAST_EXPECT(cache.getMasterKey(v.signing) == v.master);
|
||||
}
|
||||
|
||||
// Nor does one that arrives first for another master key.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
manifestFor(impostor(v), 2, true), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->masterKey == v.master);
|
||||
BEAST_EXPECT(cache.getMasterKey(v.signing) == v.master);
|
||||
}
|
||||
|
||||
// A genuine manifest waiting is not displaced by an older genuine one.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 2), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->sequence == 2);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testGossipCapacity()
|
||||
{
|
||||
testcase("gossip capacity and decay");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
std::vector<Validator> held;
|
||||
for (std::size_t i = 0; i < ManifestCache::gossipCapacity; ++i)
|
||||
{
|
||||
held.push_back(makeValidator());
|
||||
cache.noteValidation(held.back().signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(held.back(), 1), gossip) ==
|
||||
D::accepted);
|
||||
}
|
||||
BEAST_EXPECT(offered(cache) == ManifestCache::gossipCapacity);
|
||||
|
||||
// Every entry is exactly as active as a newcomer: none gives way.
|
||||
auto const newcomer = makeValidator();
|
||||
cache.noteValidation(newcomer.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(newcomer, 1), gossip) == D::full);
|
||||
|
||||
// Four half-lives on, all but the first have gone on validating. The
|
||||
// first has decayed to a sixteenth of a sighting, the rest stand at
|
||||
// seventeen sixteenths, and a newcomer is worth a whole one: it
|
||||
// displaces the first and nothing else.
|
||||
clock.advance(ManifestCache::halfLife * 4);
|
||||
for (std::size_t i = 1; i < held.size(); ++i)
|
||||
cache.noteValidation(held[i].signing);
|
||||
|
||||
auto const seq = cache.sequence();
|
||||
cache.noteValidation(newcomer.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(newcomer, 1), gossip) ==
|
||||
D::accepted);
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
BEAST_EXPECT(!cache.getTier(held[0].master));
|
||||
BEAST_EXPECT(cache.getMasterKey(held[0].signing) == held[0].signing);
|
||||
for (std::size_t i = 1; i < held.size(); ++i)
|
||||
BEAST_EXPECT(cache.getTier(held[i].master) == gossip);
|
||||
BEAST_EXPECT(offered(cache) == ManifestCache::gossipCapacity);
|
||||
|
||||
// The newcomer is now the least active, but no less active than
|
||||
// another newcomer, which therefore finds no room.
|
||||
auto const late = makeValidator();
|
||||
cache.noteValidation(late.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(late, 1), gossip) == D::full);
|
||||
|
||||
// Far enough on that the weights are rebased. Their order survives:
|
||||
// the one entry that stopped validating is the one displaced.
|
||||
clock.advance(ManifestCache::halfLife * 300);
|
||||
for (std::size_t i = 2; i < held.size(); ++i)
|
||||
cache.noteValidation(held[i].signing);
|
||||
cache.noteValidation(newcomer.signing);
|
||||
cache.noteValidation(late.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(late, 1), gossip) == D::accepted);
|
||||
BEAST_EXPECT(!cache.getTier(held[1].master));
|
||||
BEAST_EXPECT(cache.getTier(newcomer.master) == gossip);
|
||||
for (std::size_t i = 2; i < held.size(); ++i)
|
||||
BEAST_EXPECT(cache.getTier(held[i].master) == gossip);
|
||||
}
|
||||
|
||||
void
|
||||
testPinning()
|
||||
{
|
||||
testcase("pinning");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
std::vector<Validator> held;
|
||||
for (std::size_t i = 0; i < ManifestCache::gossipCapacity; ++i)
|
||||
{
|
||||
held.push_back(makeValidator());
|
||||
cache.noteValidation(held.back().signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(held.back(), 1), gossip) ==
|
||||
D::accepted);
|
||||
}
|
||||
|
||||
// Pinned, a gossip entry is retained as list and frees its slot.
|
||||
auto const seq = cache.sequence();
|
||||
cache.pin({held[0].master});
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
BEAST_EXPECT(cache.getTier(held[0].master) == ManifestSource::list);
|
||||
|
||||
auto const newcomer = makeValidator();
|
||||
cache.noteValidation(newcomer.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(newcomer, 1), gossip) ==
|
||||
D::accepted);
|
||||
for (auto const& v : held)
|
||||
BEAST_EXPECT(cache.getTier(v.master));
|
||||
|
||||
// Unpinned, it falls back into a full tier, which sheds its least
|
||||
// active entry: one of those that stopped validating.
|
||||
clock.advance(ManifestCache::halfLife);
|
||||
cache.noteValidation(held[0].signing);
|
||||
cache.noteValidation(newcomer.signing);
|
||||
cache.pin({});
|
||||
BEAST_EXPECT(cache.getTier(held[0].master) == gossip);
|
||||
BEAST_EXPECT(cache.getTier(newcomer.master) == gossip);
|
||||
std::size_t remaining = 0;
|
||||
for (std::size_t i = 1; i < held.size(); ++i)
|
||||
remaining += cache.getTier(held[i].master) ? 1 : 0;
|
||||
BEAST_EXPECT(remaining == held.size() - 2);
|
||||
BEAST_EXPECT(offered(cache) == ManifestCache::gossipCapacity);
|
||||
|
||||
// A master key a list vouched for stays in the list tier however it
|
||||
// is updated later, and is offered alongside gossip.
|
||||
auto const listed = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(listed, 1)) == D::accepted);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(rotate(listed), 2), gossip) ==
|
||||
D::accepted);
|
||||
BEAST_EXPECT(cache.getTier(listed.master) == ManifestSource::list);
|
||||
BEAST_EXPECT(offered(cache) == ManifestCache::gossipCapacity + 1);
|
||||
}
|
||||
|
||||
void
|
||||
testPrecedence()
|
||||
{
|
||||
testcase("tier precedence on key conflicts");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
// Gossip binds an ephemeral key to a master key...
|
||||
auto const a = makeValidator();
|
||||
cache.noteValidation(a.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(a, 1), gossip) == D::accepted);
|
||||
|
||||
// ...and a list binding the same key to another master key wins,
|
||||
// evicting it, where before it would have been refused.
|
||||
auto const b = impostor(a);
|
||||
BEAST_EXPECT(cache.applyManifest(manifestFor(b, 1)) == D::accepted);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == b.master);
|
||||
BEAST_EXPECT(!cache.getTier(a.master));
|
||||
|
||||
// Gossip cannot do the same to the list...
|
||||
auto const c = impostor(a);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(c, 1), gossip) ==
|
||||
D::badEphemeralKey);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == b.master);
|
||||
|
||||
// ...and between equals the first holds, as it always has.
|
||||
auto const d = impostor(a);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(d, 1)) == D::badEphemeralKey);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == b.master);
|
||||
}
|
||||
|
||||
void
|
||||
testPending()
|
||||
{
|
||||
testcase("pending");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
{
|
||||
// A newer manifest for a key already waiting replaces it.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 2), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 3), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->sequence == 3);
|
||||
BEAST_EXPECT(cache.getSequence(v.master) == 3);
|
||||
}
|
||||
|
||||
{
|
||||
// The ring holds pendingCapacity; the first to arrive makes way
|
||||
// for the one after.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
std::vector<Validator> waiting;
|
||||
for (std::size_t i = 0; i <= ManifestCache::pendingCapacity; ++i)
|
||||
{
|
||||
waiting.push_back(makeValidator());
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
manifestFor(waiting.back(), 1), gossip) == D::unseen);
|
||||
}
|
||||
|
||||
BEAST_EXPECT(!cache.noteValidation(waiting.front().signing));
|
||||
BEAST_EXPECT(!cache.getTier(waiting.front().master));
|
||||
|
||||
std::size_t released = 0;
|
||||
for (std::size_t i = 1; i < waiting.size(); ++i)
|
||||
released += cache.noteValidation(waiting[i].signing) ? 1 : 0;
|
||||
BEAST_EXPECT(released == ManifestCache::pendingCapacity);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testGossipAdmission();
|
||||
testGossipCapacity();
|
||||
testPendingSubstitution();
|
||||
testPinning();
|
||||
testPrecedence();
|
||||
testPending();
|
||||
|
||||
ManifestCache cache;
|
||||
{
|
||||
testcase("apply");
|
||||
|
||||
@@ -1,400 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/protocol/AppLoader.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class PWALoader_test : public beast::unit_test::suite
|
||||
{
|
||||
static appLoader::Result
|
||||
check(std::string const& s)
|
||||
{
|
||||
return appLoader::validate(
|
||||
reinterpret_cast<std::uint8_t const*>(s.data()), s.size());
|
||||
}
|
||||
|
||||
static std::string
|
||||
docOfSize(std::size_t len)
|
||||
{
|
||||
std::string const head = "<!DOCTYPE html><html><body>";
|
||||
std::string const tail = "</body></html>";
|
||||
if (len < head.size() + tail.size())
|
||||
return head + tail;
|
||||
return head + std::string(len - head.size() - tail.size(), 'x') + tail;
|
||||
}
|
||||
|
||||
static std::string
|
||||
doc()
|
||||
{
|
||||
return "<!DOCTYPE html><html><body>hi</body></html>";
|
||||
}
|
||||
|
||||
// AccountSet carrying sfAppLoader.
|
||||
static Json::Value
|
||||
set(jtx::Account const& a, std::string const& d)
|
||||
{
|
||||
auto jt = jtx::noop(a);
|
||||
jt[sfAppLoader.fieldName] = strHex(d);
|
||||
return jt;
|
||||
}
|
||||
|
||||
// validate() is consensus critical: these pin its edges, so a refactor
|
||||
// that moves any of them fails here rather than forking the network.
|
||||
void
|
||||
testValidator()
|
||||
{
|
||||
testcase("validator");
|
||||
using R = appLoader::Result;
|
||||
using namespace std::string_literals;
|
||||
|
||||
for (auto const& s :
|
||||
{"<!DOCTYPE html><html></html>"s,
|
||||
"<!doctype html><html></html>"s,
|
||||
"<html></html>"s,
|
||||
"<HTML></HTML>"s,
|
||||
"<html lang=\"en\" class=\"root\"></html>"s,
|
||||
" \n\t<html></html>\n\n "s,
|
||||
"<html></html >"s,
|
||||
"<html></html\n>"s,
|
||||
"\xEF\xBB\xBF<html></html>"s,
|
||||
"\xEF\xBB\xBF<!DOCTYPE html><html></html>"s,
|
||||
"<html><body>\xE6\xBC\xA2\xE5\xAD\x97</body></html>"s,
|
||||
"<html><script>var s=\"</html>\";</script></html>"s,
|
||||
"<html>\t\n\r\f</html>"s,
|
||||
"<html></html> \n"s,
|
||||
"<!DOCTYPE html>\n<!-- c --><html></html>"s,
|
||||
"<html>\xC2\x85</html>"s, // C1 controls are not rejected
|
||||
docOfSize(maxAppLoaderLength)})
|
||||
BEAST_EXPECTS(check(s) == R::ok, s);
|
||||
|
||||
std::vector<std::pair<std::string, R>> const bad{
|
||||
{""s, R::empty},
|
||||
{" \n"s, R::empty},
|
||||
{"\xEF\xBB\xBF"s, R::empty},
|
||||
{docOfSize(maxAppLoaderLength + 1), R::tooLarge},
|
||||
{"<html>\xC0\xAF</html>"s, R::badUTF8},
|
||||
{"<html>\xED\xA0\x80</html>"s, R::badUTF8},
|
||||
{"<html>\xEF\xBF\xBE</html>"s, R::badUTF8},
|
||||
{"<html>\xE6\xBC</html>"s, R::badUTF8},
|
||||
{"<html></html>\xE6"s, R::badUTF8},
|
||||
{"<html>\0</html>"s, R::badControlChar},
|
||||
{"<html>\x1B</html>"s, R::badControlChar},
|
||||
{"<html>\x7F</html>"s, R::badControlChar},
|
||||
{"hello"s, R::noDoctype},
|
||||
{"<div>frag</div>"s, R::noDoctype},
|
||||
{"<htmlish></html>"s, R::noDoctype},
|
||||
{"<!DOCTYPEhtml><html></html>"s, R::noDoctype},
|
||||
{"<!DOCTYPE"s, R::noDoctype},
|
||||
{"<!DOCTYPE html>"s, R::noHtmlElement},
|
||||
{"<!DOCTYPE html><htmlish></html>"s, R::noHtmlElement},
|
||||
{"<html>"s, R::unclosed},
|
||||
{"<html></html"s, R::unclosed},
|
||||
{"<html></htmlx>"s, R::unclosed},
|
||||
{"<html></html>trailing"s, R::trailingGarbage},
|
||||
{"<html></html><p>"s, R::trailingGarbage},
|
||||
{"<html></html>\xC2\xA0"s, R::trailingGarbage}, // NBSP
|
||||
};
|
||||
for (auto const& [s, r] : bad)
|
||||
BEAST_EXPECTS(
|
||||
check(s) == r, s + ": " + appLoader::to_string(check(s)));
|
||||
}
|
||||
|
||||
void
|
||||
testEnabled()
|
||||
{
|
||||
testcase("amendment gate");
|
||||
using namespace jtx;
|
||||
Env env{*this, supported_amendments() - featurePWALoader};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
env(set(alice, doc()), fee(XRP(1)), ter(temDISABLED));
|
||||
env(set(alice, ""), fee(XRP(1)), ter(temDISABLED));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::appLoader(alice.id())));
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAppLoaderSequence));
|
||||
}
|
||||
|
||||
void
|
||||
testSetAndRemove()
|
||||
{
|
||||
testcase("set, overwrite, remove, sequence");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
auto const kl = keylet::appLoader(alice.id());
|
||||
auto seq = [&] {
|
||||
return (*env.le(alice))[~sfAppLoaderSequence].value_or(0);
|
||||
};
|
||||
|
||||
BEAST_EXPECT(!env.le(kl));
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAppLoaderSequence));
|
||||
|
||||
// Create: the object, with the owner's new sequence; nothing inline
|
||||
// on the AccountRoot, no reserve, not in the owner directory.
|
||||
env(set(alice, doc()), fee(XRP(1)));
|
||||
env.close();
|
||||
auto sle = env.le(kl);
|
||||
if (BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT((*sle)[sfAppLoader] == makeSlice(doc()));
|
||||
BEAST_EXPECT((*sle)[sfOwner] == alice.id());
|
||||
BEAST_EXPECT((*sle)[sfAppLoaderSequence] == 1);
|
||||
}
|
||||
BEAST_EXPECT(seq() == 1);
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAppLoader));
|
||||
BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 0);
|
||||
BEAST_EXPECT(
|
||||
env.rpc(
|
||||
"json",
|
||||
"account_objects",
|
||||
R"({"account":")" + alice.human() +
|
||||
R"("})")[jss::result][jss::account_objects]
|
||||
.size() == 0);
|
||||
auto const txid = (*sle)[sfPreviousTxnID];
|
||||
|
||||
// Overwrite: new content, next sequence, rethreaded.
|
||||
env(set(alice, "<html><body>v2</body></html>"), fee(XRP(1)));
|
||||
env.close();
|
||||
sle = env.le(kl);
|
||||
if (BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
(*sle)[sfAppLoader] ==
|
||||
makeSlice(std::string("<html><body>v2</body></html>")));
|
||||
BEAST_EXPECT((*sle)[sfAppLoaderSequence] == 2);
|
||||
BEAST_EXPECT((*sle)[sfPreviousTxnID] != txid);
|
||||
}
|
||||
BEAST_EXPECT(seq() == 2);
|
||||
|
||||
// An AccountSet without the field leaves everything alone.
|
||||
env(noop(alice));
|
||||
env.close();
|
||||
BEAST_EXPECT((*env.le(kl))[sfAppLoaderSequence] == 2 && seq() == 2);
|
||||
|
||||
// Remove with an empty blob: the object goes, the sequence still
|
||||
// advances (so a copy of sequence 2 is known to be stale), and
|
||||
// removing again is a no-op that advances it again.
|
||||
env(set(alice, ""), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(kl) && seq() == 3);
|
||||
env(set(alice, ""), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(kl) && seq() == 4);
|
||||
|
||||
// Republish: the sequence carries on.
|
||||
env(set(alice, doc()), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT((*env.le(kl))[sfAppLoaderSequence] == 5 && seq() == 5);
|
||||
|
||||
// ledger_entry reaches it by account or by index.
|
||||
for (auto const& req :
|
||||
{R"({"app_loader":")" + alice.human() + R"("})",
|
||||
R"({"index":")" + to_string(kl.key) + R"("})"})
|
||||
{
|
||||
auto const r = env.rpc("json", "ledger_entry", req)[jss::result];
|
||||
BEAST_EXPECTS(
|
||||
r[jss::node][sfLedgerEntryType.fieldName] == jss::AppLoader &&
|
||||
r[jss::node][sfAppLoaderSequence.fieldName] == 5,
|
||||
req);
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
env.rpc(
|
||||
"json",
|
||||
"ledger_entry",
|
||||
R"({"app_loader":"nope"})")[jss::result][jss::error] ==
|
||||
"malformedAddress");
|
||||
}
|
||||
|
||||
void
|
||||
testFee()
|
||||
{
|
||||
testcase("fee: base plus one drop per byte");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
auto const base = env.current()->fees().base;
|
||||
auto const kl = keylet::appLoader(alice.id());
|
||||
|
||||
auto const expected = base + XRPAmount{1024};
|
||||
env(set(alice, docOfSize(1024)),
|
||||
fee(expected - XRPAmount{1}),
|
||||
ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(kl));
|
||||
auto const before = env.balance(alice);
|
||||
env(set(alice, docOfSize(1024)), fee(expected));
|
||||
env.close();
|
||||
BEAST_EXPECT(
|
||||
env.le(kl) && before - env.balance(alice) == drops(expected));
|
||||
|
||||
// Overwrites pay again; removal is base fee only.
|
||||
env(set(alice, docOfSize(100)),
|
||||
fee(base + XRPAmount{99}),
|
||||
ter(telINSUF_FEE_P));
|
||||
env(set(alice, docOfSize(100)), fee(base + XRPAmount{100}));
|
||||
env(set(alice, ""), fee(base));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(kl));
|
||||
|
||||
// Publishing takes no reserve: an account at its reserve floor can.
|
||||
Account const poor{"poor"};
|
||||
auto const reserve = env.current()->fees().accountReserve(0);
|
||||
env.fund(drops(reserve + base + XRPAmount{100}), noripple(poor));
|
||||
env.close();
|
||||
env(set(poor, docOfSize(100)), fee(base + XRPAmount{100}));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(keylet::appLoader(poor.id())));
|
||||
BEAST_EXPECT(env.balance(poor) == drops(reserve));
|
||||
env.require(owners(poor, 0));
|
||||
}
|
||||
|
||||
void
|
||||
testMalformed()
|
||||
{
|
||||
testcase("malformed documents are temMALFORMED");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
for (auto const& d :
|
||||
{std::string("hello"),
|
||||
std::string("<html>"),
|
||||
std::string("<html></html>x"),
|
||||
std::string("<html>\0</html>", 14),
|
||||
std::string("<html>\xC0\xAF</html>"),
|
||||
docOfSize(maxAppLoaderLength + 1)})
|
||||
{
|
||||
env(set(alice, d), fee(XRP(1)), ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
BEAST_EXPECT(!env.le(keylet::appLoader(alice.id())));
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAppLoaderSequence));
|
||||
}
|
||||
|
||||
void
|
||||
testAccountDelete()
|
||||
{
|
||||
testcase("account delete erases the loader");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"}, bob{"bob"};
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
env(set(alice, doc()), fee(XRP(1)));
|
||||
env.close();
|
||||
for (int i = 0; i < 256; ++i)
|
||||
env.close();
|
||||
// Not in the owner directory, so it cannot block deletion; erased
|
||||
// explicitly, or AccountRootsDeletedClean would fire on the orphan.
|
||||
env(acctdelete(alice, bob),
|
||||
fee(drops(env.current()->fees().increment)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(alice));
|
||||
BEAST_EXPECT(!env.le(keylet::appLoader(alice.id())));
|
||||
}
|
||||
|
||||
void
|
||||
testValidateRPC()
|
||||
{
|
||||
testcase("pwa_validate");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
auto validate = [&](std::string const& d, std::string const& domain) {
|
||||
Json::Value p(Json::objectValue);
|
||||
p[jss::account] = alice.human();
|
||||
p[jss::hash] = to_string(sha512Half(makeSlice(d)));
|
||||
if (!domain.empty())
|
||||
p[jss::domain] = domain;
|
||||
return env.rpc("json", "pwa_validate", to_string(p))[jss::result];
|
||||
};
|
||||
|
||||
BEAST_EXPECT(validate(doc(), "")["reason"] == "no loader");
|
||||
env(set(alice, doc()), fee(XRP(1)));
|
||||
env.close();
|
||||
auto r = validate(doc(), "");
|
||||
BEAST_EXPECT(r["valid"] == true && r["app_loader_sequence"] == 1);
|
||||
BEAST_EXPECT(r[jss::ledger_index] == env.closed()->seq());
|
||||
BEAST_EXPECT(
|
||||
validate("<html></html>", "")["reason"] == "hash mismatch");
|
||||
|
||||
// domain: the account's Domain, or a host beneath it, ignoring case.
|
||||
BEAST_EXPECT(
|
||||
validate(doc(), "pwa.test")["reason"] ==
|
||||
"domain not endorsed by account");
|
||||
auto jt = noop(alice);
|
||||
jt[sfDomain.fieldName] = strHex(std::string("Pwa.Test"));
|
||||
env(jt);
|
||||
env.close();
|
||||
for (auto const d :
|
||||
{"pwa.test", "PWA.TEST.", "pwaabcd.pwa.test", "a.b.pwa.test"})
|
||||
BEAST_EXPECTS(validate(doc(), d)["valid"] == true, d);
|
||||
for (auto const d : {"other.test", "xpwa.test", "pwa.test.evil"})
|
||||
BEAST_EXPECTS(validate(doc(), d)["valid"] == false, d);
|
||||
|
||||
// Malformed input.
|
||||
BEAST_EXPECT(
|
||||
env.rpc(
|
||||
"json",
|
||||
"pwa_validate",
|
||||
R"({"account":"x","hash":"00"})")[jss::result][jss::error] ==
|
||||
"actMalformed");
|
||||
BEAST_EXPECT(
|
||||
env.rpc(
|
||||
"pwa_validate", alice.human(), "zz")[jss::result][jss::error] ==
|
||||
"invalidParams");
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testValidator();
|
||||
testEnabled();
|
||||
testSetAndRemove();
|
||||
testFee();
|
||||
testMalformed();
|
||||
testAccountDelete();
|
||||
testValidateRPC();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(PWALoader, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,111 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/RotatingBloomFilter.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class RotatingBloomFilter_test : public beast::unit_test::suite
|
||||
{
|
||||
using time_point = std::chrono::steady_clock::time_point;
|
||||
|
||||
void
|
||||
testWindow()
|
||||
{
|
||||
testcase("window");
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
RotatingBloomFilter<1024, 4, 4> filter{10s};
|
||||
time_point const t{};
|
||||
std::string const a{"a"};
|
||||
std::string const b{"b"};
|
||||
|
||||
filter.insert(a, t);
|
||||
BEAST_EXPECT(filter.contains(a, t));
|
||||
BEAST_EXPECT(!filter.contains(b, t));
|
||||
|
||||
// Remembered to the end of the third epoch after its own, and
|
||||
// forgotten at the start of the fourth.
|
||||
BEAST_EXPECT(filter.contains(a, t + 39s));
|
||||
BEAST_EXPECT(!filter.contains(a, t + 40s));
|
||||
|
||||
// Inserting again extends it.
|
||||
filter.insert(a, t + 35s);
|
||||
BEAST_EXPECT(filter.contains(a, t + 69s));
|
||||
BEAST_EXPECT(!filter.contains(a, t + 70s));
|
||||
|
||||
// A clock that jumps finds everything expired.
|
||||
filter.insert(b, t + 70s);
|
||||
BEAST_EXPECT(!filter.contains(b, t + 1h));
|
||||
|
||||
// An insertion that read the clock before another thread rotated its
|
||||
// slot lands in the newer epoch rather than being lost.
|
||||
filter.insert(b, t + 1h + 40s);
|
||||
filter.insert(a, t + 1h);
|
||||
BEAST_EXPECT(filter.contains(a, t + 1h + 40s));
|
||||
}
|
||||
|
||||
void
|
||||
testFalsePositives()
|
||||
{
|
||||
testcase("false positives");
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using Filter = RotatingBloomFilter<std::size_t{1} << 14, 4, 4>;
|
||||
static_assert(Filter::bytes == 8192);
|
||||
|
||||
Filter filter{120s};
|
||||
time_point const t{};
|
||||
|
||||
std::uint64_t const inserted = 1000;
|
||||
for (std::uint64_t i = 0; i < inserted; ++i)
|
||||
filter.insert(i, t);
|
||||
|
||||
bool all = true;
|
||||
for (std::uint64_t i = 0; i < inserted; ++i)
|
||||
all = all && filter.contains(i, t);
|
||||
BEAST_EXPECT(all);
|
||||
|
||||
// About 0.24% in theory for a thousand elements in one generation.
|
||||
std::uint64_t const probes = 100000;
|
||||
std::uint64_t hits = 0;
|
||||
for (std::uint64_t i = inserted; i < inserted + probes; ++i)
|
||||
hits += filter.contains(i, t) ? 1 : 0;
|
||||
BEAST_EXPECT(hits < probes / 100);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testWindow();
|
||||
testFalsePositives();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(RotatingBloomFilter, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -26,17 +26,11 @@
|
||||
#include <xrpld/app/misc/detail/OnlineDeleteRanges.h>
|
||||
#include <xrpld/app/rdb/backend/SQLiteDatabase.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpld/nodestore/NodeObject.h>
|
||||
#include <xrpld/nodestore/detail/DatabaseRotatingImp.h>
|
||||
#include <xrpl/beast/utility/temp_dir.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
@@ -53,19 +47,6 @@ class SHAMapStore_test : public beast::unit_test::suite
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Same as onlineDelete, but on a node store that persists nodes, so
|
||||
// SHAMapStoreImp builds DatabaseRotatingImp and really rotates.
|
||||
// envconfig defaults to type=rwdb, which is the null node store.
|
||||
static auto
|
||||
onlineDeleteStoring(std::unique_ptr<Config> cfg, std::string const& path)
|
||||
{
|
||||
cfg = onlineDelete(std::move(cfg));
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("type", "memory");
|
||||
section.set("path", path);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static auto
|
||||
advisoryDelete(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
@@ -327,23 +308,13 @@ public:
|
||||
}
|
||||
|
||||
void
|
||||
testAutomatic(bool storing)
|
||||
testAutomatic()
|
||||
{
|
||||
testcase(
|
||||
storing ? "automatic online_delete (rotating node store)"
|
||||
: "automatic online_delete (null node store)");
|
||||
testcase("automatic online_delete");
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
beast::temp_dir nodeDir;
|
||||
Env env(
|
||||
*this,
|
||||
storing ? envconfig(onlineDeleteStoring, nodeDir.path())
|
||||
: envconfig(onlineDelete));
|
||||
BEAST_EXPECT(
|
||||
storing ==
|
||||
(dynamic_cast<NodeStore::DatabaseRotating*>(
|
||||
&env.app().getNodeStore()) != nullptr));
|
||||
Env env(*this, envconfig(onlineDelete));
|
||||
auto& store = env.app().getSHAMapStore();
|
||||
|
||||
auto ledgerSeq = waitForReady(env);
|
||||
@@ -601,412 +572,6 @@ public:
|
||||
return backend;
|
||||
}
|
||||
|
||||
std::unique_ptr<NodeStore::Backend>
|
||||
makeMemoryBackend(
|
||||
jtx::Env& env,
|
||||
NodeStoreScheduler& scheduler,
|
||||
std::string const& name)
|
||||
{
|
||||
Section section;
|
||||
section.set("type", "memory");
|
||||
section.set("path", name);
|
||||
auto backend = NodeStore::Manager::instance().make_Backend(
|
||||
section,
|
||||
megabytes(env.app().config().getValueFor(
|
||||
SizedItem::burstSize, std::nullopt)),
|
||||
scheduler,
|
||||
env.app().logs().journal("NodeStoreTest"));
|
||||
backend->open();
|
||||
return backend;
|
||||
}
|
||||
|
||||
// Ordinary fetch (duplicate == false) must copy an archived object
|
||||
// into the writable backend. The following rotation deletes the
|
||||
// archive. Without that copy, a transaction node read between
|
||||
// rotations disappears while callers can still name it.
|
||||
void
|
||||
testArchiveReadSurvivesRotation()
|
||||
{
|
||||
testcase("archive read survives rotation");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(onlineDelete));
|
||||
NodeStoreScheduler scheduler(env.app().getJobQueue());
|
||||
|
||||
Section nscfg;
|
||||
nscfg.set("type", "memory");
|
||||
nscfg.set("path", "rotating");
|
||||
|
||||
auto dbr = std::make_unique<NodeStore::DatabaseRotatingImp>(
|
||||
env.app(),
|
||||
scheduler,
|
||||
1,
|
||||
makeMemoryBackend(env, scheduler, "writable"),
|
||||
makeMemoryBackend(env, scheduler, "archive"),
|
||||
nscfg,
|
||||
env.app().logs().journal("NodeStoreTest"));
|
||||
|
||||
Blob const expected(32, 0xab);
|
||||
auto const hash = sha512Half(makeSlice(expected));
|
||||
{
|
||||
Blob data = expected;
|
||||
dbr->store(hotTRANSACTION_NODE, std::move(data), hash, 1);
|
||||
}
|
||||
|
||||
auto const matches = [&](std::shared_ptr<NodeObject> const& object) {
|
||||
return object && object->getType() == hotTRANSACTION_NODE &&
|
||||
object->getData() == expected;
|
||||
};
|
||||
|
||||
// Public fetch defaults duplicate to false. The private override
|
||||
// is the rotation copy pass.
|
||||
NodeStore::Database& db = *dbr;
|
||||
BEAST_EXPECT(matches(db.fetchNodeObject(hash)));
|
||||
|
||||
auto const nop = [](std::string const&, std::string const&) {};
|
||||
// Writable becomes the archive. The object now lives only there.
|
||||
dbr->rotate(makeMemoryBackend(env, scheduler, "writable-2"), nop);
|
||||
|
||||
BEAST_EXPECT(matches(db.fetchNodeObject(hash)));
|
||||
|
||||
dbr->rotate(makeMemoryBackend(env, scheduler, "writable-3"), nop);
|
||||
BEAST_EXPECT(matches(db.fetchNodeObject(hash)));
|
||||
}
|
||||
|
||||
// getWriteLoad, sync, storeLedger, importDatabase and for_each changed
|
||||
// lock type. In null mode no Env builds a DatabaseRotatingImp, so
|
||||
// drive them directly.
|
||||
void
|
||||
testRotatingAccessors()
|
||||
{
|
||||
testcase("rotating database accessors");
|
||||
|
||||
using namespace jtx;
|
||||
// Source ledger must come from a node store that holds its nodes.
|
||||
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("type", "memory");
|
||||
section.set("path", "rotating-accessors-app");
|
||||
return cfg;
|
||||
}));
|
||||
env.fund(XRP(10000), Account{"alice"});
|
||||
env.close();
|
||||
|
||||
NodeStoreScheduler scheduler(env.app().getJobQueue());
|
||||
Section nscfg;
|
||||
nscfg.set("type", "memory");
|
||||
nscfg.set("path", "rotating-accessors");
|
||||
auto makeDb = [&](std::string const& prefix) {
|
||||
return std::make_unique<NodeStore::DatabaseRotatingImp>(
|
||||
env.app(),
|
||||
scheduler,
|
||||
1,
|
||||
makeMemoryBackend(env, scheduler, prefix + "-writable"),
|
||||
makeMemoryBackend(env, scheduler, prefix + "-archive"),
|
||||
nscfg,
|
||||
env.app().logs().journal("NodeStoreTest"));
|
||||
};
|
||||
|
||||
auto src = makeDb("rotating-accessors-src");
|
||||
BEAST_EXPECT(src->getName() == "rotating-accessors-src-writable");
|
||||
BEAST_EXPECT(src->getWriteLoad() >= 0);
|
||||
src->sync();
|
||||
|
||||
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
||||
if (!BEAST_EXPECT(ledger))
|
||||
return;
|
||||
BEAST_EXPECT(src->storeLedger(ledger));
|
||||
|
||||
NodeStore::Database& srcDb = *src;
|
||||
auto const stateRoot = ledger->stateMap().getHash().as_uint256();
|
||||
BEAST_EXPECT(srcDb.fetchNodeObject(ledger->info().hash));
|
||||
BEAST_EXPECT(srcDb.fetchNodeObject(stateRoot));
|
||||
|
||||
// importDatabase walks the source with for_each.
|
||||
auto dst = makeDb("rotating-accessors-dst");
|
||||
dst->importDatabase(*src);
|
||||
NodeStore::Database& dstDb = *dst;
|
||||
BEAST_EXPECT(dstDb.fetchNodeObject(ledger->info().hash));
|
||||
BEAST_EXPECT(dstDb.fetchNodeObject(stateRoot));
|
||||
}
|
||||
|
||||
static auto
|
||||
nullBackend(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
cfg->LEDGER_HISTORY = 8;
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("type", "rwdb");
|
||||
section.set("path", "main");
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static auto
|
||||
nullBackendFullHistory(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
cfg = nullBackend(std::move(cfg));
|
||||
cfg->LEDGER_HISTORY = std::numeric_limits<std::uint32_t>::max();
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static auto
|
||||
nullBackendDeleteOff(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
cfg = nullBackend(std::move(cfg));
|
||||
cfg->LEDGER_HISTORY = 256;
|
||||
cfg->section(ConfigSection::nodeDatabase()).set("online_delete", "0");
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Payments keep applying after more closes than ledger_history,
|
||||
// with no node-store record of the state tree. Close time comes
|
||||
// from the resident ledger, not a node-store header walk.
|
||||
void
|
||||
testNullModeLedgerProgression()
|
||||
{
|
||||
testcase("null mode ledger progression");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(nullBackend));
|
||||
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const bobBefore = env.balance(bob);
|
||||
constexpr int payments = 12;
|
||||
for (int i = 0; i < payments; ++i)
|
||||
{
|
||||
env(pay(alice, bob, XRP(1)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
BEAST_EXPECT(env.balance(bob) == bobBefore + XRP(payments));
|
||||
BEAST_EXPECT(env.seq(alice) > 1);
|
||||
|
||||
auto const info = env.rpc(
|
||||
"json",
|
||||
"account_info",
|
||||
std::string{"{\"account\": \""} + alice.human() + "\"}");
|
||||
BEAST_EXPECT(
|
||||
info[jss::result].isMember(jss::account_data) &&
|
||||
info[jss::result][jss::account_data][jss::Account] ==
|
||||
alice.human());
|
||||
|
||||
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
||||
if (!BEAST_EXPECT(ledger))
|
||||
return;
|
||||
BEAST_EXPECT(ledger->read(keylet::account(alice.id())));
|
||||
|
||||
auto const closeTime = env.app().getLedgerMaster().getCloseTimeByHash(
|
||||
ledger->info().hash, ledger->info().seq);
|
||||
BEAST_EXPECT(closeTime && *closeTime == ledger->info().closeTime);
|
||||
|
||||
auto const validated = env.rpc("ledger", "validated");
|
||||
BEAST_EXPECT(goodLedger(
|
||||
env, validated, std::to_string(ledger->info().seq), true));
|
||||
}
|
||||
|
||||
// ledger_history=full would make the retained-ledger window grow
|
||||
// without bound, since nothing else can drop a null-mode tree.
|
||||
void
|
||||
testNullModeRejectsFullHistory()
|
||||
{
|
||||
testcase("RWDB null mode rejects ledger_history=full");
|
||||
|
||||
using namespace jtx;
|
||||
try
|
||||
{
|
||||
Env env(*this, envconfig(nullBackendFullHistory));
|
||||
fail("Env should throw when ledger_history is full");
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("ledger_history") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// online_delete=0 is an explicit disable. It must not be treated
|
||||
// as a missing key and replaced with ledger_history.
|
||||
void
|
||||
testExplicitOnlineDeleteZero()
|
||||
{
|
||||
testcase("explicit online_delete zero stays off");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(nullBackendDeleteOff));
|
||||
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
auto const balance = env.balance(alice);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.app().getSHAMapStore().getLastRotated() == 0);
|
||||
BEAST_EXPECT(env.balance(alice) == balance);
|
||||
}
|
||||
|
||||
void
|
||||
testNullModeRequiresHistory()
|
||||
{
|
||||
testcase("RWDB null mode requires ledger_history > 0");
|
||||
|
||||
using namespace jtx;
|
||||
try
|
||||
{
|
||||
Env env(*this, envconfig([](std::unique_ptr<Config> cfg) {
|
||||
cfg->LEDGER_HISTORY = 0;
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("type", "rwdb");
|
||||
section.set("path", "main");
|
||||
return cfg;
|
||||
}));
|
||||
fail("Env should throw when ledger_history is 0");
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("ledger_history") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testRetainedLedgerCloseTime()
|
||||
{
|
||||
testcase("retained ledger close time survives later closes");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(nullBackend));
|
||||
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
auto const first = env.app().getLedgerMaster().getClosedLedger();
|
||||
if (!BEAST_EXPECT(first))
|
||||
return;
|
||||
auto const firstHash = first->info().hash;
|
||||
auto const firstSeq = first->info().seq;
|
||||
auto const firstClose = first->info().closeTime;
|
||||
|
||||
for (int i = 0; i < 12; ++i)
|
||||
{
|
||||
env(pay(alice, env.master, XRP(1)));
|
||||
env.close();
|
||||
}
|
||||
|
||||
auto const closeTime =
|
||||
env.app().getLedgerMaster().getCloseTimeByHash(firstHash, firstSeq);
|
||||
BEAST_EXPECT(closeTime && *closeTime == firstClose);
|
||||
BEAST_EXPECT(env.balance(alice) < XRP(10000));
|
||||
}
|
||||
|
||||
void
|
||||
testNullBackendIsPerConfig()
|
||||
{
|
||||
testcase("null backend flag is per configuration");
|
||||
|
||||
Config rwdbCfg;
|
||||
Config upperCfg;
|
||||
Config nudbCfg;
|
||||
Config emptyCfg;
|
||||
rwdbCfg.section(ConfigSection::nodeDatabase()).set("type", "rwdb");
|
||||
upperCfg.section(ConfigSection::nodeDatabase()).set("type", "RWDB");
|
||||
nudbCfg.section(ConfigSection::nodeDatabase()).set("type", "NuDB");
|
||||
|
||||
// Evaluated per Config object, not latched process-wide.
|
||||
BEAST_EXPECT(rwdbCfg.nullBackend());
|
||||
BEAST_EXPECT(upperCfg.nullBackend());
|
||||
BEAST_EXPECT(!nudbCfg.nullBackend());
|
||||
BEAST_EXPECT(!emptyCfg.nullBackend());
|
||||
BEAST_EXPECT(rwdbCfg.nullBackend());
|
||||
}
|
||||
|
||||
// Resident lookups are used with peer-supplied values. They must find
|
||||
// in-memory ledgers but never load or edit the complete-ledger set.
|
||||
void
|
||||
testResidentLedgerLookup()
|
||||
{
|
||||
testcase("resident ledger lookup does not load or clear");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(nullBackend));
|
||||
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
for (int i = 0; i < 4; ++i)
|
||||
env.close();
|
||||
|
||||
auto& lm = env.app().getLedgerMaster();
|
||||
auto const closed = lm.getClosedLedger();
|
||||
if (!BEAST_EXPECT(closed))
|
||||
return;
|
||||
|
||||
auto const complete = lm.getCompleteLedgers();
|
||||
|
||||
auto const bySeq = lm.getResidentLedgerBySeq(closed->info().seq);
|
||||
BEAST_EXPECT(bySeq && bySeq->info().hash == closed->info().hash);
|
||||
|
||||
auto const byHash = lm.getResidentLedgerByHash(closed->info().hash);
|
||||
BEAST_EXPECT(byHash && byHash->info().seq == closed->info().seq);
|
||||
|
||||
// A recent ledger still inside the retained window.
|
||||
auto const prior = lm.getResidentLedgerBySeq(closed->info().seq - 2);
|
||||
BEAST_EXPECT(prior && prior->info().seq == closed->info().seq - 2);
|
||||
|
||||
BEAST_EXPECT(!lm.getResidentLedgerByHash(uint256{12345}));
|
||||
BEAST_EXPECT(!lm.getResidentLedgerBySeq(closed->info().seq + 1000));
|
||||
|
||||
BEAST_EXPECT(lm.getCompleteLedgers() == complete);
|
||||
|
||||
// Drop every ledger from the history cache. Null mode must still
|
||||
// reach recent ledgers through the closed/validated holders and
|
||||
// the retained window, without loading anything.
|
||||
env.app().getJobQueue().rendezvous();
|
||||
auto const older = lm.getResidentLedgerBySeq(closed->info().seq - 3);
|
||||
if (!BEAST_EXPECT(older))
|
||||
return;
|
||||
lm.clearLedgerCachePrior(closed->info().seq + 1);
|
||||
|
||||
auto const closedAgain = lm.getResidentLedgerBySeq(closed->info().seq);
|
||||
BEAST_EXPECT(
|
||||
closedAgain && closedAgain->info().hash == closed->info().hash);
|
||||
auto const closedByHash =
|
||||
lm.getResidentLedgerByHash(closed->info().hash);
|
||||
BEAST_EXPECT(
|
||||
closedByHash && closedByHash->info().seq == closed->info().seq);
|
||||
|
||||
auto const retainedBySeq = lm.getResidentLedgerBySeq(older->info().seq);
|
||||
BEAST_EXPECT(
|
||||
retainedBySeq && retainedBySeq->info().hash == older->info().hash);
|
||||
auto const retainedByHash =
|
||||
lm.getResidentLedgerByHash(older->info().hash);
|
||||
BEAST_EXPECT(
|
||||
retainedByHash && retainedByHash->info().seq == older->info().seq);
|
||||
|
||||
BEAST_EXPECT(!lm.getResidentLedgerByHash(uint256{12345}));
|
||||
BEAST_EXPECT(lm.getCompleteLedgers() == complete);
|
||||
}
|
||||
|
||||
void
|
||||
testNullFactoryDropsWrites()
|
||||
{
|
||||
testcase("type=rwdb uses NullFactory and drops node-store writes");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(nullBackend));
|
||||
|
||||
auto& db = env.app().getNodeStore();
|
||||
uint256 const hash{2};
|
||||
Blob data(32, 7);
|
||||
db.store(hotACCOUNT_NODE, std::move(data), hash, 1);
|
||||
BEAST_EXPECT(!db.fetchNodeObject(hash, 1));
|
||||
}
|
||||
|
||||
void
|
||||
testRotate()
|
||||
{
|
||||
@@ -1287,19 +852,8 @@ public:
|
||||
{
|
||||
testComputeOnlineDeleteTargets();
|
||||
testClear();
|
||||
testAutomatic(false);
|
||||
testAutomatic(true);
|
||||
testRotatingAccessors();
|
||||
testAutomatic();
|
||||
testCanDelete();
|
||||
testArchiveReadSurvivesRotation();
|
||||
testNullModeLedgerProgression();
|
||||
testNullModeRejectsFullHistory();
|
||||
testExplicitOnlineDeleteZero();
|
||||
testNullModeRequiresHistory();
|
||||
testRetainedLedgerCloseTime();
|
||||
testNullBackendIsPerConfig();
|
||||
testResidentLedgerLookup();
|
||||
testNullFactoryDropsWrites();
|
||||
testRotate();
|
||||
testPinnedRangeRestoreRequiresPinnedData();
|
||||
}
|
||||
|
||||
@@ -3104,37 +3104,6 @@ private:
|
||||
pass();
|
||||
}
|
||||
|
||||
// Export, ExportSign, ExportFinal
|
||||
// No TSHs: the exporting hook learns the outcome of its exports through
|
||||
// cbak, not hook(). See getTransactionalStakeHolders and Export_test.
|
||||
|
||||
void
|
||||
testExportTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("export tsh");
|
||||
|
||||
// hook-emitted transaction
|
||||
pass();
|
||||
}
|
||||
|
||||
void
|
||||
testExportSignTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("export sign tsh");
|
||||
|
||||
// validator-injected transaction
|
||||
pass();
|
||||
}
|
||||
|
||||
void
|
||||
testExportFinalTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("export final tsh");
|
||||
|
||||
// pseudo transaction
|
||||
pass();
|
||||
}
|
||||
|
||||
// GenesisMint
|
||||
// | otxn | tsh | mint |
|
||||
// | A | A | S |
|
||||
@@ -6177,15 +6146,6 @@ private:
|
||||
pass();
|
||||
}
|
||||
|
||||
void
|
||||
testEntropyTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("entropy tsh");
|
||||
|
||||
// pseudo transaction
|
||||
pass();
|
||||
}
|
||||
|
||||
// | otxn | tfBurnable | tsh | mint | burn | buy | sell | cancel
|
||||
// | O | false | O | N/A | S | N/A | S | S
|
||||
// | O | false | I | N/A | N | N/A | W | N/A
|
||||
|
||||
@@ -2134,118 +2134,6 @@ 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";
|
||||
@@ -4787,218 +4675,6 @@ 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)
|
||||
{
|
||||
@@ -6806,10 +6482,7 @@ public:
|
||||
{
|
||||
testcase("Test float_set");
|
||||
using namespace jtx;
|
||||
// this hook asserts the pre-fix20261005 INVALID_FLOAT result for an
|
||||
// out of range exponent; the amended EXPONENT_UNDERSIZED/OVERSIZED
|
||||
// behaviour is covered in HookAPI_test
|
||||
Env env{*this, features - fix20261005};
|
||||
Env env{*this, features};
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
@@ -13589,9 +13262,7 @@ public:
|
||||
testcase("Test sto_validate");
|
||||
using namespace jtx;
|
||||
|
||||
// this hook asserts the pre-fix20261005 TOO_SMALL result for a short
|
||||
// buffer; the amended behaviour is covered by the param hook below
|
||||
Env env{*this, features - fix20261005};
|
||||
Env env{*this, features};
|
||||
|
||||
auto const bob = Account{"bob"};
|
||||
auto const alice = Account{"alice"};
|
||||
@@ -13847,45 +13518,6 @@ public:
|
||||
testSTI(
|
||||
"011A0000000000000000000000005553440000000000", hasEnabled);
|
||||
}
|
||||
|
||||
// a buffer shorter than two bytes can't be a valid STObject:
|
||||
// fix20261005 returns 0 (invalid) instead of TOO_SMALL
|
||||
for (bool const withFix : {true, false})
|
||||
{
|
||||
Env env{
|
||||
*this,
|
||||
withFix ? features | fix20261005 : features - fix20261005};
|
||||
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// install the hook on alice
|
||||
env(ripple::test::jtx::hook(
|
||||
alice, {{hso(hook, overrideFlag)}}, 0),
|
||||
M("set sto_validate"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
|
||||
auto payJv = pay(bob, alice, XRP(1));
|
||||
Json::Value params{Json::arrayValue};
|
||||
auto& param = params[0U][jss::HookParameter];
|
||||
param[jss::HookParameterName] = strHex(std::string("V"));
|
||||
param[jss::HookParameterValue] = "E1";
|
||||
payJv[jss::HookParameters] = params;
|
||||
|
||||
env(payJv, M("test sto_validate short buffer"), fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto const result =
|
||||
env.meta()->getFieldArray(sfHookExecutions)[0].getFieldU64(
|
||||
sfHookReturnCode);
|
||||
|
||||
// negative exit codes are recorded with the MSB set
|
||||
BEAST_EXPECT(
|
||||
result ==
|
||||
(withFix ? 0ULL
|
||||
: 0x8000000000000000ULL + 4ULL /*TOO_SMALL*/));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16516,7 +16148,6 @@ 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,310 +35007,6 @@ 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,7 +22,6 @@
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/ManifestLedger.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
#include <xrpld/app/tx/detail/SetManifest.h>
|
||||
@@ -554,99 +553,9 @@ struct SetManifest_test : public beast::unit_test::suite
|
||||
submit(env, makeManifest(master, ephemeral, 1));
|
||||
env.close();
|
||||
|
||||
// Every refusal over a key in use is tef. The transaction is unsigned
|
||||
// and consumes no sequence, so anyone holding it could resubmit it in
|
||||
// every ledger; a tec would charge the master account each time.
|
||||
auto const masterBalance = env.balance(master);
|
||||
auto const otherBalance = env.balance(other);
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(other, ephemeral, 1))) ==
|
||||
"tefMANIFEST_KEY_IN_USE");
|
||||
|
||||
// A newer manifest that keeps the current ephemeral key. Every
|
||||
// manifest cache refuses it, so the ledger must too, or it would stay
|
||||
// ahead of what any node believes.
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(master, ephemeral, 2))) ==
|
||||
"tefMANIFEST_KEY_IN_USE");
|
||||
|
||||
// A master key that is already another account's ephemeral key: the
|
||||
// two kinds of manifest keylet share one namespace.
|
||||
auto const third = Account("third", KeyType::ed25519);
|
||||
auto const thirdEph = Account("thirdEph", KeyType::ed25519);
|
||||
env.fund(XRP(1000), third);
|
||||
env.close();
|
||||
|
||||
auto const thirdBalance = env.balance(third);
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(other, third, 1))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(third, thirdEph, 1))) ==
|
||||
"tefMANIFEST_KEY_IN_USE");
|
||||
env.close();
|
||||
|
||||
// Nothing was charged and nothing was written.
|
||||
BEAST_EXPECT(env.balance(master) == masterBalance);
|
||||
BEAST_EXPECT(env.balance(third) == thirdBalance);
|
||||
BEAST_EXPECT(env.balance(other) < otherBalance);
|
||||
if (auto const sle = env.le(keylet::manifest(master.pk()));
|
||||
BEAST_EXPECT(sle))
|
||||
BEAST_EXPECT(sle->getFieldU32(sfSequence) == 1);
|
||||
if (auto const sle = env.le(keylet::manifest(third.pk()));
|
||||
BEAST_EXPECT(sle))
|
||||
BEAST_EXPECT(sle->getAccountID(sfAccount) == other.id());
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(thirdEph.pk())));
|
||||
BEAST_EXPECT(
|
||||
!env.le(keylet::account(third.id()))->isFieldPresent(sfManifestID));
|
||||
}
|
||||
|
||||
void
|
||||
testAccountDelete(FeatureBitset features)
|
||||
{
|
||||
testcase("account delete");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
|
||||
auto const plain = Account("plain", KeyType::ed25519);
|
||||
auto const dest = Account("dest");
|
||||
env.fund(XRP(1000), master, plain, dest);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(master, ephemeral, 1))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
// Old enough to delete, so the manifest is the only obstacle.
|
||||
incLgrSeqForAccDel(env, master);
|
||||
incLgrSeqForAccDel(env, plain);
|
||||
|
||||
// An on-ledger manifest outlives its account: neither copy is in the
|
||||
// owner directory, and the master copy stays listed in the manifest
|
||||
// directory for good.
|
||||
env(acctdelete(master, dest),
|
||||
fee(drops(env.current()->fees().increment)),
|
||||
ter(tecHAS_OBLIGATIONS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(keylet::account(master.id())));
|
||||
BEAST_EXPECT(env.le(keylet::manifest(master.pk())));
|
||||
BEAST_EXPECT(env.le(keylet::manifest(ephemeral.pk())));
|
||||
|
||||
// An account of the same age without one goes as usual.
|
||||
env(acctdelete(plain, dest),
|
||||
fee(drops(env.current()->fees().increment)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::account(plain.id())));
|
||||
"tecDUPLICATE");
|
||||
}
|
||||
|
||||
void
|
||||
@@ -971,11 +880,6 @@ struct SetManifest_test : public beast::unit_test::suite
|
||||
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed(), all) == 4);
|
||||
|
||||
// Read from the ledger and pinned by nothing, all four are held as
|
||||
// ledger lookup cache entries.
|
||||
for (auto const& m : masters)
|
||||
BEAST_EXPECT(cache.getTier(m.pk()) == ManifestSource::ledger);
|
||||
|
||||
// The raw form is what a republishing node needs: the exact bytes the
|
||||
// master key signed, plus the sequence to compare against the ledger.
|
||||
auto const raw = cache.getRawManifest(masters[0].pk());
|
||||
@@ -991,237 +895,33 @@ struct SetManifest_test : public beast::unit_test::suite
|
||||
auto const seq = cache.sequence();
|
||||
cache.pin({masters[0].pk(), masters[1].pk()});
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
BEAST_EXPECT(cache.getTier(masters[0].pk()) == ManifestSource::list);
|
||||
|
||||
auto const seq2 = cache.sequence();
|
||||
cache.pin({masters[0].pk(), masters[1].pk()});
|
||||
BEAST_EXPECT(cache.sequence() == seq2);
|
||||
|
||||
// Only the pinned two are offered. The other two are on the ledger,
|
||||
// where any peer that is not amendment blocked can read them.
|
||||
// Everything is offered: two pinned plus two under the gossip limit.
|
||||
std::size_t reserved = 0;
|
||||
std::vector<PublicKey> offered;
|
||||
cache.for_each_gossip_manifest(
|
||||
[&](std::size_t n) { reserved = n; },
|
||||
[&](Manifest const& m) { offered.push_back(m.masterKey); });
|
||||
|
||||
BEAST_EXPECT(reserved == 2);
|
||||
BEAST_EXPECT(reserved == 4);
|
||||
BEAST_EXPECT(offered.size() == 4);
|
||||
BEAST_EXPECT(
|
||||
hash_set<PublicKey>(offered.begin(), offered.end()) ==
|
||||
hash_set<PublicKey>({masters[0].pk(), masters[1].pk()}));
|
||||
hash_set<PublicKey>(offered.begin(), offered.end()) == all);
|
||||
|
||||
// Unpinned, a master key falls back to the lookup cache. A pinned key
|
||||
// with no manifest is neither offered nor counted.
|
||||
// A pinned key with no manifest is counted in the reservation but not
|
||||
// offered, since the reservation is only an upper bound.
|
||||
cache.pin({masters[0].pk(), ephemeral.pk()});
|
||||
BEAST_EXPECT(cache.getTier(masters[1].pk()) == ManifestSource::ledger);
|
||||
offered.clear();
|
||||
cache.for_each_gossip_manifest(
|
||||
[&](std::size_t n) { reserved = n; },
|
||||
[&](Manifest const& m) { offered.push_back(m.masterKey); });
|
||||
|
||||
BEAST_EXPECT(reserved == 1);
|
||||
BEAST_EXPECT(offered.size() == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerOutranksGossip(FeatureBitset features)
|
||||
{
|
||||
testcase("ledger outranks gossip");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const eph1 = Account("eph1", KeyType::ed25519);
|
||||
auto const eph2 = Account("eph2", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master);
|
||||
env.close();
|
||||
|
||||
submit(env, makeManifest(master, eph1, 1));
|
||||
env.close();
|
||||
|
||||
auto const fromPeer = [](std::string const& s) {
|
||||
auto mo = deserializeManifest(s);
|
||||
return std::move(*mo);
|
||||
};
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
{
|
||||
// Gossip repeating what the ledger holds is stale, and it is the
|
||||
// ledger's copy that is kept, as a lookup cache entry.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{env.journal, clock};
|
||||
cache.noteValidation(eph1.pk());
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
fromPeer(makeManifest(master, eph1, 1)),
|
||||
gossip,
|
||||
env.closed().get()) == ManifestDisposition::stale);
|
||||
BEAST_EXPECT(cache.getTier(master.pk()) == ManifestSource::ledger);
|
||||
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == master.pk());
|
||||
}
|
||||
|
||||
submit(
|
||||
env,
|
||||
makeManifest(
|
||||
master, eph1, std::numeric_limits<std::uint32_t>::max()));
|
||||
env.close();
|
||||
|
||||
{
|
||||
// Revoked on-ledger, a master key cannot come back by gossip with
|
||||
// a fresh ephemeral key, even one seen validating, and even in a
|
||||
// cache that never held it.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{env.journal, clock};
|
||||
cache.noteValidation(eph2.pk());
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
fromPeer(makeManifest(master, eph2, 5)),
|
||||
gossip,
|
||||
env.closed().get()) == ManifestDisposition::stale);
|
||||
BEAST_EXPECT(cache.revoked(master.pk()));
|
||||
BEAST_EXPECT(cache.getTier(master.pk()) == ManifestSource::ledger);
|
||||
BEAST_EXPECT(cache.getMasterKey(eph2.pk()) == eph2.pk());
|
||||
}
|
||||
|
||||
{
|
||||
// A cache that took the gossip without a ledger to check it
|
||||
// against learns of the revocation at its next refresh, though it
|
||||
// never misses on the ephemeral key.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{env.journal, clock};
|
||||
cache.noteValidation(eph2.pk());
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
fromPeer(makeManifest(master, eph2, 5)), gossip) ==
|
||||
ManifestDisposition::accepted);
|
||||
BEAST_EXPECT(cache.getMasterKey(eph2.pk()) == master.pk());
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed()) == 1);
|
||||
BEAST_EXPECT(cache.revoked(master.pk()));
|
||||
BEAST_EXPECT(cache.getMasterKey(eph2.pk()) == eph2.pk());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDirectory(FeatureBitset features)
|
||||
{
|
||||
testcase("manifest directory");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
auto const revoke = std::numeric_limits<std::uint32_t>::max();
|
||||
|
||||
std::vector<Account> masters;
|
||||
std::vector<Account> ephs;
|
||||
for (int i = 0; i < 40; ++i)
|
||||
{
|
||||
masters.emplace_back("dm" + std::to_string(i), KeyType::ed25519);
|
||||
ephs.emplace_back("de" + std::to_string(i), KeyType::ed25519);
|
||||
env.fund(XRP(1000), masters.back());
|
||||
}
|
||||
env.close();
|
||||
|
||||
for (int i = 0; i < 40; ++i)
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[i], ephs[i], 1))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
auto const listed = [&]() {
|
||||
std::map<PublicKey, std::uint32_t> out;
|
||||
forEachLedgerManifest(
|
||||
*env.closed(), [&](std::shared_ptr<SLE const> const& sle) {
|
||||
out.emplace(
|
||||
PublicKey{makeSlice(sle->getFieldVL(sfPublicKey))},
|
||||
sle->getFieldU32(sfSequence));
|
||||
});
|
||||
return out;
|
||||
};
|
||||
|
||||
auto const pageOf = [&](Account const& a) {
|
||||
auto const sle = env.le(keylet::manifest(a.pk()));
|
||||
return sle && sle->isFieldPresent(sfOwnerNode)
|
||||
? std::optional<std::uint64_t>{sle->getFieldU64(sfOwnerNode)}
|
||||
: std::nullopt;
|
||||
};
|
||||
|
||||
// Each master object listed once, on the page it records; the
|
||||
// ephemeral copies are not listed.
|
||||
{
|
||||
auto const all = listed();
|
||||
BEAST_EXPECT(all.size() == 40);
|
||||
for (auto const& m : masters)
|
||||
BEAST_EXPECT(all.count(m.pk()) && all.at(m.pk()) == 1);
|
||||
for (auto const& e : ephs)
|
||||
{
|
||||
auto const sle = env.le(keylet::manifest(e.pk()));
|
||||
BEAST_EXPECT(sle && !sle->isFieldPresent(sfOwnerNode));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::optional<std::uint64_t>> pages;
|
||||
for (auto const& m : masters)
|
||||
{
|
||||
pages.push_back(pageOf(m));
|
||||
BEAST_EXPECT(pages.back());
|
||||
}
|
||||
|
||||
// Rotated to a new ephemeral key...
|
||||
std::vector<Account> rotated;
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
rotated.emplace_back("dr" + std::to_string(i), KeyType::ed25519);
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[i], rotated.back(), 2))) ==
|
||||
"tesSUCCESS");
|
||||
}
|
||||
env.close();
|
||||
|
||||
// ...revoked...
|
||||
for (int i = 10; i < 15; ++i)
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[i], ephs[i], revoke))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
// ...or replaced unsuccessfully, which changes nothing.
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[0], rotated[0], 2))) ==
|
||||
"tefPAST_MANIFEST_SEQ");
|
||||
env.close();
|
||||
|
||||
// Still each master key once, at its latest, on the same page.
|
||||
auto const all = listed();
|
||||
BEAST_EXPECT(all.size() == 40);
|
||||
for (int i = 0; i < 40; ++i)
|
||||
{
|
||||
auto const expected = i < 10 ? 2u : i < 15 ? revoke : 1u;
|
||||
BEAST_EXPECT(
|
||||
all.count(masters[i].pk()) &&
|
||||
all.at(masters[i].pk()) == expected);
|
||||
BEAST_EXPECT(pageOf(masters[i]) == pages[i]);
|
||||
}
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(ephs[i].pk())));
|
||||
BEAST_EXPECT(env.le(keylet::manifest(rotated[i].pk())));
|
||||
}
|
||||
for (int i = 10; i < 15; ++i)
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(ephs[i].pk())));
|
||||
|
||||
// Read from the directory alone, with no key to start from.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{env.journal, clock};
|
||||
BEAST_EXPECT(cache.applyLedgerDirectory(*env.closed()) == 40);
|
||||
BEAST_EXPECT(cache.getMasterKey(rotated[0].pk()) == masters[0].pk());
|
||||
BEAST_EXPECT(cache.getMasterKey(ephs[0].pk()) == ephs[0].pk());
|
||||
BEAST_EXPECT(cache.getMasterKey(ephs[20].pk()) == masters[20].pk());
|
||||
BEAST_EXPECT(cache.revoked(masters[10].pk()));
|
||||
BEAST_EXPECT(cache.applyLedgerDirectory(*env.closed()) == 0);
|
||||
BEAST_EXPECT(reserved == 5);
|
||||
BEAST_EXPECT(offered.size() == 4);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1261,13 +961,10 @@ public:
|
||||
testRetrieval(sa);
|
||||
testSigningKeyRetrieval(sa);
|
||||
testMalformed(sa);
|
||||
testAccountDelete(sa);
|
||||
testEnvelopeRejections(sa);
|
||||
testCanonicalFee(sa);
|
||||
testCorruptLedger(sa);
|
||||
testGossipSelection(sa);
|
||||
testLedgerOutranksGossip(sa);
|
||||
testDirectory(sa);
|
||||
testDisabled(sa);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5055,15 +5055,7 @@ public:
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
// featureRNG is excluded: makeConfig() runs as a validator whenever
|
||||
// voting settings are given, and a validator puts a ttENTROPY with a
|
||||
// fresh random commitment into every ledger. That makes the
|
||||
// transaction set hash, and so the canonical order in which
|
||||
// different accounts' transactions apply when the ledger closes,
|
||||
// differ from run to run, which breaks tests that assert balances
|
||||
// depending on that order (e.g. unexpected balance change).
|
||||
FeatureBitset const all{
|
||||
supported_amendments() - featureXahauGenesis - featureRNG};
|
||||
FeatureBitset const all{supported_amendments() - featureXahauGenesis};
|
||||
testQueueSeq(all);
|
||||
testQueueTicket(all);
|
||||
testTecResult(all);
|
||||
@@ -5090,15 +5082,7 @@ public:
|
||||
runMetaInfo()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
// featureRNG is excluded: makeConfig() runs as a validator whenever
|
||||
// voting settings are given, and a validator puts a ttENTROPY with a
|
||||
// fresh random commitment into every ledger. That makes the
|
||||
// transaction set hash, and so the canonical order in which
|
||||
// different accounts' transactions apply when the ledger closes,
|
||||
// differ from run to run, which breaks tests that assert balances
|
||||
// depending on that order (e.g. unexpected balance change).
|
||||
FeatureBitset const all{
|
||||
supported_amendments() - featureXahauGenesis - featureRNG};
|
||||
FeatureBitset const all{supported_amendments() - featureXahauGenesis};
|
||||
testAcctInQueueButEmpty(all);
|
||||
testRPC(all);
|
||||
testExpirationReplacement(all);
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -4677,12 +4676,11 @@ 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 && !has20261005) ? preLedger : preLedger + 1,
|
||||
has240819 ? preLedger : preLedger + 1,
|
||||
has240819 ? (preUser - feesXRP) : postUser,
|
||||
preTime));
|
||||
}
|
||||
@@ -5569,7 +5567,8 @@ struct XahauGenesis_test : public beast::unit_test::suite
|
||||
env.close();
|
||||
|
||||
// calculate rewards
|
||||
bool const hasFix = env.current()->rules().enabled(fix240819);
|
||||
bool const hasFix = env.current()->rules().enabled(fix240819) &&
|
||||
env.current()->rules().enabled(fix240911);
|
||||
STAmount const netReward =
|
||||
rewardUserAmount(*acctSle, preLedger, rateDrops);
|
||||
BEAST_EXPECT(netReward == (hasFix ? XRP(3.329999) : XRP(3.329999)));
|
||||
@@ -5660,7 +5659,8 @@ struct XahauGenesis_test : public beast::unit_test::suite
|
||||
env.close();
|
||||
|
||||
// calculate rewards
|
||||
bool const hasFix = env.current()->rules().enabled(fix240819);
|
||||
bool const hasFix = env.current()->rules().enabled(fix240819) &&
|
||||
env.current()->rules().enabled(fix240911);
|
||||
STAmount const netReward =
|
||||
rewardUserAmount(*acctSle, preLedger, rateDrops);
|
||||
BEAST_EXPECT(netReward == (hasFix ? XRP(3.479999) : XRP(6.663333)));
|
||||
@@ -5678,196 +5678,9 @@ 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);
|
||||
@@ -5910,7 +5723,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
|
||||
testGovernHookWithFeats(sa);
|
||||
testRewardHookWithFeats(sa);
|
||||
testRewardHookWithFeats(sa - fix240819);
|
||||
testRewardHookWithFeats(sa - fix20261005);
|
||||
testRewardHookWithFeats(sa - fix240819 - fix240911);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpl/basics/ReaderPreferringSharedMutex.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class ReaderPreferringSharedMutex_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testExclusiveAndShared()
|
||||
{
|
||||
testcase("exclusive and shared locking");
|
||||
|
||||
ReaderPreferringSharedMutex mutex;
|
||||
|
||||
BEAST_EXPECT(mutex.try_lock());
|
||||
BEAST_EXPECT(!mutex.try_lock());
|
||||
BEAST_EXPECT(!mutex.try_lock_shared());
|
||||
mutex.unlock();
|
||||
|
||||
mutex.lock_shared();
|
||||
BEAST_EXPECT(mutex.try_lock_shared());
|
||||
BEAST_EXPECT(!mutex.try_lock());
|
||||
mutex.unlock_shared();
|
||||
mutex.unlock_shared();
|
||||
|
||||
{
|
||||
std::shared_lock shared{mutex};
|
||||
BEAST_EXPECT(!mutex.try_lock());
|
||||
BEAST_EXPECT(mutex.try_lock_shared());
|
||||
mutex.unlock_shared();
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock exclusive{mutex};
|
||||
BEAST_EXPECT(!mutex.try_lock());
|
||||
BEAST_EXPECT(!mutex.try_lock_shared());
|
||||
}
|
||||
|
||||
mutex.lock();
|
||||
BEAST_EXPECT(!mutex.try_lock_shared());
|
||||
mutex.unlock();
|
||||
BEAST_EXPECT(mutex.try_lock_shared());
|
||||
mutex.unlock_shared();
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testExclusiveAndShared();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ReaderPreferringSharedMutex, ripple_basics, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -59,17 +59,6 @@ public:
|
||||
testUnHexFailure("123X");
|
||||
testUnHexFailure("V");
|
||||
testUnHexFailure("XAH");
|
||||
|
||||
// Bytes >= 0x80 (negative when char is signed) must be rejected, not
|
||||
// used to index the lookup table out of bounds
|
||||
for (int b = 0x80; b <= 0xff; ++b)
|
||||
{
|
||||
std::string const c(1, static_cast<char>(b));
|
||||
testUnHexFailure(c);
|
||||
testUnHexFailure("0" + c);
|
||||
testUnHexFailure(c + "0");
|
||||
testUnHexFailure("00" + c + "00");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -462,102 +462,11 @@ class base58_test : public beast::unit_test::suite
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDecodeInvalidChars()
|
||||
{
|
||||
testcase("decode_invalid_chars");
|
||||
|
||||
// Every byte outside the base58 alphabet, including bytes >= 0x80
|
||||
// (negative when char is signed), must be rejected at any position.
|
||||
// The fast decoder reads the leading `size % 10` characters and the
|
||||
// remaining characters in two separate loops; varying the length and
|
||||
// the position of the bad byte exercises both.
|
||||
constexpr std::string_view alphabet =
|
||||
"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz";
|
||||
std::array<bool, 256> inAlphabet{};
|
||||
for (unsigned char const c : alphabet)
|
||||
inAlphabet[c] = true;
|
||||
|
||||
auto const invalidCode =
|
||||
make_error_code(TokenCodecErrc::invalidEncodingChar);
|
||||
|
||||
// 52 is the longest input the fast decoder accepts
|
||||
for (std::size_t len = 1; len <= 52; ++len)
|
||||
{
|
||||
std::string base;
|
||||
for (std::size_t i = 0; i < len; ++i)
|
||||
base.push_back(alphabet[(i * 7 + len) % alphabet.size()]);
|
||||
|
||||
for (std::size_t pos = 0; pos < len; ++pos)
|
||||
{
|
||||
for (int b = 0; b < 256; ++b)
|
||||
{
|
||||
if (inAlphabet[b])
|
||||
continue;
|
||||
|
||||
std::string s = base;
|
||||
s[pos] = static_cast<char>(b);
|
||||
|
||||
std::array<std::uint8_t, 64> outBuf;
|
||||
auto const r = b58_fast::detail::b58_to_b256_be(s, outBuf);
|
||||
BEAST_EXPECT(!r && r.error() == invalidCode);
|
||||
BEAST_EXPECT(b58_ref::detail::decodeBase58(s).empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Same through the token API, using a valid AccountID as the base
|
||||
constexpr TokenType tokType = TokenType::AccountID;
|
||||
std::string const addr = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh";
|
||||
BEAST_EXPECT(!decodeBase58Token(addr, tokType).empty());
|
||||
for (std::size_t pos = 0; pos < addr.size(); ++pos)
|
||||
{
|
||||
for (int b = 0; b < 256; ++b)
|
||||
{
|
||||
if (inAlphabet[b])
|
||||
continue;
|
||||
|
||||
std::string s = addr;
|
||||
s[pos] = static_cast<char>(b);
|
||||
|
||||
std::array<std::uint8_t, 64> outBuf;
|
||||
auto const r = b58_fast::decodeBase58Token(tokType, s, outBuf);
|
||||
BEAST_EXPECT(!r && r.error() == invalidCode);
|
||||
BEAST_EXPECT(b58_ref::decodeBase58Token(s, tokType).empty());
|
||||
BEAST_EXPECT(decodeBase58Token(s, tokType).empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDecodeMaxValue()
|
||||
{
|
||||
testcase("decode_max_value");
|
||||
|
||||
// The largest value of every accepted length ('z' is digit 57) must
|
||||
// decode identically in both implementations. At 52 chars this is the
|
||||
// widest intermediate the fast decoder's bigint ever has to hold.
|
||||
for (std::size_t len = 1; len <= 52; ++len)
|
||||
{
|
||||
std::string const s(len, 'z');
|
||||
|
||||
std::array<std::uint8_t, 64> outBuf;
|
||||
auto const r = b58_fast::detail::b58_to_b256_be(s, outBuf);
|
||||
std::string const ref = b58_ref::detail::decodeBase58(s);
|
||||
if (!BEAST_EXPECT(r))
|
||||
continue;
|
||||
BEAST_EXPECT(
|
||||
std::string(r.value().begin(), r.value().end()) == ref);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testMultiprecision();
|
||||
testFastMatchesRef();
|
||||
testDecodeInvalidChars();
|
||||
testDecodeMaxValue();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1052,67 +1052,6 @@ public:
|
||||
BEAST_EXPECT(sim.synchronized());
|
||||
}
|
||||
|
||||
void
|
||||
testForcedInclusion()
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("forced inclusion");
|
||||
|
||||
// Transaction n * 1000 + k belongs to peer n - 1. Each one is placed
|
||||
// only in a single peer's open set and never relayed, so without
|
||||
// forced inclusion it has one vote in five and stays out.
|
||||
auto const owner = [](Tx const& tx) -> std::optional<PeerID> {
|
||||
if (tx.id() < 1000)
|
||||
return std::nullopt;
|
||||
return PeerID{tx.id() / 1000 - 1};
|
||||
};
|
||||
|
||||
auto const run = [&](bool withHook) {
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
PeerGroup peers = sim.createGroup(5);
|
||||
peers.trustAndConnect(
|
||||
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
|
||||
|
||||
for (Peer* p : peers)
|
||||
{
|
||||
if (withHook)
|
||||
p->txOwner = owner;
|
||||
|
||||
std::uint32_t const base =
|
||||
(static_cast<std::uint32_t>(p->id) + 1) * 1000;
|
||||
p->openTxs.insert(Tx{base + 1});
|
||||
if (p->id == PeerID{4})
|
||||
{
|
||||
// more of its own than it may force
|
||||
p->openTxs.insert(Tx{base + 2});
|
||||
p->openTxs.insert(Tx{base + 3});
|
||||
// one that names another owner, who does not hold it
|
||||
p->openTxs.insert(Tx{1999});
|
||||
}
|
||||
}
|
||||
|
||||
sim.run(1);
|
||||
BEAST_EXPECT(sim.synchronized());
|
||||
return peers[0]->lastClosedLedger.txs();
|
||||
};
|
||||
|
||||
{
|
||||
auto const txs = run(false);
|
||||
BEAST_EXPECT(txs.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto const txs = run(true);
|
||||
// every owner's own transaction, and only the lowest of the
|
||||
// several peer 4 put in its position
|
||||
TxSetType const expected{
|
||||
Tx{1001}, Tx{2001}, Tx{3001}, Tx{4001}, Tx{5001}};
|
||||
BEAST_EXPECTS(txs == expected, to_string(txs));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -1129,7 +1068,6 @@ public:
|
||||
testHubNetwork();
|
||||
testPreferredByBranch();
|
||||
testPauseForLaggards();
|
||||
testForcedInclusion();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
#include <xrpld/app/misc/NegativeUNLVote.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
#include <xrpld/app/tx/applySteps.h>
|
||||
#include <xrpld/app/tx/detail/Export.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpld/ledger/View.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
@@ -1205,229 +1203,7 @@ class UNLReportVoteNewValidator_test : public beast::unit_test::suite
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Regression test: UNLReport proposals must not be gated on the N-UNL local
|
||||
* reliability threshold.
|
||||
*
|
||||
* The local node (validator 0) validates exactly `myVals` of the 256 scored
|
||||
* ledgers, validator 1 validates none (so it is an N-UNL disable candidate and
|
||||
* absent from the report), every other validator validates all of them.
|
||||
*
|
||||
* == myVals <= 128: not active ourselves -> no UNLReport, no UNLModify
|
||||
* == 129 .. 229: UNLReport proposed, not reliable enough to vote on N-UNL
|
||||
* == >= 230: UNLReport and one UNLModify (230 used to fall into the
|
||||
* "Too many!" branch and fail)
|
||||
*/
|
||||
class UNLReportVoteLocalReliability_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testDoVoting()
|
||||
{
|
||||
testcase("Do Voting with local reliability below N-UNL threshold");
|
||||
|
||||
struct Case
|
||||
{
|
||||
std::uint32_t myVals;
|
||||
bool expectReport;
|
||||
bool expectModify;
|
||||
};
|
||||
|
||||
std::array<Case, 6> const cases = {{
|
||||
{128, false, false},
|
||||
{129, true, false},
|
||||
{229, true, false},
|
||||
{230, true, true},
|
||||
{231, true, true},
|
||||
{256, true, true},
|
||||
}};
|
||||
|
||||
for (bool const withVLImport : {true, false})
|
||||
{
|
||||
for (auto const& c : cases)
|
||||
{
|
||||
std::uint32_t const numNodes = 20;
|
||||
URNetworkHistory history = {
|
||||
*this, {numNodes, 0, false, false, withVLImport, {}}};
|
||||
BEAST_EXPECT(history.goodHistory);
|
||||
if (!history.goodHistory)
|
||||
continue;
|
||||
|
||||
// scored ledgers are [lastSeq - 256, lastSeq - 1]
|
||||
auto const lastSeq = history.lastLedger()->seq();
|
||||
history.walkHistoryAndAddValidations(
|
||||
[&](std::shared_ptr<Ledger const> const& l,
|
||||
std::size_t idx) -> bool {
|
||||
if (idx == 1)
|
||||
return false;
|
||||
if (idx != 0)
|
||||
return true;
|
||||
return l->seq() + 256 < lastSeq + c.myVals;
|
||||
});
|
||||
|
||||
NegativeUNLVote vote(
|
||||
history.UNLNodeIDs[0],
|
||||
history.env.journal,
|
||||
history.env.app());
|
||||
auto txSet = std::make_shared<SHAMap>(
|
||||
SHAMapType::TRANSACTION, history.env.app().getNodeFamily());
|
||||
vote.doVoting(
|
||||
history.lastLedger(),
|
||||
history.UNLKeySet,
|
||||
history.validations,
|
||||
txSet);
|
||||
|
||||
std::size_t const expectReport =
|
||||
c.expectReport ? numNodes - 1 + (withVLImport ? 1 : 0) : 0;
|
||||
std::size_t const expectModify = c.expectModify ? 1 : 0;
|
||||
std::string const ctx = "myVals=" + std::to_string(c.myVals) +
|
||||
" withVLImport=" + std::to_string(withVLImport);
|
||||
|
||||
BEAST_EXPECTS(countUNLRTx(txSet) == expectReport, ctx);
|
||||
BEAST_EXPECTS(unl::countTx(txSet) == expectModify, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testDoVoting();
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* featureExport: validators nominating export keys in their validations get
|
||||
* one ttUNL_REPORT ExportKeyReport each, carrying their key and proof.
|
||||
*/
|
||||
class UNLReportVoteExportKeys_test : public beast::unit_test::suite
|
||||
{
|
||||
void
|
||||
testDoVoting()
|
||||
{
|
||||
testcase("Export key reports");
|
||||
|
||||
auto const numNodes = 10;
|
||||
URNetworkHistory history = {
|
||||
*this, {numNodes, 0, false, false, false, {}}};
|
||||
BEAST_EXPECT(history.goodHistory);
|
||||
if (!history.goodHistory)
|
||||
return;
|
||||
|
||||
auto& env = history.env;
|
||||
auto const nid = env.app().config().NETWORK_ID;
|
||||
auto const signer = randomKeyPair(KeyType::secp256k1);
|
||||
|
||||
// every second validator nominates a key
|
||||
std::vector<std::optional<std::pair<PublicKey, SecretKey>>> eks;
|
||||
for (int i = 0; i < numNodes; ++i)
|
||||
eks.push_back(
|
||||
i % 2 ? std::nullopt
|
||||
: std::optional(randomKeyPair(KeyType::ed25519)));
|
||||
|
||||
auto const proofOf = [&](int i) {
|
||||
auto const& [pk, sk] = *eks[i];
|
||||
auto const p = sign(
|
||||
pk,
|
||||
sk,
|
||||
exportKeyProofData(history.UNLKeys[i], pk, nid).slice());
|
||||
return Blob(p.begin(), p.end());
|
||||
};
|
||||
|
||||
// as walkHistoryAndAddValidations, with the nominations attached
|
||||
auto const& ledgers = history.history;
|
||||
std::size_t curr = ledgers.size() > 257 ? ledgers.size() - 257 : 0;
|
||||
for (; curr != ledgers.size(); ++curr)
|
||||
{
|
||||
auto const& l = ledgers[curr];
|
||||
for (int i = 0; i < numNodes; ++i)
|
||||
{
|
||||
RCLValidation v(std::make_shared<STValidation>(
|
||||
env.app().timeKeeper().now(),
|
||||
signer.first,
|
||||
signer.second,
|
||||
history.UNLNodeIDs[i],
|
||||
[&](STValidation& sv) {
|
||||
sv.setFieldH256(sfLedgerHash, l->info().hash);
|
||||
sv.setFieldU32(sfLedgerSequence, l->seq());
|
||||
sv.setFlag(vfFullValidation);
|
||||
if (eks[i])
|
||||
{
|
||||
sv.setFieldVL(sfExportKey, eks[i]->first.slice());
|
||||
sv.setFieldVL(sfExportKeyProof, proofOf(i));
|
||||
}
|
||||
}));
|
||||
v.setTrusted();
|
||||
history.validations.add(history.UNLNodeIDs[i], v);
|
||||
}
|
||||
}
|
||||
|
||||
NegativeUNLVote vote(history.UNLNodeIDs[0], env.journal, env.app());
|
||||
auto txSet = std::make_shared<SHAMap>(
|
||||
SHAMapType::TRANSACTION, env.app().getNodeFamily());
|
||||
vote.doVoting(
|
||||
history.lastLedger(),
|
||||
history.UNLKeySet,
|
||||
history.validations,
|
||||
txSet);
|
||||
|
||||
std::size_t active = 0, reports = 0;
|
||||
for (auto i = txSet->begin(); i != txSet->end(); ++i)
|
||||
{
|
||||
SerialIter sit(i->slice());
|
||||
STTx const tx(sit);
|
||||
if (tx.getTxnType() != ttUNL_REPORT)
|
||||
continue;
|
||||
if (tx.isFieldPresent(sfActiveValidator))
|
||||
++active;
|
||||
if (!tx.isFieldPresent(sfExportKeyReport))
|
||||
continue;
|
||||
|
||||
++reports;
|
||||
BEAST_EXPECT(!tx.isFieldPresent(sfActiveValidator));
|
||||
BEAST_EXPECT(!tx.isFieldPresent(sfImportVLKey));
|
||||
|
||||
auto const& r =
|
||||
tx.peekAtField(sfExportKeyReport).downcast<STObject>();
|
||||
auto const it = std::find(
|
||||
history.UNLKeys.begin(),
|
||||
history.UNLKeys.end(),
|
||||
PublicKey(r[sfPublicKey]));
|
||||
BEAST_EXPECT(it != history.UNLKeys.end());
|
||||
if (it == history.UNLKeys.end())
|
||||
continue;
|
||||
auto const idx = it - history.UNLKeys.begin();
|
||||
BEAST_EXPECT(eks[idx].has_value());
|
||||
if (!eks[idx])
|
||||
continue;
|
||||
BEAST_EXPECT(
|
||||
r.getFieldVL(sfExportKey) ==
|
||||
Blob(eks[idx]->first.begin(), eks[idx]->first.end()));
|
||||
BEAST_EXPECT(r.getFieldVL(sfExportKeyProof) == proofOf(idx));
|
||||
|
||||
// and it passes preflight, proof and all
|
||||
BEAST_EXPECT(
|
||||
preflight(
|
||||
env.app(),
|
||||
history.lastLedger()->rules(),
|
||||
tx,
|
||||
tapNONE,
|
||||
env.journal)
|
||||
.ter == tesSUCCESS);
|
||||
}
|
||||
|
||||
BEAST_EXPECT(active == numNodes);
|
||||
BEAST_EXPECT(reports == numNodes / 2);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testDoVoting();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(UNLReport, ledger, ripple);
|
||||
BEAST_DEFINE_TESTSUITE(UNLReportVoteExportKeys, consensus, ripple);
|
||||
BEAST_DEFINE_TESTSUITE(UNLReportNoAmendment, ledger, ripple);
|
||||
BEAST_DEFINE_TESTSUITE(UNLReportFork, consensus, ripple);
|
||||
BEAST_DEFINE_TESTSUITE_PRIO(UNLReportVoteGoodScore, consensus, ripple, 1);
|
||||
@@ -1439,7 +1215,6 @@ BEAST_DEFINE_TESTSUITE_PRIO(
|
||||
ripple,
|
||||
1);
|
||||
BEAST_DEFINE_TESTSUITE(UNLReportVoteNewValidator, consensus, ripple);
|
||||
BEAST_DEFINE_TESTSUITE(UNLReportVoteLocalReliability, consensus, ripple);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
|
||||
+28
-120
@@ -421,72 +421,6 @@ port_wss_admin
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testEarlyManifestSync()
|
||||
{
|
||||
testcase("early_manifest_sync");
|
||||
|
||||
// On unless turned off.
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("");
|
||||
BEAST_EXPECT(c.EARLY_MANIFEST_SYNC);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, bool>> const values{
|
||||
{"0", false}, {"false", false}, {"1", true}, {"TRUE", true}};
|
||||
for (auto const& [value, expected] : values)
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("[early_manifest_sync]\n" + value + "\n");
|
||||
BEAST_EXPECT(c.EARLY_MANIFEST_SYNC == expected);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("[early_manifest_sync]\nsometimes\n");
|
||||
fail();
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
pass();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
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()
|
||||
{
|
||||
@@ -1168,24 +1102,23 @@ trustthesevalidators.gov
|
||||
Config cfg;
|
||||
/* NOTE: this string includes some explicit
|
||||
* space chars in order to verify proper trimming */
|
||||
std::string toLoad(
|
||||
R"(
|
||||
std::string toLoad(R"(
|
||||
[port_rpc])"
|
||||
"\x20"
|
||||
R"(
|
||||
"\x20"
|
||||
R"(
|
||||
# comment
|
||||
# indented comment
|
||||
)"
|
||||
"\x20\x20"
|
||||
R"(
|
||||
"\x20\x20"
|
||||
R"(
|
||||
[ips])"
|
||||
"\x20"
|
||||
R"(
|
||||
"\x20"
|
||||
R"(
|
||||
r.ripple.com 51235
|
||||
|
||||
[ips_fixed])"
|
||||
"\x20\x20"
|
||||
R"(
|
||||
"\x20\x20"
|
||||
R"(
|
||||
# COMMENT
|
||||
s1.ripple.com 51235
|
||||
s2.ripple.com 51235
|
||||
@@ -1211,24 +1144,23 @@ r.ripple.com 51235
|
||||
Config cfg;
|
||||
/* NOTE: this string includes some explicit
|
||||
* space chars in order to verify proper trimming */
|
||||
std::string toLoad(
|
||||
R"(
|
||||
std::string toLoad(R"(
|
||||
[port_rpc])"
|
||||
"\x20"
|
||||
R"(
|
||||
"\x20"
|
||||
R"(
|
||||
# comment
|
||||
# indented comment
|
||||
)"
|
||||
"\x20\x20"
|
||||
R"(
|
||||
"\x20\x20"
|
||||
R"(
|
||||
[ips])"
|
||||
"\x20"
|
||||
R"(
|
||||
"\x20"
|
||||
R"(
|
||||
r.ripple.com:51235
|
||||
|
||||
[ips_fixed])"
|
||||
"\x20\x20"
|
||||
R"(
|
||||
"\x20\x20"
|
||||
R"(
|
||||
# COMMENT
|
||||
s1.ripple.com:51235
|
||||
s2.ripple.com 51235
|
||||
@@ -1515,8 +1447,8 @@ r.ripple.com:51235
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: RWDB without online_delete NOT in standalone mode
|
||||
// (now allowed — SHAMapStoreImp defaults it to ledger_history)
|
||||
// Test 2: RWDB without online_delete NOT in standalone mode (should
|
||||
// throw)
|
||||
{
|
||||
Config c;
|
||||
std::string toLoad =
|
||||
@@ -1527,11 +1459,15 @@ r.ripple.com:51235
|
||||
try
|
||||
{
|
||||
c.loadFromString(toLoad);
|
||||
pass();
|
||||
fail("Expected exception for RWDB without online_delete");
|
||||
}
|
||||
catch (std::runtime_error const&)
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
fail("Should not throw for RWDB without online_delete");
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find(
|
||||
"RWDB (in-memory backend) requires online_delete") !=
|
||||
std::string::npos);
|
||||
pass();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1677,33 +1613,7 @@ r.ripple.com:51235
|
||||
}
|
||||
}
|
||||
|
||||
// Test 5: type=rwdb with pinned_type (should throw, even in
|
||||
// standalone). The rwdb node store is tree-only, so pinned data
|
||||
// would be accepted and then dropped.
|
||||
for (bool const standalone : {false, true})
|
||||
{
|
||||
Config c;
|
||||
std::string toLoad =
|
||||
"[node_db]\n"
|
||||
"type=rwdb\n"
|
||||
"online_delete=256\n"
|
||||
"pinned_type=NuDB\n"
|
||||
"pinned_path=pinned\n";
|
||||
c.setupControl(true, true, standalone);
|
||||
try
|
||||
{
|
||||
c.loadFromString(toLoad);
|
||||
fail("Expected exception for pinned_type with type=rwdb");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("type=rwdb") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 6: pinned_type without online_delete (should throw)
|
||||
// Test 5: pinned_type without online_delete (should throw)
|
||||
{
|
||||
Config c;
|
||||
std::string toLoad =
|
||||
@@ -1824,8 +1734,6 @@ r.ripple.com:51235
|
||||
testDatabasePinnedValidation();
|
||||
testOverlay();
|
||||
testNetworkID();
|
||||
testEarlyManifestSync();
|
||||
testUdpHighway();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
#include <boost/container/flat_map.hpp>
|
||||
#include <boost/container/flat_set.hpp>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
@@ -259,11 +257,6 @@ struct Peer
|
||||
// Simulation parameters
|
||||
ConsensusParms consensusParms;
|
||||
|
||||
//! Names the peer a transaction belongs to, for Consensus forced
|
||||
//! inclusion (as featureRNG uses for entropy contributions). Unset, it
|
||||
//! behaves exactly like an adaptor without the forcedTxOwner hook.
|
||||
std::function<std::optional<PeerID>(Tx const&)> txOwner;
|
||||
|
||||
//! The collectors to report events to
|
||||
CollectorRefs& collectors;
|
||||
|
||||
@@ -662,14 +655,6 @@ struct Peer
|
||||
return consensusParms;
|
||||
}
|
||||
|
||||
std::optional<PeerID>
|
||||
forcedTxOwner(Tx const& tx, Ledger const&) const
|
||||
{
|
||||
if (!txOwner)
|
||||
return std::nullopt;
|
||||
return txOwner(tx);
|
||||
}
|
||||
|
||||
// Not interested in tracking consensus mode changes for now
|
||||
void
|
||||
onModeChange(ConsensusMode, ConsensusMode)
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/InnerObjectFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
@@ -1235,90 +1234,6 @@ class Invariants_test : public beast::unit_test::suite
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
void
|
||||
testAppLoader()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
static std::string const good = "<html></html>";
|
||||
|
||||
auto insert = [](ApplyContext& ac,
|
||||
AccountID const& owner,
|
||||
uint256 const& key,
|
||||
std::string const& blob,
|
||||
std::uint32_t seq) {
|
||||
auto sle = std::make_shared<SLE>(Keylet{ltAPP_LOADER, key});
|
||||
sle->setAccountID(sfOwner, owner);
|
||||
sle->setFieldVL(sfAppLoader, Slice(blob.data(), blob.size()));
|
||||
sle->setFieldU32(sfAppLoaderSequence, seq);
|
||||
ac.view().insert(sle);
|
||||
auto root = ac.view().peek(keylet::account(owner));
|
||||
root->setFieldU32(sfAppLoaderSequence, seq);
|
||||
ac.view().update(root);
|
||||
return true;
|
||||
};
|
||||
Preclose publish = [](Account const& A1, Account const&, Env& env) {
|
||||
auto jt = noop(A1);
|
||||
jt[sfAppLoader.fieldName] = strHex(good);
|
||||
env(jt, fee(XRP(1)));
|
||||
return bool(env.le(keylet::appLoader(A1.id())));
|
||||
};
|
||||
auto bad = [&](char const* log,
|
||||
Precheck const& pre,
|
||||
std::uint16_t txType = ttACCOUNT_SET,
|
||||
Preclose const& preclose = {}) {
|
||||
doInvariantCheck(
|
||||
{{log}},
|
||||
pre,
|
||||
XRPAmount{},
|
||||
STTx{TxType(txType), [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
preclose);
|
||||
};
|
||||
|
||||
testcase << "AppLoader at the wrong key";
|
||||
bad("AppLoader is not keyed at its owner's keylet",
|
||||
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
return insert(ac, A1, keylet::appLoader(A2).key, good, 1);
|
||||
});
|
||||
testcase << "AppLoader with invalid blob";
|
||||
bad("AppLoader blob does not validate",
|
||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||
return insert(ac, A1, keylet::appLoader(A1).key, "nope", 1);
|
||||
});
|
||||
testcase << "AppLoader blob over the size limit";
|
||||
bad("AppLoader blob does not validate",
|
||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||
return insert(
|
||||
ac,
|
||||
A1,
|
||||
keylet::appLoader(A1).key,
|
||||
"<html>" + std::string(maxAppLoaderLength, 'x') + "</html>",
|
||||
1);
|
||||
});
|
||||
testcase << "AppLoader sequence differs from its owner's";
|
||||
bad(
|
||||
"AppLoader sequence does not match its owner's",
|
||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::appLoader(A1));
|
||||
sle->setFieldU32(sfAppLoaderSequence, 7);
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
ttACCOUNT_SET,
|
||||
publish);
|
||||
testcase << "AppLoader changed by the wrong transaction type";
|
||||
bad(
|
||||
"AppLoader changed by a transaction other than AccountSet or "
|
||||
"AccountDelete",
|
||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::appLoader(A1));
|
||||
ac.view().erase(sle);
|
||||
return true;
|
||||
},
|
||||
ttPAYMENT,
|
||||
publish);
|
||||
}
|
||||
|
||||
void
|
||||
testLockedBalance()
|
||||
{
|
||||
@@ -1374,7 +1289,6 @@ public:
|
||||
testNFTokenPageInvariants();
|
||||
testPermissionedDomainInvariants();
|
||||
testLockedBalance();
|
||||
testAppLoader();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -82,35 +82,6 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
if (type == "rwdb")
|
||||
{
|
||||
std::unique_ptr<Backend> backend = Manager::instance().make_Backend(
|
||||
params, megabytes(4), scheduler, journal);
|
||||
backend->open();
|
||||
try
|
||||
{
|
||||
backend->open();
|
||||
fail("rwdb open twice should throw");
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
pass();
|
||||
}
|
||||
|
||||
storeBatch(*backend, batch);
|
||||
backend->close();
|
||||
BEAST_EXPECT(!backend->isOpen());
|
||||
|
||||
std::shared_ptr<NodeObject> fetched;
|
||||
BEAST_EXPECT(
|
||||
backend->fetch(batch.front()->getHash().data(), &fetched) ==
|
||||
notFound);
|
||||
backend->store(batch.front());
|
||||
int visited = 0;
|
||||
backend->for_each([&visited](auto const&) { ++visited; });
|
||||
BEAST_EXPECT(visited == 0);
|
||||
}
|
||||
|
||||
// rwdb backend does not keep table/data after close
|
||||
if (type != "rwdb")
|
||||
{
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
#include <xrpld/nodestore/detail/DatabasePinnedImp.h>
|
||||
#include <xrpl/basics/Buffer.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/beast/utility/temp_dir.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
namespace ripple {
|
||||
@@ -34,16 +33,13 @@ namespace test {
|
||||
class DatabasePinned_test : public beast::unit_test::suite
|
||||
{
|
||||
// Build an Env whose nodestore is wired up as DatabasePinnedImp.
|
||||
// The rotating store must persist (type=memory). type=rwdb is
|
||||
// NullFactory and never builds DatabasePinnedImp. pinned_type=rwdb
|
||||
// is allowed in standalone mode.
|
||||
// pinned_type=rwdb is allowed in standalone mode (the catalogue
|
||||
// tests do the same).
|
||||
static std::unique_ptr<Config>
|
||||
pinnedEnvconfig(std::string const& nodePath)
|
||||
pinnedEnvconfig()
|
||||
{
|
||||
auto cfg = jtx::envconfig();
|
||||
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
|
||||
nodeDb.set("type", "memory");
|
||||
nodeDb.set("path", nodePath);
|
||||
nodeDb.set("pinned_type", "rwdb");
|
||||
nodeDb.set("online_delete", "256");
|
||||
return cfg;
|
||||
@@ -88,8 +84,7 @@ class DatabasePinned_test : public beast::unit_test::suite
|
||||
testcase("store routing by NodeObjectType");
|
||||
|
||||
using namespace jtx;
|
||||
beast::temp_dir td;
|
||||
Env env{*this, pinnedEnvconfig(td.path())};
|
||||
Env env{*this, pinnedEnvconfig()};
|
||||
auto* db = getPinnedDb(env);
|
||||
if (!db)
|
||||
return;
|
||||
@@ -141,8 +136,7 @@ class DatabasePinned_test : public beast::unit_test::suite
|
||||
testcase("store/fetch round-trip across both backends");
|
||||
|
||||
using namespace jtx;
|
||||
beast::temp_dir td;
|
||||
Env env{*this, pinnedEnvconfig(td.path())};
|
||||
Env env{*this, pinnedEnvconfig()};
|
||||
auto& nodeStore = env.app().getNodeStore();
|
||||
|
||||
// Store one object of each type with distinct content so the
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 Ripple Labs Inc.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpld/overlay/Compression.h>
|
||||
#include <xrpld/overlay/Message.h>
|
||||
#include <xrpld/overlay/detail/OverlayImpl.h>
|
||||
#include <xrpld/overlay/detail/PeerImp.h>
|
||||
#include <xrpld/shamap/SHAMapInnerNode.h>
|
||||
#include <xrpld/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/basics/make_SSLContext.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/messages.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class GetObjectByHash_test : public beast::unit_test::suite
|
||||
{
|
||||
using socket_type = boost::asio::ip::tcp::socket;
|
||||
using middle_type = boost::beast::tcp_stream;
|
||||
using stream_type = boost::beast::ssl_stream<middle_type>;
|
||||
|
||||
class PeerTest : public PeerImp
|
||||
{
|
||||
public:
|
||||
std::vector<std::shared_ptr<Message>> sent;
|
||||
|
||||
PeerTest(
|
||||
Application& app,
|
||||
std::shared_ptr<PeerFinder::Slot> const& slot,
|
||||
http_request_type&& request,
|
||||
PublicKey const& publicKey,
|
||||
ProtocolVersion protocol,
|
||||
Resource::Consumer consumer,
|
||||
std::unique_ptr<GetObjectByHash_test::stream_type>&& stream_ptr,
|
||||
OverlayImpl& overlay)
|
||||
: PeerImp(
|
||||
app,
|
||||
1,
|
||||
slot,
|
||||
std::move(request),
|
||||
publicKey,
|
||||
protocol,
|
||||
consumer,
|
||||
std::move(stream_ptr),
|
||||
overlay)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
send(std::shared_ptr<Message> const& m) override
|
||||
{
|
||||
sent.push_back(m);
|
||||
}
|
||||
};
|
||||
|
||||
static auto
|
||||
nullBackend(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
cfg->LEDGER_HISTORY = 8;
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("type", "rwdb");
|
||||
section.set("path", "main");
|
||||
return cfg;
|
||||
}
|
||||
|
||||
std::shared_ptr<PeerTest>
|
||||
makePeer(jtx::Env& env)
|
||||
{
|
||||
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
|
||||
http_request_type request;
|
||||
auto stream_ptr = std::make_unique<stream_type>(
|
||||
socket_type(env.app().getIOContext()), *context_);
|
||||
beast::IP::Endpoint const local(
|
||||
boost::asio::ip::make_address("172.16.0.1"));
|
||||
beast::IP::Endpoint const remote(
|
||||
boost::asio::ip::make_address("172.16.0.2"));
|
||||
PublicKey const key(std::get<0>(randomKeyPair(KeyType::ed25519)));
|
||||
auto consumer = overlay.resourceManager().newInboundEndpoint(remote);
|
||||
auto slot = overlay.peerFinder().new_inbound_slot(local, remote);
|
||||
auto peer = std::make_shared<PeerTest>(
|
||||
env.app(),
|
||||
slot,
|
||||
std::move(request),
|
||||
key,
|
||||
ProtocolVersion{1, 7},
|
||||
consumer,
|
||||
std::move(stream_ptr),
|
||||
overlay);
|
||||
overlay.add_active(peer);
|
||||
return peer;
|
||||
}
|
||||
|
||||
std::optional<protocol::TMGetObjectByHash>
|
||||
parseReply(std::shared_ptr<Message> const& message)
|
||||
{
|
||||
if (!message)
|
||||
return {};
|
||||
auto const& buf = message->getBuffer(compression::Compressed::Off);
|
||||
if (buf.size() <= compression::headerBytes)
|
||||
return {};
|
||||
protocol::TMGetObjectByHash reply;
|
||||
if (!reply.ParseFromArray(
|
||||
buf.data() + compression::headerBytes,
|
||||
static_cast<int>(buf.size() - compression::headerBytes)))
|
||||
return {};
|
||||
return reply;
|
||||
}
|
||||
|
||||
std::shared_ptr<boost::asio::ssl::context> context_ = make_SSLContext("");
|
||||
|
||||
public:
|
||||
void
|
||||
testServesLinkedAndCachedNodes()
|
||||
{
|
||||
testcase(
|
||||
"GetObjectByHash serves cache, linked tree, and ledger header");
|
||||
|
||||
using namespace jtx;
|
||||
Env env(*this, envconfig(nullBackend));
|
||||
Account const alice{"alice"};
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env(pay(alice, bob, XRP(1)));
|
||||
env.close();
|
||||
|
||||
auto const ledger = env.app().getLedgerMaster().getClosedLedger();
|
||||
if (!BEAST_EXPECT(ledger))
|
||||
return;
|
||||
|
||||
auto const stateHash = ledger->stateMap().getHash().as_uint256();
|
||||
auto const txHash = ledger->txMap().getHash().as_uint256();
|
||||
auto const headerHash = ledger->info().hash;
|
||||
auto const seq = ledger->info().seq;
|
||||
auto const rootId = SHAMapNodeID{}.getRawString();
|
||||
|
||||
auto peer = makePeer(env);
|
||||
auto sendQuery = [&](auto&& fill) {
|
||||
peer->sent.clear();
|
||||
auto packet = std::make_shared<protocol::TMGetObjectByHash>();
|
||||
packet->set_query(true);
|
||||
fill(*packet);
|
||||
// Production dispatch resets fee_ in onMessageBegin before
|
||||
// onMessage. Calling onMessage alone leaves a moderate burden
|
||||
// charge in place and Debug XRPL_ASSERT fires on a later
|
||||
// malformed request with a smaller fee.
|
||||
auto const size = packet->ByteSizeLong();
|
||||
peer->onMessageBegin(
|
||||
protocol::mtGET_OBJECTS, packet, size, size, false);
|
||||
peer->onMessage(packet);
|
||||
peer->onMessageEnd(protocol::mtGET_OBJECTS, packet);
|
||||
};
|
||||
|
||||
// TreeNodeCache hit: hash only, no nodeid.
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(stateHash.begin(), stateHash.size());
|
||||
obj.set_ledgerseq(seq);
|
||||
});
|
||||
if (auto const cached =
|
||||
env.app().getNodeFamily().getTreeNodeCache()->fetch(stateHash))
|
||||
{
|
||||
if (!BEAST_EXPECT(peer->sent.size() == 1))
|
||||
return;
|
||||
auto const reply = parseReply(peer->sent.front());
|
||||
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
|
||||
return;
|
||||
BEAST_EXPECT(uint256{reply->objects(0).hash()} == stateHash);
|
||||
}
|
||||
|
||||
// After dropping the interned cache, the same request needs a nodeid
|
||||
// so fetchLinkedTreeNode can walk the retained SHAMap.
|
||||
env.app().getNodeFamily().getTreeNodeCache()->reset();
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(stateHash.begin(), stateHash.size());
|
||||
obj.set_ledgerseq(seq);
|
||||
});
|
||||
{
|
||||
if (!BEAST_EXPECT(peer->sent.size() == 1))
|
||||
return;
|
||||
auto const reply = parseReply(peer->sent.front());
|
||||
if (!BEAST_EXPECT(reply))
|
||||
return;
|
||||
BEAST_EXPECT(reply->objects_size() == 0);
|
||||
}
|
||||
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(stateHash.begin(), stateHash.size());
|
||||
obj.set_nodeid(rootId);
|
||||
obj.set_ledgerseq(seq);
|
||||
});
|
||||
{
|
||||
if (!BEAST_EXPECT(peer->sent.size() == 1))
|
||||
return;
|
||||
auto const reply = parseReply(peer->sent.front());
|
||||
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
|
||||
return;
|
||||
BEAST_EXPECT(uint256{reply->objects(0).hash()} == stateHash);
|
||||
BEAST_EXPECT(reply->objects(0).index() == rootId);
|
||||
BEAST_EXPECT(reply->objects(0).ledgerseq() == seq);
|
||||
}
|
||||
|
||||
// Transaction map root, found after the state-map miss.
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otTRANSACTION_NODE);
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(txHash.begin(), txHash.size());
|
||||
obj.set_nodeid(rootId);
|
||||
obj.set_ledgerseq(seq);
|
||||
});
|
||||
{
|
||||
if (!BEAST_EXPECT(peer->sent.size() == 1))
|
||||
return;
|
||||
auto const reply = parseReply(peer->sent.front());
|
||||
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
|
||||
return;
|
||||
BEAST_EXPECT(uint256{reply->objects(0).hash()} == txHash);
|
||||
}
|
||||
|
||||
// Linked walk finds the node, hash does not match, object omitted.
|
||||
uint256 const wrong{2};
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otSTATE_NODE);
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(wrong.begin(), wrong.size());
|
||||
obj.set_nodeid(rootId);
|
||||
obj.set_ledgerseq(seq);
|
||||
});
|
||||
{
|
||||
if (!BEAST_EXPECT(peer->sent.size() == 1))
|
||||
return;
|
||||
auto const reply = parseReply(peer->sent.front());
|
||||
if (!BEAST_EXPECT(reply))
|
||||
return;
|
||||
BEAST_EXPECT(reply->objects_size() == 0);
|
||||
}
|
||||
|
||||
// Ledger header fallback when the node store and tree caches miss.
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otLEDGER);
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(headerHash.begin(), headerHash.size());
|
||||
obj.set_ledgerseq(seq);
|
||||
});
|
||||
{
|
||||
if (!BEAST_EXPECT(peer->sent.size() == 1))
|
||||
return;
|
||||
auto const reply = parseReply(peer->sent.front());
|
||||
if (!BEAST_EXPECT(reply && reply->objects_size() == 1))
|
||||
return;
|
||||
BEAST_EXPECT(uint256{reply->objects(0).hash()} == headerHash);
|
||||
BEAST_EXPECT(!reply->objects(0).data().empty());
|
||||
}
|
||||
|
||||
// Malformed ledger hash is rejected before a reply is sent.
|
||||
sendQuery([&](protocol::TMGetObjectByHash& packet) {
|
||||
packet.set_type(protocol::TMGetObjectByHash::otLEDGER);
|
||||
packet.set_ledgerhash("short");
|
||||
auto& obj = *packet.add_objects();
|
||||
obj.set_hash(headerHash.begin(), headerHash.size());
|
||||
});
|
||||
BEAST_EXPECT(peer->sent.empty());
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testServesLinkedAndCachedNodes();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(GetObjectByHash, overlay, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -1,542 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/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
|
||||
@@ -1,313 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/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
|
||||
@@ -1,466 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/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
|
||||
@@ -1,276 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/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
|
||||
@@ -1,198 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpl/hook/Enum.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
// A valid hook exporting both hook() and cbak() (emit test hook).
|
||||
static char const validHex[] =
|
||||
"0061736D0100000001250660027F7F017F60037F7F7E017E60017F017E600001"
|
||||
"7E60027F7F017E60047F7F7F7F017E028F010903656E76025F67000003656E76"
|
||||
"06616363657074000103656E760C6574786E5F72657365727665000203656E76"
|
||||
"0A6C65646765725F736571000303656E760C686F6F6B5F6163636F756E740004"
|
||||
"03656E760C6574786E5F64657461696C73000403656E760D6574786E5F666565"
|
||||
"5F62617365000403656E7604656D6974000503656E7608726F6C6C6261636B00"
|
||||
"01030302020205030100020627067F0141908A040B7F0041820A0B7F00418008"
|
||||
"0B7F0041908A040B7F004180080B7F004180080B070F02046362616B00090468"
|
||||
"6F6F6B000A0AA883000299800000410141011080808080001A41004100420010"
|
||||
"81808080000B88830002037F017E23808080800041206B220124808080800041"
|
||||
"0141011080808080001A41011082808080001A4100108380808000A722024105"
|
||||
"6A22034118742003410874418080FC07717220034108764180FE037120034118"
|
||||
"767272360295888080004100200241016A22034118742003410874418080FC07"
|
||||
"717220034108764180FE03712003411876727236028F8880800041C788808000"
|
||||
"41141084808080001A41DB88808000418A011085808080001A41004180888080"
|
||||
"0041E50110868080800022043C00A188808000410020044208883C00A0888080"
|
||||
"00410020044210883C009F88808000410020044218883C009E88808000410020"
|
||||
"044220883C009D88808000410020044228883C009C8880800041002004423088"
|
||||
"3C009B8880800041002004423888A7413F7141C000723A009A88808000024002"
|
||||
"402001412041808880800041E501108780808000427F550D0041E58980800041"
|
||||
"0D42D30010888080800021040C010B41F289808000411042D400108180808000"
|
||||
"21040B200141206A24808080800020040B0B900202004180080BE50112006322"
|
||||
"000000002400000000201A00000000201B000000006840000000000000007321"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0081140000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"000041E5090B1D456D6974206661696C65642E00456D69742073756363656564"
|
||||
"65642E00";
|
||||
|
||||
class HookValidate_test : public beast::unit_test::suite
|
||||
{
|
||||
static Json::Value
|
||||
call(test::jtx::Env& env, std::string const& code)
|
||||
{
|
||||
Json::Value params(Json::objectValue);
|
||||
params[jss::code] = code;
|
||||
return env.rpc("json", "hook_validate", to_string(params))[jss::result];
|
||||
}
|
||||
|
||||
static std::unique_ptr<Config>
|
||||
withRpc(std::unique_ptr<Config> cfg, bool enabled)
|
||||
{
|
||||
cfg->HOOK_VALIDATE_RPC = enabled;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void
|
||||
testDisabled()
|
||||
{
|
||||
testcase("Disabled");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, envconfig(withRpc, false)};
|
||||
auto const r = call(env, validHex);
|
||||
BEAST_EXPECT(r[jss::error] == "notSupported");
|
||||
}
|
||||
|
||||
void
|
||||
testParams()
|
||||
{
|
||||
testcase("Parameter validation");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, envconfig(withRpc, true)};
|
||||
|
||||
{
|
||||
// missing code
|
||||
auto const r = env.rpc("json", "hook_validate", "{}")[jss::result];
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
}
|
||||
{
|
||||
// code not a string
|
||||
auto const r =
|
||||
env.rpc("json", "hook_validate", "{\"code\": 1}")[jss::result];
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
}
|
||||
{
|
||||
// empty, odd-length and non-hex input
|
||||
for (auto const& bad : {"", "0", "0061736Z"})
|
||||
{
|
||||
auto const r = call(env, bad);
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
}
|
||||
}
|
||||
{
|
||||
// one byte over the maximum hook size
|
||||
auto const r =
|
||||
call(env, std::string(2 * (hook::maxHookWasmSize() + 1), '0'));
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testValidate()
|
||||
{
|
||||
testcase("Validate");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, envconfig(withRpc, true)};
|
||||
|
||||
{
|
||||
auto const r = call(env, validHex);
|
||||
BEAST_EXPECT(r[jss::valid] == true);
|
||||
BEAST_EXPECT(r.isMember(jss::instruction_count_hook));
|
||||
BEAST_EXPECT(r.isMember(jss::instruction_count_cbak));
|
||||
BEAST_EXPECT(!r.isMember(jss::vm_error));
|
||||
BEAST_EXPECT(r[jss::log].isArray());
|
||||
}
|
||||
{
|
||||
// the lowercase hex the toolchains emit is accepted too
|
||||
std::string lower = validHex;
|
||||
for (auto& c : lower)
|
||||
c = std::tolower(static_cast<unsigned char>(c));
|
||||
auto const r = call(env, lower);
|
||||
BEAST_EXPECT(r[jss::valid] == true);
|
||||
}
|
||||
{
|
||||
// not webassembly
|
||||
auto const r = call(env, std::string(140, '0'));
|
||||
BEAST_EXPECT(r[jss::valid] == false);
|
||||
BEAST_EXPECT(!r.isMember(jss::instruction_count_hook));
|
||||
BEAST_EXPECT(r[jss::log].isArray() && r[jss::log].size() > 0);
|
||||
}
|
||||
{
|
||||
std::string bad = validHex;
|
||||
bad[2] = 'F'; // corrupt magic header
|
||||
auto const r = call(env, bad);
|
||||
BEAST_EXPECT(r[jss::valid] == false);
|
||||
BEAST_EXPECT(r[jss::log].isArray() && r[jss::log].size() > 0);
|
||||
}
|
||||
{
|
||||
// truncated module
|
||||
std::string const truncated = std::string(validHex).substr(0, 200);
|
||||
auto const r = call(env, truncated);
|
||||
BEAST_EXPECT(r[jss::valid] == false);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testConfigDefault()
|
||||
{
|
||||
testcase("Config default");
|
||||
auto load = [](bool standalone, char const* content) {
|
||||
Config cfg;
|
||||
cfg.setupControl(true, true, standalone);
|
||||
cfg.loadFromString(content);
|
||||
return cfg.HOOK_VALIDATE_RPC;
|
||||
};
|
||||
BEAST_EXPECT(load(true, ""));
|
||||
BEAST_EXPECT(!load(true, "[hook_validate_rpc]\n0\n"));
|
||||
BEAST_EXPECT(!load(false, ""));
|
||||
BEAST_EXPECT(load(false, "[hook_validate_rpc]\n1\n"));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testConfigDefault();
|
||||
testDisabled();
|
||||
testParams();
|
||||
testValidate();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(HookValidate, rpc, ripple);
|
||||
|
||||
} // namespace ripple
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2998,50 +2998,6 @@ static RPCCallTestData const rpcCallTestArray[] = {
|
||||
RPCCallTestData::bad_cast,
|
||||
R"()"},
|
||||
|
||||
// hook_validate
|
||||
// ------------------------------------------------------------------------
|
||||
{"hook_validate: minimal.",
|
||||
__LINE__,
|
||||
{"hook_validate", "0061736D01000000"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "hook_validate",
|
||||
"params" : [
|
||||
{
|
||||
"api_version" : %API_VER%,
|
||||
"code" : "0061736D01000000"
|
||||
}
|
||||
]
|
||||
})"},
|
||||
{"hook_validate: too few arguments.",
|
||||
__LINE__,
|
||||
{"hook_validate"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "hook_validate",
|
||||
"params" : [
|
||||
{
|
||||
"error" : "badSyntax",
|
||||
"error_code" : 1,
|
||||
"error_message" : "Syntax error."
|
||||
}
|
||||
]
|
||||
})"},
|
||||
{"hook_validate: too many arguments.",
|
||||
__LINE__,
|
||||
{"hook_validate", "00", "00"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "hook_validate",
|
||||
"params" : [
|
||||
{
|
||||
"error" : "badSyntax",
|
||||
"error_code" : 1,
|
||||
"error_message" : "Syntax error."
|
||||
}
|
||||
]
|
||||
})"},
|
||||
|
||||
// json
|
||||
// ------------------------------------------------------------------------
|
||||
{"json: minimal.",
|
||||
|
||||
@@ -103,35 +103,6 @@ public:
|
||||
BEAST_EXPECT(fieldNames.insert(name).second);
|
||||
}
|
||||
}
|
||||
|
||||
// hook api, keylet types and return codes
|
||||
{
|
||||
auto const& hook = result[jss::result][jss::HOOK];
|
||||
BEAST_EXPECT(hook.isMember(jss::API));
|
||||
BEAST_EXPECT(hook.isMember(jss::KEYLET_TYPES));
|
||||
BEAST_EXPECT(hook.isMember(jss::RETURN_CODES));
|
||||
|
||||
auto const& accept = hook[jss::API]["accept"];
|
||||
BEAST_EXPECT(accept["return"] == "int64_t");
|
||||
BEAST_EXPECT(accept["params"].size() == 3);
|
||||
BEAST_EXPECT(accept["params"][0u][jss::type] == "uint32_t");
|
||||
BEAST_EXPECT(accept["params"][0u][jss::name] == "read_ptr");
|
||||
BEAST_EXPECT(accept["params"][2u][jss::type] == "int64_t");
|
||||
BEAST_EXPECT(accept["params"][2u][jss::name] == "error_code");
|
||||
BEAST_EXPECT(!accept.isMember("amendment"));
|
||||
|
||||
auto const& burden = hook[jss::API]["etxn_burden"];
|
||||
BEAST_EXPECT(burden["params"].size() == 0);
|
||||
|
||||
auto const& xpop = hook[jss::API]["xpop_slot"];
|
||||
BEAST_EXPECT(xpop["amendment"] == "HooksUpdate1");
|
||||
|
||||
BEAST_EXPECT(hook[jss::KEYLET_TYPES]["HOOK"] == 1);
|
||||
BEAST_EXPECT(hook[jss::KEYLET_TYPES]["MANIFEST"] == 37);
|
||||
BEAST_EXPECT(hook[jss::RETURN_CODES]["SUCCESS"] == 0);
|
||||
BEAST_EXPECT(hook[jss::RETURN_CODES]["INVALID_FLOAT"] == -10024);
|
||||
BEAST_EXPECT(hook[jss::RETURN_CODES]["TOO_MANY_NAMESPACES"] == -45);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -161,7 +132,6 @@ public:
|
||||
BEAST_EXPECT(
|
||||
!result[jss::result].isMember(jss::TRANSACTION_FLAGS_INDICES));
|
||||
BEAST_EXPECT(!result[jss::result].isMember(jss::TYPES));
|
||||
BEAST_EXPECT(!result[jss::result].isMember(jss::HOOK));
|
||||
BEAST_EXPECT(result[jss::result].isMember(jss::hash));
|
||||
}
|
||||
|
||||
@@ -186,7 +156,6 @@ public:
|
||||
BEAST_EXPECT(
|
||||
result[jss::result].isMember(jss::TRANSACTION_FLAGS_INDICES));
|
||||
BEAST_EXPECT(result[jss::result].isMember(jss::TYPES));
|
||||
BEAST_EXPECT(result[jss::result].isMember(jss::HOOK));
|
||||
BEAST_EXPECT(result[jss::result].isMember(jss::hash));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,14 +847,6 @@ public:
|
||||
Account alice("alice");
|
||||
Account bob("bob");
|
||||
Account carol("carol");
|
||||
|
||||
// Account history loads ledgers by seq. type=rwdb is tree-only
|
||||
// and cannot reconstruct those maps from the node store.
|
||||
auto storing = [] {
|
||||
auto cfg = envconfig();
|
||||
cfg->section(ConfigSection::nodeDatabase()).set("type", "memory");
|
||||
return cfg;
|
||||
};
|
||||
Account david("david");
|
||||
///////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -1023,7 +1015,7 @@ public:
|
||||
*
|
||||
* also test subscribe to the account before it is created
|
||||
*/
|
||||
Env env(*this, storing());
|
||||
Env env(*this);
|
||||
auto wscTxHistory = makeWSClient(env.app().config());
|
||||
Json::Value request;
|
||||
request[jss::account_history_tx_stream] = Json::objectValue;
|
||||
@@ -1069,7 +1061,7 @@ public:
|
||||
* subscribe genesis account tx history without txns
|
||||
* subscribe to bob's account after it is created
|
||||
*/
|
||||
Env env(*this, storing());
|
||||
Env env(*this);
|
||||
auto wscTxHistory = makeWSClient(env.app().config());
|
||||
Json::Value request;
|
||||
request[jss::account_history_tx_stream] = Json::objectValue;
|
||||
@@ -1148,7 +1140,7 @@ public:
|
||||
* subscribe account and subscribe account tx history
|
||||
* and compare txns streamed
|
||||
*/
|
||||
Env env(*this, storing());
|
||||
Env env(*this);
|
||||
auto wscAccount = makeWSClient(env.app().config());
|
||||
auto wscTxHistory = makeWSClient(env.app().config());
|
||||
|
||||
@@ -1219,7 +1211,7 @@ public:
|
||||
* alice issues USD to carol
|
||||
* mix USD and XRP payments
|
||||
*/
|
||||
Env env(*this, storing());
|
||||
Env env(*this);
|
||||
auto const USD_a = alice["USD"];
|
||||
|
||||
std::array<Account, 2> accounts = {alice, carol};
|
||||
@@ -1258,7 +1250,7 @@ public:
|
||||
/*
|
||||
* long transaction history
|
||||
*/
|
||||
Env env(*this, storing());
|
||||
Env env(*this);
|
||||
std::array<Account, 2> accounts = {alice, carol};
|
||||
env.fund(XRP(444444), accounts);
|
||||
env.close();
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
#include <boost/utility/in_place_factory.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
@@ -160,7 +158,6 @@ public:
|
||||
onUDPMessage(
|
||||
std::string const& message,
|
||||
boost::asio::ip::tcp::endpoint const& remoteEndpoint,
|
||||
Port const& port,
|
||||
std::function<void(std::string const&)> sendResponse)
|
||||
{
|
||||
}
|
||||
@@ -374,7 +371,6 @@ public:
|
||||
onUDPMessage(
|
||||
std::string const& message,
|
||||
boost::asio::ip::tcp::endpoint const& remoteEndpoint,
|
||||
Port const& port,
|
||||
std::function<void(std::string const&)> sendResponse)
|
||||
{
|
||||
}
|
||||
@@ -408,96 +404,6 @@ 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()
|
||||
{
|
||||
@@ -641,7 +547,6 @@ public:
|
||||
{
|
||||
basicTests();
|
||||
stressTest();
|
||||
udpHighwayTests();
|
||||
testBadConfig();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,13 +20,11 @@
|
||||
#include <test/shamap/common.h>
|
||||
#include <test/unit_test/SuiteJournal.h>
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpld/shamap/SHAMapInnerNode.h>
|
||||
#include <xrpld/shamap/SHAMapItem.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/beast/xor_shift_engine.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
namespace ripple {
|
||||
namespace tests {
|
||||
@@ -86,53 +84,12 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
testNullBackendIgnoresFullBelow(beast::Journal const& journal)
|
||||
{
|
||||
testcase("null backend does not skip children via FullBelowCache");
|
||||
|
||||
TestNodeFamily sourceFamily(journal);
|
||||
TestNodeFamily destFamily(journal);
|
||||
destFamily.setNullBackend(true);
|
||||
|
||||
SHAMap source(SHAMapType::FREE, sourceFamily);
|
||||
for (int i = 0; i < 32; ++i)
|
||||
source.addItem(SHAMapNodeType::tnACCOUNT_STATE, makeRandomAS());
|
||||
source.setImmutable();
|
||||
auto const rootHash = source.getHash();
|
||||
|
||||
auto const root = source.getLinkedNode(SHAMapNodeID{});
|
||||
if (!BEAST_EXPECT(root && root->isInner()))
|
||||
return;
|
||||
auto* inner = static_cast<SHAMapInnerNode*>(root.get());
|
||||
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
|
||||
{
|
||||
if (!inner->isEmptyBranch(b))
|
||||
{
|
||||
destFamily.getFullBelowCache()->insert(
|
||||
inner->getChildHash(b).as_uint256());
|
||||
}
|
||||
}
|
||||
|
||||
SHAMap destination(SHAMapType::FREE, destFamily);
|
||||
destination.setSynching();
|
||||
Serializer s;
|
||||
source.serializeRoot(s);
|
||||
BEAST_EXPECT(
|
||||
destination.addRootNode(rootHash, s.slice(), nullptr).isGood());
|
||||
|
||||
auto const missing = destination.getMissingNodes(2048, nullptr);
|
||||
BEAST_EXPECT(!missing.empty());
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
using namespace beast::severities;
|
||||
test::SuiteJournal journal("SHAMapSync_test", *this);
|
||||
|
||||
testNullBackendIgnoresFullBelow(journal);
|
||||
|
||||
TestNodeFamily f(journal), f2(journal);
|
||||
SHAMap source(SHAMapType::FREE, f);
|
||||
SHAMap destination(SHAMapType::FREE, f2);
|
||||
|
||||
@@ -19,17 +19,11 @@
|
||||
|
||||
#include <test/shamap/common.h>
|
||||
#include <test/unit_test/SuiteJournal.h>
|
||||
#include <xrpld/nodestore/NodeObject.h>
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpld/shamap/SHAMapInnerNode.h>
|
||||
#include <xrpld/shamap/SHAMapTreeNode.h>
|
||||
#include <xrpl/basics/Blob.h>
|
||||
#include <xrpl/basics/Buffer.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
namespace tests {
|
||||
@@ -128,224 +122,6 @@ public:
|
||||
|
||||
run(true, journal);
|
||||
run(false, journal);
|
||||
testCanonicalizeMergesInnerChildren(journal);
|
||||
testCanonicalizeSharesDuplicateContent(journal);
|
||||
testGetLinkedNode(journal);
|
||||
}
|
||||
|
||||
void
|
||||
testCanonicalizeSharesDuplicateContent(beast::Journal const& journal)
|
||||
{
|
||||
testcase("canonicalize shares nodes of identical maps");
|
||||
|
||||
tests::TestNodeFamily f(journal);
|
||||
SHAMap a{SHAMapType::FREE, f};
|
||||
SHAMap b{SHAMapType::FREE, f};
|
||||
|
||||
constexpr uint256 h1(
|
||||
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
constexpr uint256 h2(
|
||||
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
|
||||
for (auto* map : {&a, &b})
|
||||
{
|
||||
BEAST_EXPECT(map->addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h1, IntToVUC(1))));
|
||||
BEAST_EXPECT(map->addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h2, IntToVUC(2))));
|
||||
}
|
||||
|
||||
// Do not call getHash() first: it unshares the map and flushDirty
|
||||
// then has nothing to write.
|
||||
auto const harvestedBefore = SHAMap::canonicalInnerBranchesHarvested();
|
||||
a.flushDirty(hotTRANSACTION_NODE);
|
||||
b.flushDirty(hotTRANSACTION_NODE);
|
||||
|
||||
// b's root and leaves were canonicalized onto a's. a's root was
|
||||
// already fully linked, so there was nothing to harvest, and the
|
||||
// leaf duplicates take the non-inner early return.
|
||||
auto const rootA = a.getLinkedNode(SHAMapNodeID{});
|
||||
auto const rootB = b.getLinkedNode(SHAMapNodeID{});
|
||||
if (!BEAST_EXPECT(rootA && rootB && rootA->isInner()))
|
||||
return;
|
||||
BEAST_EXPECT(rootA.get() == rootB.get());
|
||||
BEAST_EXPECT(
|
||||
SHAMap::canonicalInnerBranchesHarvested() == harvestedBefore);
|
||||
|
||||
auto* inner = static_cast<SHAMapInnerNode*>(rootA.get());
|
||||
int shared = 0;
|
||||
for (int branch = 0; branch < SHAMapInnerNode::branchFactor; ++branch)
|
||||
{
|
||||
if (inner->isEmptyBranch(branch))
|
||||
continue;
|
||||
auto const id = SHAMapNodeID{}.getChildNodeID(branch);
|
||||
auto const leafA = a.getLinkedNode(id);
|
||||
auto const leafB = b.getLinkedNode(id);
|
||||
BEAST_EXPECT(leafA && leafB && leafA.get() == leafB.get());
|
||||
++shared;
|
||||
}
|
||||
BEAST_EXPECT(shared == 2);
|
||||
}
|
||||
|
||||
void
|
||||
testCanonicalizeMergesInnerChildren(beast::Journal const& journal)
|
||||
{
|
||||
testcase("canonicalize harvests inner children onto cache winner");
|
||||
|
||||
tests::TestNodeFamily f(journal);
|
||||
SHAMap map{SHAMapType::FREE, f};
|
||||
|
||||
constexpr uint256 h1(
|
||||
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
constexpr uint256 h2(
|
||||
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
|
||||
constexpr uint256 h3(
|
||||
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
|
||||
constexpr uint256 h4(
|
||||
"c92891fe4ef6cee585fdc6fda2e09eb4d386363158ec3321b8123e5a772c6ca8");
|
||||
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h1, IntToVUC(1))));
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h2, IntToVUC(2))));
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h3, IntToVUC(3))));
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h4, IntToVUC(4))));
|
||||
|
||||
// getHash() unshares the map (cowid=0), which makes flushDirty a
|
||||
// no-op. Snapshot first so the original stays dirty and still
|
||||
// canonicalizes onto the cache shell.
|
||||
auto snap = map.snapShot(false);
|
||||
auto const rootHash = snap->getHash();
|
||||
Serializer s;
|
||||
snap->serializeRoot(s);
|
||||
auto shell = SHAMapTreeNode::makeFromWire(s.slice());
|
||||
if (!BEAST_EXPECT(
|
||||
shell && shell->isInner() && shell->getHash() == rootHash))
|
||||
return;
|
||||
|
||||
auto innerShell = std::static_pointer_cast<SHAMapInnerNode>(shell);
|
||||
int linkedBefore = 0;
|
||||
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
|
||||
{
|
||||
if (!innerShell->isEmptyBranch(b) && innerShell->getChildPointer(b))
|
||||
++linkedBefore;
|
||||
}
|
||||
BEAST_EXPECT(linkedBefore == 0);
|
||||
|
||||
f.getTreeNodeCache()->canonicalize_replace_client(
|
||||
rootHash.as_uint256(), shell);
|
||||
|
||||
auto const harvestedBefore = SHAMap::canonicalInnerBranchesHarvested();
|
||||
map.flushDirty(hotTRANSACTION_NODE);
|
||||
|
||||
auto cached = f.getTreeNodeCache()->fetch(rootHash.as_uint256());
|
||||
if (!BEAST_EXPECT(cached && cached->isInner()))
|
||||
return;
|
||||
auto* inner = static_cast<SHAMapInnerNode*>(cached.get());
|
||||
int linkedAfter = 0;
|
||||
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
|
||||
{
|
||||
if (!inner->isEmptyBranch(b) && inner->getChildPointer(b))
|
||||
++linkedAfter;
|
||||
}
|
||||
BEAST_EXPECT(linkedAfter > 0);
|
||||
BEAST_EXPECT(
|
||||
SHAMap::canonicalInnerBranchesHarvested() > harvestedBefore);
|
||||
|
||||
auto const rootNode = map.getLinkedNode(SHAMapNodeID{});
|
||||
if (!BEAST_EXPECT(rootNode && rootNode->isInner()))
|
||||
return;
|
||||
auto* liveInner = static_cast<SHAMapInnerNode*>(rootNode.get());
|
||||
int liveLinked = 0;
|
||||
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
|
||||
{
|
||||
if (liveInner->isEmptyBranch(b))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
!map.getLinkedNode(SHAMapNodeID{}.getChildNodeID(b)));
|
||||
continue;
|
||||
}
|
||||
auto const child =
|
||||
map.getLinkedNode(SHAMapNodeID{}.getChildNodeID(b));
|
||||
if (liveInner->getChildPointer(b))
|
||||
{
|
||||
++liveLinked;
|
||||
BEAST_EXPECT(
|
||||
child && child->getHash() == liveInner->getChildHash(b));
|
||||
}
|
||||
}
|
||||
BEAST_EXPECT(liveLinked > 0);
|
||||
}
|
||||
|
||||
void
|
||||
testGetLinkedNode(beast::Journal const& journal)
|
||||
{
|
||||
testcase("getLinkedNode walks only already-linked children");
|
||||
|
||||
tests::TestNodeFamily f(journal);
|
||||
SHAMap map{SHAMapType::FREE, f};
|
||||
|
||||
constexpr uint256 h1(
|
||||
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
constexpr uint256 h2(
|
||||
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
|
||||
constexpr uint256 h3(
|
||||
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
|
||||
constexpr uint256 h4(
|
||||
"c92891fe4ef6cee585fdc6fda2e09eb4d386363158ec3321b8123e5a772c6ca8");
|
||||
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h1, IntToVUC(1))));
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h2, IntToVUC(2))));
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h3, IntToVUC(3))));
|
||||
BEAST_EXPECT(map.addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(h4, IntToVUC(4))));
|
||||
|
||||
auto const root = map.getLinkedNode(SHAMapNodeID{});
|
||||
if (!BEAST_EXPECT(root && root->isInner()))
|
||||
return;
|
||||
|
||||
auto* inner = static_cast<SHAMapInnerNode*>(root.get());
|
||||
std::optional<int> leafBranch;
|
||||
std::optional<int> emptyBranch;
|
||||
for (int b = 0; b < SHAMapInnerNode::branchFactor; ++b)
|
||||
{
|
||||
if (inner->isEmptyBranch(b))
|
||||
{
|
||||
if (!emptyBranch)
|
||||
emptyBranch = b;
|
||||
continue;
|
||||
}
|
||||
auto const child =
|
||||
map.getLinkedNode(SHAMapNodeID{}.getChildNodeID(b));
|
||||
BEAST_EXPECT(child);
|
||||
if (child && !child->isInner() && !leafBranch)
|
||||
leafBranch = b;
|
||||
}
|
||||
if (BEAST_EXPECT(emptyBranch))
|
||||
{
|
||||
BEAST_EXPECT(!map.getLinkedNode(
|
||||
SHAMapNodeID{}.getChildNodeID(*emptyBranch)));
|
||||
}
|
||||
if (BEAST_EXPECT(leafBranch))
|
||||
{
|
||||
// A leaf has no children. Asking for a grandchild must miss.
|
||||
auto const leafId = SHAMapNodeID{}.getChildNodeID(*leafBranch);
|
||||
BEAST_EXPECT(!map.getLinkedNode(leafId.getChildNodeID(0)));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -120,26 +120,11 @@ public:
|
||||
tnCache_->reset();
|
||||
}
|
||||
|
||||
bool
|
||||
isNullBackend() const override
|
||||
{
|
||||
return nullBackend_;
|
||||
}
|
||||
|
||||
void
|
||||
setNullBackend(bool v)
|
||||
{
|
||||
nullBackend_ = v;
|
||||
}
|
||||
|
||||
beast::manual_clock<std::chrono::steady_clock>
|
||||
clock()
|
||||
{
|
||||
return clock_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool nullBackend_ = false;
|
||||
};
|
||||
|
||||
} // namespace tests
|
||||
|
||||
@@ -35,18 +35,14 @@
|
||||
#include <xrpld/app/misc/TxQ.h>
|
||||
#include <xrpld/app/misc/ValidatorKeys.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
#include <xrpld/app/tx/detail/Export.h>
|
||||
#include <xrpld/consensus/LedgerTiming.h>
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/overlay/predicates.h>
|
||||
#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>
|
||||
@@ -194,18 +190,6 @@ RCLConsensus::Adaptor::share(RCLCxPeerPos const& peerPos)
|
||||
void
|
||||
RCLConsensus::Adaptor::share(RCLCxTx const& tx)
|
||||
{
|
||||
// Export signatures can only be checked against the ledger, so peers
|
||||
// would take them for badly signed transactions. They need no relay:
|
||||
// peers have them from the proposed set (see Export.h).
|
||||
try
|
||||
{
|
||||
if (STTx(SerialIter{tx.tx_->slice()}).getTxnType() == ttEXPORT_SIGN)
|
||||
return;
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
}
|
||||
|
||||
// If we didn't relay this transaction recently, relay it to all peers
|
||||
if (app_.getHashRouter().shouldRelay(tx.id()))
|
||||
{
|
||||
@@ -364,21 +348,6 @@ RCLConsensus::Adaptor::onClose(
|
||||
make_shamapitem(tx.first->getTransactionID(), s.slice()));
|
||||
}
|
||||
|
||||
// featureRNG: our entropy contribution, if we add one. It is not tracked
|
||||
// by the censorship detector: it is only valid for this ledger.
|
||||
std::optional<uint256> entropyTxID;
|
||||
|
||||
// Keep enough validation history for the next flag ledger's score table
|
||||
// even when we don't vote this round (not proposing / wrong LCL).
|
||||
// Otherwise toKeep_ goes stale, validations older than
|
||||
// validationSET_EXPIRES (10 min, < 256 ledgers) get swept, and we fail
|
||||
// the next flag's score table too.
|
||||
if (prevLedger->isVotingLedger())
|
||||
{
|
||||
auto const seq = prevLedger->info().seq + 1;
|
||||
app_.getValidations().setSeqToKeep(seq - 1, seq + FLAG_LEDGER_INTERVAL);
|
||||
}
|
||||
|
||||
// Add pseudo-transactions to the set
|
||||
if (app_.config().standalone() || (proposing && !wrongLCL))
|
||||
{
|
||||
@@ -409,26 +378,6 @@ RCLConsensus::Adaptor::onClose(
|
||||
app_.getValidations(),
|
||||
initialSet);
|
||||
}
|
||||
|
||||
// featureRNG: our entropy contribution goes into our own initial
|
||||
// position, never the open ledger, so it only becomes public once our
|
||||
// transaction set for this ledger is fixed.
|
||||
if (prevLedger->rules().enabled(featureRNG))
|
||||
{
|
||||
if (auto const entropyTx = makeEntropyTx(prevLedger))
|
||||
{
|
||||
Serializer s;
|
||||
entropyTx->add(s);
|
||||
entropyTxID = entropyTx->getTransactionID();
|
||||
if (!initialSet->addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(*entropyTxID, s.slice())))
|
||||
{
|
||||
entropyTxID.reset();
|
||||
JLOG(j_.warn()) << "RNG: failed to add entropy txn";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now we need an immutable snapshot
|
||||
@@ -440,10 +389,9 @@ RCLConsensus::Adaptor::onClose(
|
||||
RCLCensorshipDetector<TxID, LedgerIndex>::TxIDSeqVec proposed;
|
||||
|
||||
initialSet->visitLeaves(
|
||||
[&proposed, &entropyTxID, seq](
|
||||
boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
if (item->key() != entropyTxID)
|
||||
proposed.emplace_back(item->key(), seq);
|
||||
[&proposed,
|
||||
seq](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
proposed.emplace_back(item->key(), seq);
|
||||
});
|
||||
|
||||
censorshipDetector_.propose(std::move(proposed));
|
||||
@@ -463,99 +411,6 @@ RCLConsensus::Adaptor::onClose(
|
||||
validatorKeys_.nodeID}};
|
||||
}
|
||||
|
||||
std::optional<STTx>
|
||||
RCLConsensus::Adaptor::makeEntropyTx(
|
||||
std::shared_ptr<Ledger const> const& prevLedger)
|
||||
{
|
||||
if (!validatorKeys_.keys)
|
||||
return std::nullopt;
|
||||
|
||||
auto const& masterKey = validatorKeys_.keys->masterPublicKey;
|
||||
|
||||
// The commitment the ledger currently holds for us, if any.
|
||||
std::optional<uint256> commitment;
|
||||
if (auto const sle = prevLedger->read(keylet::random()))
|
||||
{
|
||||
for (auto const& entry : sle->getFieldArray(sfRandomDigests))
|
||||
{
|
||||
if (makeSlice(entry.getFieldVL(sfPublicKey)) == masterKey.slice())
|
||||
{
|
||||
commitment = entry.getFieldH256(sfNextRandomDigest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We only ever hold the secret from our last contribution, which has
|
||||
// never been published. Reveal it iff the ledger holds its commitment.
|
||||
// Otherwise (first contribution, restart, or our last contribution was
|
||||
// not included) we only commit this time, and our previous secret,
|
||||
// whatever became of it, is never used again.
|
||||
std::optional<uint256> reveal;
|
||||
if (commitment && rngSecret_ && rngSecret_->first == *commitment)
|
||||
reveal = rngSecret_->second;
|
||||
|
||||
uint256 secret;
|
||||
beast::rngfill(secret.begin(), secret.size(), crypto_prng());
|
||||
uint256 const nextCommitment = sha512Half(secret);
|
||||
rngSecret_.emplace(nextCommitment, secret);
|
||||
|
||||
auto const seq = prevLedger->info().seq + 1;
|
||||
|
||||
JLOG(j_.debug()) << "RNG: entropy txn for ledger " << seq
|
||||
<< (reveal ? " (reveal and commit)" : " (commit only)");
|
||||
|
||||
return STTx(ttENTROPY, [&](auto& obj) {
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
obj.setFieldVL(sfPublicKey, masterKey.slice());
|
||||
obj.setFieldH256(sfNextRandomDigest, nextCommitment);
|
||||
if (reveal)
|
||||
obj.setFieldH256(sfRandomData, *reveal);
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<NodeID>
|
||||
RCLConsensus::Adaptor::forcedTxOwner(
|
||||
RCLCxTx const& tx,
|
||||
RCLCxLedger const& prevLedger) const
|
||||
{
|
||||
if (!prevLedger.ledger_ || !prevLedger.ledger_->rules().enabled(featureRNG))
|
||||
return std::nullopt;
|
||||
|
||||
try
|
||||
{
|
||||
SerialIter sit(tx.tx_->slice());
|
||||
STTx const stx(sit);
|
||||
|
||||
if (stx.getTxnType() != ttENTROPY ||
|
||||
stx.getFieldU32(sfLedgerSequence) != prevLedger.seq() + 1)
|
||||
return std::nullopt;
|
||||
|
||||
auto const pk = stx.getFieldVL(sfPublicKey);
|
||||
if (!publicKeyType(makeSlice(pk)))
|
||||
return std::nullopt;
|
||||
|
||||
// Only the exact bytes makeEntropyTx would build, so an owner cannot
|
||||
// force extra fields (memos and the like) in with its contribution.
|
||||
STTx const canonical(ttENTROPY, [&](auto& obj) {
|
||||
obj.setFieldU32(sfLedgerSequence, prevLedger.seq() + 1);
|
||||
obj.setFieldVL(sfPublicKey, pk);
|
||||
obj.setFieldH256(
|
||||
sfNextRandomDigest, stx.getFieldH256(sfNextRandomDigest));
|
||||
if (stx.isFieldPresent(sfRandomData))
|
||||
obj.setFieldH256(sfRandomData, stx.getFieldH256(sfRandomData));
|
||||
});
|
||||
if (canonical.getTransactionID() != tx.id())
|
||||
return std::nullopt;
|
||||
|
||||
return calcNodeID(PublicKey(makeSlice(pk)));
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RCLConsensus::Adaptor::onForceAccept(
|
||||
Result const& result,
|
||||
@@ -1018,19 +873,6 @@ RCLConsensus::Adaptor::validate(
|
||||
v.setFieldU32(sfLoadFee, fee);
|
||||
}
|
||||
|
||||
// Nominate our export key on flag ledgers, which the next flag
|
||||
// ledger's UNL report window covers (see NegativeUNLVote).
|
||||
if (ledger.ledger_->isFlagLedger() &&
|
||||
ledger.ledger_->rules().enabled(featureExport))
|
||||
{
|
||||
if (auto const n =
|
||||
app_.getExportKeys().nominate(*ledger.ledger_))
|
||||
{
|
||||
v.setFieldVL(sfExportKey, n->first.slice());
|
||||
v.setFieldVL(sfExportKeyProof, n->second);
|
||||
}
|
||||
}
|
||||
|
||||
// If the next ledger is a flag ledger, suggest fee changes and
|
||||
// new features:
|
||||
if (ledger.ledger_->isVotingLedger())
|
||||
|
||||
@@ -34,12 +34,10 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/RippleLedgerHash.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STValidation.h>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -93,11 +91,6 @@ class RCLConsensus
|
||||
RCLCensorshipDetector<TxID, LedgerIndex> censorshipDetector_;
|
||||
NegativeUNLVote nUnlVote_;
|
||||
|
||||
// featureRNG: the secret behind the commitment in our most recent
|
||||
// entropy contribution, as {commitment, secret}. Only touched by
|
||||
// onClose, which runs under the consensus lock.
|
||||
std::optional<std::pair<uint256, uint256>> rngSecret_;
|
||||
|
||||
public:
|
||||
using Ledger_t = RCLCxLedger;
|
||||
using NodeID_t = NodeID;
|
||||
@@ -189,23 +182,7 @@ class RCLConsensus
|
||||
return parms_;
|
||||
}
|
||||
|
||||
/** featureRNG: if tx is a validator's entropy contribution for the
|
||||
ledger after prevLedger, the NodeID of that validator.
|
||||
|
||||
Consensus votes for such a transaction while it is in its owner's
|
||||
own position during the opening window of the establish phase.
|
||||
*/
|
||||
std::optional<NodeID>
|
||||
forcedTxOwner(RCLCxTx const& tx, RCLCxLedger const& prevLedger) const;
|
||||
|
||||
private:
|
||||
/** featureRNG: build our entropy contribution for the ledger after
|
||||
prevLedger, revealing our previous secret if the ledger holds its
|
||||
commitment, and committing to a fresh one.
|
||||
*/
|
||||
std::optional<STTx>
|
||||
makeEntropyTx(std::shared_ptr<Ledger const> const& prevLedger);
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// The following members implement the generic Consensus requirements
|
||||
// and are marked private to indicate ONLY Consensus<Adaptor> will call
|
||||
|
||||
@@ -23,22 +23,17 @@
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
#include <xrpld/app/rdb/Wallet.h>
|
||||
#include <xrpld/consensus/LedgerTiming.h>
|
||||
#include <xrpld/core/JobQueue.h>
|
||||
#include <xrpld/core/TimeKeeper.h>
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/overlay/predicates.h>
|
||||
#include <xrpld/perflog/PerfLog.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/messages.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
@@ -172,40 +167,6 @@ RCLValidationsAdaptor::acquire(LedgerHash const& hash)
|
||||
return RCLValidatedLedger(std::move(ledger), j_);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/** Announce a manifest the cache accepted other than from peer gossip.
|
||||
|
||||
Not to the peers it was held back from: OverlayImpl::onManifests records
|
||||
each of them against the manifest's hash.
|
||||
*/
|
||||
void
|
||||
relayManifest(Application& app, Manifest const& manifest)
|
||||
{
|
||||
app.getOPs().pubManifest(manifest);
|
||||
|
||||
// As OverlayImpl::onManifests does for gossip it accepts directly.
|
||||
if (app.validators().listed(manifest.masterKey))
|
||||
{
|
||||
auto db = app.getWalletDB().checkoutDb();
|
||||
addValidatorManifest(*db, manifest.serialized);
|
||||
}
|
||||
|
||||
auto const toSkip = app.getHashRouter().shouldRelay(manifest.hash());
|
||||
if (!toSkip)
|
||||
return;
|
||||
|
||||
protocol::TMManifests tm;
|
||||
tm.add_list()->set_stobject(
|
||||
manifest.serialized.data(), manifest.serialized.size());
|
||||
|
||||
app.overlay().foreach(send_if_not(
|
||||
std::make_shared<Message>(tm, protocol::mtMANIFESTS),
|
||||
peer_in_set(*toSkip)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
handleNewValidation(
|
||||
Application& app,
|
||||
@@ -230,29 +191,13 @@ handleNewValidation(
|
||||
// Resolving now also means this validation is classified in the round it
|
||||
// arrived in rather than written off along with every other one until the
|
||||
// trusted set is next recomputed.
|
||||
auto& manifests = app.validatorManifests();
|
||||
|
||||
std::shared_ptr<ReadView const> view;
|
||||
if (manifests.getMasterKey(signingKey) == signingKey)
|
||||
if (app.validatorManifests().getMasterKey(signingKey) == signingKey)
|
||||
{
|
||||
view = app.openLedger().current();
|
||||
if (view && !view->rules().enabled(featureOnChainManifests))
|
||||
view.reset();
|
||||
if (auto const view = app.openLedger().current();
|
||||
view && view->rules().enabled(featureOnChainManifests))
|
||||
app.validatorManifests().applyLedgerSigningKey(*view, signingKey);
|
||||
}
|
||||
|
||||
// Every caller has verified this validation's signature and found it
|
||||
// current, so its ephemeral key is live. That is what admits a manifest
|
||||
// gossiped for the key, and what keeps the manifest it belongs to from
|
||||
// being evicted. A manifest that arrived before the key was ever seen is
|
||||
// released here, and relayed: the peers that held it back are waiting on
|
||||
// the same validation.
|
||||
if (auto const released = manifests.noteValidation(signingKey, view.get()))
|
||||
relayManifest(app, *released);
|
||||
|
||||
// The ledger tier's miss path.
|
||||
if (view && manifests.getMasterKey(signingKey) == signingKey)
|
||||
manifests.applyLedgerSigningKey(*view, signingKey);
|
||||
|
||||
// Ensure validation is marked as trusted if signer currently trusted
|
||||
auto masterKey = app.validators().getTrustedKey(signingKey);
|
||||
|
||||
|
||||
@@ -351,11 +351,6 @@ public:
|
||||
Expected<std::shared_ptr<Transaction>, HookReturnCode>
|
||||
emit(Slice const& txBlob) const;
|
||||
|
||||
// featureExport: emit a ttEXPORT carrying txBlob, a transaction for the
|
||||
// other network (Account and SigningPubKey filled in if absent)
|
||||
Expected<std::shared_ptr<Transaction>, HookReturnCode>
|
||||
xport(Slice const& txBlob) const;
|
||||
|
||||
Expected<uint64_t, HookReturnCode>
|
||||
etxn_burden() const;
|
||||
|
||||
@@ -496,16 +491,6 @@ public:
|
||||
Expected<uint256, HookReturnCode>
|
||||
ledger_nonce() const;
|
||||
|
||||
/// RNG APIs (featureRNG)
|
||||
|
||||
// A uniform integer in [0, sides).
|
||||
Expected<uint64_t, HookReturnCode>
|
||||
dice(uint32_t sides) const;
|
||||
|
||||
// len (1 to 512) random bytes.
|
||||
Expected<Bytes, HookReturnCode>
|
||||
util_random(uint32_t len) const;
|
||||
|
||||
Expected<Keylet, HookReturnCode>
|
||||
ledger_keylet(Keylet const& klLo, Keylet const& klHi) const;
|
||||
|
||||
@@ -645,10 +630,6 @@ private:
|
||||
Rules const& rules,
|
||||
int recursion_depth = 0) // used internally
|
||||
const;
|
||||
|
||||
// featureRNG: advance the ledger's RNG state and return one output block.
|
||||
Expected<uint256, HookReturnCode>
|
||||
rng_next() const;
|
||||
};
|
||||
|
||||
} // namespace hook
|
||||
|
||||
@@ -86,7 +86,7 @@ bool
|
||||
canHook(
|
||||
STTx const& tx,
|
||||
ripple::uint256 hookOn,
|
||||
std::optional<ripple::Blob> hookName);
|
||||
std::optional<ripple::Slice> hookName);
|
||||
|
||||
bool
|
||||
canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit);
|
||||
@@ -129,11 +129,7 @@ apply(
|
||||
uint32_t wasmParam,
|
||||
uint8_t hookChainPosition,
|
||||
// result of apply() if this is weak exec
|
||||
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);
|
||||
std::shared_ptr<STObject const> const& provisionalMeta);
|
||||
|
||||
struct HookContext;
|
||||
|
||||
@@ -211,7 +207,6 @@ struct HookContext
|
||||
uint16_t emit_nonce_counter{
|
||||
0}; // incremented whenever nonce is called to ensure unique nonces
|
||||
uint16_t ledger_nonce_counter{0};
|
||||
uint16_t rng_draws{0}; // featureRNG: dice/random calls this execution
|
||||
int64_t expected_etxn_count{-1}; // make this a 64bit int so the uint32
|
||||
// from the hookapi cant overflow it
|
||||
std::map<ripple::uint256, bool> nonce_used{};
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/ledger/TransactionMaster.h>
|
||||
#include <xrpld/app/tx/detail/Import.h>
|
||||
#include <xrpld/ledger/ApplyViewImpl.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace hook {
|
||||
|
||||
@@ -72,13 +69,7 @@ Expected<bool, HookReturnCode>
|
||||
HookAPI::sto_validate(Bytes const& data) const
|
||||
{
|
||||
if (data.size() < 2)
|
||||
{
|
||||
// fix20261005: a buffer this short can never hold a valid STObject,
|
||||
// so report it as invalid (0) as documented, rather than TOO_SMALL
|
||||
if (hookCtx.applyCtx.view().rules().enabled(fix20261005))
|
||||
return false;
|
||||
return Unexpected(TOO_SMALL);
|
||||
}
|
||||
|
||||
unsigned char* start = const_cast<unsigned char*>(data.data());
|
||||
unsigned char* upto = start;
|
||||
@@ -848,35 +839,6 @@ HookAPI::emit(Slice const& txBlob) const
|
||||
return tpTrans;
|
||||
}
|
||||
|
||||
Expected<std::shared_ptr<Transaction>, HookReturnCode>
|
||||
HookAPI::xport(Slice const& txBlob) const
|
||||
{
|
||||
Serializer s;
|
||||
try
|
||||
{
|
||||
SerialIter sit{txBlob};
|
||||
STObject inner{sit, sfExportedTxn};
|
||||
if (!inner.isFieldPresent(sfAccount))
|
||||
inner[sfAccount] = hookCtx.result.account;
|
||||
if (!inner.isFieldPresent(sfSigningPubKey))
|
||||
inner[sfSigningPubKey] = Slice{};
|
||||
STObject outer{sfGeneric};
|
||||
outer[sfTransactionType] = static_cast<std::uint16_t>(ttEXPORT);
|
||||
outer.emplace_back(std::move(inner));
|
||||
outer.add(s);
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
return Unexpected(INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// prepare() fills in what emit() requires, including the export's fee
|
||||
auto const blob = prepare(s.slice());
|
||||
if (!blob)
|
||||
return Unexpected(blob.error());
|
||||
return emit(makeSlice(*blob));
|
||||
}
|
||||
|
||||
Expected<uint64_t, HookReturnCode>
|
||||
HookAPI::etxn_burden() const
|
||||
{
|
||||
@@ -1067,21 +1029,15 @@ HookAPI::float_set(int32_t exponent, int64_t mantissa) const
|
||||
auto normalized = hook_float::normalize_xfl(mantissa, exponent);
|
||||
|
||||
// the above function will underflow into a canonical 0
|
||||
// but this api must report that underflow.
|
||||
// The range check is on the exponent *after* the mantissa is normalized
|
||||
// to 16 digits. fix20261005: report overflow / underflow as
|
||||
// EXPONENT_OVERSIZED / EXPONENT_UNDERSIZED; before the fix both were
|
||||
// reported as INVALID_FLOAT.
|
||||
bool const fixExpCodes =
|
||||
hookCtx.applyCtx.view().rules().enabled(fix20261005);
|
||||
// but this api must report that underflow
|
||||
if (!normalized)
|
||||
{
|
||||
if (normalized.error() == XFL_OVERFLOW)
|
||||
return Unexpected(fixExpCodes ? EXPONENT_OVERSIZED : INVALID_FLOAT);
|
||||
return Unexpected(INVALID_FLOAT);
|
||||
return Unexpected(normalized.error());
|
||||
}
|
||||
if (normalized.value() == 0)
|
||||
return Unexpected(fixExpCodes ? EXPONENT_UNDERSIZED : INVALID_FLOAT);
|
||||
return Unexpected(INVALID_FLOAT);
|
||||
|
||||
return normalized;
|
||||
}
|
||||
@@ -1613,18 +1569,6 @@ 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);
|
||||
|
||||
@@ -1939,147 +1883,6 @@ HookAPI::ledger_nonce() const
|
||||
return hash;
|
||||
}
|
||||
|
||||
// featureRNG
|
||||
//
|
||||
// The ledger's RNG state lives in the Random object. The ttENTROPY
|
||||
// transactions applied at the start of the ledger seed it from validator
|
||||
// reveals; after that every draw from any Hook replaces it with
|
||||
// H(step, state). So the state iterates between API calls, between Hook
|
||||
// executions and between transactions, including transactions that end in a
|
||||
// tec: ApplyContext::discard keeps the advance, so a rollback cannot preserve
|
||||
// an RNG state for whatever executes next.
|
||||
//
|
||||
// No Hook can read the state, so none can work out what a later draw will
|
||||
// return, even one that ran earlier in the same transaction: slot_set and
|
||||
// ledger_keylet refuse the Random object, slot_set refuses ttENTROPY
|
||||
// transactions, the object is left out of provisional metadata (meta_slot),
|
||||
// and its state and commitment fields are never written to any metadata.
|
||||
//
|
||||
// Each draw returns H(output, state, txid, hook account, hook hash, chain
|
||||
// position, strong, callback, wasm param). Those extra inputs keep outputs
|
||||
// distinct for different executions even if they somehow saw the same state.
|
||||
//
|
||||
// The state the transaction's first draw saw is written to the transaction's
|
||||
// final metadata (sfRandomData), never its provisional metadata.
|
||||
Expected<uint256, HookReturnCode>
|
||||
HookAPI::rng_next() const
|
||||
{
|
||||
static constexpr std::uint32_t rngOutputTag = 0x524E474F; // 'RNGO'
|
||||
static constexpr std::uint32_t rngStepTag = 0x524E4749; // 'RNGI'
|
||||
|
||||
auto& applyCtx = hookCtx.applyCtx;
|
||||
auto& view = applyCtx.view();
|
||||
|
||||
if (!view.rules().enabled(featureRNG))
|
||||
return Unexpected(TOO_LITTLE_ENTROPY);
|
||||
|
||||
if (hookCtx.rng_draws >= hook_api::max_rng_draws)
|
||||
return Unexpected(TOO_MANY_NONCES);
|
||||
|
||||
auto sle = view.peek(keylet::random());
|
||||
if (!sle)
|
||||
return Unexpected(TOO_LITTLE_ENTROPY);
|
||||
|
||||
// A closed ledger may only draw on its own seed. Open ledger application
|
||||
// is a preview, so it may run on the last closed ledger's state; the
|
||||
// final result is decided when the ledger closes.
|
||||
auto const seq = view.seq();
|
||||
auto const seededFor = sle->getFieldU32(sfLedgerSequence);
|
||||
if (seededFor != seq && !(view.open() && seededFor + 1 == seq))
|
||||
return Unexpected(TOO_LITTLE_ENTROPY);
|
||||
|
||||
// At least 80% of the UNLReport must have contributed (at least one
|
||||
// validator if there is no UNLReport). Both fields are 16 bits on the
|
||||
// ledger; they are widened here so the scaled comparison cannot overflow.
|
||||
std::uint32_t const count = sle->getFieldU16(sfEntropyCount);
|
||||
std::uint32_t const denominator = sle->getFieldU16(sfEntropyDenominator);
|
||||
if (count == 0 || count * 5 < denominator * 4)
|
||||
return Unexpected(TOO_LITTLE_ENTROPY);
|
||||
|
||||
uint256 const state = sle->getFieldH256(sfRandomData);
|
||||
|
||||
if (auto* avi = dynamic_cast<ApplyViewImpl*>(&view))
|
||||
avi->setRandomData(state);
|
||||
|
||||
auto const& hr = hookCtx.result;
|
||||
uint256 const out = ripple::sha512Half(
|
||||
rngOutputTag,
|
||||
state,
|
||||
applyCtx.tx.getTransactionID(),
|
||||
hr.account,
|
||||
hr.hookHash,
|
||||
static_cast<std::uint8_t>(hr.hookChainPosition),
|
||||
static_cast<std::uint8_t>(hr.isStrong ? 1 : 0),
|
||||
static_cast<std::uint8_t>(hr.isCallback ? 1 : 0),
|
||||
hr.wasmParam);
|
||||
|
||||
sle->setFieldH256(sfRandomData, ripple::sha512Half(rngStepTag, state));
|
||||
view.update(sle);
|
||||
|
||||
++hookCtx.rng_draws;
|
||||
return out;
|
||||
}
|
||||
|
||||
Expected<uint64_t, HookReturnCode>
|
||||
HookAPI::dice(uint32_t sides) const
|
||||
{
|
||||
if (sides == 0)
|
||||
return Unexpected(INVALID_ARGUMENT);
|
||||
|
||||
auto block = rng_next();
|
||||
if (!block)
|
||||
return Unexpected(block.error());
|
||||
|
||||
// Rejection sampling over big-endian 64-bit words, so every side is
|
||||
// exactly equally likely. A word is rejected with probability below
|
||||
// 2^-32, so the bound on blocks is never reached in practice; it only
|
||||
// keeps the loop finite.
|
||||
std::uint64_t const max = std::numeric_limits<std::uint64_t>::max();
|
||||
std::uint64_t const excess = (max % sides + 1) % sides; // 2^64 mod sides
|
||||
std::uint64_t word = 0;
|
||||
uint256 b = *block;
|
||||
for (int i = 0; i < 16; ++i)
|
||||
{
|
||||
for (std::size_t w = 0; w < 4; ++w)
|
||||
{
|
||||
word = 0;
|
||||
for (std::size_t k = 0; k < 8; ++k)
|
||||
word = (word << 8) | b.data()[w * 8 + k];
|
||||
if (word <= max - excess)
|
||||
return word % sides;
|
||||
}
|
||||
b = ripple::sha512Half(b);
|
||||
}
|
||||
return word % sides;
|
||||
}
|
||||
|
||||
Expected<Bytes, HookReturnCode>
|
||||
HookAPI::util_random(uint32_t len) const
|
||||
{
|
||||
if (len == 0)
|
||||
return Unexpected(TOO_SMALL);
|
||||
|
||||
if (len > 512)
|
||||
return Unexpected(TOO_BIG);
|
||||
|
||||
auto block = rng_next();
|
||||
if (!block)
|
||||
return Unexpected(block.error());
|
||||
|
||||
Bytes out;
|
||||
out.reserve(len);
|
||||
uint256 b = *block;
|
||||
while (true)
|
||||
{
|
||||
auto const n = std::min<std::size_t>(32, len - out.size());
|
||||
out.insert(out.end(), b.data(), b.data() + n);
|
||||
if (out.size() >= len)
|
||||
break;
|
||||
b = ripple::sha512Half(b);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Expected<Keylet, HookReturnCode>
|
||||
HookAPI::ledger_keylet(Keylet const& klLo, Keylet const& klHi) const
|
||||
{
|
||||
@@ -2090,10 +1893,6 @@ HookAPI::ledger_keylet(Keylet const& klLo, Keylet const& klHi) const
|
||||
std::optional<ripple::uint256> found =
|
||||
hookCtx.applyCtx.view().succ(klLo.key, klHi.key.next());
|
||||
|
||||
// featureRNG: the RNG object is invisible to Hooks, even by key.
|
||||
if (found && *found == keylet::random().key)
|
||||
found = hookCtx.applyCtx.view().succ(*found, klHi.key.next());
|
||||
|
||||
if (!found)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
@@ -2338,12 +2137,6 @@ HookAPI::slot_set(Bytes const& data, uint32_t slot_no) const
|
||||
if (kl->key == beast::zero)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
// featureRNG: the RNG state must never be readable by a Hook, or an
|
||||
// earlier Hook could compute what later draws will return. Matched on
|
||||
// the key because a Hook-supplied keylet type can be ltANY.
|
||||
if (kl->key == keylet::random().key)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
auto const sle = hookCtx.applyCtx.view().read(*kl);
|
||||
if (!sle)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
@@ -2367,11 +2160,6 @@ HookAPI::slot_set(Bytes const& data, uint32_t slot_no) const
|
||||
slot_value = p->first->getSTransaction();
|
||||
else
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
// featureRNG: validator reveals are never readable by Hooks.
|
||||
if (*slot_value &&
|
||||
(*slot_value)->getFieldU16(sfTransactionType) == ttENTROPY)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
}
|
||||
else
|
||||
return Unexpected(INVALID_ARGUMENT);
|
||||
@@ -3028,19 +2816,11 @@ 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 && chargeWrite)
|
||||
if (createNamespace)
|
||||
{
|
||||
// overflow should never ever happen but check anyway
|
||||
if (namespaceCount + 1 < namespaceCount)
|
||||
@@ -3050,20 +2830,15 @@ HookAPI::set_state_cache(
|
||||
return Unexpected(TOO_MANY_NAMESPACES);
|
||||
}
|
||||
|
||||
if (chargeWrite)
|
||||
{
|
||||
stateMap.modified_entry_count++;
|
||||
stateMap.modified_entry_count++;
|
||||
|
||||
// sanity check
|
||||
if (view.rules().enabled(featureExtendedHookState) &&
|
||||
availableForReserves < hookStateScale)
|
||||
return Unexpected(INTERNAL_ERROR);
|
||||
|
||||
availableForReserves -= hookStateScale;
|
||||
}
|
||||
// sanity check
|
||||
if (view.rules().enabled(featureExtendedHookState) &&
|
||||
availableForReserves < hookStateScale)
|
||||
return Unexpected(INTERNAL_ERROR);
|
||||
|
||||
stateMap[acc] = {
|
||||
availableForReserves,
|
||||
availableForReserves - hookStateScale,
|
||||
namespaceCount,
|
||||
hookStateScale,
|
||||
{{ns, {{key, {modified, data}}}}}};
|
||||
|
||||
@@ -77,12 +77,6 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
break;
|
||||
}
|
||||
|
||||
// the exporter hears of its exports through cbak, not hook()
|
||||
case ttEXPORT:
|
||||
case ttEXPORT_SIGN:
|
||||
case ttEXPORT_FINAL:
|
||||
break;
|
||||
|
||||
case ttREMIT: {
|
||||
if (destAcc)
|
||||
ADD_TSH(*destAcc, tshSTRONG);
|
||||
@@ -564,8 +558,7 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
case ttFEE:
|
||||
case ttUNL_MODIFY:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttUNL_REPORT:
|
||||
case ttENTROPY: {
|
||||
case ttUNL_REPORT: {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -820,7 +813,7 @@ bool
|
||||
hook::canHook(
|
||||
STTx const& tx,
|
||||
ripple::uint256 hookOn,
|
||||
std::optional<ripple::Blob> hookName)
|
||||
std::optional<ripple::Slice> hookName)
|
||||
{
|
||||
if (!canHookTT(tx.getTxnType(), hookOn))
|
||||
return false;
|
||||
@@ -834,7 +827,7 @@ hook::canHook(
|
||||
// transaction, so we can't hook without the hook name
|
||||
return false;
|
||||
|
||||
return tx.getFieldVL(sfHookName) == *hookName;
|
||||
return tx[sfHookName] == *hookName;
|
||||
}
|
||||
|
||||
ripple::uint256
|
||||
@@ -1048,29 +1041,8 @@ hook::apply(
|
||||
bool isStrong,
|
||||
uint32_t wasmParam,
|
||||
uint8_t hookChainPosition,
|
||||
std::shared_ptr<STObject const> const& provisionalMeta,
|
||||
std::optional<ripple::STObject> const& emitFailureTxn)
|
||||
std::shared_ptr<STObject const> const& provisionalMeta)
|
||||
{
|
||||
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)
|
||||
@@ -1102,7 +1074,12 @@ hook::apply(
|
||||
.foreignStateSetDisabled = false,
|
||||
.provisionalMeta = provisionalMeta,
|
||||
.foreignStateGrantCache = {}},
|
||||
.emitFailure = std::move(emitFailure),
|
||||
.emitFailure = isCallback && wasmParam & 1
|
||||
? std::optional<ripple::STObject>(
|
||||
(*(applyCtx.view().peek(keylet::emittedTxn(
|
||||
applyCtx.tx.getFieldH256(sfTransactionHash)))))
|
||||
.downcast<STObject>())
|
||||
: std::optional<ripple::STObject>(),
|
||||
.module = nullptr,
|
||||
.api_ = nullptr};
|
||||
|
||||
@@ -1569,20 +1546,7 @@ hook::finalizeHookResult(
|
||||
ptr->add(s);
|
||||
SerialIter sit(s.slice());
|
||||
|
||||
ripple::STObject emittedTxn(sit, sfEmittedTxn);
|
||||
|
||||
// ltEMITTED_TXN has an SOTemplate, so the new SLE already
|
||||
// holds a non-present sfEmittedTxn slot. emplace_back()
|
||||
// appends a second entry behind it, and lookups (which go
|
||||
// through the template index) keep reporting the field as
|
||||
// absent until the SLE is serialized and re-read. set()
|
||||
// replaces the template slot. The serialized entry is the
|
||||
// same either way, but a hook slotting the entry later in
|
||||
// the same ledger can observe the difference, so gate it.
|
||||
if (applyCtx.view().rules().enabled(fix20261005))
|
||||
sleEmitted->set(std::move(emittedTxn));
|
||||
else
|
||||
sleEmitted->emplace_back(std::move(emittedTxn));
|
||||
sleEmitted->emplace_back(ripple::STObject(sit, sfEmittedTxn));
|
||||
auto page = applyCtx.view().dirInsert(
|
||||
keylet::emittedDir(), emittedId, [&](SLE::ref sle) {
|
||||
(*sle)[sfFlags] = lsfEmittedDir;
|
||||
@@ -2713,37 +2677,6 @@ DEFINE_HOOK_FUNCTION(
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
/* Export a transaction for the other network: wrap it in a ttEXPORT and emit
|
||||
* that. Writes the ttEXPORT's id. */
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
int64_t,
|
||||
xport,
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len,
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len)
|
||||
{
|
||||
HOOK_SETUP(); // populates memory_ctx, memory, memory_length, applyCtx,
|
||||
// hookCtx on current stack
|
||||
|
||||
if (NOT_IN_BOUNDS(read_ptr, read_len, memory_length) ||
|
||||
NOT_IN_BOUNDS(write_ptr, write_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
if (write_len < 32)
|
||||
return TOO_SMALL;
|
||||
|
||||
auto const res = api.xport(ripple::Slice{memory + read_ptr, read_len});
|
||||
if (!res)
|
||||
return res.error();
|
||||
|
||||
std::memcpy(memory + write_ptr, (*res)->getID().data(), 32);
|
||||
hookCtx.result.emittedTxn.push(*res);
|
||||
return uint64_t{32};
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
/* Emit a transaction from this hook. Transaction must be in STObject form,
|
||||
* fully formed and valid. XRPLD does not modify transactions it only checks
|
||||
* them for validity. */
|
||||
@@ -2920,49 +2853,6 @@ DEFINE_HOOK_FUNCTION(
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
// featureRNG: a uniform integer in [0, sides)
|
||||
DEFINE_HOOK_FUNCTION(int64_t, dice, uint32_t sides)
|
||||
{
|
||||
HOOK_SETUP();
|
||||
|
||||
auto const result = api.dice(sides);
|
||||
if (!result)
|
||||
return result.error();
|
||||
|
||||
return result.value();
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
// featureRNG: write_len (1 to 512) random bytes
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
int64_t,
|
||||
util_random,
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len)
|
||||
{
|
||||
HOOK_SETUP();
|
||||
|
||||
if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
auto const result = api.util_random(write_len);
|
||||
if (!result)
|
||||
return result.error();
|
||||
|
||||
auto const& bytes = result.value();
|
||||
|
||||
WRITE_WASM_MEMORY_AND_RETURN(
|
||||
write_ptr,
|
||||
write_len,
|
||||
bytes.data(),
|
||||
bytes.size(),
|
||||
memory,
|
||||
memory_length);
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
int64_t,
|
||||
ledger_keylet,
|
||||
|
||||
@@ -25,11 +25,7 @@
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/overlay/PeerSet.h>
|
||||
#include <xrpl/basics/CountedObject.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
@@ -179,36 +175,6 @@ private:
|
||||
clock_type::time_point mLastAction;
|
||||
|
||||
std::shared_ptr<Ledger> mLedger;
|
||||
|
||||
/** Whether this ledger is to be read for manifests as soon as it can be:
|
||||
early, before the rest of the state map, if [early_manifest_sync]
|
||||
allows, and otherwise once it is complete. */
|
||||
bool
|
||||
wantManifests();
|
||||
|
||||
/** Read the manifests from the ledger, now that they can be.
|
||||
|
||||
Dispatches the read at most once per ledger, however many paths reach
|
||||
here.
|
||||
|
||||
@param listed Master keys to read by key, whether or not the part of
|
||||
the directory read lists them. If unseated, the master keys on the
|
||||
validator lists are collected in the job, so that a caller holding
|
||||
mtx_ need not take the validator list's lock.
|
||||
@param maxPages Most manifest directory pages to read
|
||||
*/
|
||||
void
|
||||
syncManifests(
|
||||
std::optional<hash_set<PublicKey>> listed,
|
||||
std::uint64_t maxPages);
|
||||
|
||||
/** The manifests have been read, or are being, from this ledger.
|
||||
|
||||
Atomic because done() can be reached from trigger() after it has
|
||||
released mtx_.
|
||||
*/
|
||||
std::atomic<bool> mManifestsSynced{false};
|
||||
|
||||
bool mHaveHeader;
|
||||
bool mHaveState;
|
||||
bool mHaveTransactions;
|
||||
|
||||
@@ -155,12 +155,6 @@ LedgerHistory::getLedgerByHash(LedgerHash const& hash)
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::shared_ptr<Ledger const>
|
||||
LedgerHistory::getCachedLedger(LedgerHash const& hash)
|
||||
{
|
||||
return m_ledgers_by_hash.fetch(hash);
|
||||
}
|
||||
|
||||
static void
|
||||
log_one(
|
||||
ReadView const& ledger,
|
||||
|
||||
@@ -63,13 +63,6 @@ public:
|
||||
std::shared_ptr<Ledger const>
|
||||
getLedgerByHash(LedgerHash const& ledgerHash);
|
||||
|
||||
/** Return a ledger already in the hash cache.
|
||||
|
||||
Does not load from the database or wire the SHAMap.
|
||||
*/
|
||||
std::shared_ptr<Ledger const>
|
||||
getCachedLedger(LedgerHash const& ledgerHash);
|
||||
|
||||
/** Get a ledger's hash given its sequence number
|
||||
@param ledgerIndex The sequence number of the desired ledger
|
||||
@return The hash of the specified ledger
|
||||
|
||||
@@ -37,8 +37,6 @@
|
||||
#include <xrpl/protocol/RippleLedgerHash.h>
|
||||
#include <xrpl/protocol/STValidation.h>
|
||||
#include <xrpl/protocol/messages.h>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
#include <mutex>
|
||||
@@ -188,21 +186,6 @@ public:
|
||||
std::shared_ptr<Ledger const>
|
||||
getLedgerByHash(uint256 const& hash);
|
||||
|
||||
/** Return a ledger that is already resident in memory.
|
||||
|
||||
Looks in the ledger history cache, the closed and validated
|
||||
ledgers and the null-mode retained window. Unlike getLedgerByHash
|
||||
and getLedgerBySeq this never loads from SQL or the node store and
|
||||
never edits the complete-ledger set, so it is safe to call with
|
||||
values supplied by a peer.
|
||||
*/
|
||||
std::shared_ptr<Ledger const>
|
||||
getResidentLedgerByHash(uint256 const& hash);
|
||||
|
||||
/** Sequence-number form of getResidentLedgerByHash. */
|
||||
std::shared_ptr<Ledger const>
|
||||
getResidentLedgerBySeq(std::uint32_t seq);
|
||||
|
||||
void
|
||||
setLedgerRangePresent(
|
||||
std::uint32_t minV,
|
||||
@@ -340,10 +323,6 @@ private:
|
||||
void
|
||||
doAdvance(std::unique_lock<std::recursive_mutex>&);
|
||||
|
||||
// Closed, validated, then retained ledgers; never loads.
|
||||
std::shared_ptr<Ledger const>
|
||||
findResidentLedger(std::function<bool(LedgerInfo const&)> const& match);
|
||||
|
||||
std::vector<std::shared_ptr<Ledger const>>
|
||||
findNewLedgersToPublish(std::unique_lock<std::recursive_mutex>&);
|
||||
|
||||
@@ -375,14 +354,6 @@ private:
|
||||
// The last ledger we handled fetching history
|
||||
std::shared_ptr<Ledger const> mHistLedger;
|
||||
|
||||
// Sliding window of the last ledger_history published ledgers, pinned
|
||||
// so their SHAMaps stay reachable via shared_ptr. Only populated when
|
||||
// [node_db] type=rwdb (null node store), where nothing else can bring
|
||||
// a dropped tree back. Has its own lock so peer requests that consult
|
||||
// it do not contend on m_mutex.
|
||||
std::mutex mutable mRetainedLock;
|
||||
std::deque<std::shared_ptr<Ledger const>> mRetainedLedgers;
|
||||
|
||||
// Fully validated ledger, whether or not we have the ledger resident.
|
||||
std::pair<uint256, LedgerIndex> mLastValidLedger{uint256(), 0};
|
||||
|
||||
@@ -430,10 +401,6 @@ private:
|
||||
// How much history do we want to keep
|
||||
std::uint32_t const ledger_history_;
|
||||
|
||||
// [node_db] type=rwdb: the node store keeps nothing, so recent
|
||||
// ledgers are retained in memory (see mRetainedLedgers).
|
||||
bool const nullBackend_;
|
||||
|
||||
std::uint32_t const ledger_fetch_size_;
|
||||
|
||||
TaggedCache<uint256, Blob> fetch_packs_;
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_LEDGER_MANIFESTSYNC_H_INCLUDED
|
||||
#define RIPPLE_APP_LEDGER_MANIFESTSYNC_H_INCLUDED
|
||||
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpld/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Manifest directory pages read while a ledger is still being acquired.
|
||||
|
||||
Enough entries to fill the manifest cache's ledger tier, which is all a
|
||||
bulk read of the directory can admit. The master keys a node actually
|
||||
depends on are fetched by key instead, wherever they are listed, so a
|
||||
directory grown long by anyone able to fund an account cannot make the
|
||||
early phase of a sync arbitrarily long.
|
||||
*/
|
||||
std::uint64_t constexpr manifestSyncPages = 16;
|
||||
|
||||
/** The nodes a partly synced state map still needs to read its manifests.
|
||||
|
||||
Walks the manifest directory as far as the nodes held allow: the root page;
|
||||
every other page, all at once, since the root names the last of them and
|
||||
pages are only ever added after it; each master object listed; and the
|
||||
ephemeral object each unrevoked one names, which is what a validation is
|
||||
resolved through. Where a key cannot be read, the first node on its path
|
||||
that is not held is reported, once however many keys it blocks.
|
||||
|
||||
Asking peers for the nodes reported and calling again converges in a
|
||||
handful of rounds -- roughly the depth of the map plus one round each for
|
||||
the pages and for the objects they list -- on a set of nodes far smaller
|
||||
than the map. After that every read forEachLedgerManifest() and
|
||||
ManifestCache::applyLedgerDirectory() make succeeds on the partial map.
|
||||
|
||||
A map with no manifest directory converges on the path to where its root
|
||||
would be, and nothing else.
|
||||
|
||||
@param stateMap State map, with at least its root node
|
||||
@param max Most nodes to report
|
||||
@param keys Further keys to read, with the ephemeral object any manifest
|
||||
among them names: in practice keylet::manifest() of each master key on
|
||||
the node's validator lists, which needs no directory to find
|
||||
@param maxPages Most directory pages to read, the root page included
|
||||
|
||||
@return nodes needed, as getMissingNodes() reports them; empty once every
|
||||
manifest can be read
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
missingManifestNodes(
|
||||
SHAMap const& stateMap,
|
||||
std::size_t max,
|
||||
std::vector<uint256> const& keys = {},
|
||||
std::uint64_t maxPages = dirNodeMaxPages);
|
||||
|
||||
/** Ledgers to read manifests from, chosen by how many peers report them.
|
||||
|
||||
A node that has never validated a ledger, and holds no manifest for enough
|
||||
of the validators it trusts, cannot tell which validations to believe. The
|
||||
closed ledgers its peers report are then its only evidence of where the
|
||||
network is, and any recent ledger of the network holds the manifests
|
||||
published on it.
|
||||
|
||||
Choosing wrongly costs little. Every manifest read from a ledger is
|
||||
verified as from any other source and the cache only moves forward, so a
|
||||
ledger a peer made up can at worst leave something out. Reading several
|
||||
candidates, rather than trusting the single most reported, is what keeps
|
||||
one misreporting peer among a few from deciding what is read.
|
||||
|
||||
@param reported The closed ledger each peer last reported, one entry per
|
||||
peer. Zero entries are ignored.
|
||||
@param skip Ledgers already read, or being read
|
||||
@param max Most ledgers to return
|
||||
|
||||
@return distinct ledgers, the most widely reported first. Ties go to the
|
||||
larger hash, as when NetworkOPs picks a ledger by peer count, so the
|
||||
first is usually a ledger being acquired anyway.
|
||||
*/
|
||||
std::vector<uint256>
|
||||
pickManifestCandidates(
|
||||
std::vector<uint256> const& reported,
|
||||
hash_set<uint256> const& skip,
|
||||
std::size_t max);
|
||||
|
||||
/** How many validators need a manifest before a node can trust a ledger.
|
||||
|
||||
@param validators The validators that count: the trusted set once
|
||||
ValidatorList has worked it out, and every listed one before that
|
||||
@param trusted Whether `validators` is the trusted set, and so whether
|
||||
`quorum` has been worked out from it
|
||||
@param quorum ValidatorList::quorum(). Before the trusted set exists this
|
||||
is a placeholder, so the usual 80% of `validators` is assumed instead.
|
||||
|
||||
@return the number needed; never more than `validators`
|
||||
*/
|
||||
std::size_t
|
||||
manifestQuorum(std::size_t validators, bool trusted, std::size_t quorum);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -106,41 +106,6 @@ applyTransactions(
|
||||
bool certainRetry = true;
|
||||
std::size_t count = 0;
|
||||
|
||||
// featureRNG: validator entropy contributions are applied before anything
|
||||
// else, so this ledger's seed is final before any Hook can draw from it.
|
||||
// Pseudo-transactions otherwise sort to an arbitrary position. Each is
|
||||
// attempted exactly once; one that fails is never retried later, where it
|
||||
// could change the seed after Hooks had drawn from it.
|
||||
if (view.rules().enabled(featureRNG))
|
||||
{
|
||||
for (auto it = txns.begin(); it != txns.end();)
|
||||
{
|
||||
if (it->second->getTxnType() != ttENTROPY)
|
||||
{
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto const txid = it->first.getTXID();
|
||||
try
|
||||
{
|
||||
if (applyTransaction(
|
||||
app, view, *it->second, false, tapNONE, j) ==
|
||||
ApplyTransactionResult::Success)
|
||||
++count;
|
||||
else
|
||||
failed.insert(txid);
|
||||
}
|
||||
catch (std::exception const& ex)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "Transaction " << txid << " throws: " << ex.what();
|
||||
failed.insert(txid);
|
||||
}
|
||||
it = txns.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to apply all of the retriable transactions
|
||||
for (int pass = 0; pass < LEDGER_TOTAL_PASSES; ++pass)
|
||||
{
|
||||
|
||||
@@ -21,18 +21,14 @@
|
||||
#include <xrpld/app/ledger/InboundLedger.h>
|
||||
#include <xrpld/app/ledger/InboundLedgers.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/ManifestSync.h>
|
||||
#include <xrpld/app/ledger/TransactionStateSF.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
#include <xrpld/core/JobQueue.h>
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
|
||||
@@ -43,16 +39,6 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Master keys on any of this node's validator lists, trusted or not. */
|
||||
static hash_set<PublicKey>
|
||||
listedMasterKeys(Application& app)
|
||||
{
|
||||
hash_set<PublicKey> keys;
|
||||
app.validators().for_each_listed(
|
||||
[&keys](PublicKey const& pk, bool) { keys.insert(pk); });
|
||||
return keys;
|
||||
}
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
enum {
|
||||
@@ -471,19 +457,6 @@ InboundLedger::done()
|
||||
mLedger->read(keylet::fees()),
|
||||
"ripple::InboundLedger::done : valid ledger fees");
|
||||
mLedger->setImmutable();
|
||||
|
||||
// The state map arrived whole before the early walk in trigger()
|
||||
// finished, as it does when most of it is held locally already, or
|
||||
// when [early_manifest_sync] turns the early walk off. Read the
|
||||
// manifests from the whole map instead, now that it is immutable.
|
||||
// Only as many directory pages as the early walk reads: past those
|
||||
// the ledger tier is full, and the listed master keys are read by
|
||||
// key whatever the directory holds. The keys are collected in the
|
||||
// job: this runs under mtx_, and the validator list has a lock of
|
||||
// its own.
|
||||
if (!mManifestsSynced && wantManifests())
|
||||
syncManifests(std::nullopt, manifestSyncPages);
|
||||
|
||||
switch (mReason)
|
||||
{
|
||||
case Reason::HISTORY:
|
||||
@@ -663,63 +636,6 @@ InboundLedger::trigger(std::shared_ptr<Peer> const& peer, TriggerReason reason)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Partial sync. A node that has not yet validated a ledger cannot
|
||||
// tell which validations to trust until it knows each trusted
|
||||
// validator's current ephemeral key, and on-ledger is where a
|
||||
// rotation or a revocation is published. So the manifests come
|
||||
// first: just the nodes on the paths to the manifest directory and
|
||||
// the objects it lists, which can be read and ingested long before
|
||||
// the rest of the state map arrives.
|
||||
//
|
||||
// The master keys on this node's validator lists are fetched by
|
||||
// key as well as through the directory. They are what a node with
|
||||
// no manifests at all needs, and reading them by key does not
|
||||
// depend on how long the directory has grown.
|
||||
if (!mManifestsSynced && app_.config().EARLY_MANIFEST_SYNC &&
|
||||
wantManifests())
|
||||
{
|
||||
sl.unlock();
|
||||
auto listed = listedMasterKeys(app_);
|
||||
std::vector<uint256> targets;
|
||||
targets.reserve(listed.size());
|
||||
for (auto const& pk : listed)
|
||||
targets.push_back(keylet::manifest(pk).key);
|
||||
auto nodes = missingManifestNodes(
|
||||
mLedger->stateMap(),
|
||||
missingNodesFind,
|
||||
targets,
|
||||
manifestSyncPages);
|
||||
sl.lock();
|
||||
|
||||
if (!failed_ && !complete_ && !mHaveState && !mManifestsSynced)
|
||||
{
|
||||
if (nodes.empty())
|
||||
{
|
||||
syncManifests(std::move(listed), manifestSyncPages);
|
||||
}
|
||||
else
|
||||
{
|
||||
filterNodes(nodes, reason);
|
||||
|
||||
// All recently asked for: fall through to the rest of
|
||||
// the map rather than wait on them.
|
||||
if (!nodes.empty())
|
||||
{
|
||||
tmGL.set_itype(protocol::liAS_NODE);
|
||||
for (auto const& id : nodes)
|
||||
*(tmGL.add_nodeids()) = id.first.getRawString();
|
||||
|
||||
JLOG(journal_.trace())
|
||||
<< "Sending manifest node request ("
|
||||
<< nodes.size() << ") to "
|
||||
<< (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccountStateSF filter(
|
||||
mLedger->stateMap().family().db(), app_.getLedgerMaster());
|
||||
|
||||
@@ -848,73 +764,6 @@ InboundLedger::trigger(std::shared_ptr<Peer> const& peer, TriggerReason reason)
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
InboundLedger::wantManifests()
|
||||
{
|
||||
// Not for history, and not once a ledger has been validated: from then on
|
||||
// the manifests are kept current from each new ledger as it closes.
|
||||
return mReason != Reason::HISTORY &&
|
||||
!app_.getLedgerMaster().haveValidated();
|
||||
}
|
||||
|
||||
void
|
||||
InboundLedger::syncManifests(
|
||||
std::optional<hash_set<PublicKey>> listed,
|
||||
std::uint64_t maxPages)
|
||||
{
|
||||
if (mManifestsSynced.exchange(true))
|
||||
return;
|
||||
|
||||
// Reading verifies a signature per manifest, so it runs as its own job.
|
||||
// The ledger is held by shared_ptr, and reading a map while nodes are
|
||||
// still being added to it is what getMissingNodes() already does.
|
||||
app_.getJobQueue().addJob(
|
||||
jtMANIFEST,
|
||||
"InboundLedger::syncManifests",
|
||||
[ledger = mLedger,
|
||||
listed = std::move(listed),
|
||||
maxPages,
|
||||
&app = app_,
|
||||
j = journal_]() mutable {
|
||||
auto const keys =
|
||||
listed ? std::move(*listed) : listedMasterKeys(app);
|
||||
auto& cache = app.validatorManifests();
|
||||
std::size_t accepted = 0;
|
||||
std::size_t unreadable = 0;
|
||||
|
||||
// By key first, one at a time: on a partial map a key whose path
|
||||
// is not held throws, and must not cost the keys after it. One
|
||||
// can be missing if the lists changed after the nodes were found.
|
||||
for (auto const& pk : keys)
|
||||
{
|
||||
try
|
||||
{
|
||||
accepted += cache.applyLedger(*ledger, {pk});
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
++unreadable;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
accepted += cache.applyLedgerDirectory(*ledger, maxPages);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "Could not read the manifest directory of ledger "
|
||||
<< ledger->info().seq << " during sync: " << e.what();
|
||||
}
|
||||
|
||||
JLOG(j.info()) << "Read manifests from ledger "
|
||||
<< ledger->info().seq << " during sync: " << accepted
|
||||
<< " accepted, " << keys.size() << " listed, "
|
||||
<< unreadable << " not yet readable";
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
InboundLedger::filterNodes(
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>& nodes,
|
||||
|
||||
@@ -113,7 +113,6 @@ LedgerMaster::LedgerMaster(
|
||||
, fetch_depth_(
|
||||
app_.getSHAMapStore().clampFetchDepth(app_.config().FETCH_DEPTH))
|
||||
, ledger_history_(app_.config().LEDGER_HISTORY)
|
||||
, nullBackend_(app_.config().nullBackend())
|
||||
, ledger_fetch_size_(app_.config().getValueFor(SizedItem::ledgerFetch))
|
||||
, fetch_packs_(
|
||||
"FetchPack",
|
||||
@@ -938,20 +937,6 @@ LedgerMaster::setFullLedger(
|
||||
mCompleteLedgers.insert(ledger->info().seq);
|
||||
}
|
||||
|
||||
// Null node store only: pin a sliding window of recently validated
|
||||
// current ledgers so their SHAMap state trees stay resident via
|
||||
// shared_ptr. A persistent node store must not do this. It would pin
|
||||
// ledger_history full trees for nothing, and ledger_history=full
|
||||
// (uint32 max) would never trim. SHAMapStoreImp rejects both zero and
|
||||
// "full" ledger_history for type=rwdb.
|
||||
if (nullBackend_ && isCurrent)
|
||||
{
|
||||
std::lock_guard rl(mRetainedLock);
|
||||
mRetainedLedgers.push_back(ledger);
|
||||
while (mRetainedLedgers.size() > ledger_history_)
|
||||
mRetainedLedgers.pop_front();
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard ml(m_mutex);
|
||||
|
||||
@@ -1867,12 +1852,6 @@ LedgerMaster::getCloseTimeByHash(
|
||||
LedgerHash const& ledgerHash,
|
||||
std::uint32_t index)
|
||||
{
|
||||
// Resident ledgers first (getLedgerByHash would load a cache miss).
|
||||
// Close time is a header field, so fall through to the node-store
|
||||
// header when nothing is already in memory.
|
||||
if (auto const ledger = getResidentLedgerByHash(ledgerHash))
|
||||
return ledger->info().closeTime;
|
||||
|
||||
auto nodeObject = app_.getNodeStore().fetchNodeObject(ledgerHash, index);
|
||||
if (nodeObject && (nodeObject->getData().size() >= 120))
|
||||
{
|
||||
@@ -2017,44 +1996,6 @@ LedgerMaster::getLedgerByHash(uint256 const& hash)
|
||||
return {};
|
||||
}
|
||||
|
||||
std::shared_ptr<Ledger const>
|
||||
LedgerMaster::findResidentLedger(
|
||||
std::function<bool(LedgerInfo const&)> const& match)
|
||||
{
|
||||
for (auto const& ledger : {mClosedLedger.get(), mValidLedger.get()})
|
||||
{
|
||||
if (ledger && match(ledger->info()))
|
||||
return ledger;
|
||||
}
|
||||
|
||||
std::lock_guard lock(mRetainedLock);
|
||||
auto const it = std::find_if(
|
||||
mRetainedLedgers.rbegin(),
|
||||
mRetainedLedgers.rend(),
|
||||
[&](auto const& ledger) { return match(ledger->info()); });
|
||||
return it == mRetainedLedgers.rend() ? nullptr : *it;
|
||||
}
|
||||
|
||||
std::shared_ptr<Ledger const>
|
||||
LedgerMaster::getResidentLedgerByHash(uint256 const& hash)
|
||||
{
|
||||
if (auto ret = mLedgerHistory.getCachedLedger(hash))
|
||||
return ret;
|
||||
return findResidentLedger(
|
||||
[&](LedgerInfo const& info) { return info.hash == hash; });
|
||||
}
|
||||
|
||||
std::shared_ptr<Ledger const>
|
||||
LedgerMaster::getResidentLedgerBySeq(std::uint32_t seq)
|
||||
{
|
||||
// getLedgerHash only reads the in-memory index; a zero hash misses.
|
||||
if (auto ret =
|
||||
mLedgerHistory.getCachedLedger(mLedgerHistory.getLedgerHash(seq)))
|
||||
return ret;
|
||||
return findResidentLedger(
|
||||
[&](LedgerInfo const& info) { return info.seq == seq; });
|
||||
}
|
||||
|
||||
void
|
||||
LedgerMaster::setLedgerRangePresent(
|
||||
std::uint32_t minV,
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/ledger/ManifestSync.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
static_assert(
|
||||
manifestSyncPages * dirNodeMaxEntries >= ManifestCache::ledgerCapacity,
|
||||
"the early read must be able to fill the ledger tier");
|
||||
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
missingManifestNodes(
|
||||
SHAMap const& stateMap,
|
||||
std::size_t max,
|
||||
std::vector<uint256> const& keys,
|
||||
std::uint64_t maxPages)
|
||||
{
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>> missing;
|
||||
|
||||
// By hash: several keys blocked by the same node report it once.
|
||||
hash_set<uint256> reported;
|
||||
hash_set<uint256> visited;
|
||||
|
||||
auto const& root = keylet::manifestDir();
|
||||
|
||||
// The manifest directory's own pages: the only directory nodes whose
|
||||
// entries are followed. The ledger walked is not validated, and one a
|
||||
// peer made up could otherwise lead the walk from directory to directory
|
||||
// through as much of the map as it cared to serve.
|
||||
hash_set<uint256> pages{root.key};
|
||||
|
||||
// A stack, so the keys asked for by name go last and are read first: they
|
||||
// are the ones that decide whether this node can trust a validation.
|
||||
std::vector<uint256> pending{root.key};
|
||||
pending.insert(pending.end(), keys.rbegin(), keys.rend());
|
||||
|
||||
maxPages = std::clamp<std::uint64_t>(maxPages, 1, dirNodeMaxPages);
|
||||
|
||||
while (!pending.empty() && missing.size() < max)
|
||||
{
|
||||
auto const key = pending.back();
|
||||
pending.pop_back();
|
||||
|
||||
if (!visited.insert(key).second)
|
||||
continue;
|
||||
|
||||
std::optional<std::pair<SHAMapNodeID, uint256>> gap;
|
||||
auto const item = stateMap.peekItemPartial(key, gap);
|
||||
|
||||
if (gap)
|
||||
{
|
||||
if (reported.insert(gap->second).second)
|
||||
missing.push_back(*gap);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!item)
|
||||
continue;
|
||||
|
||||
std::optional<SLE const> sle;
|
||||
try
|
||||
{
|
||||
sle.emplace(SerialIter{item->slice()}, item->key());
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
// Not this walk's to judge: the node verified against its hash,
|
||||
// so a full read of the ledger would meet the same object.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sle->getType() == ltDIR_NODE)
|
||||
{
|
||||
if (!pages.contains(key))
|
||||
continue;
|
||||
|
||||
for (auto const& k : sle->getFieldV256(sfIndexes))
|
||||
pending.push_back(k);
|
||||
|
||||
// The root names the last page, every page in use has an index no
|
||||
// greater, and the directory only ever grows at its end. Asking
|
||||
// for them all now saves a round trip per page; an index not in
|
||||
// use costs at most the node that shows it absent.
|
||||
if (key == root.key)
|
||||
{
|
||||
auto const last = std::min<std::uint64_t>(
|
||||
sle->getFieldU64(sfIndexPrevious), maxPages - 1);
|
||||
for (std::uint64_t i = 1; i <= last; ++i)
|
||||
{
|
||||
auto const pageKey = keylet::page(root, i).key;
|
||||
pages.insert(pageKey);
|
||||
pending.push_back(pageKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (
|
||||
sle->getType() == ltMANIFEST &&
|
||||
sle->isFieldPresent(sfSigningPubKey) &&
|
||||
sle->getFieldU32(sfSequence) !=
|
||||
std::numeric_limits<std::uint32_t>::max())
|
||||
{
|
||||
auto const signing = sle->getFieldVL(sfSigningPubKey);
|
||||
if (publicKeyType(makeSlice(signing)))
|
||||
pending.push_back(
|
||||
keylet::manifest(PublicKey{makeSlice(signing)}).key);
|
||||
}
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
std::vector<uint256>
|
||||
pickManifestCandidates(
|
||||
std::vector<uint256> const& reported,
|
||||
hash_set<uint256> const& skip,
|
||||
std::size_t max)
|
||||
{
|
||||
hash_map<uint256, std::size_t> counts;
|
||||
for (auto const& hash : reported)
|
||||
if (hash.isNonZero() && !skip.contains(hash))
|
||||
++counts[hash];
|
||||
|
||||
std::vector<std::pair<std::size_t, uint256>> ranked;
|
||||
ranked.reserve(counts.size());
|
||||
for (auto const& [hash, count] : counts)
|
||||
ranked.emplace_back(count, hash);
|
||||
|
||||
auto const keep = std::min(max, ranked.size());
|
||||
std::partial_sort(
|
||||
ranked.begin(),
|
||||
ranked.begin() + keep,
|
||||
ranked.end(),
|
||||
[](auto const& a, auto const& b) { return a > b; });
|
||||
|
||||
std::vector<uint256> picked;
|
||||
picked.reserve(keep);
|
||||
for (std::size_t i = 0; i < keep; ++i)
|
||||
picked.push_back(ranked[i].second);
|
||||
return picked;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
manifestQuorum(std::size_t validators, bool trusted, std::size_t quorum)
|
||||
{
|
||||
if (trusted)
|
||||
return std::min(quorum, validators);
|
||||
|
||||
// ceil(0.8 * validators), in integers.
|
||||
return (validators * 4 + 4) / 5;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
Loaded 100 of 181 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user