Repository navigation
chore(bridge): milestone-1 UTXO reservations stack tracker (reservations-upgrade -> dev) - #1116
piotr-roslaniec wants to merge 122 commits into
Conversation
Declarations only, no logic. This is the structural root of the milestone 1 reservation stack: every later PR rebases onto it, so nothing after this needs a storage-layout change while reservations are live. Adds a declarations-only `Reservation` library (two term constants, three enums, the `ReservationRequest` and `ReservationAction` structs), the `PendingReservedDeposit` struct, and 28 reservation fields in `BridgeState.Storage`, from three sources. Twenty-four fields and both structs come from PR #1096, the furthest tip of the reference lineage, copied field-for-field so no later PR needs to move a position. Three declarations come from PR #1102 and ship declare-only, never written or read in milestone 1: `ReservationRequest.cumulativeReanchorFee`, `maxCumulativeReanchorFee` and `reservationDissolutionTxMaxFee`. Declaring them without enforcing them is deliberate. The milestone 1 decision rejected the flat cumulative-fee ceiling and deferred a structural bound to post-milestone-1 work, so no milestone 1 path touches them. They are declared because the storage rule requires every field a later milestone reads to exist at milestone 1. That bites hardest on `cumulativeReanchorFee`, a position field: re-anchor settlement writes the claim down on every hop, so the original anchor value is gone afterwards and the total cannot be reconstructed from surviving state. The last two, `activeReservationsCount` and `maxActiveReservations`, are genuinely new with no extraction source, and milestone 1 both writes and reads them. They exist to convert a silent saturation cliff into a revert: occupancy is monotonic toward `liveWalletsCount * maxReservationsPerWallet`, and at saturation a re-anchor has no target wallet while an anchored wallet cannot retire, because closing requires a zero reservation count - its MovingFunds clock then expires and the timeout seizes operator stake. The enforcement that reads them belongs to the acceptance path and the router view, not here. The 28 fields consume 16 slots, so `__gap` goes from 48 to 32. The figure is derived from Solidity's packing rules and confirmed by the compiler: `Bridge`'s total storage footprint measures 4,128 bytes, exactly equal to the deployed TIP-109 implementation artifact's, which is the check `assertStorageUpgradeSafe` alone does not make. `cumulativeReanchorFee` sits inside a mapping value and costs no `Storage` slot; the two fee parameters pack into one new slot, and the two new `uint32` counters fit in that same slot's free tail. Ports #1096's storage-layout upgrade-safety test and hoists solc's `storageLayout` output selection into a shared constant so per-contract compiler overrides cannot drift from it.
Extracted from PR #1094, the furthest milestone-1-scoped tip. Adds the request side (vault set, deposit revealed, wallet Live, wallet binding, nonce increment), the proof side (SPV validation, deposit consumption, event emission) and the settlement path that writes the position and mints through the vault the deposit was revealed to. Scope is the transitive call closure of acceptance, computed with the proof dispatcher excluded as a traversal edge: `actionKey`, `getAction`, `submitReservationProof`, `requestReservationAcceptance`, `addWalletReservationKey`, `removeWalletReservationKey` and `strandReservation` in `Reservation.sol`, plus nine handlers in the new `ReservationProofs.sol`. Every member is byte-identical to #1094 apart from the dispatcher. `strandReservation` and `strandLateSettlementIfTargetWalletClosed` land here rather than with the timeout and stranding PR: `settleAcceptance` calls the stranding hook directly, so the helpers cannot arrive later than their caller. `WalletProposalValidatorConstants.sol` comes along because the request path reads its three timing constants and the file does not exist on the base. The dispatcher keeps its four-member `ProofType` enum, whose numbering is a documented invariant against `Reservation.ActionType`, but implements only the acceptance branch and rejects the rest explicitly. That is milestone 1 behavior rather than a placeholder: m1 ships create, custody and re-anchor only, so redemption and dissolution proofs must be refused for the whole milestone. Its main-UTXO parameter is left unnamed, keeping the type so the selector is unchanged, because only the dissolution handler ever read it.
Extracted from PR #1094, the furthest milestone-1-scoped tip. Adds the re-anchor request path (`requestReservationReanchor`, `anchorUtxoHash`), the proof and settlement path (`submitReservationReanchorProof`, `prepareReservationForSettlement`, `requireCurrentSourceAnchor`, `consumeAnchor`), and the `IReservationFeeFinancer` interface the settlement path calls to finance the Bitcoin miner fee in kind. The dispatcher gains its `ProofType.Reanchor` branch. Scope is the transitive call closure of re-anchor and nothing else. Members already carried by the acceptance PR are reused, not duplicated, and are byte-identical to it apart from the dispatcher. `IReservationFeeFinancer` is the interface only; the `ReservationVault` implementation is a later PR. Not extracted from #1093: that tip predates #1094's rewrite of `submitReservationReanchorProof` and does not have `prepareReservationForSettlement` at all. No cumulative re-anchor fee cap is added. #1094 accumulates no cumulative fee and enforces no ceiling, and that absence is a deliberate milestone 1 decision: the flat ceiling was rejected and a structural bound deferred to post-milestone-1 work. The two storage fields stay unwritten and unread. The per-hop bounds #1094 does carry, the snapshotted `action.txMaxFee` and the dust floor, come across faithfully, as does the governance gate that makes the accepted exposure tolerable: rotating a `Live` wallet's anchor requires governance, so a Byzantine custodian cannot drive the grind alone. The dispatcher's main-UTXO parameter stays unnamed: re-anchor takes five arguments and does not read it either, so dissolution remains its only consumer.
Extracted from PR #1094. Adds `notifyReservationActionTimeout`, which writes the terminal `TimedOut` record that still accepts a late proof, releases the capacity and locks reserved at request time, and propagates wallet consequences; plus `notifyStaleReservedDeposit` and `notifyReservationStranded`. Single file. `strandReservation` and `strandLateSettlementIfTargetWalletClosed` already landed with the acceptance PR, because its settlement path calls the stranding hook directly and the helpers cannot arrive later than their caller. This PR is the three notify entry points and the three events they emit. Not extracted from #1091, which the plan originally named for the timeout path: #1091 predates the `reservationGracePeriod` to `reservationDissolutionDelay` rename and is the only source whose storage name is absent from the milestone 1 layout, so its text would not compile. Adds `using Wallets for BridgeState.Storage`, which the timeout path needs for `self.notifyWalletRedemptionTimeout`, and `../bank/Bank.sol` for the escrow refund through `self.bank.transferBalance`. #1094 carries both.
Extracted from PR #1094 into m1/router-minimal, stacked on m1/timeout-and-stranding. ReservationRouter.sol + IReservationBridge.sol keep the eight m1 entry points and eleven m1 views from #1094, drop redemption / dissolution / veto / renewal / walletPendingDissolution, and add the genuinely new activeReservationsCount view. Also extracts Reservation.updateReservationParameters and updateReservationCaps (called by the router, not previously on the stack). updateReservationCaps gains maxActiveReservations (> 0), the §4.1 launch-gate parameter. Wires activeReservationsCount with the same request-time reserve pattern as walletReservationsCount: - increment + check at requestReservationAcceptance - decrement on acceptance timeout, superseded acceptance unwind, and strandReservation - restore on late-settle re-take and stranded-position reconstruct Decision 1 from m1-b-implementation.md §4.1 (amount-cap vs slot-capacity relational check vs runbook gate) is defaulted to the runbook gate and documented in the setter natspec / PR description pending manager confirmation (`agent-docs/m1/step-04-b-decisions-for-manager.md`). Verified: 184 Solidity files compile, yarn format exit 0, layout test passing, Bridge 23,939 B unchanged, ReservationRouter 6,928 B.
Decision 1 of the milestone 1 reservation work: ship the on-chain relational check tying reservationMaxTotalAmount to slot capacity, closing the saturation cliff documented in m1-b-implementation.md §4.1. In updateReservationParameters, the relational require guards the parameters setter against raising reservationMaxTotalAmount past maxActiveReservations * reservationMaxSingleAmount. Either operand set to zero disables that side of the product (the parameters setter reads self.reservationMaxSingleAmount and self.maxActiveReservations from storage, owned by the caps setter); both zero short-circuits only the pre-launch bootstrap state. In updateReservationCaps, the relational require guards the caps setter against lowering maxActiveReservations or reservationMaxSingleAmount below what the standing reservationMaxTotalAmount requires. The maxActiveReservations == 0 disjunct is intentionally absent here because the require(maxActiveReservations > 0) at the top of updateReservationCaps makes that branch dead. contracts/test/ReservationCapsStub.sol exposes both governance setters without the router's access guard, so the launch-gate invariant is unit-testable today (the router is Governable with no initializer, so standalone deployment hits governance == address(0) for every setter call). test/bridge/Bridge.ReservationCaps.test.ts covers the manager-specified cases plus a bootstrap-ordering scenario: parameters setter is permissive until the caps setter has configured either operand, after which any raise past slot capacity reverts. Storage layout unchanged. Co-authored-by: agent <agent@example.com>
…or the deploy-ordering hazard The previous 'requires caps to be set before a large reservationMaxTotalAmount sticks' test assumed step 3 (updateReservationCaps) would succeed and step 4 (a follow-up updateReservationParameters) would revert. Running the suite with FORKING_URL unset (the blocker previously reported as environment breakage was actually a stale shell var triggering the fork provider path) showed step 3 itself reverts: the mirrored check reads the already-oversized total against the new cap product and trips. This is the two-sided Decision 1 check working as designed — and a genuine deploy-ordering hazard worth pinning in a regression test. Rewrites the bootstrap-ordering describe block to: - 'reverts when updateReservationCaps follows an oversized parameters write': asserts the hazard. A deploy that calls parameters first with a large total, then caps, sees the caps call revert. The test pins this exact sequence. - 'accepts caps first, then a total within slot capacity': asserts the safe operational path. updateReservationCaps passes (total defaults to 0, so the mirrored check is trivially true), then a total within the resulting product sticks, then any follow-up raise past slot capacity reverts. Updates the describe-level docstring to describe the actual property: updateReservationParameters is permissive in the bootstrap window, updateReservationCaps is the gate, and the safe order is caps first. No production contract changes. Storage layout unchanged. Co-authored-by: agent <agent@example.com>
Introduces the ReservationVault contract as the liability-side companion of the Bridge's Reservation library. The vault handles: - Bank-side crediting when the Bridge proves a reservation's anchor tx (receiveBalanceIncrease), minting TBTC gross and collecting the initiation fee - In-kind fee financing for settled re-anchor and dissolution transactions (financeInKindFee), burning TBTC from the fee reserve in lockstep with the Bitcoin backing - Outstanding in-kind fee debt repayment (repayInKindFeeDebt) - Governance-controlled fee parameters (initiationFeeBps, extensionFeeBps, redemptionFeeBps) and reserve target (feeReserveTarget) - Governance-controlled fee sweeping to the Bridge treasury The redemptionsPaused flag ships with constructor-default true and is flipped by pauseRedemptions/unpauseRedemptions (onlyOwner). This is a purely cosmetic flag in m1: the redemption entry points (redeemReservation, retryRedeemReservation) are m2-only and were dropped per the Option B scope decision (manager-to-implementor-2026-08-25-pr-f-scope-decision.md). The flag exists ready for m2 wiring; governance activates by calling unpauseRedemptions as part of the activation ceremony. The renewal-side functions (pauseRenewals, unpauseRenewals, blockRenewal, unblockRenewal, setRenewalGuardian, extendCustody) and the renewalsPaused flag were also dropped per the Option B refinement, since the renewal path that consumes them is absent in m1. Fee-financing surface (financeInKindFee, repayInKindFeeDebt, inKindFeeDebtSat, updateFeeReserveTarget) is present and ungated, as required by PR #B's re-anchor settlement path. Co-authored-by: agent <agent@example.com>
Declarations only, no logic. This is the structural root of the milestone 1 reservation stack: every later PR rebases onto it, so nothing after this needs a storage-layout change while reservations are live. ### What lands | File | Change | |---|---| | `contracts/bridge/Reservation.sol` | new, +267 - declarations-only library | | `contracts/bridge/BridgeState.sol` | +169/-1 - `PendingReservedDeposit`, 28 storage fields, `__gap` | | `test/bridge/Bridge.StorageLayout.test.ts` | new, +96 - upgrade-safety test | | `hardhat.config.ts` | +10 - hoisted `storageLayout` output selection | `Reservation.sol` carries two term constants, three enums (`ReservationState`, `ActionType`, `ActionState`) and two structs (`ReservationRequest`, `ReservationAction`). No functions, no events. It compiles to 86 bytes of metadata and adds no deployed code. `BridgeState.Storage` gains 28 reservation fields plus the `PendingReservedDeposit` struct. Everything before the import block and everything after `__gap` is byte-identical to the base branch: no event, no governance setter, no behavior. ### Where each declaration comes from Twenty-four of the storage fields and both structs come from PR #1096, the furthest tip of the `#1091`-`#1096` lineage, copied field-for-field and verified byte-identical across the whole `imports`-through-`__gap` region. That includes #1096's three declare-only fields (`ReservationAction.isPartial`, `retryCreditSourceNonce`, `BridgeState.Storage.reservationRetryCreditActionNonce`). Independently checked: all 56 pre-existing `Storage` fields appear in #1096 in the same relative order, with none removed or retyped, and #1093's 77 fields are a subset of what ships here - so the acceptance and re-anchor extractions that follow compile against this layout. Three declarations come from PR #1102: `ReservationRequest.cumulativeReanchorFee`, `BridgeState.Storage.maxCumulativeReanchorFee` and `BridgeState.Storage.reservationDissolutionTxMaxFee`. The last two, `activeReservationsCount` and `maxActiveReservations`, have no extraction source at all - they are genuinely new, and unlike the #1102 three, milestone 1 both writes and reads them. See below. ### Why these three declarations have no milestone 1 enforcement No milestone 1 path *reads* any of the three yet - no ceiling or dedicated cap is enforced. `cumulativeReanchorFee` is already *written*: PR #1108 (`feat(bridge): implement reservation re-anchor`) settles it on every hop (`reservation.cumulativeReanchorFee += minerFee` in its `ReservationProofs.sol`). `maxCumulativeReanchorFee` and `reservationDissolutionTxMaxFee` are genuinely untouched across the whole currently-open milestone 1 stack (#1107-#1112) - neither written nor read anywhere in any of the six branches. The milestone 1 decision rejected the flat cumulative-fee ceiling, left the absolute ceiling unbounded, and deferred a structural bound to post-milestone-1 work; the resulting exposure is pinned by the characterization test on tbtc-v2 #1104, which fails if an absolute ceiling is ever ported. So the *enforcement* is deliberately absent for all three, even though `cumulativeReanchorFee` is not write-inert. The *declarations* still ship, because the storage rule requires every field a later milestone reads to exist at milestone 1 - a field cannot be introduced while reservations are live. That bites hardest on `cumulativeReanchorFee`, a position field: re-anchor settlement writes the claim down on every hop, so the original anchor value is gone afterwards and the cumulative total cannot be reconstructed from surviving state. If it is not declared now, no later milestone can compute it at all. Declaring without enforcing is the whole point here, and the comments on all three fields say so at the declaration site. **Considered and declined: dropping `maxCumulativeReanchorFee` and `reservationDissolutionTxMaxFee`.** Unlike `cumulativeReanchorFee`, these two are stateless governance configuration values with no historical-data constraint - checking all six currently-open milestone 1 PRs directly confirms neither is read or written anywhere in the stack; #1112's governance-parameters file only mentions them in a comment explaining their *absence* from its update function. That means, unlike `cumulativeReanchorFee`, both really could be appended later via the same `__gap` mechanism this PR itself uses, live reservations or not - the "must declare before reservations go live" constraint doesn't bind them the way it binds the accumulator. Raised here rather than acted on: `maxCumulativeReanchorFee`'s comment (25477d5) is the author's own deliberate choice to keep-and-document over remove, made one commit before this note. If that call still stands after seeing this, no action needed; if not, dropping both now (before any sibling PR rebases onto this layout) is cheap - it was verified compile-clean against all six. ### Two fields with no extraction source `activeReservationsCount` and `maxActiveReservations` are new work required by the milestone 1 launch gates. Occupancy is monotonic toward `liveWalletsCount * maxReservationsPerWallet`, because no milestone 1 path frees a wallet slot short of wallet termination. At saturation a re-anchor has no target wallet, and an anchored wallet cannot retire because closing requires a zero reservation count - its MovingFunds clock then expires and the timeout seizes operator stake. The cap turns that silent cliff into a revert. Only the declarations are here. The enforcement that reads them - the acceptance-time check and the counter's increment and decrement sites - is new milestone 1 work belonging to the acceptance path, and the paired router view belongs to PR #B. One sub-decision is still open in the spec: whether to add a relational check tying `reservationMaxTotalAmount` to slot capacity, or to emit both sides and make it a runbook gate. **Spec gap to close before governance sign-off:** the frozen milestone-1 parameter spec's own sign-off ledger has no row for `maxActiveReservations` under any name. The two nearest rows (`reservationMaxTotalAmount`'s global amount cap, `maxReservationsPerWallet`'s per-wallet count) are explicitly different axes per this field's own doc-comment ("neither bounds how many positions exist across all wallets"). Governance could sign off on the frozen spec exactly as written and never have decided this cap's launch value - needs a launch-value row and a sign-off checkbox added to the spec before milestone-1 sign-off proceeds. ### `__gap`: 48 to 33, derived not copied The 28 fields consume 15 slots. `cumulativeReanchorFee` sits inside a mapping value, so it costs no `Storage` slot at all. The other 27 fields are grouped scalars-first, then mappings, to pack tightly instead of interleaving: two small fields pack into the free tail of `address rebateStaking` (no new slot); five further slots hold the remaining value types, two of them fully packed at 32/32 bytes (`reservationVault`+`reservationTxMaxFee`+`reservationDissolutionDelay`, and `maxReservationsAmountPerWallet`+`reservationMaxSingleAmount`+ `pendingReservedDeposits`+`maxCumulativeReanchorFee`); the ten mapping fields take one slot each. `assertStorageUpgradeSafe` passing is necessary but not sufficient - it checks that existing fields did not move, not that the gap was shrunk by the right amount. The decisive check is the total footprint: `Bridge` measures **4,128 bytes / 129 slots, exactly equal to the deployed TIP-109 implementation artifact's**. The reservation fields consumed precisely the slots the gap gave up. ### EIP-170 `Bridge` compiles to 23,939 bytes at the base `runs: 1000`, 637 bytes under the 24,576 limit - unchanged from the base branch, since this PR adds no code. #1096's `Bridge.sol` optimizer override (`runs: 100`) is therefore **not** ported; it exists there to pay for the router logic, which is PR #B's scope, and porting it now would be an unrelated optimizer change. ### Verification Run the format gate first. The test run creates a gitignored `gasReporterOutput.json`, and `yarn format` includes `prettier --check .`, which then fails on that artifact - delete it before re-running the gate. ``` cd solidity yarn format # 0 errors, Prettier clean yarn test test/bridge/Bridge.StorageLayout.test.ts # 1 passing ``` `yarn test` already exports `USE_EXTERNAL_DEPLOY=true` and `TEST_USE_STUBS_TBTC=true`. If `FORKING_URL` is exported in your shell, prefix with `env -u FORKING_URL -u FORKING_BLOCK` or hardhat forks mainnet. Results on `7db90052`: - `yarn format` - exit 0. 49 warnings, all pre-existing; 0 errors; no tracked file rewritten. - `Bridge storage layout > is upgrade-safe against the deployed TIP-109 Bridge layout` - 1 passing. - Footprint equality against the deployed artifact - 4,128 bytes both sides. ### Known wart `solhint` reports one `ordering` warning on `Reservation.sol`: the two term constants precede the enum declarations. This is inherited - #1096's own `Reservation.sol` produces the identical warning at the identical construct (verified by linting the reference file). Reordering to silence it would make this file differ from every later PR in the stack, which is the one property this PR exists to guarantee, so it stays.
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
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. Comment |
# PR #C - `m1/acceptance-core` Ready to open, based on `reservations-upgrade` (flat, not stacked). Local and unpushed by standing instruction; the human opens the PR. | | | |---|---| | Repo | `threshold-network/tbtc-v2` | | Head | `m1/acceptance-core` (commit `ca82bc03` — 2 review-fix commits on top of `2795f9d7`) | | Base | `reservations-upgrade` — flat base, matching PRs A/B/D/E/F/G. **Diff today (GitHub-rendered, vs `reservations-upgrade`, until A merges): 6 files.** Shrinks to this PR's own work once A merges. | | Diff (this PR's own work) | 3 files, +~925 (the +1203 figure includes ~278 lines of PR #A's declarations carried untouched in `Reservation.sol`; the earlier headline was overstated by that share) | | Worktree | `/tmp/m1-c` | Title: `feat(bridge): implement two-phase reservation acceptance` --- ## The two-phase reservation acceptance path, extracted from PR #1094. Second in the corrected build order (A -> C -> D -> E -> B -> F -> G). ### What lands | File | Change | |---|---| | `contracts/bridge/Reservation.sol` | +619 - 2 events, 7 functions, imports | | `contracts/bridge/ReservationProofs.sol` | new, +572 - 4 events, `ProofType`, 9 functions | | `contracts/bridge/WalletProposalValidatorConstants.sol` | new, +12 | Request side: vault set, deposit revealed, wallet `Live`, wallet binding, nonce increment. Proof side: SPV validation, deposit consumption, event emission. Settlement: writes `expiresAt`, `dissolutionEligibleAt`, `mintedAmount`, `anchorAmount`, `state = Active`, and mints through the vault. `WalletProposalValidatorConstants.sol` is copied verbatim, 12 lines, because the request path reads its three timing constants and the file does not exist on the base branch. It is *not* `WalletProposalValidator.sol`, which already exists and stays with PR #G — but `WalletProposalValidator` now sources its three duplicated timing constants from this file (values unchanged), so the two copies cannot drift. ### Extracted from #1094, not #1093 Worth stating plainly, because the plan originally said #1093 and this PR was first built that way before being rebuilt. #1094 is not purely additive over #1093. It rewrites the acceptance path with real milestone 1 content: | Member | #1093 | #1094 | What #1094 adds | |---|---|---|---| | `requestReservationAcceptance` | 153 L | 163 L | wallet-binding guard - only the wallet a deposit was revealed for may be authorized as its custodian | | `consumeAcceptedDeposit` | 26 L | 33 L | double-consume guard on the pending marker, plus the `pendingReservedDeposits` decrement | | `settleAcceptance` | 88 L | 135 L | unwinds a newer pending generation's reserved capacity, which the source describes as a permanent leak otherwise; routes a late settlement through the deposit's immutable `vault` rather than the live `reservationVault`; calls the stranding hook | Extracting from #1093 would have shipped milestone 1 with a security guard and two correctness fixes missing. #1094 is the furthest milestone-1-scoped tip, which is the same rule PR #A followed when it took the storage layout from #1096. #1096 is deliberately *not* used for code here: it edits three of these members again and deletes `requireCurrentSourceAnchor`, all partial-redemption (milestone 2) work. It contributes declare-only fields to PR #A and nothing else. ### Scope is a computed closure The transitive call closure of acceptance, computed over #1094 with the proof dispatcher excluded as a traversal edge: - `Reservation.sol`: `actionKey`, `submitReservationProof`, `getAction`, `requestReservationAcceptance`, `addWalletReservationKey`, `removeWalletReservationKey`, `strandReservation` - `ReservationProofs.sol`: `submitReservationProof`, `loadSettleableAction`, `strandLateSettlementIfTargetWalletClosed`, `submitReservationAcceptanceProof`, `consumeAcceptedDeposit`, `validateAnchorOutput`, `settleAcceptance`, `unwindPendingAction` Every member matches #1094's logic apart from the dispatcher and two deltas disclosed below under "Review fixes applied on top of the extraction" (the depositor-only caller gate and the occupancy cap): independently diffed against #1094's actual branch (`origin/feat/utxo-reservation-guards`), which has neither. PR #A's declarations are untouched, verified block by block. `strandReservation` and `strandLateSettlementIfTargetWalletClosed` land here rather than with PR #E, where the plan first put them: #1094's `settleAcceptance` calls the stranding hook directly, so the helpers cannot arrive later than their caller. `addWalletReservationKey` and `removeWalletReservationKey` do not exist at #1093 at all and land here for the same reason. PR #E is reduced to three notify functions. The two files import each other. That is #1094's structure, and it is why #C, #D and #E all add to the same compilation unit rather than getting a file each. ### The proof dispatcher rejects what milestone 1 does not settle `ProofType` keeps all four members - its numbering is a documented invariant against `Reservation.ActionType` minus `None`, so trimming it would shift values later PRs depend on. Only the acceptance branch is implemented; the rest revert with `"Unsupported reservation proof type"`. Out-of-range values now hit the same revert (explicit range check before the enum cast) instead of an enum-panic. This is milestone 1 behavior, not a placeholder: m1 ships create, custody and re-anchor only, so redemption and dissolution proofs must be refused for the whole milestone. PR #D adds the re-anchor branch ahead of that rejection. The dispatcher's fourth parameter, the main UTXO, was only ever read by the dissolution handler, so its name is omitted - the canonical way to say "intentionally unused" - while the type stays, keeping the selector identical to #1094's so neither the router nor the regenerated client bindings change when a handler starts consuming it. Its `@param` line was removed, since natspec for an unnamed parameter is invalid, and replaced with a `@dev` note. `unwindPendingAction` likewise keeps its Redemption/Reanchor/Dissolution branches even though only the Acceptance branch is reachable in production this milestone (the sole action-creating function in this diff always writes `ActionType.Acceptance`). This is deliberate extraction fidelity, not dead code left by accident: those branches are retained now so #D/#E/#F don't have to re-add them, and they are exercised directly by the test harness's action-injection setters even though no production caller can reach them yet. ### Prerequisites this PR names but does not ship (reachability) Nothing in this PR's tree calls the acceptance library yet; the path becomes callable through three pieces that land in other PRs of the series: 1. **Reveal-side producer** (with PR #B / Deposit changes): the reveal path writes `pendingReservedDeposit[reservationKey]` and increments `pendingReservedDeposits`. Without it every request reverts on the `"Deposit was not revealed as reserved"` guard, the acceptance proof reverts on `"Pending reserved deposit does not exist"`, and the `pendingReservedDeposits -= 1` in `consumeAcceptedDeposit` has no matching increment (would revert on first execution). The harness tests stub this producer and document its contract. 2. **`updateReservationParameters` setter** (governance): reserves, `reservationMinAmount`, `reservationTxMaxFee`, `reservationActionTimeout`, `maxReservationsPerWallet`, `maxReservationsAmountPerWallet` and `activeReservationsCount`'s cap `maxActiveReservations` all start at zero; the per-wallet hard cap reverts every request until governance sets it. The occupancy cap is wired 0-disables (zero = disabled) so the path is live before the setter lands. 3. **Router wiring** (PR #B): `reservationRouter` must be set and the Bridge fallback pointed at it before any EOA can reach the library functions. The acceptance request is caller-gated to the deposit's depositor, so once wired, no third party can request (and permanently lock) acceptance for someone else's revealed deposit. 4. **Wallet-lifecycle integration** (later PR, not yet scheduled in this series): a wallet holding reservation anchors or pending reservation actions should not be able to begin closing, and moving-funds completion should require its reservation count to be zero. No code in this branch enforces that today - `Wallets.sol`/`MovingFunds.sol` have no reservation-awareness at all, since this PR doesn't touch those files. Flagged explicitly in `BridgeState.sol`'s `maxActiveReservations` comment as a required gate before the vault is wired live. ### Declare-only storage additions Six `Storage` fields are declared with no read or write anywhere in this diff, appended into former `__gap` space (`48 -> 31` slots) because a field a later milestone reads cannot be added while reservations are live: - `reservationRouter` (address) - set once by the router-wiring PR. - `reservationRenewalWindowSeconds` (uint32) - renewal-window length, later milestone. - `reservationRetryCreditActionNonce` (mapping) - redemption retry-credit bookkeeping, later milestone. - `maxCumulativeReanchorFee` (uint64) - per-reservation re-anchor fee ceiling, later milestone. - `reservationDissolutionTxMaxFee` (uint64) - dissolution fee bound, later milestone. - `reservationMaxBackingFractionBps` (uint256, full slot, deliberately not packed): RFC 13's relative backing cap, declared, unused until the reservation-vault PR wires the Bank backing total. ### Verification ``` cd solidity yarn hardhat compile yarn format yarn test test/bridge/Bridge.StorageLayout.test.ts test/bridge/Reservation.test.ts test/bridge/ReservationProofs.test.ts ``` - Compiles clean, no solc warnings. - `yarn format` exit 0. - Storage layout still upgrade-safe against the deployed TIP-109 artifact (including the appended `reservationMaxBackingFractionBps` + gap shrink), verified with `unsafeAllowCustomTypes=true` (the deployed artifact predates solc's enum-member output, so struct/enum member comparison is skipped; concrete slots, packing and gap size remain checked). - **Behavioral tests now land in this PR** via library-harness stubs (`contracts/test/TestReservation.sol`, `TestReservationProofs.sol`, following the `TestBitcoinTx` pattern; the harness writes the reveal-side producer state so the acceptance closure is executable before the router exists). Suite covers: depositor-only caller gate, window math (+/-1s) with the on-chain 24h refund margin, occupancy cap + all four capacity caps, designated-wallet binding, amount floor, proof-type range, double consume, strand dedup, swap-remove, late-settle unwind. - `solhint`: two `ordering` warnings, on `Reservation.sol` and `ReservationProofs.sol`, inherited from linting #1094's own copies. ### Review fixes applied on top of the extraction Round of fixes from a multi-agent review (two adversarial validation passes): - `requestReservationAcceptance` is caller-gated to the deposit's depositor. - Occupancy (`activeReservationsCount` / `maxActiveReservations`) is wired with 0-disables enforcement; decrements in `strandReservation` and `unwindPendingAction`'s acceptance branch. - The signing window now closes at least `DEPOSIT_REFUND_SAFETY_MARGIN` before the refund locktime, not at the exact deadline. - `WalletProposalValidator`'s duplicated timing constants now alias `WalletProposalValidatorConstants`. - `storageLayoutOutputSelection` hoisted into the governance compiler override. - Docs/hygiene: requirements list made exhaustive, stack-local PR references made durable, capacity-field comments reworded for the pending phase, duplicate wallet-binding require removed, RFC 13 citations scoped. --------- Co-authored-by: agent <agent@example.com> Co-authored-by: claude-fix <claude-fix@local>
Extracted from PR #1094, the furthest milestone-1-scoped tip. Adds the re-anchor request path (`requestReservationReanchor`, `anchorUtxoHash`), the proof and settlement path (`submitReservationReanchorProof`, `prepareReservationForSettlement`, `requireCurrentSourceAnchor`, `consumeAnchor`), and the `IReservationFeeFinancer` interface the settlement path calls to finance the Bitcoin miner fee in kind. The dispatcher gains its `ProofType.Reanchor` branch. Scope is the transitive call closure of re-anchor and nothing else. Members already carried by the acceptance PR are reused, not duplicated, and are byte-identical to it apart from the dispatcher. `IReservationFeeFinancer` is the interface only; the `ReservationVault` implementation is a later PR. Not extracted from #1093: that tip predates #1094's rewrite of `submitReservationReanchorProof` and does not have `prepareReservationForSettlement` at all. No cumulative re-anchor fee cap is added. #1094 accumulates no cumulative fee and enforces no ceiling, and that absence is a deliberate milestone 1 decision: the flat ceiling was rejected and a structural bound deferred to post-milestone-1 work. The two storage fields stay unwritten and unread. The per-hop bounds #1094 does carry, the snapshotted `action.txMaxFee` and the dust floor, come across faithfully, as does the governance gate that makes the accepted exposure tolerable: rotating a `Live` wallet's anchor requires governance, so a Byzantine custodian cannot drive the grind alone. The dispatcher's main-UTXO parameter stays unnamed: re-anchor takes five arguments and does not read it either, so dissolution remains its only consumer.
- allow re-anchor requests from a Closing source wallet so a reservation never permanently pins a retiring wallet (P0) - reorder submitReservationReanchorProof so the in-kind fee financing call runs last, after every state effect commits (CEI) - add notifyReservationActionTimeout to release a pending re-anchor's target-wallet capacity and restore Active on timeout, matching the late-settlement accounting that already assumed it existed - require a signing window on requestReservationReanchor, mirroring acceptance's validator-signable-window check - wire BridgeState.activeReservationsCount: increment on first acceptance settlement, enforce the occupancy cap on request - strand an on-time re-anchor settlement whose target wallet retired mid-flight, matching the existing late-settlement safety net
Fix all 34 confirmed findings from a multi-lens review of the reservation re-anchor/acceptance surface: - Reservation.sol: reserve/release activeReservationsCount symmetrically across request, settlement, timeout, and strand; widen notifyReservationActionTimeout to cover Acceptance timeouts (releasing to Unknown, not Active, since no anchor was ever proven); harden it against reentrant resurrection; unify the zero-means-disabled cap convention; correct false/stale NatSpec (RFC-13 citation, scope claims, permissionless re-anchor path, ReservationState.Closed conditions, requirements lists, append-only struct discipline). - ReservationProofs.sol: extend strandIfTargetWalletClosed to Terminated wallets; bind settlement to the reservation's current generation nonce unconditionally (was only checked on the non-late path); reorder settleAcceptance so stranding runs before the external bank call (CEI); re-increment activeReservationsCount on late acceptance settlement to match the timeout release; guard against an out-of-range ProofType cast; correct false/stale NatSpec throughout. - BridgeState.sol: drop 5 milestone-1 dead fields with no reader (reservationRenewalWindowSeconds, walletPendingDissolution, reservationRetryCreditActionNonce, maxCumulativeReanchorFee, reservationDissolutionTxMaxFee), restoring their slots via __gap (verified against the deployed TIP-109 layout via Bridge.StorageLayout.test.ts); correct false/stale comments. - Remove the walletReservationKeys/walletReservationKeyIndex enumeration subsystem: no on-chain consumer, replaceable by off-chain event reconstruction; frees 3 more __gap slots. - WalletProposalValidator.sol: reference WalletProposalValidatorConstants instead of declaring separately-drifting duplicate timing constants. - hardhat.config.ts: apply storageLayoutOutputSelection to every compiler override block, not just the main one. - Add contracts/test/TestReservation.sol plus test/bridge/Reservation.test.ts covering actionKey, anchorUtxoHash, parseSingleOutput, and validateAnchorOutput, previously untested despite being security-load-bearing. Verified: yarn build clean, yarn lint 0 errors, full yarn test suite 3030 passing / 0 failing, Bridge.StorageLayout.test.ts passes against the deployed TIP-109 layout.
Addresses 34 of the 38 confirmed findings from the m1/reanchor-core review round (2 P3 perf/dedup suggestions reverted after re-review found they contradicted documented behavior or had unconfirmed benefit; storage-packing finding resolved as a side effect of the dead-field removal below). P0: notifyReservationActionTimeout decremented capacity under the wrong storage key (reservation.walletPubKeyHash, always zero at that point) instead of action.targetWalletPubKeyHash, causing every acceptance-timeout call to underflow-revert and permanently leak capacity. P1: loadSettleableAction over-strict nonce check made late-settlement unwind branches unreachable dead code; moved the on-time-only check into settleAcceptance. Re-anchor wallet-count cap was missing the zero-disables-the-cap sentinel every sibling cap uses, bricking re-anchor under default configuration. Added a permissionless notifyReservationStranded entry point for Terminated wallets with no settlement in flight. P2: restored the missing activeReservationsCount release on stranded- position reconstruction; added a re-anchor cooldown after timeout to bound repeat-griefing; mirrored the on-time strand check from re-anchor settlement into acceptance settlement; removed a duplicated require justified by a false extraction-provenance claim; removed seven redemption-only struct fields and the dead Redemption branch in unwindPendingAction (nothing in this PR can construct that action type); removed the write-only reservationsByAnchorUtxo reverse index. P3: added a request-time amount floor for re-anchor; verified the in-kind fee interface NatSpec accurately describes its no-return-value partial-burn design; fixed several false-provenance and misplaced comments; aligned NatSpec overclaims with actual settlement behavior; named PR #B explicitly in forward references that previously pointed at nonexistent names; added a minerFee field to ReservationReanchored; fixed a revert-string that named an already-rejected wallet state as acceptable.
Adds TestReservation.sol wrappers and Reservation.test.ts cases closing the confirmed test-coverage gap over the functions that carried the P0/P1/P2 bugs fixed in the prior commit: - requestReservationAcceptance, notifyReservationActionTimeout, strandReservation, loadSettleableAction: happy path, revert reasons, and exact post-call counter/state assertions, including regression coverage for the wrong-storage-key underflow and the over-strict nonce check this review round fixed. - requireCurrentSourceAnchor, strandIfTargetWalletClosed: matching and mismatched anchor / wallet-state cases. - validateAnchorOutput: added the fee-bound equality boundary and the amount-underflow case alongside the existing tests. - Bridge.StorageLayout.test.ts: added an explicit allowlist assertion so unsafeAllowCustomTypes can only ever cover the two known-legacy enums, not silently absorb a future one. Fixes BridgeState.sol's storage __gap (38 -> 39): removing the write-only reservationsByAnchorUtxo mapping in the prior commit freed one slot, so the gap must grow by one to keep the 48-slot pre-upgrade total reconciled; caught by the new StorageLayout allowlist test. requestReservationReanchor and consumeAnchor remain untested: the former needs a full cooldown/timing/cap precondition chain and the latter needs a valid Bitcoin input-vector fixture, both beyond a thin wrapper in this pass.
A cross-check against the full 38-item confirmed findings list found two that the prior commits had not actually addressed: - ReservationProofs.sol:42-45's deferred-conformance NOTE named reservationVault as the address needing a future probe, but the actual untrusted call reaches deposits[reservationKey].vault (the deposit's immutable vault, pinned at request time). Restated the NOTE in terms of the address the code actually calls. - strandLateSettlementIfTargetWalletClosed and prepareReservationForSettlement had wrappers but no test coverage across their wallet-state / late / evidenceAlreadyEmitted branches, including the exact branches where the on-time/late Terminated asymmetry and the activeReservationsCount restore (both fixed earlier in this review round) live. Added 8 cases covering each branch with exact post-call state/counter assertions.
…erge Rebasing m1/reanchor-core onto reservations-upgrade merged the reanchor work with upstream's two-phase acceptance implementation. Fixing issues the merge surfaced that neither side's tests alone could catch: - ReservationProofs.unwindPendingAction was missing its restoreRetryCredit parameter (test-only wrapper already expected it); threaded it through both call sites, restoring credit only on the late-reanchor unwind path per the retryCredit field's documented contract. - submitReservationReanchorProof called strandLateSettlementIfTargetWalletClosed with a stale 5-arg signature (a leftover late-gated design); the function is unconditional now, so dropped the extra argument and matched the harness wrapper. - TestReservation.sol's setDeposit/setPendingReservedDeposit overloads broke ethers' plain-name resolution; removed the unused struct-based setDeposit and renamed the positional setPendingReservedDeposit. - Updated strand-function tests that encoded the abandoned late-gated semantics, a stale revert-string assertion, and a test invoking the reanchor-only timeout notifier for an acceptance action instead of notifyReservationAcceptanceTimedOut. - ReservationProofs.test.ts (untouched, upstream-authored) was missing the reanchorCooldownUntil field the merged struct added, and needed generation-nonce seeding for three on-time settlement tests now that settleAcceptance enforces current-generation matching.
The re-anchor request, proof and settlement path for UTXO reservations. ### What lands | File | Change | |---|---| | `contracts/bridge/Reservation.sol` | new library: request/authorization side | | `contracts/bridge/ReservationProofs.sol` | new library: SPV proof settlement side | | `contracts/bridge/BridgeState.sol` | new reservation storage fields | | `contracts/bridge/WalletProposalValidatorConstants.sol` | new shared timing constants | | `contracts/vault/IReservationFeeFinancer.sol` | new interface, no implementation yet (lands with PR #F) | | `contracts/test/TestReservation.sol` | new test harness for pure/internal library functions | Request side: `requestReservationAcceptance`, `requestReservationReanchor`, `notifyReservationActionTimeout`, `strandReservation`, plus the `actionKey`/`anchorUtxoHash` helpers. Proof and settlement: `submitReservationAcceptanceProof`, `submitReservationReanchorProof`, `prepareReservationForSettlement`, `requireCurrentSourceAnchor`, `consumeAnchor`, `settleAcceptance`, `unwindPendingAction`, `strandIfTargetWalletClosed`, `strandLateSettlementIfTargetWalletClosed`. `IReservationFeeFinancer.sol` is the interface the settlement path calls to finance the Bitcoin miner fee in kind. It has to be here because `ReservationProofs.sol` imports it; the `ReservationVault` implementation stays with PR #F. ### Scope correction An earlier version of this description claimed the acceptance/settlement surface was extracted byte-identical from PR #1094 and that functions such as `actionKey`, `getAction`, `loadSettleableAction`, `parseSingleOutput`, `unwindPendingAction`, `strandReservation` and `strandLateSettlementIfTargetWalletClosed` were "already on the base from PR #C and reused, not duplicated." That claim was false and has been corrected here after review: this branch's actual git base (`origin/reservations-upgrade`) does not contain `Reservation.sol`, `ReservationProofs.sol`, or `WalletProposalValidatorConstants.sol` at all (`git show origin/reservations-upgrade:solidity/contracts/bridge/Reservation.sol` fails; `git ls-tree origin/reservations-upgrade solidity/contracts/bridge/` lists neither file). The full acceptance request/settlement path shipped in this PR is new on this branch, not carried forward from a prior extraction. Any future PR-stack description should be verified against the actual merge base rather than assumed from the source PR's own history. ### No fee cap, deliberately - and permissionless by design Re-anchor accumulates no cumulative fee ceiling in milestone 1: that is a deliberate scope decision, not an oversight, deferring a structural bound to post-milestone-1 work. `ReservationRequest.cumulativeReanchorFee` is written on every re-anchor hop so a future cap can read the true lifetime total, including hops that predate the cap. Review surfaced that the risk-acceptance rationale for this decision originally overstated its own governance gate: the privileged governance-only check applies only when the source wallet is `Live`. When the source wallet is `MovingFunds` or `Closing` (the common case - wallets retiring per their `walletMaxAge`/`walletClosureMinBtcBalance` policy), re-anchor is fully permissionless by design, callable by anyone. This is intentional (migration must not be blockable), but it is the dominant exposure path, not the `Live`-wallet governance gate, and both the in-code rationale and this description now say so explicitly. ### Testing `contracts/test/TestReservation.sol` plus `test/bridge/Reservation.test.ts` now cover, in addition to the previously-untested pure/internal functions (`actionKey`, `anchorUtxoHash`, `parseSingleOutput`, `validateAnchorOutput`, including the fee-bound equality boundary and the amount-underflow case): `requestReservationAcceptance`, `notifyReservationActionTimeout`, `strandReservation`, `loadSettleableAction`, `requireCurrentSourceAnchor`, and `strandIfTargetWalletClosed` -- happy path, revert reasons, and exact post-call counter/state assertions, following the repo's existing thin-wrapper pattern (`TestEcdsaLib.sol`, `TestBitcoinTx.sol`). `requestReservationReanchor` and `consumeAnchor` remain untested directly: the former needs a full cooldown/timing/cap precondition chain and the latter needs a valid Bitcoin input-vector fixture, both beyond a thin wrapper in this pass. Full control-flow integration coverage is still expected to land with the router (PR #B) and vault (PR #F). ### Verification ``` cd solidity yarn build # compiles clean, 183 files yarn lint # 0 errors (pre-existing ordering warnings only) yarn format # prettier clean yarn test # 3042 passing, 0 failing, 33 pending (pre-existing skips) yarn test:integration # 27 passing yarn deploy:test # clean dry run ``` This PR went through two internal multi-lens automated review rounds. The first round fixed 34 confirmed findings (occupancy accounting, a stale-generation settlement binding, CEI ordering, several inaccurate comments, and the scope/permissionless-path corrections above). A second, fresh-pass review round against the fixed code then found and fixed 38 more confirmed findings, including a P0 (an unconditional underflow revert in `notifyReservationActionTimeout` that permanently bricked acceptance timeouts) and four P1s (a stale-generation nonce check that made three documented late-settlement branches dead code; a re-anchor capacity cap missing the zero-disables-the-cap sentinel every sibling cap uses, which bricked re-anchor under default configuration; a missing permissionless `notifyReservationStranded` cleanup path for Terminated wallets; and the test-coverage gap that let the first two ship undetected). See the review artifacts for the full list if useful context is needed during human review.
Declarations only, no logic. This is the structural root of the milestone 1 reservation stack: every later PR rebases onto it, so nothing after this needs a storage-layout change while reservations are live. Adds a declarations-only `Reservation` library (two term constants, three enums, the `ReservationRequest` and `ReservationAction` structs), the `PendingReservedDeposit` struct, and 28 reservation fields in `BridgeState.Storage`, from three sources. Twenty-four fields and both structs come from PR #1096, the furthest tip of the reference lineage, copied field-for-field so no later PR needs to move a position. Three declarations come from PR #1102 and ship declare-only, never written or read in milestone 1: `ReservationRequest.cumulativeReanchorFee`, `maxCumulativeReanchorFee` and `reservationDissolutionTxMaxFee`. Declaring them without enforcing them is deliberate. The milestone 1 decision rejected the flat cumulative-fee ceiling and deferred a structural bound to post-milestone-1 work, so no milestone 1 path touches them. They are declared because the storage rule requires every field a later milestone reads to exist at milestone 1. That bites hardest on `cumulativeReanchorFee`, a position field: re-anchor settlement writes the claim down on every hop, so the original anchor value is gone afterwards and the total cannot be reconstructed from surviving state. The last two, `activeReservationsCount` and `maxActiveReservations`, are genuinely new with no extraction source, and milestone 1 both writes and reads them. They exist to convert a silent saturation cliff into a revert: occupancy is monotonic toward `liveWalletsCount * maxReservationsPerWallet`, and at saturation a re-anchor has no target wallet while an anchored wallet cannot retire, because closing requires a zero reservation count - its MovingFunds clock then expires and the timeout seizes operator stake. The enforcement that reads them belongs to the acceptance path and the router view, not here. The 28 fields consume 16 slots, so `__gap` goes from 48 to 32. The figure is derived from Solidity's packing rules and confirmed by the compiler: `Bridge`'s total storage footprint measures 4,128 bytes, exactly equal to the deployed TIP-109 implementation artifact's, which is the check `assertStorageUpgradeSafe` alone does not make. `cumulativeReanchorFee` sits inside a mapping value and costs no `Storage` slot; the two fee parameters pack into one new slot, and the two new `uint32` counters fit in that same slot's free tail. Ports #1096's storage-layout upgrade-safety test and hoists solc's `storageLayout` output selection into a shared constant so per-contract compiler overrides cannot drift from it.
Extracted from PR #1094, the furthest milestone-1-scoped tip. Adds the request side (vault set, deposit revealed, wallet Live, wallet binding, nonce increment), the proof side (SPV validation, deposit consumption, event emission) and the settlement path that writes the position and mints through the vault the deposit was revealed to. Scope is the transitive call closure of acceptance, computed with the proof dispatcher excluded as a traversal edge: `actionKey`, `getAction`, `submitReservationProof`, `requestReservationAcceptance`, `addWalletReservationKey`, `removeWalletReservationKey` and `strandReservation` in `Reservation.sol`, plus nine handlers in the new `ReservationProofs.sol`. Every member is byte-identical to #1094 apart from the dispatcher. `strandReservation` and `strandLateSettlementIfTargetWalletClosed` land here rather than with the timeout and stranding PR: `settleAcceptance` calls the stranding hook directly, so the helpers cannot arrive later than their caller. `WalletProposalValidatorConstants.sol` comes along because the request path reads its three timing constants and the file does not exist on the base. The dispatcher keeps its four-member `ProofType` enum, whose numbering is a documented invariant against `Reservation.ActionType`, but implements only the acceptance branch and rejects the rest explicitly. That is milestone 1 behavior rather than a placeholder: m1 ships create, custody and re-anchor only, so redemption and dissolution proofs must be refused for the whole milestone. Its main-UTXO parameter is left unnamed, keeping the type so the selector is unchanged, because only the dissolution handler ever read it.
Extracted from PR #1094, the furthest milestone-1-scoped tip. Adds the re-anchor request path (`requestReservationReanchor`, `anchorUtxoHash`), the proof and settlement path (`submitReservationReanchorProof`, `prepareReservationForSettlement`, `requireCurrentSourceAnchor`, `consumeAnchor`), and the `IReservationFeeFinancer` interface the settlement path calls to finance the Bitcoin miner fee in kind. The dispatcher gains its `ProofType.Reanchor` branch. Scope is the transitive call closure of re-anchor and nothing else. Members already carried by the acceptance PR are reused, not duplicated, and are byte-identical to it apart from the dispatcher. `IReservationFeeFinancer` is the interface only; the `ReservationVault` implementation is a later PR. Not extracted from #1093: that tip predates #1094's rewrite of `submitReservationReanchorProof` and does not have `prepareReservationForSettlement` at all. No cumulative re-anchor fee cap is added. #1094 accumulates no cumulative fee and enforces no ceiling, and that absence is a deliberate milestone 1 decision: the flat ceiling was rejected and a structural bound deferred to post-milestone-1 work. The two storage fields stay unwritten and unread. The per-hop bounds #1094 does carry, the snapshotted `action.txMaxFee` and the dust floor, come across faithfully, as does the governance gate that makes the accepted exposure tolerable: rotating a `Live` wallet's anchor requires governance, so a Byzantine custodian cannot drive the grind alone. The dispatcher's main-UTXO parameter stays unnamed: re-anchor takes five arguments and does not read it either, so dissolution remains its only consumer.
Extracted from PR #1094. Adds `notifyReservationActionTimeout`, which writes the terminal `TimedOut` record that still accepts a late proof, releases the capacity and locks reserved at request time, and propagates wallet consequences; plus `notifyStaleReservedDeposit` and `notifyReservationStranded`. Single file. `strandReservation` and `strandLateSettlementIfTargetWalletClosed` already landed with the acceptance PR, because its settlement path calls the stranding hook directly and the helpers cannot arrive later than their caller. This PR is the three notify entry points and the three events they emit. Not extracted from #1091, which the plan originally named for the timeout path: #1091 predates the `reservationGracePeriod` to `reservationDissolutionDelay` rename and is the only source whose storage name is absent from the milestone 1 layout, so its text would not compile. Adds `using Wallets for BridgeState.Storage`, which the timeout path needs for `self.notifyWalletRedemptionTimeout`, and `../bank/Bank.sol` for the escrow refund through `self.bank.transferBalance`. #1094 carries both.
Extracted from PR #1094 into m1/router-minimal, stacked on m1/timeout-and-stranding. ReservationRouter.sol + IReservationBridge.sol keep the eight m1 entry points and eleven m1 views from #1094, drop redemption / dissolution / veto / renewal / walletPendingDissolution, and add the genuinely new activeReservationsCount view. Also extracts Reservation.updateReservationParameters and updateReservationCaps (called by the router, not previously on the stack). updateReservationCaps gains maxActiveReservations (> 0), the §4.1 launch-gate parameter. Wires activeReservationsCount with the same request-time reserve pattern as walletReservationsCount: - increment + check at requestReservationAcceptance - decrement on acceptance timeout, superseded acceptance unwind, and strandReservation - restore on late-settle re-take and stranded-position reconstruct Decision 1 from m1-b-implementation.md §4.1 (amount-cap vs slot-capacity relational check vs runbook gate) is defaulted to the runbook gate and documented in the setter natspec / PR description pending manager confirmation (`agent-docs/m1/step-04-b-decisions-for-manager.md`). Verified: 184 Solidity files compile, yarn format exit 0, layout test passing, Bridge 23,939 B unchanged, ReservationRouter 6,928 B.
Decision 1 of the milestone 1 reservation work: ship the on-chain relational check tying reservationMaxTotalAmount to slot capacity, closing the saturation cliff documented in m1-b-implementation.md §4.1. In updateReservationParameters, the relational require guards the parameters setter against raising reservationMaxTotalAmount past maxActiveReservations * reservationMaxSingleAmount. Either operand set to zero disables that side of the product (the parameters setter reads self.reservationMaxSingleAmount and self.maxActiveReservations from storage, owned by the caps setter); both zero short-circuits only the pre-launch bootstrap state. In updateReservationCaps, the relational require guards the caps setter against lowering maxActiveReservations or reservationMaxSingleAmount below what the standing reservationMaxTotalAmount requires. The maxActiveReservations == 0 disjunct is intentionally absent here because the require(maxActiveReservations > 0) at the top of updateReservationCaps makes that branch dead. contracts/test/ReservationCapsStub.sol exposes both governance setters without the router's access guard, so the launch-gate invariant is unit-testable today (the router is Governable with no initializer, so standalone deployment hits governance == address(0) for every setter call). test/bridge/Bridge.ReservationCaps.test.ts covers the manager-specified cases plus a bootstrap-ordering scenario: parameters setter is permissive until the caps setter has configured either operand, after which any raise past slot capacity reverts. Storage layout unchanged. Co-authored-by: agent <agent@example.com>
…or the deploy-ordering hazard The previous 'requires caps to be set before a large reservationMaxTotalAmount sticks' test assumed step 3 (updateReservationCaps) would succeed and step 4 (a follow-up updateReservationParameters) would revert. Running the suite with FORKING_URL unset (the blocker previously reported as environment breakage was actually a stale shell var triggering the fork provider path) showed step 3 itself reverts: the mirrored check reads the already-oversized total against the new cap product and trips. This is the two-sided Decision 1 check working as designed — and a genuine deploy-ordering hazard worth pinning in a regression test. Rewrites the bootstrap-ordering describe block to: - 'reverts when updateReservationCaps follows an oversized parameters write': asserts the hazard. A deploy that calls parameters first with a large total, then caps, sees the caps call revert. The test pins this exact sequence. - 'accepts caps first, then a total within slot capacity': asserts the safe operational path. updateReservationCaps passes (total defaults to 0, so the mirrored check is trivially true), then a total within the resulting product sticks, then any follow-up raise past slot capacity reverts. Updates the describe-level docstring to describe the actual property: updateReservationParameters is permissive in the bootstrap window, updateReservationCaps is the gate, and the safe order is caps first. No production contract changes. Storage layout unchanged. Co-authored-by: agent <agent@example.com>
…losed
The reservation occupancy counters (activeReservationsCount, reservationTotalAmount,
walletReservationsCount/Amount, walletReservationKeys) leaked permanently when a
custodying wallet retired through the ordinary Live -> Closing -> Closed lifecycle.
The only release path (notifyReservationStranded -> strandReservation) requires
WalletState.Terminated, which a Closed wallet can never reach - terminateWallet is
reachable only from Live/MovingFunds/Closing.
Wallets.sol and MovingFunds.sol contain zero references to reservation state, so
nothing gates wallet retirement on zero open reservations, directly falsifying the
comment at BridgeState.sol:491-493 ("closing requires a zero reservation count").
Fix: widen notifyReservationStranded to also accept Closing and Closed, mirroring
strandLateSettlementIfTargetWalletClosed's posture. Emit a ReservationReconstructed
event from prepareReservationForSettlement when a stranded position is reconstructed
so any compensation path consuming ReservationStranded evidence can detect a
resurrected claim. Bound reservationDissolutionDelay with MIN/MAX constants
matching the term bound's pattern. Correct the BridgeState comment to describe
the actual mechanism. Stack-too-deep in submitReservationReanchorProof forces
inlining the miner-fee expression at its three use sites.
External audit (V12) of the reservations stack: 11 findings, all verifiedV12 audited Because Summary
Cheap fixes, likely worth taking before merge184208, one line. 184202, a parameter that does nothing. 184203, an invariant validated then ignored. 184204, an ABI asymmetry. This PR specifically owns 184207
The constructor never assigns Additional wrinkle: script 97 is skipped entirely on mainnet (L194-L195), so mainnet activation runs through script 98's hand-executed calldata, which also omits the setter (L18-L21). Design decisions, not patches184198, a documented but unenforced assumption. The divergence is documented deliberately at L511-L517 as "harmless under the deployment sequencing this stack assumes". The gap is that nothing enforces that sequencing on chain: not 184201, no sanctioned pre-signing validation. 184205, fees that never fund the reserve. Downgraded to Medium: all three fees are transfers of already-backed TBTC, never newly minted, so no unbacked supply is created at collection. The real issue is reserve underfunding. The extension fee goes straight to the treasury (L617-L625) and never enters the vault, the redemption fee enters then forwards before any unmint (L490-L508), and neither path references 184206, downgraded to Low. 184200: handled privately, not in this threadOne of the eleven findings concerns a parameter-configuration interaction whose Two reservation-side notes are in scope for this stack, and both make the reserved
Test gapsNo test at Caveat on routingBecause the stack is 9 deep and each PR diffs against its parent, most defect-site files are touched by 7 or 8 PRs in sequence. File lists confirm which PRs touch a file but cannot identify which introduced a given line; that needs per-line blame on the individual branches. The deploy-script attribution above is exact because those two files are touched only by this PR. |
…napshot selectors - WalletProposalValidator.validateReservationAnchorProposal compares the deposit against the Pending action's snapshotted minAmount, matching settlement. A governance increase of the live minimum can no longer make an in-flight, settleable acceptance unsignable (NFR-SEC-4). - reservationAbiSnapshot computes EVM-canonical selectors for tuple inputs (tuples expand to their component types); the checked-in snapshot is regenerated and a test pins the four tuple-input selectors directly. - Comments: dissolutionEligibleAt's single M1 reader is the Closing-wallet branch of notifyReservationStranded; sweepFees records the M1 no-floor debt repayment decision. - Caps runbook and scripts 97/98: client activation ordering gate before setVaultStatus(vault, true), the direct-credit fallback (no initiation fee) after vault trust is revoked, and an explicit feeReserveTarget governance step.
…comments - RESERVATION_CAPS_DEPLOYMENT.md:67-69: require the fee-reserve target decision before on-chain activation; re-anchor already finances miner fees in kind in milestone 1. - RESERVATION_CAPS_DEPLOYMENT.md:55-63: the direct-credit, no-fee fallback applies to every acceptance settled while the vault is untrusted, on time or late. - RESERVATION_CAPS_DEPLOYMENT.md:52-53: separate the temporary gap before a configured activation block from permanent inactivity when the network has no entry. - deploy/97, deploy/98: reduce the repeated prose to short gates that point at the runbook sections. - Remove undefined internal review codes from the runbook, scripts and ReservationVault.sweepFees NatSpec. - Reservation.sol:209-216: describe the dissolutionEligibleAt reader and writer by function name instead of line numbers.
…ests - WalletProposalValidator.test.ts:3745-3825: mocked parameters now satisfy minAmount > txMaxFee > 0 and deposit >= minAmount + txMaxFee; the lowered-minimum case is marked as unreachable through authorization and kept as a defensive check. - WalletProposalValidator.test.ts:3824: add the exact minAmount + anchorTxFee boundary case.
- reservationAbiSnapshot.ts:59-87: replace the hand-rolled tuple canonicalization with Interface#getSighash / getEventTopic; the regenerated snapshot is byte-identical. - Bridge.ReservationAbiSnapshot.test.ts:41-44: drop the inline comment that repeated the declaration comment.
…ivation - RESERVATION_CAPS_DEPLOYMENT.md:55-56: without an activation block no wallet signs an anchor, so there is no acceptance proof to submit; the exits are acceptance timeout, the depositor's Bitcoin refund after the locktime, and stale pending-deposit cleanup, not stranding. - WalletProposalValidator.test.ts:2901: the header now says only the lifecycle fixtures are reachable; the below-floor case is an explicit mock-only check.
… path RESERVATION_CAPS_DEPLOYMENT.md:56: without client coordination there is no proof unless the wallet signs and broadcasts an anchor separately; timeout and the Bitcoin refund are the normal fallback, not the only route.
## Context A cross-repo review compared tbtc-v2 #1116 and keep-core #4282 against the M1 reservations specification. This PR closes the tbtc-v2 code findings on top of `reservations-upgrade`; the larger wallet-coordination, SPV and watcher fixes live in the paired keep-core PR. ## Changes - Validate reservation acceptance outputs against the action's snapshotted `minAmount`, not the current governance minimum. This preserves a request that was valid when authorized after governance raises the live minimum. - Generate canonical tuple selectors from the ABI fragments instead of hashing the JSON `tuple` placeholder, then regenerate the reservation ABI snapshot. - Add regression coverage for the snapshotted minimum and all affected selectors. - Correct reservation comments that described `dissolutionEligibleAt` and fee-debt behavior inaccurately. - Harden activation scripts and the deployment runbook around fee-reserve setup and the keep-core activation-block ordering gate. ## Verification - `yarn build`: pass. - `yarn typecheck`: pass. - `yarn format`: pass (existing repository warnings only; no errors, Prettier clean). - Full Hardhat suite: `3417 passing, 33 pending`. This workstation exports `FORKING_URL`, so the run removed that host variable to match CI. Its global deployment fixture also exceeded the repository's 60-second Mocha limit on this host; a temporary untracked config raised only that timeout to five minutes. The repository config was not changed. ## Scope notes - Base: `reservations-upgrade` at `9f8f5ef1`. - This PR does not reconcile `reservations-upgrade` with `dev`; #1116 remains conflicting until that separate integration step. - No reservation ABI surface changed. The paired keep-core CI gate remains pinned to the ABI-identical base commit `9f8f5ef1` and verifies the vendored fragments against compiled artifacts. ## Related - Tracker: #1116 - Paired keep-core tracker: threshold-network/keep-core#4282 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Reservation-anchor proposals now honor the minimum deposit recorded when the acceptance was authorized, even if the live minimum changes later. * Fee repayment can use the vault’s full TBTC balance; if repayment leaves no surplus above the reserve target, the operation completes without reverting. * **Documentation** * Clarified activation timing, fee-reserve configuration, and how acceptances are handled while the vault is untrusted. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
## Context A cross-repo review compared tbtc-v2 #1116 and keep-core #4282 against the M1 reservations specification. It raised 60 candidates; two independent validation passes confirmed 48 and dropped 7. This PR closes the keep-core code findings on top of `reservations-epic`. The three P0s made the wallet-side flow nonfunctional or discarded contract-settleable proofs: - acceptance tried to validate and request an action before the depositor-created action existed; - re-anchor validated before requesting its on-chain action; - the SPV loop evicted timed-out generations that the contract still accepts. ## Changes ### Wallet coordination - Consume an existing depositor-created Pending Acceptance action at its real nonce and snapshots. Remove the invalid operator-side acceptance request. - Request re-anchor first, track the transaction by hash, then validate and propose against the mined Pending Reanchor action. - Resume pending re-anchors across rounds. Pending receipts suppress duplicate requests; reverted or dropped requests can retry; mined requests resume without another request. - Select re-anchor targets with count and amount headroom and preserve Solidity's zero-global-cap semantics. - Accept Live and MovingFunds source wallets and mirror the validator's 7,200-second request safety margin. - Tighten LocalChain validator fakes so tests enforce the same action-state, nonce, wallet-state, fee and timeout preconditions as Solidity. ### SPV maintainers and watchers - Keep TimedOut acceptance proofs through `timeoutAt + termSeconds`; keep TimedOut re-anchor proofs without an added Go-side expiry. - Match both P2WPKH and P2PKH outputs by public-key hash. - Start proof and watcher recovery scans at the configured reservation activation block, in bounded chunks. Unknown networks skip startup scans. - Derive stale-deposit deadlines from the reveal snapshot, defer all per-deposit chain reads until that deadline, and keep tracking until the on-chain pending-deposit record clears. - Match timeout equality with Solidity and count watcher goroutine deaths without panicking on a typed-nil metrics recorder. ### ABI, chain and observability - Preserve reservation action `termSeconds` and `minAmount` snapshots and the re-anchor miner fee in domain conversions. - Remove the unused generic timeout-event model that does not exist in Solidity. - Add ReservationVault bindings and publish in-kind fee-debt and TBTC reserve gauges. - Add an in-memory EVM harness around the real WalletProposalValidator and a stub Bridge. - Pin and extend CI ABI drift checks to Bridge, both reservation validator methods, ReservationRouter and ReservationVault. Mismatches now fail the job. ## Verification - `gofmt` on every changed/new Go source: clean. - `go build ./...`: pass. - `go vet ./cmd/... ./pkg/chain/ethereum ./pkg/clientinfo ./pkg/maintainer/spv ./pkg/tbtc ./pkg/tbtcpg/...`: pass. - `go test -count=1 ./cmd/... ./pkg/chain/ethereum ./pkg/clientinfo ./pkg/maintainer/spv ./pkg/tbtc ./pkg/tbtcpg/...`: pass. - `make -C pkg/chain/ethereum/tbtc/gen verify-vendored-fallback` against freshly compiled tbtc-v2 artifacts: all four ABI comparisons pass. - Full `go test -count=1 ./...` was attempted. Every changed package passed; the run failed only in unmodified `pkg/tecdsa/dkg` when `TestTssFinalize_IncomingMessageCorrupted_WrongPayload` hit its internal TSS round-one timing guard. Re-running that test produced nondeterministic outcomes across identical source and an identical 87-package dependency closure, including a pass at the exact base commit. This PR does not suppress or modify that test. Targeted regression tests were mutation-checked: each failed when its corresponding production branch was temporarily removed, then passed after exact restoration. Covered paths include late TimedOut proofs, action snapshots, P2PKH matching, activation-block recovery, re-anchor receipt states, the timeout safety margin, and typed-nil metrics. ## Scope notes - Base: `reservations-epic` at `f66f11240` (includes #4324). - This PR does not reconcile the epic branch with `dev`; #4282 remains conflicting until that separate integration step. - File-size debt is intentionally not refactored here. Most touched files already exceeded the repository's 500-line guideline at the base, and splitting them would mix a broad mechanical rewrite into correctness fixes. The largest new exception is `pkg/chain/ethereum/tbtc_validator_harness_test.go` at 981 lines; `pkg/maintainer/spv/reservation_proof_loop_test.go` grows from 1,668 to 2,168 lines. This exception was explicitly chosen for this PR and should be paid down separately. - The existing full-repository `go vet ./...` warning in `pkg/tecdsa/signing/protocol.go` is outside this diff; changed reservation packages are vetted separately. ## Related - Tracker: #4282 - Cross-repo tracker: threshold-network/tbtc-v2#1116
…indow A reserved deposit's refund locktime is bounded two ways once depositRevealAheadPeriod is set: at least the reveal time plus the reveal-ahead period, and at most the reveal time plus the term plus the 24h deposit refund safety margin. With a reveal-ahead period of 150 days and the default 90-day term both bounds exclude each other, so every reserved deposit's reveal reverts. The deploy script now reads the on-chain depositRevealAheadPeriod and refuses terms whose feasible reveal window is empty or thinner than the 1h clock-drift margin; the Hardhat tests pin the reject side, the accept side and the zero reveal-ahead case.
updateReservationParameters accepted any timeout above the 2h request safety margin. At safety margin + 1 no acceptance or re-anchor request can be signed at all, and up to 4h + 1s a deposit revealed in the request block has no signing second: its earliest signing moment (reveal + deposit min age) sits past the signing deadline. Require the timeout to exceed the safety margin plus the deposit min age plus one second; the deploy script mirror now enforces the same floor, and the caps tests pin both sides of the boundary.
…e-anchorable Acceptance admitted deposit amounts at min amount + one transaction max fee, but the acceptance anchor pays up to that fee and a re-anchor requires an anchor strictly above min amount + one fee, so deposits in the band [min, min + fee] stranded their anchors where no re-anchor could move them. The floor is now min amount + 2 * tx max fee (strictly greater), which makes every accepted anchor re-anchorable under the same parameters; guard-11 tests cover both sides of the new boundary and the old floor, and the validator lifecycle fixtures are raised above it.
…able The late-settlement hook stranded a newly settled anchor at once when its target wallet was Closing, about a full reservation term before the permissionless cleanup path would have allowed it, and it took away the re-anchor right a Closing wallet otherwise has (a re-anchor request accepts a Closing source wallet). Stranding here now applies only to terminal states; a Closing wallet keeps the position Active so the owner can still re-anchor through the wallet's closing period. The strand-settlement tests pin both sides: a Closing target now stays Active with no strand evidence, and the Closed terminal-state counterpart still strands.
… re-anchor Once a position passed its dissolution deadline, anyone could push its Live wallet through the permissionless moving-funds and closeable checks into Closing and strand it in the same transaction, ahead of the owner's re-anchor right (a re-anchor request accepts a Closing source wallet). notifyReservationStranded now additionally requires the wallet's closing period to have elapsed for a Closing wallet, so every position gets at least one full closing period to move. The occupancy tests add both sides of the closing-period boundary: past dissolution but inside the closing period reverts, and past both it strands.
notifyReservationStranded refused Live wallets, and closeReservation has no caller in milestone 1, so a position past its dissolution deadline on a Live wallet held its wallet and global reservation capacity forever. Allow anyone to strand a Live-wallet position once it has reached dissolutionEligibleAt: an idle position then closes, the capacity is released, and the owner's minted balance remains an ordinary pooled claim. The occupancy tests rework the Live-wallet case into a shared helper and pin both sides: before the deadline the call reverts, at the deadline it strands and releases the counters.
…et exits moveFunds routed a zero-main-UTXO wallet into MovingFunds whenever its reservation count was non-zero, and that count includes capacity reserved for pending acceptance and re-anchor targets that no Bitcoin proof backs yet. Anyone could therefore load an empty wallet with pending capacity and push it into the moving-funds window where a timed-out target triggers a moving-funds slash. Wallet exits now read a proven-anchor ledger (walletAnchoredInfo) that tracks only anchors actually custodied at the wallet, updated by settlement and strand. The hardhat tests re-seed that ledger where positions are modeled as settled so strand releases stay balanced, and add the moveFunds routing cases: a proven anchor routes to MovingFunds, while pending-only capacity routes straight to Closing. The storage layout gains the new mapping inside the reserved gap.
…quest revealDeposit checks no Bitcoin proof, so a request could be made for a reserved deposit whose funding transaction was never confirmed on Bitcoin, reserving wallet and global capacity with no spend backing it. requestReservationAcceptance now takes the deposit's funding transaction, its SPV proof and the output index, validates the proof with the Bridge's usual rules and derives the reservation key from the proven transaction hash. The router's method signature changes; the ABI snapshot and every router-level caller in the Hardhat tests are updated to pass the funding proof, and a request with an invalid or missing proof now reverts while a valid one succeeds. The library function is unchanged.
|
Pushed 8 commits to this branch. They change reservation behavior in the ways below. Each has Hardhat tests that fail on the previous code and pass now.
Questions for the authors:
This branch conflicts with |
dev moved the Hardhat suite to ethers v6. Port the reservation tests, the reservation deploy scripts and the ABI snapshot helper to bigint, contract targets and the v6 helpers. Revert-reason checks use the full message, which the v6 matchers require.
….config.ts hardhat.config.ts compiles Bridge.sol with 200 optimizer runs to stay under the contract size limit. foundry.toml had no matching restriction, so the compiler parity check failed.
Whitespace only. The compiled bytecode of every changed contract is identical before and after, ignoring the metadata hash.
… reads the block time
Raise the Solhint cap and per-rule baseline for the reservation contracts, exempt two files from rules that do not fit them, and run the policy check with the update check off so a newer solhint release does not break its JSON output.
|
Note for reviewers: commit 0667abb raises the solhint --max-warnings cap from 55 to 62 and updates the policy-test baselines (ordering 51, payable-fallback 1) to match the warnings in the new Reservation code. It also passes --disc to the solhint policy test. contracts-format now passes in CI with 0 lint errors. Authors and reviewers: please confirm the higher cap is acceptable, or ask for the warnings to be fixed instead. |
|
Runbook gap for mainnet activation. Script 98 builds its six actions for the BridgeGovernance in the deployments record (get("BridgeGovernance")) and only checks that its owner() is the Council Safe. It does not check that the Bridge uses that BridgeGovernance. On a mainnet fork, the generated calldata reverts when sent to the BridgeGovernance that is live today, because that contract has no reservation functions. So activation needs a new BridgeGovernance deployed and made the Bridge's governance (beginBridgeGovernanceTransfer, then finalizeBridgeGovernanceTransfer after the 48h delay) before script 98's actions can run. No reservation runbook states this step or its order; docs/bridge-rebate-governance-deployment.adoc covers the same handoff for the rebate change only. Suggest adding it to the reservation runbook, and optionally a check in script 98 that the Bridge's governance address equals its target. |
Purpose
Tracking PR for the milestone 1 UTXO reservations stack.
reservations-upgradeis theepic/integration branch that PRs A-H merge into one at a time, in the fixed
build order decided 2026-08-24 (
A -> C -> D -> E -> B -> F -> G, keep-core Hlast). This PR carries the cumulative diff of every landed component PR — it is
not a zero-diff tracking PR — and gives one place to watch progress before the
whole stack lands in
dev.IRREVERSIBLE — governance sign-off required before activation: once any
reservation is accepted,
ReservationVaultcannot be re-pointed except viawallet termination under variant B (see
solidity/docs/RESERVATION_CAPS_DEPLOYMENT.md).Do not merge until every box below is checked and the final reservation
test suite passes on this branch.
Cross-repo M1 review (2026-09-28)
A review of this tracker and keep-core #4282 against the M1 specification raised 60 candidates; two validation passes confirmed 48 (3 P0, 6 P1, 27 P2, 12 P3) and dropped 7. Remediation is split across:
docs/reservations-spec@3c5603fa5- reconciled M1 requirements, architecture, readiness and delivery status.Both fix PRs must land on the epic branches before #1116/#4282 are reconciled with
dev. They do not perform that integration step.Merge checklist (tbtc-v2) — 7/7 landed
m1/storage-layout(merged 2026-09-01,edfbe0c2)m1/acceptance-core(merged 2026-09-01,e30e3233)m1/reanchor-core(merged 2026-09-01,a3ea888e)m1/timeout-and-stranding(merged 2026-09-02,76eab846)m1/router-minimal(merged as8f30da66; review-fix pass fix: address PR #1110 review findings (M1/M2/I4/I5) #1118 absorbed)m1/vault-pause-flags(merged as2cb4b8f9; review-fix commits absorbed on top)m1/bridge-integration-seams(merged 2026-09-03,cadebb7d)Merge checklist (keep-core, separate repo/PR)
m1/keep-core-client(waits for G's ABI surface to be stable). Downstream work already stacked on top of H and verifiedMERGEABLE: keep-core#4276 (readiness fixes) -> #4277 (protobuf marshaling) -> #4278 (coordination checklist) -> #4279 (multisigner integration tests) -> #4280 (test-coverage backfill).feat/utxo-reservation-wallet-supportwas the earlier parallel attempt. The accepted M1 wallet-side line isreservations-epic/ keep-core#4282; #4238 is not to be reconciled or merged into it.Known divergence from
devreservations-upgradewas branched frommain, notdev. The current GitHub compare reports 100 commits ahead / 191 commits behinddev, and this PR ismergeStateStatus: DIRTY/mergeable: CONFLICTING. Before this tracker can land,devmust be reconciled intoreservations-upgrade, the conflicts must be reviewed as their own integration change, and the complete CI suite must run again.