Skip to content

Reservations epic -> dev tracking - #4282

Draft
piotr-roslaniec wants to merge 100 commits into
devfrom
reservations-epic
Draft

piotr-roslaniec wants to merge 100 commits into
devfrom
reservations-epic

Conversation

@piotr-roslaniec

@piotr-roslaniec piotr-roslaniec commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Reservations epic -> dev tracking

This PR aggregates the UTXO reservations epic (reservations-epic) and tracks its promotion to dev.

Diff caveat / current blocker: reservations-epic diverged from dev at merge-base a7ac8989 and is 109 commits ahead / 68 commits behind current dev. This PR is currently mergeStateStatus: DIRTY / mergeable: CONFLICTING — a real conflict, not a stale check, confirmed against the GitHub compare API. dev must be merged into reservations-epic to resolve the divergence before this PR can be merged; until then the diff shown here reflects only the epic's 109 unique commits and does not represent what actually lands on dev.

Merge stack (#4274 chain) — COMPLETE

The entire #4274 stack is now merged into reservations-epic.

Other PRs targeting reservations-epic

How to use this PR

  1. New reservations work should target reservations-epic (or stack onto an open reservations PR).
  2. This PR stays open as the running reservations-epic -> dev gate.
  3. Next step: merge dev into reservations-epic to resolve the current conflict, then re-run CI on the epic branch before merging this PR into dev.

Status

Stack complete: #4274, #4276, #4277, #4278, #4279, #4280 all merged into reservations-epic. This PR is blocked on resolving reservations-epic's divergence from dev (109 ahead / 68 behind, currently CONFLICTING). #4238 remains separately unmerged and untouched by this stack.

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

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.

❤️ Share

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

piotr-roslaniec added a commit that referenced this pull request Sep 3, 2026
…nt review of #4282) (#4283)

## Summary

Remediation for the 37 confirmed findings from a multi-agent review of
PR #4282
(`dev` <- `reservations-epic`, i.e. the accumulated content of
#4274+#4276+#4277).
37 raised -> 37 confirmed -> 0 dropped after arbitration and validation.

- **P1 (6 of 7 fully fixed, 1 partially fixed):** deposit-sweep
reservation-vault
exclusion, reservation look-back underflow + target-wallet check,
reservation
acceptance `eth_getLogs` bounds + nonce reconciliation + caps, SPV
proof-loop
retry-eviction data loss (symptom fixed, structural root cause deferred
- see
below), stale-deposit timeout memoization, below-dust re-anchor trigger
removal
  (M-27, resolved via tbtc-v2 source after user escalation).
- **P2/P3 (22 of 30 fixed, 8 explicitly deferred):** see "Deferred"
below.

Full-repo `go build`, `go vet`, and `go test ./...` all pass with these
fixes
applied (verified after every commit and once more at closeout).

## Deferred (1 P1 architectural root-cause + 7 P2/P3 symptoms/hygiene)

An arbiter-recommended structural fix for M-16 (remove the SPV proof
loop's
persistent-cursor design entirely in favor of the stateless
bounded-rescan
pattern every sibling proof type already uses) was attempted together
with
the M-7 nonce-aware timeout fix and an M-14 dead-code removal. That
combined
change broke three existing tests and was reverted rather than debugged
under
time pressure. Only a narrower, independently-safe subset landed: a
surgical
patch for M-3 (non-lossy cursor rewind) plus unrelated
memoization/metrics/test
fixes. **M-16's own P1 rating is only partially addressed** - the
persistent-cursor
design itself, and the M-7/M-14 symptoms it also breeds, remain
unremoved.

1. **M-16 (P1)** `pkg/maintainer/spv/reservation_proof_loop.go:227-246`
-
`reservationProofScanState`'s persistent cursor is the structural root
cause of M-3 (fixed surgically) and M-7 (below). Removing it in favor of
   the stateless bounded-rescan pattern is what broke 3 tests on first
   attempt and remains unimplemented.
2. **M-7 (P2)** `reservation_action_timeout_watch.go:260-281` -
`CheckReservationActionTimeouts` deletes `pendingActions` entries on 3
of
   4 non-notifying outcomes without asserting the tracked `requestNonce`
   against the freshly-derived one; same root cause as M-3.
3. **P2** `reservation_action_timeout_watch.go:370` +
`reservation_wiring.go:38-49` -
the timeout watcher's `WalletMembersResolver` only resolves wallets the
   local operator co-signs; an offline/disabled/colluding wallet's own
   operators get zero independent timeout coverage.
4. **P2** dead-code cluster in `reservation_proof_loop.go` /
`reservation_proof_loop_test.go` -
`findReservationAcceptanceTransaction`,
`findReservationReanchorTransaction`, and their wrapper helpers have
zero
production callers; 14 tests exercise the unused wrapper instead of the
   `isMatching*` predicates actually called in production.
5. **P2** `reservation_proof_loop.go:612,~817` - two tautological guards
are
algebraically always-false, masking that the real enforced constraint is
   only `0 < fee <= TxMaxFee`.
6. **P2** `reservation_wiring.go:237-320` `startStaleDepositPoll` - the
entire loop body runs untested inside a goroutine; existing tests assert
   only that the goroutine starts.
7. **P3** `reservation_action_timeout_watch.go:18-20` - unused
   "backward-compatibility alias" constant, zero references.
8. **P3** `reservation_proof_loop.go:644` - duplicated, truncated
comment
   fragment left by a merge.

## Known conflicts with other open PRs in this stack - read before
merging

This branched from `reservations-epic` at `bb3dcb398`. Three other
efforts are
in flight against overlapping code and were **not** reconciled here,
since they
belong to PRs this one doesn't own:

### 1. `pkg/tbtc/coordination.go` vs #4278 (hard conflict, not cosmetic)

#4278 ("remove frequency gate on reservation checklist actions") drops
`&& windowIndex%frequencyWindows == 0` from the reservation-actions
checklist
gate (custody-critical, should run every window like `ActionRedemption`)
but
its diff still references the old single `ReservationsActivationBlock`
constant. This PR's `602d0ef11` independently rewrote that same `if`
into
`reservationsActivationBlock(ce.ethereumNetwork)`, a per-network table
lookup
(`ethereum.Mainnet: 26500000`, everything else defaults to 0).

**A conflict resolution that naively favors this PR's side of that hunk
silently reinstates the frequency gate #4278 deliberately removed.**
Combined
resolution (verified against both intents):

```go
// Reservation actions (acceptance, re-anchor) are custody-critical like
// Redemption and are checked on every coordination window once the
// activation block is reached, not frequency-gated like the
// throughput-driven DepositSweep/MovedFundsSweep/MovingFunds actions
// above: a delayed reservation acceptance or re-anchor risks the
// on-chain ReservationActionTimeout backstop firing before the wallet
// subsystem gets a chance to act. The activation block is a per-network
// table (reservationsActivationBlock), not a single global constant, but
// it is still config-independent and globally observable from chain
// height alone -- which is what keeps leader and follower checklists in
// agreement without relying on local config.
if coordinationBlock >= reservationsActivationBlock(ce.ethereumNetwork) {
    actions = append(actions, ActionReservationAnchor)
    actions = append(actions, ActionReservationReanchor)
}
```

### 2. `pkg/tbtc/marshaling.go` vs #4278 (duplicate, this PR's version
wins)

#4278 independently adds the same 4 missing `Marshal`/`Unmarshal` doc
comments
this PR's `7cbb8cc2f` adds, but comment-only and with a capitalization
bug
(lowercases the exported type name, e.g. `"...converts the
reservationAnchorProposal..."`). This PR's version is a superset:
correctly
capitalized comments plus the actual nil-guard/zero-hash-rejection logic
#4278 doesn't have. On merge, take this PR's 4 lines, drop #4278's.

### 3. `pkg/tbtcpg/reservation_acceptance_test.go` vs #4280 (whole-file
conflict + one real design decision)

