Skip to content

feat: safe harbour sweep activation - #796

Open
sapinb wants to merge 21 commits into
feat/bridge-sweep-prereq-str-3937from
feat/deposit-sweep-logic-str-3938
Open

feat: safe harbour sweep activation#796
sapinb wants to merge 21 commits into
feat/bridge-sweep-prereq-str-3937from
feat/deposit-sweep-logic-str-3938

Conversation

@sapinb

@sapinb sapinb commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

Stacked on #756 . Wires the persisted safe-harbour latch to bridge behaviour; the sweep machinery it drives landed in the base branch.

  • New deposits are refused a gate in try_register_deposit (best-effort by design; anything admitted late is caught by the sweep).
  • Safe-window deposits are aborted new SafeHarbourAbort transition for Created/GraphGenerated (no partial gossiped yet), reusing the takeback graph-teardown signal. Later pre-Deposited states finish and are swept.
  • Every live deposit UTXO is swept a per-block scan seeds SweepRequested (with the frozen address) for Deposited and all withdrawal states, and SafeHarbourAbort for the safe window. Re-emission per block is the retry mechanism; the SM classifies replays as duplicates.
  • Withdrawal-path duties are suppressed at dispatch so no user is fronted and no claim advances mid-sweep. Defensive duties (contest, counterproofs, slash, unstaking burn) are never suppressed, they are the funds defense when a rogue operator stalls the sweep and fires its pre-signed claim path.

Also hardens process_graph_available: late graph links are now non-fatal (duplicate/rejected) instead of a pipeline-killing InvalidEvent: defense in depth against the catch-all pattern that previously caused a fleet crash via assignment replays.

Tests

Unit tests per transition plus six functional tests across two CI groups:
safe_harbour/ (detection, halt + unassigned sweep with exact payout/anchor values, safe-window abort) and safe_harbour_withdrawal/ (assigned-deposit sweep with fulfillment suppression, sweep-vs-payout race, contest/slash ladder under an active latch).

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature/Enhancement (non-breaking change which adds functionality or enhances an existing one)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactor
  • New or updated tests
  • Dependency Update

Notes to Reviewers

Checklist

  • I have performed a self-review of my code.
  • I have commented my code where necessary.
  • I have updated the documentation if needed.
  • My changes do not introduce new warnings.
  • I have added (where necessary) tests that prove my changes are effective or that my feature works.
  • New and existing tests pass with my changes.
  • I have disclosed my use of AI in the body of this PR.

Related Issues

STR-3938, STR-3939

@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.54601% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.39%. Comparing base (37ad0e4) to head (5a215c8).

Files with missing lines Patch % Lines
crates/bridge-sm/src/deposit/events.rs 0.00% 4 Missing ⚠️
crates/orchestrator/src/safe_harbour_scan.rs 97.84% 3 Missing ⚠️
crates/orchestrator/src/pipeline.rs 98.59% 1 Missing ⚠️
@@                          Coverage Diff                          @@
##           feat/bridge-sweep-prereq-str-3937     #796      +/-   ##
=====================================================================
+ Coverage                              87.71%   88.39%   +0.67%     
=====================================================================
  Files                                    257      258       +1     
  Lines                                  32477    32786     +309     
=====================================================================
+ Hits                                   28488    28980     +492     
+ Misses                                  3989     3806     -183     
Files with missing lines Coverage Δ
crates/bridge-sm/src/deposit/machine.rs 100.00% <100.00%> (ø)
...rates/bridge-sm/src/deposit/transitions/deposit.rs 99.69% <100.00%> (+<0.01%) ⬆️
crates/bridge-sm/src/deposit/transitions/sweep.rs 100.00% <100.00%> (ø)
...ates/orchestrator/src/events_classifier/onchain.rs 96.38% <100.00%> (+0.26%) ⬆️
crates/orchestrator/src/sm_types.rs 100.00% <100.00%> (+8.82%) ⬆️
crates/orchestrator/src/testing.rs 100.00% <100.00%> (ø)
crates/tx-graph/src/transactions/deposit.rs 93.75% <100.00%> (+0.95%) ⬆️
crates/orchestrator/src/pipeline.rs 93.86% <98.59%> (+3.21%) ⬆️
crates/orchestrator/src/safe_harbour_scan.rs 97.84% <97.84%> (ø)
crates/bridge-sm/src/deposit/events.rs 50.60% <0.00%> (+15.15%) ⬆️

... and 9 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sapinb
sapinb force-pushed the feat/deposit-sweep-logic-str-3938 branch from 4bc54c5 to 231957c Compare August 18, 2026 10:50
@sapinb
sapinb marked this pull request as ready for review August 18, 2026 10:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 231957c7ee

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/orchestrator/src/pipeline.rs Outdated
Comment thread crates/orchestrator/src/pipeline.rs Outdated
sapinb and others added 19 commits August 20, 2026 22:32
try_register_deposit is the single chokepoint that admits a DRT and
registers its DepositSM and GraphSMs, so gate it on the registry latch
right after the envelope pre-filter. The gate is best-effort by design:
a DRT admitted before the node latches is caught by the sweep once the
deposit reaches Deposited, which is what keeps the check a cheap
synchronous read on the block path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the SafeHarbourAbort event and its transition, mirroring the user
takeback: Created and GraphGenerated move to Aborted and emit the
existing DepositRequestTakenBack graph-teardown signal. The window ends
at DepositNoncesCollected, where this operator's partial signature is
already gossiped and cannot be recalled; later deposits finish and are
swept instead.

