From bff18e5a9305b1f54f900e30c7090d5a19e149d0 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Wed, 21 May 2014 17:14:23 -0500 Subject: [PATCH] Introduce better WIF API. The old functions DecodePrivateKey and EncodePrivateKey have been removed in favor of the DecodeWIF function and the String method of the new WIF type. ok @davecgh --- address.go | 100 ----------------------- address_test.go | 46 ----------- test_coverage.txt | 43 +++++----- wif.go | 198 ++++++++++++++++++++++++++++++++++++++++++++++ wif_test.go | 71 +++++++++++++++++ 5 files changed, 293 insertions(+), 165 deletions(-) create mode 100644 wif.go create mode 100644 wif_test.go diff --git a/address.go b/address.go index 805cc193b..8bef8b716 100644 --- a/address.go +++ b/address.go @@ -23,11 +23,6 @@ var ( // a non-matching checksum. ErrMalformedAddress = errors.New("malformed address") - // ErrMalformedPrivateKey describes an error where an address is - // improperly formatted, either due to an incorrect length of the - // private key or a non-matching checksum. - ErrMalformedPrivateKey = errors.New("malformed private key") - // ErrChecksumMismatch describes an error where decoding failed due // to a bad checksum. ErrChecksumMismatch = errors.New("checksum mismatch") @@ -46,12 +41,6 @@ const ( // TestNetAddr is the address identifier for TestNet TestNetAddr = 0x6f - // MainNetKey is the key identifier for MainNet - MainNetKey = 0x80 - - // TestNetKey is the key identifier for TestNet - TestNetKey = 0xef - // MainNetScriptHash is the script hash identifier for MainNet MainNetScriptHash = 0x05 @@ -511,92 +500,3 @@ func (a *AddressPubKey) AddressPubKeyHash() *AddressPubKeyHash { func (a *AddressPubKey) PubKey() *btcec.PublicKey { return a.pubKey } - -// EncodePrivateKey takes a 32-byte private key and encodes it into the -// Wallet Import Format (WIF). -func EncodePrivateKey(privKey []byte, net btcwire.BitcoinNet, compressed bool) (string, error) { - if len(privKey) != 32 { - return "", ErrMalformedPrivateKey - } - - var netID byte - switch net { - case btcwire.MainNet: - netID = MainNetKey - case btcwire.TestNet3: - netID = TestNetKey - default: - return "", ErrUnknownNet - } - - tosum := append([]byte{netID}, privKey...) - if compressed { - tosum = append(tosum, 0x01) - } - cksum := btcwire.DoubleSha256(tosum) - - // Private key before base58 encoding is 1 byte for netID, 32 bytes for - // privKey, plus an optional byte (0x01) if copressed, plus 4 bytes of checksum. - encodeLen := 37 - if compressed { - encodeLen++ - } - a := make([]byte, encodeLen, encodeLen) - a[0] = netID - copy(a[1:], privKey) - if compressed { - copy(a[32+1:], []byte{0x01}) - copy(a[32+1+1:], cksum[:4]) - } else { - copy(a[32+1:], cksum[:4]) - } - return Base58Encode(a), nil -} - -// DecodePrivateKey takes a Wallet Import Format (WIF) string and -// decodes into a 32-byte private key. -func DecodePrivateKey(wif string) ([]byte, btcwire.BitcoinNet, bool, error) { - decoded := Base58Decode(wif) - decodedLen := len(decoded) - compressed := false - - // Length of decoded privkey must be 32 bytes + an optional 1 byte (0x01) - // if compressed, plus 1 byte for netID + 4 bytes of checksum - if decodedLen == 32+6 { - compressed = true - if decoded[33] != 0x01 { - return nil, 0, compressed, ErrMalformedPrivateKey - } - } else if decodedLen != 32+5 { - return nil, 0, compressed, ErrMalformedPrivateKey - } - - var net btcwire.BitcoinNet - switch decoded[0] { - case MainNetKey: - net = btcwire.MainNet - case TestNetKey: - net = btcwire.TestNet3 - default: - return nil, 0, compressed, ErrUnknownNet - } - - // Checksum is first four bytes of double SHA256 of the identifier byte - // and privKey. Verify this matches the final 4 bytes of the decoded - // private key. - var tosum []byte - if compressed { - tosum = decoded[:32+1+1] - } else { - tosum = decoded[:32+1] - } - cksum := btcwire.DoubleSha256(tosum)[:4] - if !bytes.Equal(cksum, decoded[decodedLen-4:]) { - return nil, 0, compressed, ErrMalformedPrivateKey - } - - privKey := make([]byte, 32, 32) - copy(privKey[:], decoded[1:32+1]) - - return privKey, net, compressed, nil -} diff --git a/address_test.go b/address_test.go index 7fe4ac49a..80ef22599 100644 --- a/address_test.go +++ b/address_test.go @@ -589,49 +589,3 @@ func TestAddresses(t *testing.T) { } } } - -func TestEncodeDecodePrivateKey(t *testing.T) { - tests := []struct { - in []byte - net btcwire.BitcoinNet - compressed bool - out string - }{ - {[]byte{ - 0x0c, 0x28, 0xfc, 0xa3, 0x86, 0xc7, 0xa2, 0x27, - 0x60, 0x0b, 0x2f, 0xe5, 0x0b, 0x7c, 0xae, 0x11, - 0xec, 0x86, 0xd3, 0xbf, 0x1f, 0xbe, 0x47, 0x1b, - 0xe8, 0x98, 0x27, 0xe1, 0x9d, 0x72, 0xaa, 0x1d, - }, btcwire.MainNet, false, "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ"}, - {[]byte{ - 0xdd, 0xa3, 0x5a, 0x14, 0x88, 0xfb, 0x97, 0xb6, - 0xeb, 0x3f, 0xe6, 0xe9, 0xef, 0x2a, 0x25, 0x81, - 0x4e, 0x39, 0x6f, 0xb5, 0xdc, 0x29, 0x5f, 0xe9, - 0x94, 0xb9, 0x67, 0x89, 0xb2, 0x1a, 0x03, 0x98, - }, btcwire.TestNet3, true, "cV1Y7ARUr9Yx7BR55nTdnR7ZXNJphZtCCMBTEZBJe1hXt2kB684q"}, - } - - for x, test := range tests { - wif, err := btcutil.EncodePrivateKey(test.in, test.net, test.compressed) - if err != nil { - t.Errorf("%x: %v", x, err) - continue - } - if wif != test.out { - t.Errorf("TestEncodeDecodePrivateKey failed: want '%s', got '%s'", - test.out, wif) - continue - } - - key, _, compressed, err := btcutil.DecodePrivateKey(test.out) - if err != nil { - t.Error(err) - continue - } - if !bytes.Equal(key, test.in) || compressed != test.compressed { - t.Errorf("TestEncodeDecodePrivateKey failed: want '%x', got '%x'", - test.out, key) - } - - } -} diff --git a/test_coverage.txt b/test_coverage.txt index af66f2c4f..bffcb6328 100644 --- a/test_coverage.txt +++ b/test_coverage.txt @@ -2,60 +2,65 @@ github.com/conformal/btcutil/base58.go Base58Decode 100.00% (20/20) github.com/conformal/btcutil/base58.go Base58Encode 100.00% (15/15) github.com/conformal/btcutil/block.go Block.Tx 100.00% (12/12) +github.com/conformal/btcutil/wif.go WIF.String 100.00% (11/11) github.com/conformal/btcutil/block.go Block.Transactions 100.00% (11/11) github.com/conformal/btcutil/address.go encodeAddress 100.00% (9/9) github.com/conformal/btcutil/amount.go NewAmount 100.00% (9/9) github.com/conformal/btcutil/amount.go AmountUnit.String 100.00% (8/8) github.com/conformal/btcutil/block.go NewBlockFromBytes 100.00% (7/7) github.com/conformal/btcutil/tx.go NewTxFromBytes 100.00% (7/7) -github.com/conformal/btcutil/tx.go Tx.Sha 100.00% (5/5) github.com/conformal/btcutil/block.go Block.Sha 100.00% (5/5) +github.com/conformal/btcutil/tx.go Tx.Sha 100.00% (5/5) github.com/conformal/btcutil/address.go checkBitcoinNet 100.00% (5/5) github.com/conformal/btcutil/hash160.go calcHash 100.00% (2/2) github.com/conformal/btcutil/amount.go Amount.Format 100.00% (2/2) github.com/conformal/btcutil/address.go NewAddressScriptHash 100.00% (2/2) -github.com/conformal/btcutil/block.go OutOfRangeError.Error 100.00% (1/1) +github.com/conformal/btcutil/block.go NewBlockFromBlockAndBytes 100.00% (1/1) +github.com/conformal/btcutil/amount.go Amount.ToUnit 100.00% (1/1) +github.com/conformal/btcutil/amount.go Amount.String 100.00% (1/1) +github.com/conformal/btcutil/address.go AddressPubKeyHash.String 100.00% (1/1) +github.com/conformal/btcutil/block.go Block.MsgBlock 100.00% (1/1) +github.com/conformal/btcutil/block.go NewBlock 100.00% (1/1) +github.com/conformal/btcutil/block.go Block.SetHeight 100.00% (1/1) +github.com/conformal/btcutil/block.go Block.Height 100.00% (1/1) github.com/conformal/btcutil/address.go AddressPubKeyHash.EncodeAddress 100.00% (1/1) github.com/conformal/btcutil/address.go AddressPubKeyHash.ScriptAddress 100.00% (1/1) github.com/conformal/btcutil/address.go AddressPubKeyHash.Hash160 100.00% (1/1) +github.com/conformal/btcutil/tx.go NewTx 100.00% (1/1) github.com/conformal/btcutil/address.go AddressScriptHash.EncodeAddress 100.00% (1/1) github.com/conformal/btcutil/address.go AddressScriptHash.ScriptAddress 100.00% (1/1) +github.com/conformal/btcutil/tx.go Tx.SetIndex 100.00% (1/1) github.com/conformal/btcutil/address.go AddressScriptHash.String 100.00% (1/1) github.com/conformal/btcutil/address.go AddressScriptHash.Hash160 100.00% (1/1) +github.com/conformal/btcutil/tx.go Tx.Index 100.00% (1/1) github.com/conformal/btcutil/address.go AddressPubKey.EncodeAddress 100.00% (1/1) github.com/conformal/btcutil/address.go AddressPubKey.ScriptAddress 100.00% (1/1) +github.com/conformal/btcutil/tx.go Tx.MsgTx 100.00% (1/1) github.com/conformal/btcutil/address.go AddressPubKey.String 100.00% (1/1) -github.com/conformal/btcutil/amount.go Amount.ToUnit 100.00% (1/1) -github.com/conformal/btcutil/amount.go Amount.String 100.00% (1/1) -github.com/conformal/btcutil/block.go Block.MsgBlock 100.00% (1/1) -github.com/conformal/btcutil/block.go Block.Height 100.00% (1/1) -github.com/conformal/btcutil/block.go Block.SetHeight 100.00% (1/1) -github.com/conformal/btcutil/block.go NewBlock 100.00% (1/1) -github.com/conformal/btcutil/block.go NewBlockFromBlockAndBytes 100.00% (1/1) github.com/conformal/btcutil/hash160.go Hash160 100.00% (1/1) -github.com/conformal/btcutil/tx.go Tx.MsgTx 100.00% (1/1) -github.com/conformal/btcutil/tx.go Tx.Index 100.00% (1/1) -github.com/conformal/btcutil/tx.go Tx.SetIndex 100.00% (1/1) -github.com/conformal/btcutil/tx.go NewTx 100.00% (1/1) -github.com/conformal/btcutil/address.go AddressPubKeyHash.String 100.00% (1/1) +github.com/conformal/btcutil/block.go OutOfRangeError.Error 100.00% (1/1) github.com/conformal/btcutil/address.go DecodeAddress 95.65% (22/23) github.com/conformal/btcutil/appdata.go appDataDir 92.00% (23/25) github.com/conformal/btcutil/address.go NewAddressPubKeyHash 91.67% (11/12) github.com/conformal/btcutil/address.go NewAddressScriptHashFromHash 91.67% (11/12) -github.com/conformal/btcutil/address.go EncodePrivateKey 90.91% (20/22) github.com/conformal/btcutil/block.go Block.TxLoc 88.89% (8/9) github.com/conformal/btcutil/block.go Block.Bytes 88.89% (8/9) github.com/conformal/btcutil/address.go AddressPubKey.serialize 85.71% (6/7) -github.com/conformal/btcutil/address.go DecodePrivateKey 83.33% (20/24) github.com/conformal/btcutil/address.go NewAddressPubKey 83.33% (15/18) +github.com/conformal/btcutil/wif.go DecodeWIF 81.82% (18/22) +github.com/conformal/btcutil/wif.go NewWIF 75.00% (3/4) github.com/conformal/btcutil/block.go Block.TxSha 75.00% (3/4) +github.com/conformal/btcutil/wif.go paddedAppend 66.67% (2/3) +github.com/conformal/btcutil/address.go AddressPubKey.IsForNet 60.00% (3/5) github.com/conformal/btcutil/address.go AddressScriptHash.IsForNet 60.00% (3/5) github.com/conformal/btcutil/address.go AddressPubKeyHash.IsForNet 60.00% (3/5) -github.com/conformal/btcutil/address.go AddressPubKey.IsForNet 60.00% (3/5) github.com/conformal/btcutil/certgen.go NewTLSCertPair 0.00% (0/50) +github.com/conformal/btcutil/wif.go WIF.SerializePubKey 0.00% (0/4) +github.com/conformal/btcutil/wif.go WIF.IsForNet 0.00% (0/4) github.com/conformal/btcutil/address.go AddressPubKey.AddressPubKeyHash 0.00% (0/3) -github.com/conformal/btcutil/address.go AddressPubKey.Format 0.00% (0/1) github.com/conformal/btcutil/appdata.go AppDataDir 0.00% (0/1) +github.com/conformal/btcutil/address.go AddressPubKey.Format 0.00% (0/1) github.com/conformal/btcutil/address.go AddressPubKey.SetFormat 0.00% (0/1) -github.com/conformal/btcutil ------------------------------- 78.89% (299/379) +github.com/conformal/btcutil/address.go AddressPubKey.PubKey 0.00% (0/1) +github.com/conformal/btcutil ------------------------------- 76.70% (293/382) diff --git a/wif.go b/wif.go new file mode 100644 index 000000000..900debf3e --- /dev/null +++ b/wif.go @@ -0,0 +1,198 @@ +// Copyright (c) 2013, 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcutil + +import ( + "bytes" + "errors" + + "github.com/conformal/btcec" + "github.com/conformal/btcwire" +) + +// ErrMalformedPrivateKey describes an error where a WIF-encoded private +// key cannot be decoded due to being improperly formatted. This may occur +// if the byte length is incorrect or an unexpected magic number was +// encountered. +var ErrMalformedPrivateKey = errors.New("malformed private key") + +// These constants define the magic numbers used for identifing components +// of a WIF-encoded private key and the bitcoin address associated with it. +const ( + // mainNetKey is the magic number identifying a WIF private key for + // the MainNet bitcoin network. + mainNetKey byte = 0x80 + + // testNetKey is the magic number identifying a WIF private key for + // the regression test and TestNet3 bitcoin networks. + testNetKey byte = 0xef + + // compressMagic is the magic byte used to identify a WIF encoding for + // an address created from a compressed serialized public key. + compressMagic byte = 0x01 +) + +// WIF contains the individual components described by the Wallet Import Format +// (WIF). A WIF string is typically used to represent a private key and its +// associated address in a way that may be easily copied and imported into or +// exported from wallet software. WIF strings may be decoded into this +// structure by calling DecodeWIF or created with a user-provided private key +// by calling NewWIF. +type WIF struct { + // PrivKey is the private key being imported or exported. + PrivKey *btcec.PrivateKey + + // CompressPubKey specifies whether the address controlled by the + // imported or exported private key was created by hashing a + // compressed (33-byte) serialized public key, rather than an + // uncompressed (65-byte) one. + CompressPubKey bool + + // netID is the bitcoin network identifier byte used when + // WIF encoding the private key. + netID byte +} + +// NewWIF creates a new WIF structure to export an address and its private key +// as a string encoded in the Wallet Import Format. The net argument must be +// either btcwire.MainNet, btcwire.TestNet3 or btcwire.TestNet. The compress +// argument specifies whether the address intended to be imported or exported +// was created by serializing the public key compressed rather than +// uncompressed. +func NewWIF(privKey *btcec.PrivateKey, net btcwire.BitcoinNet, compress bool) (*WIF, error) { + // Determine the key's network identifier byte. The same byte is + // shared for TestNet3 and TestNet (the regression test network). + switch net { + case btcwire.MainNet: + return &WIF{privKey, compress, mainNetKey}, nil + case btcwire.TestNet, btcwire.TestNet3: + return &WIF{privKey, compress, testNetKey}, nil + default: + return nil, ErrUnknownNet + } +} + +// IsForNet returns whether or not the decoded WIF structure is associated +// with the passed bitcoin network. +func (w *WIF) IsForNet(net btcwire.BitcoinNet) bool { + switch net { + case btcwire.MainNet: + return w.netID == mainNetKey + case btcwire.TestNet, btcwire.TestNet3: + return w.netID == testNetKey + default: + return false + } +} + +// DecodeWIF creates a new WIF structure by decoding the string encoding of +// the import format. +// +// The WIF string must be a base58-encoded string of the following byte +// sequence: +// +// * 1 byte to identify the network, must be 0x80 for mainnet or 0xef for +// either testnet3 or the regression test network +// * 32 bytes of a binary-encoded, big-endian, zero-padded private key +// * Optional 1 byte (equal to 0x01) if the address being imported or exported +// was created by taking the RIPEMD160 after SHA256 hash of a serialized +// compressed (33-byte) public key +// * 4 bytes of checksum, must equal the first four bytes of the double SHA256 +// of every byte before the checksum in this sequence +// +// If the base58-decoded byte sequence does not match this, DecodeWIF will +// return a non-nil error. ErrMalformedPrivateKey is returned when the WIF +// is of an impossible length or the expected compressed pubkey magic number +// does not equal the expected value of 0x01. ErrChecksumMismatch is returned +// if the expected WIF checksum does not match the calculated checksum. +func DecodeWIF(wif string) (*WIF, error) { + decoded := Base58Decode(wif) + decodedLen := len(decoded) + var compress bool + + // Length of base58 decoded WIF must be 32 bytes + an optional 1 byte + // (0x01) if compressed, plus 1 byte for netID + 4 bytes of checksum. + switch decodedLen { + case 1 + btcec.PrivKeyBytesLen + 1 + 4: + if decoded[33] != compressMagic { + return nil, ErrMalformedPrivateKey + } + compress = true + case 1 + btcec.PrivKeyBytesLen + 4: + compress = false + default: + return nil, ErrMalformedPrivateKey + } + + netID := decoded[0] + if netID != mainNetKey && netID != testNetKey { + return nil, ErrUnknownNet + } + + // Checksum is first four bytes of double SHA256 of the identifier byte + // and privKey. Verify this matches the final 4 bytes of the decoded + // private key. + var tosum []byte + if compress { + tosum = decoded[:1+btcec.PrivKeyBytesLen+1] + } else { + tosum = decoded[:1+btcec.PrivKeyBytesLen] + } + cksum := btcwire.DoubleSha256(tosum)[:4] + if !bytes.Equal(cksum, decoded[decodedLen-4:]) { + return nil, ErrChecksumMismatch + } + + privKeyBytes := decoded[1 : 1+btcec.PrivKeyBytesLen] + privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), privKeyBytes) + return &WIF{privKey, compress, netID}, nil +} + +// String creates the Wallet Import Format string encoding of a WIF structure. +// See DecodeWIF for a detailed breakdown of the format and requirements of +// a valid WIF string. +func (w *WIF) String() string { + // Precalculate size. Maximum number of bytes before base58 encoding + // is one byte for the network, 32 bytes of private key, possibly one + // extra byte if the pubkey is to be compressed, and finally four + // bytes of checksum. + encodeLen := 1 + btcec.PrivKeyBytesLen + 4 + if w.CompressPubKey { + encodeLen++ + } + + a := make([]byte, 0, encodeLen) + a = append(a, w.netID) + // Pad and append bytes manually, instead of using Serialize, to + // avoid another call to make. + a = paddedAppend(btcec.PrivKeyBytesLen, a, w.PrivKey.D.Bytes()) + if w.CompressPubKey { + a = append(a, compressMagic) + } + cksum := btcwire.DoubleSha256(a)[:4] + a = append(a, cksum...) + return Base58Encode(a) +} + +// SerializePubKey serializes the associated public key of the imported or +// exported private key in either a compressed or uncompressed format. The +// serialization format chosen depends on the value of w.CompressPubKey. +func (w *WIF) SerializePubKey() []byte { + pk := (*btcec.PublicKey)(&w.PrivKey.PublicKey) + if w.CompressPubKey { + return pk.SerializeCompressed() + } + return pk.SerializeUncompressed() +} + +// paddedAppend appends the src byte slice to dst, returning the new slice. +// If the length of the source is smaller than the passed size, leading zero +// bytes are appended to the dst slice before appending src. +func paddedAppend(size uint, dst, src []byte) []byte { + for i := 0; i < int(size)-len(src); i++ { + dst = append(dst, 0) + } + return append(dst, src...) +} diff --git a/wif_test.go b/wif_test.go new file mode 100644 index 000000000..f59a28924 --- /dev/null +++ b/wif_test.go @@ -0,0 +1,71 @@ +// Copyright (c) 2013, 2014 Conformal Systems LLC. +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package btcutil_test + +import ( + "testing" + + "github.com/conformal/btcec" + . "github.com/conformal/btcutil" + "github.com/conformal/btcwire" +) + +func TestEncodeDecodeWIF(t *testing.T) { + priv1, _ := btcec.PrivKeyFromBytes(btcec.S256(), []byte{ + 0x0c, 0x28, 0xfc, 0xa3, 0x86, 0xc7, 0xa2, 0x27, + 0x60, 0x0b, 0x2f, 0xe5, 0x0b, 0x7c, 0xae, 0x11, + 0xec, 0x86, 0xd3, 0xbf, 0x1f, 0xbe, 0x47, 0x1b, + 0xe8, 0x98, 0x27, 0xe1, 0x9d, 0x72, 0xaa, 0x1d}) + + priv2, _ := btcec.PrivKeyFromBytes(btcec.S256(), []byte{ + 0xdd, 0xa3, 0x5a, 0x14, 0x88, 0xfb, 0x97, 0xb6, + 0xeb, 0x3f, 0xe6, 0xe9, 0xef, 0x2a, 0x25, 0x81, + 0x4e, 0x39, 0x6f, 0xb5, 0xdc, 0x29, 0x5f, 0xe9, + 0x94, 0xb9, 0x67, 0x89, 0xb2, 0x1a, 0x03, 0x98}) + + wif1, err := NewWIF(priv1, btcwire.MainNet, false) + if err != nil { + t.Fatal(err) + } + wif2, err := NewWIF(priv2, btcwire.TestNet3, true) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + wif *WIF + encoded string + }{ + { + wif1, + "5HueCGU8rMjxEXxiPuD5BDku4MkFqeZyd4dZ1jvhTVqvbTLvyTJ", + }, + { + wif2, + "cV1Y7ARUr9Yx7BR55nTdnR7ZXNJphZtCCMBTEZBJe1hXt2kB684q", + }, + } + + for _, test := range tests { + // Test that encoding the WIF structure matches the expected string. + s := test.wif.String() + if s != test.encoded { + t.Errorf("TestEncodeDecodePrivateKey failed: want '%s', got '%s'", + test.encoded, s) + continue + } + + // Test that decoding the expected string results in the original WIF + // structure. + w, err := DecodeWIF(test.encoded) + if err != nil { + t.Error(err) + continue + } + if got := w.String(); got != test.encoded { + t.Errorf("NewWIF failed: want '%v', got '%v'", test.wif, got) + } + } +}