Skip to content

feat(client): cache vtxos and sync them incrementally - #264

Open
bonomat wants to merge 5 commits into
masterfrom
feat/vtxo-cache
Open

feat(client): cache vtxos and sync them incrementally#264
bonomat wants to merge 5 commits into
masterfrom
feat/vtxo-cache

Conversation

@bonomat

@bonomat bonomat commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Instead of crawling the entire (ever-growing) vtxo history on every query, the client now caches vtxos and only fetches the delta since the last sync. The server filters GetVtxos by the vtxo's updated_at timestamp (milliseconds), which is bumped on creation, spend, settlement and unroll, so one delta request catches new vtxos and state changes to known ones.

Sweeps and expiry updates do not bump updated_at server-side, so the sync additionally refreshes the cached unspent vtxos by outpoint. Spent vtxos are terminal and are never refetched. The watermark is taken from the local clock with a five minute margin to absorb clock skew.

The storage is pluggable via the VtxoCacheStore trait so integrators can back the cache with e.g. Redis; InMemoryVtxoCache is the default. Client::clear_vtxo_cache forces a full refetch.

Summary by CodeRabbit

  • New Features
    • Added a configurable VTXO caching layer with an in-memory default and support for custom cache stores (including cache injection for offline clients).
    • Added script-based VTXO retrieval support and improved offchain balance computation from a single VTXO listing.
  • Performance
    • Virtual outpoint retrieval is now cache-backed with serialized sync, incremental fetching, and faster server pagination.
  • Bug Fixes
    • Cache clearing now resets cached VTXOs and sync markers.
  • Tests
    • Added unit tests covering cache replacement, filtering, unspent-outpoint behavior, and clear/reset semantics.

Instead of crawling the entire (ever-growing) vtxo history on every
query, the client now caches vtxos and only fetches the delta since the
last sync. The server filters GetVtxos by the vtxo's updated_at
timestamp (milliseconds), which is bumped on creation, spend,
settlement and unroll, so one delta request catches new vtxos and state
changes to known ones.

Sweeps and expiry updates do not bump updated_at server-side, so the
sync additionally refreshes the cached unspent vtxos by outpoint. Spent
vtxos are terminal and are never refetched. The watermark is taken from
the local clock with a five minute margin to absorb clock skew.

The storage is pluggable via the VtxoCacheStore trait so integrators
can back the cache with e.g. Redis; InMemoryVtxoCache is the default.
Client::clear_vtxo_cache forces a full refetch.
@bonomat bonomat self-assigned this Jul 29, 2026
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 722b7b73-6323-4794-b605-d1d012b5ea5f

📥 Commits

Reviewing files that changed from the base of the PR and between 8d9f4a0 and 361160a.

📒 Files selected for processing (1)
  • ark-client/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • ark-client/src/lib.rs

Walkthrough

Adds a pluggable VTXO cache with configurable storage, incremental synchronization, serialized concurrent syncing, script-based requests, timestamp tracking, reusable balance calculation, larger pagination, and cache reset support.

Changes

VTXO cache integration

Layer / File(s) Summary
VTXO cache contract and state
ark-client/src/vtxo_cache.rs
Defines the cache store interface, in-memory state, script and outpoint filtering, synchronization metadata, reset behavior, and unit tests.
Client cache-backed synchronization
ark-core/src/server.rs, ark-client/src/utils.rs, ark-client/src/lib.rs
Adds script-based requests and millisecond timestamps, wires configurable cache storage into clients, serializes synchronization, and replaces direct retrieval with cache-backed fetching.
Balance reuse and VTXO pagination
ark-client/src/lib.rs
Adds reusable balance computation from fetched VTXOs and increases the internal pagination page size.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant VtxoCacheStore
  participant ArkServer
  Client->>VtxoCacheStore: read sync state
  Client->>ArkServer: request full or incremental VTXOs
  ArkServer-->>Client: return VTXOs
  Client->>VtxoCacheStore: upsert and mark synced
  Client->>VtxoCacheStore: return cached VTXOs
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 summarizes the main client change: adding VTXO caching with incremental sync.
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 feat/vtxo-cache

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.

Base automatically changed from chore/bump-arkd-0.9.15 to master July 29, 2026 10:56
@bonomat

