Skip to content

feat(key-wallet-manager): carry addresses_derived on TransactionDetected / BlockProcessed#725

Open
QuantumExplorer wants to merge 5 commits intov0.42-devfrom
claude/condescending-pasteur-c556e2
Open

feat(key-wallet-manager): carry addresses_derived on TransactionDetected / BlockProcessed#725
QuantumExplorer wants to merge 5 commits intov0.42-devfrom
claude/condescending-pasteur-c556e2

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

Summary

  • Surface the addresses derived by gap-limit maintenance on the existing TransactionDetected / BlockProcessed events (no new event variant), so consumers can persist new pool rows transactionally with the tx/block records that triggered them.
  • Without this, UTXOs landing on freshly derived addresses orphan their parent CoreAddress row in downstream persisters that mirror the pool to disk via the existing event seam.
  • Block emission de-duplicates by (account_type, pool_type, derivation_index) so two records pushing the same boundary collapse to one entry.

Shape

New DerivedAddress (in key-wallet-manager) carries account_type, pool_type (External / Internal / Absent / AbsentHardened), derivation_index, derivation_path, address, public_key: [u8;33]. Non-ECDSA pools (BLS / EdDSA) are silently skipped during projection — those paths don't currently trigger gap-limit extension on Core txs.

TransactionInstantLocked and SyncHeightAdvanced are unchanged — IS-lock application doesn't mark addresses used, and sync-height isn't bound to a tx.

Plumbing

  • AddressPool::maintain_gap_limit now returns Vec<AddressInfo> (was Vec<Address>).
  • wallet_checker.rs tags each freshly derived AddressInfo with the owning account type and pool type via a new DerivedAddressInfo in key-wallet.
  • key-wallet-manager projects DerivedAddressInfo into DerivedAddress and dedups before emitting.
  • key-wallet remains event-channel-free; everything bubbles up via return values.

Test plan

  • cargo test -p key-wallet -p key-wallet-manager
  • cargo clippy -p key-wallet -p key-wallet-manager --all-targets
  • cargo fmt --check -p key-wallet -p key-wallet-manager
  • cargo build --workspace --tests
  • New event_tests.rs cases:
    • mempool tx to highest External → addresses_derived of length gap_limit, all on the right (account, pool), contiguous indices
    • mempool tx already inside the buffer → addresses_derived empty
    • block where one tx hits highest External + another hits highest Internal → both pools extended, deduplicated
    • block where two records both push the External boundary → exactly gap_limit (not 2× gap_limit)
    • TransactionInstantLocked exhaustively pattern-matched without addresses_derived (compile-time guard against accidental drift)

Notes for reviewers

  • Branch was cut off v0.42-dev (the worktree's tracked base), not v0.42-platform-nightly as the spec mentioned — happy to rebase onto the platform branch if that's what the reviewer wants.
  • dash-spv-ffi callback dispatch acknowledges the new field with addresses_derived: _; no FFI surface change yet (downstream Swift adapter will add the fan-out).

🤖 Generated with Claude Code

…ted / BlockProcessed

Address-pool extensions triggered by gap-limit maintenance during tx /
block processing now ride along on the existing event the persister
already consumes, rather than living silently in Rust memory. UTXOs
landing on freshly derived addresses keep their parent CoreAddress row
in downstream stores.

`AddressPool::maintain_gap_limit` returns `Vec<AddressInfo>` (was
`Vec<Address>`); the wallet checker tags each new entry with the owning
account type and pool type via a new `DerivedAddressInfo`, and the
manager projects that into `DerivedAddress` (account_type, pool_type,
derivation_index, derivation_path, address, public_key) on the event.

Block emission de-duplicates by (account_type, pool_type, index) so two
records pushing the same boundary collapse to one entry. Non-ECDSA
pools (BLS / EdDSA) are silently skipped during projection. IS-lock and
sync-height events are unchanged — those paths don't extend the pool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Warning

Rate limit exceeded

@QuantumExplorer has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 17 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d1c02863-5b22-45d9-9336-a24425506387

📥 Commits

Reviewing files that changed from the base of the PR and between a9fb28f and 2ceea91.

📒 Files selected for processing (12)
  • dash-spv-ffi/src/bin/ffi_cli.rs
  • dash-spv-ffi/src/callbacks.rs
  • dash-spv-ffi/tests/dashd_sync/callbacks.rs
  • key-wallet-manager/src/event_tests.rs
  • key-wallet-manager/src/events.rs
  • key-wallet-manager/src/lib.rs
  • key-wallet-manager/src/process_block.rs
  • key-wallet/src/managed_account/address_pool.rs
  • key-wallet/src/managed_account/managed_platform_account.rs
  • key-wallet/src/transaction_checking/account_checker.rs
  • key-wallet/src/transaction_checking/mod.rs
  • key-wallet/src/transaction_checking/wallet_checker.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/condescending-pasteur-c556e2

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
Review rate limit: 0/1 reviews remaining, refill in 24 minutes and 17 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

Codecov Report

❌ Patch coverage is 91.59292% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.95%. Comparing base (a9fb28f) to head (2ceea91).
⚠️ Report is 2 commits behind head on v0.42-dev.

Files with missing lines Patch % Lines
key-wallet-manager/src/events.rs 92.64% 10 Missing ⚠️
dash-spv-ffi/src/bin/ffi_cli.rs 0.00% 4 Missing ⚠️
dash-spv-ffi/src/callbacks.rs 93.75% 3 Missing ⚠️
key-wallet/src/managed_account/address_pool.rs 87.50% 1 Missing ⚠️
...et/src/managed_account/managed_platform_account.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##           v0.42-dev     #725      +/-   ##
=============================================
+ Coverage      70.86%   70.95%   +0.08%     
=============================================
  Files            319      319              
  Lines          68183    68403     +220     
=============================================
+ Hits           48320    48535     +215     
- Misses         19863    19868       +5     
Flag Coverage Δ
core 75.81% <ø> (+<0.01%) ⬆️
ffi 45.73% <86.53%> (+0.23%) ⬆️
rpc 20.00% <ø> (ø)
spv 87.34% <ø> (+0.04%) ⬆️
wallet 69.84% <93.10%> (+0.23%) ⬆️
Files with missing lines Coverage Δ
key-wallet-manager/src/lib.rs 62.36% <100.00%> (+0.25%) ⬆️
key-wallet-manager/src/process_block.rs 92.56% <100.00%> (+0.31%) ⬆️
...wallet/src/transaction_checking/account_checker.rs 67.02% <ø> (ø)
...-wallet/src/transaction_checking/wallet_checker.rs 99.39% <100.00%> (+<0.01%) ⬆️
key-wallet/src/managed_account/address_pool.rs 66.93% <87.50%> (+0.91%) ⬆️
...et/src/managed_account/managed_platform_account.rs 64.28% <0.00%> (ø)
dash-spv-ffi/src/callbacks.rs 80.14% <93.75%> (+1.71%) ⬆️
dash-spv-ffi/src/bin/ffi_cli.rs 0.00% <0.00%> (ø)
key-wallet-manager/src/events.rs 71.28% <92.64%> (+49.24%) ⬆️

... and 4 files with indirect coverage changes

QuantumExplorer and others added 2 commits May 5, 2026 01:19
`AccountType` is now imported in account_checker.rs, which makes the
explicit `(crate::account::AccountType)` link target redundant under
`-D warnings`. And `[AddressInfo]` in key-wallet-manager/src/lib.rs
needs the fully-qualified path since the type isn't imported into the
crate root.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ivedAddress

`account_type` (which carries Dashpay identity ids, IdentityTopUp's
registration_index, and the like), `pool_type`, and `derivation_index`
fully determine the path. Consumers that need a rendered path can
recompute it deterministically rather than carrying a redundant
String on every event.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

Clean expansion of TransactionDetected/BlockProcessed with addresses_derived, plus solid plumbing through key-wallet's DerivedAddressInfo. No blocking issues: the FFI deferral and Dashpay xpub gap are pre-existing limitations explicitly out of scope. Concerns are about test fidelity (dedup branch isn't actually exercised; payload fields aren't pinned), silent drops in DerivedAddress::from_info that hide future regressions, and small fragility around maintain_gap_limit's new shape.

Reviewed commit: ee0adcd

🟡 5 suggestion(s) | 💬 3 nitpick(s)

1 additional finding

💬 nitpick: Dashpay Core accounts still skip gap-limit maintenance entirely

key-wallet/src/wallet/helper.rs (lines 863-866)

extended_public_key_for_account_type returns None for DashpayReceivingFunds / DashpayExternalAccount, and WalletTransactionChecker only runs maintain_gap_limit when an xpub is available (wallet_checker.rs:170-183). So although the standard tx router still routes Dashpay account types, no addresses_derived entries can ever be emitted for them. This is pre-existing behavior — not a regression from this PR — but it does mean the new persistence contract has a known blind spot for Dashpay single-pool ECDSA accounts. Worth either wiring the helper or documenting the gap so consumers don't assume the contract holds for every routed account type.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `key-wallet-manager/src/event_tests.rs`:
- [SUGGESTION] lines 910-956: Dedup test does not actually exercise the dedup branch
  `test_block_with_two_records_pushing_external_boundary_dedupes` sends two txs paying to the same `highest_addr` and asserts that the resulting `addresses_derived` is exactly `gap_limit` entries. But it cannot trigger the dedup branch in `project_derived_addresses`. Walking the wallet_checker flow (`wallet_checker.rs:166-199`):

- After tx1: `mark_address_used` sets `highest_used = highest_before`; `maintain_gap_limit` derives indices `highest_before+1 ..= highest_before+gap_limit` (so `target == highest_generated == highest_before + gap_limit`).
- After tx2: `mark_address_used` is a no-op for the same index; `maintain_gap_limit` recomputes `target = highest_used + gap_limit = highest_before + gap_limit`, the `while highest_generated < target` guard is false, and the loop runs zero times.

So `per_wallet_derived` ends up with exactly `gap_limit` entries from tx1, no overlapping `(account, pool, index)` keys are fed into `project_derived_addresses`, and the `seen.insert` early-continue arm is never hit. The `keys.dedup()` invariant in the assert passes vacuously. A regression that drops the dedup key (e.g. switching to `(account, pool)` only, or removing `seen.insert` entirely) would not fail this test.

A real dedup test would need to either unit-test `project_derived_addresses` directly with two overlapping `DerivedAddressInfo` entries, or construct a flow where two checker invocations within the same block actually produce the same `(account_type, pool_type, index)` triple.
- [SUGGESTION] lines 743-957: New tests don't pin the persistence-critical address/public_key payload
  The new event tests assert pool counts, pool types, and contiguous derivation indices, but never verify that each emitted `DerivedAddress` actually carries the expected `address` and matching 33-byte compressed `public_key`. Since the entire purpose of this field is so persisters can write `CoreAddress` rows transactionally, a regression in `DerivedAddress::from_info` (e.g. wrong key bytes copied, address built from the wrong derivation path) could corrupt the persisted data while the suite stays green. At least one test should pin `derived.address` and `derived.public_key` against the wallet's `AddressInfo` for the same `(account, pool, index)`. Coverage of an absent/single-pool special account would also surface failure modes that the External/Internal-only tests miss.

In `key-wallet-manager/src/events.rs`:
- [SUGGESTION] lines 58-67: DerivedAddress::from_info silently drops non-ECDSA / non-33-byte entries with no observability
  `from_info` returns `None` in three distinct cases — `public_key.is_none()`, `PublicKeyType::ECDSA(bytes)` with `bytes.len() != 33`, and BLS / EdDSA — and `project_derived_addresses` then silently discards them. Today the producer is `address_pool::generate_address_at_index`, which always stores 33-byte compressed keys via `dashcore::PublicKey::new(...).to_bytes()`, so the length-mismatch arm is unreachable from this code path. The BLS/EdDSA arm is reachable in principle if a future change wires gap-limit extension into a BLS/EdDSA pool from the Core-tx path — and at that point the freshly derived address will silently disappear from `TransactionDetected` / `BlockProcessed`, defeating the persistence contract this PR is built around (downstream persisters will orphan UTXOs landing on those addresses with no log to trace it from).

A one-line `tracing::warn!` (or `debug!` for the BLS/EdDSA case + `warn!` for the ECDSA length mismatch, which would indicate a bug) at the drop sites would make the failure observable. Including `account_type`, `pool_type`, and `index` in the log makes it actionable.
- [SUGGESTION] line 57: DerivedAddress::from_info should be pub(crate)
  `DerivedAddress::from_info` is `pub`, but its only argument type is `key_wallet::transaction_checking::DerivedAddressInfo` and the only in-tree caller is the private `project_derived_addresses` helper. Making it `pub` exposes a `key_wallet::transaction_checking` type as part of `key-wallet-manager`'s public API surface and turns future refactors of `DerivedAddressInfo` (or its projection rules) into avoidable source-compatibility hazards for external users. Restricting to `pub(crate)` keeps the compatibility boundary at `WalletEvent` / `DerivedAddress`, where it belongs.

In `dash-spv-ffi/src/callbacks.rs`:
- [SUGGESTION] lines 787-855: FFI dispatch drops addresses_derived; persistence guarantee not yet realized for FFI consumers
  `FFIWalletEventCallbacks::dispatch` matches both affected variants with `addresses_derived: _` (lines 787 and 855), and the existing `OnTransactionDetectedCallback` / `OnWalletBlockProcessedCallback` C function pointers have no slot for the new payload. The Rust side correctly avoids an ABI break (`WalletEvent` is not `repr(C)`, so growing the variant is invisible to C), but Swift / mobile consumers behind `dash-spv-ffi` cannot yet observe the derived-address metadata, so the orphaned-`CoreAddress`-row failure mode this PR is designed to prevent persists for FFI persisters until a paired callback (or new signature) lands.

Per the PR description this is an intentional deferral, which is fine — but a short tracking comment / TODO in the dispatch arms (or a follow-up issue link) would keep the deferral from being lost. No correctness or panic-safety issue at the boundary as written.

Comment thread key-wallet-manager/src/event_tests.rs
Comment thread key-wallet-manager/src/events.rs Outdated
Comment thread key-wallet-manager/src/events.rs Outdated
Comment thread key-wallet-manager/src/event_tests.rs
Comment thread key-wallet/src/managed_account/address_pool.rs
Comment thread key-wallet-manager/src/process_block.rs Outdated
Comment thread dash-spv-ffi/src/callbacks.rs Outdated
…al dedup test

Address review feedback on PR #725:

- `DerivedAddress::from_info` is now `pub(crate)` since the only caller
  is the in-crate `project_derived_addresses` helper. Keeps the
  compatibility boundary at `WalletEvent` / `DerivedAddress` and avoids
  re-exporting `key_wallet::transaction_checking::DerivedAddressInfo`
  through this crate's public API.
- Drops in `from_info` now log via `tracing` — `warn!` for the
  unreachable-by-construction "ECDSA but not 33 bytes" arm (which
  signals a bug if ever hit), `warn!` for "no public key", and `debug!`
  for the BLS / EdDSA fall-through (expected if a future change wires
  gap-limit extension into one of those pools, currently unreachable).
- Mempool path now attributes derivations per-record via partition on
  `record.account_type == DerivedAddressInfo.account_type`. A tx that
  pays into multiple accounts of the same wallet now ships each record
  with its own account's derivations — consistent with the field's
  documented "transactional with `record`" semantics. Doc tightened
  accordingly.
- `maintain_gap_limit` previously silently produced a shorter `Vec` if
  the just-inserted entry wasn't found; now it panics with an explicit
  invariant message, turning a regression that breaks the post-insert
  invariant into a loud failure rather than a silent infinite loop on
  the outer `while`.
- New unit tests for `project_derived_addresses` directly exercise the
  dedup branch (overlapping `(account, pool, index)` → first-wins),
  distinct indices, distinct pools, and the missing-pubkey drop. The
  existing block-level test couldn't reach the dedup branch since
  `maintain_gap_limit` is idempotent on a second call.
- The "highest external" event test now pins each emitted
  `(address, public_key)` against the wallet's own `AddressInfo` for
  the same `(account, pool, index)`. Catches regressions in
  `from_info` that would silently corrupt persister rows while keeping
  the count-and-index assertions green.
- `dash-spv-ffi` dispatch arms tagged with TODOs documenting that
  `addresses_derived` is dropped at the FFI seam pending a paired
  callback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 4, 2026
@github-actions github-actions Bot added the ready-for-review CodeRabbit has approved this PR label May 4, 2026
Closes the FFI-side TODOs left by PR #725: the Rust event already
carries `Vec<DerivedAddress>`, but the C callback signature had no slot
for it, so the field was dropped at the dispatch seam — defeating the
persistence contract for FFI consumers (Swift / mobile persisters
mirroring the address pool to disk).

- New `FFIDerivedAddress { account_type, pool_type, derivation_index,
  address, public_key[33] }` and `FFIDerivedAddressPoolType` (4-variant,
  mirrors `key_wallet::AddressPoolType` 1:1; kept distinct from the
  existing `FFIAddressPoolType` which collapses Absent / AbsentHardened).
- `OnTransactionDetectedCallback` and `OnWalletBlockProcessedCallback`
  signatures grow `addresses_derived: *const FFIDerivedAddress,
  addresses_derived_count: u32`. The C ABI is broken — every consumer
  has to update its callback signature; this is intentional, since the
  alternative (a paired auxiliary callback) would let consumers persist
  records without the matching address rows and reintroduce the
  orphan-`CoreAddress`-row failure mode the field is designed to
  prevent.
- Memory: `FFIDerivedAddress` owns the `address` C string (freed on
  drop via `CString::from_raw`) and the `FFIAccountType` (which has its
  own Drop for the optional Dashpay identity ids). The `Vec` lives on
  the Rust stack across the callback and is dropped after.
- `dash-spv-ffi/src/bin/ffi_cli.rs` and the dashd integration test
  callbacks updated for the new signatures (counts logged in the CLI;
  arrays are not retained in tests).
- cbindgen header regenerates correctly (verified via `cargo build`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot removed the ready-for-review CodeRabbit has approved this PR label May 4, 2026
Copy link
Copy Markdown
Member Author

@QuantumExplorer QuantumExplorer left a comment

Choose a reason for hiding this comment

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

Self Reviewed

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.

2 participants