Skip to content

Commit 7ac24c9

Browse files
committed
Add hd wallet examples for both ecdsa and eddsa
1 parent b33dde9 commit 7ac24c9

8 files changed

Lines changed: 1121 additions & 10 deletions

File tree

examples/hdwallet/ecdsa/main.go

Lines changed: 434 additions & 0 deletions
Large diffs are not rendered by default.

examples/hdwallet/eddsa/main.go

Lines changed: 417 additions & 0 deletions
Large diffs are not rendered by default.

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/bnb-chain/tss-lib/v2 v2.0.2
1414
github.com/btcsuite/btcd v0.24.2
1515
github.com/btcsuite/btcd/btcec/v2 v2.3.2
16+
github.com/btcsuite/btcutil v1.0.2
1617
github.com/decred/dcrd/dcrec/edwards/v2 v2.0.3
1718
github.com/dgraph-io/badger/v4 v4.7.0
1819
github.com/google/uuid v1.6.0
@@ -43,7 +44,6 @@ require (
4344
github.com/aws/aws-sdk-go-v2/service/sts v1.38.1 // indirect
4445
github.com/aws/smithy-go v1.23.0 // indirect
4546
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
46-
github.com/btcsuite/btcutil v1.0.2 // indirect
4747
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4848
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
4949
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
@@ -107,4 +107,4 @@ require (
107107

108108
replace github.com/agl/ed25519 => github.com/binance-chain/edwards25519 v0.0.0-20200305024217-f36fc4b53d43
109109

110-
replace github.com/bnb-chain/tss-lib/v2 => github.com/fystack/tss-lib/v2 v2.0.1
110+
replace github.com/bnb-chain/tss-lib/v2 => github.com/fystack/tss-lib/v2 v2.0.3

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
116116
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
117117
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
118118
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
119-
github.com/fystack/tss-lib/v2 v2.0.1 h1:xnC2+DYShoVWco1geliW0km9IvGD7T2FqFOeXM3/7K0=
120-
github.com/fystack/tss-lib/v2 v2.0.1/go.mod h1:s4LRfEqj89DhfNb+oraW0dURt5LtOHWXb9Gtkghn0L8=
119+
github.com/fystack/tss-lib/v2 v2.0.3 h1:A0HGL5GDPpKbNW+0ZXgv1Ri3+ks88AvxTS7OK40gnUY=
120+
github.com/fystack/tss-lib/v2 v2.0.3/go.mod h1:s4LRfEqj89DhfNb+oraW0dURt5LtOHWXb9Gtkghn0L8=
121121
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
122122
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
123123
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=

pkg/ckdutil/child_derivation.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package ckdutil
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha512"
6+
"encoding/binary"
7+
"encoding/hex"
8+
"fmt"
9+
"math/big"
10+
11+
"github.com/btcsuite/btcd/btcec/v2"
12+
"github.com/decred/dcrd/dcrec/edwards/v2"
13+
)
14+
15+
const (
16+
hardenedKeyStart = 0x80000000
17+
// Compressed pubkey: 1-byte prefix (02/03) + 32-byte X coordinate.
18+
pubKeyBytesLenCompressed = 33
19+
// BIP32 specifies child index serialized as 4-byte big-endian (ser32).
20+
childIndexBytes = 4
21+
pubKeyCompressedEven byte = 0x2
22+
pubKeyCompressedOdd byte = 0x3
23+
)
24+
25+
// DeriveEd25519ChildCompressed derives a non-hardened child public key on ed25519 and returns the 32-byte compressed key.
26+
func DeriveEd25519ChildCompressed(masterPubKey []byte, chainCodeHex string, path []uint32) ([]byte, error) {
27+
if len(masterPubKey) == 0 {
28+
return nil, fmt.Errorf("master public key is empty")
29+
}
30+
31+
pubKey, err := edwards.ParsePubKey(masterPubKey)
32+
if err != nil {
33+
return nil, fmt.Errorf("decode master pubkey: %w", err)
34+
}
35+
36+
return deriveEd25519ChildCompressed(pubKey, chainCodeHex, path)
37+
}
38+
39+
// DeriveSecp256k1ChildCompressed derives a non-hardened child public key on secp256k1 and returns the 33-byte compressed key.
40+
func DeriveSecp256k1ChildCompressed(masterPubKey []byte, chainCodeHex string, path []uint32) ([]byte, error) {
41+
if len(masterPubKey) != 33 {
42+
return nil, fmt.Errorf("invalid master pubkey length: %d", len(masterPubKey))
43+
}
44+
45+
curve := btcec.S256()
46+
pubKey, err := btcec.ParsePubKey(masterPubKey)
47+
if err != nil {
48+
return nil, fmt.Errorf("decode master pubkey: %w", err)
49+
}
50+
51+
chainCode, err := hex.DecodeString(chainCodeHex)
52+
if err != nil {
53+
return nil, fmt.Errorf("decode chain code: %w", err)
54+
}
55+
if len(chainCode) != 32 {
56+
return nil, fmt.Errorf("invalid chain code length: %d", len(chainCode))
57+
}
58+
59+
currentX := new(big.Int).Set(pubKey.X())
60+
currentY := new(big.Int).Set(pubKey.Y())
61+
currentChainCode := append([]byte(nil), chainCode...)
62+
63+
for _, index := range path {
64+
if index >= hardenedKeyStart {
65+
return nil, fmt.Errorf("hardened derivation not supported: %d", index)
66+
}
67+
68+
data := make([]byte, pubKeyBytesLenCompressed+childIndexBytes)
69+
copy(data, serializeCompressed(currentX, currentY))
70+
binary.BigEndian.PutUint32(data[pubKeyBytesLenCompressed:], index)
71+
72+
mac := hmac.New(sha512.New, currentChainCode)
73+
mac.Write(data)
74+
ilr := mac.Sum(nil)
75+
il := ilr[:32]
76+
ir := ilr[32:]
77+
78+
ilNum := new(big.Int).SetBytes(il)
79+
if ilNum.Sign() == 0 || ilNum.Cmp(curve.Params().N) >= 0 {
80+
return nil, fmt.Errorf("invalid IL for index %d", index)
81+
}
82+
83+
deltaX, deltaY := curve.ScalarBaseMult(ilNum.Bytes())
84+
childX, childY := curve.Add(currentX, currentY, deltaX, deltaY)
85+
if childX == nil || childY == nil || childX.Sign() == 0 || childY.Sign() == 0 {
86+
return nil, fmt.Errorf("invalid child point at index %d", index)
87+
}
88+
89+
currentX, currentY = childX, childY
90+
currentChainCode = ir
91+
}
92+
93+
return serializeCompressed(currentX, currentY), nil
94+
}
95+
96+
// --- shared helpers (non-hardened) ---
97+
98+
func deriveEd25519ChildCompressed(masterPub *edwards.PublicKey, chainCodeHex string, path []uint32) ([]byte, error) {
99+
if masterPub == nil || masterPub.X == nil || masterPub.Y == nil {
100+
return nil, fmt.Errorf("invalid master public key")
101+
}
102+
103+
chainCode, err := hex.DecodeString(chainCodeHex)
104+
if err != nil {
105+
return nil, fmt.Errorf("decode chain code: %w", err)
106+
}
107+
if len(chainCode) != 32 {
108+
return nil, fmt.Errorf("invalid chain code length: %d", len(chainCode))
109+
}
110+
111+
curve := edwards.Edwards()
112+
currentX := new(big.Int).Set(masterPub.X)
113+
currentY := new(big.Int).Set(masterPub.Y)
114+
currentChainCode := append([]byte(nil), chainCode...)
115+
116+
for _, index := range path {
117+
if index >= hardenedKeyStart {
118+
return nil, fmt.Errorf("hardened derivation not supported: %d", index)
119+
}
120+
121+
data := make([]byte, pubKeyBytesLenCompressed+childIndexBytes)
122+
copy(data, serializeCompressed(currentX, currentY))
123+
binary.BigEndian.PutUint32(data[pubKeyBytesLenCompressed:], index)
124+
125+
mac := hmac.New(sha512.New, currentChainCode)
126+
mac.Write(data)
127+
ilr := mac.Sum(nil)
128+
il := ilr[:32]
129+
ir := ilr[32:]
130+
131+
ilNum := new(big.Int).SetBytes(il)
132+
ilNum.Mod(ilNum, curve.Params().N)
133+
if ilNum.Sign() == 0 || ilNum.Cmp(curve.Params().N) >= 0 {
134+
return nil, fmt.Errorf("invalid IL for index %d", index)
135+
}
136+
137+
deltaX, deltaY := curve.ScalarBaseMult(ilNum.Bytes())
138+
childX, childY := curve.Add(currentX, currentY, deltaX, deltaY)
139+
if childX == nil || childY == nil || childX.Sign() == 0 || childY.Sign() == 0 {
140+
return nil, fmt.Errorf("invalid child point at index %d", index)
141+
}
142+
143+
currentX, currentY = childX, childY
144+
currentChainCode = ir
145+
}
146+
147+
childPub := edwards.PublicKey{
148+
Curve: curve,
149+
X: currentX,
150+
Y: currentY,
151+
}
152+
153+
return childPub.SerializeCompressed(), nil
154+
}
155+
156+
// serializeCompressed matches the node compression (33 bytes).
157+
func serializeCompressed(x, y *big.Int) []byte {
158+
b := make([]byte, 0, pubKeyBytesLenCompressed)
159+
format := pubKeyCompressedEven
160+
if isOdd(y) {
161+
format = pubKeyCompressedOdd
162+
}
163+
b = append(b, format)
164+
return paddedAppend(b, 32, x.Bytes())
165+
}
166+
167+
func isOdd(a *big.Int) bool {
168+
return a.Bit(0) == 1
169+
}
170+
171+
func paddedAppend(dst []byte, srcPaddedSize int, src []byte) []byte {
172+
return append(dst, paddedBytes(srcPaddedSize, src)...)
173+
}
174+
175+
func paddedBytes(size int, src []byte) []byte {
176+
offset := size - len(src)
177+
tmp := src
178+
if offset > 0 {
179+
tmp = make([]byte, size)
180+
copy(tmp[offset:], src)
181+
}
182+
return tmp
183+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package ckdutil
2+
3+
import (
4+
"encoding/hex"
5+
"testing"
6+
7+
tsscrypto "github.com/bnb-chain/tss-lib/v2/crypto"
8+
"github.com/bnb-chain/tss-lib/v2/tss"
9+
"github.com/btcsuite/btcd/btcec/v2"
10+
"github.com/decred/dcrd/dcrec/edwards/v2"
11+
"github.com/fystack/mpcium/pkg/mpc"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestEd25519StandaloneMatchesTSS(t *testing.T) {
16+
chainCode := make([]byte, 32)
17+
for i := range chainCode {
18+
chainCode[i] = byte(i + 1)
19+
}
20+
chainCodeHex := hex.EncodeToString(chainCode)
21+
22+
curve := edwards.Edwards()
23+
masterPub := edwards.PublicKey{
24+
Curve: curve,
25+
X: curve.Params().Gx,
26+
Y: curve.Params().Gy,
27+
}
28+
masterPubBytes := masterPub.SerializeCompressed()
29+
30+
masterPoint, err := tsscrypto.NewECPoint(curve, masterPub.X, masterPub.Y)
31+
require.NoError(t, err)
32+
33+
ckd, err := mpc.NewCKDFromHex(chainCodeHex)
34+
require.NoError(t, err)
35+
36+
for i := 0; i < 100; i++ {
37+
path := []uint32{44, 501, uint32(i), 0}
38+
39+
localChild, err := DeriveEd25519ChildCompressed(masterPubBytes, chainCodeHex, path)
40+
require.NoErrorf(t, err, "local derivation failed at index %d", i)
41+
42+
_, tssChild, err := ckd.Derive("wallet-ed25519-test", masterPoint, path, tss.Edwards())
43+
require.NoErrorf(t, err, "tss derivation failed at index %d", i)
44+
45+
tssPub := edwards.PublicKey{Curve: curve, X: tssChild.PublicKey.X(), Y: tssChild.PublicKey.Y()}
46+
require.Equalf(t, tssPub.SerializeCompressed(), localChild, "pubkey mismatch at index %d", i)
47+
}
48+
}
49+
50+
func TestSecp256k1StandaloneMatchesTSS(t *testing.T) {
51+
chainCode := make([]byte, 32)
52+
for i := range chainCode {
53+
chainCode[i] = byte(0xaa - i)
54+
}
55+
chainCodeHex := hex.EncodeToString(chainCode)
56+
57+
curve := btcec.S256()
58+
masterX, masterY := curve.Params().Gx, curve.Params().Gy
59+
masterPubBytes := serializeCompressed(masterX, masterY)
60+
61+
masterPoint, err := tsscrypto.NewECPoint(curve, masterX, masterY)
62+
require.NoError(t, err)
63+
64+
ckd, err := mpc.NewCKDFromHex(chainCodeHex)
65+
require.NoError(t, err)
66+
67+
for i := 0; i < 1000; i++ {
68+
path := []uint32{44, 60, 0, 0, uint32(i)}
69+
70+
localChild, err := DeriveSecp256k1ChildCompressed(masterPubBytes, chainCodeHex, path)
71+
require.NoErrorf(t, err, "local derivation failed at index %d", i)
72+
73+
_, tssChild, err := ckd.Derive("wallet-secp-test", masterPoint, path, tss.S256())
74+
require.NoErrorf(t, err, "tss derivation failed at index %d", i)
75+
76+
tssChildBytes := serializeCompressed(tssChild.PublicKey.X(), tssChild.PublicKey.Y())
77+
require.Equalf(t, tssChildBytes, localChild, "pubkey mismatch at index %d", i)
78+
}
79+
}

pkg/mpc/ecdsa_signing_session.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,14 @@ func (s *ecdsaSigningSession) Init(tx *big.Int) error {
137137
}
138138

139139
if len(s.derivationPath) > 0 {
140-
logger.Info("Deriving key from derivation path", "derivationPath", s.derivationPath)
141140
il, extendedChildPk, errorDerivation := s.ckd.Derive(s.walletID, data.ECDSAPub, s.derivationPath, tss.S256())
142141
if errorDerivation != nil {
143-
return errors.Wrap(errorDerivation, "Failed to derive key")
142+
return errors.Wrap(errorDerivation, fmt.Sprintf("Failed to derive key, derivationPath: %v", s.derivationPath))
144143
}
145144
keyDerivationDelta := il
146145
err = s.ckd.ECDSAUpdateSinglePublicKeyAndAdjustBigXj(keyDerivationDelta, &data, extendedChildPk.PublicKey, tss.S256())
147146
if err != nil {
148-
return errors.Wrap(err, "Failed to update public key")
147+
return errors.Wrap(err, fmt.Sprintf("Failed to update public key, derivationPath: %v", s.derivationPath))
149148
}
150149

151150
s.party = signing.NewLocalPartyWithKDD(tx, params, data, keyDerivationDelta, s.outCh, s.endCh, 0)

pkg/mpc/eddsa_signing_session.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,14 @@ func (s *eddsaSigningSession) Init(tx *big.Int) error {
126126
}
127127

128128
if len(s.derivationPath) > 0 {
129-
logger.Info("Deriving key from derivation path", "derivationPath", s.derivationPath)
130129
il, extendedChildPk, errorDerivation := s.ckd.Derive(s.walletID, data.EDDSAPub, s.derivationPath, tss.Edwards())
131130
if errorDerivation != nil {
132-
return errors.Wrap(errorDerivation, "Failed to derive key")
131+
return errors.Wrap(errorDerivation, fmt.Sprintf("Failed to derive key, derivationPath: %v", s.derivationPath))
133132
}
134133
keyDerivationDelta := il
135134
err = s.ckd.EDDSAUpdateSinglePublicKeyAndAdjustBigXj(keyDerivationDelta, &data, extendedChildPk.PublicKey, tss.Edwards())
136135
if err != nil {
137-
return errors.Wrap(err, "Failed to update public key")
136+
return errors.Wrap(err, fmt.Sprintf("Failed to update public key, derivationPath: %v", s.derivationPath))
138137
}
139138

140139
s.party = signing.NewLocalPartyWithKDD(tx, params, data, keyDerivationDelta, s.outCh, s.endCh, 0)

0 commit comments

Comments
 (0)