#4280 ("M2 test-coverage backfill") independently rewrote large parts of
the
same shared test harness this PR's `726f05ed7` touched - the same
`reservationAcceptanceLocalChain` type, constructor, and ~14 shared
methods,
plus `scenarioReservationAcceptanceChain`/`registerReservedDeposits`/
`expectedAnchorsEqual`. This is a heavy line-level conflict across the
whole
file, not just redundant test names. Specifics:

- `TestReservationAcceptanceTask_AmountCapBoundaries` (this PR, cap
boundaries
only) is a strict subset of #4280's
`TestReservationAcceptanceTask_BoundaryChecks`
  (adds `MaxReservationsPerWallet`, net-of-fee `ReservationMinAmount`,
`ActiveReservationsCount`). Left in place rather than deleted
preemptively -
#4280 is still open and two-deep-stacked (on #4278, also open) and could
  stall or be reworked; delete this PR's version only in the merge that
  actually lands #4280.
- This PR's
`TestReservationAcceptanceTask_VaultNotConfigured_ZeroAddress`
(finding: dead vault-not-configured guard) has no equivalent on #4280's
  side - a "just take #4280's file" resolution silently drops it.
- **Real design decision, not just a merge conflict:** #4280's
  `TestReservationAcceptanceTask_Stateless_PastEventsError` exercises
`PastReservationAcceptanceRequestedEvents` returning an error and
asserts
fail-closed skip-on-error. This PR's `hasPendingAction` (from
`726f05ed7`)
no longer calls `PastReservationAcceptanceRequestedEvents` at all - it
uses
a different, generation-scoped pending-action check instead. Ported onto
this PR's code as-is, that test would either pass vacuously or fail for
an
unrelated reason. **This PR intentionally left
`PastReservationAcceptanceRequestedEvents`
  on the `tbtcpg.Chain` interface (`chain.go:263`) and the test double's
`acceptanceEvents`/`acceptanceEventsErr` fields in place, undeleted,
even
though they now have zero production callers** - removing them here
would
have foreclosed reconciling #4280's test against whichever
pending-action
mechanism is ultimately kept. Whoever merges this PR and #4280 needs to
pick one mechanism and either delete the losing side's interface
method/test
  or keep both if there's a reason for two independent checks.

## Testing

- `go build ./...`, `go vet ./...`: clean.
- `go test ./...`: full repo suite, 0 failures (verified at closeout
after
  every commit landed).
piotr-roslaniec added a commit that referenced this pull request Sep 4, 2026
…m steps

COPY ./ci-shims/tbtc-artifacts in the Dockerfile sourced a directory
excluded by .gitignore and created only by client.yml's shim step;
release.yml builds the identical Dockerfile with no equivalent step,
so a fresh-checkout release build hard-failed on the missing COPY
source. Un-ignore the directory and track a placeholder so it always
exists regardless of which workflow builds the image.

Also delete the client.yml steps gated on
github.base_ref == 'reservations-epic': PR #4282's base is dev, so the
gate never fires for this PR, and once the epic lands on dev it can
never fire again. The gen/Makefile fallback rules already supply the
same artifact surface and remain the only reachable mechanism.
piotr-roslaniec added a commit that referenced this pull request Sep 7, 2026
#4282

40 confirmed findings fixed (2 P0, 7 P1, 24 P2, 7 P3); 1 P2 finding (in-kind
fee reserve watch) intentionally left unfixed - its only two remediations are
building unscoped new functionality or editing a separate spec repo, both
out of scope here.

P0:
- wire notifyReservationAcceptanceTimedOut end to end (vendored ABI, Go
  bindings, chain interfaces, ethereum adapter, watcher dispatch by
  ActionType) - acceptance-type timeout notifications could never
  succeed on-chain before this
- gate deposit-sweep's reserved-deposit filter on the reservation vault
  address instead of calling isReservedDeposit unconditionally on every
  deposit pre-activation, which risked halting sweeping network-wide
  during the binary-upgrade-before-Bridge-upgrade window

P1:
- action-timeout watcher no longer requires wallet-member resolution to
  notify a Reanchor timeout, so a dead/closed wallet's stuck action is
  now permissionlessly resolvable
- drop the load-failure eviction that permanently dropped a tracked
  action from timeout coverage after 3 RPC hiccups
- remove the mainnet reservations-activation-block placeholder pending a
  real rollout height
- correct GetReservation/GetReservationAction doc comments (absence is
  Unknown state, not an error)
- propagate chain-read errors instead of masking them as no-op
- memoize per-pass proof-invariant chain reads shared across proof-loop
  transactions
- add the missing reservations-remaining below-dust regression subtest

P2/P3: gauge visibility, nonce-bound preload validation, notifiedAt
semantics, address-comparison case sensitivity, wallet-vault fee-floor
check, dual-flag config validation, duplicate/stale comments and tests,
and assorted simplifications.

Also reverts an in-flight hard-fail config-validation check for the
Tbtc/Spv reservations dual-flag pairing back to a warning: start's config
categories never load the Maintainer section, so the check would have
hard-failed startup for any legitimate split deployment running the SPV
maintainer as a separate process.

Self-verification follow-up (same review pass, iterated against external
review of the fixes themselves): re-checked every "fixed" claim above
against the actual diff and against the reasoning behind each fix, and
made three further corrections:

