Skip to content

feat: add DIP-14 256-bit child key derivation (Derive256) - #7511

Open
PastaPastaPasta wants to merge 1 commit into
dashpay:developfrom
PastaPastaPasta:dip14-derive256
Open

feat: add DIP-14 256-bit child key derivation (Derive256)#7511
PastaPastaPasta wants to merge 1 commit into
dashpay:developfrom
PastaPastaPasta:dip14-derive256

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

DIP-14 extends BIP32 child key derivation to 256-bit child indexes with an explicit hardened flag. It underpins the DIP-13 platform key hierarchy and DIP-15 friendship-derived address spaces, where child indexes are 256-bit identity hashes so that the derivation path between two identities cannot be ground out by a third party (a 31-bit index space would be brute-forceable by cycling identities).

Dash Core currently has no DIP-14 primitive. This PR adds it at the CKey/CPubKey level, extracted from the Platform GUI work in PastaPastaPasta#49 as the first wallet-side prerequisite.

What was done?

  • DIP14Hash in hash.{h,cpp}: the HMAC-SHA512 with a 256-bit big-endian (ser256) child index, alongside BIP32Hash.
  • CKey::Derive256: private child derivation with a 256-bit index and explicit hardened flag.
  • CPubKey::Derive256: the public (non-hardened only) counterpart. Non-hardened 256-bit public derivation matches private derivation, which is what lets a counterparty derive addresses from an exported xpub without seeing private keys.

Compatibility-mode semantics (worth calling out, since the DIP-14 pseudocode is loosely worded here): indexes below 2^32 are interpreted as raw BIP32 index space, per the DIP's Compatibility section ("if a child key's index is less than 2^32 … will match the derivation outlined in BIP32") and its published test vectors. Concretely, the hardened flag is folded into the high bit on the private side, and the public side rejects indexes with the high bit set exactly as BIP32 does. Vector 2 (m/9'/5'/15'/0'/…) only reproduces under this interpretation.

How Has This Been Tested?

New unit suite dip14_tests:

  • the four DIP-14 spec test vectors from dashpay/dips dip-0014.md, including the mixed 32-bit/256-bit hardened path;
  • BIP32 compatibility of the sub-2^32 fallback (private hardened/non-hardened and public, checked against Derive);
  • public/private derivation consistency over a 256-bit two-step path, and rejection of hardened indexes on the public side.

dip14_tests, key_tests, bip32_tests, and hash_tests pass locally (macOS, clang). lint-whitespace, lint-include-guards, lint-includes clean; the new Dash-specific test file is listed in test/util/data/non-backported.txt.

Breaking Changes

None. Additive API only; no existing derivation path changes behavior.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

🤖 Generated with Claude Code

DIP-14 extends BIP32 child key derivation to 256-bit child indexes with an explicit hardened flag, replacing the high-bit convention. It underpins the DIP-13 platform key hierarchy and DIP-15 friendship-derived address spaces, where child indexes are 256-bit identity hashes so that derivation paths between identities cannot be ground out by a third party.

Add DIP14Hash (the HMAC-SHA512 with ser256(i)), CKey::Derive256 and CPubKey::Derive256. Indexes below 2^32 are interpreted as raw BIP32 index space for compatibility, per the DIP-14 compatibility requirement and its published test vectors: the hardened flag is folded into the high bit on the private side, and the public side rejects indexes with the high bit set exactly as BIP32 does. Non-hardened 256-bit public derivation matches private derivation, which is what lets a contact derive addresses from an exported xpub without seeing private keys.

Covered by the four DIP-14 spec test vectors (dashpay/dips dip-0014.md) plus BIP32-compatibility and public/private consistency checks in the new dip14_tests suite.

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

thepastaclaw commented Aug 2, 2026

Copy link
Copy Markdown

✅ Final review complete — no blockers (commit 8a6299d)

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change adds DIP-14 hashing and 256-bit child-key derivation. CKey::Derive256 supports hardened and non-hardened derivation. CPubKey::Derive256 supports non-hardened derivation and rejects hardened 32-bit indexes. 32-bit indexes remain compatible with BIP32. New tests cover DIP-14 vectors, compatibility, private/public consistency, and rejection cases.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant CKey_or_CPubKey
  participant DIP14Hash
  participant ChildKey_or_ChildPubKey
  Caller->>CKey_or_CPubKey: request Derive256 with child index and chain code
  CKey_or_CPubKey->>DIP14Hash: hash derivation inputs for non-32-bit index
  DIP14Hash-->>CKey_or_CPubKey: return tweak and child chain code
  CKey_or_CPubKey->>ChildKey_or_ChildPubKey: apply tweak and store derived key
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: adding DIP-14 256-bit child key derivation through Derive256.
Description check ✅ Passed The description directly explains the DIP-14 derivation APIs, compatibility behavior, tests, and additive scope.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/pubkey.cpp (1)