No on-chain takeback exists for this abort, so the signal carries the
deposit-request txid. Replays against an aborted deposit are duplicates
(the per-block scan re-emits until the deposit leaves the safe window);
every other state is classified explicitly so a future state cannot
silently inherit a fatal arm.

The abort can also pre-empt graph generation, so a late GraphAvailable
signal reaching an aborted deposit is classified as a non-fatal
rejection instead of falling through to the fatal invalid-event arm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…bour scan

After each buried block is processed, enumerate the registry's deposits
while the safe-harbour latch is set: Deposited deposits are seeded with
SweepRequested carrying the frozen safe-harbour descriptor, and
safe-window deposits (Created/GraphGenerated) with SafeHarbourAbort.
The seeds flow through the same applicator batch as block events, so
duties and persistence follow the normal path.

Re-running every block is the retry mechanism: the deposit SM classifies
replays as duplicates, so emission is idempotent until each deposit
leaves its scanned state. Mid-signing deposits (partial already
gossiped) are left to finish and become sweepable at Deposited, and an
Assigned deposit whose deadline lapses reverts to Deposited and becomes
sweepable on its own.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ddress

The flagship safe-harbour sweep scenario: N completed deposits, a real
Defcon1 council tx, then one single-input sweep tx per deposit paying
the frozen address exactly deposit - fee - anchor, with the
operator-keyed CPFP anchor present at dust value.

The shared helpers (activation via dev-cli + latch wait, sweep-tx wait
and shape assertions, fee constants pinned to the Rust helpers) live in
utils/safe_harbour.py for the remaining sweep tests to reuse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A DRT confirmed after every operator latches must never be admitted as a
deposit. A pre-activation deposit first proves the pipeline works, then
the post-activation DRT is watched across its burial and an observation
window on every operator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Holds one operator down so the deposit deterministically pins in Created
(pausing block production would not work: graph generation and signing
progress off-chain), then activates. The live operators must abort the
deposit, and the held-down operator must converge after restart by
either refusing the replayed DRT or aborting the admitted deposit —
the halt gate is best-effort by design, so both outcomes are legal, but
completing the deposit is not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sweep is N-of-N, so one offline operator must stall it: the deposit
UTXO stays unspent across an observation window while the operator is
down. After the restart the operator latches from the ASM tip and joins
the round via nagging, and the sweep confirms with the expected shape.
No new bridge code is involved — this pins the nag/retry and latch
recovery paths working together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r is active

The deposit SM stops emitting withdrawal duties once it enters the sweep
flow, but that leaves the switch-over window and the graph SMs (which
have no sweep flow of their own) uncovered. Drop is_withdrawal_path()
duties in the pipeline dispatch loop — the loop holds the registry, the
duty dispatcher does not — so no user is fronted and no claim advances
towards a payout while deposits are being swept.

The defensive duties (contest, counterproofs, slash, unstaking burn)
never match the taxonomy and always dispatch: they are the only funds
defense when a rogue operator stalls the sweep and fires its pre-signed
claim path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-block scan now seeds SweepRequested from all withdrawal states
(Assigned, Fulfilled, PayoutDescriptorReceived, PayoutNoncesCollected,
CooperativePathFailed) in addition to Deposited. The deposit UTXO is a
fixed N-of-N key-path output that withdrawal progress never touches, so
the sweep entry already accepts every one of these states — this wires
the trigger. Whatever payout progress the source carried is abandoned;
a racing payout is resolved on chain, whichever tx spends the outpoint
first.

Deposits already in the sweep flow are skipped rather than re-seeded so
routine scans do not log a duplicate per deposit per block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The assignee is held down when the assignment lands, pinning the deposit
in Assigned with no fulfillment possible. Activation must sweep the
deposit out of Assigned on the live operators, and the restarted
assignee must join the stalled N-of-N round instead of fulfilling: the
mux orders safe-harbour state ahead of the replayed assignment, so the
fulfillment duty is suppressed at dispatch. Asserts the sweep shape and
that the assignee's log contains no fulfillment submission.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Activation lands after the fulfillment confirms, while the cooperative
payout round is in flight. The winner is intentionally unspecified: the
test asserts the deposit outpoint is spent by exactly one transaction —
the sweep (full shape checked) or the payout — and that every operator
converges with no pending withdrawal and a live RPC afterwards.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scenario the duty-suppression carve-out exists for: a claimer goes
mute after posting its claim (stalling the N-of-N sweep) and the safe
harbour activates. The watchtower contest, bridge-proof timeout, and
slash must all proceed with the latch set — extraction through the
pre-signed claim path costs the claimer its stake. Once the claimer
returns, the stalled sweep completes to the frozen address, proving the
funds never left through the claim path.

Mirrors the bridge-proof-timeout slashing flow with activation inserted
after the claim confirms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
process_graph_available sent every state past Created (except the
abort-tolerated Aborted) to InvalidEvent via a catch-all. GraphMessage
is not softened, so a hit is fatal: the orchestrator raises an
InvariantViolation that takes the node down, and the failure recurs on
restart because the unpersisted batch rolls back and nagging redelivers
the partial that re-emits the signal.

No current sequence reaches the arm — the graph SM emits GraphAvailable
exactly once (final graph partial), and both abort paths tear the
graphs down in the same atomic batch — so this is hardening: the arm's
safety rests on invariants maintained elsewhere, and the same
catch-all pattern already caused a real fleet crash when the ASM
replayed assignments into the sweep states. States past Created hold
the full graph set, so a late link there is a replayed signal
(duplicate); the match is exhaustive so a future state must be
classified rather than silently inherit a fatal arm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…test

The halt test's only unique content was a post-activation DRT and an
observation window; its pre-activation sanity deposit duplicated the
flagship's deposits, and the flagship's sweep-confirmation waits already
span the window. Broadcasting the DRT right after activation and
asserting it is never admitted once the sweeps confirm covers the same
contract with one fewer environment spin-up, and the combined scenario
is the activation contract in one place: halt new custody, sweep
existing deposits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The liveness test was mechanically a subset of the assigned test —
operator down at activation, sweep completes after restart via nagging
and latch recovery — differing only in its stall assertion and in
sweeping from Deposited rather than Assigned. The from-Deposited path
is covered by the unassigned flagship, so asserting the stall (deposit
UTXO unspent while the assignee is down) before the restart in the
assigned test preserves the full contract with one fewer environment
spin-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI runs one parallel job per directory under tests/, and safe_harbour
had grown into the largest group. Keep the activation semantics
(detection, halt + unassigned sweep, safe-window abort) in
safe_harbour/ and move the in-flight withdrawal interplay (assigned
sweep, sweep-vs-payout race, defensive duties) to
safe_harbour_withdrawal/, which also groups the tests that need the
checkpoint/assignment machinery. Group discovery picks up the new
directory automatically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wait_until_bridge_proof_timedout polls pendingWithdrawalInfo, which
reports None once the scan sweeps the deposit out of the withdrawal
states — within a couple of blocks of the latch — so the phase check
could never succeed under safe harbour and timed out in CI. The
contest-proof connector spend already proves the timeout on-chain (only
the proof-timeout tx can spend it with the claimer down), and the slash
wait covers the graph-side tracking, since the slash only fires after
the graphs process the timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sapinb
sapinb requested review from Zk2u and storopoli as code owners August 20, 2026 16:48
@sapinb
sapinb force-pushed the feat/deposit-sweep-logic-str-3938 branch from 231957c to ac41bd9 Compare August 20, 2026 16:49

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac41bd94d7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/orchestrator/src/pipeline.rs Outdated
…very

The mux prioritizes buried blocks over ASM events, so the activation
block was processed before the latch was set and the scan only ran on
the next buried block — sweeps and aborts lagged activation by a block
interval, unbounded if block production stalls, and a latch recovered
from persistence waited the same way after a restart. Latch first, then
run the scan through the regular applicator/persist/dispatch stages: a
first activation falls through to the stages it previously skipped,
startup seeds the scan once when a persisted latch is recovered, and
the per-block scan remains the retry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rajil1213
Rajil1213 previously approved these changes Aug 21, 2026

@Rajil1213 Rajil1213 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall looks good.

This still needs rebasing, and perhaps more importantly, updating the orchestrator design doc to include the latch as part of the pipeline.

Comment thread crates/orchestrator/src/events_classifier/onchain.rs
Comment thread crates/orchestrator/src/safe_harbour_scan.rs Outdated
bridge-sm, the rpc-server monitoring tests and the safe-harbour scan each
hand-rolled the same two connectors before calling DepositTx::new. Expose
build_test_deposit_tx behind tx-graph's existing test_utils feature and
point all three at it.

The fixture lives in tx-graph rather than test-utils because test-utils
depends only on primitives; hosting a DepositTx there would invert that
against tx-graph's dev-dependency on it.

Two incidental changes: the monitoring fixture is now deterministic (it
drew fresh random keys per call, and nothing reads the tx), and the
recovery delay is uniformly 1008 -- it only shapes the deposit-request
tapscript, since the Normal spend path always sequences as Sequence::MAX.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a215c8f04

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/orchestrator/src/safe_harbour_scan.rs
Comment thread crates/orchestrator/src/safe_harbour_scan.rs
@sapinb
sapinb added this pull request to the merge queue Aug 25, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Aug 25, 2026
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