fix(key-wallet): derive BLS operator & Ed25519 platform-node keys per DashSync/dashbls#879
Conversation
… DashSync/dashbls Fixes three independent bugs that made masternode operator (BLS) and platform node (Ed25519) keys diverge from DashSync/dashwallet-ios for the same mnemonic (#878): 1. Hardened BLS child derivation prepended a spurious 0x00 to the HMAC input (secp256k1 BIP32 convention). dashbls uses sk(32) || i(4) || {0,1} with no leading zero, so every hardened level diverged. 2. Non-hardened BLS derivation fed the parent public key into the HMAC in modern/IETF serialization. dashbls serializes it with the legacy flag (fLegacy=true) throughout the HD chain. ExtendedBLSPrivKey/ ExtendedBLSPubKey now use legacy bytes in the HMAC input and expose public_key_bytes_legacy()/to_bytes_legacy(). 3. The default wallet flow derived a secp256k1 BIP32 key at the provider path, reused its secret bytes as the BLS/SLIP-0010 master seed, and applied the provider path again on top. BLSAccount::from_seed / EdDSAAccount::from_seed now take the wallet seed (e.g. the 64-byte BIP39 seed), feed it directly into the target-curve master and apply the account path once; add_bls_account/add_eddsa_account use the wallet's own seed. Seedless wallets (extended-priv-key based, watch- only, external-signable) skip auto-creation of these accounts and require an explicit seed. Also fixes BLSAccount::has_internal_and_external() to false — operator keys live in a single pool (account/i), so seed-based signing derivation (derive_from_seed_private_key_at) now works and yields m/9'/coin'/3'/3'/i. Derivation is pinned by test vectors generated with dashbls (dashpay/bls-signatures @ 0842b17, ExtendedPrivateKey with fLegacy=true) from the standard BIP39 test mnemonic, and SLIP-0010 vectors verified with an independent implementation. Keys previously persisted from these accounts change after this fix — the old ones never corresponded to anything on-chain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 23 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (11)
✨ 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 |
…default, legacy explicit) Mirror dashbls's fLegacy parameter instead of hard-coding legacy in the HD chain: derive_priv/derive_pub/derive_path use the modern (IETF) serialization like dashbls PrivateChild/PublicChild with the default fLegacy=false, and *_legacy / *_with_mode variants select the legacy Dash serialization. The provider-key account layer (BLSAccount, BLS address pools) derives with the legacy variants, which is what dashbls/DashSync use for masternode operator keys. Hardened derivation never serializes the public key, so both modes agree there; only non-hardened children differ. Modern-mode derivation is pinned by dashbls-generated vectors alongside the legacy ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reviewed |
…tform-node at index) (#881) * feat(key-wallet): gate-free provider-key derivation entry points (#880) 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> * ci: pin sanitizer jobs to nightly-2026-07-13 (rustc ICE on latest nightly) 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> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Fixes #878.
key-wallet derived wrong masternode operator BLS keys and Ed25519 platform node ids — they didn't match DashSync/dashwallet-ios (and therefore real on-chain ProRegTx registrations) for the same mnemonic. All three bugs from the issue are fixed together:
Bug 1 — spurious leading
0x00in hardened BLS child derivationExtendedBLSPrivKey::derive_privbuilt the hardened HMAC input as0x00 ‖ sk ‖ i ‖ {0,1}(the secp256k1 BIP32 convention). dashbls (ExtendedPrivateKey::PrivateChild) usessk ‖ i ‖ {0,1}with no leading zero. Since every level of the provider paths is hardened, derivation diverged from the first child. (This fix is mode-independent — dashbls never serializes the pubkey for hardened children.)Bug 2 — serialization mode of the HD chain
Non-hardened derivation feeds the parent public key into the HMAC, so the G1 serialization format is part of the derivation itself — dashbls parameterizes it as
fLegacy. key-wallet only implemented the modern/IETF mode, while DashSync derives operator keys withfLegacy = true. The API now mirrors dashbls and supports both modes:derive_priv/derive_pub/derive_path— modern (IETF) serialization, like dashblsPrivateChild(i)with the defaultfLegacy = falsederive_priv_legacy/derive_pub_legacy/derive_path_legacy— legacy Dash serialization (fLegacy = true)*_with_mode(…, SerializationFormat)— explicit modeThe provider-key account layer (
BLSAccount, BLS address pools) derives with the legacy variants, matching DashSync for masternode operator keys. Output serialization is likewise available in both formats:to_bytes()/public_key_bytes()(modern, unchanged — same bytes as v19+ ProRegTx payloads) and newto_bytes_legacy()/public_key_bytes_legacy()(DashSync'sKeyKind_BLSformat). Storage/serde formats are unchanged.Bug 3 — hybrid secp256k1→BLS/Ed25519 seed in the default wallet flow
The default flow derived a secp256k1 BIP32 key at the provider path, reused its 32 secret bytes as the BLS/SLIP-0010 master seed, then applied the provider path again. Now:
BLSAccount::from_seed/EdDSAAccount::from_seedtake the wallet seed (&[u8], e.g. the 64-byte BIP39 seed), feed it directly into the target-curve master (dashblsFromSeed/ SLIP-0010) and apply the account path once; the stored account xpub is the account-level key, so keyiis itsith child — exactly DashSync's structure.Wallet::add_bls_account/add_eddsa_accountwithNoneuse the wallet's own seed (newWallet::wallet_seed_bytes()); wallets without a seed (extended-priv-key based, watch-only, external-signable) get a typed error, and default account creation skips these accounts for seedless wallets instead of fabricating keys that never correspond to anything on-chain.BLSAccount::has_internal_and_external()corrected tofalse(operator keys live in a singleaccount/ipool), which also unblocks the seed-based signing pathderive_from_seed_private_key_atfor operator accounts (it previously always errored).Test vectors
Reference vectors were generated with dashbls itself (dashpay/bls-signatures @
0842b17, the C++ library DashSync uses via FFI) usingExtendedPrivateKey::FromSeed+PrivateChild(i, fLegacy=true/false)through its Rust bindings, from the standard BIP39 test mnemonic (abandon … about, empty passphrase):derivation_bls_bip32.rsfor both modes (master,m/9'/5'/3'/3'chain, legacy non-hardened children 0–2 in both output serializations, modern-mode children, chia-style short-seed vectors, cross-mode divergence check),tests/provider_key_derivation_tests.rs(mainnet + testnet operator keys via both the watch-side xpub path and the seed-based signing path; Ed25519 platform node keys pinned to SLIP-0010 vectors verified with an independent implementation),bls_account_derive_private_key_from_seed/eddsa_account_derive_private_key_from_seedreturn the reference keys.Breaking / migration notes
BLSAccount::from_seed,EdDSAAccount::from_seed,Wallet::add_bls_account,Wallet::add_eddsa_accountsignatures changed ([u8; 32]→&[u8]), and the seed parameter now means the wallet seed, not a raw curve seed.Testing
cargo test -p key-wallet --all-features— 564 passedcargo test -p key-wallet-ffi— 260 passedcargo test -p dash-spvwith dashd integration tests — all passedcargo test -p dash-spv-ffi --test dashd_sync— 7 passedcargo fmt --check,cargo clippy --all-features --all-targets— clean🤖 Generated with Claude Code