Skip to content

feat: rebroadcast unconfirmed self-sent transactions#627

Merged
xdustinface merged 3 commits intov0.42-devfrom
feat/rebroadcast-unconfirmed-tx
Apr 17, 2026
Merged

feat: rebroadcast unconfirmed self-sent transactions#627
xdustinface merged 3 commits intov0.42-devfrom
feat/rebroadcast-unconfirmed-tx

Conversation

@xdustinface
Copy link
Copy Markdown
Collaborator

@xdustinface xdustinface commented Apr 3, 2026

Each self-sent transaction in recent_sends tracks when it was last broadcast. Transactions whose last broadcast was more than 10 minutes ago are rebroadcast to all peers on each tick.

Add BroadcastMessage variant to NetworkRequest so the mempool manager can request a broadcast via the request queue.

Clean up recent_sends when a transaction is confirmed or IS-locked, since there is no need to rebroadcast finalized transactions.

This will only work properly with #626 merged since we don't record self sent transactions without that PR.

Summary by CodeRabbit

  • New Features

    • Unconfirmed transactions are now automatically rebroadcast every 10 minutes to improve propagation and confirmation reliability.
    • Nodes can issue broadcast messages to multiple peers at once.
  • Bug Fixes / Reliability

    • Broadcast operations now log summary warnings when some peer deliveries fail, aiding diagnosis.
  • Tests

    • Added tests verifying rebroadcast timing and behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

A broadcast request variant and sender were added to the network layer; the network manager now spawns tasks to broadcast messages to all peers. The mempool manager periodically rebroadcasts aged unconfirmed transactions (10 minutes) by using the new broadcast path during its sync tick.

Changes

Cohort / File(s) Summary
Network Broadcast Infrastructure
dash-spv/src/network/mod.rs, dash-spv/src/network/manager.rs
Added NetworkRequest::BroadcastMessage and RequestSender::broadcast(); request processor now handles BroadcastMessage by spawning an async task that calls broadcast(msg).await and logs aggregated per-peer failures.
Mempool Transaction Rebroadcasting
dash-spv/src/sync/mempool/manager.rs, dash-spv/src/sync/mempool/sync_manager.rs
Added REBROADCAST_INTERVAL (600s) and MempoolManager::rebroadcast_if_due() which selects aged unconfirmed txs, updates timestamps, and calls requests.broadcast(NetworkMessage::Tx). Integrated call into mempool tick. Added tests for rebroadcast timing.

Sequence Diagram(s)

sequenceDiagram
    participant MM as MempoolManager
    participant RS as RequestSender
    participant NM as NetworkManager
    participant P as Peer

    MM->>MM: tick()
    MM->>MM: identify txs with last_broadcast >= REBROADCAST_INTERVAL
    MM->>RS: broadcast(NetworkMessage::Tx)
    activate RS
    RS->>NM: enqueue BroadcastMessage
    deactivate RS
    NM->>NM: spawn task to broadcast to peers
    activate NM
    NM->>P: send tx to Peer (for each)
    P-->>NM: success / failure per peer
    NM->>NM: aggregate results & log failures
    deactivate NM
    MM->>MM: update recent_sends timestamps
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A broadcast hop across the net,
Ten minutes wait, then out they set—
Unconfirmed hops sent far and wide,
Each little tx finds a tide;
Hops and logs, rebroadcast delight. 🌿

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: rebroadcast unconfirmed self-sent transactions' clearly and directly summarizes the main change: adding rebroadcast functionality for unconfirmed self-sent transactions, which is the primary objective across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rebroadcast-unconfirmed-tx

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 and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 82.89474% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.98%. Comparing base (d4ff050) to head (0ca9b39).
⚠️ Report is 2 commits behind head on v0.42-dev.

Files with missing lines Patch % Lines
dash-spv/src/network/manager.rs 0.00% 12 Missing ⚠️
dash-spv/src/sync/mempool/manager.rs 98.30% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##           v0.42-dev     #627      +/-   ##
=============================================
- Coverage      67.99%   67.98%   -0.01%     
=============================================
  Files            318      319       +1     
  Lines          67877    67970      +93     
=============================================
+ Hits           46153    46210      +57     
- Misses         21724    21760      +36     
Flag Coverage Δ
core 75.25% <ø> (-0.02%) ⬇️
ffi 39.20% <ø> (-0.04%) ⬇️
rpc 20.00% <ø> (ø)
spv 85.32% <82.89%> (-0.16%) ⬇️
wallet 67.62% <ø> (ø)
Files with missing lines Coverage Δ
dash-spv/src/network/mod.rs 77.14% <100.00%> (+1.14%) ⬆️
dash-spv/src/sync/mempool/sync_manager.rs 98.72% <ø> (ø)
dash-spv/src/sync/mempool/manager.rs 97.25% <98.30%> (+0.05%) ⬆️
dash-spv/src/network/manager.rs 64.98% <0.00%> (-2.36%) ⬇️

... and 22 files with indirect coverage changes

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
dash-spv/src/sync/mempool/manager.rs (1)

349-353: Keep recent_sends cleanup inside MempoolState::remove_transaction.

Handling it only here fixes confirmations, but it leaves the invariant split across callers. Any other path removing a tx in dash-spv/src/types.rs can still orphan a recent_sends entry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dash-spv/src/sync/mempool/manager.rs` around lines 349 - 353, The loop in
manager.rs is performing recent_sends cleanup outside the state, splitting the
invariant; update MempoolState::remove_transaction (in dash-spv/src/types.rs) to
also remove the txid from its recent_sends map when a transaction is removed and
return a clear indicator (e.g., Option or bool) that the tx was removed, then
delete the external recent_sends removal from the loop in mempool/manager.rs and
use the returned indicator to populate removed; ensure the unique symbol names
referenced are MempoolState::remove_transaction, recent_sends, and the loop over
txids in the mempool_state.write() block so all callers rely on the single
internal cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dash-spv/src/sync/mempool/manager.rs`:
- Around line 423-427: Current code calls take_rebroadcast_due() which resets
timestamps up front and then ignores the Result from RequestSender::broadcast(),
causing txs to be suppressed even on send failure; change the flow to first
collect due txs without consuming the rebroadcast window (e.g., add or use a
non-destructive accessor like list_rebroadcast_due or a peek_rebroadcast_due on
mempool_state), attempt to broadcast each tx and check the Result from
RequestSender::broadcast(), and only call the mutation that marks a tx as
rebroadcasted (the counterpart of take_rebroadcast_due, e.g., mark_rebroadcasted
or consuming method) after a successful send; also stop discarding the broadcast
Result so failures can be retried or the timestamp left unchanged.
- Around line 371-374: The code marks a transaction InstantSend
(tx.is_instant_send = true) but doesn't move its pending amount into the
InstantSend bucket, causing pending balance drift and negative totals when
remove_transaction() later subtracts from pending_instant_balance. Fix by
transferring the transaction's pending amount from state.pending_balance into
state.pending_instant_balance at the same spot where you set tx.is_instant_send
(i.e., decrement state.pending_balance and increment
state.pending_instant_balance by the transaction's pending amount field), and
ensure the same unique field name you use for the tx pending amount is used so
remove_transaction() and other logic remain consistent.

---

Nitpick comments:
In `@dash-spv/src/sync/mempool/manager.rs`:
- Around line 349-353: The loop in manager.rs is performing recent_sends cleanup
outside the state, splitting the invariant; update
MempoolState::remove_transaction (in dash-spv/src/types.rs) to also remove the
txid from its recent_sends map when a transaction is removed and return a clear
indicator (e.g., Option or bool) that the tx was removed, then delete the
external recent_sends removal from the loop in mempool/manager.rs and use the
returned indicator to populate removed; ensure the unique symbol names
referenced are MempoolState::remove_transaction, recent_sends, and the loop over
txids in the mempool_state.write() block so all callers rely on the single
internal cleanup.
🪄 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: c86f9796-b03b-4c13-9304-093f3d938b20

📥 Commits

Reviewing files that changed from the base of the PR and between a7acd5a and b747d56.

📒 Files selected for processing (5)
  • dash-spv/src/network/manager.rs
  • dash-spv/src/network/mod.rs
  • dash-spv/src/sync/mempool/manager.rs
  • dash-spv/src/sync/mempool/sync_manager.rs
  • dash-spv/src/types.rs

Comment thread dash-spv/src/sync/mempool/manager.rs Outdated
Comment thread dash-spv/src/sync/mempool/manager.rs Outdated
@xdustinface xdustinface marked this pull request as draft April 3, 2026 06:28
@github-actions github-actions bot added the merge-conflict The PR conflicts with the target branch. label Apr 6, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 6, 2026

This PR has merge conflicts with the base branch. Please rebase or merge the base branch into your branch to resolve them.

@xdustinface xdustinface force-pushed the feat/rebroadcast-unconfirmed-tx branch from b747d56 to ea9cd8e Compare April 7, 2026 00:02
@github-actions github-actions bot removed the merge-conflict The PR conflicts with the target branch. label Apr 7, 2026
@xdustinface xdustinface marked this pull request as ready for review April 7, 2026 00:03
@xdustinface
Copy link
Copy Markdown
Collaborator Author

xdustinface commented Apr 7, 2026

@CodeRabbit review!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

@xdustinface Sure, I'll review the changes in PR #627 right away!

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

coderabbitai[bot]
coderabbitai bot previously approved these changes Apr 7, 2026
@github-actions github-actions bot added the ready-for-review CodeRabbit has approved this PR label Apr 7, 2026
@xdustinface xdustinface force-pushed the feat/rebroadcast-unconfirmed-tx branch from ea9cd8e to 1e54e4c Compare April 10, 2026 15:09
@github-actions github-actions bot removed the ready-for-review CodeRabbit has approved this PR label Apr 10, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Apr 10, 2026
@github-actions github-actions bot added the merge-conflict The PR conflicts with the target branch. label Apr 12, 2026
@github-actions
Copy link
Copy Markdown

This PR has merge conflicts with the base branch. Please rebase or merge the base branch into your branch to resolve them.

Each self-sent transaction in `recent_sends` tracks when it was last
broadcast. Transactions whose last broadcast was more than 10 minutes
ago are rebroadcast to all peers on each tick.

Add `BroadcastMessage` variant to `NetworkRequest` so the mempool
manager can request a broadcast via the request queue.

Clean up `recent_sends` when a transaction is confirmed or IS-locked,
since there is no need to rebroadcast finalized transactions.
@xdustinface xdustinface force-pushed the feat/rebroadcast-unconfirmed-tx branch from 1e54e4c to 821c4f8 Compare April 15, 2026 09:32
@github-actions github-actions bot removed the merge-conflict The PR conflicts with the target branch. label Apr 15, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Apr 15, 2026
@xdustinface xdustinface requested a review from ZocoLini April 15, 2026 09:40
Project `now` forward via a private `rebroadcast_if_due_at` helper instead of subtracting `REBROADCAST_INTERVAL` from `Instant::now()`. On Windows, `Instant` is backed by `QueryPerformanceCounter` measured from boot, so on a freshly provisioned runner with uptime below the interval the subtraction underflows and panics.
@github-actions github-actions bot added the ready-for-review CodeRabbit has approved this PR label Apr 15, 2026
@github-actions github-actions bot removed the ready-for-review CodeRabbit has approved this PR label Apr 15, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
dash-spv/src/sync/mempool/manager.rs (1)

457-469: ⚠️ Potential issue | 🟠 Major

Don’t consume the rebroadcast window before enqueue succeeds.

Line 463 updates last_broadcast before Line 468 enqueues the broadcast, and the enqueue result is discarded. If the request channel is closed, the tx is still delayed for another interval even though nothing was queued.

Proposed fix
 async fn rebroadcast_if_due_at(&mut self, requests: &RequestSender, now: Instant) {
-    let mut due: Vec<Transaction> = Vec::new();
-    for (txid, last_broadcast) in &mut self.recent_sends {
+    let mut due: Vec<(Txid, Transaction)> = Vec::new();
+    for (txid, last_broadcast) in &self.recent_sends {
         if now.saturating_duration_since(*last_broadcast) < REBROADCAST_INTERVAL {
             continue;
         }
         if let Some(unconfirmed) = self.transactions.get(txid) {
-            due.push(unconfirmed.transaction.clone());
-            *last_broadcast = now;
+            due.push((*txid, unconfirmed.transaction.clone()));
         }
     }

-    for tx in &due {
-        let _ = requests.broadcast(NetworkMessage::Tx(tx.clone()));
+    let mut sent = 0usize;
+    for (txid, tx) in due {
+        if requests.broadcast(NetworkMessage::Tx(tx)).is_ok() {
+            if let Some(last_broadcast) = self.recent_sends.get_mut(&txid) {
+                *last_broadcast = now;
+            }
+            sent += 1;
+        } else {
+            tracing::warn!("Failed to enqueue rebroadcast for tx {}", txid);
+        }
     }

-    if !due.is_empty() {
-        tracing::info!("Rebroadcast {} unconfirmed transaction(s) to all peers", due.len());
+    if sent > 0 {
+        tracing::info!("Rebroadcast {} unconfirmed transaction(s) to all peers", sent);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dash-spv/src/sync/mempool/manager.rs` around lines 457 - 469, The code
updates the rebroadcast timestamp (in self.recent_sends via last_broadcast)
before attempting to enqueue the transaction (requests.broadcast), so a failed
enqueue still consumes the rebroadcast window; change the flow in the loop that
builds due and updates last_broadcast: instead of setting *last_broadcast = now
when pushing to due, only collect txids (or references) into due and leave
last_broadcast unchanged, then when iterating over due call
requests.broadcast(NetworkMessage::Tx(tx.clone())) and only on success update
the corresponding entry in self.recent_sends (using the txid) to now; ensure
failed sends do not update last_broadcast and consider logging or propagating
the enqueue error from requests.broadcast to aid debugging.
🧹 Nitpick comments (1)
dash-spv/src/sync/mempool/manager.rs (1)

1643-1704: Add a failure-path rebroadcast test (queue send failure).

These tests cover due/not-due timing well, but they don’t cover the case where enqueue fails (e.g., dropped request receiver). Add one test asserting recent_sends is not advanced when broadcast enqueue returns Err, so this behavior doesn’t regress.

As per coding guidelines, "Write unit tests for new functionality."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dash-spv/src/sync/mempool/manager.rs` around lines 1643 - 1704, Add a new
#[tokio::test] that simulates enqueue failure by dropping the request receiver
before calling the rebroadcast path (use create_test_manager() to get (manager,
requests, rx), then drop(rx) so sending to requests will Err), insert an
UnconfirmedTransaction and set its recent_sends entry to a past Instant (use
manager.transactions.insert with UnconfirmedTransaction::new and
manager.recent_sends.insert), call manager.rebroadcast_if_due_at(&requests,
later).await (or manager.rebroadcast_if_due(&requests).await) and finally assert
that manager.recent_sends still contains the original timestamp (i.e., was not
advanced) to ensure failed enqueue does not update recent_sends.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@dash-spv/src/sync/mempool/manager.rs`:
- Around line 457-469: The code updates the rebroadcast timestamp (in
self.recent_sends via last_broadcast) before attempting to enqueue the
transaction (requests.broadcast), so a failed enqueue still consumes the
rebroadcast window; change the flow in the loop that builds due and updates
last_broadcast: instead of setting *last_broadcast = now when pushing to due,
only collect txids (or references) into due and leave last_broadcast unchanged,
then when iterating over due call
requests.broadcast(NetworkMessage::Tx(tx.clone())) and only on success update
the corresponding entry in self.recent_sends (using the txid) to now; ensure
failed sends do not update last_broadcast and consider logging or propagating
the enqueue error from requests.broadcast to aid debugging.

---

Nitpick comments:
In `@dash-spv/src/sync/mempool/manager.rs`:
- Around line 1643-1704: Add a new #[tokio::test] that simulates enqueue failure
by dropping the request receiver before calling the rebroadcast path (use
create_test_manager() to get (manager, requests, rx), then drop(rx) so sending
to requests will Err), insert an UnconfirmedTransaction and set its recent_sends
entry to a past Instant (use manager.transactions.insert with
UnconfirmedTransaction::new and manager.recent_sends.insert), call
manager.rebroadcast_if_due_at(&requests, later).await (or
manager.rebroadcast_if_due(&requests).await) and finally assert that
manager.recent_sends still contains the original timestamp (i.e., was not
advanced) to ensure failed enqueue does not update recent_sends.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: afd6b863-eadf-4398-ae24-a04eec395def

📥 Commits

Reviewing files that changed from the base of the PR and between 821c4f8 and 6c9cc1f.

📒 Files selected for processing (1)
  • dash-spv/src/sync/mempool/manager.rs

coderabbitai[bot]
coderabbitai bot previously approved these changes Apr 15, 2026
@github-actions github-actions bot added the ready-for-review CodeRabbit has approved this PR label Apr 15, 2026
ZocoLini
ZocoLini previously approved these changes Apr 16, 2026
Copy link
Copy Markdown
Collaborator

@ZocoLini ZocoLini left a comment

Choose a reason for hiding this comment

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

Not really blocking my comment, its a performance related issue but not really a problem

Comment thread dash-spv/src/sync/mempool/manager.rs
…ecting into a `Vec`

Addresses ZocoLini review comment on PR #627
#627 (comment)
@xdustinface xdustinface dismissed stale reviews from ZocoLini and coderabbitai[bot] via 0ca9b39 April 16, 2026 22:53
@github-actions github-actions bot removed the ready-for-review CodeRabbit has approved this PR label Apr 16, 2026
@github-actions github-actions bot added the ready-for-review CodeRabbit has approved this PR label Apr 16, 2026
@xdustinface xdustinface merged commit 7e2aa23 into v0.42-dev Apr 17, 2026
43 checks passed
@xdustinface xdustinface deleted the feat/rebroadcast-unconfirmed-tx branch April 17, 2026 00:01
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