Skip to content

feat(key-wallet): gate-free provider-key derivation API (operator/platform-node at index)#881

Merged
QuantumExplorer merged 2 commits into
devfrom
claude/rust-dashcore-880-806be4
Jul 14, 2026
Merged

feat(key-wallet): gate-free provider-key derivation API (operator/platform-node at index)#881
QuantumExplorer merged 2 commits into
devfrom
claude/rust-dashcore-880-806be4

Conversation

@QuantumExplorer

Copy link
Copy Markdown
Member

Closes #880.

Problem

key-wallet's provider-key convenience wrappers are gated on is_watch_only in opposite directions:

  • BLSAccount::derive_bls_key_at_path / derive_bls_key_at_index (public derivation) require is_watch_only == true and fail on resident accounts.
  • derive_from_seed_*_at route through derive_xpriv_from_master_xpriv, which requires is_watch_only == false and fails on restored/external-signable (watch-only) accounts.

So no single upstream API served both wallet states, forcing downstream consumers (dashpay/platform#4120, Kotlin SDK) to re-compose derivation from low-level primitives — the duplication that caused the stale-derivation bug behind #878 / #879.

Changes

Gate-free, wallet-state-agnostic entry points, per the API proposed in the issue:

  • BLSAccount::operator_public_key_at(&self, index) — legacy-scheme (fLegacy = true) non-hardened public derivation off the stored account xpub. Works on resident and watch-only accounts; no is_watch_only gate. Returns ExtendedBLSPubKey so both serializations are obtainable (to_bytes_legacy() for the legacy Dash format, to_bytes() for the modern/IETF format used in v19+ ProRegTx payloads). Rejects non-ProviderOperatorKeys accounts so a mixed-up account lookup fails loudly instead of yielding valid-looking wrong keys.
  • BLSAccount::operator_private_key_at(seed, network, index) — associated fn; re-derives the full DIP-3 operator path (m/9'/5'/3'/3' mainnet, m/9'/1'/3'/3' otherwise) from the raw BIP39 seed in the legacy BLS scheme, then the non-hardened child. Needs no account state at all.
  • EdDSAAccount::platform_node_key_at(seed, network, index) — associated fn; SLIP-0010 hardened derivation of the platform node key (m/9'/5'/3'/4'/i') from the raw seed.

This absorbs the protocol knowledge downstream currently re-encodes (legacy-scheme choice for BLS HD, hardened-leaf convention for Ed25519, account-path re-application from the raw seed) into key-wallet, next to the derivation itself. Purely additive — no existing API changed.

Tests

  • operator_key_at_is_wallet_state_agnostic — pins operator_public_key_at / operator_private_key_at to the existing dashbls golden vectors (mainnet + testnet) and asserts identical results on the resident account and its to_watch_only() counterpart, plus that the old gated wrapper still fails on the resident account (documenting the asymmetry this fixes).
  • operator_public_key_at_rejects_non_operator_accounts — the account-type guard.
  • platform_node_key_at_is_wallet_state_agnostic — pins to the SLIP-0010 golden vector and cross-checks against the account-based seed path.

cargo test -p key-wallet --all-features, cargo fmt --check, and cargo clippy --all-features --all-targets -- -D warnings all pass (one pre-existing timing-threshold flake in performance_tests under parallel load; passes in isolation).

🤖 Generated with Claude Code

Add wallet-state-agnostic entry points for deriving provider keys at an
index, with no is_watch_only gate in either direction:

- BLSAccount::operator_public_key_at(index): legacy-scheme non-hardened
  public derivation off the stored account xpub; works on resident AND
  watch-only accounts. Returns the extended key so both the legacy and
  modern/IETF serializations are obtainable. Rejects non-operator
  accounts so a mixed-up lookup fails loudly.
- BLSAccount::operator_private_key_at(seed, network, index): re-derives
  the full DIP-3 operator path from the raw BIP39 seed in the legacy BLS
  scheme, then the non-hardened child; needs no account state.
- EdDSAAccount::platform_node_key_at(seed, network, index): SLIP-0010
  hardened derivation of the platform node key from the raw seed.

The existing convenience wrappers are gated on is_watch_only in opposite
directions (public derivation requires watch-only, seed derivation
requires non-watch-only), so no single upstream API served both resident
and restored/external-signable wallets — forcing downstream consumers to
re-compose low-level primitives (see dashpay/platform#4120 and the stale
derivation bug behind #878/#879).

New tests pin the entry points to the existing dashbls / SLIP-0010
golden vectors and assert they behave identically on resident and
watch-only accounts.

Closes #880

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

Next review available in: 28 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6a572213-c9d9-40ad-afda-60204a3ddc9c

📥 Commits

Reviewing files that changed from the base of the PR and between 56a4078 and 3bab652.

📒 Files selected for processing (4)
  • .github/workflows/sanitizer.yml
  • key-wallet/src/account/bls_account.rs
  • key-wallet/src/account/eddsa_account.rs
  • key-wallet/src/tests/provider_key_derivation_tests.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/rust-dashcore-880-806be4

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.

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.22%. Comparing base (56a4078) to head (3bab652).

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #881      +/-   ##
==========================================
- Coverage   74.25%   74.22%   -0.03%     
==========================================
  Files         326      326              
  Lines       74553    74585      +32     
==========================================
+ Hits        55357    55360       +3     
- Misses      19196    19225      +29     
Flag Coverage Δ
core 77.09% <ø> (ø)
ffi 50.31% <ø> (ø)
rpc 20.00% <ø> (ø)
spv 90.88% <ø> (-0.23%) ⬇️
wallet 74.29% <100.00%> (+0.08%) ⬆️
Files with missing lines Coverage Δ
key-wallet/src/account/bls_account.rs 71.13% <100.00%> (+4.78%) ⬆️
key-wallet/src/account/eddsa_account.rs 71.58% <100.00%> (+0.89%) ⬆️

... and 7 files with indirect coverage changes

…htly)

The 2026-07-14 nightly (rustc daf2e5e18) ICEs while expanding the
libtest harness for dash-spv-ffi tests ("attribute is missing tokens:
rustc_test_entrypoint_marker" in rustc_ast/src/attr/mod.rs), breaking
the Address Sanitizer job for every PR. Pin ASAN/TSAN to the last
known-good nightly until a fixed nightly ships.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@QuantumExplorer

Copy link
Copy Markdown
Member Author

Reviewed

@QuantumExplorer QuantumExplorer merged commit bc6cbf9 into dev Jul 14, 2026
35 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/rust-dashcore-880-806be4 branch July 14, 2026 02:15
QuantumExplorer added a commit to dashpay/platform that referenced this pull request Jul 14, 2026
…er via rust-dashcore#881

Bumps the pin to bc6cbf96 picking up the gate-free provider-key API
(dashpay/rust-dashcore#881, from our #880): operator_public_key_at /
operator_private_key_at / platform_node_key_at. All derivation
composition in provider_key_at_index.rs is deleted — platform now
resolves wallets, calls upstream, and maps types. The private-reveal
keeps an always-on consistency check (secret's pubkey must equal the
xpub-derived one; the two upstream paths derive from independent
sources and don't verify each other). Provider key accounts are also
routed through the address-pool restore so their persisted rows
rehydrate the in-memory pools at load (used-flags and beyond-gap
indices survive restarts).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

key-wallet: gate-free provider-key derivation API (operator/platform-node at index) — watch-only gating asymmetry forces downstream duplication

1 participant