Skip to content

fix(platform-wallet): wait indefinitely for asset-lock ChainLock finality#4006

Merged
QuantumExplorer merged 1 commit into
v4.1-devfrom
claude/musing-hopper-7f759d
Jul 5, 2026
Merged

fix(platform-wallet): wait indefinitely for asset-lock ChainLock finality#4006
QuantumExplorer merged 1 commit into
v4.1-devfrom
claude/musing-hopper-7f759d

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Jul 5, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

Found during ADDR-09 QA on SwiftExampleApp against testnet. A Platform identity top-up (asset lock d407f18f:0) broadcast successfully and Core SPV synced to tip, but the top-up modal declared "Top Up failed" after ~6–8 min. The asset lock was genuinely committed on-chain and merely pending finality — not failed.

Root cause: when an InstantSend proof doesn't propagate, the funding flow falls back to a bounded 180s ChainLock wait (CL_FALLBACK_TIMEOUT). Testnet ChainLocks can take ~15 min, so the wait elapsed and the flow returned FinalityTimeout, surfaced to the user as a hard failure.

A ChainLock is deterministic finality that will eventually cover any broadcast asset-lock tx, so there is no correct point at which to declare failure. Per direction: there should be no timeout for chain locks at all.

What was done?

The ChainLock wait is now unbounded for every user-facing asset-lock funding flow.

  • The wait primitives — wait_for_chain_lock, upgrade_to_chain_lock_proof, wait_for_proof, resume_asset_lock — now take Option<Duration>, where None waits indefinitely (the loop just parks on the SPV Notify until the lock event arrives).
  • All user-facing flows pass None at their ChainLock fallback: identity register/top-up (registration.rs), platform-address top-up (platform_addresses/fund_from_asset_lock.rs, the reported flow), and user-initiated shielded funding (shielded_send.rs).
  • The 300s build/resume wait is retained as an InstantSend-preference window (not a finality timeout): on expiry it falls through to the now-unbounded ChainLock wait rather than failing.
  • Deliberate exception: the shielded seed pool keeps a bounded Some(CL_FALLBACK_TIMEOUT). Its FinalityTimeout is a pacing signal for the unconfirmed-ancestor stall (batches chain ~25 unconfirmed change outputs; IS/CL proofs stop arriving until a block lands), not a finality failure. shielded_fund_from_asset_lock gained a cl_wait: Option<Duration> parameter for this; CL_FALLBACK_TIMEOUT is now shielded-gated as its only remaining consumer.
  • The two asset-lock resume/catch-up FFI entry points map timeout_secs == 0 to unbounded. No exported C symbol or signature changed, so the iOS app picks this up as pure runtime behavior after an xcframework rebuild.

Net effect: the top-up no longer reports a false "failed" — it stays pending finality until the ChainLock lands, then completes.

How Has This Been Tested?

  • cargo check -p platform-wallet --features shielded --all-targets — clean.
  • cargo check -p rs-platform-wallet-ffi (default) and --features shielded --all-targets — clean (confirmed no dead-code warning after shielded-gating CL_FALLBACK_TIMEOUT).
  • cargo clippy -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 on-device: requires an xcframework rebuild (build_ios.sh) to validate the top-up on the simulator against testnet.

Breaking Changes

None. No consensus-affecting change and no FFI 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

Summary by CodeRabbit

  • New Features

    • Asset-lock and shielded funding flows can now wait indefinitely when no timeout is provided.
    • Shielded funding now supports a caller-controlled ChainLock fallback wait policy.
  • Bug Fixes

    • Improved fallback handling so slow finality resolution no longer causes unnecessary timeout failures.
    • Updated wallet funding and recovery flows to continue waiting for proof or ChainLock when appropriate.

…lity

A Platform funding operation (identity registration/top-up, platform-
address top-up, shielded funding) that broadcast its asset lock but did
not obtain an InstantSend proof fell back to a 180s ChainLock wait
(CL_FALLBACK_TIMEOUT). On testnet, ChainLocks can take ~15min, so the
bounded wait elapsed and the flow returned FinalityTimeout — surfaced to
the user as "Top Up failed" even though the asset lock is committed
on-chain and merely pending finality.

A ChainLock is deterministic finality that will eventually cover any
broadcast asset-lock tx, so there is no correct point at which to declare
failure. Make the ChainLock wait unbounded:

- wait_for_chain_lock / upgrade_to_chain_lock_proof / wait_for_proof /
  resume_asset_lock now take Option<Duration>; None waits indefinitely.
- All user-facing funding flows pass None at their ChainLock fallback.
- The 300s build/resume wait is kept as an InstantSend-preference window
  (not a finality timeout): on expiry it falls through to the unbounded
  ChainLock wait rather than failing.

The shielded seed pool is the one deliberate exception: its
FinalityTimeout is a pacing signal for the unconfirmed-ancestor stall, so
it keeps a bounded Some(CL_FALLBACK_TIMEOUT) via a new cl_wait parameter
on shielded_fund_from_asset_lock. CL_FALLBACK_TIMEOUT is now shielded-
gated as its only remaining consumer.

The two asset-lock resume/catch-up FFI entry points treat timeout_secs==0
as unbounded. No exported C symbol or signature changed.

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

Review Change Stack

📝 Walkthrough

Walkthrough

Asset-lock proof waiting, resume, and shielded funding APIs are changed to accept Option<Duration> timeouts instead of Duration, enabling indefinite waits when None. Callers in FFI, identity registration, platform address funding, and shielded seed pool are updated accordingly, with documentation clarifying fallback semantics.

Changes

Optional ChainLock fallback timeouts

Layer / File(s) Summary
Core proof-wait and resume APIs
packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs, .../sync/recovery.rs
upgrade_to_chain_lock_proof, wait_for_chain_lock, wait_for_proof, and resume_asset_lock change their timeout parameter to Option<Duration>; polling loops refactored to wait indefinitely when None.
FFI resume/catch-up and orchestration wiring
packages/rs-platform-wallet-ffi/src/asset_lock/sync.rs, .../asset_lock/orchestration.rs, .../asset_lock/build.rs
timeout_secs == 0 now maps to unbounded waits in FFI; CL_FALLBACK_TIMEOUT gated behind shielded feature; 300s waits wrapped in Some.
Identity registration and platform address funding
packages/rs-platform-wallet/src/wallet/identity/network/registration.rs, .../platform_addresses/fund_from_asset_lock.rs
ChainLock fallback calls replace CL_FALLBACK_TIMEOUT with None, removing the constant import and updating latency documentation to describe unbounded waits.
Shielded funding and seed pool
packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs, .../shielded/seed_pool.rs, packages/rs-platform-wallet-ffi/src/shielded_send.rs
shielded_fund_from_asset_lock gains a cl_wait: Option<Duration> parameter used in fallback paths; seed pool passes Some(CL_FALLBACK_TIMEOUT); FFI docs clarify indefinite wait semantics.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant AssetLockSync as "sync/proof.rs"
  Caller->>AssetLockSync: wait_for_proof(out_point, timeout: Option<Duration>)
  alt timeout is Some
    AssetLockSync->>AssetLockSync: compute deadline, select notification vs sleep
    AssetLockSync-->>Caller: FinalityTimeout on expiry
  else timeout is None
    AssetLockSync->>AssetLockSync: await notification indefinitely
    AssetLockSync-->>Caller: proof resolved
  end
Loading

Possibly related PRs

  • dashpay/platform#3753: Introduces the Type 18 shielded_fund_from_asset_lock pipeline and FFI entrypoints that the main PR's cl_wait and timeout changes directly modify.
  • dashpay/platform#3985: Also modifies create_funded_asset_lock_proof in the asset-lock build flow, overlapping with this PR's timeout handling change in the same function.

Suggested labels: `ready for final review`

Suggested reviewers: `shumkov`, `lklimek`, `llbartekll`, `ZocoLini`

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: removing the ChainLock wait timeout for asset-lock flows.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/musing-hopper-7f759d

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.

@QuantumExplorer QuantumExplorer merged commit e8b9da2 into v4.1-dev Jul 5, 2026
15 of 16 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/musing-hopper-7f759d branch July 5, 2026 11:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/rs-platform-wallet/src/wallet/identity/network/registration.rs (1)

113-118: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale doc: "Idempotency note" still describes the bounded 180s wait.

The doc comment states "The IS→CL retry is bounded (180s waiting for ChainLock)." but the calls below (Line 186, 195, 247, 403-405, 441) now pass None for an unbounded wait. Compare with fund_from_asset_lock.rs, where the equivalent doc block was updated to describe the new unbounded semantics — this file's doc was missed.

📝 Proposed doc fix
     /// # Idempotency note
     ///
-    /// The IS→CL retry is bounded (180s waiting for ChainLock). If the
-    /// CL retry itself fails, the asset-lock stays tracked (cleanup
+    /// The IS→CL retry now waits for ChainLock indefinitely (no
+    /// timeout). If the process is killed or the caller otherwise
+    /// never observes completion, the asset-lock stays tracked (cleanup
     /// only runs on Platform success) so subsequent registration
     /// attempts can resume via `FromExistingAssetLock`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet/src/wallet/identity/network/registration.rs`
around lines 113 - 118, Update the stale “Idempotency note” in registration.rs
to match the current unbounded retry behavior: the comment still says the IS→CL
retry is bounded to 180s, but the retry calls in the registration flow now pass
None. Revise the doc block near the registration logic to describe the unbounded
wait semantics, using the same wording style as the updated doc in
fund_from_asset_lock.rs, and keep the note aligned with the retry path in the
relevant registration functions.
🧹 Nitpick comments (2)
packages/rs-platform-wallet/src/wallet/asset_lock/orchestration.rs (1)

423-429: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated 300s IS-preference window magic number.

Duration::from_secs(300) is now hardcoded here and in build.rs (Line 395) as the InstantSend-preference window. Since CL_FALLBACK_TIMEOUT already exists as a named constant in this file for the analogous bounded-wait concept, consider adding a similarly named constant (e.g. IS_PREFERENCE_WINDOW: Duration = Duration::from_secs(300)) shared by both call sites, so the two windows can't silently drift apart in a future change.

♻️ Proposed refactor
+/// InstantSend-preference window before falling back to an unbounded
+/// ChainLock wait. Shared by `build.rs::create_funded_asset_lock_proof`
+/// and `resolve_funding_with_is_timeout_fallback`.
+pub(crate) const IS_PREFERENCE_WINDOW: Duration = Duration::from_secs(300);
+
 match self
-    .resume_asset_lock(&out_point, Some(Duration::from_secs(300)))
+    .resume_asset_lock(&out_point, Some(IS_PREFERENCE_WINDOW))
     .await
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet/src/wallet/asset_lock/orchestration.rs` around
lines 423 - 429, The InstantSend preference window is duplicated as a hardcoded
300-second duration, so add a named shared constant for it in the same area that
defines CL_FALLBACK_TIMEOUT and use that constant at both call sites, including
the resume_asset_lock path. Update the code that currently passes
Duration::from_secs(300) so it references the new constant instead of embedding
the magic number, keeping the analogous bounded-wait values centralized and
consistent.
packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs (1)

318-341: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate select/timeout logic — extract a shared helper.

The match deadline { Some(dl) => {...} None => {...} } block is identical in wait_for_chain_lock (Lines 321-339) and wait_for_proof (Lines 551-569): compute remaining time, early-return FinalityTimeout if elapsed, tokio::select! between the armed notification and a timed sleep, or await the notification indefinitely when there's no deadline. Extracting this into a small shared helper (e.g. wait_on_notify_or_deadline(notified, deadline, out_point) -> Result<(), PlatformWalletError>) would remove the duplication and keep both call sites in sync if the pattern needs to change later (e.g. adding jitter/backoff).

Also applies to: 548-571

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs` around lines
318 - 341, The deadline-wait logic is duplicated in both wait_for_chain_lock and
wait_for_proof, so extract the shared notification/timeout handling into a small
helper such as wait_on_notify_or_deadline. Move the compute-remaining, immediate
FinalityTimeout return, tokio::select! between notified and sleep, and
indefinite await behavior into that helper, then have both call sites pass their
notified future, deadline, and out_point and handle the Result.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/rs-platform-wallet/src/wallet/identity/network/registration.rs`:
- Around line 113-118: Update the stale “Idempotency note” in registration.rs to
match the current unbounded retry behavior: the comment still says the IS→CL
retry is bounded to 180s, but the retry calls in the registration flow now pass
None. Revise the doc block near the registration logic to describe the unbounded
wait semantics, using the same wording style as the updated doc in
fund_from_asset_lock.rs, and keep the note aligned with the retry path in the
relevant registration functions.

---

Nitpick comments:
In `@packages/rs-platform-wallet/src/wallet/asset_lock/orchestration.rs`:
- Around line 423-429: The InstantSend preference window is duplicated as a
hardcoded 300-second duration, so add a named shared constant for it in the same
area that defines CL_FALLBACK_TIMEOUT and use that constant at both call sites,
including the resume_asset_lock path. Update the code that currently passes
Duration::from_secs(300) so it references the new constant instead of embedding
the magic number, keeping the analogous bounded-wait values centralized and
consistent.

In `@packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs`:
- Around line 318-341: The deadline-wait logic is duplicated in both
wait_for_chain_lock and wait_for_proof, so extract the shared
notification/timeout handling into a small helper such as
wait_on_notify_or_deadline. Move the compute-remaining, immediate
FinalityTimeout return, tokio::select! between notified and sleep, and
indefinite await behavior into that helper, then have both call sites pass their
notified future, deadline, and out_point and handle the Result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0ec05108-a7cc-4728-9828-1a5be7444da1

📥 Commits

Reviewing files that changed from the base of the PR and between a1d40ca and 4e6888a.

📒 Files selected for processing (10)
  • packages/rs-platform-wallet-ffi/src/asset_lock/sync.rs
  • packages/rs-platform-wallet-ffi/src/shielded_send.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/build.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/orchestration.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/sync/proof.rs
  • packages/rs-platform-wallet/src/wallet/asset_lock/sync/recovery.rs
  • packages/rs-platform-wallet/src/wallet/identity/network/registration.rs
  • packages/rs-platform-wallet/src/wallet/platform_addresses/fund_from_asset_lock.rs
  • packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs
  • packages/rs-platform-wallet/src/wallet/shielded/seed_pool.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant