Skip to content

Commit 7876bc5

Browse files
authored
Merge pull request #45 from bitcoin-sv/feature/shamir-key-split-combine
shamir key share split & combine
2 parents d1925a2 + aef3d35 commit 7876bc5

File tree

22 files changed

+1615
-19
lines changed

22 files changed

+1615
-19
lines changed

.vscode/extensions.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"recommendations": ["SonarSource.sonarlint-vscode"]
3+
}

.vscode/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"sonarlint.connectedMode.project": {
3+
"connectionId": "bitcoin-sv",
4+
"projectKey": "bitcoin-sv_go-sdk"
5+
}
6+
}

CHANGELOG.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,41 @@ All notable changes to this project will be documented in this file. The format
55
## Table of Contents
66

77
- [Unreleased](#unreleased)
8-
- [1.1.2 - 2024-09-02](#111---2024-09-02)
8+
- [1.1.3 - 2024-09-04](#113---2024-09-04)
9+
- [1.1.2 - 2024-09-02](#112---2024-09-02)
910
- [1.1.1 - 2024-08-28](#111---2024-08-28)
1011
- [1.1.0 - 2024-08-19](#110---2024-08-19)
1112
- [1.0.0 - 2024-06-06](#100---2024-06-06)
1213

14+
## [1.1.3] - 2024-09-04
15+
16+
- Add shamir key splitting
17+
- Added PublicKey.ToHash() - sha256 hash, then ripemd160 of the public key (matching ts implementation)`
18+
- Added new KeyShares and polynomial primitives, and polynomial operations to support key splitting
19+
- Tests for all new keyshare, private key, and polynomial operations
20+
- added recommended vscode plugin and extension settings for this repo in .vscode directory
21+
- handle base58 decode errors
22+
- additional tests for script/address.go
23+
24+
### Added
25+
- `PrivateKey.ToKeyShares`
26+
- `PrivateKey.ToPolynomial`
27+
- `PrivateKey.ToBackupShares`
28+
- `PrivateKeyFromKeyShares`
29+
- `PrivateKeyFromBackupShares`
30+
- `PublicKey.ToHash()`
31+
- New tests for the new `PrivateKey` methods
32+
- new primitive `keyshares`
33+
- `NewKeyShares` returns a new `KeyShares` struct
34+
- `NewKeySharesFromBackupFormat`
35+
- `KeyShares.ToBackupFormat`
36+
- `polonomial.go` and tests for core functionality used by `KeyShares` and `PrivateKey`
37+
- `util.Umod` in `util` package `big.go`
38+
- `util.NewRandomBigInt` in `util` package `big.go`
39+
40+
### Changed
41+
- `base58.Decode` now returns an error in the case of invalid characters
42+
1343
## [1.1.2] - 2024-09-02
1444
- Fix OP_BIN2NUM to copy bytes and prevent stack corruption & add corresponding test
1545

RELEASE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
1. `merge tagged branch to master branch`
44
2. `git tag v1.0.0`
55
3. `git push origin v1.0.0`
6-
4. `goreleaser release`
6+
4. `GITHUB_TOKEN=xxxxxxxx goreleaser release --clean`

compat/base58/base58.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package compat
66

77
import (
8+
"errors"
89
"math/big"
910
)
1011

@@ -14,15 +15,15 @@ var bigRadix = big.NewInt(58)
1415
var bigZero = big.NewInt(0)
1516

1617
// Decode decodes a modified base58 string to a byte slice.
17-
func Decode(b string) []byte {
18+
func Decode(b string) ([]byte, error) {
1819
answer := big.NewInt(0)
1920
j := big.NewInt(1)
2021

2122
scratch := new(big.Int)
2223
for i := len(b) - 1; i >= 0; i-- {
2324
tmp := b58[b[i]]
2425
if tmp == 255 {
25-
return []byte("")
26+
return []byte(""), errors.New("bad character in encoding")
2627
}
2728
scratch.SetInt64(int64(tmp))
2829
scratch.Mul(j, scratch)
@@ -42,7 +43,7 @@ func Decode(b string) []byte {
4243
val := make([]byte, flen)
4344
copy(val[numZeros:], tmpval)
4445

45-
return val
46+
return val, nil
4647
}
4748

4849
// Encode encodes a byte slice to a modified base58 string.

compat/bip32/extendedkey.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,10 @@ func NewMaster(seed []byte, net *chaincfg.Params) (*ExtendedKey, error) {
537537
func NewKeyFromString(key string) (*ExtendedKey, error) {
538538
// The base58-decoded extended key must consist of a serialized payload
539539
// plus an additional 4 bytes for the checksum.
540-
decoded := base58.Decode(key)
540+
decoded, err := base58.Decode(key)
541+
if err != nil {
542+
return nil, err
543+
}
541544
if len(decoded) != serializedKeyLen+4 {
542545
return nil, ErrInvalidKeyLen
543546
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main
2+
3+
import (
4+
"log"
5+
6+
ec "github.com/bitcoin-sv/go-sdk/primitives/ec"
7+
)
8+
9+
func main() {
10+
expectedWif := "KxPEP4DCP2a4g3YU5amfXjFH4kWmz8EHWrTugXocGWgWBbhGsX7a"
11+
12+
// Restore a key from 3 of 5 key shares
13+
shares := []string{
14+
"89Gtabj94hosNkJtAtSeJTBKrrZ2BpoVYr2Kmt5UFzjR.69DcY9ngWU7afbj1Na84BahFUMPb6qkBa1hmzDkDcp18.3.bbc45478",
15+
"CsA3JhDRqBb1z58FxoixZmdsLTvHuehfwZzPgqJVA3Yv.4PP6QQcmFxikiX38yYUCqE2LFmht2MjXkf4nRjMqYBgw.3.bbc45478",
16+
"BVk1tcvJEbhUfZagStg15rFRxQDeLzgSN15rWkGhNf19.CUB7p6zK3JPBkBriRRGdWj4y3Z3qCfsaCYutmMWKv1VJ.3.bbc45478",
17+
}
18+
19+
pk, _ := ec.PrivateKeyFromBackupShares(shares)
20+
21+
if pk.Wif() == expectedWif {
22+
log.Println("Private key:", pk.Wif())
23+
}
24+
25+
// Prints The Original Private key
26+
// KxPEP4DCP2a4g3YU5amfXjFH4kWmz8EHWrTugXocGWgWBbhGsX7a
27+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main
2+
3+
import (
4+
"encoding/hex"
5+
"log"
6+
7+
ec "github.com/bitcoin-sv/go-sdk/primitives/ec"
8+
)
9+
10+
func main() {
11+
pk, _ := ec.PrivateKeyFromWif("KxPEP4DCP2a4g3YU5amfXjFH4kWmz8EHWrTugXocGWgWBbhGsX7a")
12+
log.Println("Private key:", hex.EncodeToString(pk.PubKey().ToHash())[:8])
13+
totalShares := 5
14+
threshold := 3
15+
shares, _ := pk.ToBackupShares(threshold, totalShares)
16+
17+
for i, share := range shares {
18+
log.Printf("Share %d: %s", i+1, share)
19+
}
20+
21+
// Prints
22+
// Share 1: <share1-x>.<share1-y>.3.bbc45478
23+
// Share 2: <share1-x>.<share1-y>.3.bbc45478
24+
// Share 3: <share1-x>.<share1-y>.3.bbc45478
25+
// Share 4: <share1-x>.<share1-y>.3.bbc45478
26+
// Share 5: <share1-x>.<share1-y>.3.bbc45478
27+
}

primitives/ec/privatekey.go

Lines changed: 137 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111

1212
base58 "github.com/bitcoin-sv/go-sdk/compat/base58"
1313
crypto "github.com/bitcoin-sv/go-sdk/primitives/hash"
14+
keyshares "github.com/bitcoin-sv/go-sdk/primitives/keyshares"
15+
"github.com/bitcoin-sv/go-sdk/util"
1416
)
1517

1618
var (
@@ -41,7 +43,7 @@ const compressMagic byte = 0x01
4143
// package.
4244
type PrivateKey e.PrivateKey
4345

44-
// PrivateKeyFromBytes returns a private and public key for `curve' based on the
46+
// PrivateKeyFromBytes returns a private and public key based on the
4547
// private key passed as an argument as a byte slice.
4648
func PrivateKeyFromBytes(pk []byte) (*PrivateKey, *PublicKey) {
4749
x, y := S256().ScalarBaseMult(pk)
@@ -57,7 +59,6 @@ func PrivateKeyFromBytes(pk []byte) (*PrivateKey, *PublicKey) {
5759
}
5860

5961
// NewPrivateKey is a wrapper for ecdsa.GenerateKey that returns a PrivateKey
60-
// instead of the normal ecdsa.PrivateKey.
6162
func NewPrivateKey() (*PrivateKey, error) {
6263
key, err := e.GenerateKey(S256(), rand.Reader)
6364
if err != nil {
@@ -66,7 +67,7 @@ func NewPrivateKey() (*PrivateKey, error) {
6667
return (*PrivateKey)(key), nil
6768
}
6869

69-
// PrivateKey is an ecdsa.PrivateKey with additional functions to
70+
// PrivateKeyFromHex returns a private key from a hex string.
7071
func PrivateKeyFromHex(privKeyHex string) (*PrivateKey, error) {
7172
if len(privKeyHex) == 0 {
7273
return nil, errors.New("private key hex is empty")
@@ -79,9 +80,12 @@ func PrivateKeyFromHex(privKeyHex string) (*PrivateKey, error) {
7980
return privKey, nil
8081
}
8182

82-
// PrivateKey is an ecdsa.PrivateKey with additional functions to
83+
// PrivateKeyFromWif returns a private key from a WIF string.
8384
func PrivateKeyFromWif(wif string) (*PrivateKey, error) {
84-
decoded := base58.Decode(wif)
85+
decoded, err := base58.Decode(wif)
86+
if err != nil {
87+
return nil, err
88+
}
8589
decodedLen := len(decoded)
8690
var compress bool
8791

@@ -198,3 +202,131 @@ func (p *PrivateKey) DeriveChild(pub *PublicKey, invoiceNumber string) (*Private
198202
privKey, _ := PrivateKeyFromBytes(newPrivKey.Bytes())
199203
return privKey, nil
200204
}
205+
206+
func (p *PrivateKey) ToPolynomial(threshold int) (*keyshares.Polynomial, error) {
207+
// Check for invalid threshold
208+
if threshold < 2 {
209+
return nil, fmt.Errorf("threshold must be at least 2")
210+
}
211+
212+
curve := keyshares.NewCurve()
213+
points := make([]*keyshares.PointInFiniteField, 0)
214+
215+
// Set the first point to (0, key)
216+
points = append(points, keyshares.NewPointInFiniteField(big.NewInt(0), p.D))
217+
218+
// Generate random points for the rest of the polynomial
219+
for i := 1; i < threshold; i++ {
220+
x := util.Umod(util.NewRandomBigInt(32), curve.P)
221+
y := util.Umod(util.NewRandomBigInt(32), curve.P)
222+
223+
points = append(points, keyshares.NewPointInFiniteField(x, y))
224+
}
225+
return keyshares.NewPolynomial(points, threshold), nil
226+
}
227+
228+
/**
229+
* Splits the private key into shares using Shamir's Secret Sharing Scheme.
230+
*
231+
* @param threshold The minimum number of shares required to reconstruct the private key.
232+
* @param totalShares The total number of shares to generate.
233+
* @returns A KeyShares object containing the shares, threshold and integrity.
234+
*
235+
* @example
236+
* key, _ := NewPrivateKey()
237+
* shares, _ := key.ToKeyShares(2, 5)
238+
*/
239+
func (p *PrivateKey) ToKeyShares(threshold int, totalShares int) (keyShares *keyshares.KeyShares, error error) {
240+
if threshold < 2 {
241+
return nil, errors.New("threshold must be at least 2")
242+
}
243+
if totalShares < 2 {
244+
return nil, errors.New("totalShares must be at least 2")
245+
}
246+
if threshold > totalShares {
247+
return nil, errors.New("threshold should be less than or equal to totalShares")
248+
}
249+
250+
poly, err := p.ToPolynomial(threshold)
251+
if err != nil {
252+
return nil, err
253+
}
254+
255+
points := make([]*keyshares.PointInFiniteField, 0)
256+
for range totalShares {
257+
pk, err := NewPrivateKey()
258+
if err != nil {
259+
return nil, err
260+
}
261+
x := new(big.Int).Set(pk.D)
262+
y := new(big.Int).Set(poly.ValueAt(x))
263+
points = append(points, keyshares.NewPointInFiniteField(x, y))
264+
}
265+
266+
integrity := hex.EncodeToString(p.PubKey().ToHash())[:8]
267+
return keyshares.NewKeyShares(points, threshold, integrity), nil
268+
}
269+
270+
// PrivateKeyFromKeyShares combines shares to reconstruct the private key
271+
func PrivateKeyFromKeyShares(keyShares *keyshares.KeyShares) (*PrivateKey, error) {
272+
if keyShares.Threshold < 2 {
273+
return nil, errors.New("threshold should be at least 2")
274+
}
275+
276+
if len(keyShares.Points) < keyShares.Threshold {
277+
return nil, fmt.Errorf("at least %d shares are required to reconstruct the private key", keyShares.Threshold)
278+
}
279+
280+
// check to see if two points have the same x value
281+
for i := 0; i < keyShares.Threshold; i++ {
282+
for j := i + 1; j < keyShares.Threshold; j++ {
283+
if keyShares.Points[i].X.Cmp(keyShares.Points[j].X) == 0 {
284+
return nil, fmt.Errorf("duplicate share detected, each must be unique")
285+
}
286+
}
287+
}
288+
289+
poly := keyshares.NewPolynomial(keyShares.Points, keyShares.Threshold)
290+
polyBytes := poly.ValueAt(big.NewInt(0)).Bytes()
291+
privateKey, publicKey := PrivateKeyFromBytes(polyBytes)
292+
integrityHash := hex.EncodeToString(publicKey.ToHash())[:8]
293+
if keyShares.Integrity != integrityHash {
294+
return nil, fmt.Errorf("integrity hash mismatch %s != %s", keyShares.Integrity, integrityHash)
295+
}
296+
return privateKey, nil
297+
}
298+
299+
/**
300+
* @method ToBackupShares
301+
*
302+
* Creates a backup of the private key by splitting it into shares.
303+
*
304+
*
305+
* @param threshold The number of shares which will be required to reconstruct the private key.
306+
* @param shares The total number of shares to generate for distribution.
307+
* @returns
308+
*/
309+
func (p *PrivateKey) ToBackupShares(threshold int, shares int) ([]string, error) {
310+
keyShares, err := p.ToKeyShares(threshold, shares)
311+
if err != nil {
312+
return nil, err
313+
}
314+
return keyShares.ToBackupFormat()
315+
}
316+
317+
/**
318+
*
319+
* @method PrivateKeyFromBackupShares
320+
*
321+
* Creates a private key from backup shares.
322+
*
323+
* @param shares in backup format
324+
* @returns PrivateKey
325+
*/
326+
func PrivateKeyFromBackupShares(shares []string) (*PrivateKey, error) {
327+
keyShares, err := keyshares.NewKeySharesFromBackupFormat(shares)
328+
if err != nil {
329+
return nil, err
330+
}
331+
return PrivateKeyFromKeyShares(keyShares)
332+
}

0 commit comments

Comments
 (0)