oor: validate incoming ancestor packages (#366) - #439
Closed
ellemouton wants to merge 3 commits into
Closed
Conversation
Incoming receive materialization persists ancestor OOR artifacts for later unroll recovery. Those artifacts come from operator/indexer responses, so accepting only parseable PSBTs lets a bad response overwrite recovery state. Validate the finalized package shape, txid-derived session id, and ancestor reachability before converting RPC responses or writing package rows.
Cover the positive case where a checkpoint actually spends an ancestor Ark output, the duplicate-ancestor rejection path, and the per-ancestor checkpoint count cap added on the RPC adapter. The existing validation commit only asserted the negative cases, so a later regression that silently dropped reachability or bounding would not have been caught.
UpsertPackage previously checked only the stored direction before falling through to delete and re-insert the Ark PSBT and checkpoint rows. That made the artifact store a rewrite surface: a malicious operator/indexer response that produced a parseable package for a known session id could overwrite previously stored recovery artifacts even when the FSM-layer validation passed (e.g. when the attacker holds the original Ark transaction). Treat retried upserts as idempotent only when the serialized Ark PSBT and checkpoint payload match the existing row. Any divergence now fails the upsert so the original recovery artifact survives.
There was a problem hiding this comment.
Code Review
This pull request implements validation for incoming Out-of-Round (OOR) package graphs to ensure that ancestor artifacts are valid and reachable from the root package's checkpoint chain. It also adds an idempotency check in the database layer to prevent overwriting existing packages with conflicting payloads and enforces receive limits on ancestor checkpoints. I have no feedback to provide.
ellemouton
marked this pull request as ready for review
May 15, 2026 02:06
Member
Author
|
Superseded by consolidated PR #459. Closing to reduce CI load. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #366.
Summary
UpsertPackagecalls that try to overwrite an existing row's Ark PSBT or checkpoint payload.The bug
The incoming OOR receive path persists operator/indexer-supplied ancestor packages as recovery artifacts for later unroll. Two attack vectors were open:
UpsertPackagechecked only the stored direction before falling through to delete and re-insert the Ark PSBT and checkpoint rows. A response producing a parseable package for a session id the attacker already knew (e.g. they hold the original Ark transaction) could overwrite the previously stored recovery artifacts, destroying recovery state for a real received VTXO.The fix
Vector (a) — ingest-time validation (
a9147ad8). RequireSessionID == ark_txid, runoortx.ValidateFinalizePackageto check the canonical Ark PSBT shape, the exact checkpoint set, and the finalize-sig material, then walk the checkpoint-input graph to require each ancestor checkpoint to be reachable from the root. Ancestor count is capped via the existingReceiveLimits. Validation runs in two places for defense in depth:IncomingTransferEventFromResponseWithLimits— the RPC adapter, so bad responses are rejected before they touch the FSM.validateMaterializeIncoming— the persistence handler, so any future code path that reaches materialization without going through the adapter is still gated.Vector (b) — payload-equality on duplicate upsert (
64228380). On a duplicate-PK upsert, compare the existing Ark PSBT and checkpoint bytes against the incoming payload. Equal payloads are treated as idempotent (early return). A mismatched payload fails with"oor package %x already exists with different payload", preserving the original artifact. The compare runs inside the existingExecTx;ListOORPackageCheckpointsorders by index ASC, so positionalbytes.Equalis sound.Commit layout
Three atomic commits per the repo's commit style:
a9147ad8 oor: Validate incoming ancestor packages before storing9ebf1444 oor: Add coverage for ancestor package validation graph64228380 db: Reject same-direction OOR package payload rewritesThe
db:payload-rewrite rejection is in scope for #366 (vector b) but kept as its own commit because it is an independently reasoning-able change at a different layer. Happy to squash before merge if a reviewer prefers a single security-fix commit.Test plan
oorunit tests cover the positive (ancestor actually spent by checkpoint), duplicate-ancestor rejection, and per-ancestor checkpoint cap paths.dbunit tests cover the same-direction same-payload (idempotent) and same-direction divergent-payload (rejected) upsert cases.make fmt-changed+ native linter clean on the branch.