bonomat commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

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

🤖 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-client/src/lib.rs`:
- Around line 1794-1797: Update clear_vtxo_cache to acquire
self.inner.vtxo_sync_lock before clearing self.inner.vtxo_cache, ensuring cache
invalidation is serialized with in-flight VTXO synchronization. Preserve the
existing async Result return behavior and clear operation.

In `@ark-client/src/vtxo_cache.rs`:
- Around line 38-41: Add store-scoped synchronization to VtxoCacheStore so the
complete sync sequence, including synced_scripts and watermark reads/updates,
executes atomically across all Client instances sharing a store. Expose and use
a lease/transaction mechanism, or maintain per-script watermarks atomically, and
update the client sync flow to hold that protection through delta fetching and
state advancement.
🪄 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: b828c536-c403-4068-88ac-a15c9613acf8

📥 Commits

Reviewing files that changed from the base of the PR and between 45aeb65 and 625ae15.

📒 Files selected for processing (4)
  • ark-client/src/lib.rs
  • ark-client/src/utils.rs
  • ark-client/src/vtxo_cache.rs
  • ark-core/src/server.rs

Comment thread ark-client/src/lib.rs
Comment on lines +1794 to +1797
/// Clear the VTXO cache, forcing the next VTXO query to fetch everything from the server
/// again.
pub async fn clear_vtxo_cache(&self) -> Result<(), Error> {
self.inner.vtxo_cache.clear().await

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Serialize cache clearing with an in-flight sync.

clear_vtxo_cache can run after upsert but before mark_synced; the sync then records the script and watermark over an empty cache, so later calls return no VTXOs instead of doing a full fetch. Acquire vtxo_sync_lock before clearing.

Proposed fix
 pub async fn clear_vtxo_cache(&self) -> Result<(), Error> {
+    let _sync_guard = self.vtxo_sync_lock.lock().await;
     self.inner.vtxo_cache.clear().await
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Clear the VTXO cache, forcing the next VTXO query to fetch everything from the server
/// again.
pub async fn clear_vtxo_cache(&self) -> Result<(), Error> {
self.inner.vtxo_cache.clear().await
/// Clear the VTXO cache, forcing the next VTXO query to fetch everything from the server
/// again.
pub async fn clear_vtxo_cache(&self) -> Result<(), Error> {
let _sync_guard = self.vtxo_sync_lock.lock().await;
self.inner.vtxo_cache.clear().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 `@ark-client/src/lib.rs` around lines 1794 - 1797, Update clear_vtxo_cache to
acquire self.inner.vtxo_sync_lock before clearing self.inner.vtxo_cache,
ensuring cache invalidation is serialized with in-flight VTXO synchronization.
Preserve the existing async Result return behavior and clear operation.

Comment on lines +38 to +41
/// Implementations must be safe for concurrent use. The client serializes cache _syncs_
/// internally, but reads may happen concurrently.
#[async_trait]
pub trait VtxoCacheStore: Send + Sync {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Add cache-scoped synchronization for shared stores.

Send + Sync only protects individual calls. Two Client instances sharing this store can interleave synced_scripts/watermark updates: one can mark a script synced after the other captured its script set, then the other can advance the global watermark without fetching that script’s delta. After the five-minute overlap, its updates can be skipped permanently. Add a store-owned sync lease/transaction, or use atomically maintained per-script watermarks.

🤖 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-client/src/vtxo_cache.rs` around lines 38 - 41, Add store-scoped
synchronization to VtxoCacheStore so the complete sync sequence, including
synced_scripts and watermark reads/updates, executes atomically across all
Client instances sharing a store. Expose and use a lease/transaction mechanism,
or maintain per-script watermarks atomically, and update the client sync flow to
hold that protection through delta fetching and state advancement.

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

This PR reshapes how the client learns about VTXO state (spent / settled / unrolled / swept / expired) — i.e. the wallet's view of protocol-level truth. The cache logic looks reasonable but has correctness holes worth resolving before merge. Please get a maintainer familiar with arkd's updated_at semantics to sign off.

Server-side claims I verified in arkd (internal/infrastructure/db/sqlite/sqlc/query.sql): upsert (create), UpdateVtxoSpent, UpdateVtxoSettled, UpdateVtxoUnrolled all bump updated_at; UpdateVtxoExpiration and sweep markers do not. So the PR's cache invariants match server behavior today. SelectVtxosWithPubkeys uses updated_at >= :after (inclusive), so passing back an after we've already seen is dupe-safe via upsert.

Findings:

  1. clear_vtxo_cache doesn't hold vtxo_sync_lock (ark-client/src/lib.rs new clear_vtxo_cache, ~L1795). Interleaved with an in-flight get_virtual_tx_outpoints, this sequence is possible:

    • sync fetches + upserts a batch,
    • clear() wipes state,
    • sync completes and calls mark_synced(unknown_scripts) + set_last_sync_ms(now_ms).

    Result: synced_scripts is populated and last_sync_ms is set, but the actual VTXOs are gone. The very next get_virtual_tx_outpoints call sees a non-empty synced_scripts, skips the full fetch, only issues a delta with after = now_ms - 5min, and permanently loses every VTXO whose updated_at is older than that watermark. On a wallet with any historical activity that means silent balance loss until the caller thinks to clear again. Either acquire the sync lock in clear_vtxo_cache, or document that clearing must not race with sync (with a hard debug-assertion behind the trait).

  2. Clock skew is only compensated in one direction (ark-client/src/vtxo_cache.rs L26–29 and lib.rs delta computation after = (last_sync_ms - SYNC_MARGIN_MS).max(1)). The watermark is written from the local clock but compared against server-side updated_at. If the client is more than SYNC_MARGIN_MS ahead of the server, server updates whose real updated_at is <= client_watermark - 5min never come back in future deltas. In practice this hides spent/settled/unrolled transitions — a protocol-critical VTXO state — from the wallet indefinitely, with no error surfaced. Options: keep the max server-observed updated_at (would need the server response to expose it) and use that as the watermark; or clamp SYNC_MARGIN_MS symmetrically and document the drift budget; or fall back to a periodic full re-sync. This is the failure mode most likely to bite mobile / IoT wallets whose clocks drift.

  3. No coverage on the sync algorithm itself. The tests in ark-client/src/vtxo_cache.rs exercise only CacheState (upsert, sort, filter, clear). None of the interesting integration behavior in get_virtual_tx_outpoints is tested: mixed unknown+synced scripts, delta-only path, refresh-outpoints ordering, partial-failure mid-sync, concurrent clear vs sync (#1), first-sync race where a VTXO is upserted before mark_synced runs, empty inputs. Danger already flagged this: "Source changed with no test changes — protocol code needs tests." A minimal fake VtxoCacheStore + ark_grpc mock would let you cover the interesting cases.

  4. Cross-script staleness (lib.rs new sync flow). refresh_outpoints = cache.unspent_outpoints_for(&scripts) filters by current request scripts, but the delta covers all synced_scripts. So if I ever call get_virtual_tx_outpoints({A, B, C}), then later only {A}, the cached unspent VTXOs on B and C never get their swept/expiry state refreshed until someone requests them again. list_vtxos() (which passes all active addresses) sidesteps this; direct callers of list_vtxos_for_addresses won't. Either refresh all synced-script unspent outpoints, or document that unspent state on non-requested scripts may lag.

  5. Unbounded growth / large request bodies. synced_scripts only ever grows — long-lived wallets that churn through addresses will accumulate every historical script forever. And on a mature cache, both the delta request (every synced script) and the refresh request (every unspent outpoint) go out as a single un-chunked gRPC message, while fetch_all_vtxos only paginates the response. This will eventually hit the gRPC message-size ceiling. Not a launch blocker, but worth an eviction / chunking follow-up.

  6. Nit: the module doc says the server "filters GetVtxos by the VTXO's updated_at timestamp"; the actual query is updated_at >= :after (inclusive). Behavior is fine (upsert makes duplicates harmless), but mentioning the inclusive semantic prevents a future contributor from tightening the client to strict > and breaking recovery.

  7. Cross-repo: GetVtxosRequest::new_for_scripts is additive; no existing public API changes. Consumers of ark-core in the other SDKs are unaffected.

Please block on (1) and (2) before merging. (3) is strongly recommended given the protocol surface. (4)–(6) can be follow-ups.

bonomat added 3 commits July 30, 2026 08:48
offchain_balance discarded the vtxo list it fetched, so callers needing
both the balance and the list paid for two full syncs back-to-back.

Move the classification fold into OffChainBalance::from_vtxo_list and
add Client::offchain_balance_with_vtxos, which returns the balance
together with the list it was computed from. offchain_balance is now a
thin wrapper and fetches the server info only once.
The unknown-scripts fetch, the delta fetch and the unspent outpoint
refresh are independent, so run them via try_join instead of
sequentially. Warm sync latency drops from the sum of the three round
trips to the slowest one. The watermark still only advances when all
three succeed.
The server honors the requested page size (its per-endpoint max is only
the default for requests that don't set one) and re-runs the full vtxo
query for every page before paginating in memory, so fewer, larger
pages are strictly cheaper on both ends. 2000 vtxos stay comfortably
within tonic's 4 MiB response limit.

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

♻️ Duplicate comments (1)
ark-client/src/lib.rs (1)

1871-1873: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

clear_vtxo_cache still doesn't serialize with an in-flight sync.

This was flagged in a previous review round on this stack and remains unfixed. If clear_vtxo_cache runs concurrently with get_virtual_tx_outpoints, it can execute after that call's upserts but before its mark_synced/set_last_sync_ms. The in-flight sync then finishes by recording scripts as synced and advancing the watermark over a cache that was just wiped, so subsequent calls treat those scripts as fully synced and never do a full refetch — silently losing all historical VTXOs for them.

🔒 Proposed fix
 pub async fn clear_vtxo_cache(&self) -> Result<(), Error> {
+    let _sync_guard = self.vtxo_sync_lock.lock().await;
     self.inner.vtxo_cache.clear().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 `@ark-client/src/lib.rs` around lines 1871 - 1873, Update clear_vtxo_cache to
synchronize with the in-flight get_virtual_tx_outpoints sync, using the same
lock or coordination mechanism that protects its upsert and
mark_synced/set_last_sync_ms sequence. Ensure cache clearing cannot occur
between those operations, so a clear either completes before the sync or after
the entire sync has finished.
🧹 Nitpick comments (1)
ark-client/src/lib.rs (1)

1834-1845: 🚀 Performance & Scalability | 🔵 Trivial

Global delta scope will grow with the wallet's known-script set.

delta_fut intentionally re-fetches the delta for every script ever marked synced (not just the ones requested), which is necessary for watermark correctness as the comment explains. But get_offchain_addresses_with_server_info/persist_watch_boarding_outputs enumerate a script per (server key × exit-delay candidate × delegator), so synced_scripts can grow substantially for wallets with several deprecated signers/delegators/legacy exit delays. Every future get_virtual_tx_outpoints call then pays a delta query over that entire, ever-growing set regardless of what was actually requested.

Worth keeping an eye on server-side query cost as this list grows; a per-script (or per-batch) watermark could avoid the global re-scan if this becomes a bottleneck in practice.

🤖 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-client/src/lib.rs` around lines 1834 - 1845, The global delta query in
delta_fut scans the ever-growing synced_scripts set on every
get_virtual_tx_outpoints call; assess and, if appropriate, redesign this flow to
use per-script or batched watermarks while preserving complete delta coverage
and preventing skipped updates. Focus changes around delta_fut and the
associated synced_scripts watermark handling, and retain the existing behavior
unless the new scope-aware approach is implemented safely.
🤖 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.

Duplicate comments:
In `@ark-client/src/lib.rs`:
- Around line 1871-1873: Update clear_vtxo_cache to synchronize with the
in-flight get_virtual_tx_outpoints sync, using the same lock or coordination
mechanism that protects its upsert and mark_synced/set_last_sync_ms sequence.
Ensure cache clearing cannot occur between those operations, so a clear either
completes before the sync or after the entire sync has finished.

---

Nitpick comments:
In `@ark-client/src/lib.rs`:
- Around line 1834-1845: The global delta query in delta_fut scans the
ever-growing synced_scripts set on every get_virtual_tx_outpoints call; assess
and, if appropriate, redesign this flow to use per-script or batched watermarks
while preserving complete delta coverage and preventing skipped updates. Focus
changes around delta_fut and the associated synced_scripts watermark handling,
and retain the existing behavior unless the new scope-aware approach is
implemented safely.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c0493c3-8979-42d7-ab9f-8bcd2424cdf1

📥 Commits

Reviewing files that changed from the base of the PR and between 625ae15 and 8d9f4a0.

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

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

Follow-up pass on 8d9f4a0 (3 commits since 625ae15, all in ark-client/src/lib.rs, +113/-61). Zero prior findings fixed; six of seven still open. One new regression from the parallelization commit.

Status of prior findings

  1. clear_vtxo_cache doesn't hold vtxo_sync_lockStill open. L1871-1873 untouched. Same silent-balance-loss race as before. CodeRabbit is flagging it independently too.
  2. Unidirectional clock-skew compensation (SYNC_MARGIN_MS protects against server-ahead-of-client, not client-ahead-of-server) — Still open. The delta_after computation was extracted to L1822 but the semantics are identical.
  3. No test coverage on the sync algorithmStill open. Danger flags it again. The refactor to try_join! and the new from_vtxo_list split make this more urgent, not less — both are new joinpoints that unit tests would have exercised trivially.
  4. Cross-script staleness in refresh_outpoints (only refreshes unspent outpoints on requested scripts, not all synced ones) — Still open. L1820 unchanged.
  5. Unbounded synced_scripts growth and un-chunked delta/refresh request bodiesStill open. Commit 8d9f4a0 bumps response pagination (PAGE_SIZE 100→2000) which is fine — I verified in arkd internal/core/application/indexer.go L1397 that the server treats maxSize as a default only when PageSize <= 0, so the claim in the commit message is accurate, and 2000 fits comfortably under tonic's 4 MiB default. But that is orthogonal to the request-body concern — every synced script and every unspent outpoint still ships in a single un-chunked gRPC message. Follow-up eviction/chunking still warranted.
  6. Doc nit on inclusive updated_at >= afterStill open. vtxo_cache.rs not touched.
  7. Cross-repo additive OK — Still applies; no new public API breakage in this delta.

New findings on the incremental diff

8. Parallelizing the three sync fetches introduces a delta-vs-refresh ordering regression (commit 72e67f2, ark-client/src/lib.rs L1856-1861).

The old sequential code guaranteed refresh was queried strictly after delta returned, so refresh always observed at-least-as-fresh state and last-write-wins was correct. In the new try_join! version the three server queries execute in undefined order, but the upserts still run in fixed order: unknown → delta → refresh. That leaves this window:

  • VTXO X is cached unspent (so it's in refresh_outpoints), X's script is in synced_scripts, X's updated_at is inside the delta window (recent activity).
  • t_refresh_query executes before the spend → returns X as unspent.
  • Spend of X hits the server → updated_at bumped.
  • t_delta_query executes after the spend → returns X as spent.
  • Upsert order: delta (spent) first, then refresh (unspent). Final cache state: unspent.

Because the spend bumps updated_at and the value stays inside the next delta window for SYNC_MARGIN_MS, this self-heals on the next sync — so it's transient staleness, not silent permanent loss like finding #1. Still a real regression from the ordering guarantee the sequential code provided, and a wallet displaying "unspent" for something the server considers spent is protocol-visible.

Simplest fix: swap the upsert order so delta is applied last (delta covers everything with a bumped updated_at, which by definition includes any spend that raced the sync). Cleaner fix: give upsert a spent-wins-over-unspent merge rule, since spent is terminal — that also removes the ordering coupling entirely and would be the more robust invariant for the cache trait to advertise.

9. Nit (commit 8d9f4a0, PAGE_SIZE=2000). No safety net if a VTXO ever carries an unusually long commitment_txids chain or assets list — the response is uncapped, so a pathological account could still exceed tonic's 4 MiB default. Not a launch blocker; consider either bumping max_decoding_message_size explicitly or halving-and-retrying on ResourceExhausted.

10. Refactor commit 60f11e2 looks correct. OffChainBalance::from_vtxo_list is a straight extraction of the existing computation. offchain_balance_with_vtxos fetches server_info once, calls list_vtxos_with_server_info (which takes the sync lock), then classifies — one sync, no double-fetch. offchain_balance is now a thin wrapper. Public API surface grows (new from_vtxo_list, new offchain_balance_with_vtxos) but nothing breaks. No issues.

Recommendation

Please still block on prior findings #1 and #2, and either reorder the upserts or add a spent-wins merge to close #8 (all three are simple, contained changes). Then get a maintainer to sign off on the arkd updated_at semantics assumption before merge.

Inlining three copies of the fetch future into the sync future made it
large enough to overflow the thread stack in debug builds (caught by
the e2e suite). Box each fetch future so the sync future stays small.

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

Third pass on 361160a (1 commit, 1 file, +9/-7 in ark-client/src/lib.rs). Pure debug-build stack-overflow fix: each of unknown_fut / delta_fut / refresh_fut is now Box::pin(async { ... }) so the outer sync future stays small. Semantics of the concurrent fetch are unchanged. 0 of 8 prior findings fixed in this delta.

Delta review

The Box::pin wrapping is correct — it heap-allocates each subfuture's state instead of embedding all three into the parent generator, which is the standard fix for try_join!-of-large-async-blocks blowing the debug stack. try_join! still runs them concurrently with the same all-or-nothing cancellation semantics, and the surrounding lock / upsert / mark_synced / set_last_sync_ms sequence is byte-identical (L1805, L1858-1866). No new correctness concerns from this hunk itself.

Worth noting for the author: this incident is a soft signal that this function is doing too much for one async body — the fact that inlining three fetches overflowed the debug stack means the outer future is already carrying a lot of state (guard + cache handle + three script sets + refresh outpoints + delta watermark + three request builders + join output). Boxing works, but a future refactor that extracts the "fetch three deltas concurrently" step into a helper async fn would give the compiler natural boxing at the fn boundary and shrink the outer frame without hand-boxing.

Status of prior findings (all against unchanged code)

  1. clear_vtxo_cache doesn't hold vtxo_sync_lock (lib.rs L1873-1875) — Still open. Untouched. Silent-balance-loss race persists. CodeRabbit is flagging it independently as well.
  2. Unidirectional clock-skew compensation (lib.rs L1822, vtxo_cache.rs SYNC_MARGIN_MS) — Still open. delta_after computation unchanged.
  3. No test coverage on the sync algorithmStill open. Danger flags again ("Source changed with no test changes"). The Box::pin change is exactly the kind of thing a unit test around get_virtual_tx_outpoints would have caught before e2e — even a smoke test that constructs a Client with a fake VtxoCacheStore and asserts the sync path runs to completion in a debug build would have prevented this round-trip.
  4. Cross-script staleness in refresh_outpoints (lib.rs L1820) — Still open. Filter scope unchanged.
  5. Unbounded synced_scripts growth / un-chunked delta+refresh request bodiesStill open.
  6. Doc nit on inclusive updated_at >= after in vtxo_cache.rsStill open. File not touched in this delta.
  7. Cross-repo additive OK — Still applies; no public API change in this hunk.
  8. Delta-vs-refresh ordering regression from parallelization (lib.rs L1858-1863, upsert order unknown → delta → refresh) — Still open. The Box::pin change did not touch the upsert order and did not add a spent-wins merge rule. A spent-then-delta-races-refresh window still allows the cache to end in the stale unspent state until the next sync.

Recommendation

Unchanged from last pass: block on (1) and (2), fix (8) with either an upsert reorder (delta last) or a spent-wins merge rule in the VtxoCacheStore trait, and please add at least a minimal fake-store unit test for get_virtual_tx_outpoints before merge — the pattern of "e2e catches what unit tests would have" is now recurring on this PR. Human protocol review on the arkd updated_at assumptions still recommended.

@luckysori luckysori 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 did not look at what any agent said, but LGTM. I will run my own AI-assisted review too.

@@ -0,0 +1,281 @@
//! # VTXO cache
//!
//! The Ark client caches VTXOs and syncs them incrementally, instead of crawling the entire

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.

We should use Arkade instead of Ark.

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.

Just curious, in which specific cases should we use Arkade instead of Ark? In comments only? I am asking this because this is the second time I am seeing this review comment and do not fully understand where specifically Arkade should be used in place of Ark.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We should mostly use Arkade here because Arkade is an Ark implementation. It also slightly differs to the original Ark protocol as proposed in 2023.
So yes, we should use Arkade here.

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.

4 participants