- deposit-sweep follower-side (ValidateDepositSweepProposal) never
  actually got the reservation-vault gate described above - only the
  leader side (deposit_sweep task) did, so a follower could still sign a
  sweep a leader had already correctly rejected. Implemented the
  matching gate on the follower path: fetch ReservationParameters with
  the same bounded retry as the leader (fewer attempts only makes a
  wrong "reservations aren't live" guess more likely, never less, so
  there is no safety argument for the follower retrying less than the
  leader), then hard-reject on any IsReservedDeposit error or a
  confirmed reservation for any deposit the cheap vault-match prefilter
  flags as a candidate - unlike the neighboring fee soft-check just
  below, which stays deliberately log-only because a merely underpriced
  sweep is not irreversible the way sweeping a reservation is. Added the
  regression test proving detection
  (TestValidateDepositSweepProposal_RejectsReservedDeposit) and its
  counterpart proving the fail-open path still sweeps normally when
  ReservationParameters is unavailable
  (TestValidateDepositSweepProposal_SweepsWhenReservationParametersUnavailable,
  mirroring the leader's existing test of the same shape).
- the P1 loadFailures-eviction removal above initially grew a backoff
  mechanism to avoid re-logging every poll tick during a persistent
  failure, but the backoff suppressed the downstream timeout-notify
  check for the same tick it suppressed the retry - a single transient
  RPC error could have delayed a genuinely-overdue Bridge notification
  by up to 10 minutes, worse than the log spam it was meant to prevent
  (a real outage fails every tracked action identically regardless of
  backoff, so there was nothing for a per-entry backoff to usefully
  save). Removed it; kept the simpler P1 fix alone - retry every tick,
  evict only on an observed non-Pending state.
- reservation_wiring.go's walletMembersResolver parameter was threaded
  through WireReservationWatchers but never read by anything downstream
  of the P1 fix that removed its only consumer; deleted the dead
  parameter from the function, its test, and the cmd/start.go call site
  (which used to feed it the WalletMembersResolver tbtc.Initialize
  returned), and corrected the call site's comment (the paired-flag
  validation is a warning, not a hard error, per the note above).
  Finished the resulting clean cutover: with cmd/start.go (Initialize's
  only caller anywhere in the repo) no longer reading it,
  tbtc.Initialize now returns only error, and the WalletMembersResolver
  interface plus node.ResolveWalletMembers (its one implementation) are
  deleted with it.
piotr-roslaniec added a commit that referenced this pull request Sep 7, 2026
…4282

