Skip to content

feat(bridge): two-phase authorize-then-prove reservation settlement - #1091

Draft
mswilkison wants to merge 5 commits into
feat/utxo-reservation-routerfrom
feat/utxo-reservation-settlement
Draft

feat(bridge): two-phase authorize-then-prove reservation settlement#1091
mswilkison wants to merge 5 commits into
feat/utxo-reservation-routerfrom
feat/utxo-reservation-settlement

Conversation

@mswilkison

Copy link
Copy Markdown
Contributor

Stacked on #1090 (router). Implements the settlement state machine of RFC 13 — the shared fix for the review's Critical and most High findings, whose root cause was a single-phase lifecycle racing timeout/veto against confirmed Bitcoin spends.

The machine

Every reservation Bitcoin action (acceptance anchor, in-kind redemption, re-anchor, dissolution) is now an explicitly requested generation keyed by (reservationKey, requestNonce):

  • Request = authorize + reserve + snapshot. All capacity/lifecycle checks happen here and are reserved (global total, per-wallet count, target liveness, per-wallet dissolution lock); every proof-critical parameter (fee bound, redeemer script, target wallet, expected main UTXO, timeout) is snapshotted into the generation's ReservationAction record. A signed authorized action can never become unprovable because state moved later (H-02).
  • Proof names its generation and validates only against the record. Reserved-redemption proofs enforce the generation's watchtower delay in the Bridge proof path (H-03); a vetoed generation never settles — the early signature stays fraud-exposed by design.
  • Timeout writes a terminal, late-proof-accepting record (mirror of timedOutRedemptions). A late proof settles: consumed outpoints are marked in spentMainUTXOs (honest-but-late signature defeats fraud challenges), the lineage closes, and no Bank movement repeats — no double refund, no double burn (C-01). If the current pending redemption also matches the tx, the proof must settle it (escrow burned — the correct accounting); a non-matching pending generation is unwound (Superseded, escrow refunded) since its anchor provably no longer exists.
  • Re-anchor/dissolution authorization (H-01): explicit requests with nonce, source-state rules (MovingFunds permissionless / Live governance-only), distinct capacity-reserved target; proof requires the output to pay the recorded target. Same-wallet re-anchors no longer exist — the byte-ambiguity with dissolution is gone.
  • Dissolution lock (H-07): one in-flight dissolution per wallet + snapshotted expected main UTXO; registry drift (a sweep landing first against a no-main-UTXO snapshot) re-registers the confirmed output as a moved-funds sweep request.
  • Lifecycle gating (H-05 + part of H-06): requests need a Live/MovingFunds wallet; post-grace only dissolution is requestable (request/timeout cycling can't defeat the stranding bound); a wallet holding anchors cannot begin closing — with no main UTXO it enters MovingFunds to drain them; redemption and dissolution timeouts slash like pooled redemption timeouts.
  • Retry entitlement (M-02): minted only when a fee-paid generation times out, single-use, consumed by the fee-free retry. Per-generation veto keying (M-03): objections never accumulate across generations; reservation-scoped safety check is ban-state only.
  • Snapshots (M-01): per-position term/grace at acceptance; per-generation fee bounds/timeouts at request. Governance changes are prospective only.

Closes

C-01, H-01, H-02, H-03, H-05 (request side), H-07, M-01, M-02, M-03. (H-06 termination stranding, M-04/M-05 reveal-side guards, and the H-04/M-06 backing model land in the next PRs of the stack.)

Shape

The settlement side is a new external library ReservationProofs, reached through Reservation so the router links exactly one library. Storage appends: reservationActionTimeout, reservationActions, walletPendingDissolution (gap 42→39, append-only). Sizes: router 6,007 B / Reservation 14,760 B / ReservationProofs 19,416 B / Bridge unchanged 22,403 B — all with ≥5 KB EIP-170 margin.

Includes a config-time backport of the upstream @openzeppelin/upgrades-core fix for foreign link-reference splices crashing unrelated proxy validations (droppable on plugin upgrade).

Tests

Full suite 3068 passing / 0 failing. New adversarial settlement suite: C-01 late-proof regression (no second refund, fraud-defeat registration, no double-settle), late-proof matrix vs newer pending generations, H-03 delay + vetoed-generation permanence, H-07 serialized dissolutions + drift fallback, H-02 fill-then-prove, acceptance-authorization timeout with late anchor settlement, post-grace stranding bound, wallet retirement through MovingFunds.

keep-core follow-up (flagged)

Proposal structs gained requestNonce and proofs now name their generation — threshold-network/keep-core#4238's coordination/assemblers must carry the authorized nonce and consult action records (gated on this ABI publishing), including the new requestReservationAcceptance/requestReservationDissolution/notifyReservationActionTimeout entry points.

Replace the single-phase reservation lifecycle (request -> prove, with all
validity checked at proof time against live state) with the two-phase,
nonce-bound state machine specified by RFC 13. Every Bitcoin action of a
reservation's life — acceptance anchor, in-kind redemption, re-anchor,
dissolution — is now an explicitly requested *generation*:

- The request increments the position's monotonic request nonce, performs
  and RESERVES every capacity/lifecycle check (global total, per-wallet
  count, target-wallet liveness, the per-wallet dissolution lock), and
  snapshots every proof- and settlement-critical parameter into a
  ReservationAction record keyed by (reservationKey, nonce). A wallet that
  signs an authorized action can never find it unprovable because state
  moved after signing.
- The SPV proof names its generation and validates exclusively against the
  record's snapshots. Reserved-redemption proofs additionally enforce the
  watchtower delay of the generation on-chain — an early-broadcasting
  wallet cannot finalize before the guardians' window closes, and a vetoed
  generation never settles (the unauthorized signature stays exposed to
  the fraud machinery).
- Timeouts write terminal, late-proof-accepting records: a Bitcoin spend
  that confirmed before its proof landed still settles — consumed
  outpoints are marked honestly spent (defeating fraud challenges against
  the honest-but-late signature) and the anchor lineage closes — but no
  Bank movement is repeated: the timeout already refunded the escrow and
  slashed the wallet. If the position's current pending redemption also
  matches the transaction, the proof must settle that generation instead
  (burning the escrow, the correct accounting); a non-matching pending
  generation is unwound (Superseded) with its escrow refunded, because
  its anchor provably no longer exists. This closes the claim double-spend
  race, where a timeout/veto racing a confirmed redemption both refunded
  the claim on Ethereum and paid the BTC, with the late proof reverting.
- Veto and objection state is keyed per generation, so objections never
  accumulate across generations and an unbanned owner's reservation is
  always processable again; the request path uses a reservation-scoped
  safety check (ban state only).
- Re-anchor and dissolution gain explicit on-chain authorization: source
  wallet in MovingFunds (permissionless) or Live with governance consent;
  target Live, distinct, capacity-reserved; the proof requires the output
  to pay the recorded target — a same-wallet re-anchor no longer exists,
  removing the re-anchor/dissolution byte-ambiguity. Dissolutions take a
  per-wallet main-UTXO action lock and snapshot the expected main UTXO;
  registry drift (a sweep landing before a no-main-UTXO dissolution's
  proof) re-registers the output for the moved-funds sweep machinery.
- Redemption requests are gated on a Live/MovingFunds wallet and the
  (snapshotted) grace bound; after grace only dissolution can be
  requested, so request/timeout cycling cannot defeat the stranding
  bound. A fee-paid generation that times out mints a single-use retry
  entitlement consumed by the fee-free retry path. A wallet with
  reservation anchors can no longer begin closing: with no main UTXO it
  enters MovingFunds to drain its anchors, and both redemption and
  dissolution timeouts slash like a pooled redemption timeout.

The settlement side lives in a new external library, ReservationProofs,
reached through Reservation so the ReservationRouter links exactly one
library. Storage appends: reservationActionTimeout, reservationActions,
walletPendingDissolution (gap 42 -> 39). Sizes: router 6,007 B,
Reservation 14,760 B, ReservationProofs 19,416 B, Bridge unchanged at
22,403 B.
…validation

@openzeppelin/upgrades-core@1.20.0's getUnlinkedBytecode probes every
linkable contract by splicing its library-link placeholders into the
bytecode being proxy-deployed. When a foreign contract's link position
lands inside that bytecode's metadata-trim boundary, the truncated
placeholder makes getVersion throw 'Bytecode is not a valid hex string'
and the whole proxy deployment fails — observed for unrelated proxy
deployments once the ReservationRouter's link references entered the
compilation set. Newer upgrades-core versions wrap each probe in
try/catch; this config-time shim applies the same per-candidate
tolerance without a dependency bump and can be dropped when the plugin
is upgraded.
Adapt the reservation suite to the authorize-then-prove flows (explicit
acceptance/re-anchor/dissolution requests, generation-named proofs,
unified action timeout, per-generation veto keying, retry entitlement)
and add an adversarial settlement suite covering: the claim double-spend
regression (timeout racing a confirmed redemption settles late with no
second refund and fraud-defeat registration), the late-proof matrix
against newer pending generations (matching generation must settle,
non-matching is unwound with its escrow refunded), on-chain watchtower
delay enforcement and the never-settling vetoed generation, the
per-wallet dissolution lock with serialized no-main-UTXO dissolutions
and the registry-drift fallback, capacity-reserved-before-signing
(fill-then-prove), acceptance authorization timeout with late anchor
settlement, the post-grace stranding bound, and wallet lifecycle
integration (reservation-holding wallets retire through MovingFunds).

The pooled WalletProposalValidator suite now fakes the Bridge from the
merged Bridge+ReservationRouter ABI (router-resident views must return
zeroed defaults rather than empty revert data), and the TIP-109
deployment tests expect the 6-library Bridge link map (Reservation
moved behind the router).
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: de26bdba-8046-41e1-b53f-f6ec23eaa1ff

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The CI slither (0.9.0) crashes with an internal AssertionError while
generating slithir for qualified cross-library event emissions
(emit Reservation.X(...) from ReservationProofs) — reproduced locally
against the pinned version. Declare the settlement-side events in the
library that emits them instead; the router keeps re-declaring the full
set for the ABI, so nothing changes for off-chain consumers. Annotate
the new reentrancy-events findings (all behind trusted-Bank refunds in
the late-settlement unwind path) and the deliberate enabled-timestamp
strict-equality check, matching the repository's existing conventions.

Also make the upgrades-core validation shim loop-free (the repo's
eslint config forbids for-of) and fix a template-literal-without-
interpolation lint error in the router test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant