Compare commits

...
Author SHA1 Message Date
Nicholas Dudfield b7ac456d29 test(export): follow new-account sequencing in XRPL ticket fixtures 2026-09-26 14:50:21 +07:00
Nicholas Dudfield 29dc65e5ba chore: merge origin/dev into feature-export-rng 2026-09-26 14:35:11 +07:00
Nicholas Dudfield 13751d6199 chore: merge feature-export-rng-fixes (#820) 2026-09-26 14:23:25 +07:00
Nicholas Dudfield 4d5ca17f72 fix(rng): synchronize shipped Hook SDK and check runtime ABI 2026-09-26 11:49:26 +07:00
Nicholas Dudfield 9e4d5904fb feat(rng): add explicit same-account strong veto allowance 2026-09-26 11:37:45 +07:00
Nicholas Dudfield 6c21f9d5f0 test(rng): pin account-owner Hook ordering and veto policy 2026-09-26 10:01:38 +07:00
Nicholas Dudfield 106ca672c9 refactor(rng): let entropy pseudo initialize ledger execution context 2026-09-26 09:44:14 +07:00
Nicholas Dudfield 56a7822107 feat(rng): make later strong vetoes an explicit draw policy 2026-09-26 08:18:59 +07:00
Nicholas Dudfield ea36944b5c docs(rng): clarify pre-activation API remains under design 2026-09-26 07:49:48 +07:00
Nicholas Dudfield 3fee098758 fix(rng): reject draws before later strong Hook execution 2026-09-26 07:31:07 +07:00
Nicholas Dudfield 113a12c4e3 docs(hooks): mark deferred review item 2026-09-26 07:31:03 +07:00
Nicholas Dudfield 2635c260a4 fix(hooks): restrict raw consensus entropy slot access 2026-09-25 17:00:47 +07:00
Nicholas Dudfield 4c325570b8 test(consensus): check that busy publication stores under the compute lock 2026-09-25 10:37:54 +07:00
Nicholas Dudfield b92ef011bf refactor(consensus): compute and store the busy flag through one private template
publishBusy() delegates to publishBusyAfter(), which keeps the same busyMu_ critical section, predicate and relaxed store, and runs a caller-supplied step between compute and store. Production passes an empty step; tests use it to hold a computed value before its store.
2026-09-25 10:37:49 +07:00
Nicholas Dudfield 4d4c5aa43a test(consensus): check round fields and export eligibility survive the accept build 2026-09-25 10:27:27 +07:00
Nicholas Dudfield 6211fcb039 test(consensus): check that share admission alone raises the busy flag 2026-09-25 10:04:00 +07:00
Nicholas Dudfield 9e23d7520e test(consensus): cover busy-flag publication and manifest-scoped share suppression
Drive the real extension manager through share admission racing a tick-side phase change under the busy lock, the accept-time build, round reset and latch expiry, checking the published flag against the recomputed predicate at each step. Drive the real TMExportShares receive path on an inbound PeerImp: an unattributable signer is received once, an unrelated manifest keeps identical bytes suppressed, the signer's manifest lets the same bytes through once at an unchanged validated ledger, and later duplicates stay suppressed; receipt, collector admission, relay and stream publication are asserted separately.
2026-09-25 10:02:40 +07:00
Richard Holland a91b7136b7 Merge branch 'dev' into feature-export-rng 2026-09-25 12:17:56 +10:00
Nicholas Dudfield 440b488210 Revert "[DROP] ci: skip the toolchain ppa while launchpad is unreachable from the runners"
This reverts commit bacd1fac81.
2026-09-24 19:34:27 +07:00
Nicholas Dudfield 25a31b4a20 fix(export): retry shares when manifest attribution changes
Scope receive suppression to the share wire hash, validated sequence and resolved master identity. A valid share received before its manifest can then be retried at the same validated ledger without reopening work for unrelated manifest churn. Log the attribution used for suppression.

Reproduced with a real token-based validator restart and delayed manifest processing: four later copies were suppressed, leaving two of three contributions. The fix admits exactly one copy. Verified RNG on/off and manifests-first/late DSF cases with three replays each; 11 suites, 202 cases, zero failures. DSF fixtures remain in the integration worktree for separate landing.
2026-09-24 15:24:40 +07:00
Nicholas Dudfield bacd1fac81 [DROP] ci: skip the toolchain ppa while launchpad is unreachable from the runners 2026-09-24 06:18:42 +07:00
Nicholas Dudfield a686dbd3a0 Merge remote-tracking branch 'origin/feature-export-rng' into feature-export-rng-fixes 2026-09-24 05:53:15 +07:00
Nicholas Dudfield edf24309ad Revert "fix(test): expect config-forced amendments only where the Env forces them"
This reverts commit 4c097b590a.
2026-09-24 05:42:03 +07:00
Nicholas Dudfield 355c232951 refactor(consensus): publish establish state unconditionally from the tick 2026-09-24 05:41:58 +07:00
Nicholas Dudfield 23146b264d fix(test): expect server_definitions enabled for the requested feature set 2026-09-24 05:41:49 +07:00
Nicholas Dudfield 63ed0e4aa2 Revert "ci: register the detected apple-clang version with conan on macOS"
This reverts commit 5d128dc1b4.
2026-09-24 05:22:38 +07:00
Nicholas Dudfield 5d128dc1b4 ci: register the detected apple-clang version with conan on macOS
Conan's bundled settings.yml does not yet list apple-clang 21, so the
macOS runner failed at profile validation on every branch.
2026-09-23 21:52:16 +07:00
Nicholas Dudfield 4c097b590a fix(test): expect config-forced amendments only where the Env forces them
The dev merge brought #765's server_definitions test, whose default-Env
case assumed every supported amendment is config-forced. The jtx fixture
on this lineage deliberately leaves ConsensusEntropy out of the default
set, so enabled is now expected exactly for the Env's configured
features, with ledger_enabled still false. Also formats the include the
merge brought into ServerDefinitions.cpp.
2026-09-23 21:52:12 +07:00
Nicholas Dudfield 8d4f0ae661 Merge remote-tracking branch 'origin/feature-export-rng' into feature-export-rng-fixes 2026-09-23 21:39:35 +07:00
Nicholas Dudfield 99a2f03137 fix(consensus): call the export gate publisher unconditionally
The backport guarded the call with if constexpr (requires ...) instead of
porting the publisher onto the CSF peer and the test's fake extension, so
a missing method would silently fall back to the raw assignment. Both
stubs now define publishExportSigGateStarted() and the tick calls it.
2026-09-23 20:52:18 +07:00
Nicholas Dudfield 1b105b6a99 fix(consensus): run the accepted-set sanitizer outside the consensus mutex
Port the DSF catch-up review narrowing: makeLiveBuildTxSet is pure over its immutable argument. Keep the state-dependent ordering-salt and materialization critical sections unchanged; no harness seams are introduced.
2026-09-23 17:26:39 +07:00
Richard Holland 945eb98f96 Merge branch 'dev' into feature-export-rng 2026-09-23 15:35:13 +10:00
Nicholas Dudfield c6d15aac93 fix(consensus): publish the heartbeat busy flag off the consensus lock
The accept job no longer clears extension round state at the end of
onPreBuild. The heartbeat reads a published atomic, updated where a
contributing input changes, under a mutex that covers those fields.

ported from 285fd6df90
2026-09-23 10:19:26 +07:00
Nicholas Dudfield 04a1d8a7b1 fix(consensus): hold the consensus mutex around accept-time extension prep
doAccept reacquires the consensus mutex for the live-build view, the
ordering salt, and the replay or pre-build block. Ledger building,
open-ledger locks, and endConsensus stay outside it.
2026-09-23 09:45:40 +07:00
Richard HollandandRichardAHBot bc5b90282a export-rng cleanup - remove test infrastructure and STARTDIAG debug logging (#817)
remove python scripts etc from the PR they should live elsewhere.

---------

Co-authored-by: RichardAHBot <[email protected]>
2026-09-23 11:45:32 +10:00
Nicholas Dudfield 64974242d2 docs(consensus): distinguish rechecked alignment from terminal expiry 2026-09-18 12:43:23 +07:00
Nicholas Dudfield e02e9cb1d7 docs(consensus): describe independent extension gate progress 2026-09-18 12:41:40 +07:00
Nicholas Dudfield 6e934f0557 fix(consensus): advance rng and export gates independently 2026-09-18 12:29:33 +07:00
Nicholas Dudfield 47babd73aa test(consensus): cover rng export enablement matrix 2026-09-18 12:23:07 +07:00
Nicholas Dudfield 476a5bf76a test(consensus): require concurrent rng and export gate progress 2026-09-18 12:20:53 +07:00
Nicholas Dudfield c15b69fd9d docs(consensus): describe share transport without incident history 2026-09-18 11:47:40 +07:00
Nicholas Dudfield 540e04bcc9 docs(export): record runnable share dispatch repair 2026-09-18 11:45:17 +07:00
Nicholas Dudfield 12f9200a2a fix(export): dispatch share verification on bounded workers 2026-09-18 11:34:13 +07:00
Nicholas Dudfield 2a2c110e7e test(export): pin runnable share receive posting boundary 2026-09-18 11:29:56 +07:00
Nicholas Dudfield e0201c7365 docs(consensus): clarify proposal roots versus local sidecar maps 2026-09-18 10:05:24 +07:00
Nicholas Dudfield 2002e13e54 docs(consensus): distinguish export gate intent from current execution 2026-09-18 09:59:30 +07:00
Nicholas Dudfield bac9e1df65 chore(export): trace share admission and publication lifecycle 2026-09-18 09:27:21 +07:00
Nicholas Dudfield 9308e5a191 chore(build): refresh app overlay dependency report 2026-09-17 17:15:20 +07:00
Nicholas Dudfield a9488e39f2 fix(hooks): generate xport callback fee documentation 2026-09-17 17:15:16 +07:00
Nicholas Dudfield 4db2f4ea4c fix(build): avoid structured binding captures on clang 14 2026-09-17 17:15:11 +07:00
Nicholas Dudfield 7bf9b931fb fix(consensus): expire export coordination instead of republishing past deadline
A tick after the sidecar window must not start another observation interval
when the root changed or first material arrived late. Expiry is terminal
for the round; an already-published unchanged root may still align on a
late first decision. Live admission unchanged.
2026-09-17 15:38:57 +07:00
Nicholas Dudfield 1e8097d175 test(consensus): cover export deadline bypass during root publication 2026-09-17 15:32:12 +07:00
Nicholas Dudfield 95bfc258e9 fix(consensus): pin export candidacy to the round parent 2026-09-17 15:26:55 +07:00
Nicholas Dudfield 786e317a62 test(consensus): reproduce export gate cursor changes on a keyless observer 2026-09-17 15:19:30 +07:00
Nicholas Dudfield d0ec00473c fix(consensus): rebuild accepted export witnesses against the round parent 2026-09-17 14:16:47 +07:00
Nicholas Dudfield 459196778b test(consensus): correct cursor fixture buffer and admission calls 2026-09-17 14:12:02 +07:00
Nicholas Dudfield 8a7e3896c3 test(consensus): reproduce witness rebuild dependence on validation cursor 2026-09-17 14:11:21 +07:00
Nicholas Dudfield 9ead7aadeb chore(consensus): trace export witness omission decisions 2026-09-17 13:48:06 +07:00
Nicholas Dudfield b1730b8381 fix(import): preserve proof checks in simulation and cluster ingress 2026-09-17 09:42:43 +07:00
Nicholas Dudfield f980837abc fix(import): authorize callback proof delivery without carrier signatures 2026-09-17 09:33:21 +07:00
Nicholas Dudfield 474be6384d feat(export): expose exact callback fees through xport 2026-09-17 07:34:51 +07:00
Nicholas Dudfield dec1da2ec8 test(export): cover native callback fee limits in amount audit 2026-09-16 20:58:15 +07:00
Nicholas Dudfield 63eafad824 fix(import): ignore absent fields in callback relay checks 2026-09-16 20:50:34 +07:00
Nicholas Dudfield 61b739a5c7 fix(import): constrain callback allowance to proof delivery 2026-09-16 20:34:57 +07:00
Nicholas Dudfield fa831d40f7 feat(export): add capped third-party callback fee allowance 2026-09-16 20:29:50 +07:00
Nicholas Dudfield d1450a774b test(import): account for callback multisigning fees 2026-09-16 20:03:31 +07:00
Nicholas Dudfield 9ed50bb71c fix(import): enforce account authorization for Export callbacks 2026-09-16 20:01:35 +07:00
Nicholas Dudfield 65b39982f0 test(export): refresh witness vector after field allocation merge 2026-09-16 17:30:52 +07:00
Nicholas Dudfield e8236cad86 fix(consensus): bind NegativeUNL active-view cap to CE and Export 2026-09-16 17:15:19 +07:00
Nicholas Dudfield 56cb021a60 fix: repair protocol definitions after dev merge 2026-09-16 16:51:27 +07:00
Richard Holland 6be897804b Merge branch 'dev' into feature-export-rng 2026-09-11 12:43:12 +10:00
Nicholas Dudfield 04c24c3edf test(overlay): initialize proposal gate fixture 2026-08-27 18:33:28 +07:00
Nicholas Dudfield 3ef17dac6c style(overlay): satisfy full-tree clang-format 2026-08-27 18:18:53 +07:00
Nicholas Dudfield e84206020f style(overlay): apply clang-format to protocol gates 2026-08-27 17:57:48 +07:00
Nicholas Dudfield fa7d1ae56c test(overlay): pin ExportShares relay cutoff; rename extension-message heading
Add a capable/legacy OverlayImpl TMExportShares relay assertion so the
new cutoff cannot be deleted while handshake tests stay green. Rename
the stale CE-only disconnect heading.
2026-08-27 17:50:31 +07:00
Nicholas Dudfield 29fff4eeb7 feat(overlay): negotiate and gate xahau-export-shares
Advertise xahau-export-shares on XRPL/2.2, AND-negotiate it, require it
when featureExport is enabled, and skip TMExportShares (and already-gated
TMProposeSet) to leftover peers. Same cutoff as Consensus Entropy.
2026-08-27 17:13:08 +07:00
Nicholas Dudfield 4601bf6aec test(testnet): pin protocol gate upgrade binary 2026-08-27 16:50:36 +07:00
Nicholas Dudfield df0a779fed fix(overlay): close active feature cutoff races 2026-08-27 16:48:12 +07:00
Nicholas Dudfield 12a18040db feat(overlay): gate peers on active protocol features 2026-08-27 16:40:34 +07:00
Nicholas Dudfield 38aab0700e test(testnet): pin legacy probe to entropy ledger 2026-08-27 15:57:25 +07:00
Nicholas Dudfield f74e6833fd test(testnet): probe blocked legacy RPC boundary 2026-08-27 15:39:28 +07:00
Nicholas Dudfield 0d3c1d7967 feat(overlay): negotiate consensus entropy capability 2026-08-27 14:49:54 +07:00
Nicholas Dudfield eed61594c3 refactor(proof): remove unused TrieNode helpers
TrieNode::hasChildren and TrieNode::childCount had no call sites
(clangd references and a workspace word search both found only the
definitions). Drop the dead methods; keep the node fields.
2026-08-20 07:33:42 +07:00
Nicholas Dudfield ee058c5d63 style(consensus): apply clang-format cleanup 2026-08-03 12:21:50 +07:00
Nicholas Dudfield 2fe5ca261e refactor(consensus): remove unused extension surfaces 2026-08-03 12:19:01 +07:00
Nicholas Dudfield 2badcb15da test(entropy): harden API scenario and formatting 2026-07-27 11:49:49 +07:00
Nicholas Dudfield d5a3b466d4 test(entropy): reject retired hook import names 2026-07-27 11:40:12 +07:00
Nicholas Dudfield f8c3d5bc0d test(entropy): exercise namespaced hook APIs on testnet 2026-07-27 11:37:34 +07:00
Nicholas Dudfield 7dad88b44d feat(entropy): namespace commit-reveal hook imports 2026-07-27 11:29:59 +07:00
Nicholas Dudfield 1641485e8d fix(export): contain witness materialization failures 2026-07-17 10:35:40 +07:00
Nicholas Dudfield fb918c3a7d fix(export): enforce release target cap in witnesses 2026-07-17 10:03:19 +07:00
Nicholas Dudfield dcad746261 feat(export): price signing work and bound witnesses 2026-07-17 09:54:36 +07:00
Nicholas Dudfield ae8a413acd docs: resolve final coherence review nits 2026-07-17 07:45:18 +07:00
Nicholas Dudfield d125ac958a test(csf): model export view as anchored 2026-07-16 23:14:27 +07:00
Nicholas Dudfield 4e313098a6 fix(export): require anchored view for finalization 2026-07-16 23:09:16 +07:00
Nicholas Dudfield 9dcaebd8e5 style: format export callback assertion 2026-07-16 20:59:50 +07:00
Nicholas Dudfield 1670645482 style: apply clang-format 18 2026-07-16 20:41:42 +07:00
Nicholas Dudfield fd144943b2 docs(entropy): state attribution gate ordering 2026-07-16 20:30:31 +07:00
Nicholas Dudfield 7241f6195d docs(export): name callback transition ownership 2026-07-16 20:28:13 +07:00
Nicholas Dudfield b88d769535 docs(entropy): distinguish root authority from selection 2026-07-16 20:25:00 +07:00
Nicholas Dudfield a03aa1145d docs(consensus): close late intent review gaps 2026-07-16 20:21:13 +07:00
Nicholas Dudfield 0a6f9e7983 test(entropy): avoid stale status failure alias 2026-07-16 20:09:20 +07:00
Nicholas Dudfield e70dc740b7 test(hooks): cover repeated Export emit bypass 2026-07-16 20:04:30 +07:00
Nicholas Dudfield 263fbe222a test(entropy): pin Hook stream and stale status 2026-07-16 20:02:18 +07:00
Nicholas Dudfield 286c836872 test(hooks): allow Export in gate fixture 2026-07-16 20:01:21 +07:00
Nicholas Dudfield 2c93c96245 test(hooks): exercise Export emit gate directly 2026-07-16 19:53:08 +07:00
Nicholas Dudfield f69520a426 test(entropy): harden rotation and digest coverage 2026-07-16 19:48:16 +07:00
Nicholas Dudfield c94d3f3473 docs(entropy): correct replay and deadline wording 2026-07-16 19:42:42 +07:00
Nicholas Dudfield fa763268e2 fix(entropy): canonicalize dice word order 2026-07-16 19:41:55 +07:00
Nicholas Dudfield 8ef38062af test(entropy): make digest order explicit 2026-07-16 19:35:01 +07:00
Nicholas Dudfield 516fda4536 fix(hooks): require xport for export emission 2026-07-16 19:33:04 +07:00
Nicholas Dudfield 8f74614857 test(export): pin observer peer quorum 2026-07-16 19:30:32 +07:00
Nicholas Dudfield a6bcde3044 test(ledger): cover consensus entropy replay 2026-07-16 19:29:43 +07:00
Nicholas Dudfield 1be657098f test(entropy): pin digest and manifest rotation 2026-07-16 19:28:59 +07:00
Nicholas Dudfield fd6c6e87da docs(export): correct observer and XPOP ordering 2026-07-16 19:21:48 +07:00
Nicholas Dudfield a0875f518b docs(entropy): pin hook ABI and bounded timing 2026-07-16 19:21:13 +07:00
Nicholas Dudfield 3a4f86c5e8 docs(entropy): pin commit reveal proof chain 2026-07-16 19:19:03 +07:00
Nicholas Dudfield 0560c88e1a docs(entropy): pin selector and tier contract 2026-07-16 19:17:23 +07:00
Nicholas Dudfield 9f1614a234 docs(entropy): pin synthetic authority and ordering 2026-07-16 19:15:11 +07:00
Nicholas Dudfield 5e27d411d1 docs(export): pin admission and attribution rules 2026-07-16 19:13:13 +07:00
Nicholas Dudfield 1ff3ad46c3 docs(export): pin consensus activation boundary 2026-07-16 19:12:20 +07:00
Nicholas Dudfield 8180336145 docs(export): separate admission and publication clocks 2026-07-16 19:11:21 +07:00
Nicholas Dudfield d52c6bac6f docs(export): pin witness authority boundary 2026-07-16 19:10:35 +07:00
Nicholas Dudfield 53500b1c8e docs(export): pin callback authorization contract 2026-07-16 19:08:02 +07:00
Nicholas Dudfield b0e7affa73 fix(consensus): authenticate cluster proposals 2026-07-16 18:59:05 +07:00
Nicholas Dudfield f40041d018 test(consensus): exercise manifest arrival ordering 2026-07-16 18:23:50 +07:00
Nicholas Dudfield 4eee7c46cb fix(ledger): preserve parent context during live apply 2026-07-16 18:20:43 +07:00
Nicholas Dudfield e5d62430fb fix(consensus): freeze proofed entropy key attribution 2026-07-16 18:18:53 +07:00
Nicholas Dudfield 7917d437a3 fix(export): validate witness evidence against parent 2026-07-16 18:18:49 +07:00
Nicholas Dudfield 2577c1a017 fix(consensus): prevent minority sidecar vetoes 2026-07-16 18:02:15 +07:00
Nicholas Dudfield 99a27974b7 docs(consensus): align normative extension contracts 2026-07-16 17:51:25 +07:00
Nicholas Dudfield f0e36c0ba4 refactor(export): name admission lifecycle precisely 2026-07-16 17:36:03 +07:00
Nicholas Dudfield 4b229bda82 docs(export): correct share wire contract 2026-07-16 17:34:37 +07:00
Nicholas Dudfield 3e9da7623c fix(entropy): tighten pseudo application invariants 2026-07-16 17:31:01 +07:00
Nicholas Dudfield 4acc0dedd8 test(consensus): pin sanitized pseudo derivation 2026-07-16 17:12:15 +07:00
Nicholas Dudfield 2a671fc3a7 fix(consensus): sanitize live extension build inputs 2026-07-16 17:05:08 +07:00
Nicholas Dudfield 6706d642f8 test(export): update contributor field golden bytes 2026-07-16 16:55:55 +07:00
Nicholas Dudfield 28cea3f21b refactor(export): align pending witness terminology 2026-07-16 16:50:11 +07:00
Nicholas Dudfield 59697f5ae7 refactor(export): remove retired witness threshold 2026-07-16 16:46:34 +07:00
Nicholas Dudfield e7330acbbe fix(consensus): separate live pseudo authority from replay 2026-07-16 16:44:37 +07:00
Nicholas Dudfield 6b97686dde refactor(export): remove redundant trigger publication state 2026-07-16 16:37:49 +07:00
Nicholas Dudfield f5d9824e4a refactor(export): use dedicated contributor field 2026-07-16 16:34:53 +07:00
Nicholas Dudfield fdbba707e9 docs(export): state witness attribution boundary 2026-07-16 16:29:51 +07:00
Nicholas Dudfield a31140a4b6 refactor(export): remove unused stream key constant 2026-07-16 16:17:54 +07:00
Nicholas Dudfield 508a95f85e refactor(json): remove retired shares key constant 2026-07-16 16:17:34 +07:00
Nicholas Dudfield e6b8a55810 refactor(consensus): remove retired entropy flags 2026-07-16 16:16:53 +07:00
Nicholas Dudfield b0da027f7a refactor(export): remove retired authority cap helper 2026-07-16 16:16:17 +07:00
Nicholas Dudfield 94d8ba48af refactor(export): remove retired result metadata 2026-07-16 16:13:56 +07:00
Nicholas Dudfield e18acab7af refactor(export): remove retired expiry result 2026-07-16 16:11:29 +07:00
Nicholas Dudfield 1ea862a6f3 refactor(export): remove retired retry result 2026-07-16 16:10:59 +07:00
Nicholas Dudfield 25a60b33cd fix(export): reject zero target ticket sequence 2026-07-16 15:46:55 +07:00
Nicholas Dudfield 4f9d58eb46 refactor(export): remove legacy collector lane 2026-07-16 15:44:21 +07:00
Nicholas Dudfield c021365516 chore: refresh generated and formatted files 2026-07-16 15:22:27 +07:00
Nicholas Dudfield 8806ee94d0 test: pin packed entropy status ABI 2026-07-16 14:52:46 +07:00
Nicholas Dudfield ec5d22f557 feat: pack entropy status metadata 2026-07-16 14:39:58 +07:00
Nicholas Dudfield bf6cf9c84a feat: simplify entropy Hook quality gates 2026-07-16 14:11:51 +07:00
Nicholas Dudfield 142fde5acd test: pin Export committee replay guard 2026-07-16 13:48:02 +07:00
Nicholas Dudfield 66214c8ab3 test: isolate Export committee immutability guard 2026-07-16 13:36:58 +07:00
Nicholas Dudfield 2fdce2a7c4 test: cover Export committee invariant branches 2026-07-16 13:31:57 +07:00
Nicholas Dudfield 80ca259233 export: close committee review gaps 2026-07-16 13:20:42 +07:00
Nicholas Dudfield b20d135388 Implement account-owned Export committees 2026-07-16 12:54:07 +07:00
Nicholas Dudfield 9d44f6fb0c refactor(export): name transaction phases explicitly 2026-07-16 11:02:42 +07:00
Nicholas Dudfield 0eda858e09 test(export): bind stream evidence to witness cursors 2026-07-16 10:28:23 +07:00
Nicholas Dudfield 1c2b726769 test(export): preserve missing-unl negative fixture 2026-07-16 10:20:33 +07:00
Nicholas Dudfield efb4e2e3e4 test(export): cover share stream and signer recovery 2026-07-16 10:16:53 +07:00
Nicholas Dudfield d9cafebff3 test(export): prove no-veto behavior directly 2026-07-16 10:06:17 +07:00
Nicholas Dudfield ffd50fc180 refactor(export): key lifecycle control by origin 2026-07-16 09:41:56 +07:00
Nicholas Dudfield 81b46570b8 refactor(export): cut over to export latches 2026-07-16 08:59:49 +07:00
Nicholas Dudfield fe039ef8fa docs(export): ratify latch cleanup policy 2026-07-16 08:38:46 +07:00
Nicholas Dudfield 389732038d test(export): cover witness attribution replay 2026-07-14 19:58:02 +07:00
Nicholas Dudfield 7971c76943 test(export): cover attributed witness conformance 2026-07-14 19:55:55 +07:00
Nicholas Dudfield 45a7b17ef0 test(export): prove witness insertion order independence 2026-07-14 19:54:54 +07:00
Nicholas Dudfield f02eaeabe0 test(export): accept exact witness recovery 2026-07-14 19:54:40 +07:00
Nicholas Dudfield c9d9b1ea39 test(export): require acquired witness validation 2026-07-14 19:53:52 +07:00
Nicholas Dudfield eca5e299b2 test(export): bind recovery jump to witness branch 2026-07-14 19:46:41 +07:00
Nicholas Dudfield dd07422823 test(export): model divergent witness recovery 2026-07-14 19:41:40 +07:00
Nicholas Dudfield 111f73dae6 test(export): fix deferred share count expectation 2026-07-14 19:41:29 +07:00
Nicholas Dudfield 5cbedf6a70 fix(export): charge foreign manifest masters 2026-07-14 19:18:20 +07:00
Nicholas Dudfield a5bab40247 test(export): cover wrong relay signer charging 2026-07-14 19:04:59 +07:00
Nicholas Dudfield 9f55a61a48 fix(export): close deferred relay races 2026-07-14 19:02:09 +07:00
Nicholas Dudfield da9e6b3cc7 fix(export): preserve witness signer attribution 2026-07-14 19:02:01 +07:00
Nicholas Dudfield 9286edc57f fix(export): defer validation-skewed relay shares 2026-07-14 18:27:56 +07:00
Nicholas Dudfield 18569dd931 test(export): assert replay precedes local authoring 2026-07-14 18:13:29 +07:00
Nicholas Dudfield e7fcd7729a test(consensus): cover export stream replay races 2026-07-14 17:26:13 +07:00
Nicholas Dudfield dc4d230eaa refactor(export): unify share stream events 2026-07-14 17:09:21 +07:00
Nicholas Dudfield 32bf843c2b test(export): validate rebase and shadow evidence 2026-07-14 17:04:26 +07:00
Nicholas Dudfield 1a8680efe4 fix(export): retain canceled enhanced latches 2026-07-14 17:00:18 +07:00
Nicholas Dudfield 0fd2496e8c docs(export): define global pending-work cap 2026-07-14 16:52:59 +07:00
Nicholas Dudfield 82214b3117 test(export): harden direct witness scenarios 2026-07-14 16:52:53 +07:00
Nicholas Dudfield f25fd80d72 fix(export): release witnessed pending slots 2026-07-14 16:42:19 +07:00
Nicholas Dudfield 6d6f36ab7a docs(export): describe durable intent lifecycle 2026-07-14 16:41:53 +07:00
Nicholas Dudfield 0c79ac2631 fix(export): enforce candidate publication deadline 2026-07-14 16:40:36 +07:00
Nicholas Dudfield 1c5949075d test(export): follow post-validation witness lifecycle 2026-07-14 16:40:08 +07:00
Nicholas Dudfield 4bffbc0f78 test(export): cover terminal stream lifecycle 2026-07-14 16:30:31 +07:00
Nicholas Dudfield 572365baf5 fix(export): enforce global live latch cap 2026-07-14 16:26:14 +07:00
Nicholas Dudfield 018719fdaa fix(export): separate admission and publication windows 2026-07-14 16:23:27 +07:00
Nicholas Dudfield b6b77e2eb3 refactor(export): remove obsolete signature upgrader 2026-07-14 16:20:04 +07:00
Nicholas Dudfield 9febb4957e fix(export): make share snapshots terminal-safe 2026-07-14 16:17:15 +07:00
Nicholas Dudfield 691cec51bf fix(export): harden post-validation share release 2026-07-14 15:42:12 +07:00
Nicholas Dudfield 130d4bc9fc test(export): cover later-ledger witnesses 2026-07-14 15:20:52 +07:00
Nicholas Dudfield f3c3fdbe8c Add export signature subscription stream 2026-07-14 15:00:55 +07:00
Nicholas Dudfield 210b77dc4a fix(export): make witness replay state-derived 2026-07-14 14:55:58 +07:00
Nicholas Dudfield 69b9866f4b feat(export): release validated witnesses in later ledgers 2026-07-14 14:52:11 +07:00
Nicholas Dudfield dadf6c99b4 feat(export): admit durable post-validation intents 2026-07-14 14:31:02 +07:00
Nicholas Dudfield 43a8be7429 feat(overlay): relay admitted export shares 2026-07-14 14:27:47 +07:00
Nicholas Dudfield 4c2860a9e1 fix(export): harden relay and collector identity 2026-07-14 14:17:48 +07:00
Nicholas Dudfield e6facf0d7c feat(export): define canonical share relay frame 2026-07-14 14:09:53 +07:00
Nicholas Dudfield 10bbfc790b fix(export): harden collector admission lifecycle 2026-07-14 14:09:43 +07:00
Nicholas Dudfield 79a07106f8 feat(export): add post-validation signature collector 2026-07-14 14:02:04 +07:00
Nicholas Dudfield b827a4752f fix(export): make latch mutations format-safe 2026-07-14 13:56:52 +07:00
Nicholas Dudfield 4c3e313e3a feat(export): pin committee quorum profile 2026-07-14 13:50:05 +07:00
Nicholas Dudfield 47b65260cb feat(export): add durable latch directory helpers 2026-07-14 13:50:00 +07:00
Nicholas Dudfield 060c015cc5 test(export): pin enhanced latch bytes 2026-07-14 13:28:48 +07:00
Nicholas Dudfield eed809fcc0 feat(export): add bounded latch format 2026-07-14 13:18:30 +07:00
Nicholas Dudfield 8bd0d42bf5 test(export): harden origin memo invariants 2026-07-14 12:56:50 +07:00
Nicholas Dudfield 5c01906a4c feat(export): validate canonical release memo capacity 2026-07-14 12:53:04 +07:00
Nicholas Dudfield 2da84ef6f7 test(export): fix release memo byte fixture 2026-07-14 12:46:33 +07:00
Nicholas Dudfield 0df7ba6daf feat(export): add canonical origin memo codec 2026-07-14 12:42:51 +07:00
Nicholas Dudfield aa819730e0 refactor(export): add pending directory keylet 2026-07-14 12:31:23 +07:00
Nicholas Dudfield 632a7fb520 refactor(export): add origin-keyed latch keylet 2026-07-14 12:29:16 +07:00
Nicholas Dudfield 6cfac96cc9 test(export): complete collector compatibility matrix 2026-07-13 19:10:55 +07:00
Nicholas Dudfield 5a8e5d99ac test(export): pin legacy collector replacement semantics 2026-07-13 19:08:36 +07:00
Nicholas Dudfield 37d9eaaa2b fix(protocol): reset moved validator bitsets 2026-07-13 19:05:22 +07:00
Nicholas Dudfield 5657f75d3c test(consensus): cover Export root serialization 2026-07-13 19:03:05 +07:00
Nicholas Dudfield 97385c0f83 test(consensus): pin Export root wire allocation 2026-07-13 19:00:33 +07:00
Nicholas Dudfield c2c1795dbd fix(ledger): harden validated work scheduling 2026-07-13 18:56:57 +07:00
Nicholas Dudfield f8a8d8bfc0 fix(protocol): own validated bitset storage 2026-07-13 18:56:56 +07:00
Nicholas Dudfield 62050233a5 fix(protocol): require validated validator bitsets 2026-07-13 18:44:13 +07:00
Nicholas Dudfield 0536e2e9eb refactor(ledger): queue validated-ledger work 2026-07-13 18:39:44 +07:00
Nicholas Dudfield f3897836f3 refactor(consensus): share validator bitset helpers 2026-07-13 18:37:47 +07:00
Nicholas Dudfield cdbac5e7f8 test(export): regenerate hook fixture with hookz 2026-07-13 16:36:49 +07:00
Nicholas Dudfield 5f8145eadc Revert "Remove Export sidecar consensus authority"
This reverts commit e6237b34ab.
2026-07-13 16:21:55 +07:00
Nicholas Dudfield f6e03d4529 Revert "Restore Export transition test coverage"
This reverts commit 95c86b29fe.
2026-07-13 16:21:50 +07:00
Nicholas Dudfield 95c86b29fe Restore Export transition test coverage 2026-07-13 15:15:35 +07:00
Nicholas Dudfield e6237b34ab Remove Export sidecar consensus authority 2026-07-13 14:48:59 +07:00
Nicholas Dudfield 4a2ca38b7f test: pin Export serialization size inventory 2026-07-13 12:02:18 +07:00
Nicholas Dudfield c5edcdbbcd docs: mark account incarnation allocation evidence 2026-07-13 10:13:14 +07:00
Nicholas Dudfield 2e9a56bf27 docs: mark overlay artifact carrier precedents 2026-07-13 09:58:31 +07:00
Nicholas Dudfield 2a3ceea6b0 docs: mark consensus dispute inclusion evidence 2026-07-13 09:33:14 +07:00
Nicholas Dudfield 9d44673673 docs: mark export pipeline evidence 2026-07-13 06:22:50 +07:00
Nicholas Dudfield fa71bb62c2 Revert "feat(consensus): consolidate UNL report amendment"
This reverts commit 0cdd7164f4.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield a3084b2843 Revert "feat(consensus): expose manifest binding identity"
This reverts commit dae0d64b3e.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield 22d89fb897 Revert "feat(consensus): add UNL report member registry"
This reverts commit ab30826c5c.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield e1926ba777 Revert "feat(consensus): publish UNL report member evidence"
This reverts commit 92c7839822.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield b8fad6acbc Revert "fix(consensus): publish initial member revocations"
This reverts commit 25073d6cb1.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield ae43022a5d Revert "fix(consensus): recycle member evidence cache"
This reverts commit 1287cce0f7.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield de86c4342b Revert "test(consensus): cover member registry replay"
This reverts commit 726dde697e.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield 78fe429f41 Revert "docs(consensus): describe evidence cache recycling"
This reverts commit 387a56819d.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield 203eed213a Revert "feat(consensus): resolve ledger member bindings"
This reverts commit 6e9a62f6f6.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield 9be37877b7 Revert "test(consensus): harden member binding resolver"
This reverts commit 4f3c0affb4.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield ee430c9438 Revert "perf(consensus): filter member evidence before verification"
This reverts commit 2e35da2c1f.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield f13868a56f Revert "fix(consensus): enforce cached manifest binding identity"
This reverts commit 946f521586.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield c4841195e2 Revert "chore(ci): refresh UNL report generated artifacts"
This reverts commit 558016442d.
2026-07-13 05:56:15 +07:00
Nicholas Dudfield 23d5a15111 Revert "fix(test): support clang-14 manifest fixtures"
This reverts commit d874a3725c.
2026-07-13 05:56:14 +07:00
Nicholas Dudfield d874a3725c fix(test): support clang-14 manifest fixtures 2026-07-12 10:49:22 +07:00
Nicholas Dudfield f0dba68abe refactor(consensus): split export quorum roles 2026-07-12 10:42:48 +07:00
Nicholas Dudfield 96c3727986 fix(build): unblock AppleClang warning builds 2026-07-12 10:21:37 +07:00
Nicholas Dudfield 53b29c34f8 [drop] build: confine git metadata rebuilds 2026-07-12 10:21:04 +07:00
Nicholas Dudfield 558016442d chore(ci): refresh UNL report generated artifacts 2026-07-12 10:12:45 +07:00
Nicholas Dudfield 946f521586 fix(consensus): enforce cached manifest binding identity 2026-07-12 10:07:15 +07:00
Nicholas Dudfield 2e35da2c1f perf(consensus): filter member evidence before verification 2026-07-12 10:00:06 +07:00
Nicholas Dudfield 86501e2916 feat(consensus): retain original validator identities 2026-07-12 09:53:18 +07:00
Nicholas Dudfield 4f3c0affb4 test(consensus): harden member binding resolver 2026-07-12 09:53:03 +07:00
Nicholas Dudfield 6e9a62f6f6 feat(consensus): resolve ledger member bindings 2026-07-12 09:48:18 +07:00
Nicholas Dudfield 387a56819d docs(consensus): describe evidence cache recycling 2026-07-12 09:47:59 +07:00
Nicholas Dudfield 726dde697e test(consensus): cover member registry replay 2026-07-12 09:27:34 +07:00
Nicholas Dudfield 1287cce0f7 fix(consensus): recycle member evidence cache 2026-07-12 09:27:16 +07:00
Nicholas Dudfield 25073d6cb1 fix(consensus): publish initial member revocations 2026-07-11 14:59:03 +07:00
Nicholas Dudfield 92c7839822 feat(consensus): publish UNL report member evidence 2026-07-11 14:50:53 +07:00
Nicholas Dudfield ab30826c5c feat(consensus): add UNL report member registry 2026-07-11 14:17:47 +07:00
Nicholas Dudfield dae0d64b3e feat(consensus): expose manifest binding identity 2026-07-11 13:48:28 +07:00
Nicholas Dudfield 0cdd7164f4 feat(consensus): consolidate UNL report amendment 2026-07-11 13:32:11 +07:00
Nicholas Dudfield 84d738ce8a docs(export): define bounded authority and committee path 2026-07-10 17:01:12 +07:00
Nicholas Dudfield 5e2cbf62fe docs(export): simplify submitter co-signer model 2026-07-10 16:24:35 +07:00
Nicholas Dudfield 75d08560d4 docs(export): describe optional executor gate 2026-07-10 16:21:03 +07:00
Nicholas Dudfield b942a29fd8 docs(export): define quorum and cancellation contract 2026-07-10 16:14:29 +07:00
Nicholas Dudfield 3cb0583d0c style(export): match CI clang-format 2026-07-10 16:04:23 +07:00
Nicholas Dudfield 56bf9f2eed fix(export): align callback identity and signer authority 2026-07-10 15:50:39 +07:00
Nicholas Dudfield 3a865b5c0a style: apply clang-format 18 to nUNL cap fix + entropy pseudo ingress 2026-07-07 18:54:23 +07:00
Nicholas Dudfield 4e7996a86a docs(export): clarify nUNL cap proof denominator 2026-07-07 18:15:48 +07:00
Nicholas Dudfield c558478337 docs(consensus): document nUNL cap amendment dependency 2026-07-07 18:13:13 +07:00
Nicholas Dudfield 7f6a29770c fix(consensus): align nUNL vote cap with active view 2026-07-07 18:09:50 +07:00
Nicholas Dudfield 7161937720 test(export): cover audit gaps 2026-07-07 17:42:41 +07:00
Nicholas Dudfield f6e37bcc53 test(consensus): cover export pseudo acquired-set rejection 2026-07-07 17:32:32 +07:00
Nicholas Dudfield 4dd3882b30 fix(resource): strip inbound loopback ports from resource keys 2026-07-07 17:22:24 +07:00
Nicholas Dudfield c7e5f54761 docs(consensus): anchor bounded extension waits 2026-07-07 16:45:05 +07:00
Nicholas Dudfield 990e15a468 docs(consensus): add walkthrough source anchors 2026-07-07 16:37:14 +07:00
Nicholas Dudfield c03a92c5d2 fix(consensus): reject acquired CE pseudo transactions 2026-07-07 15:44:23 +07:00
Nicholas Dudfield 9bd964b7ff test(consensus): pin active NegativeUNL cap invariant 2026-07-07 15:31:50 +07:00
Nicholas Dudfield 96f84a1c15 fix(consensus): cap NegativeUNL shrink against UNLReport active view to preserve entropy intersection 2026-07-07 15:19:16 +07:00
Nicholas Dudfield effead7b0d test(consensus): rolling-upgrade + CE-activation rollout scenario
.testnet/scenarios/rollout/: a 7-node localhost net (5 UNL validators quorum 4 +
2 non-UNL trackers) starts on @release, rolling-restarts the validators + 1
tracker to @export-rng (mesh-gated, quorum preserved), votes ConsensusEntropy,
activates at flag ledger 256, and asserts the upgraded tracker keeps tracking
while the @release straggler is amendment-blocked but not crashed. Requires
distinct loopback aliases (x-testnet setup-aliases).

Also drops the dead XAHAU_RESOURCE_PER_PORT env from the entropy/export/latency
suites (never read by the binary; the localhost mesh now comes from distinct-IP
fixed peers).
2026-07-07 11:52:51 +07:00
Nicholas Dudfield dd678ee6ce docs(consensus): document the CE proposal wire-format rollout dependency
After featureConsensusEntropy activates, proposals may carry a serialized
ExtendedPosition in the legacy currenttxhash protobuf field, so pre-CE binaries
that require an exact 32-byte currenttxhash cannot participate as proposal peers;
disabling sidecar fetch/reconciliation does NOT restore compatibility (the
wire-format change is independent of the sidecar gate). Verified against the
implementation (RCLConsensus, ConsensusExtensions, PeerImp).
2026-07-07 11:52:36 +07:00
Nicholas Dudfield 3eaf7b9e3d feat(consensus): record entropy contributors 2026-07-03 19:21:10 +07:00
Nicholas Dudfield 02313caa00 fix(hooks): remove consensus entropy keylet 2026-07-03 12:27:52 +07:00
Nicholas Dudfield 7c5ddb06d8 feat(hooks): expose consensus entropy keylet 2026-07-03 07:23:07 +07:00
Nicholas Dudfield e8d40472e9 feat(consensus): add validator full entropy tier 2026-07-02 21:08:37 +07:00
Nicholas Dudfield ac659faec7 fix(consensus): refresh extension gates before round cleanup 2026-07-02 19:26:32 +07:00
Nicholas Dudfield 986646ec90 fix(consensus): refresh extension gates on round start 2026-07-02 18:35:29 +07:00
Nicholas Dudfield e3e1d847a4 test(consensus): assert export quorum cohort sync 2026-07-02 18:34:44 +07:00
Nicholas Dudfield fa876927a9 feat: record consensus entropy denominator 2026-07-02 17:38:40 +07:00
Nicholas Dudfield 9a99c2d400 chore: update levelization results 2026-07-02 16:56:02 +07:00
Nicholas Dudfield e001aca8ae fix(consensus): trust accepted export signature roots 2026-07-02 16:09:29 +07:00
Nicholas Dudfield 777743792e docs(consensus): qualify entropy fallback boundary as lab-only 2026-07-02 14:53:21 +07:00
Nicholas Dudfield 3af3b33c07 docs(consensus): clarify entropy boundary after tx-set agreement 2026-07-02 14:39:40 +07:00
Nicholas Dudfield 030206d632 docs(consensus): document entropy fallback boundary 2026-07-02 14:33:43 +07:00
Nicholas Dudfield 9324bd59e5 fix(consensus): keep entropy selection rooted in accepted hash
Revert the local entropyFailed selector override introduced during sidecar reconciliation excision. ConsensusEntropy injection is ledger-defining, so accepted entropy roots remain authoritative; nodes without an accepted root still fall back through the normal missing-root path.

Update CSF parity, the focused onPreBuild regression, and the design intent note to capture that local timeout/diagnostic state must not override an accepted root.
2026-07-02 13:50:42 +07:00
Nicholas Dudfield 3c01901119 feat(consensus): excise sidecar reconciliation
Remove peer fetch/acquire/merge support for CE/export sidecar SHAMaps and keep them as local same-process snapshots only.

Harden generic transaction-set paths so sidecar maps are not advertised, served, acquired, or wrapped as RCLTxSet, and delete the stale fetch-positive .testnet suite.

Make entropy failure terminal at selection time and mirror that invariant in the CSF harness; keep Export witness materialization rooted in accepted exportSigSetHash rather than local timeout state.
2026-07-02 12:52:43 +07:00
Nicholas Dudfield e8fa4e1154 fix(consensus): stop serving sidecars when reconciliation is off 2026-07-01 11:30:42 +07:00
Nicholas Dudfield d4650441fb feat(consensus): default sidecar reconciliation off
Compile out sidecar acquisition and merge plumbing unless xahaud_sidecar_reconciliation is enabled, and move fetch-positive .testnet scenarios into an opt-in suite.

When reconciliation is enabled, gate eager fetches on current trusted root support so first-seen transient roots do not trigger network acquisition. This keeps the recovery path available for measurement without making it the default path.

Verified default OFF with x-format-changed, git diff --check, x-quick-check, cmake configure/build, and ./build/rippled --unittest=ConsensusExtensions. ON path syntax-checked; Claude is assigned the parallel ON build/test review in a separate worktree.
2026-06-30 16:10:50 +07:00
Nicholas Dudfield 67c2c44263 feat(consensus): gate sidecar reconciliation 2026-06-30 14:03:56 +07:00
Nicholas Dudfield ee6c4ff31f test(consensus): measure entropy sidecar fetch yield 2026-06-30 12:07:33 +07:00
Nicholas Dudfield 63f99ad9d8 feat(consensus): salt tx ordering with entropy 2026-06-29 16:32:16 +07:00
Nicholas Dudfield be649d5112 fix(export): bound retry window 2026-06-29 14:28:20 +07:00
Nicholas Dudfield 8c48e500e7 fix(consensus): domain-separate participant diagnostics 2026-06-29 11:49:26 +07:00
Nicholas Dudfield 5195fc64b3 fix(hooks): block callback from canceling current export latch 2026-06-29 10:54:31 +07:00
Nicholas Dudfield f849b70a96 docs(export): clarify target network id guard 2026-06-29 10:18:53 +07:00
Nicholas Dudfield 8ad82f328d fix(export): prevent callback hook from pre-canceling import latch 2026-06-29 10:03:04 +07:00
Nicholas Dudfield 618314afb7 test(consensus): isolate export sidecar rejection cases 2026-06-29 07:55:06 +07:00
Nicholas Dudfield 0668caacf2 test(consensus): fix round-pinned sidecar fixture 2026-06-29 07:26:19 +07:00
Nicholas Dudfield 583c4e2461 test(export): prove sidecar fetch under proposal loss 2026-06-28 20:36:53 +07:00
Nicholas Dudfield 2fe984297a fix(consensus): filter agreed export signers by active view 2026-06-28 20:10:28 +07:00
Nicholas Dudfield d6de1d90b5 fix(consensus): pin RNG round sequence at start 2026-06-28 20:01:08 +07:00
Nicholas Dudfield 3eda8d664e fix(consensus): share entropy sidecar leaf admission 2026-06-28 19:53:42 +07:00
Nicholas Dudfield a0866d1559 fix(import): use ledger time for VL freshness 2026-06-28 19:45:36 +07:00
Nicholas Dudfield 4a12f4c231 fix(hook): harden xport and xpop host paths 2026-06-28 19:41:20 +07:00
Nicholas Dudfield a93b8cfdc4 refactor(export): share inner export transaction parsing 2026-06-28 19:18:40 +07:00
Nicholas Dudfield c2bc4fabe8 fix(consensus): filter export signature candidates 2026-06-28 19:02:32 +07:00
Nicholas Dudfield 94ffc86bd9 fix(consensus): cap export sidecar candidates 2026-06-28 18:18:45 +07:00
Nicholas Dudfield 1d158a910b fix(consensus): cap fetched sidecar cardinality 2026-06-28 17:18:14 +07:00
Nicholas Dudfield 62dc85df86 test(export): cover replay witness pre-scan 2026-06-28 15:49:06 +07:00
Nicholas Dudfield d7a8467705 refactor(consensus): share active export signer filter 2026-06-28 15:44:18 +07:00
Nicholas Dudfield 8cc4574e0f refactor(export): split closed-ledger result assembly 2026-06-28 15:39:37 +07:00
Nicholas Dudfield cf3a9ef96b fix(consensus): freeze rng commits before reveal 2026-06-28 15:15:17 +07:00
Nicholas Dudfield 806aa56aec fix(consensus): centralize rng admission 2026-06-28 14:52:00 +07:00
Nicholas Dudfield cb1f932236 refactor(consensus): share sidecar build helpers 2026-06-28 14:20:55 +07:00
Nicholas Dudfield e01c0cba70 fix(export): require replay parent for historical apply 2026-06-28 13:53:19 +07:00
Nicholas Dudfield 5e27c2fea9 refactor(consensus): centralize sidecar admission checks 2026-06-28 13:43:16 +07:00
Nicholas Dudfield 19ec9ece35 fix(replay): enforce manual replay ledger hash 2026-06-28 13:33:53 +07:00
Nicholas Dudfield a714eaa621 fix(consensus): bind entropy reveals to proofed commits 2026-06-28 13:32:26 +07:00
Nicholas Dudfield f0f998d897 test(export): cover stale ImportVL callback rejection 2026-06-28 12:51:32 +07:00
Nicholas Dudfield 1dbda8f6b3 style(export): satisfy clang-format 2026-06-28 12:34:50 +07:00
Nicholas Dudfield 5149f5ae9b refactor(import): make VL ratchet explicit for callbacks 2026-06-28 12:32:58 +07:00
Nicholas Dudfield 46632be87b refactor(rng): reuse named commit quorum threshold 2026-06-28 12:13:39 +07:00
Nicholas Dudfield 8bd79c4759 test(export): align testnet export metadata assertions 2026-06-28 12:06:33 +07:00
Nicholas Dudfield 6661e7a40a test(export): move reserve setup to failing test 2026-06-28 11:55:29 +07:00
Nicholas Dudfield 46207a3fca test(export): initialize account root before reserve edit 2026-06-28 11:38:48 +07:00
Nicholas Dudfield a16e8886c2 test(testnet): resolve export signature witnesses 2026-06-28 11:24:14 +07:00
Nicholas Dudfield 268db0b6d2 fix(export): enforce shadow ticket reserve 2026-06-28 11:24:06 +07:00
Nicholas Dudfield 73f3b7a167 fix(consensus): reject miskeyed rng sidecar leaves 2026-06-28 11:24:00 +07:00
Nicholas Dudfield 1b4535cee5 docs(consensus): clarify rng proposal key checks 2026-06-28 10:30:17 +07:00
Nicholas Dudfield bb6242213b fix(hooks): enforce xport emit budget 2026-06-28 10:16:53 +07:00
Nicholas Dudfield 0aa8e3ca67 fix(consensus): reject non-secp rng proposal proofs 2026-06-28 10:16:29 +07:00
Nicholas Dudfield 516edb8147 fix(hooks): return export wrapper hash from xport 2026-06-28 10:09:00 +07:00
Nicholas Dudfield 1186a3a7c4 style: apply clang-format 2026-06-27 17:38:11 +07:00
Nicholas Dudfield 9d9329ece4 refactor: reuse proof parser and named entropy tiers 2026-06-27 17:32:37 +07:00
Nicholas Dudfield 465131bb3e refactor: name legacy NetworkID transaction boundary 2026-06-27 16:59:20 +07:00
Nicholas Dudfield 1b0c633eda refactor(consensus): trim small duplicated extension constants
Reuse SidecarPeerAlignment::quorumAligned for export sidecar gates, name the shared sidecar convergence timeout, and centralize the RNG commitment tuple.

Also make ExportLimits::maxExportsPerHook the value enforced by xport_reserve, with a static assertion against the hook ABI constant.
2026-06-27 15:24:06 +07:00
Nicholas Dudfield ceabb893bf docs(consensus): tighten proposal proof wording
Clarify that fetched RNG commit proofs carry the signed proposal fields plus signature, not only the serialized position bytes.
2026-06-27 15:16:35 +07:00
Nicholas Dudfield d5ca581cea docs(consensus): explain sidecar reconciliation model
Document the common proposal-relay path, why RNG/Export use fixed-denominator quorum over proposal-borne material, and how sidecar SHAMap fetch/proofs act as secondary distribution rather than authority.

This captures the reconciliation cost/benefit in the committed design doc, separate from the reviewer-facing PR description.
2026-06-27 15:13:22 +07:00
Nicholas Dudfield 8a81588b71 fix(consensus): let quorum-aligned entropy conflicts proceed
Use the fixed-denominator entropy threshold as the conflict boundary: once our entropy hash is quorum-aligned, a below-threshold conflicting minority or silent peer cannot force fallback by withholding full observation.

Update the consensus entropy invariant spine and design doc so the no-veto conflict rule is explicit, and flip the regression test to cover the liveness-preserving path.
2026-06-27 15:09:30 +07:00
Nicholas Dudfield 0a9023c334 fix(export): avoid live manifest filtering of accepted witnesses
Materialize ttEXPORT_SIGNATURES from the accepted export sidecar root during live consensus builds, then let apply trust that tx-stream witness membership while still verifying each signature and enforcing quorum.

Direct apply paths remain conservative, and historical replay uses the same witness-membership rule after manifests may have rotated. Update the intent docs and tests for live, direct, and replay modes.
2026-06-27 13:39:58 +07:00
Nicholas Dudfield c38567792e fix(hooks): separate callback rng streams 2026-06-27 13:06:28 +07:00
Nicholas Dudfield 3cfc79ea59 fix(consensus): bind rng sidecar proofs to round parent 2026-06-27 13:04:08 +07:00
Nicholas Dudfield eb3a3b888f fix(consensus): allow shared diagnostics in export-only rounds 2026-06-27 12:56:19 +07:00
Nicholas Dudfield c56cb34d6d fix(consensus): total-order entropy selector inputs 2026-06-27 12:45:21 +07:00
Nicholas Dudfield 306566ccb6 fix(hooks): bound export reservations by emit budget 2026-06-27 12:40:16 +07:00
Nicholas Dudfield 8ddbe0891a docs(consensus): capture sidecar validation boundary 2026-06-27 12:32:10 +07:00
Nicholas Dudfield deb1b2af0b fix(consensus): validate extension sidecar inputs
Reject malformed RNG sidecar signing keys before PublicKey construction so fetched untrusted leaves cannot trigger an uncatchable LogicError abort. Also re-check proposal signatures at the extension harvest boundary so cluster-relayed invalid proposals cannot populate RNG or export sidecar state.
2026-06-27 12:25:52 +07:00
Nicholas Dudfield 4dcb3f3557 fix(consensus): remove local entropy fallback shortcuts
Encode fallback-vs-nonfallback as a ledger-defining decision, remove the prevProposers/bootstrap and visible-peer impossible-quorum shortcuts, and use proofed commit material for the timeout transition. Update unit/CSF/testnet coverage and document the remaining validation-backstopped timing residual.

Tests: x-run-tests --stop-on-fail ConsensusExtensions; x-run-tests --stop-on-fail ConsensusRng; x-format-changed; git diff --check
2026-06-27 12:08:35 +07:00
Nicholas Dudfield efcd2a0ab7 fix(export): close replay witness review gaps
Thread the replay parent through ApplyOptions so export replay can rebuild the historical validator view without relying on LedgerMaster visibility for freshly replayed parents.

Harden ttEXPORT_SIGNATURES canonicality before witness indexing, validate witness signer accounts against their pubkeys, reject ambiguous absent NetworkID on low-ID source chains, and lock the consensus phase accessor.

Add focused regression coverage and clarify that destination SignerList/quorum compatibility is an operator/client contract.
2026-06-27 10:53:34 +07:00
Nicholas Dudfield 8ddb0a1134 docs(export): document witness assembly contract
State that clients expanding sfExportSignatureHash must match ExportResultBuilder's canonical signer ordering, empty SigningPubKey, and target-chain signer cap before hashing or submitting.
2026-06-27 10:14:43 +07:00
Nicholas Dudfield 891077daa7 fix(export): harden witness edge checks
Validate ttEXPORT_SIGNATURES witness bindings in preflight, guard standalone witness synthesis against malformed inner transactions, and align ticket-path XPOP quorum with Export's ceiling threshold.

Regenerate the genesis hook sfcodes header and add it to generated-header verification so CI catches future drift.
2026-06-27 10:10:45 +07:00
Nicholas Dudfield f0224cac1e fix(export): replay witnesses without current manifests
Historical LedgerReplay now treats ttEXPORT_SIGNATURES witnesses as validated-history membership input, while still verifying signatures and threshold. Live build and validation keep the current manifest-backed active-signer check.

Harden witness handling by indexing only canonical same-ledger pseudos, dropping duplicate witnesses as ambiguous, and replacing pre-existing mismatched witnesses with the accepted sidecar witness during onPreBuild.

Document the lean witness-vs-XPOP proof distinction and cover the manifest-rotation replay case with an Export regression test.
2026-06-27 09:38:07 +07:00
Nicholas Dudfield 845798b072 fix(export): replay signatures through witness pseudo 2026-06-27 08:13:23 +07:00
Nicholas Dudfield 9f04dcf582 docs(export): capture replay witness invariant 2026-06-27 06:00:16 +07:00
Nicholas Dudfield 8a8a0b434d test(entropy): assert sub-quorum closed-ledger fallback 2026-06-27 05:34:01 +07:00
Nicholas Dudfield 2fbf2e11bc fix(consensus): drop late sidecars after accept 2026-06-27 05:25:31 +07:00
Nicholas Dudfield bac359fd96 docs(consensus): add entropy intent spine 2026-06-27 05:14:00 +07:00
Nicholas Dudfield fcedd01761 chore(hooks): regenerate sfcodes header 2026-06-26 22:22:46 +07:00
Nicholas Dudfield 9f24cd0b75 docs(consensus): describe entropy no-veto gate 2026-06-26 22:11:43 +07:00
Nicholas Dudfield d6e7cf428b fix(consensus): undo b12cee5d4 entropy observation veto 2026-06-26 21:56:40 +07:00
Nicholas Dudfield 72f73ccbad test(hooks): pin export reservation budget 2026-06-26 21:31:34 +07:00
Nicholas Dudfield a78f09a0fe fix(import): allow ticket xpop network id 2026-06-26 20:48:26 +07:00
Nicholas Dudfield f3526952b6 fix(import): skip b2m fee checks for ticket imports 2026-06-26 20:29:16 +07:00
Nicholas Dudfield da31668a2a fix(export): require retry expiry 2026-06-26 20:26:23 +07:00
Nicholas Dudfield 5dc214ab48 fix(hooks): report exports separately from emits 2026-06-26 20:20:09 +07:00
Nicholas Dudfield 4e217fcc8f fix(hooks): remove dice modulo bias 2026-06-26 20:13:26 +07:00
Nicholas Dudfield d5608c3110 style(export): match CI clang-format wrapping 2026-06-26 20:06:57 +07:00
Nicholas Dudfield 7dddc8256a refactor(proof): remove unused tx leaf helper 2026-06-26 19:57:44 +07:00
Nicholas Dudfield 45a69538cb fix(consensus): ignore stale acquired sidecars 2026-06-26 19:51:20 +07:00
Nicholas Dudfield e8cbf0f968 fix(consensus): drop own-key proposals without aborting 2026-06-26 19:38:40 +07:00
Nicholas Dudfield 1cdf850e4a fix(consensus): guard keyless proposal authoring 2026-06-26 19:35:07 +07:00
Nicholas Dudfield cddcb83047 refactor(consensus): remove unused reveal proof cache
Reveal sidecars intentionally omit proof blobs to keep entropySet hashes deterministic. Remove the write-only proposalProofs_ cache and update the surrounding comments to match the current sidecar format.
2026-06-26 19:22:33 +07:00
Nicholas Dudfield ed3c0f07a7 fix(export): return duplicate for live shadow tickets
A second pending export for the same shadow-ticket key is a ledger-object conflict, not an internal failure. Return tecDUPLICATE and cover the live-latch case in Export_test.
2026-06-26 19:14:10 +07:00
Nicholas Dudfield c74295e51d docs(consensus): clarify validator view fallback scope
Document that makeActiveValidatorView's null-ledger fallback is for startup, standalone, and diagnostics paths, while closed-ledger apply callers must fail closed before using it.
2026-06-26 19:06:18 +07:00
Nicholas Dudfield 370e02ee1f docs(export): note shadow-ticket replay caveat
Document that shadow tickets are re-creatable latches rather than permanent tombstones, so import-callback hooks that move value must deduplicate same-XPOP replays themselves.
2026-06-26 18:00:10 +07:00
Nicholas Dudfield 38672fba9f fix(consensus): reject noncanonical extended positions
Require legacy consensus positions to use the exact 32-byte form. Extended encodings now need a nonzero known flag byte, preventing alternate wire encodings of the same tx-set hash.
2026-06-26 17:57:55 +07:00
Nicholas Dudfield 932ccbe0e5 fix(export): bind trusted wire signatures to proposal digest
Require wire-carried export signatures to match the signed ExtendedPosition digest before harvesting, and keep the per-blob size guard local to the helper.
2026-06-26 17:48:19 +07:00
Nicholas Dudfield 927ec66a9e fix(consensus): gate observed participant diagnostics
Treat observedParticipantsHash as entropy extension material during proposal precheck, so disabled featureConsensusEntropy rejects diagnostic-only extension payloads too.
2026-06-26 17:44:26 +07:00
Nicholas Dudfield 3c108d3a87 fix(protocol): preserve local TER code values
Keep telENV_RPC_FAILED in its original local-error slot and append telSHADOW_TICKET_REQUIRED after it, with a TER regression test pinning both values.
2026-06-26 17:40:46 +07:00
Nicholas Dudfield 07030e2b87 fix(export): retry without parent validator view
Network-mode Export apply must build its signer universe from the closed ledger's parent. If that parent ledger is unavailable, retry instead of falling back through makeActiveValidatorView to the node-local latest validated ledger.
2026-06-26 17:23:19 +07:00
Nicholas Dudfield 8373621445 docs(consensus): clarify liveness exit scope
Document that stalled, moved-on, and expired consensus exits are generic consensus-engine liveness behavior coordinated by software release, not amendment-gated ledger-rule behavior.
2026-06-26 17:18:56 +07:00
Nicholas Dudfield b4208459cf fix(consensus): harden export signatures and weak RNG domain
Bound proposal-carried export signature blobs before hashing them during proposal precheck, and apply the same size cap at export signature harvest as defense in depth.

Cap the unverified export-signature cache by distinct tx hash while leaving verified signatures and existing entries ungated, so real exports can still reach quorum.

Domain-separate Hook RNG using the actual strong/weak execution role at draw time, and document the residual commit/reveal withholding bias across all entropy tiers.
2026-06-26 17:08:32 +07:00
Nicholas Dudfield d091ebfb7e Pin extension apply to accepted sidecar roots
Require entropy injection and export apply to consume only sidecar roots accepted by their convergence gates. Local timeout state and later collector growth no longer participate in closed-ledger material once a sidecar root is accepted.

Also guard RCLConsensus::extensionsBusy() under the consensus mutex, cap exported multisig signer arrays to the protocol maximum after canonical signer sorting, and document the remaining accept-vs-timeout liveness tradeoff.

Add regressions for unaccepted export sidecar roots, late export collector mutation, entropy accepted-root handling, exported signer caps, and CSF accepted-sidecar behavior.
2026-06-26 16:50:19 +07:00
Nicholas Dudfield 229eb5b573 refactor(consensus): clarify extension sidecar seams 2026-06-26 13:08:46 +07:00
Nicholas Dudfield 938d3055ce fix(consensus): harden threshold helpers 2026-06-26 11:47:16 +07:00
Nicholas Dudfield aee3a638ee fix(consensus): avoid NegativeUNL cap overflow 2026-06-26 10:26:47 +07:00
Nicholas Dudfield 4fb91ea9f5 refactor(consensus): expose threshold policy seams 2026-06-24 18:58:24 +07:00
Nicholas Dudfield acb492a2c9 docs(consensus): polish guided review excerpts 2026-06-24 14:50:43 +07:00
Nicholas Dudfield 92fe444323 docs(consensus): tighten guided review anchors 2026-06-24 14:15:23 +07:00
Nicholas Dudfield 9549901014 docs(consensus): clarify extended position identity 2026-06-24 13:35:25 +07:00
Nicholas Dudfield 9f6e7dd315 refactor(consensus): make extended position identity explicit 2026-06-24 13:10:57 +07:00
Nicholas Dudfield f6d986bdbc chore: remove stale branch TODO comments 2026-06-24 12:48:52 +07:00
Nicholas Dudfield e55bf43986 test: document consensus entropy fixture opt-in 2026-06-24 12:38:09 +07:00
Nicholas Dudfield b4beb92c34 chore: match levelization output 2026-06-23 16:49:01 +07:00
Nicholas Dudfield 40edbfc7f2 chore: update levelization results 2026-06-23 16:46:25 +07:00
Nicholas Dudfield ff205b1b81 docs(hooks): generate entropy API comments 2026-06-23 16:43:45 +07:00
Nicholas Dudfield ddfb1dbeb6 docs: clarify entropy and runtime test APIs 2026-06-23 16:42:05 +07:00
Nicholas Dudfield 8bf1ece0a0 test(export): pin expiry and no-veto boundaries 2026-06-23 16:42:02 +07:00
Nicholas Dudfield 639e153f3b fix(consensus): gate proposal extensions by parent ledger 2026-06-23 16:41:58 +07:00
Nicholas Dudfield 8e542c32e0 test(testnet): clarify directed runtime latency syntax 2026-06-23 16:19:10 +07:00
Nicholas Dudfield 38f4d53ebf test(testnet): add runtime latency probe suite 2026-06-23 16:04:58 +07:00
Nicholas Dudfield fe66a11c69 build(test): allow external hook Env tests 2026-06-23 13:38:43 +07:00
Nicholas Dudfield 77e48d553c test(export): cover sidecar rejection preflights 2026-06-23 11:53:51 +07:00
Nicholas Dudfield 439031dc92 Merge remote-tracking branch 'origin/dev' into feature-export-rng 2026-06-23 10:55:43 +07:00
Nicholas Dudfield 1a5b934881 test(csf): model sidecar split-brain equivocation 2026-06-23 10:52:38 +07:00
Nicholas Dudfield 7f0d2959e8 test(export): cover no-veto sidecar withholding 2026-06-23 10:34:01 +07:00
Nicholas Dudfield 1440d1495f test(consensus): cover split-brain sidecar threshold 2026-06-23 10:20:34 +07:00
Nicholas Dudfield 57f5a9d6cc test(testnet): cover export without UNLReport 2026-06-23 10:14:09 +07:00
Nicholas Dudfield cacd1f71fe test(consensus): mint tier 2 with active nUNL 2026-06-23 10:11:40 +07:00
Nicholas Dudfield 93a6f0fbec docs(testnet): add export degradation projection marker 2026-06-23 10:07:02 +07:00
Nicholas Dudfield c5b1cb222d docs(consensus): add test projection markers 2026-06-23 10:03:04 +07:00
Nicholas Dudfield e447f9f021 docs(consensus): add export apply projection markers 2026-06-23 09:55:01 +07:00
Nicholas Dudfield 0757094ed2 docs(consensus): add formal proof projection markers 2026-06-23 09:41:55 +07:00
Nicholas Dudfield 4b8107d57c docs(consensus): clarify nUNL entropy gate thresholds 2026-06-23 08:42:22 +07:00
Nicholas Dudfield d4b9e2f22c chore(formal): ignore local Lean proof workspace 2026-06-23 08:14:03 +07:00
Nicholas Dudfield 3fc199017e docs(cmake): note runtime config conan opt-in 2026-06-22 16:57:30 +07:00
Nicholas Dudfield 4d55443976 refactor(runtime-config): scope fault injection controls 2026-06-22 16:51:23 +07:00
Nicholas Dudfield 0d1d649867 refactor(consensus): remove orphaned txset cache callback 2026-06-22 15:33:29 +07:00
Nicholas Dudfield 60469dbd86 refactor(rng): remove explicit-final proposal path 2026-06-22 15:07:18 +07:00
Nicholas Dudfield 3970912735 chore: update levelization results 2026-06-22 13:31:56 +07:00
Nicholas Dudfield 456f4144ba docs(rng): mark explicit-final for removal 2026-06-22 12:50:19 +07:00
Nicholas Dudfield 570cad4c44 test(export): pin network apply regressions 2026-06-22 12:14:42 +07:00
Nicholas Dudfield b13868b71e fix(export): apply agreed sidecar signatures
Use the agreed exportSigSetHash sidecar map, not the live ExportSigCollector, as the network-mode ttEXPORT signer snapshot. Preserve export convergence state through RNG onPreBuild cleanup so buildLCL apply can see the gate result, and require a UNLReport-backed validator view before non-standalone Export finalization. Add regression coverage for agreed-vs-live collector mutation and onPreBuild export-state preservation.
2026-06-22 09:43:03 +07:00
Nicholas Dudfield 288b9e6d25 docs(rng): clarify commit substate pipelining 2026-06-22 09:06:12 +07:00
Nicholas Dudfield e55c2c6dc8 fix(export): allow quorum-aligned sidecar despite missing observation 2026-06-22 09:00:46 +07:00
Nicholas Dudfield 35e981e509 docs(rng): align ConsensusExtensionsDesign with the UNLReport-only + F1 changes
A drift review (10-section adversarial pass + skeptical verification) found the design doc had fallen behind two recent code changes: the UNLReport-only gate (8368e12ab) and the F1 active-view alignment filter (fb9e2710c).

- Fallback Semantics: add the second fallback trigger -- a non-UNLReport-backed view mints consensus_fallback regardless of alignment.
- Validator Set And Quorum: note the config-fallback view is !fromUNLReport and forces consensus_fallback entropy.
- Entropy Alignment Rules: state the UNLReport precondition for non-fallback tiers; correct the alignment-count formula to the active-view-filtered count (non-active proposers and a non-active local +1 excluded -- the F1 fix); separate the two distinct counts (the peer-alignment GATE count vs the agreed entropySetMap_ leaf count that drives the tier LABEL); flag the explicit-final unfiltered exception.
- Worked examples: note they assume a UNLReport-anchored view (else all fall back).
- Export Principles: document that export success also requires full observation (peersSeen == txConverged), not just quorum alignment.
- Review Checklist: add items for the UNLReport-anchored-tier and alignment-count-universe (F1) invariants.

Doc-only. Raw drift findings kept in .ai-docs (gitignored).
2026-06-22 08:41:11 +07:00
Nicholas Dudfield b9a733c831 docs(rng): flag the F1 gap on the explicit-final alignment path
The explicit-final proposal path counts participants/alignment over the unfiltered trusted-proposer set (ctx.peerPositions) plus an unconditional local +1, not the active validator view -- the same gap the F1 fix closed for the main entropy gate (inspectTxConvergedSidecarPeers). It is default-off/experimental with no robust timing model found (per the existing TBD, may never ship); if it is ever enabled it must apply the same active-view membership filter. Comment-only.

Follow-up to F1 (fb9e2710c).
2026-06-18 13:32:38 +07:00
Nicholas Dudfield 301e546aa9 chore(rng): assert active view source at round start 2026-06-18 10:42:28 +07:00
Nicholas Dudfield e2da1db6d2 chore(rng): harden entropy fallback diagnostics 2026-06-18 10:32:07 +07:00
Nicholas Dudfield 16cd02156e test(rng): use a nonzero sentinel in the hook entropy-requirement test
testInvalidEntropyRequirements rejects four invalid dice/random requirements then returned accept(0,0,0) on success. But a valid dice(6,..) returns 0..5, so a regression that let the min_tier=0 requirement through (returning a value that happened to be 0) would still pass ~1/6 of the time -- the weakest spot is exactly the min_tier lower bound it most needs to prove (review finding). Return sentinel 42 (distinct from any dice/random result and from INVALID_ARGUMENT) and assert ==42, so any leaked requirement returns its own non-42 code and fails. WASM block recompiled; ConsensusEntropy 138 tests, 0 failures.

Follow-up to F1 (fb9e2710c).
2026-06-18 09:54:30 +07:00
Nicholas Dudfield fb9e2710cc fix(rng): scope sidecar alignment count to the active validator view (F1)
The entropy/export bless-vs-fallback gate counted alignment over ALL tx-converged trusted proposers (currPeerPositions_), while the thresholds and the entropy leaf set are computed over the active validator view. A node that locally trusts proposers outside the on-ledger UNLReport active set could pad alignedParticipants(), inflating the counting universe N above originalViewSize and eroding the Tier-2 intersection margin (2t - N) below the Byzantine floor f -- so two equivocation cohorts padded by non-active aligners could each clear the gate (review finding F1). Backstopped by the 80% validation quorum, but the proof's universe and the code's universe must match.

inspectTxConvergedSidecarPeers now takes an active-view membership predicate and counts only member peers toward alignment; the local +1 is gated on localIsActiveValidator(). Both the RNG entropy gate and the export-sig gate pass ext.isUNLReportMember / ext.localIsActiveValidator, mirroring buildEntropySet / hasQuorumOfCommits' containsNode filter. New method on ConsensusExtensions + CSF Peer + the FakeExtensions stub.

Regression test (Sidecar peer alignment helper): a trusted-but-non-active aligned proposer pushes unfiltered aligned to 2 but filtered aligned stays 1, and a non-active local node's +1 is suppressed -- padding cannot satisfy the gate. ConsensusExtensions 900, ConsensusRng 386, Consensus 1399, all green.

Note: the explicit-final proposal path (ConsensusExtensionsTick.h ~:934) counts over prevProposers and is NOT yet filtered (separate, experimental path).
2026-06-18 09:44:56 +07:00
Nicholas Dudfield c8fad50d66 Merge origin/dev into feature-export-rng
# Conflicts:
#	src/xrpld/app/hook/applyHook.h
2026-06-17 11:45:42 +07:00
Nicholas Dudfield d9b5fc26fc style(rng): apply clang-format 2026-06-17 11:35:05 +07:00
Nicholas Dudfield 72d03620f9 test(rng): require UNLReport in testnet entropy suites 2026-06-17 11:33:04 +07:00
Nicholas Dudfield 8368e12ab3 fix(rng): require UNLReport view for non-fallback entropy 2026-06-17 11:32:51 +07:00
Nicholas Dudfield 23f745bd01 fix(hook): validate entropy requirements 2026-06-17 11:32:38 +07:00
Nicholas Dudfield 27ae39b91a fix(rng): use a genuinely forkable nUNL config in the anchor test
The tier-2 anchor pin (20d52d8b6) used an 8/6 (original/effective) nUNL config whose rationale was wrong: it computed cohort overlap against the original view (2*4-8=0), but aligned cohorts form in the EFFECTIVE view, so two 4-of-6 cohorts overlap by 2*4-6=2 > floor(8/5)=1 -- 8/6 is NOT actually forkable, the original-view anchor is merely conservative there. The test still caught the originalViewSize->size() regression, but the stated reason was misleading (review catch).

Switch to 10 active / 2 disabled (original 10, effective 8), a genuinely forkable case: a 5-of-8 cohort (what the effective-size threshold would admit) overlaps only 2*5-8=2, which does NOT exceed the f=floor(10/5)=2 faulty the original UNL still tolerates -> an equivocator could mint two distinct tier-2 digests. The correct original anchor requires 7 (overlap 6 > 2) and keeps the band closed (tier2==quorum==7); a regression to size() drops the floor to 5 and re-opens the forkable [5,7) band. Assertions and rationale updated to match; suite green (897, 0 failures).
2026-06-16 17:33:57 +07:00
Nicholas Dudfield 20d52d8b66 test(rng): pin tier-2 threshold to pre-nUNL original view
tier2Threshold() anchors to originalViewSize (pre-nUNL), not the effective post-nUNL size(): nUNL shrinks the effective view while leaving faulty nodes in it, so a sub-quorum fraction of the effective view can exceed the Byzantine bound. A one-line regression to size() would relabel a forkable cohort as participant_aligned -- and no existing test caught it (the only nUNL test never calls tier2Threshold()/selectEntropy, and the only tier-2 selection test has no nUNL, so original == effective there).

Add testTier2ThresholdAnchorsToOriginalView: 8 active validators, 2 disabled via NegativeUNL (original 8, effective 6), asserting tier2Threshold()==5 (from the original 8) not 4 (from the effective 6), with quorum/gate cross-checks. Mutation-verified: flipping the production read to size() turns exactly this test red (tier2Threshold + entropyGateThreshold) while the existing tier-2 selection test stays green.

Also: pointer comment at tier2Threshold() linking the invariant to its guard test, and a design-doc fix -- the worked example used n=5, where the band is empty (quorum == participant_aligned == 4) so it illustrated an unreachable tier; now n=6.
2026-06-16 17:02:00 +07:00
Nicholas Dudfield b070785dee fix(rng): make tier-2 scenario's 5/6 check fallback-tolerant
The 5/6 phase asserted validator_quorum on whichever single ledger the validated tip happened to sit on. But the ledger right at the node-5 drop can be a transient consensus_fallback (EntropyTier=1, count=0) — deterministic and by design, while the commit/reveal pipeline re-primes — so when the tip landed there the assert failed (tier=1, observed ~1 run in 4). Same class of bug as the old post-recovery flake: depending on exactly where the tip lands.

Now settle 4 ledgers past the drop, then scan the post-drop validated ledgers for a clean validator_quorum (tier 3, count >= quorum). The window is entirely 5-node cohorts, so a tier-3 there has count == 5 (faithful to '5/6 still tier 3'); the transition fallback is tolerated, not asserted on. Verified 3/3: each run found tier=3 count=5.

Also harden _closed_entropy() to raise on != 1 ConsensusEntropy pseudo-tx (mirroring get_entropy_tx) instead of silently skipping — a duplicate/missing injection now fails with a clear error rather than resurfacing as a generic 'no tier-2 ledger'.
2026-06-16 11:15:55 +07:00
Nicholas Dudfield def617e3f9 test(rng): harden tier-2 testnet scenario per review + robust on-ledger check
Codex review minors: assert_validator_quorum at the 5/6 boundary (EntropyTier=3 AND count >= quorum AND non-zero digest, not just tier==3 — catches a bad tier/count pairing); added explicit assert_validator_quorum / assert_consensus_fallback helpers and an entropy_fields() warning that its is_fallback (tier != 3) lumps participant_aligned in with fallback (safe only where no tier-2 band exists).

Robustness: tier 2 is below the validation quorum, so the validated tip stalls and which provisional ledgers it later reaches is timing-dependent (the post-recovery inspection was fragile). Replaced it with direct inspection of the surviving cohort's CLOSED ledger via ledger('closed') DURING the window; recovery is now a pure liveness check.

Verified on a live testnet: PASS — EntropyTier=2 count=4 confirmed on 4 distinct provisional ledgers (seq 7-10).
2026-06-16 10:51:33 +07:00
Nicholas Dudfield c322b59961 test(rng): tier-2 (participant_aligned) testnet scenario at n=6
A 6-node smoke (the smallest NON-degenerate tier-2 size: tier2 floor 4, quorum 5) driving the 4/6 band. n=5 has no band (tier2 == quorum), which is why the existing degradation smoke only ever sees tier 3 / fallback.

Tier 2 is below the 80% validation quorum, so the 4/6 cohort's ledgers are provisional: the scenario confirms tier-2 injection from the cohort's logs during the window, then verifies the on-ledger EntropyTier=2 count=4 POST-RECOVERY once those ledgers become canonical and validate (the mechanism the degradation smoke also relies on). Adds the assert_participant_aligned helper and the node_count:6 suite entry.

Verified on a live testnet: PASS — 12 tier-2 injections in the 4/6 window, seq 7 & 8 validated as participant_aligned (count 4).
2026-06-16 10:28:57 +07:00
Nicholas Dudfield c3cc3513c9 docs(rng): refresh tier 2 terminology 2026-06-16 10:05:57 +07:00
Nicholas Dudfield a0be935227 docs(rng): correct stale tier-2 comments
Codex final-review minors, both comment-only (no behavior change): EntropyTier participant_aligned no longer says "reserved for a future" tier (it is implemented in this stack); and the commit-timeout path comment said "fixed UNL quorum" but the code now uses entropyGateThreshold() (min(quorum, tier2)).
2026-06-16 09:45:08 +07:00
Nicholas Dudfield 6b1b18dd38 refactor(rng): guard CSF entropy finalize to reveal-type sidecar
Codex final-review minor (optional hardening): finalizeRoundEntropy now requires the fetched sidecar entry to be reveal-type before counting it as entropy. lastEntropySetHash_ only ever names a reveal set (hashRngSet's per-type salt rules out a cross-type hash collision), so this is purely defensive — behavior unchanged, all CSF sims green.
2026-06-16 09:30:42 +07:00
Nicholas Dudfield 946f25249b fix(rng): CSF finalizes from advertised set, not live pendingReveals_
Codex review caught a CSF/production fidelity gap: production injects entropy from the AGREED entropySetMap_ (selectEntropy), which is frozen at advertise time — late-fetched or conflicting reveals merge into pendingReveals_ but are NOT injected unless a rebuild republishes the hash. The CSF peer fetched into pendingReveals_ AND finalized from pendingReveals_, so a conflict/fetch sim could count reveals production would never inject from.

Track the last advertised entropy-set hash (buildEntropySet) and finalize from the sidecar-store snapshot under that hash — the analog of the frozen entropySetMap_. Clean/no-conflict sims are unchanged (the snapshot equals pendingReveals_ there); the model is now faithful for conflict/fetch cases too. Production unaffected (test-harness only). Addresses finding 1 of codex-tier2-final-review.
2026-06-15 18:44:05 +07:00
Nicholas Dudfield a95306142e test(rng): tier-2 mint sim — in-band cohort at n=6
6 validators is the smallest non-degenerate tier-2 size (f=1, one-wide band {4}: tier2=4, quorum=5). Isolate 2 so the surviving 4-cohort is below the 80% quorum but at the tier-2 floor; it mints participant_aligned entropy (count 4), and all four agree on the same non-zero digest with branches==1 — no hang, no fork. Distributed confirmation of the selector ladder the unit tests already cover.
2026-06-15 17:48:53 +07:00
Nicholas Dudfield 8d43154061 feat(rng): model tier 2 in CSF sims + reconcile sub-quorum sims
The CSF peer now mirrors production's tier-2 behavior so the RNG simulations
exercise participant_aligned, not just validator_quorum/fallback:
- entropyGateThreshold() = min(quorumThreshold(), tier2Threshold()), plus a
  tier2Threshold() helper (calculateParticipantThreshold over the sim's UNL).
- finalizeRoundEntropy() labels by aligned count via the 3-tier ladder
  (>= quorum -> validator_quorum, >= tier2 -> participant_aligned, else
  fallback) instead of hardcoding tier 3.

The n%5==0 band-collapse keeps the bulk of the suite unaffected: at n=5
tier2 == quorum == 4, so the gate is unchanged for every n=5 and n<=2 network.
Only the two n=3 sub-quorum sims cross a live band (n=3: tier2=2 < quorum=3) and
now mint tier 2 instead of falling back -- the feature working, with each test's
intent preserved:
- "impossible quorum fallback" -> "quorum-impossible cohort falls to
  participant_aligned": 2 of 3 is below 80% but at the tier-2 floor, so it makes
  progress as tier 2 (no hang, no fork) rather than falling all the way back.
- "persistent loss does not shrink quorum": the 2 survivors now mint the
  labeled-weaker tier 2, NOT tier 3 -- the tier-3 quorum still did not shrink
  (a min_tier=3 hook rejects it). Intent preserved, outcome relabeled.

Other CSF suites (Consensus, ByzantineFailureSim) are unaffected (they do not
enable RNG). Next: dedicated tier-2 mint + conflict sims at n=6 (smallest size
with a non-degenerate band, f=1).
2026-06-15 17:41:47 +07:00
Nicholas Dudfield 156a8cbb85 refactor(rng): address review minors (dead-API rename + explicit-final dedup)
Two small follow-ups from the Codex review of the tier-2 implementation, both
on the consensus-extension internals:

- Rename shouldZeroEntropy() -> belowValidatorQuorum(). The selector replaced its
  callers, leaving it production-dead and MISNAMED: post-tier-2, "below the 80%
  validator quorum" is no longer "zero entropy" (a participant_aligned set is
  sub-quorum but non-zero). The new name + a doc comment make it a tier-3
  eligibility predicate only and warn against gating injection on it
  (selectEntropy() owns the tiering). Behavior unchanged.

- buildExplicitFinalProposalTxSet now dedups the entropy pseudo-tx by VALUE, not
  type. Its comment claimed it "mirrors onPreBuild", but onPreBuild went
  value-based: it verifies a present pseudo-tx is the EXACT txID it would have
  produced and logs a determinism-violation on mismatch. Explicit-final now does
  the same (skip-duplicate-verified on match, error log on mismatch), returning
  the base unchanged either way. Default-off experimental path, so low blast
  radius, but the two paths now agree.
2026-06-15 17:14:30 +07:00
Nicholas Dudfield 14ebe74a56 fix(rng): derive tier2 threshold from intersection bound (safe at n%5==0)
calculateParticipantThreshold returned ceil(0.6*n). At every n divisible by 5
(n=5,10,15,20,...) that leaves two aligned cohorts overlapping in exactly
floor(0.2*n) = f validators -- NOT strictly greater than f. So up to f Byzantine
nodes can occupy the entire overlap, leaving no honest validator shared between
the two cohorts, and a single equivocator backed by f-1 colluders can split the
round into two distinct aligned digests -> fork. The spec caught n=5 ("never 5")
but the same failure recurs at every multiple of 5.

Derive the floor from the safety invariant instead: the smallest t with
2t - n > floor(n/5), i.e. floor((n + floor(n/5)) / 2) + 1. This equals
ceil(0.6*n) everywhere except multiples of 5, where it is one higher
(n=10 -> 7, not 6). n=5 now collapses the band (tier2 == quorum), so the
"never 5" operational caveat is enforced by the math rather than a footnote.

Tests: the arithmetic test now asserts the defining invariant
2t - n > floor(n/5) AND t <= quorum for every n in 1..256 (this fails at n=10
under the old formula), plus the bumped boundaries. The onPreBuild tier-2 test
moves off n=5 (no band) onto n=6 (tier2=4, quorum=5; 5/4/3 revealers ->
validator_quorum / participant_aligned / fallback).

Found by Codex adversarial review of the tier-2 implementation.
2026-06-15 16:59:55 +07:00
Nicholas Dudfield 580f07ce35 feat(rng): enable tier 2 (participant_aligned) sub-quorum entropy
A 60-79% aligned cohort now mints entropy labelled participant_aligned (tier 2)
instead of falling back. Healthy >=80% rounds are unchanged (validator_quorum),
and a true minority (<60%) still falls back.

Mechanics:
- entropyGateThreshold() = min(quorumThreshold(), tier2Threshold()): the bar at
  which the pipeline engages and the entropy conflict gate resolves. In the
  normal band this is the 60% floor (of the ORIGINAL, pre-nUNL view); under
  heavy nUNL the band collapses to the 80% quorum and tier 2 vanishes.
- selectEntropy() labels the AGREED entropySetMap_ by participant count:
  >= quorum -> validator_quorum, >= tier2 -> participant_aligned, else fallback.
- Tick.h gates (bootstrap-skip, impossible-quorum, commit-timeout, entropy
  conflict gate) key off entropyGateThreshold() so sub-quorum rounds reach
  injection instead of short-circuiting.

Determinism: the tier LABEL is a function of the agreed set's leaf count
(identical on every node holding that hash); the local entropyGateThreshold
alignment only decides proceed-vs-fall-back, so divergent local views fall back
rather than fork. The 60% floor is over the ORIGINAL view -- the
quorum-intersection bound that stops a single equivocator minting two distinct
aligned digests under the ~20% Byzantine bound.

hasQuorumOfCommits() is deliberately LEFT at 80%: healthy networks keep their
exact fast-path and only step down to tier 2 via the commit-timeout path, so
this adds zero behavior change above quorum (the degraded band pays one pipeline
timeout; a fast-path is a separable follow-up). Folds into featureConsensusEntropy
(not yet active), so no new amendment.

Tests: new onPreBuild tier-2 case (5-validator view; 4/3/2 revealers ->
validator_quorum / participant_aligned / fallback) plus threshold assertions.
CSF Peer and the tick test stub mirror entropyGateThreshold() to quorumThreshold()
for now -- the end-to-end tier-2 sims (which lower it) land next. Extracted
makeUNLReportLedger / harvestCommitReveal test helpers, now shared across the
view and harvest tests.
2026-06-15 15:55:43 +07:00
Nicholas Dudfield d4c0ba3769 refactor(rng): unify entropy selection into one deterministic selector
Collapse the duplicated tier-selection logic in onPreBuild and
buildExplicitFinalProposalTxSet into a single selectEntropy() over the AGREED
entropySetMap_. No behavior change on the production (implicit) path — the
onPreBuild fallback/entropy-set/standalone/mismatch tests pass byte-identically.

Fixes the pre-existing divergence flagged in review: buildExplicitFinalProposalTxSet
derived entropy from local pendingReveals_ while onPreBuild used the agreed
entropySetMap_, so the two could mint different digests for the same round.
Both now share the selector, so the implicit and (experimental, default-off)
explicit-final paths — and any two nodes — derive identical entropy from
identical agreed inputs.

selectEntropy() returns {digest, tier, count} and is a pure function of agreed
round state, so it is directly unit-testable. Injection becomes unconditional:
the selector always yields a fallback digest when there is no validator entropy,
so every RNG-enabled ledger still carries exactly one ConsensusEntropy tx.

Sets up the tier-2 (participant_aligned) ladder, which lands next.
2026-06-15 15:22:14 +07:00
Nicholas Dudfield 378d6b78c8 feat(rng): add pre-nUNL originalViewSize + tier2 participant threshold
Foundation for Tier 2 (participant_aligned) sub-quorum entropy. No behavior
change — nothing consumes these yet; the tier ladder, gates, and selector
arrive in later commits.

- ActiveValidatorView::originalViewSize: master-key count BEFORE the nUNL
  subtraction. size() stays the effective (post-nUNL) count used by the 80%
  validator-quorum gate; originalViewSize is the original-UNL denominator that
  the 60% Tier 2 floor anchors to, since nUNL can shrink the effective view
  while leaving faulty nodes in it.
- calculateParticipantThreshold(): ceil(0.6 * count), the quorum-intersection
  floor (two such cohorts always share an honest validator under the ~20%
  Byzantine bound).
- ConsensusExtensions::tier2Threshold(): ceil(0.6 * originalView), anchored to
  originalViewSize.

Tests: originalViewSize asserted on the UNLReport, fallback, and real-ledger
nUNL paths; new arithmetic testcase locks ceil(0.6) and the sizing-note
boundaries (5->3 banded/unsafe, 6->4 smallest-safe, 8->5 one-nUNL-off).
2026-06-15 15:11:41 +07:00
Nicholas Dudfield 4b219cdef8 test(testnet): use strength-ordered enum names, drop 'Tier 3' shorthand
The 'Tier N' preference-order shorthand collides with EntropyTier's
strength-ordered values (consensus_fallback=1). Use the enum names in
the degradation-smoke comments/descr to remove the ambiguity.
2026-06-12 17:24:23 +07:00
Nicholas Dudfield 9d4b97c824 fix(rng): defensive mismatch-log reads + mismatch-path test
Follow-up to d6481a386, addressing review of that diff:

- The mismatch-log branch in onPreBuild read present pseudo-tx fields
  (sfDigest/sfEntropyTier/sfEntropyCount) unconditionally — the same
  throw hazard fairRng was hardened against, here inside onPreBuild
  during build. Read them defensively (isFieldPresent ? value :
  '<missing>'). (Note: STTx deserialization enforces all soeREQUIRED
  fields, so a tier-less ttCONSENSUS_ENTROPY cannot actually reach the
  set — this is belt-and-suspenders, not a reachable bug.)
- Clarify the comment + add action=keep-agreed-and-flag: this is
  detect-and-log, NOT rejection. The present pseudo-tx is KEPT and still
  applied at BuildLedger; a hard-fail policy on mismatch is a deliberate
  future decision (determinism-violation vs halt-risk-under-skew).
- Add testOnPreBuildEntropyMismatchKeepsAgreed: a present-but-different
  entropy pseudo-tx is kept (not replaced), set stays at one entry.
- Fix stale 'type-based dedup' comment in the standalone test.
2026-06-12 17:09:53 +07:00
Nicholas Dudfield d6481a3869 fix(rng): value-based entropy pseudo-tx dedup + tier-read hardening
Address review findings on the tier 3 commits:

- onPreBuild dedup was type-based (skip if any ttCONSENSUS_ENTROPY
  present), which silently trusts a pre-present pseudo-tx. Injection is
  deterministic, so every honest node derives the identical pseudo-tx
  (identical txID) for the same agreed inputs. Switch to value-based
  dedup: skip only when the present pseudo-tx EQUALS the one we would
  produce; a present-but-different entropy pseudo-tx is a determinism
  violation (version skew / divergent peer) and is now logged at error
  rather than accepted blindly. (The earlier 'cannot reconstruct the
  txID locally' justification was wrong — determinism guarantees it can.)
- fairRng: read sfEntropyTier defensively (missing => 0 => fail closed).
  The field is soeREQUIRED so any entry this code wrote carries it; this
  only guards a pre-tier-3 persisted entry on a long-lived testnet.
- quorum_degradation_smoke: assert EntropyTier==consensus_fallback(1) and
  EntropyCount==0 and non-zero digest explicitly, not just is_fallback.
2026-06-12 16:39:39 +07:00
Nicholas Dudfield 0cf6f73441 test(testnet): align degradation smoke comments with tier 3 semantics 2026-06-11 13:57:53 +07:00
Nicholas Dudfield 08a6f3cd57 docs(consensus): describe tier 3 fallback entropy semantics 2026-06-10 16:58:43 +07:00
Nicholas Dudfield 77d78236e8 feat(hooks): dice/random take required min_tier + min_count args
The entropy-quality requirement becomes an explicit, required argument
at every call site — there is deliberately no default and no network
constant:

- dice(sides) -> dice(sides, min_tier, min_count)
- random(ptr, len) -> random(ptr, len, min_tier, min_count)
- fairRng gates on freshness && tier >= min_tier && count >= min_count;
  the hard-coded 'EntropyCount >= 5' network constant is deleted —
  the hook author states what their application needs and gets
  TOO_LITTLE_ENTROPY when this ledger cannot meet it
- WASM imports have no default parameters, so the old no-argument shape
  was really a hidden constant invisible at the call site; requiring the
  arguments makes weak-entropy acceptance (min_tier=1) a deliberate,
  reviewable opt-in rather than an accident
- hook/extern.h and hook/sfcodes.h regenerated (CI-verified)
- test hooks updated and recompiled; new test pins the requirement
  gate: dice(6, 3, 21) against count=20 returns TOO_LITTLE_ENTROPY
2026-06-10 16:56:05 +07:00
Nicholas Dudfield c92c0656ec feat(rng): tier 3 consensus-bound fallback entropy
Replace the zero-entropy fallback with a deterministic consensus-bound
digest so every RNG-enabled ledger carries usable entropy:

- sha512Half(HashPrefix::entropyFallback, prevLedgerHash, baseTxSetHash,
  seq) — all inputs are already consensus-agreed at injection time, so
  no new agreement machinery is needed and the digest is identical on
  every node building the same ledger
- new sfEntropyTier (UINT8) on the ttCONSENSUS_ENTROPY pseudo-tx and
  ConsensusEntropy ledger entry: EntropyCount says how many validators
  contributed, EntropyTier says which gate the result passed
  (validator_quorum vs consensus_fallback; participant_aligned reserved)
- the fallback digest derives from the BASE (pre-injection) tx set hash
  to avoid circularity; entropy pseudo-tx dedup is now type-based since
  an explicit-final synthetic set can carry a pseudo-tx whose txID
  implicit nodes cannot re-derive
- unparseable-entropy-set residual now falls back instead of skipping
  injection, so a fresh ConsensusEntropy entry exists every ledger
- CSF Peer mirrors the fallback analog; sims assert deterministic
  non-zero fallback digests across same-LCL peers
- testnet scenarios updated: degraded windows expect labeled fallback
  entropy, never validator-tier

The fallback tier is user-influenceable via tx submission (quiet-ledger
grinding) and is labeled accordingly — hook-facing gating lands with the
min_tier/min_count API change.
2026-06-10 16:32:49 +07:00
Nicholas Dudfield fbdec3be66 chore: update levelization results 2026-06-10 12:46:09 +07:00
Nicholas Dudfield bb619cc100 fix: defer proposal feature checks 2026-06-10 11:09:12 +07:00
Nicholas Dudfield 10f22c84f2 fix: preserve malformed proposal rejection order 2026-06-10 09:08:33 +07:00
Nicholas Dudfield dd21024c0e test: improve export rng coverage 2026-06-09 19:56:46 +07:00
Nicholas Dudfield 16a72172b4 test(coverage): cover xpop empty proof output 2026-06-09 14:29:32 +07:00
Nicholas Dudfield 265012e16a test(coverage): expand runtime config option coverage 2026-06-09 14:26:04 +07:00
Nicholas Dudfield d072527bc5 test(coverage): cover consensus extension refresh paths 2026-06-09 14:23:20 +07:00
Nicholas Dudfield 5f5ce12fa6 test(coverage): cover export signature helper edges 2026-06-09 14:18:12 +07:00
Nicholas Dudfield 0804a01b9b test(coverage): cover consensus extension tick states 2026-06-09 14:12:54 +07:00
Nicholas Dudfield 80cd1bed34 test(coverage): cover export metadata roundtrip 2026-06-09 13:50:44 +07:00
Nicholas Dudfield b4e98ac1d7 test(coverage): cover peer proposal wrapper 2026-06-09 13:40:04 +07:00
Nicholas Dudfield 4f7e751fbd test(coverage): avoid xpop levelization dependency 2026-06-09 13:36:25 +07:00
Nicholas Dudfield 748fef6267 test(coverage): cover xpop proof edge cases 2026-06-09 13:32:26 +07:00
Nicholas Dudfield 4d48c9f949 test(coverage): cover export rng helper seams 2026-06-09 13:21:08 +07:00
Nicholas Dudfield 03c1216661 merge: absorb origin/dev 2026-06-09 09:44:39 +07:00
Nicholas Dudfield 537474cb5f style(consensus): apply ci clang-format 2026-06-09 09:20:27 +07:00
Nicholas Dudfield ec086d6765 test(export): cover signature upgrade race guard 2026-06-08 15:50:32 +07:00
Nicholas Dudfield 7362c1dac1 fix(hook): preserve xport nonce failure result 2026-06-08 15:50:17 +07:00
Nicholas Dudfield f0550ca625 refactor(export): extract signature upgrade policy 2026-06-08 14:49:16 +07:00
Nicholas Dudfield 526b60bf3d refactor(hook): extract xport wrapper builder 2026-06-08 14:37:58 +07:00
Nicholas Dudfield 9347b47639 refactor(export): extract result assembly 2026-06-08 14:21:44 +07:00
Nicholas Dudfield 9988568a08 refactor(consensus): extract export signature harvester 2026-06-08 14:10:43 +07:00
Nicholas Dudfield 13260b9ef7 test(runtime-config): cover effective config paths 2026-06-08 13:58:39 +07:00
Nicholas Dudfield 65dab780de refactor(consensus): share sidecar alignment scan 2026-06-05 15:07:44 +07:00
Nicholas Dudfield ee70e4cdbb refactor(consensus): extract active validator view builder 2026-06-05 15:02:51 +07:00
Nicholas Dudfield 7fb1509673 test(testnet): harden scenario feature checks 2026-06-04 15:50:31 +07:00
Nicholas Dudfield 64620e2825 test(testnet): assert export degradation logs 2026-06-02 12:30:07 +08:00
Nicholas Dudfield a87a7896ca test(testnet): align degradation log checks 2026-06-02 12:25:12 +08:00
Nicholas Dudfield 443aca8611 chore(logging): polish RNG final diagnostics 2026-06-01 14:08:12 +08:00
Nicholas Dudfield 804b76b4ab chore(logging): normalize consensus extension diagnostics 2026-06-01 13:57:46 +08:00
Nicholas Dudfield ab6571a20f fix(runtime-config): avoid overlay dependency 2026-05-22 14:58:22 +08:00
Nicholas Dudfield 331e1606a3 feat(runtime-config): support startup message filters 2026-05-22 14:50:59 +08:00
Nicholas Dudfield 24d6dea1a2 test(testnet): enable consensus extension diagnostics
Add ConsensusExtensions debug logging to the entropy and export scenario suite defaults so participant bitmap diagnostics are captured without live log-level changes.
2026-05-21 10:21:56 +08:00
Nicholas Dudfield 03e0bb5fc3 feat(consensus): add active participant diagnostics
Carry a signed observedParticipantsHash in ExtendedPosition and log a local canonical bitmap over the active validator view for degraded-round debugging. The field is diagnostic only and does not affect proposal equality or quorum thresholds.
2026-05-21 09:56:51 +08:00
Nicholas Dudfield 0a77dbf68e Merge remote-tracking branch 'origin/dev' into feature-export-rng 2026-05-19 12:11:18 +08:00
Nicholas Dudfield 60a9a2c9fb Merge remote-tracking branch 'origin/dev' into feature-export-rng
# Conflicts:
#	include/xrpl/protocol/Feature.h
#	include/xrpl/protocol/detail/features.macro
2026-05-18 09:15:17 +07:00
Nicholas Dudfield 445d0070d8 Merge remote-tracking branch 'origin/dev' into feature-export-rng
# Conflicts:
#	hook/sfcodes.h
#	include/xrpl/protocol/Feature.h
#	include/xrpl/protocol/detail/sfields.macro
2026-04-30 14:22:06 +07:00
Nicholas Dudfield 61a8d8bba7 chore(hook): update generated tx flags 2026-04-30 11:03:58 +07:00
Nicholas Dudfield fbedb8a73a Merge remote-tracking branch 'origin/dev' into feature-export-rng 2026-04-30 10:57:07 +07:00
Nicholas Dudfield 8ae541fcc1 Merge remote-tracking branch 'origin/dev' into feature-export-rng
# Conflicts:
#	src/test/app/SetHookTSH_test.cpp
#	src/xrpld/app/tx/detail/InvariantCheck.cpp
#	src/xrpld/app/tx/detail/InvariantCheck.h
2026-04-30 08:14:51 +07:00
Nicholas Dudfield c8f3f6f05f docs(consensus): clarify extension fallback semantics 2026-04-29 15:27:46 +07:00
Nicholas Dudfield b12cee5d47 fix(consensus): wait for sidecar observation
Require tx-converged peers to advertise sidecar hashes before accepting RNG entropy or export signature success from local quorum alignment.

The RNG reveal fast path now publishes the entropy set and waits for peer observation instead of accepting in the same tick. On timeout, RNG clears the advertised entropy hash and falls back to deterministic zero.

Add unit and CSF regression coverage for asymmetric peer observation.
2026-04-28 12:20:42 +07:00
Nicholas Dudfield a3b1e45f4d fix(consensus): bound export sidecar observation
When a candidate set contains ttEXPORT but a node has no local verified export sig material yet, give tx-converged peers one bounded opportunity to advertise an exportSigSetHash before closed-ledger apply.

This is a safety coordination window, not a wait-for-Export-success mechanism. If no advertised sidecar arrives or fetched material cannot be merged by the deadline, Export convergence is marked failed and the transaction retries or expires through normal rules.

Add CSF coverage for a peer that can only succeed by fetching peer-advertised export sidecars, plus a direct ConsensusExtensionsTick test for the pre-advertisement observation window. Document the consensus-extension priority order: safe, fast, works.
2026-04-28 11:15:03 +07:00
Nicholas Dudfield 3938ba7af4 docs(consensus): record export sidecar material flow
Clarify that export sidecar publication is local verified material only, and fetched sidecar leaves must be active-view checked, candidate-tx verified, and promoted into ExportSigCollector before closed-ledger apply can use them.
2026-04-28 10:59:00 +07:00
Nicholas Dudfield 96b1104646 fix(consensus): use quorum for export-only sidecars
Export-only originally used unanimity as a conservative substitute for the CE/RNG sidecar machinery. That made sense before Export had its own signed ExtendedPosition field and exportSigSetHash convergence gate.

Now Export sidecars are signed and converged independently of RNG, so a quorum-aligned exportSigSetHash plus verified active-view signature quorum is deterministic enough for Export-only mode. Keeping unanimity would let one active validator veto an otherwise converged export round.

Update CSF and testnet coverage to treat Export-only the same way: one missing/conflicting signer in a 5-validator network succeeds at 4/5, while below-quorum still retries or expires.
2026-04-28 10:54:04 +07:00
Nicholas Dudfield 92bdd2ed9f fix(consensus): harvest replayed export signatures 2026-04-28 10:16:52 +07:00
Nicholas Dudfield d87cfdc604 fix(consensus): clear export sigs when export disabled 2026-04-28 09:17:17 +07:00
Nicholas Dudfield a956abb2d1 docs(consensus): clarify export sig quorum gates 2026-04-28 08:56:53 +07:00
Nicholas Dudfield aa36a80ab7 docs(consensus): document sidecar acquire rationale 2026-04-28 08:35:45 +07:00
Nicholas Dudfield e729aa11eb fix(hooks): preserve finalization semantics
Keep hook result/state finalization non-fatal while enforcing the hook-export backlog cap through the transaction-level ApplyContext guard. This avoids resetting non-success tec metadata and preserves hook_again weak execution behavior.
2026-04-28 07:47:20 +07:00
Nicholas Dudfield c58da3da58 fix(export): cap hook export backlog
Enforce the pending export cap for hook-emitted ttEXPORT work before commit. Replace the non-present sfEmittedTxn template field when building ltEMITTED_TXN entries so in-flight ledger checks see the emitted wrapper.

Overflowing xport emission now returns tecDIR_FULL and leaves the emitted backlog capped at ExportLimits::maxPendingExports.
2026-04-27 22:55:23 +07:00
Nicholas Dudfield 0c2c59d258 fix(export): enforce pending export limits
Cap pending ttEXPORT work in open/apply ledgers, including hook-emitted exports when TxQ drains the emitted directory into the open ledger. Enforce the same bound for per-account shadow tickets so durable pending imports cannot grow unbounded.
2026-04-27 21:30:24 +07:00
Nicholas Dudfield 15662eb1b1 fix(consensus): cap export proposal signatures
Limit outbound TMProposeSet export signature attachments to ExportLimits::maxPendingExports so honest proposals stay within the same bound enforced by inbound proposal validation. Extra exports remain unsigned for that proposal and rely on the existing retry/expiry path.
2026-04-27 21:03:00 +07:00
Nicholas Dudfield 492fe90643 fix(consensus): expire stale export signatures
Stamp export signatures learned from proposals, sidecar sets, and candidate tx-set upgrades with a ledger sequence so cleanupStale can age them out. Remove invalid unverified signatures after tx-local verification fails, with a buffer match check to avoid deleting newer replacements.
2026-04-27 20:56:54 +07:00
Nicholas Dudfield ea413873b2 fix(consensus): preserve export state without rng 2026-04-27 20:34:58 +07:00
Nicholas Dudfield 625419eab7 fix(consensus): verify export sigs against tx set 2026-04-27 18:07:09 +07:00
Nicholas Dudfield 2218bdd7f3 fix(consensus): require export sigset quorum alignment 2026-04-27 17:36:06 +07:00
Nicholas Dudfield f13233b00a docs(consensus): clarify validation sidecar signing rule
Remove the stale TMValidation exportSignatures field from the draft proto path now that export signatures ride signed proposal sidecars. Document that any future validation-carried ConsensusExtensions data must be covered by the signed validation payload and duplicate/replay identity, not an unsigned wrapper field.
2026-04-27 15:45:27 +07:00
Nicholas Dudfield a61f334ca2 docs(consensus): capture extension design principles
Document the consensus-extension invariants for RNG, sidecars, export sig convergence, validator quorum, zero-entropy fallback, and proposal signing. Link the note from the RCL consensus README so future changes have a durable checklist.
2026-04-27 15:33:15 +07:00
Nicholas Dudfield 53a119ce30 fix(consensus): require rng entropy quorum alignment
Count the local proposer when deciding whether the previous round had enough participants for RNG, since prevProposers only tracks peers. This avoids a 4/5 honest quorum being treated as below quorum after one validator diverges.

Allow an already quorum-aligned entropySetHash to proceed despite below-quorum conflicting hashes, while retaining zero-entropy fallback when no entropy hash reaches quorum alignment. Add CSF coverage for a persistent single bogus entropy hash and for conflicting bogus hashes without quorum.
2026-04-27 15:29:36 +07:00
Nicholas Dudfield 63d1197345 fix(consensus): zero rng on unresolved entropy hash conflict 2026-04-27 15:10:39 +07:00
Nicholas Dudfield aafd5b940b test(consensus): avoid brittle rng lcl quorum check 2026-04-27 14:47:18 +07:00
Nicholas Dudfield efc497cf23 chore(levelization): refresh app overlay loop summary
This does not introduce a new levelization cycle; the existing xrpld.app <-> xrpld.overlay loop now has equal aggregate include counts after the consensus-extension work. Treat this as essentially the same architectural situation, not a meaningful worsening by itself.

TODO: if we want to fix the boundary properly, extract a small shared consensus-extension wire/interface layer below both app and overlay instead of shaving includes to change the generated ratio.
2026-04-27 14:01:54 +07:00
Nicholas Dudfield f4e78c9a24 fix(consensus): apply negative unl to sidecar validator view 2026-04-27 12:50:43 +07:00
Nicholas Dudfield 7b5865c69c fix(consensus): sign export proposal attachments 2026-04-27 11:57:29 +07:00
Nicholas Dudfield 9f1ad521e1 fix(consensus): use active validator snapshots for sidecars 2026-04-27 10:59:33 +07:00
Nicholas Dudfield 26bbef8efd fix(consensus): harden sidecar quorum inputs 2026-04-27 10:14:12 +07:00
Nicholas Dudfield 6e71f84867 refactor: add typed sidecar SHAMap sync 2026-04-27 09:58:34 +07:00
Nicholas Dudfield ab9b48f67a Merge remote-tracking branch 'origin/dev' into feature-export-rng
# Conflicts:
#	.github/workflows/levelization.yml
#	Builds/levelization/README.md
#	Builds/levelization/levelization.py
#	Builds/levelization/levelization.sh
#	cmake/RippledCore.cmake
2026-04-27 09:14:59 +07:00
Alloy Networks cd00ed72d8 change build instructions url 2026-04-24 11:12:28 +10:00
tequ 05a3e04f2d Fix BEAST_ENHANCED_LOGGING not working and restore original behavior 2026-04-24 11:11:40 +10:00
tequ 66f7294120 Test: hint build_test_hooks.sh when hook wasm is empty in hso() 2026-04-24 11:10:46 +10:00
Nicholas Dudfield 7f6ac75617 Revert "chore: use improved levelization script with threading and argparse"
This reverts commit 5c1d7d9ae9.
2026-04-24 11:09:19 +10:00
Nicholas Dudfield 4150f0383c chore: use improved levelization script with threading and argparse 2026-04-24 11:09:19 +10:00
Nicholas Dudfield 25123b370a chore: replace levelization shell script with python
Backport of XRPLF/rippled#6325. The python version runs ~80x faster.
2026-04-24 11:09:19 +10:00
tequ f90ed41802 enable ccache direct_mode 2026-04-24 11:06:51 +10:00
tequ 8c4c158d3a output ccache configuration in release-builder 2026-04-24 11:06:51 +10:00
tequ 2d2951875d fix: typo SignersListSet 2026-04-24 11:05:20 +10:00
tequ 9bfca63574 Update util_keylet fee test 2026-04-24 11:00:31 +10:00
tequ 1ba444ae7f Updated tests to align with the changes merged into the dev branch. 2026-04-24 11:00:31 +10:00
tequ f96d9b6e51 Add tests for Hooks fee 2026-04-24 11:00:31 +10:00
Nicholas Dudfield 04077c1a55 test(testnet): assert zero entropy in degraded ledgers 2026-04-10 12:04:46 +07:00
Nicholas Dudfield d94079d762 test(rng): relax PartialReveals sync assertion 2026-04-10 11:18:52 +07:00
Nicholas Dudfield 92ec07a1be chore: regenerate hook/sfcodes.h + format fix
Regenerate sfcodes.h to include the new sfSidecarType field
(UINT8, code 20).  Also apply clang-format to ConsensusExtensions.cpp.
2026-04-10 10:36:50 +07:00
Nicholas Dudfield 664db62588 fix: sidecar kind lost on cache hit + harden export sig parse
1. Record SidecarKind in pendingRngFetches_ before calling
   onAcquiredSidecarSet on local-cache-hit path. Without this,
   cached reveal/exportSig sets silently fell back to commit kind
   and were rejected by the sfSidecarType check.

2. Wrap export sig visitLeaves callback in try/catch (matching the
   RNG path) and enforce sfSidecarType == sidecarExportSig before
   processing — closes the shape-only acceptance gap.
2026-04-10 10:22:58 +07:00
Nicholas Dudfield 03a436d918 refactor: convert sidecar SHAMap entries from STTx to STObject
Replace STTx-based sidecar entries with plain STObject(sfGeneric)
using sfSidecarType (UINT8) discriminator. Eliminates unnecessary
transaction envelope overhead (sfSequence, sfFee, sfFlags) and
content-sniffing heuristics from the parse path.

Build: STObject with sidecarRngCommit/sidecarRngReveal/sidecarExportSig
Parse: sfSidecarType dispatch + typed field accessors
2026-04-10 10:14:06 +07:00
Nicholas Dudfield 7474048295 refactor: typed sidecar dispatch — eliminate content-sniffing heuristic
Replace the content-sniffing heuristic in onAcquiredSidecarSet with
typed dispatch based on SidecarKind.

The type is already known at fetch time:
- commitSetHash → SidecarKind::commit
- entropySetHash → SidecarKind::reveal
- exportSigSetHash → SidecarKind::exportSig

pendingRngFetches_ changes from hash_set<uint256> to
hash_map<uint256, SidecarKind>.  When the set arrives,
look up the kind by hash and dispatch — no leaf inspection.

This is the set-classification fix (Option E from the design doc):
no new SField, no STTx changes, no protocol additions, no RNG
proof-chain churn.
2026-04-10 09:18:43 +07:00
Nicholas Dudfield 1ee660529e fix: RPC handler sync, unused local, idiomatic Buffer comparison
- Add rng_poll_ms, no_export_sig, bootstrap_fast_start to the
  runtime_config RPC handler (SET and GET paths) so all ConfigVals
  fields are configurable live via admin RPC.
- Remove unused `added` counter in CSF fetchRngSetIfNeeded (was
  causing compiler warnings after debug logging removal).
- Use Buffer::operator== instead of std::memcmp in upgradeSignature,
  drop <cstring> include.
2026-04-10 08:56:16 +07:00
Nicholas Dudfield 311dfa1c23 chore: add TODO for RuntimeConfig activation gating
Both runtime_config and disconnect RPC handlers are already
Role::ADMIN.  Add a TODO to consider gating the entire
RuntimeConfig system on a config flag or compile-time define
for production nodes.
2026-04-10 08:31:54 +07:00
Nicholas Dudfield f27cd2c567 refactor: consolidate env vars into RuntimeConfig
Move XAHAU_RNG_POLL_MS and XAHAUD_NO_EXPORT_SIG into RuntimeConfig
as rngPollMs and noExportSig fields.  Both are now configurable via
the XAHAU_RUNTIME_CONFIG JSON blob or individual env vars, and
controllable at runtime via the runtime_config RPC.

rngPollMs is clamped to minimum 50ms (prevents tight-loop polling).
Default remains 250ms.

This removes the last loose std::getenv calls from production code
outside of RuntimeConfig.  All env-var-based configuration now flows
through a single system.
2026-04-10 08:24:20 +07:00
Nicholas Dudfield f34fdc297c fix(export): close upgradeSignature TOCTOU with buffer comparison
upgradeSignature now takes the verified buffer and compares it against
the currently stored buffer before promoting to verified.  This guards
against concurrent overlay threads overwriting the buffer between the
caller's unverifiedSignatures() snapshot and the upgrade call.

If the stored buffer was overwritten (different size or content), the
upgrade is silently skipped — the new buffer will be verified on its
next encounter.
2026-04-10 08:19:45 +07:00
Nicholas Dudfield 65fa63883d chore: remove CSF debug logging that floods CI output
Strip JLOG(j_.debug()) calls from buildEntropySet, fetchRngSetIfNeeded,
and finalizeRoundEntropy in CSF Peer.h.  These were added for local
debugging and caused CI failures due to output size limits.
2026-04-09 20:21:37 +07:00
Nicholas Dudfield d8c683fb4c test(rng): fix AlignmentRequired test to run 1 round not 3
Running 3 rounds caused peer 0 to desync on round 2, dropping
prevProposers for the majority on round 3, triggering bootstrap
skip → zero entropy on the last round.  The gate works correctly
(logs show aligned=3, peersSeen=3) but the test was checking the
LAST round's entropy, not the round where the gate was exercised.

Run 1 round after warmup — sufficient to exercise the gate.
2026-04-09 18:09:17 +07:00
Nicholas Dudfield fd53af304b fix(rng): measure entropy deadline from publish time, not reveal start
The entropy convergence deadline was measured from revealPhaseStart_,
which is set when entering ConvergingReveal.  By the time the entropy
set is published (after reveal timeout + observation tick), most of
the deadline budget was already spent — leaving insufficient time
for peer alignment.

Add entropyPublishStart_ timestamp set when the entropy set is first
published.  All convergence gate deadlines now measure from this
point, giving the full 2x rngREVEAL_TIMEOUT window for peer
proposals to propagate and alignment to be observed.
2026-04-09 18:06:18 +07:00
Nicholas Dudfield 2a3f0ec923 fix(rng): bounded wait for alignment instead of immediate fallback
When peers have published entropySetHash but none match ours yet
(e.g. a subset peer is the only one seen so far), wait for the
bounded deadline instead of immediately falling back to zero.
Other aligned peers may not have published yet — give them time.

Only fall back to zero if no alignment is observed within the
deadline (2x rngREVEAL_TIMEOUT).
2026-04-09 17:58:41 +07:00
Nicholas Dudfield 00f1f7ba30 fix(rng): subset-aware conflict detection in entropy convergence gate
After fetch/merge, if our entropy set hash didn't change, the
conflicting peer had a subset of our data — not a real threat.
Clear the conflict flag so we don't fall back to zero when a peer
simply has fewer reveals than us.

If the hash DID change (merge added data), re-count alignment
with the updated hash before treating it as a real conflict.

This prevents the majority from falling back to zero just because
one peer (e.g. isolated) has a smaller reveal set.
2026-04-09 17:53:58 +07:00
Nicholas Dudfield 49f05e4e47 fix(rng): require positive peer alignment for non-zero entropy
The observation tick alone was insufficient — a node could pass the
gate without any peer confirming its entropySetHash.  Now the gate
requires at least one tx-converged peer with a matching hash before
accepting non-zero entropy.

Three cases after the observation tick:
1. aligned > 0: peers confirm our hash → proceed with entropy
2. conflict: fetch/merge/rebuild → bounded wait → zero fallback
3. aligned=0, peersSeen=0: no peers published yet → bounded wait →
   zero fallback if still no peers at deadline
4. aligned=0, peersSeen>0: peers published but none match → zero

Also:
- CSF finalizeRoundEntropy now uses shouldZeroEntropy() (quorum check)
- Two new TDD tests:
  - testRngNoEntropyWithoutPeerAlignment: healthy network must agree
  - testRngAlignmentRequiredForNonZeroEntropy: isolated peer must not
    produce non-zero entropy that differs from majority
2026-04-09 17:51:51 +07:00
Nicholas Dudfield 1f51b9c594 fix(csf): quorum threshold in shouldZeroEntropy + test adjustments
CSF shouldZeroEntropy() now checks reveals < quorumThreshold (80% of
UNL), matching production.  MajorRevealLoss test adjusted to verify
majority group agreement rather than requiring full synchronization
(peer 0 may desync when it misses most reveals).

All 15 ConsensusRng tests now pass.
2026-04-09 17:40:05 +07:00
Nicholas Dudfield 88a548a8ef fix(rng): observation tick + CSF quorum threshold in shouldZeroEntropy
Two fixes addressing the asymmetric-view problem:

1. Convergence gate now forces one observation tick after first
   publishing the entropySet before accepting.  Previously a node
   could publish + accept in the same tick, never seeing a peer's
   different hash.  The entropySetPublished_ flag ensures at least
   one round-trip for proposal propagation.

2. CSF shouldZeroEntropy() now checks quorum threshold (80% of UNL),
   matching production behavior.  Previously it only checked empty().

Result: PartialReveals test now passes — all 6 peers converge on
the same entropy (count=6) via union merge after the observation tick.
14/15 ConsensusRng tests pass.
2026-04-09 17:31:36 +07:00
Nicholas Dudfield db302a0f78 fix(rng): add selfSeedReveal to fix CSF reveal counting
The CSF never self-seeded its own reveal into pendingReveals_ because
harvestRngData only processes peer proposals, not self.  The real code
handles this in decorateMessage, but the CSF has no equivalent.

Add selfSeedReveal() called from the tick at reveal transition.
Both the real ConsensusExtensions and the CSF Extensions implement it.
The real code now has belt-and-suspenders: tick + decorateMessage.

This fixes CSF peers having N-1 reveals instead of N, which caused
every peer to compute entropy from a different subset.
2026-04-09 17:23:53 +07:00
Nicholas Dudfield 383d9ec2e7 feat(csf): add SidecarStore for sidecar set fetch/merge simulation
Add a content-addressed SidecarStore to the CSF, simulating the
InboundTransactions SHAMap fetch pipeline.  Tagged entries (commit
or reveal) are published by hash during buildCommitSet/buildEntropySet
and fetched by hash during fetchRngSetIfNeeded, with type-aware
union merge into the correct local pending set.

Also adds debug logging to CSF Extensions for entropy pipeline
troubleshooting.
2026-04-09 17:17:53 +07:00
Nicholas Dudfield 52671bfc99 test(rng): add XAHAU_RNG_TEST env var filter for focused test runs
Set XAHAU_RNG_TEST=<substring> to run only matching test methods.
e.g. XAHAU_RNG_TEST=SingleByzantine runs only that test.
2026-04-09 16:51:26 +07:00
Nicholas Dudfield 8307fca3b9 fix(rng): add entropySetHash convergence gate before accept
Add a bounded pre-accept convergence check for entropySetHash,
closing the gap where two honest validators could accept with
different reveal subsets and compute different entropy (ledger fork).

After publishing the entropy set, the gate:
1. Inspects tx-converged peer positions for conflicting entropySetHash
2. Fetches differing sets via fetchRngSetIfNeeded (union merge)
3. Rebuilds and re-publishes the local entropy set after merge
4. Waits within a bounded window (2x rngREVEAL_TIMEOUT)
5. Falls back to zero entropy if conflict persists past deadline

This follows the same pattern as the existing commitSetHash conflict
handling and exportSigSetHash convergence gate.  Union merge ensures
monotonic set growth — honest timing skew resolves quickly, and
hostile hash spam hits the hard deadline and falls back safely.

The "one bad actor shouldn't deny entropy" optimization (supermajority
vote) is deferred to a follow-up patch per codex recommendation.
2026-04-09 16:30:02 +07:00
Nicholas Dudfield 6526621c16 test(rng): add TDD tests for entropySetHash convergence gate
Three new CSF tests that document expected behavior for the
entropySetHash convergence gate (not yet implemented):

1. testRngEntropyConvergesWithPartialReveals: two groups each drop
   one peer's reveal, creating different quorate subsets.  Must not
   fork — either converge via SHAMap merge or both fall back to zero.

2. testRngEntropyFallbackOnMajorRevealLoss: one peer drops most
   reveals (below quorum locally).  Network must still agree.

3. testRngSingleByzantineCannotDenyEntropy: one Byzantine peer
   (future: forced garbage entropySetHash) should not prevent the
   other 80% from producing valid entropy.

Also adds dropRevealFrom_ test knob to CSF Peer::Extensions for
simulating asymmetric reveal delivery.
2026-04-09 16:26:30 +07:00
Nicholas Dudfield 2a9b1c9c22 fix(export): guard against empty verified sigs + add invariant asserts
- Skip addVerifiedSignature in decorateMessage when sigBuf is empty
  (sign() threw — don't mark a failed sign as "verified")
- Add XRPL_ASSERT in addVerifiedSignature and addUnverifiedSignature
  requiring non-empty signature buffers
- Add XRPL_ASSERT in checkQuorumAndSnapshot verifying that every
  entry in the verified set exists in the signatures map with a
  non-empty buffer
2026-04-09 16:02:35 +07:00
Nicholas Dudfield 54ca21b604 fix(export): verified-only quorum, SHAMap, and transactor upgrade pass
Enforce the contract: source chain finalizes an export only when it
has a quorum of cryptographically verified multisignatures.

ExportSigCollector changes:
- signatureCount() now counts verified entries only
- checkQuorumAndSnapshot() returns verified-only snapshot
- snapshot() and snapshotWithSigs() return verified-only data
- buildExportSigSet (via snapshot) publishes verified-only entries
- unverifiedSignatures() returns sigs needing verification
- upgradeSignature() promotes unverified to verified
- addStandaloneSignature() marks as verified (no consensus to check)
- All add methods now set firstSeenSeq (fixes stale cleanup bug)

Export::doApply changes:
- Upgrade pass before quorum check: deserializes the inner tx (which
  is always available as ctx_.tx), verifies any unverified sigs via
  buildMultiSigningData + verify(), upgrades them in the collector
- Then checks quorum on verified-only count
- Assembles blob from verified-only snapshot

This means:
- Unverified sigs (relay ordering) are local cache only
- They don't count toward quorum until upgraded
- SHAMap convergence operates on verified sigs only
- Destination chain verification remains defense-in-depth
2026-04-09 15:54:41 +07:00
Nicholas Dudfield 462db6004c fix(rng): replace nonexistent leafCount() with std::distance
SHAMap has no leafCount() method — it was a local variable in
SHAMap.cpp, not a public API.  Use std::distance(begin(), end())
on the SHAMap's ForwardRange iterators instead.  Cost is O(n) but
the set is bounded by UNL size (~20-35 entries).
2026-04-09 15:42:04 +07:00
Nicholas Dudfield cfca708aae fix(rng): remove pendingReveals fallback from entropy output path
shouldZeroEntropy() and sfEntropyCount no longer fall back to
pendingReveals_.  If entropySetMap_ is null, entropy failed — the
pipeline didn't complete, and the map is the only canonical source.

pendingReveals_ is now strictly an internal staging area for the
commit/reveal pipeline.  All final entropy decisions flow through
entropySetMap_, which is the consensus-agreed set.
2026-04-09 15:40:22 +07:00
Nicholas Dudfield 5f70e5259c fix(rng): use entropySetMap for shouldZeroEntropy and sfEntropyCount
The H2 entropy fix switched the digest computation to entropySetMap_
but shouldZeroEntropy() and sfEntropyCount still used pendingReveals_.
Since pendingReveals_ can diverge from the published entropySetMap_
(late reveals mutate it after the map hash is published), two nodes
agreeing on the same entropySetHash could still build different
ttCONSENSUS_ENTROPY pseudo-transactions.

Now shouldZeroEntropy() checks entropySetMap_ leaf count when the map
is available, and sfEntropyCount uses the map's leaf count.  Both
fall back to pendingReveals_ only during pipeline stages before the
map is built.
2026-04-09 15:35:00 +07:00
Nicholas Dudfield 8697c5d821 refactor(export): explicit verified/unverified sig API in collector
Replace the ambiguous addSignature/hasSignature API with clearly
named methods that make verification state explicit:

  addVerifiedSignature()   — sig passed buildMultiSigningData + verify()
  addUnverifiedSignature() — trusted source but no multisign check yet
  addStandaloneSignature() — pubkey-only for standalone/test mode
  hasVerifiedSignature()   — only returns true for verified sigs

Unverified sigs (relay ordering fallback) are no longer treated as
verified by the cache.  When the same sig is encountered again via a
path that CAN verify (e.g. SHAMap merge after the tx arrives), the
verification runs and upgrades it to verified.

addUnverifiedSignature() won't overwrite a verified sig, preventing
downgrade.  SigEntry tracks verified validators in a separate set.
2026-04-09 15:34:13 +07:00
Nicholas Dudfield 9436e5868e fix(export): soften hard reject to best-effort verify for relay ordering
Revert the hard reject when ttEXPORT is not in the open ledger.
Under relay ordering, a node can receive a proposal with export sigs
before the ttEXPORT tx itself arrives.  Dropping these sigs loses a
valid validator contribution for the entire round with no recovery
path until terRETRY_EXPORT on the next round.

Post C1+C2, the proposal-level authentication is sufficient trust:
checkSign() verified the sender holds the private key, and sender
binding verified the embedded pubkey matches.  Store the sig and
let the multisign content be verified on the destination chain.
The collector's stale cleanup (256 ledgers) bounds retention.

When the tx IS in the open ledger (common case), the multisign sig
is still fully verified via buildMultiSigningData + verify().
2026-04-09 15:22:20 +07:00
Nicholas Dudfield c6fa973cf6 fix(rng): compute entropy from entropySetMap instead of pendingReveals
H2: Compute final entropy from the agreed-upon entropySetMap_ SHAMap
rather than from the local pendingReveals_ in-memory map.

Previously, two nodes with different reveal subsets at timeout would
compute different entropy from their local pendingReveals_ maps,
despite both passing haveConsensus() (which only checks txSetHash).
This could cause a ledger fork.

Now the entropy computation reads directly from the entropySetMap_
whose hash was published in proposals and converged via SHAMap
fetch/merge.  Nodes that agree on entropySetHash deterministically
produce the same entropy regardless of local pendingReveals_ state.

If entropySetMap_ is null (bootstrap skip, pipeline failure), the
existing shouldZeroEntropy() fallback handles it.
2026-04-09 15:18:45 +07:00
Nicholas Dudfield 939e03714c fix(export): cap exportSignatures count per proposal
Reject proposals with more than ExportLimits::maxPendingExports (8)
export sig entries.  Honest validators attach at most one sig per
pending export, bounded by the same limit.  Prevents DoS via
proposals with millions of entries triggering lock contention on
the validator list and collector mutexes.
2026-04-09 15:13:15 +07:00
Nicholas Dudfield 969f98f57e perf(export): skip redundant sig verification via collector lookup
Add hasSignature() to ExportSigCollector — checks if a verified sig
already exists for a given (txHash, validator) pair.  Both the
proposal ingestion path and the SHAMap merge path now check this
before calling verify(), avoiding redundant ed25519 verification
when the same sig arrives via multiple paths.

No external sig cache exists in rippled, so the collector itself
serves as the verification cache: once a sig is stored (always
post-verify), subsequent encounters skip the crypto work.
2026-04-09 15:03:57 +07:00
Nicholas Dudfield 435deb0e78 fix(export): close remaining sig verification gaps
Three fixes from codex review:

1. Remove unsafe fallback in proposal ingestion path: reject export
   sigs when the ttEXPORT tx is not in the open ledger instead of
   storing them unverified.  The tx must be in the open ledger for
   validators to have signed it, so this is not a legitimate case.

2. Add full sig verification to the SHAMap merge path
   (onAcquiredSidecarSet): verify each export sig entry against
   buildMultiSigningData + verify() before storing in the collector.
   Previously this path only checked trusted() on the pubkey,
   allowing a malicious UNL validator to publish a sidecar set with
   forged sigs for other validators.

3. Close cluster mode bypass: always call checkSign() and gate export
   sig harvesting on sigValid, even when cluster() is true.  Cluster
   trust is for relay/resource charging, not for accepting on-chain
   cryptographic artifacts.
2026-04-09 14:59:20 +07:00
Nicholas Dudfield b80352e512 fix(export): verify multisign signatures at ingestion time
C3: Cryptographically verify each export signature blob against the
inner transaction's signing data before storing in the collector.
Looks up the ttEXPORT tx from the open ledger, reconstructs the
signing data via buildMultiSigningData, and calls verify().

If the tx isn't in our open ledger yet (timing/relay), the sig is
stored unverified as a fallback — it can be verified later at the
SHAMap merge path or will be rejected at Export::doApply if invalid.

This runs on the jtPROPOSAL_t job queue thread (not the IO strand
or transactor), so the verify() cost has no impact on consensus
critical path performance.
2026-04-09 14:43:30 +07:00
Nicholas Dudfield 57c46c61fc fix(export): two-pass sender validation and atomic quorum+snapshot
C2 hardening: validate all export sig blobs before committing any,
preventing partial state if a later blob fails the sender binding
check. Also moves the trusted() check before the loop since senderPK
is constant.

H1: Add checkQuorumAndSnapshot() to ExportSigCollector that performs
the quorum threshold check and signature snapshot under a single lock
acquisition. Export::doApply now uses this instead of separate
signatureCount() + snapshotWithSigs() calls, eliminating the TOCTOU
window where concurrent overlay threads could mutate the collector
between the two operations.
2026-04-09 14:39:35 +07:00
Nicholas Dudfield 37ff13df50 fix(export): move sig harvesting after checkSign and bind pubkey to sender
C1: Move onTrustedPeerMessage() from the synchronous onMessage(TMProposeSet)
handler into checkPropose(), after checkSign() verifies the proposal's
cryptographic signature. Previously, export sigs were ingested before
signature verification, allowing any peer to inject forged sigs by
spoofing nodepubkey to a trusted validator's key.

C2: Add sender binding in onTrustedPeerMessage() — each export sig
blob's embedded validator pubkey must match the proposal sender's
nodepubkey. Reject the entire proposal's export sigs on any mismatch,
preventing a compromised validator from impersonating other validators
to single-handedly forge quorum.
2026-04-09 14:32:38 +07:00
Nicholas Dudfield 1b363b7eac fix: correct stale function name in ConsensusExtensionsTick comment 2026-04-09 14:03:07 +07:00
Nicholas Dudfield 9562b457cf chore: remove stale Peer-level RNG forwarders
All callsites now go through ce() consistently.
2026-03-23 09:57:53 +07:00
Nicholas Dudfield 724633ceb5 refactor(consensus): decouple CSF tests from xrpld.app via PeerTick.h
Move ConsensusExtensionsTick.h from xrpld/app/consensus/ to
xrpld/consensus/ — it's a pure template with no app-layer deps.
Extract Peer::Extensions::onTick() definition into test/csf/PeerTick.h
so Peer.h no longer includes from xrpld/app/.

Eliminates the test.csf > xrpld.app levelization edge.

Add --explain flag to levelization.py for tracing dependency edges.
2026-03-23 09:36:59 +07:00
Nicholas Dudfield 152d82e798 refactor(consensus): extract RNG/Export into ConsensusExtensions
Extract all Xahau consensus extension logic (RNG commit-reveal entropy
and Export validator multisign collection) from Consensus.h and
RCLCxAdaptor into a dedicated ConsensusExtensions class owned by
Application.

Implements all 10 lifecycle hooks from the design doc:
  onRoundStart, onTrustedPeerMessage, onTrustedPeerProposal,
  decoratePosition, decorateMessage, onTick, onPreBuild,
  onAcceptComplete, isSidecarSet, onAcquiredSidecarSet

Key design decisions:
- ConsensusTick<> template in ConsensusTypes.h keeps dependency
  direction clean (generic consensus defines contract, extensions
  implement it)
- extensionsTick<> shared template in ConsensusExtensionsTick.h
  ensures CSF test framework runs the same state machine as production
- ExportSigCollector ownership moved from global singleton to CE
- Sidecar acquisition routed through RCLConsensus mutex for thread
  safety (isExtensionSet + gotExtensionSet)
- RCLCxAdaptor reduced to thin ce() accessor + generic consensus
  interface methods

Files:
  new: ConsensusExtensions.h/.cpp, ConsensusExtensionsTick.h
  reduced: Consensus.h (-1060 lines), RCLConsensus.cpp (-1400 lines)
  updated: ConsensusTypes.h, Application.h/.cpp, NetworkOPs.cpp,
           PeerImp.cpp, Export.cpp, ExportSigCollector.h, Peer.h

7/7 testnet scenarios + 1463 consensus + 260 Export unit tests pass.
2026-03-19 20:23:19 +07:00
Nicholas Dudfield 0bb31ce7ce chore: add projected-source markers for consensus extension docs
Non-functional comment markers (//@@start, //@@end) for projected-source
documentation extraction.
2026-03-19 12:14:11 +07:00
Nicholas Dudfield 4cb3de0497 refactor(export): build xport wrapper via STObject then serialise to STTx
Replace the duplicated throwaway-STTx + real-STTx pattern with a
single STObject: set all fields including fee=0, serialise to compute
the fee, patch the fee, then serialise once into the final STTx.

20 lines shorter, no duplication.
2026-03-18 17:36:30 +07:00
Nicholas Dudfield c6b315412d test(export): harden scenario tests with proper assertions
Replace warnings and logs with hard assertions across all export
scenario tests:

- export_helpers: add assert_hook_accepted(), assert_export_result(),
  assert_shadow_ticket() shared assertion helpers
- steady_state_export: assert hook ACCEPT + emitCount, ExportResult
  contents (signers, inner tx fields), shadow ticket exists
- retriable_export: assert ExportResult well-formed, shadow ticket
  created, payment not blocked
- export_degradation: assert export FAILS (not just log), no shadow
  ticket, payment still works
- export_unanimity: assert ExportResult + shadow ticket on success,
  absence on failure
2026-03-18 17:18:12 +07:00
Nicholas Dudfield 72395bec75 chore: clang-format 2026-03-18 17:12:41 +07:00
Nicholas Dudfield 8ed4d86f0f test(export): verify emitted ttEXPORT lifecycle end-to-end
testXportPayment now asserts the full emitted tx lifecycle:
- hook fires with ACCEPT, emitCount=1, returnCode=0
- sfHookEmissions present with 1 entry
- ltEMITTED_TXN in AffectedNodes
- emitted dir is not empty
- after close, emitted ttEXPORT appears in closed ledger

Also add FOCUSED_TEST env var gate for fast iteration during
development (set FOCUSED_TEST=1 to run only focused_test()).
2026-03-18 17:11:19 +07:00
Nicholas Dudfield 419fd16b9a chore: move export scenarios to export-suite.yml, add SuiteLogsWithOverrides
Move export scenario tests from suite.yml into their own
export-suite.yml file. The defaults already set CE+Export features
so individual test entries no longer need to repeat them.

Add SuiteLogsWithOverrides test utility: a Logs subclass that routes
specified journal partitions to stderr (always visible) while keeping
others on suite_.log (only on failure). Useful for debugging specific
subsystems during test development.
2026-03-18 17:09:47 +07:00
Nicholas Dudfield a8097cd9a6 fix(export): compute emit fee before STTx construction
Mutating the fee via const_cast after STTx construction left a stale
cached getTransactionID(). When the emitted ttEXPORT was serialised
into the emitted directory and later deserialised, the round-tripped
txid differed from the original, causing tefNONDIR_EMIT in
Transactor::preclaim (the emitted dir entry was keyed with the stale
hash).

Build a throwaway STTx with fee=0 to calculate the fee size, then
construct the real STTx with the correct fee from the start.
2026-03-18 17:04:46 +07:00
Nicholas Dudfield 02a0552325 docs(export): clarify LLS semantics for retriable exports
Add comment explaining the three-state outcome table for exports
relative to LastLedgerSequence:
  ledger < LLS:  tesSUCCESS or terRETRY_EXPORT
  ledger == LLS: tesSUCCESS or tecEXPORT_EXPIRED
  ledger > LLS:  tefMAX_LEDGER (never reaches doApply)
2026-03-18 14:43:50 +07:00
Nicholas Dudfield 3698193b0a chore: clang-format 2026-03-18 14:21:00 +07:00
Nicholas Dudfield de43ca2385 refactor(export): store multisigned tx as sfExportedTxn object in metadata
Use sfExportedTxn (OBJECT) instead of sfBlob (VL) for the multisigned
transaction in sfExportResult metadata. This renders as readable JSON
with all fields visible (Account, Signers, etc.) instead of an opaque
hex blob.

Also compute the tx hash directly via getHash(HashPrefix::transactionID)
on the STObject instead of serializing/deserializing through STTx.
2026-03-18 14:16:25 +07:00
Nicholas Dudfield 8c747a1916 feat(export): produce multisigned blob in export metadata
Export::doApply now builds the fully multisigned inner tx and stores
it as sfBlob in sfExportResult metadata. In standalone mode, the node
signs directly with its own validator keys (no consensus needed).

Key changes:
- ExportSigCollector stores actual multisign signatures, not just pubkeys
- RCLConsensus proposal attachment computes real multisign sigs over inner tx
- PeerImp harvests variable-length sig entries from proposals
- Export::doApply assembles Signers array, builds multisigned blob first,
  then uses its hash for the shadow ticket (getTransactionID includes all
  fields including Signers)
- Import skips OperationLimit and signing key checks for export callback
  path (sfTicketSequence present) — shadow ticket proves the relationship
- Full Export→XRPL→Import round-trip test: export on Xahau, submit
  multisigned blob to XRPL (with matching SignerList), build XPOP,
  import back, verify shadow ticket consumed
2026-03-18 13:54:29 +07:00
Nicholas Dudfield cea110f29a feat: add XPOP test helper and XPOP_test suite
- src/test/jtx/xpop.h: test utilities for building XPOPs from Env ledgers
  (TestValidator, TestVLPublisher, TestXPOPContext, buildTestXPOP)
- src/test/app/XPOP_test.cpp: 4 tests (173 assertions)
  - LedgerProof construction from payment tx
  - XPOP v1 JSON structure verification
  - Merkle proof verification for multi-tx ledgers
  - Full Import round-trip: source Env payment → XPOP → dest Env Import → tesSUCCESS
2026-03-18 11:59:34 +07:00
Nicholas Dudfield 3ca056a94b feat: add XPOP test helper and XPOP_test suite
- src/test/jtx/xpop.h: test utilities for building XPOPs from Env ledgers
  (TestValidator, TestVLPublisher, buildTestXPOP)
- src/test/app/XPOP_test.cpp: 3 tests (133 assertions)
  - LedgerProof construction from payment tx
  - XPOP v1 JSON structure verification
  - Merkle proof verification for multi-tx ledgers
2026-03-18 11:41:16 +07:00
Nicholas Dudfield 705d8400db feat: add proof module for XPOP construction
New module at src/xrpld/app/proof/ with layered design:

- ProofBuilder: SHAMap merkle proof extraction (extractProofV1)
  Binary trie proof with 16-way branching, root hash verification.
- LedgerProof: proof-of-ledger (header fields + tx blob/meta + merkle proof)
  buildLedgerProof() extracts everything from a closed Ledger.
- XPOPv1: JSON format builder matching Import.cpp expectations
  buildXPOPv1() creates complete XPOP with validation signatures.

Designed for versioning: v1 JSON (current Import compat), future v2
binary proofs and account state proofs layer on the same core.
2026-03-18 11:29:29 +07:00
Nicholas Dudfield 655b751698 chore: regenerate hook/sfcodes.h for new sfields
Adds sfCancelTicketSequence (UINT32 101) and sfExportResult (OBJECT 98).
2026-03-18 11:06:04 +07:00
Nicholas Dudfield f324081277 fix(import): verify XPOP tx hash matches shadow ticket
Shadow tickets store the exported transaction hash. Import now verifies
the XPOP's inner tx hash matches, preventing use of a different XPOP
with the same TicketSequence.
2026-03-18 10:52:19 +07:00
Nicholas Dudfield 24a284180a fix(export): address review findings from code audit
- Validate incoming export sig pubkeys against trusted UNL (PeerImp)
- Fix handleAcquiredRngSet misclassifying export sets when local map is null
- Add stale-entry cleanup (cleanupStale with 256-ledger timeout)
- Gate sig attachment on featureExport amendment (RCLConsensus)
- Fail with tefINTERNAL if ExportResult metadata can't be written
- Make export and cancel mutually exclusive (reject both in preflight)
- Remove dead ExportLimits.h include
2026-03-18 10:30:41 +07:00
Nicholas Dudfield 6f003cc983 feat(export): add SHAMap-based export sig convergence
Add deterministic export sig set convergence using CE infrastructure:
- exportSigSetHash in ExtendedPosition (flag 0x10)
- buildExportSigSet() builds SHAMap from collected sigs
- hasPendingExportSigs() gates convergence in phaseEstablish
- Parallel convergence gate alongside RNG sub-states
- handleAcquiredRngSet extended to merge export sig sets
- Tiered quorum: 80% with CE, 100% unanimity without CE

Scenario tests for all 3 feature combos:
- CE+Export, 1 node down: 4/5=80% → tesSUCCESS
- Export only, all up: 5/5=100% → tesSUCCESS
- Export only, 1 node down: 4/5≠100% → tecEXPORT_EXPIRED
2026-03-18 10:17:28 +07:00
Nicholas Dudfield 3a58020388 fix(export): tiered quorum threshold based on CE availability
Without CE: require unanimity (100% UNL) to avoid non-deterministic
quorum disagreement. With CE: use standard 80% quorum via
calculateQuorumThreshold (SHAMap convergence will ensure agreement).
Standalone/unit tests: require 1 sig.
2026-03-18 09:46:36 +07:00
Nicholas Dudfield 829441b52e fix(export): deduplicate export sigs across proposals within a round 2026-03-18 09:32:27 +07:00
Nicholas Dudfield 3a055663cc chore: add export-sig-attachment marker for projected-source 2026-03-18 09:26:33 +07:00
Nicholas Dudfield 985a194bdc feat(export): migrate to retriable ttEXPORT with proposal-based sigs
Replace the old ltEXPORTED_TXN + ttEXPORT_FINALIZE (validation-based
sigs, TxQ injection) approach with a retriable ttEXPORT that collects
validator signatures via TMProposeSet during consensus.

Added:
- terRETRY_EXPORT: keeps tx in retry set across ledger boundaries
- tecEXPORT_EXPIRED (200): LLS expiry frees sequence cleanly
- sfExportResult (OBJECT 98): signed export result in tx metadata
- ExportSigCollector: minimal thread-safe sig tracker
- Proposal sig attachment (RCLConsensus) + harvesting (PeerImp)
- exportSignatures field in TMProposeSet (ripple.proto)
- Metadata plumbing (TxMeta, ApplyViewImpl, ApplyStateTable)
- Hook xport() now emits ttEXPORT via normal emitted txn path

Removed:
- ttEXPORT_FINALIZE (type 90) pseudo-tx and Change::applyExportFinalize
- ltEXPORTED_TXN ledger entry and exportedDir/exportedTxn keylets
- ExportSignatureCollector (replaced by ExportSigCollector)
- TxQ export injection (quorum check + rawTxInsert)
- Validation-based export signing in RCLConsensus
- Application::getExportSignatureCollector

Verified on 5-node testnet: golden path (same-ledger finalization with
ExportResult in metadata), degraded path (tecEXPORT_EXPIRED on sub-quorum),
and hook xport() path (emitted ttEXPORT with shadow ticket creation).
2026-03-18 09:23:52 +07:00
Nicholas Dudfield 869f366d8a feat(export): add sfCancelTicketSequence for shadow ticket cancellation
Add sfCancelTicketSequence (UINT32 field 101) to ttEXPORT, allowing
users to cancel shadow tickets via a transaction. Both sfExportedTxn
and sfCancelTicketSequence are optional — at least one must be present.
This allows export, cancel, or both in a single transaction.

Test: create shadow ticket via export, cancel via sfCancelTicketSequence,
verify ticket is gone and owner reserve is freed.
2026-03-17 14:13:57 +07:00
Nicholas Dudfield 03936aa928 fix(export): require TicketSequence on exported transactions
Exported transactions must use TicketSequence (with Sequence=0)
because a bounced tx on the destination chain would jam sequential
sequence numbers. This is enforced in both the hook xport() API
and the Export transactor via ExportLedgerOps::validateTicketSequence().

Adds test: ttEXPORT rejects export without TicketSequence.
Updates existing test hooks to include TicketSequence in exported txns.
2026-03-17 12:30:55 +07:00
Nicholas Dudfield 6d180307ad feat(export): split Import into B2M and export callback paths
When the inner XPOP transaction has sfTicketSequence, Import now
takes the export callback path: consume the shadow ticket via
ExportLedgerOps::cancelShadowTicket() and return. No B2M balance
crediting, no account creation. Hooks fire normally and can inspect
the result via xpop_slot().

The B2M path is unchanged for non-ticket imports.

Also migrates the shadow ticket check in preclaim from the old
hookState namespace approach to keylet::shadowTicket().

Removes the unused shadowTicketNamespace constant.
2026-03-17 12:23:27 +07:00
Nicholas Dudfield f2ca499c97 feat(export): add ltSHADOW_TICKET and xport_cancel hook API
Introduce shadow tickets for export replay protection:

- ltSHADOW_TICKET ledger entry: account-owned, keyed by
  account + ticket sequence. Fields: sfAccount, sfTicketSequence,
  sfTransactionHash, sfLedgerSequence, sfOwnerNode.

- ExportLedgerOps::createShadowTicket(): creates shadow ticket
  when exported tx has sfTicketSequence. Charges owner reserve.
  Called from both hook xport() path and Export transactor.

- ExportLedgerOps::cancelShadowTicket(): deletes shadow ticket,
  frees reserve. Used by xport_cancel hook API.

- xport_cancel(ticket_seq) hook API: allows hooks to cancel
  shadow tickets for exports that will never get a callback.

- InvariantCheck: add ltSHADOW_TICKET to valid entry types.

- Test: verify shadow ticket creation with correct fields and
  owner count bump via ttEXPORT with TicketSequence.
2026-03-17 12:13:41 +07:00
Nicholas Dudfield bd68364f25 feat(export): add ttEXPORT user transaction and extract ExportLedgerOps
Rename the existing ttEXPORT pseudo-tx to ttEXPORT_FINALIZE (type 90)
to make room for a user-submittable ttEXPORT (type 91).

ttEXPORT allows non-hook users to submit export transactions directly,
creating the same ltEXPORTED_TXN entries that the hook xport() API
creates inline.

Extract shared logic into ExportLedgerOps.h:
- createExportedTxn(): creates ltEXPORTED_TXN, enforces directory cap
- validateNetworkID(): self-target and unconfigured guards
- validateExportAccount(): account ownership check

Both the hook API (HookAPI.cpp) and the Export transactor now call
into ExportLedgerOps, eliminating duplicated validation and ledger
mutation code.
2026-03-17 11:43:45 +07:00
Nicholas Dudfield 42a6407815 fix(export): reject exports when NETWORK_ID is unconfigured
If the node's NETWORK_ID is 0 (default/unconfigured) and the exported
transaction has no sfNetworkID field, we can't distinguish self-targeting
from legitimate cross-chain export. Reject to be safe.

Also adds exportTestConfig() helper and test for the unconfigured case.
2026-03-17 07:41:36 +07:00
Nicholas Dudfield a387c853ab test(export): add NetworkID self-target guard test
Verify that xport() rejects exported transactions whose sfNetworkID
matches the local network. The hook builds a Payment with
NetworkID=21337 (matching the test env), and the guard correctly
returns EXPORT_FAILURE causing tecHOOK_REJECTED.

Also fix log level for the guard rejection to warn (not trace).
2026-03-17 07:31:35 +07:00
Nicholas Dudfield 9311e567d3 fix(export): reject exports targeting the local network
Explicitly forbid exported transactions whose sfNetworkID matches the
local network's ID. An exported txn re-executing on its origin chain
could cause exploits or logic issues.

The check is intentionally non-mandatory: XRPL mainnet (the primary
export target) doesn't use NetworkID, so absent = allowed.
2026-03-16 17:30:14 +07:00
Nicholas Dudfield c26582bdf9 fix(export): move ExportLimits.h to xrpl/protocol
Both xrpld.overlay and xrpl.hook depend on xrpl.protocol, so placing
the header there avoids introducing a new xrpld.overlay > xrpl.hook
levelization dependency.
2026-03-16 15:58:58 +07:00
Nicholas Dudfield 417b999c7f chore(levelization): add xrpld.overlay > xrpl.hook dependency
New include of ExportLimits.h in PeerImp.cpp introduces this
module dependency (from feat(export) commit 89274b538).
2026-03-16 15:47:45 +07:00
Nicholas Dudfield 0205be4500 chore: add testnet scenario scripts
Entropy and export scenario scripts for local testnet validation.
2026-03-16 15:17:32 +07:00
Nicholas Dudfield 89274b5387 feat(export): wip export system limits
- max_export per hook: 4 → 2
- maxPendingExports: cap exported directory at 8 entries (tecDIR_FULL)
- clamp inbound signature processing in PeerImp to directory cap

The directory cap is the root DoS constraint: each pending export
requires every validator to sign and broadcast every round. Inbound
processing and signing throughput are transitively bounded by it.
2026-03-16 13:59:06 +07:00
Nicholas Dudfield b65d9faf12 docs(consensus): add MERGE NOTE comments for upstream 86ef16dbeb resolution
Extends merge guidance to cover the empty-disputes bugfix (not yet in
sync-2.5.0): !disputes.empty() guard, stalled() j/clog params,
"should be rare" doc wording, debug→warn promotion, and auto-merged
testDisputes duplicate warning.
2026-03-11 10:45:04 +07:00
Nicholas Dudfield aa1a7e5320 docs(consensus): add MERGE NOTE comments for sync-2.5.0 resolution
Inline comments at all 6 conflict points guiding the maintainer
through the expected merge conflicts when sync-2.5.0 lands:
ledgerMAX_CONSENSUS const, bootstrap params, calculateQuorumThreshold,
effectiveParms+stalled in haveConsensus, DisputedTx::stalled() j/clog
params, and testDisputes placement.
2026-03-11 10:09:02 +07:00
Nicholas Dudfield 6f0f17aad9 fix(consensus): cherry-pick upstream 86ef16dbeb empty-disputes stall fix
Cherry-pick of ripple/rippled@86ef16dbeb ("Fix: Don't flag consensus
as stalled prematurely (#5627)"). Not yet in any xahau sync branch.

Fixes false stall detection when there are no disputed transactions:
std::ranges::all_of on an empty set is vacuously true, so consensus
was incorrectly flagged as stalled. Adds !result_->disputes.empty()
guard.

Also adds diagnostic logging to DisputedTx::stalled() and the
stall detection path in haveConsensus(), and promotes the
"Need validated ledger" log from debug to warn.
2026-03-11 09:47:11 +07:00
Nicholas Dudfield 407bfa1467 feat(consensus): cherry-pick dd085e5d8 (upstream d22a5057b9) anti-stall mechanisms
Cherry-pick of ripple/rippled@d22a5057b9 / xahau dd085e5d8 ("Prevent
consensus from getting stuck in the establish phase (#5277)"), resolved
against our RNG pipeline and bootstrap fast-start changes.

Upstream adds three layered anti-stall mechanisms:
- Stateful per-dispute avalanche state machine (init→mid→late→stuck)
- Stall detection: declares consensus when all disputes individually settled
- Hard expiration: clamp(10× prev round, 15s, 120s) wall-clock safety net

Conflict resolution:
- ConsensusParms.h: kept both avalanche state machine (const members,
  avMIN_ROUNDS, avSTALLED_ROUNDS, getNeededWeight) and our bootstrap
  params (bootstrapRoundTimeSeed, bootstrapStableRoundsRequired).
  ledgerMAX_CONSENSUS left non-const for bootstrap override.
- Consensus.h: pass both stalled flag and effectiveParms to checkConsensus.
  Stall check uses original parms, bootstrap override only affects max
  consensus timeout.
- Consensus_test.cpp: kept all 12 RNG tests and new testDisputes test.
2026-03-11 09:36:38 +07:00
Nicholas Dudfield f0dfcf6b81 fix(consensus): cap bootstrap ledgerMAX_CONSENSUS at 5s
Use an explicit 5s cap instead of dividing the default 15s.
5s is the sweet spot: long enough for peers to exchange proposals
and converge naturally, short enough to avoid wasted time.
Shorter values (e.g. 3.75s) cause nodes to hit reachedMax before
peers converge, cascading into slower subsequent rounds.
2026-03-10 14:30:20 +07:00
Nicholas Dudfield 503d2ebf98 feat(consensus): add XAHAUD_BOOTSTRAP_FAST_START for faster cold-start
Seed prevRoundTime_ to 3s instead of 15s on first round, override
idle interval to bypass closeTimeResolution (10-30s on early ledgers),
and halve ledgerMAX_CONSENSUS during bootstrap. Auto-disables after 3
consecutive rounds with UNL quorum participation.

Cuts 5-node testnet cold-start from ~28s to ~13s.

Also adds projected-source markers to TxQ, NetworkOPs, and Submit for
the transaction-submission documentation template.
2026-03-10 12:52:56 +07:00
Nicholas Dudfield e52bc51384 refactor(consensus): extract shouldZeroEntropy() for quorum-gated entropy
Consolidate the repeated entropy fallback condition
(entropyFailed || no reveals || sub-quorum reveals) into a single
method. Fixes EntropyCount field reporting non-zero when the digest
was correctly zeroed due to sub-quorum reveals.
2026-03-10 08:42:10 +07:00
Nicholas Dudfield 91860db578 fix(consensus): require quorum-many reveals for non-zero entropy
Sub-quorum reveals (e.g. 3/4 threshold) were producing real entropy,
allowing a minority of validators to disproportionately influence the
output. Both injectEntropyPseudoTx and buildExplicitFinalProposalTxSet
now fall back to zero entropy when reveals < quorumThreshold().
2026-03-09 17:13:02 +07:00
Nicholas Dudfield 0b317a8e7a fix(consensus): skip rng pipeline during bootstrap convergence
When prevProposers < quorumThreshold, the network is still converging
and RNG can only produce zero entropy. Skip the commit/reveal pipeline
to avoid PIPELINE_TIMEOUT and conflict-wait delays that compound across
staggered startup rounds.
2026-03-09 16:27:36 +07:00
Nicholas Dudfield dbd230b695 feat(rpc): add rng state to consensus_info response 2026-03-09 16:05:42 +07:00
Nicholas Dudfield 30cefcba85 chore: clang-format alignment fixes 2026-03-06 18:39:37 +07:00
Nicholas Dudfield 94edb5759d fix(export): gate pre-quorum on verified signature count
hasQuorum() and getExportsWithQuorum() were using raw signerMap.size()
which includes unverified signatures. TxQ could inject a ttEXPORT
pseudo-tx that then fails the stricter verified-signature check in
Change::applyExport(). Use verifiedSignatureCount() instead so TxQ
only injects when cryptographically verified quorum is actually met.

Also add cmake plumbing for enhanced logging: link date::date-tz when
available and enable BEAST_ENHANCED_LOGGING for Debug builds.
2026-03-06 18:38:54 +07:00
Nicholas Dudfield ce57b6a3a0 fix(consensus): fix rng quorum to active UNL and demote rng log noise
Quorum fix:
- Rename expectedProposers_ → likelyParticipants_ to clarify role
- Fix commit quorum to 80% of active UNL snapshot (not shrinkable by
  recent proposer count, which was allowing 2/3 to pass as quorum)
- hasQuorumOfCommits() now uses simple threshold check only
- Add CSF test: persistent loss does not shrink quorum

Log level cleanup:
- Demote ~30 RNG/STALLDIAG per-peer/per-tick lines from info/debug to
  debug/trace across Consensus.h and RCLConsensus.cpp
- Principle: per-peer/per-tick → trace; state transitions → debug;
  milestones → info
- Reduces testnet log volume by ~93%
2026-03-06 18:36:43 +07:00
Nicholas Dudfield fca5cad470 fix(log): catch tzdb exception in local-time formatter
date::current_zone() can throw if the timezone database is unavailable
or misconfigured (e.g. minimal container images). Fall back to UTC
formatting so enhanced logging does not make startup fatal.
2026-03-06 18:36:22 +07:00
Nicholas Dudfield bb77c2090b consensus: gate RNG substates by amendment state 2026-03-06 14:09:06 +07:00
Nicholas Dudfield 90a94294e4 protocol: split export and consensus entropy amendments 2026-03-06 14:08:15 +07:00
Nicholas Dudfield c2209b4472 docs(consensus): explain why seq=3 may mirror seq=2
Clarify inline that seq=3 publish can carry unchanged txSetHash while still providing extra entropySetHash delivery/fetch opportunity under packet loss or reordering.
2026-03-03 17:41:55 +07:00
Nicholas Dudfield 8fcb2ed336 docs(consensus): annotate implicit entropy injection rationale
Document why synthetic entropy pseudo-tx is canonically injected at onAccept/buildLCL and why explicit-final proposal remains experimental/default-off.
2026-03-03 17:31:04 +07:00
Nicholas Dudfield fd1567d1ba consensus: document explicit-final tradeoffs and tighten rng diagnostics
Keep explicit final proposal as an opt-in experimental path with implicit mode as default.

Add inline rationale/TBD notes, extend stall diagnostics, and cover runtime-config + CSF txn-path behavior with tests.
2026-03-03 17:08:38 +07:00
Nicholas Dudfield d32f34d3bf build(levelization): add fast python generator with CI parity check
Add Builds/levelization/levelization.py for fast local iteration and semantic comparison against canonical shell output via --compare-to.

Keep Builds/levelization/levelization.sh as canonical path, and update levelization workflow to fail if python output diverges from shell-generated results.

Also harden interactive-shell detection in levelization.sh for portability and document local usage in README.
2026-03-03 10:17:46 +07:00
Nicholas Dudfield c491c5c82f refactor(consensus): reduce header fanout for faster iteration
Decouple RCLConsensus.h from Consensus.h by forward-declaring Consensus and storing Consensus<Adaptor> behind std::unique_ptr, moving thin wrappers out-of-line into RCLConsensus.cpp.

Also remove direct RCLConsensus.h include from NetworkOPs.h (forward declare), and add explicit includes in DatagramMonitor.h and ServerDefinitions.cpp to replace transitive dependencies.

Keep RNG fast-path behavior unchanged in Consensus.h; build and ripple.consensus.Consensus remain green.
2026-03-03 09:49:59 +07:00
Nicholas Dudfield 74817765ae consensus: restore full entropySet broadcast and document fanout tradeoffs 2026-03-03 08:32:09 +07:00
Nicholas Dudfield fc23fa8535 consensus: reduce entropy-set proposal fanout
Keep entropy-set recovery path but elect a deterministic single broadcaster (lowest NodeID among tx-converged participants) instead of every proposer broadcasting entropySetHash.

This lowers steady-state proposal chatter while preserving liveness for lagging peers that need entropy-set fetch/merge.
2026-03-03 07:42:27 +07:00
Nicholas Dudfield 34c0f17b6b runtimeconfig: add rng_claim_drop_pct testing control
Expose rng_claim_drop_pct in runtime config (RPC + env) as a clamped 0-100 percentage used by RNG claim-drop testing.

Include RuntimeConfig RPC tests for round-trip and clamping behavior.
2026-03-03 07:20:32 +07:00
Nicholas Dudfield 765ad6a278 consensus: harden RNG set convergence under dropped claims
Track active RNG round sequence for fetched set validation so lagging observers can merge current-round commit sets instead of rejecting them as closed+1 out-of-round.

Refresh/re-publish commitSetHash after fetch-merge conflicts and publish entropySetHash in ConvergingReveal so peers can recover reveal sets.

Add inline tradeoff notes: extra proposal traffic is accepted to preserve consensus safety/liveness under packet loss or drop injection.
2026-03-03 07:14:46 +07:00
Nicholas Dudfield f623ca89b9 chore(levelization): update loops result after format/merge 2026-03-02 17:01:47 +07:00
Nicholas Dudfield e4865f09f9 Merge remote-tracking branch 'origin/dev' into feature-export-rng 2026-03-02 16:59:57 +07:00
Nicholas Dudfield 4c182e4738 consensus: guard commit-set conflicts and extend RNG CSF coverage 2026-03-02 16:59:41 +07:00
Nicholas Dudfield d0c869c8a6 fix(consensus): tighten RNG acquired-set validation and observer quorum
Harden acquired RNG merge paths with strict entry typing, trusted key/node binding, round-sequence gating, reveal-to-commit linkage checks, and stale reveal/proof invalidation on commitment changes.

Adjust proposer expectation logic so non-proposing observers are not counted as expected committers, and add a CSF regression test covering observer self-commit exclusion.
2026-03-02 16:36:03 +07:00
Nicholas Dudfield cac5efcd3c fix(consensus): harden acquired RNG set ingestion
Reject mixed commit/reveal maps, enforce per-entry type checks, bind node identity to trusted validator keys, and gate acquired entries to the active round.

Also verify acquired reveals against stored commitments and clear stale reveal/proof state when commitments change.
2026-03-02 16:18:55 +07:00
Nicholas Dudfield 514e60b71c fix(export): age and validate stashed tx data for signature checks 2026-03-02 15:54:53 +07:00
Nicholas Dudfield 2a34e32e05 fix(export): harden addSignature validation and verification 2026-03-02 15:46:07 +07:00
Nicholas Dudfield b969024a25 fix(export): update duplicates and prevent phantom pending entries 2026-03-02 15:39:43 +07:00
Nicholas Dudfield f30b9a4c3a fix(export): avoid stale-age poisoning from rejected signatures 2026-03-02 15:35:36 +07:00
Nicholas Dudfield 0e019fec4e fix(export): prune invalid early signatures when stashing tx data 2026-03-02 15:29:42 +07:00
Nicholas Dudfield 7e0c72fd22 fix(export): run stale signature cleanup during TxQ processing 2026-03-02 15:27:30 +07:00
Nicholas Dudfield 07d741cdd7 fix(export): harden collector duplicate and identity handling 2026-03-02 15:25:19 +07:00
Nicholas Dudfield b99c38c09d test(consensus): add asymmetric delay reveal-timeout scenario 2026-03-02 15:11:01 +07:00
Nicholas Dudfield 64e50209ff fix(consensus): invalidate stale reveals when commitment changes
Add RNG regression tests for non-UNL data, reveal-without-commit, invalid reveal, and commitment-change stale-reveal handling in CSF consensus tests.
2026-03-02 15:04:35 +07:00
Nicholas Dudfield b1ce2103ad test(csf): add RNG consensus hooks and edge-case tests 2026-03-02 14:28:34 +07:00
Nicholas Dudfield 50c4cf1df3 refactor: move xport_reserve and xport logic into HookAPI class
Move core xport_reserve and xport implementations from applyHook.cpp
DEFINE_HOOK_FUNCTION wrappers into the decoupled HookAPI class, following
the same pattern used for etxn_reserve and emit.
2026-03-02 14:10:03 +07:00
Nicholas Dudfield 6fc14f398d feat(rpc): add disconnect by ip:port [TESTNET] 2026-03-02 12:06:00 +07:00
Nicholas Dudfield 592a8600c7 fix: add missing <mutex> include for GCC compatibility 2026-02-27 16:42:10 +07:00
Nicholas Dudfield e71768700a chore: update levelization after RuntimeConfig overlay dependency 2026-02-27 16:40:00 +07:00
Nicholas Dudfield e598e405bd fix: harden RuntimeConfig validation and add startup diagnostics
- Error on unknown message_types instead of silently widening scope
- Make messageCategories optional so per-peer can override global filter
  to "all categories" (nullopt=inherit, empty set=explicitly all)
- Clamp send_drop_pct to 0-100% range
- Add STARTDIAG: logging for consensus startup diagnostics
- Add 3 test cases (11 total, 58 assertions)
2026-02-27 13:38:26 +07:00
Nicholas Dudfield 8af3ce2f5b fix: allow extended proposals in PeerImp and add message type filtering
- Fix convergence regression caused by 2.4.0 merge: replace
  stringIsUint256Sized(currenttxhash) with size() < uint256::size()
  to accept extended proposals (>32 bytes) containing RNG fields
- Add message_types filter to RuntimeConfig for targeting specific
  protocol message categories (proposal, validation, transaction, etc.)
- Add appliesTo() method and messageCategories set to ConfigVals
- Add category name mapping helpers in RPC handler
- Add 2 test cases for message type filtering (8 total)
2026-02-27 13:10:49 +07:00
Nicholas Dudfield b67cb78b97 feat: add RuntimeConfig service with overlay artificial delays
Add a generic RuntimeConfig service for runtime-configurable parameters,
initially supporting artificial send delays and packet drops for testing
consensus behavior on local testnets.

- RuntimeConfig class with atomic fast-path gate (zero cost when inactive)
- Per-peer targeting via "*" (global) and "ip:port" keys with inheritance
- Pre-merged caching at write time for single-lookup read path
- Admin RPC handler `runtime_config` (set/clear/clear_all/get)
- Env var support: XAHAU_RUNTIME_CONFIG (JSON) or XAHAU_SEND_* vars
- PeerImp::send() integration with delay timer and probabilistic drops
- RPC handler test covering all operations and merge behavior
2026-02-27 09:46:19 +07:00
tequ 8cfee6c8a3 Merge fixAMMClawbackRounding amendment into featureAMMClawback amendment 2026-02-25 19:07:45 +10:00
yinyiqian1 8673599d2b fixAMMClawbackRounding: adjust last holder's LPToken balance (#5513)
Due to rounding, the LPTokenBalance of the last LP might not match the LP's trustline balance. This was fixed for `AMMWithdraw` in `fixAMMv1_1` by adjusting the LPTokenBalance to be the same as the trustline balance. Since `AMMClawback` is also performing a withdrawal, we need to adjust LPTokenBalance as well in `AMMClawback.`

This change includes:
1. Refactored `verifyAndAdjustLPTokenBalance` function in `AMMUtils`, which both`AMMWithdraw` and `AMMClawback` call to adjust LPTokenBalance.
2. Added the unit test `testLastHolderLPTokenBalance` to test the scenario.
3. Modify the existing unit tests for `fixAMMClawbackRounding`.
2026-02-25 19:07:45 +10:00
Nicholas Dudfield 0b1b82282e fix: reject single-signed exports and fix test hook SigningPubKey
Add single-sign rejection check in Change::applyExport() matching
rippled's multi-sign validation: SigningPubKey must be present but
empty, TxnSignature must not be present.

Fix Export_test.cpp hook to encode an empty VL blob for SigningPubKey
instead of 33 zero bytes (AI slop from export-uvtxn branch).
2026-02-25 14:55:55 +07:00
Nicholas Dudfield d4c5a7e8ab fix: update copyright headers to 2026 XRPL Labs for new files 2026-02-25 14:38:40 +07:00
Nicholas Dudfield 82837864fa fix: extract calculateQuorumThreshold() and revert Import.cpp quorum change
Extract duplicated (n * 80 + 99) / 100 ceiling quorum formula into shared
calculateQuorumThreshold() in ConsensusParms.h, matching the standard
ValidatorList::calculateQuorum(). Used by ExportSignatureCollector,
Change.cpp, and RCLConsensus.cpp.

Revert Import.cpp quorum from ceiling back to original truncating formula
(totalValidatorCount * 0.8) since Import handles XPOP imports, not the
new Export feature. Added TODO for future upgrade.
2026-02-25 14:22:43 +07:00
Nicholas Dudfield e1caee6459 fix: regenerate hook/sfcodes.h after sfHookExportCount field code change 2026-02-25 13:40:25 +07:00
Nicholas Dudfield 3206b4a4e1 fix: address @tequdev review comments (cbak, render, Change.cpp, markers)
- Remove unnecessary cbak() stubs from ConsensusEntropy test hooks and
  recompile WASM (cbak is optional per Guard.h validator)
- Restore RCLCxPeerPos::render() lost during merge (delegates to
  ConsensusProposal::render())
- Fix Change.cpp applyAmendment() fixInnerObjTemplate2 reversion:
  use STObject::makeInnerObject() and bracket assignment (fbcff932)
- Restore txq-export-quorum-check documentation marker in TxQ.cpp
2026-02-25 13:25:41 +07:00
tequ ec65e622aa Merge fixAMMv1_3 amendment into featureAMM amendment 2026-02-25 16:20:43 +10:00
Gregory Tsipenyuk 65837f49e1 fix: Add AMMv1_3 amendment (#5203)
* Add AMM bid/create/deposit/swap/withdraw/vote invariants:
  - Deposit, Withdrawal invariants: `sqrt(asset1Balance * asset2Balance) >= LPTokens`.
  - Bid: `sqrt(asset1Balance * asset2Balance) > LPTokens` and the pool balances don't change.
  - Create: `sqrt(asset1Balance * assetBalance2) == LPTokens`.
  - Swap: `asset1BalanceAfter * asset2BalanceAfter >= asset1BalanceBefore * asset2BalanceBefore`
     and `LPTokens` don't change.
  - Vote: `LPTokens` and pool balances don't change.
  - All AMM and swap transactions: amounts and tokens are greater than zero, except on withdrawal if all tokens
    are withdrawn.
* Add AMM deposit and withdraw rounding to ensure AMM invariant:
  - On deposit, tokens out are rounded downward and deposit amount is rounded upward.
  - On withdrawal, tokens in are rounded upward and withdrawal amount is rounded downward.
* Add Order Book Offer invariant to verify consumed amounts. Consumed amounts are less than the offer.
* Fix Bid validation. `AuthAccount` can't have duplicate accounts or the submitter account.
2026-02-25 16:20:43 +10:00
Nicholas Dudfield 0c2e09050e fix: move sfHookExportCount to Xahau-reserved field code range
sfHookExportCount was at field code 23, colliding with the mainline
rippled UINT16 range. Move to 98 in the Xahau-reserved range.

Also reorder sfExportedTxn (90) before sfAmountEntry (91) for
consistency.
2026-02-25 12:05:28 +07:00
Nicholas Dudfield 83922d5c20 fix: restore XRPL_ASSERT and UNREACHABLE macros reverted during merge
The merge with origin/dev accidentally reverted 19 XRPL_ASSERT() calls
back to plain assert() and 1 UNREACHABLE() back to assert(0). These
macros provide descriptive diagnostic messages on failure and are the
project convention since the rippled 2.4.0 migration.

Files fixed:
- Consensus.h: 9 XRPL_ASSERT reversions
- RCLConsensus.cpp: 5 XRPL_ASSERT reversions
- BuildLedger.cpp: 3 XRPL_ASSERT reversions
- Change.cpp: 1 UNREACHABLE + 1 XRPL_ASSERT reversion
2026-02-25 11:55:07 +07:00
Nicholas Dudfield 6bae42ff01 fix: restore CLOG consensus logging removed during merge
The merge with origin/dev accidentally stripped all CLOG diagnostic
statements from the consensus code path. This restores the clog
parameter to internal Consensus.h functions (checkLedger, phaseOpen,
closeLedger, updateOurPositions, handleWrongLedger, leaveConsensus,
createDisputes) and re-adds all 46 CLOG statements that provide
per-round diagnostic detail for phase transitions, convergence
progress, dispute tracking, and pause decisions.

Also restores the origin/dev structure of Consensus.cpp by removing
the anonymous-namespace wrapper and forwarding overloads that were
merge artifacts.
2026-02-25 11:53:27 +07:00
Nicholas Dudfield 35e86d926e fix(consensus-entropy): align pseudo tx/sle formats and hook handling
Add missing ttEXPORT/ttCONSENSUS_ENTROPY pseudo transaction fields required by runtime logic and ensure corresponding ledger entries carry threading/sequence fields.

Handle ttEXPORT and ttCONSENSUS_ENTROPY in hook stakeholder routing to avoid Unknown transaction type assertion during ledger close.
2026-02-24 18:44:00 +07:00
Nicholas Dudfield 9c4ee9315d chore: update levelization results after merge 2026-02-24 15:56:36 +07:00
Nicholas Dudfield 0f17cf02aa chore: clang-format 2026-02-24 15:51:55 +07:00
Nicholas Dudfield 7753dc3cbe fix(invariants): exempt export and entropy pseudo-ledger entries
Handle ltEXPORTED_TXN and ltCONSENSUS_ENTROPY in LedgerEntryTypesMatch so creating/destroying these pseudo-ledger entries does not trigger XRP balance invariant violations.
2026-02-24 15:48:32 +07:00
Nicholas Dudfield cc7f3c59ae merge: port export-rng onto post-2.4.0 tree restructure
Resolve the origin/dev post-2.4.0 sync conflicts across the xrpld path migration and macro-based protocol registration changes.

Re-apply export/RNG integration on top of the new structure, including consensus/build plumbing, tx/apply paths, peer ingest, and tests.

Regenerate hook headers and restore a green build via x-run-tests (Export_test build path).
2026-02-24 15:32:45 +07:00
RichardAH e5b21f026e Merge pull request #692 from Xahau/sync-2.4.0-rebased
Sync: Ripple(d) 2.4.0
2026-02-24 16:07:09 +10:00
Nicholas Dudfield e8c1b25ab4 fix: harden export signature trust model and quorum verification
- unify validator trust checks into isExportValidatorTrusted() preferring
  UNLReport with local trust fallback
- add last-line-of-defense sig verification in Change::applyExport()
  requiring 80% (ceil) verified trusted UNL signatures
- filter untrusted export signatures at ingestion in PeerImp
- fix Import quorum from floor(n*0.8) to ceil(n*80%) matching export side
2026-02-24 12:52:23 +07:00
Nicholas Dudfield b9dd854595 refactor: unify featureExport + featureConsensusEntropy into featureExportRNG
Single amendment flag for both features. numFeatures 94 → 93.
Exclude featureExportRNG from default test set to prevent
ConsensusEntropy pseudo-tx injection from breaking existing tests.
2026-02-21 17:46:46 +07:00
Nicholas Dudfield 3bead8dcb6 merge: integrate origin/export-uvtxn into consensus-phase-entropy
Resolve 14 conflicts keeping both sides. Renumber TOO_LITTLE_ENTROPY
from -46 to -48 to avoid collision with export error codes.
Fix sfHookExportCount to soeOPTIONAL in InnerObjectFormats (only set
when featureExportRNG is enabled).
2026-02-21 17:41:37 +07:00
Nicholas Dudfield 908a78a1d9 fix: regenerate hook/extern.h to match hook_api.macro ordering 2026-02-20 10:11:37 +07:00
Nicholas Dudfield a9e3dc41d4 fix: add featureExport stub for standalone guard_checker build 2026-02-20 10:05:58 +07:00
Nicholas Dudfield 02990eb4ee Merge remote-tracking branch 'origin/dev' into consensus-phase-entropy
# Conflicts:
#	hook/extern.h
#	src/ripple/app/hook/hook_api.macro
#	src/ripple/protocol/Feature.h
#	src/ripple/protocol/impl/Feature.cpp
2026-02-19 10:57:40 +07:00
Nicholas Dudfield ce76632322 Merge remote-tracking branch 'origin/dev' into export-uvtxn
# Conflicts:
#	Builds/CMake/RippledCore.cmake
#	hook/extern.h
#	src/ripple/app/hook/Guard.h
#	src/ripple/app/hook/applyHook.h
#	src/ripple/app/hook/guard_checker.cpp
#	src/ripple/app/tx/impl/Change.cpp
#	src/ripple/app/tx/impl/SetHook.cpp
#	src/ripple/protocol/Feature.h
#	src/ripple/protocol/impl/Feature.cpp
#	src/ripple/protocol/jss.h
2026-02-19 10:12:55 +07:00
Nicholas Dudfield 9eac54d690 Merge remote-tracking branch 'origin/dev' into consensus-phase-entropy
# Conflicts:
#	src/ripple/app/hook/Guard.h
#	src/ripple/app/hook/applyHook.h
#	src/ripple/app/tx/impl/SetHook.cpp
2026-02-17 10:12:46 +07:00
Nicholas Dudfield 24e4ac16ad docs(consensus): add extraction markers for remaining RNG sections
Add @@start/@@end comment markers to pseudo-tx submission filtering,
fast-polling, local testnet resource bucketing, and test environment
gating. No logic changes.
2026-02-13 12:52:14 +07:00
Nicholas Dudfield 94ce15d233 docs(consensus): add extraction markers for guided code review
Add @@start/@@end comment markers to key RNG pipeline sections for
automated documentation extraction. No logic changes.
2026-02-13 12:47:22 +07:00
Nicholas Dudfield 8f331a538e fix(consensus): harden proposal parser and guard dice(0) UB
Address findings from code review:

- dice(0): add early return with INVALID_ARGUMENT before modulo
  operation to prevent undefined behavior
- fromSerialIter: return std::optional to safely reject malformed
  payloads (truncated, unknown flag bits, trailing bytes) instead
  of throwing
- Update all callers (PeerImp, RCLConsensus, tests) for optional
- Add unit tests for dice(0) error code and 7 malformed wire cases
2026-02-12 16:18:30 +07:00
Nicholas Dudfield 7425ab0a39 fix(consensus): avoid structured bindings in lambda captures
clang-14 (CI) does not implement P2036R3 — structured bindings cannot
be captured by lambdas. Use explicit .first/.second instead.
2026-02-10 19:02:42 +07:00
Nicholas Dudfield c5292bfe0d fix(test): use large dice range to avoid deterministic collision
Standalone synthetic entropy produces identical dice(6) results for
consecutive calls due to hash collision mod 6. Switch to dice(1000000)
and add diagnostic output for return code debugging.
2026-02-10 18:53:24 +07:00
Nicholas Dudfield 79b2f9f410 feat(hooks): add consensus entropy definitions to hook headers
Add dice/random externs, TOO_LITTLE_ENTROPY error code, sfEntropyCount
field code, and ttCONSENSUS_ENTROPY transaction type to hook SDK headers.
2026-02-10 18:53:16 +07:00
Nicholas Dudfield e8358a82b1 feat(hooks): register dice/random with WasmEdge and add hook API tests
- ADD_HOOK_FUNCTION for dice/random (was defined+declared but not registered)
- Relax fairRng() seq check to allow previous ledger entropy (open ledger)
- Add hook tests: dice range, random fill, consecutive calls differ
- TODO: open-ledger entropy semantics need further thought
2026-02-10 17:58:52 +07:00
Nicholas Dudfield d850e740e1 feat(consensus): standalone synthetic entropy and ConsensusEntropy test
Generate deterministic entropy in standalone mode so Hook APIs (dice/random)
work for testing. Add test suite verifying SLE creation on ledger close.
2026-02-10 17:27:57 +07:00
Nicholas Dudfield 61a166bcb0 feat(hooks): add dice() and random() hook APIs for consensus entropy
Port the Hook API surface from the tt-rng branch, adapted to use our
commit-reveal consensus entropy (ltCONSENSUS_ENTROPY / sfDigest).

Hook APIs:
- dice(sides): returns random int [0, sides) from consensus entropy
- random(write_ptr, write_len): fills buffer with 1-512 random bytes

Internal fairRng() derives per-execution entropy by hashing: ledger
seq + tx ID + hook hash + account + chain position + execution phase
+ consensus entropy + incrementing call counter. This ensures each
call within a single hook execution returns different values.

Quality gate: fairRng returns empty (TOO_LITTLE_ENTROPY) if fewer
than 5 validators contributed, preventing weak entropy from being
consumed by hooks.

Also adds sfEntropyCount and sfLedgerSequence to the consensus
entropy SLE and pseudo-tx, enabling the freshness and quality
checks needed by the Hook API.
2026-02-10 17:12:27 +07:00
Nicholas Dudfield 41a41ec625 feat(consensus): intersect expected proposers with UNL Report and adaptive quorum
setExpectedProposers() now filters incoming proposers against the
on-chain UNL Report, preventing non-UNL nodes from inflating the
expected set and causing unnecessary timeouts.

quorumThreshold() uses expectedProposers_.size() (recent proposers ∩
UNL) when available, falling back to full UNL Report count on cold
boot. This adapts to actual network conditions rather than relying
on a potentially stale UNL Report that over-counts offline validators.

Renamed activeUNLNodeIds_/cacheActiveUNL/isActiveUNLMember to
unlReportNodeIds_/cacheUNLReport/isUNLReportMember to make the
on-chain data source explicit.
2026-02-10 16:14:47 +07:00
Nicholas Dudfield bc98c589b7 docs(consensus): fix stale quorum comment in phaseEstablish
Update inline comment to reflect that hasQuorumOfCommits() checks
expected proposers first, with 80% of active UNL as fallback.
2026-02-06 16:56:01 +07:00
Nicholas Dudfield 4f009e4698 fix(consensus): proceed with partial commitSet on timeout instead of zero entropy
When expected proposers don't all arrive before rngPIPELINE_TIMEOUT,
check if we still have quorum (80% of UNL). If so, build the commitSet
with available commits and continue to reveals. Only fall back to zero
entropy when truly below quorum.

Previously any missing expected proposer caused a full timeout with zero
entropy for that round. Now: kill 3 of 20 nodes → one 3s timeout round
per kill but entropy preserved (17/16 quorum met).
2026-02-06 16:40:33 +07:00
Nicholas Dudfield b6811a6f59 feat(consensus): deterministic commitSets via expected proposers and seq=0 proofs
Wait for commits from last round's proposers (falling back to activeUNL
on cold boot) instead of 80% of UNL. This ensures all nodes build the
commitSet at the same moment with the same entries.

Split proof storage: commitProofs_ (seq=0 only, deterministic) and
proposalProofs_ (latest with reveal, for entropySet). Previously the
proof blob contained whichever proposeSeq was last seen, causing
identical commits to produce different SHAMap hashes across nodes.

20-node testnet: all nodes now produce identical commitSet hashes.
2026-02-06 16:27:10 +07:00
Nicholas Dudfield ae88fd3d24 feat(consensus): add dedicated reveal-phase timeout measured from phase entry
Previously rngPIPELINE_TIMEOUT (3s) was measured from round start,
meaning txSet convergence could eat into the reveal budget. Now reveals
get their own rngREVEAL_TIMEOUT (1.5s) measured from the moment we
enter ConvergingReveal, ensuring consistent time for reveal collection
regardless of how long txSet convergence took.
2026-02-06 16:00:40 +07:00
Nicholas Dudfield db3ed0c2eb fix(consensus): wait for all committers' reveals and fix local testnet resource charging
- Change hasMinimumReveals() to wait for reveals from ALL committers
  (pendingCommits_.size()) instead of 80% quorum. The commit set is
  deterministic, so we know exactly which reveals to expect.
  rngPIPELINE_TIMEOUT remains the safety valve for crash/partition.
  Fixes reveal-set non-determinism causing entropy divergence on
  15-node testnets.

- Resource manager: preserve port for loopback addresses so local
  testnet nodes each get their own resource bucket instead of all
  sharing one on 127.0.0.1 (causing rate-limit disconnections).

- Make RNG fast-poll interval configurable via XAHAU_RNG_POLL_MS
  env var (default 250ms) for testnet tuning.
2026-02-06 15:22:42 +07:00
Nicholas Dudfield 960808b172 fix(consensus): skip RNG wait when quorum is impossible and base threshold on active UNL
When fewer participants are present than the quorum threshold, skip the
RNG commit wait immediately instead of waiting the full pipeline timeout.
Also base the quorum on activeUNLNodeIds_ (UNL Report with fallback)
instead of the full trusted key set, so the denominator reflects who is
actually active on the network.
2026-02-06 14:34:28 +07:00
Nicholas Dudfield a9dffd38ff fix(consensus): shorten RNG pipeline timeout to 3s for faster recovery
Add rngPIPELINE_TIMEOUT (3s) to replace ledgerMAX_CONSENSUS (10s) in
the commit/reveal quorum gates. Late-joining nodes enter as
proposing=false and cannot contribute commitments until promoted, so
waiting beyond a few seconds just delays the ZERO-entropy fallback and
penalizes recovery. Add inline comments documenting the late-joiner
constraint and SHAMap sync's role as a dropped-proposal safety net.
2026-02-06 14:04:53 +07:00
Nicholas Dudfield 382e6fa673 fix(consensus): verify reveals match commitments and cache UNL for observers
Prevent grinding attacks by verifying sha512Half(reveal, pubKey, seq)
matches the stored commitment before accepting a reveal. Also move
cacheActiveUNL() into startRound so non-proposing nodes (exchanges,
block explorers) correctly accept RNG data instead of diverging with
zero entropy.
2026-02-06 13:28:55 +07:00
Nicholas Dudfield 2905b0509c perf(consensus): gate RNG SHAMap fetches on sub-state
During ConvergingTx all RNG data arrives via proposal leaves, so
fetching a peer's commitSet before we've built our own just generates
unnecessary traffic. Only fetch commitSetHash once in ConvergingCommit+,
and entropySetHash once in ConvergingReveal.
2026-02-06 13:18:53 +07:00
Nicholas Dudfield 4911c1bf52 feat(consensus): embed proposal signature proofs in RNG SHAMap entries
Prevents spoofed SHAMap entries by embedding verifiable proof blobs
(proposal signature + metadata) in each commit/reveal entry via sfBlob.

- Store ProposalProof in harvestRngData (peers) and propose() (self)
- serializeProof: pack proposeSeq/closeTime/prevLedger/position/sig
- verifyProof: reconstruct signingHash, verify against public key
- Embed proofs in buildCommitSet/buildEntropySet via sfBlob field
- Verify proofs in handleAcquiredRngSet (both diff and visitLeaves paths)
- Add stall fix: gate ConvergingTx on timeout when commits unavailable
- Clear proposalProofs_ in clearRngState
2026-02-06 11:47:42 +07:00
Nicholas Dudfield 1744d21410 docs(consensus): explain union convergence model for RNG sets 2026-02-06 11:17:52 +07:00
Nicholas Dudfield 34ff53f65d feat(consensus): add UNL enforcement for RNG commit-reveal pipeline
Cache active UNL NodeIDs per round from UNL Report (in-ledger),
falling back to getTrustedMasterKeys() on fresh testnets.
Reject non-UNL validators at all entry points: harvestRngData,
buildCommitSet, buildEntropySet, and handleAcquiredRngSet.
2026-02-06 11:12:03 +07:00
Nicholas Dudfield 893f8d5a10 feat(consensus): replace fake hashes with real SHAMap-backed commitSet/entropySet
Build real ephemeral (unbacked) SHAMaps for commitSet and entropySet using
ttCONSENSUS_ENTROPY entries with tfEntropyCommit/tfEntropyReveal flags.
Reuse InboundTransactions pipeline for peer fetch/diff/merge with no new
classes. Encode NodeID in sfAccount to avoid master-vs-signing key mismatch.
Add isPseudoTx guard in ConsensusTransSetSF to prevent pseudo-tx submission.
Route acquired RNG sets via isRngSet/gotRngSet in NetworkOPs mapComplete.
2026-02-06 10:38:06 +07:00
Nicholas Dudfield 3e5389d652 feat(consensus): add 250ms fast-poll for RNG sub-state transitions
During ConvergingCommit and ConvergingReveal sub-states, poll at 250ms
instead of the default 1s ledgerGRANULARITY. This reduces total RNG
pipeline overhead from ~3s to ~500ms while keeping the normal heartbeat
interval unchanged for all other consensus phases.
2026-02-06 09:21:42 +07:00
Nicholas Dudfield c44dea3acf fix(consensus): resolve commit-reveal pipeline bugs enabling non-zero entropy
Three critical fixes that unblock the RNG commit-reveal pipeline:

- Remove entropy secret regeneration in ConvergingTx->ConvergingCommit
  transition that was overwriting the onClose() secret, breaking reveal
  verification against the original commitment
- Change ExtendedPosition operator== to compare txSetHash only, preventing
  deadlock where nodes transitioning sub-states at different times would
  break haveConsensus() for all peers
- Self-seed own commitment and reveal into pending collections so the
  node counts toward its own quorum checks

Also adds ExtendedPosition_test with signing, suppression, serialization
round-trip and equality tests, iterator safety fix in BuildLedger, wire
compatibility early-return, and RNG debug logging throughout the pipeline.
2026-02-06 09:03:26 +07:00
Nicholas Dudfield a6dd54fa48 feat(consensus): add featureConsensusEntropy amendment gating
- Register ConsensusEntropy amendment (Supported::yes, DefaultNo)
- Gate entropy pseudo-tx injection behind amendment in doAccept()
- Gate preflight with temDISABLED when amendment not enabled
- Bump numFeatures 90 -> 91
- Exclude featureConsensusEntropy from default test environment to
  avoid breaking existing test transaction count assumptions
2026-02-06 07:29:48 +07:00
Nicholas Dudfield 28bd0a22d3 feat(consensus): add entropy injection, tx ordering, and dispatch registration
- Implement injectEntropyPseudoTx() to combine reveals into final
  entropy hash and inject as pseudo-tx into CanonicalTXSet in doAccept()
- Modify BuildLedger applyTransactions() to apply entropy tx FIRST
  before all other transactions to prevent front-running
- Remove redundant explicit threading in applyConsensusEntropy() as
  sfPreviousTxnID/sfPreviousTxnLgrSeq are set automatically by
  ApplyStateTable::threadItem()
- Register ttCONSENSUS_ENTROPY in applySteps.cpp dispatch tables
  (preflight, preclaim, calculateBaseFee, apply)
- Add ltCONSENSUS_ENTROPY to InvariantCheck.cpp valid type whitelist
2026-02-06 05:43:19 +07:00
Nicholas Dudfield 960fffcf82 feat(consensus): add ttCONSENSUS_ENTROPY pseudo-transaction protocol layer
Add protocol definitions for consensus-derived entropy pseudo-transaction:
- ttCONSENSUS_ENTROPY = 105 transaction type
- ltCONSENSUS_ENTROPY = 0x0058 ledger entry type
- keylet::consensusEntropy() singleton keylet (namespace 'X')
- applyConsensusEntropy() handler in Change.cpp
- Added to isPseudoTx() in STTx.cpp

The entropy value is stored in sfDigest field of the singleton ledger object.
This provides the protocol foundation for same-ledger entropy injection.
2026-02-05 17:26:31 +07:00
Nicholas Dudfield e7867c07a1 feat(consensus): add RNG sub-state gating logic in phaseEstablish
- Add clearRngState() call in startRoundInternal
- Reset estState_ in closeLedger when entering establish phase
- Implement three-phase RNG checkpoint gating:
  - ConvergingTx: wait for quorum commits, build commitSet
  - ConvergingCommit: reveal entropy, transition immediately
  - ConvergingReveal: wait for reveals or timeout, build entropySet
- Use if constexpr for test framework compatibility
2026-02-05 16:53:00 +07:00
Nicholas Dudfield a828e8a44d feat(consensus): add RNG wire protocol and harvest logic
- Serialize full ExtendedPosition in share() and propose()
- Deserialize ExtendedPosition in PeerImp using fromSerialIter()
- Add harvestRngData() to collect commits/reveals from peer proposals
- Conditionally call harvest via if constexpr for test compatibility
2026-02-05 16:41:13 +07:00
Nicholas Dudfield bb33e7cf64 feat(consensus): add ExtendedPosition for RNG entropy support
Introduce data structures for consensus-derived randomness using
commit-reveal scheme:

- Add ExtendedPosition struct with consensus targets (txSetHash,
  commitSetHash, entropySetHash) and pipelined leaves (myCommitment,
  myReveal)
- operator== excludes leaves to allow convergence with unique leaves
- add() includes ALL fields to prevent signature stripping attacks
- Add EstablishState enum for sub-phases: ConvergingTx, ConvergingCommit,
  ConvergingReveal
- Update Consensus template to use Adaptor::Position_t
- Add Position_t typedef to RCLConsensus::Adaptor and test CSF Peer

This is the foundational data structure work for the RNG implementation.
The gating logic and entropy computation will follow.
2026-02-05 16:20:54 +07:00
Nicholas Dudfield 7e8e0654cd chore: add documentation markers for pr-description-outline 2026-01-28 15:00:14 +07:00
Nicholas Dudfield 38af0626e0 chore: add documentation markers for pr-description 2026-01-28 14:50:56 +07:00
Nicholas Dudfield 8500e86f57 chore: remove projected-source documentation markers 2026-01-28 11:30:47 +07:00
Nicholas Dudfield 1fc4fd9bfd chore: regenerate hook headers for export feature 2026-01-28 11:27:13 +07:00
Nicholas Dudfield e4875e5398 refactor: remove ttEXPORT_SIGN and UVTx infrastructure
- Delete ExportSign.cpp/h transactor (ttEXPORT_SIGN no longer used)
- Remove isUVTx() function and all UVTx checks from Transactor/TxQ
- Remove ttEXPORT_SIGN from TxFormats enum and format definition
- Remove jss::ExportSign
- Move signPendingExports() to ExportSignatureCollector

Export signatures are now collected ephemerally via TMValidation
messages, not via ttEXPORT_SIGN transactions.
2026-01-28 10:59:45 +07:00
Nicholas Dudfield 5b1b142be0 chore: remove stray iostream includes 2026-01-28 10:34:11 +07:00
Nicholas Dudfield 5ba832204a test: remove unused scaffolding from Export_test
- Remove accept_wasm and emit_wasm hooks (not export-related)
- Remove testBasicSetup, testEmitPayment, testXportPayment
- Keep only testXportPaymentWithValidator which tests the export flow
2026-01-28 10:31:49 +07:00
Nicholas Dudfield 1257b3a65c Merge remote-tracking branch 'origin/dev' into export-uvtxn 2026-01-28 10:21:37 +07:00
Nicholas Dudfield 6013ed2cb6 refactor: remove vestigial on-ledger export signature code
- Remove makeExportSignTxns() function (signatures now via TMValidation)
- Simplify ExportSign::doApply() to no-op (ttEXPORT_SIGN kept for protocol)
- Remove sfSigners from ltEXPORTED_TXN format (collected in memory now)
- Remove unused OpenView include and forward declaration
- Remove vestigial comment in TxQ about makeExportSignTxns
2026-01-28 10:19:14 +07:00
Nicholas Dudfield 034010716e feat: add signature verification cache for export signatures
Add cryptographic verification of export signatures as they arrive:
- stashTxnData() caches serialized txn for verification
- verifyAndAddSignature() verifies against cached data, rejects invalid
- isSignatureVerified() / verifySignature() for Transactor fallback
- Cleanup methods updated to clear verification cache

Also removes leftover debug std::cerr from OpenView, STObject, and tests.
2026-01-27 18:03:55 +07:00
Nicholas Dudfield b28793b0fa chore: clean up export debug logging
- remove DBG_EXPORT macros and all usages
- remove [EXPORT-TRACE] and [EXPORT-TIMING] debug prefixes
- adjust log levels (verbose logs to trace, summaries to debug)
- upgrade "quorum reached" to info level (important event)
- standardize log prefixes to use "Export:"
- re-enable relay loop in OpenLedger.cpp
- remove reentrant call detection debug code
2026-01-27 16:21:02 +07:00
Nicholas Dudfield 4bce392c31 feat: continuous signature broadcasting for export robustness
Validators now sign ALL pending ltEXPORTED_TXN entries every ledger
(not just those from the current ledger). Signatures are cached in
ExportSignatureCollector and re-broadcast until the export is finalized.

Changes:
- Add hasSignatureFrom() and getSignatureFrom() to collector for
  checking/retrieving cached signatures
- signPendingExports() now iterates ALL pending exports, uses cached
  signature if available, otherwise signs fresh
- Signatures keep broadcasting until ltEXPORTED_TXN is deleted

This ensures:
- Late validators can contribute (sign when they come online)
- Network partitions self-heal (signatures propagate on reconnect)
- Node restarts recover (re-sign from ledger state)

The ltEXPORTED_TXN acts as a "ticket" - signatures only valid while it
exists. No explicit expiry check needed; ledger state is the gatekeeper.
2026-01-26 18:25:24 +07:00
Nicholas Dudfield 244a28b981 feat: implement ephemeral export signature collection
Replace on-ledger ttEXPORT_SIGN transactions with ephemeral signature
collection via TMValidation messages. This eliminates O(n²) metadata
bloat from accumulating signatures on-ledger.

Changes:
- Add ExportSignatureCollector for in-memory signature storage with
  quorum tracking (80% UNL threshold)
- Extend TMValidation protobuf with exportSignatures field
- Sign pending exports during validate() and broadcast via validation
- Extract signatures from received TMValidation in PeerImp
- TxQ checks quorum from memory instead of ledger
- Inject ttEXPORT when quorum reached (can be ledger N+1 or N+2)
- Clean up collector after ttEXPORT processed

Includes [EXPORT-TIMING] debug logging for timing analysis.
2026-01-26 17:54:17 +07:00
Nicholas Dudfield f2838351c9 chore: add [EXPORT-TRACE] debug logging for export flow tracing
adds step-by-step trace logging with [EXPORT-TRACE] prefix to track
the complete export transaction lifecycle:
- STEP-1: xport() creates ltEXPORTED_TXN
- STEP-2a: rawTxInsert ttEXPORT_SIGN in callback
- STEP-2b: doApply ttEXPORT_SIGN
- STEP-3a: rawTxInsert ttEXPORT
- STEP-4: doApply ttEXPORT (cleanup)

filter with: grep '\[EXPORT-TRACE\]'
2026-01-23 08:10:33 +07:00
Nicholas Dudfield dae082d6a5 chore: format files with clang-format 2026-01-22 16:42:05 +07:00
Nicholas Dudfield 619a4a68f7 fix: resolve export feature bugs and add comprehensive tests
- fix Guard.h: add import_whitelist_2 to signature lookup chain
  (was causing "Function type is inconsistent" errors for xport APIs)
- fix InvariantCheck.cpp: add ltEXPORTED_TXN to valid ledger entry types
  (was causing "invalid ledger entry type added" invariant failures)
- add SetHook.cpp: TODO comment documenting API version confusion

- add Export_test.cpp: comprehensive test suite for export feature
  - testBasicSetup: verify hook installation works
  - testEmitPayment: verify emit() flow works
  - testXportPayment: verify xport() creates ltEXPORTED_TXN
  - includes DebugLogs helper for per-partition log levels
  - parameterized runXportTest helper for future validator tests

Note: validator signing flow (ttEXPORT_SIGN) still needs debugging -
causes internal error on env.close() when validator config enabled.
2026-01-22 09:51:50 +07:00
Nicholas Dudfield 4a6db8bb05 Merge remote-tracking branch 'origin/dev' into export-uvtxn 2026-01-22 08:07:58 +07:00
Nicholas Dudfield c86479bc58 fix: correct xport api signature and sfExportedTxn type usage
- Fix xport hook API whitelist to declare 4 args (I32, I32, I32, I32)
  instead of 2, matching the actual implementation signature
- Fix TxQ.cpp to use emplace_back with STObject for sfExportedTxn
  instead of setFieldVL, since sfExportedTxn is OBJECT type not VL.
  The previous code would throw "Wrong field type" at runtime.
2026-01-22 07:41:12 +07:00
Nicholas Dudfield dc6a2dc6ff refactor: separate ExportSign transactor from Change
Move ttEXPORT_SIGN handling to dedicated ExportSign transactor class,
following the same pattern as ttENTROPY/Entropy from the RNG feature.
UVTxns (signed validator transactions) should not be mixed with
pseudo-transactions in the Change transactor.

- Create ExportSign.h/cpp with preflight, preclaim, doApply
- Route ttEXPORT_SIGN through ExportSign in applySteps.cpp
- Remove UVTx branches from Change transactor
- Add documentation markers to View.h for inUNLReport functions
2026-01-22 07:41:12 +07:00
Nicholas Dudfield c01b9a657b feat: implement uvtxn pattern for ttEXPORT_SIGN
Port the UNL Validator Transaction (UVTxn) pattern from the RNG feature
to allow validators to submit signed ttEXPORT_SIGN transactions without
requiring a funded account.

Changes:
- Add isUVTx() to identify UVTxn transaction types
- Add inUNLReport() templates to check validator UNLReport membership
- Add getValidationSecretKey() to Application for signing
- Modify Transactor for UVTxn bypasses (fee, seq, signature checks)
- Add makeExportSignTxns() to generate validator signatures
- Hook into RCLConsensus to submit ttEXPORT_SIGN during accept
- Update applySteps.cpp routing for ttEXPORT_SIGN
- Remove direct ttEXPORT_SIGN injection from TxQ::accept

Note: Currently uses Change transactor with UVTx branches.
May refactor to dedicated ExportSign transactor class.
2026-01-20 13:44:38 +07:00
Nicholas Dudfield 652b181b5d chore: clang format 2026-01-20 12:44:14 +07:00
RichardAHandtequ 8329d78f32 Update src/ripple/app/tx/impl/Import.cpp
Co-authored-by: tequ <[email protected]>
2025-12-21 13:42:46 +10:00
RichardAHandtequ bf4579c1d1 Update src/ripple/app/tx/impl/Change.cpp
Co-authored-by: tequ <[email protected]>
2025-12-21 13:42:37 +10:00
RichardAHandtequ 73e099eb23 Update src/ripple/app/hook/impl/applyHook.cpp
Co-authored-by: tequ <[email protected]>
2025-12-21 13:42:29 +10:00
RichardAHandtequ 2e311b4259 Update src/ripple/app/hook/applyHook.h
Co-authored-by: tequ <[email protected]>
2025-12-21 13:42:20 +10:00
RichardAH 7c8e940091 Merge branch 'dev' into export 2025-12-19 13:27:02 +10:00
Richard Holland 9b90c50789 featureExport compiling, untested 2025-12-19 14:19:17 +11:00
Richard Holland a18e2cb2c6 remainder of the export feature... untested uncompiled 2025-12-14 19:04:37 +11:00
Richard Holland be5f425122 change symbol name to xport 2025-12-14 13:27:44 +11:00
Richard Holland fc6f4762da export hook apis, untested 2025-12-13 15:46:08 +11:00
244 changed files with 45125 additions and 600 deletions

No files matched your search

+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
run: |
# Download install.sh
curl -o /tmp/wasienv-install.sh https://raw.githubusercontent.com/wasienv/wasienv/master/install.sh
# Replace /bin to /local/bin
sed -i 's|/bin|/local/bin|g' /tmp/wasienv-install.sh
@@ -5,6 +5,18 @@ on:
pull_request:
jobs:
verify-hook-sdk-abi:
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check shipped SDK against runtime ABI and constants
run: clang++ -std=c++17 -Iinclude -fsyntax-only hook/tests/sdk_abi.cpp
- name: Compile entropy calls using shipped C headers
run: clang -std=c11 -Werror -Wno-pointer-to-int-cast -fsyntax-only hook/tests/entropy_api.c
verify-generated-headers:
strategy:
fail-fast: false
@@ -16,6 +28,8 @@ jobs:
generator: ./hook/generate_extern.sh
- target: hook/sfcodes.h
generator: bash ./hook/generate_sfcodes.sh
- target: hook/genesis/headers/sfcodes.h
generator: bash ./hook/generate_sfcodes.sh
- target: hook/tts.h
generator: ./hook/generate_tts.sh
- target: hook/ls_flags.h
+1 -1
View File
@@ -26,7 +26,7 @@ Loop: xrpld.app xrpld.nodestore
xrpld.app > xrpld.nodestore
Loop: xrpld.app xrpld.overlay
xrpld.overlay ~= xrpld.app
xrpld.overlay == xrpld.app
Loop: xrpld.app xrpld.peerfinder
xrpld.app > xrpld.peerfinder
+6
View File
@@ -12,6 +12,7 @@ libxrpl.server > xrpl.basics
libxrpl.server > xrpl.json
libxrpl.server > xrpl.protocol
libxrpl.server > xrpl.server
test.app > test.shamap
test.app > test.toplevel
test.app > test.unit_test
test.app > xrpl.basics
@@ -21,6 +22,7 @@ test.app > xrpld.ledger
test.app > xrpld.nodestore
test.app > xrpld.overlay
test.app > xrpld.rpc
test.app > xrpld.shamap
test.app > xrpl.hook
test.app > xrpl.json
test.app > xrpl.protocol
@@ -36,6 +38,7 @@ test.beast > xrpl.basics
test.conditions > xrpl.basics
test.conditions > xrpld.conditions
test.consensus > test.csf
test.consensus > test.jtx
test.consensus > test.toplevel
test.consensus > test.unit_test
test.consensus > xrpl.basics
@@ -43,6 +46,9 @@ test.consensus > xrpld.app
test.consensus > xrpld.consensus
test.consensus > xrpld.core
test.consensus > xrpld.ledger
test.consensus > xrpld.overlay
test.consensus > xrpld.shamap
test.consensus > xrpl.json
test.consensus > xrpl.protocol
test.core > test.jtx
test.core > test.toplevel
+9 -3
View File
@@ -21,7 +21,11 @@ endif()
project (xrpl)
set(Boost_NO_BOOST_CMAKE ON)
# make GIT_COMMIT_HASH define available to all sources
# Only sources that expose Git metadata should rebuild when HEAD changes.
set(git_info_sources
"${CMAKE_CURRENT_SOURCE_DIR}/src/xrpld/app/main/Main.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/xrpld/app/misc/NetworkOPs.cpp")
find_package(Git)
if(Git_FOUND)
execute_process(COMMAND ${GIT_EXECUTABLE} --git-dir=${CMAKE_CURRENT_SOURCE_DIR}/.git rev-parse HEAD
@@ -29,7 +33,8 @@ if(Git_FOUND)
if(gch)
set(GIT_COMMIT_HASH "${gch}")
message(STATUS gch: ${GIT_COMMIT_HASH})
add_definitions(-DGIT_COMMIT_HASH="${GIT_COMMIT_HASH}")
set_property(SOURCE ${git_info_sources} APPEND PROPERTY
COMPILE_DEFINITIONS "GIT_COMMIT_HASH=\"${GIT_COMMIT_HASH}\"")
endif()
execute_process(COMMAND ${GIT_EXECUTABLE} --git-dir=${CMAKE_CURRENT_SOURCE_DIR}/.git rev-parse --abbrev-ref HEAD
@@ -37,7 +42,8 @@ if(Git_FOUND)
if(gb)
set(GIT_BRANCH "${gb}")
message(STATUS gb: ${GIT_BRANCH})
add_definitions(-DGIT_BRANCH="${GIT_BRANCH}")
set_property(SOURCE ${git_info_sources} APPEND PROPERTY
COMPILE_DEFINITIONS "GIT_BRANCH=\"${GIT_BRANCH}\"")
endif()
endif() #git
+1 -1
View File
@@ -2,7 +2,7 @@
**Note:** Throughout this README, references to "we" or "our" pertain to the community and contributors involved in the Xahau network. It does not imply a legal entity or a specific collection of individuals.
[Xahau](https://xahau.network/) is a decentralized cryptographic ledger that builds upon the robust foundation of the XRP Ledger. It inherits the XRP Ledger's Byzantine Fault Tolerant consensus algorithm and enhances it with additional features and functionalities. Developers and users familiar with the XRP Ledger will find that most documentation and tutorials available on [xrpl.org](https://xrpl.org) are relevant and applicable to Xahau, including those related to running validators and managing validator keys. For Xahau specific documentation you can visit our [documentation](https://xahau.network/)
[Xahau](https://xahau.network/) is a decentralized cryptographic ledger that builds upon the robust foundation of the XRP Ledger. It inherits the XRP Ledger's Byzantine Fault Tolerant consensus algorithm under the normal XRPL assumptions about configured validator-list overlap, timing, and fault bounds, and enhances it with additional features and functionalities. Developers and users familiar with the XRP Ledger will find that most documentation and tutorials available on [xrpl.org](https://xrpl.org) are relevant and applicable to Xahau, including those related to running validators and managing validator keys. For Xahau specific documentation you can visit our [documentation](https://xahau.network/)
## XAH
XAH is the public, counterparty-free asset native to Xahau and functions primarily as network gas. Transactions submitted to the Xahau network must supply an appropriate amount of XAH, to be burnt by the network as a fee, in order to be successfully included in a validated ledger. In addition, XAH also acts as a bridge currency within the Xahau DEX. XAH is traded on the open-market and is available for anyone to access. Xahau was created in 2023 with a supply of 600 million units of XAH.
+18
View File
@@ -165,6 +165,9 @@ if(xrpld)
if(tests)
target_compile_definitions(rippled PUBLIC ENABLE_TESTS)
endif()
if(xahaud_runtime_test_config)
target_compile_definitions(rippled PUBLIC XAHAUD_ENABLE_RUNTIME_TEST_CONFIG=1)
endif()
target_include_directories(rippled
PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
@@ -180,6 +183,21 @@ if(xrpld)
"${CMAKE_CURRENT_SOURCE_DIR}/src/test/*.cpp"
)
target_sources(rippled PRIVATE ${sources})
set(HOOKS_TEST_DIR "" CACHE PATH "External hook Env-test directory")
if(NOT HOOKS_TEST_DIR AND DEFINED ENV{HOOKS_TEST_DIR})
set(HOOKS_TEST_DIR "$ENV{HOOKS_TEST_DIR}")
endif()
if(HOOKS_TEST_DIR)
file(GLOB_RECURSE hook_test_sources CONFIGURE_DEPENDS
"${HOOKS_TEST_DIR}/*_test.cpp"
)
if(hook_test_sources)
message(STATUS "Including external hook Env tests from ${HOOKS_TEST_DIR}")
target_sources(rippled PRIVATE ${hook_test_sources})
target_include_directories(rippled PRIVATE "${HOOKS_TEST_DIR}")
endif()
endif()
endif()
target_link_libraries(rippled
+3
View File
@@ -22,6 +22,9 @@ target_compile_definitions (opts
$<$<BOOL:${beast_no_unit_test_inline}>:BEAST_NO_UNIT_TEST_INLINE=1>
$<$<BOOL:${beast_disable_autolink}>:BEAST_DONT_AUTOLINK_TO_WIN32_LIBRARIES=1>
$<$<BOOL:${single_io_service_thread}>:RIPPLE_SINGLE_IO_SERVICE_THREAD=1>
# Enhanced logging is enabled for Debug builds, or explicitly via
# -DBEAST_ENHANCED_LOGGING=ON for other build types.
$<$<OR:$<CONFIG:Debug>,$<BOOL:${BEAST_ENHANCED_LOGGING}>>:BEAST_ENHANCED_LOGGING=1>
$<$<BOOL:${voidstar}>:ENABLE_VOIDSTAR>)
target_compile_options (opts
INTERFACE
+7
View File
@@ -12,6 +12,13 @@ option(xrpld "Build xrpld" ON)
option(tests "Build tests" ON)
option(xahaud_runtime_test_config
"Enable XAHAUD_RUNTIME_TEST_CONFIG env and runtime_config RPC fault-injection controls"
OFF)
# Conan 2 local opt-in:
# [conf]
# tools.cmake.cmaketoolchain:extra_variables={"xahaud_runtime_test_config":"ON"}
option(unity "Creates a build using UNITY support in cmake. This is the default" ON)
if(unity)
if(NOT is_ci)
+4
View File
@@ -47,5 +47,9 @@
#define MEM_OVERLAP -43
#define TOO_MANY_STATE_MODIFICATIONS -44
#define TOO_MANY_NAMESPACES -45
#define EXPORT_FAILURE -46
#define TOO_MANY_EXPORTED_TXN -47
#define TOO_LITTLE_ENTROPY -48
#define LATER_STRONG_HOOK -49
#define HOOK_ERROR_CODES
#endif //HOOK_ERROR_CODES
+61
View File
@@ -339,6 +339,67 @@ prepare(
uint32_t read_ptr,
uint32_t read_len);
extern int64_t
xport_reserve(uint32_t count);
// callback_fee_drops: 0 omits third-party delivery permission; otherwise the
// exact Import fee authorized by the emitted Export intent, in native drops.
extern int64_t
xport(
uint32_t write_ptr,
uint32_t write_len,
uint32_t read_ptr,
uint32_t read_len,
uint32_t committee_hash_ptr,
uint32_t committee_hash_len,
uint64_t callback_fee_drops);
extern int64_t
xport_cancel(uint32_t read_ptr, uint32_t read_len, uint32_t flags);
/*
Consensus entropy APIs.
min_tier is a required fail-closed floor:
1 = consensus_fallback, 2 = participant_aligned,
3 = validator_quorum, 4 = validator_full.
entropy_cr_status returns a packed non-negative value:
bits 32..39 tier, 16..31 count, 0..15 denominator.
Check for a negative error before using the ENTROPY_* macros.
Classify tier before count/denominator arithmetic: fallback is tier 1
with count=denominator=0. Common policies are denominator-count <= 1,
5*count >= 4*denominator (use widened arithmetic), or count >= floor.
In strong execution, flags=0 requires the last eligible strong Hook.
Otherwise: LATER_STRONG_HOOK before drawing or changing output.
ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO permits later strong Hooks
only on the installed account of the drawing Hook.
ENTROPY_ALLOW_ANY_STRONG_VETO permits them on any account, including
the same account. Both bits mean ANY; unknown bits are invalid.
Allowed Hooks retain their ordinary veto authority. An opt-in must
account for downstream code and whoever can replace it.
Draws return TOO_LITTLE_ENTROPY if the visible input is missing,
stale, or below min_tier, independently of composition flags.
Closed execution requires current-ledger input; open-ledger and
simulate execution can use previous-ledger input and are provisional
previews. Final ordered execution may see different entropy.
*/
extern int64_t
entropy_cr_dice(uint32_t sides, uint32_t min_tier, uint32_t flags);
extern int64_t
entropy_cr_random(
uint32_t write_ptr,
uint32_t write_len,
uint32_t min_tier,
uint32_t flags);
extern int64_t
entropy_cr_status(void);
#ifdef __cplusplus
}
#endif
+38
View File
@@ -41,6 +41,44 @@ APPLY_HOOK="$SCRIPT_DIR/../include/xrpl/hook/hook_api.macro"
# Insert __attribute__((noduplicate)) before _g
sub(/[[:space:]]+_g/, " __attribute__((noduplicate)) _g", line);
}
if (line ~ /[[:space:]]+xport[[:space:]]*\(/) {
print "// callback_fee_drops: 0 omits third-party delivery permission; otherwise the";
print "// exact Import fee authorized by the emitted Export intent, in native drops.";
}
if (line ~ /[[:space:]]+entropy_cr_dice[[:space:]]*\(/) {
print "/*";
print " Consensus entropy APIs.";
print "";
print " min_tier is a required fail-closed floor:";
print " 1 = consensus_fallback, 2 = participant_aligned,";
print " 3 = validator_quorum, 4 = validator_full.";
print "";
print " entropy_cr_status returns a packed non-negative value:";
print " bits 32..39 tier, 16..31 count, 0..15 denominator.";
print " Check for a negative error before using the ENTROPY_* macros.";
print "";
print " Classify tier before count/denominator arithmetic: fallback is tier 1";
print " with count=denominator=0. Common policies are denominator-count <= 1,";
print " 5*count >= 4*denominator (use widened arithmetic), or count >= floor.";
print "";
print " In strong execution, flags=0 requires the last eligible strong Hook.";
print " Otherwise: LATER_STRONG_HOOK before drawing or changing output.";
print " ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO permits later strong Hooks";
print " only on the installed account of the drawing Hook.";
print " ENTROPY_ALLOW_ANY_STRONG_VETO permits them on any account, including";
print " the same account. Both bits mean ANY; unknown bits are invalid.";
print " Allowed Hooks retain their ordinary veto authority. An opt-in must";
print " account for downstream code and whoever can replace it.";
print "";
print " Draws return TOO_LITTLE_ENTROPY if the visible input is missing,";
print " stale, or below min_tier, independently of composition flags.";
print " Closed execution requires current-ledger input; open-ledger and";
print " simulate execution can use previous-ledger input and are provisional";
print " previews. Final ordered execution may see different entropy.";
print "*/";
}
# printf("\n");
+20 -1
View File
@@ -348,5 +348,24 @@ util_verify(
uint32_t kread_len);
extern int64_t xpop_slot(uint32_t, uint32_t);
extern int64_t
xport_reserve(uint32_t count);
// callback_fee_drops: 0 omits third-party delivery permission; otherwise the
// exact Import fee authorized by the emitted Export intent, in native drops.
extern int64_t
xport(
uint32_t write_ptr,
uint32_t write_len,
uint32_t read_ptr,
uint32_t read_len,
uint32_t committee_hash_ptr,
uint32_t committee_hash_len,
uint64_t callback_fee_drops);
extern int64_t
xport_cancel(uint32_t read_ptr, uint32_t read_len, uint32_t flags);
#define HOOK_EXTERN
#endif // HOOK_EXTERN
#endif // HOOK_EXTERN
+1 -1
View File
@@ -47,4 +47,4 @@
#include "macro.h"
#include "types.h"
#endif
#endif
+60 -52
View File
@@ -607,31 +607,37 @@ int out_len = 0;\
#define PREPARE_PAYMENT_SIMPLE_SIZE 248U
#endif
#define PREPARE_PAYMENT_SIMPLE(buf_out_master, drops_amount_raw, to_address, dest_tag_raw, src_tag_raw)\
{\
uint8_t* buf_out = buf_out_master;\
uint8_t acc[20];\
uint64_t drops_amount = (drops_amount_raw);\
uint32_t dest_tag = (dest_tag_raw);\
uint32_t src_tag = (src_tag_raw);\
uint32_t cls = (uint32_t)ledger_seq();\
hook_account(SBUF(acc));\
_01_02_ENCODE_TT (buf_out, ttPAYMENT ); /* uint16 | size 3 */ \
_02_02_ENCODE_FLAGS (buf_out, tfCANONICAL ); /* uint32 | size 5 */ \
_02_03_ENCODE_TAG_SRC (buf_out, src_tag ); /* uint32 | size 5 */ \
_02_04_ENCODE_SEQUENCE (buf_out, 0 ); /* uint32 | size 5 */ \
_02_14_ENCODE_TAG_DST (buf_out, dest_tag ); /* uint32 | size 5 */ \
_02_26_ENCODE_FLS (buf_out, cls + 1 ); /* uint32 | size 6 */ \
_02_27_ENCODE_LLS (buf_out, cls + 5 ); /* uint32 | size 6 */ \
_06_01_ENCODE_DROPS_AMOUNT (buf_out, drops_amount ); /* amount | size 9 */ \
uint8_t* fee_ptr = buf_out;\
_06_08_ENCODE_DROPS_FEE (buf_out, 0 ); /* amount | size 9 */ \
_07_03_ENCODE_SIGNING_PUBKEY_NULL (buf_out ); /* pk | size 35 */ \
_08_01_ENCODE_ACCOUNT_SRC (buf_out, acc ); /* account | size 22 */ \
_08_03_ENCODE_ACCOUNT_DST (buf_out, to_address ); /* account | size 22 */ \
int64_t edlen = etxn_details((uint32_t)buf_out, PREPARE_PAYMENT_SIMPLE_SIZE); /* emitdet | size 1?? */ \
int64_t fee = etxn_fee_base(buf_out_master, PREPARE_PAYMENT_SIMPLE_SIZE); \
_06_08_ENCODE_DROPS_FEE (fee_ptr, fee ); \
#define PREPARE_PAYMENT_SIMPLE( \
buf_out_master, drops_amount_raw, to_address, dest_tag_raw, src_tag_raw) \
{ \
uint8_t* buf_out = buf_out_master; \
uint8_t acc[20]; \
uint64_t drops_amount = (drops_amount_raw); \
uint32_t dest_tag = (dest_tag_raw); \
uint32_t src_tag = (src_tag_raw); \
uint32_t cls = (uint32_t)ledger_seq(); \
hook_account(SBUF(acc)); \
_01_02_ENCODE_TT(buf_out, ttPAYMENT); /* uint16 | size 3 */ \
_02_02_ENCODE_FLAGS(buf_out, tfCANONICAL); /* uint32 | size 5 */ \
_02_03_ENCODE_TAG_SRC(buf_out, src_tag); /* uint32 | size 5 */ \
_02_04_ENCODE_SEQUENCE(buf_out, 0); /* uint32 | size 5 */ \
_02_14_ENCODE_TAG_DST(buf_out, dest_tag); /* uint32 | size 5 */ \
_02_26_ENCODE_FLS(buf_out, cls + 1); /* uint32 | size 6 */ \
_02_27_ENCODE_LLS(buf_out, cls + 5); /* uint32 | size 6 */ \
_06_01_ENCODE_DROPS_AMOUNT( \
buf_out, drops_amount); /* amount | size 9 */ \
uint8_t* fee_ptr = buf_out; \
_06_08_ENCODE_DROPS_FEE(buf_out, 0); /* amount | size 9 */ \
_07_03_ENCODE_SIGNING_PUBKEY_NULL(buf_out); /* pk | size 35 */ \
_08_01_ENCODE_ACCOUNT_SRC(buf_out, acc); /* account | size 22 */ \
_08_03_ENCODE_ACCOUNT_DST( \
buf_out, to_address); /* account | size 22 */ \
int64_t edlen = etxn_details( \
(uint32_t)buf_out, \
PREPARE_PAYMENT_SIMPLE_SIZE); /* emitdet | size 1?? */ \
int64_t fee = \
etxn_fee_base(buf_out_master, PREPARE_PAYMENT_SIMPLE_SIZE); \
_06_08_ENCODE_DROPS_FEE(fee_ptr, fee); \
}
#ifdef HAS_CALLBACK
@@ -639,33 +645,35 @@ int out_len = 0;\
#else
#define PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE 287
#endif
#define PREPARE_PAYMENT_SIMPLE_TRUSTLINE(buf_out_master, tlamt, to_address, dest_tag_raw, src_tag_raw)\
{\
uint8_t* buf_out = buf_out_master;\
uint8_t acc[20];\
uint32_t dest_tag = (dest_tag_raw);\
uint32_t src_tag = (src_tag_raw);\
uint32_t cls = (uint32_t)ledger_seq();\
hook_account(SBUF(acc));\
_01_02_ENCODE_TT (buf_out, ttPAYMENT ); /* uint16 | size 3 */ \
_02_02_ENCODE_FLAGS (buf_out, tfCANONICAL ); /* uint32 | size 5 */ \
_02_03_ENCODE_TAG_SRC (buf_out, src_tag ); /* uint32 | size 5 */ \
_02_04_ENCODE_SEQUENCE (buf_out, 0 ); /* uint32 | size 5 */ \
_02_14_ENCODE_TAG_DST (buf_out, dest_tag ); /* uint32 | size 5 */ \
_02_26_ENCODE_FLS (buf_out, cls + 1 ); /* uint32 | size 6 */ \
_02_27_ENCODE_LLS (buf_out, cls + 5 ); /* uint32 | size 6 */ \
_06_01_ENCODE_TL_AMOUNT (buf_out, tlamt ); /* amount | size 48 */ \
uint8_t* fee_ptr = buf_out;\
_06_08_ENCODE_DROPS_FEE (buf_out, 0 ); /* amount | size 9 */ \
_07_03_ENCODE_SIGNING_PUBKEY_NULL (buf_out ); /* pk | size 35 */ \
_08_01_ENCODE_ACCOUNT_SRC (buf_out, acc ); /* account | size 22 */ \
_08_03_ENCODE_ACCOUNT_DST (buf_out, to_address ); /* account | size 22 */ \
etxn_details((uint32_t)buf_out, PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE); /* emitdet | size 1?? */ \
int64_t fee = etxn_fee_base(buf_out_master, PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE); \
_06_08_ENCODE_DROPS_FEE (fee_ptr, fee ); \
#define PREPARE_PAYMENT_SIMPLE_TRUSTLINE( \
buf_out_master, tlamt, to_address, dest_tag_raw, src_tag_raw) \
{ \
uint8_t* buf_out = buf_out_master; \
uint8_t acc[20]; \
uint32_t dest_tag = (dest_tag_raw); \
uint32_t src_tag = (src_tag_raw); \
uint32_t cls = (uint32_t)ledger_seq(); \
hook_account(SBUF(acc)); \
_01_02_ENCODE_TT(buf_out, ttPAYMENT); /* uint16 | size 3 */ \
_02_02_ENCODE_FLAGS(buf_out, tfCANONICAL); /* uint32 | size 5 */ \
_02_03_ENCODE_TAG_SRC(buf_out, src_tag); /* uint32 | size 5 */ \
_02_04_ENCODE_SEQUENCE(buf_out, 0); /* uint32 | size 5 */ \
_02_14_ENCODE_TAG_DST(buf_out, dest_tag); /* uint32 | size 5 */ \
_02_26_ENCODE_FLS(buf_out, cls + 1); /* uint32 | size 6 */ \
_02_27_ENCODE_LLS(buf_out, cls + 5); /* uint32 | size 6 */ \
_06_01_ENCODE_TL_AMOUNT(buf_out, tlamt); /* amount | size 48 */ \
uint8_t* fee_ptr = buf_out; \
_06_08_ENCODE_DROPS_FEE(buf_out, 0); /* amount | size 9 */ \
_07_03_ENCODE_SIGNING_PUBKEY_NULL(buf_out); /* pk | size 35 */ \
_08_01_ENCODE_ACCOUNT_SRC(buf_out, acc); /* account | size 22 */ \
_08_03_ENCODE_ACCOUNT_DST( \
buf_out, to_address); /* account | size 22 */ \
etxn_details( \
(uint32_t)buf_out, \
PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE); /* emitdet | size 1?? */ \
int64_t fee = etxn_fee_base( \
buf_out_master, PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE); \
_06_08_ENCODE_DROPS_FEE(fee_ptr, fee); \
}
#endif
+137 -7
View File
@@ -3,18 +3,31 @@
#define sfCloseResolution ((16U << 16U) + 1U)
#define sfMethod ((16U << 16U) + 2U)
#define sfTransactionResult ((16U << 16U) + 3U)
#define sfScale ((16U << 16U) + 4U)
#define sfAssetScale ((16U << 16U) + 5U)
#define sfTickSize ((16U << 16U) + 16U)
#define sfUNLModifyDisabling ((16U << 16U) + 17U)
#define sfHookResult ((16U << 16U) + 18U)
#define sfWasLockingChainSend ((16U << 16U) + 19U)
#define sfSidecarType ((16U << 16U) + 20U)
#define sfEntropyTier ((16U << 16U) + 21U)
#define sfLedgerEntryType ((1U << 16U) + 1U)
#define sfTransactionType ((1U << 16U) + 2U)
#define sfSignerWeight ((1U << 16U) + 3U)
#define sfTransferFee ((1U << 16U) + 4U)
#define sfTradingFee ((1U << 16U) + 5U)
#define sfDiscountedFee ((1U << 16U) + 6U)
#define sfVersion ((1U << 16U) + 16U)
#define sfHookStateChangeCount ((1U << 16U) + 17U)
#define sfHookEmitCount ((1U << 16U) + 18U)
#define sfHookExecutionIndex ((1U << 16U) + 19U)
#define sfHookApiVersion ((1U << 16U) + 20U)
#define sfHookStateScale ((1U << 16U) + 21U)
#define sfLedgerFixType ((1U << 16U) + 22U)
#define sfHookExportCount ((1U << 16U) + 98U)
#define sfEntropyCount ((1U << 16U) + 99U)
#define sfEntropyDenominator ((1U << 16U) + 100U)
#define sfExportCount ((1U << 16U) + 101U)
#define sfNetworkID ((2U << 16U) + 1U)
#define sfFlags ((2U << 16U) + 2U)
#define sfSourceTag ((2U << 16U) + 3U)
@@ -29,6 +42,7 @@
#define sfWalletSize ((2U << 16U) + 12U)
#define sfOwnerCount ((2U << 16U) + 13U)
#define sfDestinationTag ((2U << 16U) + 14U)
#define sfLastUpdateTime ((2U << 16U) + 15U)
#define sfHighQualityIn ((2U << 16U) + 16U)
#define sfHighQualityOut ((2U << 16U) + 17U)
#define sfLowQualityIn ((2U << 16U) + 18U)
@@ -60,7 +74,15 @@
#define sfBurnedNFTokens ((2U << 16U) + 44U)
#define sfHookStateCount ((2U << 16U) + 45U)
#define sfEmitGeneration ((2U << 16U) + 46U)
#define sfLockCount ((2U << 16U) + 47U)
#define sfVoteWeight ((2U << 16U) + 48U)
#define sfLockCount ((2U << 16U) + 49U)
#define sfFirstNFTokenSequence ((2U << 16U) + 50U)
#define sfOracleDocumentID ((2U << 16U) + 51U)
#define sfStartTime ((2U << 16U) + 93U)
#define sfRepeatCount ((2U << 16U) + 94U)
#define sfDelaySeconds ((2U << 16U) + 95U)
#define sfXahauActivationLgrSeq ((2U << 16U) + 96U)
#define sfImportSequence ((2U << 16U) + 97U)
#define sfRewardTime ((2U << 16U) + 98U)
#define sfRewardLgrFirst ((2U << 16U) + 99U)
#define sfRewardLgrLast ((2U << 16U) + 100U)
@@ -80,12 +102,26 @@
#define sfHookInstructionCount ((3U << 16U) + 17U)
#define sfHookReturnCode ((3U << 16U) + 18U)
#define sfReferenceCount ((3U << 16U) + 19U)
#define sfXChainClaimID ((3U << 16U) + 20U)
#define sfXChainAccountCreateCount ((3U << 16U) + 21U)
#define sfXChainAccountClaimCount ((3U << 16U) + 22U)
#define sfAssetPrice ((3U << 16U) + 23U)
#define sfMaximumAmount ((3U << 16U) + 24U)
#define sfOutstandingAmount ((3U << 16U) + 25U)
#define sfMPTAmount ((3U << 16U) + 26U)
#define sfIssuerNode ((3U << 16U) + 27U)
#define sfSubjectNode ((3U << 16U) + 28U)
#define sfExportNode ((3U << 16U) + 29U)
#define sfTouchCount ((3U << 16U) + 97U)
#define sfAccountIndex ((3U << 16U) + 98U)
#define sfAccountCount ((3U << 16U) + 99U)
#define sfRewardAccumulator ((3U << 16U) + 100U)
#define sfEmailHash ((4U << 16U) + 1U)
#define sfTakerPaysCurrency ((10U << 16U) + 1U)
#define sfTakerPaysIssuer ((10U << 16U) + 2U)
#define sfTakerGetsCurrency ((10U << 16U) + 3U)
#define sfTakerGetsIssuer ((10U << 16U) + 4U)
#define sfTakerPaysCurrency ((17U << 16U) + 1U)
#define sfTakerPaysIssuer ((17U << 16U) + 2U)
#define sfTakerGetsCurrency ((17U << 16U) + 3U)
#define sfTakerGetsIssuer ((17U << 16U) + 4U)
#define sfMPTokenIssuanceID ((21U << 16U) + 1U)
#define sfLedgerHash ((5U << 16U) + 1U)
#define sfParentHash ((5U << 16U) + 2U)
#define sfTransactionHash ((5U << 16U) + 3U)
@@ -99,6 +135,8 @@
#define sfEmitParentTxnID ((5U << 16U) + 11U)
#define sfEmitNonce ((5U << 16U) + 12U)
#define sfEmitHookHash ((5U << 16U) + 13U)
#define sfObjectID ((5U << 16U) + 14U)
#define sfAMMID ((5U << 16U) + 15U)
#define sfBookDirectory ((5U << 16U) + 16U)
#define sfInvoiceID ((5U << 16U) + 17U)
#define sfNickname ((5U << 16U) + 18U)
@@ -120,6 +158,18 @@
#define sfOfferID ((5U << 16U) + 34U)
#define sfEscrowID ((5U << 16U) + 35U)
#define sfURITokenID ((5U << 16U) + 36U)
#define sfDomainID ((5U << 16U) + 37U)
#define sfExportSignatureHash ((5U << 16U) + 38U)
#define sfExportCommitteeHash ((5U << 16U) + 39U)
#define sfManifestID ((5U << 16U) + 91U)
#define sfHookOnOutgoing ((5U << 16U) + 93U)
#define sfHookOnIncoming ((5U << 16U) + 94U)
#define sfCron ((5U << 16U) + 95U)
#define sfHookCanEmit ((5U << 16U) + 96U)
#define sfEmittedTxnID ((5U << 16U) + 97U)
#define sfGovernanceMarks ((5U << 16U) + 98U)
#define sfGovernanceFlags ((5U << 16U) + 99U)
#define sfNumber ((9U << 16U) + 1U)
#define sfAmount ((6U << 16U) + 1U)
#define sfBalance ((6U << 16U) + 2U)
#define sfLimitAmount ((6U << 16U) + 3U)
@@ -130,12 +180,27 @@
#define sfFee ((6U << 16U) + 8U)
#define sfSendMax ((6U << 16U) + 9U)
#define sfDeliverMin ((6U << 16U) + 10U)
#define sfAmount2 ((6U << 16U) + 11U)
#define sfBidMin ((6U << 16U) + 12U)
#define sfBidMax ((6U << 16U) + 13U)
#define sfMinimumOffer ((6U << 16U) + 16U)
#define sfRippleEscrow ((6U << 16U) + 17U)
#define sfDeliveredAmount ((6U << 16U) + 18U)
#define sfNFTokenBrokerFee ((6U << 16U) + 19U)
#define sfHookCallbackFee ((6U << 16U) + 20U)
#define sfLockedBalance ((6U << 16U) + 21U)
#define sfBaseFeeDrops ((6U << 16U) + 22U)
#define sfReserveBaseDrops ((6U << 16U) + 23U)
#define sfReserveIncrementDrops ((6U << 16U) + 24U)
#define sfLPTokenOut ((6U << 16U) + 25U)
#define sfLPTokenIn ((6U << 16U) + 26U)
#define sfEPrice ((6U << 16U) + 27U)
#define sfPrice ((6U << 16U) + 28U)
#define sfSignatureReward ((6U << 16U) + 29U)
#define sfMinAccountCreateAmount ((6U << 16U) + 30U)
#define sfLPTokenBalance ((6U << 16U) + 31U)
#define sfExportCallbackFee ((6U << 16U) + 32U)
#define sfTrustLineRewardAccumulator ((6U << 16U) + 99U)
#define sfPublicKey ((7U << 16U) + 1U)
#define sfMessageKey ((7U << 16U) + 2U)
#define sfSigningPubKey ((7U << 16U) + 3U)
@@ -161,6 +226,18 @@
#define sfHookParameterName ((7U << 16U) + 24U)
#define sfHookParameterValue ((7U << 16U) + 25U)
#define sfBlob ((7U << 16U) + 26U)
#define sfDIDDocument ((7U << 16U) + 27U)
#define sfData ((7U << 16U) + 28U)
#define sfAssetClass ((7U << 16U) + 29U)
#define sfProvider ((7U << 16U) + 30U)
#define sfMPTokenMetadata ((7U << 16U) + 31U)
#define sfCredentialType ((7U << 16U) + 32U)
#define sfEntropyContributors ((7U << 16U) + 33U)
#define sfExportCommittee ((7U << 16U) + 34U)
#define sfExportContributors ((7U << 16U) + 35U)
#define sfHookName ((7U << 16U) + 97U)
#define sfRemarkValue ((7U << 16U) + 98U)
#define sfRemarkName ((7U << 16U) + 99U)
#define sfAccount ((8U << 16U) + 1U)
#define sfOwner ((8U << 16U) + 2U)
#define sfDestination ((8U << 16U) + 3U)
@@ -170,13 +247,32 @@
#define sfRegularKey ((8U << 16U) + 8U)
#define sfNFTokenMinter ((8U << 16U) + 9U)
#define sfEmitCallback ((8U << 16U) + 10U)
#define sfHolder ((8U << 16U) + 11U)
#define sfHookAccount ((8U << 16U) + 16U)
#define sfOtherChainSource ((8U << 16U) + 18U)
#define sfOtherChainDestination ((8U << 16U) + 19U)
#define sfAttestationSignerAccount ((8U << 16U) + 20U)
#define sfAttestationRewardAccount ((8U << 16U) + 21U)
#define sfLockingChainDoor ((8U << 16U) + 22U)
#define sfIssuingChainDoor ((8U << 16U) + 23U)
#define sfSubject ((8U << 16U) + 24U)
#define sfInform ((8U << 16U) + 99U)
#define sfIndexes ((19U << 16U) + 1U)
#define sfHashes ((19U << 16U) + 2U)
#define sfAmendments ((19U << 16U) + 3U)
#define sfNFTokenOffers ((19U << 16U) + 4U)
#define sfHookNamespaces ((19U << 16U) + 5U)
#define sfCredentialIDs ((19U << 16U) + 6U)
#define sfURITokenIDs ((19U << 16U) + 99U)
#define sfPaths ((18U << 16U) + 1U)
#define sfBaseAsset ((26U << 16U) + 1U)
#define sfQuoteAsset ((26U << 16U) + 2U)
#define sfLockingChainIssue ((24U << 16U) + 1U)
#define sfIssuingChainIssue ((24U << 16U) + 2U)
#define sfAsset ((24U << 16U) + 3U)
#define sfAsset2 ((24U << 16U) + 4U)
#define sfClaimCurrency ((24U << 16U) + 5U)
#define sfXChainBridge ((25U << 16U) + 1U)
#define sfTransactionMetaData ((14U << 16U) + 2U)
#define sfCreatedNode ((14U << 16U) + 3U)
#define sfDeletedNode ((14U << 16U) + 4U)
@@ -191,13 +287,33 @@
#define sfEmitDetails ((14U << 16U) + 13U)
#define sfHook ((14U << 16U) + 14U)
#define sfSigner ((14U << 16U) + 16U)
#define sfExportSigner ((14U << 16U) + 17U)
#define sfMajority ((14U << 16U) + 18U)
#define sfDisabledValidator ((14U << 16U) + 19U)
#define sfEmittedTxn ((14U << 16U) + 20U)
#define sfHookExecution ((14U << 16U) + 21U)
#define sfHookDefinition ((14U << 16U) + 22U)
#define sfHookParameter ((14U << 16U) + 23U)
#define sfHookGrant ((14U << 16U) + 24U)
#define sfVoteEntry ((14U << 16U) + 25U)
#define sfAuctionSlot ((14U << 16U) + 26U)
#define sfAuthAccount ((14U << 16U) + 27U)
#define sfXChainClaimProofSig ((14U << 16U) + 28U)
#define sfXChainCreateAccountProofSig ((14U << 16U) + 29U)
#define sfXChainClaimAttestationCollectionElement ((14U << 16U) + 30U)
#define sfXChainCreateAccountAttestationCollectionElement ((14U << 16U) + 31U)
#define sfPriceData ((14U << 16U) + 32U)
#define sfCredential ((14U << 16U) + 33U)
#define sfExportedTxn ((14U << 16U) + 89U)
#define sfManifest ((14U << 16U) + 90U)
#define sfAmountEntry ((14U << 16U) + 91U)
#define sfMintURIToken ((14U << 16U) + 92U)
#define sfHookEmission ((14U << 16U) + 93U)
#define sfImportVLKey ((14U << 16U) + 94U)
#define sfActiveValidator ((14U << 16U) + 95U)
#define sfGenesisMint ((14U << 16U) + 96U)
#define sfRemark ((14U << 16U) + 97U)
#define sfHighReward ((14U << 16U) + 98U)
#define sfLowReward ((14U << 16U) + 99U)
#define sfSigners ((15U << 16U) + 3U)
#define sfSignerEntries ((15U << 16U) + 4U)
#define sfTemplate ((15U << 16U) + 5U)
@@ -207,9 +323,23 @@
#define sfMemos ((15U << 16U) + 9U)
#define sfNFTokens ((15U << 16U) + 10U)
#define sfHooks ((15U << 16U) + 11U)
#define sfVoteSlots ((15U << 16U) + 12U)
#define sfMajorities ((15U << 16U) + 16U)
#define sfDisabledValidators ((15U << 16U) + 17U)
#define sfHookExecutions ((15U << 16U) + 18U)
#define sfHookParameters ((15U << 16U) + 19U)
#define sfHookGrants ((15U << 16U) + 20U)
#define sfActiveValidators ((15U << 16U) + 95U)
#define sfXChainClaimAttestations ((15U << 16U) + 21U)
#define sfXChainCreateAccountAttestations ((15U << 16U) + 22U)
#define sfExportSigners ((15U << 16U) + 23U)
#define sfPriceDataSeries ((15U << 16U) + 24U)
#define sfAuthAccounts ((15U << 16U) + 25U)
#define sfAuthorizeCredentials ((15U << 16U) + 26U)
#define sfUnauthorizeCredentials ((15U << 16U) + 27U)
#define sfAcceptedCredentials ((15U << 16U) + 28U)
#define sfAmounts ((15U << 16U) + 92U)
#define sfHookEmissions ((15U << 16U) + 93U)
#define sfImportVLKeys ((15U << 16U) + 94U)
#define sfActiveValidators ((15U << 16U) + 95U)
#define sfGenesisMints ((15U << 16U) + 96U)
#define sfRemarks ((15U << 16U) + 97U)
+4
View File
@@ -56,6 +56,10 @@ enum ltURI_TOKEN {
enum remarks {
lsfImmutable = 1,
};
enum ltEXPORT_LATCH {
lsfExportXpopSeen = 0x00000001,
lsfExportCanceled = 0x00000002,
};
enum ltMPTOKEN_ISSUANCE {
lsfMPTLocked = 0x00000001,
lsfMPTCanLock = 0x00000002,
+9 -2
View File
@@ -145,6 +145,15 @@ int out_len = 0;\
#define SUB_OFFSET(x) ((int32_t)(x >> 32))
#define SUB_LENGTH(x) ((int32_t)(x & 0xFFFFFFFFULL))
#define ENTROPY_TIER(x) (((uint64_t)(x) >> 32U) & 0xFFU)
#define ENTROPY_COUNT(x) (((uint64_t)(x) >> 16U) & 0xFFFFU)
#define ENTROPY_DENOMINATOR(x) ((uint64_t)(x) & 0xFFFFU)
// Zero flags require the last eligible strong Hook. ANY includes SAME_ACCOUNT;
// both bits mean ANY. Allowed later Hooks retain ordinary veto authority.
#define ENTROPY_ALLOW_ANY_STRONG_VETO (1U << 0)
#define ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO (1U << 1)
#define BUFFER_EQUAL_20(buf1, buf2)\
(\
*(((uint64_t*)(buf1)) + 0) == *(((uint64_t*)(buf2)) + 0) &&\
@@ -377,5 +386,3 @@ int out_len = 0;\
#define amDELIVEREDAMOUNT 18U
#endif
+16
View File
@@ -9,6 +9,8 @@
#define sfUNLModifyDisabling ((16U << 16U) + 17U)
#define sfHookResult ((16U << 16U) + 18U)
#define sfWasLockingChainSend ((16U << 16U) + 19U)
#define sfSidecarType ((16U << 16U) + 20U)
#define sfEntropyTier ((16U << 16U) + 21U)
#define sfLedgerEntryType ((1U << 16U) + 1U)
#define sfTransactionType ((1U << 16U) + 2U)
#define sfSignerWeight ((1U << 16U) + 3U)
@@ -22,6 +24,10 @@
#define sfHookApiVersion ((1U << 16U) + 20U)
#define sfHookStateScale ((1U << 16U) + 21U)
#define sfLedgerFixType ((1U << 16U) + 22U)
#define sfHookExportCount ((1U << 16U) + 98U)
#define sfEntropyCount ((1U << 16U) + 99U)
#define sfEntropyDenominator ((1U << 16U) + 100U)
#define sfExportCount ((1U << 16U) + 101U)
#define sfNetworkID ((2U << 16U) + 1U)
#define sfFlags ((2U << 16U) + 2U)
#define sfSourceTag ((2U << 16U) + 3U)
@@ -105,6 +111,7 @@
#define sfMPTAmount ((3U << 16U) + 26U)
#define sfIssuerNode ((3U << 16U) + 27U)
#define sfSubjectNode ((3U << 16U) + 28U)
#define sfExportNode ((3U << 16U) + 29U)
#define sfTouchCount ((3U << 16U) + 97U)
#define sfAccountIndex ((3U << 16U) + 98U)
#define sfAccountCount ((3U << 16U) + 99U)
@@ -152,6 +159,8 @@
#define sfEscrowID ((5U << 16U) + 35U)
#define sfURITokenID ((5U << 16U) + 36U)
#define sfDomainID ((5U << 16U) + 37U)
#define sfExportSignatureHash ((5U << 16U) + 38U)
#define sfExportCommitteeHash ((5U << 16U) + 39U)
#define sfManifestID ((5U << 16U) + 91U)
#define sfHookOnOutgoing ((5U << 16U) + 93U)
#define sfHookOnIncoming ((5U << 16U) + 94U)
@@ -190,6 +199,7 @@
#define sfSignatureReward ((6U << 16U) + 29U)
#define sfMinAccountCreateAmount ((6U << 16U) + 30U)
#define sfLPTokenBalance ((6U << 16U) + 31U)
#define sfExportCallbackFee ((6U << 16U) + 32U)
#define sfTrustLineRewardAccumulator ((6U << 16U) + 99U)
#define sfPublicKey ((7U << 16U) + 1U)
#define sfMessageKey ((7U << 16U) + 2U)
@@ -222,6 +232,9 @@
#define sfProvider ((7U << 16U) + 30U)
#define sfMPTokenMetadata ((7U << 16U) + 31U)
#define sfCredentialType ((7U << 16U) + 32U)
#define sfEntropyContributors ((7U << 16U) + 33U)
#define sfExportCommittee ((7U << 16U) + 34U)
#define sfExportContributors ((7U << 16U) + 35U)
#define sfHookName ((7U << 16U) + 97U)
#define sfRemarkValue ((7U << 16U) + 98U)
#define sfRemarkName ((7U << 16U) + 99U)
@@ -274,6 +287,7 @@
#define sfEmitDetails ((14U << 16U) + 13U)
#define sfHook ((14U << 16U) + 14U)
#define sfSigner ((14U << 16U) + 16U)
#define sfExportSigner ((14U << 16U) + 17U)
#define sfMajority ((14U << 16U) + 18U)
#define sfDisabledValidator ((14U << 16U) + 19U)
#define sfEmittedTxn ((14U << 16U) + 20U)
@@ -289,6 +303,7 @@
#define sfXChainCreateAccountAttestationCollectionElement ((14U << 16U) + 31U)
#define sfPriceData ((14U << 16U) + 32U)
#define sfCredential ((14U << 16U) + 33U)
#define sfExportedTxn ((14U << 16U) + 89U)
#define sfManifest ((14U << 16U) + 90U)
#define sfAmountEntry ((14U << 16U) + 91U)
#define sfMintURIToken ((14U << 16U) + 92U)
@@ -316,6 +331,7 @@
#define sfHookGrants ((15U << 16U) + 20U)
#define sfXChainClaimAttestations ((15U << 16U) + 21U)
#define sfXChainCreateAccountAttestations ((15U << 16U) + 22U)
#define sfExportSigners ((15U << 16U) + 23U)
#define sfPriceDataSeries ((15U << 16U) + 24U)
#define sfAuthAccounts ((15U << 16U) + 25U)
#define sfAuthorizeCredentials ((15U << 16U) + 26U)
+29
View File
@@ -0,0 +1,29 @@
// Compile against the shipped SDK without hand-declaring host imports.
#include "../hookapi.h"
_Static_assert(ENTROPY_ALLOW_ANY_STRONG_VETO == 1, "ANY flag value");
_Static_assert(
ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO == 2,
"SAME_ACCOUNT flag value");
_Static_assert(LATER_STRONG_HOOK == -49, "composition refusal code");
int64_t
hook(uint32_t reserved)
{
_g(1, 1);
int64_t status = entropy_cr_status();
if (status < 0)
return rollback(0, 0, status);
uint8_t bytes[32];
for (uint32_t flags = 0; GUARD(4), flags < 4; ++flags)
{
int64_t draw = entropy_cr_dice(1, 3, flags);
if (draw != 0)
return rollback(0, 0, draw);
int64_t count = entropy_cr_random(SBUF(bytes), 3, flags);
if (count != sizeof(bytes))
return rollback(0, 0, count);
}
return accept(0, 0, ENTROPY_TIER(status));
}
+29
View File
@@ -0,0 +1,29 @@
// Compile-only check: shipped SDK declarations must match the runtime ABI,
// not just the prototype comments used by generate_extern.sh.
#include <cstdint>
#include <type_traits>
// Use the dependency-free guard-checker view of the host enum definitions.
#define GUARD_CHECKER_BUILD 1
#include <xrpl/hook/Enum.h>
constexpr auto hostAnyStrongVeto = hook_api::ENTROPY_ALLOW_ANY_STRONG_VETO;
constexpr auto hostSameAccountStrongVeto =
hook_api::ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO;
constexpr auto hostLaterStrongHook =
static_cast<int64_t>(hook_api::hook_return_code::LATER_STRONG_HOOK);
#include "../hookapi.h"
static_assert(ENTROPY_ALLOW_ANY_STRONG_VETO == hostAnyStrongVeto);
static_assert(
ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO == hostSameAccountStrongVeto);
static_assert(LATER_STRONG_HOOK == hostLaterStrongHook);
#define HOOK_API_DEFINITION( \
RETURN_TYPE, FUNCTION_NAME, PARAMS_TUPLE, AMENDMENT) \
static_assert( \
std::is_same_v<decltype(&FUNCTION_NAME), RETURN_TYPE(*) PARAMS_TUPLE>, \
"Hook SDK/runtime ABI mismatch: " #FUNCTION_NAME);
#include <xrpl/hook/hook_api.macro>
#undef HOOK_API_DEFINITION
+3
View File
@@ -61,6 +61,7 @@
#define ttNFTOKEN_MODIFY 70
#define ttPERMISSIONED_DOMAIN_SET 71
#define ttPERMISSIONED_DOMAIN_DELETE 72
#define ttEXPORT 90
#define ttMANIFEST_SET 91
#define ttCRON 92
#define ttCRON_SET 93
@@ -75,3 +76,5 @@
#define ttUNL_MODIFY 102
#define ttEMIT_FAILURE 103
#define ttUNL_REPORT 104
#define ttCONSENSUS_ENTROPY 105
#define ttEXPORT_SIGNATURES 106
+7
View File
@@ -115,3 +115,10 @@ enum AMMClawbackFlags : uint32_t {
enum BridgeModifyFlags : uint32_t {
tfClearAccountCreateAmount = 0x00010000,
};
enum ExportFlags : uint32_t {
// Lifecycle control: erase the named latch and forfeit any later callback.
tfExportEraseLatch = 0x00010000,
// Committee control: erase the named immutable committee object.
tfExportEraseCommittee = 0x00020000,
};
+13 -5
View File
@@ -15,6 +15,8 @@
#define uint256 std::string
#define featureHooksUpdate1 "1"
#define featureHooksUpdate2 "1"
#define featureExport "1"
#define featureConsensusEntropy "1"
#define fix20250131 "1"
#define fixGuardDepth32 "1"
namespace hook_api {
@@ -385,9 +387,16 @@ 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,
EXPORT_FAILURE = -46,
TOO_MANY_EXPORTED_TXN = -47,
TOO_LITTLE_ENTROPY = -48,
LATER_STRONG_HOOK = -49,
};
inline constexpr uint32_t ENTROPY_ALLOW_ANY_STRONG_VETO = 1U << 0;
inline constexpr uint32_t ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO = 1U << 1;
enum class ExitType : uint8_t {
UNSET = 0,
WASM_ERROR = 1,
@@ -399,6 +408,7 @@ const uint16_t max_state_modifications = 256;
const uint8_t max_slots = 255;
const uint8_t max_nonce = 255;
const uint8_t max_emit = 255;
const uint8_t max_export = 2;
const uint8_t max_params = 16;
const double fee_base_multiplier = 1.1f;
@@ -416,6 +426,7 @@ getImportWhitelist(Rules const& rules)
#undef HOOK_API_DEFINITION
#define int64_t 0x7EU
#define uint64_t 0x7EU
#define int32_t 0x7FU
#define uint32_t 0x7FU
@@ -432,6 +443,7 @@ getImportWhitelist(Rules const& rules)
#undef HOOK_API_DEFINITION
#undef HOOK_WRAP_PARAMS
#undef int64_t
#undef uint64_t
#undef int32_t
#undef uint32_t
#pragma pop_macro("HOOK_API_DEFINITION")
@@ -439,10 +451,6 @@ getImportWhitelist(Rules const& rules)
return whitelist;
}
#undef HOOK_API_DEFINITION
#undef I32
#undef I64
enum GuardRulesVersion : uint64_t {
GuardRuleFix20250131 = 0x00000001,
GuardRuleDepth32 = 0x00000002,
+38
View File
@@ -372,3 +372,41 @@ HOOK_API_DEFINITION(
HOOK_API_DEFINITION(
int64_t, prepare, (uint32_t, uint32_t, uint32_t, uint32_t),
featureHooksUpdate2)
// int64_t xport_reserve(uint32_t count);
HOOK_API_DEFINITION(
int64_t, xport_reserve, (uint32_t),
featureExport)
// int64_t xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t committee_hash_ptr, uint32_t committee_hash_len, uint64_t callback_fee_drops);
HOOK_API_DEFINITION(
int64_t, xport, (uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint64_t),
featureExport)
// int64_t xport_cancel(uint32_t read_ptr, uint32_t read_len, uint32_t flags);
HOOK_API_DEFINITION(
int64_t, xport_cancel, (uint32_t, uint32_t, uint32_t),
featureExport)
// flags=0 requires terminal strong execution; otherwise LATER_STRONG_HOOK
// is returned before generating randomness or changing output/counter state.
// ENTROPY_ALLOW_SAME_ACCOUNT_STRONG_VETO (bit 1) permits later strong Hooks
// only on the drawing Hook's account. ENTROPY_ALLOW_ANY_STRONG_VETO (bit 0)
// permits them on any account, including the same account. Both bits mean ANY.
// Allowed Hooks retain ordinary vetoes. Unknown bits are INVALID_ARGUMENT. All modes
// enforce min_tier/freshness. The drawing Hook can still fail; fair settlement
// requires committed inputs and must prevent retries selecting a new outcome.
// int64_t entropy_cr_dice(uint32_t sides, uint32_t min_tier, uint32_t flags);
HOOK_API_DEFINITION(
int64_t, entropy_cr_dice, (uint32_t, uint32_t, uint32_t),
featureConsensusEntropy)
// int64_t entropy_cr_random(uint32_t write_ptr, uint32_t write_len, uint32_t min_tier, uint32_t flags);
HOOK_API_DEFINITION(
int64_t, entropy_cr_random, (uint32_t, uint32_t, uint32_t, uint32_t),
featureConsensusEntropy)
// int64_t entropy_cr_status(void);
HOOK_API_DEFINITION(
int64_t, entropy_cr_status, (),
featureConsensusEntropy)
+2
View File
@@ -0,0 +1,2 @@
---
DisableFormat: true
+27 -2
View File
@@ -27,6 +27,7 @@ enum MessageType
mtREPLAY_DELTA_RESPONSE = 60;
mtHAVE_TRANSACTIONS = 63;
mtTRANSACTIONS = 64;
mtEXPORT_SHARES = 65;
}
// token, iterations, target, challenge = issue demand for proof of work
@@ -36,6 +37,7 @@ enum MessageType
//------------------------------------------------------------------------------
/* Provides the current ephemeral key for a validator. */
//@@start opaque-self-verifying-overlay-artifact-precedent
message TMManifest
{
// A Manifest object in the Ripple serialization format.
@@ -49,6 +51,7 @@ message TMManifests
// The manifests sent when a peer first connects to another peer are `history`.
optional bool history = 2 [deprecated=true];
}
//@@end opaque-self-verifying-overlay-artifact-precedent
//------------------------------------------------------------------------------
@@ -118,6 +121,15 @@ message TMTransactions
repeated TMTransaction transactions = 1;
}
// Canonical post-validation Export signature contributions. Receivers enforce
// ExportLimits caps for entry count, each frame, aggregate frame payload, and
// encoded protobuf size. The encoded-size cap includes protobuf tags and
// length prefixes but excludes the overlay message header.
message TMExportShares
{
repeated bytes shares = 1;
}
enum NodeStatus
{
@@ -153,7 +165,11 @@ message TMStatusChange
message TMProposeSet
{
required uint32 proposeSeq = 1;
required bytes currentTxHash = 2; // the hash of the ledger we are proposing
// Proposed transaction-set identity. Legacy/plain proposals carry the
// tx-set hash directly; ConsensusExtensions proposals carry a serialized
// ExtendedPosition whose first field is that tx-set hash, followed by
// signed RNG/Export sidecar fields.
required bytes currentTxHash = 2;
required bytes nodePubKey = 3;
required uint32 closeTime = 4;
required bytes signature = 5; // signature of above fields
@@ -166,6 +182,16 @@ message TMProposeSet
// Number of hops traveled
optional uint32 hops = 12 [deprecated=true];
//@@start proposal-export-share-container-binding
// Canonical post-validation ExportShare frames authored by the proposing
// validator and re-advertised for anti-entropy. The proposal's
// ExtendedPosition includes a digest of this repeated field, so the exact
// frames are covered by the proposal signature. Each frame carries its
// version, owner, origin transaction, origin ledger sequence and hash,
// committee position, signing key, and multisign signature.
repeated bytes exportSignatures = 13;
//@@end proposal-export-share-container-binding
}
enum TxSetStatus
@@ -384,4 +410,3 @@ message TMHaveTransactions
{
repeated bytes hashes = 1;
}
+61
View File
@@ -0,0 +1,61 @@
#ifndef RIPPLE_PROTOCOL_ENTROPY_TIER_H_INCLUDED
#define RIPPLE_PROTOCOL_ENTROPY_TIER_H_INCLUDED
#include <cstdint>
namespace ripple {
/// Which gate the ledger's entropy passed. Stored in sfEntropyTier (UINT8)
/// on the ttCONSENSUS_ENTROPY pseudo-transaction.
///
/// EntropyCount says how many validators contributed; EntropyDenominator says
/// how many active validators were in the ledger-anchored view for that
/// non-fallback result; EntropyTier says which gate the result passed. Fallback
/// entropy carries count=0/denominator=0 because no validator-derived
/// denominator was accepted. Tier values are strength-ordered so consumers can
/// gate with a numeric comparison (tier >= required).
///
/// RESIDUAL BIAS — applies to fallback, participant_aligned, and
/// validator_quorum. This is a commit/reveal scheme: a validator can withhold
/// its reveal until after observing peers' reveals, choosing between two
/// outcomes (its contribution in vs. out) — up to one bit of influence per
/// withholder, and colluding withholders near a threshold can instead force a
/// downgrade to a lower tier. These tiers bound and *label* manipulation (it is
/// observable and limited); they are NOT bias-resistant against a colluding
/// validator minority. A hook that requires validator_full fails closed if any
/// active validator withholds, trading availability for no selective-
/// withholding slack.
enum EntropyTier : std::uint8_t {
/// No usable entropy (reserved; a fresh ConsensusEntropy input should
/// always carry one of the tiers below).
entropyTierNone = 0,
/// Consensus-bound deterministic fallback: derived from already-agreed
/// round inputs (parent ledger hash, base tx set hash, sequence) under
/// HashPrefix::entropyFallback when no agreed reveal set reaches either
/// participant_aligned or validator_quorum. Unpredictable in practice but
/// user-influenceable via transaction submission — never suitable for
/// value-bearing outcomes.
entropyTierConsensusFallback = 1,
/// Participant-aligned sub-quorum entropy: the agreed reveal set aligned at
/// the tier-2 participant threshold — below the 80% validator quorum but at
/// or above the equivocation-intersection floor over the original
/// (pre-nUNL)
/// view. Weaker than validator_quorum; opt-in for hooks via min_tier.
entropyTierParticipantAligned = 2,
/// Validator commit/reveal entropy whose sidecar set passed the
/// active-validator-view quorum alignment gate.
entropyTierValidatorQuorum = 3,
/// Validator commit/reveal entropy with reveals from every validator in
/// the ledger-anchored active view. Any missing active validator downgrades
/// the tier, so hooks can require this to fail closed on selective
/// withholding.
entropyTierValidatorFull = 4,
};
} // namespace ripple
#endif
+131
View File
@@ -0,0 +1,131 @@
#ifndef RIPPLE_PROTOCOL_EXPORTCOMMITTEE_H_INCLUDED
#define RIPPLE_PROTOCOL_EXPORTCOMMITTEE_H_INCLUDED
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/UintTypes.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <vector>
namespace ripple {
/** One canonical, network-neutral Export validator committee. */
struct ExportCommitteeProfile
{
std::vector<PublicKey> members;
std::size_t quorum;
std::optional<std::uint16_t>
position(PublicKey const& master) const
{
auto const it =
std::lower_bound(members.begin(), members.end(), master);
if (it == members.end() || *it != master)
return std::nullopt;
return static_cast<std::uint16_t>(std::distance(members.begin(), it));
}
};
/** Parse a strictly sorted sequence of compressed validator master keys. */
inline std::optional<ExportCommitteeProfile>
resolveExportCommittee(Slice roster)
{
constexpr std::size_t publicKeyBytes = 33;
if (roster.empty() || roster.size() % publicKeyBytes != 0)
return std::nullopt;
auto const count = roster.size() / publicKeyBytes;
if (count == 0 || count > ExportLimits::maxCommitteeMembers)
return std::nullopt;
std::vector<PublicKey> members;
members.reserve(count);
for (std::size_t offset = 0; offset < roster.size();
offset += publicKeyBytes)
{
Slice const encoded{roster.data() + offset, publicKeyBytes};
if (!publicKeyType(encoded))
return std::nullopt;
PublicKey member{encoded};
if (!members.empty() && !(members.back() < member))
return std::nullopt;
members.push_back(std::move(member));
}
return ExportCommitteeProfile{
std::move(members), ExportLimits::committeeQuorumThreshold(count)};
}
inline Blob
serializeExportCommittee(std::vector<PublicKey> members)
{
if (members.empty() || members.size() > ExportLimits::maxCommitteeMembers)
return {};
std::sort(members.begin(), members.end());
if (std::adjacent_find(members.begin(), members.end()) != members.end())
return {};
Blob roster;
roster.reserve(members.size() * 33);
for (auto const& member : members)
roster.insert(roster.end(), member.begin(), member.end());
return roster;
}
/** Canonicalize an unordered transaction roster into strict stored form. */
inline std::optional<Blob>
canonicalizeExportCommittee(Slice roster)
{
constexpr std::size_t publicKeyBytes = 33;
if (roster.empty() || roster.size() % publicKeyBytes != 0)
return std::nullopt;
auto const count = roster.size() / publicKeyBytes;
if (count == 0 || count > ExportLimits::maxCommitteeMembers)
return std::nullopt;
std::vector<PublicKey> members;
members.reserve(count);
for (std::size_t offset = 0; offset < roster.size();
offset += publicKeyBytes)
{
Slice const encoded{roster.data() + offset, publicKeyBytes};
if (!publicKeyType(encoded))
return std::nullopt;
members.emplace_back(encoded);
}
auto canonical = serializeExportCommittee(std::move(members));
if (canonical.empty())
return std::nullopt;
return canonical;
}
/** Content identity of one already-validated canonical roster. */
inline uint256
exportCommitteeHash(Slice roster)
{
auto const profile = resolveExportCommittee(roster);
if (!profile)
return {};
Serializer serialized;
serialized.add32(HashPrefix::exportCommittee);
serialized.add32(static_cast<std::uint32_t>(profile->members.size()));
serialized.addRaw(roster);
return serialized.getSHA512Half();
}
} // namespace ripple
#endif
+119
View File
@@ -0,0 +1,119 @@
#ifndef RIPPLE_PROTOCOL_EXPORT_LIMITS_H_INCLUDED
#define RIPPLE_PROTOCOL_EXPORT_LIMITS_H_INCLUDED
#include <xrpl/protocol/ValidatorBitset.h>
#include <cstddef>
#include <cstdint>
namespace ripple {
// Export system caps.
//
// These limits bound the DoS surface of the export signature system:
// - Each selected validator signs once after exact source validation
// - Pending shares are re-advertised through bounded proposal/relay batches
// - Inbound signature processing involves crypto verification per sig
// - Durable and per-message caps bound scans, crypto, and sidecar leaves
struct ExportLimits
{
// Ordinary XRPL multisigning accepts at most 32 signers. An account that
// reserves a separate operator signer must select fewer validators.
static constexpr std::size_t maxCommitteeMembers = 32;
static constexpr std::size_t maxCommitteeRosterBytes =
maxCommitteeMembers * 33;
static constexpr std::size_t maxCommitteeContributorBytes =
validatorBitsetBytes(maxCommitteeMembers);
// Export-specific serialized payload bounds. The release target includes
// the canonical origin Memo appended after validation; the witness wraps
// that target plus the accepted committee signatures.
static constexpr std::size_t maxExportReleaseTargetBytes = 2'048;
static constexpr std::size_t maxExportWitnessBytes = 8'192;
// V1 witnesses require the standard 80% quorum of the intent-selected
// committee, rounded up. The intent selects members but cannot lower this
// threshold. Split quotient/remainder arithmetic avoids overflow while
// preserving ceil(memberCount * 0.8).
static constexpr std::size_t
committeeQuorumThreshold(std::size_t memberCount)
{
auto const quotient = memberCount / 5;
auto const remainder = memberCount % 5;
return quotient * 4 + (remainder * 4 + 4) / 5;
}
// Maximum exports a single hook execution may produce. Hook API ABI
// constant hook_api::max_export must stay equal.
static constexpr std::uint8_t maxExportsPerHook = 2;
// Maximum Export intents admitted in one ledger and maximum live latches
// owned by one account.
static constexpr std::uint8_t maxPendingExports = 8;
// Global pending-latch and per-validation scan bound. Witnessed or canceled
// latches release this signing-work slot while their account owner count
// and reserve continue to bound retained state. This is a provisional
// activation tuning value.
static constexpr std::uint16_t maxLiveExportLatches = 64;
// Maximum admission window requested through the mandatory outer
// LastLedgerSequence. This bounds how long an Export may remain queued
// before entering a ledger; it does not bound post-validation release.
static constexpr std::uint32_t maxAdmissionWindowLedgers = 5;
// Fixed source-ledger window for post-validation share publication and
// witness materialization, measured from the ledger that admits the
// intent. This is a provisional activation tuning value. Keeping it
// separate from the outer LastLedgerSequence prevents queue delay from
// consuming the publication window.
static constexpr std::uint32_t maxPublicationLedgers = 5;
// A fully-canonical secp256k1 signature is at most 72 bytes; Ed25519 is 64.
static constexpr std::size_t maxCanonicalExportSignatureBytes = 72;
// Export-signature sidecar leaves encode an origin, committee position,
// signing key, and signature in an STObject envelope. Keep this comfortably
// above the canonical encoding while bounding peer-supplied leaf bytes
// before parse/hash work.
static constexpr std::size_t maxExportSignatureSidecarBytes = 256;
// Post-validation relay framing. Values are deliberately conservative
// local tuning knobs and require measurement before activation; changing
// them does not change the canonical per-share format.
// version + AccountID + 2 hashes + ledger sequence + committee position +
// compressed public key + one-byte VL prefix + maximum signature.
static constexpr std::size_t maxSerializedExportShareBytes =
1 + 20 + 32 + 4 + 32 + 2 + 33 + 1 + maxCanonicalExportSignatureBytes;
static constexpr std::size_t maxExportSharesPerRelay = 32;
static constexpr std::size_t maxExportShareRelayPayloadBytes =
maxSerializedExportShareBytes * maxExportSharesPerRelay;
// Protobuf repeated-bytes framing is one tag byte plus a two-byte varint
// length for every maximum-size frame. This excludes the overlay header.
static constexpr std::size_t maxExportShareRelayMessageBytes =
maxExportShareRelayPayloadBytes + 3 * maxExportSharesPerRelay;
// Provisional activation fee schedule. These values intentionally price
// Export intents above ordinary transactions until production measurements
// can replace the conservative work and storage estimates.
//
// Witness bytes are estimated from the exact serialized target size, a
// fixed allowance for the witness envelope/origin stamp/contributor mask,
// and one bounded signer entry for every selected member. The accepted
// sidecar witness retains every valid committee contribution, not merely
// the minimum quorum subset.
static constexpr std::uint64_t feeWitnessFixedAllowanceBytes = 384;
static constexpr std::uint64_t feeWitnessSignerAllowanceBytes = 128;
static constexpr std::uint64_t feeSharePublicationRounds =
maxPublicationLedgers + 1;
static constexpr std::uint64_t feeWorkUnitsPerSharePublication = 1;
static constexpr std::uint64_t feeWorkUnitsPerWitnessSignature = 2;
static constexpr std::uint64_t feeWitnessChunkBytes = 256;
static constexpr std::uint64_t feeWorkUnitsPerWitnessChunk = 1;
static constexpr std::uint64_t feePermanentWitnessByteDrops = 1;
static constexpr std::uint64_t feeSurchargeRoundDrops = 1'000;
};
} // namespace ripple
#endif
+104
View File
@@ -0,0 +1,104 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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_EXPORTORIGINMEMO_H_INCLUDED
#define RIPPLE_PROTOCOL_EXPORTORIGINMEMO_H_INCLUDED
#include <xrpl/basics/Expected.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/UintTypes.h>
#include <cstdint>
#include <optional>
#include <string_view>
namespace ripple::ExportOriginMemo {
inline constexpr std::string_view memoType = "xahau/export";
inline constexpr std::uint8_t version = 1;
inline constexpr std::uint8_t allowedFlags = 0;
inline constexpr std::size_t identityBytes = 42;
inline constexpr std::size_t releaseBytes = 78;
struct Origin
{
std::uint32_t sourceDomain;
std::uint32_t targetDomain;
uint256 transactionHash;
bool
operator==(Origin const&) const = default;
};
struct Anchor
{
LedgerIndex ledgerSequence;
uint256 ledgerHash;
bool
operator==(Anchor const&) const = default;
};
struct Stamp
{
Origin origin;
std::optional<Anchor> anchor;
bool
operator==(Stamp const&) const = default;
};
enum class Error {
reservedMemoPresent,
reservedMemoMissing,
reservedMemoPosition,
malformedMemo,
unsupportedVersion,
unsupportedFlags,
localChecks
};
/** Return true when any Memo reserves the Export protocol MemoType. */
bool
hasReservedMemo(STTx const& tx);
/** Append the canonical identity projection as the final Memo.
Existing Memos remain byte-identical and in their original order. This
function does not normalize transaction authorization fields; callers must
supply the canonical unsigned multisign base.
*/
Expected<STTx, Error>
identityForm(STTx const& base, Origin const& origin);
/** Append the canonical release projection as the final Memo. */
Expected<STTx, Error>
releaseForm(STTx const& base, Origin const& origin, Anchor const& anchor);
/** Parse one canonical final Export Memo. */
Expected<Stamp, Error>
parse(STTx const& tx);
/** Replace a canonical release projection with its identity projection. */
Expected<STTx, Error>
projectIdentity(STTx const& stamped);
} // namespace ripple::ExportOriginMemo
#endif
+142
View File
@@ -0,0 +1,142 @@
#ifndef RIPPLE_PROTOCOL_EXPORTSHARE_H_INCLUDED
#define RIPPLE_PROTOCOL_EXPORTSHARE_H_INCLUDED
#include <xrpl/basics/Buffer.h>
#include <xrpl/protocol/AccountID.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/digest.h>
#include <cstdint>
#include <cstring>
#include <optional>
#include <stdexcept>
namespace ripple {
/** Canonical post-validation Export signature contribution.
The multisign signature authenticates the derived destination transaction.
The remaining fields are bounded lookup and release context which live
admission checks against the origin latch and local validated chain.
*/
struct ExportShare
{
static constexpr std::uint8_t currentVersion = 1;
std::uint8_t version{currentVersion};
AccountID owner;
uint256 originTxn;
LedgerIndex originLedgerSeq{0};
uint256 originLedgerHash;
std::uint16_t committeePosition{0};
PublicKey signingKey;
Buffer signature;
bool
hasCanonicalSignature() const
{
auto const keyType = publicKeyType(signingKey);
if (!keyType)
return false;
auto const sig = Slice{signature.data(), signature.size()};
if (*keyType == KeyType::secp256k1)
{
auto const canonicality = ecdsaCanonicality(sig);
return canonicality &&
*canonicality == ECDSACanonicality::fullyCanonical;
}
// Full Ed25519 canonicality is enforced by cryptographic admission.
// The framing layer can still reject every impossible wire length.
return *keyType == KeyType::ed25519 && signature.size() == 64;
}
bool
validShape() const
{
return version == currentVersion && owner != beast::zero &&
!originTxn.isZero() && originLedgerSeq != 0 &&
!originLedgerHash.isZero() &&
committeePosition < ExportLimits::maxCommitteeMembers &&
hasCanonicalSignature();
}
Serializer
serialize() const
{
if (!validShape())
throw std::invalid_argument("invalid ExportShare shape");
Serializer result;
result.add8(version);
result.addBitString(owner);
result.addBitString(originTxn);
result.add32(originLedgerSeq);
result.addBitString(originLedgerHash);
result.add16(committeePosition);
result.addRaw(signingKey.slice());
result.addVL(signature);
return result;
}
uint256
wireHash() const
{
// Raw-wire suppression only. Routing context such as committeePosition
// is checked against validated state before relay and is not
// authenticated by the destination multisignature.
auto const bytes = serialize();
return sha512Half(bytes.slice());
}
static std::optional<ExportShare>
parse(Slice bytes)
{
if (bytes.empty() ||
bytes.size() > ExportLimits::maxSerializedExportShareBytes)
return std::nullopt;
try
{
SerialIter sit{bytes};
auto const version = sit.get8();
auto const owner = sit.getBitString<160, detail::AccountIDTag>();
auto const originTxn = sit.get256();
auto const originLedgerSeq = sit.get32();
auto const originLedgerHash = sit.get256();
auto const committeePosition = sit.get16();
auto const keySlice = sit.getSlice(33);
if (!publicKeyType(keySlice))
return std::nullopt;
PublicKey const signingKey{keySlice};
auto signature = sit.getVLBuffer();
if (!sit.empty())
return std::nullopt;
ExportShare result{
version,
owner,
originTxn,
originLedgerSeq,
originLedgerHash,
committeePosition,
signingKey,
std::move(signature)};
if (!result.validShape())
return std::nullopt;
return result;
}
catch (std::exception const&)
{
return std::nullopt;
}
}
};
} // namespace ripple
#endif
+18
View File
@@ -96,6 +96,24 @@ enum class HashPrefix : std::uint32_t {
/** Credentials signature */
credential = detail::make_hash_prefix('C', 'R', 'D'),
/** consensus extension sidecar object */
sidecar = detail::make_hash_prefix('S', 'C', 'R'),
/** consensus diagnostic observed participant set */
observedParticipants = detail::make_hash_prefix('O', 'B', 'P'),
/** consensus-bound fallback entropy digest (Tier 1: derived from
already-agreed round inputs when no agreed reveal set reaches an
accepted validator-participant tier; never to be confused with
validator entropy) */
entropyFallback = detail::make_hash_prefix('E', 'F', 'B'),
/** consensus entropy transaction-ordering salt */
entropyTxnOrder = detail::make_hash_prefix('E', 'T', 'O'),
/** immutable Export committee roster */
exportCommittee = detail::make_hash_prefix('E', 'C', 'M'),
};
template <class Hasher>
+9
View File
@@ -454,6 +454,15 @@ getVLInfo(Json::Value const& xpop, beast::Journal const& j)
<< "Import: unl blob was not valid json (after base64 decoding)";
return {};
}
auto const isNonNegativeUInt = [](Json::Value const& value) {
return value.isUInt() || (value.isInt() && value.asInt() >= 0);
};
if (!list.isMember(jss::sequence) ||
!isNonNegativeUInt(list[jss::sequence]))
{
JLOG(j.warn()) << "Import: unl blob sequence was missing or negative";
return {};
}
auto const sequence = list[jss::sequence].asUInt();
auto const m = deserializeManifest(base64_decode(
xpop[jss::validation][jss::unl][jss::manifest].asString()));
+16
View File
@@ -62,6 +62,22 @@ emittedDir() noexcept;
Keylet
emittedTxn(uint256 const& id) noexcept;
/** Locate an Export latch by its permanent source issuance identity.
The source Export transaction ID is content-addressed and cannot be reused,
including after AccountDelete.
*/
Keylet
exportLatch(AccountID const& account, uint256 const& originTxnHash) noexcept;
/** Locate an immutable Export committee owned by an account. */
Keylet
exportCommittee(AccountID const& account, uint256 const& digest) noexcept;
/** The fixed global directory of Export latches awaiting witness work. */
Keylet const&
pendingExports() noexcept;
Keylet
hookDefinition(uint256 const& hash) noexcept;
+4
View File
@@ -186,6 +186,10 @@ enum LedgerSpecificFlags {
// remarks
lsfImmutable = 1,
// ltEXPORT_LATCH
lsfExportXpopSeen = 0x00000001,
lsfExportCanceled = 0x00000002,
// ltMPTOKEN_ISSUANCE
lsfMPTLocked = 0x00000001, // Also used in ltMPTOKEN
lsfMPTCanLock = 0x00000002,
+13
View File
@@ -43,6 +43,19 @@ std::size_t constexpr txMinSizeBytes = 10;
/** Largest legal byte size of a transaction. */
std::size_t constexpr txMaxSizeBytes = megabytes(1);
/** Largest network ID whose transactions retain the legacy canonical form.
Transactions on these networks must omit sfNetworkID. Transactions on
networks with larger IDs must include sfNetworkID matching the network.
*/
std::uint32_t constexpr maxNetworkIDWithoutTxField = 1024;
constexpr bool
requiresTxNetworkID(std::uint32_t networkID)
{
return networkID > maxNetworkIDWithoutTxField;
}
/** The maximum number of unfunded offers to delete at once */
std::size_t constexpr unfundedOfferRemoveLimit = 1000;
+21
View File
@@ -0,0 +1,21 @@
#ifndef RIPPLE_PROTOCOL_SIDECAR_TYPE_H_INCLUDED
#define RIPPLE_PROTOCOL_SIDECAR_TYPE_H_INCLUDED
#include <cstdint>
namespace ripple {
/// Discriminator for sidecar set entries (SHAMap leaves used for
/// consensus extension data: RNG commit/reveal, export signatures).
///
/// Stored in sfSidecarType (UINT8) on each STObject entry.
/// Makes sidecar sets self-describing — no content-sniffing needed.
enum SidecarType : std::uint8_t {
sidecarRngCommit = 1,
sidecarRngReveal = 2,
sidecarExportSig = 3,
};
} // namespace ripple
#endif
+4
View File
@@ -68,7 +68,10 @@ enum TELcodes : TERUnderlyingType {
telNON_LOCAL_EMITTED_TXN,
telIMPORT_VL_KEY_NOT_RECOGNISED,
telCAN_NOT_QUEUE_IMPORT,
// Keep test-only sentinels before appended branch-local TEL codes so their
// numeric values do not move.
telENV_RPC_FAILED,
telEXPORT_LATCH_REQUIRED,
};
//------------------------------------------------------------------------------
@@ -365,6 +368,7 @@ enum TECcodes : TERUnderlyingType {
tecARRAY_TOO_LARGE = 197,
tecLOCKED = 198,
tecBAD_CREDENTIALS = 199,
tecEXPORT_COMMITTEE_UNAVAILABLE = 201,
tecLAST_POSSIBLE_ENTRY = 255,
};
+11
View File
@@ -274,6 +274,17 @@ enum BridgeModifyFlags : uint32_t {
tfClearAccountCreateAmount = 0x00010000,
};
constexpr std::uint32_t tfBridgeModifyMask = ~(tfUniversal | tfClearAccountCreateAmount);
// Export flags:
enum ExportFlags : uint32_t {
// Lifecycle control: erase the named latch and forfeit any later callback.
tfExportEraseLatch = 0x00010000,
// Committee control: erase the named immutable committee object.
tfExportEraseCommittee = 0x00020000,
};
constexpr std::uint32_t tfExportMask =
~(tfUniversal | tfExportEraseLatch | tfExportEraseCommittee);
// clang-format on
} // namespace ripple
-1
View File
@@ -176,7 +176,6 @@ private:
std::optional<STAmount> mDelivered;
std::optional<STArray> mHookExecutions;
std::optional<STArray> mHookEmissions;
STArray mNodes;
};
+158
View File
@@ -0,0 +1,158 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright 2026 Xahau
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_VALIDATORBITSET_H_INCLUDED
#define RIPPLE_PROTOCOL_VALIDATORBITSET_H_INCLUDED
#include <xrpl/basics/Blob.h>
#include <xrpl/basics/Slice.h>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <utility>
namespace ripple {
class ValidatedValidatorBitset
{
Blob bitset_;
std::size_t memberCount_ = 0;
std::size_t selected_ = 0;
ValidatedValidatorBitset(
Slice bitset,
std::size_t memberCount,
std::size_t selected)
: bitset_(bitset.begin(), bitset.end())
, memberCount_(memberCount)
, selected_(selected)
{
}
public:
ValidatedValidatorBitset(ValidatedValidatorBitset const&) = default;
ValidatedValidatorBitset&
operator=(ValidatedValidatorBitset const&) = default;
ValidatedValidatorBitset(ValidatedValidatorBitset&& other) noexcept
: bitset_(std::move(other.bitset_))
, memberCount_(other.memberCount_)
, selected_(other.selected_)
{
other.bitset_.clear();
other.memberCount_ = 0;
other.selected_ = 0;
}
ValidatedValidatorBitset&
operator=(ValidatedValidatorBitset&& other) noexcept
{
if (this != &other)
{
bitset_ = std::move(other.bitset_);
memberCount_ = other.memberCount_;
selected_ = other.selected_;
other.bitset_.clear();
other.memberCount_ = 0;
other.selected_ = 0;
}
return *this;
}
/** Construct an owning, validated validator bitset. */
static std::optional<ValidatedValidatorBitset>
make(Slice bitset, std::size_t memberCount);
std::size_t
selected() const
{
return selected_;
}
bool
contains(std::size_t position) const
{
if (position >= memberCount_)
return false;
auto const byte = position / 8;
return (bitset_[byte] &
static_cast<std::uint8_t>(1u << (position % 8))) != 0;
}
};
constexpr std::size_t
validatorBitsetBytes(std::size_t memberCount)
{
return memberCount / 8 + (memberCount % 8 != 0);
}
/** Validate an LSB-first bitset over a fixed canonical validator universe.
A valid bitset has exactly enough bytes for the universe and leaves every
unused high bit in the final byte clear. The returned population can be
used by a caller's profile-specific admission and quorum rules.
*/
inline std::optional<ValidatedValidatorBitset>
validateValidatorBitset(Slice bitset, std::size_t memberCount)
{
return ValidatedValidatorBitset::make(bitset, memberCount);
}
inline std::optional<ValidatedValidatorBitset>
ValidatedValidatorBitset::make(Slice bitset, std::size_t memberCount)
{
if (bitset.size() != validatorBitsetBytes(memberCount))
return std::nullopt;
auto const remainder = memberCount % 8;
if (remainder != 0)
{
auto const allowed = static_cast<std::uint8_t>((1u << remainder) - 1u);
if ((bitset[bitset.size() - 1] & static_cast<std::uint8_t>(~allowed)) !=
0)
return std::nullopt;
}
std::size_t selected = 0;
for (auto const byte : bitset)
selected += std::popcount(byte);
return ValidatedValidatorBitset{bitset, memberCount, selected};
}
template <class Predicate>
Blob
makeValidatorBitset(std::size_t memberCount, Predicate&& selected)
{
Blob bitset(validatorBitsetBytes(memberCount), 0);
for (std::size_t i = 0; i < memberCount; ++i)
{
if (selected(i))
bitset[i / 8] |= static_cast<std::uint8_t>(1u << (i % 8));
}
return bitset;
}
} // namespace ripple
#endif
+2 -1
View File
@@ -70,7 +70,8 @@ XRPL_FEATURE(XChainBridge, Supported::no, VoteBehavior::DefaultNo
XRPL_FEATURE(AMM, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (ReducedOffersV1, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FEATURE(HooksUpdate2, Supported::yes, VoteBehavior::DefaultNo)
// replaced to HookOnV2_1
XRPL_FEATURE(Export, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(ConsensusEntropy, Supported::yes, VoteBehavior::DefaultNo)
XRPL_FEATURE(HookOnV2, Supported::no, VoteBehavior::DefaultNo)
XRPL_FIX (HookAPI20251128, Supported::yes, VoteBehavior::DefaultYes)
XRPL_FIX (CronStacking, Supported::yes, VoteBehavior::DefaultYes)
@@ -257,11 +257,14 @@ LEDGER_ENTRY(ltACCOUNT_ROOT, 0x0061, AccountRoot, account, ({
{sfImportSequence, soeOPTIONAL},
{sfGovernanceFlags, soeOPTIONAL},
{sfGovernanceMarks, soeOPTIONAL},
//@@start account-index-optional-accountroot-field
{sfAccountIndex, soeOPTIONAL},
//@@end account-index-optional-accountroot-field
{sfTouchCount, soeOPTIONAL},
{sfHookStateScale, soeOPTIONAL},
{sfCron, soeOPTIONAL},
{sfAMMID, soeOPTIONAL},
{sfExportCount, soeOPTIONAL},
{sfManifestID, soeOPTIONAL},
}))
@@ -278,6 +281,7 @@ LEDGER_ENTRY(ltDIR_NODE, 0x0064, DirectoryNode, directory, ({
{sfTakerGetsIssuer, soeOPTIONAL}, // order book directories
{sfExchangeRate, soeOPTIONAL}, // order book directories
{sfReferenceCount, soeOPTIONAL}, // for hook state directories
{sfExportCount, soeOPTIONAL}, // live latches on Export root
{sfIndexes, soeREQUIRED},
{sfRootIndex, soeREQUIRED},
{sfIndexNext, soeOPTIONAL},
@@ -418,7 +422,9 @@ LEDGER_ENTRY(ltFEE_SETTINGS, 0x0073, FeeSettings, fee, ({
{sfReserveBaseDrops, soeOPTIONAL},
{sfReserveIncrementDrops, soeOPTIONAL},
{sfXahauActivationLgrSeq, soeOPTIONAL},
//@@start account-count-optional-fees-field
{sfAccountCount, soeOPTIONAL},
//@@end account-count-optional-fees-field
{sfNetworkID, soeOPTIONAL},
{sfPreviousTxnID, soeOPTIONAL},
{sfPreviousTxnLgrSeq, soeOPTIONAL},
@@ -593,6 +599,39 @@ LEDGER_ENTRY(ltDID, 0x008D, DID, did, ({
{sfPreviousTxnLgrSeq, soeREQUIRED},
}))
//@@start export-latch-ledger-entry
/** A live lifecycle latch for an Export intent.
Created when an Export intent is admitted. It records witness and
proof-of-execution arrival in either order and is erased once both facts
exist. Account-owned (pays reserve).
sfDigest stores the canonical target signing-intent hash.
\sa keylet::exportLatch
*/
LEDGER_ENTRY(ltEXPORT_LATCH, 0x5374, ExportLatch, export_latch, ({
{sfAccount, soeREQUIRED},
{sfExportCallbackFee, soeOPTIONAL},
{sfTicketSequence, soeREQUIRED},
{sfTransactionHash, soeOPTIONAL},
{sfDigest, soeREQUIRED},
{sfLedgerSequence, soeREQUIRED},
{sfExportCommitteeHash, soeREQUIRED},
{sfExportSignatureHash, soeOPTIONAL},
{sfLastLedgerSequence, soeOPTIONAL},
{sfOwnerNode, soeREQUIRED},
{sfExportNode, soeOPTIONAL},
}))
//@@end export-latch-ledger-entry
/** An immutable, account-owned Export validator committee. */
LEDGER_ENTRY(ltEXPORT_COMMITTEE, 0x4563, ExportCommittee, export_committee, ({
{sfAccount, soeREQUIRED},
{sfExportCommitteeHash, soeREQUIRED},
{sfExportCommittee, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},
}))
LEDGER_ENTRY(ltMANIFEST, 0x004D, Manifest, manifest_entry, ({
{sfAccount, soeREQUIRED},
{sfPublicKey, soeREQUIRED},
@@ -615,4 +654,3 @@ LEDGER_ENTRY(ltMANIFEST, 0x004D, Manifest, manifest_entry, ({
#undef EXPAND
#undef LEDGER_ENTRY_DUPLICATE
+16 -2
View File
@@ -42,6 +42,8 @@ TYPED_SFIELD(sfTickSize, UINT8, 16)
TYPED_SFIELD(sfUNLModifyDisabling, UINT8, 17)
TYPED_SFIELD(sfHookResult, UINT8, 18)
TYPED_SFIELD(sfWasLockingChainSend, UINT8, 19)
TYPED_SFIELD(sfSidecarType, UINT8, 20)
TYPED_SFIELD(sfEntropyTier, UINT8, 21)
// 16-bit integers (common)
TYPED_SFIELD(sfLedgerEntryType, UINT16, 1, SField::sMD_Never)
@@ -59,6 +61,10 @@ TYPED_SFIELD(sfHookExecutionIndex, UINT16, 19)
TYPED_SFIELD(sfHookApiVersion, UINT16, 20)
TYPED_SFIELD(sfHookStateScale, UINT16, 21)
TYPED_SFIELD(sfLedgerFixType, UINT16, 22)
TYPED_SFIELD(sfHookExportCount, UINT16, 98)
TYPED_SFIELD(sfEntropyCount, UINT16, 99)
TYPED_SFIELD(sfEntropyDenominator, UINT16, 100)
TYPED_SFIELD(sfExportCount, UINT16, 101)
// 32-bit integers (common)
TYPED_SFIELD(sfNetworkID, UINT32, 1)
@@ -153,6 +159,7 @@ 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)
TYPED_SFIELD(sfExportNode, UINT64, 29)
TYPED_SFIELD(sfTouchCount, UINT64, 97)
TYPED_SFIELD(sfAccountIndex, UINT64, 98)
TYPED_SFIELD(sfAccountCount, UINT64, 99)
@@ -210,6 +217,8 @@ TYPED_SFIELD(sfOfferID, UINT256, 34)
TYPED_SFIELD(sfEscrowID, UINT256, 35)
TYPED_SFIELD(sfURITokenID, UINT256, 36)
TYPED_SFIELD(sfDomainID, UINT256, 37)
TYPED_SFIELD(sfExportSignatureHash, UINT256, 38)
TYPED_SFIELD(sfExportCommitteeHash, UINT256, 39)
TYPED_SFIELD(sfManifestID, UINT256, 91)
TYPED_SFIELD(sfHookOnOutgoing, UINT256, 93)
TYPED_SFIELD(sfHookOnIncoming, UINT256, 94)
@@ -258,6 +267,7 @@ TYPED_SFIELD(sfPrice, AMOUNT, 28)
TYPED_SFIELD(sfSignatureReward, AMOUNT, 29)
TYPED_SFIELD(sfMinAccountCreateAmount, AMOUNT, 30)
TYPED_SFIELD(sfLPTokenBalance, AMOUNT, 31)
TYPED_SFIELD(sfExportCallbackFee, AMOUNT, 32)
TYPED_SFIELD(sfTrustLineRewardAccumulator,AMOUNT, 99)
// variable length (common)
@@ -294,6 +304,9 @@ TYPED_SFIELD(sfAssetClass, VL, 29)
TYPED_SFIELD(sfProvider, VL, 30)
TYPED_SFIELD(sfMPTokenMetadata, VL, 31)
TYPED_SFIELD(sfCredentialType, VL, 32)
TYPED_SFIELD(sfEntropyContributors, VL, 33)
TYPED_SFIELD(sfExportCommittee, VL, 34)
TYPED_SFIELD(sfExportContributors, VL, 35)
TYPED_SFIELD(sfHookName, VL, 97)
TYPED_SFIELD(sfRemarkValue, VL, 98)
TYPED_SFIELD(sfRemarkName, VL, 99)
@@ -366,7 +379,7 @@ UNTYPED_SFIELD(sfHook, OBJECT, 14)
// inner object (uncommon)
UNTYPED_SFIELD(sfSigner, OBJECT, 16)
// 17 unused
UNTYPED_SFIELD(sfExportSigner, OBJECT, 17)
UNTYPED_SFIELD(sfMajority, OBJECT, 18)
UNTYPED_SFIELD(sfDisabledValidator, OBJECT, 19)
UNTYPED_SFIELD(sfEmittedTxn, OBJECT, 20)
@@ -383,6 +396,7 @@ UNTYPED_SFIELD(sfXChainClaimAttestationCollectionElement, OBJECT, 30)
UNTYPED_SFIELD(sfXChainCreateAccountAttestationCollectionElement, OBJECT, 31)
UNTYPED_SFIELD(sfPriceData, OBJECT, 32)
UNTYPED_SFIELD(sfCredential, OBJECT, 33)
UNTYPED_SFIELD(sfExportedTxn, OBJECT, 89)
UNTYPED_SFIELD(sfManifest, OBJECT, 90)
UNTYPED_SFIELD(sfAmountEntry, OBJECT, 91)
UNTYPED_SFIELD(sfMintURIToken, OBJECT, 92)
@@ -417,7 +431,7 @@ UNTYPED_SFIELD(sfHookParameters, ARRAY, 19)
UNTYPED_SFIELD(sfHookGrants, ARRAY, 20)
UNTYPED_SFIELD(sfXChainClaimAttestations, ARRAY, 21)
UNTYPED_SFIELD(sfXChainCreateAccountAttestations, ARRAY, 22)
// 23 unused
UNTYPED_SFIELD(sfExportSigners, ARRAY, 23)
UNTYPED_SFIELD(sfPriceDataSeries, ARRAY, 24)
UNTYPED_SFIELD(sfAuthAccounts, ARRAY, 25)
UNTYPED_SFIELD(sfAuthorizeCredentials, ARRAY, 26)
@@ -500,6 +500,17 @@ TRANSACTION(ttPERMISSIONED_DOMAIN_DELETE, 72, PermissionedDomainDelete, ({
{sfDomainID, soeREQUIRED},
}))
//@@start export-transaction-types
/* User-submittable Export. Its mutually exclusive forms create an intent,
control an existing latch, or manage an immutable committee object. */
TRANSACTION(ttEXPORT, 90, Export, ({
{sfExportedTxn, soeOPTIONAL},
{sfExportCallbackFee, soeOPTIONAL},
{sfTransactionHash, soeOPTIONAL},
{sfExportCommitteeHash, soeOPTIONAL},
{sfExportCommittee, soeOPTIONAL},
}))
//@@end export-transaction-types
/* sfAccount is supplied by TxFormats::commonFields; listing it here as well
makes the SOTemplate reject the format at construction. */
TRANSACTION(ttMANIFEST_SET, 91, SetManifest, ({
@@ -612,3 +623,20 @@ TRANSACTION(ttUNL_REPORT, 104, UNLReport, ({
{sfActiveValidator, soeOPTIONAL},
{sfImportVLKey, soeOPTIONAL},
}))
TRANSACTION(ttCONSENSUS_ENTROPY, 105, ConsensusEntropy, ({
{sfLedgerSequence, soeREQUIRED},
{sfDigest, soeREQUIRED},
{sfEntropyCount, soeREQUIRED},
{sfEntropyDenominator, soeREQUIRED},
{sfEntropyContributors, soeREQUIRED},
{sfEntropyTier, soeREQUIRED},
}))
TRANSACTION(ttEXPORT_SIGNATURES, 106, ExportSignatures, ({
{sfLedgerSequence, soeREQUIRED},
{sfTransactionHash, soeREQUIRED},
{sfExportedTxn, soeREQUIRED},
{sfExportContributors, soeREQUIRED},
{sfExportSigners, soeREQUIRED},
}))
+5
View File
@@ -534,6 +534,9 @@ JSS(open_ledger_fee); // out: TxQ
JSS(open_ledger_level); // out: TxQ
JSS(oracles); // in: get_aggregate_price
JSS(oracle_document_id); // in: get_aggregate_price
JSS(origin_ledger_hash); // out: NetworkOPs
JSS(origin_ledger_seq); // out: NetworkOPs
JSS(origin_txid); // out: NetworkOPs
JSS(owner); // in: LedgerEntry, out: NetworkOPs
JSS(owner_funds); // in/out: Ledger, NetworkOPs, AcceptedLedgerTx
JSS(page_index);
@@ -655,6 +658,7 @@ JSS(state_now); // in: Subscribe
JSS(status); // error
JSS(stop); // in: LedgerCleaner
JSS(stop_history_tx_only); // in: Unsubscribe, stop history tx stream
JSS(stream); // out: NetworkOPs
JSS(streams); // in: Subscribe, Unsubscribe
JSS(strict); // in: AccountCurrencies, AccountInfo
JSS(sub_index); // in: LedgerEntry
@@ -736,6 +740,7 @@ JSS(TRANSACTION_FLAGS); // out: RPC server_definitions
JSS(TRANSACTION_FLAGS_INDICES); // out: RPC server_definitions
JSS(type_hex); // out: STPathSet
JSS(unl); // out: UnlList
JSS(committee_position); // out: NetworkOPs
JSS(unlimited); // out: Connection.h
JSS(uptime); // out: GetCounts
JSS(uri); // out: ValidatorSites
+13 -3
View File
@@ -31,6 +31,7 @@
#include <cassert>
#include <cstring>
#include <ctime>
#include <exception>
#include <fstream>
#include <functional>
#include <iostream>
@@ -351,9 +352,18 @@ Logs::format(
if (useLocalTime)
{
auto now = std::chrono::system_clock::now();
auto local = date::make_zoned(date::current_zone(), now);
output = date::format(fmt, local);
try
{
auto now = std::chrono::system_clock::now();
auto local = date::make_zoned(date::current_zone(), now);
output = date::format(fmt, local);
}
catch (std::exception const&)
{
// Enhanced logging should not make startup fatal if tzdb lookup is
// unavailable or misconfigured. Fall back to UTC formatting.
output = date::format(fmt, std::chrono::system_clock::now());
}
}
else
{
+2 -2
View File
@@ -56,9 +56,9 @@ getVersionString()
(versionString == std::string("0.") + std::string("0.0") ||
versionString == std::string("0.0.0+DEBUG")))
{
std::string y = std::string(__DATE__ + 7);
std::string y = std::string(&__DATE__[7]);
std::string d = std::string(
__DATE__ + 4 + (__DATE__[4] == ' ' ? 1 : 0),
&__DATE__[4 + (__DATE__[4] == ' ' ? 1 : 0)],
__DATE__[4] == ' ' ? 1 : 2);
std::string m;
switch (__DATE__[0])
+211
View File
@@ -0,0 +1,211 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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/protocol/ExportOriginMemo.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/Serializer.h>
#include <algorithm>
#include <exception>
#include <iterator>
#include <string>
namespace ripple::ExportOriginMemo {
namespace {
Blob const&
memoTypeBytes()
{
static Blob const value(memoType.begin(), memoType.end());
return value;
}
bool
isReserved(STObject const& memo)
{
return memo.getFName() == sfMemo && memo.isFieldPresent(sfMemoType) &&
memo.getFieldVL(sfMemoType) == memoTypeBytes();
}
STObject
cloneObject(STTx const& tx)
{
Serializer serializer;
tx.add(serializer);
SerialIter sit{serializer.slice()};
STObject result{sfGeneric};
result.set(sit);
return result;
}
Blob
serialize(Origin const& origin, std::optional<Anchor> const& anchor)
{
//@@start export-origin-memo-wire-format
Serializer serializer;
serializer.add8(version);
serializer.add8(allowedFlags);
serializer.add32(origin.sourceDomain);
serializer.add32(origin.targetDomain);
serializer.addBitString(origin.transactionHash);
if (anchor)
{
serializer.add32(anchor->ledgerSequence);
serializer.addBitString(anchor->ledgerHash);
}
return serializer.getData();
//@@end export-origin-memo-wire-format
}
Expected<STTx, Error>
append(
STTx const& base,
Origin const& origin,
std::optional<Anchor> const& anchor)
{
if (hasReservedMemo(base))
return Unexpected(Error::reservedMemoPresent);
//@@start export-origin-memo-canonical-append
auto object = cloneObject(base);
STArray memos = object.isFieldPresent(sfMemos)
? object.getFieldArray(sfMemos)
: STArray{sfMemos};
STObject memo{sfMemo};
memo.setFieldVL(sfMemoType, memoTypeBytes());
memo.setFieldVL(sfMemoData, serialize(origin, anchor));
memos.emplace_back(std::move(memo));
object.setFieldArray(sfMemos, memos);
STTx result{std::move(object)};
std::string reason;
if (!passesLocalChecks(result, reason))
return Unexpected(Error::localChecks);
return result;
//@@end export-origin-memo-canonical-append
}
Expected<Stamp, Error>
parseData(Blob const& data)
{
if (data.size() != identityBytes && data.size() != releaseBytes)
return Unexpected(Error::malformedMemo);
try
{
SerialIter sit{makeSlice(data)};
if (sit.get8() != version)
return Unexpected(Error::unsupportedVersion);
if (sit.get8() != allowedFlags)
return Unexpected(Error::unsupportedFlags);
Stamp stamp{
.origin =
Origin{
.sourceDomain = sit.get32(),
.targetDomain = sit.get32(),
.transactionHash = sit.get256()},
.anchor = std::nullopt};
if (data.size() == releaseBytes)
stamp.anchor = Anchor{sit.get32(), sit.get256()};
if (!sit.empty())
return Unexpected(Error::malformedMemo);
return stamp;
}
catch (std::exception const&)
{
return Unexpected(Error::malformedMemo);
}
}
} // namespace
bool
hasReservedMemo(STTx const& tx)
{
if (!tx.isFieldPresent(sfMemos))
return false;
auto const& memos = tx.getFieldArray(sfMemos);
return std::any_of(memos.begin(), memos.end(), isReserved);
}
Expected<STTx, Error>
identityForm(STTx const& base, Origin const& origin)
{
return append(base, origin, std::nullopt);
}
Expected<STTx, Error>
releaseForm(STTx const& base, Origin const& origin, Anchor const& anchor)
{
return append(base, origin, anchor);
}
Expected<Stamp, Error>
parse(STTx const& tx)
{
std::string reason;
if (!passesLocalChecks(tx, reason))
return Unexpected(Error::localChecks);
if (!tx.isFieldPresent(sfMemos))
return Unexpected(Error::reservedMemoMissing);
auto const& memos = tx.getFieldArray(sfMemos);
auto found = memos.end();
for (auto it = memos.begin(); it != memos.end(); ++it)
{
if (!isReserved(*it))
continue;
if (found != memos.end())
return Unexpected(Error::reservedMemoPosition);
found = it;
}
if (found == memos.end())
return Unexpected(Error::reservedMemoMissing);
if (found != std::prev(memos.end()))
return Unexpected(Error::reservedMemoPosition);
if (found->isFieldPresent(sfMemoFormat) ||
!found->isFieldPresent(sfMemoData))
return Unexpected(Error::malformedMemo);
return parseData(found->getFieldVL(sfMemoData));
}
Expected<STTx, Error>
projectIdentity(STTx const& stamped)
{
auto parsed = parse(stamped);
if (!parsed)
return Unexpected(parsed.error());
auto object = cloneObject(stamped);
auto memos = object.getFieldArray(sfMemos);
memos.erase(std::prev(memos.end()));
if (memos.empty())
object.delField(sfMemos);
else
object.setFieldArray(sfMemos, memos);
return identityForm(STTx{std::move(object)}, parsed.value().origin);
}
} // namespace ripple::ExportOriginMemo
+31
View File
@@ -74,6 +74,9 @@ enum class LedgerNameSpace : std::uint16_t {
HOOK_DEFINITION = 'D',
EMITTED_TXN = 'E',
EMITTED_DIR = 'F',
EXPORT_LATCH = 0x5374, // St
EXPORT_COMMITTEE = 0x4563, // Ec
EXPORT_PENDING_DIR = 0x4570, // Ep
NFTOKEN_OFFER = 'q',
NFTOKEN_BUY_OFFERS = 'h',
NFTOKEN_SELL_OFFERS = 'i',
@@ -189,6 +192,34 @@ emittedTxn(uint256 const& id) noexcept
return {ltEMITTED_TXN, indexHash(LedgerNameSpace::EMITTED_TXN, id)};
}
//@@start export-origin-keylet
Keylet
exportLatch(AccountID const& account, uint256 const& originTxnHash) noexcept
{
return {
ltEXPORT_LATCH,
indexHash(LedgerNameSpace::EXPORT_LATCH, account, originTxnHash)};
}
//@@end export-origin-keylet
Keylet
exportCommittee(AccountID const& account, uint256 const& digest) noexcept
{
return {
ltEXPORT_COMMITTEE,
indexHash(LedgerNameSpace::EXPORT_COMMITTEE, account, digest)};
}
//@@start export-pending-directory-keylet
Keylet const&
pendingExports() noexcept
{
static Keylet const ret{
ltDIR_NODE, indexHash(LedgerNameSpace::EXPORT_PENDING_DIR)};
return ret;
}
//@@end export-pending-directory-keylet
Keylet
hook(AccountID const& id) noexcept
{
@@ -53,6 +53,13 @@ InnerObjectFormats::InnerObjectFormats()
{sfTxnSignature, soeREQUIRED},
});
add(sfExportSigner.jsonName,
sfExportSigner.getCode(),
{
{sfSigningPubKey, soeREQUIRED},
{sfTxnSignature, soeREQUIRED},
});
add(sfMajority.jsonName,
sfMajority.getCode(),
{
@@ -78,6 +85,7 @@ InnerObjectFormats::InnerObjectFormats()
{sfHookExecutionIndex, soeREQUIRED},
{sfHookStateChangeCount, soeREQUIRED},
{sfHookEmitCount, soeREQUIRED},
{sfHookExportCount, soeOPTIONAL},
{sfFlags, soeOPTIONAL}});
add(sfHookEmission.jsonName,
+2 -1
View File
@@ -684,7 +684,8 @@ isPseudoTx(STObject const& tx)
auto tt = safe_cast<TxType>(*t);
return tt == ttAMENDMENT || tt == ttFEE || tt == ttUNL_MODIFY ||
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON;
tt == ttEMIT_FAILURE || tt == ttUNL_REPORT || tt == ttCRON ||
tt == ttCONSENSUS_ENTROPY || tt == ttEXPORT_SIGNATURES;
}
} // namespace ripple
+2
View File
@@ -124,6 +124,7 @@ transResults()
MAKE_ERROR(tecARRAY_TOO_LARGE, "Array is too large."),
MAKE_ERROR(tecLOCKED, "Fund is locked."),
MAKE_ERROR(tecBAD_CREDENTIALS, "Bad credentials."),
MAKE_ERROR(tecEXPORT_COMMITTEE_UNAVAILABLE, "Export committee is unavailable in the parent validator view."),
MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."),
MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."),
@@ -174,6 +175,7 @@ transResults()
MAKE_ERROR(telIMPORT_VL_KEY_NOT_RECOGNISED, "Import vl key was not recognized."),
MAKE_ERROR(telCAN_NOT_QUEUE_IMPORT, "Import transaction was not able to be directly applied and cannot be queued."),
MAKE_ERROR(telENV_RPC_FAILED, "Unit test RPC failure."),
MAKE_ERROR(telEXPORT_LATCH_REQUIRED, "The imported Export callback has no matching Export latch."),
MAKE_ERROR(temMALFORMED, "Malformed transaction."),
MAKE_ERROR(temBAD_AMM_TOKENS, "Malformed: Invalid LPTokens."),
+36
View File
@@ -18,6 +18,7 @@
//==============================================================================
#include <test/jtx.h>
#include <xrpl/protocol/ExportCommittee.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/jss.h>
@@ -469,6 +470,40 @@ public:
env.close();
}
void
testExportCommitteeCleanup(FeatureBitset features)
{
using namespace jtx;
testcase("Export committee cleanup");
Env env{*this, features | featureExport};
Account const alice("alice");
Account const becky("becky");
env.fund(XRP(10000), alice, becky);
env.close();
auto const roster = serializeExportCommittee({alice.pk()});
auto const digest = exportCommitteeHash(makeSlice(roster));
Json::Value setup;
setup[jss::TransactionType] = "Export";
setup[jss::Account] = alice.human();
setup[sfExportCommittee.jsonName] = strHex(roster);
env(setup);
env.close();
auto const committeeKey = keylet::exportCommittee(alice.id(), digest);
BEAST_EXPECT(env.closed()->exists(committeeKey));
incLgrSeqForAccDel(env, alice);
auto const acctDelFee{drops(env.current()->fees().increment)};
env(acctdelete(alice, becky), fee(acctDelFee));
env.close();
BEAST_EXPECT(!env.closed()->exists(keylet::account(alice.id())));
BEAST_EXPECT(!env.closed()->exists(committeeKey));
}
void
testResurrection(FeatureBitset features)
{
@@ -1279,6 +1314,7 @@ public:
testBasics(features);
testDirectories(features);
testOwnedTypes(features);
testExportCommitteeCleanup(features);
testResurrection(features);
testAmendmentEnable(features);
testTooManyOffers(features);
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+292
View File
@@ -0,0 +1,292 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <xrpld/app/ledger/OpenLedger.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/misc/ValidatorKeys.h>
#include <xrpld/app/tx/apply.h>
#include <xrpl/protocol/ExportCommittee.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/ExportOriginMemo.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/jss.h>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <vector>
namespace ripple {
namespace test {
struct ExportFee_test : public beast::unit_test::suite
{
static std::unique_ptr<Config>
exportTestConfig()
{
auto cfg = jtx::envconfig(jtx::validator, "");
cfg->NETWORK_ID = 21337;
return cfg;
}
void
seedUNLReportLedger(jtx::Env& env, PublicKey const& masterKey)
{
env.app().openLedger().modify(
[&](OpenView& view, beast::Journal) -> bool {
STTx tx(ttUNL_REPORT, [&](auto& obj) {
obj.setFieldU32(sfLedgerSequence, env.current()->seq());
auto active = std::make_unique<STObject>(sfActiveValidator);
active->setFieldVL(sfPublicKey, masterKey);
obj.set(std::move(active));
});
auto const txID = tx.getTransactionID();
auto serialized = std::make_shared<Serializer>();
tx.add(*serialized);
env.app().getHashRouter().setFlags(txID, SF_PRIVATE2);
view.rawTxInsert(txID, std::move(serialized), nullptr);
return true;
});
BEAST_EXPECT(env.close(
env.now() + std::chrono::seconds{5}, std::chrono::milliseconds{0}));
BEAST_EXPECT(env.le(keylet::UNLReport()));
}
static STObject
exportedPayment(
AccountID const& source,
AccountID const& destination,
LedgerIndex const first,
LedgerIndex const last)
{
STObject target(sfExportedTxn);
target.setFieldU16(sfTransactionType, ttPAYMENT);
target.setFieldU32(sfFlags, tfFullyCanonicalSig);
target.setFieldU32(sfSequence, 0);
target.setFieldU32(sfTicketSequence, 1);
target.setFieldU32(sfNetworkID, 1);
target.setFieldU32(sfFirstLedgerSequence, first);
target.setFieldU32(sfLastLedgerSequence, last);
target.setFieldAmount(sfAmount, XRPAmount{1'000'000});
target.setFieldAmount(sfFee, XRPAmount{10});
target.setFieldVL(sfSigningPubKey, Blob{});
target.setAccountID(sfAccount, source);
target.setAccountID(sfDestination, destination);
return target;
}
static Json::Value
exportIntent(
jtx::Account const& account,
STObject const& target,
LedgerIndex const last,
Blob const& committee)
{
Json::Value tx;
tx[jss::TransactionType] = jss::Export;
tx[jss::Account] = account.human();
tx[jss::LastLedgerSequence] = last;
tx[sfExportedTxn.jsonName] = target.getJson(JsonOptions::none);
tx[sfExportCommitteeHash.jsonName] =
to_string(exportCommitteeHash(makeSlice(committee)));
tx[sfExportCommittee.jsonName] = strHex(committee);
return tx;
}
static void
addHookParameterPayload(STObject& target, std::size_t count)
{
STArray parameters(sfHookParameters);
for (std::size_t i = 0; i < count; ++i)
{
STObject parameter(sfHookParameter);
parameter.setFieldVL(
sfHookParameterName, Blob{static_cast<std::uint8_t>(i + 1)});
parameter.setFieldVL(sfHookParameterValue, Blob(256, 0xA5));
parameters.emplace_back(std::move(parameter));
}
target.setFieldArray(sfHookParameters, std::move(parameters));
}
static XRPAmount
expectedFee(ReadView const& view, STTx const& tx, std::size_t committeeSize)
{
Serializer target;
tx.peekAtField(sfExportedTxn).downcast<STObject>().add(target);
auto const witnessBytes = target.size() +
ExportLimits::feeWitnessFixedAllowanceBytes +
committeeSize * ExportLimits::feeWitnessSignerAllowanceBytes;
auto const workUnits = committeeSize *
ExportLimits::feeSharePublicationRounds *
ExportLimits::feeWorkUnitsPerSharePublication +
committeeSize * ExportLimits::feeWorkUnitsPerWitnessSignature +
((witnessBytes + ExportLimits::feeWitnessChunkBytes - 1) /
ExportLimits::feeWitnessChunkBytes) *
ExportLimits::feeWorkUnitsPerWitnessChunk;
auto const unrounded = workUnits * view.fees().base.drops() +
witnessBytes * ExportLimits::feePermanentWitnessByteDrops;
auto const surcharge =
((unrounded + ExportLimits::feeSurchargeRoundDrops - 1) /
ExportLimits::feeSurchargeRoundDrops) *
ExportLimits::feeSurchargeRoundDrops;
return view.fees().base +
XRPAmount{static_cast<std::int64_t>(surcharge)};
}
void
testIntentFeeAndEnforcement(FeatureBitset const& features)
{
testcase("Export intent fee prices publication and witness work");
using namespace jtx;
Env env{*this, exportTestConfig(), features};
Account const alice{"alice"};
Account const carol{"carol"};
env.fund(XRP(10'000), alice, carol);
env.close();
auto const& validatorKeys = env.app().getValidatorKeys();
BEAST_EXPECT(validatorKeys.keys);
if (!validatorKeys.keys)
return;
auto const committee =
serializeExportCommittee({validatorKeys.keys->masterPublicKey});
seedUNLReportLedger(env, validatorKeys.keys->masterPublicKey);
auto const sequence = env.current()->seq();
auto const last = sequence + ExportLimits::maxAdmissionWindowLedgers;
auto const target =
exportedPayment(alice.id(), carol.id(), sequence + 1, last);
auto const intent = exportIntent(alice, target, last, committee);
auto const prepared = env.jt(intent);
auto const required = calculateBaseFee(*env.current(), *prepared.stx);
auto const expected = expectedFee(*env.current(), *prepared.stx, 1);
BEAST_EXPECT(required == expected);
// Any positive surcharge rounds to at least one provisional fee unit.
BEAST_EXPECT(
required >= env.current()->fees().base +
XRPAmount{static_cast<std::int64_t>(
ExportLimits::feeSurchargeRoundDrops)});
env(intent, fee(required - drops(1)), ter(telINSUF_FEE_P));
env(intent, fee(required), ter(tesSUCCESS));
env.close();
auto oversizedTarget = exportedPayment(
alice.id(), carol.id(), env.current()->seq() + 1, last + 1);
addHookParameterPayload(oversizedTarget, 8);
auto projectedTarget = oversizedTarget;
auto const projected = ExportOriginMemo::releaseForm(
STTx{std::move(projectedTarget)},
ExportOriginMemo::Origin{21337, 1, uint256{}},
ExportOriginMemo::Anchor{0, uint256{}});
BEAST_EXPECT(projected);
if (!projected)
return;
Serializer projectedBytes;
projected.value().add(projectedBytes);
BEAST_EXPECT(
projectedBytes.size() > ExportLimits::maxExportReleaseTargetBytes);
auto const oversizedIntent =
exportIntent(alice, oversizedTarget, last + 1, committee);
auto const oversizedFee =
calculateBaseFee(*env.current(), *env.jt(oversizedIntent).stx);
env(oversizedIntent, fee(oversizedFee), ter(temMALFORMED));
}
void
testFeeShape(FeatureBitset const& features)
{
testcase("Export fee scales by committee while admin stays ordinary");
using namespace jtx;
Env env{*this, exportTestConfig(), features};
Account const alice{"alice"};
Account const carol{"carol"};
env.fund(XRP(10'000), alice, carol);
env.close();
std::vector<PublicKey> members;
members.reserve(ExportLimits::maxCommitteeMembers);
for (std::size_t i = 0; i < ExportLimits::maxCommitteeMembers; ++i)
members.push_back(randomKeyPair(KeyType::secp256k1).first);
auto const largeCommittee = serializeExportCommittee(members);
auto const oneMember = serializeExportCommittee({members.front()});
Json::Value setup;
setup[jss::TransactionType] = jss::Export;
setup[jss::Account] = alice.human();
setup[sfExportCommittee.jsonName] = strHex(largeCommittee);
auto const setupFee =
calculateBaseFee(*env.current(), *env.jt(setup).stx);
BEAST_EXPECT(setupFee == env.current()->fees().base);
Json::Value oneMemberSetup = setup;
oneMemberSetup[sfExportCommittee.jsonName] = strHex(oneMember);
env(oneMemberSetup, fee(env.current()->fees().base), ter(tesSUCCESS));
env.close();
auto const sequence = env.current()->seq();
auto const last = sequence + ExportLimits::maxAdmissionWindowLedgers;
auto const target =
exportedPayment(alice.id(), carol.id(), sequence + 1, last);
auto const smallIntent = exportIntent(alice, target, last, oneMember);
auto const largeIntent =
exportIntent(alice, target, last, largeCommittee);
auto const smallFee =
calculateBaseFee(*env.current(), *env.jt(smallIntent).stx);
auto const largeFee =
calculateBaseFee(*env.current(), *env.jt(largeIntent).stx);
auto residentIntent = smallIntent;
residentIntent.removeMember(sfExportCommittee.jsonName);
auto const residentFee =
calculateBaseFee(*env.current(), *env.jt(residentIntent).stx);
BEAST_EXPECT(largeFee > smallFee);
BEAST_EXPECT(residentFee == smallFee);
BEAST_EXPECT(
largeFee ==
expectedFee(
*env.current(),
*env.jt(largeIntent).stx,
ExportLimits::maxCommitteeMembers));
}
void
run() override
{
auto const features = jtx::supported_amendments() | featureExport;
testIntentFeeAndEnforcement(features);
testFeeShape(features);
}
};
BEAST_DEFINE_TESTSUITE(ExportFee, app, ripple);
} // namespace test
} // namespace ripple
+455
View File
@@ -0,0 +1,455 @@
//------------------------------------------------------------------------------
/*
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 WHATSOEVER
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
#include <xrpld/ledger/Sandbox.h>
#include <xrpl/protocol/Protocol.h>
#include <set>
#include <vector>
namespace ripple {
namespace test {
struct ExportLatch_test : beast::unit_test::suite
{
static std::shared_ptr<SLE>
makeLatch(AccountID const& account, std::uint32_t ordinal)
{
uint256 const origin{ordinal + 1};
auto latch =
std::make_shared<SLE>(keylet::exportLatch(account, origin));
latch->setAccountID(sfAccount, account);
latch->setFieldU32(sfTicketSequence, 50'000 + ordinal);
latch->setFieldH256(sfTransactionHash, origin);
latch->setFieldH256(sfDigest, uint256{10'000 + ordinal});
latch->setFieldU32(sfLedgerSequence, 4'000'000 + ordinal);
latch->setFieldH256(sfExportCommitteeHash, uint256{20'000 + ordinal});
return latch;
}
void
testDirectoryLifecycle()
{
testcase("enhanced latch directory lifecycle");
using namespace jtx;
Account const alice{"alice"};
Env env{*this};
env.fund(XRP(10'000), alice);
env(ticket::create(alice, dirNodeMaxEntries));
env.close();
beast::Journal j{beast::Journal::getNullSink()};
Sandbox sb{env.closed().get(), tapNONE};
std::vector<Keylet> latches;
latches.reserve(dirNodeMaxEntries + 1);
for (std::uint32_t i = 0; i <= dirNodeMaxEntries; ++i)
{
auto latch = makeLatch(alice.id(), i);
latches.emplace_back(keylet::unchecked(latch->key()));
auto const ter =
ExportLedgerOps::insertPendingExportLatch(sb, sb, latch, j);
if (!BEAST_EXPECTS(
isTesSuccess(ter),
"ordinal=" + std::to_string(i) + " ter=" + transToken(ter)))
return;
}
auto const account = sb.read(keylet::account(alice.id()));
auto const pending = sb.read(keylet::pendingExports());
BEAST_EXPECT(account);
BEAST_EXPECT(pending);
if (!account || !pending)
return;
BEAST_EXPECT(account->getFieldU16(sfExportCount) == 33);
BEAST_EXPECT(account->getFieldU32(sfOwnerCount) == 65);
BEAST_EXPECT(pending->getFieldU16(sfExportCount) == 33);
BEAST_EXPECT(pending->getFieldV256(sfIndexes).size() == 32);
auto const first = sb.read(latches.front());
auto const last = sb.read(latches.back());
BEAST_EXPECT(first && first->getFieldU64(sfOwnerNode) == 1);
BEAST_EXPECT(first && first->isFieldPresent(sfExportNode));
BEAST_EXPECT(first && first->getFieldU64(sfExportNode) == 0);
BEAST_EXPECT(last && last->getFieldU64(sfOwnerNode) == 2);
BEAST_EXPECT(last && last->getFieldU64(sfExportNode) == 1);
if (!first || !last)
return;
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::removePendingExportLink(sb, latches.front(), j)));
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::removePendingExportLink(sb, latches.back(), j)));
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::removePendingExportLink(sb, latches.back(), j)));
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::eraseExportLatch(sb, sb, latches.front(), j)));
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::eraseExportLatch(sb, sb, latches[1], j)));
auto const afterAccount = sb.read(keylet::account(alice.id()));
auto const afterPending = sb.read(keylet::pendingExports());
BEAST_EXPECT(afterAccount->getFieldU16(sfExportCount) == 31);
BEAST_EXPECT(afterAccount->getFieldU32(sfOwnerCount) == 63);
BEAST_EXPECT(afterPending->getFieldU16(sfExportCount) == 30);
Sandbox reopened{&sb};
std::set<uint256> recovered;
forEachItem(
reopened,
keylet::pendingExports(),
[&](std::shared_ptr<SLE const> const& sle) {
recovered.insert(sle->key());
});
BEAST_EXPECT(recovered.size() == 30);
std::set<uint256> expected;
for (std::size_t i = 2; i + 1 < latches.size(); ++i)
expected.insert(latches[i].key);
BEAST_EXPECT(recovered == expected);
for (std::size_t i = 2; i < latches.size(); ++i)
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::eraseExportLatch(sb, sb, latches[i], j)));
auto const finalAccount = sb.read(keylet::account(alice.id()));
auto const finalPending = sb.read(keylet::pendingExports());
BEAST_EXPECT(finalAccount->getFieldU16(sfExportCount) == 0);
BEAST_EXPECT(finalAccount->getFieldU32(sfOwnerCount) == 32);
BEAST_EXPECT(finalPending);
BEAST_EXPECT(finalPending->getFieldU16(sfExportCount) == 0);
BEAST_EXPECT(finalPending->getFieldV256(sfIndexes).empty());
BEAST_EXPECT(finalPending->getFieldU64(sfIndexNext) == 0);
BEAST_EXPECT(finalPending->getFieldU64(sfIndexPrevious) == 0);
}
void
testWitnessedLatchReleasesGlobalPendingCap()
{
testcase("witnessed latch releases global pending cap");
using namespace jtx;
Account const alice{"alice"};
Env env{*this};
env.fund(XRP(10'000), alice);
env.close();
beast::Journal j{beast::Journal::getNullSink()};
Sandbox sb{env.closed().get(), tapNONE};
std::vector<Keylet> latches;
latches.reserve(ExportLimits::maxLiveExportLatches);
for (std::uint32_t i = 0; i < ExportLimits::maxLiveExportLatches; ++i)
{
auto latch = makeLatch(alice.id(), i);
latches.emplace_back(keylet::unchecked(latch->key()));
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::insertPendingExportLatch(sb, sb, latch, j)));
}
auto const accountAtCap = sb.read(keylet::account(alice.id()));
auto const rootAtCap = sb.read(keylet::pendingExports());
BEAST_EXPECT(accountAtCap);
BEAST_EXPECT(rootAtCap);
if (!accountAtCap || !rootAtCap)
return;
BEAST_EXPECT(
rootAtCap->getFieldU16(sfExportCount) ==
ExportLimits::maxLiveExportLatches);
auto const ownerCountAtCap = accountAtCap->getFieldU32(sfOwnerCount);
auto replacement =
makeLatch(alice.id(), ExportLimits::maxLiveExportLatches);
auto const replacementKey = keylet::unchecked(replacement->key());
BEAST_EXPECT(
ExportLedgerOps::insertPendingExportLatch(sb, sb, replacement, j) ==
tecDIR_FULL);
auto const accountAfterRejection = sb.read(keylet::account(alice.id()));
auto const rootAfterRejection = sb.read(keylet::pendingExports());
BEAST_EXPECT(accountAfterRejection);
BEAST_EXPECT(rootAfterRejection);
if (!accountAfterRejection || !rootAfterRejection)
return;
BEAST_EXPECT(!sb.exists(replacementKey));
BEAST_EXPECT(
accountAfterRejection->getFieldU16(sfExportCount) ==
ExportLimits::maxLiveExportLatches);
BEAST_EXPECT(
accountAfterRejection->getFieldU32(sfOwnerCount) ==
ownerCountAtCap);
BEAST_EXPECT(
rootAfterRejection->getFieldU16(sfExportCount) ==
ExportLimits::maxLiveExportLatches);
uint256 const witnessHash{99'999};
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::recordExportWitness(
sb, sb, latches.front(), witnessHash, j)));
auto const witnessed = sb.read(latches.front());
auto const accountAfterWitness = sb.read(keylet::account(alice.id()));
auto const rootAfterWitness = sb.read(keylet::pendingExports());
BEAST_EXPECT(witnessed);
BEAST_EXPECT(accountAfterWitness);
BEAST_EXPECT(rootAfterWitness);
if (!witnessed || !accountAfterWitness || !rootAfterWitness)
return;
BEAST_EXPECT(!witnessed->isFieldPresent(sfExportNode));
BEAST_EXPECT(
witnessed->getFieldH256(sfExportSignatureHash) == witnessHash);
BEAST_EXPECT(
accountAfterWitness->getFieldU16(sfExportCount) ==
ExportLimits::maxLiveExportLatches);
BEAST_EXPECT(
accountAfterWitness->getFieldU32(sfOwnerCount) == ownerCountAtCap);
BEAST_EXPECT(
rootAfterWitness->getFieldU16(sfExportCount) ==
ExportLimits::maxLiveExportLatches - 1);
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::insertPendingExportLatch(sb, sb, replacement, j)));
BEAST_EXPECT(
sb.read(keylet::pendingExports())->getFieldU16(sfExportCount) ==
ExportLimits::maxLiveExportLatches);
BEAST_EXPECT(sb.exists(latches.front()));
}
void
testRetainedLatchAcceptsBothFactOrders()
{
testcase("retained latch accepts XPOP and witness in either order");
using namespace jtx;
Account const alice{"alice"};
Env env{*this};
env.fund(XRP(10'000), alice);
env.close();
beast::Journal j{beast::Journal::getNullSink()};
Sandbox sb{env.closed().get(), tapNONE};
auto const baselineAccount = sb.read(keylet::account(alice.id()));
BEAST_EXPECT(baselineAccount);
if (!baselineAccount)
return;
auto const baselineExportCount =
ExportLedgerOps::exportLatchCount(*baselineAccount);
auto const baselineOwnerCount =
baselineAccount->getFieldU32(sfOwnerCount);
auto const baselineReserve =
sb.fees().accountReserve(baselineOwnerCount);
auto xpopFirst = makeLatch(alice.id(), 0);
auto const xpopFirstKey = keylet::unchecked(xpopFirst->key());
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::insertPendingExportLatch(sb, sb, xpopFirst, j)));
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::controlExportLatch(
sb,
sb,
alice.id(),
xpopFirst->getFieldH256(sfTransactionHash),
false,
j)));
auto const canceled = sb.read(xpopFirstKey);
BEAST_EXPECT(canceled);
if (!canceled)
return;
auto const canceledFlags = canceled->isFieldPresent(sfFlags)
? canceled->getFieldU32(sfFlags)
: std::uint32_t{0};
BEAST_EXPECT((canceledFlags & lsfExportCanceled) != 0);
BEAST_EXPECT(!canceled->isFieldPresent(sfExportNode));
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::recordExportXpop(sb, sb, xpopFirstKey, j)));
auto const afterXpop = sb.read(xpopFirstKey);
BEAST_EXPECT(afterXpop);
if (!afterXpop)
return;
auto const xpopFlags = afterXpop->getFieldU32(sfFlags);
BEAST_EXPECT((xpopFlags & lsfExportCanceled) != 0);
BEAST_EXPECT((xpopFlags & lsfExportXpopSeen) != 0);
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::recordExportWitness(
sb, sb, xpopFirstKey, uint256{99'999}, j)));
BEAST_EXPECT(!sb.exists(xpopFirstKey));
auto const accountAfterWitness = sb.read(keylet::account(alice.id()));
auto const rootAfterWitness = sb.read(keylet::pendingExports());
BEAST_EXPECT(accountAfterWitness);
BEAST_EXPECT(rootAfterWitness);
if (!accountAfterWitness || !rootAfterWitness)
return;
BEAST_EXPECT(
ExportLedgerOps::exportLatchCount(*accountAfterWitness) ==
baselineExportCount);
BEAST_EXPECT(
accountAfterWitness->getFieldU32(sfOwnerCount) ==
baselineOwnerCount);
BEAST_EXPECT(
sb.fees().accountReserve(accountAfterWitness->getFieldU32(
sfOwnerCount)) == baselineReserve);
BEAST_EXPECT(ExportLedgerOps::exportLatchCount(*rootAfterWitness) == 0);
auto witnessFirst = makeLatch(alice.id(), 1);
auto const witnessFirstKey = keylet::unchecked(witnessFirst->key());
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::insertPendingExportLatch(
sb, sb, witnessFirst, j)));
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::controlExportLatch(
sb,
sb,
alice.id(),
witnessFirst->getFieldH256(sfTransactionHash),
false,
j)));
// The accepted witness may already have been materialized before
// cancellation even when consensus orders its application afterward.
uint256 const witnessHash{100'000};
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::recordExportWitness(
sb, sb, witnessFirstKey, witnessHash, j)));
auto const afterWitness = sb.read(witnessFirstKey);
BEAST_EXPECT(afterWitness);
if (!afterWitness)
return;
auto const witnessFlags = afterWitness->getFieldU32(sfFlags);
BEAST_EXPECT((witnessFlags & lsfExportCanceled) != 0);
BEAST_EXPECT(
afterWitness->getFieldH256(sfExportSignatureHash) == witnessHash);
BEAST_EXPECT(!afterWitness->isFieldPresent(sfExportNode));
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::recordExportXpop(sb, sb, witnessFirstKey, j)));
BEAST_EXPECT(!sb.exists(witnessFirstKey));
auto const accountAfterXpop = sb.read(keylet::account(alice.id()));
auto const rootAfterXpop = sb.read(keylet::pendingExports());
BEAST_EXPECT(accountAfterXpop);
BEAST_EXPECT(rootAfterXpop);
if (!accountAfterXpop || !rootAfterXpop)
return;
BEAST_EXPECT(
ExportLedgerOps::exportLatchCount(*accountAfterXpop) ==
baselineExportCount);
BEAST_EXPECT(
accountAfterXpop->getFieldU32(sfOwnerCount) == baselineOwnerCount);
BEAST_EXPECT(
sb.fees().accountReserve(accountAfterXpop->getFieldU32(
sfOwnerCount)) == baselineReserve);
BEAST_EXPECT(ExportLedgerOps::exportLatchCount(*rootAfterXpop) == 0);
}
void
testExplicitEraseAndExpiryRetention()
{
testcase("explicit erase and expiry retention");
using namespace jtx;
Account const alice{"alice"};
Env env{*this};
env.fund(XRP(10'000), alice);
env.close();
beast::Journal j{beast::Journal::getNullSink()};
Sandbox sb{env.closed().get(), tapNONE};
auto const baselineAccount = sb.read(keylet::account(alice.id()));
auto const baselinePending = sb.read(keylet::pendingExports());
BEAST_EXPECT(baselineAccount);
if (!baselineAccount)
return;
auto const baselineExportCount =
ExportLedgerOps::exportLatchCount(*baselineAccount);
auto const baselineOwnerCount =
baselineAccount->getFieldU32(sfOwnerCount);
auto const baselinePendingCount = baselinePending
? ExportLedgerOps::exportLatchCount(*baselinePending)
: std::uint16_t{0};
auto erased = makeLatch(alice.id(), 0);
auto const erasedOrigin = erased->getFieldH256(sfTransactionHash);
auto const erasedKey = keylet::exportLatch(alice.id(), erasedOrigin);
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::insertPendingExportLatch(sb, sb, erased, j)));
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::controlExportLatch(
sb, sb, alice.id(), erasedOrigin, true, j)));
BEAST_EXPECT(!sb.exists(erasedKey));
auto const afterEraseAccount = sb.read(keylet::account(alice.id()));
auto const afterErasePending = sb.read(keylet::pendingExports());
BEAST_EXPECT(afterEraseAccount);
BEAST_EXPECT(afterErasePending);
if (!afterEraseAccount || !afterErasePending)
return;
BEAST_EXPECT(
ExportLedgerOps::exportLatchCount(*afterEraseAccount) ==
baselineExportCount);
BEAST_EXPECT(
afterEraseAccount->getFieldU32(sfOwnerCount) == baselineOwnerCount);
BEAST_EXPECT(
ExportLedgerOps::exportLatchCount(*afterErasePending) ==
baselinePendingCount);
auto expired = makeLatch(alice.id(), 1);
expired->setFieldU32(sfLastLedgerSequence, 100);
auto const expiredOrigin = expired->getFieldH256(sfTransactionHash);
auto const expiredKey = keylet::exportLatch(alice.id(), expiredOrigin);
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::insertPendingExportLatch(sb, sb, expired, j)));
BEAST_EXPECT(isTesSuccess(
ExportLedgerOps::pruneExpiredExportLatches(sb, sb, 101, j)));
auto const retained = sb.read(expiredKey);
auto const afterExpiryAccount = sb.read(keylet::account(alice.id()));
auto const afterExpiryPending = sb.read(keylet::pendingExports());
BEAST_EXPECT(retained);
BEAST_EXPECT(afterExpiryAccount);
BEAST_EXPECT(afterExpiryPending);
if (!retained || !afterExpiryAccount || !afterExpiryPending)
return;
BEAST_EXPECT(!retained->isFieldPresent(sfExportNode));
BEAST_EXPECT((retained->getFieldU32(sfFlags) & lsfExportCanceled) != 0);
BEAST_EXPECT(
ExportLedgerOps::exportLatchCount(*afterExpiryAccount) ==
baselineExportCount + 1);
BEAST_EXPECT(
afterExpiryAccount->getFieldU32(sfOwnerCount) ==
baselineOwnerCount + 1);
BEAST_EXPECT(
ExportLedgerOps::exportLatchCount(*afterExpiryPending) ==
baselinePendingCount);
BEAST_EXPECT(isTesSuccess(ExportLedgerOps::controlExportLatch(
sb, sb, alice.id(), expiredOrigin, true, j)));
BEAST_EXPECT(!sb.exists(expiredKey));
}
void
run() override
{
testDirectoryLifecycle();
testWitnessedLatchReleasesGlobalPendingCap();
testRetainedLatchAcceptsBothFactOrders();
testExplicitEraseAndExpiryRetention();
}
};
BEAST_DEFINE_TESTSUITE(ExportLatch, app, ripple);
} // namespace test
} // namespace ripple
+881
View File
@@ -0,0 +1,881 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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/tx/detail/ExportResultBuilder.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/ExportOriginMemo.h>
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Seed.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/digest.h>
#include <algorithm>
#include <array>
#include <cstring>
#include <optional>
namespace ripple {
namespace test {
namespace {
uint256
makeHash(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
STTx
makeSTTx(STObject const& obj)
{
Serializer s;
obj.add(s);
SerialIter sit{s.slice()};
return STTx{std::ref(sit)};
}
STTx
makeExportedPayment(AccountID const& src, AccountID const& dst)
{
STObject obj(sfExportedTxn);
obj.setFieldU16(sfTransactionType, ttPAYMENT);
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfTicketSequence, 1);
obj.setFieldU32(sfFirstLedgerSequence, 2);
obj.setFieldU32(sfLastLedgerSequence, 6);
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
obj.setFieldAmount(sfFee, XRPAmount{10});
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setAccountID(sfAccount, src);
obj.setAccountID(sfDestination, dst);
return makeSTTx(obj);
}
std::optional<STTx>
makeExportedPaymentWithSize(
AccountID const& src,
AccountID const& dst,
std::size_t const serializedSize)
{
auto const base = makeExportedPayment(src, dst);
for (std::size_t payloadBytes = 0; payloadBytes <= serializedSize;
++payloadBytes)
{
auto candidate = base;
STArray memos(sfMemos);
STObject memo(sfMemo);
memo.setFieldVL(sfMemoData, Blob(payloadBytes, 0xCC));
memos.emplace_back(std::move(memo));
candidate.setFieldArray(sfMemos, std::move(memos));
auto const size = candidate.getSerializer().size();
if (size == serializedSize)
return candidate;
if (size > serializedSize)
return std::nullopt;
}
return std::nullopt;
}
STTx
makeExportedPaymentChannelClaim(
AccountID const& src,
Blob const& channelSignature)
{
STObject obj(sfExportedTxn);
obj.setFieldU16(sfTransactionType, ttPAYCHAN_CLAIM);
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
obj.setFieldU32(sfSequence, 0);
obj.setFieldU32(sfTicketSequence, 1);
obj.setFieldU32(sfFirstLedgerSequence, 2);
obj.setFieldU32(sfLastLedgerSequence, 6);
obj.setFieldAmount(sfFee, XRPAmount{10});
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setAccountID(sfAccount, src);
obj.setFieldH256(sfChannel, makeHash("payment-channel"));
obj.setFieldVL(sfSignature, channelSignature);
return makeSTTx(obj);
}
ExportResultBuilder::PositionedSignatureSnapshot
positionedSignatures(ExportResultBuilder::SignatureSnapshot const& signatures)
{
ExportResultBuilder::PositionedSignatureSnapshot result;
std::uint16_t position = 0;
for (auto const& [key, signature] : signatures)
{
result.emplace(
position++,
ExportResultBuilder::PositionedSignature{key, signature});
}
return result;
}
std::pair<PublicKey, SecretKey>
deterministicKeyPair(char const* label)
{
return generateKeyPair(KeyType::secp256k1, generateSeed(label));
}
} // namespace
class ExportResultBuilder_test : public beast::unit_test::suite
{
public:
void
testAssemblesMultiSignedTransaction()
{
testcase("assembles multisigned transaction");
auto const signerA = randomKeyPair(KeyType::secp256k1);
auto const signerB = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(signerA.first), calcAccountID(signerB.first));
ExportResultBuilder::SignatureSnapshot signatures;
signatures.emplace(
signerA.first,
ExportResultBuilder::signExportedTxn(
innerTx, signerA.first, signerA.second));
signatures.emplace(
signerB.first,
ExportResultBuilder::signExportedTxn(
innerTx, signerB.first, signerB.second));
auto const multiSigned =
ExportResultBuilder::buildMultiSignedExportedTxn(
innerTx, signatures);
BEAST_EXPECT(multiSigned.getFieldVL(sfSigningPubKey).empty());
BEAST_EXPECT(multiSigned.isFieldPresent(sfSigners));
auto const& signers = multiSigned.getFieldArray(sfSigners);
BEAST_EXPECT(signers.size() == 2);
if (signers.size() == 2)
{
BEAST_EXPECT(
signers[0].getAccountID(sfAccount) <
signers[1].getAccountID(sfAccount));
}
for (auto const& signer : signers)
{
auto const pkVL = signer.getFieldVL(sfSigningPubKey);
PublicKey const pk{makeSlice(pkVL)};
auto const sigVL = signer.getFieldVL(sfTxnSignature);
auto const signerAcctID = signer.getAccountID(sfAccount);
auto const sigData = buildMultiSigningData(innerTx, signerAcctID);
BEAST_EXPECT(verify(pk, sigData.slice(), makeSlice(sigVL)));
}
auto const signedTxHash =
multiSigned.getHash(HashPrefix::transactionID);
Serializer serialized;
multiSigned.add(serialized);
SerialIter sit(serialized.slice());
STTx signedTx{std::ref(sit)};
BEAST_EXPECT(signedTx.getTransactionID() == signedTxHash);
}
void
testBuildMultiSignedExportedTxnDirect()
{
testcase("builds multisigned exported transaction directly");
auto const signerA = randomKeyPair(KeyType::secp256k1);
auto const signerB = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(signerA.first), calcAccountID(dst.first));
ExportResultBuilder::SignatureSnapshot signatures;
signatures.emplace(signerB.first, Buffer{});
signatures.emplace(
signerA.first,
ExportResultBuilder::signExportedTxn(
innerTx, signerA.first, signerA.second));
auto multiSigned = ExportResultBuilder::buildMultiSignedExportedTxn(
innerTx, signatures);
BEAST_EXPECT(multiSigned.getFieldVL(sfSigningPubKey).empty());
BEAST_EXPECT(multiSigned.isFieldPresent(sfSigners));
auto const& signers = multiSigned.getFieldArray(sfSigners);
BEAST_EXPECT(signers.size() == 1);
if (signers.size() == 1)
{
BEAST_EXPECT(
signers[0].getAccountID(sfAccount) ==
calcAccountID(signerA.first));
BEAST_EXPECT(
makeSlice(signers[0].getFieldVL(sfSigningPubKey)) ==
signerA.first.slice());
}
ExportResultBuilder::SignatureSnapshot none;
auto unsignedMulti =
ExportResultBuilder::buildMultiSignedExportedTxn(innerTx, none);
BEAST_EXPECT(unsignedMulti.getFieldVL(sfSigningPubKey).empty());
BEAST_EXPECT(!unsignedMulti.isFieldPresent(sfSigners));
}
void
testExportIntentHashIgnoresSignerSubset()
{
testcase("export intent hash ignores signer subset");
auto const src = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const otherDst = randomKeyPair(KeyType::secp256k1);
auto const signerA = randomKeyPair(KeyType::secp256k1);
auto const signerB = randomKeyPair(KeyType::secp256k1);
auto const signerC = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(src.first), calcAccountID(dst.first));
ExportResultBuilder::SignatureSnapshot subsetAB;
subsetAB.emplace(
signerA.first,
ExportResultBuilder::signExportedTxn(
innerTx, signerA.first, signerA.second));
subsetAB.emplace(
signerB.first,
ExportResultBuilder::signExportedTxn(
innerTx, signerB.first, signerB.second));
ExportResultBuilder::SignatureSnapshot subsetBC;
subsetBC.emplace(
signerB.first,
ExportResultBuilder::signExportedTxn(
innerTx, signerB.first, signerB.second));
subsetBC.emplace(
signerC.first,
ExportResultBuilder::signExportedTxn(
innerTx, signerC.first, signerC.second));
auto const signedAB =
makeSTTx(ExportResultBuilder::buildMultiSignedExportedTxn(
innerTx, subsetAB));
auto const signedBC =
makeSTTx(ExportResultBuilder::buildMultiSignedExportedTxn(
innerTx, subsetBC));
BEAST_EXPECT(
signedAB.getTransactionID() != signedBC.getTransactionID());
auto const intentHash = ExportResultBuilder::exportIntentHash(innerTx);
BEAST_EXPECT(
ExportResultBuilder::exportIntentHash(signedAB) == intentHash);
BEAST_EXPECT(
ExportResultBuilder::exportIntentHash(signedBC) == intentHash);
auto singleAuthorized = innerTx;
singleAuthorized.setFieldVL(sfSigningPubKey, signerA.first.slice());
singleAuthorized.setFieldVL(sfTxnSignature, Blob{1, 2, 3});
BEAST_EXPECT(
ExportResultBuilder::exportIntentHash(singleAuthorized) ==
intentHash);
auto const otherIntent = makeExportedPayment(
calcAccountID(src.first), calcAccountID(otherDst.first));
BEAST_EXPECT(
ExportResultBuilder::exportIntentHash(otherIntent) != intentHash);
Blob const claimSignatureA{1, 2, 3};
auto const claimA = makeExportedPaymentChannelClaim(
calcAccountID(src.first), claimSignatureA);
auto const claimB = makeExportedPaymentChannelClaim(
calcAccountID(src.first), Blob{4, 5, 6});
BEAST_EXPECT(
ExportResultBuilder::exportIntentHash(claimA) ==
ExportResultBuilder::exportIntentHash(claimB));
auto const normalizedClaim =
ExportResultBuilder::buildMultiSignedExportedTxn(claimA, {});
BEAST_EXPECT(
normalizedClaim.getFieldVL(sfSignature) == claimSignatureA);
}
void
testCapsSignerArray()
{
testcase("caps exported signer array");
auto const src = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(src.first), calcAccountID(dst.first));
ExportResultBuilder::SignatureSnapshot signatures;
while (signatures.size() < STTx::maxMultiSigners() + 4)
{
auto const signer = randomKeyPair(KeyType::secp256k1);
signatures.emplace(
signer.first,
ExportResultBuilder::signExportedTxn(
innerTx, signer.first, signer.second));
}
auto const multiSigned =
ExportResultBuilder::buildMultiSignedExportedTxn(
innerTx, signatures);
BEAST_EXPECT(multiSigned.isFieldPresent(sfSigners));
if (multiSigned.isFieldPresent(sfSigners))
{
auto const& signers = multiSigned.getFieldArray(sfSigners);
BEAST_EXPECT(signers.size() == STTx::maxMultiSigners());
for (std::size_t i = 1; i < signers.size(); ++i)
{
BEAST_EXPECT(
signers[i - 1].getAccountID(sfAccount) <
signers[i].getAccountID(sfAccount));
}
}
}
void
testSignatureWitnessRoundTrip()
{
testcase("signature witness round trip");
auto const src = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(src.first), calcAccountID(dst.first));
auto const exportTxHash = makeHash("outer-export-witness");
ExportResultBuilder::SignatureSnapshot signatures;
while (signatures.size() < STTx::maxMultiSigners())
{
auto const signer = randomKeyPair(KeyType::secp256k1);
signatures.emplace(
signer.first,
ExportResultBuilder::signExportedTxn(
innerTx, signer.first, signer.second));
}
auto const positioned = positionedSignatures(signatures);
Blob const contributors(
STTx::maxMultiSigners() / 8, std::uint8_t{0xFF});
auto witness = ExportResultBuilder::buildSignatureWitness(
exportTxHash, innerTx, positioned, STTx::maxMultiSigners(), 654);
BEAST_EXPECT(witness.getTxnType() == ttEXPORT_SIGNATURES);
BEAST_EXPECT(witness.getFieldU32(sfLedgerSequence) == 654);
BEAST_EXPECT(witness.getFieldH256(sfTransactionHash) == exportTxHash);
BEAST_EXPECT(witness.getFieldVL(sfExportContributors) == contributors);
BEAST_EXPECT(!witness.isFieldPresent(sfSigners));
auto const& exported =
witness.peekAtField(sfExportedTxn).downcast<STObject>();
BEAST_EXPECT(exported.getFieldU16(sfTransactionType) == ttPAYMENT);
BEAST_EXPECT(!exported.isFieldPresent(sfSigners));
BEAST_EXPECT(
witness.getFieldArray(sfExportSigners).size() ==
STTx::maxMultiSigners());
auto decoded = ExportResultBuilder::signaturesFromWitness(witness);
BEAST_EXPECT(decoded);
if (decoded)
{
BEAST_EXPECT(decoded->size() == STTx::maxMultiSigners());
std::uint16_t expectedPosition = 0;
for (auto const& [position, signature] : *decoded)
{
BEAST_EXPECT(position == expectedPosition++);
auto const it = signatures.find(signature.signingKey);
BEAST_EXPECT(it != signatures.end());
if (it != signatures.end())
BEAST_EXPECT(it->second == signature.signature);
}
}
}
void
testSparseSignatureWitnessRoundTrip()
{
testcase("sparse cross-byte signature witness round trip");
auto const src = deterministicKeyPair("d-attrib-source");
auto const dst = deterministicKeyPair("d-attrib-destination");
auto const signer0 = deterministicKeyPair("d-attrib-signer-0");
auto const signer7 = deterministicKeyPair("d-attrib-signer-7");
auto const signer9 = deterministicKeyPair("d-attrib-signer-9");
auto const innerTx = makeExportedPayment(
calcAccountID(src.first), calcAccountID(dst.first));
ExportResultBuilder::PositionedSignatureSnapshot signatures;
for (auto const& [position, signer] : std::array{
std::pair{std::uint16_t{0}, std::cref(signer0)},
std::pair{std::uint16_t{7}, std::cref(signer7)},
std::pair{std::uint16_t{9}, std::cref(signer9)}})
{
signatures.emplace(
position,
ExportResultBuilder::PositionedSignature{
signer.get().first,
ExportResultBuilder::signExportedTxn(
innerTx, signer.get().first, signer.get().second)});
}
auto const witness = ExportResultBuilder::buildSignatureWitness(
makeHash("d-attrib-export"), innerTx, signatures, 10, 654);
Blob const expectedContributors{0x81, 0x02};
BEAST_EXPECT(
witness.getFieldVL(sfExportContributors) == expectedContributors);
auto const serialized = witness.getSerializer();
BEAST_EXPECT(
strHex(serialized.slice()) ==
"12006A2400000000260000028E5316E18499CA512AC0B68C103C249FF20029D4"
"6CEFCCD8E6882A481768546DCBF368400000000000000073007023028102811400"
"00000000000000000000000000000000000000E0591200002280000000240000"
"0000201A00000002201B000000062029000000016140000000000F424068400000"
"000000000A7300811431C972A33313AC474C4996CF7463C8D5648AA1B283143A9"
"D59046AB0269024A1C12118FB57F514A891BEE1F017E011732102006E9F20ADB30"
"B695D059FEA08BF342FA6BD712433B769FD8D1A40098F4780F674463044022033"
"A39198530AD920FB0F21A041E4CFD2C9509C085B541BD1058F2B71F8D3F01A02"
"206B203D44035D8D653F5F146CF05EA769FB60ABC6246D3EC42CD5B14C541B583"
"3E1E0117321030D2368AFD1505BCFDCF91815E073871917B2E276CF1999E0B781"
"E9EC681B68D774473045022100824DDF6B8F117F53F11B35EFACC608676B3A0C2"
"19591A01E446BFAE747DAE6AC02205955CB02DAA217DE0E3617E0B75E66FE5D09"
"6326C4E754DDC46DFCEB992CC394E1E0117321028B8C98F69CFF73B9D66CC108"
"8161E10E9B31AE2C075BADC4656C27D10EDE23D774473045022100C4F6FC146A"
"2406E20E527572957265FB9EAA3C2F22D40D3B5924880D8E70797602202E54B5"
"C0FDDD58D45909F7B47FB5B90E9DE3DD57A1B167E4FDA10CF48E2BFD0FE1F1");
BEAST_EXPECT(
to_string(witness.getTransactionID()) ==
"17BEE8F0DF68D5226CAA778C9DF1B768618AC36ADB0CF6A49A62248173357EF8");
SerialIter iter{serialized.slice()};
STTx roundTripped{std::ref(iter)};
BEAST_EXPECT(
roundTripped.getFieldVL(sfExportContributors) ==
expectedContributors);
BEAST_EXPECT(
roundTripped.getTransactionID() == witness.getTransactionID());
auto const decoded =
ExportResultBuilder::signaturesFromWitness(roundTripped);
BEAST_EXPECT(decoded);
if (decoded)
{
BEAST_EXPECT(decoded->size() == signatures.size());
auto expected = signatures.begin();
for (auto const& [position, signature] : *decoded)
{
BEAST_EXPECT(expected != signatures.end());
if (expected == signatures.end())
break;
BEAST_EXPECT(position == expected->first);
BEAST_EXPECT(
signature.signingKey == expected->second.signingKey);
BEAST_EXPECT(signature.signature == expected->second.signature);
++expected;
}
BEAST_EXPECT(expected == signatures.end());
}
}
void
testDestinationSignerOrderIsIndependent()
{
testcase("destination signer order is independent of witness order");
auto const src = deterministicKeyPair("d-attrib-order-source");
auto const dst = deterministicKeyPair("d-attrib-order-destination");
auto const innerTx = makeExportedPayment(
calcAccountID(src.first), calcAccountID(dst.first));
std::array signers{
deterministicKeyPair("d-attrib-order-signer-0"),
deterministicKeyPair("d-attrib-order-signer-1"),
deterministicKeyPair("d-attrib-order-signer-2")};
std::sort(
signers.begin(),
signers.end(),
[](auto const& lhs, auto const& rhs) {
return calcAccountID(lhs.first) < calcAccountID(rhs.first);
});
ExportResultBuilder::PositionedSignatureSnapshot positioned;
std::array<std::uint16_t, 3> const positions{0, 7, 9};
for (std::size_t i = 0; i < signers.size(); ++i)
{
auto const& signer = signers[signers.size() - i - 1];
positioned.emplace(
positions[i],
ExportResultBuilder::PositionedSignature{
signer.first,
ExportResultBuilder::signExportedTxn(
innerTx, signer.first, signer.second)});
}
auto const witness = ExportResultBuilder::buildSignatureWitness(
makeHash("d-attrib-order-export"), innerTx, positioned, 10, 654);
auto const& witnessEntries = witness.getFieldArray(sfExportSigners);
BEAST_EXPECT(witnessEntries.size() == signers.size());
if (witnessEntries.size() == signers.size())
{
for (std::size_t i = 0; i < witnessEntries.size(); ++i)
{
auto const key = PublicKey{
makeSlice(witnessEntries[i].getFieldVL(sfSigningPubKey))};
BEAST_EXPECT(
calcAccountID(key) ==
calcAccountID(signers[signers.size() - i - 1].first));
}
}
auto const decoded =
ExportResultBuilder::signaturesFromWitness(witness);
BEAST_EXPECT(decoded);
if (!decoded)
return;
ExportResultBuilder::SignatureSnapshot destinationSignatures;
for (auto const& [_, signature] : *decoded)
destinationSignatures.emplace(
signature.signingKey, signature.signature);
auto const assembled = ExportResultBuilder::buildMultiSignedExportedTxn(
innerTx, destinationSignatures);
auto const& destinationEntries = assembled.getFieldArray(sfSigners);
BEAST_EXPECT(destinationEntries.size() == signers.size());
if (destinationEntries.size() == signers.size())
{
for (std::size_t i = 0; i < destinationEntries.size(); ++i)
{
BEAST_EXPECT(
destinationEntries[i].getAccountID(sfAccount) ==
calcAccountID(signers[i].first));
}
}
}
void
testRejectsMalformedWitnessSigners()
{
testcase("signature witness rejects malformed signer entries");
auto const src = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const signer0 = randomKeyPair(KeyType::secp256k1);
auto const signer1 = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(src.first), calcAccountID(dst.first));
auto const exportTxHash = makeHash("bad-witness-signer");
ExportResultBuilder::PositionedSignatureSnapshot signatures;
signatures.emplace(
0,
ExportResultBuilder::PositionedSignature{
signer0.first,
ExportResultBuilder::signExportedTxn(
innerTx, signer0.first, signer0.second)});
signatures.emplace(
1,
ExportResultBuilder::PositionedSignature{
signer1.first,
ExportResultBuilder::signExportedTxn(
innerTx, signer1.first, signer1.second)});
auto const witness = ExportResultBuilder::buildSignatureWitness(
exportTxHash, innerTx, signatures, 2, 654);
BEAST_EXPECT(ExportResultBuilder::signaturesFromWitness(witness));
using Mutator = void (*)(STArray&);
struct MalformedCase
{
char const* name;
Mutator mutate;
};
std::array<MalformedCase, 8> const malformedCases{{
{"missing public key",
[](STArray& entries) { entries[0].delField(sfSigningPubKey); }},
{"missing signature",
[](STArray& entries) { entries[0].delField(sfTxnSignature); }},
{"extra account",
[](STArray& entries) {
entries[0].setAccountID(sfAccount, AccountID{});
}},
{"wrong entry type",
[](STArray& entries) { entries[0].setFName(sfSigner); }},
{"malformed public key",
[](STArray& entries) {
entries[0].setFieldVL(sfSigningPubKey, Blob{0x02});
}},
{"empty signature",
[](STArray& entries) {
entries[0].setFieldVL(sfTxnSignature, Blob{});
}},
{"oversized signature",
[](STArray& entries) {
entries[0].setFieldVL(
sfTxnSignature,
Blob(
ExportLimits::maxCanonicalExportSignatureBytes + 1,
0x30));
}},
{"duplicate key",
[](STArray& entries) {
entries[1].setFieldVL(
sfSigningPubKey, entries[0].getFieldVL(sfSigningPubKey));
}},
}};
for (auto const& malformedCase : malformedCases)
{
auto malformed = witness;
auto& entries = malformed.peekFieldArray(sfExportSigners);
malformedCase.mutate(entries);
BEAST_EXPECTS(
!ExportResultBuilder::signaturesFromWitness(malformed),
malformedCase.name);
}
auto malformedBitmap = ExportResultBuilder::buildSignatureWitness(
exportTxHash, innerTx, signatures, 2, 654);
malformedBitmap.setFieldVL(sfExportContributors, Blob{0x00});
BEAST_EXPECT(
!ExportResultBuilder::signaturesFromWitness(malformedBitmap));
ExportResultBuilder::PositionedSignatureSnapshot oversized;
for (std::uint16_t position = 0;
position <= ExportLimits::maxCommitteeMembers;
++position)
{
auto const key = randomKeyPair(KeyType::secp256k1).first;
oversized.emplace(
position,
ExportResultBuilder::PositionedSignature{key, Buffer{0x01}});
}
except([&] {
ExportResultBuilder::buildSignatureWitness(
exportTxHash, innerTx, oversized, oversized.size(), 654);
});
}
void
testSerializedSizeInventory()
{
//@@start export-serialized-size-inventory
testcase("serialized size inventory");
auto const src = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto innerTx = makeExportedPayment(
calcAccountID(src.first), calcAccountID(dst.first));
innerTx.setFieldArray(sfMemos, STArray(sfMemos, 1));
STObject memo{sfMemo};
memo.setFieldVL(
sfMemoType,
Blob{
'x',
'a',
'h',
'a',
'u',
'-',
'e',
'x',
'p',
'o',
'r',
't',
'-',
'v',
'1'});
memo.setFieldVL(sfMemoData, Blob(45, 0xA5));
innerTx.peekFieldArray(sfMemos).emplace_back(std::move(memo));
std::array<std::uint8_t, 72> signatureBytes;
signatureBytes.fill(0x5A);
ExportResultBuilder::SignatureSnapshot signatures;
while (signatures.size() < STTx::maxMultiSigners())
{
auto const signer = randomKeyPair(KeyType::secp256k1);
signatures.emplace(
signer.first,
Buffer{signatureBytes.data(), signatureBytes.size()});
}
auto const multiSigned =
ExportResultBuilder::buildMultiSignedExportedTxn(
innerTx, signatures);
auto const positioned = positionedSignatures(signatures);
auto const witness = ExportResultBuilder::buildSignatureWitness(
makeHash("size-inventory-export"),
innerTx,
positioned,
STTx::maxMultiSigners(),
654);
auto const release = ExportOriginMemo::releaseForm(
innerTx,
ExportOriginMemo::Origin{21337, 0, makeHash("size-origin")},
ExportOriginMemo::Anchor{653, makeHash("size-ledger")});
BEAST_EXPECT(release);
if (!release)
return;
auto const releaseWitness = ExportResultBuilder::buildSignatureWitness(
makeHash("size-inventory-export"),
release.value(),
positioned,
STTx::maxMultiSigners(),
654);
auto const innerBytes = innerTx.getSerializer().size();
auto const multiSignedBytes = multiSigned.getSerializer().size();
auto const selfContainedWitnessBytes = witness.getSerializer().size();
auto const releaseWitnessBytes = releaseWitness.getSerializer().size();
auto const pricedWitnessBytes = innerBytes +
ExportLimits::feeWitnessFixedAllowanceBytes +
STTx::maxMultiSigners() *
ExportLimits::feeWitnessSignerAllowanceBytes;
constexpr std::size_t legacyShareBytes = 32 + 33 + 72;
log << "Export serialized-size inventory:\n"
<< " unsigned target + issuance Memo: " << innerBytes << "\n"
<< " 32-signer target: " << multiSignedBytes << "\n"
<< " self-contained 32-signer witness: "
<< selfContainedWitnessBytes << "\n"
<< " stamped self-contained witness: " << releaseWitnessBytes
<< "\n"
<< " one legacy share blob: " << legacyShareBytes << "\n"
<< " 32 legacy share blobs: "
<< legacyShareBytes * STTx::maxMultiSigners() << std::endl;
BEAST_EXPECT(signatures.size() == STTx::maxMultiSigners());
BEAST_EXPECT(!witness.isFieldPresent(sfSigners));
auto const& exported =
witness.peekAtField(sfExportedTxn).downcast<STObject>();
BEAST_EXPECT(!exported.isFieldPresent(sfSigners));
BEAST_EXPECT(
witness.getFieldArray(sfExportSigners).size() ==
STTx::maxMultiSigners());
Blob const contributors(
STTx::maxMultiSigners() / 8, std::uint8_t{0xFF});
BEAST_EXPECT(witness.getFieldVL(sfExportContributors) == contributors);
BEAST_EXPECT(innerBytes == 163);
BEAST_EXPECT(multiSignedBytes == 4453);
BEAST_EXPECT(selfContainedWitnessBytes == 3839);
BEAST_EXPECT(pricedWitnessBytes == 4643);
BEAST_EXPECT(releaseWitnessBytes > selfContainedWitnessBytes);
BEAST_EXPECT(pricedWitnessBytes >= releaseWitnessBytes);
BEAST_EXPECT(pricedWitnessBytes <= ExportLimits::maxExportWitnessBytes);
BEAST_EXPECT(legacyShareBytes * STTx::maxMultiSigners() == 4384);
STArray oversizedMemos(sfMemos);
STObject oversizedMemo(sfMemo);
oversizedMemo.setFieldVL(sfMemoData, Blob(6'000, 0xCC));
oversizedMemos.emplace_back(std::move(oversizedMemo));
auto oversizedTarget = innerTx;
oversizedTarget.setFieldArray(sfMemos, oversizedMemos);
except([&] {
ExportResultBuilder::buildSignatureWitness(
makeHash("oversized-witness"),
oversizedTarget,
positioned,
STTx::maxMultiSigners(),
654);
});
auto oversizedWitness = witness;
auto& oversizedExported = const_cast<STObject&>(
oversizedWitness.peekAtField(sfExportedTxn).downcast<STObject>());
oversizedExported.setFieldArray(sfMemos, std::move(oversizedMemos));
BEAST_EXPECT(
!ExportResultBuilder::signaturesFromWitness(oversizedWitness));
//@@end export-serialized-size-inventory
}
void
testReleaseTargetSizeBoundary()
{
testcase("release target serialized size boundary");
auto const src = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const signer = randomKeyPair(KeyType::secp256k1);
auto const atLimit = makeExportedPaymentWithSize(
calcAccountID(src.first),
calcAccountID(dst.first),
ExportLimits::maxExportReleaseTargetBytes);
auto const overLimit = makeExportedPaymentWithSize(
calcAccountID(src.first),
calcAccountID(dst.first),
ExportLimits::maxExportReleaseTargetBytes + 1);
BEAST_EXPECT(atLimit);
BEAST_EXPECT(overLimit);
if (!atLimit || !overLimit)
return;
ExportResultBuilder::PositionedSignatureSnapshot signatures;
signatures.emplace(
0,
ExportResultBuilder::PositionedSignature{
signer.first,
ExportResultBuilder::signExportedTxn(
*atLimit, signer.first, signer.second)});
auto const witness = ExportResultBuilder::buildSignatureWitness(
makeHash("target-at-limit"), *atLimit, signatures, 1, 654);
BEAST_EXPECT(ExportResultBuilder::signaturesFromWitness(witness));
except([&] {
ExportResultBuilder::buildSignatureWitness(
makeHash("target-over-limit"), *overLimit, signatures, 1, 654);
});
auto oversizedWitness = witness;
auto& embedded = const_cast<STObject&>(
oversizedWitness.peekAtField(sfExportedTxn).downcast<STObject>());
embedded.setFieldArray(sfMemos, overLimit->getFieldArray(sfMemos));
Serializer embeddedBytes;
embedded.add(embeddedBytes);
BEAST_EXPECT(
embeddedBytes.size() ==
ExportLimits::maxExportReleaseTargetBytes + 1);
BEAST_EXPECT(
!ExportResultBuilder::signaturesFromWitness(oversizedWitness));
}
void
run() override
{
testAssemblesMultiSignedTransaction();
testBuildMultiSignedExportedTxnDirect();
testExportIntentHashIgnoresSignerSubset();
testCapsSignerArray();
testSignatureWitnessRoundTrip();
testSparseSignatureWitnessRoundTrip();
testDestinationSignerOrderIsIndependent();
testRejectsMalformedWitnessSigners();
testSerializedSizeInventory();
testReleaseTargetSizeBoundary();
}
};
BEAST_DEFINE_TESTSUITE(ExportResultBuilder, app, ripple);
} // namespace test
} // namespace ripple
+265
View File
@@ -0,0 +1,265 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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 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/ExportSigCollector.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/digest.h>
namespace ripple {
namespace test {
namespace {
uint256
origin(std::uint32_t value)
{
Serializer s;
s.add32(value);
return sha512Half(s.slice());
}
PublicKey
publicKey(char const* hex)
{
auto const raw = strUnHex(hex);
return PublicKey{makeSlice(*raw)};
}
Buffer
signature(std::uint8_t value)
{
std::uint8_t bytes[] = {value, std::uint8_t(value + 1)};
return Buffer{bytes, sizeof(bytes)};
}
} // namespace
class ExportSigCollector_test : public beast::unit_test::suite
{
PublicKey const keyA_ = publicKey(
"0388935426E0D08083314842EDFBB2D517BD47699F9A4527318A8E10468C97C05"
"2");
PublicKey const keyB_ = randomKeyPair(KeyType::secp256k1).first;
static ExportSigCollector::Contribution
contribution(
ExportSigCollector::Position position,
PublicKey const& key,
std::uint8_t sig)
{
return {position, key, signature(sig)};
}
public:
void
testAdmissionAndConflict()
{
testcase("admission and absorbing conflict");
ExportSigCollector collector;
auto const w = origin(1);
BEAST_EXPECT(collector.registerOrigin(w, 9));
auto first = collector.beginAttributedAdmission(
w, contribution(7, keyA_, 1), 10);
BEAST_EXPECT(first.result == ExportSigCollector::BeginResult::verify);
BEAST_EXPECT(first.ticket.has_value());
BEAST_EXPECT(collector.fullUnionSnapshot().empty());
if (!first.ticket)
return;
auto accepted =
collector.admitContribution(std::move(*first.ticket), true, 10);
BEAST_EXPECT(
accepted.result == ExportSigCollector::AdmitResult::accepted);
auto snapshot = collector.fullUnionSnapshot();
BEAST_EXPECT(snapshot.at(w).size() == 1);
BEAST_EXPECT(snapshot.at(w).front().position == 7);
auto duplicate = collector.beginAttributedAdmission(
w, contribution(7, keyA_, 1), 11);
BEAST_EXPECT(
duplicate.result == ExportSigCollector::BeginResult::duplicate);
auto second = collector.beginAttributedAdmission(
w, contribution(7, keyB_, 2), 11);
auto third = collector.beginAttributedAdmission(
w, contribution(7, keyA_, 3), 11);
BEAST_EXPECT(second.ticket.has_value());
BEAST_EXPECT(third.result == ExportSigCollector::BeginResult::capacity);
if (!second.ticket)
return;
auto conflicted =
collector.admitContribution(std::move(*second.ticket), true, 11);
BEAST_EXPECT(
conflicted.result == ExportSigCollector::AdmitResult::conflicted);
BEAST_EXPECT(conflicted.priorContribution.has_value());
BEAST_EXPECT(conflicted.conflictingContribution.has_value());
BEAST_EXPECT(
collector.positionStatus(w, 7) ==
ExportSigCollector::PositionStatus::conflicted);
BEAST_EXPECT(collector.fullUnionSnapshot().empty());
BEAST_EXPECT(
collector.beginAttributedAdmission(w, contribution(7, keyA_, 4), 12)
.result == ExportSigCollector::BeginResult::conflicted);
}
void
testReservationAndOriginLifecycle()
{
testcase("verification reservations and origin registration");
ExportSigCollector collector;
auto const w = origin(2);
BEAST_EXPECT(collector.registerOrigin(w, 19));
auto invalid = collector.beginAttributedAdmission(
w, contribution(3, keyA_, 10), 20);
BEAST_EXPECT(invalid.ticket.has_value());
if (!invalid.ticket)
return;
BEAST_EXPECT(
collector.admitContribution(std::move(*invalid.ticket), false, 20)
.result == ExportSigCollector::AdmitResult::invalid);
auto valid = collector.beginAttributedAdmission(
w, contribution(3, keyA_, 11), 20);
BEAST_EXPECT(valid.ticket.has_value());
if (!valid.ticket)
return;
BEAST_EXPECT(
collector.admitContribution(std::move(*valid.ticket), true, 20)
.result == ExportSigCollector::AdmitResult::accepted);
auto other = collector.beginAttributedAdmission(
w, contribution(4, keyB_, 12), 20);
BEAST_EXPECT(other.ticket.has_value());
if (!other.ticket)
return;
BEAST_EXPECT(
collector.admitContribution(std::move(*other.ticket), true, 20)
.result == ExportSigCollector::AdmitResult::accepted);
BEAST_EXPECT(collector.registerOrigin(w, 21));
BEAST_EXPECT(
collector.positionStatus(w, 3) ==
ExportSigCollector::PositionStatus::unique);
BEAST_EXPECT(collector.fullUnionSnapshot().at(w).size() == 2);
// Idempotent registration refreshes the stale-cleanup cursor.
collector.cleanupStale(277);
BEAST_EXPECT(!collector.fullUnionSnapshot().empty());
collector.cleanupStale(278);
BEAST_EXPECT(collector.fullUnionSnapshot().empty());
}
void
testMalformedBoundaries()
{
testcase("malformed contribution boundaries");
ExportSigCollector collector;
auto good = contribution(0, keyA_, 1);
BEAST_EXPECT(
collector.beginAttributedAdmission(uint256{}, good).result ==
ExportSigCollector::BeginResult::malformed);
auto const w = origin(3);
BEAST_EXPECT(
collector.beginAttributedAdmission(w, good, 1).result ==
ExportSigCollector::BeginResult::unknownOrigin);
BEAST_EXPECT(collector.registerOrigin(w, 1));
good.position = ExportLimits::maxCommitteeMembers;
BEAST_EXPECT(
collector.beginAttributedAdmission(w, good, 1).result ==
ExportSigCollector::BeginResult::malformed);
good.position = 0;
good.signature = Buffer{};
BEAST_EXPECT(
collector.beginAttributedAdmission(w, good, 1).result ==
ExportSigCollector::BeginResult::malformed);
std::vector<std::uint8_t> oversized(
ExportLimits::maxCanonicalExportSignatureBytes + 1, 0xAB);
good.signature = Buffer{oversized.data(), oversized.size()};
BEAST_EXPECT(
collector.beginAttributedAdmission(w, good, 1).result ==
ExportSigCollector::BeginResult::malformed);
good = contribution(5, keyA_, 5);
auto abandoned = collector.beginAttributedAdmission(w, good, 2);
BEAST_EXPECT(abandoned.ticket.has_value());
if (!abandoned.ticket)
return;
BEAST_EXPECT(collector.cancelAdmission(std::move(*abandoned.ticket)));
BEAST_EXPECT(
collector.beginAttributedAdmission(w, good, 2).result ==
ExportSigCollector::BeginResult::verify);
auto expired =
collector.beginAttributedAdmission(w, contribution(6, keyA_, 6), 3);
BEAST_EXPECT(expired.ticket.has_value());
BEAST_EXPECT(
collector.beginAttributedAdmission(w, contribution(6, keyB_, 7), 5)
.result == ExportSigCollector::BeginResult::verify);
collector.clear(w);
BEAST_EXPECT(
collector.beginAttributedAdmission(w, good, 7).result ==
ExportSigCollector::BeginResult::unknownOrigin);
BEAST_EXPECT(collector.registerOrigin(w, 7));
}
void
testOriginRegistrationBounds()
{
testcase("origin registration bounds");
ExportSigCollector collector;
BEAST_EXPECT(!collector.registerOrigin(uint256{}, 1));
BEAST_EXPECT(!collector.registerOrigin(origin(100), 0));
bool registeredAll = true;
for (std::size_t i = 0; i < ExportSigCollector::maxTrackedOrigins; ++i)
registeredAll =
collector.registerOrigin(
origin(static_cast<std::uint32_t>(i + 1'000)), 1) &&
registeredAll;
BEAST_EXPECT(registeredAll);
BEAST_EXPECT(!collector.registerOrigin(origin(99'999), 1));
BEAST_EXPECT(collector.registerOrigin(origin(1'000), 2));
}
void
run() override
{
testAdmissionAndConflict();
testReservationAndOriginLifecycle();
testMalformedBoundaries();
testOriginRegistrationBounds();
}
};
BEAST_DEFINE_TESTSUITE(ExportSigCollector, app, ripple);
} // namespace test
} // namespace ripple
File diff suppressed because it is too large. Load diff
+505
View File
@@ -0,0 +1,505 @@
// This file is generated by hookz build-test-hooks
#ifndef EXPORT_TEST_WASM_INCLUDED
#define EXPORT_TEST_WASM_INCLUDED
#include <map>
#include <stdint.h>
#include <string>
#include <vector>
namespace ripple {
namespace test {
inline std::map<std::string, std::vector<uint8_t>> export_test_wasm = {
/* ==== WASM: 0 ==== */
{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 xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t committee_hash_ptr, uint32_t committee_hash_len, uint64_t callback_fee_drops);
extern int64_t xport_reserve(uint32_t count);
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
extern int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t name_ptr, uint32_t name_len);
extern int64_t otxn_type(void);
extern int64_t ledger_seq(void);
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x) if (!(x)) rollback((uint32_t)#x, sizeof(#x), __LINE__)
#define ttPAYMENT 0
#define tfCANONICAL 0x80000000UL
#define amAMOUNT 1
#define amFEE 8
#define atACCOUNT 1
#define atDESTINATION 3
#define ENCODE_TT(buf_out, tt) \
buf_out[0] = 0x12U; \
buf_out[1] = (tt >> 8) & 0xFFU; \
buf_out[2] = tt & 0xFFU; \
buf_out += 3;
#define ENCODE_FLAGS(buf_out, flags) \
buf_out[0] = 0x22U; \
buf_out[1] = (flags >> 24) & 0xFFU; \
buf_out[2] = (flags >> 16) & 0xFFU; \
buf_out[3] = (flags >> 8) & 0xFFU; \
buf_out[4] = flags & 0xFFU; \
buf_out += 5;
#define ENCODE_SEQUENCE(buf_out, seq) \
buf_out[0] = 0x24U; \
buf_out[1] = (seq >> 24) & 0xFFU; \
buf_out[2] = (seq >> 16) & 0xFFU; \
buf_out[3] = (seq >> 8) & 0xFFU; \
buf_out[4] = seq & 0xFFU; \
buf_out += 5;
#define ENCODE_FLS(buf_out, fls) \
buf_out[0] = 0x20U; \
buf_out[1] = 0x1AU; \
buf_out[2] = (fls >> 24) & 0xFFU; \
buf_out[3] = (fls >> 16) & 0xFFU; \
buf_out[4] = (fls >> 8) & 0xFFU; \
buf_out[5] = fls & 0xFFU; \
buf_out += 6;
#define ENCODE_LLS(buf_out, lls) \
buf_out[0] = 0x20U; \
buf_out[1] = 0x1BU; \
buf_out[2] = (lls >> 24) & 0xFFU; \
buf_out[3] = (lls >> 16) & 0xFFU; \
buf_out[4] = (lls >> 8) & 0xFFU; \
buf_out[5] = lls & 0xFFU; \
buf_out += 6;
#define ENCODE_DROPS(buf_out, drops, amt_type) \
buf_out[0] = 0x60U + amt_type; \
buf_out[1] = 0x40U + ((drops >> 56) & 0x3FU); \
buf_out[2] = (drops >> 48) & 0xFFU; \
buf_out[3] = (drops >> 40) & 0xFFU; \
buf_out[4] = (drops >> 32) & 0xFFU; \
buf_out[5] = (drops >> 24) & 0xFFU; \
buf_out[6] = (drops >> 16) & 0xFFU; \
buf_out[7] = (drops >> 8) & 0xFFU; \
buf_out[8] = drops & 0xFFU; \
buf_out += 9;
#define ENCODE_SIGNING_PUBKEY_EMPTY(buf_out) \
buf_out[0] = 0x73U; \
buf_out[1] = 0x00U; \
buf_out += 2;
#define ENCODE_ACCOUNT(buf_out, acc, acc_type) \
buf_out[0] = 0x80U + acc_type; \
buf_out[1] = 0x14U; \
for (int i = 0; i < 20; ++i) buf_out[2+i] = acc[i]; \
buf_out += 22;
#define PREPARE_PAYMENT_SIMPLE_SIZE 270U
int64_t hook(uint32_t reserved) {
_g(1, 1);
if (otxn_type() != ttPAYMENT)
return accept(0, 0, 0);
ASSERT(xport_reserve(1) == 1);
uint8_t dst[20];
int64_t dst_len = otxn_param(SBUF(dst), "DST", 3);
ASSERT(dst_len == 20);
uint8_t acc[20];
ASSERT(hook_account(SBUF(acc)) == 20);
uint8_t committee[32];
ASSERT(otxn_param(SBUF(committee), "COMMITTEE", 9) == 32);
uint32_t cls = (uint32_t)ledger_seq();
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
uint8_t* buf = tx;
ENCODE_TT(buf, ttPAYMENT);
ENCODE_FLAGS(buf, tfCANONICAL);
ENCODE_SEQUENCE(buf, 0);
ENCODE_FLS(buf, cls + 1);
ENCODE_LLS(buf, cls + 5);
// sfTicketSequence = UINT32 field 41 = 0x20 0x29
buf[0] = 0x20U; buf[1] = 0x29U;
buf[2] = 0; buf[3] = 0; buf[4] = 0; buf[5] = 1;
buf += 6;
uint64_t drops = 1000000;
ENCODE_DROPS(buf, drops, amAMOUNT);
ENCODE_DROPS(buf, 10, amFEE);
ENCODE_SIGNING_PUBKEY_EMPTY(buf);
ENCODE_ACCOUNT(buf, acc, atACCOUNT);
ENCODE_ACCOUNT(buf, dst, atDESTINATION);
uint8_t hash[32];
uint8_t callback_fee_bytes[8];
uint64_t callback_fee = 0;
if (otxn_param(SBUF(callback_fee_bytes), "CBFEE", 5) == 8)
callback_fee =
((uint64_t)callback_fee_bytes[0] << 56) |
((uint64_t)callback_fee_bytes[1] << 48) |
((uint64_t)callback_fee_bytes[2] << 40) |
((uint64_t)callback_fee_bytes[3] << 32) |
((uint64_t)callback_fee_bytes[4] << 24) |
((uint64_t)callback_fee_bytes[5] << 16) |
((uint64_t)callback_fee_bytes[6] << 8) |
((uint64_t)callback_fee_bytes[7]);
int64_t xport_result = xport(
SBUF(hash), (uint32_t)tx, buf - tx, SBUF(committee), callback_fee);
ASSERT(xport_result == 32);
return accept(0, 0, 0);
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x30U,
0x07U, 0x60U, 0x00U, 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU,
0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x02U, 0x7FU,
0x7FU, 0x01U, 0x7FU, 0x60U, 0x04U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U,
0x7EU, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x07U, 0x7FU,
0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU, 0x02U, 0x8BU,
0x01U, 0x09U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U,
0x03U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x09U, 0x6FU, 0x74U, 0x78U, 0x6EU,
0x5FU, 0x74U, 0x79U, 0x70U, 0x65U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU,
0x76U, 0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x01U,
0x03U, 0x65U, 0x6EU, 0x76U, 0x0DU, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U,
0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x00U, 0x02U,
0x03U, 0x65U, 0x6EU, 0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U,
0x61U, 0x63U, 0x6BU, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU,
0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x70U, 0x61U, 0x72U, 0x61U, 0x6DU,
0x00U, 0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x68U, 0x6FU, 0x6FU,
0x6BU, 0x5FU, 0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x00U,
0x05U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x6CU, 0x65U, 0x64U, 0x67U,
0x65U, 0x72U, 0x5FU, 0x73U, 0x65U, 0x71U, 0x00U, 0x00U, 0x03U, 0x65U,
0x6EU, 0x76U, 0x05U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x00U, 0x06U,
0x03U, 0x02U, 0x01U, 0x02U, 0x05U, 0x03U, 0x01U, 0x00U, 0x02U, 0x06U,
0x08U, 0x01U, 0x7FU, 0x01U, 0x41U, 0x80U, 0x80U, 0x04U, 0x0BU, 0x07U,
0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x09U, 0x0AU,
0xD8U, 0x04U, 0x01U, 0xD5U, 0x04U, 0x02U, 0x01U, 0x7EU, 0x02U, 0x7FU,
0x23U, 0x00U, 0x41U, 0xA0U, 0x03U, 0x6BU, 0x22U, 0x00U, 0x24U, 0x00U,
0x41U, 0x01U, 0x41U, 0x01U, 0x10U, 0x00U, 0x1AU, 0x02U, 0x7EU, 0x10U,
0x01U, 0x42U, 0x00U, 0x52U, 0x04U, 0x40U, 0x41U, 0x00U, 0x41U, 0x00U,
0x42U, 0x00U, 0x10U, 0x02U, 0x0CU, 0x01U, 0x0BU, 0x41U, 0x01U, 0x10U,
0x03U, 0x42U, 0x01U, 0x52U, 0x04U, 0x40U, 0x41U, 0xD9U, 0x80U, 0x04U,
0x41U, 0x16U, 0x42U, 0xDFU, 0x00U, 0x10U, 0x04U, 0x1AU, 0x0BU, 0x20U,
0x00U, 0x41U, 0x80U, 0x03U, 0x6AU, 0x41U, 0x14U, 0x41U, 0x80U, 0x80U,
0x04U, 0x41U, 0x03U, 0x10U, 0x05U, 0x42U, 0x14U, 0x52U, 0x04U, 0x40U,
0x41U, 0xEFU, 0x80U, 0x04U, 0x41U, 0x0EU, 0x42U, 0xE3U, 0x00U, 0x10U,
0x04U, 0x1AU, 0x0BU, 0x20U, 0x00U, 0x41U, 0xE0U, 0x02U, 0x6AU, 0x41U,
0x14U, 0x10U, 0x06U, 0x42U, 0x14U, 0x52U, 0x04U, 0x40U, 0x41U, 0xFDU,
0x80U, 0x04U, 0x41U, 0x1EU, 0x42U, 0xE6U, 0x00U, 0x10U, 0x04U, 0x1AU,
0x0BU, 0x20U, 0x00U, 0x41U, 0xC0U, 0x02U, 0x6AU, 0x41U, 0x20U, 0x41U,
0x84U, 0x80U, 0x04U, 0x41U, 0x09U, 0x10U, 0x05U, 0x42U, 0x20U, 0x52U,
0x04U, 0x40U, 0x41U, 0xA7U, 0x80U, 0x04U, 0x41U, 0x32U, 0x42U, 0xE9U,
0x00U, 0x10U, 0x04U, 0x1AU, 0x0BU, 0x10U, 0x07U, 0x21U, 0x01U, 0x20U,
0x00U, 0x42U, 0x80U, 0x80U, 0x80U, 0xD0U, 0xB0U, 0x8EU, 0xC0U, 0xC0U,
0x14U, 0x37U, 0x00U, 0x5DU, 0x20U, 0x00U, 0x42U, 0x80U, 0x80U, 0x80U,
0x80U, 0xF0U, 0xC1U, 0x90U, 0xA0U, 0xE8U, 0x00U, 0x37U, 0x00U, 0x51U,
0x20U, 0x00U, 0x42U, 0xA0U, 0xD2U, 0x80U, 0x80U, 0x80U, 0xA0U, 0xC0U,
0xB0U, 0xC0U, 0x00U, 0x37U, 0x00U, 0x49U, 0x20U, 0x00U, 0x41U, 0xA0U,
0x36U, 0x3BU, 0x00U, 0x43U, 0x20U, 0x00U, 0x41U, 0xA0U, 0x34U, 0x3BU,
0x00U, 0x3DU, 0x20U, 0x00U, 0x41U, 0x00U, 0x36U, 0x00U, 0x39U, 0x20U,
0x00U, 0x41U, 0x24U, 0x3AU, 0x00U, 0x38U, 0x20U, 0x00U, 0x42U, 0x92U,
0x80U, 0x80U, 0x90U, 0x82U, 0x10U, 0x37U, 0x03U, 0x30U, 0x20U, 0x00U,
0x20U, 0x00U, 0x29U, 0x03U, 0xE0U, 0x02U, 0x37U, 0x00U, 0x65U, 0x20U,
0x00U, 0x20U, 0x00U, 0x29U, 0x03U, 0xE8U, 0x02U, 0x37U, 0x00U, 0x6DU,
0x20U, 0x00U, 0x20U, 0x00U, 0x28U, 0x02U, 0xF0U, 0x02U, 0x36U, 0x00U,
0x75U, 0x20U, 0x00U, 0x20U, 0x01U, 0xA7U, 0x22U, 0x03U, 0x41U, 0x05U,
0x6AU, 0x22U, 0x02U, 0x3AU, 0x00U, 0x48U, 0x20U, 0x00U, 0x20U, 0x02U,
0x41U, 0x08U, 0x76U, 0x3AU, 0x00U, 0x47U, 0x20U, 0x00U, 0x20U, 0x02U,
0x41U, 0x10U, 0x76U, 0x3AU, 0x00U, 0x46U, 0x20U, 0x00U, 0x20U, 0x02U,
0x41U, 0x18U, 0x76U, 0x3AU, 0x00U, 0x45U, 0x20U, 0x00U, 0x20U, 0x03U,
0x41U, 0x01U, 0x6AU, 0x22U, 0x02U, 0x3AU, 0x00U, 0x42U, 0x20U, 0x00U,
0x20U, 0x02U, 0x41U, 0x08U, 0x76U, 0x3AU, 0x00U, 0x41U, 0x20U, 0x00U,
0x20U, 0x02U, 0x41U, 0x10U, 0x76U, 0x3AU, 0x00U, 0x40U, 0x20U, 0x00U,
0x20U, 0x02U, 0x41U, 0x18U, 0x76U, 0x3AU, 0x00U, 0x3FU, 0x20U, 0x00U,
0x41U, 0x83U, 0x29U, 0x3BU, 0x00U, 0x79U, 0x20U, 0x00U, 0x20U, 0x00U,
0x29U, 0x03U, 0x80U, 0x03U, 0x37U, 0x00U, 0x7BU, 0x20U, 0x00U, 0x20U,
0x00U, 0x29U, 0x03U, 0x88U, 0x03U, 0x37U, 0x00U, 0x83U, 0x01U, 0x20U,
0x00U, 0x20U, 0x00U, 0x28U, 0x02U, 0x90U, 0x03U, 0x36U, 0x00U, 0x8BU,
0x01U, 0x42U, 0x00U, 0x21U, 0x01U, 0x20U, 0x00U, 0x41U, 0x10U, 0x6AU,
0x41U, 0x20U, 0x20U, 0x00U, 0x41U, 0x30U, 0x6AU, 0x41U, 0xDFU, 0x00U,
0x20U, 0x00U, 0x41U, 0xC0U, 0x02U, 0x6AU, 0x41U, 0x20U, 0x20U, 0x00U,
0x41U, 0x08U, 0x6AU, 0x41U, 0x08U, 0x41U, 0x8EU, 0x80U, 0x04U, 0x41U,
0x05U, 0x10U, 0x05U, 0x42U, 0x08U, 0x51U, 0x04U, 0x7EU, 0x20U, 0x00U,
0x29U, 0x00U, 0x08U, 0x22U, 0x01U, 0x42U, 0x38U, 0x86U, 0x20U, 0x01U,
0x42U, 0x80U, 0xFEU, 0x03U, 0x83U, 0x42U, 0x28U, 0x86U, 0x84U, 0x20U,
0x01U, 0x42U, 0x80U, 0x80U, 0xFCU, 0x07U, 0x83U, 0x42U, 0x18U, 0x86U,
0x20U, 0x01U, 0x42U, 0x80U, 0x80U, 0x80U, 0xF8U, 0x0FU, 0x83U, 0x42U,
0x08U, 0x86U, 0x84U, 0x84U, 0x20U, 0x01U, 0x42U, 0x08U, 0x88U, 0x42U,
0x80U, 0x80U, 0x80U, 0xF8U, 0x0FU, 0x83U, 0x20U, 0x01U, 0x42U, 0x18U,
0x88U, 0x42U, 0x80U, 0x80U, 0xFCU, 0x07U, 0x83U, 0x84U, 0x20U, 0x01U,
0x42U, 0x28U, 0x88U, 0x42U, 0x80U, 0xFEU, 0x03U, 0x83U, 0x20U, 0x01U,
0x42U, 0x38U, 0x88U, 0x84U, 0x84U, 0x84U, 0x05U, 0x20U, 0x01U, 0x0BU,
0x10U, 0x08U, 0x42U, 0x20U, 0x52U, 0x04U, 0x40U, 0x41U, 0x94U, 0x80U,
0x04U, 0x41U, 0x13U, 0x42U, 0x91U, 0x01U, 0x10U, 0x04U, 0x1AU, 0x0BU,
0x41U, 0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x02U, 0x0BU, 0x21U,
0x01U, 0x20U, 0x00U, 0x41U, 0xA0U, 0x03U, 0x6AU, 0x24U, 0x00U, 0x20U,
0x01U, 0x0BU, 0x0BU, 0xA3U, 0x01U, 0x01U, 0x00U, 0x41U, 0x80U, 0x80U,
0x04U, 0x0BU, 0x9AU, 0x01U, 0x44U, 0x53U, 0x54U, 0x00U, 0x43U, 0x4FU,
0x4DU, 0x4DU, 0x49U, 0x54U, 0x54U, 0x45U, 0x45U, 0x00U, 0x43U, 0x42U,
0x46U, 0x45U, 0x45U, 0x00U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x5FU,
0x72U, 0x65U, 0x73U, 0x75U, 0x6CU, 0x74U, 0x20U, 0x3DU, 0x3DU, 0x20U,
0x33U, 0x32U, 0x00U, 0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x70U, 0x61U,
0x72U, 0x61U, 0x6DU, 0x28U, 0x53U, 0x42U, 0x55U, 0x46U, 0x28U, 0x63U,
0x6FU, 0x6DU, 0x6DU, 0x69U, 0x74U, 0x74U, 0x65U, 0x65U, 0x29U, 0x2CU,
0x20U, 0x22U, 0x43U, 0x4FU, 0x4DU, 0x4DU, 0x49U, 0x54U, 0x54U, 0x45U,
0x45U, 0x22U, 0x2CU, 0x20U, 0x39U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U,
0x33U, 0x32U, 0x00U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x5FU, 0x72U,
0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x28U, 0x31U, 0x29U, 0x20U,
0x3DU, 0x3DU, 0x20U, 0x31U, 0x00U, 0x64U, 0x73U, 0x74U, 0x5FU, 0x6CU,
0x65U, 0x6EU, 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, 0x61U, 0x63U, 0x63U,
0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U, 0x30U,
}},
/* ==== WASM: 1 ==== */
{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 xport(uint32_t write_ptr, uint32_t write_len, uint32_t read_ptr, uint32_t read_len, uint32_t committee_hash_ptr, uint32_t committee_hash_len, uint64_t callback_fee_drops);
extern int64_t xport_reserve(uint32_t count);
extern int64_t hook_account(uint32_t write_ptr, uint32_t write_len);
extern int64_t otxn_param(uint32_t write_ptr, uint32_t write_len, uint32_t name_ptr, uint32_t name_len);
extern int64_t otxn_type(void);
extern int64_t ledger_seq(void);
#define SBUF(x) (uint32_t)(x), sizeof(x)
#define ASSERT(x) if (!(x)) rollback((uint32_t)#x, sizeof(#x), __LINE__)
#define ttPAYMENT 0
#define tfCANONICAL 0x80000000UL
#define amAMOUNT 1
#define amFEE 8
#define atACCOUNT 1
#define atDESTINATION 3
#define ENCODE_TT(buf_out, tt) \
buf_out[0] = 0x12U; \
buf_out[1] = (tt >> 8) & 0xFFU; \
buf_out[2] = tt & 0xFFU; \
buf_out += 3;
#define ENCODE_FLAGS(buf_out, flags) \
buf_out[0] = 0x22U; \
buf_out[1] = (flags >> 24) & 0xFFU; \
buf_out[2] = (flags >> 16) & 0xFFU; \
buf_out[3] = (flags >> 8) & 0xFFU; \
buf_out[4] = flags & 0xFFU; \
buf_out += 5;
#define ENCODE_SEQUENCE(buf_out, seq) \
buf_out[0] = 0x24U; \
buf_out[1] = (seq >> 24) & 0xFFU; \
buf_out[2] = (seq >> 16) & 0xFFU; \
buf_out[3] = (seq >> 8) & 0xFFU; \
buf_out[4] = seq & 0xFFU; \
buf_out += 5;
// sfNetworkID = UINT32 field 1 = 0x21
#define ENCODE_NETWORK_ID(buf_out, id) \
buf_out[0] = 0x21U; \
buf_out[1] = (id >> 24) & 0xFFU; \
buf_out[2] = (id >> 16) & 0xFFU; \
buf_out[3] = (id >> 8) & 0xFFU; \
buf_out[4] = id & 0xFFU; \
buf_out += 5;
#define ENCODE_FLS(buf_out, fls) \
buf_out[0] = 0x20U; \
buf_out[1] = 0x1AU; \
buf_out[2] = (fls >> 24) & 0xFFU; \
buf_out[3] = (fls >> 16) & 0xFFU; \
buf_out[4] = (fls >> 8) & 0xFFU; \
buf_out[5] = fls & 0xFFU; \
buf_out += 6;
#define ENCODE_LLS(buf_out, lls) \
buf_out[0] = 0x20U; \
buf_out[1] = 0x1BU; \
buf_out[2] = (lls >> 24) & 0xFFU; \
buf_out[3] = (lls >> 16) & 0xFFU; \
buf_out[4] = (lls >> 8) & 0xFFU; \
buf_out[5] = lls & 0xFFU; \
buf_out += 6;
#define ENCODE_DROPS(buf_out, drops, amt_type) \
buf_out[0] = 0x60U + amt_type; \
buf_out[1] = 0x40U + ((drops >> 56) & 0x3FU); \
buf_out[2] = (drops >> 48) & 0xFFU; \
buf_out[3] = (drops >> 40) & 0xFFU; \
buf_out[4] = (drops >> 32) & 0xFFU; \
buf_out[5] = (drops >> 24) & 0xFFU; \
buf_out[6] = (drops >> 16) & 0xFFU; \
buf_out[7] = (drops >> 8) & 0xFFU; \
buf_out[8] = drops & 0xFFU; \
buf_out += 9;
#define ENCODE_SIGNING_PUBKEY_EMPTY(buf_out) \
buf_out[0] = 0x73U; \
buf_out[1] = 0x00U; \
buf_out += 2;
#define ENCODE_ACCOUNT(buf_out, acc, acc_type) \
buf_out[0] = 0x80U + acc_type; \
buf_out[1] = 0x14U; \
for (int i = 0; i < 20; ++i) buf_out[2+i] = acc[i]; \
buf_out += 22;
#define PREPARE_PAYMENT_SIMPLE_SIZE 270U
int64_t hook(uint32_t reserved) {
_g(1, 1);
if (otxn_type() != ttPAYMENT)
return accept(0, 0, 0);
ASSERT(xport_reserve(1) == 1);
uint8_t dst[20];
int64_t dst_len = otxn_param(SBUF(dst), "DST", 3);
ASSERT(dst_len == 20);
uint8_t acc[20];
ASSERT(hook_account(SBUF(acc)) == 20);
uint8_t committee[32];
ASSERT(otxn_param(SBUF(committee), "COMMITTEE", 9) == 32);
uint32_t cls = (uint32_t)ledger_seq();
uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
uint8_t* buf = tx;
ENCODE_TT(buf, ttPAYMENT);
ENCODE_NETWORK_ID(buf, 21337); // must precede Sequence (canonical order)
ENCODE_FLAGS(buf, tfCANONICAL);
ENCODE_SEQUENCE(buf, 0);
ENCODE_FLS(buf, cls + 1);
ENCODE_LLS(buf, cls + 5);
uint64_t drops = 1000000;
ENCODE_DROPS(buf, drops, amAMOUNT);
ENCODE_DROPS(buf, 10, amFEE);
ENCODE_SIGNING_PUBKEY_EMPTY(buf);
ENCODE_ACCOUNT(buf, acc, atACCOUNT);
ENCODE_ACCOUNT(buf, dst, atDESTINATION);
uint8_t hash[32];
int64_t xport_result = xport(
SBUF(hash), (uint32_t)tx, buf - tx, SBUF(committee), 0);
// xport should return EXPORT_FAILURE (-46), ASSERT will rollback
ASSERT(xport_result == 32);
return accept(0, 0, 0);
}
)[test.hook]",
{
0x00U, 0x61U, 0x73U, 0x6DU, 0x01U, 0x00U, 0x00U, 0x00U, 0x01U, 0x30U,
0x07U, 0x60U, 0x00U, 0x01U, 0x7EU, 0x60U, 0x03U, 0x7FU, 0x7FU, 0x7EU,
0x01U, 0x7EU, 0x60U, 0x01U, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x02U, 0x7FU,
0x7FU, 0x01U, 0x7FU, 0x60U, 0x04U, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x01U,
0x7EU, 0x60U, 0x02U, 0x7FU, 0x7FU, 0x01U, 0x7EU, 0x60U, 0x07U, 0x7FU,
0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x7FU, 0x7EU, 0x01U, 0x7EU, 0x02U, 0x8BU,
0x01U, 0x09U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x02U, 0x5FU, 0x67U, 0x00U,
0x03U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x09U, 0x6FU, 0x74U, 0x78U, 0x6EU,
0x5FU, 0x74U, 0x79U, 0x70U, 0x65U, 0x00U, 0x00U, 0x03U, 0x65U, 0x6EU,
0x76U, 0x06U, 0x61U, 0x63U, 0x63U, 0x65U, 0x70U, 0x74U, 0x00U, 0x01U,
0x03U, 0x65U, 0x6EU, 0x76U, 0x0DU, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U,
0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x00U, 0x02U,
0x03U, 0x65U, 0x6EU, 0x76U, 0x08U, 0x72U, 0x6FU, 0x6CU, 0x6CU, 0x62U,
0x61U, 0x63U, 0x6BU, 0x00U, 0x01U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU,
0x6FU, 0x74U, 0x78U, 0x6EU, 0x5FU, 0x70U, 0x61U, 0x72U, 0x61U, 0x6DU,
0x00U, 0x04U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0CU, 0x68U, 0x6FU, 0x6FU,
0x6BU, 0x5FU, 0x61U, 0x63U, 0x63U, 0x6FU, 0x75U, 0x6EU, 0x74U, 0x00U,
0x05U, 0x03U, 0x65U, 0x6EU, 0x76U, 0x0AU, 0x6CU, 0x65U, 0x64U, 0x67U,
0x65U, 0x72U, 0x5FU, 0x73U, 0x65U, 0x71U, 0x00U, 0x00U, 0x03U, 0x65U,
0x6EU, 0x76U, 0x05U, 0x78U, 0x70U, 0x6FU, 0x72U, 0x74U, 0x00U, 0x06U,
0x03U, 0x02U, 0x01U, 0x02U, 0x05U, 0x03U, 0x01U, 0x00U, 0x02U, 0x06U,
0x08U, 0x01U, 0x7FU, 0x01U, 0x41U, 0x80U, 0x80U, 0x04U, 0x0BU, 0x07U,
0x08U, 0x01U, 0x04U, 0x68U, 0x6FU, 0x6FU, 0x6BU, 0x00U, 0x09U, 0x0AU,
0xE9U, 0x03U, 0x01U, 0xE6U, 0x03U, 0x02U, 0x02U, 0x7FU, 0x01U, 0x7EU,
0x23U, 0x00U, 0x41U, 0x90U, 0x03U, 0x6BU, 0x22U, 0x00U, 0x24U, 0x00U,
0x41U, 0x01U, 0x41U, 0x01U, 0x10U, 0x00U, 0x1AU, 0x02U, 0x7EU, 0x10U,
0x01U, 0x42U, 0x00U, 0x52U, 0x04U, 0x40U, 0x41U, 0x00U, 0x41U, 0x00U,
0x42U, 0x00U, 0x10U, 0x02U, 0x0CU, 0x01U, 0x0BU, 0x41U, 0x01U, 0x10U,
0x03U, 0x42U, 0x01U, 0x52U, 0x04U, 0x40U, 0x41U, 0xD3U, 0x80U, 0x04U,
0x41U, 0x16U, 0x42U, 0xE8U, 0x00U, 0x10U, 0x04U, 0x1AU, 0x0BU, 0x20U,
0x00U, 0x41U, 0xF0U, 0x02U, 0x6AU, 0x41U, 0x14U, 0x41U, 0x80U, 0x80U,
0x04U, 0x41U, 0x03U, 0x10U, 0x05U, 0x42U, 0x14U, 0x52U, 0x04U, 0x40U,
0x41U, 0xE9U, 0x80U, 0x04U, 0x41U, 0x0EU, 0x42U, 0xECU, 0x00U, 0x10U,
0x04U, 0x1AU, 0x0BU, 0x20U, 0x00U, 0x41U, 0xD0U, 0x02U, 0x6AU, 0x41U,
0x14U, 0x10U, 0x06U, 0x42U, 0x14U, 0x52U, 0x04U, 0x40U, 0x41U, 0xF7U,
0x80U, 0x04U, 0x41U, 0x1EU, 0x42U, 0xEFU, 0x00U, 0x10U, 0x04U, 0x1AU,
0x0BU, 0x20U, 0x00U, 0x41U, 0xB0U, 0x02U, 0x6AU, 0x41U, 0x20U, 0x41U,
0x84U, 0x80U, 0x04U, 0x41U, 0x09U, 0x10U, 0x05U, 0x42U, 0x20U, 0x52U,
0x04U, 0x40U, 0x41U, 0xA1U, 0x80U, 0x04U, 0x41U, 0x32U, 0x42U, 0xF2U,
0x00U, 0x10U, 0x04U, 0x1AU, 0x0BU, 0x10U, 0x07U, 0x21U, 0x03U, 0x20U,
0x00U, 0x42U, 0x80U, 0x80U, 0x80U, 0xD0U, 0xB0U, 0x8EU, 0xC0U, 0xC0U,
0x14U, 0x37U, 0x02U, 0x4CU, 0x20U, 0x00U, 0x42U, 0x80U, 0x80U, 0x80U,
0x80U, 0xF0U, 0xC1U, 0x90U, 0xA0U, 0xE8U, 0x00U, 0x37U, 0x03U, 0x40U,
0x20U, 0x00U, 0x41U, 0xE1U, 0x80U, 0x01U, 0x3BU, 0x01U, 0x3EU, 0x20U,
0x00U, 0x41U, 0xA0U, 0x36U, 0x3BU, 0x01U, 0x38U, 0x20U, 0x00U, 0x41U,
0xA0U, 0x34U, 0x3BU, 0x01U, 0x32U, 0x20U, 0x00U, 0x41U, 0x00U, 0x36U,
0x01U, 0x2EU, 0x20U, 0x00U, 0x41U, 0x80U, 0xC8U, 0x00U, 0x3BU, 0x01U,
0x2CU, 0x20U, 0x00U, 0x41U, 0xA2U, 0x80U, 0x02U, 0x36U, 0x02U, 0x28U,
0x20U, 0x00U, 0x42U, 0x92U, 0x80U, 0x80U, 0x88U, 0x82U, 0x80U, 0xC0U,
0xA9U, 0xD9U, 0x00U, 0x37U, 0x03U, 0x20U, 0x20U, 0x00U, 0x20U, 0x03U,
0xA7U, 0x22U, 0x02U, 0x41U, 0x05U, 0x6AU, 0x22U, 0x01U, 0x3AU, 0x00U,
0x3DU, 0x20U, 0x00U, 0x20U, 0x01U, 0x41U, 0x08U, 0x76U, 0x3AU, 0x00U,
0x3CU, 0x20U, 0x00U, 0x20U, 0x01U, 0x41U, 0x10U, 0x76U, 0x3AU, 0x00U,
0x3BU, 0x20U, 0x00U, 0x20U, 0x01U, 0x41U, 0x18U, 0x76U, 0x3AU, 0x00U,
0x3AU, 0x20U, 0x00U, 0x20U, 0x02U, 0x41U, 0x01U, 0x6AU, 0x22U, 0x01U,
0x3AU, 0x00U, 0x37U, 0x20U, 0x00U, 0x20U, 0x01U, 0x41U, 0x08U, 0x76U,
0x3AU, 0x00U, 0x36U, 0x20U, 0x00U, 0x20U, 0x01U, 0x41U, 0x10U, 0x76U,
0x3AU, 0x00U, 0x35U, 0x20U, 0x00U, 0x20U, 0x01U, 0x41U, 0x18U, 0x76U,
0x3AU, 0x00U, 0x34U, 0x20U, 0x00U, 0x41U, 0x83U, 0x29U, 0x3BU, 0x01U,
0x68U, 0x20U, 0x00U, 0x20U, 0x00U, 0x28U, 0x02U, 0xE0U, 0x02U, 0x36U,
0x02U, 0x64U, 0x20U, 0x00U, 0x20U, 0x00U, 0x29U, 0x03U, 0xD8U, 0x02U,
0x37U, 0x02U, 0x5CU, 0x20U, 0x00U, 0x20U, 0x00U, 0x29U, 0x03U, 0xD0U,
0x02U, 0x37U, 0x02U, 0x54U, 0x20U, 0x00U, 0x20U, 0x00U, 0x29U, 0x03U,
0xF0U, 0x02U, 0x37U, 0x01U, 0x6AU, 0x20U, 0x00U, 0x20U, 0x00U, 0x29U,
0x03U, 0xF8U, 0x02U, 0x37U, 0x01U, 0x72U, 0x20U, 0x00U, 0x20U, 0x00U,
0x28U, 0x02U, 0x80U, 0x03U, 0x36U, 0x01U, 0x7AU, 0x20U, 0x00U, 0x41U,
0x20U, 0x20U, 0x00U, 0x41U, 0x20U, 0x6AU, 0x41U, 0xDEU, 0x00U, 0x20U,
0x00U, 0x41U, 0xB0U, 0x02U, 0x6AU, 0x41U, 0x20U, 0x42U, 0x00U, 0x10U,
0x08U, 0x42U, 0x20U, 0x52U, 0x04U, 0x40U, 0x41U, 0x8EU, 0x80U, 0x04U,
0x41U, 0x13U, 0x42U, 0x8CU, 0x01U, 0x10U, 0x04U, 0x1AU, 0x0BU, 0x41U,
0x00U, 0x41U, 0x00U, 0x42U, 0x00U, 0x10U, 0x02U, 0x0BU, 0x21U, 0x03U,
0x20U, 0x00U, 0x41U, 0x90U, 0x03U, 0x6AU, 0x24U, 0x00U, 0x20U, 0x03U,
0x0BU, 0x0BU, 0x9DU, 0x01U, 0x01U, 0x00U, 0x41U, 0x80U, 0x80U, 0x04U,
0x0BU, 0x94U, 0x01U, 0x44U, 0x53U, 0x54U, 0x00U, 0x43U, 0x4FU, 0x4DU,
0x4DU, 0x49U, 0x54U, 0x54U, 0x45U, 0x45U, 0x00U, 0x78U, 0x70U, 0x6FU,
0x72U, 0x74U, 0x5FU, 0x72U, 0x65U, 0x73U, 0x75U, 0x6CU, 0x74U, 0x20U,
0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U, 0x6FU, 0x74U, 0x78U, 0x6EU,
0x5FU, 0x70U, 0x61U, 0x72U, 0x61U, 0x6DU, 0x28U, 0x53U, 0x42U, 0x55U,
0x46U, 0x28U, 0x63U, 0x6FU, 0x6DU, 0x6DU, 0x69U, 0x74U, 0x74U, 0x65U,
0x65U, 0x29U, 0x2CU, 0x20U, 0x22U, 0x43U, 0x4FU, 0x4DU, 0x4DU, 0x49U,
0x54U, 0x54U, 0x45U, 0x45U, 0x22U, 0x2CU, 0x20U, 0x39U, 0x29U, 0x20U,
0x3DU, 0x3DU, 0x20U, 0x33U, 0x32U, 0x00U, 0x78U, 0x70U, 0x6FU, 0x72U,
0x74U, 0x5FU, 0x72U, 0x65U, 0x73U, 0x65U, 0x72U, 0x76U, 0x65U, 0x28U,
0x31U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x31U, 0x00U, 0x64U, 0x73U,
0x74U, 0x5FU, 0x6CU, 0x65U, 0x6EU, 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,
0x61U, 0x63U, 0x63U, 0x29U, 0x29U, 0x20U, 0x3DU, 0x3DU, 0x20U, 0x32U,
0x30U,
}},
};
}
} // namespace ripple
#endif
+560 -5
View File
@@ -19,12 +19,19 @@
#include <test/app/Import_json.h>
#include <test/jtx.h>
#include <xrpld/app/hook/HookAPI.h>
#include <xrpld/app/ledger/TransactionMaster.h>
#include <xrpld/app/tx/detail/Export.h>
#include <xrpld/app/tx/detail/ExportLedgerOps.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_writer.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/ExportOriginMemo.h>
#include <xrpl/protocol/SField.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/TxFlags.h>
#include <limits>
#include <optional>
#include <tuple>
#include <vector>
@@ -35,6 +42,64 @@ namespace test {
class HookAPI_test : public beast::unit_test::suite
{
private:
STTx
makeSTTx(STObject const& obj)
{
Serializer s;
obj.add(s);
SerialIter sit{s.slice()};
return STTx{std::ref(sit)};
}
Blob
serialize(STTx const& tx)
{
Serializer s;
tx.add(s);
return {s.begin(), s.end()};
}
STTx
makeExportedPayment(
AccountID const& src,
AccountID const& dst,
std::optional<std::uint32_t> ticketSequence = 1)
{
STObject obj(sfExportedTxn);
obj.setFieldU16(sfTransactionType, ttPAYMENT);
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
obj.setFieldU32(sfSequence, 0);
if (ticketSequence)
obj.setFieldU32(sfTicketSequence, *ticketSequence);
obj.setFieldU32(sfFirstLedgerSequence, 2);
obj.setFieldU32(sfLastLedgerSequence, 6);
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
obj.setFieldAmount(sfFee, XRPAmount{10});
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setAccountID(sfAccount, src);
obj.setAccountID(sfDestination, dst);
return makeSTTx(obj);
}
STTx
makeExportWrapper(AccountID const& account, STTx const& innerTx)
{
STObject obj(sfGeneric);
obj.setFieldU16(sfTransactionType, ttEXPORT);
obj.setAccountID(sfAccount, account);
obj.setFieldU32(sfSequence, 0);
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setFieldAmount(sfFee, XRPAmount{0});
obj.setFieldU32(sfFirstLedgerSequence, 2);
obj.setFieldU32(sfLastLedgerSequence, 6);
auto const innerBlob = serialize(innerTx);
SerialIter sit{makeSlice(innerBlob)};
obj.set(std::make_unique<STObject>(sit, sfExportedTxn));
return makeSTTx(obj);
}
ApplyContext
createApplyContext(jtx::Env& env, OpenView& ov, STTx const& tx)
{
@@ -147,7 +212,9 @@ public:
BEAST_EXPECT(st.getAccountID(sfAccount) == alice.id());
auto const seq = applyCtx.view().info().seq;
BEAST_EXPECT(st.getFieldU32(sfFirstLedgerSequence) == seq + 1);
BEAST_EXPECT(st.getFieldU32(sfLastLedgerSequence) == seq + 5);
BEAST_EXPECT(
st.getFieldU32(sfLastLedgerSequence) ==
seq + ExportLimits::maxAdmissionWindowLedgers);
BEAST_EXPECT(st.isFieldPresent(sfEmitDetails));
auto const result2 =
@@ -177,7 +244,8 @@ public:
obj[sfSequence] = 0;
obj[sfSigningPubKey] = Slice{};
obj[sfFirstLedgerSequence] = env.closed()->seq() + 1;
obj[sfLastLedgerSequence] = env.closed()->seq() + 5;
obj[sfLastLedgerSequence] =
env.closed()->seq() + ExportLimits::maxAdmissionWindowLedgers;
obj[sfFee] = env.closed()->fees().base;
auto& emitDetails = obj.peekFieldObject(sfEmitDetails);
@@ -193,7 +261,8 @@ public:
obj[sfSequence] = 0;
obj[sfSigningPubKey] = Slice{};
obj[sfFirstLedgerSequence] = env.closed()->seq() + 1;
obj[sfLastLedgerSequence] = env.closed()->seq() + 5;
obj[sfLastLedgerSequence] =
env.closed()->seq() + ExportLimits::maxAdmissionWindowLedgers;
obj[sfFee] = env.closed()->fees().base;
STObject hookobj(sfHook);
auto& hooks = obj.peekFieldArray(sfHooks);
@@ -272,6 +341,35 @@ public:
auto const result = api.emit(tx.getSerializer().slice());
BEAST_EXPECT(result.error() == EMISSION_FAILURE);
}
{
// Export wrappers require xport() so they cannot bypass its
// committee check or per-Hook Export reservation.
auto hookCtx = makeStubHookContext(
applyCtx,
alice.id(),
alice.id(),
{
.expected_etxn_count = 3,
});
auto& api = hookCtx.api();
auto const inner = makeExportedPayment(alice.id(), bob.id());
auto wrapper = makeExportWrapper(alice.id(), inner);
// No account-owned committee exists for this digest.
wrapper.setFieldH256(sfExportCommitteeHash, uint256{1});
for (int attempt = 0; attempt < 3; ++attempt)
{
auto const prepared =
api.prepare(wrapper.getSerializer().slice());
BEAST_EXPECT(prepared.has_value());
if (!prepared)
continue;
auto const result =
api.emit(Slice{prepared->data(), prepared->size()});
BEAST_EXPECT(result.error() == EMISSION_FAILURE);
}
BEAST_EXPECT(hookCtx.result.emittedTxn.empty());
BEAST_EXPECT(hookCtx.export_count == 0);
}
{
// HookCanEmit (non-SetHook)
auto hookCtx = makeStubHookContext(
@@ -530,10 +628,12 @@ public:
}
{
// Invalid sfLastLedgerSequence
// (greater than current ledger seq + 5)
// (greater than the export admission window)
auto tx = emitInvokeTx;
auto const currentSeq = applyCtx.view().info().seq;
tx.setFieldU32(sfLastLedgerSequence, currentSeq + 6);
tx.setFieldU32(
sfLastLedgerSequence,
currentSeq + ExportLimits::maxAdmissionWindowLedgers + 1);
auto const result = api.emit(tx.getSerializer().slice());
BEAST_EXPECT(result.error() == EMISSION_FAILURE);
}
@@ -732,6 +832,32 @@ public:
else
BEAST_EXPECT(result.value() == baseFee + memoSize);
}
{
// Export's distributed-work fee must not be bypassed by the
// pre-fixHookAPI20251128 generic fee path.
auto const bob = Account{"bob"};
auto const roster = serializeExportCommittee(
{randomKeyPair(KeyType::secp256k1).first});
auto const committeeHash = exportCommitteeHash(makeSlice(roster));
auto committee = std::make_shared<SLE>(
keylet::exportCommittee(alice.id(), committeeHash));
committee->setAccountID(sfAccount, alice.id());
committee->setFieldH256(sfExportCommitteeHash, committeeHash);
committee->setFieldVL(sfExportCommittee, roster);
committee->setFieldU64(sfOwnerNode, 0);
applyCtx.view().insert(committee);
auto exportTx = makeExportWrapper(
alice.id(), makeExportedPayment(alice.id(), bob.id()));
exportTx.setFieldH256(sfExportCommitteeHash, committeeHash);
auto const expected =
Export::calculateBaseFee(applyCtx.view(), exportTx).drops();
auto const result =
api.etxn_fee_base(exportTx.getSerializer().slice());
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(result.value() == expected);
BEAST_EXPECT(result.value() > env.closed()->fees().base.drops());
}
}
void
@@ -948,6 +1074,314 @@ public:
}
}
void
test_xport_reserve(FeatureBitset features)
{
testcase("Test xport_reserve");
using namespace jtx;
using namespace hook_api;
auto const alice = Account{"alice"};
Env env{*this, features};
STTx invokeTx = STTx(ttINVOKE, [&](STObject& obj) {});
OpenView ov{*env.current()};
ApplyContext applyCtx = createApplyContext(env, ov, invokeTx);
auto const roster =
serializeExportCommittee({randomKeyPair(KeyType::secp256k1).first});
auto const committeeHash = exportCommitteeHash(makeSlice(roster));
auto committee = std::make_shared<SLE>(
keylet::exportCommittee(alice.id(), committeeHash));
committee->setAccountID(sfAccount, alice.id());
committee->setFieldH256(sfExportCommitteeHash, committeeHash);
committee->setFieldVL(sfExportCommittee, roster);
committee->setFieldU64(sfOwnerNode, 0);
applyCtx.view().insert(committee);
{
// ALREADY_SET
StubHookContext stubCtx{.expected_export_count = 1};
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
auto& api = hookCtx.api();
auto const result = api.xport_reserve(2);
BEAST_EXPECT(result.error() == ALREADY_SET);
}
{
// TOO_SMALL
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
auto const result = api.xport_reserve(0);
BEAST_EXPECT(result.error() == TOO_SMALL);
}
{
// TOO_BIG
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
auto const result = api.xport_reserve(hook_api::max_export + 1);
BEAST_EXPECT(result.error() == TOO_BIG);
}
{
// SUCCESS
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
auto const result = api.xport_reserve(2);
BEAST_EXPECT(result.has_value());
BEAST_EXPECT(hookCtx.expected_export_count == 2);
BEAST_EXPECT(hookCtx.expected_etxn_count == 2);
}
{
// xport_reserve composes with an earlier emit reservation.
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
BEAST_EXPECT(api.etxn_reserve(1).has_value());
BEAST_EXPECT(api.xport_reserve(2).has_value());
BEAST_EXPECT(hookCtx.expected_export_count == 2);
BEAST_EXPECT(hookCtx.expected_etxn_count == 3);
}
{
// xport_reserve can fill the remaining emitted-txn budget.
StubHookContext stubCtx{
.expected_etxn_count =
static_cast<int64_t>(hook_api::max_emit) -
hook_api::max_export};
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
auto& api = hookCtx.api();
BEAST_EXPECT(api.xport_reserve(hook_api::max_export).has_value());
BEAST_EXPECT(hookCtx.expected_export_count == hook_api::max_export);
BEAST_EXPECT(hookCtx.expected_etxn_count == hook_api::max_emit);
}
{
// xport_reserve shares the emitted-txn budget and must fail
// without partially setting the export reservation.
StubHookContext stubCtx{.expected_etxn_count = hook_api::max_emit};
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
auto& api = hookCtx.api();
auto const result = api.xport_reserve(1);
BEAST_EXPECT(result.error() == TOO_BIG);
BEAST_EXPECT(hookCtx.expected_export_count == -1);
BEAST_EXPECT(hookCtx.expected_etxn_count == hook_api::max_emit);
}
{
// xport_reserve consumes the shared emitted-txn reservation slot,
// so a later etxn_reserve cannot reset it.
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
BEAST_EXPECT(api.xport_reserve(1).has_value());
BEAST_EXPECT(api.etxn_reserve(1).error() == ALREADY_SET);
BEAST_EXPECT(hookCtx.expected_export_count == 1);
BEAST_EXPECT(hookCtx.expected_etxn_count == 1);
}
{
// xport shares emit()'s emitted transaction queue and must not
// append after normal emits fill the reserved budget.
std::string reason;
auto tx = std::make_shared<ripple::Transaction>(
std::make_shared<ripple::STTx const>(invokeTx),
reason,
env.app());
std::queue<std::shared_ptr<ripple::Transaction>> emittedTxn;
emittedTxn.push(tx);
auto hookCtx = makeStubHookContext(
applyCtx,
alice.id(),
alice.id(),
{
.expected_etxn_count = 1,
.expected_export_count = 1,
.result = {.emittedTxn = emittedTxn},
});
auto& api = hookCtx.api();
auto const result = api.xport(Slice{}, committeeHash, 0);
BEAST_EXPECT(result.error() == TOO_MANY_EMITTED_TXN);
}
{
// xport must enforce the emitted-txn generation cap before
// constructing a generation-10 wrapper.
auto const bob = Account{"bob"};
auto const innerTx = makeExportedPayment(alice.id(), bob.id());
auto const serialized = serialize(innerTx);
StubHookContext stubCtx{
.expected_etxn_count = 1,
.expected_export_count = 1,
.generation = 9,
.burden = 1,
.result = {.hookHash = uint256{3}},
};
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
auto& api = hookCtx.api();
auto const result = api.xport(
Slice(serialized.data(), serialized.size()), committeeHash, 0);
BEAST_EXPECT(result.error() == EXPORT_FAILURE);
}
{
// xport shares emit()'s burden accounting; overflow must fail
// instead of silently substituting burden=1 in EmitDetails.
auto const bob = Account{"bob"};
auto const innerTx = makeExportedPayment(alice.id(), bob.id());
auto const serialized = serialize(innerTx);
StubHookContext stubCtx{
.expected_etxn_count = 2,
.expected_export_count = 1,
.burden = std::numeric_limits<uint64_t>::max(),
.result = {.hookHash = uint256{3}},
};
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), stubCtx);
auto& api = hookCtx.api();
auto const result = api.xport(
Slice(serialized.data(), serialized.size()), committeeHash, 0);
BEAST_EXPECT(result.error() == FEE_TOO_LARGE);
}
}
void
test_xport_cancel(FeatureBitset features)
{
testcase("Test xport_cancel");
using namespace jtx;
using namespace hook_api;
auto const alice = Account{"alice"};
auto const bob = Account{"bob"};
Env env{*this, features};
env.fund(XRP(10000), alice, bob);
env.close();
std::uint32_t const importingTicket = 7;
std::uint32_t const otherTicket = 8;
uint256 const importingOrigin{1};
uint256 const otherOrigin{2};
auto importingTx =
makeExportedPayment(alice.id(), bob.id(), importingTicket);
auto otherTx = makeExportedPayment(alice.id(), bob.id(), otherTicket);
auto const importingRelease = ExportOriginMemo::releaseForm(
importingTx,
ExportOriginMemo::Origin{21337, 0, importingOrigin},
ExportOriginMemo::Anchor{1, uint256{99}});
BEAST_EXPECT(importingRelease);
if (!importingRelease)
return;
auto xpopJson = import::loadXpop(ImportTCAccountSet::w_seed);
xpopJson[jss::transaction][jss::blob] =
strHex(serialize(importingRelease.value()));
std::string const xpopStr = Json::FastWriter().write(xpopJson);
STTx importTx = STTx(ttIMPORT, [&](STObject& obj) {
obj.setAccountID(sfAccount, alice.id());
obj.setFieldVL(sfBlob, *strUnHex(strHex(xpopStr)));
});
OpenView ov{*env.current()};
ApplyContext applyCtx = createApplyContext(env, ov, importTx);
auto insertLatch = [&](ApplyContext& ctx,
STTx const& target,
uint256 const& origin) {
auto latch =
std::make_shared<SLE>(keylet::exportLatch(alice.id(), origin));
latch->setAccountID(sfAccount, alice.id());
latch->setFieldU32(
sfTicketSequence, target.getFieldU32(sfTicketSequence));
latch->setFieldH256(sfTransactionHash, origin);
latch->setFieldH256(
sfDigest, ExportResultBuilder::exportIntentHash(target));
latch->setFieldU32(sfLedgerSequence, ctx.view().info().seq);
latch->setFieldH256(sfExportCommitteeHash, uint256{100});
return ExportLedgerOps::insertPendingExportLatch(
ctx.view(), ctx.rawView(), latch, env.journal);
};
BEAST_EXPECT(
isTesSuccess(insertLatch(applyCtx, importingTx, importingOrigin)));
BEAST_EXPECT(isTesSuccess(insertLatch(applyCtx, otherTx, otherOrigin)));
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
auto const blocked = api.xport_cancel(importingOrigin, 0);
BEAST_EXPECT(!blocked.has_value());
BEAST_EXPECT(blocked.error() == PREREQUISITE_NOT_MET);
BEAST_EXPECT(hookCtx.applyCtx.view().exists(
keylet::exportLatch(alice.id(), importingOrigin)));
auto const invalidFlags = api.xport_cancel(otherOrigin, 0x00020000);
BEAST_EXPECT(!invalidFlags.has_value());
BEAST_EXPECT(invalidFlags.error() == INVALID_ARGUMENT);
auto const cancelled = api.xport_cancel(otherOrigin, 0);
BEAST_EXPECT(cancelled.has_value());
auto const canceledLatch = hookCtx.applyCtx.view().read(
keylet::exportLatch(alice.id(), otherOrigin));
BEAST_EXPECT(canceledLatch);
BEAST_EXPECT(
canceledLatch &&
(canceledLatch->getFieldU32(sfFlags) & lsfExportCanceled) != 0);
BEAST_EXPECT(
canceledLatch && !canceledLatch->isFieldPresent(sfExportNode));
auto const erased = api.xport_cancel(otherOrigin, tfExportEraseLatch);
BEAST_EXPECT(erased.has_value());
BEAST_EXPECT(!hookCtx.applyCtx.view().exists(
keylet::exportLatch(alice.id(), otherOrigin)));
auto exportTx = makeExportWrapper(alice.id(), importingTx);
OpenView exportOv{*env.current()};
ApplyContext exportApplyCtx =
createApplyContext(env, exportOv, exportTx);
std::uint32_t const exportOtherTicket = 9;
auto exportOtherTx =
makeExportedPayment(alice.id(), bob.id(), exportOtherTicket);
auto const exportImportingOrigin = exportTx.getTransactionID();
uint256 const exportOtherOrigin{4};
BEAST_EXPECT(isTesSuccess(
insertLatch(exportApplyCtx, importingTx, exportImportingOrigin)));
BEAST_EXPECT(isTesSuccess(
insertLatch(exportApplyCtx, exportOtherTx, exportOtherOrigin)));
auto exportHookCtx =
makeStubHookContext(exportApplyCtx, alice.id(), alice.id(), {});
auto& exportApi = exportHookCtx.api();
auto const exportBlocked =
exportApi.xport_cancel(exportImportingOrigin, 0);
BEAST_EXPECT(!exportBlocked.has_value());
BEAST_EXPECT(exportBlocked.error() == PREREQUISITE_NOT_MET);
BEAST_EXPECT(exportHookCtx.applyCtx.view().exists(
keylet::exportLatch(alice.id(), exportImportingOrigin)));
auto const exportCancelled =
exportApi.xport_cancel(exportOtherOrigin, 0);
BEAST_EXPECT(exportCancelled.has_value());
auto const canceledExportLatch = exportHookCtx.applyCtx.view().read(
keylet::exportLatch(alice.id(), exportOtherOrigin));
BEAST_EXPECT(canceledExportLatch);
BEAST_EXPECT(
canceledExportLatch &&
(canceledExportLatch->getFieldU32(sfFlags) & lsfExportCanceled) !=
0);
BEAST_EXPECT(
canceledExportLatch &&
!canceledExportLatch->isFieldPresent(sfExportNode));
}
void
test_fee_base(FeatureBitset features)
{
@@ -2668,6 +3102,30 @@ public:
// TODO: test INVALID_TXN
{
// Auto-allocation must not choose the explicit destination slot.
auto const xpopJson = import::loadXpop(ImportTCAccountSet::w_seed);
std::string xpopStr = Json::FastWriter().write(xpopJson);
STTx invokeTx = STTx(ttIMPORT, [&](STObject& obj) {
obj.setFieldVL(sfBlob, *strUnHex(strHex(xpopStr)));
});
OpenView ov{*env.current()};
ApplyContext applyCtx = createApplyContext(env, ov, invokeTx);
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
auto const result = api.xpop_slot(0, 1);
BEAST_EXPECT(result.has_value());
if (result)
{
BEAST_EXPECT(result.value().first != result.value().second);
BEAST_EXPECT(result.value().second == 1);
BEAST_EXPECT(hookCtx.slot.count(result.value().first) == 1);
BEAST_EXPECT(hookCtx.slot.count(result.value().second) == 1);
}
}
{
// Success
auto const xpopJson = import::loadXpop(ImportTCAccountSet::w_seed);
@@ -3251,6 +3709,100 @@ public:
INVALID_ARGUMENT);
}
void
test_slot_set_entropy(FeatureBitset features)
{
testcase(
"Entropy has no state entry; slots cannot expose the seed pseudo");
using namespace jtx;
using namespace hook;
Env env{*this, features | featureConsensusEntropy};
Account const alice{"entropy-slot-alice"};
env.fund(XRP(10000), alice);
env.close();
// Keep the ordinary transaction in the real unvalidated cache path;
// validated lookup otherwise depends on optional test SQL history.
env(pay(alice, Account::master, XRP(1)));
auto const ordinary = env.tx();
auto const retiredEntropyKey = sha512Half(std::uint16_t{'X'});
auto const entropy = env.closed()->consensusEntropy();
if (!BEAST_EXPECT(entropy != nullptr))
return;
BEAST_EXPECT(
entropy->getFieldU32(sfLedgerSequence) == env.closed()->seq());
auto bytes = [](LedgerEntryType type, uint256 const& key) {
auto const t = static_cast<std::uint16_t>(type);
Bytes out{
static_cast<std::uint8_t>(t >> 8),
static_cast<std::uint8_t>(t)};
out.insert(out.end(), key.begin(), key.end());
return out;
};
auto cache = [&](std::shared_ptr<STTx const> const& tx) {
std::string reason;
auto transaction =
std::make_shared<Transaction>(tx, reason, env.app());
env.app().getMasterTransaction().canonicalize(&transaction);
auto const id = tx->getTransactionID();
return Bytes{id.begin(), id.end()};
};
auto const ordinaryID = cache(ordinary);
std::optional<Bytes> entropyID;
for (auto const& [tx, meta] : env.closed()->txs)
if (tx->getTxnType() == ttCONSENSUS_ENTROPY)
entropyID = cache(tx);
if (!BEAST_EXPECT(entropyID.has_value()))
return;
BEAST_EXPECT(ordinaryID.size() == 32 && entropyID->size() == 32);
auto check = [&](ReadView const& base) {
OpenView ov{&base};
STTx invokeTx{ttINVOKE, [](STObject&) {}};
auto applyCtx = createApplyContext(env, ov, invokeTx);
auto hookCtx =
makeStubHookContext(applyCtx, alice.id(), alice.id(), {});
auto& api = hookCtx.api();
auto const accountKey = keylet::account(alice.id());
auto const accountBytes = bytes(accountKey.type, accountKey.key);
auto const initial = api.slot_set(accountBytes, 1);
if (!BEAST_EXPECT(initial && *initial == 1))
return;
auto const* preserved = api.slot(1).value();
auto denied = [&](Bytes const& key, HookReturnCode error) {
for (auto const destination : {0u, 1u})
{
auto const result = api.slot_set(key, destination);
BEAST_EXPECT(!result && result.error() == error);
// Denial must neither overwrite an existing slot nor
// allocate an automatic slot containing the raw object.
BEAST_EXPECT(api.slot(1).value() == preserved);
auto const next = api.slot_set(accountBytes, 0);
BEAST_EXPECT(next && *next == 2);
if (next)
BEAST_EXPECT(api.slot_clear(*next).has_value());
}
};
for (auto const type : {ltANY, ltCHILD})
denied(bytes(type, retiredEntropyKey), DOESNT_EXIST);
// The entropy pseudo-transaction is another raw-digest carrier.
denied(*entropyID, NOT_AUTHORIZED);
for (auto const type : {ltACCOUNT_ROOT, ltANY, ltCHILD})
{
auto const result =
api.slot_set(bytes(type, accountKey.key), 1);
BEAST_EXPECT(result && *result == 1);
}
auto const transaction = api.slot_set(ordinaryID, 1);
BEAST_EXPECT(transaction && *transaction == 1);
BEAST_EXPECT(applyCtx.view().consensusEntropy() != nullptr);
BEAST_EXPECT(
!applyCtx.view().read(keylet::unchecked(retiredEntropyKey)));
};
check(*env.current());
check(*env.closed());
}
void
test_slot_size(FeatureBitset features)
{
@@ -4721,6 +5273,8 @@ public:
test_etxn_fee_base(features);
test_etxn_nonce(features);
test_etxn_reserve(features);
test_xport_reserve(features);
test_xport_cancel(features);
test_fee_base(features);
test_otxn_field(features);
@@ -4770,6 +5324,7 @@ public:
test_slot_count(features);
test_slot_float(features);
test_slot_set(features);
test_slot_set_entropy(features);
test_slot_size(features);
test_slot_subarray(features);
test_slot_subfield(features);
+20
View File
@@ -26,6 +26,7 @@
#include <xrpld/app/tx/detail/Import.h>
#include <xrpld/core/ConfigSections.h>
#include <xrpld/ledger/Dir.h>
#include <xrpl/basics/base64.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/json/json_writer.h>
#include <xrpl/protocol/Feature.h>
@@ -1560,6 +1561,25 @@ class Import_test : public beast::unit_test::suite
BEAST_EXPECT(getVLInfo(*xpop, env.journal).has_value() == false);
}
// Import: unl blob sequence was missing or negative
{
Json::Value tmpXpop = xpop;
auto const data = base64_decode(
tmpXpop[jss::validation][jss::unl][jss::blob].asString());
Json::Value list;
Json::Reader reader;
BEAST_REQUIRE(reader.parse(data, list));
list[jss::sequence] = -1;
auto const listJson = writer.write(list);
tmpXpop[jss::validation][jss::unl][jss::blob] =
base64_encode(listJson);
std::string strJson = writer.write(tmpXpop);
Blob raw(strJson.begin(), strJson.end());
auto const xpop = syntaxCheckXPOP(raw, env.journal);
BEAST_REQUIRE(xpop);
BEAST_EXPECT(getVLInfo(*xpop, env.journal).has_value() == false);
}
// Import: valid unl blob
{
Json::Value tmpXpop = xpop;
+66
View File
@@ -21,6 +21,7 @@
#include <test/jtx/Env.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/ledger/detail/PublishGap.h>
#include <xrpld/app/ledger/detail/ValidatedLedgerWorkQueue.h>
#include <xrpl/basics/RangeSet.h>
#include <xrpl/protocol/jss.h>
@@ -253,6 +254,70 @@ class LedgerMaster_test : public beast::unit_test::suite
BEAST_EXPECT(!lm.isPinned(seq));
}
void
testValidatedLedgerWorkQueue()
{
testcase("validated ledger work queue");
using detail::ValidatedLedgerWork;
using detail::ValidatedLedgerWorkQueue;
ValidatedLedgerWorkQueue queue{2};
ValidatedLedgerWork const a{10, uint256{1}};
ValidatedLedgerWork const fork{10, uint256{2}};
ValidatedLedgerWork const b{11, uint256{3}};
auto result = queue.enqueue(a);
BEAST_EXPECT(result.inserted);
BEAST_EXPECT(result.needsDrain);
BEAST_EXPECT(!result.evicted);
result = queue.enqueue(a);
BEAST_EXPECT(!result.inserted);
BEAST_EXPECT(!result.needsDrain);
BEAST_EXPECT(queue.size() == 1);
// Sequence alone is not identity: a different hash remains distinct.
result = queue.enqueue(fork);
BEAST_EXPECT(result.inserted);
BEAST_EXPECT(!result.needsDrain);
BEAST_EXPECT(queue.size() == 2);
// Saturation drops the oldest pending event and retains newer work.
result = queue.enqueue(b);
BEAST_EXPECT(result.inserted);
BEAST_EXPECT(!result.needsDrain);
BEAST_EXPECT(result.evicted && *result.evicted == a);
std::vector<ValidatedLedgerWork> drained;
auto const count =
queue.drain([&](ValidatedLedgerWork const& work) noexcept {
// An exact duplicate cannot be queued while it is active.
auto const duplicate = queue.enqueue(work);
BEAST_EXPECT(!duplicate.inserted);
drained.push_back(work);
});
BEAST_EXPECT(count == 2);
BEAST_EXPECT(drained.size() == 2);
BEAST_EXPECT(drained[0] == fork);
BEAST_EXPECT(drained[1] == b);
BEAST_EXPECT(queue.size() == 0);
BEAST_EXPECT(!queue.drainScheduled());
// A completed or failed-to-post drain can be scheduled again. A
// failed post preserves the queued item, so the exact same identity
// must be able to re-arm scheduling without inserting a duplicate.
result = queue.enqueue(a);
BEAST_EXPECT(result.needsDrain);
queue.cancelDrain();
BEAST_EXPECT(!queue.drainScheduled());
result = queue.enqueue(a);
BEAST_EXPECT(!result.inserted);
BEAST_EXPECT(result.needsDrain);
BEAST_EXPECT(queue.size() == 1);
}
void
testSetPinnedRangesImmediateMerge()
{
@@ -334,6 +399,7 @@ public:
using namespace test::jtx;
FeatureBitset const all{supported_amendments() - featureXahauGenesis};
testCanSkipPinnedGap();
testValidatedLedgerWorkQueue();
testWithFeats(all);
testPinUnpinSymmetry();
testSetPinnedRangesImmediateMerge();
+70 -1
View File
@@ -30,6 +30,8 @@
#include <xrpld/overlay/PeerSet.h>
#include <xrpld/overlay/detail/PeerImp.h>
#include <xrpl/basics/Slice.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Serializer.h>
#include <chrono>
#include <thread>
@@ -40,7 +42,7 @@ namespace test {
struct LedgerReplay_test : public beast::unit_test::suite
{
void
run() override
testReplay()
{
testcase("Replay ledger");
@@ -73,6 +75,73 @@ struct LedgerReplay_test : public beast::unit_test::suite
BEAST_EXPECT(replayed->info().hash == lastClosed->info().hash);
}
void
testConsensusEntropyReplay()
{
testcase("Replay ledger with consensus entropy");
using namespace jtx;
auto const alice = Account("alice");
auto const bob = Account("bob");
Env env = [&] {
auto c = jtx::envconfig();
auto& sectionNode = c->section(ConfigSection::nodeDatabase());
sectionNode.set("type", "memory");
c->overwrite(SECTION_RELATIONAL_DB, "backend", "sqlite");
return jtx::Env(
*this,
std::move(c),
supported_amendments() | featureConsensusEntropy,
nullptr);
}();
env.fund(XRP(100000), alice, bob);
env.close();
LedgerMaster& ledgerMaster = env.app().getLedgerMaster();
auto const lastClosed = ledgerMaster.getClosedLedger();
auto const lastClosedParent =
ledgerMaster.getLedgerByHash(lastClosed->info().parentHash);
LedgerReplay const replayData(lastClosedParent, lastClosed);
auto const& orderedTxns = replayData.orderedTxns();
BEAST_EXPECT(orderedTxns.size() > 1);
if (orderedTxns.empty())
return;
auto const& [index, entropyTx] = *orderedTxns.begin();
BEAST_EXPECT(index == 0);
BEAST_EXPECT(entropyTx->getTxnType() == ttCONSENSUS_ENTROPY);
auto const persisted =
lastClosed->txRead(entropyTx->getTransactionID());
BEAST_EXPECT(persisted.first);
BEAST_EXPECT(
persisted.second &&
persisted.second->getFieldU32(sfTransactionIndex) == index);
if (persisted.first)
{
Serializer persistedBytes;
persisted.first->add(persistedBytes);
Serializer replayBytes;
entropyTx->add(replayBytes);
BEAST_EXPECT(replayBytes.peekData() == persistedBytes.peekData());
}
auto const replayed =
buildLedger(replayData, tapNONE, env.app(), env.journal);
BEAST_EXPECT(replayed->info().hash == lastClosed->info().hash);
}
void
run() override
{
testReplay();
testConsensusEntropyReplay();
}
};
enum class InboundLedgersBehavior {
+9
View File
@@ -1878,6 +1878,15 @@ class MPToken_test : public beast::unit_test::suite
[jss::Amount] = mpt.getJson(JsonOptions::none);
test(jv, sfAmounts.jsonName.c_str());
}
// Export callback fee authorization is a native-only amount.
{
Json::Value jv;
jv[jss::TransactionType] = jss::Export;
jv[jss::Account] = alice.human();
jv[sfExportCallbackFee.jsonName] =
mpt.getJson(JsonOptions::none);
test(jv, sfExportCallbackFee.fieldName);
}
// ClaimReward
{
Json::Value jv = reward::claim(alice);
+15 -11
View File
@@ -21,6 +21,7 @@
#include <test/jtx/Env.h>
#include <xrpld/core/ConfigSections.h>
#include <xrpl/basics/BasicConfig.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/jss.h>
namespace ripple {
@@ -97,11 +98,12 @@ public:
runTx(env, jv, telNETWORK_ID_MAKES_TX_NON_CANONICAL);
}
// any network up to and including networkid 1024 cannot support
// NetworkID
// Legacy networks cannot support NetworkID.
{
test::jtx::Env env{*this, makeNetworkConfig(1024)};
BEAST_EXPECT(env.app().config().NETWORK_ID == 1024);
test::jtx::Env env{
*this, makeNetworkConfig(maxNetworkIDWithoutTxField)};
BEAST_EXPECT(
env.app().config().NETWORK_ID == maxNetworkIDWithoutTxField);
// try to submit a txn without network id, this should work
Json::Value jv;
@@ -110,18 +112,20 @@ public:
runTx(env, jv, tesSUCCESS);
// now submit with a network id, this will fail
jv[jss::NetworkID] = 1024;
jv[jss::NetworkID] = maxNetworkIDWithoutTxField;
runTx(env, jv, telNETWORK_ID_MAKES_TX_NON_CANONICAL);
jv[jss::NetworkID] = 1000;
runTx(env, jv, telNETWORK_ID_MAKES_TX_NON_CANONICAL);
}
// any network above networkid 1024 will produce an error if fed a txn
// absent networkid
// Any network above the legacy range rejects a txn absent NetworkID.
{
test::jtx::Env env{*this, makeNetworkConfig(1025)};
BEAST_EXPECT(env.app().config().NETWORK_ID == 1025);
test::jtx::Env env{
*this, makeNetworkConfig(maxNetworkIDWithoutTxField + 1)};
BEAST_EXPECT(
env.app().config().NETWORK_ID ==
maxNetworkIDWithoutTxField + 1);
// try to submit a txn without network id, this should not work
{
env.fund(XRP(200), alice);
@@ -150,11 +154,11 @@ public:
jv[jss::NetworkID] = 0;
runTx(env, jv, telWRONG_NETWORK);
jv[jss::NetworkID] = 1024;
jv[jss::NetworkID] = maxNetworkIDWithoutTxField;
runTx(env, jv, telWRONG_NETWORK);
// submit the correct network id
jv[jss::NetworkID] = 1025;
jv[jss::NetworkID] = maxNetworkIDWithoutTxField + 1;
runTx(env, jv, tesSUCCESS);
}
}
+231 -1
View File
@@ -16,9 +16,17 @@
//==============================================================================
#include <test/jtx.h>
#include <xrpld/app/ledger/Ledger.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/tx/apply.h>
#include <xrpld/app/tx/detail/ApplyContext.h>
#include <xrpld/app/tx/detail/ExportResultBuilder.h>
#include <xrpld/ledger/PaymentSandbox.h>
#include <xrpld/ledger/Sandbox.h>
#include <xrpl/protocol/EntropyTier.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/STAccount.h>
#include <xrpl/protocol/Sign.h>
#include <string>
#include <vector>
@@ -27,6 +35,27 @@ namespace test {
struct PseudoTx_test : public beast::unit_test::suite
{
STTx
consensusEntropyTx(
std::uint32_t seq,
std::uint8_t tier,
std::uint16_t count,
std::uint16_t denominator,
Blob contributors)
{
return STTx(ttCONSENSUS_ENTROPY, [&](auto& obj) {
obj.setAccountID(sfAccount, AccountID());
obj.setFieldU32(sfSequence, 0);
obj.setFieldAmount(sfFee, STAmount{});
obj.setFieldU32(sfLedgerSequence, seq);
obj.setFieldH256(sfDigest, uint256(3));
obj.setFieldU16(sfEntropyCount, count);
obj.setFieldU16(sfEntropyDenominator, denominator);
obj.setFieldVL(sfEntropyContributors, contributors);
obj.setFieldU8(sfEntropyTier, tier);
});
}
std::vector<STTx>
getPseudoTxs(Rules const& rules, std::uint32_t seq)
{
@@ -56,6 +85,27 @@ struct PseudoTx_test : public beast::unit_test::suite
obj.setFieldU32(sfLedgerSequence, seq);
}));
res.emplace_back(consensusEntropyTx(
seq, entropyTierValidatorQuorum, 1, 1, Blob{0x01}));
auto const secret = generateSecretKey(KeyType::secp256k1, randomSeed());
auto const publicKey = derivePublicKey(KeyType::secp256k1, secret);
ExportResultBuilder::PositionedSignatureSnapshot signatures;
std::uint8_t const signatureBytes[] = {1, 2, 3};
signatures.emplace(
0,
ExportResultBuilder::PositionedSignature{
publicKey, Buffer{signatureBytes, sizeof(signatureBytes)}});
auto const releaseTarget = STTx(ttPAYMENT, [&](auto& obj) {
obj.setAccountID(sfAccount, AccountID(1));
obj.setAccountID(sfDestination, AccountID(2));
obj.setFieldU32(sfSequence, 0);
obj.setFieldAmount(sfFee, STAmount{});
obj.setFieldVL(sfSigningPubKey, Blob{});
});
res.emplace_back(ExportResultBuilder::buildSignatureWitness(
uint256(4), releaseTarget, signatures, 1, seq));
return res;
}
@@ -92,7 +142,7 @@ struct PseudoTx_test : public beast::unit_test::suite
[&](OpenView& view, beast::Journal j) {
auto const result =
ripple::apply(env.app(), view, stx, tapNONE, j);
BEAST_EXPECT(!result.applied && result.ter == temINVALID);
BEAST_EXPECT(!result.applied);
return result.applied;
});
}
@@ -109,6 +159,183 @@ struct PseudoTx_test : public beast::unit_test::suite
}
}
void
expectOpenLedgerResult(jtx::Env& env, STTx const& tx, TER expected)
{
env.app().openLedger().modify([&](OpenView& view, beast::Journal j) {
auto const result = ripple::apply(env.app(), view, tx, tapNONE, j);
BEAST_EXPECT(result.ter == expected);
BEAST_EXPECT(!result.applied);
return result.applied;
});
}
void
testConsensusEntropyContributorMaskPreflight()
{
testcase("ConsensusEntropy contributor mask preflight");
using namespace jtx;
Env env(*this, supported_amendments() | featureConsensusEntropy);
auto const seq = env.closed()->seq() + 1;
expectOpenLedgerResult(
env,
consensusEntropyTx(seq, entropyTierConsensusFallback, 0, 0, Blob{}),
temINVALID);
expectOpenLedgerResult(
env,
consensusEntropyTx(
seq, entropyTierValidatorQuorum, 2, 3, Blob{0x03}),
temINVALID);
expectOpenLedgerResult(
env,
consensusEntropyTx(
seq, entropyTierConsensusFallback, 1, 1, Blob{0x01}),
temMALFORMED);
expectOpenLedgerResult(
env,
consensusEntropyTx(
seq, entropyTierValidatorQuorum, 1, 9, Blob{0x01}),
temMALFORMED);
expectOpenLedgerResult(
env,
consensusEntropyTx(
seq, entropyTierValidatorQuorum, 1, 9, Blob{0x01, 0x02}),
temMALFORMED);
expectOpenLedgerResult(
env,
consensusEntropyTx(
seq, entropyTierValidatorQuorum, 2, 3, Blob{0x01}),
temMALFORMED);
expectOpenLedgerResult(
env,
consensusEntropyTx(seq, entropyTierValidatorFull, 2, 3, Blob{0x03}),
temMALFORMED);
}
void
testConsensusEntropyLedgerSequence()
{
testcase("ConsensusEntropy current ledger binding");
using namespace jtx;
Env env(*this, supported_amendments() | featureConsensusEntropy);
auto const parent = env.app().getLedgerMaster().getClosedLedger();
auto const seq = parent->seq() + 1;
auto applyToNextLedger = [&](STTx const& tx, TER expected) {
auto next = std::make_shared<Ledger>(
*parent, env.app().timeKeeper().closeTime());
OpenView accum(&*next);
auto const result =
ripple::apply(env.app(), accum, tx, tapNONE, env.journal);
BEAST_EXPECT(result.ter == expected);
BEAST_EXPECT(result.applied == isTesSuccess(expected));
};
applyToNextLedger(
consensusEntropyTx(
seq + 1, entropyTierValidatorQuorum, 1, 1, Blob{0x01}),
tefFAILURE);
applyToNextLedger(
consensusEntropyTx(
seq, entropyTierValidatorQuorum, 1, 1, Blob{0x01}),
tesSUCCESS);
}
void
testEntropyContextPublication()
{
testcase("Entropy context commit, discard, dry-run, and duplicate");
using namespace jtx;
Env env{*this, supported_amendments() | featureConsensusEntropy};
env.close();
auto const parent = env.app().getLedgerMaster().getClosedLedger();
auto next = std::make_shared<Ledger>(*parent, parent->info().closeTime);
OpenView view{next.get()};
BEAST_EXPECT(!view.consensusEntropy());
auto const tx = consensusEntropyTx(
view.seq(), entropyTierConsensusFallback, 0, 0, Blob{});
auto const input = std::make_shared<STTx const>(tx);
// Staging is transaction-local and cannot contaminate the base or its
// sibling snapshot when an attempt is discarded.
OpenView sibling{view};
ApplyContext staged{
env.app(),
view,
tx,
tesSUCCESS,
view.fees().base,
tapNONE,
env.journal};
staged.stageConsensusEntropy(input);
BEAST_EXPECT(!view.consensusEntropy());
staged.discard();
// A synthetic successful apply after discard detects leaked staging.
// This isolated view is never committed as a ledger.
staged.apply(tesSUCCESS);
BEAST_EXPECT(!view.consensusEntropy());
BEAST_EXPECT(!sibling.consensusEntropy());
OpenView failedView{next.get()};
ApplyContext failed{
env.app(),
failedView,
tx,
tesSUCCESS,
failedView.fees().base,
tapNONE,
env.journal};
failed.stageConsensusEntropy(input);
failed.apply(tecINVARIANT_FAILED);
BEAST_EXPECT(!failedView.consensusEntropy());
OpenView dryView{next.get()};
auto const dry =
ripple::apply(env.app(), dryView, tx, tapDRY_RUN, env.journal);
BEAST_EXPECT(isTesSuccess(dry.ter));
BEAST_EXPECT(!dryView.consensusEntropy());
BEAST_EXPECT(dryView.txCount() == 0);
OpenView committed{next.get()};
auto const result =
ripple::apply(env.app(), committed, tx, tapNONE, env.journal);
BEAST_EXPECT(result.applied && isTesSuccess(result.ter));
auto const published = committed.consensusEntropy();
if (!BEAST_EXPECT(published != nullptr))
return;
BEAST_EXPECT(published->getTransactionID() == tx.getTransactionID());
BEAST_EXPECT(!sibling.consensusEntropy());
OpenView copied{committed};
Sandbox sandbox{&copied, tapNONE};
PaymentSandbox payment{&sandbox, tapNONE};
BEAST_EXPECT(sandbox.consensusEntropy() == published);
BEAST_EXPECT(payment.consensusEntropy() == published);
// Discarding an ordinary transaction view leaves the ledger input.
ApplyContext ordinary{
env.app(),
committed,
tx,
tesSUCCESS,
committed.fees().base,
tapNONE,
env.journal};
ordinary.discard();
BEAST_EXPECT(ordinary.view().consensusEntropy() == published);
auto duplicate = consensusEntropyTx(
committed.seq(), entropyTierValidatorQuorum, 1, 1, Blob{0x01});
auto const rejected = ripple::apply(
env.app(), committed, duplicate, tapNONE, env.journal);
BEAST_EXPECT(rejected.ter == tefFAILURE && !rejected.applied);
BEAST_EXPECT(committed.consensusEntropy() == published);
BEAST_EXPECT(committed.txCount() == 1);
}
void
run() override
{
@@ -119,6 +346,9 @@ struct PseudoTx_test : public beast::unit_test::suite
testPrevented(all - featureXRPFees);
testPrevented(all);
testAllowed();
testConsensusEntropyContributorMaskPreflight();
testConsensusEntropyLedgerSequence();
testEntropyContextPublication();
}
};
+29
View File
@@ -5688,6 +5688,26 @@ private:
BEAST_EXPECT(!features[featurePermissionedDomains]);
}
void
testExportTSH(FeatureBitset features)
{
testcase("export tsh");
// ttEXPORT is a wrapper/consensus-driven path, not a hook-dispatched
// user transaction for triggered strong/weak hook execution.
pass();
}
void
testExportSignaturesTSH(FeatureBitset features)
{
testcase("export signatures tsh");
// Pseudo transaction consumed by ttEXPORT during ledger replay; it is
// never a hook-dispatched originating transaction.
pass();
}
void
testSetFeeTSH(FeatureBitset features)
{
@@ -6128,6 +6148,15 @@ private:
pass();
}
void
testConsensusEntropyTSH(FeatureBitset features)
{
testcase("consensus 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
+456
View File
@@ -0,0 +1,456 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2026 XRPL Labs
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/import.h>
#include <test/jtx/xpop.h>
#include <test/shamap/common.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/proof/LedgerProof.h>
#include <xrpld/app/proof/ProofBuilder.h>
#include <xrpld/app/proof/XPOPv1.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/protocol/Import.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <cstring>
namespace ripple {
namespace test {
namespace {
uint256
makeHash(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
} // namespace
struct XPOP_test : public beast::unit_test::suite
{
void
testBuildLedgerProof()
{
testcase("Build LedgerProof from a payment");
using namespace jtx;
Env env{*this};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
// Submit a payment and close the ledger.
env(pay(alice, bob, XRP(100)));
env.close();
// Get the tx hash from the last closed ledger.
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
BEAST_EXPECT(lcl);
// Find a payment tx in the ledger.
uint256 paymentHash;
bool found = false;
lcl->txMap().visitLeaves(
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
if (!found)
{
paymentHash = item->key();
found = true;
}
});
BEAST_EXPECT(found);
// Build the proof.
auto const lp = proof::buildLedgerProof(*lcl, paymentHash);
BEAST_EXPECT(lp.has_value());
if (lp)
{
// Verify header fields are populated.
BEAST_EXPECT(lp->ledgerIndex > 0);
BEAST_EXPECT(lp->totalCoins > 0);
BEAST_EXPECT(lp->parentHash != uint256{});
BEAST_EXPECT(lp->txRoot != uint256{});
BEAST_EXPECT(lp->accountRoot != uint256{});
// Verify tx blob is non-empty.
BEAST_EXPECT(!lp->txBlob.empty());
BEAST_EXPECT(!lp->metaBlob.empty());
// Verify merkle proof exists and is valid.
BEAST_EXPECT(lp->txProof.has_value());
if (lp->txProof)
{
auto const computedRoot = lp->txProof->computeRoot();
BEAST_EXPECT(computedRoot.has_value());
if (computedRoot)
BEAST_EXPECT(*computedRoot == lp->txRoot);
}
// Verify ledger hash reconstruction.
auto const computedHash = lp->computeLedgerHash();
BEAST_EXPECT(computedHash == lcl->info().hash);
}
auto missing = proof::buildLedgerProof(*lcl, makeHash("missing-tx"));
BEAST_EXPECT(!missing);
auto const missingProof =
proof::extractProofV1(lcl->txMap(), makeHash("missing-proof"));
BEAST_EXPECT(!missingProof);
}
void
testProofBuilderEdgeCases()
{
testcase("ProofBuilder edge cases");
proof::MerkleProof empty;
BEAST_EXPECT(!empty.computeRoot());
BEAST_EXPECT(!empty.verify(makeHash("root")));
BEAST_EXPECT(empty.toJsonV1().isNull());
proof::MerkleProof manual;
manual.key = makeHash("manual-key");
manual.leafHash = makeHash("manual-leaf");
proof::ProofNode leafParent;
leafParent.targetBranch = 3;
leafParent.isLeafParent = true;
leafParent.branches[0] = makeHash("manual-sibling-0");
manual.path.push_back(leafParent);
auto const computedRoot = manual.computeRoot();
BEAST_EXPECT(computedRoot.has_value());
if (computedRoot)
BEAST_EXPECT(manual.verify(*computedRoot));
auto const proofJson = manual.toJsonV1();
BEAST_EXPECT(proofJson.isArray());
BEAST_EXPECT(proofJson.size() == 16);
BEAST_EXPECT(proofJson[3].asString() == to_string(manual.leafHash));
}
void
testProofBuilderSyntheticTrie()
{
testcase("ProofBuilder synthetic trie collisions");
tests::TestNodeFamily f{beast::Journal{beast::Journal::getNullSink()}};
SHAMap map{SHAMapType::TRANSACTION, f};
auto const keyA = uint256{
"1000000000000000000000000000000000000000000000000000000000000001"};
auto const keyB = uint256{
"1800000000000000000000000000000000000000000000000000000000000002"};
auto const keyC = uint256{
"2000000000000000000000000000000000000000000000000000000000000003"};
auto const keyD = uint256{
"1f00000000000000000000000000000000000000000000000000000000000004"};
auto add = [&](uint256 const& key, Blob data) {
return map.addItem(
SHAMapNodeType::tnTRANSACTION_NM,
make_shamapitem(key, makeSlice(data)));
};
auto payload = [](std::uint8_t first) {
Blob data;
data.reserve(12);
for (std::uint8_t i = 0; i < 12; ++i)
data.push_back(first + i);
return data;
};
BEAST_EXPECT(add(keyA, payload(0x01)));
BEAST_EXPECT(add(keyB, payload(0x11)));
BEAST_EXPECT(add(keyC, payload(0x21)));
BEAST_EXPECT(add(keyD, payload(0x31)));
map.invariants();
auto const proof = proof::extractProofV1(map, keyA);
BEAST_EXPECT(proof.has_value());
if (proof)
{
BEAST_EXPECT(proof->path.size() == 2);
auto const computedRoot = proof->computeRoot();
BEAST_EXPECT(computedRoot.has_value());
if (computedRoot)
BEAST_EXPECT(proof->verify(*computedRoot));
auto const json = proof->toJsonV1();
BEAST_EXPECT(json.isArray());
BEAST_EXPECT(json[proof->path.front().targetBranch].isArray());
}
auto const nearMiss = uint256{
"10000000000000000000000000000000000000000000000000000000000000ff"};
BEAST_EXPECT(!proof::extractProofV1(map, nearMiss));
}
void
testBuildXPOPv1()
{
testcase("Build XPOP v1 JSON from a payment");
using namespace jtx;
Env env{*this};
Account const alice{"alice"};
Account const bob{"bob"};
env.fund(XRP(10000), alice, bob);
env.close();
env(pay(alice, bob, XRP(100)));
env.close();
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
BEAST_EXPECT(lcl);
// Find a tx.
uint256 txHash;
lcl->txMap().visitLeaves(
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
txHash = item->key();
});
// Build XPOP using the test helper.
auto const xpopCtx = xpop::TestXPOPContext::create(3);
auto const xpop = xpopCtx.buildXPOP(*lcl, txHash);
BEAST_EXPECT(!xpop.isNull());
// Verify structure.
BEAST_EXPECT(xpop.isMember(jss::ledger));
BEAST_EXPECT(xpop.isMember(jss::transaction));
BEAST_EXPECT(xpop.isMember(jss::validation));
// Ledger section.
auto const& lgr = xpop[jss::ledger];
BEAST_EXPECT(lgr.isMember(jss::index));
BEAST_EXPECT(lgr.isMember(jss::coins));
BEAST_EXPECT(lgr.isMember(jss::phash));
BEAST_EXPECT(lgr.isMember(jss::txroot));
BEAST_EXPECT(lgr.isMember(jss::acroot));
BEAST_EXPECT(lgr.isMember(jss::close));
BEAST_EXPECT(lgr.isMember(jss::pclose));
BEAST_EXPECT(lgr.isMember(jss::cres));
BEAST_EXPECT(lgr.isMember(jss::flags));
// Transaction section.
auto const& txn = xpop[jss::transaction];
BEAST_EXPECT(txn.isMember(jss::blob));
BEAST_EXPECT(txn.isMember(jss::meta));
BEAST_EXPECT(txn.isMember(jss::proof));
BEAST_EXPECT(txn[jss::blob].asString().size() > 0);
BEAST_EXPECT(txn[jss::meta].asString().size() > 0);
// Validation section.
auto const& val = xpop[jss::validation];
BEAST_EXPECT(val.isMember(jss::data));
BEAST_EXPECT(val.isMember(jss::unl));
BEAST_EXPECT(val[jss::data].size() == 3); // 3 validators
auto const& unl = val[jss::unl];
BEAST_EXPECT(unl.isMember(jss::public_key));
BEAST_EXPECT(unl.isMember(jss::manifest));
BEAST_EXPECT(unl.isMember(jss::blob));
BEAST_EXPECT(unl.isMember(jss::signature));
BEAST_EXPECT(unl.isMember(jss::version));
auto const encoded = proof::xpopToHex(xpop);
BEAST_EXPECT(!encoded.empty());
BEAST_EXPECT(strUnHex(encoded).has_value());
auto const missing = proof::buildXPOPv1(
*lcl,
makeHash("missing-xpop-tx"),
std::vector<proof::ValidatorKeys>{},
xpopCtx.vlData);
BEAST_EXPECT(missing.isNull());
}
void
testBuildXPOPv1WithoutMerkleProof()
{
testcase("Build XPOP v1 without merkle proof");
auto const xpopCtx = jtx::xpop::TestXPOPContext::create(0);
proof::LedgerProof lp;
lp.ledgerIndex = 17;
lp.totalCoins = 12345;
lp.parentHash = makeHash("xpop-parent");
lp.txRoot = makeHash("xpop-tx-root");
lp.accountRoot = makeHash("xpop-account-root");
lp.parentCloseTime = 100;
lp.closeTime = 200;
lp.closeTimeResolution = 10;
lp.closeFlags = 1;
lp.txBlob = Blob{0x12, 0x00, 0x00};
lp.metaBlob = Blob{0x01, 0x02};
auto const xpop = proof::buildXPOPv1(
lp, std::vector<proof::ValidatorKeys>{}, xpopCtx.vlData);
BEAST_EXPECT(!xpop.isNull());
BEAST_EXPECT(xpop[jss::transaction][jss::proof].isArray());
BEAST_EXPECT(xpop[jss::transaction][jss::proof].size() == 0);
BEAST_EXPECT(xpop[jss::validation][jss::data].size() == 0);
auto const encoded = proof::xpopToHex(xpop);
BEAST_EXPECT(!encoded.empty());
BEAST_EXPECT(strUnHex(encoded).has_value());
}
void
testMerkleProofVerification()
{
testcase("Merkle proof verifies against tx root");
using namespace jtx;
Env env{*this};
Account const alice{"alice"};
Account const bob{"bob"};
Account const carol{"carol"};
env.fund(XRP(10000), alice, bob, carol);
env.close();
// Multiple transactions to create a deeper trie.
env(pay(alice, bob, XRP(10)));
env(pay(bob, carol, XRP(5)));
env(pay(carol, alice, XRP(1)));
env.close();
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
BEAST_EXPECT(lcl);
// Verify proof for each transaction in the ledger.
int proofCount = 0;
lcl->txMap().visitLeaves(
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
auto const lp = proof::buildLedgerProof(*lcl, item->key());
BEAST_EXPECT(lp.has_value());
if (lp && lp->txProof)
{
// Proof must verify against the ledger's tx root.
BEAST_EXPECT(lp->txProof->verify(lp->txRoot));
// JSON v1 serialization must round-trip.
auto const json = lp->txProof->toJsonV1();
BEAST_EXPECT(!json.isNull());
BEAST_EXPECT(json.isArray());
++proofCount;
}
});
// We should have proven at least 3 transactions.
BEAST_EXPECT(proofCount >= 3);
}
void
testImportWithGeneratedXPOP()
{
testcase("Import accepts dynamically generated XPOP");
using namespace jtx;
// Create XPOP context (VL publisher + validators).
auto const xpopCtx = xpop::TestXPOPContext::create(3);
// --- Source "network": generate a payment and build XPOP ---
Env srcEnv{*this};
Account const alice{"alice"};
Account const bob{"bob"};
srcEnv.fund(XRP(10000), alice, bob);
srcEnv.close();
// Import requires: no sfNetworkID + sfOperationLimit = dest NETWORK_ID.
Json::Value payTx;
payTx[jss::TransactionType] = jss::Payment;
payTx[jss::Account] = alice.human();
payTx[jss::Destination] = bob.human();
payTx[jss::Amount] = "100000000";
payTx[sfOperationLimit.jsonName] = 21337;
srcEnv(payTx, fee(XRP(1)));
srcEnv.close();
// Find the tx hash and build the XPOP.
auto const srcLcl = srcEnv.app().getLedgerMaster().getClosedLedger();
BEAST_EXPECT(srcLcl);
uint256 paymentHash;
srcLcl->txMap().visitLeaves(
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
paymentHash = item->key();
});
auto const xpopJson = xpopCtx.buildXPOP(*srcLcl, paymentHash);
BEAST_EXPECT(!xpopJson.isNull());
// --- Destination "network": import the XPOP ---
Env dstEnv{*this, xpopCtx.makeEnvConfig(21337)};
// Burn some XRP so B2M can credit.
auto const master = Account("masterpassphrase");
dstEnv(noop(master), fee(10'000'000'000), ter(tesSUCCESS));
dstEnv.close();
Account const importAlice{"alice"};
dstEnv.fund(XRP(1000), importAlice);
dstEnv.close();
auto const feeDrops = dstEnv.current()->fees().base;
// Submit the import — should succeed (B2M path).
dstEnv(
import::import(importAlice, xpopJson),
fee(feeDrops * 10),
ter(tesSUCCESS));
dstEnv.close();
}
void
run() override
{
testBuildLedgerProof();
testProofBuilderEdgeCases();
testProofBuilderSyntheticTrie();
testBuildXPOPv1();
testBuildXPOPv1WithoutMerkleProof();
testMerkleProofVerification();
testImportWithGeneratedXPOP();
}
};
BEAST_DEFINE_TESTSUITE(XPOP, app, ripple);
} // namespace test
} // namespace ripple
+634
View File
@@ -0,0 +1,634 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <xrpld/app/hook/detail/XportWrapperBuilder.h>
#include <xrpl/basics/Expected.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/ExportOriginMemo.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/STObject.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TxFlags.h>
#include <xrpl/protocol/TxFormats.h>
#include <xrpl/protocol/digest.h>
#include <cstring>
#include <limits>
#include <optional>
namespace ripple {
namespace test {
namespace {
uint256
makeHash(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
STTx
makeSTTx(STObject const& obj)
{
Serializer s;
obj.add(s);
SerialIter sit{s.slice()};
return STTx{std::ref(sit)};
}
Blob
serialize(STTx const& tx)
{
Serializer s;
tx.add(s);
return {s.begin(), s.end()};
}
STTx
makeExportedPayment(
AccountID const& src,
AccountID const& dst,
std::optional<std::uint32_t> networkID = std::nullopt,
std::optional<std::uint32_t> ticketSequence = 1,
std::uint32_t sequence = 0)
{
STObject obj(sfExportedTxn);
obj.setFieldU16(sfTransactionType, ttPAYMENT);
obj.setFieldU32(sfFlags, tfFullyCanonicalSig);
obj.setFieldU32(sfSequence, sequence);
if (ticketSequence)
obj.setFieldU32(sfTicketSequence, *ticketSequence);
obj.setFieldU32(sfFirstLedgerSequence, 2);
obj.setFieldU32(sfLastLedgerSequence, 6);
obj.setFieldAmount(sfAmount, XRPAmount{1000000});
obj.setFieldAmount(sfFee, XRPAmount{10});
obj.setFieldVL(sfSigningPubKey, Blob{});
obj.setAccountID(sfAccount, src);
obj.setAccountID(sfDestination, dst);
if (networkID)
obj.setFieldU32(sfNetworkID, *networkID);
return makeSTTx(obj);
}
STTx
withMemo(STTx tx, Blob type, Blob data)
{
STArray memos = tx.isFieldPresent(sfMemos) ? tx.getFieldArray(sfMemos)
: STArray{sfMemos};
STObject memo{sfMemo};
memo.setFieldVL(sfMemoType, std::move(type));
memo.setFieldVL(sfMemoData, std::move(data));
memos.emplace_back(std::move(memo));
tx.setFieldArray(sfMemos, memos);
return tx;
}
beast::Journal
nullJournal()
{
return beast::Journal{beast::Journal::getNullSink()};
}
hook::XportWrapperBuilder::Input
makeInput(
Slice innerTxBlob,
AccountID const& exporter,
std::uint32_t networkID = 21337,
hook::XportWrapperBuilder::NonceGenerator generateNonce =
[] {
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
},
hook::XportWrapperBuilder::FeeCalculator calculateFee =
[](Slice const&) {
return Expected<std::uint64_t, ::hook_api::hook_return_code>{12345};
})
{
return hook::XportWrapperBuilder::Input{
innerTxBlob,
makeHash("committee"),
exporter,
networkID,
10,
makeHash("parent-tx"),
makeHash("hook-hash"),
true,
3,
7,
std::move(generateNonce),
std::move(calculateFee),
nullJournal()};
}
} // namespace
class XportWrapperBuilder_test : public beast::unit_test::suite
{
public:
void
testBuildsWrapper()
{
testcase("builds xport wrapper");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const serialized = serialize(innerTx);
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first)));
BEAST_EXPECT(result);
if (!result)
return;
auto const& wrapper = result->wrapperTx;
BEAST_EXPECT(wrapper.getTransactionID() != innerTx.getTransactionID());
BEAST_EXPECT(wrapper.getTxnType() == ttEXPORT);
BEAST_EXPECT(
wrapper.getAccountID(sfAccount) == calcAccountID(exporter.first));
BEAST_EXPECT(wrapper.getFieldU32(sfSequence) == 0);
BEAST_EXPECT(wrapper.getFieldU32(sfFirstLedgerSequence) == 11);
BEAST_EXPECT(
wrapper.getFieldU32(sfLastLedgerSequence) ==
10 + ExportLimits::maxAdmissionWindowLedgers);
BEAST_EXPECT(wrapper.getFieldAmount(sfFee) == STAmount{12345});
BEAST_EXPECT(!wrapper.isFieldPresent(sfExportCallbackFee));
BEAST_EXPECT(wrapper.getFieldVL(sfSigningPubKey).empty());
BEAST_EXPECT(
wrapper.getFieldH256(sfExportCommitteeHash) ==
makeHash("committee"));
auto const& exported =
wrapper.peekAtField(sfExportedTxn).downcast<STObject>();
Serializer exportedSer;
exported.add(exportedSer);
STTx parsedInner{SerialIter{exportedSer.slice()}};
BEAST_EXPECT(
parsedInner.getTransactionID() == innerTx.getTransactionID());
auto const& emitDetails =
wrapper.peekAtField(sfEmitDetails).downcast<STObject>();
BEAST_EXPECT(emitDetails.getFieldU32(sfEmitGeneration) == 3);
BEAST_EXPECT(emitDetails.getFieldU64(sfEmitBurden) == 7);
BEAST_EXPECT(
emitDetails.getFieldH256(sfEmitParentTxnID) ==
makeHash("parent-tx"));
BEAST_EXPECT(
emitDetails.getFieldH256(sfEmitNonce) == makeHash("nonce"));
BEAST_EXPECT(
emitDetails.getFieldH256(sfEmitHookHash) == makeHash("hook-hash"));
BEAST_EXPECT(
emitDetails.getAccountID(sfEmitCallback) ==
calcAccountID(exporter.first));
}
void
testCallbackFee()
{
testcase(
"exact callback fee is optional, bounded, and priced in wrapper");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const inner = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const blob = serialize(inner);
auto input = makeInput(makeSlice(blob), calcAccountID(exporter.first));
bool priced = false;
input.calculateFee = [&](Slice const& bytes) {
STTx const wrapper{SerialIter{bytes}};
BEAST_EXPECT(
wrapper.isFieldPresent(sfExportCallbackFee) ==
(input.callbackFeeDrops != 0));
if (input.callbackFeeDrops != 0)
BEAST_EXPECT(
wrapper.getFieldAmount(sfExportCallbackFee).xrp() ==
XRPAmount{
static_cast<std::int64_t>(input.callbackFeeDrops)});
priced = true;
return Expected<std::uint64_t, ::hook_api::hook_return_code>{12345};
};
for (auto amount :
{std::uint64_t{0},
std::uint64_t{1000},
static_cast<std::uint64_t>(INITIAL_XRP.drops())})
{
input.callbackFeeDrops = amount;
priced = false;
auto const result = hook::XportWrapperBuilder::build(input);
BEAST_EXPECT(result);
BEAST_EXPECT(priced);
if (!result)
continue;
BEAST_EXPECT(
result->wrapperTx.getFieldAmount(sfFee) == STAmount{12345});
auto const& target = result->wrapperTx.peekAtField(sfExportedTxn)
.downcast<STObject>();
BEAST_EXPECT(!target.isFieldPresent(sfExportCallbackFee));
}
for (auto amount :
{static_cast<std::uint64_t>(INITIAL_XRP.drops()) + 1,
std::numeric_limits<std::uint64_t>::max()})
{
input.callbackFeeDrops = amount;
priced = false;
auto const result = hook::XportWrapperBuilder::build(input);
BEAST_EXPECT(!result);
if (!result)
BEAST_EXPECT(
result.error() ==
::hook_api::hook_return_code::INVALID_ARGUMENT);
BEAST_EXPECT(!priced);
}
}
void
testBuildsWrapperWithoutCallback()
{
testcase("builds xport wrapper without callback");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const serialized = serialize(innerTx);
auto input = makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first));
input.hasCallback = false;
auto const result = hook::XportWrapperBuilder::build(input);
BEAST_EXPECT(result);
if (!result)
return;
auto const& emitDetails =
result->wrapperTx.peekAtField(sfEmitDetails).downcast<STObject>();
BEAST_EXPECT(!emitDetails.isFieldPresent(sfEmitCallback));
}
void
testPreservesUserMemos()
{
testcase("preserves user memos");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = withMemo(
makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first)),
Blob{'u', 's', 'e', 'r'},
Blob{1, 2, 3, 4});
auto const serialized = serialize(innerTx);
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first)));
BEAST_EXPECT(result);
if (!result)
return;
auto const& exported =
result->wrapperTx.peekAtField(sfExportedTxn).downcast<STObject>();
Serializer exportedSer;
exported.add(exportedSer);
STTx parsedInner{SerialIter{exportedSer.slice()}};
BEAST_EXPECT(serialize(parsedInner) == serialized);
BEAST_EXPECT(!ExportOriginMemo::hasReservedMemo(parsedInner));
}
void
testRejectsInvalidInputs()
{
testcase("rejects invalid inputs");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const other = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const serialized = serialize(innerTx);
{
auto input = makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first));
input.committeeHash = {};
auto const result = hook::XportWrapperBuilder::build(input);
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() ==
::hook_api::hook_return_code::INVALID_ARGUMENT);
}
{
Blob malformed{1, 2, 3};
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(malformed.data(), malformed.size()),
calcAccountID(exporter.first),
21337,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
}
auto expectSigningEnvelopeRejected = [&](STTx tx) {
auto const blob = serialize(tx);
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(blob.data(), blob.size()),
calcAccountID(exporter.first),
21337,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
};
{
auto signedInner = innerTx;
signedInner.setFieldVL(sfSigningPubKey, exporter.first.slice());
expectSigningEnvelopeRejected(std::move(signedInner));
}
{
auto signedInner = innerTx;
signedInner.setFieldVL(sfTxnSignature, Blob{1, 2, 3});
expectSigningEnvelopeRejected(std::move(signedInner));
}
{
auto signedInner = innerTx;
STArray signers(sfSigners);
STObject signer(sfSigner);
signer.setAccountID(sfAccount, calcAccountID(exporter.first));
signer.setFieldVL(sfSigningPubKey, exporter.first.slice());
signer.setFieldVL(sfTxnSignature, Blob{1, 2, 3});
signers.push_back(std::move(signer));
signedInner.setFieldArray(sfSigners, signers);
expectSigningEnvelopeRejected(std::move(signedInner));
}
{
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(other.first),
21337,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
}
{
auto const networkTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first), 21337);
auto const serializedNetwork = serialize(networkTx);
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serializedNetwork.data(), serializedNetwork.size()),
calcAccountID(exporter.first),
21337,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
}
{
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first),
0,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
}
{
auto const noTicketTx = makeExportedPayment(
calcAccountID(exporter.first),
calcAccountID(dst.first),
std::nullopt,
std::nullopt);
auto const serializedNoTicket = serialize(noTicketTx);
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serializedNoTicket.data(), serializedNoTicket.size()),
calcAccountID(exporter.first),
21337,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
}
{
auto const sequencedTicketTx = makeExportedPayment(
calcAccountID(exporter.first),
calcAccountID(dst.first),
std::nullopt,
1,
9);
auto const serializedSequencedTicket = serialize(sequencedTicketTx);
bool nonceCalled = false;
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(
serializedSequencedTicket.data(),
serializedSequencedTicket.size()),
calcAccountID(exporter.first),
21337,
[&nonceCalled] {
nonceCalled = true;
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
BEAST_EXPECT(!nonceCalled);
}
{
auto const reservedType = Blob(
ExportOriginMemo::memoType.begin(),
ExportOriginMemo::memoType.end());
auto const reservedMemoTx = withMemo(
innerTx, reservedType, Blob(ExportOriginMemo::identityBytes));
expectSigningEnvelopeRejected(reservedMemoTx);
}
{
auto const fullMemoTx =
withMemo(innerTx, Blob{'u', 's', 'e', 'r'}, Blob(930, 0x5A));
std::string reason;
BEAST_EXPECT(passesLocalChecks(fullMemoTx, reason));
expectSigningEnvelopeRejected(fullMemoTx);
}
}
void
testRejectsMissingCallbacks()
{
testcase("rejects missing callbacks");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const serialized = serialize(innerTx);
{
auto input = makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first));
input.generateNonce = {};
auto const result = hook::XportWrapperBuilder::build(input);
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::INTERNAL_ERROR);
}
{
auto input = makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first));
input.calculateFee = {};
auto const result = hook::XportWrapperBuilder::build(input);
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
}
}
void
testMapsNonceFailureToInternalError()
{
testcase("maps nonce failure to internal error");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const serialized = serialize(innerTx);
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first),
21337,
[] {
return Expected<uint256, ::hook_api::hook_return_code>{
Unexpected(::hook_api::hook_return_code::TOO_MANY_NONCES)};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::INTERNAL_ERROR);
}
void
testRejectsFeeFailure()
{
testcase("rejects fee failure");
auto const exporter = randomKeyPair(KeyType::secp256k1);
auto const dst = randomKeyPair(KeyType::secp256k1);
auto const innerTx = makeExportedPayment(
calcAccountID(exporter.first), calcAccountID(dst.first));
auto const serialized = serialize(innerTx);
auto const result = hook::XportWrapperBuilder::build(makeInput(
Slice(serialized.data(), serialized.size()),
calcAccountID(exporter.first),
21337,
[] {
return Expected<uint256, ::hook_api::hook_return_code>{
makeHash("nonce")};
},
[](Slice const&) {
return Expected<std::uint64_t, ::hook_api::hook_return_code>{
Unexpected(::hook_api::hook_return_code::EXPORT_FAILURE)};
}));
BEAST_EXPECT(!result);
BEAST_EXPECT(
result.error() == ::hook_api::hook_return_code::EXPORT_FAILURE);
}
void
run() override
{
testBuildsWrapper();
testCallbackFee();
testBuildsWrapperWithoutCallback();
testPreservesUserMemos();
testRejectsInvalidInputs();
testRejectsMissingCallbacks();
testMapsNonceFailureToInternalError();
testRejectsFeeFailure();
}
};
BEAST_DEFINE_TESTSUITE(XportWrapperBuilder, app, ripple);
} // namespace test
} // namespace ripple
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+564 -14
View File
@@ -22,6 +22,8 @@
#include <xrpld/consensus/ConsensusProposal.h>
#include <xrpl/beast/clock/manual_clock.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/json/to_string.h>
#include <optional>
#include <utility>
namespace ripple {
@@ -36,10 +38,18 @@ public:
{
}
static void
enableSilentTracing(csf::Sim& sim)
{
sim.sink.silent(true);
sim.sink.threshold(beast::severities::kTrace);
}
void
testShouldCloseLedger()
{
using namespace std::chrono_literals;
testcase("should close ledger");
// Use default parameters
ConsensusParms const p{};
@@ -78,46 +88,102 @@ public:
testCheckConsensus()
{
using namespace std::chrono_literals;
testcase("check consensus");
// Use default parameterss
ConsensusParms const p{};
///////////////
// Disputes still in doubt
//
// Not enough time has elapsed
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 2, 0, 3s, 2s, p, true, journal_));
checkConsensus(10, 2, 2, 0, 3s, 2s, false, p, true, journal_));
// If not enough peers have propsed, ensure
// more time for proposals
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 2, 0, 3s, 4s, p, true, journal_));
checkConsensus(10, 2, 2, 0, 3s, 4s, false, p, true, journal_));
// Enough time has elapsed and we all agree
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 2, 2, 0, 3s, 10s, p, true, journal_));
checkConsensus(10, 2, 2, 0, 3s, 10s, false, p, true, journal_));
// Enough time has elapsed and we don't yet agree
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 1, 0, 3s, 10s, p, true, journal_));
checkConsensus(10, 2, 1, 0, 3s, 10s, false, p, true, journal_));
// Our peers have moved on
// Enough time has elapsed and we all agree
BEAST_EXPECT(
ConsensusState::MovedOn ==
checkConsensus(10, 2, 1, 8, 3s, 10s, p, true, journal_));
checkConsensus(10, 2, 1, 8, 3s, 10s, false, p, true, journal_));
// If no peers, don't agree until time has passed.
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(0, 0, 0, 0, 3s, 10s, p, true, journal_));
checkConsensus(0, 0, 0, 0, 3s, 10s, false, p, true, journal_));
// Agree if no peers and enough time has passed.
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(0, 0, 0, 0, 3s, 16s, p, true, journal_));
checkConsensus(0, 0, 0, 0, 3s, 16s, false, p, true, journal_));
// Expire if too much time has passed without agreement
BEAST_EXPECT(
ConsensusState::Expired ==
checkConsensus(10, 8, 1, 0, 1s, 19s, false, p, true, journal_));
///////////////
// Stalled
//
// Not enough time has elapsed
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 2, 0, 3s, 2s, true, p, true, journal_));
// If not enough peers have propsed, ensure
// more time for proposals
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(10, 2, 2, 0, 3s, 4s, true, p, true, journal_));
// Enough time has elapsed and we all agree
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 2, 2, 0, 3s, 10s, true, p, true, journal_));
// Enough time has elapsed and we don't yet agree, but there's nothing
// left to dispute
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 2, 1, 0, 3s, 10s, true, p, true, journal_));
// Our peers have moved on
// Enough time has elapsed and we all agree, nothing left to dispute
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 2, 1, 8, 3s, 10s, true, p, true, journal_));
// If no peers, don't agree until time has passed.
BEAST_EXPECT(
ConsensusState::No ==
checkConsensus(0, 0, 0, 0, 3s, 10s, true, p, true, journal_));
// Agree if no peers and enough time has passed.
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(0, 0, 0, 0, 3s, 16s, true, p, true, journal_));
// We are done if there's nothing left to dispute, no matter how much
// time has passed
BEAST_EXPECT(
ConsensusState::Yes ==
checkConsensus(10, 8, 1, 0, 1s, 19s, true, p, true, journal_));
}
void
@@ -125,6 +191,7 @@ public:
{
using namespace std::chrono_literals;
using namespace csf;
testcase("standalone");
Sim s;
PeerGroup peers = s.createGroup(1);
@@ -149,9 +216,12 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("peers agree");
//@@start peers-agree
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup peers = sim.createGroup(5);
// Connected trust and network graphs with single fixed delay
@@ -179,6 +249,7 @@ public:
BEAST_EXPECT(lcl.txs().find(Tx{i}) != lcl.txs().end());
}
}
//@@end peers-agree
}
void
@@ -186,15 +257,18 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("slow peers");
// Several tests of a complete trust graph with a subset of peers
// that have significantly longer network delays to the rest of the
// network
//@@start slow-peer-scenario
// Test when a slow peer doesn't delay a consensus quorum (4/5 agree)
{
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup slow = sim.createGroup(1);
PeerGroup fast = sim.createGroup(4);
PeerGroup network = fast + slow;
@@ -229,16 +303,18 @@ public:
BEAST_EXPECT(
peer->prevRoundTime == network[0]->prevRoundTime);
// Slow peer's transaction (Tx{0}) didn't make it in time
BEAST_EXPECT(lcl.txs().find(Tx{0}) == lcl.txs().end());
for (std::uint32_t i = 2; i < network.size(); ++i)
BEAST_EXPECT(lcl.txs().find(Tx{i}) != lcl.txs().end());
// Tx 0 didn't make it
// Tx 0 is still in the open transaction set for next round
BEAST_EXPECT(
peer->openTxs.find(Tx{0}) != peer->openTxs.end());
}
}
}
//@@end slow-peer-scenario
// Test when the slow peers delay a consensus quorum (4/6 agree)
{
@@ -251,6 +327,7 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup slow = sim.createGroup(2);
PeerGroup fast = sim.createGroup(4);
PeerGroup network = fast + slow;
@@ -351,6 +428,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("close time disagree");
// This is a very specialized test to get ledgers to disagree on
// the close time. It unfortunately assumes knowledge about current
@@ -379,6 +457,7 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup groupA = sim.createGroup(2);
PeerGroup groupB = sim.createGroup(2);
@@ -412,11 +491,59 @@ public:
}
}
void
testBootstrapFastStart()
{
using namespace csf;
using namespace std::chrono;
testcase("bootstrap fast start");
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup peers = sim.createGroup(4);
peers.trustAndConnect(
peers, round<milliseconds>(0.2 * parms.ledgerGRANULARITY));
for (Peer* peer : peers)
{
peer->ce().testBootstrapFastStartEnabled_ = true;
peer->targetLedgers =
static_cast<int>(parms.bootstrapStableRoundsRequired);
peer->start();
auto const json = peer->consensus.getJson(true);
BEAST_EXPECT(json.isMember("bootstrap_fast_start"));
BEAST_EXPECT(json["bootstrap_fast_start"].asBool());
BEAST_EXPECT(
json["previous_mseconds"].asInt() ==
parms.bootstrapRoundTimeSeed.count());
BEAST_EXPECT(json["bootstrap_stable_rounds"].asInt() == 0);
}
sim.scheduler.step();
if (BEAST_EXPECT(sim.synchronized()))
{
for (Peer* peer : peers)
{
BEAST_EXPECT(
peer->completedLedgers ==
static_cast<int>(parms.bootstrapStableRoundsRequired));
auto const json = peer->consensus.getJson(true);
BEAST_EXPECT(!json.isMember("bootstrap_fast_start"));
BEAST_EXPECT(peer->prevRoundTime < parms.ledgerIDLE_INTERVAL);
}
}
}
void
testWrongLCL()
{
using namespace csf;
using namespace std::chrono;
testcase("wrong LCL");
// Specialized test to exercise a temporary fork in which some peers
// are working on an incorrect prior ledger.
@@ -426,6 +553,7 @@ public:
// the wrong LCL at different phases of consensus
for (auto validationDelay : {0ms, parms.ledgerMIN_CLOSE})
{
//@@start wrong-lcl-scenario
// Consider 10 peers:
// 0 1 2 3 4 5 6 7 8 9
// minority majorityA majorityB
@@ -446,8 +574,10 @@ public:
// This topology can potentially fork with the above trust relations
// but that is intended for this test.
//@@end wrong-lcl-scenario
Sim sim;
enableSilentTracing(sim);
PeerGroup minority = sim.createGroup(2);
PeerGroup majorityA = sim.createGroup(3);
@@ -552,6 +682,7 @@ public:
// after it is already in the establish phase of the next round.
Sim sim;
enableSilentTracing(sim);
PeerGroup loner = sim.createGroup(1);
PeerGroup friends = sim.createGroup(3);
loner.trust(loner + friends);
@@ -589,6 +720,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("consensus close time rounding");
// This is a specialized test engineered to yield ledgers with different
// close times even though the peers believe they had close time
@@ -596,6 +728,7 @@ public:
ConsensusParms parms;
Sim sim;
enableSilentTracing(sim);
// This requires a group of 4 fast and 2 slow peers to create a
// situation in which a subset of peers requires seeing additional
@@ -604,9 +737,6 @@ public:
PeerGroup fast = sim.createGroup(4);
PeerGroup network = fast + slow;
for (Peer* peer : network)
peer->consensusParms = parms;
// Connected trust graph
network.trust(network);
@@ -692,6 +822,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("fork");
std::uint32_t numPeers = 10;
// Vary overlap between two UNLs
@@ -699,6 +830,7 @@ public:
{
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
std::uint32_t numA = (numPeers - overlap) / 2;
std::uint32_t numB = numPeers - numA - overlap;
@@ -729,9 +861,13 @@ public:
}
sim.run(1);
// Fork should not happen for 40% or greater overlap
// Since the overlapped nodes have a UNL that is the union of the
// two cliques, the maximum sized UNL list is the number of peers
//@@start fork-threshold
// Historical CSF topology regression: in this symmetric two-clique
// setup, the overlapped nodes trust the union of both cliques and
// the legacy simulator expects synchronization above the 40%
// overlap boundary. This is not a general XRP LCP safety theorem;
// modern consensus analysis has stricter heterogeneous-UNL
// assumptions.
if (overlap > 0.4 * numPeers)
BEAST_EXPECT(sim.synchronized());
else
@@ -740,6 +876,7 @@ public:
// One for cliqueA, one for cliqueB and one for nodes in both
BEAST_EXPECT(sim.branches() <= 3);
}
//@@end fork-threshold
}
}
@@ -748,12 +885,14 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("hub network");
// Simulate a set of 5 validators that aren't directly connected but
// rely on a single hub node for communication
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
PeerGroup validators = sim.createGroup(5);
PeerGroup center = sim.createGroup(1);
validators.trust(validators);
@@ -835,6 +974,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("preferred by branch");
// Simulate network splits that are prevented from forking when using
// preferred ledger by trie. This is a contrived example that involves
@@ -868,6 +1008,7 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
// Goes A->B->D
PeerGroup groupABD = sim.createGroup(2);
@@ -967,6 +1108,7 @@ public:
{
using namespace csf;
using namespace std::chrono;
testcase("pause for laggards");
// Test that validators that jump ahead of the network slow
// down.
@@ -990,6 +1132,7 @@ public:
ConsensusParms const parms{};
Sim sim;
enableSilentTracing(sim);
SimDuration delay = round<milliseconds>(0.2 * parms.ledgerGRANULARITY);
PeerGroup behind = sim.createGroup(3);
@@ -1052,6 +1195,410 @@ public:
BEAST_EXPECT(sim.synchronized());
}
// RNG consensus tests in ConsensusRng_test.cpp
// MERGE NOTE (sync-2.5.0): upstream testDisputes() is already present
// below with j/clog stalled() params from 86ef16dbeb. If upstream
// auto-merges a duplicate, delete it — keep only this version.
void
testDisputes()
{
testcase("disputes");
using namespace csf;
// Test dispute objects directly
using Dispute = DisputedTx<Tx, PeerID>;
Tx const txTrue{99};
Tx const txFalse{98};
Tx const txFollowingTrue{97};
Tx const txFollowingFalse{96};
int const numPeers = 100;
ConsensusParms p;
std::size_t peersUnchanged = 0;
auto logs = std::make_unique<Logs>(beast::severities::kError);
auto j = logs->journal("Test");
auto clog = std::make_unique<std::stringstream>();
// Three cases:
// 1 proposing, initial vote yes
// 2 proposing, initial vote no
// 3 not proposing, initial vote doesn't matter after the first update,
// use yes
{
Dispute proposingTrue{txTrue.id(), true, numPeers, journal_};
Dispute proposingFalse{txFalse.id(), false, numPeers, journal_};
Dispute followingTrue{
txFollowingTrue.id(), true, numPeers, journal_};
Dispute followingFalse{
txFollowingFalse.id(), false, numPeers, journal_};
BEAST_EXPECT(proposingTrue.ID() == 99);
BEAST_EXPECT(proposingFalse.ID() == 98);
BEAST_EXPECT(followingTrue.ID() == 97);
BEAST_EXPECT(followingFalse.ID() == 96);
// Create an even split in the peer votes
for (int i = 0; i < numPeers; ++i)
{
BEAST_EXPECT(proposingTrue.setVote(PeerID(i), i < 50));
BEAST_EXPECT(proposingFalse.setVote(PeerID(i), i < 50));
BEAST_EXPECT(followingTrue.setVote(PeerID(i), i < 50));
BEAST_EXPECT(followingFalse.setVote(PeerID(i), i < 50));
}
// Switch the middle vote to match mine
BEAST_EXPECT(proposingTrue.setVote(PeerID(50), true));
BEAST_EXPECT(proposingFalse.setVote(PeerID(49), false));
BEAST_EXPECT(followingTrue.setVote(PeerID(50), true));
BEAST_EXPECT(followingFalse.setVote(PeerID(49), false));
// no changes yet
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
BEAST_EXPECT(
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!proposingFalse.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingTrue.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingFalse.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(clog->str() == "");
// I'm in the majority, my vote should not change
BEAST_EXPECT(!proposingTrue.updateVote(5, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(5, true, p));
BEAST_EXPECT(!followingTrue.updateVote(5, false, p));
BEAST_EXPECT(!followingFalse.updateVote(5, false, p));
BEAST_EXPECT(!proposingTrue.updateVote(10, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(10, true, p));
BEAST_EXPECT(!followingTrue.updateVote(10, false, p));
BEAST_EXPECT(!followingFalse.updateVote(10, false, p));
peersUnchanged = 2;
BEAST_EXPECT(
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!proposingFalse.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingTrue.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingFalse.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(clog->str() == "");
// Right now, the vote is 51%. The requirement is about to jump to
// 65%
BEAST_EXPECT(proposingTrue.updateVote(55, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(55, true, p));
BEAST_EXPECT(!followingTrue.updateVote(55, false, p));
BEAST_EXPECT(!followingFalse.updateVote(55, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == false);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// 16 validators change their vote to match my original vote
for (int i = 0; i < 16; ++i)
{
auto pTrue = PeerID(numPeers - i - 1);
auto pFalse = PeerID(i);
BEAST_EXPECT(proposingTrue.setVote(pTrue, true));
BEAST_EXPECT(proposingFalse.setVote(pFalse, false));
BEAST_EXPECT(followingTrue.setVote(pTrue, true));
BEAST_EXPECT(followingFalse.setVote(pFalse, false));
}
// The vote should now be 66%, threshold is 65%
BEAST_EXPECT(proposingTrue.updateVote(60, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(60, true, p));
BEAST_EXPECT(!followingTrue.updateVote(60, false, p));
BEAST_EXPECT(!followingFalse.updateVote(60, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// Threshold jumps to 70%
BEAST_EXPECT(proposingTrue.updateVote(86, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(86, true, p));
BEAST_EXPECT(!followingTrue.updateVote(86, false, p));
BEAST_EXPECT(!followingFalse.updateVote(86, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == false);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// 5 more validators change their vote to match my original vote
for (int i = 16; i < 21; ++i)
{
auto pTrue = PeerID(numPeers - i - 1);
auto pFalse = PeerID(i);
BEAST_EXPECT(proposingTrue.setVote(pTrue, true));
BEAST_EXPECT(proposingFalse.setVote(pFalse, false));
BEAST_EXPECT(followingTrue.setVote(pTrue, true));
BEAST_EXPECT(followingFalse.setVote(pFalse, false));
}
// The vote should now be 71%, threshold is 70%
BEAST_EXPECT(proposingTrue.updateVote(90, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(90, true, p));
BEAST_EXPECT(!followingTrue.updateVote(90, false, p));
BEAST_EXPECT(!followingFalse.updateVote(90, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// The vote should now be 71%, threshold is 70%
BEAST_EXPECT(!proposingTrue.updateVote(150, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(150, true, p));
BEAST_EXPECT(!followingTrue.updateVote(150, false, p));
BEAST_EXPECT(!followingFalse.updateVote(150, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// The vote should now be 71%, threshold is 70%
BEAST_EXPECT(!proposingTrue.updateVote(190, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(190, true, p));
BEAST_EXPECT(!followingTrue.updateVote(190, false, p));
BEAST_EXPECT(!followingFalse.updateVote(190, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
peersUnchanged = 3;
BEAST_EXPECT(
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!proposingFalse.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingTrue.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingFalse.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(clog->str() == "");
// Threshold jumps to 95%
BEAST_EXPECT(proposingTrue.updateVote(220, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(220, true, p));
BEAST_EXPECT(!followingTrue.updateVote(220, false, p));
BEAST_EXPECT(!followingFalse.updateVote(220, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == false);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// 25 more validators change their vote to match my original vote
for (int i = 21; i < 46; ++i)
{
auto pTrue = PeerID(numPeers - i - 1);
auto pFalse = PeerID(i);
BEAST_EXPECT(proposingTrue.setVote(pTrue, true));
BEAST_EXPECT(proposingFalse.setVote(pFalse, false));
BEAST_EXPECT(followingTrue.setVote(pTrue, true));
BEAST_EXPECT(followingFalse.setVote(pFalse, false));
}
// The vote should now be 96%, threshold is 95%
BEAST_EXPECT(proposingTrue.updateVote(250, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(250, true, p));
BEAST_EXPECT(!followingTrue.updateVote(250, false, p));
BEAST_EXPECT(!followingFalse.updateVote(250, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
for (peersUnchanged = 0; peersUnchanged < 6; ++peersUnchanged)
{
BEAST_EXPECT(
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!proposingFalse.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingTrue.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(
!followingFalse.stalled(p, false, peersUnchanged, j, clog));
BEAST_EXPECT(clog->str() == "");
}
auto expectStalled = [this, &clog](
int txid,
bool ourVote,
int ourTime,
int peerTime,
int support,
std::uint32_t line) {
using namespace std::string_literals;
auto const s = clog->str();
expect(s.find("stalled"), s, __FILE__, line);
expect(
s.starts_with("Transaction "s + std::to_string(txid)),
s,
__FILE__,
line);
expect(
s.find("voting "s + (ourVote ? "YES" : "NO")) != s.npos,
s,
__FILE__,
line);
expect(
s.find("for "s + std::to_string(ourTime) + " rounds."s) !=
s.npos,
s,
__FILE__,
line);
expect(
s.find(
"votes in "s + std::to_string(peerTime) + " rounds.") !=
s.npos,
s,
__FILE__,
line);
expect(
s.ends_with(
"has "s + std::to_string(support) + "% support. "s),
s,
__FILE__,
line);
clog = std::make_unique<std::stringstream>();
};
for (int i = 0; i < 1; ++i)
{
BEAST_EXPECT(!proposingTrue.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!followingTrue.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(
!followingFalse.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// true vote has changed recently, so not stalled
BEAST_EXPECT(!proposingTrue.stalled(p, true, 0, j, clog));
BEAST_EXPECT(clog->str() == "");
// remaining votes have been unchanged in so long that we only
// need to hit the second round at 95% to be stalled, regardless
// of peers
BEAST_EXPECT(proposingFalse.stalled(p, true, 0, j, clog));
expectStalled(98, false, 11, 0, 2, __LINE__);
BEAST_EXPECT(followingTrue.stalled(p, false, 0, j, clog));
expectStalled(97, true, 11, 0, 97, __LINE__);
BEAST_EXPECT(followingFalse.stalled(p, false, 0, j, clog));
expectStalled(96, false, 11, 0, 3, __LINE__);
// true vote has changed recently, so not stalled
BEAST_EXPECT(
!proposingTrue.stalled(p, true, peersUnchanged, j, clog));
BEAST_EXPECTS(clog->str() == "", clog->str());
// remaining votes have been unchanged in so long that we only
// need to hit the second round at 95% to be stalled, regardless
// of peers
BEAST_EXPECT(
proposingFalse.stalled(p, true, peersUnchanged, j, clog));
expectStalled(98, false, 11, 6, 2, __LINE__);
BEAST_EXPECT(
followingTrue.stalled(p, false, peersUnchanged, j, clog));
expectStalled(97, true, 11, 6, 97, __LINE__);
BEAST_EXPECT(
followingFalse.stalled(p, false, peersUnchanged, j, clog));
expectStalled(96, false, 11, 6, 3, __LINE__);
}
for (int i = 1; i < 3; ++i)
{
BEAST_EXPECT(!proposingTrue.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!followingTrue.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(
!followingFalse.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
// true vote changed 2 rounds ago, and peers are changing, so
// not stalled
BEAST_EXPECT(!proposingTrue.stalled(p, true, 0, j, clog));
BEAST_EXPECTS(clog->str() == "", clog->str());
// still stalled
BEAST_EXPECT(proposingFalse.stalled(p, true, 0, j, clog));
expectStalled(98, false, 11 + i, 0, 2, __LINE__);
BEAST_EXPECT(followingTrue.stalled(p, false, 0, j, clog));
expectStalled(97, true, 11 + i, 0, 97, __LINE__);
BEAST_EXPECT(followingFalse.stalled(p, false, 0, j, clog));
expectStalled(96, false, 11 + i, 0, 3, __LINE__);
// true vote changed 2 rounds ago, and peers are NOT changing,
// so stalled
BEAST_EXPECT(
proposingTrue.stalled(p, true, peersUnchanged, j, clog));
expectStalled(99, true, 1 + i, 6, 97, __LINE__);
// still stalled
BEAST_EXPECT(
proposingFalse.stalled(p, true, peersUnchanged, j, clog));
expectStalled(98, false, 11 + i, 6, 2, __LINE__);
BEAST_EXPECT(
followingTrue.stalled(p, false, peersUnchanged, j, clog));
expectStalled(97, true, 11 + i, 6, 97, __LINE__);
BEAST_EXPECT(
followingFalse.stalled(p, false, peersUnchanged, j, clog));
expectStalled(96, false, 11 + i, 6, 3, __LINE__);
}
for (int i = 3; i < 5; ++i)
{
BEAST_EXPECT(!proposingTrue.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!proposingFalse.updateVote(250 + 10 * i, true, p));
BEAST_EXPECT(!followingTrue.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(
!followingFalse.updateVote(250 + 10 * i, false, p));
BEAST_EXPECT(proposingTrue.getOurVote() == true);
BEAST_EXPECT(proposingFalse.getOurVote() == false);
BEAST_EXPECT(followingTrue.getOurVote() == true);
BEAST_EXPECT(followingFalse.getOurVote() == false);
BEAST_EXPECT(proposingTrue.stalled(p, true, 0, j, clog));
expectStalled(99, true, 1 + i, 0, 97, __LINE__);
BEAST_EXPECT(proposingFalse.stalled(p, true, 0, j, clog));
expectStalled(98, false, 11 + i, 0, 2, __LINE__);
BEAST_EXPECT(followingTrue.stalled(p, false, 0, j, clog));
expectStalled(97, true, 11 + i, 0, 97, __LINE__);
BEAST_EXPECT(followingFalse.stalled(p, false, 0, j, clog));
expectStalled(96, false, 11 + i, 0, 3, __LINE__);
BEAST_EXPECT(
proposingTrue.stalled(p, true, peersUnchanged, j, clog));
expectStalled(99, true, 1 + i, 6, 97, __LINE__);
BEAST_EXPECT(
proposingFalse.stalled(p, true, peersUnchanged, j, clog));
expectStalled(98, false, 11 + i, 6, 2, __LINE__);
BEAST_EXPECT(
followingTrue.stalled(p, false, peersUnchanged, j, clog));
expectStalled(97, true, 11 + i, 6, 97, __LINE__);
BEAST_EXPECT(
followingFalse.stalled(p, false, peersUnchanged, j, clog));
expectStalled(96, false, 11 + i, 6, 3, __LINE__);
}
}
}
void
run() override
{
@@ -1062,12 +1609,15 @@ public:
testPeersAgree();
testSlowPeers();
testCloseTimeDisagree();
testBootstrapFastStart();
testWrongLCL();
testConsensusCloseTimeRounding();
testFork();
testHubNetwork();
testPreferredByBranch();
testPauseForLaggards();
// RNG consensus tests moved to ConsensusRng_test.cpp
testDisputes();
}
};
@@ -0,0 +1,772 @@
//------------------------------------------------------------------------------
/*
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/consensus/RCLCxPeerPos.h>
#include <xrpld/consensus/ConsensusProposal.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/digest.h>
#include <xrpl/protocol/jss.h>
#include <cstring>
#include <sstream>
namespace ripple {
namespace test {
class ExtendedPosition_test : public beast::unit_test::suite
{
// Generate deterministic test hashes
static uint256
makeHash(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
void
testSerializationRoundTrip()
{
testcase("Serialization round-trip");
//@@start test-extended-position-legacy-compat
// Empty position (legacy compat)
{
auto const txSet = makeHash("txset-a");
ExtendedPosition pos{txSet};
Serializer s;
pos.add(s);
// Should be exactly 32 bytes (no flags byte)
BEAST_EXPECT(s.getDataLength() == 32);
SerialIter sit(s.slice());
auto deserialized =
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
BEAST_EXPECT(deserialized.has_value());
if (!deserialized)
return;
BEAST_EXPECT(deserialized->txSetHash == txSet);
BEAST_EXPECT(!deserialized->myCommitment);
BEAST_EXPECT(!deserialized->myReveal);
BEAST_EXPECT(!deserialized->commitSetHash);
BEAST_EXPECT(!deserialized->entropySetHash);
BEAST_EXPECT(!deserialized->exportSigSetHash);
BEAST_EXPECT(!deserialized->exportSignaturesHash);
BEAST_EXPECT(!deserialized->observedParticipantsHash);
}
//@@end test-extended-position-legacy-compat
//@@start test-extended-position-export-root-wire-bit
// Pin the existing Export root allocation independently of the
// all-fields fixture: byte 32 is flags, and bit 0x10 carries exactly
// one following uint256 exportSigSetHash.
{
auto const txSet = makeHash("txset-export-root-wire");
auto const exportRoot = makeHash("export-root-wire");
ExtendedPosition pos{txSet};
pos.exportSigSetHash = exportRoot;
Serializer s;
pos.add(s);
BEAST_EXPECT(s.getDataLength() == 65);
BEAST_EXPECT(s.peekData()[32] == 0x10);
SerialIter sit(s.slice());
auto const deserialized =
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
BEAST_EXPECT(deserialized);
if (deserialized)
{
BEAST_EXPECT(deserialized->txSetHash == txSet);
BEAST_EXPECT(deserialized->exportSigSetHash == exportRoot);
BEAST_EXPECT(!deserialized->exportSignaturesHash);
}
}
//@@end test-extended-position-export-root-wire-bit
// Position with commitment
{
auto const txSet = makeHash("txset-b");
auto const commit = makeHash("commit-b");
ExtendedPosition pos{txSet};
pos.myCommitment = commit;
Serializer s;
pos.add(s);
// 32 (txSet) + 1 (flags) + 32 (commitment) = 65
BEAST_EXPECT(s.getDataLength() == 65);
SerialIter sit(s.slice());
auto deserialized =
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
BEAST_EXPECT(deserialized.has_value());
if (!deserialized)
return;
BEAST_EXPECT(deserialized->txSetHash == txSet);
BEAST_EXPECT(deserialized->myCommitment == commit);
BEAST_EXPECT(!deserialized->myReveal);
}
// Position with diagnostic participant hash only
{
auto const txSet = makeHash("txset-participants");
auto const participants = makeHash("participants");
ExtendedPosition pos{txSet};
pos.observedParticipantsHash = participants;
Serializer s;
pos.add(s);
// 32 (txSet) + 1 (flags) + 32 (participant hash) = 65
BEAST_EXPECT(s.getDataLength() == 65);
SerialIter sit(s.slice());
auto deserialized =
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
BEAST_EXPECT(deserialized.has_value());
if (!deserialized)
return;
BEAST_EXPECT(deserialized->txSetHash == txSet);
BEAST_EXPECT(
deserialized->observedParticipantsHash == participants);
BEAST_EXPECT(!deserialized->myCommitment);
BEAST_EXPECT(!deserialized->myReveal);
}
// Position with all fields
{
auto const txSet = makeHash("txset-c");
auto const commitSet = makeHash("commitset-c");
auto const entropySet = makeHash("entropyset-c");
auto const exportSigSet = makeHash("exportsigset-c");
auto const exportSigs = makeHash("exportsigs-c");
auto const participants = makeHash("participants-c");
auto const commit = makeHash("commit-c");
auto const reveal = makeHash("reveal-c");
ExtendedPosition pos{txSet};
pos.commitSetHash = commitSet;
pos.entropySetHash = entropySet;
pos.exportSigSetHash = exportSigSet;
pos.exportSignaturesHash = exportSigs;
pos.observedParticipantsHash = participants;
pos.myCommitment = commit;
pos.myReveal = reveal;
Serializer s;
pos.add(s);
// 32 + 1 + 7*32 = 257
BEAST_EXPECT(s.getDataLength() == 257);
SerialIter sit(s.slice());
auto deserialized =
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
BEAST_EXPECT(deserialized.has_value());
if (!deserialized)
return;
BEAST_EXPECT(deserialized->txSetHash == txSet);
BEAST_EXPECT(deserialized->commitSetHash == commitSet);
BEAST_EXPECT(deserialized->entropySetHash == entropySet);
BEAST_EXPECT(deserialized->exportSigSetHash == exportSigSet);
BEAST_EXPECT(deserialized->exportSignaturesHash == exportSigs);
BEAST_EXPECT(
deserialized->observedParticipantsHash == participants);
BEAST_EXPECT(deserialized->myCommitment == commit);
BEAST_EXPECT(deserialized->myReveal == reveal);
}
}
void
testSigningConsistency()
{
testcase("Signing hash consistency");
// The signing hash from ConsensusProposal::signingHash() must match
// what a receiver would compute via the same function after
// deserializing the ExtendedPosition from the wire.
auto const [pk, sk] = randomKeyPair(KeyType::secp256k1);
auto const nodeId = calcNodeID(pk);
auto const prevLedger = makeHash("prevledger");
auto const closeTime =
NetClock::time_point{NetClock::duration{1234567}};
//@@start test-extended-position-signing-binds-fields
// Test with commitment (the case that was failing)
{
auto const txSet = makeHash("txset-sign");
auto const commit = makeHash("commitment-sign");
ExtendedPosition pos{txSet};
pos.myCommitment = commit;
using Proposal =
ConsensusProposal<NodeID, uint256, ExtendedPosition>;
Proposal prop{
prevLedger,
Proposal::seqJoin,
pos,
closeTime,
NetClock::time_point{},
nodeId};
// Sign it (same as propose() does)
auto const signingHash = prop.signingHash();
auto sig = signDigest(pk, sk, signingHash);
// Serialize position to wire format
Serializer positionData;
pos.add(positionData);
auto const posSlice = positionData.slice();
// Deserialize (same as PeerImp::onMessage does)
SerialIter sit(posSlice);
auto const maybeReceivedPos =
ExtendedPosition::fromSerialIter(sit, posSlice.size());
BEAST_EXPECT(maybeReceivedPos.has_value());
if (!maybeReceivedPos)
return;
// Reconstruct proposal on receiver side
Proposal receivedProp{
prevLedger,
Proposal::seqJoin,
*maybeReceivedPos,
closeTime,
NetClock::time_point{},
nodeId};
// The signing hash must match
BEAST_EXPECT(receivedProp.signingHash() == signingHash);
// Verify signature (same as checkSign does)
BEAST_EXPECT(
verifyDigest(pk, receivedProp.signingHash(), sig, false));
}
//@@end test-extended-position-signing-binds-fields
// Test without commitment (legacy case)
{
auto const txSet = makeHash("txset-legacy");
ExtendedPosition pos{txSet};
using Proposal =
ConsensusProposal<NodeID, uint256, ExtendedPosition>;
Proposal prop{
prevLedger,
Proposal::seqJoin,
pos,
closeTime,
NetClock::time_point{},
nodeId};
auto const signingHash = prop.signingHash();
auto sig = signDigest(pk, sk, signingHash);
Serializer positionData;
pos.add(positionData);
SerialIter sit(positionData.slice());
auto const maybeReceivedPos = ExtendedPosition::fromSerialIter(
sit, positionData.getDataLength());
BEAST_EXPECT(maybeReceivedPos.has_value());
if (!maybeReceivedPos)
return;
Proposal receivedProp{
prevLedger,
Proposal::seqJoin,
*maybeReceivedPos,
closeTime,
NetClock::time_point{},
nodeId};
BEAST_EXPECT(receivedProp.signingHash() == signingHash);
BEAST_EXPECT(
verifyDigest(pk, receivedProp.signingHash(), sig, false));
}
}
void
testSuppressionConsistency()
{
testcase("Suppression hash consistency");
// proposalUniqueId must produce the same result on sender and
// receiver when given the same ExtendedPosition data.
auto const [pk, sk] = randomKeyPair(KeyType::secp256k1);
auto const prevLedger = makeHash("prevledger-supp");
auto const closeTime =
NetClock::time_point{NetClock::duration{1234567}};
std::uint32_t const proposeSeq = 0;
auto const txSet = makeHash("txset-supp");
auto const commit = makeHash("commitment-supp");
ExtendedPosition pos{txSet};
pos.myCommitment = commit;
// Sign (to get a real signature for suppression)
using Proposal = ConsensusProposal<NodeID, uint256, ExtendedPosition>;
Proposal prop{
prevLedger,
proposeSeq,
pos,
closeTime,
NetClock::time_point{},
calcNodeID(pk)};
auto sig = signDigest(pk, sk, prop.signingHash());
// Sender computes suppression
auto const senderSuppression =
proposalUniqueId(pos, prevLedger, proposeSeq, closeTime, pk, sig);
// Simulate wire: serialize and deserialize
Serializer positionData;
pos.add(positionData);
SerialIter sit(positionData.slice());
auto const maybeReceivedPos =
ExtendedPosition::fromSerialIter(sit, positionData.getDataLength());
BEAST_EXPECT(maybeReceivedPos.has_value());
if (!maybeReceivedPos)
return;
// Receiver computes suppression
auto const receiverSuppression = proposalUniqueId(
*maybeReceivedPos, prevLedger, proposeSeq, closeTime, pk, sig);
BEAST_EXPECT(senderSuppression == receiverSuppression);
}
void
testPeerPosition()
{
testcase("RCLCxPeerPos signed proposal wrapper");
auto const [pk, sk] = randomKeyPair(KeyType::secp256k1);
auto const nodeId = calcNodeID(pk);
auto const prevLedger = makeHash("prevledger-peer");
auto const closeTime =
NetClock::time_point{NetClock::duration{7654321}};
ExtendedPosition pos{makeHash("txset-peer")};
pos.commitSetHash = makeHash("commitset-peer");
pos.entropySetHash = makeHash("entropyset-peer");
pos.exportSigSetHash = makeHash("exportsigset-peer");
pos.exportSignaturesHash = makeHash("exportsigs-peer");
pos.observedParticipantsHash = makeHash("participants-peer");
pos.myCommitment = makeHash("commitment-peer");
pos.myReveal = makeHash("reveal-peer");
using Proposal = ConsensusProposal<NodeID, uint256, ExtendedPosition>;
Proposal prop{
prevLedger,
Proposal::seqJoin,
pos,
closeTime,
NetClock::time_point{},
nodeId};
auto const sig = signDigest(pk, sk, prop.signingHash());
auto const suppression = proposalUniqueId(
pos, prevLedger, prop.proposeSeq(), closeTime, pk, sig);
std::vector<std::string> exportSignatures{
"export-sig-a", "export-sig-b"};
RCLCxPeerPos peer{
pk,
sig,
suppression,
Proposal{
prevLedger,
Proposal::seqJoin,
pos,
closeTime,
NetClock::time_point{},
nodeId},
exportSignatures};
BEAST_EXPECT(peer.checkSign());
BEAST_EXPECT(peer.publicKey() == pk);
BEAST_EXPECT(peer.signature().size() == sig.size());
BEAST_EXPECT(peer.suppressionID() == suppression);
BEAST_EXPECT(peer.proposal().position().txSetHash == pos.txSetHash);
BEAST_EXPECT(peer.exportSignatures() == exportSignatures);
BEAST_EXPECT(!peer.render().empty());
auto const json = peer.getJson();
BEAST_EXPECT(json.isMember(jss::peer_id));
BEAST_EXPECT(
json[jss::peer_id].asString() ==
toBase58(TokenType::NodePublic, pk));
auto badSig = sig;
badSig.data()[badSig.size() - 1] ^= 0x01;
RCLCxPeerPos badPeer{
pk,
badSig,
suppression,
Proposal{
prevLedger,
Proposal::seqJoin,
pos,
closeTime,
NetClock::time_point{},
nodeId}};
BEAST_EXPECT(!badPeer.checkSign());
auto mutated = pos;
mutated.myReveal = makeHash("reveal-peer-mutated");
BEAST_EXPECT(
proposalUniqueId(
mutated, prevLedger, prop.proposeSeq(), closeTime, pk, sig) !=
suppression);
mutated = pos;
//@@start test-extended-position-export-root-binds-proposal
mutated.exportSigSetHash = makeHash("exportsigset-peer-mutated");
BEAST_EXPECT(
proposalUniqueId(
mutated, prevLedger, prop.proposeSeq(), closeTime, pk, sig) !=
suppression);
Proposal mutatedRootProposal{
prevLedger,
Proposal::seqJoin,
mutated,
closeTime,
NetClock::time_point{},
nodeId};
BEAST_EXPECT(mutatedRootProposal.signingHash() != prop.signingHash());
BEAST_EXPECT(
!verifyDigest(pk, mutatedRootProposal.signingHash(), sig, false));
//@@end test-extended-position-export-root-binds-proposal
mutated = pos;
mutated.exportSignaturesHash = makeHash("exportsigs-peer-mutated");
BEAST_EXPECT(
proposalUniqueId(
mutated, prevLedger, prop.proposeSeq(), closeTime, pk, sig) !=
suppression);
BEAST_EXPECT(
proposalUniqueId(
pos,
makeHash("prevledger-peer-mutated"),
prop.proposeSeq(),
closeTime,
pk,
sig) != suppression);
BEAST_EXPECT(
proposalUniqueId(
pos, prevLedger, prop.proposeSeq() + 1, closeTime, pk, sig) !=
suppression);
BEAST_EXPECT(
proposalUniqueId(
pos,
prevLedger,
prop.proposeSeq(),
NetClock::time_point{NetClock::duration{7654322}},
pk,
sig) != suppression);
auto const [otherPk, unusedSk] = randomKeyPair(KeyType::secp256k1);
(void)unusedSk;
BEAST_EXPECT(
proposalUniqueId(
pos, prevLedger, prop.proposeSeq(), closeTime, otherPk, sig) !=
suppression);
}
void
testMalformedPayload()
{
testcase("Malformed payload rejected");
//@@start test-extended-position-malformed-canonicality
// Too short (< 32 bytes)
{
Serializer s;
s.add32(0xDEADBEEF); // only 4 bytes
SerialIter sit(s.slice());
auto result =
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
BEAST_EXPECT(!result.has_value());
}
// Empty payload
{
Serializer s;
SerialIter sit(s.slice());
auto result = ExtendedPosition::fromSerialIter(sit, 0);
BEAST_EXPECT(!result.has_value());
}
// Flags claim fields that aren't present (truncated)
{
auto const txSet = makeHash("txset-malformed");
Serializer s;
s.addBitString(txSet);
// flags = 0x0F (all 4 fields), but no field data follows
s.add8(0x0F);
SerialIter sit(s.slice());
auto result =
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
BEAST_EXPECT(!result.has_value());
}
// Flags claim 2 fields but only 1 field's worth of data
{
auto const txSet = makeHash("txset-malformed2");
auto const commit = makeHash("commit-malformed2");
Serializer s;
s.addBitString(txSet);
// flags = 0x03 (commitSetHash + entropySetHash), but only
// provide commitSetHash data
s.add8(0x03);
s.addBitString(commit);
SerialIter sit(s.slice());
auto result =
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
BEAST_EXPECT(!result.has_value());
}
// Unknown flag bits above known extension fields (wire malleability)
{
auto const txSet = makeHash("txset-unkflags");
Serializer s;
s.addBitString(txSet);
s.add8(0x81); // bit 7 is unknown, bit 0 = commitSetHash
s.addBitString(makeHash("commitset-unkflags"));
SerialIter sit(s.slice());
auto result =
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
BEAST_EXPECT(!result.has_value());
}
// Size says extended payload, but no flag byte remains.
{
auto const txSet = makeHash("txset-missing-flags");
Serializer s;
s.addBitString(txSet);
SerialIter sit(s.slice());
auto result = ExtendedPosition::fromSerialIter(sit, 33);
BEAST_EXPECT(!result.has_value());
}
// Zero flags are a non-canonical duplicate of the 32-byte legacy form.
{
auto const txSet = makeHash("txset-zero-flags");
Serializer s;
s.addBitString(txSet);
s.add8(0);
SerialIter sit(s.slice());
auto result =
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
BEAST_EXPECT(!result.has_value());
}
// Trailing extra bytes after valid fields
{
auto const txSet = makeHash("txset-trailing");
auto const commitSet = makeHash("commitset-trailing");
Serializer s;
s.addBitString(txSet);
s.add8(0x01); // commitSetHash only
s.addBitString(commitSet);
s.add32(0xDEADBEEF); // 4 extra trailing bytes
SerialIter sit(s.slice());
auto result =
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
BEAST_EXPECT(!result.has_value());
}
// Valid flags with exactly the right amount of data (should succeed)
{
auto const txSet = makeHash("txset-ok");
auto const commitSet = makeHash("commitset-ok");
Serializer s;
s.addBitString(txSet);
s.add8(0x01); // commitSetHash only
s.addBitString(commitSet);
SerialIter sit(s.slice());
auto result =
ExtendedPosition::fromSerialIter(sit, s.getDataLength());
BEAST_EXPECT(result.has_value());
if (result)
{
BEAST_EXPECT(result->txSetHash == txSet);
BEAST_EXPECT(result->commitSetHash == commitSet);
BEAST_EXPECT(!result->entropySetHash);
}
}
//@@end test-extended-position-malformed-canonicality
}
void
testTxSetIdentity()
{
testcase("Tx-set identity is explicit");
auto const txSet = makeHash("txset-eq");
auto const txSet2 = makeHash("txset-eq-2");
ExtendedPosition a{txSet};
a.myCommitment = makeHash("commit1-eq");
ExtendedPosition b{txSet};
b.myCommitment = makeHash("commit2-eq");
// Same txSetHash, different leaves -> same consensus tx-set key.
BEAST_EXPECT(positionTxSetID(a) == positionTxSetID(b));
// Same txSetHash, different commitSetHash -> same tx-set key
// (sub-state quorum handles commitSetHash agreement)
b.commitSetHash = makeHash("cs-eq");
BEAST_EXPECT(positionTxSetID(a) == positionTxSetID(b));
// Same txSetHash, different entropySetHash -> same tx-set key
b.entropySetHash = makeHash("es-eq");
BEAST_EXPECT(positionTxSetID(a) == positionTxSetID(b));
// Same txSetHash, different export signature digest -> same tx-set key
b.exportSignaturesHash = makeHash("export-sigs-eq");
BEAST_EXPECT(positionTxSetID(a) == positionTxSetID(b));
// Same txSetHash, different participant diagnostics -> same tx-set key
b.observedParticipantsHash = makeHash("participants-eq");
BEAST_EXPECT(positionTxSetID(a) == positionTxSetID(b));
// Different txSetHash -> different tx-set key
ExtendedPosition c{txSet2};
BEAST_EXPECT(positionTxSetID(a) != positionTxSetID(c));
BEAST_EXPECT(positionTxSetID(a) == txSet);
BEAST_EXPECT(positionTxSetID(a) != txSet2);
a.updateTxSet(txSet2);
BEAST_EXPECT(positionTxSetID(a) == txSet2);
BEAST_EXPECT(positionTxSetID(a) != positionTxSetID(b));
}
void
testExportSignatureDigest()
{
testcase("Export signature digest");
//@@start test-extended-position-export-signature-digest
std::vector<std::string> blobs;
blobs.emplace_back("txhash-pubkey-sig-a");
blobs.emplace_back("txhash-pubkey-sig-b");
auto const digest = proposalExportSignaturesHash(blobs);
BEAST_EXPECT(digest == proposalExportSignaturesHash(blobs));
auto reordered = blobs;
std::swap(reordered[0], reordered[1]);
BEAST_EXPECT(digest != proposalExportSignaturesHash(reordered));
auto mutated = blobs;
mutated[1].push_back('x');
BEAST_EXPECT(digest != proposalExportSignaturesHash(mutated));
//@@end test-extended-position-export-signature-digest
}
void
testStringJsonAndHash()
{
testcase("String, JSON, and hash helpers");
auto const txSet = makeHash("txset-json");
auto const commitSet = makeHash("commitset-json");
auto const entropySet = makeHash("entropyset-json");
auto const exportSigSet = makeHash("exportsigset-json");
auto const exportSigs = makeHash("exportsigs-json");
auto const participants = makeHash("participants-json");
ExtendedPosition pos{txSet};
pos.commitSetHash = commitSet;
pos.entropySetHash = entropySet;
pos.exportSigSetHash = exportSigSet;
pos.exportSignaturesHash = exportSigs;
pos.observedParticipantsHash = participants;
BEAST_EXPECT(to_string(pos) == to_string(txSet));
std::ostringstream os;
os << pos;
BEAST_EXPECT(os.str() == to_string(txSet));
auto const json = pos.getJson();
BEAST_EXPECT(json["tx_set"].asString() == to_string(txSet));
BEAST_EXPECT(json["commit_set"].asString() == to_string(commitSet));
BEAST_EXPECT(json["entropy_set"].asString() == to_string(entropySet));
BEAST_EXPECT(
json["export_sig_set"].asString() == to_string(exportSigSet));
BEAST_EXPECT(
json["export_signatures"].asString() == to_string(exportSigs));
BEAST_EXPECT(
json["observed_participants"].asString() ==
to_string(participants));
auto const fullHash = sha512Half(pos);
auto withDifferentLeaf = pos;
withDifferentLeaf.myCommitment = makeHash("commitment-json");
BEAST_EXPECT(fullHash != sha512Half(withDifferentLeaf));
auto sameTxDifferentSidecar = pos;
sameTxDifferentSidecar.entropySetHash = makeHash("entropyset-other");
BEAST_EXPECT(
positionTxSetID(pos) == positionTxSetID(sameTxDifferentSidecar));
BEAST_EXPECT(sha512Half(pos) != sha512Half(sameTxDifferentSidecar));
}
public:
void
run() override
{
testSerializationRoundTrip();
testSigningConsistency();
testSuppressionConsistency();
testPeerPosition();
testMalformedPayload();
testTxSetIdentity();
testExportSignatureDigest();
testStringJsonAndHash();
}
};
BEAST_DEFINE_TESTSUITE(ExtendedPosition, consensus, ripple);
} // namespace test
} // namespace ripple
+157 -2
View File
@@ -27,6 +27,8 @@
#include <xrpl/basics/Log.h>
#include <xrpl/beast/unit_test.h>
#include <limits>
namespace ripple {
namespace test {
@@ -546,8 +548,12 @@ struct NetworkHistory
std::optional<int> numLedgers;
};
NetworkHistory(beast::unit_test::suite& suite, Parameter const& p)
: env(suite, jtx::supported_amendments() | featureNegativeUNL)
NetworkHistory(
beast::unit_test::suite& suite,
Parameter const& p,
FeatureBitset features = jtx::supported_amendments() |
featureNegativeUNL)
: env(suite, features | featureNegativeUNL)
, param(p)
, validations(env.app().getValidations())
{
@@ -1120,6 +1126,41 @@ class NegativeUNLVoteInternal_test : public beast::unit_test::suite
}
}
void
testFindAllCandidatesWithActiveViewCap()
{
testcase("Find All Candidates with active-view cap denominator");
jtx::Env env(*this);
NegativeUNLVote vote(NodeID(0xA0), env.journal, env.app());
std::vector<NodeID> nodeIds;
hash_set<NodeID> unl;
hash_set<NodeID> negUnl;
hash_map<NodeID, std::uint32_t> scoreTable;
for (std::size_t i = 0; i < 20; ++i)
{
nodeIds.emplace_back(i + 1);
unl.insert(nodeIds.back());
scoreTable[nodeIds.back()] =
NegativeUNLVote::negativeUNLLowWaterMark - 1;
}
for (std::size_t i = 0; i < 3; ++i)
negUnl.insert(nodeIds[i]);
auto const legacyCandidates =
vote.findAllCandidates(unl, negUnl, scoreTable);
BEAST_EXPECT(legacyCandidates.toDisableCandidates.size() == 17);
BEAST_EXPECT(legacyCandidates.toReEnableCandidates.empty());
auto const activeViewCandidates =
vote.findAllCandidates(unl, negUnl, scoreTable, 10);
BEAST_EXPECT(activeViewCandidates.toDisableCandidates.empty());
BEAST_EXPECT(activeViewCandidates.toReEnableCandidates.empty());
}
void
testFindAllCandidatesCombination()
{
@@ -1375,6 +1416,7 @@ class NegativeUNLVoteInternal_test : public beast::unit_test::suite
testPickOneCandidate();
testBuildScoreTableSpecialCases();
testFindAllCandidates();
testFindAllCandidatesWithActiveViewCap();
testFindAllCandidatesCombination();
testNewValidators();
}
@@ -1613,6 +1655,82 @@ class NegativeUNLVoteOffline_test : public beast::unit_test::suite
class NegativeUNLVoteMaxListed_test : public beast::unit_test::suite
{
void
seedUNLReport(NetworkHistory& history, std::size_t activeCount)
{
BEAST_EXPECT(activeCount <= history.UNLKeys.size());
auto& ledger = history.history.back();
OpenView accum(&*ledger);
PublicKey const importKey = history.UNLKeys.back();
for (std::size_t i = 0; i < activeCount; ++i)
{
auto tx = unl::createUNLReportTx(
ledger->seq(), importKey, history.UNLKeys[i]);
BEAST_EXPECT(unl::applyAndTestResult(history.env, accum, tx, true));
}
accum.apply(*ledger);
auto const sle = ledger->read(keylet::UNLReport());
BEAST_EXPECT(sle);
if (sle)
{
BEAST_EXPECT(sle->isFieldPresent(sfActiveValidators));
if (sle->isFieldPresent(sfActiveValidators))
BEAST_EXPECT(
sle->getFieldArray(sfActiveValidators).size() ==
activeCount);
}
}
std::size_t
voteCountWithUNLReport(FeatureBitset features, bool withReport = true)
{
NetworkHistory history = {*this, {20, 3, false, false, {}}, features};
BEAST_EXPECT(history.goodHistory);
if (!history.goodHistory)
return 0;
if (withReport)
seedUNLReport(history, 10);
history.walkHistoryAndAddValidations(
[&](std::shared_ptr<Ledger const> const&, std::size_t idx) -> bool {
return idx == history.UNLNodeIDs.size() - 1;
});
NegativeUNLVote vote(
history.UNLNodeIDs.back(), history.env.journal, history.env.app());
auto txSet = std::make_shared<SHAMap>(
SHAMapType::TRANSACTION, history.env.app().getNodeFamily());
vote.doVoting(
history.lastLedger(),
history.UNLKeySet,
history.validations,
txSet);
return unl::countTx(txSet);
}
void
testMaxListedPolicy()
{
testcase("Max listed policy");
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(0) == 0);
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(1) == 1);
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(4) == 1);
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(5) == 2);
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(8) == 2);
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(9) == 3);
BEAST_EXPECT(NegativeUNLVote::maxNegativeUNLListed(32) == 8);
auto const max = std::numeric_limits<std::size_t>::max();
BEAST_EXPECT(
NegativeUNLVote::maxNegativeUNLListed(max) ==
max / NegativeUNLVote::negativeUNLMaxListedDenominator + 1);
}
void
testDoVoting()
{
@@ -1637,10 +1755,47 @@ class NegativeUNLVoteMaxListed_test : public beast::unit_test::suite
}
}
void
testActiveViewCap()
{
auto const legacyFeatures = (jtx::supported_amendments() -
featureConsensusEntropy - featureExport) |
featureNegativeUNL;
for (bool const entropy : {false, true})
{
for (bool const exportEnabled : {false, true})
{
testcase(
std::string("Active-view cap: CE=") +
(entropy ? "on" : "off") +
", Export=" + (exportEnabled ? "on" : "off"));
auto features = legacyFeatures;
if (entropy)
features.set(featureConsensusEntropy);
if (exportEnabled)
features.set(featureExport);
// Three validators are already disabled. A 20-validator
// trusted UNL permits a fourth disable vote, whereas the
// parent report's 10 active validators cap the list at three.
BEAST_EXPECT(
voteCountWithUNLReport(features) ==
(entropy || exportEnabled ? 0 : 1));
// Without a parent report, all four feature combinations
// retain the trusted-UNL denominator.
BEAST_EXPECT(voteCountWithUNLReport(features, false) == 1);
}
}
}
void
run() override
{
testMaxListedPolicy();
testDoVoting();
testActiveViewCap();
}
};
@@ -0,0 +1,386 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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 AUTHORS 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/consensus/ProposalPrecheck.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/ExportShare.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/digest.h>
#include <cstring>
#include <string_view>
#include <vector>
namespace ripple {
namespace test {
namespace {
uint256
makeHash(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
void
setPreviousLedger(protocol::TMProposeSet& set)
{
auto const prev = makeHash("proposal-precheck-prev");
set.set_previousledger(prev.data(), prev.size());
}
void
setPosition(protocol::TMProposeSet& set, ExtendedPosition const& position)
{
Serializer s;
position.add(s);
set.set_currenttxhash(s.data(), s.size());
}
} // namespace
class ProposalPrecheck_test : public beast::unit_test::suite
{
public:
void
run() override
{
using enum detail::ProposalPrecheckResult;
//@@start test-peer-proposal-authentication
testcase("cluster transport does not bypass proposal authentication");
{
BEAST_EXPECT(detail::proposalSignatureAccepted(false, true));
BEAST_EXPECT(detail::proposalSignatureAccepted(true, true));
BEAST_EXPECT(!detail::proposalSignatureAccepted(false, false));
BEAST_EXPECT(!detail::proposalSignatureAccepted(true, false));
}
//@@end test-peer-proposal-authentication
//@@start test-proposal-precheck-legacy-ok
testcase("legacy and extended ok");
{
protocol::TMProposeSet set;
setPreviousLedger(set);
ExtendedPosition position{makeHash("legacy-position")};
setPosition(set, position);
auto const precheck =
detail::checkProposalExtensions(set, false, false);
BEAST_EXPECT(precheck.result == ok);
BEAST_EXPECT(precheck.position);
if (precheck.position)
BEAST_EXPECT(
precheck.position->txSetHash == position.txSetHash);
}
//@@end test-proposal-precheck-legacy-ok
testcase("feature predicates are lazy");
{
int entropyChecks = 0;
int exportChecks = 0;
auto const entropyEnabled = [&] {
++entropyChecks;
return false;
};
auto const exportEnabled = [&] {
++exportChecks;
return false;
};
protocol::TMProposeSet plainSet;
setPreviousLedger(plainSet);
ExtendedPosition plain{makeHash("plain-lazy-position")};
setPosition(plainSet, plain);
BEAST_EXPECT(
detail::checkProposalExtensions(
plainSet, entropyEnabled, exportEnabled)
.result == ok);
BEAST_EXPECT(entropyChecks == 0);
BEAST_EXPECT(exportChecks == 0);
protocol::TMProposeSet entropySet;
setPreviousLedger(entropySet);
ExtendedPosition entropy{makeHash("entropy-lazy-position")};
entropy.myCommitment = makeHash("lazy-commitment");
setPosition(entropySet, entropy);
BEAST_EXPECT(
detail::checkProposalExtensions(
entropySet, entropyEnabled, exportEnabled)
.result == entropyDisabled);
BEAST_EXPECT(entropyChecks == 1);
BEAST_EXPECT(exportChecks == 0);
protocol::TMProposeSet exportSet;
setPreviousLedger(exportSet);
ExtendedPosition exportPos{makeHash("export-lazy-position")};
exportPos.exportSigSetHash = makeHash("lazy-export-sidecar");
setPosition(exportSet, exportPos);
BEAST_EXPECT(
detail::checkProposalExtensions(
exportSet, entropyEnabled, exportEnabled)
.result == exportDisabled);
BEAST_EXPECT(entropyChecks == 1);
BEAST_EXPECT(exportChecks == 1);
}
testcase("malformed hashes and extended payload");
{
protocol::TMProposeSet set;
setPreviousLedger(set);
set.set_currenttxhash("short", 5);
BEAST_EXPECT(
detail::checkProposalExtensions(set, true, true).result ==
badHashes);
set.clear_currenttxhash();
std::string malformed(uint256::size(), '\0');
malformed.push_back(static_cast<char>(0x80));
set.set_currenttxhash(malformed.data(), malformed.size());
BEAST_EXPECT(
detail::checkProposalExtensions(set, true, true).result ==
badPosition);
protocol::TMProposeSet badPrev;
ExtendedPosition position{makeHash("bad-prev-position")};
setPosition(badPrev, position);
badPrev.set_previousledger("short", 5);
BEAST_EXPECT(
detail::checkProposalExtensions(badPrev, true, true).result ==
badHashes);
}
//@@start test-proposal-extension-feature-gating
testcase("feature gating");
{
protocol::TMProposeSet entropySet;
setPreviousLedger(entropySet);
ExtendedPosition entropy{makeHash("entropy-position")};
entropy.myCommitment = makeHash("commitment");
setPosition(entropySet, entropy);
BEAST_EXPECT(
detail::checkProposalExtensions(entropySet, false, true)
.result == entropyDisabled);
protocol::TMProposeSet observedSet;
setPreviousLedger(observedSet);
ExtendedPosition observed{makeHash("observed-position")};
observed.observedParticipantsHash =
makeHash("observed-participants");
setPosition(observedSet, observed);
BEAST_EXPECT(
detail::checkProposalExtensions(observedSet, false, true)
.result == ok);
BEAST_EXPECT(
detail::checkProposalExtensions(observedSet, true, false)
.result == ok);
BEAST_EXPECT(
detail::checkProposalExtensions(observedSet, false, false)
.result == extensionDiagnosticsDisabled);
protocol::TMProposeSet exportSet;
setPreviousLedger(exportSet);
ExtendedPosition exportPos{makeHash("export-position")};
exportPos.exportSigSetHash = makeHash("export-sidecar");
setPosition(exportSet, exportPos);
BEAST_EXPECT(
detail::checkProposalExtensions(exportSet, true, false)
.result == exportDisabled);
}
//@@end test-proposal-extension-feature-gating
//@@start test-proposal-export-signature-binding
testcase("export signature binding");
{
protocol::TMProposeSet tooMany;
setPreviousLedger(tooMany);
ExtendedPosition position{makeHash("too-many-position")};
setPosition(tooMany, position);
for (std::size_t i = 0; i <= ExportLimits::maxExportSharesPerRelay;
++i)
tooMany.add_exportsignatures("sig");
BEAST_EXPECT(
detail::checkProposalExtensions(tooMany, true, true).result ==
tooManyExportSignatures);
protocol::TMProposeSet unsignedSigs;
setPreviousLedger(unsignedSigs);
setPosition(unsignedSigs, position);
unsignedSigs.add_exportsignatures("sig");
BEAST_EXPECT(
detail::checkProposalExtensions(unsignedSigs, true, true)
.result == unsignedExportSignatures);
protocol::TMProposeSet mismatch;
setPreviousLedger(mismatch);
ExtendedPosition mismatchPos{makeHash("mismatch-position")};
mismatchPos.exportSignaturesHash =
proposalExportSignaturesHash(std::vector<std::string>{"one"});
setPosition(mismatch, mismatchPos);
mismatch.add_exportsignatures("two");
BEAST_EXPECT(
detail::checkProposalExtensions(mismatch, true, true).result ==
exportSignaturesHashMismatch);
protocol::TMProposeSet missing;
setPreviousLedger(missing);
setPosition(missing, mismatchPos);
BEAST_EXPECT(
detail::checkProposalExtensions(missing, true, true).result ==
missingExportSignatures);
protocol::TMProposeSet okSet;
setPreviousLedger(okSet);
std::vector<std::string> const sigs{"signed-export"};
ExtendedPosition okPos{makeHash("export-ok-position")};
okPos.exportSignaturesHash = proposalExportSignaturesHash(sigs);
setPosition(okSet, okPos);
okSet.add_exportsignatures(sigs.front());
BEAST_EXPECT(
detail::checkProposalExtensions(okSet, true, true).result ==
ok);
// A single oversized blob is rejected before its bytes are hashed,
// even though the count is within the relay cap. This bounds the
// pre-auth SHA512 work on the proposal ingress path.
protocol::TMProposeSet oversized;
setPreviousLedger(oversized);
std::string const bigSig(
ExportLimits::maxSerializedExportShareBytes + 1, 'x');
std::vector<std::string> const bigSigs{bigSig};
ExtendedPosition oversizedPos{
makeHash("export-oversized-position")};
oversizedPos.exportSignaturesHash =
proposalExportSignaturesHash(bigSigs);
setPosition(oversized, oversizedPos);
oversized.add_exportsignatures(bigSig);
BEAST_EXPECT(
detail::checkProposalExtensions(oversized, true, true).result ==
oversizedExportSignature);
// A maximum-size blob is still accepted.
protocol::TMProposeSet maxSized;
setPreviousLedger(maxSized);
std::string const maxSig(
ExportLimits::maxSerializedExportShareBytes, 'x');
std::vector<std::string> const maxSigs{maxSig};
ExtendedPosition maxPos{makeHash("export-maxsize-position")};
maxPos.exportSignaturesHash = proposalExportSignaturesHash(maxSigs);
setPosition(maxSized, maxPos);
maxSized.add_exportsignatures(maxSig);
BEAST_EXPECT(
detail::checkProposalExtensions(maxSized, true, true).result ==
ok);
auto const [key, secret] = randomKeyPair(KeyType::ed25519);
auto const signature = sign(key, secret, Slice{"share", 5});
auto const share =
ExportShare{
ExportShare::currentVersion,
calcAccountID(key),
makeHash("export-origin"),
42,
makeHash("export-ledger"),
3,
key,
signature}
.serialize();
protocol::TMProposeSet encodedShare;
setPreviousLedger(encodedShare);
std::vector<std::string> const frames{std::string{
reinterpret_cast<char const*>(share.data()), share.size()}};
ExtendedPosition sharePos{makeHash("export-share-position")};
sharePos.exportSignaturesHash =
proposalExportSignaturesHash(frames);
setPosition(encodedShare, sharePos);
encodedShare.add_exportsignatures(frames.front());
BEAST_EXPECT(
detail::checkProposalExtensions(encodedShare, true, true)
.result == ok);
}
//@@end test-proposal-export-signature-binding
testcase("rejection diagnostics");
{
BEAST_EXPECT(!detail::proposalPrecheckRejection(ok));
auto const check = [&](detail::ProposalPrecheckResult result,
char const* logMessage,
char const* feeReason) {
auto const rejection =
detail::proposalPrecheckRejection(result);
BEAST_EXPECT(rejection);
if (rejection)
{
BEAST_EXPECT(
std::string_view{rejection->logMessage} == logMessage);
BEAST_EXPECT(
std::string_view{rejection->feeReason} == feeReason);
}
};
check(badHashes, "Proposal: malformed", "bad hashes");
check(
badPosition,
"Proposal: malformed extended position",
"bad proposal position");
check(
extensionDiagnosticsDisabled,
"Proposal: extension diagnostics while consensus extensions "
"disabled",
"extension diagnostics disabled");
check(
entropyDisabled,
"Proposal: entropy fields while featureConsensusEntropy "
"disabled",
"entropy fields disabled");
check(
exportDisabled,
"Proposal: export fields while featureExport disabled",
"export fields disabled");
check(
tooManyExportSignatures,
"Proposal: too many export signatures",
"too many export sigs");
check(
oversizedExportSignature,
"Proposal: oversized export signature",
"oversized export sig");
check(
unsignedExportSignatures,
"Proposal: unsigned export signatures",
"unsigned export sigs");
check(
exportSignaturesHashMismatch,
"Proposal: export signatures hash mismatch",
"export sig hash mismatch");
check(
missingExportSignatures,
"Proposal: missing signed export signatures",
"missing export sigs");
BEAST_EXPECT(!detail::proposalPrecheckRejection(
static_cast<detail::ProposalPrecheckResult>(255)));
}
}
};
BEAST_DEFINE_TESTSUITE(ProposalPrecheck, consensus, ripple);
} // namespace test
} // namespace ripple
+25
View File
@@ -21,6 +21,9 @@
#include <xrpld/core/JobQueue.h>
#include <xrpl/beast/unit_test.h>
#include <atomic>
#include <memory>
namespace ripple {
namespace test {
@@ -63,6 +66,27 @@ class JobQueue_test : public beast::unit_test::suite
}
}
void
testRejectMeasurementJobs()
{
testcase("measurement-only jobs are rejected without retaining work");
jtx::Env env{*this};
auto& queue = env.app().getJobQueue();
auto ran = std::make_shared<std::atomic<bool>>(false);
for (auto const type : {jtPEER, jtGENERIC})
{
BEAST_EXPECT(JobTypes::instance().get(type).special());
BEAST_EXPECT(!queue.addJob(
type, "RejectMeasurementJob", [ran] { *ran = true; }));
BEAST_EXPECT(queue.getJobCountTotal(type) == 0);
auto measurement = queue.makeLoadEvent(type, "MeasurementOnly");
BEAST_EXPECT(measurement != nullptr);
}
queue.rendezvous();
queue.stop();
BEAST_EXPECT(!*ran);
}
void
testPostCoro()
{
@@ -154,6 +178,7 @@ public:
run() override
{
testAddJob();
testRejectMeasurementJobs();
testPostCoro();
}
};
+1
View File
@@ -22,6 +22,7 @@
#include <test/csf/Histogram.h>
#include <test/csf/Peer.h>
#include <test/csf/PeerGroup.h>
#include <test/csf/PeerTick.h>
#include <test/csf/Proposal.h>
#include <test/csf/Scheduler.h>
#include <test/csf/Sim.h>
+1059 -14
View File
File diff suppressed because it is too large. Load diff
+14
View File
@@ -0,0 +1,14 @@
#ifndef RIPPLE_TEST_CSF_PEERTICK_H_INCLUDED
#define RIPPLE_TEST_CSF_PEERTICK_H_INCLUDED
#include <test/csf/Peer.h>
#include <xrpld/consensus/ConsensusExtensionsTick.h>
template <class Ctx>
ripple::ExtensionTickResult
ripple::test::csf::Peer::Extensions::onTick(Ctx const& ctx)
{
return ripple::extensionsTick(*this, ctx);
}
#endif
+71 -4
View File
@@ -23,17 +23,84 @@
#include <test/csf/Validation.h>
#include <test/csf/ledgers.h>
#include <xrpld/consensus/ConsensusProposal.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/hash/hash_append.h>
#include <cstdint>
#include <optional>
#include <ostream>
#include <string>
namespace ripple {
namespace test {
namespace csf {
/** Proposal is a position taken in the consensus process and is represented
directly from the generic types.
/** Position sidecar for CSF that can model RNG commit/reveal fields.
Core tx-set convergence remains keyed on txSetHash only, matching
production's ExtendedPosition behavior.
*/
using Proposal = ConsensusProposal<PeerID, Ledger::ID, TxSet::ID>;
struct RngPosition
{
TxSet::ID txSetHash{};
std::optional<uint256> commitSetHash;
std::optional<uint256> entropySetHash;
std::optional<uint256> exportSigSetHash;
std::optional<uint256> myCommitment;
std::optional<uint256> myReveal;
std::optional<Ledger::ID> exportSignatureOrigin;
std::optional<uint256> myExportSignature;
RngPosition() = default;
explicit RngPosition(TxSet::ID txSet) : txSetHash(txSet)
{
}
void
updateTxSet(TxSet::ID txSet)
{
txSetHash = txSet;
}
};
inline std::string
to_string(RngPosition const& pos)
{
return std::to_string(pos.txSetHash);
}
inline std::ostream&
operator<<(std::ostream& os, RngPosition const& pos)
{
return os << pos.txSetHash;
}
template <class Hasher>
void
hash_append(Hasher& h, RngPosition const& pos)
{
using beast::hash_append;
auto appendOpt = [&](auto const& o) {
hash_append(h, static_cast<std::uint8_t>(o.has_value() ? 1 : 0));
if (o)
hash_append(h, *o);
};
hash_append(h, pos.txSetHash);
appendOpt(pos.commitSetHash);
appendOpt(pos.entropySetHash);
appendOpt(pos.exportSigSetHash);
appendOpt(pos.myCommitment);
appendOpt(pos.myReveal);
appendOpt(pos.exportSignatureOrigin);
appendOpt(pos.myExportSignature);
}
/** Proposal is a position taken in the consensus process.
*/
using Proposal = ConsensusProposal<PeerID, Ledger::ID, RngPosition>;
using ProposalPosition = RngPosition;
} // namespace csf
} // namespace test
} // namespace ripple
#endif
#endif
+13 -1
View File
@@ -25,6 +25,7 @@
#include <test/csf/Digraph.h>
#include <test/csf/Peer.h>
#include <test/csf/PeerGroup.h>
#include <test/csf/PeerTick.h>
#include <test/csf/Scheduler.h>
#include <test/csf/SimTime.h>
#include <test/csf/TrustGraph.h>
@@ -41,6 +42,7 @@ namespace csf {
class BasicSink : public beast::Journal::Sink
{
Scheduler::clock_type const& clock_;
bool silent_ = false;
public:
BasicSink(Scheduler::clock_type const& clock)
@@ -48,11 +50,19 @@ public:
{
}
void
silent(bool value)
{
silent_ = value;
}
void
write(beast::severities::Severity level, std::string const& text) override
{
if (level < threshold())
return;
if (silent_)
return;
std::cout << clock_.now().time_since_epoch().count() << " " << text
<< std::endl;
@@ -83,6 +93,7 @@ public:
BasicNetwork<Peer*> net;
TrustGraph<Peer*> trustGraph;
CollectorRefs collectors;
SidecarStore sidecarStore;
/** Create a simulation
@@ -119,7 +130,8 @@ public:
net,
trustGraph,
collectors,
j);
j,
sidecarStore);
newPeers.emplace_back(&peers.back());
}
PeerGroup res{newPeers};
+1 -1
View File
@@ -48,7 +48,7 @@ public:
{
}
ID
ID const&
id() const
{
return id_;
+3 -1
View File
@@ -98,7 +98,8 @@ LedgerOracle::accept(
Ledger const& parent,
TxSetType const& txs,
NetClock::duration closeTimeResolution,
NetClock::time_point const& consensusCloseTime)
NetClock::time_point const& consensusCloseTime,
std::optional<uint256> const& consensusExtensionEffect)
{
using namespace std::chrono_literals;
Ledger::Instance next(*parent.instance_);
@@ -114,6 +115,7 @@ LedgerOracle::accept(
next.parentCloseTime = parent.closeTime();
next.parentID = parent.id();
next.consensusExtensionEffect = consensusExtensionEffect;
next.ancestors.push_back(parent.id());
auto it = instances_.left.find(next);
+15 -2
View File
@@ -22,6 +22,7 @@
#include <test/csf/Tx.h>
#include <xrpld/consensus/LedgerTiming.h>
#include <xrpl/basics/UnorderedContainers.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/chrono.h>
#include <xrpl/basics/comparators.h>
#include <xrpl/basics/tagged_integer.h>
@@ -100,6 +101,10 @@ private:
//! Parent ledger close time
NetClock::time_point parentCloseTime;
//! Synthetic accept-time consensus-extension effect. This models
//! pseudo-transaction bytes that are not part of the agreed TxSet.
std::optional<uint256> consensusExtensionEffect;
//! IDs of this ledgers ancestors. Since each ledger already has unique
//! ancestors based on the parentID, this member is not needed for any
//! of the operators below.
@@ -115,7 +120,8 @@ private:
closeTime,
closeTimeAgree,
parentID,
parentCloseTime);
parentCloseTime,
consensusExtensionEffect);
}
friend bool
@@ -211,6 +217,12 @@ public:
return instance_->txs;
}
std::optional<uint256> const&
consensusExtensionEffect() const
{
return instance_->consensusExtensionEffect;
}
/** Determine whether ancestor is really an ancestor of this ledger */
bool
isAncestor(Ledger const& ancestor) const;
@@ -275,7 +287,8 @@ public:
Ledger const& curr,
TxSetType const& txs,
NetClock::duration closeTimeResolution,
NetClock::time_point const& consensusCloseTime);
NetClock::time_point const& consensusCloseTime,
std::optional<uint256> const& consensusExtensionEffect = std::nullopt);
Ledger
accept(Ledger const& curr, Tx tx)
+12 -1
View File
@@ -82,7 +82,18 @@ supported_amendments()
Throw<std::runtime_error>(
"Unknown feature: " + s + " in supportedAmendments.");
}
return FeatureBitset(feats);
//@@start rng-test-environment-gating
// ConsensusEntropy is a supported amendment, but it is intentionally
// excluded from the default jtx fixture set.
//
// Enabling it changes every closed ledger by injecting a deterministic
// ttCONSENSUS_ENTROPY pseudo-transaction. Many legacy Env tests make
// exact transaction-count / metadata-shape assertions and are not
// trying to exercise ledger-wide entropy materialization. Keep those
// default fixtures stable; tests that cover entropy opt in explicitly
// with `supported_amendments() | featureConsensusEntropy`.
return FeatureBitset(feats) - featureConsensusEntropy;
//@@end rng-test-environment-gating
}();
return ids;
}
+1
View File
@@ -91,6 +91,7 @@ struct StubHookContext
uint16_t emit_nonce_counter{0};
uint16_t ledger_nonce_counter{0};
int64_t expected_etxn_count{-1};
int64_t expected_export_count{-1};
std::map<ripple::uint256, bool> nonce_used{};
uint32_t generation = 0;
uint64_t burden = 0;
+5 -3
View File
@@ -42,6 +42,7 @@
#include <xrpl/protocol/HashPrefix.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/SystemParameters.h>
#include <xrpl/protocol/TER.h>
@@ -130,7 +131,8 @@ Env::close(
// Go through the rpc interface unless we need to simulate
// a specific consensus delay.
if (consensusDelay)
app().getOPs().acceptLedger(consensusDelay);
app().getOPs().acceptLedger(
consensusDelay, "Env::close(consensusDelay)");
else
{
auto resp = rpc("ledger_accept");
@@ -510,7 +512,7 @@ Env::acct_autofill(JTx& jt, Account const& account)
jtx::fill_seq(jv, *current());
uint32_t networkID = app().config().NETWORK_ID;
if (!jv.isMember(jss::NetworkID) && networkID > 1024)
if (!jv.isMember(jss::NetworkID) && requiresTxNetworkID(networkID))
jv[jss::NetworkID] = std::to_string(networkID);
// Must come last
@@ -537,7 +539,7 @@ Env::autofill(JTx& jt)
if (jt.fill_netid)
{
uint32_t networkID = app().config().NETWORK_ID;
if (!jv.isMember(jss::NetworkID) && networkID > 1024)
if (!jv.isMember(jss::NetworkID) && requiresTxNetworkID(networkID))
jv[jss::NetworkID] = std::to_string(networkID);
}
+1
View File
@@ -111,6 +111,7 @@ makeStubHookContext(
.emit_nonce_counter = stubHookContext.emit_nonce_counter,
.ledger_nonce_counter = stubHookContext.ledger_nonce_counter,
.expected_etxn_count = stubHookContext.expected_etxn_count,
.expected_export_count = stubHookContext.expected_export_count,
.nonce_used = stubHookContext.nonce_used,
.generation = stubHookContext.generation,
.burden = stubHookContext.burden,
+212
View File
@@ -0,0 +1,212 @@
#ifndef RIPPLE_TEST_JTX_XPOP_H_INCLUDED
#define RIPPLE_TEST_JTX_XPOP_H_INCLUDED
#include <test/jtx/Env.h>
#include <xrpld/app/ledger/LedgerMaster.h>
#include <xrpld/app/proof/LedgerProof.h>
#include <xrpld/app/proof/XPOPv1.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/basics/base64.h>
#include <xrpl/protocol/PublicKey.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Sign.h>
#include <xrpl/protocol/digest.h>
namespace ripple {
namespace test {
namespace jtx {
namespace xpop {
/// Build a manifest string (binary, not base64).
inline std::string
makeManifestRaw(
PublicKey const& masterPub,
SecretKey const& masterSec,
PublicKey const& signingPub,
SecretKey const& signingSec,
int seq = 1)
{
STObject st(sfGeneric);
st[sfSequence] = seq;
st[sfPublicKey] = masterPub;
st[sfSigningPubKey] = signingPub;
sign(st, HashPrefix::manifest, *publicKeyType(signingPub), signingSec);
sign(
st,
HashPrefix::manifest,
*publicKeyType(masterPub),
masterSec,
sfMasterSignature);
Serializer s;
st.add(s);
return std::string(static_cast<char const*>(s.data()), s.size());
}
/// A complete test validator with all keys and manifest.
struct TestValidator
{
PublicKey masterPublic;
SecretKey masterSecret;
PublicKey signingPublic;
SecretKey signingSecret;
std::string manifestRaw;
std::string manifestBase64;
static TestValidator
create()
{
auto const ms = randomSecretKey();
auto const mp = derivePublicKey(KeyType::ed25519, ms);
auto const [sp, ss] = randomKeyPair(KeyType::secp256k1);
auto raw = makeManifestRaw(mp, ms, sp, ss, 1);
return {mp, ms, sp, ss, raw, base64_encode(raw)};
}
proof::ValidatorKeys
toValidatorKeys() const
{
return {
masterPublic,
masterSecret,
signingPublic,
signingSecret,
manifestBase64};
}
};
/// A complete test VL publisher with keys and manifest.
struct TestVLPublisher
{
PublicKey masterPublic;
SecretKey masterSecret;
PublicKey signingPublic;
SecretKey signingSecret;
std::string manifestBase64;
static TestVLPublisher
create()
{
auto const ms = randomSecretKey();
auto const mp = derivePublicKey(KeyType::ed25519, ms);
auto const [sp, ss] = randomKeyPair(KeyType::secp256k1);
return {
mp, ms, sp, ss, base64_encode(makeManifestRaw(mp, ms, sp, ss, 1))};
}
/// Build VL data for these validators.
proof::VLData
buildVLData(
std::vector<TestValidator> const& validators,
std::uint32_t sequence = 1,
std::uint32_t expiration = 767784645) const
{
// Build the JSON blob
std::string data = "{\"sequence\":" + std::to_string(sequence) +
",\"expiration\":" + std::to_string(expiration) +
",\"validators\":[";
for (std::size_t i = 0; i < validators.size(); ++i)
{
if (i > 0)
data += ",";
data += "{\"validation_public_key\":\"" +
strHex(validators[i].masterPublic) + "\",\"manifest\":\"" +
validators[i].manifestBase64 + "\"}";
}
data += "]}";
auto const blob = base64_encode(data);
auto const sig =
strHex(sign(signingPublic, signingSecret, makeSlice(data)));
return proof::VLData{
masterPublic, masterSecret, manifestBase64, blob, sig, 1};
}
};
/// Everything needed to build and import XPOPs in tests.
struct TestXPOPContext
{
std::vector<TestValidator> validators;
TestVLPublisher publisher;
proof::VLData vlData;
static TestXPOPContext
create(int validatorCount = 5)
{
auto pub = TestVLPublisher::create();
std::vector<TestValidator> vals;
for (int i = 0; i < validatorCount; ++i)
vals.push_back(TestValidator::create());
auto vl = pub.buildVLData(vals);
return {std::move(vals), std::move(pub), std::move(vl)};
}
/// Get the VL master public key hex for IMPORT_VL_KEYS config.
std::string
vlKeyHex() const
{
return strHex(publisher.masterPublic);
}
/// Build an Env config with NETWORK_ID and IMPORT_VL_KEYS set.
std::unique_ptr<Config>
makeEnvConfig(std::uint32_t networkID = 21337) const
{
auto cfg = envconfig(jtx::validator, "");
cfg->NETWORK_ID = networkID;
auto const keyHex = vlKeyHex();
auto const pkHex = strUnHex(keyHex);
if (pkHex)
cfg->IMPORT_VL_KEYS.emplace(keyHex, makeSlice(*pkHex));
return cfg;
}
/// Build XPOP from a closed ledger for a specific tx.
Json::Value
buildXPOP(Ledger const& ledger, uint256 const& txHash) const
{
std::vector<proof::ValidatorKeys> valKeys;
for (auto const& v : validators)
valKeys.push_back(v.toValidatorKeys());
return proof::buildXPOPv1(ledger, txHash, valKeys, vlData);
}
/// Build XPOP from an Env's last closed ledger.
Json::Value
buildXPOP(Env& env, uint256 const& txHash) const
{
auto const lcl = env.app().getLedgerMaster().getClosedLedger();
if (!lcl)
return {};
return buildXPOP(*lcl, txHash);
}
};
/// Build a complete XPOP v1 JSON from an Env's last closed ledger.
/// Creates fresh validator keys and VL publisher for each call.
inline Json::Value
buildTestXPOP(Env& env, uint256 const& txHash, int validatorCount = 5)
{
auto ctx = TestXPOPContext::create(validatorCount);
return ctx.buildXPOP(env, txHash);
}
/// Get the hex-encoded XPOP blob suitable for sfBlob in ttIMPORT.
inline std::string
buildTestXPOPHex(Env& env, uint256 const& txHash, int validatorCount = 5)
{
auto const xpop = buildTestXPOP(env, txHash, validatorCount);
if (xpop.isNull())
return {};
return proof::xpopToHex(xpop);
}
} // namespace xpop
} // namespace jtx
} // namespace test
} // namespace ripple
#endif
+97
View File
@@ -24,11 +24,14 @@
#include <xrpld/app/tx/detail/ApplyContext.h>
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/protocol/ExportCommittee.h>
#include <xrpl/protocol/InnerObjectFormats.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <boost/algorithm/string/predicate.hpp>
#include <algorithm>
namespace ripple {
class Invariants_test : public beast::unit_test::suite
@@ -1234,6 +1237,99 @@ class Invariants_test : public beast::unit_test::suite
{tecINVARIANT_FAILED, tecINVARIANT_FAILED});
}
void
testValidExportCommittee()
{
using namespace test::jtx;
testcase << "ValidExportCommittee";
// A zero digest is never a valid content-addressed committee.
doInvariantCheck(
{{"Invariant failed: malformed or mutated Export committee"}},
[](Account const& A1, Account const& A2, ApplyContext& ac) {
auto const roster = serializeExportCommittee({A2.pk()});
auto const digest = exportCommitteeHash(makeSlice(roster));
auto sle = std::make_shared<SLE>(
keylet::exportCommittee(A1.id(), digest));
sle->setAccountID(sfAccount, A1.id());
sle->setFieldH256(sfExportCommitteeHash, uint256{});
sle->setFieldVL(sfExportCommittee, roster);
sle->setFieldU64(sfOwnerNode, 0);
ac.view().insert(sle);
return true;
});
// The SLE key must be derived from its account and roster digest.
doInvariantCheck(
{{"Invariant failed: malformed or mutated Export committee"}},
[](Account const& A1, Account const& A2, ApplyContext& ac) {
auto const roster = serializeExportCommittee({A2.pk()});
auto const digest = exportCommitteeHash(makeSlice(roster));
auto const otherRoster = serializeExportCommittee({A1.pk()});
auto const otherDigest =
exportCommitteeHash(makeSlice(otherRoster));
auto sle = std::make_shared<SLE>(
keylet::exportCommittee(A1.id(), otherDigest));
sle->setAccountID(sfAccount, A1.id());
sle->setFieldH256(sfExportCommitteeHash, digest);
sle->setFieldVL(sfExportCommittee, roster);
sle->setFieldU64(sfOwnerNode, 0);
ac.view().insert(sle);
return true;
});
// Valid keys in noncanonical roster order must not gain a second
// content identity.
doInvariantCheck(
{{"Invariant failed: malformed or mutated Export committee"}},
[](Account const& A1, Account const& A2, ApplyContext& ac) {
auto roster = serializeExportCommittee({A1.pk(), A2.pk()});
std::rotate(
roster.begin(),
roster.begin() + A1.pk().size(),
roster.end());
auto const digest = exportCommitteeHash(makeSlice(roster));
auto sle = std::make_shared<SLE>(
keylet::exportCommittee(A1.id(), digest));
sle->setAccountID(sfAccount, A1.id());
sle->setFieldH256(sfExportCommitteeHash, digest);
sle->setFieldVL(sfExportCommittee, roster);
sle->setFieldU64(sfOwnerNode, 0);
ac.view().insert(sle);
return true;
});
// Once created, even a change that preserves the content-addressed
// fields is an invariant violation.
doInvariantCheck(
{{"Invariant failed: malformed or mutated Export committee"}},
[](Account const& A1, Account const& A2, ApplyContext& ac) {
auto const roster = serializeExportCommittee({A2.pk()});
auto const digest = exportCommitteeHash(makeSlice(roster));
auto sle =
ac.view().peek(keylet::exportCommittee(A1.id(), digest));
if (!sle)
return false;
sle->setFieldU64(
sfOwnerNode, sle->getFieldU64(sfOwnerNode) + 1);
ac.view().update(sle);
return true;
},
XRPAmount{},
STTx{ttACCOUNT_SET, [](STObject&) {}},
{tecINVARIANT_FAILED, tefINVARIANT_FAILED},
[](Account const& A1, Account const& A2, Env& env) {
Json::Value setup;
setup[jss::TransactionType] = jss::Export;
setup[jss::Account] = A1.human();
setup[sfExportCommittee.jsonName] =
strHex(serializeExportCommittee({A2.pk()}));
env(setup);
return true;
});
}
void
testLockedBalance()
{
@@ -1288,6 +1384,7 @@ public:
testValidNewAccountRoot();
testNFTokenPageInvariants();
testPermissionedDomainInvariants();
testValidExportCommittee();
testLockedBalance();
}
};
@@ -0,0 +1,294 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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 WHATSOEVER
RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx/Env.h>
#include <xrpld/overlay/Message.h>
#include <xrpld/overlay/detail/ExportShareJob.h>
#include <xrpld/overlay/detail/ProtocolMessage.h>
#include <xrpld/overlay/detail/TrafficCount.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/ExportShare.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Sign.h>
#include <boost/asio/buffer.hpp>
#include <boost/beast/core/multi_buffer.hpp>
#include <algorithm>
#include <atomic>
#include <condition_variable>
#include <future>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <type_traits>
namespace ripple {
namespace test {
class ExportShareTransport_test : public beast::unit_test::suite
{
static ExportShare
makeShare()
{
auto const [key, secret] = randomKeyPair(KeyType::secp256k1);
auto const signature = sign(key, secret, Slice{"export-share", 12});
return ExportShare{
ExportShare::currentVersion,
calcAccountID(key),
uint256{1},
4'200'000,
uint256{2},
17,
key,
signature};
}
struct Handler
{
bool seen = false;
int shares = 0;
bool
compressionEnabled() const
{
return false;
}
void
onMessageUnknown(std::uint16_t)
{
}
void
onMessageBegin(
std::uint16_t,
std::shared_ptr<::google::protobuf::Message> const&,
std::size_t,
std::size_t,
bool)
{
}
void
onMessageEnd(
std::uint16_t,
std::shared_ptr<::google::protobuf::Message> const&)
{
}
template <class Message>
void
onMessage(std::shared_ptr<Message> const& message)
{
if constexpr (std::is_same_v<Message, protocol::TMExportShares>)
{
seen = true;
shares = message->shares_size();
}
}
};
void
testBatchBounds()
{
testcase("ExportShare batch structural bounds");
auto const encoded = makeShare().serialize();
protocol::TMExportShares batch;
batch.add_shares(encoded.data(), encoded.size());
auto const parsed = detail::parseExportShareBatch(batch);
BEAST_EXPECT(parsed && parsed->size() == 1);
BEAST_EXPECT(
batch.ByteSizeLong() <=
ExportLimits::maxExportShareRelayMessageBytes);
protocol::TMExportShares maxBatch;
for (std::size_t i = 0; i < ExportLimits::maxExportSharesPerRelay; ++i)
maxBatch.add_shares(encoded.data(), encoded.size());
auto const payloadBytes =
encoded.size() * ExportLimits::maxExportSharesPerRelay;
BEAST_EXPECT(
payloadBytes <= ExportLimits::maxExportShareRelayPayloadBytes);
BEAST_EXPECT(maxBatch.ByteSizeLong() > payloadBytes);
BEAST_EXPECT(
maxBatch.ByteSizeLong() <=
ExportLimits::maxExportShareRelayMessageBytes);
BEAST_EXPECT(detail::parseExportShareBatch(maxBatch));
protocol::TMExportShares empty;
BEAST_EXPECT(!detail::parseExportShareBatch(empty));
protocol::TMExportShares tooMany;
for (std::size_t i = 0; i <= ExportLimits::maxExportSharesPerRelay; ++i)
tooMany.add_shares(encoded.data(), encoded.size());
BEAST_EXPECT(!detail::parseExportShareBatch(tooMany));
protocol::TMExportShares oversized;
oversized.add_shares(
std::string(ExportLimits::maxSerializedExportShareBytes + 1, 'x'));
BEAST_EXPECT(!detail::parseExportShareBatch(oversized));
protocol::TMExportShares malformed;
malformed.add_shares("not-an-export-share");
BEAST_EXPECT(!detail::parseExportShareBatch(malformed));
}
void
testReceiveJobDispatch()
{
testcase("ExportShare production posting boundary dispatches work");
auto const type = detail::exportShareJobType;
auto const& info = JobTypes::instance().get(type);
// Fail before posting an undrainable special-category job: otherwise
// the old bug would also hang the application's shutdown in this test.
if (!BEAST_EXPECT(!info.special() && info.limit() > 0))
return;
jtx::Env env{*this};
auto& queue = env.app().getJobQueue();
auto completed = std::make_shared<std::promise<std::thread::id>>();
auto result = completed->get_future();
auto payload = std::make_shared<ExportShare>(makeShare());
std::weak_ptr<ExportShare> retained = payload;
BEAST_EXPECT(detail::postExportShareJob(queue, [completed, payload] {
// Exercise captured payload access on an actual worker.
if (payload->validShape())
completed->set_value(std::this_thread::get_id());
}));
payload.reset();
using namespace std::chrono_literals;
if (BEAST_EXPECT(result.wait_for(5s) == std::future_status::ready))
BEAST_EXPECT(result.get() != std::this_thread::get_id());
queue.rendezvous();
BEAST_EXPECT(queue.getJobCountTotal(type) == 0);
BEAST_EXPECT(retained.expired());
queue.stop();
auto ranAfterStop = std::make_shared<std::atomic<bool>>(false);
BEAST_EXPECT(!detail::postExportShareJob(
queue, [ranAfterStop] { *ranAfterStop = true; }));
BEAST_EXPECT(!*ranAfterStop);
}
void
testReceiveConcurrency()
{
testcase("ExportShare workers limit concurrency and drain queued work");
auto const type = detail::exportShareJobType;
if (!BEAST_EXPECT(JobTypes::instance().get(type).limit() == 2))
return;
auto config = jtx::envconfig();
config->FORCE_MULTI_THREAD = true;
config->WORKERS = 4;
jtx::Env env{*this, std::move(config)};
auto& queue = env.app().getJobQueue();
struct State
{
std::mutex mutex;
std::condition_variable changed;
int started = 0;
int active = 0;
int peak = 0;
int finished = 0;
bool release = false;
};
auto state = std::make_shared<State>();
for (int i = 0; i < 3; ++i)
{
BEAST_EXPECT(detail::postExportShareJob(queue, [state] {
std::unique_lock lock(state->mutex);
++state->started;
state->peak = std::max(state->peak, ++state->active);
state->changed.notify_all();
state->changed.wait(lock, [&] { return state->release; });
--state->active;
++state->finished;
state->changed.notify_all();
}));
}
using namespace std::chrono_literals;
{
std::unique_lock lock(state->mutex);
BEAST_EXPECT(state->changed.wait_for(
lock, 5s, [&] { return state->started >= 2; }));
BEAST_EXPECT(state->started == 2);
BEAST_EXPECT(queue.getJobCountTotal(type) == 3);
BEAST_EXPECT(queue.getJobCount(type) == 1);
// Release workers even if an expectation above failed.
state->release = true;
state->changed.notify_all();
BEAST_EXPECT(state->changed.wait_for(
lock, 5s, [&] { return state->finished == 3; }));
BEAST_EXPECT(state->peak == 2);
}
queue.rendezvous();
BEAST_EXPECT(queue.getJobCountTotal(type) == 0);
}
void
testDispatchAndTraffic()
{
testcase("ExportShare protocol dispatch and traffic category");
auto const encoded = makeShare().serialize();
protocol::TMExportShares batch;
batch.add_shares(encoded.data(), encoded.size());
BEAST_EXPECT(protocol::mtEXPORT_SHARES == 65);
BEAST_EXPECT(
protocolMessageName(protocol::mtEXPORT_SHARES) == "export_shares");
BEAST_EXPECT(
TrafficCount::categorize(batch, protocol::mtEXPORT_SHARES, true) ==
TrafficCount::category::export_shares);
Message message{batch, protocol::mtEXPORT_SHARES};
auto const& wire = message.getBuffer(compression::Compressed::Off);
boost::beast::multi_buffer buffers;
buffers.commit(boost::asio::buffer_copy(
buffers.prepare(wire.size()), boost::asio::buffer(wire)));
Handler handler;
std::size_t hint = 0;
auto const [consumed, ec] =
invokeProtocolMessage(buffers.data(), handler, hint);
BEAST_EXPECT(!ec);
BEAST_EXPECT(consumed == wire.size());
BEAST_EXPECT(handler.seen);
BEAST_EXPECT(handler.shares == 1);
}
public:
void
run() override
{
testBatchBounds();
testDispatchAndTraffic();
testReceiveJobDispatch();
testReceiveConcurrency();
}
};
BEAST_DEFINE_TESTSUITE(ExportShareTransport, overlay, ripple);
} // namespace test
} // namespace ripple
+21 -1
View File
@@ -49,6 +49,26 @@ public:
BEAST_EXPECT(!isFeatureValue(headers, "feature4", "10"));
BEAST_EXPECT(isFeatureValue(headers, "feature4", "1"));
BEAST_EXPECT(!featureEnabled(headers, "v6"));
auto const capableRequest =
makeFeaturesRequestHeader(false, false, false, false);
headers.set("X-Protocol-Ctl", capableRequest);
BEAST_EXPECT(featureEnabled(headers, FEATURE_CONSENSUS_ENTROPY));
BEAST_EXPECT(featureEnabled(headers, FEATURE_EXPORT_SHARES));
http_request_type request;
boost::beast::http::fields response;
response.set(
"X-Protocol-Ctl",
makeFeaturesResponseHeader(request, false, false, false, false));
BEAST_EXPECT(!featureEnabled(response, FEATURE_CONSENSUS_ENTROPY));
BEAST_EXPECT(!featureEnabled(response, FEATURE_EXPORT_SHARES));
request.set("X-Protocol-Ctl", capableRequest);
response.set(
"X-Protocol-Ctl",
makeFeaturesResponseHeader(request, false, false, false, false));
BEAST_EXPECT(featureEnabled(response, FEATURE_CONSENSUS_ENTROPY));
BEAST_EXPECT(featureEnabled(response, FEATURE_EXPORT_SHARES));
}
void
@@ -61,4 +81,4 @@ public:
BEAST_DEFINE_TESTSUITE(handshake, ripple_data, ripple);
} // namespace test
} // namespace ripple
} // namespace ripple
+246 -1
View File
@@ -18,11 +18,16 @@
//==============================================================================
#include <test/jtx.h>
#include <test/jtx/Env.h>
#include <xrpld/app/misc/HashRouter.h>
#include <xrpld/app/misc/NetworkOPs.h>
#include <xrpld/overlay/detail/OverlayImpl.h>
#include <xrpld/overlay/detail/PeerImp.h>
#include <xrpld/peerfinder/detail/SlotImp.h>
#include <xrpl/basics/make_SSLContext.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/ExportShare.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Sign.h>
namespace ripple {
@@ -118,9 +123,18 @@ private:
}
~PeerTest() = default;
bool trustedCluster_ = false;
bool
cluster() const override
{
return trustedCluster_;
}
void
run() override
{
run_++;
}
void
send(std::shared_ptr<Message> const&) override
@@ -137,11 +151,13 @@ private:
{
queueTx_ = 0;
sendTx_ = 0;
run_ = 0;
sid_ = 0;
}
inline static std::size_t sid_ = 0;
inline static std::uint16_t queueTx_ = 0;
inline static std::uint16_t sendTx_ = 0;
inline static std::uint16_t run_ = 0;
};
std::uint16_t lid_{0};
@@ -240,12 +256,241 @@ private:
}
}
void
testUnsignedImportClusterProof()
{
testcase("cluster signature trust does not skip unsigned Import proof");
using namespace jtx;
Env env(*this);
Account const alice{"alice"};
env.fund(XRP(1000), alice);
env.close();
BEAST_EXPECT(!env.app().getValidationPublicKey());
env.app().getOPs().setMode(OperatingMode::FULL);
env.app().getOPs().clearNeedNetworkLedger();
std::vector<std::shared_ptr<PeerTest>> peers;
PeerTest::init();
PeerTest::sid_ = 1;
lid_ = 0;
rid_ = 1;
std::uint16_t disabled = 1;
addPeer(env, peers, disabled);
peers.front()->trustedCluster_ = true;
auto const deliver = [&](std::shared_ptr<STTx const> const& tx) {
auto packet = std::make_shared<protocol::TMTransaction>();
auto const bytes = tx->getSerializer();
packet->set_rawtransaction(bytes.data(), bytes.size());
packet->set_status(protocol::tsNEW);
packet->set_deferred(false);
peers.front()->onMessage(packet);
env.app().getJobQueue().rendezvous();
};
auto const sequence = env.seq(alice);
auto const balance = env.balance(alice);
Json::Value malformed;
malformed[jss::TransactionType] = jss::Import;
malformed[jss::Account] = alice.human();
malformed[jss::SigningPubKey] = "";
malformed[jss::Blob] = "00";
auto const candidate = env.jt(malformed, sig(none), fee(100));
deliver(candidate.stx);
BEAST_EXPECT(
env.app().getHashRouter().getFlags(
candidate.stx->getTransactionID()) &
SF_BAD);
BEAST_EXPECT(env.seq(alice) == sequence);
BEAST_EXPECT(env.balance(alice) == balance);
// Ordinary signed cluster traffic still follows the normal path.
auto const ordinary = env.jt(noop(alice), fee(100));
deliver(ordinary.stx);
BEAST_EXPECT(env.seq(alice) == sequence + 1);
}
void
testProtocolFeatureGate()
{
testcase("required protocol feature gate");
jtx::Env env(*this);
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
std::vector<std::shared_ptr<PeerTest>> peers;
PeerTest::init();
lid_ = 0;
rid_ = 1;
boost::beast::http::fields legacy;
boost::beast::http::fields capable;
capable.set(
"X-Protocol-Ctl",
makeFeaturesRequestHeader(false, false, false, false));
BEAST_EXPECT(!overlay.isProtocolFeatureRequired(
ProtocolFeature::ConsensusEntropy));
BEAST_EXPECT(!overlay.missingRequiredProtocolFeatureInHandshake(
legacy, make_protocol(2, 2)));
// Leave a legacy peer in the active map while eviction is posted to its
// strand. Proposal filtering must take effect synchronously with the
// atomic requirement, without waiting for close().
std::uint16_t disabled = 1;
addPeer(env, peers, disabled);
BEAST_EXPECT(PeerTest::run_ == 1);
overlay.requireProtocolFeature(ProtocolFeature::ConsensusEntropy);
BEAST_EXPECT(overlay.isProtocolFeatureRequired(
ProtocolFeature::ConsensusEntropy));
BEAST_EXPECT(
overlay.missingRequiredProtocolFeatureInHandshake(
legacy, make_protocol(2, 2)) ==
ProtocolFeature::ConsensusEntropy);
BEAST_EXPECT(!overlay.missingRequiredProtocolFeatureInHandshake(
capable, make_protocol(2, 2)));
protocol::TMProposeSet proposal;
proposal.set_proposeseq(1);
proposal.set_currenttxhash(std::string(32, '\0'));
proposal.set_nodepubkey(std::string(33, '\0'));
proposal.set_closetime(0);
proposal.set_signature(std::string(64, '\0'));
proposal.set_previousledger(std::string(32, '\0'));
BEAST_EXPECT(proposal.IsInitialized());
overlay.broadcast(proposal);
BEAST_EXPECT(PeerTest::sendTx_ == 0);
// A capable peer admitted after the cutoff still runs and receives
// proposals; a later legacy peer is inserted only for lifecycle cleanup
// and is failed before run().
disabled = 0;
addPeer(env, peers, disabled);
BEAST_EXPECT(PeerTest::run_ == 2);
overlay.broadcast(proposal);
BEAST_EXPECT(PeerTest::sendTx_ == 1);
disabled = 1;
addPeer(env, peers, disabled);
BEAST_EXPECT(PeerTest::run_ == 2);
// Requiring an already-required feature is intentionally idempotent.
overlay.requireProtocolFeature(ProtocolFeature::ConsensusEntropy);
BEAST_EXPECT(overlay.isProtocolFeatureRequired(
ProtocolFeature::ConsensusEntropy));
}
void
testProtocolFeatureGateFromLedger()
{
testcase("active ledger installs protocol feature gate");
auto config = jtx::envconfig();
config->features.insert(featureConsensusEntropy);
config->features.insert(featureExport);
jtx::Env env(*this, std::move(config));
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
BEAST_EXPECT(overlay.isProtocolFeatureRequired(
ProtocolFeature::ConsensusEntropy));
BEAST_EXPECT(
overlay.isProtocolFeatureRequired(ProtocolFeature::ExportShares));
}
void
testGenericProtocolFeatureAdmission()
{
testcase("generic protocol feature admission");
jtx::Env env(*this);
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
auto const capableCtl =
makeFeaturesRequestHeader(false, false, false, false);
boost::beast::http::fields empty;
boost::beast::http::fields capable;
capable.set("X-Protocol-Ctl", capableCtl);
overlay.requireProtocolFeature(ProtocolFeature::ExportShares);
BEAST_EXPECT(
overlay.isProtocolFeatureRequired(ProtocolFeature::ExportShares));
BEAST_EXPECT(
overlay.missingRequiredProtocolFeatureInHandshake(
empty, make_protocol(2, 2)) == ProtocolFeature::ExportShares);
BEAST_EXPECT(!overlay.missingRequiredProtocolFeatureInHandshake(
capable, make_protocol(2, 2)));
overlay.requireProtocolFeature(
ProtocolFeature::ValidatorList2Propagation);
BEAST_EXPECT(
overlay.missingRequiredProtocolFeatureInHandshake(
empty, make_protocol(2, 1)) ==
ProtocolFeature::ValidatorList2Propagation);
BEAST_EXPECT(!overlay.missingRequiredProtocolFeatureInHandshake(
capable, make_protocol(2, 2)));
overlay.requireProtocolFeature(ProtocolFeature::LedgerReplay);
BEAST_EXPECT(
overlay.missingRequiredProtocolFeatureInHandshake(
capable, make_protocol(2, 2)) == ProtocolFeature::LedgerReplay);
boost::beast::http::fields withReplay;
withReplay.set(
"X-Protocol-Ctl", capableCtl + std::string("ledgerreplay=1;"));
BEAST_EXPECT(!overlay.missingRequiredProtocolFeatureInHandshake(
withReplay, make_protocol(2, 2)));
}
static protocol::TMExportShares
makeExportShareBatch(uint256 const& sidecarHash)
{
auto const [key, secret] = randomKeyPair(KeyType::secp256k1);
auto const signature = sign(key, secret, Slice{"export-share", 12});
ExportShare const share{
ExportShare::currentVersion,
calcAccountID(key),
sidecarHash,
4'200'000,
uint256{2},
17,
key,
signature};
auto const encoded = share.serialize();
protocol::TMExportShares batch;
batch.add_shares(encoded.data(), encoded.size());
return batch;
}
void
testExportSharesRelayCutoff()
{
testcase("export share relay respects protocol feature gate");
jtx::Env env(*this);
auto& overlay = dynamic_cast<OverlayImpl&>(env.app().overlay());
std::vector<std::shared_ptr<PeerTest>> peers;
PeerTest::init();
lid_ = 0;
rid_ = 1;
std::uint16_t disabled = 1;
addPeer(env, peers, disabled);
overlay.requireProtocolFeature(ProtocolFeature::ExportShares);
auto legacyBatch = makeExportShareBatch(uint256{1});
overlay.relay(legacyBatch);
BEAST_EXPECT(PeerTest::sendTx_ == 0);
disabled = 0;
addPeer(env, peers, disabled);
auto capableBatch = makeExportShareBatch(uint256{3});
overlay.relay(capableBatch);
BEAST_EXPECT(PeerTest::sendTx_ == 1);
}
void
run() override
{
bool log = false;
std::set<Peer::id_t> skip = {0, 1, 2, 3, 4};
testConfig(log);
testUnsignedImportClusterProof();
testProtocolFeatureGate();
testProtocolFeatureGateFromLedger();
testGenericProtocolFeatureAdmission();
testExportSharesRelayCutoff();
// relay to all peers, no hash queue
testRelay("feature disabled", false, 10, 0, 10, 25, 10, 0);
// relay to nPeers - skip (10-5=5)
@@ -283,4 +528,4 @@ private:
BEAST_DEFINE_TESTSUITE(tx_reduce_relay, ripple_data, ripple);
} // namespace test
} // namespace ripple
} // namespace ripple
+270
View File
@@ -0,0 +1,270 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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/StringUtilities.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/ExportCommittee.h>
#include <xrpl/protocol/ExportLimits.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/STLedgerEntry.h>
#include <xrpl/protocol/STTx.h>
#include <xrpl/protocol/SecretKey.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/digest.h>
#include <algorithm>
#include <array>
#include <cstring>
#include <vector>
namespace ripple {
namespace test {
namespace {
uint256
makeOrigin(char const* label)
{
return sha512Half(Slice(label, std::strlen(label)));
}
} // namespace
class ExportKeylet_test : public beast::unit_test::suite
{
public:
void
testOriginIdentity()
{
testcase("origin transaction identity");
AccountID const fixtureAccount{1};
uint256 const fixtureOrigin{2};
uint256 const expected{
"04FEBC83B833D1A2EBFD1605BA3A0574F"
"F0B0B8C9690ADBE35CD2F258984EB56"};
BEAST_EXPECT(
keylet::exportLatch(fixtureAccount, fixtureOrigin).key == expected);
auto const firstAccount =
calcAccountID(randomKeyPair(KeyType::secp256k1).first);
auto const secondAccount =
calcAccountID(randomKeyPair(KeyType::secp256k1).first);
auto const firstOrigin = makeOrigin("first-export-origin");
auto const secondOrigin = makeOrigin("second-export-origin");
auto const key = keylet::exportLatch(firstAccount, firstOrigin);
BEAST_EXPECT(key.type == ltEXPORT_LATCH);
BEAST_EXPECT(
key.key == keylet::exportLatch(firstAccount, firstOrigin).key);
BEAST_EXPECT(
key.key != keylet::exportLatch(firstAccount, secondOrigin).key);
BEAST_EXPECT(
key.key != keylet::exportLatch(secondAccount, firstOrigin).key);
}
void
testPendingDirectory()
{
testcase("pending Export directory");
uint256 const expected{
"70D4A5FF7B4087C41414E71BD1FF2ABE"
"5F49C380C158ACDCC76254279B176018"};
auto const& pending = keylet::pendingExports();
BEAST_EXPECT(pending.type == ltDIR_NODE);
BEAST_EXPECT(pending.key == expected);
BEAST_EXPECT(&pending == &keylet::pendingExports());
}
void
testStructuralLimits()
{
testcase("structural Export limits");
BEAST_EXPECT(ExportLimits::maxCommitteeMembers == 32);
BEAST_EXPECT(ExportLimits::maxCommitteeRosterBytes == 1056);
BEAST_EXPECT(ExportLimits::maxCommitteeContributorBytes == 4);
BEAST_EXPECT(
ExportLimits::maxCommitteeMembers == STTx::maxMultiSigners());
std::array<std::size_t, 11> constexpr expected{
0, 1, 2, 3, 4, 4, 5, 6, 7, 8, 8};
for (std::size_t members = 0; members < expected.size(); ++members)
BEAST_EXPECT(
ExportLimits::committeeQuorumThreshold(members) ==
expected[members]);
BEAST_EXPECT(ExportLimits::committeeQuorumThreshold(28) == 23);
BEAST_EXPECT(ExportLimits::committeeQuorumThreshold(32) == 26);
}
void
testCommitteeProfile()
{
testcase("Export committee profile");
std::vector<PublicKey> masters;
masters.reserve(28);
for (std::size_t i = 0; i < 28; ++i)
masters.push_back(randomKeyPair(KeyType::secp256k1).first);
auto const roster = serializeExportCommittee(masters);
auto profile = resolveExportCommittee(makeSlice(roster));
BEAST_EXPECT(profile.has_value());
if (profile)
{
BEAST_EXPECT(profile->members.size() == 28);
BEAST_EXPECT(profile->quorum == 23);
BEAST_EXPECT(profile->position(profile->members.front()) == 0);
BEAST_EXPECT(profile->position(profile->members.back()) == 27);
}
BEAST_EXPECT(!exportCommitteeHash(makeSlice(roster)).isZero());
Blob unordered;
for (auto it = masters.rbegin(); it != masters.rend(); ++it)
unordered.insert(unordered.end(), it->begin(), it->end());
BEAST_EXPECT(!resolveExportCommittee(makeSlice(unordered)));
auto const canonical =
canonicalizeExportCommittee(makeSlice(unordered));
BEAST_EXPECT(canonical.has_value());
if (canonical)
{
BEAST_EXPECT(*canonical == roster);
auto const unorderedProfile =
resolveExportCommittee(makeSlice(*canonical));
BEAST_EXPECT(unorderedProfile.has_value());
if (unorderedProfile)
BEAST_EXPECT(unorderedProfile->members == profile->members);
BEAST_EXPECT(
exportCommitteeHash(makeSlice(*canonical)) ==
exportCommitteeHash(makeSlice(roster)));
}
Serializer expectedDigest;
expectedDigest.add32(HashPrefix::exportCommittee);
expectedDigest.add32(static_cast<std::uint32_t>(masters.size()));
expectedDigest.addRaw(makeSlice(roster));
BEAST_EXPECT(
exportCommitteeHash(makeSlice(roster)) ==
expectedDigest.getSHA512Half());
BEAST_EXPECT(!resolveExportCommittee(Slice{}));
Blob malformed(34, 0);
BEAST_EXPECT(!resolveExportCommittee(makeSlice(malformed)));
Blob duplicate;
duplicate.insert(
duplicate.end(), masters.front().begin(), masters.front().end());
duplicate.insert(
duplicate.end(), masters.front().begin(), masters.front().end());
BEAST_EXPECT(!resolveExportCommittee(makeSlice(duplicate)));
std::vector<PublicKey> tooMany;
for (std::size_t i = 0; i <= ExportLimits::maxCommitteeMembers; ++i)
tooMany.push_back(randomKeyPair(KeyType::secp256k1).first);
BEAST_EXPECT(serializeExportCommittee(std::move(tooMany)).empty());
}
void
testExportLatchFormat()
{
testcase("Export latch format");
BEAST_EXPECT(sfExportCount.fieldCode == field_code(STI_UINT16, 101));
BEAST_EXPECT(sfExportNode.fieldCode == field_code(STI_UINT64, 29));
BEAST_EXPECT(
sfExportCommitteeHash.fieldCode == field_code(STI_UINT256, 39));
BEAST_EXPECT(sfExportCommittee.fieldCode == field_code(STI_VL, 34));
AccountID const account{1};
uint256 const origin{2};
uint256 const intentDigest{3};
uint256 const committeeDigest{4};
uint256 const witnessHash{5};
SLE latch{keylet::exportLatch(account, origin)};
latch.setAccountID(sfAccount, account);
latch.setFieldU32(sfTicketSequence, 55'001);
latch.setFieldH256(sfTransactionHash, origin);
latch.setFieldH256(sfDigest, intentDigest);
latch.setFieldU32(sfLedgerSequence, 4'123'200);
latch.setFieldH256(sfExportCommitteeHash, committeeDigest);
latch.setFieldU32(sfFlags, 1);
latch.setFieldH256(sfExportSignatureHash, witnessHash);
latch.setFieldU64(sfOwnerNode, 7);
latch.setFieldU64(sfExportNode, 8);
auto const serialized = latch.getSerializer();
SerialIter sit{serialized.slice()};
SLE const parsed{sit, latch.key()};
BEAST_EXPECT(parsed.getType() == ltEXPORT_LATCH);
BEAST_EXPECT(parsed.getAccountID(sfAccount) == account);
BEAST_EXPECT(parsed.getFieldU32(sfTicketSequence) == 55'001);
BEAST_EXPECT(parsed.getFieldH256(sfTransactionHash) == origin);
BEAST_EXPECT(parsed.getFieldH256(sfDigest) == intentDigest);
BEAST_EXPECT(parsed.getFieldU32(sfLedgerSequence) == 4'123'200);
BEAST_EXPECT(
parsed.getFieldH256(sfExportCommitteeHash) == committeeDigest);
BEAST_EXPECT(parsed.getFieldU32(sfFlags) == 1);
BEAST_EXPECT(parsed.getFieldH256(sfExportSignatureHash) == witnessHash);
BEAST_EXPECT(parsed.getFieldU64(sfOwnerNode) == 7);
BEAST_EXPECT(parsed.getFieldU64(sfExportNode) == 8);
SLE accountRoot{keylet::account(account)};
accountRoot.setFieldU16(sfExportCount, 9);
BEAST_EXPECT(accountRoot.getFieldU16(sfExportCount) == 9);
SLE pendingRoot{keylet::pendingExports()};
pendingRoot.setFieldU16(sfExportCount, 10);
BEAST_EXPECT(pendingRoot.getFieldU16(sfExportCount) == 10);
auto const master = randomKeyPair(KeyType::secp256k1).first;
auto const roster = serializeExportCommittee({master});
auto const digest = exportCommitteeHash(makeSlice(roster));
auto const committeeKey = keylet::exportCommittee(account, digest);
BEAST_EXPECT(committeeKey.type == ltEXPORT_COMMITTEE);
SLE committee{committeeKey};
committee.setAccountID(sfAccount, account);
committee.setFieldH256(sfExportCommitteeHash, digest);
committee.setFieldVL(sfExportCommittee, roster);
committee.setFieldU64(sfOwnerNode, 11);
auto const committeeBytes = committee.getSerializer();
SerialIter committeeIter{committeeBytes.slice()};
SLE const parsedCommittee{committeeIter, committeeKey.key};
BEAST_EXPECT(parsedCommittee.getType() == ltEXPORT_COMMITTEE);
BEAST_EXPECT(parsedCommittee.getFieldVL(sfExportCommittee) == roster);
}
void
run() override
{
testOriginIdentity();
testPendingDirectory();
testStructuralLimits();
testCommitteeProfile();
testExportLatchFormat();
}
};
BEAST_DEFINE_TESTSUITE(ExportKeylet, protocol, ripple);
} // namespace test
} // namespace ripple
+386
View File
@@ -0,0 +1,386 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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/StringUtilities.h>
#include <xrpl/basics/strHex.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/ExportOriginMemo.h>
#include <xrpl/protocol/STAmount.h>
#include <xrpl/protocol/STArray.h>
#include <xrpl/protocol/Serializer.h>
#include <xrpl/protocol/TxFlags.h>
namespace ripple::test {
namespace {
STTx
makePayment(Blob userMemo = {})
{
STObject object{sfGeneric};
object.setFieldU16(sfTransactionType, ttPAYMENT);
object.setFieldU32(sfFlags, tfFullyCanonicalSig);
object.setFieldU32(sfSequence, 0);
object.setFieldU32(sfTicketSequence, 1);
object.setFieldAmount(sfAmount, XRPAmount{1});
object.setFieldAmount(sfFee, XRPAmount{10});
object.setFieldVL(sfSigningPubKey, Blob{});
object.setAccountID(sfAccount, AccountID{1});
object.setAccountID(sfDestination, AccountID{2});
if (!userMemo.empty())
{
STArray memos{sfMemos};
STObject memo{sfMemo};
memo.setFieldVL(sfMemoType, Blob{'u', 's', 'e', 'r'});
memo.setFieldVL(sfMemoData, userMemo);
memos.emplace_back(std::move(memo));
object.setFieldArray(sfMemos, memos);
}
return STTx{std::move(object)};
}
STTx
replaceReservedData(STTx const& tx, Blob const& data)
{
Serializer serializer;
tx.add(serializer);
SerialIter sit{serializer.slice()};
STObject object{sfGeneric};
object.set(sit);
object.peekFieldArray(sfMemos).back().setFieldVL(sfMemoData, data);
return STTx{std::move(object)};
}
Blob
reservedData(STTx const& tx)
{
return tx.getFieldArray(sfMemos).back().getFieldVL(sfMemoData);
}
STObject
cloneObject(STTx const& tx)
{
Serializer serializer;
tx.add(serializer);
SerialIter sit{serializer.slice()};
STObject object{sfGeneric};
object.set(sit);
return object;
}
STTx
reparse(STTx const& tx)
{
return STTx{cloneObject(tx)};
}
std::size_t
memoBytes(STArray const& memos)
{
Serializer serializer;
memos.add(serializer);
return serializer.getDataLength();
}
Blob
serializeMemos(STArray const& memos)
{
Serializer serializer;
memos.add(serializer);
return serializer.getData();
}
std::size_t
memoBytes(STTx const& tx)
{
return memoBytes(tx.getFieldArray(sfMemos));
}
} // namespace
class ExportOriginMemo_test : public beast::unit_test::suite
{
ExportOriginMemo::Origin const origin_{21337, 0, uint256{2}};
ExportOriginMemo::Anchor const anchor_{4123200, uint256{3}};
public:
void
testExactEncodingAndProjection()
{
testcase("exact encoding and projection");
auto const identity =
ExportOriginMemo::identityForm(makePayment(), origin_);
auto const release =
ExportOriginMemo::releaseForm(makePayment(), origin_, anchor_);
BEAST_EXPECT(identity.has_value());
BEAST_EXPECT(release.has_value());
if (!identity || !release)
return;
auto const expectedIdentity = strUnHex(
"0100000053590000000000000000000000000000000000000000000000000000"
"00000000000000000002");
auto const expectedRelease = strUnHex(
"0100000053590000000000000000000000000000000000000000000000000000"
"00000000000000000002003EEA40000000000000000000000000000000000000"
"0000000000000000000000000003");
BEAST_EXPECT(expectedIdentity);
BEAST_EXPECT(expectedRelease);
if (expectedIdentity && expectedRelease)
{
BEAST_EXPECT(reservedData(identity.value()) == *expectedIdentity);
BEAST_EXPECTS(
reservedData(release.value()) == *expectedRelease,
strHex(reservedData(release.value())) +
" != " + strHex(*expectedRelease));
}
auto const parsedIdentity = ExportOriginMemo::parse(identity.value());
auto const parsedRelease = ExportOriginMemo::parse(release.value());
BEAST_EXPECT(parsedIdentity.has_value());
BEAST_EXPECT(parsedRelease.has_value());
if (parsedIdentity && parsedRelease)
{
BEAST_EXPECT(parsedIdentity.value().origin == origin_);
BEAST_EXPECT(!parsedIdentity.value().anchor);
BEAST_EXPECT(parsedRelease.value().origin == origin_);
BEAST_EXPECT(parsedRelease.value().anchor == anchor_);
}
auto const projected =
ExportOriginMemo::projectIdentity(release.value());
BEAST_EXPECT(projected.has_value());
if (projected)
{
BEAST_EXPECT(
projected.value().getSerializer().peekData() ==
identity.value().getSerializer().peekData());
BEAST_EXPECT(
projected.value().getTransactionID() ==
reparse(projected.value()).getTransactionID());
auto const projectedAgain =
ExportOriginMemo::projectIdentity(projected.value());
BEAST_EXPECT(projectedAgain.has_value());
if (projectedAgain)
BEAST_EXPECT(
projectedAgain.value().getSerializer().peekData() ==
projected.value().getSerializer().peekData());
}
}
void
testPreservesUserMemos()
{
testcase("preserves user Memos");
Blob const userData{0xde, 0xad, 0xbe, 0xef};
auto const base = makePayment(userData);
auto const baseBytes = base.getSerializer().peekData();
auto const release =
ExportOriginMemo::releaseForm(base, origin_, anchor_);
BEAST_EXPECT(release.has_value());
BEAST_EXPECT(base.getSerializer().peekData() == baseBytes);
if (!release)
return;
auto const& memos = release.value().getFieldArray(sfMemos);
BEAST_EXPECT(memos.size() == 2);
BEAST_EXPECT(
memos[0].getFieldVL(sfMemoType) == Blob({'u', 's', 'e', 'r'}));
BEAST_EXPECT(memos[0].getFieldVL(sfMemoData) == userData);
BEAST_EXPECT(
memos[1].getFieldVL(sfMemoType) ==
Blob(
ExportOriginMemo::memoType.begin(),
ExportOriginMemo::memoType.end()));
auto object = cloneObject(makePayment());
STArray heterogeneous{sfMemos};
STObject first{sfMemo};
first.setFieldVL(sfMemoType, Blob{'a'});
first.setFieldVL(sfMemoData, Blob{1});
first.setFieldVL(sfMemoFormat, Blob{'t', 'e', 'x', 't'});
heterogeneous.emplace_back(std::move(first));
STObject second{sfMemo};
second.setFieldVL(sfMemoData, Blob{2, 3});
heterogeneous.emplace_back(std::move(second));
object.setFieldArray(sfMemos, heterogeneous);
auto const heterogeneousBase = STTx{std::move(object)};
auto const heterogeneousBytes =
serializeMemos(heterogeneousBase.getFieldArray(sfMemos));
auto const heterogeneousRelease =
ExportOriginMemo::releaseForm(heterogeneousBase, origin_, anchor_);
BEAST_EXPECT(heterogeneousRelease.has_value());
if (heterogeneousRelease)
{
auto preserved =
heterogeneousRelease.value().getFieldArray(sfMemos);
preserved.erase(std::prev(preserved.end()));
BEAST_EXPECT(serializeMemos(preserved) == heterogeneousBytes);
}
}
void
testRejectsNonCanonicalReservedMemo()
{
testcase("rejects non-canonical reserved Memo");
auto const identity =
ExportOriginMemo::identityForm(makePayment(), origin_);
BEAST_EXPECT(identity.has_value());
if (!identity)
return;
auto const duplicate =
ExportOriginMemo::identityForm(identity.value(), origin_);
BEAST_EXPECT(!duplicate);
if (!duplicate)
BEAST_EXPECT(
duplicate.error() ==
ExportOriginMemo::Error::reservedMemoPresent);
{
auto object = cloneObject(identity.value());
auto memos = object.getFieldArray(sfMemos);
memos.emplace_back(memos.back());
object.setFieldArray(sfMemos, memos);
auto const parsedDuplicate =
ExportOriginMemo::parse(STTx{std::move(object)});
BEAST_EXPECT(
!parsedDuplicate &&
parsedDuplicate.error() ==
ExportOriginMemo::Error::reservedMemoPosition);
}
{
auto const withUser =
ExportOriginMemo::identityForm(makePayment(Blob{1}), origin_);
BEAST_EXPECT(withUser.has_value());
if (withUser)
{
auto object = cloneObject(withUser.value());
auto memos = object.getFieldArray(sfMemos);
std::swap(memos[0], memos.back());
object.setFieldArray(sfMemos, memos);
auto const parsedMisplaced =
ExportOriginMemo::parse(STTx{std::move(object)});
BEAST_EXPECT(
!parsedMisplaced &&
parsedMisplaced.error() ==
ExportOriginMemo::Error::reservedMemoPosition);
}
}
{
auto object = cloneObject(identity.value());
object.peekFieldArray(sfMemos).back().setFieldVL(
sfMemoFormat, Blob{'b', 'i', 'n'});
auto const parsedFormat =
ExportOriginMemo::parse(STTx{std::move(object)});
BEAST_EXPECT(
!parsedFormat &&
parsedFormat.error() == ExportOriginMemo::Error::malformedMemo);
}
auto malformed = reservedData(identity.value());
malformed[0] = 2;
auto parsed = ExportOriginMemo::parse(
replaceReservedData(identity.value(), malformed));
BEAST_EXPECT(
!parsed &&
parsed.error() == ExportOriginMemo::Error::unsupportedVersion);
malformed[0] = ExportOriginMemo::version;
malformed[1] = 1;
parsed = ExportOriginMemo::parse(
replaceReservedData(identity.value(), malformed));
BEAST_EXPECT(
!parsed &&
parsed.error() == ExportOriginMemo::Error::unsupportedFlags);
malformed.pop_back();
parsed = ExportOriginMemo::parse(
replaceReservedData(identity.value(), malformed));
BEAST_EXPECT(
!parsed &&
parsed.error() == ExportOriginMemo::Error::malformedMemo);
}
void
testProjectedMemoSize()
{
testcase("projected Memo size");
auto const base = makePayment(Blob(930, 0xa5));
std::string reason;
BEAST_EXPECT(passesLocalChecks(base, reason));
auto const identity = ExportOriginMemo::identityForm(base, origin_);
auto const release =
ExportOriginMemo::releaseForm(base, origin_, anchor_);
BEAST_EXPECT(identity.has_value());
BEAST_EXPECT(
!release &&
release.error() == ExportOriginMemo::Error::localChecks);
auto testExactBoundary = [&](auto project) {
std::optional<std::size_t> lastAcceptedBytes;
std::size_t firstRejected = 0;
for (std::size_t size = 1; size <= 1024; ++size)
{
auto candidate = project(makePayment(Blob(size, 0xa5)));
if (candidate)
{
lastAcceptedBytes = memoBytes(candidate.value());
continue;
}
firstRejected = size;
BEAST_EXPECT(
candidate.error() == ExportOriginMemo::Error::localChecks);
break;
}
BEAST_EXPECT(lastAcceptedBytes.has_value());
BEAST_EXPECT(firstRejected != 0);
if (lastAcceptedBytes)
BEAST_EXPECT(*lastAcceptedBytes == 1024);
};
testExactBoundary([&](STTx const& tx) {
return ExportOriginMemo::identityForm(tx, origin_);
});
testExactBoundary([&](STTx const& tx) {
return ExportOriginMemo::releaseForm(tx, origin_, anchor_);
});
}
void
run() override
{
testExactEncodingAndProjection();
testPreservesUserMemos();
testRejectsNonCanonicalReservedMemo();
testProjectedMemoSize();
}
};
BEAST_DEFINE_TESTSUITE(ExportOriginMemo, protocol, ripple);
} // namespace ripple::test
+123
View File
@@ -0,0 +1,123 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
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 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/StringUtilities.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/ExportShare.h>
#include <xrpl/protocol/SecretKey.h>
namespace ripple {
namespace test {
class ExportShare_test : public beast::unit_test::suite
{
static ExportShare
makeShare()
{
auto const [key, secret] = randomKeyPair(KeyType::secp256k1);
auto const signature = sign(key, secret, Slice{"export-share", 12});
return ExportShare{
ExportShare::currentVersion,
calcAccountID(key),
uint256{1},
4'200'000,
uint256{2},
17,
key,
signature};
}
public:
void
testRoundTrip()
{
testcase("canonical Export share round trip");
auto const share = makeShare();
auto const encoded = share.serialize();
BEAST_EXPECT(
encoded.size() <= ExportLimits::maxSerializedExportShareBytes);
auto const parsed = ExportShare::parse(encoded.slice());
BEAST_EXPECT(parsed.has_value());
if (!parsed)
return;
BEAST_EXPECT(parsed->version == share.version);
BEAST_EXPECT(parsed->owner == share.owner);
BEAST_EXPECT(parsed->originTxn == share.originTxn);
BEAST_EXPECT(parsed->originLedgerSeq == share.originLedgerSeq);
BEAST_EXPECT(parsed->originLedgerHash == share.originLedgerHash);
BEAST_EXPECT(parsed->committeePosition == share.committeePosition);
BEAST_EXPECT(parsed->signingKey == share.signingKey);
BEAST_EXPECT(parsed->signature == share.signature);
BEAST_EXPECT(parsed->wireHash() == share.wireHash());
BEAST_EXPECT(parsed->serialize().slice() == encoded.slice());
}
void
testMalformed()
{
testcase("malformed Export share framing");
auto share = makeShare();
auto encoded = share.serialize().peekData();
BEAST_EXPECT(!ExportShare::parse(Slice{}));
BEAST_EXPECT(!ExportShare::parse(Slice{encoded.data(), 20}));
encoded.push_back(0);
BEAST_EXPECT(!ExportShare::parse(makeSlice(encoded)));
share.version = ExportShare::currentVersion + 1;
BEAST_EXPECT(!share.validShape());
bool threw = false;
try
{
(void)share.serialize();
}
catch (std::invalid_argument const&)
{
threw = true;
}
BEAST_EXPECT(threw);
share = makeShare();
share.committeePosition = ExportLimits::maxCommitteeMembers;
BEAST_EXPECT(!share.validShape());
share = makeShare();
share.signature = Buffer{73};
BEAST_EXPECT(!share.validShape());
share = makeShare();
share.signature = Buffer{4};
BEAST_EXPECT(!share.validShape());
}
void
run() override
{
testRoundTrip();
testMalformed();
}
};
BEAST_DEFINE_TESTSUITE(ExportShare, protocol, ripple);
} // namespace test
} // namespace ripple
Loaded 100 of 244 files, more files were not shown because too many files have changed in this diff. Show more