Skip to content

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

Description

@QuantumExplorer

Summary

key-wallet has no wallet-state-agnostic entry points for deriving provider keys (operator BLS, platform node Ed25519) at a given index. The existing convenience wrappers are gated on is_watch_only in opposite directions, so no single upstream API works for both resident and restored/watch-only wallets — forcing downstream consumers to re-compose derivation from low-level primitives, which reintroduces the exact duplication that caused dashpay/platform's stale-derivation bug (see #878 / #879 and dashpay/platform#4120).

The gating asymmetry (the concrete deficiency)

  • BLSAccount::derive_bls_key_at_path / derive_bls_key_at_index (public derivation) require is_watch_only == true — on a resident account they fail with InvalidParameter("Cannot derive keys from BLS account without private key access") (bls_account.rs:154-159).
  • derive_from_seed_extended_xpriv_at / derive_from_seed_private_key_at (seed derivation) route through derive_xpriv_from_master_xpriv, which requires is_watch_only == false — on a restored/external-signable account they fail with Error::WatchOnly (bls_account.rs:274, eddsa_account.rs:269).

A resident (Wallet::from_mnemonic) wallet's provider account is non-watch-only; a restored external-signable wallet's account is watch-only (BLSAccount::new / EdDSAAccount::new set is_watch_only: true). Consumers that must serve both wallet states (mobile wallets restore as external-signable; tests/tools build resident wallets) can use neither wrapper universally.

What downstream is forced to do today

dashpay/platform's rs-platform-wallet now composes the gate-free low-level primitives directly (after dashpay/platform#4120):

  • operator public: account.bls_public_key.derive_pub_legacy(child)
  • operator private: ExtendedBLSPrivKey::new_master(network, seed).derive_path_legacy(account_path).derive_priv_legacy(child)
  • platform node: ExtendedEd25519PrivKey::new_master(network, seed).derive_priv(account_path).derive_priv([hardened child])

This composition encodes protocol knowledge that belongs next to the derivation itself: the legacy-scheme choice for BLS HD, the hardened-leaf convention for Ed25519, and the account-path re-application from the raw BIP39 seed. Every consumer (platform, the Kotlin SDK, future bindings) has to rediscover and maintain the same sequence — and history shows what happens when it drifts: platform carried a stale pre-#879 duplicate that kept producing wrong keys after the upstream fix landed.

Proposed API

Gate-free, wallet-state-agnostic entry points on the account types (names indicative):

impl BLSAccount {
    /// Public-only derivation off the account xpub (legacy scheme).
    /// Works on watch-only AND resident accounts.
    pub fn operator_public_key_at(&self, index: u32) -> Result<PublicKey, Error>;

    /// Private derivation from the raw BIP39 seed (legacy scheme, full
    /// account path re-derived). Works regardless of watch-only status.
    pub fn operator_private_key_at(seed: &[u8], network: Network, index: u32) -> Result<SecretKey, Error>;
}

impl EdDSAAccount {
    /// SLIP-10 hardened derivation from the raw BIP39 seed.
    pub fn platform_node_key_at(seed: &[u8], network: Network, index: u32) -> Result<SigningKey, Error>;
}

(Or equivalent free functions / Wallet-level helpers — the key properties are: no is_watch_only gate, raw-seed input for private paths, xpub input for public paths, and both BLS serializations obtainable from the result.)

Golden vectors already exist in key-wallet/src/tests/provider_key_derivation_tests.rs — the new entry points should be pinned to them.

Impact

  • dashpay/platform can shrink rs-platform-wallet/src/wallet/provider_key_at_index.rs to a pure adapter (wallet lookup + FFI type mapping), eliminating the duplicated-derivation bug class permanently.
  • Kotlin/other bindings get a single audited entry point instead of re-composing primitives.

Related: #878 (derivation divergence), #879 (the fix), #876 (provider payload surfacing), dashpay/platform#4120 (downstream consumption + the composition this issue proposes to absorb).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions