Sort the weak peer snapshot by ID only in stepping mode before broadcast and relay callbacks consume the injected random engine. Preserve the existing dispatch order outside stepping.
Reserve an unused pair of simulated peer IDs between replays and require the same ledger payload, event fingerprint and count. This exercises allocation history without changing applications, ports or clocks.
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.
The fault-injection draws moved from PeerImp.cpp and
ConsensusExtensions.cpp into RuntimeFaultRandom.h; the allowlist still
named the old files, so the tripwire went red on the pushed head.
Traffic is a 3-validator mesh, and its payload hash covers ledger
hashes plus transaction ids and sequences. The hub pin is five
validators linked only to one non-validator hub.
Replace pre-delivery sampling and formatted-log matching with actual stale-proposal dispatch plus a typed collector commit in the same authenticated proposal job. Capture the round parent even in open phase, require exact carried material, and run with observer trace disabled so enhanced logging cannot change the oracle.
The dispute and fork tables and the traffic and CSF event-order pins
live in goldens/dsf.h. --unittest-arg=goldens=print writes the observed
values. The assertions are unchanged.
forkCheckedSeqs counts the sequences validatedForkFree walks. The pinned
2000 is a max consumed beat, Kind weights profiled cost, eventCost rejects
overflow, and fingerprints require a nanosecond clock. Pinned rows unchanged.
Every unittest process, including one job, removes and recreates the
shared-memory object and message queue. Give that pair a parent-pid
suffix and publish the names in the environment so spawned children
and the in-process child open only that pair.
Use the per-node PRNG only in single-threaded stepping mode. Preserve separate per-thread MT engines outside stepping so threaded fault hooks do not race an injected engine. Test all four draw domains and three bit-for-bit RuntimeConfig drop/claim/reveal replays; jitter timer delivery remains out of scope.
Fix issue 2029: live clocks advance while a stopped node has no TimeKeeper. Keep the threaded runner network-time cursor and apply it before startup on restart or late add, preserving existing per-tick conversion and stepping clock semantics. A 25-second offline regression reconnects through production handshake checks; restoring the old stop-time snapshot makes it fail with the observed clock exception.
Closes the catch-up review finding that the before-lock probe was masked by earlier accept-time acquisitions. Rendezvous immediately before onPreBuild and observe a failed try_lock on that same mutex, with bounded waits and captured-job cleanup.
The catch-up review found that makeLiveBuildTxSet reads only its immutable argument. Avoid holding C during its transaction-count-sized scan; retain and measure the state-dependent ordering-salt and materialization locks.
Use one strict predicate in both unwitnessed fallback oracles and the directed held-material scenario. Assert live at deadline-1 and deadline, then a positive expired classification after the deadline on real accepted history.
Closes the catch-up review finding that unwitnessed pending latches could be labeled expired using only submission-based run targets. Require accepted history to pass the latch deadline; equality remains live.
Closes the catch-up review finding in 020db70424: do not call a consuming stop predicate again while classifying the exit. Cover a one-shot predicate and heartbeat/step budgets that never evaluate it.
The profiled dispute takes a seed base. Two K=3 rows: 0x1000000000000001
resolves at beat 54, and the default seed accepts at 10, saturates under
K=3, and recovers in 6 beats at K=0. The profiled loop records its stop
reason. Recovery beats are recorded only on success. No consensus policy
changed.
numeric_limits digits is the storage width, not how many values the
engine emits. A 32-bit engine in a 64-bit result type was treated as
a full word, which biases a range and can reject forever. A full
64-bit range is still one draw. Other power-of-two ranges are
concatenated. The rest follow the standard independent_bits rejection.
The dsf lineage pins its own constants. Its schedule is not the donor
branch, so the #814 fingerprints never held here. These are the macOS
values on this branch.
The four direct uniform_int_distribution calls on this lineage were still
stdlib-defined. Peer send drop and delay jitter, and the RNG claim and
reveal drops, now use rand_int. A zero jitter adds nothing, because
rand_int rejects a zero-width range.
std::sample is not the same on libstdc++ and libc++, so saturated
acquisition cells diverged on Linux after rand_int was made portable.
Selection-sample the id-sorted peer list with rand_int, and take a
must-keep tail without a zero-width draw.
std::uniform_int_distribution is not the same on libc++ and libstdc++,
so a seeded engine produced different harness event streams on Linux.
Rejection-sample a closed range from the engine's own 64-bit output.
Seeding overloads are unchanged.
Validator 2 is absent from node 0's peer positions while its stale
proposals arrive, and present again after the delay is lifted. The wire
count and the later collector commit stay positive.
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.
A self-rescheduling job never lets the queue look quiet, so virtual time
never advanced and bring-up stalled at ledger 7. After a few non-quiet
polls with a frozen pipe and frozen post counters, the drain returns and
the clock moves. The stall clock remains the failure signal.
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.
Count crafted injections separately from real unauthorized emissions,
and hold validations toward validator 2 until it has closed the origin
without releasing.
Inject a non-member signer, a wrong committee position, and a wrong
origin through the observer's real ingress, and pin each resolution
reason before collector admission.
Count validator 2's divergent accepted builds on the stale-proposal case,
and add a five-validator case where one isolated node cannot stop the
other four from witnessing.
Hold validator 2's proposals for two observed rounds after it releases,
drop its direct frames, and require node 0 to log a prevLedger-mismatch
rejection before the observer admits the same origin.
The observer journal mark is taken before the drop lifts, and the no-witness
check applies only to the decided round. The next round witnesses once from
proposal carriage, with no direct re-release.
Count every validation, proposal and direct frame on the wires around
validator 2 after the timed-out accept, and sample its mode and consensus
info. The failure line names the known-red review.
The case stays failing and is omitted from the default run behind
DISABLED(B2C:late-after-deadline). After the drop lifts, links from
validator 2 carry no direct frames and no proposals, and validator 2
logs no further local release frame for the origin.
B2a now shows the tecDUPLICATE view reset left the pending link in place.
B2b derives the gate bound from sidecarConvergenceTimeout and logs gateSeq.
B2c drops validator 2 until that gate times out. The decided round has no
witness, and neither a collector-commit nor a witness appears before the
publication window ends. That red result is kept.
B2a pins the linked latch at publication expiry, tecDUPLICATE on ticket
reuse, and the unlink plus owner-reserve after a later successful intent.
B2b drops validator 2's Export frames until the gate is inside its 3s
deadline, then witnesses once after a post-gate root publish.
B2a holds direct and proposal-carried Export frames on every link until
validated sequence passes the publication window, with gate timeouts and
no witness. Releasing that material is admitted as publication-expired.
A later intent from the same owner witnesses once.
Require submit/pre-heal own releases to be zero, unauthorized releases
zero on every validator through recovery, and recovered ledgers at
stallValid to match the saved accepted hash. Drop the unproven expiry
replacement; this origin must validate and witness in the bounded heal.
B5 no-quorum leg: isolate validator 2, confirm configured quorum is 3,
and show survivors close while validated seq stalls. A fresh Export
enters that speculative closed history with zero own releases. After
heal, authoritative history is compatible and the origin is witnessed
once.
B7 now waits for the honest encoding to be admitted on the observer,
checks that observer's validated latch is still live, and sends one
forged frame on the observer's peer. The observer extension journal
must contain a single collector-commit record for that wire hash with
signatureVerified=false and AdmitResult::invalid.
Pre-commit leak finder hits protobuf TMExportShares::shares(); wire
field name, not audited-hook vocabulary.
B7 injects a parseable share with a corrupted committee signature
through real PeerImp after an honest contribution, while the origin
is still pending. The collector keeps the honest encoding and the
cohort still produces one matching witness.
Pre-commit leak finder hit protobuf TMExportShares::shares(); that is
the wire field name, not audited-hook vocabulary.
Run Builds/levelization/levelization.py after the harness port. The
xrpld.core <-> xrpld.net loop is gone (Config no longer includes
HTTPClient); new ordering edges are the harness's test.jtx and
test.consensus dependencies on xrpld.overlay/peerfinder/net.
The first origin can already be witnessed before stop, so observer
hasEligiblePendingExports is not current evidence. B4 keeps lagged
validatedLedgerWork after isolate/flush, requires the gap origin pending
on a live validator, and checks the restarted observer's replay cursor.
Codex B4: current observer validatedLedgerWork after isolate/flush, not
combined with post-flush pending-origin state. Gap witness is taken from
a live validator while the observer is down. Restart stays before the
stale deadline; catch-up runs past it. Peer metadata matches by origin.
laggedPendingJobCount is per-node/name and drops when the job runs.
B4 now checks that count after isolate/flush, restarts before the old
deadline, and requires a witness created while the observer was down.
Delay observer validatedLedgerWork until Export work is queued, isolate
and stop it while validators continue, then restart/reconnect. Old
scheduled actions must not run past the lag horizon; the replacement
stays keyless and acquires the witness history.
Parse errors, wrong sidecar type, missing identity fields, or duplicate
committee positions abort the candidate count. Clear the job observer
after the passive phase.
Origin A uses committee {0,1} (qC=2); B uses {0,1,2} (qC=3). Drop only
validator 2's B frames toward the observer. At a real job boundary the
observer's existing sidecar map must show A=2 and B below qC. Validators
may still witness B; the observer may acquire.
Whole-frame drop of starved-carrying frames also starves complete
origins from those validators. Exclusive drops leave proposal-carried
shares filling qC. Obstacle, not a new transport selector.
Whole-frame drop of starved-carrying proposals/direct batches also
removes complete origins' material from those validators. Exclusive
drops leave proposal-carried shares filling qC. Cannot isolate one
origin below qC without stripping signed fields.
Drop validators 1-2 whole frames that carry the starved origin. Mid-run
asserts the collector holds it below qC with no witness; complete origins
still emit once. Heal then check no repeated or unexpected witnesses.
B1 records send/receive batch identities and requires a real inversion.
B3 scans every ledger, compares witness bytes/metadata across nodes, and
requires the observer locally built those ledgers. Below-qC stays open.
Delay by sender id toward the observer so even frames overtake node 0
without a global counter. Overlapping origins keep three distinct
witnesses; below-qC isolation needs per-origin proposal surgery and is
left as a gap.
B1 delays odd direct Export frames so even frames overtake them, with
an inversion counter. B3 submits two owners / three tickets and starves
one origin to one validator contribution. Three replays each. No
production changes.
The N=3 target-4 trace gains two validatedLedgerWork events at 42.010s on nodes 0 and 1. Removing only those events reproduces the prior 330-event fingerprint exactly. Retain all replay, hash-agreement and weighted-pressure assertions.
Merge a992572b70; retain validator identity fields, phase locking, manual heartbeat control and extension polling. Share the send observation hook across immediate and delayed transport sends.
LOAD restart called overlay() from tryAdvance/getFetchPack while
overlay_ was still created later in setup. Keep beginConsensus after
the ledger exists.
MultiNode already installed send/message/lifecycle/validation hooks.
Call them from PeerImp, NetworkOPs, and handleNewValidation so
SteppingNetwork forensics are not empty. Production hooks stay empty.
Wait for all deferred reads then sort by parentID/branch. Refill
resumes in SHAMapNodeID order so missing-node requests do not follow
reader completion or heap address order.
xahaud envconfig defaults relational to per-app rwdb, so LOAD latest
after stopNode cannot find the saved ledger. Donor unit tests leave
sqlite files under database_path; match that for restart policy.
JobType aliases, include paths, OverlayImpl method names, Config
COMPRESSION flags, and PeerFinder::Config::makeConfig. HarnessNet
does not compile yet.
Mechanical copy of Transport, SimOverlay/SimTransport, MultiNode,
scheduler/controller/network, and HarnessNet/SteppingDeterminism suites
with xahaud name mappings. Overlay weld onto PeerImp is still required
before these compile.
setupWithoutListeners stores setup without binding sockets. stepping
harnesses can disable signals and the stall detector via peeredEnvconfig.
Config grows observe-only peer/validation hooks, empty in production.
PeerImp arms TimeoutCounterTimer when makePeerTimer is installed.
inlineStrands builds the existing strand type over an inline executor.
Tracking, ping RTT and uptime use getPreciseStopwatch.
getScore draws from Application::getPrng. addPeers, getPeerWithTree and
getPeerWithLedger collect and sort by peer id before scoring so draws
do not follow unordered overlay traversal.
LedgerHistory, TransactionMaster, NodeFamily caches, PeerFinder and
reduce-relay idle checks use getStopwatch. Fetch-pack deadlines use the
same clock. Overlay tx relay, ping sequence and InboundLedger peer
sampling use getPrng; fetch-pack scoring and acquire sampling sort by
peer id first.
Caches, HashRouter, Validations, LedgerMaster and InboundLedgers use
the injected stopwatch. NetworkOPs passes that clock into RCLConsensus
instead of the process-global clock. manualHeartbeat suppresses the
asio heartbeat; heartbeatTick posts the same job.
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.
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.
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.
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.
.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).
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).
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
State that clients expanding sfExportSignatureHash must match ExportResultBuilder's canonical signer ordering, empty SigningPubKey, and target-chain signer cap before hashing or submitting.
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.
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.
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.
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.
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.
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.
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.
Require wire-carried export signatures to match the signed ExtendedPosition digest before harvesting, and keep the per-blob size guard local to the helper.
Treat observedParticipantsHash as entropy extension material during proposal precheck, so disabled featureConsensusEntropy rejects diagnostic-only extension payloads too.
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.
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.
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.
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.
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.
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).
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).
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).
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).
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).
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.
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'.
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).
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).
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)).
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.
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.
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.
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).
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.
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.
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.
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.
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).
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.
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.
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.
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
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.
Add ConsensusExtensions debug logging to the entropy and export scenario suite defaults so participant bitmap diagnostics are captured without live log-level changes.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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).
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.
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
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.
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.
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.
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.
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.
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.
- 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
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
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).
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.
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.
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.
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().
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.
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.
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.
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.
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.
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.
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.
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.
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.
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()).
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.
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.
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.
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
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.
- 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
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.
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).
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.
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.
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.
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.
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.
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.
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).
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.
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.
- 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.
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.
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.
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.
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.
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.
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.
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().
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.
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.
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%
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.
Clarify inline that seq=3 publish can carry unchanged txSetHash while still providing extra entropySetHash delivery/fetch opportunity under packet loss or reordering.
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.
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.
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.
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.
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.
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.
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.
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.
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.
- 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)
- 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)
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
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`.
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).
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.
- 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
* 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.
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.
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
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.
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.
Handle ltEXPORTED_TXN and ltCONSENSUS_ENTROPY in LedgerEntryTypesMatch so creating/destroying these pseudo-ledger entries does not trigger XRP balance invariant violations.
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).
- 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
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.
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).
Add @@start/@@end comment markers to pseudo-tx submission filtering,
fast-polling, local testnet resource bucketing, and test environment
gating. No logic changes.
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
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.
- 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
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.
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.
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).
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.
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.
- 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.
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.
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.
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.
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.
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
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.
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.
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.
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.
- 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
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.
- 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
- 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
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.
- 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.
- 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
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.
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.
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.
- 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.
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
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.
@@ -83,17 +83,9 @@ The [commandline](https://xrpl.org/docs/references/http-websocket-apis/api-conve
The `network_id` field was added in the `server_info` response in version 1.5.0 (2019), but it is not returned in [reporting mode](https://xrpl.org/rippled-server-modes.html#reporting-mode). However, use of reporting mode is now discouraged, in favor of using [Clio](https://github.com/XRPLF/clio) instead.
## XRP Ledger server version 2.5.0
As of 2025-04-04, version 2.5.0 is in development. You can use a pre-release version by building from source or [using the `nightly` package](https://xrpl.org/docs/infrastructure/installation/install-rippled-on-ubuntu).
### Additions and bugfixes in 2.5.0
-`channel_authorize`: If `signing_support` is not enabled in the config, the RPC is disabled.
## XRP Ledger server version 2.4.0
[Version 2.4.0](https://github.com/XRPLF/rippled/releases/tag/2.4.0) was releasedon March 4, 2025.
As of 2025-01-28, version 2.4.0 is in development. You can use a pre-release version by building from source or [using the `nightly` package](https://xrpl.org/docs/infrastructure/installation/install-rippled-on-ubuntu).
# A hit under src/ or include/ is accepted only when its path starts with one
# of these prefixes. The justification is for reviewers; the script ignores it.
src/test/ Tests are not on the consensus schedule.
include/xrpl/basics/random.h Seeding overloads only. rand_int does not call these distributions.
include/xrpl/basics/hardened_hash.h One-time hash salt drawn from OS entropy.
include/xrpl/crypto/csprng.h CSPRNG declaration. OS entropy, not a consensus draw.
src/libxrpl/crypto/csprng.cpp OS entropy for the CSPRNG.
src/xrpld/peerfinder/ default_prng shuffles. Not the harness engine.
src/xrpld/app/misc/RuntimeFaultRandom.h Fault-injection engines for RuntimeConfig faults outside stepping mode; under stepping they draw from the injected engine.
src/xrpld/overlay/detail/OverlayImpl.cpp std::shuffle of relay peers. Runs only when TX_REDUCE_RELAY_ENABLE is on, and that flag is off. Uses app_.getPrng when it does run.
**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.
## xahaud
The server software that powers Xahau is called `xahaud` and is available in this repository under the permissive [ISC open-source license](LICENSE.md). The `xahaud` server software is written primarily in C++ and runs on a variety of platforms. The `xahaud` server software can run in several modes depending on its configuration.
| `antithesis-sdk`| [Project](https://github.com/antithesishq/antithesis-sdk-cpp/) | [Antithesis](https://antithesis.com/docs/using_antithesis/sdk/cpp/overview.html) SDK for C++ |
| `ed25519-donna` | [Project](https://github.com/floodyberry/ed25519-donna) | [Ed25519](http://ed25519.cr.yp.to/) digital signatures |
| `secp256k1` | [Project](https://github.com/bitcoin-core/secp256k1) | ECDSA digital signatures using the **secp256k1** curve |
| `antithesis-sdk`| [Project](https://github.com/antithesishq/antithesis-sdk-cpp/) | [Antithesis](https://antithesis.com/docs/using_antithesis/sdk/cpp/overview.html) SDK for C++ |
| `ed25519-donna` | [Project](https://github.com/floodyberry/ed25519-donna) | [Ed25519](http://ed25519.cr.yp.to/) digital signatures |
| `rocksdb` | [Recipe](https://github.com/conan-io/conan-center-index/tree/master/recipes/rocksdb) | Fast key/value database. (Supports rotational disks better than NuDB.) |
| `secp256k1` | [Project](https://github.com/bitcoin-core/secp256k1) | ECDSA digital signatures using the **secp256k1** curve |
This library provides methods for C++ programs to configure the [Antithesis](https://antithesis.com) platform. It contains three kinds of functionality:
-Assertion macros that allow you to define test properties about your software or workload.
-Randomness functions for requesting both structured and unstructured randomness from the Antithesis platform.
- Lifecycle functions that inform the Antithesis environment that particular test phases or milestones have been reached.
* Assertion macros that allow you to define test properties about your software or workload.
*Randomness functions for requesting both structured and unstructured randomness from the Antithesis platform.
*Lifecycle functions that inform the Antithesis environment that particular test phases or milestones have been reached.
For general usage guidance see the [Antithesis C++ SDK Documentation](https://antithesis.com/docs/using_antithesis/sdk/cpp/overview/)
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.