Implement new wallet and chained address creation.
This commit is contained in:
parent
cfde81a062
commit
9eae969230
6 changed files with 823 additions and 181 deletions
113
cmd.go
113
cmd.go
|
@ -18,16 +18,24 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/conformal/btcwallet/tx"
|
||||||
"github.com/conformal/btcwallet/wallet"
|
"github.com/conformal/btcwallet/wallet"
|
||||||
"github.com/conformal/seelog"
|
"github.com/conformal/seelog"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
log seelog.LoggerInterface = seelog.Default
|
log seelog.LoggerInterface = seelog.Default
|
||||||
cfg *config
|
cfg *config
|
||||||
wallets = make(map[string]*wallet.Wallet)
|
wallets = struct {
|
||||||
|
sync.RWMutex
|
||||||
|
m map[string]*BtcWallet
|
||||||
|
}{
|
||||||
|
m: make(map[string]*BtcWallet),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -38,6 +46,7 @@ func main() {
|
||||||
}
|
}
|
||||||
cfg = tcfg
|
cfg = tcfg
|
||||||
|
|
||||||
|
/*
|
||||||
// Open wallet
|
// Open wallet
|
||||||
file, err := os.Open(cfg.WalletFile)
|
file, err := os.Open(cfg.WalletFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -47,9 +56,14 @@ func main() {
|
||||||
if _, err = w.ReadFrom(file); err != nil {
|
if _, err = w.ReadFrom(file); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Associate this wallet with default account.
|
// Open wallet
|
||||||
wallets[""] = w
|
btcw, err := OpenOrCreateWallet(cfg, "")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_ = btcw
|
||||||
|
|
||||||
// Start HTTP server to listen and send messages to frontend and btcd
|
// Start HTTP server to listen and send messages to frontend and btcd
|
||||||
// backend. Try reconnection if connection failed.
|
// backend. Try reconnection if connection failed.
|
||||||
|
@ -64,3 +78,96 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BtcWallet struct {
|
||||||
|
*wallet.Wallet
|
||||||
|
tx.UtxoStore
|
||||||
|
tx.TxStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenOrCreateWallet(cfg *config, account string) (*BtcWallet, error) {
|
||||||
|
// Open wallet file specified by account.
|
||||||
|
var wname string
|
||||||
|
if account == "" {
|
||||||
|
wname = "btcwallet"
|
||||||
|
} else {
|
||||||
|
wname = fmt.Sprintf("btcwallet-%s", account)
|
||||||
|
}
|
||||||
|
|
||||||
|
wdir := filepath.Join(cfg.DataDir, wname)
|
||||||
|
fi, err := os.Stat(wdir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// Attempt data directory creation
|
||||||
|
if err = os.MkdirAll(wdir, 0700); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !fi.IsDir() {
|
||||||
|
return nil, fmt.Errorf("Data directory '%s' is not a directory.", cfg.DataDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wfilepath := filepath.Join(wdir, "wallet.bin")
|
||||||
|
txfilepath := filepath.Join(wdir, "tx.bin")
|
||||||
|
utxofilepath := filepath.Join(wdir, "utxo.bin")
|
||||||
|
var wfile, txfile, utxofile *os.File
|
||||||
|
if wfile, err = os.Open(wfilepath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if wfile, err = os.Create(wfilepath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if txfile, err = os.Open(txfilepath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if txfile, err = os.Create(txfilepath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if utxofile, err = os.Open(utxofilepath); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
if utxofile, err = os.Create(utxofilepath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wlt := new(wallet.Wallet)
|
||||||
|
if _, err = wlt.ReadFrom(wfile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var txs tx.TxStore
|
||||||
|
if _, err = txs.ReadFrom(txfile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var utxos tx.UtxoStore
|
||||||
|
if _, err = utxos.ReadFrom(utxofile); err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &BtcWallet{
|
||||||
|
Wallet: wlt,
|
||||||
|
UtxoStore: utxos,
|
||||||
|
TxStore: txs,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Associate this wallet with default account.
|
||||||
|
wallets.Lock()
|
||||||
|
wallets.m[account] = w
|
||||||
|
wallets.Unlock()
|
||||||
|
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
27
cmdmgr.go
27
cmdmgr.go
|
@ -219,8 +219,11 @@ func GetAddressesByAccount(reply chan []byte, msg []byte) {
|
||||||
params := v["params"].([]interface{})
|
params := v["params"].([]interface{})
|
||||||
|
|
||||||
var result interface{}
|
var result interface{}
|
||||||
if w := wallets[params[0].(string)]; w != nil {
|
wallets.RLock()
|
||||||
result = w.GetActiveAddresses()
|
w := wallets.m[params[0].(string)]
|
||||||
|
wallets.RUnlock()
|
||||||
|
if w != nil {
|
||||||
|
result = w.Wallet.GetActiveAddresses()
|
||||||
} else {
|
} else {
|
||||||
result = []interface{}{}
|
result = []interface{}{}
|
||||||
}
|
}
|
||||||
|
@ -235,7 +238,10 @@ func GetNewAddress(reply chan []byte, msg []byte) {
|
||||||
json.Unmarshal(msg, &v)
|
json.Unmarshal(msg, &v)
|
||||||
params := v["params"].([]interface{})
|
params := v["params"].([]interface{})
|
||||||
if len(params) == 0 || params[0].(string) == "" {
|
if len(params) == 0 || params[0].(string) == "" {
|
||||||
if w := wallets[""]; w != nil {
|
wallets.RLock()
|
||||||
|
w := wallets.m[""]
|
||||||
|
wallets.RUnlock()
|
||||||
|
if w != nil {
|
||||||
addr := w.NextUnusedAddress()
|
addr := w.NextUnusedAddress()
|
||||||
ReplySuccess(reply, v["id"], addr)
|
ReplySuccess(reply, v["id"], addr)
|
||||||
}
|
}
|
||||||
|
@ -257,7 +263,10 @@ func WalletIsLocked(reply chan []byte, msg []byte) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if w := wallets[account]; w != nil {
|
wallets.RLock()
|
||||||
|
w := wallets.m[account]
|
||||||
|
wallets.RUnlock()
|
||||||
|
if w != nil {
|
||||||
result := w.IsLocked()
|
result := w.IsLocked()
|
||||||
ReplySuccess(reply, v["id"], result)
|
ReplySuccess(reply, v["id"], result)
|
||||||
} else {
|
} else {
|
||||||
|
@ -272,7 +281,10 @@ func WalletIsLocked(reply chan []byte, msg []byte) {
|
||||||
func WalletLock(reply chan []byte, msg []byte) {
|
func WalletLock(reply chan []byte, msg []byte) {
|
||||||
var v map[string]interface{}
|
var v map[string]interface{}
|
||||||
json.Unmarshal(msg, &v)
|
json.Unmarshal(msg, &v)
|
||||||
if w := wallets[""]; w != nil {
|
wallets.RLock()
|
||||||
|
w := wallets.m[""]
|
||||||
|
wallets.RUnlock()
|
||||||
|
if w != nil {
|
||||||
if err := w.Lock(); err != nil {
|
if err := w.Lock(); err != nil {
|
||||||
ReplyError(reply, v["id"], &WalletWrongEncState)
|
ReplyError(reply, v["id"], &WalletWrongEncState)
|
||||||
} else {
|
} else {
|
||||||
|
@ -302,7 +314,10 @@ func WalletPassphrase(reply chan []byte, msg []byte) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if w := wallets[""]; w != nil {
|
wallets.RLock()
|
||||||
|
w := wallets.m[""]
|
||||||
|
wallets.RUnlock()
|
||||||
|
if w != nil {
|
||||||
if err := w.Unlock([]byte(passphrase)); err != nil {
|
if err := w.Unlock([]byte(passphrase)); err != nil {
|
||||||
ReplyError(reply, v["id"], &WalletPassphraseIncorrect)
|
ReplyError(reply, v["id"], &WalletPassphraseIncorrect)
|
||||||
return
|
return
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/conformal/go-flags"
|
"github.com/conformal/go-flags"
|
||||||
"os"
|
"os"
|
||||||
|
@ -34,6 +33,7 @@ const (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
defaultConfigFile = filepath.Join(btcwalletHomeDir(), defaultConfigFilename)
|
defaultConfigFile = filepath.Join(btcwalletHomeDir(), defaultConfigFilename)
|
||||||
|
defaultDataDir = btcwalletHomeDir()
|
||||||
)
|
)
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
|
@ -42,7 +42,7 @@ type config struct {
|
||||||
DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"`
|
DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"`
|
||||||
ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"`
|
ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"`
|
||||||
SvrPort int `short:"p" long:"serverport" description:"Port to serve frontend websocket connections on"`
|
SvrPort int `short:"p" long:"serverport" description:"Port to serve frontend websocket connections on"`
|
||||||
WalletFile string `short:"f" long:"walletfile" description:"Path to wallet file"`
|
DataDir string `short:"D" long:"datadir" description:"Directory to store wallets and transactions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// btcwalletHomeDir returns an OS appropriate home directory for btcwallet.
|
// btcwalletHomeDir returns an OS appropriate home directory for btcwallet.
|
||||||
|
@ -92,6 +92,7 @@ func loadConfig() (*config, []string, error) {
|
||||||
ConfigFile: defaultConfigFile,
|
ConfigFile: defaultConfigFile,
|
||||||
BtcdPort: defaultBtcdPort,
|
BtcdPort: defaultBtcdPort,
|
||||||
SvrPort: defaultServerPort,
|
SvrPort: defaultServerPort,
|
||||||
|
DataDir: defaultDataDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
// A config file in the current directory takes precedence.
|
// A config file in the current directory takes precedence.
|
||||||
|
@ -141,9 +142,11 @@ func loadConfig() (*config, []string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// wallet file must be valid
|
// wallet file must be valid
|
||||||
|
/*
|
||||||
if !fileExists(cfg.WalletFile) {
|
if !fileExists(cfg.WalletFile) {
|
||||||
return &cfg, nil, errors.New("Wallet file does not exist.")
|
return &cfg, nil, errors.New("Wallet file does not exist.")
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return &cfg, remainingArgs, nil
|
return &cfg, remainingArgs, nil
|
||||||
}
|
}
|
||||||
|
|
38
sockets.go
38
sockets.go
|
@ -23,6 +23,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/conformal/btcjson"
|
"github.com/conformal/btcjson"
|
||||||
"github.com/conformal/btcwire"
|
"github.com/conformal/btcwire"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
@ -183,6 +184,8 @@ func BtcdHandler(ws *websocket.Conn) {
|
||||||
// TODO(jrick): hook this up with addresses in wallet.
|
// TODO(jrick): hook this up with addresses in wallet.
|
||||||
// reqTxsForAddress("addr")
|
// reqTxsForAddress("addr")
|
||||||
|
|
||||||
|
reqUtxoForAddress("1PZ67BehXWbzoqkovph4Cyfiz9LiFfTUot")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case rply, ok := <-replies:
|
case rply, ok := <-replies:
|
||||||
|
@ -290,6 +293,16 @@ func ProcessBtcdNotificationReply(b []byte) {
|
||||||
case "btcd:blockdisconnected":
|
case "btcd:blockdisconnected":
|
||||||
// TODO(jrick): rollback txs and utxos from removed block.
|
// TODO(jrick): rollback txs and utxos from removed block.
|
||||||
|
|
||||||
|
case "btcd:recvtx":
|
||||||
|
log.Info("got recvtx (ignoring)")
|
||||||
|
|
||||||
|
case "btcd:sendtx":
|
||||||
|
log.Info("got sendtx (ignoring)")
|
||||||
|
|
||||||
|
case "btcd:utxo":
|
||||||
|
result := m["result"].(map[string]interface{})
|
||||||
|
spew.Dump(result)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
frontendNotificationMaster <- b
|
frontendNotificationMaster <- b
|
||||||
}
|
}
|
||||||
|
@ -381,3 +394,28 @@ func reqTxsForAddress(addr string) {
|
||||||
|
|
||||||
btcdMsgs <- msg
|
btcdMsgs <- msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reqUtxoForAddress(addr string) {
|
||||||
|
seq.Lock()
|
||||||
|
n := seq.n
|
||||||
|
seq.n++
|
||||||
|
seq.Unlock()
|
||||||
|
|
||||||
|
m := &btcjson.Message{
|
||||||
|
Jsonrpc: "",
|
||||||
|
Id: fmt.Sprintf("btcwallet(%d)", n),
|
||||||
|
Method: "requestutxos",
|
||||||
|
Params: []interface{}{
|
||||||
|
addr,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
msg, _ := json.Marshal(m)
|
||||||
|
|
||||||
|
replyHandlers.Lock()
|
||||||
|
replyHandlers.m[n] = func(result interface{}) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
replyHandlers.Unlock()
|
||||||
|
|
||||||
|
btcdMsgs <- msg
|
||||||
|
}
|
||||||
|
|
694
wallet/wallet.go
694
wallet/wallet.go
|
@ -21,6 +21,7 @@ import (
|
||||||
"code.google.com/p/go.crypto/ripemd160"
|
"code.google.com/p/go.crypto/ripemd160"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
@ -29,10 +30,17 @@ import (
|
||||||
"github.com/conformal/btcec"
|
"github.com/conformal/btcec"
|
||||||
"github.com/conformal/btcutil"
|
"github.com/conformal/btcutil"
|
||||||
"github.com/conformal/btcwire"
|
"github.com/conformal/btcwire"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ = spew.Dump
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Length in bytes of KDF output.
|
// Length in bytes of KDF output.
|
||||||
kdfOutputBytes = 32
|
kdfOutputBytes = 32
|
||||||
|
@ -42,6 +50,11 @@ const (
|
||||||
maxCommentLen = (1 << 16) - 1
|
maxCommentLen = (1 << 16) - 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultKdfComputeTime = 0.25
|
||||||
|
defaultKdfMaxMem = 32 * 1024 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
// Possible errors when dealing with wallets.
|
// Possible errors when dealing with wallets.
|
||||||
var (
|
var (
|
||||||
ChecksumErr = errors.New("Checksum mismatch")
|
ChecksumErr = errors.New("Checksum mismatch")
|
||||||
|
@ -49,6 +62,14 @@ var (
|
||||||
WalletDoesNotExist = errors.New("Non-existant wallet")
|
WalletDoesNotExist = errors.New("Non-existant wallet")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// '\xbaWALLET\x00'
|
||||||
|
fileID = [8]byte{0xba, 0x57, 0x41, 0x4c, 0x4c, 0x45, 0x54, 0x00}
|
||||||
|
|
||||||
|
mainnetMagicBytes = [4]byte{0xf9, 0xbe, 0xb4, 0xd9}
|
||||||
|
testnetMagicBytes = [4]byte{0x0b, 0x11, 0x09, 0x07}
|
||||||
|
)
|
||||||
|
|
||||||
type entryHeader byte
|
type entryHeader byte
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -83,6 +104,38 @@ func binaryWrite(w io.Writer, order binary.ByteOrder, data interface{}) (n int64
|
||||||
return int64(written), err
|
return int64(written), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate the hash of hasher over buf.
|
||||||
|
func calcHash(buf []byte, hasher hash.Hash) []byte {
|
||||||
|
hasher.Write(buf)
|
||||||
|
return hasher.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate hash160 which is ripemd160(sha256(data))
|
||||||
|
func calcHash160(buf []byte) []byte {
|
||||||
|
return calcHash(calcHash(buf, sha256.New()), ripemd160.New())
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate hash256 which is sha256(sha256(data))
|
||||||
|
func calcHash256(buf []byte) []byte {
|
||||||
|
return calcHash(calcHash(buf, sha256.New()), sha256.New())
|
||||||
|
}
|
||||||
|
|
||||||
|
// First byte in uncompressed pubKey field.
|
||||||
|
const pubkeyUncompressed = 0x4
|
||||||
|
|
||||||
|
// pubkeyFromPrivkey creates a 65-byte encoded pubkey based on a
|
||||||
|
// 32-byte privkey.
|
||||||
|
func pubkeyFromPrivkey(privkey []byte) (pubkey []byte) {
|
||||||
|
x, y := btcec.S256().ScalarBaseMult(privkey)
|
||||||
|
|
||||||
|
pubkey = make([]byte, 65)
|
||||||
|
pubkey[0] = pubkeyUncompressed
|
||||||
|
copy(pubkey[1:33], x.Bytes())
|
||||||
|
copy(pubkey[33:], y.Bytes())
|
||||||
|
|
||||||
|
return pubkey
|
||||||
|
}
|
||||||
|
|
||||||
func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte {
|
func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte {
|
||||||
saltedpass := append(passphrase, salt...)
|
saltedpass := append(passphrase, salt...)
|
||||||
lutbl := make([]byte, memReqts)
|
lutbl := make([]byte, memReqts)
|
||||||
|
@ -125,14 +178,65 @@ func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte {
|
||||||
// based on the ROMix algorithm described in Colin Percival's paper
|
// based on the ROMix algorithm described in Colin Percival's paper
|
||||||
// "Stronger Key Derivation via Sequential Memory-Hard Functions"
|
// "Stronger Key Derivation via Sequential Memory-Hard Functions"
|
||||||
// (http://www.tarsnap.com/scrypt/scrypt.pdf).
|
// (http://www.tarsnap.com/scrypt/scrypt.pdf).
|
||||||
func Key(passphrase, salt []byte, memReqts uint64, nIters uint32) []byte {
|
func Key(passphrase []byte, params *kdfParameters) []byte {
|
||||||
masterKey := passphrase
|
masterKey := passphrase
|
||||||
for i := uint32(0); i < nIters; i++ {
|
for i := uint32(0); i < params.nIter; i++ {
|
||||||
masterKey = keyOneIter(masterKey, salt, memReqts)
|
masterKey = keyOneIter(masterKey, params.salt[:], params.mem)
|
||||||
}
|
}
|
||||||
return masterKey
|
return masterKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// leftPad returns a new slice of length size. The contents of input are right
|
||||||
|
// aligned in the new slice.
|
||||||
|
func leftPad(input []byte, size int) (out []byte) {
|
||||||
|
n := len(input)
|
||||||
|
if n > size {
|
||||||
|
n = size
|
||||||
|
}
|
||||||
|
out = make([]byte, size)
|
||||||
|
copy(out[len(out)-n:], input)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChainedPrivKey deterministically generates new private key using a
|
||||||
|
// previous address and chaincode. privkey and chaincode must be 32
|
||||||
|
// bytes long, and pubkey may either be 65 bytes or nil (in which case it
|
||||||
|
// is generated by the privkey).
|
||||||
|
func ChainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) {
|
||||||
|
if len(privkey) != 32 {
|
||||||
|
return nil, fmt.Errorf("Invalid privkey length %d (must be 32)",
|
||||||
|
len(privkey))
|
||||||
|
}
|
||||||
|
if len(chaincode) != 32 {
|
||||||
|
return nil, fmt.Errorf("Invalid chaincode length %d (must be 32)",
|
||||||
|
len(chaincode))
|
||||||
|
}
|
||||||
|
if pubkey == nil {
|
||||||
|
pubkey = pubkeyFromPrivkey(privkey)
|
||||||
|
} else if len(pubkey) != 65 {
|
||||||
|
return nil, fmt.Errorf("Invalid pubkey length %d.", len(pubkey))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a perfect example of YOLO crypto. Armory claims this XORing
|
||||||
|
// with the SHA256 hash of the pubkey is done to add extra entropy (why
|
||||||
|
// you'd want to add entropy to a deterministic function, I don't know),
|
||||||
|
// even though the pubkey is generated directly from the privkey. In
|
||||||
|
// terms of security or privacy, this is a complete waste of CPU cycles,
|
||||||
|
// but we do the same because we want to keep compatibility with
|
||||||
|
// Armory's chained address generation.
|
||||||
|
xorbytes := make([]byte, 32)
|
||||||
|
chainMod := calcHash256(pubkey)
|
||||||
|
for i, _ := range xorbytes {
|
||||||
|
xorbytes[i] = chainMod[i] ^ chaincode[i]
|
||||||
|
}
|
||||||
|
chainXor := new(big.Int).SetBytes(xorbytes)
|
||||||
|
privint := new(big.Int).SetBytes(privkey)
|
||||||
|
|
||||||
|
t := new(big.Int).Mul(chainXor, privint)
|
||||||
|
b := t.Mod(t, btcec.S256().N).Bytes()
|
||||||
|
return leftPad(b, 32), nil
|
||||||
|
}
|
||||||
|
|
||||||
type varEntries []io.WriterTo
|
type varEntries []io.WriterTo
|
||||||
|
|
||||||
func (v *varEntries) WriteTo(w io.Writer) (n int64, err error) {
|
func (v *varEntries) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
@ -214,98 +318,140 @@ func (v *varEntries) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
// from and write to any type of byte streams, including files.
|
// from and write to any type of byte streams, including files.
|
||||||
// TODO(jrick) remove as many more magic numbers as possible.
|
// TODO(jrick) remove as many more magic numbers as possible.
|
||||||
type Wallet struct {
|
type Wallet struct {
|
||||||
fileID [8]byte
|
|
||||||
version uint32
|
version uint32
|
||||||
netMagicBytes [4]byte
|
net btcwire.BitcoinNet
|
||||||
walletFlags [8]byte
|
flags walletFlags
|
||||||
uniqID [6]byte
|
uniqID [6]byte
|
||||||
createDate [8]byte
|
createDate int64
|
||||||
name [32]byte
|
name [32]byte
|
||||||
description [256]byte
|
desc [256]byte
|
||||||
highestUsed int64
|
highestUsed int64
|
||||||
kdfParams kdfParameters
|
kdfParams kdfParameters
|
||||||
encryptionParams [256]byte
|
|
||||||
keyGenerator btcAddress
|
keyGenerator btcAddress
|
||||||
appendedEntries varEntries
|
addrMap map[[ripemd160.Size]byte]*btcAddress
|
||||||
|
addrCommentMap map[[ripemd160.Size]byte]*[]byte
|
||||||
|
txCommentMap map[[sha256.Size]byte]*[]byte
|
||||||
|
|
||||||
// These are not serialized
|
// These are not serialized
|
||||||
key struct {
|
key struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
secret []byte
|
secret []byte
|
||||||
}
|
}
|
||||||
addrMap map[[ripemd160.Size]byte]*btcAddress
|
|
||||||
addrCommentMap map[[ripemd160.Size]byte]*[]byte
|
|
||||||
chainIdxMap map[int64]*[ripemd160.Size]byte
|
chainIdxMap map[int64]*[ripemd160.Size]byte
|
||||||
txCommentMap map[[sha256.Size]byte]*[]byte
|
|
||||||
lastChainIdx int64
|
lastChainIdx int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteTo serializes a Wallet and writes it to a io.Writer,
|
// NewWallet() creates and initializes a new Wallet. name's and
|
||||||
// returning the number of bytes written and any errors encountered.
|
// desc's binary representation must not exceed 32 and 256 bytes,
|
||||||
func (wallet *Wallet) WriteTo(w io.Writer) (n int64, err error) {
|
// respectively. All address private keys are encrypted with passphrase.
|
||||||
// Iterate through each entry needing to be written. If data
|
// The wallet is returned unlocked.
|
||||||
// implements io.WriterTo, use its WriteTo func. Otherwise,
|
func NewWallet(name, desc string, passphrase []byte) (*Wallet, error) {
|
||||||
// data is a pointer to a fixed size value.
|
if binary.Size(name) > 32 {
|
||||||
datas := []interface{}{
|
return nil, errors.New("name exceeds 32 byte maximum size")
|
||||||
&wallet.fileID,
|
|
||||||
&wallet.version,
|
|
||||||
&wallet.netMagicBytes,
|
|
||||||
&wallet.walletFlags,
|
|
||||||
&wallet.uniqID,
|
|
||||||
&wallet.createDate,
|
|
||||||
&wallet.name,
|
|
||||||
&wallet.description,
|
|
||||||
&wallet.highestUsed,
|
|
||||||
&wallet.kdfParams,
|
|
||||||
&wallet.encryptionParams,
|
|
||||||
&wallet.keyGenerator,
|
|
||||||
make([]byte, 1024),
|
|
||||||
&wallet.appendedEntries,
|
|
||||||
}
|
|
||||||
var read int64
|
|
||||||
for _, data := range datas {
|
|
||||||
if s, ok := data.(io.WriterTo); ok {
|
|
||||||
read, err = s.WriteTo(w)
|
|
||||||
} else {
|
|
||||||
read, err = binaryWrite(w, binary.LittleEndian, data)
|
|
||||||
}
|
|
||||||
n += read
|
|
||||||
if err != nil {
|
|
||||||
return n, err
|
|
||||||
}
|
}
|
||||||
|
if binary.Size(desc) > 256 {
|
||||||
|
return nil, errors.New("desc exceeds 256 byte maximum size")
|
||||||
}
|
}
|
||||||
|
|
||||||
return n, nil
|
kdfp := computeKdfParameters(defaultKdfComputeTime, defaultKdfMaxMem)
|
||||||
|
|
||||||
|
rootkey, chaincode := make([]byte, 32), make([]byte, 32)
|
||||||
|
rand.Read(rootkey)
|
||||||
|
rand.Read(chaincode)
|
||||||
|
root, err := newRootBtcAddress(rootkey, nil, chaincode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
aeskey := Key([]byte(passphrase), kdfp)
|
||||||
|
if err := root.encrypt(aeskey); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of pregenerated addresses.
|
||||||
|
const pregenerated = 100
|
||||||
|
|
||||||
|
// TODO(jrick): not sure we will need uniqID, but would be good for
|
||||||
|
// compat with armory.
|
||||||
|
w := &Wallet{
|
||||||
|
version: 0, // TODO(jrick): implement versioning
|
||||||
|
net: btcwire.MainNet,
|
||||||
|
flags: walletFlags{
|
||||||
|
useEncryption: true,
|
||||||
|
watchingOnly: false,
|
||||||
|
},
|
||||||
|
createDate: time.Now().Unix(),
|
||||||
|
//highestUsed:
|
||||||
|
kdfParams: *kdfp,
|
||||||
|
keyGenerator: *root,
|
||||||
|
addrMap: make(map[[ripemd160.Size]byte]*btcAddress),
|
||||||
|
addrCommentMap: make(map[[ripemd160.Size]byte]*[]byte),
|
||||||
|
txCommentMap: make(map[[sha256.Size]byte]*[]byte),
|
||||||
|
chainIdxMap: make(map[int64]*[ripemd160.Size]byte),
|
||||||
|
lastChainIdx: pregenerated - 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add root address to maps.
|
||||||
|
w.addrMap[w.keyGenerator.pubKeyHash] = &w.keyGenerator
|
||||||
|
w.chainIdxMap[w.keyGenerator.chainIndex] = &w.keyGenerator.pubKeyHash
|
||||||
|
|
||||||
|
// Pre-generate 100 encrypted addresses and add to maps.
|
||||||
|
addr := &w.keyGenerator
|
||||||
|
cc := addr.chaincode[:]
|
||||||
|
for i := 0; i < pregenerated; i++ {
|
||||||
|
privkey, err := ChainedPrivKey(addr.privKeyCT, addr.pubKey[:], cc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
newaddr, err := newBtcAddress(privkey, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = newaddr.encrypt(aeskey); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.addrMap[newaddr.pubKeyHash] = newaddr
|
||||||
|
newaddr.chainIndex = addr.chainIndex + 1
|
||||||
|
w.chainIdxMap[newaddr.chainIndex] = &newaddr.pubKeyHash
|
||||||
|
copy(newaddr.chaincode[:], cc) // armory does this.. but why?
|
||||||
|
addr = newaddr
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(w.name[:], []byte(name))
|
||||||
|
copy(w.desc[:], []byte(desc))
|
||||||
|
return w, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFrom reads data from a io.Reader and saves it to a Wallet,
|
// ReadFrom reads data from a io.Reader and saves it to a Wallet,
|
||||||
// returning the number of bytes read and any errors encountered.
|
// returning the number of bytes read and any errors encountered.
|
||||||
func (wallet *Wallet) ReadFrom(r io.Reader) (n int64, err error) {
|
func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
var read int64
|
var read int64
|
||||||
|
|
||||||
wallet.addrMap = make(map[[ripemd160.Size]byte]*btcAddress)
|
w.addrMap = make(map[[ripemd160.Size]byte]*btcAddress)
|
||||||
wallet.addrCommentMap = make(map[[ripemd160.Size]byte]*[]byte)
|
w.addrCommentMap = make(map[[ripemd160.Size]byte]*[]byte)
|
||||||
wallet.chainIdxMap = make(map[int64]*[ripemd160.Size]byte)
|
w.chainIdxMap = make(map[int64]*[ripemd160.Size]byte)
|
||||||
wallet.txCommentMap = make(map[[sha256.Size]byte]*[]byte)
|
w.txCommentMap = make(map[[sha256.Size]byte]*[]byte)
|
||||||
|
|
||||||
|
var id [8]byte
|
||||||
|
var appendedEntries varEntries
|
||||||
|
|
||||||
// Iterate through each entry needing to be read. If data
|
// Iterate through each entry needing to be read. If data
|
||||||
// implements io.ReaderFrom, use its ReadFrom func. Otherwise,
|
// implements io.ReaderFrom, use its ReadFrom func. Otherwise,
|
||||||
// data is a pointer to a fixed sized value.
|
// data is a pointer to a fixed sized value.
|
||||||
datas := []interface{}{
|
datas := []interface{}{
|
||||||
&wallet.fileID,
|
&id,
|
||||||
&wallet.version,
|
&w.version,
|
||||||
&wallet.netMagicBytes,
|
&w.net,
|
||||||
&wallet.walletFlags,
|
&w.flags,
|
||||||
&wallet.uniqID,
|
&w.uniqID,
|
||||||
&wallet.createDate,
|
&w.createDate,
|
||||||
&wallet.name,
|
&w.name,
|
||||||
&wallet.description,
|
&w.desc,
|
||||||
&wallet.highestUsed,
|
&w.highestUsed,
|
||||||
&wallet.kdfParams,
|
&w.kdfParams,
|
||||||
&wallet.encryptionParams,
|
make([]byte, 256),
|
||||||
&wallet.keyGenerator,
|
&w.keyGenerator,
|
||||||
make([]byte, 1024),
|
make([]byte, 1024),
|
||||||
&wallet.appendedEntries,
|
&appendedEntries,
|
||||||
}
|
}
|
||||||
for _, data := range datas {
|
for _, data := range datas {
|
||||||
var err error
|
var err error
|
||||||
|
@ -320,27 +466,31 @@ func (wallet *Wallet) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if id != fileID {
|
||||||
|
return n, errors.New("Unknown File ID.")
|
||||||
|
}
|
||||||
|
|
||||||
// Add root address to address map
|
// Add root address to address map
|
||||||
wallet.addrMap[wallet.keyGenerator.pubKeyHash] = &wallet.keyGenerator
|
w.addrMap[w.keyGenerator.pubKeyHash] = &w.keyGenerator
|
||||||
wallet.chainIdxMap[wallet.keyGenerator.chainIndex] = &wallet.keyGenerator.pubKeyHash
|
w.chainIdxMap[w.keyGenerator.chainIndex] = &w.keyGenerator.pubKeyHash
|
||||||
|
|
||||||
// Fill unserializied fields.
|
// Fill unserializied fields.
|
||||||
wts := ([]io.WriterTo)(wallet.appendedEntries)
|
wts := ([]io.WriterTo)(appendedEntries)
|
||||||
for _, wt := range wts {
|
for _, wt := range wts {
|
||||||
switch wt.(type) {
|
switch wt.(type) {
|
||||||
case *addrEntry:
|
case *addrEntry:
|
||||||
e := wt.(*addrEntry)
|
e := wt.(*addrEntry)
|
||||||
wallet.addrMap[e.pubKeyHash160] = &e.addr
|
w.addrMap[e.pubKeyHash160] = &e.addr
|
||||||
wallet.chainIdxMap[e.addr.chainIndex] = &e.pubKeyHash160
|
w.chainIdxMap[e.addr.chainIndex] = &e.pubKeyHash160
|
||||||
if wallet.lastChainIdx < e.addr.chainIndex {
|
if w.lastChainIdx < e.addr.chainIndex {
|
||||||
wallet.lastChainIdx = e.addr.chainIndex
|
w.lastChainIdx = e.addr.chainIndex
|
||||||
}
|
}
|
||||||
case *addrCommentEntry:
|
case *addrCommentEntry:
|
||||||
e := wt.(*addrCommentEntry)
|
e := wt.(*addrCommentEntry)
|
||||||
wallet.addrCommentMap[e.pubKeyHash160] = &e.comment
|
w.addrCommentMap[e.pubKeyHash160] = &e.comment
|
||||||
case *txCommentEntry:
|
case *txCommentEntry:
|
||||||
e := wt.(*txCommentEntry)
|
e := wt.(*txCommentEntry)
|
||||||
wallet.txCommentMap[e.txHash] = &e.comment
|
w.txCommentMap[e.txHash] = &e.comment
|
||||||
default:
|
default:
|
||||||
return n, errors.New("Unknown appended entry")
|
return n, errors.New("Unknown appended entry")
|
||||||
}
|
}
|
||||||
|
@ -349,19 +499,82 @@ func (wallet *Wallet) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WriteTo serializes a Wallet and writes it to a io.Writer,
|
||||||
|
// returning the number of bytes written and any errors encountered.
|
||||||
|
func (w *Wallet) WriteTo(wtr io.Writer) (n int64, err error) {
|
||||||
|
wts := make([]io.WriterTo, len(w.addrMap)-1)
|
||||||
|
for hash, addr := range w.addrMap {
|
||||||
|
if addr.chainIndex != -1 { // ignore root address
|
||||||
|
e := &addrEntry{
|
||||||
|
pubKeyHash160: hash,
|
||||||
|
addr: *addr,
|
||||||
|
}
|
||||||
|
wts[addr.chainIndex] = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for hash, comment := range w.addrCommentMap {
|
||||||
|
e := &addrCommentEntry{
|
||||||
|
pubKeyHash160: hash,
|
||||||
|
comment: *comment,
|
||||||
|
}
|
||||||
|
wts = append(wts, e)
|
||||||
|
}
|
||||||
|
for hash, comment := range w.txCommentMap {
|
||||||
|
e := &txCommentEntry{
|
||||||
|
txHash: hash,
|
||||||
|
comment: *comment,
|
||||||
|
}
|
||||||
|
wts = append(wts, e)
|
||||||
|
}
|
||||||
|
appendedEntries := varEntries(wts)
|
||||||
|
|
||||||
|
// Iterate through each entry needing to be written. If data
|
||||||
|
// implements io.WriterTo, use its WriteTo func. Otherwise,
|
||||||
|
// data is a pointer to a fixed size value.
|
||||||
|
datas := []interface{}{
|
||||||
|
&fileID,
|
||||||
|
&w.version,
|
||||||
|
&w.net,
|
||||||
|
&w.flags,
|
||||||
|
&w.uniqID,
|
||||||
|
&w.createDate,
|
||||||
|
&w.name,
|
||||||
|
&w.desc,
|
||||||
|
&w.highestUsed,
|
||||||
|
&w.kdfParams,
|
||||||
|
make([]byte, 256),
|
||||||
|
&w.keyGenerator,
|
||||||
|
make([]byte, 1024),
|
||||||
|
&appendedEntries,
|
||||||
|
}
|
||||||
|
var written int64
|
||||||
|
for _, data := range datas {
|
||||||
|
if s, ok := data.(io.WriterTo); ok {
|
||||||
|
written, err = s.WriteTo(wtr)
|
||||||
|
} else {
|
||||||
|
written, err = binaryWrite(wtr, binary.LittleEndian, data)
|
||||||
|
}
|
||||||
|
n += written
|
||||||
|
if err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Unlock derives an AES key from passphrase and wallet's KDF
|
// Unlock derives an AES key from passphrase and wallet's KDF
|
||||||
// parameters and unlocks the root key of the wallet.
|
// parameters and unlocks the root key of the wallet.
|
||||||
func (wallet *Wallet) Unlock(passphrase []byte) error {
|
func (w *Wallet) Unlock(passphrase []byte) error {
|
||||||
key := Key(passphrase, wallet.kdfParams.salt[:],
|
key := Key(passphrase, &w.kdfParams)
|
||||||
wallet.kdfParams.mem, wallet.kdfParams.nIter)
|
|
||||||
|
|
||||||
// Attempt unlocking root address
|
// Attempt unlocking root address
|
||||||
if err := wallet.keyGenerator.unlock(key); err != nil {
|
if err := w.keyGenerator.unlock(key); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
wallet.key.Lock()
|
w.key.Lock()
|
||||||
wallet.key.secret = key
|
w.key.secret = key
|
||||||
wallet.key.Unlock()
|
w.key.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -369,42 +582,52 @@ func (wallet *Wallet) Unlock(passphrase []byte) error {
|
||||||
// Lock does a best effort to zero the keys.
|
// Lock does a best effort to zero the keys.
|
||||||
// Being go this might not succeed but try anway.
|
// Being go this might not succeed but try anway.
|
||||||
// TODO(jrick)
|
// TODO(jrick)
|
||||||
func (wallet *Wallet) Lock() (err error) {
|
func (w *Wallet) Lock() (err error) {
|
||||||
wallet.key.Lock()
|
// Remove clear text private keys from all entries.
|
||||||
if wallet.key.secret != nil {
|
for _, addr := range w.addrMap {
|
||||||
for i, _ := range wallet.key.secret {
|
addr.privKeyCT = nil
|
||||||
wallet.key.secret[i] = 0
|
|
||||||
}
|
}
|
||||||
wallet.key.secret = nil
|
|
||||||
|
w.key.Lock()
|
||||||
|
if w.key.secret != nil {
|
||||||
|
for i, _ := range w.key.secret {
|
||||||
|
w.key.secret[i] = 0
|
||||||
|
}
|
||||||
|
w.key.secret = nil
|
||||||
} else {
|
} else {
|
||||||
err = fmt.Errorf("Wallet already locked")
|
err = fmt.Errorf("Wallet already locked")
|
||||||
}
|
}
|
||||||
wallet.key.Unlock()
|
w.key.Unlock()
|
||||||
return err
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wallet *Wallet) IsLocked() (locked bool) {
|
// IsLocked returns whether a wallet is unlocked (in which case the
|
||||||
wallet.key.Lock()
|
// key is saved in memory), or locked.
|
||||||
locked = wallet.key.secret == nil
|
func (w *Wallet) IsLocked() (locked bool) {
|
||||||
wallet.key.Unlock()
|
w.key.Lock()
|
||||||
|
locked = w.key.secret == nil
|
||||||
|
w.key.Unlock()
|
||||||
return locked
|
return locked
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns wallet version as string and int.
|
// Returns wallet version as string and int.
|
||||||
// TODO(jrick)
|
// TODO(jrick)
|
||||||
func (wallet *Wallet) Version() (string, int) {
|
func (w *Wallet) Version() (string, int) {
|
||||||
return "", 0
|
return "", 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(jrick)
|
// NextUnusedAddress attempts to get the next chained address. It
|
||||||
func (wallet *Wallet) NextUnusedAddress() string {
|
// currently relies on pre-generated addresses and will return an empty
|
||||||
_ = wallet.lastChainIdx
|
// string if the address pool has run out. TODO(jrick)
|
||||||
wallet.highestUsed++
|
func (w *Wallet) NextUnusedAddress() string {
|
||||||
new160, err := wallet.addr160ForIdx(wallet.highestUsed)
|
_ = w.lastChainIdx
|
||||||
|
w.highestUsed++
|
||||||
|
new160, err := w.addr160ForIdx(w.highestUsed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
addr := wallet.addrMap[*new160]
|
addr := w.addrMap[*new160]
|
||||||
if addr != nil {
|
if addr != nil {
|
||||||
return btcutil.Base58Encode(addr.pubKeyHash[:])
|
return btcutil.Base58Encode(addr.pubKeyHash[:])
|
||||||
} else {
|
} else {
|
||||||
|
@ -412,39 +635,105 @@ func (wallet *Wallet) NextUnusedAddress() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wallet *Wallet) addr160ForIdx(idx int64) (*[ripemd160.Size]byte, error) {
|
func (w *Wallet) addr160ForIdx(idx int64) (*[ripemd160.Size]byte, error) {
|
||||||
if idx > wallet.lastChainIdx {
|
if idx > w.lastChainIdx {
|
||||||
return nil, errors.New("Chain index out of range")
|
return nil, errors.New("Chain index out of range")
|
||||||
}
|
}
|
||||||
return wallet.chainIdxMap[idx], nil
|
return w.chainIdxMap[idx], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wallet *Wallet) GetActiveAddresses() []string {
|
// GetActiveAddresses returns all wallet addresses that have been
|
||||||
|
// requested to be generated. These do not include pre-generated
|
||||||
|
// addresses.
|
||||||
|
func (w *Wallet) GetActiveAddresses() []string {
|
||||||
addrs := []string{}
|
addrs := []string{}
|
||||||
for i := int64(-1); i <= wallet.highestUsed; i++ {
|
for i := int64(-1); i <= w.highestUsed; i++ {
|
||||||
addr160, err := wallet.addr160ForIdx(i)
|
addr160, err := w.addr160ForIdx(i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return addrs
|
return addrs
|
||||||
}
|
}
|
||||||
addr := wallet.addrMap[*addr160]
|
addr := w.addrMap[*addr160]
|
||||||
addrs = append(addrs, btcutil.Base58Encode(addr.pubKeyHash[:]))
|
addrs = append(addrs, btcutil.Base58Encode(addr.pubKeyHash[:]))
|
||||||
}
|
}
|
||||||
return addrs
|
return addrs
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
type walletFlags struct {
|
||||||
func OpenWallet(file string) (*Wallet, error) {
|
useEncryption bool
|
||||||
|
watchingOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wf *walletFlags) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
|
raw := make([]byte, 8)
|
||||||
|
n, err = binaryRead(r, binary.LittleEndian, raw)
|
||||||
|
wf.useEncryption = raw[0] != 0
|
||||||
|
wf.watchingOnly = raw[1] != 0
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wf *walletFlags) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
raw := make([]byte, 8)
|
||||||
|
if wf.useEncryption {
|
||||||
|
raw[0] = 1
|
||||||
|
}
|
||||||
|
if wf.watchingOnly {
|
||||||
|
raw[1] = 1
|
||||||
|
}
|
||||||
|
return binaryWrite(w, binary.LittleEndian, raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
type addrFlags struct {
|
||||||
|
hasPrivKey bool
|
||||||
|
hasPubKey bool
|
||||||
|
encrypted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (af *addrFlags) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
|
var read int64
|
||||||
|
var b [8]byte
|
||||||
|
read, err = binaryRead(r, binary.LittleEndian, &b)
|
||||||
|
if err != nil {
|
||||||
|
return n + read, err
|
||||||
|
}
|
||||||
|
n += read
|
||||||
|
|
||||||
|
if b[0]&(1<<0) != 0 {
|
||||||
|
af.hasPrivKey = true
|
||||||
|
}
|
||||||
|
if b[0]&(1<<1) != 0 {
|
||||||
|
af.hasPubKey = true
|
||||||
|
}
|
||||||
|
if b[0]&(1<<2) == 0 {
|
||||||
|
return n, errors.New("Address flag specifies unencrypted address.")
|
||||||
|
}
|
||||||
|
af.encrypted = true
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (af *addrFlags) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
var b [8]byte
|
||||||
|
if af.hasPrivKey {
|
||||||
|
b[0] |= 1 << 0
|
||||||
|
}
|
||||||
|
if af.hasPubKey {
|
||||||
|
b[0] |= 1 << 1
|
||||||
|
}
|
||||||
|
if !af.encrypted {
|
||||||
|
// We only support encrypted privkeys.
|
||||||
|
return n, errors.New("Address must be encrypted.")
|
||||||
|
}
|
||||||
|
b[0] |= 1 << 2
|
||||||
|
|
||||||
|
return binaryWrite(w, binary.LittleEndian, b)
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
type btcAddress struct {
|
type btcAddress struct {
|
||||||
pubKeyHash [ripemd160.Size]byte
|
pubKeyHash [ripemd160.Size]byte
|
||||||
version uint32
|
flags addrFlags
|
||||||
flags uint64
|
|
||||||
chaincode [32]byte
|
chaincode [32]byte
|
||||||
chainIndex int64
|
chainIndex int64
|
||||||
chainDepth int64
|
chainDepth int64 // currently unused (will use when extending a locked wallet)
|
||||||
initVector [16]byte
|
initVector [16]byte
|
||||||
privKey [32]byte
|
privKey [32]byte
|
||||||
pubKey [65]byte
|
pubKey [65]byte
|
||||||
|
@ -452,7 +741,57 @@ type btcAddress struct {
|
||||||
lastSeen uint64
|
lastSeen uint64
|
||||||
firstBlock uint32
|
firstBlock uint32
|
||||||
lastBlock uint32
|
lastBlock uint32
|
||||||
privKeyCT []byte // Points to clear text private key if unlocked.
|
privKeyCT []byte // non-nil if unlocked.
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBtcAddress initializes and returns a new address. privkey must
|
||||||
|
// be 32 bytes. iv must be 16 bytes, or nil (in which case it is
|
||||||
|
// randomly generated).
|
||||||
|
func newBtcAddress(privkey, iv []byte) (addr *btcAddress, err error) {
|
||||||
|
if len(privkey) != 32 {
|
||||||
|
return nil, errors.New("Private key is not 32 bytes.")
|
||||||
|
}
|
||||||
|
if iv == nil {
|
||||||
|
iv = make([]byte, 16)
|
||||||
|
rand.Read(iv)
|
||||||
|
} else if len(iv) != 16 {
|
||||||
|
return nil, errors.New("Init vector must be nil or 16 bytes large.")
|
||||||
|
}
|
||||||
|
|
||||||
|
addr = &btcAddress{
|
||||||
|
privKeyCT: privkey,
|
||||||
|
flags: addrFlags{
|
||||||
|
hasPrivKey: true,
|
||||||
|
hasPubKey: true,
|
||||||
|
},
|
||||||
|
firstSeen: math.MaxUint64,
|
||||||
|
firstBlock: math.MaxUint32,
|
||||||
|
}
|
||||||
|
copy(addr.initVector[:], iv)
|
||||||
|
pub := pubkeyFromPrivkey(privkey)
|
||||||
|
copy(addr.pubKey[:], pub)
|
||||||
|
copy(addr.pubKeyHash[:], calcHash160(pub))
|
||||||
|
|
||||||
|
return addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newRootBtcAddress generates a new address, also setting the
|
||||||
|
// chaincode and chain index to represent this address as a root
|
||||||
|
// address.
|
||||||
|
func newRootBtcAddress(privKey, iv, chaincode []byte) (addr *btcAddress, err error) {
|
||||||
|
if len(chaincode) != 32 {
|
||||||
|
return nil, errors.New("Chaincode is not 32 bytes.")
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err = newBtcAddress(privKey, iv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(addr.chaincode[:], chaincode)
|
||||||
|
addr.chainIndex = -1
|
||||||
|
|
||||||
|
return addr, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFrom reads an encrypted address from an io.Reader.
|
// ReadFrom reads an encrypted address from an io.Reader.
|
||||||
|
@ -470,7 +809,7 @@ func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
datas := []interface{}{
|
datas := []interface{}{
|
||||||
&addr.pubKeyHash,
|
&addr.pubKeyHash,
|
||||||
&chkPubKeyHash,
|
&chkPubKeyHash,
|
||||||
&addr.version,
|
make([]byte, 4), // version
|
||||||
&addr.flags,
|
&addr.flags,
|
||||||
&addr.chaincode,
|
&addr.chaincode,
|
||||||
&chkChaincode,
|
&chkChaincode,
|
||||||
|
@ -488,7 +827,12 @@ func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
&addr.lastBlock,
|
&addr.lastBlock,
|
||||||
}
|
}
|
||||||
for _, data := range datas {
|
for _, data := range datas {
|
||||||
if read, err = binaryRead(r, binary.LittleEndian, data); err != nil {
|
if rf, ok := data.(io.ReaderFrom); ok {
|
||||||
|
read, err = rf.ReadFrom(r)
|
||||||
|
} else {
|
||||||
|
read, err = binaryRead(r, binary.LittleEndian, data)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
return n + read, err
|
return n + read, err
|
||||||
}
|
}
|
||||||
n += read
|
n += read
|
||||||
|
@ -511,8 +855,6 @@ func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(jrick) verify encryption
|
|
||||||
|
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -522,7 +864,7 @@ func (addr *btcAddress) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
datas := []interface{}{
|
datas := []interface{}{
|
||||||
&addr.pubKeyHash,
|
&addr.pubKeyHash,
|
||||||
walletHash(addr.pubKeyHash[:]),
|
walletHash(addr.pubKeyHash[:]),
|
||||||
&addr.version,
|
make([]byte, 4), //version
|
||||||
&addr.flags,
|
&addr.flags,
|
||||||
&addr.chaincode,
|
&addr.chaincode,
|
||||||
walletHash(addr.chaincode[:]),
|
walletHash(addr.chaincode[:]),
|
||||||
|
@ -540,7 +882,11 @@ func (addr *btcAddress) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
&addr.lastBlock,
|
&addr.lastBlock,
|
||||||
}
|
}
|
||||||
for _, data := range datas {
|
for _, data := range datas {
|
||||||
|
if wt, ok := data.(io.WriterTo); ok {
|
||||||
|
written, err = wt.WriteTo(w)
|
||||||
|
} else {
|
||||||
written, err = binaryWrite(w, binary.LittleEndian, data)
|
written, err = binaryWrite(w, binary.LittleEndian, data)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return n + written, err
|
return n + written, err
|
||||||
}
|
}
|
||||||
|
@ -549,25 +895,66 @@ func (addr *btcAddress) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (addr *btcAddress) unlock(key []byte) error {
|
// encrypt attempts to encrypt an address's clear text private key,
|
||||||
aesBlockDecrypter, err := aes.NewCipher([]byte(key))
|
// failing if the address is already encrypted or if the private key is
|
||||||
|
// not 32 bytes. If successful, the encryption flag is set.
|
||||||
|
func (a *btcAddress) encrypt(key []byte) error {
|
||||||
|
if a.flags.encrypted {
|
||||||
|
return errors.New("Address already encrypted.")
|
||||||
|
}
|
||||||
|
if len(a.privKeyCT) != 32 {
|
||||||
|
return errors.New("Invalid clear text private key.")
|
||||||
|
}
|
||||||
|
|
||||||
|
aesBlockEncrypter, err := aes.NewCipher(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
aesDecrypter := cipher.NewCFBDecrypter(aesBlockDecrypter, addr.initVector[:])
|
aesEncrypter := cipher.NewCFBEncrypter(aesBlockEncrypter, a.initVector[:])
|
||||||
ct := make([]byte, 32)
|
|
||||||
aesDecrypter.XORKeyStream(ct, addr.privKey[:])
|
|
||||||
addr.privKeyCT = ct
|
|
||||||
|
|
||||||
pubKey, err := btcec.ParsePubKey(addr.pubKey[:], btcec.S256())
|
aesEncrypter.XORKeyStream(a.privKey[:], a.privKeyCT)
|
||||||
|
|
||||||
|
a.flags.encrypted = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// lock removes the reference this address holds to its clear text
|
||||||
|
// private key. This function fails if the address is not encrypted.
|
||||||
|
func (a *btcAddress) lock() error {
|
||||||
|
if !a.flags.encrypted {
|
||||||
|
return errors.New("Unable to lock unencrypted address.")
|
||||||
|
}
|
||||||
|
|
||||||
|
a.privKeyCT = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unlock decrypts and stores a pointer to this address's private key,
|
||||||
|
// failing if the address is not encrypted, or the provided key is
|
||||||
|
// incorrect.
|
||||||
|
func (a *btcAddress) unlock(key []byte) error {
|
||||||
|
if !a.flags.encrypted {
|
||||||
|
return errors.New("Unable to unlock unencrypted address.")
|
||||||
|
}
|
||||||
|
|
||||||
|
aesBlockDecrypter, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
aesDecrypter := cipher.NewCFBDecrypter(aesBlockDecrypter, a.initVector[:])
|
||||||
|
ct := make([]byte, 32)
|
||||||
|
aesDecrypter.XORKeyStream(ct, a.privKey[:])
|
||||||
|
|
||||||
|
pubKey, err := btcec.ParsePubKey(a.pubKey[:], btcec.S256())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ParsePubKey faild:", err)
|
return fmt.Errorf("ParsePubKey faild:", err)
|
||||||
}
|
}
|
||||||
x, y := btcec.S256().ScalarBaseMult(addr.privKeyCT)
|
x, y := btcec.S256().ScalarBaseMult(ct)
|
||||||
if x.Cmp(pubKey.X) != 0 || y.Cmp(pubKey.Y) != 0 {
|
if x.Cmp(pubKey.X) != 0 || y.Cmp(pubKey.Y) != 0 {
|
||||||
return fmt.Errorf("decryption failed")
|
return errors.New("Decryption failed.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.privKeyCT = ct
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -576,16 +963,6 @@ func (addr *btcAddress) changeEncryptionKey(oldkey, newkey []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(jrick)
|
|
||||||
func (addr *btcAddress) verifyEncryptionKey() {
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(jrick)
|
|
||||||
func newRandomAddress(key []byte) *btcAddress {
|
|
||||||
addr := &btcAddress{}
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func walletHash(b []byte) uint32 {
|
func walletHash(b []byte) uint32 {
|
||||||
sum := btcwire.DoubleSha256(b)
|
sum := btcwire.DoubleSha256(b)
|
||||||
return binary.LittleEndian.Uint32(sum)
|
return binary.LittleEndian.Uint32(sum)
|
||||||
|
@ -605,6 +982,42 @@ type kdfParameters struct {
|
||||||
salt [32]byte
|
salt [32]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// computeKdfParameters returns best guess parameters to the
|
||||||
|
// memory-hard key derivation function to make the computation last
|
||||||
|
// targetSec seconds, while using no more than maxMem bytes of memory.
|
||||||
|
func computeKdfParameters(targetSec float64, maxMem uint64) *kdfParameters {
|
||||||
|
params := &kdfParameters{}
|
||||||
|
rand.Read(params.salt[:])
|
||||||
|
|
||||||
|
testKey := []byte("This is an example key to test KDF iteration speed")
|
||||||
|
|
||||||
|
memoryReqtBytes := uint64(1024)
|
||||||
|
approxSec := float64(0)
|
||||||
|
|
||||||
|
for approxSec <= targetSec/4 && memoryReqtBytes < maxMem {
|
||||||
|
memoryReqtBytes *= 2
|
||||||
|
before := time.Now()
|
||||||
|
_ = keyOneIter(testKey, params.salt[:], memoryReqtBytes)
|
||||||
|
approxSec = time.Since(before).Seconds()
|
||||||
|
}
|
||||||
|
|
||||||
|
allItersSec := float64(0)
|
||||||
|
nIter := uint32(1)
|
||||||
|
for allItersSec < 0.02 { // This is a magic number straight from armory's source.
|
||||||
|
nIter *= 2
|
||||||
|
before := time.Now()
|
||||||
|
for i := uint32(0); i < nIter; i++ {
|
||||||
|
_ = keyOneIter(testKey, params.salt[:], memoryReqtBytes)
|
||||||
|
}
|
||||||
|
allItersSec = time.Since(before).Seconds()
|
||||||
|
}
|
||||||
|
|
||||||
|
params.mem = memoryReqtBytes
|
||||||
|
params.nIter = nIter
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
func (params *kdfParameters) WriteTo(w io.Writer) (n int64, err error) {
|
func (params *kdfParameters) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
var written int64
|
var written int64
|
||||||
|
|
||||||
|
@ -657,7 +1070,7 @@ func (params *kdfParameters) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
return n, err
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write params
|
// Read params
|
||||||
buf := bytes.NewBuffer(chkedBytes)
|
buf := bytes.NewBuffer(chkedBytes)
|
||||||
datas = []interface{}{
|
datas = []interface{}{
|
||||||
¶ms.mem,
|
¶ms.mem,
|
||||||
|
@ -695,7 +1108,8 @@ func (e *addrEntry) WriteTo(w io.Writer) (n int64, err error) {
|
||||||
|
|
||||||
// Write btcAddress
|
// Write btcAddress
|
||||||
written, err = e.addr.WriteTo(w)
|
written, err = e.addr.WriteTo(w)
|
||||||
return n + written, err
|
n += written
|
||||||
|
return n, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *addrEntry) ReadFrom(r io.Reader) (n int64, err error) {
|
func (e *addrEntry) ReadFrom(r io.Reader) (n int64, err error) {
|
||||||
|
|
|
@ -17,16 +17,33 @@
|
||||||
package wallet
|
package wallet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"crypto/rand"
|
||||||
"encoding/binary"
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ = spew.Dump
|
||||||
|
|
||||||
func TestBtcAddressSerializer(t *testing.T) {
|
func TestBtcAddressSerializer(t *testing.T) {
|
||||||
var addr = btcAddress{
|
kdfp := &kdfParameters{
|
||||||
pubKeyHash: [20]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19},
|
mem: 1024,
|
||||||
|
nIter: 5,
|
||||||
|
}
|
||||||
|
rand.Read(kdfp.salt[:])
|
||||||
|
key := Key([]byte("banana"), kdfp)
|
||||||
|
privKey := make([]byte, 32)
|
||||||
|
rand.Read(privKey)
|
||||||
|
addr, err := newBtcAddress(privKey, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = addr.encrypt(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Create("btcaddress.bin")
|
file, err := os.Create("btcaddress.bin")
|
||||||
|
@ -46,15 +63,63 @@ func TestBtcAddressSerializer(t *testing.T) {
|
||||||
var readAddr btcAddress
|
var readAddr btcAddress
|
||||||
_, err = readAddr.ReadFrom(file)
|
_, err = readAddr.ReadFrom(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
spew.Dump(&readAddr)
|
|
||||||
t.Error(err.Error())
|
t.Error(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
buf1, buf2 := new(bytes.Buffer), new(bytes.Buffer)
|
if err = readAddr.unlock(key); err != nil {
|
||||||
binary.Write(buf1, binary.LittleEndian, addr)
|
t.Error(err.Error())
|
||||||
binary.Write(buf2, binary.LittleEndian, readAddr)
|
return
|
||||||
if !bytes.Equal(buf1.Bytes(), buf2.Bytes()) {
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(addr, &readAddr) {
|
||||||
t.Error("Original and read btcAddress differ.")
|
t.Error("Original and read btcAddress differ.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWalletCreationSerialization(t *testing.T) {
|
||||||
|
w1, err := NewWallet("banana wallet", "A wallet for testing.", []byte("banana"))
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Error creating new wallet: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create("newwallet.bin")
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if _, err := w1.WriteTo(file); err != nil {
|
||||||
|
t.Error("Error writing new wallet: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Seek(0, 0)
|
||||||
|
|
||||||
|
w2 := new(Wallet)
|
||||||
|
_, err = w2.ReadFrom(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("Error reading newly written wallet: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w1.Lock()
|
||||||
|
w2.Lock()
|
||||||
|
|
||||||
|
if err = w1.Unlock([]byte("banana")); err != nil {
|
||||||
|
t.Error("Decrypting original wallet failed: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = w2.Unlock([]byte("banana")); err != nil {
|
||||||
|
t.Error("Decrypting newly read wallet failed: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(w1, w2) {
|
||||||
|
t.Error("Created and read-in wallets do not match.")
|
||||||
|
spew.Dump(w1, w2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue