Skip to content

Adopt the oneshot VTXO subscription pattern - #265

Open
muchai254 wants to merge 17 commits into
arkade-os:masterfrom
muchai254:muchai254/VTXO-subscription-pattern
Open

Adopt the oneshot VTXO subscription pattern#265
muchai254 wants to merge 17 commits into
arkade-os:masterfrom
muchai254:muchai254/VTXO-subscription-pattern

Conversation

@muchai254

@muchai254 muchai254 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #254.

  • Adds a single-call method subscribe_to_scripts_stream that opens a subscription and returns the stream together.
  • Switches the SDK's own consumers over to this single-call flow.
  • Marks the old two-step methods as deprecated

Summary by CodeRabbit

  • New Features

    • Added a streamlined subscription API that returns both the subscription ID and live event stream.
    • Existing script subscriptions can now be updated in place as monitored scripts change.
    • Subscription filters now support adding newly monitored addresses.
  • Bug Fixes

    • Improved reconnection behavior with consistent retry handling.
    • Added handshake timeouts and faster shutdown response.
    • Improved handling of subscription startup failures and unexpected closures.
  • Deprecations

    • Older subscribe and unsubscribe methods are deprecated in favor of the new subscription and update APIs.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 29 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 698d8d11-0cd5-459f-840d-effc70c17cc1

📥 Commits

Reviewing files that changed from the base of the PR and between 5efdb3f and 5c711df.

📒 Files selected for processing (1)
  • ark-client/src/boltz.rs

Walkthrough

The SDK adds a combined scripts subscription API that returns the subscription ID and event stream. It deprecates older helpers and updates watcher handshake handling, script filters, retries, and sample usage.

Changes

VTXO subscription flow

Layer / File(s) Summary
gRPC subscription API
ark-grpc/src/client.rs, ark-grpc/Cargo.toml
Adds startup-frame parsing, a 30-second startup timeout, deprecation annotations, handshake tests, and runtime support.
Watcher subscription integration
ark-client/src/lib.rs, ark-client/src/vtxo_watcher.rs
Exposes the combined API, handles stop and retry outcomes around the bounded handshake, and applies additive script filters.
Sample subscription usage
ark-client-sample/src/main.rs
Uses one call to obtain the subscription ID and stream before processing notifications.

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

Sequence Diagram(s)

sequenceDiagram
  participant VTXOWatcher
  participant ArkClient
  participant ArkGrpcClient
  participant SubscriptionStream
  VTXOWatcher->>ArkClient: subscribe_to_scripts_stream(scripts)
  ArkClient->>ArkGrpcClient: Open script subscription
  ArkGrpcClient->>SubscriptionStream: Read startup frames
  SubscriptionStream-->>ArkGrpcClient: SubscriptionStarted and event stream
  ArkGrpcClient-->>ArkClient: Subscription ID and remaining stream
  ArkClient-->>VTXOWatcher: Return subscription result
  VTXOWatcher->>ArkClient: update_subscription(subscription_id, filter)
  ArkClient->>ArkGrpcClient: Apply additional script filter
Loading
🚥 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 change: adopting the oneshot VTXO subscription pattern.
Linked Issues check ✅ Passed The changes implement the oneshot subscription flow requested by issue #254 and update consumers to use it.
Out of Scope Changes check ✅ Passed The changes support issue #254 through API updates, consumer migration, timeout handling, deprecations, and related tests.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@ark-grpc/src/client.rs`:
- Around line 896-915: Bound the subscription-start handshake in the loop
receiving `stream.next()` from `get_subscription`: apply the existing
timeout/deadline mechanism to each wait so a missing `SubscriptionStarted` or
heartbeat-only stream expires and returns an error, allowing the watcher’s
retry/backoff path to run. Preserve the current handling for successful startup,
heartbeats, events, stream errors, and closure.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: af3f8991-b408-47fa-ac08-336cf99cd915

📥 Commits

Reviewing files that changed from the base of the PR and between 45aeb65 and 8aff6b7.

📒 Files selected for processing (4)
  • ark-client-sample/src/main.rs
  • ark-client/src/lib.rs
  • ark-client/src/vtxo_watcher.rs
  • ark-grpc/src/client.rs

Comment thread ark-grpc/src/client.rs Outdated

@arkana-ai-bot arkana-ai-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.

PROTOCOL-CRITICAL: human review required.

Summary

The oneshot-subscription pattern is the right direction — collapsing the two-step subscribe+connect into a single call removes a race window. The switch to update_subscription in refresh_subscription_scripts is also cleaner. However there are two correctness issues that need fixing before merge.


[CRITICAL] Handshake loop is not cancellable — watcher cannot be stopped while blocked on SubscriptionStarted

ark-grpc/src/client.rs (new subscribe_to_scripts_stream, lines added after get_subscription call):

let subscription_id = loop {
    match stream.next().await {
        Some(Ok(SubscriptionResponse::SubscriptionStarted { .. })) => { break subscription_id; }
        Some(Ok(SubscriptionResponse::Heartbeat)) => continue,   // ← unbounded
        ...
    }
};

This loop drains heartbeats indefinitely while waiting for the SubscriptionStarted frame. It is awaited as a plain Future inside run_watcher_loop (vtxo_watcher.rs, replacing old lines ~161-183):

let (subscription_id, mut stream) =
    match client.subscribe_to_scripts_stream(addresses.clone()).await {   // ← no select!
        ...
    };

stop_rx is never select!-ed against during this await. Consequences:

  1. A server that emits only heartbeats (slow path, degraded node) will stall the watcher task indefinitely.
  2. VtxoWatcherHandle::stop() / Drop signals stop_rx but the task never wakes to observe it — graceful shutdown silently fails.
  3. The backoff reconnection logic cannot fire.

The pre-existing two-step code had the same gap for the unary subscribe_to_scripts RPC, but a unary call is bounded by gRPC's per-call deadline. The new streaming handshake loop is unbounded.

Fix: wrap the handshake future with tokio::time::timeout (e.g. 30 s) and propagate Err so callers can retry with backoff. Example:

let subscription_id = tokio::time::timeout(Duration::from_secs(30), async {
    loop {
        match stream.next().await {
            Some(Ok(SubscriptionResponse::SubscriptionStarted { subscription_id })) => {
                return Ok(subscription_id);
            }
            Some(Ok(SubscriptionResponse::Heartbeat)) => continue,
            Some(Ok(SubscriptionResponse::Event(_))) => {
                return Err(Error::conversion("event before subscription_started"));
            }
            Some(Err(e)) => return Err(e),
            None => return Err(Error::conversion("stream closed before subscription_started")),
        }
    }
})
.await
.map_err(|_| Error::conversion("timed out waiting for subscription_started"))??;

The watcher call site still needs a select! on stop_rx around the whole subscribe_to_scripts_stream call for clean shutdown, or the timeout alone is enough to bound the hang (pick one, document the choice).


[SIGNIFICANT] expressions: Vec::new() in refresh_subscription_scripts silently clears subscription expressions

ark-client/src/vtxo_watcher.rs, refresh_subscription_scripts (new code replacing old line ~462):

let filter = SubscriptionFilter {
    expressions: Vec::new(),      // ← clears all expressions on the subscription
    add_scripts: new_addrs.iter().map(|addr| addr.to_p2tr_script_pubkey()).collect(),
    remove_scripts: Vec::new(),
};
client.update_subscription(subscription_id.to_string(), filter).await?;

The update_subscription contract (ark-client/src/lib.rs:2211-2214 and ark-grpc/src/client.rs:797-799) explicitly states: "The filter's expressions are always overwritten as a whole (an empty list clears them)." Passing expressions: Vec::new() destroys any expressions already on the subscription. This is safe today because no expressions are ever set in this flow, but it is a latent footgun: if a future caller opens the stream with expressions, refresh_subscription_scripts will silently wipe them on the first 10-second discovery tick.

Fix: either document the deliberate clearing in a comment, or if the intent is truly "only add scripts, touch nothing else," submit a follow-up to expose a dedicated add_scripts-only RPC variant (or pass None for expressions once the filter type supports it).


[SIGNIFICANT] No tests for any new code path

Danger flagged this correctly. The new subscribe_to_scripts_stream method (ark-grpc/src/client.rs) and the updated refresh_subscription_scripts (vtxo_watcher.rs) have zero test coverage. At minimum needed:

  • Happy path: heartbeats followed by SubscriptionStarted — ID extracted, remaining stream items delivered.
  • Error path: Event arrives before SubscriptionStartedErr returned.
  • Error path: stream closes before SubscriptionStartedErr returned.
  • Timeout path (once added): server sends only heartbeats past the deadline — Err returned and caller can retry.

The existing test module in vtxo_watcher.rs covers group_by_expiry_day, select_vtxos_for_self_renewal, etc. but nothing exercises the subscription machinery. A mock get_subscription via ark_grpc::test_utils (already imported in lib.rs tests) would work.


Minor

ark-client/src/lib.rs subscribe_to_scripts and unsubscribe_from_scripts: the #[allow(deprecated)] attributes are correct (the wrappers call the now-deprecated underlying gRPC methods) but benefit from an inline comment — // calls the deprecated grpc method; suppress the warning on this intentional wrapper — otherwise the next reader will wonder if it is an oversight.


No cross-repo breakage found

Checked ts-sdk, go-sdk, dotnet-sdk — none consume subscribe_to_scripts/unsubscribe_from_scripts from this SDK's crate directly. No external breakage. The #[deprecated] attributes correctly guide internal SDK consumers.

@arkana-ai-bot arkana-ai-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.

PROTOCOL-CRITICAL: human review required.

Since 8aff6b7: 4 commits, 4 prior issues closed, 1 new moderate finding.


Prior findings — closure accounting

[CRITICAL] Handshake loop not cancellable → Fixed (5444d37a)

subscribe_within_deadline correctly races stop_rx.changed() against tokio::time::timeout(SUBSCRIBE_TIMEOUT, handshake) via tokio::select!. The outer watcher loop also has the *stop_rx.borrow() guard (vtxo_watcher.rs:155) before entering the call, so a stop signal that arrives during a successful handshake is caught on the very next loop iteration. Backoff on timeout and backoff on error are both wired correctly. Four unit tests cover all exit paths (Ready, Stopped, Retry-on-timeout, Retry-on-error). ✅

[SIGNIFICANT] expressions: Vec::new() silently clears subscription expressions → Fixed (3e031ebf)

additional_scripts_filter was extracted as a standalone function and carries a clear invariant comment at vtxo_watcher.rs:524-527 warning future callers that expressions opened with the stream must be carried forward here instead of silently wiped. The latent footgun is documented at its origin. ✅

[SIGNIFICANT] No test coverage → Fixed (f5bbe331)

ark-grpc/src/client.rs tests for read_subscription_started cover: happy path with leading heartbeats, event-before-started, close-before-started (both empty and heartbeats-only), and error propagation. vtxo_watcher.rs tests cover additional_scripts_filter (selects unsubscribed, returns None when all subscribed) and all four subscribe_within_deadline outcomes. Coverage is solid for all flagged paths. ✅

Minor: #[allow(deprecated)] unexplained → Fixed (f0e080fb)

Both ark-client/src/lib.rs:2157-2160 and ark-grpc/src/client.rs:735-737 now carry #[deprecated(note = ...)] alongside inline prose explaining that the #[allow(deprecated)] is an intentional self-referential suppression. ✅


New finding

[MODERATE] subscribe_to_scripts_stream / read_subscription_started public API has no internal timeout — external callers can hang indefinitely

read_subscription_started at ark-grpc/src/client.rs:1625-1641 is an unbounded loop over stream.next().await. It is called from subscribe_to_scripts_stream in both ark-grpc/src/client.rs:897-901 and ark-client/src/lib.rs:2260-2263. Neither call site applies a deadline.

The 30-second SUBSCRIBE_TIMEOUT exists only inside subscribe_within_deadline in the watcher (vtxo_watcher.rs:468). The public API surface has no self-defense. Any caller that invokes subscribe_to_scripts_stream directly will block indefinitely if the server emits only heartbeats before subscription_started.

The sample program is directly affected:

// ark-client-sample/src/main.rs:777-783
let (subscription_id, mut subscription_stream) = client
    .subscribe_to_scripts_stream(vec![address.0])  // ← no timeout; can hang forever
    .await
    .map_err(|e| anyhow!(e))?;

An SDK consumer integrating this into a payment flow with a misbehaving or degraded node has no recourse except wrapping the call externally, which is easy to miss.

Options (pick one):

Option A — move the timeout inside read_subscription_started (preferred, makes the API self-defending):

async fn read_subscription_started<S>(mut stream: S) -> Result<(String, S), Error>
where
    S: Stream<Item = Result<SubscriptionResponse, Error>> + Unpin,
{
    tokio::time::timeout(Duration::from_secs(30), async {
        loop {
            match stream.next().await {
                ...
            }
        }
    })
    .await
    .map_err(|_| Error::conversion("timed out waiting for subscription_started"))?
}

Option B — document the missing deadline in the public subscribe_to_scripts_stream doc comment:

/// # Deadlines
///
/// This method blocks until the server sends `subscription_started`. If the server
/// sends only heartbeats indefinitely (degraded node) the call will not return.
/// Wrap with `tokio::time::timeout` if you need a bounded wait.

Option A is preferable because it makes the safe path the default and prevents the footgun in the sample and in downstream SDK consumers.


No new cross-repo breakage

The old subscribe_to_scripts and unsubscribe_from_scripts are deprecated (not removed). Checked ts-sdk, go-sdk, dotnet-sdk: none import these methods. No external breakage.


Remaining blocker

The one open item is the unbounded public API (read_subscription_started / subscribe_to_scripts_stream). Everything else in this diff is correctly implemented and well tested.

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
ark-grpc/src/client.rs (1)

1768-1844: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a deterministic regression test for the timeout branch.

The added tests cover startup parsing and stream preservation, but do not clearly exercise the subscribe_to_scripts_stream timeout error path at Lines [907]-[912]. Add a short/injectable timeout or paused-clock test for a stream that remains open while emitting only heartbeats.

🤖 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 `@ark-grpc/src/client.rs` around lines 1768 - 1844, Add a deterministic test
covering the timeout branch in subscribe_to_scripts_stream: use an injectable or
paused clock with an open stream that emits only SubscriptionResponse::Heartbeat
values, advance the clock beyond the startup timeout, and assert the expected
timeout error. Keep the test focused on timeout behavior without changing the
existing startup parsing tests.
🤖 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 `@ark-grpc/src/client.rs`:
- Around line 879-881: Update the documentation comment for the public
subscribe_to_scripts_stream API to remove the intra-doc link to the private
SUBSCRIPTION_START_TIMEOUT constant, replacing it with plain-text timeout
wording such as the duration; keep the described bounded-wait and heartbeat
behavior unchanged.
- Around line 901-913: Wrap both the get_subscription call and
read_subscription_started in the same tokio::time::timeout so
SUBSCRIPTION_START_TIMEOUT begins before opening the subscription RPC. Update
the surrounding subscribe_to_scripts_stream flow to propagate the inner result
and convert timeout expiration to the existing “timed out waiting for
subscription_started” error, preserving current success and error behavior.

---

Nitpick comments:
In `@ark-grpc/src/client.rs`:
- Around line 1768-1844: Add a deterministic test covering the timeout branch in
subscribe_to_scripts_stream: use an injectable or paused clock with an open
stream that emits only SubscriptionResponse::Heartbeat values, advance the clock
beyond the startup timeout, and assert the expected timeout error. Keep the test
focused on timeout behavior without changing the existing startup parsing tests.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 324bd28c-653c-4f58-807e-97715313b5e5

📥 Commits

Reviewing files that changed from the base of the PR and between f0e080f and 74df25e.

📒 Files selected for processing (4)
  • ark-client/src/lib.rs
  • ark-client/src/vtxo_watcher.rs
  • ark-grpc/Cargo.toml
  • ark-grpc/src/client.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • ark-client/src/lib.rs
  • ark-client/src/vtxo_watcher.rs

Comment thread ark-grpc/src/client.rs Outdated
Comment thread ark-grpc/src/client.rs Outdated

@arkana-ai-bot arkana-ai-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.

PROTOCOL-CRITICAL: human review required.

2 commits since last pass (66c6c2c, 74df25e). 1 prior finding fixed. 0 still open. 1 new minor finding.


Prior findings — closure accounting

[MODERATE] Public API has no internal timeout (subscribe_to_scripts_stream / read_subscription_started hang indefinitely on a heartbeat-only server) → Fixed (74df25e)

subscribe_to_scripts_stream in ark-grpc/src/client.rs now wraps read_subscription_started(stream) with tokio::time::timeout(SUBSCRIPTION_START_TIMEOUT, …) (30 s). The watcher's own subscribe_within_deadline still provides a second layer at the call site for cooperative shutdown. The timeout constant is documented at ark-grpc/src/client.rs:1634 and the doc comment on the public function at line ~878 explains the bounded-wait guarantee. ark-grpc/Cargo.toml correctly adds tokio (with time feature, no extras) to [dependencies]. The sample program path (ark-client-sample/src/main.rs:777-783) is now also protected. ✅

All findings from the review before this one (handshake cancellability, expressions: Vec::new() footgun, test coverage, #[allow(deprecated)] prose) were carried forward unchanged — no regressions observed.


New finding

[MINOR] Intra-doc link to a private constant in a public function doc comment

ark-grpc/src/client.rs:878:

/// The wait for `subscription_started` is bounded by [`SUBSCRIPTION_START_TIMEOUT`]. A

SUBSCRIPTION_START_TIMEOUT is declared const (no pub), so this is a private item. Rustdoc emits a rustdoc::private_intra_doc_links warning and the rendered HTML docs show a broken anchor. No #![deny(rustdoc::private_intra_doc_links)] exists in this crate, so it won't currently break CI, but it degrades the public docs.

Two fixes, either works:

  • Replace the link with plain text: "bounded by a 30-second timeout" — one-liner, zero visibility change.
  • Make the constant pub(crate) so the link resolves: pub(crate) const SUBSCRIPTION_START_TIMEOUT: Duration = …; at line 1634.

The parallel doc comment in ark-client/src/lib.rs:2250-2253 ("The wait for the subscription to start is internally bounded") deliberately omits the duration, which is acceptable for a facade, but appending (by default 30 seconds) would be more useful to callers who need to size their own outer timeouts.


No new cross-repo breakage

subscribe_to_scripts and unsubscribe_from_scripts remain deprecated-not-removed. ts-sdk, go-sdk, dotnet-sdk confirmed clear as before.


Status

Substantive protocol issues are all resolved. The remaining item is a documentation quality nit that has no correctness or safety impact. Safe to merge once the doc link is addressed (or documented as a known warning to clear later).

@arkana-ai-bot arkana-ai-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.

PROTOCOL-CRITICAL: human review required.

1 commit relevant to subscription code since 74df25e (7e36fc45); 1 prior finding fixed; 0 remain open. Ready to merge.

The bulk of the incremental diff (37 of 39 commits) is unrelated Boltz / contract-manager work brought in by merging master (5efdb3f0). Scope of this pass is scoped to the subscription changes only.


Prior finding — closure accounting

[MINOR] Intra-doc link to private constant in public doc comment → Fixed (7e36fc45)

ark-grpc/src/client.rs:880–881 now reads:

/// Startup is bounded by a 30-second timeout, covering both opening the streaming
/// RPC and waiting for `subscription_started`.

The broken [SUBSCRIPTION_START_TIMEOUT] link is gone. Rustdoc will no longer emit private_intra_doc_links. ✅

Incidental improvement in the same commit: get_subscription (the gRPC stream open) is now inside the startup async block and therefore also raced against SUBSCRIPTION_START_TIMEOUT. Previously only read_subscription_started was under the timeout; a transport stall on stream open would have escaped the deadline. Now it cannot.


New findings

None. No new issues introduced by the subscription-scoped changes.


Open CodeRabbit nitpick (not mine, not blocking)

CodeRabbit flagged that subscribe_to_scripts_stream itself has no test for the timeout branch — only read_subscription_started is exercised by the current unit tests (ark-grpc/src/client.rs:1768–1844). A stalled get_subscription open hitting the deadline is now possible to test (the startup async block is a plain Future) and would close the gap cleanly with a paused-clock or short-duration test. Not a blocker, but worth a follow-up if the test suite ever targets line coverage.


Status

All findings from every prior pass are resolved. No correctness, security, or protocol issues remain open in the subscription code.

@muchai254

Copy link
Copy Markdown
Contributor Author

Requesting for manual review @luckysori, @bonomat. I also need help verifying that the timeout constants I have used are suitable.

@arkana-ai-bot arkana-ai-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.

PROTOCOL-CRITICAL: human review required.

1 commit since last pass (5efdb3f5c711df); 1 prior issue closed; 2 new findings, 1 moderate.


Prior finding closure

[MODERATE] / has no internal timeout → Fixed (already present at 5efdb3f)

The tokio::time::timeout(SUBSCRIPTION_START_TIMEOUT, startup) wrapper at ark-grpc/src/client.rs:907-912 was already in place at the base commit. The doc comment at lines 880-882 explicitly documents the 30-second bounded wait. My second review incorrectly identified this as absent — the fix predates this incremental commit. Retraction acknowledged.


New findings (5c711df: ark-client/src/boltz.rs only)

[MODERATE] Boltz watcher handshake is not cancellable — stop_rx ignored for up to 30 s

boltz.rs:4427-4438

let (subscription_id, mut stream) =
    match client.subscribe_to_scripts_stream(addresses.clone()).await {  // ← no select!

The vtxo_watcher equivalent wraps this call in subscribe_within_deadline (vtxo_watcher.rs:168-180), which tokio::select!s against stop_rx:

let handshake = client.subscribe_to_scripts_stream(addresses.clone());
let (subscription_id, mut stream) =
    match subscribe_within_deadline(&mut stop_rx, SUBSCRIBE_TIMEOUT, handshake).await {

In the boltz watcher, stop_rx is not observed during the handshake. If the server sends only heartbeats, a stop() / Drop signal on the handle cannot wake the task — it is stranded for the full 30 s SUBSCRIPTION_START_TIMEOUT. For a VHTLC watcher handling time-locked submarine-swap HTLCs, a 30 s shutdown lag is a meaningful operational hazard and also breaks the behavioral contract established by the existing subscribe_within_deadline pattern.

Fix: reuse subscribe_within_deadline (it is a free function in vtxo_watcher.rs — consider moving it to a shared module, or duplicate it in boltz.rs for now):

let handshake = client.subscribe_to_scripts_stream(addresses.clone());
let (subscription_id, mut stream) =
    match subscribe_within_deadline(&mut stop_rx, SUBSCRIBE_TIMEOUT, handshake).await {
        SubscribeOutcome::Ready(sub) => sub,
        SubscribeOutcome::Stopped => return,
        SubscribeOutcome::Retry => {
            backoff = (backoff * 2).min(VHTLC_WATCHER_MAX_BACKOFF);
            continue;
        }
    };

[MINOR] Missing invariant comment on expressions: Vec::new() in refresh_boltz_vhtlc_subscription

boltz.rs:4544

let filter = SubscriptionFilter {
    expressions: Vec::new(),   // ← no comment
    add_scripts: ...,
    remove_scripts: Vec::new(),
};

This is exactly the pattern fixed in vtxo_watcher.rs with the protective comment at vtxo_watcher.rs:522-525:

// `update_subscription` overwrites expressions as a whole but treats scripts as additive. This
// flow adds scripts and never sets expressions, so the empty list has nothing to clear. A
// future caller that opens the stream with expressions must carry them here instead of wiping
// them on the first discovery tick.

Copy that comment verbatim to boltz.rs:4544. A VHTLC maintainer adding expression filtering will hit the same silent-wipe footgun without it.

[MINOR] No test coverage for the new boltz subscription path

boltz.rs:4427-4438 and 4524-4560

run_boltz_vhtlc_watcher_loop's new subscribe_to_scripts_stream call and refresh_boltz_vhtlc_subscription have zero test coverage. The vtxo_watcher established the baseline: additional_scripts_filter unit tests and four subscribe_within_deadline outcome tests. The boltz path should have equivalent coverage for refresh_boltz_vhtlc_subscription (happy path: only new addrs submitted; skip when all already subscribed) and for the subscription outcome dispatch (stop, retry-on-timeout, retry-on-error). Danger flagged this correctly.


No cross-repo impact

This commit is confined to ark-client/src/boltz.rs. The new API surface (subscribe_to_scripts_stream, update_subscription) is unchanged. No breakage to ts-sdk, go-sdk, or dotnet-sdk consumers.

@arkana-ai-bot arkana-ai-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.

PROTOCOL-CRITICAL: human review required.

1 commit since last pass (5efdb3f5c711df); 1 prior issue closed; 2 new findings, 1 moderate.


Prior finding closure

[MODERATE] subscribe_to_scripts_stream / read_subscription_started has no internal timeout → Fixed (already present at 5efdb3f)

The tokio::time::timeout(SUBSCRIPTION_START_TIMEOUT, startup) wrapper at ark-grpc/src/client.rs:907-912 was already in place at the base commit. The doc comment at lines 880-882 explicitly documents the 30-second bounded wait. My second review incorrectly identified this as absent — the fix predates this incremental commit. Retraction acknowledged.


New findings (5c711df: ark-client/src/boltz.rs only)

[MODERATE] Boltz watcher handshake is not cancellable — stop_rx ignored for up to 30 s

boltz.rs:4427-4438

let (subscription_id, mut stream) =
    match client.subscribe_to_scripts_stream(addresses.clone()).await {  // ← no select!

The vtxo_watcher equivalent wraps this call in subscribe_within_deadline (vtxo_watcher.rs:168-180), which tokio::select!s against stop_rx:

let handshake = client.subscribe_to_scripts_stream(addresses.clone());
let (subscription_id, mut stream) =
    match subscribe_within_deadline(&mut stop_rx, SUBSCRIBE_TIMEOUT, handshake).await {

In the boltz watcher, stop_rx is not observed during the handshake. If the server sends only heartbeats, a stop() / Drop signal on the handle cannot wake the task — it is stranded for the full 30 s SUBSCRIPTION_START_TIMEOUT. For a VHTLC watcher handling time-locked submarine-swap HTLCs, a 30 s shutdown lag is a meaningful operational hazard and also breaks the behavioral contract established by the existing subscribe_within_deadline pattern.

Fix: reuse subscribe_within_deadline (it is a free function in vtxo_watcher.rs — consider moving it to a shared module, or duplicate it in boltz.rs for now):

let handshake = client.subscribe_to_scripts_stream(addresses.clone());
let (subscription_id, mut stream) =
    match subscribe_within_deadline(&mut stop_rx, SUBSCRIBE_TIMEOUT, handshake).await {
        SubscribeOutcome::Ready(sub) => sub,
        SubscribeOutcome::Stopped => return,
        SubscribeOutcome::Retry => {
            backoff = (backoff * 2).min(VHTLC_WATCHER_MAX_BACKOFF);
            continue;
        }
    };

[MINOR] Missing invariant comment on expressions: Vec::new() in refresh_boltz_vhtlc_subscription

boltz.rs:4544

let filter = SubscriptionFilter {
    expressions: Vec::new(),   // ← no comment
    add_scripts: ...,
    remove_scripts: Vec::new(),
};

This is exactly the pattern fixed in vtxo_watcher.rs with the protective comment at vtxo_watcher.rs:522-525:

// `update_subscription` overwrites expressions as a whole but treats scripts as additive. This
// flow adds scripts and never sets expressions, so the empty list has nothing to clear. A
// future caller that opens the stream with expressions must carry them here instead of wiping
// them on the first discovery tick.

Copy that comment verbatim to boltz.rs:4544. A VHTLC maintainer adding expression filtering will hit the same silent-wipe footgun without it.

[MINOR] No test coverage for the new boltz subscription path

boltz.rs:4427-4438 and 4524-4560

run_boltz_vhtlc_watcher_loop's new subscribe_to_scripts_stream call and refresh_boltz_vhtlc_subscription have zero test coverage. The vtxo_watcher established the baseline: additional_scripts_filter unit tests and four subscribe_within_deadline outcome tests. The boltz path should have equivalent coverage for refresh_boltz_vhtlc_subscription (happy path: only new addrs submitted; skip when all already subscribed) and for the subscription outcome dispatch (stop, retry-on-timeout, retry-on-error). Danger flagged this correctly.


No cross-repo impact

This commit is confined to ark-client/src/boltz.rs. The new API surface (subscribe_to_scripts_stream, update_subscription) is unchanged. No breakage to ts-sdk, go-sdk, or dotnet-sdk consumers.

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.

Use new VTXO subscription pattern

2 participants