Skip to content

fix(platform-wallet): re-broadcast a Broadcast-status lock on resume#4009

Merged
QuantumExplorer merged 1 commit into
v4.1-devfrom
claude/rebroadcast-on-broadcast-resume
Jul 5, 2026
Merged

fix(platform-wallet): re-broadcast a Broadcast-status lock on resume#4009
QuantumExplorer merged 1 commit into
v4.1-devfrom
claude/rebroadcast-on-broadcast-resume

Conversation

@QuantumExplorer

Copy link
Copy Markdown
Member

Follow-up to the ADDR-09 finality work (#4006 / #4007). Surfaced by the review of #4007: resuming an orphaned Broadcast-status lock could hang forever.

Issue being fixed or feature implemented

resume_asset_lock's Broadcast arm only waited for a proof — it never re-broadcast the funding tx (only the Built arm does). If the tx had been evicted from every mempool (Core's default -mempoolexpiry is two weeks) or the original broadcast reached no peers (an SPV connectivity gap — a real condition on the testnet QA setup), no InstantSend/ChainLock proof can ever arrive. And since the user-facing funding flows now wait for the ChainLock indefinitely (#4006), the resume hangs forever: the controller stays .inFlight, Cancel is disabled, the network toggle is disabled app-wide, and no in-app recovery exists short of relaunch. #4007's new "Resume on a Broadcast orphan" button makes this reachable from the UI.

What was done?

Add a defensive re-broadcast to the Broadcast arm of resume_asset_lock before the wait.

Unlike the Built arm — whose tx was never broadcast, so a broadcast error is genuine and propagates — a Broadcast-status tx may still be in a mempool or already mined, where the network reports "already known" / "already in block chain". The broadcaster can't distinguish that from a real rejection (DapiBroadcaster classifies every failure as MaybeSent), so the re-broadcast is best-effort: log the error and proceed to wait_for_proof regardless, rather than failing a resume on a tx that is actually fine. If it was mined, the wait resolves immediately from the persisted record.

This path is shared by the identity, platform-address, and shielded resume flows plus the app-launch catch-up sweep; re-broadcasting an already-known tx is idempotent, so the extra call is harmless where the tx is still live.

How Has This Been Tested?

  • cargo check -p platform-wallet --features shielded --all-targets — clean.
  • cargo test -p platform-wallet --features shielded asset_lock — 19 asset-lock unit tests pass.
  • cargo fmt --all applied.

Not yet exercised end-to-end (would require an evicted/undelivered asset-lock tx on testnet).

Breaking Changes

None. No consensus or ABI change.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

Resuming a tracked asset lock at `Broadcast` status only waited for a
proof; it never re-broadcast the funding tx (only the `Built` arm does).
If the tx had been evicted from every mempool (Core's default
`-mempoolexpiry` is two weeks) or the original broadcast reached no peers
(an SPV connectivity gap), no InstantSend/ChainLock proof could ever
arrive — and since the user-facing funding flows now wait for the
ChainLock indefinitely (#4006), the resume would hang forever with no
recovery path.

Add a defensive re-broadcast to the `Broadcast` arm of `resume_asset_lock`
before the wait. Unlike the `Built` arm (whose tx was never broadcast, so
a broadcast error is genuine and propagates), a `Broadcast`-status tx may
still be in a mempool or already mined, where the network reports "already
known" / "already in block chain". The broadcaster can't distinguish that
from a real rejection (DAPI classifies every failure as `MaybeSent`), so
the re-broadcast is best-effort: log the error and proceed to
`wait_for_proof` regardless rather than failing a resume on a tx that is
actually fine (if it was mined, the wait resolves immediately from the
persisted record).

This path is shared by the identity, platform-address, and shielded
resume flows plus the app-launch catch-up sweep; re-broadcasting an
already-known tx is idempotent, so the extra call is harmless where the
tx is still live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added this to the v4.1.0 milestone Jul 5, 2026
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@QuantumExplorer, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 18 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: df9a4515-a5e8-433c-ba80-05586b8cd075

📥 Commits

Reviewing files that changed from the base of the PR and between e8b9da2 and 35a8885.

📒 Files selected for processing (1)
  • packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/rebroadcast-on-broadcast-resume

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.

@thepastaclaw

thepastaclaw commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit 35a8885)

@QuantumExplorer QuantumExplorer merged commit 096a804 into v4.1-dev Jul 5, 2026
17 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/rebroadcast-on-broadcast-resume branch July 5, 2026 12:29

@thepastaclaw thepastaclaw 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.

Code Review

Small, well-scoped defensive fix that adds a best-effort re-broadcast to resume_asset_lock's Broadcast arm before waiting for a proof. Implementation is correct — the swallowed broadcaster error is documented and matches DAPI's MaybeSent semantics. All four reviewers converged on the same gap: the new arm has no unit test, even though the crate's generic TransactionBroadcaster and existing test doubles (AlwaysMaybeSentBroadcaster, RejectFirstBroadcaster) make this cheap to pin down.

Source: reviewers=claude-general(opus), codex-general(gpt-5.5), claude-security-auditor(opus), codex-security-auditor(gpt-5.5), claude-rust-quality(opus), codex-rust-quality(gpt-5.5); verifier=claude(opus)

🟡 1 suggestion(s) | 💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs:267-301: New `Broadcast`-arm re-broadcast has no unit test
  The functional fix in this PR is the side effect added to `resume_asset_lock`'s `AssetLockStatus::Broadcast` arm: it must (a) invoke `self.broadcaster.broadcast(&tx).await` before entering `wait_for_proof`, and (b) continue to `wait_for_proof` even if that re-broadcast returns `Err`. Neither invariant is exercised by the existing asset-lock tests, which cover the initial-broadcast rejection/ambiguity paths but not resume from an already-`Broadcast` row.

  The crate is set up to test this cheaply — `AssetLockManager` is generic over `B: TransactionBroadcaster + ?Sized`, and the existing test doubles (`AlwaysMaybeSentBroadcaster` in `test_support.rs`, plus the `RejectFirstBroadcaster`-style pattern used in `core/broadcast.rs`) can assert both invariants with a counter and an `Err`-returning broadcaster. Without such a test, a future refactor could silently demote the `if let Err` back to `?` (reintroducing the hang) or drop the re-broadcast call entirely, and CI would still pass. Given the failure mode this PR is defending against is 'resume hangs forever with no in-app recovery', this contract is worth pinning down.

Comment on lines +288 to +297
if let Err(e) = self.broadcaster.broadcast(&tx).await {
tracing::debug!(
outpoint = %out_point,
error = %e,
"resume_asset_lock: defensive re-broadcast of a \
Broadcast-status lock returned an error (likely \
already in a mempool or mined); proceeding to wait \
for proof"
);
}

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.

💬 Nitpick: tracing::debug! may hide genuine broadcaster failures on resume

Swallowing the broadcaster error is correct (DAPI can't distinguish 'already known' / 'already in block chain' from a real rejection because everything uncertain is MaybeSent). But at debug! level, the pathological case this PR is trying to defend against — the broadcaster reaching zero peers — leaves no trace at default log levels, after which the unbounded wait_for_proof from #4006 blocks silently. Promoting to warn! (or info! with a clear 'best-effort re-broadcast reported error' phrasing) would keep the healthy case quiet (the line only fires on Err) while making the failure diagnosable from a normal log capture. Volume isn't a concern since this only runs on resume, not steady-state.

source: ['claude']

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