How Hierarchical Deterministic (HD) Wallet Derivation Works with CKD in mpcium

mpcium implements BIP-32 HD wallet derivation through two complementary Child Key Derivation (CKD) systems—a pure-Go client-side utility in pkg/ckdutil for local public key generation, and an MPC-aware wrapper in pkg/mpc/ckd.go that securely derives child keys inside threshold signing sessions using tss-lib.

The mpcium library provides a complete Hierarchical Deterministic (HD) wallet implementation that follows the BIP-32 specification for Child Key Derivation (CKD). It supports both standalone client-side derivation for address generation and secure multi-party computation (MPC) integrated derivation, enabling non-hardened child key generation for Ed25519 and Secp256k1 curves while keeping master secrets distributed across threshold nodes.

The Dual CKD Architecture in mpcium

mpcium provides two distinct CKD implementations to serve different operational contexts:

  • Pure-Go Client-Side CKD (pkg/ckdutil/child_derivation.go): Implements non-hardened derivation locally for Ed25519 and Secp256k1 curves using standard HMAC-SHA-512 operations, allowing clients to generate child public addresses without network interaction.

  • MPC-Aware CKD (pkg/mpc/ckd.go): Wraps the BIP-32 logic from tss-lib to perform derivation inside active MPC sessions, enabling distributed key generation and signing while maintaining the secrecy of chain codes and private key shards.

Both implementations follow identical mathematical procedures to ensure derivations remain consistent across client and server contexts.

Client-Side HD Derivation with ckdutil

The pkg/ckdutil package provides standalone functions for non-hardened derivation, requiring only the master public key and chain code.

The BIP-32 Derivation Algorithm

The core logic in DeriveEd25519ChildCompressed and DeriveSecp256k1ChildCompressed implements the standard CKD algorithm:

  1. Input validation: Verifies the master key length (32 bytes for Ed25519, 33 bytes for Secp256k1), confirms the chain code is exactly 32 bytes, and ensures every derivation index is non-hardened (index < 0x80000000).

  2. Path iteration: For each index in the derivation path:

    • Serialize the compressed parent public key (33 bytes).
    • Append the 4-byte big-endian index to create HMAC data.
    • Compute H = HMAC-SHA512(parentChainCode, data).
    • Split H into IL (first 32 bytes) and IR (last 32 bytes).
    • Convert IL to scalar k, rejecting zero or overflow values (≥ curve order).
    • Calculate the delta point Δ = k·G via scalar multiplication.
    • Perform point addition: P_child = P_parent + Δ.
    • Set the new chain code to IR.
  3. Output compression: Return the final public point as a compressed byte slice.

Curve-Specific Implementations

For Ed25519 keys, use DeriveEd25519ChildCompressed:

import "github.com/fystack/mpcium/pkg/ckdutil"

childPub, err := ckdutil.DeriveEd25519ChildCompressed(
    masterPubKey,        // []byte - 32-byte Ed25519 public key
    chainCodeHex,        // string - hex-encoded 32-byte chain code
    []uint32{44, 501, 0, 0}, // BIP-44 path: m/44'/501'/0/0
)

For Secp256k1 keys, use DeriveSecp256k1ChildCompressed:

childPub, err := ckdutil.DeriveSecp256k1ChildCompressed(
    masterPubKey,        // []byte - 33-byte compressed secp256k1 key
    chainCodeHex,        // string - hex-encoded chain code
    []uint32{44, 0, 0, 0},
)

MPC-Aware CKD for Threshold Signing

When derivation must occur inside the MPC protocol to prevent chain code exposure, pkg/mpc/ckd.go provides the CKD struct.

Integration with tss-lib

The MPC-aware implementation wraps ckd.DeriveChildKeyFromHierarchy from tss-lib, creating a temporary parent extended key that holds:

  • The master public key as a *crypto.ECPoint
  • The derivation depth and parent fingerprint
  • The 32-byte chain code (validated via NewCKDFromHex)

Derivation Process Inside MPC Sessions

To derive a child key during an MPC signing session:

import (
    "github.com/fystack/mpcium/pkg/mpc"
    "github.com/bnb-chain/tss-lib/v2/tss"
)

ckd, err := mpc.NewCKDFromHex(chainCodeHex) // validates 32-byte length
if err != nil {
    return err
}

delta, childExtKey, err := ckd.Derive(
    walletID,                // unique wallet identifier
    masterPubECPoint,        // *crypto.ECPoint from tss-lib
    []uint32{44, 501, 0, 0}, // derivation path
    tss.Edwards(),           // or tss.Secp256k1()
)

The Derive method returns:

  • delta: The scalar value (IL) used for point addition
  • childExtKey: The extended key containing the derived public point and new chain code

The MPC node updates its internal BigXj values using the returned delta, synchronizing its state with the derived child key for subsequent signing operations.

Verifying Derivation Parity Between Client and MPC

The example in examples/hdwallet/eddsa/main.go demonstrates cryptographic verification that both derivation methods produce identical public keys:

// Client-side derivation
clientChild, _ := ckdutil.DeriveEd25519ChildCompressed(
    masterPubKey, chainCodeHex, path,
)

// MPC-side derivation
tssChild, _ := deriveChildPublicKeyEd25519ViaTSS(
    masterPubKey, chainCodeHex, path, walletID,
)

// Parity check
if !slices.Equal(clientChild, tssChild) {
    logger.Warn("Derived child pubkey mismatch")
}

This parity check confirms that the local CKD utility and MPC-aware implementation agree on the BIP-32 mathematics, ensuring address generation and signature verification remain consistent.

Complete End-to-End Implementation

A typical workflow combining master wallet creation and HD derivation follows this pattern:

// 1. Create master wallet via MPC
// Returns master public key via KeygenResultEvent and uses configured chain_code

// 2. Derive child address locally
childPub, _ := ckdutil.DeriveEd25519ChildCompressed(
    masterPubKey, 
    chainCodeHex, 
    []uint32{44, 501, accountIndex, 0},
)

// 3. Generate blockchain address (e.g., Solana)
address := base58.Encode(childPub)

// 4. Sign transaction using derived path
signMsg := &types.SignTxMessage{
    WalletID:       walletID,
    KeyType:        types.KeyTypeEd25519,
    DerivationPath: []uint32{44, 501, accountIndex, 0},
    Tx:             transactionHash,
}
mpcClient.SignTransaction(signMsg)
// The MPC node internally re-derives the same child key via pkg/mpc/ckd.go

Summary

  • mpcium implements standard BIP-32 HD wallet derivation supporting Ed25519 and Secp256k1 curves.
  • The ckdutil package (pkg/ckdutil/child_derivation.go) provides client-side non-hardened derivation using HMAC-SHA-512 and elliptic curve point addition.
  • The mpc package (pkg/mpc/ckd.go) wraps tss-lib to enable threshold-friendly CKD that keeps chain codes secret during distributed signing.
  • Both implementations use identical mathematical operations: serializing parent keys, computing HMAC-SHA-512, splitting into IL/IR scalars, and performing point addition (P_child = P_parent + k·G).
  • Parity verification ensures client-derived public keys match those generated inside MPC sessions, preventing address derivation errors.
  • The system exclusively supports non-hardened derivation (indices < 0x80000000), requiring the master public key but not the private key for child generation.

Frequently Asked Questions

What is the difference between hardened and non-hardened derivation in mpcium?

mpcium exclusively supports non-hardened derivation (also called public derivation), where child keys are derived from the parent public key and chain code using indices less than 0x80000000 (2^31). Non-hardened derivation allows child public keys to be computed without access to the parent private key, enabling clients to generate addresses independently. Hardened derivation, which requires the parent private key and uses indices ≥ 2^31, is not implemented in the current codebase to maintain the security properties of threshold signing, where private keys never exist in complete form.

How does mpcium secure the chain code during MPC operations?

The chain code is treated as a 32-byte secret supplied via configuration (chain_code key) and validated through NewCKDFromHex in pkg/mpc/ckd.go. During MPC sessions, the chain code remains within the threshold network, never exposed to individual clients or the network layer. The CKD.Derive method computes the HMAC-SHA-512 operations inside the secure MPC environment using tss-lib, ensuring the chain code participates in child key generation without leaving the distributed signing nodes.

Which elliptic curves does mpcium support for HD wallet derivation?

mpcium supports Ed25519 (Edwards-curve Digital Signature Algorithm) and Secp256k1 (used in Bitcoin and Ethereum) through dedicated functions in pkg/ckdutil/child_derivation.go. For Ed25519, use DeriveEd25519ChildCompressed with 32-byte public keys. For Secp256k1, use DeriveSecp256k1ChildCompressed with 33-byte compressed public keys. The MPC implementation accepts curve specifications via tss.Edwards() or tss.Secp256k1() when calling ckd.Derive.

How can I verify that client-side and MPC derivations produce matching keys?

The repository provides parity checking examples in examples/hdwallet/eddsa/main.go and examples/hdwallet/ecdsa/main.go. Derive the child public key locally using ckdutil.DeriveEd25519ChildCompressed (or the Secp256k1 equivalent), then derive the same path using the MPC-aware function deriveChildPublicKeyEd25519ViaTSS which internally calls mpc.CKD.Derive. Compare the resulting byte slices using slices.Equal; identical 32-byte or 33-byte outputs confirm both implementations follow the same BIP-32 mathematics.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →