Skip to content

fix(dash-spv): recover InstantSend locks received before quorum sync completes#895

Open
bfoss765 wants to merge 2 commits into
dashpay:devfrom
bfoss765:fix/spv-islock-after-restart
Open

fix(dash-spv): recover InstantSend locks received before quorum sync completes#895
bfoss765 wants to merge 2 commits into
dashpay:devfrom
bfoss765:fix/spv-islock-after-restart

Conversation

@bfoss765

@bfoss765 bfoss765 commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Problem

A wallet integration observed a shielded fund-from-asset-lock stall for ~3.5 minutes on a freshly (re)started SPV client. The asset-lock transaction was broadcast through dash-spv; the network's InstantSend lock for it appeared on the P2P network within ~2 seconds (a parallel engine on the same host received and verified it immediately), but this client — started ~21 seconds earlier — never used the islock. The asset-lock proof flow fell back to waiting for the transaction to be mined before it could proceed.

Root cause

The islock is received and correctly queued into pending_instantlocks when its BLS signature can't yet be verified (quorum sync hasn't loaded the signing quorum) — it is not dropped. The defect is the re-validation trigger:

  • Re-validation of the pending queue was edge-triggered exclusively by the MasternodeStateUpdated event.
  • After the initial masternode sync completes, that event only re-fires on a new block.
  • tick only pruned the cache; it never retried validation.

So an islock queued moments after a fresh start isn't re-checked until the next block — which, for a just-broadcast transaction, is effectively the block that mines it.

Fix

Make re-validation level-triggered:

  • Track last_validated_engine_height and re-run validate_pending from tick whenever the engine's masternode-list height has advanced past it (and pending locks exist), so a queued islock is verified as soon as the required quorum data lands — independent of whether a MasternodeStateUpdated event reaches the manager.
  • Pending expiry switches from an attempt counter to a time-based TTL (1h), because tick-driven retries would otherwise exhaust the attempt counter within seconds during a fresh sync and drop the lock prematurely.
  • on_disconnect resets the marker; the existing MasternodeStateUpdated path is retained and also updates the marker.

Contained to dash-spv/src/sync/instantsend/{manager.rs,sync_manager.rs}.

Tests

  • test_tick_revalidates_pending_when_engine_advances (core red→green: pre-fix tick never retried)
  • test_tick_skips_revalidation_without_engine_advance (throttle — no redundant BLS work per tick)
  • test_pending_expires_after_ttl_on_revalidation
  • test_on_disconnect_resets_validation_marker

cargo test -p dash-spv green (483 lib tests incl. the 4 new); cargo clippy -p dash-spv --all-targets clean; cargo fmt --check clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved InstantSend lock validation by switching pending expiry from retry-count tracking to a time-based TTL.
    • Pending InstantLocks are revalidated only when masternode engine height advances (or on first validation after reconnect).
    • Expired pending locks are automatically pruned on tick processing.
    • Disconnects now clear queued pending locks and related unvalidated state, resetting validation tracking so locks are properly revalidated after reconnect.

…completes

A wallet integration observed a shielded fund-from-asset-lock stall for
~3.5 minutes on testnet. The wallet broadcast an asset-lock transaction and
the network produced an InstantSend lock for it within ~2 seconds, but a
freshly (re)started dash-spv client never used the islock — it fell back to
waiting for the transaction to be mined before the asset-lock proof flow
could proceed.

Root cause: an islock that arrives before quorum sync has loaded the quorum
needed to verify its BLS signature is correctly queued in
`InstantSendManager::pending_instantlocks`, but re-validation of that queue
was edge-triggered exclusively by the `MasternodeStateUpdated` sync event
(`instantsend/sync_manager.rs`). After the initial masternode sync completes
that event only re-fires on a new MnListDiff/QRInfo — i.e. on a new block —
so a lock queued just after a fresh start is not re-checked until the next
block, which for a just-broadcast transaction is effectively the block that
mines it. `tick` only pruned the cache and never retried validation.

Fix: make re-validation level-triggered. `tick` now re-runs `validate_pending`
as soon as the masternode engine advances past the height at which the queue
was last checked (tracked by `last_validated_engine_height`), so a queued
islock is verified the moment the required quorum data lands instead of
waiting for the next event. Pending expiry is switched from an attempt counter
to a time-based TTL, since tick-driven retries would otherwise exhaust the
counter long before the hour-long window elapses during a fresh sync. The
existing `MasternodeStateUpdated` path is retained and also updates the marker.

Tests (deterministic, no dashd required):
- test_tick_revalidates_pending_when_engine_advances
- test_tick_skips_revalidation_without_engine_advance
- test_pending_expires_after_ttl_on_revalidation
- test_on_disconnect_resets_validation_marker

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

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The InstantSend manager replaces retry-count expiry with TTL-based pending-lock retention, gates revalidation on masternode engine-height advancement, performs expiry during every tick, resets unvalidated state on disconnect, and transitions to Synced when pending work is drained.

Changes

InstantLock validation lifecycle

Layer / File(s) Summary
Pending lock tracking
dash-spv/src/sync/instantsend/manager.rs
Pending InstantLocks store first_seen, while InstantSendManager tracks the last validated engine height.
TTL-based pending validation
dash-spv/src/sync/instantsend/manager.rs
validate_pending snapshots engine height, expires locks beyond PENDING_TTL, validates eligible locks, updates progress, and records the validation height.
Engine-gated tick and disconnect behavior
dash-spv/src/sync/instantsend/manager.rs, dash-spv/src/sync/instantsend/sync_manager.rs
Ticks expire pending locks independently of engine advancement and revalidate after height changes; disconnect handling clears unvalidated state, and tests cover synced transitions and re-announcement behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant InstantSendSyncManager
  participant SharedEngine
  participant InstantSendManager
  InstantSendSyncManager->>InstantSendManager: inspect pending InstantLocks
  InstantSendSyncManager->>InstantSendManager: request engine_height()
  InstantSendManager->>SharedEngine: read masternode-list height
  SharedEngine-->>InstantSendManager: current engine height
  InstantSendSyncManager->>InstantSendSyncManager: compare validation height
  InstantSendSyncManager->>InstantSendManager: expire_pending()
  InstantSendSyncManager->>InstantSendManager: validate_pending()
  InstantSendManager-->>InstantSendSyncManager: update pending and sync state
Loading

Suggested reviewers: xdustinface

🚥 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 main fix: recovering InstantSend locks that arrive before quorum sync completes.
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 unit tests (beta)
  • Create PR with unit tests

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.

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

Actionable comments posted: 3

🤖 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.

Inline comments:
In `@dash-spv/src/sync/instantsend/sync_manager.rs`:
- Around line 117-123: Move pending-lock TTL expiry cleanup out of the
engine-advancement-gated path in tick, ensuring it runs whenever
pending_instantlocks is non-empty even when the engine height is unchanged.
Reuse the existing expiry logic without rerunning BLS verification, then retain
validate_pending for advancement-triggered validation.
- Around line 124-125: Update the tick-driven path in the sync manager around
validate_pending so it applies the synced-state transition after validation
instead of returning immediately. Preserve the validation result and invoke the
same transition logic used by handle_sync_event, ensuring final-lock validation
or expiry can move the manager out of Syncing or WaitForEvents.
- Around line 28-32: Update `on_disconnect` to also clear the cached unvalidated
instant locks alongside `pending_instantlocks`. Ensure the cache used by
`process_instantlock` for duplicate detection is emptied so locks announced
after reconnect are queued and validated again.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ef042840-586f-44ba-a699-c1d66e1f12bc

📥 Commits

Reviewing files that changed from the base of the PR and between 19690d3 and 394e372.

📒 Files selected for processing (2)
  • dash-spv/src/sync/instantsend/manager.rs
  • dash-spv/src/sync/instantsend/sync_manager.rs

Comment thread dash-spv/src/sync/instantsend/sync_manager.rs
Comment thread dash-spv/src/sync/instantsend/sync_manager.rs
Comment thread dash-spv/src/sync/instantsend/sync_manager.rs Outdated
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.81657% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.62%. Comparing base (19690d3) to head (c0f0da9).

Files with missing lines Patch % Lines
dash-spv/src/sync/instantsend/manager.rs 98.80% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #895      +/-   ##
==========================================
+ Coverage   74.54%   74.62%   +0.07%     
==========================================
  Files         327      327              
  Lines       75032    75196     +164     
==========================================
+ Hits        55936    56116     +180     
+ Misses      19096    19080      -16     
Flag Coverage Δ
core 77.29% <ø> (ø)
ffi 51.15% <ø> (+0.20%) ⬆️
rpc 20.00% <ø> (ø)
spv 91.26% <98.81%> (+0.05%) ⬆️
wallet 74.41% <ø> (ø)
Files with missing lines Coverage Δ
dash-spv/src/sync/instantsend/sync_manager.rs 100.00% <100.00%> (+11.53%) ⬆️
dash-spv/src/sync/instantsend/manager.rs 96.36% <98.80%> (+3.16%) ⬆️

... and 19 files with indirect coverage changes

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

I think two of the coderabbit comments should be addressed.

Comment thread dash-spv/src/sync/instantsend/sync_manager.rs
Comment thread dash-spv/src/sync/instantsend/sync_manager.rs Outdated
…ependent of engine advance, synced-state transition after tick validation

- on_disconnect now drops unvalidated instantlock cache entries (via
  clear_unvalidated_on_disconnect) alongside the pending queue, so a lock
  re-announced after reconnect is no longer deduplicated against a stale
  cache entry and is re-queued/re-validated fresh.
- tick runs a cheap, advancement-independent expire_pending pass on every
  tick, so pending locks past PENDING_TTL are dropped even when the engine
  height is static; full BLS re-validation stays gated on engine advance.
- tick applies the synced-state transition after resolving pending work
  (via the shared transition_synced_if_idle helper, also used by
  handle_sync_event), so a final lock resolved by tick no longer leaves the
  manager stuck in Syncing/WaitForEvents.
- tests: re-announce after disconnect is not a duplicate; pending expires by
  TTL under a static engine height; tick resolving the last pending lock
  transitions out of Syncing. Existing tick tests updated for the new
  expiry/validation split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bfoss765

Copy link
Copy Markdown
Contributor Author

Addressed all three review findings in c0f0da9b:

  1. Unvalidated cache on disconnecton_disconnect now clears the unvalidated-lock cache alongside the pending queue (via a pub(super) helper, since the cache map is private to the manager module), so a re-announced lock after a disconnect is processed fresh instead of deduped. Test: test_reannounce_after_disconnect_is_not_duplicate.
  2. TTL expiry independent of engine advancement — a cheap, BLS-free expire_pending runs on every tick before the advancement gate; full re-validation stays gated on engine advance. Test: test_tick_expires_pending_without_engine_advance.
  3. Synced-state transition after tick validation — the post-validation transition is extracted to a shared helper now called by both handle_sync_event and tick (guarded so a freshly-idle manager isn't marked synced without work). Test: test_tick_transitions_synced_when_last_pending_resolved.

SKIP_DASHD_TESTS=1 cargo test -p dash-spv green (486 + integration, 0 failures); clippy and fmt clean.

🤖 Generated with Claude Code

@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)
dash-spv/src/sync/instantsend/manager.rs (1)

46-53: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use a monotonic clock for pending TTL

SystemTime::elapsed() can return Err after a backward clock change, which leaves the pending lock in the queue until wall time catches up. Store first_seen as Instant instead, and update the test fixtures accordingly.

🤖 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 `@dash-spv/src/sync/instantsend/manager.rs` around lines 46 - 53, Change
PendingInstantLock::first_seen from SystemTime to Instant and initialize it with
the monotonic clock when the lock is received. Update the pending-lock
expiration logic to use Instant::elapsed(), and adjust all related test fixtures
and imports to construct Instant timestamps instead of SystemTime values.
🤖 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 `@dash-spv/src/sync/instantsend/manager.rs`:
- Around line 46-53: Change PendingInstantLock::first_seen from SystemTime to
Instant and initialize it with the monotonic clock when the lock is received.
Update the pending-lock expiration logic to use Instant::elapsed(), and adjust
all related test fixtures and imports to construct Instant timestamps instead of
SystemTime values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9ccc4157-95ab-40a3-b2ca-6416d25085a0

📥 Commits

Reviewing files that changed from the base of the PR and between 394e372 and c0f0da9.

📒 Files selected for processing (2)
  • dash-spv/src/sync/instantsend/manager.rs
  • dash-spv/src/sync/instantsend/sync_manager.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • dash-spv/src/sync/instantsend/sync_manager.rs

@github-actions github-actions Bot added the ready-for-review CodeRabbit has approved this PR label Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review CodeRabbit has approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants