Skip to content

fix(manifest): bound live propagation and durable recovery - #795

Closed
sublimator wants to merge 35 commits into
onchain-manifestsfrom
onchain-manifests-canonical-or-signed
Closed

fix(manifest): bound live propagation and durable recovery#795
sublimator wants to merge 35 commits into
onchain-manifestsfrom
onchain-manifests-canonical-or-signed

Conversation

@sublimator

@sublimator sublimator commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

This is a stacked draft against #779, not an alternative to it. It combines
that PR's on-ledger durability with the bounded live-propagation work from
#794, then makes the boundary between those two jobs explicit.

The existing forever-growing manifest cache quietly does two jobs. It moves
new manifests through the network, but it also acts as accidental durable
memory: once enough long-lived nodes have seen a manifest, reconnecting peers
keep teaching it to each other indefinitely. That gives useful cockroach-like
survival, but only by retaining and exchanging an unbounded cache.

This stack replaces those two properties separately. Validations drive fast
live propagation; the ledger provides deliberate durable recovery.

Before sending a validation, a node sends its best manifest for that signing
key using the existing envelopes. The receiver holds at most one unverified
normal manifest per connection and does not verify or retain it until a
matching validation arrives. Once both signatures pass, locally listed
validators may enter the durable local cache; other permitted validations can
carry the association ephemerally without growing it. Peer resource charges
cover malformed, invalid, and useless work.

The process cache now has an explicit retention boundary. Current policy and
locally configured manifests remain protected. Everything else that is worth
retaining -- including old wallet rows, cold on-ledger discoveries, and the
narrow unlisted-revocation correction lane -- shares one 1,000-master LRU.
Eviction removes the manifest, reverse signing-key mapping, and recency row
together. Losing that residue is safe: a later paired validation or the
validated-ledger cold lookup can recover it. This is a narrow cache bound, not
the tier-2 list, candidate population, or permit machinery from #777.

This also repairs missed associations without a new protocol negotiation. A
naked validation from a known signer is an implicit request: after it
authenticates, the receiver returns its current manifest on that connection.
An unknown signer may use the on-ledger signing-key index only after paying the
cost of a valid validation signature; successful recovery carries the manifest
onward and repairs the sender, while unresolved work is charged and dropped.
Revocations retain a narrow bounded correction lane because a revoked
validator deliberately has no later validation to carry them.

SetManifest is not a second fast-gossip plane. It follows ordinary
transaction relay and becomes authoritative durable state only through normal
ledger admission. A fee-blocked or otherwise unapplied wrapper is not
harvested into the cache and does not trigger a separate full-mesh
TMManifests broadcast. The node that submits an automatic wrapper already
learned the manifest through live propagation; the wrapper's remaining job is
to make that binding recoverable after the live network forgets it.

Registration and updates now have distinct authority, which is the main
behavioural change from #779 as drafted: there, any relayer could place any
validator's manifest on the ledger; here, the first registration is an
ordinary account-signed SetManifest, so joining the durable registry is the
key holder's explicit act and no previously signed manifest can be conscripted
by a third party. Registration creates one canonical owned ltMANIFEST, adds
it to the owner directory, increments OwnerCount, and charges one owner
reserve. Once that slot exists, rotations and revocations may use either
another ordinary account-signed transaction or a canonical unsigned wrapper
whose authority comes from the embedded manifest signature — so the network
can carry a registered validator's updates for it, but only its owner can
enrol it.

The unsigned form is deliberately one exact transaction, so anyone can relay
it but nobody can vary it. Canonicality covers the complete envelope, including
the Fee and the absence of memos or other extension fields. Its ruleset-fixed
Fee is 1,000 drops plus 100 drops per manifest byte. It receives no free or
priority queue lane; account-signed updates retain ordinary fee behaviour.

Sequence handling stays intentionally small. A valid higher sequence becomes
authoritative immediately; lower and identical manifests are ignored. If two
different manifests use the same sequence, the version in the validated ledger
replaces the cache. Equivocation and validator-list dismissal policy remain
outside this PR.

For an active manifest, the complete object is stored under both lookup
directions, matching #779's direct lookup geometry. The stable master-key copy
is the one owned, reserve-backed directory object; the active signing-key copy
is a full duplicate, and each copy points to the other. Revocation removes the
signing-key copy and leaves the owned master-key record. The master-key lookup
is the regular reconciliation path for locally trusted validators. Resolving
an unknown validation signing key takes one bounded read, but remains a rare
cold-cache fallback — validation prerequisites normally populate the mapping,
so startup, missed propagation, and local cache loss are its expected
occasions. Distinct negative probes are capped per ledger so valid-signature
traffic cannot manufacture unbounded ledger reads.

For now, the owned manifest is an explicit account obligation, so
AccountDelete returns tecHAS_OBLIGATIONS. Expiration, tombstone collection,
and any later rule for releasing that obligation are deferred rather than
being bundled into this refinement.

The PR remains a draft so these refinements can be discussed alongside #779
before either protocol shape is treated as final.

A naked validation that authenticates against the current cached manifest
for its signing key is an implicit request for that manifest. The recipient
returns one singleton TMManifests on the same connection, bounded by a
strand-owned per-connection repair ledger (256 rows, cleared on overflow,
at most one singleton per master/sequence). Forged, malformed, unknown, and
paired traffic draws no response. A validation with no available manifest
is still sent naked.
Three scenarios on the new scripts primitives: mid-run rotation to
sequence 2 through an old relay (supersede, admission, repair re-arm);
a config-injected revocation seeded by the old node with the remaining
four of five validators holding exact quorum; and wallet-wipe recovery
through an old upstream with resumed paired forwarding. Waits are
event-driven on terminal log facts with heartbeat logging.
The original design sketch had honest nodes informing senders of fresher
manifests upon receipt of stale; it was parked as an amplification
surface pending an explicit bounded one-shot model. The repair ledger is
that model, so the lane lands as one more caller of sendManifestRepair
at the singleton global-sequence guard, reusing the snapshot the guard
already fetches.

Strictly-stale only: equal-sequence arrivals are ordinary always-send
traffic and draw nothing. The trigger needs no signature verification;
safety comes from bounds, not authentication - answers are limited to
masters this node retains, once per sequence per connection via the
shared ledger, and monotone sequences make correction exchanges
converge in one round trip. A retained revocation is itself the answer:
a stale normal manifest for a revoked master draws the tombstone, giving
revocations a demand-driven re-supply path to any peer that demonstrably
still uses the dead identity. Retention, not listing, qualifies a master
for an answer.

Deferred: the legacy batch lane does not correct (singletons only); no
live-scenario control yet (the mixed old-binary connect dump is the
natural trigger); design-doc sync for the reopened corrections language
left to the integration pass.
# Conflicts:
#	src/xrpld/app/misc/Manifest.h
#	src/xrpld/app/misc/detail/Manifest.cpp
#	src/xrpld/overlay/detail/OverlayImpl.cpp
Cold lookup treated the open ledger as ledger-authoritative, so an
unvalidated SetManifest could stick in ManifestCache. Unknown naked
validations also occupied the validation hash before the probe,
poisoning a later verified pair.

Probe only the validated ledger and refuse open views. Restore
drop-before-claim when no probe will run. Give cold probes their own
HashRouter domain until the signer resolves. Comments match the
settled split: paired TMManifests for live rotation, standalone
revocations, paid SetManifest for durability only.
A cache-miss naked validation queued a probe job per hash, so a burst of
unknown signers bypassed the connection's one-in-flight token. Share that
token with the cold lookup, and do not probe while a pending candidate
occupies the slot.
The early naked-unknown drop concluded what checkValidation used to conclude
after verifying a signature, but returned without any resource charge. Since
a cold probe now holds the connection's one in-flight token, that free path
is the one every further unknown naked validation on the connection takes,
so an unresolvable signing key could be replayed at no cost.

Charge feeUselessData at the drop, matching the job's charge for the same
conclusion. Also correct the duplicate-repair comment: the repair is a cache
lookup that no-ops unless some connection's probe already resolved the key,
not a check that the first probe resolved.
@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.34850% with 123 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.8%. Comparing base (2234b8f) to head (a604dde).
⚠️ Report is 2 commits behind head on onchain-manifests.

