mirror of
https://github.com/Xahau/xahaud.git
synced 2026-10-22 03:37:08 +00:00
Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d00580a52 | ||
|
|
5db97be877 | ||
|
|
d70fd6fa72 | ||
|
|
f9747d1633 | ||
|
|
0074cf9e95 | ||
|
|
b9f2914c0e | ||
|
|
023f55e283 | ||
|
|
d20e8ffb56 | ||
|
|
c63ee8cc5f | ||
|
|
64a30b2a15 | ||
|
|
0025355f43 | ||
|
|
7046616cdb | ||
|
|
a2e7f3c645 | ||
|
|
004752d1c2 | ||
|
|
22e6ad21b4 | ||
|
|
3423ee4665 | ||
|
|
de7afa9895 | ||
|
|
983c5dfe0c | ||
|
|
47b00e9a65 | ||
|
|
ee1d44f7d6 | ||
|
|
5995cc2d00 | ||
|
|
e9cc9e6fcb | ||
|
|
350347c615 | ||
|
|
7112b80d8f | ||
|
|
3de9cc3ba7 | ||
|
|
e35ae4af20 | ||
|
|
d916f108da | ||
|
|
a09c180a93 | ||
|
|
33939f0b02 | ||
|
|
779a07804b | ||
|
|
60caaea495 | ||
|
|
4bbcc2d953 | ||
|
|
72d793d912 | ||
|
|
db25f9799c | ||
|
|
e470d8a70b | ||
|
|
4fb88f5f80 | ||
|
|
b2cdcf429e | ||
|
|
3fd6ee472a | ||
|
|
b2aeb1cd92 | ||
|
|
0f56d2d807 | ||
|
|
e904290e4e | ||
|
|
723103150c |
No files matched your search
@@ -26,7 +26,7 @@ Loop: xrpld.app xrpld.nodestore
|
||||
xrpld.app > xrpld.nodestore
|
||||
|
||||
Loop: xrpld.app xrpld.overlay
|
||||
xrpld.overlay ~= xrpld.app
|
||||
xrpld.overlay == xrpld.app
|
||||
|
||||
Loop: xrpld.app xrpld.peerfinder
|
||||
xrpld.app > xrpld.peerfinder
|
||||
|
||||
@@ -12,6 +12,7 @@ libxrpl.server > xrpl.basics
|
||||
libxrpl.server > xrpl.json
|
||||
libxrpl.server > xrpl.protocol
|
||||
libxrpl.server > xrpl.server
|
||||
test.app > test.shamap
|
||||
test.app > test.toplevel
|
||||
test.app > test.unit_test
|
||||
test.app > xrpl.basics
|
||||
@@ -21,6 +22,7 @@ test.app > xrpld.ledger
|
||||
test.app > xrpld.nodestore
|
||||
test.app > xrpld.overlay
|
||||
test.app > xrpld.rpc
|
||||
test.app > xrpld.shamap
|
||||
test.app > xrpl.hook
|
||||
test.app > xrpl.json
|
||||
test.app > xrpl.protocol
|
||||
@@ -101,6 +103,7 @@ test.overlay > xrpld.overlay
|
||||
test.overlay > xrpld.peerfinder
|
||||
test.overlay > xrpld.shamap
|
||||
test.overlay > xrpl.protocol
|
||||
test.overlay > xrpl.resource
|
||||
test.peerfinder > test.beast
|
||||
test.peerfinder > test.unit_test
|
||||
test.peerfinder > xrpl.basics
|
||||
@@ -124,6 +127,7 @@ test.rpc > xrpl.basics
|
||||
test.rpc > xrpld.app
|
||||
test.rpc > xrpld.core
|
||||
test.rpc > xrpld.net
|
||||
test.rpc > xrpld.nodestore
|
||||
test.rpc > xrpld.overlay
|
||||
test.rpc > xrpld.rpc
|
||||
test.rpc > xrpld.shamap
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ else
|
||||
# GH Action, runner
|
||||
if [[ "$(git rev-parse --abbrev-ref HEAD)" == "release" ]]; then
|
||||
echo "building on the release branch... placing it in builds/candidate"
|
||||
mkdir /data/builds/candidate
|
||||
mkdir -p /data/builds/candidate
|
||||
cp /io/release-build/xahaud /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4
|
||||
cp /io/release-build/release.info /data/builds/candidate/$(date +%Y).$(date +%-m).$(date +%-d)-$(git rev-parse --abbrev-ref HEAD)+$4.releaseinfo
|
||||
else
|
||||
|
||||
@@ -166,10 +166,13 @@
|
||||
# ws Websockets
|
||||
# wss Secure Websockets
|
||||
# peer Peer Protocol
|
||||
# pwa On-ledger AppLoader documents (featurePWALoader), for a
|
||||
# reverse proxy only. Read "pwa ports" below first.
|
||||
#
|
||||
# Restrictions:
|
||||
#
|
||||
# Only one port may be configured to support the peer protocol.
|
||||
# The pwa protocol cannot be combined with any other protocol.
|
||||
# A port cannot have websocket and non websocket protocols at the
|
||||
# same time. It is possible have both Websockets and Secure Websockets
|
||||
# together in one port.
|
||||
@@ -264,6 +267,119 @@
|
||||
# "secure_gateway" connections, then they will be treated as
|
||||
# "admin" addresses.
|
||||
#
|
||||
# pwa ports
|
||||
#
|
||||
# A port whose protocol is "pwa" serves accounts' on-ledger AppLoader
|
||||
# documents (featurePWALoader) as HTML. This makes the node a web host
|
||||
# for third-party content: anyone can publish a loader to an account
|
||||
# for about a drop per byte, and nothing in protocol takes one down.
|
||||
# Hosting, and a hosting provider's obligations, are yours.
|
||||
#
|
||||
# One host per account, on a zone used for nothing else:
|
||||
#
|
||||
# https://pwa<account-index>.<domain>/<r-address>
|
||||
# https://<domain>/<r-address> 301 to the above
|
||||
# https://<custom host>/ see custom_domains
|
||||
#
|
||||
# The first label of the Host header is "pwa" followed by the decimal
|
||||
# AccountIndex of the account (an account created before AccountIndex
|
||||
# existed has no host); the rest must be one of the port's "domain"
|
||||
# values; the path must be that account's exact r-address. One
|
||||
# trailing slash and a query string are ignored; anything else is
|
||||
# 404. The bare zone never serves a document (every page would share
|
||||
# one origin): it redirects, keeping :port and query.
|
||||
# Never set cookies at or above the zone and never serve your own
|
||||
# site from it. Wildcard DNS and a wildcard certificate cover it.
|
||||
#
|
||||
# Rules enforced at startup and on every request:
|
||||
#
|
||||
# - "pwa" is the port's only protocol; no admin, user/password or
|
||||
# ssl_* keys (terminate TLS at the proxy).
|
||||
# - "domain" is required: the zone(s), comma-separated. The proxy must
|
||||
# forward the Host header unchanged.
|
||||
# - "secure_gateway" is required and may not cover every address.
|
||||
# Every request must come from it carrying X-Forwarded-For, whose
|
||||
# RIGHTMOST entry (the one the proxy wrote) is the client. The
|
||||
# "Forwarded" header is ignored.
|
||||
# - Clients are metered per IPv4 address, per IPv6 /64; a miss costs
|
||||
# what a hit does; over-use is answered 503 until it decays.
|
||||
# - GET and HEAD only; every response closes the connection; only
|
||||
# validated state is served, with an ETag (304 on If-None-Match).
|
||||
# X-NetworkID, X-LedgerSeq, X-LedgerHash and X-PWASeq name the
|
||||
# validated ledger and AppLoaderSequence the document came from, so
|
||||
# a client (a browser extension, say) can vet it against a second
|
||||
# node with the pwa_validate RPC (account, hash of the bytes, host).
|
||||
# - Lookups run on at most two job queue threads, below client RPC;
|
||||
# beyond a bounded backlog they are refused with 503. Put a cache in
|
||||
# front (the ETag makes revalidation cheap).
|
||||
# - The admin RPC "pwa_info" reports requests by outcome and bytes
|
||||
# served; "reset": true zeroes them, "flush_dns": true drops the
|
||||
# custom domain cache (CLI: xahaud pwa_info [reset] [flush_dns]).
|
||||
#
|
||||
# Example (also add port_pwa to the [server] list):
|
||||
#
|
||||
# [port_pwa]
|
||||
# port = 8088
|
||||
# ip = 127.0.0.1
|
||||
# protocol = pwa
|
||||
# secure_gateway = 127.0.0.1
|
||||
# domain = pwapp.example
|
||||
#
|
||||
# with nginx in front, TLS terminated there, Host passed through:
|
||||
#
|
||||
# server {
|
||||
# listen 443 ssl;
|
||||
# server_name pwapp.example ~^pwa[0-9]+\.pwapp\.example$;
|
||||
# ssl_certificate /etc/ssl/wildcard.pwapp.example.pem;
|
||||
# ssl_certificate_key /etc/ssl/wildcard.pwapp.example.key;
|
||||
# location / {
|
||||
# proxy_pass http://127.0.0.1:8088;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Forwarded-For $remote_addr;
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# custom_domains = 0 | 1
|
||||
#
|
||||
# Default 0. With 1, a request whose Host is outside every zone is
|
||||
# served as the account named by DNS, so a domain owner can point
|
||||
# their own name at your proxy:
|
||||
#
|
||||
# app.example.org. CNAME pwa-proxy.your.example.
|
||||
# _xahau-pwa.app.example.org. TXT "rAbC..."
|
||||
#
|
||||
# The record lives on the _xahau-pwa label beneath the host because
|
||||
# the host is usually a CNAME to your proxy. Exactly one address must
|
||||
# be present there (other TXT strings are ignored; two addresses count
|
||||
# as none). The named account must consent: its Domain field must
|
||||
# equal the host or a parent of it (ignoring case), else 503. Lists
|
||||
# apply as at the account's own host; only the root path is a
|
||||
# document. No usable record is 404 (cached 15 minutes), a resolver
|
||||
# failure 503 (cached 60 s), a record is cached for its TTL (clamped
|
||||
# to 60..3600 s). Lookups run on their own job queue threads, at most
|
||||
# four at a time and one per host; a request that misses the cache is
|
||||
# answered 429 with Retry-After and a refreshing page rather than
|
||||
# waiting, so a flood of Host headers cannot tie up the node. The
|
||||
# admin RPC pwa_info with "flush_dns": true drops the cache. Your
|
||||
# proxy must accept, and hold certificates for, the hosts you will
|
||||
# serve (e.g. Caddy on-demand TLS); xahaud only sees the Host header.
|
||||
#
|
||||
# resolver = <IPv4 address>[:port]
|
||||
#
|
||||
# The recursive resolver for custom_domains; default is the system
|
||||
# resolver. xahaud does not validate DNSSEC, so prefer a validating
|
||||
# resolver on the same host.
|
||||
#
|
||||
# Documents are served with "Content-Security-Policy: sandbox
|
||||
# allow-scripts ...": an opaque origin that cannot read anything from
|
||||
# the node but can still send requests anywhere, including the
|
||||
# visitor's own machine, so keep admin ports behind credentials.
|
||||
#
|
||||
# [pwa_allowlist] and [pwa_blocklist], one r-address per line, apply to
|
||||
# every pwa port: if the allowlist is non-empty only those accounts are
|
||||
# served; blocklisted accounts are never served (403), whatever the
|
||||
# allowlist says. An account on both is a startup error.
|
||||
#
|
||||
# ssl_key = <filename>
|
||||
# ssl_cert = <filename>
|
||||
# ssl_chain = <filename>
|
||||
@@ -448,6 +564,30 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
# [udp_highway]
|
||||
#
|
||||
# 0 or 1.
|
||||
#
|
||||
# 0: Disable the Xahau UDP Superhighway (XUSH). [default]
|
||||
# 1: Enable the Xahau UDP Superhighway (XUSH).
|
||||
#
|
||||
# The UDP Superhighway is a best-effort transport that runs alongside the
|
||||
# peer protocol. When it is enabled the server also listens for UDP
|
||||
# datagrams on its peer port number, and every transaction it relays to its
|
||||
# peers is also sent, over UDP, to a small random set of other servers that
|
||||
# have the highway enabled. It never replaces the peer protocol: anything
|
||||
# sent over the highway is also relayed normally.
|
||||
#
|
||||
# To receive highway traffic, inbound UDP must be allowed to the peer port
|
||||
# (in addition to inbound TCP).
|
||||
#
|
||||
# A server that keeps its address private (peer_private = 1, or any server
|
||||
# configured as a validator) only exchanges highway traffic with the peers
|
||||
# listed in [ips_fixed], and never advertises highway endpoints. Fixed peers
|
||||
# are never advertised to other servers.
|
||||
#
|
||||
#
|
||||
#
|
||||
# [peers_max]
|
||||
#
|
||||
# The largest number of desired peer connections (incoming or outgoing).
|
||||
@@ -674,6 +814,38 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
# [early_manifest_sync]
|
||||
#
|
||||
# Controls whether, while acquiring its first ledger, the server fetches and
|
||||
# reads the on-ledger validator manifests ahead of the rest of that ledger's
|
||||
# state. Doing so lets it resolve validators' current signing keys, including
|
||||
# rotations and revocations published on-ledger, within seconds of starting
|
||||
# a sync rather than once the whole state has arrived.
|
||||
#
|
||||
# The manifests read are those of the master keys on the server's own
|
||||
# validator lists, fetched by key, and as many more from the on-ledger
|
||||
# manifest directory as the manifest cache keeps.
|
||||
#
|
||||
# The cost is a dozen or so request/response rounds with peers. With more
|
||||
# than one peer serving the ledger these overlap with fetching the rest of
|
||||
# its state; with only one, they come first.
|
||||
#
|
||||
# It also lets a server that starts with nothing but the master keys of its
|
||||
# validators and a few peers find its way. Until it has validated a ledger,
|
||||
# while it holds manifests for too few of its validators to make a quorum,
|
||||
# it reads them from up to three of the ledgers its peers report, the most
|
||||
# widely reported first, every 15 seconds. Every manifest read is checked
|
||||
# against its master key's signature, so a ledger reported by a peer that
|
||||
# is not on the network can leave something out but cannot add anything.
|
||||
#
|
||||
# Legal values are:
|
||||
# 1 - Fetch the manifests first, and from peers' ledgers if needed
|
||||
# 0 - Fetch the ledger's state in the usual order, and read the manifests
|
||||
# from it once it is complete
|
||||
#
|
||||
# The default is 1. A ledger without on-ledger manifests costs only the few
|
||||
# nodes on the paths to where their directory and objects would be.
|
||||
#
|
||||
#
|
||||
#
|
||||
# [ledger_history]
|
||||
@@ -1416,6 +1588,19 @@
|
||||
# contains breaking changes that require a new API version number.
|
||||
# They are not ready for public consumption.
|
||||
#
|
||||
# [hook_validate_rpc]
|
||||
#
|
||||
# 0 or 1.
|
||||
#
|
||||
# 0: Disable the hook_validate RPC method
|
||||
# 1: Enable the hook_validate RPC method, which runs the Hook guard
|
||||
# checker and WASM VM validation against caller-supplied code using
|
||||
# the current open ledger's rules.
|
||||
#
|
||||
# The default is 0 on networked nodes and 1 in standalone mode (-a).
|
||||
# When enabled the method is available to non-admin (user role) clients,
|
||||
# so consider the CPU cost before enabling it on a public-facing node.
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# 10. Example Settings
|
||||
|
||||
@@ -151,4 +151,3 @@ D686F2538F410C9D0D856788E98E3579595DAF7B38D38887F81ECAC934B06040 HooksUpdate1
|
||||
4B8466415FAB32FFA89D9DCBE166A42340115771DF611A7160F8D7439C87ECD8 fixNSDelete
|
||||
EDB4EE4C524E16BDD91D9A529332DED08DCAAA51CC6DC897ACFA1A0ED131C5B6 fix240819
|
||||
8063140E9260799D6716756B891CEC3E7006C4E4F277AB84670663A88F94B9C4 fixPageCap
|
||||
88693F108C3CD8A967F3F4253A32DEF5E35F9406ACD2A11B88B11D90865763A9 fix240911
|
||||
@@ -97,6 +97,7 @@ target_link_libraries (ripple_syslibs
|
||||
crypt32
|
||||
>
|
||||
$<$<NOT:$<BOOL:${MSVC}>>:dl>
|
||||
$<$<NOT:$<BOOL:${MSVC}>>:resolv>
|
||||
$<$<NOT:$<OR:$<BOOL:${MSVC}>,$<BOOL:${APPLE}>>>:rt>)
|
||||
|
||||
if (NOT MSVC)
|
||||
|
||||
@@ -47,5 +47,6 @@
|
||||
#define MEM_OVERLAP -43
|
||||
#define TOO_MANY_STATE_MODIFICATIONS -44
|
||||
#define TOO_MANY_NAMESPACES -45
|
||||
#define TOO_LITTLE_ENTROPY -46
|
||||
#define HOOK_ERROR_CODES
|
||||
#endif //HOOK_ERROR_CODES
|
||||
@@ -339,6 +339,12 @@ prepare(
|
||||
uint32_t read_ptr,
|
||||
uint32_t read_len);
|
||||
|
||||
extern int64_t
|
||||
dice(uint32_t sides);
|
||||
|
||||
extern int64_t
|
||||
util_random(uint32_t write_ptr, uint32_t write_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+1
-1
@@ -46,8 +46,8 @@
|
||||
|
||||
#include "error.h"
|
||||
#include "extern.h"
|
||||
#include "sfcodes.h"
|
||||
#include "macro.h"
|
||||
#include "sfcodes.h"
|
||||
#include "tts.h"
|
||||
|
||||
#include "ls_flags.h"
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#define sfHookApiVersion ((1U << 16U) + 20U)
|
||||
#define sfHookStateScale ((1U << 16U) + 21U)
|
||||
#define sfLedgerFixType ((1U << 16U) + 22U)
|
||||
#define sfEntropyCount ((1U << 16U) + 99U)
|
||||
#define sfEntropyDenominator ((1U << 16U) + 100U)
|
||||
#define sfNetworkID ((2U << 16U) + 1U)
|
||||
#define sfFlags ((2U << 16U) + 2U)
|
||||
#define sfSourceTag ((2U << 16U) + 3U)
|
||||
@@ -80,6 +82,7 @@
|
||||
#define sfRewardTime ((2U << 16U) + 98U)
|
||||
#define sfRewardLgrFirst ((2U << 16U) + 99U)
|
||||
#define sfRewardLgrLast ((2U << 16U) + 100U)
|
||||
#define sfAppLoaderSequence ((2U << 16U) + 101U)
|
||||
#define sfIndexNext ((3U << 16U) + 1U)
|
||||
#define sfIndexPrevious ((3U << 16U) + 2U)
|
||||
#define sfBookNode ((3U << 16U) + 3U)
|
||||
@@ -105,6 +108,9 @@
|
||||
#define sfMPTAmount ((3U << 16U) + 26U)
|
||||
#define sfIssuerNode ((3U << 16U) + 27U)
|
||||
#define sfSubjectNode ((3U << 16U) + 28U)
|
||||
#define sfLastNoteTime ((3U << 16U) + 94U)
|
||||
#define sfLastTxnTime ((3U << 16U) + 95U)
|
||||
#define sfTime ((3U << 16U) + 96U)
|
||||
#define sfTouchCount ((3U << 16U) + 97U)
|
||||
#define sfAccountIndex ((3U << 16U) + 98U)
|
||||
#define sfAccountCount ((3U << 16U) + 99U)
|
||||
@@ -152,6 +158,8 @@
|
||||
#define sfEscrowID ((5U << 16U) + 35U)
|
||||
#define sfURITokenID ((5U << 16U) + 36U)
|
||||
#define sfDomainID ((5U << 16U) + 37U)
|
||||
#define sfNextRandomDigest ((5U << 16U) + 89U)
|
||||
#define sfRandomData ((5U << 16U) + 90U)
|
||||
#define sfManifestID ((5U << 16U) + 91U)
|
||||
#define sfHookOnOutgoing ((5U << 16U) + 93U)
|
||||
#define sfHookOnIncoming ((5U << 16U) + 94U)
|
||||
@@ -222,6 +230,10 @@
|
||||
#define sfProvider ((7U << 16U) + 30U)
|
||||
#define sfMPTokenMetadata ((7U << 16U) + 31U)
|
||||
#define sfCredentialType ((7U << 16U) + 32U)
|
||||
#define sfNote ((7U << 16U) + 93U)
|
||||
#define sfNoteSignature ((7U << 16U) + 94U)
|
||||
#define sfJsonTxDelta ((7U << 16U) + 95U)
|
||||
#define sfAppLoader ((7U << 16U) + 96U)
|
||||
#define sfHookName ((7U << 16U) + 97U)
|
||||
#define sfRemarkValue ((7U << 16U) + 98U)
|
||||
#define sfRemarkName ((7U << 16U) + 99U)
|
||||
@@ -289,6 +301,7 @@
|
||||
#define sfXChainCreateAccountAttestationCollectionElement ((14U << 16U) + 31U)
|
||||
#define sfPriceData ((14U << 16U) + 32U)
|
||||
#define sfCredential ((14U << 16U) + 33U)
|
||||
#define sfRandomDigestEntry ((14U << 16U) + 88U)
|
||||
#define sfManifest ((14U << 16U) + 90U)
|
||||
#define sfAmountEntry ((14U << 16U) + 91U)
|
||||
#define sfMintURIToken ((14U << 16U) + 92U)
|
||||
@@ -321,6 +334,7 @@
|
||||
#define sfAuthorizeCredentials ((15U << 16U) + 26U)
|
||||
#define sfUnauthorizeCredentials ((15U << 16U) + 27U)
|
||||
#define sfAcceptedCredentials ((15U << 16U) + 28U)
|
||||
#define sfRandomDigests ((15U << 16U) + 91U)
|
||||
#define sfAmounts ((15U << 16U) + 92U)
|
||||
#define sfHookEmissions ((15U << 16U) + 93U)
|
||||
#define sfImportVLKeys ((15U << 16U) + 94U)
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
#define ttNFTOKEN_MODIFY 70
|
||||
#define ttPERMISSIONED_DOMAIN_SET 71
|
||||
#define ttPERMISSIONED_DOMAIN_DELETE 72
|
||||
#define ttENTROPY 89
|
||||
#define ttNOTE_CLAIM 90
|
||||
#define ttMANIFEST_SET 91
|
||||
#define ttCRON 92
|
||||
#define ttCRON_SET 93
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#define uint256 std::string
|
||||
#define featureHooksUpdate1 "1"
|
||||
#define featureHooksUpdate2 "1"
|
||||
#define featureRNG "1"
|
||||
#define fix20250131 "1"
|
||||
#define fixGuardDepth32 "1"
|
||||
namespace hook_api {
|
||||
@@ -385,7 +386,9 @@ enum class hook_return_code : int64_t {
|
||||
MEM_OVERLAP = -43, // one or more specified buffers are the same memory
|
||||
TOO_MANY_STATE_MODIFICATIONS = -44, // more than 256 modified state
|
||||
// entires in the combined hook chains
|
||||
TOO_MANY_NAMESPACES = -45
|
||||
TOO_MANY_NAMESPACES = -45,
|
||||
TOO_LITTLE_ENTROPY = -46 // featureRNG: no seed for this ledger, or too few
|
||||
// validators contributed to it
|
||||
};
|
||||
|
||||
enum class ExitType : uint8_t {
|
||||
@@ -398,6 +401,7 @@ enum class ExitType : uint8_t {
|
||||
const uint16_t max_state_modifications = 256;
|
||||
const uint8_t max_slots = 255;
|
||||
const uint8_t max_nonce = 255;
|
||||
const uint16_t max_rng_draws = 256; // featureRNG, per Hook execution
|
||||
const uint8_t max_emit = 255;
|
||||
const uint8_t max_params = 16;
|
||||
const double fee_base_multiplier = 1.1f;
|
||||
|
||||
@@ -1096,7 +1096,11 @@ validateGuards(
|
||||
{
|
||||
int name_len = parseLeb128(wasm, i, &i);
|
||||
CHECK_SHORT_HOOK();
|
||||
if (name_len == 4)
|
||||
// Only compare the name if all four bytes are in bounds. A
|
||||
// name running past the end is rejected as a short hook below
|
||||
// either way, so this does not change which hooks are valid.
|
||||
if (name_len == 4 &&
|
||||
static_cast<std::size_t>(i) + 4 <= wasm.size())
|
||||
{
|
||||
if (wasm[i] == 'h' && wasm[i + 1] == 'o' &&
|
||||
wasm[i + 2] == 'o' && wasm[i + 3] == 'k')
|
||||
|
||||
@@ -298,4 +298,12 @@ HOOK_API_DEFINITION(
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, prepare, (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len),
|
||||
featureHooksUpdate2)
|
||||
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, dice, (uint32_t sides),
|
||||
featureRNG)
|
||||
|
||||
HOOK_API_DEFINITION(
|
||||
int64_t, util_random, (uint32_t write_ptr, uint32_t write_len),
|
||||
featureRNG)
|
||||
// clang-format on
|
||||
@@ -0,0 +1,66 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_PROTOCOL_APPLOADER_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_APPLOADER_H_INCLUDED
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Validation of an AppLoader document (sfAppLoader), the bootstrap page of
|
||||
a Progressive Web App served from an account.
|
||||
|
||||
Consensus cannot afford a real HTML parser, so this is a shallow sanity
|
||||
gate and nothing more. A document must be non-empty and at most
|
||||
maxAppLoaderLength bytes; well-formed UTF-8; free of C0 control
|
||||
characters other than TAB, LF, FF and CR, and of DEL; begin, after an
|
||||
optional BOM and whitespace, with an HTML doctype or an <html start tag;
|
||||
and contain an <html start tag followed later by a </html> end tag with
|
||||
nothing but whitespace after the last one. Nothing parses comments,
|
||||
scripts or attributes, and content is not policed.
|
||||
|
||||
Consensus critical: a change to what it accepts needs an amendment.
|
||||
*/
|
||||
namespace appLoader {
|
||||
|
||||
enum class Result : std::uint8_t {
|
||||
ok = 0,
|
||||
empty,
|
||||
tooLarge,
|
||||
badUTF8,
|
||||
badControlChar,
|
||||
noDoctype, // does not open with a doctype or <html tag
|
||||
noHtmlElement, // no <html start tag
|
||||
unclosed, // no </html> end tag after it
|
||||
trailingGarbage, // non-whitespace after the last </html>
|
||||
};
|
||||
|
||||
/** Total: every input yields a result. `data` may be null iff size is 0. */
|
||||
Result
|
||||
validate(std::uint8_t const* data, std::size_t size);
|
||||
|
||||
char const*
|
||||
to_string(Result r);
|
||||
|
||||
} // namespace appLoader
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,92 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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_PROTOCOL_CREDITNOTE_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_CREDITNOTE_H_INCLUDED
|
||||
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STAmount.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Credit notes (featureCreditNote)
|
||||
//
|
||||
// A note is a bearer instrument the issuer signs blind, typically offline:
|
||||
// "whoever holds this may take Amount from my account until Expiration".
|
||||
// It is a JsonTx document in the sense of featureJsonTx - the same character
|
||||
// set, number rules and signing prefix as a plaintext-signed transaction - so
|
||||
// anything that can sign a JsonTx preimage can sign a note, and the bytes the
|
||||
// issuer read are the bytes the ledger checks. There is no canonical form and
|
||||
// no delta: NoteClaim carries the note verbatim.
|
||||
//
|
||||
// The document is exactly one object with exactly these five members, in any
|
||||
// order, spelled exactly so (no case folding - what you see is what you get):
|
||||
//
|
||||
// "NetworkID" bare integer the network the note redeems on
|
||||
// "Account" r-address the issuer; who pays
|
||||
// "Amount" bare integer drops of the native currency, or
|
||||
// object {"currency","issuer","value"}, all strings,
|
||||
// an issued currency as a Payment spells it
|
||||
// "IssueTime" ISO 8601 Date().toISOString(), as sfTime renders
|
||||
// "Expiration" ISO 8601 after IssueTime, at most creditNoteMaxLifeMs
|
||||
// later
|
||||
//
|
||||
// TransactionType is not a member, and IssueTime is not a transaction field,
|
||||
// so no note is a valid transaction preimage and no transaction preimage is
|
||||
// a valid note: the shared signing prefix needs no further separation.
|
||||
//
|
||||
// Replay protection is the issuer's AccountRoot sfLastNoteTime: a note can
|
||||
// be redeemed only while its IssueTime is strictly greater, and redeeming
|
||||
// records its IssueTime there. Each note redeems at most once, and redeeming
|
||||
// one retires every unredeemed note the issuer signed before it.
|
||||
//
|
||||
// CONSENSUS SURFACE. Everything in parseCreditNote decides whether a note is
|
||||
// valid, and is a consensus rule once featureCreditNote activates.
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
struct CreditNote
|
||||
{
|
||||
std::uint32_t networkID = 0;
|
||||
AccountID account;
|
||||
STAmount amount;
|
||||
std::uint64_t issueTime = 0; // milliseconds since the ripple epoch
|
||||
std::uint64_t expiration = 0; // milliseconds since the ripple epoch
|
||||
};
|
||||
|
||||
/** Parses and validates the exact bytes of a note.
|
||||
|
||||
Accepts only the document described above, after jsontx_strict has
|
||||
accepted it as a JsonTx document. Checks the shape, the account and
|
||||
currency spellings, that Amount is positive and legal, and that the
|
||||
validity window is well formed and no longer than creditNoteMaxLifeMs.
|
||||
Everything that needs a ledger - the signing key's authority, the window
|
||||
against the close time, sfLastNoteTime, funds and trust lines - is left to
|
||||
the NoteClaim transactor. Throws std::runtime_error with the reason.
|
||||
*/
|
||||
CreditNote
|
||||
parseCreditNote(std::string_view raw);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -118,6 +118,10 @@ negativeUNL() noexcept;
|
||||
Keylet const&
|
||||
UNLReport() noexcept;
|
||||
|
||||
/** The (fixed) index of the featureRNG random number generator object. */
|
||||
Keylet const&
|
||||
random() noexcept;
|
||||
|
||||
/** The beginning of an order book */
|
||||
struct book_t
|
||||
{
|
||||
@@ -328,6 +332,14 @@ xChainCreateAccountClaimID(STXChainBridge const& bridge, std::uint64_t seq);
|
||||
Keylet
|
||||
did(AccountID const& account) noexcept;
|
||||
|
||||
/** An AppLoader belonging to an account.
|
||||
|
||||
There is at most one per account, so the key is derived from the owner's
|
||||
AccountID alone.
|
||||
*/
|
||||
Keylet
|
||||
appLoader(AccountID const& account) noexcept;
|
||||
|
||||
Keylet
|
||||
oracle(AccountID const& account, std::uint32_t const& documentID) noexcept;
|
||||
|
||||
@@ -376,6 +388,16 @@ permissionedDomain(uint256 const& domainID) noexcept;
|
||||
Keylet
|
||||
manifest(PublicKey const& pk) noexcept;
|
||||
|
||||
/** The directory listing every master key's ltMANIFEST object.
|
||||
|
||||
One entry per master key, made when its first manifest is published and
|
||||
kept for good: a newer manifest, a revocation included, replaces the object
|
||||
at the same key and keeps its place. The copy at the ephemeral key is not
|
||||
listed; it is named by the master object.
|
||||
*/
|
||||
Keylet const&
|
||||
manifestDir() noexcept;
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
// Everything below is deprecated and should be removed in favor of keylets:
|
||||
@@ -406,7 +428,7 @@ struct keyletDesc
|
||||
|
||||
// This list should include all of the keylet functions that take a single
|
||||
// AccountID parameter.
|
||||
std::array<keyletDesc<AccountID const&>, 6> const directAccountKeylets{
|
||||
std::array<keyletDesc<AccountID const&>, 7> const directAccountKeylets{
|
||||
{{&keylet::account, jss::AccountRoot, false},
|
||||
{&keylet::ownerDir, jss::DirectoryNode, true},
|
||||
{&keylet::signers, jss::SignerList, true},
|
||||
@@ -414,7 +436,8 @@ std::array<keyletDesc<AccountID const&>, 6> const directAccountKeylets{
|
||||
// test it anyway, since the invariant checks for it.
|
||||
{&keylet::nftpage_min, jss::NFTokenPage, true},
|
||||
{&keylet::nftpage_max, jss::NFTokenPage, true},
|
||||
{&keylet::did, jss::DID, true}}};
|
||||
{&keylet::did, jss::DID, true},
|
||||
{&keylet::appLoader, jss::AppLoader, true}}};
|
||||
|
||||
MPTID
|
||||
makeMptID(std::uint32_t sequence, AccountID const& account);
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012-2014 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.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_PROTOCOL_JSONTXSIGNATURES_H_INCLUDED
|
||||
#define RIPPLE_PROTOCOL_JSONTXSIGNATURES_H_INCLUDED
|
||||
|
||||
#include <xrpl/protocol/SField.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// jsontx: plaintext-JSON signing support (featureJsonTx)
|
||||
//
|
||||
// A JsonTx is authorised by an ed25519 signature over
|
||||
//
|
||||
// jsontx_sign_prefix || preimage
|
||||
//
|
||||
// where the preimage is the exact JSON text the signer read. The node rebuilds
|
||||
// the preimage from the binary transaction plus sfJsonTxDelta, a small delta
|
||||
// against a canonical form the node derives itself. The delta is
|
||||
// attacker-controlled: it is not covered by the signature it helps
|
||||
// reconstruct. Every bound below is therefore explicit, and unsanitize_jsontx
|
||||
// accepts only the exact encoding sanitize_jsontx would have produced.
|
||||
//
|
||||
// CONSENSUS SURFACE. Once featureJsonTx activates, everything that decides
|
||||
// whether a preimage verifies is a consensus rule and can only change behind
|
||||
// a further amendment. That is:
|
||||
//
|
||||
// - every constant and every function in this file, including the delta
|
||||
// encoder's exact output (jsontx_verify compares it byte for byte);
|
||||
// - STObject::getJson(JsonOptions::none) and the getJson of every ST type
|
||||
// that can appear in a transaction (STAmount, STUInt64, STPathSet,
|
||||
// STIssue, STVector256, ...), because the node's canonical form is
|
||||
// derived from it;
|
||||
// - Json::FastWriter, which serializes that JSON before canonicalization;
|
||||
// - STParsedJSON, only in so far as the submit RPC builds the transaction
|
||||
// from the canonical form: a change there changes which preimages can be
|
||||
// submitted, not which verify.
|
||||
//
|
||||
// None of that code was consensus-critical before. A change to how any field
|
||||
// renders as JSON - including one merged from upstream rippled - changes
|
||||
// which signatures verify and must be amendment-gated. JSONTxSignatures_test
|
||||
// pins this for every field of every transaction format.
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
inline constexpr std::size_t jsontx_max_text = 8192; // canonical and original
|
||||
inline constexpr std::size_t jsontx_max_diff = 2048; // delta bytes
|
||||
inline constexpr std::size_t jsontx_max_ops =
|
||||
jsontx_max_diff / 2; // delta instructions
|
||||
inline constexpr std::size_t jsontx_min_copy = 4; // encoder match threshold
|
||||
inline constexpr std::size_t jsontx_max_cand = 64; // encoder candidate cap
|
||||
inline constexpr std::size_t jsontx_max_depth = 32; // JSON nesting
|
||||
|
||||
// The largest magnitude a bare JSON integer may have: 2^53 - 1, the range
|
||||
// RFC 8259 section 6 calls interoperable and RFC 7493 (I-JSON) section 2.2
|
||||
// requires. Past it an IEEE 754 double - which is what JSON.parse and most
|
||||
// other parsers produce - can no longer hold every integer, so the number a
|
||||
// wallet shows after parsing the preimage may not be the number that
|
||||
// executes. Larger values are written as strings, which is how getJson
|
||||
// renders every amount and UInt64 anyway.
|
||||
inline constexpr std::uint64_t jsontx_max_int = (std::uint64_t{1} << 53) - 1;
|
||||
|
||||
// Domain separation. Without it the signed message is bare JSON text, and any
|
||||
// wallet feature that signs a user-visible text message with the account key
|
||||
// (several do, over the raw bytes) could be driven to sign a live Payment
|
||||
// presented as a "log in" message. 0xFF can never occur in UTF-8, so no text
|
||||
// signer can produce these bytes; the rest follows HashPrefix convention.
|
||||
inline constexpr std::string_view jsontx_sign_prefix{
|
||||
"\xFF"
|
||||
"JTX",
|
||||
4};
|
||||
|
||||
// The exact message a JsonTx signer signs for `preimage`.
|
||||
std::string
|
||||
jsontx_signing_data(std::string_view preimage);
|
||||
|
||||
// ASCII-only case folding; never consults the locale.
|
||||
std::string
|
||||
jsontx_lower(std::string_view s);
|
||||
|
||||
// Case-insensitive field-name -> canonical SField, over the serializable
|
||||
// fields doServerDefinitions publishes. sfInvalid if unknown.
|
||||
SField const&
|
||||
jsontx_field(std::string const& name);
|
||||
|
||||
// days from 1970-01-01 (Howard Hinnant's civil calendar algorithm)
|
||||
constexpr std::int64_t
|
||||
jsontx_days(int y, unsigned m, unsigned d)
|
||||
{
|
||||
y -= m <= 2;
|
||||
std::int64_t const era = (y >= 0 ? y : y - 399) / 400;
|
||||
unsigned const yoe = static_cast<unsigned>(y - era * 400);
|
||||
unsigned const doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
|
||||
unsigned const doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
||||
return era * 146097 + doe - 719468;
|
||||
}
|
||||
|
||||
inline constexpr std::int64_t jsontx_epoch_day = jsontx_days(2000, 1, 1);
|
||||
static_assert(jsontx_epoch_day == 10957); // matches chrono.h epoch_offset
|
||||
|
||||
// 9999-12-31T23:59:59.999Z: past this toISOString() switches to expanded years
|
||||
// and the fixed 24 character shape no longer holds
|
||||
inline constexpr std::uint64_t jsontx_max_time =
|
||||
(static_cast<std::uint64_t>(jsontx_days(9999, 12, 31) - jsontx_epoch_day) *
|
||||
86400 +
|
||||
86399) *
|
||||
1000 +
|
||||
999;
|
||||
|
||||
// Strict Date().toISOString() -> milliseconds since the ripple epoch.
|
||||
std::uint64_t
|
||||
jsontx_iso(std::string_view s);
|
||||
|
||||
// The exact inverse over [0, jsontx_max_time].
|
||||
std::string
|
||||
jsontx_iso_str(std::uint64_t ms);
|
||||
|
||||
// How STUInt64::getJson spells v for field f (hex, or base ten for
|
||||
// sMD_BaseTen fields).
|
||||
std::string
|
||||
jsontx_u64_str(SField const& f, std::uint64_t v);
|
||||
|
||||
// Validates that `raw` is a JsonTx document: exactly one JSON object, nothing
|
||||
// before or after it but whitespace, printable ASCII only, no escapes, no
|
||||
// comments, no booleans or nulls, no duplicate keys, integers spelled
|
||||
// -?(0|[1-9][0-9]*), never -0, and no larger in magnitude than
|
||||
// jsontx_max_int, nesting at most jsontx_max_depth. Throws.
|
||||
void
|
||||
jsontx_strict(std::string_view raw);
|
||||
|
||||
// The canonical form of `raw` alone: whitespace stripped, field names in
|
||||
// their Xahau spelling, members ordered by field code, numbers formatted per
|
||||
// field type, and - at the root only - an omitted Sequence written as
|
||||
// "Sequence":0 when the document has a Time or a TicketSequence. Throws on
|
||||
// anything it cannot canonicalize.
|
||||
std::string
|
||||
jsontx_canonical(std::string_view raw);
|
||||
|
||||
// Returns { canonical, delta } where applying delta to canonical with
|
||||
// unsanitize_jsontx reproduces `raw` byte for byte. Throws.
|
||||
std::pair<std::string, std::string>
|
||||
sanitize_jsontx(std::string_view raw);
|
||||
|
||||
// Applies an UNTRUSTED delta to a canonical form the node derived itself.
|
||||
std::string
|
||||
unsanitize_jsontx(std::string_view sanitized, std::string_view diff);
|
||||
|
||||
// The complete untrusted-side check, shared by the submit RPC and the
|
||||
// relay/consensus path. Returns the reconstructed preimage or throws.
|
||||
std::string
|
||||
jsontx_verify(STTx const& stx, std::string_view diff);
|
||||
|
||||
// As above with the delta taken from sfJsonTxDelta. This is what
|
||||
// checkValidity calls in place of STTx::checkSign for a transaction
|
||||
// carrying a delta.
|
||||
std::string
|
||||
jsontx_verify(STTx const& stx);
|
||||
|
||||
} // namespace ripple
|
||||
#endif
|
||||
@@ -96,6 +96,25 @@ std::size_t constexpr maxDIDAttestationLength = 256;
|
||||
/** The maximum length of a domain */
|
||||
std::size_t constexpr maxDomainLength = 256;
|
||||
|
||||
/** The maximum length of an AppLoader document.
|
||||
|
||||
The AppLoader holds the bootstrap document of a Progressive Web App
|
||||
served from an account. It is deliberately capped: the blob lives in a
|
||||
ledger object that carries no owner reserve at all, so the only things
|
||||
standing between it and unbounded ledger growth are this limit and the
|
||||
per-byte transaction fee below.
|
||||
*/
|
||||
std::size_t constexpr maxAppLoaderLength = 4096;
|
||||
|
||||
/** Drops of transaction fee charged per byte of AppLoader document.
|
||||
|
||||
Storage is paid for through the fee rather than through an owner
|
||||
reserve, so that a loader can be published by an account sitting at its
|
||||
reserve floor. The fee is charged on every AccountSet that carries the
|
||||
field, including overwrites.
|
||||
*/
|
||||
std::uint64_t constexpr appLoaderFeeDropsPerByte = 1;
|
||||
|
||||
/** The maximum length of a URI inside a Credential */
|
||||
std::size_t constexpr maxCredentialURILength = 256;
|
||||
|
||||
@@ -154,6 +173,35 @@ std::size_t constexpr maxPriceScale = 20;
|
||||
*/
|
||||
std::size_t constexpr maxTrim = 25;
|
||||
|
||||
/** sfTime validity window (featureJsonTx).
|
||||
|
||||
A transaction carrying sfTime (milliseconds since the ripple epoch) is
|
||||
accepted only while the parent ledger's close time lies within
|
||||
[Time - txTimeMaxFuture, Time + txTimeMaxAge]. The upper bound is the
|
||||
stand-in for LastLedgerSequence: once a validated ledger closes later
|
||||
than Time + txTimeMaxAge the transaction can never apply. The lower bound
|
||||
keeps the validity period of a signed transaction short and limits how
|
||||
far ahead of the network a fast client clock can push sfLastTxnTime.
|
||||
|
||||
Both are consensus rules once featureJsonTx activates.
|
||||
*/
|
||||
std::uint64_t constexpr txTimeMaxAgeMs = 300'000;
|
||||
std::uint64_t constexpr txTimeMaxFutureMs = 120'000;
|
||||
|
||||
/** Credit notes (featureCreditNote).
|
||||
|
||||
A note is a JsonTx document the issuer signs blind, possibly offline,
|
||||
that entitles whoever bears it to Amount from the issuer's account
|
||||
between its IssueTime and its Expiration. The cap on its text keeps a
|
||||
NoteClaim small and the parser's work bounded; the cap on its life
|
||||
bounds both how long a lost note stays live and how far an issuer's
|
||||
sfLastNoteTime can be pushed ahead by one of their own notes.
|
||||
|
||||
Both are consensus rules once featureCreditNote activates.
|
||||
*/
|
||||
std::size_t constexpr maxCreditNoteBytes = 512;
|
||||
std::uint64_t constexpr creditNoteMaxLifeMs = 3'600'000;
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -101,6 +101,18 @@ public:
|
||||
SeqProxy
|
||||
getSeqProxy() const;
|
||||
|
||||
/** True if replay protection comes from sfTime rather than a sequence.
|
||||
|
||||
That is: sfTime is present, Sequence is 0 and there is no
|
||||
TicketSequence. Such a transaction neither checks nor consumes the
|
||||
account's Sequence; it must instead carry a Time strictly greater
|
||||
than the account's sfLastTxnTime, and records its Time there. Its
|
||||
SeqProxy is sequence(0), so anything that derives an object id from
|
||||
the SeqProxy must use seqID() instead.
|
||||
*/
|
||||
bool
|
||||
isTimeSequenced() const;
|
||||
|
||||
boost::container::flat_set<AccountID>
|
||||
getMentionedAccounts() const;
|
||||
|
||||
|
||||
@@ -197,6 +197,7 @@ enum TEFcodes : TERUnderlyingType {
|
||||
tefINVALID_LEDGER_FIX_TYPE,
|
||||
tefPAST_MANIFEST_SEQ,
|
||||
tefREVOKED_MANIFEST,
|
||||
tefMANIFEST_KEY_IN_USE,
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -152,6 +152,26 @@ public:
|
||||
return static_cast<bool>(mHookEmissions);
|
||||
}
|
||||
|
||||
/** featureRNG: the RNG state this transaction's first Hook draw saw.
|
||||
Every value the transaction's Hooks drew is derived from it. */
|
||||
void
|
||||
setRandomData(uint256 const& randomData)
|
||||
{
|
||||
mRandomData = randomData;
|
||||
}
|
||||
|
||||
bool
|
||||
hasRandomData() const
|
||||
{
|
||||
return static_cast<bool>(mRandomData);
|
||||
}
|
||||
|
||||
uint256 const&
|
||||
getRandomData() const
|
||||
{
|
||||
return *mRandomData;
|
||||
}
|
||||
|
||||
STAmount
|
||||
getDeliveredAmount() const
|
||||
{
|
||||
@@ -176,6 +196,7 @@ private:
|
||||
std::optional<STAmount> mDelivered;
|
||||
std::optional<STArray> mHookExecutions;
|
||||
std::optional<STArray> mHookEmissions;
|
||||
std::optional<uint256> mRandomData;
|
||||
|
||||
STArray mNodes;
|
||||
};
|
||||
|
||||
@@ -34,9 +34,13 @@
|
||||
// If you add an amendment here, then do not forget to increment `numFeatures`
|
||||
// in include/xrpl/protocol/Feature.h.
|
||||
|
||||
XRPL_FEATURE(JsonTx, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(CreditNote, 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::DefaultNo)
|
||||
XRPL_FIX (20260929, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FEATURE(HookOnV2_1, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(OnChainManifests, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (HookMap, Supported::yes, VoteBehavior::DefaultYes)
|
||||
@@ -88,7 +92,6 @@ XRPL_FIX (ReduceImport, Supported::yes, VoteBehavior::DefaultYe
|
||||
XRPL_FEATURE(Touch, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FEATURE(Remarks, Supported::yes, VoteBehavior::DefaultNo)
|
||||
XRPL_FIX (FloatDivide, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (240911, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (PageCap, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (240819, Supported::yes, VoteBehavior::DefaultYes)
|
||||
XRPL_FIX (NSDelete, Supported::yes, VoteBehavior::DefaultNo)
|
||||
@@ -164,6 +167,7 @@ XRPL_FEATURE(CryptoConditionsSuite, Supported::yes, VoteBehavior::Obsolete)
|
||||
// pre-amendment code has been removed and the identifiers are deprecated.
|
||||
// All known amendments and amendments that may appear in a validated
|
||||
// ledger must be registered either here or above with the "active" amendments
|
||||
XRPL_RETIRE(fix240911)
|
||||
XRPL_RETIRE(fixXahauV2)
|
||||
XRPL_RETIRE(fixXahauV1)
|
||||
XRPL_RETIRE(MultiSign)
|
||||
|
||||
@@ -142,6 +142,22 @@ LEDGER_ENTRY(ltIMPORT_VLSEQ, 0x0049, ImportVLSequence, import_vlseq, ({
|
||||
{sfPreviousTxnLgrSeq, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** The bootstrap document of a Progressive Web App (featurePWALoader).
|
||||
|
||||
At most one per account, keyed on the owner alone (keylet::appLoader).
|
||||
It is in no owner directory and takes no reserve: its storage is paid
|
||||
for by a per-byte fee on the AccountSet that writes it, and
|
||||
DeleteAccount erases it explicitly. AppLoaderSequence equals the owner's
|
||||
AccountRoot AppLoaderSequence as of the AccountSet that wrote it.
|
||||
*/
|
||||
LEDGER_ENTRY(ltAPP_LOADER, 0x004c, AppLoader, app_loader, ({
|
||||
{sfOwner, soeREQUIRED},
|
||||
{sfAppLoader, soeREQUIRED},
|
||||
{sfAppLoaderSequence, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** The ledger object which tracks the current negative UNL state.
|
||||
|
||||
\note This is a singleton: only one such object exists in the ledger.
|
||||
@@ -180,6 +196,26 @@ LEDGER_ENTRY_DUPLICATE(ltUNL_REPORT, 0x0052, UNLReport, unl_report, ({
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** featureRNG: the consensus random number generator.
|
||||
|
||||
A singleton. sfRandomData is seeded from validator reveals by the
|
||||
ttENTROPY transactions applied at the start of each ledger, and then
|
||||
iterated by every Hook API draw for the rest of that ledger.
|
||||
sfRandomData and sfRandomDigests are never written to metadata, and
|
||||
Hooks cannot load this object.
|
||||
|
||||
\sa keylet::random
|
||||
*/
|
||||
LEDGER_ENTRY(ltRANDOM, 0x526E, Random, random, ({
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfRandomData, soeREQUIRED},
|
||||
{sfEntropyCount, soeREQUIRED},
|
||||
{sfEntropyDenominator, soeREQUIRED},
|
||||
{sfRandomDigests, soeREQUIRED},
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** A ledger object which contains a signer list for an account.
|
||||
|
||||
\sa keylet::signers
|
||||
@@ -262,7 +298,10 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
|
||||
{sfHookStateScale, soeOPTIONAL},
|
||||
{sfCron, soeOPTIONAL},
|
||||
{sfAMMID, soeOPTIONAL},
|
||||
{sfAppLoaderSequence, soeOPTIONAL},
|
||||
{sfManifestID, soeOPTIONAL},
|
||||
{sfLastTxnTime, soeOPTIONAL},
|
||||
{sfLastNoteTime, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** A ledger object which contains a list of object identifiers.
|
||||
@@ -609,6 +648,7 @@ LEDGER_ENTRY(ltMANIFEST, 0x004D, Manifest, manifest_entry, ({
|
||||
{sfMasterSignature, soeREQUIRED},
|
||||
{sfSignature, soeOPTIONAL}, /* absent if the master is revoked */
|
||||
{sfManifestID, soeOPTIONAL}, /* pointer to the identical other copy on the other key */
|
||||
{sfOwnerNode, soeOPTIONAL}, /* master copy only: its page in keylet::manifestDir() */
|
||||
{sfPreviousTxnID, soeREQUIRED},
|
||||
{sfPreviousTxnLgrSeq, soeREQUIRED},
|
||||
}))
|
||||
|
||||
@@ -59,6 +59,8 @@ TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19)
|
||||
TYPED_SFIELD(sfHookApiVersion, UINT16, 20)
|
||||
TYPED_SFIELD(sfHookStateScale, UINT16, 21)
|
||||
TYPED_SFIELD(sfLedgerFixType, UINT16, 22)
|
||||
TYPED_SFIELD(sfEntropyCount, UINT16, 99)
|
||||
TYPED_SFIELD(sfEntropyDenominator, UINT16, 100)
|
||||
|
||||
// 32-bit integers (common)
|
||||
TYPED_SFIELD(sfNetworkID, UINT32, 1)
|
||||
@@ -123,6 +125,7 @@ TYPED_SFIELD(sfImportSequence, UINT32, 97)
|
||||
TYPED_SFIELD(sfRewardTime, UINT32, 98)
|
||||
TYPED_SFIELD(sfRewardLgrFirst, UINT32, 99)
|
||||
TYPED_SFIELD(sfRewardLgrLast, UINT32, 100)
|
||||
TYPED_SFIELD(sfAppLoaderSequence, UINT32, 101)
|
||||
|
||||
// 64-bit integers (common)
|
||||
TYPED_SFIELD(sfIndexNext, UINT64, 1)
|
||||
@@ -153,6 +156,10 @@ TYPED_SFIELD(sfOutstandingAmount, UINT64, 25, SField::sMD_BaseTen|SFie
|
||||
TYPED_SFIELD(sfMPTAmount, UINT64, 26, SField::sMD_BaseTen|SField::sMD_Default)
|
||||
TYPED_SFIELD(sfIssuerNode, UINT64, 27)
|
||||
TYPED_SFIELD(sfSubjectNode, UINT64, 28)
|
||||
// featureCreditNote: IssueTime of the last note this account redeemed
|
||||
TYPED_SFIELD(sfLastNoteTime, UINT64, 94)
|
||||
TYPED_SFIELD(sfLastTxnTime, UINT64, 95)
|
||||
TYPED_SFIELD(sfTime, UINT64, 96)
|
||||
TYPED_SFIELD(sfTouchCount, UINT64, 97)
|
||||
TYPED_SFIELD(sfAccountIndex, UINT64, 98)
|
||||
TYPED_SFIELD(sfAccountCount, UINT64, 99)
|
||||
@@ -210,6 +217,9 @@ TYPED_SFIELD(sfOfferID, UINT256, 34)
|
||||
TYPED_SFIELD(sfEscrowID, UINT256, 35)
|
||||
TYPED_SFIELD(sfURITokenID, UINT256, 36)
|
||||
TYPED_SFIELD(sfDomainID, UINT256, 37)
|
||||
TYPED_SFIELD(sfNextRandomDigest, UINT256, 89)
|
||||
// RNG running state: never written to metadata (see featureRNG)
|
||||
TYPED_SFIELD(sfRandomData, UINT256, 90, SField::sMD_Never)
|
||||
TYPED_SFIELD(sfManifestID, UINT256, 91)
|
||||
TYPED_SFIELD(sfHookOnOutgoing, UINT256, 93)
|
||||
TYPED_SFIELD(sfHookOnIncoming, UINT256, 94)
|
||||
@@ -294,6 +304,13 @@ TYPED_SFIELD(sfAssetClass, VL, 29)
|
||||
TYPED_SFIELD(sfProvider, VL, 30)
|
||||
TYPED_SFIELD(sfMPTokenMetadata, VL, 31)
|
||||
TYPED_SFIELD(sfCredentialType, VL, 32)
|
||||
// featureCreditNote: the note text and the issuer's signature over it. Both
|
||||
// are covered by the bearer's TxnSignature (neither is notSigning) so a
|
||||
// redemption cannot be re-pointed at a different note in flight.
|
||||
TYPED_SFIELD(sfNote, VL, 93)
|
||||
TYPED_SFIELD(sfNoteSignature, VL, 94)
|
||||
TYPED_SFIELD(sfJsonTxDelta, VL, 95, SField::sMD_Default, SField::notSigning)
|
||||
TYPED_SFIELD(sfAppLoader, VL, 96)
|
||||
TYPED_SFIELD(sfHookName, VL, 97)
|
||||
TYPED_SFIELD(sfRemarkValue, VL, 98)
|
||||
TYPED_SFIELD(sfRemarkName, VL, 99)
|
||||
@@ -383,6 +400,7 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
|
||||
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
|
||||
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
|
||||
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
|
||||
UNTYPED_SFIELD(sfRandomDigestEntry, OBJECT, 88)
|
||||
UNTYPED_SFIELD(sfManifest, OBJECT, 90)
|
||||
UNTYPED_SFIELD(sfAmountEntry, OBJECT, 91)
|
||||
UNTYPED_SFIELD(sfMintURIToken, OBJECT, 92)
|
||||
@@ -423,6 +441,8 @@ UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25)
|
||||
UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26)
|
||||
UNTYPED_SFIELD(sfUnauthorizeCredentials, ARRAY, 27)
|
||||
UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28)
|
||||
// RNG validator commitments: never written to metadata (see featureRNG)
|
||||
UNTYPED_SFIELD(sfRandomDigests, ARRAY, 91, SField::sMD_Never)
|
||||
UNTYPED_SFIELD(sfAmounts, ARRAY, 92)
|
||||
UNTYPED_SFIELD(sfHookEmissions, ARRAY, 93)
|
||||
UNTYPED_SFIELD(sfImportVLKeys, ARRAY, 94)
|
||||
|
||||
@@ -74,6 +74,7 @@ TRANSACTION(ttACCOUNT_SET, 3, AccountSet, ({
|
||||
{sfTickSize, soeOPTIONAL},
|
||||
{sfNFTokenMinter, soeOPTIONAL},
|
||||
{sfHookStateScale, soeOPTIONAL},
|
||||
{sfAppLoader, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** This transaction type cancels an existing escrow. */
|
||||
@@ -500,6 +501,39 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
|
||||
{sfDomainID, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/** featureRNG: a validator's entropy contribution for one ledger.
|
||||
|
||||
Each proposing UNLReport validator places exactly one of these in its own
|
||||
initial position when it closes the ledger. It reveals the secret behind
|
||||
the validator's commitment currently stored in the Random object (if any)
|
||||
and commits to the secret it will reveal next time. It never enters the
|
||||
open ledger, so a reveal only becomes public after the revealing
|
||||
validator's transaction set is locked.
|
||||
*/
|
||||
TRANSACTION(ttENTROPY, 89, Entropy, ({
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
{sfPublicKey, soeREQUIRED},
|
||||
{sfNextRandomDigest, soeREQUIRED},
|
||||
{sfRandomData, soeOPTIONAL},
|
||||
}))
|
||||
|
||||
/** featureCreditNote: redeem a credit note.
|
||||
|
||||
The bearer (sfAccount) submits the exact bytes the issuer signed (sfNote,
|
||||
a JsonTx document naming NetworkID, Account, Amount, IssueTime and
|
||||
Expiration), the issuer's ed25519 signature over
|
||||
jsontx_sign_prefix || sfNote (sfNoteSignature) and the key that made it
|
||||
(sfPublicKey, the issuer's master or regular key). Amount moves from the
|
||||
note's Account to the bearer; the note's IssueTime is recorded in the
|
||||
issuer's AccountRoot as sfLastNoteTime, and only notes issued after it
|
||||
can still be redeemed.
|
||||
*/
|
||||
TRANSACTION(ttNOTE_CLAIM, 90, NoteClaim, ({
|
||||
{sfNote, soeREQUIRED},
|
||||
{sfNoteSignature, soeREQUIRED},
|
||||
{sfPublicKey, soeREQUIRED},
|
||||
}))
|
||||
|
||||
/* sfAccount is supplied by TxFormats::commonFields; listing it here as well
|
||||
makes the SOTemplate reject the format at construction. */
|
||||
TRANSACTION(ttMANIFEST_SET, 91, SetManifest, ({
|
||||
|
||||
@@ -171,6 +171,7 @@ JSS(amendment_blocked); // out: NetworkOPs
|
||||
JSS(amm_account); // in: amm_info
|
||||
JSS(amount); // out: AccountChannels, amm_info
|
||||
JSS(amount2); // out: amm_info
|
||||
JSS(anchored_through); // out: CatalogueLoad
|
||||
JSS(api_version); // in: many, out: Version
|
||||
JSS(api_version_low); // out: Version
|
||||
JSS(applied); // out: SubmitTransaction
|
||||
@@ -238,7 +239,7 @@ JSS(closed); // out: NetworkOPs, LedgerToJson,
|
||||
// handlers/Ledger
|
||||
JSS(closed_ledger); // out: NetworkOPs
|
||||
JSS(cluster); // out: PeerImp
|
||||
JSS(code); // out: errors
|
||||
JSS(code); // in: HookValidate; out: errors
|
||||
JSS(command); // in: RPCHandler
|
||||
JSS(complete); // out: NetworkOPs, InboundLedger
|
||||
JSS(complete_ledgers); // out: NetworkOPs, PeerImp
|
||||
@@ -378,6 +379,8 @@ JSS(index); // in: LedgerEntry,
|
||||
// LedgerEntry, TxHistory, LedgerData
|
||||
JSS(info); // out: ServerInfo, ConsensusInfo, FetchInfo
|
||||
JSS(initial_sync_duration_us);
|
||||
JSS(instruction_count_cbak); // out: HookValidate
|
||||
JSS(instruction_count_hook); // out: HookValidate
|
||||
JSS(internal_command); // in: Internal
|
||||
JSS(invalid_API_version); // out: Many, when a request has an invalid
|
||||
// version
|
||||
@@ -451,6 +454,7 @@ JSS(local); // out: resource/Logic.h
|
||||
JSS(local_txs); // out: GetCounts
|
||||
JSS(local_static_keys); // out: ValidatorList
|
||||
JSS(locked_balance); // out: AccountLines
|
||||
JSS(log); // out: HookValidate
|
||||
JSS(low); // out: BookChanges
|
||||
JSS(lock_count); // out: AccountLines
|
||||
JSS(lowest_sequence); // out: AccountInfo
|
||||
@@ -589,7 +593,6 @@ JSS(queue_data); // out: AccountInfo
|
||||
JSS(queued); // out: SubmitTransaction
|
||||
JSS(queued_duration_us);
|
||||
JSS(quote_asset); // in: get_aggregate_price
|
||||
JSS(random); // out: Random
|
||||
JSS(raw_meta); // out: AcceptedLedgerTx
|
||||
JSS(receive_currencies); // out: AccountCurrencies
|
||||
JSS(reference_level); // out: TxQ
|
||||
@@ -607,6 +610,7 @@ JSS(reserve_base_native); // out: NetworkOPs
|
||||
JSS(reserve_inc); // out: NetworkOPs
|
||||
JSS(reserve_inc_xrp); // out: NetworkOPs
|
||||
JSS(reserve_inc_native); // out: NetworkOPs
|
||||
JSS(reset); // in/out: PWAInfo
|
||||
JSS(response); // websocket
|
||||
JSS(result); // RPC
|
||||
JSS(ripple_lines); // out: NetworkOPs
|
||||
@@ -637,6 +641,7 @@ JSS(server_status); // out: NetworkOPs
|
||||
JSS(server_version); // out: NetworkOPs
|
||||
JSS(settle_delay); // out: AccountChannels
|
||||
JSS(severity); // in: LogLevel
|
||||
JSS(sig);
|
||||
JSS(signature); // out: NetworkOPs, ChannelAuthorize
|
||||
JSS(signature_verified); // out: ChannelVerify
|
||||
JSS(signing_key); // out: NetworkOPs
|
||||
@@ -741,6 +746,7 @@ JSS(TRANSACTION_FLAGS_INDICES); // out: RPC server_definitions
|
||||
JSS(type_hex); // out: STPathSet
|
||||
JSS(unl); // out: UnlList
|
||||
JSS(unlimited); // out: Connection.h
|
||||
JSS(unpinned); // out: CatalogueUnpin
|
||||
JSS(uptime); // out: GetCounts
|
||||
JSS(uri); // out: ValidatorSites
|
||||
JSS(url); // in/out: Subscribe, Unsubscribe
|
||||
@@ -748,6 +754,7 @@ JSS(url_password); // in: Subscribe
|
||||
JSS(url_username); // in: Subscribe
|
||||
JSS(urlgravatar); //
|
||||
JSS(username); // in: Subscribe
|
||||
JSS(valid); // out: HookValidate
|
||||
JSS(validated); // out: NetworkOPs, RPCHelpers, AccountTx*
|
||||
// Tx
|
||||
JSS(validator_list_expires); // out: NetworkOps, ValidatorList
|
||||
@@ -769,6 +776,7 @@ JSS(validator_sites); // out: ValidatorSites
|
||||
JSS(value); // out: STAmount
|
||||
JSS(version); // out: RPCVersion
|
||||
JSS(vetoed); // out: AmendmentTableImpl
|
||||
JSS(vm_error); // out: HookValidate
|
||||
JSS(volume_a); // out: BookChanges
|
||||
JSS(volume_b); // out: BookChanges
|
||||
JSS(vote); // in: Feature
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#include <xrpl/basics/BasicConfig.h>
|
||||
#include <xrpl/beast/net/IPEndpoint.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/network_v4.hpp>
|
||||
#include <boost/asio/ip/network_v6.hpp>
|
||||
@@ -57,6 +58,12 @@ struct Port
|
||||
std::vector<boost::asio::ip::network_v6> admin_nets_v6;
|
||||
std::vector<boost::asio::ip::network_v4> secure_gateway_nets_v4;
|
||||
std::vector<boost::asio::ip::network_v6> secure_gateway_nets_v6;
|
||||
// "pwa" ports only; see the "pwa ports" section of xahaud-example.cfg.
|
||||
std::set<std::string> pwa_domains; // zones, lower case, no trailing dot
|
||||
std::set<AccountID> pwa_allowlist; // if non-empty, only these are served
|
||||
std::set<AccountID> pwa_blocklist; // never served; wins over allowlist
|
||||
bool pwa_custom_domains = false; // serve hosts named by TXT records
|
||||
std::optional<beast::IP::Endpoint> pwa_resolver; // for those; else system
|
||||
std::string user;
|
||||
std::string password;
|
||||
std::string admin_user;
|
||||
@@ -93,8 +100,18 @@ struct Port
|
||||
return protocol.count("udp") > 0;
|
||||
}
|
||||
|
||||
bool
|
||||
has_peer() const
|
||||
{
|
||||
return protocol.count("peer") > 0;
|
||||
}
|
||||
|
||||
// Maximum UDP packet size (default 64KB)
|
||||
std::size_t udp_packet_size = 65536;
|
||||
|
||||
// If this is a peer port, also listen for UDP Superhighway (XUSH)
|
||||
// datagrams on the same port number.
|
||||
bool udp_highway = false;
|
||||
};
|
||||
|
||||
std::ostream&
|
||||
@@ -126,6 +143,9 @@ struct ParsedPort
|
||||
std::vector<boost::asio::ip::network_v6> admin_nets_v6;
|
||||
std::vector<boost::asio::ip::network_v4> secure_gateway_nets_v4;
|
||||
std::vector<boost::asio::ip::network_v6> secure_gateway_nets_v6;
|
||||
std::set<std::string> pwa_domains;
|
||||
bool pwa_custom_domains = false;
|
||||
std::optional<beast::IP::Endpoint> pwa_resolver;
|
||||
};
|
||||
|
||||
void
|
||||
|
||||
@@ -298,7 +298,7 @@ Door<Handler>::Door(
|
||||
port_.protocol.count("peer") > 0)
|
||||
, plain_(
|
||||
port_.protocol.count("http") > 0 || port_.protocol.count("ws") > 0 ||
|
||||
port_.protocol.count("ws2"))
|
||||
port_.protocol.count("ws2") || port_.protocol.count("pwa"))
|
||||
{
|
||||
reOpen();
|
||||
}
|
||||
|
||||
@@ -208,6 +208,16 @@ ServerImpl<Handler>::ports(std::vector<Port> const& ports)
|
||||
|
||||
sp->run();
|
||||
}
|
||||
|
||||
// UDP Superhighway (XUSH) datagrams arrive on the peer port
|
||||
// number. TCP and UDP port numbers are independent, so this
|
||||
// needs no socket sharing with the TCP door.
|
||||
if (internalPort.has_peer() && internalPort.udp_highway)
|
||||
{
|
||||
if (auto sp = ios_.emplace<UDPDoor<Handler>>(
|
||||
handler_, io_context_, internalPort, j_))
|
||||
sp->run();
|
||||
}
|
||||
}
|
||||
}
|
||||
return eps;
|
||||
|
||||
@@ -78,10 +78,9 @@ public:
|
||||
error_code ec;
|
||||
|
||||
// Create UDP endpoint from port configuration
|
||||
auto const addr = port_.ip.to_v4();
|
||||
boost::asio::ip::udp::endpoint udp_endpoint(addr, port_.port);
|
||||
boost::asio::ip::udp::endpoint udp_endpoint(port_.ip, port_.port);
|
||||
|
||||
socket_.open(boost::asio::ip::udp::v4(), ec);
|
||||
socket_.open(udp_endpoint.protocol(), ec);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(j_.error()) << "UDP socket open failed: " << ec.message();
|
||||
@@ -94,17 +93,22 @@ public:
|
||||
{
|
||||
JLOG(j_.error())
|
||||
<< "UDP set reuse_address failed: " << ec.message();
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
socket_.bind(udp_endpoint, ec);
|
||||
if (ec)
|
||||
{
|
||||
JLOG(j_.error()) << "UDP socket bind failed: " << ec.message();
|
||||
// Close the socket so that run() does not start receiving on an
|
||||
// unbound socket, which would silently bind an ephemeral port.
|
||||
JLOG(j_.error()) << "UDP socket bind to " << udp_endpoint
|
||||
<< " failed: " << ec.message();
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
JLOG(j_.info()) << "UDP-RPC listening on " << udp_endpoint;
|
||||
JLOG(j_.info()) << "UDP listening on " << udp_endpoint;
|
||||
}
|
||||
|
||||
endpoint_type
|
||||
@@ -169,6 +173,7 @@ private:
|
||||
handler_.onUDPMessage(
|
||||
std::string(recv_buffer_.data(), bytes_transferred),
|
||||
tcp_endpoint,
|
||||
port_,
|
||||
[this, tcp_endpoint](std::string const& response) {
|
||||
do_send(response, tcp_endpoint);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/basics/UTF8.h>
|
||||
#include <xrpl/protocol/AppLoader.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace ripple {
|
||||
namespace appLoader {
|
||||
|
||||
namespace {
|
||||
|
||||
// HTML "ASCII whitespace": TAB, LF, FF, CR, SPACE.
|
||||
inline bool
|
||||
isSpace(std::uint8_t c) noexcept
|
||||
{
|
||||
return c == 0x09 || c == 0x0A || c == 0x0C || c == 0x0D || c == 0x20;
|
||||
}
|
||||
|
||||
inline std::uint8_t
|
||||
lower(std::uint8_t c) noexcept
|
||||
{
|
||||
return (c >= 'A' && c <= 'Z') ? static_cast<std::uint8_t>(c + 32) : c;
|
||||
}
|
||||
|
||||
// Case-insensitive match of lower-case `lit` at `pos`; never reads past
|
||||
// the end.
|
||||
bool
|
||||
matchAt(
|
||||
std::uint8_t const* data,
|
||||
std::size_t size,
|
||||
std::size_t pos,
|
||||
std::string_view lit) noexcept
|
||||
{
|
||||
if (pos > size || size - pos < lit.size())
|
||||
return false;
|
||||
for (std::size_t i = 0; i < lit.size(); ++i)
|
||||
if (lower(data[pos + i]) != static_cast<std::uint8_t>(lit[i]))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
findFrom(
|
||||
std::uint8_t const* data,
|
||||
std::size_t size,
|
||||
std::size_t from,
|
||||
std::string_view lit) noexcept
|
||||
{
|
||||
for (std::size_t i = from; i + lit.size() <= size; ++i)
|
||||
if (matchAt(data, size, i, lit))
|
||||
return i;
|
||||
return size;
|
||||
}
|
||||
|
||||
// So that "<htmlish>" is not mistaken for "<html>".
|
||||
inline bool
|
||||
isTagNameEnd(std::uint8_t c) noexcept
|
||||
{
|
||||
return isSpace(c) || c == '>' || c == '/';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result
|
||||
validate(std::uint8_t const* data, std::size_t size)
|
||||
{
|
||||
if (size == 0 || data == nullptr)
|
||||
return Result::empty;
|
||||
if (size > maxAppLoaderLength)
|
||||
return Result::tooLarge;
|
||||
if (!isValidUTF8(Slice(data, size)))
|
||||
return Result::badUTF8;
|
||||
for (std::size_t i = 0; i < size; ++i)
|
||||
if (!isSpace(data[i]) && (data[i] < 0x20 || data[i] == 0x7F))
|
||||
return Result::badControlChar;
|
||||
|
||||
std::size_t begin = 0;
|
||||
if (size >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF)
|
||||
begin = 3;
|
||||
while (begin < size && isSpace(data[begin]))
|
||||
++begin;
|
||||
if (begin == size)
|
||||
return Result::empty;
|
||||
|
||||
// "<!doctype" followed by whitespace is the only doctype form any
|
||||
// browser treats as standards-mode HTML.
|
||||
bool const doctype = matchAt(data, size, begin, "<!doctype") &&
|
||||
begin + 9 < size && isSpace(data[begin + 9]);
|
||||
bool const html = matchAt(data, size, begin, "<html") && begin + 5 < size &&
|
||||
isTagNameEnd(data[begin + 5]);
|
||||
if (!doctype && !html)
|
||||
return Result::noDoctype;
|
||||
|
||||
std::size_t open = begin;
|
||||
for (;;)
|
||||
{
|
||||
open = findFrom(data, size, open, "<html");
|
||||
if (open == size)
|
||||
return Result::noHtmlElement;
|
||||
if (open + 5 < size && isTagNameEnd(data[open + 5]))
|
||||
break;
|
||||
++open;
|
||||
}
|
||||
|
||||
// The LAST well-formed </html>, so that one quoted inside a script
|
||||
// does not shadow the real one. Linear: each probe resumes one past
|
||||
// the previous match.
|
||||
// closeEnd == 0 means none found: a well-formed </html> always ends
|
||||
// past position 0.
|
||||
std::size_t closeEnd = 0;
|
||||
for (std::size_t probe = open;;)
|
||||
{
|
||||
std::size_t const found = findFrom(data, size, probe, "</html");
|
||||
if (found == size)
|
||||
break;
|
||||
std::size_t j = found + 6;
|
||||
while (j < size && isSpace(data[j]))
|
||||
++j;
|
||||
if (j < size && data[j] == '>')
|
||||
closeEnd = j + 1;
|
||||
probe = found + 1;
|
||||
}
|
||||
if (closeEnd == 0)
|
||||
return Result::unclosed;
|
||||
|
||||
for (std::size_t i = closeEnd; i < size; ++i)
|
||||
if (!isSpace(data[i]))
|
||||
return Result::trailingGarbage;
|
||||
return Result::ok;
|
||||
}
|
||||
|
||||
char const*
|
||||
to_string(Result r)
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
case Result::ok:
|
||||
return "ok";
|
||||
case Result::empty:
|
||||
return "empty document";
|
||||
case Result::tooLarge:
|
||||
return "document exceeds maxAppLoaderLength";
|
||||
case Result::badUTF8:
|
||||
return "malformed UTF-8";
|
||||
case Result::badControlChar:
|
||||
return "forbidden control character";
|
||||
case Result::noDoctype:
|
||||
return "does not open with a doctype or <html> tag";
|
||||
case Result::noHtmlElement:
|
||||
return "missing <html> start tag";
|
||||
case Result::unclosed:
|
||||
return "missing or misplaced </html> end tag";
|
||||
case Result::trailingGarbage:
|
||||
return "content after </html>";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
} // namespace appLoader
|
||||
} // namespace ripple
|
||||
@@ -0,0 +1,363 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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 <xrpl/basics/contract.h>
|
||||
#include <xrpl/protocol/CreditNote.h>
|
||||
#include <xrpl/protocol/Issue.h>
|
||||
#include <xrpl/protocol/JSONTxSignatures.h>
|
||||
#include <xrpl/protocol/UintTypes.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
namespace {
|
||||
|
||||
[[noreturn]] void
|
||||
fail(std::string const& why)
|
||||
{
|
||||
Throw<std::runtime_error>("creditnote: " + why);
|
||||
}
|
||||
|
||||
constexpr bool
|
||||
digit(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
constexpr bool
|
||||
space(char c)
|
||||
{
|
||||
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
|
||||
}
|
||||
|
||||
// A note's values are strings, bare integers, and - for an issued Amount -
|
||||
// one object of strings. Nothing else.
|
||||
struct Val
|
||||
{
|
||||
enum class Kind : std::uint8_t { string, number, object };
|
||||
|
||||
Kind kind = Kind::object;
|
||||
std::string text; // string contents, or the number token
|
||||
std::vector<std::string> keys; // object member names, in source order
|
||||
std::vector<Val> items; // object member values
|
||||
};
|
||||
|
||||
// A reader for the note language only. jsontx_strict has already accepted
|
||||
// the document - one object, printable ASCII, no escapes, no duplicates,
|
||||
// bounded integers - so this reader's job is shape, not sanitation; it still
|
||||
// refuses anything outside that language rather than assume it.
|
||||
class Reader
|
||||
{
|
||||
std::string_view s_;
|
||||
std::size_t p_ = 0;
|
||||
|
||||
void
|
||||
skip()
|
||||
{
|
||||
while (p_ < s_.size() && space(s_[p_]))
|
||||
++p_;
|
||||
}
|
||||
|
||||
char
|
||||
peek() const
|
||||
{
|
||||
if (p_ >= s_.size())
|
||||
fail("unexpected end of note");
|
||||
return s_[p_];
|
||||
}
|
||||
|
||||
void
|
||||
expect(char c, char const* what)
|
||||
{
|
||||
if (peek() != c)
|
||||
fail(what);
|
||||
++p_;
|
||||
}
|
||||
|
||||
std::string
|
||||
string()
|
||||
{
|
||||
expect('"', "expected a string");
|
||||
std::size_t const b = p_;
|
||||
for (;;)
|
||||
{
|
||||
if (p_ >= s_.size())
|
||||
fail("unterminated string");
|
||||
auto const c = static_cast<unsigned char>(s_[p_]);
|
||||
if (c == '"')
|
||||
break;
|
||||
if (c == '\\' || c < 0x20 || c > 0x7E)
|
||||
fail("strings are printable ASCII without escapes");
|
||||
++p_;
|
||||
}
|
||||
std::string r(s_.substr(b, p_ - b));
|
||||
++p_; // closing quote
|
||||
return r;
|
||||
}
|
||||
|
||||
std::string
|
||||
number()
|
||||
{
|
||||
std::size_t const b = p_;
|
||||
if (peek() == '-')
|
||||
++p_;
|
||||
if (!digit(peek()))
|
||||
fail("malformed number");
|
||||
if (s_[p_] == '0')
|
||||
{
|
||||
++p_;
|
||||
if (p_ < s_.size() && digit(s_[p_]))
|
||||
fail("malformed number");
|
||||
}
|
||||
else
|
||||
{
|
||||
while (p_ < s_.size() && digit(s_[p_]))
|
||||
++p_;
|
||||
}
|
||||
if (p_ < s_.size() && (s_[p_] == '.' || s_[p_] == 'e' || s_[p_] == 'E'))
|
||||
fail("numbers are integers");
|
||||
return std::string(s_.substr(b, p_ - b));
|
||||
}
|
||||
|
||||
Val
|
||||
value(int depth)
|
||||
{
|
||||
skip();
|
||||
char const c = peek();
|
||||
if (c == '{')
|
||||
return object(depth + 1);
|
||||
Val v;
|
||||
if (c == '"')
|
||||
{
|
||||
v.kind = Val::Kind::string;
|
||||
v.text = string();
|
||||
return v;
|
||||
}
|
||||
if (c == '-' || digit(c))
|
||||
{
|
||||
v.kind = Val::Kind::number;
|
||||
v.text = number();
|
||||
return v;
|
||||
}
|
||||
fail("unexpected token");
|
||||
}
|
||||
|
||||
Val
|
||||
object(int depth)
|
||||
{
|
||||
// the note, and at most one object inside it (an issued Amount)
|
||||
if (depth > 2)
|
||||
fail("nested too deeply");
|
||||
|
||||
Val v;
|
||||
v.kind = Val::Kind::object;
|
||||
expect('{', "expected an object");
|
||||
skip();
|
||||
if (peek() == '}')
|
||||
{
|
||||
++p_;
|
||||
return v;
|
||||
}
|
||||
for (;;)
|
||||
{
|
||||
skip();
|
||||
auto key = string();
|
||||
if (std::find(v.keys.begin(), v.keys.end(), key) != v.keys.end())
|
||||
fail("duplicate member " + key);
|
||||
skip();
|
||||
expect(':', "expected ':'");
|
||||
v.items.push_back(value(depth));
|
||||
v.keys.push_back(std::move(key));
|
||||
skip();
|
||||
char const c = peek();
|
||||
if (c == ',')
|
||||
{
|
||||
++p_;
|
||||
continue;
|
||||
}
|
||||
if (c == '}')
|
||||
{
|
||||
++p_;
|
||||
return v;
|
||||
}
|
||||
fail("expected ',' or '}'");
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
explicit Reader(std::string_view s) : s_(s)
|
||||
{
|
||||
}
|
||||
|
||||
Val
|
||||
parse()
|
||||
{
|
||||
skip();
|
||||
if (p_ >= s_.size() || s_[p_] != '{')
|
||||
fail("a note is a JSON object");
|
||||
auto root = object(1);
|
||||
skip();
|
||||
if (p_ != s_.size())
|
||||
fail("trailing data after note");
|
||||
return root;
|
||||
}
|
||||
};
|
||||
|
||||
Val const&
|
||||
member(Val const& obj, std::string_view key)
|
||||
{
|
||||
for (std::size_t i = 0; i < obj.keys.size(); ++i)
|
||||
if (obj.keys[i] == key)
|
||||
return obj.items[i];
|
||||
fail("missing " + std::string(key));
|
||||
}
|
||||
|
||||
std::string const&
|
||||
str(Val const& obj, std::string_view key)
|
||||
{
|
||||
auto const& v = member(obj, key);
|
||||
if (v.kind != Val::Kind::string)
|
||||
fail(std::string(key) + " must be a string");
|
||||
return v.text;
|
||||
}
|
||||
|
||||
// A non-negative bare integer, already known to be well formed and at most
|
||||
// jsontx_max_int, as a number.
|
||||
std::uint64_t
|
||||
unsignedValue(Val const& v, std::string_view key)
|
||||
{
|
||||
if (v.kind != Val::Kind::number)
|
||||
fail(std::string(key) + " must be a bare integer");
|
||||
if (v.text.empty() || v.text[0] == '-')
|
||||
fail(std::string(key) + " must not be negative");
|
||||
std::uint64_t n = 0;
|
||||
for (char const c : v.text)
|
||||
n = n * 10 + static_cast<std::uint64_t>(c - '0');
|
||||
return n;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
CreditNote
|
||||
parseCreditNote(std::string_view raw)
|
||||
{
|
||||
if (raw.size() > maxCreditNoteBytes)
|
||||
fail("note too large");
|
||||
|
||||
// The note is a JsonTx document first: same character set, integer
|
||||
// rules and bounds as a plaintext-signed transaction preimage.
|
||||
jsontx_strict(raw);
|
||||
|
||||
auto const root = Reader(raw).parse();
|
||||
|
||||
static constexpr std::array<std::string_view, 5> allowed{
|
||||
"NetworkID", "Account", "Amount", "IssueTime", "Expiration"};
|
||||
|
||||
// Exactly the five, each exactly once (the reader refused duplicates).
|
||||
// Anything else - including a TransactionType - is not a note.
|
||||
if (root.keys.size() != allowed.size())
|
||||
fail(
|
||||
"a note has exactly NetworkID, Account, Amount, IssueTime and "
|
||||
"Expiration");
|
||||
for (auto const& k : root.keys)
|
||||
if (std::find(allowed.begin(), allowed.end(), k) == allowed.end())
|
||||
fail("unknown member " + k);
|
||||
|
||||
CreditNote note;
|
||||
|
||||
{
|
||||
auto const n = unsignedValue(member(root, "NetworkID"), "NetworkID");
|
||||
if (n > std::numeric_limits<std::uint32_t>::max())
|
||||
fail("NetworkID out of range");
|
||||
note.networkID = static_cast<std::uint32_t>(n);
|
||||
}
|
||||
|
||||
{
|
||||
auto const id = parseBase58<AccountID>(str(root, "Account"));
|
||||
if (!id || *id == noAccount() || isXRP(*id))
|
||||
fail("Account is not an r-address");
|
||||
note.account = *id;
|
||||
}
|
||||
|
||||
{
|
||||
auto const& a = member(root, "Amount");
|
||||
if (a.kind == Val::Kind::number)
|
||||
{
|
||||
// drops of the native currency; at most jsontx_max_int, which
|
||||
// is below the total supply
|
||||
auto const drops = unsignedValue(a, "Amount");
|
||||
note.amount = STAmount{XRPAmount{static_cast<std::int64_t>(drops)}};
|
||||
}
|
||||
else if (a.kind == Val::Kind::object)
|
||||
{
|
||||
if (a.keys.size() != 3)
|
||||
fail(
|
||||
"an issued Amount has exactly currency, issuer and "
|
||||
"value");
|
||||
|
||||
Currency currency;
|
||||
if (!to_currency(currency, str(a, "currency")) || isXRP(currency) ||
|
||||
isBadCurrency(currency))
|
||||
fail("bad currency");
|
||||
|
||||
auto const issuer = parseBase58<AccountID>(str(a, "issuer"));
|
||||
if (!issuer || *issuer == noAccount() || isXRP(*issuer))
|
||||
fail("issuer is not an r-address");
|
||||
|
||||
try
|
||||
{
|
||||
note.amount =
|
||||
amountFromString(Issue{currency, *issuer}, str(a, "value"));
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
fail("bad value");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fail("Amount is drops or an issued amount");
|
||||
}
|
||||
|
||||
if (note.amount.signum() <= 0 || !isLegalNet(note.amount))
|
||||
fail("Amount must be positive");
|
||||
}
|
||||
|
||||
note.issueTime = jsontx_iso(str(root, "IssueTime"));
|
||||
note.expiration = jsontx_iso(str(root, "Expiration"));
|
||||
|
||||
if (note.expiration <= note.issueTime)
|
||||
fail("Expiration must be after IssueTime");
|
||||
if (note.expiration - note.issueTime > creditNoteMaxLifeMs)
|
||||
fail(
|
||||
"a note lives at most " +
|
||||
std::to_string(creditNoteMaxLifeMs / 60000) + " minutes");
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -81,6 +81,7 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
IMPORT_VLSEQ = 'I',
|
||||
UNL_REPORT = 'R',
|
||||
CRON = 'L',
|
||||
APP_LOADER = 'W',
|
||||
AMM = 'A',
|
||||
BRIDGE = LEDGER_NAMESPACE2(0x01, 'H'),
|
||||
XCHAIN_CLAIM_ID = 'Q',
|
||||
@@ -90,8 +91,10 @@ enum class LedgerNameSpace : std::uint16_t {
|
||||
MPTOKEN_ISSUANCE = '~',
|
||||
MPTOKEN = 't',
|
||||
MANIFEST = 'M',
|
||||
MANIFEST_DIR = LEDGER_NAMESPACE2(0x01, 'M'),
|
||||
CREDENTIAL = LEDGER_NAMESPACE2(0x01, 'D'),
|
||||
PERMISSIONED_DOMAIN = 'm',
|
||||
RANDOM = 0x526E, // Rn
|
||||
|
||||
// No longer used or supported. Left here to reserve the space
|
||||
// to avoid accidental reuse.
|
||||
@@ -276,6 +279,13 @@ UNLReport() noexcept
|
||||
return ret;
|
||||
}
|
||||
|
||||
Keylet const&
|
||||
random() noexcept
|
||||
{
|
||||
static Keylet const ret{ltRANDOM, indexHash(LedgerNameSpace::RANDOM)};
|
||||
return ret;
|
||||
}
|
||||
|
||||
Keylet
|
||||
book_t::operator()(Book const& b) const
|
||||
{
|
||||
@@ -613,6 +623,12 @@ did(AccountID const& account) noexcept
|
||||
return {ltDID, indexHash(LedgerNameSpace::DID, account)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
appLoader(AccountID const& account) noexcept
|
||||
{
|
||||
return {ltAPP_LOADER, indexHash(LedgerNameSpace::APP_LOADER, account)};
|
||||
}
|
||||
|
||||
Keylet
|
||||
oracle(AccountID const& account, std::uint32_t const& documentID) noexcept
|
||||
{
|
||||
@@ -677,6 +693,14 @@ manifest(PublicKey const& pk) noexcept
|
||||
return {ltMANIFEST, indexHash(LedgerNameSpace::MANIFEST, pk.slice())};
|
||||
}
|
||||
|
||||
Keylet const&
|
||||
manifestDir() noexcept
|
||||
{
|
||||
static Keylet const ret{
|
||||
ltDIR_NODE, indexHash(LedgerNameSpace::MANIFEST_DIR)};
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace keylet
|
||||
|
||||
} // namespace ripple
|
||||
@@ -143,6 +143,14 @@ InnerObjectFormats::InnerObjectFormats()
|
||||
{sfAccount, soeOPTIONAL},
|
||||
});
|
||||
|
||||
add(sfRandomDigestEntry.jsonName,
|
||||
sfRandomDigestEntry.getCode(),
|
||||
{
|
||||
{sfPublicKey, soeREQUIRED},
|
||||
{sfNextRandomDigest, soeREQUIRED},
|
||||
{sfLedgerSequence, soeREQUIRED},
|
||||
});
|
||||
|
||||
add(sfAmountEntry.jsonName,
|
||||
sfAmountEntry.getCode(),
|
||||
{
|
||||
|
||||
@@ -0,0 +1,992 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2012-2014 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/contract.h>
|
||||
#include <xrpl/json/json_writer.h>
|
||||
#include <xrpl/protocol/JSONTxSignatures.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
namespace {
|
||||
|
||||
[[noreturn]] void
|
||||
fail(std::string const& why)
|
||||
{
|
||||
Throw<std::runtime_error>("jsontx: " + why);
|
||||
}
|
||||
|
||||
// Not std::isdigit: that consults the locale, and every comparison in this
|
||||
// file decides whether a signature verifies.
|
||||
constexpr bool
|
||||
digit(char c)
|
||||
{
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
// json's whitespace set, exactly
|
||||
constexpr bool
|
||||
space(char c)
|
||||
{
|
||||
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// The preimage parser.
|
||||
//
|
||||
// Not Json::Reader. The vendored jsoncpp reader accepts comments, stops once
|
||||
// it has a root value rather than requiring end of input, keeps the last of
|
||||
// two identical keys, and holds numbers as 32-bit ints or doubles - so a bare
|
||||
// integer past 2^32 is refused outright and a fractional token is rounded by
|
||||
// sscanf. Each of those is a way for the text a signer reads to differ from
|
||||
// what executes, or for a valid transaction to be unsignable. This parser
|
||||
// accepts a deliberately small language instead:
|
||||
//
|
||||
// - exactly one object, with only whitespace around it;
|
||||
// - printable ASCII only (0x20-0x7E) inside strings, and no backslash at
|
||||
// all. Every string a transaction renders through getJson is plain
|
||||
// printable ASCII (hex, base58, decimal, currency codes, type names), and
|
||||
// jsontx_verify requires the preimage's strings to equal those byte for
|
||||
// byte, so nothing an escape or a non-ASCII byte could spell would ever
|
||||
// verify. Refusing them outright also rules out bidi overrides,
|
||||
// homoglyphs and terminal control sequences in the text being approved;
|
||||
// - integers spelled -?(0|[1-9][0-9]*), never -0, and within
|
||||
// +/-jsontx_max_int, kept as their exact digits rather than converted to
|
||||
// anything;
|
||||
// - no true, false or null: no transaction field renders as one;
|
||||
// - no two members of one object with the same name.
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
struct Node
|
||||
{
|
||||
enum class Kind : std::uint8_t { object, array, string, number };
|
||||
|
||||
Kind kind = Kind::object;
|
||||
std::string text; // string contents, or the number token
|
||||
std::vector<std::string> keys; // object member names, in source order
|
||||
std::vector<Node> items; // object member values, or array items
|
||||
};
|
||||
|
||||
class Reader
|
||||
{
|
||||
std::string_view s_;
|
||||
std::size_t p_ = 0;
|
||||
|
||||
void
|
||||
skip()
|
||||
{
|
||||
while (p_ < s_.size() && space(s_[p_]))
|
||||
++p_;
|
||||
}
|
||||
|
||||
char
|
||||
peek() const
|
||||
{
|
||||
if (p_ >= s_.size())
|
||||
fail("unexpected end of document");
|
||||
return s_[p_];
|
||||
}
|
||||
|
||||
void
|
||||
expect(char c, char const* what)
|
||||
{
|
||||
if (peek() != c)
|
||||
fail(what);
|
||||
++p_;
|
||||
}
|
||||
|
||||
std::string
|
||||
string()
|
||||
{
|
||||
++p_; // opening quote
|
||||
std::size_t const b = p_;
|
||||
for (;;)
|
||||
{
|
||||
if (p_ >= s_.size())
|
||||
fail("unterminated string");
|
||||
auto const c = static_cast<unsigned char>(s_[p_]);
|
||||
if (c == '"')
|
||||
break;
|
||||
if (c == '\\')
|
||||
fail("escape sequences are not allowed");
|
||||
if (c < 0x20 || c > 0x7E)
|
||||
fail("strings must be printable ASCII");
|
||||
++p_;
|
||||
}
|
||||
std::string r(s_.substr(b, p_ - b));
|
||||
++p_; // closing quote
|
||||
return r;
|
||||
}
|
||||
|
||||
std::string
|
||||
number()
|
||||
{
|
||||
std::size_t const b = p_;
|
||||
if (s_[p_] == '-')
|
||||
++p_;
|
||||
if (p_ >= s_.size() || !digit(s_[p_]))
|
||||
fail("number must be a plain integer");
|
||||
if (s_[p_] == '0')
|
||||
++p_;
|
||||
else
|
||||
while (p_ < s_.size() && digit(s_[p_]))
|
||||
++p_;
|
||||
// a fraction, an exponent, a leading zero or anything else glued on
|
||||
if (p_ < s_.size() && !space(s_[p_]) && s_[p_] != ',' &&
|
||||
s_[p_] != '}' && s_[p_] != ']')
|
||||
fail("number must be a plain integer");
|
||||
std::string tok(s_.substr(b, p_ - b));
|
||||
if (tok == "-0")
|
||||
fail("number must be a plain integer");
|
||||
|
||||
// The interoperable range, symmetric about zero. Sixteen digits is
|
||||
// the most 2^53 - 1 has, so the length test also keeps from_chars
|
||||
// well away from overflow.
|
||||
std::string_view const mag =
|
||||
std::string_view(tok).substr(tok.front() == '-' ? 1 : 0);
|
||||
std::uint64_t v = 0;
|
||||
if (mag.size() > 16 ||
|
||||
std::from_chars(mag.data(), mag.data() + mag.size(), v).ec !=
|
||||
std::errc{} ||
|
||||
v > jsontx_max_int)
|
||||
fail(
|
||||
"integer outside +/-(2^53 - 1); write larger values as "
|
||||
"strings");
|
||||
return tok;
|
||||
}
|
||||
|
||||
void
|
||||
value(Node& n, std::size_t depth)
|
||||
{
|
||||
switch (peek())
|
||||
{
|
||||
case '{':
|
||||
object(n, depth + 1);
|
||||
return;
|
||||
case '[':
|
||||
array(n, depth + 1);
|
||||
return;
|
||||
case '"':
|
||||
n.kind = Node::Kind::string;
|
||||
n.text = string();
|
||||
return;
|
||||
case 't':
|
||||
case 'f':
|
||||
fail("boolean values are not allowed");
|
||||
case 'n':
|
||||
fail("null values are not allowed");
|
||||
case '/':
|
||||
fail("comments are not allowed");
|
||||
default:
|
||||
if (peek() == '-' || digit(peek()))
|
||||
{
|
||||
n.kind = Node::Kind::number;
|
||||
n.text = number();
|
||||
return;
|
||||
}
|
||||
fail("unexpected character");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
object(Node& n, std::size_t depth)
|
||||
{
|
||||
if (depth > jsontx_max_depth)
|
||||
fail("document nested too deeply");
|
||||
n.kind = Node::Kind::object;
|
||||
++p_; // '{'
|
||||
skip();
|
||||
if (peek() != '}')
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
skip();
|
||||
if (peek() != '"')
|
||||
fail("member name must be a string");
|
||||
n.keys.push_back(string());
|
||||
skip();
|
||||
expect(':', "expected ':'");
|
||||
skip();
|
||||
value(n.items.emplace_back(), depth);
|
||||
skip();
|
||||
if (peek() == ',')
|
||||
{
|
||||
++p_;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect('}', "expected ',' or '}'");
|
||||
|
||||
// Two identical names are one member to jsoncpp (the last wins) and
|
||||
// may be either to whatever the signer's wallet parsed.
|
||||
std::vector<std::string_view> ks(n.keys.begin(), n.keys.end());
|
||||
std::sort(ks.begin(), ks.end());
|
||||
if (std::adjacent_find(ks.begin(), ks.end()) != ks.end())
|
||||
fail("duplicate member name");
|
||||
}
|
||||
|
||||
void
|
||||
array(Node& n, std::size_t depth)
|
||||
{
|
||||
if (depth > jsontx_max_depth)
|
||||
fail("document nested too deeply");
|
||||
n.kind = Node::Kind::array;
|
||||
++p_; // '['
|
||||
skip();
|
||||
if (peek() != ']')
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
skip();
|
||||
value(n.items.emplace_back(), depth);
|
||||
skip();
|
||||
if (peek() == ',')
|
||||
{
|
||||
++p_;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
expect(']', "expected ',' or ']'");
|
||||
}
|
||||
|
||||
public:
|
||||
explicit Reader(std::string_view s) : s_(s)
|
||||
{
|
||||
}
|
||||
|
||||
Node
|
||||
parse()
|
||||
{
|
||||
Node root;
|
||||
skip();
|
||||
if (p_ >= s_.size() || s_[p_] != '{')
|
||||
fail("document must be an object");
|
||||
object(root, 1);
|
||||
skip();
|
||||
if (p_ != s_.size())
|
||||
fail(
|
||||
s_[p_] == '/' ? "comments are not allowed"
|
||||
: "trailing data after document");
|
||||
return root;
|
||||
}
|
||||
};
|
||||
|
||||
Node
|
||||
parse(std::string_view raw)
|
||||
{
|
||||
if (raw.size() > jsontx_max_text)
|
||||
fail("document too large");
|
||||
return Reader(raw).parse();
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Canonicalization, directed by field type.
|
||||
//
|
||||
// Keys of a transaction or inner object are SField names, matched
|
||||
// case-insensitively and re-emitted in their canonical spelling, ordered by
|
||||
// field code. A field's type decides what its value may be and how it is
|
||||
// written. The handful of non-field objects a transaction renders (an issued
|
||||
// amount, an Issue, a path step) have their keys sorted bytewise and hold
|
||||
// only scalars.
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
quoted(std::string& o, std::string_view s)
|
||||
{
|
||||
o += '"';
|
||||
o += s;
|
||||
o += '"';
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
unsignedValue(Node const& v, SField const& f)
|
||||
{
|
||||
std::uint64_t n = 0;
|
||||
auto const& t = v.text;
|
||||
auto const r = std::from_chars(t.data(), t.data() + t.size(), n);
|
||||
if (t.front() == '-' || r.ec != std::errc{} || r.ptr != t.data() + t.size())
|
||||
fail("'" + f.fieldName + "' is out of range");
|
||||
return n;
|
||||
}
|
||||
|
||||
char const*
|
||||
kindName(Node::Kind k)
|
||||
{
|
||||
switch (k)
|
||||
{
|
||||
case Node::Kind::object:
|
||||
return "an object";
|
||||
case Node::Kind::array:
|
||||
return "an array";
|
||||
case Node::Kind::string:
|
||||
return "a string";
|
||||
case Node::Kind::number:
|
||||
return "a number";
|
||||
}
|
||||
return "?"; // LCOV_EXCL_LINE
|
||||
}
|
||||
|
||||
void
|
||||
require(Node const& v, Node::Kind k, std::string const& what)
|
||||
{
|
||||
if (v.kind != k)
|
||||
fail(what + " must be " + kindName(k) + ", not " + kindName(v.kind));
|
||||
}
|
||||
|
||||
void
|
||||
emitFields(Node const& n, std::string& o, bool root = false);
|
||||
|
||||
// A string or a number, where the ledger wants a string: numbers become the
|
||||
// quoted spelling of their exact digits.
|
||||
void
|
||||
emitScalar(Node const& v, std::string& o, std::string const& what)
|
||||
{
|
||||
if (v.kind != Node::Kind::string && v.kind != Node::Kind::number)
|
||||
fail(what + " must be a string or a number");
|
||||
quoted(o, v.text);
|
||||
}
|
||||
|
||||
// {"currency":..,"issuer":..,"value":..} and the like
|
||||
void
|
||||
emitPlain(Node const& n, std::string const& what, std::string& o)
|
||||
{
|
||||
require(n, Node::Kind::object, what);
|
||||
std::vector<std::size_t> idx(n.keys.size());
|
||||
for (std::size_t i = 0; i < idx.size(); ++i)
|
||||
idx[i] = i;
|
||||
std::sort(idx.begin(), idx.end(), [&n](auto a, auto b) {
|
||||
return n.keys[a] < n.keys[b];
|
||||
});
|
||||
o += '{';
|
||||
for (auto const i : idx)
|
||||
{
|
||||
if (o.back() != '{')
|
||||
o += ',';
|
||||
quoted(o, n.keys[i]);
|
||||
o += ':';
|
||||
emitScalar(n.items[i], o, what + "." + n.keys[i]);
|
||||
}
|
||||
o += '}';
|
||||
}
|
||||
|
||||
void
|
||||
emitField(SField const& f, Node const& v, std::string& o)
|
||||
{
|
||||
std::string const& name = f.fieldName;
|
||||
|
||||
switch (f.fieldType)
|
||||
{
|
||||
case STI_OBJECT:
|
||||
case STI_XCHAIN_BRIDGE: // renders its members under field names
|
||||
require(v, Node::Kind::object, "'" + name + "'");
|
||||
emitFields(v, o);
|
||||
return;
|
||||
|
||||
case STI_ARRAY:
|
||||
require(v, Node::Kind::array, "'" + name + "'");
|
||||
o += '[';
|
||||
for (auto const& e : v.items)
|
||||
{
|
||||
if (o.back() != '[')
|
||||
o += ',';
|
||||
// each element is {"InnerFieldName": {...}}
|
||||
require(e, Node::Kind::object, "an element of '" + name + "'");
|
||||
if (e.keys.size() != 1)
|
||||
fail(
|
||||
"an element of '" + name +
|
||||
"' must have exactly one member");
|
||||
emitFields(e, o);
|
||||
}
|
||||
o += ']';
|
||||
return;
|
||||
|
||||
case STI_PATHSET:
|
||||
require(v, Node::Kind::array, "'" + name + "'");
|
||||
o += '[';
|
||||
for (auto const& path : v.items)
|
||||
{
|
||||
if (o.back() != '[')
|
||||
o += ',';
|
||||
require(path, Node::Kind::array, "a path in '" + name + "'");
|
||||
o += '[';
|
||||
for (auto const& step : path.items)
|
||||
{
|
||||
if (o.back() != '[')
|
||||
o += ',';
|
||||
emitPlain(step, "a path step in '" + name + "'", o);
|
||||
}
|
||||
o += ']';
|
||||
}
|
||||
o += ']';
|
||||
return;
|
||||
|
||||
case STI_VECTOR256:
|
||||
require(v, Node::Kind::array, "'" + name + "'");
|
||||
o += '[';
|
||||
for (auto const& e : v.items)
|
||||
{
|
||||
if (o.back() != '[')
|
||||
o += ',';
|
||||
require(e, Node::Kind::string, "an element of '" + name + "'");
|
||||
quoted(o, e.text);
|
||||
}
|
||||
o += ']';
|
||||
return;
|
||||
|
||||
case STI_AMOUNT:
|
||||
case STI_ISSUE:
|
||||
if (v.kind == Node::Kind::object)
|
||||
emitPlain(v, "'" + name + "'", o);
|
||||
else
|
||||
emitScalar(v, o, "'" + name + "'");
|
||||
return;
|
||||
|
||||
case STI_UINT8:
|
||||
case STI_UINT16:
|
||||
case STI_UINT32:
|
||||
if (v.kind == Node::Kind::string) // e.g. TransactionType
|
||||
{
|
||||
quoted(o, v.text);
|
||||
return;
|
||||
}
|
||||
require(v, Node::Kind::number, "'" + name + "'");
|
||||
{
|
||||
std::uint64_t const max = f.fieldType == STI_UINT8 ? 0xFFu
|
||||
: f.fieldType == STI_UINT16 ? 0xFFFFu
|
||||
: 0xFFFF'FFFFu;
|
||||
if (unsignedValue(v, f) > max)
|
||||
fail("'" + name + "' is out of range");
|
||||
}
|
||||
o += v.text; // already minimal: the parser refuses leading zeros
|
||||
return;
|
||||
|
||||
case STI_UINT64:
|
||||
if (f == sfTime)
|
||||
{
|
||||
// Spelled Date().toISOString() in the preimage and stored as
|
||||
// milliseconds. Re-emitting the round-tripped spelling rather
|
||||
// than the input is what makes this a fixed point.
|
||||
require(v, Node::Kind::string, "'" + name + "'");
|
||||
quoted(o, jsontx_iso_str(jsontx_iso(v.text)));
|
||||
return;
|
||||
}
|
||||
if (v.kind == Node::Kind::string)
|
||||
{
|
||||
quoted(o, v.text);
|
||||
return;
|
||||
}
|
||||
require(v, Node::Kind::number, "'" + name + "'");
|
||||
quoted(o, jsontx_u64_str(f, unsignedValue(v, f)));
|
||||
return;
|
||||
|
||||
default: // hashes, blobs, accounts, currencies, numbers
|
||||
emitScalar(v, o, "'" + name + "'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
emitFields(Node const& n, std::string& o, bool root)
|
||||
{
|
||||
std::vector<std::pair<SField const*, Node const*>> ks;
|
||||
ks.reserve(n.keys.size() + 1);
|
||||
for (std::size_t i = 0; i < n.keys.size(); ++i)
|
||||
{
|
||||
auto const& f = jsontx_field(n.keys[i]);
|
||||
if (f == sfInvalid)
|
||||
fail("unknown field '" + n.keys[i] + "'");
|
||||
ks.emplace_back(&f, &n.items[i]);
|
||||
}
|
||||
|
||||
// An omitted Sequence is Sequence 0 when something else sequences the
|
||||
// transaction: a Time (time-sequenced) or a TicketSequence. Both need
|
||||
// Sequence 0, and neither can mean anything else by it. The canonical
|
||||
// form always carries the field, because the node's own rendering does:
|
||||
// getJson emits every required field. Written this way the two spellings
|
||||
// of such a preimage - with and without "Sequence": 0 - canonicalize to
|
||||
// the same bytes, and the delta simply skips the ones the signer left
|
||||
// out.
|
||||
//
|
||||
// Only a constant may be implied here, never anything read from the
|
||||
// ledger such as the account's next sequence: the transaction must be a
|
||||
// pure function of the preimage, or the binding check means nothing.
|
||||
//
|
||||
// With neither field, nothing is implied: an absent Sequence is left
|
||||
// absent, and the submit RPC refuses the document for want of one.
|
||||
if (root)
|
||||
{
|
||||
auto const has = [&ks](SField const& f) {
|
||||
return std::any_of(ks.begin(), ks.end(), [&f](auto const& k) {
|
||||
return *k.first == f;
|
||||
});
|
||||
};
|
||||
if (!has(sfSequence) && (has(sfTime) || has(sfTicketSequence)))
|
||||
{
|
||||
static Node const zero{Node::Kind::number, "0", {}, {}};
|
||||
ks.emplace_back(&sfSequence, &zero);
|
||||
}
|
||||
}
|
||||
|
||||
// field codes are unique, so this order is total
|
||||
std::sort(ks.begin(), ks.end(), [](auto const& a, auto const& b) {
|
||||
return a.first->fieldCode < b.first->fieldCode;
|
||||
});
|
||||
|
||||
o += '{';
|
||||
for (std::size_t j = 0; j < ks.size(); ++j)
|
||||
{
|
||||
auto const& [f, v] = ks[j];
|
||||
if (j && f == ks[j - 1].first) // e.g. "Fee" and "fee"
|
||||
fail("duplicate field '" + f->fieldName + "'");
|
||||
if (o.back() != '{')
|
||||
o += ',';
|
||||
quoted(o, f->fieldName);
|
||||
o += ':';
|
||||
emitField(*f, *v, o);
|
||||
}
|
||||
o += '}';
|
||||
}
|
||||
|
||||
std::string
|
||||
canonical(Node const& root)
|
||||
{
|
||||
std::string out;
|
||||
emitFields(root, out, true);
|
||||
// Usually far smaller than the input, but a bare number in a field the
|
||||
// ledger wants as a string gains two quote characters, so a document of
|
||||
// bare amounts can grow past the cap. unsanitize_jsontx refuses a
|
||||
// canonical form that large, so refuse it here too.
|
||||
if (out.size() > jsontx_max_text)
|
||||
fail("canonical form too large");
|
||||
return out;
|
||||
}
|
||||
|
||||
void
|
||||
varint(std::string& o, std::uint64_t v)
|
||||
{
|
||||
do
|
||||
{
|
||||
std::uint8_t const c = v & 0x7F;
|
||||
v >>= 7;
|
||||
o += static_cast<char>(c | (v ? 0x80 : 0));
|
||||
} while (v);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
std::string
|
||||
jsontx_signing_data(std::string_view preimage)
|
||||
{
|
||||
std::string r;
|
||||
r.reserve(jsontx_sign_prefix.size() + preimage.size());
|
||||
r += jsontx_sign_prefix;
|
||||
r += preimage;
|
||||
return r;
|
||||
}
|
||||
|
||||
// Not boost::algorithm::to_lower_copy or std::tolower: both consult the global
|
||||
// locale, under which a byte such as 0xC9 folds on one node and not another.
|
||||
std::string
|
||||
jsontx_lower(std::string_view s)
|
||||
{
|
||||
std::string r(s);
|
||||
for (auto& c : r)
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
c = static_cast<char>(c - 'A' + 'a');
|
||||
return r;
|
||||
}
|
||||
|
||||
SField const&
|
||||
jsontx_field(std::string const& name)
|
||||
{
|
||||
static auto const tbl = [] {
|
||||
std::unordered_map<std::string, SField const*> m;
|
||||
for (auto const& [code, f] : SField::knownCodeToField)
|
||||
if (f->isUseful() && f->isBinary() && f->fieldType < 10000 &&
|
||||
!f->fieldName.empty())
|
||||
{
|
||||
// Two fields folding to one key would make the lookup depend
|
||||
// on iteration order. None do today; keep it that way.
|
||||
if (!m.emplace(jsontx_lower(f->fieldName), f).second)
|
||||
LogicError(
|
||||
"jsontx: field names collide case-insensitively: " +
|
||||
f->fieldName);
|
||||
}
|
||||
return m;
|
||||
}();
|
||||
|
||||
auto const i = tbl.find(jsontx_lower(name));
|
||||
return i == tbl.end() ? sfInvalid : *i->second;
|
||||
}
|
||||
|
||||
std::uint64_t
|
||||
jsontx_iso(std::string_view s)
|
||||
{
|
||||
static constexpr char pat[] = "0000-00-00T00:00:00.000Z";
|
||||
if (s.size() != 24)
|
||||
fail("Time must be an ISO 8601 instant");
|
||||
for (std::size_t i = 0; i < 24; ++i)
|
||||
if (pat[i] == '0' ? !digit(s[i]) : s[i] != pat[i])
|
||||
fail("malformed Time");
|
||||
|
||||
auto const n = [&s](std::size_t i, std::size_t c) {
|
||||
int v = 0;
|
||||
while (c--)
|
||||
v = v * 10 + (s[i++] - '0');
|
||||
return v;
|
||||
};
|
||||
int const y = n(0, 4), mo = n(5, 2), d = n(8, 2), h = n(11, 2),
|
||||
mi = n(14, 2), se = n(17, 2), ms = n(20, 3);
|
||||
if (mo < 1 || mo > 12)
|
||||
fail("Time month out of range");
|
||||
bool const leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
|
||||
int const dim =
|
||||
mo == 2 ? (leap ? 29 : 28) : ((mo % 2 == 1) == (mo <= 7) ? 31 : 30);
|
||||
// 60 is refused: JS cannot emit a leap second and the ledger cannot
|
||||
// represent one
|
||||
if (d < 1 || d > dim || h > 23 || mi > 59 || se > 59)
|
||||
fail("Time out of range");
|
||||
|
||||
std::int64_t const t = (jsontx_days(y, mo, d) - jsontx_epoch_day) * 86400 +
|
||||
h * 3600 + mi * 60 + se;
|
||||
if (t < 0)
|
||||
fail("Time precedes the ripple epoch");
|
||||
return static_cast<std::uint64_t>(t) * 1000 + ms;
|
||||
}
|
||||
|
||||
std::string
|
||||
jsontx_iso_str(std::uint64_t ms)
|
||||
{
|
||||
if (ms > jsontx_max_time)
|
||||
fail("Time out of range");
|
||||
std::int64_t const z =
|
||||
static_cast<std::int64_t>(ms / 86400000) + jsontx_epoch_day + 719468;
|
||||
unsigned const tod = static_cast<unsigned>(ms / 1000 % 86400);
|
||||
std::int64_t const era = (z >= 0 ? z : z - 146096) / 146097;
|
||||
unsigned const doe = static_cast<unsigned>(z - era * 146097);
|
||||
unsigned const yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
unsigned const doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
unsigned const mp = (5 * doy + 2) / 153;
|
||||
unsigned const d = doy - (153 * mp + 2) / 5 + 1;
|
||||
unsigned const m = mp + (mp < 10 ? 3 : -9);
|
||||
std::int64_t const y =
|
||||
static_cast<std::int64_t>(yoe) + era * 400 + (m <= 2);
|
||||
|
||||
// jsontx_max_time bounds y to [2000, 9999], so every field fits its
|
||||
// width. Hand-rolled: this spelling is part of the signed preimage and a
|
||||
// library's padding rules are not.
|
||||
char buf[24];
|
||||
auto const pad = [&buf](std::size_t at, std::uint64_t v, std::size_t w) {
|
||||
while (w--)
|
||||
{
|
||||
buf[at + w] = static_cast<char>('0' + v % 10);
|
||||
v /= 10;
|
||||
}
|
||||
};
|
||||
pad(0, static_cast<std::uint64_t>(y), 4);
|
||||
buf[4] = '-';
|
||||
pad(5, m, 2);
|
||||
buf[7] = '-';
|
||||
pad(8, d, 2);
|
||||
buf[10] = 'T';
|
||||
pad(11, tod / 3600, 2);
|
||||
buf[13] = ':';
|
||||
pad(14, tod / 60 % 60, 2);
|
||||
buf[16] = ':';
|
||||
pad(17, tod % 60, 2);
|
||||
buf[19] = '.';
|
||||
pad(20, ms % 1000, 3);
|
||||
buf[23] = 'Z';
|
||||
return std::string(buf, sizeof(buf));
|
||||
}
|
||||
|
||||
std::string
|
||||
jsontx_u64_str(SField const& f, std::uint64_t v)
|
||||
{
|
||||
char buf[20];
|
||||
auto const r = std::to_chars(
|
||||
buf, buf + sizeof(buf), v, f.shouldMeta(SField::sMD_BaseTen) ? 10 : 16);
|
||||
return std::string(buf, r.ptr);
|
||||
}
|
||||
|
||||
void
|
||||
jsontx_strict(std::string_view raw)
|
||||
{
|
||||
(void)parse(raw);
|
||||
}
|
||||
|
||||
std::string
|
||||
jsontx_canonical(std::string_view raw)
|
||||
{
|
||||
return canonical(parse(raw));
|
||||
}
|
||||
|
||||
std::pair<std::string, std::string>
|
||||
sanitize_jsontx(std::string_view raw)
|
||||
{
|
||||
std::string out = canonical(parse(raw));
|
||||
|
||||
// Greedy copy/insert delta over `raw`, sourcing from `out`.
|
||||
// op 0x00 <varint len> <bytes> literal
|
||||
// op 0x01 <varint off> <varint len> copy from canonical
|
||||
//
|
||||
// This encoder is normative - unsanitize_jsontx only accepts its exact
|
||||
// output - so it must emit identical bytes on every node and stdlib.
|
||||
// Candidates for a 4-gram are tried in ascending offset order and ties
|
||||
// keep the lowest offset. The index is a sorted vector of
|
||||
// (gram << 32 | offset): offsets are unique, so the order is total and
|
||||
// std::sort is deterministic.
|
||||
std::string diff, lit;
|
||||
std::size_t ops = 0;
|
||||
|
||||
auto const gram = [](std::string_view s, std::size_t i) {
|
||||
return std::uint32_t(std::uint8_t(s[i])) << 24 |
|
||||
std::uint32_t(std::uint8_t(s[i + 1])) << 16 |
|
||||
std::uint32_t(std::uint8_t(s[i + 2])) << 8 |
|
||||
std::uint32_t(std::uint8_t(s[i + 3]));
|
||||
};
|
||||
auto const flush = [&] {
|
||||
if (lit.empty())
|
||||
return;
|
||||
diff += char(0);
|
||||
varint(diff, lit.size());
|
||||
diff += lit;
|
||||
lit.clear();
|
||||
++ops;
|
||||
};
|
||||
|
||||
std::vector<std::uint64_t> idx;
|
||||
if (out.size() >= jsontx_min_copy)
|
||||
{
|
||||
idx.reserve(out.size() - jsontx_min_copy + 1);
|
||||
for (std::size_t i = 0; i + jsontx_min_copy <= out.size(); ++i)
|
||||
idx.push_back(std::uint64_t(gram(out, i)) << 32 | i);
|
||||
std::sort(idx.begin(), idx.end());
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < raw.size();)
|
||||
{
|
||||
std::size_t bo = 0, bl = 0;
|
||||
if (i + jsontx_min_copy <= raw.size())
|
||||
{
|
||||
std::uint64_t const g = gram(raw, i);
|
||||
auto it = std::lower_bound(idx.begin(), idx.end(), g << 32);
|
||||
for (std::size_t tried = 0;
|
||||
it != idx.end() && (*it >> 32) == g && tried < jsontx_max_cand;
|
||||
++it, ++tried)
|
||||
{
|
||||
std::size_t const off = *it & 0xFFFF'FFFFu;
|
||||
std::size_t l = 0;
|
||||
while (i + l < raw.size() && off + l < out.size() &&
|
||||
out[off + l] == raw[i + l])
|
||||
++l;
|
||||
if (l > bl)
|
||||
bl = l, bo = off;
|
||||
}
|
||||
}
|
||||
if (bl >= jsontx_min_copy)
|
||||
{
|
||||
flush();
|
||||
diff += char(1);
|
||||
varint(diff, bo);
|
||||
varint(diff, bl);
|
||||
++ops;
|
||||
i += bl;
|
||||
}
|
||||
else
|
||||
lit += raw[i++];
|
||||
}
|
||||
flush();
|
||||
|
||||
// The bounds unsanitize_jsontx applies, applied here so that a document
|
||||
// this accepts is never one the verifier then refuses.
|
||||
if (diff.size() > jsontx_max_diff || ops > jsontx_max_ops)
|
||||
fail("formatting differs too much from canonical form");
|
||||
|
||||
return {std::move(out), std::move(diff)};
|
||||
}
|
||||
|
||||
// Copies read only from `sanitized`, never from the output being built, so a
|
||||
// short delta cannot expand geometrically. Offsets and lengths are range
|
||||
// checked before use, varints are length- and minimality-bounded, and the two
|
||||
// encodings the encoder can never emit - an unmerged literal run, and a copy
|
||||
// abutting the previous copy in the source - are refused.
|
||||
std::string
|
||||
unsanitize_jsontx(std::string_view sanitized, std::string_view diff)
|
||||
{
|
||||
if (sanitized.size() > jsontx_max_text || diff.size() > jsontx_max_diff)
|
||||
fail("oversize delta input");
|
||||
|
||||
std::string out;
|
||||
std::size_t p = 0, ops = 0, prevEnd = 0;
|
||||
int prev = -1;
|
||||
|
||||
auto const read = [&](std::uint64_t max) -> std::uint64_t {
|
||||
std::uint64_t v = 0;
|
||||
for (int s = 0; s <= 21; s += 7) // four bytes at most
|
||||
{
|
||||
if (p >= diff.size())
|
||||
fail("truncated delta");
|
||||
std::uint8_t const c = diff[p++];
|
||||
v |= std::uint64_t(c & 0x7F) << s;
|
||||
if (c & 0x80)
|
||||
continue;
|
||||
if (s && !(c & 0x7F))
|
||||
fail("non-minimal varint");
|
||||
if (v > max)
|
||||
fail("delta value out of range");
|
||||
return v;
|
||||
}
|
||||
fail("overlong varint");
|
||||
};
|
||||
|
||||
while (p < diff.size())
|
||||
{
|
||||
if (++ops > jsontx_max_ops)
|
||||
fail("too many delta ops");
|
||||
|
||||
std::uint8_t const op = diff[p++];
|
||||
if (op > 1)
|
||||
fail("unknown delta op");
|
||||
|
||||
if (op == 0) // literal
|
||||
{
|
||||
if (prev == 0)
|
||||
fail("unmerged literal run");
|
||||
auto const n = read(jsontx_max_text);
|
||||
if (n == 0 || n > diff.size() - p)
|
||||
fail("bad literal length");
|
||||
if (out.size() + n > jsontx_max_text)
|
||||
fail("delta expands too far");
|
||||
out += diff.substr(p, n);
|
||||
p += n;
|
||||
}
|
||||
else // copy from the canonical form
|
||||
{
|
||||
auto const off = read(sanitized.size());
|
||||
auto const n = read(sanitized.size() - off);
|
||||
if (n < jsontx_min_copy)
|
||||
fail("undersize copy");
|
||||
if (prev == 1 && off == prevEnd)
|
||||
fail("unmerged copy run");
|
||||
if (out.size() + n > jsontx_max_text)
|
||||
fail("delta expands too far");
|
||||
out += sanitized.substr(off, n);
|
||||
prevEnd = off + n;
|
||||
}
|
||||
prev = op;
|
||||
}
|
||||
|
||||
if (out.empty())
|
||||
fail("empty delta");
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string
|
||||
jsontx_verify(STTx const& stx, std::string_view diff)
|
||||
{
|
||||
// The cheap refusals first: this runs on every relay of a transaction
|
||||
// carrying a delta, before any signature is known to be good.
|
||||
if (!stx.isFieldPresent(sfTxnSignature) || stx.isFieldPresent(sfSigners))
|
||||
fail("expects a lone TxnSignature");
|
||||
|
||||
auto const pkb = stx.getSigningPubKey();
|
||||
if (publicKeyType(makeSlice(pkb)) != KeyType::ed25519)
|
||||
fail("SigningPubKey must be ed25519");
|
||||
|
||||
if (diff.size() > jsontx_max_diff)
|
||||
fail("oversize delta");
|
||||
|
||||
// No transaction serializes to more than twice its canonical text
|
||||
// (JSONTxSignatures_test checks every field of every format; the worst
|
||||
// is a bare three-letter currency code, about 1.3x). Allowing twice that
|
||||
// again, plus the delta and signature this counts but the canonical form
|
||||
// does not, loses nothing that could verify, and spares getJson on an
|
||||
// arbitrarily large object.
|
||||
if (stx.getSerializer().size() > 4 * jsontx_max_text + jsontx_max_diff)
|
||||
fail("transaction too large");
|
||||
|
||||
// Out comes everything the signer did not have in front of them: the
|
||||
// signature and the delta carrier. SigningPubKey stays; it being inside
|
||||
// the preimage binds key to signature and stops a third party re-signing
|
||||
// a captured preimage under their own key.
|
||||
auto txj = stx.STObject::getJson(JsonOptions::none);
|
||||
txj.removeMember(sfTxnSignature.fieldName);
|
||||
txj.removeMember(sfJsonTxDelta.fieldName);
|
||||
|
||||
// sfTime is milliseconds on the wire and an ISO 8601 instant in the
|
||||
// preimage: a bijection over the representable range, so the delta
|
||||
// carries nothing for the field.
|
||||
if (stx.isFieldPresent(sfTime))
|
||||
txj[sfTime.fieldName] = jsontx_iso_str(stx.getFieldU64(sfTime));
|
||||
|
||||
// canonical form, derived only from data the node already holds
|
||||
auto const san = jsontx_canonical(Json::FastWriter{}.write(txj));
|
||||
|
||||
// reconstruct the signed preimage under the caps above
|
||||
auto const raw = unsanitize_jsontx(san, diff);
|
||||
|
||||
// The signature before the binding check purely for cost: the binding
|
||||
// check re-canonicalizes and re-encodes, and without the key an attacker
|
||||
// cannot get past this line. Both must pass.
|
||||
if (!verify(
|
||||
PublicKey(makeSlice(pkb)),
|
||||
makeSlice(jsontx_signing_data(raw)),
|
||||
makeSlice(stx.getFieldVL(sfTxnSignature))))
|
||||
fail("signature does not verify");
|
||||
|
||||
// Bind the preimage to the transaction. This is the load-bearing check:
|
||||
// a delta of pure literals can reconstruct ANY text, so without it every
|
||||
// JsonTx signature the key ever produced would authorise this
|
||||
// transaction. Comparing the delta too makes it a pure function of the
|
||||
// preimage, which rules out a second delta reconstructing the same bytes
|
||||
// and yielding a second valid transaction id.
|
||||
auto const [san2, diff2] = sanitize_jsontx(raw);
|
||||
if (san2 != san || diff2 != diff)
|
||||
fail("preimage does not match transaction");
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
std::string
|
||||
jsontx_verify(STTx const& stx)
|
||||
{
|
||||
if (!stx.isFieldPresent(sfJsonTxDelta))
|
||||
fail("no delta");
|
||||
|
||||
auto const delta = stx.getFieldVL(sfJsonTxDelta);
|
||||
return jsontx_verify(
|
||||
stx,
|
||||
std::string_view(
|
||||
reinterpret_cast<char const*>(delta.data()), delta.size()));
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -198,6 +198,13 @@ STTx::getSeqProxy() const
|
||||
return SeqProxy{SeqProxy::ticket, *ticketSeq};
|
||||
}
|
||||
|
||||
bool
|
||||
STTx::isTimeSequenced() const
|
||||
{
|
||||
return isFieldPresent(sfTime) && getFieldU32(sfSequence) == 0 &&
|
||||
!isFieldPresent(sfTicketSequence);
|
||||
}
|
||||
|
||||
void
|
||||
STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey)
|
||||
{
|
||||
@@ -214,6 +221,15 @@ STTx::checkSign(
|
||||
RequireFullyCanonicalSig requireCanonicalSig,
|
||||
Rules const& rules) const
|
||||
{
|
||||
// sfJsonTxDelta is a non-signing field, so the binary signing hash does
|
||||
// not cover it: a binary signature stays valid with any delta appended.
|
||||
// Were that accepted, anyone relaying a binary-signed transaction could
|
||||
// mint as many new transaction ids for it as there are deltas. A delta
|
||||
// means the signature is over the JSON preimage, which jsontx_verify
|
||||
// checks; a binary signature never authorises one.
|
||||
if (isFieldPresent(sfJsonTxDelta))
|
||||
return Unexpected("Binary signature cannot authorise a JsonTx.");
|
||||
|
||||
try
|
||||
{
|
||||
// Determine whether we're single- or multi-signing by looking
|
||||
@@ -684,7 +700,8 @@ isPseudoTx(STObject const& tx)
|
||||
|
||||
auto tt = safe_cast<TxType>(*t);
|
||||
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY ||
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON;
|
||||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON ||
|
||||
tt == ttENTROPY;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -153,6 +153,7 @@ transResults()
|
||||
MAKE_ERROR(tefINVALID_LEDGER_FIX_TYPE, "The LedgerFixType field has an invalid value."),
|
||||
MAKE_ERROR(tefPAST_MANIFEST_SEQ, "The submitted manifest's sequence is not newer than the current."),
|
||||
MAKE_ERROR(tefREVOKED_MANIFEST, "The submitted manifest is for a revoked master key."),
|
||||
MAKE_ERROR(tefMANIFEST_KEY_IN_USE, "A key the submitted manifest names is already in use by another manifest."),
|
||||
|
||||
MAKE_ERROR(telLOCAL_ERROR, "Local failure."),
|
||||
MAKE_ERROR(telBAD_DOMAIN, "Domain too long."),
|
||||
|
||||
@@ -49,6 +49,8 @@ TxFormats::TxFormats()
|
||||
{sfNetworkID, soeOPTIONAL},
|
||||
{sfHookParameters, soeOPTIONAL},
|
||||
{sfHookName, soeOPTIONAL},
|
||||
{sfTime, soeOPTIONAL},
|
||||
{sfJsonTxDelta, soeOPTIONAL},
|
||||
};
|
||||
|
||||
#pragma push_macro("UNWRAP")
|
||||
|
||||
@@ -49,6 +49,9 @@ TxMeta::TxMeta(
|
||||
|
||||
if (obj.isFieldPresent(sfHookEmissions))
|
||||
setHookEmissions(obj.getFieldArray(sfHookEmissions));
|
||||
|
||||
if (obj.isFieldPresent(sfRandomData))
|
||||
setRandomData(obj.getFieldH256(sfRandomData));
|
||||
}
|
||||
|
||||
TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj)
|
||||
@@ -75,6 +78,9 @@ TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, STObject const& obj)
|
||||
|
||||
if (obj.isFieldPresent(sfHookEmissions))
|
||||
setHookEmissions(obj.getFieldArray(sfHookEmissions));
|
||||
|
||||
if (obj.isFieldPresent(sfRandomData))
|
||||
setRandomData(obj.getFieldH256(sfRandomData));
|
||||
}
|
||||
|
||||
TxMeta::TxMeta(uint256 const& txid, std::uint32_t ledger, Blob const& vec)
|
||||
@@ -245,6 +251,9 @@ TxMeta::getAsObject() const
|
||||
if (hasHookEmissions())
|
||||
metaData.setFieldArray(sfHookEmissions, getHookEmissions());
|
||||
|
||||
if (hasRandomData())
|
||||
metaData.setFieldH256(sfRandomData, getRandomData());
|
||||
|
||||
return metaData;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,10 @@
|
||||
#include <xrpl/beast/core/LexicalCast.h>
|
||||
#include <xrpl/beast/rfc2616.h>
|
||||
#include <xrpl/server/Port.h>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/algorithm/string/classification.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/split.hpp>
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
@@ -66,6 +69,14 @@ operator<<(std::ostream& os, Port const& p)
|
||||
}
|
||||
}
|
||||
|
||||
if (!p.pwa_domains.empty())
|
||||
{
|
||||
os << "domains:";
|
||||
for (auto const& d : p.pwa_domains)
|
||||
os << " " << d;
|
||||
os << ", ";
|
||||
}
|
||||
|
||||
if (p.secure_gateway_nets_v4.size() || p.secure_gateway_nets_v6.size())
|
||||
{
|
||||
os << "secure_gateway nets:";
|
||||
@@ -311,6 +322,64 @@ parse_Port(ParsedPort& port, Section const& section, std::ostream& log)
|
||||
port.secure_gateway_nets_v4,
|
||||
port.secure_gateway_nets_v6);
|
||||
|
||||
// pwa ports: the zone(s) documents are served under, normalised like a
|
||||
// Host header: lower case, no trailing dot, LDH labels of 1..63, with
|
||||
// room left for the longest "pwa<AccountIndex>" label in front.
|
||||
if (auto const optResult = section.get("domain"))
|
||||
{
|
||||
std::vector<std::string> domains;
|
||||
boost::algorithm::split(
|
||||
domains, *optResult, boost::algorithm::is_any_of(","));
|
||||
for (auto& domain : domains)
|
||||
{
|
||||
boost::algorithm::trim(domain);
|
||||
boost::algorithm::to_lower(domain);
|
||||
if (!domain.empty() && domain.back() == '.')
|
||||
domain.pop_back();
|
||||
if (domain.empty())
|
||||
continue;
|
||||
bool valid = domain.size() + 24 <= 253;
|
||||
std::vector<std::string> labels;
|
||||
boost::algorithm::split(
|
||||
labels, domain, boost::algorithm::is_any_of("."));
|
||||
for (auto const& l : labels)
|
||||
valid = valid && !l.empty() && l.size() <= 63 &&
|
||||
l.front() != '-' && l.back() != '-' &&
|
||||
l.find_first_not_of(
|
||||
"abcdefghijklmnopqrstuvwxyz0123456789-") ==
|
||||
std::string::npos;
|
||||
if (!valid)
|
||||
{
|
||||
log << "Invalid value '" << domain << "' for key 'domain' in ["
|
||||
<< section.name()
|
||||
<< "]: expected a DNS zone such as pwapp.example";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
port.pwa_domains.insert(domain);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom domains on pwa ports, and the IPv4 recursive resolver to use for
|
||||
// their TXT records (default: the system resolver).
|
||||
if (auto const v = section.get("custom_domains"))
|
||||
if (!beast::lexicalCastChecked(port.pwa_custom_domains, *v))
|
||||
{
|
||||
log << "Invalid value '" << *v << "' for key 'custom_domains' in ["
|
||||
<< section.name() << "]: expected 0 or 1";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
if (auto const v = section.get("resolver"))
|
||||
{
|
||||
auto const ep = beast::IP::Endpoint::from_string_checked(*v);
|
||||
if (!ep || !ep->is_v4() || ep->address().is_unspecified())
|
||||
{
|
||||
log << "Invalid value '" << *v << "' for key 'resolver' in ["
|
||||
<< section.name() << "]: expected an IPv4 address[:port]";
|
||||
Throw<std::exception>();
|
||||
}
|
||||
port.pwa_resolver = ep->port() ? *ep : ep->at_port(53);
|
||||
}
|
||||
|
||||
set(port.user, "user", section);
|
||||
set(port.password, "password", section);
|
||||
set(port.admin_user, "admin_user", section);
|
||||
|
||||
@@ -578,6 +578,25 @@ public:
|
||||
ter(tecNO_TARGET));
|
||||
env.close();
|
||||
}
|
||||
// HookName removed with a HookName-only update
|
||||
// The sfHooks entry keeps an STI_NOTPRESENT placeholder for
|
||||
// sfHookName, which preclaim must treat as "no name".
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = issuer.human();
|
||||
jv[jss::TransactionType] = jss::SetHook;
|
||||
jv[jss::Flags] = 0;
|
||||
jv[jss::Hooks] = Json::Value{Json::arrayValue};
|
||||
jv[jss::Hooks][0U][jss::Hook][jss::HookName] = "";
|
||||
env(jv, fee(XRP(1)));
|
||||
env.close();
|
||||
|
||||
env(reward::claim(alice),
|
||||
reward::issuer(issuer),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
}
|
||||
// Vaild HookOn
|
||||
{
|
||||
auto hookObj = hso(jtx::genesis::AcceptHook, overrideFlag);
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
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/basics/strHex.h>
|
||||
#include <xrpl/protocol/CreditNote.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/JSONTxSignatures.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
struct CreditNote_test : public beast::unit_test::suite
|
||||
{
|
||||
//--------------------------------------------------------------------------
|
||||
// helpers
|
||||
|
||||
// The open ledger's parent close time in milliseconds since the ripple
|
||||
// epoch: the clock NoteClaim checks the note's window against. A fresh
|
||||
// Env starts seconds after the epoch, too early to backdate a note from
|
||||
// (the subtraction would wrap), so a young clock is first moved an hour on.
|
||||
static std::uint64_t
|
||||
nowMs(jtx::Env& env)
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
if (env.current()->parentCloseTime().time_since_epoch() < 1h)
|
||||
env.close(1h);
|
||||
return static_cast<std::uint64_t>(env.current()
|
||||
->parentCloseTime()
|
||||
.time_since_epoch()
|
||||
.count()) *
|
||||
1000;
|
||||
}
|
||||
|
||||
static std::optional<std::uint64_t>
|
||||
lastNoteTime(jtx::Env& env, jtx::Account const& a)
|
||||
{
|
||||
auto const sle = env.le(a);
|
||||
if (!sle || !sle->isFieldPresent(sfLastNoteTime))
|
||||
return std::nullopt;
|
||||
return sle->getFieldU64(sfLastNoteTime);
|
||||
}
|
||||
|
||||
// The note laid out the way a browser's JSON.stringify would.
|
||||
static std::string
|
||||
noteText(
|
||||
jtx::Env& env,
|
||||
jtx::Account const& issuer,
|
||||
std::string const& amount,
|
||||
std::uint64_t issueTime,
|
||||
std::uint64_t expiration)
|
||||
{
|
||||
return "{\"NetworkID\":" +
|
||||
std::to_string(env.app().config().NETWORK_ID) + ",\"Account\":\"" +
|
||||
issuer.human() + "\",\"Amount\":" + amount + ",\"IssueTime\":\"" +
|
||||
jsontx_iso_str(issueTime) + "\",\"Expiration\":\"" +
|
||||
jsontx_iso_str(expiration) + "\"}";
|
||||
}
|
||||
|
||||
static std::string
|
||||
iou(jtx::IOU const& currency, std::string const& value)
|
||||
{
|
||||
return "{\"currency\":\"" + to_string(currency.currency) +
|
||||
"\",\"issuer\":\"" + currency.account.human() + "\",\"value\":\"" +
|
||||
value + "\"}";
|
||||
}
|
||||
|
||||
static Buffer
|
||||
noteSign(jtx::Account const& signer, std::string const& text)
|
||||
{
|
||||
return sign(
|
||||
signer.pk(), signer.sk(), makeSlice(jsontx_signing_data(text)));
|
||||
}
|
||||
|
||||
// A NoteClaim from `bearer` carrying `text`, signed by `signer`'s key.
|
||||
static Json::Value
|
||||
claim(
|
||||
jtx::Account const& bearer,
|
||||
std::string const& text,
|
||||
jtx::Account const& signer)
|
||||
{
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = "NoteClaim";
|
||||
jv[jss::Account] = bearer.human();
|
||||
jv[sfNote.fieldName] = strHex(text);
|
||||
jv[sfNoteSignature.fieldName] = strHex(noteSign(signer, text));
|
||||
jv[sfPublicKey.fieldName] = strHex(signer.pk().slice());
|
||||
return jv;
|
||||
}
|
||||
|
||||
// A fresh ten-minute note issued a minute ago.
|
||||
static std::string
|
||||
freshNote(
|
||||
jtx::Env& env,
|
||||
jtx::Account const& issuer,
|
||||
std::string const& amount,
|
||||
std::int64_t offsetMs = 0)
|
||||
{
|
||||
auto const issue = static_cast<std::uint64_t>(
|
||||
static_cast<std::int64_t>(nowMs(env)) - 60'000 + offsetMs);
|
||||
return noteText(env, issuer, amount, issue, issue + 600'000);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
void
|
||||
testParse()
|
||||
{
|
||||
testcase("parse");
|
||||
using namespace jtx;
|
||||
Env env{*this, supported_amendments()};
|
||||
Account const alice{"alice", KeyType::ed25519};
|
||||
Account const gw{"gw"};
|
||||
auto const USD = gw["USD"];
|
||||
|
||||
auto const ok = [&](std::string const& text) {
|
||||
try
|
||||
{
|
||||
parseCreditNote(text);
|
||||
return true;
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
std::uint64_t const t = 800'000'000'000ULL;
|
||||
auto const good = noteText(env, alice, "10000000", t, t + 600'000);
|
||||
|
||||
BEAST_EXPECT(ok(good));
|
||||
{
|
||||
auto const n = parseCreditNote(good);
|
||||
BEAST_EXPECT(n.account == alice.id());
|
||||
BEAST_EXPECT(n.amount == STAmount{XRPAmount{10'000'000}});
|
||||
BEAST_EXPECT(n.issueTime == t);
|
||||
BEAST_EXPECT(n.expiration == t + 600'000);
|
||||
}
|
||||
|
||||
// whitespace is the issuer's to choose; the bytes are what is signed
|
||||
BEAST_EXPECT(
|
||||
ok(" {\n \"NetworkID\": 0 ,\n\"Account\":\"" + alice.human() +
|
||||
"\",\"Amount\":1,\"IssueTime\":\"" + jsontx_iso_str(t) +
|
||||
"\",\"Expiration\":\"" + jsontx_iso_str(t + 1) + "\" }\n"));
|
||||
|
||||
// issued amounts
|
||||
BEAST_EXPECT(ok(noteText(env, alice, iou(USD, "10"), t, t + 600'000)));
|
||||
BEAST_EXPECT(ok(noteText(env, alice, iou(USD, "0.5"), t, t + 600'000)));
|
||||
BEAST_EXPECT(!ok(noteText(env, alice, iou(USD, "0"), t, t + 600'000)));
|
||||
BEAST_EXPECT(!ok(noteText(env, alice, iou(USD, "-1"), t, t + 600'000)));
|
||||
BEAST_EXPECT(!ok(noteText(
|
||||
env,
|
||||
alice,
|
||||
"{\"currency\":\"USD\",\"issuer\":\"" + gw.human() + "\"}",
|
||||
t,
|
||||
t + 600'000)));
|
||||
BEAST_EXPECT(!ok(noteText(
|
||||
env,
|
||||
alice,
|
||||
"{\"currency\":\"XAH\",\"issuer\":\"" + gw.human() +
|
||||
"\",\"value\":\"1\"}",
|
||||
t,
|
||||
t + 600'000)));
|
||||
|
||||
// shape
|
||||
BEAST_EXPECT(!ok(noteText(env, alice, "0", t, t + 600'000)));
|
||||
BEAST_EXPECT(!ok(noteText(env, alice, "-5", t, t + 600'000)));
|
||||
BEAST_EXPECT(!ok(noteText(env, alice, "\"10\"", t, t + 600'000)));
|
||||
BEAST_EXPECT(!ok(noteText(env, alice, "1.5", t, t + 600'000)));
|
||||
BEAST_EXPECT(!ok(""));
|
||||
BEAST_EXPECT(!ok("[]"));
|
||||
BEAST_EXPECT(!ok(good + " x"));
|
||||
BEAST_EXPECT(!ok(good.substr(0, good.size() - 1)));
|
||||
|
||||
// exactly the five members, spelled exactly so
|
||||
BEAST_EXPECT(
|
||||
!ok(good.substr(0, good.size() - 1) + ",\"Memo\":\"hi\"}"));
|
||||
BEAST_EXPECT(
|
||||
!ok(good.substr(0, good.size() - 1) +
|
||||
",\"TransactionType\":\"Payment\"}"));
|
||||
{
|
||||
auto bad = good;
|
||||
bad.replace(bad.find("\"Account\""), 9, "\"account\"");
|
||||
BEAST_EXPECT(!ok(bad));
|
||||
}
|
||||
{
|
||||
auto bad = good;
|
||||
bad.replace(bad.find("\"IssueTime\""), 11, "\"Expiration\"");
|
||||
BEAST_EXPECT(!ok(bad)); // duplicate Expiration
|
||||
}
|
||||
|
||||
// window
|
||||
BEAST_EXPECT(!ok(noteText(env, alice, "1", t, t)));
|
||||
BEAST_EXPECT(!ok(noteText(env, alice, "1", t + 1, t)));
|
||||
BEAST_EXPECT(ok(noteText(env, alice, "1", t, t + creditNoteMaxLifeMs)));
|
||||
BEAST_EXPECT(
|
||||
!ok(noteText(env, alice, "1", t, t + creditNoteMaxLifeMs + 1)));
|
||||
|
||||
// a time the way sfTime renders, or nothing
|
||||
BEAST_EXPECT(
|
||||
!ok("{\"NetworkID\":0,\"Account\":\"" + alice.human() +
|
||||
"\",\"Amount\":1,\"IssueTime\":" + std::to_string(t) +
|
||||
",\"Expiration\":\"" + jsontx_iso_str(t + 1) + "\"}"));
|
||||
|
||||
// size
|
||||
{
|
||||
std::string big = good;
|
||||
big.insert(1, std::string(maxCreditNoteBytes, ' '));
|
||||
BEAST_EXPECT(!ok(big));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDisabled()
|
||||
{
|
||||
testcase("disabled");
|
||||
using namespace jtx;
|
||||
Env env{*this, supported_amendments() - featureCreditNote};
|
||||
Account const alice{"alice", KeyType::ed25519};
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
env(claim(bob, freshNote(env, alice, "10000000"), alice),
|
||||
ter(temDISABLED));
|
||||
}
|
||||
|
||||
void
|
||||
testXAH()
|
||||
{
|
||||
testcase("native");
|
||||
using namespace jtx;
|
||||
Env env{*this, supported_amendments()};
|
||||
Account const alice{"alice", KeyType::ed25519};
|
||||
Account const bob{"bob"};
|
||||
Account const carol{"carol"}; // secp256k1
|
||||
Account const dave{"dave", KeyType::ed25519}; // never funded
|
||||
env.fund(XRP(10000), alice, bob, carol);
|
||||
env.close();
|
||||
|
||||
auto const text = freshNote(env, alice, "10000000");
|
||||
|
||||
// a well-formed ed25519 signature by a key the issuer does not hold
|
||||
env(claim(bob, text, dave), ter(tecNO_PERMISSION));
|
||||
|
||||
// signature over different bytes
|
||||
{
|
||||
auto jv = claim(bob, text, alice);
|
||||
jv[sfNote.fieldName] = strHex(freshNote(env, alice, "10000001"));
|
||||
env(jv, ter(temBAD_SIGNATURE));
|
||||
}
|
||||
|
||||
// not ed25519
|
||||
env(claim(bob, text, carol), ter(temMALFORMED));
|
||||
|
||||
// wrong network
|
||||
{
|
||||
auto bad = text;
|
||||
auto const nid = "\"NetworkID\":" +
|
||||
std::to_string(env.app().config().NETWORK_ID);
|
||||
bad.replace(bad.find(nid), nid.size(), "\"NetworkID\":9");
|
||||
env(claim(bob, bad, alice), ter(temMALFORMED));
|
||||
}
|
||||
|
||||
// redeeming your own note
|
||||
env(claim(alice, text, alice), ter(temREDUNDANT));
|
||||
|
||||
// too early, expired
|
||||
env(claim(bob, freshNote(env, alice, "10000000", 120'000), alice),
|
||||
ter(terPRE_SEQ));
|
||||
{
|
||||
auto const t = nowMs(env) - 700'000;
|
||||
env(claim(
|
||||
bob,
|
||||
noteText(env, alice, "10000000", t, t + 600'000),
|
||||
alice),
|
||||
ter(tecEXPIRED));
|
||||
}
|
||||
|
||||
// issuer cannot cover it above their reserve
|
||||
env(claim(bob, freshNote(env, alice, "10000000000"), alice),
|
||||
ter(tecUNFUNDED_PAYMENT));
|
||||
|
||||
BEAST_EXPECT(!lastNoteTime(env, alice));
|
||||
|
||||
// the note redeems, and once
|
||||
auto const issued = parseCreditNote(text).issueTime;
|
||||
STAmount const before = env.balance(bob);
|
||||
STAmount const fee{env.current()->fees().base};
|
||||
env(claim(bob, text, alice));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob) == before + XRP(10).value() - fee);
|
||||
BEAST_EXPECT(lastNoteTime(env, alice) == issued);
|
||||
env(claim(bob, text, alice), ter(tefPAST_SEQ));
|
||||
env(claim(carol, text, alice), ter(tefPAST_SEQ));
|
||||
|
||||
// a note issued before the redeemed one is retired with it ...
|
||||
env(claim(
|
||||
bob,
|
||||
noteText(env, alice, "1000000", issued - 1, issued + 600'000),
|
||||
alice),
|
||||
ter(tefPAST_SEQ));
|
||||
|
||||
// ... and one issued after it still redeems
|
||||
auto const later =
|
||||
noteText(env, alice, "1000000", issued + 1, issued + 600'000);
|
||||
env(claim(carol, later, alice));
|
||||
env.close();
|
||||
BEAST_EXPECT(lastNoteTime(env, alice) == issued + 1);
|
||||
}
|
||||
|
||||
void
|
||||
testRegularKey()
|
||||
{
|
||||
testcase("regular key");
|
||||
using namespace jtx;
|
||||
Env env{*this, supported_amendments()};
|
||||
Account const alice{"alice"}; // secp256k1 master
|
||||
Account const alie{"alie", KeyType::ed25519};
|
||||
Account const bob{"bob"};
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
// alice's master is secp256k1, which cannot sign a note
|
||||
env(claim(bob, freshNote(env, alice, "1000000"), alice),
|
||||
ter(temMALFORMED));
|
||||
|
||||
// an ed25519 key she does not hold yet
|
||||
env(claim(bob, freshNote(env, alice, "1000000"), alie),
|
||||
ter(tecNO_PERMISSION));
|
||||
// Close before the regkey: a tec stays in the open ledger and is
|
||||
// re-applied at close in canonical order, where it could land after
|
||||
// the regkey and redeem.
|
||||
env.close();
|
||||
|
||||
// as her regular key it signs for her
|
||||
env(regkey(alice, alie));
|
||||
env.close();
|
||||
env(claim(bob, freshNote(env, alice, "1000000"), alie));
|
||||
env.close();
|
||||
|
||||
// and a disabled master no longer does
|
||||
Account const carol{"carol", KeyType::ed25519};
|
||||
env.fund(XRP(10000), carol);
|
||||
env(regkey(carol, alie));
|
||||
env(fset(carol, asfDisableMaster), sig(carol));
|
||||
env.close();
|
||||
env(claim(bob, freshNote(env, carol, "1000000"), carol),
|
||||
ter(tecNO_PERMISSION));
|
||||
env(claim(bob, freshNote(env, carol, "1000000", 1), alie));
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testIOU()
|
||||
{
|
||||
testcase("issued currency");
|
||||
using namespace jtx;
|
||||
Env env{*this, supported_amendments()};
|
||||
Account const alice{"alice", KeyType::ed25519};
|
||||
Account const bob{"bob"};
|
||||
Account const gw{"gw", KeyType::ed25519};
|
||||
auto const USD = gw["USD"];
|
||||
env.fund(XRP(10000), alice, bob, gw);
|
||||
env.close();
|
||||
env(trust(alice, USD(1000)));
|
||||
env(pay(gw, alice, USD(100)));
|
||||
env.close();
|
||||
|
||||
// bob has no trust line; none is created
|
||||
env(claim(bob, freshNote(env, alice, iou(USD, "10")), alice),
|
||||
ter(tecNO_LINE));
|
||||
|
||||
env(trust(bob, USD(1000)));
|
||||
env.close();
|
||||
|
||||
// more than alice holds
|
||||
env(claim(bob, freshNote(env, alice, iou(USD, "101")), alice),
|
||||
ter(tecUNFUNDED_PAYMENT));
|
||||
env.close();
|
||||
|
||||
// frozen line
|
||||
env(trust(gw, bob["USD"](0), tfSetFreeze));
|
||||
env.close();
|
||||
env(claim(bob, freshNote(env, alice, iou(USD, "10")), alice),
|
||||
ter(tecFROZEN));
|
||||
// The rejected claim must close before the unfreeze: otherwise the
|
||||
// close re-applies both in canonical order and, with the unfreeze
|
||||
// first, the note redeems after all.
|
||||
env.close();
|
||||
env(trust(gw, bob["USD"](0), tfClearFreeze));
|
||||
env.close();
|
||||
|
||||
// transit: bob receives 10, alice pays 10 plus the transfer fee
|
||||
env(rate(gw, 1.25));
|
||||
env.close();
|
||||
env(claim(bob, freshNote(env, alice, iou(USD, "10")), alice));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob, USD) == USD(10));
|
||||
BEAST_EXPECT(env.balance(alice, USD) == USD(87.5));
|
||||
|
||||
// issue: the gateway's own note, no fee
|
||||
env(claim(bob, freshNote(env, gw, iou(USD, "5")), gw));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob, USD) == USD(15));
|
||||
|
||||
// redemption: a note cashed by the gateway, no line needed on its side
|
||||
env(claim(gw, freshNote(env, alice, iou(USD, "7"), 1000), alice));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(alice, USD) == USD(80.5));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testParse();
|
||||
testDisabled();
|
||||
testXAH();
|
||||
testRegularKey();
|
||||
testIOU();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(CreditNote, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -3043,6 +3043,91 @@ public:
|
||||
BEAST_EXPECT(acct->value() == alice.id());
|
||||
}
|
||||
|
||||
void
|
||||
test_otxn_field_emit_failure(FeatureBitset features)
|
||||
{
|
||||
testcase("Test otxn_field on emit failure callback");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace hook_api;
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
|
||||
// the cleanup pseudo carries sfLedgerSequence / sfTransactionHash,
|
||||
// the original emitted txn carries sfDestination
|
||||
STTx const pseudo = STTx(ttEMIT_FAILURE, [&](STObject& obj) {
|
||||
obj.setFieldU32(sfLedgerSequence, 1);
|
||||
obj.setFieldH256(sfTransactionHash, uint256{5});
|
||||
});
|
||||
STTx const emitted = STTx(ttPAYMENT, [&](STObject& obj) {
|
||||
obj[sfAccount] = alice.id();
|
||||
obj[sfDestination] = bob.id();
|
||||
});
|
||||
|
||||
for (bool const withFix : {false, true})
|
||||
{
|
||||
Env env{
|
||||
*this,
|
||||
withFix ? features | fix20261005 : features - fix20261005};
|
||||
OpenView ov{*env.current()};
|
||||
ApplyContext applyCtx = createApplyContext(env, ov, pseudo);
|
||||
StubHookContext stubCtx{.emitFailure = emitted};
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
|
||||
auto& api = hookCtx.api();
|
||||
|
||||
// present on both: read from the emitted txn either way
|
||||
{
|
||||
auto const result = api.otxn_field(sfAccount.getCode());
|
||||
BEAST_EXPECT(result.has_value());
|
||||
auto const* acct =
|
||||
dynamic_cast<STAccount const*>(result.value());
|
||||
BEAST_EXPECT(acct && acct->value() == alice.id());
|
||||
}
|
||||
|
||||
// only on the emitted txn: before the fix presence was checked
|
||||
// on the pseudo, so the field was reported missing
|
||||
{
|
||||
auto const result = api.otxn_field(sfDestination.getCode());
|
||||
if (withFix)
|
||||
{
|
||||
BEAST_EXPECT(result.has_value());
|
||||
auto const* dest =
|
||||
dynamic_cast<STAccount const*>(result.value());
|
||||
BEAST_EXPECT(dest && dest->value() == bob.id());
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
!result.has_value() && result.error() == DOESNT_EXIST);
|
||||
}
|
||||
}
|
||||
|
||||
// only on the pseudo: before the fix this throws out of
|
||||
// getField on the emitted txn
|
||||
if (withFix)
|
||||
{
|
||||
auto const result = api.otxn_field(sfLedgerSequence.getCode());
|
||||
BEAST_EXPECT(
|
||||
!result.has_value() && result.error() == DOESNT_EXIST);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool threw = false;
|
||||
try
|
||||
{
|
||||
(void)api.otxn_field(sfLedgerSequence.getCode());
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
threw = true;
|
||||
}
|
||||
BEAST_EXPECT(threw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_otxn_id(FeatureBitset features)
|
||||
{
|
||||
@@ -3741,6 +3826,174 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_state_foreign_read_accounting(FeatureBitset features)
|
||||
{
|
||||
testcase("Test state_foreign read does not consume write resources");
|
||||
|
||||
using namespace jtx;
|
||||
using namespace hook_api;
|
||||
using namespace hook;
|
||||
|
||||
bool const hasFix = features[fix20261005];
|
||||
bool const hasExtended = features[featureExtendedHookState];
|
||||
|
||||
Env env{*this, features};
|
||||
|
||||
auto const fees = env.current()->fees();
|
||||
auto const alice = Account{"alice"};
|
||||
auto const bob = Account{"bob"};
|
||||
auto const carol = Account{"carol"};
|
||||
env.fund(XRP(10000), alice);
|
||||
// exactly one state reserve slot available
|
||||
env.fund(fees.accountReserve(0) + fees.increment, bob);
|
||||
// no state reserve slot available
|
||||
env.fund(fees.accountReserve(0), carol);
|
||||
env.close();
|
||||
|
||||
uint256 const readKey{1};
|
||||
uint256 const writeKey{2};
|
||||
uint256 const ns{3};
|
||||
Bytes const existingData{0xAA, 0xBB};
|
||||
Bytes writeData{0x01};
|
||||
|
||||
OpenView ov{*env.current()};
|
||||
|
||||
// Place an existing state entry (and its namespace directory) on
|
||||
// ledger for each account, so state_foreign's cache-miss path reads it.
|
||||
auto insertState = [&](AccountID const& acc) {
|
||||
auto const dirKeylet = keylet::hookStateDir(acc, ns);
|
||||
auto dir = std::make_shared<SLE>(dirKeylet);
|
||||
dir->setFieldH256(sfRootIndex, dirKeylet.key);
|
||||
dir->setFieldV256(
|
||||
sfIndexes,
|
||||
STVector256{std::vector<uint256>{
|
||||
keylet::hookState(acc, readKey, ns).key}});
|
||||
ov.rawInsert(dir);
|
||||
|
||||
auto hs =
|
||||
std::make_shared<SLE>(keylet::hookState(acc, readKey, ns));
|
||||
hs->setFieldVL(sfHookStateData, existingData);
|
||||
hs->setFieldH256(sfHookStateKey, readKey);
|
||||
hs->setFieldU64(sfOwnerNode, 0);
|
||||
ov.rawInsert(hs);
|
||||
};
|
||||
insertState(alice.id());
|
||||
insertState(bob.id());
|
||||
insertState(carol.id());
|
||||
|
||||
STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
|
||||
ApplyContext applyCtx = createApplyContext(env, ov, invokeTx);
|
||||
|
||||
auto availableSlots = [&](AccountID const& acc) -> int64_t {
|
||||
auto const sle = ov.read(keylet::account(acc));
|
||||
int64_t avail = sle->getFieldAmount(sfBalance).xrp().drops() -
|
||||
fees.accountReserve(sle->getFieldU32(sfOwnerCount)).drops();
|
||||
return avail / fees.increment.drops();
|
||||
};
|
||||
|
||||
{
|
||||
// A read must not consume the shared modification limit.
|
||||
HookStateMap stateMap;
|
||||
auto hookCtx = makeStubHookContext(
|
||||
applyCtx, alice.id(), alice.id(), {}, stateMap);
|
||||
auto& api = hookCtx.api();
|
||||
|
||||
stateMap.modified_entry_count = max_state_modifications - 1;
|
||||
|
||||
auto const read = api.state_foreign(readKey, ns, alice.id());
|
||||
BEAST_EXPECT(read.has_value() && read.value() == existingData);
|
||||
|
||||
int64_t const slots = availableSlots(alice.id());
|
||||
BEAST_EXPECT(slots > 1);
|
||||
BEAST_EXPECT(
|
||||
stateMap.modified_entry_count ==
|
||||
max_state_modifications - (hasFix ? 1 : 0));
|
||||
BEAST_EXPECT(
|
||||
std::get<0>(stateMap[alice.id()]) == slots - (hasFix ? 0 : 1));
|
||||
|
||||
auto const write =
|
||||
api.state_foreign_set(writeKey, ns, alice.id(), writeData);
|
||||
if (hasFix)
|
||||
{
|
||||
BEAST_EXPECT(write.has_value());
|
||||
BEAST_EXPECT(
|
||||
stateMap.modified_entry_count == max_state_modifications);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!write.has_value());
|
||||
BEAST_EXPECT(write.error() == TOO_MANY_STATE_MODIFICATIONS);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// A read must not consume the reserve budget.
|
||||
HookStateMap stateMap;
|
||||
auto hookCtx =
|
||||
makeStubHookContext(applyCtx, bob.id(), bob.id(), {}, stateMap);
|
||||
auto& api = hookCtx.api();
|
||||
|
||||
BEAST_EXPECT(availableSlots(bob.id()) == 1);
|
||||
|
||||
auto const read = api.state_foreign(readKey, ns, bob.id());
|
||||
BEAST_EXPECT(read.has_value() && read.value() == existingData);
|
||||
BEAST_EXPECT(std::get<0>(stateMap[bob.id()]) == (hasFix ? 1 : 0));
|
||||
|
||||
auto const write =
|
||||
api.state_foreign_set(writeKey, ns, bob.id(), writeData);
|
||||
if (hasFix)
|
||||
{
|
||||
BEAST_EXPECT(write.has_value());
|
||||
BEAST_EXPECT(std::get<0>(stateMap[bob.id()]) == 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!write.has_value());
|
||||
BEAST_EXPECT(write.error() == RESERVE_INSUFFICIENT);
|
||||
}
|
||||
|
||||
// Modifying the entry that was read needs no new reserve and
|
||||
// is still charged as a modification.
|
||||
auto const before = stateMap.modified_entry_count;
|
||||
Bytes overwrite{0x02};
|
||||
auto const update =
|
||||
api.state_foreign_set(readKey, ns, bob.id(), overwrite);
|
||||
BEAST_EXPECT(update.has_value());
|
||||
BEAST_EXPECT(stateMap.modified_entry_count == before + 1);
|
||||
}
|
||||
|
||||
{
|
||||
// A read on an account with no spare reserve must succeed;
|
||||
// previously ExtendedHookState's sanity check rejected it.
|
||||
HookStateMap stateMap;
|
||||
auto hookCtx = makeStubHookContext(
|
||||
applyCtx, carol.id(), carol.id(), {}, stateMap);
|
||||
auto& api = hookCtx.api();
|
||||
|
||||
BEAST_EXPECT(availableSlots(carol.id()) == 0);
|
||||
|
||||
auto const read = api.state_foreign(readKey, ns, carol.id());
|
||||
if (hasExtended && !hasFix)
|
||||
{
|
||||
BEAST_EXPECT(!read.has_value());
|
||||
BEAST_EXPECT(read.error() == INTERNAL_ERROR);
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(read.has_value() && read.value() == existingData);
|
||||
BEAST_EXPECT(stateMap.modified_entry_count == (hasFix ? 0 : 1));
|
||||
}
|
||||
|
||||
// Writing a new key still fails on reserve either way.
|
||||
auto const write =
|
||||
api.state_foreign_set(writeKey, ns, carol.id(), writeData);
|
||||
BEAST_EXPECT(!write.has_value());
|
||||
if (!write.has_value())
|
||||
BEAST_EXPECT(write.error() == RESERVE_INSUFFICIENT);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_state_foreign_set_max(FeatureBitset features)
|
||||
{
|
||||
@@ -4958,6 +5211,7 @@ public:
|
||||
test_fee_base(features);
|
||||
|
||||
test_otxn_field(features);
|
||||
test_otxn_field_emit_failure(features);
|
||||
|
||||
test_ledger_keylet(features);
|
||||
|
||||
@@ -5012,6 +5266,10 @@ public:
|
||||
|
||||
test_state(features);
|
||||
test_state_foreign(features);
|
||||
test_state_foreign_read_accounting(features);
|
||||
test_state_foreign_read_accounting(features - fix20261005);
|
||||
test_state_foreign_read_accounting(
|
||||
features - fix20261005 - featureExtendedHookState);
|
||||
test_state_foreign_set(features - fixHookMap);
|
||||
test_state_foreign_set(features);
|
||||
test_state_foreign_set_max(features);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -24,6 +24,7 @@
|
||||
#include <xrpld/app/rdb/Wallet.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/base64.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/basics/contract.h>
|
||||
#include <xrpl/protocol/STExchange.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
@@ -975,9 +976,482 @@ public:
|
||||
".example.com"));
|
||||
}
|
||||
|
||||
struct Validator
|
||||
{
|
||||
SecretKey masterSecret;
|
||||
PublicKey master;
|
||||
SecretKey signingSecret;
|
||||
PublicKey signing;
|
||||
};
|
||||
|
||||
static Validator
|
||||
makeValidator()
|
||||
{
|
||||
auto const ms = randomSecretKey();
|
||||
auto const ss = randomSecretKey();
|
||||
return {
|
||||
ms,
|
||||
derivePublicKey(KeyType::ed25519, ms),
|
||||
ss,
|
||||
derivePublicKey(KeyType::secp256k1, ss)};
|
||||
}
|
||||
|
||||
/** The same master key with a fresh ephemeral key. */
|
||||
static Validator
|
||||
rotate(Validator const& v)
|
||||
{
|
||||
auto const ss = randomSecretKey();
|
||||
return {
|
||||
v.masterSecret,
|
||||
v.master,
|
||||
ss,
|
||||
derivePublicKey(KeyType::secp256k1, ss)};
|
||||
}
|
||||
|
||||
/** Another master key claiming the same ephemeral key. */
|
||||
static Validator
|
||||
impostor(Validator const& v)
|
||||
{
|
||||
auto const ms = randomSecretKey();
|
||||
return {
|
||||
ms,
|
||||
derivePublicKey(KeyType::ed25519, ms),
|
||||
v.signingSecret,
|
||||
v.signing};
|
||||
}
|
||||
|
||||
Manifest
|
||||
manifestFor(Validator const& v, int seq, bool invalidSig = false)
|
||||
{
|
||||
return makeManifest(
|
||||
v.masterSecret,
|
||||
KeyType::ed25519,
|
||||
v.signingSecret,
|
||||
KeyType::secp256k1,
|
||||
seq,
|
||||
invalidSig);
|
||||
}
|
||||
|
||||
static std::size_t
|
||||
offered(ManifestCache const& cache)
|
||||
{
|
||||
std::size_t n = 0;
|
||||
cache.for_each_gossip_manifest(
|
||||
[](std::size_t) {}, [&n](Manifest const&) { ++n; });
|
||||
return n;
|
||||
}
|
||||
|
||||
void
|
||||
testGossipAdmission()
|
||||
{
|
||||
testcase("gossip admission");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
// Gossip for a master key not held, before its ephemeral key has
|
||||
// signed anything: held back, not admitted.
|
||||
auto const a = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(a, 1), gossip) == D::unseen);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == a.signing);
|
||||
BEAST_EXPECT(!cache.getTier(a.master));
|
||||
|
||||
// Its first validation releases it, once.
|
||||
auto const seq = cache.sequence();
|
||||
auto const released = cache.noteValidation(a.signing);
|
||||
BEAST_EXPECT(released && released->masterKey == a.master);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == a.master);
|
||||
BEAST_EXPECT(cache.getTier(a.master) == gossip);
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
BEAST_EXPECT(!cache.noteValidation(a.signing));
|
||||
|
||||
// Seen first, then gossiped: admitted at once.
|
||||
auto const b = makeValidator();
|
||||
BEAST_EXPECT(!cache.noteValidation(b.signing));
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(b, 1), gossip) == D::accepted);
|
||||
|
||||
// A master key held from gossip rotates without its new key having
|
||||
// been seen...
|
||||
auto const b2 = rotate(b);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(b2, 2), gossip) == D::accepted);
|
||||
BEAST_EXPECT(cache.getMasterKey(b2.signing) == b.master);
|
||||
BEAST_EXPECT(cache.getMasterKey(b.signing) == b.signing);
|
||||
|
||||
// ...and is revoked from gossip too, since that only takes trust away.
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
makeRevocation(b.masterSecret, KeyType::ed25519), gossip) ==
|
||||
D::accepted);
|
||||
BEAST_EXPECT(cache.revoked(b.master));
|
||||
BEAST_EXPECT(cache.getMasterKey(b2.signing) == b2.signing);
|
||||
BEAST_EXPECT(cache.getTier(b.master) == gossip);
|
||||
|
||||
// A revocation for a master key not held names no ephemeral key that
|
||||
// could have been seen, so gossip cannot introduce one...
|
||||
auto const c = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
makeRevocation(c.masterSecret, KeyType::ed25519), gossip) ==
|
||||
D::unseen);
|
||||
BEAST_EXPECT(!cache.getTier(c.master));
|
||||
|
||||
// ...but a list can.
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(makeRevocation(
|
||||
c.masterSecret, KeyType::ed25519)) == D::accepted);
|
||||
BEAST_EXPECT(cache.getTier(c.master) == ManifestSource::list);
|
||||
BEAST_EXPECT(cache.revoked(c.master));
|
||||
|
||||
// A forgery waits unverified, and is dropped when released.
|
||||
auto const d = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(d, 1, true), gossip) == D::unseen);
|
||||
BEAST_EXPECT(!cache.noteValidation(d.signing));
|
||||
BEAST_EXPECT(!cache.getTier(d.master));
|
||||
|
||||
// A key is remembered for three to four epochs of the filter.
|
||||
auto const e = makeValidator();
|
||||
cache.noteValidation(e.signing);
|
||||
clock.advance(ManifestCache::seenInterval * 3);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(e, 1), gossip) == D::accepted);
|
||||
|
||||
auto const f = makeValidator();
|
||||
cache.noteValidation(f.signing);
|
||||
clock.advance(ManifestCache::seenInterval * 4);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(f, 1), gossip) == D::unseen);
|
||||
}
|
||||
|
||||
void
|
||||
testPendingSubstitution()
|
||||
{
|
||||
testcase("pending ring: forged replacement");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
// Same master and signing key, higher sequence, bad master signature.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 2, true), gossip) ==
|
||||
D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->sequence == 1);
|
||||
BEAST_EXPECT(cache.getMasterKey(v.signing) == v.master);
|
||||
}
|
||||
|
||||
// Different master claiming the same signing key, bad master signature.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
manifestFor(impostor(v), 2, true), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->masterKey == v.master);
|
||||
BEAST_EXPECT(cache.getMasterKey(v.signing) == v.master);
|
||||
}
|
||||
|
||||
// A forgery that arrives first, for the same master key with a
|
||||
// higher sequence, does not keep out the genuine manifest.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 2, true), gossip) ==
|
||||
D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->sequence == 1);
|
||||
BEAST_EXPECT(cache.getMasterKey(v.signing) == v.master);
|
||||
}
|
||||
|
||||
// Nor does one that arrives first for another master key.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
manifestFor(impostor(v), 2, true), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->masterKey == v.master);
|
||||
BEAST_EXPECT(cache.getMasterKey(v.signing) == v.master);
|
||||
}
|
||||
|
||||
// A genuine manifest waiting is not displaced by an older genuine one.
|
||||
{
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 2), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->sequence == 2);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testGossipCapacity()
|
||||
{
|
||||
testcase("gossip capacity and decay");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
std::vector<Validator> held;
|
||||
for (std::size_t i = 0; i < ManifestCache::gossipCapacity; ++i)
|
||||
{
|
||||
held.push_back(makeValidator());
|
||||
cache.noteValidation(held.back().signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(held.back(), 1), gossip) ==
|
||||
D::accepted);
|
||||
}
|
||||
BEAST_EXPECT(offered(cache) == ManifestCache::gossipCapacity);
|
||||
|
||||
// Every entry is exactly as active as a newcomer: none gives way.
|
||||
auto const newcomer = makeValidator();
|
||||
cache.noteValidation(newcomer.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(newcomer, 1), gossip) == D::full);
|
||||
|
||||
// Four half-lives on, all but the first have gone on validating. The
|
||||
// first has decayed to a sixteenth of a sighting, the rest stand at
|
||||
// seventeen sixteenths, and a newcomer is worth a whole one: it
|
||||
// displaces the first and nothing else.
|
||||
clock.advance(ManifestCache::halfLife * 4);
|
||||
for (std::size_t i = 1; i < held.size(); ++i)
|
||||
cache.noteValidation(held[i].signing);
|
||||
|
||||
auto const seq = cache.sequence();
|
||||
cache.noteValidation(newcomer.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(newcomer, 1), gossip) ==
|
||||
D::accepted);
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
BEAST_EXPECT(!cache.getTier(held[0].master));
|
||||
BEAST_EXPECT(cache.getMasterKey(held[0].signing) == held[0].signing);
|
||||
for (std::size_t i = 1; i < held.size(); ++i)
|
||||
BEAST_EXPECT(cache.getTier(held[i].master) == gossip);
|
||||
BEAST_EXPECT(offered(cache) == ManifestCache::gossipCapacity);
|
||||
|
||||
// The newcomer is now the least active, but no less active than
|
||||
// another newcomer, which therefore finds no room.
|
||||
auto const late = makeValidator();
|
||||
cache.noteValidation(late.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(late, 1), gossip) == D::full);
|
||||
|
||||
// Far enough on that the weights are rebased. Their order survives:
|
||||
// the one entry that stopped validating is the one displaced.
|
||||
clock.advance(ManifestCache::halfLife * 300);
|
||||
for (std::size_t i = 2; i < held.size(); ++i)
|
||||
cache.noteValidation(held[i].signing);
|
||||
cache.noteValidation(newcomer.signing);
|
||||
cache.noteValidation(late.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(late, 1), gossip) == D::accepted);
|
||||
BEAST_EXPECT(!cache.getTier(held[1].master));
|
||||
BEAST_EXPECT(cache.getTier(newcomer.master) == gossip);
|
||||
for (std::size_t i = 2; i < held.size(); ++i)
|
||||
BEAST_EXPECT(cache.getTier(held[i].master) == gossip);
|
||||
}
|
||||
|
||||
void
|
||||
testPinning()
|
||||
{
|
||||
testcase("pinning");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
std::vector<Validator> held;
|
||||
for (std::size_t i = 0; i < ManifestCache::gossipCapacity; ++i)
|
||||
{
|
||||
held.push_back(makeValidator());
|
||||
cache.noteValidation(held.back().signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(held.back(), 1), gossip) ==
|
||||
D::accepted);
|
||||
}
|
||||
|
||||
// Pinned, a gossip entry is retained as list and frees its slot.
|
||||
auto const seq = cache.sequence();
|
||||
cache.pin({held[0].master});
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
BEAST_EXPECT(cache.getTier(held[0].master) == ManifestSource::list);
|
||||
|
||||
auto const newcomer = makeValidator();
|
||||
cache.noteValidation(newcomer.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(newcomer, 1), gossip) ==
|
||||
D::accepted);
|
||||
for (auto const& v : held)
|
||||
BEAST_EXPECT(cache.getTier(v.master));
|
||||
|
||||
// Unpinned, it falls back into a full tier, which sheds its least
|
||||
// active entry: one of those that stopped validating.
|
||||
clock.advance(ManifestCache::halfLife);
|
||||
cache.noteValidation(held[0].signing);
|
||||
cache.noteValidation(newcomer.signing);
|
||||
cache.pin({});
|
||||
BEAST_EXPECT(cache.getTier(held[0].master) == gossip);
|
||||
BEAST_EXPECT(cache.getTier(newcomer.master) == gossip);
|
||||
std::size_t remaining = 0;
|
||||
for (std::size_t i = 1; i < held.size(); ++i)
|
||||
remaining += cache.getTier(held[i].master) ? 1 : 0;
|
||||
BEAST_EXPECT(remaining == held.size() - 2);
|
||||
BEAST_EXPECT(offered(cache) == ManifestCache::gossipCapacity);
|
||||
|
||||
// A master key a list vouched for stays in the list tier however it
|
||||
// is updated later, and is offered alongside gossip.
|
||||
auto const listed = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(listed, 1)) == D::accepted);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(rotate(listed), 2), gossip) ==
|
||||
D::accepted);
|
||||
BEAST_EXPECT(cache.getTier(listed.master) == ManifestSource::list);
|
||||
BEAST_EXPECT(offered(cache) == ManifestCache::gossipCapacity + 1);
|
||||
}
|
||||
|
||||
void
|
||||
testPrecedence()
|
||||
{
|
||||
testcase("tier precedence on key conflicts");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
// Gossip binds an ephemeral key to a master key...
|
||||
auto const a = makeValidator();
|
||||
cache.noteValidation(a.signing);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(a, 1), gossip) == D::accepted);
|
||||
|
||||
// ...and a list binding the same key to another master key wins,
|
||||
// evicting it, where before it would have been refused.
|
||||
auto const b = impostor(a);
|
||||
BEAST_EXPECT(cache.applyManifest(manifestFor(b, 1)) == D::accepted);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == b.master);
|
||||
BEAST_EXPECT(!cache.getTier(a.master));
|
||||
|
||||
// Gossip cannot do the same to the list...
|
||||
auto const c = impostor(a);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(c, 1), gossip) ==
|
||||
D::badEphemeralKey);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == b.master);
|
||||
|
||||
// ...and between equals the first holds, as it always has.
|
||||
auto const d = impostor(a);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(d, 1)) == D::badEphemeralKey);
|
||||
BEAST_EXPECT(cache.getMasterKey(a.signing) == b.master);
|
||||
}
|
||||
|
||||
void
|
||||
testPending()
|
||||
{
|
||||
testcase("pending");
|
||||
|
||||
using D = ManifestDisposition;
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
{
|
||||
// A newer manifest for a key already waiting replaces it.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
auto const v = makeValidator();
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 2), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 1), gossip) == D::unseen);
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(manifestFor(v, 3), gossip) == D::unseen);
|
||||
auto const released = cache.noteValidation(v.signing);
|
||||
BEAST_EXPECT(released && released->sequence == 3);
|
||||
BEAST_EXPECT(cache.getSequence(v.master) == 3);
|
||||
}
|
||||
|
||||
{
|
||||
// The ring holds pendingCapacity; the first to arrive makes way
|
||||
// for the one after.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{
|
||||
beast::Journal{beast::Journal::getNullSink()}, clock};
|
||||
|
||||
std::vector<Validator> waiting;
|
||||
for (std::size_t i = 0; i <= ManifestCache::pendingCapacity; ++i)
|
||||
{
|
||||
waiting.push_back(makeValidator());
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
manifestFor(waiting.back(), 1), gossip) == D::unseen);
|
||||
}
|
||||
|
||||
BEAST_EXPECT(!cache.noteValidation(waiting.front().signing));
|
||||
BEAST_EXPECT(!cache.getTier(waiting.front().master));
|
||||
|
||||
std::size_t released = 0;
|
||||
for (std::size_t i = 1; i < waiting.size(); ++i)
|
||||
released += cache.noteValidation(waiting[i].signing) ? 1 : 0;
|
||||
BEAST_EXPECT(released == ManifestCache::pendingCapacity);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testGossipAdmission();
|
||||
testGossipCapacity();
|
||||
testPendingSubstitution();
|
||||
testPinning();
|
||||
testPrecedence();
|
||||
testPending();
|
||||
|
||||
ManifestCache cache;
|
||||
{
|
||||
testcase("apply");
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2025 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/protocol/AppLoader.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class PWALoader_test : public beast::unit_test::suite
|
||||
{
|
||||
static appLoader::Result
|
||||
check(std::string const& s)
|
||||
{
|
||||
return appLoader::validate(
|
||||
reinterpret_cast<std::uint8_t const*>(s.data()), s.size());
|
||||
}
|
||||
|
||||
static std::string
|
||||
docOfSize(std::size_t len)
|
||||
{
|
||||
std::string const head = "<!DOCTYPE html><html><body>";
|
||||
std::string const tail = "</body></html>";
|
||||
if (len < head.size() + tail.size())
|
||||
return head + tail;
|
||||
return head + std::string(len - head.size() - tail.size(), 'x') + tail;
|
||||
}
|
||||
|
||||
static std::string
|
||||
doc()
|
||||
{
|
||||
return "<!DOCTYPE html><html><body>hi</body></html>";
|
||||
}
|
||||
|
||||
// AccountSet carrying sfAppLoader.
|
||||
static Json::Value
|
||||
set(jtx::Account const& a, std::string const& d)
|
||||
{
|
||||
auto jt = jtx::noop(a);
|
||||
jt[sfAppLoader.fieldName] = strHex(d);
|
||||
return jt;
|
||||
}
|
||||
|
||||
// validate() is consensus critical: these pin its edges, so a refactor
|
||||
// that moves any of them fails here rather than forking the network.
|
||||
void
|
||||
testValidator()
|
||||
{
|
||||
testcase("validator");
|
||||
using R = appLoader::Result;
|
||||
using namespace std::string_literals;
|
||||
|
||||
for (auto const& s :
|
||||
{"<!DOCTYPE html><html></html>"s,
|
||||
"<!doctype html><html></html>"s,
|
||||
"<html></html>"s,
|
||||
"<HTML></HTML>"s,
|
||||
"<html lang=\"en\" class=\"root\"></html>"s,
|
||||
" \n\t<html></html>\n\n "s,
|
||||
"<html></html >"s,
|
||||
"<html></html\n>"s,
|
||||
"\xEF\xBB\xBF<html></html>"s,
|
||||
"\xEF\xBB\xBF<!DOCTYPE html><html></html>"s,
|
||||
"<html><body>\xE6\xBC\xA2\xE5\xAD\x97</body></html>"s,
|
||||
"<html><script>var s=\"</html>\";</script></html>"s,
|
||||
"<html>\t\n\r\f</html>"s,
|
||||
"<html></html> \n"s,
|
||||
"<!DOCTYPE html>\n<!-- c --><html></html>"s,
|
||||
"<html>\xC2\x85</html>"s, // C1 controls are not rejected
|
||||
docOfSize(maxAppLoaderLength)})
|
||||
BEAST_EXPECTS(check(s) == R::ok, s);
|
||||
|
||||
std::vector<std::pair<std::string, R>> const bad{
|
||||
{""s, R::empty},
|
||||
{" \n"s, R::empty},
|
||||
{"\xEF\xBB\xBF"s, R::empty},
|
||||
{docOfSize(maxAppLoaderLength + 1), R::tooLarge},
|
||||
{"<html>\xC0\xAF</html>"s, R::badUTF8},
|
||||
{"<html>\xED\xA0\x80</html>"s, R::badUTF8},
|
||||
{"<html>\xEF\xBF\xBE</html>"s, R::badUTF8},
|
||||
{"<html>\xE6\xBC</html>"s, R::badUTF8},
|
||||
{"<html></html>\xE6"s, R::badUTF8},
|
||||
{"<html>\0</html>"s, R::badControlChar},
|
||||
{"<html>\x1B</html>"s, R::badControlChar},
|
||||
{"<html>\x7F</html>"s, R::badControlChar},
|
||||
{"hello"s, R::noDoctype},
|
||||
{"<div>frag</div>"s, R::noDoctype},
|
||||
{"<htmlish></html>"s, R::noDoctype},
|
||||
{"<!DOCTYPEhtml><html></html>"s, R::noDoctype},
|
||||
{"<!DOCTYPE"s, R::noDoctype},
|
||||
{"<!DOCTYPE html>"s, R::noHtmlElement},
|
||||
{"<!DOCTYPE html><htmlish></html>"s, R::noHtmlElement},
|
||||
{"<html>"s, R::unclosed},
|
||||
{"<html></html"s, R::unclosed},
|
||||
{"<html></htmlx>"s, R::unclosed},
|
||||
{"<html></html>trailing"s, R::trailingGarbage},
|
||||
{"<html></html><p>"s, R::trailingGarbage},
|
||||
{"<html></html>\xC2\xA0"s, R::trailingGarbage}, // NBSP
|
||||
};
|
||||
for (auto const& [s, r] : bad)
|
||||
BEAST_EXPECTS(
|
||||
check(s) == r, s + ": " + appLoader::to_string(check(s)));
|
||||
}
|
||||
|
||||
void
|
||||
testEnabled()
|
||||
{
|
||||
testcase("amendment gate");
|
||||
using namespace jtx;
|
||||
Env env{*this, supported_amendments() - featurePWALoader};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
env(set(alice, doc()), fee(XRP(1)), ter(temDISABLED));
|
||||
env(set(alice, ""), fee(XRP(1)), ter(temDISABLED));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(keylet::appLoader(alice.id())));
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAppLoaderSequence));
|
||||
}
|
||||
|
||||
void
|
||||
testSetAndRemove()
|
||||
{
|
||||
testcase("set, overwrite, remove, sequence");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
auto const kl = keylet::appLoader(alice.id());
|
||||
auto seq = [&] {
|
||||
return (*env.le(alice))[~sfAppLoaderSequence].value_or(0);
|
||||
};
|
||||
|
||||
BEAST_EXPECT(!env.le(kl));
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAppLoaderSequence));
|
||||
|
||||
// Create: the object, with the owner's new sequence; nothing inline
|
||||
// on the AccountRoot, no reserve, not in the owner directory.
|
||||
env(set(alice, doc()), fee(XRP(1)));
|
||||
env.close();
|
||||
auto sle = env.le(kl);
|
||||
if (BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT((*sle)[sfAppLoader] == makeSlice(doc()));
|
||||
BEAST_EXPECT((*sle)[sfOwner] == alice.id());
|
||||
BEAST_EXPECT((*sle)[sfAppLoaderSequence] == 1);
|
||||
}
|
||||
BEAST_EXPECT(seq() == 1);
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAppLoader));
|
||||
BEAST_EXPECT((*env.le(alice))[sfOwnerCount] == 0);
|
||||
BEAST_EXPECT(
|
||||
env.rpc(
|
||||
"json",
|
||||
"account_objects",
|
||||
R"({"account":")" + alice.human() +
|
||||
R"("})")[jss::result][jss::account_objects]
|
||||
.size() == 0);
|
||||
auto const txid = (*sle)[sfPreviousTxnID];
|
||||
|
||||
// Overwrite: new content, next sequence, rethreaded.
|
||||
env(set(alice, "<html><body>v2</body></html>"), fee(XRP(1)));
|
||||
env.close();
|
||||
sle = env.le(kl);
|
||||
if (BEAST_EXPECT(sle))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
(*sle)[sfAppLoader] ==
|
||||
makeSlice(std::string("<html><body>v2</body></html>")));
|
||||
BEAST_EXPECT((*sle)[sfAppLoaderSequence] == 2);
|
||||
BEAST_EXPECT((*sle)[sfPreviousTxnID] != txid);
|
||||
}
|
||||
BEAST_EXPECT(seq() == 2);
|
||||
|
||||
// An AccountSet without the field leaves everything alone.
|
||||
env(noop(alice));
|
||||
env.close();
|
||||
BEAST_EXPECT((*env.le(kl))[sfAppLoaderSequence] == 2 && seq() == 2);
|
||||
|
||||
// Remove with an empty blob: the object goes, the sequence still
|
||||
// advances (so a copy of sequence 2 is known to be stale), and
|
||||
// removing again is a no-op that advances it again.
|
||||
env(set(alice, ""), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(kl) && seq() == 3);
|
||||
env(set(alice, ""), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(kl) && seq() == 4);
|
||||
|
||||
// Republish: the sequence carries on.
|
||||
env(set(alice, doc()), fee(XRP(1)));
|
||||
env.close();
|
||||
BEAST_EXPECT((*env.le(kl))[sfAppLoaderSequence] == 5 && seq() == 5);
|
||||
|
||||
// ledger_entry reaches it by account or by index.
|
||||
for (auto const& req :
|
||||
{R"({"app_loader":")" + alice.human() + R"("})",
|
||||
R"({"index":")" + to_string(kl.key) + R"("})"})
|
||||
{
|
||||
auto const r = env.rpc("json", "ledger_entry", req)[jss::result];
|
||||
BEAST_EXPECTS(
|
||||
r[jss::node][sfLedgerEntryType.fieldName] == jss::AppLoader &&
|
||||
r[jss::node][sfAppLoaderSequence.fieldName] == 5,
|
||||
req);
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
env.rpc(
|
||||
"json",
|
||||
"ledger_entry",
|
||||
R"({"app_loader":"nope"})")[jss::result][jss::error] ==
|
||||
"malformedAddress");
|
||||
}
|
||||
|
||||
void
|
||||
testFee()
|
||||
{
|
||||
testcase("fee: base plus one drop per byte");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
auto const base = env.current()->fees().base;
|
||||
auto const kl = keylet::appLoader(alice.id());
|
||||
|
||||
auto const expected = base + XRPAmount{1024};
|
||||
env(set(alice, docOfSize(1024)),
|
||||
fee(expected - XRPAmount{1}),
|
||||
ter(telINSUF_FEE_P));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(kl));
|
||||
auto const before = env.balance(alice);
|
||||
env(set(alice, docOfSize(1024)), fee(expected));
|
||||
env.close();
|
||||
BEAST_EXPECT(
|
||||
env.le(kl) && before - env.balance(alice) == drops(expected));
|
||||
|
||||
// Overwrites pay again; removal is base fee only.
|
||||
env(set(alice, docOfSize(100)),
|
||||
fee(base + XRPAmount{99}),
|
||||
ter(telINSUF_FEE_P));
|
||||
env(set(alice, docOfSize(100)), fee(base + XRPAmount{100}));
|
||||
env(set(alice, ""), fee(base));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(kl));
|
||||
|
||||
// Publishing takes no reserve: an account at its reserve floor can.
|
||||
Account const poor{"poor"};
|
||||
auto const reserve = env.current()->fees().accountReserve(0);
|
||||
env.fund(drops(reserve + base + XRPAmount{100}), noripple(poor));
|
||||
env.close();
|
||||
env(set(poor, docOfSize(100)), fee(base + XRPAmount{100}));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.le(keylet::appLoader(poor.id())));
|
||||
BEAST_EXPECT(env.balance(poor) == drops(reserve));
|
||||
env.require(owners(poor, 0));
|
||||
}
|
||||
|
||||
void
|
||||
testMalformed()
|
||||
{
|
||||
testcase("malformed documents are temMALFORMED");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
for (auto const& d :
|
||||
{std::string("hello"),
|
||||
std::string("<html>"),
|
||||
std::string("<html></html>x"),
|
||||
std::string("<html>\0</html>", 14),
|
||||
std::string("<html>\xC0\xAF</html>"),
|
||||
docOfSize(maxAppLoaderLength + 1)})
|
||||
{
|
||||
env(set(alice, d), fee(XRP(1)), ter(temMALFORMED));
|
||||
env.close();
|
||||
}
|
||||
BEAST_EXPECT(!env.le(keylet::appLoader(alice.id())));
|
||||
BEAST_EXPECT(!env.le(alice)->isFieldPresent(sfAppLoaderSequence));
|
||||
}
|
||||
|
||||
void
|
||||
testAccountDelete()
|
||||
{
|
||||
testcase("account delete erases the loader");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"}, bob{"bob"};
|
||||
env.fund(XRP(1000), alice, bob);
|
||||
env.close();
|
||||
env(set(alice, doc()), fee(XRP(1)));
|
||||
env.close();
|
||||
for (int i = 0; i < 256; ++i)
|
||||
env.close();
|
||||
// Not in the owner directory, so it cannot block deletion; erased
|
||||
// explicitly, or AccountRootsDeletedClean would fire on the orphan.
|
||||
env(acctdelete(alice, bob),
|
||||
fee(drops(env.current()->fees().increment)));
|
||||
env.close();
|
||||
BEAST_EXPECT(!env.le(alice));
|
||||
BEAST_EXPECT(!env.le(keylet::appLoader(alice.id())));
|
||||
}
|
||||
|
||||
void
|
||||
testValidateRPC()
|
||||
{
|
||||
testcase("pwa_validate");
|
||||
using namespace jtx;
|
||||
Env env{*this};
|
||||
Account const alice{"alice"};
|
||||
env.fund(XRP(1000), alice);
|
||||
env.close();
|
||||
auto validate = [&](std::string const& d, std::string const& domain) {
|
||||
Json::Value p(Json::objectValue);
|
||||
p[jss::account] = alice.human();
|
||||
p[jss::hash] = to_string(sha512Half(makeSlice(d)));
|
||||
if (!domain.empty())
|
||||
p[jss::domain] = domain;
|
||||
return env.rpc("json", "pwa_validate", to_string(p))[jss::result];
|
||||
};
|
||||
|
||||
BEAST_EXPECT(validate(doc(), "")["reason"] == "no loader");
|
||||
env(set(alice, doc()), fee(XRP(1)));
|
||||
env.close();
|
||||
auto r = validate(doc(), "");
|
||||
BEAST_EXPECT(r["valid"] == true && r["app_loader_sequence"] == 1);
|
||||
BEAST_EXPECT(r[jss::ledger_index] == env.closed()->seq());
|
||||
BEAST_EXPECT(
|
||||
validate("<html></html>", "")["reason"] == "hash mismatch");
|
||||
|
||||
// domain: the account's Domain, or a host beneath it, ignoring case.
|
||||
BEAST_EXPECT(
|
||||
validate(doc(), "pwa.test")["reason"] ==
|
||||
"domain not endorsed by account");
|
||||
auto jt = noop(alice);
|
||||
jt[sfDomain.fieldName] = strHex(std::string("Pwa.Test"));
|
||||
env(jt);
|
||||
env.close();
|
||||
for (auto const d :
|
||||
{"pwa.test", "PWA.TEST.", "pwaabcd.pwa.test", "a.b.pwa.test"})
|
||||
BEAST_EXPECTS(validate(doc(), d)["valid"] == true, d);
|
||||
for (auto const d : {"other.test", "xpwa.test", "pwa.test.evil"})
|
||||
BEAST_EXPECTS(validate(doc(), d)["valid"] == false, d);
|
||||
|
||||
// Malformed input.
|
||||
BEAST_EXPECT(
|
||||
env.rpc(
|
||||
"json",
|
||||
"pwa_validate",
|
||||
R"({"account":"x","hash":"00"})")[jss::result][jss::error] ==
|
||||
"actMalformed");
|
||||
BEAST_EXPECT(
|
||||
env.rpc(
|
||||
"pwa_validate", alice.human(), "zz")[jss::result][jss::error] ==
|
||||
"invalidParams");
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testValidator();
|
||||
testEnabled();
|
||||
testSetAndRemove();
|
||||
testFee();
|
||||
testMalformed();
|
||||
testAccountDelete();
|
||||
testValidateRPC();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(PWALoader, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,111 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/misc/RotatingBloomFilter.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class RotatingBloomFilter_test : public beast::unit_test::suite
|
||||
{
|
||||
using time_point = std::chrono::steady_clock::time_point;
|
||||
|
||||
void
|
||||
testWindow()
|
||||
{
|
||||
testcase("window");
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
RotatingBloomFilter<1024, 4, 4> filter{10s};
|
||||
time_point const t{};
|
||||
std::string const a{"a"};
|
||||
std::string const b{"b"};
|
||||
|
||||
filter.insert(a, t);
|
||||
BEAST_EXPECT(filter.contains(a, t));
|
||||
BEAST_EXPECT(!filter.contains(b, t));
|
||||
|
||||
// Remembered to the end of the third epoch after its own, and
|
||||
// forgotten at the start of the fourth.
|
||||
BEAST_EXPECT(filter.contains(a, t + 39s));
|
||||
BEAST_EXPECT(!filter.contains(a, t + 40s));
|
||||
|
||||
// Inserting again extends it.
|
||||
filter.insert(a, t + 35s);
|
||||
BEAST_EXPECT(filter.contains(a, t + 69s));
|
||||
BEAST_EXPECT(!filter.contains(a, t + 70s));
|
||||
|
||||
// A clock that jumps finds everything expired.
|
||||
filter.insert(b, t + 70s);
|
||||
BEAST_EXPECT(!filter.contains(b, t + 1h));
|
||||
|
||||
// An insertion that read the clock before another thread rotated its
|
||||
// slot lands in the newer epoch rather than being lost.
|
||||
filter.insert(b, t + 1h + 40s);
|
||||
filter.insert(a, t + 1h);
|
||||
BEAST_EXPECT(filter.contains(a, t + 1h + 40s));
|
||||
}
|
||||
|
||||
void
|
||||
testFalsePositives()
|
||||
{
|
||||
testcase("false positives");
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
using Filter = RotatingBloomFilter<std::size_t{1} << 14, 4, 4>;
|
||||
static_assert(Filter::bytes == 8192);
|
||||
|
||||
Filter filter{120s};
|
||||
time_point const t{};
|
||||
|
||||
std::uint64_t const inserted = 1000;
|
||||
for (std::uint64_t i = 0; i < inserted; ++i)
|
||||
filter.insert(i, t);
|
||||
|
||||
bool all = true;
|
||||
for (std::uint64_t i = 0; i < inserted; ++i)
|
||||
all = all && filter.contains(i, t);
|
||||
BEAST_EXPECT(all);
|
||||
|
||||
// About 0.24% in theory for a thousand elements in one generation.
|
||||
std::uint64_t const probes = 100000;
|
||||
std::uint64_t hits = 0;
|
||||
for (std::uint64_t i = inserted; i < inserted + probes; ++i)
|
||||
hits += filter.contains(i, t) ? 1 : 0;
|
||||
BEAST_EXPECT(hits < probes / 100);
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testWindow();
|
||||
testFalsePositives();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(RotatingBloomFilter, app, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -33,9 +33,11 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
@@ -93,6 +95,27 @@ class SHAMapStore_test : public beast::unit_test::suite
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// Online delete with DatabasePinned over in-memory backends. Unlike
|
||||
// pinnedPersistent, the hot store keeps nodes (type=rwdb is the null
|
||||
// node store), so ledgers closed in the test can be fetched. There is
|
||||
// no database_path, so every SHAMapStoreImp opens its own private
|
||||
// state.db: a second instance built by a test does not see (or trip
|
||||
// over) the rotation state of the Env's own.
|
||||
static auto
|
||||
pinnedInMemory(
|
||||
std::unique_ptr<Config> cfg,
|
||||
std::string const& nodePath,
|
||||
std::string const& pinnedPath)
|
||||
{
|
||||
cfg = onlineDelete(std::move(cfg));
|
||||
auto& section = cfg->section(ConfigSection::nodeDatabase());
|
||||
section.set("type", "memory");
|
||||
section.set("path", nodePath);
|
||||
section.set("pinned_type", "memory");
|
||||
section.set("pinned_path", pinnedPath);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
bool
|
||||
goodLedger(
|
||||
jtx::Env& env,
|
||||
@@ -1153,6 +1176,127 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// catalogue_unpin of everything (or a rolled-back first load) leaves the
|
||||
// pinned set empty. That must persist as "no pins", not as a string that
|
||||
// fails to parse on the next start.
|
||||
void
|
||||
testPinnedRangeRestoreEmpty()
|
||||
{
|
||||
testcase("pinned range restore of an empty set");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
beast::temp_dir tempDir;
|
||||
auto const tempPath = boost::filesystem::path(tempDir.path());
|
||||
auto const nodePath = (tempPath / "node").string();
|
||||
auto const pinnedPath = (tempPath / "pinned").string();
|
||||
|
||||
Env env(*this, envconfig(pinnedInMemory, nodePath, pinnedPath));
|
||||
|
||||
NodeStoreScheduler scheduler(env.app().getJobQueue());
|
||||
auto const journal = env.app().journal("SHAMapStoreTest");
|
||||
try
|
||||
{
|
||||
SHAMapStoreImp restoreCheck(env.app(), scheduler, journal);
|
||||
|
||||
RangeSet<std::uint32_t> ranges;
|
||||
ranges.insert(range(100u, 200u));
|
||||
restoreCheck.setPinnedRanges(ranges);
|
||||
restoreCheck.setPinnedRanges(RangeSet<std::uint32_t>{});
|
||||
|
||||
restoreCheck.start();
|
||||
restoreCheck.stop();
|
||||
pass();
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
fail(
|
||||
std::string("restart with no pinned ranges threw: ") +
|
||||
e.what());
|
||||
}
|
||||
BEAST_EXPECT(
|
||||
env.app().getLedgerMaster().getPinnedLedgersRangeSet().empty());
|
||||
}
|
||||
|
||||
// A crash between a pinned-range checkpoint and the backend committing
|
||||
// the last ledgers leaves the recorded interval's tail missing. Startup
|
||||
// trims it back to the last ledger present instead of refusing to start.
|
||||
void
|
||||
testPinnedRangeRestoreTrimsMissingTail()
|
||||
{
|
||||
testcase("pinned range restore trims a missing tail");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
beast::temp_dir tempDir;
|
||||
auto const tempPath = boost::filesystem::path(tempDir.path());
|
||||
auto const nodePath = (tempPath / "node").string();
|
||||
auto const pinnedPath = (tempPath / "pinned").string();
|
||||
|
||||
Env env(*this, envconfig(pinnedInMemory, nodePath, pinnedPath));
|
||||
for (int i = 0; i < 4; ++i)
|
||||
env.close();
|
||||
|
||||
auto& rdb = env.app().getRelationalDatabase();
|
||||
auto const last = env.app().getLedgerMaster().getValidLedgerIndex();
|
||||
auto const first = last - 2;
|
||||
|
||||
// Ledger saves may complete asynchronously; wait for the SQL rows.
|
||||
for (int i = 0; i < 100 && rdb.getHashByIndex(last).isZero(); ++i)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
if (!BEAST_EXPECT(
|
||||
rdb.getHashByIndex(first).isNonZero() &&
|
||||
rdb.getHashByIndex(last).isNonZero()))
|
||||
return;
|
||||
|
||||
NodeStoreScheduler scheduler(env.app().getJobQueue());
|
||||
auto const journal = env.app().journal("SHAMapStoreTest");
|
||||
try
|
||||
{
|
||||
SHAMapStoreImp restoreCheck(env.app(), scheduler, journal);
|
||||
|
||||
// Recorded through last + 10, but only [first, last] exist.
|
||||
RangeSet<std::uint32_t> recorded;
|
||||
recorded.insert(range(first, last + 10));
|
||||
restoreCheck.setPinnedRanges(recorded);
|
||||
|
||||
restoreCheck.start();
|
||||
restoreCheck.stop();
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
fail(std::string("restart with a missing tail threw: ") + e.what());
|
||||
return;
|
||||
}
|
||||
|
||||
RangeSet<std::uint32_t> expected;
|
||||
expected.insert(range(first, last));
|
||||
BEAST_EXPECT(
|
||||
env.app().getLedgerMaster().getPinnedLedgersRangeSet() == expected);
|
||||
|
||||
// A missing tail longer than the trim limit (65536 ledgers) is not
|
||||
// an interrupted load: it means the wrong pinned store, so startup
|
||||
// still refuses.
|
||||
try
|
||||
{
|
||||
SHAMapStoreImp restoreCheck(env.app(), scheduler, journal);
|
||||
|
||||
RangeSet<std::uint32_t> recorded;
|
||||
recorded.insert(range(first, last + 70000));
|
||||
restoreCheck.setPinnedRanges(recorded);
|
||||
|
||||
restoreCheck.start();
|
||||
restoreCheck.stop();
|
||||
fail("Expected startup failure for an over-long missing tail");
|
||||
}
|
||||
catch (std::runtime_error const& e)
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::string(e.what()).find("Persisted pinned interval") !=
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: compare two RangeSet<uint32_t> for set-equality. The
|
||||
// test cares about which sequences are in the result, not the
|
||||
// particular interval representation.
|
||||
@@ -1302,6 +1446,8 @@ public:
|
||||
testNullFactoryDropsWrites();
|
||||
testRotate();
|
||||
testPinnedRangeRestoreRequiresPinnedData();
|
||||
testPinnedRangeRestoreEmpty();
|
||||
testPinnedRangeRestoreTrimsMissingTail();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <xrpl/hook/Enum.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/JSONTxSignatures.h>
|
||||
#include <xrpl/protocol/PayChan.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/Sign.h>
|
||||
@@ -6146,6 +6147,15 @@ private:
|
||||
pass();
|
||||
}
|
||||
|
||||
void
|
||||
testEntropyTSH(FeatureBitset features)
|
||||
{
|
||||
testcase("entropy tsh");
|
||||
|
||||
// pseudo transaction
|
||||
pass();
|
||||
}
|
||||
|
||||
// | otxn | tfBurnable | tsh | mint | burn | buy | sell | cancel
|
||||
// | O | false | O | N/A | S | N/A | S | S
|
||||
// | O | false | I | N/A | N | N/A | W | N/A
|
||||
@@ -8428,6 +8438,115 @@ private:
|
||||
return *hash;
|
||||
}
|
||||
|
||||
// NoteClaim
|
||||
// | otxn | tsh | claim |
|
||||
// | B | B | S | bearer
|
||||
// | B | I | S | note issuer (pays the note)
|
||||
void
|
||||
testNoteClaimTSH(FeatureBitset features)
|
||||
{
|
||||
using namespace test::jtx;
|
||||
using namespace std::literals;
|
||||
testcase("note claim TSH");
|
||||
|
||||
features = features | featureCreditNote;
|
||||
|
||||
// A fresh ten-minute XAH note from `issuer`, issued a minute ago and
|
||||
// laid out the way CreditNote_test builds them.
|
||||
auto const makeNote = [](Env& env, Account const& issuer) {
|
||||
// a fresh Env's clock is too young to backdate a note from
|
||||
if (env.current()->parentCloseTime().time_since_epoch() < 1h)
|
||||
env.close(1h);
|
||||
auto const nowMs =
|
||||
static_cast<std::uint64_t>(env.current()
|
||||
->parentCloseTime()
|
||||
.time_since_epoch()
|
||||
.count()) *
|
||||
1000;
|
||||
auto const issue = nowMs - 60'000;
|
||||
return "{\"NetworkID\":" +
|
||||
std::to_string(env.app().config().NETWORK_ID) +
|
||||
",\"Account\":\"" + issuer.human() +
|
||||
"\",\"Amount\":10000000,\"IssueTime\":\"" +
|
||||
jsontx_iso_str(issue) + "\",\"Expiration\":\"" +
|
||||
jsontx_iso_str(issue + 600'000) + "\"}";
|
||||
};
|
||||
|
||||
// A NoteClaim from `bearer` redeeming `text`, signed by `issuer`.
|
||||
auto const claim = [](Account const& bearer,
|
||||
Account const& issuer,
|
||||
std::string const& text) {
|
||||
Json::Value jv;
|
||||
jv[jss::TransactionType] = "NoteClaim";
|
||||
jv[jss::Account] = bearer.human();
|
||||
jv[sfNote.fieldName] = strHex(text);
|
||||
jv[sfNoteSignature.fieldName] = strHex(sign(
|
||||
issuer.pk(),
|
||||
issuer.sk(),
|
||||
makeSlice(jsontx_signing_data(text))));
|
||||
jv[sfPublicKey.fieldName] = strHex(issuer.pk().slice());
|
||||
return jv;
|
||||
};
|
||||
|
||||
// otxn: bearer
|
||||
// tsh bearer
|
||||
// w/s: strong
|
||||
for (bool const testStrong : {true, false})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
auto const issuer = Account("alice", KeyType::ed25519);
|
||||
auto const bearer = Account("bob");
|
||||
env.fund(XRP(1000), issuer, bearer);
|
||||
env.close();
|
||||
|
||||
if (!testStrong)
|
||||
addWeakTSH(env, bearer);
|
||||
|
||||
setTSHHook(env, bearer, testStrong);
|
||||
|
||||
env(claim(bearer, issuer, makeNote(env, issuer)),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
testTSHStrongWeak(env, tshSTRONG, __LINE__);
|
||||
}
|
||||
|
||||
// otxn: bearer
|
||||
// tsh issuer
|
||||
// w/s: strong
|
||||
//
|
||||
// The issuer is who pays, so their hook may roll the redemption back.
|
||||
for (bool const testStrong : {true, false})
|
||||
{
|
||||
test::jtx::Env env{
|
||||
*this,
|
||||
network::makeNetworkConfig(21337, "10", "1000000", "200000"),
|
||||
features};
|
||||
|
||||
auto const issuer = Account("alice", KeyType::ed25519);
|
||||
auto const bearer = Account("bob");
|
||||
env.fund(XRP(1000), issuer, bearer);
|
||||
env.close();
|
||||
|
||||
if (!testStrong)
|
||||
addWeakTSH(env, issuer);
|
||||
|
||||
setTSHHook(env, issuer, testStrong);
|
||||
|
||||
env(claim(bearer, issuer, makeNote(env, issuer)),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
testTSHStrongWeak(env, tshSTRONG, __LINE__);
|
||||
}
|
||||
}
|
||||
|
||||
// SetManifest
|
||||
// | otxn | tsh | manifest |
|
||||
// | M | M | N/A |
|
||||
|
||||
@@ -2134,6 +2134,118 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
/// Rename and remove the HookName with HookName-only updates
|
||||
Env env{*this, features};
|
||||
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
// Create with hook name
|
||||
auto jvh = hso(accept_wasm);
|
||||
jvh[jss::HookName] = "41424344";
|
||||
env(ripple::test::jtx::hook(alice, {{jvh}}, 0),
|
||||
M("Create named hook"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
|
||||
auto const hookNameOnlyUpdate = [&](std::string const& name) {
|
||||
Json::Value jv;
|
||||
jv[jss::Account] = alice.human();
|
||||
jv[jss::TransactionType] = jss::SetHook;
|
||||
jv[jss::Flags] = 0;
|
||||
jv[jss::Hooks] = Json::Value{Json::arrayValue};
|
||||
Json::Value iv;
|
||||
iv[jss::HookName] = name;
|
||||
jv[jss::Hooks][0U][jss::Hook] = iv;
|
||||
return jv;
|
||||
};
|
||||
|
||||
// Rename: update carrying only a non-empty sfHookName
|
||||
env(hookNameOnlyUpdate("4142434445"),
|
||||
M("Rename hook with a HookName-only update"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
{
|
||||
auto const hooks = env.le(keylet::hook(alice));
|
||||
BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks));
|
||||
auto const& hooksArray = hooks->getFieldArray(sfHooks);
|
||||
BEAST_EXPECT(hooksArray.size() == 1);
|
||||
BEAST_EXPECT(hooksArray[0].isFieldPresent(sfHookName));
|
||||
BEAST_EXPECT(
|
||||
strHex(hooksArray[0].getFieldVL(sfHookName)) ==
|
||||
"4142434445");
|
||||
}
|
||||
|
||||
// Remove: update carrying only an empty sfHookName
|
||||
env(hookNameOnlyUpdate(""),
|
||||
M("Remove hook name with a HookName-only update"),
|
||||
HSFEE);
|
||||
|
||||
// In the same open ledger as the removal: the open view hands
|
||||
// the next transaction the very sfHooks entry the SetHook just
|
||||
// modified, so the fee calculation and the hook chain must
|
||||
// tolerate the removed name. This is the path that crashed the
|
||||
// node at submit, and crashed every validator replaying the
|
||||
// pair while building the ledger.
|
||||
{
|
||||
auto jv = invoke::invoke(alice);
|
||||
auto const expectedFee =
|
||||
calculateBaseFee(*env.current(), *env.jt(jv).stx);
|
||||
BEAST_EXPECT(expectedFee == drops(19));
|
||||
env(jv,
|
||||
M("Invoke in the same open ledger as the removal"),
|
||||
HSFEE);
|
||||
}
|
||||
env.close();
|
||||
{
|
||||
auto const hookExecutions =
|
||||
env.meta()->getFieldArray(sfHookExecutions);
|
||||
BEAST_EXPECT(hookExecutions.size() == 1);
|
||||
BEAST_EXPECT(
|
||||
hookExecutions[0].getFieldH256(sfHookHash) == accept_hash);
|
||||
}
|
||||
{
|
||||
auto const hooks = env.le(keylet::hook(alice));
|
||||
BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks));
|
||||
auto const& hooksArray = hooks->getFieldArray(sfHooks);
|
||||
BEAST_EXPECT(hooksArray.size() == 1);
|
||||
BEAST_EXPECT(!hooksArray[0].isFieldPresent(sfHookName));
|
||||
}
|
||||
|
||||
// The now non-named hook must still be charged for and
|
||||
// executed by transactions in later ledgers, where the entry
|
||||
// is re-read from its serialized form.
|
||||
{
|
||||
auto jv = invoke::invoke(alice);
|
||||
auto const expectedFee =
|
||||
calculateBaseFee(*env.current(), *env.jt(jv).stx);
|
||||
BEAST_EXPECT(expectedFee == drops(19));
|
||||
env(jv, M("Invoke after removing the hook name"), HSFEE);
|
||||
env.close();
|
||||
auto const hookExecutions =
|
||||
env.meta()->getFieldArray(sfHookExecutions);
|
||||
BEAST_EXPECT(hookExecutions.size() == 1);
|
||||
BEAST_EXPECT(
|
||||
hookExecutions[0].getFieldH256(sfHookHash) == accept_hash);
|
||||
}
|
||||
|
||||
// Removing the name of a hook that has none must be a no-op,
|
||||
// not a thrown FieldErr (-> tefEXCEPTION): makeFieldAbsent on
|
||||
// the untemplated entry throws when the field is missing.
|
||||
env(hookNameOnlyUpdate(""),
|
||||
M("Remove hook name again (no-op)"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
{
|
||||
auto const hooks = env.le(keylet::hook(alice));
|
||||
BEAST_EXPECT(hooks && hooks->isFieldPresent(sfHooks));
|
||||
auto const& hooksArray = hooks->getFieldArray(sfHooks);
|
||||
BEAST_EXPECT(hooksArray.size() == 1);
|
||||
BEAST_EXPECT(!hooksArray[0].isFieldPresent(sfHookName));
|
||||
}
|
||||
}
|
||||
|
||||
// Install named hook
|
||||
auto jvh = hso(accept_wasm);
|
||||
jvh[jss::HookName] = "41424344";
|
||||
@@ -4675,6 +4787,218 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_emit_failure_callback(FeatureBitset features)
|
||||
{
|
||||
testcase("Test emit failure callback");
|
||||
using namespace jtx;
|
||||
|
||||
// An emitted txn that is never applied expires into a ttEMIT_FAILURE
|
||||
// cleanup pseudo, which removes its ltEMITTED_TXN entry and then
|
||||
// runs the emitting Hook's cbak. Before fix20261005 the callback
|
||||
// re-read that (already erased) entry and dereferenced null, taking
|
||||
// the process down, so only the fixed behaviour can be run here.
|
||||
Env env{*this, features | fix20261005};
|
||||
|
||||
auto const alice = Account{"alice"};
|
||||
env.fund(XRP(10000), alice);
|
||||
env.close();
|
||||
|
||||
TestHook hook_wasm = wasm[R"[test.hook](
|
||||
#include <stdint.h>
|
||||
extern int32_t _g (uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t emit (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
extern int64_t hook_account (uint32_t write_ptr, uint32_t write_len);
|
||||
extern int64_t etxn_reserve (uint32_t);
|
||||
extern int64_t etxn_fee_base (uint32_t read_ptr, uint32_t read_len);
|
||||
extern int64_t etxn_details (uint32_t write_ptr, uint32_t write_len);
|
||||
extern int64_t ledger_seq (void);
|
||||
extern int64_t otxn_id (uint32_t, uint32_t, uint32_t);
|
||||
extern int64_t otxn_type (void);
|
||||
extern int64_t otxn_field (uint32_t write_ptr, uint32_t write_len, uint32_t field_id);
|
||||
extern int64_t state_set (uint32_t, uint32_t, uint32_t, uint32_t);
|
||||
#define SBUF(x) (uint32_t)(x), sizeof(x)
|
||||
#define ASSERT(x)\
|
||||
if (!(x))\
|
||||
rollback((uint32_t)#x, sizeof(#x), __LINE__);
|
||||
#define DOESNT_EXIST (-5)
|
||||
#define ttINVOKE 99
|
||||
#define sfAccount ((8U << 16U) + 1U)
|
||||
#define sfLedgerSequence ((2U << 16U) + 6U)
|
||||
|
||||
uint8_t key[32] = { [31] = 1U };
|
||||
|
||||
// an Invoke from the hook account, emitted with a callback
|
||||
uint8_t txn[229] =
|
||||
{
|
||||
/* size, upto, field name */
|
||||
/* 3, 0, tt = Invoke */ 0x12U, 0x00U, 0x63U,
|
||||
/* 5, 3, flags */ 0x22U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
/* 5, 8, sequence */ 0x24U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
/* 6, 13, firstledgersequence */ 0x20U, 0x1AU, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
/* 6, 19, lastledgersequence */ 0x20U, 0x1BU, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
/* 9, 25, fee */ 0x68U, 0x40U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
/* 35, 34, signingpubkey */ 0x73U, 0x21U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
/* 22, 69, account */ 0x81U, 0x14U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
/* 138, 91, emit details */
|
||||
/* 0, 229, */
|
||||
};
|
||||
#define FLS_OUT (txn + 15U)
|
||||
#define LLS_OUT (txn + 21U)
|
||||
#define FEE_OUT (txn + 26U)
|
||||
#define ACCOUNT_OUT (txn + 71U)
|
||||
#define EMIT_OUT (txn + 91U)
|
||||
#define SET_UINT32(ptr, value)\
|
||||
{\
|
||||
uint32_t v = (value);\
|
||||
(ptr)[0] = v >> 24U; (ptr)[1] = v >> 16U; (ptr)[2] = v >> 8U; (ptr)[3] = v;\
|
||||
}
|
||||
#define SET_NATIVE_AMOUNT(ptr, amount)\
|
||||
{\
|
||||
uint64_t a = (amount);\
|
||||
(ptr)[0] = 0x40U + ((a >> 56U) & 0x3FU);\
|
||||
(ptr)[1] = a >> 48U; (ptr)[2] = a >> 40U; (ptr)[3] = a >> 32U;\
|
||||
(ptr)[4] = a >> 24U; (ptr)[5] = a >> 16U; (ptr)[6] = a >> 8U; (ptr)[7] = a;\
|
||||
}
|
||||
#define ACC_EQ(a, b)\
|
||||
(*(uint64_t*)(a) == *(uint64_t*)(b) &&\
|
||||
*(uint64_t*)((a) + 8U) == *(uint64_t*)((b) + 8U) &&\
|
||||
*(uint32_t*)((a) + 16U) == *(uint32_t*)((b) + 16U))
|
||||
|
||||
int64_t cbak(uint32_t r)
|
||||
{
|
||||
_g(1,1);
|
||||
|
||||
// 1 = the emitted txn failed (ttEMIT_FAILURE)
|
||||
ASSERT(r == 1);
|
||||
|
||||
// the otxn is the original emitted txn, not the pseudo
|
||||
ASSERT(otxn_type() == ttINVOKE);
|
||||
|
||||
uint8_t me[20];
|
||||
ASSERT(hook_account(SBUF(me)) == 20);
|
||||
uint8_t acc[20];
|
||||
ASSERT(otxn_field(SBUF(acc), sfAccount) == 20);
|
||||
ASSERT(ACC_EQ(acc, me));
|
||||
|
||||
// carried by the pseudo but not by the emitted txn
|
||||
ASSERT(otxn_field(0, 0, sfLedgerSequence) == DOESNT_EXIST);
|
||||
|
||||
// record the id of the emitted txn that failed
|
||||
uint8_t id[32];
|
||||
ASSERT(otxn_id(SBUF(id), 0) == 32);
|
||||
ASSERT(state_set(SBUF(id), SBUF(key)) == 32);
|
||||
|
||||
return accept(0,0,0);
|
||||
}
|
||||
|
||||
int64_t hook(uint32_t r)
|
||||
{
|
||||
_g(1,1);
|
||||
|
||||
ASSERT(etxn_reserve(1) == 1);
|
||||
|
||||
// FirstLedgerSequence is the current ledger, so by the time
|
||||
// the emission directory is processed for the next ledger it
|
||||
// has already passed: the txn is never applied and expires
|
||||
// once LastLedgerSequence is behind the ledger
|
||||
uint32_t seq = (uint32_t)ledger_seq();
|
||||
SET_UINT32(FLS_OUT, seq);
|
||||
SET_UINT32(LLS_OUT, seq + 1);
|
||||
ASSERT(hook_account(ACCOUNT_OUT, 20) == 20);
|
||||
ASSERT(etxn_details(EMIT_OUT, 138U) == 138);
|
||||
int64_t fee = etxn_fee_base(SBUF(txn));
|
||||
ASSERT(fee > 0);
|
||||
SET_NATIVE_AMOUNT(FEE_OUT, fee);
|
||||
|
||||
uint8_t emithash[32];
|
||||
ASSERT(emit(SBUF(emithash), SBUF(txn)) == 32);
|
||||
|
||||
return accept(0,0,0);
|
||||
}
|
||||
)[test.hook]"];
|
||||
|
||||
HASH_WASM(hook);
|
||||
|
||||
env(ripple::test::jtx::hook(alice, {{hso(hook_wasm, overrideFlag)}}, 0),
|
||||
M("set emit failure callback"),
|
||||
HSFEE);
|
||||
env.close();
|
||||
|
||||
env(invoke::invoke(alice),
|
||||
M("emit failure callback: emit"),
|
||||
fee(XRP(1)),
|
||||
ter(tesSUCCESS));
|
||||
|
||||
// the emitted txn, from the ltEMITTED_TXN this created
|
||||
std::optional<uint256> emittedID;
|
||||
{
|
||||
auto const meta = env.meta(); // closes the ledger
|
||||
BEAST_REQUIRE(meta);
|
||||
for (auto const& node : meta->getFieldArray(sfAffectedNodes))
|
||||
{
|
||||
if (node.getFName() != sfCreatedNode ||
|
||||
node.getFieldU16(sfLedgerEntryType) != ltEMITTED_TXN)
|
||||
continue;
|
||||
auto const& nf = const_cast<ripple::STObject&>(node)
|
||||
.getField(sfNewFields)
|
||||
.downcast<STObject>();
|
||||
auto const& et = const_cast<ripple::STObject&>(nf)
|
||||
.getField(sfEmittedTxn)
|
||||
.downcast<STObject>();
|
||||
Serializer s;
|
||||
et.add(s);
|
||||
SerialIter sit(s.slice());
|
||||
emittedID = STTx(sit).getTransactionID();
|
||||
}
|
||||
}
|
||||
BEAST_REQUIRE(emittedID);
|
||||
auto const emittedKeylet = keylet::emittedTxn(*emittedID);
|
||||
BEAST_REQUIRE(env.le(emittedKeylet));
|
||||
|
||||
// close until the cleanup pseudo has been applied
|
||||
std::shared_ptr<STObject const> failureMeta;
|
||||
for (int i = 0; i < 5 && !failureMeta; ++i)
|
||||
{
|
||||
env.close();
|
||||
for (auto const& [tx, meta] : env.closed()->txs)
|
||||
{
|
||||
// the emitted txn itself never makes it into a ledger
|
||||
BEAST_EXPECT(tx->getTransactionID() != *emittedID);
|
||||
if (tx->getTxnType() == ttEMIT_FAILURE &&
|
||||
tx->getFieldH256(sfTransactionHash) == *emittedID)
|
||||
failureMeta = meta;
|
||||
}
|
||||
}
|
||||
BEAST_REQUIRE(failureMeta);
|
||||
|
||||
// the cleanup removed the entry and its directory slot
|
||||
BEAST_EXPECT(!env.le(emittedKeylet));
|
||||
BEAST_EXPECT(!env.le(keylet::emittedDir()));
|
||||
|
||||
// the callback ran to accept() with the original emitted txn
|
||||
BEAST_REQUIRE(failureMeta->isFieldPresent(sfHookExecutions));
|
||||
auto const hookExecutions =
|
||||
failureMeta->getFieldArray(sfHookExecutions);
|
||||
BEAST_REQUIRE(hookExecutions.size() == 1);
|
||||
BEAST_EXPECT(hookExecutions[0].getFieldH256(sfHookHash) == hook_hash);
|
||||
BEAST_EXPECT(
|
||||
hookExecutions[0].getAccountID(sfHookAccount) == alice.id());
|
||||
BEAST_EXPECT(hookExecutions[0].getFieldU64(sfHookReturnCode) == 0);
|
||||
BEAST_EXPECT(hookExecutions[0].getFieldU8(sfHookResult) == 3);
|
||||
|
||||
uint256 key;
|
||||
key.data()[31] = 1;
|
||||
auto const state =
|
||||
env.le(keylet::hookState(alice.id(), key, uint256{beast::zero}));
|
||||
BEAST_REQUIRE(state);
|
||||
BEAST_EXPECT(
|
||||
state->getFieldVL(sfHookStateData) ==
|
||||
Blob(emittedID->begin(), emittedID->end()));
|
||||
}
|
||||
|
||||
void
|
||||
test_etxn_details(FeatureBitset features)
|
||||
{
|
||||
@@ -16192,6 +16516,7 @@ public:
|
||||
testGuards(features);
|
||||
|
||||
test_emit(features); //
|
||||
test_emit_failure_callback(features);
|
||||
test_prepare(features);
|
||||
// test_etxn_burden(features); // tested above
|
||||
// test_etxn_generation(features); // tested above
|
||||
|
||||
@@ -35007,6 +35007,310 @@ std::map<std::string, std::vector<uint8_t>> wasm = {
|
||||
0x65U, 0x64U, 0x2EU, 0x00U,
|
||||
}},
|
||||
|
||||
{R"[test.hook](
|
||||
#include <stdint.h>
|
||||
extern int32_t _g (uint32_t id, uint32_t maxiter);
|
||||
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t rollback (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
|
||||
extern int64_t emit (uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len);
|
||||
extern int64_t hook_account (uint32_t write_ptr, uint32_t write_len);
|
||||
extern int64_t etxn_reserve (uint32_t);
|
||||
extern int64_t etxn_fee_base (uint32_t read_ptr, uint32_t read_len);
|
||||
extern int64_t etxn_details (uint32_t write_ptr, uint32_t write_len);
|
||||
extern int64_t ledger_seq (void);
|
||||
extern int64_t otxn_id (uint32_t, uint32_t, uint32_t);
|
||||
extern int64_t otxn_type (void);
|
||||
extern int64_t otxn_field (uint32_t write_ptr, uint32_t write_len, uint32_t field_id);
|
||||
extern int64_t state_set (uint32_t, uint32_t, uint32_t, uint32_t);
|
||||
#define SBUF(x) (uint32_t)(x), sizeof(x)
|
||||
#define ASSERT(x)\
|
||||
if (!(x))\
|
||||
rollback((uint32_t)#x, sizeof(#x), __LINE__);
|
||||
#define DOESNT_EXIST (-5)
|
||||
#define ttINVOKE 99
|
||||
#define sfAccount ((8U << 16U) + 1U)
|
||||
#define sfLedgerSequence ((2U << 16U) + 6U)
|
||||
|
||||
uint8_t key[32] = { [31] = 1U };
|
||||
|
||||
// an Invoke from the hook account, emitted with a callback
|
||||
uint8_t txn[229] =
|
||||
{
|
||||
/* size, upto, field name */
|
||||
/* 3, 0, tt = Invoke */ 0x12U, 0x00U, 0x63U,
|
||||
/* 5, 3, flags */ 0x22U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
/* 5, 8, sequence */ 0x24U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
/* 6, 13, firstledgersequence */ 0x20U, 0x1AU, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
/* 6, 19, lastledgersequence */ 0x20U, 0x1BU, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
/* 9, 25, fee */ 0x68U, 0x40U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
/* 35, 34, signingpubkey */ 0x73U, 0x21U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
/* 22, 69, account */ 0x81U, 0x14U, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
/* 138, 91, emit details */
|
||||
/* 0, 229, */
|
||||
};
|
||||
#define FLS_OUT (txn + 15U)
|
||||
#define LLS_OUT (txn + 21U)
|
||||
#define FEE_OUT (txn + 26U)
|
||||
#define ACCOUNT_OUT (txn + 71U)
|
||||
#define EMIT_OUT (txn + 91U)
|
||||
#define SET_UINT32(ptr, value)\
|
||||
{\
|
||||
uint32_t v = (value);\
|
||||
(ptr)[0] = v >> 24U; (ptr)[1] = v >> 16U; (ptr)[2] = v >> 8U; (ptr)[3] = v;\
|
||||
}
|
||||
#define SET_NATIVE_AMOUNT(ptr, amount)\
|
||||
{\
|
||||
uint64_t a = (amount);\
|
||||
(ptr)[0] = 0x40U + ((a >> 56U) & 0x3FU);\
|
||||
(ptr)[1] = a >> 48U; (ptr)[2] = a >> 40U; (ptr)[3] = a >> 32U;\
|
||||
(ptr)[4] = a >> 24U; (ptr)[5] = a >> 16U; (ptr)[6] = a >> 8U; (ptr)[7] = a;\
|
||||
}
|
||||
#define ACC_EQ(a, b)\
|
||||
(*(uint64_t*)(a) == *(uint64_t*)(b) &&\
|
||||
*(uint64_t*)((a) + 8U) == *(uint64_t*)((b) + 8U) &&\
|
||||
*(uint32_t*)((a) + 16U) == *(uint32_t*)((b) + 16U))
|
||||
|
||||
int64_t cbak(uint32_t r)
|
||||
{
|
||||
_g(1,1);
|
||||
|
||||
// 1 = the emitted txn failed (ttEMIT_FAILURE)
|
||||
ASSERT(r == 1);
|
||||
|
||||
// the otxn is the original emitted txn, not the pseudo
|
||||
ASSERT(otxn_type() == ttINVOKE);
|
||||
|
||||
uint8_t me[20];
|
||||
ASSERT(hook_account(SBUF(me)) == 20);
|
||||
uint8_t acc[20];
|
||||
ASSERT(otxn_field(SBUF(acc), sfAccount) == 20);
|
||||
ASSERT(ACC_EQ(acc, me));
|
||||
|
||||
// carried by the pseudo but not by the emitted txn
|
||||
ASSERT(otxn_field(0, 0, sfLedgerSequence) == DOESNT_EXIST);
|
||||
|
||||
// record the id of the emitted txn that failed
|
||||
uint8_t id[32];
|
||||
ASSERT(otxn_id(SBUF(id), 0) == 32);
|
||||
ASSERT(state_set(SBUF(id), SBUF(key)) == 32);
|
||||
|
||||
return accept(0,0,0);
|
||||
}
|
||||
|
||||
int64_t hook(uint32_t r)
|
||||
{
|
||||
_g(1,1);
|
||||
|
||||
ASSERT(etxn_reserve(1) == 1);
|
||||
|
||||
// FirstLedgerSequence is the current ledger, so by the time
|
||||
// the emission directory is processed for the next ledger it
|
||||
// has already passed: the txn is never applied and expires
|
||||
// once LastLedgerSequence is behind the ledger
|
||||
uint32_t seq = (uint32_t)ledger_seq();
|
||||
SET_UINT32(FLS_OUT, seq);
|
||||
SET_UINT32(LLS_OUT, seq + 1);
|
||||
ASSERT(hook_account(ACCOUNT_OUT, 20) == 20);
|
||||
ASSERT(etxn_details(EMIT_OUT, 138U) == 138);
|
||||
int64_t fee = etxn_fee_base(SBUF(txn));
|
||||
ASSERT(fee > 0);
|
||||
SET_NATIVE_AMOUNT(FEE_OUT, fee);
|
||||
|
||||
uint8_t emithash[32];
|
||||
ASSERT(emit(SBUF(emithash), SBUF(txn)) == 32);
|
||||
|
||||
return accept(0,0,0);
|
||||
}
|
||||
)[test.hook]",
|
||||
{
|
||||
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x2CU,
|
||||
0x07U, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7FU, 0x60U, 0x03U, 0x7FU,
|
||||
0x7FU, 0x7EU, 0x01U, 0x7EU, 0x60U, 0x00U, 0x01U, 0x7EU, 0x60U, 0x02U,
|
||||
0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7FU, 0x01U,
|
||||
0x7EU, 0x60U, 0x04U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U,
|
||||
0x01U, 0x7FU, 0x01U, 0x7EU, 0x02U, 0xCEU, 0x01U, 0x0DU, 0x03U, 0x65U,
|
||||
0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU,
|
||||
0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U, 0x61U, 0x63U, 0x6BU,
|
||||
0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x09U, 0x6FU, 0x74U, 0x78U,
|
||||
0x6EU, 0x5FU, 0x74U, 0x79U, 0x70U, 0x65U, 0x00U, 0x02U, 0x03U, 0x65U,
|
||||
0x6EU, 0x76U, 0x0CU, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x61U, 0x63U,
|
||||
0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x00U, 0x03U, 0x03U, 0x65U, 0x6EU,
|
||||
0x76U, 0x0AU, 0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x66U, 0x69U, 0x65U,
|
||||
0x6CU, 0x64U, 0x00U, 0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x07U, 0x6FU,
|
||||
0x74U, 0x78U, 0x6EU, 0x5FU, 0x69U, 0x64U, 0x00U, 0x04U, 0x03U, 0x65U,
|
||||
0x6EU, 0x76U, 0x09U, 0x73U, 0x74U, 0x61U, 0x74U, 0x65U, 0x5FU, 0x73U,
|
||||
0x65U, 0x74U, 0x00U, 0x05U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x06U, 0x61U,
|
||||
0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU,
|
||||
0x76U, 0x0CU, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x72U, 0x65U, 0x73U,
|
||||
0x65U, 0x72U, 0x76U, 0x65U, 0x00U, 0x06U, 0x03U, 0x65U, 0x6EU, 0x76U,
|
||||
0x0AU, 0x6CU, 0x65U, 0x64U, 0x67U, 0x65U, 0x72U, 0x5FU, 0x73U, 0x65U,
|
||||
0x71U, 0x00U, 0x02U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x65U, 0x74U,
|
||||
0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, 0x61U, 0x69U, 0x6CU, 0x73U,
|
||||
0x00U, 0x03U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0DU, 0x65U, 0x74U, 0x78U,
|
||||
0x6EU, 0x5FU, 0x66U, 0x65U, 0x65U, 0x5FU, 0x62U, 0x61U, 0x73U, 0x65U,
|
||||
0x00U, 0x03U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x04U, 0x65U, 0x6DU, 0x69U,
|
||||
0x74U, 0x00U, 0x05U, 0x03U, 0x03U, 0x02U, 0x06U, 0x06U, 0x05U, 0x03U,
|
||||
0x01U, 0x00U, 0x02U, 0x06U, 0x08U, 0x01U, 0x7FU, 0x01U, 0x41U, 0x90U,
|
||||
0x8DU, 0x04U, 0x0BU, 0x07U, 0x0FU, 0x02U, 0x04U, 0x63U, 0x62U, 0x61U,
|
||||
0x6BU, 0x00U, 0x0DU, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x0EU,
|
||||
0x0AU, 0x85U, 0x87U, 0x00U, 0x02U, 0x88U, 0x83U, 0x00U, 0x02U, 0x01U,
|
||||
0x7FU, 0x01U, 0x7EU, 0x23U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U,
|
||||
0xE0U, 0x00U, 0x6BU, 0x22U, 0x01U, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U,
|
||||
0x00U, 0x41U, 0x01U, 0x41U, 0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U,
|
||||
0x00U, 0x1AU, 0x02U, 0x40U, 0x20U, 0x00U, 0x41U, 0x01U, 0x46U, 0x0DU,
|
||||
0x00U, 0x41U, 0xD5U, 0x89U, 0x80U, 0x80U, 0x00U, 0x41U, 0x07U, 0x42U,
|
||||
0xC5U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU,
|
||||
0x02U, 0x40U, 0x10U, 0x82U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0xE3U,
|
||||
0x00U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xB3U, 0x88U, 0x80U, 0x80U, 0x00U,
|
||||
0x41U, 0x18U, 0x42U, 0xC8U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U,
|
||||
0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0xC0U, 0x00U,
|
||||
0x6AU, 0x41U, 0x14U, 0x10U, 0x83U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U,
|
||||
0x14U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xBCU, 0x8AU, 0x80U, 0x80U, 0x00U,
|
||||
0x41U, 0x1DU, 0x42U, 0xCBU, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U,
|
||||
0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0x20U, 0x6AU,
|
||||
0x41U, 0x14U, 0x41U, 0x81U, 0x80U, 0x20U, 0x10U, 0x84U, 0x80U, 0x80U,
|
||||
0x80U, 0x00U, 0x42U, 0x14U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xF1U, 0x89U,
|
||||
0x80U, 0x80U, 0x00U, 0x41U, 0x27U, 0x42U, 0xCDU, 0x00U, 0x10U, 0x81U,
|
||||
0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x02U, 0x40U,
|
||||
0x20U, 0x01U, 0x29U, 0x03U, 0x20U, 0x20U, 0x01U, 0x29U, 0x03U, 0x40U,
|
||||
0x52U, 0x0DU, 0x00U, 0x20U, 0x01U, 0x29U, 0x03U, 0x28U, 0x20U, 0x01U,
|
||||
0x29U, 0x03U, 0x48U, 0x52U, 0x0DU, 0x00U, 0x20U, 0x01U, 0x28U, 0x02U,
|
||||
0x30U, 0x20U, 0x01U, 0x28U, 0x02U, 0x50U, 0x46U, 0x0DU, 0x01U, 0x0BU,
|
||||
0x41U, 0xE1U, 0x8AU, 0x80U, 0x80U, 0x00U, 0x41U, 0x10U, 0x42U, 0xCEU,
|
||||
0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U,
|
||||
0x40U, 0x41U, 0x00U, 0x41U, 0x00U, 0x41U, 0x86U, 0x80U, 0x08U, 0x10U,
|
||||
0x84U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x7BU, 0x51U, 0x0DU, 0x00U,
|
||||
0x41U, 0x80U, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x33U, 0x42U, 0xD1U,
|
||||
0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U,
|
||||
0x40U, 0x20U, 0x01U, 0x41U, 0x20U, 0x41U, 0x00U, 0x10U, 0x85U, 0x80U,
|
||||
0x80U, 0x80U, 0x00U, 0x42U, 0x20U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xEFU,
|
||||
0x88U, 0x80U, 0x80U, 0x00U, 0x41U, 0x1BU, 0x42U, 0xD5U, 0x00U, 0x10U,
|
||||
0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x02U, 0x40U, 0x20U,
|
||||
0x01U, 0x41U, 0x20U, 0x41U, 0x80U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U,
|
||||
0x20U, 0x10U, 0x86U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x20U, 0x51U,
|
||||
0x0DU, 0x00U, 0x41U, 0x8AU, 0x89U, 0x80U, 0x80U, 0x00U, 0x41U, 0x25U,
|
||||
0x42U, 0xD6U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU,
|
||||
0x0BU, 0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x87U, 0x80U,
|
||||
0x80U, 0x80U, 0x00U, 0x21U, 0x02U, 0x20U, 0x01U, 0x41U, 0xE0U, 0x00U,
|
||||
0x6AU, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x20U, 0x02U, 0x0BU,
|
||||
0xF6U, 0x83U, 0x00U, 0x03U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x01U, 0x7FU,
|
||||
0x23U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0x20U, 0x6BU, 0x22U,
|
||||
0x01U, 0x24U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x41U, 0x01U, 0x41U,
|
||||
0x01U, 0x10U, 0x80U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x02U, 0x40U,
|
||||
0x41U, 0x01U, 0x10U, 0x88U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x01U,
|
||||
0x51U, 0x0DU, 0x00U, 0x41U, 0xDCU, 0x89U, 0x80U, 0x80U, 0x00U, 0x41U,
|
||||
0x15U, 0x42U, 0xDFU, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U,
|
||||
0x1AU, 0x0BU, 0x41U, 0x00U, 0x10U, 0x89U, 0x80U, 0x80U, 0x80U, 0x00U,
|
||||
0x22U, 0x02U, 0x3CU, 0x00U, 0xB2U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U,
|
||||
0x00U, 0x20U, 0x02U, 0x42U, 0x08U, 0x88U, 0x3CU, 0x00U, 0xB1U, 0x8BU,
|
||||
0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U, 0x42U, 0x10U, 0x88U,
|
||||
0x3CU, 0x00U, 0xB0U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U,
|
||||
0x02U, 0x42U, 0x18U, 0x88U, 0x3CU, 0x00U, 0xAFU, 0x8BU, 0x80U, 0x80U,
|
||||
0x00U, 0x41U, 0x00U, 0x20U, 0x02U, 0xA7U, 0x41U, 0x01U, 0x6AU, 0x22U,
|
||||
0x03U, 0x3AU, 0x00U, 0xB8U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U,
|
||||
0x20U, 0x03U, 0x41U, 0x08U, 0x76U, 0x3AU, 0x00U, 0xB7U, 0x8BU, 0x80U,
|
||||
0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x03U, 0x41U, 0x10U, 0x76U, 0x3AU,
|
||||
0x00U, 0xB6U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x03U,
|
||||
0x41U, 0x18U, 0x76U, 0x3AU, 0x00U, 0xB5U, 0x8BU, 0x80U, 0x80U, 0x00U,
|
||||
0x02U, 0x40U, 0x41U, 0xE7U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x14U,
|
||||
0x10U, 0x83U, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x14U, 0x51U, 0x0DU,
|
||||
0x00U, 0x41U, 0x98U, 0x8AU, 0x80U, 0x80U, 0x00U, 0x41U, 0x24U, 0x42U,
|
||||
0xE8U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU,
|
||||
0x02U, 0x40U, 0x41U, 0xFBU, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x8AU,
|
||||
0x01U, 0x10U, 0x8AU, 0x80U, 0x80U, 0x80U, 0x00U, 0x42U, 0x8AU, 0x01U,
|
||||
0x51U, 0x0DU, 0x00U, 0x41U, 0xCBU, 0x88U, 0x80U, 0x80U, 0x00U, 0x41U,
|
||||
0x24U, 0x42U, 0xE9U, 0x00U, 0x10U, 0x81U, 0x80U, 0x80U, 0x80U, 0x00U,
|
||||
0x1AU, 0x0BU, 0x02U, 0x40U, 0x41U, 0xA0U, 0x8BU, 0x80U, 0x80U, 0x00U,
|
||||
0x41U, 0xE5U, 0x01U, 0x10U, 0x8BU, 0x80U, 0x80U, 0x80U, 0x00U, 0x22U,
|
||||
0x02U, 0x42U, 0x00U, 0x55U, 0x0DU, 0x00U, 0x41U, 0xD9U, 0x8AU, 0x80U,
|
||||
0x80U, 0x00U, 0x41U, 0x08U, 0x42U, 0xEBU, 0x00U, 0x10U, 0x81U, 0x80U,
|
||||
0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x41U, 0x00U, 0x20U, 0x02U, 0x3CU,
|
||||
0x00U, 0xC1U, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U,
|
||||
0x42U, 0x08U, 0x88U, 0x3CU, 0x00U, 0xC0U, 0x8BU, 0x80U, 0x80U, 0x00U,
|
||||
0x41U, 0x00U, 0x20U, 0x02U, 0x42U, 0x10U, 0x88U, 0x3CU, 0x00U, 0xBFU,
|
||||
0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U, 0x42U, 0x18U,
|
||||
0x88U, 0x3CU, 0x00U, 0xBEU, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U,
|
||||
0x20U, 0x02U, 0x42U, 0x20U, 0x88U, 0x3CU, 0x00U, 0xBDU, 0x8BU, 0x80U,
|
||||
0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U, 0x42U, 0x28U, 0x88U, 0x3CU,
|
||||
0x00U, 0xBCU, 0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0x00U, 0x20U, 0x02U,
|
||||
0x42U, 0x30U, 0x88U, 0x3CU, 0x00U, 0xBBU, 0x8BU, 0x80U, 0x80U, 0x00U,
|
||||
0x41U, 0x00U, 0x20U, 0x02U, 0x42U, 0x38U, 0x88U, 0xA7U, 0x41U, 0x3FU,
|
||||
0x71U, 0x41U, 0xC0U, 0x00U, 0x72U, 0x3AU, 0x00U, 0xBAU, 0x8BU, 0x80U,
|
||||
0x80U, 0x00U, 0x02U, 0x40U, 0x20U, 0x01U, 0x41U, 0x20U, 0x41U, 0xA0U,
|
||||
0x8BU, 0x80U, 0x80U, 0x00U, 0x41U, 0xE5U, 0x01U, 0x10U, 0x8CU, 0x80U,
|
||||
0x80U, 0x80U, 0x00U, 0x42U, 0x20U, 0x51U, 0x0DU, 0x00U, 0x41U, 0xAFU,
|
||||
0x89U, 0x80U, 0x80U, 0x00U, 0x41U, 0x26U, 0x42U, 0xEFU, 0x00U, 0x10U,
|
||||
0x81U, 0x80U, 0x80U, 0x80U, 0x00U, 0x1AU, 0x0BU, 0x41U, 0x00U, 0x41U,
|
||||
0x00U, 0x42U, 0x00U, 0x10U, 0x87U, 0x80U, 0x80U, 0x80U, 0x00U, 0x21U,
|
||||
0x02U, 0x20U, 0x01U, 0x41U, 0x20U, 0x6AU, 0x24U, 0x80U, 0x80U, 0x80U,
|
||||
0x80U, 0x00U, 0x20U, 0x02U, 0x0BU, 0x0BU, 0x85U, 0x05U, 0x02U, 0x00U,
|
||||
0x41U, 0x80U, 0x08U, 0x0BU, 0xF1U, 0x02U, 0x6FU, 0x74U, 0x78U, 0x6EU,
|
||||
0x5FU, 0x66U, 0x69U, 0x65U, 0x6CU, 0x64U, 0x28U, 0x30U, 0x2CU, 0x20U,
|
||||
0x30U, 0x2CU, 0x20U, 0x73U, 0x66U, 0x4CU, 0x65U, 0x64U, 0x67U, 0x65U,
|
||||
0x72U, 0x53U, 0x65U, 0x71U, 0x75U, 0x65U, 0x6EU, 0x63U, 0x65U, 0x29U,
|
||||
0x20U, 0x3DU, 0x3DU, 0x20U, 0x44U, 0x4FU, 0x45U, 0x53U, 0x4EU, 0x54U,
|
||||
0x5FU, 0x45U, 0x58U, 0x49U, 0x53U, 0x54U, 0x00U, 0x6FU, 0x74U, 0x78U,
|
||||
0x6EU, 0x5FU, 0x74U, 0x79U, 0x70U, 0x65U, 0x28U, 0x29U, 0x20U, 0x3DU,
|
||||
0x3DU, 0x20U, 0x74U, 0x74U, 0x49U, 0x4EU, 0x56U, 0x4FU, 0x4BU, 0x45U,
|
||||
0x00U, 0x65U, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x64U, 0x65U, 0x74U, 0x61U,
|
||||
0x69U, 0x6CU, 0x73U, 0x28U, 0x45U, 0x4DU, 0x49U, 0x54U, 0x5FU, 0x4FU,
|
||||
0x55U, 0x54U, 0x2CU, 0x20U, 0x31U, 0x33U, 0x38U, 0x55U, 0x29U, 0x20U,
|
||||
0x3DU, 0x3DU, 0x20U, 0x31U, 0x33U, 0x38U, 0x00U, 0x6FU, 0x74U, 0x78U,
|
||||
0x6EU, 0x5FU, 0x69U, 0x64U, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U,
|
||||
0x69U, 0x64U, 0x29U, 0x2CU, 0x20U, 0x30U, 0x29U, 0x20U, 0x3DU, 0x3DU,
|
||||
0x20U, 0x33U, 0x32U, 0x00U, 0x73U, 0x74U, 0x61U, 0x74U, 0x65U, 0x5FU,
|
||||
0x73U, 0x65U, 0x74U, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U, 0x69U,
|
||||
0x64U, 0x29U, 0x2CU, 0x20U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U, 0x6BU,
|
||||
0x65U, 0x79U, 0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U,
|
||||
0x00U, 0x65U, 0x6DU, 0x69U, 0x74U, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U,
|
||||
0x28U, 0x65U, 0x6DU, 0x69U, 0x74U, 0x68U, 0x61U, 0x73U, 0x68U, 0x29U,
|
||||
0x2CU, 0x20U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U, 0x74U, 0x78U, 0x6EU,
|
||||
0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U, 0x72U,
|
||||
0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U, 0x00U, 0x65U, 0x74U, 0x78U, 0x6EU,
|
||||
0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x28U, 0x31U,
|
||||
0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U, 0x00U, 0x6FU, 0x74U, 0x78U,
|
||||
0x6EU, 0x5FU, 0x66U, 0x69U, 0x65U, 0x6CU, 0x64U, 0x28U, 0x53U, 0x42U,
|
||||
0x55U, 0x46U, 0x28U, 0x61U, 0x63U, 0x63U, 0x29U, 0x2CU, 0x20U, 0x73U,
|
||||
0x66U, 0x41U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x29U, 0x20U,
|
||||
0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U, 0x00U, 0x68U, 0x6FU, 0x6FU, 0x6BU,
|
||||
0x5FU, 0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x28U, 0x41U,
|
||||
0x43U, 0x43U, 0x4FU, 0x55U, 0x4EU, 0x54U, 0x5FU, 0x4FU, 0x55U, 0x54U,
|
||||
0x2CU, 0x20U, 0x32U, 0x30U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U,
|
||||
0x30U, 0x00U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x5FU, 0x61U, 0x63U, 0x63U,
|
||||
0x6FU, 0x75U, 0x6EU, 0x74U, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U,
|
||||
0x6DU, 0x65U, 0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U,
|
||||
0x00U, 0x66U, 0x65U, 0x65U, 0x20U, 0x3EU, 0x20U, 0x30U, 0x00U, 0x41U,
|
||||
0x43U, 0x43U, 0x5FU, 0x45U, 0x51U, 0x28U, 0x61U, 0x63U, 0x63U, 0x2CU,
|
||||
0x20U, 0x6DU, 0x65U, 0x29U, 0x00U, 0x00U, 0x41U, 0x80U, 0x0BU, 0x0BU,
|
||||
0x85U, 0x02U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x01U, 0x12U, 0x00U, 0x63U, 0x22U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x24U, 0x00U, 0x00U, 0x00U, 0x00U, 0x20U, 0x1AU, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x20U, 0x1BU, 0x00U, 0x00U, 0x00U, 0x00U, 0x68U,
|
||||
0x40U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x73U, 0x21U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x81U, 0x14U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U,
|
||||
0x00U, 0x00U, 0x00U,
|
||||
}},
|
||||
|
||||
};
|
||||
}
|
||||
} // namespace ripple
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/ManifestLedger.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/app/tx/apply.h>
|
||||
#include <xrpld/app/tx/detail/SetManifest.h>
|
||||
@@ -553,9 +554,99 @@ struct SetManifest_test : public beast::unit_test::suite
|
||||
submit(env, makeManifest(master, ephemeral, 1));
|
||||
env.close();
|
||||
|
||||
// Every refusal over a key in use is tef. The transaction is unsigned
|
||||
// and consumes no sequence, so anyone holding it could resubmit it in
|
||||
// every ledger; a tec would charge the master account each time.
|
||||
auto const masterBalance = env.balance(master);
|
||||
auto const otherBalance = env.balance(other);
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(other, ephemeral, 1))) ==
|
||||
"tecDUPLICATE");
|
||||
"tefMANIFEST_KEY_IN_USE");
|
||||
|
||||
// A newer manifest that keeps the current ephemeral key. Every
|
||||
// manifest cache refuses it, so the ledger must too, or it would stay
|
||||
// ahead of what any node believes.
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(master, ephemeral, 2))) ==
|
||||
"tefMANIFEST_KEY_IN_USE");
|
||||
|
||||
// A master key that is already another account's ephemeral key: the
|
||||
// two kinds of manifest keylet share one namespace.
|
||||
auto const third = Account("third", KeyType::ed25519);
|
||||
auto const thirdEph = Account("thirdEph", KeyType::ed25519);
|
||||
env.fund(XRP(1000), third);
|
||||
env.close();
|
||||
|
||||
auto const thirdBalance = env.balance(third);
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(other, third, 1))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(third, thirdEph, 1))) ==
|
||||
"tefMANIFEST_KEY_IN_USE");
|
||||
env.close();
|
||||
|
||||
// Nothing was charged and nothing was written.
|
||||
BEAST_EXPECT(env.balance(master) == masterBalance);
|
||||
BEAST_EXPECT(env.balance(third) == thirdBalance);
|
||||
BEAST_EXPECT(env.balance(other) < otherBalance);
|
||||
if (auto const sle = env.le(keylet::manifest(master.pk()));
|
||||
BEAST_EXPECT(sle))
|
||||
BEAST_EXPECT(sle->getFieldU32(sfSequence) == 1);
|
||||
if (auto const sle = env.le(keylet::manifest(third.pk()));
|
||||
BEAST_EXPECT(sle))
|
||||
BEAST_EXPECT(sle->getAccountID(sfAccount) == other.id());
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(thirdEph.pk())));
|
||||
BEAST_EXPECT(
|
||||
!env.le(keylet::account(third.id()))->isFieldPresent(sfManifestID));
|
||||
}
|
||||
|
||||
void
|
||||
testAccountDelete(FeatureBitset features)
|
||||
{
|
||||
testcase("account delete");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const ephemeral = Account("ephemeral", KeyType::ed25519);
|
||||
auto const plain = Account("plain", KeyType::ed25519);
|
||||
auto const dest = Account("dest");
|
||||
env.fund(XRP(1000), master, plain, dest);
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(
|
||||
engineResult(submit(env, makeManifest(master, ephemeral, 1))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
// Old enough to delete, so the manifest is the only obstacle.
|
||||
incLgrSeqForAccDel(env, master);
|
||||
incLgrSeqForAccDel(env, plain);
|
||||
|
||||
// An on-ledger manifest outlives its account: neither copy is in the
|
||||
// owner directory, and the master copy stays listed in the manifest
|
||||
// directory for good.
|
||||
env(acctdelete(master, dest),
|
||||
fee(drops(env.current()->fees().increment)),
|
||||
ter(tecHAS_OBLIGATIONS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(env.le(keylet::account(master.id())));
|
||||
BEAST_EXPECT(env.le(keylet::manifest(master.pk())));
|
||||
BEAST_EXPECT(env.le(keylet::manifest(ephemeral.pk())));
|
||||
|
||||
// An account of the same age without one goes as usual.
|
||||
env(acctdelete(plain, dest),
|
||||
fee(drops(env.current()->fees().increment)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::account(plain.id())));
|
||||
}
|
||||
|
||||
void
|
||||
@@ -880,6 +971,11 @@ struct SetManifest_test : public beast::unit_test::suite
|
||||
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed(), all) == 4);
|
||||
|
||||
// Read from the ledger and pinned by nothing, all four are held as
|
||||
// ledger lookup cache entries.
|
||||
for (auto const& m : masters)
|
||||
BEAST_EXPECT(cache.getTier(m.pk()) == ManifestSource::ledger);
|
||||
|
||||
// The raw form is what a republishing node needs: the exact bytes the
|
||||
// master key signed, plus the sequence to compare against the ledger.
|
||||
auto const raw = cache.getRawManifest(masters[0].pk());
|
||||
@@ -895,33 +991,237 @@ struct SetManifest_test : public beast::unit_test::suite
|
||||
auto const seq = cache.sequence();
|
||||
cache.pin({masters[0].pk(), masters[1].pk()});
|
||||
BEAST_EXPECT(cache.sequence() > seq);
|
||||
BEAST_EXPECT(cache.getTier(masters[0].pk()) == ManifestSource::list);
|
||||
|
||||
auto const seq2 = cache.sequence();
|
||||
cache.pin({masters[0].pk(), masters[1].pk()});
|
||||
BEAST_EXPECT(cache.sequence() == seq2);
|
||||
|
||||
// Everything is offered: two pinned plus two under the gossip limit.
|
||||
// Only the pinned two are offered. The other two are on the ledger,
|
||||
// where any peer that is not amendment blocked can read them.
|
||||
std::size_t reserved = 0;
|
||||
std::vector<PublicKey> offered;
|
||||
cache.for_each_gossip_manifest(
|
||||
[&](std::size_t n) { reserved = n; },
|
||||
[&](Manifest const& m) { offered.push_back(m.masterKey); });
|
||||
|
||||
BEAST_EXPECT(reserved == 4);
|
||||
BEAST_EXPECT(offered.size() == 4);
|
||||
BEAST_EXPECT(reserved == 2);
|
||||
BEAST_EXPECT(
|
||||
hash_set<PublicKey>(offered.begin(), offered.end()) == all);
|
||||
hash_set<PublicKey>(offered.begin(), offered.end()) ==
|
||||
hash_set<PublicKey>({masters[0].pk(), masters[1].pk()}));
|
||||
|
||||
// A pinned key with no manifest is counted in the reservation but not
|
||||
// offered, since the reservation is only an upper bound.
|
||||
// Unpinned, a master key falls back to the lookup cache. A pinned key
|
||||
// with no manifest is neither offered nor counted.
|
||||
cache.pin({masters[0].pk(), ephemeral.pk()});
|
||||
BEAST_EXPECT(cache.getTier(masters[1].pk()) == ManifestSource::ledger);
|
||||
offered.clear();
|
||||
cache.for_each_gossip_manifest(
|
||||
[&](std::size_t n) { reserved = n; },
|
||||
[&](Manifest const& m) { offered.push_back(m.masterKey); });
|
||||
|
||||
BEAST_EXPECT(reserved == 5);
|
||||
BEAST_EXPECT(offered.size() == 4);
|
||||
BEAST_EXPECT(reserved == 1);
|
||||
BEAST_EXPECT(offered.size() == 1);
|
||||
}
|
||||
|
||||
void
|
||||
testLedgerOutranksGossip(FeatureBitset features)
|
||||
{
|
||||
testcase("ledger outranks gossip");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
|
||||
auto const master = Account("master", KeyType::ed25519);
|
||||
auto const eph1 = Account("eph1", KeyType::ed25519);
|
||||
auto const eph2 = Account("eph2", KeyType::ed25519);
|
||||
env.fund(XRP(1000), master);
|
||||
env.close();
|
||||
|
||||
submit(env, makeManifest(master, eph1, 1));
|
||||
env.close();
|
||||
|
||||
auto const fromPeer = [](std::string const& s) {
|
||||
auto mo = deserializeManifest(s);
|
||||
return std::move(*mo);
|
||||
};
|
||||
auto const gossip = ManifestSource::gossip;
|
||||
|
||||
{
|
||||
// Gossip repeating what the ledger holds is stale, and it is the
|
||||
// ledger's copy that is kept, as a lookup cache entry.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{env.journal, clock};
|
||||
cache.noteValidation(eph1.pk());
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
fromPeer(makeManifest(master, eph1, 1)),
|
||||
gossip,
|
||||
env.closed().get()) == ManifestDisposition::stale);
|
||||
BEAST_EXPECT(cache.getTier(master.pk()) == ManifestSource::ledger);
|
||||
BEAST_EXPECT(cache.getMasterKey(eph1.pk()) == master.pk());
|
||||
}
|
||||
|
||||
submit(
|
||||
env,
|
||||
makeManifest(
|
||||
master, eph1, std::numeric_limits<std::uint32_t>::max()));
|
||||
env.close();
|
||||
|
||||
{
|
||||
// Revoked on-ledger, a master key cannot come back by gossip with
|
||||
// a fresh ephemeral key, even one seen validating, and even in a
|
||||
// cache that never held it.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{env.journal, clock};
|
||||
cache.noteValidation(eph2.pk());
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
fromPeer(makeManifest(master, eph2, 5)),
|
||||
gossip,
|
||||
env.closed().get()) == ManifestDisposition::stale);
|
||||
BEAST_EXPECT(cache.revoked(master.pk()));
|
||||
BEAST_EXPECT(cache.getTier(master.pk()) == ManifestSource::ledger);
|
||||
BEAST_EXPECT(cache.getMasterKey(eph2.pk()) == eph2.pk());
|
||||
}
|
||||
|
||||
{
|
||||
// A cache that took the gossip without a ledger to check it
|
||||
// against learns of the revocation at its next refresh, though it
|
||||
// never misses on the ephemeral key.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{env.journal, clock};
|
||||
cache.noteValidation(eph2.pk());
|
||||
BEAST_EXPECT(
|
||||
cache.applyManifest(
|
||||
fromPeer(makeManifest(master, eph2, 5)), gossip) ==
|
||||
ManifestDisposition::accepted);
|
||||
BEAST_EXPECT(cache.getMasterKey(eph2.pk()) == master.pk());
|
||||
BEAST_EXPECT(cache.applyLedger(*env.closed()) == 1);
|
||||
BEAST_EXPECT(cache.revoked(master.pk()));
|
||||
BEAST_EXPECT(cache.getMasterKey(eph2.pk()) == eph2.pk());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDirectory(FeatureBitset features)
|
||||
{
|
||||
testcase("manifest directory");
|
||||
using namespace jtx;
|
||||
|
||||
Env env{*this, makeConfig(), features};
|
||||
auto const revoke = std::numeric_limits<std::uint32_t>::max();
|
||||
|
||||
std::vector<Account> masters;
|
||||
std::vector<Account> ephs;
|
||||
for (int i = 0; i < 40; ++i)
|
||||
{
|
||||
masters.emplace_back("dm" + std::to_string(i), KeyType::ed25519);
|
||||
ephs.emplace_back("de" + std::to_string(i), KeyType::ed25519);
|
||||
env.fund(XRP(1000), masters.back());
|
||||
}
|
||||
env.close();
|
||||
|
||||
for (int i = 0; i < 40; ++i)
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[i], ephs[i], 1))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
auto const listed = [&]() {
|
||||
std::map<PublicKey, std::uint32_t> out;
|
||||
forEachLedgerManifest(
|
||||
*env.closed(), [&](std::shared_ptr<SLE const> const& sle) {
|
||||
out.emplace(
|
||||
PublicKey{makeSlice(sle->getFieldVL(sfPublicKey))},
|
||||
sle->getFieldU32(sfSequence));
|
||||
});
|
||||
return out;
|
||||
};
|
||||
|
||||
auto const pageOf = [&](Account const& a) {
|
||||
auto const sle = env.le(keylet::manifest(a.pk()));
|
||||
return sle && sle->isFieldPresent(sfOwnerNode)
|
||||
? std::optional<std::uint64_t>{sle->getFieldU64(sfOwnerNode)}
|
||||
: std::nullopt;
|
||||
};
|
||||
|
||||
// Each master object listed once, on the page it records; the
|
||||
// ephemeral copies are not listed.
|
||||
{
|
||||
auto const all = listed();
|
||||
BEAST_EXPECT(all.size() == 40);
|
||||
for (auto const& m : masters)
|
||||
BEAST_EXPECT(all.count(m.pk()) && all.at(m.pk()) == 1);
|
||||
for (auto const& e : ephs)
|
||||
{
|
||||
auto const sle = env.le(keylet::manifest(e.pk()));
|
||||
BEAST_EXPECT(sle && !sle->isFieldPresent(sfOwnerNode));
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::optional<std::uint64_t>> pages;
|
||||
for (auto const& m : masters)
|
||||
{
|
||||
pages.push_back(pageOf(m));
|
||||
BEAST_EXPECT(pages.back());
|
||||
}
|
||||
|
||||
// Rotated to a new ephemeral key...
|
||||
std::vector<Account> rotated;
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
rotated.emplace_back("dr" + std::to_string(i), KeyType::ed25519);
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[i], rotated.back(), 2))) ==
|
||||
"tesSUCCESS");
|
||||
}
|
||||
env.close();
|
||||
|
||||
// ...revoked...
|
||||
for (int i = 10; i < 15; ++i)
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[i], ephs[i], revoke))) ==
|
||||
"tesSUCCESS");
|
||||
env.close();
|
||||
|
||||
// ...or replaced unsuccessfully, which changes nothing.
|
||||
BEAST_EXPECT(
|
||||
engineResult(
|
||||
submit(env, makeManifest(masters[0], rotated[0], 2))) ==
|
||||
"tefPAST_MANIFEST_SEQ");
|
||||
env.close();
|
||||
|
||||
// Still each master key once, at its latest, on the same page.
|
||||
auto const all = listed();
|
||||
BEAST_EXPECT(all.size() == 40);
|
||||
for (int i = 0; i < 40; ++i)
|
||||
{
|
||||
auto const expected = i < 10 ? 2u : i < 15 ? revoke : 1u;
|
||||
BEAST_EXPECT(
|
||||
all.count(masters[i].pk()) &&
|
||||
all.at(masters[i].pk()) == expected);
|
||||
BEAST_EXPECT(pageOf(masters[i]) == pages[i]);
|
||||
}
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(ephs[i].pk())));
|
||||
BEAST_EXPECT(env.le(keylet::manifest(rotated[i].pk())));
|
||||
}
|
||||
for (int i = 10; i < 15; ++i)
|
||||
BEAST_EXPECT(!env.le(keylet::manifest(ephs[i].pk())));
|
||||
|
||||
// Read from the directory alone, with no key to start from.
|
||||
TestStopwatch clock;
|
||||
ManifestCache cache{env.journal, clock};
|
||||
BEAST_EXPECT(cache.applyLedgerDirectory(*env.closed()) == 40);
|
||||
BEAST_EXPECT(cache.getMasterKey(rotated[0].pk()) == masters[0].pk());
|
||||
BEAST_EXPECT(cache.getMasterKey(ephs[0].pk()) == ephs[0].pk());
|
||||
BEAST_EXPECT(cache.getMasterKey(ephs[20].pk()) == masters[20].pk());
|
||||
BEAST_EXPECT(cache.revoked(masters[10].pk()));
|
||||
BEAST_EXPECT(cache.applyLedgerDirectory(*env.closed()) == 0);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -961,10 +1261,13 @@ public:
|
||||
testRetrieval(sa);
|
||||
testSigningKeyRetrieval(sa);
|
||||
testMalformed(sa);
|
||||
testAccountDelete(sa);
|
||||
testEnvelopeRejections(sa);
|
||||
testCanonicalFee(sa);
|
||||
testCorruptLedger(sa);
|
||||
testGossipSelection(sa);
|
||||
testLedgerOutranksGossip(sa);
|
||||
testDirectory(sa);
|
||||
testDisabled(sa);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5055,7 +5055,15 @@ public:
|
||||
run() override
|
||||
{
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{supported_amendments() - featureXahauGenesis};
|
||||
// featureRNG is excluded: makeConfig() runs as a validator whenever
|
||||
// voting settings are given, and a validator puts a ttENTROPY with a
|
||||
// fresh random commitment into every ledger. That makes the
|
||||
// transaction set hash, and so the canonical order in which
|
||||
// different accounts' transactions apply when the ledger closes,
|
||||
// differ from run to run, which breaks tests that assert balances
|
||||
// depending on that order (e.g. unexpected balance change).
|
||||
FeatureBitset const all{
|
||||
supported_amendments() - featureXahauGenesis - featureRNG};
|
||||
testQueueSeq(all);
|
||||
testQueueTicket(all);
|
||||
testTecResult(all);
|
||||
@@ -5082,7 +5090,15 @@ public:
|
||||
runMetaInfo()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
FeatureBitset const all{supported_amendments() - featureXahauGenesis};
|
||||
// featureRNG is excluded: makeConfig() runs as a validator whenever
|
||||
// voting settings are given, and a validator puts a ttENTROPY with a
|
||||
// fresh random commitment into every ledger. That makes the
|
||||
// transaction set hash, and so the canonical order in which
|
||||
// different accounts' transactions apply when the ledger closes,
|
||||
// differ from run to run, which breaks tests that assert balances
|
||||
// depending on that order (e.g. unexpected balance change).
|
||||
FeatureBitset const all{
|
||||
supported_amendments() - featureXahauGenesis - featureRNG};
|
||||
testAcctInQueueButEmpty(all);
|
||||
testRPC(all);
|
||||
testExpirationReplacement(all);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <xrpl/protocol/STAccount.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -4676,11 +4677,12 @@ struct XahauGenesis_test : public beast::unit_test::suite
|
||||
// validate account fields
|
||||
STAmount const postUser = preUser + netReward;
|
||||
bool const has240819 = env.current()->rules().enabled(fix240819);
|
||||
bool const has20261005 = env.current()->rules().enabled(fix20261005);
|
||||
BEAST_EXPECT(expectAccountFields(
|
||||
env,
|
||||
user,
|
||||
preLedger,
|
||||
has240819 ? preLedger : preLedger + 1,
|
||||
(has240819 && !has20261005) ? preLedger : preLedger + 1,
|
||||
has240819 ? (preUser - feesXRP) : postUser,
|
||||
preTime));
|
||||
}
|
||||
@@ -5567,8 +5569,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
|
||||
env.close();
|
||||
|
||||
// calculate rewards
|
||||
bool const hasFix = env.current()->rules().enabled(fix240819) &&
|
||||
env.current()->rules().enabled(fix240911);
|
||||
bool const hasFix = env.current()->rules().enabled(fix240819);
|
||||
STAmount const netReward =
|
||||
rewardUserAmount(*acctSle, preLedger, rateDrops);
|
||||
BEAST_EXPECT(netReward == (hasFix ? XRP(3.329999) : XRP(3.329999)));
|
||||
@@ -5659,8 +5660,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
|
||||
env.close();
|
||||
|
||||
// calculate rewards
|
||||
bool const hasFix = env.current()->rules().enabled(fix240819) &&
|
||||
env.current()->rules().enabled(fix240911);
|
||||
bool const hasFix = env.current()->rules().enabled(fix240819);
|
||||
STAmount const netReward =
|
||||
rewardUserAmount(*acctSle, preLedger, rateDrops);
|
||||
BEAST_EXPECT(netReward == (hasFix ? XRP(3.479999) : XRP(6.663333)));
|
||||
@@ -5678,9 +5678,196 @@ struct XahauGenesis_test : public beast::unit_test::suite
|
||||
postUser == (hasFix ? XRP(2002.479999) : XRP(2005.663333)));
|
||||
}
|
||||
|
||||
void
|
||||
test20261005(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
using namespace std::chrono_literals;
|
||||
testcase("test fix20261005");
|
||||
|
||||
bool const has240819 = features[fix240819];
|
||||
bool const has20261005 = features[fix20261005];
|
||||
bool const nofix = !(has240819 && has20261005);
|
||||
|
||||
Env env{
|
||||
*this, makeNetworkConfig(21337), features - featureXahauGenesis};
|
||||
|
||||
STAmount const feesXRP = XRP(1);
|
||||
|
||||
Account const user = Account("user");
|
||||
env.fund(XRP(200), user);
|
||||
env.close();
|
||||
|
||||
// setup governance
|
||||
Account const alice = Account("alice");
|
||||
Account const bob = Account("bob");
|
||||
Account const carol = Account("carol");
|
||||
Account const david = Account("david");
|
||||
Account const edward = Account("edward");
|
||||
|
||||
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
||||
env.close();
|
||||
|
||||
std::vector<AccountID> initial_members_ids{
|
||||
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
||||
|
||||
setupGov(env, initial_members_ids);
|
||||
|
||||
{
|
||||
std::vector<uint8_t> vote_data{
|
||||
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
||||
|
||||
updateTopic(
|
||||
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
||||
}
|
||||
|
||||
BEAST_EXPECT(hasUNLReport(env) == false);
|
||||
|
||||
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
env(noop(user),
|
||||
fee(env.balance(user) - drops(500'000)),
|
||||
ter(tesSUCCESS));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(user) == drops(500'000));
|
||||
BEAST_EXPECT(env.balance(user) < XRP(1));
|
||||
|
||||
std::uint32_t const drainMarker =
|
||||
env.le(keylet::account(user))->getFieldU32(sfRewardLgrLast);
|
||||
|
||||
for (int i = 0; i < 20; ++i)
|
||||
env.close(10s);
|
||||
|
||||
BEAST_EXPECT(env.balance(user) < XRP(1));
|
||||
|
||||
env(pay(alice, user, XRP(1000)));
|
||||
env.close();
|
||||
|
||||
std::uint32_t const fundMarker =
|
||||
env.le(keylet::account(user))->getFieldU32(sfRewardLgrLast);
|
||||
|
||||
bool const markerFrozen = has240819 && !has20261005;
|
||||
if (markerFrozen)
|
||||
BEAST_EXPECT(fundMarker == drainMarker);
|
||||
else
|
||||
BEAST_EXPECT(fundMarker > drainMarker);
|
||||
|
||||
STAmount const preUser = env.balance(user);
|
||||
|
||||
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
||||
env.close();
|
||||
|
||||
env.close();
|
||||
|
||||
STAmount const postUser = env.balance(user);
|
||||
|
||||
if (nofix)
|
||||
BEAST_EXPECT(postUser > preUser + XRP(1));
|
||||
else
|
||||
BEAST_EXPECT(postUser < preUser + XRP(1));
|
||||
}
|
||||
|
||||
void
|
||||
testRewardAccumulatorOverflow(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
testcase("reward accumulator overflow");
|
||||
|
||||
bool const has20261005 = features[fix20261005];
|
||||
|
||||
Env env{
|
||||
*this, makeNetworkConfig(21337), features - featureXahauGenesis};
|
||||
|
||||
STAmount const feesXRP = XRP(1);
|
||||
|
||||
Account const user = Account("user");
|
||||
env.fund(XRP(10000), user);
|
||||
env.close();
|
||||
|
||||
// setup governance
|
||||
Account const alice = Account("alice");
|
||||
Account const bob = Account("bob");
|
||||
Account const carol = Account("carol");
|
||||
Account const david = Account("david");
|
||||
Account const edward = Account("edward");
|
||||
|
||||
env.fund(XRP(10000), alice, bob, carol, david, edward);
|
||||
env.close();
|
||||
|
||||
std::vector<AccountID> initial_members_ids{
|
||||
alice.id(), bob.id(), carol.id(), david.id(), edward.id()};
|
||||
|
||||
setupGov(env, initial_members_ids);
|
||||
|
||||
{
|
||||
std::vector<uint8_t> vote_data{
|
||||
0x00U, 0x80U, 0xC6U, 0xA4U, 0x7EU, 0x8DU, 0x03U, 0x55U};
|
||||
|
||||
updateTopic(
|
||||
env, alice, bob, carol, david, edward, 'R', 'D', vote_data);
|
||||
}
|
||||
|
||||
// opt in so the account carries the reward fields
|
||||
env(claimReward(user, env.master), fee(feesXRP), ter(tesSUCCESS));
|
||||
env.close();
|
||||
env.close();
|
||||
|
||||
auto const preSle = env.le(keylet::account(user));
|
||||
BEAST_REQUIRE(preSle);
|
||||
BEAST_REQUIRE(preSle->isFieldPresent(sfRewardLgrLast));
|
||||
BEAST_REQUIRE(preSle->isFieldPresent(sfRewardAccumulator));
|
||||
|
||||
std::uint32_t const preMarker = preSle->getFieldU32(sfRewardLgrLast);
|
||||
BEAST_EXPECT(preMarker < env.current()->seq());
|
||||
|
||||
constexpr std::uint64_t maxAccum =
|
||||
std::numeric_limits<std::uint64_t>::max();
|
||||
|
||||
// Push the accumulator to the brink in the open ledger so that any
|
||||
// non-zero bal * lgrElapsed wraps around in finishApply.
|
||||
env.app().openLedger().modify(
|
||||
[&](OpenView& view, beast::Journal) -> bool {
|
||||
auto const sle = view.read(keylet::account(user));
|
||||
if (!sle)
|
||||
return false;
|
||||
auto replacement = std::make_shared<SLE>(*sle);
|
||||
replacement->setFieldU64(sfRewardAccumulator, maxAccum);
|
||||
view.rawReplace(replacement);
|
||||
return true;
|
||||
});
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.le(keylet::account(user))->getFieldU64(sfRewardAccumulator) ==
|
||||
maxAccum);
|
||||
|
||||
// Any balance change on user runs the reward block in finishApply.
|
||||
env(pay(alice, user, XRP(1)), ter(tesSUCCESS));
|
||||
|
||||
// Check the open ledger before close() rebuilds it without the
|
||||
// injected accumulator.
|
||||
auto const postSle = env.le(keylet::account(user));
|
||||
BEAST_REQUIRE(postSle);
|
||||
|
||||
// the overflowing contribution is never added
|
||||
BEAST_EXPECT(postSle->getFieldU64(sfRewardAccumulator) == maxAccum);
|
||||
|
||||
// with the fix the marker advances so the span can't be reused;
|
||||
// without it the marker is left behind
|
||||
if (has20261005)
|
||||
BEAST_EXPECT(
|
||||
postSle->getFieldU32(sfRewardLgrLast) == env.current()->seq());
|
||||
else
|
||||
BEAST_EXPECT(postSle->getFieldU32(sfRewardLgrLast) == preMarker);
|
||||
|
||||
env.close();
|
||||
}
|
||||
|
||||
void
|
||||
testRewardHookWithFeats(FeatureBitset features)
|
||||
{
|
||||
test20261005(features);
|
||||
testRewardAccumulatorOverflow(features);
|
||||
testLastCloseTime(features);
|
||||
testInvalidRate0(features);
|
||||
testInvalidRateGreater1(features);
|
||||
@@ -5723,7 +5910,7 @@ struct XahauGenesis_test : public beast::unit_test::suite
|
||||
testGovernHookWithFeats(sa);
|
||||
testRewardHookWithFeats(sa);
|
||||
testRewardHookWithFeats(sa - fix240819);
|
||||
testRewardHookWithFeats(sa - fix240819 - fix240911);
|
||||
testRewardHookWithFeats(sa - fix20261005);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1052,6 +1052,67 @@ public:
|
||||
BEAST_EXPECT(sim.synchronized());
|
||||
}
|
||||
|
||||
void
|
||||
testForcedInclusion()
|
||||
{
|
||||
using namespace csf;
|
||||
using namespace std::chrono;
|
||||
testcase("forced inclusion");
|
||||
|
||||
// Transaction n * 1000 + k belongs to peer n - 1. Each one is placed
|
||||
// only in a single peer's open set and never relayed, so without
|
||||
// forced inclusion it has one vote in five and stays out.
|
||||
auto const owner = [](Tx const& tx) -> std::optional<PeerID> {
|
||||
if (tx.id() < 1000)
|
||||
return std::nullopt;
|
||||
return PeerID{tx.id() / 1000 - 1};
|
||||
};
|
||||
|
||||
auto const run = [&](bool withHook) {
|
||||
ConsensusParms const parms{};
|
||||
Sim sim;
|
||||
PeerGroup peers = sim.createGroup(5);
|
||||
peers.trustAndConnect(
|
||||
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
|
||||
|
||||
for (Peer* p : peers)
|
||||
{
|
||||
if (withHook)
|
||||
p->txOwner = owner;
|
||||
|
||||
std::uint32_t const base =
|
||||
(static_cast<std::uint32_t>(p->id) + 1) * 1000;
|
||||
p->openTxs.insert(Tx{base + 1});
|
||||
if (p->id == PeerID{4})
|
||||
{
|
||||
// more of its own than it may force
|
||||
p->openTxs.insert(Tx{base + 2});
|
||||
p->openTxs.insert(Tx{base + 3});
|
||||
// one that names another owner, who does not hold it
|
||||
p->openTxs.insert(Tx{1999});
|
||||
}
|
||||
}
|
||||
|
||||
sim.run(1);
|
||||
BEAST_EXPECT(sim.synchronized());
|
||||
return peers[0]->lastClosedLedger.txs();
|
||||
};
|
||||
|
||||
{
|
||||
auto const txs = run(false);
|
||||
BEAST_EXPECT(txs.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto const txs = run(true);
|
||||
// every owner's own transaction, and only the lowest of the
|
||||
// several peer 4 put in its position
|
||||
TxSetType const expected{
|
||||
Tx{1001}, Tx{2001}, Tx{3001}, Tx{4001}, Tx{5001}};
|
||||
BEAST_EXPECTS(txs == expected, to_string(txs));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -1068,6 +1129,7 @@ public:
|
||||
testHubNetwork();
|
||||
testPreferredByBranch();
|
||||
testPauseForLaggards();
|
||||
testForcedInclusion();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -421,6 +421,72 @@ port_wss_admin
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testEarlyManifestSync()
|
||||
{
|
||||
testcase("early_manifest_sync");
|
||||
|
||||
// On unless turned off.
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("");
|
||||
BEAST_EXPECT(c.EARLY_MANIFEST_SYNC);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, bool>> const values{
|
||||
{"0", false}, {"false", false}, {"1", true}, {"TRUE", true}};
|
||||
for (auto const& [value, expected] : values)
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("[early_manifest_sync]\n" + value + "\n");
|
||||
BEAST_EXPECT(c.EARLY_MANIFEST_SYNC == expected);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("[early_manifest_sync]\nsometimes\n");
|
||||
fail();
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
pass();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testUdpHighway()
|
||||
{
|
||||
testcase("udp_highway");
|
||||
|
||||
// The UDP Superhighway is opt-in: off unless turned on.
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("");
|
||||
BEAST_EXPECT(!c.UDP_HIGHWAY);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, bool>> const values{
|
||||
{"0", false}, {"false", false}, {"1", true}, {"TRUE", true}};
|
||||
for (auto const& [value, expected] : values)
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("[udp_highway]\n" + value + "\n");
|
||||
BEAST_EXPECT(c.UDP_HIGHWAY == expected);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Config c;
|
||||
c.loadFromString("[udp_highway]\nsometimes\n");
|
||||
fail();
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
pass();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testNetworkID()
|
||||
{
|
||||
@@ -1056,6 +1122,36 @@ trustthesevalidators.gov
|
||||
}
|
||||
}
|
||||
|
||||
// Standalone forces ledger_history to zero, but [node_db] type=rwdb
|
||||
// (the null node store) sizes its retained-ledger window from it and
|
||||
// SHAMapStoreImp throws on zero. cfg/rippled.cfg ships exactly this
|
||||
// configuration, and jtx never calls Config::setup, so pin it here.
|
||||
void
|
||||
testSetupStandaloneRwdb()
|
||||
{
|
||||
testcase("setup standalone rwdb keeps ledger_history");
|
||||
|
||||
for (std::string const type : {"rwdb", "NuDB"})
|
||||
{
|
||||
detail::RippledCfgGuard const cfg(
|
||||
*this,
|
||||
"testSetupStandaloneRwdb_" + type,
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
"[node_db]\ntype=" + type + "\npath=main\n");
|
||||
Config config;
|
||||
config.setup(
|
||||
cfg.configFile(),
|
||||
/*bQuiet*/ true,
|
||||
/* bSilent */ true,
|
||||
/* bStandalone */ true);
|
||||
BEAST_EXPECT(config.standalone());
|
||||
BEAST_EXPECT(config.nullBackend() == (type == "rwdb"));
|
||||
BEAST_EXPECT(config.LEDGER_HISTORY == (type == "rwdb" ? 256 : 0));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testPort()
|
||||
{
|
||||
@@ -1747,6 +1843,7 @@ r.ripple.com:51235
|
||||
testValidatorsFile();
|
||||
testSetup(false);
|
||||
testSetup(true);
|
||||
testSetupStandaloneRwdb();
|
||||
testPort();
|
||||
testZeroPort();
|
||||
testWhitespace();
|
||||
@@ -1758,6 +1855,8 @@ r.ripple.com:51235
|
||||
testDatabasePinnedValidation();
|
||||
testOverlay();
|
||||
testNetworkID();
|
||||
testEarlyManifestSync();
|
||||
testUdpHighway();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
#include <boost/container/flat_map.hpp>
|
||||
#include <boost/container/flat_set.hpp>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
@@ -257,6 +259,11 @@ struct Peer
|
||||
// Simulation parameters
|
||||
ConsensusParms consensusParms;
|
||||
|
||||
//! Names the peer a transaction belongs to, for Consensus forced
|
||||
//! inclusion (as featureRNG uses for entropy contributions). Unset, it
|
||||
//! behaves exactly like an adaptor without the forcedTxOwner hook.
|
||||
std::function<std::optional<PeerID>(Tx const&)> txOwner;
|
||||
|
||||
//! The collectors to report events to
|
||||
CollectorRefs& collectors;
|
||||
|
||||
@@ -655,6 +662,14 @@ struct Peer
|
||||
return consensusParms;
|
||||
}
|
||||
|
||||
std::optional<PeerID>
|
||||
forcedTxOwner(Tx const& tx, Ledger const&) const
|
||||
{
|
||||
if (!txOwner)
|
||||
return std::nullopt;
|
||||
return txOwner(tx);
|
||||
}
|
||||
|
||||
// Not interested in tracking consensus mode changes for now
|
||||
void
|
||||
onModeChange(ConsensusMode, ConsensusMode)
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <xrpld/app/tx/detail/Transactor.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/InnerObjectFormats.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
@@ -1234,6 +1235,90 @@ class Invariants_test : public beast::unit_test::suite
|
||||
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
|
||||
}
|
||||
|
||||
void
|
||||
testAppLoader()
|
||||
{
|
||||
using namespace test::jtx;
|
||||
static std::string const good = "<html></html>";
|
||||
|
||||
auto insert = [](ApplyContext& ac,
|
||||
AccountID const& owner,
|
||||
uint256 const& key,
|
||||
std::string const& blob,
|
||||
std::uint32_t seq) {
|
||||
auto sle = std::make_shared<SLE>(Keylet{ltAPP_LOADER, key});
|
||||
sle->setAccountID(sfOwner, owner);
|
||||
sle->setFieldVL(sfAppLoader, Slice(blob.data(), blob.size()));
|
||||
sle->setFieldU32(sfAppLoaderSequence, seq);
|
||||
ac.view().insert(sle);
|
||||
auto root = ac.view().peek(keylet::account(owner));
|
||||
root->setFieldU32(sfAppLoaderSequence, seq);
|
||||
ac.view().update(root);
|
||||
return true;
|
||||
};
|
||||
Preclose publish = [](Account const& A1, Account const&, Env& env) {
|
||||
auto jt = noop(A1);
|
||||
jt[sfAppLoader.fieldName] = strHex(good);
|
||||
env(jt, fee(XRP(1)));
|
||||
return bool(env.le(keylet::appLoader(A1.id())));
|
||||
};
|
||||
auto bad = [&](char const* log,
|
||||
Precheck const& pre,
|
||||
std::uint16_t txType = ttACCOUNT_SET,
|
||||
Preclose const& preclose = {}) {
|
||||
doInvariantCheck(
|
||||
{{log}},
|
||||
pre,
|
||||
XRPAmount{},
|
||||
STTx{TxType(txType), [](STObject&) {}},
|
||||
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
|
||||
preclose);
|
||||
};
|
||||
|
||||
testcase << "AppLoader at the wrong key";
|
||||
bad("AppLoader is not keyed at its owner's keylet",
|
||||
[&](Account const& A1, Account const& A2, ApplyContext& ac) {
|
||||
return insert(ac, A1, keylet::appLoader(A2).key, good, 1);
|
||||
});
|
||||
testcase << "AppLoader with invalid blob";
|
||||
bad("AppLoader blob does not validate",
|
||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||
return insert(ac, A1, keylet::appLoader(A1).key, "nope", 1);
|
||||
});
|
||||
testcase << "AppLoader blob over the size limit";
|
||||
bad("AppLoader blob does not validate",
|
||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||
return insert(
|
||||
ac,
|
||||
A1,
|
||||
keylet::appLoader(A1).key,
|
||||
"<html>" + std::string(maxAppLoaderLength, 'x') + "</html>",
|
||||
1);
|
||||
});
|
||||
testcase << "AppLoader sequence differs from its owner's";
|
||||
bad(
|
||||
"AppLoader sequence does not match its owner's",
|
||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::appLoader(A1));
|
||||
sle->setFieldU32(sfAppLoaderSequence, 7);
|
||||
ac.view().update(sle);
|
||||
return true;
|
||||
},
|
||||
ttACCOUNT_SET,
|
||||
publish);
|
||||
testcase << "AppLoader changed by the wrong transaction type";
|
||||
bad(
|
||||
"AppLoader changed by a transaction other than AccountSet or "
|
||||
"AccountDelete",
|
||||
[&](Account const& A1, Account const&, ApplyContext& ac) {
|
||||
auto sle = ac.view().peek(keylet::appLoader(A1));
|
||||
ac.view().erase(sle);
|
||||
return true;
|
||||
},
|
||||
ttPAYMENT,
|
||||
publish);
|
||||
}
|
||||
|
||||
void
|
||||
testLockedBalance()
|
||||
{
|
||||
@@ -1289,6 +1374,7 @@ public:
|
||||
testNFTokenPageInvariants();
|
||||
testPermissionedDomainInvariants();
|
||||
testLockedBalance();
|
||||
testAppLoader();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -66,6 +66,14 @@ public:
|
||||
// Write the batch
|
||||
storeBatch(*backend, batch);
|
||||
|
||||
// Durability on demand: RocksDB drains its write queue and
|
||||
// syncs its WAL; in-memory backends have nothing to wait for;
|
||||
// NuDB commits on its own thread and cannot promise it.
|
||||
backend->store(batch.front());
|
||||
bool const durable = backend->syncDurable();
|
||||
BEAST_EXPECT(durable == (type != "nudb"));
|
||||
backend->sync();
|
||||
|
||||
{
|
||||
// Read it back in
|
||||
Batch copy;
|
||||
|
||||
@@ -0,0 +1,542 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/core/JobQueue.h>
|
||||
#include <xrpld/overlay/detail/OverlayImpl.h>
|
||||
#include <xrpld/overlay/detail/XUSH.h>
|
||||
#include <xrpl/beast/net/IPAddressConversion.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/resource/ResourceManager.h>
|
||||
#include <xrpl/resource/detail/Tuning.h>
|
||||
|
||||
#include <boost/asio/ip/udp.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
// Tests the UDP Superhighway (XUSH) handling in OverlayImpl: inbound datagram
|
||||
// processing, outbound publishing and the XUSHPEER advertisement timer.
|
||||
class OverlayXUSH_test : public beast::unit_test::suite
|
||||
{
|
||||
using udp = boost::asio::ip::udp;
|
||||
|
||||
static std::unique_ptr<Config>
|
||||
highway(std::unique_ptr<Config> cfg)
|
||||
{
|
||||
cfg->UDP_HIGHWAY = true;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static beast::IP::Endpoint
|
||||
ep(std::string const& s)
|
||||
{
|
||||
return beast::IP::Endpoint::from_string(s);
|
||||
}
|
||||
|
||||
static std::set<beast::IP::Endpoint>
|
||||
asSet(std::vector<beast::IP::Endpoint> const& v)
|
||||
{
|
||||
return {v.begin(), v.end()};
|
||||
}
|
||||
|
||||
static Slice
|
||||
slice(Buffer const& b)
|
||||
{
|
||||
return Slice(b.data(), b.size());
|
||||
}
|
||||
|
||||
static std::string
|
||||
str(Buffer const& b)
|
||||
{
|
||||
return std::string(reinterpret_cast<char const*>(b.data()), b.size());
|
||||
}
|
||||
|
||||
static Buffer
|
||||
serialize(STTx const& stx)
|
||||
{
|
||||
Serializer s;
|
||||
stx.add(s);
|
||||
return Buffer(s.data(), s.size());
|
||||
}
|
||||
|
||||
// A pseudo-transaction, which servers never submit
|
||||
static STTx
|
||||
pseudoTx()
|
||||
{
|
||||
return STTx(ttAMENDMENT, [](auto& obj) {
|
||||
obj.setAccountID(sfAccount, AccountID());
|
||||
obj.setFieldH256(sfAmendment, uint256(2));
|
||||
obj.setFieldU32(sfLedgerSequence, 5);
|
||||
});
|
||||
}
|
||||
|
||||
// A local UDP socket that highway datagrams can be sent to
|
||||
struct Sink
|
||||
{
|
||||
boost::asio::io_context io;
|
||||
udp::socket socket;
|
||||
|
||||
explicit Sink(boost::asio::ip::address const& addr)
|
||||
: socket(io, udp::endpoint(addr, 0))
|
||||
{
|
||||
socket.non_blocking(true);
|
||||
}
|
||||
|
||||
beast::IP::Endpoint
|
||||
endpoint() const
|
||||
{
|
||||
auto const local = socket.local_endpoint();
|
||||
return beast::IP::Endpoint(local.address(), local.port());
|
||||
}
|
||||
|
||||
void
|
||||
sendTo(Buffer const& datagram, std::uint16_t port)
|
||||
{
|
||||
socket.send_to(
|
||||
boost::asio::buffer(datagram.data(), datagram.size()),
|
||||
udp::endpoint(socket.local_endpoint().address(), port));
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
receive(std::chrono::milliseconds timeout = std::chrono::seconds(5))
|
||||
{
|
||||
auto const deadline = std::chrono::steady_clock::now() + timeout;
|
||||
std::array<char, 4096> buf;
|
||||
while (std::chrono::steady_clock::now() < deadline)
|
||||
{
|
||||
udp::endpoint from;
|
||||
boost::system::error_code ec;
|
||||
auto const n =
|
||||
socket.receive_from(boost::asio::buffer(buf), from, 0, ec);
|
||||
if (!ec)
|
||||
return std::string(buf.data(), n);
|
||||
if (ec != boost::asio::error::would_block &&
|
||||
ec != boost::asio::error::try_again)
|
||||
return std::nullopt;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Receives datagrams until one of the given type arrives
|
||||
std::optional<std::string>
|
||||
receive(xush::MessageType type)
|
||||
{
|
||||
while (auto d = receive())
|
||||
{
|
||||
if (xush::classify(makeSlice(*d)) == type)
|
||||
return d;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
struct Fixture
|
||||
{
|
||||
jtx::Env env;
|
||||
OverlayImpl& overlay;
|
||||
PeerFinder::Manager& peerFinder;
|
||||
Resource::Manager& resources;
|
||||
|
||||
Fixture(beast::unit_test::suite& suite, bool enabled)
|
||||
: env(suite, enabled ? jtx::envconfig(highway) : jtx::envconfig())
|
||||
, overlay(dynamic_cast<OverlayImpl&>(env.app().overlay()))
|
||||
, peerFinder(overlay.peerFinder())
|
||||
, resources(env.app().getResourceManager())
|
||||
{
|
||||
}
|
||||
|
||||
// Delivers a datagram as if it had been received from `remote`
|
||||
void
|
||||
deliver(Buffer const& datagram, beast::IP::Endpoint const& remote)
|
||||
{
|
||||
overlay.processXUSH(
|
||||
str(datagram), beast::IP::to_asio_endpoint(remote));
|
||||
}
|
||||
|
||||
void
|
||||
deliver(
|
||||
std::vector<Buffer> const& datagrams,
|
||||
beast::IP::Endpoint const& remote)
|
||||
{
|
||||
for (auto const& d : datagrams)
|
||||
deliver(d, remote);
|
||||
}
|
||||
|
||||
// Tracks the resource balance charged to `remote`
|
||||
Resource::Consumer
|
||||
probe(beast::IP::Endpoint const& remote)
|
||||
{
|
||||
return resources.newInboundEndpoint(remote);
|
||||
}
|
||||
|
||||
std::set<beast::IP::Endpoint>
|
||||
targets()
|
||||
{
|
||||
return asSet(peerFinder.highway_targets(1000));
|
||||
}
|
||||
|
||||
// Whether `probe` was charged at least `fee` since `before`. Balances
|
||||
// are normalized by the decay window, so a charge of `fee` raises the
|
||||
// balance by at least fee / decayWindowSeconds.
|
||||
static bool
|
||||
charged(Resource::Consumer& probe, int before, int fee)
|
||||
{
|
||||
return probe.balance() - before >=
|
||||
fee / Resource::decayWindowSeconds;
|
||||
}
|
||||
|
||||
bool
|
||||
isBad(uint256 const& txid)
|
||||
{
|
||||
env.app().getJobQueue().rendezvous();
|
||||
return env.app().getHashRouter().getFlags(txid) & SF_BAD;
|
||||
}
|
||||
};
|
||||
|
||||
void
|
||||
testDisabled()
|
||||
{
|
||||
testcase("disabled");
|
||||
using namespace jtx;
|
||||
Fixture f(*this, false);
|
||||
auto const remote = ep("203.0.113.1:51235");
|
||||
|
||||
// Nothing is learned or sent while the highway is disabled
|
||||
f.deliver(xush::encodePeers({ep("8.8.8.8:51235")}), remote);
|
||||
BEAST_EXPECT(f.targets().empty());
|
||||
|
||||
Sink sink(boost::asio::ip::make_address("127.0.0.1"));
|
||||
f.peerFinder.addFixedPeer("sink", {sink.endpoint()});
|
||||
BEAST_EXPECT(f.targets().empty());
|
||||
|
||||
Account const alice("alice");
|
||||
f.env.fund(XRP(10000), alice);
|
||||
f.env.close();
|
||||
|
||||
Buffer const blob(64);
|
||||
f.overlay.publishTxXUSH(slice(blob), uint256(1));
|
||||
BEAST_EXPECT(!sink.receive(std::chrono::milliseconds(200)));
|
||||
}
|
||||
|
||||
void
|
||||
testPeers()
|
||||
{
|
||||
testcase("XUSHPEER");
|
||||
Fixture f(*this, true);
|
||||
auto const remote = ep("203.0.113.2:51235");
|
||||
auto probe = f.probe(remote);
|
||||
|
||||
BEAST_EXPECT(f.targets().empty());
|
||||
|
||||
f.deliver(
|
||||
xush::encodePeers({ep("8.8.8.8:51235"), ep("1.1.1.1:51235")}),
|
||||
remote);
|
||||
BEAST_EXPECT(
|
||||
f.targets() == asSet({ep("8.8.8.8:51235"), ep("1.1.1.1:51235")}));
|
||||
auto const balance = probe.balance();
|
||||
|
||||
// Unknown datagram types are ignored and cost almost nothing
|
||||
f.deliver(Buffer("XUSHPING", 8), remote);
|
||||
BEAST_EXPECT(!Fixture::charged(probe, balance, 100));
|
||||
|
||||
// A malformed advertisement is charged for
|
||||
Buffer malformed(xush::tagSize + 2);
|
||||
std::memcpy(malformed.data(), "XUSHPEER\x05\x00", malformed.size());
|
||||
f.deliver(malformed, remote);
|
||||
BEAST_EXPECT(Fixture::charged(probe, balance, 200));
|
||||
BEAST_EXPECT(f.targets().size() == 2);
|
||||
}
|
||||
|
||||
void
|
||||
testTransaction()
|
||||
{
|
||||
testcase("XUSHTXNF transaction");
|
||||
using namespace jtx;
|
||||
Fixture f(*this, true);
|
||||
auto& env = f.env;
|
||||
auto const remote = ep("203.0.113.3:51235");
|
||||
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const jt = env.jt(pay(alice, bob, XRP(100)));
|
||||
auto const txid = jt.stx->getTransactionID();
|
||||
|
||||
// Fragments arrive out of order and are reassembled
|
||||
auto datagrams = xush::encodeTxn(slice(serialize(*jt.stx)), txid, 40);
|
||||
BEAST_EXPECT(datagrams.size() > 2);
|
||||
std::reverse(datagrams.begin(), datagrams.end());
|
||||
f.deliver(datagrams, remote);
|
||||
env.app().getJobQueue().rendezvous();
|
||||
|
||||
BEAST_EXPECT(!f.isBad(txid));
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob) == XRP(10100));
|
||||
|
||||
// Delivering it again is harmless
|
||||
f.deliver(datagrams, remote);
|
||||
env.app().getJobQueue().rendezvous();
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob) == XRP(10100));
|
||||
}
|
||||
|
||||
void
|
||||
testBadTransactions()
|
||||
{
|
||||
testcase("XUSHTXNF bad transactions");
|
||||
using namespace jtx;
|
||||
Fixture f(*this, true);
|
||||
auto& env = f.env;
|
||||
auto const remote = ep("203.0.113.4:51235");
|
||||
auto probe = f.probe(remote);
|
||||
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
auto const good = env.jt(pay(alice, bob, XRP(100)));
|
||||
auto const blob = serialize(*good.stx);
|
||||
auto const txid = good.stx->getTransactionID();
|
||||
|
||||
// The ID in the header must match the transaction
|
||||
auto balance = probe.balance();
|
||||
f.deliver(xush::encodeTxn(slice(blob), uint256(1)), remote);
|
||||
BEAST_EXPECT(Fixture::charged(probe, balance, 400));
|
||||
BEAST_EXPECT(!f.isBad(txid));
|
||||
|
||||
// Garbage that doesn't parse as a transaction
|
||||
balance = probe.balance();
|
||||
f.deliver(
|
||||
xush::encodeTxn(makeSlice(std::string("garbage")), uint256(2)),
|
||||
remote);
|
||||
BEAST_EXPECT(Fixture::charged(probe, balance, 400));
|
||||
|
||||
// Fragments of one transaction that disagree with each other
|
||||
balance = probe.balance();
|
||||
f.deliver(xush::encodeTxn(slice(blob), uint256(3), 40).front(), remote);
|
||||
f.deliver(xush::encodeTxn(slice(blob), uint256(3), 60).front(), remote);
|
||||
BEAST_EXPECT(Fixture::charged(probe, balance, 400));
|
||||
|
||||
// A malformed fragment
|
||||
balance = probe.balance();
|
||||
f.deliver(Buffer("XUSHTXNF", 8), remote);
|
||||
BEAST_EXPECT(Fixture::charged(probe, balance, 200));
|
||||
|
||||
// Pseudo-transactions are never accepted
|
||||
balance = probe.balance();
|
||||
auto const pseudo = pseudoTx();
|
||||
f.deliver(
|
||||
xush::encodeTxn(
|
||||
slice(serialize(pseudo)), pseudo.getTransactionID()),
|
||||
remote);
|
||||
BEAST_EXPECT(Fixture::charged(probe, balance, 2000));
|
||||
|
||||
// An expired transaction is remembered as bad
|
||||
auto const expired =
|
||||
env.jt(pay(alice, bob, XRP(100)), last_ledger_seq(1));
|
||||
auto const expiredId = expired.stx->getTransactionID();
|
||||
balance = probe.balance();
|
||||
f.deliver(
|
||||
xush::encodeTxn(slice(serialize(*expired.stx)), expiredId), remote);
|
||||
BEAST_EXPECT(f.isBad(expiredId));
|
||||
BEAST_EXPECT(Fixture::charged(probe, balance, 150));
|
||||
|
||||
// A badly signed transaction is remembered as bad
|
||||
auto corrupted = good.stx->getJson(JsonOptions::none);
|
||||
corrupted[jss::TxnSignature] = std::string(128, '0');
|
||||
STTx const badSig(parse(corrupted));
|
||||
auto const badSigId = badSig.getTransactionID();
|
||||
balance = probe.balance();
|
||||
f.deliver(xush::encodeTxn(slice(serialize(badSig)), badSigId), remote);
|
||||
BEAST_EXPECT(f.isBad(badSigId));
|
||||
BEAST_EXPECT(Fixture::charged(probe, balance, 2000));
|
||||
|
||||
// A known bad transaction is charged for again
|
||||
balance = probe.balance();
|
||||
f.deliver(xush::encodeTxn(slice(serialize(badSig)), badSigId), remote);
|
||||
BEAST_EXPECT(Fixture::charged(probe, balance, 150));
|
||||
|
||||
env.close();
|
||||
BEAST_EXPECT(env.balance(bob) == XRP(10000));
|
||||
}
|
||||
|
||||
void
|
||||
testDisconnect()
|
||||
{
|
||||
testcase("resource limit");
|
||||
Fixture f(*this, true);
|
||||
auto const remote = ep("203.0.113.5:51235");
|
||||
auto probe = f.probe(remote);
|
||||
|
||||
// A sender that keeps misbehaving is ignored
|
||||
Buffer const blob(64);
|
||||
for (int i = 0; i < 10000 && probe.balance() < Resource::dropThreshold;
|
||||
++i)
|
||||
f.deliver(xush::encodeTxn(slice(blob), uint256(1)), remote);
|
||||
BEAST_EXPECT(probe.balance() >= Resource::dropThreshold);
|
||||
|
||||
f.deliver(xush::encodePeers({ep("8.8.8.8:51235")}), remote);
|
||||
BEAST_EXPECT(f.targets().empty());
|
||||
|
||||
// Other senders are not affected
|
||||
f.deliver(
|
||||
xush::encodePeers({ep("8.8.8.8:51235")}), ep("203.0.113.6:51235"));
|
||||
BEAST_EXPECT(f.targets() == asSet({ep("8.8.8.8:51235")}));
|
||||
}
|
||||
|
||||
void
|
||||
testPublish()
|
||||
{
|
||||
testcase("publish");
|
||||
using namespace jtx;
|
||||
Fixture f(*this, true);
|
||||
auto& env = f.env;
|
||||
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
env.fund(XRP(10000), alice, bob);
|
||||
env.close();
|
||||
|
||||
Sink sink4(boost::asio::ip::make_address("127.0.0.1"));
|
||||
f.peerFinder.addFixedPeer("sink4", {sink4.endpoint()});
|
||||
|
||||
std::optional<Sink> sink6;
|
||||
try
|
||||
{
|
||||
sink6.emplace(boost::asio::ip::make_address("::1"));
|
||||
f.peerFinder.addFixedPeer("sink6", {sink6->endpoint()});
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
log << "IPv6 loopback unavailable, skipping" << std::endl;
|
||||
}
|
||||
|
||||
// A transaction relayed by NetworkOPs is published on the highway
|
||||
env(pay(alice, bob, XRP(100)));
|
||||
auto const stx = env.tx();
|
||||
auto const blob = serialize(*stx);
|
||||
|
||||
auto check = [&](Sink& sink) {
|
||||
auto const d = sink.receive(xush::MessageType::txn);
|
||||
if (!BEAST_EXPECT(d))
|
||||
return;
|
||||
auto const fragment = xush::decodeTxnFragment(makeSlice(*d));
|
||||
if (!BEAST_EXPECT(fragment))
|
||||
return;
|
||||
BEAST_EXPECT(fragment->txid == stx->getTransactionID());
|
||||
BEAST_EXPECT(fragment->count == 1);
|
||||
BEAST_EXPECT(fragment->payload == slice(blob));
|
||||
};
|
||||
check(sink4);
|
||||
if (sink6)
|
||||
check(*sink6);
|
||||
|
||||
// Large transactions are fragmented
|
||||
Buffer big(5000);
|
||||
for (std::size_t i = 0; i < big.size(); ++i)
|
||||
big.data()[i] = static_cast<std::uint8_t>(i);
|
||||
f.overlay.publishTxXUSH(slice(big), uint256(7));
|
||||
|
||||
xush::Reassembler reassembler;
|
||||
std::optional<Buffer> rebuilt;
|
||||
while (!rebuilt)
|
||||
{
|
||||
auto const d = sink4.receive(xush::MessageType::txn);
|
||||
if (!BEAST_EXPECT(d))
|
||||
break;
|
||||
auto const fragment = xush::decodeTxnFragment(makeSlice(*d));
|
||||
if (!BEAST_EXPECT(fragment && fragment->txid == uint256(7)))
|
||||
break;
|
||||
auto result = reassembler.add(
|
||||
sink4.endpoint(),
|
||||
*fragment,
|
||||
xush::Reassembler::clock_type::now());
|
||||
if (result.status == xush::Reassembler::Status::complete)
|
||||
rebuilt = std::move(result.txn);
|
||||
}
|
||||
BEAST_EXPECT(rebuilt && *rebuilt == big);
|
||||
|
||||
// Transactions too large for the highway are not sent
|
||||
Buffer const huge(xush::maxTxnSize + 1);
|
||||
f.overlay.publishTxXUSH(slice(huge), uint256(8));
|
||||
f.overlay.publishTxXUSH(slice(big), uint256(9));
|
||||
auto const d = sink4.receive(xush::MessageType::txn);
|
||||
if (BEAST_EXPECT(d))
|
||||
{
|
||||
auto const fragment = xush::decodeTxnFragment(makeSlice(*d));
|
||||
BEAST_EXPECT(fragment && fragment->txid == uint256(9));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSendPeers()
|
||||
{
|
||||
testcase("advertise");
|
||||
Fixture f(*this, true);
|
||||
|
||||
Sink sink(boost::asio::ip::make_address("127.0.0.1"));
|
||||
f.peerFinder.addFixedPeer("sink", {sink.endpoint()});
|
||||
|
||||
// Learned endpoints are advertised to targets once per second, but
|
||||
// fixed peers are never advertised.
|
||||
f.peerFinder.add_highway_peers({ep("8.8.8.8:51235")});
|
||||
|
||||
auto const d = sink.receive(xush::MessageType::peers);
|
||||
if (!BEAST_EXPECT(d))
|
||||
return;
|
||||
auto const endpoints = xush::decodePeers(makeSlice(*d));
|
||||
BEAST_EXPECT(
|
||||
endpoints && asSet(*endpoints) == asSet({ep("8.8.8.8:51235")}));
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testDisabled();
|
||||
testPeers();
|
||||
testTransaction();
|
||||
testBadTransactions();
|
||||
testDisconnect();
|
||||
testPublish();
|
||||
testSendPeers();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(OverlayXUSH, overlay, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -0,0 +1,313 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/overlay/Message.h>
|
||||
#include <xrpld/overlay/detail/ProtocolMessage.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/protocol/messages.h>
|
||||
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/beast/core/multi_buffer.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
/** Receive-side framing tests for invokeProtocolMessage.
|
||||
|
||||
PeerImp::onReadMessage hands its entire read buffer to
|
||||
invokeProtocolMessage, which must decode exactly one frame and report
|
||||
exactly that frame's size as consumed. A single read routinely contains
|
||||
several frames, so the payload parser must never look past the payload
|
||||
size declared in the frame header.
|
||||
*/
|
||||
class ProtocolMessage_test : public beast::unit_test::suite
|
||||
{
|
||||
using Compressed = compression::Compressed;
|
||||
|
||||
/** Minimal stand-in for the PeerImp handler interface. */
|
||||
struct Handler
|
||||
{
|
||||
bool compression = false;
|
||||
std::vector<std::shared_ptr<::google::protobuf::Message>> received;
|
||||
std::vector<std::uint16_t> types;
|
||||
|
||||
bool
|
||||
compressionEnabled() const
|
||||
{
|
||||
return compression;
|
||||
}
|
||||
|
||||
void
|
||||
onMessageBegin(
|
||||
std::uint16_t,
|
||||
std::shared_ptr<::google::protobuf::Message> const&,
|
||||
std::size_t,
|
||||
std::size_t,
|
||||
bool)
|
||||
{
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void
|
||||
onMessage(std::shared_ptr<T> const& m)
|
||||
{
|
||||
received.push_back(m);
|
||||
}
|
||||
|
||||
void
|
||||
onMessageEnd(
|
||||
std::uint16_t type,
|
||||
std::shared_ptr<::google::protobuf::Message> const&)
|
||||
{
|
||||
types.push_back(type);
|
||||
}
|
||||
|
||||
void
|
||||
onMessageUnknown(std::uint16_t type)
|
||||
{
|
||||
types.push_back(type);
|
||||
}
|
||||
};
|
||||
|
||||
/** Loads wire bytes into a multi_buffer as one or two committed
|
||||
segments, split at `split` (no split if split is 0 or past the end).
|
||||
*/
|
||||
static void
|
||||
load(
|
||||
boost::beast::multi_buffer& rb,
|
||||
std::vector<std::uint8_t> const& wire,
|
||||
std::size_t split = 0)
|
||||
{
|
||||
auto commit = [&rb](std::uint8_t const* p, std::size_t n) {
|
||||
if (n != 0)
|
||||
rb.commit(boost::asio::buffer_copy(
|
||||
rb.prepare(n), boost::asio::buffer(p, n)));
|
||||
};
|
||||
|
||||
if (split == 0 || split >= wire.size())
|
||||
{
|
||||
commit(wire.data(), wire.size());
|
||||
return;
|
||||
}
|
||||
commit(wire.data(), split);
|
||||
commit(wire.data() + split, wire.size() - split);
|
||||
}
|
||||
|
||||
/** Mirrors the consume loop in PeerImp::onReadMessage. */
|
||||
static boost::system::error_code
|
||||
drain(boost::beast::multi_buffer& rb, Handler& handler)
|
||||
{
|
||||
std::size_t hint = 0;
|
||||
while (rb.size() > 0)
|
||||
{
|
||||
auto const [consumed, ec] =
|
||||
invokeProtocolMessage(rb.data(), handler, hint);
|
||||
if (ec)
|
||||
return ec;
|
||||
if (consumed == 0)
|
||||
break;
|
||||
rb.consume(consumed);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static std::vector<std::uint8_t>
|
||||
frame(
|
||||
::google::protobuf::Message const& m,
|
||||
protocol::MessageType type,
|
||||
Compressed compressed = Compressed::Off)
|
||||
{
|
||||
Message msg(m, type);
|
||||
return msg.getBuffer(compressed);
|
||||
}
|
||||
|
||||
static protocol::TMPing
|
||||
makePing(protocol::TMPing::pingType type, std::uint32_t seq)
|
||||
{
|
||||
protocol::TMPing ping;
|
||||
ping.set_type(type);
|
||||
ping.set_seq(seq);
|
||||
return ping;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
std::shared_ptr<T>
|
||||
receivedAs(Handler const& h, std::size_t i)
|
||||
{
|
||||
if (!BEAST_EXPECT(i < h.received.size()))
|
||||
return {};
|
||||
auto p = std::dynamic_pointer_cast<T>(h.received[i]);
|
||||
BEAST_EXPECT(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
void
|
||||
testCoalescedUncompressed()
|
||||
{
|
||||
testcase("Coalesced uncompressed frames");
|
||||
|
||||
auto const first = makePing(protocol::TMPing::ptPING, 7);
|
||||
auto second = makePing(protocol::TMPing::ptPONG, 99);
|
||||
second.set_nettime(5);
|
||||
|
||||
auto wire = frame(first, protocol::mtPING);
|
||||
auto const firstSize = wire.size();
|
||||
auto const tail = frame(second, protocol::mtPING);
|
||||
wire.insert(wire.end(), tail.begin(), tail.end());
|
||||
|
||||
// Both frames delivered by a single read, with every possible
|
||||
// segmentation of the read buffer (0 means one contiguous segment).
|
||||
for (std::size_t split = 0; split < wire.size(); ++split)
|
||||
{
|
||||
boost::beast::multi_buffer rb;
|
||||
load(rb, wire, split);
|
||||
|
||||
Handler h;
|
||||
auto const ec = drain(rb, h);
|
||||
|
||||
BEAST_EXPECTS(
|
||||
!ec, "split " + std::to_string(split) + ": " + ec.message());
|
||||
BEAST_EXPECT(rb.size() == 0);
|
||||
if (!BEAST_EXPECT(h.received.size() == 2))
|
||||
continue;
|
||||
|
||||
auto const a = receivedAs<protocol::TMPing>(h, 0);
|
||||
auto const b = receivedAs<protocol::TMPing>(h, 1);
|
||||
if (!a || !b)
|
||||
continue;
|
||||
BEAST_EXPECT(a->type() == protocol::TMPing::ptPING);
|
||||
BEAST_EXPECT(a->seq() == 7);
|
||||
BEAST_EXPECT(!a->has_nettime());
|
||||
BEAST_EXPECT(b->type() == protocol::TMPing::ptPONG);
|
||||
BEAST_EXPECT(b->seq() == 99);
|
||||
BEAST_EXPECT(b->nettime() == 5);
|
||||
}
|
||||
|
||||
// The first frame on its own, followed by a partial second frame,
|
||||
// is delivered alone and the remainder is left for the next read.
|
||||
{
|
||||
boost::beast::multi_buffer rb;
|
||||
std::vector<std::uint8_t> partial(
|
||||
wire.begin(), wire.begin() + firstSize + 3);
|
||||
load(rb, partial);
|
||||
|
||||
Handler h;
|
||||
BEAST_EXPECT(!drain(rb, h));
|
||||
BEAST_EXPECT(h.received.size() == 1);
|
||||
BEAST_EXPECT(rb.size() == 3);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testUncompressedThenCompressed()
|
||||
{
|
||||
testcase("Uncompressed frame followed by compressed frame");
|
||||
|
||||
auto const ping = makePing(protocol::TMPing::ptPING, 1234);
|
||||
|
||||
protocol::TMTransaction tx;
|
||||
tx.set_rawtransaction(std::string(4096, 'x'));
|
||||
tx.set_status(protocol::tsNEW);
|
||||
|
||||
auto wire = frame(ping, protocol::mtPING);
|
||||
auto const txFrame = frame(tx, protocol::mtTRANSACTION, Compressed::On);
|
||||
// The second frame must really be compressed for this test to mean
|
||||
// anything: its header is what the first parse used to run into.
|
||||
if (!BEAST_EXPECT(!txFrame.empty() && (txFrame[0] & 0x80)))
|
||||
return;
|
||||
wire.insert(wire.end(), txFrame.begin(), txFrame.end());
|
||||
|
||||
for (std::size_t split = 0; split < wire.size(); split += 7)
|
||||
{
|
||||
boost::beast::multi_buffer rb;
|
||||
load(rb, wire, split);
|
||||
|
||||
Handler h;
|
||||
h.compression = true;
|
||||
auto const ec = drain(rb, h);
|
||||
|
||||
BEAST_EXPECTS(
|
||||
!ec, "split " + std::to_string(split) + ": " + ec.message());
|
||||
BEAST_EXPECT(rb.size() == 0);
|
||||
if (!BEAST_EXPECT(h.received.size() == 2))
|
||||
continue;
|
||||
|
||||
auto const p = receivedAs<protocol::TMPing>(h, 0);
|
||||
auto const t = receivedAs<protocol::TMTransaction>(h, 1);
|
||||
if (!p || !t)
|
||||
continue;
|
||||
BEAST_EXPECT(p->type() == protocol::TMPing::ptPING);
|
||||
BEAST_EXPECT(p->seq() == 1234);
|
||||
BEAST_EXPECT(t->rawtransaction() == tx.rawtransaction());
|
||||
BEAST_EXPECT(t->status() == protocol::tsNEW);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testDeclaredSizeIsAuthoritative()
|
||||
{
|
||||
testcase("Declared payload size bounds the parse");
|
||||
|
||||
auto const ping = makePing(protocol::TMPing::ptPING, 7);
|
||||
auto wire = frame(ping, protocol::mtPING);
|
||||
|
||||
// Shrink the declared payload size by one byte without removing the
|
||||
// byte from the stream. The declared payload alone is a truncated
|
||||
// TMPing, so the frame must be rejected rather than completed from
|
||||
// bytes that lie beyond it.
|
||||
std::uint32_t declared = (std::uint32_t(wire[0]) << 24) |
|
||||
(std::uint32_t(wire[1]) << 16) | (std::uint32_t(wire[2]) << 8) |
|
||||
std::uint32_t(wire[3]);
|
||||
if (!BEAST_EXPECT(declared > 0))
|
||||
return;
|
||||
--declared;
|
||||
wire[0] = static_cast<std::uint8_t>(declared >> 24);
|
||||
wire[1] = static_cast<std::uint8_t>(declared >> 16);
|
||||
wire[2] = static_cast<std::uint8_t>(declared >> 8);
|
||||
wire[3] = static_cast<std::uint8_t>(declared);
|
||||
|
||||
boost::beast::multi_buffer rb;
|
||||
load(rb, wire);
|
||||
|
||||
Handler h;
|
||||
auto const ec = drain(rb, h);
|
||||
BEAST_EXPECT(ec == boost::system::errc::bad_message);
|
||||
BEAST_EXPECT(h.received.empty());
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testCoalescedUncompressed();
|
||||
testUncompressedThenCompressed();
|
||||
testDeclaredSizeIsAuthoritative();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(ProtocolMessage, overlay, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -0,0 +1,466 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/overlay/detail/XUSH.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace test {
|
||||
|
||||
class XUSH_test : public beast::unit_test::suite
|
||||
{
|
||||
using Status = xush::Reassembler::Status;
|
||||
|
||||
static beast::IP::Endpoint
|
||||
ep(std::string const& s)
|
||||
{
|
||||
return beast::IP::Endpoint::from_string(s);
|
||||
}
|
||||
|
||||
static Slice
|
||||
slice(Buffer const& b)
|
||||
{
|
||||
return Slice(b.data(), b.size());
|
||||
}
|
||||
|
||||
static Buffer
|
||||
makeTxn(std::size_t size)
|
||||
{
|
||||
Buffer b(size);
|
||||
for (std::size_t i = 0; i < size; ++i)
|
||||
b.data()[i] = static_cast<std::uint8_t>(i * 31 + 7);
|
||||
return b;
|
||||
}
|
||||
|
||||
static void
|
||||
putU32(std::uint8_t* p, std::uint32_t v)
|
||||
{
|
||||
p[0] = static_cast<std::uint8_t>(v >> 24);
|
||||
p[1] = static_cast<std::uint8_t>(v >> 16);
|
||||
p[2] = static_cast<std::uint8_t>(v >> 8);
|
||||
p[3] = static_cast<std::uint8_t>(v);
|
||||
}
|
||||
|
||||
// A XUSHTXNF datagram with an arbitrary (possibly invalid) header
|
||||
static Buffer
|
||||
rawFragment(
|
||||
uint256 const& txid,
|
||||
std::uint32_t totalSize,
|
||||
std::uint32_t count,
|
||||
std::uint32_t index,
|
||||
std::size_t payloadSize)
|
||||
{
|
||||
Buffer b(xush::txnHeaderSize + payloadSize);
|
||||
std::memset(b.data(), 0xAB, b.size());
|
||||
std::memcpy(b.data(), "XUSHTXNF", xush::tagSize);
|
||||
std::memcpy(b.data() + xush::tagSize, txid.data(), txid.size());
|
||||
putU32(b.data() + 40, totalSize);
|
||||
putU32(b.data() + 44, count);
|
||||
putU32(b.data() + 48, index);
|
||||
return b;
|
||||
}
|
||||
|
||||
// A XUSHPEER datagram with arbitrary (possibly invalid) counts
|
||||
static Buffer
|
||||
rawPeers(std::uint8_t n4, std::uint8_t n6, std::size_t bodySize)
|
||||
{
|
||||
Buffer b(xush::tagSize + 2 + bodySize);
|
||||
std::memset(b.data(), 0, b.size());
|
||||
std::memcpy(b.data(), "XUSHPEER", xush::tagSize);
|
||||
b.data()[8] = n4;
|
||||
b.data()[9] = n6;
|
||||
return b;
|
||||
}
|
||||
|
||||
void
|
||||
testClassify()
|
||||
{
|
||||
testcase("classify");
|
||||
using namespace xush;
|
||||
|
||||
auto type = [](std::string const& s) { return classify(makeSlice(s)); };
|
||||
|
||||
BEAST_EXPECT(type("XUSHPEER") == MessageType::peers);
|
||||
BEAST_EXPECT(type("XUSHPEER and more") == MessageType::peers);
|
||||
BEAST_EXPECT(type("XUSHTXNF") == MessageType::txn);
|
||||
BEAST_EXPECT(type("XUSHPING") == MessageType::unknown);
|
||||
BEAST_EXPECT(type("XUSHPEE") == MessageType::unknown);
|
||||
BEAST_EXPECT(type("") == MessageType::unknown);
|
||||
BEAST_EXPECT(type("{\"command\":\"ping\"}") == MessageType::unknown);
|
||||
}
|
||||
|
||||
void
|
||||
testPeers()
|
||||
{
|
||||
testcase("XUSHPEER");
|
||||
using namespace xush;
|
||||
|
||||
std::vector<beast::IP::Endpoint> const endpoints{
|
||||
ep("1.2.3.4:51235"), ep("[2001:db8::1]:21337"), ep("5.6.7.8:1")};
|
||||
|
||||
auto const datagram = encodePeers(endpoints);
|
||||
BEAST_EXPECT(datagram.size() == tagSize + 2 + 2 * 8 + 20);
|
||||
BEAST_EXPECT(classify(slice(datagram)) == MessageType::peers);
|
||||
|
||||
if (auto const decoded = decodePeers(slice(datagram));
|
||||
BEAST_EXPECT(decoded))
|
||||
{
|
||||
BEAST_EXPECT(
|
||||
std::set<beast::IP::Endpoint>(
|
||||
decoded->begin(), decoded->end()) ==
|
||||
std::set<beast::IP::Endpoint>(
|
||||
endpoints.begin(), endpoints.end()));
|
||||
}
|
||||
|
||||
// An empty advertisement is valid
|
||||
if (auto const decoded = decodePeers(slice(encodePeers({})));
|
||||
BEAST_EXPECT(decoded))
|
||||
BEAST_EXPECT(decoded->empty());
|
||||
|
||||
// At most maxAdvertisedPeers are advertised, and the result fits in
|
||||
// a single datagram even if they are all IPv6
|
||||
{
|
||||
std::vector<beast::IP::Endpoint> many;
|
||||
for (std::uint16_t i = 1; i <= maxAdvertisedPeers + 10; ++i)
|
||||
many.emplace_back(
|
||||
boost::asio::ip::make_address("2001:db8::1"), i);
|
||||
|
||||
auto const big = encodePeers(many);
|
||||
BEAST_EXPECT(big.size() <= maxDatagramSize);
|
||||
|
||||
auto const decoded = decodePeers(slice(big));
|
||||
BEAST_EXPECT(decoded && decoded->size() == maxAdvertisedPeers);
|
||||
}
|
||||
|
||||
// Truncated or padded
|
||||
BEAST_EXPECT(!decodePeers(Slice(datagram.data(), datagram.size() - 1)));
|
||||
{
|
||||
Buffer padded(datagram.size() + 1);
|
||||
std::memcpy(padded.data(), datagram.data(), datagram.size());
|
||||
BEAST_EXPECT(!decodePeers(slice(padded)));
|
||||
}
|
||||
BEAST_EXPECT(!decodePeers(Slice(datagram.data(), tagSize + 1)));
|
||||
|
||||
// Too many endpoints, even with a consistent size
|
||||
BEAST_EXPECT(!decodePeers(slice(rawPeers(
|
||||
maxAdvertisedPeers + 1, 0, (maxAdvertisedPeers + 1) * 8))));
|
||||
BEAST_EXPECT(decodePeers(
|
||||
slice(rawPeers(maxAdvertisedPeers, 0, maxAdvertisedPeers * 8))));
|
||||
|
||||
// A port that doesn't fit in 16 bits
|
||||
{
|
||||
auto bad = rawPeers(1, 0, 8);
|
||||
putU32(bad.data() + tagSize + 2 + 4, 0x10000);
|
||||
BEAST_EXPECT(!decodePeers(slice(bad)));
|
||||
putU32(bad.data() + tagSize + 2 + 4, 0xFFFF);
|
||||
BEAST_EXPECT(decodePeers(slice(bad)));
|
||||
}
|
||||
|
||||
// Wrong tag
|
||||
BEAST_EXPECT(!decodePeers(slice(rawFragment(uint256{1}, 1, 1, 0, 1))));
|
||||
}
|
||||
|
||||
void
|
||||
testTxnEncoding()
|
||||
{
|
||||
testcase("XUSHTXNF encoding");
|
||||
using namespace xush;
|
||||
|
||||
uint256 const txid{42};
|
||||
|
||||
// A typical transaction fits in a single datagram
|
||||
{
|
||||
auto const txn = makeTxn(200);
|
||||
auto const datagrams = encodeTxn(slice(txn), txid);
|
||||
BEAST_EXPECT(datagrams.size() == 1);
|
||||
BEAST_EXPECT(datagrams[0].size() == txnHeaderSize + 200);
|
||||
|
||||
auto const fragment = decodeTxnFragment(slice(datagrams[0]));
|
||||
if (BEAST_EXPECT(fragment))
|
||||
{
|
||||
BEAST_EXPECT(fragment->txid == txid);
|
||||
BEAST_EXPECT(fragment->totalSize == 200);
|
||||
BEAST_EXPECT(fragment->count == 1);
|
||||
BEAST_EXPECT(fragment->index == 0);
|
||||
BEAST_EXPECT(fragment->payload == slice(txn));
|
||||
}
|
||||
}
|
||||
|
||||
// Larger transactions are split, and no datagram is too large
|
||||
for (auto const size :
|
||||
{maxFragmentPayload,
|
||||
maxFragmentPayload + 1,
|
||||
3 * maxFragmentPayload,
|
||||
3 * maxFragmentPayload + 17})
|
||||
{
|
||||
auto const txn = makeTxn(size);
|
||||
auto const datagrams = encodeTxn(slice(txn), txid);
|
||||
auto const count =
|
||||
(size + maxFragmentPayload - 1) / maxFragmentPayload;
|
||||
BEAST_EXPECT(datagrams.size() == count);
|
||||
|
||||
std::size_t total = 0;
|
||||
for (std::size_t i = 0; i < datagrams.size(); ++i)
|
||||
{
|
||||
BEAST_EXPECT(datagrams[i].size() <= maxDatagramSize);
|
||||
auto const fragment = decodeTxnFragment(slice(datagrams[i]));
|
||||
if (!BEAST_EXPECT(fragment))
|
||||
continue;
|
||||
BEAST_EXPECT(fragment->index == i);
|
||||
BEAST_EXPECT(fragment->count == count);
|
||||
BEAST_EXPECT(fragment->totalSize == size);
|
||||
BEAST_EXPECT(
|
||||
fragment->payload ==
|
||||
Slice(
|
||||
txn.data() + i * maxFragmentPayload,
|
||||
fragment->payload.size()));
|
||||
total += fragment->payload.size();
|
||||
}
|
||||
BEAST_EXPECT(total == size);
|
||||
}
|
||||
|
||||
// Limits
|
||||
BEAST_EXPECT(encodeTxn(Slice{}, txid).empty());
|
||||
BEAST_EXPECT(encodeTxn(slice(Buffer(maxTxnSize + 1)), txid).empty());
|
||||
{
|
||||
auto const datagrams = encodeTxn(slice(Buffer(maxTxnSize)), txid);
|
||||
BEAST_EXPECT(!datagrams.empty());
|
||||
BEAST_EXPECT(datagrams.size() <= maxFragments);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testTxnDecoding()
|
||||
{
|
||||
testcase("XUSHTXNF decoding");
|
||||
using namespace xush;
|
||||
|
||||
uint256 const id{7};
|
||||
auto decode = [](Buffer const& b) {
|
||||
return decodeTxnFragment(slice(b)).has_value();
|
||||
};
|
||||
|
||||
BEAST_EXPECT(decode(rawFragment(id, 10, 2, 1, 5)));
|
||||
BEAST_EXPECT(decode(rawFragment(id, 10, 1, 0, 10)));
|
||||
BEAST_EXPECT(decode(rawFragment(id, maxTxnSize, maxFragments, 0, 1)));
|
||||
|
||||
// No payload
|
||||
BEAST_EXPECT(!decode(rawFragment(id, 10, 1, 0, 0)));
|
||||
// Bad total size
|
||||
BEAST_EXPECT(!decode(rawFragment(id, 0, 1, 0, 1)));
|
||||
BEAST_EXPECT(!decode(rawFragment(id, maxTxnSize + 1, 2, 0, 1)));
|
||||
// Bad fragment count
|
||||
BEAST_EXPECT(!decode(rawFragment(id, 10, 0, 0, 1)));
|
||||
BEAST_EXPECT(!decode(rawFragment(id, 10, 11, 0, 1)));
|
||||
BEAST_EXPECT(
|
||||
!decode(rawFragment(id, maxTxnSize, maxFragments + 1, 0, 1)));
|
||||
// Bad index
|
||||
BEAST_EXPECT(!decode(rawFragment(id, 10, 2, 2, 1)));
|
||||
// Payload larger than the transaction
|
||||
BEAST_EXPECT(!decode(rawFragment(id, 10, 2, 0, 11)));
|
||||
// Wrong tag
|
||||
BEAST_EXPECT(!decodeTxnFragment(slice(encodePeers({}))));
|
||||
}
|
||||
|
||||
void
|
||||
testReassembly()
|
||||
{
|
||||
testcase("reassembly");
|
||||
using namespace xush;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
auto const now = Reassembler::clock_type::now();
|
||||
auto const alice = ep("1.2.3.4:5000");
|
||||
auto const bob = ep("5.6.7.8:5000");
|
||||
auto const carol = ep("9.9.9.9:5000");
|
||||
|
||||
uint256 const id1{1};
|
||||
uint256 const id2{2};
|
||||
|
||||
auto const txn1 = makeTxn(2 * maxFragmentPayload + 100);
|
||||
auto const txn2 = makeTxn(2 * maxFragmentPayload + 200);
|
||||
auto const d1 = encodeTxn(slice(txn1), id1);
|
||||
auto const d2 = encodeTxn(slice(txn2), id2);
|
||||
BEAST_EXPECT(d1.size() == 3 && d2.size() == 3);
|
||||
|
||||
auto f1 = [&](std::size_t i) {
|
||||
return *decodeTxnFragment(slice(d1[i]));
|
||||
};
|
||||
auto f2 = [&](std::size_t i) {
|
||||
return *decodeTxnFragment(slice(d2[i]));
|
||||
};
|
||||
|
||||
// Out of order, with a repeated fragment
|
||||
{
|
||||
Reassembler r;
|
||||
BEAST_EXPECT(r.add(alice, f1(2), now).status == Status::pending);
|
||||
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
|
||||
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
|
||||
BEAST_EXPECT(r.pending() == 1);
|
||||
|
||||
auto const result = r.add(alice, f1(1), now);
|
||||
BEAST_EXPECT(result.status == Status::complete);
|
||||
BEAST_EXPECT(result.txn == txn1);
|
||||
BEAST_EXPECT(r.pending() == 0);
|
||||
BEAST_EXPECT(r.bytes() == 0);
|
||||
}
|
||||
|
||||
// A transaction in a single datagram needs no state
|
||||
{
|
||||
Reassembler r;
|
||||
auto const small = makeTxn(100);
|
||||
auto const d = encodeTxn(slice(small), id2);
|
||||
auto const result =
|
||||
r.add(alice, *decodeTxnFragment(slice(d[0])), now);
|
||||
BEAST_EXPECT(result.status == Status::complete);
|
||||
BEAST_EXPECT(result.txn == small);
|
||||
BEAST_EXPECT(r.pending() == 0);
|
||||
|
||||
// ... but must be consistent
|
||||
auto bad = *decodeTxnFragment(slice(d[0]));
|
||||
bad.totalSize += 1;
|
||||
BEAST_EXPECT(r.add(alice, bad, now).status == Status::invalid);
|
||||
}
|
||||
|
||||
// Senders are kept apart
|
||||
{
|
||||
Reassembler r;
|
||||
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
|
||||
BEAST_EXPECT(r.add(alice, f1(1), now).status == Status::pending);
|
||||
BEAST_EXPECT(r.add(bob, f1(2), now).status == Status::pending);
|
||||
BEAST_EXPECT(r.pending() == 2);
|
||||
BEAST_EXPECT(r.add(alice, f1(2), now).status == Status::complete);
|
||||
BEAST_EXPECT(r.pending() == 1);
|
||||
}
|
||||
|
||||
// A header that disagrees with earlier fragments
|
||||
{
|
||||
Reassembler r;
|
||||
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
|
||||
auto bad = f1(1);
|
||||
bad.count += 1;
|
||||
BEAST_EXPECT(r.add(alice, bad, now).status == Status::invalid);
|
||||
BEAST_EXPECT(r.pending() == 0);
|
||||
BEAST_EXPECT(r.bytes() == 0);
|
||||
}
|
||||
|
||||
// More bytes than the advertised total size
|
||||
{
|
||||
Reassembler r;
|
||||
auto first = f1(0);
|
||||
first.count = 2;
|
||||
first.totalSize = maxFragmentPayload + 10;
|
||||
auto second = f1(1);
|
||||
second.count = 2;
|
||||
second.totalSize = first.totalSize;
|
||||
BEAST_EXPECT(r.add(alice, first, now).status == Status::pending);
|
||||
BEAST_EXPECT(r.add(alice, second, now).status == Status::invalid);
|
||||
BEAST_EXPECT(r.pending() == 0);
|
||||
}
|
||||
|
||||
// Every fragment present but fewer bytes than the total size
|
||||
{
|
||||
Reassembler r;
|
||||
auto first = f1(0);
|
||||
first.count = 2;
|
||||
first.totalSize = 2 * maxFragmentPayload + 5;
|
||||
auto second = f1(1);
|
||||
second.count = 2;
|
||||
second.totalSize = first.totalSize;
|
||||
BEAST_EXPECT(r.add(alice, first, now).status == Status::pending);
|
||||
BEAST_EXPECT(r.add(alice, second, now).status == Status::invalid);
|
||||
BEAST_EXPECT(r.pending() == 0);
|
||||
}
|
||||
|
||||
// Partial transactions time out
|
||||
{
|
||||
Reassembler r(
|
||||
Reassembler::defaultMaxPending,
|
||||
Reassembler::defaultMaxBytes,
|
||||
30s);
|
||||
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
|
||||
BEAST_EXPECT(r.add(alice, f1(1), now).status == Status::pending);
|
||||
|
||||
// Not yet
|
||||
BEAST_EXPECT(
|
||||
r.add(bob, f2(0), now + 30s).status == Status::pending);
|
||||
BEAST_EXPECT(r.pending() == 2);
|
||||
|
||||
// Now: the first transaction has to start over
|
||||
BEAST_EXPECT(
|
||||
r.add(bob, f2(1), now + 31s).status == Status::pending);
|
||||
BEAST_EXPECT(r.pending() == 1);
|
||||
BEAST_EXPECT(
|
||||
r.add(alice, f1(2), now + 31s).status == Status::pending);
|
||||
BEAST_EXPECT(r.pending() == 2);
|
||||
}
|
||||
|
||||
// The number of partial transactions is limited
|
||||
{
|
||||
Reassembler r(2, Reassembler::defaultMaxBytes, 30s);
|
||||
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
|
||||
BEAST_EXPECT(r.add(bob, f1(0), now).status == Status::pending);
|
||||
BEAST_EXPECT(r.add(carol, f1(0), now).status == Status::overloaded);
|
||||
BEAST_EXPECT(r.pending() == 2);
|
||||
|
||||
// Fragments of transactions already being rebuilt are accepted
|
||||
BEAST_EXPECT(r.add(alice, f1(1), now).status == Status::pending);
|
||||
BEAST_EXPECT(r.add(alice, f1(2), now).status == Status::complete);
|
||||
BEAST_EXPECT(r.add(carol, f1(0), now).status == Status::pending);
|
||||
}
|
||||
|
||||
// The number of bytes buffered is limited
|
||||
{
|
||||
Reassembler r(
|
||||
Reassembler::defaultMaxPending,
|
||||
maxFragmentPayload + Reassembler::fragmentOverhead,
|
||||
30s);
|
||||
BEAST_EXPECT(r.add(alice, f1(0), now).status == Status::pending);
|
||||
BEAST_EXPECT(r.add(alice, f1(1), now).status == Status::overloaded);
|
||||
BEAST_EXPECT(r.add(bob, f1(0), now).status == Status::overloaded);
|
||||
BEAST_EXPECT(r.pending() == 1);
|
||||
BEAST_EXPECT(
|
||||
r.bytes() ==
|
||||
maxFragmentPayload + Reassembler::fragmentOverhead);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testClassify();
|
||||
testPeers();
|
||||
testTxnEncoding();
|
||||
testTxnDecoding();
|
||||
testReassembly();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(XUSH, overlay, ripple);
|
||||
|
||||
} // namespace test
|
||||
} // namespace ripple
|
||||
@@ -0,0 +1,276 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/unit_test/SuiteJournal.h>
|
||||
#include <xrpld/peerfinder/PeerfinderManager.h>
|
||||
#include <xrpld/peerfinder/detail/Tuning.h>
|
||||
#include <xrpld/peerfinder/make_Manager.h>
|
||||
#include <xrpl/basics/BasicConfig.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/insight/NullCollector.h>
|
||||
#include <xrpl/beast/net/IPAddressConversion.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace PeerFinder {
|
||||
|
||||
// Tests the UDP Superhighway (XUSH) endpoint set kept by PeerFinder::Manager
|
||||
class HighwayPeers_test : public beast::unit_test::suite
|
||||
{
|
||||
test::SuiteJournal journal_{"HighwayPeers_test", *this};
|
||||
|
||||
struct Fixture
|
||||
{
|
||||
boost::asio::io_context io;
|
||||
TestStopwatch clock;
|
||||
BasicConfig basicConfig;
|
||||
std::unique_ptr<Manager> manager;
|
||||
|
||||
Fixture(beast::Journal journal, bool enabled, bool peerPrivate)
|
||||
: manager(make_Manager(
|
||||
io,
|
||||
clock,
|
||||
journal,
|
||||
basicConfig,
|
||||
beast::insight::NullCollector::New(),
|
||||
false))
|
||||
{
|
||||
Config config;
|
||||
config.udpHighway = enabled;
|
||||
config.peerPrivate = peerPrivate;
|
||||
manager->setConfig(config);
|
||||
}
|
||||
|
||||
~Fixture()
|
||||
{
|
||||
manager->stop();
|
||||
}
|
||||
};
|
||||
|
||||
static beast::IP::Endpoint
|
||||
ep(std::string const& s)
|
||||
{
|
||||
return beast::IP::Endpoint::from_string(s);
|
||||
}
|
||||
|
||||
// A distinct public IPv4 endpoint
|
||||
static beast::IP::Endpoint
|
||||
publicEp(std::uint32_t i)
|
||||
{
|
||||
return beast::IP::Endpoint(
|
||||
boost::asio::ip::address_v4(0x2C000000 + i), 51235);
|
||||
}
|
||||
|
||||
static std::set<beast::IP::Endpoint>
|
||||
asSet(std::vector<beast::IP::Endpoint> const& v)
|
||||
{
|
||||
return {v.begin(), v.end()};
|
||||
}
|
||||
|
||||
void
|
||||
testDisabled()
|
||||
{
|
||||
testcase("disabled");
|
||||
Fixture f(journal_, false, false);
|
||||
|
||||
f.manager->addFixedPeer("fixed", {ep("10.0.0.1:51235")});
|
||||
f.manager->add_highway_peers({ep("8.8.8.8:51235")});
|
||||
f.manager->onRedirects(
|
||||
beast::IP::to_asio_endpoint(ep("8.8.4.4:51235")),
|
||||
{beast::IP::to_asio_endpoint(ep("1.1.1.1:51235"))});
|
||||
|
||||
BEAST_EXPECT(f.manager->highway_targets(100).empty());
|
||||
BEAST_EXPECT(f.manager->highway_adverts(100).empty());
|
||||
}
|
||||
|
||||
void
|
||||
testLearning()
|
||||
{
|
||||
testcase("learning");
|
||||
Fixture f(journal_, true, false);
|
||||
|
||||
// Only valid public endpoints are learned from other servers
|
||||
f.manager->add_highway_peers(
|
||||
{ep("8.8.8.8:51235"),
|
||||
ep("10.0.0.1:51235"),
|
||||
ep("127.0.0.1:51235"),
|
||||
ep("9.9.9.9:0"),
|
||||
ep("0.0.0.0:51235")});
|
||||
BEAST_EXPECT(
|
||||
asSet(f.manager->highway_targets(100)) ==
|
||||
std::set<beast::IP::Endpoint>{ep("8.8.8.8:51235")});
|
||||
|
||||
// Fixed peers are trusted, even with a private address, but they
|
||||
// are never advertised
|
||||
f.manager->addFixedPeer("fixed", {ep("10.0.0.2:51235")});
|
||||
BEAST_EXPECT(
|
||||
asSet(f.manager->highway_targets(100)) ==
|
||||
(std::set<beast::IP::Endpoint>{
|
||||
ep("8.8.8.8:51235"), ep("10.0.0.2:51235")}));
|
||||
BEAST_EXPECT(
|
||||
asSet(f.manager->highway_adverts(100)) ==
|
||||
std::set<beast::IP::Endpoint>{ep("8.8.8.8:51235")});
|
||||
|
||||
// A learned endpoint that is later configured as fixed stops being
|
||||
// advertised
|
||||
f.manager->addFixedPeer("fixed2", {ep("8.8.8.8:51235")});
|
||||
BEAST_EXPECT(f.manager->highway_adverts(100).empty());
|
||||
BEAST_EXPECT(f.manager->highway_targets(100).size() == 2);
|
||||
|
||||
// Redirects are learned too
|
||||
f.manager->onRedirects(
|
||||
beast::IP::to_asio_endpoint(ep("8.8.4.4:51235")),
|
||||
{beast::IP::to_asio_endpoint(ep("1.1.1.1:51235"))});
|
||||
BEAST_EXPECT(f.manager->highway_targets(100).size() == 3);
|
||||
}
|
||||
|
||||
void
|
||||
testSampling()
|
||||
{
|
||||
testcase("sampling");
|
||||
Fixture f(journal_, true, false);
|
||||
|
||||
std::vector<beast::IP::Endpoint> endpoints;
|
||||
for (std::uint32_t i = 0; i < 100; ++i)
|
||||
endpoints.push_back(publicEp(i));
|
||||
f.manager->add_highway_peers(endpoints);
|
||||
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
auto const targets = f.manager->highway_targets(16);
|
||||
BEAST_EXPECT(targets.size() == 16);
|
||||
BEAST_EXPECT(asSet(targets).size() == 16);
|
||||
}
|
||||
|
||||
BEAST_EXPECT(
|
||||
asSet(f.manager->highway_targets(1000)) == asSet(endpoints));
|
||||
BEAST_EXPECT(f.manager->highway_targets(0).empty());
|
||||
}
|
||||
|
||||
void
|
||||
testPrivate()
|
||||
{
|
||||
testcase("private");
|
||||
Fixture f(journal_, true, true);
|
||||
|
||||
f.manager->addFixedPeer("fixed", {ep("10.0.0.2:51235")});
|
||||
f.manager->add_highway_peers({ep("8.8.8.8:51235")});
|
||||
|
||||
// A private server only talks to its fixed peers, and advertises
|
||||
// nothing
|
||||
BEAST_EXPECT(
|
||||
asSet(f.manager->highway_targets(100)) ==
|
||||
std::set<beast::IP::Endpoint>{ep("10.0.0.2:51235")});
|
||||
BEAST_EXPECT(f.manager->highway_adverts(100).empty());
|
||||
}
|
||||
|
||||
void
|
||||
testExpiry()
|
||||
{
|
||||
testcase("expiry");
|
||||
using namespace std::chrono_literals;
|
||||
Fixture f(journal_, true, false);
|
||||
|
||||
auto const ttl = Tuning::highwayPeerSecondsToLive;
|
||||
auto const advertised = ep("8.8.8.8:51235");
|
||||
auto const redirected = ep("1.1.1.1:51235");
|
||||
auto const fixed = ep("10.0.0.2:51235");
|
||||
|
||||
f.manager->addFixedPeer("fixed", {fixed});
|
||||
f.manager->add_highway_peers({advertised});
|
||||
f.manager->add_highway_peers({redirected});
|
||||
|
||||
// Advertisements don't extend the life of an entry, but
|
||||
// authenticated sources (here, a redirect) do
|
||||
f.clock.advance(ttl / 2);
|
||||
f.manager->add_highway_peers({advertised, redirected});
|
||||
f.manager->onRedirects(
|
||||
beast::IP::to_asio_endpoint(ep("8.8.4.4:51235")),
|
||||
{beast::IP::to_asio_endpoint(redirected)});
|
||||
|
||||
f.clock.advance(ttl / 2);
|
||||
f.manager->once_per_second();
|
||||
BEAST_EXPECT(f.manager->highway_targets(100).size() == 3);
|
||||
|
||||
f.clock.advance(1s);
|
||||
f.manager->once_per_second();
|
||||
BEAST_EXPECT(
|
||||
asSet(f.manager->highway_targets(100)) ==
|
||||
(std::set<beast::IP::Endpoint>{redirected, fixed}));
|
||||
|
||||
f.clock.advance(ttl / 2);
|
||||
f.manager->once_per_second();
|
||||
BEAST_EXPECT(
|
||||
asSet(f.manager->highway_targets(100)) ==
|
||||
std::set<beast::IP::Endpoint>{fixed});
|
||||
|
||||
// An expired entry can be learned again
|
||||
f.manager->add_highway_peers({advertised});
|
||||
BEAST_EXPECT(f.manager->highway_targets(100).size() == 2);
|
||||
}
|
||||
|
||||
void
|
||||
testLimit()
|
||||
{
|
||||
testcase("limit");
|
||||
Fixture f(journal_, true, false);
|
||||
|
||||
auto const max = Tuning::highwayPeersMax;
|
||||
|
||||
std::vector<beast::IP::Endpoint> endpoints;
|
||||
for (std::uint32_t i = 0; i < max + 100; ++i)
|
||||
endpoints.push_back(publicEp(i));
|
||||
f.manager->add_highway_peers(endpoints);
|
||||
BEAST_EXPECT(f.manager->highway_targets(max + 100).size() == max);
|
||||
|
||||
// Fixed peers don't count towards the limit
|
||||
f.manager->addFixedPeer("fixed", {ep("10.0.0.2:51235")});
|
||||
BEAST_EXPECT(f.manager->highway_targets(max + 100).size() == max + 1);
|
||||
|
||||
// Disabling the highway forgets everything
|
||||
Config config;
|
||||
config.udpHighway = false;
|
||||
f.manager->setConfig(config);
|
||||
BEAST_EXPECT(f.manager->highway_targets(max + 100).empty());
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testDisabled();
|
||||
testLearning();
|
||||
testSampling();
|
||||
testPrivate();
|
||||
testExpiry();
|
||||
testLimit();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(HighwayPeers, peerfinder, ripple);
|
||||
|
||||
} // namespace PeerFinder
|
||||
} // namespace ripple
|
||||
File diff suppressed because it is too large.
Load diff
@@ -18,16 +18,22 @@
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/app/ledger/Ledger.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/core/ConfigSections.h>
|
||||
#include <xrpld/nodestore/detail/DatabasePinnedImp.h>
|
||||
#include <xrpl/beast/unit_test.h>
|
||||
#include <xrpl/beast/utility/temp_dir.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <grpc/impl/codegen/compression_types.h>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
namespace ripple {
|
||||
@@ -102,14 +108,28 @@ struct CatalogueEnvOpts
|
||||
};
|
||||
|
||||
// envconfig with pinned_type set so catalogue_load passes the config guard.
|
||||
// Uses rwdb for in-process speed (allowed in standalone).
|
||||
//
|
||||
// envconfig() selects [node_db] type=rwdb, which is the tree-only null node
|
||||
// store: SHAMapStore never builds DatabasePinned on it, so catalogue_load
|
||||
// would refuse to run. Use the in-memory "memory" store for the hot side
|
||||
// instead. It is shared by path within the process, so each config gets its
|
||||
// own path (SHAMapStore creates the directory; nothing is written to it).
|
||||
// The pinned side uses rwdb, an in-memory persistent backend.
|
||||
inline std::unique_ptr<Config>
|
||||
catalogueEnvconfig(CatalogueEnvOpts const& opts = {})
|
||||
{
|
||||
auto cfg = test::jtx::envconfig();
|
||||
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
|
||||
if (opts.withPinnedType)
|
||||
{
|
||||
nodeDb.set("type", "memory");
|
||||
nodeDb.set(
|
||||
"path",
|
||||
(boost::filesystem::temp_directory_path() /
|
||||
boost::filesystem::unique_path("catalogue-test-%%%%-%%%%-%%%%"))
|
||||
.string());
|
||||
nodeDb.set("pinned_type", "rwdb");
|
||||
}
|
||||
nodeDb.set("online_delete", opts.onlineDelete);
|
||||
return cfg;
|
||||
}
|
||||
@@ -1067,6 +1087,621 @@ class Catalogue_test : public beast::unit_test::suite
|
||||
}
|
||||
|
||||
public:
|
||||
// ---- Editing uncompressed catalogues -----------------------------------
|
||||
//
|
||||
// Ledger record: seq, hash, txHash, accountHash, parentHash, drops,
|
||||
// closeFlags, closeTimeResolution, closeTime, parentCloseTime; then the
|
||||
// state map and the tx map, each a run of leaf records ending in
|
||||
// tnTERMINAL. A leaf record is type (1), key (32), and, unless it is a
|
||||
// tnREMOVE, size (4) and data.
|
||||
|
||||
static constexpr std::size_t catLedgerHeaderSize =
|
||||
4 + 32 * 4 + 8 + sizeof(int) + 4 + 8 + 8;
|
||||
|
||||
struct CatLedgerRecord
|
||||
{
|
||||
std::size_t header; // first byte of the ledger header
|
||||
std::size_t state; // first byte of the state map
|
||||
std::size_t tx; // first byte of the tx map
|
||||
std::size_t end; // one past the tx map's tnTERMINAL
|
||||
};
|
||||
|
||||
// Position just past the map that starts at `pos`.
|
||||
static std::size_t
|
||||
skipCatalogueMap(std::vector<std::uint8_t> const& bytes, std::size_t pos)
|
||||
{
|
||||
while (pos < bytes.size())
|
||||
{
|
||||
auto const type = bytes[pos++];
|
||||
if (type == 255) // tnTERMINAL
|
||||
return pos;
|
||||
pos += 32;
|
||||
if (type == 254) // tnREMOVE
|
||||
continue;
|
||||
if (pos + 4 > bytes.size())
|
||||
break;
|
||||
std::uint32_t size = 0;
|
||||
std::memcpy(&size, &bytes[pos], 4);
|
||||
pos += 4 + size;
|
||||
}
|
||||
return bytes.size();
|
||||
}
|
||||
|
||||
// Position of the size field of the first leaf (not a removal) in the
|
||||
// map that starts at `pos`, if the map has one.
|
||||
static std::optional<std::size_t>
|
||||
firstLeafSizeField(std::vector<std::uint8_t> const& bytes, std::size_t pos)
|
||||
{
|
||||
while (pos < bytes.size())
|
||||
{
|
||||
auto const type = bytes[pos];
|
||||
if (type == 255)
|
||||
return std::nullopt;
|
||||
if (type != 254)
|
||||
return pos + 1 + 32;
|
||||
pos += 1 + 32;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static std::vector<CatLedgerRecord>
|
||||
indexCatalogue(std::vector<std::uint8_t> const& bytes)
|
||||
{
|
||||
std::vector<CatLedgerRecord> records;
|
||||
std::size_t pos = sizeof(TestCATLHeader);
|
||||
while (pos + catLedgerHeaderSize <= bytes.size())
|
||||
{
|
||||
CatLedgerRecord r;
|
||||
r.header = pos;
|
||||
r.state = pos + catLedgerHeaderSize;
|
||||
r.tx = skipCatalogueMap(bytes, r.state);
|
||||
r.end = skipCatalogueMap(bytes, r.tx);
|
||||
records.push_back(r);
|
||||
pos = r.end;
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
static std::vector<std::uint8_t>
|
||||
readCatalogue(std::string const& path)
|
||||
{
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
return {
|
||||
std::istreambuf_iterator<char>(in),
|
||||
std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
static void
|
||||
writeCatalogue(std::string const& path, std::vector<std::uint8_t> const& b)
|
||||
{
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
out.write(reinterpret_cast<char const*>(b.data()), b.size());
|
||||
}
|
||||
|
||||
static Json::Value
|
||||
loadCatalogue(test::jtx::Env& env, std::string const& path, bool ignoreHash)
|
||||
{
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::input_file] = path;
|
||||
if (ignoreHash)
|
||||
params[jss::ignore_hash] = true;
|
||||
return env.client().invoke("catalogue_load", params)[jss::result];
|
||||
}
|
||||
|
||||
static bool
|
||||
errorContains(Json::Value const& result, std::string const& what)
|
||||
{
|
||||
return result[jss::status] == "error" &&
|
||||
result[jss::error_message].asString().find(what) !=
|
||||
std::string::npos;
|
||||
}
|
||||
|
||||
void
|
||||
testCatalogueLoadFailures(FeatureBitset features)
|
||||
{
|
||||
testcase("catalogue_load: failures part way through a file");
|
||||
using namespace test::jtx;
|
||||
using beast::severities::kDisabled;
|
||||
|
||||
boost::filesystem::path tempDir =
|
||||
boost::filesystem::temp_directory_path() /
|
||||
boost::filesystem::unique_path();
|
||||
boost::filesystem::create_directories(tempDir);
|
||||
auto const goodPath = (tempDir / "good.catl").string();
|
||||
|
||||
// An uncompressed catalogue of ledgers 3-6, so records can be edited.
|
||||
{
|
||||
Env env{*this, catalogueEnvconfig(), features, nullptr, kDisabled};
|
||||
prepareLedgerData(env, 3);
|
||||
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::min_ledger] = 3;
|
||||
params[jss::max_ledger] = 6;
|
||||
params[jss::output_file] = goodPath;
|
||||
params[jss::compression_level] = 0;
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_create", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
}
|
||||
|
||||
auto const good = readCatalogue(goodPath);
|
||||
auto const records = indexCatalogue(good);
|
||||
if (!BEAST_EXPECT(
|
||||
records.size() == 4 && records.back().end == good.size()))
|
||||
return;
|
||||
|
||||
// Write `bytes` to a new file, load it into a fresh server, and hand
|
||||
// the result and the resulting pinned set to `check`.
|
||||
int fileNo = 0;
|
||||
auto loadEdited = [&](std::vector<std::uint8_t> const& bytes,
|
||||
bool ignoreHash,
|
||||
auto&& check) {
|
||||
auto const path =
|
||||
(tempDir / ("edited" + std::to_string(++fileNo) + ".catl"))
|
||||
.string();
|
||||
writeCatalogue(path, bytes);
|
||||
Env env{*this, catalogueEnvconfig(), features, nullptr, kDisabled};
|
||||
auto const result = loadCatalogue(env, path, ignoreHash);
|
||||
check(
|
||||
result, env.app().getLedgerMaster().getPinnedLedgersRangeSet());
|
||||
};
|
||||
|
||||
auto const hashField = offsetof(TestCATLHeader, hash);
|
||||
auto const maxLedgerField = offsetof(TestCATLHeader, max_ledger);
|
||||
// Offsets within a ledger header record.
|
||||
constexpr std::size_t accountHashAt = 4 + 32 * 2;
|
||||
constexpr std::size_t dropsAt = 4 + 32 * 4;
|
||||
|
||||
// The stored checksum is wrong but every ledger is intact: all four
|
||||
// load, then the whole-file check fails and the run is rolled back.
|
||||
{
|
||||
auto bytes = good;
|
||||
bytes[hashField] ^= 0xff;
|
||||
loadEdited(
|
||||
bytes, false, [&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(
|
||||
result, "catalogue hash verification failed"));
|
||||
BEAST_EXPECT(pinned.empty());
|
||||
});
|
||||
}
|
||||
|
||||
// A ledger part way through is corrupt. The load stops there, the
|
||||
// rest of the file is still hashed, the checksum fails, and the
|
||||
// ledgers loaded before it are rolled back too.
|
||||
{
|
||||
auto bytes = good;
|
||||
bytes[records[2].header + dropsAt] ^= 0x01;
|
||||
loadEdited(
|
||||
bytes, false, [&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(
|
||||
result, "catalogue hash verification failed"));
|
||||
BEAST_EXPECT(pinned.empty());
|
||||
});
|
||||
}
|
||||
|
||||
// A checksum failure only rolls back what this run added: ledgers
|
||||
// that were already pinned stay pinned.
|
||||
{
|
||||
auto bytes = good;
|
||||
bytes[hashField] ^= 0xff;
|
||||
auto const badPath = (tempDir / "badsum.catl").string();
|
||||
writeCatalogue(badPath, bytes);
|
||||
|
||||
Env env{*this, catalogueEnvconfig(), features, nullptr, kDisabled};
|
||||
BEAST_EXPECT(
|
||||
loadCatalogue(env, goodPath, false)[jss::status] ==
|
||||
jss::success);
|
||||
BEAST_EXPECT(errorContains(
|
||||
loadCatalogue(env, badPath, false),
|
||||
"catalogue hash verification failed"));
|
||||
auto& lm = env.app().getLedgerMaster();
|
||||
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-6");
|
||||
|
||||
// The pinned store: an empty batch is a no-op, an in-memory
|
||||
// backend is durable on demand, and a fetch made for
|
||||
// online_delete's copy pass (duplicate == true) finds nodes
|
||||
// that exist only in the pinned store.
|
||||
auto* const pinnedDb = dynamic_cast<NodeStore::DatabasePinnedImp*>(
|
||||
&env.app().getNodeStore());
|
||||
if (BEAST_EXPECT(pinnedDb))
|
||||
{
|
||||
pinnedDb->storePinnedBatch({});
|
||||
BEAST_EXPECT(pinnedDb->syncPinned());
|
||||
auto const stateRoot =
|
||||
uint256::fromVoid(&good[records[0].header + accountHashAt]);
|
||||
BEAST_EXPECT(env.app().getNodeStore().fetchNodeObject(
|
||||
stateRoot, 0, NodeStore::FetchType::synchronous, true));
|
||||
}
|
||||
}
|
||||
|
||||
// Out of sequence: ledger 4's record claims to be ledger 9.
|
||||
{
|
||||
auto bytes = good;
|
||||
std::uint32_t const nine = 9;
|
||||
std::memcpy(&bytes[records[1].header], &nine, 4);
|
||||
loadEdited(
|
||||
bytes, true, [&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(result, "out of sequence"));
|
||||
BEAST_EXPECT(boost::icl::contains(pinned, 3u));
|
||||
BEAST_EXPECT(!boost::icl::contains(pinned, 4u));
|
||||
});
|
||||
}
|
||||
|
||||
// The header promises a ledger 7 the file does not contain. Ledgers
|
||||
// 3-6 load (hash checks are off) before the file runs out.
|
||||
{
|
||||
auto bytes = good;
|
||||
std::uint32_t const seven = 7;
|
||||
std::memcpy(&bytes[maxLedgerField], &seven, 4);
|
||||
loadEdited(
|
||||
bytes, true, [&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(
|
||||
result, "Unexpected end of catalogue file"));
|
||||
BEAST_EXPECT(to_string(pinned) == "3-6");
|
||||
});
|
||||
}
|
||||
|
||||
// An absurd leaf size in each kind of map is rejected while parsing.
|
||||
auto withHugeLeaf = [&](std::size_t sizeField) {
|
||||
auto bytes = good;
|
||||
std::uint32_t const huge = 0xFFFFFFFF;
|
||||
std::memcpy(&bytes[sizeField], &huge, 4);
|
||||
return bytes;
|
||||
};
|
||||
|
||||
// ... in the base ledger's state map.
|
||||
if (auto const field = firstLeafSizeField(good, records[0].state);
|
||||
BEAST_EXPECT(field))
|
||||
{
|
||||
loadEdited(
|
||||
withHugeLeaf(*field),
|
||||
true,
|
||||
[&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(
|
||||
result, "Failed to load base ledger state"));
|
||||
BEAST_EXPECT(pinned.empty());
|
||||
});
|
||||
}
|
||||
|
||||
// ... in a later ledger's state delta.
|
||||
{
|
||||
std::optional<std::size_t> index;
|
||||
std::optional<std::size_t> field;
|
||||
for (std::size_t i = 1; i < records.size() && !field; ++i)
|
||||
if ((field = firstLeafSizeField(good, records[i].state)))
|
||||
index = i;
|
||||
if (BEAST_EXPECT(field))
|
||||
{
|
||||
std::uint32_t const seq = 3 + *index;
|
||||
loadEdited(
|
||||
withHugeLeaf(*field),
|
||||
true,
|
||||
[&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(
|
||||
result, "Failed to apply ledger delta"));
|
||||
BEAST_EXPECT(boost::icl::contains(pinned, seq - 1));
|
||||
BEAST_EXPECT(!boost::icl::contains(pinned, seq));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ... in a ledger's transaction map.
|
||||
{
|
||||
std::optional<std::size_t> index;
|
||||
std::optional<std::size_t> field;
|
||||
for (std::size_t i = 0; i < records.size() && !field; ++i)
|
||||
if ((field = firstLeafSizeField(good, records[i].tx)))
|
||||
index = i;
|
||||
if (BEAST_EXPECT(field))
|
||||
{
|
||||
std::uint32_t const seq = 3 + *index;
|
||||
loadEdited(
|
||||
withHugeLeaf(*field),
|
||||
true,
|
||||
[&](auto const& result, auto const& pinned) {
|
||||
BEAST_EXPECT(errorContains(
|
||||
result, "Failed to apply ledger delta"));
|
||||
BEAST_EXPECT(!boost::icl::contains(pinned, seq));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
boost::filesystem::remove_all(tempDir);
|
||||
}
|
||||
|
||||
void
|
||||
testCataloguePinnedStoreRequired(FeatureBitset features)
|
||||
{
|
||||
testcase("catalogue_load / catalogue_unpin: pinned store required");
|
||||
using namespace test::jtx;
|
||||
using beast::severities::kDisabled;
|
||||
|
||||
// pinned_type is set, but [node_db] type=rwdb is the null node
|
||||
// store, on which SHAMapStore never builds the pinned store.
|
||||
{
|
||||
auto cfg = test::jtx::envconfig();
|
||||
auto& nodeDb = cfg->section(ConfigSection::nodeDatabase());
|
||||
nodeDb.set("pinned_type", "rwdb");
|
||||
nodeDb.set("online_delete", "256");
|
||||
Env env{*this, std::move(cfg), features, nullptr, kDisabled};
|
||||
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::input_file] = "/nonexistent.catl";
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_load", params)[jss::result];
|
||||
BEAST_EXPECT(
|
||||
errorContains(result, "requires the pinned node store"));
|
||||
}
|
||||
|
||||
// catalogue_unpin needs pinned_type configured at all.
|
||||
{
|
||||
Env env{
|
||||
*this,
|
||||
catalogueEnvconfig({.withPinnedType = false}),
|
||||
features,
|
||||
nullptr,
|
||||
kDisabled};
|
||||
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::ledger_index_min] = 1;
|
||||
params[jss::ledger_index_max] = 2;
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_unpin", params)[jss::result];
|
||||
BEAST_EXPECT(errorContains(result, "pinned_type"));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testCatalogueChainLinkage(FeatureBitset features)
|
||||
{
|
||||
testcase("catalogue_load: Ledgers must chain");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{
|
||||
*this,
|
||||
catalogueEnvconfig(),
|
||||
features,
|
||||
nullptr,
|
||||
beast::severities::kDisabled,
|
||||
};
|
||||
prepareLedgerData(env, 3);
|
||||
|
||||
boost::filesystem::path tempDir =
|
||||
boost::filesystem::temp_directory_path() /
|
||||
boost::filesystem::unique_path();
|
||||
boost::filesystem::create_directories(tempDir);
|
||||
auto cataloguePath = (tempDir / "test.catl").string();
|
||||
|
||||
// Uncompressed, so ledger records can be located and edited.
|
||||
{
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::min_ledger] = 3;
|
||||
params[jss::max_ledger] = 6;
|
||||
params[jss::output_file] = cataloguePath;
|
||||
params[jss::compression_level] = 0;
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_create", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> bytes;
|
||||
{
|
||||
std::ifstream in(cataloguePath, std::ios::binary);
|
||||
bytes.assign(
|
||||
std::istreambuf_iterator<char>(in),
|
||||
std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
// Ledger record: seq, hash, txHash, accountHash, parentHash, drops,
|
||||
// closeFlags, closeTimeResolution, closeTime, parentCloseTime; then
|
||||
// the state map and tx map as leaf records ending in tnTERMINAL.
|
||||
constexpr std::size_t ledgerHeaderSize =
|
||||
4 + 32 * 4 + 8 + sizeof(int) + 4 + 8 + 8;
|
||||
auto skipMap = [&](std::size_t pos) -> std::size_t {
|
||||
while (pos < bytes.size())
|
||||
{
|
||||
auto const type = bytes[pos++];
|
||||
if (type == 255) // tnTERMINAL
|
||||
return pos;
|
||||
pos += 32;
|
||||
if (type == 254) // tnREMOVE
|
||||
continue;
|
||||
if (pos + 4 > bytes.size())
|
||||
break;
|
||||
std::uint32_t size = 0;
|
||||
std::memcpy(&size, &bytes[pos], 4);
|
||||
pos += 4 + size;
|
||||
}
|
||||
return bytes.size();
|
||||
};
|
||||
|
||||
// Skip the base ledger (3) to reach ledger 4.
|
||||
std::size_t pos = sizeof(TestCATLHeader) + ledgerHeaderSize;
|
||||
pos = skipMap(skipMap(pos));
|
||||
if (!BEAST_EXPECT(pos + ledgerHeaderSize <= bytes.size()))
|
||||
return;
|
||||
|
||||
LedgerInfo info;
|
||||
std::size_t p = pos;
|
||||
auto take = [&](void* dst, std::size_t n) {
|
||||
std::memcpy(dst, &bytes[p], n);
|
||||
p += n;
|
||||
};
|
||||
std::uint64_t drops = 0, closeTime = 0, parentCloseTime = 0;
|
||||
std::uint32_t resolution = 0;
|
||||
take(&info.seq, 4);
|
||||
std::size_t const hashOffset = p;
|
||||
take(info.hash.data(), 32);
|
||||
take(info.txHash.data(), 32);
|
||||
take(info.accountHash.data(), 32);
|
||||
std::size_t const parentOffset = p;
|
||||
take(info.parentHash.data(), 32);
|
||||
take(&drops, 8);
|
||||
take(&info.closeFlags, sizeof(info.closeFlags));
|
||||
take(&resolution, 4);
|
||||
take(&closeTime, 8);
|
||||
take(&parentCloseTime, 8);
|
||||
info.drops = drops;
|
||||
info.closeTime = NetClock::time_point{NetClock::duration{closeTime}};
|
||||
info.parentCloseTime =
|
||||
NetClock::time_point{NetClock::duration{parentCloseTime}};
|
||||
info.closeTimeResolution = NetClock::duration{resolution};
|
||||
|
||||
BEAST_EXPECT(info.seq == 4);
|
||||
// Sanity: the record parses to a ledger whose hash checks out.
|
||||
BEAST_EXPECT(calculateLedgerHash(info) == info.hash);
|
||||
|
||||
// Re-parent ledger 4 and fix up its hash, so the file stays
|
||||
// internally consistent ledger by ledger; only the chain check can
|
||||
// catch it.
|
||||
info.parentHash = ~info.parentHash;
|
||||
info.hash = calculateLedgerHash(info);
|
||||
std::memcpy(&bytes[parentOffset], info.parentHash.data(), 32);
|
||||
std::memcpy(&bytes[hashOffset], info.hash.data(), 32);
|
||||
{
|
||||
std::ofstream out(
|
||||
cataloguePath, std::ios::binary | std::ios::trunc);
|
||||
out.write(
|
||||
reinterpret_cast<char const*>(bytes.data()), bytes.size());
|
||||
}
|
||||
|
||||
Env loadEnv{
|
||||
*this,
|
||||
catalogueEnvconfig(),
|
||||
features,
|
||||
nullptr,
|
||||
beast::severities::kDisabled,
|
||||
};
|
||||
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::input_file] = cataloguePath;
|
||||
params[jss::ignore_hash] = true; // the file was edited
|
||||
auto const result =
|
||||
loadEnv.client().invoke("catalogue_load", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == "error");
|
||||
BEAST_EXPECT(
|
||||
result[jss::error_message].asString().find(
|
||||
"does not chain to its predecessor") != std::string::npos);
|
||||
|
||||
// The base ledger verified and stays pinned; nothing after it does.
|
||||
auto const pinned =
|
||||
loadEnv.app().getLedgerMaster().getPinnedLedgersRangeSet();
|
||||
BEAST_EXPECT(boost::icl::contains(pinned, 3u));
|
||||
BEAST_EXPECT(!boost::icl::contains(pinned, 4u));
|
||||
|
||||
boost::filesystem::remove_all(tempDir);
|
||||
}
|
||||
|
||||
void
|
||||
testCatalogueUnpin(FeatureBitset features)
|
||||
{
|
||||
testcase("catalogue_unpin");
|
||||
using namespace test::jtx;
|
||||
|
||||
Env env{
|
||||
*this,
|
||||
catalogueEnvconfig(),
|
||||
features,
|
||||
nullptr,
|
||||
beast::severities::kDisabled,
|
||||
};
|
||||
prepareLedgerData(env, 5);
|
||||
|
||||
boost::filesystem::path tempDir =
|
||||
boost::filesystem::temp_directory_path() /
|
||||
boost::filesystem::unique_path();
|
||||
boost::filesystem::create_directories(tempDir);
|
||||
auto cataloguePath = (tempDir / "test.catl").string();
|
||||
|
||||
{
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::min_ledger] = 3;
|
||||
params[jss::max_ledger] = 8;
|
||||
params[jss::output_file] = cataloguePath;
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_create", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
}
|
||||
{
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::input_file] = cataloguePath;
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_load", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
}
|
||||
|
||||
auto& lm = env.app().getLedgerMaster();
|
||||
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-8");
|
||||
|
||||
auto unpin = [&](std::optional<Json::Value> min,
|
||||
std::optional<Json::Value> max) {
|
||||
Json::Value params{Json::objectValue};
|
||||
if (min)
|
||||
params[jss::ledger_index_min] = *min;
|
||||
if (max)
|
||||
params[jss::ledger_index_max] = *max;
|
||||
return env.client().invoke("catalogue_unpin", params)[jss::result];
|
||||
};
|
||||
|
||||
// Bad parameters
|
||||
BEAST_EXPECT(unpin({}, {})[jss::status] == "error");
|
||||
BEAST_EXPECT(unpin(5, {})[jss::status] == "error");
|
||||
BEAST_EXPECT(unpin(6, 5)[jss::status] == "error");
|
||||
BEAST_EXPECT(unpin(-1, 5)[jss::status] == "error");
|
||||
BEAST_EXPECT(unpin("x", 5)[jss::status] == "error");
|
||||
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-8");
|
||||
|
||||
// Cut a hole
|
||||
{
|
||||
auto const result = unpin(5, 6);
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
BEAST_EXPECT(result[jss::unpinned] == "5-6");
|
||||
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "3-4,7-8");
|
||||
BEAST_EXPECT(lm.isPinned(4) && lm.isPinned(7));
|
||||
BEAST_EXPECT(!lm.isPinned(5) && !lm.isPinned(6));
|
||||
}
|
||||
|
||||
// A range only partly pinned unpins just the pinned part
|
||||
{
|
||||
auto const result = unpin(1, 4);
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
BEAST_EXPECT(result[jss::unpinned] == "3-4");
|
||||
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "7-8");
|
||||
}
|
||||
|
||||
// Nothing pinned in range: success, nothing unpinned. (Unsigned
|
||||
// JSON values are accepted as well as non-negative signed ones.)
|
||||
{
|
||||
auto const result = unpin(Json::UInt(5), Json::UInt(6));
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
BEAST_EXPECT(result[jss::unpinned] == "empty");
|
||||
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "7-8");
|
||||
}
|
||||
|
||||
// Unpinning the rest leaves nothing pinned. (state.db then records
|
||||
// an empty set, which must not stop the next start; see
|
||||
// SHAMapStore_test.)
|
||||
{
|
||||
auto const result = unpin(1, 100);
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
BEAST_EXPECT(result[jss::unpinned] == "7-8");
|
||||
BEAST_EXPECT(result[jss::complete_ledgers_pinned] == "empty");
|
||||
BEAST_EXPECT(lm.getPinnedLedgersRangeSet().empty());
|
||||
}
|
||||
|
||||
// Re-loading the file pins it again
|
||||
{
|
||||
Json::Value params{Json::objectValue};
|
||||
params[jss::input_file] = cataloguePath;
|
||||
auto const result =
|
||||
env.client().invoke("catalogue_load", params)[jss::result];
|
||||
BEAST_EXPECT(result[jss::status] == jss::success);
|
||||
BEAST_EXPECT(to_string(lm.getPinnedLedgersRangeSet()) == "3-8");
|
||||
}
|
||||
|
||||
boost::filesystem::remove_all(tempDir);
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
@@ -1081,6 +1716,10 @@ public:
|
||||
testCatalogueFileSize(all);
|
||||
testCatalogueCompression(all);
|
||||
testCatalogueStatus(all);
|
||||
testCatalogueChainLinkage(all);
|
||||
testCatalogueUnpin(all);
|
||||
testCatalogueLoadFailures(all);
|
||||
testCataloguePinnedStoreRequired(all);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <test/jtx.h>
|
||||
#include <xrpld/core/Config.h>
|
||||
#include <xrpl/hook/Enum.h>
|
||||
#include <xrpl/json/to_string.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
// A valid hook exporting both hook() and cbak() (emit test hook).
|
||||
static char const validHex[] =
|
||||
"0061736D0100000001250660027F7F017F60037F7F7E017E60017F017E600001"
|
||||
"7E60027F7F017E60047F7F7F7F017E028F010903656E76025F67000003656E76"
|
||||
"06616363657074000103656E760C6574786E5F72657365727665000203656E76"
|
||||
"0A6C65646765725F736571000303656E760C686F6F6B5F6163636F756E740004"
|
||||
"03656E760C6574786E5F64657461696C73000403656E760D6574786E5F666565"
|
||||
"5F62617365000403656E7604656D6974000503656E7608726F6C6C6261636B00"
|
||||
"01030302020205030100020627067F0141908A040B7F0041820A0B7F00418008"
|
||||
"0B7F0041908A040B7F004180080B7F004180080B070F02046362616B00090468"
|
||||
"6F6F6B000A0AA883000299800000410141011080808080001A41004100420010"
|
||||
"81808080000B88830002037F017E23808080800041206B220124808080800041"
|
||||
"0141011080808080001A41011082808080001A4100108380808000A722024105"
|
||||
"6A22034118742003410874418080FC07717220034108764180FE037120034118"
|
||||
"767272360295888080004100200241016A22034118742003410874418080FC07"
|
||||
"717220034108764180FE03712003411876727236028F8880800041C788808000"
|
||||
"41141084808080001A41DB88808000418A011085808080001A41004180888080"
|
||||
"0041E50110868080800022043C00A188808000410020044208883C00A0888080"
|
||||
"00410020044210883C009F88808000410020044218883C009E88808000410020"
|
||||
"044220883C009D88808000410020044228883C009C8880800041002004423088"
|
||||
"3C009B8880800041002004423888A7413F7141C000723A009A88808000024002"
|
||||
"402001412041808880800041E501108780808000427F550D0041E58980800041"
|
||||
"0D42D30010888080800021040C010B41F289808000411042D400108180808000"
|
||||
"21040B200141206A24808080800020040B0B900202004180080BE50112006322"
|
||||
"000000002400000000201A00000000201B000000006840000000000000007321"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0081140000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"0000000000000000000000000000000000000000000000000000000000000000"
|
||||
"000041E5090B1D456D6974206661696C65642E00456D69742073756363656564"
|
||||
"65642E00";
|
||||
|
||||
class HookValidate_test : public beast::unit_test::suite
|
||||
{
|
||||
static Json::Value
|
||||
call(test::jtx::Env& env, std::string const& code)
|
||||
{
|
||||
Json::Value params(Json::objectValue);
|
||||
params[jss::code] = code;
|
||||
return env.rpc("json", "hook_validate", to_string(params))[jss::result];
|
||||
}
|
||||
|
||||
static std::unique_ptr<Config>
|
||||
withRpc(std::unique_ptr<Config> cfg, bool enabled)
|
||||
{
|
||||
cfg->HOOK_VALIDATE_RPC = enabled;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void
|
||||
testDisabled()
|
||||
{
|
||||
testcase("Disabled");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, envconfig(withRpc, false)};
|
||||
auto const r = call(env, validHex);
|
||||
BEAST_EXPECT(r[jss::error] == "notSupported");
|
||||
}
|
||||
|
||||
void
|
||||
testParams()
|
||||
{
|
||||
testcase("Parameter validation");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, envconfig(withRpc, true)};
|
||||
|
||||
{
|
||||
// missing code
|
||||
auto const r = env.rpc("json", "hook_validate", "{}")[jss::result];
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
}
|
||||
{
|
||||
// code not a string
|
||||
auto const r =
|
||||
env.rpc("json", "hook_validate", "{\"code\": 1}")[jss::result];
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
}
|
||||
{
|
||||
// empty, odd-length and non-hex input
|
||||
for (auto const& bad : {"", "0", "0061736Z"})
|
||||
{
|
||||
auto const r = call(env, bad);
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
}
|
||||
}
|
||||
{
|
||||
// one byte over the maximum hook size
|
||||
auto const r =
|
||||
call(env, std::string(2 * (hook::maxHookWasmSize() + 1), '0'));
|
||||
BEAST_EXPECT(r[jss::error] == "invalidParams");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testValidate()
|
||||
{
|
||||
testcase("Validate");
|
||||
using namespace test::jtx;
|
||||
Env env{*this, envconfig(withRpc, true)};
|
||||
|
||||
{
|
||||
auto const r = call(env, validHex);
|
||||
BEAST_EXPECT(r[jss::valid] == true);
|
||||
BEAST_EXPECT(r.isMember(jss::instruction_count_hook));
|
||||
BEAST_EXPECT(r.isMember(jss::instruction_count_cbak));
|
||||
BEAST_EXPECT(!r.isMember(jss::vm_error));
|
||||
BEAST_EXPECT(r[jss::log].isArray());
|
||||
}
|
||||
{
|
||||
// the lowercase hex the toolchains emit is accepted too
|
||||
std::string lower = validHex;
|
||||
for (auto& c : lower)
|
||||
c = std::tolower(static_cast<unsigned char>(c));
|
||||
auto const r = call(env, lower);
|
||||
BEAST_EXPECT(r[jss::valid] == true);
|
||||
}
|
||||
{
|
||||
// not webassembly
|
||||
auto const r = call(env, std::string(140, '0'));
|
||||
BEAST_EXPECT(r[jss::valid] == false);
|
||||
BEAST_EXPECT(!r.isMember(jss::instruction_count_hook));
|
||||
BEAST_EXPECT(r[jss::log].isArray() && r[jss::log].size() > 0);
|
||||
}
|
||||
{
|
||||
std::string bad = validHex;
|
||||
bad[2] = 'F'; // corrupt magic header
|
||||
auto const r = call(env, bad);
|
||||
BEAST_EXPECT(r[jss::valid] == false);
|
||||
BEAST_EXPECT(r[jss::log].isArray() && r[jss::log].size() > 0);
|
||||
}
|
||||
{
|
||||
// truncated module
|
||||
std::string const truncated = std::string(validHex).substr(0, 200);
|
||||
auto const r = call(env, truncated);
|
||||
BEAST_EXPECT(r[jss::valid] == false);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testConfigDefault()
|
||||
{
|
||||
testcase("Config default");
|
||||
auto load = [](bool standalone, char const* content) {
|
||||
Config cfg;
|
||||
cfg.setupControl(true, true, standalone);
|
||||
cfg.loadFromString(content);
|
||||
return cfg.HOOK_VALIDATE_RPC;
|
||||
};
|
||||
BEAST_EXPECT(load(true, ""));
|
||||
BEAST_EXPECT(!load(true, "[hook_validate_rpc]\n0\n"));
|
||||
BEAST_EXPECT(!load(false, ""));
|
||||
BEAST_EXPECT(load(false, "[hook_validate_rpc]\n1\n"));
|
||||
}
|
||||
|
||||
void
|
||||
run() override
|
||||
{
|
||||
testConfigDefault();
|
||||
testDisabled();
|
||||
testParams();
|
||||
testValidate();
|
||||
}
|
||||
};
|
||||
|
||||
BEAST_DEFINE_TESTSUITE(HookValidate, rpc, ripple);
|
||||
|
||||
} // namespace ripple
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1934,6 +1934,56 @@ static RPCCallTestData const rpcCallTestArray[] = {
|
||||
RPCCallTestData::bad_cast,
|
||||
R"()"},
|
||||
|
||||
// catalogue_unpin
|
||||
// ------------------------------------------------------------
|
||||
{"catalogue_unpin: minimal.",
|
||||
__LINE__,
|
||||
{"catalogue_unpin", "5000001", "6000000"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "catalogue_unpin",
|
||||
"params" : [
|
||||
{
|
||||
"api_version" : %API_VER%,
|
||||
"ledger_index_max" : 6000000,
|
||||
"ledger_index_min" : 5000001
|
||||
}
|
||||
]
|
||||
})"},
|
||||
{"catalogue_unpin: too few arguments.",
|
||||
__LINE__,
|
||||
{"catalogue_unpin", "5000001"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "catalogue_unpin",
|
||||
"params" : [
|
||||
{
|
||||
"error" : "badSyntax",
|
||||
"error_code" : 1,
|
||||
"error_message" : "Syntax error."
|
||||
}
|
||||
]
|
||||
})"},
|
||||
{"catalogue_unpin: too many arguments.",
|
||||
__LINE__,
|
||||
{"catalogue_unpin", "1", "2", "3"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "catalogue_unpin",
|
||||
"params" : [
|
||||
{
|
||||
"error" : "badSyntax",
|
||||
"error_code" : 1,
|
||||
"error_message" : "Syntax error."
|
||||
}
|
||||
]
|
||||
})"},
|
||||
{"catalogue_unpin: non-integer bound.",
|
||||
__LINE__,
|
||||
{"catalogue_unpin", "first", "6000000"},
|
||||
RPCCallTestData::bad_cast,
|
||||
R"()"},
|
||||
|
||||
// channel_authorize
|
||||
// -----------------------------------------------------------
|
||||
{"channel_authorize: minimal.",
|
||||
@@ -2998,6 +3048,50 @@ static RPCCallTestData const rpcCallTestArray[] = {
|
||||
RPCCallTestData::bad_cast,
|
||||
R"()"},
|
||||
|
||||
// hook_validate
|
||||
// ------------------------------------------------------------------------
|
||||
{"hook_validate: minimal.",
|
||||
__LINE__,
|
||||
{"hook_validate", "0061736D01000000"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "hook_validate",
|
||||
"params" : [
|
||||
{
|
||||
"api_version" : %API_VER%,
|
||||
"code" : "0061736D01000000"
|
||||
}
|
||||
]
|
||||
})"},
|
||||
{"hook_validate: too few arguments.",
|
||||
__LINE__,
|
||||
{"hook_validate"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "hook_validate",
|
||||
"params" : [
|
||||
{
|
||||
"error" : "badSyntax",
|
||||
"error_code" : 1,
|
||||
"error_message" : "Syntax error."
|
||||
}
|
||||
]
|
||||
})"},
|
||||
{"hook_validate: too many arguments.",
|
||||
__LINE__,
|
||||
{"hook_validate", "00", "00"},
|
||||
RPCCallTestData::no_exception,
|
||||
R"({
|
||||
"method" : "hook_validate",
|
||||
"params" : [
|
||||
{
|
||||
"error" : "badSyntax",
|
||||
"error_code" : 1,
|
||||
"error_message" : "Syntax error."
|
||||
}
|
||||
]
|
||||
})"},
|
||||
|
||||
// json
|
||||
// ------------------------------------------------------------------------
|
||||
{"json: minimal.",
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
#include <boost/utility/in_place_factory.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
@@ -158,6 +160,7 @@ public:
|
||||
onUDPMessage(
|
||||
std::string const& message,
|
||||
boost::asio::ip::tcp::endpoint const& remoteEndpoint,
|
||||
Port const& port,
|
||||
std::function<void(std::string const&)> sendResponse)
|
||||
{
|
||||
}
|
||||
@@ -371,6 +374,7 @@ public:
|
||||
onUDPMessage(
|
||||
std::string const& message,
|
||||
boost::asio::ip::tcp::endpoint const& remoteEndpoint,
|
||||
Port const& port,
|
||||
std::function<void(std::string const&)> sendResponse)
|
||||
{
|
||||
}
|
||||
@@ -404,6 +408,96 @@ public:
|
||||
pass();
|
||||
}
|
||||
|
||||
// Records the datagrams delivered to onUDPMessage
|
||||
struct UDPHandler : TestHandler
|
||||
{
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::vector<std::pair<std::string, bool>> received;
|
||||
|
||||
void
|
||||
onUDPMessage(
|
||||
std::string const& message,
|
||||
boost::asio::ip::tcp::endpoint const& remoteEndpoint,
|
||||
Port const& port,
|
||||
std::function<void(std::string const&)> sendResponse)
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
received.emplace_back(message, port.has_peer());
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
bool
|
||||
wait(std::size_t count, std::chrono::milliseconds timeout)
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
return cv.wait_for(
|
||||
lock, timeout, [&] { return received.size() >= count; });
|
||||
}
|
||||
};
|
||||
|
||||
// A peer port with udp_highway set also accepts UDP datagrams on the
|
||||
// same port number; one without it does not.
|
||||
void
|
||||
udpHighwayTests()
|
||||
{
|
||||
testcase("UDP Superhighway door");
|
||||
using namespace std::chrono_literals;
|
||||
using udp = boost::asio::ip::udp;
|
||||
|
||||
TestSink sink{*this};
|
||||
TestThread thread;
|
||||
sink.threshold(beast::severities::Severity::kAll);
|
||||
beast::Journal journal{sink};
|
||||
|
||||
auto const addr = boost::asio::ip::make_address(getEnvLocalhostAddr());
|
||||
|
||||
auto run = [&](bool highway) {
|
||||
UDPHandler handler;
|
||||
auto s = make_Server(handler, thread.get_io_context(), journal);
|
||||
|
||||
std::vector<Port> ports(1);
|
||||
ports.back().name = "peer";
|
||||
ports.back().ip = addr;
|
||||
ports.back().port = 0;
|
||||
ports.back().protocol.insert("peer");
|
||||
ports.back().udp_highway = highway;
|
||||
|
||||
auto const eps = s->ports(ports);
|
||||
if (!BEAST_EXPECT(eps.size() == 1))
|
||||
return;
|
||||
|
||||
// The TCP door picked the port number; the UDP door shares it.
|
||||
auto const port = eps.begin()->second.port();
|
||||
BEAST_EXPECT(port != 0);
|
||||
|
||||
boost::asio::io_context io;
|
||||
udp::socket client(io, udp::endpoint(addr, 0));
|
||||
std::string const message = "XUSHTEST";
|
||||
client.send_to(
|
||||
boost::asio::buffer(message), udp::endpoint(addr, port));
|
||||
|
||||
if (highway)
|
||||
{
|
||||
if (BEAST_EXPECT(handler.wait(1, 5s)))
|
||||
{
|
||||
std::lock_guard lock(handler.mutex);
|
||||
BEAST_EXPECT(handler.received.front().first == message);
|
||||
BEAST_EXPECT(handler.received.front().second);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
BEAST_EXPECT(!handler.wait(1, 200ms));
|
||||
}
|
||||
|
||||
s = nullptr;
|
||||
};
|
||||
|
||||
run(true);
|
||||
run(false);
|
||||
}
|
||||
|
||||
void
|
||||
testBadConfig()
|
||||
{
|
||||
@@ -547,6 +641,7 @@ public:
|
||||
{
|
||||
basicTests();
|
||||
stressTest();
|
||||
udpHighwayTests();
|
||||
testBadConfig();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
namespace tests {
|
||||
@@ -131,6 +133,7 @@ public:
|
||||
testCanonicalizeMergesInnerChildren(journal);
|
||||
testCanonicalizeSharesDuplicateContent(journal);
|
||||
testGetLinkedNode(journal);
|
||||
testUpsertAndFlushToSink(journal);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -284,6 +287,82 @@ public:
|
||||
BEAST_EXPECT(liveLinked > 0);
|
||||
}
|
||||
|
||||
void
|
||||
testUpsertAndFlushToSink(beast::Journal const& journal)
|
||||
{
|
||||
testcase("upsertGiveItem and flushDirty to a sink");
|
||||
|
||||
tests::TestNodeFamily f(journal);
|
||||
SHAMap a{SHAMapType::FREE, f}; // built with upsertGiveItem
|
||||
SHAMap b{SHAMapType::FREE, f}; // reference: addItem/updateGiveItem
|
||||
|
||||
// Distinct first nibbles: a root with three leaf children.
|
||||
constexpr uint256 h1(
|
||||
"092891fe4ef6cee585fdc6fda0e09eb4d386363158ec3321b8123e5a772c6ca7");
|
||||
constexpr uint256 h2(
|
||||
"436ccbac3347baa1f1e53baeef1f43334da88f1f6d70d963b833afd6dfa289fe");
|
||||
constexpr uint256 h3(
|
||||
"b92891fe4ef6cee585fdc6fda1e09eb4d386363158ec3321b8123e5a772c6ca8");
|
||||
auto const nm = SHAMapNodeType::tnTRANSACTION_NM;
|
||||
|
||||
// Absent keys are added.
|
||||
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h1, IntToVUC(1))));
|
||||
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h2, IntToVUC(2))));
|
||||
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h3, IntToVUC(3))));
|
||||
// A present key has its data replaced.
|
||||
BEAST_EXPECT(a.upsertGiveItem(nm, make_shamapitem(h2, IntToVUC(20))));
|
||||
// A change of leaf type is refused and leaves the item untouched.
|
||||
BEAST_EXPECT(!a.upsertGiveItem(
|
||||
SHAMapNodeType::tnTRANSACTION_MD,
|
||||
make_shamapitem(h1, IntToVUC(10))));
|
||||
|
||||
BEAST_EXPECT(b.addItem(nm, make_shamapitem(h1, IntToVUC(1))));
|
||||
BEAST_EXPECT(b.addItem(nm, make_shamapitem(h2, IntToVUC(2))));
|
||||
BEAST_EXPECT(b.addItem(nm, make_shamapitem(h3, IntToVUC(3))));
|
||||
BEAST_EXPECT(b.updateGiveItem(nm, make_shamapitem(h2, IntToVUC(20))));
|
||||
|
||||
{
|
||||
auto const v20 = IntToVUC(20);
|
||||
auto const& item = a.peekItem(h2);
|
||||
BEAST_EXPECT(
|
||||
item && item->slice() == Slice(v20.data(), v20.size()));
|
||||
auto const v1 = IntToVUC(1);
|
||||
auto const& item1 = a.peekItem(h1);
|
||||
BEAST_EXPECT(
|
||||
item1 && item1->slice() == Slice(v1.data(), v1.size()));
|
||||
}
|
||||
|
||||
// Flush to a sink. Do not call getHash() on `a` first: it unshares
|
||||
// the map and the flush would then have nothing to write.
|
||||
auto const cacheBefore = f.getTreeNodeCache()->size();
|
||||
std::vector<uint256> written;
|
||||
bool sinkArgsOk = true;
|
||||
SHAMap::FlushSink const sink =
|
||||
[&](NodeObjectType t, Blob&& data, uint256 const& hash) {
|
||||
sinkArgsOk =
|
||||
sinkArgsOk && t == hotTRANSACTION_NODE && !data.empty();
|
||||
written.push_back(hash);
|
||||
};
|
||||
a.flushDirty(hotTRANSACTION_NODE, sink);
|
||||
|
||||
BEAST_EXPECT(sinkArgsOk);
|
||||
BEAST_EXPECT(written.size() == 4); // root + three leaves
|
||||
// Unlike flushDirty(t), nothing was canonicalized into the cache.
|
||||
BEAST_EXPECT(f.getTreeNodeCache()->size() == cacheBefore);
|
||||
|
||||
// The upserted map matches the reference, and its root was written.
|
||||
BEAST_EXPECT(a.getHash() == b.getHash());
|
||||
BEAST_EXPECT(
|
||||
std::find(
|
||||
written.begin(), written.end(), a.getHash().as_uint256()) !=
|
||||
written.end());
|
||||
|
||||
// Everything is now shared: a second flush writes nothing.
|
||||
written.clear();
|
||||
a.flushDirty(hotTRANSACTION_NODE, sink);
|
||||
BEAST_EXPECT(written.empty());
|
||||
}
|
||||
|
||||
void
|
||||
testGetLinkedNode(beast::Journal const& journal)
|
||||
{
|
||||
|
||||
@@ -41,8 +41,11 @@
|
||||
#include <xrpl/basics/random.h>
|
||||
#include <xrpl/beast/core/LexicalCast.h>
|
||||
#include <xrpl/beast/utility/instrumentation.h>
|
||||
#include <xrpl/beast/utility/rngfill.h>
|
||||
#include <xrpl/crypto/csprng.h>
|
||||
#include <xrpl/protocol/BuildInfo.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/digest.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -348,6 +351,10 @@ RCLConsensus::Adaptor::onClose(
|
||||
make_shamapitem(tx.first->getTransactionID(), s.slice()));
|
||||
}
|
||||
|
||||
// featureRNG: our entropy contribution, if we add one. It is not tracked
|
||||
// by the censorship detector: it is only valid for this ledger.
|
||||
std::optional<uint256> entropyTxID;
|
||||
|
||||
// Keep enough validation history for the next flag ledger's score table
|
||||
// even when we don't vote this round (not proposing / wrong LCL).
|
||||
// Otherwise toKeep_ goes stale, validations older than
|
||||
@@ -389,6 +396,26 @@ RCLConsensus::Adaptor::onClose(
|
||||
app_.getValidations(),
|
||||
initialSet);
|
||||
}
|
||||
|
||||
// featureRNG: our entropy contribution goes into our own initial
|
||||
// position, never the open ledger, so it only becomes public once our
|
||||
// transaction set for this ledger is fixed.
|
||||
if (prevLedger->rules().enabled(featureRNG))
|
||||
{
|
||||
if (auto const entropyTx = makeEntropyTx(prevLedger))
|
||||
{
|
||||
Serializer s;
|
||||
entropyTx->add(s);
|
||||
entropyTxID = entropyTx->getTransactionID();
|
||||
if (!initialSet->addItem(
|
||||
SHAMapNodeType::tnTRANSACTION_NM,
|
||||
make_shamapitem(*entropyTxID, s.slice())))
|
||||
{
|
||||
entropyTxID.reset();
|
||||
JLOG(j_.warn()) << "RNG: failed to add entropy txn";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now we need an immutable snapshot
|
||||
@@ -400,9 +427,10 @@ RCLConsensus::Adaptor::onClose(
|
||||
RCLCensorshipDetector<TxID, LedgerIndex>::TxIDSeqVec proposed;
|
||||
|
||||
initialSet->visitLeaves(
|
||||
[&proposed,
|
||||
seq](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
proposed.emplace_back(item->key(), seq);
|
||||
[&proposed, &entropyTxID, seq](
|
||||
boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
if (item->key() != entropyTxID)
|
||||
proposed.emplace_back(item->key(), seq);
|
||||
});
|
||||
|
||||
censorshipDetector_.propose(std::move(proposed));
|
||||
@@ -422,6 +450,99 @@ RCLConsensus::Adaptor::onClose(
|
||||
validatorKeys_.nodeID}};
|
||||
}
|
||||
|
||||
std::optional<STTx>
|
||||
RCLConsensus::Adaptor::makeEntropyTx(
|
||||
std::shared_ptr<Ledger const> const& prevLedger)
|
||||
{
|
||||
if (!validatorKeys_.keys)
|
||||
return std::nullopt;
|
||||
|
||||
auto const& masterKey = validatorKeys_.keys->masterPublicKey;
|
||||
|
||||
// The commitment the ledger currently holds for us, if any.
|
||||
std::optional<uint256> commitment;
|
||||
if (auto const sle = prevLedger->read(keylet::random()))
|
||||
{
|
||||
for (auto const& entry : sle->getFieldArray(sfRandomDigests))
|
||||
{
|
||||
if (makeSlice(entry.getFieldVL(sfPublicKey)) == masterKey.slice())
|
||||
{
|
||||
commitment = entry.getFieldH256(sfNextRandomDigest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We only ever hold the secret from our last contribution, which has
|
||||
// never been published. Reveal it iff the ledger holds its commitment.
|
||||
// Otherwise (first contribution, restart, or our last contribution was
|
||||
// not included) we only commit this time, and our previous secret,
|
||||
// whatever became of it, is never used again.
|
||||
std::optional<uint256> reveal;
|
||||
if (commitment && rngSecret_ && rngSecret_->first == *commitment)
|
||||
reveal = rngSecret_->second;
|
||||
|
||||
uint256 secret;
|
||||
beast::rngfill(secret.begin(), secret.size(), crypto_prng());
|
||||
uint256 const nextCommitment = sha512Half(secret);
|
||||
rngSecret_.emplace(nextCommitment, secret);
|
||||
|
||||
auto const seq = prevLedger->info().seq + 1;
|
||||
|
||||
JLOG(j_.debug()) << "RNG: entropy txn for ledger " << seq
|
||||
<< (reveal ? " (reveal and commit)" : " (commit only)");
|
||||
|
||||
return STTx(ttENTROPY, [&](auto& obj) {
|
||||
obj.setFieldU32(sfLedgerSequence, seq);
|
||||
obj.setFieldVL(sfPublicKey, masterKey.slice());
|
||||
obj.setFieldH256(sfNextRandomDigest, nextCommitment);
|
||||
if (reveal)
|
||||
obj.setFieldH256(sfRandomData, *reveal);
|
||||
});
|
||||
}
|
||||
|
||||
std::optional<NodeID>
|
||||
RCLConsensus::Adaptor::forcedTxOwner(
|
||||
RCLCxTx const& tx,
|
||||
RCLCxLedger const& prevLedger) const
|
||||
{
|
||||
if (!prevLedger.ledger_ || !prevLedger.ledger_->rules().enabled(featureRNG))
|
||||
return std::nullopt;
|
||||
|
||||
try
|
||||
{
|
||||
SerialIter sit(tx.tx_->slice());
|
||||
STTx const stx(sit);
|
||||
|
||||
if (stx.getTxnType() != ttENTROPY ||
|
||||
stx.getFieldU32(sfLedgerSequence) != prevLedger.seq() + 1)
|
||||
return std::nullopt;
|
||||
|
||||
auto const pk = stx.getFieldVL(sfPublicKey);
|
||||
if (!publicKeyType(makeSlice(pk)))
|
||||
return std::nullopt;
|
||||
|
||||
// Only the exact bytes makeEntropyTx would build, so an owner cannot
|
||||
// force extra fields (memos and the like) in with its contribution.
|
||||
STTx const canonical(ttENTROPY, [&](auto& obj) {
|
||||
obj.setFieldU32(sfLedgerSequence, prevLedger.seq() + 1);
|
||||
obj.setFieldVL(sfPublicKey, pk);
|
||||
obj.setFieldH256(
|
||||
sfNextRandomDigest, stx.getFieldH256(sfNextRandomDigest));
|
||||
if (stx.isFieldPresent(sfRandomData))
|
||||
obj.setFieldH256(sfRandomData, stx.getFieldH256(sfRandomData));
|
||||
});
|
||||
if (canonical.getTransactionID() != tx.id())
|
||||
return std::nullopt;
|
||||
|
||||
return calcNodeID(PublicKey(makeSlice(pk)));
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
RCLConsensus::Adaptor::onForceAccept(
|
||||
Result const& result,
|
||||
|
||||
@@ -34,10 +34,12 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/RippleLedgerHash.h>
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/STValidation.h>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
@@ -91,6 +93,11 @@ class RCLConsensus
|
||||
RCLCensorshipDetector<TxID, LedgerIndex> censorshipDetector_;
|
||||
NegativeUNLVote nUnlVote_;
|
||||
|
||||
// featureRNG: the secret behind the commitment in our most recent
|
||||
// entropy contribution, as {commitment, secret}. Only touched by
|
||||
// onClose, which runs under the consensus lock.
|
||||
std::optional<std::pair<uint256, uint256>> rngSecret_;
|
||||
|
||||
public:
|
||||
using Ledger_t = RCLCxLedger;
|
||||
using NodeID_t = NodeID;
|
||||
@@ -182,7 +189,23 @@ class RCLConsensus
|
||||
return parms_;
|
||||
}
|
||||
|
||||
/** featureRNG: if tx is a validator's entropy contribution for the
|
||||
ledger after prevLedger, the NodeID of that validator.
|
||||
|
||||
Consensus votes for such a transaction while it is in its owner's
|
||||
own position during the opening window of the establish phase.
|
||||
*/
|
||||
std::optional<NodeID>
|
||||
forcedTxOwner(RCLCxTx const& tx, RCLCxLedger const& prevLedger) const;
|
||||
|
||||
private:
|
||||
/** featureRNG: build our entropy contribution for the ledger after
|
||||
prevLedger, revealing our previous secret if the ledger holds its
|
||||
commitment, and committing to a fresh one.
|
||||
*/
|
||||
std::optional<STTx>
|
||||
makeEntropyTx(std::shared_ptr<Ledger const> const& prevLedger);
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// The following members implement the generic Consensus requirements
|
||||
// and are marked private to indicate ONLY Consensus<Adaptor> will call
|
||||
|
||||
@@ -23,17 +23,22 @@
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/misc/HashRouter.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
#include <xrpld/app/rdb/Wallet.h>
|
||||
#include <xrpld/consensus/LedgerTiming.h>
|
||||
#include <xrpld/core/JobQueue.h>
|
||||
#include <xrpld/core/TimeKeeper.h>
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/overlay/predicates.h>
|
||||
#include <xrpld/perflog/PerfLog.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/StringUtilities.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
#include <xrpl/protocol/messages.h>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
@@ -167,6 +172,40 @@ RCLValidationsAdaptor::acquire(LedgerHash const& hash)
|
||||
return RCLValidatedLedger(std::move(ledger), j_);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/** Announce a manifest the cache accepted other than from peer gossip.
|
||||
|
||||
Not to the peers it was held back from: OverlayImpl::onManifests records
|
||||
each of them against the manifest's hash.
|
||||
*/
|
||||
void
|
||||
relayManifest(Application& app, Manifest const& manifest)
|
||||
{
|
||||
app.getOPs().pubManifest(manifest);
|
||||
|
||||
// As OverlayImpl::onManifests does for gossip it accepts directly.
|
||||
if (app.validators().listed(manifest.masterKey))
|
||||
{
|
||||
auto db = app.getWalletDB().checkoutDb();
|
||||
addValidatorManifest(*db, manifest.serialized);
|
||||
}
|
||||
|
||||
auto const toSkip = app.getHashRouter().shouldRelay(manifest.hash());
|
||||
if (!toSkip)
|
||||
return;
|
||||
|
||||
protocol::TMManifests tm;
|
||||
tm.add_list()->set_stobject(
|
||||
manifest.serialized.data(), manifest.serialized.size());
|
||||
|
||||
app.overlay().foreach(send_if_not(
|
||||
std::make_shared<Message>(tm, protocol::mtMANIFESTS),
|
||||
peer_in_set(*toSkip)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
handleNewValidation(
|
||||
Application& app,
|
||||
@@ -191,13 +230,29 @@ handleNewValidation(
|
||||
// Resolving now also means this validation is classified in the round it
|
||||
// arrived in rather than written off along with every other one until the
|
||||
// trusted set is next recomputed.
|
||||
if (app.validatorManifests().getMasterKey(signingKey) == signingKey)
|
||||
auto& manifests = app.validatorManifests();
|
||||
|
||||
std::shared_ptr<ReadView const> view;
|
||||
if (manifests.getMasterKey(signingKey) == signingKey)
|
||||
{
|
||||
if (auto const view = app.openLedger().current();
|
||||
view && view->rules().enabled(featureOnChainManifests))
|
||||
app.validatorManifests().applyLedgerSigningKey(*view, signingKey);
|
||||
view = app.openLedger().current();
|
||||
if (view && !view->rules().enabled(featureOnChainManifests))
|
||||
view.reset();
|
||||
}
|
||||
|
||||
// Every caller has verified this validation's signature and found it
|
||||
// current, so its ephemeral key is live. That is what admits a manifest
|
||||
// gossiped for the key, and what keeps the manifest it belongs to from
|
||||
// being evicted. A manifest that arrived before the key was ever seen is
|
||||
// released here, and relayed: the peers that held it back are waiting on
|
||||
// the same validation.
|
||||
if (auto const released = manifests.noteValidation(signingKey, view.get()))
|
||||
relayManifest(app, *released);
|
||||
|
||||
// The ledger tier's miss path.
|
||||
if (view && manifests.getMasterKey(signingKey) == signingKey)
|
||||
manifests.applyLedgerSigningKey(*view, signingKey);
|
||||
|
||||
// Ensure validation is marked as trusted if signer currently trusted
|
||||
auto masterKey = app.validators().getTrustedKey(signingKey);
|
||||
|
||||
|
||||
@@ -491,6 +491,16 @@ public:
|
||||
Expected<uint256, HookReturnCode>
|
||||
ledger_nonce() const;
|
||||
|
||||
/// RNG APIs (featureRNG)
|
||||
|
||||
// A uniform integer in [0, sides).
|
||||
Expected<uint64_t, HookReturnCode>
|
||||
dice(uint32_t sides) const;
|
||||
|
||||
// len (1 to 512) random bytes.
|
||||
Expected<Bytes, HookReturnCode>
|
||||
util_random(uint32_t len) const;
|
||||
|
||||
Expected<Keylet, HookReturnCode>
|
||||
ledger_keylet(Keylet const& klLo, Keylet const& klHi) const;
|
||||
|
||||
@@ -630,6 +640,10 @@ private:
|
||||
Rules const& rules,
|
||||
int recursion_depth = 0) // used internally
|
||||
const;
|
||||
|
||||
// featureRNG: advance the ledger's RNG state and return one output block.
|
||||
Expected<uint256, HookReturnCode>
|
||||
rng_next() const;
|
||||
};
|
||||
|
||||
} // namespace hook
|
||||
|
||||
@@ -86,7 +86,7 @@ bool
|
||||
canHook(
|
||||
STTx const& tx,
|
||||
ripple::uint256 hookOn,
|
||||
std::optional<ripple::Slice> hookName);
|
||||
std::optional<ripple::Blob> hookName);
|
||||
|
||||
bool
|
||||
canEmit(ripple::TxType txType, ripple::uint256 hookCanEmit);
|
||||
@@ -129,7 +129,11 @@ apply(
|
||||
uint32_t wasmParam,
|
||||
uint8_t hookChainPosition,
|
||||
// result of apply() if this is weak exec
|
||||
std::shared_ptr<STObject const> const& provisionalMeta);
|
||||
std::shared_ptr<STObject const> const& provisionalMeta,
|
||||
// fix20261005: the original emitted transaction for a ttEMIT_FAILURE
|
||||
// callback, captured before the cleanup pseudo removed its
|
||||
// ltEMITTED_TXN entry. Ignored before the fix.
|
||||
std::optional<ripple::STObject> const& emitFailureTxn = std::nullopt);
|
||||
|
||||
struct HookContext;
|
||||
|
||||
@@ -207,6 +211,7 @@ struct HookContext
|
||||
uint16_t emit_nonce_counter{
|
||||
0}; // incremented whenever nonce is called to ensure unique nonces
|
||||
uint16_t ledger_nonce_counter{0};
|
||||
uint16_t rng_draws{0}; // featureRNG: dice/random calls this execution
|
||||
int64_t expected_etxn_count{-1}; // make this a 64bit int so the uint32
|
||||
// from the hookapi cant overflow it
|
||||
std::map<ripple::uint256, bool> nonce_used{};
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/ledger/TransactionMaster.h>
|
||||
#include <xrpld/app/tx/detail/Import.h>
|
||||
#include <xrpld/ledger/ApplyViewImpl.h>
|
||||
#include <xrpl/protocol/STParsedJSON.h>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
namespace hook {
|
||||
|
||||
@@ -1581,6 +1584,18 @@ HookAPI::otxn_field(uint32_t field_id) const
|
||||
if (fieldType == sfInvalid)
|
||||
return Unexpected(INVALID_FIELD);
|
||||
|
||||
// fix20261005: on an emit failure callback the field is read from the
|
||||
// original emitted transaction, so check presence there rather than on
|
||||
// the ttEMIT_FAILURE pseudo (which carries sfLedgerSequence and
|
||||
// sfTransactionHash the emitted txn lacks, and lacks most of its fields).
|
||||
if (hookCtx.emitFailure &&
|
||||
hookCtx.applyCtx.view().rules().enabled(fix20261005))
|
||||
{
|
||||
if (!hookCtx.emitFailure->isFieldPresent(fieldType))
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
return &hookCtx.emitFailure->getField(fieldType);
|
||||
}
|
||||
|
||||
if (!hookCtx.applyCtx.tx.isFieldPresent(fieldType))
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
@@ -1895,6 +1910,147 @@ HookAPI::ledger_nonce() const
|
||||
return hash;
|
||||
}
|
||||
|
||||
// featureRNG
|
||||
//
|
||||
// The ledger's RNG state lives in the Random object. The ttENTROPY
|
||||
// transactions applied at the start of the ledger seed it from validator
|
||||
// reveals; after that every draw from any Hook replaces it with
|
||||
// H(step, state). So the state iterates between API calls, between Hook
|
||||
// executions and between transactions, including transactions that end in a
|
||||
// tec: ApplyContext::discard keeps the advance, so a rollback cannot preserve
|
||||
// an RNG state for whatever executes next.
|
||||
//
|
||||
// No Hook can read the state, so none can work out what a later draw will
|
||||
// return, even one that ran earlier in the same transaction: slot_set and
|
||||
// ledger_keylet refuse the Random object, slot_set refuses ttENTROPY
|
||||
// transactions, the object is left out of provisional metadata (meta_slot),
|
||||
// and its state and commitment fields are never written to any metadata.
|
||||
//
|
||||
// Each draw returns H(output, state, txid, hook account, hook hash, chain
|
||||
// position, strong, callback, wasm param). Those extra inputs keep outputs
|
||||
// distinct for different executions even if they somehow saw the same state.
|
||||
//
|
||||
// The state the transaction's first draw saw is written to the transaction's
|
||||
// final metadata (sfRandomData), never its provisional metadata.
|
||||
Expected<uint256, HookReturnCode>
|
||||
HookAPI::rng_next() const
|
||||
{
|
||||
static constexpr std::uint32_t rngOutputTag = 0x524E474F; // 'RNGO'
|
||||
static constexpr std::uint32_t rngStepTag = 0x524E4749; // 'RNGI'
|
||||
|
||||
auto& applyCtx = hookCtx.applyCtx;
|
||||
auto& view = applyCtx.view();
|
||||
|
||||
if (!view.rules().enabled(featureRNG))
|
||||
return Unexpected(TOO_LITTLE_ENTROPY);
|
||||
|
||||
if (hookCtx.rng_draws >= hook_api::max_rng_draws)
|
||||
return Unexpected(TOO_MANY_NONCES);
|
||||
|
||||
auto sle = view.peek(keylet::random());
|
||||
if (!sle)
|
||||
return Unexpected(TOO_LITTLE_ENTROPY);
|
||||
|
||||
// A closed ledger may only draw on its own seed. Open ledger application
|
||||
// is a preview, so it may run on the last closed ledger's state; the
|
||||
// final result is decided when the ledger closes.
|
||||
auto const seq = view.seq();
|
||||
auto const seededFor = sle->getFieldU32(sfLedgerSequence);
|
||||
if (seededFor != seq && !(view.open() && seededFor + 1 == seq))
|
||||
return Unexpected(TOO_LITTLE_ENTROPY);
|
||||
|
||||
// At least 80% of the UNLReport must have contributed (at least one
|
||||
// validator if there is no UNLReport). Both fields are 16 bits on the
|
||||
// ledger; they are widened here so the scaled comparison cannot overflow.
|
||||
std::uint32_t const count = sle->getFieldU16(sfEntropyCount);
|
||||
std::uint32_t const denominator = sle->getFieldU16(sfEntropyDenominator);
|
||||
if (count == 0 || count * 5 < denominator * 4)
|
||||
return Unexpected(TOO_LITTLE_ENTROPY);
|
||||
|
||||
uint256 const state = sle->getFieldH256(sfRandomData);
|
||||
|
||||
if (auto* avi = dynamic_cast<ApplyViewImpl*>(&view))
|
||||
avi->setRandomData(state);
|
||||
|
||||
auto const& hr = hookCtx.result;
|
||||
uint256 const out = ripple::sha512Half(
|
||||
rngOutputTag,
|
||||
state,
|
||||
applyCtx.tx.getTransactionID(),
|
||||
hr.account,
|
||||
hr.hookHash,
|
||||
static_cast<std::uint8_t>(hr.hookChainPosition),
|
||||
static_cast<std::uint8_t>(hr.isStrong ? 1 : 0),
|
||||
static_cast<std::uint8_t>(hr.isCallback ? 1 : 0),
|
||||
hr.wasmParam);
|
||||
|
||||
sle->setFieldH256(sfRandomData, ripple::sha512Half(rngStepTag, state));
|
||||
view.update(sle);
|
||||
|
||||
++hookCtx.rng_draws;
|
||||
return out;
|
||||
}
|
||||
|
||||
Expected<uint64_t, HookReturnCode>
|
||||
HookAPI::dice(uint32_t sides) const
|
||||
{
|
||||
if (sides == 0)
|
||||
return Unexpected(INVALID_ARGUMENT);
|
||||
|
||||
auto block = rng_next();
|
||||
if (!block)
|
||||
return Unexpected(block.error());
|
||||
|
||||
// Rejection sampling over big-endian 64-bit words, so every side is
|
||||
// exactly equally likely. A word is rejected with probability below
|
||||
// 2^-32, so the bound on blocks is never reached in practice; it only
|
||||
// keeps the loop finite.
|
||||
std::uint64_t const max = std::numeric_limits<std::uint64_t>::max();
|
||||
std::uint64_t const excess = (max % sides + 1) % sides; // 2^64 mod sides
|
||||
std::uint64_t word = 0;
|
||||
uint256 b = *block;
|
||||
for (int i = 0; i < 16; ++i)
|
||||
{
|
||||
for (std::size_t w = 0; w < 4; ++w)
|
||||
{
|
||||
word = 0;
|
||||
for (std::size_t k = 0; k < 8; ++k)
|
||||
word = (word << 8) | b.data()[w * 8 + k];
|
||||
if (word <= max - excess)
|
||||
return word % sides;
|
||||
}
|
||||
b = ripple::sha512Half(b);
|
||||
}
|
||||
return word % sides;
|
||||
}
|
||||
|
||||
Expected<Bytes, HookReturnCode>
|
||||
HookAPI::util_random(uint32_t len) const
|
||||
{
|
||||
if (len == 0)
|
||||
return Unexpected(TOO_SMALL);
|
||||
|
||||
if (len > 512)
|
||||
return Unexpected(TOO_BIG);
|
||||
|
||||
auto block = rng_next();
|
||||
if (!block)
|
||||
return Unexpected(block.error());
|
||||
|
||||
Bytes out;
|
||||
out.reserve(len);
|
||||
uint256 b = *block;
|
||||
while (true)
|
||||
{
|
||||
auto const n = std::min<std::size_t>(32, len - out.size());
|
||||
out.insert(out.end(), b.data(), b.data() + n);
|
||||
if (out.size() >= len)
|
||||
break;
|
||||
b = ripple::sha512Half(b);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Expected<Keylet, HookReturnCode>
|
||||
HookAPI::ledger_keylet(Keylet const& klLo, Keylet const& klHi) const
|
||||
{
|
||||
@@ -1905,6 +2061,10 @@ HookAPI::ledger_keylet(Keylet const& klLo, Keylet const& klHi) const
|
||||
std::optional<ripple::uint256> found =
|
||||
hookCtx.applyCtx.view().succ(klLo.key, klHi.key.next());
|
||||
|
||||
// featureRNG: the RNG object is invisible to Hooks, even by key.
|
||||
if (found && *found == keylet::random().key)
|
||||
found = hookCtx.applyCtx.view().succ(*found, klHi.key.next());
|
||||
|
||||
if (!found)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
@@ -2149,6 +2309,12 @@ HookAPI::slot_set(Bytes const& data, uint32_t slot_no) const
|
||||
if (kl->key == beast::zero)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
// featureRNG: the RNG state must never be readable by a Hook, or an
|
||||
// earlier Hook could compute what later draws will return. Matched on
|
||||
// the key because a Hook-supplied keylet type can be ltANY.
|
||||
if (kl->key == keylet::random().key)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
auto const sle = hookCtx.applyCtx.view().read(*kl);
|
||||
if (!sle)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
@@ -2172,6 +2338,11 @@ HookAPI::slot_set(Bytes const& data, uint32_t slot_no) const
|
||||
slot_value = p->first->getSTransaction();
|
||||
else
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
|
||||
// featureRNG: validator reveals are never readable by Hooks.
|
||||
if (*slot_value &&
|
||||
(*slot_value)->getFieldU16(sfTransactionType) == ttENTROPY)
|
||||
return Unexpected(DOESNT_EXIST);
|
||||
}
|
||||
else
|
||||
return Unexpected(INVALID_ARGUMENT);
|
||||
@@ -2828,11 +2999,19 @@ HookAPI::set_state_cache(
|
||||
if (availableForReserves < hookStateScale && modified)
|
||||
return Unexpected(RESERVE_INSUFFICIENT);
|
||||
|
||||
// Prior to fix20261005 the first-account branch charged write
|
||||
// resources (a state modification, a reserve slot and, if needed, a
|
||||
// namespace slot) even when the entry was only being read into the
|
||||
// cache (modified == false). Every other branch only charges for
|
||||
// writes. With the fix, reads into the cache are free; a later write
|
||||
// of the same key is charged by the existing-key branch below.
|
||||
bool const chargeWrite = modified || !view.rules().enabled(fix20261005);
|
||||
|
||||
int64_t namespaceCount = accSLE->isFieldPresent(sfHookNamespaces)
|
||||
? accSLE->getFieldV256(sfHookNamespaces).size()
|
||||
: 0;
|
||||
|
||||
if (createNamespace)
|
||||
if (createNamespace && chargeWrite)
|
||||
{
|
||||
// overflow should never ever happen but check anyway
|
||||
if (namespaceCount + 1 < namespaceCount)
|
||||
@@ -2842,15 +3021,20 @@ HookAPI::set_state_cache(
|
||||
return Unexpected(TOO_MANY_NAMESPACES);
|
||||
}
|
||||
|
||||
stateMap.modified_entry_count++;
|
||||
if (chargeWrite)
|
||||
{
|
||||
stateMap.modified_entry_count++;
|
||||
|
||||
// sanity check
|
||||
if (view.rules().enabled(featureExtendedHookState) &&
|
||||
availableForReserves < hookStateScale)
|
||||
return Unexpected(INTERNAL_ERROR);
|
||||
// sanity check
|
||||
if (view.rules().enabled(featureExtendedHookState) &&
|
||||
availableForReserves < hookStateScale)
|
||||
return Unexpected(INTERNAL_ERROR);
|
||||
|
||||
availableForReserves -= hookStateScale;
|
||||
}
|
||||
|
||||
stateMap[acc] = {
|
||||
availableForReserves - hookStateScale,
|
||||
availableForReserves,
|
||||
namespaceCount,
|
||||
hookStateScale,
|
||||
{{ns, {{key, {modified, data}}}}}};
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/basics/Slice.h>
|
||||
#include <xrpl/protocol/AccountID.h>
|
||||
#include <xrpl/protocol/CreditNote.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/TxFlags.h>
|
||||
@@ -472,6 +473,25 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
case ttCRON_SET: {
|
||||
break;
|
||||
}
|
||||
case ttNOTE_CLAIM: {
|
||||
// The note's issuer is who pays, so they are a strong stake
|
||||
// holder: their hook can roll a redemption back. The note has
|
||||
// already passed preflight by the time hooks run, so a parse
|
||||
// failure here only means no issuer hook executes.
|
||||
if (!tx.isFieldPresent(sfNote))
|
||||
break;
|
||||
try
|
||||
{
|
||||
auto const raw = tx.getFieldVL(sfNote);
|
||||
auto const note = parseCreditNote(std::string_view(
|
||||
reinterpret_cast<char const*>(raw.data()), raw.size()));
|
||||
ADD_TSH(note.account, tshSTRONG);
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ttMANIFEST_SET: {
|
||||
// The ephemeral key's logical account, meaning the r-address its
|
||||
// public key hashes to, is a weak stake holder: the manifest names
|
||||
@@ -558,7 +578,8 @@ getTransactionalStakeHolders(STTx const& tx, ReadView const& rv)
|
||||
case ttFEE:
|
||||
case ttUNL_MODIFY:
|
||||
case ttEMIT_FAILURE:
|
||||
case ttUNL_REPORT: {
|
||||
case ttUNL_REPORT:
|
||||
case ttENTROPY: {
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -813,7 +834,7 @@ bool
|
||||
hook::canHook(
|
||||
STTx const& tx,
|
||||
ripple::uint256 hookOn,
|
||||
std::optional<ripple::Slice> hookName)
|
||||
std::optional<ripple::Blob> hookName)
|
||||
{
|
||||
if (!canHookTT(tx.getTxnType(), hookOn))
|
||||
return false;
|
||||
@@ -827,7 +848,7 @@ hook::canHook(
|
||||
// transaction, so we can't hook without the hook name
|
||||
return false;
|
||||
|
||||
return tx[sfHookName] == *hookName;
|
||||
return tx.getFieldVL(sfHookName) == *hookName;
|
||||
}
|
||||
|
||||
ripple::uint256
|
||||
@@ -1041,8 +1062,29 @@ hook::apply(
|
||||
bool isStrong,
|
||||
uint32_t wasmParam,
|
||||
uint8_t hookChainPosition,
|
||||
std::shared_ptr<STObject const> const& provisionalMeta)
|
||||
std::shared_ptr<STObject const> const& provisionalMeta,
|
||||
std::optional<ripple::STObject> const& emitFailureTxn)
|
||||
{
|
||||
std::optional<ripple::STObject> emitFailure;
|
||||
if (isCallback && (wasmParam & 1))
|
||||
{
|
||||
if (applyCtx.view().rules().enabled(fix20261005))
|
||||
{
|
||||
// fix20261005: by the time the callback runs the ttEMIT_FAILURE
|
||||
// pseudo has already erased the ltEMITTED_TXN entry from this
|
||||
// view, so it can't be read back here. The caller captures the
|
||||
// original emitted transaction before base application.
|
||||
emitFailure = emitFailureTxn;
|
||||
}
|
||||
else
|
||||
{
|
||||
// pre-fix: reads the entry the pseudo has already erased
|
||||
emitFailure = (*(applyCtx.view().peek(keylet::emittedTxn(
|
||||
applyCtx.tx.getFieldH256(sfTransactionHash)))))
|
||||
.downcast<STObject>();
|
||||
}
|
||||
}
|
||||
|
||||
HookContext hookCtx = {
|
||||
.applyCtx = applyCtx,
|
||||
// we will return this context object (RVO / move constructed)
|
||||
@@ -1074,12 +1116,7 @@ hook::apply(
|
||||
.foreignStateSetDisabled = false,
|
||||
.provisionalMeta = provisionalMeta,
|
||||
.foreignStateGrantCache = {}},
|
||||
.emitFailure = isCallback && wasmParam & 1
|
||||
? std::optional<ripple::STObject>(
|
||||
(*(applyCtx.view().peek(keylet::emittedTxn(
|
||||
applyCtx.tx.getFieldH256(sfTransactionHash)))))
|
||||
.downcast<STObject>())
|
||||
: std::optional<ripple::STObject>(),
|
||||
.emitFailure = std::move(emitFailure),
|
||||
.module = nullptr,
|
||||
.api_ = nullptr};
|
||||
|
||||
@@ -2866,6 +2903,49 @@ DEFINE_HOOK_FUNCTION(
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
// featureRNG: a uniform integer in [0, sides)
|
||||
DEFINE_HOOK_FUNCTION(int64_t, dice, uint32_t sides)
|
||||
{
|
||||
HOOK_SETUP();
|
||||
|
||||
auto const result = api.dice(sides);
|
||||
if (!result)
|
||||
return result.error();
|
||||
|
||||
return result.value();
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
// featureRNG: write_len (1 to 512) random bytes
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
int64_t,
|
||||
util_random,
|
||||
uint32_t write_ptr,
|
||||
uint32_t write_len)
|
||||
{
|
||||
HOOK_SETUP();
|
||||
|
||||
if (NOT_IN_BOUNDS(write_ptr, write_len, memory_length))
|
||||
return OUT_OF_BOUNDS;
|
||||
|
||||
auto const result = api.util_random(write_len);
|
||||
if (!result)
|
||||
return result.error();
|
||||
|
||||
auto const& bytes = result.value();
|
||||
|
||||
WRITE_WASM_MEMORY_AND_RETURN(
|
||||
write_ptr,
|
||||
write_len,
|
||||
bytes.data(),
|
||||
bytes.size(),
|
||||
memory,
|
||||
memory_length);
|
||||
|
||||
HOOK_TEARDOWN();
|
||||
}
|
||||
|
||||
DEFINE_HOOK_FUNCTION(
|
||||
int64_t,
|
||||
ledger_keylet,
|
||||
|
||||
@@ -25,7 +25,8 @@ namespace ripple {
|
||||
|
||||
AcceptedLedger::AcceptedLedger(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
Application& app)
|
||||
Application& app,
|
||||
bool withJson)
|
||||
: mLedger(ledger)
|
||||
{
|
||||
transactions_.reserve(256);
|
||||
@@ -33,7 +34,7 @@ AcceptedLedger::AcceptedLedger(
|
||||
auto insertAll = [&](auto const& txns) {
|
||||
for (auto const& item : txns)
|
||||
transactions_.emplace_back(std::make_unique<AcceptedLedgerTx>(
|
||||
ledger, item.first, item.second));
|
||||
ledger, item.first, item.second, withJson));
|
||||
};
|
||||
|
||||
transactions_.reserve(256);
|
||||
|
||||
@@ -44,9 +44,15 @@ namespace ripple {
|
||||
class AcceptedLedger : public CountedObject<AcceptedLedger>
|
||||
{
|
||||
public:
|
||||
/** @param withJson build each transaction's publishable JSON. Pass
|
||||
false only for ledgers that will never be published from this
|
||||
object (e.g. pinned catalogue history being saved to SQL);
|
||||
getJson() on such transactions returns a null value.
|
||||
*/
|
||||
AcceptedLedger(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
Application& app);
|
||||
Application& app,
|
||||
bool withJson = true);
|
||||
|
||||
std::shared_ptr<ReadView const> const&
|
||||
getLedger() const
|
||||
|
||||
@@ -28,7 +28,8 @@ namespace ripple {
|
||||
AcceptedLedgerTx::AcceptedLedgerTx(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
std::shared_ptr<STTx const> const& txn,
|
||||
std::shared_ptr<STObject const> const& met)
|
||||
std::shared_ptr<STObject const> const& met,
|
||||
bool withJson)
|
||||
: mTxn(txn)
|
||||
, mMeta(txn->getTransactionID(), ledger->seq(), *met)
|
||||
, mAffected(mMeta.getAffectedAccounts())
|
||||
@@ -41,6 +42,11 @@ AcceptedLedgerTx::AcceptedLedgerTx(
|
||||
met->add(s);
|
||||
mRawMeta = std::move(s.modData());
|
||||
|
||||
// The JSON (full tx + meta + hex raw meta per transaction) is only
|
||||
// needed for publishing. Bulk SQL saves of pinned history skip it.
|
||||
if (!withJson)
|
||||
return;
|
||||
|
||||
mJson = Json::objectValue;
|
||||
mJson[jss::transaction] = mTxn->getJson(JsonOptions::none);
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ public:
|
||||
AcceptedLedgerTx(
|
||||
std::shared_ptr<ReadView const> const& ledger,
|
||||
std::shared_ptr<STTx const> const&,
|
||||
std::shared_ptr<STObject const> const&);
|
||||
std::shared_ptr<STObject const> const&,
|
||||
bool withJson = true);
|
||||
|
||||
std::shared_ptr<STTx const> const&
|
||||
getTxn() const
|
||||
@@ -88,6 +89,7 @@ public:
|
||||
std::string
|
||||
getEscMeta() const;
|
||||
|
||||
/** Null if constructed with withJson == false. */
|
||||
Json::Value const&
|
||||
getJson() const
|
||||
{
|
||||
|
||||
@@ -25,7 +25,11 @@
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/overlay/PeerSet.h>
|
||||
#include <xrpl/basics/CountedObject.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
@@ -175,6 +179,36 @@ private:
|
||||
clock_type::time_point mLastAction;
|
||||
|
||||
std::shared_ptr<Ledger> mLedger;
|
||||
|
||||
/** Whether this ledger is to be read for manifests as soon as it can be:
|
||||
early, before the rest of the state map, if [early_manifest_sync]
|
||||
allows, and otherwise once it is complete. */
|
||||
bool
|
||||
wantManifests();
|
||||
|
||||
/** Read the manifests from the ledger, now that they can be.
|
||||
|
||||
Dispatches the read at most once per ledger, however many paths reach
|
||||
here.
|
||||
|
||||
@param listed Master keys to read by key, whether or not the part of
|
||||
the directory read lists them. If unseated, the master keys on the
|
||||
validator lists are collected in the job, so that a caller holding
|
||||
mtx_ need not take the validator list's lock.
|
||||
@param maxPages Most manifest directory pages to read
|
||||
*/
|
||||
void
|
||||
syncManifests(
|
||||
std::optional<hash_set<PublicKey>> listed,
|
||||
std::uint64_t maxPages);
|
||||
|
||||
/** The manifests have been read, or are being, from this ledger.
|
||||
|
||||
Atomic because done() can be reached from trigger() after it has
|
||||
released mtx_.
|
||||
*/
|
||||
std::atomic<bool> mManifestsSynced{false};
|
||||
|
||||
bool mHaveHeader;
|
||||
bool mHaveState;
|
||||
bool mHaveTransactions;
|
||||
|
||||
@@ -228,6 +228,19 @@ public:
|
||||
isPinned(std::uint32_t seq);
|
||||
void
|
||||
unpinLedger(std::uint32_t seq);
|
||||
|
||||
/** Remove `ranges` from the pinned set.
|
||||
|
||||
@param dropFromComplete of the ledgers actually unpinned, those to
|
||||
also remove from complete_ledgers now (used to roll back a
|
||||
load). Otherwise unpinned ledgers stay complete until online
|
||||
delete releases them.
|
||||
@return the ledgers that were pinned and no longer are.
|
||||
*/
|
||||
RangeSet<std::uint32_t>
|
||||
unpinLedgers(
|
||||
RangeSet<std::uint32_t> const& ranges,
|
||||
RangeSet<std::uint32_t> const& dropFromComplete = {});
|
||||
bool
|
||||
isValidated(ReadView const& ledger);
|
||||
bool
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_LEDGER_MANIFESTSYNC_H_INCLUDED
|
||||
#define RIPPLE_APP_LEDGER_MANIFESTSYNC_H_INCLUDED
|
||||
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpld/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/base_uint.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Manifest directory pages read while a ledger is still being acquired.
|
||||
|
||||
Enough entries to fill the manifest cache's ledger tier, which is all a
|
||||
bulk read of the directory can admit. The master keys a node actually
|
||||
depends on are fetched by key instead, wherever they are listed, so a
|
||||
directory grown long by anyone able to fund an account cannot make the
|
||||
early phase of a sync arbitrarily long.
|
||||
*/
|
||||
std::uint64_t constexpr manifestSyncPages = 16;
|
||||
|
||||
/** The nodes a partly synced state map still needs to read its manifests.
|
||||
|
||||
Walks the manifest directory as far as the nodes held allow: the root page;
|
||||
every other page, all at once, since the root names the last of them and
|
||||
pages are only ever added after it; each master object listed; and the
|
||||
ephemeral object each unrevoked one names, which is what a validation is
|
||||
resolved through. Where a key cannot be read, the first node on its path
|
||||
that is not held is reported, once however many keys it blocks.
|
||||
|
||||
Asking peers for the nodes reported and calling again converges in a
|
||||
handful of rounds -- roughly the depth of the map plus one round each for
|
||||
the pages and for the objects they list -- on a set of nodes far smaller
|
||||
than the map. After that every read forEachLedgerManifest() and
|
||||
ManifestCache::applyLedgerDirectory() make succeeds on the partial map.
|
||||
|
||||
A map with no manifest directory converges on the path to where its root
|
||||
would be, and nothing else.
|
||||
|
||||
@param stateMap State map, with at least its root node
|
||||
@param max Most nodes to report
|
||||
@param keys Further keys to read, with the ephemeral object any manifest
|
||||
among them names: in practice keylet::manifest() of each master key on
|
||||
the node's validator lists, which needs no directory to find
|
||||
@param maxPages Most directory pages to read, the root page included
|
||||
|
||||
@return nodes needed, as getMissingNodes() reports them; empty once every
|
||||
manifest can be read
|
||||
*/
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
missingManifestNodes(
|
||||
SHAMap const& stateMap,
|
||||
std::size_t max,
|
||||
std::vector<uint256> const& keys = {},
|
||||
std::uint64_t maxPages = dirNodeMaxPages);
|
||||
|
||||
/** Ledgers to read manifests from, chosen by how many peers report them.
|
||||
|
||||
A node that has never validated a ledger, and holds no manifest for enough
|
||||
of the validators it trusts, cannot tell which validations to believe. The
|
||||
closed ledgers its peers report are then its only evidence of where the
|
||||
network is, and any recent ledger of the network holds the manifests
|
||||
published on it.
|
||||
|
||||
Choosing wrongly costs little. Every manifest read from a ledger is
|
||||
verified as from any other source and the cache only moves forward, so a
|
||||
ledger a peer made up can at worst leave something out. Reading several
|
||||
candidates, rather than trusting the single most reported, is what keeps
|
||||
one misreporting peer among a few from deciding what is read.
|
||||
|
||||
@param reported The closed ledger each peer last reported, one entry per
|
||||
peer. Zero entries are ignored.
|
||||
@param skip Ledgers already read, or being read
|
||||
@param max Most ledgers to return
|
||||
|
||||
@return distinct ledgers, the most widely reported first. Ties go to the
|
||||
larger hash, as when NetworkOPs picks a ledger by peer count, so the
|
||||
first is usually a ledger being acquired anyway.
|
||||
*/
|
||||
std::vector<uint256>
|
||||
pickManifestCandidates(
|
||||
std::vector<uint256> const& reported,
|
||||
hash_set<uint256> const& skip,
|
||||
std::size_t max);
|
||||
|
||||
/** How many validators need a manifest before a node can trust a ledger.
|
||||
|
||||
@param validators The validators that count: the trusted set once
|
||||
ValidatorList has worked it out, and every listed one before that
|
||||
@param trusted Whether `validators` is the trusted set, and so whether
|
||||
`quorum` has been worked out from it
|
||||
@param quorum ValidatorList::quorum(). Before the trusted set exists this
|
||||
is a placeholder, so the usual 80% of `validators` is assumed instead.
|
||||
|
||||
@return the number needed; never more than `validators`
|
||||
*/
|
||||
std::size_t
|
||||
manifestQuorum(std::size_t validators, bool trusted, std::size_t quorum);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -106,6 +106,41 @@ applyTransactions(
|
||||
bool certainRetry = true;
|
||||
std::size_t count = 0;
|
||||
|
||||
// featureRNG: validator entropy contributions are applied before anything
|
||||
// else, so this ledger's seed is final before any Hook can draw from it.
|
||||
// Pseudo-transactions otherwise sort to an arbitrary position. Each is
|
||||
// attempted exactly once; one that fails is never retried later, where it
|
||||
// could change the seed after Hooks had drawn from it.
|
||||
if (view.rules().enabled(featureRNG))
|
||||
{
|
||||
for (auto it = txns.begin(); it != txns.end();)
|
||||
{
|
||||
if (it->second->getTxnType() != ttENTROPY)
|
||||
{
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto const txid = it->first.getTXID();
|
||||
try
|
||||
{
|
||||
if (applyTransaction(
|
||||
app, view, *it->second, false, tapNONE, j) ==
|
||||
ApplyTransactionResult::Success)
|
||||
++count;
|
||||
else
|
||||
failed.insert(txid);
|
||||
}
|
||||
catch (std::exception const& ex)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "Transaction " << txid << " throws: " << ex.what();
|
||||
failed.insert(txid);
|
||||
}
|
||||
it = txns.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to apply all of the retriable transactions
|
||||
for (int pass = 0; pass < LEDGER_TOTAL_PASSES; ++pass)
|
||||
{
|
||||
|
||||
@@ -21,14 +21,18 @@
|
||||
#include <xrpld/app/ledger/InboundLedger.h>
|
||||
#include <xrpld/app/ledger/InboundLedgers.h>
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/ManifestSync.h>
|
||||
#include <xrpld/app/ledger/TransactionStateSF.h>
|
||||
#include <xrpld/app/main/Application.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpld/app/misc/NetworkOPs.h>
|
||||
#include <xrpld/app/misc/ValidatorList.h>
|
||||
#include <xrpld/core/JobQueue.h>
|
||||
#include <xrpld/overlay/Overlay.h>
|
||||
#include <xrpld/shamap/SHAMapNodeID.h>
|
||||
#include <xrpl/basics/Log.h>
|
||||
#include <xrpl/protocol/HashPrefix.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
#include <xrpl/resource/Fees.h>
|
||||
|
||||
@@ -39,6 +43,16 @@
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Master keys on any of this node's validator lists, trusted or not. */
|
||||
static hash_set<PublicKey>
|
||||
listedMasterKeys(Application& app)
|
||||
{
|
||||
hash_set<PublicKey> keys;
|
||||
app.validators().for_each_listed(
|
||||
[&keys](PublicKey const& pk, bool) { keys.insert(pk); });
|
||||
return keys;
|
||||
}
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
enum {
|
||||
@@ -457,6 +471,19 @@ InboundLedger::done()
|
||||
mLedger->read(keylet::fees()),
|
||||
"ripple::InboundLedger::done : valid ledger fees");
|
||||
mLedger->setImmutable();
|
||||
|
||||
// The state map arrived whole before the early walk in trigger()
|
||||
// finished, as it does when most of it is held locally already, or
|
||||
// when [early_manifest_sync] turns the early walk off. Read the
|
||||
// manifests from the whole map instead, now that it is immutable.
|
||||
// Only as many directory pages as the early walk reads: past those
|
||||
// the ledger tier is full, and the listed master keys are read by
|
||||
// key whatever the directory holds. The keys are collected in the
|
||||
// job: this runs under mtx_, and the validator list has a lock of
|
||||
// its own.
|
||||
if (!mManifestsSynced && wantManifests())
|
||||
syncManifests(std::nullopt, manifestSyncPages);
|
||||
|
||||
switch (mReason)
|
||||
{
|
||||
case Reason::HISTORY:
|
||||
@@ -636,6 +663,63 @@ InboundLedger::trigger(std::shared_ptr<Peer> const& peer, TriggerReason reason)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Partial sync. A node that has not yet validated a ledger cannot
|
||||
// tell which validations to trust until it knows each trusted
|
||||
// validator's current ephemeral key, and on-ledger is where a
|
||||
// rotation or a revocation is published. So the manifests come
|
||||
// first: just the nodes on the paths to the manifest directory and
|
||||
// the objects it lists, which can be read and ingested long before
|
||||
// the rest of the state map arrives.
|
||||
//
|
||||
// The master keys on this node's validator lists are fetched by
|
||||
// key as well as through the directory. They are what a node with
|
||||
// no manifests at all needs, and reading them by key does not
|
||||
// depend on how long the directory has grown.
|
||||
if (!mManifestsSynced && app_.config().EARLY_MANIFEST_SYNC &&
|
||||
wantManifests())
|
||||
{
|
||||
sl.unlock();
|
||||
auto listed = listedMasterKeys(app_);
|
||||
std::vector<uint256> targets;
|
||||
targets.reserve(listed.size());
|
||||
for (auto const& pk : listed)
|
||||
targets.push_back(keylet::manifest(pk).key);
|
||||
auto nodes = missingManifestNodes(
|
||||
mLedger->stateMap(),
|
||||
missingNodesFind,
|
||||
targets,
|
||||
manifestSyncPages);
|
||||
sl.lock();
|
||||
|
||||
if (!failed_ && !complete_ && !mHaveState && !mManifestsSynced)
|
||||
{
|
||||
if (nodes.empty())
|
||||
{
|
||||
syncManifests(std::move(listed), manifestSyncPages);
|
||||
}
|
||||
else
|
||||
{
|
||||
filterNodes(nodes, reason);
|
||||
|
||||
// All recently asked for: fall through to the rest of
|
||||
// the map rather than wait on them.
|
||||
if (!nodes.empty())
|
||||
{
|
||||
tmGL.set_itype(protocol::liAS_NODE);
|
||||
for (auto const& id : nodes)
|
||||
*(tmGL.add_nodeids()) = id.first.getRawString();
|
||||
|
||||
JLOG(journal_.trace())
|
||||
<< "Sending manifest node request ("
|
||||
<< nodes.size() << ") to "
|
||||
<< (peer ? "selected peer" : "all peers");
|
||||
mPeerSet->sendRequest(tmGL, peer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccountStateSF filter(
|
||||
mLedger->stateMap().family().db(), app_.getLedgerMaster());
|
||||
|
||||
@@ -764,6 +848,73 @@ InboundLedger::trigger(std::shared_ptr<Peer> const& peer, TriggerReason reason)
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
InboundLedger::wantManifests()
|
||||
{
|
||||
// Not for history, and not once a ledger has been validated: from then on
|
||||
// the manifests are kept current from each new ledger as it closes.
|
||||
return mReason != Reason::HISTORY &&
|
||||
!app_.getLedgerMaster().haveValidated();
|
||||
}
|
||||
|
||||
void
|
||||
InboundLedger::syncManifests(
|
||||
std::optional<hash_set<PublicKey>> listed,
|
||||
std::uint64_t maxPages)
|
||||
{
|
||||
if (mManifestsSynced.exchange(true))
|
||||
return;
|
||||
|
||||
// Reading verifies a signature per manifest, so it runs as its own job.
|
||||
// The ledger is held by shared_ptr, and reading a map while nodes are
|
||||
// still being added to it is what getMissingNodes() already does.
|
||||
app_.getJobQueue().addJob(
|
||||
jtMANIFEST,
|
||||
"InboundLedger::syncManifests",
|
||||
[ledger = mLedger,
|
||||
listed = std::move(listed),
|
||||
maxPages,
|
||||
&app = app_,
|
||||
j = journal_]() mutable {
|
||||
auto const keys =
|
||||
listed ? std::move(*listed) : listedMasterKeys(app);
|
||||
auto& cache = app.validatorManifests();
|
||||
std::size_t accepted = 0;
|
||||
std::size_t unreadable = 0;
|
||||
|
||||
// By key first, one at a time: on a partial map a key whose path
|
||||
// is not held throws, and must not cost the keys after it. One
|
||||
// can be missing if the lists changed after the nodes were found.
|
||||
for (auto const& pk : keys)
|
||||
{
|
||||
try
|
||||
{
|
||||
accepted += cache.applyLedger(*ledger, {pk});
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
++unreadable;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
accepted += cache.applyLedgerDirectory(*ledger, maxPages);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(j.warn())
|
||||
<< "Could not read the manifest directory of ledger "
|
||||
<< ledger->info().seq << " during sync: " << e.what();
|
||||
}
|
||||
|
||||
JLOG(j.info()) << "Read manifests from ledger "
|
||||
<< ledger->info().seq << " during sync: " << accepted
|
||||
<< " accepted, " << keys.size() << " listed, "
|
||||
<< unreadable << " not yet readable";
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
InboundLedger::filterNodes(
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>& nodes,
|
||||
|
||||
@@ -501,7 +501,9 @@ LedgerMaster::storeLedger(std::shared_ptr<Ledger const> ledger, bool pin)
|
||||
std::lock_guard sl(mPinnedLock);
|
||||
mPinnedLedgers.insert(range(seq, seq));
|
||||
}
|
||||
JLOG(m_journal.info()) << "Pinned ledger : " << seq;
|
||||
// catalogue_load pins every ledger it imports; at info this would
|
||||
// emit one line per ledger for the whole file.
|
||||
JLOG(m_journal.debug()) << "Pinned ledger : " << seq;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -586,6 +588,19 @@ LedgerMaster::unpinLedger(std::uint32_t seq)
|
||||
mPinnedLedgers.erase(range(seq, seq));
|
||||
}
|
||||
|
||||
RangeSet<std::uint32_t>
|
||||
LedgerMaster::unpinLedgers(
|
||||
RangeSet<std::uint32_t> const& ranges,
|
||||
RangeSet<std::uint32_t> const& dropFromComplete)
|
||||
{
|
||||
std::scoped_lock lock(mCompleteLock, mPinnedLock);
|
||||
RangeSet<std::uint32_t> removed = mPinnedLedgers & ranges;
|
||||
mPinnedLedgers -= removed;
|
||||
if (!dropFromComplete.empty())
|
||||
mCompleteLedgers -= (removed & dropFromComplete);
|
||||
return removed;
|
||||
}
|
||||
|
||||
bool
|
||||
LedgerMaster::isValidated(ReadView const& ledger)
|
||||
{
|
||||
|
||||
@@ -161,6 +161,18 @@ public:
|
||||
if (!sleAcct)
|
||||
return false;
|
||||
|
||||
// A time-sequenced transaction's SeqProxy is sequence(0), which
|
||||
// the sequence test below would call past and sweep at once. It
|
||||
// is spent once the account has recorded its Time or a later one
|
||||
// - applied, or superseded - and otherwise lives out holdLedgers
|
||||
// like anything else (preclaim refuses it once it expires).
|
||||
if (auto const time = txn.getTX()->at(~sfTime);
|
||||
time && txn.getTX()->isTimeSequenced())
|
||||
{
|
||||
auto const last = sleAcct->at(~sfLastTxnTime);
|
||||
return last && *last >= *time;
|
||||
}
|
||||
|
||||
SeqProxy const acctSeq =
|
||||
SeqProxy::sequence(sleAcct->getFieldU32(sfSequence));
|
||||
SeqProxy const seqProx = txn.getSeqProxy();
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#include <xrpld/app/ledger/ManifestSync.h>
|
||||
#include <xrpld/app/misc/Manifest.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/protocol/Indexes.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
static_assert(
|
||||
manifestSyncPages * dirNodeMaxEntries >= ManifestCache::ledgerCapacity,
|
||||
"the early read must be able to fill the ledger tier");
|
||||
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>>
|
||||
missingManifestNodes(
|
||||
SHAMap const& stateMap,
|
||||
std::size_t max,
|
||||
std::vector<uint256> const& keys,
|
||||
std::uint64_t maxPages)
|
||||
{
|
||||
std::vector<std::pair<SHAMapNodeID, uint256>> missing;
|
||||
|
||||
// By hash: several keys blocked by the same node report it once.
|
||||
hash_set<uint256> reported;
|
||||
hash_set<uint256> visited;
|
||||
|
||||
auto const& root = keylet::manifestDir();
|
||||
|
||||
// The manifest directory's own pages: the only directory nodes whose
|
||||
// entries are followed. The ledger walked is not validated, and one a
|
||||
// peer made up could otherwise lead the walk from directory to directory
|
||||
// through as much of the map as it cared to serve.
|
||||
hash_set<uint256> pages{root.key};
|
||||
|
||||
// A stack, so the keys asked for by name go last and are read first: they
|
||||
// are the ones that decide whether this node can trust a validation.
|
||||
std::vector<uint256> pending{root.key};
|
||||
pending.insert(pending.end(), keys.rbegin(), keys.rend());
|
||||
|
||||
maxPages = std::clamp<std::uint64_t>(maxPages, 1, dirNodeMaxPages);
|
||||
|
||||
while (!pending.empty() && missing.size() < max)
|
||||
{
|
||||
auto const key = pending.back();
|
||||
pending.pop_back();
|
||||
|
||||
if (!visited.insert(key).second)
|
||||
continue;
|
||||
|
||||
std::optional<std::pair<SHAMapNodeID, uint256>> gap;
|
||||
auto const item = stateMap.peekItemPartial(key, gap);
|
||||
|
||||
if (gap)
|
||||
{
|
||||
if (reported.insert(gap->second).second)
|
||||
missing.push_back(*gap);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!item)
|
||||
continue;
|
||||
|
||||
std::optional<SLE const> sle;
|
||||
try
|
||||
{
|
||||
sle.emplace(SerialIter{item->slice()}, item->key());
|
||||
}
|
||||
catch (std::exception const&)
|
||||
{
|
||||
// Not this walk's to judge: the node verified against its hash,
|
||||
// so a full read of the ledger would meet the same object.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sle->getType() == ltDIR_NODE)
|
||||
{
|
||||
if (!pages.contains(key))
|
||||
continue;
|
||||
|
||||
for (auto const& k : sle->getFieldV256(sfIndexes))
|
||||
pending.push_back(k);
|
||||
|
||||
// The root names the last page, every page in use has an index no
|
||||
// greater, and the directory only ever grows at its end. Asking
|
||||
// for them all now saves a round trip per page; an index not in
|
||||
// use costs at most the node that shows it absent.
|
||||
if (key == root.key)
|
||||
{
|
||||
auto const last = std::min<std::uint64_t>(
|
||||
sle->getFieldU64(sfIndexPrevious), maxPages - 1);
|
||||
for (std::uint64_t i = 1; i <= last; ++i)
|
||||
{
|
||||
auto const pageKey = keylet::page(root, i).key;
|
||||
pages.insert(pageKey);
|
||||
pending.push_back(pageKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (
|
||||
sle->getType() == ltMANIFEST &&
|
||||
sle->isFieldPresent(sfSigningPubKey) &&
|
||||
sle->getFieldU32(sfSequence) !=
|
||||
std::numeric_limits<std::uint32_t>::max())
|
||||
{
|
||||
auto const signing = sle->getFieldVL(sfSigningPubKey);
|
||||
if (publicKeyType(makeSlice(signing)))
|
||||
pending.push_back(
|
||||
keylet::manifest(PublicKey{makeSlice(signing)}).key);
|
||||
}
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
std::vector<uint256>
|
||||
pickManifestCandidates(
|
||||
std::vector<uint256> const& reported,
|
||||
hash_set<uint256> const& skip,
|
||||
std::size_t max)
|
||||
{
|
||||
hash_map<uint256, std::size_t> counts;
|
||||
for (auto const& hash : reported)
|
||||
if (hash.isNonZero() && !skip.contains(hash))
|
||||
++counts[hash];
|
||||
|
||||
std::vector<std::pair<std::size_t, uint256>> ranked;
|
||||
ranked.reserve(counts.size());
|
||||
for (auto const& [hash, count] : counts)
|
||||
ranked.emplace_back(count, hash);
|
||||
|
||||
auto const keep = std::min(max, ranked.size());
|
||||
std::partial_sort(
|
||||
ranked.begin(),
|
||||
ranked.begin() + keep,
|
||||
ranked.end(),
|
||||
[](auto const& a, auto const& b) { return a > b; });
|
||||
|
||||
std::vector<uint256> picked;
|
||||
picked.reserve(keep);
|
||||
for (std::size_t i = 0; i < keep; ++i)
|
||||
picked.push_back(ranked[i].second);
|
||||
return picked;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
manifestQuorum(std::size_t validators, bool trusted, std::size_t quorum)
|
||||
{
|
||||
if (trusted)
|
||||
return std::min(quorum, validators);
|
||||
|
||||
// ceil(0.8 * validators), in integers.
|
||||
return (validators * 4 + 4) / 5;
|
||||
}
|
||||
|
||||
} // namespace ripple
|
||||
@@ -228,11 +228,7 @@ public:
|
||||
|
||||
boost::asio::signal_set m_signals;
|
||||
|
||||
// Once we get C++20, we could use `std::atomic_flag` for `isTimeToStop`
|
||||
// and eliminate the need for the condition variable and the mutex.
|
||||
std::condition_variable stoppingCondition_;
|
||||
mutable std::mutex stoppingMutex_;
|
||||
std::atomic<bool> isTimeToStop = false;
|
||||
std::atomic_flag isTimeToStop;
|
||||
|
||||
std::atomic<bool> checkSigs_;
|
||||
|
||||
@@ -1585,10 +1581,7 @@ ApplicationImp::run()
|
||||
getLoadManager().activateDeadlockDetector();
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lk{stoppingMutex_};
|
||||
stoppingCondition_.wait(lk, [this] { return isTimeToStop.load(); });
|
||||
}
|
||||
isTimeToStop.wait(false, std::memory_order_relaxed);
|
||||
|
||||
JLOG(m_journal.debug()) << "Application stopping";
|
||||
|
||||
@@ -1675,14 +1668,14 @@ ApplicationImp::run()
|
||||
void
|
||||
ApplicationImp::signalStop(std::string msg)
|
||||
{
|
||||
if (!isTimeToStop.exchange(true))
|
||||
if (!isTimeToStop.test_and_set(std::memory_order_acquire))
|
||||
{
|
||||
if (msg.empty())
|
||||
JLOG(m_journal.warn()) << "Server stopping";
|
||||
else
|
||||
JLOG(m_journal.warn()) << "Server stopping: " << msg;
|
||||
|
||||
stoppingCondition_.notify_all();
|
||||
isTimeToStop.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1701,7 +1694,7 @@ ApplicationImp::checkSigs(bool check)
|
||||
bool
|
||||
ApplicationImp::isStopping() const
|
||||
{
|
||||
return isTimeToStop.load();
|
||||
return isTimeToStop.test(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
int
|
||||
|
||||
@@ -159,6 +159,7 @@ printHelp(const po::options_description& desc)
|
||||
" gateway_balances [<ledger>] <issuer_account> [ <hotwallet> [ "
|
||||
"<hotwallet> ]]\n"
|
||||
" get_counts\n"
|
||||
" hook_validate <hex>\n"
|
||||
" json <method> <json>\n"
|
||||
" ledger [<id>|current|closed|validated] [full]\n"
|
||||
" ledger_accept\n"
|
||||
|
||||
@@ -30,6 +30,11 @@ operator<(CanonicalTXSet::Key const& lhs, CanonicalTXSet::Key const& rhs)
|
||||
if (lhs.account_ > rhs.account_)
|
||||
return false;
|
||||
|
||||
// Empty for everything but a time-sequenced transaction, so this sorts
|
||||
// those after the rest of the account's and leaves the rest alone.
|
||||
if (lhs.time_ != rhs.time_)
|
||||
return lhs.time_ < rhs.time_;
|
||||
|
||||
if (lhs.seqProxy_ < rhs.seqProxy_)
|
||||
return true;
|
||||
|
||||
@@ -48,12 +53,28 @@ CanonicalTXSet::accountKey(AccountID const& account)
|
||||
return ret;
|
||||
}
|
||||
|
||||
CanonicalTXSet::Key
|
||||
CanonicalTXSet::makeKey(
|
||||
uint256 const& account,
|
||||
STTx const& tx,
|
||||
uint256 const& id)
|
||||
{
|
||||
return Key(
|
||||
account,
|
||||
tx.getSeqProxy(),
|
||||
tx.isTimeSequenced()
|
||||
? std::optional<std::uint64_t>(tx.getFieldU64(sfTime))
|
||||
: std::nullopt,
|
||||
id);
|
||||
}
|
||||
|
||||
void
|
||||
CanonicalTXSet::insert(std::shared_ptr<STTx const> const& txn)
|
||||
{
|
||||
map_.insert(std::make_pair(
|
||||
Key(accountKey(txn->getAccountID(sfAccount)),
|
||||
txn->getSeqProxy(),
|
||||
makeKey(
|
||||
accountKey(txn->getAccountID(sfAccount)),
|
||||
*txn,
|
||||
txn->getTransactionID()),
|
||||
txn));
|
||||
}
|
||||
@@ -71,10 +92,13 @@ CanonicalTXSet::popAcctTransaction(std::shared_ptr<STTx const> const& tx)
|
||||
//
|
||||
// 3. After handling all transactions with Sequences, return Tickets
|
||||
// with the lowest Ticket ID first.
|
||||
//
|
||||
// 4. After those, return time-sequenced transactions with the oldest
|
||||
// Time first.
|
||||
std::shared_ptr<STTx const> result;
|
||||
uint256 const effectiveAccount{accountKey(tx->getAccountID(sfAccount))};
|
||||
|
||||
Key const after(effectiveAccount, tx->getSeqProxy(), beast::zero);
|
||||
Key const after = makeKey(effectiveAccount, *tx, beast::zero);
|
||||
auto const itrNext{map_.lower_bound(after)};
|
||||
if (itrNext != map_.end() &&
|
||||
itrNext->first.getAccount() == effectiveAccount)
|
||||
|
||||
@@ -25,13 +25,31 @@
|
||||
#include <xrpl/protocol/STTx.h>
|
||||
#include <xrpl/protocol/SeqProxy.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Holds transactions which were deferred to the next pass of consensus.
|
||||
|
||||
"Canonical" refers to the order in which transactions are applied.
|
||||
|
||||
- Puts transactions from the same account in SeqProxy order
|
||||
- Puts transactions from the same account in SeqProxy order: Sequences,
|
||||
then Tickets
|
||||
- Then the account's time-sequenced transactions (featureJsonTx), oldest
|
||||
Time first
|
||||
|
||||
Without a time-sequenced transaction in the set this is the SeqProxy
|
||||
order it always was. A Time on a transaction with a Sequence or a Ticket
|
||||
changes nothing: only time-sequenced transactions check or record
|
||||
sfLastTxnTime, so oldest first is all their order has to satisfy.
|
||||
|
||||
They go after the Tickets for the reason Tickets go after Sequences. A
|
||||
time-sequenced transaction does not say which Sequence it followed, but
|
||||
what it does can depend on it - a TicketCreate numbers its Tickets from
|
||||
the account Sequence. A transaction with a Sequence or a Ticket that
|
||||
really came after it fails terPRE_SEQ or terPRE_TICKET here and is
|
||||
retried; one that came before it, applied second, could have its
|
||||
Sequence or Ticket taken and fail for good.
|
||||
|
||||
*/
|
||||
// VFALCO TODO rename to SortedTxSet
|
||||
@@ -41,8 +59,11 @@ private:
|
||||
class Key
|
||||
{
|
||||
public:
|
||||
Key(uint256 const& account, SeqProxy seqProx, uint256 const& id)
|
||||
: account_(account), txId_(id), seqProxy_(seqProx)
|
||||
Key(uint256 const& account,
|
||||
SeqProxy seqProx,
|
||||
std::optional<std::uint64_t> time,
|
||||
uint256 const& id)
|
||||
: account_(account), txId_(id), seqProxy_(seqProx), time_(time)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -95,6 +116,11 @@ private:
|
||||
uint256 account_;
|
||||
uint256 txId_;
|
||||
SeqProxy seqProxy_;
|
||||
// sfTime of a time-sequenced transaction, and nothing for any other.
|
||||
// Compared before seqProxy_: an empty optional is less than every
|
||||
// Time, so time-sequenced transactions come after the rest of their
|
||||
// account's, and among themselves oldest first.
|
||||
std::optional<std::uint64_t> time_;
|
||||
};
|
||||
|
||||
friend bool
|
||||
@@ -104,6 +130,10 @@ private:
|
||||
uint256
|
||||
accountKey(AccountID const& account);
|
||||
|
||||
// The Key for tx under the salted account key, id breaking ties
|
||||
static Key
|
||||
makeKey(uint256 const& account, STTx const& tx, uint256 const& id);
|
||||
|
||||
public:
|
||||
using const_iterator =
|
||||
std::map<Key, std::shared_ptr<STTx const>>::const_iterator;
|
||||
|
||||
+432
-122
@@ -20,13 +20,20 @@
|
||||
#ifndef RIPPLE_APP_MISC_MANIFEST_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_MANIFEST_H_INCLUDED
|
||||
|
||||
#include <xrpld/app/misc/RotatingBloomFilter.h>
|
||||
#include <xrpl/basics/UnorderedContainers.h>
|
||||
#include <xrpl/basics/chrono.h>
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/PublicKey.h>
|
||||
#include <xrpl/protocol/SecretKey.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
@@ -36,6 +43,8 @@
|
||||
namespace ripple {
|
||||
|
||||
class ReadView;
|
||||
class STLedgerEntry;
|
||||
using SLE = STLedgerEntry;
|
||||
|
||||
/*
|
||||
Validator key manifests
|
||||
@@ -64,8 +73,9 @@ class ReadView;
|
||||
master public key, and (b) the most senior of all valid manifests it has
|
||||
seen for that validator, if any. On startup, the [validator_token] config
|
||||
entry (which contains the manifest for this validator) is decoded and
|
||||
added to the manifest cache. Other manifests are added as "gossip"
|
||||
received from rippled peers.
|
||||
added to the manifest cache. Other manifests arrive in published validator
|
||||
lists, from ltMANIFEST ledger objects, and as "gossip" received from
|
||||
rippled peers; see ManifestCache for how each is retained.
|
||||
|
||||
When an ephemeral key is compromised, a new signing key pair is created,
|
||||
along with a new manifest vouching for it (with a higher sequence number),
|
||||
@@ -242,7 +252,17 @@ enum class ManifestDisposition {
|
||||
badEphemeralKey,
|
||||
|
||||
/// Timely, but invalid signature
|
||||
invalid
|
||||
invalid,
|
||||
|
||||
/// Gossip introducing a master key not held, whose ephemeral key has not
|
||||
/// recently signed a validation this node verified, or which names no
|
||||
/// ephemeral key at all. Not admitted and not relayed; if it names an
|
||||
/// ephemeral key it is held pending, unverified, until that key is seen.
|
||||
unseen,
|
||||
|
||||
/// Gossip with nowhere to go: every manifest held from gossip has been
|
||||
/// more active than this one could claim to be.
|
||||
full
|
||||
};
|
||||
|
||||
inline std::string
|
||||
@@ -260,6 +280,58 @@ to_string(ManifestDisposition m)
|
||||
return "badEphemeralKey";
|
||||
case ManifestDisposition::invalid:
|
||||
return "invalid";
|
||||
case ManifestDisposition::unseen:
|
||||
return "unseen";
|
||||
case ManifestDisposition::full:
|
||||
return "full";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/** Where a held manifest came from, in increasing order of precedence.
|
||||
|
||||
The source never makes a manifest valid. Whatever its source, a manifest
|
||||
is accepted only if its signatures verify and its sequence is newer than
|
||||
the one held. The source decides how long the cache keeps it, and which
|
||||
side loses when two held manifests claim the same key.
|
||||
*/
|
||||
enum class ManifestSource : std::uint8_t {
|
||||
/** Relayed by a peer: the legacy transport, open to anyone.
|
||||
|
||||
A master key not already held from gossip is admitted only if its
|
||||
ephemeral key has recently signed a validation this node verified, and
|
||||
only into one of ManifestCache::gossipCapacity slots, which go to the
|
||||
most active.
|
||||
*/
|
||||
gossip = 0,
|
||||
|
||||
/** Read from an ltMANIFEST ledger object.
|
||||
|
||||
The ledger is the store and this is a lookup cache over it, of
|
||||
ManifestCache::ledgerCapacity entries: dropping one costs a SHAMap
|
||||
read the next time it is wanted, and loses nothing. Revocations are
|
||||
expected to live here, since a validator revokes on-ledger.
|
||||
*/
|
||||
ledger = 1,
|
||||
|
||||
/** Carried in a validator list, the config, or the wallet those were
|
||||
saved to. Never evicted.
|
||||
*/
|
||||
list = 2,
|
||||
};
|
||||
|
||||
inline std::string
|
||||
to_string(ManifestSource s)
|
||||
{
|
||||
switch (s)
|
||||
{
|
||||
case ManifestSource::gossip:
|
||||
return "gossip";
|
||||
case ManifestSource::ledger:
|
||||
return "ledger";
|
||||
case ManifestSource::list:
|
||||
return "list";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
@@ -267,65 +339,51 @@ to_string(ManifestDisposition m)
|
||||
|
||||
class DatabaseCon;
|
||||
|
||||
/** Remembers manifests with the highest sequence number. */
|
||||
/** Remembers manifests with the highest sequence number.
|
||||
|
||||
Every master key held is retained in one of three tiers:
|
||||
|
||||
- list: the master key is pinned (trusted), configured, or a validator
|
||||
list or the wallet carried a manifest for it. Never evicted, whatever
|
||||
transport later delivered a newer manifest for it.
|
||||
|
||||
- ledger: otherwise, if the manifest held was read from the ledger. A
|
||||
lookup cache over the ledger, capped at ledgerCapacity; when full the
|
||||
least active entry is dropped, to be read again on demand.
|
||||
|
||||
- gossip: otherwise. Capped at gossipCapacity. A master key enters only
|
||||
if its ephemeral key is in seen_, a rotating bloom filter of ephemeral
|
||||
keys that recently signed a validation this node verified, and only by
|
||||
displacing an entry less active than a single fresh sighting.
|
||||
|
||||
Activity is the number of validations seen from the held ephemeral key,
|
||||
decayed exponentially with a half-life of halfLife. It is what decides
|
||||
eviction in both capped tiers.
|
||||
|
||||
Gossip that fails the bloom filter is held, unverified, in a fixed ring of
|
||||
pendingCapacity entries keyed by ephemeral key. The first validation
|
||||
signed by that key releases it into the cache. Without this a validator's
|
||||
manifest, which normally arrives before its first validation, would be
|
||||
refused everywhere and never offered again.
|
||||
|
||||
A held manifest that conflicts with a new one over a key, and is held in a
|
||||
lower tier than the new one would be, is evicted rather than the new one
|
||||
refused. A manifest is never evicted by one ranked at or below it.
|
||||
|
||||
Ledger access goes through readLedger() alone, so a source other than a
|
||||
full SHAMap can be substituted there.
|
||||
*/
|
||||
class ManifestCache
|
||||
{
|
||||
private:
|
||||
beast::Journal j_;
|
||||
std::shared_mutex mutable mutex_;
|
||||
|
||||
/** Active manifests stored by master public key. */
|
||||
hash_map<PublicKey, Manifest> map_;
|
||||
|
||||
/** Master public keys stored by current ephemeral public key. */
|
||||
hash_map<PublicKey, PublicKey> signingToMasterKeys_;
|
||||
|
||||
/** Master keys always offered to a peer, whatever their recency.
|
||||
|
||||
Set by pin(); in practice the master keys on the configured validator
|
||||
lists, which are the manifests consensus actually depends on.
|
||||
*/
|
||||
hash_set<PublicKey> pinned_;
|
||||
|
||||
/** Recency of use, keyed by master public key.
|
||||
|
||||
The structure of this map is guarded by mutex_ exactly as map_ is:
|
||||
entries are created next to it in applyManifest() and are never
|
||||
removed. The counters themselves are atomic, so recording a hit is a
|
||||
write to an atomic rather than a structural modification and is legal
|
||||
while only a shared lock is held.
|
||||
|
||||
This is deliberately not a second mutex. A second mutex would have to
|
||||
be ordered against mutex_, and that ordering would be an unenforced
|
||||
invariant that any future caller could invert.
|
||||
*/
|
||||
hash_map<PublicKey, std::atomic<std::uint64_t>> mutable lastUsed_;
|
||||
std::atomic<std::uint64_t> mutable tick_{0};
|
||||
|
||||
/** Ephemeral keys already probed against the ledger, and where.
|
||||
|
||||
Bounds the reads driven by incoming validations to one per key per
|
||||
ledger. A key that resolves does not come back here: the mapping then
|
||||
lives in signingToMasterKeys_ and applyLedgerSigningKey() answers from
|
||||
it before reaching this map.
|
||||
|
||||
Unlike lastUsed_ this is written structurally, so it is guarded by
|
||||
mutex_ in exclusive mode.
|
||||
*/
|
||||
hash_map<PublicKey, std::uint32_t> probed_;
|
||||
|
||||
/** Record that a manifest was looked up.
|
||||
|
||||
@pre The caller holds mutex_, shared or exclusive.
|
||||
*/
|
||||
void
|
||||
touch(PublicKey const& masterKey) const;
|
||||
|
||||
std::atomic<std::uint32_t> seq_{0};
|
||||
|
||||
public:
|
||||
/** Ceiling on the unpinned manifests offered to a newly connected peer. */
|
||||
static constexpr std::size_t gossipLimit = 64;
|
||||
/** Manifests held for gossip-tier master keys. */
|
||||
static constexpr std::size_t gossipCapacity = 64;
|
||||
|
||||
/** Manifests held for ledger-tier master keys: a lookup cache. */
|
||||
static constexpr std::size_t ledgerCapacity = 512;
|
||||
|
||||
/** Gossip manifests waiting, unverified, for their ephemeral key. */
|
||||
static constexpr std::size_t pendingCapacity = 64;
|
||||
|
||||
/** Ceiling on remembered ephemeral key probes before they are dropped.
|
||||
|
||||
@@ -334,11 +392,202 @@ public:
|
||||
*/
|
||||
static constexpr std::size_t probeLimit = 4096;
|
||||
|
||||
explicit ManifestCache(
|
||||
beast::Journal j = beast::Journal(beast::Journal::getNullSink()))
|
||||
: j_(j)
|
||||
/** Time over which a master key's validation activity halves. */
|
||||
static constexpr std::chrono::seconds halfLife{300};
|
||||
|
||||
/** Epoch of seen_. A key is remembered for three to four of them. */
|
||||
static constexpr std::chrono::seconds seenInterval{120};
|
||||
|
||||
/** Ephemeral keys recently seen signing a verified validation.
|
||||
|
||||
2 KiB (16 Kibit) per generation, 4 generations: 8 KiB in all. Around
|
||||
0.2% false positives per live generation with a thousand distinct keys
|
||||
in it.
|
||||
*/
|
||||
using SeenFilter = RotatingBloomFilter<std::size_t{1} << 14, 4, 4>;
|
||||
|
||||
private:
|
||||
struct Slot
|
||||
{
|
||||
}
|
||||
Slot(
|
||||
ManifestSource source_,
|
||||
ManifestSource tier_,
|
||||
bool vouched_,
|
||||
double weight_)
|
||||
: source(source_), tier(tier_), vouched(vouched_), weight(weight_)
|
||||
{
|
||||
}
|
||||
|
||||
/// Where the manifest held for this master key came from.
|
||||
ManifestSource source;
|
||||
|
||||
/// What it is retained as; see tierOf().
|
||||
ManifestSource tier;
|
||||
|
||||
/// A validator list, the config or the wallet once carried a manifest
|
||||
/// for this master key. Survives later updates from other sources.
|
||||
bool vouched;
|
||||
|
||||
/** Validations seen from the held ephemeral key, decayed.
|
||||
|
||||
Stored as the sum, over the times t it was seen, of
|
||||
2^((t - weightBase_) / halfLife). Every slot shares weightBase_, so
|
||||
comparing two stored weights compares their decayed values at any
|
||||
common instant, without computing a decay.
|
||||
|
||||
Atomic so that a sighting is recorded under a shared lock.
|
||||
Everything else in a slot changes only under an exclusive one.
|
||||
*/
|
||||
std::atomic<double> weight;
|
||||
};
|
||||
|
||||
struct Pending
|
||||
{
|
||||
/// Arrival order; the lowest is replaced first.
|
||||
std::uint64_t order = 0;
|
||||
std::optional<Manifest> manifest;
|
||||
/// manifest has been checked with Manifest::verify(), and passed.
|
||||
bool verified = false;
|
||||
};
|
||||
|
||||
beast::Journal j_;
|
||||
Stopwatch& clock_;
|
||||
std::shared_mutex mutable mutex_;
|
||||
|
||||
/** Active manifests stored by master public key. */
|
||||
hash_map<PublicKey, Manifest> map_;
|
||||
|
||||
/** Master public keys stored by current ephemeral public key. */
|
||||
hash_map<PublicKey, PublicKey> signingToMasterKeys_;
|
||||
|
||||
/** Retention state, by master public key. In step with map_. */
|
||||
hash_map<PublicKey, Slot> slots_;
|
||||
|
||||
/** Number of slots_ in each tier, indexed by ManifestSource. */
|
||||
std::array<std::size_t, 3> tierSize_{};
|
||||
|
||||
/** Master keys retained as list regardless of source; set by pin(). */
|
||||
hash_set<PublicKey> pinned_;
|
||||
|
||||
/** Master keys of the configured validator token and revocation. */
|
||||
hash_set<PublicKey> configured_;
|
||||
|
||||
std::array<Pending, pendingCapacity> pending_;
|
||||
std::uint64_t pendingOrder_ = 0;
|
||||
std::size_t pendingSize_ = 0;
|
||||
|
||||
/** Ephemeral keys already probed against the ledger, and where.
|
||||
|
||||
Bounds the reads driven by incoming validations to one per key per
|
||||
ledger. Guarded by mutex_ in exclusive mode.
|
||||
*/
|
||||
hash_map<PublicKey, std::uint32_t> probed_;
|
||||
|
||||
/** Reference instant for Slot::weight. Moved forward by rebase(). */
|
||||
Stopwatch::time_point weightBase_;
|
||||
|
||||
SeenFilter seen_{seenInterval};
|
||||
|
||||
std::atomic<std::uint32_t> seq_{0};
|
||||
|
||||
/** Tier a master key is retained in, given where its manifest came from.
|
||||
|
||||
@pre The caller holds mutex_, shared or exclusive.
|
||||
*/
|
||||
ManifestSource
|
||||
tierOf(PublicKey const& master, ManifestSource source, bool vouched) const;
|
||||
|
||||
/** Weight of a single sighting at now. @pre mutex_ held. */
|
||||
double
|
||||
unit(Stopwatch::time_point now) const;
|
||||
|
||||
/** Whether weightBase_ is far enough behind to need moving now.
|
||||
|
||||
@pre mutex_ held.
|
||||
*/
|
||||
bool
|
||||
overdue(Stopwatch::time_point now) const;
|
||||
|
||||
/** Move weightBase_ toward now, rescaling every weight to match.
|
||||
|
||||
@pre mutex_ held exclusively.
|
||||
*/
|
||||
void
|
||||
rebase(Stopwatch::time_point now);
|
||||
|
||||
/** The least active entry in a tier, other than exclude.
|
||||
|
||||
@pre mutex_ held.
|
||||
*/
|
||||
std::optional<std::pair<PublicKey, double>>
|
||||
coldest(ManifestSource tier, PublicKey const* exclude) const;
|
||||
|
||||
/** Whether master could enter tier, competing with weight.
|
||||
|
||||
A capped tier that is full admits by evicting its least active entry.
|
||||
The lookup cache always does; the gossip tier only if that entry is
|
||||
less active than weight.
|
||||
|
||||
@pre mutex_ held.
|
||||
*/
|
||||
bool
|
||||
admissible(ManifestSource tier, PublicKey const& master, double weight)
|
||||
const;
|
||||
|
||||
/** Evict what admissible() said could be evicted.
|
||||
|
||||
@pre mutex_ held exclusively.
|
||||
*/
|
||||
bool
|
||||
makeRoom(ManifestSource tier, PublicKey const& master, double weight);
|
||||
|
||||
/** Forget a master key entirely. @pre mutex_ held exclusively. */
|
||||
void
|
||||
erase(PublicKey const& master);
|
||||
|
||||
/** Move a slot to another tier. @pre mutex_ held exclusively. */
|
||||
void
|
||||
retier(Slot& slot, ManifestSource tier);
|
||||
|
||||
/** Hold gossip until its ephemeral key is seen.
|
||||
|
||||
@param verified m has already been checked with Manifest::verify().
|
||||
Only a verified manifest may displace one already waiting for
|
||||
the same ephemeral key.
|
||||
|
||||
@pre mutex_ held exclusively; m names an ephemeral key.
|
||||
*/
|
||||
void
|
||||
addPending(Manifest m, bool verified);
|
||||
|
||||
/** @pre mutex_ held. */
|
||||
bool
|
||||
hasPending(PublicKey const& signingKey) const;
|
||||
|
||||
/** @pre mutex_ held exclusively. */
|
||||
std::optional<Manifest>
|
||||
takePending(PublicKey const& signingKey);
|
||||
|
||||
/** Adopt a gossip entry the ledger turns out to hold verbatim.
|
||||
|
||||
It is then a ledger lookup cache entry, and frees its gossip slot.
|
||||
|
||||
@pre mutex_ held exclusively.
|
||||
*/
|
||||
void
|
||||
adopt(Manifest const& m);
|
||||
|
||||
/** The ledger object for a manifest, keyed by its master or ephemeral key.
|
||||
|
||||
The one place the cache reads the ledger.
|
||||
*/
|
||||
std::shared_ptr<SLE const>
|
||||
readLedger(ReadView const& view, PublicKey const& key) const;
|
||||
|
||||
public:
|
||||
explicit ManifestCache(
|
||||
beast::Journal j = beast::Journal(beast::Journal::getNullSink()),
|
||||
Stopwatch& clock = stopwatch());
|
||||
|
||||
/** A monotonically increasing number used to detect new manifests. */
|
||||
std::uint32_t
|
||||
@@ -408,24 +657,65 @@ public:
|
||||
bool
|
||||
revoked(PublicKey const& pk) const;
|
||||
|
||||
/** Returns the tier a master key is held in, if it is held.
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::optional<ManifestSource>
|
||||
getTier(PublicKey const& pk) const;
|
||||
|
||||
/** Add manifest to cache.
|
||||
|
||||
@param m Manifest to add
|
||||
|
||||
@param source Where it came from. The default is the trusted path
|
||||
taken by validator lists, the config and the wallet.
|
||||
|
||||
@param view For gossip, a ledger to consult first. A master key whose
|
||||
ledger object is at the same or a later sequence -- above all, a
|
||||
revocation -- has that object ingested instead, and the gossip is
|
||||
stale. Ignored for other sources.
|
||||
|
||||
@return `ManifestDisposition::accepted` if successful, or
|
||||
`stale` or `invalid` otherwise
|
||||
the disposition explaining why admission was refused
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
ManifestDisposition
|
||||
applyManifest(Manifest m);
|
||||
applyManifest(
|
||||
Manifest m,
|
||||
ManifestSource source = ManifestSource::list,
|
||||
ReadView const* view = nullptr);
|
||||
|
||||
/** Set the master keys that are always offered to peers.
|
||||
/** Record a verified, current validation signed by an ephemeral key.
|
||||
|
||||
Replaces any previous set. Bumps sequence() when the set actually
|
||||
changes, so a cached gossip message built from it is rebuilt.
|
||||
Marks the key as seen for gossip admission, adds to the activity of the
|
||||
master key it belongs to, and releases a pending manifest for it.
|
||||
|
||||
@param signingKey Ephemeral key that signed the validation
|
||||
|
||||
@param view Passed on to applyManifest() for a released manifest
|
||||
|
||||
@return the manifest released and accepted, if any, for the caller to
|
||||
relay
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::optional<Manifest>
|
||||
noteValidation(PublicKey const& signingKey, ReadView const* view = nullptr);
|
||||
|
||||
/** Set the master keys retained as list, whatever their source.
|
||||
|
||||
Replaces any previous set. A key that leaves falls back to the tier of
|
||||
its source, which sheds its least active entries if that puts it over
|
||||
capacity. Bumps sequence() when the set actually changes, so a cached
|
||||
gossip message built from it is rebuilt.
|
||||
|
||||
@param keys Master public keys to pin
|
||||
|
||||
@@ -458,8 +748,7 @@ public:
|
||||
manifest found and feeds it through applyManifest(), so an on-chain
|
||||
manifest is subject to exactly the same staleness, revocation and
|
||||
key-reuse rules -- and the same signature check -- as one arriving by
|
||||
peer gossip or in a published list. It is a third source of manifests,
|
||||
not a more trusted one.
|
||||
peer gossip or in a published list.
|
||||
|
||||
Probes a known key set rather than scanning the ledger's transactions:
|
||||
this costs one SHAMap read per key, and picks up manifests published in
|
||||
@@ -477,22 +766,39 @@ public:
|
||||
std::size_t
|
||||
applyLedger(ReadView const& view, hash_set<PublicKey> const& masterKeys);
|
||||
|
||||
/** Refresh from the ledger everything held or pinned.
|
||||
|
||||
Probes every pinned and configured master key, and every master key
|
||||
held in any tier. This is what brings in a revocation, which is
|
||||
published on-ledger by master key and nowhere else, for a master key
|
||||
whose ephemeral key the cache already resolves and so never misses on.
|
||||
|
||||
@return the number of manifests newly accepted
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::size_t
|
||||
applyLedger(ReadView const& view);
|
||||
|
||||
/** Resolve an ephemeral signing key against a manifest published on-ledger.
|
||||
|
||||
applyLedger() probes a known master key set, which cannot help a key
|
||||
this node has no manifest for: the master key is exactly what is
|
||||
missing. SetManifest writes a second copy of every manifest keyed by
|
||||
its ephemeral key, so that case is one read rather than a search.
|
||||
The miss path of the ledger tier. applyLedger() probes a known master
|
||||
key set, which cannot help a key this node has no manifest for: the
|
||||
master key is exactly what is missing. SetManifest writes a second copy
|
||||
of every manifest keyed by its ephemeral key, so that case is one read
|
||||
rather than a search.
|
||||
|
||||
Anything found is fed through applyManifest(), so an on-chain manifest
|
||||
faces the same signature check and the same staleness, revocation and
|
||||
key-reuse rules as one arriving by gossip. The ledger is a transport
|
||||
here, not an authority: the answer is read back out of the cache rather
|
||||
than taken from the ledger object, because applyManifest() may decline
|
||||
it.
|
||||
key-reuse rules as one arriving by gossip. The answer is read back out
|
||||
of the cache rather than taken from the ledger object, because
|
||||
applyManifest() may decline it.
|
||||
|
||||
A key is probed at most once per ledger, and a key that resolves is
|
||||
answered from the cache thereafter without any ledger read.
|
||||
answered from the cache thereafter without any ledger read, until it
|
||||
is evicted.
|
||||
|
||||
@param view Ledger to read from
|
||||
@param signingKey Ephemeral public key to resolve
|
||||
@@ -506,6 +812,37 @@ public:
|
||||
std::optional<PublicKey>
|
||||
applyLedgerSigningKey(ReadView const& view, PublicKey const& signingKey);
|
||||
|
||||
/** Ingest the manifests listed in the ledger's manifest directory.
|
||||
|
||||
Every master key with a manifest on-ledger is listed in
|
||||
keylet::manifestDir(), so unlike applyLedger() this needs no key to
|
||||
start from. Pinned, configured and held master keys are refreshed as
|
||||
applyLedger() would; any other is admitted to the ledger tier only
|
||||
while it has room, so a bulk read fills the lookup cache without
|
||||
churning it.
|
||||
|
||||
The view need not be complete: one in which the directory and the
|
||||
objects it lists can be read will do. That is what lets a node read
|
||||
them during sync, before it holds the rest of the state map. The
|
||||
ledger need not be validated either: every manifest read is verified
|
||||
as from any other source, and the cache only ever moves forward, so
|
||||
the worst an unvalidated ledger can do is leave something out.
|
||||
|
||||
@param view Ledger to read from
|
||||
@param maxPages Most directory pages to read; see
|
||||
forEachLedgerManifest()
|
||||
|
||||
@return the number of manifests newly accepted
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
May be called concurrently
|
||||
*/
|
||||
std::size_t
|
||||
applyLedgerDirectory(
|
||||
ReadView const& view,
|
||||
std::uint64_t maxPages = std::numeric_limits<std::uint64_t>::max());
|
||||
|
||||
/** Populate manifest cache with manifests in database and config.
|
||||
|
||||
@param dbCon Database connection with dbTable
|
||||
@@ -542,11 +879,15 @@ public:
|
||||
void
|
||||
load(DatabaseCon& dbCon, std::string const& dbTable);
|
||||
|
||||
/** Save cached manifests to database.
|
||||
/** Save list-tier manifests to database.
|
||||
|
||||
Ledger-tier manifests are on the ledger already, revocations included,
|
||||
and gossip-tier ones are not worth carrying across a restart.
|
||||
|
||||
@param dbCon Database connection with `ValidatorManifests` table
|
||||
|
||||
@param isTrusted Function that returns true if manifest is trusted
|
||||
@param isTrusted Function that returns true if manifest is trusted.
|
||||
Called without the cache locked, so it may call back into it.
|
||||
|
||||
@par Thread Safety
|
||||
|
||||
@@ -614,19 +955,16 @@ public:
|
||||
|
||||
/** Invokes the callback for the manifests worth offering a new peer.
|
||||
|
||||
Offering the whole cache means offering everything the node has ever
|
||||
seen, which grows without bound and is mostly of no use to the peer.
|
||||
This offers the pinned set plus up to gossipLimit further manifests,
|
||||
most recently used first. A manifest left out still reaches the peer by
|
||||
ordinary relay if it turns out to be needed.
|
||||
The list and gossip tiers: at most the listed validators plus
|
||||
gossipCapacity. The ledger tier is left out, since any peer that is
|
||||
not amendment blocked can read it from the ledger.
|
||||
|
||||
@note Do not call ManifestCache member functions from within the
|
||||
callback. This can re-lock the mutex from the same thread, which is UB.
|
||||
@note Do not write ManifestCache member variables from
|
||||
within the callback. This can lead to data races.
|
||||
|
||||
@param pf Pre-function called with the maximum number of times f will be
|
||||
called (useful for memory allocations)
|
||||
@param pf Pre-function called with the number of times f will be called
|
||||
|
||||
@param f Function called for each manifest
|
||||
|
||||
@@ -640,40 +978,12 @@ public:
|
||||
{
|
||||
std::shared_lock lock{mutex_};
|
||||
|
||||
// Rank the unpinned entries by recency and keep the head. Pointers
|
||||
// into map_ stay valid: it is node based and the shared lock is held
|
||||
// throughout.
|
||||
std::vector<std::pair<std::uint64_t, PublicKey const*>> ranked;
|
||||
ranked.reserve(map_.size());
|
||||
for (auto const& [key, manifest] : map_)
|
||||
{
|
||||
(void)manifest;
|
||||
if (pinned_.count(key))
|
||||
continue;
|
||||
auto const used = lastUsed_.find(key);
|
||||
ranked.emplace_back(
|
||||
used == lastUsed_.end()
|
||||
? 0
|
||||
: used->second.load(std::memory_order_relaxed),
|
||||
&key);
|
||||
}
|
||||
pf(tierSize_[static_cast<std::size_t>(ManifestSource::list)] +
|
||||
tierSize_[static_cast<std::size_t>(ManifestSource::gossip)]);
|
||||
|
||||
auto const keep = std::min(gossipLimit, ranked.size());
|
||||
std::partial_sort(
|
||||
ranked.begin(),
|
||||
ranked.begin() + keep,
|
||||
ranked.end(),
|
||||
[](auto const& a, auto const& b) { return a.first > b.first; });
|
||||
|
||||
// An upper bound: a pinned key need not have a manifest yet.
|
||||
pf(pinned_.size() + keep);
|
||||
|
||||
for (auto const& key : pinned_)
|
||||
if (auto const iter = map_.find(key); iter != map_.end())
|
||||
f(iter->second);
|
||||
|
||||
for (std::size_t i = 0; i < keep; ++i)
|
||||
f(map_.find(*ranked[i].second)->second);
|
||||
for (auto const& [key, slot] : slots_)
|
||||
if (slot.tier != ManifestSource::ledger)
|
||||
f(map_.find(key)->second);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_MISC_MANIFESTLEDGER_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_MANIFESTLEDGER_H_INCLUDED
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/Protocol.h>
|
||||
#include <xrpl/protocol/STLedgerEntry.h>
|
||||
#include <xrpl/protocol/TER.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
class ApplyView;
|
||||
class ReadView;
|
||||
struct Manifest;
|
||||
|
||||
/** Write a manifest to the ledger, replacing any earlier one for its master.
|
||||
|
||||
A manifest is stored twice so that it can be found from either key:
|
||||
|
||||
keylet::manifest(masterKey) -> obj1, sfManifestID -> obj2
|
||||
keylet::manifest(signingKey) -> obj2, sfManifestID -> obj1
|
||||
|
||||
A revocation has no signing key, so it is stored only as obj1, with no
|
||||
sfManifestID. The master key's account points at obj1 with its own
|
||||
sfManifestID.
|
||||
|
||||
obj1 is listed in keylet::manifestDir() and records its page there in
|
||||
sfOwnerNode. The entry is made with the master key's first manifest and is
|
||||
never removed: every later manifest for the master key, a revocation
|
||||
included, is written at the same key, so the entry stays and the page is
|
||||
carried from the old obj1 to the new. Removing and re-adding it instead
|
||||
would move it between pages on every key rotation for no gain. obj2 is not
|
||||
listed; it moves with the ephemeral key, and a reader finds it from obj1.
|
||||
|
||||
Both of the old objects are erased and both new ones written afresh, so the
|
||||
two copies can never drift apart.
|
||||
|
||||
@param view View to write to
|
||||
@param account The master key's account root, which is updated
|
||||
@param manifest The manifest as signed, in its serialized STObject form
|
||||
@param parsed The same manifest, deserialized and checked by the caller
|
||||
@param j Journal for reporting a corrupt ledger
|
||||
|
||||
@return tesSUCCESS; tecDIR_FULL if the directory cannot take another
|
||||
master key; tefBAD_LEDGER if the existing objects are inconsistent
|
||||
*/
|
||||
TER
|
||||
writeManifestObjects(
|
||||
ApplyView& view,
|
||||
std::shared_ptr<SLE> const& account,
|
||||
STObject const& manifest,
|
||||
Manifest const& parsed,
|
||||
beast::Journal j);
|
||||
|
||||
/** Visit every master key's manifest object listed in keylet::manifestDir().
|
||||
|
||||
Each master key is visited once, with its current object: a revocation if
|
||||
it has been revoked. Entries that do not name an ltMANIFEST object are
|
||||
skipped.
|
||||
|
||||
The view need not be complete. It must be able to read the directory and
|
||||
the objects listed in it, which is all this reads; a view over a partially
|
||||
synced state map that holds them will do.
|
||||
|
||||
@param view View to read from
|
||||
@param f Called with each master manifest object
|
||||
@param maxPages Most directory pages to read, the root page included.
|
||||
Pages are only ever appended, so this reads the oldest entries. A
|
||||
partial view need hold no more pages than this.
|
||||
*/
|
||||
void
|
||||
forEachLedgerManifest(
|
||||
ReadView const& view,
|
||||
std::function<void(std::shared_ptr<SLE const> const&)> const& f,
|
||||
std::uint64_t maxPages = dirNodeMaxPages);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <xrpld/app/ledger/LedgerMaster.h>
|
||||
#include <xrpld/app/ledger/LedgerToJson.h>
|
||||
#include <xrpld/app/ledger/LocalTxs.h>
|
||||
#include <xrpld/app/ledger/ManifestSync.h>
|
||||
#include <xrpld/app/ledger/OpenLedger.h>
|
||||
#include <xrpld/app/ledger/OrderBookDB.h>
|
||||
#include <xrpld/app/ledger/TransactionMaster.h>
|
||||
@@ -319,6 +320,13 @@ private:
|
||||
void
|
||||
publishNewerManifests(ReadView const& ledger);
|
||||
|
||||
/** Read manifests from ledgers peers report, while too few of this node's
|
||||
validators can be resolved to trust any ledger. See
|
||||
[early_manifest_sync].
|
||||
*/
|
||||
void
|
||||
bootstrapManifests();
|
||||
|
||||
public:
|
||||
bool
|
||||
beginConsensus(
|
||||
@@ -667,6 +675,12 @@ private:
|
||||
std::atomic<OperatingMode> mMode;
|
||||
|
||||
std::atomic<bool> needNetworkLedger_{false};
|
||||
|
||||
/** State of bootstrapManifests(), which only the heartbeat calls. */
|
||||
std::mutex manifestBootstrapMutex_;
|
||||
hash_set<uint256> manifestBootstrapTried_;
|
||||
std::chrono::steady_clock::time_point manifestBootstrapNext_{};
|
||||
|
||||
std::atomic<bool> amendmentBlocked_{false};
|
||||
std::atomic<bool> amendmentWarned_{false};
|
||||
std::atomic<bool> unlBlocked_{false};
|
||||
@@ -1027,6 +1041,8 @@ NetworkOPsImp::processHeartbeatTimer()
|
||||
}
|
||||
CLOG(clog.ss()) << ". ";
|
||||
|
||||
bootstrapManifests();
|
||||
|
||||
setHeartbeatTimer();
|
||||
}
|
||||
|
||||
@@ -1246,6 +1262,105 @@ NetworkOPsImp::publishNewerManifests(ReadView const& ledger)
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/** How often a node that cannot resolve enough of its validators looks for
|
||||
ledgers to read their manifests from. */
|
||||
constexpr std::chrono::seconds manifestBootstrapInterval{15};
|
||||
|
||||
/** Most ledgers it starts reading at each look. */
|
||||
constexpr std::size_t manifestBootstrapWidth = 3;
|
||||
|
||||
/** Ledgers remembered as read, past which the record starts over. */
|
||||
constexpr std::size_t manifestBootstrapMemory = 256;
|
||||
|
||||
} // namespace
|
||||
|
||||
void
|
||||
NetworkOPsImp::bootstrapManifests()
|
||||
{
|
||||
// Only until a ledger has been validated: from then on the manifests are
|
||||
// kept current from each ledger as it closes, in beginConsensus().
|
||||
if (!app_.config().EARLY_MANIFEST_SYNC || m_ledgerMaster.haveValidated())
|
||||
return;
|
||||
|
||||
std::vector<uint256> picked;
|
||||
{
|
||||
auto const now = std::chrono::steady_clock::now();
|
||||
std::lock_guard lock(manifestBootstrapMutex_);
|
||||
if (now < manifestBootstrapNext_)
|
||||
return;
|
||||
manifestBootstrapNext_ = now + manifestBootstrapInterval;
|
||||
|
||||
// The validators whose manifests matter: the trusted ones once the
|
||||
// trusted set has been worked out, every listed one before that.
|
||||
auto keys = app_.validators().getTrustedMasterKeys();
|
||||
bool const trusted = !keys.empty();
|
||||
if (!trusted)
|
||||
app_.validators().for_each_listed(
|
||||
[&keys](PublicKey const& pk, bool) { keys.insert(pk); });
|
||||
if (keys.empty())
|
||||
return;
|
||||
|
||||
// Enough of them to make a quorum.
|
||||
std::size_t const needed =
|
||||
manifestQuorum(keys.size(), trusted, app_.validators().quorum());
|
||||
|
||||
auto const& manifests = app_.validatorManifests();
|
||||
std::size_t resolved = 0;
|
||||
for (auto const& pk : keys)
|
||||
if (manifests.getManifest(pk))
|
||||
++resolved;
|
||||
|
||||
if (resolved >= needed)
|
||||
return;
|
||||
|
||||
std::vector<uint256> reported;
|
||||
for (auto const& peer : app_.overlay().getActivePeers())
|
||||
reported.push_back(peer->getClosedLedgerHash());
|
||||
|
||||
if (manifestBootstrapTried_.size() > manifestBootstrapMemory)
|
||||
manifestBootstrapTried_.clear();
|
||||
|
||||
picked = pickManifestCandidates(
|
||||
reported, manifestBootstrapTried_, manifestBootstrapWidth);
|
||||
if (picked.empty())
|
||||
return;
|
||||
|
||||
manifestBootstrapTried_.insert(picked.begin(), picked.end());
|
||||
|
||||
JLOG(m_journal.info())
|
||||
<< "Manifests held for " << resolved << " of " << keys.size()
|
||||
<< " validators, " << needed << " needed: reading manifests from "
|
||||
<< picked.size() << " ledger(s) reported by " << reported.size()
|
||||
<< " peer(s)";
|
||||
}
|
||||
|
||||
// Each is acquired as consensus would acquire it. Before a ledger has
|
||||
// been validated, InboundLedger reads the manifests first, by key and from
|
||||
// the directory, and only then fetches the rest. The first pick is
|
||||
// usually the ledger checkLastClosedLedger() is already acquiring. Ledgers
|
||||
// of the same network share nearly all their nodes, so each further one
|
||||
// costs little more than what changed; one that only a peer has fails and
|
||||
// costs nothing more.
|
||||
//
|
||||
// Runs on the heartbeat, ahead of re-arming it, so nothing here may throw.
|
||||
for (auto const& hash : picked)
|
||||
{
|
||||
JLOG(m_journal.debug()) << "Reading manifests from ledger " << hash;
|
||||
try
|
||||
{
|
||||
app_.getInboundLedgers().acquire(
|
||||
hash, 0, InboundLedger::Reason::CONSENSUS);
|
||||
}
|
||||
catch (std::exception const& e)
|
||||
{
|
||||
JLOG(m_journal.warn()) << "Could not start reading manifests from "
|
||||
<< "ledger " << hash << ": " << e.what();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
NetworkOPsImp::processTransaction(
|
||||
std::shared_ptr<Transaction>& transaction,
|
||||
@@ -1610,6 +1725,12 @@ NetworkOPsImp::apply(std::unique_lock<std::mutex>& batchLock)
|
||||
tx.set_deferred(e.result == terQUEUED);
|
||||
// FIXME: This should be when we received it
|
||||
app_.overlay().relay(e.transaction->getID(), tx, *toSkip);
|
||||
|
||||
// Also fan it out over the UDP Superhighway, if enabled.
|
||||
// shouldRelay() above ensures we do this at most once.
|
||||
app_.overlay().publishTxXUSH(
|
||||
s.slice(), e.transaction->getID());
|
||||
|
||||
e.transaction->setBroadcast();
|
||||
}
|
||||
}
|
||||
@@ -2081,13 +2202,13 @@ NetworkOPsImp::beginConsensus(
|
||||
app_.validators().setNegativeUNL(prevLedger->negativeUNL());
|
||||
// Pull in any manifests published on-ledger before the trusted set is
|
||||
// recomputed, so a validator that rotated its ephemeral key on-chain is
|
||||
// resolved to the new signing key in this same round. The master keys come
|
||||
// from the published lists, so this needs no bootstrap: only the ephemeral
|
||||
// half of the mapping ever comes from a manifest.
|
||||
// resolved to the new signing key in this same round. Every master key the
|
||||
// cache holds or pins is probed, not only the trusted ones: a revocation is
|
||||
// published on-ledger by master key alone, and a master key whose ephemeral
|
||||
// key the cache already resolves would otherwise never be looked up again.
|
||||
if (prevLedger->rules().enabled(featureOnChainManifests))
|
||||
{
|
||||
app_.validatorManifests().applyLedger(
|
||||
*prevLedger, app_.validators().getTrustedMasterKeys());
|
||||
app_.validatorManifests().applyLedger(*prevLedger);
|
||||
|
||||
// The reverse of applyLedger above. Manifests reach us by peer gossip
|
||||
// and in published validator lists, both of which can arrive before
|
||||
@@ -2103,8 +2224,8 @@ NetworkOPsImp::beginConsensus(
|
||||
app_.overlay(),
|
||||
app_.getHashRouter());
|
||||
|
||||
// Pin the trusted master keys so they are always offered to a new peer and
|
||||
// cannot be crowded out of the gossip set by more recently used manifests.
|
||||
// Pin the trusted master keys: their manifests are retained in the list
|
||||
// tier and offered to every new peer, whichever source delivered them.
|
||||
app_.validatorManifests().pin(app_.validators().getTrustedMasterKeys());
|
||||
|
||||
if (!changes.added.empty() || !changes.removed.empty())
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
//------------------------------------------------------------------------------
|
||||
/*
|
||||
This file is part of rippled: https://github.com/ripple/rippled
|
||||
Copyright (c) 2026 XRPL-Labs
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
//==============================================================================
|
||||
|
||||
#ifndef RIPPLE_APP_MISC_ROTATINGBLOOMFILTER_H_INCLUDED
|
||||
#define RIPPLE_APP_MISC_ROTATINGBLOOMFILTER_H_INCLUDED
|
||||
|
||||
#include <xrpl/basics/hardened_hash.h>
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
/** Approximate membership of everything inserted recently, in fixed space.
|
||||
|
||||
Time is cut into epochs of a fixed interval. Each of the Generations
|
||||
filters holds the insertions of one epoch: an insertion goes to the filter
|
||||
for the current epoch, and a query consults every filter whose epoch is
|
||||
one of the last Generations. An element is therefore reported for at least
|
||||
(Generations - 1) and at most Generations intervals after it was last
|
||||
inserted, and then forgotten.
|
||||
|
||||
A filter is cleared lazily, when its slot is first wanted for a newer
|
||||
epoch. Nothing has to tick: an idle filter costs nothing, and a clock that
|
||||
jumps forward simply finds every slot expired.
|
||||
|
||||
Within the window there are no false negatives. False positives occur at
|
||||
the usual bloom rate for one epoch's insertions, once per live generation.
|
||||
The footprint is Generations * Bits / 8 bytes however much is inserted:
|
||||
saturating the filter raises the false positive rate and never grows it.
|
||||
|
||||
Bit positions come from a hash seeded randomly per instance, so nobody
|
||||
without the seed can choose elements that set a particular set of bits.
|
||||
|
||||
@par Thread Safety
|
||||
Safe to call concurrently. The internal mutex is never held while calling
|
||||
out, so this is a leaf in any lock order.
|
||||
*/
|
||||
template <std::size_t Bits, std::size_t Hashes, std::size_t Generations>
|
||||
class RotatingBloomFilter
|
||||
{
|
||||
static_assert(
|
||||
Bits >= 64 && (Bits & (Bits - 1)) == 0,
|
||||
"Bits must be a power of two, at least 64");
|
||||
static_assert(
|
||||
Bits <= (std::size_t{1} << 32),
|
||||
"bit positions are computed in 32 bits");
|
||||
static_assert(Hashes >= 1 && Hashes <= 16);
|
||||
static_assert(
|
||||
Generations >= 2,
|
||||
"a single generation would forget everything at each rotation");
|
||||
|
||||
public:
|
||||
using clock_type = std::chrono::steady_clock;
|
||||
using time_point = clock_type::time_point;
|
||||
using duration = clock_type::duration;
|
||||
|
||||
/** Size of the filter state, in bytes. */
|
||||
static constexpr std::size_t bytes = Generations * Bits / 8;
|
||||
|
||||
explicit RotatingBloomFilter(duration interval)
|
||||
: interval_(interval > duration::zero() ? interval : duration{1})
|
||||
{
|
||||
epochs_.fill(none);
|
||||
}
|
||||
|
||||
RotatingBloomFilter(RotatingBloomFilter const&) = delete;
|
||||
RotatingBloomFilter&
|
||||
operator=(RotatingBloomFilter const&) = delete;
|
||||
|
||||
/** Length of one epoch. */
|
||||
duration
|
||||
interval() const
|
||||
{
|
||||
return interval_;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void
|
||||
insert(T const& item, time_point now)
|
||||
{
|
||||
auto const positions = probe(item);
|
||||
auto const epoch = epochOf(now);
|
||||
auto const slot = slotOf(epoch);
|
||||
|
||||
std::lock_guard lock{mutex_};
|
||||
|
||||
// The slot last held an earlier epoch, now out of the window or about
|
||||
// to be: it is reused for this one. A slot already ahead of us (the
|
||||
// caller read the clock before another thread rotated) keeps its bits,
|
||||
// and ours are added to them: that can only remember the element a
|
||||
// little longer than asked, never forget it early.
|
||||
if (epochs_[slot] < epoch)
|
||||
{
|
||||
bits_[slot].fill(0);
|
||||
epochs_[slot] = epoch;
|
||||
}
|
||||
|
||||
for (auto const p : positions)
|
||||
bits_[slot][p / 64] |= std::uint64_t{1} << (p % 64);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
bool
|
||||
contains(T const& item, time_point now) const
|
||||
{
|
||||
auto const positions = probe(item);
|
||||
auto const oldest =
|
||||
epochOf(now) - static_cast<std::int64_t>(Generations);
|
||||
|
||||
std::lock_guard lock{mutex_};
|
||||
|
||||
for (std::size_t g = 0; g < Generations; ++g)
|
||||
{
|
||||
if (epochs_[g] == none || epochs_[g] <= oldest)
|
||||
continue;
|
||||
|
||||
bool all = true;
|
||||
for (auto const p : positions)
|
||||
{
|
||||
if ((bits_[g][p / 64] & (std::uint64_t{1} << (p % 64))) == 0)
|
||||
{
|
||||
all = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (all)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::size_t words = Bits / 64;
|
||||
static constexpr std::int64_t none =
|
||||
std::numeric_limits<std::int64_t>::min();
|
||||
|
||||
std::int64_t
|
||||
epochOf(time_point now) const
|
||||
{
|
||||
// Floor division, so a clock reading before its own epoch still maps
|
||||
// to consecutive epochs.
|
||||
auto const t = now.time_since_epoch().count();
|
||||
auto const i = interval_.count();
|
||||
return t >= 0 ? t / i : -((-t + i - 1) / i);
|
||||
}
|
||||
|
||||
static std::size_t
|
||||
slotOf(std::int64_t epoch)
|
||||
{
|
||||
auto const g = static_cast<std::int64_t>(Generations);
|
||||
return static_cast<std::size_t>(((epoch % g) + g) % g);
|
||||
}
|
||||
|
||||
/** Bit positions for an element: Kirsch-Mitzenmacher double hashing.
|
||||
|
||||
Hashes positions from a single 64 bit hash, as h1 + i * h2. Forcing h2
|
||||
odd makes it invertible modulo the power of two table size, so the
|
||||
positions for one element are all distinct.
|
||||
*/
|
||||
template <class T>
|
||||
std::array<std::uint32_t, Hashes>
|
||||
probe(T const& item) const
|
||||
{
|
||||
std::uint64_t const h = hash_(item);
|
||||
auto const h1 = static_cast<std::uint32_t>(h);
|
||||
auto const h2 = static_cast<std::uint32_t>(h >> 32) | 1u;
|
||||
constexpr auto mask = static_cast<std::uint32_t>(Bits - 1);
|
||||
|
||||
std::array<std::uint32_t, Hashes> positions;
|
||||
for (std::size_t i = 0; i < Hashes; ++i)
|
||||
positions[i] = (h1 + static_cast<std::uint32_t>(i) * h2) & mask;
|
||||
return positions;
|
||||
}
|
||||
|
||||
hardened_hash<> const hash_;
|
||||
duration const interval_;
|
||||
std::mutex mutable mutex_;
|
||||
std::array<std::int64_t, Generations> epochs_;
|
||||
std::array<std::array<std::uint64_t, words>, Generations> bits_{};
|
||||
};
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
#endif
|
||||
Loaded 100 of 175 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user