Comment-only audit and fix pass across all comments this PR added or
modified: drift against current code, self-containment (drop references
to other repos / markdown docs that don't exist in keep-core), redundant
text, and inconsistent voice. No behavioral change - gofmt/build/vet
clean, diff confirmed comment-only against d1697f5.
piotr-roslaniec added a commit that referenced this pull request Sep 8, 2026
…m steps

COPY ./ci-shims/tbtc-artifacts in the Dockerfile sourced a directory
excluded by .gitignore and created only by client.yml's shim step;
release.yml builds the identical Dockerfile with no equivalent step,
so a fresh-checkout release build hard-failed on the missing COPY
source. Un-ignore the directory and track a placeholder so it always
exists regardless of which workflow builds the image.

Also delete the client.yml steps gated on
github.base_ref == 'reservations-epic': PR #4282's base is dev, so the
gate never fires for this PR, and once the epic lands on dev it can
never fire again. The gen/Makefile fallback rules already supply the
same artifact surface and remain the only reachable mechanism.
piotr-roslaniec added a commit that referenced this pull request Sep 8, 2026
#4282

40 confirmed findings fixed (2 P0, 7 P1, 24 P2, 7 P3); 1 P2 finding (in-kind
fee reserve watch) intentionally left unfixed - its only two remediations are
building unscoped new functionality or editing a separate spec repo, both
out of scope here.

P0:
- wire notifyReservationAcceptanceTimedOut end to end (vendored ABI, Go
  bindings, chain interfaces, ethereum adapter, watcher dispatch by
  ActionType) - acceptance-type timeout notifications could never
  succeed on-chain before this
- gate deposit-sweep's reserved-deposit filter on the reservation vault
  address instead of calling isReservedDeposit unconditionally on every
  deposit pre-activation, which risked halting sweeping network-wide
  during the binary-upgrade-before-Bridge-upgrade window

P1:
- action-timeout watcher no longer requires wallet-member resolution to
  notify a Reanchor timeout, so a dead/closed wallet's stuck action is
  now permissionlessly resolvable
- drop the load-failure eviction that permanently dropped a tracked
  action from timeout coverage after 3 RPC hiccups
- remove the mainnet reservations-activation-block placeholder pending a
  real rollout height
- correct GetReservation/GetReservationAction doc comments (absence is
  Unknown state, not an error)
- propagate chain-read errors instead of masking them as no-op
- memoize per-pass proof-invariant chain reads shared across proof-loop
  transactions
- add the missing reservations-remaining below-dust regression subtest

P2/P3: gauge visibility, nonce-bound preload validation, notifiedAt
semantics, address-comparison case sensitivity, wallet-vault fee-floor
check, dual-flag config validation, duplicate/stale comments and tests,
and assorted simplifications.

Also reverts an in-flight hard-fail config-validation check for the
Tbtc/Spv reservations dual-flag pairing back to a warning: start's config
categories never load the Maintainer section, so the check would have
hard-failed startup for any legitimate split deployment running the SPV
maintainer as a separate process.

Self-verification follow-up (same review pass, iterated against external
review of the fixes themselves): re-checked every "fixed" claim above
against the actual diff and against the reasoning behind each fix, and
made three further corrections:

- deposit-sweep follower-side (ValidateDepositSweepProposal) never
  actually got the reservation-vault gate described above - only the
  leader side (deposit_sweep task) did, so a follower could still sign a
  sweep a leader had already correctly rejected. Implemented the
  matching gate on the follower path: fetch ReservationParameters with
  the same bounded retry as the leader (fewer attempts only makes a
  wrong "reservations aren't live" guess more likely, never less, so
  there is no safety argument for the follower retrying less than the
  leader), then hard-reject on any IsReservedDeposit error or a
  confirmed reservation for any deposit the cheap vault-match prefilter
  flags as a candidate - unlike the neighboring fee soft-check just
  below, which stays deliberately log-only because a merely underpriced
  sweep is not irreversible the way sweeping a reservation is. Added the
  regression test proving detection
  (TestValidateDepositSweepProposal_RejectsReservedDeposit) and its
  counterpart proving the fail-open path still sweeps normally when
  ReservationParameters is unavailable
  (TestValidateDepositSweepProposal_SweepsWhenReservationParametersUnavailable,
  mirroring the leader's existing test of the same shape).
- the P1 loadFailures-eviction removal above initially grew a backoff
  mechanism to avoid re-logging every poll tick during a persistent
  failure, but the backoff suppressed the downstream timeout-notify
  check for the same tick it suppressed the retry - a single transient
  RPC error could have delayed a genuinely-overdue Bridge notification
  by up to 10 minutes, worse than the log spam it was meant to prevent
  (a real outage fails every tracked action identically regardless of
  backoff, so there was nothing for a per-entry backoff to usefully
  save). Removed it; kept the simpler P1 fix alone - retry every tick,
  evict only on an observed non-Pending state.
- reservation_wiring.go's walletMembersResolver parameter was threaded
  through WireReservationWatchers but never read by anything downstream
  of the P1 fix that removed its only consumer; deleted the dead
  parameter from the function, its test, and the cmd/start.go call site
  (which used to feed it the WalletMembersResolver tbtc.Initialize
  returned), and corrected the call site's comment (the paired-flag
  validation is a warning, not a hard error, per the note above).
  Finished the resulting clean cutover: with cmd/start.go (Initialize's
  only caller anywhere in the repo) no longer reading it,
  tbtc.Initialize now returns only error, and the WalletMembersResolver
  interface plus node.ResolveWalletMembers (its one implementation) are
  deleted with it.
piotr-roslaniec added a commit that referenced this pull request Sep 8, 2026
…4282

Comment-only audit and fix pass across all comments this PR added or
modified: drift against current code, self-containment (drop references
to other repos / markdown docs that don't exist in keep-core), redundant
text, and inconsistent voice. No behavioral change - gofmt/build/vet
clean, diff confirmed comment-only against d1697f5.
Companion of the tbtc-v2 UTXO reservation draft (threshold-network/
tbtc-v2#1088). A reservation is a deposit the wallet anchors -- spends
in a 1-input-1-output transaction into a fresh wallet-controlled output
with no refund path -- instead of sweeping, so the reserved coins never
commingle with the pooled supply and are redeemable in-kind.

Adds the wallet-side foundations:
- wallet action types for the four reservation lifecycle actions
  (anchor, reserved redemption, re-anchor, dissolution), appended after
  the existing enum values to preserve serialized compatibility,
- coordination proposal types with marshaling and factory registration
  (JSON-based for now; switching to protobuf once the reservation
  message types are added to the coordination proto definition),
- Chain interface extensions for reading reservations and parameters
  and validating the four proposal kinds via WalletProposalValidator,
- unsigned transaction assembly for all four lifecycle shapes,
  enforcing the 1-input-1-output lineage (dissolution additionally
  spends the wallet main UTXO as its second input, per the Bridge
  rules),
- tests for action parsing, proposal marshaling roundtrips, and
  assembler input validation.

The Ethereum chain implementation stubs the new interface methods with
descriptive errors: the contract bindings can only be regenerated once
the reservation Bridge API is published with the @keep-network/tbtc-v2
package. Coordination executor wiring and tbtcpg proposal generation
follow in the same step.
piotr-roslaniec added a commit that referenced this pull request Sep 8, 2026
…m steps

COPY ./ci-shims/tbtc-artifacts in the Dockerfile sourced a directory
excluded by .gitignore and created only by client.yml's shim step;
release.yml builds the identical Dockerfile with no equivalent step,
so a fresh-checkout release build hard-failed on the missing COPY
source. Un-ignore the directory and track a placeholder so it always
exists regardless of which workflow builds the image.

Also delete the client.yml steps gated on
github.base_ref == 'reservations-epic': PR #4282's base is dev, so the
gate never fires for this PR, and once the epic lands on dev it can
never fire again. The gen/Makefile fallback rules already supply the
same artifact surface and remain the only reachable mechanism.
piotr-roslaniec added a commit that referenced this pull request Sep 8, 2026
#4282

40 confirmed findings fixed (2 P0, 7 P1, 24 P2, 7 P3); 1 P2 finding (in-kind
fee reserve watch) intentionally left unfixed - its only two remediations are
building unscoped new functionality or editing a separate spec repo, both
out of scope here.

P0:
- wire notifyReservationAcceptanceTimedOut end to end (vendored ABI, Go
  bindings, chain interfaces, ethereum adapter, watcher dispatch by
  ActionType) - acceptance-type timeout notifications could never
  succeed on-chain before this
- gate deposit-sweep's reserved-deposit filter on the reservation vault
  address instead of calling isReservedDeposit unconditionally on every
  deposit pre-activation, which risked halting sweeping network-wide
  during the binary-upgrade-before-Bridge-upgrade window

P1:
- action-timeout watcher no longer requires wallet-member resolution to
  notify a Reanchor timeout, so a dead/closed wallet's stuck action is
  now permissionlessly resolvable
- drop the load-failure eviction that permanently dropped a tracked
  action from timeout coverage after 3 RPC hiccups
- remove the mainnet reservations-activation-block placeholder pending a
  real rollout height
- correct GetReservation/GetReservationAction doc comments (absence is
  Unknown state, not an error)
- propagate chain-read errors instead of masking them as no-op
- memoize per-pass proof-invariant chain reads shared across proof-loop
  transactions
- add the missing reservations-remaining below-dust regression subtest

P2/P3: gauge visibility, nonce-bound preload validation, notifiedAt
semantics, address-comparison case sensitivity, wallet-vault fee-floor
check, dual-flag config validation, duplicate/stale comments and tests,
and assorted simplifications.

Also reverts an in-flight hard-fail config-validation check for the
Tbtc/Spv reservations dual-flag pairing back to a warning: start's config
categories never load the Maintainer section, so the check would have
hard-failed startup for any legitimate split deployment running the SPV
maintainer as a separate process.

Self-verification follow-up (same review pass, iterated against external
review of the fixes themselves): re-checked every "fixed" claim above
against the actual diff and against the reasoning behind each fix, and
made three further corrections:

- deposit-sweep follower-side (ValidateDepositSweepProposal) never
  actually got the reservation-vault gate described above - only the
  leader side (deposit_sweep task) did, so a follower could still sign a
  sweep a leader had already correctly rejected. Implemented the
  matching gate on the follower path: fetch ReservationParameters with
  the same bounded retry as the leader (fewer attempts only makes a
  wrong "reservations aren't live" guess more likely, never less, so
  there is no safety argument for the follower retrying less than the
  leader), then hard-reject on any IsReservedDeposit error or a
  confirmed reservation for any deposit the cheap vault-match prefilter
  flags as a candidate - unlike the neighboring fee soft-check just
  below, which stays deliberately log-only because a merely underpriced
  sweep is not irreversible the way sweeping a reservation is. Added the
  regression test proving detection
  (TestValidateDepositSweepProposal_RejectsReservedDeposit) and its
  counterpart proving the fail-open path still sweeps normally when
  ReservationParameters is unavailable
  (TestValidateDepositSweepProposal_SweepsWhenReservationParametersUnavailable,
  mirroring the leader's existing test of the same shape).
- the P1 loadFailures-eviction removal above initially grew a backoff
  mechanism to avoid re-logging every poll tick during a persistent
  failure, but the backoff suppressed the downstream timeout-notify
  check for the same tick it suppressed the retry - a single transient
  RPC error could have delayed a genuinely-overdue Bridge notification
  by up to 10 minutes, worse than the log spam it was meant to prevent
  (a real outage fails every tracked action identically regardless of
  backoff, so there was nothing for a per-entry backoff to usefully
  save). Removed it; kept the simpler P1 fix alone - retry every tick,
  evict only on an observed non-Pending state.
- reservation_wiring.go's walletMembersResolver parameter was threaded
  through WireReservationWatchers but never read by anything downstream
  of the P1 fix that removed its only consumer; deleted the dead
  parameter from the function, its test, and the cmd/start.go call site
  (which used to feed it the WalletMembersResolver tbtc.Initialize
  returned), and corrected the call site's comment (the paired-flag
  validation is a warning, not a hard error, per the note above).
  Finished the resulting clean cutover: with cmd/start.go (Initialize's
  only caller anywhere in the repo) no longer reading it,
  tbtc.Initialize now returns only error, and the WalletMembersResolver
  interface plus node.ResolveWalletMembers (its one implementation) are
  deleted with it.
piotr-roslaniec added a commit that referenced this pull request Sep 8, 2026
…4282

Comment-only audit and fix pass across all comments this PR added or
modified: drift against current code, self-containment (drop references
to other repos / markdown docs that don't exist in keep-core), redundant
text, and inconsistent voice. No behavioral change - gofmt/build/vet
clean, diff confirmed comment-only against d1697f5.
mswilkison and others added 11 commits September 8, 2026 15:35
Regenerates the @keep-network/tbtc-v2 ABI bindings against the
m1 bridge-integration surface (/tmp/m1-g @ 9362cda1), adding the
ReservationRouter contract to the required_contracts list and
introducing a fix_reservation_router_collision Makefile hook that
renames ReservationRouter's BitcoinTxInfo / BitcoinTxProof /
BitcoinTxUTXO structs to BitcoinTxInfo4 / BitcoinTxProof3 /
BitcoinTxUTXO4 (the next free suffixes after Bridge / WalletProposalValidator
/ MaintainerProxy).

The new abi/Bridge.go surface carries the reservation selectors exposed
on the Bridge itself -- isReservedDeposit, setReservationRouter, and
getReservationRouter -- in addition to the existing Bridge API; the
regenerated MaintainerProxy, WalletProposalValidator, RedemptionWatchtower,
and Relay bindings reflect minor ABI surface additions that landed in
the same bridge-integration commit.

The abi/ReservationRouter.go binding is the encoding source for the
six read/validate methods filled in on the next commit; its call site
is the Bridge address (Bridge.fallback routes the router selector via
delegatecall), so all reads, writes, and event/log filters must target
the Bridge address, not the router's own deployment address.
…bindings

Replaces the seven reservation read/validate stubs in tbtc.go
(those declared today in pkg/tbtc/chain.go:430-480, previously
returning "reservations not supported yet" errors) with real
implementations backed by the regenerated abigen bindings.

Three view reads (GetReservation, GetReservationAction,
ReservationParameters) are reached through tc.reservationRouter, a new
binding constructed against the Bridge address -- the router code only
executes via Bridge.fallback's delegatecall, so binding the
ReservationRouter ABI at the Bridge address is the only configuration
that gives the operator a live read path (the deployed router address
holds empty storage). Two on-chain proposal validators
(ValidateReservationAnchorProposal,
ValidateReservationReanchorProposal) are reached through the existing
tc.walletProposalValidator handle against the regenerated
WalletProposalValidatorReservation*Proposal ABI structs.

Two further validators (ValidateReservedRedemptionProposal,
ValidateReservationDissolutionProposal) remain unsupported on this
milestone's bridge-integration surface -- the WalletProposalValidator
contract does not expose those entry points -- so their bodies return
an explicit "validator not exposed on the m1 bridge-integration
surface" error. The chain.go interface declarations are satisfied so
the package compiles; downstream tasks can replace these bodies once
the missing validators land on the contract side.

Thin field-by-field abigen-to-Go converters are added for the three
view structs (convertReservationFromAbiType,
convertReservationActionFromAbiType,
convertReservationParametersFromAbiType), plus three small parsers
(parseReservationState, parseReservationActionType,
parseReservationActionState) that mirror the on-chain enum layouts.
The reservationRouter field is constructed in newTbtcChain via the
reservationRouterBinding helper, which makes the storage/address
rationale explicit at the call site rather than burying it in the
struct field comment.
…t subscriptions

Adds the second half of the PR H reservation chain-interface surface
(section 1.2 of the build brief):

* Six write methods bound to the Bridge address via the
  reservationRouter handle (RequestReservationAcceptance,
  RequestReservationReanchor, SubmitReservationProof,
  NotifyReservationActionTimeout, NotifyStaleReservedDeposit,
  NotifyReservationStranded). Submission pattern mirrors the
  existing SubmitRedemptionProofWithReimbursement flow: GasEstimate
  + 20% margin + ethutil.TransactionOptions.
* Twelve additional read/view methods (ReservationCaps,
  WalletReservationsAmount, WalletReservationsCount,
  WalletReservations, ReservationByAnchorUtxo, ReservedDepositWallet,
  PendingReservedDeposits, Reservations, ReservationActions,
  ActiveReservationsCount, ReservationRouter, IsReservedDeposit),
  plus ReservationParametersFull as an alias of ReservationParameters.
  IsReservedDeposit and ReservationRouter read via the Bridge
  binding because they map to Bridge state.
* New Go types ReservationRequest, ReservationActionRecord,
  BitcoinTxInfo, BitcoinTxProof, BitcoinTxUTXO mirror the on-chain
  ReservationRouter view structs verbatim.
* Thirteen event subscriptions and twelve filter structs for every
  reservation event listed in the brief, filtering against the
  Bridge address (delegatecall preserves the caller's address
  context so router-emitted events carry the Bridge address).
* localChain mocks for all of the above so the interface stays
  satisfiable by the test double.

The reservationRouter binding remains bound to the Bridge address
(invariant 3 of ReservationRouter.sol) - no second binding against
the router's standalone address is constructed.
ReservationAnchorProposal/ReservationReanchorProposal's Marshal panicked
on a nil *big.Int field (3 fields across the two types), and the two
Unmarshal implementations disagreed on zero-hash rejection: re-anchor
rejected an all-zero TargetWalletPublicKeyHash but anchor accepted an
all-zero DepositFundingTxHash. Both proposal types were also the only
wire-format methods in the file missing the doc-comment convention
every sibling type follows.
ReservationsActivationBlock was a single compile-time constant
(26,500,000) with no per-network override, unlike
DepositSweepEveryWindowActivationBlock's graceful testnet degrade -
this gate's degrade path was 'feature entirely absent' on every
lower-tip chain for years. Threaded ethereumNetwork through
node.NewNode (mirroring the existing groupParameters/chain plumbing)
down to the activation-block lookup, and copied
DepositSweepEveryWindowActivationBlock's upgrade-precondition warning
onto this gate's comment: a genuinely-old binary's coordination
messages fail to unmarshal, causing followers to spuriously fault an
honest, upgraded leader for the entire rollout window.

Also wires the reservation metrics recorder into the proposal
generator alongside the existing redemption metrics wiring, so cached
coordination executors created before metrics are set still pick up
the reservation gauges (see pkg/clientinfo saturation gauges commit).
The maintainer already warned when its reservation flag was on but the
client's was off; the client emitted no mirrored warning for the
reverse case, where the client originates reservation actions but the
maintainer never proves them on-chain, guaranteeing action timeouts.
Emit the paired warning naming the maintainer flag it depends on.
Free-slot and occupancy monitors were named as B-specific operational
duties and leading indicators of the reservation saturation cliff, but
only per-action execution counters existed - nobody could see
reservation capacity approaching its cap before acceptances silently
stopped.

Registers four gauges (active_reservations_count,
max_active_reservations, live_wallets_count, wallet_reservations_count)
gated on the same reservationsEnabled flag as the existing wallet
action counters, sourced from chain calls the acceptance/re-anchor
tasks already make. Wires a metrics recorder into both tasks via the
proposal generator, mirroring the existing redemption metrics wiring.
…ut memoization

The reservation proof loop's retry eviction (after 3 failed
GetReservationAction passes) deleted the pending event without rewinding
the scan cursor, which had already advanced past the event's block - a
transient RPC outage permanently stranded an already-confirmed Bitcoin
anchor/re-anchor transaction's SPV proof. Eviction now rewinds the
cursor behind the lost event's block so the next pass re-fetches and
rediscovers it, applied symmetrically to both the acceptance and
re-anchor paths.

The stale-deposit watcher's deriveTimeoutFromReveal re-ran a full
216,000-block eth_getLogs scan every poll tick for every deposit with
no requested action yet, with no memoization of the derived, immutable
timeout. Added a per-deposit-key memoization map so subsequent ticks
skip straight to the timeout comparison.

Also completes the reanchor-proof submission counters (2 of 10 return
paths in submitReservationActionProof were still uncounted) and adds
TestIsReservedDeposit_PointerIdentity, pinning the fix for this PR's
own historical pointer-identity map-key bug.

Several P2/P3 findings in this cluster (M-7 nonce-aware timeout check,
WalletMembersResolver architecture, dead-code cluster removal,
tautological guard removal, unused alias, startStaleDepositPoll
testability) remain deferred - see spv-cluster-followups.md. A first
attempt combining these with the structural M-16 redesign broke 3
existing tests and was reverted; only the independently-safe P1 subset
above is included here.
…ce reconciliation, caps

Reservation acceptance:
- findReservationAcceptanceCandidate ran an unbounded eth_getLogs scan
  from genesis per candidate deposit per coordination window - errors on
  RPC providers that cap eth_getLogs ranges (silently skipping the
  candidate forever), and made the RequestNonce+1 retry branch dead code.
  Bounded the scan to ReservationAcceptanceLookBackBlocks, matching every
  other event scan in the diff.
- Added a vault-not-configured guard against the actual zero-address hex
  string the chain.Address converter produces (the prior check compared
  against "", which the converter never returns).
- RequestNonce is predicted client-side before the on-chain write and
  was never reconciled by re-reading the reservation afterward; a
  follower would hard-fail GetReservationAction forever if the Bridge
  assigned a different nonce. Added hasPendingAction, a re-read-and-
  compare guard against duplicate in-flight requests, shared by both
  the acceptance and re-anchor paths.
- maxActiveReservations==0 (the global circuit-breaker cap) was treated
  as unlimited instead of failing closed; added boundary tests at the
  exact ==cap/==cap+1 edge for all three amount-based caps.
- Fixed the typed-nil interface return on the below-minimum path.

Reservation re-anchor (M-27, resolved via tbtc-v2 source): the
below-dust trigger requested a re-anchor from the client's ordinary
operator key even though tbtc-v2's on-chain requestReservationReanchor
caps that path to privileged callers, making the trigger dead code as
shipped. Removed the privileged-caller trigger; wired
NotifyMovingFundsBelowDust (permissionless, already present in the
Bridge ABI but never called from this client) so a wallet whose
reservations have already fully drained closes once its last
reservation's own re-anchor completes and its main UTXO computes below
the moving-funds dust threshold. Added a pre-check
(AssembleReservationReanchorTransaction, build-and-discard) before
requesting re-anchor, mirroring the acceptance path's existing
pre-check. Moved hasPendingAction to reservation_acceptance.go, its
only real caller, and corrected its doc comment.

Extends the shared Chain interface (NotifyMovingFundsBelowDust) and its
LocalChain/TbtcChain implementations to support the above.
…BroadcastChannel

NewTimeTicker's piping goroutine selects between an already-elapsed
timerTick.C and ctx.Done(); when ReleaseBroadcastChannel's cancel()
races an elapsed tick, Go's pseudo-random select can let exactly one
straggler tick (a harmless retransmission of an already-sent message)
through before the goroutine observes cancellation. The test's strict
zero-deliveries-after-release assertion made this flaky (~80% failure
rate reproduced locally in isolation). Absorb the one possible
straggler in a short settle window before measuring the real invariant:
no continued firing once release has taken effect.
…eBroadcastChannel

The settle-window drain added to absorb the expected single straggler
tick discarded its count unchecked, so a genuine regression where the
ticker fires more than once after release would only be caught by the
second, stricter window - not at the settle step itself, where it's
easier to diagnose. Assert the settle window sees at most one tick.
The 'records acceptance request, skipping duplicate on subsequent run'
test relied on hasPendingAction's fail-closed default on a missing
GetReservationAction record, not on the intended pending-state
detection path - its comment still described the removed
PastReservationAcceptanceRequestedEvents mechanism. Explicitly set
the action record to Pending after the first run so the second run's
dedup assertion genuinely exercises hasPendingAction's happy path.
…mable candidate scan

- GetReservation read errors now skip the deposit for this window
  instead of falling through with a fabricated nonce and bypassed
  eligibility/pending-action gates; only a successful read reporting
  State==Unknown && RequestNonce==0 is treated as 'not yet created'.
- Pre-write assemble/validate failures in proposeReservationAcceptance
  no longer abort the whole coordination window: Run retries the next
  candidate via a skip-set instead of starving every other deposit on
  the wallet behind one permanently-rejected one. Post-write failures
  still abort as before.
- Add an incremental per-wallet reveal cursor so each window scans only
  the delta since the last run instead of the full ~30-day look-back,
  plus a hard cap on candidates examined per run.
- Add scenario_8.json: two simultaneous candidates (one ineligible, one
  eligible) proving the scan doesn't stop at the first ineligible
  deposit, guarding against a regression of the head-of-line-blocking
  fix.
- Drop the stale merge-history comment in tbtcpg.go.
…n metric gating

- ReservationReanchorTask.Run now breaks instead of continuing once a
  post-write post-condition check fails after RequestReservationReanchor
  already authorized an action, guaranteeing at most one on-chain
  authorization per pass.
- findTargetWallet falls back to an unbounded registration-event scan
  when the bounded ~30-day window yields no live wallet, instead of
  returning no proposal indefinitely while live wallets exist.
- Publish live_wallets_count unconditionally every Run, matching the
  sibling saturation gauges, instead of only after guards that are
  false in steady state.
- Register wallet_action_reservation_* counters/histograms
  unconditionally: reservation action execution is not gated on
  Tbtc.Reservations.Enabled, so gating their registration silently
  dropped observability for exactly the operators most likely to be
  surprised by unconditional execution.
…he growth

- CheckStaleReservedDeposit now returns Keep instead of Drop for a
  reserved deposit on a Live wallet, so the poller re-evaluates it
  after any wallet state change instead of permanently orphaning it
  the first time it observes a Live wallet.
- notified/memoizedTimeout caches are now cleared when a deposit
  resolves (Drop/Notified), instead of growing for the process
  lifetime; the memoized deadline is invalidated when the governance
  ReservationActionTimeout parameter changes instead of silently
  reusing an earlier, shorter cached deadline.
- Doc comment no longer claims the check is 'intentionally pure' given
  its Bridge-notification side effect and receiver-map mutations.
…ations

- pollPendingActions no longer deletes a tracked action after a
  successful check; a submitted-but-dropped/reverted notification
  previously vanished from tracking with err==nil and was never
  retried. Eviction now relies solely on the existing state-driven
  branch (action no longer Pending on-chain).
- Reuse the action already loaded during the poll pass in the
  timeout-notification path instead of re-reading it a second time.
- Bound repeated GetReservationAction read failures with a retry
  counter so permanently unreadable entries are eventually evicted
  instead of accumulating forever.
- Delete the unreferenced backward-compatibility alias constant.
- Fix placeholder-era doc comments describing a synchronous
  driving integration that was never wired; production only ever
  starts the fixed-interval background Run loop.
…d eviction cycle

- Delete the tautological Outputs[0].Value != amount-fee comparisons in
  the acceptance and re-anchor transaction matchers (fee is itself
  defined as amount - Outputs[0].Value on the preceding line, so the
  check could never be true); the fee<=TxMaxFee bound is the real
  guard.
- Remove the retry-count/cursor-rewind-on-eviction machinery: on the
  third consecutive read failure it rewound the scan cursor and
  deleted the retry counter, so the next pass re-fetched the identical
  event with its retry count reset to zero, an unbounded
  evict-rediscover-retry-from-zero cycle. Errors are now logged and the
  event stays pending until a successful read shows a terminal state.
- Delete the unused wallet-event adapter types and their test-only
  production wrapper functions; retarget their tests at the
  candidate-map/predicate logic actually used in production.
- Enforce the same fee<=TxMaxFee bound in the acceptance matcher's
  not-found branch as the found branch, instead of only OutputValue>0.
…rnals

WireReservationWatchers is the only production composition point for
this watcher; its constructor, type, and check method had no external
callers. Make them package-private, matching this package's other
watcher internals.
…ch-error handling

- Bound WireReservationWatchers' startup wallet-registration scan to a
  ~30-day look-back instead of scanning full chain history
  (StartBlock:0) on every reservation-enabled client boot; older
  wallets remain covered by the live subscription and the existing
  per-wallet close re-check.
- Compare a revealed deposit's event.Vault against the locally-known
  ReservationVault before calling IsReservedDeposit, skipping the
  eth_call entirely for deposits targeting an unrelated vault.
- Fix the stale-deposit poll's batch-error handling: a single failed
  IsReservedDeposit call previously broke out of the whole event batch
  and then advanced lastSeenBlock past the entire window regardless,
  silently dropping every deposit after the failed one; it now skips
  only the failed deposit and still examines the rest of the window.
- Wire up the stranding watcher's now-package-private constructor and
  type, and fix its placeholder-era poll-interval doc comment.
buildReservationProofMainUtxo's naming/comments read as if it resolves
a wallet main UTXO, but ReservationRouter.sol's own devdoc marks the
parameter unused until milestone 2. Document that explicitly and pin
the current (harmless) encoding with a regression test so a future
milestone-2 activation of this parameter doesn't silently inherit the
wrong value.
- Rewrite the Reservations config field docs: the flag gates proposal
  generation, watcher wiring, and metrics registration only; execution
  and co-signing are unconditional past the activation block by
  design, not 'side-effect free' as the comment previously claimed.
- Add an explicit ethereum.Sepolia entry to the reservations
  activation-block table; restrict the activate-at-0 fallback to
  ethereum.Developer/Unknown so no other public network silently
  inherits immediate activation.
- Replace a misleading coordination_test.go block constant that
  implied mainnet-gate significance it didn't have in that test.
- Note in ParseWalletActionType why wire slots 7 and 9 are rejected:
  intentionally incomplete until M2 action types are implemented.
…m steps

COPY ./ci-shims/tbtc-artifacts in the Dockerfile sourced a directory
excluded by .gitignore and created only by client.yml's shim step;
release.yml builds the identical Dockerfile with no equivalent step,
so a fresh-checkout release build hard-failed on the missing COPY
source. Un-ignore the directory and track a placeholder so it always
exists regardless of which workflow builds the image.

Also delete the client.yml steps gated on
github.base_ref == 'reservations-epic': PR #4282's base is dev, so the
gate never fires for this PR, and once the epic lands on dev it can
never fire again. The gen/Makefile fallback rules already supply the
same artifact surface and remain the only reachable mechanism.
The gitkeep placeholder tracked in the prior commit made
'ls -A /tmp/tbtc-artifacts' always non-empty, so the shim-copy guard
fired unconditionally on every development build even with no shim
JSON present, and 'cp /tmp/tbtc-artifacts/*.json ...' then failed on
the unmatched glob. Test for *.json specifically instead of any file
in the directory.
An invented finite block height for Sepolia silently becomes live
behavior if nobody edits it before release - the exact rollout risk
the review finding was about. Sepolia now has no map entry and falls
through to the existing math.MaxUint64 never-activate default,
provably inert until a real rollout height is chosen and added
explicitly.
…laked

Skipping a deposit outright on a single IsReservedDeposit error left it
permanently unscanned once lastSeenBlock advanced past its event's
block range. Track it in pending instead: CheckStaleReservedDeposit
performs its own independent IsReservedDeposit re-check every tick, so
speculatively tracking it is safe and lets the next tick resolve it
correctly.
…ification

The prior notified-bool fix stopped deleting the entry but never
retried it either: once notified was set, pollPendingActions skipped
the entry forever regardless of whether the notification tx actually
landed, leaving a dropped/reverted notification permanently
unretried - the same end state as the eviction bug it replaced, just
with a leaked map entry instead of a missing one.

Replace the bool with a notifiedAt timestamp and re-offer the action
to NotifyReservationActionTimeout once actionTimeoutRenotifyInterval
(10 minutes, matching this package's existing backoff convention) has
elapsed since the last attempt and the action is still Pending. Add a
test that drives nowFn past the backoff window and asserts the retry
notification is actually submitted.
The 144 bound is applied by flag registration, so Config values built
programmatically (tests, wiring paths that bypass cmd/flags.go) yielded
a cap of 0 and getProofInfo skipped every proof as
proofSkipExceededMaxHeaders. Normalize once at proveReservationTransaction
and pin the behavior with a subtest that submits under an explicit 0 -
it fails if the guard is ever removed.
#4282

40 confirmed findings fixed (2 P0, 7 P1, 24 P2, 7 P3); 1 P2 finding (in-kind
fee reserve watch) intentionally left unfixed - its only two remediations are
building unscoped new functionality or editing a separate spec repo, both
out of scope here.

P0:
- wire notifyReservationAcceptanceTimedOut end to end (vendored ABI, Go
  bindings, chain interfaces, ethereum adapter, watcher dispatch by
  ActionType) - acceptance-type timeout notifications could never
  succeed on-chain before this
- gate deposit-sweep's reserved-deposit filter on the reservation vault
  address instead of calling isReservedDeposit unconditionally on every
  deposit pre-activation, which risked halting sweeping network-wide
  during the binary-upgrade-before-Bridge-upgrade window

P1:
- action-timeout watcher no longer requires wallet-member resolution to
  notify a Reanchor timeout, so a dead/closed wallet's stuck action is
  now permissionlessly resolvable
- drop the load-failure eviction that permanently dropped a tracked
  action from timeout coverage after 3 RPC hiccups
- remove the mainnet reservations-activation-block placeholder pending a
  real rollout height
- correct GetReservation/GetReservationAction doc comments (absence is
  Unknown state, not an error)
- propagate chain-read errors instead of masking them as no-op
- memoize per-pass proof-invariant chain reads shared across proof-loop
  transactions
- add the missing reservations-remaining below-dust regression subtest

P2/P3: gauge visibility, nonce-bound preload validation, notifiedAt
semantics, address-comparison case sensitivity, wallet-vault fee-floor
check, dual-flag config validation, duplicate/stale comments and tests,
and assorted simplifications.

Also reverts an in-flight hard-fail config-validation check for the
Tbtc/Spv reservations dual-flag pairing back to a warning: start's config
categories never load the Maintainer section, so the check would have
hard-failed startup for any legitimate split deployment running the SPV
maintainer as a separate process.

Self-verification follow-up (same review pass, iterated against external
review of the fixes themselves): re-checked every "fixed" claim above
against the actual diff and against the reasoning behind each fix, and
made three further corrections:

- deposit-sweep follower-side (ValidateDepositSweepProposal) never
  actually got the reservation-vault gate described above - only the
  leader side (deposit_sweep task) did, so a follower could still sign a
  sweep a leader had already correctly rejected. Implemented the
  matching gate on the follower path: fetch ReservationParameters with
  the same bounded retry as the leader (fewer attempts only makes a
  wrong "reservations aren't live" guess more likely, never less, so
  there is no safety argument for the follower retrying less than the
  leader), then hard-reject on any IsReservedDeposit error or a
  confirmed reservation for any deposit the cheap vault-match prefilter
  flags as a candidate - unlike the neighboring fee soft-check just
  below, which stays deliberately log-only because a merely underpriced
  sweep is not irreversible the way sweeping a reservation is. Added the
  regression test proving detection
  (TestValidateDepositSweepProposal_RejectsReservedDeposit) and its
  counterpart proving the fail-open path still sweeps normally when
  ReservationParameters is unavailable
  (TestValidateDepositSweepProposal_SweepsWhenReservationParametersUnavailable,
  mirroring the leader's existing test of the same shape).
- the P1 loadFailures-eviction removal above initially grew a backoff
  mechanism to avoid re-logging every poll tick during a persistent
  failure, but the backoff suppressed the downstream timeout-notify
  check for the same tick it suppressed the retry - a single transient
  RPC error could have delayed a genuinely-overdue Bridge notification
  by up to 10 minutes, worse than the log spam it was meant to prevent
  (a real outage fails every tracked action identically regardless of
  backoff, so there was nothing for a per-entry backoff to usefully
  save). Removed it; kept the simpler P1 fix alone - retry every tick,
  evict only on an observed non-Pending state.
- reservation_wiring.go's walletMembersResolver parameter was threaded
  through WireReservationWatchers but never read by anything downstream
  of the P1 fix that removed its only consumer; deleted the dead
  parameter from the function, its test, and the cmd/start.go call site
  (which used to feed it the WalletMembersResolver tbtc.Initialize
  returned), and corrected the call site's comment (the paired-flag
  validation is a warning, not a hard error, per the note above).
  Finished the resulting clean cutover: with cmd/start.go (Initialize's
  only caller anywhere in the repo) no longer reading it,
  tbtc.Initialize now returns only error, and the WalletMembersResolver
  interface plus node.ResolveWalletMembers (its one implementation) are
  deleted with it.
…4282

Comment-only audit and fix pass across all comments this PR added or
modified: drift against current code, self-containment (drop references
to other repos / markdown docs that don't exist in keep-core), redundant
text, and inconsistent voice. No behavioral change - gofmt/build/vet
clean, diff confirmed comment-only against d1697f5.
PR #4280 is merged and explicitly deferred these two tests (per its own
description) - it did not land the missing go-ethereum simulated-backend
infrastructure. The prior wording ('blocked on that infra landing. See
PR #4280') read as if #4280 was the pending vehicle for that infra, which
is now misleading since #4280 shipped without it. Point instead at the
real state: the infra is unbuilt and has no owning PR.
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.

2 participants