261-287: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Duplicate tweak-add/serialize postscript between Derive and Derive256 in both CKey and CPubKey. In both classes, the new Derive256 method duplicates the existing Derive method's postscript logic verbatim, differing only in how the 64-byte hash output is computed (BIP32Hash vs DIP14Hash).

  • src/pubkey.cpp#L261-L287: extract the pubkey-parse/tweak-add/serialize sequence (currently duplicated from Derive at lines 247-257) into a shared private helper that both Derive and Derive256 call with the precomputed 64-byte hash output.
  • src/key.cpp#L310-L335: extract the chain-code-copy/key-set/tweak-add/clear-on-failure sequence (currently duplicated from Derive at lines 303-307) into a shared private helper that both Derive and Derive256 call with the precomputed 64-byte hash output.

Keeping these two implementations of the same crypto invariant in sync manually risks future divergence if one path is updated without the other.

As per coding guidelines, "Keep changes narrow; do not mix cleanup, formatting, refactoring, and behavior changes unless explicitly requested," so this consolidation is better suited to a follow-up change rather than this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pubkey.cpp` around lines 261 - 287, Defer this consolidation to a
follow-up change; make no modifications at src/pubkey.cpp lines 261-287 or
src/key.cpp lines 310-335 in this PR. The requested future refactor should
extract the shared postscript from CPubKey::Derive and Derive256 into a private
helper, and likewise extract the shared chain-code/key/tweak handling from
CKey::Derive and Derive256, while preserving behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/pubkey.cpp`:
- Around line 261-287: Defer this consolidation to a follow-up change; make no
modifications at src/pubkey.cpp lines 261-287 or src/key.cpp lines 310-335 in
this PR. The requested future refactor should extract the shared postscript from
CPubKey::Derive and Derive256 into a private helper, and likewise extract the
shared chain-code/key/tweak handling from CKey::Derive and Derive256, while
preserving behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4dd5455d-e1e4-45b4-a93f-f46f1d8e51b9

📥 Commits

Reviewing files that changed from the base of the PR and between 862ef3c and 8a6299d.

📒 Files selected for processing (9)
  • src/Makefile.test.include
  • src/hash.cpp
  • src/hash.h
  • src/key.cpp
  • src/key.h
  • src/pubkey.cpp
  • src/pubkey.h
  • src/test/dip14_tests.cpp
  • test/util/data/non-backported.txt

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Final validation — Codex + Sonnet

This PR adds a clean, additive DIP-14 256-bit child key derivation primitive (CKey::Derive256 / CPubKey::Derive256 / DIP14Hash) that correctly falls back to BIP32 semantics for sub-2^32 indexes and is verified against published test vectors. The implementation, build registration, and non-backported tracking are all correct; only two minor test-robustness/style suggestions were found, neither blocking.

Review provenance

  • Codex reviewers: gpt-5.6-sol — general (completed), gpt-5.6-sol — dash-core-commit-history (completed)
  • Verifier: claude-sonnet-5 — final-verifier
  • Sonnet reviewers: claude-sonnet-5 — general (completed), claude-sonnet-5 — dash-core-commit-history (failed), claude-sonnet-5 — dash-core-commit-history (completed)

🟡 1 suggestion(s) | 💬 1 nitpick(s)

1 additional finding(s) omitted (not in diff).

🤖 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 `src/test/fuzz/key.cpp`:
- [SUGGESTION] src/test/fuzz/key.cpp:88-97: New Derive256 primitive is not exercised by the existing key/pubkey fuzz harnesses
  src/test/fuzz/key.cpp already fuzzes CKey::Derive and CPubKey::Derive with randomized ChainCode/key material (lines 91-97 and around 240-244), which has historically caught edge cases in secp256k1 tweak-add failure paths and boundary values. Derive256 is new parsing/arithmetic surface — the 28-zero-byte BIP32-compatibility check, ReadBE32 boundary handling, and hardened-bit folding — that would benefit from the same fuzzing treatment, especially since Span<const unsigned char> callers could pass adversarial 32-byte indexes (all-zero, all-0xFF, boundary values around 2^32) not covered by the four fixed spec vectors in dip14_tests.cpp. This is confirmed: grep shows Derive256 is only exercised in src/test/dip14_tests.cpp, never in the fuzz targets. Not a defect in the current code, but a natural follow-up for parity with the existing harness.

Comment thread src/key.h
Comment on lines +160 to +163
//! Derive DIP-14 child key with a 256-bit index (32 bytes, big-endian).
//! Indexes below 2^32 fall back to BIP32 derivation for compatibility
//! (the hardened flag is then folded into the 32-bit index).
[[nodiscard]] bool Derive256(CKey& keyChild, ChainCode& ccChild, Span<const unsigned char> nChild, bool hardened, const ChainCode& cc) const;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💬 Nitpick: Span for a fixed-32-byte index relies on a runtime assert instead of compile-time size safety

Derive256 takes Span nChild and only validates its length with assert(nChild.size() == 32) at the top of the function body (src/key.cpp:311, src/pubkey.cpp:263). A std::array<unsigned char, 32> (or a dedicated 32-byte index type) would make the 32-byte contract enforced by the type system rather than a runtime assertion, removing any reliance on assert being compiled in for safety against a mis-sized Span reaching nChild.begin() + 28 / nChild.data() + 28. This is consistent with existing Span usage elsewhere in the file, so it's a minor style preference rather than a defect.

source: ['claude']

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