Skip to content

Commit

Permalink
wallet - deterministic secret derivation
Browse files Browse the repository at this point in the history
  • Loading branch information
elnosh committed Jun 13, 2024
1 parent 79f33f0 commit f4cf566
Show file tree
Hide file tree
Showing 10 changed files with 511 additions and 83 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
cmd/nutw/nutw
**/.env
*.txt
83 changes: 83 additions & 0 deletions cashu/nuts/nut13/nut13.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package nut13

import (
"encoding/binary"
"encoding/hex"

"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)

func DeriveKeysetPath(master *hdkeychain.ExtendedKey, keysetId string) (*hdkeychain.ExtendedKey, error) {
keysetBytes, err := hex.DecodeString(keysetId)
if err != nil {
return nil, err
}
bigEndianBytes := binary.BigEndian.Uint64(keysetBytes)
keysetIdInt := bigEndianBytes % (1<<31 - 1)

// m/129372
purpose, err := master.Derive(hdkeychain.HardenedKeyStart + 129372)
if err != nil {
return nil, err
}

// m/129372'/0'
coinType, err := purpose.Derive(hdkeychain.HardenedKeyStart + 0)
if err != nil {
return nil, err
}

// m/129372'/0'/keyset_k_int'
keysetPath, err := coinType.Derive(hdkeychain.HardenedKeyStart + uint32(keysetIdInt))
if err != nil {
return nil, err
}

return keysetPath, nil
}

func DeriveBlindingFactor(keysetPath *hdkeychain.ExtendedKey, counter uint32) (*secp256k1.PrivateKey, error) {
// m/129372'/0'/keyset_k_int'/counter'
counterPath, err := keysetPath.Derive(hdkeychain.HardenedKeyStart + counter)
if err != nil {
return nil, err
}

// m/129372'/0'/keyset_k_int'/counter'/1
rDerivationPath, err := counterPath.Derive(1)
if err != nil {
return nil, err
}

rkey, err := rDerivationPath.ECPrivKey()
if err != nil {
return nil, err
}

return rkey, nil
}

func DeriveSecret(keysetPath *hdkeychain.ExtendedKey, counter uint32) (string, error) {
// m/129372'/0'/keyset_k_int'/counter'
counterPath, err := keysetPath.Derive(hdkeychain.HardenedKeyStart + counter)
if err != nil {
return "", err
}

// m/129372'/0'/keyset_k_int'/counter'/0
secretDerivationPath, err := counterPath.Derive(0)
if err != nil {
return "", err
}

secretKey, err := secretDerivationPath.ECPrivKey()
if err != nil {
return "", err
}

secretBytes := secretKey.Serialize()
secret := hex.EncodeToString(secretBytes)

return secret, nil
}
74 changes: 74 additions & 0 deletions cashu/nuts/nut13/nut13_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package nut13

import (
"encoding/hex"
"testing"

"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/tyler-smith/go-bip39"
)

func TestSecretDerivation(t *testing.T) {
mnemonic := "half depart obvious quality work element tank gorilla view sugar picture humble"
keysetId := "009a1f293253e41e"

seed := bip39.NewSeed(mnemonic, "")
master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
if err != nil {
t.Fatal(err)
}

keysetPath, err := DeriveKeysetPath(master, keysetId)
if err != nil {
t.Fatalf("could not derive keyset path: %v", err)
}

secrets := make([]string, 5)
rs := make([]string, 5)

var i uint32 = 0
for ; i < 5; i++ {
secret, err := DeriveSecret(keysetPath, i)
if err != nil {
t.Fatalf("error deriving secret: %v", err)
}
secrets[i] = secret

rkey, err := DeriveBlindingFactor(keysetPath, i)
if err != nil {
t.Fatalf("error deriving r: %v", err)
}

rbytes := rkey.Serialize()
r := hex.EncodeToString(rbytes)
rs[i] = r
}

expectedSecrets := []string{
"485875df74771877439ac06339e284c3acfcd9be7abf3bc20b516faeadfe77ae",
"8f2b39e8e594a4056eb1e6dbb4b0c38ef13b1b2c751f64f810ec04ee35b77270",
"bc628c79accd2364fd31511216a0fab62afd4a18ff77a20deded7b858c9860c8",
"59284fd1650ea9fa17db2b3acf59ecd0f2d52ec3261dd4152785813ff27a33bf",
"576c23393a8b31cc8da6688d9c9a96394ec74b40fdaf1f693a6bb84284334ea0",
}

expectedRs := []string{
"ad00d431add9c673e843d4c2bf9a778a5f402b985b8da2d5550bf39cda41d679",
"967d5232515e10b81ff226ecf5a9e2e2aff92d66ebc3edf0987eb56357fd6248",
"b20f47bb6ae083659f3aa986bfa0435c55c6d93f687d51a01f26862d9b9a4899",
"fb5fca398eb0b1deb955a2988b5ac77d32956155f1c002a373535211a2dfdc29",
"5f09bfbfe27c439a597719321e061e2e40aad4a36768bb2bcc3de547c9644bf9",
}

for i := 0; i < 5; i++ {
if expectedSecrets[i] != secrets[i] {
t.Fatalf("secret at index: %v does not match. Expected '%v' but got '%v'", i, expectedSecrets[i], secrets[i])
}

if expectedRs[i] != rs[i] {
t.Fatalf("r at index: %v does not match. Expected '%v' but got '%v'", i, expectedRs[i], rs[i])
}
}

}
60 changes: 56 additions & 4 deletions crypto/keyset.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import (
const maxOrder = 64

type Keyset struct {
Id string
//MintURL string
Id string
Unit string
Active bool
Keys map[uint64]KeyPair
Expand All @@ -36,7 +35,7 @@ type WalletKeyset struct {
Unit string
Active bool
PublicKeys map[uint64]*secp256k1.PublicKey
Counter uint64
Counter uint32
}

func GenerateKeyset(seed, derivationPath string) *Keyset {
Expand Down Expand Up @@ -130,7 +129,6 @@ func (ks *Keyset) UnmarshalJSON(data []byte) error {
}

ks.Id = temp.Id
//ks.MintURL = temp.MintURL
ks.Unit = temp.Unit
ks.Active = temp.Active

Expand Down Expand Up @@ -181,3 +179,57 @@ func (kp *KeyPair) UnmarshalJSON(data []byte) error {

return nil
}

type WalletKeysetTemp struct {
Id string
MintURL string
Unit string
Active bool
PublicKeys map[uint64][]byte
Counter uint32
}

func (wk *WalletKeyset) MarshalJSON() ([]byte, error) {
temp := &WalletKeysetTemp{
Id: wk.Id,
MintURL: wk.MintURL,
Unit: wk.Unit,
Active: wk.Active,
PublicKeys: func() map[uint64][]byte {
m := make(map[uint64][]byte)
for k, v := range wk.PublicKeys {
m[k] = v.SerializeCompressed()
}
return m
}(),
Counter: wk.Counter,
}

return json.Marshal(temp)
}

func (wk *WalletKeyset) UnmarshalJSON(data []byte) error {
temp := &WalletKeysetTemp{}

if err := json.Unmarshal(data, &temp); err != nil {
return err
}

wk.Id = temp.Id
wk.MintURL = temp.MintURL
wk.Unit = temp.Unit
wk.Active = temp.Active
wk.Counter = temp.Counter

wk.PublicKeys = make(map[uint64]*secp256k1.PublicKey)
for k, v := range temp.PublicKeys {
kp, err := secp256k1.ParsePubKey(v)
if err != nil {
return err
}

wk.PublicKeys[k] = kp
}

return nil
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
github.com/tyler-smith/go-bip39 v1.1.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
Expand Down
Loading

0 comments on commit f4cf566

Please sign in to comment.