Files with missing lines Patch % Lines
src/xrpld/overlay/detail/PeerImp.cpp 86.1% 56 Missing ⚠️
src/xrpld/app/tx/detail/SetManifest.cpp 84.2% 29 Missing ⚠️
src/xrpld/app/misc/detail/Manifest.cpp 90.0% 14 Missing ⚠️
src/xrpld/net/detail/RPCSub.cpp 74.4% 11 Missing ⚠️
src/xrpld/overlay/detail/OverlayImpl.cpp 85.7% 7 Missing ⚠️
src/xrpld/app/hook/detail/HookAPI.cpp 25.0% 3 Missing ⚠️
src/xrpld/app/misc/NetworkOPs.cpp 60.0% 2 Missing ⚠️
src/xrpld/app/misc/detail/TxQ.cpp 92.3% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                 Coverage Diff                 @@
##           onchain-manifests    #795     +/-   ##
===================================================
+ Coverage               77.3%   77.8%   +0.5%     
===================================================
  Files                    839     839             
  Lines                  78958   79627    +669     
  Branches               11592   11429    -163     
===================================================
+ Hits                   61064   61971    +907     
+ Misses                 17884   17646    -238     
  Partials                  10      10             
Files with missing lines Coverage Δ
include/xrpl/protocol/detail/ledger_entries.macro 100.0% <ø> (ø)
src/xrpld/app/consensus/RCLConsensus.cpp 60.4% <100.0%> (ø)
src/xrpld/app/consensus/RCLValidations.cpp 90.2% <100.0%> (+15.7%) ⬆️
src/xrpld/app/main/Application.cpp 66.9% <100.0%> (-0.1%) ⬇️
src/xrpld/app/misc/Manifest.h 91.3% <100.0%> (-6.7%) ⬇️
src/xrpld/app/misc/ValidatorList.h 100.0% <ø> (ø)
src/xrpld/app/misc/detail/ValidatorList.cpp 86.9% <100.0%> (+0.9%) ⬆️
src/xrpld/app/rdb/detail/Wallet.cpp 82.1% <100.0%> (+3.6%) ⬆️
src/xrpld/app/tx/detail/SetManifest.h 100.0% <ø> (ø)
src/xrpld/app/tx/detail/Transactor.cpp 85.5% <100.0%> (ø)
... and 16 more

... and 15 files with indirect coverage changes

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread include/xrpl/protocol/detail/ledger_entries.macro Outdated

LEDGER_ENTRY(ltMANIFEST, 0x004D, Manifest, manifest_entry, ({
{sfAccount, soeREQUIRED},
{sfOwnerNode, soeREQUIRED},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manifests are not directory objects, cannot be deleted and do not need to be referenced in this way

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably they should even pay 2 reserves here (this exploration does not do that), for all the reasons reserves exist in the first place. They're taking 2 slots in the memory-resident state tree.

I think if you're charging a present AccountRoot the Fee, it's clearly an owned object
Pick a lane, right?

It should pay a reserve for it like anything else as it's on the ledger
It can be listed under account_objects, be seen in explorers, and it can be an obligation blocking account delete

I would also say the enrollment to online manifests should be a user txn, not really much more hassle than setting up the .well-known/xahaud.toml

Then you have something that fits in with the existing reserve system and validators can keep their authority.

So basically, a normal Account object, with the convenient auto-submitted update txns with canonical Fee

re: deletion, you probably don't need them to be super-duper-for-reals-permanent and they could be deleted.

No point having tombstones in 2035 for a defunct business that was a validator, that no one even has configured anymore.

But that's a "later" provided AccountDelete sees manifests as obligations with no current method to delete.


But this doesn't really matter, the main thing is bounding the cache, and tying precisely the manifests to validations where possible, rather than dumping the whole cache on connection, or some smaller yet still arbitrary LRU subset.

@sublimator sublimator mentioned this pull request Sep 2, 2026
@sublimator sublimator closed this Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants