arkd-wallet: source UTXOs from NBXplorer events and cache wallet key paths - #1093
arkd-wallet: source UTXOs from NBXplorer events and cache wallet key paths#1093bitcoin-coder-bob wants to merge 7 commits into
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review. WalkthroughThis PR adds a bounded LRU cache for script-to-key-path mappings. The wallet populates and reads the cache during UTXO operations and private-key derivation. NBXplorer now builds UTXOs directly from matching websocket transaction events. ChangesKey Path Caching and Websocket UTXO Parsing
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Refactor Sequence Diagram(s)sequenceDiagram
participant WebsocketReader
participant EventHandler as utxosFromTransactionEvent
participant NotificationsChannel
WebsocketReader->>EventHandler: newtransaction event
EventHandler->>EventHandler: validate group and parse outputs
EventHandler->>NotificationsChannel: UTXOs with key paths
sequenceDiagram
participant Caller
participant Wallet as getPrivateKeyFromScript
participant KeyPathCache
participant NBXplorer
Caller->>Wallet: script
Wallet->>KeyPathCache: get(script)
alt Cache hit
KeyPathCache-->>Wallet: derivation scheme and key path
Wallet-->>Caller: derived private key
else Cache miss
Wallet->>NBXplorer: lookup script
NBXplorer-->>Wallet: key path result
Wallet->>KeyPathCache: set successful mapping
Wallet-->>Caller: derived private key
end
Merge Risk: ⚪ Minimal · up to No concrete merge-blocking risk is identified in the available review evidence. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
ghost
left a comment
There was a problem hiding this comment.
Code Review — arkd-wallet: source UTXOs from NBXplorer events and cache wallet key paths
Reviewer: Arkana (automated, aggressive)
Verdict: Request changes (1 actionable issue) + request human sign-off (signing path modified)
Summary
Clean, well-tested PR. The two optimizations are sound:
- Parsing UTXO data directly from the
newtransactionevent payload instead of re-fetching the entire group UTXO set — eliminates an O(n) HTTP call per event. - Caching
script → (derivationScheme, keyPath)to skip per-input NBXplorer lookups during signing.
Test coverage is thorough (event parsing, cache hit/miss/fallback/concurrency, edge cases). The json.RawMessage change for event.Data and the Confirmations int → uint32 alignment are correct improvements. The behavioral fix in getPrivateKeyFromScript (skipping schemes that return an empty KeyPath instead of trying to derive from "") is a genuine bug fix.
No cross-repo breakage: ports.Utxo is only consumed within pkg/arkd-wallet/, and the new KeyPath field is added with named-field construction everywhere.
🔴 ACTIONABLE — Unbounded cache growth
pkg/arkd-wallet/core/application/wallet/keypath_cache.go
The keyPathCache map grows without bound. Every UTXO the wallet ever sees (via ListConnectorUtxos, SelectUtxos, withdrawAll, getBalance, and event notifications) adds an entry that is never evicted.
For a long-running arkd server managing many watched addresses (e.g. thousands of boarding UTXOs per round across many rounds), this is a slow memory leak. The comment says "cached entries never need invalidation" — true for correctness, but irrelevant for memory management. Scripts from spent UTXOs will never be looked up again, yet their entries persist forever.
Options (pick one):
- LRU with a reasonable cap (e.g. 10k–50k entries). A signing operation only needs the scripts of the inputs being signed, so eviction is always safe — the NBXplorer fallback path still works.
- Periodic sweep: clear the cache when the wallet detects a new round/block (coarse but simple).
- Explicit eviction after signing: remove entries for scripts that were just signed (they're spent now).
The first option is simplest and safest. An LRU miss just falls back to the existing HTTP path, so there's zero correctness risk.
🟡 STYLE — Positional struct init is fragile
pkg/arkd-wallet/core/application/wallet/service.go:65
return &wallet{opts, newOutpointLocker(time.Minute), nil, newKeyPathCache(), make(chan bool)}This uses positional field initialization. If anyone reorders the wallet struct fields, this silently breaks at compile time only if types mismatch — otherwise it's a silent bug. Prefer named fields:
return &wallet{
WalletOptions: opts,
locker: newOutpointLocker(time.Minute),
keyPaths: newKeyPathCache(),
readyCh: make(chan bool),
}Not blocking, but worth fixing while you're here.
🟢 LOOKS GOOD
utxosFromTransactionEvent— pure function, well-scoped, correctTrackedSourcefiltering, proper error propagation. Themake([]ports.Utxo, 0, len(newTxEvent.Outputs))pre-allocation is a nice touch.event.Data→json.RawMessage— correct elimination of the redundant marshal→unmarshal roundtrip.matchedOutputtype — clean mapping of the NBXplorer event payload. Fields match the NBXplorer 2.6.7 schema.castUtxonow carriesKeyPath— correct one-line addition.getPrivateKeyFromScriptempty-KeyPath guard — this is a bug fix: the old code would pass""toderiveKeyon the first scheme match with empty KeyPath, likely erroring out and halting the loop before trying the connector scheme. Now it correctly falls through.- Test coverage — event parsing (group vs derivation-scheme vs newblock, single/multi/empty outputs, confirmations, malformed JSON, invalid txid, keyPath passthrough), cache (set/get/miss/overwrite/concurrent), signing path (locked wallet, cache hit, cache miss + backfill, connector fallback, empty KeyPath fallback, unknown script). Benchmark is well-structured.
⚠️ PROTOCOL-ADJACENT — Human review required
This PR modifies getPrivateKeyFromScript, which is on the signing path. The change is a caching layer — the underlying deriveKey call and its parameters are unchanged — so the risk of fund loss is minimal. However, per protocol-critical review rules:
A human must review and approve this PR before merge. Specifically, verify:
- The cache can never return a stale/wrong
(derivationScheme, keyPath)for a given script (it can't, because the mapping is immutable by derivation, but a human should confirm this assumption holds for all address types the wallet manages). - The empty-KeyPath guard in the fallback path is correct behavior (it is — NBXplorer returns empty KeyPath for scripts not derived from the queried scheme).
ghost
left a comment
There was a problem hiding this comment.
Follow-up Review — new commit 923414a
Reviewer: Arkana (automated, aggressive)
Verdict: ✅ Approve (Arkana side)
Both issues from my previous review are resolved:
🔴 Unbounded cache → Fixed
sync.RWMutex + map[string]keyPathEntry replaced with hashicorp/golang-lru/v2 lru.Cache capped at 50,000 entries. The library is concurrency-safe internally, so the manual mutex removal is correct. Eviction of old entries is safe — a cache miss falls back to the NBXplorer HTTP path with zero correctness risk.
New TestKeyPathCache_Bounded test verifies the cap holds and oldest entries are evicted. Good.
🟡 Positional struct init → Fixed
wallet{} in New() now uses named fields. Clean.
No new issues in the incremental diff. The go.mod addition of hashicorp/golang-lru/v2 v2.0.7 is appropriate.
⚠️ Human review still required
This PR modifies getPrivateKeyFromScript (signing path). Per protocol-critical rules, a human must also approve before merge. The two things to verify remain unchanged from my first review:
- The
script → (derivationScheme, keyPath)mapping is truly immutable for all address types the wallet manages. - The empty-KeyPath guard correctly skips schemes that don't own the script.
ghost
left a comment
There was a problem hiding this comment.
Follow-up Review — new commit 923414a
Reviewer: Arkana (automated, aggressive)
Verdict: ✅ Approve (Arkana side)
Both issues from my previous review are resolved:
🔴 Unbounded cache → Fixed
sync.RWMutex + map[string]keyPathEntry replaced with hashicorp/golang-lru/v2 lru.Cache capped at 50,000 entries. The library is concurrency-safe internally, so the manual mutex removal is correct. Eviction of old entries is safe — a cache miss falls back to the NBXplorer HTTP path with zero correctness risk.
New TestKeyPathCache_Bounded test verifies the cap holds and oldest entries are evicted. Good.
🟡 Positional struct init → Fixed
wallet{} in New() now uses named fields. Clean.
No new issues in the incremental diff. The go.mod addition of hashicorp/golang-lru/v2 v2.0.7 is appropriate.
⚠️ Human review still required
This PR modifies getPrivateKeyFromScript (signing path). Per protocol-critical rules, a human must also approve before merge. The two things to verify remain unchanged from my first review:
- The
script → (derivationScheme, keyPath)mapping is truly immutable for all address types the wallet manages. - The empty-KeyPath guard correctly skips schemes that don't own the script.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Catchup review — head 923414a
Re-verified the PR at the current head. My earlier concerns (unbounded cache, positional struct init) are resolved; the LRU cap + eviction test and named-field construction are in place. No new blockers on this pass — a few small residual notes only.
Confirmed correct
- Cache scoping.
cacheKeyPathsis invoked with the exact derivation scheme used for the enclosingNbxplorer.GetUtxoscall at every call site (service.go:344,397,842,851,999). Because the main and connector accounts are distinct BIP86 xpubs,script → (scheme, keyPath)is deterministic — no way to seed a wrong-key entry. - LRU + concurrency.
hashicorp/golang-lru/v2is safe for concurrent Add/Get. Cache is separate fromkeyMgr, so lock/unlock cycles do not corrupt it; on a Restore-to-a-different-seed, stale entries would failderiveKeywith "invalid xpub" rather than sign with a wrong key. - Small bug fix in the fallback. Previously
getPrivateKeyFromScript(service.go:1013) took the first non-error response fromGetScriptPubKeyDetailsand passed the (possibly empty)KeyPathstraight toderiveKey, which errors on""and aborted signing before ever trying the connector scheme. The newif scriptPubKeyDetails.KeyPath == "" { continue }correctly probes the other scheme and only returnsnil, nilwhen neither owns the script — surfacing the "not a wallet script" error at the SignTransaction level as intended.
Residual notes (non-blocking)
Confirmationstype widenedint → uint32innewTransactionEvent.TransactionData(types.go:141). Newtransaction events from NBXplorer are only sent for accepted mempool/confirmed txs (value ≥ 0), so this is fine in practice — but any future path that surfaces reorg-invalidated txs through this same struct (NBXplorer uses -1 elsewhere) would silently fail to unmarshal. Worth a mental note.matchedOutput.Value uint64(types.go:132) assumes the BTCMoneyserializer emits a bare integer, matchingutxoResponse.Value. Correct for BTC/NBXplorer 2.6.7. There is no in-tree integration test against a real NBXplorer instance that exercises the new field names (outputs,matchedOutput.*); the mocked-JSON unit tests are the sole shape check. A single e2e inarkade-regtestcovering the deposit-notification path would harden this against NBXplorer version drift.- Log-spam surface. The new
log.Errorf("failed to parse transaction event: %s", err)inGetAddressNotificationsfires on every unparseable outer frame with no rate limit. Previously errors were swallowed. If a broken upstream ever emits a stream of non-JSON frames this could flood logs. Minor. - Cross-repo impact.
ports.Utxolives inside thepkg/arkd-walletsubmodule and is not consumed by the external SDKs — safe.
Comment-only from me; not blocking merge.
|
This PR has been open for 3+ days without review. @bitcoin-coder-bob is anyone looking at this? |
|
This PR has been open for 30+ days without review. @bitcoin-coder-bob is anyone looking at this? |
|
This PR has been open for 5 days without review. @bitcoin-coder-bob is anyone looking at this? |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
This PR (NBXplorer UTXO efficiency) has been open 76 days without review. @bitcoin-coder-bob is anyone picking this up?
|
This PR has been open 80+ days without a formal review. @bitcoin-coder-bob — this is the bottom of the arkd-wallet stack; getting this merged would unblock the chain above it. |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Arkana review — #1093 (sha 923414a)
arkd-wallet: source UTXOs from NBXplorer events and cache wallet key paths
Key path cache
- LRU cache (50k entries) for script → (derivation scheme, key path)
- Entries are immutable (same script always maps to the same key path), so there are no staleness concerns
- LRU eviction is safe: a miss falls back to the NBXplorer per-input lookup
set()correctly ignores empty script or key path entries- Good test coverage: basic set/get, miss, empty rejection, bounded eviction, concurrent access
Looks correct
- Cache is populated at
ListConnectorUtxoscall time, then hits during signing — right placement - The 50k entry bound keeps memory bounded even for a long-running operator wallet with heavy connector churn
- Concurrent test exercises the
hashicorp/golang-lru/v2thread-safety, which is the correct dependency
One note
Only saw keypath_cache.go and part of service.go in the diff — the NBXplorer event-sourcing aspect of the PR title isn't covered here. Confirm the event-based UTXO path doesn't introduce any ordering assumptions (e.g. that a UTXO event always arrives before the signing call that needs its key path).
Verdict: cache layer looks well-designed and tested. Performance-only change, no correctness concerns here.
|
This PR has been open for 12+ weeks without a review. @bitcoin-coder-bob — is anyone looking at this? Is it still waiting on other PRs in the stack, or does it need a reviewer assigned? |
…iency # Conflicts: # pkg/arkd-wallet/go.mod
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Follow-up Review — merge commit `e3dda22`
Status: 1 commit since `923414a` — a merge of `origin/master` into the branch. Both prior issues remain Fixed. Zero remaining actionable issues in the incremental diff.
What changed since `923414a`
Commit `e3dda22` is a pure merge commit (parents: `923414a` + `f863e48`). It brings 118 master commits into the branch. The only conflict was `pkg/arkd-wallet/go.mod`, resolved correctly by preserving the `github.com/hashicorp/golang-lru/v2 v2.0.7` entry the PR introduced. No `pkg/arkd-wallet/core/` source files changed in this incremental step.
The notable master commit included here is `f863e48` ("Migrate to btcd v0.26 and the v2 sub-modules", PR #1173). The PR's test imports — `github.com/btcsuite/btcd/chaincfg/v2` and `github.com/btcsuite/btcd/wire/v2` — are consistent with that migration, and CI shows no compile failures.
Prior-issue closure
| # | Finding | Status |
|---|---|---|
| 1 | 🔴 Unbounded cache growth (keyPathCache map leaked indefinitely) |
Fixed in `923414a` — LRU cap of 50 000 entries via hashicorp/golang-lru/v2. TestKeyPathCache_Bounded verifies the cap. |
| 2 | 🟡 Positional struct init in New() (wallet{}) |
Fixed in `923414a` — named fields used throughout. |
New findings (incremental diff)
go.mod conflict resolution — `pkg/arkd-wallet/go.mod` correctly keeps hashicorp/golang-lru/v2 v2.0.7 from the PR side and absorbs all master-side dependency bumps. No phantom or dropped dependency. No issue.
Nothing else in the incremental diff touches wallet logic. No new findings.
⚠️ Standing requirement — human review still required
This PR modifies `getPrivateKeyFromScript` (signing path). Arkana's approval covers the caching and event-parsing mechanics. A human must also approve before merge, confirming:
- The `script → (derivationScheme, keyPath)` mapping is immutable for all address types this wallet manages.
- The empty-KeyPath guard correctly skips schemes that don't own the queried script.
…vent Regroup only for the tests. The parse failure of a notification frame is benign for the wallet and was previously swallowed, so it logs at warn.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Follow-up Review — head `7138bb9`
Status: 1 commit since `e3dda22`; 1 prior residual note addressed; 2 non-blocking residual notes unchanged; 0 new findings.
What changed since `e3dda22`
Commit `7138bb9` ("arkd-wallet: group the key path cache tests, warn on an unparseable event") touches exactly two files:
-
pkg/arkd-wallet/core/application/wallet/keypath_cache_test.go—TestKeyPathCache_BoundedandTestKeyPathCache_Concurrentre-homed ast.Run("bounded", …)andt.Run("concurrent", …)subtests insideTestKeyPathCache. Pure organisational refactor; test logic is byte-for-byte identical. -
pkg/arkd-wallet/core/infrastructure/nbxplorer/service.go:702—log.Errorf("failed to parse transaction event: …")→log.Warnf(…). One-line change.
Prior-issue closure
| # | Finding | Status |
|---|---|---|
| 🔴 Unbounded cache growth | Fixed in 923414a — LRU cap 50 000 + TestKeyPathCache_Bounded. Not regressed. |
|
🟡 Positional struct init in New() |
Fixed in 923414a. Not regressed. |
|
| Log-spam on malformed frames (residual note #3, catchup review) | Fixed in 7138bb9 — demoted to Warnf. Directly addresses the concern. |
|
Confirmations int → uint32 widening in newTransactionEvent.TransactionData (residual note #1) |
Still open (non-blocking). Reorg-invalidated frames from a future NBXplorer version that emits -1 would silently fail to unmarshal. No change in this commit; awareness recorded. |
|
No in-tree integration test for outputs/matchedOutput.* field names against a live NBXplorer instance (residual note #2) |
Still open (non-blocking). Unit tests with mocked JSON remain the sole shape check; e2e coverage would harden against version drift. No change in this commit. |
New findings
None. The incremental diff is two cosmetic changes with no logic impact.
⚠️ Standing requirement — human review still required
This PR modifies getPrivateKeyFromScript (signing path). Arkana's approval covers the caching and event-parsing mechanics. A human must also approve before merge, confirming:
- The
script → (derivationScheme, keyPath)mapping is immutable for all address types this wallet manages. - The empty-
KeyPathguard correctly skips schemes that don't own the queried script.
Both new test files opened with scaffolding. The nbxplorer file led with three JSON builders and a fixture const block, and the wallet file led with an 87-line fake and its constructor, so in each case the tests started well down the file. Moves only. The fake, the builders, the fixtures and the constructor now sit after the tests and the benchmark, with no change to any of them or to the tests that use them. Also drops a colon from the keypath cache comment.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Follow-up Review — head `167701c`
Status: 3 files changed since `7138bb9`; 0 prior issues newly resolved; 2 standing non-blocking residuals unchanged; 1 new cosmetic finding (test-only).
All three files in this increment are pure reorganisations — zero logic changes.
Prior-issue closure
| Finding | Status |
|---|---|
| 🔴 Unbounded cache growth | Fixed (in `923414a`). Not regressed. |
| 🟡 Positional struct init in `New()` | Fixed (in `923414a`). Not regressed. |
Log-spam on malformed frames → Warnf |
Fixed (in `7138bb9`). Not regressed. |
| `Confirmations int → uint32` reorg-edge residual | Still open (non-blocking, no change in this commit). |
| No in-tree e2e test for `outputs`/`matchedOutput.*` field names | Still open (non-blocking, no change in this commit). |
Incremental diff
pkg/arkd-wallet/core/application/wallet/keypath_cache.go — one-word comment edit ("immutable: a given" → "immutable, since a given"). No logic impact.
pkg/arkd-wallet/core/application/wallet/service_test.go — fakeNbxplorer struct and newTestWallet helper moved from top of file to bottom. Pure reorganisation; logic and coverage identical.
pkg/arkd-wallet/core/infrastructure/nbxplorer/service_test.go — BenchmarkNotificationProcessing moved earlier in the file, with constants and helpers relocated alongside it. Pure reorganisation, but the move left two comment artefacts:
-
Lines 141–142: The comment that originally documented
fetchAndFilterGroupUtxos("// fetchAndFilterGroupUtxos replicates the previous (pre-optimization) behavior: // on every event it fetched the whole group UTXO set over HTTP and filtered it") is now truncated at two lines and immediately precedesBenchmarkNotificationProcessing, making it read as if it documents the benchmark. The third line ("It is kept here only as a benchmark baseline.") and thefunc fetchAndFilterGroupUtxosdeclaration were separated by the move. -
Lines 280–282 (end of file): Three orphaned comment lines — "// BenchmarkNotificationProcessing compares the per-event work of the new / event-payload parsing against the old approach … the new one" — are stranded at the bottom of the file with no function following them. The function they described is now at line 144.
Neither artefact affects compilation or test behaviour (Go allows comment blocks not attached to declarations), but they are misleading to future readers. Worth a follow-up tidy in the same PR or as a quick post-merge cleanup.
New findings
No logic, protocol, or security findings. The two comment artefacts noted above are the only new observations; they are non-blocking.
⚠️ Standing requirement — human review still required
This PR modifies `getPrivateKeyFromScript` (signing path). Arkana's review covers the caching and event-parsing mechanics. A human must also approve before merge, confirming:
- The `script → (derivationScheme, keyPath)` mapping is immutable for all address types this wallet manages.
- The empty-KeyPath guard correctly skips schemes that do not own the queried script.
Moving the scaffolding split two comments from the functions they document. fetchAndFilterGroupUtxos lost its last line and ended up above BenchmarkNotificationProcessing, so it read as the benchmark's doc, and the benchmark's own doc was left stranded at the end of the file with nothing after it. Go accepts both, which is why nothing failed. Each comment is back above its own declaration. The helper's wording no longer leans on a colon or on describing itself as the previous behaviour.
The first pass missed it because the colon sat at the end of the line. Fixing it also needed the clause on the next line to start as a sentence.
arkana-ai-bot
left a comment
There was a problem hiding this comment.
Follow-up Review — head `93108ae`
Status: 2 cosmetic-only commits since `167701c`; 1 prior finding fixed; 2 standing non-blocking residuals unchanged; 0 new findings.
Prior-issue closure
| Finding | Status |
|---|---|
| 🔴 Unbounded cache growth | Fixed (in `923414a`). Not regressed. |
| 🟡 Positional struct init in `New()` | Fixed (in `923414a`). Not regressed. |
Log-spam on malformed frames → Warnf |
Fixed (in `7138bb9`). Not regressed. |
| Misplaced/truncated doc comments after scaffold move (flagged in `167701c` review) | Fixed in `56f37e3`. `BenchmarkNotificationProcessing` now carries its correct full doc comment above it; `fetchAndFilterGroupUtxos` has its own proper comment directly above the function. Both were detached by the previous reorganisation commit; they are correctly reattached. |
keypath_cache.go comment colon → sentence break (same `167701c` review) |
Fixed in `93108ae`. "always safe:\n// a miss simply falls back" → "always safe.\n// A miss simply falls back". No logic impact. |
`Confirmations int → uint32` reorg-edge residual (pkg/arkd-wallet/core/infrastructure/nbxplorer/types.go) |
Still open (non-blocking, no change). A future NBXplorer version that emits `-1` for reorg-invalidated txs through this same struct would silently fail to unmarshal. No regression in this increment; awareness recorded. |
| No in-tree e2e test for `outputs`/`matchedOutput.*` JSON field-name shapes against a live NBXplorer instance | Still open (non-blocking, no change). Unit tests with mocked JSON remain the sole shape guard against future NBXplorer wire-format drift. |
Incremental diff
Both commits are documentation-only. No logic, no data flow, no interfaces changed.
- `56f37e3` (
pkg/arkd-wallet/core/infrastructure/nbxplorer/service_test.go) — restores two doc comments to their correct positions; see prior-issue row above. - `93108ae` (
pkg/arkd-wallet/core/application/wallet/keypath_cache.go) — punctuation fix; one colon → period, next sentence capitalised.
No new findings.
⚠️ Standing requirement — human review still required
This PR modifies `getPrivateKeyFromScript` (signing path). Arkana's approval covers the caching and event-parsing mechanics. A human must also approve before merge, confirming:
- The `script → (derivationScheme, keyPath)` mapping is immutable for all address types the wallet manages.
- The empty-`KeyPath` guard correctly skips schemes that do not own the script, and the "not a wallet script" error surfaces correctly at the `SignTransaction` level.
Summary
Two efficiency fixes to how
arkd-wallettalks to NBXplorer. Both eliminate HTTPcalls for data NBXplorer already hands us in payloads we were discarding.
1. Build deposit UTXOs from the
newtransactionevent payloadThe websocket notification handler previously called
searchNewUTXOson everynewtransactionevent, which didGET /v1/cryptos/BTC/groups/{groupID}/utxos(the entire group UTXO set) and filtered it client-side by txid. That work scales
linearly with the number of watched scripts and fires constantly.
NBXplorer 2.6.7's
newtransactionevent already contains the matched outputs. Wenow parse them directly via a pure
utxosFromTransactionEvent(message, groupID),scoped to
trackedSource == "GROUP:{groupID}"(the identifier NBXplorer reportsfor addresses added through the group addresses endpoint). No extra HTTP call, and
the cost no longer depends on group size.
event.Datais nowjson.RawMessagetodrop a redundant re-marshal;
ports.UtxogainedKeyPath, carried bycastUtxo.2. Cache
script -> keyPathto skip per-input lookups when signinggetPrivateKeyFromScriptused up to twoGetScriptPubKeyDetailsHTTP calls perinput. The
script -> {scheme, keyPath}mapping is immutable, and UTXO listingsalready return
keyPath(we were dropping it). A bounded LRU cache(50k entries) is populated from the wallet's account
GetUtxoscall sites andconsulted first; the HTTP path remains as a fallback that backfills the cache. A
cache miss is always safe (it just falls back), so eviction has zero correctness
risk and memory stays flat on a long-running server. Warm-cache signing makes zero
metadata calls.
Benchmarks
BenchmarkNotificationProcessing— new event-parse is constant regardless ofgroup size; the old fetch-and-filter scales linearly:
(allocations at 10k: 30,141 -> 27; bytes/op: 13.9 MB -> 1.6 KB.) #2 is covered by
call-count tests: cache hit = 0 lookups, connector fallback = 2.
Testing
go test -race ./core/...— all subtests pass.empty outputs, non-zero confirmations, malformed JSON, invalid txid, keyPath
passthrough.
oldest-first), 100-goroutine concurrency;
getPrivateKeyFromScriptcache-hit/fallback/empty-keyPath behavior.
golangci-lint(v2.9.0): no new issues vsmaster.Review feedback addressed
asserting the bound and LRU eviction. (Note: the cache is populated only from
the wallet's account UTXO listings, not from event notifications.)
walletis now constructed with named fields.Notes
schemes are ignored exactly as before. Malformed events are now logged (benign).
(the two accounts are distinct taproot xpubs), so a stale/wrong entry is not
possible.
/events?lastEventId, single-RPC block-time, the/balanceendpoint, andreserve=truefor change addresses.Summary by CodeRabbit
Improvements
Tests