-
Notifications
You must be signed in to change notification settings - Fork 232
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement bip32 #1676
Implement bip32 #1676
Conversation
Codecov Report
@@ Coverage Diff @@
## v0.11.0-dev #1676 +/- ##
===============================================
+ Coverage 59.14% 59.29% +0.15%
===============================================
Files 556 565 +9
Lines 22035 22399 +364
===============================================
+ Hits 13032 13281 +249
- Misses 6930 7008 +78
- Partials 2073 2110 +37
Continue to review full report at Codecov.
|
@@ -0,0 +1,41 @@ | |||
package bip32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I don't think we want the word "bip32" in our code, this is good time to think about a more general name, that does not reference other coins.
- Sounds like this is code that is more general then libkaspawallet, I'd place it on the same level as the bech32 package (a.k.a. under "util")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- This is an implementation of bip32. I don't see any problem with this name.
- bip32 is related only to wallets, so I think this place is ok
func TestBIP32SpecVectors(t *testing.T) { | ||
type testPath struct { | ||
path string | ||
extPub string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use full words for names.
|
||
type testVector struct { | ||
seed string | ||
pathes []testPath |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: paths
|
||
import "github.com/pkg/errors" | ||
|
||
var BitcoinMainnetPrivate = [4]byte{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it needed? My guess is only to verify against bitcoin-provided test vectors.
As such, I'd rather if this was only in test code.
EDIT: Read some more of the code, now I understand this is not as trivial as I thought.
As such, I'd rather not have bitcoin vectors at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an implementation of the bip32 spec, so it makes sense to use the test vectors that are specified by the spec
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, but this should be in test code only
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but then how can we use toPublicVersion?
t.Fatalf("DecodeString: %+v", err) | ||
} | ||
|
||
masterKey, err := NewMaster(seed, BitcoinMainnetPrivate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a few test vectors with Kaspa master
return sha2.Sum(nil) | ||
} | ||
|
||
func validateChecksum(data []byte) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a comment that explains what's the expected structure of data (I'd also rename it to dataWithChecksum
)
|
||
func parsePath(pathString string) (*path, error) { | ||
parts := strings.Split(pathString, "/") | ||
if len(parts) == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can't a path be just "m" or "M"?
It is being used this way, for example, here: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#full-wallet-sharing-m
Not sure it's even going to happen in the context of this function, but as a general-purpose function I think just "m" should be supported
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you are right. I think this condition can never happen
} | ||
|
||
func parseIndex(indexString string) (uint32, error) { | ||
const isHardenedSuffix = "'" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really annoying that the spec uses the unprintable H while everyone in the world uses '.
As such, I'd make this suffix much more visible (either in some readme, or somewhere else where it's clear what this means).
checkSumLen | ||
|
||
func DeserializeExtendedPrivateKey(extKeyString string) (*ExtendedKey, error) { | ||
serializedBytes := base58.Decode(extKeyString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really want to encode this in base58?
And even worse - using btcutil library for doing that...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is part of the spec. I can copy the library though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we want to copy the library then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you missed copying the library.
} | ||
|
||
var KaspaMainnetPrivate = [4]byte{ | ||
0x01, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we want something human-recognizable here.
var iL, iR [32]byte | ||
copy(iL[:], I[:32]) | ||
copy(iR[:], I[32:]) | ||
|
||
privateKey, err := secp256k1.DeserializeECDSAPrivateKeyFromSlice(iL[:]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit,
var iL secp256k1.SerializedPrivateKey
copy(iL[:], I[:32])
privateKey, err := secp256k1.DeserializeECDSAPrivateKey(&iL)
is a little more efficient
publicKey *secp256k1.ECDSAPublicKey | ||
Version [4]byte | ||
Depth uint8 | ||
Fingerprint [4]byte |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think "ParentFingerprint" is more correct
return &kCopy, nil | ||
} | ||
|
||
func calcI(extKey *ExtendedKey, i uint32) ([]byte, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe make this a function on ExtendedKey
?
if isHardened(i) { | ||
if !extKey.IsPrivate() { | ||
return nil, errors.Errorf("Cannot calculate hardened child for public key") | ||
} | ||
|
||
mac := newHMACWriter(extKey.ChainCode[:]) | ||
mac.InfallibleWrite([]byte{0x00}) | ||
mac.InfallibleWrite(extKey.PrivateKey.Serialize()[:]) | ||
mac.InfallibleWrite(ser32(i)) | ||
return mac.Sum(nil), nil | ||
} | ||
|
||
mac := newHMACWriter(extKey.ChainCode[:]) | ||
publicKey, err := extKey.PublicKey() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
serializedPublicKey, err := publicKey.Serialize() | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error serializing public key") | ||
} | ||
|
||
mac.InfallibleWrite(serializedPublicKey[:]) | ||
mac.InfallibleWrite(ser32(i)) | ||
return mac.Sum(nil), nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, I find that writing it like this allows to easily see the difference between hardened and non-hardened (and reduces code duplication):
func calcI(extKey *ExtendedKey, i uint32) ([]byte, error) {
if isHardened(i) && !extKey.IsPrivate() {
return nil, errors.Errorf("Cannot calculate hardened child for public key")
}
mac := newHMACWriter(extKey.ChainCode[:])
if isHardened(i) {
mac.InfallibleWrite([]byte{0x00})
mac.InfallibleWrite(extKey.PrivateKey.Serialize()[:])
} else {
publicKey, err := extKey.PublicKey()
if err != nil {
return nil, err
}
serializedPublicKey, err := publicKey.Serialize()
if err != nil {
return nil, errors.Wrap(err, "error serializing public key")
}
mac.InfallibleWrite(serializedPublicKey[:])
}
mac.InfallibleWrite(ser32(i))
return mac.Sum(nil), nil
}
return fingerPrintFromPoint(point) | ||
} | ||
|
||
func fingerPrintFromPoint(point *secp256k1.ECDSAPublicKey) ([4]byte, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, maybe make Fingerprint()
a public function on ExtendedKey
? I think users will want it (especially as ExtendedKey
already exports the parent's fingerprint publicly)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why users may need it. For now I'll keep it private
return fingerprint, nil | ||
} | ||
|
||
func hash160(data []byte) []byte { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move this to hash.go
sha1 := sha256.New() | ||
sha2 := sha256.New() | ||
sha1.Write(data) | ||
sha2.Write(sha1.Sum(nil)) | ||
return sha2.Sum(nil) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, you can do:
inner := sha256.Sum256(data)
outer := sha256.Sum256(inner[:])
return outer[:]
|
||
func (extKey *ExtendedKey) serialize() ([]byte, error) { | ||
var serialized [extendedKeySerializationLen]byte | ||
copy(serialized[:versionSerializationLen], extKey.Version[:]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a little weird that this is the only place that you specify the end of the slice
serialized[versionSerializationLen+depthSerializationLen+fingerprintSerializationLen+childNumberSerializationLen+chainCodeSerializationLen] = 0 | ||
copy( | ||
serialized[versionSerializationLen+depthSerializationLen+fingerprintSerializationLen+childNumberSerializationLen+chainCodeSerializationLen+1:], | ||
extKey.PrivateKey.Serialize()[:], | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, This is too long for my IDE
checkSum := doubleSha256(serialized[:len(serialized)-checkSumLen]) | ||
copy( | ||
serialized[versionSerializationLen+depthSerializationLen+fingerprintSerializationLen+childNumberSerializationLen+chainCodeSerializationLen+keySerializationLen:], | ||
checkSum, | ||
) | ||
return serialized[:], nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit, I think the checksum is more part of the base58 encoding than part of the serialization,
but serialize
is currently a private function so we don't care about the distinction (it also saves use from another allocation+copy)
"github.com/pkg/errors" | ||
) | ||
|
||
type ExtendedKey struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm... oh well I guess.
pathes: []testPath{ | ||
seed: "000102030405060708090a0b0c0d0e0f", | ||
version: BitcoinMainnetPrivate, | ||
paths: []testPath{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have no test vectors that check public key deriviation (a.k.a capital M paths), maybe write some such as well?
(Or alternatively, for each test-vector here, also test what happens when you replace m with M)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TestExtendedKey_DeriveFromPath checks that.
t.Fatalf("Test (%d, %d): deserializing and serializing the ext pub didn't preserve the data", i, j) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func TestExtendedPublicKey_Path(t *testing.T) { | ||
// TestExtendedKey_DeriveFromPath checks that path that derive from extended public key and extended |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suspected typo: derive from extended public key and extended public key
I think you want to replace one "public" to "private"
checkSumLen | ||
|
||
func DeserializeExtendedPrivateKey(extKeyString string) (*ExtendedKey, error) { | ||
serializedBytes := base58.Decode(extKeyString) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you missed copying the library.
var BitcoinMainnetPublic = [4]byte{ | ||
0x04, | ||
0x88, | ||
0xb2, | ||
0x1e, | ||
} | ||
|
||
// KaspaMainnetPrivate is the version that is used for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we maybe want to define versions for kaspa testnet/mainnet/simnet as well?
// KaspaTestnetPrivate is the version that is used for | ||
// kaspa testnet bip32 public extended keys. | ||
var KaspaTestnetPrivate = [4]byte{ | ||
0x03, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use either decimal or hex for these values, but not a mixture. (This also refers to the fact that the previous versions were all hexa, and the new ones are mostly decimal).
Also, maybe write in the comment what this encodes to? (a.k.a. kpriv/tpub/etc)
No description provided.